diff --git a/.github/workflows/latest-bitcoind.yml b/.github/workflows/latest-bitcoind.yml index 3ad462e1f1..711a904e92 100644 --- a/.github/workflows/latest-bitcoind.yml +++ b/.github/workflows/latest-bitcoind.yml @@ -1,10 +1,10 @@ name: Latest Bitcoin Core on: - workflow_dispatch: # Build can be triggered manually from github.com - schedule: - # Run at midnight on Sunday and Wednesday. - - cron: '0 0 * * 0,3' + push: + branches: [ master ] + pull_request: + branches: [ master ] permissions: contents: read @@ -18,11 +18,12 @@ jobs: - name: Checkout bitcoind master uses: actions/checkout@v3 with: - repository: bitcoin/bitcoin + repository: furszy/bitcoin-core + ref: 2025_threadpool_http_server path: bitcoin - name: Install bitcoind dependencies - run: sudo apt-get install build-essential cmake pkg-config bsdmainutils python3 libevent-dev libboost-dev libminiupnpc-dev libnatpmp-dev libzmq3-dev libsqlite3-dev systemtap-sdt-dev + run: sudo apt-get install build-essential cmake pkg-config bsdmainutils python3 libevent-dev libboost-dev libminiupnpc-dev libnatpmp-dev libzmq3-dev libsqlite3-dev systemtap-sdt-dev libcapnp-dev capnproto working-directory: ./bitcoin - name: Init and configure cmake build @@ -48,5 +49,5 @@ jobs: run: echo "fs.file-max = 1024000" | sudo tee -a /etc/sysctl.conf - name: Run eclair tests - run: BITCOIND_DIR=$GITHUB_WORKSPACE/bitcoin/build/src ./mvnw test + run: BITCOIND_DIR=$GITHUB_WORKSPACE/bitcoin/build/bin ./mvnw test working-directory: ./eclair diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 6daff0cf58..6b5598aa86 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,10 +1,6 @@ name: Build & Test on: - push: - branches: [ master ] - pull_request: - branches: [ master ] jobs: diff --git a/.mvn/checksums/checksums-central.sha256 b/.mvn/checksums/checksums-central.sha256 index bfed5a8a66..89bea518fe 100644 --- a/.mvn/checksums/checksums-central.sha256 +++ b/.mvn/checksums/checksums-central.sha256 @@ -3,7 +3,6 @@ 000d8187952b223702fde296df799563f35de82ce72adb4e7bf157342378fbe3 org/apache/commons/commons-parent/54/commons-parent-54.pom 001cde5b3c6ba91070425cfe9f2e695e4aeb8bc290a2d4cd96531127ab244fe5 org/slf4j/slf4j-api/1.7.32/slf4j-api-1.7.32.pom 002dd542501d89246aeb5b482d5bcbdcdb789cb6a36a22704d3524192f63ff09 org/apache/maven/surefire/surefire-api/3.1.2/surefire-api-3.1.2.pom -00506d89fa1688adff0c322464af4a581b545343f9588e661d745bf097eb8642 fr/acinq/bitcoin/bitcoin-kmp-jvm/0.22.1/bitcoin-kmp-jvm-0.22.1.jar 0076c249b4387d8369146528fd5dacb3efba098dc02ecf9ac81debdfc2e12fd5 com/typesafe/config/1.4.2/config-1.4.2.jar 0124227bc47efc9a00b9aa4fc3ef7f70823d322213c26489e5369a914339c84a org/codehaus/plexus/plexus-component-annotations/1.5.4/plexus-component-annotations-1.5.4.pom 012acc11ee3c243649b1a54bac3f9448ed6e396aac6cc7d4364492e5aab88992 com/typesafe/akka/akka-http_2.13/10.2.7/akka-http_2.13-10.2.7.jar @@ -68,7 +67,6 @@ 0f4f5fb28609a4d2b38b7f7128be7cf9b541f25283d71b4e56066d99683aafff com/google/inject/guice/4.2.2/guice-4.2.2-no_aop.jar 0f8e64feaa068b8a02de45e9343f9b2fc401235b81e7fe3f80b2aeba001691b2 com/github/luben/zstd-jni/1.5.2-5/zstd-jni-1.5.2-5.jar 0fa7dd96ff039af75db12bca1d9af5529df55e80b16ce86ecc8cf38a13699c9d org/apache/maven/maven-project/2.0/maven-project-2.0.jar -0fd4adec795881112d1afe957cd53915992ad66f8b1555323a13baff94b573e9 fr/acinq/secp256k1/secp256k1-kmp-jni-jvm-linux/0.17.1/secp256k1-kmp-jni-jvm-linux-0.17.1.jar 0ffa0ad084ebff5712540a7b7ea0abda487c53d3a18f78c98d1a3675dab9bf61 org/codehaus/plexus/plexus-utils/3.1.0/plexus-utils-3.1.0.jar 109edd22a65676a023c73fde368d89ea2021b1b99f84fc9de478743dd1ae436a org/scalatest/scalatest-funspec_2.13/3.2.16/scalatest-funspec_2.13-3.2.16.pom 11067f6a75fded12bcdc8daf7a66ddd942ce289c3daf88a3fe0f8b12858a2ee6 org/codehaus/plexus/plexus-utils/3.0.24/plexus-utils-3.0.24.pom @@ -119,7 +117,6 @@ 18e822e03b73cacd096584d5db7cbff9118d5e0e2928863b6ecc5fa975dc2e19 org/scala-sbt/zinc-persist-core-assembly/1.8.0/zinc-persist-core-assembly-1.8.0.pom 191acabbce3e466008fe4274d09793603eb0083e3b36c692feb958aa8b067821 com/typesafe/akka/akka-cluster-tools_2.13/2.6.20/akka-cluster-tools_2.13-2.6.20.jar 1933a6037439b389bda2feaccfc0113880fd8d88f7d240d2052b91108dd5ae89 org/apache/apache/5/apache-5.pom -19536b6e970fd3074ec0ebb52bf00664ba0f1a49af8f15645e80e859869f84e7 fr/acinq/secp256k1/secp256k1-kmp-jni-common/0.17.1/secp256k1-kmp-jni-common-0.17.1.jar 1955432f6d8af32806d82d8218fedfe4be3bb3e23d59a74d81449ffc74bf7bd1 com/fasterxml/jackson/jackson-bom/2.12.7/jackson-bom-2.12.7.pom 19889dbdf1b254b2601a5ee645b8147a974644882297684c798afe5d63d78dfe com/google/code/findbugs/jsr305/3.0.2/jsr305-3.0.2.pom 1a31736abe1527968fb72d60c371acfa7e131985bf18cf5dc4f0497b8dee14e8 org/scala-sbt/zinc-classpath_2.13/1.8.0/zinc-classpath_2.13-1.8.0.pom @@ -157,7 +154,6 @@ 1ff4fb95c218af4a46f71d625212c70f377ccf97ad2e26cb8d4c10709265bf62 org/codehaus/plexus/plexus-utils/1.5.8/plexus-utils-1.5.8.pom 2068320bd6bad744c3673ab048f67e30bef8f518996fa380033556600669905d org/codehaus/mojo/animal-sniffer-annotations/1.14/animal-sniffer-annotations-1.14.jar 20a643c46de6bc30cc81fa3b9e3bb1697afa37168cc183745497b704969bbb92 software/amazon/ion/ion-java/1.0.2/ion-java-1.0.2.pom -20b886313fb59260ebd1eda96dce4c5c4c8f8da4ededcaaab3f853e295bcddec fr/acinq/secp256k1/secp256k1-kmp-jni-jvm-linux/0.17.1/secp256k1-kmp-jni-jvm-linux-0.17.1.pom 211b306cfc44f8f96df3a0a3ddaf75ba8c5289eed77d60d72f889bb855f535e5 org/tukaani/xz/1.9/xz-1.9.jar 214677844a23cf33f09f58ecd29dec08dd005ffa4e20381b4b5c56059236a237 org/apache/maven/shared/maven-common-artifact-filters/3.3.2/maven-common-artifact-filters-3.3.2.pom 214abd2300180923c113b62e196d664e0f35639973e82e055e5a645cd3d72c27 org/scalatest/scalatest-propspec_2.13/3.2.16/scalatest-propspec_2.13-3.2.16.pom @@ -166,7 +162,6 @@ 22339626db9f6c7c82481345556d97765073691e5eb7cfe90171120e71056a04 io/netty/netty-codec-haproxy/4.1.94.Final/netty-codec-haproxy-4.1.94.Final.jar 224fe4d0c650f085c012f0a03c1995c598c7b5c506bc5350b727c75874330f00 org/codehaus/plexus/plexus-classworlds/1.2-alpha-9/plexus-classworlds-1.2-alpha-9.pom 22e81ee9d5349ba0e897adb5c7fcec25656c9e0b7b86c3e93e9a7af360481c0d org/scala-lang/scala-library/2.12.8/scala-library-2.12.8.pom -23138489ee293a68197068877745f700bfa11af989187bed3c3074cd8c079778 org/jetbrains/kotlin/kotlin-stdlib-jdk7/1.5.31/kotlin-stdlib-jdk7-1.5.31.pom 2340855d40ce6125d9a23ab80d94848efa50b2957cf93531e2a7dcf631b4f22b org/apache/maven/maven-settings/3.0/maven-settings-3.0.pom 240113a2f22fd1f0b182b32baecf0e7876b3a8e41f3c4da3335eeb9ffb24b9f4 org/sonatype/sisu/sisu-guice/2.1.7/sisu-guice-2.1.7-noaop.jar 2431faf4c35b658b2e98f2ea4e10f5e7bd95d11bbb75338856088fe1099c14fb org/apache/maven/maven-resolver-provider/3.8.6/maven-resolver-provider-3.8.6.pom @@ -212,6 +207,7 @@ 2b6461b313e56918416a773206ff7d64cf101a662cb03a1428f05f9dabe4b1ea io/netty/netty-transport/4.1.94.Final/netty-transport-4.1.94.Final.pom 2be8b810cf0937ff4bb7bef8ce78a8faad17ca2182751055ac7df54d5510b908 org/apache/maven/shared/maven-common-artifact-filters/3.3.2/maven-common-artifact-filters-3.3.2.jar 2bf4e59f3acd106fea6145a9a88fe8956509f8b9c0fdd11eb96fee757269e3f3 classworlds/classworlds/1.1-alpha-2/classworlds-1.1-alpha-2.jar +2c09293dbd05c38c5cc59ea6ee7c8105110eb89d758d8299bb428a78cfa8f867 fr/acinq/bitcoin/bitcoin-kmp-jvm/0.28.0/bitcoin-kmp-jvm-0.28.0.pom 2c317f6041219f16f34bd47ea7618e57552d4f1f707378701b9efed4adecf70a org/apache/maven/maven-plugin-api/3.8.6/maven-plugin-api-3.8.6.jar 2c3ba62ee26ffc694cfb2fc6132e4ccd7b4acf553aa86d9143dc0f77822f68ec org/apache/maven/maven-plugin-api/3.2.5/maven-plugin-api-3.2.5.pom 2c9071d7d1522b0c762057c34e8b9bfae39927a0eb755c36aa76519721870d7b com/google/guava/guava/32.1.1-jre/guava-32.1.1-jre.pom @@ -228,10 +224,9 @@ 2e8cb2d546a01c2259cb17f1e06732db3d14b079d19622bf8400c82cb1ee6b96 org/apache/maven/shared/file-management/3.1.0/file-management-3.1.0.jar 2eb0ea667bc489384478231dda7516407d4b5b22a138077229871de9362a7ae2 org/apache/maven/resolver/maven-resolver-util/1.9.18/maven-resolver-util-1.9.18.jar 2ed07d65845131f5336a86476c9a4056b59d0b58b9815ab3679bb0f36f35f705 org/junit/junit-bom/5.9.2/junit-bom-5.9.2.pom -2fc474115f22031ae8fe377701e7116ed46898e9d10b347ee552770d7f338fb1 fr/acinq/bitcoin/bitcoin-kmp-jvm/0.22.1/bitcoin-kmp-jvm-0.22.1.pom +30ad98b52f64377b3353c6f31289f15fce2217f01f5e3a4862305da6c78b6a71 fr/acinq/secp256k1/secp256k1-kmp-jni-jvm-extract/0.21.0/secp256k1-kmp-jni-jvm-extract-0.21.0.pom 30e8e86a673564df28b14507c7943238fcedbb87b71dc7ceee1f676c5315c1eb org/mockito/mockito-core/4.3.1/mockito-core-4.3.1.pom 30f5789efa39ddbf96095aada3fc1260c4561faf2f714686717cb2dc5049475a net/java/jvnet-parent/3/jvnet-parent-3.pom -31189762fb88f45f0586d93597a2ee6efc3cc50333d85d7446e5ebab2aa9d124 fr/acinq/secp256k1/secp256k1-kmp-jvm/0.17.1/secp256k1-kmp-jvm-0.17.1.jar 317d515c5c69278a980c6219901d327efb4210ca44ae1f07c2a304d487ec7cae org/apache/maven/maven-aether-provider/3.2.5/maven-aether-provider-3.2.5.pom 321ddbb7ee6fe4f53dea6b4cd6db74154d6bfa42391c1f763b361b9f485acf05 org/ow2/ow2/1.5.1/ow2-1.5.1.pom 322d96e5277de60c00dd4833c19a6e8c6f148aa61bafc7debd3d962beb55d847 org/scalatest/scalatest-mustmatchers_2.13/3.2.16/scalatest-mustmatchers_2.13-3.2.16.pom @@ -242,7 +237,6 @@ 3379989fc569011ac7010faf858438e3d7ff8da8682c4e9b79c4761c1d36c254 io/kamon/kamon-apm-reporter_2.13/2.7.4/kamon-apm-reporter_2.13-2.7.4.jar 339a066e988e770e038933484446ed6a6eac7c341d9ef476e9548a16f94fc886 io/netty/netty-transport-classes-kqueue/4.1.94.Final/netty-transport-classes-kqueue-4.1.94.Final.jar 33baa364c6d77f5bcce4dbe216f8523a68c522c2f742a16fbd114b38df7a8fe4 com/github/jnr/jffi/1.2.18/jffi-1.2.18-native.jar -33d148db0e11debd0d90677d28242bced907f9c77730000fd597867089039d86 org/jetbrains/kotlin/kotlin-stdlib-jdk7/1.8.21/kotlin-stdlib-jdk7-1.8.21.jar 33fac5d689f8aed46324072d72cf6a4552c3ca9ac899a41b4019482a9d406c3a org/codehaus/plexus/plexus-utils/1.5.7/plexus-utils-1.5.7.pom 34259ad87457b81158c9cc62a98f5f836d3c085bd11915309e6c97c54587207f org/scala-sbt/util-interface/1.8.0/util-interface-1.8.0.pom 347a7a9400f9964e87c91d3980e48eebdc8d024bc3b36f7f22189c662853a51c org/ow2/asm/asm-tree/5.0.3/asm-tree-5.0.3.jar @@ -269,7 +263,6 @@ 380920c8640425bede10d3e6a66d6e85e21409dff45380a17ab56de38d300632 io/netty/netty-transport-classes-kqueue/4.1.94.Final/netty-transport-classes-kqueue-4.1.94.Final.pom 380c65c3223afa9cebe23874d49194d4c864d37a5696e9e9b29c189857afd57c io/kamon/kamon-akka_2.13/2.7.4/kamon-akka_2.13-2.7.4.jar 38246291439393fd08f54c6d7fedde2db0fd5c94d0910f17b99e8d59a2858e98 org/apache/maven/doxia/doxia/1.0/doxia-1.0.pom -3839d728d7c309a5c368b0270b44b4f1c7878ff5ca5d32a9a204faa3491459d8 org/jetbrains/kotlin/kotlin-stdlib-jdk8/1.8.21/kotlin-stdlib-jdk8-1.8.21.pom 386b2ca1f0b50aaffc840b486d1b53ca32c3c324e717e14cc8d86b3990a3ff14 io/netty/netty-transport-sctp/4.1.94.Final/netty-transport-sctp-4.1.94.Final.pom 38a154d751dabff3f02c038b6850b197ca861f9a553b9064a30a327fd8928c6d org/checkerframework/checker-compat-qual/2.0.0/checker-compat-qual-2.0.0.pom 38ac8193ee672763dc2293de52ba5816b921dc685f52d894f7e683a920408a90 com/softwaremill/sttp/client3/core_2.13/3.8.16/core_2.13-3.8.16.pom @@ -283,7 +276,6 @@ 39f3675b910e6e9b93825f8284bec9f4ad3044cd20a6f7c8ff9e2f8695ebf21e com/github/jnr/jnr-x86asm/1.0.2/jnr-x86asm-1.0.2.jar 39f4cb3e190e19191dde0d63f116e1aeb215c726a5f919f6cb17b82f79d74381 org/scala-lang/modules/scala-collection-contrib_2.13/0.2.1/scala-collection-contrib_2.13-0.2.1.pom 3a2e69d06d641d1f3b293126dc9e2e4ea6563bf8c36c87e0ab6fa4292d04b79c org/apache/commons/commons-parent/34/commons-parent-34.pom -3aa956885af558aec0b582aa497d059b0db09439f200d7c6bf965b9653175bbf fr/acinq/secp256k1/secp256k1-kmp-jvm/0.17.1/secp256k1-kmp-jvm-0.17.1.pom 3b0559bb8432f28937efe6ca193ef54a8506d0075d73fd7406b9b116c6a11063 org/sonatype/plexus/plexus-sec-dispatcher/1.3/plexus-sec-dispatcher-1.3.jar 3b1a46b4bc26a0176acaf99312ff2f3a631faf3224b0996af546aa48bd73c647 org/apache/maven/maven-settings/3.0/maven-settings-3.0.jar 3b93391acabb4274bd5ec73793ba8b80a441210f6f547bccff2bb6e82559d63e org/scalatest/scalatest-mustmatchers_2.13/3.2.16/scalatest-mustmatchers_2.13-3.2.16.jar @@ -295,7 +287,7 @@ 3d3d9753f36e88039e63f8757bfe643830d69443e168c4ecdaf5f47a2c1d94ce org/apache/maven/maven-builder-support/3.8.6/maven-builder-support-3.8.6.jar 3d6fdeb72b2967f1fa2784134fb832d08d8d6e879b7ace7712f2c7281994fc1e org/apache/maven/maven-model/3.0/maven-model-3.0.pom 3d8b64316e38439de6e0f14131e6d7d58d74e924f03d313b3cd8292a966aa010 com/fasterxml/jackson/core/jackson-core/2.12.6/jackson-core-2.12.6.pom -3db752a30074f06ee6c57984aa6f27da44f4d2bbc7f5442651f6988f1cb2b7d7 org/jetbrains/kotlin/kotlin-stdlib-jdk8/1.8.21/kotlin-stdlib-jdk8-1.8.21.jar +3de1342b450f137ba2bdf5e93f15f6fa524404a7e3fcb36122a1bb968f3bab7a fr/acinq/secp256k1/secp256k1-kmp-jni-jvm-darwin/0.21.0/secp256k1-kmp-jni-jvm-darwin-0.21.0.pom 3e395d6fbc43c09a3774cac8694ce527398305ea3fd5492d80e25af27d382a9c org/codehaus/mojo/mojo-parent/34/mojo-parent-34.pom 3e48e181d5efa33edb06aaa14b9af5ed2a7582e87963f87602cfdad8fa659bb4 org/codehaus/plexus/plexus-containers/1.0-alpha-20/plexus-containers-1.0-alpha-20.pom 3e49037174820bbd0df63420a977255886398954c2a06291fa61f727ac35b377 org/apache/apache/29/apache-29.pom @@ -326,7 +318,6 @@ 445aa12de102053be827cf70f283f26e79e2eef8807a709b388dfc1b5e542ad6 org/scoverage/scalac-scoverage-runtime_2.13/1.4.1/scalac-scoverage-runtime_2.13-1.4.1.jar 44b85e2f8035a52ec993a20816e8c6d7ae74fee288b6651f4b7447fa2b022d18 org/scala-sbt/collections_2.13/1.8.0/collections_2.13-1.8.0.pom 44ec063ad46df37a3a5e7b25ed11e116f858bc581888c371dea8e85ad5b211dc org/scala-lang/modules/scala-xml_2.12/1.2.0/scala-xml_2.12-1.2.0.pom -45110aab0074792b815807b02b2daf18d2b37e8747ac0692aad7a0d02da893df org/jetbrains/kotlin/kotlin-stdlib-jdk8/1.5.31/kotlin-stdlib-jdk8-1.5.31.pom 454381d9535918f78b4024a9655fba4b3e522312bcf78c263cf8c6dda873c604 org/mockito/mockito-scala-scalatest_2.13/1.17.5/mockito-scala-scalatest_2.13-1.17.5.pom 45a8e898eb668337aea6caeee2ca53be0efe9af631554bd69a781542762cb2be io/netty/netty-all/4.1.94.Final/netty-all-4.1.94.Final.pom 468ddd2df93670b14b2258a3da80a9e2b49205f199d4a6185a12907207114655 org/apache/maven/surefire/surefire-booter/3.1.2/surefire-booter-3.1.2.pom @@ -338,11 +329,9 @@ 481803338ffc3507a7c19f1172e0be6b677e07445f310040ea8dff5d82efa5c4 io/netty/netty-transport-native-kqueue/4.1.94.Final/netty-transport-native-kqueue-4.1.94.Final-osx-aarch_64.jar 482811ea38f339d2b0867a7852a195b56e329fdb37066b2f3035a209c111a2bc org/apache/maven/shared/maven-dependency-tree/3.2.1/maven-dependency-tree-3.2.1.jar 483751e48fb2d1f43c61ad3ab00ffa466edc0333d8fc2fdb5a26e95d32982394 org/ow2/asm/asm/9.4/asm-9.4.pom -4847f28f95c216c1f8ff0a27ee11847333a4a21f5fa644450eb274a2f8e32d1f fr/acinq/secp256k1/secp256k1-kmp-jni-jvm-mingw/0.17.1/secp256k1-kmp-jni-jvm-mingw-0.17.1.jar 487ef4984d70158f58c37ce8766931ba4c7e57375865884b756391bfd6d00c39 org/slf4j/slf4j-bom/2.0.12/slf4j-bom-2.0.12.pom 488c2a42351a2e9cba24aa3d7843eedb3fbb13957469563f2d75e959f848454c org/apache/maven/wrapper/maven-wrapper-parent/3.3.2/maven-wrapper-parent-3.3.2.pom 48dc26f27f51520a5e8b6b9911901634fef425059b74505555d49584ace9b809 io/netty/netty-codec-http2/4.1.94.Final/netty-codec-http2-4.1.94.Final.pom -4922121d3f0bc60ce407f372de42d02a0b7e94e75d0c3d1508b6835721f249ef org/jetbrains/kotlin/kotlin-stdlib/2.1.10/kotlin-stdlib-2.1.10.pom 494f50adb7e0962753ff06949f052adf369453f688428f2a7657d7e81df136a5 io/netty/netty-resolver-dns-classes-macos/4.1.94.Final/netty-resolver-dns-classes-macos-4.1.94.Final.jar 498949e5576b022559d1622e534c18e052f94dec883924b67e0a4e8676c07b17 org/apache/maven/reporting/maven-reporting-api/3.0/maven-reporting-api-3.0.jar 49a9898da3758659388f1333c53ccadb6fbd142a7d18aa7b1a33577090684279 org/jheaps/jheaps/0.14/jheaps-0.14.jar @@ -430,15 +419,13 @@ 5c31b96ad7c7c52a9bc9c6e8ee9ae963575819accfd6f99133ddaab8ec8e6780 io/netty/netty-handler-ssl-ocsp/4.1.94.Final/netty-handler-ssl-ocsp-4.1.94.Final.pom 5c856fefc046a88de0118ac5e45cddf638975fa980c007d242633276f7266f02 org/scala-lang/modules/scala-parser-combinators_2.13/1.1.2/scala-parser-combinators_2.13-1.1.2.pom 5c8b507a80901fcdaef89f50c639176b516e8866c6bf07be1ab8ab9da5a4877f org/eclipse/aether/aether-util/1.0.0.v20140518/aether-util-1.0.0.v20140518.pom -5c99dbce74deece99df8e65fb19e7740798c4d10b71eae8a48d1e83272ec84a9 fr/acinq/bitcoin-lib_2.13/0.36/bitcoin-lib_2.13-0.36.pom 5ca374eb4e6194ec0cd7004366decd39d4d048145a6380f99741a9414f38cebb org/apache/maven/maven-model-builder/3.8.6/maven-model-builder-3.8.6.jar 5cb1e9f9cf0be011487545694ff0a178237c6bfcbb21c97865cdc52c60b9347a com/jcraft/jzlib/1.1.1/jzlib-1.1.1.jar 5cecc8bcb58d45e2a57ccccaad5d520abf0e81dea2be56ec1ed38c182cff4bd0 io/kamon/kamon-apm-reporter_2.13/2.7.4/kamon-apm-reporter_2.13-2.7.4.pom 5e583878df905b5f33a230ef690a52b8f19dab9cc892bedee069f3d8af4e960a org/codehaus/plexus/plexus-utils/3.3.1/plexus-utils-3.3.1.pom 5e93a63b3042023e558dcbeb19914ce82e6a6fbccdbd68797f80b135121a8bde org/apache/maven/shared/file-management/3.1.0/file-management-3.1.0.pom -5f2ac1ca8dc8b37a3f4314e716d36969ebf0227a75181d32699d0a8f645b1c21 org/jetbrains/kotlin/kotlin-stdlib/2.1.10/kotlin-stdlib-2.1.10.jar 5f32ecab9c8f6dff1f9e41b853e40d8f23a2508219154ef67cc00d4556f5f5dd org/eclipse/sisu/sisu-inject/0.3.5/sisu-inject-0.3.5.pom -5f4b94dd3065a7764c37fa15de2ad6d81f40d59f8cb33f17d181c6384fb7a72e org/jetbrains/kotlin/kotlin-stdlib-jdk8/1.9.10/kotlin-stdlib-jdk8-1.9.10.pom +5f6af6dd4b828a07b0a1fed9fd21ac3a07f39cbe7e9c07d6380ca35171df3e56 fr/acinq/secp256k1/secp256k1-kmp-jni-jvm/0.21.0/secp256k1-kmp-jni-jvm-0.21.0.pom 5fe611b07793d5ff3378ca3ae2c75e38bae08e73c3ae0acdf116ae5d6978d19f org/codehaus/plexus/plexus-container-default/1.0-alpha-20/plexus-container-default-1.0-alpha-20.pom 6047c86dae48672243662c26074731be6328edcc170a366807d56f57ce2a1965 org/apache/maven/doxia/doxia-module-xdoc/1.0/doxia-module-xdoc-1.0.pom 606b5fa03b171d8204aac0fbace11ee28e71175a0f869bd45f09c9319e7e88dc org/eclipse/aether/aether/1.0.0.v20140518/aether-1.0.0.v20140518.pom @@ -446,7 +433,6 @@ 61988e54486a5dc38f06c70fdae5b108556c63bd433697b9f4305fcdb30fa40e org/apache/maven/shared/maven-shared-incremental/1.1/maven-shared-incremental-1.1.jar 61b49d85c73476d990fe289d7024ddc48739273777f5b6a15ac80ff9b7790a3b io/netty/netty-codec-dns/4.1.94.Final/netty-codec-dns-4.1.94.Final.pom 61b4d7c515a0894ffad925fd7052620c1425a86433fd35113b5fab0de890a57f com/squareup/okio/okio-jvm/3.6.0/okio-jvm-3.6.0.pom -61db33bdf92609cc096453b9fd70e97eccb55b6cdb4f140a2941e706053e6583 fr/acinq/secp256k1/secp256k1-kmp-jni-jvm-mingw/0.17.1/secp256k1-kmp-jni-jvm-mingw-0.17.1.pom 62aa1c1679ade09f173dbf1b6c32d35c55e75800238fd5e48f13aa4337c0875b com/fasterxml/jackson/jackson-parent/2.12/jackson-parent-2.12.pom 62aee8cd05e9e0e41dbdb22a82d2569af1ee137978527a1ffef5394a9114c6e3 io/netty/netty-handler/4.1.94.Final/netty-handler-4.1.94.Final.pom 62bd7a7198193c5373a992c122990279e413af3307162472a5d3cbb8ecadb35e org/scala-lang/scala-reflect/2.13.10/scala-reflect-2.13.10.jar @@ -457,6 +443,7 @@ 6498a4b122c6d78207a63225407f971b8174be229c0221faf3975720cd357c35 com/hierynomus/asn-one/0.5.0/asn-one-0.5.0.jar 64d0edb5f21cfff600b1c3ab7d45f9754cd18ba5fbf83b3d1bb7c4849437d8e3 org/apache/maven/shared/maven-shared-components/34/maven-shared-components-34.pom 65819ca7c5a218ff49f8ebe72f3b39219c81f137d017acc1aec15b1054cf5236 org/ow2/asm/asm-tree/5.0.3/asm-tree-5.0.3.pom +65d12d85a3b865c160db9147851712a64b10dadd68b22eea22a95bf8a8670dca org/jetbrains/kotlin/kotlin-stdlib/2.2.0/kotlin-stdlib-2.2.0.jar 65f383685c8aa802b4d1b5735e13ac86137ceadd141d76cfe854323b3329a45d org/scalatest/scalatest-matchers-core_2.13/3.2.16/scalatest-matchers-core_2.13-3.2.16.pom 6627123a5e0c5ea2bc977b732b3a3b7e3f1380bd7557456ccb65aeb62da0a44c org/apache/maven/doxia/doxia-module-xhtml/1.0/doxia-module-xhtml-1.0.pom 664561bc6cbc608c712054aa15336f454ab296af45da9570379bba4071f66c85 com/amazonaws/aws-java-sdk-core/1.12.504/aws-java-sdk-core-1.12.504.jar @@ -484,7 +471,6 @@ 6b374ad2606f738c037e68b060f9b0d4c658ceefd7ac55ef15f7b6c584bf3aa0 org/scalatest/scalatest-diagrams_2.13/3.2.16/scalatest-diagrams_2.13-3.2.16.jar 6b87237de8c2e1740cf80627c7f3ce3e15de1930bb250c55a1eca94fa3e014df org/codehaus/codehaus-parent/4/codehaus-parent-4.pom 6bc8b8459150b59e9598f3dad9ad245a1ef2150d082581017184eb163e592829 net/java/dev/jna/jna/5.9.0/jna-5.9.0.pom -6ca8e5bbfe56b87be0faadd7b76eeb157f1dfde9f4e2903dd50a324ec629d1a0 fr/acinq/bitcoin-lib_2.13/0.36/bitcoin-lib_2.13-0.36.jar 6cba0e04f9eecb16486000793bb27e103b0f4d0485e0554e2b4c0aed13d2ed16 net/java/dev/jna/jna-platform/5.14.0/jna-platform-5.14.0.pom 6cfea9ac4c0c9f0de6f8be10ee4d631ae2b659607a0f3ea499c4c04a9d6a70e6 org/json4s/json4s-jackson-core_2.13/4.0.6/json4s-jackson-core_2.13-4.0.6.jar 6d10081f730242fb36a0a61d9b6dec015e56c866cf90e5c33e655743539e16a5 com/github/jnr/jnr-ffi/2.1.9/jnr-ffi-2.1.9.pom @@ -527,7 +513,6 @@ 75fe6bcd0e2b3da248bf55a3973fbd79cde29a6a80a02f9ad1fdf5593096bb60 org/apache/maven/surefire/surefire/3.1.2/surefire-3.1.2.pom 762fcdd4ce8621c5fa0a2cf6495ad26972a8093eb432aa3e402bc2d4e2500c53 org/apache/maven/maven-parent/41/maven-parent-41.pom 765b9c71832d2faad9611869c1f07263e3dfd000ebefdcd434d1f14cb2be3ea1 org/apache/apache/26/apache-26.pom -766689e14b091eab919c31daee1cd3f537b31aa477155cf1571bbfdf5ef6224d fr/acinq/secp256k1/secp256k1-kmp-jni-jvm-extract/0.17.1/secp256k1-kmp-jni-jvm-extract-0.17.1.jar 766ad2a0783f2687962c8ad74ceecc38a28b9f72a2d085ee438b7813e928d0c7 com/google/code/findbugs/jsr305/3.0.2/jsr305-3.0.2.jar 76b4a485a8c4c4f35d6d1c6af89c15cd8c3ad4f65671bd191ab822fefeba4147 eu/neilalexander/jnacl/1.0.0/jnacl-1.0.0.pom 76c899e9b5e0c28ae0cb56d480f2fbdf76da8aef3982ef6fa301588571da7fd5 com/amazonaws/aws-java-sdk-secretsmanager/1.12.504/aws-java-sdk-secretsmanager-1.12.504.jar @@ -543,6 +528,7 @@ 7868cb444944c97f8623aacad804cf51330326d6886d8ee88e99275461397936 org/apache/maven/maven-settings/3.8.6/maven-settings-3.8.6.pom 788fdf48a5ff60fb909b7e574d62036b5547cc975446dc18c99168b4ec124126 org/scala-sbt/zinc-classfile_2.13/1.8.0/zinc-classfile_2.13-1.8.0.jar 78eb9ada74929fcd63d07adc4f49236841a45cc29d5f817bf45801f513fd7e6c org/apache/httpcomponents/httpclient/4.5.13/httpclient-4.5.13.pom +78fa74b91ba0333d156ddd51e260c7cc13258ce2b6e7d4f1ae9b112d8f418869 fr/acinq/bitcoin/bitcoin-kmp-jvm/0.28.0/bitcoin-kmp-jvm-0.28.0.jar 78fefb752b801705c8d238e1355a962209bbf2501000f921d59efa5bf648c014 org/apache/maven/maven-model-builder/3.8.6/maven-model-builder-3.8.6.pom 7925d9c5a0e2040d24b8fae3f612eb399cbffe5838b33ba368777dc7bddf6dda org/apache/maven/shared/maven-shared-utils/3.3.4/maven-shared-utils-3.3.4.jar 794d140cb5764d5a10eaf8606756feeb9c5e7c22732211ba002486e1045c80af org/scala-sbt/zinc-classfile_2.13/1.8.0/zinc-classfile_2.13-1.8.0.pom @@ -550,6 +536,7 @@ 79c9792073fdee3cdbebd61a76ba8c2dd11624a9f85d128bae56bda19e20475c org/codehaus/plexus/plexus-utils/3.3.0/plexus-utils-3.3.0.pom 79cf2bac9ae8027e31535ed6c238b0ed6f60fa3d3ebfdf6af475a4dadeb14bbe org/apache/maven/surefire/surefire-extensions-spi/3.1.2/surefire-extensions-spi-3.1.2.pom 79d5c44035af6622b04e311ff41eacb04d0471b787b0d4c20b239b1d70d5e724 org/scala-lang/scala-reflect/2.13.10/scala-reflect-2.13.10.pom +79e0304ce11a5c1558712ea33a348b9256f78d598c69f7c48e810a3b889e2ee8 fr/acinq/secp256k1/secp256k1-kmp-jni-common/0.21.0/secp256k1-kmp-jni-common-0.21.0.pom 7a5001ab88105b4f37c4fab3b62d977316290a13f8b14c6684f25f2a32efdef1 org/codehaus/plexus/plexus-utils/3.2.1/plexus-utils-3.2.1.pom 7ace6e7fa6acd495bde67492a267b70ca91ecd870d2a686e1b3fe7d1aa9299c4 io/kamon/kamon-executors_2.13/2.7.4/kamon-executors_2.13-2.7.4.pom 7beb97394c5a89facc822be158ce3b1809bb71a189ab11afd77d354ba18a6ba4 org/scalatest/scalatest-refspec_2.13/3.2.16/scalatest-refspec_2.13-3.2.16.jar @@ -561,7 +548,6 @@ 7cd6047a0beec84b4d0d4c1a8ff4c9adb10c3f18e4fe18622f6ee7d4592b9a87 org/codehaus/plexus/plexus-io/3.4.1/plexus-io-3.4.1.pom 7ceae563bd483c66469fb098ed6345752361a71932146f02b83d54bb05c5af3d com/github/oshi/oshi-core/6.4.13/oshi-core-6.4.13.pom 7d34653ffe62be7714d2f1969660a17df9ec1b19d8250c4f03db1123c82ba6a0 org/ow2/asm/asm/5.0.3/asm-5.0.3.pom -7d4b70547910676b3bdfc8925a88f3b6bfb24582c9784542805544ceef490a92 org/jetbrains/kotlin/kotlin-stdlib-common/1.9.10/kotlin-stdlib-common-1.9.10.pom 7d547087459c89b8717ec5081aa45922d605299da39e32a811041db1b7e2514e org/eclipse/aether/aether-spi/1.0.0.v20140518/aether-spi-1.0.0.v20140518.pom 7d807b97b01639282d3f4ef5793f9e2fb0729212494324db804b59e623ac36e0 org/apache/maven/wagon/wagon-provider-api/2.12/wagon-provider-api-2.12.pom 7d95ad21733b060bfda2142b62439a196bde7644f9f127c299ae86d92179b518 org/codehaus/plexus/plexus-classworlds/2.2.3/plexus-classworlds-2.2.3.jar @@ -622,6 +608,7 @@ 8b30025f0ecb40d2b71a71ffeb6e97dfc7c43ce3cf2c698e51c7afac474b10ea org/json4s/json4s-jackson-core_2.13/4.0.6/json4s-jackson-core_2.13-4.0.6.pom 8c0e6aa7f35593016f2c5e78b604b57f023cdaca3561fe2fe36f2b5dbbae1d16 org/eclipse/sisu/org.eclipse.sisu.inject/0.3.4/org.eclipse.sisu.inject-0.3.4.jar 8c19e7148bee907597129b2fd706839c45db849c72a25285ec1674f0ffdabf8e org/zeromq/jeromq/0.5.2/jeromq-0.5.2.jar +8c3c821007c13411558739b9f3d5382eb81551db3895cffb89561e56c0f4dc16 org/jetbrains/kotlin/kotlin-stdlib/2.2.0/kotlin-stdlib-2.2.0.pom 8cbcb2aacd7f4a7759866ce91b2f910310fbe5a586b5fc7b9bdb76af9257e7c4 org/codehaus/plexus/plexus-components/1.3.1/plexus-components-1.3.1.pom 8d40d3d6f3f4cba522f5a04989e87ffc7662118dd184956b24683fdfdb2e9ede io/zonky/test/postgres/embedded-postgres-binaries-windows-amd64/14.5.0/embedded-postgres-binaries-windows-amd64-14.5.0.jar 8d87c70724a4f1e03f9e04b9e25f538b00b4826dd4044e65e61525fbe3c5f3dd org/apache/commons/commons-compress/1.22/commons-compress-1.22.pom @@ -634,6 +621,7 @@ 8e5f6e5327d4aa11dde8011ffb7715f30fb12a5a5fb3d6372e1bb5b3a296e0fc org/apache/maven/maven-plugin-api/3.8.2/maven-plugin-api-3.8.2.pom 8e63292e5c53bb93c4a6b0c213e79f15990fed250c1340f1c343880e1c9c39b5 com/squareup/okio/okio/3.6.0/okio-3.6.0.jar 8edf94ccf2a956fb75871b44bd5e085c15c965e725edc5c89ed21259b2f19cf0 org/json4s/json4s-scalap_2.13/4.0.3/json4s-scalap_2.13-4.0.3.pom +8f020542e4628d083277edf3b21f981924769484fc5f4af7cd92eaf8458afca8 fr/acinq/secp256k1/secp256k1-kmp-jni-jvm-linux/0.21.0/secp256k1-kmp-jni-jvm-linux-0.21.0.jar 8f3c20e3e2d565d26f33e8d4857a37d0d7f8ac39b62a7026496fcab1bdac30d4 org/bouncycastle/bcprov-jdk15on/1.70/bcprov-jdk15on-1.70.jar 8f73857d9862c58517f982660f90aba003b71de385793680bda65d9c9cfdce8c org/scalatest/scalatest-featurespec_2.13/3.2.16/scalatest-featurespec_2.13-3.2.16.jar 8f74dc3a67c107376168b9f7b1b94e317e172768d398f826330931a02ff57b98 org/jboss/weld/weld-api-bom/1.0/weld-api-bom-1.0.pom @@ -653,6 +641,7 @@ 91fbba37f1c8b251cf9ea9e7d3a369eb79eb1e6a5df1d4bbf483dd0380740281 com/google/guava/guava/32.1.1-jre/guava-32.1.1-jre.jar 920135797dcca5917b5a5c017642a58d340a4cd1bcd12f56f892a5663bd7bddc com/google/errorprone/error_prone_annotations/2.18.0/error_prone_annotations-2.18.0.pom 9283e684d401d821a4cbb2646f9611cbbcd7828d2499483d13a4b507775a4cd7 org/scalatest/scalatest-compatible/3.2.16/scalatest-compatible-3.2.16.jar +92e3885f9bc33dd159752afe9f5ca11d9178344345b48042786b9a707c4e42ce fr/acinq/secp256k1/secp256k1-kmp-jvm/0.21.0/secp256k1-kmp-jvm-0.21.0.jar 92eee24bc3c843e4881d46c1dd6505471ee3142facfb466b428cfea5a56c6b60 org/ow2/asm/asm/9.6/asm-9.6.pom 92f1c78b5b6775430e3ad4ca3bc5ea0fe862f28b1ee69fa293e2d1b33be977da org/scalatest/scalatest-wordspec_2.13/3.2.16/scalatest-wordspec_2.13-3.2.16.jar 930f23cb1386d71ef771d7cc18d1a792755f555aad69e0de05fcadc5768caf8f org/apache/maven/plugins/maven-wrapper-plugin/3.3.2/maven-wrapper-plugin-3.3.2.pom @@ -692,7 +681,7 @@ 9a8273601fbeb2fd822bcd1df06e0ddb9d76e6e884b0f96a00567f6a2e5d5c0f com/typesafe/akka/akka-actor_2.13/2.6.20/akka-actor_2.13-2.6.20.jar 9aa9dfeb2e85e1d5e7932c87140697cecc2b0fadd933d679fd420a2e43831a82 oro/oro/2.0.8/oro-2.0.8.pom 9b28bb307017938a94d06c85b2b099bc46912b859d084fb293e569f432eadb7c org/codehaus/plexus/plexus-sec-dispatcher/2.0/plexus-sec-dispatcher-2.0.pom -9bb107d5d5e3930bc5977f007a43cd20b7d24d91b1c1e528ea6ee0f248f14d36 org/jetbrains/kotlin/kotlin-stdlib-jdk7/1.8.21/kotlin-stdlib-jdk7-1.8.21.pom +9b4790e9528c6c19f1633300dd14cf13dd70ca9a4fe906269be340c3b33c37b8 fr/acinq/bitcoin-lib_2.13/0.45/bitcoin-lib_2.13-0.45.pom 9bb6808b89502af52b4662dfcd797945cb869a54b57f994e1652cab9f4d8f835 org/scala-sbt/zinc-compile-core_2.13/1.8.0/zinc-compile-core_2.13-1.8.0.pom 9c5f7cd5226ac8c3798cb1f800c031f7dedc1606dc50dc29567877c8224459a7 org/sonatype/forge/forge-parent/6/forge-parent-6.pom 9c62e83b103e38b10351603e246d7e54899d4a8f1d305176f5546dd3f8c55358 joda-time/joda-time/2.10.10/joda-time-2.10.10.pom @@ -713,6 +702,7 @@ 9ffc02a1bc3d846293ad9f889db9f42d0920e953acc4c4825726ff031405a7d0 org/ow2/asm/asm-analysis/5.0.3/asm-analysis-5.0.3.pom a07ee27454a0a0421a293cc9867bf6ab5d2e1c8b3de12e164fcb94d7600f8b18 org/scala-sbt/util-relation_2.13/1.8.0/util-relation_2.13-1.8.0.pom a0882b82514190c2bac7d1a459872a75f005fc0f3e88b2bc0390367146e35db7 org/scala-lang/scala-library/2.13.8/scala-library-2.13.8.jar +a0fd00a12de6ea8d0bd94ff37550eeab558af0d7b76fd7234c62f1851b90930e fr/acinq/bitcoin-lib_2.13/0.45/bitcoin-lib_2.13-0.45.jar a12578dde1ba00bd9b816d388a0b879928d00bab3c83c240f7013bf4196c579a org/slf4j/slf4j-api/2.0.16/slf4j-api-2.0.16.jar a171ee4c734dd2da837e4b16be9df4661afab72a41adaf31eb84dfdaf936ca26 com/google/guava/failureaccess/1.0.1/failureaccess-1.0.1.jar a17955976070c0573235ee662f2794a78082758b61accffce8d3f8aedcd91047 org/apache-extras/beanshell/bsh/2.0b6/bsh-2.0b6.jar @@ -721,7 +711,6 @@ a1ce5ff1ede64dd739ecdbeae94c2fbf90a5056d3e0aa9044b6eea0f95cdecf5 org/codehaus/m a1d90278a9c3effef6c45db86c660749d2910d8d7361ed81983565950f667e85 org/apache/maven/shared/maven-filtering/3.3.1/maven-filtering-3.3.1.pom a25a12836c84216d27e672a67e83585cf6866b1220820b6f04c22b061cba983e io/zonky/test/postgres/embedded-postgres-binaries-darwin-amd64/14.5.0/embedded-postgres-binaries-darwin-amd64-14.5.0.jar a25b2184d4127dc2dc8d1e6cb04d67603fb1176b9e4f3d3662ead0fee2f9b436 io/kamon/kamon-core_2.13/2.7.4/kamon-core_2.13-2.7.4.jar -a25bf47353ce899d843cbddee516d621a73473e7fba97f8d0301e7b4aed7c15f org/jetbrains/kotlin/kotlin-stdlib-jdk7/1.5.31/kotlin-stdlib-jdk7-1.5.31.jar a28f034d79815cd7a4b107f66182c21ebf45fbb2fbe2c15697860b720c8a3bf7 org/codehaus/plexus/plexus-languages/1.2.0/plexus-languages-1.2.0.pom a29daf6fc6a29db301b2df5b0c8715c86ff5c08fe00bbef2a14e3bdbb15d31f8 org/apache/maven/doxia/doxia-module-xdoc/1.0/doxia-module-xdoc-1.0.jar a2b4dc98a15bcf116b473efa4a6ffbe74aaccfe4cad327346be237ce0d49604e com/softwaremill/sttp/shared/core_2.13/1.3.13/core_2.13-1.3.13.jar @@ -753,6 +742,7 @@ a89a2f99088e244b3e52e35f19f5f7d3aba03dbb3cfaea044e2a694119e88f79 org/codehaus/p a8cac905ad0e52e724f33faa395415ef7fdbede354d7c25a11072c87c064dc1d net/java/dev/jna/jna-platform/5.12.0/jna-platform-5.12.0.jar a901f87b115c55070c7ee43efff63e20e7b02d30af2443ae292bf1f4e532d3aa org/apache/httpcomponents/httpcomponents-parent/11/httpcomponents-parent-11.pom a909af8c61361b3cea2aece5e9cd9acbe36badee294ea710a8dad77d90c64654 org/codehaus/plexus/plexus-archiver/4.6.1/plexus-archiver-4.6.1.jar +a9409750e84e2b9f10dbc05ca1778b6162d91425438c232dfb62110a8dbbcaf5 fr/acinq/secp256k1/secp256k1-kmp-jni-jvm-linux/0.21.0/secp256k1-kmp-jni-jvm-linux-0.21.0.pom a941745d7faeb8dc9a75edc2c330c994b7440b9a44d21142716b6053967a41c1 org/apache/maven/shared/maven-shared-utils/3.4.2/maven-shared-utils-3.4.2.pom a98e6af5cdf1b2456486ae0bb8128e52fee8d6996629af60606bbc9c1aadf8ec org/apache/maven/resolver/maven-resolver-util/1.9.18/maven-resolver-util-1.9.18.pom a9afce635b3e48663b4b356119873288326503030c779f723c70a3deae6b8d2c org/scala-sbt/zinc-apiinfo_2.13/1.8.0/zinc-apiinfo_2.13-1.8.0.jar @@ -775,9 +765,11 @@ ae4caceb3840730c2537f9b7fb55a01baba580286b4122951488bcee558c2449 net/java/dev/j aeb3b70a24c103ed0c54541f35f5c511d27ce60cae376a596d7d653697f8cc95 com/squareup/okio/okio/3.6.0/okio-3.6.0.pom af10c108da014f17cafac7b52b2b4b5a3a1c18265fa2af97a325d9143537b380 org/apache/apache/21/apache-21.pom af650fa4dd400bc5769320bcef08ed93813f349cabe8213d469acafbbd945d8a com/fasterxml/oss-parent/41/oss-parent-41.pom +af68fdbaf7c3ce7f7597aef53c6d483e77efc34ce0d88743782274330e2b4c4d fr/acinq/secp256k1/secp256k1-kmp-jvm/0.21.0/secp256k1-kmp-jvm-0.21.0.pom af6b01b48de15f2d4ded1e3884a0fdfd2deb4ef0156dccc6a2bfc13d3952c4ff org/apache/maven/plugins/maven-surefire-plugin/3.1.2/maven-surefire-plugin-3.1.2.pom aff0951639837c4e3a4699a421fa79f410032f603f5c6a5bba435e98531f3984 org/eclipse/aether/aether-util/1.0.0.v20140518/aether-util-1.0.0.v20140518.jar affa829a13c7ae4a39d4ca52ba126c71d577319c7b38fa09a4cec24187415f5b io/netty/netty-codec-xml/4.1.94.Final/netty-codec-xml-4.1.94.Final.jar +b0a1369969fcd10b1892fd41f95df0a0779d30f0f0ac6e641167997ad27c9305 fr/acinq/secp256k1/secp256k1-kmp-jni-jvm-mingw/0.21.0/secp256k1-kmp-jni-jvm-mingw-0.21.0.pom b0cb73444c6379583b2a53a2648c48de420eb1f49156675374d8a6756ef3a2fa com/squareup/okio/okio-jvm/3.0.0/okio-jvm-3.0.0.pom b1050081b14bb7a3a7e55a4d3ef01b5dcfabc453b4573a4fc019767191d5f4e0 com/squareup/okhttp3/okhttp/4.12.0/okhttp-4.12.0.jar b12663187d9ffc6a1ee76139c0ef497fe9400efbe2ebe01616fe2703656fb4f0 org/apache/maven/shared/maven-filtering/3.3.1/maven-filtering-3.3.1.jar @@ -800,12 +792,10 @@ b4a96b898513b73cdd96947cd080b3f3acbbd6126720ede9e075a0eb2d346d03 org/apache/mav b4d6f1e2b9172fb7510e18b14aea4ca5a0646fc8b3a2d70a2ebffa6fe48787dc org/sonatype/plexus/plexus-sec-dispatcher/1.4/plexus-sec-dispatcher-1.4.pom b518bcfc7dad77df2701bbc93c013e7165361f8b4761a995346ecda2f9d4e250 org/scoverage/scoverage-maven-plugin/1.4.1/scoverage-maven-plugin-1.4.1.pom b51f8867c92b6a722499557fc3a1fdea77bdf9ef574722fe90ce436a29559454 org/sonatype/oss/oss-parent/7/oss-parent-7.pom -b548f7767aacf029d2417e47440742bd6d3ebede19b60386e23554ce5c4c5fdc org/jetbrains/kotlin/kotlin-stdlib-jdk8/1.5.31/kotlin-stdlib-jdk8-1.5.31.jar b58e459509e190bed737f3592bc1950485322846cf10e78ded1d065153012d70 junit/junit/3.8.1/junit-3.8.1.jar b5bd73d2cbb9db55857f272c9df8ea64bd324d40598a2f84db3faab5df21a074 org/json4s/json4s-core_2.13/4.0.3/json4s-core_2.13-4.0.3.pom b5d9ea39bee8502c99608bfe50e250013b87cdc4c9f777ea6ec82722c0567b64 org/scalatest/scalatest-propspec_2.13/3.2.16/scalatest-propspec_2.13-3.2.16.jar b613357e1bad4dfc1dead801691c9460f9585fe7c6b466bc25186212d7d18487 org/apache/maven/shared/maven-shared-utils/3.4.2/maven-shared-utils-3.4.2.jar -b615e946b8c3d2bea99652ecda97d57ecf83bf5f34c65d286790af23e51283c2 org/jetbrains/kotlin/kotlin-stdlib-common/1.5.31/kotlin-stdlib-common-1.5.31.pom b619f9ca71d8579a42295bd8e8303992307bb3bd15caa90a75193aa5830e298e com/typesafe/akka/akka-cluster-tools_2.13/2.6.20/akka-cluster-tools_2.13-2.6.20.pom b657bef2e1eb11e029a70cd688bde6adad29e4e99dacb18516bf651ecca32435 org/apache/maven/plugins/maven-clean-plugin/3.2.0/maven-clean-plugin-3.2.0.jar b6617c2c7169b8d6c2440972e0d36e1265a80ed1d0def0263f1b400f4f3494a3 org/codehaus/plexus/plexus-container-default/1.0-alpha-8/plexus-container-default-1.0-alpha-8.pom @@ -820,10 +810,10 @@ b80031b8cb961b73b5cb35a7c720f5f6044c2ec527fdfc341e713cfca1997c08 com/fasterxml/ b817c67a40c94249fd59d4e686e3327ed0d3d3fae426b20da0f1e75652cfc461 org/postgresql/postgresql/42.6.0/postgresql-42.6.0.jar b8bcf4a49b85db0081c6b9305f56fd79dff75be33d8ec3e0f36042d3991adf49 org/apache/maven/plugins/maven-assembly-plugin/3.6.0/maven-assembly-plugin-3.6.0.pom b93f689f924e1718e27ae835c96c0b3397768427489685d97643eb9244cf9d64 com/github/jsqlparser/jsqlparser/4.1/jsqlparser-4.1.pom +b9853cf6917a7254a02a64931c870103eaaf879732ecf3c86fc95928e996740a fr/acinq/secp256k1/secp256k1-kmp-jni-common/0.21.0/secp256k1-kmp-jni-common-0.21.0.jar b993e4a0d96633ccafdcbd3f3d1d8c09874827a495d3c75d6d573110a547e717 com/amazonaws/jmespath-java/1.12.504/jmespath-java-1.12.504.pom b9efc41c3832ea329300f0b514ac7b96e7090eb7a42ad39ec1926ab276408294 org/scalatest/scalatest-maven-plugin/2.2.0/scalatest-maven-plugin-2.2.0.jar ba03294ee53e7ba31838e4950f280d033c7744c6c7b31253afc75aa351fbd989 org/apache/maven/maven-core/3.0/maven-core-3.0.jar -ba52b31d92e3c2b33c4c9905d112b255120096905dbc57b0112e4acbcc25a872 fr/acinq/secp256k1/secp256k1-kmp-jni-jvm-darwin/0.17.1/secp256k1-kmp-jni-jvm-darwin-0.17.1.pom ba748c36b9eb6019af3e4adaddd894ffab3d6d7b687b008f4642b99db18a9146 io/kamon/kanela-agent/1.0.18/kanela-agent-1.0.18.jar bb388d37fbcdd3cde64c3cede21838693218dc451f04040c5df360a78ed7e812 org/slf4j/slf4j-parent/1.7.36/slf4j-parent-1.7.36.pom bb8fba5306f2bb8fb92ce3379ba66fb2056aa0e150ce469f929445aeb900175f org/apache/maven/plugins/maven-plugins/37/maven-plugins-37.pom @@ -840,7 +830,6 @@ bcf3700301e8221ef14da27a2f0cff71fcd03fc45276bfd84adace401e88bebc org/apache/mav bd231c504e03c942e283bcd5f2bafeafad55644099693a76f18e178bc13a9eb0 org/scalatest/scalatest-flatspec_2.13/3.2.16/scalatest-flatspec_2.13-3.2.16.pom bd26e9bc5e94e2d3974a93fdf921658eff4f033bfd4c5208607760ab54298617 io/netty/netty-resolver/4.1.94.Final/netty-resolver-4.1.94.Final.jar bda25fcd0fa6b0d2fe359c571867d5b28b2b7200e3dd39ffe7b9890828109f4c org/scala-sbt/util-logging_2.13/1.8.0/util-logging_2.13-1.8.0.jar -bdd6bac43479570dffcc3eb2570995956e3e3b8966c7bf33dbf48aa7df63168d fr/acinq/secp256k1/secp256k1-kmp-jni-jvm/0.17.1/secp256k1-kmp-jni-jvm-0.17.1.pom bde3617ce9b5bcf9584126046080043af6a4b3baea40a3b153f02e7bbc32acac org/codehaus/plexus/plexus-component-annotations/2.1.0/plexus-component-annotations-2.1.0.jar bdf2a77186c9463b0e6b5b29be35b91528bb59f3148e23c52719470b9bbb3a47 io/netty/netty-resolver-dns-classes-macos/4.1.94.Final/netty-resolver-dns-classes-macos-4.1.94.Final.pom be64a0cc1f28ea9cd5c970dd7e7557af72c808d738c495b397bf897c9921e907 com/squareup/okio/okio-jvm/3.0.0/okio-jvm-3.0.0.jar @@ -871,11 +860,9 @@ c56a0dbd90cea691f83e58fa9a6388fb3ac6bc3c14b8c04d2e112544651fa528 org/apache/mav c5994010bcdce1d2bd603a4d50c47191ddbd7875d1157b23aaa26d33c82fda13 org/eclipse/sisu/org.eclipse.sisu.inject/0.3.5/org.eclipse.sisu.inject-0.3.5.jar c5a14770370e73a69367b131da1533890200b1e2aa70643b73f9ff31ef2e69ec org/scala-lang/scala-compiler/2.13.11/scala-compiler-2.13.11.jar c60ae538ba66adbc06aae205fbe2306211d3d213ab6df3239ec03cdde2458ad6 org/codehaus/plexus/plexus-classworlds/2.7.0/plexus-classworlds-2.7.0.jar -c63eb297a6b15de7f556afbf45adbfbfc3bd055d3ab7704f9391ce1e56cb1110 fr/acinq/secp256k1/secp256k1-kmp-jni-common/0.17.1/secp256k1-kmp-jni-common-0.17.1.pom c676575845fc8ad6dcfeca0827f405066a140f5fa5434715598179c8ba1c0817 com/google/guava/guava/16.0.1/guava-16.0.1.pom c71f3a79d600a5f60b8cdd3703f0c2940c3a4f0c5e121854f26b9809fbbbbee5 com/typesafe/ssl-config-core_2.13/0.4.3/ssl-config-core_2.13-0.4.3.pom c7f920b287c838e1d81e2a63f8677bb2fa99137058a2b11e6535afde9c81a0e5 com/squareup/okhttp3/okhttp/4.10.0/okhttp-4.10.0.pom -c7fa67c7961320b89d85a3ca59a2e18c2c65850845595dcae4b46af6945edcd5 org/jetbrains/kotlin/kotlin-stdlib-jdk7/1.9.10/kotlin-stdlib-jdk7-1.9.10.pom c86ee198a35a3715487860f419cbf642e7e4d9e8714777947dbe6a4e3a20ab58 commons-codec/commons-codec/1.15/commons-codec-1.15.pom c8b1b0b5aecc9316b4a87e99cac26991a3e6f6bf7e38486ea8fe887415b132ff io/netty/netty-transport-udt/4.1.94.Final/netty-transport-udt-4.1.94.Final.jar c8f4d8720972136be18385efd76cc074d35ff1a7a2b97a977590aa0015539bb2 com/swoval/file-tree-views/2.1.9/file-tree-views-2.1.9.pom @@ -884,7 +871,6 @@ c938e4d8cdf0674496749a87e6d3b29aa41b1b35a39898a1ade2bc9eae214c17 org/apache/mav c9686fd8ba7c62f0928f09d0b105aadc82bdab06241424ba2660e285dc559546 org/scala-lang/scala-library/2.13.10/scala-library-2.13.10.pom c9f70e161dc5194536831729605eb0ffdfabe0d01f199e6234f8dd26ccef4ddf org/scala-sbt/util-relation_2.13/1.8.0/util-relation_2.13-1.8.0.jar c9fa3c4799615ebf299f616e4817efd16dcff341e1ee04e51e741d8add68bb43 org/json4s/json4s-ast_2.13/4.0.6/json4s-ast_2.13-4.0.6.jar -cb3850b77fb0b4ee9330a22fee7c9a73e8036b8a532363147cdf4986c890d7f9 org/apache/maven/plugins/maven-deploy-plugin/3.1.2/maven-deploy-plugin-3.1.2.jar cb49812dc1bfb0ea4f20f398bcae1a88c6406e213e67f7524fb10d4f8ad9347b org/apache/commons/commons-exec/1.3/commons-exec-1.3.jar cb8d84a3e63aea90d0d7a333a02e50ac751d2b05db55745d981b5eff893f647b io/netty/netty-common/4.1.94.Final/netty-common-4.1.94.Final.jar cbbc96b250008d587a067377c736b017a7c1a82c5280687c2a90860c1e2eb987 ch/qos/logback/logback-classic/1.5.16/logback-classic-1.5.16.pom @@ -895,7 +881,6 @@ cd313494c670b483ec256972af1698b330e598f807002354eb765479f604b09c org/apache/com cd4da2ffd9adddfa30878350878286a5cfb332f7aeb08a39f24465c55e0cfb38 org/codehaus/plexus/plexus-io/3.4.0/plexus-io-3.4.0.jar cdba07964d1bb40a0761485c6b1e8c2f8fd9eb1d19c53928ac0d7f9510105c57 org/slf4j/slf4j-api/1.7.30/slf4j-api-1.7.30.jar cdcad9355b625743f40e4cead9a96353404e010c39c808d23b044be331afa251 org/apache/maven/resolver/maven-resolver-util/1.6.3/maven-resolver-util-1.6.3.jar -cde3341ba18a2ba262b0b7cf6c55b20c90e8d434e42c9a13e6a3f770db965a88 org/jetbrains/kotlin/kotlin-stdlib-common/1.9.10/kotlin-stdlib-common-1.9.10.jar ce6f6b7ab76d800fa28cd0671c8a3d55cb7f5cf787c0498bbae98a9862acbce8 org/codehaus/janino/janino/3.1.10/janino-3.1.10.jar cfa008d15f052e69221e8c3193056ff95c3c594271321ccac8d72dc1a770619c com/fasterxml/jackson/dataformat/jackson-dataformat-cbor/2.12.6/jackson-dataformat-cbor-2.12.6.jar cfd872c0ec27f53ae68f43dbc0fecded8add773079a53afbd390e407b42ce72f org/apache/apache/32/apache-32.pom @@ -922,13 +907,11 @@ d38fee3ac551747264be86b489e727c48ffcc75e1644e3ce3fc58c51029df118 org/scalatest/ d3cb55d6b1542fee5cad6fcfd21386b9d9efe5f08af692b4ffc7e14d2fb0ffa8 com/typesafe/akka/akka-stream-testkit_2.13/2.6.20/akka-stream-testkit_2.13-2.6.20.pom d3ef575e3e4979678dc01bf1dcce51021493b4d11fb7f1be8ad982877c16a1c0 org/slf4j/slf4j-api/1.7.36/slf4j-api-1.7.36.jar d4044a9eb806297ee9ae7e9f2e2455b12fd14b502405de5dd57b87f1511ca390 com/github/jsqlparser/jsqlparser/4.1/jsqlparser-4.1.jar -d449cd543a2de26aa67f747760064d48d04c7ab083c49ad752f2a1f3cafa4094 fr/acinq/secp256k1/secp256k1-kmp-jni-jvm-darwin/0.17.1/secp256k1-kmp-jni-jvm-darwin-0.17.1.jar d492b15a6d2ea3f1cc39c422c953c40c12289073dbe8360d98c0f6f9ec74fc44 com/jcraft/jsch/0.1.55/jsch-0.1.55.jar d52a2616a1389fce951de0e97a780b88f1bdf0c947b315a76fd47cd6bbfb239b com/google/errorprone/error_prone_parent/2.1.3/error_prone_parent-2.1.3.pom d5db404de0552d8dce641621132da55539d5125c3152bfec4043a16526d93856 com/thoughtworks/paranamer/paranamer/2.8/paranamer-2.8.pom d5e650c50ef6958c028ed024b59af04cf3d38e1453a77d542b6b484bc0f4ca0b org/sonatype/plexus/plexus-sec-dispatcher/1.3/plexus-sec-dispatcher-1.3.pom d63302d578efa240ff0f573b67952b92f4eed6274988154f214e3a7bd302b5c1 org/scalatest/scalatest-freespec_2.13/3.2.16/scalatest-freespec_2.13-3.2.16.jar -d68bb8fc4a521a4d0bdefda68f5eaf33395f56d9b9e2b69a6be356496122cb89 fr/acinq/secp256k1/secp256k1-kmp-jni-jvm-extract/0.17.1/secp256k1-kmp-jni-jvm-extract-0.17.1.pom d6a0871adbc47ce4b08ab7c23c6e32047bd197641f5f3fd35d165faed79a89e8 io/netty/netty-resolver-dns/4.1.94.Final/netty-resolver-dns-4.1.94.Final.jar d7d713c993363764ba1667296b835ee914e19b2e70921eafe96b37f0d5b4088f javax/enterprise/cdi-api/1.2/cdi-api-1.2.pom d811cf9f118cc2cad130c9427ad55c174d4c0a6f1a00ac1ae1c9bcbec5c44b19 io/github/java-diff-utils/java-diff-utils-parent/4.12/java-diff-utils-parent-4.12.pom @@ -939,12 +922,13 @@ d9440bbd99b48c4e2c924657a7580a33c8eab24d6d2c4551ae7791b00ea89792 org/apache/mav d95678e3af56b17c7db6cff9645efad5eb59be9f3c1caaaf5f0146edf04691d7 org/apache/commons/commons-compress/1.20/commons-compress-1.20.pom da3f53be1d45ef3af667fdfa26f54c3ebc0e196b8c4dfb646491dbe3e31cb20b org/glassfish/json/1.1.4/json-1.1.4.pom da73e32b58132e64daf12269fd9d011c0b303f234840f179908725a632b6b57c org/sonatype/plexus/plexus-sec-dispatcher/1.4/plexus-sec-dispatcher-1.4.jar -da9c0ccf0d6d6497dc3d794e681a7da19b0fc8cd7b58ed11c69c4014ce68c19f fr/acinq/secp256k1/secp256k1-kmp-jni-jvm/0.17.1/secp256k1-kmp-jni-jvm-0.17.1.jar dac807f65b07698ff39b1b07bfef3d87ae3fd46d91bbf8a2bc02b2a831616f68 org/apache/commons/commons-lang3/3.8.1/commons-lang3-3.8.1.jar dada2833d2f72fe87a60f5cd115762a8d7e8979d19dd35559893ff3ad1d9253c org/scodec/scodec-core_2.13/1.11.10/scodec-core_2.13-1.11.10.pom daddea1ea0be0f56978ab3006b8ac92834afeefbd9b7e4e6316fca57df0fa636 commons-logging/commons-logging/1.2/commons-logging-1.2.jar db5f50d0615eb423bf46aef97bf0e2ffcf96f135fff6028fb6f8ffa6df280b98 org/jline/jline-terminal/3.19.0/jline-terminal-3.19.0.pom +dbf7d1a66c749d7ec18aea711009d491d04575c806a9a6345598badd7cb0b26a fr/acinq/secp256k1/secp256k1-kmp-jni-jvm-darwin/0.21.0/secp256k1-kmp-jni-jvm-darwin-0.21.0.jar dc2807225359adffd593c60a7d1352969554113a0a5a20962f23a1cfd6539ffc org/jline/jline-parent/3.19.0/jline-parent-3.19.0.pom +dcebf76a37ae96af964e6491232fbe87298d7e8cffbbfad1990e30050f6fffb9 fr/acinq/secp256k1/secp256k1-kmp-jni-jvm/0.21.0/secp256k1-kmp-jni-jvm-0.21.0.jar dd0317f46922124e81571781a4276d425fab4abc95f43701b9a6bb7ac72584d4 org/scala-lang/scala-library/2.13.1/scala-library-2.13.1.pom dd14ed05a563983fc4496224dca9562c683aa68f70a9d279082d6fb5bd8232d8 org/apache/maven/maven-project/2.0/maven-project-2.0.pom dd5fc215db73fc436a70366aa38859517582bcc8f2805c7b95c0f159dd215cf7 org/apache/maven/doxia/doxia-modules/1.0/doxia-modules-1.0.pom @@ -953,7 +937,6 @@ dd8e7c92185a678d1b7b933f31209b6203c8ffa91e9880475a1be0346b9617e3 joda-time/joda de22a4c6f54fe31276a823b1bbd3adfd6823529e732f431b5eff0852c2b9252b org/apache/maven/maven-artifact/3.8.6/maven-artifact-3.8.6.jar de4952944a2bcbc30fdfb7477d9fdc45e371c457d804d1a3213e6101dcdc4138 com/typesafe/akka/akka-http-core_2.13/10.2.7/akka-http-core_2.13-10.2.7.jar de893ceb40c4659cbcd3f5507edb9474c825f72c5c0b61892cc87ace7197f6fe org/apache/velocity/velocity/1.5/velocity-1.5.pom -dfa2a18e26b028388ee1968d199bf6f166f737ab7049c25a5e2da614404e22ad org/jetbrains/kotlin/kotlin-stdlib-common/1.5.31/kotlin-stdlib-common-1.5.31.jar dfef7842e75acf825475d508a27c782d3b2f53a0acfdd543041070868364546d com/hierynomus/asn-one/0.5.0/asn-one-0.5.0.pom e003802501574637f7abdc4e83e6d509a31e9ff825d12da6d1e419acf9688705 org/codehaus/plexus/plexus-interpolation/1.25/plexus-interpolation-1.25.jar e006dd8894f9fc7b75fc32bb12fe5ed8be65667d5b454f99e2e0b8c5bb8d30b3 org/junit/junit-bom/5.10.0/junit-bom-5.10.0.pom @@ -986,6 +969,7 @@ e5aebcc93079a6f6470e93a8b8c446ede7db704fc1c75c7362f7e15af76f851b io/netty/netty e5f820f02dc5513b3f9518f6403fbcbcb0f1654cb965c759eb1e31ddd80f57fb org/codehaus/plexus/plexus-utils/3.0.17/plexus-utils-3.0.17.pom e68f33343d832398f3c8aa78afcd808d56b7c1020de4d3ad8ce47909095ee904 junit/junit/3.8.1/junit-3.8.1.pom e68fc19a48cec582a6732fd0b10dbfe9feca25060963def89e547f8a3759d379 org/apache/apache/25/apache-25.pom +e6cc1e9a6b37b8e2dde72a73dacf5cd0a4315206dfb22365c7dc89e1dd6121fc fr/acinq/secp256k1/secp256k1-kmp-jni-jvm-extract/0.21.0/secp256k1-kmp-jni-jvm-extract-0.21.0.jar e6d066a767c5dcaf8b625ed88478b0084883fee256d0e5935b5c896df59f1a91 org/mockito/mockito-scala_2.13/1.17.5/mockito-scala_2.13-1.17.5.pom e6d79207a0b814b5642e26dce24ebc0edaf32a3948fa542ab7097fc44f9592fe io/kamon/kamon-prometheus_2.13/2.7.4/kamon-prometheus_2.13-2.7.4.pom e71e6d9b6a3d559c409030e9ba83c8514cb625b937aeecb900d7a15613622c86 org/json4s/json4s-core_2.13/4.0.3/json4s-core_2.13-4.0.3.jar @@ -1035,6 +1019,7 @@ f515c2578178f45247ecca7a9e1db109531b1c42f2424e253ceeb0f6b8d42374 net/java/dev/j f54a0a28ce3d62af0e1cfe41dde616f645c28e452e77f77b78bc36e74d5e1a69 org/sonatype/aether/aether-spi/1.7/aether-spi-1.7.jar f55a922027a78fdd8b3ea15a0d8bfe3fec6a5ceac30c86aa8afa4a5b9b0df603 org/checkerframework/checker-qual/3.33.0/checker-qual-3.33.0.pom f5ecc6eaa4a32ee0c115d31525f588f491b2cc75fdeb4ed3c0c662c12ac0c32f org/apache/maven/maven-plugin-api/3.0/maven-plugin-api-3.0.jar +f5f9ceeac3b73ff9b82dcb22ea50ee47ee2d3e50c9c9e084496f6ab10344e5d7 fr/acinq/secp256k1/secp256k1-kmp-jni-jvm-mingw/0.21.0/secp256k1-kmp-jni-jvm-mingw-0.21.0.jar f62aeef7eb34196c9f4a7642cd610e4f175a88c040246918b19e4c3fbc0b8098 org/apache/maven/surefire/surefire-api/3.1.2/surefire-api-3.1.2.jar f66c2b6e2a1999b574bc8d37544d018c3fcab67877f4c9b311f9d23443f0096e pl/project13/maven/git-commit-id-plugin/4.9.10/git-commit-id-plugin-4.9.10.jar f70e12ebea93f119f4f63766c2b8a3386c34bb48e588df710cb98c8e3822f7c7 org/apache/maven/maven-core/3.0/maven-core-3.0.pom @@ -1074,7 +1059,6 @@ fd3edb9fd9b7cabd67a0c29c0c9c0a6d1ae7a40053956aec281f42ccad1bdcf1 org/slf4j/slf4 fd94a8ef572719510ee0a275632423060efa3f4756e996f92b34e1c1f5d4ef96 io/netty/netty-resolver-dns-native-macos/4.1.94.Final/netty-resolver-dns-native-macos-4.1.94.Final-osx-aarch_64.jar fdc1a8e8a231d73a6f2258e2a48cf0f7fd7113366ee81c026b184dbc3021efe2 org/scala-lang/scala-library/2.13.13/scala-library-2.13.13.pom fdfbcc92e87f424578b303bcb47e0f55fee990c4b6da0006c9e75879d1e442e4 org/scala-lang/scala-reflect/2.13.8/scala-reflect-2.13.8.jar -fe044f57bad86ad0d5afc50957b80b8bd5ddf0e2014e352132d9249a307ad0d1 org/apache/maven/plugins/maven-deploy-plugin/3.1.2/maven-deploy-plugin-3.1.2.pom fe6b68b358059e530bcb4fdf17da534cc710e3bd443acd73f566fb6f824b44ab com/softwaremill/sttp/client3/okhttp-backend_2.13/3.8.16/okhttp-backend_2.13-3.8.16.jar ff513db0361fd41237bef4784968bc15aae478d4ec0a9496f811072ccaf3841d org/apache/apache/13/apache-13.pom ff690ffc550b7ada3a4b79ef4ca89bf002b24f43a13a35d10195c3bba63d7654 org/sonatype/aether/aether-util/1.7/aether-util-1.7.jar diff --git a/.mvn/maven.config b/.mvn/maven.config index 28a25b9be0..acf51640eb 100644 --- a/.mvn/maven.config +++ b/.mvn/maven.config @@ -6,3 +6,5 @@ -Daether.artifactResolver.postProcessor.trustedChecksums.checksumAlgorithms=SHA-256 -Daether.artifactResolver.postProcessor.trustedChecksums.failIfMissing=true -Daether.artifactResolver.postProcessor.trustedChecksums.snapshots=false +# Uncomment the following line to generate checksums +# -Daether.artifactResolver.postProcessor.trustedChecksums.record=true diff --git a/BUILD.md b/BUILD.md index 29d432c1fb..23f7d64c0c 100644 --- a/BUILD.md +++ b/BUILD.md @@ -94,6 +94,6 @@ To make local development and testing easier, SNAPSHOT dependencies are not veri To re-create the trusted checksums file, run: ```shell -$ rm ~/.m2/wrapper ~/.sbt -rf +$ rm -Rf ~/.m2/wrapper ~/.sbt $ ./mvnw clean install -DskipTests -Daether.artifactResolver.postProcessor.trustedChecksums.record ``` diff --git a/README.md b/README.md index 41c226ab24..98c5409fe7 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ This means that instead of re-implementing them, Eclair benefits from the verifi * Eclair needs a _synchronized_, _segwit-ready_, **_zeromq-enabled_**, _wallet-enabled_, _non-pruning_, _tx-indexing_ [Bitcoin Core](https://github.com/bitcoin/bitcoin) node. * You must configure your Bitcoin node to use `bech32` or `bech32m` (segwit) addresses. If your wallet has "non-segwit UTXOs" (outputs that are neither `p2sh-segwit`, `bech32` or `bech32m`), you must send them to a `bech32` or `bech32m` address before running Eclair. -* Eclair requires Bitcoin Core 28.1 or higher. If you are upgrading an existing wallet, you may need to create a new address and send all your funds to that address. +* Eclair requires Bitcoin Core 29 or higher. If you are upgrading an existing wallet, you may need to create a new address and send all your funds to that address. Run bitcoind with the following minimal `bitcoin.conf`: @@ -71,8 +71,8 @@ server=1 rpcuser=foo rpcpassword=bar txindex=1 -addresstype=bech32 -changetype=bech32 +addresstype=bech32m +changetype=bech32m zmqpubhashblock=tcp://127.0.0.1:29000 zmqpubrawtx=tcp://127.0.0.1:29000 ``` @@ -284,11 +284,12 @@ eclair.chain = "regtest" eclair.bitcoind.rpcport=18443 ``` -For signet, add `signet=1` in `bitcoin.conf` or start with `-signet`, and modify `eclair.conf`: +For signet, add `signet=1` in `bitcoin.conf` or start with `-signet`, and modify `eclair.conf` as follows. The `signet-check-tx` config parameter should be the txid of a transaction that exists in your signet, "" to skip this check, or if not specified, the default signet txid value will be used. ```conf eclair.chain = "signet" eclair.bitcoind.rpcport=38332 +eclair.bitcoind.signet-check-tx= ``` You may also want to take advantage of the new configuration sections in `bitcoin.conf` to manage parameters that are network specific, diff --git a/docs/Configure.md b/docs/Configure.md index 3e3626610f..1c5e59653e 100644 --- a/docs/Configure.md +++ b/docs/Configure.md @@ -261,32 +261,16 @@ eclair { path-finding { experiments { control = ${eclair.router.path-finding.default} { - percentage = 50 - } - - // alternative routing heuristics (replaces ratios) - test-failure-cost = ${eclair.router.path-finding.default} { - use-ratios = false - - locked-funds-risk = 1e-8 // msat per msat locked per block. It should be your expected interest rate per block multiplied by the probability that something goes wrong and your funds stay locked. - // 1e-8 corresponds to an interest rate of ~5% per year (1e-6 per block) and a probability of 1% that the channel will fail and our funds will be locked. - - // Virtual fee for failed payments - // Corresponds to how much you are willing to pay to get one less failed payment attempt - failure-cost { - fee-base-msat = 2000 - fee-proportional-millionths = 500 - } - percentage = 10 + percentage = 70 } // To optimize for fees only: test-fees-only = ${eclair.router.path-finding.default} { - ratios { - base = 1 - cltv = 0 - channel-age = 0 - channel-capacity = 0 + // By setting everything to zero, only fees will be taken into account. + locked-funds-risk = 0 + failure-cost { + fee-base-msat = 0 + fee-proportional-millionths = 0 } hop-cost { fee-base-msat = 0 @@ -297,12 +281,6 @@ eclair { // To optimize for shorter paths: test-short-paths = ${eclair.router.path-finding.default} { - ratios { - base = 1 - cltv = 0 - channel-age = 0 - channel-capacity = 0 - } hop-cost { // High hop cost penalizes strongly longer paths fee-base-msat = 10000 @@ -313,30 +291,8 @@ eclair { // To optimize for successful payments: test-pay-safe = ${eclair.router.path-finding.default} { - ratios { - base = 0 - cltv = 0 - channel-age = 0.5 // Old channels should have less risk of failures - channel-capacity = 0.5 // High capacity channels are more likely to have enough liquidity to relay our payment - } - hop-cost { - // Less hops means less chances of failures - fee-base-msat = 1000 - fee-proportional-millionths = 1000 - } - percentage = 10 - } - - // To optimize for fast payments: - test-pay-fast = ${eclair.router.path-finding.default} { - ratios { - base = 0.2 - cltv = 0.5 // In case of failure we want our funds back as fast as possible - channel-age = 0.3 // Older channels are more likely to run smoothly - channel-capacity = 0 - } - hop-cost { - // Shorter paths should be faster + failure-cost { + // High failure cost will penalize paths that are less likely to succeed. fee-base-msat = 10000 fee-proportional-millionths = 10000 } diff --git a/docs/FAQ.md b/docs/FAQ.md index 802216d480..b41872fef3 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -1,6 +1,6 @@ # FAQ -## What does it mean for a channel to be "enabled" or "disabled" ? +## What does it mean for a channel to be "enabled" or "disabled"? A channel is disabled if a `channel_update` message has been broadcast for that channel with the `disable` bit set (see [BOLT 7](https://github.com/lightning/bolts/blob/master/07-routing-gossip.md#the-channel_update-message)). It means that the channel still exists but cannot be used to route payments, until it has been re-enabled. @@ -13,6 +13,16 @@ There are other cases when a channel becomes disabled, for example when its bala Note that you can have multiple channels between the same nodes, and that some of them can be enabled while others are disabled (i.e. enable/disable is channel-specific, not node-specific). -## How should you stop an Eclair node ? +## How do I make my closing transactions confirm faster? + +When channels are unilaterally closed, there is a delay before which closing transactions can be published: you must wait for this delay before you can get your funds back. + +Once published, transactions will be automatically RBF-ed by `eclair` based on your configuration values for the [`eclair.on-chain-fees` section](../eclair-core/src/main/resources/reference.conf). + +Note that there is an upper bound on the feerate that will be used, configured by the `eclair.on-chain-fees.max-closing-feerate` parameter. +If the current feerate is higher than this value, your transactions will not confirm. +You should update `eclair.on-chain-fees.max-closing-feerate` in your `eclair.conf` and restart your node: your transactions will automatically be RBF-ed using the new feerate. + +## How should you stop an Eclair node? To stop your node you just need to kill its process, there is no API command to do this. The JVM handles the quit signal and notifies the node to perform clean-up. For example, there is a hook to cleanly free DB locks when using Postgres. diff --git a/docs/PostgreSQL.md b/docs/PostgreSQL.md index 315d45b3f8..3039642fb3 100644 --- a/docs/PostgreSQL.md +++ b/docs/PostgreSQL.md @@ -2,11 +2,11 @@ By default, Eclair stores its data on the machine's local file system (typically in `~/.eclair` directory) using SQLite. -It also supports PostgreSQL version 10.6 and higher as a database backend. +It also supports PostgreSQL version 10.6 and higher as a database backend. To enable PostgreSQL support set the `driver` parameter to `postgres`: -``` +```conf eclair.db.driver = postgres ``` @@ -14,7 +14,7 @@ eclair.db.driver = postgres To configure the connection settings use the `database`, `host`, `port` `username` and `password` parameters: -``` +```conf eclair.db.postgres.database = "mydb" eclair.db.postgres.host = "127.0.0.1" # Default: "localhost" eclair.db.postgres.port = 12345 # Default: 5432 @@ -22,14 +22,14 @@ eclair.db.postgres.username = "myuser" eclair.db.postgres.password = "mypassword" ``` -Eclair uses Hikari connection pool (https://github.com/brettwooldridge/HikariCP) which has a lot of configuration -parameters. Some of them can be set in Eclair config file. The most important is `pool.max-size`, it defines the maximum -allowed number of simultaneous connections to the database. +Eclair uses [Hikari connection pool](https://github.com/brettwooldridge/HikariCP) which has a lot of configuration parameters. +Some of them can be set in Eclair config file. +The most important is `pool.max-size`, it defines the maximum allowed number of simultaneous connections to the database. -A good rule of thumb is to set `pool.max-size` to the CPU core count times 2. -See https://github.com/brettwooldridge/HikariCP/wiki/About-Pool-Sizing for better estimation. +A good rule of thumb is to set `pool.max-size` to the CPU core count times 2. +See https://github.com/brettwooldridge/HikariCP/wiki/About-Pool-Sizing for better estimation. -``` +```conf eclair.db.postgres.pool { max-size = 8 # Default: 10 connection-timeout = 10 seconds # Default: 30 seconds @@ -39,18 +39,18 @@ eclair.db.postgres.pool { ``` ### Locking settings - -Running multiple Eclair processes connected to the same database can lead to data corruption and loss of funds. -That's why Eclair supports database locking mechanisms to prevent multiple Eclair instances from accessing one database together. + +Running multiple Eclair processes connected to the same database can lead to data corruption and loss of funds. +That's why Eclair supports database locking mechanisms to prevent multiple Eclair instances from accessing one database together. Use `postgres.lock-type` parameter to set the locking schemes. - Lock type | Description ----|--- -`lease` | At the beginning, Eclair acquires a lease for the database that expires after some time. Then it constantly extends the lease. On each lease extension and each database transaction, Eclair checks if the lease belongs to the Eclair instance. If it doesn't, Eclair assumes that the database was updated by another Eclair process and terminates. Note that this is just a safeguard feature for Eclair rather than a bulletproof database-wide lock, because third-party applications still have the ability to access the database without honoring this locking scheme. -`none` | No locking at all. Useful for tests. DO NOT USE ON MAINNET! + Lock type | Description +-----------|---------------- + `lease` | At the beginning, Eclair acquires a lease for the database that expires after some time. Then it constantly extends the lease. On each lease extension and each database transaction, Eclair checks if the lease belongs to the Eclair instance. If it doesn't, Eclair assumes that the database was updated by another Eclair process and terminates. Note that this is just a safeguard feature for Eclair rather than a bulletproof database-wide lock, because third-party applications still have the ability to access the database without honoring this locking scheme. + `none` | No locking at all. Useful for tests. DO NOT USE ON MAINNET! -``` +```conf eclair.db.postgres.lock-type = "none" // Default: "lease" ``` @@ -58,10 +58,9 @@ eclair.db.postgres.lock-type = "none" // Default: "lease" There are two main configuration parameters for the lease locking scheme: `lease.interval` and `lease.renew-interval`. `lease.interval` defines lease validity time. During the lease time no other node can acquire the lock, except the lease holder. -After that time the lease is assumed expired, any node can acquire the lease. So that only one node can update the database -at a time. Eclair extends the lease every `lease.renew-interval` until terminated. +After that time the lease is assumed expired, any node can acquire the lease. So that only one node can update the database at a time. Eclair extends the lease every `lease.renew-interval` until terminated. -``` +```conf eclair.db.postgres.lease { interval = 30 seconds // Default: 5 minutes renew-interval = 10 seconds // Default: 1 minute @@ -70,16 +69,13 @@ eclair.db.postgres.lease { ### Backups and replication -The PostgreSQL driver doesn't support Eclair's built-in online backups. Instead, you should use the tools provided -by PostgreSQL. +The PostgreSQL driver doesn't support Eclair's built-in online backups. Instead, you should use the tools provided by PostgreSQL. #### Backup/Restore -For nodes with infrequent channel updates its easier to use `pg_dump` to perform the task. +For nodes with infrequent channel updates its easier to use `pg_dump` to perform the task. -It's important to stop the node to prevent any channel updates while a backup/restore operation is in progress. It makes -sense to back up the database after each channel update, to prevent restoring an outdated channel's state and consequently -losing the funds associated with that channel. +It's important to stop the node to prevent any channel updates while a backup/restore operation is in progress. It makes sense to back up the database after each channel update, to prevent restoring an outdated channel's state and consequently losing the funds associated with that channel. For more information about backup refer to the official PostgreSQL documentation: https://www.postgresql.org/docs/current/backup.html @@ -87,58 +83,24 @@ For more information about backup refer to the official PostgreSQL documentation For busier nodes it isn't practical to use `pg_dump`. Fortunately, PostgreSQL provides built-in database replication which makes the backup/restore process more seamless. -To set up database replication you need to create a main database, that accepts all changes from the node, and a replica database. -Once replication is configured, the main database will automatically send all the changes to the replica. +To set up database replication you need to create a main database, that accepts all changes from the node, and a replica database. +Once replication is configured, the main database will automatically send all the changes to the replica. In case of failure of the main database, the node can be simply reconfigured to use the replica instead of the main database. -PostgreSQL supports [different types of replication](https://www.postgresql.org/docs/current/different-replication-solutions.html). -The most suitable type for an Eclair node is [synchronous streaming replication](https://www.postgresql.org/docs/current/warm-standby.html#SYNCHRONOUS-REPLICATION), -because it provides a very important feature, that helps keep the replicated channel's state up to date: +PostgreSQL supports [different types of replication](https://www.postgresql.org/docs/current/different-replication-solutions.html). +The most suitable type for an Eclair node is [synchronous streaming replication](https://www.postgresql.org/docs/current/warm-standby.html#SYNCHRONOUS-REPLICATION), because it provides a very important feature, that helps keep the replicated channel's state up to date: > When requesting synchronous replication, each commit of a write transaction will wait until confirmation is received that the commit has been written to the write-ahead log on disk of both the primary and standby server. -Follow the official PostgreSQL high availability documentation for the instructions to set up synchronous streaming replication: https://www.postgresql.org/docs/current/high-availability.html +Follow the official PostgreSQL [high availability documentation](https://www.postgresql.org/docs/current/high-availability.html) for the instructions to set up synchronous streaming replication. ### Safeguard to prevent accidental loss of funds due to database misconfiguration Using Eclair with an outdated version of the database or a database created with another seed might lead to loss of funds. -Every time Eclair starts, it checks if the Postgres database connection settings have changed since the last start. -If in fact the settings have changed, Eclair stops immediately to prevent potentially dangerous -but accidental configuration changes to come into effect. - -Eclair stores the latest database settings in the `${data-dir}/last_jdbcurl` file, and compares its contents with the database settings from the config file. - -The node operator can force Eclair to accept new database -connection settings by removing the `last_jdbcurl` file. - -### Migrating from Sqlite to Postgres - -Eclair supports migrating your existing node from Sqlite to Postgres. Note that the opposite (from Postgres to Sqlite) is not supported. - -:warning: Once you have migrated from Sqlite to Postgres there is no going back! - -To migrate from Sqlite to Postgres, follow these steps: -1. Stop Eclair -2. Edit `eclair.conf` - 1. Set `eclair.db.postgres.*` as explained in the section [Connection Settings](#connection-settings). - 2. Set `eclair.db.driver=dual-sqlite-primary`. This will make Eclair use both databases backends. All calls to sqlite will be replicated in postgres. - 3. Set `eclair.db.dual.migrate-on-restart=true`. This will make Eclair migrate the data from Sqlite to Postgres at startup. - 4. Set `eclair.db.dual.compare-on-restart=true`. This will make Eclair compare Sqlite and Postgres at startup. The result of the comparison is displayed in the logs. -3. Delete the file `~/.eclair/last_jdbcurl`. The purpose of this file is to prevent accidental change in the database backend. -4. Start Eclair. You should see in the logs: - 1. `migrating all tables...` - 2. `migration complete` - 3. `comparing all tables...` - 4. `comparison complete identical=true` (NB: if `identical=false`, contact support) -5. Eclair should then finish startup and operate normally. Data has been migrated to Postgres, and Sqlite/Postgres will be maintained in sync going forward. -6. Edit `eclair.conf` and set `eclair.db.dual.migrate-on-restart=false` but do not restart Eclair yet. -7. We recommend that you leave Eclair in dual db mode for a while, to make sure that you don't have issues with your new Postgres database. This a good time to set up [Backups and replication](#backups-and-replication). -8. After some time has passed, restart Eclair. You should see in the logs: - 1. `comparing all tables...` - 2. `comparison complete identical=true` (NB: if `identical=false`, contact support) -9. At this point we have confidence that the Postgres backend works normally, and we are ready to drop Sqlite for good. -10. Edit `eclair.conf` - 1. Set `eclair.db.driver=postgres` - 2. Set `eclair.db.dual.compare-on-restart=false` -11. Restart Eclair. From this moment, you cannot go back to Sqlite! If you try to do so, Eclair will refuse to start. \ No newline at end of file +Every time Eclair starts, it checks if the Postgres database connection settings have changed since the last start. +If in fact the settings have changed, Eclair stops immediately to prevent potentially dangerous but accidental configuration changes to come into effect. + +Eclair stores the latest database settings in the `${data-dir}/last_jdbcurl` file, and compares its contents with the database settings from the config file. + +The node operator can force Eclair to accept new database connection settings by removing the `last_jdbcurl` file. diff --git a/docs/release-notes/eclair-v0.13.0.md b/docs/release-notes/eclair-v0.13.0.md new file mode 100644 index 0000000000..280242dd44 --- /dev/null +++ b/docs/release-notes/eclair-v0.13.0.md @@ -0,0 +1,268 @@ +# Eclair v0.13.0 + +This release contains a lot of refactoring and an initial implementation of taproot channels. +The specification for taproot channels is still ongoing (https://github.com/lightning/bolts/pull/995) so this cannot be used yet, but it is ready for cross-compatibility tests with other implementations. + +This release also contains improvements to splicing based on recent specification updates, and better Bolt 12 support. +Note that the splicing specification is still pending, so it cannot be used with other implementations yet. + +This is the last release of eclair where channels that don't use anchor outputs will be supported. +If you have channels that don't use anchor outputs, you should close them (see below for more details). + +As usual, this release contains various performance improvements, more configuration options and bug fixes. +In particular, the amount of data stored for each channel has been optimized (especially during force-close), which reduces the size of the channels DB. +Also, the performance of the on-chain watcher during mass force-close has been drastically improved. + +## Major changes + +### Package relay + +With Bitcoin Core 28.1, eclair starts relying on the `submitpackage` RPC during channel force-close. +When using anchor outputs, allows propagating our local commitment transaction to peers who are also running Bitcoin Core 28.x or newer, even if the commitment feerate is low (package relay). + +This removes the need for increasing the commitment feerate based on mempool conditions, which ensures that channels won't be force-closed anymore when nodes disagree on the current feerate. + +### Deprecation warning for non-anchor channels + +This is the last release where `eclair` will support non-anchor channels. +Starting with the next release, those channels will be deprecated and `eclair` will refuse to start. +Please make sure you close your existing non-anchor channels whenever convenient. + +You can list those channels using the following command: + +```sh +$ eclair-cli channels | jq '.[] | { channelId: .data.commitments.channelParams.channelId, commitmentFormat: .data.commitments.active[].commitmentFormat }' | jq 'select(.["commitmentFormat"] == "legacy")' +``` + +If your peer is online, you can then cooperatively close those channels using the following command: + +```sh +$ eclair-cli close --channelId= --preferredFeerateSatByte= +``` + +If your peer isn't online, you may want to force-close those channels to recover your funds: + +```sh +$ eclair-cli forceclose --channelId= +``` + +### Database migration of channel data + +When updating your node, eclair will automatically migrate all of your channel data to the latest (internal) encoding. +Depending on the number of open channels, this may be a bit slow: don't worry if this initial start-up is taking more time than usual. +This will only happen the first time you restart your node. + +This is an important step towards removing legacy code from our codebase, which we will do before the next release. + +### Attribution data + +Eclair now supports attributable failures which allow nodes to prove they are not the source of the failure. +Previously a failing node could choose not to report the failure and we would penalize all nodes of the route. +If all nodes of the route support attributable failures, we only need to penalize two nodes (there is still some uncertainty as to which of the two nodes is the failing one). +See https://github.com/lightning/bolts/pull/1044 for more details. + +Attribution data also provides hold times from payment relayers, both for fulfilled and failed HTLCs. + +Support is enabled by default. +It can be disabled by setting `eclair.features.option_attribution_data = disabled`. + +### Local reputation and HTLC endorsement + +To protect against jamming attacks, eclair gives a reputation to its neighbors and uses it to decide if a HTLC should be relayed given how congested the outgoing channel is. +The reputation is basically how much this node paid us in fees divided by how much they should have paid us for the liquidity and slots that they blocked. +The reputation is per incoming node and endorsement level. +The confidence that the HTLC will be fulfilled is transmitted to the next node using the endorsement TLV of the `update_add_htlc` message. +Note that HTLCs that are considered dangerous are still relayed: this is the first phase of a network-wide experimentation aimed at collecting data. + +To configure, edit `eclair.conf`: + +```eclair.conf +// We assign reputations to our peers to prioritize payments during congestion. +// The reputation is computed as fees paid divided by what should have been paid if all payments were successful. +eclair.relay.peer-reputation { + // Set this parameter to false to disable the reputation algorithm and simply relay the incoming endorsement + // value, as described by https://github.com/lightning/blips/blob/master/blip-0004.md, + enabled = true + // Reputation decays with the following half life to emphasize recent behavior. + half-life = 30 days + // Payments that stay pending for longer than this get penalized + max-relay-duration = 5 minutes +} +``` + +### Use past payment attempts to estimate payment success + +When setting `eclair.router.path-finding.use-ratios = false` in `eclair.conf`, we estimate the probability that a given route can relay a given payment as part of route selection. +Until now this estimate was naively assuming the channel balances to be uniformly distributed. +By setting `eclair.router.path-finding.use-past-relay-data = true`, we will now use data from past payment attempts (both successes and failures) to provide a better estimate, hopefully improving route selection. + +### API changes + +- `listoffers` now returns more details about each offer (see #3037 for more details). +- `parseoffer` is added to display offer fields in a human-readable format (see #3037 for more details). +- `forceclose` has a new optional parameter `maxClosingFeerateSatByte`: see the `max-closing-feerate` configuration section below for more details. + +### Configuration changes + +The default configuration value for `eclair.features.option_channel_type` is now `mandatory` instead of `optional`. This change has been added to the BOLTs in [#1232](https://github.com/lightning/bolts/pull/1232). + +We added a configuration parameter to facilitate custom signet use. +The parameter `eclair.bitcoind.signet-check-tx` should be set to the txid of a transaction that exists in your signet or set to `""` to skip this check. +See issue [#3078](https://github.com/ACINQ/eclair/issues/3078) for details. + +### Miscellaneous improvements and bug fixes + +#### Add `max-closing-feerate` configuration parameter + +We added a new configuration value to `eclair.conf` to limit the feerate used for force-close transactions where funds aren't at risk: `eclair.on-chain-fees.max-closing-feerate`. +This ensures that you won't end up paying a lot of fees during mempool congestion: your node will wait for the feerate to decrease to get your non-urgent transactions confirmed. + +The default value from `eclair.conf` can be overridden by using the `forceclose` API with the `maxClosingFeerateSatByte` set, which allows a per-channel override. This is particularly useful for channels where you have a large balance, which you may wish to recover more quickly. + +If you need those transactions to confirm because you are low on liquidity, you can either: + +- update `eclair.on-chain-fees.max-closing-feerate` and restart your node: `eclair` will automatically RBF all available transactions for all closing channels. +- use the `forceclose` API with the `maxClosingFeerateSatByte` set, to update a selection of channels without restarting your node. + +#### Remove confirmation scaling based on funding amount + +We previously scaled the number of confirmations based on the channel funding amount. +However, this doesn't work with splicing, where the channel capacity may change drastically. +It's much simpler to always use the same number of confirmations, while choosing a value that is large enough to protect against malicious reorgs. +We now by default use 8 confirmations, which can be modified in `eclair.conf`: + +```conf +// Minimum number of confirmations for channel transactions to be safe from reorgs. +eclair.channel.min-depth-blocks = 8 +``` + +Note however that we require `min-depth` to be at least 6 blocks, since the BOLTs require this before announcing channels. +See #3044 for more details. + +## Verifying signatures + +You will need `gpg` and our release signing key E04E48E72C205463. Note that you can get it: + +- from our website: https://acinq.co/pgp/drouinf2.asc +- from github user @sstone, a committer on eclair: https://api.github.com/users/sstone/gpg_keys + +To import our signing key: + +```sh +$ gpg --import drouinf2.asc +``` + +To verify the release file checksums and signatures: + +```sh +$ gpg -d SHA256SUMS.asc > SHA256SUMS.stripped +$ sha256sum -c SHA256SUMS.stripped +``` + +## Building + +Eclair builds are deterministic. To reproduce our builds, please use the following environment (*): + +- Ubuntu 24.04.1 +- Adoptium OpenJDK 21.0.6 + +Then use the following command to generate the eclair-node packages: + +```sh +./mvnw clean install -DskipTests +``` + +That should generate `eclair-node/target/eclair-node--XXXXXXX-bin.zip` with sha256 checksums that match the one we provide and sign in `SHA256SUMS.asc` + +(*) You may be able to build the exact same artefacts with other operating systems or versions of JDK 21, we have not tried everything. + +## Upgrading + +This release is fully compatible with previous eclair versions. You don't need to close your channels, just stop eclair, upgrade and restart. + +## Changelog + +- [af05a55](https://github.com/ACINQ/eclair/commit/af05a55351fce881c9f9e8d0ef2ab369adfa5cd0) Back to dev (#3035) +- [c2f6852](https://github.com/ACINQ/eclair/commit/c2f68521f84aadd30cb70b2994e5b2e0aa6a1db1) Stop using `QuickLens` in non-test code (#3036) +- [bdaa625](https://github.com/ACINQ/eclair/commit/bdaa625aa8feca39f5d9c135cc3de2f0c05603ee) Improve Bolt12 offer APIs (#3037) +- [b98930b](https://github.com/ACINQ/eclair/commit/b98930b071634abcc933339880e15e8ae335d4f8) Fix flaky GossipIntegrationSpec test (#3039) +- [5d6e556](https://github.com/ACINQ/eclair/commit/5d6e556e618ac9c7136df6be3e74a5edbfe9f699) Support p2tr bitcoin wallet (#3026) +- [9a63ed9](https://github.com/ACINQ/eclair/commit/9a63ed9e210306add3138a1e3fef7804cdd151c3) Fix plugin loading in Windows (#3043) +- [9210dff](https://github.com/ACINQ/eclair/commit/9210dffe2c3fb7f9e403748d194dfe9b5003ecab) Keep track on which side initiated a mutual close (#3042) +- [9c85128](https://github.com/ACINQ/eclair/commit/9c851286acad7d04912edb4d84cedb76c914d84a) Remove amount-based confirmation scaling (#3044) +- [b4e938d](https://github.com/ACINQ/eclair/commit/b4e938d7caa10115cfb66f524a0a2bc36a979e4d) Optional `payment_secret` in trampoline outer payload (#3045) +- [2dfdc26](https://github.com/ACINQ/eclair/commit/2dfdc26b4278250f2cbc8238048b1d8f219a8aed) Make option_channel_type mandatory (#3046) +- [fbc7004](https://github.com/ACINQ/eclair/commit/fbc7004ecf24e971352271e2ec7f656779764bf0) Add more force-close tests for HTLC settlement (#3040) +- [24e397a](https://github.com/ACINQ/eclair/commit/24e397a9ce2f8761150f7895ed83faa54591c8b8) Add an index on `audit.relayed(channel_id)` (#3048) +- [9ea9e97](https://github.com/ACINQ/eclair/commit/9ea9e97a4609428f06abac7b89274f4a827da72b) Fix tests that sometimes get p2tr inputs (#3049) +- [3d415bc](https://github.com/ACINQ/eclair/commit/3d415bc9127cdd25779bda77d90307a42e82e21f) Use package relay for anchor force-close (#2963) +- [8df52bb](https://github.com/ACINQ/eclair/commit/8df52bb7f750b1ce30ff2361bc6e9a78f69955fc) Fix comments for `feerate-tolerance` in `reference.conf` (#3053) +- [e0a9c0a](https://github.com/ACINQ/eclair/commit/e0a9c0a03b00f15cb924ab4a0ae9d3fc0aa48bff) Relay non-blinded failure from wallet nodes (#3050) +- [650681f](https://github.com/ACINQ/eclair/commit/650681f78e28b0839943b9dec25335ed5ef9ef19) ChannelKeyManager: add optional list of spent outputs to sign() methods (#3047) +- [633738b](https://github.com/ACINQ/eclair/commit/633738b12dd3f86353ffceb7dad94b62f892228e) [test] latest-bitcoind: build output location has moved to bin/ (#3057) +- [cb7f95d](https://github.com/ACINQ/eclair/commit/cb7f95d479e8f262193b0664852f60d7ac098bc5) Add test for Bolt11 features minimal encoding (#3058) +- [4088359](https://github.com/ACINQ/eclair/commit/40883591e5a818bdef75ed95a003babebf2aab35) fixup! Fix flaky GossipIntegrationSpec test (#3039) (#3061) +- [383d141](https://github.com/ACINQ/eclair/commit/383d1413c336460882ad23d469f8a4fba00a817f) Remove support for claiming remote anchor output (#3062) +- [63f4ca8](https://github.com/ACINQ/eclair/commit/63f4ca8e10b24687992292dc07d8825ac05da2a5) Use bitcoin-lib 0.37 (#3063) +- [ecd4634](https://github.com/ACINQ/eclair/commit/ecd46342c38862167448f29a45d2f222bc8a20ce) Simplify channel keys management (#3064) +- [a626281](https://github.com/ACINQ/eclair/commit/a62628181f0b0876872c52bb626b185a93b33fd4) Remove duplication around skipping dust transactions (#3068) +- [76eb6cb](https://github.com/ACINQ/eclair/commit/76eb6cb9226160ddc068b0fd0b98cef951779e75) Move `addSigs` into each dedicated class (#3069) +- [3b714df](https://github.com/ACINQ/eclair/commit/3b714df04950054c1a66010050de4b9b75fe6a61) Refactor HTLC-penalty txs creation (#3071) +- [826284c](https://github.com/ACINQ/eclair/commit/826284cb277c28c7eef14aa275f3d6e3255c8e66) Remove `LegacyClaimHtlcSuccess` transaction class (#3072) +- [2ada7e7](https://github.com/ACINQ/eclair/commit/2ada7e7f82a991942b3efd95de30f08307c4fd22) Rework the `TransactionWithInputInfo` architecture (#3074) +- [f14b92d](https://github.com/ACINQ/eclair/commit/f14b92d7df8727d6362d86251d717a93f37df251) Increase default revocation timeout (#3082) +- [fdc2077](https://github.com/ACINQ/eclair/commit/fdc207797a9ef29e9910ac9ac03441b151a7229c) Refactor replaceable transactions (#3075) +- [9b0c00a](https://github.com/ACINQ/eclair/commit/9b0c00a2a28d3ba6c7f3d01fbd2d8704ebbdc75d) Add an option to specify a custom signet tx to check (#3088) +- [055695f](https://github.com/ACINQ/eclair/commit/055695fe0a2fee6dcfe2bfebe81a7e9c55ceaad4) Attributable failures (#3065) +- [1e23081](https://github.com/ACINQ/eclair/commit/1e23081751aa82ca1f9d23c90b919fa1f79026b6) Refactor some closing helper functions (#3089) +- [dd622ad](https://github.com/ACINQ/eclair/commit/dd622ad882a8860923989b5ed03348f26655dc77) Add low-level taproot helpers (#3086) +- [a1c6988](https://github.com/ACINQ/eclair/commit/a1c6988e0b11103a4faf58bb1a5802b853dac562) Stricter batching of `commit_sig` messages on the wire (#3083) +- [62182f9](https://github.com/ACINQ/eclair/commit/62182f91b1681778f6c4a7f327be4fc62ab21248) Increase limits for flaky test (#3091) +- [fb84a9d](https://github.com/ACINQ/eclair/commit/fb84a9d1115bf80bb2b3269fafa31fd5cc9dfacb) Cleaner handling of HTLC settlement during force-close (#3090) +- [f8b1272](https://github.com/ACINQ/eclair/commit/f8b1272cb40c8f5212b8d1fb69652221cb6b2c17) Watch spent outputs before watching for confirmation (#3092) +- [2c10538](https://github.com/ACINQ/eclair/commit/2c105381c0391f9ed0ef86b155be5d16c22324fa) Rework closing channel balance computation (#3096) +- [345aef0](https://github.com/ACINQ/eclair/commit/345aef0268096c57ea73842c928ec06a36e02eab) Add more splice channel_reestablish tests (#3094) +- [e7b9b89](https://github.com/ACINQ/eclair/commit/e7b9b896a9244893e13dce2a4eeadc594c364f09) Parse offers and pay offers with currency (#3101) +- [100e174](https://github.com/ACINQ/eclair/commit/100e174a34f572500814f1146285fbc9dabbeb0e) Add attribution data to UpdateFulfillHtlc (#3100) +- [52b7652](https://github.com/ACINQ/eclair/commit/52b7652b83e3645b4bda7737b3611984c7be0a53) Remove non-final transactions from `XxxCommitPublished` (#3097) +- [7b67f33](https://github.com/ACINQ/eclair/commit/7b67f332baf2312ef75bbf1f580643d90f831935) Stop storing commit tx and HTLC txs in channel data (#3099) +- [8abb525](https://github.com/ACINQ/eclair/commit/8abb5255270b02c6c7038e8c550a985762cc8037) Round hold times to decaseconds (#3112) +- [8e3e206](https://github.com/ACINQ/eclair/commit/8e3e206d4a653604cde661ff15d7d3edcec037b8) Increase channel spent delay to 72 blocks (#3110) +- [297f7f0](https://github.com/ACINQ/eclair/commit/297f7f05f4e8eccfd993b803a4d5e5ac2bf71d6e) Prepare attribution data for trampoline payments (#3109) +- [9418ea1](https://github.com/ACINQ/eclair/commit/9418ea1740479fbcd03df5f745979a5404939db9) Remove obsolete interop tests (#3114) +- [961f844](https://github.com/ACINQ/eclair/commit/961f84403125fc2a07400a574e21f2de4651a0ab) Simplify force-close transaction signing and replaceable publishers (#3106) +- [2e6c6fe](https://github.com/ACINQ/eclair/commit/2e6c6feadd4c9039a66f55f45dceee8da382b261) Use `Uint64` for `max_htlc_value_in_flight_msat` consistently (#3113) +- [4a34b8c](https://github.com/ACINQ/eclair/commit/4a34b8c73f2f562e97138e45704ca374595c2c5f) Ensure `htlc_maximum_msat` is at least `htlc_minimum_msat` (#3117) +- [e6585bf](https://github.com/ACINQ/eclair/commit/e6585bf7e68836022685296006c285b0569628cc) Keep original features byte vector in Bolt12 TLVs (#3121) +- [5e829ac](https://github.com/ACINQ/eclair/commit/5e829ac4c9e3508fb23b9ceda021674d2ec6376a) Stricter Bolt11 invoice parsing (#3122) +- [6fb7ac1](https://github.com/ACINQ/eclair/commit/6fb7ac1381d4aa603da756ff9f71ffc840d3404a) Refactor channel params: extract commitment params (#3116) +- [bddacda](https://github.com/ACINQ/eclair/commit/bddacda988f2f5a1a2588dc0fb7998d2dc9a4065) Publish hold times to the event stream (#3123) +- [3a9b791](https://github.com/ACINQ/eclair/commit/3a9b79184b71ac49d837e7473400e9cda7c665b2) Fix flaky 0-conf watch-published event (#3124) +- [d7c020d](https://github.com/ACINQ/eclair/commit/d7c020d14c57b3b463839da1c7a71fd157c77ef2) Refactor attribution helpers and commands (#3125) +- [17ac335](https://github.com/ACINQ/eclair/commit/17ac335c477b1cbf6fd372848eacbf020bd5f889) Upgrade to bitcoin-lib 0.41 (#3128) +- [43c3986](https://github.com/ACINQ/eclair/commit/43c3986870bbf864333b203011c6066216d6bfc4) Endorse htlc and local reputation (#2716) +- [09fc936](https://github.com/ACINQ/eclair/commit/09fc9368128a7af05474e1bb8ef4624bfb0656b1) Rename maxHtlcAmount to maxHtlcValueInFlight in Commitments (#3131) +- [a307b70](https://github.com/ACINQ/eclair/commit/a307b70b838d46ee28d2b5f39626802ca1de0767) Add unconfirmed transaction pruning when computing closing balance (#3119) +- [9af7084](https://github.com/ACINQ/eclair/commit/9af708446c9a09670855ef33779a0bca1a53eaaa) Add outgoing reputation (#3133) +- [65e2639](https://github.com/ACINQ/eclair/commit/65e26391754744c3e50938ca82f1db0106461808) Fix flaky on-chain balance test (#3132) +- [af3cd55](https://github.com/ACINQ/eclair/commit/af3cd55912d752056b437d57c41f480684e14c4a) Add recent invoice spec test vectors (#3137) +- [5703cd4](https://github.com/ACINQ/eclair/commit/5703cd45ebfb3fdcaa8db03b1d72afce2ee479d3) Use actual CLTV delta for reputation (#3134) +- [b651e5b](https://github.com/ACINQ/eclair/commit/b651e5b987528fbb9a5981eed5bf84414e52f94f) Offers with currency must set amount. (#3140) +- [49bee72](https://github.com/ACINQ/eclair/commit/49bee72fd7b4c6efaebc6c9521d604c1d5a40718) Extract `CommitParams` to individual commitments (#3118) +- [d8ce91b](https://github.com/ACINQ/eclair/commit/d8ce91b4efea704ee56a9938928824d1ed2665f9) Simple taproot channels (#3103) +- [0e0da42](https://github.com/ACINQ/eclair/commit/0e0da422986bd66672afc5e0f549e85f2f945cae) Allow omitting `previousTx` for taproot splices (#3143) +- [d7ee663](https://github.com/ACINQ/eclair/commit/d7ee6638c7c2094bbcd9309d5505a3f4f9c8dbbc) Split commit nonces from funding nonce in `tx_complete` (#3145) +- [3d5fd33](https://github.com/ACINQ/eclair/commit/3d5fd3347f291f9f2307bdc7a6a3e84708a58cd0) Fix minor incompatibilities with feature branches (#3148) +- [012b382](https://github.com/ACINQ/eclair/commit/012b3828b8d6e111755d3d00c49611837b7c79c2) Adjust `batch_size` on `commit_sig` retransmission (#3147) +- [50f16cb](https://github.com/ACINQ/eclair/commit/50f16cbd584bb3ecc00d75be7f04c261e41c886b) Add `GossipTimestampFilter` buffer during gossip queries to fix flaky tests (#3152) +- [18ea362](https://github.com/ACINQ/eclair/commit/18ea362861cc141c0c8e1842cc3188bddeae315a) Fix `LocalFundingStatus.ConfirmedFundingTx` migration (#3151) +- [d48dd21](https://github.com/ACINQ/eclair/commit/d48dd2140f8994a5362a7fc95aa10bd8366bcbab) Allow overriding `max-closing-feerate` with `forceclose` API (#3142) +- [f93d02f](https://github.com/ACINQ/eclair/commit/f93d02fb72ee6b1dc4fce953c9ecd120af260401) Allow non-initiator RBF for dual funding (#3021) +- [d4dfb86](https://github.com/ACINQ/eclair/commit/d4dfb8648e65dfc4ea15fd5dc872edf4cefd1d3e) Use balance estimates from past payments in path-finding (#2308) +- [07c0cfd](https://github.com/ACINQ/eclair/commit/07c0cfd6cdd50de7e5b65deda40123369ae16a36) Re-encode channel data using v5 codecs (#3149) +- [70a2e29](https://github.com/ACINQ/eclair/commit/70a2e297c063ec57896bc691bbe525412fd1648b) Use helpers for feerates conversions (#3156) +- [c9ff501](https://github.com/ACINQ/eclair/commit/c9ff5019edaf7ed338adcc6b5da6f0588b3504a8) Catch close commands in `Offline(WaitForDualFundingSigned)` (#3159) +- [2a5b0f1](https://github.com/ACINQ/eclair/commit/2a5b0f14f49de713a6ebec004d1890ff13475a11) Fix comparison of utxos in the balance (#3160) +- [6075196](https://github.com/ACINQ/eclair/commit/60751962703d8ba3b20ca91339df64f3d40ea819) Relax taproot feature dependency (#3161) +- [e8ec148](https://github.com/ACINQ/eclair/commit/e8ec148951b873bf3c96a71d9193db759209720c) Add high-S signature Bolt 11 test vector (#3163) +- [f32e0b6](https://github.com/ACINQ/eclair/commit/f32e0b681c05087baa202234f490f85510b913d5) Fix flaky `OfferPaymentSpec` (#3164) diff --git a/docs/release-notes/eclair-v0.13.1.md b/docs/release-notes/eclair-v0.13.1.md new file mode 100644 index 0000000000..c8d217b4ae --- /dev/null +++ b/docs/release-notes/eclair-v0.13.1.md @@ -0,0 +1,145 @@ +# Eclair v0.13.1 + +This release contains database changes to prepare for the removal of pre-anchor channels. +Closed channels will be moved to a new table on restart, which may take some time, but will only happen once. + +:warning: Note that you will need to run the v0.13.0 release first to migrate your channel data to the latest internal encoding. + +This is the last release of eclair where channels that don't use anchor outputs will be supported. +If you have channels that don't use anchor outputs, you should close them now. +You can list those channels using the following command: + +```sh +$ eclair-cli channels | jq '.[] | { channelId: .data.commitments.channelParams.channelId, commitmentFormat: .data.commitments.active[].commitmentFormat }' | jq 'select(.["commitmentFormat"] == "legacy")' +``` + +If your peer is online, you can then cooperatively close those channels using the following command: + +```sh +$ eclair-cli close --channelId= --preferredFeerateSatByte= +``` + +If your peer isn't online, you may want to force-close those channels to recover your funds: + +```sh +$ eclair-cli forceclose --channelId= +``` + +:warning: This release also updates the dependency on Bitcoin Core to v29.x (we recommend using v29.2). + +## Major changes + +### Move closed channels to dedicated database table + +We previously kept closed channels in the same database table as active channels, with a flag indicating that it was closed. +This creates performance issues for nodes with a large history of channels, and creates backwards-compatibility issues when changing the channel data format. + +We now store closed channels in a dedicated table, where we only keep relevant information regarding the channel. +When restarting your node, the channels table will automatically be cleaned up and closed channels will move to the new table. +This may take some time depending on your channels history, but will only happen once. + +### Remove support for legacy channel codecs + +We remove the code used to deserialize channel data from versions of eclair prior to v0.13. +Node operators running a version of `eclair` older than v0.13 must first upgrade to v0.13.0 to migrate their channel data, and then upgrade to the latest version. + +### Update minimal version of Bitcoin Core + +With this release, eclair requires using Bitcoin Core 29.x. +Newer versions of Bitcoin Core may be used, but have not been extensively tested. + +### New MPP splitting strategy + +Eclair can send large payments using multiple low-capacity routes by sending as much as it can through each route (if `randomize-route-selection = false`) or some random fraction (if `randomize-route-selection = true`). +These splitting strategies are now specified using `mpp.splitting-strategy = "full-capacity"` or `mpp.splitting-strategy = "randomize"`. +In addition, a new strategy is available: `mpp.splitting-strategy = "max-expected-amount"` will send through each route the amount that maximizes the expected delivered amount (amount sent multiplied by the success probability). + +Eclair's path-finding algorithm can be customized by modifying the `eclair.router.path-finding.experiments.*` sections of your `eclair.conf`. +The new `mpp.splitting-strategy` goes in these sections, or in `eclair.router.path-finding.default` from which they inherit. + +### Configuration changes + +No notable changes. + +### API changes + +- the `closedchannels` API now returns human-readable channel data + +### Miscellaneous improvements and bug fixes + +No notable changes. + +## Verifying signatures + +You will need `gpg` and our release signing key E04E48E72C205463. Note that you can get it: + +- from our website: https://acinq.co/pgp/drouinf2.asc +- from github user @sstone, a committer on eclair: https://api.github.com/users/sstone/gpg_keys + +To import our signing key: + +```sh +$ gpg --import drouinf2.asc +``` + +To verify the release file checksums and signatures: + +```sh +$ gpg -d SHA256SUMS.asc > SHA256SUMS.stripped +$ sha256sum -c SHA256SUMS.stripped +``` + +## Building + +Eclair builds are deterministic. To reproduce our builds, please use the following environment (*): + +- Ubuntu 24.04.1 +- Adoptium OpenJDK 21.0.6 + +Then use the following command to generate the eclair-node packages: + +```sh +./mvnw clean install -DskipTests +``` + +That should generate `eclair-node/target/eclair-node--XXXXXXX-bin.zip` with sha256 checksums that match the one we provide and sign in `SHA256SUMS.asc` + +(*) You may be able to build the exact same artefacts with other operating systems or versions of JDK 21, we have not tried everything. + +## Upgrading + +This release is fully compatible with previous eclair versions. You don't need to close your channels, just stop eclair, upgrade and restart. + +## Changelog + +- [3abc17f](https://github.com/ACINQ/eclair/commit/3abc17f9aa9eb3354e1616ff056f1511015a3ced) Back to dev (#3155) +- [f029584](https://github.com/ACINQ/eclair/commit/f02958467214dbe88cdaeeacbd30bc0a2627d161) Update Bitcoin Core to v29.1 (#3153) +- [ae3e44b](https://github.com/ACINQ/eclair/commit/ae3e44be7066c6ed45b8b8021d5de95e086954da) Remove legacy channel codecs and DB migrations (#3150) +- [a7e57ea](https://github.com/ACINQ/eclair/commit/a7e57ea7c8af1876067af78d98f5abb0e6e6d0fa) Update taproot commit weight to match `lnd` (#3158) +- [d3ac75d](https://github.com/ACINQ/eclair/commit/d3ac75dfb5fb0c07bfb43326ad4c11e77cd209d7) Fix flaky `ReputationRecorder` test (#3166) +- [c13d530](https://github.com/ACINQ/eclair/commit/c13d530f3283d5741c2d6c4efa055896a907a041) Fix flaky test in `OfferPaymentSpec` (#3165) +- [379abc5](https://github.com/ACINQ/eclair/commit/379abc55b76668e83a7144f225e9e9a52c260c88) Resign next remote commit on reconnection (#3157) +- [e351320](https://github.com/ACINQ/eclair/commit/e351320503faf79bc60c48e86a023c6e84436cf7) Increase timeout for flaky onion message tests (#3167) +- [76f8d53](https://github.com/ACINQ/eclair/commit/76f8d53aea564d169e4abc774bc098e21ef53366) Correctly fill PSBT for taproot `interactive-tx` (#3169) +- [9f00ebf](https://github.com/ACINQ/eclair/commit/9f00ebf98923455c53f500c3ba34bd35f0e505cf) Always count local CLTV delta in route finding (#3174) +- [08a1fc6](https://github.com/ACINQ/eclair/commit/08a1fc6603c7614164bb32f2628d5100de5061f2) Kill the connection if a peer sends multiple ping requests in parallel (#3172) +- [fa1b0ee](https://github.com/ACINQ/eclair/commit/fa1b0eef5d5f9a5075860c5868a5cb45b9b3980a) Remove `PaymentWeightRatios` from the routing config (#3171) +- [abe2cc9](https://github.com/ACINQ/eclair/commit/abe2cc9cc38df82394f793e5924f8b802a1095fe) Reject offers with some fields present but empty (#3175) +- [ad8b2e3](https://github.com/ACINQ/eclair/commit/ad8b2e3b36a9e09981fc34710d0ba66a2368fb91) Add "phoenix zero reserve" feature bit (#3176) +- [5b4368a](https://github.com/ACINQ/eclair/commit/5b4368a65bee501c80cb8681da171d704db55888) Fix encoding of channel type TLV in splice_init/splice_ack (#3178) +- [90778ca](https://github.com/ACINQ/eclair/commit/90778ca483746143bcab9cb086bb009cd89a5562) Smaller default value for `peer-connection.max-no-channels` (#3180) +- [d04ed3d](https://github.com/ACINQ/eclair/commit/d04ed3dc34b4a43a912c49e09f47dbf4354b933c) Deduplicate closing balance during mutual close (#3182) +- [2b39bf6](https://github.com/ACINQ/eclair/commit/2b39bf622f14cfe227cfc225c48423145e54e5c6) Update `bitcoin-lib` (#3179) +- [16a309e](https://github.com/ACINQ/eclair/commit/16a309e49e83e094fb02063776a324c6a4f31f9c) Add `DATA_CLOSED` class when active channel is closed (#3170) +- [56a3acd](https://github.com/ACINQ/eclair/commit/56a3acdf625ebb455f1c0cbb06339694f2554b49) Add `zero-conf` test tag for Phoenix taproot tests (#3181) +- [cc75b13](https://github.com/ACINQ/eclair/commit/cc75b135f403ab7b4bb8bf09c75a8c4249fa66d9) Nits (#3183) +- [d1863f9](https://github.com/ACINQ/eclair/commit/d1863f9457988e2b78d0c3460b5bb09ae2d610c1) Create fresh shutdown nonce on reconnection (#3184) +- [3208266](https://github.com/ACINQ/eclair/commit/32082666ecf0bca6d0ab87185ef0f4c32a94bb44) Use bitcoin-lib 0.44 (#3185) +- [51a144c](https://github.com/ACINQ/eclair/commit/51a144c8a231b4fe4df6dfb6f51ce45a1188aed3) Split MPP by maximizing expected delivered amount (#2792) +- [409c7c1](https://github.com/ACINQ/eclair/commit/409c7c17148ef1d18d04a3e74739c2cdd89e87ca) Don't store anchor transaction in channel data (#3187) +- [0baddd9](https://github.com/ACINQ/eclair/commit/0baddd91b1901317dc74e7ebf3cb00fee3499536) Only store txs spending our commit outputs (#3188) +- [711c52a](https://github.com/ACINQ/eclair/commit/711c52ab717a55a87f2ea56d9f8115a61170e1d8) Avoid negative on-the-fly funding fee (#3189) +- [656a2fe](https://github.com/ACINQ/eclair/commit/656a2fe6859d426c6f47afa488ac88cd7cfda440) Update Bitcoin Core to v29.2 (#3190) +- [7372a87](https://github.com/ACINQ/eclair/commit/7372a8779e3b1c2bc14975adcbdd53bb7a216ccb) Update `bitcoin-lib` (#3193) +- [9771b2d](https://github.com/ACINQ/eclair/commit/9771b2d862ec779e6281152a3b93dd0d438a4d91) Configure bitcoind test instances to use bech32m addresses (#3195) +- [701f229](https://github.com/ACINQ/eclair/commit/701f2297d2e385d7125268a51e30a4acd5e0ff73) More flexible mixing of clearnet addresses and tor proxy (#3054) diff --git a/docs/release-notes/eclair-vnext.md b/docs/release-notes/eclair-vnext.md index 1681f2ecea..9665470690 100644 --- a/docs/release-notes/eclair-vnext.md +++ b/docs/release-notes/eclair-vnext.md @@ -4,40 +4,22 @@ ## Major changes - +### Remove support for non-anchor channels -### Package relay +We remove the code used to support legacy channels that don't use anchor outputs or taproot. +If you still have such channels, eclair won't start: you will need to close those channels, and will only be able to update eclair once they have been successfully closed. -With Bitcoin Core 28.1, eclair starts relying on the `submitpackage` RPC during channel force-close. -When using anchor outputs, allows propagating our local commitment transaction to peers who are also running Bitcoin Core 28.x or newer, even if the commitment feerate is low (package relay). +### Configuration changes -This removes the need for increasing the commitment feerate based on mempool conditions, which ensures that channels won't be force-closed anymore when nodes disagree on the current feerate. + ### API changes -- `listoffers` now returns more details about each offer. - - -### Configuration changes - -- The default for `eclair.features.option_channel_type` is now `mandatory` instead of `optional`. This change prepares nodes to always assume the behavior of `option_channel_type` from peers when Bolts PR [#1232](https://github.com/lightning/bolts/pull/1232) is adopted. Until [#1232](https://github.com/lightning/bolts/pull/1232) is adopted you can still set `option_channel_type` to `optional` in your `eclair.conf` file for specific peers that do not yet support this option, see `Configure.md` for more information. + ### Miscellaneous improvements and bug fixes -#### Remove confirmation scaling based on funding amount - -We previously scaled the number of confirmations based on the channel funding amount. -However, this doesn't work with splicing, where the channel capacity may change drastically. -It's much simpler to always use the same number of confirmations, while choosing a value that is large enough to protect against malicious reorgs. -We now by default use 8 confirmations, which can be modified in `eclair.conf`: - -```conf -// Minimum number of confirmations for channel transactions to be safe from reorgs. -eclair.channel.min-depth-blocks = 8 -``` - -Note however that we require `min-depth` to be at least 6 blocks, since the BOLTs require this before announcing channels. -See #3044 for more details. + ## Verifying signatures @@ -66,7 +48,7 @@ Eclair builds are deterministic. To reproduce our builds, please use the followi - Ubuntu 24.04.1 - Adoptium OpenJDK 21.0.6 -Use the following command to generate the eclair-node package: +Then use the following command to generate the eclair-node packages: ```sh ./mvnw clean install -DskipTests @@ -82,4 +64,4 @@ This release is fully compatible with previous eclair versions. You don't need t ## Changelog - \ No newline at end of file + diff --git a/eclair-core/pom.xml b/eclair-core/pom.xml index 01b566c346..0d74be9165 100644 --- a/eclair-core/pom.xml +++ b/eclair-core/pom.xml @@ -21,7 +21,7 @@ fr.acinq.eclair eclair_2.13 - 0.13.0-SNAPSHOT + 0.14.0-SNAPSHOT eclair-core_2.13 @@ -87,8 +87,8 @@ true - https://bitcoincore.org/bin/bitcoin-core-28.1/bitcoin-28.1-x86_64-linux-gnu.tar.gz - 07f77afd326639145b9ba9562912b2ad2ccec47b8a305bd075b4f4cb127b7ed7 + https://bitcoincore.org/bin/bitcoin-core-29.2/bitcoin-29.2-x86_64-linux-gnu.tar.gz + 1fd58d0ae94b8a9e21bbaeab7d53395a44976e82bd5492b0a894826c135f9009 @@ -99,8 +99,8 @@ - https://bitcoincore.org/bin/bitcoin-core-28.1/bitcoin-28.1-x86_64-apple-darwin.tar.gz - c85d1a0ebedeff43b99db2c906b50f14547b84175a4d0ebb039a9809789af280 + https://bitcoincore.org/bin/bitcoin-core-29.2/bitcoin-29.2-x86_64-apple-darwin.tar.gz + 69ca05fbe838123091cf4d6d2675352f36cf55f49e2e6fb3b52fcf32b5e8dd9f @@ -111,8 +111,8 @@ - https://bitcoincore.org/bin/bitcoin-core-28.1/bitcoin-28.1-win64.zip - 2d636ad562b347c96d36870d6ed810f4a364f446ca208258299f41048b35eab0 + https://bitcoincore.org/bin/bitcoin-core-29.2/bitcoin-29.2-win64.zip + 83f90a5bab1fc30849862aa1db88906b91e0730b78993c085f9e547a1c3cce79 diff --git a/eclair-core/src/main/resources/reference.conf b/eclair-core/src/main/resources/reference.conf index a58d0cc735..a6b71251d3 100644 --- a/eclair-core/src/main/resources/reference.conf +++ b/eclair-core/src/main/resources/reference.conf @@ -39,9 +39,11 @@ eclair { // - unlock: eclair will automatically unlock the corresponding utxos // - ignore: eclair will leave these utxos locked and start startup-locked-utxos-behavior = "stop" - final-pubkey-refresh-delay = 3 seconds + final-address-refresh-delay = 3 seconds // If true, eclair will poll bitcoind for 30 seconds at start-up before giving up. wait-for-bitcoind-up = true + // The txid of a transaction that exists in your custom signet network or "" to skip this check. + signet-check-tx = "ff1027486b628b2d160859205a3401fb2ee379b43527153b0b50a92c17ee7955" // coinbase of block #5000 of default signet } node-alias = "eclair" @@ -72,6 +74,7 @@ eclair { option_shutdown_anysegwit = optional option_dual_fund = optional option_quiesce = optional + option_attribution_data = optional option_onion_messages = optional // This feature should only be enabled when acting as an LSP for mobile wallets. // When activating this feature, the peer-storage section should be customized to match desired SLAs. @@ -84,7 +87,7 @@ eclair { // node that you trust using override-init-features (see below). option_zeroconf = disabled keysend = disabled - option_simple_close=optional + option_simple_close = optional trampoline_payment_prototype = disabled async_payment_prototype = disabled on_the_fly_funding = disabled @@ -166,7 +169,7 @@ eclair { // (with the default behavior, it would "only" cause a local force-close of the channel). unhandled-exception-strategy = "local-close" // local-close or stop - revocation-timeout = 20 seconds // after sending a commit_sig, we will wait for at most that duration before disconnecting + revocation-timeout = 60 seconds // after sending a commit_sig, we will wait for at most that duration before disconnecting channel-open-limits { max-pending-channels-per-peer = 3 // maximum number of pending channels we will accept from a given peer @@ -174,8 +177,6 @@ eclair { channel-opener-whitelist = [] // a list of public keys; we will ignore rate limits on pending channels from these peers } - accept-incoming-static-remote-key-channels = false // whether we accept new incoming static_remote_key channels (which are obsolete, nodes should use anchor_output now) - quiescence-timeout = 1 minutes // maximum time we will stay quiescent (or wait to reach quiescence) before disconnecting channel-update { @@ -248,6 +249,18 @@ eclair { // Number of blocks before the incoming HTLC expires that an async payment must be triggered by the receiver cancel-safety-before-timeout-blocks = 144 } + + // We assign reputation to our peers to prioritize payments during congestion. + // The reputation is computed as fees paid divided by what should have been paid if all payments were successful. + peer-reputation { + // Set this parameter to false to disable the reputation algorithm and simply relay the incoming endorsement + // value, as described by https://github.com/lightning/blips/blob/master/blip-0004.md, + enabled = true + // Reputation decays with the following half life to emphasize recent behavior. + half-life = 30 days + // Payments that stay pending for longer than this get penalized. + max-relay-duration = 5 minutes + } } on-chain-fees { @@ -268,9 +281,17 @@ eclair { closing = medium } + // Maximum feerate that will be used when closing channels for outputs that aren't at risk (main balance and HTLC 3rd-stage transactions). + // Using a low value here ensures that you won't be paying high fees when the mempool is congested and you're not in + // a hurry to get your channel funds back. + // If closing transactions don't confirm and you need to get the funds back quickly, you should increase this value + // and restart your node: closing transactions will automatically be RBF-ed to match the current feerate. + // This value is in satoshis per byte. + max-closing-feerate = 10 + feerate-tolerance { - ratio-low = 0.5 // will allow remote fee rates as low as half our local feerate (only enforced when not using anchor outputs) - ratio-high = 10.0 // will allow remote fee rates as high as 10 times our local feerate (for all commitment formats) + ratio-low = 0.5 // will allow remote fee rates as low as half our local feerate for funding/splice transactions + ratio-high = 10.0 // will allow remote fee rates as high as 10 times our local feerate for commitment transactions and funding/splice transactions // when using anchor outputs, we only need to use a commitment feerate that allows the tx to propagate: we will use CPFP to speed up confirmation if needed. // the following value is the maximum feerate we'll use for our commit tx (in sat/byte) anchor-output-max-commit-feerate = 10 @@ -374,15 +395,15 @@ eclair { } peer-connection { - auth-timeout = 15 seconds // will disconnect if connection authentication doesn't happen within that timeframe - init-timeout = 15 seconds // will disconnect if initialization doesn't happen within that timeframe + auth-timeout = 30 seconds // will disconnect if connection authentication doesn't happen within that timeframe + init-timeout = 30 seconds // will disconnect if initialization doesn't happen within that timeframe ping-interval = 30 seconds - ping-timeout = 20 seconds // will disconnect if peer takes longer than that to respond + ping-timeout = 60 seconds // will disconnect if peer takes longer than that to respond ping-disconnect = true // disconnect if no answer to our pings // When enabled, if we receive an incoming connection, we will echo the source IP address in our init message. // This should be disabled if your node is behind a load balancer that doesn't preserve source IP addresses. send-remote-address-init = true - max-no-channels = 250 // maximum number of incoming connections from peers that do not have any channels with us + max-no-channels = 64 // maximum number of incoming connections from peers that do not have any channels with us } // When relaying payments or messages to mobile peers who are disconnected, we may try to wake them up using a mobile @@ -404,6 +425,9 @@ eclair { router { watch-spent-window = 60 minutes // at startup watches on public channels will be put back within that window to reduce herd effect; must be > 0s + // when we detect that a remote channel has been spent on-chain, we wait for 72 blocks before removing it from the graph + // if this was a splice instead of a close, we will be able to simply update the channel in our graph and keep its reputation + channel-spent-splice-delay = 72 channel-exclude-duration = 60 seconds // when a temporary channel failure is returned, we exclude the channel from our payment routes for this duration broadcast-interval = 60 seconds // see BOLT #7 @@ -432,6 +456,12 @@ eclair { default { randomize-route-selection = true // when computing a route for a payment we randomize the final selection + mpp { + min-amount-satoshis = 15000 // minimum amount sent via partial HTLCs + max-parts = 5 // maximum number of HTLCs sent per payment: increasing this value will impact performance + splitting-strategy = "randomize" // Can be either "full-capacity", "randomize" or "max-expected-amount" + } + boundaries { max-route-length = 6 // max route length for the 'first pass', if none is found then a second pass is made with no limit max-cltv = 2016 // max acceptable cltv expiry for the payment (2016 ~ 2 weeks) @@ -440,16 +470,6 @@ eclair { max-fee-proportional-percent = 3 // that's 3% } - use-ratios = true // if false, will use failure-cost - // channel 'weight' is computed with the following formula: (channelFee + hop-cost) * (ratio-base + cltvDelta * ratio-cltv + channelAge * ratio-channel-age + channelCapacity * ratio-channel-capacity) - // the following parameters can be used to ask the router to use heuristics to find i.e: 'cltv-optimized' routes, **the sum of the four ratios must be 1** - ratios { - base = 0.0 - cltv = 0.05 // when computing the weight for a channel, consider its CLTV delta in this proportion - channel-age = 0.4 // when computing the weight for a channel, consider its AGE in this proportion - channel-capacity = 0.55 // when computing the weight for a channel, consider its CAPACITY in this proportion - } - hop-cost { // virtual fee for additional hops: how much you are willing to pay to get one less hop in the payment path fee-base-msat = 500 @@ -460,7 +480,6 @@ eclair { // 1e-8 corresponds to an interest rate of ~5% per year (1e-6 per block) and a probability of 1% that the channel will fail and our funds will be locked. // virtual fee for failed payments: how much you are willing to pay to get one less failed payment attempt - // ignored if use-ratio = true failure-cost { fee-base-msat = 2000 fee-proportional-millionths = 500 @@ -471,10 +490,10 @@ eclair { // probability of success, however is penalizes less the paths with a low probability of success. use-log-probability = false - mpp { - min-amount-satoshis = 15000 // minimum amount sent via partial HTLCs - max-parts = 5 // maximum number of HTLCs sent per payment: increasing this value will impact performance - } + // When set to false, we assume that the balances of channels are uniformly distributed. That means the + // probability that we can send an amount X through a channel of capacity C is 1 - X / C. + // When set to true, past payments attempts will be used to compute a better estimate. + use-past-relay-data = false } // The path-finding algo uses one or more sets of parameters named experiments. Each experiment has a percentage @@ -517,7 +536,7 @@ eclair { } db { - driver = "sqlite" // sqlite, postgres, dual-sqlite-primary, dual-postgres-primary + driver = "sqlite" // sqlite, postgres postgres { database = "eclair" host = "localhost" @@ -559,10 +578,6 @@ eclair { } } } - dual { - migrate-on-restart = false // migrate sqlite -> postgres on restart (only applies if sqlite is primary) - compare-on-restart = false // compare sqlite and postgres dbs on restart (only applies if sqlite is primary) - } // During normal channel operation, we need to store information about past HTLCs to be able to punish our peer if // they publish a revoked commitment. Once a channel closes or a splice transaction confirms, we can clean up past // data (which reduces the size of our DB). Since there may be millions of rows to delete and we don't want to slow diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/DBChecker.scala b/eclair-core/src/main/scala/fr/acinq/eclair/DBChecker.scala index 4222767777..2a6c9b8b26 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/DBChecker.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/DBChecker.scala @@ -32,11 +32,15 @@ object DBChecker extends Logging { Try(nodeParams.db.channels.listLocalChannels()) match { case Success(channels) => channels.foreach { - case data: ChannelDataWithCommitments if !data.commitments.validateSeed(nodeParams.channelKeyManager) => throw InvalidChannelSeedException(data.channelId) + case data: ChannelDataWithCommitments => + val channelKeys = nodeParams.channelKeyManager.channelKeys(data.channelParams.channelConfig, data.channelParams.localParams.fundingKeyPath) + if (!data.commitments.validateSeed(channelKeys)) { + throw InvalidChannelSeedException(data.channelId) + } case _ => () } channels - case Failure(_) => throw IncompatibleDBException + case Failure(t) => throw IncompatibleDBException(t) } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala index 2310e35cef..1c7ec9a859 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala @@ -96,15 +96,15 @@ trait Eclair { def rbfOpen(channelId: ByteVector32, targetFeerate: FeeratePerKw, fundingFeeBudget: Satoshi, lockTime_opt: Option[Long])(implicit timeout: Timeout): Future[CommandResponse[CMD_BUMP_FUNDING_FEE]] - def spliceIn(channelId: ByteVector32, amountIn: Satoshi, pushAmount_opt: Option[MilliSatoshi])(implicit timeout: Timeout): Future[CommandResponse[CMD_SPLICE]] + def spliceIn(channelId: ByteVector32, amountIn: Satoshi, pushAmount_opt: Option[MilliSatoshi], channelType_opt: Option[ChannelType])(implicit timeout: Timeout): Future[CommandResponse[CMD_SPLICE]] - def spliceOut(channelId: ByteVector32, amountOut: Satoshi, scriptOrAddress: Either[ByteVector, String])(implicit timeout: Timeout): Future[CommandResponse[CMD_SPLICE]] + def spliceOut(channelId: ByteVector32, amountOut: Satoshi, scriptOrAddress: Either[ByteVector, String], channelType_opt: Option[ChannelType])(implicit timeout: Timeout): Future[CommandResponse[CMD_SPLICE]] def rbfSplice(channelId: ByteVector32, targetFeerate: FeeratePerKw, fundingFeeBudget: Satoshi, lockTime_opt: Option[Long])(implicit timeout: Timeout): Future[CommandResponse[CMD_BUMP_FUNDING_FEE]] def close(channels: List[ApiTypes.ChannelIdentifier], scriptPubKey_opt: Option[ByteVector], closingFeerates_opt: Option[ClosingFeerates])(implicit timeout: Timeout): Future[Map[ApiTypes.ChannelIdentifier, Either[Throwable, CommandResponse[CMD_CLOSE]]]] - def forceClose(channels: List[ApiTypes.ChannelIdentifier])(implicit timeout: Timeout): Future[Map[ApiTypes.ChannelIdentifier, Either[Throwable, CommandResponse[CMD_FORCECLOSE]]]] + def forceClose(channels: List[ApiTypes.ChannelIdentifier], maxClosingFeerate_opt: Option[FeeratePerKw])(implicit timeout: Timeout): Future[Map[ApiTypes.ChannelIdentifier, Either[Throwable, CommandResponse[CMD_FORCECLOSE]]]] def forceCloseResetFundingIndex(channel: ApiTypes.ChannelIdentifier, resetFundingTxIndex: Int)(implicit timeout: Timeout): Future[CommandResponse[CMD_FORCECLOSE]] @@ -116,7 +116,7 @@ trait Eclair { def channelInfo(channel: ApiTypes.ChannelIdentifier)(implicit timeout: Timeout): Future[CommandResponse[CMD_GET_CHANNEL_INFO]] - def closedChannels(nodeId_opt: Option[PublicKey], paginated_opt: Option[Paginated])(implicit timeout: Timeout): Future[Iterable[RES_GET_CHANNEL_INFO]] + def closedChannels(nodeId_opt: Option[PublicKey], paginated_opt: Option[Paginated])(implicit timeout: Timeout): Future[Iterable[DATA_CLOSED]] def peers()(implicit timeout: Timeout): Future[Iterable[PeerInfo]] @@ -244,7 +244,7 @@ class EclairImpl(val appKit: Kit) extends Eclair with Logging with SpendFromChan fundingAmount = fundingAmount, channelType_opt = channelType_opt, pushAmount_opt = pushAmount_opt, - fundingTxFeerate_opt = fundingFeerate_opt.map(FeeratePerKw(_)), + fundingTxFeerate_opt = fundingFeerate_opt.map(_.perKw), fundingTxFeeBudget_opt = Some(fundingFeeBudget), requestFunding_opt = None, channelFlags_opt = announceChannel_opt.map(announceChannel => ChannelFlags(announceChannel = announceChannel)), @@ -260,15 +260,15 @@ class EclairImpl(val appKit: Kit) extends Eclair with Logging with SpendFromChan ) } - override def spliceIn(channelId: ByteVector32, amountIn: Satoshi, pushAmount_opt: Option[MilliSatoshi])(implicit timeout: Timeout): Future[CommandResponse[CMD_SPLICE]] = { + override def spliceIn(channelId: ByteVector32, amountIn: Satoshi, pushAmount_opt: Option[MilliSatoshi], channelType_opt: Option[ChannelType])(implicit timeout: Timeout): Future[CommandResponse[CMD_SPLICE]] = { val spliceIn = SpliceIn(additionalLocalFunding = amountIn, pushAmount = pushAmount_opt.getOrElse(0.msat)) sendToChannelTyped( channel = Left(channelId), - cmdBuilder = CMD_SPLICE(_, spliceIn_opt = Some(spliceIn), spliceOut_opt = None, requestFunding_opt = None) + cmdBuilder = CMD_SPLICE(_, spliceIn_opt = Some(spliceIn), spliceOut_opt = None, requestFunding_opt = None, channelType_opt = channelType_opt) ) } - override def spliceOut(channelId: ByteVector32, amountOut: Satoshi, scriptOrAddress: Either[ByteVector, String])(implicit timeout: Timeout): Future[CommandResponse[CMD_SPLICE]] = { + override def spliceOut(channelId: ByteVector32, amountOut: Satoshi, scriptOrAddress: Either[ByteVector, String], channelType_opt: Option[ChannelType])(implicit timeout: Timeout): Future[CommandResponse[CMD_SPLICE]] = { val script = scriptOrAddress match { case Left(script) => script case Right(address) => addressToPublicKeyScript(this.appKit.nodeParams.chainHash, address) match { @@ -279,7 +279,7 @@ class EclairImpl(val appKit: Kit) extends Eclair with Logging with SpendFromChan val spliceOut = SpliceOut(amount = amountOut, scriptPubKey = script) sendToChannelTyped( channel = Left(channelId), - cmdBuilder = CMD_SPLICE(_, spliceIn_opt = None, spliceOut_opt = Some(spliceOut), requestFunding_opt = None) + cmdBuilder = CMD_SPLICE(_, spliceIn_opt = None, spliceOut_opt = Some(spliceOut), requestFunding_opt = None, channelType_opt = channelType_opt) ) } @@ -294,8 +294,8 @@ class EclairImpl(val appKit: Kit) extends Eclair with Logging with SpendFromChan sendToChannels(channels, CMD_CLOSE(ActorRef.noSender, scriptPubKey_opt, closingFeerates_opt)) } - override def forceClose(channels: List[ApiTypes.ChannelIdentifier])(implicit timeout: Timeout): Future[Map[ApiTypes.ChannelIdentifier, Either[Throwable, CommandResponse[CMD_FORCECLOSE]]]] = { - sendToChannels(channels, CMD_FORCECLOSE(ActorRef.noSender)) + override def forceClose(channels: List[ApiTypes.ChannelIdentifier], maxClosingFeerate_opt: Option[FeeratePerKw])(implicit timeout: Timeout): Future[Map[ApiTypes.ChannelIdentifier, Either[Throwable, CommandResponse[CMD_FORCECLOSE]]]] = { + sendToChannels(channels, CMD_FORCECLOSE(ActorRef.noSender, maxClosingFeerate_opt)) } override def forceCloseResetFundingIndex(channel: ApiTypes.ChannelIdentifier, resetFundingTxIndex: Int)(implicit timeout: Timeout): Future[CommandResponse[CMD_FORCECLOSE]] = { @@ -347,11 +347,9 @@ class EclairImpl(val appKit: Kit) extends Eclair with Logging with SpendFromChan sendToChannelTyped(channel = channel, cmdBuilder = CMD_GET_CHANNEL_INFO(_)) } - override def closedChannels(nodeId_opt: Option[PublicKey], paginated_opt: Option[Paginated])(implicit timeout: Timeout): Future[Iterable[RES_GET_CHANNEL_INFO]] = { + override def closedChannels(nodeId_opt: Option[PublicKey], paginated_opt: Option[Paginated])(implicit timeout: Timeout): Future[Iterable[DATA_CLOSED]] = { Future { - appKit.nodeParams.db.channels.listClosedChannels(nodeId_opt, paginated_opt).map { data => - RES_GET_CHANNEL_INFO(nodeId = data.remoteNodeId, channelId = data.channelId, channel = ActorRef.noSender, state = CLOSED, data = data) - } + appKit.nodeParams.db.channels.listClosedChannels(nodeId_opt, paginated_opt) } } @@ -440,7 +438,7 @@ class EclairImpl(val appKit: Kit) extends Eclair with Logging with SpendFromChan if (blocks < 3) appKit.nodeParams.currentBitcoinCoreFeerates.fast else if (blocks > 6) appKit.nodeParams.currentBitcoinCoreFeerates.slow else appKit.nodeParams.currentBitcoinCoreFeerates.medium - case Right(feeratePerByte) => FeeratePerKw(feeratePerByte) + case Right(feeratePerByte) => feeratePerByte.perKw } appKit.wallet match { case w: BitcoinCoreClient => @@ -454,7 +452,7 @@ class EclairImpl(val appKit: Kit) extends Eclair with Logging with SpendFromChan override def cpfpBumpFees(targetFeeratePerByte: FeeratePerByte, outpoints: Set[OutPoint]): Future[TxId] = { appKit.wallet match { - case w: BitcoinCoreClient => w.cpfp(outpoints, FeeratePerKw(targetFeeratePerByte)).map(_.txid) + case w: BitcoinCoreClient => w.cpfp(outpoints, targetFeeratePerByte.perKw).map(_.txid) case _ => Future.failed(new IllegalArgumentException("this call is only available with a bitcoin core backend")) } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala index b7b60e287c..12e5127208 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala @@ -36,17 +36,15 @@ object FeatureSupport { /** Not a sealed trait, so it can be extended by plugins. */ trait Feature { - def rfcName: String def mandatory: Int def optional: Int = mandatory + 1 - def supportBit(support: FeatureSupport): Int = support match { case Mandatory => mandatory case Optional => optional } - override def toString = rfcName + override def toString: String = rfcName } /** Feature scope as defined in Bolt 9. */ @@ -68,11 +66,9 @@ trait Bolt12Feature extends InvoiceFeature */ trait PermanentChannelFeature extends InitFeature // <- not in the spec /** - * Permanent channel feature negotiated in the channel type. Those features take precedence over permanent channel - * features negotiated in init messages. For example, if the channel type is option_static_remotekey, then even if - * the option_anchor_outputs feature is supported by both peers, it won't apply to the channel. + * Features that can be included in the [[fr.acinq.eclair.wire.protocol.ChannelTlv.ChannelTypeTlv]]. */ -trait ChannelTypeFeature extends PermanentChannelFeature +trait ChannelTypeFeature extends InitFeature // @formatter:on case class UnknownFeature(bitIndex: Int) @@ -259,6 +255,7 @@ object Features { val mandatory = 26 } + // Note that this is a permanent channel feature because it permanently affects the channel reserve, which is set at 1%. case object DualFunding extends Feature with InitFeature with NodeFeature with PermanentChannelFeature { val rfcName = "option_dual_fund" val mandatory = 28 @@ -270,6 +267,11 @@ object Features { val mandatory = 34 } + case object AttributionData extends Feature with InitFeature with NodeFeature with Bolt11Feature { + val rfcName = "option_attribution_data" + val mandatory = 36 + } + case object OnionMessages extends Feature with InitFeature with NodeFeature { val rfcName = "option_onion_messages" val mandatory = 38 @@ -285,7 +287,7 @@ object Features { val mandatory = 44 } - case object ScidAlias extends Feature with InitFeature with NodeFeature with ChannelTypeFeature { + case object ScidAlias extends Feature with InitFeature with NodeFeature with ChannelTypeFeature with PermanentChannelFeature { val rfcName = "option_scid_alias" val mandatory = 46 } @@ -295,7 +297,7 @@ object Features { val mandatory = 48 } - case object ZeroConf extends Feature with InitFeature with NodeFeature with ChannelTypeFeature { + case object ZeroConf extends Feature with InitFeature with NodeFeature with ChannelTypeFeature with PermanentChannelFeature { val rfcName = "option_zeroconf" val mandatory = 50 } @@ -310,6 +312,11 @@ object Features { val mandatory = 60 } + case object PhoenixZeroReserve extends Feature with InitFeature with ChannelTypeFeature with PermanentChannelFeature { + val rfcName = "phoenix_zero_reserve" + val mandatory = 128 + } + /** This feature bit indicates that the node is a mobile wallet that can be woken up via push notifications. */ case object WakeUpNotificationClient extends Feature with InitFeature { val rfcName = "wake_up_notification_client" @@ -339,6 +346,16 @@ object Features { val mandatory = 154 } + case object SimpleTaprootChannelsPhoenix extends Feature with InitFeature with NodeFeature with ChannelTypeFeature { + val rfcName = "option_simple_taproot_phoenix" + val mandatory = 564 + } + + case object SimpleTaprootChannelsStaging extends Feature with InitFeature with NodeFeature with ChannelTypeFeature { + val rfcName = "option_simple_taproot_staging" + val mandatory = 180 + } + /** * Activate this feature to provide on-the-fly funding to remote nodes, as specified in bLIP 36: https://github.com/lightning/blips/blob/master/blip-0036.md. * TODO: add NodeFeature once bLIP is merged. @@ -373,6 +390,7 @@ object Features { ShutdownAnySegwit, DualFunding, Quiescence, + AttributionData, OnionMessages, ProvideStorage, ChannelType, @@ -381,12 +399,15 @@ object Features { ZeroConf, KeySend, SimpleClose, + SimpleTaprootChannelsPhoenix, + SimpleTaprootChannelsStaging, WakeUpNotificationClient, TrampolinePaymentPrototype, AsyncPaymentPrototype, SplicePrototype, OnTheFlyFunding, - FundingFeeCredit + FundingFeeCredit, + PhoenixZeroReserve ) // Features may depend on other features, as specified in Bolt 9. @@ -400,6 +421,7 @@ object Features { TrampolinePaymentPrototype -> (PaymentSecret :: Nil), KeySend -> (VariableLengthOnion :: Nil), SimpleClose -> (ShutdownAnySegwit :: Nil), + SimpleTaprootChannelsPhoenix -> (ChannelType :: SimpleClose :: Nil), AsyncPaymentPrototype -> (TrampolinePaymentPrototype :: Nil), OnTheFlyFunding -> (SplicePrototype :: Nil), FundingFeeCredit -> (OnTheFlyFunding :: Nil) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/MilliSatoshi.scala b/eclair-core/src/main/scala/fr/acinq/eclair/MilliSatoshi.scala index c62933cd8d..100829ce83 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/MilliSatoshi.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/MilliSatoshi.scala @@ -16,7 +16,7 @@ package fr.acinq.eclair -import fr.acinq.bitcoin.scalacompat.{Btc, BtcAmount, MilliBtc, Satoshi, btc2satoshi, millibtc2satoshi} +import fr.acinq.bitcoin.scalacompat.{Btc, BtcAmount, BtcDouble, MilliBtc, Satoshi, btc2satoshi, millibtc2satoshi} /** * Created by t-bast on 22/08/2019. @@ -60,6 +60,8 @@ case class MilliSatoshi(private val underlying: Long) extends Ordered[MilliSatos object MilliSatoshi { + val MaxMoney: MilliSatoshi = toMilliSatoshi(21e6.btc) + private def satoshi2millisatoshi(input: Satoshi): MilliSatoshi = MilliSatoshi(input.toLong * 1000L) def toMilliSatoshi(amount: BtcAmount): MilliSatoshi = amount match { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala index 59d0dfca97..2bee26cadb 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala @@ -21,9 +21,9 @@ import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.bitcoin.scalacompat.{Block, BlockHash, Crypto, Satoshi, SatoshiLong} import fr.acinq.eclair.Setup.Seeds import fr.acinq.eclair.blockchain.fee._ +import fr.acinq.eclair.channel.ChannelFlags import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.channel.fsm.Channel.{BalanceThreshold, ChannelConf, UnhandledExceptionStrategy} -import fr.acinq.eclair.channel.{ChannelFlags, ChannelType, ChannelTypes} import fr.acinq.eclair.crypto.Noise.KeyPair import fr.acinq.eclair.crypto.keymanager.{ChannelKeyManager, NodeKeyManager, OnChainKeyManager} import fr.acinq.eclair.db._ @@ -33,12 +33,13 @@ import fr.acinq.eclair.message.OnionMessages.OnionMessageConfig import fr.acinq.eclair.payment.offer.OffersConfig import fr.acinq.eclair.payment.relay.OnTheFlyFunding import fr.acinq.eclair.payment.relay.Relayer.{AsyncPaymentsParams, RelayFees, RelayParams} +import fr.acinq.eclair.reputation.Reputation import fr.acinq.eclair.router.Announcements.AddressException -import fr.acinq.eclair.router.Graph.{HeuristicsConstants, PaymentWeightRatios} +import fr.acinq.eclair.router.Graph.HeuristicsConstants import fr.acinq.eclair.router.Router._ import fr.acinq.eclair.router.{Graph, PathFindingExperimentConf, Router} import fr.acinq.eclair.tor.Socks5ProxyParams -import fr.acinq.eclair.transactions.Transactions +import fr.acinq.eclair.transactions.Transactions.{PhoenixSimpleTaprootChannelCommitmentFormat, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat, ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat} import fr.acinq.eclair.wire.protocol._ import grizzled.slf4j.Logging import scodec.bits.ByteVector @@ -128,14 +129,17 @@ case class NodeParams(nodeKeyManager: NodeKeyManager, max = (fundingFeerate * feerateTolerance.ratioHigh).max(minimumFeerate), ) // We use the most likely commitment format, even though there is no guarantee that this is the one that will be used. - val commitmentFormat = ChannelTypes.defaultFromFeatures(localFeatures, remoteFeatures, announceChannel = false).commitmentFormat - val commitmentFeerate = onChainFeeConf.getCommitmentFeerate(currentBitcoinCoreFeerates, remoteNodeId, commitmentFormat, channelConf.minFundingPrivateSatoshis) + val commitmentFormat = if (Features.canUseFeature(localFeatures, remoteFeatures, Features.SimpleTaprootChannelsPhoenix)) { + PhoenixSimpleTaprootChannelCommitmentFormat + } else if (Features.canUseFeature(localFeatures, remoteFeatures, Features.SimpleTaprootChannelsStaging)) { + ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat + } else { + ZeroFeeHtlcTxAnchorOutputsCommitmentFormat + } + val commitmentFeerate = onChainFeeConf.getCommitmentFeerate(currentBitcoinCoreFeerates, remoteNodeId, commitmentFormat) val commitmentRange = RecommendedFeeratesTlv.CommitmentFeerateRange( - min = (commitmentFeerate * feerateTolerance.ratioLow).max(minimumFeerate), - max = (commitmentFormat match { - case Transactions.DefaultCommitmentFormat => commitmentFeerate * feerateTolerance.ratioHigh - case _: Transactions.AnchorOutputsCommitmentFormat => (commitmentFeerate * feerateTolerance.ratioHigh).max(feerateTolerance.anchorOutputMaxCommitFeerate) - }).max(minimumFeerate), + min = Seq(commitmentFeerate * feerateTolerance.ratioLow, minimumFeerate).max, + max = Seq(commitmentFeerate * feerateTolerance.ratioHigh, feerateTolerance.anchorOutputMaxCommitFeerate, minimumFeerate).max, ) RecommendedFeerates(chainHash, fundingFeerate, commitmentFeerate, TlvStream(fundingRange, commitmentRange)) } @@ -459,25 +463,20 @@ object NodeParams extends Logging { maxCltv = CltvExpiryDelta(config.getInt("boundaries.max-cltv")), maxFeeFlat = Satoshi(config.getLong("boundaries.max-fee-flat-sat")).toMilliSatoshi, maxFeeProportional = config.getDouble("boundaries.max-fee-proportional-percent") / 100.0), - heuristics = if (config.getBoolean("use-ratios")) { - PaymentWeightRatios( - baseFactor = config.getDouble("ratios.base"), - cltvDeltaFactor = config.getDouble("ratios.cltv"), - ageFactor = config.getDouble("ratios.channel-age"), - capacityFactor = config.getDouble("ratios.channel-capacity"), - hopFees = getRelayFees(config.getConfig("hop-cost")), - ) - } else { - HeuristicsConstants( - lockedFundsRisk = config.getDouble("locked-funds-risk"), - failureFees = getRelayFees(config.getConfig("failure-cost")), - hopFees = getRelayFees(config.getConfig("hop-cost")), - useLogProbability = config.getBoolean("use-log-probability"), - ) - }, + heuristics = HeuristicsConstants( + lockedFundsRisk = config.getDouble("locked-funds-risk"), + failureFees = getRelayFees(config.getConfig("failure-cost")), + hopFees = getRelayFees(config.getConfig("hop-cost")), + useLogProbability = config.getBoolean("use-log-probability"), + usePastRelaysData = config.getBoolean("use-past-relay-data")), mpp = MultiPartParams( Satoshi(config.getLong("mpp.min-amount-satoshis")).toMilliSatoshi, - config.getInt("mpp.max-parts")), + config.getInt("mpp.max-parts"), + config.getString("mpp.splitting-strategy") match { + case "full-capacity" => MultiPartParams.FullCapacity + case "randomize" => MultiPartParams.Randomize + case "max-expected-amount" => MultiPartParams.MaxExpectedAmount + }), experimentName = name, experimentPercentage = config.getInt("percentage")) @@ -603,10 +602,10 @@ object NodeParams extends Logging { quiescenceTimeout = FiniteDuration(config.getDuration("channel.quiescence-timeout").getSeconds, TimeUnit.SECONDS), balanceThresholds = config.getConfigList("channel.channel-update.balance-thresholds").asScala.map(conf => BalanceThreshold(Satoshi(conf.getLong("available-sat")), Satoshi(conf.getLong("max-htlc-sat")))).toSeq, minTimeBetweenUpdates = FiniteDuration(config.getDuration("channel.channel-update.min-time-between-updates").getSeconds, TimeUnit.SECONDS), - acceptIncomingStaticRemoteKeyChannels = config.getBoolean("channel.accept-incoming-static-remote-key-channels") ), onChainFeeConf = OnChainFeeConf( feeTargets = feeTargets, + maxClosingFeerate = FeeratePerByte(Satoshi(config.getLong("on-chain-fees.max-closing-feerate"))).perKw, safeUtxosThreshold = config.getInt("on-chain-fees.safe-utxos-threshold"), spendAnchorWithoutHtlcs = config.getBoolean("on-chain-fees.spend-anchor-without-htlcs"), anchorWithoutHtlcsMaxFee = Satoshi(config.getLong("on-chain-fees.anchor-without-htlcs-max-fee-satoshis")), @@ -615,7 +614,7 @@ object NodeParams extends Logging { defaultFeerateTolerance = FeerateTolerance( config.getDouble("on-chain-fees.feerate-tolerance.ratio-low"), config.getDouble("on-chain-fees.feerate-tolerance.ratio-high"), - FeeratePerKw(FeeratePerByte(Satoshi(config.getLong("on-chain-fees.feerate-tolerance.anchor-output-max-commit-feerate")))), + FeeratePerByte(Satoshi(config.getLong("on-chain-fees.feerate-tolerance.anchor-output-max-commit-feerate"))).perKw, DustTolerance( Satoshi(config.getLong("on-chain-fees.feerate-tolerance.dust-tolerance.max-exposure-satoshis")), config.getBoolean("on-chain-fees.feerate-tolerance.dust-tolerance.close-on-update-fee-overflow") @@ -626,7 +625,7 @@ object NodeParams extends Logging { val tolerance = FeerateTolerance( e.getDouble("feerate-tolerance.ratio-low"), e.getDouble("feerate-tolerance.ratio-high"), - FeeratePerKw(FeeratePerByte(Satoshi(e.getLong("feerate-tolerance.anchor-output-max-commit-feerate")))), + FeeratePerByte(Satoshi(e.getLong("feerate-tolerance.anchor-output-max-commit-feerate"))).perKw, DustTolerance( Satoshi(e.getLong("feerate-tolerance.dust-tolerance.max-exposure-satoshis")), e.getBoolean("feerate-tolerance.dust-tolerance.close-on-update-fee-overflow") @@ -640,7 +639,12 @@ object NodeParams extends Logging { privateChannelFees = getRelayFees(config.getConfig("relay.fees.private-channels")), minTrampolineFees = getRelayFees(config.getConfig("relay.fees.min-trampoline")), enforcementDelay = FiniteDuration(config.getDuration("relay.fees.enforcement-delay").getSeconds, TimeUnit.SECONDS), - asyncPaymentsParams = AsyncPaymentsParams(asyncPaymentHoldTimeoutBlocks, asyncPaymentCancelSafetyBeforeTimeoutBlocks) + asyncPaymentsParams = AsyncPaymentsParams(asyncPaymentHoldTimeoutBlocks, asyncPaymentCancelSafetyBeforeTimeoutBlocks), + peerReputationConfig = Reputation.Config( + enabled = config.getBoolean("relay.peer-reputation.enabled"), + halfLife = FiniteDuration(config.getDuration("relay.peer-reputation.half-life").getSeconds, TimeUnit.SECONDS), + maxRelayDuration = FiniteDuration(config.getDuration("relay.peer-reputation.max-relay-duration").getSeconds, TimeUnit.SECONDS), + ), ), db = database, autoReconnect = config.getBoolean("auto-reconnect"), @@ -663,6 +667,7 @@ object NodeParams extends Logging { ), routerConf = RouterConf( watchSpentWindow = watchSpentWindow, + channelSpentSpliceDelay = config.getInt("router.channel-spent-splice-delay"), channelExcludeDuration = FiniteDuration(config.getDuration("router.channel-exclude-duration").getSeconds, TimeUnit.SECONDS), routerBroadcastInterval = FiniteDuration(config.getDuration("router.broadcast-interval").getSeconds, TimeUnit.SECONDS), syncConf = Router.SyncConf( diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/PluginParams.scala b/eclair-core/src/main/scala/fr/acinq/eclair/PluginParams.scala index 18032a6e32..ddc9705a3b 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/PluginParams.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/PluginParams.scala @@ -20,7 +20,7 @@ import akka.actor.typed.ActorRef import akka.event.LoggingAdapter import fr.acinq.bitcoin.scalacompat.{ByteVector32, Satoshi} import fr.acinq.eclair.channel.Origin -import fr.acinq.eclair.io.OpenChannelInterceptor.{DefaultParams, OpenChannelNonInitiator} +import fr.acinq.eclair.io.OpenChannelInterceptor.OpenChannelNonInitiator import fr.acinq.eclair.payment.relay.PostRestartHtlcCleaner.IncomingHtlc import fr.acinq.eclair.wire.protocol.{Error, LiquidityAds} @@ -61,13 +61,13 @@ trait CustomCommitmentsPlugin extends PluginParams { // @formatter:off trait InterceptOpenChannelCommand -case class InterceptOpenChannelReceived(replyTo: ActorRef[InterceptOpenChannelResponse], openChannelNonInitiator: OpenChannelNonInitiator, defaultParams: DefaultParams) extends InterceptOpenChannelCommand { +case class InterceptOpenChannelReceived(replyTo: ActorRef[InterceptOpenChannelResponse], openChannelNonInitiator: OpenChannelNonInitiator) extends InterceptOpenChannelCommand { val remoteFundingAmount: Satoshi = openChannelNonInitiator.open.fold(_.fundingSatoshis, _.fundingAmount) val temporaryChannelId: ByteVector32 = openChannelNonInitiator.open.fold(_.temporaryChannelId, _.temporaryChannelId) } sealed trait InterceptOpenChannelResponse -case class AcceptOpenChannel(temporaryChannelId: ByteVector32, defaultParams: DefaultParams, addFunding_opt: Option[LiquidityAds.AddFunding]) extends InterceptOpenChannelResponse +case class AcceptOpenChannel(temporaryChannelId: ByteVector32, addFunding_opt: Option[LiquidityAds.AddFunding]) extends InterceptOpenChannelResponse case class RejectOpenChannel(temporaryChannelId: ByteVector32, error: Error) extends InterceptOpenChannelResponse // @formatter:on diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala index 19d16af68d..0038338984 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala @@ -45,6 +45,7 @@ import fr.acinq.eclair.payment.offer.{DefaultOfferHandler, OfferManager} import fr.acinq.eclair.payment.receive.PaymentHandler import fr.acinq.eclair.payment.relay.{AsyncPaymentTriggerer, PostRestartHtlcCleaner, Relayer} import fr.acinq.eclair.payment.send.{Autoprobe, PaymentInitiator} +import fr.acinq.eclair.reputation.ReputationRecorder import fr.acinq.eclair.router._ import fr.acinq.eclair.tor.{Controller, TorProtocolHandler} import fr.acinq.eclair.wire.protocol.NodeAddress @@ -98,11 +99,18 @@ class Setup(val datadir: File, val config = system.settings.config.getConfig("eclair") val Seeds(nodeSeed, channelSeed) = seeds_opt.getOrElse(NodeParams.getSeeds(datadir)) val chain = config.getString("chain") + val chainCheckTx = chain match { + case "mainnet" => Some("2157b554dcfda405233906e461ee593875ae4b1b97615872db6a25130ecc1dd6") // coinbase of #500000 + case "testnet" => Some("8f38a0dd41dc0ae7509081e262d791f8d53ed6f884323796d5ec7b0966dd3825") // coinbase of #1500000 + case "testnet4" => Some("5c50d460b3b98ea0c70baa0f50d1f0cc6ffa553788b4a7e23918bcdd558828fa") // coinbase of #40000 + case "signet" => if (config.hasPath("bitcoind.signet-check-tx") && config.getString("bitcoind.signet-check-tx").nonEmpty) Some(config.getString("bitcoind.signet-check-tx")) else None + case "regtest" => None + } val chaindir = new File(datadir, chain) chaindir.mkdirs() - val nodeKeyManager = new LocalNodeKeyManager(nodeSeed, NodeParams.hashFromChain(chain)) - val channelKeyManager = new LocalChannelKeyManager(channelSeed, NodeParams.hashFromChain(chain)) + val nodeKeyManager = LocalNodeKeyManager(nodeSeed, NodeParams.hashFromChain(chain)) + val channelKeyManager = LocalChannelKeyManager(channelSeed, NodeParams.hashFromChain(chain)) /** * This counter holds the current blockchain height. @@ -157,12 +165,9 @@ class Setup(val datadir: File, .filter(value => (value \ "spendable").extract[Boolean]) .map(value => (value \ "address").extract[String]) } - _ <- chain match { - case "mainnet" => bitcoinClient.invoke("getrawtransaction", "2157b554dcfda405233906e461ee593875ae4b1b97615872db6a25130ecc1dd6") // coinbase of #500000 - case "testnet" => bitcoinClient.invoke("getrawtransaction", "8f38a0dd41dc0ae7509081e262d791f8d53ed6f884323796d5ec7b0966dd3825") // coinbase of #1500000 - case "testnet4" => bitcoinClient.invoke("getrawtransaction", "5c50d460b3b98ea0c70baa0f50d1f0cc6ffa553788b4a7e23918bcdd558828fa") // coinbase of #40000 - case "signet" => bitcoinClient.invoke("getrawtransaction", "ff1027486b628b2d160859205a3401fb2ee379b43527153b0b50a92c17ee7955") // coinbase of #5000 - case "regtest" => Future.successful(()) + _ <- chainCheckTx match { + case Some(txid) => bitcoinClient.invoke("getrawtransaction", txid) + case None => Future.successful(()) } } yield BitcoinStatus(bitcoinVersion, chainHash, ibd, progress, blocks, headers, unspentAddresses) @@ -190,7 +195,7 @@ class Setup(val datadir: File, await(getBitcoinStatus(bitcoinClient), 30 seconds, "bitcoind did not respond after 30 seconds") } logger.info(s"bitcoind version=${bitcoinStatus.version}") - assert(bitcoinStatus.version >= 280100, "Eclair requires Bitcoin Core 28.1 or higher") + assert(bitcoinStatus.version >= 290000, "Eclair requires Bitcoin Core 29 or higher") bitcoinStatus.unspentAddresses.foreach { address => val isSegwit = addressToPublicKeyScript(bitcoinStatus.chainHash, address).map(script => Script.isNativeWitnessScript(script)).getOrElse(false) assert(isSegwit, s"Your wallet contains non-segwit UTXOs (e.g. address=$address). You must send those UTXOs to a segwit address to use Eclair (check out our README for more details).") @@ -232,11 +237,11 @@ class Setup(val datadir: File, defaultFeerates = { val confDefaultFeerates = FeeratesPerKB( - minimum = FeeratePerKB(FeeratePerByte(Satoshi(config.getLong("on-chain-fees.default-feerates.minimum")))), - slow = FeeratePerKB(FeeratePerByte(Satoshi(config.getLong("on-chain-fees.default-feerates.slow")))), - medium = FeeratePerKB(FeeratePerByte(Satoshi(config.getLong("on-chain-fees.default-feerates.medium")))), - fast = FeeratePerKB(FeeratePerByte(Satoshi(config.getLong("on-chain-fees.default-feerates.fast")))), - fastest = FeeratePerKB(FeeratePerByte(Satoshi(config.getLong("on-chain-fees.default-feerates.fastest")))), + minimum = FeeratePerByte(Satoshi(config.getLong("on-chain-fees.default-feerates.minimum"))).perKB, + slow = FeeratePerByte(Satoshi(config.getLong("on-chain-fees.default-feerates.slow"))).perKB, + medium = FeeratePerByte(Satoshi(config.getLong("on-chain-fees.default-feerates.medium"))).perKB, + fast = FeeratePerByte(Satoshi(config.getLong("on-chain-fees.default-feerates.fast"))).perKB, + fastest = FeeratePerByte(Satoshi(config.getLong("on-chain-fees.default-feerates.fastest"))).perKB, ) feeratesPerKw.set(FeeratesPerKw(confDefaultFeerates)) confDefaultFeerates @@ -263,9 +268,8 @@ class Setup(val datadir: File, }) _ <- feeratesRetrieved.future - finalPubkey = new AtomicReference[PublicKey](null) finalPubkeyScript = new AtomicReference[Seq[ScriptElt]](null) - pubkeyRefreshDelay = FiniteDuration(config.getDuration("bitcoind.final-pubkey-refresh-delay").getSeconds, TimeUnit.SECONDS) + finalAddressRefreshDelay = FiniteDuration(config.getDuration("bitcoind.final-address-refresh-delay").getSeconds, TimeUnit.SECONDS) // there are 3 possibilities regarding onchain key management: // 1) there is no `eclair-signer.conf` file in Eclair's data directory, Eclair will not manage Bitcoin core keys, and Eclair's API will not return bitcoin core descriptors. This is the default mode. // 2) there is an `eclair-signer.conf` file in Eclair's data directory, but the name of the wallet set in `eclair-signer.conf` does not match the `eclair.bitcoind.wallet` setting in `eclair.conf`. @@ -273,14 +277,8 @@ class Setup(val datadir: File, // This is how you would create a new bitcoin wallet whose private keys are managed by Eclair. // 3) there is an `eclair-signer.conf` file in Eclair's data directory, and the name of the wallet set in `eclair-signer.conf` matches the `eclair.bitcoind.wallet` setting in `eclair.conf`. // Eclair will assume that this is a watch-only bitcoin wallet that has been created from descriptors generated by Eclair, and will manage its private keys, and here we pass the onchain key manager to our bitcoin client. - bitcoinClient = new BitcoinCoreClient(bitcoin, nodeParams.liquidityAdsConfig.lockUtxos, if (bitcoin.wallet == onChainKeyManager_opt.map(_.walletName)) onChainKeyManager_opt else None) with OnChainPubkeyCache { - val refresher: typed.ActorRef[OnChainAddressRefresher.Command] = system.spawn(Behaviors.supervise(OnChainAddressRefresher(this, finalPubkey, finalPubkeyScript, pubkeyRefreshDelay)).onFailure(typed.SupervisorStrategy.restart), name = "onchain-address-manager") - - override def getP2wpkhPubkey(renew: Boolean): PublicKey = { - val key = finalPubkey.get() - if (renew) refresher ! OnChainAddressRefresher.RenewPubkey - key - } + bitcoinClient = new BitcoinCoreClient(bitcoin, nodeParams.liquidityAdsConfig.lockUtxos, if (bitcoin.wallet == onChainKeyManager_opt.map(_.walletName)) onChainKeyManager_opt else None) with OnChainAddressCache { + val refresher: typed.ActorRef[OnChainAddressRefresher.Command] = system.spawn(Behaviors.supervise(OnChainAddressRefresher(this, finalPubkeyScript, finalAddressRefreshDelay)).onFailure(typed.SupervisorStrategy.restart), name = "on-chain-address-manager") override def getReceivePublicKeyScript(renew: Boolean): Seq[ScriptElt] = { val script = finalPubkeyScript.get() @@ -289,8 +287,6 @@ class Setup(val datadir: File, } } _ = if (bitcoinClient.useEclairSigner) logger.info("using eclair to sign bitcoin core transactions") - initialPubkey <- bitcoinClient.getP2wpkhPubkey() - _ = finalPubkey.set(initialPubkey) // We use the default address type configured on the Bitcoin Core node. initialPubkeyScript <- bitcoinClient.getReceivePublicKeyScript(addressType_opt = None) _ = finalPubkeyScript.set(initialPubkeyScript) @@ -375,7 +371,12 @@ class Setup(val datadir: File, paymentHandler = system.actorOf(SimpleSupervisor.props(PaymentHandler.props(nodeParams, register, offerManager), "payment-handler", SupervisorStrategy.Resume)) triggerer = system.spawn(Behaviors.supervise(AsyncPaymentTriggerer()).onFailure(typed.SupervisorStrategy.resume), name = "async-payment-triggerer") peerReadyManager = system.spawn(Behaviors.supervise(PeerReadyManager()).onFailure(typed.SupervisorStrategy.restart), name = "peer-ready-manager") - relayer = system.actorOf(SimpleSupervisor.props(Relayer.props(nodeParams, router, register, paymentHandler, Some(postRestartCleanUpInitialized)), "relayer", SupervisorStrategy.Resume)) + reputationRecorder_opt = if (nodeParams.relayParams.peerReputationConfig.enabled) { + Some(system.spawn(Behaviors.supervise(ReputationRecorder(nodeParams.relayParams.peerReputationConfig)).onFailure(typed.SupervisorStrategy.resume), name = "reputation-recorder")) + } else { + None + } + relayer = system.actorOf(SimpleSupervisor.props(Relayer.props(nodeParams, router, register, paymentHandler, reputationRecorder_opt, Some(postRestartCleanUpInitialized)), "relayer", SupervisorStrategy.Resume)) _ = relayer ! PostRestartHtlcCleaner.Init(channels) // Before initializing the switchboard (which re-connects us to the network) and the user-facing parts of the system, // we want to make sure the handler for post-restart broken HTLCs has finished initializing. @@ -394,7 +395,7 @@ class Setup(val datadir: File, _ = for (i <- 0 until config.getInt("autoprobe-count")) yield system.actorOf(SimpleSupervisor.props(Autoprobe.props(nodeParams, router, paymentInitiator), s"payment-autoprobe-$i", SupervisorStrategy.Restart)) - balanceActor = system.spawn(BalanceActor(nodeParams.db, bitcoinClient, channelsListener, nodeParams.balanceCheckInterval), name = "balance-actor") + balanceActor = system.spawn(BalanceActor(bitcoinClient, nodeParams.channelConf.minDepth, channelsListener, nodeParams.balanceCheckInterval), name = "balance-actor") postman = system.spawn(Behaviors.supervise(Postman(nodeParams, switchboard, router.toTyped, register, offerManager)).onFailure(typed.SupervisorStrategy.restart), name = "postman") @@ -484,7 +485,7 @@ case class Kit(nodeParams: NodeParams, postman: typed.ActorRef[Postman.Command], offerManager: typed.ActorRef[OfferManager.Command], defaultOfferHandler: typed.ActorRef[OfferManager.HandlerCommand], - wallet: OnChainWallet with OnChainPubkeyCache) + wallet: OnChainWallet with OnChainAddressCache) object Kit { @@ -508,7 +509,7 @@ case class BitcoinWalletNotLoadedException(wallet: String, loaded: List[String]) case object EmptyAPIPasswordException extends RuntimeException("must set a password for the json-rpc api") -case object IncompatibleDBException extends RuntimeException("database is not compatible with this version of eclair") +case class IncompatibleDBException(t: Throwable) extends RuntimeException(s"database is not compatible with this version of eclair: ${t.getMessage}") case object IncompatibleNetworkDBException extends RuntimeException("network database is not compatible with this version of eclair") diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/SpendFromChannelAddress.scala b/eclair-core/src/main/scala/fr/acinq/eclair/SpendFromChannelAddress.scala index 8f799def24..5196498093 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/SpendFromChannelAddress.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/SpendFromChannelAddress.scala @@ -3,9 +3,9 @@ package fr.acinq.eclair import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.bitcoin.scalacompat.{ByteVector64, DeterministicWallet, OutPoint, Satoshi, SatoshiLong, Script, ScriptWitness, Transaction, TxIn, TxOut, addressToPublicKeyScript} import fr.acinq.eclair.blockchain.fee.FeeratePerKw -import fr.acinq.eclair.transactions.Scripts.multiSig2of2 +import fr.acinq.eclair.channel.{ChannelConfig, ChannelSpendSignature} +import fr.acinq.eclair.transactions.Transactions._ import fr.acinq.eclair.transactions.{Scripts, Transactions} -import fr.acinq.eclair.transactions.Transactions.{DefaultCommitmentFormat, InputInfo, PlaceHolderPubKey, PlaceHolderSig, TxOwner} import scodec.bits.ByteVector import scala.concurrent.Future @@ -14,9 +14,6 @@ trait SpendFromChannelAddress { this: EclairImpl => - /** these dummy witnesses are used as a placeholder to accurately compute the weight */ - private val dummy2of2Witness = Scripts.witness2of2(PlaceHolderSig, PlaceHolderSig, PlaceHolderPubKey, PlaceHolderPubKey) - private def buildTx(outPoint: OutPoint, outputAmount: Satoshi, pubKeyScript: ByteVector, witness: ScriptWitness) = Transaction(2, txIn = Seq(TxIn(outPoint, ByteVector.empty, 0, witness)), txOut = Seq(TxOut(outputAmount, pubKeyScript)), @@ -27,12 +24,13 @@ trait SpendFromChannelAddress { inputTx <- appKit.wallet.getTransaction(outPoint.txid) inputAmount = inputTx.txOut(outPoint.index.toInt).amount Right(pubKeyScript) = addressToPublicKeyScript(appKit.nodeParams.chainHash, address).map(Script.write) + channelKeys = appKit.nodeParams.channelKeyManager.channelKeys(ChannelConfig.standard, fundingKeyPath) + localFundingPubkey = channelKeys.fundingKey(fundingTxIndex).publicKey // build the tx a first time with a zero amount to compute the weight - fee = Transactions.weight2fee(feerate, buildTx(outPoint, 0.sat, pubKeyScript, dummy2of2Witness).weight()) - _ = assert(inputAmount - fee > Transactions.dustLimit(pubKeyScript), s"amount insufficient (fee=$fee)") - unsignedTx = buildTx(outPoint, inputAmount - fee, pubKeyScript, dummy2of2Witness) - // the following are not used, but need to be sent to the counterparty - localFundingPubkey = appKit.nodeParams.channelKeyManager.fundingPublicKey(fundingKeyPath, fundingTxIndex).publicKey + dummyWitness = Scripts.witness2of2(PlaceHolderSig, PlaceHolderSig, localFundingPubkey, localFundingPubkey) + fee = Transactions.weight2fee(feerate, buildTx(outPoint, 0.sat, pubKeyScript, dummyWitness).weight()) + _ = assert(inputAmount - fee > Scripts.dustLimit(pubKeyScript), s"amount insufficient (fee=$fee)") + unsignedTx = buildTx(outPoint, inputAmount - fee, pubKeyScript, dummyWitness) } yield SpendFromChannelPrep(fundingTxIndex, localFundingPubkey, inputAmount, unsignedTx) } @@ -41,18 +39,13 @@ trait SpendFromChannelAddress { _ <- Future.successful(()) outPoint = unsignedTx.txIn.head.outPoint inputTx <- appKit.wallet.getTransaction(outPoint.txid) - localFundingPubkey = appKit.nodeParams.channelKeyManager.fundingPublicKey(fundingKeyPath, fundingTxIndex) - fundingRedeemScript = multiSig2of2(localFundingPubkey.publicKey, remoteFundingPubkey) - inputInfo = InputInfo(outPoint, inputTx.txOut(outPoint.index.toInt), fundingRedeemScript) - localSig = appKit.nodeParams.channelKeyManager.sign( - Transactions.SpliceTx(inputInfo, unsignedTx), // classify as splice, doesn't really matter - localFundingPubkey, - TxOwner.Local, // unused - DefaultCommitmentFormat, // unused - Map.empty - ) - witness = Scripts.witness2of2(localSig, remoteSig, localFundingPubkey.publicKey, remoteFundingPubkey) - signedTx = unsignedTx.updateWitness(0, witness) + channelKeys = appKit.nodeParams.channelKeyManager.channelKeys(ChannelConfig.standard, fundingKeyPath) + localFundingKey = channelKeys.fundingKey(fundingTxIndex) + inputInfo = InputInfo(outPoint, inputTx.txOut(outPoint.index.toInt)) + // classify as splice, doesn't really matter + tx = Transactions.SpliceTx(inputInfo, unsignedTx) + localSig = tx.sign(localFundingKey, remoteFundingPubkey, extraUtxos = Map.empty) + signedTx = tx.aggregateSigs(localFundingKey.publicKey, remoteFundingPubkey, localSig, ChannelSpendSignature.IndividualSignature(remoteSig)) } yield SpendFromChannelResult(signedTx) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/balance/BalanceActor.scala b/eclair-core/src/main/scala/fr/acinq/eclair/balance/BalanceActor.scala index cc3237378e..667ba1c257 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/balance/BalanceActor.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/balance/BalanceActor.scala @@ -1,3 +1,19 @@ +/* + * Copyright 2024 ACINQ SAS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package fr.acinq.eclair.balance import akka.actor.typed.eventstream.EventStream @@ -7,12 +23,11 @@ import fr.acinq.bitcoin.scalacompat.{Btc, ByteVector32, SatoshiLong} import fr.acinq.eclair.NotificationsLogger import fr.acinq.eclair.NotificationsLogger.NotifyNodeOperator import fr.acinq.eclair.balance.BalanceActor._ -import fr.acinq.eclair.balance.CheckBalance.{GlobalBalance, computeOffChainBalance} +import fr.acinq.eclair.balance.CheckBalance.{GlobalBalance, OffChainBalance} import fr.acinq.eclair.balance.Monitoring.{Metrics, Tags} import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinCoreClient import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinCoreClient.Utxo import fr.acinq.eclair.channel.PersistentChannelData -import fr.acinq.eclair.db.Databases import scala.concurrent.ExecutionContext import scala.concurrent.duration._ @@ -29,11 +44,11 @@ object BalanceActor { private final case class WrappedGlobalBalanceWithChannels(wrapped: Try[GlobalBalance], channelsCount: Int) extends Command // @formatter:on - def apply(db: Databases, bitcoinClient: BitcoinCoreClient, channelsListener: ActorRef[ChannelsListener.GetChannels], interval: FiniteDuration)(implicit ec: ExecutionContext): Behavior[Command] = { + def apply(bitcoinClient: BitcoinCoreClient, minDepth: Int, channelsListener: ActorRef[ChannelsListener.GetChannels], interval: FiniteDuration)(implicit ec: ExecutionContext): Behavior[Command] = { Behaviors.setup { context => Behaviors.withTimers { timers => timers.startTimerWithFixedDelay(TickBalance, interval) - new BalanceActor(context, db, bitcoinClient, channelsListener).apply(refBalance_opt = None, previousBalance_opt = None) + new BalanceActor(context, bitcoinClient, minDepth, channelsListener).apply(refBalance_opt = None, previousBalance_opt = None) } } } @@ -41,14 +56,14 @@ object BalanceActor { } private class BalanceActor(context: ActorContext[Command], - db: Databases, bitcoinClient: BitcoinCoreClient, + minDepth: Int, channelsListener: ActorRef[ChannelsListener.GetChannels])(implicit ec: ExecutionContext) { private val log = context.log /** - * @param refBalance_opt the reference balance computed once at startup, useful for telling if we are making or losing money overall + * @param refBalance_opt the reference balance computed once at startup, useful for telling if we are making or losing money overall * @param previousBalance_opt the last computed balance, it is useful to make a detailed diff between two successive balance checks * @return */ @@ -65,7 +80,7 @@ private class BalanceActor(context: ActorContext[Command], Behaviors.same case WrappedChannels(res) => val channelsCount = res.channels.size - context.pipeToSelf(CheckBalance.computeGlobalBalance(res.channels, db, bitcoinClient))(b => WrappedGlobalBalanceWithChannels(b, channelsCount)) + context.pipeToSelf(CheckBalance.computeGlobalBalance(res.channels, bitcoinClient, minDepth))(b => WrappedGlobalBalanceWithChannels(b, channelsCount)) Behaviors.same case WrappedGlobalBalanceWithChannels(res, channelsCount) => res match { @@ -89,44 +104,45 @@ private class BalanceActor(context: ActorContext[Command], } previousBalance_opt match { case Some(previousBalance) => + // On-chain metrics: log.info("on-chain diff={}", balance.onChain.total - previousBalance.onChain.total) - val utxosBefore = previousBalance.onChain.confirmed ++ previousBalance.onChain.unconfirmed - val utxosAfter = balance.onChain.confirmed ++ balance.onChain.unconfirmed - val utxosAdded = utxosAfter -- utxosBefore.keys - val utxosRemoved = utxosBefore -- utxosAfter.keys + val utxosBefore = previousBalance.onChain.utxos.map(utxo => utxo.outPoint -> utxo).toMap + val utxosAfter = balance.onChain.utxos.map(utxo => utxo.outPoint -> utxo).toMap + val utxosAdded = (utxosAfter -- utxosBefore.keys).values + val utxosRemoved = (utxosBefore -- utxosAfter.keys).values utxosAdded - .toList.sortBy(-_._2) - .foreach { case (outPoint, amount) => log.info("+ utxo={} amount={}", outPoint, amount) } + .toList.sortBy(_.amount) + .foreach(utxo => log.info("+ utxo={} amount={}", utxo.outPoint, utxo.amount)) utxosRemoved - .toList.sortBy(-_._2) - .foreach { case (outPoint, amount) => log.info("- utxo={} amount={}", outPoint, amount) } - + .toList.sortBy(_.amount) + .foreach(utxo => log.info("- utxo={} amount={}", utxo.outPoint, utxo.amount)) + // Off-chain metrics: log.info("off-chain diff={}", balance.offChain.total - previousBalance.offChain.total) - val offchainBalancesBefore = previousBalance.channels.view.mapValues(computeOffChainBalance(previousBalance.knownPreimages, _).total) - val offchainBalancesAfter = balance.channels.view.mapValues(computeOffChainBalance(balance.knownPreimages, _).total) - offchainBalancesAfter - .map { case (channelId, balanceAfter) => (channelId, balanceAfter - offchainBalancesBefore.getOrElse(channelId, Btc(0))) } + val offChainBalancesBefore = previousBalance.channels.view.mapValues(channel => OffChainBalance().addChannelBalance(channel, previousBalance.onChain.recentlySpentInputs).total) + val offChainBalancesAfter = balance.channels.view.mapValues(channel => OffChainBalance().addChannelBalance(channel, balance.onChain.recentlySpentInputs).total) + offChainBalancesAfter + .map { case (channelId, balanceAfter) => (channelId, balanceAfter - offChainBalancesBefore.getOrElse(channelId, Btc(0))) } .filter { case (_, balanceDiff) => balanceDiff > 0.sat } .toList.sortBy(-_._2) .foreach { case (channelId, balanceDiff) => log.info("+ channelId={} amount={}", channelId, balanceDiff) } - offchainBalancesBefore - .map { case (channelId, balanceBefore) => (channelId, balanceBefore - offchainBalancesAfter.getOrElse(channelId, Btc(0))) } + offChainBalancesBefore + .map { case (channelId, balanceBefore) => (channelId, balanceBefore - offChainBalancesAfter.getOrElse(channelId, Btc(0))) } .filter { case (_, balanceDiff) => balanceDiff > 0.sat } .toList.sortBy(-_._2) .foreach { case (channelId, balanceDiff) => log.info("- channelId={} amount={}", channelId, balanceDiff) } case None => () } - - log.info("current balance: total={} onchain.confirmed={} onchain.unconfirmed={} offchain={}", balance.total.toDouble, balance.onChain.totalConfirmed.toDouble, balance.onChain.totalUnconfirmed.toDouble, balance.offChain.total.toDouble) + log.info("current balance: total={} onchain.deeply-confirmed={} onchain.recently-confirmed={} onchain.unconfirmed={} offchain={}", balance.total.toDouble, balance.onChain.totalDeeplyConfirmed.toDouble, balance.onChain.totalRecentlyConfirmed.toDouble, balance.onChain.totalUnconfirmed.toDouble, balance.offChain.total.toDouble) log.debug("current balance details: {}", balance) // This is a very rough estimation of the fee we would need to pay for a force-close with 5 pending HTLCs at 100 sat/byte. val perChannelFeeBumpingReserve = 50_000.sat // Instead of scaling this linearly with the number of channels we have, we use sqrt(channelsCount) to reflect // the fact that if you have channels with many peers, only a subset of these peers will likely be malicious. val estimatedFeeBumpingReserve = perChannelFeeBumpingReserve * Math.sqrt(channelsCount) - if (balance.onChain.totalConfirmed < estimatedFeeBumpingReserve) { + val totalConfirmedBalance = balance.onChain.totalDeeplyConfirmed + balance.onChain.totalRecentlyConfirmed + if (totalConfirmedBalance < estimatedFeeBumpingReserve) { val message = - s"""On-chain confirmed balance is low (${balance.onChain.totalConfirmed.toMilliBtc}): eclair may not be able to guarantee funds safety in case channels force-close. + s"""On-chain confirmed balance is low (${totalConfirmedBalance.toMilliBtc}): eclair may not be able to guarantee funds safety in case channels force-close. |You have $channelsCount channels, which could cost $estimatedFeeBumpingReserve in fees if some of these channels are malicious. |Please note that the value above is a very arbitrary estimation: the real cost depends on the feerate and the number of malicious channels. |You should add more utxos to your bitcoin wallet to guarantee funds safety. @@ -134,16 +150,15 @@ private class BalanceActor(context: ActorContext[Command], context.system.eventStream ! EventStream.Publish(NotifyNodeOperator(NotificationsLogger.Warning, message)) } Metrics.GlobalBalance.withoutTags().update(balance.total.toMilliBtc.toDouble) - Metrics.GlobalBalanceDetailed.withTag(Tags.BalanceType, Tags.BalanceTypes.OnchainConfirmed).update(balance.onChain.totalConfirmed.toMilliBtc.toDouble) - Metrics.GlobalBalanceDetailed.withTag(Tags.BalanceType, Tags.BalanceTypes.OnchainUnconfirmed).update(balance.onChain.totalUnconfirmed.toMilliBtc.toDouble) - Metrics.GlobalBalanceDetailed.withTag(Tags.BalanceType, Tags.BalanceTypes.Offchain).withTag(Tags.OffchainState, Tags.OffchainStates.waitForFundingConfirmed).update(balance.offChain.waitForFundingConfirmed.toMilliBtc.toDouble) - Metrics.GlobalBalanceDetailed.withTag(Tags.BalanceType, Tags.BalanceTypes.Offchain).withTag(Tags.OffchainState, Tags.OffchainStates.waitForChannelReady).update(balance.offChain.waitForChannelReady.toMilliBtc.toDouble) - Metrics.GlobalBalanceDetailed.withTag(Tags.BalanceType, Tags.BalanceTypes.Offchain).withTag(Tags.OffchainState, Tags.OffchainStates.normal).update(balance.offChain.normal.total.toMilliBtc.toDouble) - Metrics.GlobalBalanceDetailed.withTag(Tags.BalanceType, Tags.BalanceTypes.Offchain).withTag(Tags.OffchainState, Tags.OffchainStates.shutdown).update(balance.offChain.shutdown.total.toMilliBtc.toDouble) - Metrics.GlobalBalanceDetailed.withTag(Tags.BalanceType, Tags.BalanceTypes.Offchain).withTag(Tags.OffchainState, Tags.OffchainStates.closingLocal).update(balance.offChain.closing.localCloseBalance.total.toMilliBtc.toDouble) - Metrics.GlobalBalanceDetailed.withTag(Tags.BalanceType, Tags.BalanceTypes.Offchain).withTag(Tags.OffchainState, Tags.OffchainStates.closingRemote).update(balance.offChain.closing.remoteCloseBalance.total.toMilliBtc.toDouble) - Metrics.GlobalBalanceDetailed.withTag(Tags.BalanceType, Tags.BalanceTypes.Offchain).withTag(Tags.OffchainState, Tags.OffchainStates.closingUnknown).update(balance.offChain.closing.unknownCloseBalance.total.toMilliBtc.toDouble) - Metrics.GlobalBalanceDetailed.withTag(Tags.BalanceType, Tags.BalanceTypes.Offchain).withTag(Tags.OffchainState, Tags.OffchainStates.waitForPublishFutureCommitment).update(balance.offChain.waitForPublishFutureCommitment.toMilliBtc.toDouble) + Metrics.GlobalBalanceDetailed.withTag(Tags.BalanceType, Tags.BalanceTypes.OnChainDeeplyConfirmed).update(balance.onChain.totalDeeplyConfirmed.toMilliBtc.toDouble) + Metrics.GlobalBalanceDetailed.withTag(Tags.BalanceType, Tags.BalanceTypes.OnChainRecentlyConfirmed).update(balance.onChain.totalRecentlyConfirmed.toMilliBtc.toDouble) + Metrics.GlobalBalanceDetailed.withTag(Tags.BalanceType, Tags.BalanceTypes.OnChainUnconfirmed).update(balance.onChain.totalUnconfirmed.toMilliBtc.toDouble) + Metrics.GlobalBalanceDetailed.withTag(Tags.BalanceType, Tags.BalanceTypes.OffChain).withTag(Tags.OffChainState, Tags.OffChainStates.waitForFundingConfirmed).update(balance.offChain.waitForFundingConfirmed.toMilliBtc.toDouble) + Metrics.GlobalBalanceDetailed.withTag(Tags.BalanceType, Tags.BalanceTypes.OffChain).withTag(Tags.OffChainState, Tags.OffChainStates.waitForChannelReady).update(balance.offChain.waitForChannelReady.toMilliBtc.toDouble) + Metrics.GlobalBalanceDetailed.withTag(Tags.BalanceType, Tags.BalanceTypes.OffChain).withTag(Tags.OffChainState, Tags.OffChainStates.normal).update(balance.offChain.normal.total.toMilliBtc.toDouble) + Metrics.GlobalBalanceDetailed.withTag(Tags.BalanceType, Tags.BalanceTypes.OffChain).withTag(Tags.OffChainState, Tags.OffChainStates.shutdown).update(balance.offChain.shutdown.total.toMilliBtc.toDouble) + Metrics.GlobalBalanceDetailed.withTag(Tags.BalanceType, Tags.BalanceTypes.OffChain).withTag(Tags.OffChainState, Tags.OffChainStates.closing).update(balance.offChain.closing.total.toMilliBtc.toDouble) + Metrics.GlobalBalanceDetailed.withTag(Tags.BalanceType, Tags.BalanceTypes.OffChain).withTag(Tags.OffChainState, Tags.OffChainStates.waitForPublishFutureCommitment).update(balance.offChain.waitForPublishFutureCommitment.toMilliBtc.toDouble) refBalance_opt match { case Some(refBalance) => val normalizedValue = 100 + (if (refBalance.total.toSatoshi.toLong > 0) (balance.total.toSatoshi.toLong - refBalance.total.toSatoshi.toLong) * 1000D / refBalance.total.toSatoshi.toLong else 0) @@ -162,7 +177,7 @@ private class BalanceActor(context: ActorContext[Command], Behaviors.same } case GetGlobalBalance(replyTo, channels) => - CheckBalance.computeGlobalBalance(channels, db, bitcoinClient) onComplete (replyTo ! _) + CheckBalance.computeGlobalBalance(channels, bitcoinClient, minDepth) onComplete (replyTo ! _) Behaviors.same } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/balance/CheckBalance.scala b/eclair-core/src/main/scala/fr/acinq/eclair/balance/CheckBalance.scala index c76aa0bc19..b67b14e0e7 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/balance/CheckBalance.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/balance/CheckBalance.scala @@ -1,76 +1,157 @@ +/* + * Copyright 2024 ACINQ SAS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package fr.acinq.eclair.balance -import fr.acinq.bitcoin.scalacompat.{Btc, ByteVector32, OutPoint, Satoshi, SatoshiLong, Script, TxId} +import fr.acinq.bitcoin.scalacompat.{BlockId, Btc, ByteVector32, KotlinUtils, OutPoint, Satoshi, SatoshiLong} import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinCoreClient import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinCoreClient.Utxo import fr.acinq.eclair.channel.Helpers.Closing -import fr.acinq.eclair.channel.Helpers.Closing.{CurrentRemoteClose, LocalClose, NextRemoteClose, RemoteClose} +import fr.acinq.eclair.channel.Helpers.Closing._ import fr.acinq.eclair.channel._ -import fr.acinq.eclair.db.Databases -import fr.acinq.eclair.transactions.DirectedHtlc.{incoming, outgoing} -import fr.acinq.eclair.transactions.Transactions -import fr.acinq.eclair.transactions.Transactions._ -import fr.acinq.eclair.wire.protocol.{UpdateAddHtlc, UpdateFulfillHtlc} +import fr.acinq.eclair.transactions.DirectedHtlc.incoming +import fr.acinq.eclair.wire.protocol.UpdateAddHtlc import scala.concurrent.{ExecutionContext, Future} +import scala.jdk.CollectionConverters.CollectionHasAsScala object CheckBalance { /** - * Helper to avoid accidental deduplication caused by the [[Set]] - * Amounts are truncated to the [[Satoshi]] because that is what would happen on-chain. + * Helper to avoid accidental deduplication caused by the [[Set]]. + * Amounts are truncated to [[Satoshi]] because that is what would happen on-chain. */ implicit class HtlcsWithSum(htlcs: Set[UpdateAddHtlc]) { def sumAmount: Satoshi = htlcs.toList.map(_.amountMsat.truncateToSatoshi).sum } - /** if local has preimage of an incoming htlc, then we know it will get the funds */ - private def localHasPreimage(c: CommitmentChanges, htlcId: Long): Boolean = { - c.localChanges.all.collectFirst { case u: UpdateFulfillHtlc if u.id == htlcId => true }.isDefined - } - - /** if remote proved it had the preimage of an outgoing htlc, then we know it won't timeout */ - private def remoteHasPreimage(c: CommitmentChanges, htlcId: Long): Boolean = { - c.remoteChanges.all.collectFirst { case u: UpdateFulfillHtlc if u.id == htlcId => true }.isDefined - } + private def mainBalance(commit: LocalCommit): Satoshi = commit.spec.toLocal.truncateToSatoshi /** - * For more fine-grained analysis, we count the in-flight amounts separately from the main amounts. + * For more fine-grained analysis, we count the in-flight HTLC amounts separately from the main amounts. * - * The base assumption regarding htlcs is that they will all timeout. That means that we ignore incoming htlcs (except - * if we know the preimage), and we count outgoing htlcs in our balance. + * We assume that pending htlcs will all be fulfilled and thus count incoming HTLCs in our balance. + * When HTLCs are relayed, the upstream and downstream channels will cancel each other, because the HTLC is added to + * our balance in the upstream channel and deduced from our balance in the downstream channel (minus fees). + * + * If an HTLC is failed downstream, the failure is immediately propagated to the upstream channel (even if it is in + * the middle of a force-close): the HTLC amount is thus added back to our balance in the downstream channel and + * removed from the upstream channel, so it correctly cancels itself in the global balance. If the downstream channel + * is force-closing, the HTLC will be considered failed only when the HTLC-timeout transaction is confirmed, at which + * point we relay the failure upstream: the HTLC amount is removed from the upstream channel and is added to our + * on-chain balance in the closing downstream channel. */ - case class MainAndHtlcBalance(toLocal: Btc = 0.sat, htlcs: Btc = 0.sat) { + case class MainAndHtlcBalance(toLocal: Btc = 0 sat, htlcs: Btc = 0 sat) { val total: Btc = toLocal + htlcs - } - /** - * In the closing state some transactions may be published or even confirmed. They will be taken into account if we - * do a `bitcoin-cli getbalance` and we don't want to count them twice. - * - * That's why we keep track of the id of each transaction that pays us any amount. It allows us to double check from - * bitcoin core and remove any published transaction. - */ - case class PossiblyPublishedMainBalance(toLocal: Map[OutPoint, Btc] = Map.empty) { - val total: Btc = toLocal.values.map(_.toSatoshi).sum - } + def addChannelBalance(commitments: Commitments): MainAndHtlcBalance = { + // We take the last commitment into account: it's the most likely to (eventually) confirm. + MainAndHtlcBalance( + toLocal = this.toLocal + mainBalance(commitments.latest.localCommit), + htlcs = this.htlcs + commitments.latest.localCommit.spec.htlcs.collect(incoming).sumAmount + ) + } - case class PossiblyPublishedMainAndHtlcBalance(toLocal: Map[OutPoint, Btc] = Map.empty, htlcs: Map[OutPoint, Btc] = Map.empty, htlcsUnpublished: Btc = 0.sat) { - private val totalToLocal: Btc = toLocal.values.map(_.toSatoshi).sum - private val totalHtlcs: Btc = htlcs.values.map(_.toSatoshi).sum - val total: Btc = totalToLocal + totalHtlcs + htlcsUnpublished - } + /** Add our balance for a confirmed local close. */ + def addLocalClose(lcp: LocalCommitPublished, recentlySpentInputs: Set[OutPoint]): MainAndHtlcBalance = { + // If our main transaction isn't confirmed or in the mempool yet, we count it in our off-chain balance. + // Once it confirms or appears in the mempool, it will be included in our on-chain balance, so we ignore it in our off-chain balance. + val additionalToLocal = lcp.localOutput_opt match { + case Some(outpoint) if !lcp.irrevocablySpent.contains(outpoint) && !recentlySpentInputs.contains(outpoint) => lcp.commitTx.txOut(outpoint.index.toInt).amount + case _ => 0 sat + } + val additionalHtlcs = lcp.htlcOutputs.map { outpoint => + val htlcAmount = lcp.commitTx.txOut(outpoint.index.toInt).amount + lcp.irrevocablySpent.get(outpoint) match { + case Some(spendingTx) => + // If the HTLC was spent by us, there will be an entry in our 3rd-stage transactions. + // Otherwise it was spent by the remote and we don't have anything to add to our balance. + val delayedHtlcOutpoint = OutPoint(spendingTx.txid, 0) + val htlcSpentByUs = lcp.htlcDelayedOutputs.contains(delayedHtlcOutpoint) + // If our 3rd-stage transaction isn't confirmed yet, we should count it in our off-chain balance. + // Once confirmed or seen in the mempool, we should ignore it since it will appear in our on-chain balance. + val htlcDelayedPending = !lcp.irrevocablySpent.contains(delayedHtlcOutpoint) + if (htlcSpentByUs && htlcDelayedPending && !recentlySpentInputs.contains(delayedHtlcOutpoint)) htlcAmount else 0 sat + case None => + // We assume that HTLCs will be fulfilled, so we only count incoming HTLCs in our off-chain balance. + if (lcp.incomingHtlcs.contains(outpoint)) htlcAmount else 0 sat + } + }.sum + MainAndHtlcBalance(toLocal = toLocal + additionalToLocal, htlcs = htlcs + additionalHtlcs) + } - /** - * Unless they got evicted, mutual close transactions will also appear in the on-chain balance and will disappear - * from here after on pruning. - */ - case class ClosingBalance(localCloseBalance: PossiblyPublishedMainAndHtlcBalance = PossiblyPublishedMainAndHtlcBalance(), - remoteCloseBalance: PossiblyPublishedMainAndHtlcBalance = PossiblyPublishedMainAndHtlcBalance(), - mutualCloseBalance: PossiblyPublishedMainBalance = PossiblyPublishedMainBalance(), - unknownCloseBalance: MainAndHtlcBalance = MainAndHtlcBalance()) { + /** Add our balance for a confirmed remote close. */ + def addRemoteClose(rcp: RemoteCommitPublished, recentlySpentInputs: Set[OutPoint]): MainAndHtlcBalance = { + // If our main transaction isn't confirmed or in the mempool yet, we count it in our off-chain balance. + // Once it confirms or appears in the mempool, it will be included in our on-chain balance, so we ignore it in our off-chain balance. + val additionalToLocal = rcp.localOutput_opt match { + case Some(outpoint) if !rcp.irrevocablySpent.contains(outpoint) && !recentlySpentInputs.contains(outpoint) => rcp.commitTx.txOut(outpoint.index.toInt).amount + case _ => 0 sat + } + // If HTLC transactions are confirmed, they will appear in our on-chain balance if we were the one to claim them. + // We only need to include incoming HTLCs that haven't been claimed yet (since we assume that they will be fulfilled). + // Note that it is their commitment, so incoming/outgoing are inverted. + val additionalHtlcs = rcp.incomingHtlcs.keys.map { + case outpoint if !rcp.irrevocablySpent.contains(outpoint) && !recentlySpentInputs.contains(outpoint) => rcp.commitTx.txOut(outpoint.index.toInt).amount + case _ => 0 sat + }.sum + MainAndHtlcBalance(toLocal = toLocal + additionalToLocal, htlcs = htlcs + additionalHtlcs) + } - val total: Btc = localCloseBalance.total + remoteCloseBalance.total + mutualCloseBalance.total + unknownCloseBalance.total + /** Add our balance for a confirmed revoked close. */ + def addRevokedClose(rvk: RevokedCommitPublished, recentlySpentInputs: Set[OutPoint]): MainAndHtlcBalance = { + // If our main transaction isn't confirmed or in the mempool yet, we count it in our off-chain balance. + // Once it confirms or appears in the mempool, it will be included in our on-chain balance, so we ignore it in our off-chain balance. + // We do the same thing for our main penalty transaction claiming their main output. + val additionalToLocal = rvk.localOutput_opt match { + case Some(outpoint) if !rvk.irrevocablySpent.contains(outpoint) && !recentlySpentInputs.contains(outpoint) => rvk.commitTx.txOut(outpoint.index.toInt).amount + case _ => 0 sat + } + val additionalToRemote = rvk.remoteOutput_opt match { + case Some(outpoint) if !rvk.irrevocablySpent.contains(outpoint) && !recentlySpentInputs.contains(outpoint) => rvk.commitTx.txOut(outpoint.index.toInt).amount + case _ => 0 sat + } + val additionalHtlcs = rvk.htlcOutputs.map(htlcOutpoint => { + val htlcAmount = rvk.commitTx.txOut(htlcOutpoint.index.toInt).amount + rvk.irrevocablySpent.get(htlcOutpoint) match { + case Some(spendingTx) => + // The spending transaction may claim a batch of HTLCs at once, we only look at the current one. + spendingTx.txIn.zipWithIndex.collectFirst { case (txIn, i) if txIn.outPoint == htlcOutpoint => i } match { + case Some(outputIndex) => + // If they managed to get their HTLC transaction confirmed, we published an HTLC-delayed penalty transaction. + val delayedHtlcOutpoint = OutPoint(spendingTx.txid, outputIndex) + val htlcSpentByThem = rvk.htlcDelayedOutputs.contains(delayedHtlcOutpoint) + // If our 3rd-stage transaction isn't confirmed yet, we should count it in our off-chain balance. + // Once confirmed, we should ignore it since it will appear in our on-chain balance. + val htlcDelayedPending = !rvk.irrevocablySpent.contains(delayedHtlcOutpoint) + // Note that if the HTLC output was spent by us, it should appear in our on-chain balance, so we don't + // count it here. + if (htlcSpentByThem && htlcDelayedPending && !recentlySpentInputs.contains(delayedHtlcOutpoint)) htlcAmount else 0 sat + case None => + // This should never happen unless our data is corrupted. + 0 sat + } + // We ignore this HTLC if it's already included in our on-chain balance. + case None if recentlySpentInputs.contains(htlcOutpoint) => 0 sat + // We assume that our penalty transaction will confirm before their HTLC transaction. + case None => htlcAmount + } + }).sum + MainAndHtlcBalance(toLocal = toLocal + additionalToLocal + additionalToRemote, htlcs = htlcs + additionalHtlcs) + } } /** @@ -80,251 +161,140 @@ object CheckBalance { waitForChannelReady: Btc = 0.sat, normal: MainAndHtlcBalance = MainAndHtlcBalance(), shutdown: MainAndHtlcBalance = MainAndHtlcBalance(), - negotiating: Btc = 0.sat, - closing: ClosingBalance = ClosingBalance(), + negotiating: MainAndHtlcBalance = MainAndHtlcBalance(), + closing: MainAndHtlcBalance = MainAndHtlcBalance(), waitForPublishFutureCommitment: Btc = 0.sat) { - val total: Btc = waitForFundingConfirmed + waitForChannelReady + normal.total + shutdown.total + negotiating + closing.total + waitForPublishFutureCommitment - } - - private def updateMainBalance(current: Btc, localCommit: LocalCommit): Btc = { - val toLocal = localCommit.spec.toLocal.truncateToSatoshi - current + toLocal - } - - private def updateMainAndHtlcBalance(current: MainAndHtlcBalance, c: Commitments, knownPreimages: Set[(ByteVector32, Long)]): MainAndHtlcBalance = { - // We take the last commitment into account: it's the most likely to (eventually) confirm. - val commitment = c.latest - val toLocal = commitment.localCommit.spec.toLocal.truncateToSatoshi - // we only count htlcs in if we know the preimage - val htlcIn = commitment.localCommit.spec.htlcs.collect(incoming) - .filter(add => knownPreimages.contains((add.channelId, add.id)) || localHasPreimage(c.changes, add.id)) - .sumAmount - val htlcOut = commitment.localCommit.spec.htlcs.collect(outgoing).sumAmount - current.copy( - toLocal = current.toLocal + toLocal, - htlcs = current.htlcs + htlcIn + htlcOut - ) - } - - private def updatePossiblyPublishedBalance(current: PossiblyPublishedMainAndHtlcBalance, b1: PossiblyPublishedMainAndHtlcBalance): PossiblyPublishedMainAndHtlcBalance = { - current.copy( - toLocal = current.toLocal ++ b1.toLocal, - htlcs = current.htlcs ++ b1.htlcs, - htlcsUnpublished = current.htlcsUnpublished + b1.htlcsUnpublished - ) - } - - def computeLocalCloseBalance(changes: CommitmentChanges, l: LocalClose, originChannels: Map[Long, Origin], knownPreimages: Set[(ByteVector32, Long)]): PossiblyPublishedMainAndHtlcBalance = { - import l._ - val toLocal = localCommitPublished.claimMainDelayedOutputTx.toSeq.map(c => OutPoint(c.tx.txid, 0) -> c.tx.txOut.head.amount.toBtc).toMap - // incoming htlcs for which we have a preimage and the to-local delay has expired: we have published a claim tx that pays directly to our wallet - val htlcsInOnChain = localCommitPublished.htlcTxs.values.flatten.collect { case htlcTx: HtlcSuccessTx => htlcTx } - .filter(htlcTx => localCommitPublished.claimHtlcDelayedTxs.exists(_.input.outPoint.txid == htlcTx.tx.txid)) - .map(_.htlcId) - .toSet - // outgoing htlcs that have timed out and the to-local delay has expired: we have published a claim tx that pays directly to our wallet - val htlcsOutOnChain = localCommitPublished.htlcTxs.values.flatten.collect { case htlcTx: HtlcTimeoutTx => htlcTx } - .filter(htlcTx => localCommitPublished.claimHtlcDelayedTxs.exists(_.input.outPoint.txid == htlcTx.tx.txid)) - .map(_.htlcId) - .toSet - // incoming htlcs for which we have a preimage but we are still waiting for the to-local delay - val htlcIn = localCommit.spec.htlcs.collect(incoming) - .filterNot(htlc => htlcsInOnChain.contains(htlc.id)) // we filter the htlc that already pay us on-chain - .filter(add => knownPreimages.contains((add.channelId, add.id)) || localHasPreimage(changes, add.id)) - .sumAmount - // outgoing htlcs for which remote didn't prove it had the preimage are expected to time out if they were relayed, - // and succeed if they were sent from this node - val htlcOut = localCommit.spec.htlcs.collect(outgoing) - .filterNot(htlc => htlcsOutOnChain.contains(htlc.id)) // we filter the htlc that already pay us on-chain - .filterNot(htlc => originChannels.get(htlc.id).exists(_.upstream.isInstanceOf[Upstream.Local])) - .filterNot(htlc => remoteHasPreimage(changes, htlc.id)) - .sumAmount - // all claim txs have possibly been published - val htlcs = localCommitPublished.claimHtlcDelayedTxs - .map(c => OutPoint(c.tx.txid, 0) -> c.tx.txOut.head.amount.toBtc).toMap - PossiblyPublishedMainAndHtlcBalance( - toLocal = toLocal, - htlcs = htlcs, - htlcsUnpublished = htlcIn + htlcOut - ) - } - - def computeRemoteCloseBalance(c: Commitments, r: RemoteClose, knownPreimages: Set[(ByteVector32, Long)]): PossiblyPublishedMainAndHtlcBalance = { - import r._ - val toLocal = if (c.params.channelFeatures.paysDirectlyToWallet) { - // If static remote key is enabled, the commit tx directly pays to our wallet - // We use the pubkeyscript to retrieve our output - val finalScriptPubKey = Script.write(Script.pay2wpkh(c.params.localParams.walletStaticPaymentBasepoint.get)) - Transactions.findPubKeyScriptIndex(remoteCommitPublished.commitTx, finalScriptPubKey) match { - case Right(outputIndex) => Map(OutPoint(remoteCommitPublished.commitTx.txid, outputIndex) -> remoteCommitPublished.commitTx.txOut(outputIndex).amount.toBtc) - case _ => Map.empty[OutPoint, Btc] // either we don't have an output (below dust), or we have used a non-default pubkey script - } - } else { - remoteCommitPublished.claimMainOutputTx.toSeq.map(c => OutPoint(c.tx.txid, 0) -> c.tx.txOut.head.amount.toBtc).toMap - } - // incoming htlcs for which we have a preimage: we have published a claim tx that pays directly to our wallet - val htlcsInOnChain = remoteCommitPublished.claimHtlcTxs.values.flatten.collect { case htlcTx: ClaimHtlcSuccessTx => htlcTx } - .map(_.htlcId) - .toSet - // outgoing htlcs that have timed out: we have published a claim tx that pays directly to our wallet - val htlcsOutOnChain = remoteCommitPublished.claimHtlcTxs.values.flatten.collect { case htlcTx: ClaimHtlcTimeoutTx => htlcTx } - .map(_.htlcId) - .toSet - // incoming htlcs for which we have a preimage - val htlcIn = remoteCommit.spec.htlcs.collect(outgoing) - .filter(add => knownPreimages.contains((add.channelId, add.id)) || localHasPreimage(c.changes, add.id)) - .filterNot(htlc => htlcsInOnChain.contains(htlc.id)) // we filter the htlc that already pay us on-chain - .sumAmount - // all outgoing htlcs for which remote didn't prove it had the preimage are expected to time out - val htlcOut = remoteCommit.spec.htlcs.collect(incoming) - .filterNot(htlc => htlcsOutOnChain.contains(htlc.id)) // we filter the htlc that already pay us on-chain - .filterNot(htlc => remoteHasPreimage(c.changes, htlc.id)) - .sumAmount - // all claim txs have possibly been published - val htlcs = remoteCommitPublished.claimHtlcTxs.values.flatten - .map(c => OutPoint(c.tx.txid, 0) -> c.tx.txOut.head.amount.toBtc).toMap - PossiblyPublishedMainAndHtlcBalance( - toLocal = toLocal, - htlcs = htlcs, - htlcsUnpublished = htlcIn + htlcOut - ) - } + val total: Btc = waitForFundingConfirmed + waitForChannelReady + normal.total + shutdown.total + negotiating.total + closing.total + waitForPublishFutureCommitment - def computeOffChainBalance(knownPreimages: Set[(ByteVector32, Long)]): (OffChainBalance, PersistentChannelData) => OffChainBalance = { - case (r, d: DATA_WAIT_FOR_FUNDING_CONFIRMED) => r.copy(waitForFundingConfirmed = updateMainBalance(r.waitForFundingConfirmed, d.commitments.latest.localCommit)) - case (r, d: DATA_WAIT_FOR_CHANNEL_READY) => r.copy(waitForChannelReady = updateMainBalance(r.waitForChannelReady, d.commitments.latest.localCommit)) - case (r, _: DATA_WAIT_FOR_DUAL_FUNDING_SIGNED) => r // we ignore our balance from unsigned commitments - case (r, d: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED) => r.copy(waitForFundingConfirmed = updateMainBalance(r.waitForFundingConfirmed, d.commitments.latest.localCommit)) - case (r, d: DATA_WAIT_FOR_DUAL_FUNDING_READY) => r.copy(waitForChannelReady = updateMainBalance(r.waitForChannelReady, d.commitments.latest.localCommit)) - case (r, d: DATA_NORMAL) => r.copy(normal = updateMainAndHtlcBalance(r.normal, d.commitments, knownPreimages)) - case (r, d: DATA_SHUTDOWN) => r.copy(shutdown = updateMainAndHtlcBalance(r.shutdown, d.commitments, knownPreimages)) - case (r, d: DATA_NEGOTIATING) => r.copy(negotiating = updateMainBalance(r.negotiating, d.commitments.latest.localCommit)) - case (r, d: DATA_NEGOTIATING_SIMPLE) => r.copy(negotiating = updateMainBalance(r.negotiating, d.commitments.latest.localCommit)) - case (r, d: DATA_CLOSING) => - Closing.isClosingTypeAlreadyKnown(d) match { - case None if d.mutualClosePublished.nonEmpty && d.localCommitPublished.isEmpty && d.remoteCommitPublished.isEmpty && d.nextRemoteCommitPublished.isEmpty && d.revokedCommitPublished.isEmpty => - // There can be multiple mutual close transactions for the same channel, but most of the time there will - // only be one. We use the last one in the list, which should be the one we have seen last in our local - // mempool. In the worst case scenario, there are several mutual closes and the one that made it to the - // mempool or the chain isn't the one we are keeping track of here. As a consequence the transaction won't - // be pruned and we will count twice the amount in the global (onChain + offChain) balance, until the - // mutual close tx gets deeply confirmed and the channel is removed. - val mutualClose = d.mutualClosePublished.last - val outputInfo_opt = mutualClose.toLocalOutput match { - case Some(outputInfo) => Some(outputInfo) - case None => - // Normally this would mean that we don't actually have an output, but due to a migration - // the data might not be accurate, see [[ChannelTypes0.migrateClosingTx]] - // As a (hackish) workaround, we use the pubkeyscript to retrieve our output - Transactions.findPubKeyScriptIndex(mutualClose.tx, d.finalScriptPubKey) match { - case Right(outputIndex) => Some(OutputInfo(outputIndex, mutualClose.tx.txOut(outputIndex).amount, d.finalScriptPubKey)) - case _ => None // either we don't have an output (below dust), or we have used a non-default pubkey script - } - } - outputInfo_opt match { - case Some(outputInfo) => r.copy(closing = r.closing.copy(mutualCloseBalance = r.closing.mutualCloseBalance.copy(toLocal = r.closing.mutualCloseBalance.toLocal + (OutPoint(mutualClose.tx.txid, outputInfo.index) -> outputInfo.amount)))) - case None => r + def addChannelBalance(channel: PersistentChannelData, recentlySpentInputs: Set[OutPoint]): OffChainBalance = channel match { + case d: DATA_WAIT_FOR_FUNDING_CONFIRMED => this.copy(waitForFundingConfirmed = this.waitForFundingConfirmed + mainBalance(d.commitments.latest.localCommit)) + case d: DATA_WAIT_FOR_CHANNEL_READY => this.copy(waitForChannelReady = this.waitForChannelReady + mainBalance(d.commitments.latest.localCommit)) + case _: DATA_WAIT_FOR_DUAL_FUNDING_SIGNED => this // we ignore our balance from unsigned commitments + case d: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED => this.copy(waitForFundingConfirmed = this.waitForFundingConfirmed + mainBalance(d.commitments.latest.localCommit)) + case d: DATA_WAIT_FOR_DUAL_FUNDING_READY => this.copy(waitForChannelReady = this.waitForChannelReady + mainBalance(d.commitments.latest.localCommit)) + case d: DATA_NORMAL => this.copy(normal = this.normal.addChannelBalance(d.commitments)) + case d: DATA_SHUTDOWN => this.copy(shutdown = this.shutdown.addChannelBalance(d.commitments)) + // If one of our closing transactions is in the mempool or recently confirmed, and thus included in our on-chain + // balance, we ignore this channel in our off-chain balance to avoid counting it twice. + case d: DATA_NEGOTIATING if recentlySpentInputs.contains(d.commitments.latest.fundingInput) => this + case d: DATA_NEGOTIATING_SIMPLE if recentlySpentInputs.contains(d.commitments.latest.fundingInput) => this + // Otherwise, that means the closing transactions aren't in the mempool yet, so we include our off-chain balance. + case d: DATA_NEGOTIATING => this.copy(negotiating = this.negotiating.addChannelBalance(d.commitments)) + case d: DATA_NEGOTIATING_SIMPLE => this.copy(negotiating = this.negotiating.addChannelBalance(d.commitments)) + case d: DATA_CLOSING => + Closing.isClosingTypeAlreadyKnown(d) match { + // A mutual close transaction is confirmed: the channel should transition to the CLOSED state. + // We can ignore it as our channel balance should appear in our on-chain balance. + case Some(_: MutualClose) => this + // A commitment transaction is confirmed: we compute the channel balance that we expect to get back on-chain. + case Some(c: LocalClose) => this.copy(closing = this.closing.addLocalClose(c.localCommitPublished, recentlySpentInputs)) + case Some(c: CurrentRemoteClose) => this.copy(closing = this.closing.addRemoteClose(c.remoteCommitPublished, recentlySpentInputs)) + case Some(c: NextRemoteClose) => this.copy(closing = this.closing.addRemoteClose(c.remoteCommitPublished, recentlySpentInputs)) + case Some(c: RevokedClose) => this.copy(closing = this.closing.addRevokedClose(c.revokedCommitPublished, recentlySpentInputs)) + // In the recovery case, we can only claim our main output, HTLC outputs are lost. + // Once our main transaction confirms, the channel will transition to the CLOSED state and our channel funds + // will appear in our on-chain balance (minus on-chain fees). + case Some(c: RecoveryClose) => c.remoteCommitPublished.localOutput_opt match { + case Some(localOutput) => + val localBalance = c.remoteCommitPublished.commitTx.txOut(localOutput.index.toInt).amount + this.copy(closing = this.closing.copy(toLocal = this.closing.toLocal + localBalance)) + case None => this } - case Some(localClose: LocalClose) => r.copy(closing = r.closing.copy(localCloseBalance = updatePossiblyPublishedBalance(r.closing.localCloseBalance, computeLocalCloseBalance(d.commitments.changes, localClose, d.commitments.originChannels, knownPreimages)))) - case _ if d.remoteCommitPublished.nonEmpty || d.nextRemoteCommitPublished.nonEmpty => - // We have seen the remote commit, it may or may not have been confirmed. We may have published our own - // local commit too, which may take precedence. But if we are aware of the remote commit, it means that - // our bitcoin core has already seen it (since it's the one who told us about it) and we make - // the assumption that the remote commit won't be replaced by our local commit. - val remoteClose = if (d.remoteCommitPublished.isDefined) { - CurrentRemoteClose(d.commitments.latest.remoteCommit, d.remoteCommitPublished.get) - } else { - NextRemoteClose(d.commitments.latest.nextRemoteCommit_opt.get.commit, d.nextRemoteCommitPublished.get) - } - r.copy(closing = r.closing.copy(remoteCloseBalance = updatePossiblyPublishedBalance(r.closing.remoteCloseBalance, computeRemoteCloseBalance(d.commitments, remoteClose, knownPreimages)))) - case _ => r.copy(closing = r.closing.copy(unknownCloseBalance = updateMainAndHtlcBalance(r.closing.unknownCloseBalance, d.commitments, knownPreimages))) - } - case (r, d: DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT) => r.copy(waitForPublishFutureCommitment = updateMainBalance(r.waitForPublishFutureCommitment, d.commitments.latest.localCommit)) - } - - def computeOffChainBalance(knownPreimages: Set[(ByteVector32, Long)], channel: PersistentChannelData): OffChainBalance = { - computeOffChainBalance(knownPreimages)(OffChainBalance(), channel) + // If we have a fully signed mutual close transaction and a closing transaction is in our mempool or recently + // confirmed, the channel will most likely end up being mutual-closed (since the feerate is higher than any + // force-close transaction). We thus ignore this channel in our off-chain balance to avoid counting it twice. + case None if d.mutualClosePublished.nonEmpty && recentlySpentInputs.contains(d.commitments.latest.fundingInput) => this + // We don't know yet which type of closing will confirm on-chain, so we use our default off-chain balance. + case None => this.copy(closing = this.closing.addChannelBalance(d.commitments)) + } + case d: DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT => this.copy(waitForPublishFutureCommitment = this.waitForPublishFutureCommitment + mainBalance(d.commitments.latest.localCommit)) + } } /** - * Compute the overall balance a list of channels. + * Compute the overall balance for a list of channels. * * Assumptions: - * - If the commitment transaction hasn't been published, we simply take our local amount (and htlc amount in states - * where they may exist, namely [[NORMAL]] and [[SHUTDOWN]]). - * - In [[CLOSING]] state: - * - If we know for sure we are in a mutual close scenario, then we don't count the amount, because the tx will - * already have been published. - * - If we know for sure we are in a local, then we take the amounts based on the outputs of - * the transactions, whether delayed or not. This ensures that mining fees are taken into account. - * - If we have detected that a remote commit was published, then we assume the closing type will be remote, even - * it is not yet confirmed. Like for local commits, we take amounts based on outputs of transactions. - * - In the other cases, we simply take our local amount - * - TODO?: we disregard anchor outputs + * - If the funding transaction isn't confirmed yet, we simply take our (future) local amount into account. + * - If the funding transaction is confirmed, we take our main balance and pending HTLCs into account. + * - In the [[CLOSING]] state: while closing transactions are unconfirmed, we use the channel amounts, which don't + * take on-chain fees into account. Once closing transactions confirm, we ignore the corresponding channel amounts, + * the final amounts are included in our on-chain balance, which takes into account the on-chain fees paid. */ - def computeOffChainBalance(channels: Iterable[PersistentChannelData], knownPreimages: Set[(ByteVector32, Long)]): OffChainBalance = { - channels.foldLeft(OffChainBalance()) { - computeOffChainBalance(knownPreimages) - } + def computeOffChainBalance(channels: Iterable[PersistentChannelData], recentlySpentInputs: Set[OutPoint]): OffChainBalance = { + channels.foldLeft(OffChainBalance()) { case (balance, channel) => balance.addChannelBalance(channel, recentlySpentInputs) } } - /** - * Query bitcoin core to prune all amounts related to transactions that have already been published - */ - def prunePublishedTransactions(br: OffChainBalance, bitcoinClient: BitcoinCoreClient)(implicit ec: ExecutionContext): Future[OffChainBalance] = { - for { - txs: Iterable[Option[(TxId, Int)]] <- Future.sequence((br.closing.localCloseBalance.toLocal.keys ++ - br.closing.localCloseBalance.htlcs.keys ++ - br.closing.remoteCloseBalance.toLocal.keys ++ - br.closing.remoteCloseBalance.htlcs.keys ++ - br.closing.mutualCloseBalance.toLocal.keys) - .map(outPoint => bitcoinClient.getTxConfirmations(outPoint.txid).map(_ map { confirmations => outPoint.txid -> confirmations }))) - txMap: Map[TxId, Int] = txs.flatten.toMap - } yield { - br.copy(closing = br.closing.copy( - localCloseBalance = br.closing.localCloseBalance.copy( - toLocal = br.closing.localCloseBalance.toLocal.filterNot { case (outPoint, _) => txMap.contains(outPoint.txid) }, - htlcs = br.closing.localCloseBalance.htlcs.filterNot { case (outPoint, _) => txMap.contains(outPoint.txid) }, - ), - remoteCloseBalance = br.closing.remoteCloseBalance.copy( - toLocal = br.closing.remoteCloseBalance.toLocal.filterNot { case (outPoint, _) => txMap.contains(outPoint.txid) }, - htlcs = br.closing.remoteCloseBalance.htlcs.filterNot { case (outPoint, _) => txMap.contains(outPoint.txid) }, - ), - mutualCloseBalance = br.closing.mutualCloseBalance.copy( - toLocal = br.closing.mutualCloseBalance.toLocal.filterNot { case (outPoint, _) => txMap.contains(outPoint.txid) }, - ) - )) - } - } - - case class DetailedOnChainBalance(confirmed: Map[OutPoint, Btc] = Map.empty, unconfirmed: Map[OutPoint, Btc] = Map.empty, utxos: Seq[Utxo]) { - val totalConfirmed: Btc = confirmed.values.map(_.toSatoshi).sum + case class DetailedOnChainBalance(deeplyConfirmed: Map[OutPoint, Btc] = Map.empty, recentlyConfirmed: Map[OutPoint, Btc] = Map.empty, unconfirmed: Map[OutPoint, Btc] = Map.empty, utxos: Seq[Utxo], recentlySpentInputs: Set[OutPoint]) { + val totalDeeplyConfirmed: Btc = deeplyConfirmed.values.map(_.toSatoshi).sum + val totalRecentlyConfirmed: Btc = recentlyConfirmed.values.map(_.toSatoshi).sum val totalUnconfirmed: Btc = unconfirmed.values.map(_.toSatoshi).sum - val total: Btc = totalConfirmed + totalUnconfirmed + val total: Btc = totalDeeplyConfirmed + totalRecentlyConfirmed + totalUnconfirmed } /** - * Returns the on-chain balance, but discards the unconfirmed incoming swap-in transactions, because they may be RBF-ed. - * Confirmed swap-in transactions are counted, because we can spend them, but we keep track of what we still owe to our - * users. + * Compute our on-chain balance: we distinguish unconfirmed transactions (which may be RBF-ed or even double-spent), + * recently confirmed transactions (which aren't yet settled in our off-chain balance until they've reached min-depth) + * and deeply confirmed transactions (which are settled in our off-chain balance). + * + * Note that this may create temporary glitches when doing 0-conf splices, which will appear in the off-chain balance + * immediately but will only be correctly accounted for in our on-chain balance after being deeply confirmed. Those + * cases can be detected by looking at the unconfirmed and recently confirmed on-chain balance. */ - private def computeOnChainBalance(bitcoinClient: BitcoinCoreClient)(implicit ec: ExecutionContext): Future[DetailedOnChainBalance] = for { + def computeOnChainBalance(bitcoinClient: BitcoinCoreClient, minDepth: Int)(implicit ec: ExecutionContext): Future[DetailedOnChainBalance] = for { utxos <- bitcoinClient.listUnspent() - detailed = utxos.foldLeft(DetailedOnChainBalance(utxos = utxos)) { + unconfirmedRecentlySpentInputs <- getUnconfirmedRecentlySpentInputs(bitcoinClient, utxos) + confirmedRecentlySpentInputs <- getConfirmedRecentlySpentInputs(bitcoinClient, minDepth) + detailed = utxos.foldLeft(DetailedOnChainBalance(utxos = utxos, recentlySpentInputs = unconfirmedRecentlySpentInputs ++ confirmedRecentlySpentInputs)) { case (total, utxo) if utxo.confirmations == 0 => total.copy(unconfirmed = total.unconfirmed + (utxo.outPoint -> utxo.amount)) - case (total, utxo) => total.copy(confirmed = total.confirmed + (utxo.outPoint -> utxo.amount)) + case (total, utxo) if utxo.confirmations < minDepth => total.copy(recentlyConfirmed = total.recentlyConfirmed + (utxo.outPoint -> utxo.amount)) + case (total, utxo) => total.copy(deeplyConfirmed = total.deeplyConfirmed + (utxo.outPoint -> utxo.amount)) } } yield detailed - case class GlobalBalance(onChain: DetailedOnChainBalance, offChain: OffChainBalance, channels: Map[ByteVector32, PersistentChannelData], knownPreimages: Set[(ByteVector32, Long)]) { + /** + * We list utxos that were spent by our unconfirmed transactions: they will be included in our on-chain balance, and + * thus need to be ignored from our off-chain balance. + */ + private def getUnconfirmedRecentlySpentInputs(bitcoinClient: BitcoinCoreClient, utxos: Seq[Utxo])(implicit ec: ExecutionContext): Future[Set[OutPoint]] = { + val unconfirmedTxs = utxos.filter(_.confirmations == 0).map(_.txid).toSet + Future.sequence(unconfirmedTxs.map(txId => bitcoinClient.getTransaction(txId).map(Some(_)).recover { case _ => None })).map(_.flatten.flatMap(_.txIn.map(_.outPoint))) + } + + /** + * We list utxos that were spent in recent blocks, up to min-depth: those utxos will be included in our on-chain + * balance if they belong to us, and thus need to be ignored from our off-chain balance. + * + * Note that since we may spend our inputs before they reach min-depth (e.g. to fund unrelated channels), some of + * those utxos don't appear in our on-chain balance, which is fine since we already spent them! In that case, they + * must not be counted in our off-chain balance either, since we've used them already. This is why we cannot rely + * only on listUnspent to deduplicate utxos between on-chain and off-chain balances. + */ + private def getConfirmedRecentlySpentInputs(bitcoinClient: BitcoinCoreClient, minDepth: Int)(implicit ec: ExecutionContext): Future[Set[OutPoint]] = for { + currentBlockHeight <- bitcoinClient.getBlockHeight() + currentBlockId <- bitcoinClient.getBlockId(currentBlockHeight.toInt) + // We look one block past our min-depth in case there's a race with a new block. + spentInputs <- scanPastBlocks(bitcoinClient, currentBlockId, Set.empty, remaining = minDepth + 1) + } yield spentInputs + + private def scanPastBlocks(bitcoinClient: BitcoinCoreClient, blockId: BlockId, spentInputs: Set[OutPoint], remaining: Int)(implicit ec: ExecutionContext): Future[Set[OutPoint]] = { + bitcoinClient.getBlock(blockId).flatMap(block => { + val spentInputs1 = spentInputs ++ block.tx.asScala.flatMap(_.txIn.asScala.map(_.outPoint)).map(KotlinUtils.kmp2scala).toSet + if (remaining > 0) { + scanPastBlocks(bitcoinClient, BlockId(KotlinUtils.kmp2scala(block.header.hashPreviousBlock)), spentInputs1, remaining - 1) + } else { + Future.successful(spentInputs1) + } + }) + } + + case class GlobalBalance(onChain: DetailedOnChainBalance, offChain: OffChainBalance, channels: Map[ByteVector32, PersistentChannelData]) { val total: Btc = onChain.total + offChain.total } - def computeGlobalBalance(channels: Map[ByteVector32, PersistentChannelData], db: Databases, bitcoinClient: BitcoinCoreClient)(implicit ec: ExecutionContext): Future[GlobalBalance] = for { - onChain <- CheckBalance.computeOnChainBalance(bitcoinClient) - knownPreimages = db.pendingCommands.listSettlementCommands().collect { case (channelId, cmd: CMD_FULFILL_HTLC) => (channelId, cmd.id) }.toSet - offChainRaw = CheckBalance.computeOffChainBalance(channels.values, knownPreimages) - offChainPruned <- CheckBalance.prunePublishedTransactions(offChainRaw, bitcoinClient) - } yield GlobalBalance(onChain, offChainPruned, channels, knownPreimages) + def computeGlobalBalance(channels: Map[ByteVector32, PersistentChannelData], bitcoinClient: BitcoinCoreClient, minDepth: Int)(implicit ec: ExecutionContext): Future[GlobalBalance] = for { + onChain <- CheckBalance.computeOnChainBalance(bitcoinClient, minDepth) + offChain = CheckBalance.computeOffChainBalance(channels.values, onChain.recentlySpentInputs) + } yield GlobalBalance(onChain, offChain, channels) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/balance/Monitoring.scala b/eclair-core/src/main/scala/fr/acinq/eclair/balance/Monitoring.scala index 0fb652c107..0c12c7d427 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/balance/Monitoring.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/balance/Monitoring.scala @@ -32,25 +32,24 @@ object Monitoring { object Tags { val BalanceType = "type" - val OffchainState = "state" + val OffChainState = "state" val DiffSign = "sign" val UtxoStatus = "status" object BalanceTypes { - val OnchainConfirmed = "onchain.confirmed" - val OnchainUnconfirmed = "onchain.unconfirmed" - val Offchain = "offchain" + val OnChainDeeplyConfirmed = "onchain.deeply-confirmed" + val OnChainRecentlyConfirmed = "onchain.recently-confirmed" + val OnChainUnconfirmed = "onchain.unconfirmed" + val OffChain = "offchain" } - object OffchainStates { + object OffChainStates { val waitForFundingConfirmed = "waitForFundingConfirmed" val waitForChannelReady = "waitForChannelReady" val normal = "normal" val shutdown = "shutdown" val negotiating = "negotiating" - val closingLocal = "closing-local" - val closingRemote = "closing-remote" - val closingUnknown = "closing-unknown" + val closing = "closing" val waitForPublishFutureCommitment = "waitForPublishFutureCommitment" } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/OnChainWallet.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/OnChainWallet.scala index caefb87fdb..0d2a217526 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/OnChainWallet.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/OnChainWallet.scala @@ -17,7 +17,6 @@ package fr.acinq.eclair.blockchain import fr.acinq.bitcoin.psbt.Psbt -import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.bitcoin.scalacompat.{OutPoint, Satoshi, ScriptElt, Transaction, TxId} import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinCoreClient.AddressType import fr.acinq.eclair.blockchain.fee.FeeratePerKw @@ -120,18 +119,10 @@ trait OnChainAddressGenerator { /** Generate the public key script for a new wallet address. */ def getReceivePublicKeyScript(addressType_opt: Option[AddressType] = None)(implicit ec: ExecutionContext): Future[Seq[ScriptElt]] - /** Generate a p2wpkh wallet address and return the corresponding public key. */ - def getP2wpkhPubkey()(implicit ec: ExecutionContext): Future[PublicKey] - } /** A caching layer for [[OnChainAddressGenerator]] that provides synchronous access to wallet addresses and keys. */ -trait OnChainPubkeyCache { - - /** - * @param renew applies after requesting the current pubkey, and is asynchronous. - */ - def getP2wpkhPubkey(renew: Boolean): PublicKey +trait OnChainAddressCache { /** * @param renew applies after requesting the current script, and is asynchronous. diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/OnChainAddressRefresher.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/OnChainAddressRefresher.scala index 885cfeef0a..b880846a25 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/OnChainAddressRefresher.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/OnChainAddressRefresher.scala @@ -3,7 +3,6 @@ package fr.acinq.eclair.blockchain.bitcoind import akka.actor.typed.Behavior import akka.actor.typed.scaladsl.{ActorContext, Behaviors, TimerScheduler} -import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.bitcoin.scalacompat.{Script, ScriptElt} import fr.acinq.eclair.blockchain.OnChainAddressGenerator @@ -19,18 +18,16 @@ object OnChainAddressRefresher { // @formatter:off sealed trait Command - case object RenewPubkey extends Command case object RenewPubkeyScript extends Command - private case class SetPubkey(pubkey: PublicKey) extends Command private case class SetPubkeyScript(script: Seq[ScriptElt]) extends Command private case class Error(reason: Throwable) extends Command private case object Done extends Command // @formatter:on - def apply(generator: OnChainAddressGenerator, finalPubkey: AtomicReference[PublicKey], finalPubkeyScript: AtomicReference[Seq[ScriptElt]], delay: FiniteDuration): Behavior[Command] = { + def apply(generator: OnChainAddressGenerator, finalPubkeyScript: AtomicReference[Seq[ScriptElt]], delay: FiniteDuration): Behavior[Command] = { Behaviors.setup { context => Behaviors.withTimers { timers => - val refresher = new OnChainAddressRefresher(generator, finalPubkey, finalPubkeyScript, context, timers, delay) + val refresher = new OnChainAddressRefresher(generator, finalPubkeyScript, context, timers, delay) refresher.idle() } } @@ -38,7 +35,6 @@ object OnChainAddressRefresher { } private class OnChainAddressRefresher(generator: OnChainAddressGenerator, - finalPubkey: AtomicReference[PublicKey], finalPubkeyScript: AtomicReference[Seq[ScriptElt]], context: ActorContext[OnChainAddressRefresher.Command], timers: TimerScheduler[OnChainAddressRefresher.Command], delay: FiniteDuration) { @@ -47,13 +43,6 @@ private class OnChainAddressRefresher(generator: OnChainAddressGenerator, /** In that state, we're ready to renew our on-chain address whenever requested. */ def idle(): Behavior[Command] = Behaviors.receiveMessage { - case RenewPubkey => - context.log.debug("renewing pubkey (current={})", finalPubkey.get()) - context.pipeToSelf(generator.getP2wpkhPubkey()) { - case Success(pubkey) => SetPubkey(pubkey) - case Failure(reason) => Error(reason) - } - renewing() case RenewPubkeyScript => context.log.debug("renewing script (current={})", Script.write(finalPubkeyScript.get()).toHex) context.pipeToSelf(generator.getReceivePublicKeyScript()) { @@ -68,14 +57,11 @@ private class OnChainAddressRefresher(generator: OnChainAddressGenerator, /** We ignore concurrent requests while waiting for bitcoind to respond. */ private def renewing(): Behavior[Command] = Behaviors.receiveMessage { - case SetPubkey(pubkey) => - timers.startSingleTimer(Done, delay) - delaying(Some(pubkey), None) case SetPubkeyScript(script) => timers.startSingleTimer(Done, delay) - delaying(None, Some(script)) + delaying(script) case Error(reason) => - context.log.error("cannot renew public key or script", reason) + context.log.error("cannot renew script", reason) idle() case cmd => context.log.debug("ignoring command={} while waiting for bitcoin core's response", cmd) @@ -83,29 +69,16 @@ private class OnChainAddressRefresher(generator: OnChainAddressGenerator, } /** - * After receiving our new script or pubkey from bitcoind, we wait before updating our current values. + * After receiving our new address from bitcoind, we wait before updating our current value. * While waiting, we ignore additional requests to renew. * * This ensures that a burst of requests during a mass force-close use the same final on-chain address instead of * creating a lot of address churn on our bitcoin wallet. - * - * Note that while we're updating our final script, we will ignore requests to update our final public key (and the - * other way around). This is fine, since the public key is only used: - * - when opening static_remotekey channels, which is disabled by default - * - when closing channels with peers that don't support shutdown_anysegwit (which should be widely supported) - * - * In practice, we most likely always use [[RenewPubkeyScript]]. */ - private def delaying(nextPubkey_opt: Option[PublicKey], nextScript_opt: Option[Seq[ScriptElt]]): Behavior[Command] = Behaviors.receiveMessage { + private def delaying(nextScript: Seq[ScriptElt]): Behavior[Command] = Behaviors.receiveMessage { case Done => - nextPubkey_opt.foreach { nextPubkey => - context.log.info("setting pubkey to {}", nextPubkey) - finalPubkey.set(nextPubkey) - } - nextScript_opt.foreach { nextScript => - context.log.info("setting script to {}", Script.write(nextScript).toHex) - finalPubkeyScript.set(nextScript) - } + context.log.info("setting script to {}", Script.write(nextScript).toHex) + finalPubkeyScript.set(nextScript) idle() case cmd => context.log.debug("rate-limiting command={}", cmd) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/ZmqWatcher.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/ZmqWatcher.scala index 299516b424..5643df795b 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/ZmqWatcher.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/ZmqWatcher.scala @@ -143,8 +143,7 @@ object ZmqWatcher { case class WatchFundingConfirmed(replyTo: ActorRef[WatchFundingConfirmedTriggered], txId: TxId, minDepth: Int) extends WatchConfirmed[WatchFundingConfirmedTriggered] case class WatchFundingConfirmedTriggered(blockHeight: BlockHeight, txIndex: Int, tx: Transaction) extends WatchConfirmedTriggered - case class RelativeDelay(parentTxId: TxId, delay: Long) - case class WatchTxConfirmed(replyTo: ActorRef[WatchTxConfirmedTriggered], txId: TxId, minDepth: Int, delay_opt: Option[RelativeDelay] = None) extends WatchConfirmed[WatchTxConfirmedTriggered] + case class WatchTxConfirmed(replyTo: ActorRef[WatchTxConfirmedTriggered], txId: TxId, minDepth: Int) extends WatchConfirmed[WatchTxConfirmedTriggered] case class WatchTxConfirmedTriggered(blockHeight: BlockHeight, txIndex: Int, tx: Transaction) extends WatchConfirmedTriggered case class WatchParentTxConfirmed(replyTo: ActorRef[WatchParentTxConfirmedTriggered], txId: TxId, minDepth: Int) extends WatchConfirmed[WatchParentTxConfirmedTriggered] @@ -463,10 +462,10 @@ private class ZmqWatcher(nodeParams: NodeParams, blockHeight: AtomicLong, client private def checkConfirmed(w: WatchConfirmed[_ <: WatchConfirmedTriggered], currentHeight: BlockHeight): Future[Unit] = { log.debug("checking confirmations of txid={}", w.txId) - // NB: this is very inefficient since internally we call `getrawtransaction` three times, but it doesn't really - // matter because this only happens once, when the watched transaction has reached min_depth client.getTxConfirmations(w.txId).flatMap { case Some(confirmations) if confirmations >= w.minDepth => + // NB: this is very inefficient since internally we call `getrawtransaction` three times, but it doesn't really + // matter because this only happens once, when the watched transaction has reached min_depth client.getTransaction(w.txId).flatMap { tx => client.getTransactionShortId(w.txId).map { case (height, index) => w match { @@ -483,27 +482,11 @@ private class ZmqWatcher(nodeParams: NodeParams, blockHeight: AtomicLong, client context.self ! SetWatchHint(w, CheckAfterBlock(currentHeight + w.minDepth - confirmations)) Future.successful(()) case None => - w match { - case WatchTxConfirmed(_, _, _, Some(relativeDelay)) => - log.debug("txId={} has a relative delay of {} blocks, checking parentTxId={}", w.txId, relativeDelay.delay, relativeDelay.parentTxId) - // Note how we add one block to avoid an off-by-one: - // - if the parent is confirmed at block P - // - the CSV delay is D and the minimum depth is M - // - the first block that can include the child is P + D - // - the first block at which we can reach minimum depth is P + D + M - // - if we are currently at block P + N, the parent has C = N + 1 confirmations - // - we want to check at block P + N + D + M + 1 - C = P + N + D + M + 1 - (N + 1) = P + D + M - val delay = relativeDelay.delay + w.minDepth + 1 - client.getTxConfirmations(relativeDelay.parentTxId).map(_.getOrElse(0)).collect { - case confirmations if confirmations < delay => context.self ! SetWatchHint(w, CheckAfterBlock(currentHeight + delay - confirmations)) - } - case _ => - // The transaction is unconfirmed: we don't need to check again at every new block: we can check only once - // every minDepth blocks, which is more efficient. If the transaction is included at the current height in - // a reorg, we will trigger the watch one block later than expected, but this is fine. - context.self ! SetWatchHint(w, CheckAfterBlock(currentHeight + w.minDepth)) - Future.successful(()) - } + // The transaction is unconfirmed: we don't need to check again at every new block: we can check only once + // every minDepth blocks, which is more efficient. If the transaction is included at the current height in + // a reorg, we will trigger the watch one block later than expected, but this is fine. + context.self ! SetWatchHint(w, CheckAfterBlock(currentHeight + w.minDepth)) + Future.successful(()) } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/rpc/BitcoinCoreClient.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/rpc/BitcoinCoreClient.scala index 5dfecb60ce..d48600e8a7 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/rpc/BitcoinCoreClient.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/rpc/BitcoinCoreClient.scala @@ -263,7 +263,7 @@ class BitcoinCoreClient(val rpcClient: BitcoinJsonRPCClient, val lockUtxos: Bool def fundTransaction(tx: Transaction, feeRate: FeeratePerKw, replaceable: Boolean = true, changePosition: Option[Int] = None, externalInputsWeight: Map[OutPoint, Long] = Map.empty, minInputConfirmations_opt: Option[Int] = None, feeBudget_opt: Option[Satoshi] = None)(implicit ec: ExecutionContext): Future[FundTransactionResponse] = { val options = FundTransactionOptions( - feeRate = BigDecimal(FeeratePerKB(feeRate).toLong).bigDecimal.scaleByPowerOfTen(-8), + feeRate = BigDecimal(feeRate.perKB.toLong).bigDecimal.scaleByPowerOfTen(-8), replaceable = replaceable, // We must either *always* lock inputs selected for funding or *never* lock them, otherwise locking wouldn't work // at all, as the following scenario highlights: @@ -357,7 +357,7 @@ class BitcoinCoreClient(val rpcClient: BitcoinJsonRPCClient, val lockUtxos: Bool for { // TODO: we should check that mempoolMinFee is not dangerously high - feerate <- mempoolMinFee().map(minFee => FeeratePerKw(minFee).max(targetFeerate)) + feerate <- mempoolMinFee().map(minFee => minFee.perKw.max(targetFeerate)) // we ask bitcoin core to add inputs to the funding tx, and use the specified change address FundTransactionResponse(tx, fee, _) <- fundTransaction(partialFundingTx, feerate, feeBudget_opt = feeBudget_opt) lockedUtxos = tx.txIn.map(_.outPoint) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/FeeProvider.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/FeeProvider.scala index 2123ef84c9..0ef8e8a6ec 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/FeeProvider.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/FeeProvider.scala @@ -32,19 +32,21 @@ case object CannotRetrieveFeerates extends RuntimeException("cannot retrieve fee /** Fee rate in satoshi-per-bytes. */ case class FeeratePerByte(feerate: Satoshi) { + def perKw: FeeratePerKw = FeeratePerKw(this) def perKB: FeeratePerKB = FeeratePerKB(this) override def toString: String = s"$feerate/byte" } object FeeratePerByte { - def apply(feeratePerKB: FeeratePerKB): FeeratePerByte = FeeratePerByte(feeratePerKB.feerate / 1000) - def apply(feeratePerKw: FeeratePerKw): FeeratePerByte = FeeratePerByte(FeeratePerKB(feeratePerKw)) + private[fee] def apply(feeratePerKB: FeeratePerKB): FeeratePerByte = FeeratePerByte(feeratePerKB.feerate / 1000) + private[fee] def apply(feeratePerKw: FeeratePerKw): FeeratePerByte = FeeratePerByte(FeeratePerKB(feeratePerKw)) } /** Fee rate in satoshi-per-kilo-bytes (1 kB = 1000 bytes). */ case class FeeratePerKB(feerate: Satoshi) extends Ordered[FeeratePerKB] { // @formatter:off def perByte: FeeratePerByte = FeeratePerByte(this) + def perKw: FeeratePerKw = FeeratePerKw(this) override def compare(that: FeeratePerKB): Int = feerate.compare(that.feerate) def max(other: FeeratePerKB): FeeratePerKB = if (this > other) this else other def min(other: FeeratePerKB): FeeratePerKB = if (this < other) this else other @@ -55,8 +57,8 @@ case class FeeratePerKB(feerate: Satoshi) extends Ordered[FeeratePerKB] { object FeeratePerKB { // @formatter:off - def apply(feeratePerByte: FeeratePerByte): FeeratePerKB = FeeratePerKB(feeratePerByte.feerate * 1000) - def apply(feeratePerKw: FeeratePerKw): FeeratePerKB = FeeratePerKB(feeratePerKw.feerate * 4) + private[fee] def apply(feeratePerByte: FeeratePerByte): FeeratePerKB = FeeratePerKB(feeratePerByte.feerate * 1000) + private[fee] def apply(feeratePerKw: FeeratePerKw): FeeratePerKB = FeeratePerKB(feeratePerKw.feerate * 4) // @formatter:on } @@ -64,6 +66,7 @@ object FeeratePerKB { case class FeeratePerKw(feerate: Satoshi) extends Ordered[FeeratePerKw] { // @formatter:off def perByte: FeeratePerByte = FeeratePerByte(this) + def perKB: FeeratePerKB = FeeratePerKB(this) override def compare(that: FeeratePerKw): Int = feerate.compare(that.feerate) def max(other: FeeratePerKw): FeeratePerKw = if (this > other) this else other def min(other: FeeratePerKw): FeeratePerKw = if (this < other) this else other @@ -104,8 +107,8 @@ object FeeratePerKw { val MinimumFeeratePerKw = FeeratePerKw(253 sat) // @formatter:off - def apply(feeratePerKB: FeeratePerKB): FeeratePerKw = MinimumFeeratePerKw.max(FeeratePerKw(feeratePerKB.feerate / 4)) - def apply(feeratePerByte: FeeratePerByte): FeeratePerKw = FeeratePerKw(FeeratePerKB(feeratePerByte)) + private[fee] def apply(feeratePerKB: FeeratePerKB): FeeratePerKw = MinimumFeeratePerKw.max(FeeratePerKw(feeratePerKB.feerate / 4)) + private[fee] def apply(feeratePerByte: FeeratePerByte): FeeratePerKw = FeeratePerKw(FeeratePerKB(feeratePerByte)) // @formatter:on } @@ -140,7 +143,7 @@ object FeeratesPerKw { fastest = FeeratePerKw(feerates.fastest)) /** Used in tests */ - def single(feeratePerKw: FeeratePerKw, networkMinFee: FeeratePerKw = FeeratePerKw(FeeratePerByte(1 sat))): FeeratesPerKw = FeeratesPerKw( + def single(feeratePerKw: FeeratePerKw, networkMinFee: FeeratePerKw = FeeratePerByte(1 sat).perKw): FeeratesPerKw = FeeratesPerKw( minimum = networkMinFee, slow = feeratePerKw, medium = feeratePerKw, diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/OnChainFeeConf.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/OnChainFeeConf.scala index a7514601ac..2a009c4ebc 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/OnChainFeeConf.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/OnChainFeeConf.scala @@ -17,10 +17,10 @@ package fr.acinq.eclair.blockchain.fee import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey -import fr.acinq.bitcoin.scalacompat.Satoshi +import fr.acinq.bitcoin.scalacompat.{Satoshi, SatoshiLong} import fr.acinq.eclair.BlockHeight import fr.acinq.eclair.transactions.Transactions -import fr.acinq.eclair.transactions.Transactions.{CommitmentFormat, UnsafeLegacyAnchorOutputsCommitmentFormat, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat} +import fr.acinq.eclair.transactions.Transactions._ // @formatter:off sealed trait ConfirmationPriority extends Ordered[ConfirmationPriority] { @@ -65,32 +65,15 @@ case class DustTolerance(maxExposure: Satoshi, closeOnUpdateFeeOverflow: Boolean case class FeerateTolerance(ratioLow: Double, ratioHigh: Double, anchorOutputMaxCommitFeerate: FeeratePerKw, dustTolerance: DustTolerance) { /** - * @param commitmentFormat commitment format (anchor outputs allows a much higher tolerance since fees can be adjusted after tx is published) - * @param networkFeerate reference fee rate (value we estimate from our view of the network) - * @param proposedFeerate fee rate proposed (new proposal through update_fee or previous proposal used in our current commit tx) + * @param networkFeerate reference fee rate (value we estimate from our view of the network) + * @param proposedFeerate fee rate proposed (new proposal through update_fee or previous proposal used in our current commit tx) * @return true if the difference between proposed and reference fee rates is too high. */ - def isFeeDiffTooHigh(commitmentFormat: CommitmentFormat, networkFeerate: FeeratePerKw, proposedFeerate: FeeratePerKw): Boolean = { - isProposedFeerateTooLow(commitmentFormat, networkFeerate, proposedFeerate) || isProposedFeerateTooHigh(commitmentFormat, networkFeerate, proposedFeerate) - } - - def isProposedFeerateTooHigh(commitmentFormat: CommitmentFormat, networkFeerate: FeeratePerKw, proposedFeerate: FeeratePerKw): Boolean = { - commitmentFormat match { - case Transactions.DefaultCommitmentFormat => networkFeerate * ratioHigh < proposedFeerate - case ZeroFeeHtlcTxAnchorOutputsCommitmentFormat | UnsafeLegacyAnchorOutputsCommitmentFormat => networkFeerate * ratioHigh < proposedFeerate - } - } - - def isProposedFeerateTooLow(commitmentFormat: CommitmentFormat, networkFeerate: FeeratePerKw, proposedFeerate: FeeratePerKw): Boolean = { - commitmentFormat match { - case Transactions.DefaultCommitmentFormat => proposedFeerate < networkFeerate * ratioLow - // When using anchor outputs, we allow low feerates: fees will be set with CPFP and RBF at broadcast time. - case ZeroFeeHtlcTxAnchorOutputsCommitmentFormat | UnsafeLegacyAnchorOutputsCommitmentFormat => false - } - } + def isProposedCommitFeerateTooHigh(networkFeerate: FeeratePerKw, proposedFeerate: FeeratePerKw): Boolean = networkFeerate * ratioHigh < proposedFeerate } case class OnChainFeeConf(feeTargets: FeeTargets, + maxClosingFeerate: FeeratePerKw, safeUtxosThreshold: Int, spendAnchorWithoutHtlcs: Boolean, anchorWithoutHtlcsMaxFee: Satoshi, @@ -102,31 +85,45 @@ case class OnChainFeeConf(feeTargets: FeeTargets, def feerateToleranceFor(nodeId: PublicKey): FeerateTolerance = perNodeFeerateTolerance.getOrElse(nodeId, defaultFeerateTolerance) /** To avoid spamming our peers with fee updates every time there's a small variation, we only update the fee when the difference exceeds a given ratio. */ - def shouldUpdateFee(currentFeeratePerKw: FeeratePerKw, nextFeeratePerKw: FeeratePerKw): Boolean = - currentFeeratePerKw.toLong == 0 || Math.abs((currentFeeratePerKw.toLong - nextFeeratePerKw.toLong).toDouble / currentFeeratePerKw.toLong) > updateFeeMinDiffRatio + def shouldUpdateFee(currentFeeratePerKw: FeeratePerKw, nextFeeratePerKw: FeeratePerKw, commitmentFormat: CommitmentFormat): Boolean = { + commitmentFormat match { + case Transactions.UnsafeLegacyAnchorOutputsCommitmentFormat | Transactions.PhoenixSimpleTaprootChannelCommitmentFormat => + // If we're not already using 1 sat/byte, we update the fee. + FeeratePerKw(FeeratePerByte(1 sat)) < currentFeeratePerKw + case Transactions.ZeroFeeHtlcTxAnchorOutputsCommitmentFormat | Transactions.ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat => + // If the fee has a large enough change, we update the fee. + currentFeeratePerKw.toLong == 0 || Math.abs((currentFeeratePerKw.toLong - nextFeeratePerKw.toLong).toDouble / currentFeeratePerKw.toLong) > updateFeeMinDiffRatio + } + } def getFundingFeerate(feerates: FeeratesPerKw): FeeratePerKw = feeTargets.funding.getFeerate(feerates) /** * Get the feerate that should apply to a channel commitment transaction: - * - if we're using anchor outputs, we use a feerate that allows network propagation of the commit tx: we will use CPFP to speed up confirmation if needed - * - otherwise we use a feerate that should get the commit tx confirmed within the configured block target + * - if the remote peer is a mobile wallet that supports anchor outputs, we use 1 sat/byte + * - otherwise, we use a feerate that should allow network propagation of the commit tx on its own: we will use CPFP + * on the anchor output to speed up confirmation if needed or to help propagation * * @param remoteNodeId nodeId of our channel peer * @param commitmentFormat commitment format */ - def getCommitmentFeerate(feerates: FeeratesPerKw, remoteNodeId: PublicKey, commitmentFormat: CommitmentFormat, channelCapacity: Satoshi): FeeratePerKw = { + def getCommitmentFeerate(feerates: FeeratesPerKw, remoteNodeId: PublicKey, commitmentFormat: CommitmentFormat): FeeratePerKw = { val networkFeerate = feerates.fast val networkMinFee = feerates.minimum - commitmentFormat match { - case Transactions.DefaultCommitmentFormat => networkFeerate - case _: Transactions.AnchorOutputsCommitmentFormat => + case Transactions.UnsafeLegacyAnchorOutputsCommitmentFormat | Transactions.PhoenixSimpleTaprootChannelCommitmentFormat => + // Since Bitcoin Core v28, 1-parent-1-child package relay has been deployed: it should be ok if the commit tx + // doesn't propagate on its own. + FeeratePerKw(FeeratePerByte(1 sat)) + case Transactions.ZeroFeeHtlcTxAnchorOutputsCommitmentFormat | Transactions.ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat => val targetFeerate = networkFeerate.min(feerateToleranceFor(remoteNodeId).anchorOutputMaxCommitFeerate) // We make sure the feerate is always greater than the propagation threshold. targetFeerate.max(networkMinFee * 1.25) } } - def getClosingFeerate(feerates: FeeratesPerKw): FeeratePerKw = feeTargets.closing.getFeerate(feerates) + def getClosingFeerate(feerates: FeeratesPerKw, maxClosingFeerateOverride_opt: Option[FeeratePerKw]): FeeratePerKw = { + feeTargets.closing.getFeerate(feerates).min(maxClosingFeerateOverride_opt.getOrElse(maxClosingFeerate)) + } + } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala index 2e1a9c887f..81772a8cc6 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala @@ -20,10 +20,11 @@ import akka.actor.{ActorRef, PossiblyHarmful, typed} import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.bitcoin.scalacompat.{ByteVector32, DeterministicWallet, OutPoint, Satoshi, SatoshiLong, Transaction, TxId, TxOut} import fr.acinq.eclair.blockchain.fee.{ConfirmationTarget, FeeratePerKw} -import fr.acinq.eclair.channel.LocalFundingStatus.DualFundedUnconfirmedFundingTx +import fr.acinq.eclair.channel.Helpers.Closing import fr.acinq.eclair.channel.fund.InteractiveTxBuilder._ import fr.acinq.eclair.channel.fund.{InteractiveTxBuilder, InteractiveTxSigningSession} import fr.acinq.eclair.io.Peer +import fr.acinq.eclair.reputation.Reputation import fr.acinq.eclair.transactions.CommitmentSpec import fr.acinq.eclair.transactions.Transactions._ import fr.acinq.eclair.wire.protocol.{ChannelAnnouncement, ChannelReady, ChannelReestablish, ChannelUpdate, ClosingSigned, CommitSig, FailureReason, FundingCreated, FundingSigned, Init, LiquidityAds, OnionRoutingPacket, OpenChannel, OpenDualFundedChannel, Shutdown, SpliceInit, Stfu, TxInitRbf, TxSignatures, UpdateAddHtlc, UpdateFailHtlc, UpdateFailMalformedHtlc, UpdateFulfillHtlc} @@ -91,6 +92,12 @@ case object ERR_INFORMATION_LEAK extends ChannelState 8888888888 Y8P 8888888888 888 Y888 888 "Y8888P" */ +case class ProposedCommitParams(localDustLimit: Satoshi, + localHtlcMinimum: MilliSatoshi, + localMaxHtlcValueInFlight: UInt64, + localMaxAcceptedHtlcs: Int, + toRemoteDelay: CltvExpiryDelta) + case class INPUT_INIT_CHANNEL_INITIATOR(temporaryChannelId: ByteVector32, fundingAmount: Satoshi, dualFunded: Boolean, @@ -100,7 +107,8 @@ case class INPUT_INIT_CHANNEL_INITIATOR(temporaryChannelId: ByteVector32, pushAmount_opt: Option[MilliSatoshi], requireConfirmedInputs: Boolean, requestFunding_opt: Option[LiquidityAds.RequestFunding], - localParams: LocalParams, + localChannelParams: LocalChannelParams, + proposedCommitParams: ProposedCommitParams, remote: ActorRef, remoteInit: Init, channelFlags: ChannelFlags, @@ -114,7 +122,8 @@ case class INPUT_INIT_CHANNEL_NON_INITIATOR(temporaryChannelId: ByteVector32, dualFunded: Boolean, pushAmount_opt: Option[MilliSatoshi], requireConfirmedInputs: Boolean, - localParams: LocalParams, + localChannelParams: LocalChannelParams, + proposedCommitParams: ProposedCommitParams, remote: ActorRef, remoteInit: Init, channelConfig: ChannelConfig, @@ -139,12 +148,19 @@ case class INPUT_RESTORED(data: PersistentChannelData) sealed trait Upstream { def amountIn: MilliSatoshi } object Upstream { /** We haven't restarted and have full information about the upstream parent(s). */ - sealed trait Hot extends Upstream + sealed trait Hot extends Upstream { + /** + * Occupancy of the incoming channel (both slot and value occupancy combined) that will be compared to the outgoing confidence. + */ + def incomingChannelOccupancy: Double + } object Hot { /** Our node is forwarding a single incoming HTLC. */ - case class Channel(add: UpdateAddHtlc, receivedAt: TimestampMilli, receivedFrom: PublicKey) extends Hot { + case class Channel(add: UpdateAddHtlc, receivedAt: TimestampMilli, receivedFrom: PublicKey, incomingChannelOccupancy: Double) extends Hot { override val amountIn: MilliSatoshi = add.amountMsat val expiryIn: CltvExpiry = add.cltvExpiry + + override def toString: String = s"Channel(amountIn=$amountIn, receivedAt=${receivedAt.toLong}, receivedFrom=${receivedFrom.toHex}, endorsement=${add.endorsement}, incomingChannelOccupancy=$incomingChannelOccupancy)" } /** Our node is forwarding a payment based on a set of HTLCs from potentially multiple upstream channels. */ case class Trampoline(received: List[Channel]) extends Hot { @@ -152,6 +168,10 @@ object Upstream { // We must use the lowest expiry of the incoming HTLC set. val expiryIn: CltvExpiry = received.map(_.add.cltvExpiry).min val receivedAt: TimestampMilli = received.map(_.receivedAt).max + + override def incomingChannelOccupancy: Double = received.map(_.incomingChannelOccupancy).max + + override def toString: String = s"Trampoline(${received.map(_.toString).mkString(",")})" } } @@ -160,7 +180,7 @@ object Upstream { object Cold { def apply(hot: Hot): Cold = hot match { case Local(id) => Local(id) - case Hot.Channel(add, _, _) => Cold.Channel(add.channelId, add.id, add.amountMsat) + case Hot.Channel(add, _, _, _) => Cold.Channel(add.channelId, add.id, add.amountMsat) case Hot.Trampoline(received) => Cold.Trampoline(received.map(r => Cold.Channel(r.add.channelId, r.add.id, r.add.amountMsat))) } @@ -175,7 +195,10 @@ object Upstream { } /** Our node is the origin of the payment: there are no matching upstream HTLCs. */ - case class Local(id: UUID) extends Hot with Cold { override val amountIn: MilliSatoshi = 0 msat } + case class Local(id: UUID) extends Hot with Cold { + override val amountIn: MilliSatoshi = 0 msat + override def incomingChannelOccupancy: Double = 0.0 + } } /** @@ -208,14 +231,17 @@ final case class CMD_ADD_HTLC(replyTo: ActorRef, cltvExpiry: CltvExpiry, onion: OnionRoutingPacket, nextPathKey_opt: Option[PublicKey], - confidence: Double, + reputationScore: Reputation.Score, fundingFee_opt: Option[LiquidityAds.FundingFee], origin: Origin.Hot, commit: Boolean = false) extends HasReplyToCommand with ForbiddenCommandDuringQuiescenceNegotiation with ForbiddenCommandWhenQuiescent +case class FailureAttributionData(htlcReceivedAt: TimestampMilli, trampolineReceivedAt_opt: Option[TimestampMilli]) +case class FulfillAttributionData(htlcReceivedAt: TimestampMilli, trampolineReceivedAt_opt: Option[TimestampMilli], downstreamAttribution_opt: Option[ByteVector]) + sealed trait HtlcSettlementCommand extends HasOptionalReplyToCommand with ForbiddenCommandDuringQuiescenceNegotiation with ForbiddenCommandWhenQuiescent { def id: Long } -final case class CMD_FULFILL_HTLC(id: Long, r: ByteVector32, commit: Boolean = false, replyTo_opt: Option[ActorRef] = None) extends HtlcSettlementCommand -final case class CMD_FAIL_HTLC(id: Long, reason: FailureReason, delay_opt: Option[FiniteDuration] = None, commit: Boolean = false, replyTo_opt: Option[ActorRef] = None) extends HtlcSettlementCommand +final case class CMD_FULFILL_HTLC(id: Long, r: ByteVector32, attribution_opt: Option[FulfillAttributionData], commit: Boolean = false, replyTo_opt: Option[ActorRef] = None) extends HtlcSettlementCommand +final case class CMD_FAIL_HTLC(id: Long, reason: FailureReason, attribution_opt: Option[FailureAttributionData], delay_opt: Option[FiniteDuration] = None, commit: Boolean = false, replyTo_opt: Option[ActorRef] = None) extends HtlcSettlementCommand final case class CMD_FAIL_MALFORMED_HTLC(id: Long, onionHash: ByteVector32, failureCode: Int, commit: Boolean = false, replyTo_opt: Option[ActorRef] = None) extends HtlcSettlementCommand final case class CMD_UPDATE_FEE(feeratePerKw: FeeratePerKw, commit: Boolean = false, replyTo_opt: Option[ActorRef] = None) extends HasOptionalReplyToCommand with ForbiddenCommandDuringQuiescenceNegotiation with ForbiddenCommandWhenQuiescent final case class CMD_SIGN(replyTo_opt: Option[ActorRef] = None) extends HasOptionalReplyToCommand with ForbiddenCommandWhenQuiescent @@ -227,7 +253,7 @@ final case class ClosingFeerates(preferred: FeeratePerKw, min: FeeratePerKw, max sealed trait CloseCommand extends HasReplyToCommand final case class CMD_CLOSE(replyTo: ActorRef, scriptPubKey: Option[ByteVector], feerates: Option[ClosingFeerates]) extends CloseCommand with ForbiddenCommandDuringQuiescenceNegotiation with ForbiddenCommandWhenQuiescent -final case class CMD_FORCECLOSE(replyTo: ActorRef, resetFundingTxIndex_opt: Option[Int] = None) extends CloseCommand +final case class CMD_FORCECLOSE(replyTo: ActorRef, maxClosingFeerate_opt: Option[FeeratePerKw] = None, resetFundingTxIndex_opt: Option[Int] = None) extends CloseCommand final case class CMD_BUMP_FORCE_CLOSE_FEE(replyTo: akka.actor.typed.ActorRef[CommandResponse[CMD_BUMP_FORCE_CLOSE_FEE]], confirmationTarget: ConfirmationTarget) extends Command sealed trait ChannelFundingCommand extends Command { @@ -235,7 +261,7 @@ sealed trait ChannelFundingCommand extends Command { } case class SpliceIn(additionalLocalFunding: Satoshi, pushAmount: MilliSatoshi = 0 msat) case class SpliceOut(amount: Satoshi, scriptPubKey: ByteVector) -final case class CMD_SPLICE(replyTo: akka.actor.typed.ActorRef[CommandResponse[ChannelFundingCommand]], spliceIn_opt: Option[SpliceIn], spliceOut_opt: Option[SpliceOut], requestFunding_opt: Option[LiquidityAds.RequestFunding]) extends ChannelFundingCommand { +final case class CMD_SPLICE(replyTo: akka.actor.typed.ActorRef[CommandResponse[ChannelFundingCommand]], spliceIn_opt: Option[SpliceIn], spliceOut_opt: Option[SpliceOut], requestFunding_opt: Option[LiquidityAds.RequestFunding], channelType_opt:Option[ChannelType]) extends ChannelFundingCommand { require(spliceIn_opt.isDefined || spliceOut_opt.isDefined, "there must be a splice-in or a splice-out") val additionalLocalFunding: Satoshi = spliceIn_opt.map(_.additionalLocalFunding).getOrElse(0 sat) val pushAmount: MilliSatoshi = spliceIn_opt.map(_.pushAmount).getOrElse(0 msat) @@ -277,7 +303,7 @@ final case class RES_ADD_FAILED[+T <: ChannelException](c: CMD_ADD_HTLC, t: T, c sealed trait HtlcResult object HtlcResult { sealed trait Fulfill extends HtlcResult { def paymentPreimage: ByteVector32 } - case class RemoteFulfill(fulfill: UpdateFulfillHtlc) extends Fulfill { override val paymentPreimage = fulfill.paymentPreimage } + case class RemoteFulfill(fulfill: UpdateFulfillHtlc) extends Fulfill { override val paymentPreimage: ByteVector32 = fulfill.paymentPreimage } case class OnChainFulfill(paymentPreimage: ByteVector32) extends Fulfill sealed trait Fail extends HtlcResult case class RemoteFail(fail: UpdateFailHtlc) extends Fail @@ -308,111 +334,87 @@ final case class RES_GET_CHANNEL_INFO(nodeId: PublicKey, channelId: ByteVector32 case class ClosingTxProposed(unsignedTx: ClosingTx, localClosingSigned: ClosingSigned) +/** + * When a commitment is published, we keep track of all outputs that can be spent (even if we don't yet have the data + * to spend them, for example the preimage for received HTLCs). Once all of those outputs have been spent by a confirmed + * transaction, the channel close is complete. + * + * Note that we only store transactions after they have been confirmed: we're using RBF to get transactions confirmed, + * and it would be wasteful to store previous versions of the transactions that have been replaced. + */ sealed trait CommitPublished { /** Commitment tx. */ def commitTx: Transaction - /** Map of relevant outpoints that have been spent and the confirmed transaction that spends them. */ + /** Our main output, if we had some balance in the channel. */ + def localOutput_opt: Option[OutPoint] + /** Our anchor output, if one is available to CPFP the [[commitTx]]. */ + def anchorOutput_opt: Option[OutPoint] + /** + * Outputs corresponding to HTLCs that we may be able to claim (even when we don't have the preimage yet). + * Note that some HTLC outputs of the [[commitTx]] may not be included, if we know that we will never claim them + * (such as HTLCs that we didn't relay or that were failed downstream). + */ + def htlcOutputs: Set[OutPoint] + /** Map of outpoints that have been spent and the confirmed transaction that spends them. */ def irrevocablySpent: Map[OutPoint, Transaction] - + /** Returns true if the commitment transaction is confirmed. */ def isConfirmed: Boolean = { // NB: if multiple transactions end up in the same block, the first confirmation we receive may not be the commit tx. // However if the confirmed tx spends from the commit tx, we know that the commit tx is already confirmed and we know // the type of closing. irrevocablySpent.values.exists(tx => tx.txid == commitTx.txid) || irrevocablySpent.keys.exists(_.txid == commitTx.txid) } + /** + * Returns true when all outputs that can be claimed have been spent: we can forget the channel at that point. + * Note that some of those outputs may be claimed by our peer (e.g. HTLCs that reached their expiry). + */ + def isDone: Boolean } /** * Details about a force-close where we published our commitment. * - * @param claimMainDelayedOutputTx tx claiming our main output (if we have one). - * @param htlcTxs txs claiming HTLCs. There will be one entry for each pending HTLC. The value will be - * None only for incoming HTLCs for which we don't have the preimage (we can't claim them yet). - * @param claimHtlcDelayedTxs 3rd-stage txs (spending the output of HTLC txs). - * @param claimAnchorTxs txs spending anchor outputs to bump the feerate of the commitment tx (if applicable). - * We currently only claim our local anchor, but it would be nice to claim both when it - * is economical to do so to avoid polluting the utxo set. + * @param htlcDelayedOutputs when an HTLC transaction confirms, we must claim its output using a 3rd-stage delayed + * transaction. An entry containing the corresponding output must be added to this set to + * ensure that we don't forget the channel too soon, and correctly wait until we've spent it. */ -case class LocalCommitPublished(commitTx: Transaction, claimMainDelayedOutputTx: Option[ClaimLocalDelayedOutputTx], htlcTxs: Map[OutPoint, Option[HtlcTx]], claimHtlcDelayedTxs: List[HtlcDelayedTx], claimAnchorTxs: List[ClaimAnchorOutputTx], irrevocablySpent: Map[OutPoint, Transaction]) extends CommitPublished { - /** - * A local commit is considered done when: - * - all commitment tx outputs that we can spend have been spent and confirmed (even if the spending tx was not ours) - * - all 3rd stage txs (txs spending htlc txs) have been confirmed - */ - def isDone: Boolean = { - val confirmedTxs = irrevocablySpent.values.map(_.txid).toSet - // is the commitment tx confirmed (we need to check this because we may not have any outputs)? - val isCommitTxConfirmed = confirmedTxs.contains(commitTx.txid) - // is our main output confirmed (if we have one)? - val isMainOutputConfirmed = claimMainDelayedOutputTx.forall(tx => irrevocablySpent.contains(tx.input.outPoint)) - // are all htlc outputs from the commitment tx spent (we need to check them all because we may receive preimages later)? - val allHtlcsSpent = (htlcTxs.keySet -- irrevocablySpent.keys).isEmpty - // are all outputs from htlc txs spent? - val unconfirmedHtlcDelayedTxs = claimHtlcDelayedTxs.map(_.input.outPoint) - // only the txs which parents are already confirmed may get confirmed (note that this eliminates outputs that have been double-spent by a competing tx) - .filter(input => confirmedTxs.contains(input.txid)) - // has the tx already been confirmed? - .filterNot(input => irrevocablySpent.contains(input)) - isCommitTxConfirmed && isMainOutputConfirmed && allHtlcsSpent && unconfirmedHtlcDelayedTxs.isEmpty +case class LocalCommitPublished(commitTx: Transaction, localOutput_opt: Option[OutPoint], anchorOutput_opt: Option[OutPoint], incomingHtlcs: Map[OutPoint, Long], outgoingHtlcs: Map[OutPoint, Long], htlcDelayedOutputs: Set[OutPoint], irrevocablySpent: Map[OutPoint, Transaction]) extends CommitPublished { + override val htlcOutputs: Set[OutPoint] = incomingHtlcs.keySet ++ outgoingHtlcs.keySet + override val isDone: Boolean = { + val mainOutputSpent = localOutput_opt.forall(o => irrevocablySpent.contains(o)) + val allHtlcsSpent = (htlcOutputs -- irrevocablySpent.keySet).isEmpty + val allHtlcTxsSpent = (htlcDelayedOutputs -- irrevocablySpent.keySet).isEmpty + isConfirmed && mainOutputSpent && allHtlcsSpent && allHtlcTxsSpent } } /** - * Details about a force-close where they published their commitment. - * - * @param claimMainOutputTx tx claiming our main output (if we have one). - * @param claimHtlcTxs txs claiming HTLCs. There will be one entry for each pending HTLC. The value will be None - * only for incoming HTLCs for which we don't have the preimage (we can't claim them yet). - * @param claimAnchorTxs txs spending anchor outputs to bump the feerate of the commitment tx (if applicable). - * We currently only claim our local anchor, but it would be nice to claim both when it is - * economical to do so to avoid polluting the utxo set. + * Details about a force-close where they published their commitment (current or next). */ -case class RemoteCommitPublished(commitTx: Transaction, claimMainOutputTx: Option[ClaimRemoteCommitMainOutputTx], claimHtlcTxs: Map[OutPoint, Option[ClaimHtlcTx]], claimAnchorTxs: List[ClaimAnchorOutputTx], irrevocablySpent: Map[OutPoint, Transaction]) extends CommitPublished { - /** - * A remote commit is considered done when all commitment tx outputs that we can spend have been spent and confirmed - * (even if the spending tx was not ours). - */ - def isDone: Boolean = { - val confirmedTxs = irrevocablySpent.values.map(_.txid).toSet - // is the commitment tx confirmed (we need to check this because we may not have any outputs)? - val isCommitTxConfirmed = confirmedTxs.contains(commitTx.txid) - // is our main output confirmed (if we have one)? - val isMainOutputConfirmed = claimMainOutputTx.forall(tx => irrevocablySpent.contains(tx.input.outPoint)) - // are all htlc outputs from the commitment tx spent (we need to check them all because we may receive preimages later)? - val allHtlcsSpent = (claimHtlcTxs.keySet -- irrevocablySpent.keys).isEmpty - isCommitTxConfirmed && isMainOutputConfirmed && allHtlcsSpent +case class RemoteCommitPublished(commitTx: Transaction, localOutput_opt: Option[OutPoint], anchorOutput_opt: Option[OutPoint], incomingHtlcs: Map[OutPoint, Long], outgoingHtlcs: Map[OutPoint, Long], irrevocablySpent: Map[OutPoint, Transaction]) extends CommitPublished { + override val htlcOutputs: Set[OutPoint] = incomingHtlcs.keySet ++ outgoingHtlcs.keySet + override val isDone: Boolean = { + val mainOutputSpent = localOutput_opt.forall(o => irrevocablySpent.contains(o)) + val allHtlcsSpent = (htlcOutputs -- irrevocablySpent.keySet).isEmpty + isConfirmed && mainOutputSpent && allHtlcsSpent } } /** * Details about a force-close where they published one of their revoked commitments. + * In that case, we're able to spend every output of the commitment transaction (if economical). * - * @param claimMainOutputTx tx claiming our main output (if we have one). - * @param mainPenaltyTx penalty tx claiming their main output (if they have one). - * @param htlcPenaltyTxs penalty txs claiming every HTLC output. - * @param claimHtlcDelayedPenaltyTxs penalty txs claiming the output of their HTLC txs (if they managed to get them confirmed before our htlcPenaltyTxs). + * @param htlcDelayedOutputs if our peer manages to get some of their HTLC transactions confirmed before our penalty + * transactions, we must spend the output(s) of their HTLC transactions. */ -case class RevokedCommitPublished(commitTx: Transaction, claimMainOutputTx: Option[ClaimRemoteCommitMainOutputTx], mainPenaltyTx: Option[MainPenaltyTx], htlcPenaltyTxs: List[HtlcPenaltyTx], claimHtlcDelayedPenaltyTxs: List[ClaimHtlcDelayedOutputPenaltyTx], irrevocablySpent: Map[OutPoint, Transaction]) extends CommitPublished { - /** - * A revoked commit is considered done when all commitment tx outputs that we can spend have been spent and confirmed - * (even if the spending tx was not ours). - */ - def isDone: Boolean = { - val confirmedTxs = irrevocablySpent.values.map(_.txid).toSet - // is the commitment tx confirmed (we need to check this because we may not have any outputs)? - val isCommitTxConfirmed = confirmedTxs.contains(commitTx.txid) - // are there remaining spendable outputs from the commitment tx? - val unspentCommitTxOutputs = { - val commitOutputsSpendableByUs = (claimMainOutputTx.toSeq ++ mainPenaltyTx.toSeq ++ htlcPenaltyTxs).map(_.input.outPoint) - commitOutputsSpendableByUs.toSet -- irrevocablySpent.keys - } - // are all outputs from htlc txs spent? - val unconfirmedHtlcDelayedTxs = claimHtlcDelayedPenaltyTxs.map(_.input.outPoint) - // only the txs which parents are already confirmed may get confirmed (note that this eliminates outputs that have been double-spent by a competing tx) - .filter(input => confirmedTxs.contains(input.txid)) - // if one of the tx inputs has been spent, the tx has already been confirmed or a competing tx has been confirmed - .filterNot(input => irrevocablySpent.contains(input)) - isCommitTxConfirmed && unspentCommitTxOutputs.isEmpty && unconfirmedHtlcDelayedTxs.isEmpty +case class RevokedCommitPublished(commitTx: Transaction, localOutput_opt: Option[OutPoint], remoteOutput_opt: Option[OutPoint], htlcOutputs: Set[OutPoint], htlcDelayedOutputs: Set[OutPoint], irrevocablySpent: Map[OutPoint, Transaction]) extends CommitPublished { + // We don't use the anchor output, we can CPFP the commitment with any other output. + override val anchorOutput_opt: Option[OutPoint] = None + override val isDone: Boolean = { + val mainOutputsSpent = (localOutput_opt.toSeq ++ remoteOutput_opt.toSeq).forall(o => irrevocablySpent.contains(o)) + val allHtlcsSpent = (htlcOutputs -- irrevocablySpent.keySet).isEmpty + val allHtlcTxsSpent = (htlcDelayedOutputs -- irrevocablySpent.keySet).isEmpty + isConfirmed && mainOutputsSpent && allHtlcsSpent && allHtlcTxsSpent } } @@ -428,6 +430,7 @@ case class RevokedCommitPublished(commitTx: Transaction, claimMainOutputTx: Opti case class ShortIdAliases(localAlias: Alias, remoteAlias_opt: Option[Alias]) sealed trait LocalFundingStatus { + /** While the transaction is unconfirmed, we keep the funding transaction (if available) to allow rebroadcasting. */ def signedTx_opt: Option[Transaction] /** We store local signatures for the purpose of retransmitting if the funding/splicing flow is interrupted. */ def localSigs_opt: Option[TxSignatures] @@ -456,8 +459,8 @@ object LocalFundingStatus { case class ZeroconfPublishedFundingTx(tx: Transaction, localSigs_opt: Option[TxSignatures], liquidityPurchase_opt: Option[LiquidityAds.PurchaseBasicInfo]) extends UnconfirmedFundingTx with Locked { override val signedTx_opt: Option[Transaction] = Some(tx) } - case class ConfirmedFundingTx(tx: Transaction, shortChannelId: RealShortChannelId, localSigs_opt: Option[TxSignatures], liquidityPurchase_opt: Option[LiquidityAds.PurchaseBasicInfo]) extends LocalFundingStatus with Locked { - override val signedTx_opt: Option[Transaction] = Some(tx) + case class ConfirmedFundingTx(spentInputs: Seq[OutPoint], txOut: TxOut, shortChannelId: RealShortChannelId, localSigs_opt: Option[TxSignatures], liquidityPurchase_opt: Option[LiquidityAds.PurchaseBasicInfo]) extends LocalFundingStatus with Locked { + override val signedTx_opt: Option[Transaction] = None } } @@ -544,6 +547,7 @@ case object Nothing extends TransientChannelData { sealed trait PersistentChannelData extends ChannelData { def remoteNodeId: PublicKey + def channelParams: ChannelParams } sealed trait ChannelDataWithoutCommitments extends PersistentChannelData { val channelId: ByteVector32 = channelParams.channelId @@ -553,33 +557,47 @@ sealed trait ChannelDataWithoutCommitments extends PersistentChannelData { sealed trait ChannelDataWithCommitments extends PersistentChannelData { val channelId: ByteVector32 = commitments.channelId val remoteNodeId: PublicKey = commitments.remoteNodeId + val channelParams: ChannelParams = commitments.channelParams def commitments: Commitments } +sealed trait ClosedData extends ChannelData + final case class DATA_WAIT_FOR_OPEN_CHANNEL(initFundee: INPUT_INIT_CHANNEL_NON_INITIATOR) extends TransientChannelData { val channelId: ByteVector32 = initFundee.temporaryChannelId } final case class DATA_WAIT_FOR_ACCEPT_CHANNEL(initFunder: INPUT_INIT_CHANNEL_INITIATOR, lastSent: OpenChannel) extends TransientChannelData { val channelId: ByteVector32 = initFunder.temporaryChannelId } -final case class DATA_WAIT_FOR_FUNDING_INTERNAL(params: ChannelParams, +final case class DATA_WAIT_FOR_FUNDING_INTERNAL(channelParams: ChannelParams, + channelType: SupportedChannelType, + localCommitParams: CommitParams, + remoteCommitParams: CommitParams, fundingAmount: Satoshi, pushAmount: MilliSatoshi, commitTxFeerate: FeeratePerKw, remoteFundingPubKey: PublicKey, remoteFirstPerCommitmentPoint: PublicKey, replyTo: akka.actor.typed.ActorRef[Peer.OpenChannelResponse]) extends TransientChannelData { - val channelId: ByteVector32 = params.channelId + val channelId: ByteVector32 = channelParams.channelId + val commitmentFormat: CommitmentFormat = channelType.commitmentFormat } -final case class DATA_WAIT_FOR_FUNDING_CREATED(params: ChannelParams, +final case class DATA_WAIT_FOR_FUNDING_CREATED(channelParams: ChannelParams, + channelType: SupportedChannelType, + localCommitParams: CommitParams, + remoteCommitParams: CommitParams, fundingAmount: Satoshi, pushAmount: MilliSatoshi, commitTxFeerate: FeeratePerKw, remoteFundingPubKey: PublicKey, remoteFirstPerCommitmentPoint: PublicKey) extends TransientChannelData { - val channelId: ByteVector32 = params.channelId + val channelId: ByteVector32 = channelParams.channelId + val commitmentFormat: CommitmentFormat = channelType.commitmentFormat } -final case class DATA_WAIT_FOR_FUNDING_SIGNED(params: ChannelParams, +final case class DATA_WAIT_FOR_FUNDING_SIGNED(channelParams: ChannelParams, + channelType: SupportedChannelType, + localCommitParams: CommitParams, + remoteCommitParams: CommitParams, remoteFundingPubKey: PublicKey, fundingTx: Transaction, fundingTxFee: Satoshi, @@ -588,7 +606,8 @@ final case class DATA_WAIT_FOR_FUNDING_SIGNED(params: ChannelParams, remoteCommit: RemoteCommit, lastSent: FundingCreated, replyTo: akka.actor.typed.ActorRef[Peer.OpenChannelResponse]) extends TransientChannelData { - val channelId: ByteVector32 = params.channelId + val channelId: ByteVector32 = channelParams.channelId + val commitmentFormat: CommitmentFormat = channelType.commitmentFormat } final case class DATA_WAIT_FOR_FUNDING_CONFIRMED(commitments: Commitments, waitingSince: BlockHeight, // how long have we been waiting for the funding tx to confirm @@ -606,6 +625,8 @@ final case class DATA_WAIT_FOR_ACCEPT_DUAL_FUNDED_CHANNEL(init: INPUT_INIT_CHANN } final case class DATA_WAIT_FOR_DUAL_FUNDING_CREATED(channelId: ByteVector32, channelParams: ChannelParams, + localCommitParams: CommitParams, + remoteCommitParams: CommitParams, secondRemotePerCommitmentPoint: PublicKey, localPushAmount: MilliSatoshi, remotePushAmount: MilliSatoshi, @@ -616,8 +637,7 @@ final case class DATA_WAIT_FOR_DUAL_FUNDING_SIGNED(channelParams: ChannelParams, secondRemotePerCommitmentPoint: PublicKey, localPushAmount: MilliSatoshi, remotePushAmount: MilliSatoshi, - signingSession: InteractiveTxSigningSession.WaitingForSigs, - remoteChannelData_opt: Option[ByteVector]) extends ChannelDataWithoutCommitments + signingSession: InteractiveTxSigningSession.WaitingForSigs) extends ChannelDataWithoutCommitments final case class DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED(commitments: Commitments, localPushAmount: MilliSatoshi, remotePushAmount: MilliSatoshi, @@ -625,9 +645,9 @@ final case class DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED(commitments: Commitments, lastChecked: BlockHeight, // last time we checked if the channel was double-spent status: DualFundingStatus, deferred: Option[ChannelReady]) extends ChannelDataWithCommitments { - def allFundingTxs: Seq[DualFundedUnconfirmedFundingTx] = commitments.active.map(_.localFundingStatus).collect { case fundingTx: DualFundedUnconfirmedFundingTx => fundingTx } - def latestFundingTx: DualFundedUnconfirmedFundingTx = commitments.latest.localFundingStatus.asInstanceOf[DualFundedUnconfirmedFundingTx] - def previousFundingTxs: Seq[DualFundedUnconfirmedFundingTx] = allFundingTxs diff Seq(latestFundingTx) + def allFundingTxs: Seq[LocalFundingStatus.DualFundedUnconfirmedFundingTx] = commitments.active.map(_.localFundingStatus).collect { case fundingTx: LocalFundingStatus.DualFundedUnconfirmedFundingTx => fundingTx } + def latestFundingTx: LocalFundingStatus.DualFundedUnconfirmedFundingTx = commitments.latest.localFundingStatus.asInstanceOf[LocalFundingStatus.DualFundedUnconfirmedFundingTx] + def previousFundingTxs: Seq[LocalFundingStatus.DualFundedUnconfirmedFundingTx] = allFundingTxs diff Seq(latestFundingTx) } final case class DATA_WAIT_FOR_DUAL_FUNDING_READY(commitments: Commitments, aliases: ShortIdAliases) extends ChannelDataWithCommitments @@ -635,10 +655,10 @@ final case class DATA_NORMAL(commitments: Commitments, aliases: ShortIdAliases, lastAnnouncement_opt: Option[ChannelAnnouncement], channelUpdate: ChannelUpdate, + spliceStatus: SpliceStatus, localShutdown: Option[Shutdown], remoteShutdown: Option[Shutdown], - closeStatus_opt: Option[CloseStatus], - spliceStatus: SpliceStatus) extends ChannelDataWithCommitments { + closeStatus_opt: Option[CloseStatus]) extends ChannelDataWithCommitments { val lastAnnouncedCommitment_opt: Option[AnnouncedCommitment] = lastAnnouncement_opt.flatMap(ann => commitments.resolveCommitment(ann.shortChannelId).map(c => AnnouncedCommitment(c, ann))) val lastAnnouncedFundingTxId_opt: Option[TxId] = lastAnnouncedCommitment_opt.map(_.fundingTxId) val isNegotiatingQuiescence: Boolean = spliceStatus.isNegotiatingQuiescence @@ -650,7 +670,7 @@ final case class DATA_NEGOTIATING(commitments: Commitments, closingTxProposed: List[List[ClosingTxProposed]], // one list for every negotiation (there can be several in case of disconnection) bestUnpublishedClosingTx_opt: Option[ClosingTx]) extends ChannelDataWithCommitments { require(closingTxProposed.nonEmpty, "there must always be a list for the current negotiation") - require(!commitments.params.localParams.paysClosingFees || closingTxProposed.forall(_.nonEmpty), "initiator must have at least one closing signature for every negotiation attempt because it initiates the closing") + require(!commitments.localChannelParams.paysClosingFees || closingTxProposed.forall(_.nonEmpty), "initiator must have at least one closing signature for every negotiation attempt because it initiates the closing") } final case class DATA_NEGOTIATING_SIMPLE(commitments: Commitments, lastClosingFeerate: FeeratePerKw, @@ -671,52 +691,147 @@ final case class DATA_CLOSING(commitments: Commitments, remoteCommitPublished: Option[RemoteCommitPublished] = None, nextRemoteCommitPublished: Option[RemoteCommitPublished] = None, futureRemoteCommitPublished: Option[RemoteCommitPublished] = None, - revokedCommitPublished: List[RevokedCommitPublished] = Nil) extends ChannelDataWithCommitments { + revokedCommitPublished: List[RevokedCommitPublished] = Nil, + maxClosingFeerate_opt: Option[FeeratePerKw] = None) extends ChannelDataWithCommitments { val spendingTxs: List[Transaction] = mutualClosePublished.map(_.tx) ::: localCommitPublished.map(_.commitTx).toList ::: remoteCommitPublished.map(_.commitTx).toList ::: nextRemoteCommitPublished.map(_.commitTx).toList ::: futureRemoteCommitPublished.map(_.commitTx).toList ::: revokedCommitPublished.map(_.commitTx) require(spendingTxs.nonEmpty, "there must be at least one tx published in this state") } final case class DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT(commitments: Commitments, remoteChannelReestablish: ChannelReestablish) extends ChannelDataWithCommitments +/** We use this class when a channel shouldn't be stored in the DB (e.g. because it never confirmed). */ +case class IgnoreClosedData(previousData: ChannelData) extends ClosedData { + val channelId: ByteVector32 = previousData.channelId +} + /** - * @param initFeatures current connection features, or last features used if the channel is disconnected. Note that these - * features are updated at each reconnection and may be different from the channel permanent features - * (see [[ChannelFeatures]]). + * This class contains the data we will keep in our DB for every closed channel. + * It shouldn't contain data we may wish to remove in the future, otherwise we'll have backwards-compatibility issues. + * This is why for example the commitmentFormat is a string instead of using the [[CommitmentFormat]] trait, to allow + * storing legacy cases that we don't support anymore for active channels. + * + * Note that we only store channels that have been fully opened and for which we had something at stake. Channels that + * are cancelled before having a confirmed funding transactions are ignored, which protects against spam. */ -case class LocalParams(nodeId: PublicKey, - fundingKeyPath: DeterministicWallet.KeyPath, - dustLimit: Satoshi, - maxHtlcValueInFlightMsat: MilliSatoshi, - initialRequestedChannelReserve_opt: Option[Satoshi], - htlcMinimum: MilliSatoshi, - toSelfDelay: CltvExpiryDelta, - maxAcceptedHtlcs: Int, - isChannelOpener: Boolean, - paysCommitTxFees: Boolean, - upfrontShutdownScript_opt: Option[ByteVector], - walletStaticPaymentBasepoint: Option[PublicKey], - initFeatures: Features[InitFeature]) { +final case class DATA_CLOSED(channelId: ByteVector32, + remoteNodeId: PublicKey, + fundingTxId: TxId, + fundingOutputIndex: Long, + fundingTxIndex: Long, + fundingKeyPath: String, + channelFeatures: String, + isChannelOpener: Boolean, + commitmentFormat: String, + announced: Boolean, + capacity: Satoshi, + closingTxId: TxId, + closingType: String, + closingScript: ByteVector, + localBalance: MilliSatoshi, + remoteBalance: MilliSatoshi, + closingAmount: Satoshi) extends ClosedData + +object DATA_CLOSED { + def apply(d: DATA_NEGOTIATING_SIMPLE, closingTx: ClosingTx): DATA_CLOSED = DATA_CLOSED( + channelId = d.channelId, + remoteNodeId = d.remoteNodeId, + fundingTxId = d.commitments.latest.fundingTxId, + fundingOutputIndex = d.commitments.latest.fundingInput.index, + fundingTxIndex = d.commitments.latest.fundingTxIndex, + fundingKeyPath = d.commitments.channelParams.localParams.fundingKeyPath.toString(), + channelFeatures = d.commitments.channelParams.channelFeatures.toString, + isChannelOpener = d.commitments.latest.channelParams.localParams.isChannelOpener, + commitmentFormat = d.commitments.latest.commitmentFormat.toString, + announced = d.commitments.latest.channelParams.announceChannel, + capacity = d.commitments.latest.capacity, + closingTxId = closingTx.tx.txid, + closingType = Helpers.Closing.MutualClose(closingTx).toString, + closingScript = d.localScriptPubKey, + localBalance = d.commitments.latest.localCommit.spec.toLocal, + remoteBalance = d.commitments.latest.localCommit.spec.toRemote, + closingAmount = closingTx.toLocalOutput_opt.map(_.amount).getOrElse(0 sat) + ) + + def apply(d: DATA_CLOSING, closingType: Helpers.Closing.ClosingType): DATA_CLOSED = DATA_CLOSED( + channelId = d.channelId, + remoteNodeId = d.remoteNodeId, + fundingTxId = d.commitments.latest.fundingTxId, + fundingOutputIndex = d.commitments.latest.fundingInput.index, + fundingTxIndex = d.commitments.latest.fundingTxIndex, + fundingKeyPath = d.commitments.channelParams.localParams.fundingKeyPath.toString(), + channelFeatures = d.commitments.channelParams.channelFeatures.toString, + isChannelOpener = d.commitments.latest.channelParams.localParams.isChannelOpener, + commitmentFormat = d.commitments.latest.commitmentFormat.toString, + announced = d.commitments.latest.channelParams.announceChannel, + capacity = d.commitments.latest.capacity, + closingTxId = closingType match { + case Closing.MutualClose(closingTx) => closingTx.tx.txid + case Closing.LocalClose(_, localCommitPublished) => localCommitPublished.commitTx.txid + case Closing.CurrentRemoteClose(_, remoteCommitPublished) => remoteCommitPublished.commitTx.txid + case Closing.NextRemoteClose(_, remoteCommitPublished) => remoteCommitPublished.commitTx.txid + case Closing.RecoveryClose(remoteCommitPublished) => remoteCommitPublished.commitTx.txid + case Closing.RevokedClose(revokedCommitPublished) => revokedCommitPublished.commitTx.txid + }, + closingType = closingType.toString, + closingScript = d.finalScriptPubKey, + localBalance = closingType match { + case _: Closing.CurrentRemoteClose => d.commitments.latest.remoteCommit.spec.toRemote + case _: Closing.NextRemoteClose => d.commitments.latest.nextRemoteCommit_opt.getOrElse(d.commitments.latest.remoteCommit).spec.toRemote + case _ => d.commitments.latest.localCommit.spec.toLocal + }, + remoteBalance = closingType match { + case _: Closing.CurrentRemoteClose => d.commitments.latest.remoteCommit.spec.toLocal + case _: Closing.NextRemoteClose => d.commitments.latest.nextRemoteCommit_opt.getOrElse(d.commitments.latest.remoteCommit).spec.toLocal + case _ => d.commitments.latest.localCommit.spec.toRemote + }, + closingAmount = closingType match { + case Closing.MutualClose(closingTx) => closingTx.toLocalOutput_opt.map(_.amount).getOrElse(0 sat) + case Closing.LocalClose(_, localCommitPublished) => Closing.closingBalance(d.finalScriptPubKey, localCommitPublished) + case Closing.CurrentRemoteClose(_, remoteCommitPublished) => Closing.closingBalance(d.finalScriptPubKey, remoteCommitPublished) + case Closing.NextRemoteClose(_, remoteCommitPublished) => Closing.closingBalance(d.finalScriptPubKey, remoteCommitPublished) + case Closing.RecoveryClose(remoteCommitPublished) => Closing.closingBalance(d.finalScriptPubKey, remoteCommitPublished) + case Closing.RevokedClose(revokedCommitPublished) => Closing.closingBalance(d.finalScriptPubKey, revokedCommitPublished) + } + ) +} + +/** Local params that apply for the channel's lifetime. */ +case class LocalChannelParams(nodeId: PublicKey, + fundingKeyPath: DeterministicWallet.KeyPath, + // Channel reserve applied to the remote peer, if we're not using [[Features.DualFunding]] (in + // which case the reserve is set to 1%). If the channel is spliced, this initial value will be + // ignored in favor of a 1% reserve of the resulting capacity. + initialRequestedChannelReserve_opt: Option[Satoshi], + isChannelOpener: Boolean, + paysCommitTxFees: Boolean, + upfrontShutdownScript_opt: Option[ByteVector], + // Current connection features, or last features used if the channel is disconnected. Note that + // these features are updated at each reconnection and may be different from the channel permanent + // features (see [[ChannelFeatures]]). + initFeatures: Features[InitFeature]) { // The node responsible for the commit tx fees is also the node paying the mutual close fees. // The other node's balance may be empty, which wouldn't allow them to pay the closing fees. val paysClosingFees: Boolean = paysCommitTxFees } -/** - * @param initFeatures see [[LocalParams.initFeatures]] - */ -case class RemoteParams(nodeId: PublicKey, - dustLimit: Satoshi, - maxHtlcValueInFlightMsat: UInt64, // this is not MilliSatoshi because it can exceed the total amount of MilliSatoshi - initialRequestedChannelReserve_opt: Option[Satoshi], +/** Remote params that apply for the channel's lifetime. */ +case class RemoteChannelParams(nodeId: PublicKey, + // See comment in LocalChannelParams for details. + initialRequestedChannelReserve_opt: Option[Satoshi], + revocationBasepoint: PublicKey, + paymentBasepoint: PublicKey, + delayedPaymentBasepoint: PublicKey, + htlcBasepoint: PublicKey, + // See comment in LocalChannelParams for details. + initFeatures: Features[InitFeature], + upfrontShutdownScript_opt: Option[ByteVector]) + +/** Configuration parameters that apply to local or remote commitment transactions, and may be updated dynamically. */ +case class CommitParams(dustLimit: Satoshi, htlcMinimum: MilliSatoshi, - toSelfDelay: CltvExpiryDelta, + maxHtlcValueInFlight: UInt64, maxAcceptedHtlcs: Int, - revocationBasepoint: PublicKey, - paymentBasepoint: PublicKey, - delayedPaymentBasepoint: PublicKey, - htlcBasepoint: PublicKey, - initFeatures: Features[InitFeature], - upfrontShutdownScript_opt: Option[ByteVector]) + toSelfDelay: CltvExpiryDelta) /** * The [[nonInitiatorPaysCommitFees]] parameter is set to true when the sender wants the receiver to pay the commitment transaction fees. diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelEvents.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelEvents.scala index f827873337..9421fe50b2 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelEvents.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelEvents.scala @@ -21,7 +21,7 @@ import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.bitcoin.scalacompat.{ByteVector32, Satoshi, Transaction, TxId} import fr.acinq.eclair.blockchain.fee.FeeratePerKw import fr.acinq.eclair.channel.Helpers.Closing.ClosingType -import fr.acinq.eclair.wire.protocol.{ChannelAnnouncement, ChannelUpdate, LiquidityAds} +import fr.acinq.eclair.wire.protocol.{ChannelAnnouncement, ChannelUpdate, HtlcFailureMessage, LiquidityAds, UpdateAddHtlc, UpdateFulfillHtlc} import fr.acinq.eclair.{BlockHeight, Features, MilliSatoshi, RealShortChannelId, ShortChannelId} /** @@ -62,7 +62,7 @@ case class LocalChannelUpdate(channel: ActorRef, channelId: ByteVector32, aliase * However we only include the real scid if option_scid_alias is disabled, because we otherwise want to hide it. */ def scidsForRouting: Seq[ShortChannelId] = { - val canUseRealScid = !commitments.params.channelFeatures.hasFeature(Features.ScidAlias) + val canUseRealScid = !commitments.channelParams.channelFeatures.hasFeature(Features.ScidAlias) if (canUseRealScid) { announcement_opt.map(_.shortChannelId).toSeq :+ aliases.localAlias } else { @@ -104,3 +104,11 @@ case class ChannelPersisted(channel: ActorRef, remoteNodeId: PublicKey, channelI case class LocalCommitConfirmed(channel: ActorRef, remoteNodeId: PublicKey, channelId: ByteVector32, refundAtBlock: BlockHeight) extends ChannelEvent case class ChannelClosed(channel: ActorRef, channelId: ByteVector32, closingType: ClosingType, commitments: Commitments) extends ChannelEvent + +case class OutgoingHtlcAdded(add: UpdateAddHtlc, remoteNodeId: PublicKey, upstream: Upstream.Hot, fee: MilliSatoshi) + +sealed trait OutgoingHtlcSettled + +case class OutgoingHtlcFailed(fail: HtlcFailureMessage) extends OutgoingHtlcSettled + +case class OutgoingHtlcFulfilled(fulfill: UpdateFulfillHtlc) extends OutgoingHtlcSettled diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelExceptions.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelExceptions.scala index eb37925c1d..884ed42273 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelExceptions.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelExceptions.scala @@ -34,6 +34,7 @@ case class RemoteError(e: protocol.Error) extends ChannelError // @formatter:on class ChannelException(val channelId: ByteVector32, message: String) extends RuntimeException(message) +class ChannelJammingException(override val channelId: ByteVector32, message: String) extends ChannelException(channelId, message) // @formatter:off case class InvalidChainHash (override val channelId: ByteVector32, local: BlockHash, remote: BlockHash) extends ChannelException(channelId, s"invalid chainHash (local=$local remote=$remote)") @@ -43,7 +44,7 @@ case class FundingAmountTooHigh (override val channelId: Byte case class InvalidFundingBalances (override val channelId: ByteVector32, fundingAmount: Satoshi, localBalance: MilliSatoshi, remoteBalance: MilliSatoshi) extends ChannelException(channelId, s"invalid balances funding_amount=$fundingAmount local=$localBalance remote=$remoteBalance") case class InvalidPushAmount (override val channelId: ByteVector32, pushAmount: MilliSatoshi, max: MilliSatoshi) extends ChannelException(channelId, s"invalid pushAmount=$pushAmount (max=$max)") case class InvalidMaxAcceptedHtlcs (override val channelId: ByteVector32, maxAcceptedHtlcs: Int, max: Int) extends ChannelException(channelId, s"invalid max_accepted_htlcs=$maxAcceptedHtlcs (max=$max)") -case class InvalidChannelType (override val channelId: ByteVector32, ourChannelType: ChannelType, theirChannelType: ChannelType) extends ChannelException(channelId, s"invalid channel_type=$theirChannelType, expected channel_type=$ourChannelType") +case class InvalidChannelType (override val channelId: ByteVector32, remoteChannelType: ChannelType) extends ChannelException(channelId, s"invalid channel_type=$remoteChannelType") case class MissingChannelType (override val channelId: ByteVector32) extends ChannelException(channelId, "option_channel_type was negotiated but channel_type is missing") case class DustLimitTooSmall (override val channelId: ByteVector32, dustLimit: Satoshi, min: Satoshi) extends ChannelException(channelId, s"dustLimit=$dustLimit is too small (min=$min)") case class DustLimitTooLarge (override val channelId: ByteVector32, dustLimit: Satoshi, max: Satoshi) extends ChannelException(channelId, s"dustLimit=$dustLimit is too large (max=$max)") @@ -94,8 +95,8 @@ case class InvalidSpliceTxAbortNotAcked (override val channelId: Byte case class InvalidSpliceNotQuiescent (override val channelId: ByteVector32) extends ChannelException(channelId, "invalid splice attempt: the channel is not quiescent") case class InvalidSpliceWithUnconfirmedTx (override val channelId: ByteVector32, fundingTx: TxId) extends ChannelException(channelId, s"invalid splice attempt: the current funding transaction is still unconfirmed (txId=$fundingTx), you should use tx_init_rbf instead") case class InvalidRbfTxConfirmed (override val channelId: ByteVector32) extends ChannelException(channelId, "no need to rbf, transaction is already confirmed") -case class InvalidRbfNonInitiator (override val channelId: ByteVector32) extends ChannelException(channelId, "cannot initiate rbf: we're not the initiator of this interactive-tx attempt") case class InvalidRbfZeroConf (override val channelId: ByteVector32) extends ChannelException(channelId, "cannot initiate rbf: we're using zero-conf for this interactive-tx attempt") +case class InvalidRbfOverridesLiquidityPurchase (override val channelId: ByteVector32, purchasedAmount: Satoshi) extends ChannelException(channelId, s"cannot initiate rbf attempt: our peer wanted to purchase $purchasedAmount of liquidity that we would override, they must initiate rbf") case class InvalidRbfMissingLiquidityPurchase (override val channelId: ByteVector32, expectedAmount: Satoshi) extends ChannelException(channelId, s"cannot accept rbf attempt: the previous attempt contained a liquidity purchase of $expectedAmount but this one doesn't contain any liquidity purchase") case class InvalidRbfAttempt (override val channelId: ByteVector32) extends ChannelException(channelId, "invalid rbf attempt") case class NoMoreHtlcsClosingInProgress (override val channelId: ByteVector32) extends ChannelException(channelId, "cannot send new htlcs, closing in progress") @@ -114,7 +115,7 @@ case class HtlcOverriddenByLocalCommit (override val channelId: Byte case class FeerateTooSmall (override val channelId: ByteVector32, remoteFeeratePerKw: FeeratePerKw) extends ChannelException(channelId, s"remote fee rate is too small: remoteFeeratePerKw=${remoteFeeratePerKw.toLong}") case class FeerateTooDifferent (override val channelId: ByteVector32, localFeeratePerKw: FeeratePerKw, remoteFeeratePerKw: FeeratePerKw) extends ChannelException(channelId, s"local/remote feerates are too different: remoteFeeratePerKw=${remoteFeeratePerKw.toLong} localFeeratePerKw=${localFeeratePerKw.toLong}") case class InvalidAnnouncementSignatures (override val channelId: ByteVector32, annSigs: AnnouncementSignatures) extends ChannelException(channelId, s"invalid announcement signatures: $annSigs") -case class InvalidCommitmentSignature (override val channelId: ByteVector32, fundingTxId: TxId, fundingTxIndex: Long, unsignedCommitTx: Transaction) extends ChannelException(channelId, s"invalid commitment signature: fundingTxId=$fundingTxId fundingTxIndex=$fundingTxIndex commitTxId=${unsignedCommitTx.txid} commitTx=$unsignedCommitTx") +case class InvalidCommitmentSignature (override val channelId: ByteVector32, fundingTxId: TxId, commitmentNumber: Long, unsignedCommitTx: Transaction) extends ChannelException(channelId, s"invalid commitment signature: fundingTxId=$fundingTxId commitmentNumber=$commitmentNumber commitTxId=${unsignedCommitTx.txid} commitTx=$unsignedCommitTx") case class InvalidHtlcSignature (override val channelId: ByteVector32, txId: TxId) extends ChannelException(channelId, s"invalid htlc signature: txId=$txId") case class CannotGenerateClosingTx (override val channelId: ByteVector32) extends ChannelException(channelId, "failed to generate closing transaction: all outputs are trimmed") case class MissingCloseSignature (override val channelId: ByteVector32) extends ChannelException(channelId, "closing_complete is missing a signature for a closing transaction including our output") @@ -128,7 +129,7 @@ case class UnexpectedHtlcId (override val channelId: Byte case class ExpiryTooSmall (override val channelId: ByteVector32, minimum: CltvExpiry, actual: CltvExpiry, blockHeight: BlockHeight) extends ChannelException(channelId, s"expiry too small: minimum=$minimum actual=$actual blockHeight=$blockHeight") case class ExpiryTooBig (override val channelId: ByteVector32, maximum: CltvExpiry, actual: CltvExpiry, blockHeight: BlockHeight) extends ChannelException(channelId, s"expiry too big: maximum=$maximum actual=$actual blockHeight=$blockHeight") case class HtlcValueTooSmall (override val channelId: ByteVector32, minimum: MilliSatoshi, actual: MilliSatoshi) extends ChannelException(channelId, s"htlc value too small: minimum=$minimum actual=$actual") -case class HtlcValueTooHighInFlight (override val channelId: ByteVector32, maximum: MilliSatoshi, actual: MilliSatoshi) extends ChannelException(channelId, s"in-flight htlcs hold too much value: maximum=$maximum actual=$actual") +case class HtlcValueTooHighInFlight (override val channelId: ByteVector32, maximum: UInt64, actual: MilliSatoshi) extends ChannelException(channelId, s"in-flight htlcs hold too much value: maximum=${maximum.toBigInt} msat actual=$actual") case class TooManyAcceptedHtlcs (override val channelId: ByteVector32, maximum: Long) extends ChannelException(channelId, s"too many accepted htlcs: maximum=$maximum") case class LocalDustHtlcExposureTooHigh (override val channelId: ByteVector32, maximum: Satoshi, actual: MilliSatoshi) extends ChannelException(channelId, s"dust htlcs hold too much value: maximum=$maximum actual=$actual") case class RemoteDustHtlcExposureTooHigh (override val channelId: ByteVector32, maximum: Satoshi, actual: MilliSatoshi) extends ChannelException(channelId, s"dust htlcs hold too much value: maximum=$maximum actual=$actual") @@ -150,4 +151,12 @@ case class CommandUnavailableInThisState (override val channelId: Byte case class ForbiddenDuringSplice (override val channelId: ByteVector32, command: String) extends ChannelException(channelId, s"cannot process $command while splicing") case class ForbiddenDuringQuiescence (override val channelId: ByteVector32, command: String) extends ChannelException(channelId, s"cannot process $command while quiescent") case class ConcurrentRemoteSplice (override val channelId: ByteVector32) extends ChannelException(channelId, "splice attempt canceled, remote initiated splice before us") +case class TooManySmallHtlcs (override val channelId: ByteVector32, number: Long, below: MilliSatoshi) extends ChannelJammingException(channelId, s"too many small htlcs: $number HTLCs below $below") +case class IncomingConfidenceTooLow (override val channelId: ByteVector32, confidence: Double, occupancy: Double) extends ChannelJammingException(channelId, s"incoming confidence too low: confidence=$confidence occupancy=$occupancy") +case class OutgoingConfidenceTooLow (override val channelId: ByteVector32, confidence: Double, occupancy: Double) extends ChannelJammingException(channelId, s"outgoing confidence too low: confidence=$confidence occupancy=$occupancy") +case class MissingCommitNonce (override val channelId: ByteVector32, fundingTxId: TxId, commitmentNumber: Long) extends ChannelException(channelId, s"commit nonce for funding tx $fundingTxId and commitmentNumber=$commitmentNumber is missing") +case class InvalidCommitNonce (override val channelId: ByteVector32, fundingTxId: TxId, commitmentNumber: Long) extends ChannelException(channelId, s"commit nonce for funding tx $fundingTxId and commitmentNumber=$commitmentNumber is not valid") +case class MissingFundingNonce (override val channelId: ByteVector32, fundingTxId: TxId) extends ChannelException(channelId, s"funding nonce for funding tx $fundingTxId is missing") +case class InvalidFundingNonce (override val channelId: ByteVector32, fundingTxId: TxId) extends ChannelException(channelId, s"funding nonce for funding tx $fundingTxId is not valid") +case class MissingClosingNonce (override val channelId: ByteVector32) extends ChannelException(channelId, "closing nonce is missing") // @formatter:on \ No newline at end of file diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelFeatures.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelFeatures.scala index 3ab4c20b0e..e557ceed7b 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelFeatures.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelFeatures.scala @@ -16,7 +16,8 @@ package fr.acinq.eclair.channel -import fr.acinq.eclair.transactions.Transactions.{CommitmentFormat, DefaultCommitmentFormat, UnsafeLegacyAnchorOutputsCommitmentFormat, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat} +import fr.acinq.bitcoin.scalacompat.ByteVector32 +import fr.acinq.eclair.transactions.Transactions._ import fr.acinq.eclair.{ChannelTypeFeature, FeatureSupport, Features, InitFeature, PermanentChannelFeature} /** @@ -25,22 +26,10 @@ import fr.acinq.eclair.{ChannelTypeFeature, FeatureSupport, Features, InitFeatur /** * Subset of Bolt 9 features used to configure a channel and applicable over the lifetime of that channel. - * Even if one of these features is later disabled at the connection level, it will still apply to the channel until the - * channel is upgraded or closed. + * Even if one of these features is later disabled at the connection level, it will still apply to the channel. */ case class ChannelFeatures(features: Set[PermanentChannelFeature]) { - /** True if our main output in the remote commitment is directly sent (without any delay) to one of our wallet addresses. */ - val paysDirectlyToWallet: Boolean = hasFeature(Features.StaticRemoteKey) && !hasFeature(Features.AnchorOutputs) && !hasFeature(Features.AnchorOutputsZeroFeeHtlcTx) - /** Legacy option_anchor_outputs is used for Phoenix, because Phoenix doesn't have an on-chain wallet to pay for fees. */ - val commitmentFormat: CommitmentFormat = if (hasFeature(Features.AnchorOutputs)) { - UnsafeLegacyAnchorOutputsCommitmentFormat - } else if (hasFeature(Features.AnchorOutputsZeroFeeHtlcTx)) { - ZeroFeeHtlcTxAnchorOutputsCommitmentFormat - } else { - DefaultCommitmentFormat - } - def hasFeature(feature: PermanentChannelFeature): Boolean = features.contains(feature) override def toString: String = features.mkString(",") @@ -51,19 +40,20 @@ object ChannelFeatures { def apply(features: PermanentChannelFeature*): ChannelFeatures = ChannelFeatures(Set.from(features)) - /** Enrich the channel type with other permanent features that will be applied to the channel. */ + /** Configure permanent channel features based on local and remote feature. */ def apply(channelType: SupportedChannelType, localFeatures: Features[InitFeature], remoteFeatures: Features[InitFeature], announceChannel: Boolean): ChannelFeatures = { - val additionalPermanentFeatures = Features.knownFeatures.collect { + val permanentFeatures = Features.knownFeatures.collect { // If we both support 0-conf or scid_alias, we use it even if it wasn't in the channel-type. + // Note that we cannot use scid_alias if the channel is announced. case Features.ScidAlias if Features.canUseFeature(localFeatures, remoteFeatures, Features.ScidAlias) && !announceChannel => Some(Features.ScidAlias) + case Features.ScidAlias => None case Features.ZeroConf if Features.canUseFeature(localFeatures, remoteFeatures, Features.ZeroConf) => Some(Features.ZeroConf) - // Other channel-type features are negotiated in the channel-type, we ignore their value from the init message. - case _: ChannelTypeFeature => None - // We add all other permanent channel features that aren't negotiated as part of the channel-type. + // We add all other permanent channel features that we both support. case f: PermanentChannelFeature if Features.canUseFeature(localFeatures, remoteFeatures, f) => Some(f) }.flatten - val allPermanentFeatures = channelType.features.toSeq ++ additionalPermanentFeatures - ChannelFeatures(allPermanentFeatures: _*) + // Some permanent features can be negotiated as part of the channel-type. + val channelTypeFeatures = channelType.features.collect { case f: PermanentChannelFeature => f } + ChannelFeatures(permanentFeatures ++ channelTypeFeatures) } } @@ -78,9 +68,6 @@ sealed trait SupportedChannelType extends ChannelType { /** Known channel-type features */ override def features: Set[ChannelTypeFeature] - /** True if our main output in the remote commitment is directly sent (without any delay) to one of our wallet addresses. */ - def paysDirectlyToWallet: Boolean - /** Format of the channel transactions. */ def commitmentFormat: CommitmentFormat } @@ -88,25 +75,6 @@ sealed trait SupportedChannelType extends ChannelType { object ChannelTypes { // @formatter:off - case class Standard(scidAlias: Boolean = false, zeroConf: Boolean = false) extends SupportedChannelType { - override def features: Set[ChannelTypeFeature] = Set( - if (scidAlias) Some(Features.ScidAlias) else None, - if (zeroConf) Some(Features.ZeroConf) else None, - ).flatten - override def paysDirectlyToWallet: Boolean = false - override def commitmentFormat: CommitmentFormat = DefaultCommitmentFormat - override def toString: String = s"standard${if (scidAlias) "+scid_alias" else ""}${if (zeroConf) "+zeroconf" else ""}" - } - case class StaticRemoteKey(scidAlias: Boolean = false, zeroConf: Boolean = false) extends SupportedChannelType { - override def features: Set[ChannelTypeFeature] = Set( - if (scidAlias) Some(Features.ScidAlias) else None, - if (zeroConf) Some(Features.ZeroConf) else None, - Some(Features.StaticRemoteKey) - ).flatten - override def paysDirectlyToWallet: Boolean = true - override def commitmentFormat: CommitmentFormat = DefaultCommitmentFormat - override def toString: String = s"static_remotekey${if (scidAlias) "+scid_alias" else ""}${if (zeroConf) "+zeroconf" else ""}" - } case class AnchorOutputs(scidAlias: Boolean = false, zeroConf: Boolean = false) extends SupportedChannelType { override def features: Set[ChannelTypeFeature] = Set( if (scidAlias) Some(Features.ScidAlias) else None, @@ -114,7 +82,6 @@ object ChannelTypes { Some(Features.StaticRemoteKey), Some(Features.AnchorOutputs) ).flatten - override def paysDirectlyToWallet: Boolean = false override def commitmentFormat: CommitmentFormat = UnsafeLegacyAnchorOutputsCommitmentFormat override def toString: String = s"anchor_outputs${if (scidAlias) "+scid_alias" else ""}${if (zeroConf) "+zeroconf" else ""}" } @@ -125,25 +92,34 @@ object ChannelTypes { Some(Features.StaticRemoteKey), Some(Features.AnchorOutputsZeroFeeHtlcTx) ).flatten - override def paysDirectlyToWallet: Boolean = false override def commitmentFormat: CommitmentFormat = ZeroFeeHtlcTxAnchorOutputsCommitmentFormat override def toString: String = s"anchor_outputs_zero_fee_htlc_tx${if (scidAlias) "+scid_alias" else ""}${if (zeroConf) "+zeroconf" else ""}" } + case class SimpleTaprootChannelsStaging(scidAlias: Boolean = false, zeroConf: Boolean = false) extends SupportedChannelType { + override def features: Set[ChannelTypeFeature] = Set( + if (scidAlias) Some(Features.ScidAlias) else None, + if (zeroConf) Some(Features.ZeroConf) else None, + Some(Features.SimpleTaprootChannelsStaging), + ).flatten + override def commitmentFormat: CommitmentFormat = ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat + override def toString: String = s"simple_taproot_channel_staging${if (scidAlias) "+scid_alias" else ""}${if (zeroConf) "+zeroconf" else ""}" + } + case class UnsupportedChannelType(featureBits: Features[InitFeature]) extends ChannelType { override def features: Set[InitFeature] = featureBits.activated.keySet override def toString: String = s"0x${featureBits.toByteVector.toHex}" } + + // Phoenix uses custom channel types, that we may remove in the future. + case object SimpleTaprootChannelsPhoenix extends SupportedChannelType { + override def features: Set[ChannelTypeFeature] = Set(Features.PhoenixZeroReserve, Features.SimpleTaprootChannelsPhoenix) + override def commitmentFormat: CommitmentFormat = PhoenixSimpleTaprootChannelCommitmentFormat + override def toString: String = "phoenix_simple_taproot_channel" + } + // @formatter:on private val features2ChannelType: Map[Features[_ <: InitFeature], SupportedChannelType] = Set( - Standard(), - Standard(zeroConf = true), - Standard(scidAlias = true), - Standard(scidAlias = true, zeroConf = true), - StaticRemoteKey(), - StaticRemoteKey(zeroConf = true), - StaticRemoteKey(scidAlias = true), - StaticRemoteKey(scidAlias = true, zeroConf = true), AnchorOutputs(), AnchorOutputs(zeroConf = true), AnchorOutputs(scidAlias = true), @@ -151,40 +127,26 @@ object ChannelTypes { AnchorOutputsZeroFeeHtlcTx(), AnchorOutputsZeroFeeHtlcTx(zeroConf = true), AnchorOutputsZeroFeeHtlcTx(scidAlias = true), - AnchorOutputsZeroFeeHtlcTx(scidAlias = true, zeroConf = true)) - .map(channelType => Features(channelType.features.map(_ -> FeatureSupport.Mandatory).toMap) -> channelType) - .toMap + AnchorOutputsZeroFeeHtlcTx(scidAlias = true, zeroConf = true), + SimpleTaprootChannelsStaging(), + SimpleTaprootChannelsStaging(zeroConf = true), + SimpleTaprootChannelsStaging(scidAlias = true), + SimpleTaprootChannelsStaging(scidAlias = true, zeroConf = true), + SimpleTaprootChannelsPhoenix, + ).map { + channelType => Features(channelType.features.map(_ -> FeatureSupport.Mandatory).toMap) -> channelType + }.toMap // NB: Bolt 2: features must exactly match in order to identify a channel type. def fromFeatures(features: Features[InitFeature]): ChannelType = features2ChannelType.getOrElse(features, UnsupportedChannelType(features)) - /** Pick the channel type based on local and remote feature bits, as defined by the spec. */ - def defaultFromFeatures(localFeatures: Features[InitFeature], remoteFeatures: Features[InitFeature], announceChannel: Boolean): SupportedChannelType = { - def canUse(feature: InitFeature): Boolean = Features.canUseFeature(localFeatures, remoteFeatures, feature) - - val scidAlias = canUse(Features.ScidAlias) && !announceChannel // alias feature is incompatible with public channel - val zeroConf = canUse(Features.ZeroConf) - if (canUse(Features.AnchorOutputsZeroFeeHtlcTx)) { - AnchorOutputsZeroFeeHtlcTx(scidAlias, zeroConf) - } else if (canUse(Features.AnchorOutputs)) { - AnchorOutputs(scidAlias, zeroConf) - } else if (canUse(Features.StaticRemoteKey)) { - StaticRemoteKey(scidAlias, zeroConf) - } else { - Standard(scidAlias, zeroConf) - } - } - /** Check if a given channel type is compatible with our features. */ - def areCompatible(localFeatures: Features[InitFeature], remoteChannelType: ChannelType): Option[SupportedChannelType] = remoteChannelType match { - case _: UnsupportedChannelType => None + def areCompatible(channelId: ByteVector32, localFeatures: Features[InitFeature], remoteChannelType_opt: Option[ChannelType]): Either[ChannelException, SupportedChannelType] = remoteChannelType_opt match { + case None => Left(MissingChannelType(channelId)) + case Some(channelType: UnsupportedChannelType) => Left(InvalidChannelType(channelId, channelType)) // We ensure that we support the features necessary for this channel type. - case proposedChannelType: SupportedChannelType => - if (proposedChannelType.features.forall(f => localFeatures.hasFeature(f))) { - Some(proposedChannelType) - } else { - None - } + case Some(proposedChannelType: SupportedChannelType) if proposedChannelType.features.forall(f => localFeatures.hasFeature(f)) => Right(proposedChannelType) + case Some(proposedChannelType: SupportedChannelType) => Left(InvalidChannelType(channelId, proposedChannelType)) } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala index 2bf8cc5e34..d2b4065fda 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala @@ -1,52 +1,44 @@ package fr.acinq.eclair.channel import akka.event.LoggingAdapter -import fr.acinq.bitcoin.crypto.musig2.IndividualNonce import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} -import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, Crypto, Satoshi, SatoshiLong, Script, Transaction, TxId} -import fr.acinq.eclair.blockchain.fee.{FeeratePerByte, FeeratePerKw, FeeratesPerKw, OnChainFeeConf} +import fr.acinq.bitcoin.scalacompat.Musig2.IndividualNonce +import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, Crypto, OutPoint, Satoshi, SatoshiLong, Transaction, TxId} +import fr.acinq.eclair.blockchain.fee.{FeeratePerKw, FeeratesPerKw, OnChainFeeConf} +import fr.acinq.eclair.channel.ChannelSpendSignature.{IndividualSignature, PartialSignatureWithNonce} import fr.acinq.eclair.channel.Helpers.Closing import fr.acinq.eclair.channel.Monitoring.{Metrics, Tags} import fr.acinq.eclair.channel.fsm.Channel.ChannelConf -import fr.acinq.eclair.crypto.keymanager.ChannelKeyManager -import fr.acinq.eclair.crypto.{Generators, ShaChain} +import fr.acinq.eclair.crypto.keymanager.{ChannelKeys, LocalCommitmentKeys, RemoteCommitmentKeys} +import fr.acinq.eclair.crypto.{NonceGenerator, ShaChain} import fr.acinq.eclair.payment.OutgoingPaymentPacket +import fr.acinq.eclair.reputation.Reputation import fr.acinq.eclair.router.Announcements import fr.acinq.eclair.transactions.Transactions._ import fr.acinq.eclair.transactions._ import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{BlockHeight, CltvExpiry, CltvExpiryDelta, Feature, Features, MilliSatoshi, MilliSatoshiLong, NodeParams, RealShortChannelId, payment} +import fr.acinq.eclair.{BlockHeight, CltvExpiry, CltvExpiryDelta, Feature, Features, MilliSatoshi, MilliSatoshiLong, NodeParams, RealShortChannelId, UInt64, payment} import scodec.bits.ByteVector /** Static channel parameters shared by all commitments. */ case class ChannelParams(channelId: ByteVector32, channelConfig: ChannelConfig, channelFeatures: ChannelFeatures, - localParams: LocalParams, remoteParams: RemoteParams, + localParams: LocalChannelParams, remoteParams: RemoteChannelParams, channelFlags: ChannelFlags) { - - require(channelFeatures.paysDirectlyToWallet == localParams.walletStaticPaymentBasepoint.isDefined, s"localParams.walletStaticPaymentBasepoint must be defined only for commitments that pay directly to our wallet (channel features: $channelFeatures") require(channelFeatures.hasFeature(Features.DualFunding) == localParams.initialRequestedChannelReserve_opt.isEmpty, "custom local channel reserve is incompatible with dual-funded channels") require(channelFeatures.hasFeature(Features.DualFunding) == remoteParams.initialRequestedChannelReserve_opt.isEmpty, "custom remote channel reserve is incompatible with dual-funded channels") - val commitmentFormat: CommitmentFormat = channelFeatures.commitmentFormat val announceChannel: Boolean = channelFlags.announceChannel - val localNodeId: PublicKey = localParams.nodeId val remoteNodeId: PublicKey = remoteParams.nodeId - - // We can safely cast to millisatoshis since we verify that it's less than a valid millisatoshi amount. - val maxHtlcAmount: MilliSatoshi = remoteParams.maxHtlcValueInFlightMsat.toBigInt.min(localParams.maxHtlcValueInFlightMsat.toLong).toLong.msat - // If we've set the 0-conf feature bit for this peer, we will always use 0-conf with them. val zeroConf: Boolean = localParams.initFeatures.hasFeature(Features.ZeroConf) - /** - * We update local/global features at reconnection - */ + /** We update local/global features at reconnection. */ def updateFeatures(localInit: Init, remoteInit: Init): ChannelParams = copy( localParams = localParams.copy(initFeatures = localInit.features), - remoteParams = remoteParams.copy(initFeatures = remoteInit.features) + remoteParams = remoteParams.copy(initFeatures = remoteInit.features), ) /** @@ -55,20 +47,6 @@ case class ChannelParams(channelId: ByteVector32, */ def minDepth(defaultMinDepth: Int): Option[Int] = if (zeroConf) None else Some(defaultMinDepth) - /** Channel reserve that applies to our funds. */ - def localChannelReserveForCapacity(capacity: Satoshi, isSplice: Boolean): Satoshi = if (channelFeatures.hasFeature(Features.DualFunding) || isSplice) { - (capacity / 100).max(remoteParams.dustLimit) - } else { - remoteParams.initialRequestedChannelReserve_opt.get // this is guarded by a require() in Params - } - - /** Channel reserve that applies to our peer's funds. */ - def remoteChannelReserveForCapacity(capacity: Satoshi, isSplice: Boolean): Satoshi = if (channelFeatures.hasFeature(Features.DualFunding) || isSplice) { - (capacity / 100).max(localParams.dustLimit) - } else { - localParams.initialRequestedChannelReserve_opt.get // this is guarded by a require() in Params - } - /** * @param localScriptPubKey local script pubkey (provided in CMD_CLOSE, as an upfront shutdown script, or set to the current final onchain script) * @return an exception if the provided script is not valid @@ -166,68 +144,98 @@ object CommitmentChanges { } } -case class HtlcTxAndRemoteSig(htlcTx: HtlcTx, remoteSig: ByteVector64) - -/** We don't store the fully signed transaction, otherwise someone with read access to our database could force-close our channels. */ -sealed trait RemoteSignature - -object RemoteSignature { - case class FullSignature(sig: ByteVector64) extends RemoteSignature - - case class PartialSignatureWithNonce(partialSig: ByteVector32, nonce: IndividualNonce) extends RemoteSignature - - def apply(sig: ByteVector64): RemoteSignature = FullSignature(sig) - - def apply(partialSig: ByteVector32, nonce: IndividualNonce): RemoteSignature = PartialSignatureWithNonce(partialSig: ByteVector32, nonce: IndividualNonce) -} +/** + * The channel funding output requires signatures from both channel participants to be spent. + * Depending on the segwit version used, those signatures have a different format. + * For commitment transactions, we usually only store the remote signature instead of the fully signed transaction, + * otherwise someone with read access to our database could force-close our channels. + */ +sealed trait ChannelSpendSignature -case class CommitTxAndRemoteSig(commitTx: CommitTx, remoteSig: RemoteSignature) +object ChannelSpendSignature { + /** When using a 2-of-2 multisig, we need two individual ECDSA signatures. */ + case class IndividualSignature(sig: ByteVector64) extends ChannelSpendSignature -object CommitTxAndRemoteSig { - def apply(commitTx: CommitTx, remoteSig: ByteVector64): CommitTxAndRemoteSig = CommitTxAndRemoteSig(commitTx, RemoteSignature(remoteSig)) + /** When using Musig2, we need two partial signatures and the signer's nonce. */ + case class PartialSignatureWithNonce(partialSig: ByteVector32, nonce: IndividualNonce) extends ChannelSpendSignature } -/** The local commitment maps to a commitment transaction that we can sign and broadcast if necessary. */ -case class LocalCommit(index: Long, spec: CommitmentSpec, commitTxAndRemoteSig: CommitTxAndRemoteSig, htlcTxsAndRemoteSigs: List[HtlcTxAndRemoteSig]) +/** + * The local commitment maps to a commitment transaction that we can sign and broadcast if necessary. + * The [[htlcRemoteSigs]] are stored in the order in which HTLC outputs appear in the commitment transaction. + */ +case class LocalCommit(index: Long, spec: CommitmentSpec, txId: TxId, remoteSig: ChannelSpendSignature, htlcRemoteSigs: List[ByteVector64]) object LocalCommit { - def fromCommitSig(keyManager: ChannelKeyManager, params: ChannelParams, fundingTxId: TxId, - fundingTxIndex: Long, remoteFundingPubKey: PublicKey, commitInput: InputInfo, - commit: CommitSig, localCommitIndex: Long, spec: CommitmentSpec, localPerCommitmentPoint: PublicKey): Either[ChannelException, LocalCommit] = { - val (localCommitTx, htlcTxs) = Commitment.makeLocalTxs(keyManager, params.channelConfig, params.channelFeatures, localCommitIndex, params.localParams, params.remoteParams, fundingTxIndex, remoteFundingPubKey, commitInput, localPerCommitmentPoint, spec) - if (!localCommitTx.checkSig(commit.signature, remoteFundingPubKey, TxOwner.Remote, params.commitmentFormat)) { - return Left(InvalidCommitmentSignature(params.channelId, fundingTxId, fundingTxIndex, localCommitTx.tx)) + def fromCommitSig(channelParams: ChannelParams, commitParams: CommitParams, commitKeys: LocalCommitmentKeys, fundingTxId: TxId, + fundingKey: PrivateKey, remoteFundingPubKey: PublicKey, commitInput: InputInfo, + commit: CommitSig, localCommitIndex: Long, spec: CommitmentSpec, commitmentFormat: CommitmentFormat): Either[ChannelException, LocalCommit] = { + val (localCommitTx, htlcTxs) = Commitment.makeLocalTxs(channelParams, commitParams, commitKeys, localCommitIndex, fundingKey, remoteFundingPubKey, commitInput, commitmentFormat, spec) + val remoteCommitSigOk = commitmentFormat match { + case _: SegwitV0CommitmentFormat => localCommitTx.checkRemoteSig(fundingKey.publicKey, remoteFundingPubKey, commit.signature) + case _: SimpleTaprootChannelCommitmentFormat => commit.sigOrPartialSig match { + case _: IndividualSignature => false + case remoteSig: PartialSignatureWithNonce => + val localNonce = NonceGenerator.verificationNonce(fundingTxId, fundingKey, remoteFundingPubKey, localCommitIndex) + localCommitTx.checkRemotePartialSignature(fundingKey.publicKey, remoteFundingPubKey, remoteSig, localNonce.publicNonce) + } + } + if (!remoteCommitSigOk) { + return Left(InvalidCommitmentSignature(channelParams.channelId, fundingTxId, localCommitIndex, localCommitTx.tx)) } val sortedHtlcTxs = htlcTxs.sortBy(_.input.outPoint.index) if (commit.htlcSignatures.size != sortedHtlcTxs.size) { - return Left(HtlcSigCountMismatch(params.channelId, sortedHtlcTxs.size, commit.htlcSignatures.size)) + return Left(HtlcSigCountMismatch(channelParams.channelId, sortedHtlcTxs.size, commit.htlcSignatures.size)) } - val remoteHtlcPubkey = Generators.derivePubKey(params.remoteParams.htlcBasepoint, localPerCommitmentPoint) - val htlcTxsAndRemoteSigs = sortedHtlcTxs.zip(commit.htlcSignatures).toList.map { + val htlcRemoteSigs = sortedHtlcTxs.zip(commit.htlcSignatures).toList.map { case (htlcTx: HtlcTx, remoteSig) => - if (!htlcTx.checkSig(remoteSig, remoteHtlcPubkey, TxOwner.Remote, params.commitmentFormat)) { - return Left(InvalidHtlcSignature(params.channelId, htlcTx.tx.txid)) + if (!htlcTx.checkRemoteSig(commitKeys, remoteSig)) { + return Left(InvalidHtlcSignature(channelParams.channelId, htlcTx.tx.txid)) } - HtlcTxAndRemoteSig(htlcTx, remoteSig) + remoteSig } - Right(LocalCommit(localCommitIndex, spec, CommitTxAndRemoteSig(localCommitTx, RemoteSignature.FullSignature(commit.signature)), htlcTxsAndRemoteSigs)) + Right(LocalCommit(localCommitIndex, spec, localCommitTx.tx.txid, commit.sigOrPartialSig, htlcRemoteSigs)) } } /** The remote commitment maps to a commitment transaction that only our peer can sign and broadcast. */ -case class RemoteCommit(index: Long, spec: CommitmentSpec, txid: TxId, remotePerCommitmentPoint: PublicKey) { - def sign(keyManager: ChannelKeyManager, params: ChannelParams, fundingTxIndex: Long, remoteFundingPubKey: PublicKey, commitInput: InputInfo): CommitSig = { - val (remoteCommitTx, htlcTxs) = Commitment.makeRemoteTxs(keyManager, params.channelConfig, params.channelFeatures, index, params.localParams, params.remoteParams, fundingTxIndex, remoteFundingPubKey, commitInput, remotePerCommitmentPoint, spec) - val sig = keyManager.sign(remoteCommitTx, keyManager.fundingPublicKey(params.localParams.fundingKeyPath, fundingTxIndex), TxOwner.Remote, params.commitmentFormat, Map.empty) - val channelKeyPath = keyManager.keyPath(params.localParams, params.channelConfig) +case class RemoteCommit(index: Long, spec: CommitmentSpec, txId: TxId, remotePerCommitmentPoint: PublicKey) { + def sign(channelParams: ChannelParams, commitParams: CommitParams, channelKeys: ChannelKeys, fundingTxIndex: Long, remoteFundingPubKey: PublicKey, commitInput: InputInfo, commitmentFormat: CommitmentFormat, remoteNonce_opt: Option[IndividualNonce], batchSize: Int = 1): Either[ChannelException, CommitSig] = { + val fundingKey = channelKeys.fundingKey(fundingTxIndex) + val commitKeys = RemoteCommitmentKeys(channelParams, channelKeys, remotePerCommitmentPoint) + val (remoteCommitTx, htlcTxs) = Commitment.makeRemoteTxs(channelParams, commitParams, commitKeys, index, fundingKey, remoteFundingPubKey, commitInput, commitmentFormat, spec) val sortedHtlcTxs = htlcTxs.sortBy(_.input.outPoint.index) - val htlcSigs = sortedHtlcTxs.map(keyManager.sign(_, keyManager.htlcPoint(channelKeyPath), remotePerCommitmentPoint, TxOwner.Remote, params.commitmentFormat, Map.empty)) - CommitSig(params.channelId, sig, htlcSigs.toList) + val htlcSigs = sortedHtlcTxs.map(_.localSig(commitKeys)) + commitmentFormat match { + case _: SegwitV0CommitmentFormat => + val sig = remoteCommitTx.sign(fundingKey, remoteFundingPubKey) + Right(CommitSig(channelParams.channelId, sig, htlcSigs.toList, batchSize)) + case _: SimpleTaprootChannelCommitmentFormat => + remoteNonce_opt match { + case Some(remoteNonce) => + val localNonce = NonceGenerator.signingNonce(fundingKey.publicKey, remoteFundingPubKey, commitInput.outPoint.txid) + remoteCommitTx.partialSign(fundingKey, remoteFundingPubKey, localNonce, Seq(localNonce.publicNonce, remoteNonce)) match { + case Left(_) => Left(InvalidCommitNonce(channelParams.channelId, commitInput.outPoint.txid, index)) + case Right(psig) => Right(CommitSig(channelParams.channelId, psig, htlcSigs.toList, batchSize)) + } + case None => Left(MissingCommitNonce(channelParams.channelId, commitInput.outPoint.txid, index)) + } + } } } -/** We have the next remote commit when we've sent our commit_sig but haven't yet received their revoke_and_ack. */ -case class NextRemoteCommit(sig: CommitSig, commit: RemoteCommit) +/** + * If we ignore revoked commitments, there can be at most three concurrent commitment transactions during a force-close: + * - the local commitment + * - the remote commitment + * - the next remote commitment, if we sent commit_sig but haven't yet received revoke_and_ack + */ +case class CommitTxIds(localCommitTxId: TxId, remoteCommitTxId: TxId, nextRemoteCommitTxId_opt: Option[TxId]) { + val txIds: Set[TxId] = nextRemoteCommitTxId_opt match { + case Some(nextRemoteCommitTxId) => Set(localCommitTxId, remoteCommitTxId, nextRemoteCommitTxId) + case None => Set(localCommitTxId, remoteCommitTxId) + } +} /** * A minimal commitment for a given funding tx. @@ -242,23 +250,51 @@ case class NextRemoteCommit(sig: CommitSig, commit: RemoteCommit) */ case class Commitment(fundingTxIndex: Long, firstRemoteCommitIndex: Long, + fundingInput: OutPoint, + fundingAmount: Satoshi, remoteFundingPubKey: PublicKey, - localFundingStatus: LocalFundingStatus, remoteFundingStatus: RemoteFundingStatus, - localCommit: LocalCommit, remoteCommit: RemoteCommit, nextRemoteCommit_opt: Option[NextRemoteCommit]) { - val commitInput: InputInfo = localCommit.commitTxAndRemoteSig.commitTx.input - val fundingTxId: TxId = commitInput.outPoint.txid - val capacity: Satoshi = commitInput.txOut.amount + localFundingStatus: LocalFundingStatus, + remoteFundingStatus: RemoteFundingStatus, + commitmentFormat: CommitmentFormat, + localCommitParams: CommitParams, + localCommit: LocalCommit, + remoteCommitParams: CommitParams, + remoteCommit: RemoteCommit, + nextRemoteCommit_opt: Option[RemoteCommit]) { + val fundingTxId: TxId = fundingInput.txid + val commitTxIds: CommitTxIds = CommitTxIds(localCommit.txId, remoteCommit.txId, nextRemoteCommit_opt.map(_.txId)) + val capacity: Satoshi = fundingAmount + // We can safely cast to millisatoshis since we verify that it's less than a valid millisatoshi amount. + val maxHtlcValueInFlight: MilliSatoshi = Seq(localCommitParams.maxHtlcValueInFlight, remoteCommitParams.maxHtlcValueInFlight, UInt64(MilliSatoshi.MaxMoney.toLong)).min.toBigInt.toLong.msat /** Once the funding transaction is confirmed, short_channel_id matching this transaction. */ val shortChannelId_opt: Option[RealShortChannelId] = localFundingStatus match { case f: LocalFundingStatus.ConfirmedFundingTx => Some(f.shortChannelId) case _ => None } + def localFundingKey(channelKeys: ChannelKeys): PrivateKey = channelKeys.fundingKey(fundingTxIndex) + + def commitInput(fundingKey: PrivateKey): InputInfo = Transactions.makeFundingInputInfo(fundingInput.txid, fundingInput.index.toInt, fundingAmount, fundingKey.publicKey, remoteFundingPubKey, commitmentFormat) + + def commitInput(channelKeys: ChannelKeys): InputInfo = commitInput(localFundingKey(channelKeys)) + + def localKeys(params: ChannelParams, channelKeys: ChannelKeys): LocalCommitmentKeys = LocalCommitmentKeys(params, channelKeys, localCommit.index) + + def remoteKeys(params: ChannelParams, channelKeys: ChannelKeys, remotePerCommitmentPoint: PublicKey): RemoteCommitmentKeys = RemoteCommitmentKeys(params, channelKeys, remotePerCommitmentPoint) + /** Channel reserve that applies to our funds. */ - def localChannelReserve(params: ChannelParams): Satoshi = params.localChannelReserveForCapacity(capacity, fundingTxIndex > 0) + def localChannelReserve(params: ChannelParams): Satoshi = if (params.channelFeatures.hasFeature(Features.DualFunding) || fundingTxIndex > 0) { + (fundingAmount / 100).max(remoteCommitParams.dustLimit) + } else { + params.remoteParams.initialRequestedChannelReserve_opt.get // this is guarded by a require() in ChannelParams + } /** Channel reserve that applies to our peer's funds. */ - def remoteChannelReserve(params: ChannelParams): Satoshi = params.remoteChannelReserveForCapacity(capacity, fundingTxIndex > 0) + def remoteChannelReserve(params: ChannelParams): Satoshi = if (params.channelFeatures.hasFeature(Features.DualFunding) || fundingTxIndex > 0) { + (fundingAmount / 100).max(localCommitParams.dustLimit) + } else { + params.localParams.initialRequestedChannelReserve_opt.get // this is guarded by a require() in ChannelParams + } // NB: when computing availableBalanceForSend and availableBalanceForReceive, the initiator keeps an extra buffer on // top of its usual channel reserve to avoid getting channels stuck in case the on-chain feerate increases (see @@ -289,16 +325,16 @@ case class Commitment(fundingTxIndex: Long, def availableBalanceForSend(params: ChannelParams, changes: CommitmentChanges): MilliSatoshi = { import params._ // we need to base the next current commitment on the last sig we sent, even if we didn't yet receive their revocation - val remoteCommit1 = nextRemoteCommit_opt.map(_.commit).getOrElse(remoteCommit) + val remoteCommit1 = nextRemoteCommit_opt.getOrElse(remoteCommit) val reduced = CommitmentSpec.reduce(remoteCommit1.spec, changes.remoteChanges.acked, changes.localChanges.proposed) val balanceNoFees = (reduced.toRemote - localChannelReserve(params)).max(0 msat) if (localParams.paysCommitTxFees) { // The initiator always pays the on-chain fees, so we must subtract that from the amount we can send. - val commitFees = commitTxTotalCostMsat(remoteParams.dustLimit, reduced, commitmentFormat) + val commitFees = commitTxTotalCostMsat(remoteCommitParams.dustLimit, reduced, commitmentFormat) // the initiator needs to keep a "funder fee buffer" (see explanation above) - val funderFeeBuffer = commitTxTotalCostMsat(remoteParams.dustLimit, reduced.copy(commitTxFeerate = reduced.commitTxFeerate * 2), commitmentFormat) + htlcOutputFee(reduced.commitTxFeerate * 2, commitmentFormat) + val funderFeeBuffer = commitTxTotalCostMsat(remoteCommitParams.dustLimit, reduced.copy(commitTxFeerate = reduced.commitTxFeerate * 2), commitmentFormat) + htlcOutputFee(reduced.commitTxFeerate * 2, commitmentFormat) val amountToReserve = commitFees.max(funderFeeBuffer) - if (balanceNoFees - amountToReserve < offeredHtlcTrimThreshold(remoteParams.dustLimit, reduced, commitmentFormat)) { + if (balanceNoFees - amountToReserve < offeredHtlcTrimThreshold(remoteCommitParams.dustLimit, reduced, commitmentFormat)) { // htlc will be trimmed (balanceNoFees - amountToReserve).max(0 msat) } else { @@ -324,11 +360,11 @@ case class Commitment(fundingTxIndex: Long, balanceNoFees } else { // The initiator always pays the on-chain fees, so we must subtract that from the amount we can receive. - val commitFees = commitTxTotalCostMsat(localParams.dustLimit, reduced, commitmentFormat) + val commitFees = commitTxTotalCostMsat(localCommitParams.dustLimit, reduced, commitmentFormat) // we expected the initiator to keep a "funder fee buffer" (see explanation above) - val funderFeeBuffer = commitTxTotalCostMsat(localParams.dustLimit, reduced.copy(commitTxFeerate = reduced.commitTxFeerate * 2), commitmentFormat) + htlcOutputFee(reduced.commitTxFeerate * 2, commitmentFormat) + val funderFeeBuffer = commitTxTotalCostMsat(localCommitParams.dustLimit, reduced.copy(commitTxFeerate = reduced.commitTxFeerate * 2), commitmentFormat) + htlcOutputFee(reduced.commitTxFeerate * 2, commitmentFormat) val amountToReserve = commitFees.max(funderFeeBuffer) - if (balanceNoFees - amountToReserve < receivedHtlcTrimThreshold(localParams.dustLimit, reduced, commitmentFormat)) { + if (balanceNoFees - amountToReserve < receivedHtlcTrimThreshold(localCommitParams.dustLimit, reduced, commitmentFormat)) { // htlc will be trimmed (balanceNoFees - amountToReserve).max(0 msat) } else { @@ -343,28 +379,27 @@ case class Commitment(fundingTxIndex: Long, } /** Sign the announcement for this commitment, if the funding transaction is confirmed. */ - def signAnnouncement(nodeParams: NodeParams, params: ChannelParams): Option[AnnouncementSignatures] = { + def signAnnouncement(nodeParams: NodeParams, params: ChannelParams, fundingKey: PrivateKey): Option[AnnouncementSignatures] = { localFundingStatus match { case funding: LocalFundingStatus.ConfirmedFundingTx if params.announceChannel => val features = Features.empty[Feature] // empty features for now - val fundingPubKey = nodeParams.channelKeyManager.fundingPublicKey(params.localParams.fundingKeyPath, fundingTxIndex) val witness = Announcements.generateChannelAnnouncementWitness( nodeParams.chainHash, funding.shortChannelId, nodeParams.nodeKeyManager.nodeId, params.remoteParams.nodeId, - fundingPubKey.publicKey, + fundingKey.publicKey, remoteFundingPubKey, features ) - val localBitcoinSig = nodeParams.channelKeyManager.signChannelAnnouncement(witness, fundingPubKey.path) + val localBitcoinSig = Announcements.signChannelAnnouncement(witness, fundingKey) val localNodeSig = nodeParams.nodeKeyManager.signChannelAnnouncement(witness) Some(AnnouncementSignatures(params.channelId, funding.shortChannelId, localNodeSig, localBitcoinSig)) case _ => None } } - def hasNoPendingHtlcs: Boolean = localCommit.spec.htlcs.isEmpty && remoteCommit.spec.htlcs.isEmpty && nextRemoteCommit_opt.isEmpty + private def hasNoPendingHtlcs: Boolean = localCommit.spec.htlcs.isEmpty && remoteCommit.spec.htlcs.isEmpty && nextRemoteCommit_opt.isEmpty def hasNoPendingHtlcsOrFeeUpdate(changes: CommitmentChanges): Boolean = hasNoPendingHtlcs && (changes.localChanges.signed ++ changes.localChanges.acked ++ changes.remoteChanges.signed ++ changes.remoteChanges.acked).collectFirst { case _: UpdateFee => true }.isEmpty @@ -378,7 +413,7 @@ case class Commitment(fundingTxIndex: Long, localCommit.spec.htlcs.collect(DirectedHtlc.outgoing).filter(expired) ++ remoteCommit.spec.htlcs.collect(DirectedHtlc.incoming).filter(expired) ++ - nextRemoteCommit_opt.toSeq.flatMap(_.commit.spec.htlcs.collect(DirectedHtlc.incoming).filter(expired).toSet) + nextRemoteCommit_opt.toSeq.flatMap(_.spec.htlcs.collect(DirectedHtlc.incoming).filter(expired).toSet) } /** @@ -389,7 +424,7 @@ case class Commitment(fundingTxIndex: Long, * NB: if we're in the middle of fulfilling or failing that HTLC, it will not be returned by this function. */ def getOutgoingHtlcCrossSigned(htlcId: Long): Option[UpdateAddHtlc] = for { - localSigned <- nextRemoteCommit_opt.map(_.commit).getOrElse(remoteCommit).spec.findIncomingHtlcById(htlcId) + localSigned <- nextRemoteCommit_opt.getOrElse(remoteCommit).spec.findIncomingHtlcById(htlcId) remoteSigned <- localCommit.spec.findOutgoingHtlcById(htlcId) } yield { require(localSigned.add == remoteSigned.add) @@ -404,7 +439,7 @@ case class Commitment(fundingTxIndex: Long, * NB: if we're in the middle of fulfilling or failing that HTLC, it will not be returned by this function. */ def getIncomingHtlcCrossSigned(htlcId: Long): Option[UpdateAddHtlc] = for { - localSigned <- nextRemoteCommit_opt.map(_.commit).getOrElse(remoteCommit).spec.findOutgoingHtlcById(htlcId) + localSigned <- nextRemoteCommit_opt.getOrElse(remoteCommit).spec.findOutgoingHtlcById(htlcId) remoteSigned <- localCommit.spec.findIncomingHtlcById(htlcId) } yield { require(localSigned.add == remoteSigned.add) @@ -423,31 +458,19 @@ case class Commitment(fundingTxIndex: Long, localCommit.spec.htlcs.collect(DirectedHtlc.incoming).filter(nearlyExpired) } - def canSendAdd(amount: MilliSatoshi, params: ChannelParams, changes: CommitmentChanges, feerates: FeeratesPerKw, feeConf: OnChainFeeConf): Either[ChannelException, Unit] = { - // we allowed mismatches between our feerates and our remote's as long as commitments didn't contain any HTLC at risk - // we need to verify that we're not disagreeing on feerates anymore before offering new HTLCs - // NB: there may be a pending update_fee that hasn't been applied yet that needs to be taken into account - val localFeerate = feeConf.getCommitmentFeerate(feerates, params.remoteNodeId, params.commitmentFormat, capacity) - val remoteFeerate = localCommit.spec.commitTxFeerate +: changes.remoteChanges.all.collect { case f: UpdateFee => f.feeratePerKw } - // What we want to avoid is having an HTLC in a commitment transaction that has a very low feerate, which we won't - // be able to confirm in time to claim the HTLC, so we only need to check that the feerate isn't too low. - remoteFeerate.find(feerate => feeConf.feerateToleranceFor(params.remoteNodeId).isProposedFeerateTooLow(params.commitmentFormat, localFeerate, feerate)) match { - case Some(feerate) => return Left(FeerateTooDifferent(params.channelId, localFeeratePerKw = localFeerate, remoteFeeratePerKw = feerate)) - case None => - } - + def canSendAdd(amount: MilliSatoshi, params: ChannelParams, changes: CommitmentChanges, feeConf: OnChainFeeConf, reputationScore: Reputation.Score): Either[ChannelException, Unit] = { // let's compute the current commitments *as seen by them* with the additional htlc // we need to base the next current commitment on the last sig we sent, even if we didn't yet receive their revocation - val remoteCommit1 = nextRemoteCommit_opt.map(_.commit).getOrElse(remoteCommit) + val remoteCommit1 = nextRemoteCommit_opt.getOrElse(remoteCommit) val reduced = CommitmentSpec.reduce(remoteCommit1.spec, changes.remoteChanges.acked, changes.localChanges.proposed) // the HTLC we are about to create is outgoing, but from their point of view it is incoming val outgoingHtlcs = reduced.htlcs.collect(DirectedHtlc.incoming) // note that the initiator pays the fee, so if sender != initiator, both sides will have to afford this payment - val fees = commitTxTotalCost(params.remoteParams.dustLimit, reduced, params.commitmentFormat) + val fees = commitTxTotalCost(remoteCommitParams.dustLimit, reduced, commitmentFormat) // the initiator needs to keep an extra buffer to be able to handle a x2 feerate increase and an additional htlc to avoid // getting the channel stuck (see https://github.com/lightningnetwork/lightning-rfc/issues/728). - val funderFeeBuffer = commitTxTotalCostMsat(params.remoteParams.dustLimit, reduced.copy(commitTxFeerate = reduced.commitTxFeerate * 2), params.commitmentFormat) + htlcOutputFee(reduced.commitTxFeerate * 2, params.commitmentFormat) + val funderFeeBuffer = commitTxTotalCostMsat(remoteCommitParams.dustLimit, reduced.copy(commitTxFeerate = reduced.commitTxFeerate * 2), commitmentFormat) + htlcOutputFee(reduced.commitTxFeerate * 2, commitmentFormat) // NB: increasing the feerate can actually remove htlcs from the commit tx (if they fall below the trim threshold) // which may result in a lower commit tx fee; this is why we take the max of the two. val missingForSender = reduced.toRemote - localChannelReserve(params) - (if (params.localParams.paysCommitTxFees) fees.max(funderFeeBuffer.truncateToSatoshi) else 0.sat) @@ -478,47 +501,38 @@ case class Commitment(fundingTxIndex: Long, // We apply local *and* remote restrictions, to ensure both peers are happy with the resulting number of HTLCs. // NB: we need the `toSeq` because otherwise duplicate amountMsat would be removed (since outgoingHtlcs is a Set). val htlcValueInFlight = outgoingHtlcs.toSeq.map(_.amountMsat).sum - val allowedHtlcValueInFlight = params.maxHtlcAmount - if (allowedHtlcValueInFlight < htlcValueInFlight) { - return Left(HtlcValueTooHighInFlight(params.channelId, maximum = allowedHtlcValueInFlight, actual = htlcValueInFlight)) + if (maxHtlcValueInFlight < htlcValueInFlight) { + return Left(HtlcValueTooHighInFlight(params.channelId, maximum = UInt64(maxHtlcValueInFlight.toLong), actual = htlcValueInFlight)) } - if (Seq(params.localParams.maxAcceptedHtlcs, params.remoteParams.maxAcceptedHtlcs).min < outgoingHtlcs.size) { - return Left(TooManyAcceptedHtlcs(params.channelId, maximum = Seq(params.localParams.maxAcceptedHtlcs, params.remoteParams.maxAcceptedHtlcs).min)) + if (Seq(localCommitParams.maxAcceptedHtlcs, remoteCommitParams.maxAcceptedHtlcs).min < outgoingHtlcs.size) { + return Left(TooManyAcceptedHtlcs(params.channelId, maximum = Seq(localCommitParams.maxAcceptedHtlcs, remoteCommitParams.maxAcceptedHtlcs).min)) } // If sending this htlc would overflow our dust exposure, we reject it. val maxDustExposure = feeConf.feerateToleranceFor(params.remoteNodeId).dustTolerance.maxExposure val localReduced = DustExposure.reduceForDustExposure(localCommit.spec, changes.localChanges.all, changes.remoteChanges.all) - val localDustExposureAfterAdd = DustExposure.computeExposure(localReduced, params.localParams.dustLimit, params.commitmentFormat) + val localDustExposureAfterAdd = DustExposure.computeExposure(localReduced, localCommitParams.dustLimit, commitmentFormat) if (localDustExposureAfterAdd > maxDustExposure) { return Left(LocalDustHtlcExposureTooHigh(params.channelId, maxDustExposure, localDustExposureAfterAdd)) } val remoteReduced = DustExposure.reduceForDustExposure(remoteCommit1.spec, changes.remoteChanges.all, changes.localChanges.all) - val remoteDustExposureAfterAdd = DustExposure.computeExposure(remoteReduced, params.remoteParams.dustLimit, params.commitmentFormat) + val remoteDustExposureAfterAdd = DustExposure.computeExposure(remoteReduced, remoteCommitParams.dustLimit, commitmentFormat) if (remoteDustExposureAfterAdd > maxDustExposure) { return Left(RemoteDustHtlcExposureTooHigh(params.channelId, maxDustExposure, remoteDustExposureAfterAdd)) } - Right(()) + // Jamming protection + // Must be the last checks so that they can be ignored for shadow deployment. + reputationScore.checkOutgoingChannelOccupancy(params.channelId, this, outgoingHtlcs.toSeq) } - def canReceiveAdd(amount: MilliSatoshi, params: ChannelParams, changes: CommitmentChanges, feerates: FeeratesPerKw, feeConf: OnChainFeeConf): Either[ChannelException, Unit] = { - // we allowed mismatches between our feerates and our remote's as long as commitments didn't contain any HTLC at risk - // we need to verify that we're not disagreeing on feerates anymore before accepting new HTLCs - // NB: there may be a pending update_fee that hasn't been applied yet that needs to be taken into account - val localFeerate = feeConf.getCommitmentFeerate(feerates, params.remoteNodeId, params.commitmentFormat, capacity) - val remoteFeerate = localCommit.spec.commitTxFeerate +: changes.remoteChanges.all.collect { case f: UpdateFee => f.feeratePerKw } - remoteFeerate.find(feerate => feeConf.feerateToleranceFor(params.remoteNodeId).isProposedFeerateTooLow(params.commitmentFormat, localFeerate, feerate)) match { - case Some(feerate) => return Left(FeerateTooDifferent(params.channelId, localFeeratePerKw = localFeerate, remoteFeeratePerKw = feerate)) - case None => - } - + def canReceiveAdd(amount: MilliSatoshi, params: ChannelParams, changes: CommitmentChanges): Either[ChannelException, Unit] = { // let's compute the current commitment *as seen by us* including this additional htlc val reduced = CommitmentSpec.reduce(localCommit.spec, changes.localChanges.acked, changes.remoteChanges.proposed) val incomingHtlcs = reduced.htlcs.collect(DirectedHtlc.incoming) // note that the initiator pays the fee, so if sender != initiator, both sides will have to afford this payment - val fees = commitTxTotalCost(params.localParams.dustLimit, reduced, params.commitmentFormat) + val fees = commitTxTotalCost(localCommitParams.dustLimit, reduced, commitmentFormat) // NB: we don't enforce the funderFeeReserve (see sendAdd) because it would confuse a remote initiator that doesn't have this mitigation in place // We could enforce it once we're confident a large portion of the network implements it. val missingForSender = reduced.toRemote - remoteChannelReserve(params) - (if (params.localParams.paysCommitTxFees) 0.sat else fees) @@ -538,12 +552,12 @@ case class Commitment(fundingTxIndex: Long, // NB: we need the `toSeq` because otherwise duplicate amountMsat would be removed (since incomingHtlcs is a Set). val htlcValueInFlight = incomingHtlcs.toSeq.map(_.amountMsat).sum - if (params.localParams.maxHtlcValueInFlightMsat < htlcValueInFlight) { - return Left(HtlcValueTooHighInFlight(params.channelId, maximum = params.localParams.maxHtlcValueInFlightMsat, actual = htlcValueInFlight)) + if (localCommitParams.maxHtlcValueInFlight < htlcValueInFlight) { + return Left(HtlcValueTooHighInFlight(params.channelId, maximum = localCommitParams.maxHtlcValueInFlight, actual = htlcValueInFlight)) } - if (incomingHtlcs.size > params.localParams.maxAcceptedHtlcs) { - return Left(TooManyAcceptedHtlcs(params.channelId, maximum = params.localParams.maxAcceptedHtlcs)) + if (incomingHtlcs.size > localCommitParams.maxAcceptedHtlcs) { + return Left(TooManyAcceptedHtlcs(params.channelId, maximum = localCommitParams.maxAcceptedHtlcs)) } Right(()) @@ -554,7 +568,7 @@ case class Commitment(fundingTxIndex: Long, val reduced = CommitmentSpec.reduce(remoteCommit.spec, changes.remoteChanges.acked, changes.localChanges.proposed) // a node cannot spend pending incoming htlcs, and need to keep funds above the reserve required by the counterparty, after paying the fee // we look from remote's point of view, so if local is initiator remote doesn't pay the fees - val fees = commitTxTotalCost(params.remoteParams.dustLimit, reduced, params.commitmentFormat) + val fees = commitTxTotalCost(remoteCommitParams.dustLimit, reduced, commitmentFormat) val missing = reduced.toRemote.truncateToSatoshi - localChannelReserve(params) - fees if (missing < 0.sat) { return Left(CannotAffordFees(params.channelId, missing = -missing, reserve = localChannelReserve(params), fees = fees)) @@ -565,12 +579,12 @@ case class Commitment(fundingTxIndex: Long, // this is the commitment as it would be if our update_fee was immediately signed by both parties (it is only an // estimate because there can be concurrent updates) val localReduced = DustExposure.reduceForDustExposure(localCommit.spec, changes.localChanges.all, changes.remoteChanges.all) - val localDustExposureAfterFeeUpdate = DustExposure.computeExposure(localReduced, targetFeerate, params.localParams.dustLimit, params.commitmentFormat) + val localDustExposureAfterFeeUpdate = DustExposure.computeExposure(localReduced, targetFeerate, localCommitParams.dustLimit, commitmentFormat) if (localDustExposureAfterFeeUpdate > maxDustExposure) { return Left(LocalDustHtlcExposureTooHigh(params.channelId, maxDustExposure, localDustExposureAfterFeeUpdate)) } val remoteReduced = DustExposure.reduceForDustExposure(remoteCommit.spec, changes.remoteChanges.all, changes.localChanges.all) - val remoteDustExposureAfterFeeUpdate = DustExposure.computeExposure(remoteReduced, targetFeerate, params.remoteParams.dustLimit, params.commitmentFormat) + val remoteDustExposureAfterFeeUpdate = DustExposure.computeExposure(remoteReduced, targetFeerate, remoteCommitParams.dustLimit, commitmentFormat) if (remoteDustExposureAfterFeeUpdate > maxDustExposure) { return Left(RemoteDustHtlcExposureTooHigh(params.channelId, maxDustExposure, remoteDustExposureAfterFeeUpdate)) } @@ -579,11 +593,8 @@ case class Commitment(fundingTxIndex: Long, } def canReceiveFee(targetFeerate: FeeratePerKw, params: ChannelParams, changes: CommitmentChanges, feerates: FeeratesPerKw, feeConf: OnChainFeeConf): Either[ChannelException, Unit] = { - val localFeerate = feeConf.getCommitmentFeerate(feerates, params.remoteNodeId, params.commitmentFormat, capacity) - if (feeConf.feerateToleranceFor(params.remoteNodeId).isProposedFeerateTooHigh(params.commitmentFormat, localFeerate, targetFeerate)) { - return Left(FeerateTooDifferent(params.channelId, localFeeratePerKw = localFeerate, remoteFeeratePerKw = targetFeerate)) - } else if (feeConf.feerateToleranceFor(params.remoteNodeId).isProposedFeerateTooLow(params.commitmentFormat, localFeerate, targetFeerate) && hasPendingOrProposedHtlcs(changes)) { - // If the proposed feerate is too low, but we don't have any pending HTLC, we temporarily accept it. + val localFeerate = feeConf.getCommitmentFeerate(feerates, params.remoteNodeId, commitmentFormat) + if (feeConf.feerateToleranceFor(params.remoteNodeId).isProposedCommitFeerateTooHigh(localFeerate, targetFeerate)) { return Left(FeerateTooDifferent(params.channelId, localFeeratePerKw = localFeerate, remoteFeeratePerKw = targetFeerate)) } else { // let's compute the current commitment *as seen by us* including this change @@ -593,7 +604,7 @@ case class Commitment(fundingTxIndex: Long, // (it also means that we need to check the fee of the initial commitment tx somewhere) val reduced = CommitmentSpec.reduce(localCommit.spec, changes.localChanges.acked, changes.remoteChanges.proposed) // a node cannot spend pending incoming htlcs, and need to keep funds above the reserve required by the counterparty, after paying the fee - val fees = commitTxTotalCost(params.localParams.dustLimit, reduced, params.commitmentFormat) + val fees = commitTxTotalCost(localCommitParams.dustLimit, reduced, commitmentFormat) val missing = reduced.toRemote.truncateToSatoshi - remoteChannelReserve(params) - fees if (missing < 0.sat) { return Left(CannotAffordFees(params.channelId, missing = -missing, reserve = remoteChannelReserve(params), fees = fees)) @@ -602,14 +613,14 @@ case class Commitment(fundingTxIndex: Long, if (feeConf.feerateToleranceFor(params.remoteNodeId).dustTolerance.closeOnUpdateFeeOverflow) { val maxDustExposure = feeConf.feerateToleranceFor(params.remoteNodeId).dustTolerance.maxExposure val localReduced = DustExposure.reduceForDustExposure(localCommit.spec, changes.localChanges.all, changes.remoteChanges.all) - val localDustExposureAfterFeeUpdate = DustExposure.computeExposure(localReduced, targetFeerate, params.localParams.dustLimit, params.commitmentFormat) + val localDustExposureAfterFeeUpdate = DustExposure.computeExposure(localReduced, targetFeerate, localCommitParams.dustLimit, commitmentFormat) if (localDustExposureAfterFeeUpdate > maxDustExposure) { return Left(LocalDustHtlcExposureTooHigh(params.channelId, maxDustExposure, localDustExposureAfterFeeUpdate)) } // this is the commitment as it would be if their update_fee was immediately signed by both parties (it is only an // estimate because there can be concurrent updates) val remoteReduced = DustExposure.reduceForDustExposure(remoteCommit.spec, changes.remoteChanges.all, changes.localChanges.all) - val remoteDustExposureAfterFeeUpdate = DustExposure.computeExposure(remoteReduced, targetFeerate, params.remoteParams.dustLimit, params.commitmentFormat) + val remoteDustExposureAfterFeeUpdate = DustExposure.computeExposure(remoteReduced, targetFeerate, remoteCommitParams.dustLimit, commitmentFormat) if (remoteDustExposureAfterFeeUpdate > maxDustExposure) { return Left(RemoteDustHtlcExposureTooHigh(params.channelId, maxDustExposure, remoteDustExposureAfterFeeUpdate)) } @@ -618,28 +629,34 @@ case class Commitment(fundingTxIndex: Long, Right(()) } - def sendCommit(keyManager: ChannelKeyManager, params: ChannelParams, changes: CommitmentChanges, remoteNextPerCommitmentPoint: PublicKey, batchSize: Int)(implicit log: LoggingAdapter): (Commitment, CommitSig) = { + def sendCommit(params: ChannelParams, channelKeys: ChannelKeys, commitKeys: RemoteCommitmentKeys, changes: CommitmentChanges, remoteNextPerCommitmentPoint: PublicKey, batchSize: Int, nextRemoteNonce_opt: Option[IndividualNonce])(implicit log: LoggingAdapter): Either[ChannelException, (Commitment, CommitSig)] = { // remote commitment will include all local proposed changes + remote acked changes val spec = CommitmentSpec.reduce(remoteCommit.spec, changes.remoteChanges.acked, changes.localChanges.proposed) - val (remoteCommitTx, htlcTxs) = Commitment.makeRemoteTxs(keyManager, params.channelConfig, params.channelFeatures, remoteCommit.index + 1, params.localParams, params.remoteParams, fundingTxIndex, remoteFundingPubKey, commitInput, remoteNextPerCommitmentPoint, spec) - val sig = keyManager.sign(remoteCommitTx, keyManager.fundingPublicKey(params.localParams.fundingKeyPath, fundingTxIndex), TxOwner.Remote, params.commitmentFormat, Map.empty) - - val sortedHtlcTxs: Seq[TransactionWithInputInfo] = htlcTxs.sortBy(_.input.outPoint.index) - val channelKeyPath = keyManager.keyPath(params.localParams, params.channelConfig) - val htlcSigs = sortedHtlcTxs.map(keyManager.sign(_, keyManager.htlcPoint(channelKeyPath), remoteNextPerCommitmentPoint, TxOwner.Remote, params.commitmentFormat, Map.empty)) - + val fundingKey = localFundingKey(channelKeys) + val (remoteCommitTx, htlcTxs) = Commitment.makeRemoteTxs(params, remoteCommitParams, commitKeys, remoteCommit.index + 1, fundingKey, remoteFundingPubKey, commitInput(fundingKey), commitmentFormat, spec) + val htlcSigs = htlcTxs.sortBy(_.input.outPoint.index).map(_.localSig(commitKeys)) // NB: IN/OUT htlcs are inverted because this is the remote commit log.info(s"built remote commit number=${remoteCommit.index + 1} toLocalMsat=${spec.toLocal.toLong} toRemoteMsat=${spec.toRemote.toLong} htlc_in={} htlc_out={} feeratePerKw=${spec.commitTxFeerate} txid=${remoteCommitTx.tx.txid} fundingTxId=$fundingTxId", spec.htlcs.collect(DirectedHtlc.outgoing).map(_.id).mkString(","), spec.htlcs.collect(DirectedHtlc.incoming).map(_.id).mkString(",")) Metrics.recordHtlcsInFlight(spec, remoteCommit.spec) - - val commitSig = CommitSig(params.channelId, sig, htlcSigs.toList, TlvStream(Set( - if (batchSize > 1) Some(CommitSigTlv.BatchTlv(batchSize)) else None - ).flatten[CommitSigTlv])) - val nextRemoteCommit = NextRemoteCommit(commitSig, RemoteCommit(remoteCommit.index + 1, spec, remoteCommitTx.tx.txid, remoteNextPerCommitmentPoint)) - (copy(nextRemoteCommit_opt = Some(nextRemoteCommit)), commitSig) + val sig = commitmentFormat match { + case _: SegwitV0CommitmentFormat => remoteCommitTx.sign(fundingKey, remoteFundingPubKey) + case _: SimpleTaprootChannelCommitmentFormat => + nextRemoteNonce_opt match { + case Some(remoteNonce) => + val localNonce = NonceGenerator.signingNonce(fundingKey.publicKey, remoteFundingPubKey, fundingTxId) + remoteCommitTx.partialSign(fundingKey, remoteFundingPubKey, localNonce, Seq(localNonce.publicNonce, remoteNonce)) match { + case Left(_) => return Left(InvalidCommitNonce(params.channelId, fundingTxId, remoteCommit.index + 1)) + case Right(psig) => psig + } + case None => return Left(MissingCommitNonce(params.channelId, fundingTxId, remoteCommit.index + 1)) + } + } + val commitSig = CommitSig(params.channelId, sig, htlcSigs.toList, batchSize) + val nextRemoteCommit = RemoteCommit(remoteCommit.index + 1, spec, remoteCommitTx.tx.txid, remoteNextPerCommitmentPoint) + Right((copy(nextRemoteCommit_opt = Some(nextRemoteCommit)), commitSig)) } - def receiveCommit(keyManager: ChannelKeyManager, params: ChannelParams, changes: CommitmentChanges, localPerCommitmentPoint: PublicKey, commit: CommitSig)(implicit log: LoggingAdapter): Either[ChannelException, Commitment] = { + def receiveCommit(params: ChannelParams, channelKeys: ChannelKeys, commitKeys: LocalCommitmentKeys, changes: CommitmentChanges, commit: CommitSig)(implicit log: LoggingAdapter): Either[ChannelException, Commitment] = { // they sent us a signature for *their* view of *our* next commit tx // so in terms of rev.hashes and indexes we have: // ourCommit.index -> our current revocation hash, which is about to become our old revocation hash @@ -649,82 +666,81 @@ case class Commitment(fundingTxIndex: Long, // we will reply to this sig with our old revocation hash preimage (at index) and our next revocation hash (at index + 1) // and will increment our index val localCommitIndex = localCommit.index + 1 + val fundingKey = localFundingKey(channelKeys) val spec = CommitmentSpec.reduce(localCommit.spec, changes.localChanges.acked, changes.remoteChanges.proposed) - LocalCommit.fromCommitSig(keyManager, params, fundingTxId, fundingTxIndex, remoteFundingPubKey, commitInput, commit, localCommitIndex, spec, localPerCommitmentPoint).map { localCommit1 => - log.info(s"built local commit number=$localCommitIndex toLocalMsat=${spec.toLocal.toLong} toRemoteMsat=${spec.toRemote.toLong} htlc_in={} htlc_out={} feeratePerKw=${spec.commitTxFeerate} txid=${localCommit1.commitTxAndRemoteSig.commitTx.tx.txid} fundingTxId=$fundingTxId", spec.htlcs.collect(DirectedHtlc.incoming).map(_.id).mkString(","), spec.htlcs.collect(DirectedHtlc.outgoing).map(_.id).mkString(",")) + LocalCommit.fromCommitSig(params, localCommitParams, commitKeys, fundingTxId, fundingKey, remoteFundingPubKey, commitInput(fundingKey), commit, localCommitIndex, spec, commitmentFormat).map { localCommit1 => + log.info(s"built local commit number=$localCommitIndex toLocalMsat=${spec.toLocal.toLong} toRemoteMsat=${spec.toRemote.toLong} htlc_in={} htlc_out={} feeratePerKw=${spec.commitTxFeerate} txid=${localCommit1.txId} fundingTxId=$fundingTxId", spec.htlcs.collect(DirectedHtlc.incoming).map(_.id).mkString(","), spec.htlcs.collect(DirectedHtlc.outgoing).map(_.id).mkString(",")) copy(localCommit = localCommit1) } } /** Return a fully signed commit tx, that can be published as-is. */ - def fullySignedLocalCommitTx(params: ChannelParams, keyManager: ChannelKeyManager): CommitTx = { - val unsignedCommitTx = localCommit.commitTxAndRemoteSig.commitTx - val localSig = keyManager.sign(unsignedCommitTx, keyManager.fundingPublicKey(params.localParams.fundingKeyPath, fundingTxIndex), TxOwner.Local, params.commitmentFormat, Map.empty) - val RemoteSignature.FullSignature(remoteSig) = localCommit.commitTxAndRemoteSig.remoteSig - val commitTx = addSigs(unsignedCommitTx, keyManager.fundingPublicKey(params.localParams.fundingKeyPath, fundingTxIndex).publicKey, remoteFundingPubKey, localSig, remoteSig) - // We verify the remote signature when receiving their commit_sig, so this check should always pass. - require(checkSpendable(commitTx).isSuccess, "commit signatures are invalid") - commitTx + def fullySignedLocalCommitTx(params: ChannelParams, channelKeys: ChannelKeys): Transaction = { + val fundingKey = localFundingKey(channelKeys) + val commitKeys = localKeys(params, channelKeys) + val (unsignedCommitTx, _) = Commitment.makeLocalTxs(params, localCommitParams, commitKeys, localCommit.index, fundingKey, remoteFundingPubKey, commitInput(fundingKey), commitmentFormat, localCommit.spec) + localCommit.remoteSig match { + case remoteSig: IndividualSignature => + val localSig = unsignedCommitTx.sign(fundingKey, remoteFundingPubKey) + unsignedCommitTx.aggregateSigs(fundingKey.publicKey, remoteFundingPubKey, localSig, remoteSig) + case remoteSig: PartialSignatureWithNonce => + val localNonce = if (fundingTxIndex == 0 && localCommit.index == 0 && !params.channelFeatures.hasFeature(Features.DualFunding)) { + // With channel establishment v1, we exchange the first nonce before the funding tx and remote funding key are known. + NonceGenerator.verificationNonce(NonceGenerator.dummyFundingTxId, fundingKey, NonceGenerator.dummyRemoteFundingPubKey, localCommit.index) + } else { + NonceGenerator.verificationNonce(fundingTxId, fundingKey, remoteFundingPubKey, localCommit.index) + } + // We have already validated the remote nonce and partial signature when we received it, so we're guaranteed + // that the following code cannot produce an error. + val Right(localSig) = unsignedCommitTx.partialSign(fundingKey, remoteFundingPubKey, localNonce, Seq(localNonce.publicNonce, remoteSig.nonce)) + val Right(signedTx) = unsignedCommitTx.aggregateSigs(fundingKey.publicKey, remoteFundingPubKey, localSig, remoteSig) + signedTx + } + } + + /** Return the HTLC transactions for our local commit and the corresponding remote signatures. */ + def htlcTxs(params: ChannelParams, channelKeys: ChannelKeys): Seq[(UnsignedHtlcTx, ByteVector64)] = { + val fundingKey = localFundingKey(channelKeys) + val commitKeys = localKeys(params, channelKeys) + htlcTxs(params, fundingKey, commitKeys) + } + + /** Return the HTLC transactions for our local commit and the corresponding remote signatures. */ + def htlcTxs(params: ChannelParams, fundingKey: PrivateKey, commitKeys: LocalCommitmentKeys): Seq[(UnsignedHtlcTx, ByteVector64)] = { + val (_, htlcTxs) = Commitment.makeLocalTxs(params, localCommitParams, commitKeys, localCommit.index, fundingKey, remoteFundingPubKey, commitInput(fundingKey), commitmentFormat, localCommit.spec) + htlcTxs.sortBy(_.input.outPoint.index).zip(localCommit.htlcRemoteSigs) } } object Commitment { - def makeLocalTxs(keyManager: ChannelKeyManager, - channelConfig: ChannelConfig, - channelFeatures: ChannelFeatures, + def makeLocalTxs(channelParams: ChannelParams, + commitParams: CommitParams, + commitKeys: LocalCommitmentKeys, commitTxNumber: Long, - localParams: LocalParams, - remoteParams: RemoteParams, - fundingTxIndex: Long, + localFundingKey: PrivateKey, remoteFundingPubKey: PublicKey, commitmentInput: InputInfo, - localPerCommitmentPoint: PublicKey, - spec: CommitmentSpec): (CommitTx, Seq[HtlcTx]) = { - val channelKeyPath = keyManager.keyPath(localParams, channelConfig) - val localFundingPubkey = keyManager.fundingPublicKey(localParams.fundingKeyPath, fundingTxIndex).publicKey - val localDelayedPaymentPubkey = Generators.derivePubKey(keyManager.delayedPaymentPoint(channelKeyPath).publicKey, localPerCommitmentPoint) - val localHtlcPubkey = Generators.derivePubKey(keyManager.htlcPoint(channelKeyPath).publicKey, localPerCommitmentPoint) - val remotePaymentPubkey = if (channelFeatures.hasFeature(Features.StaticRemoteKey)) { - remoteParams.paymentBasepoint - } else { - Generators.derivePubKey(remoteParams.paymentBasepoint, localPerCommitmentPoint) - } - val remoteHtlcPubkey = Generators.derivePubKey(remoteParams.htlcBasepoint, localPerCommitmentPoint) - val localRevocationPubkey = Generators.revocationPubKey(remoteParams.revocationBasepoint, localPerCommitmentPoint) - val localPaymentBasepoint = localParams.walletStaticPaymentBasepoint.getOrElse(keyManager.paymentPoint(channelKeyPath).publicKey) - val outputs = makeCommitTxOutputs(localParams.paysCommitTxFees, localParams.dustLimit, localRevocationPubkey, remoteParams.toSelfDelay, localDelayedPaymentPubkey, remotePaymentPubkey, localHtlcPubkey, remoteHtlcPubkey, localFundingPubkey, remoteFundingPubKey, spec, channelFeatures.commitmentFormat) - val commitTx = makeCommitTx(commitmentInput, commitTxNumber, localPaymentBasepoint, remoteParams.paymentBasepoint, localParams.isChannelOpener, outputs) - val htlcTxs = makeHtlcTxs(commitTx.tx, localParams.dustLimit, localRevocationPubkey, remoteParams.toSelfDelay, localDelayedPaymentPubkey, spec.htlcTxFeerate(channelFeatures.commitmentFormat), outputs, channelFeatures.commitmentFormat) + commitmentFormat: CommitmentFormat, + spec: CommitmentSpec): (CommitTx, Seq[UnsignedHtlcTx]) = { + val outputs = makeCommitTxOutputs(localFundingKey.publicKey, remoteFundingPubKey, commitKeys.publicKeys, channelParams.localParams.paysCommitTxFees, commitParams.dustLimit, commitParams.toSelfDelay, spec, commitmentFormat) + val commitTx = makeCommitTx(commitmentInput, commitTxNumber, commitKeys.ourPaymentBasePoint, channelParams.remoteParams.paymentBasepoint, channelParams.localParams.isChannelOpener, outputs) + val htlcTxs = makeHtlcTxs(commitTx.tx, outputs, commitmentFormat) (commitTx, htlcTxs) } - def makeRemoteTxs(keyManager: ChannelKeyManager, - channelConfig: ChannelConfig, - channelFeatures: ChannelFeatures, + def makeRemoteTxs(channelParams: ChannelParams, + commitParams: CommitParams, + commitKeys: RemoteCommitmentKeys, commitTxNumber: Long, - localParams: LocalParams, - remoteParams: RemoteParams, - fundingTxIndex: Long, + localFundingKey: PrivateKey, remoteFundingPubKey: PublicKey, commitmentInput: InputInfo, - remotePerCommitmentPoint: PublicKey, - spec: CommitmentSpec): (CommitTx, Seq[HtlcTx]) = { - val channelKeyPath = keyManager.keyPath(localParams, channelConfig) - val localFundingPubkey = keyManager.fundingPublicKey(localParams.fundingKeyPath, fundingTxIndex).publicKey - val localPaymentBasepoint = localParams.walletStaticPaymentBasepoint.getOrElse(keyManager.paymentPoint(channelKeyPath).publicKey) - val localPaymentPubkey = if (channelFeatures.hasFeature(Features.StaticRemoteKey)) { - localPaymentBasepoint - } else { - Generators.derivePubKey(localPaymentBasepoint, remotePerCommitmentPoint) - } - val localHtlcPubkey = Generators.derivePubKey(keyManager.htlcPoint(channelKeyPath).publicKey, remotePerCommitmentPoint) - val remoteDelayedPaymentPubkey = Generators.derivePubKey(remoteParams.delayedPaymentBasepoint, remotePerCommitmentPoint) - val remoteHtlcPubkey = Generators.derivePubKey(remoteParams.htlcBasepoint, remotePerCommitmentPoint) - val remoteRevocationPubkey = Generators.revocationPubKey(keyManager.revocationPoint(channelKeyPath).publicKey, remotePerCommitmentPoint) - val outputs = makeCommitTxOutputs(!localParams.paysCommitTxFees, remoteParams.dustLimit, remoteRevocationPubkey, localParams.toSelfDelay, remoteDelayedPaymentPubkey, localPaymentPubkey, remoteHtlcPubkey, localHtlcPubkey, remoteFundingPubKey, localFundingPubkey, spec, channelFeatures.commitmentFormat) - val commitTx = makeCommitTx(commitmentInput, commitTxNumber, remoteParams.paymentBasepoint, localPaymentBasepoint, !localParams.isChannelOpener, outputs) - val htlcTxs = makeHtlcTxs(commitTx.tx, remoteParams.dustLimit, remoteRevocationPubkey, localParams.toSelfDelay, remoteDelayedPaymentPubkey, spec.htlcTxFeerate(channelFeatures.commitmentFormat), outputs, channelFeatures.commitmentFormat) + commitmentFormat: CommitmentFormat, + spec: CommitmentSpec): (CommitTx, Seq[UnsignedHtlcTx]) = { + val outputs = makeCommitTxOutputs(remoteFundingPubKey, localFundingKey.publicKey, commitKeys.publicKeys, !channelParams.localParams.paysCommitTxFees, commitParams.dustLimit, commitParams.toSelfDelay, spec, commitmentFormat) + val commitTx = makeCommitTx(commitmentInput, commitTxNumber, channelParams.remoteParams.paymentBasepoint, commitKeys.ourPaymentBasePoint, !channelParams.localParams.isChannelOpener, outputs) + val htlcTxs = makeHtlcTxs(commitTx.tx, outputs, commitmentFormat) (commitTx, htlcTxs) } } @@ -737,29 +753,40 @@ case class AnnouncedCommitment(commitment: Commitment, announcement: ChannelAnno } /** Subset of Commitments when we want to work with a single, specific commitment. */ -case class FullCommitment(params: ChannelParams, changes: CommitmentChanges, - fundingTxIndex: Long, - firstRemoteCommitIndex: Long, - remoteFundingPubKey: PublicKey, - localFundingStatus: LocalFundingStatus, remoteFundingStatus: RemoteFundingStatus, - localCommit: LocalCommit, remoteCommit: RemoteCommit, nextRemoteCommit_opt: Option[NextRemoteCommit]) { - val channelId = params.channelId - val shortChannelId_opt = localFundingStatus match { - case f: LocalFundingStatus.ConfirmedFundingTx => Some(f.shortChannelId) - case _ => None - } - val localParams = params.localParams - val remoteParams = params.remoteParams - val commitInput = localCommit.commitTxAndRemoteSig.commitTx.input - val fundingTxId = commitInput.outPoint.txid - val capacity = commitInput.txOut.amount - val commitment = Commitment(fundingTxIndex, firstRemoteCommitIndex, remoteFundingPubKey, localFundingStatus, remoteFundingStatus, localCommit, remoteCommit, nextRemoteCommit_opt) +case class FullCommitment(channelParams: ChannelParams, changes: CommitmentChanges, commitment: Commitment) { + val channelId: ByteVector32 = channelParams.channelId + val shortChannelId_opt: Option[RealShortChannelId] = commitment.shortChannelId_opt + val fundingTxIndex: Long = commitment.fundingTxIndex + val fundingInput: OutPoint = commitment.fundingInput + val fundingTxId: TxId = commitment.fundingTxId + val remoteFundingPubKey: PublicKey = commitment.remoteFundingPubKey + val localFundingStatus: LocalFundingStatus = commitment.localFundingStatus + val commitTxIds: CommitTxIds = commitment.commitTxIds + val localChannelParams: LocalChannelParams = channelParams.localParams + val localCommitParams: CommitParams = commitment.localCommitParams + val localCommit: LocalCommit = commitment.localCommit + val remoteChannelParams: RemoteChannelParams = channelParams.remoteParams + val remoteCommitParams: CommitParams = commitment.remoteCommitParams + val remoteCommit: RemoteCommit = commitment.remoteCommit + val nextRemoteCommit_opt: Option[RemoteCommit] = commitment.nextRemoteCommit_opt + val commitmentFormat: CommitmentFormat = commitment.commitmentFormat + val capacity: Satoshi = commitment.fundingAmount + + def localKeys(channelKeys: ChannelKeys): LocalCommitmentKeys = commitment.localKeys(channelParams, channelKeys) + + def remoteKeys(channelKeys: ChannelKeys, remotePerCommitmentPoint: PublicKey): RemoteCommitmentKeys = commitment.remoteKeys(channelParams, channelKeys, remotePerCommitmentPoint) + + def commitInput(channelKeys: ChannelKeys): InputInfo = commitment.commitInput(channelKeys) - def localChannelReserve: Satoshi = commitment.localChannelReserve(params) + def localChannelReserve: Satoshi = commitment.localChannelReserve(channelParams) - def remoteChannelReserve: Satoshi = commitment.remoteChannelReserve(params) + def remoteChannelReserve: Satoshi = commitment.remoteChannelReserve(channelParams) - def fullySignedLocalCommitTx(keyManager: ChannelKeyManager): CommitTx = commitment.fullySignedLocalCommitTx(params, keyManager) + def fullySignedLocalCommitTx(channelKeys: ChannelKeys): Transaction = commitment.fullySignedLocalCommitTx(channelParams, channelKeys) + + def htlcTxs(channelKeys: ChannelKeys): Seq[(UnsignedHtlcTx, ByteVector64)] = commitment.htlcTxs(channelParams, channelKeys) + + def htlcTxs(fundingKey: PrivateKey, commitKeys: LocalCommitmentKeys): Seq[(UnsignedHtlcTx, ByteVector64)] = commitment.htlcTxs(channelParams, fundingKey, commitKeys) def specs2String: String = { s"""specs: @@ -774,10 +801,10 @@ case class FullCommitment(params: ChannelParams, changes: CommitmentChanges, | htlcs: |${remoteCommit.spec.htlcs.map(h => s" ${h.direction} ${h.add.id} ${h.add.cltvExpiry}").mkString("\n")} |next remotecommit: - | toLocal: ${nextRemoteCommit_opt.map(_.commit.spec.toLocal).getOrElse("N/A")} - | toRemote: ${nextRemoteCommit_opt.map(_.commit.spec.toRemote).getOrElse("N/A")} + | toLocal: ${nextRemoteCommit_opt.map(_.spec.toLocal).getOrElse("N/A")} + | toRemote: ${nextRemoteCommit_opt.map(_.spec.toRemote).getOrElse("N/A")} | htlcs: - |${nextRemoteCommit_opt.map(_.commit.spec.htlcs.map(h => s" ${h.direction} ${h.add.id} ${h.add.cltvExpiry}").mkString("\n")).getOrElse("N/A")}""".stripMargin + |${nextRemoteCommit_opt.map(_.spec.htlcs.map(h => s" ${h.direction} ${h.add.id} ${h.add.cltvExpiry}").mkString("\n")).getOrElse("N/A")}""".stripMargin } } @@ -790,7 +817,7 @@ case class WaitForRev(sentAfterLocalCommitIndex: Long) * commitment, which funding tx is not yet confirmed, and will be pruned when it confirms * @param remoteChannelData_opt peer backup */ -case class Commitments(params: ChannelParams, +case class Commitments(channelParams: ChannelParams, changes: CommitmentChanges, active: Seq[Commitment], inactive: Seq[Commitment] = Nil, @@ -803,25 +830,29 @@ case class Commitments(params: ChannelParams, require(active.nonEmpty, "there must be at least one active commitment") - val channelId: ByteVector32 = params.channelId - val localNodeId: PublicKey = params.localNodeId - val remoteNodeId: PublicKey = params.remoteNodeId - val announceChannel: Boolean = params.announceChannel + val channelId: ByteVector32 = channelParams.channelId + val localNodeId: PublicKey = channelParams.localNodeId + val remoteNodeId: PublicKey = channelParams.remoteNodeId + val announceChannel: Boolean = channelParams.announceChannel + + val localChannelParams: LocalChannelParams = channelParams.localParams + val remoteChannelParams: RemoteChannelParams = channelParams.remoteParams // Commitment numbers are the same for all active commitments. - val localCommitIndex = active.head.localCommit.index - val remoteCommitIndex = active.head.remoteCommit.index - val nextRemoteCommitIndex = remoteCommitIndex + 1 + val localCommitIndex: Long = active.head.localCommit.index + val remoteCommitIndex: Long = active.head.remoteCommit.index + val nextRemoteCommitIndex: Long = remoteCommitIndex + 1 // While we have multiple active commitments, we use the most restrictive one. - val capacity = active.map(_.capacity).min - lazy val availableBalanceForSend: MilliSatoshi = active.map(_.availableBalanceForSend(params, changes)).min - lazy val availableBalanceForReceive: MilliSatoshi = active.map(_.availableBalanceForReceive(params, changes)).min + val capacity: Satoshi = active.map(_.capacity).min + val maxHtlcValueInFlight: MilliSatoshi = active.map(_.maxHtlcValueInFlight).min + lazy val availableBalanceForSend: MilliSatoshi = active.map(_.availableBalanceForSend(channelParams, changes)).min + lazy val availableBalanceForReceive: MilliSatoshi = active.map(_.availableBalanceForReceive(channelParams, changes)).min val all: Seq[Commitment] = active ++ inactive // We always use the last commitment that was created, to make sure we never go back in time. - val latest = FullCommitment(params, changes, active.head.fundingTxIndex, active.head.firstRemoteCommitIndex, active.head.remoteFundingPubKey, active.head.localFundingStatus, active.head.remoteFundingStatus, active.head.localCommit, active.head.remoteCommit, active.head.nextRemoteCommit_opt) + val latest: FullCommitment = FullCommitment(channelParams, changes, active.head) val lastLocalLocked_opt: Option[Commitment] = active.filter(_.localFundingStatus.isInstanceOf[LocalFundingStatus.Locked]).sortBy(_.fundingTxIndex).lastOption val lastRemoteLocked_opt: Option[Commitment] = active.filter(c => c.remoteFundingStatus == RemoteFundingStatus.Locked).sortBy(_.fundingTxIndex).lastOption @@ -837,17 +868,17 @@ case class Commitments(params: ChannelParams, def hasPendingOrProposedHtlcs: Boolean = active.head.hasPendingOrProposedHtlcs(changes) def timedOutOutgoingHtlcs(currentHeight: BlockHeight): Set[UpdateAddHtlc] = active.head.timedOutOutgoingHtlcs(currentHeight) def almostTimedOutIncomingHtlcs(currentHeight: BlockHeight, fulfillSafety: CltvExpiryDelta): Set[UpdateAddHtlc] = active.head.almostTimedOutIncomingHtlcs(currentHeight, fulfillSafety) - def getOutgoingHtlcCrossSigned(htlcId: Long): Option[UpdateAddHtlc] = active.head.getOutgoingHtlcCrossSigned(htlcId) + private def getOutgoingHtlcCrossSigned(htlcId: Long): Option[UpdateAddHtlc] = active.head.getOutgoingHtlcCrossSigned(htlcId) def getIncomingHtlcCrossSigned(htlcId: Long): Option[UpdateAddHtlc] = active.head.getIncomingHtlcCrossSigned(htlcId) // @formatter:on - def updateInitFeatures(localInit: Init, remoteInit: Init): Commitments = this.copy(params = params.updateFeatures(localInit, remoteInit)) + def updateInitFeatures(localInit: Init, remoteInit: Init): Commitments = this.copy(channelParams = channelParams.updateFeatures(localInit, remoteInit)) /** * @param cmd add HTLC command * @return either Left(failure, error message) where failure is a failure message (see BOLT #4 and the Failure Message class) or Right(new commitments, updateAddHtlc) */ - def sendAdd(cmd: CMD_ADD_HTLC, currentHeight: BlockHeight, channelConf: ChannelConf, feerates: FeeratesPerKw, feeConf: OnChainFeeConf): Either[ChannelException, (Commitments, UpdateAddHtlc)] = { + def sendAdd(cmd: CMD_ADD_HTLC, currentHeight: BlockHeight, channelConf: ChannelConf, feeConf: OnChainFeeConf)(implicit log: LoggingAdapter): Either[ChannelException, (Commitments, UpdateAddHtlc)] = { // we must ensure we're not relaying htlcs that are already expired, otherwise the downstream channel will instantly close // NB: we add a 3 blocks safety to reduce the probability of running into this when our bitcoin node is slightly outdated val minExpiry = CltvExpiry(currentHeight + 3) @@ -861,47 +892,68 @@ case class Commitments(params: ChannelParams, } // even if remote advertises support for 0 msat htlc, we limit ourselves to values strictly positive, hence the max(1 msat) - val htlcMinimum = params.remoteParams.htlcMinimum.max(1 msat) + val htlcMinimum = active.map(_.remoteCommitParams.htlcMinimum).max.max(1 msat) if (cmd.amount < htlcMinimum) { - return Left(HtlcValueTooSmall(params.channelId, minimum = htlcMinimum, actual = cmd.amount)) + return Left(HtlcValueTooSmall(channelId, minimum = htlcMinimum, actual = cmd.amount)) } - val add = UpdateAddHtlc(channelId, changes.localNextHtlcId, cmd.amount, cmd.paymentHash, cmd.cltvExpiry, cmd.onion, cmd.nextPathKey_opt, cmd.confidence, cmd.fundingFee_opt) + val add = UpdateAddHtlc(channelId, changes.localNextHtlcId, cmd.amount, cmd.paymentHash, cmd.cltvExpiry, cmd.onion, cmd.nextPathKey_opt, cmd.reputationScore.endorsement, cmd.fundingFee_opt) // we increment the local htlc index and add an entry to the origins map val changes1 = changes.addLocalProposal(add).copy(localNextHtlcId = changes.localNextHtlcId + 1) val originChannels1 = originChannels + (add.id -> cmd.origin) // we verify that this htlc is allowed in every active commitment - active.map(_.canSendAdd(add.amountMsat, params, changes1, feerates, feeConf)) - .collectFirst { case Left(f) => Left(f) } - .getOrElse(Right(copy(changes = changes1, originChannels = originChannels1), add)) + val failures = active.map(_.canSendAdd(add.amountMsat, channelParams, changes1, feeConf, cmd.reputationScore)) + // and that we don't exceed the authorized channel occupancy (jamming) + .appended(cmd.reputationScore.checkIncomingChannelOccupancy(cmd.origin.upstream.incomingChannelOccupancy, channelId)) + .collect { case Left(f) => f } + if (failures.isEmpty) { + Right(copy(changes = changes1, originChannels = originChannels1), add) + } else if (failures.forall(_.isInstanceOf[ChannelJammingException])) { + // We ignore jamming protection for now, but we log which HTLCs would be dropped if it was enabled. + val failure = failures.collectFirst { case f: ChannelJammingException => f }.get + Metrics.dropHtlc(failure, Tags.Directions.Outgoing) + failure match { + case f: TooManySmallHtlcs => log.info("TooManySmallHtlcs: {} outgoing HTLCs are below {}", f.number, f.below) + case f: IncomingConfidenceTooLow => log.info("IncomingConfidenceTooLow: confidence is {}% while channel is {}% full", (100 * f.confidence).toInt, (100 * f.occupancy).toInt) + case f: OutgoingConfidenceTooLow => log.info("OutgoingConfidenceTooLow: confidence is {}% while channel is {}% full", (100 * f.confidence).toInt, (100 * f.occupancy).toInt) + case _ => () + } + Right(copy(changes = changes1, originChannels = originChannels1), add) + } else { + // In most cases, the same failure will be returned for every commitment. Even if that's not the case, we can only + // send a single failure message to our peer, so we use the one that applies to the most recent active commitment. + val failure = failures.filterNot(_.isInstanceOf[ChannelJammingException]).head + Metrics.dropHtlc(failure, Tags.Directions.Outgoing) + Left(failure) + } } - def receiveAdd(add: UpdateAddHtlc, feerates: FeeratesPerKw, feeConf: OnChainFeeConf): Either[ChannelException, Commitments] = { + def receiveAdd(add: UpdateAddHtlc): Either[ChannelException, Commitments] = { if (add.id != changes.remoteNextHtlcId) { return Left(UnexpectedHtlcId(channelId, expected = changes.remoteNextHtlcId, actual = add.id)) } // we used to not enforce a strictly positive minimum, hence the max(1 msat) - val htlcMinimum = params.localParams.htlcMinimum.max(1 msat) + val htlcMinimum = active.map(_.localCommitParams.htlcMinimum).max.max(1 msat) if (add.amountMsat < htlcMinimum) { return Left(HtlcValueTooSmall(channelId, minimum = htlcMinimum, actual = add.amountMsat)) } val changes1 = changes.addRemoteProposal(add).copy(remoteNextHtlcId = changes.remoteNextHtlcId + 1) // we verify that this htlc is allowed in every active commitment - active.map(_.canReceiveAdd(add.amountMsat, params, changes1, feerates, feeConf)) + active.map(_.canReceiveAdd(add.amountMsat, channelParams, changes1)) .collectFirst { case Left(f) => Left(f) } .getOrElse(Right(copy(changes = changes1))) } - def sendFulfill(cmd: CMD_FULFILL_HTLC): Either[ChannelException, (Commitments, UpdateFulfillHtlc)] = + def sendFulfill(cmd: CMD_FULFILL_HTLC, nodeSecret: PrivateKey, useAttributionData: Boolean): Either[ChannelException, (Commitments, UpdateFulfillHtlc)] = getIncomingHtlcCrossSigned(cmd.id) match { case Some(htlc) if CommitmentChanges.alreadyProposed(changes.localChanges.proposed, htlc.id) => // we have already sent a fail/fulfill for this htlc Left(UnknownHtlcId(channelId, cmd.id)) case Some(htlc) if htlc.paymentHash == Crypto.sha256(cmd.r) => - payment.Monitoring.Metrics.recordIncomingPaymentDistribution(params.remoteNodeId, htlc.amountMsat) - val fulfill = UpdateFulfillHtlc(channelId, cmd.id, cmd.r) + payment.Monitoring.Metrics.recordIncomingPaymentDistribution(remoteNodeId, htlc.amountMsat) + val fulfill = OutgoingPaymentPacket.buildHtlcFulfill(nodeSecret, useAttributionData, cmd, htlc) Right((copy(changes = changes.addLocalProposal(fulfill)), fulfill)) case Some(_) => Left(InvalidHtlcPreimage(channelId, cmd.id)) case None => Left(UnknownHtlcId(channelId, cmd.id)) @@ -911,7 +963,7 @@ case class Commitments(params: ChannelParams, getOutgoingHtlcCrossSigned(fulfill.id) match { case Some(htlc) if htlc.paymentHash == Crypto.sha256(fulfill.paymentPreimage) => originChannels.get(fulfill.id) match { case Some(origin) => - payment.Monitoring.Metrics.recordOutgoingPaymentDistribution(params.remoteNodeId, htlc.amountMsat) + payment.Monitoring.Metrics.recordOutgoingPaymentDistribution(remoteNodeId, htlc.amountMsat) Right(copy(changes = changes.addRemoteProposal(fulfill)), origin, htlc) case None => Left(UnknownHtlcId(channelId, fulfill.id)) } @@ -919,14 +971,14 @@ case class Commitments(params: ChannelParams, case None => Left(UnknownHtlcId(channelId, fulfill.id)) } - def sendFail(cmd: CMD_FAIL_HTLC, nodeSecret: PrivateKey): Either[ChannelException, (Commitments, HtlcFailureMessage)] = + def sendFail(cmd: CMD_FAIL_HTLC, nodeSecret: PrivateKey, useAttributableFailures: Boolean): Either[ChannelException, (Commitments, HtlcFailureMessage)] = getIncomingHtlcCrossSigned(cmd.id) match { case Some(htlc) if CommitmentChanges.alreadyProposed(changes.localChanges.proposed, htlc.id) => // we have already sent a fail/fulfill for this htlc Left(UnknownHtlcId(channelId, cmd.id)) case Some(htlc) => // we need the shared secret to build the error packet - OutgoingPaymentPacket.buildHtlcFailure(nodeSecret, cmd, htlc).map(fail => (copy(changes = changes.addLocalProposal(fail)), fail)) + OutgoingPaymentPacket.buildHtlcFailure(nodeSecret, useAttributableFailures, cmd, htlc).map(fail => (copy(changes = changes.addLocalProposal(fail)), fail)) case None => Left(UnknownHtlcId(channelId, cmd.id)) } @@ -972,45 +1024,51 @@ case class Commitments(params: ChannelParams, } def sendFee(cmd: CMD_UPDATE_FEE, feeConf: OnChainFeeConf): Either[ChannelException, (Commitments, UpdateFee)] = { - if (!params.localParams.paysCommitTxFees) { + if (!channelParams.localParams.paysCommitTxFees) { Left(NonInitiatorCannotSendUpdateFee(channelId)) } else { val fee = UpdateFee(channelId, cmd.feeratePerKw) // update_fee replace each other, so we can remove previous ones val changes1 = changes.copy(localChanges = changes.localChanges.copy(proposed = changes.localChanges.proposed.filterNot(_.isInstanceOf[UpdateFee]) :+ fee)) - active.map(_.canSendFee(cmd.feeratePerKw, params, changes1, feeConf)) + active.map(_.canSendFee(cmd.feeratePerKw, channelParams, changes1, feeConf)) .collectFirst { case Left(f) => Left(f) } .getOrElse { - Metrics.LocalFeeratePerByte.withTag(Tags.CommitmentFormat, params.commitmentFormat.toString).record(FeeratePerByte(cmd.feeratePerKw).feerate.toLong) + Metrics.LocalFeeratePerByte.withTag(Tags.CommitmentFormat, active.head.commitmentFormat.toString).record(cmd.feeratePerKw.perByte.feerate.toLong) Right(copy(changes = changes1), fee) } } } def receiveFee(fee: UpdateFee, feerates: FeeratesPerKw, feeConf: OnChainFeeConf)(implicit log: LoggingAdapter): Either[ChannelException, Commitments] = { - if (params.localParams.paysCommitTxFees) { + if (channelParams.localParams.paysCommitTxFees) { Left(NonInitiatorCannotSendUpdateFee(channelId)) } else if (fee.feeratePerKw < FeeratePerKw.MinimumFeeratePerKw) { Left(FeerateTooSmall(channelId, remoteFeeratePerKw = fee.feeratePerKw)) } else { - val localFeeratePerKw = feeConf.getCommitmentFeerate(feerates, params.remoteNodeId, params.commitmentFormat, active.head.capacity) + val localFeeratePerKw = feeConf.getCommitmentFeerate(feerates, remoteNodeId, active.head.commitmentFormat) log.info("remote feeratePerKw={}, local feeratePerKw={}, ratio={}", fee.feeratePerKw, localFeeratePerKw, fee.feeratePerKw.toLong.toDouble / localFeeratePerKw.toLong) // update_fee replace each other, so we can remove previous ones val changes1 = changes.copy(remoteChanges = changes.remoteChanges.copy(proposed = changes.remoteChanges.proposed.filterNot(_.isInstanceOf[UpdateFee]) :+ fee)) - active.map(_.canReceiveFee(fee.feeratePerKw, params, changes1, feerates, feeConf)) + active.map(_.canReceiveFee(fee.feeratePerKw, channelParams, changes1, feerates, feeConf)) .collectFirst { case Left(f) => Left(f) } .getOrElse { - Metrics.RemoteFeeratePerByte.withTag(Tags.CommitmentFormat, params.commitmentFormat.toString).record(FeeratePerByte(fee.feeratePerKw).feerate.toLong) + Metrics.RemoteFeeratePerByte.withTag(Tags.CommitmentFormat, active.head.commitmentFormat.toString).record(fee.feeratePerKw.perByte.feerate.toLong) Right(copy(changes = changes1)) } } } - def sendCommit(keyManager: ChannelKeyManager)(implicit log: LoggingAdapter): Either[ChannelException, (Commitments, Seq[CommitSig])] = { + def sendCommit(channelKeys: ChannelKeys, nextRemoteCommitNonces: Map[TxId, IndividualNonce])(implicit log: LoggingAdapter): Either[ChannelException, (Commitments, CommitSigs)] = { remoteNextCommitInfo match { case Right(_) if !changes.localHasChanges => Left(CannotSignWithoutChanges(channelId)) case Right(remoteNextPerCommitmentPoint) => - val (active1, sigs) = active.map(_.sendCommit(keyManager, params, changes, remoteNextPerCommitmentPoint, active.size)).unzip + val commitKeys = RemoteCommitmentKeys(channelParams, channelKeys, remoteNextPerCommitmentPoint) + val (active1, sigs) = active.map(c => { + c.sendCommit(channelParams, channelKeys, commitKeys, changes, remoteNextPerCommitmentPoint, active.size, nextRemoteCommitNonces.get(c.fundingTxId)) match { + case Left(e) => return Left(e) + case Right((c, cs)) => (c, cs) + } + }).unzip val commitments1 = copy( changes = changes.copy( localChanges = changes.localChanges.copy(proposed = Nil, signed = changes.localChanges.proposed), @@ -1019,34 +1077,43 @@ case class Commitments(params: ChannelParams, active = active1, remoteNextCommitInfo = Left(WaitForRev(localCommitIndex)) ) - Right(commitments1, sigs) + Right(commitments1, CommitSigs(sigs)) case Left(_) => Left(CannotSignBeforeRevocation(channelId)) } } - def receiveCommit(commits: Seq[CommitSig], keyManager: ChannelKeyManager)(implicit log: LoggingAdapter): Either[ChannelException, (Commitments, RevokeAndAck)] = { + def receiveCommit(commitSigs: CommitSigs, channelKeys: ChannelKeys)(implicit log: LoggingAdapter): Either[ChannelException, (Commitments, RevokeAndAck)] = { // We may receive more commit_sig than the number of active commitments, because there can be a race where we send // splice_locked while our peer is sending us a batch of commit_sig. When that happens, we simply need to discard // the commit_sig that belong to commitments we deactivated. - if (commits.size < active.size) { - return Left(CommitSigCountMismatch(channelId, active.size, commits.size)) + val sigs = commitSigs match { + case batch: CommitSigBatch if batch.batchSize < active.size => return Left(CommitSigCountMismatch(channelId, active.size, batch.batchSize)) + case batch: CommitSigBatch => batch.messages + case _: CommitSig if active.size > 1 => return Left(CommitSigCountMismatch(channelId, active.size, 1)) + case commitSig: CommitSig => Seq(commitSig) } - val channelKeyPath = keyManager.keyPath(params.localParams, params.channelConfig) - val localPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, localCommitIndex + 1) // Signatures are sent in order (most recent first), calling `zip` will drop trailing sigs that are for deactivated/pruned commitments. - val active1 = active.zip(commits).map { case (commitment, commit) => - commitment.receiveCommit(keyManager, params, changes, localPerCommitmentPoint, commit) match { + val commitKeys = LocalCommitmentKeys(channelParams, channelKeys, localCommitIndex + 1) + val active1 = active.zip(sigs).map { case (commitment, commit) => + commitment.receiveCommit(channelParams, channelKeys, commitKeys, changes, commit) match { case Left(f) => return Left(f) case Right(commitment1) => commitment1 } } // we will send our revocation preimage + our next revocation hash - val localPerCommitmentSecret = keyManager.commitmentSecret(channelKeyPath, localCommitIndex) - val localNextPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, localCommitIndex + 2) + val localPerCommitmentSecret = channelKeys.commitmentSecret(localCommitIndex) + val localNextPerCommitmentPoint = channelKeys.commitmentPoint(localCommitIndex + 2) + val localCommitNonces = active.flatMap(c => c.commitmentFormat match { + case _: SegwitV0CommitmentFormat => None + case _: SimpleTaprootChannelCommitmentFormat => + val localNonce = NonceGenerator.verificationNonce(c.fundingTxId, c.localFundingKey(channelKeys), c.remoteFundingPubKey, localCommitIndex + 2) + Some(c.fundingTxId -> localNonce.publicNonce) + }) val revocation = RevokeAndAck( channelId = channelId, perCommitmentSecret = localPerCommitmentSecret, - nextPerCommitmentPoint = localNextPerCommitmentPoint + nextPerCommitmentPoint = localNextPerCommitmentPoint, + nextCommitNonces = localCommitNonces, ) val commitments1 = copy( changes = changes.copy( @@ -1063,6 +1130,9 @@ case class Commitments(params: ChannelParams, remoteNextCommitInfo match { case Right(_) => Left(UnexpectedRevocation(channelId)) case Left(_) if revocation.perCommitmentSecret.publicKey != active.head.remoteCommit.remotePerCommitmentPoint => Left(InvalidRevocation(channelId)) + case Left(_) if active.exists(c => c.commitmentFormat.isInstanceOf[TaprootCommitmentFormat] && !revocation.nextCommitNonces.contains(c.fundingTxId)) => + val missingNonce = active.find(c => c.commitmentFormat.isInstanceOf[TaprootCommitmentFormat] && !revocation.nextCommitNonces.contains(c.fundingTxId)).get + Left(MissingCommitNonce(channelId, missingNonce.fundingTxId, remoteCommitIndex + 1)) case Left(_) => // Since htlcs are shared across all commitments, we generate the actions only once based on the first commitment. val receivedHtlcs = changes.remoteChanges.signed.collect { @@ -1093,27 +1163,27 @@ case class Commitments(params: ChannelParams, case _ => true }) // NB: we are supposed to keep nextRemoteCommit_opt consistent with remoteNextCommitInfo: this should exist. - val nextRemoteSpec = active.head.nextRemoteCommit_opt.get.commit.spec + val nextRemoteSpec = active.head.nextRemoteCommit_opt.get.spec val remoteSpecWithoutNewHtlcs = nextRemoteSpec.copy(htlcs = nextRemoteSpec.htlcs.filter { case OutgoingHtlc(add) if receivedHtlcs.contains(add) => false case _ => true }) val localReduced = DustExposure.reduceForDustExposure(localSpecWithoutNewHtlcs, changes.localChanges.all, changes.remoteChanges.acked) - val localCommitDustExposure = DustExposure.computeExposure(localReduced, params.localParams.dustLimit, params.commitmentFormat) + val localCommitDustExposure = active.map(c => DustExposure.computeExposure(localReduced, c.localCommitParams.dustLimit, c.commitmentFormat)).max val remoteReduced = DustExposure.reduceForDustExposure(remoteSpecWithoutNewHtlcs, changes.remoteChanges.acked, changes.localChanges.all) - val remoteCommitDustExposure = DustExposure.computeExposure(remoteReduced, params.remoteParams.dustLimit, params.commitmentFormat) + val remoteCommitDustExposure = active.map(c => DustExposure.computeExposure(remoteReduced, c.remoteCommitParams.dustLimit, c.commitmentFormat)).max // we sort incoming htlcs by decreasing amount: we want to prioritize higher amounts. val sortedReceivedHtlcs = receivedHtlcs.sortBy(_.amountMsat).reverse DustExposure.filterBeforeForward( maxDustExposure, localReduced, - params.localParams.dustLimit, + active.map(_.localCommitParams.dustLimit).max, localCommitDustExposure, remoteReduced, - params.remoteParams.dustLimit, + active.map(_.remoteCommitParams.dustLimit).max, remoteCommitDustExposure, sortedReceivedHtlcs, - params.commitmentFormat) + active.head.commitmentFormat) } val actions = acceptedHtlcs.map(add => PostRevocationAction.RelayHtlc(add)) ++ rejectedHtlcs.map(add => PostRevocationAction.RejectHtlc(add)) ++ @@ -1129,7 +1199,7 @@ case class Commitments(params: ChannelParams, // we remove the newly completed htlcs from the origin map val originChannels1 = originChannels -- completedOutgoingHtlcs val active1 = active.map(c => c.copy( - remoteCommit = c.nextRemoteCommit_opt.get.commit, + remoteCommit = c.nextRemoteCommit_opt.get, nextRemoteCommit_opt = None, )) val commitments1 = copy( @@ -1150,24 +1220,19 @@ case class Commitments(params: ChannelParams, this.copy(changes = changes.discardUnsignedUpdates()) } - def validateSeed(keyManager: ChannelKeyManager): Boolean = { + def validateSeed(channelKeys: ChannelKeys): Boolean = { active.forall { commitment => - val localFundingKey = keyManager.fundingPublicKey(params.localParams.fundingKeyPath, commitment.fundingTxIndex).publicKey - val remoteFundingKey = commitment.remoteFundingPubKey - val fundingScript = Script.write(Scripts.multiSig2of2(localFundingKey, remoteFundingKey)) - commitment.commitInput match { - case InputInfo.SegwitInput(_, _, redeemScript) => redeemScript == fundingScript - case _: InputInfo.TaprootInput => false + commitment.localFundingStatus match { + // We ignore unconfirmed transactions for simplicity. + case _: LocalFundingStatus.UnconfirmedFundingTx => true + case tx: LocalFundingStatus.ConfirmedFundingTx => + val localFundingKey = commitment.localFundingKey(channelKeys).publicKey + val redeemInfo = Transactions.makeFundingScript(localFundingKey, commitment.remoteFundingPubKey, commitment.commitmentFormat) + tx.txOut.publicKeyScript == redeemInfo.pubkeyScript } } } - /** This function should be used to ignore a commit_sig that we've already received. */ - def ignoreRetransmittedCommitSig(commitSig: CommitSig): Boolean = { - val RemoteSignature.FullSignature(latestRemoteSig) = latest.localCommit.commitTxAndRemoteSig.remoteSig - params.channelFeatures.hasFeature(Features.DualFunding) && commitSig.batchSize == 1 && latestRemoteSig == commitSig.signature - } - def localFundingSigs(fundingTxId: TxId): Option[TxSignatures] = { all.find(_.fundingTxId == fundingTxId).flatMap(_.localFundingStatus.localSigs_opt) } @@ -1268,7 +1333,7 @@ case class Commitments(params: ChannelParams, case Some(lastConfirmed) => // NB: we cannot prune active commitments, even if we know that they have been double-spent, because our peer // may not yet be aware of it, and will expect us to send commit_sig. - val pruned = if (params.announceChannel) { + val pruned = if (channelParams.announceChannel) { // If the most recently confirmed commitment isn't announced yet, we cannot prune the last commitment we // announced, because our channel updates are based on its announcement (and its short_channel_id). // If we never announced the channel, we don't need to announce old commitments, we will directly announce the last one. @@ -1293,7 +1358,7 @@ case class Commitments(params: ChannelParams, * @param spendingTx A transaction that may spend a current or former funding tx */ def resolveCommitment(spendingTx: Transaction): Option[Commitment] = { - all.find(c => spendingTx.txIn.map(_.outPoint).contains(c.commitInput.outPoint)) + all.find(c => spendingTx.txIn.map(_.outPoint).contains(c.fundingInput)) } /** Find the corresponding commitment based on its short_channel_id (once funding transaction is confirmed). */ diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/DustExposure.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/DustExposure.scala index 2279e87ec1..a8af34738f 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/DustExposure.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/DustExposure.scala @@ -36,7 +36,7 @@ object DustExposure { * However, this cannot fully protect us if the feerate increases too much (in which case we may have to force-close). */ def feerateForDustExposure(currentFeerate: FeeratePerKw): FeeratePerKw = { - (currentFeerate * 1.25).max(currentFeerate + FeeratePerKw(FeeratePerByte(10 sat))) + (currentFeerate * 1.25).max(currentFeerate + FeeratePerByte(10 sat).perKw) } /** Test whether the given HTLC contributes to our dust exposure with the default dust feerate calculation. */ diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala index 24a1448459..bec02e006c 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala @@ -17,22 +17,22 @@ package fr.acinq.eclair.channel import akka.event.{DiagnosticLoggingAdapter, LoggingAdapter} -import fr.acinq.bitcoin.ScriptFlags import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey, sha256} -import fr.acinq.bitcoin.scalacompat.Script._ +import fr.acinq.bitcoin.scalacompat.Musig2.{IndividualNonce, LocalNonce} import fr.acinq.bitcoin.scalacompat._ import fr.acinq.eclair._ -import fr.acinq.eclair.blockchain.OnChainPubkeyCache +import fr.acinq.eclair.blockchain.OnChainAddressCache import fr.acinq.eclair.blockchain.fee._ +import fr.acinq.eclair.channel.ChannelSpendSignature.{IndividualSignature, PartialSignatureWithNonce} import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.channel.fsm.Channel.REFRESH_CHANNEL_UPDATE_INTERVAL -import fr.acinq.eclair.crypto.keymanager.ChannelKeyManager -import fr.acinq.eclair.crypto.{Generators, ShaChain} +import fr.acinq.eclair.channel.fund.InteractiveTxSigningSession +import fr.acinq.eclair.crypto.keymanager.{ChannelKeys, LocalCommitmentKeys, RemoteCommitmentKeys} +import fr.acinq.eclair.crypto.{NonceGenerator, ShaChain} import fr.acinq.eclair.db.ChannelsDb import fr.acinq.eclair.payment.relay.Relayer.RelayFees import fr.acinq.eclair.router.Announcements import fr.acinq.eclair.transactions.DirectedHtlc._ -import fr.acinq.eclair.transactions.Scripts._ import fr.acinq.eclair.transactions.Transactions._ import fr.acinq.eclair.transactions._ import fr.acinq.eclair.wire.protocol._ @@ -99,7 +99,7 @@ object Helpers { } /** Called by the fundee of a single-funded channel. */ - def validateParamsSingleFundedFundee(nodeParams: NodeParams, channelType: SupportedChannelType, localFeatures: Features[InitFeature], open: OpenChannel, remoteNodeId: PublicKey, remoteFeatures: Features[InitFeature]): Either[ChannelException, (ChannelFeatures, Option[ByteVector])] = { + def validateParamsSingleFundedFundee(nodeParams: NodeParams, localFeatures: Features[InitFeature], open: OpenChannel, remoteNodeId: PublicKey, remoteFeatures: Features[InitFeature]): Either[ChannelException, (ChannelFeatures, Option[ByteVector])] = { // BOLT #2: if the chain_hash value, within the open_channel, message is set to a hash of a chain that is unknown to the receiver: // MUST reject the channel. if (nodeParams.chainHash != open.chainHash) return Left(InvalidChainHash(open.temporaryChannelId, local = nodeParams.chainHash, remote = open.chainHash)) @@ -133,11 +133,19 @@ object Helpers { return Left(ChannelReserveNotMet(open.temporaryChannelId, toLocalMsat, toRemoteMsat, open.channelReserveSatoshis)) } + val channelType = ChannelTypes.areCompatible(open.temporaryChannelId, localFeatures, open.channelType_opt) match { + case Left(f) => return Left(f) + case Right(proposedChannelType) => proposedChannelType + } val channelFeatures = ChannelFeatures(channelType, localFeatures, remoteFeatures, open.channelFlags.announceChannel) + channelType.commitmentFormat match { + case _: SimpleTaprootChannelCommitmentFormat => if (open.commitNonce_opt.isEmpty) return Left(MissingCommitNonce(open.temporaryChannelId, TxId(ByteVector32.Zeroes), commitmentNumber = 0)) + case _: AnchorOutputsCommitmentFormat => () + } // BOLT #2: The receiving node MUST fail the channel if: it considers feerate_per_kw too small for timely processing or unreasonably large. - val localFeeratePerKw = nodeParams.onChainFeeConf.getCommitmentFeerate(nodeParams.currentBitcoinCoreFeerates, remoteNodeId, channelFeatures.commitmentFormat, open.fundingSatoshis) - if (nodeParams.onChainFeeConf.feerateToleranceFor(remoteNodeId).isFeeDiffTooHigh(channelFeatures.commitmentFormat, localFeeratePerKw, open.feeratePerKw)) return Left(FeerateTooDifferent(open.temporaryChannelId, localFeeratePerKw, open.feeratePerKw)) + val localFeeratePerKw = nodeParams.onChainFeeConf.getCommitmentFeerate(nodeParams.currentBitcoinCoreFeerates, remoteNodeId, channelType.commitmentFormat) + if (nodeParams.onChainFeeConf.feerateToleranceFor(remoteNodeId).isProposedCommitFeerateTooHigh(localFeeratePerKw, open.feeratePerKw)) return Left(FeerateTooDifferent(open.temporaryChannelId, localFeeratePerKw, open.feeratePerKw)) // we don't check that the funder's amount for the initial commitment transaction is sufficient for full fee payment // now, but it will be done later when we receive `funding_created` @@ -150,7 +158,6 @@ object Helpers { /** Called by the non-initiator of a dual-funded channel. */ def validateParamsDualFundedNonInitiator(nodeParams: NodeParams, - channelType: SupportedChannelType, open: OpenDualFundedChannel, fundingScript: ByteVector, remoteNodeId: PublicKey, @@ -180,11 +187,15 @@ object Helpers { if (open.dustLimit < Channel.MIN_DUST_LIMIT) return Left(DustLimitTooSmall(open.temporaryChannelId, open.dustLimit, Channel.MIN_DUST_LIMIT)) if (open.dustLimit > nodeParams.channelConf.maxRemoteDustLimit) return Left(DustLimitTooLarge(open.temporaryChannelId, open.dustLimit, nodeParams.channelConf.maxRemoteDustLimit)) + val channelType = ChannelTypes.areCompatible(open.temporaryChannelId, localFeatures, open.channelType_opt) match { + case Left(f) => return Left(f) + case Right(proposedChannelType) => proposedChannelType + } val channelFeatures = ChannelFeatures(channelType, localFeatures, remoteFeatures, open.channelFlags.announceChannel) // BOLT #2: The receiving node MUST fail the channel if: it considers feerate_per_kw too small for timely processing or unreasonably large. - val localFeeratePerKw = nodeParams.onChainFeeConf.getCommitmentFeerate(nodeParams.currentBitcoinCoreFeerates, remoteNodeId, channelFeatures.commitmentFormat, open.fundingAmount) - if (nodeParams.onChainFeeConf.feerateToleranceFor(remoteNodeId).isFeeDiffTooHigh(channelFeatures.commitmentFormat, localFeeratePerKw, open.commitmentFeerate)) return Left(FeerateTooDifferent(open.temporaryChannelId, localFeeratePerKw, open.commitmentFeerate)) + val localFeeratePerKw = nodeParams.onChainFeeConf.getCommitmentFeerate(nodeParams.currentBitcoinCoreFeerates, remoteNodeId, channelType.commitmentFormat) + if (nodeParams.onChainFeeConf.feerateToleranceFor(remoteNodeId).isProposedCommitFeerateTooHigh(localFeeratePerKw, open.commitmentFeerate)) return Left(FeerateTooDifferent(open.temporaryChannelId, localFeeratePerKw, open.commitmentFeerate)) for { script_opt <- extractShutdownScript(open.temporaryChannelId, localFeatures, remoteFeatures, open.upfrontShutdownScript_opt) @@ -192,29 +203,19 @@ object Helpers { } yield (channelFeatures, script_opt, willFund_opt) } - private def validateChannelType(channelId: ByteVector32, channelType: SupportedChannelType, channelFlags: ChannelFlags, openChannelType_opt: Option[ChannelType], acceptChannelType_opt: Option[ChannelType], localFeatures: Features[InitFeature], remoteFeatures: Features[InitFeature]): Option[ChannelException] = { - acceptChannelType_opt match { - case Some(theirChannelType) if acceptChannelType_opt != openChannelType_opt => - // if channel_type is set, and channel_type was set in open_channel, and they are not equal types: MUST reject the channel. - Some(InvalidChannelType(channelId, channelType, theirChannelType)) - case None if Features.canUseFeature(localFeatures, remoteFeatures, Features.ChannelType) => - // Bolt 2: if `option_channel_type` is negotiated: MUST set `channel_type` - Some(MissingChannelType(channelId)) - case None if channelType != ChannelTypes.defaultFromFeatures(localFeatures, remoteFeatures, channelFlags.announceChannel) => - // If we have overridden the default channel type, but they didn't support explicit channel type negotiation, - // we need to abort because they expect a different channel type than what we offered. - Some(InvalidChannelType(channelId, channelType, ChannelTypes.defaultFromFeatures(localFeatures, remoteFeatures, channelFlags.announceChannel))) - case _ => - // we agree on channel type - None + private def validateChannelTypeInitiator(channelId: ByteVector32, openChannelType_opt: Option[ChannelType], acceptChannelType_opt: Option[ChannelType]): Either[ChannelException, SupportedChannelType] = { + (openChannelType_opt, acceptChannelType_opt) match { + case (Some(proposed: SupportedChannelType), Some(received)) if proposed == received => Right(proposed) + case (Some(_), Some(received)) => Left(InvalidChannelType(channelId, received)) + case _ => Left(MissingChannelType(channelId)) } } /** Called by the funder of a single-funded channel. */ - def validateParamsSingleFundedFunder(nodeParams: NodeParams, channelType: SupportedChannelType, localFeatures: Features[InitFeature], remoteFeatures: Features[InitFeature], open: OpenChannel, accept: AcceptChannel): Either[ChannelException, (ChannelFeatures, Option[ByteVector])] = { - validateChannelType(open.temporaryChannelId, channelType, open.channelFlags, open.channelType_opt, accept.channelType_opt, localFeatures, remoteFeatures) match { - case Some(t) => return Left(t) - case None => // we agree on channel type + def validateParamsSingleFundedFunder(nodeParams: NodeParams, localFeatures: Features[InitFeature], remoteFeatures: Features[InitFeature], open: OpenChannel, accept: AcceptChannel): Either[ChannelException, (ChannelFeatures, Option[ByteVector])] = { + val channelType = validateChannelTypeInitiator(open.temporaryChannelId, open.channelType_opt, accept.channelType_opt) match { + case Left(t) => return Left(t) + case Right(channelType) => channelType } if (accept.maxAcceptedHtlcs > Channel.MAX_ACCEPTED_HTLCS) return Left(InvalidMaxAcceptedHtlcs(accept.temporaryChannelId, accept.maxAcceptedHtlcs, Channel.MAX_ACCEPTED_HTLCS)) @@ -241,20 +242,23 @@ object Helpers { if (reserveToFundingRatio > nodeParams.channelConf.maxReserveToFundingRatio) return Left(ChannelReserveTooHigh(open.temporaryChannelId, accept.channelReserveSatoshis, reserveToFundingRatio, nodeParams.channelConf.maxReserveToFundingRatio)) val channelFeatures = ChannelFeatures(channelType, localFeatures, remoteFeatures, open.channelFlags.announceChannel) + channelType.commitmentFormat match { + case _: SimpleTaprootChannelCommitmentFormat => if (accept.commitNonce_opt.isEmpty) return Left(MissingCommitNonce(open.temporaryChannelId, TxId(ByteVector32.Zeroes), commitmentNumber = 0)) + case _: AnchorOutputsCommitmentFormat => () + } extractShutdownScript(accept.temporaryChannelId, localFeatures, remoteFeatures, accept.upfrontShutdownScript_opt).map(script_opt => (channelFeatures, script_opt)) } /** Called by the initiator of a dual-funded channel. */ def validateParamsDualFundedInitiator(nodeParams: NodeParams, remoteNodeId: PublicKey, - channelType: SupportedChannelType, localFeatures: Features[InitFeature], remoteFeatures: Features[InitFeature], open: OpenDualFundedChannel, accept: AcceptDualFundedChannel): Either[ChannelException, (ChannelFeatures, Option[ByteVector], Option[LiquidityAds.Purchase])] = { - validateChannelType(open.temporaryChannelId, channelType, open.channelFlags, open.channelType_opt, accept.channelType_opt, localFeatures, remoteFeatures) match { - case Some(t) => return Left(t) - case None => // we agree on channel type + val channelType = validateChannelTypeInitiator(open.temporaryChannelId, open.channelType_opt, accept.channelType_opt) match { + case Left(t) => return Left(t) + case Right(channelType) => channelType } // BOLT #2: Channel funding limits @@ -275,7 +279,7 @@ object Helpers { for { script_opt <- extractShutdownScript(accept.temporaryChannelId, localFeatures, remoteFeatures, accept.upfrontShutdownScript_opt) - fundingScript = Funding.makeFundingPubKeyScript(open.fundingPubkey, accept.fundingPubkey) + fundingScript = Transactions.makeFundingScript(open.fundingPubkey, accept.fundingPubkey, channelType.commitmentFormat).pubkeyScript liquidityPurchase_opt <- LiquidityAds.validateRemoteFunding(open.requestFunding_opt, remoteNodeId, accept.temporaryChannelId, fundingScript, accept.fundingAmount, open.fundingFeerate, isChannelCreation = true, accept.willFund_opt) } yield { val channelFeatures = ChannelFeatures(channelType, localFeatures, remoteFeatures, open.channelFlags.announceChannel) @@ -292,10 +296,8 @@ object Helpers { } /** Compute the temporaryChannelId of a dual-funded channel. */ - def dualFundedTemporaryChannelId(nodeParams: NodeParams, localParams: LocalParams, channelConfig: ChannelConfig): ByteVector32 = { - val channelKeyPath = nodeParams.channelKeyManager.keyPath(localParams, channelConfig) - val revocationBasepoint = nodeParams.channelKeyManager.revocationPoint(channelKeyPath).publicKey - Crypto.sha256(ByteVector.fill(33)(0) ++ revocationBasepoint.value) + def dualFundedTemporaryChannelId(channelKeys: ChannelKeys): ByteVector32 = { + Crypto.sha256(ByteVector.fill(33)(0) ++ channelKeys.revocationBasePoint.value) } /** Compute the channelId of a dual-funded channel. */ @@ -332,6 +334,22 @@ object Helpers { } } + def channelUpdate(nodeParams: NodeParams, shortChannelId: ShortChannelId, commitments: Commitments, relayFees: RelayFees, enable: Boolean): ChannelUpdate = { + Announcements.makeChannelUpdate( + chainHash = nodeParams.chainHash, + nodeSecret = nodeParams.privateKey, + remoteNodeId = commitments.remoteNodeId, + shortChannelId = shortChannelId, + cltvExpiryDelta = nodeParams.channelConf.expiryDelta, + htlcMinimumMsat = commitments.latest.remoteCommitParams.htlcMinimum, + feeBaseMsat = relayFees.feeBase, + feeProportionalMillionths = relayFees.feeProportionalMillionths, + htlcMaximumMsat = maxHtlcAmount(nodeParams, commitments), + isPrivate = !commitments.announceChannel, + enable = enable, + ) + } + /** * Compute the delay until we need to refresh the channel_update for our channel not to be considered stale by * other nodes. @@ -356,14 +374,17 @@ object Helpers { def maxHtlcAmount(nodeParams: NodeParams, commitments: Commitments): MilliSatoshi = { if (!commitments.announceChannel) { // The channel is private, let's not change the channel update needlessly. - return commitments.params.maxHtlcAmount + return commitments.maxHtlcValueInFlight } for (balanceThreshold <- nodeParams.channelConf.balanceThresholds) { if (commitments.availableBalanceForSend <= balanceThreshold.available) { - return balanceThreshold.maxHtlcAmount.toMilliSatoshi.max(commitments.params.remoteParams.htlcMinimum).min(commitments.params.maxHtlcAmount) + // Our maximum HTLC amount must always be greater than htlc_minimum_msat. + val allowedHtlcAmount = Seq(balanceThreshold.maxHtlcAmount.toMilliSatoshi, commitments.latest.localCommitParams.htlcMinimum, commitments.latest.remoteCommitParams.htlcMinimum).max + // But it cannot exceed the channel's max_htlc_value_in_flight_msat. + return allowedHtlcAmount.min(commitments.maxHtlcValueInFlight) } } - commitments.params.maxHtlcAmount + commitments.maxHtlcValueInFlight } def getRelayFees(nodeParams: NodeParams, remoteNodeId: PublicKey, announceChannel: Boolean): RelayFees = { @@ -373,36 +394,30 @@ object Helpers { object Funding { - def makeFundingPubKeyScript(localFundingKey: PublicKey, remoteFundingKey: PublicKey): ByteVector = write(pay2wsh(multiSig2of2(localFundingKey, remoteFundingKey))) - - def makeFundingInputInfo(fundingTxId: TxId, fundingTxOutputIndex: Int, fundingSatoshis: Satoshi, fundingPubkey1: PublicKey, fundingPubkey2: PublicKey): InputInfo.SegwitInput = { - val fundingScript = multiSig2of2(fundingPubkey1, fundingPubkey2) - val fundingTxOut = TxOut(fundingSatoshis, pay2wsh(fundingScript)) - InputInfo.SegwitInput(OutPoint(fundingTxId, fundingTxOutputIndex), fundingTxOut, write(fundingScript)) - } - /** * Creates both sides' first commitment transaction. * * @return (localSpec, localTx, remoteSpec, remoteTx) */ - def makeFirstCommitTxs(keyManager: ChannelKeyManager, - params: ChannelParams, + def makeFirstCommitTxs(channelParams: ChannelParams, + localCommitParams: CommitParams, remoteCommitParams: CommitParams, localFundingAmount: Satoshi, remoteFundingAmount: Satoshi, localPushAmount: MilliSatoshi, remotePushAmount: MilliSatoshi, - commitTxFeerate: FeeratePerKw, + commitTxFeerate: FeeratePerKw, commitmentFormat: CommitmentFormat, fundingTxId: TxId, fundingTxOutputIndex: Int, - remoteFundingPubKey: PublicKey, - remoteFirstPerCommitmentPoint: PublicKey): Either[ChannelException, (CommitmentSpec, CommitTx, CommitmentSpec, CommitTx)] = { - makeCommitTxs(keyManager, params, + localFundingKey: PrivateKey, remoteFundingPubKey: PublicKey, + localCommitKeys: LocalCommitmentKeys, remoteCommitKeys: RemoteCommitmentKeys): Either[ChannelException, (CommitmentSpec, CommitTx, CommitmentSpec, CommitTx)] = { + makeCommitTxs(channelParams, localCommitParams, remoteCommitParams, fundingAmount = localFundingAmount + remoteFundingAmount, toLocal = localFundingAmount.toMilliSatoshi - localPushAmount + remotePushAmount, toRemote = remoteFundingAmount.toMilliSatoshi + localPushAmount - remotePushAmount, localHtlcs = Set.empty, commitTxFeerate, + commitmentFormat, fundingTxIndex = 0, fundingTxId, fundingTxOutputIndex, - remoteFundingPubKey = remoteFundingPubKey, remotePerCommitmentPoint = remoteFirstPerCommitmentPoint, + localFundingKey, remoteFundingPubKey, + localCommitKeys, remoteCommitKeys, localCommitmentIndex = 0, remoteCommitmentIndex = 0).map { case (localSpec, localCommit, remoteSpec, remoteCommit, _) => (localSpec, localCommit, remoteSpec, remoteCommit) } @@ -412,39 +427,37 @@ object Helpers { * This creates commitment transactions for both sides at an arbitrary `commitmentIndex` and with (optional) `htlc` * outputs. This function should only be used when commitments are synchronized (local and remote htlcs match). */ - def makeCommitTxs(keyManager: ChannelKeyManager, - params: ChannelParams, + def makeCommitTxs(channelParams: ChannelParams, + localCommitParams: CommitParams, + remoteCommitParams: CommitParams, fundingAmount: Satoshi, toLocal: MilliSatoshi, toRemote: MilliSatoshi, localHtlcs: Set[DirectedHtlc], commitTxFeerate: FeeratePerKw, + commitmentFormat: CommitmentFormat, fundingTxIndex: Long, fundingTxId: TxId, fundingTxOutputIndex: Int, - remoteFundingPubKey: PublicKey, - remotePerCommitmentPoint: PublicKey, - localCommitmentIndex: Long, remoteCommitmentIndex: Long): Either[ChannelException, (CommitmentSpec, CommitTx, CommitmentSpec, CommitTx, Seq[HtlcTx])] = { - import params._ + localFundingKey: PrivateKey, remoteFundingPubKey: PublicKey, + localCommitKeys: LocalCommitmentKeys, remoteCommitKeys: RemoteCommitmentKeys, + localCommitmentIndex: Long, remoteCommitmentIndex: Long): Either[ChannelException, (CommitmentSpec, CommitTx, CommitmentSpec, CommitTx, Seq[UnsignedHtlcTx])] = { val localSpec = CommitmentSpec(localHtlcs, commitTxFeerate, toLocal = toLocal, toRemote = toRemote) val remoteSpec = CommitmentSpec(localHtlcs.map(_.opposite), commitTxFeerate, toLocal = toRemote, toRemote = toLocal) - if (!localParams.paysCommitTxFees) { + if (!channelParams.localParams.paysCommitTxFees) { // They are responsible for paying the commitment transaction fee: we need to make sure they can afford it! // Note that the reserve may not always be met: we could be using dual funding with a large funding amount on // our side and a small funding amount on their side. But we shouldn't care as long as they can pay the fees for // the commitment transaction. - val fees = commitTxTotalCost(remoteParams.dustLimit, remoteSpec, channelFeatures.commitmentFormat) + val fees = commitTxTotalCost(remoteCommitParams.dustLimit, remoteSpec, commitmentFormat) val missing = fees - toRemote.truncateToSatoshi if (missing > 0.sat) { - return Left(CannotAffordFirstCommitFees(channelId, missing = missing, fees = fees)) + return Left(CannotAffordFirstCommitFees(channelParams.channelId, missing = missing, fees = fees)) } } - val fundingPubKey = keyManager.fundingPublicKey(localParams.fundingKeyPath, fundingTxIndex) - val channelKeyPath = keyManager.keyPath(localParams, channelConfig) - val commitmentInput = makeFundingInputInfo(fundingTxId, fundingTxOutputIndex, fundingAmount, fundingPubKey.publicKey, remoteFundingPubKey) - val localPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, localCommitmentIndex) - val (localCommitTx, _) = Commitment.makeLocalTxs(keyManager, channelConfig, channelFeatures, localCommitmentIndex, localParams, remoteParams, fundingTxIndex, remoteFundingPubKey, commitmentInput, localPerCommitmentPoint, localSpec) - val (remoteCommitTx, htlcTxs) = Commitment.makeRemoteTxs(keyManager, channelConfig, channelFeatures, remoteCommitmentIndex, localParams, remoteParams, fundingTxIndex, remoteFundingPubKey, commitmentInput, remotePerCommitmentPoint, remoteSpec) + val commitmentInput = makeFundingInputInfo(fundingTxId, fundingTxOutputIndex, fundingAmount, localFundingKey.publicKey, remoteFundingPubKey, commitmentFormat) + val (localCommitTx, _) = Commitment.makeLocalTxs(channelParams, localCommitParams, localCommitKeys, localCommitmentIndex, localFundingKey, remoteFundingPubKey, commitmentInput, commitmentFormat, localSpec) + val (remoteCommitTx, htlcTxs) = Commitment.makeRemoteTxs(channelParams, remoteCommitParams, remoteCommitKeys, remoteCommitmentIndex, localFundingKey, remoteFundingPubKey, commitmentInput, commitmentFormat, remoteSpec) val sortedHtlcTxs = htlcTxs.sortBy(_.input.outPoint.index) Right(localSpec, localCommitTx, remoteSpec, remoteCommitTx, sortedHtlcTxs) } @@ -468,7 +481,7 @@ object Helpers { /** * Check whether we are in sync with our peer. */ - def checkSync(keyManager: ChannelKeyManager, commitments: Commitments, remoteChannelReestablish: ChannelReestablish): SyncResult = { + def checkSync(channelKeys: ChannelKeys, commitments: Commitments, remoteChannelReestablish: ChannelReestablish): SyncResult = { // This is done in two steps: // - step 1: we check our local commitment @@ -479,17 +492,26 @@ object Helpers { def checkRemoteCommit(remoteChannelReestablish: ChannelReestablish, retransmitRevocation_opt: Option[RevokeAndAck]): SyncResult = { commitments.remoteNextCommitInfo match { case Left(waitingForRevocation) if remoteChannelReestablish.nextLocalCommitmentNumber == commitments.nextRemoteCommitIndex => - // we just sent a new commit_sig but they didn't receive it - // we resend the same updates and the same sig, and preserve the same ordering + // We just sent a new commit_sig but they didn't receive it: we resend the same updates and sign them again, + // and preserve the same ordering of messages. val signedUpdates = commitments.changes.localChanges.signed - val commitSigs = commitments.active.flatMap(_.nextRemoteCommit_opt).map(_.sig) + val channelParams = commitments.channelParams + val batchSize = commitments.active.size + val commitSigs = CommitSigs(commitments.active.flatMap(c => { + val commitInput = c.commitInput(c.localFundingKey(channelKeys)) + val remoteCommitNonce_opt = remoteChannelReestablish.nextCommitNonces.get(c.fundingTxId) + // Note that we ignore errors and simply skip failures to sign: we've already signed those updates before + // the disconnection, so we don't expect any error here unless our peer sends an invalid nonce. In that + // case, we simply won't send back our commit_sig until they fix their node. + c.nextRemoteCommit_opt.flatMap(_.sign(channelParams, c.remoteCommitParams, channelKeys, c.fundingTxIndex, c.remoteFundingPubKey, commitInput, c.commitmentFormat, remoteCommitNonce_opt, batchSize).toOption) + })) retransmitRevocation_opt match { case None => - SyncResult.Success(retransmit = signedUpdates ++ commitSigs) + SyncResult.Success(retransmit = signedUpdates :+ commitSigs) case Some(revocation) if commitments.localCommitIndex > waitingForRevocation.sentAfterLocalCommitIndex => - SyncResult.Success(retransmit = signedUpdates ++ commitSigs ++ Seq(revocation)) + SyncResult.Success(retransmit = signedUpdates :+ commitSigs :+ revocation) case Some(revocation) => - SyncResult.Success(retransmit = Seq(revocation) ++ signedUpdates ++ commitSigs) + SyncResult.Success(retransmit = revocation +: signedUpdates :+ commitSigs) } case Left(_) if remoteChannelReestablish.nextLocalCommitmentNumber == (commitments.nextRemoteCommitIndex + 1) => // we just sent a new commit_sig, they have received it but we haven't received their revocation @@ -528,13 +550,20 @@ object Helpers { checkRemoteCommit(remoteChannelReestablish, retransmitRevocation_opt = None) } else if (commitments.localCommitIndex == remoteChannelReestablish.nextRemoteRevocationNumber + 1) { // they just sent a new commit_sig, we have received it but they didn't receive our revocation - val channelKeyPath = keyManager.keyPath(commitments.params.localParams, commitments.params.channelConfig) - val localPerCommitmentSecret = keyManager.commitmentSecret(channelKeyPath, commitments.localCommitIndex - 1) - val localNextPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, commitments.localCommitIndex + 1) + val localPerCommitmentSecret = channelKeys.commitmentSecret(commitments.localCommitIndex - 1) + val localNextPerCommitmentPoint = channelKeys.commitmentPoint(commitments.localCommitIndex + 1) + val localCommitNonces = commitments.active.flatMap(c => c.commitmentFormat match { + case _: SegwitV0CommitmentFormat => None + case _: SimpleTaprootChannelCommitmentFormat => + val fundingKey = channelKeys.fundingKey(c.fundingTxIndex) + val n = NonceGenerator.verificationNonce(c.fundingTxId, fundingKey, c.remoteFundingPubKey, commitments.localCommitIndex + 1).publicNonce + Some(c.fundingTxId -> n) + }) val revocation = RevokeAndAck( channelId = commitments.channelId, perCommitmentSecret = localPerCommitmentSecret, - nextPerCommitmentPoint = localNextPerCommitmentPoint + nextPerCommitmentPoint = localNextPerCommitmentPoint, + nextCommitNonces = localCommitNonces, ) checkRemoteCommit(remoteChannelReestablish, retransmitRevocation_opt = Some(revocation)) } else if (commitments.localCommitIndex > remoteChannelReestablish.nextRemoteRevocationNumber + 1) { @@ -542,8 +571,7 @@ object Helpers { } else { // if next_remote_revocation_number is greater than our local commitment index, it means that either we are using an outdated commitment, or they are lying // but first we need to make sure that the last per_commitment_secret that they claim to have received from us is correct for that next_remote_revocation_number minus 1 - val channelKeyPath = keyManager.keyPath(commitments.params.localParams, commitments.params.channelConfig) - if (keyManager.commitmentSecret(channelKeyPath, remoteChannelReestablish.nextRemoteRevocationNumber - 1) == remoteChannelReestablish.yourLastPerCommitmentSecret) { + if (channelKeys.commitmentSecret(remoteChannelReestablish.nextRemoteRevocationNumber - 1) == remoteChannelReestablish.yourLastPerCommitmentSecret) { SyncResult.LocalLateProven( ourLocalCommitmentNumber = commitments.localCommitIndex, theirRemoteCommitmentNumber = remoteChannelReestablish.nextRemoteRevocationNumber @@ -558,19 +586,31 @@ object Helpers { } } } + + def checkCommitNonces(channelReestablish: ChannelReestablish, commitments: Commitments, pendingSig_opt: Option[InteractiveTxSigningSession.WaitingForSigs]): Option[ChannelException] = { + pendingSig_opt match { + case Some(pendingSig) if pendingSig.fundingParams.commitmentFormat.isInstanceOf[TaprootCommitmentFormat] && !channelReestablish.nextCommitNonces.contains(pendingSig.fundingTxId) => + Some(MissingCommitNonce(commitments.channelId, pendingSig.fundingTxId, commitments.remoteCommitIndex + 1)) + case _ => + commitments.active + .find(c => c.commitmentFormat.isInstanceOf[TaprootCommitmentFormat] && !channelReestablish.nextCommitNonces.contains(c.fundingTxId)) + .map(c => MissingCommitNonce(commitments.channelId, c.fundingTxId, commitments.remoteCommitIndex + 1)) + } + } + } object Closing { // @formatter:off sealed trait ClosingType - case class MutualClose(tx: ClosingTx) extends ClosingType - case class LocalClose(localCommit: LocalCommit, localCommitPublished: LocalCommitPublished) extends ClosingType + case class MutualClose(tx: ClosingTx) extends ClosingType { override def toString: String = "mutual-close" } + case class LocalClose(localCommit: LocalCommit, localCommitPublished: LocalCommitPublished) extends ClosingType { override def toString: String = "local-close" } sealed trait RemoteClose extends ClosingType { def remoteCommit: RemoteCommit; def remoteCommitPublished: RemoteCommitPublished } - case class CurrentRemoteClose(remoteCommit: RemoteCommit, remoteCommitPublished: RemoteCommitPublished) extends RemoteClose - case class NextRemoteClose(remoteCommit: RemoteCommit, remoteCommitPublished: RemoteCommitPublished) extends RemoteClose - case class RecoveryClose(remoteCommitPublished: RemoteCommitPublished) extends ClosingType - case class RevokedClose(revokedCommitPublished: RevokedCommitPublished) extends ClosingType + case class CurrentRemoteClose(remoteCommit: RemoteCommit, remoteCommitPublished: RemoteCommitPublished) extends RemoteClose { override def toString: String = "remote-close" } + case class NextRemoteClose(remoteCommit: RemoteCommit, remoteCommitPublished: RemoteCommitPublished) extends RemoteClose { override def toString: String = "next-remote-close" } + case class RecoveryClose(remoteCommitPublished: RemoteCommitPublished) extends ClosingType { override def toString: String = "recovery-close" } + case class RevokedClose(revokedCommitPublished: RevokedCommitPublished) extends ClosingType { override def toString: String = "revoked-close" } // @formatter:on /** @@ -607,7 +647,7 @@ object Helpers { case _ if closing.remoteCommitPublished.exists(_.isConfirmed) => Some(CurrentRemoteClose(closing.commitments.latest.remoteCommit, closing.remoteCommitPublished.get)) case _ if closing.nextRemoteCommitPublished.exists(_.isConfirmed) => - Some(NextRemoteClose(closing.commitments.latest.nextRemoteCommit_opt.get.commit, closing.nextRemoteCommitPublished.get)) + Some(NextRemoteClose(closing.commitments.latest.nextRemoteCommit_opt.get, closing.nextRemoteCommitPublished.get)) case _ if closing.futureRemoteCommitPublished.exists(_.isConfirmed) => Some(RecoveryClose(closing.futureRemoteCommitPublished.get)) case _ if closing.revokedCommitPublished.exists(_.isConfirmed) => @@ -633,7 +673,7 @@ object Helpers { case closing: DATA_CLOSING if closing.remoteCommitPublished.exists(_.isDone) => Some(CurrentRemoteClose(closing.commitments.latest.remoteCommit, closing.remoteCommitPublished.get)) case closing: DATA_CLOSING if closing.nextRemoteCommitPublished.exists(_.isDone) => - Some(NextRemoteClose(closing.commitments.latest.nextRemoteCommit_opt.get.commit, closing.nextRemoteCommitPublished.get)) + Some(NextRemoteClose(closing.commitments.latest.nextRemoteCommit_opt.get, closing.nextRemoteCommitPublished.get)) case closing: DATA_CLOSING if closing.futureRemoteCommitPublished.exists(_.isDone) => Some(RecoveryClose(closing.futureRemoteCommitPublished.get)) case closing: DATA_CLOSING if closing.revokedCommitPublished.exists(_.isDone) => @@ -643,14 +683,8 @@ object Helpers { object MutualClose { - def generateFinalScriptPubKey(wallet: OnChainPubkeyCache, allowAnySegwit: Boolean, renew: Boolean = true): ByteVector = { - if (!allowAnySegwit) { - // If our peer only supports segwit v0, we cannot let bitcoind choose the address type: we always use p2wpkh. - val finalPubKey = wallet.getP2wpkhPubkey(renew) - Script.write(Script.pay2wpkh(finalPubKey)) - } else { - Script.write(wallet.getReceivePublicKeyScript(renew)) - } + def generateFinalScriptPubKey(wallet: OnChainAddressCache, renew: Boolean = true): ByteVector = { + Script.write(wallet.getReceivePublicKeyScript(renew)) } def isValidFinalScriptPubkey(scriptPubKey: ByteVector, allowAnySegwit: Boolean, allowOpReturn: Boolean): Boolean = { @@ -665,56 +699,54 @@ object Helpers { } } - def firstClosingFee(commitment: FullCommitment, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector, feerates: ClosingFeerates)(implicit log: LoggingAdapter): ClosingFees = { + def firstClosingFee(channelKeys: ChannelKeys, commitment: FullCommitment, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector, feerates: ClosingFeerates)(implicit log: LoggingAdapter): ClosingFees = { // this is just to estimate the weight, it depends on size of the pubkey scripts - val dummyClosingTx = Transactions.makeClosingTx(commitment.commitInput, localScriptPubkey, remoteScriptPubkey, commitment.localParams.paysClosingFees, Satoshi(0), Satoshi(0), commitment.localCommit.spec) - val closingWeight = Transaction.weight(Transactions.addSigs(dummyClosingTx, Transactions.PlaceHolderPubKey, commitment.remoteFundingPubKey, Transactions.PlaceHolderSig, Transactions.PlaceHolderSig).tx) + val dummyClosingTx = ClosingTx.createUnsignedTx(commitment.commitInput(channelKeys), localScriptPubkey, remoteScriptPubkey, commitment.localChannelParams.paysClosingFees, 0 sat, 0 sat, commitment.localCommit.spec) + val dummyPubkey = commitment.remoteFundingPubKey + val dummySig = IndividualSignature(Transactions.PlaceHolderSig) + val closingWeight = dummyClosingTx.aggregateSigs(dummyPubkey, dummyPubkey, dummySig, dummySig).weight() log.info(s"using feerates=$feerates for initial closing tx") feerates.computeFees(closingWeight) } - def firstClosingFee(commitment: FullCommitment, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector, feerates: FeeratesPerKw, onChainFeeConf: OnChainFeeConf)(implicit log: LoggingAdapter): ClosingFees = { - val requestedFeerate = onChainFeeConf.getClosingFeerate(feerates) - val preferredFeerate = commitment.params.commitmentFormat match { - case DefaultCommitmentFormat => - // we "MUST set fee_satoshis less than or equal to the base fee of the final commitment transaction" - requestedFeerate.min(commitment.localCommit.spec.commitTxFeerate) - case _: AnchorOutputsCommitmentFormat => requestedFeerate - } + def firstClosingFee(channelKeys: ChannelKeys, commitment: FullCommitment, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector, feerates: FeeratesPerKw, onChainFeeConf: OnChainFeeConf)(implicit log: LoggingAdapter): ClosingFees = { + val requestedFeerate = onChainFeeConf.getClosingFeerate(feerates, maxClosingFeerateOverride_opt = None) // NB: we choose a minimum fee that ensures the tx will easily propagate while allowing low fees since we can // always use CPFP to speed up confirmation if necessary. - val closingFeerates = ClosingFeerates(preferredFeerate, preferredFeerate.min(ConfirmationPriority.Slow.getFeerate(feerates)), preferredFeerate * 2) - firstClosingFee(commitment, localScriptPubkey, remoteScriptPubkey, closingFeerates) + val closingFeerates = ClosingFeerates(requestedFeerate, requestedFeerate.min(ConfirmationPriority.Slow.getFeerate(feerates)), requestedFeerate * 2) + firstClosingFee(channelKeys, commitment, localScriptPubkey, remoteScriptPubkey, closingFeerates) } def nextClosingFee(localClosingFee: Satoshi, remoteClosingFee: Satoshi): Satoshi = ((localClosingFee + remoteClosingFee) / 4) * 2 - def makeFirstClosingTx(keyManager: ChannelKeyManager, commitment: FullCommitment, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector, feerates: FeeratesPerKw, onChainFeeConf: OnChainFeeConf, closingFeerates_opt: Option[ClosingFeerates])(implicit log: LoggingAdapter): (ClosingTx, ClosingSigned) = { + def makeFirstClosingTx(channelKeys: ChannelKeys, commitment: FullCommitment, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector, feerates: FeeratesPerKw, onChainFeeConf: OnChainFeeConf, closingFeerates_opt: Option[ClosingFeerates])(implicit log: LoggingAdapter): (ClosingTx, ClosingSigned) = { val closingFees = closingFeerates_opt match { - case Some(closingFeerates) => firstClosingFee(commitment, localScriptPubkey, remoteScriptPubkey, closingFeerates) - case None => firstClosingFee(commitment, localScriptPubkey, remoteScriptPubkey, feerates, onChainFeeConf) + case Some(closingFeerates) => firstClosingFee(channelKeys, commitment, localScriptPubkey, remoteScriptPubkey, closingFeerates) + case None => firstClosingFee(channelKeys, commitment, localScriptPubkey, remoteScriptPubkey, feerates, onChainFeeConf) } - makeClosingTx(keyManager, commitment, localScriptPubkey, remoteScriptPubkey, closingFees) + makeClosingTx(channelKeys, commitment, localScriptPubkey, remoteScriptPubkey, closingFees) } - def makeClosingTx(keyManager: ChannelKeyManager, commitment: FullCommitment, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector, closingFees: ClosingFees)(implicit log: LoggingAdapter): (ClosingTx, ClosingSigned) = { + def makeClosingTx(channelKeys: ChannelKeys, commitment: FullCommitment, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector, closingFees: ClosingFees)(implicit log: LoggingAdapter): (ClosingTx, ClosingSigned) = { log.debug("making closing tx with closing fee={} and commitments:\n{}", closingFees.preferred, commitment.specs2String) - val dustLimit = commitment.localParams.dustLimit.max(commitment.remoteParams.dustLimit) - val closingTx = Transactions.makeClosingTx(commitment.commitInput, localScriptPubkey, remoteScriptPubkey, commitment.localParams.paysClosingFees, dustLimit, closingFees.preferred, commitment.localCommit.spec) - val localClosingSig = keyManager.sign(closingTx, keyManager.fundingPublicKey(commitment.localParams.fundingKeyPath, commitment.fundingTxIndex), TxOwner.Local, commitment.params.commitmentFormat, Map.empty) + val dustLimit = commitment.localCommitParams.dustLimit.max(commitment.remoteCommitParams.dustLimit) + val closingTx = ClosingTx.createUnsignedTx(commitment.commitInput(channelKeys), localScriptPubkey, remoteScriptPubkey, commitment.localChannelParams.paysClosingFees, dustLimit, closingFees.preferred, commitment.localCommit.spec) + val localClosingSig = closingTx.sign(channelKeys.fundingKey(commitment.fundingTxIndex), commitment.remoteFundingPubKey).sig val closingSigned = ClosingSigned(commitment.channelId, closingFees.preferred, localClosingSig, TlvStream(ClosingSignedTlv.FeeRange(closingFees.min, closingFees.max))) log.debug(s"signed closing txid=${closingTx.tx.txid} with closing fee=${closingSigned.feeSatoshis}") log.debug(s"closingTxid=${closingTx.tx.txid} closingTx=${closingTx.tx}}") (closingTx, closingSigned) } - def checkClosingSignature(keyManager: ChannelKeyManager, commitment: FullCommitment, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector, remoteClosingFee: Satoshi, remoteClosingSig: ByteVector64)(implicit log: LoggingAdapter): Either[ChannelException, (ClosingTx, ClosingSigned)] = { - val (closingTx, closingSigned) = makeClosingTx(keyManager, commitment, localScriptPubkey, remoteScriptPubkey, ClosingFees(remoteClosingFee, remoteClosingFee, remoteClosingFee)) + def checkClosingSignature(channelKeys: ChannelKeys, commitment: FullCommitment, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector, remoteClosingFee: Satoshi, remoteClosingSig: ByteVector64)(implicit log: LoggingAdapter): Either[ChannelException, (ClosingTx, ClosingSigned)] = { + val (closingTx, closingSigned) = makeClosingTx(channelKeys, commitment, localScriptPubkey, remoteScriptPubkey, ClosingFees(remoteClosingFee, remoteClosingFee, remoteClosingFee)) if (checkClosingDustAmounts(closingTx)) { - val signedClosingTx = Transactions.addSigs(closingTx, keyManager.fundingPublicKey(commitment.localParams.fundingKeyPath, commitment.fundingTxIndex).publicKey, commitment.remoteFundingPubKey, closingSigned.signature, remoteClosingSig) - Transactions.checkSpendable(signedClosingTx) match { - case Success(_) => Right(signedClosingTx, closingSigned) - case _ => Left(InvalidCloseSignature(commitment.channelId, signedClosingTx.tx.txid)) + val fundingPubkey = channelKeys.fundingKey(commitment.fundingTxIndex).publicKey + if (closingTx.checkRemoteSig(fundingPubkey, commitment.remoteFundingPubKey, IndividualSignature(remoteClosingSig))) { + val signedTx = closingTx.aggregateSigs(fundingPubkey, commitment.remoteFundingPubKey, IndividualSignature(closingSigned.signature), IndividualSignature(remoteClosingSig)) + Right(closingTx.copy(tx = signedTx), closingSigned) + } else { + Left(InvalidCloseSignature(commitment.channelId, closingTx.tx.txid)) } } else { Left(InvalidCloseAmountBelowDust(commitment.channelId, closingTx.tx.txid)) @@ -722,30 +754,60 @@ object Helpers { } /** We are the closer: we sign closing transactions for which we pay the fees. */ - def makeSimpleClosingTx(currentBlockHeight: BlockHeight, keyManager: ChannelKeyManager, commitment: FullCommitment, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector, feerate: FeeratePerKw): Either[ChannelException, (ClosingTxs, ClosingComplete)] = { + def makeSimpleClosingTx(currentBlockHeight: BlockHeight, channelKeys: ChannelKeys, commitment: FullCommitment, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector, feerate: FeeratePerKw, remoteNonce_opt: Option[IndividualNonce]): Either[ChannelException, (ClosingTxs, ClosingComplete, CloserNonces)] = { // We must convert the feerate to a fee: we must build dummy transactions to compute their weight. + val commitInput = commitment.commitInput(channelKeys) val closingFee = { - val dummyClosingTxs = Transactions.makeSimpleClosingTxs(commitment.commitInput, commitment.localCommit.spec, SimpleClosingTxFee.PaidByUs(0 sat), currentBlockHeight.toLong, localScriptPubkey, remoteScriptPubkey) + val dummyClosingTxs = Transactions.makeSimpleClosingTxs(commitInput, commitment.localCommit.spec, SimpleClosingTxFee.PaidByUs(0 sat), currentBlockHeight.toLong, localScriptPubkey, remoteScriptPubkey) dummyClosingTxs.preferred_opt match { case Some(dummyTx) => - val dummySignedTx = Transactions.addSigs(dummyTx, Transactions.PlaceHolderPubKey, Transactions.PlaceHolderPubKey, Transactions.PlaceHolderSig, Transactions.PlaceHolderSig) - SimpleClosingTxFee.PaidByUs(Transactions.weight2fee(feerate, dummySignedTx.tx.weight())) + commitment.commitmentFormat match { + case _: AnchorOutputsCommitmentFormat => + val dummyPubkey = commitment.remoteFundingPubKey + val dummySig = IndividualSignature(Transactions.PlaceHolderSig) + val dummySignedTx = dummyTx.aggregateSigs(dummyPubkey, dummyPubkey, dummySig, dummySig) + SimpleClosingTxFee.PaidByUs(Transactions.weight2fee(feerate, dummySignedTx.weight())) + case _: SimpleTaprootChannelCommitmentFormat => + val dummySignedTx = dummyTx.tx.updateWitness(dummyTx.inputIndex, Script.witnessKeyPathPay2tr(Transactions.PlaceHolderSig)) + SimpleClosingTxFee.PaidByUs(Transactions.weight2fee(feerate, dummySignedTx.weight())) + } case None => return Left(CannotGenerateClosingTx(commitment.channelId)) } } // Now that we know the fee we're ready to pay, we can create our closing transactions. - val closingTxs = Transactions.makeSimpleClosingTxs(commitment.commitInput, commitment.localCommit.spec, closingFee, currentBlockHeight.toLong, localScriptPubkey, remoteScriptPubkey) + val closingTxs = Transactions.makeSimpleClosingTxs(commitInput, commitment.localCommit.spec, closingFee, currentBlockHeight.toLong, localScriptPubkey, remoteScriptPubkey) closingTxs.preferred_opt match { case Some(closingTx) if closingTx.fee > 0.sat => () case _ => return Left(CannotGenerateClosingTx(commitment.channelId)) } - val localFundingPubKey = keyManager.fundingPublicKey(commitment.localParams.fundingKeyPath, commitment.fundingTxIndex) - val closingComplete = ClosingComplete(commitment.channelId, localScriptPubkey, remoteScriptPubkey, closingFee.fee, currentBlockHeight.toLong, TlvStream(Set( - closingTxs.localAndRemote_opt.map(tx => ClosingTlv.CloserAndCloseeOutputs(keyManager.sign(tx, localFundingPubKey, TxOwner.Local, commitment.params.commitmentFormat, Map.empty))), - closingTxs.localOnly_opt.map(tx => ClosingTlv.CloserOutputOnly(keyManager.sign(tx, localFundingPubKey, TxOwner.Local, commitment.params.commitmentFormat, Map.empty))), - closingTxs.remoteOnly_opt.map(tx => ClosingTlv.CloseeOutputOnly(keyManager.sign(tx, localFundingPubKey, TxOwner.Local, commitment.params.commitmentFormat, Map.empty))), - ).flatten[ClosingTlv])) - Right(closingTxs, closingComplete) + val localFundingKey = channelKeys.fundingKey(commitment.fundingTxIndex) + val localNonces = CloserNonces.generate(localFundingKey.publicKey, commitment.remoteFundingPubKey, commitment.fundingTxId) + val tlvs: TlvStream[ClosingCompleteTlv] = commitment.commitmentFormat match { + case _: SimpleTaprootChannelCommitmentFormat => + remoteNonce_opt match { + case None => return Left(MissingClosingNonce(commitment.channelId)) + case Some(remoteNonce) => + // If we cannot create our partial signature for one of our closing txs, we just skip it. + // It will only happen if our peer sent an invalid nonce, in which case we cannot do anything anyway + // apart from eventually force-closing. + def localSig(tx: ClosingTx, localNonce: LocalNonce): Option[PartialSignatureWithNonce] = { + tx.partialSign(localFundingKey, commitment.remoteFundingPubKey, localNonce, Seq(localNonce.publicNonce, remoteNonce)).toOption + } + + TlvStream(Set( + closingTxs.localAndRemote_opt.flatMap(tx => localSig(tx, localNonces.localAndRemote)).map(ClosingCompleteTlv.CloserAndCloseeOutputsPartialSignature(_)), + closingTxs.localOnly_opt.flatMap(tx => localSig(tx, localNonces.localOnly)).map(ClosingCompleteTlv.CloserOutputOnlyPartialSignature(_)), + closingTxs.remoteOnly_opt.flatMap(tx => localSig(tx, localNonces.remoteOnly)).map(ClosingCompleteTlv.CloseeOutputOnlyPartialSignature(_)), + ).flatten[ClosingCompleteTlv]) + } + case _: AnchorOutputsCommitmentFormat => TlvStream(Set( + closingTxs.localAndRemote_opt.map(tx => ClosingTlv.CloserAndCloseeOutputs(tx.sign(localFundingKey, commitment.remoteFundingPubKey).sig)), + closingTxs.localOnly_opt.map(tx => ClosingTlv.CloserOutputOnly(tx.sign(localFundingKey, commitment.remoteFundingPubKey).sig)), + closingTxs.remoteOnly_opt.map(tx => ClosingTlv.CloseeOutputOnly(tx.sign(localFundingKey, commitment.remoteFundingPubKey).sig)), + ).flatten[ClosingCompleteTlv]) + } + val closingComplete = ClosingComplete(commitment.channelId, localScriptPubkey, remoteScriptPubkey, closingFee.fee, currentBlockHeight.toLong, tlvs) + Right(closingTxs, closingComplete, localNonces) } /** @@ -754,33 +816,70 @@ object Helpers { * Callers should ignore failures: since the protocol is fully asynchronous, failures here simply mean that they * are not using our latest script (race condition between our closing_complete and theirs). */ - def signSimpleClosingTx(keyManager: ChannelKeyManager, commitment: FullCommitment, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector, closingComplete: ClosingComplete): Either[ChannelException, (ClosingTx, ClosingSig)] = { + def signSimpleClosingTx(channelKeys: ChannelKeys, commitment: FullCommitment, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector, closingComplete: ClosingComplete, localNonce_opt: Option[LocalNonce]): Either[ChannelException, (ClosingTx, ClosingSig, Option[LocalNonce])] = { val closingFee = SimpleClosingTxFee.PaidByThem(closingComplete.fees) - val closingTxs = Transactions.makeSimpleClosingTxs(commitment.commitInput, commitment.localCommit.spec, closingFee, closingComplete.lockTime, localScriptPubkey, remoteScriptPubkey) + val closingTxs = Transactions.makeSimpleClosingTxs(commitment.commitInput(channelKeys), commitment.localCommit.spec, closingFee, closingComplete.lockTime, localScriptPubkey, remoteScriptPubkey) // If our output isn't dust, they must provide a signature for a transaction that includes it. // Note that we're the closee, so we look for signatures including the closee output. - (closingTxs.localAndRemote_opt, closingTxs.localOnly_opt) match { - case (Some(_), Some(_)) if closingComplete.closerAndCloseeOutputsSig_opt.isEmpty && closingComplete.closeeOutputOnlySig_opt.isEmpty => return Left(MissingCloseSignature(commitment.channelId)) - case (Some(_), None) if closingComplete.closerAndCloseeOutputsSig_opt.isEmpty => return Left(MissingCloseSignature(commitment.channelId)) - case (None, Some(_)) if closingComplete.closeeOutputOnlySig_opt.isEmpty => return Left(MissingCloseSignature(commitment.channelId)) - case _ => () - } - // We choose the closing signature that matches our preferred closing transaction. - val closingTxsWithSigs = Seq( - closingComplete.closerAndCloseeOutputsSig_opt.flatMap(remoteSig => closingTxs.localAndRemote_opt.map(tx => (tx, remoteSig, localSig => ClosingTlv.CloserAndCloseeOutputs(localSig)))), - closingComplete.closeeOutputOnlySig_opt.flatMap(remoteSig => closingTxs.localOnly_opt.map(tx => (tx, remoteSig, localSig => ClosingTlv.CloseeOutputOnly(localSig)))), - closingComplete.closerOutputOnlySig_opt.flatMap(remoteSig => closingTxs.remoteOnly_opt.map(tx => (tx, remoteSig, localSig => ClosingTlv.CloserOutputOnly(localSig)))), - ).flatten - closingTxsWithSigs.headOption match { - case Some((closingTx, remoteSig, sigToTlv)) => - val localFundingPubKey = keyManager.fundingPublicKey(commitment.localParams.fundingKeyPath, commitment.fundingTxIndex) - val localSig = keyManager.sign(closingTx, localFundingPubKey, TxOwner.Local, commitment.params.commitmentFormat, Map.empty) - val signedClosingTx = Transactions.addSigs(closingTx, localFundingPubKey.publicKey, commitment.remoteFundingPubKey, localSig, remoteSig) - Transactions.checkSpendable(signedClosingTx) match { - case Failure(_) => Left(InvalidCloseSignature(commitment.channelId, signedClosingTx.tx.txid)) - case Success(_) => Right(signedClosingTx, ClosingSig(commitment.channelId, remoteScriptPubkey, localScriptPubkey, closingComplete.fees, closingComplete.lockTime, TlvStream(sigToTlv(localSig)))) + commitment.commitmentFormat match { + case _: SimpleTaprootChannelCommitmentFormat => localNonce_opt match { + case None => Left(MissingClosingNonce(commitment.channelId)) + case Some(localNonce) => + (closingTxs.localAndRemote_opt, closingTxs.localOnly_opt) match { + case (Some(_), Some(_)) if closingComplete.closerAndCloseeOutputsPartialSig_opt.isEmpty && closingComplete.closeeOutputOnlyPartialSig_opt.isEmpty => return Left(MissingCloseSignature(commitment.channelId)) + case (Some(_), None) if closingComplete.closerAndCloseeOutputsPartialSig_opt.isEmpty => return Left(MissingCloseSignature(commitment.channelId)) + case (None, Some(_)) if closingComplete.closeeOutputOnlyPartialSig_opt.isEmpty => return Left(MissingCloseSignature(commitment.channelId)) + case _ => () + } + // We choose the closing signature that matches our preferred closing transaction. + val closingTxsWithSigs = Seq( + closingComplete.closerAndCloseeOutputsPartialSig_opt.flatMap(remoteSig => closingTxs.localAndRemote_opt.map(tx => (tx, remoteSig, localSig => ClosingSigTlv.CloserAndCloseeOutputsPartialSignature(localSig)))), + closingComplete.closeeOutputOnlyPartialSig_opt.flatMap(remoteSig => closingTxs.localOnly_opt.map(tx => (tx, remoteSig, localSig => ClosingSigTlv.CloseeOutputOnlyPartialSignature(localSig)))), + closingComplete.closerOutputOnlyPartialSig_opt.flatMap(remoteSig => closingTxs.remoteOnly_opt.map(tx => (tx, remoteSig, localSig => ClosingSigTlv.CloserOutputOnlyPartialSignature(localSig)))), + ).flatten + closingTxsWithSigs.headOption match { + case Some((closingTx, remoteSig, sigToTlv)) => + val localFundingKey = channelKeys.fundingKey(commitment.fundingTxIndex) + val signedClosingTx_opt = for { + localSig <- closingTx.partialSign(localFundingKey, commitment.remoteFundingPubKey, localNonce, Seq(localNonce.publicNonce, remoteSig.nonce)).toOption + signedTx <- closingTx.aggregateSigs(localFundingKey.publicKey, commitment.remoteFundingPubKey, localSig, remoteSig).toOption + } yield (closingTx.copy(tx = signedTx), localSig.partialSig) + signedClosingTx_opt match { + case Some((signedClosingTx, localSig)) if signedClosingTx.validate(extraUtxos = Map.empty) => + val nextLocalNonce = NonceGenerator.signingNonce(localFundingKey.publicKey, commitment.remoteFundingPubKey, commitment.fundingTxId) + val tlvs = TlvStream[ClosingSigTlv](sigToTlv(localSig), ClosingSigTlv.NextCloseeNonce(nextLocalNonce.publicNonce)) + Right(signedClosingTx, ClosingSig(commitment.channelId, remoteScriptPubkey, localScriptPubkey, closingComplete.fees, closingComplete.lockTime, tlvs), Some(nextLocalNonce)) + case _ => Left(InvalidCloseSignature(commitment.channelId, closingTx.tx.txid)) + } + case None => Left(MissingCloseSignature(commitment.channelId)) + } + } + case _: AnchorOutputsCommitmentFormat => + (closingTxs.localAndRemote_opt, closingTxs.localOnly_opt) match { + case (Some(_), Some(_)) if closingComplete.closerAndCloseeOutputsSig_opt.isEmpty && closingComplete.closeeOutputOnlySig_opt.isEmpty => return Left(MissingCloseSignature(commitment.channelId)) + case (Some(_), None) if closingComplete.closerAndCloseeOutputsSig_opt.isEmpty => return Left(MissingCloseSignature(commitment.channelId)) + case (None, Some(_)) if closingComplete.closeeOutputOnlySig_opt.isEmpty => return Left(MissingCloseSignature(commitment.channelId)) + case _ => () + } + // We choose the closing signature that matches our preferred closing transaction. + val closingTxsWithSigs = Seq( + closingComplete.closerAndCloseeOutputsSig_opt.flatMap(remoteSig => closingTxs.localAndRemote_opt.map(tx => (tx, remoteSig, localSig => ClosingTlv.CloserAndCloseeOutputs(localSig)))), + closingComplete.closeeOutputOnlySig_opt.flatMap(remoteSig => closingTxs.localOnly_opt.map(tx => (tx, remoteSig, localSig => ClosingTlv.CloseeOutputOnly(localSig)))), + closingComplete.closerOutputOnlySig_opt.flatMap(remoteSig => closingTxs.remoteOnly_opt.map(tx => (tx, remoteSig, localSig => ClosingTlv.CloserOutputOnly(localSig)))), + ).flatten + closingTxsWithSigs.headOption match { + case Some((closingTx, remoteSig, sigToTlv)) => + val localFundingKey = channelKeys.fundingKey(commitment.fundingTxIndex) + val localSig = closingTx.sign(localFundingKey, commitment.remoteFundingPubKey) + val signedTx = closingTx.aggregateSigs(localFundingKey.publicKey, commitment.remoteFundingPubKey, localSig, IndividualSignature(remoteSig)) + val signedClosingTx = closingTx.copy(tx = signedTx) + if (signedClosingTx.validate(extraUtxos = Map.empty)) { + Right(signedClosingTx, ClosingSig(commitment.channelId, remoteScriptPubkey, localScriptPubkey, closingComplete.fees, closingComplete.lockTime, TlvStream(sigToTlv(localSig.sig))), None) + } else { + Left(InvalidCloseSignature(commitment.channelId, signedClosingTx.tx.txid)) + } + case None => Left(MissingCloseSignature(commitment.channelId)) } - case None => Left(MissingCloseSignature(commitment.channelId)) } } @@ -791,20 +890,38 @@ object Helpers { * sent another closing_complete before receiving their closing_sig, which is now obsolete: we ignore it and wait * for their next closing_sig that will match our latest closing_complete. */ - def receiveSimpleClosingSig(keyManager: ChannelKeyManager, commitment: FullCommitment, closingTxs: ClosingTxs, closingSig: ClosingSig): Either[ChannelException, ClosingTx] = { + def receiveSimpleClosingSig(channelKeys: ChannelKeys, commitment: FullCommitment, closingTxs: ClosingTxs, closingSig: ClosingSig, localNonces_opt: Option[CloserNonces], remoteNonce_opt: Option[IndividualNonce]): Either[ChannelException, ClosingTx] = { val closingTxsWithSig = Seq( - closingSig.closerAndCloseeOutputsSig_opt.flatMap(sig => closingTxs.localAndRemote_opt.map(tx => (tx, sig))), - closingSig.closerOutputOnlySig_opt.flatMap(sig => closingTxs.localOnly_opt.map(tx => (tx, sig))), - closingSig.closeeOutputOnlySig_opt.flatMap(sig => closingTxs.remoteOnly_opt.map(tx => (tx, sig))), + closingSig.closerAndCloseeOutputsSig_opt.flatMap(sig => closingTxs.localAndRemote_opt.map(tx => (tx, IndividualSignature(sig)))), + closingSig.closerAndCloseeOutputsPartialSig_opt.flatMap(sig => remoteNonce_opt.flatMap(nonce => closingTxs.localAndRemote_opt.map(tx => (tx, PartialSignatureWithNonce(sig, nonce))))), + closingSig.closerOutputOnlySig_opt.flatMap(sig => closingTxs.localOnly_opt.map(tx => (tx, IndividualSignature(sig)))), + closingSig.closerOutputOnlyPartialSig_opt.flatMap(sig => remoteNonce_opt.flatMap(nonce => closingTxs.localOnly_opt.map(tx => (tx, PartialSignatureWithNonce(sig, nonce))))), + closingSig.closeeOutputOnlySig_opt.flatMap(sig => closingTxs.remoteOnly_opt.map(tx => (tx, IndividualSignature(sig)))), + closingSig.closeeOutputOnlyPartialSig_opt.flatMap(sig => remoteNonce_opt.flatMap(nonce => closingTxs.remoteOnly_opt.map(tx => (tx, PartialSignatureWithNonce(sig, nonce))))) ).flatten closingTxsWithSig.headOption match { case Some((closingTx, remoteSig)) => - val localFundingPubKey = keyManager.fundingPublicKey(commitment.localParams.fundingKeyPath, commitment.fundingTxIndex) - val localSig = keyManager.sign(closingTx, localFundingPubKey, TxOwner.Local, commitment.params.commitmentFormat, Map.empty) - val signedClosingTx = Transactions.addSigs(closingTx, localFundingPubKey.publicKey, commitment.remoteFundingPubKey, localSig, remoteSig) - Transactions.checkSpendable(signedClosingTx) match { - case Failure(_) => Left(InvalidCloseSignature(commitment.channelId, signedClosingTx.tx.txid)) - case Success(_) => Right(signedClosingTx) + val localFundingKey = channelKeys.fundingKey(commitment.fundingTxIndex) + val signedClosingTx_opt = remoteSig match { + case remoteSig: IndividualSignature => + val localSig = closingTx.sign(localFundingKey, commitment.remoteFundingPubKey) + val signedTx = closingTx.aggregateSigs(localFundingKey.publicKey, commitment.remoteFundingPubKey, localSig, remoteSig) + Some(closingTx.copy(tx = signedTx)) + case remoteSig: PartialSignatureWithNonce => + val localNonce = localNonces_opt match { + case Some(localNonces) if closingTx.tx.txOut.size == 2 => localNonces.localAndRemote + case Some(localNonces) if closingTx.toLocalOutput_opt.nonEmpty => localNonces.localOnly + case Some(localNonces) => localNonces.remoteOnly + case None => return Left(InvalidCloseSignature(commitment.channelId, closingTx.tx.txid)) + } + for { + localSig <- closingTx.partialSign(localFundingKey, commitment.remoteFundingPubKey, localNonce, Seq(localNonce.publicNonce, remoteSig.nonce)).toOption + signedTx <- closingTx.aggregateSigs(localFundingKey.publicKey, commitment.remoteFundingPubKey, localSig, remoteSig).toOption + } yield closingTx.copy(tx = signedTx) + } + signedClosingTx_opt match { + case Some(signedClosingTx) if signedClosingTx.validate(extraUtxos = Map.empty) => Right(signedClosingTx) + case _ => Left(InvalidCloseSignature(commitment.channelId, closingTx.tx.txid)) } case None => Left(MissingCloseSignature(commitment.channelId)) } @@ -816,23 +933,24 @@ object Helpers { * The various dust limits are detailed in https://github.com/lightningnetwork/lightning-rfc/blob/master/03-transactions.md#dust-limits */ def checkClosingDustAmounts(closingTx: ClosingTx): Boolean = { - closingTx.tx.txOut.forall(txOut => txOut.amount >= Transactions.dustLimit(txOut.publicKeyScript)) + closingTx.tx.txOut.forall(txOut => txOut.amount >= Scripts.dustLimit(txOut.publicKeyScript)) } + } /** Wraps transaction generation in a Try and filters failures to avoid one transaction negatively impacting a whole commitment. */ - private def withTxGenerationLog[T <: TransactionWithInputInfo](desc: String, logSuccess: Boolean = true, logSkipped: Boolean = true, logFailure: Boolean = true)(generateTx: => Either[TxGenerationSkipped, T])(implicit log: LoggingAdapter): Option[T] = { + private def withTxGenerationLog[T <: ForceCloseTransaction](desc: String)(generateTx: => Either[TxGenerationSkipped, T])(implicit log: LoggingAdapter): Option[T] = { Try { generateTx } match { - case Success(Right(txinfo)) => - if (logSuccess) log.info(s"tx generation success: desc=$desc txid=${txinfo.tx.txid} amount=${txinfo.tx.txOut.map(_.amount).sum} tx=${txinfo.tx}") - Some(txinfo) + case Success(Right(txInfo)) => + log.info(s"tx generation success: desc=$desc txId=${txInfo.tx.txid} amount=${txInfo.amountIn} input=${txInfo.input.outPoint}") + Some(txInfo) case Success(Left(skipped)) => - if (logSkipped) log.info(s"tx generation skipped: desc=$desc reason: ${skipped.toString}") + log.info(s"tx generation skipped: desc=$desc reason: ${skipped.toString}") None case Failure(t) => - if (logFailure) log.warning(s"tx generation failure: desc=$desc reason: ${t.getMessage}") + log.warning(s"tx generation failure: desc=$desc reason: ${t.getMessage}") None } } @@ -844,112 +962,110 @@ object Helpers { if (localPaysCommitTxFees) commitInput.txOut.amount - commitTx.txOut.map(_.amount).sum else 0 sat } - /** - * This function checks if the proposed confirmation target is more aggressive than whatever confirmation target - * we previously had. Note that absolute targets are always considered more aggressive than relative targets. - */ - private def shouldUpdateAnchorTxs(anchorTxs: List[ClaimAnchorOutputTx], confirmationTarget: ConfirmationTarget): Boolean = { - anchorTxs - .collect { case tx: ClaimLocalAnchorOutputTx => tx.confirmationTarget } - .forall { - case ConfirmationTarget.Absolute(current) => confirmationTarget match { - case ConfirmationTarget.Absolute(proposed) => proposed < current - case _: ConfirmationTarget.Priority => false - } - case ConfirmationTarget.Priority(current) => confirmationTarget match { - case _: ConfirmationTarget.Absolute => true - case ConfirmationTarget.Priority(proposed) => current < proposed - } - } + /** Return the confirmation target that should be used for our local commitment. */ + def confirmationTarget(localCommit: LocalCommit, localDustLimit: Satoshi, commitmentFormat: CommitmentFormat, onChainFeeConf: OnChainFeeConf): ConfirmationTarget = { + confirmationTarget(localCommit.spec, localDustLimit, commitmentFormat, onChainFeeConf) + } + + /** Return the confirmation target that should be used for the given remote commitment. */ + def confirmationTarget(remoteCommit: RemoteCommit, remoteDustLimit: Satoshi, commitmentFormat: CommitmentFormat, onChainFeeConf: OnChainFeeConf): ConfirmationTarget = { + confirmationTarget(remoteCommit.spec, remoteDustLimit, commitmentFormat, onChainFeeConf) + } + + private def confirmationTarget(spec: CommitmentSpec, dustLimit: Satoshi, commitmentFormat: CommitmentFormat, onChainFeeConf: OnChainFeeConf): ConfirmationTarget = { + val offeredHtlcs = Transactions.trimOfferedHtlcs(dustLimit, spec, commitmentFormat).map(_.add) + val receivedHtlcs = Transactions.trimReceivedHtlcs(dustLimit, spec, commitmentFormat).map(_.add) + (offeredHtlcs ++ receivedHtlcs).map(_.cltvExpiry).minOption match { + // If there are pending HTLCs, we must get the commit tx confirmed before they timeout. + case Some(htlcExpiry) => ConfirmationTarget.Absolute(htlcExpiry.blockHeight) + // Otherwise, we don't have funds at risk, so we can aim for a slower confirmation. + case None => ConfirmationTarget.Priority(onChainFeeConf.feeTargets.closing) + } } + /** Return the default confirmation target for an HTLC transaction. */ + def confirmationTarget(htlcTx: SignedHtlcTx): ConfirmationTarget = ConfirmationTarget.Absolute(htlcTx.htlcExpiry.blockHeight) + + /** Return the default confirmation target for a Claim-HTLC transaction. */ + def confirmationTarget(claimHtlcTx: ClaimHtlcTx): ConfirmationTarget = ConfirmationTarget.Absolute(claimHtlcTx.htlcExpiry.blockHeight) + object LocalClose { - /** - * Claim all the HTLCs that we've received from our current commit tx. This will be done using 2nd stage HTLC transactions. - * - * @param commitment our commitment data, which includes payment preimages - * @return a list of transactions (one per output of the commit tx that we can claim) - */ - def claimCommitTxOutputs(keyManager: ChannelKeyManager, commitment: FullCommitment, tx: Transaction, feerates: FeeratesPerKw, onChainFeeConf: OnChainFeeConf, finalScriptPubKey: ByteVector)(implicit log: LoggingAdapter): LocalCommitPublished = { - require(commitment.localCommit.commitTxAndRemoteSig.commitTx.tx.txid == tx.txid, "txid mismatch, provided tx is not the current local commit tx") - val channelKeyPath = keyManager.keyPath(commitment.localParams, commitment.params.channelConfig) - val localPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, commitment.localCommit.index.toInt) - val localRevocationPubkey = Generators.revocationPubKey(commitment.remoteParams.revocationBasepoint, localPerCommitmentPoint) - val localDelayedPubkey = Generators.derivePubKey(keyManager.delayedPaymentPoint(channelKeyPath).publicKey, localPerCommitmentPoint) - val feeratePerKwDelayed = onChainFeeConf.getClosingFeerate(feerates) - - // first we will claim our main output as soon as the delay is over - val mainDelayedTx = withTxGenerationLog("local-main-delayed") { - Transactions.makeClaimLocalDelayedOutputTx(tx, commitment.localParams.dustLimit, localRevocationPubkey, commitment.remoteParams.toSelfDelay, localDelayedPubkey, finalScriptPubKey, feeratePerKwDelayed).map(claimDelayed => { - val sig = keyManager.sign(claimDelayed, keyManager.delayedPaymentPoint(channelKeyPath), localPerCommitmentPoint, TxOwner.Local, commitment.params.commitmentFormat, Map.empty) - Transactions.addSigs(claimDelayed, sig) - }) - } + /** Transactions spending outputs of our commitment transaction. */ + case class SecondStageTransactions(mainDelayedTx_opt: Option[ClaimLocalDelayedOutputTx], anchorTx_opt: Option[ClaimLocalAnchorTx], htlcTxs: Seq[SignedHtlcTx]) - val htlcTxs: Map[OutPoint, Option[HtlcTx]] = claimHtlcOutputs(keyManager, commitment) + /** Transactions spending outputs of our HTLC transactions. */ + case class ThirdStageTransactions(htlcDelayedTxs: Seq[HtlcDelayedTx]) + /** Claim all the outputs that belong to us in our local commitment transaction. */ + def claimCommitTxOutputs(channelKeys: ChannelKeys, commitment: FullCommitment, commitTx: Transaction, closingFeerate: FeeratePerKw, finalScriptPubKey: ByteVector, spendAnchorWithoutHtlcs: Boolean)(implicit log: LoggingAdapter): (LocalCommitPublished, SecondStageTransactions) = { + require(commitment.localCommit.txId == commitTx.txid, "txid mismatch, provided tx is not the current local commit tx") + val fundingKey = channelKeys.fundingKey(commitment.fundingTxIndex) + val commitmentKeys = commitment.localKeys(channelKeys) + val mainDelayedTx_opt = withTxGenerationLog("local-main-delayed") { + ClaimLocalDelayedOutputTx.createUnsignedTx(commitmentKeys, commitTx, commitment.localCommitParams.dustLimit, commitment.localCommitParams.toSelfDelay, finalScriptPubKey, closingFeerate, commitment.commitmentFormat) + } + val unsignedHtlcTxs = commitment.htlcTxs(fundingKey, commitmentKeys) + val (incomingHtlcs, htlcSuccessTxs) = claimIncomingHtlcOutputs(commitmentKeys, commitment.changes, unsignedHtlcTxs) + val (outgoingHtlcs, htlcTimeoutTxs) = claimOutgoingHtlcOutputs(commitmentKeys, unsignedHtlcTxs) + val anchorOutput_opt = ClaimLocalAnchorTx.findInput(commitTx, fundingKey, commitmentKeys, commitment.commitmentFormat).toOption + val spendAnchor = incomingHtlcs.nonEmpty || outgoingHtlcs.nonEmpty || spendAnchorWithoutHtlcs + val anchorTx_opt = if (spendAnchor) { + claimAnchor(fundingKey, commitmentKeys, commitTx, commitment.commitmentFormat) + } else { + None + } val lcp = LocalCommitPublished( - commitTx = tx, - claimMainDelayedOutputTx = mainDelayedTx, - htlcTxs = htlcTxs, - claimHtlcDelayedTxs = Nil, // we will claim these once the htlc txs are confirmed - claimAnchorTxs = Nil, + commitTx = commitTx, + localOutput_opt = mainDelayedTx_opt.map(_.input.outPoint), + anchorOutput_opt = anchorOutput_opt.map(_.outPoint), + incomingHtlcs = incomingHtlcs, + outgoingHtlcs = outgoingHtlcs, + htlcDelayedOutputs = Set.empty, // we will add these once the htlc txs are confirmed irrevocablySpent = Map.empty ) - val spendAnchors = htlcTxs.nonEmpty || onChainFeeConf.spendAnchorWithoutHtlcs - if (spendAnchors) { - // If we don't have pending HTLCs, we don't have funds at risk, so we can aim for a slower confirmation. - val confirmCommitBefore = htlcTxs.values.flatten.map(htlcTx => htlcTx.confirmationTarget).minByOption(_.confirmBefore).getOrElse(ConfirmationTarget.Priority(onChainFeeConf.feeTargets.closing)) - claimAnchors(keyManager, commitment, lcp, confirmCommitBefore) - } else { - lcp - } + val txs = SecondStageTransactions(mainDelayedTx_opt, anchorTx_opt, htlcSuccessTxs ++ htlcTimeoutTxs) + (lcp, txs) } - def claimAnchors(keyManager: ChannelKeyManager, commitment: FullCommitment, lcp: LocalCommitPublished, confirmationTarget: ConfirmationTarget)(implicit log: LoggingAdapter): LocalCommitPublished = { - if (shouldUpdateAnchorTxs(lcp.claimAnchorTxs, confirmationTarget)) { - val localFundingPubKey = keyManager.fundingPublicKey(commitment.localParams.fundingKeyPath, commitment.fundingTxIndex).publicKey - val claimAnchorTxs = List( - withTxGenerationLog("local-anchor") { - Transactions.makeClaimLocalAnchorOutputTx(lcp.commitTx, localFundingPubKey, confirmationTarget) - }, - withTxGenerationLog("remote-anchor") { - Transactions.makeClaimRemoteAnchorOutputTx(lcp.commitTx, commitment.remoteFundingPubKey) - } - ).flatten - lcp.copy(claimAnchorTxs = claimAnchorTxs) - } else { - lcp + def claimAnchor(fundingKey: PrivateKey, commitKeys: LocalCommitmentKeys, commitTx: Transaction, commitmentFormat: CommitmentFormat)(implicit log: LoggingAdapter): Option[ClaimLocalAnchorTx] = { + withTxGenerationLog("local-anchor") { + ClaimLocalAnchorTx.createUnsignedTx(fundingKey, commitKeys, commitTx, commitmentFormat) } } + /** Create outputs of the local commitment transaction, allowing us for example to identify HTLC outputs. */ + def makeLocalCommitTxOutputs(channelKeys: ChannelKeys, commitKeys: LocalCommitmentKeys, commitment: FullCommitment): Seq[CommitmentOutput] = { + val fundingKey = channelKeys.fundingKey(commitment.fundingTxIndex) + makeCommitTxOutputs(fundingKey.publicKey, commitment.remoteFundingPubKey, commitKeys.publicKeys, commitment.localChannelParams.paysCommitTxFees, commitment.localCommitParams.dustLimit, commitment.localCommitParams.toSelfDelay, commitment.localCommit.spec, commitment.commitmentFormat) + } + /** - * Claim the outputs of a local commit tx corresponding to HTLCs. + * Claim the outputs of a local commit tx corresponding to incoming HTLCs. If we don't have the preimage for an + * incoming HTLC, we still include an entry in the map because we may receive that preimage later. */ - def claimHtlcOutputs(keyManager: ChannelKeyManager, commitment: FullCommitment)(implicit log: LoggingAdapter): Map[OutPoint, Option[HtlcTx]] = { - val channelKeyPath = keyManager.keyPath(commitment.localParams, commitment.params.channelConfig) - val localPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, commitment.localCommit.index.toInt) - - // We collect all the preimages we wanted to reveal to our peer. - val hash2Preimage: Map[ByteVector32, ByteVector32] = commitment.changes.localChanges.all.collect { case u: UpdateFulfillHtlc => u.paymentPreimage }.map(r => Crypto.sha256(r) -> r).toMap + private def claimIncomingHtlcOutputs(commitKeys: LocalCommitmentKeys, changes: CommitmentChanges, unsignedHtlcTxs: Seq[(UnsignedHtlcTx, ByteVector64)])(implicit log: LoggingAdapter): (Map[OutPoint, Long], Seq[HtlcSuccessTx]) = { + // We collect all the preimages available. + val preimages = (changes.localChanges.all ++ changes.remoteChanges.all).collect { + case u: UpdateFulfillHtlc => Crypto.sha256(u.paymentPreimage) -> u.paymentPreimage + }.toMap // We collect incoming HTLCs that we started failing but didn't cross-sign. - val failedIncomingHtlcs: Set[Long] = commitment.changes.localChanges.all.collect { + val failedIncomingHtlcs: Set[Long] = changes.localChanges.all.collect { case u: UpdateFailHtlc => u.id case u: UpdateFailMalformedHtlc => u.id }.toSet // We collect incoming HTLCs that we haven't relayed: they may have been signed by our peer, but we haven't // received their revocation yet. - val nonRelayedIncomingHtlcs: Set[Long] = commitment.changes.remoteChanges.all.collect { case add: UpdateAddHtlc => add.id }.toSet - - commitment.localCommit.htlcTxsAndRemoteSigs.collect { - case HtlcTxAndRemoteSig(txInfo@HtlcSuccessTx(_, _, paymentHash, _, _), remoteSig) => - if (hash2Preimage.contains(paymentHash)) { + val nonRelayedIncomingHtlcs: Set[Long] = changes.remoteChanges.all.collect { case add: UpdateAddHtlc => add.id }.toSet + val incomingHtlcs = unsignedHtlcTxs.collect { + case (txInfo: UnsignedHtlcSuccessTx, remoteSig) => + if (preimages.contains(txInfo.paymentHash)) { // We immediately spend incoming htlcs for which we have the preimage. - Some(txInfo.input.outPoint -> withTxGenerationLog("htlc-success") { - val localSig = keyManager.sign(txInfo, keyManager.htlcPoint(channelKeyPath), localPerCommitmentPoint, TxOwner.Local, commitment.params.commitmentFormat, Map.empty) - Right(Transactions.addSigs(txInfo, localSig, remoteSig, hash2Preimage(paymentHash), commitment.params.commitmentFormat)) - }) + val preimage = preimages(txInfo.paymentHash) + val htlcTx_opt = withTxGenerationLog("htlc-success") { + Right(txInfo.addRemoteSig(commitKeys, remoteSig, preimage)) + } + Some(txInfo.input.outPoint, txInfo.htlcId, htlcTx_opt) } else if (failedIncomingHtlcs.contains(txInfo.htlcId)) { // We can ignore incoming htlcs that we started failing: our peer will claim them after the timeout. // We don't track those outputs because we want to move to the CLOSED state even if our peer never claims them. @@ -961,17 +1077,62 @@ object Helpers { // For all other incoming htlcs, we may receive the preimage later from downstream. We thus want to track // the corresponding outputs to ensure we don't move to the CLOSED state until they've been spent, either // by us if we receive the preimage, or by our peer after the timeout. - Some(txInfo.input.outPoint -> None) + Some(txInfo.input.outPoint, txInfo.htlcId, None) } - case HtlcTxAndRemoteSig(txInfo: HtlcTimeoutTx, remoteSig) => + }.flatten + val htlcOutputs = incomingHtlcs.collect { case (outpoint, htlcId, _) => outpoint -> htlcId }.toMap + val htlcTxs = incomingHtlcs.collect { case (_, _, htlcTx_opt) => htlcTx_opt }.flatten + (htlcOutputs, htlcTxs) + } + + /** + * Claim the outputs of a local commit tx corresponding to outgoing HTLCs, after their timeout. + */ + private def claimOutgoingHtlcOutputs(commitKeys: LocalCommitmentKeys, unsignedHtlcTxs: Seq[(UnsignedHtlcTx, ByteVector64)])(implicit log: LoggingAdapter): (Map[OutPoint, Long], Seq[HtlcTimeoutTx]) = { + val outgoingHtlcs = unsignedHtlcTxs.collect { + case (txInfo: UnsignedHtlcTimeoutTx, remoteSig) => // We track all outputs that belong to outgoing htlcs. Our peer may or may not have the preimage: if they // claim the output, we will learn the preimage from their transaction, otherwise we will get our funds // back after the timeout. - Some(txInfo.input.outPoint -> withTxGenerationLog("htlc-timeout") { - val localSig = keyManager.sign(txInfo, keyManager.htlcPoint(channelKeyPath), localPerCommitmentPoint, TxOwner.Local, commitment.params.commitmentFormat, Map.empty) - Right(Transactions.addSigs(txInfo, localSig, remoteSig, commitment.params.commitmentFormat)) - }) - }.flatten.toMap + val htlcTx_opt = withTxGenerationLog("htlc-timeout") { + Right(txInfo.addRemoteSig(commitKeys, remoteSig)) + } + (txInfo.input.outPoint, txInfo.htlcId, htlcTx_opt) + } + val htlcOutputs = outgoingHtlcs.collect { case (outpoint, htlcId, _) => outpoint -> htlcId }.toMap + val htlcTxs = outgoingHtlcs.collect { case (_, _, htlcTx_opt) => htlcTx_opt }.flatten + (htlcOutputs, htlcTxs) + } + + /** Claim the outputs of incoming HTLCs for the payment_hash matching the preimage provided. */ + def claimHtlcsWithPreimage(channelKeys: ChannelKeys, commitKeys: LocalCommitmentKeys, commitment: FullCommitment, preimage: ByteVector32)(implicit log: LoggingAdapter): Seq[HtlcSuccessTx] = { + val fundingKey = channelKeys.fundingKey(commitment.fundingTxIndex) + commitment.htlcTxs(fundingKey, commitKeys).collect { + case (txInfo: UnsignedHtlcSuccessTx, remoteSig) if txInfo.paymentHash == Crypto.sha256(preimage) => + withTxGenerationLog("htlc-success") { + Right(txInfo.addRemoteSig(commitKeys, remoteSig, preimage)) + } + }.flatten + } + + /** + * An incoming HTLC that we've forwarded has been failed downstream: if the channel wasn't closing we would relay + * that failure. Since the channel is closing, our peer should claim the HTLC on-chain after the timeout. + * We stop tracking the corresponding output because we want to move to the CLOSED state even if our peer never + * claims it (which may happen if the HTLC amount is low and on-chain fees are high). + */ + def ignoreFailedIncomingHtlc(htlcId: Long, localCommitPublished: LocalCommitPublished, commitment: FullCommitment): LocalCommitPublished = { + // If we have the preimage (e.g. for partially fulfilled multi-part payments), we keep the HTLC-success tx. + val preimages = (commitment.changes.localChanges.all ++ commitment.changes.remoteChanges.all).collect { + case u: UpdateFulfillHtlc => Crypto.sha256(u.paymentPreimage) -> u.paymentPreimage + }.toMap + val htlcsWithPreimage = commitment.localCommit.spec.htlcs.collect { + case IncomingHtlc(add: UpdateAddHtlc) if preimages.contains(add.paymentHash) => add.id + } + val outpoints = localCommitPublished.incomingHtlcs.collect { + case (outpoint, id) if id == htlcId && !htlcsWithPreimage.contains(id) => outpoint + }.toSet + localCommitPublished.copy(incomingHtlcs = localCommitPublished.incomingHtlcs -- outpoints) } /** @@ -980,133 +1141,97 @@ object Helpers { * NB: with anchor outputs, it's possible to have transactions that spend *many* HTLC outputs at once, but we're not * doing that because it introduces a lot of subtle edge cases. */ - def claimHtlcDelayedOutput(localCommitPublished: LocalCommitPublished, keyManager: ChannelKeyManager, commitment: FullCommitment, tx: Transaction, feerates: FeeratesPerKw, onChainFeeConf: OnChainFeeConf, finalScriptPubKey: ByteVector)(implicit log: LoggingAdapter): (LocalCommitPublished, Option[HtlcDelayedTx]) = { - if (tx.txIn.exists(txIn => localCommitPublished.htlcTxs.contains(txIn.outPoint))) { - val feeratePerKwDelayed = onChainFeeConf.getClosingFeerate(feerates) - val channelKeyPath = keyManager.keyPath(commitment.localParams, commitment.params.channelConfig) - val localPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, commitment.localCommit.index.toInt) - val localRevocationPubkey = Generators.revocationPubKey(commitment.remoteParams.revocationBasepoint, localPerCommitmentPoint) - val localDelayedPubkey = Generators.derivePubKey(keyManager.delayedPaymentPoint(channelKeyPath).publicKey, localPerCommitmentPoint) + def claimHtlcDelayedOutput(localCommitPublished: LocalCommitPublished, channelKeys: ChannelKeys, commitment: FullCommitment, tx: Transaction, closingFeerate: FeeratePerKw, finalScriptPubKey: ByteVector)(implicit log: LoggingAdapter): (LocalCommitPublished, ThirdStageTransactions) = { + if (tx.txIn.exists(txIn => localCommitPublished.htlcOutputs.contains(txIn.outPoint))) { + val commitKeys = commitment.localKeys(channelKeys) + // Note that this will return None if the transaction wasn't one of our HTLC transactions, which may happen + // if our peer was able to claim the HTLC output before us (race condition between success and timeout). val htlcDelayedTx_opt = withTxGenerationLog("htlc-delayed") { - // Note that this will return None if the transaction wasn't one of our HTLC transactions, which may happen - // if our peer was able to claim the HTLC output before us (race condition between success and timeout). - Transactions.makeHtlcDelayedTx(tx, commitment.localParams.dustLimit, localRevocationPubkey, commitment.remoteParams.toSelfDelay, localDelayedPubkey, finalScriptPubKey, feeratePerKwDelayed).map(claimDelayed => { - val sig = keyManager.sign(claimDelayed, keyManager.delayedPaymentPoint(channelKeyPath), localPerCommitmentPoint, TxOwner.Local, commitment.params.commitmentFormat, Map.empty) - Transactions.addSigs(claimDelayed, sig) - }) + HtlcDelayedTx.createUnsignedTx(commitKeys, tx, commitment.localCommitParams.dustLimit, commitment.localCommitParams.toSelfDelay, finalScriptPubKey, closingFeerate, commitment.commitmentFormat) } - val localCommitPublished1 = localCommitPublished.copy(claimHtlcDelayedTxs = localCommitPublished.claimHtlcDelayedTxs ++ htlcDelayedTx_opt.toSeq) - (localCommitPublished1, htlcDelayedTx_opt) + val localCommitPublished1 = localCommitPublished.copy(htlcDelayedOutputs = localCommitPublished.htlcDelayedOutputs ++ htlcDelayedTx_opt.map(_.input.outPoint).toSeq) + (localCommitPublished1, ThirdStageTransactions(htlcDelayedTx_opt.toSeq)) } else { - (localCommitPublished, None) + (localCommitPublished, ThirdStageTransactions(Nil)) } } - } - - object RemoteClose { /** - * Claim all the HTLCs that we've received from their current commit tx, if the channel used option_static_remotekey - * we don't need to claim our main output because it directly pays to one of our wallet's p2wpkh addresses. - * - * @param commitment our commitment data, which includes payment preimages - * @param remoteCommit the remote commitment data to use to claim outputs (it can be their current or next commitment) - * @param tx the remote commitment transaction that has just been published - * @return a list of transactions (one per output of the commit tx that we can claim) + * Claim the outputs of all 2nd-stage HTLC transactions that have been confirmed. */ - def claimCommitTxOutputs(keyManager: ChannelKeyManager, commitment: FullCommitment, remoteCommit: RemoteCommit, tx: Transaction, feerates: FeeratesPerKw, onChainFeeConf: OnChainFeeConf, finalScriptPubKey: ByteVector)(implicit log: LoggingAdapter): RemoteCommitPublished = { - require(remoteCommit.txid == tx.txid, "txid mismatch, provided tx is not the current remote commit tx") + def claimHtlcDelayedOutputs(localCommitPublished: LocalCommitPublished, channelKeys: ChannelKeys, commitment: FullCommitment, closingFeerate: FeeratePerKw, finalScriptPubKey: ByteVector)(implicit log: LoggingAdapter): ThirdStageTransactions = { + val confirmedHtlcTxs = localCommitPublished.htlcOutputs.flatMap(htlcOutput => localCommitPublished.irrevocablySpent.get(htlcOutput)) + val htlcDelayedTxs = confirmedHtlcTxs.flatMap(tx => claimHtlcDelayedOutput(localCommitPublished, channelKeys, commitment, tx, closingFeerate, finalScriptPubKey)._2.htlcDelayedTxs) + ThirdStageTransactions(htlcDelayedTxs.toSeq) + } + + } - val htlcTxs: Map[OutPoint, Option[ClaimHtlcTx]] = claimHtlcOutputs(keyManager, commitment, remoteCommit, feerates, finalScriptPubKey) + object RemoteClose { + /** Transactions spending outputs of a remote commitment transaction. */ + case class SecondStageTransactions(mainTx_opt: Option[ClaimRemoteDelayedOutputTx], anchorTx_opt: Option[ClaimRemoteAnchorTx], htlcTxs: Seq[ClaimHtlcTx]) + + /** Claim all the outputs that belong to us in the remote commitment transaction (which can be either their current or next commitment). */ + def claimCommitTxOutputs(channelKeys: ChannelKeys, commitment: FullCommitment, remoteCommit: RemoteCommit, commitTx: Transaction, closingFeerate: FeeratePerKw, finalScriptPubKey: ByteVector, spendAnchorWithoutHtlcs: Boolean)(implicit log: LoggingAdapter): (RemoteCommitPublished, SecondStageTransactions) = { + require(remoteCommit.txId == commitTx.txid, "txid mismatch, provided tx is not the current remote commit tx") + val fundingKey = channelKeys.fundingKey(commitment.fundingTxIndex) + val commitKeys = commitment.remoteKeys(channelKeys, remoteCommit.remotePerCommitmentPoint) + val outputs = makeRemoteCommitTxOutputs(channelKeys, commitKeys, commitment, remoteCommit) + val mainTx_opt = claimMainOutput(commitKeys, commitTx, commitment.localCommitParams.dustLimit, commitment.commitmentFormat, closingFeerate, finalScriptPubKey) + val (incomingHtlcs, htlcSuccessTxs) = claimIncomingHtlcOutputs(commitKeys, commitTx, outputs, commitment, remoteCommit, finalScriptPubKey) + val (outgoingHtlcs, htlcTimeoutTxs) = claimOutgoingHtlcOutputs(commitKeys, commitTx, outputs, commitment, remoteCommit, finalScriptPubKey) + val anchorOutput_opt = ClaimRemoteAnchorTx.findInput(commitTx, fundingKey, commitKeys, commitment.commitmentFormat).toOption + val spendAnchor = incomingHtlcs.nonEmpty || outgoingHtlcs.nonEmpty || spendAnchorWithoutHtlcs + val anchorTx_opt = if (spendAnchor) { + claimAnchor(fundingKey, commitKeys, commitTx, commitment.commitmentFormat) + } else { + None + } val rcp = RemoteCommitPublished( - commitTx = tx, - claimMainOutputTx = claimMainOutput(keyManager, commitment.params, remoteCommit.remotePerCommitmentPoint, tx, feerates, onChainFeeConf, finalScriptPubKey), - claimHtlcTxs = htlcTxs, - claimAnchorTxs = Nil, + commitTx = commitTx, + localOutput_opt = mainTx_opt.map(_.input.outPoint), + anchorOutput_opt = anchorOutput_opt.map(_.outPoint), + incomingHtlcs = incomingHtlcs, + outgoingHtlcs = outgoingHtlcs, irrevocablySpent = Map.empty ) - val spendAnchors = htlcTxs.nonEmpty || onChainFeeConf.spendAnchorWithoutHtlcs - if (spendAnchors) { - // If we don't have pending HTLCs, we don't have funds at risk, so we use the normal closing priority. - val confirmCommitBefore = htlcTxs.values.flatten.map(htlcTx => htlcTx.confirmationTarget).minByOption(_.confirmBefore).getOrElse(ConfirmationTarget.Priority(onChainFeeConf.feeTargets.closing)) - claimAnchors(keyManager, commitment, rcp, confirmCommitBefore) - } else { - rcp - } + val txs = SecondStageTransactions(mainTx_opt, anchorTx_opt, htlcSuccessTxs ++ htlcTimeoutTxs) + (rcp, txs) } - def claimAnchors(keyManager: ChannelKeyManager, commitment: FullCommitment, rcp: RemoteCommitPublished, confirmationTarget: ConfirmationTarget)(implicit log: LoggingAdapter): RemoteCommitPublished = { - if (shouldUpdateAnchorTxs(rcp.claimAnchorTxs, confirmationTarget)) { - val localFundingPubkey = keyManager.fundingPublicKey(commitment.localParams.fundingKeyPath, commitment.fundingTxIndex).publicKey - val claimAnchorTxs = List( - withTxGenerationLog("local-anchor") { - Transactions.makeClaimLocalAnchorOutputTx(rcp.commitTx, localFundingPubkey, confirmationTarget) - }, - withTxGenerationLog("remote-anchor") { - Transactions.makeClaimRemoteAnchorOutputTx(rcp.commitTx, commitment.remoteFundingPubKey) - } - ).flatten - rcp.copy(claimAnchorTxs = claimAnchorTxs) - } else { - rcp + def claimAnchor(fundingKey: PrivateKey, commitKeys: RemoteCommitmentKeys, commitTx: Transaction, commitmentFormat: CommitmentFormat)(implicit log: LoggingAdapter): Option[ClaimRemoteAnchorTx] = { + withTxGenerationLog("remote-anchor") { + ClaimRemoteAnchorTx.createUnsignedTx(fundingKey, commitKeys, commitTx, commitmentFormat) } } - /** - * Claim our main output only - * - * @param remotePerCommitmentPoint the remote perCommitmentPoint corresponding to this commitment - * @param tx the remote commitment transaction that has just been published - * @return an optional [[ClaimRemoteCommitMainOutputTx]] transaction claiming our main output - */ - def claimMainOutput(keyManager: ChannelKeyManager, params: ChannelParams, remotePerCommitmentPoint: PublicKey, tx: Transaction, feerates: FeeratesPerKw, onChainFeeConf: OnChainFeeConf, finalScriptPubKey: ByteVector)(implicit log: LoggingAdapter): Option[ClaimRemoteCommitMainOutputTx] = { - if (params.channelFeatures.paysDirectlyToWallet) { - // the commitment tx sends funds directly to our wallet, no claim tx needed - None - } else { - val channelKeyPath = keyManager.keyPath(params.localParams, params.channelConfig) - val localPubkey = Generators.derivePubKey(keyManager.paymentPoint(channelKeyPath).publicKey, remotePerCommitmentPoint) - val localPaymentPoint = keyManager.paymentPoint(channelKeyPath).publicKey - val feeratePerKwMain = onChainFeeConf.getClosingFeerate(feerates) - - params.commitmentFormat match { - case DefaultCommitmentFormat => withTxGenerationLog("remote-main") { - Transactions.makeClaimP2WPKHOutputTx(tx, params.localParams.dustLimit, localPubkey, finalScriptPubKey, feeratePerKwMain).map(claimMain => { - val sig = keyManager.sign(claimMain, keyManager.paymentPoint(channelKeyPath), remotePerCommitmentPoint, TxOwner.Local, params.commitmentFormat, Map.empty) - Transactions.addSigs(claimMain, localPubkey, sig) - }) - } - case _: AnchorOutputsCommitmentFormat => withTxGenerationLog("remote-main-delayed") { - Transactions.makeClaimRemoteDelayedOutputTx(tx, params.localParams.dustLimit, localPaymentPoint, finalScriptPubKey, feeratePerKwMain).map(claimMain => { - val sig = keyManager.sign(claimMain, keyManager.paymentPoint(channelKeyPath), TxOwner.Local, params.commitmentFormat, Map.empty) - Transactions.addSigs(claimMain, sig) - }) - } + /** Claim our main output from the remote commitment transaction, if available. */ + def claimMainOutput(commitKeys: RemoteCommitmentKeys, commitTx: Transaction, dustLimit: Satoshi, commitmentFormat: CommitmentFormat, feerate: FeeratePerKw, finalScriptPubKey: ByteVector)(implicit log: LoggingAdapter): Option[ClaimRemoteDelayedOutputTx] = { + commitmentFormat match { + case _: AnchorOutputsCommitmentFormat | _: SimpleTaprootChannelCommitmentFormat => withTxGenerationLog("remote-main-delayed") { + ClaimRemoteDelayedOutputTx.createUnsignedTx(commitKeys, commitTx, dustLimit, finalScriptPubKey, feerate, commitmentFormat) } } } + /** Create outputs of the remote commitment transaction, allowing us for example to identify HTLC outputs. */ + def makeRemoteCommitTxOutputs(channelKeys: ChannelKeys, commitKeys: RemoteCommitmentKeys, commitment: FullCommitment, remoteCommit: RemoteCommit): Seq[CommitmentOutput] = { + val fundingKey = channelKeys.fundingKey(commitment.fundingTxIndex) + makeCommitTxOutputs(commitment.remoteFundingPubKey, fundingKey.publicKey, commitKeys.publicKeys, !commitment.localChannelParams.paysCommitTxFees, commitment.remoteCommitParams.dustLimit, commitment.remoteCommitParams.toSelfDelay, remoteCommit.spec, commitment.commitmentFormat) + } + /** - * Claim our htlc outputs only from the remote commitment. + * Claim the outputs of a remote commit tx corresponding to incoming HTLCs. If we don't have the preimage for an + * incoming HTLC, we still include an entry in the map because we may receive that preimage later. */ - def claimHtlcOutputs(keyManager: ChannelKeyManager, commitment: FullCommitment, remoteCommit: RemoteCommit, feerates: FeeratesPerKw, finalScriptPubKey: ByteVector)(implicit log: LoggingAdapter): Map[OutPoint, Option[ClaimHtlcTx]] = { - val (remoteCommitTx, _) = Commitment.makeRemoteTxs(keyManager, commitment.params.channelConfig, commitment.params.channelFeatures, remoteCommit.index, commitment.localParams, commitment.remoteParams, commitment.fundingTxIndex, commitment.remoteFundingPubKey, commitment.commitInput, remoteCommit.remotePerCommitmentPoint, remoteCommit.spec) - require(remoteCommitTx.tx.txid == remoteCommit.txid, "txid mismatch, cannot recompute the current remote commit tx") - val channelKeyPath = keyManager.keyPath(commitment.localParams, commitment.params.channelConfig) - val localFundingPubkey = keyManager.fundingPublicKey(commitment.localParams.fundingKeyPath, commitment.fundingTxIndex).publicKey - val localHtlcPubkey = Generators.derivePubKey(keyManager.htlcPoint(channelKeyPath).publicKey, remoteCommit.remotePerCommitmentPoint) - val remoteHtlcPubkey = Generators.derivePubKey(commitment.remoteParams.htlcBasepoint, remoteCommit.remotePerCommitmentPoint) - val remoteRevocationPubkey = Generators.revocationPubKey(keyManager.revocationPoint(channelKeyPath).publicKey, remoteCommit.remotePerCommitmentPoint) - val remoteDelayedPaymentPubkey = Generators.derivePubKey(commitment.remoteParams.delayedPaymentBasepoint, remoteCommit.remotePerCommitmentPoint) - val localPaymentBasepoint = commitment.localParams.walletStaticPaymentBasepoint.getOrElse(keyManager.paymentPoint(channelKeyPath).publicKey) - val localPaymentPubkey = if (commitment.params.channelFeatures.hasFeature(Features.StaticRemoteKey)) localPaymentBasepoint else Generators.derivePubKey(localPaymentBasepoint, remoteCommit.remotePerCommitmentPoint) - val outputs = makeCommitTxOutputs(!commitment.localParams.paysCommitTxFees, commitment.remoteParams.dustLimit, remoteRevocationPubkey, commitment.localParams.toSelfDelay, remoteDelayedPaymentPubkey, localPaymentPubkey, remoteHtlcPubkey, localHtlcPubkey, commitment.remoteFundingPubKey, localFundingPubkey, remoteCommit.spec, commitment.params.commitmentFormat) - // We need to use a rather high fee for htlc-claim because we compete with the counterparty. - val feeratePerKwHtlc = feerates.fast - - // We collect all the preimages we wanted to reveal to our peer. - val hash2Preimage: Map[ByteVector32, ByteVector32] = commitment.changes.localChanges.all.collect { case u: UpdateFulfillHtlc => u.paymentPreimage }.map(r => Crypto.sha256(r) -> r).toMap + private def claimIncomingHtlcOutputs(commitKeys: RemoteCommitmentKeys, commitTx: Transaction, outputs: Seq[CommitmentOutput], commitment: FullCommitment, remoteCommit: RemoteCommit, finalScriptPubKey: ByteVector)(implicit log: LoggingAdapter): (Map[OutPoint, Long], Seq[ClaimHtlcSuccessTx]) = { + // The feerate will be set by the publisher actor based on the HTLC expiry, we don't care which feerate is used here. + val feerate = FeeratePerByte(1 sat).perKw + // We collect all the preimages available. + val preimages = (commitment.changes.localChanges.all ++ commitment.changes.remoteChanges.all).collect { + case u: UpdateFulfillHtlc => Crypto.sha256(u.paymentPreimage) -> u.paymentPreimage + }.toMap // We collect incoming HTLCs that we started failing but didn't cross-sign. val failedIncomingHtlcs: Set[Long] = commitment.changes.localChanges.all.collect { case u: UpdateFailHtlc => u.id @@ -1115,53 +1240,100 @@ object Helpers { // We collect incoming HTLCs that we haven't relayed: they may have been signed by our peer, but they haven't // sent their revocation yet. val nonRelayedIncomingHtlcs: Set[Long] = commitment.changes.remoteChanges.all.collect { case add: UpdateAddHtlc => add.id }.toSet - // Remember we are looking at the remote commitment so IN for them is really OUT for us and vice versa. - remoteCommit.spec.htlcs.collect { + val incomingHtlcs = remoteCommit.spec.htlcs.collect { case OutgoingHtlc(add: UpdateAddHtlc) => - // NB: we first generate the tx skeleton and finalize it below if we have the preimage, so we set logSuccess to false to avoid logging twice. - withTxGenerationLog("claim-htlc-success", logSuccess = false) { - Transactions.makeClaimHtlcSuccessTx(remoteCommitTx.tx, outputs, commitment.localParams.dustLimit, localHtlcPubkey, remoteHtlcPubkey, remoteRevocationPubkey, finalScriptPubKey, add, feeratePerKwHtlc, commitment.params.commitmentFormat) - }.map(claimHtlcTx => { - if (hash2Preimage.contains(add.paymentHash)) { - // We immediately spend incoming htlcs for which we have the preimage. - Some(claimHtlcTx.input.outPoint -> withTxGenerationLog("claim-htlc-success") { - val sig = keyManager.sign(claimHtlcTx, keyManager.htlcPoint(channelKeyPath), remoteCommit.remotePerCommitmentPoint, TxOwner.Local, commitment.params.commitmentFormat, Map.empty) - Right(Transactions.addSigs(claimHtlcTx, sig, hash2Preimage(add.paymentHash))) - }) - } else if (failedIncomingHtlcs.contains(add.id)) { - // We can ignore incoming htlcs that we started failing: our peer will claim them after the timeout. - // We don't track those outputs because we want to move to the CLOSED state even if our peer never claims them. - None - } else if (nonRelayedIncomingHtlcs.contains(add.id)) { - // Similarly, we can also ignore incoming htlcs that we haven't relayed, because we can't receive the preimage. - None - } else { - // For all other incoming htlcs, we may receive the preimage later from downstream. We thus want to track - // the corresponding outputs to ensure we don't move to the CLOSED state until they've been spent, either - // by us if we receive the preimage, or by our peer after the timeout. - Some(claimHtlcTx.input.outPoint -> None) - } - }) + if (preimages.contains(add.paymentHash)) { + // We immediately spend incoming htlcs for which we have the preimage. + val preimage = preimages(add.paymentHash) + withTxGenerationLog("claim-htlc-success") { + ClaimHtlcSuccessTx.createUnsignedTx(commitKeys, commitTx, commitment.localCommitParams.dustLimit, outputs, finalScriptPubKey, add, preimage, feerate, commitment.commitmentFormat) + }.map(claimHtlcTx => (claimHtlcTx.input.outPoint, add.id, Some(claimHtlcTx))) + } else if (failedIncomingHtlcs.contains(add.id)) { + // We can ignore incoming htlcs that we started failing: our peer will claim them after the timeout. + // We don't track those outputs because we want to move to the CLOSED state even if our peer never claims them. + None + } else if (nonRelayedIncomingHtlcs.contains(add.id)) { + // Similarly, we can also ignore incoming htlcs that we haven't relayed, because we can't receive the preimage. + None + } else { + // For all other incoming htlcs, we may receive the preimage later from downstream. We thus want to track + // the corresponding outputs to ensure we don't move to the CLOSED state until they've been spent, either + // by us if we receive the preimage, or by our peer after the timeout. + ClaimHtlcSuccessTx.findInput(commitTx, outputs, add).map(input => (input.outPoint, add.id, None)) + } + }.flatten.toSeq + val htlcOutputs = incomingHtlcs.collect { case (outpoint, htlcId, _) => outpoint -> htlcId }.toMap + val htlcTxs = incomingHtlcs.collect { case (_, _, htlcTx_opt) => htlcTx_opt }.flatten + (htlcOutputs, htlcTxs) + } + + /** + * Claim the outputs of a remote commit tx corresponding to outgoing HTLCs, after their timeout. + */ + private def claimOutgoingHtlcOutputs(commitKeys: RemoteCommitmentKeys, commitTx: Transaction, outputs: Seq[CommitmentOutput], commitment: FullCommitment, remoteCommit: RemoteCommit, finalScriptPubKey: ByteVector)(implicit log: LoggingAdapter): (Map[OutPoint, Long], Seq[ClaimHtlcTimeoutTx]) = { + // The feerate will be set by the publisher actor based on the HTLC expiry, we don't care which feerate is used here. + val feerate = FeeratePerByte(1 sat).perKw + // Remember we are looking at the remote commitment so IN for them is really OUT for us and vice versa. + val outgoingHtlcs = remoteCommit.spec.htlcs.collect { case IncomingHtlc(add: UpdateAddHtlc) => // We track all outputs that belong to outgoing htlcs. Our peer may or may not have the preimage: if they // claim the output, we will learn the preimage from their transaction, otherwise we will get our funds // back after the timeout. - // NB: we first generate the tx skeleton and finalize it below, so we set logSuccess to false to avoid logging twice. - withTxGenerationLog("claim-htlc-timeout", logSuccess = false) { - Transactions.makeClaimHtlcTimeoutTx(remoteCommitTx.tx, outputs, commitment.localParams.dustLimit, localHtlcPubkey, remoteHtlcPubkey, remoteRevocationPubkey, finalScriptPubKey, add, feeratePerKwHtlc, commitment.params.commitmentFormat) - }.map(claimHtlcTx => { - Some(claimHtlcTx.input.outPoint -> withTxGenerationLog("claim-htlc-timeout") { - val sig = keyManager.sign(claimHtlcTx, keyManager.htlcPoint(channelKeyPath), remoteCommit.remotePerCommitmentPoint, TxOwner.Local, commitment.params.commitmentFormat, Map.empty) - Right(Transactions.addSigs(claimHtlcTx, sig)) - }) - }) - }.toSeq.flatten.flatten.toMap + withTxGenerationLog("claim-htlc-timeout") { + ClaimHtlcTimeoutTx.createUnsignedTx(commitKeys, commitTx, commitment.localCommitParams.dustLimit, outputs, finalScriptPubKey, add, feerate, commitment.commitmentFormat) + }.map(claimHtlcTx => (claimHtlcTx.input.outPoint, add.id, Some(claimHtlcTx))) + }.flatten.toSeq + val htlcOutputs = outgoingHtlcs.collect { case (outpoint, htlcId, _) => outpoint -> htlcId }.toMap + val htlcTxs = outgoingHtlcs.collect { case (_, _, htlcTx_opt) => htlcTx_opt }.flatten + (htlcOutputs, htlcTxs) } + + /** Claim the outputs of incoming HTLCs for the payment_hash matching the preimage provided. */ + def claimHtlcsWithPreimage(channelKeys: ChannelKeys, commitKeys: RemoteCommitmentKeys, remoteCommitPublished: RemoteCommitPublished, commitment: FullCommitment, remoteCommit: RemoteCommit, preimage: ByteVector32, finalScriptPubKey: ByteVector)(implicit log: LoggingAdapter): Seq[ClaimHtlcSuccessTx] = { + val outputs = makeRemoteCommitTxOutputs(channelKeys, commitKeys, commitment, remoteCommit) + // The feerate will be set by the publisher actor based on the HTLC expiry, we don't care which feerate is used here. + val feerate = FeeratePerByte(1 sat).perKw + remoteCommit.spec.htlcs.collect { + // Remember we are looking at the remote commitment so IN for them is really OUT for us and vice versa. + case OutgoingHtlc(add: UpdateAddHtlc) if add.paymentHash == Crypto.sha256(preimage) => + withTxGenerationLog("claim-htlc-success") { + ClaimHtlcSuccessTx.createUnsignedTx(commitKeys, remoteCommitPublished.commitTx, commitment.localCommitParams.dustLimit, outputs, finalScriptPubKey, add, preimage, feerate, commitment.commitmentFormat) + } + }.flatten.toSeq + } + + /** + * An incoming HTLC that we've forwarded has been failed downstream: if the channel wasn't closing we would relay + * that failure. Since the channel is closing, our peer should claim the HTLC on-chain after the timeout. + * We stop tracking the corresponding output because we want to move to the CLOSED state even if our peer never + * claims it (which may happen if the HTLC amount is low and on-chain fees are high). + */ + def ignoreFailedIncomingHtlc(htlcId: Long, remoteCommitPublished: RemoteCommitPublished, commitment: FullCommitment, remoteCommit: RemoteCommit): RemoteCommitPublished = { + // If we have the preimage (e.g. for partially fulfilled multi-part payments), we keep the HTLC-success tx. + val preimages = (commitment.changes.localChanges.all ++ commitment.changes.remoteChanges.all).collect { + case u: UpdateFulfillHtlc => Crypto.sha256(u.paymentPreimage) -> u.paymentPreimage + }.toMap + // Remember we are looking at the remote commitment so IN for them is really OUT for us and vice versa. + val htlcsWithPreimage = remoteCommit.spec.htlcs.collect { + case OutgoingHtlc(add: UpdateAddHtlc) if preimages.contains(add.paymentHash) => add.id + } + val outpoints = remoteCommitPublished.incomingHtlcs.collect { + case (outpoint, id) if id == htlcId && !htlcsWithPreimage.contains(id) => outpoint + }.toSet + remoteCommitPublished.copy(incomingHtlcs = remoteCommitPublished.incomingHtlcs -- outpoints) + } + } object RevokedClose { + /** Transactions spending outputs of a revoked remote commitment transactions. */ + case class SecondStageTransactions(mainTx_opt: Option[ClaimRemoteDelayedOutputTx], mainPenaltyTx_opt: Option[MainPenaltyTx], htlcPenaltyTxs: Seq[HtlcPenaltyTx]) + + /** Transactions spending outputs of confirmed remote HTLC transactions. */ + case class ThirdStageTransactions(htlcDelayedPenaltyTxs: Seq[ClaimHtlcDelayedOutputPenaltyTx]) + /** * When an unexpected transaction spending the funding tx is detected, we must be in one of the following scenarios: * @@ -1173,15 +1345,13 @@ object Helpers { * * This function returns the per-commitment secret in the first case, and None in the other cases. */ - def getRemotePerCommitmentSecret(keyManager: ChannelKeyManager, params: ChannelParams, remotePerCommitmentSecrets: ShaChain, commitTx: Transaction): Option[(Long, PrivateKey)] = { - import params._ + def getRemotePerCommitmentSecret(params: ChannelParams, channelKeys: ChannelKeys, remotePerCommitmentSecrets: ShaChain, commitTx: Transaction): Option[(Long, PrivateKey)] = { // a valid tx will always have at least one input, but this ensures we don't throw in tests val sequence = commitTx.txIn.headOption.map(_.sequence).getOrElse(0L) val obscuredTxNumber = Transactions.decodeTxNumber(sequence, commitTx.lockTime) - val channelKeyPath = keyManager.keyPath(localParams, channelConfig) - val localPaymentPoint = localParams.walletStaticPaymentBasepoint.getOrElse(keyManager.paymentPoint(channelKeyPath).publicKey) + val localPaymentPoint = channelKeys.paymentBasePoint // this tx has been published by remote, so we need to invert local/remote params - val txNumber = Transactions.obscuredCommitTxNumber(obscuredTxNumber, !localParams.isChannelOpener, remoteParams.paymentBasepoint, localPaymentPoint) + val txNumber = Transactions.obscuredCommitTxNumber(obscuredTxNumber, !params.localParams.isChannelOpener, params.remoteParams.paymentBasepoint, localPaymentPoint) if (txNumber > 0xffffffffffffL) { // txNumber must be lesser than 48 bits long None @@ -1195,81 +1365,45 @@ object Helpers { * When a revoked commitment transaction spending the funding tx is detected, we build a set of transactions that * will punish our peer by stealing all their funds. */ - def claimCommitTxOutputs(keyManager: ChannelKeyManager, params: ChannelParams, commitTx: Transaction, commitmentNumber: Long, remotePerCommitmentSecret: PrivateKey, db: ChannelsDb, feerates: FeeratesPerKw, onChainFeeConf: OnChainFeeConf, finalScriptPubKey: ByteVector)(implicit log: LoggingAdapter): RevokedCommitPublished = { - import params._ + def claimCommitTxOutputs(channelParams: ChannelParams, channelKeys: ChannelKeys, commitTx: Transaction, commitmentNumber: Long, remotePerCommitmentSecret: PrivateKey, toSelfDelay: CltvExpiryDelta, commitmentFormat: CommitmentFormat, db: ChannelsDb, dustLimit: Satoshi, feerates: FeeratesPerKw, onChainFeeConf: OnChainFeeConf, finalScriptPubKey: ByteVector)(implicit log: LoggingAdapter): (RevokedCommitPublished, SecondStageTransactions) = { log.warning("a revoked commit has been published with commitmentNumber={}", commitmentNumber) - val channelKeyPath = keyManager.keyPath(localParams, channelConfig) - val localPaymentPoint = localParams.walletStaticPaymentBasepoint.getOrElse(keyManager.paymentPoint(channelKeyPath).publicKey) - val remotePerCommitmentPoint = remotePerCommitmentSecret.publicKey - val remoteDelayedPaymentPubkey = Generators.derivePubKey(remoteParams.delayedPaymentBasepoint, remotePerCommitmentPoint) - val remoteRevocationPubkey = Generators.revocationPubKey(keyManager.revocationPoint(channelKeyPath).publicKey, remotePerCommitmentPoint) - val remoteHtlcPubkey = Generators.derivePubKey(remoteParams.htlcBasepoint, remotePerCommitmentPoint) - val localPaymentPubkey = Generators.derivePubKey(keyManager.paymentPoint(channelKeyPath).publicKey, remotePerCommitmentPoint) - val localHtlcPubkey = Generators.derivePubKey(keyManager.htlcPoint(channelKeyPath).publicKey, remotePerCommitmentPoint) - - val feerateMain = onChainFeeConf.getClosingFeerate(feerates) - // we need to use a high fee here for punishment txs because after a delay they can be spent by the counterparty + val commitKeys = RemoteCommitmentKeys(channelParams, channelKeys, remotePerCommitmentSecret.publicKey) + val revocationKey = channelKeys.revocationKey(remotePerCommitmentSecret) + + val feerateMain = onChainFeeConf.getClosingFeerate(feerates, maxClosingFeerateOverride_opt = None) + // We need to use a high fee here for punishment txs because after a delay they can be spent by the counterparty. val feeratePenalty = feerates.fast - // first we will claim our main output right away - val mainTx = channelFeatures match { - case ct if ct.paysDirectlyToWallet => - log.info(s"channel uses option_static_remotekey to pay directly to our wallet, there is nothing to do") - None - case ct => ct.commitmentFormat match { - case DefaultCommitmentFormat => withTxGenerationLog("remote-main") { - Transactions.makeClaimP2WPKHOutputTx(commitTx, localParams.dustLimit, localPaymentPubkey, finalScriptPubKey, feerateMain).map(claimMain => { - val sig = keyManager.sign(claimMain, keyManager.paymentPoint(channelKeyPath), remotePerCommitmentPoint, TxOwner.Local, commitmentFormat, Map.empty) - Transactions.addSigs(claimMain, localPaymentPubkey, sig) - }) - } - case _: AnchorOutputsCommitmentFormat => withTxGenerationLog("remote-main-delayed") { - Transactions.makeClaimRemoteDelayedOutputTx(commitTx, localParams.dustLimit, localPaymentPoint, finalScriptPubKey, feerateMain).map(claimMain => { - val sig = keyManager.sign(claimMain, keyManager.paymentPoint(channelKeyPath), TxOwner.Local, commitmentFormat, Map.empty) - Transactions.addSigs(claimMain, sig) - }) - } + // First we will claim our main output right away. + val mainTx_opt = commitmentFormat match { + case _: AnchorOutputsCommitmentFormat | _: SimpleTaprootChannelCommitmentFormat => withTxGenerationLog("remote-main-delayed") { + ClaimRemoteDelayedOutputTx.createUnsignedTx(commitKeys, commitTx, dustLimit, finalScriptPubKey, feerateMain, commitmentFormat) } } - // then we punish them by stealing their main output - val mainPenaltyTx = withTxGenerationLog("main-penalty") { - Transactions.makeMainPenaltyTx(commitTx, localParams.dustLimit, remoteRevocationPubkey, finalScriptPubKey, localParams.toSelfDelay, remoteDelayedPaymentPubkey, feeratePenalty).map(txinfo => { - val sig = keyManager.sign(txinfo, keyManager.revocationPoint(channelKeyPath), remotePerCommitmentSecret, TxOwner.Local, commitmentFormat, Map.empty) - Transactions.addSigs(txinfo, sig) - }) + // Then we punish them by stealing their main output. + val mainPenaltyTx_opt = withTxGenerationLog("main-penalty") { + MainPenaltyTx.createUnsignedTx(commitKeys, revocationKey, commitTx, dustLimit, finalScriptPubKey, toSelfDelay, feeratePenalty, commitmentFormat) } - // we retrieve the information needed to rebuild htlc scripts - val htlcInfos = db.listHtlcInfos(channelId, commitmentNumber) + // We retrieve the historical information needed to rebuild htlc scripts. + val htlcInfos = db.listHtlcInfos(channelParams.channelId, commitmentNumber) log.info("got {} htlcs for commitmentNumber={}", htlcInfos.size, commitmentNumber) - val htlcsRedeemScripts = ( - htlcInfos.map { case (paymentHash, cltvExpiry) => Scripts.htlcReceived(remoteHtlcPubkey, localHtlcPubkey, remoteRevocationPubkey, Crypto.ripemd160(paymentHash), cltvExpiry, commitmentFormat) } ++ - htlcInfos.map { case (paymentHash, _) => Scripts.htlcOffered(remoteHtlcPubkey, localHtlcPubkey, remoteRevocationPubkey, Crypto.ripemd160(paymentHash), commitmentFormat) } - ) - .map(redeemScript => Script.write(pay2wsh(redeemScript)) -> Script.write(redeemScript)) - .toMap - - // and finally we steal the htlc outputs - val htlcPenaltyTxs = commitTx.txOut.zipWithIndex.collect { case (txOut, outputIndex) if htlcsRedeemScripts.contains(txOut.publicKeyScript) => - val htlcRedeemScript = htlcsRedeemScripts(txOut.publicKeyScript) - withTxGenerationLog("htlc-penalty") { - Transactions.makeHtlcPenaltyTx(commitTx, outputIndex, htlcRedeemScript, localParams.dustLimit, finalScriptPubKey, feeratePenalty).map(htlcPenalty => { - val sig = keyManager.sign(htlcPenalty, keyManager.revocationPoint(channelKeyPath), remotePerCommitmentSecret, TxOwner.Local, commitmentFormat, Map.empty) - Transactions.addSigs(htlcPenalty, sig, remoteRevocationPubkey) - }) - } - }.toList.flatten + // And finally we steal the htlc outputs. + val htlcPenaltyTxs = HtlcPenaltyTx.createUnsignedTxs(commitKeys, revocationKey, commitTx, htlcInfos, dustLimit, finalScriptPubKey, feeratePenalty, commitmentFormat) + .flatMap(htlcPenaltyTx => withTxGenerationLog("htlc-penalty")(htlcPenaltyTx)) - RevokedCommitPublished( + val rvk = RevokedCommitPublished( commitTx = commitTx, - claimMainOutputTx = mainTx, - mainPenaltyTx = mainPenaltyTx, - htlcPenaltyTxs = htlcPenaltyTxs, - claimHtlcDelayedPenaltyTxs = Nil, // we will generate and spend those if they publish their HtlcSuccessTx or HtlcTimeoutTx + localOutput_opt = mainTx_opt.map(_.input.outPoint), + remoteOutput_opt = mainPenaltyTx_opt.map(_.input.outPoint), + htlcOutputs = htlcPenaltyTxs.map(_.input.outPoint).toSet, + htlcDelayedOutputs = Set.empty, // we will generate and spend those if their HtlcSuccessTx or HtlcTimeoutTx confirms irrevocablySpent = Map.empty ) + val txs = SecondStageTransactions(mainTx_opt, mainPenaltyTx_opt, htlcPenaltyTxs) + (rvk, txs) } /** @@ -1285,49 +1419,46 @@ object Helpers { * NB: when anchor outputs is used, htlc transactions can be aggregated in a single transaction if they share the same * lockTime (thanks to the use of sighash_single | sighash_anyonecanpay), so we may need to claim multiple outputs. */ - def claimHtlcTxOutputs(keyManager: ChannelKeyManager, params: ChannelParams, remotePerCommitmentSecrets: ShaChain, revokedCommitPublished: RevokedCommitPublished, htlcTx: Transaction, feerates: FeeratesPerKw, finalScriptPubKey: ByteVector)(implicit log: LoggingAdapter): (RevokedCommitPublished, Seq[ClaimHtlcDelayedOutputPenaltyTx]) = { + def claimHtlcTxOutputs(channelParams: ChannelParams, channelKeys: ChannelKeys, remotePerCommitmentSecrets: ShaChain, toSelfDelay: CltvExpiryDelta, commitmentFormat: CommitmentFormat, revokedCommitPublished: RevokedCommitPublished, htlcTx: Transaction, dustLimit: Satoshi, feerates: FeeratesPerKw, finalScriptPubKey: ByteVector)(implicit log: LoggingAdapter): (RevokedCommitPublished, ThirdStageTransactions) = { // We published HTLC-penalty transactions for every HTLC output: this transaction may be ours, or it may be one // of their HTLC transactions that confirmed before our HTLC-penalty transaction. If it is spending an HTLC // output, we assume that it's an HTLC transaction published by our peer and try to create penalty transactions // that spend it, which will automatically be skipped if this was instead one of our HTLC-penalty transactions. - val htlcOutputs = revokedCommitPublished.htlcPenaltyTxs.map(_.input.outPoint).toSet - val spendsHtlcOutput = htlcTx.txIn.exists(txIn => htlcOutputs.contains(txIn.outPoint)) + val spendsHtlcOutput = htlcTx.txIn.exists(txIn => revokedCommitPublished.htlcOutputs.contains(txIn.outPoint)) if (spendsHtlcOutput) { - import params._ - val commitTx = revokedCommitPublished.commitTx - val obscuredTxNumber = Transactions.decodeTxNumber(commitTx.txIn.head.sequence, commitTx.lockTime) - val channelKeyPath = keyManager.keyPath(localParams, channelConfig) - val localPaymentPoint = localParams.walletStaticPaymentBasepoint.getOrElse(keyManager.paymentPoint(channelKeyPath).publicKey) - // If this tx has been published by the remote, we need to invert local/remote params. - val txNumber = Transactions.obscuredCommitTxNumber(obscuredTxNumber, !localParams.isChannelOpener, remoteParams.paymentBasepoint, localPaymentPoint) - // Now we know what commit number this tx is referring to, we can derive the commitment point from the shachain. - remotePerCommitmentSecrets.getHash(0xFFFFFFFFFFFFL - txNumber) - .map(d => PrivateKey(d)) - .map(remotePerCommitmentSecret => { - val remotePerCommitmentPoint = remotePerCommitmentSecret.publicKey - val remoteDelayedPaymentPubkey = Generators.derivePubKey(remoteParams.delayedPaymentBasepoint, remotePerCommitmentPoint) - val remoteRevocationPubkey = Generators.revocationPubKey(keyManager.revocationPoint(channelKeyPath).publicKey, remotePerCommitmentPoint) - // We need to use a high fee when spending HTLC txs because after a delay they can also be spent by the counterparty. - val feeratePerKwPenalty = feerates.fastest - val penaltyTxs = Transactions.makeClaimHtlcDelayedOutputPenaltyTxs(htlcTx, localParams.dustLimit, remoteRevocationPubkey, localParams.toSelfDelay, remoteDelayedPaymentPubkey, finalScriptPubKey, feeratePerKwPenalty).flatMap(claimHtlcDelayedOutputPenaltyTx => { - withTxGenerationLog("htlc-delayed-penalty") { - claimHtlcDelayedOutputPenaltyTx.map(htlcDelayedPenalty => { - val sig = keyManager.sign(htlcDelayedPenalty, keyManager.revocationPoint(channelKeyPath), remotePerCommitmentSecret, TxOwner.Local, commitmentFormat, Map.empty) - val signedTx = Transactions.addSigs(htlcDelayedPenalty, sig) - // We need to make sure that the tx is indeed valid. - Transaction.correctlySpends(signedTx.tx, Seq(htlcTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - log.warning("txId={} is a 2nd level htlc tx spending revoked commit txId={}: publishing htlc-penalty txId={}", htlcTx.txid, revokedCommitPublished.commitTx.txid, signedTx.tx.txid) - signedTx - }) - } - }) - val revokedCommitPublished1 = revokedCommitPublished.copy(claimHtlcDelayedPenaltyTxs = revokedCommitPublished.claimHtlcDelayedPenaltyTxs ++ penaltyTxs) - (revokedCommitPublished1, penaltyTxs) - }).getOrElse((revokedCommitPublished, Nil)) + getRemotePerCommitmentSecret(channelParams, channelKeys, remotePerCommitmentSecrets, revokedCommitPublished.commitTx).map { + case (_, remotePerCommitmentSecret) => + val commitmentKeys = RemoteCommitmentKeys(channelParams, channelKeys, remotePerCommitmentSecret.publicKey) + val revocationKey = channelKeys.revocationKey(remotePerCommitmentSecret) + val penaltyTxs = claimHtlcTxOutputs(commitmentKeys, revocationKey, toSelfDelay, commitmentFormat, htlcTx, dustLimit, feerates, finalScriptPubKey) + val revokedCommitPublished1 = revokedCommitPublished.copy(htlcDelayedOutputs = revokedCommitPublished.htlcDelayedOutputs ++ penaltyTxs.map(_.input.outPoint)) + val txs = ThirdStageTransactions(penaltyTxs) + (revokedCommitPublished1, txs) + }.getOrElse((revokedCommitPublished, ThirdStageTransactions(Nil))) } else { - (revokedCommitPublished, Nil) + (revokedCommitPublished, ThirdStageTransactions(Nil)) } } + + private def claimHtlcTxOutputs(commitmentKeys: RemoteCommitmentKeys, revocationKey: PrivateKey, toSelfDelay: CltvExpiryDelta, commitmentFormat: CommitmentFormat, htlcTx: Transaction, dustLimit: Satoshi, feerates: FeeratesPerKw, finalScriptPubKey: ByteVector)(implicit log: LoggingAdapter): Seq[ClaimHtlcDelayedOutputPenaltyTx] = { + // We need to use a high fee when spending HTLC txs because after a delay they can also be spent by the counterparty. + val feeratePenalty = feerates.fastest + ClaimHtlcDelayedOutputPenaltyTx.createUnsignedTxs(commitmentKeys, revocationKey, htlcTx, dustLimit, toSelfDelay, finalScriptPubKey, feeratePenalty, commitmentFormat).flatMap(penaltyTx => { + withTxGenerationLog("htlc-delayed-penalty")(penaltyTx) + }) + } + + /** + * Claim the outputs of all 2nd-stage HTLC transactions that have been confirmed. + */ + def claimHtlcTxsOutputs(channelParams: ChannelParams, channelKeys: ChannelKeys, remotePerCommitmentSecret: PrivateKey, toSelfDelay: CltvExpiryDelta, commitmentFormat: CommitmentFormat, revokedCommitPublished: RevokedCommitPublished, dustLimit: Satoshi, feerates: FeeratesPerKw, finalScriptPubKey: ByteVector)(implicit log: LoggingAdapter): ThirdStageTransactions = { + val commitmentKeys = RemoteCommitmentKeys(channelParams, channelKeys, remotePerCommitmentSecret.publicKey) + val revocationKey = channelKeys.revocationKey(remotePerCommitmentSecret) + val confirmedHtlcTxs = revokedCommitPublished.htlcOutputs.flatMap(htlcOutput => revokedCommitPublished.irrevocablySpent.get(htlcOutput)) + val penaltyTxs = confirmedHtlcTxs.flatMap(htlcTx => claimHtlcTxOutputs(commitmentKeys, revocationKey, toSelfDelay, commitmentFormat, htlcTx, dustLimit, feerates, finalScriptPubKey)) + ThirdStageTransactions(penaltyTxs.toSeq) + } + } /** @@ -1358,7 +1489,7 @@ object Helpers { val fromRemote = commitment.remoteCommit.spec.htlcs.collect { case IncomingHtlc(add) if add.paymentHash == paymentHash => (add, paymentPreimage) } - val fromNextRemote = commitment.nextRemoteCommit_opt.map(_.commit.spec.htlcs).getOrElse(Set.empty).collect { + val fromNextRemote = commitment.nextRemoteCommit_opt.map(_.spec.htlcs).getOrElse(Set.empty).collect { case IncomingHtlc(add) if add.paymentHash == paymentHash => (add, paymentPreimage) } fromLocal ++ fromRemote ++ fromNextRemote @@ -1370,30 +1501,27 @@ object Helpers { * more htlcs have timed out and need to be failed in an upstream channel. Trimmed htlcs can be failed as soon as * the commitment tx has been confirmed. * - * @param tx a tx that has reached mindepth * @return a set of htlcs that need to be failed upstream */ - def trimmedOrTimedOutHtlcs(commitmentFormat: CommitmentFormat, localCommit: LocalCommit, localCommitPublished: LocalCommitPublished, localDustLimit: Satoshi, tx: Transaction)(implicit log: LoggingAdapter): Set[UpdateAddHtlc] = { - val untrimmedHtlcs = Transactions.trimOfferedHtlcs(localDustLimit, localCommit.spec, commitmentFormat).map(_.add) - if (tx.txid == localCommit.commitTxAndRemoteSig.commitTx.tx.txid) { + def trimmedOrTimedOutHtlcs(channelKeys: ChannelKeys, commitment: FullCommitment, localCommit: LocalCommit, confirmedTx: Transaction)(implicit log: LoggingAdapter): Set[UpdateAddHtlc] = { + if (confirmedTx.txid == localCommit.txId) { // The commitment tx is confirmed: we can immediately fail all dust htlcs (they don't have an output in the tx). + val untrimmedHtlcs = Transactions.trimOfferedHtlcs(commitment.localCommitParams.dustLimit, localCommit.spec, commitment.commitmentFormat).map(_.add) localCommit.spec.htlcs.collect(outgoing) -- untrimmedHtlcs - } else { - // Maybe this is a timeout tx: in that case we can resolve and fail the corresponding htlc. - tx.txIn.flatMap(txIn => localCommitPublished.htlcTxs.get(txIn.outPoint) match { + } else if (confirmedTx.txIn.exists(_.outPoint.txid == localCommit.txId)) { + // The transaction spends the commitment tx: maybe it is a timeout tx, in which case we can resolve and fail the + // corresponding htlc. + val outputs = LocalClose.makeLocalCommitTxOutputs(channelKeys, commitment.localKeys(channelKeys), commitment) + confirmedTx.txIn.filter(_.outPoint.txid == localCommit.txId).flatMap(txIn => outputs(txIn.outPoint.index.toInt) match { // This may also be our peer claiming the HTLC by revealing the preimage: in that case we have already // extracted the preimage with [[extractPreimages]] and relayed it upstream. - case Some(Some(HtlcTimeoutTx(_, _, htlcId, _))) if Scripts.extractPreimagesFromClaimHtlcSuccess(tx).isEmpty => - untrimmedHtlcs.find(_.id == htlcId) match { - case Some(htlc) => - log.info("htlc-timeout tx for htlc #{} paymentHash={} expiry={} has been confirmed (tx={})", htlcId, htlc.paymentHash, tx.lockTime, tx) - Some(htlc) - case None => - log.error("could not find htlc #{} for htlc-timeout tx={}", htlcId, tx) - None - } + case CommitmentOutput.OutHtlc(htlc, _, _) if Scripts.extractPreimagesFromClaimHtlcSuccess(confirmedTx).isEmpty => + log.info("htlc-timeout tx for htlc #{} paymentHash={} expiry={} has been confirmed (tx={})", htlc.add.id, htlc.add.paymentHash, htlc.add.cltvExpiry, confirmedTx) + Some(htlc.add) case _ => None }).toSet + } else { + Set.empty } } @@ -1402,30 +1530,29 @@ object Helpers { * more htlcs have timed out and need to be failed in an upstream channel. Trimmed htlcs can be failed as soon as * the commitment tx has been confirmed. * - * @param tx a tx that has reached mindepth * @return a set of htlcs that need to be failed upstream */ - def trimmedOrTimedOutHtlcs(commitmentFormat: CommitmentFormat, remoteCommit: RemoteCommit, remoteCommitPublished: RemoteCommitPublished, remoteDustLimit: Satoshi, tx: Transaction)(implicit log: LoggingAdapter): Set[UpdateAddHtlc] = { - val untrimmedHtlcs = Transactions.trimReceivedHtlcs(remoteDustLimit, remoteCommit.spec, commitmentFormat).map(_.add) - if (tx.txid == remoteCommit.txid) { + def trimmedOrTimedOutHtlcs(channelKeys: ChannelKeys, commitment: FullCommitment, remoteCommit: RemoteCommit, confirmedTx: Transaction)(implicit log: LoggingAdapter): Set[UpdateAddHtlc] = { + if (confirmedTx.txid == remoteCommit.txId) { // The commitment tx is confirmed: we can immediately fail all dust htlcs (they don't have an output in the tx). + val untrimmedHtlcs = Transactions.trimReceivedHtlcs(commitment.remoteCommitParams.dustLimit, remoteCommit.spec, commitment.commitmentFormat).map(_.add) remoteCommit.spec.htlcs.collect(incoming) -- untrimmedHtlcs - } else { - // Maybe this is a timeout tx: in that case we can resolve and fail the corresponding htlc. - tx.txIn.flatMap(txIn => remoteCommitPublished.claimHtlcTxs.get(txIn.outPoint) match { + } else if (confirmedTx.txIn.exists(_.outPoint.txid == remoteCommit.txId)) { + // The transaction spends the commitment tx: maybe it is a timeout tx, in which case we can resolve and fail the + // corresponding htlc. + val commitKeys = commitment.remoteKeys(channelKeys, remoteCommit.remotePerCommitmentPoint) + val outputs = RemoteClose.makeRemoteCommitTxOutputs(channelKeys, commitKeys, commitment, remoteCommit) + confirmedTx.txIn.filter(_.outPoint.txid == remoteCommit.txId).flatMap(txIn => outputs(txIn.outPoint.index.toInt) match { // This may also be our peer claiming the HTLC by revealing the preimage: in that case we have already // extracted the preimage with [[extractPreimages]] and relayed it upstream. - case Some(Some(ClaimHtlcTimeoutTx(_, _, htlcId, _))) if Scripts.extractPreimagesFromHtlcSuccess(tx).isEmpty => - untrimmedHtlcs.find(_.id == htlcId) match { - case Some(htlc) => - log.info("claim-htlc-timeout tx for htlc #{} paymentHash={} expiry={} has been confirmed (tx={})", htlcId, htlc.paymentHash, tx.lockTime, tx) - Some(htlc) - case None => - log.error("could not find htlc #{} for claim-htlc-timeout tx={}", htlcId, tx) - None - } + // Note: we're looking at the remote commitment, so it's an incoming HTLC for them (outgoing for us). + case CommitmentOutput.InHtlc(htlc, _, _) if Scripts.extractPreimagesFromHtlcSuccess(confirmedTx).isEmpty => + log.info("claim-htlc-timeout tx for htlc #{} paymentHash={} expiry={} has been confirmed (tx={})", htlc.add.id, htlc.add.paymentHash, htlc.add.cltvExpiry, confirmedTx) + Some(htlc.add) case _ => None }).toSet + } else { + Set.empty } } @@ -1436,11 +1563,11 @@ object Helpers { * @param tx a transaction that is sufficiently buried in the blockchain */ def onChainOutgoingHtlcs(localCommit: LocalCommit, remoteCommit: RemoteCommit, nextRemoteCommit_opt: Option[RemoteCommit], tx: Transaction): Set[UpdateAddHtlc] = { - if (localCommit.commitTxAndRemoteSig.commitTx.tx.txid == tx.txid) { + if (localCommit.txId == tx.txid) { localCommit.spec.htlcs.collect(outgoing) - } else if (remoteCommit.txid == tx.txid) { + } else if (remoteCommit.txId == tx.txid) { remoteCommit.spec.htlcs.collect(incoming) - } else if (nextRemoteCommit_opt.map(_.txid).contains(tx.txid)) { + } else if (nextRemoteCommit_opt.map(_.txId).contains(tx.txid)) { nextRemoteCommit_opt.get.spec.htlcs.collect(incoming) } else { Set.empty @@ -1455,10 +1582,10 @@ object Helpers { def overriddenOutgoingHtlcs(d: DATA_CLOSING, tx: Transaction): Set[UpdateAddHtlc] = { val localCommit = d.commitments.latest.localCommit val remoteCommit = d.commitments.latest.remoteCommit - val nextRemoteCommit_opt = d.commitments.latest.nextRemoteCommit_opt.map(_.commit) + val nextRemoteCommit_opt = d.commitments.latest.nextRemoteCommit_opt // NB: from the p.o.v of remote, their incoming htlcs are our outgoing htlcs. val outgoingHtlcs = localCommit.spec.htlcs.collect(outgoing) ++ (remoteCommit.spec.htlcs ++ nextRemoteCommit_opt.map(_.spec.htlcs).getOrElse(Set.empty)).collect(incoming) - if (localCommit.commitTxAndRemoteSig.commitTx.tx.txid == tx.txid) { + if (localCommit.txId == tx.txid) { // Our commit got confirmed: any htlc that is *not* in our commit will never reach the chain. outgoingHtlcs -- localCommit.spec.htlcs.collect(outgoing) } else if (d.revokedCommitPublished.map(_.commitTx.txid).contains(tx.txid)) { @@ -1469,10 +1596,10 @@ object Helpers { // upstream. In the best case scenario, we already fulfilled upstream, then the fail will be a no-op and we // will pocket the htlc amount. outgoingHtlcs - } else if (remoteCommit.txid == tx.txid) { + } else if (remoteCommit.txId == tx.txid) { // Their current commit got confirmed: any htlc that is *not* in their current commit will never reach the chain. outgoingHtlcs -- remoteCommit.spec.htlcs.collect(incoming) - } else if (nextRemoteCommit_opt.map(_.txid).contains(tx.txid)) { + } else if (nextRemoteCommit_opt.map(_.txId).contains(tx.txid)) { // Their next commit got confirmed: any htlc that is *not* in their next commit will never reach the chain. outgoingHtlcs -- nextRemoteCommit_opt.map(_.spec.htlcs).getOrElse(Set.empty).collect(incoming) } else { @@ -1490,18 +1617,15 @@ object Helpers { * * @param tx a transaction that has been irrevocably confirmed */ - def updateLocalCommitPublished(localCommitPublished: LocalCommitPublished, tx: Transaction): LocalCommitPublished = { + def updateIrrevocablySpent(localCommitPublished: LocalCommitPublished, tx: Transaction): LocalCommitPublished = { // even if our txs only have one input, maybe our counterparty uses a different scheme so we need to iterate // over all of them to check if they are relevant val relevantOutpoints = tx.txIn.map(_.outPoint).filter(outPoint => { - // is this the commit tx itself? (we could do this outside of the loop...) val isCommitTx = localCommitPublished.commitTx.txid == tx.txid - // does the tx spend an output of the local commitment tx? - val spendsTheCommitTx = localCommitPublished.commitTx.txid == outPoint.txid - // is the tx one of our 3rd stage delayed txs? (a 3rd stage tx is a tx spending the output of an htlc tx, which - // is itself spending the output of the commitment tx) - val is3rdStageDelayedTx = localCommitPublished.claimHtlcDelayedTxs.map(_.input.outPoint).contains(outPoint) - isCommitTx || spendsTheCommitTx || is3rdStageDelayedTx + val isMainTx = localCommitPublished.localOutput_opt.contains(outPoint) + val isHtlcTx = localCommitPublished.htlcOutputs.contains(outPoint) + val isHtlcDelayedTx = localCommitPublished.htlcDelayedOutputs.contains(outPoint) + isCommitTx || isMainTx || isHtlcTx || isHtlcDelayedTx }) // then we add the relevant outpoints to the map keeping track of which txid spends which outpoint localCommitPublished.copy(irrevocablySpent = localCommitPublished.irrevocablySpent ++ relevantOutpoints.map(o => o -> tx).toMap) @@ -1517,15 +1641,14 @@ object Helpers { * * @param tx a transaction that has been irrevocably confirmed */ - def updateRemoteCommitPublished(remoteCommitPublished: RemoteCommitPublished, tx: Transaction): RemoteCommitPublished = { + def updateIrrevocablySpent(remoteCommitPublished: RemoteCommitPublished, tx: Transaction): RemoteCommitPublished = { // even if our txs only have one input, maybe our counterparty uses a different scheme so we need to iterate // over all of them to check if they are relevant val relevantOutpoints = tx.txIn.map(_.outPoint).filter(outPoint => { - // is this the commit tx itself? (we could do this outside of the loop...) val isCommitTx = remoteCommitPublished.commitTx.txid == tx.txid - // does the tx spend an output of the remote commitment tx? - val spendsTheCommitTx = remoteCommitPublished.commitTx.txid == outPoint.txid - isCommitTx || spendsTheCommitTx + val isMainTx = remoteCommitPublished.localOutput_opt.contains(outPoint) + val isHtlcTx = remoteCommitPublished.htlcOutputs.contains(outPoint) + isCommitTx || isMainTx || isHtlcTx }) // then we add the relevant outpoints to the map keeping track of which txid spends which outpoint remoteCommitPublished.copy(irrevocablySpent = remoteCommitPublished.irrevocablySpent ++ relevantOutpoints.map(o => o -> tx).toMap) @@ -1541,41 +1664,27 @@ object Helpers { * * @param tx a transaction that has been irrevocably confirmed */ - def updateRevokedCommitPublished(revokedCommitPublished: RevokedCommitPublished, tx: Transaction): RevokedCommitPublished = { + def updateIrrevocablySpent(revokedCommitPublished: RevokedCommitPublished, tx: Transaction): RevokedCommitPublished = { // even if our txs only have one input, maybe our counterparty uses a different scheme so we need to iterate // over all of them to check if they are relevant val relevantOutpoints = tx.txIn.map(_.outPoint).filter(outPoint => { - // is this the commit tx itself? (we could do this outside of the loop...) val isCommitTx = revokedCommitPublished.commitTx.txid == tx.txid - // does the tx spend an output of the remote commitment tx? - val spendsTheCommitTx = revokedCommitPublished.commitTx.txid == outPoint.txid - // is the tx one of our 3rd stage delayed txs? (a 3rd stage tx is a tx spending the output of an htlc tx, which - // is itself spending the output of the commitment tx) - val is3rdStageDelayedTx = revokedCommitPublished.claimHtlcDelayedPenaltyTxs.map(_.input.outPoint).contains(outPoint) - isCommitTx || spendsTheCommitTx || is3rdStageDelayedTx + val isMainTx = revokedCommitPublished.localOutput_opt.contains(outPoint) + val isMainPenaltyTx = revokedCommitPublished.remoteOutput_opt.contains(outPoint) + val isHtlcPenaltyTx = revokedCommitPublished.htlcOutputs.contains(outPoint) + val isHtlcDelayedPenaltyTx = revokedCommitPublished.htlcDelayedOutputs.contains(outPoint) + isCommitTx || isMainTx || isMainPenaltyTx || isHtlcPenaltyTx || isHtlcDelayedPenaltyTx }) // then we add the relevant outpoints to the map keeping track of which txid spends which outpoint revokedCommitPublished.copy(irrevocablySpent = revokedCommitPublished.irrevocablySpent ++ relevantOutpoints.map(o => o -> tx).toMap) } - /** - * This helper function tells if some of the utxos consumed by the given transaction have already been irrevocably spent (possibly by this very transaction). - * - * It can be useful to: - * - not attempt to publish this tx when we know this will fail - * - not watch for confirmations if we know the tx is already confirmed - * - not watch the corresponding utxo when we already know the final spending tx - * - * @param tx an arbitrary transaction - * @param irrevocablySpent a map of known spent outpoints - * @return true if we know for sure that the utxos consumed by the tx have already irrevocably been spent, false otherwise - */ - def inputsAlreadySpent(tx: Transaction, irrevocablySpent: Map[OutPoint, Transaction]): Boolean = { - tx.txIn.exists(txIn => irrevocablySpent.contains(txIn.outPoint)) - } - - def inputAlreadySpent(input: OutPoint, irrevocablySpent: Map[OutPoint, Transaction]): Boolean = { - irrevocablySpent.contains(input) + /** Returns the amount we've successfully claimed from a force-closed channel. */ + def closingBalance(closingScript: ByteVector, commit: CommitPublished): Satoshi = { + commit.irrevocablySpent.values.flatMap(_.txOut) + .filter(_.publicKeyScript == closingScript) + .map(_.amount) + .sum } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Monitoring.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Monitoring.scala index 748751968c..648a0ce927 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Monitoring.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Monitoring.scala @@ -35,6 +35,7 @@ object Monitoring { val RemoteFeeratePerByte = Kamon.histogram("channels.remote-feerate-per-byte") val Splices = Kamon.histogram("channels.splices", "Splices") val ProcessMessage = Kamon.timer("channels.messages-processed") + val HtlcDropped = Kamon.counter("channels.htlc-dropped") def recordHtlcsInFlight(remoteSpec: CommitmentSpec, previousRemoteSpec: CommitmentSpec): Unit = { for (direction <- Tags.Directions.Incoming :: Tags.Directions.Outgoing :: Nil) { @@ -75,6 +76,10 @@ object Monitoring { Metrics.Splices.withTag(Tags.Origin, Tags.Origins.Remote).withTag(Tags.SpliceType, Tags.SpliceTypes.SpliceCpfp).record(Math.abs(fundingParams.remoteContribution.toLong)) } } + + def dropHtlc(reason: ChannelException, direction: String): Unit = { + HtlcDropped.withTag(Tags.Reason, reason.getClass.getSimpleName).withTag(Tags.Direction, direction).increment() + } } object Tags { @@ -85,6 +90,7 @@ object Monitoring { val State = "state" val CommitmentFormat = "commitment-format" val SpliceType = "splice-type" + val Reason = "reason" object Events { val Created = "created" diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala index 3e2e1bd78e..84ac61dce2 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala @@ -21,6 +21,7 @@ import akka.actor.typed.scaladsl.adapter.{ClassicActorContextOps, actorRefAdapte import akka.actor.{Actor, ActorContext, ActorRef, FSM, OneForOneStrategy, PossiblyHarmful, Props, SupervisorStrategy, typed} import akka.event.Logging.MDC import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} +import fr.acinq.bitcoin.scalacompat.Musig2.{IndividualNonce, LocalNonce} import fr.acinq.bitcoin.scalacompat.{ByteVector32, Satoshi, SatoshiLong, Transaction, TxId} import fr.acinq.eclair.Logs.LogCategory import fr.acinq.eclair._ @@ -38,17 +39,19 @@ import fr.acinq.eclair.channel.Monitoring.{Metrics, Tags} import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.fund.InteractiveTxBuilder._ import fr.acinq.eclair.channel.fund.{InteractiveTxBuilder, InteractiveTxFunder, InteractiveTxSigningSession} -import fr.acinq.eclair.channel.publish.TxPublisher -import fr.acinq.eclair.channel.publish.TxPublisher.{PublishFinalTx, PublishReplaceableTx, SetChannelId} -import fr.acinq.eclair.crypto.keymanager.ChannelKeyManager +import fr.acinq.eclair.channel.publish.TxPublisher.{PublishReplaceableTx, SetChannelId} +import fr.acinq.eclair.channel.publish._ +import fr.acinq.eclair.crypto.NonceGenerator +import fr.acinq.eclair.crypto.keymanager.ChannelKeys import fr.acinq.eclair.db.DbEventHandler.ChannelEvent.EventType import fr.acinq.eclair.db.PendingCommandsDb import fr.acinq.eclair.io.Peer import fr.acinq.eclair.io.Peer.LiquidityPurchaseSigned import fr.acinq.eclair.payment.relay.Relayer import fr.acinq.eclair.payment.{Bolt11Invoice, PaymentSettlingOnChain} +import fr.acinq.eclair.reputation.Reputation import fr.acinq.eclair.router.Announcements -import fr.acinq.eclair.transactions.Transactions.ClosingTx +import fr.acinq.eclair.transactions.Transactions._ import fr.acinq.eclair.transactions._ import fr.acinq.eclair.wire.protocol._ @@ -106,12 +109,31 @@ object Channel { remoteRbfLimits: RemoteRbfLimits, quiescenceTimeout: FiniteDuration, balanceThresholds: Seq[BalanceThreshold], - minTimeBetweenUpdates: FiniteDuration, - acceptIncomingStaticRemoteKeyChannels: Boolean) { + minTimeBetweenUpdates: FiniteDuration) { require(0 <= maxHtlcValueInFlightPercent && maxHtlcValueInFlightPercent <= 100, "max-htlc-value-in-flight-percent must be between 0 and 100") require(balanceThresholds.sortBy(_.available) == balanceThresholds, "channel-update.balance-thresholds must be sorted by available-sat") def minFundingSatoshis(flags: ChannelFlags): Satoshi = if (flags.announceChannel) minFundingPublicSatoshis else minFundingPrivateSatoshis + + def maxHtlcValueInFlight(fundingAmount: Satoshi, unlimited: Boolean): UInt64 = { + if (unlimited) { + // We don't want to impose limits on the amount in flight, typically to allow fully emptying the channel. + UInt64.MaxValue + } else { + // NB: when we're the initiator, we don't know yet if the remote peer will contribute to the funding amount, so + // the percentage-based value may be underestimated. That's ok, this is a security parameter so it makes sense to + // base it on the amount that we're contributing instead of the total funding amount. + UInt64(maxHtlcValueInFlightMsat.min(fundingAmount * maxHtlcValueInFlightPercent / 100).toLong) + } + } + + def commitParams(fundingAmount: Satoshi, unlimitedMaxHtlcValueInFlight: Boolean): ProposedCommitParams = ProposedCommitParams( + localDustLimit = dustLimit, + localHtlcMinimum = htlcMinimum, + localMaxHtlcValueInFlight = maxHtlcValueInFlight(fundingAmount, unlimitedMaxHtlcValueInFlight), + localMaxAcceptedHtlcs = maxAcceptedHtlcs, + toRemoteDelay = toRemoteDelay, + ) } trait TxPublisherFactory { @@ -124,8 +146,8 @@ object Channel { } } - def props(nodeParams: NodeParams, wallet: OnChainChannelFunder with OnChainPubkeyCache, remoteNodeId: PublicKey, blockchain: typed.ActorRef[ZmqWatcher.Command], relayer: ActorRef, txPublisherFactory: TxPublisherFactory): Props = - Props(new Channel(nodeParams, wallet, remoteNodeId, blockchain, relayer, txPublisherFactory)) + def props(nodeParams: NodeParams, channelKeys: ChannelKeys, wallet: OnChainChannelFunder with OnChainAddressCache, remoteNodeId: PublicKey, blockchain: typed.ActorRef[ZmqWatcher.Command], relayer: ActorRef, txPublisherFactory: TxPublisherFactory): Props = + Props(new Channel(nodeParams, channelKeys, wallet, remoteNodeId, blockchain, relayer, txPublisherFactory)) // https://github.com/lightningnetwork/lightning-rfc/blob/master/02-peer-protocol.md#requirements val MAX_FUNDING_WITHOUT_WUMBO: Satoshi = 16777216 sat // = 2^24 @@ -189,7 +211,7 @@ object Channel { } -class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with OnChainPubkeyCache, val remoteNodeId: PublicKey, val blockchain: typed.ActorRef[ZmqWatcher.Command], val relayer: ActorRef, val txPublisherFactory: Channel.TxPublisherFactory)(implicit val ec: ExecutionContext = ExecutionContext.Implicits.global) +class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wallet: OnChainChannelFunder with OnChainAddressCache, val remoteNodeId: PublicKey, val blockchain: typed.ActorRef[ZmqWatcher.Command], val relayer: ActorRef, val txPublisherFactory: Channel.TxPublisherFactory)(implicit val ec: ExecutionContext = ExecutionContext.Implicits.global) extends FSM[ChannelState, ChannelData] with FSMDiagnosticActorLogging[ChannelState, ChannelData] with ChannelOpenSingleFunded @@ -199,7 +221,14 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with import Channel._ - val keyManager: ChannelKeyManager = nodeParams.channelKeyManager + // Remote nonces that must be used when signing the next remote commitment transaction (one per active commitment). + var remoteNextCommitNonces: Map[TxId, IndividualNonce] = Map.empty + + // Closee nonces are first exchanged in shutdown messages, and replaced by a new nonce after each closing_sig. + var localCloseeNonce_opt: Option[LocalNonce] = None + var remoteCloseeNonce_opt: Option[IndividualNonce] = None + // Closer nonces are randomly generated when sending our closing_complete. + var localCloserNonces_opt: Option[CloserNonces] = None // we pass these to helpers classes so that they have the logging context implicit def implicitLog: akka.event.DiagnosticLoggingAdapter = diagLog @@ -213,8 +242,6 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with // choose to not make this an Option (that would be None before the first connection), and instead embrace the fact // that the active connection may point to dead letters at all time var activeConnection = context.system.deadLetters - // we aggregate sigs for splices before processing - var sigStash = Seq.empty[CommitSig] // we stash announcement_signatures if we receive them earlier than expected var announcementSigsStash = Map.empty[RealShortChannelId, AnnouncementSignatures] // we record the announcement_signatures messages we already sent to avoid unnecessary retransmission @@ -278,7 +305,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with goto(WAIT_FOR_INIT_SINGLE_FUNDED_CHANNEL) } - case Event(input: INPUT_INIT_CHANNEL_NON_INITIATOR, Nothing) if !input.localParams.isChannelOpener => + case Event(input: INPUT_INIT_CHANNEL_NON_INITIATOR, Nothing) if !input.localChannelParams.isChannelOpener => activeConnection = input.remote txPublisher ! SetChannelId(remoteNodeId, input.temporaryChannelId) if (input.dualFunded) { @@ -291,7 +318,6 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with log.debug("restoring channel") context.system.eventStream.publish(ChannelRestored(self, data.channelId, peer, remoteNodeId, data)) txPublisher ! SetChannelId(remoteNodeId, data.channelId) - // We watch all unconfirmed funding txs, whatever our state is. // There can be multiple funding txs due to rbf, and they can be unconfirmed in any state due to zero-conf. // To avoid a herd effect on restart, we add a delay before watching funding txs. @@ -320,17 +346,17 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with watchFundingConfirmed(commitment.fundingTxId, Some(nodeParams.channelConf.minDepth), herdDelay_opt) case fundingTx: LocalFundingStatus.DualFundedUnconfirmedFundingTx => publishFundingTx(fundingTx) - val minDepth_opt = data.commitments.params.minDepth(nodeParams.channelConf.minDepth) + val minDepth_opt = data.commitments.channelParams.minDepth(nodeParams.channelConf.minDepth) watchFundingConfirmed(fundingTx.sharedTx.txId, minDepth_opt, herdDelay_opt) case fundingTx: LocalFundingStatus.ZeroconfPublishedFundingTx => watchFundingConfirmed(fundingTx.tx.txid, Some(nodeParams.channelConf.minDepth), herdDelay_opt) case _: LocalFundingStatus.ConfirmedFundingTx => data match { - case closing: DATA_CLOSING if Closing.nothingAtStake(closing) || Closing.isClosingTypeAlreadyKnown(closing).isDefined => - // no need to do anything - () + case closing: DATA_CLOSING if Closing.nothingAtStake(closing) => () + // No need to watch the funding tx, it has already been spent and the spending tx has already reached mindepth. + case closing: DATA_CLOSING if Closing.isClosingTypeAlreadyKnown(closing).isDefined => () + // In all other cases we need to be ready for any type of closing. case closing: DATA_CLOSING => - // in all other cases we need to be ready for any type of closing watchFundingSpent(commitment, closing.spendingTxs.map(_.txid).toSet, herdDelay_opt) case negotiating: DATA_NEGOTIATING => val closingTxs = negotiating.closingTxProposed.flatten.map(_.unsignedTx.tx.txid).toSet @@ -346,40 +372,81 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with } } } - + // We resume the channel and re-publish closing transactions if it was closing. data match { // NB: order matters! case closing: DATA_CLOSING if Closing.nothingAtStake(closing) => log.info("we have nothing at stake, going straight to CLOSED") context.system.eventStream.publish(ChannelAborted(self, remoteNodeId, closing.channelId)) - goto(CLOSED) using closing + goto(CLOSED) using IgnoreClosedData(closing) case closing: DATA_CLOSING => - val localPaysClosingFees = closing.commitments.params.localParams.paysClosingFees - // we don't put back the WatchSpent if the commitment tx has already been published and the spending tx already reached mindepth + val localPaysClosingFees = closing.commitments.localChannelParams.paysClosingFees val closingType_opt = Closing.isClosingTypeAlreadyKnown(closing) log.info(s"channel is closing (closingType=${closingType_opt.map(c => EventType.Closed(c).label).getOrElse("UnknownYet")})") - // if the closing type is known: - // - there is no need to watch the funding tx because it has already been spent and the spending tx has already reached mindepth - // - there is no need to attempt to publish transactions for other type of closes - // - there is a single commitment, the others have all been invalidated + // If the closing type is known: + // - there is no need to attempt to publish transactions for other type of closes + // - there may be 3rd-stage transactions to publish + // - there is a single commitment, the others have all been invalidated + val commitment = closing.commitments.latest + val closingFeerate = nodeParams.onChainFeeConf.getClosingFeerate(nodeParams.currentBitcoinCoreFeerates, closing.maxClosingFeerate_opt) closingType_opt match { case Some(c: Closing.MutualClose) => doPublish(c.tx, localPaysClosingFees) case Some(c: Closing.LocalClose) => - doPublish(c.localCommitPublished, closing.commitments.latest) + val (_, secondStageTransactions) = Closing.LocalClose.claimCommitTxOutputs(channelKeys, commitment, c.localCommitPublished.commitTx, closingFeerate, closing.finalScriptPubKey, nodeParams.onChainFeeConf.spendAnchorWithoutHtlcs) + doPublish(c.localCommitPublished, secondStageTransactions, commitment) + val thirdStageTransactions = Closing.LocalClose.claimHtlcDelayedOutputs(c.localCommitPublished, channelKeys, commitment, closingFeerate, closing.finalScriptPubKey) + doPublish(c.localCommitPublished, thirdStageTransactions) case Some(c: Closing.RemoteClose) => - doPublish(c.remoteCommitPublished, closing.commitments.latest) + val (_, secondStageTransactions) = Closing.RemoteClose.claimCommitTxOutputs(channelKeys, commitment, c.remoteCommit, c.remoteCommitPublished.commitTx, closingFeerate, closing.finalScriptPubKey, nodeParams.onChainFeeConf.spendAnchorWithoutHtlcs) + doPublish(c.remoteCommitPublished, secondStageTransactions, commitment) case Some(c: Closing.RecoveryClose) => - doPublish(c.remoteCommitPublished, closing.commitments.latest) + // We cannot do anything in that case: we've already published our recovery transaction before restarting, + // and must wait for it to confirm. + doPublish(c.remoteCommitPublished, Closing.RemoteClose.SecondStageTransactions(None, None, Nil), commitment) case Some(c: Closing.RevokedClose) => - doPublish(c.revokedCommitPublished) + Closing.RevokedClose.getRemotePerCommitmentSecret(closing.commitments.channelParams, channelKeys, closing.commitments.remotePerCommitmentSecrets, c.revokedCommitPublished.commitTx).foreach { + case (commitmentNumber, remotePerCommitmentSecret) => + // TODO: once we allow changing the commitment format or to_self_delay during a splice, those values may be incorrect. + val toSelfDelay = closing.commitments.latest.remoteCommitParams.toSelfDelay + val commitmentFormat = closing.commitments.latest.commitmentFormat + val dustLimit = closing.commitments.latest.localCommitParams.dustLimit + val (_, secondStageTransactions) = Closing.RevokedClose.claimCommitTxOutputs(closing.commitments.channelParams, channelKeys, c.revokedCommitPublished.commitTx, commitmentNumber, remotePerCommitmentSecret, toSelfDelay, commitmentFormat, nodeParams.db.channels, dustLimit, nodeParams.currentBitcoinCoreFeerates, nodeParams.onChainFeeConf, closing.finalScriptPubKey) + doPublish(c.revokedCommitPublished, secondStageTransactions) + val thirdStageTransactions = Closing.RevokedClose.claimHtlcTxsOutputs(closing.commitments.channelParams, channelKeys, remotePerCommitmentSecret, toSelfDelay, commitmentFormat, c.revokedCommitPublished, dustLimit, nodeParams.currentBitcoinCoreFeerates, closing.finalScriptPubKey) + doPublish(c.revokedCommitPublished, thirdStageTransactions) + } case None => + // The closing type isn't known yet: + // - we publish transactions for all types of closes that we detected + // - there may be other commitments, but we'll adapt if we receive WatchAlternativeCommitTxConfirmedTriggered + // - there cannot be 3rd-stage transactions yet, no need to re-compute them closing.mutualClosePublished.foreach(mcp => doPublish(mcp, localPaysClosingFees)) - closing.localCommitPublished.foreach(lcp => doPublish(lcp, closing.commitments.latest)) - closing.remoteCommitPublished.foreach(rcp => doPublish(rcp, closing.commitments.latest)) - closing.nextRemoteCommitPublished.foreach(rcp => doPublish(rcp, closing.commitments.latest)) - closing.revokedCommitPublished.foreach(doPublish) - closing.futureRemoteCommitPublished.foreach(rcp => doPublish(rcp, closing.commitments.latest)) + closing.localCommitPublished.foreach(lcp => { + val (_, secondStageTransactions) = Closing.LocalClose.claimCommitTxOutputs(channelKeys, commitment, lcp.commitTx, closingFeerate, closing.finalScriptPubKey, nodeParams.onChainFeeConf.spendAnchorWithoutHtlcs) + doPublish(lcp, secondStageTransactions, commitment) + }) + closing.remoteCommitPublished.foreach(rcp => { + val (_, secondStageTransactions) = Closing.RemoteClose.claimCommitTxOutputs(channelKeys, commitment, commitment.remoteCommit, rcp.commitTx, closingFeerate, closing.finalScriptPubKey, nodeParams.onChainFeeConf.spendAnchorWithoutHtlcs) + doPublish(rcp, secondStageTransactions, commitment) + }) + closing.nextRemoteCommitPublished.foreach(rcp => { + val remoteCommit = commitment.nextRemoteCommit_opt.get + val (_, secondStageTransactions) = Closing.RemoteClose.claimCommitTxOutputs(channelKeys, commitment, remoteCommit, rcp.commitTx, closingFeerate, closing.finalScriptPubKey, nodeParams.onChainFeeConf.spendAnchorWithoutHtlcs) + doPublish(rcp, secondStageTransactions, commitment) + }) + closing.revokedCommitPublished.foreach(rvk => { + Closing.RevokedClose.getRemotePerCommitmentSecret(closing.commitments.channelParams, channelKeys, closing.commitments.remotePerCommitmentSecrets, rvk.commitTx).foreach { + case (commitmentNumber, remotePerCommitmentSecret) => + // TODO: once we allow changing the commitment format or to_self_delay during a splice, those values may be incorrect. + val toSelfDelay = closing.commitments.latest.remoteCommitParams.toSelfDelay + val commitmentFormat = closing.commitments.latest.commitmentFormat + val dustLimit = closing.commitments.latest.localCommitParams.dustLimit + val (_, secondStageTransactions) = Closing.RevokedClose.claimCommitTxOutputs(closing.commitments.channelParams, channelKeys, rvk.commitTx, commitmentNumber, remotePerCommitmentSecret, toSelfDelay, commitmentFormat, nodeParams.db.channels, dustLimit, nodeParams.currentBitcoinCoreFeerates, nodeParams.onChainFeeConf, closing.finalScriptPubKey) + doPublish(rvk, secondStageTransactions) + } + }) + closing.futureRemoteCommitPublished.foreach(rcp => doPublish(rcp, Closing.RemoteClose.SecondStageTransactions(None, None, Nil), commitment)) } // no need to go OFFLINE, we can directly switch to CLOSING goto(CLOSING) using closing @@ -463,22 +530,25 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with handleAddHtlcCommandError(c, error, Some(d.channelUpdate)) case Event(c: CMD_ADD_HTLC, d: DATA_NORMAL) => - d.commitments.sendAdd(c, nodeParams.currentBlockHeight, nodeParams.channelConf, nodeParams.currentBitcoinCoreFeerates, nodeParams.onChainFeeConf) match { + d.commitments.sendAdd(c, nodeParams.currentBlockHeight, nodeParams.channelConf, nodeParams.onChainFeeConf) match { case Right((commitments1, add)) => if (c.commit) self ! CMD_SIGN() context.system.eventStream.publish(AvailableBalanceChanged(self, d.channelId, d.aliases, commitments1, d.lastAnnouncement_opt)) + val relayFee = nodeFee(d.channelUpdate.relayFees, add.amountMsat) + context.system.eventStream.publish(OutgoingHtlcAdded(add, remoteNodeId, c.origin.upstream, relayFee)) + log.info("OutgoingHtlcAdded: channelId={}, id={}, endorsement={}, remoteNodeId={}, upstream={}, fee={}, now={}, blockHeight={}, expiry={}", Array(add.channelId.toHex, add.id, add.endorsement, remoteNodeId.toHex, c.origin.upstream.toString, relayFee, TimestampMilli.now().toLong, nodeParams.currentBlockHeight.toLong, add.cltvExpiry)) handleCommandSuccess(c, d.copy(commitments = commitments1)) sending add case Left(cause) => handleAddHtlcCommandError(c, cause, Some(d.channelUpdate)) } case Event(add: UpdateAddHtlc, d: DATA_NORMAL) => - d.commitments.receiveAdd(add, nodeParams.currentBitcoinCoreFeerates, nodeParams.onChainFeeConf) match { + d.commitments.receiveAdd(add) match { case Right(commitments1) => stay() using d.copy(commitments = commitments1) case Left(cause) => handleLocalError(cause, d, Some(add)) } case Event(c: CMD_FULFILL_HTLC, d: DATA_NORMAL) => - d.commitments.sendFulfill(c) match { + d.commitments.sendFulfill(c, nodeParams.privateKey, nodeParams.features.hasFeature(Features.AttributionData)) match { case Right((commitments1, fulfill)) => if (c.commit) self ! CMD_SIGN() context.system.eventStream.publish(AvailableBalanceChanged(self, d.channelId, d.aliases, commitments1, d.lastAnnouncement_opt)) @@ -493,6 +563,8 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with case Right((commitments1, origin, htlc)) => // we forward preimages as soon as possible to the upstream channel because it allows us to pull funds relayer ! RES_ADD_SETTLED(origin, htlc, HtlcResult.RemoteFulfill(fulfill)) + context.system.eventStream.publish(OutgoingHtlcFulfilled(fulfill)) + log.info("OutgoingHtlcFulfilled: channelId={}, id={}", fulfill.channelId.toHex, fulfill.id) stay() using d.copy(commitments = commitments1) case Left(cause) => handleLocalError(cause, d, Some(fulfill)) } @@ -503,7 +575,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with log.debug("delaying CMD_FAIL_HTLC with id={} for {}", c.id, delay) context.system.scheduler.scheduleOnce(delay, self, c.copy(delay_opt = None)) stay() - case None => d.commitments.sendFail(c, nodeParams.privateKey) match { + case None => d.commitments.sendFail(c, nodeParams.privateKey, nodeParams.features.hasFeature(Features.AttributionData)) match { case Right((commitments1, fail)) => if (c.commit) self ! CMD_SIGN() context.system.eventStream.publish(AvailableBalanceChanged(self, d.channelId, d.aliases, commitments1, d.lastAnnouncement_opt)) @@ -526,12 +598,16 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with } case Event(fail: UpdateFailHtlc, d: DATA_NORMAL) => + context.system.eventStream.publish(OutgoingHtlcFailed(fail)) + log.info("OutgoingHtlcFailed: channelId={}, id={}", fail.channelId.toHex, fail.id) d.commitments.receiveFail(fail) match { case Right((commitments1, _, _)) => stay() using d.copy(commitments = commitments1) case Left(cause) => handleLocalError(cause, d, Some(fail)) } case Event(fail: UpdateFailMalformedHtlc, d: DATA_NORMAL) => + context.system.eventStream.publish(OutgoingHtlcFailed(fail)) + log.info("OutgoingHtlcFailed: channelId={}, id={}", fail.channelId.toHex, fail.id) d.commitments.receiveFailMalformed(fail) match { case Right((commitments1, _, _)) => stay() using d.copy(commitments = commitments1) case Left(cause) => handleLocalError(cause, d, Some(fail)) @@ -558,16 +634,16 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with log.debug("ignoring CMD_SIGN (nothing to sign)") stay() case Right(_) => - d.commitments.sendCommit(keyManager) match { + d.commitments.sendCommit(channelKeys, remoteNextCommitNonces) match { case Right((commitments1, commit)) => log.debug("sending a new sig, spec:\n{}", commitments1.latest.specs2String) - val nextRemoteCommit = commitments1.latest.nextRemoteCommit_opt.get.commit + val nextRemoteCommit = commitments1.latest.nextRemoteCommit_opt.get val nextCommitNumber = nextRemoteCommit.index - // we persist htlc data in order to be able to claim htlc outputs in case a revoked tx is published by our - // counterparty, so only htlcs above remote's dust_limit matter - val trimmedHtlcs = Transactions.trimOfferedHtlcs(d.commitments.params.remoteParams.dustLimit, nextRemoteCommit.spec, commitments1.params.commitmentFormat) ++ - Transactions.trimReceivedHtlcs(commitments1.params.remoteParams.dustLimit, nextRemoteCommit.spec, commitments1.params.commitmentFormat) - trimmedHtlcs.map(_.add).foreach { htlc => + // We persist htlc data in order to be able to claim htlc outputs in case a revoked tx is published by our + // counterparty, so only htlcs above remote's dust_limit matter. + val trimmedOfferedHtlcs = d.commitments.active.flatMap(c => Transactions.trimOfferedHtlcs(c.remoteCommitParams.dustLimit, nextRemoteCommit.spec, c.commitmentFormat)).map(_.add).toSet + val trimmedReceivedHtlcs = d.commitments.active.flatMap(c => Transactions.trimReceivedHtlcs(c.remoteCommitParams.dustLimit, nextRemoteCommit.spec, c.commitmentFormat)).map(_.add).toSet + (trimmedOfferedHtlcs ++ trimmedReceivedHtlcs).foreach { htlc => log.debug(s"adding paymentHash=${htlc.paymentHash} cltvExpiry=${htlc.cltvExpiry} to htlcs db for commitNumber=$nextCommitNumber") nodeParams.db.channels.addHtlcInfo(d.channelId, nextCommitNumber, htlc.paymentHash, htlc.cltvExpiry) } @@ -583,77 +659,67 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with stay() } - case Event(commit: CommitSig, d: DATA_NORMAL) => - aggregateSigs(commit) match { - case Some(sigs) => - d.spliceStatus match { - case s: SpliceStatus.SpliceInProgress => - log.debug("received their commit_sig, deferring message") - stay() using d.copy(spliceStatus = s.copy(remoteCommitSig = Some(commit))) - case SpliceStatus.SpliceAborted => - log.warning("received commit_sig after sending tx_abort, they probably sent it before receiving our tx_abort, ignoring...") - stay() - case SpliceStatus.SpliceWaitingForSigs(signingSession) => - signingSession.receiveCommitSig(nodeParams, d.commitments.params, commit) match { - case Left(f) => - rollbackFundingAttempt(signingSession.fundingTx.tx, Nil) - stay() using d.copy(spliceStatus = SpliceStatus.SpliceAborted) sending TxAbort(d.channelId, f.getMessage) - case Right(signingSession1) => signingSession1 match { - case signingSession1: InteractiveTxSigningSession.WaitingForSigs => - // In theory we don't have to store their commit_sig here, as they would re-send it if we disconnect, but - // it is more consistent with the case where we send our tx_signatures first. - val d1 = d.copy(spliceStatus = SpliceStatus.SpliceWaitingForSigs(signingSession1)) - stay() using d1 storing() - case signingSession1: InteractiveTxSigningSession.SendingSigs => - // We don't have their tx_sigs, but they have ours, and could publish the funding tx without telling us. - // That's why we move on immediately to the next step, and will update our unsigned funding tx when we - // receive their tx_sigs. - val minDepth_opt = d.commitments.params.minDepth(nodeParams.channelConf.minDepth) - watchFundingConfirmed(signingSession.fundingTx.txId, minDepth_opt, delay_opt = None) - val commitments1 = d.commitments.add(signingSession1.commitment) - val d1 = d.copy(commitments = commitments1, spliceStatus = SpliceStatus.NoSplice) - stay() using d1 storing() sending signingSession1.localSigs calling endQuiescence(d1) - } + case Event(commit: CommitSigs, d: DATA_NORMAL) => + (d.spliceStatus, commit) match { + case (s: SpliceStatus.SpliceInProgress, sig: CommitSig) => + log.debug("received their commit_sig, deferring message") + stay() using d.copy(spliceStatus = s.copy(remoteCommitSig = Some(sig))) + case (SpliceStatus.SpliceAborted, _: CommitSig) => + log.warning("received commit_sig after sending tx_abort, they probably sent it before receiving our tx_abort, ignoring...") + stay() + case (SpliceStatus.SpliceWaitingForSigs(signingSession), sig: CommitSig) => + signingSession.receiveCommitSig(d.commitments.channelParams, channelKeys, sig, nodeParams.currentBlockHeight) match { + case Left(f) => + rollbackFundingAttempt(signingSession.fundingTx.tx, Nil) + stay() using d.copy(spliceStatus = SpliceStatus.SpliceAborted) sending TxAbort(d.channelId, f.getMessage) + case Right(signingSession1) => signingSession1 match { + case signingSession1: InteractiveTxSigningSession.WaitingForSigs => + // In theory we don't have to store their commit_sig here, as they would re-send it if we disconnect, but + // it is more consistent with the case where we send our tx_signatures first. + val d1 = d.copy(spliceStatus = SpliceStatus.SpliceWaitingForSigs(signingSession1)) + stay() using d1 storing() + case signingSession1: InteractiveTxSigningSession.SendingSigs => + // We don't have their tx_sigs, but they have ours, and could publish the funding tx without telling us. + // That's why we move on immediately to the next step, and will update our unsigned funding tx when we + // receive their tx_sigs. + val minDepth_opt = d.commitments.channelParams.minDepth(nodeParams.channelConf.minDepth) + watchFundingConfirmed(signingSession.fundingTx.txId, minDepth_opt, delay_opt = None) + val commitments1 = d.commitments.add(signingSession1.commitment) + val d1 = d.copy(commitments = commitments1, spliceStatus = SpliceStatus.NoSplice) + stay() using d1 storing() sending signingSession1.localSigs calling endQuiescence(d1) + } + } + case _ => + // NB: in all other cases we process the commit_sigs normally. We could do a full pattern matching on all + // splice statuses, but it would force us to handle every corner case where our peer doesn't behave correctly + // whereas they will all simply lead to a force-close. + d.commitments.receiveCommit(commit, channelKeys) match { + case Right((commitments1, revocation)) => + log.debug("received a new sig, spec:\n{}", commitments1.latest.specs2String) + if (commitments1.changes.localHasChanges) { + // if we have newly acknowledged changes let's sign them + self ! CMD_SIGN() } - case _ if d.commitments.ignoreRetransmittedCommitSig(commit) => - // We haven't received our peer's tx_signatures for the latest funding transaction and asked them to resend it on reconnection. - // They also resend their corresponding commit_sig, but we have already received it so we should ignore it. - // Note that the funding transaction may have confirmed while we were reconnecting. - log.info("ignoring commit_sig, we're still waiting for tx_signatures") - stay() - case _ => - // NB: in all other cases we process the commit_sig normally. We could do a full pattern matching on all - // splice statuses, but it would force us to handle corner cases like race condition between splice_init - // and a non-splice commit_sig - d.commitments.receiveCommit(sigs, keyManager) match { - case Right((commitments1, revocation)) => - log.debug("received a new sig, spec:\n{}", commitments1.latest.specs2String) - if (commitments1.changes.localHasChanges) { - // if we have newly acknowledged changes let's sign them - self ! CMD_SIGN() - } - if (d.commitments.availableBalanceForSend != commitments1.availableBalanceForSend) { - // we send this event only when our balance changes - context.system.eventStream.publish(AvailableBalanceChanged(self, d.channelId, d.aliases, commitments1, d.lastAnnouncement_opt)) - } - context.system.eventStream.publish(ChannelSignatureReceived(self, commitments1)) - // If we're now quiescent, we may send our stfu message. - val (d1, toSend) = d.spliceStatus match { - case SpliceStatus.NegotiatingQuiescence(cmd_opt, QuiescenceNegotiation.Initiator.QuiescenceRequested) if commitments1.localIsQuiescent => - val stfu = Stfu(d.channelId, initiator = true) - val spliceStatus1 = SpliceStatus.NegotiatingQuiescence(cmd_opt, QuiescenceNegotiation.Initiator.SentStfu(stfu)) - (d.copy(commitments = commitments1, spliceStatus = spliceStatus1), Seq(revocation, stfu)) - case SpliceStatus.NegotiatingQuiescence(_, _: QuiescenceNegotiation.NonInitiator.ReceivedStfu) if commitments1.localIsQuiescent => - val stfu = Stfu(d.channelId, initiator = false) - (d.copy(commitments = commitments1, spliceStatus = SpliceStatus.NonInitiatorQuiescent), Seq(revocation, stfu)) - case _ => - (d.copy(commitments = commitments1), Seq(revocation)) - } - stay() using d1 storing() sending toSend - case Left(cause) => handleLocalError(cause, d, Some(commit)) + if (d.commitments.availableBalanceForSend != commitments1.availableBalanceForSend) { + // we send this event only when our balance changes + context.system.eventStream.publish(AvailableBalanceChanged(self, d.channelId, d.aliases, commitments1, d.lastAnnouncement_opt)) } + context.system.eventStream.publish(ChannelSignatureReceived(self, commitments1)) + // If we're now quiescent, we may send our stfu message. + val (d1, toSend) = d.spliceStatus match { + case SpliceStatus.NegotiatingQuiescence(cmd_opt, QuiescenceNegotiation.Initiator.QuiescenceRequested) if commitments1.localIsQuiescent => + val stfu = Stfu(d.channelId, initiator = true) + val spliceStatus1 = SpliceStatus.NegotiatingQuiescence(cmd_opt, QuiescenceNegotiation.Initiator.SentStfu(stfu)) + (d.copy(commitments = commitments1, spliceStatus = spliceStatus1), Seq(revocation, stfu)) + case SpliceStatus.NegotiatingQuiescence(_, _: QuiescenceNegotiation.NonInitiator.ReceivedStfu) if commitments1.localIsQuiescent => + val stfu = Stfu(d.channelId, initiator = false) + (d.copy(commitments = commitments1, spliceStatus = SpliceStatus.NonInitiatorQuiescent), Seq(revocation, stfu)) + case _ => + (d.copy(commitments = commitments1), Seq(revocation)) + } + stay() using d1 storing() sending toSend + case Left(cause) => handleLocalError(cause, d, Some(commit)) } - case None => stay() } case Event(revocation: RevokeAndAck, d: DATA_NORMAL) => @@ -662,15 +728,17 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with d.commitments.receiveRevocation(revocation, nodeParams.onChainFeeConf.feerateToleranceFor(remoteNodeId).dustTolerance.maxExposure) match { case Right((commitments1, actions)) => cancelTimer(RevocationTimeout.toString) + remoteNextCommitNonces = revocation.nextCommitNonces log.debug("received a new rev, spec:\n{}", commitments1.latest.specs2String) actions.foreach { case PostRevocationAction.RelayHtlc(add) => log.debug("forwarding incoming htlc {} to relayer", add) - relayer ! Relayer.RelayForward(add, remoteNodeId) + relayer ! Relayer.RelayForward(add, remoteNodeId, Reputation.incomingOccupancy(commitments1)) case PostRevocationAction.RejectHtlc(add) => log.debug("rejecting incoming htlc {}", add) // NB: we don't set commit = true, we will sign all updates at once afterwards. - self ! CMD_FAIL_HTLC(add.id, FailureReason.LocalFailure(TemporaryChannelFailure(Some(d.channelUpdate))), commit = true) + val attribution = FailureAttributionData(htlcReceivedAt = TimestampMilli.now(), trampolineReceivedAt_opt = None) + self ! CMD_FAIL_HTLC(add.id, FailureReason.LocalFailure(TemporaryChannelFailure(Some(d.channelUpdate))), Some(attribution), commit = true) case PostRevocationAction.RelayFailure(result) => log.debug("forwarding {} to relayer", result) relayer ! result @@ -681,7 +749,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with if (d.remoteShutdown.isDefined && !commitments1.changes.localHasUnsignedOutgoingHtlcs) { // we were waiting for our pending htlcs to be signed before replying with our local shutdown val finalScriptPubKey = getOrGenerateFinalScriptPubKey(d) - val localShutdown = Shutdown(d.channelId, finalScriptPubKey) + val localShutdown = createShutdown(d.commitments, finalScriptPubKey) // this should always be defined, we provide a fallback for backward compat with older channels val closeStatus = d.closeStatus_opt.getOrElse(CloseStatus.NonInitiator(None)) // note: it means that we had pending htlcs to sign, therefore we go to SHUTDOWN, not to NEGOTIATING @@ -705,16 +773,16 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with handleCommandError(CannotCloseWithUnsignedOutgoingUpdateFee(d.channelId), c) } else { val localScriptPubKey = c.scriptPubKey.getOrElse(getOrGenerateFinalScriptPubKey(d)) - d.commitments.params.validateLocalShutdownScript(localScriptPubKey) match { + d.commitments.channelParams.validateLocalShutdownScript(localScriptPubKey) match { case Left(e) => handleCommandError(e, c) case Right(localShutdownScript) => - val shutdown = Shutdown(d.channelId, localShutdownScript) + val shutdown = createShutdown(d.commitments, localShutdownScript) handleCommandSuccess(c, d.copy(localShutdown = Some(shutdown), closeStatus_opt = Some(CloseStatus.Initiator(c.feerates)))) storing() sending shutdown } } case Event(remoteShutdown@Shutdown(_, remoteScriptPubKey, _), d: DATA_NORMAL) => - d.commitments.params.validateRemoteShutdownScript(remoteScriptPubKey) match { + d.commitments.channelParams.validateRemoteShutdownScript(remoteScriptPubKey) match { case Left(e) => log.warning(s"they sent an invalid closing script: ${e.getMessage}") context.system.scheduler.scheduleOnce(2 second, peer, Peer.Disconnect(remoteNodeId)) @@ -734,6 +802,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with // we did not send a shutdown message // there are pending signed changes => go to SHUTDOWN // there are no htlcs => go to NEGOTIATING + remoteCloseeNonce_opt = remoteShutdown.closeeNonce_opt if (d.commitments.changes.remoteHasUnsignedOutgoingHtlcs) { handleLocalError(CannotCloseWithUnsignedOutgoingHtlcs(d.channelId), d, Some(remoteShutdown)) } else if (d.commitments.changes.remoteHasUnsignedOutgoingUpdateFee) { @@ -751,13 +820,14 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with } // in the meantime we won't send new changes stay() using d.copy(remoteShutdown = Some(remoteShutdown), closeStatus_opt = Some(CloseStatus.NonInitiator(None))) + } else if (d.commitments.latest.commitmentFormat.isInstanceOf[TaprootCommitmentFormat] && remoteShutdown.closeeNonce_opt.isEmpty) { + handleLocalError(MissingClosingNonce(d.channelId), d, Some(remoteShutdown)) } else { // so we don't have any unsigned outgoing changes val (localShutdown, sendList) = d.localShutdown match { - case Some(localShutdown) => - (localShutdown, Nil) + case Some(localShutdown) => (localShutdown, Nil) case None => - val localShutdown = Shutdown(d.channelId, getOrGenerateFinalScriptPubKey(d)) + val localShutdown = createShutdown(d.commitments, getOrGenerateFinalScriptPubKey(d)) // we need to send our shutdown if we didn't previously (localShutdown, localShutdown :: Nil) } @@ -770,12 +840,12 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with // are there pending signed changes on either side? we need to have received their last revocation! if (d.commitments.hasNoPendingHtlcsOrFeeUpdate) { // there are no pending signed changes, let's directly negotiate a closing transaction - if (Features.canUseFeature(d.commitments.params.localParams.initFeatures, d.commitments.params.remoteParams.initFeatures, Features.SimpleClose)) { + if (Features.canUseFeature(d.commitments.localChannelParams.initFeatures, d.commitments.remoteChannelParams.initFeatures, Features.SimpleClose)) { val (d1, closingComplete_opt) = startSimpleClose(d.commitments, localShutdown, remoteShutdown, closeStatus) goto(NEGOTIATING_SIMPLE) using d1 storing() sending sendList ++ closingComplete_opt.toSeq - } else if (d.commitments.params.localParams.paysClosingFees) { + } else if (d.commitments.localChannelParams.paysClosingFees) { // we pay the closing fees, so we initiate the negotiation by sending the first closing_signed - val (closingTx, closingSigned) = MutualClose.makeFirstClosingTx(keyManager, d.commitments.latest, localShutdown.scriptPubKey, remoteShutdownScript, nodeParams.currentFeeratesForFundingClosing, nodeParams.onChainFeeConf, closeStatus.feerates_opt) + val (closingTx, closingSigned) = MutualClose.makeFirstClosingTx(channelKeys, d.commitments.latest, localShutdown.scriptPubKey, remoteShutdownScript, nodeParams.currentFeeratesForFundingClosing, nodeParams.onChainFeeConf, closeStatus.feerates_opt) goto(NEGOTIATING) using DATA_NEGOTIATING(d.commitments, localShutdown, remoteShutdown, List(List(ClosingTxProposed(closingTx, closingSigned))), bestUnpublishedClosingTx_opt = None) storing() sending sendList :+ closingSigned } else { // we are not the channel initiator, will wait for their closing_signed @@ -790,7 +860,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with case Event(ProcessCurrentBlockHeight(c), d: DATA_NORMAL) => handleNewBlock(c, d) - case Event(c: CurrentFeerates.BitcoinCore, d: DATA_NORMAL) => handleCurrentFeerate(c, d) + case Event(c: CurrentFeerates.BitcoinCore, d: DATA_NORMAL) => handleCurrentFeerate(d) case Event(_: ChannelReady, d: DATA_NORMAL) => // After a reconnection, if the channel hasn't been used yet, our peer cannot be sure we received their channel_ready @@ -811,7 +881,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with case Some(c) if d.lastAnnouncement_opt.exists(_.shortChannelId == remoteAnnSigs.shortChannelId) => if (!announcementSigsSent.contains(remoteAnnSigs.shortChannelId)) { log.info("re-sending announcement_signatures for scid={}", remoteAnnSigs.shortChannelId) - val localAnnSigs_opt = c.signAnnouncement(nodeParams, d.commitments.params) + val localAnnSigs_opt = c.signAnnouncement(nodeParams, d.commitments.channelParams, channelKeys.fundingKey(c.fundingTxIndex)) localAnnSigs_opt.foreach(annSigs => announcementSigsSent += annSigs.shortChannelId) stay() sending localAnnSigs_opt.toSeq } else { @@ -819,17 +889,17 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with stay() } case Some(c) => - c.signAnnouncement(nodeParams, d.commitments.params) match { + c.signAnnouncement(nodeParams, d.commitments.channelParams, channelKeys.fundingKey(c.fundingTxIndex)) match { case Some(localAnnSigs) => - val fundingPubKey = keyManager.fundingPublicKey(d.commitments.params.localParams.fundingKeyPath, c.fundingTxIndex) - val channelAnn = Announcements.makeChannelAnnouncement(nodeParams.chainHash, localAnnSigs.shortChannelId, nodeParams.nodeId, remoteNodeId, fundingPubKey.publicKey, c.remoteFundingPubKey, localAnnSigs.nodeSignature, remoteAnnSigs.nodeSignature, localAnnSigs.bitcoinSignature, remoteAnnSigs.bitcoinSignature) + val fundingPubKey = channelKeys.fundingKey(c.fundingTxIndex).publicKey + val channelAnn = Announcements.makeChannelAnnouncement(nodeParams.chainHash, localAnnSigs.shortChannelId, nodeParams.nodeId, remoteNodeId, fundingPubKey, c.remoteFundingPubKey, localAnnSigs.nodeSignature, remoteAnnSigs.nodeSignature, localAnnSigs.bitcoinSignature, remoteAnnSigs.bitcoinSignature) if (!Announcements.checkSigs(channelAnn)) { handleLocalError(InvalidAnnouncementSignatures(d.channelId, remoteAnnSigs), d, Some(remoteAnnSigs)) } else { log.info("announcing channelId={} on the network with shortChannelId={} for fundingTxIndex={}", d.channelId, localAnnSigs.shortChannelId, c.fundingTxIndex) // We generate a new channel_update because we can now use the scid of the announced funding transaction. val scidForChannelUpdate = Helpers.scidForChannelUpdate(Some(channelAnn), d.aliases.localAlias) - val channelUpdate = Announcements.makeChannelUpdate(nodeParams, remoteNodeId, scidForChannelUpdate, d.commitments.params, d.channelUpdate.relayFees, Helpers.maxHtlcAmount(nodeParams, d.commitments), enable = true) + val channelUpdate = Helpers.channelUpdate(nodeParams, scidForChannelUpdate, d.commitments, d.channelUpdate.relayFees, enable = true) context.system.eventStream.publish(ShortChannelIdAssigned(self, d.channelId, Some(channelAnn), d.aliases, remoteNodeId)) // We use goto() instead of stay() because we want to fire transitions. goto(NORMAL) using d.copy(lastAnnouncement_opt = Some(channelAnn), channelUpdate = channelUpdate) storing() @@ -851,7 +921,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with } case Event(c: CMD_UPDATE_RELAY_FEE, d: DATA_NORMAL) => - val channelUpdate1 = Announcements.makeChannelUpdate(nodeParams, remoteNodeId, scidForChannelUpdate(d), d.commitments.params, Relayer.RelayFees(c.feeBase, c.feeProportionalMillionths), Helpers.maxHtlcAmount(nodeParams, d.commitments), enable = true) + val channelUpdate1 = Helpers.channelUpdate(nodeParams, scidForChannelUpdate(d), d.commitments, Relayer.RelayFees(c.feeBase, c.feeProportionalMillionths), enable = true) log.debug(s"updating relay fees: prev={} next={}", d.channelUpdate.toStringShort, channelUpdate1.toStringShort) val replyTo = if (c.replyTo == ActorRef.noSender) sender() else c.replyTo replyTo ! RES_SUCCESS(c, d.channelId) @@ -860,7 +930,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with case Event(BroadcastChannelUpdate(reason), d: DATA_NORMAL) => val age = TimestampSecond.now() - d.channelUpdate.timestamp - val channelUpdate1 = Announcements.makeChannelUpdate(nodeParams, remoteNodeId, scidForChannelUpdate(d), d.commitments.params, d.channelUpdate.relayFees, Helpers.maxHtlcAmount(nodeParams, d.commitments), enable = true) + val channelUpdate1 = Helpers.channelUpdate(nodeParams, scidForChannelUpdate(d), d.commitments, d.channelUpdate.relayFees, enable = true) reason match { case Reconnected if d.commitments.announceChannel && Announcements.areSame(channelUpdate1, d.channelUpdate) && age < REFRESH_CHANNEL_UPDATE_INTERVAL => // we already sent an identical channel_update not long ago (flapping protection in case we keep being disconnected/reconnected) @@ -882,7 +952,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with } case Event(cmd: CMD_SPLICE, d: DATA_NORMAL) => - if (!d.commitments.params.remoteParams.initFeatures.hasFeature(Features.SplicePrototype)) { + if (!d.commitments.remoteChannelParams.initFeatures.hasFeature(Features.SplicePrototype)) { log.warning("cannot initiate splice, peer doesn't support splicing") cmd.replyTo ! RES_FAILURE(cmd, CommandUnavailableInThisState(d.channelId, "splice", NORMAL)) stay() @@ -971,7 +1041,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with stay() using d.copy(spliceStatus = SpliceStatus.NegotiatingQuiescence(None, QuiescenceNegotiation.NonInitiator.ReceivedStfu(msg))) case SpliceStatus.NegotiatingQuiescence(Some(cmd), QuiescenceNegotiation.Initiator.SentStfu(_)) => // If both sides send stfu at the same time, the quiescence initiator is the channel opener. - if (!msg.initiator || d.commitments.params.localParams.isChannelOpener) { + if (!msg.initiator || d.commitments.localChannelParams.isChannelOpener) { cmd match { case cmd: CMD_SPLICE => initiateSplice(cmd, d) match { case Left(f) => @@ -1026,32 +1096,46 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with stay() using d.copy(spliceStatus = SpliceStatus.SpliceAborted) sending TxAbort(d.channelId, InvalidSpliceWithUnconfirmedTx(d.channelId, d.commitments.latest.fundingTxId).getMessage) } else { val parentCommitment = d.commitments.latest.commitment - val localFundingPubKey = nodeParams.channelKeyManager.fundingPublicKey(d.commitments.params.localParams.fundingKeyPath, parentCommitment.fundingTxIndex + 1).publicKey - val fundingScript = Funding.makeFundingPubKeyScript(localFundingPubKey, msg.fundingPubKey) + val localFundingPubKey = channelKeys.fundingKey(parentCommitment.fundingTxIndex + 1).publicKey + val fundingScript = Transactions.makeFundingScript(localFundingPubKey, msg.fundingPubKey, parentCommitment.commitmentFormat).pubkeyScript LiquidityAds.validateRequest(nodeParams.privateKey, d.channelId, fundingScript, msg.feerate, isChannelCreation = false, msg.requestFunding_opt, nodeParams.liquidityAdsConfig.rates_opt, msg.useFeeCredit_opt) match { case Left(t) => log.warning("rejecting splice request with invalid liquidity ads: {}", t.getMessage) stay() using d.copy(spliceStatus = SpliceStatus.SpliceAborted) sending TxAbort(d.channelId, t.getMessage) case Right(willFund_opt) => log.info(s"accepting splice with remote.in.amount=${msg.fundingContribution} remote.in.push=${msg.pushAmount}") + // We only support updating phoenix channels to taproot: we ignore other attempts at upgrading the + // commitment format and will simply apply the previous commitment format. + val nextCommitmentFormat = msg.channelType_opt match { + case Some(ChannelTypes.SimpleTaprootChannelsPhoenix) if parentCommitment.commitmentFormat == UnsafeLegacyAnchorOutputsCommitmentFormat => + log.info("accepting upgrade to {} during splice from commitment format {}", ChannelTypes.SimpleTaprootChannelsPhoenix, parentCommitment.commitmentFormat) + PhoenixSimpleTaprootChannelCommitmentFormat + case Some(channelType) => + log.info("rejecting upgrade to {} during splice from commitment format {}", channelType, parentCommitment.commitmentFormat) + parentCommitment.commitmentFormat + case _ => + parentCommitment.commitmentFormat + } val spliceAck = SpliceAck(d.channelId, fundingContribution = willFund_opt.map(_.purchase.amount).getOrElse(0 sat), fundingPubKey = localFundingPubKey, pushAmount = 0.msat, requireConfirmedInputs = nodeParams.channelConf.requireConfirmedInputsForDualFunding, willFund_opt = willFund_opt.map(_.willFund), - feeCreditUsed_opt = msg.useFeeCredit_opt + feeCreditUsed_opt = msg.useFeeCredit_opt, + channelType_opt = msg.channelType_opt ) val fundingParams = InteractiveTxParams( channelId = d.channelId, isInitiator = false, localContribution = spliceAck.fundingContribution, remoteContribution = msg.fundingContribution, - sharedInput_opt = Some(Multisig2of2Input(parentCommitment)), + sharedInput_opt = Some(SharedFundingInput(channelKeys, parentCommitment)), remoteFundingPubKey = msg.fundingPubKey, localOutputs = Nil, + commitmentFormat = nextCommitmentFormat, lockTime = msg.lockTime, - dustLimit = d.commitments.params.localParams.dustLimit.max(d.commitments.params.remoteParams.dustLimit), + dustLimit = parentCommitment.localCommitParams.dustLimit.max(parentCommitment.remoteCommitParams.dustLimit), targetFeerate = msg.feerate, requireConfirmedInputs = RequireConfirmedInputs(forLocal = msg.requireConfirmedInputs, forRemote = spliceAck.requireConfirmedInputs) ) @@ -1059,7 +1143,10 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with val txBuilder = context.spawnAnonymous(InteractiveTxBuilder( sessionId, nodeParams, fundingParams, - channelParams = d.commitments.params, + channelParams = d.commitments.channelParams, + localCommitParams = parentCommitment.localCommitParams, + remoteCommitParams = parentCommitment.remoteCommitParams, + channelKeys = channelKeys, purpose = InteractiveTxBuilder.SpliceTx(parentCommitment, d.commitments.changes), localPushAmount = spliceAck.pushAmount, remotePushAmount = msg.pushAmount, liquidityPurchase_opt = willFund_opt.map(_.purchase), @@ -1085,20 +1172,27 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with case SpliceStatus.SpliceRequested(cmd, spliceInit) => log.info("our peer accepted our splice request and will contribute {} to the funding transaction", msg.fundingContribution) val parentCommitment = d.commitments.latest.commitment + // We only support updating phoenix channels to taproot: we ignore other attempts at upgrading the + // commitment format and will simply apply the previous commitment format. + val nextCommitmentFormat = msg.channelType_opt match { + case Some(ChannelTypes.SimpleTaprootChannelsPhoenix) if parentCommitment.commitmentFormat == UnsafeLegacyAnchorOutputsCommitmentFormat => PhoenixSimpleTaprootChannelCommitmentFormat + case _ => parentCommitment.commitmentFormat + } val fundingParams = InteractiveTxParams( channelId = d.channelId, isInitiator = true, localContribution = spliceInit.fundingContribution, remoteContribution = msg.fundingContribution, - sharedInput_opt = Some(Multisig2of2Input(parentCommitment)), + sharedInput_opt = Some(SharedFundingInput(channelKeys, parentCommitment)), remoteFundingPubKey = msg.fundingPubKey, localOutputs = cmd.spliceOutputs, + commitmentFormat = nextCommitmentFormat, lockTime = spliceInit.lockTime, - dustLimit = d.commitments.params.localParams.dustLimit.max(d.commitments.params.remoteParams.dustLimit), + dustLimit = parentCommitment.localCommitParams.dustLimit.max(parentCommitment.remoteCommitParams.dustLimit), targetFeerate = spliceInit.feerate, requireConfirmedInputs = RequireConfirmedInputs(forLocal = msg.requireConfirmedInputs, forRemote = spliceInit.requireConfirmedInputs) ) - val fundingScript = Funding.makeFundingPubKeyScript(spliceInit.fundingPubKey, msg.fundingPubKey) + val fundingScript = Transactions.makeFundingScript(spliceInit.fundingPubKey, msg.fundingPubKey, parentCommitment.commitmentFormat).pubkeyScript LiquidityAds.validateRemoteFunding(spliceInit.requestFunding_opt, remoteNodeId, d.channelId, fundingScript, msg.fundingContribution, spliceInit.feerate, isChannelCreation = false, msg.willFund_opt) match { case Left(t) => log.info("rejecting splice attempt: invalid liquidity ads response ({})", t.getMessage) @@ -1109,7 +1203,10 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with val txBuilder = context.spawnAnonymous(InteractiveTxBuilder( sessionId, nodeParams, fundingParams, - channelParams = d.commitments.params, + channelParams = d.commitments.channelParams, + localCommitParams = parentCommitment.localCommitParams, + remoteCommitParams = parentCommitment.remoteCommitParams, + channelKeys = channelKeys, purpose = InteractiveTxBuilder.SpliceTx(parentCommitment, d.commitments.changes), localPushAmount = cmd.pushAmount, remotePushAmount = msg.pushAmount, liquidityPurchase_opt = liquidityPurchase_opt, @@ -1149,7 +1246,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with log.info("rejecting rbf attempt: last attempt was less than {} blocks ago", nodeParams.channelConf.remoteRbfLimits.attemptDeltaBlocks) stay() using d.copy(spliceStatus = SpliceStatus.SpliceAborted) sending TxAbort(d.channelId, InvalidRbfAttemptTooSoon(d.channelId, rbf.latestFundingTx.createdAt, rbf.latestFundingTx.createdAt + nodeParams.channelConf.remoteRbfLimits.attemptDeltaBlocks).getMessage) case Right(rbf) => - val fundingScript = d.commitments.latest.commitInput.txOut.publicKeyScript + val fundingScript = d.commitments.latest.commitInput(channelKeys).txOut.publicKeyScript LiquidityAds.validateRequest(nodeParams.privateKey, d.channelId, fundingScript, msg.feerate, isChannelCreation = false, msg.requestFunding_opt, nodeParams.liquidityAdsConfig.rates_opt, feeCreditUsed_opt = None) match { case Left(t) => log.warning("rejecting rbf request with invalid liquidity ads: {}", t.getMessage) @@ -1165,9 +1262,10 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with isInitiator = false, localContribution = fundingContribution, remoteContribution = msg.fundingContribution, - sharedInput_opt = Some(Multisig2of2Input(rbf.parentCommitment)), + sharedInput_opt = Some(SharedFundingInput(channelKeys, rbf.parentCommitment)), remoteFundingPubKey = rbf.latestFundingTx.fundingParams.remoteFundingPubKey, localOutputs = rbf.latestFundingTx.fundingParams.localOutputs, + commitmentFormat = rbf.latestFundingTx.fundingParams.commitmentFormat, lockTime = msg.lockTime, dustLimit = rbf.latestFundingTx.fundingParams.dustLimit, targetFeerate = msg.feerate, @@ -1177,7 +1275,10 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with val txBuilder = context.spawnAnonymous(InteractiveTxBuilder( sessionId, nodeParams, fundingParams, - channelParams = d.commitments.params, + channelParams = d.commitments.channelParams, + localCommitParams = rbf.parentCommitment.localCommitParams, + remoteCommitParams = rbf.parentCommitment.remoteCommitParams, + channelKeys = channelKeys, purpose = rbf, localPushAmount = 0 msat, remotePushAmount = 0 msat, willFund_opt.map(_.purchase), @@ -1205,7 +1306,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with case SpliceStatus.RbfRequested(cmd, txInitRbf) => getSpliceRbfContext(Some(cmd), d) match { case Right(rbf) => - val fundingScript = d.commitments.latest.commitInput.txOut.publicKeyScript + val fundingScript = d.commitments.latest.commitInput(channelKeys).txOut.publicKeyScript LiquidityAds.validateRemoteFunding(cmd.requestFunding_opt, remoteNodeId, d.channelId, fundingScript, msg.fundingContribution, txInitRbf.feerate, isChannelCreation = false, msg.willFund_opt) match { case Left(t) => log.info("rejecting rbf attempt: invalid liquidity ads response ({})", t.getMessage) @@ -1218,9 +1319,10 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with isInitiator = true, localContribution = txInitRbf.fundingContribution, remoteContribution = msg.fundingContribution, - sharedInput_opt = Some(Multisig2of2Input(rbf.parentCommitment)), + sharedInput_opt = Some(SharedFundingInput(channelKeys, rbf.parentCommitment)), remoteFundingPubKey = rbf.latestFundingTx.fundingParams.remoteFundingPubKey, localOutputs = rbf.latestFundingTx.fundingParams.localOutputs, + commitmentFormat = rbf.latestFundingTx.fundingParams.commitmentFormat, lockTime = txInitRbf.lockTime, dustLimit = rbf.latestFundingTx.fundingParams.dustLimit, targetFeerate = txInitRbf.feerate, @@ -1230,7 +1332,10 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with val txBuilder = context.spawnAnonymous(InteractiveTxBuilder( sessionId, nodeParams, fundingParams, - channelParams = d.commitments.params, + channelParams = d.commitments.channelParams, + localCommitParams = rbf.parentCommitment.localCommitParams, + remoteCommitParams = rbf.parentCommitment.remoteCommitParams, + channelKeys = channelKeys, purpose = rbf, localPushAmount = 0 msat, remotePushAmount = 0 msat, liquidityPurchase_opt = liquidityPurchase_opt, @@ -1295,12 +1400,13 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with log.info("ignoring outgoing interactive-tx message {} from previous session", msg.getClass.getSimpleName) stay() } - case InteractiveTxBuilder.Succeeded(signingSession, commitSig, liquidityPurchase_opt) => + case InteractiveTxBuilder.Succeeded(signingSession, commitSig, liquidityPurchase_opt, nextRemoteCommitNonce_opt) => log.info(s"splice tx created with fundingTxIndex=${signingSession.fundingTxIndex} fundingTxId=${signingSession.fundingTx.txId}") + nextRemoteCommitNonce_opt.foreach { case (txId, nonce) => remoteNextCommitNonces = remoteNextCommitNonces + (txId -> nonce) } cmd_opt.foreach(cmd => cmd.replyTo ! RES_SPLICE(fundingTxIndex = signingSession.fundingTxIndex, signingSession.fundingTx.txId, signingSession.fundingParams.fundingAmount, signingSession.localCommit.fold(_.spec, _.spec).toLocal)) remoteCommitSig_opt.foreach(self ! _) liquidityPurchase_opt.collect { - case purchase if !signingSession.fundingParams.isInitiator => peer ! LiquidityPurchaseSigned(d.channelId, signingSession.fundingTx.txId, signingSession.fundingTxIndex, d.commitments.params.remoteParams.htlcMinimum, purchase) + case purchase if !signingSession.fundingParams.isInitiator => peer ! LiquidityPurchaseSigned(d.channelId, signingSession.fundingTx.txId, signingSession.fundingTxIndex, signingSession.remoteCommitParams.htlcMinimum, purchase) } val d1 = d.copy(spliceStatus = SpliceStatus.SpliceWaitingForSigs(signingSession)) stay() using d1 storing() sending commitSig @@ -1319,7 +1425,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with d.commitments.latest.localFundingStatus match { case dfu@LocalFundingStatus.DualFundedUnconfirmedFundingTx(fundingTx: PartiallySignedSharedTransaction, _, _, _) if fundingTx.txId == msg.txId => // we already sent our tx_signatures - InteractiveTxSigningSession.addRemoteSigs(keyManager, d.commitments.params, dfu.fundingParams, fundingTx, msg) match { + InteractiveTxSigningSession.addRemoteSigs(channelKeys, dfu.fundingParams, fundingTx, msg) match { case Left(cause) => log.warning("received invalid tx_signatures for fundingTxId={}: {}", msg.txId, cause.getMessage) // The funding transaction may still confirm (since our peer should be able to generate valid signatures), @@ -1340,12 +1446,12 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with d.spliceStatus match { case SpliceStatus.SpliceWaitingForSigs(signingSession) => // we have not yet sent our tx_signatures - signingSession.receiveTxSigs(nodeParams, d.commitments.params, msg) match { + signingSession.receiveTxSigs(channelKeys, msg, nodeParams.currentBlockHeight) match { case Left(f) => rollbackFundingAttempt(signingSession.fundingTx.tx, previousTxs = Seq.empty) // no splice rbf yet stay() using d.copy(spliceStatus = SpliceStatus.SpliceAborted) sending TxAbort(d.channelId, f.getMessage) case Right(signingSession1) => - val minDepth_opt = d.commitments.params.minDepth(nodeParams.channelConf.minDepth) + val minDepth_opt = d.commitments.channelParams.minDepth(nodeParams.channelConf.minDepth) watchFundingConfirmed(signingSession.fundingTx.txId, minDepth_opt, delay_opt = None) val commitments1 = d.commitments.add(signingSession1.commitment) val d1 = d.copy(commitments = commitments1, spliceStatus = SpliceStatus.NoSplice) @@ -1387,7 +1493,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with // If the channel is public and we've received the remote splice_locked, we send our announcement_signatures // in order to generate the channel_announcement. val remoteLocked = commitment.fundingTxIndex == 0 || d.commitments.all.exists(c => c.fundingTxId == commitment.fundingTxId && c.remoteFundingStatus == RemoteFundingStatus.Locked) - val localAnnSigs_opt = if (d.commitments.announceChannel && remoteLocked) commitment.signAnnouncement(nodeParams, commitments1.params) else None + val localAnnSigs_opt = if (d.commitments.announceChannel && remoteLocked) commitment.signAnnouncement(nodeParams, commitments1.channelParams, channelKeys.fundingKey(commitment.fundingTxIndex)) else None localAnnSigs_opt match { case Some(localAnnSigs) => announcementSigsSent += localAnnSigs.shortChannelId @@ -1422,7 +1528,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with None } // If the commitment is confirmed, we were waiting to receive the remote splice_locked before sending our announcement_signatures. - val localAnnSigs_opt = commitment.signAnnouncement(nodeParams, commitments1.params) match { + val localAnnSigs_opt = commitment.signAnnouncement(nodeParams, commitments1.channelParams, channelKeys.fundingKey(commitment.fundingTxIndex)) match { case Some(localAnnSigs) if !announcementSigsSent.contains(localAnnSigs.shortChannelId) => announcementSigsSent += localAnnSigs.shortChannelId // If we've already received the remote announcement_signatures, we're now ready to process them. @@ -1450,7 +1556,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with // if we have pending unsigned htlcs, then we cancel them and generate an update with the disabled flag set, that will be returned to the sender in a temporary channel failure if (d.commitments.changes.localChanges.proposed.collectFirst { case add: UpdateAddHtlc => add }.isDefined) { log.debug("updating channel_update announcement (reason=disabled)") - val channelUpdate1 = Announcements.makeChannelUpdate(nodeParams, remoteNodeId, scidForChannelUpdate(d), d.commitments.params, d.channelUpdate.relayFees, Helpers.maxHtlcAmount(nodeParams, d.commitments), enable = false) + val channelUpdate1 = Helpers.channelUpdate(nodeParams, scidForChannelUpdate(d), d.commitments, d.channelUpdate.relayFees, enable = false) // NB: the htlcs stay in the commitments.localChange, they will be cleaned up after reconnection d.commitments.changes.localChanges.proposed.collect { case add: UpdateAddHtlc => relayer ! RES_ADD_SETTLED(d.commitments.originChannels(add.id), add, HtlcResult.DisconnectedBeforeSigned(channelUpdate1)) @@ -1477,7 +1583,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with when(SHUTDOWN)(handleExceptions { case Event(c: CMD_FULFILL_HTLC, d: DATA_SHUTDOWN) => - d.commitments.sendFulfill(c) match { + d.commitments.sendFulfill(c, nodeParams.privateKey, nodeParams.features.hasFeature(Features.AttributionData)) match { case Right((commitments1, fulfill)) => if (c.commit) self ! CMD_SIGN() handleCommandSuccess(c, d.copy(commitments = commitments1)) sending fulfill @@ -1496,7 +1602,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with } case Event(c: CMD_FAIL_HTLC, d: DATA_SHUTDOWN) => - d.commitments.sendFail(c, nodeParams.privateKey) match { + d.commitments.sendFail(c, nodeParams.privateKey, nodeParams.features.hasFeature(Features.AttributionData)) match { case Right((commitments1, fail)) => if (c.commit) self ! CMD_SIGN() handleCommandSuccess(c, d.copy(commitments = commitments1)) sending fail @@ -1548,16 +1654,16 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with log.debug("ignoring CMD_SIGN (nothing to sign)") stay() case Right(_) => - d.commitments.sendCommit(keyManager) match { + d.commitments.sendCommit(channelKeys, remoteNextCommitNonces) match { case Right((commitments1, commit)) => log.debug("sending a new sig, spec:\n{}", commitments1.latest.specs2String) - val nextRemoteCommit = commitments1.latest.nextRemoteCommit_opt.get.commit + val nextRemoteCommit = commitments1.latest.nextRemoteCommit_opt.get val nextCommitNumber = nextRemoteCommit.index - // we persist htlc data in order to be able to claim htlc outputs in case a revoked tx is published by our - // counterparty, so only htlcs above remote's dust_limit matter - val trimmedHtlcs = Transactions.trimOfferedHtlcs(d.commitments.params.remoteParams.dustLimit, nextRemoteCommit.spec, d.commitments.params.commitmentFormat) ++ - Transactions.trimReceivedHtlcs(d.commitments.params.remoteParams.dustLimit, nextRemoteCommit.spec, d.commitments.params.commitmentFormat) - trimmedHtlcs.map(_.add).foreach { htlc => + // We persist htlc data in order to be able to claim htlc outputs in case a revoked tx is published by our + // counterparty, so only htlcs above remote's dust_limit matter. + val trimmedOfferedHtlcs = d.commitments.active.flatMap(c => Transactions.trimOfferedHtlcs(c.remoteCommitParams.dustLimit, nextRemoteCommit.spec, c.commitmentFormat)).map(_.add).toSet + val trimmedReceivedHtlcs = d.commitments.active.flatMap(c => Transactions.trimReceivedHtlcs(c.remoteCommitParams.dustLimit, nextRemoteCommit.spec, c.commitmentFormat)).map(_.add).toSet + (trimmedOfferedHtlcs ++ trimmedReceivedHtlcs).foreach { htlc => log.debug(s"adding paymentHash=${htlc.paymentHash} cltvExpiry=${htlc.cltvExpiry} to htlcs db for commitNumber=$nextCommitNumber") nodeParams.db.channels.addHtlcInfo(d.channelId, nextCommitNumber, htlc.paymentHash, htlc.cltvExpiry) } @@ -1572,36 +1678,32 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with stay() } - case Event(commit: CommitSig, d@DATA_SHUTDOWN(_, localShutdown, remoteShutdown, closeStatus)) => - aggregateSigs(commit) match { - case Some(sigs) => - d.commitments.receiveCommit(sigs, keyManager) match { - case Right((commitments1, revocation)) => - // we always reply with a revocation - log.debug("received a new sig:\n{}", commitments1.latest.specs2String) - context.system.eventStream.publish(ChannelSignatureReceived(self, commitments1)) - if (commitments1.hasNoPendingHtlcsOrFeeUpdate) { - if (Features.canUseFeature(d.commitments.params.localParams.initFeatures, d.commitments.params.remoteParams.initFeatures, Features.SimpleClose)) { - val (d1, closingComplete_opt) = startSimpleClose(d.commitments, localShutdown, remoteShutdown, closeStatus) - goto(NEGOTIATING_SIMPLE) using d1 storing() sending revocation +: closingComplete_opt.toSeq - } else if (d.commitments.params.localParams.paysClosingFees) { - // we pay the closing fees, so we initiate the negotiation by sending the first closing_signed - val (closingTx, closingSigned) = MutualClose.makeFirstClosingTx(keyManager, commitments1.latest, localShutdown.scriptPubKey, remoteShutdown.scriptPubKey, nodeParams.currentFeeratesForFundingClosing, nodeParams.onChainFeeConf, d.closeStatus.feerates_opt) - goto(NEGOTIATING) using DATA_NEGOTIATING(commitments1, localShutdown, remoteShutdown, List(List(ClosingTxProposed(closingTx, closingSigned))), bestUnpublishedClosingTx_opt = None) storing() sending revocation :: closingSigned :: Nil - } else { - // we are not the channel initiator, will wait for their closing_signed - goto(NEGOTIATING) using DATA_NEGOTIATING(commitments1, localShutdown, remoteShutdown, closingTxProposed = List(List()), bestUnpublishedClosingTx_opt = None) storing() sending revocation - } - } else { - if (commitments1.changes.localHasChanges) { - // if we have newly acknowledged changes let's sign them - self ! CMD_SIGN() - } - stay() using d.copy(commitments = commitments1) storing() sending revocation - } - case Left(cause) => handleLocalError(cause, d, Some(commit)) + case Event(commit: CommitSigs, d@DATA_SHUTDOWN(_, localShutdown, remoteShutdown, closeStatus)) => + d.commitments.receiveCommit(commit, channelKeys) match { + case Right((commitments1, revocation)) => + // we always reply with a revocation + log.debug("received a new sig:\n{}", commitments1.latest.specs2String) + context.system.eventStream.publish(ChannelSignatureReceived(self, commitments1)) + if (commitments1.hasNoPendingHtlcsOrFeeUpdate) { + if (Features.canUseFeature(d.commitments.localChannelParams.initFeatures, d.commitments.remoteChannelParams.initFeatures, Features.SimpleClose)) { + val (d1, closingComplete_opt) = startSimpleClose(commitments1, localShutdown, remoteShutdown, closeStatus) + goto(NEGOTIATING_SIMPLE) using d1 storing() sending revocation +: closingComplete_opt.toSeq + } else if (d.commitments.localChannelParams.paysClosingFees) { + // we pay the closing fees, so we initiate the negotiation by sending the first closing_signed + val (closingTx, closingSigned) = MutualClose.makeFirstClosingTx(channelKeys, commitments1.latest, localShutdown.scriptPubKey, remoteShutdown.scriptPubKey, nodeParams.currentFeeratesForFundingClosing, nodeParams.onChainFeeConf, d.closeStatus.feerates_opt) + goto(NEGOTIATING) using DATA_NEGOTIATING(commitments1, localShutdown, remoteShutdown, List(List(ClosingTxProposed(closingTx, closingSigned))), bestUnpublishedClosingTx_opt = None) storing() sending revocation :: closingSigned :: Nil + } else { + // we are not the channel initiator, will wait for their closing_signed + goto(NEGOTIATING) using DATA_NEGOTIATING(commitments1, localShutdown, remoteShutdown, closingTxProposed = List(List()), bestUnpublishedClosingTx_opt = None) storing() sending revocation + } + } else { + if (commitments1.changes.localHasChanges) { + // if we have newly acknowledged changes let's sign them + self ! CMD_SIGN() + } + stay() using d.copy(commitments = commitments1) storing() sending revocation } - case None => stay() + case Left(cause) => handleLocalError(cause, d, Some(commit)) } case Event(revocation: RevokeAndAck, d@DATA_SHUTDOWN(_, localShutdown, remoteShutdown, closeStatus)) => @@ -1610,28 +1712,31 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with d.commitments.receiveRevocation(revocation, nodeParams.onChainFeeConf.feerateToleranceFor(remoteNodeId).dustTolerance.maxExposure) match { case Right((commitments1, actions)) => cancelTimer(RevocationTimeout.toString) + remoteNextCommitNonces = revocation.nextCommitNonces log.debug("received a new rev, spec:\n{}", commitments1.latest.specs2String) actions.foreach { case PostRevocationAction.RelayHtlc(add) => // BOLT 2: A sending node SHOULD fail to route any HTLC added after it sent shutdown. log.debug("closing in progress: failing {}", add) - self ! CMD_FAIL_HTLC(add.id, FailureReason.LocalFailure(PermanentChannelFailure()), commit = true) + val attribution = FailureAttributionData(htlcReceivedAt = TimestampMilli.now(), trampolineReceivedAt_opt = None) + self ! CMD_FAIL_HTLC(add.id, FailureReason.LocalFailure(PermanentChannelFailure()), Some(attribution), commit = true) case PostRevocationAction.RejectHtlc(add) => // BOLT 2: A sending node SHOULD fail to route any HTLC added after it sent shutdown. log.debug("closing in progress: rejecting {}", add) - self ! CMD_FAIL_HTLC(add.id, FailureReason.LocalFailure(PermanentChannelFailure()), commit = true) + val attribution = FailureAttributionData(htlcReceivedAt = TimestampMilli.now(), trampolineReceivedAt_opt = None) + self ! CMD_FAIL_HTLC(add.id, FailureReason.LocalFailure(PermanentChannelFailure()), Some(attribution), commit = true) case PostRevocationAction.RelayFailure(result) => log.debug("forwarding {} to relayer", result) relayer ! result } if (commitments1.hasNoPendingHtlcsOrFeeUpdate) { log.debug("switching to NEGOTIATING spec:\n{}", commitments1.latest.specs2String) - if (Features.canUseFeature(d.commitments.params.localParams.initFeatures, d.commitments.params.remoteParams.initFeatures, Features.SimpleClose)) { - val (d1, closingComplete_opt) = startSimpleClose(d.commitments, localShutdown, remoteShutdown, d.closeStatus) + if (Features.canUseFeature(d.commitments.localChannelParams.initFeatures, d.commitments.remoteChannelParams.initFeatures, Features.SimpleClose)) { + val (d1, closingComplete_opt) = startSimpleClose(commitments1, localShutdown, remoteShutdown, closeStatus) goto(NEGOTIATING_SIMPLE) using d1 storing() sending closingComplete_opt.toSeq - } else if (d.commitments.params.localParams.paysClosingFees) { + } else if (d.commitments.localChannelParams.paysClosingFees) { // we pay the closing fees, so we initiate the negotiation by sending the first closing_signed - val (closingTx, closingSigned) = MutualClose.makeFirstClosingTx(keyManager, commitments1.latest, localShutdown.scriptPubKey, remoteShutdown.scriptPubKey, nodeParams.currentFeeratesForFundingClosing, nodeParams.onChainFeeConf, d.closeStatus.feerates_opt) + val (closingTx, closingSigned) = MutualClose.makeFirstClosingTx(channelKeys, commitments1.latest, localShutdown.scriptPubKey, remoteShutdown.scriptPubKey, nodeParams.currentFeeratesForFundingClosing, nodeParams.onChainFeeConf, d.closeStatus.feerates_opt) goto(NEGOTIATING) using DATA_NEGOTIATING(commitments1, localShutdown, remoteShutdown, List(List(ClosingTxProposed(closingTx, closingSigned))), bestUnpublishedClosingTx_opt = None) storing() sending closingSigned } else { // we are not the channel initiator, will wait for their closing_signed @@ -1650,29 +1755,31 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with if (shutdown.scriptPubKey != d.remoteShutdown.scriptPubKey) { log.debug("our peer updated their shutdown script (previous={}, current={})", d.remoteShutdown.scriptPubKey, shutdown.scriptPubKey) } + remoteCloseeNonce_opt = shutdown.closeeNonce_opt stay() using d.copy(remoteShutdown = shutdown) storing() case Event(r: RevocationTimeout, d: DATA_SHUTDOWN) => handleRevocationTimeout(r, d) case Event(ProcessCurrentBlockHeight(c), d: DATA_SHUTDOWN) => handleNewBlock(c, d) - case Event(c: CurrentFeerates.BitcoinCore, d: DATA_SHUTDOWN) => handleCurrentFeerate(c, d) + case Event(c: CurrentFeerates.BitcoinCore, d: DATA_SHUTDOWN) => handleCurrentFeerate(d) case Event(c: CMD_CLOSE, d: DATA_SHUTDOWN) => - val useSimpleClose = Features.canUseFeature(d.commitments.params.localParams.initFeatures, d.commitments.params.remoteParams.initFeatures, Features.SimpleClose) - val localShutdown_opt = c.scriptPubKey match { - case Some(scriptPubKey) if scriptPubKey != d.localShutdown.scriptPubKey && useSimpleClose => Some(Shutdown(d.channelId, scriptPubKey)) + val useSimpleClose = Features.canUseFeature(d.commitments.localChannelParams.initFeatures, d.commitments.remoteChannelParams.initFeatures, Features.SimpleClose) + val nextScriptPubKey_opt = c.scriptPubKey match { + case Some(scriptPubKey) if scriptPubKey != d.localShutdown.scriptPubKey && useSimpleClose => Some(scriptPubKey) case _ => None } if (c.scriptPubKey.exists(_ != d.localShutdown.scriptPubKey) && !useSimpleClose) { handleCommandError(ClosingAlreadyInProgress(d.channelId), c) - } else if (localShutdown_opt.nonEmpty || c.feerates.nonEmpty) { + } else if (nextScriptPubKey_opt.nonEmpty || c.feerates.nonEmpty) { val closeStatus1 = d.closeStatus match { case initiator: CloseStatus.Initiator => initiator.copy(feerates_opt = c.feerates.orElse(initiator.feerates_opt)) case nonInitiator: CloseStatus.NonInitiator => nonInitiator.copy(feerates_opt = c.feerates.orElse(nonInitiator.feerates_opt)) // NB: this is the corner case where we can be non-initiator and have custom feerates } - val d1 = d.copy(localShutdown = localShutdown_opt.getOrElse(d.localShutdown), closeStatus = closeStatus1) - handleCommandSuccess(c, d1) storing() sending localShutdown_opt.toSeq + val shutdown = createShutdown(d.commitments, nextScriptPubKey_opt.getOrElse(d.localShutdown.scriptPubKey)) + val d1 = d.copy(localShutdown = shutdown, closeStatus = closeStatus1) + handleCommandSuccess(c, d1) storing() sending shutdown } else { handleCommandError(ClosingAlreadyInProgress(d.channelId), c) } @@ -1693,7 +1800,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with case Event(c: ClosingSigned, d: DATA_NEGOTIATING) => val (remoteClosingFee, remoteSig) = (c.feeSatoshis, c.signature) - MutualClose.checkClosingSignature(keyManager, d.commitments.latest, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, remoteClosingFee, remoteSig) match { + MutualClose.checkClosingSignature(channelKeys, d.commitments.latest, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, remoteClosingFee, remoteSig) match { case Right((signedClosingTx, closingSignedRemoteFees)) => val lastLocalClosingSigned_opt = d.closingTxProposed.last.lastOption if (lastLocalClosingSigned_opt.exists(_.localClosingSigned.feeSatoshis == remoteClosingFee)) { @@ -1713,10 +1820,10 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with handleMutualClose(signedClosingTx, Left(d.copy(bestUnpublishedClosingTx_opt = Some(signedClosingTx)))) sending closingSignedRemoteFees } else { c.feeRange_opt match { - case Some(ClosingSignedTlv.FeeRange(minFee, maxFee)) if !d.commitments.params.localParams.paysClosingFees => + case Some(ClosingSignedTlv.FeeRange(minFee, maxFee)) if !d.commitments.localChannelParams.paysClosingFees => // if we are not paying the closing fees and they proposed a fee range, we pick a value in that range and they should accept it without further negotiation // we don't care much about the closing fee since they're paying it (not us) and we can use CPFP if we want to speed up confirmation - val localClosingFees = MutualClose.firstClosingFee(d.commitments.latest, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, nodeParams.currentFeeratesForFundingClosing, nodeParams.onChainFeeConf) + val localClosingFees = MutualClose.firstClosingFee(channelKeys, d.commitments.latest, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, nodeParams.currentFeeratesForFundingClosing, nodeParams.onChainFeeConf) if (maxFee < localClosingFees.min) { log.warning("their highest closing fee is below our minimum fee: {} < {}", maxFee, localClosingFees.min) stay() sending Warning(d.channelId, s"closing fee range must not be below ${localClosingFees.min}") @@ -1731,7 +1838,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with log.info("accepting their closing fee={}", remoteClosingFee) handleMutualClose(signedClosingTx, Left(d.copy(bestUnpublishedClosingTx_opt = Some(signedClosingTx)))) sending closingSignedRemoteFees } else { - val (closingTx, closingSigned) = MutualClose.makeClosingTx(keyManager, d.commitments.latest, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, ClosingFees(closingFee, minFee, maxFee)) + val (closingTx, closingSigned) = MutualClose.makeClosingTx(channelKeys, d.commitments.latest, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, ClosingFees(closingFee, minFee, maxFee)) log.info("proposing closing fee={} in their fee range (min={} max={})", closingSigned.feeSatoshis, minFee, maxFee) val closingTxProposed1 = (d.closingTxProposed: @unchecked) match { case previousNegotiations :+ currentNegotiation => previousNegotiations :+ (currentNegotiation :+ ClosingTxProposed(closingTx, closingSigned)) @@ -1743,9 +1850,9 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with val lastLocalClosingFee_opt = lastLocalClosingSigned_opt.map(_.localClosingSigned.feeSatoshis) val (closingTx, closingSigned) = { // if we are not the channel initiator and we were waiting for them to send their first closing_signed, we don't have a lastLocalClosingFee, so we compute a firstClosingFee - val localClosingFees = MutualClose.firstClosingFee(d.commitments.latest, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, nodeParams.currentFeeratesForFundingClosing, nodeParams.onChainFeeConf) + val localClosingFees = MutualClose.firstClosingFee(channelKeys, d.commitments.latest, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, nodeParams.currentFeeratesForFundingClosing, nodeParams.onChainFeeConf) val nextPreferredFee = MutualClose.nextClosingFee(lastLocalClosingFee_opt.getOrElse(localClosingFees.preferred), remoteClosingFee) - MutualClose.makeClosingTx(keyManager, d.commitments.latest, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, localClosingFees.copy(preferred = nextPreferredFee)) + MutualClose.makeClosingTx(channelKeys, d.commitments.latest, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, localClosingFees.copy(preferred = nextPreferredFee)) } val closingTxProposed1 = (d.closingTxProposed: @unchecked) match { case previousNegotiations :+ currentNegotiation => previousNegotiations :+ (currentNegotiation :+ ClosingTxProposed(closingTx, closingSigned)) @@ -1774,7 +1881,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with handleCommandError(ClosingAlreadyInProgress(d.channelId), c) } else { log.info("updating our closing feerates: {}", feerates) - val (closingTx, closingSigned) = MutualClose.makeFirstClosingTx(keyManager, d.commitments.latest, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, nodeParams.currentFeeratesForFundingClosing, nodeParams.onChainFeeConf, Some(feerates)) + val (closingTx, closingSigned) = MutualClose.makeFirstClosingTx(channelKeys, d.commitments.latest, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, nodeParams.currentFeeratesForFundingClosing, nodeParams.onChainFeeConf, Some(feerates)) val closingTxProposed1 = d.closingTxProposed match { case previousNegotiations :+ currentNegotiation => previousNegotiations :+ (currentNegotiation :+ ClosingTxProposed(closingTx, closingSigned)) case previousNegotiations => previousNegotiations :+ List(ClosingTxProposed(closingTx, closingSigned)) @@ -1791,6 +1898,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with when(NEGOTIATING_SIMPLE)(handleExceptions { case Event(shutdown: Shutdown, d: DATA_NEGOTIATING_SIMPLE) => + remoteCloseeNonce_opt = shutdown.closeeNonce_opt if (shutdown.scriptPubKey != d.remoteScriptPubKey) { // This may lead to a signature mismatch: peers must use closing_complete to update their closing script. log.warning("received shutdown changing remote script, this may lead to a signature mismatch: previous={}, current={}", d.remoteScriptPubKey, shutdown.scriptPubKey) @@ -1801,15 +1909,16 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with case Event(c: CMD_CLOSE, d: DATA_NEGOTIATING_SIMPLE) => val localScript = c.scriptPubKey.getOrElse(d.localScriptPubKey) - val closingFeerate = c.feerates.map(_.preferred).getOrElse(nodeParams.onChainFeeConf.getClosingFeerate(nodeParams.currentBitcoinCoreFeerates)) + val closingFeerate = c.feerates.map(_.preferred).getOrElse(nodeParams.onChainFeeConf.getClosingFeerate(nodeParams.currentBitcoinCoreFeerates, maxClosingFeerateOverride_opt = None)) if (closingFeerate < d.lastClosingFeerate) { val err = InvalidRbfFeerate(d.channelId, closingFeerate, d.lastClosingFeerate * 1.2) handleCommandError(err, c) } else { - MutualClose.makeSimpleClosingTx(nodeParams.currentBlockHeight, keyManager, d.commitments.latest, localScript, d.remoteScriptPubKey, closingFeerate) match { + MutualClose.makeSimpleClosingTx(nodeParams.currentBlockHeight, channelKeys, d.commitments.latest, localScript, d.remoteScriptPubKey, closingFeerate, remoteCloseeNonce_opt) match { case Left(f) => handleCommandError(f, c) - case Right((closingTxs, closingComplete)) => + case Right((closingTxs, closingComplete, closerNonces)) => log.debug("signing local mutual close transactions: {}", closingTxs) + localCloserNonces_opt = Some(closerNonces) handleCommandSuccess(c, d.copy(lastClosingFeerate = closingFeerate, localScriptPubKey = localScript, proposedClosingTxs = d.proposedClosingTxs :+ closingTxs)) storing() sending closingComplete } } @@ -1822,12 +1931,13 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with // No need to persist their latest script, they will re-sent it on reconnection. stay() using d.copy(remoteScriptPubKey = closingComplete.closerScriptPubKey) sending Warning(d.channelId, InvalidCloseeScript(d.channelId, closingComplete.closeeScriptPubKey, d.localScriptPubKey).getMessage) } else { - MutualClose.signSimpleClosingTx(keyManager, d.commitments.latest, closingComplete.closeeScriptPubKey, closingComplete.closerScriptPubKey, closingComplete) match { + MutualClose.signSimpleClosingTx(channelKeys, d.commitments.latest, closingComplete.closeeScriptPubKey, closingComplete.closerScriptPubKey, closingComplete, localCloseeNonce_opt) match { case Left(f) => log.warning("invalid closing_complete: {}", f.getMessage) stay() sending Warning(d.channelId, f.getMessage) - case Right((signedClosingTx, closingSig)) => + case Right((signedClosingTx, closingSig, nextCloseeNonce_opt)) => log.debug("signing remote mutual close transaction: {}", signedClosingTx.tx) + localCloseeNonce_opt = nextCloseeNonce_opt val d1 = d.copy(remoteScriptPubKey = closingComplete.closerScriptPubKey, publishedClosingTxs = d.publishedClosingTxs :+ signedClosingTx) stay() using d1 storing() calling doPublish(signedClosingTx, localPaysClosingFees = false) sending closingSig } @@ -1837,13 +1947,15 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with // Note that if we sent two closing_complete in a row, without waiting for their closing_sig for the first one, // this will fail because we only care about our latest closing_complete. This is fine, we should receive their // closing_sig for the last closing_complete afterwards. - MutualClose.receiveSimpleClosingSig(keyManager, d.commitments.latest, d.proposedClosingTxs.last, closingSig) match { + MutualClose.receiveSimpleClosingSig(channelKeys, d.commitments.latest, d.proposedClosingTxs.last, closingSig, localCloserNonces_opt, remoteCloseeNonce_opt) match { case Left(f) => log.warning("invalid closing_sig: {}", f.getMessage) + remoteCloseeNonce_opt = closingSig.nextCloseeNonce_opt stay() sending Warning(d.channelId, f.getMessage) case Right(signedClosingTx) => log.debug("received signatures for local mutual close transaction: {}", signedClosingTx.tx) val d1 = d.copy(publishedClosingTxs = d.publishedClosingTxs :+ signedClosingTx) + remoteCloseeNonce_opt = closingSig.nextCloseeNonce_opt stay() using d1 storing() calling doPublish(signedClosingTx, localPaysClosingFees = true) } @@ -1858,24 +1970,46 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with when(CLOSING)(handleExceptions { case Event(c: HtlcSettlementCommand, d: DATA_CLOSING) => (c match { - case c: CMD_FULFILL_HTLC => d.commitments.sendFulfill(c) - case c: CMD_FAIL_HTLC => d.commitments.sendFail(c, nodeParams.privateKey) + case c: CMD_FULFILL_HTLC => d.commitments.sendFulfill(c, nodeParams.privateKey, nodeParams.features.hasFeature(Features.AttributionData)) + case c: CMD_FAIL_HTLC => d.commitments.sendFail(c, nodeParams.privateKey, nodeParams.features.hasFeature(Features.AttributionData)) case c: CMD_FAIL_MALFORMED_HTLC => d.commitments.sendFailMalformed(c) }) match { case Right((commitments1, _)) => - log.info("got valid settlement for htlc={}, recalculating htlc transactions", c.id) val commitment = commitments1.latest - val localCommitPublished1 = d.localCommitPublished.map(localCommitPublished => localCommitPublished.copy(htlcTxs = Closing.LocalClose.claimHtlcOutputs(keyManager, commitment))) - val remoteCommitPublished1 = d.remoteCommitPublished.map(remoteCommitPublished => remoteCommitPublished.copy(claimHtlcTxs = Closing.RemoteClose.claimHtlcOutputs(keyManager, commitment, commitment.remoteCommit, nodeParams.currentBitcoinCoreFeerates, d.finalScriptPubKey))) - val nextRemoteCommitPublished1 = d.nextRemoteCommitPublished.map(remoteCommitPublished => remoteCommitPublished.copy(claimHtlcTxs = Closing.RemoteClose.claimHtlcOutputs(keyManager, commitment, commitment.nextRemoteCommit_opt.get.commit, nodeParams.currentBitcoinCoreFeerates, d.finalScriptPubKey))) - - def republish(): Unit = { - localCommitPublished1.foreach(lcp => doPublish(lcp, commitment)) - remoteCommitPublished1.foreach(rcp => doPublish(rcp, commitment)) - nextRemoteCommitPublished1.foreach(rcp => doPublish(rcp, commitment)) + val d1 = c match { + case c: CMD_FULFILL_HTLC => + log.info("htlc #{} with payment_hash={} was fulfilled downstream, recalculating htlc-success transactions", c.id, c.r) + // We may be able to publish HTLC-success transactions for which we didn't have the preimage. + // We are already watching the corresponding outputs: no need to set additional watches. + d.localCommitPublished.foreach(lcp => { + val commitKeys = commitment.localKeys(channelKeys) + Closing.LocalClose.claimHtlcsWithPreimage(channelKeys, commitKeys, commitment, c.r).foreach(htlcTx => { + txPublisher ! TxPublisher.PublishReplaceableTx(htlcTx, lcp.commitTx, commitment, Closing.confirmationTarget(htlcTx)) + }) + }) + d.remoteCommitPublished.foreach(rcp => { + val remoteCommit = commitment.remoteCommit + val commitKeys = commitment.remoteKeys(channelKeys, remoteCommit.remotePerCommitmentPoint) + Closing.RemoteClose.claimHtlcsWithPreimage(channelKeys, commitKeys, rcp, commitment, remoteCommit, c.r, d.finalScriptPubKey).foreach(htlcTx => { + txPublisher ! TxPublisher.PublishReplaceableTx(htlcTx, rcp.commitTx, commitment, Closing.confirmationTarget(htlcTx)) + }) + }) + d.nextRemoteCommitPublished.foreach(nrcp => { + val remoteCommit = commitment.nextRemoteCommit_opt.get + val commitKeys = commitment.remoteKeys(channelKeys, remoteCommit.remotePerCommitmentPoint) + Closing.RemoteClose.claimHtlcsWithPreimage(channelKeys, commitKeys, nrcp, commitment, remoteCommit, c.r, d.finalScriptPubKey).foreach(htlcTx => { + txPublisher ! TxPublisher.PublishReplaceableTx(htlcTx, nrcp.commitTx, commitment, Closing.confirmationTarget(htlcTx)) + }) + }) + d.copy(commitments = commitments1) + case _: CMD_FAIL_HTLC | _: CMD_FAIL_MALFORMED_HTLC => + log.info("htlc #{} was failed downstream, recalculating watched htlc outputs", c.id) + val lcp1 = d.localCommitPublished.map(lcp => Closing.LocalClose.ignoreFailedIncomingHtlc(c.id, lcp, commitment)) + val rcp1 = d.remoteCommitPublished.map(rcp => Closing.RemoteClose.ignoreFailedIncomingHtlc(c.id, rcp, commitment, commitment.remoteCommit)) + val nrcp1 = d.nextRemoteCommitPublished.map(nrcp => Closing.RemoteClose.ignoreFailedIncomingHtlc(c.id, nrcp, commitment, commitment.nextRemoteCommit_opt.get)) + d.copy(commitments = commitments1, localCommitPublished = lcp1, remoteCommitPublished = rcp1, nextRemoteCommitPublished = nrcp1) } - - handleCommandSuccess(c, d.copy(commitments = commitments1, localCommitPublished = localCommitPublished1, remoteCommitPublished = remoteCommitPublished1, nextRemoteCommitPublished = nextRemoteCommitPublished1)) storing() calling republish() + handleCommandSuccess(c, d1) storing() case Left(cause) => handleCommandError(cause, c) } @@ -1911,7 +2045,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with inactive = Nil ) val d1 = d.copy(commitments = commitments2) - spendLocalCurrent(d1) + spendLocalCurrent(d1, d.maxClosingFeerate_opt) } else { // We're still on the same splice history, nothing to do stay() using d.copy(commitments = commitments1) storing() @@ -1942,10 +2076,10 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with } else if (d.futureRemoteCommitPublished.exists(_.commitTx.txid == tx.txid)) { // this is because WatchSpent watches never expire and we are notified multiple times stay() - } else if (tx.txid == d.commitments.latest.remoteCommit.txid) { + } else if (tx.txid == d.commitments.latest.remoteCommit.txId) { // counterparty may attempt to spend its last commit tx at any time handleRemoteSpentCurrent(tx, d) - } else if (d.commitments.latest.nextRemoteCommit_opt.exists(_.commit.txid == tx.txid)) { + } else if (d.commitments.latest.nextRemoteCommit_opt.exists(_.txId == tx.txid)) { // counterparty may attempt to spend its last commit tx at any time handleRemoteSpentNext(tx, d) } else if (tx.txIn.map(_.outPoint.txid).contains(d.commitments.latest.fundingTxId)) { @@ -1992,12 +2126,12 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with // We reset the state to match the commitment that confirmed. val d1 = d.copy(commitments = commitments1) // This commitment may be revoked: we need to verify that its index matches our latest known index before overwriting our previous commitments. - if (commitment.localCommit.commitTxAndRemoteSig.commitTx.tx.txid == tx.txid) { + if (commitment.localCommit.txId == tx.txid) { // Our local commit has been published from the outside, it's unexpected but let's deal with it anyway. - spendLocalCurrent(d1) - } else if (commitment.remoteCommit.txid == tx.txid && commitment.remoteCommit.index == d.commitments.remoteCommitIndex) { + spendLocalCurrent(d1, d.maxClosingFeerate_opt) + } else if (commitment.remoteCommit.txId == tx.txid && commitment.remoteCommit.index == d.commitments.remoteCommitIndex) { handleRemoteSpentCurrent(tx, d1) - } else if (commitment.nextRemoteCommit_opt.exists(_.commit.txid == tx.txid) && commitment.remoteCommit.index == d.commitments.remoteCommitIndex && d.commitments.remoteNextCommitInfo.isLeft) { + } else if (commitment.nextRemoteCommit_opt.exists(_.txId == tx.txid) && commitment.remoteCommit.index == d.commitments.remoteCommitIndex && d.commitments.remoteNextCommitInfo.isLeft) { handleRemoteSpentNext(tx, d1) } else { // Our counterparty is trying to broadcast a revoked commit tx (cheating attempt). @@ -2011,7 +2145,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with // so the fail command will be a no-op. val outgoingHtlcs = d.commitments.latest.localCommit.spec.htlcs.collect(DirectedHtlc.outgoing) ++ d.commitments.latest.remoteCommit.spec.htlcs.collect(DirectedHtlc.incoming) ++ - d.commitments.latest.nextRemoteCommit_opt.map(_.commit.spec.htlcs.collect(DirectedHtlc.incoming)).getOrElse(Set.empty) + d.commitments.latest.nextRemoteCommit_opt.map(_.spec.htlcs.collect(DirectedHtlc.incoming)).getOrElse(Set.empty) outgoingHtlcs.foreach { add => d.commitments.originChannels.get(add.id) match { case Some(origin) => @@ -2027,14 +2161,13 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with stay() } - case Event(WatchOutputSpentTriggered(amount, tx), d: DATA_CLOSING) => - // one of the outputs of the local/remote/revoked commit was spent - // we just put a watch to be notified when it is confirmed + case Event(WatchOutputSpentTriggered(_, tx), d: DATA_CLOSING) => + // One of the outputs of the local/remote/revoked commit transaction or of an HTLC transaction was spent. + // We put a watch to be notified when the transaction confirms: it may double-spend one of our transactions. blockchain ! WatchTxConfirmed(self, tx.txid, nodeParams.channelConf.minDepth) - // when a remote or local commitment tx containing outgoing htlcs is published on the network, - // we watch it in order to extract payment preimage if funds are pulled by the counterparty - // we can then use these preimages to fulfill origin htlcs - log.debug(s"processing bitcoin output spent by txid={} tx={}", tx.txid, tx) + // If this is an HTLC transaction, it may reveal preimages that we haven't received yet. + // If we successfully extract those preimages, we can forward them upstream. + log.debug("processing bitcoin output spent by txid={} tx={}", tx.txid, tx) val extracted = Closing.extractPreimages(d.commitments.latest, tx) extracted.foreach { case (htlc, preimage) => d.commitments.originChannels.get(htlc.id) match { @@ -2042,20 +2175,12 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with log.info("fulfilling htlc #{} paymentHash={} origin={}", htlc.id, htlc.paymentHash, origin) relayer ! RES_ADD_SETTLED(origin, htlc, HtlcResult.OnChainFulfill(preimage)) case None => - // if we don't have the origin, it means that we already have forwarded the fulfill so that's not a big deal. - // this can happen if they send a signature containing the fulfill, then fail the channel before we have time to sign it + // If we don't have the origin, it means that we already have forwarded the fulfill so that's not a big deal. + // This can happen if they send a signature containing the fulfill, then fail the channel before we have time to sign it. log.warning("cannot fulfill htlc #{} paymentHash={} (origin not found)", htlc.id, htlc.paymentHash) } } - val revokedCommitPublished1 = d.revokedCommitPublished.map { rev => - // this transaction may be an HTLC transaction spending a revoked commitment - // in that case, we immediately publish an HTLC-penalty transaction spending its output(s) - val (rev1, penaltyTxs) = Closing.RevokedClose.claimHtlcTxOutputs(keyManager, d.commitments.params, d.commitments.remotePerCommitmentSecrets, rev, tx, nodeParams.currentBitcoinCoreFeerates, d.finalScriptPubKey) - penaltyTxs.foreach(claimTx => txPublisher ! PublishFinalTx(claimTx, claimTx.fee, None)) - penaltyTxs.foreach(claimTx => blockchain ! WatchOutputSpent(self, tx.txid, claimTx.input.outPoint.index.toInt, claimTx.amountIn, hints = Set(claimTx.tx.txid))) - rev1 - } - stay() using d.copy(revokedCommitPublished = revokedCommitPublished1) storing() + stay() case Event(WatchTxConfirmedTriggered(blockHeight, _, tx), d: DATA_CLOSING) => log.info("txid={} has reached mindepth, updating closing state", tx.txid) @@ -2063,22 +2188,30 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with // first we check if this tx belongs to one of the current local/remote commits, update it and update the channel data val d1 = d.copy( localCommitPublished = d.localCommitPublished.map(localCommitPublished => { - // If the tx is one of our HTLC txs, we now publish a 3rd-stage claim-htlc-tx that claims its output. - val (localCommitPublished1, claimHtlcTx_opt) = Closing.LocalClose.claimHtlcDelayedOutput(localCommitPublished, keyManager, d.commitments.latest, tx, nodeParams.currentBitcoinCoreFeerates, nodeParams.onChainFeeConf, d.finalScriptPubKey) - claimHtlcTx_opt.foreach(claimHtlcTx => { - txPublisher ! PublishFinalTx(claimHtlcTx, claimHtlcTx.fee, None) - blockchain ! WatchTxConfirmed(self, claimHtlcTx.tx.txid, nodeParams.channelConf.minDepth, Some(RelativeDelay(tx.txid, d.commitments.params.remoteParams.toSelfDelay.toInt.toLong))) - }) - Closing.updateLocalCommitPublished(localCommitPublished1, tx) + // If the tx is one of our HTLC txs, we now publish a 3rd-stage transaction that claims its output. + val closingFeerate = nodeParams.onChainFeeConf.getClosingFeerate(nodeParams.currentBitcoinCoreFeerates, d.maxClosingFeerate_opt) + val (localCommitPublished1, htlcDelayedTxs) = Closing.LocalClose.claimHtlcDelayedOutput(localCommitPublished, channelKeys, d.commitments.latest, tx, closingFeerate, d.finalScriptPubKey) + doPublish(localCommitPublished1, htlcDelayedTxs) + Closing.updateIrrevocablySpent(localCommitPublished1, tx) }), - remoteCommitPublished = d.remoteCommitPublished.map(Closing.updateRemoteCommitPublished(_, tx)), - nextRemoteCommitPublished = d.nextRemoteCommitPublished.map(Closing.updateRemoteCommitPublished(_, tx)), - futureRemoteCommitPublished = d.futureRemoteCommitPublished.map(Closing.updateRemoteCommitPublished(_, tx)), - revokedCommitPublished = d.revokedCommitPublished.map(Closing.updateRevokedCommitPublished(_, tx)) + remoteCommitPublished = d.remoteCommitPublished.map(Closing.updateIrrevocablySpent(_, tx)), + nextRemoteCommitPublished = d.nextRemoteCommitPublished.map(Closing.updateIrrevocablySpent(_, tx)), + futureRemoteCommitPublished = d.futureRemoteCommitPublished.map(Closing.updateIrrevocablySpent(_, tx)), + revokedCommitPublished = d.revokedCommitPublished.map(rvk => { + // If the tx is one of our peer's HTLC txs, they were able to claim the output before us. + // In that case, we immediately publish a penalty transaction spending their HTLC tx to steal their funds. + // TODO: once we allow changing the commitment format or to_self_delay during a splice, those values may be incorrect. + val toSelfDelay = d.commitments.latest.remoteCommitParams.toSelfDelay + val commitmentFormat = d.commitments.latest.commitmentFormat + val dustLimit = d.commitments.latest.localCommitParams.dustLimit + val (rvk1, penaltyTxs) = Closing.RevokedClose.claimHtlcTxOutputs(d.commitments.channelParams, channelKeys, d.commitments.remotePerCommitmentSecrets, toSelfDelay, commitmentFormat, rvk, tx, dustLimit, nodeParams.currentBitcoinCoreFeerates, d.finalScriptPubKey) + doPublish(rvk1, penaltyTxs) + Closing.updateIrrevocablySpent(rvk1, tx) + }) ) // if the local commitment tx just got confirmed, let's send an event telling when we will get the main output refund if (d1.localCommitPublished.exists(_.commitTx.txid == tx.txid)) { - context.system.eventStream.publish(LocalCommitConfirmed(self, remoteNodeId, d.channelId, blockHeight + d.commitments.params.remoteParams.toSelfDelay.toInt)) + context.system.eventStream.publish(LocalCommitConfirmed(self, remoteNodeId, d.channelId, blockHeight + d.commitments.latest.localCommitParams.toSelfDelay.toInt)) } // if the local or remote commitment tx just got confirmed, we abandon anchor transactions that were created based // on the other commitment: they will never confirm so we must free their wallet inputs. @@ -2107,8 +2240,8 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with } // we may need to fail some htlcs in case a commitment tx was published and they have reached the timeout threshold val timedOutHtlcs = Closing.isClosingTypeAlreadyKnown(d1) match { - case Some(c: Closing.LocalClose) => Closing.trimmedOrTimedOutHtlcs(d.commitments.params.commitmentFormat, c.localCommit, c.localCommitPublished, d.commitments.params.localParams.dustLimit, tx) - case Some(c: Closing.RemoteClose) => Closing.trimmedOrTimedOutHtlcs(d.commitments.params.commitmentFormat, c.remoteCommit, c.remoteCommitPublished, d.commitments.params.remoteParams.dustLimit, tx) + case Some(c: Closing.LocalClose) => Closing.trimmedOrTimedOutHtlcs(channelKeys, d.commitments.latest, c.localCommit, tx) + case Some(c: Closing.RemoteClose) => Closing.trimmedOrTimedOutHtlcs(channelKeys, d.commitments.latest, c.remoteCommit, tx) case Some(_: Closing.RevokedClose) => Set.empty[UpdateAddHtlc] // revoked commitments are handled using [[overriddenOutgoingHtlcs]] below case Some(_: Closing.RecoveryClose) => Set.empty[UpdateAddHtlc] // we lose htlc outputs in dataloss protection scenarios (future remote commit) case Some(_: Closing.MutualClose) => Set.empty[UpdateAddHtlc] @@ -2138,7 +2271,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with } // for our outgoing payments, let's send events if we know that they will settle on chain Closing - .onChainOutgoingHtlcs(d.commitments.latest.localCommit, d.commitments.latest.remoteCommit, d.commitments.latest.nextRemoteCommit_opt.map(_.commit), tx) + .onChainOutgoingHtlcs(d.commitments.latest.localCommit, d.commitments.latest.remoteCommit, d.commitments.latest.nextRemoteCommit_opt, tx) .map(add => (add, d.commitments.originChannels.get(add.id).map(_.upstream).collect { case Upstream.Local(id) => id })) // we resolve the payment id if this was a local payment .collect { case (add, Some(id)) => context.system.eventStream.publish(PaymentSettlingOnChain(id, amount = add.amountMsat, add.paymentHash)) } // finally, if one of the unilateral closes is done, we move to CLOSED state, otherwise we stay() @@ -2146,7 +2279,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with case Some(closingType) => log.info("channel closed (type={})", EventType.Closed(closingType).label) context.system.eventStream.publish(ChannelClosed(self, d.channelId, closingType, d.commitments)) - goto(CLOSED) using d1 storing() + goto(CLOSED) using DATA_CLOSED(d1, closingType) case None => stay() using d1 storing() } @@ -2162,29 +2295,41 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with case Event(c: CMD_CLOSE, d: DATA_CLOSING) => handleCommandError(ClosingAlreadyInProgress(d.channelId), c) case Event(c: CMD_BUMP_FORCE_CLOSE_FEE, d: DATA_CLOSING) => - d.commitments.params.commitmentFormat match { - case _: Transactions.AnchorOutputsCommitmentFormat => - val lcp1 = d.localCommitPublished.map(lcp => Closing.LocalClose.claimAnchors(keyManager, d.commitments.latest, lcp, c.confirmationTarget)) - val rcp1 = d.remoteCommitPublished.map(rcp => Closing.RemoteClose.claimAnchors(keyManager, d.commitments.latest, rcp, c.confirmationTarget)) - val nrcp1 = d.nextRemoteCommitPublished.map(nrcp => Closing.RemoteClose.claimAnchors(keyManager, d.commitments.latest, nrcp, c.confirmationTarget)) + val commitmentFormat = d.commitments.latest.commitmentFormat + commitmentFormat match { + case _: Transactions.AnchorOutputsCommitmentFormat | _: SimpleTaprootChannelCommitmentFormat => + val commitment = d.commitments.latest + val fundingKey = channelKeys.fundingKey(commitment.fundingTxIndex) + val localAnchor_opt = for { + lcp <- d.localCommitPublished + commitKeys = commitment.localKeys(channelKeys) + anchorTx <- Closing.LocalClose.claimAnchor(fundingKey, commitKeys, lcp.commitTx, commitmentFormat) + } yield PublishReplaceableTx(anchorTx, lcp.commitTx, commitment, c.confirmationTarget) + val remoteAnchor_opt = for { + rcp <- d.remoteCommitPublished + commitKeys = commitment.remoteKeys(channelKeys, commitment.remoteCommit.remotePerCommitmentPoint) + anchorTx <- Closing.RemoteClose.claimAnchor(fundingKey, commitKeys, rcp.commitTx, commitmentFormat) + } yield PublishReplaceableTx(anchorTx, rcp.commitTx, commitment, c.confirmationTarget) + val nextRemoteAnchor_opt = for { + nrcp <- d.nextRemoteCommitPublished + commitKeys = commitment.remoteKeys(channelKeys, commitment.nextRemoteCommit_opt.get.remotePerCommitmentPoint) + anchorTx <- Closing.RemoteClose.claimAnchor(fundingKey, commitKeys, nrcp.commitTx, commitmentFormat) + } yield PublishReplaceableTx(anchorTx, nrcp.commitTx, commitment, c.confirmationTarget) // We favor the remote commitment(s) because they're more interesting than the local commitment (no CSV delays). - if (rcp1.nonEmpty) { - rcp1.foreach(rcp => rcp.claimAnchorTxs.collect { case tx: Transactions.ClaimLocalAnchorOutputTx => txPublisher ! PublishReplaceableTx(tx, d.commitments.latest, rcp.commitTx) }) + if (remoteAnchor_opt.nonEmpty) { + remoteAnchor_opt.foreach { publishTx => txPublisher ! publishTx } c.replyTo ! RES_SUCCESS(c, d.channelId) - stay() using d.copy(remoteCommitPublished = rcp1) storing() - } else if (nrcp1.nonEmpty) { - nrcp1.foreach(rcp => rcp.claimAnchorTxs.collect { case tx: Transactions.ClaimLocalAnchorOutputTx => txPublisher ! PublishReplaceableTx(tx, d.commitments.latest, rcp.commitTx) }) + } else if (nextRemoteAnchor_opt.nonEmpty) { + nextRemoteAnchor_opt.foreach { publishTx => txPublisher ! publishTx } c.replyTo ! RES_SUCCESS(c, d.channelId) - stay() using d.copy(nextRemoteCommitPublished = nrcp1) storing() - } else if (lcp1.nonEmpty) { - lcp1.foreach(lcp => lcp.claimAnchorTxs.collect { case tx: Transactions.ClaimLocalAnchorOutputTx => txPublisher ! PublishReplaceableTx(tx, d.commitments.latest, lcp.commitTx) }) + } else if (localAnchor_opt.nonEmpty) { + localAnchor_opt.foreach { publishTx => txPublisher ! publishTx } c.replyTo ! RES_SUCCESS(c, d.channelId) - stay() using d.copy(localCommitPublished = lcp1) storing() } else { log.warning("cannot bump force-close fees, local or remote commit not published") c.replyTo ! RES_FAILURE(c, CommandUnavailableInThisState(d.channelId, "rbf-force-close", stateName)) - stay() } + stay() case _ => log.warning("cannot bump force-close fees, channel is not using anchor outputs") c.replyTo ! RES_FAILURE(c, CommandUnavailableInThisState(d.channelId, "rbf-force-close", stateName)) @@ -2211,9 +2356,12 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with when(CLOSED)(handleExceptions { case Event(Symbol("shutdown"), _) => stateData match { - case d: PersistentChannelData => - log.info(s"deleting database record for channelId=${d.channelId}") - nodeParams.db.channels.removeChannel(d.channelId) + case d: DATA_CLOSED => + log.info(s"moving channelId=${d.channelId} to the closed channels DB") + nodeParams.db.channels.removeChannel(d.channelId, Some(d)) + case _: PersistentChannelData | _: IgnoreClosedData => + log.info("deleting database record for channelId={}", stateData.channelId) + nodeParams.db.channels.removeChannel(stateData.channelId, None) case _: TransientChannelData => // nothing was stored in the DB } log.info("shutting down") @@ -2256,16 +2404,30 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with case Event(INPUT_RECONNECTED(r, localInit, remoteInit), d: DATA_WAIT_FOR_DUAL_FUNDING_SIGNED) => activeConnection = r - val channelKeyPath = keyManager.keyPath(d.channelParams.localParams, d.channelParams.channelConfig) - val myFirstPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, 0) - val nextFundingTlv: Set[ChannelReestablishTlv] = Set(ChannelReestablishTlv.NextFundingTlv(d.signingSession.fundingTx.txId)) + val myFirstPerCommitmentPoint = channelKeys.commitmentPoint(0) + val nextFundingTlv: Set[ChannelReestablishTlv] = Set(ChannelReestablishTlv.NextFundingTlv(d.signingSession.fundingTxId)) + val nonceTlvs = d.signingSession.fundingParams.commitmentFormat match { + case _: SegwitV0CommitmentFormat => Set.empty + case _: SimpleTaprootChannelCommitmentFormat => + val localFundingKey = channelKeys.fundingKey(0) + val remoteFundingPubKey = d.signingSession.fundingParams.remoteFundingPubKey + val currentCommitNonce_opt = d.signingSession.localCommit match { + case Left(_) => Some(NonceGenerator.verificationNonce(d.signingSession.fundingTxId, localFundingKey, remoteFundingPubKey, 0)) + case Right(_) => None + } + val nextCommitNonce = NonceGenerator.verificationNonce(d.signingSession.fundingTxId, localFundingKey, remoteFundingPubKey, 1) + Set( + Some(ChannelReestablishTlv.NextLocalNoncesTlv(List(d.signingSession.fundingTxId -> nextCommitNonce.publicNonce))), + currentCommitNonce_opt.map(n => ChannelReestablishTlv.CurrentCommitNonceTlv(n.publicNonce)), + ).flatten[ChannelReestablishTlv] + } val channelReestablish = ChannelReestablish( channelId = d.channelId, nextLocalCommitmentNumber = d.signingSession.nextLocalCommitmentNumber, nextRemoteRevocationNumber = 0, yourLastPerCommitmentSecret = PrivateKey(ByteVector32.Zeroes), myCurrentPerCommitmentPoint = myFirstPerCommitmentPoint, - TlvStream(nextFundingTlv), + TlvStream(nextFundingTlv ++ nonceTlvs), ) val d1 = Helpers.updateFeatures(d, localInit, remoteInit) goto(SYNCING) using d1 sending channelReestablish @@ -2274,8 +2436,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with activeConnection = r val remotePerCommitmentSecrets = d.commitments.remotePerCommitmentSecrets val yourLastPerCommitmentSecret = remotePerCommitmentSecrets.lastIndex.flatMap(remotePerCommitmentSecrets.getHash).getOrElse(ByteVector32.Zeroes) - val channelKeyPath = keyManager.keyPath(d.commitments.params.localParams, d.commitments.params.channelConfig) - val myCurrentPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, d.commitments.localCommitIndex) + val myCurrentPerCommitmentPoint = channelKeys.commitmentPoint(d.commitments.localCommitIndex) // If we disconnected while signing a funding transaction, we may need our peer to retransmit their commit_sig. val nextLocalCommitmentNumber = d match { case d: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED => d.status match { @@ -2306,18 +2467,48 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with } case _ => Set.empty } - val lastFundingLockedTlvs: Set[ChannelReestablishTlv] = if (d.commitments.params.remoteParams.initFeatures.hasFeature(Features.SplicePrototype)) { + val lastFundingLockedTlvs: Set[ChannelReestablishTlv] = if (d.commitments.remoteChannelParams.initFeatures.hasFeature(Features.SplicePrototype)) { d.commitments.lastLocalLocked_opt.map(c => ChannelReestablishTlv.MyCurrentFundingLockedTlv(c.fundingTxId)).toSet ++ d.commitments.lastRemoteLocked_opt.map(c => ChannelReestablishTlv.YourLastFundingLockedTlv(c.fundingTxId)).toSet } else Set.empty + // We send our verification nonces for all active commitments. + val nextCommitNonces: Map[TxId, IndividualNonce] = d.commitments.active.flatMap(c => { + c.commitmentFormat match { + case _: SegwitV0CommitmentFormat => None + case _: SimpleTaprootChannelCommitmentFormat => + val localFundingKey = channelKeys.fundingKey(c.fundingTxIndex) + Some(c.fundingTxId -> NonceGenerator.verificationNonce(c.fundingTxId, localFundingKey, c.remoteFundingPubKey, d.commitments.localCommitIndex + 1).publicNonce) + } + }).toMap + // If an interactive-tx session hasn't been fully signed, we also need to include the corresponding nonces. + val (interactiveTxCurrentCommitNonce_opt, interactiveTxNextCommitNonce): (Option[IndividualNonce], Map[TxId, IndividualNonce]) = d match { + case d: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED => d.status match { + case DualFundingStatus.RbfWaitingForSigs(signingSession) if signingSession.fundingParams.commitmentFormat.isInstanceOf[TaprootCommitmentFormat] => + val nextCommitNonce = Map(signingSession.fundingTxId -> signingSession.nextCommitNonce(channelKeys).publicNonce) + (signingSession.currentCommitNonce_opt(channelKeys).map(_.publicNonce), nextCommitNonce) + case _ => (None, Map.empty) + } + case d: DATA_NORMAL => d.spliceStatus match { + case SpliceStatus.SpliceWaitingForSigs(signingSession) if signingSession.fundingParams.commitmentFormat.isInstanceOf[TaprootCommitmentFormat] => + val nextCommitNonce = Map(signingSession.fundingTxId -> signingSession.nextCommitNonce(channelKeys).publicNonce) + (signingSession.currentCommitNonce_opt(channelKeys).map(_.publicNonce), nextCommitNonce) + case _ => (None, Map.empty) + } + case _ => (None, Map.empty) + } + val nonceTlvs = Set( + interactiveTxCurrentCommitNonce_opt.map(nonce => ChannelReestablishTlv.CurrentCommitNonceTlv(nonce)), + if (nextCommitNonces.nonEmpty || interactiveTxNextCommitNonce.nonEmpty) Some(ChannelReestablishTlv.NextLocalNoncesTlv(nextCommitNonces.toSeq ++ interactiveTxNextCommitNonce.toSeq)) else None + ).flatten + val channelReestablish = ChannelReestablish( channelId = d.channelId, nextLocalCommitmentNumber = nextLocalCommitmentNumber, nextRemoteRevocationNumber = d.commitments.remoteCommitIndex, yourLastPerCommitmentSecret = PrivateKey(yourLastPerCommitmentSecret), myCurrentPerCommitmentPoint = myCurrentPerCommitmentPoint, - tlvStream = TlvStream(rbfTlv ++ lastFundingLockedTlvs) + tlvStream = TlvStream(rbfTlv ++ lastFundingLockedTlvs ++ nonceTlvs) ) // we update local/remote connection-local global/local features, we don't persist it right now val d1 = Helpers.updateFeatures(d, localInit, remoteInit) @@ -2325,7 +2516,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with case Event(ProcessCurrentBlockHeight(c), d: ChannelDataWithCommitments) => handleNewBlock(c, d) - case Event(c: CurrentFeerates.BitcoinCore, d: ChannelDataWithCommitments) => handleCurrentFeerateDisconnected(c, d) + case Event(c: CurrentFeerates.BitcoinCore, d: ChannelDataWithCommitments) => stay() case Event(c: CMD_ADD_HTLC, d: DATA_NORMAL) => handleAddDisconnected(c, d) @@ -2349,241 +2540,215 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with }) when(SYNCING)(handleExceptions { - case Event(_: ChannelReestablish, _: DATA_WAIT_FOR_FUNDING_CONFIRMED) => - goto(WAIT_FOR_FUNDING_CONFIRMED) + case Event(channelReestablish: ChannelReestablish, d: DATA_WAIT_FOR_FUNDING_CONFIRMED) => + Helpers.Syncing.checkCommitNonces(channelReestablish, d.commitments, None) match { + case Some(f) => handleLocalError(f, d, Some(channelReestablish)) + case None => + remoteNextCommitNonces = channelReestablish.nextCommitNonces + goto(WAIT_FOR_FUNDING_CONFIRMED) + } case Event(channelReestablish: ChannelReestablish, d: DATA_WAIT_FOR_DUAL_FUNDING_SIGNED) => - channelReestablish.nextFundingTxId_opt match { - case Some(fundingTxId) if fundingTxId == d.signingSession.fundingTx.txId && channelReestablish.nextLocalCommitmentNumber == 0 => - // They haven't received our commit_sig: we retransmit it, and will send our tx_signatures once we've received - // their commit_sig or their tx_signatures (depending on who must send tx_signatures first). - val commitSig = d.signingSession.remoteCommit.sign(keyManager, d.channelParams, d.signingSession.fundingTxIndex, d.signingSession.fundingParams.remoteFundingPubKey, d.signingSession.commitInput) - goto(WAIT_FOR_DUAL_FUNDING_SIGNED) sending commitSig - case _ => goto(WAIT_FOR_DUAL_FUNDING_SIGNED) + d.signingSession.fundingParams.commitmentFormat match { + case _: SimpleTaprootChannelCommitmentFormat if !channelReestablish.nextCommitNonces.contains(d.signingSession.fundingTxId) => + val f = MissingCommitNonce(d.channelId, d.signingSession.fundingTxId, commitmentNumber = 1) + handleLocalError(f, d, Some(channelReestablish)) + case _ => + remoteNextCommitNonces = channelReestablish.nextCommitNonces + channelReestablish.nextFundingTxId_opt match { + case Some(fundingTxId) if fundingTxId == d.signingSession.fundingTx.txId && channelReestablish.nextLocalCommitmentNumber == 0 => + // They haven't received our commit_sig: we retransmit it, and will send our tx_signatures once we've received + // their commit_sig or their tx_signatures (depending on who must send tx_signatures first). + val fundingParams = d.signingSession.fundingParams + val remoteNonce_opt = channelReestablish.currentCommitNonce_opt + d.signingSession.remoteCommit.sign(d.channelParams, d.signingSession.remoteCommitParams, channelKeys, d.signingSession.fundingTxIndex, fundingParams.remoteFundingPubKey, d.signingSession.commitInput(channelKeys), fundingParams.commitmentFormat, remoteNonce_opt) match { + case Left(e) => handleLocalError(e, d, Some(channelReestablish)) + case Right(commitSig) => goto(WAIT_FOR_DUAL_FUNDING_SIGNED) sending commitSig + } + case _ => goto(WAIT_FOR_DUAL_FUNDING_SIGNED) + } } case Event(channelReestablish: ChannelReestablish, d: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED) => - channelReestablish.nextFundingTxId_opt match { - case Some(fundingTxId) => - d.status match { - case DualFundingStatus.RbfWaitingForSigs(signingSession) if signingSession.fundingTx.txId == fundingTxId => - if (channelReestablish.nextLocalCommitmentNumber == 0) { - // They haven't received our commit_sig: we retransmit it. - // We're also waiting for signatures from them, and will send our tx_signatures once we receive them. - val commitSig = signingSession.remoteCommit.sign(keyManager, d.commitments.params, signingSession.fundingTxIndex, signingSession.fundingParams.remoteFundingPubKey, signingSession.commitInput) - goto(WAIT_FOR_DUAL_FUNDING_CONFIRMED) sending commitSig - } else { - // They have already received our commit_sig, but we were waiting for them to send either commit_sig or - // tx_signatures first. We wait for their message before sending our tx_signatures. - goto(WAIT_FOR_DUAL_FUNDING_CONFIRMED) - } - case _ if d.latestFundingTx.sharedTx.txId == fundingTxId => - // We've already received their commit_sig and sent our tx_signatures. We retransmit our tx_signatures - // and our commit_sig if they haven't received it already. - if (channelReestablish.nextLocalCommitmentNumber == 0) { - val commitSig = d.commitments.latest.remoteCommit.sign(keyManager, d.commitments.params, d.commitments.latest.fundingTxIndex, d.commitments.latest.remoteFundingPubKey, d.commitments.latest.commitInput) - goto(WAIT_FOR_DUAL_FUNDING_CONFIRMED) sending Seq(commitSig, d.latestFundingTx.sharedTx.localSigs) - } else { - goto(WAIT_FOR_DUAL_FUNDING_CONFIRMED) sending d.latestFundingTx.sharedTx.localSigs + val pendingRbf_opt = d.status match { + // Note that we only consider RBF attempts that are also pending for our peer: otherwise it means we have + // disconnected before they sent their commit_sig, in which case they will abort the RBF attempt on reconnection. + case DualFundingStatus.RbfWaitingForSigs(signingSession) if channelReestablish.nextFundingTxId_opt.contains(signingSession.fundingTxId) => Some(signingSession) + case _ => None + } + Helpers.Syncing.checkCommitNonces(channelReestablish, d.commitments, pendingRbf_opt) match { + case Some(f) => handleLocalError(f, d, Some(channelReestablish)) + case None => + remoteNextCommitNonces = channelReestablish.nextCommitNonces + channelReestablish.nextFundingTxId_opt match { + case Some(fundingTxId) => + d.status match { + case DualFundingStatus.RbfWaitingForSigs(signingSession) if signingSession.fundingTx.txId == fundingTxId => + if (channelReestablish.nextLocalCommitmentNumber == 0) { + // They haven't received our commit_sig: we retransmit it. + // We're also waiting for signatures from them, and will send our tx_signatures once we receive them. + val fundingParams = signingSession.fundingParams + val remoteNonce_opt = channelReestablish.currentCommitNonce_opt + signingSession.remoteCommit.sign(d.commitments.channelParams, signingSession.remoteCommitParams, channelKeys, signingSession.fundingTxIndex, signingSession.fundingParams.remoteFundingPubKey, signingSession.commitInput(channelKeys), fundingParams.commitmentFormat, remoteNonce_opt) match { + case Left(e) => handleLocalError(e, d, Some(channelReestablish)) + case Right(commitSig) => goto(WAIT_FOR_DUAL_FUNDING_CONFIRMED) sending commitSig + } + } else { + // They have already received our commit_sig, but we were waiting for them to send either commit_sig or + // tx_signatures first. We wait for their message before sending our tx_signatures. + goto(WAIT_FOR_DUAL_FUNDING_CONFIRMED) + } + case _ if d.latestFundingTx.sharedTx.txId == fundingTxId => + // We've already received their commit_sig and sent our tx_signatures. We retransmit our tx_signatures + // and our commit_sig if they haven't received it already. + if (channelReestablish.nextLocalCommitmentNumber == 0) { + val remoteNonce_opt = channelReestablish.currentCommitNonce_opt + d.commitments.latest.remoteCommit.sign(d.commitments.channelParams, d.commitments.latest.remoteCommitParams, channelKeys, d.commitments.latest.fundingTxIndex, d.commitments.latest.remoteFundingPubKey, d.commitments.latest.commitInput(channelKeys), d.commitments.latest.commitmentFormat, remoteNonce_opt) match { + case Left(e) => handleLocalError(e, d, Some(channelReestablish)) + case Right(commitSig) => goto(WAIT_FOR_DUAL_FUNDING_CONFIRMED) sending Seq(commitSig, d.latestFundingTx.sharedTx.localSigs) + } + } else { + goto(WAIT_FOR_DUAL_FUNDING_CONFIRMED) sending d.latestFundingTx.sharedTx.localSigs + } + case _ => + // The fundingTxId must be for an RBF attempt that we didn't store (we got disconnected before receiving + // their tx_complete): we tell them to abort that RBF attempt. + goto(WAIT_FOR_DUAL_FUNDING_CONFIRMED) using d.copy(status = DualFundingStatus.RbfAborted) sending TxAbort(d.channelId, RbfAttemptAborted(d.channelId).getMessage) } - case _ => - // The fundingTxId must be for an RBF attempt that we didn't store (we got disconnected before receiving - // their tx_complete): we tell them to abort that RBF attempt. - goto(WAIT_FOR_DUAL_FUNDING_CONFIRMED) using d.copy(status = DualFundingStatus.RbfAborted) sending TxAbort(d.channelId, RbfAttemptAborted(d.channelId).getMessage) + case None => goto(WAIT_FOR_DUAL_FUNDING_CONFIRMED) } - case None => goto(WAIT_FOR_DUAL_FUNDING_CONFIRMED) } - case Event(_: ChannelReestablish, d: DATA_WAIT_FOR_CHANNEL_READY) => - log.debug("re-sending channel_ready") - val channelReady = createChannelReady(d.aliases, d.commitments.params) - goto(WAIT_FOR_CHANNEL_READY) sending channelReady + case Event(channelReestablish: ChannelReestablish, d: DATA_WAIT_FOR_CHANNEL_READY) => + Helpers.Syncing.checkCommitNonces(channelReestablish, d.commitments, None) match { + case Some(f) => handleLocalError(f, d, Some(channelReestablish)) + case None => + remoteNextCommitNonces = channelReestablish.nextCommitNonces + val channelReady = createChannelReady(d.aliases, d.commitments) + goto(WAIT_FOR_CHANNEL_READY) sending channelReady + } case Event(channelReestablish: ChannelReestablish, d: DATA_WAIT_FOR_DUAL_FUNDING_READY) => - log.debug("re-sending channel_ready") - val channelReady = createChannelReady(d.aliases, d.commitments.params) - // We've already received their commit_sig and sent our tx_signatures. We retransmit our tx_signatures - // and our commit_sig if they haven't received it already. - channelReestablish.nextFundingTxId_opt match { - case Some(fundingTxId) if fundingTxId == d.commitments.latest.fundingTxId => - d.commitments.latest.localFundingStatus.localSigs_opt match { - case Some(txSigs) if channelReestablish.nextLocalCommitmentNumber == 0 => - log.info("re-sending commit_sig and tx_signatures for fundingTxIndex={} fundingTxId={}", d.commitments.latest.fundingTxIndex, d.commitments.latest.fundingTxId) - val commitSig = d.commitments.latest.remoteCommit.sign(keyManager, d.commitments.params, d.commitments.latest.fundingTxIndex, d.commitments.latest.remoteFundingPubKey, d.commitments.latest.commitInput) - goto(WAIT_FOR_DUAL_FUNDING_READY) sending Seq(commitSig, txSigs, channelReady) - case Some(txSigs) => - log.info("re-sending tx_signatures for fundingTxIndex={} fundingTxId={}", d.commitments.latest.fundingTxIndex, d.commitments.latest.fundingTxId) - goto(WAIT_FOR_DUAL_FUNDING_READY) sending Seq(txSigs, channelReady) - case None => - log.warning("cannot retransmit tx_signatures, we don't have them (status={})", d.commitments.latest.localFundingStatus) - goto(WAIT_FOR_DUAL_FUNDING_READY) sending channelReady + Helpers.Syncing.checkCommitNonces(channelReestablish, d.commitments, None) match { + case Some(f) => handleLocalError(f, d, Some(channelReestablish)) + case None => + remoteNextCommitNonces = channelReestablish.nextCommitNonces + val channelReady = createChannelReady(d.aliases, d.commitments) + // We've already received their commit_sig and sent our tx_signatures. We retransmit our tx_signatures + // and our commit_sig if they haven't received it already. + channelReestablish.nextFundingTxId_opt match { + case Some(fundingTxId) if fundingTxId == d.commitments.latest.fundingTxId => + d.commitments.latest.localFundingStatus.localSigs_opt match { + case Some(txSigs) if channelReestablish.nextLocalCommitmentNumber == 0 => + log.info("re-sending commit_sig and tx_signatures for fundingTxIndex={} fundingTxId={}", d.commitments.latest.fundingTxIndex, d.commitments.latest.fundingTxId) + val remoteNonce_opt = channelReestablish.currentCommitNonce_opt + d.commitments.latest.remoteCommit.sign(d.commitments.channelParams, d.commitments.latest.remoteCommitParams, channelKeys, d.commitments.latest.fundingTxIndex, d.commitments.latest.remoteFundingPubKey, d.commitments.latest.commitInput(channelKeys), d.commitments.latest.commitmentFormat, remoteNonce_opt) match { + case Left(e) => handleLocalError(e, d, Some(channelReestablish)) + case Right(commitSig) => goto(WAIT_FOR_DUAL_FUNDING_READY) sending Seq(commitSig, txSigs, channelReady) + } + case Some(txSigs) => + log.info("re-sending tx_signatures for fundingTxIndex={} fundingTxId={}", d.commitments.latest.fundingTxIndex, d.commitments.latest.fundingTxId) + goto(WAIT_FOR_DUAL_FUNDING_READY) sending Seq(txSigs, channelReady) + case None => + log.warning("cannot retransmit tx_signatures, we don't have them (status={})", d.commitments.latest.localFundingStatus) + goto(WAIT_FOR_DUAL_FUNDING_READY) sending channelReady + } + case _ => goto(WAIT_FOR_DUAL_FUNDING_READY) sending channelReady } - case _ => goto(WAIT_FOR_DUAL_FUNDING_READY) sending channelReady } case Event(channelReestablish: ChannelReestablish, d: DATA_NORMAL) => - Syncing.checkSync(keyManager, d.commitments, channelReestablish) match { + Syncing.checkSync(channelKeys, d.commitments, channelReestablish) match { case syncFailure: SyncResult.Failure => handleSyncFailure(channelReestablish, syncFailure, d) case syncSuccess: SyncResult.Success => - var sendQueue = Queue.empty[LightningMessage] // normal case, our data is up-to-date - - // re-send channel_ready and announcement_signatures if necessary - d.commitments.lastLocalLocked_opt match { - case None => () - // We only send channel_ready for initial funding transactions. - case Some(c) if c.fundingTxIndex != 0 => () - case Some(c) => - val remoteSpliceSupport = d.commitments.params.remoteParams.initFeatures.hasFeature(Features.SplicePrototype) - // If our peer has not received our channel_ready, we retransmit it. - val notReceivedByRemote = remoteSpliceSupport && channelReestablish.yourLastFundingLocked_opt.isEmpty - // If next_local_commitment_number is 1 in both the channel_reestablish it sent and received, then the node - // MUST retransmit channel_ready, otherwise it MUST NOT - val notReceivedByRemoteLegacy = !remoteSpliceSupport && channelReestablish.nextLocalCommitmentNumber == 1 && c.localCommit.index == 0 - // If this is a public channel and we haven't announced the channel, we retransmit our channel_ready and - // will also send announcement_signatures. - val notAnnouncedYet = d.commitments.announceChannel && c.shortChannelId_opt.nonEmpty && d.lastAnnouncement_opt.isEmpty - if (notAnnouncedYet || notReceivedByRemote || notReceivedByRemoteLegacy) { - log.debug("re-sending channel_ready") - val channelKeyPath = keyManager.keyPath(d.commitments.params.localParams, d.commitments.params.channelConfig) - val nextPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, 1) - sendQueue = sendQueue :+ ChannelReady(d.commitments.channelId, nextPerCommitmentPoint) - } - if (notAnnouncedYet) { - // The funding transaction is confirmed, so we've already sent our announcement_signatures. - // We haven't announced the channel yet, which means we haven't received our peer's announcement_signatures. - // We retransmit our announcement_signatures to let our peer know that we're ready to announce the channel. - val localAnnSigs = c.signAnnouncement(nodeParams, d.commitments.params) - localAnnSigs.foreach(annSigs => { - announcementSigsSent += annSigs.shortChannelId - sendQueue = sendQueue :+ annSigs - }) - } - } - - // resume splice signing session if any - val spliceStatus1 = channelReestablish.nextFundingTxId_opt match { - case Some(fundingTxId) => - d.spliceStatus match { - case SpliceStatus.SpliceWaitingForSigs(signingSession) if signingSession.fundingTx.txId == fundingTxId => - if (channelReestablish.nextLocalCommitmentNumber == d.commitments.remoteCommitIndex) { - // They haven't received our commit_sig: we retransmit it. - // We're also waiting for signatures from them, and will send our tx_signatures once we receive them. - log.info("re-sending commit_sig for splice attempt with fundingTxIndex={} fundingTxId={}", signingSession.fundingTxIndex, signingSession.fundingTx.txId) - val commitSig = signingSession.remoteCommit.sign(keyManager, d.commitments.params, signingSession.fundingTxIndex, signingSession.fundingParams.remoteFundingPubKey, signingSession.commitInput) - sendQueue = sendQueue :+ commitSig - } - d.spliceStatus - case _ if d.commitments.latest.fundingTxId == fundingTxId => - d.commitments.latest.localFundingStatus match { - case dfu: LocalFundingStatus.DualFundedUnconfirmedFundingTx => - // We've already received their commit_sig and sent our tx_signatures. We retransmit our - // tx_signatures and our commit_sig if they haven't received it already. - if (channelReestablish.nextLocalCommitmentNumber == d.commitments.remoteCommitIndex) { - log.info("re-sending commit_sig and tx_signatures for fundingTxIndex={} fundingTxId={}", d.commitments.latest.fundingTxIndex, d.commitments.latest.fundingTxId) - val commitSig = d.commitments.latest.remoteCommit.sign(keyManager, d.commitments.params, d.commitments.latest.fundingTxIndex, d.commitments.latest.remoteFundingPubKey, d.commitments.latest.commitInput) - sendQueue = sendQueue :+ commitSig :+ dfu.sharedTx.localSigs - } else { - log.info("re-sending tx_signatures for fundingTxIndex={} fundingTxId={}", d.commitments.latest.fundingTxIndex, d.commitments.latest.fundingTxId) - sendQueue = sendQueue :+ dfu.sharedTx.localSigs - } - case fundingStatus => - // They have not received our tx_signatures, but they must have received our commit_sig, otherwise we would be in the case above. - log.info("re-sending tx_signatures for fundingTxIndex={} fundingTxId={} (already published or confirmed)", d.commitments.latest.fundingTxIndex, d.commitments.latest.fundingTxId) - sendQueue = sendQueue ++ fundingStatus.localSigs_opt.toSeq - } - d.spliceStatus - case _ => - // The fundingTxId must be for a splice attempt that we didn't store (we got disconnected before receiving - // their tx_complete): we tell them to abort that splice attempt. - log.info(s"aborting obsolete splice attempt for fundingTxId=$fundingTxId") - sendQueue = sendQueue :+ TxAbort(d.channelId, SpliceAttemptAborted(d.channelId).getMessage) - SpliceStatus.SpliceAborted - } - case None => d.spliceStatus - } + var sendQueue = Queue.empty[LightningMessage] + // We re-send channel_ready and announcement_signatures for the initial funding transaction if necessary. + val (channelReady_opt, announcementSigs_opt) = resendChannelReadyIfNeeded(channelReestablish, d) + sendQueue = sendQueue ++ channelReady_opt.toSeq ++ announcementSigs_opt.toSeq + // If we disconnected in the middle of a signing a splice transaction, we re-send our signatures or abort. + val (spliceStatus1, spliceMessages) = resumeSpliceSigningSessionIfNeeded(channelReestablish, d) + sendQueue = sendQueue ++ spliceMessages // Prune previous funding transactions and RBF attempts if we already sent splice_locked for the last funding // transaction that is also locked by our counterparty; we either missed their splice_locked or it confirmed // while disconnected. - val commitments1: Commitments = channelReestablish.myCurrentFundingLocked_opt + val commitments1 = channelReestablish.myCurrentFundingLocked_opt .flatMap(remoteFundingTxLocked => d.commitments.updateRemoteFundingStatus(remoteFundingTxLocked, d.lastAnnouncedFundingTxId_opt).toOption.map(_._1)) .getOrElse(d.commitments) // We then clean up unsigned updates that haven't been received before the disconnection. .discardUnsignedUpdates() - commitments1.lastLocalLocked_opt match { - case None => () - // We only send splice_locked for splice transactions. - case Some(c) if c.fundingTxIndex == 0 => () - case Some(c) => - // If our peer has not received our splice_locked, we retransmit it. - val notReceivedByRemote = !channelReestablish.yourLastFundingLocked_opt.contains(c.fundingTxId) - // If this is a public channel and we haven't announced the splice, we retransmit our splice_locked and - // will exchange announcement_signatures afterwards. - val notAnnouncedYet = commitments1.announceChannel && d.lastAnnouncement_opt.forall(ann => !c.shortChannelId_opt.contains(ann.shortChannelId)) - if (notReceivedByRemote || notAnnouncedYet) { - // Retransmission of local announcement_signatures for splices are done when receiving splice_locked, no need - // to retransmit here. - log.debug("re-sending splice_locked for fundingTxId={}", c.fundingTxId) - spliceLockedSent += (c.fundingTxId -> c.fundingTxIndex) - trimSpliceLockedSentIfNeeded() - sendQueue = sendQueue :+ SpliceLocked(d.channelId, c.fundingTxId) - } + // If there is a pending splice, we need to receive nonces for the corresponding transaction if we're using taproot. + val pendingSplice_opt = spliceStatus1 match { + // Note that we only consider splices that are also pending for our peer: otherwise it means we have disconnected + // before they sent their commit_sig, in which case they will abort the splice attempt on reconnection. + case SpliceStatus.SpliceWaitingForSigs(signingSession) if channelReestablish.nextFundingTxId_opt.contains(signingSession.fundingTxId) => Some(signingSession) + case _ => None } + Helpers.Syncing.checkCommitNonces(channelReestablish, commitments1, pendingSplice_opt) match { + case Some(f) => handleLocalError(f, d, Some(channelReestablish)) + case None => + remoteNextCommitNonces = channelReestablish.nextCommitNonces + // We re-send our latest splice_locked if needed. + val spliceLocked_opt = resendSpliceLockedIfNeeded(channelReestablish, commitments1, d.lastAnnouncement_opt) + sendQueue = sendQueue ++ spliceLocked_opt.toSeq + // We may need to retransmit updates and/or commit_sig and/or revocation to resume the channel. + sendQueue = sendQueue ++ syncSuccess.retransmit + + commitments1.remoteNextCommitInfo match { + case Left(_) => + // we expect them to (re-)send the revocation immediately + startSingleTimer(RevocationTimeout.toString, RevocationTimeout(commitments1.remoteCommitIndex, peer), nodeParams.channelConf.revocationTimeout) + case _ => () + } - // we may need to retransmit updates and/or commit_sig and/or revocation - sendQueue = sendQueue ++ syncSuccess.retransmit - - commitments1.remoteNextCommitInfo match { - case Left(_) => - // we expect them to (re-)send the revocation immediately - startSingleTimer(RevocationTimeout.toString, RevocationTimeout(commitments1.remoteCommitIndex, peer), nodeParams.channelConf.revocationTimeout) - case _ => () - } + // do I have something to sign? + if (commitments1.changes.localHasChanges) { + self ! CMD_SIGN() + } - // do I have something to sign? - if (commitments1.changes.localHasChanges) { - self ! CMD_SIGN() - } + // BOLT 2: A node if it has sent a previous shutdown MUST retransmit shutdown. + val shutdown_opt = d.localShutdown match { + case None => None + case Some(shutdown) => + log.debug("re-sending local shutdown") + val shutdown1 = createShutdown(commitments1, shutdown.scriptPubKey) + sendQueue = sendQueue :+ shutdown1 + Some(shutdown1) + } - // BOLT 2: A node if it has sent a previous shutdown MUST retransmit shutdown. - d.localShutdown.foreach { - localShutdown => - log.debug("re-sending local_shutdown") - sendQueue = sendQueue :+ localShutdown - } + if (d.commitments.announceChannel) { + // we will re-enable the channel after some delay to prevent flappy updates in case the connection is unstable + startSingleTimer(Reconnected.toString, BroadcastChannelUpdate(Reconnected), 10 seconds) + } else { + // except for private channels where our peer is likely a mobile wallet: they will stay online only for a short period of time, + // so we need to re-enable them immediately to ensure we can route payments to them. It's also less of a problem to frequently + // refresh the channel update for private channels, since we won't broadcast it to the rest of the network. + self ! BroadcastChannelUpdate(Reconnected) + } - if (d.commitments.announceChannel) { - // we will re-enable the channel after some delay to prevent flappy updates in case the connection is unstable - startSingleTimer(Reconnected.toString, BroadcastChannelUpdate(Reconnected), 10 seconds) - } else { - // except for private channels where our peer is likely a mobile wallet: they will stay online only for a short period of time, - // so we need to re-enable them immediately to ensure we can route payments to them. It's also less of a problem to frequently - // refresh the channel update for private channels, since we won't broadcast it to the rest of the network. - self ! BroadcastChannelUpdate(Reconnected) - } + // We usually handle feerate updates once per block (~10 minutes), but when our remote is a mobile wallet that + // only briefly connects and then disconnects, we may never have the opportunity to send our `update_fee`, so + // we send it (if needed) when reconnected. + val shutdownInProgress = d.localShutdown.nonEmpty || d.remoteShutdown.nonEmpty + if (d.commitments.localChannelParams.paysCommitTxFees && !shutdownInProgress) { + val currentFeeratePerKw = d.commitments.latest.localCommit.spec.commitTxFeerate + val networkFeeratePerKw = nodeParams.onChainFeeConf.getCommitmentFeerate(nodeParams.currentBitcoinCoreFeerates, remoteNodeId, d.commitments.latest.commitmentFormat) + if (nodeParams.onChainFeeConf.shouldUpdateFee(currentFeeratePerKw, networkFeeratePerKw, d.commitments.latest.commitmentFormat)) { + self ! CMD_UPDATE_FEE(networkFeeratePerKw, commit = true) + } + } - // We usually handle feerate updates once per block (~10 minutes), but when our remote is a mobile wallet that - // only briefly connects and then disconnects, we may never have the opportunity to send our `update_fee`, so - // we send it (if needed) when reconnected. - val shutdownInProgress = d.localShutdown.nonEmpty || d.remoteShutdown.nonEmpty - if (d.commitments.params.localParams.paysCommitTxFees && !shutdownInProgress) { - val currentFeeratePerKw = d.commitments.latest.localCommit.spec.commitTxFeerate - val networkFeeratePerKw = nodeParams.onChainFeeConf.getCommitmentFeerate(nodeParams.currentBitcoinCoreFeerates, remoteNodeId, d.commitments.params.commitmentFormat, d.commitments.latest.capacity) - if (nodeParams.onChainFeeConf.shouldUpdateFee(currentFeeratePerKw, networkFeeratePerKw)) { - self ! CMD_UPDATE_FEE(networkFeeratePerKw, commit = true) - } - } + // We tell the peer that the channel is ready to process payments that may be queued. + if (!shutdownInProgress) { + val fundingTxIndex = commitments1.active.map(_.fundingTxIndex).min + peer ! ChannelReadyForPayments(self, remoteNodeId, d.channelId, fundingTxIndex) + } - // We tell the peer that the channel is ready to process payments that may be queued. - if (!shutdownInProgress) { - val fundingTxIndex = commitments1.active.map(_.fundingTxIndex).min - peer ! ChannelReadyForPayments(self, remoteNodeId, d.channelId, fundingTxIndex) + goto(NORMAL) using d.copy(commitments = commitments1, spliceStatus = spliceStatus1, localShutdown = shutdown_opt) sending sendQueue } - - goto(NORMAL) using d.copy(commitments = commitments1, spliceStatus = spliceStatus1) sending sendQueue } case Event(c: CMD_ADD_HTLC, d: DATA_NORMAL) => handleAddDisconnected(c, d) @@ -2599,23 +2764,30 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with case Event(c: CMD_UPDATE_RELAY_FEE, d: DATA_NORMAL) => handleUpdateRelayFeeDisconnected(c, d) case Event(channelReestablish: ChannelReestablish, d: DATA_SHUTDOWN) => - Syncing.checkSync(keyManager, d.commitments, channelReestablish) match { - case syncFailure: SyncResult.Failure => - handleSyncFailure(channelReestablish, syncFailure, d) - case syncSuccess: SyncResult.Success => - val commitments1 = d.commitments.discardUnsignedUpdates() - val sendQueue = Queue.empty[LightningMessage] ++ syncSuccess.retransmit :+ d.localShutdown - // BOLT 2: A node if it has sent a previous shutdown MUST retransmit shutdown. - goto(SHUTDOWN) using d.copy(commitments = commitments1) sending sendQueue + Helpers.Syncing.checkCommitNonces(channelReestablish, d.commitments, None) match { + case Some(f) => handleLocalError(f, d, Some(channelReestablish)) + case None => + remoteNextCommitNonces = channelReestablish.nextCommitNonces + Syncing.checkSync(channelKeys, d.commitments, channelReestablish) match { + case syncFailure: SyncResult.Failure => + handleSyncFailure(channelReestablish, syncFailure, d) + case syncSuccess: SyncResult.Success => + val commitments1 = d.commitments.discardUnsignedUpdates() + // We retransmit our shutdown: we may have updated our script and they may not have received it. + val shutdown = createShutdown(commitments1, d.localShutdown.scriptPubKey) + val sendQueue = Queue.empty[LightningMessage] ++ syncSuccess.retransmit :+ shutdown + // BOLT 2: A node if it has sent a previous shutdown MUST retransmit shutdown. + goto(SHUTDOWN) using d.copy(commitments = commitments1, localShutdown = shutdown) sending sendQueue + } } case Event(_: ChannelReestablish, d: DATA_NEGOTIATING) => // BOLT 2: A node if it has sent a previous shutdown MUST retransmit shutdown. // negotiation restarts from the beginning, and is initialized by the channel initiator // note: in any case we still need to keep all previously sent closing_signed, because they may publish one of them - if (d.commitments.params.localParams.paysClosingFees) { + if (d.commitments.localChannelParams.paysClosingFees) { // we could use the last closing_signed we sent, but network fees may have changed while we were offline so it is better to restart from scratch - val (closingTx, closingSigned) = Closing.MutualClose.makeFirstClosingTx(keyManager, d.commitments.latest, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, nodeParams.currentFeeratesForFundingClosing, nodeParams.onChainFeeConf, None) + val (closingTx, closingSigned) = Closing.MutualClose.makeFirstClosingTx(channelKeys, d.commitments.latest, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, nodeParams.currentFeeratesForFundingClosing, nodeParams.onChainFeeConf, None) val closingTxProposed1 = d.closingTxProposed :+ List(ClosingTxProposed(closingTx, closingSigned)) goto(NEGOTIATING) using d.copy(closingTxProposed = closingTxProposed1) storing() sending d.localShutdown :: closingSigned :: Nil } else { @@ -2626,7 +2798,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with case Event(_: ChannelReestablish, d: DATA_NEGOTIATING_SIMPLE) => // We retransmit our shutdown: we may have updated our script and they may not have received it. - val localShutdown = Shutdown(d.channelId, d.localScriptPubKey) + val localShutdown = createShutdown(d.commitments, d.localScriptPubKey) goto(NEGOTIATING_SIMPLE) using d sending localShutdown // This handler is a workaround for an issue in lnd: starting with versions 0.10 / 0.11, they sometimes fail to send @@ -2649,7 +2821,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with case Event(ProcessCurrentBlockHeight(c), d: ChannelDataWithCommitments) => handleNewBlock(c, d) - case Event(c: CurrentFeerates.BitcoinCore, d: ChannelDataWithCommitments) => handleCurrentFeerateDisconnected(c, d) + case Event(c: CurrentFeerates.BitcoinCore, d: ChannelDataWithCommitments) => stay() case Event(getTxResponse: GetTxWithMetaResponse, d: DATA_WAIT_FOR_FUNDING_CONFIRMED) if getTxResponse.txid == d.commitments.latest.fundingTxId => handleGetFundingTx(getTxResponse, d.waitingSince, d.fundingTx_opt) @@ -2693,6 +2865,9 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with case Event(c: CMD_FORCECLOSE, d) => d match { + case data: DATA_WAIT_FOR_DUAL_FUNDING_SIGNED => + rollbackFundingAttempt(data.signingSession.fundingTx.tx, Nil) + handleFastClose(c, d.channelId) case data: ChannelDataWithCommitments => val replyTo = if (c.replyTo == ActorRef.noSender) sender() else c.replyTo val failure = ForcedLocalCommit(d.channelId) @@ -2777,7 +2952,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with // slightly before us. In that case, the WatchConfirmed may trigger first, and it would be inefficient to let the // WatchPublished override our funding status: it will make us set a new WatchConfirmed that will instantly // trigger and rewrite the funding status again. - val alreadyConfirmed = d.commitments.active.map(_.localFundingStatus).collect { case f: LocalFundingStatus.ConfirmedFundingTx => f.tx }.exists(_.txid == w.tx.txid) + val alreadyConfirmed = d.commitments.active.exists(c => c.fundingTxId == w.tx.txid && c.localFundingStatus.isInstanceOf[LocalFundingStatus.ConfirmedFundingTx]) if (alreadyConfirmed) { stay() } else { @@ -2847,28 +3022,28 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with stay() using d.copy(publishedClosingTxs = d.publishedClosingTxs :+ closingTx) storing() calling doPublish(closingTx, localPaysClosingFees = true) } else { // This is one of the transactions we published. - val closingTx = d.findClosingTx(tx).get blockchain ! WatchTxConfirmed(self, tx.txid, nodeParams.channelConf.minDepth) stay() } case Event(WatchTxConfirmedTriggered(_, _, tx), d: DATA_NEGOTIATING_SIMPLE) if d.findClosingTx(tx).nonEmpty => - val closingType = MutualClose(d.findClosingTx(tx).get) + val closingTx = d.findClosingTx(tx).get + val closingType = MutualClose(closingTx) log.info("channel closed (type={})", EventType.Closed(closingType).label) context.system.eventStream.publish(ChannelClosed(self, d.channelId, closingType, d.commitments)) - goto(CLOSED) using d storing() + goto(CLOSED) using DATA_CLOSED(d, closingTx) case Event(WatchFundingSpentTriggered(tx), d: ChannelDataWithCommitments) => if (d.commitments.all.map(_.fundingTxId).contains(tx.txid)) { // if the spending tx is itself a funding tx, this is a splice and there is nothing to do stay() - } else if (tx.txid == d.commitments.latest.remoteCommit.txid) { + } else if (tx.txid == d.commitments.latest.remoteCommit.txId) { handleRemoteSpentCurrent(tx, d) - } else if (d.commitments.latest.nextRemoteCommit_opt.exists(_.commit.txid == tx.txid)) { + } else if (d.commitments.latest.nextRemoteCommit_opt.exists(_.txId == tx.txid)) { handleRemoteSpentNext(tx, d) - } else if (tx.txid == d.commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx.txid) { + } else if (tx.txid == d.commitments.latest.localCommit.txId) { log.warning(s"processing local commit spent from the outside") - spendLocalCurrent(d) + spendLocalCurrent(d, maxClosingFeerateOverride_opt = None) } else if (tx.txIn.map(_.outPoint.txid).contains(d.commitments.latest.fundingTxId)) { handleRemoteSpentOther(tx, d) } else { @@ -2877,7 +3052,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with log.warning("a commit tx for an older commitment has been published fundingTxId={} fundingTxIndex={}", tx.txid, commitment.fundingTxIndex) // We watch the commitment tx, in the meantime we force close using the latest commitment. blockchain ! WatchAlternativeCommitTxConfirmed(self, tx.txid, nodeParams.channelConf.minDepth) - spendLocalCurrent(d) + spendLocalCurrent(d, maxClosingFeerateOverride_opt = None) case None => // This must be a former funding tx that has already been pruned, because watches are unordered. log.warning(s"ignoring unrecognized tx=${tx.txid}") @@ -2894,10 +3069,10 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with case d: ChannelDataWithCommitments => Some(d.commitments) case _: ChannelDataWithoutCommitments => None case _: TransientChannelData => None + case _: ClosedData => None } context.system.eventStream.publish(ChannelStateChanged(self, nextStateData.channelId, peer, remoteNodeId, state, nextState, commitments_opt)) } - if (nextState == CLOSED) { // channel is closed, scheduling this actor for self destruction context.system.scheduler.scheduleOnce(1 minute, self, Symbol("shutdown")) @@ -2949,7 +3124,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with log.debug("emitting channel down event") if (d.lastAnnouncement_opt.nonEmpty) { // We tell the rest of the network that this channel shouldn't be used anymore. - val disabledUpdate = Announcements.makeChannelUpdate(nodeParams, remoteNodeId, Helpers.scidForChannelUpdate(d), d.commitments.params, d.channelUpdate.relayFees, Helpers.maxHtlcAmount(nodeParams, d.commitments), enable = false) + val disabledUpdate = Helpers.channelUpdate(nodeParams, scidForChannelUpdate(d), d.commitments, d.channelUpdate.relayFees, enable = false) context.system.eventStream.publish(LocalChannelUpdate(self, d.channelId, d.aliases, remoteNodeId, d.lastAnnouncedCommitment_opt, disabledUpdate, d.commitments)) } val lcd = LocalChannelDown(self, d.channelId, d.commitments.all.flatMap(_.shortChannelId_opt), d.aliases, remoteNodeId) @@ -3017,13 +3192,16 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with } } - /** On disconnection we clear up stashes. */ + /** On disconnection we clear up temporary mutable state that applies to the previous connection. */ onTransition { case _ -> OFFLINE => - sigStash = Nil announcementSigsStash = Map.empty announcementSigsSent = Set.empty spliceLockedSent = Map.empty[TxId, Long] + remoteNextCommitNonces = Map.empty + localCloseeNonce_opt = None + remoteCloseeNonce_opt = None + localCloserNonces_opt = None } /* @@ -3037,63 +3215,15 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with 888 888 d88P 888 888 Y888 8888888P" 88888888 8888888888 888 T88b "Y8888P" */ - /** For splices we will send one commit_sig per active commitments. */ - private def aggregateSigs(commit: CommitSig): Option[Seq[CommitSig]] = { - sigStash = sigStash :+ commit - log.debug("received sig for batch of size={}", commit.batchSize) - if (sigStash.size == commit.batchSize) { - val sigs = sigStash - sigStash = Nil - Some(sigs) - } else { - None - } - } - - private def handleCurrentFeerate(c: CurrentFeerates, d: ChannelDataWithCommitments) = { + private def handleCurrentFeerate(d: ChannelDataWithCommitments) = { val commitments = d.commitments.latest - val networkFeeratePerKw = nodeParams.onChainFeeConf.getCommitmentFeerate(nodeParams.currentBitcoinCoreFeerates, remoteNodeId, d.commitments.params.commitmentFormat, commitments.capacity) + val networkFeeratePerKw = nodeParams.onChainFeeConf.getCommitmentFeerate(nodeParams.currentBitcoinCoreFeerates, remoteNodeId, d.commitments.latest.commitmentFormat) val currentFeeratePerKw = commitments.localCommit.spec.commitTxFeerate - val shouldUpdateFee = d.commitments.params.localParams.paysCommitTxFees && nodeParams.onChainFeeConf.shouldUpdateFee(currentFeeratePerKw, networkFeeratePerKw) - val shouldClose = !d.commitments.params.localParams.paysCommitTxFees && - nodeParams.onChainFeeConf.feerateToleranceFor(d.commitments.remoteNodeId).isProposedFeerateTooLow(d.commitments.params.commitmentFormat, networkFeeratePerKw, currentFeeratePerKw) && - d.commitments.hasPendingOrProposedHtlcs // we close only if we have HTLCs potentially at risk + val shouldUpdateFee = d.commitments.localChannelParams.paysCommitTxFees && nodeParams.onChainFeeConf.shouldUpdateFee(currentFeeratePerKw, networkFeeratePerKw, d.commitments.latest.commitmentFormat) if (shouldUpdateFee) { self ! CMD_UPDATE_FEE(networkFeeratePerKw, commit = true) - stay() - } else if (shouldClose) { - handleLocalError(FeerateTooDifferent(d.channelId, localFeeratePerKw = networkFeeratePerKw, remoteFeeratePerKw = commitments.localCommit.spec.commitTxFeerate), d, Some(c)) - } else { - stay() - } - } - - /** - * This is used to check for the commitment fees when the channel is not operational but we have something at stake - * - * @param c the new feerates - * @param d the channel commtiments - * @return - */ - private def handleCurrentFeerateDisconnected(c: CurrentFeerates, d: ChannelDataWithCommitments) = { - val commitments = d.commitments.latest - val networkFeeratePerKw = nodeParams.onChainFeeConf.getCommitmentFeerate(nodeParams.currentBitcoinCoreFeerates, remoteNodeId, d.commitments.params.commitmentFormat, commitments.capacity) - val currentFeeratePerKw = commitments.localCommit.spec.commitTxFeerate - // if the network fees are too high we risk to not be able to confirm our current commitment - val shouldClose = networkFeeratePerKw > currentFeeratePerKw && - nodeParams.onChainFeeConf.feerateToleranceFor(d.commitments.remoteNodeId).isProposedFeerateTooLow(d.commitments.params.commitmentFormat, networkFeeratePerKw, currentFeeratePerKw) && - d.commitments.hasPendingOrProposedHtlcs // we close only if we have HTLCs potentially at risk - if (shouldClose) { - if (nodeParams.onChainFeeConf.closeOnOfflineMismatch) { - log.warning(s"closing OFFLINE channel due to fee mismatch: currentFeeratePerKw=$currentFeeratePerKw networkFeeratePerKw=$networkFeeratePerKw") - handleLocalError(FeerateTooDifferent(d.channelId, localFeeratePerKw = currentFeeratePerKw, remoteFeeratePerKw = networkFeeratePerKw), d, Some(c)) - } else { - log.warning(s"channel is OFFLINE but its fee mismatch is over the threshold: currentFeeratePerKw=$currentFeeratePerKw networkFeeratePerKw=$networkFeeratePerKw") - stay() - } - } else { - stay() } + stay() } private def handleCommandSuccess(c: channel.Command, newData: ChannelData) = { @@ -3143,7 +3273,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with if (d.channelUpdate.channelFlags.isEnabled) { // if the channel isn't disabled we generate a new channel_update log.debug("updating channel_update announcement (reason=disabled)") - val channelUpdate1 = Announcements.makeChannelUpdate(nodeParams, remoteNodeId, scidForChannelUpdate(d), d.commitments.params, d.channelUpdate.relayFees, Helpers.maxHtlcAmount(nodeParams, d.commitments), enable = false) + val channelUpdate1 = Helpers.channelUpdate(nodeParams, scidForChannelUpdate(d), d.commitments, d.channelUpdate.relayFees, enable = false) // then we update the state and replay the request self forward c // we use goto() to fire transitions @@ -3156,7 +3286,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with } private def handleUpdateRelayFeeDisconnected(c: CMD_UPDATE_RELAY_FEE, d: DATA_NORMAL) = { - val channelUpdate1 = Announcements.makeChannelUpdate(nodeParams, remoteNodeId, scidForChannelUpdate(d), d.commitments.params, Relayer.RelayFees(c.feeBase, c.feeProportionalMillionths), Helpers.maxHtlcAmount(nodeParams, d.commitments), enable = false) + val channelUpdate1 = Helpers.channelUpdate(nodeParams, scidForChannelUpdate(d), d.commitments, Relayer.RelayFees(c.feeBase, c.feeProportionalMillionths), enable = false) log.debug(s"updating relay fees: prev={} next={}", d.channelUpdate.toStringShort, channelUpdate1.toStringShort) val replyTo = if (c.replyTo == ActorRef.noSender) sender() else c.replyTo replyTo ! RES_SUCCESS(c, d.channelId) @@ -3254,6 +3384,117 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with } } + private def resendChannelReadyIfNeeded(channelReestablish: ChannelReestablish, d: DATA_NORMAL): (Option[ChannelReady], Option[AnnouncementSignatures]) = { + d.commitments.lastLocalLocked_opt match { + case None => (None, None) + // We only send channel_ready for initial funding transactions. + case Some(c) if c.fundingTxIndex != 0 => (None, None) + case Some(c) => + val remoteSpliceSupport = d.commitments.remoteChannelParams.initFeatures.hasFeature(Features.SplicePrototype) + // If our peer has not received our channel_ready, we retransmit it. + val notReceivedByRemote = remoteSpliceSupport && channelReestablish.yourLastFundingLocked_opt.isEmpty + // If next_local_commitment_number is 1 in both the channel_reestablish it sent and received, then the node + // MUST retransmit channel_ready, otherwise it MUST NOT + val notReceivedByRemoteLegacy = !remoteSpliceSupport && channelReestablish.nextLocalCommitmentNumber == 1 && c.localCommit.index == 0 + // If this is a public channel and we haven't announced the channel, we retransmit our channel_ready and + // will also send announcement_signatures. + val notAnnouncedYet = d.commitments.announceChannel && c.shortChannelId_opt.nonEmpty && d.lastAnnouncement_opt.isEmpty + val channelReady_opt = if (notAnnouncedYet || notReceivedByRemote || notReceivedByRemoteLegacy) { + log.debug("re-sending channel_ready") + Some(createChannelReady(d.aliases, d.commitments)) + } else { + None + } + val announcementSigs_opt = if (notAnnouncedYet) { + // The funding transaction is confirmed, so we've already sent our announcement_signatures. + // We haven't announced the channel yet, which means we haven't received our peer's announcement_signatures. + // We retransmit our announcement_signatures to let our peer know that we're ready to announce the channel. + val localAnnSigs = c.signAnnouncement(nodeParams, d.commitments.channelParams, channelKeys.fundingKey(c.fundingTxIndex)) + localAnnSigs.foreach(annSigs => announcementSigsSent += annSigs.shortChannelId) + localAnnSigs + } else { + None + } + (channelReady_opt, announcementSigs_opt) + } + } + + private def resumeSpliceSigningSessionIfNeeded(channelReestablish: ChannelReestablish, d: DATA_NORMAL): (SpliceStatus, Queue[LightningMessage]) = { + var sendQueue = Queue.empty[LightningMessage] + val spliceStatus1 = channelReestablish.nextFundingTxId_opt match { + case Some(fundingTxId) => + d.spliceStatus match { + case SpliceStatus.SpliceWaitingForSigs(signingSession) if signingSession.fundingTx.txId == fundingTxId => + if (channelReestablish.nextLocalCommitmentNumber == d.commitments.remoteCommitIndex) { + // They haven't received our commit_sig: we retransmit it. + // We're also waiting for signatures from them, and will send our tx_signatures once we receive them. + log.info("re-sending commit_sig for splice attempt with fundingTxIndex={} fundingTxId={}", signingSession.fundingTxIndex, signingSession.fundingTx.txId) + val fundingParams = signingSession.fundingParams + val remoteNonce_opt = channelReestablish.currentCommitNonce_opt + signingSession.remoteCommit.sign(d.commitments.channelParams, signingSession.remoteCommitParams, channelKeys, signingSession.fundingTxIndex, fundingParams.remoteFundingPubKey, signingSession.commitInput(channelKeys), fundingParams.commitmentFormat, remoteNonce_opt) match { + case Left(f) => sendQueue = sendQueue :+ Warning(d.channelId, f.getMessage) + case Right(commitSig) => sendQueue = sendQueue :+ commitSig + } + } + d.spliceStatus + case _ if d.commitments.latest.fundingTxId == fundingTxId => + d.commitments.latest.localFundingStatus match { + case dfu: LocalFundingStatus.DualFundedUnconfirmedFundingTx => + // We've already received their commit_sig and sent our tx_signatures. We retransmit our + // tx_signatures and our commit_sig if they haven't received it already. + if (channelReestablish.nextLocalCommitmentNumber == d.commitments.remoteCommitIndex) { + log.info("re-sending commit_sig and tx_signatures for fundingTxIndex={} fundingTxId={}", d.commitments.latest.fundingTxIndex, d.commitments.latest.fundingTxId) + val remoteNonce_opt = channelReestablish.currentCommitNonce_opt + d.commitments.latest.remoteCommit.sign(d.commitments.channelParams, d.commitments.latest.remoteCommitParams, channelKeys, d.commitments.latest.fundingTxIndex, d.commitments.latest.remoteFundingPubKey, d.commitments.latest.commitInput(channelKeys), d.commitments.latest.commitmentFormat, remoteNonce_opt) match { + case Left(f) => sendQueue = sendQueue :+ Warning(d.channelId, f.getMessage) + case Right(commitSig) => sendQueue = sendQueue :+ commitSig :+ dfu.sharedTx.localSigs + } + } else { + log.info("re-sending tx_signatures for fundingTxIndex={} fundingTxId={}", d.commitments.latest.fundingTxIndex, d.commitments.latest.fundingTxId) + sendQueue = sendQueue :+ dfu.sharedTx.localSigs + } + case fundingStatus => + // They have not received our tx_signatures, but they must have received our commit_sig, otherwise we would be in the case above. + log.info("re-sending tx_signatures for fundingTxIndex={} fundingTxId={} (already published or confirmed)", d.commitments.latest.fundingTxIndex, d.commitments.latest.fundingTxId) + sendQueue = sendQueue ++ fundingStatus.localSigs_opt.toSeq + } + d.spliceStatus + case _ => + // The fundingTxId must be for a splice attempt that we didn't store (we got disconnected before receiving + // their tx_complete): we tell them to abort that splice attempt. + log.info(s"aborting obsolete splice attempt for fundingTxId=$fundingTxId") + sendQueue = sendQueue :+ TxAbort(d.channelId, SpliceAttemptAborted(d.channelId).getMessage) + SpliceStatus.SpliceAborted + } + case None => d.spliceStatus + } + (spliceStatus1, sendQueue) + } + + private def resendSpliceLockedIfNeeded(channelReestablish: ChannelReestablish, commitments: Commitments, lastAnnouncement_opt: Option[ChannelAnnouncement]): Option[SpliceLocked] = { + commitments.lastLocalLocked_opt match { + case None => None + // We only send splice_locked for splice transactions. + case Some(c) if c.fundingTxIndex == 0 => None + case Some(c) => + // If our peer has not received our splice_locked, we retransmit it. + val notReceivedByRemote = !channelReestablish.yourLastFundingLocked_opt.contains(c.fundingTxId) + // If this is a public channel and we haven't announced the splice, we retransmit our splice_locked and + // will exchange announcement_signatures afterwards. + val notAnnouncedYet = commitments.announceChannel && lastAnnouncement_opt.forall(ann => !c.shortChannelId_opt.contains(ann.shortChannelId)) + if (notReceivedByRemote || notAnnouncedYet) { + // Retransmission of local announcement_signatures for splices are done when receiving splice_locked, no need + // to retransmit here. + log.debug("re-sending splice_locked for fundingTxId={}", c.fundingTxId) + spliceLockedSent += (c.fundingTxId -> c.fundingTxIndex) + trimSpliceLockedSentIfNeeded() + Some(SpliceLocked(commitments.channelId, c.fundingTxId)) + } else { + None + } + } + } + /** * Return full information about a known closing tx. */ @@ -3270,17 +3511,17 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with val targetFeerate = nodeParams.onChainFeeConf.getFundingFeerate(nodeParams.currentFeeratesForFundingClosing) val fundingContribution = InteractiveTxFunder.computeSpliceContribution( isInitiator = true, - sharedInput = Multisig2of2Input(parentCommitment), + sharedInput = SharedFundingInput(channelKeys, parentCommitment), spliceInAmount = cmd.additionalLocalFunding, spliceOut = cmd.spliceOutputs, targetFeerate = targetFeerate) - val commitTxFees = if (d.commitments.params.localParams.paysCommitTxFees) { - Transactions.commitTxTotalCost(d.commitments.params.remoteParams.dustLimit, parentCommitment.remoteCommit.spec, d.commitments.params.commitmentFormat) + val commitTxFees = if (d.commitments.localChannelParams.paysCommitTxFees) { + Transactions.commitTxTotalCost(parentCommitment.remoteCommitParams.dustLimit, parentCommitment.remoteCommit.spec, parentCommitment.commitmentFormat) } else { 0.sat } - if (fundingContribution < 0.sat && parentCommitment.localCommit.spec.toLocal + fundingContribution < parentCommitment.localChannelReserve(d.commitments.params).max(commitTxFees)) { - log.warning(s"cannot do splice: insufficient funds (commitTxFees=$commitTxFees reserve=${parentCommitment.localChannelReserve(d.commitments.params)})") + if (fundingContribution < 0.sat && parentCommitment.localCommit.spec.toLocal + fundingContribution < parentCommitment.localChannelReserve(d.commitments.channelParams).max(commitTxFees)) { + log.warning(s"cannot do splice: insufficient funds (commitTxFees=$commitTxFees reserve=${parentCommitment.localChannelReserve(d.commitments.channelParams)})") Left(InvalidSpliceRequest(d.channelId)) } else if (cmd.spliceOut_opt.map(_.scriptPubKey).exists(!MutualClose.isValidFinalScriptPubkey(_, allowAnySegwit = true, allowOpReturn = false))) { log.warning("cannot do splice: invalid splice-out script") @@ -3291,10 +3532,11 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with fundingContribution = fundingContribution, lockTime = nodeParams.currentBlockHeight.toLong, feerate = targetFeerate, - fundingPubKey = keyManager.fundingPublicKey(d.commitments.params.localParams.fundingKeyPath, parentCommitment.fundingTxIndex + 1).publicKey, + fundingPubKey = channelKeys.fundingKey(parentCommitment.fundingTxIndex + 1).publicKey, pushAmount = cmd.pushAmount, requireConfirmedInputs = nodeParams.channelConf.requireConfirmedInputsForDualFunding, - requestFunding_opt = cmd.requestFunding_opt + requestFunding_opt = cmd.requestFunding_opt, + channelType_opt = cmd.channelType_opt ) Right(spliceInit) } @@ -3304,13 +3546,13 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with getSpliceRbfContext(Some(cmd), d).flatMap(rbf => { // We use the same contribution as the previous splice attempt. val fundingContribution = rbf.latestFundingTx.fundingParams.localContribution - val commitTxFees = if (d.commitments.params.localParams.paysCommitTxFees) { - Transactions.commitTxTotalCost(d.commitments.params.remoteParams.dustLimit, rbf.parentCommitment.remoteCommit.spec, d.commitments.params.commitmentFormat) + val commitTxFees = if (d.commitments.localChannelParams.paysCommitTxFees) { + Transactions.commitTxTotalCost(rbf.parentCommitment.remoteCommitParams.dustLimit, rbf.parentCommitment.remoteCommit.spec, rbf.latestFundingTx.fundingParams.commitmentFormat) } else { 0.sat } - if (fundingContribution < 0.sat && rbf.parentCommitment.localCommit.spec.toLocal + fundingContribution < rbf.parentCommitment.localChannelReserve(d.commitments.params).max(commitTxFees)) { - log.warning(s"cannot do rbf: insufficient funds (commitTxFees=$commitTxFees reserve=${rbf.parentCommitment.localChannelReserve(d.commitments.params)})") + if (fundingContribution < 0.sat && rbf.parentCommitment.localCommit.spec.toLocal + fundingContribution < rbf.parentCommitment.localChannelReserve(d.commitments.channelParams).max(commitTxFees)) { + log.warning(s"cannot do rbf: insufficient funds (commitTxFees=$commitTxFees reserve=${rbf.parentCommitment.localChannelReserve(d.commitments.channelParams)})") Left(InvalidSpliceRequest(d.channelId)) } else { val txInitRbf = TxInitRbf(d.channelId, cmd.lockTime, cmd.targetFeerate, fundingContribution, rbf.latestFundingTx.fundingParams.requireConfirmedInputs.forRemote, cmd.requestFunding_opt) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenDualFunded.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenDualFunded.scala index 6eb40193eb..586b14a560 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenDualFunded.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenDualFunded.scala @@ -19,7 +19,6 @@ package fr.acinq.eclair.channel.fsm import akka.actor.typed.scaladsl.adapter.{ClassicActorContextOps, actorRefAdapter} import fr.acinq.bitcoin.scalacompat.SatoshiLong import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ -import fr.acinq.eclair.channel.Helpers.Funding import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.fsm.Channel._ import fr.acinq.eclair.channel.fund.InteractiveTxBuilder.{FullySignedSharedTransaction, InteractiveTxParams, PartiallySignedSharedTransaction, RequireConfirmedInputs} @@ -27,8 +26,9 @@ import fr.acinq.eclair.channel.fund.{InteractiveTxBuilder, InteractiveTxSigningS import fr.acinq.eclair.channel.publish.TxPublisher.SetChannelId import fr.acinq.eclair.crypto.ShaChain import fr.acinq.eclair.io.Peer.{LiquidityPurchaseSigned, OpenChannelResponse} +import fr.acinq.eclair.transactions.Transactions import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{ToMilliSatoshiConversion, UInt64, randomBytes32} +import fr.acinq.eclair.{ToMilliSatoshiConversion, randomBytes32} /** * Created by t-bast on 19/04/2022. @@ -104,9 +104,8 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers { when(WAIT_FOR_INIT_DUAL_FUNDED_CHANNEL)(handleExceptions { case Event(input: INPUT_INIT_CHANNEL_INITIATOR, _) => - val fundingPubKey = keyManager.fundingPublicKey(input.localParams.fundingKeyPath, fundingTxIndex = 0).publicKey - val channelKeyPath = keyManager.keyPath(input.localParams, input.channelConfig) - val upfrontShutdownScript_opt = input.localParams.upfrontShutdownScript_opt.map(scriptPubKey => ChannelTlv.UpfrontShutdownScriptTlv(scriptPubKey)) + val fundingPubKey = channelKeys.fundingKey(fundingTxIndex = 0).publicKey + val upfrontShutdownScript_opt = input.localChannelParams.upfrontShutdownScript_opt.map(scriptPubKey => ChannelTlv.UpfrontShutdownScriptTlv(scriptPubKey)) val tlvs: Set[OpenDualFundedChannelTlv] = Set( upfrontShutdownScript_opt, Some(ChannelTlv.ChannelTypeTlv(input.channelType)), @@ -120,19 +119,19 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers { fundingFeerate = input.fundingTxFeerate, commitmentFeerate = input.commitTxFeerate, fundingAmount = input.fundingAmount, - dustLimit = input.localParams.dustLimit, - maxHtlcValueInFlightMsat = UInt64(input.localParams.maxHtlcValueInFlightMsat.toLong), - htlcMinimum = input.localParams.htlcMinimum, - toSelfDelay = input.localParams.toSelfDelay, - maxAcceptedHtlcs = input.localParams.maxAcceptedHtlcs, + dustLimit = input.proposedCommitParams.localDustLimit, + maxHtlcValueInFlightMsat = input.proposedCommitParams.localMaxHtlcValueInFlight, + htlcMinimum = input.proposedCommitParams.localHtlcMinimum, + toSelfDelay = input.proposedCommitParams.toRemoteDelay, + maxAcceptedHtlcs = input.proposedCommitParams.localMaxAcceptedHtlcs, lockTime = nodeParams.currentBlockHeight.toLong, fundingPubkey = fundingPubKey, - revocationBasepoint = keyManager.revocationPoint(channelKeyPath).publicKey, - paymentBasepoint = input.localParams.walletStaticPaymentBasepoint.getOrElse(keyManager.paymentPoint(channelKeyPath).publicKey), - delayedPaymentBasepoint = keyManager.delayedPaymentPoint(channelKeyPath).publicKey, - htlcBasepoint = keyManager.htlcPoint(channelKeyPath).publicKey, - firstPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, 0), - secondPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, 1), + revocationBasepoint = channelKeys.revocationBasePoint, + paymentBasepoint = channelKeys.paymentBasePoint, + delayedPaymentBasepoint = channelKeys.delayedPaymentBasePoint, + htlcBasepoint = channelKeys.htlcBasePoint, + firstPerCommitmentPoint = channelKeys.commitmentPoint(0), + secondPerCommitmentPoint = channelKeys.commitmentPoint(1), channelFlags = input.channelFlags, tlvStream = TlvStream(tlvs)) goto(WAIT_FOR_ACCEPT_DUAL_FUNDED_CHANNEL) using DATA_WAIT_FOR_ACCEPT_DUAL_FUNDED_CHANNEL(input, open) sending open @@ -140,38 +139,29 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers { when(WAIT_FOR_OPEN_DUAL_FUNDED_CHANNEL)(handleExceptions { case Event(open: OpenDualFundedChannel, d: DATA_WAIT_FOR_OPEN_DUAL_FUNDED_CHANNEL) => - import d.init.{localParams, remoteInit} - val localFundingPubkey = keyManager.fundingPublicKey(localParams.fundingKeyPath, fundingTxIndex = 0).publicKey - val fundingScript = Funding.makeFundingPubKeyScript(localFundingPubkey, open.fundingPubkey) - Helpers.validateParamsDualFundedNonInitiator(nodeParams, d.init.channelType, open, fundingScript, remoteNodeId, localParams.initFeatures, remoteInit.features, d.init.fundingContribution_opt) match { + val localFundingPubkey = channelKeys.fundingKey(fundingTxIndex = 0).publicKey + val fundingScript = Transactions.makeFundingScript(localFundingPubkey, open.fundingPubkey, d.init.channelType.commitmentFormat).pubkeyScript + Helpers.validateParamsDualFundedNonInitiator(nodeParams, open, fundingScript, remoteNodeId, d.init.localChannelParams.initFeatures, d.init.remoteInit.features, d.init.fundingContribution_opt) match { case Left(t) => handleLocalError(t, d, Some(open)) case Right((channelFeatures, remoteShutdownScript, willFund_opt)) => context.system.eventStream.publish(ChannelCreated(self, peer, remoteNodeId, isOpener = false, open.temporaryChannelId, open.commitmentFeerate, Some(open.fundingFeerate))) - val remoteParams = RemoteParams( + val remoteChannelParams = RemoteChannelParams( nodeId = remoteNodeId, - dustLimit = open.dustLimit, - maxHtlcValueInFlightMsat = open.maxHtlcValueInFlightMsat, initialRequestedChannelReserve_opt = None, // channel reserve will be computed based on channel capacity - htlcMinimum = open.htlcMinimum, - toSelfDelay = open.toSelfDelay, - maxAcceptedHtlcs = open.maxAcceptedHtlcs, revocationBasepoint = open.revocationBasepoint, paymentBasepoint = open.paymentBasepoint, delayedPaymentBasepoint = open.delayedPaymentBasepoint, htlcBasepoint = open.htlcBasepoint, - initFeatures = remoteInit.features, + initFeatures = d.init.remoteInit.features, upfrontShutdownScript_opt = remoteShutdownScript) - log.debug("remote params: {}", remoteParams) - val channelKeyPath = keyManager.keyPath(localParams, d.init.channelConfig) - val revocationBasePoint = keyManager.revocationPoint(channelKeyPath).publicKey // We've exchanged open_channel2 and accept_channel2, we now know the final channelId. - val channelId = Helpers.computeChannelId(open.revocationBasepoint, revocationBasePoint) - val channelParams = ChannelParams(channelId, d.init.channelConfig, channelFeatures, localParams, remoteParams, open.channelFlags) + val channelId = Helpers.computeChannelId(open.revocationBasepoint, channelKeys.revocationBasePoint) + val channelParams = ChannelParams(channelId, d.init.channelConfig, channelFeatures, d.init.localChannelParams, remoteChannelParams, open.channelFlags) + val localCommitParams = CommitParams(d.init.proposedCommitParams.localDustLimit, d.init.proposedCommitParams.localHtlcMinimum, d.init.proposedCommitParams.localMaxHtlcValueInFlight, d.init.proposedCommitParams.localMaxAcceptedHtlcs, open.toSelfDelay) + val remoteCommitParams = CommitParams(open.dustLimit, open.htlcMinimum, open.maxHtlcValueInFlightMsat, open.maxAcceptedHtlcs, d.init.proposedCommitParams.toRemoteDelay) val localAmount = d.init.fundingContribution_opt.map(_.fundingAmount).getOrElse(0 sat) - val minDepth_opt = channelParams.minDepth(nodeParams.channelConf.minDepth) - val upfrontShutdownScript_opt = localParams.upfrontShutdownScript_opt.map(scriptPubKey => ChannelTlv.UpfrontShutdownScriptTlv(scriptPubKey)) val tlvs: Set[AcceptDualFundedChannelTlv] = Set( - upfrontShutdownScript_opt, + d.init.localChannelParams.upfrontShutdownScript_opt.map(scriptPubKey => ChannelTlv.UpfrontShutdownScriptTlv(scriptPubKey)), Some(ChannelTlv.ChannelTypeTlv(d.init.channelType)), if (d.init.requireConfirmedInputs) Some(ChannelTlv.RequireConfirmedInputsTlv()) else None, willFund_opt.map(l => ChannelTlv.ProvideFundingTlv(l.willFund)), @@ -181,19 +171,19 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers { val accept = AcceptDualFundedChannel( temporaryChannelId = open.temporaryChannelId, fundingAmount = localAmount, - dustLimit = localParams.dustLimit, - maxHtlcValueInFlightMsat = UInt64(localParams.maxHtlcValueInFlightMsat.toLong), - htlcMinimum = localParams.htlcMinimum, - minimumDepth = minDepth_opt.getOrElse(0).toLong, - toSelfDelay = localParams.toSelfDelay, - maxAcceptedHtlcs = localParams.maxAcceptedHtlcs, + dustLimit = localCommitParams.dustLimit, + maxHtlcValueInFlightMsat = localCommitParams.maxHtlcValueInFlight, + htlcMinimum = localCommitParams.htlcMinimum, + minimumDepth = channelParams.minDepth(nodeParams.channelConf.minDepth).getOrElse(0).toLong, + toSelfDelay = remoteCommitParams.toSelfDelay, + maxAcceptedHtlcs = localCommitParams.maxAcceptedHtlcs, fundingPubkey = localFundingPubkey, - revocationBasepoint = revocationBasePoint, - paymentBasepoint = localParams.walletStaticPaymentBasepoint.getOrElse(keyManager.paymentPoint(channelKeyPath).publicKey), - delayedPaymentBasepoint = keyManager.delayedPaymentPoint(channelKeyPath).publicKey, - htlcBasepoint = keyManager.htlcPoint(channelKeyPath).publicKey, - firstPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, 0), - secondPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, 1), + revocationBasepoint = channelKeys.revocationBasePoint, + paymentBasepoint = channelKeys.paymentBasePoint, + delayedPaymentBasepoint = channelKeys.delayedPaymentBasePoint, + htlcBasepoint = channelKeys.htlcBasePoint, + firstPerCommitmentPoint = channelKeys.commitmentPoint(0), + secondPerCommitmentPoint = channelKeys.commitmentPoint(1), tlvStream = TlvStream(tlvs)) peer ! ChannelIdAssigned(self, remoteNodeId, accept.temporaryChannelId, channelId) // we notify the peer asap so it knows how to route messages txPublisher ! SetChannelId(remoteNodeId, channelId) @@ -201,12 +191,13 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers { // We start the interactive-tx funding protocol. val fundingParams = InteractiveTxParams( channelId = channelId, - isInitiator = localParams.isChannelOpener, + isInitiator = d.init.localChannelParams.isChannelOpener, localContribution = accept.fundingAmount, remoteContribution = open.fundingAmount, sharedInput_opt = None, remoteFundingPubKey = open.fundingPubkey, localOutputs = Nil, + commitmentFormat = d.init.channelType.commitmentFormat, lockTime = open.lockTime, dustLimit = open.dustLimit.max(accept.dustLimit), targetFeerate = open.fundingFeerate, @@ -216,12 +207,12 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers { val txBuilder = context.spawnAnonymous(InteractiveTxBuilder( randomBytes32(), nodeParams, fundingParams, - channelParams, purpose, + channelParams, localCommitParams, remoteCommitParams, channelKeys, purpose, localPushAmount = accept.pushAmount, remotePushAmount = open.pushAmount, willFund_opt.map(_.purchase), wallet)) txBuilder ! InteractiveTxBuilder.Start(self) - goto(WAIT_FOR_DUAL_FUNDING_CREATED) using DATA_WAIT_FOR_DUAL_FUNDING_CREATED(channelId, channelParams, open.secondPerCommitmentPoint, accept.pushAmount, open.pushAmount, txBuilder, deferred = None, replyTo_opt = None) sending accept + goto(WAIT_FOR_DUAL_FUNDING_CREATED) using DATA_WAIT_FOR_DUAL_FUNDING_CREATED(channelId, channelParams, localCommitParams, remoteCommitParams, open.secondPerCommitmentPoint, accept.pushAmount, open.pushAmount, txBuilder, deferred = None, replyTo_opt = None) sending accept } case Event(c: CloseCommand, d) => handleFastClose(c, d.channelId) @@ -233,8 +224,7 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers { when(WAIT_FOR_ACCEPT_DUAL_FUNDED_CHANNEL)(handleExceptions { case Event(accept: AcceptDualFundedChannel, d: DATA_WAIT_FOR_ACCEPT_DUAL_FUNDED_CHANNEL) => - import d.init.{localParams, remoteInit} - Helpers.validateParamsDualFundedInitiator(nodeParams, remoteNodeId, d.init.channelType, localParams.initFeatures, remoteInit.features, d.lastSent, accept) match { + Helpers.validateParamsDualFundedInitiator(nodeParams, remoteNodeId, d.init.localChannelParams.initFeatures, d.init.remoteInit.features, d.lastSent, accept) match { case Left(t) => d.init.replyTo ! OpenChannelResponse.Rejected(t.getMessage) handleLocalError(t, d, Some(accept)) @@ -244,33 +234,30 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers { peer ! ChannelIdAssigned(self, remoteNodeId, accept.temporaryChannelId, channelId) // we notify the peer asap so it knows how to route messages txPublisher ! SetChannelId(remoteNodeId, channelId) context.system.eventStream.publish(ChannelIdAssigned(self, remoteNodeId, accept.temporaryChannelId, channelId)) - val remoteParams = RemoteParams( + val remoteChannelParams = RemoteChannelParams( nodeId = remoteNodeId, - dustLimit = accept.dustLimit, - maxHtlcValueInFlightMsat = accept.maxHtlcValueInFlightMsat, initialRequestedChannelReserve_opt = None, // channel reserve will be computed based on channel capacity - htlcMinimum = accept.htlcMinimum, - toSelfDelay = accept.toSelfDelay, - maxAcceptedHtlcs = accept.maxAcceptedHtlcs, revocationBasepoint = accept.revocationBasepoint, paymentBasepoint = accept.paymentBasepoint, delayedPaymentBasepoint = accept.delayedPaymentBasepoint, htlcBasepoint = accept.htlcBasepoint, - initFeatures = remoteInit.features, + initFeatures = d.init.remoteInit.features, upfrontShutdownScript_opt = remoteShutdownScript) - log.debug("remote params: {}", remoteParams) // We start the interactive-tx funding protocol. - val channelParams = ChannelParams(channelId, d.init.channelConfig, channelFeatures, localParams, remoteParams, d.lastSent.channelFlags) + val channelParams = ChannelParams(channelId, d.init.channelConfig, channelFeatures, d.init.localChannelParams, remoteChannelParams, d.lastSent.channelFlags) + val localCommitParams = CommitParams(d.init.proposedCommitParams.localDustLimit, d.init.proposedCommitParams.localHtlcMinimum, d.init.proposedCommitParams.localMaxHtlcValueInFlight, d.init.proposedCommitParams.localMaxAcceptedHtlcs, accept.toSelfDelay) + val remoteCommitParams = CommitParams(accept.dustLimit, accept.htlcMinimum, accept.maxHtlcValueInFlightMsat, accept.maxAcceptedHtlcs, d.init.proposedCommitParams.toRemoteDelay) val localAmount = d.lastSent.fundingAmount val remoteAmount = accept.fundingAmount val fundingParams = InteractiveTxParams( channelId = channelId, - isInitiator = localParams.isChannelOpener, + isInitiator = d.init.localChannelParams.isChannelOpener, localContribution = localAmount, remoteContribution = remoteAmount, sharedInput_opt = None, remoteFundingPubKey = accept.fundingPubkey, localOutputs = Nil, + commitmentFormat = d.init.channelType.commitmentFormat, lockTime = d.lastSent.lockTime, dustLimit = d.lastSent.dustLimit.max(accept.dustLimit), targetFeerate = d.lastSent.fundingFeerate, @@ -280,12 +267,12 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers { val txBuilder = context.spawnAnonymous(InteractiveTxBuilder( randomBytes32(), nodeParams, fundingParams, - channelParams, purpose, + channelParams, localCommitParams, remoteCommitParams, channelKeys, purpose, localPushAmount = d.lastSent.pushAmount, remotePushAmount = accept.pushAmount, liquidityPurchase_opt = liquidityPurchase_opt, wallet)) txBuilder ! InteractiveTxBuilder.Start(self) - goto(WAIT_FOR_DUAL_FUNDING_CREATED) using DATA_WAIT_FOR_DUAL_FUNDING_CREATED(channelId, channelParams, accept.secondPerCommitmentPoint, d.lastSent.pushAmount, accept.pushAmount, txBuilder, deferred = None, replyTo_opt = Some(d.init.replyTo)) + goto(WAIT_FOR_DUAL_FUNDING_CREATED) using DATA_WAIT_FOR_DUAL_FUNDING_CREATED(channelId, channelParams, localCommitParams, remoteCommitParams, accept.secondPerCommitmentPoint, d.lastSent.pushAmount, accept.pushAmount, txBuilder, deferred = None, replyTo_opt = Some(d.init.replyTo)) } case Event(c: CloseCommand, d: DATA_WAIT_FOR_ACCEPT_DUAL_FUNDED_CHANNEL) => @@ -298,11 +285,11 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers { case Event(INPUT_DISCONNECTED, d: DATA_WAIT_FOR_ACCEPT_DUAL_FUNDED_CHANNEL) => d.init.replyTo ! OpenChannelResponse.Disconnected - goto(CLOSED) + goto(CLOSED) using IgnoreClosedData(d) case Event(TickChannelOpenTimeout, d: DATA_WAIT_FOR_ACCEPT_DUAL_FUNDED_CHANNEL) => d.init.replyTo ! OpenChannelResponse.TimedOut - goto(CLOSED) + goto(CLOSED) using IgnoreClosedData(d) }) when(WAIT_FOR_DUAL_FUNDING_CREATED)(handleExceptions { @@ -315,12 +302,12 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers { log.info("our peer aborted the dual funding flow: ascii='{}' bin={}", msg.toAscii, msg.data) d.txBuilder ! InteractiveTxBuilder.Abort d.replyTo_opt.foreach(_ ! OpenChannelResponse.RemoteError(msg.toAscii)) - goto(CLOSED) sending TxAbort(d.channelId, DualFundingAborted(d.channelId).getMessage) + goto(CLOSED) using IgnoreClosedData(d) sending TxAbort(d.channelId, DualFundingAborted(d.channelId).getMessage) case _: TxSignatures => log.info("received unexpected tx_signatures") d.txBuilder ! InteractiveTxBuilder.Abort d.replyTo_opt.foreach(_ ! OpenChannelResponse.Rejected(UnexpectedFundingSignatures(d.channelId).getMessage)) - goto(CLOSED) sending TxAbort(d.channelId, UnexpectedFundingSignatures(d.channelId).getMessage) + goto(CLOSED) using IgnoreClosedData(d) sending TxAbort(d.channelId, UnexpectedFundingSignatures(d.channelId).getMessage) case _: TxInitRbf => log.info("ignoring unexpected tx_init_rbf message") stay() sending Warning(d.channelId, InvalidRbfAttempt(d.channelId).getMessage) @@ -335,17 +322,18 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers { case Event(msg: InteractiveTxBuilder.Response, d: DATA_WAIT_FOR_DUAL_FUNDING_CREATED) => msg match { case InteractiveTxBuilder.SendMessage(_, msg) => stay() sending msg - case InteractiveTxBuilder.Succeeded(status, commitSig, liquidityPurchase_opt) => + case InteractiveTxBuilder.Succeeded(status, commitSig, liquidityPurchase_opt, nextRemoteCommitNonce_opt) => + nextRemoteCommitNonce_opt.foreach { case (txId, nonce) => remoteNextCommitNonces = remoteNextCommitNonces + (txId -> nonce) } d.deferred.foreach(self ! _) d.replyTo_opt.foreach(_ ! OpenChannelResponse.Created(d.channelId, status.fundingTx.txId, status.fundingTx.tx.localFees.truncateToSatoshi)) liquidityPurchase_opt.collect { - case purchase if !status.fundingParams.isInitiator => peer ! LiquidityPurchaseSigned(d.channelId, status.fundingTx.txId, status.fundingTxIndex, d.channelParams.remoteParams.htlcMinimum, purchase) + case purchase if !status.fundingParams.isInitiator => peer ! LiquidityPurchaseSigned(d.channelId, status.fundingTx.txId, status.fundingTxIndex, d.remoteCommitParams.htlcMinimum, purchase) } - val d1 = DATA_WAIT_FOR_DUAL_FUNDING_SIGNED(d.channelParams, d.secondRemotePerCommitmentPoint, d.localPushAmount, d.remotePushAmount, status, None) + val d1 = DATA_WAIT_FOR_DUAL_FUNDING_SIGNED(d.channelParams, d.secondRemotePerCommitmentPoint, d.localPushAmount, d.remotePushAmount, status) goto(WAIT_FOR_DUAL_FUNDING_SIGNED) using d1 storing() sending commitSig case f: InteractiveTxBuilder.Failed => d.replyTo_opt.foreach(_ ! OpenChannelResponse.Rejected(f.cause.getMessage)) - goto(CLOSED) sending TxAbort(d.channelId, f.cause.getMessage) + goto(CLOSED) using IgnoreClosedData(d) sending TxAbort(d.channelId, f.cause.getMessage) } case Event(c: CloseCommand, d: DATA_WAIT_FOR_DUAL_FUNDING_CREATED) => @@ -361,20 +349,20 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers { case Event(INPUT_DISCONNECTED, d: DATA_WAIT_FOR_DUAL_FUNDING_CREATED) => d.txBuilder ! InteractiveTxBuilder.Abort d.replyTo_opt.foreach(_ ! OpenChannelResponse.Disconnected) - goto(CLOSED) + goto(CLOSED) using IgnoreClosedData(d) case Event(TickChannelOpenTimeout, d: DATA_WAIT_FOR_DUAL_FUNDING_CREATED) => d.txBuilder ! InteractiveTxBuilder.Abort d.replyTo_opt.foreach(_ ! OpenChannelResponse.TimedOut) - goto(CLOSED) + goto(CLOSED) using IgnoreClosedData(d) }) when(WAIT_FOR_DUAL_FUNDING_SIGNED)(handleExceptions { case Event(commitSig: CommitSig, d: DATA_WAIT_FOR_DUAL_FUNDING_SIGNED) => - d.signingSession.receiveCommitSig(nodeParams, d.channelParams, commitSig) match { + d.signingSession.receiveCommitSig(d.channelParams, channelKeys, commitSig, nodeParams.currentBlockHeight) match { case Left(f) => rollbackFundingAttempt(d.signingSession.fundingTx.tx, Nil) - goto(CLOSED) sending Error(d.channelId, f.getMessage) + goto(CLOSED) using IgnoreClosedData(d) sending Error(d.channelId, f.getMessage) case Right(signingSession1) => signingSession1 match { case signingSession1: InteractiveTxSigningSession.WaitingForSigs => // In theory we don't have to store their commit_sig here, as they would re-send it if we disconnect, but @@ -388,7 +376,7 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers { val minDepth_opt = d.channelParams.minDepth(nodeParams.channelConf.minDepth) watchFundingConfirmed(d.signingSession.fundingTx.txId, minDepth_opt, delay_opt = None) val commitments = Commitments( - params = d.channelParams, + channelParams = d.channelParams, changes = CommitmentChanges.init(), active = List(signingSession1.commitment), remoteNextCommitInfo = Right(d.secondRemotePerCommitmentPoint), @@ -403,15 +391,15 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers { case Event(msg: InteractiveTxMessage, d: DATA_WAIT_FOR_DUAL_FUNDING_SIGNED) => msg match { case txSigs: TxSignatures => - d.signingSession.receiveTxSigs(nodeParams, d.channelParams, txSigs) match { + d.signingSession.receiveTxSigs(channelKeys, txSigs, nodeParams.currentBlockHeight) match { case Left(f) => rollbackFundingAttempt(d.signingSession.fundingTx.tx, Nil) - goto(CLOSED) sending Error(d.channelId, f.getMessage) + goto(CLOSED) using IgnoreClosedData(d) sending Error(d.channelId, f.getMessage) case Right(signingSession) => val minDepth_opt = d.channelParams.minDepth(nodeParams.channelConf.minDepth) watchFundingConfirmed(d.signingSession.fundingTx.txId, minDepth_opt, delay_opt = None) val commitments = Commitments( - params = d.channelParams, + channelParams = d.channelParams, changes = CommitmentChanges.init(), active = List(signingSession.commitment), remoteNextCommitInfo = Right(d.secondRemotePerCommitmentPoint), @@ -424,7 +412,7 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers { case msg: TxAbort => log.info("our peer aborted the dual funding flow: ascii='{}' bin={}", msg.toAscii, msg.data) rollbackFundingAttempt(d.signingSession.fundingTx.tx, Nil) - goto(CLOSED) sending TxAbort(d.channelId, DualFundingAborted(d.channelId).getMessage) + goto(CLOSED) using IgnoreClosedData(d) sending TxAbort(d.channelId, DualFundingAborted(d.channelId).getMessage) case msg: InteractiveTxConstructionMessage => log.info("received unexpected interactive-tx message: {}", msg.getClass.getSimpleName) stay() sending Warning(d.channelId, UnexpectedInteractiveTxMessage(d.channelId, msg).getMessage) @@ -452,7 +440,7 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers { when(WAIT_FOR_DUAL_FUNDING_CONFIRMED)(handleExceptions { case Event(txSigs: TxSignatures, d: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED) => d.latestFundingTx.sharedTx match { - case fundingTx: PartiallySignedSharedTransaction => InteractiveTxSigningSession.addRemoteSigs(keyManager, d.commitments.params, d.latestFundingTx.fundingParams, fundingTx, txSigs) match { + case fundingTx: PartiallySignedSharedTransaction => InteractiveTxSigningSession.addRemoteSigs(channelKeys, d.latestFundingTx.fundingParams, fundingTx, txSigs) match { case Left(cause) => val unsignedFundingTx = fundingTx.tx.buildUnsignedTx() log.warning("received invalid tx_signatures for txid={} (current funding txid={}): {}", txSigs.txId, unsignedFundingTx.txid, cause.getMessage) @@ -471,12 +459,12 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers { case _: FullySignedSharedTransaction => d.status match { case DualFundingStatus.RbfWaitingForSigs(signingSession) => - signingSession.receiveTxSigs(nodeParams, d.commitments.params, txSigs) match { + signingSession.receiveTxSigs(channelKeys, txSigs, nodeParams.currentBlockHeight) match { case Left(f) => rollbackRbfAttempt(signingSession, d) stay() using d.copy(status = DualFundingStatus.RbfAborted) sending TxAbort(d.channelId, f.getMessage) case Right(signingSession1) => - val minDepth_opt = d.commitments.params.minDepth(nodeParams.channelConf.minDepth) + val minDepth_opt = d.commitments.channelParams.minDepth(nodeParams.channelConf.minDepth) watchFundingConfirmed(signingSession.fundingTx.txId, minDepth_opt, delay_opt = None) val commitments1 = d.commitments.add(signingSession1.commitment) val d1 = DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED(commitments1, d.localPushAmount, d.remotePushAmount, d.waitingSince, d.lastChecked, DualFundingStatus.WaitingForConfirmations, d.deferred) @@ -493,17 +481,21 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers { } case Event(cmd: CMD_BUMP_FUNDING_FEE, d: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED) => - if (!d.latestFundingTx.fundingParams.isInitiator) { - cmd.replyTo ! RES_FAILURE(cmd, InvalidRbfNonInitiator(d.channelId)) - stay() - } else if (d.commitments.params.zeroConf) { - cmd.replyTo ! RES_FAILURE(cmd, InvalidRbfZeroConf(d.channelId)) - stay() - } else if (cmd.requestFunding_opt.isEmpty && d.latestFundingTx.liquidityPurchase_opt.nonEmpty) { - cmd.replyTo ! RES_FAILURE(cmd, InvalidRbfMissingLiquidityPurchase(d.channelId, d.latestFundingTx.liquidityPurchase_opt.get.amount)) - stay() - } else { - d.status match { + d.latestFundingTx.liquidityPurchase_opt match { + case Some(purchase) if !d.latestFundingTx.fundingParams.isInitiator => + // If we're not the channel initiator and they are purchasing liquidity, they must initiate RBF, otherwise + // the liquidity purchase will be lost (since only the initiator can purchase liquidity). + cmd.replyTo ! RES_FAILURE(cmd, InvalidRbfOverridesLiquidityPurchase(d.channelId, purchase.amount)) + stay() + case Some(purchase) if cmd.requestFunding_opt.isEmpty => + // If we were purchasing liquidity, we must keep purchasing liquidity across RBF attempts, otherwise our + // peer will simply reject the RBF attempt. + cmd.replyTo ! RES_FAILURE(cmd, InvalidRbfMissingLiquidityPurchase(d.channelId, purchase.amount)) + stay() + case _ if d.commitments.channelParams.zeroConf => + cmd.replyTo ! RES_FAILURE(cmd, InvalidRbfZeroConf(d.channelId)) + stay() + case _ => d.status match { case DualFundingStatus.WaitingForConfirmations => val minNextFeerate = d.latestFundingTx.fundingParams.minNextFeerate if (cmd.targetFeerate < minNextFeerate) { @@ -521,11 +513,7 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers { } case Event(msg: TxInitRbf, d: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED) => - if (d.latestFundingTx.fundingParams.isInitiator) { - // Only the initiator is allowed to initiate RBF. - log.info("rejecting tx_init_rbf, we're the initiator, not them!") - stay() sending Error(d.channelId, InvalidRbfNonInitiator(d.channelId).getMessage) - } else if (d.commitments.params.zeroConf) { + if (d.commitments.channelParams.zeroConf) { log.info("rejecting tx_init_rbf, we're using zero-conf") stay() using d.copy(status = DualFundingStatus.RbfAborted) sending TxAbort(d.channelId, InvalidRbfZeroConf(d.channelId).getMessage) } else { @@ -551,7 +539,7 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers { log.info("rejecting rbf attempt: last attempt was less than {} blocks ago", nodeParams.channelConf.remoteRbfLimits.attemptDeltaBlocks) stay() using d.copy(status = DualFundingStatus.RbfAborted) sending TxAbort(d.channelId, InvalidRbfAttemptTooSoon(d.channelId, d.latestFundingTx.createdAt, d.latestFundingTx.createdAt + nodeParams.channelConf.remoteRbfLimits.attemptDeltaBlocks).getMessage) } else { - val fundingScript = d.commitments.latest.commitInput.txOut.publicKeyScript + val fundingScript = d.commitments.latest.commitInput(channelKeys).txOut.publicKeyScript LiquidityAds.validateRequest(nodeParams.privateKey, d.channelId, fundingScript, msg.feerate, isChannelCreation = true, msg.requestFunding_opt, nodeParams.liquidityAdsConfig.rates_opt, None) match { case Left(t) => log.warning("rejecting rbf attempt: invalid liquidity ads request ({})", t.getMessage) @@ -563,6 +551,7 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers { val fundingContribution = willFund_opt.map(_.purchase.amount).getOrElse(d.latestFundingTx.fundingParams.localContribution) log.info("accepting rbf with remote.in.amount={} local.in.amount={}", msg.fundingContribution, fundingContribution) val fundingParams = d.latestFundingTx.fundingParams.copy( + isInitiator = false, localContribution = fundingContribution, remoteContribution = msg.fundingContribution, lockTime = msg.lockTime, @@ -572,7 +561,10 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers { val txBuilder = context.spawnAnonymous(InteractiveTxBuilder( randomBytes32(), nodeParams, fundingParams, - channelParams = d.commitments.params, + channelParams = d.commitments.channelParams, + localCommitParams = d.commitments.active.head.localCommitParams, + remoteCommitParams = d.commitments.active.head.remoteCommitParams, + channelKeys = channelKeys, purpose = InteractiveTxBuilder.FundingTxRbf(d.commitments.active.head, previousTransactions = d.allFundingTxs.map(_.sharedTx), feeBudget_opt = None), localPushAmount = d.localPushAmount, remotePushAmount = d.remotePushAmount, liquidityPurchase_opt = willFund_opt.map(_.purchase), @@ -603,12 +595,13 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers { stay() using d.copy(status = DualFundingStatus.RbfAborted) sending TxAbort(d.channelId, error.getMessage) case DualFundingStatus.RbfRequested(cmd) => val fundingParams = d.latestFundingTx.fundingParams.copy( + isInitiator = true, // we don't change our funding contribution remoteContribution = msg.fundingContribution, lockTime = cmd.lockTime, targetFeerate = cmd.targetFeerate, ) - val fundingScript = d.commitments.latest.commitInput.txOut.publicKeyScript + val fundingScript = d.commitments.latest.commitInput(channelKeys).txOut.publicKeyScript LiquidityAds.validateRemoteFunding(cmd.requestFunding_opt, remoteNodeId, d.channelId, fundingScript, msg.fundingContribution, cmd.targetFeerate, isChannelCreation = true, msg.willFund_opt) match { case Left(t) => log.warning("rejecting rbf attempt: invalid liquidity ads response ({})", t.getMessage) @@ -619,7 +612,10 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers { val txBuilder = context.spawnAnonymous(InteractiveTxBuilder( randomBytes32(), nodeParams, fundingParams, - channelParams = d.commitments.params, + channelParams = d.commitments.channelParams, + localCommitParams = d.commitments.active.head.localCommitParams, + remoteCommitParams = d.commitments.active.head.remoteCommitParams, + channelKeys = channelKeys, purpose = InteractiveTxBuilder.FundingTxRbf(d.commitments.active.head, previousTransactions = d.allFundingTxs.map(_.sharedTx), feeBudget_opt = Some(cmd.fundingFeeBudget)), localPushAmount = d.localPushAmount, remotePushAmount = d.remotePushAmount, liquidityPurchase_opt = liquidityPurchase_opt, @@ -648,7 +644,7 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers { log.debug("received their commit_sig, deferring message") stay() using d.copy(status = s.copy(remoteCommitSig = Some(commitSig))) case DualFundingStatus.RbfWaitingForSigs(signingSession) => - signingSession.receiveCommitSig(nodeParams, d.commitments.params, commitSig) match { + signingSession.receiveCommitSig(d.commitments.channelParams, channelKeys, commitSig, nodeParams.currentBlockHeight) match { case Left(f) => rollbackRbfAttempt(signingSession, d) stay() using d.copy(status = DualFundingStatus.RbfAborted) sending TxAbort(d.channelId, f.getMessage) @@ -657,7 +653,7 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers { // No need to store their commit_sig, they will re-send it if we disconnect. stay() using d.copy(status = DualFundingStatus.RbfWaitingForSigs(signingSession1)) case signingSession1: InteractiveTxSigningSession.SendingSigs => - val minDepth_opt = d.commitments.params.minDepth(nodeParams.channelConf.minDepth) + val minDepth_opt = d.commitments.channelParams.minDepth(nodeParams.channelConf.minDepth) watchFundingConfirmed(signingSession.fundingTx.txId, minDepth_opt, delay_opt = None) val commitments1 = d.commitments.add(signingSession1.commitment) val d1 = DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED(commitments1, d.localPushAmount, d.remotePushAmount, d.waitingSince, d.lastChecked, DualFundingStatus.WaitingForConfirmations, d.deferred) @@ -698,11 +694,12 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers { case DualFundingStatus.RbfInProgress(cmd_opt, _, remoteCommitSig_opt) => msg match { case InteractiveTxBuilder.SendMessage(_, msg) => stay() sending msg - case InteractiveTxBuilder.Succeeded(signingSession, commitSig, liquidityPurchase_opt) => + case InteractiveTxBuilder.Succeeded(signingSession, commitSig, liquidityPurchase_opt, nextRemoteCommitNonce_opt) => + nextRemoteCommitNonce_opt.foreach { case (txId, nonce) => remoteNextCommitNonces = remoteNextCommitNonces + (txId -> nonce) } cmd_opt.foreach(cmd => cmd.replyTo ! RES_BUMP_FUNDING_FEE(rbfIndex = d.previousFundingTxs.length, signingSession.fundingTx.txId, signingSession.fundingTx.tx.localFees.truncateToSatoshi)) remoteCommitSig_opt.foreach(self ! _) liquidityPurchase_opt.collect { - case purchase if !signingSession.fundingParams.isInitiator => peer ! LiquidityPurchaseSigned(d.channelId, signingSession.fundingTx.txId, signingSession.fundingTxIndex, d.commitments.params.remoteParams.htlcMinimum, purchase) + case purchase if !signingSession.fundingParams.isInitiator => peer ! LiquidityPurchaseSigned(d.channelId, signingSession.fundingTx.txId, signingSession.fundingTxIndex, signingSession.remoteCommitParams.htlcMinimum, purchase) } val d1 = d.copy(status = DualFundingStatus.RbfWaitingForSigs(signingSession)) stay() using d1 storing() sending commitSig @@ -725,7 +722,7 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers { // We still watch the funding tx for confirmation even if we can use the zero-conf channel right away. watchFundingConfirmed(w.tx.txid, Some(nodeParams.channelConf.minDepth), delay_opt = None) val shortIds = createShortIdAliases(d.channelId) - val channelReady = createChannelReady(shortIds, d.commitments.params) + val channelReady = createChannelReady(shortIds, d.commitments) d.deferred.foreach(self ! _) goto(WAIT_FOR_DUAL_FUNDING_READY) using DATA_WAIT_FOR_DUAL_FUNDING_READY(commitments1, shortIds) storing() sending channelReady case Left(_) => stay() @@ -735,7 +732,7 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers { acceptFundingTxConfirmed(w, d) match { case Right((commitments1, _)) => val shortIds = createShortIdAliases(d.channelId) - val channelReady = createChannelReady(shortIds, d.commitments.params) + val channelReady = createChannelReady(shortIds, d.commitments) reportRbfFailure(d.status, InvalidRbfTxConfirmed(d.channelId)) val toSend = d.status match { case DualFundingStatus.WaitingForConfirmations | DualFundingStatus.RbfAborted => Seq(channelReady) @@ -781,7 +778,7 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers { when(WAIT_FOR_DUAL_FUNDING_READY)(handleExceptions { case Event(channelReady: ChannelReady, d: DATA_WAIT_FOR_DUAL_FUNDING_READY) => val d1 = receiveChannelReady(d.aliases, channelReady, d.commitments) - val annSigs_opt = d1.commitments.all.find(_.fundingTxIndex == 0).flatMap(_.signAnnouncement(nodeParams, d1.commitments.params)) + val annSigs_opt = d1.commitments.all.find(_.fundingTxIndex == 0).flatMap(_.signAnnouncement(nodeParams, d1.commitments.channelParams, channelKeys.fundingKey(fundingTxIndex = 0))) annSigs_opt.foreach(annSigs => announcementSigsSent += annSigs.shortChannelId) goto(NORMAL) using d1 storing() sending annSigs_opt.toSeq diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunded.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunded.scala index 934b9d6d7b..1f10caa287 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunded.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunded.scala @@ -19,24 +19,24 @@ package fr.acinq.eclair.channel.fsm import akka.actor.Status import akka.actor.typed.scaladsl.adapter.actorRefAdapter import akka.pattern.pipe -import fr.acinq.bitcoin.scalacompat.{SatoshiLong, Script} +import fr.acinq.bitcoin.scalacompat.SatoshiLong import fr.acinq.eclair.blockchain.OnChainWallet.MakeFundingTxResponse import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ +import fr.acinq.eclair.channel.ChannelSpendSignature.{IndividualSignature, PartialSignatureWithNonce} import fr.acinq.eclair.channel.Helpers.Funding import fr.acinq.eclair.channel.LocalFundingStatus.SingleFundedUnconfirmedFundingTx import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.fsm.Channel._ import fr.acinq.eclair.channel.publish.TxPublisher.SetChannelId -import fr.acinq.eclair.crypto.ShaChain +import fr.acinq.eclair.crypto.keymanager.{LocalCommitmentKeys, RemoteCommitmentKeys} +import fr.acinq.eclair.crypto.{NonceGenerator, ShaChain} import fr.acinq.eclair.io.Peer.OpenChannelResponse -import fr.acinq.eclair.transactions.Transactions.TxOwner -import fr.acinq.eclair.transactions.{Scripts, Transactions} -import fr.acinq.eclair.wire.protocol.{AcceptChannel, AnnouncementSignatures, ChannelReady, ChannelTlv, Error, FundingCreated, FundingSigned, OpenChannel, TlvStream} -import fr.acinq.eclair.{Features, MilliSatoshiLong, UInt64, randomKey, toLongId} +import fr.acinq.eclair.transactions.Transactions +import fr.acinq.eclair.transactions.Transactions.{AnchorOutputsCommitmentFormat, SimpleTaprootChannelCommitmentFormat} +import fr.acinq.eclair.wire.protocol.{AcceptChannel, AcceptChannelTlv, AnnouncementSignatures, ChannelReady, ChannelTlv, Error, FundingCreated, FundingSigned, OpenChannel, OpenChannelTlv, TlvStream} +import fr.acinq.eclair.{MilliSatoshiLong, randomKey, toLongId} import scodec.bits.ByteVector -import scala.util.{Failure, Success} - /** * Created by t-bast on 28/03/2022. */ @@ -73,122 +73,124 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers { when(WAIT_FOR_INIT_SINGLE_FUNDED_CHANNEL)(handleExceptions { case Event(input: INPUT_INIT_CHANNEL_INITIATOR, _) => - val fundingPubKey = keyManager.fundingPublicKey(input.localParams.fundingKeyPath, fundingTxIndex = 0).publicKey - val channelKeyPath = keyManager.keyPath(input.localParams, input.channelConfig) + val fundingKey = channelKeys.fundingKey(fundingTxIndex = 0) // In order to allow TLV extensions and keep backwards-compatibility, we include an empty upfront_shutdown_script if this feature is not used // See https://github.com/lightningnetwork/lightning-rfc/pull/714. - val localShutdownScript = input.localParams.upfrontShutdownScript_opt.getOrElse(ByteVector.empty) + val localShutdownScript = input.localChannelParams.upfrontShutdownScript_opt.getOrElse(ByteVector.empty) + val localNonce = input.channelType.commitmentFormat match { + case _: SimpleTaprootChannelCommitmentFormat => Some(NonceGenerator.verificationNonce(NonceGenerator.dummyFundingTxId, fundingKey, NonceGenerator.dummyRemoteFundingPubKey, 0).publicNonce) + case _: AnchorOutputsCommitmentFormat => None + } val open = OpenChannel( chainHash = nodeParams.chainHash, temporaryChannelId = input.temporaryChannelId, fundingSatoshis = input.fundingAmount, pushMsat = input.pushAmount_opt.getOrElse(0 msat), - dustLimitSatoshis = input.localParams.dustLimit, - maxHtlcValueInFlightMsat = UInt64(input.localParams.maxHtlcValueInFlightMsat.toLong), - channelReserveSatoshis = input.localParams.initialRequestedChannelReserve_opt.get, - htlcMinimumMsat = input.localParams.htlcMinimum, + dustLimitSatoshis = input.proposedCommitParams.localDustLimit, + maxHtlcValueInFlightMsat = input.proposedCommitParams.localMaxHtlcValueInFlight, + channelReserveSatoshis = input.localChannelParams.initialRequestedChannelReserve_opt.get, + htlcMinimumMsat = input.proposedCommitParams.localHtlcMinimum, feeratePerKw = input.commitTxFeerate, - toSelfDelay = input.localParams.toSelfDelay, - maxAcceptedHtlcs = input.localParams.maxAcceptedHtlcs, - fundingPubkey = fundingPubKey, - revocationBasepoint = keyManager.revocationPoint(channelKeyPath).publicKey, - paymentBasepoint = input.localParams.walletStaticPaymentBasepoint.getOrElse(keyManager.paymentPoint(channelKeyPath).publicKey), - delayedPaymentBasepoint = keyManager.delayedPaymentPoint(channelKeyPath).publicKey, - htlcBasepoint = keyManager.htlcPoint(channelKeyPath).publicKey, - firstPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, 0), + toSelfDelay = input.proposedCommitParams.toRemoteDelay, + maxAcceptedHtlcs = input.proposedCommitParams.localMaxAcceptedHtlcs, + fundingPubkey = fundingKey.publicKey, + revocationBasepoint = channelKeys.revocationBasePoint, + paymentBasepoint = channelKeys.paymentBasePoint, + delayedPaymentBasepoint = channelKeys.delayedPaymentBasePoint, + htlcBasepoint = channelKeys.htlcBasePoint, + firstPerCommitmentPoint = channelKeys.commitmentPoint(0), channelFlags = input.channelFlags, tlvStream = TlvStream( - ChannelTlv.UpfrontShutdownScriptTlv(localShutdownScript), - ChannelTlv.ChannelTypeTlv(input.channelType) + Set( + Some(ChannelTlv.UpfrontShutdownScriptTlv(localShutdownScript)), + Some(ChannelTlv.ChannelTypeTlv(input.channelType)), + localNonce.map(n => ChannelTlv.NextLocalNonceTlv(n)) + ).flatten[OpenChannelTlv] )) goto(WAIT_FOR_ACCEPT_CHANNEL) using DATA_WAIT_FOR_ACCEPT_CHANNEL(input, open) sending open }) when(WAIT_FOR_OPEN_CHANNEL)(handleExceptions { case Event(open: OpenChannel, d: DATA_WAIT_FOR_OPEN_CHANNEL) => - Helpers.validateParamsSingleFundedFundee(nodeParams, d.initFundee.channelType, d.initFundee.localParams.initFeatures, open, remoteNodeId, d.initFundee.remoteInit.features) match { + Helpers.validateParamsSingleFundedFundee(nodeParams, d.initFundee.localChannelParams.initFeatures, open, remoteNodeId, d.initFundee.remoteInit.features) match { case Left(t) => handleLocalError(t, d, Some(open)) case Right((channelFeatures, remoteShutdownScript)) => context.system.eventStream.publish(ChannelCreated(self, peer, remoteNodeId, isOpener = false, open.temporaryChannelId, open.feeratePerKw, None)) - val remoteParams = RemoteParams( + val remoteChannelParams = RemoteChannelParams( nodeId = remoteNodeId, - dustLimit = open.dustLimitSatoshis, - maxHtlcValueInFlightMsat = open.maxHtlcValueInFlightMsat, initialRequestedChannelReserve_opt = Some(open.channelReserveSatoshis), // our peer requires us to always have at least that much satoshis in our balance - htlcMinimum = open.htlcMinimumMsat, - toSelfDelay = open.toSelfDelay, - maxAcceptedHtlcs = open.maxAcceptedHtlcs, revocationBasepoint = open.revocationBasepoint, paymentBasepoint = open.paymentBasepoint, delayedPaymentBasepoint = open.delayedPaymentBasepoint, htlcBasepoint = open.htlcBasepoint, initFeatures = d.initFundee.remoteInit.features, upfrontShutdownScript_opt = remoteShutdownScript) - log.debug("remote params: {}", remoteParams) - val fundingPubkey = keyManager.fundingPublicKey(d.initFundee.localParams.fundingKeyPath, fundingTxIndex = 0).publicKey - val channelKeyPath = keyManager.keyPath(d.initFundee.localParams, d.initFundee.channelConfig) - val params = ChannelParams(d.initFundee.temporaryChannelId, d.initFundee.channelConfig, channelFeatures, d.initFundee.localParams, remoteParams, open.channelFlags) - val minimumDepth = params.minDepth(nodeParams.channelConf.minDepth) - log.info("will use fundingMinDepth={}", minimumDepth) + val fundingKey = channelKeys.fundingKey(fundingTxIndex = 0) + val channelParams = ChannelParams(d.initFundee.temporaryChannelId, d.initFundee.channelConfig, channelFeatures, d.initFundee.localChannelParams, remoteChannelParams, open.channelFlags) + val localCommitParams = CommitParams(d.initFundee.proposedCommitParams.localDustLimit, d.initFundee.proposedCommitParams.localHtlcMinimum, d.initFundee.proposedCommitParams.localMaxHtlcValueInFlight, d.initFundee.proposedCommitParams.localMaxAcceptedHtlcs, open.toSelfDelay) + val remoteCommitParams = CommitParams(open.dustLimitSatoshis, open.htlcMinimumMsat, open.maxHtlcValueInFlightMsat, open.maxAcceptedHtlcs, d.initFundee.proposedCommitParams.toRemoteDelay) // In order to allow TLV extensions and keep backwards-compatibility, we include an empty upfront_shutdown_script if this feature is not used. // See https://github.com/lightningnetwork/lightning-rfc/pull/714. - val localShutdownScript = d.initFundee.localParams.upfrontShutdownScript_opt.getOrElse(ByteVector.empty) + val localShutdownScript = d.initFundee.localChannelParams.upfrontShutdownScript_opt.getOrElse(ByteVector.empty) + val localNonce = d.initFundee.channelType.commitmentFormat match { + case _: SimpleTaprootChannelCommitmentFormat => Some(NonceGenerator.verificationNonce(NonceGenerator.dummyFundingTxId, fundingKey, NonceGenerator.dummyRemoteFundingPubKey, 0).publicNonce) + case _: AnchorOutputsCommitmentFormat => None + } val accept = AcceptChannel(temporaryChannelId = open.temporaryChannelId, - dustLimitSatoshis = d.initFundee.localParams.dustLimit, - maxHtlcValueInFlightMsat = UInt64(d.initFundee.localParams.maxHtlcValueInFlightMsat.toLong), - channelReserveSatoshis = d.initFundee.localParams.initialRequestedChannelReserve_opt.get, - minimumDepth = minimumDepth.getOrElse(0).toLong, - htlcMinimumMsat = d.initFundee.localParams.htlcMinimum, - toSelfDelay = d.initFundee.localParams.toSelfDelay, - maxAcceptedHtlcs = d.initFundee.localParams.maxAcceptedHtlcs, - fundingPubkey = fundingPubkey, - revocationBasepoint = keyManager.revocationPoint(channelKeyPath).publicKey, - paymentBasepoint = d.initFundee.localParams.walletStaticPaymentBasepoint.getOrElse(keyManager.paymentPoint(channelKeyPath).publicKey), - delayedPaymentBasepoint = keyManager.delayedPaymentPoint(channelKeyPath).publicKey, - htlcBasepoint = keyManager.htlcPoint(channelKeyPath).publicKey, - firstPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, 0), - tlvStream = TlvStream( - ChannelTlv.UpfrontShutdownScriptTlv(localShutdownScript), - ChannelTlv.ChannelTypeTlv(d.initFundee.channelType) - )) - goto(WAIT_FOR_FUNDING_CREATED) using DATA_WAIT_FOR_FUNDING_CREATED(params, open.fundingSatoshis, open.pushMsat, open.feeratePerKw, open.fundingPubkey, open.firstPerCommitmentPoint) sending accept + dustLimitSatoshis = localCommitParams.dustLimit, + maxHtlcValueInFlightMsat = localCommitParams.maxHtlcValueInFlight, + channelReserveSatoshis = d.initFundee.localChannelParams.initialRequestedChannelReserve_opt.get, + minimumDepth = channelParams.minDepth(nodeParams.channelConf.minDepth).getOrElse(0).toLong, + htlcMinimumMsat = localCommitParams.htlcMinimum, + toSelfDelay = remoteCommitParams.toSelfDelay, + maxAcceptedHtlcs = localCommitParams.maxAcceptedHtlcs, + fundingPubkey = fundingKey.publicKey, + revocationBasepoint = channelKeys.revocationBasePoint, + paymentBasepoint = channelKeys.paymentBasePoint, + delayedPaymentBasepoint = channelKeys.delayedPaymentBasePoint, + htlcBasepoint = channelKeys.htlcBasePoint, + firstPerCommitmentPoint = channelKeys.commitmentPoint(0), + tlvStream = TlvStream(Set( + Some(ChannelTlv.UpfrontShutdownScriptTlv(localShutdownScript)), + Some(ChannelTlv.ChannelTypeTlv(d.initFundee.channelType)), + localNonce.map(n => ChannelTlv.NextLocalNonceTlv(n)) + ).flatten[AcceptChannelTlv])) + remoteNextCommitNonces = open.commitNonce_opt.map(n => NonceGenerator.dummyFundingTxId -> n).toMap + goto(WAIT_FOR_FUNDING_CREATED) using DATA_WAIT_FOR_FUNDING_CREATED(channelParams, d.initFundee.channelType, localCommitParams, remoteCommitParams, open.fundingSatoshis, open.pushMsat, open.feeratePerKw, open.fundingPubkey, open.firstPerCommitmentPoint) sending accept } case Event(c: CloseCommand, d) => handleFastClose(c, d.channelId) case Event(e: Error, d: DATA_WAIT_FOR_OPEN_CHANNEL) => handleRemoteError(e, d) - case Event(INPUT_DISCONNECTED, _) => goto(CLOSED) + case Event(INPUT_DISCONNECTED, d: DATA_WAIT_FOR_OPEN_CHANNEL) => goto(CLOSED) using IgnoreClosedData(d) }) when(WAIT_FOR_ACCEPT_CHANNEL)(handleExceptions { - case Event(accept: AcceptChannel, d@DATA_WAIT_FOR_ACCEPT_CHANNEL(init, open)) => - Helpers.validateParamsSingleFundedFunder(nodeParams, init.channelType, init.localParams.initFeatures, init.remoteInit.features, open, accept) match { + case Event(accept: AcceptChannel, d: DATA_WAIT_FOR_ACCEPT_CHANNEL) => + Helpers.validateParamsSingleFundedFunder(nodeParams, d.initFunder.localChannelParams.initFeatures, d.initFunder.remoteInit.features, d.lastSent, accept) match { case Left(t) => d.initFunder.replyTo ! OpenChannelResponse.Rejected(t.getMessage) handleLocalError(t, d, Some(accept)) case Right((channelFeatures, remoteShutdownScript)) => - val remoteParams = RemoteParams( + val remoteChannelParams = RemoteChannelParams( nodeId = remoteNodeId, - dustLimit = accept.dustLimitSatoshis, - maxHtlcValueInFlightMsat = accept.maxHtlcValueInFlightMsat, initialRequestedChannelReserve_opt = Some(accept.channelReserveSatoshis), // our peer requires us to always have at least that much satoshis in our balance - htlcMinimum = accept.htlcMinimumMsat, - toSelfDelay = accept.toSelfDelay, - maxAcceptedHtlcs = accept.maxAcceptedHtlcs, revocationBasepoint = accept.revocationBasepoint, paymentBasepoint = accept.paymentBasepoint, delayedPaymentBasepoint = accept.delayedPaymentBasepoint, htlcBasepoint = accept.htlcBasepoint, - initFeatures = init.remoteInit.features, + initFeatures = d.initFunder.remoteInit.features, upfrontShutdownScript_opt = remoteShutdownScript) - log.debug("remote params: {}", remoteParams) log.info("remote will use fundingMinDepth={}", accept.minimumDepth) - val localFundingPubkey = keyManager.fundingPublicKey(init.localParams.fundingKeyPath, fundingTxIndex = 0) - val fundingPubkeyScript = Script.write(Script.pay2wsh(Scripts.multiSig2of2(localFundingPubkey.publicKey, accept.fundingPubkey))) - wallet.makeFundingTx(fundingPubkeyScript, init.fundingAmount, init.fundingTxFeerate, init.fundingTxFeeBudget_opt).pipeTo(self) - val params = ChannelParams(init.temporaryChannelId, init.channelConfig, channelFeatures, init.localParams, remoteParams, open.channelFlags) - goto(WAIT_FOR_FUNDING_INTERNAL) using DATA_WAIT_FOR_FUNDING_INTERNAL(params, init.fundingAmount, init.pushAmount_opt.getOrElse(0 msat), init.commitTxFeerate, accept.fundingPubkey, accept.firstPerCommitmentPoint, d.initFunder.replyTo) + val localFundingKey = channelKeys.fundingKey(fundingTxIndex = 0) + val fundingPubkeyScript = Transactions.makeFundingScript(localFundingKey.publicKey, accept.fundingPubkey, d.initFunder.channelType.commitmentFormat).pubkeyScript + wallet.makeFundingTx(fundingPubkeyScript, d.initFunder.fundingAmount, d.initFunder.fundingTxFeerate, d.initFunder.fundingTxFeeBudget_opt).pipeTo(self) + val channelParams = ChannelParams(d.initFunder.temporaryChannelId, d.initFunder.channelConfig, channelFeatures, d.initFunder.localChannelParams, remoteChannelParams, d.lastSent.channelFlags) + val localCommitParams = CommitParams(d.initFunder.proposedCommitParams.localDustLimit, d.initFunder.proposedCommitParams.localHtlcMinimum, d.initFunder.proposedCommitParams.localMaxHtlcValueInFlight, d.initFunder.proposedCommitParams.localMaxAcceptedHtlcs, accept.toSelfDelay) + val remoteCommitParams = CommitParams(accept.dustLimitSatoshis, accept.htlcMinimumMsat, accept.maxHtlcValueInFlightMsat, accept.maxAcceptedHtlcs, d.initFunder.proposedCommitParams.toRemoteDelay) + remoteNextCommitNonces = accept.commitNonce_opt.map(n => NonceGenerator.dummyFundingTxId -> n).toMap + goto(WAIT_FOR_FUNDING_INTERNAL) using DATA_WAIT_FOR_FUNDING_INTERNAL(channelParams, d.initFunder.channelType, localCommitParams, remoteCommitParams, d.initFunder.fundingAmount, d.initFunder.pushAmount_opt.getOrElse(0 msat), d.initFunder.commitTxFeerate, accept.fundingPubkey, accept.firstPerCommitmentPoint, d.initFunder.replyTo) } case Event(c: CloseCommand, d: DATA_WAIT_FOR_ACCEPT_CHANNEL) => @@ -201,36 +203,50 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers { case Event(INPUT_DISCONNECTED, d: DATA_WAIT_FOR_ACCEPT_CHANNEL) => d.initFunder.replyTo ! OpenChannelResponse.Disconnected - goto(CLOSED) + goto(CLOSED) using IgnoreClosedData(d) case Event(TickChannelOpenTimeout, d: DATA_WAIT_FOR_ACCEPT_CHANNEL) => d.initFunder.replyTo ! OpenChannelResponse.TimedOut - goto(CLOSED) + goto(CLOSED) using IgnoreClosedData(d) }) when(WAIT_FOR_FUNDING_INTERNAL)(handleExceptions { - case Event(MakeFundingTxResponse(fundingTx, fundingTxOutputIndex, fundingTxFee), d@DATA_WAIT_FOR_FUNDING_INTERNAL(params, fundingAmount, pushMsat, commitTxFeerate, remoteFundingPubKey, remoteFirstPerCommitmentPoint, replyTo)) => - val temporaryChannelId = params.channelId + case Event(MakeFundingTxResponse(fundingTx, fundingTxOutputIndex, fundingTxFee), d: DATA_WAIT_FOR_FUNDING_INTERNAL) => + val temporaryChannelId = d.channelParams.channelId // let's create the first commitment tx that spends the yet uncommitted funding tx - Funding.makeFirstCommitTxs(keyManager, params, localFundingAmount = fundingAmount, remoteFundingAmount = 0 sat, localPushAmount = pushMsat, remotePushAmount = 0 msat, commitTxFeerate, fundingTx.txid, fundingTxOutputIndex, remoteFundingPubKey = remoteFundingPubKey, remoteFirstPerCommitmentPoint = remoteFirstPerCommitmentPoint) match { + val fundingKey = channelKeys.fundingKey(fundingTxIndex = 0) + val localCommitmentKeys = LocalCommitmentKeys(d.channelParams, channelKeys, localCommitIndex = 0) + val remoteCommitmentKeys = RemoteCommitmentKeys(d.channelParams, channelKeys, d.remoteFirstPerCommitmentPoint) + Funding.makeFirstCommitTxs(d.channelParams, d.localCommitParams, d.remoteCommitParams, localFundingAmount = d.fundingAmount, remoteFundingAmount = 0 sat, localPushAmount = d.pushAmount, remotePushAmount = 0 msat, d.commitTxFeerate, d.commitmentFormat, fundingTx.txid, fundingTxOutputIndex, fundingKey, d.remoteFundingPubKey, localCommitmentKeys, remoteCommitmentKeys) match { case Left(ex) => handleLocalError(ex, d, None) case Right((localSpec, localCommitTx, remoteSpec, remoteCommitTx)) => - require(fundingTx.txOut(fundingTxOutputIndex).publicKeyScript == localCommitTx.input.txOut.publicKeyScript, s"pubkey script mismatch!") - val localSigOfRemoteTx = keyManager.sign(remoteCommitTx, keyManager.fundingPublicKey(params.localParams.fundingKeyPath, fundingTxIndex = 0), TxOwner.Remote, params.commitmentFormat, Map.empty) - // signature of their initial commitment tx that pays remote pushMsat - val fundingCreated = FundingCreated( - temporaryChannelId = temporaryChannelId, - fundingTxId = fundingTx.txid, - fundingOutputIndex = fundingTxOutputIndex, - signature = localSigOfRemoteTx - ) - val channelId = toLongId(fundingTx.txid, fundingTxOutputIndex) - val params1 = params.copy(channelId = channelId) - peer ! ChannelIdAssigned(self, remoteNodeId, temporaryChannelId, channelId) // we notify the peer asap so it knows how to route messages - txPublisher ! SetChannelId(remoteNodeId, channelId) - context.system.eventStream.publish(ChannelIdAssigned(self, remoteNodeId, temporaryChannelId, channelId)) - // NB: we don't send a ChannelSignatureSent for the first commit - goto(WAIT_FOR_FUNDING_SIGNED) using DATA_WAIT_FOR_FUNDING_SIGNED(params1, remoteFundingPubKey, fundingTx, fundingTxFee, localSpec, localCommitTx, RemoteCommit(0, remoteSpec, remoteCommitTx.tx.txid, remoteFirstPerCommitmentPoint), fundingCreated, replyTo) sending fundingCreated + require(fundingTx.txOut(fundingTxOutputIndex).publicKeyScript == localCommitTx.input.txOut.publicKeyScript, "pubkey script mismatch!") + val remoteCommit = RemoteCommit(0, remoteSpec, remoteCommitTx.tx.txid, d.remoteFirstPerCommitmentPoint) + val localSigOfRemoteTx = d.commitmentFormat match { + case _: SimpleTaprootChannelCommitmentFormat => + val localNonce = NonceGenerator.verificationNonce(NonceGenerator.dummyFundingTxId, fundingKey, NonceGenerator.dummyRemoteFundingPubKey, 0) + remoteNextCommitNonces.get(NonceGenerator.dummyFundingTxId) match { + case Some(remoteNonce) => + remoteCommitTx.partialSign(fundingKey, d.remoteFundingPubKey, localNonce, Seq(localNonce.publicNonce, remoteNonce)) match { + case Left(_) => Left(InvalidCommitNonce(d.channelId, NonceGenerator.dummyFundingTxId, commitmentNumber = 0)) + case Right(psig) => Right(psig) + } + case None => Left(MissingCommitNonce(d.channelId, NonceGenerator.dummyFundingTxId, commitmentNumber = 0)) + } + case _: AnchorOutputsCommitmentFormat => Right(remoteCommitTx.sign(fundingKey, d.remoteFundingPubKey)) + } + localSigOfRemoteTx match { + case Left(f) => handleLocalError(f, d, None) + case Right(localSig) => + val fundingCreated = FundingCreated(temporaryChannelId, fundingTx.txid, fundingTxOutputIndex, localSig) + val channelId = toLongId(fundingTx.txid, fundingTxOutputIndex) + val channelParams1 = d.channelParams.copy(channelId = channelId) + peer ! ChannelIdAssigned(self, remoteNodeId, temporaryChannelId, channelId) // we notify the peer asap so it knows how to route messages + txPublisher ! SetChannelId(remoteNodeId, channelId) + context.system.eventStream.publish(ChannelIdAssigned(self, remoteNodeId, temporaryChannelId, channelId)) + // NB: we don't send a ChannelSignatureSent for the first commit + goto(WAIT_FOR_FUNDING_SIGNED) using DATA_WAIT_FOR_FUNDING_SIGNED(channelParams1, d.channelType, d.localCommitParams, d.remoteCommitParams, d.remoteFundingPubKey, fundingTx, fundingTxFee, localSpec, localCommitTx, remoteCommit, fundingCreated, d.replyTo) sending fundingCreated + } } case Event(Status.Failure(t), d: DATA_WAIT_FOR_FUNDING_INTERNAL) => @@ -248,57 +264,81 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers { case Event(INPUT_DISCONNECTED, d: DATA_WAIT_FOR_FUNDING_INTERNAL) => d.replyTo ! OpenChannelResponse.Disconnected - goto(CLOSED) + goto(CLOSED) using IgnoreClosedData(d) case Event(TickChannelOpenTimeout, d: DATA_WAIT_FOR_FUNDING_INTERNAL) => d.replyTo ! OpenChannelResponse.TimedOut - goto(CLOSED) + goto(CLOSED) using IgnoreClosedData(d) }) when(WAIT_FOR_FUNDING_CREATED)(handleExceptions { - case Event(FundingCreated(_, fundingTxId, fundingTxOutputIndex, remoteSig, _), d@DATA_WAIT_FOR_FUNDING_CREATED(params, fundingAmount, pushMsat, commitTxFeerate, remoteFundingPubKey, remoteFirstPerCommitmentPoint)) => - val temporaryChannelId = params.channelId - // they fund the channel with their funding tx, so the money is theirs (but we are paid pushMsat) - Funding.makeFirstCommitTxs(keyManager, params, localFundingAmount = 0 sat, remoteFundingAmount = fundingAmount, localPushAmount = 0 msat, remotePushAmount = pushMsat, commitTxFeerate, fundingTxId, fundingTxOutputIndex, remoteFundingPubKey = remoteFundingPubKey, remoteFirstPerCommitmentPoint = remoteFirstPerCommitmentPoint) match { - case Left(ex) => handleLocalError(ex, d, None) + case Event(fc@FundingCreated(_, fundingTxId, fundingTxOutputIndex, _, _), d: DATA_WAIT_FOR_FUNDING_CREATED) => + val temporaryChannelId = d.channelParams.channelId + val fundingKey = channelKeys.fundingKey(fundingTxIndex = 0) + val localCommitmentKeys = LocalCommitmentKeys(d.channelParams, channelKeys, localCommitIndex = 0) + val remoteCommitmentKeys = RemoteCommitmentKeys(d.channelParams, channelKeys, d.remoteFirstPerCommitmentPoint) + Funding.makeFirstCommitTxs(d.channelParams, d.localCommitParams, d.remoteCommitParams, localFundingAmount = 0 sat, remoteFundingAmount = d.fundingAmount, localPushAmount = 0 msat, remotePushAmount = d.pushAmount, d.commitTxFeerate, d.commitmentFormat, fundingTxId, fundingTxOutputIndex, fundingKey, d.remoteFundingPubKey, localCommitmentKeys, remoteCommitmentKeys) match { + case Left(ex) => handleLocalError(ex, d, Some(fc)) case Right((localSpec, localCommitTx, remoteSpec, remoteCommitTx)) => // check remote signature validity - val fundingPubKey = keyManager.fundingPublicKey(params.localParams.fundingKeyPath, fundingTxIndex = 0) - val localSigOfLocalTx = keyManager.sign(localCommitTx, fundingPubKey, TxOwner.Local, params.commitmentFormat, Map.empty) - val signedLocalCommitTx = Transactions.addSigs(localCommitTx, fundingPubKey.publicKey, remoteFundingPubKey, localSigOfLocalTx, remoteSig) - Transactions.checkSpendable(signedLocalCommitTx) match { - case Failure(_) => handleLocalError(InvalidCommitmentSignature(temporaryChannelId, fundingTxId, fundingTxIndex = 0, localCommitTx.tx), d, None) - case Success(_) => - val localSigOfRemoteTx = keyManager.sign(remoteCommitTx, fundingPubKey, TxOwner.Remote, params.commitmentFormat, Map.empty) + val isRemoteSigValid = fc.sigOrPartialSig match { + case psig: PartialSignatureWithNonce => + val localNonce = NonceGenerator.verificationNonce(NonceGenerator.dummyFundingTxId, fundingKey, NonceGenerator.dummyRemoteFundingPubKey, 0) + localCommitTx.checkRemotePartialSignature(fundingKey.publicKey, d.remoteFundingPubKey, psig, localNonce.publicNonce) + case sig: IndividualSignature => + localCommitTx.checkRemoteSig(fundingKey.publicKey, d.remoteFundingPubKey, sig) + } + isRemoteSigValid match { + case false => handleLocalError(InvalidCommitmentSignature(temporaryChannelId, fundingTxId, commitmentNumber = 0, localCommitTx.tx), d, Some(fc)) + case true => val channelId = toLongId(fundingTxId, fundingTxOutputIndex) - val fundingSigned = FundingSigned( - channelId = channelId, - signature = localSigOfRemoteTx - ) - val commitment = Commitment( - fundingTxIndex = 0, - firstRemoteCommitIndex = 0, - remoteFundingPubKey = remoteFundingPubKey, - localFundingStatus = SingleFundedUnconfirmedFundingTx(None), - remoteFundingStatus = RemoteFundingStatus.NotLocked, - localCommit = LocalCommit(0, localSpec, CommitTxAndRemoteSig(localCommitTx, remoteSig), htlcTxsAndRemoteSigs = Nil), - remoteCommit = RemoteCommit(0, remoteSpec, remoteCommitTx.tx.txid, remoteFirstPerCommitmentPoint), - nextRemoteCommit_opt = None) - val commitments = Commitments( - params = params.copy(channelId = channelId), - changes = CommitmentChanges.init(), - active = List(commitment), - remoteNextCommitInfo = Right(randomKey().publicKey), // we will receive their next per-commitment point in the next message, so we temporarily put a random byte array - remotePerCommitmentSecrets = ShaChain.init, - originChannels = Map.empty) - peer ! ChannelIdAssigned(self, remoteNodeId, temporaryChannelId, channelId) // we notify the peer asap so it knows how to route messages - txPublisher ! SetChannelId(remoteNodeId, channelId) - context.system.eventStream.publish(ChannelIdAssigned(self, remoteNodeId, temporaryChannelId, channelId)) - context.system.eventStream.publish(ChannelSignatureReceived(self, commitments)) - // NB: we don't send a ChannelSignatureSent for the first commit - log.info("waiting for them to publish the funding tx for channelId={} fundingTxid={}", channelId, commitment.fundingTxId) - watchFundingConfirmed(commitment.fundingTxId, params.minDepth(nodeParams.channelConf.minDepth), delay_opt = None) - goto(WAIT_FOR_FUNDING_CONFIRMED) using DATA_WAIT_FOR_FUNDING_CONFIRMED(commitments, nodeParams.currentBlockHeight, None, Right(fundingSigned)) storing() sending fundingSigned + val localSigOfRemoteTx = d.commitmentFormat match { + case _: SimpleTaprootChannelCommitmentFormat => + val localNonce = NonceGenerator.verificationNonce(NonceGenerator.dummyFundingTxId, fundingKey, NonceGenerator.dummyRemoteFundingPubKey, 0) + remoteNextCommitNonces.get(NonceGenerator.dummyFundingTxId) match { + case Some(remoteNonce) => + remoteCommitTx.partialSign(fundingKey, d.remoteFundingPubKey, localNonce, Seq(localNonce.publicNonce, remoteNonce)) match { + case Left(_) => Left(InvalidCommitNonce(channelId, NonceGenerator.dummyFundingTxId, commitmentNumber = 0)) + case Right(psig) => Right(psig) + } + case None => Left(MissingCommitNonce(channelId, NonceGenerator.dummyFundingTxId, commitmentNumber = 0)) + } + case _: AnchorOutputsCommitmentFormat => Right(remoteCommitTx.sign(fundingKey, d.remoteFundingPubKey)) + } + localSigOfRemoteTx match { + case Left(f) => handleLocalError(f, d, Some(fc)) + case Right(localSig) => + val fundingSigned = FundingSigned(channelId, localSig) + val commitment = Commitment( + fundingTxIndex = 0, + firstRemoteCommitIndex = 0, + fundingInput = localCommitTx.input.outPoint, + fundingAmount = localCommitTx.input.txOut.amount, + remoteFundingPubKey = d.remoteFundingPubKey, + localFundingStatus = SingleFundedUnconfirmedFundingTx(None), + remoteFundingStatus = RemoteFundingStatus.NotLocked, + commitmentFormat = d.commitmentFormat, + localCommitParams = d.localCommitParams, + localCommit = LocalCommit(0, localSpec, localCommitTx.tx.txid, fc.sigOrPartialSig, htlcRemoteSigs = Nil), + remoteCommitParams = d.remoteCommitParams, + remoteCommit = RemoteCommit(0, remoteSpec, remoteCommitTx.tx.txid, d.remoteFirstPerCommitmentPoint), + nextRemoteCommit_opt = None) + val commitments = Commitments( + channelParams = d.channelParams.copy(channelId = channelId), + changes = CommitmentChanges.init(), + active = List(commitment), + remoteNextCommitInfo = Right(randomKey().publicKey), // we will receive their next per-commitment point in the next message, so we temporarily put a random byte array + remotePerCommitmentSecrets = ShaChain.init, + originChannels = Map.empty) + peer ! ChannelIdAssigned(self, remoteNodeId, temporaryChannelId, channelId) // we notify the peer asap so it knows how to route messages + txPublisher ! SetChannelId(remoteNodeId, channelId) + context.system.eventStream.publish(ChannelIdAssigned(self, remoteNodeId, temporaryChannelId, channelId)) + context.system.eventStream.publish(ChannelSignatureReceived(self, commitments)) + // NB: we don't send a ChannelSignatureSent for the first commit + log.info("waiting for them to publish the funding tx for channelId={} fundingTxid={}", channelId, commitment.fundingTxId) + watchFundingConfirmed(commitment.fundingTxId, d.channelParams.minDepth(nodeParams.channelConf.minDepth), delay_opt = None) + goto(WAIT_FOR_FUNDING_CONFIRMED) using DATA_WAIT_FOR_FUNDING_CONFIRMED(commitments, nodeParams.currentBlockHeight, None, Right(fundingSigned)) storing() sending fundingSigned + } } } @@ -306,34 +346,44 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers { case Event(e: Error, d: DATA_WAIT_FOR_FUNDING_CREATED) => handleRemoteError(e, d) - case Event(INPUT_DISCONNECTED, _) => goto(CLOSED) + case Event(INPUT_DISCONNECTED, d: DATA_WAIT_FOR_FUNDING_CREATED) => goto(CLOSED) using IgnoreClosedData(d) }) when(WAIT_FOR_FUNDING_SIGNED)(handleExceptions { - case Event(msg@FundingSigned(_, remoteSig, _), d@DATA_WAIT_FOR_FUNDING_SIGNED(params, remoteFundingPubKey, fundingTx, fundingTxFee, localSpec, localCommitTx, remoteCommit, fundingCreated, _)) => + case Event(fundingSigned: FundingSigned, d: DATA_WAIT_FOR_FUNDING_SIGNED) => // we make sure that their sig checks out and that our first commit tx is spendable - val fundingPubKey = keyManager.fundingPublicKey(params.localParams.fundingKeyPath, fundingTxIndex = 0) - val localSigOfLocalTx = keyManager.sign(localCommitTx, fundingPubKey, TxOwner.Local, params.commitmentFormat, Map.empty) - val signedLocalCommitTx = Transactions.addSigs(localCommitTx, fundingPubKey.publicKey, remoteFundingPubKey, localSigOfLocalTx, remoteSig) - Transactions.checkSpendable(signedLocalCommitTx) match { - case Failure(cause) => + val fundingKey = channelKeys.fundingKey(fundingTxIndex = 0) + val isRemoteSigValid = fundingSigned.sigOrPartialSig match { + case psig: PartialSignatureWithNonce => + val localNonce = NonceGenerator.verificationNonce(NonceGenerator.dummyFundingTxId, fundingKey, NonceGenerator.dummyRemoteFundingPubKey, 0) + d.localCommitTx.checkRemotePartialSignature(fundingKey.publicKey, d.remoteFundingPubKey, psig, localNonce.publicNonce) + case sig: IndividualSignature => + d.localCommitTx.checkRemoteSig(fundingKey.publicKey, d.remoteFundingPubKey, sig) + } + isRemoteSigValid match { + case false => // we rollback the funding tx, it will never be published - wallet.rollback(fundingTx) - d.replyTo ! OpenChannelResponse.Rejected(cause.getMessage) - handleLocalError(InvalidCommitmentSignature(d.channelId, fundingTx.txid, fundingTxIndex = 0, localCommitTx.tx), d, Some(msg)) - case Success(_) => + wallet.rollback(d.fundingTx) + d.replyTo ! OpenChannelResponse.Rejected("invalid commit signatures") + handleLocalError(InvalidCommitmentSignature(d.channelId, d.fundingTx.txid, commitmentNumber = 0, d.localCommitTx.tx), d, Some(fundingSigned)) + case true => val commitment = Commitment( fundingTxIndex = 0, firstRemoteCommitIndex = 0, - remoteFundingPubKey = remoteFundingPubKey, - localFundingStatus = SingleFundedUnconfirmedFundingTx(Some(fundingTx)), + fundingInput = d.localCommitTx.input.outPoint, + fundingAmount = d.localCommitTx.input.txOut.amount, + remoteFundingPubKey = d.remoteFundingPubKey, + localFundingStatus = SingleFundedUnconfirmedFundingTx(Some(d.fundingTx)), remoteFundingStatus = RemoteFundingStatus.NotLocked, - localCommit = LocalCommit(0, localSpec, CommitTxAndRemoteSig(localCommitTx, remoteSig), htlcTxsAndRemoteSigs = Nil), - remoteCommit = remoteCommit, + commitmentFormat = d.commitmentFormat, + localCommitParams = d.localCommitParams, + localCommit = LocalCommit(0, d.localSpec, d.localCommitTx.tx.txid, fundingSigned.sigOrPartialSig, htlcRemoteSigs = Nil), + remoteCommitParams = d.remoteCommitParams, + remoteCommit = d.remoteCommit, nextRemoteCommit_opt = None ) val commitments = Commitments( - params = params, + channelParams = d.channelParams, changes = CommitmentChanges.init(), active = List(commitment), remoteNextCommitInfo = Right(randomKey().publicKey), // we will receive their next per-commitment point in the next message, so we temporarily put a random byte array @@ -341,11 +391,11 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers { originChannels = Map.empty) val blockHeight = nodeParams.currentBlockHeight context.system.eventStream.publish(ChannelSignatureReceived(self, commitments)) - log.info(s"publishing funding tx fundingTxid=${commitment.fundingTxId}") - watchFundingConfirmed(commitment.fundingTxId, params.minDepth(nodeParams.channelConf.minDepth), delay_opt = None) + log.info("publishing funding tx fundingTxId={}", commitment.fundingTxId) + watchFundingConfirmed(commitment.fundingTxId, d.channelParams.minDepth(nodeParams.channelConf.minDepth), delay_opt = None) // we will publish the funding tx only after the channel state has been written to disk because we want to // make sure we first persist the commitment that returns back the funds to us in case of problem - goto(WAIT_FOR_FUNDING_CONFIRMED) using DATA_WAIT_FOR_FUNDING_CONFIRMED(commitments, blockHeight, None, Left(fundingCreated)) storing() calling publishFundingTx(d.channelId, fundingTx, fundingTxFee, d.replyTo) + goto(WAIT_FOR_FUNDING_CONFIRMED) using DATA_WAIT_FOR_FUNDING_CONFIRMED(commitments, blockHeight, None, Left(d.lastSent)) storing() calling publishFundingTx(d.channelId, d.fundingTx, d.fundingTxFee, d.replyTo) } case Event(c: CloseCommand, d: DATA_WAIT_FOR_FUNDING_SIGNED) => @@ -364,13 +414,13 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers { // we rollback the funding tx, it will never be published wallet.rollback(d.fundingTx) d.replyTo ! OpenChannelResponse.Disconnected - goto(CLOSED) + goto(CLOSED) using IgnoreClosedData(d) case Event(TickChannelOpenTimeout, d: DATA_WAIT_FOR_FUNDING_SIGNED) => // we rollback the funding tx, it will never be published wallet.rollback(d.fundingTx) d.replyTo ! OpenChannelResponse.TimedOut - goto(CLOSED) + goto(CLOSED) using IgnoreClosedData(d) }) when(WAIT_FOR_FUNDING_CONFIRMED)(handleExceptions { @@ -380,9 +430,9 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers { // notification that the funding tx has been successfully published: in that case we don't put a duplicate watch // - we're not using zero-conf, but our peer decided to trust us anyway, in which case we can skip waiting for // confirmations if we're the initiator (no risk of double-spend) and they provided a channel alias - val switchToZeroConf = d.commitments.params.localParams.isChannelOpener && + val switchToZeroConf = d.commitments.localChannelParams.isChannelOpener && remoteChannelReady.alias_opt.isDefined && - !d.commitments.params.zeroConf + !d.commitments.channelParams.zeroConf if (switchToZeroConf) { log.info("this channel isn't zero-conf, but we are funder and they sent an early channel_ready with an alias: no need to wait for confirmations") blockchain ! WatchPublished(self, d.commitments.latest.fundingTxId) @@ -398,7 +448,7 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers { // We still watch the funding tx for confirmation even if we can use the zero-conf channel right away. watchFundingConfirmed(w.tx.txid, Some(nodeParams.channelConf.minDepth), delay_opt = None) val shortIds = createShortIdAliases(d.channelId) - val channelReady = createChannelReady(shortIds, d.commitments.params) + val channelReady = createChannelReady(shortIds, d.commitments) d.deferred.foreach(self ! _) goto(WAIT_FOR_CHANNEL_READY) using DATA_WAIT_FOR_CHANNEL_READY(commitments1, shortIds) storing() sending channelReady case Left(_) => stay() @@ -408,7 +458,7 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers { acceptFundingTxConfirmed(w, d) match { case Right((commitments1, _)) => val shortIds = createShortIdAliases(d.channelId) - val channelReady = createChannelReady(shortIds, d.commitments.params) + val channelReady = createChannelReady(shortIds, d.commitments) d.deferred.foreach(self ! _) goto(WAIT_FOR_CHANNEL_READY) using DATA_WAIT_FOR_CHANNEL_READY(commitments1, shortIds) storing() sending channelReady case Left(_) => stay() @@ -425,7 +475,7 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers { case Event(ProcessCurrentBlockHeight(c), d: DATA_WAIT_FOR_FUNDING_CONFIRMED) => d.fundingTx_opt match { case Some(_) => stay() // we are funder, we're still waiting for the funding tx to be confirmed case None if c.blockHeight - d.waitingSince > FUNDING_TIMEOUT_FUNDEE => - log.warning(s"funding tx hasn't been published in ${c.blockHeight - d.waitingSince} blocks") + log.warning("funding tx hasn't been published in {} blocks", c.blockHeight - d.waitingSince) self ! BITCOIN_FUNDING_TIMEOUT stay() case None => stay() // let's wait longer @@ -439,7 +489,7 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers { when(WAIT_FOR_CHANNEL_READY)(handleExceptions { case Event(channelReady: ChannelReady, d: DATA_WAIT_FOR_CHANNEL_READY) => val d1 = receiveChannelReady(d.aliases, channelReady, d.commitments) - val annSigs_opt = d1.commitments.all.find(_.fundingTxIndex == 0).flatMap(_.signAnnouncement(nodeParams, d1.commitments.params)) + val annSigs_opt = d1.commitments.all.find(_.fundingTxIndex == 0).flatMap(_.signAnnouncement(nodeParams, d1.commitments.channelParams, channelKeys.fundingKey(fundingTxIndex = 0))) annSigs_opt.foreach(annSigs => announcementSigsSent += annSigs.shortChannelId) goto(NORMAL) using d1 storing() sending annSigs_opt.toSeq diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/CommonFundingHandlers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/CommonFundingHandlers.scala index 6c93742af4..0ce656c82e 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/CommonFundingHandlers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/CommonFundingHandlers.scala @@ -24,9 +24,10 @@ import fr.acinq.eclair.channel.Helpers.getRelayFees import fr.acinq.eclair.channel.LocalFundingStatus.{ConfirmedFundingTx, DualFundedUnconfirmedFundingTx, SingleFundedUnconfirmedFundingTx} import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.fsm.Channel.{BroadcastChannelUpdate, PeriodicRefresh, REFRESH_CHANNEL_UPDATE_INTERVAL} +import fr.acinq.eclair.crypto.NonceGenerator import fr.acinq.eclair.db.RevokedHtlcInfoCleaner -import fr.acinq.eclair.router.Announcements -import fr.acinq.eclair.wire.protocol.{AnnouncementSignatures, ChannelReady, ChannelReadyTlv, TlvStream} +import fr.acinq.eclair.transactions.Transactions.{AnchorOutputsCommitmentFormat, SimpleTaprootChannelCommitmentFormat} +import fr.acinq.eclair.wire.protocol._ import fr.acinq.eclair.{RealShortChannelId, ShortChannelId} import scala.concurrent.duration.{DurationInt, FiniteDuration} @@ -44,8 +45,8 @@ trait CommonFundingHandlers extends CommonHandlers { * @param delay_opt optional delay to reduce herd effect at startup. */ def watchFundingSpent(commitment: Commitment, additionalKnownSpendingTxs: Set[TxId], delay_opt: Option[FiniteDuration]): Unit = { - val knownSpendingTxs = Set(commitment.localCommit.commitTxAndRemoteSig.commitTx.tx.txid, commitment.remoteCommit.txid) ++ commitment.nextRemoteCommit_opt.map(_.commit.txid).toSet ++ additionalKnownSpendingTxs - val watch = WatchFundingSpent(self, commitment.commitInput.outPoint.txid, commitment.commitInput.outPoint.index.toInt, knownSpendingTxs) + val knownSpendingTxs = commitment.commitTxIds.txIds ++ additionalKnownSpendingTxs + val watch = WatchFundingSpent(self, commitment.fundingInput.txid, commitment.fundingInput.index.toInt, knownSpendingTxs) delay_opt match { case Some(delay) => context.system.scheduler.scheduleOnce(delay, blockchain.toClassic, watch) case None => blockchain ! watch @@ -74,7 +75,7 @@ trait CommonFundingHandlers extends CommonHandlers { case _: SingleFundedUnconfirmedFundingTx => // in the single-funding case, as fundee, it is the first time we see the full funding tx, we must verify that it is // valid (it pays the correct amount to the correct script). We also check as funder even if it's not really useful - Try(Transaction.correctlySpends(d.commitments.latest.fullySignedLocalCommitTx(keyManager).tx, Seq(w.tx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) match { + Try(Transaction.correctlySpends(d.commitments.latest.fullySignedLocalCommitTx(channelKeys), Seq(w.tx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) match { case Success(_) => () case Failure(t) => log.error(t, s"rejecting channel with invalid funding tx: ${w.tx.bin}") @@ -85,8 +86,8 @@ trait CommonFundingHandlers extends CommonHandlers { context.system.eventStream.publish(TransactionConfirmed(d.channelId, remoteNodeId, w.tx)) d.commitments.all.find(_.fundingTxId == w.tx.txid) match { case Some(c) => - val scid = RealShortChannelId(w.blockHeight, w.txIndex, c.commitInput.outPoint.index.toInt) - val fundingStatus = ConfirmedFundingTx(w.tx, scid, d.commitments.localFundingSigs(w.tx.txid), d.commitments.liquidityPurchase(w.tx.txid)) + val scid = RealShortChannelId(w.blockHeight, w.txIndex, c.fundingInput.index.toInt) + val fundingStatus = ConfirmedFundingTx(w.tx.txIn.map(_.outPoint), w.tx.txOut(c.fundingInput.index.toInt), scid, d.commitments.localFundingSigs(w.tx.txid), d.commitments.liquidityPurchase(w.tx.txid)) // When a splice transaction confirms, it double-spends all the commitment transactions that only applied to the // previous funding transaction. Our peer cannot publish the corresponding revoked commitments anymore, so we can // clean-up the htlc data that we were storing for the matching penalty transactions. @@ -122,11 +123,18 @@ trait CommonFundingHandlers extends CommonHandlers { aliases } - def createChannelReady(aliases: ShortIdAliases, params: ChannelParams): ChannelReady = { - val channelKeyPath = keyManager.keyPath(params.localParams, params.channelConfig) - val nextPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, 1) - // we always send our local alias, even if it isn't explicitly supported, that's an optional TLV anyway - ChannelReady(params.channelId, nextPerCommitmentPoint, TlvStream(ChannelReadyTlv.ShortChannelIdTlv(aliases.localAlias))) + def createChannelReady(aliases: ShortIdAliases, commitments: Commitments): ChannelReady = { + val params = commitments.channelParams + val nextPerCommitmentPoint = channelKeys.commitmentPoint(1) + // Note that we always send our local alias, even if it isn't explicitly supported, that's an optional TLV anyway. + commitments.latest.commitmentFormat match { + case _: SimpleTaprootChannelCommitmentFormat => + val localFundingKey = channelKeys.fundingKey(fundingTxIndex = 0) + val nextLocalNonce = NonceGenerator.verificationNonce(commitments.latest.fundingTxId, localFundingKey, commitments.latest.remoteFundingPubKey, 1) + ChannelReady(params.channelId, nextPerCommitmentPoint, aliases.localAlias, nextLocalNonce.publicNonce) + case _: AnchorOutputsCommitmentFormat => + ChannelReady(params.channelId, nextPerCommitmentPoint, aliases.localAlias) + } } def receiveChannelReady(aliases: ShortIdAliases, channelReady: ChannelReady, commitments: Commitments): DATA_NORMAL = { @@ -139,7 +147,7 @@ trait CommonFundingHandlers extends CommonHandlers { val scidForChannelUpdate = Helpers.scidForChannelUpdate(channelAnnouncement_opt = None, aliases1.localAlias) log.info("using shortChannelId={} for initial channel_update", scidForChannelUpdate) val relayFees = getRelayFees(nodeParams, remoteNodeId, commitments.announceChannel) - val initialChannelUpdate = Announcements.makeChannelUpdate(nodeParams, remoteNodeId, scidForChannelUpdate, commitments.params, relayFees, Helpers.maxHtlcAmount(nodeParams, commitments), enable = true) + val initialChannelUpdate = Helpers.channelUpdate(nodeParams, scidForChannelUpdate, commitments, relayFees, enable = true) // We need to periodically re-send channel updates, otherwise channel will be considered stale and get pruned by network. context.system.scheduler.scheduleWithFixedDelay(initialDelay = REFRESH_CHANNEL_UPDATE_INTERVAL, delay = REFRESH_CHANNEL_UPDATE_INTERVAL, receiver = self, message = BroadcastChannelUpdate(PeriodicRefresh)) val commitments1 = commitments.copy( @@ -150,8 +158,9 @@ trait CommonFundingHandlers extends CommonHandlers { }, remoteNextCommitInfo = Right(channelReady.nextPerCommitmentPoint) ) + channelReady.nextCommitNonce_opt.foreach(nonce => remoteNextCommitNonces = remoteNextCommitNonces + (commitments.latest.fundingTxId -> nonce)) peer ! ChannelReadyForPayments(self, remoteNodeId, commitments.channelId, fundingTxIndex = 0) - DATA_NORMAL(commitments1, aliases1, None, initialChannelUpdate, None, None, None, SpliceStatus.NoSplice) + DATA_NORMAL(commitments1, aliases1, None, initialChannelUpdate, SpliceStatus.NoSplice, None, None, None) } def delayEarlyAnnouncementSigs(remoteAnnSigs: AnnouncementSignatures): Unit = { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/CommonHandlers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/CommonHandlers.scala index 64493d3251..451150db44 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/CommonHandlers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/CommonHandlers.scala @@ -18,11 +18,13 @@ package fr.acinq.eclair.channel.fsm import akka.actor.FSM import fr.acinq.bitcoin.scalacompat.ByteVector32 -import fr.acinq.eclair.{Features, MilliSatoshiLong} +import fr.acinq.eclair.Features import fr.acinq.eclair.channel.Helpers.Closing.MutualClose import fr.acinq.eclair.channel._ +import fr.acinq.eclair.crypto.NonceGenerator import fr.acinq.eclair.db.PendingCommandsDb import fr.acinq.eclair.io.Peer +import fr.acinq.eclair.transactions.Transactions.{AnchorOutputsCommitmentFormat, SimpleTaprootChannelCommitmentFormat} import fr.acinq.eclair.wire.protocol.{ClosingComplete, HtlcSettlementMessage, LightningMessage, Shutdown, UpdateMessage} import scodec.bits.ByteVector @@ -57,7 +59,7 @@ trait CommonHandlers { nodeParams.db.channels.addOrUpdateChannel(d) context.system.eventStream.publish(ChannelPersisted(self, remoteNodeId, d.channelId, d)) state - case _: TransientChannelData => + case _: TransientChannelData | _: ClosedData => log.error(s"can't store data=${state.stateData} in state=${state.stateName}") state } @@ -110,10 +112,10 @@ trait CommonHandlers { case d: DATA_NEGOTIATING_SIMPLE => d.localScriptPubKey case d: DATA_CLOSING => d.finalScriptPubKey case d => - val allowAnySegwit = Features.canUseFeature(data.commitments.params.localParams.initFeatures, data.commitments.params.remoteParams.initFeatures, Features.ShutdownAnySegwit) - d.commitments.params.localParams.upfrontShutdownScript_opt match { + val allowAnySegwit = Features.canUseFeature(data.commitments.localChannelParams.initFeatures, data.commitments.remoteChannelParams.initFeatures, Features.ShutdownAnySegwit) + d.commitments.localChannelParams.upfrontShutdownScript_opt match { case Some(upfrontShutdownScript) => - if (data.commitments.params.channelFeatures.hasFeature(Features.UpfrontShutdownScript)) { + if (data.commitments.channelParams.channelFeatures.hasFeature(Features.UpfrontShutdownScript)) { // we have a shutdown script, and the option_upfront_shutdown_script is enabled: we have to use it upfrontShutdownScript } else { @@ -132,17 +134,31 @@ trait CommonHandlers { finalScriptPubkey } + def createShutdown(commitments: Commitments, finalScriptPubKey: ByteVector): Shutdown = { + commitments.latest.commitmentFormat match { + case _: SimpleTaprootChannelCommitmentFormat => + // We create a fresh local closee nonce every time we send shutdown. + val localFundingPubKey = channelKeys.fundingKey(commitments.latest.fundingTxIndex).publicKey + val localCloseeNonce = NonceGenerator.signingNonce(localFundingPubKey, commitments.latest.remoteFundingPubKey, commitments.latest.fundingTxId) + localCloseeNonce_opt = Some(localCloseeNonce) + Shutdown(commitments.channelId, finalScriptPubKey, localCloseeNonce.publicNonce) + case _: AnchorOutputsCommitmentFormat => + Shutdown(commitments.channelId, finalScriptPubKey) + } + } + def startSimpleClose(commitments: Commitments, localShutdown: Shutdown, remoteShutdown: Shutdown, closeStatus: CloseStatus): (DATA_NEGOTIATING_SIMPLE, Option[ClosingComplete]) = { val localScript = localShutdown.scriptPubKey val remoteScript = remoteShutdown.scriptPubKey - val closingFeerate = closeStatus.feerates_opt.map(_.preferred).getOrElse(nodeParams.onChainFeeConf.getClosingFeerate(nodeParams.currentBitcoinCoreFeerates)) - MutualClose.makeSimpleClosingTx(nodeParams.currentBlockHeight, keyManager, commitments.latest, localScript, remoteScript, closingFeerate) match { + val closingFeerate = closeStatus.feerates_opt.map(_.preferred).getOrElse(nodeParams.onChainFeeConf.getClosingFeerate(nodeParams.currentBitcoinCoreFeerates, maxClosingFeerateOverride_opt = None)) + MutualClose.makeSimpleClosingTx(nodeParams.currentBlockHeight, channelKeys, commitments.latest, localScript, remoteScript, closingFeerate, remoteShutdown.closeeNonce_opt) match { case Left(f) => log.warning("cannot create local closing txs, waiting for remote closing_complete: {}", f.getMessage) val d = DATA_NEGOTIATING_SIMPLE(commitments, closingFeerate, localScript, remoteScript, Nil, Nil) (d, None) - case Right((closingTxs, closingComplete)) => + case Right((closingTxs, closingComplete, closerNonces)) => log.debug("signing local mutual close transactions: {}", closingTxs) + localCloserNonces_opt = Some(closerNonces) val d = DATA_NEGOTIATING_SIMPLE(commitments, closingFeerate, localScript, remoteScript, closingTxs :: Nil, Nil) (d, Some(closingComplete)) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/DualFundingHandlers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/DualFundingHandlers.scala index 227fb75ef3..0f891bea1a 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/DualFundingHandlers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/DualFundingHandlers.scala @@ -19,7 +19,7 @@ package fr.acinq.eclair.channel.fsm import fr.acinq.bitcoin.scalacompat.{Transaction, TxIn} import fr.acinq.eclair.NotificationsLogger import fr.acinq.eclair.NotificationsLogger.NotifyNodeOperator -import fr.acinq.eclair.blockchain.CurrentBlockHeight +import fr.acinq.eclair.blockchain.{CurrentBlockHeight, NewTransaction} import fr.acinq.eclair.channel.Helpers.Closing import fr.acinq.eclair.channel.LocalFundingStatus.DualFundedUnconfirmedFundingTx import fr.acinq.eclair.channel._ @@ -50,7 +50,14 @@ trait DualFundingHandlers extends CommonFundingHandlers { // Note that we don't use wallet.commit because we don't want to rollback on failure, since our peer may be able // to publish and we may be able to RBF. wallet.publishTransaction(fundingTx.signedTx).onComplete { - case Success(_) => context.system.eventStream.publish(TransactionPublished(dualFundedTx.fundingParams.channelId, remoteNodeId, fundingTx.signedTx, fundingTx.tx.localFees.truncateToSatoshi, "funding")) + case Success(_) => + context.system.eventStream.publish(TransactionPublished(dualFundedTx.fundingParams.channelId, remoteNodeId, fundingTx.signedTx, fundingTx.tx.localFees.truncateToSatoshi, "funding")) + // We rely on Bitcoin Core ZMQ notifications to learn about transactions that appear in our mempool, but + // it doesn't provide strong guarantees that we'll always receive an event. This can be an issue for 0-conf + // funding transactions, where we end up delaying our channel_ready or splice_locked. + // If we've successfully published the transaction, we can emit the event ourselves instead of waiting for + // ZMQ: this is safe because duplicate events will be ignored. + context.system.eventStream.publish(NewTransaction(fundingTx.signedTx)) case Failure(t) => log.warning("error while publishing funding tx: {}", t.getMessage) // tx may be published by our peer, we can't fail-fast } } @@ -58,7 +65,7 @@ trait DualFundingHandlers extends CommonFundingHandlers { /** Return true if we should stop waiting for confirmations when receiving our peer's channel_ready. */ def switchToZeroConf(remoteChannelReady: ChannelReady, d: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED): Boolean = { - if (!d.commitments.params.zeroConf) { + if (!d.commitments.channelParams.zeroConf) { // We're not using zero-conf, but our peer decided to trust us anyway. We can skip waiting for confirmations if: // - they provided a channel alias // - there is a single version of the funding tx (otherwise we don't know which one to use) @@ -103,7 +110,7 @@ trait DualFundingHandlers extends CommonFundingHandlers { if (fundingTxIds.subsetOf(e.fundingTxIds)) { log.warning("{} funding attempts have been double-spent, forgetting channel", fundingTxIds.size) d.allFundingTxs.map(_.sharedTx.tx.buildUnsignedTx()).foreach(tx => wallet.rollback(tx)) - goto(CLOSED) sending Error(d.channelId, FundingTxDoubleSpent(d.channelId).getMessage) + goto(CLOSED) using IgnoreClosedData(d) sending Error(d.channelId, FundingTxDoubleSpent(d.channelId).getMessage) } else { // Not all funding attempts have been double-spent, the channel may still confirm. // For example, we may have published an RBF attempt while we were checking if funding attempts were double-spent. diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ErrorHandlers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ErrorHandlers.scala index 361a846880..6807f366ff 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ErrorHandlers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ErrorHandlers.scala @@ -18,16 +18,16 @@ package fr.acinq.eclair.channel.fsm import akka.actor.typed.scaladsl.adapter.actorRefAdapter import akka.actor.{ActorRef, FSM} -import fr.acinq.bitcoin.scalacompat.{ByteVector32, OutPoint, SatoshiLong, Transaction, TxId} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, OutPoint, SatoshiLong, Transaction} import fr.acinq.eclair.NotificationsLogger import fr.acinq.eclair.NotificationsLogger.NotifyNodeOperator -import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.{RelativeDelay, WatchOutputSpent, WatchTxConfirmed} +import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.{WatchOutputSpent, WatchTxConfirmed} +import fr.acinq.eclair.blockchain.fee.FeeratePerKw import fr.acinq.eclair.channel.Helpers.Closing import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.fsm.Channel.UnhandledExceptionStrategy import fr.acinq.eclair.channel.publish.TxPublisher.{PublishFinalTx, PublishReplaceableTx, PublishTx} -import fr.acinq.eclair.transactions.Transactions -import fr.acinq.eclair.transactions.Transactions.ClosingTx +import fr.acinq.eclair.transactions.Transactions._ import fr.acinq.eclair.wire.protocol.{AcceptChannel, ChannelReestablish, Error, OpenChannel, Warning} import java.sql.SQLException @@ -46,7 +46,7 @@ trait ErrorHandlers extends CommonHandlers { def handleFastClose(c: CloseCommand, channelId: ByteVector32) = { val replyTo = if (c.replyTo == ActorRef.noSender) sender() else c.replyTo replyTo ! RES_SUCCESS(c, channelId) - goto(CLOSED) + goto(CLOSED) using IgnoreClosedData(stateData) } def handleMutualClose(closingTx: ClosingTx, d: Either[DATA_NEGOTIATING, DATA_CLOSING]) = { @@ -55,12 +55,12 @@ trait ErrorHandlers extends CommonHandlers { case Left(negotiating) => DATA_CLOSING(negotiating.commitments, waitingSince = nodeParams.currentBlockHeight, finalScriptPubKey = negotiating.localShutdown.scriptPubKey, mutualCloseProposed = negotiating.closingTxProposed.flatten.map(_.unsignedTx), mutualClosePublished = closingTx :: Nil) case Right(closing) => closing.copy(mutualClosePublished = closing.mutualClosePublished :+ closingTx) } - goto(CLOSING) using nextData storing() calling doPublish(closingTx, nextData.commitments.params.localParams.paysClosingFees) + goto(CLOSING) using nextData storing() calling doPublish(closingTx, nextData.commitments.localChannelParams.paysClosingFees) } def doPublish(closingTx: ClosingTx, localPaysClosingFees: Boolean): Unit = { val fee = if (localPaysClosingFees) closingTx.fee else 0.sat - txPublisher ! PublishFinalTx(closingTx, fee, None) + txPublisher ! PublishFinalTx(closingTx.tx, closingTx.input.outPoint, closingTx.desc, fee, None) blockchain ! WatchTxConfirmed(self, closingTx.tx.txid, nodeParams.channelConf.minDepth) } @@ -91,20 +91,24 @@ trait ErrorHandlers extends CommonHandlers { val closing = DATA_CLOSING(negotiating.commitments, waitingSince = nodeParams.currentBlockHeight, finalScriptPubKey = negotiating.localScriptPubKey, mutualCloseProposed = negotiating.proposedClosingTxs.flatMap(_.all), mutualClosePublished = negotiating.publishedClosingTxs) goto(CLOSING) using closing storing() case dd: ChannelDataWithCommitments => + val maxClosingFeerate_opt = msg match { + case Some(cmd: CMD_FORCECLOSE) => cmd.maxClosingFeerate_opt + case _ => None + } // We publish our commitment even if we have nothing at stake: it's a nice thing to do because it lets our peer // get their funds back without delays. cause match { case _: InvalidFundingTx => // invalid funding tx in the single-funding case: we just close the channel - goto(CLOSED) + goto(CLOSED) using IgnoreClosedData(d) case _: ChannelException => // known channel exception: we force close using our current commitment - spendLocalCurrent(dd) sending error + spendLocalCurrent(dd, maxClosingFeerate_opt) sending error case _ => // unhandled exception: we apply the configured strategy nodeParams.channelConf.unhandledExceptionStrategy match { case UnhandledExceptionStrategy.LocalClose => - spendLocalCurrent(dd) sending error + spendLocalCurrent(dd, maxClosingFeerate_opt) sending error case UnhandledExceptionStrategy.Stop => log.error("unhandled exception: standard procedure would be to force-close the channel, but eclair has been configured to halt instead.") NotificationsLogger.logFatalError( @@ -121,8 +125,9 @@ trait ErrorHandlers extends CommonHandlers { } } // When there is no commitment yet, we just send an error to our peer and go to CLOSED state. - case _: ChannelDataWithoutCommitments => goto(CLOSED) sending error - case _: TransientChannelData => goto(CLOSED) sending error + case _: ChannelDataWithoutCommitments => goto(CLOSED) using IgnoreClosedData(d) sending error + case _: TransientChannelData => goto(CLOSED) using IgnoreClosedData(d) sending error + case _: ClosedData => stay() } } @@ -156,13 +161,14 @@ trait ErrorHandlers extends CommonHandlers { log.warning("ignoring remote 'link failed to shutdown', probably coming from lnd") stay() sending Warning(d.channelId, "ignoring your 'link failed to shutdown' to avoid an unnecessary force-close") } else { - spendLocalCurrent(hasCommitments) + spendLocalCurrent(hasCommitments, maxClosingFeerateOverride_opt = None) } // When there is no commitment yet, we just go to CLOSED state in case an error occurs. case waitForDualFundingSigned: DATA_WAIT_FOR_DUAL_FUNDING_SIGNED => rollbackFundingAttempt(waitForDualFundingSigned.signingSession.fundingTx.tx, Nil) - goto(CLOSED) - case _: TransientChannelData => goto(CLOSED) + goto(CLOSED) using IgnoreClosedData(d) + case _: TransientChannelData => goto(CLOSED) using IgnoreClosedData(d) + case _: ClosedData => stay() } } @@ -170,20 +176,11 @@ trait ErrorHandlers extends CommonHandlers { * This helper method will publish txs only if they haven't yet reached minDepth */ private def publishIfNeeded(txs: Iterable[PublishTx], irrevocablySpent: Map[OutPoint, Transaction]): Unit = { - val (skip, process) = txs.partition(publishTx => Closing.inputAlreadySpent(publishTx.input, irrevocablySpent)) + val (skip, process) = txs.partition(publishTx => irrevocablySpent.contains(publishTx.input)) process.foreach { publishTx => txPublisher ! publishTx } skip.foreach(publishTx => log.debug("no need to republish tx spending {}:{}, it has already been confirmed", publishTx.input.txid, publishTx.input.index)) } - /** - * This helper method will watch txs only if they haven't yet reached minDepth - */ - private def watchConfirmedIfNeeded(txs: Iterable[Transaction], irrevocablySpent: Map[OutPoint, Transaction], relativeDelays: Map[TxId, RelativeDelay]): Unit = { - val (skip, process) = txs.partition(Closing.inputsAlreadySpent(_, irrevocablySpent)) - process.foreach(tx => blockchain ! WatchTxConfirmed(self, tx.txid, nodeParams.channelConf.minDepth, relativeDelays.get(tx.txid))) - skip.foreach(tx => log.debug(s"no need to watch txid=${tx.txid}, it has already been confirmed")) - } - /** * This helper method will watch txs only if the utxo they spend hasn't already been irrevocably spent * @@ -199,7 +196,14 @@ trait ErrorHandlers extends CommonHandlers { skip.foreach(output => log.debug(s"no need to watch output=${output.txid}:${output.index}, it has already been spent by txid=${irrevocablySpent.get(output).map(_.txid)}")) } - def spendLocalCurrent(d: ChannelDataWithCommitments) = { + /** This helper method will watch the given output only if it hasn't already been irrevocably spent. */ + private def watchSpentIfNeeded(input: InputInfo, irrevocablySpent: Map[OutPoint, Transaction]): Unit = { + if (!irrevocablySpent.contains(input.outPoint)) { + blockchain ! WatchOutputSpent(self, input.outPoint.txid, input.outPoint.index.toInt, input.txOut.amount, Set.empty) + } + } + + def spendLocalCurrent(d: ChannelDataWithCommitments, maxClosingFeerateOverride_opt: Option[FeeratePerKw]): FSM.State[ChannelState, ChannelData] = { val outdatedCommitment = d match { case _: DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT => true case closing: DATA_CLOSING if closing.futureRemoteCommitPublished.isDefined => true @@ -213,74 +217,87 @@ trait ErrorHandlers extends CommonHandlers { val commitment = d.commitments.latest log.error(s"force-closing with fundingIndex=${commitment.fundingTxIndex}") context.system.eventStream.publish(NotifyNodeOperator(NotificationsLogger.Error, s"force-closing channel ${d.channelId} with fundingIndex=${commitment.fundingTxIndex}")) - val commitTx = commitment.fullySignedLocalCommitTx(keyManager).tx - val localCommitPublished = Closing.LocalClose.claimCommitTxOutputs(keyManager, commitment, commitTx, nodeParams.currentBitcoinCoreFeerates, nodeParams.onChainFeeConf, finalScriptPubKey) + val commitTx = commitment.fullySignedLocalCommitTx(channelKeys) + val closingFeerate = nodeParams.onChainFeeConf.getClosingFeerate(nodeParams.currentBitcoinCoreFeerates, maxClosingFeerateOverride_opt) + val (localCommitPublished, closingTxs) = Closing.LocalClose.claimCommitTxOutputs(channelKeys, commitment, commitTx, closingFeerate, finalScriptPubKey, nodeParams.onChainFeeConf.spendAnchorWithoutHtlcs) val nextData = d match { - case closing: DATA_CLOSING => closing.copy(localCommitPublished = Some(localCommitPublished)) + case closing: DATA_CLOSING => closing.copy(localCommitPublished = Some(localCommitPublished), maxClosingFeerate_opt = maxClosingFeerateOverride_opt.orElse(closing.maxClosingFeerate_opt)) case negotiating: DATA_NEGOTIATING => DATA_CLOSING(d.commitments, waitingSince = nodeParams.currentBlockHeight, finalScriptPubKey = finalScriptPubKey, negotiating.closingTxProposed.flatten.map(_.unsignedTx), localCommitPublished = Some(localCommitPublished)) case negotiating: DATA_NEGOTIATING_SIMPLE => DATA_CLOSING(d.commitments, waitingSince = nodeParams.currentBlockHeight, finalScriptPubKey = finalScriptPubKey, mutualCloseProposed = negotiating.proposedClosingTxs.flatMap(_.all), mutualClosePublished = negotiating.publishedClosingTxs, localCommitPublished = Some(localCommitPublished)) - case _ => DATA_CLOSING(d.commitments, waitingSince = nodeParams.currentBlockHeight, finalScriptPubKey = finalScriptPubKey, mutualCloseProposed = Nil, localCommitPublished = Some(localCommitPublished)) + case _ => DATA_CLOSING(d.commitments, waitingSince = nodeParams.currentBlockHeight, finalScriptPubKey = finalScriptPubKey, mutualCloseProposed = Nil, localCommitPublished = Some(localCommitPublished), maxClosingFeerate_opt = maxClosingFeerateOverride_opt) } - goto(CLOSING) using nextData storing() calling doPublish(localCommitPublished, commitment) + goto(CLOSING) using nextData storing() calling doPublish(localCommitPublished, closingTxs, commitment) } } - def doPublish(localCommitPublished: LocalCommitPublished, commitment: FullCommitment): Unit = { - import localCommitPublished._ - - val localPaysCommitTxFees = commitment.localParams.paysCommitTxFees - val publishQueue = commitment.params.commitmentFormat match { - case Transactions.DefaultCommitmentFormat => - val redeemableHtlcTxs = htlcTxs.values.flatten.map(tx => PublishFinalTx(tx, tx.fee, Some(commitTx.txid))) - List(PublishFinalTx(commitTx, commitment.commitInput.outPoint, commitment.capacity, "commit-tx", Closing.commitTxFee(commitment.commitInput, commitTx, localPaysCommitTxFees), None)) ++ (claimMainDelayedOutputTx.map(tx => PublishFinalTx(tx, tx.fee, None)) ++ redeemableHtlcTxs ++ claimHtlcDelayedTxs.map(tx => PublishFinalTx(tx, tx.fee, None))) - case _: Transactions.AnchorOutputsCommitmentFormat => - val redeemableHtlcTxs = htlcTxs.values.flatten.map(tx => PublishReplaceableTx(tx, commitment, commitTx)) - val claimLocalAnchor = claimAnchorTxs.collect { case tx: Transactions.ClaimLocalAnchorOutputTx if !localCommitPublished.isConfirmed => PublishReplaceableTx(tx, commitment, commitTx) } - List(PublishFinalTx(commitTx, commitment.commitInput.outPoint, commitment.capacity, "commit-tx", Closing.commitTxFee(commitment.commitInput, commitTx, localPaysCommitTxFees), None)) ++ claimLocalAnchor ++ claimMainDelayedOutputTx.map(tx => PublishFinalTx(tx, tx.fee, None)) ++ redeemableHtlcTxs ++ claimHtlcDelayedTxs.map(tx => PublishFinalTx(tx, tx.fee, None)) + /** Publish 2nd-stage transactions for our local commitment. */ + def doPublish(lcp: LocalCommitPublished, txs: Closing.LocalClose.SecondStageTransactions, commitment: FullCommitment): Unit = { + val publishCommitTx = PublishFinalTx(lcp.commitTx, commitment.fundingInput, "commit-tx", Closing.commitTxFee(commitment.commitInput(channelKeys), lcp.commitTx, commitment.localChannelParams.paysCommitTxFees), None) + val publishAnchorTx_opt = txs.anchorTx_opt match { + case Some(anchorTx) if !lcp.isConfirmed => + val confirmationTarget = Closing.confirmationTarget(commitment.localCommit, commitment.localCommitParams.dustLimit, commitment.commitmentFormat, nodeParams.onChainFeeConf) + Some(PublishReplaceableTx(anchorTx, lcp.commitTx, commitment, confirmationTarget)) + case _ => None + } + val publishMainDelayedTx_opt = txs.mainDelayedTx_opt.map(tx => PublishFinalTx(tx, None)) + val publishHtlcTxs = txs.htlcTxs.map(htlcTx => PublishReplaceableTx(htlcTx, lcp.commitTx, commitment, Closing.confirmationTarget(htlcTx))) + val publishQueue = Seq(publishCommitTx) ++ publishAnchorTx_opt ++ publishMainDelayedTx_opt ++ publishHtlcTxs + publishIfNeeded(publishQueue, lcp.irrevocablySpent) + + if (!lcp.isConfirmed) { + // We watch the commitment transaction: once confirmed, it invalidates other types of force-close. + blockchain ! WatchTxConfirmed(self, lcp.commitTx.txid, nodeParams.channelConf.minDepth) } - publishIfNeeded(publishQueue, irrevocablySpent) - - // We watch: - // - the commitment tx itself, so that we can handle the case where we don't have any outputs - // - 'final txs' that send funds to our wallet and that spend outputs that only us control - // Our 'final txs" have a long relative delay: we provide that information to the watcher for efficiency. - val relativeDelays = (claimMainDelayedOutputTx ++ claimHtlcDelayedTxs).map(tx => tx.tx.txid -> RelativeDelay(tx.input.outPoint.txid, commitment.remoteParams.toSelfDelay.toInt.toLong)).toMap - val watchConfirmedQueue = List(commitTx) ++ claimMainDelayedOutputTx.map(_.tx) ++ claimHtlcDelayedTxs.map(_.tx) - watchConfirmedIfNeeded(watchConfirmedQueue, irrevocablySpent, relativeDelays) - - // We watch outputs of the commitment tx that both parties may spend. - // We also watch our local anchor: this ensures that we will correctly detect when it's confirmed and count its fees - // in the audit DB, even if we restart before confirmation. - val watchSpentQueue = htlcTxs.keys ++ claimAnchorTxs.collect { case tx: Transactions.ClaimLocalAnchorOutputTx if !localCommitPublished.isConfirmed => tx.input.outPoint } - watchSpentIfNeeded(commitTx, watchSpentQueue, irrevocablySpent) + + // We watch outputs of the commitment transaction that we may spend: every time we detect a spending transaction, + // we will watch for its confirmation. This ensures that we detect double-spends that could come from: + // - our own RBF attempts + // - remote transactions for outputs that both parties may spend (e.g. HTLCs) + val watchSpentQueue = lcp.localOutput_opt ++ (if (!lcp.isConfirmed) lcp.anchorOutput_opt else None) ++ lcp.htlcOutputs.toSeq + watchSpentIfNeeded(lcp.commitTx, watchSpentQueue, lcp.irrevocablySpent) + } + + /** Publish 3rd-stage transactions for our local commitment. */ + def doPublish(lcp: LocalCommitPublished, txs: Closing.LocalClose.ThirdStageTransactions): Unit = { + val publishHtlcDelayedTxs = txs.htlcDelayedTxs.map(tx => PublishFinalTx(tx, None)) + publishIfNeeded(publishHtlcDelayedTxs, lcp.irrevocablySpent) + // We watch the spent outputs to detect our RBF attempts. + txs.htlcDelayedTxs.foreach(tx => watchSpentIfNeeded(tx.input, lcp.irrevocablySpent)) } def handleRemoteSpentCurrent(commitTx: Transaction, d: ChannelDataWithCommitments) = { val commitments = d.commitments.latest log.warning(s"they published their current commit in txid=${commitTx.txid}") - require(commitTx.txid == commitments.remoteCommit.txid, "txid mismatch") + require(commitTx.txid == commitments.remoteCommit.txId, "txid mismatch") val finalScriptPubKey = getOrGenerateFinalScriptPubKey(d) - context.system.eventStream.publish(TransactionPublished(d.channelId, remoteNodeId, commitTx, Closing.commitTxFee(commitments.commitInput, commitTx, d.commitments.params.localParams.paysCommitTxFees), "remote-commit")) - val remoteCommitPublished = Closing.RemoteClose.claimCommitTxOutputs(keyManager, commitments, commitments.remoteCommit, commitTx, nodeParams.currentBitcoinCoreFeerates, nodeParams.onChainFeeConf, finalScriptPubKey) + val closingFeerate = d match { + case closing: DATA_CLOSING => nodeParams.onChainFeeConf.getClosingFeerate(nodeParams.currentBitcoinCoreFeerates, closing.maxClosingFeerate_opt) + case _ => nodeParams.onChainFeeConf.getClosingFeerate(nodeParams.currentBitcoinCoreFeerates, maxClosingFeerateOverride_opt = None) + } + context.system.eventStream.publish(TransactionPublished(d.channelId, remoteNodeId, commitTx, Closing.commitTxFee(commitments.commitInput(channelKeys), commitTx, d.commitments.localChannelParams.paysCommitTxFees), "remote-commit")) + val (remoteCommitPublished, closingTxs) = Closing.RemoteClose.claimCommitTxOutputs(channelKeys, commitments, commitments.remoteCommit, commitTx, closingFeerate, finalScriptPubKey, nodeParams.onChainFeeConf.spendAnchorWithoutHtlcs) val nextData = d match { case closing: DATA_CLOSING => closing.copy(remoteCommitPublished = Some(remoteCommitPublished)) case negotiating: DATA_NEGOTIATING => DATA_CLOSING(d.commitments, waitingSince = nodeParams.currentBlockHeight, finalScriptPubKey = finalScriptPubKey, mutualCloseProposed = negotiating.closingTxProposed.flatten.map(_.unsignedTx), remoteCommitPublished = Some(remoteCommitPublished)) case negotiating: DATA_NEGOTIATING_SIMPLE => DATA_CLOSING(d.commitments, waitingSince = nodeParams.currentBlockHeight, finalScriptPubKey = finalScriptPubKey, mutualCloseProposed = negotiating.proposedClosingTxs.flatMap(_.all), mutualClosePublished = negotiating.publishedClosingTxs, remoteCommitPublished = Some(remoteCommitPublished)) case _ => DATA_CLOSING(d.commitments, waitingSince = nodeParams.currentBlockHeight, finalScriptPubKey = finalScriptPubKey, mutualCloseProposed = Nil, remoteCommitPublished = Some(remoteCommitPublished)) } - goto(CLOSING) using nextData storing() calling doPublish(remoteCommitPublished, commitments) + goto(CLOSING) using nextData storing() calling doPublish(remoteCommitPublished, closingTxs, commitments) } def handleRemoteSpentNext(commitTx: Transaction, d: ChannelDataWithCommitments) = { val commitment = d.commitments.latest log.warning(s"they published their next commit in txid=${commitTx.txid}") require(commitment.nextRemoteCommit_opt.nonEmpty, "next remote commit must be defined") - val remoteCommit = commitment.nextRemoteCommit_opt.get.commit - require(commitTx.txid == remoteCommit.txid, "txid mismatch") - + val remoteCommit = commitment.nextRemoteCommit_opt.get + require(commitTx.txid == remoteCommit.txId, "txid mismatch") val finalScriptPubKey = getOrGenerateFinalScriptPubKey(d) - context.system.eventStream.publish(TransactionPublished(d.channelId, remoteNodeId, commitTx, Closing.commitTxFee(commitment.commitInput, commitTx, d.commitments.params.localParams.paysCommitTxFees), "next-remote-commit")) - val remoteCommitPublished = Closing.RemoteClose.claimCommitTxOutputs(keyManager, commitment, remoteCommit, commitTx, nodeParams.currentBitcoinCoreFeerates, nodeParams.onChainFeeConf, finalScriptPubKey) + val closingFeerate = d match { + case closing: DATA_CLOSING => nodeParams.onChainFeeConf.getClosingFeerate(nodeParams.currentBitcoinCoreFeerates, closing.maxClosingFeerate_opt) + case _ => nodeParams.onChainFeeConf.getClosingFeerate(nodeParams.currentBitcoinCoreFeerates, maxClosingFeerateOverride_opt = None) + } + context.system.eventStream.publish(TransactionPublished(d.channelId, remoteNodeId, commitTx, Closing.commitTxFee(commitment.commitInput(channelKeys), commitTx, d.commitments.localChannelParams.paysCommitTxFees), "next-remote-commit")) + val (remoteCommitPublished, closingTxs) = Closing.RemoteClose.claimCommitTxOutputs(channelKeys, commitment, remoteCommit, commitTx, closingFeerate, finalScriptPubKey, nodeParams.onChainFeeConf.spendAnchorWithoutHtlcs) val nextData = d match { case closing: DATA_CLOSING => closing.copy(nextRemoteCommitPublished = Some(remoteCommitPublished)) case negotiating: DATA_NEGOTIATING => DATA_CLOSING(d.commitments, waitingSince = nodeParams.currentBlockHeight, finalScriptPubKey = finalScriptPubKey, mutualCloseProposed = negotiating.closingTxProposed.flatten.map(_.unsignedTx), nextRemoteCommitPublished = Some(remoteCommitPublished)) @@ -288,37 +305,52 @@ trait ErrorHandlers extends CommonHandlers { // NB: if there is a next commitment, we can't be in DATA_WAIT_FOR_FUNDING_CONFIRMED so we don't have the case where fundingTx is defined case _ => DATA_CLOSING(d.commitments, waitingSince = nodeParams.currentBlockHeight, finalScriptPubKey = finalScriptPubKey, mutualCloseProposed = Nil, nextRemoteCommitPublished = Some(remoteCommitPublished)) } - goto(CLOSING) using nextData storing() calling doPublish(remoteCommitPublished, commitment) + goto(CLOSING) using nextData storing() calling doPublish(remoteCommitPublished, closingTxs, commitment) } - def doPublish(remoteCommitPublished: RemoteCommitPublished, commitment: FullCommitment): Unit = { - import remoteCommitPublished._ - - val claimLocalAnchor = claimAnchorTxs.collect { case tx: Transactions.ClaimLocalAnchorOutputTx if !remoteCommitPublished.isConfirmed => PublishReplaceableTx(tx, commitment, commitTx) } - val redeemableHtlcTxs = claimHtlcTxs.values.flatten.map(tx => PublishReplaceableTx(tx, commitment, commitTx)) - val publishQueue = claimLocalAnchor ++ claimMainOutputTx.map(tx => PublishFinalTx(tx, tx.fee, None)).toSeq ++ redeemableHtlcTxs - publishIfNeeded(publishQueue, irrevocablySpent) - - // We watch: - // - the commitment tx itself, so that we can handle the case where we don't have any outputs - // - 'final txs' that send funds to our wallet and that spend outputs that only us control - val watchConfirmedQueue = List(commitTx) ++ claimMainOutputTx.map(_.tx) - watchConfirmedIfNeeded(watchConfirmedQueue, irrevocablySpent, relativeDelays = Map.empty) + /** Publish 2nd-stage transactions for the remote commitment (no need for 3rd-stage transactions in that case). */ + def doPublish(rcp: RemoteCommitPublished, txs: Closing.RemoteClose.SecondStageTransactions, commitment: FullCommitment): Unit = { + val remoteCommit = commitment.nextRemoteCommit_opt match { + case Some(commit) if rcp.commitTx.txid == commit.txId => commit + case _ => commitment.remoteCommit + } + val publishAnchorTx_opt = txs.anchorTx_opt match { + case Some(anchorTx) if !rcp.isConfirmed => + val confirmationTarget = Closing.confirmationTarget(remoteCommit, commitment.remoteCommitParams.dustLimit, commitment.commitmentFormat, nodeParams.onChainFeeConf) + Some(PublishReplaceableTx(anchorTx, rcp.commitTx, commitment, confirmationTarget)) + case _ => None + } + val publishMainTx_opt = txs.mainTx_opt.map(tx => PublishFinalTx(tx, None)) + val publishHtlcTxs = txs.htlcTxs.map(htlcTx => PublishReplaceableTx(htlcTx, rcp.commitTx, commitment, Closing.confirmationTarget(htlcTx))) + val publishQueue = publishAnchorTx_opt ++ publishMainTx_opt ++ publishHtlcTxs + publishIfNeeded(publishQueue, rcp.irrevocablySpent) + + if (!rcp.isConfirmed) { + // We watch the commitment transaction: once confirmed, it invalidates other types of force-close. + blockchain ! WatchTxConfirmed(self, rcp.commitTx.txid, nodeParams.channelConf.minDepth) + } - // We watch outputs of the commitment tx that both parties may spend. - val watchSpentQueue = claimHtlcTxs.keys - watchSpentIfNeeded(commitTx, watchSpentQueue, irrevocablySpent) + // We watch outputs of the commitment transaction that we may spend: every time we detect a spending transaction, + // we will watch for its confirmation. This ensures that we detect double-spends that could come from: + // - our own RBF attempts + // - remote transactions for outputs that both parties may spend (e.g. HTLCs) + val watchSpentQueue = rcp.localOutput_opt ++ (if (!rcp.isConfirmed) rcp.anchorOutput_opt else None) ++ rcp.htlcOutputs.toSeq + watchSpentIfNeeded(rcp.commitTx, watchSpentQueue, rcp.irrevocablySpent) } def handleRemoteSpentOther(tx: Transaction, d: ChannelDataWithCommitments) = { val commitment = d.commitments.latest - log.warning(s"funding tx spent in txid=${tx.txid}") + log.warning("funding tx spent by txid={}", tx.txid) val finalScriptPubKey = getOrGenerateFinalScriptPubKey(d) - Closing.RevokedClose.getRemotePerCommitmentSecret(keyManager, d.commitments.params, d.commitments.remotePerCommitmentSecrets, tx) match { + Closing.RevokedClose.getRemotePerCommitmentSecret(d.commitments.channelParams, channelKeys, d.commitments.remotePerCommitmentSecrets, tx) match { case Some((commitmentNumber, remotePerCommitmentSecret)) => - val revokedCommitPublished = Closing.RevokedClose.claimCommitTxOutputs(keyManager, d.commitments.params, tx, commitmentNumber, remotePerCommitmentSecret, nodeParams.db.channels, nodeParams.currentBitcoinCoreFeerates, nodeParams.onChainFeeConf, finalScriptPubKey) - log.warning(s"txid=${tx.txid} was a revoked commitment, publishing the penalty tx") - context.system.eventStream.publish(TransactionPublished(d.channelId, remoteNodeId, tx, Closing.commitTxFee(commitment.commitInput, tx, d.commitments.params.localParams.paysCommitTxFees), "revoked-commit")) + // TODO: once we allow changing the commitment format or to_self_delay during a splice, those values may be incorrect. + val toSelfDelay = commitment.remoteCommitParams.toSelfDelay + val commitmentFormat = commitment.commitmentFormat + val dustLimit = commitment.localCommitParams.dustLimit + val (revokedCommitPublished, closingTxs) = Closing.RevokedClose.claimCommitTxOutputs(d.commitments.channelParams, channelKeys, tx, commitmentNumber, remotePerCommitmentSecret, toSelfDelay, commitmentFormat, nodeParams.db.channels, dustLimit, nodeParams.currentBitcoinCoreFeerates, nodeParams.onChainFeeConf, finalScriptPubKey) + log.warning("txid={} was a revoked commitment, publishing the penalty tx", tx.txid) + context.system.eventStream.publish(TransactionPublished(d.channelId, remoteNodeId, tx, Closing.commitTxFee(commitment.commitInput(channelKeys), tx, d.commitments.localChannelParams.paysCommitTxFees), "revoked-commit")) val exc = FundingTxSpent(d.channelId, tx.txid) val error = Error(d.channelId, exc.getMessage) val nextData = d match { @@ -328,20 +360,26 @@ trait ErrorHandlers extends CommonHandlers { // NB: if there is a revoked commitment, we can't be in DATA_WAIT_FOR_FUNDING_CONFIRMED so we don't have the case where fundingTx is defined case _ => DATA_CLOSING(d.commitments, waitingSince = nodeParams.currentBlockHeight, finalScriptPubKey = finalScriptPubKey, mutualCloseProposed = Nil, revokedCommitPublished = revokedCommitPublished :: Nil) } - goto(CLOSING) using nextData storing() calling doPublish(revokedCommitPublished) sending error + goto(CLOSING) using nextData storing() calling doPublish(revokedCommitPublished, closingTxs) sending error case None => d match { case d: DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT => - log.warning(s"they published a future commit (because we asked them to) in txid=${tx.txid}") - context.system.eventStream.publish(TransactionPublished(d.channelId, remoteNodeId, tx, Closing.commitTxFee(d.commitments.latest.commitInput, tx, d.commitments.latest.localParams.paysCommitTxFees), "future-remote-commit")) + log.warning("they published a future commit (because we asked them to) in txid={}", tx.txid) + context.system.eventStream.publish(TransactionPublished(d.channelId, remoteNodeId, tx, Closing.commitTxFee(d.commitments.latest.commitInput(channelKeys), tx, d.commitments.localChannelParams.paysCommitTxFees), "future-remote-commit")) val remotePerCommitmentPoint = d.remoteChannelReestablish.myCurrentPerCommitmentPoint + val commitKeys = d.commitments.latest.remoteKeys(channelKeys, remotePerCommitmentPoint) + val closingFeerate = nodeParams.onChainFeeConf.getClosingFeerate(nodeParams.currentBitcoinCoreFeerates, maxClosingFeerateOverride_opt = None) + val mainTx_opt = Closing.RemoteClose.claimMainOutput(commitKeys, tx, d.commitments.latest.localCommitParams.dustLimit, d.commitments.latest.commitmentFormat, closingFeerate, finalScriptPubKey) + mainTx_opt.foreach(tx => log.warning("publishing our recovery transaction: tx={}", tx.toString)) val remoteCommitPublished = RemoteCommitPublished( commitTx = tx, - claimMainOutputTx = Closing.RemoteClose.claimMainOutput(keyManager, d.commitments.params, remotePerCommitmentPoint, tx, nodeParams.currentBitcoinCoreFeerates, nodeParams.onChainFeeConf, finalScriptPubKey), - claimHtlcTxs = Map.empty, - claimAnchorTxs = List.empty, + localOutput_opt = mainTx_opt.map(_.input.outPoint), + anchorOutput_opt = None, + incomingHtlcs = Map.empty, + outgoingHtlcs = Map.empty, irrevocablySpent = Map.empty) + val closingTxs = Closing.RemoteClose.SecondStageTransactions(mainTx_opt, anchorTx_opt = None, htlcTxs = Nil) val nextData = DATA_CLOSING(d.commitments, waitingSince = nodeParams.currentBlockHeight, finalScriptPubKey = finalScriptPubKey, mutualCloseProposed = Nil, futureRemoteCommitPublished = Some(remoteCommitPublished)) - goto(CLOSING) using nextData storing() calling doPublish(remoteCommitPublished, d.commitments.latest) + goto(CLOSING) using nextData storing() calling doPublish(remoteCommitPublished, closingTxs, d.commitments.latest) case _ => // the published tx doesn't seem to be a valid commitment transaction log.error(s"couldn't identify txid=${tx.txid}, something very bad is going on!!!") @@ -351,21 +389,27 @@ trait ErrorHandlers extends CommonHandlers { } } - def doPublish(revokedCommitPublished: RevokedCommitPublished): Unit = { - import revokedCommitPublished._ + /** Publish 2nd-stage transactions for a revoked remote commitment. */ + def doPublish(rvk: RevokedCommitPublished, txs: Closing.RevokedClose.SecondStageTransactions): Unit = { + val publishQueue = (txs.mainTx_opt ++ txs.mainPenaltyTx_opt ++ txs.htlcPenaltyTxs).map(tx => PublishFinalTx(tx, None)) + publishIfNeeded(publishQueue, rvk.irrevocablySpent) - val publishQueue = (claimMainOutputTx ++ mainPenaltyTx ++ htlcPenaltyTxs ++ claimHtlcDelayedPenaltyTxs).map(tx => PublishFinalTx(tx, tx.fee, None)) - publishIfNeeded(publishQueue, irrevocablySpent) + if (!rvk.isConfirmed) { + // We watch the commitment transaction: once confirmed, it invalidates other types of force-close. + blockchain ! WatchTxConfirmed(self, rvk.commitTx.txid, nodeParams.channelConf.minDepth) + } - // We watch: - // - the commitment tx itself, so that we can handle the case where we don't have any outputs - // - 'final txs' that send funds to our wallet and that spend outputs that only us control - val watchConfirmedQueue = List(commitTx) ++ claimMainOutputTx.map(_.tx) - watchConfirmedIfNeeded(watchConfirmedQueue, irrevocablySpent, relativeDelays = Map.empty) + // We watch outputs of the commitment tx that both parties may spend, or that we may RBF. + val watchSpentQueue = rvk.localOutput_opt ++ rvk.remoteOutput_opt ++ rvk.htlcOutputs.toSeq + watchSpentIfNeeded(rvk.commitTx, watchSpentQueue, rvk.irrevocablySpent) + } - // We watch outputs of the commitment tx that both parties may spend. - val watchSpentQueue = (mainPenaltyTx ++ htlcPenaltyTxs).map(_.input.outPoint) - watchSpentIfNeeded(commitTx, watchSpentQueue, irrevocablySpent) + /** Publish 3rd-stage transactions for a revoked remote commitment. */ + def doPublish(rvk: RevokedCommitPublished, txs: Closing.RevokedClose.ThirdStageTransactions): Unit = { + val publishQueue = txs.htlcDelayedPenaltyTxs.map(tx => PublishFinalTx(tx, None)) + publishIfNeeded(publishQueue, rvk.irrevocablySpent) + // We watch the spent outputs to detect our own RBF attempts. + txs.htlcDelayedPenaltyTxs.foreach(tx => watchSpentIfNeeded(tx.input, rvk.irrevocablySpent)) } def handleOutdatedCommitment(channelReestablish: ChannelReestablish, d: ChannelDataWithCommitments) = { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/SingleFundingHandlers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/SingleFundingHandlers.scala index 6f9bcf7bc1..95a1192493 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/SingleFundingHandlers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/SingleFundingHandlers.scala @@ -79,7 +79,7 @@ trait SingleFundingHandlers extends CommonFundingHandlers { // if we are funder, we never give up // we cannot correctly set the fee, but it was correctly set when we initially published the transaction log.debug("republishing the funding tx...") - txPublisher ! PublishFinalTx(fundingTx, fundingTx.txIn.head.outPoint, 0 sat, "funding", 0 sat, None) + txPublisher ! PublishFinalTx(fundingTx, fundingTx.txIn.head.outPoint, "funding", 0 sat, None) // we also check if the funding tx has been double-spent checkDoubleSpent(fundingTx) context.system.scheduler.scheduleOnce(1 day, blockchain.toClassic, GetTxWithMeta(self, txid)) @@ -97,21 +97,26 @@ trait SingleFundingHandlers extends CommonFundingHandlers { } def handleFundingPublishFailed(d: PersistentChannelData) = { - log.error(s"failed to publish funding tx") + log.error("failed to publish funding tx") val exc = ChannelFundingError(d.channelId) val error = Error(d.channelId, exc.getMessage) // NB: we don't use the handleLocalError handler because it would result in the commit tx being published, which we don't want: // implementation *guarantees* that in case of BITCOIN_FUNDING_PUBLISH_FAILED, the funding tx hasn't and will never be published, so we can close the channel right away context.system.eventStream.publish(ChannelErrorOccurred(self, stateData.channelId, remoteNodeId, LocalError(exc), isFatal = true)) - goto(CLOSED) sending error + goto(CLOSED) using IgnoreClosedData(d) sending error } def handleFundingTimeout(d: PersistentChannelData) = { - log.warning(s"funding tx hasn't been confirmed in time, cancelling channel delay=$FUNDING_TIMEOUT_FUNDEE") + // We log the commit tx: if our peer loses their channel backup, they will need that commit tx to recover their funds. + val commitTx_opt = d match { + case _: ChannelDataWithoutCommitments => None + case d: ChannelDataWithCommitments => Some(d.commitments.latest.fullySignedLocalCommitTx(channelKeys)) + } + log.warning("funding tx hasn't been confirmed after {} blocks, ignoring channel (commitTx={})", FUNDING_TIMEOUT_FUNDEE, commitTx_opt.getOrElse("n/a")) val exc = FundingTxTimedout(d.channelId) val error = Error(d.channelId, exc.getMessage) context.system.eventStream.publish(ChannelErrorOccurred(self, stateData.channelId, remoteNodeId, LocalError(exc), isFatal = true)) - goto(CLOSED) sending error + goto(CLOSED) using IgnoreClosedData(d) sending error } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fund/InteractiveTxBuilder.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fund/InteractiveTxBuilder.scala index 94dec79916..8eb18a7a2c 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fund/InteractiveTxBuilder.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fund/InteractiveTxBuilder.scala @@ -23,21 +23,24 @@ import akka.actor.typed.{ActorRef, Behavior} import akka.event.LoggingAdapter import fr.acinq.bitcoin.ScriptFlags import fr.acinq.bitcoin.psbt.Psbt -import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey -import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, LexicographicalOrdering, OutPoint, Satoshi, SatoshiLong, Script, ScriptWitness, Transaction, TxId, TxIn, TxOut} +import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} +import fr.acinq.bitcoin.scalacompat.Musig2.{IndividualNonce, LocalNonce} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, LexicographicalOrdering, OutPoint, Satoshi, SatoshiLong, Script, ScriptWitness, Transaction, TxId, TxIn, TxOut} import fr.acinq.eclair.blockchain.OnChainChannelFunder import fr.acinq.eclair.blockchain.fee.FeeratePerKw +import fr.acinq.eclair.channel.ChannelSpendSignature.IndividualSignature import fr.acinq.eclair.channel.Helpers.Closing.MutualClose import fr.acinq.eclair.channel.Helpers.Funding import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.fund.InteractiveTxBuilder.Output.Local import fr.acinq.eclair.channel.fund.InteractiveTxBuilder.Purpose import fr.acinq.eclair.channel.fund.InteractiveTxSigningSession.UnsignedLocalCommit -import fr.acinq.eclair.crypto.keymanager.ChannelKeyManager -import fr.acinq.eclair.transactions.Transactions.{CommitTx, HtlcTx, InputInfo, TxOwner} -import fr.acinq.eclair.transactions.{CommitmentSpec, DirectedHtlc, Scripts, Transactions} +import fr.acinq.eclair.crypto.NonceGenerator +import fr.acinq.eclair.crypto.keymanager.{ChannelKeys, LocalCommitmentKeys, RemoteCommitmentKeys} +import fr.acinq.eclair.transactions.Transactions._ +import fr.acinq.eclair.transactions._ import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{Logs, MilliSatoshi, MilliSatoshiLong, NodeParams, ToMilliSatoshiConversion, UInt64} +import fr.acinq.eclair.{BlockHeight, Logs, MilliSatoshi, MilliSatoshiLong, NodeParams, ToMilliSatoshiConversion, UInt64} import scodec.bits.ByteVector import scala.concurrent.{ExecutionContext, Future} @@ -92,7 +95,7 @@ object InteractiveTxBuilder { sealed trait Response case class SendMessage(sessionId: ByteVector32, msg: LightningMessage) extends Response - case class Succeeded(signingSession: InteractiveTxSigningSession.WaitingForSigs, commitSig: CommitSig, liquidityPurchase_opt: Option[LiquidityAds.Purchase]) extends Response + case class Succeeded(signingSession: InteractiveTxSigningSession.WaitingForSigs, commitSig: CommitSig, liquidityPurchase_opt: Option[LiquidityAds.Purchase], nextRemoteCommitNonce_opt: Option[(TxId, IndividualNonce)]) extends Response sealed trait Failed extends Response { def cause: ChannelException } case class LocalFailure(cause: ChannelException) extends Failed case class RemoteFailure(cause: ChannelException) extends Failed @@ -101,27 +104,31 @@ object InteractiveTxBuilder { case class RequireConfirmedInputs(forLocal: Boolean, forRemote: Boolean) /** An input that is already shared between participants (e.g. the current funding output when doing a splice). */ - sealed trait SharedFundingInput { - // @formatter:off - def info: InputInfo - def weight: Int - // @formatter:on - } - - case class Multisig2of2Input(info: InputInfo, fundingTxIndex: Long, remoteFundingPubkey: PublicKey) extends SharedFundingInput { - override val weight: Int = 388 - - def sign(keyManager: ChannelKeyManager, params: ChannelParams, tx: Transaction, spentUtxos: Map[OutPoint, TxOut]): ByteVector64 = { - val localFundingPubkey = keyManager.fundingPublicKey(params.localParams.fundingKeyPath, fundingTxIndex) - keyManager.sign(Transactions.SpliceTx(info, tx), localFundingPubkey, TxOwner.Local, params.commitmentFormat, spentUtxos) + case class SharedFundingInput(info: InputInfo, fundingTxIndex: Long, remoteFundingPubkey: PublicKey, commitmentFormat: CommitmentFormat) { + val weight: Int = commitmentFormat.fundingInputWeight + + def sign(channelId: ByteVector32, channelKeys: ChannelKeys, tx: Transaction, localNonce_opt: Option[LocalNonce], remoteNonce_opt: Option[IndividualNonce], spentUtxos: Map[OutPoint, TxOut]): Either[ChannelException, ChannelSpendSignature] = { + val localFundingKey = channelKeys.fundingKey(fundingTxIndex) + val spliceTx = Transactions.SpliceTx(info, tx) + commitmentFormat match { + case _: SegwitV0CommitmentFormat => Right(spliceTx.sign(localFundingKey, remoteFundingPubkey, spentUtxos)) + case _: SimpleTaprootChannelCommitmentFormat => (localNonce_opt, remoteNonce_opt) match { + case (Some(localNonce), Some(remoteNonce)) => spliceTx.partialSign(localFundingKey, remoteFundingPubkey, spentUtxos, localNonce, Seq(localNonce.publicNonce, remoteNonce)) match { + case Left(_) => Left(InvalidFundingNonce(channelId, tx.txid)) + case Right(sig) => Right(sig) + } + case _ => Left(MissingFundingNonce(channelId, tx.txid)) + } + } } } - object Multisig2of2Input { - def apply(commitment: Commitment): Multisig2of2Input = Multisig2of2Input( - info = commitment.commitInput, + object SharedFundingInput { + def apply(channelKeys: ChannelKeys, commitment: Commitment): SharedFundingInput = SharedFundingInput( + info = commitment.commitInput(channelKeys), fundingTxIndex = commitment.fundingTxIndex, - remoteFundingPubkey = commitment.remoteFundingPubKey + remoteFundingPubkey = commitment.remoteFundingPubKey, + commitmentFormat = commitment.commitmentFormat, ) } @@ -145,6 +152,7 @@ object InteractiveTxBuilder { sharedInput_opt: Option[SharedFundingInput], remoteFundingPubKey: PublicKey, localOutputs: List[TxOut], + commitmentFormat: CommitmentFormat, lockTime: Long, dustLimit: Satoshi, targetFeerate: FeeratePerKw, @@ -316,11 +324,11 @@ object InteractiveTxBuilder { remoteInputs: Seq[IncomingInput] = Nil, localOutputs: Seq[OutgoingOutput] = Nil, remoteOutputs: Seq[IncomingOutput] = Nil, - txCompleteSent: Boolean = false, - txCompleteReceived: Boolean = false, + txCompleteSent: Option[TxComplete] = None, + txCompleteReceived: Option[TxComplete] = None, inputsReceivedCount: Int = 0, outputsReceivedCount: Int = 0) { - val isComplete: Boolean = txCompleteSent && txCompleteReceived + val isComplete: Boolean = txCompleteSent.isDefined && txCompleteReceived.isDefined } /** Unsigned transaction created collaboratively. */ @@ -336,6 +344,8 @@ object InteractiveTxBuilder { val remoteFees: MilliSatoshi = remoteAmountIn - remoteAmountOut // Note that the truncation is a no-op: sub-satoshi balances are carried over from inputs to outputs and cancel out. val fees: Satoshi = (localFees + remoteFees).truncateToSatoshi + // Outputs spent by this transaction, in the order in which they appear in the transaction inputs. + val spentOutputs: Seq[TxOut] = (sharedInput_opt.toSeq ++ localInputs ++ remoteInputs).sortBy(_.serialId).map(_.txOut) // When signing transactions that include taproot inputs, we must provide details about all of the transaction's inputs. val inputDetails: Map[OutPoint, TxOut] = (sharedInput_opt.toSeq.map(i => i.outPoint -> i.txOut) ++ localInputs.map(i => i.outPoint -> i.txOut) ++ remoteInputs.map(i => i.outPoint -> i.txOut)).toMap @@ -388,6 +398,9 @@ object InteractiveTxBuilder { nodeParams: NodeParams, fundingParams: InteractiveTxParams, channelParams: ChannelParams, + localCommitParams: CommitParams, + remoteCommitParams: CommitParams, + channelKeys: ChannelKeys, purpose: Purpose, localPushAmount: MilliSatoshi, remotePushAmount: MilliSatoshi, @@ -424,7 +437,7 @@ object InteractiveTxBuilder { replyTo ! LocalFailure(InvalidLiquidityAdsPaymentType(channelParams.channelId, liquidityPurchase_opt.get.paymentDetails.paymentType, Set(LiquidityAds.PaymentType.FromChannelBalance, LiquidityAds.PaymentType.FromChannelBalanceForFutureHtlc))) Behaviors.stopped } else { - val actor = new InteractiveTxBuilder(replyTo, sessionId, nodeParams, channelParams, fundingParams, purpose, localPushAmount, remotePushAmount, liquidityPurchase_opt, wallet, stash, context) + val actor = new InteractiveTxBuilder(replyTo, sessionId, nodeParams, channelParams, localCommitParams, remoteCommitParams, channelKeys, fundingParams, purpose, localPushAmount, remotePushAmount, liquidityPurchase_opt, wallet, stash, context) actor.start() } case Abort => Behaviors.stopped @@ -443,6 +456,9 @@ private class InteractiveTxBuilder(replyTo: ActorRef[InteractiveTxBuilder.Respon sessionId: ByteVector32, nodeParams: NodeParams, channelParams: ChannelParams, + localCommitParams: CommitParams, + remoteCommitParams: CommitParams, + channelKeys: ChannelKeys, fundingParams: InteractiveTxBuilder.InteractiveTxParams, purpose: Purpose, localPushAmount: MilliSatoshi, @@ -455,15 +471,21 @@ private class InteractiveTxBuilder(replyTo: ActorRef[InteractiveTxBuilder.Respon import InteractiveTxBuilder._ private val log = context.log - private val keyManager = nodeParams.channelKeyManager - private val localFundingPubKey: PublicKey = keyManager.fundingPublicKey(channelParams.localParams.fundingKeyPath, purpose.fundingTxIndex).publicKey - private val fundingPubkeyScript: ByteVector = Script.write(Script.pay2wsh(Scripts.multiSig2of2(localFundingPubKey, fundingParams.remoteFundingPubKey))) + private val localFundingKey: PrivateKey = channelKeys.fundingKey(purpose.fundingTxIndex) + private val fundingPubkeyScript: ByteVector = Transactions.makeFundingScript(localFundingKey.publicKey, fundingParams.remoteFundingPubKey, fundingParams.commitmentFormat).pubkeyScript private val remoteNodeId = channelParams.remoteParams.nodeId private val previousTransactions: Seq[InteractiveTxBuilder.SignedSharedTransaction] = purpose match { case rbf: FundingTxRbf => rbf.previousTransactions case rbf: SpliceTxRbf => rbf.previousTransactions case _ => Nil } + // Nonce we will use to sign the shared input, if we are splicing a taproot channel. + private val localFundingNonce_opt: Option[LocalNonce] = fundingParams.sharedInput_opt.flatMap(sharedInput => sharedInput.commitmentFormat match { + case _: SegwitV0CommitmentFormat => None + case _: SimpleTaprootChannelCommitmentFormat => + val previousFundingKey = channelKeys.fundingKey(sharedInput.fundingTxIndex).publicKey + Some(NonceGenerator.signingNonce(previousFundingKey, sharedInput.remoteFundingPubkey, sharedInput.info.outPoint.txid)) + }) def start(): Behavior[Command] = { val txFunder = context.spawnAnonymous(InteractiveTxFunder(remoteNodeId, fundingParams, fundingPubkeyScript, purpose, wallet)) @@ -518,16 +540,39 @@ private class InteractiveTxBuilder(replyTo: ActorRef[InteractiveTxBuilder.Respon case i: Input.Shared => TxAddInput(fundingParams.channelId, i.serialId, i.outPoint, i.sequence) } replyTo ! SendMessage(sessionId, message) - val next = session.copy(toSend = tail, localInputs = session.localInputs :+ addInput, txCompleteSent = false) + val next = session.copy(toSend = tail, localInputs = session.localInputs :+ addInput, txCompleteSent = None) receive(next) case (addOutput: Output) +: tail => val message = TxAddOutput(fundingParams.channelId, addOutput.serialId, addOutput.amount, addOutput.pubkeyScript) replyTo ! SendMessage(sessionId, message) - val next = session.copy(toSend = tail, localOutputs = session.localOutputs :+ addOutput, txCompleteSent = false) + val next = session.copy(toSend = tail, localOutputs = session.localOutputs :+ addOutput, txCompleteSent = None) receive(next) case Nil => - replyTo ! SendMessage(sessionId, TxComplete(fundingParams.channelId)) - val next = session.copy(txCompleteSent = true) + val txComplete = fundingParams.commitmentFormat match { + case _: SegwitV0CommitmentFormat => TxComplete(fundingParams.channelId) + case _: SimpleTaprootChannelCommitmentFormat => + // We don't have more inputs or outputs to contribute to the shared transaction. + // If our peer doesn't have anything more to contribute either, we will proceed to exchange commitment + // signatures spending this shared transaction, so we need to provide nonces to create those signatures. + // If our peer adds more inputs or outputs, we will simply send a new tx_complete message in response with + // nonces for the updated shared transaction. + // Note that we don't validate the shared transaction at that point: this will be done later once we've + // both sent tx_complete. If the shared transaction is invalid, we will abort and discard our nonces. + val fundingTxId = Transaction( + version = 2, + txIn = (session.localInputs.map(i => i.serialId -> TxIn(i.outPoint, Nil, i.sequence)) ++ session.remoteInputs.map(i => i.serialId -> TxIn(i.outPoint, Nil, i.sequence))).sortBy(_._1).map(_._2), + txOut = (session.localOutputs.map(o => o.serialId -> TxOut(o.amount, o.pubkeyScript)) ++ session.remoteOutputs.map(o => o.serialId -> TxOut(o.amount, o.pubkeyScript))).sortBy(_._1).map(_._2), + lockTime = fundingParams.lockTime + ).txid + TxComplete( + channelId = fundingParams.channelId, + commitNonce = NonceGenerator.verificationNonce(fundingTxId, localFundingKey, fundingParams.remoteFundingPubKey, purpose.localCommitIndex).publicNonce, + nextCommitNonce = NonceGenerator.verificationNonce(fundingTxId, localFundingKey, fundingParams.remoteFundingPubKey, purpose.localCommitIndex + 1).publicNonce, + fundingNonce_opt = localFundingNonce_opt.map(_.publicNonce), + ) + } + replyTo ! SendMessage(sessionId, txComplete) + val next = session.copy(txCompleteSent = Some(txComplete)) if (next.isComplete) { validateAndSign(next) } else { @@ -543,30 +588,32 @@ private class InteractiveTxBuilder(replyTo: ActorRef[InteractiveTxBuilder.Respon if (session.remoteInputs.exists(_.serialId == addInput.serialId)) { return Left(DuplicateSerialId(fundingParams.channelId, addInput.serialId)) } - // We check whether this is the shared input or a remote input. - val input = addInput.previousTx_opt match { - case Some(previousTx) if previousTx.txOut.length <= addInput.previousTxOutput => - return Left(InputOutOfBounds(fundingParams.channelId, addInput.serialId, previousTx.txid, addInput.previousTxOutput)) - case Some(previousTx) if fundingParams.sharedInput_opt.exists(_.info.outPoint == OutPoint(previousTx, addInput.previousTxOutput.toInt)) => - return Left(InvalidSharedInput(fundingParams.channelId, addInput.serialId)) - case Some(previousTx) if !Script.isNativeWitnessScript(previousTx.txOut(addInput.previousTxOutput.toInt).publicKeyScript) => - return Left(NonSegwitInput(fundingParams.channelId, addInput.serialId, previousTx.txid, addInput.previousTxOutput)) - case Some(previousTx) => - Input.Remote(addInput.serialId, OutPoint(previousTx, addInput.previousTxOutput.toInt), previousTx.txOut(addInput.previousTxOutput.toInt), addInput.sequence) - case None => - (addInput.sharedInput_opt, fundingParams.sharedInput_opt) match { - case (Some(outPoint), Some(sharedInput)) if outPoint == sharedInput.info.outPoint => - Input.Shared(addInput.serialId, outPoint, sharedInput.info.txOut.publicKeyScript, addInput.sequence, purpose.previousLocalBalance, purpose.previousRemoteBalance, purpose.htlcBalance) - case _ => - return Left(PreviousTxMissing(fundingParams.channelId, addInput.serialId)) - } + // We check whether this is the shared input or a remote input, and validate input details if it is not the shared input. + // The remote input details are usually provided by sending the entire previous transaction. + // But when splicing a taproot channel, it is possible to only send the previous txOut (which saves bandwidth and + // allows spending transactions that are larger than 65kB) because the signature of the shared taproot input will + // commit to *every* txOut that is being spent, which protects against malleability issues. + // See https://delvingbitcoin.org/t/malleability-issues-when-creating-shared-transactions-with-segwit-v0/497 for more details. + val remoteInputInfo_opt = (addInput.previousTx_opt, addInput.previousTxOut_opt) match { + case (Some(previousTx), _) if previousTx.txOut.length <= addInput.previousTxOutput => return Left(InputOutOfBounds(fundingParams.channelId, addInput.serialId, previousTx.txid, addInput.previousTxOutput)) + case (Some(previousTx), _) => Some(InputInfo(OutPoint(previousTx, addInput.previousTxOutput.toInt), previousTx.txOut(addInput.previousTxOutput.toInt))) + case (None, Some(_)) if !fundingParams.sharedInput_opt.exists(_.commitmentFormat.isInstanceOf[TaprootCommitmentFormat]) => return Left(PreviousTxMissing(fundingParams.channelId, addInput.serialId)) + case (None, Some(inputInfo)) => Some(inputInfo) + case (None, None) => None + } + val input = remoteInputInfo_opt match { + case Some(input) if !Script.isNativeWitnessScript(input.txOut.publicKeyScript) => return Left(NonSegwitInput(fundingParams.channelId, addInput.serialId, input.outPoint.txid, addInput.previousTxOutput)) + case Some(input) if addInput.sequence > 0xfffffffdL => return Left(NonReplaceableInput(fundingParams.channelId, addInput.serialId, input.outPoint.txid, input.outPoint.index, addInput.sequence)) + case Some(input) if fundingParams.sharedInput_opt.exists(_.info.outPoint == input.outPoint) => return Left(InvalidSharedInput(fundingParams.channelId, addInput.serialId)) + case Some(input) => Input.Remote(addInput.serialId, input.outPoint, input.txOut, addInput.sequence) + case None => (addInput.sharedInput_opt, fundingParams.sharedInput_opt) match { + case (Some(outPoint), Some(sharedInput)) if outPoint == sharedInput.info.outPoint => Input.Shared(addInput.serialId, outPoint, sharedInput.info.txOut.publicKeyScript, addInput.sequence, purpose.previousLocalBalance, purpose.previousRemoteBalance, purpose.htlcBalance) + case _ => return Left(PreviousTxMissing(fundingParams.channelId, addInput.serialId)) + } } if (session.localInputs.exists(_.outPoint == input.outPoint) || session.remoteInputs.exists(_.outPoint == input.outPoint)) { return Left(DuplicateInput(fundingParams.channelId, addInput.serialId, input.outPoint.txid, input.outPoint.index)) } - if (input.sequence > 0xfffffffdL) { - return Left(NonReplaceableInput(fundingParams.channelId, addInput.serialId, input.outPoint.txid, input.outPoint.index, addInput.sequence)) - } Right(input) } @@ -603,7 +650,7 @@ private class InteractiveTxBuilder(replyTo: ActorRef[InteractiveTxBuilder.Respon val next = session.copy( remoteInputs = session.remoteInputs :+ input, inputsReceivedCount = session.inputsReceivedCount + 1, - txCompleteReceived = false, + txCompleteReceived = None, ) send(next) } @@ -616,7 +663,7 @@ private class InteractiveTxBuilder(replyTo: ActorRef[InteractiveTxBuilder.Respon val next = session.copy( remoteOutputs = session.remoteOutputs :+ output, outputsReceivedCount = session.outputsReceivedCount + 1, - txCompleteReceived = false, + txCompleteReceived = None, ) send(next) } @@ -625,7 +672,7 @@ private class InteractiveTxBuilder(replyTo: ActorRef[InteractiveTxBuilder.Respon case Some(_) => val next = session.copy( remoteInputs = session.remoteInputs.filterNot(_.serialId == removeInput.serialId), - txCompleteReceived = false, + txCompleteReceived = None, ) send(next) case None => @@ -637,15 +684,15 @@ private class InteractiveTxBuilder(replyTo: ActorRef[InteractiveTxBuilder.Respon case Some(_) => val next = session.copy( remoteOutputs = session.remoteOutputs.filterNot(_.serialId == removeOutput.serialId), - txCompleteReceived = false, + txCompleteReceived = None, ) send(next) case None => replyTo ! RemoteFailure(UnknownSerialId(fundingParams.channelId, removeOutput.serialId)) unlockAndStop(session) } - case _: TxComplete => - val next = session.copy(txCompleteReceived = true) + case txComplete: TxComplete => + val next = session.copy(txCompleteReceived = Some(txComplete)) if (next.isComplete) { validateAndSign(next) } else { @@ -675,7 +722,7 @@ private class InteractiveTxBuilder(replyTo: ActorRef[InteractiveTxBuilder.Respon replyTo ! RemoteFailure(cause) unlockAndStop(session) case Right(completeTx) => - signCommitTx(completeTx) + signCommitTx(completeTx, session.txCompleteReceived.flatMap(_.fundingNonce_opt), session.txCompleteReceived.flatMap(_.commitNonces_opt)) } case _: WalletFailure => replyTo ! RemoteFailure(UnconfirmedInteractiveTxInputs(fundingParams.channelId)) @@ -731,14 +778,14 @@ private class InteractiveTxBuilder(replyTo: ActorRef[InteractiveTxBuilder.Respon return Left(InvalidCompleteInteractiveTx(fundingParams.channelId)) } - val sharedInput_opt = fundingParams.sharedInput_opt.map(_ => { + val sharedInput_opt = fundingParams.sharedInput_opt.map(sharedInput => { if (fundingParams.remoteContribution >= 0.sat) { // If remote has a positive contribution, we do not check their post-splice reserve level, because they are improving // their situation, even if they stay below the requirement. Note that if local splices-in some funds in the same // operation, remote post-splice reserve may actually be worse than before, but that's not their fault. } else { // If remote removes funds from the channel, it must meet reserve requirements post-splice - val remoteReserve = channelParams.remoteChannelReserveForCapacity(fundingParams.fundingAmount, isSplice = true) + val remoteReserve = (fundingParams.fundingAmount / 100).max(fundingParams.dustLimit) if (sharedOutput.remoteAmount < remoteReserve) { log.warn("invalid interactive tx: peer takes too much funds out and falls below the channel reserve ({} < {})", sharedOutput.remoteAmount, remoteReserve) return Left(InvalidCompleteInteractiveTx(fundingParams.channelId)) @@ -748,6 +795,13 @@ private class InteractiveTxBuilder(replyTo: ActorRef[InteractiveTxBuilder.Respon log.warn("invalid interactive tx: shared input included multiple times") return Left(InvalidCompleteInteractiveTx(fundingParams.channelId)) } + sharedInput.commitmentFormat match { + case _: SegwitV0CommitmentFormat => () + case _: SimpleTaprootChannelCommitmentFormat => + // If we're spending a taproot channel, our peer must provide a nonce for the shared input. + val remoteFundingNonce_opt = session.txCompleteReceived.flatMap(_.fundingNonce_opt) + if (remoteFundingNonce_opt.isEmpty) return Left(MissingFundingNonce(fundingParams.channelId, sharedInput.info.outPoint.txid)) + } sharedInputs.headOption match { case Some(input) => input case None => @@ -763,6 +817,14 @@ private class InteractiveTxBuilder(replyTo: ActorRef[InteractiveTxBuilder.Respon return Left(InvalidCompleteInteractiveTx(fundingParams.channelId)) } + // If we're using taproot, our peer must provide commit nonces for the funding transaction. + fundingParams.commitmentFormat match { + case _: SegwitV0CommitmentFormat => () + case _: SimpleTaprootChannelCommitmentFormat => + val remoteCommitNonces_opt = session.txCompleteReceived.flatMap(_.commitNonces_opt) + if (remoteCommitNonces_opt.isEmpty) return Left(MissingCommitNonce(fundingParams.channelId, tx.txid, purpose.remoteCommitIndex)) + } + // The transaction isn't signed yet, and segwit witnesses can be arbitrarily low (e.g. when using an OP_1 script), // so we use empty witnesses to provide a lower bound on the transaction weight. if (tx.weight() > Transactions.MAX_STANDARD_TX_WEIGHT) { @@ -828,38 +890,58 @@ private class InteractiveTxBuilder(replyTo: ActorRef[InteractiveTxBuilder.Respon Right(sharedTx) } - private def signCommitTx(completeTx: SharedTransaction): Behavior[Command] = { + private def signCommitTx(completeTx: SharedTransaction, remoteFundingNonce_opt: Option[IndividualNonce], remoteCommitNonces_opt: Option[TxCompleteTlv.CommitNonces]): Behavior[Command] = { val fundingTx = completeTx.buildUnsignedTx() val fundingOutputIndex = fundingTx.txOut.indexWhere(_.publicKeyScript == fundingPubkeyScript) val liquidityFee = fundingParams.liquidityFees(liquidityPurchase_opt) - Funding.makeCommitTxs(keyManager, channelParams, + val localCommitmentKeys = LocalCommitmentKeys(channelParams, channelKeys, purpose.localCommitIndex) + val remoteCommitmentKeys = RemoteCommitmentKeys(channelParams, channelKeys, purpose.remotePerCommitmentPoint) + Funding.makeCommitTxs(channelParams, localCommitParams, remoteCommitParams, fundingAmount = fundingParams.fundingAmount, toLocal = completeTx.sharedOutput.localAmount - localPushAmount + remotePushAmount - liquidityFee, toRemote = completeTx.sharedOutput.remoteAmount - remotePushAmount + localPushAmount + liquidityFee, localHtlcs = purpose.localHtlcs, purpose.commitTxFeerate, + fundingParams.commitmentFormat, fundingTxIndex = purpose.fundingTxIndex, fundingTx.txid, fundingOutputIndex, - remotePerCommitmentPoint = purpose.remotePerCommitmentPoint, remoteFundingPubKey = fundingParams.remoteFundingPubKey, + localFundingKey, fundingParams.remoteFundingPubKey, + localCommitmentKeys, remoteCommitmentKeys, localCommitmentIndex = purpose.localCommitIndex, remoteCommitmentIndex = purpose.remoteCommitIndex) match { case Left(cause) => replyTo ! RemoteFailure(cause) unlockAndStop(completeTx) case Right((localSpec, localCommitTx, remoteSpec, remoteCommitTx, sortedHtlcTxs)) => require(fundingTx.txOut(fundingOutputIndex).publicKeyScript == localCommitTx.input.txOut.publicKeyScript, "pubkey script mismatch!") - val fundingPubKey = keyManager.fundingPublicKey(channelParams.localParams.fundingKeyPath, purpose.fundingTxIndex) - val localSigOfRemoteTx = keyManager.sign(remoteCommitTx, fundingPubKey, TxOwner.Remote, channelParams.channelFeatures.commitmentFormat, Map.empty) - val localPerCommitmentPoint = keyManager.htlcPoint(keyManager.keyPath(channelParams.localParams, channelParams.channelConfig)) - val htlcSignatures = sortedHtlcTxs.map(keyManager.sign(_, localPerCommitmentPoint, purpose.remotePerCommitmentPoint, TxOwner.Remote, channelParams.commitmentFormat, Map.empty)).toList - val localCommitSig = CommitSig(fundingParams.channelId, localSigOfRemoteTx, htlcSignatures) - val localCommit = UnsignedLocalCommit(purpose.localCommitIndex, localSpec, localCommitTx, htlcTxs = Nil) - val remoteCommit = RemoteCommit(purpose.remoteCommitIndex, remoteSpec, remoteCommitTx.tx.txid, purpose.remotePerCommitmentPoint) - signFundingTx(completeTx, localCommitSig, localCommit, remoteCommit) + val localSigOfRemoteTx = fundingParams.commitmentFormat match { + case _: SegwitV0CommitmentFormat => Right(remoteCommitTx.sign(localFundingKey, fundingParams.remoteFundingPubKey)) + case _: SimpleTaprootChannelCommitmentFormat => + remoteCommitNonces_opt match { + case Some(remoteNonces) => + val localNonce = NonceGenerator.signingNonce(localFundingKey.publicKey, fundingParams.remoteFundingPubKey, fundingTx.txid) + remoteCommitTx.partialSign(localFundingKey, fundingParams.remoteFundingPubKey, localNonce, Seq(localNonce.publicNonce, remoteNonces.commitNonce)) match { + case Left(_) => Left(InvalidCommitNonce(channelParams.channelId, fundingTx.txid, purpose.remoteCommitIndex)) + case Right(localSig) => Right(localSig) + } + case None => Left(MissingCommitNonce(fundingParams.channelId, fundingTx.txid, purpose.remoteCommitIndex)) + } + } + localSigOfRemoteTx match { + case Left(cause) => + replyTo ! RemoteFailure(cause) + unlockAndStop(completeTx) + case Right(localSigOfRemoteTx) => + val htlcSignatures = sortedHtlcTxs.map(_.localSig(remoteCommitmentKeys)).toList + val localCommitSig = CommitSig(fundingParams.channelId, localSigOfRemoteTx, htlcSignatures, batchSize = 1) + val localCommit = UnsignedLocalCommit(purpose.localCommitIndex, localSpec, localCommitTx.tx.txid) + val remoteCommit = RemoteCommit(purpose.remoteCommitIndex, remoteSpec, remoteCommitTx.tx.txid, purpose.remotePerCommitmentPoint) + signFundingTx(completeTx, remoteFundingNonce_opt, remoteCommitNonces_opt.map(_.nextCommitNonce), localCommitSig, localCommit, remoteCommit) + } } } - private def signFundingTx(completeTx: SharedTransaction, commitSig: CommitSig, localCommit: UnsignedLocalCommit, remoteCommit: RemoteCommit): Behavior[Command] = { - signTx(completeTx) + private def signFundingTx(completeTx: SharedTransaction, remoteFundingNonce_opt: Option[IndividualNonce], nextRemoteCommitNonce_opt: Option[IndividualNonce], commitSig: CommitSig, localCommit: UnsignedLocalCommit, remoteCommit: RemoteCommit): Behavior[Command] = { + signTx(completeTx, remoteFundingNonce_opt) Behaviors.receiveMessagePartial { case SignTransactionResult(signedTx) => log.info(s"interactive-tx txid=${signedTx.txId} partially signed with {} local inputs, {} remote inputs, {} local outputs and {} remote outputs", signedTx.tx.localInputs.length, signedTx.tx.remoteInputs.length, signedTx.tx.localOutputs.length, signedTx.tx.remoteOutputs.length) @@ -890,11 +972,13 @@ private class InteractiveTxBuilder(replyTo: ActorRef[InteractiveTxBuilder.Respon fundingParams, purpose.fundingTxIndex, signedTx, + localCommitParams, Left(localCommit), + remoteCommitParams, remoteCommit, liquidityPurchase_opt.map(_.basicInfo(isBuyer = fundingParams.isInitiator)) ) - replyTo ! Succeeded(signingSession, commitSig, liquidityPurchase_opt) + replyTo ! Succeeded(signingSession, commitSig, liquidityPurchase_opt, nextRemoteCommitNonce_opt.map(n => signedTx.txId -> n)) Behaviors.stopped case WalletFailure(t) => log.error("could not sign funding transaction: ", t) @@ -909,52 +993,57 @@ private class InteractiveTxBuilder(replyTo: ActorRef[InteractiveTxBuilder.Respon } } - private def signTx(unsignedTx: SharedTransaction): Unit = { + private def signTx(unsignedTx: SharedTransaction, remoteFundingNonce_opt: Option[IndividualNonce]): Unit = { import fr.acinq.bitcoin.scalacompat.KotlinUtils._ val tx = unsignedTx.buildUnsignedTx() - val sharedSig_opt = fundingParams.sharedInput_opt.collect { - case i: Multisig2of2Input => i.sign(keyManager, channelParams, tx, unsignedTx.inputDetails) + val sharedSig_opt = fundingParams.sharedInput_opt match { + case Some(i) => i.sign(fundingParams.channelId, channelKeys, tx, localFundingNonce_opt, remoteFundingNonce_opt, unsignedTx.inputDetails).map(sig => Some(sig)) + case None => Right(None) } - if (unsignedTx.localInputs.isEmpty) { - context.self ! SignTransactionResult(PartiallySignedSharedTransaction(unsignedTx, TxSignatures(fundingParams.channelId, tx, Nil, sharedSig_opt))) - } else { - // We track our wallet inputs and outputs, so we can verify them when we sign the transaction: if Eclair is managing bitcoin core wallet keys, it will - // only sign our wallet inputs, and check that it can re-compute private keys for our wallet outputs. - val ourWalletInputs = unsignedTx.localInputs.map(i => tx.txIn.indexWhere(_.outPoint == i.outPoint)) - val ourWalletOutputs = unsignedTx.localOutputs.flatMap { - case Output.Local.Change(_, amount, pubkeyScript) => Some(tx.txOut.indexWhere(output => output.amount == amount && output.publicKeyScript == pubkeyScript)) - // Non-change outputs may go to an external address (typically during a splice-out). - // Here we only keep outputs which are ours i.e explicitly go back into our wallet. - // We trust that non-change outputs are valid: this only works if the entry point for creating such outputs is trusted (for example, a secure API call). - case _: Output.Local.NonChange => None - } - // If this is a splice, the PSBT we create must contain the shared input, because if we use taproot wallet inputs - // we need information about *all* of the transaction's inputs, not just the one we're signing. - val psbt = unsignedTx.sharedInput_opt.flatMap { - si => new Psbt(tx).updateWitnessInput(si.outPoint, si.txOut, null, null, null, java.util.Map.of(), null, null, java.util.Map.of()).toOption - }.getOrElse(new Psbt(tx)) - context.pipeToSelf(wallet.signPsbt(psbt, ourWalletInputs, ourWalletOutputs).map { - response => - val localOutpoints = unsignedTx.localInputs.map(_.outPoint).toSet - val partiallySignedTx = response.partiallySignedTx - // Partially signed PSBT must include spent amounts for all inputs that were signed, and we can "trust" these amounts because they are included - // in the hash that we signed (see BIP143). If our bitcoin node lied about them, then our signatures are invalid. - val actualLocalAmountIn = ourWalletInputs.map(i => kmp2scala(response.psbt.getInput(i).getWitnessUtxo.amount)).sum - val expectedLocalAmountIn = unsignedTx.localInputs.map(i => i.txOut.amount).sum - require(actualLocalAmountIn == expectedLocalAmountIn, s"local spent amount $actualLocalAmountIn does not match what we expect ($expectedLocalAmountIn): bitcoin core may be malicious") - val actualLocalAmountOut = ourWalletOutputs.map(i => partiallySignedTx.txOut(i).amount).sum - val expectedLocalAmountOut = unsignedTx.localOutputs.map { - case c: Output.Local.Change => c.amount - case _: Output.Local.NonChange => 0.sat - }.sum - require(actualLocalAmountOut == expectedLocalAmountOut, s"local output amount $actualLocalAmountOut does not match what we expect ($expectedLocalAmountOut): bitcoin core may be malicious") - val sigs = partiallySignedTx.txIn.filter(txIn => localOutpoints.contains(txIn.outPoint)).map(_.witness) - PartiallySignedSharedTransaction(unsignedTx, TxSignatures(fundingParams.channelId, partiallySignedTx, sigs, sharedSig_opt)) - }) { - case Failure(t) => WalletFailure(t) - case Success(signedTx) => SignTransactionResult(signedTx) - } + sharedSig_opt match { + case Left(f) => + context.self ! WalletFailure(f) + case Right(sharedSig_opt) if unsignedTx.localInputs.isEmpty => + context.self ! SignTransactionResult(PartiallySignedSharedTransaction(unsignedTx, TxSignatures(fundingParams.channelId, tx, Nil, sharedSig_opt))) + case Right(sharedSig_opt) => + // We track our wallet inputs and outputs, so we can verify them when we sign the transaction: if Eclair is managing bitcoin core wallet keys, it will + // only sign our wallet inputs, and check that it can re-compute private keys for our wallet outputs. + val ourWalletInputs = unsignedTx.localInputs.map(i => tx.txIn.indexWhere(_.outPoint == i.outPoint)) + val ourWalletOutputs = unsignedTx.localOutputs.flatMap { + case Output.Local.Change(_, amount, pubkeyScript) => Some(tx.txOut.indexWhere(output => output.amount == amount && output.publicKeyScript == pubkeyScript)) + // Non-change outputs may go to an external address (typically during a splice-out). + // Here we only keep outputs which are ours i.e explicitly go back into our wallet. + // We trust that non-change outputs are valid: this only works if the entry point for creating such outputs is trusted (for example, a secure API call). + case _: Output.Local.NonChange => None + } + // We must include all remote inputs in the PSBT we create, because if we use taproot wallet inputs we need + // information about *all* of the transaction's inputs, not just the one we're signing. If this is a splice, + // we also need to include the shared input, for the same reason. + val psbt = (unsignedTx.remoteInputs ++ unsignedTx.sharedInput_opt.toSeq).foldLeft(new Psbt(tx)) { + case (psbt, input) => psbt.updateWitnessInput(input.outPoint, input.txOut, null, null, null, java.util.Map.of(), null, null, java.util.Map.of()).toOption.getOrElse(psbt) + } + context.pipeToSelf(wallet.signPsbt(psbt, ourWalletInputs, ourWalletOutputs).map { + response => + val localOutpoints = unsignedTx.localInputs.map(_.outPoint).toSet + val partiallySignedTx = response.partiallySignedTx + // Partially signed PSBT must include spent amounts for all inputs that were signed, and we can "trust" these amounts because they are included + // in the hash that we signed (see BIP143). If our bitcoin node lied about them, then our signatures are invalid. + val actualLocalAmountIn = ourWalletInputs.map(i => kmp2scala(response.psbt.getInput(i).getWitnessUtxo.amount)).sum + val expectedLocalAmountIn = unsignedTx.localInputs.map(i => i.txOut.amount).sum + require(actualLocalAmountIn == expectedLocalAmountIn, s"local spent amount $actualLocalAmountIn does not match what we expect ($expectedLocalAmountIn): bitcoin core may be malicious") + val actualLocalAmountOut = ourWalletOutputs.map(i => partiallySignedTx.txOut(i).amount).sum + val expectedLocalAmountOut = unsignedTx.localOutputs.map { + case c: Output.Local.Change => c.amount + case _: Output.Local.NonChange => 0.sat + }.sum + require(actualLocalAmountOut == expectedLocalAmountOut, s"local output amount $actualLocalAmountOut does not match what we expect ($expectedLocalAmountOut): bitcoin core may be malicious") + val sigs = partiallySignedTx.txIn.filter(txIn => localOutpoints.contains(txIn.outPoint)).map(_.witness) + PartiallySignedSharedTransaction(unsignedTx, TxSignatures(fundingParams.channelId, partiallySignedTx, sigs, sharedSig_opt)) + }) { + case Failure(t) => WalletFailure(t) + case Success(signedTx) => SignTransactionResult(signedTx) + } } } @@ -1013,7 +1102,7 @@ object InteractiveTxSigningSession { // +-------+ +-------+ /** A local commitment for which we haven't received our peer's signatures. */ - case class UnsignedLocalCommit(index: Long, spec: CommitmentSpec, commitTx: CommitTx, htlcTxs: List[HtlcTx]) + case class UnsignedLocalCommit(index: Long, spec: CommitmentSpec, txId: TxId) private def shouldSignFirst(isInitiator: Boolean, channelParams: ChannelParams, tx: SharedTransaction): Boolean = { val sharedAmountIn = tx.sharedInput_opt.map(_.txOut.amount).getOrElse(0 sat) @@ -1031,7 +1120,7 @@ object InteractiveTxSigningSession { } } - def addRemoteSigs(keyManager: ChannelKeyManager, params: ChannelParams, fundingParams: InteractiveTxParams, partiallySignedTx: PartiallySignedSharedTransaction, remoteSigs: TxSignatures)(implicit log: LoggingAdapter): Either[ChannelException, FullySignedSharedTransaction] = { + def addRemoteSigs(channelKeys: ChannelKeys, fundingParams: InteractiveTxParams, partiallySignedTx: PartiallySignedSharedTransaction, remoteSigs: TxSignatures)(implicit log: LoggingAdapter): Either[ChannelException, FullySignedSharedTransaction] = { if (partiallySignedTx.tx.localInputs.length != partiallySignedTx.localSigs.witnesses.length) { return Left(InvalidFundingSignature(fundingParams.channelId, Some(partiallySignedTx.txId))) } @@ -1039,18 +1128,26 @@ object InteractiveTxSigningSession { log.info("invalid tx_signatures: witness count mismatch (expected={}, got={})", partiallySignedTx.tx.remoteInputs.length, remoteSigs.witnesses.length) return Left(InvalidFundingSignature(fundingParams.channelId, Some(partiallySignedTx.txId))) } - val sharedSigs_opt = fundingParams.sharedInput_opt match { - case Some(sharedInput: Multisig2of2Input) => - (partiallySignedTx.localSigs.previousFundingTxSig_opt, remoteSigs.previousFundingTxSig_opt) match { - case (Some(localSig), Some(remoteSig)) => - val localFundingPubkey = keyManager.fundingPublicKey(params.localParams.fundingKeyPath, sharedInput.fundingTxIndex).publicKey - Some(Scripts.witness2of2(localSig, remoteSig, localFundingPubkey, sharedInput.remoteFundingPubkey)) - case _ => - log.info("invalid tx_signatures: missing shared input signatures") - return Left(InvalidFundingSignature(fundingParams.channelId, Some(partiallySignedTx.txId))) - } - case None => None - } + val sharedSigs_opt = fundingParams.sharedInput_opt.map(sharedInput => { + val localFundingPubkey = channelKeys.fundingKey(sharedInput.fundingTxIndex).publicKey + val spliceTx = Transactions.SpliceTx(sharedInput.info, partiallySignedTx.tx.buildUnsignedTx()) + val signedTx_opt = sharedInput.commitmentFormat match { + case _: SegwitV0CommitmentFormat => + (partiallySignedTx.localSigs.previousFundingTxSig_opt, remoteSigs.previousFundingTxSig_opt) match { + case (Some(localSig), Some(remoteSig)) => Right(spliceTx.aggregateSigs(localFundingPubkey, sharedInput.remoteFundingPubkey, IndividualSignature(localSig), IndividualSignature(remoteSig))) + case _ => Left(InvalidFundingSignature(fundingParams.channelId, Some(partiallySignedTx.txId))) + } + case _: SimpleTaprootChannelCommitmentFormat => + (partiallySignedTx.localSigs.previousFundingTxPartialSig_opt, remoteSigs.previousFundingTxPartialSig_opt) match { + case (Some(localSig), Some(remoteSig)) => spliceTx.aggregateSigs(localFundingPubkey, sharedInput.remoteFundingPubkey, localSig, remoteSig, partiallySignedTx.tx.inputDetails) + case _ => Left(InvalidFundingSignature(fundingParams.channelId, Some(partiallySignedTx.txId))) + } + } + signedTx_opt match { + case Left(_) => return Left(InvalidFundingSignature(fundingParams.channelId, Some(partiallySignedTx.txId))) + case Right(signedTx) => signedTx.txIn(spliceTx.inputIndex).witness + } + }) val txWithSigs = FullySignedSharedTransaction(partiallySignedTx.tx, partiallySignedTx.localSigs, remoteSigs, sharedSigs_opt) if (remoteSigs.txId != txWithSigs.signedTx.txid) { log.info("invalid tx_signatures: txId mismatch (expected={}, got={})", txWithSigs.signedTx.txid, remoteSigs.txId) @@ -1085,10 +1182,12 @@ object InteractiveTxSigningSession { case class WaitingForSigs(fundingParams: InteractiveTxParams, fundingTxIndex: Long, fundingTx: PartiallySignedSharedTransaction, + localCommitParams: CommitParams, localCommit: Either[UnsignedLocalCommit, LocalCommit], + remoteCommitParams: CommitParams, remoteCommit: RemoteCommit, liquidityPurchase_opt: Option[LiquidityAds.PurchaseBasicInfo]) extends InteractiveTxSigningSession { - val commitInput: InputInfo = localCommit.fold(_.commitTx.input, _.commitTxAndRemoteSig.commitTx.input) + val fundingTxId: TxId = fundingTx.txId val localCommitIndex: Long = localCommit.fold(_.index, _.index) // This value tells our peer whether we need them to retransmit their commit_sig on reconnection or not. val nextLocalCommitmentNumber: Long = localCommit match { @@ -1096,15 +1195,35 @@ object InteractiveTxSigningSession { case Right(commit) => commit.index + 1 } - def receiveCommitSig(nodeParams: NodeParams, channelParams: ChannelParams, remoteCommitSig: CommitSig)(implicit log: LoggingAdapter): Either[ChannelException, InteractiveTxSigningSession] = { + def localFundingKey(channelKeys: ChannelKeys): PrivateKey = channelKeys.fundingKey(fundingTxIndex) + + def commitInput(fundingKey: PrivateKey): InputInfo = { + val fundingScript = Transactions.makeFundingScript(fundingKey.publicKey, fundingParams.remoteFundingPubKey, fundingParams.commitmentFormat).pubkeyScript + val fundingOutput = OutPoint(fundingTxId, fundingTx.tx.buildUnsignedTx().txOut.indexWhere(txOut => txOut.amount == fundingParams.fundingAmount && txOut.publicKeyScript == fundingScript)) + InputInfo(fundingOutput, TxOut(fundingParams.fundingAmount, fundingScript)) + } + + def commitInput(channelKeys: ChannelKeys): InputInfo = commitInput(localFundingKey(channelKeys)) + + /** Nonce for the current commitment, which our peer will need if they must re-send their commit_sig for our current commitment transaction. */ + def currentCommitNonce_opt(channelKeys: ChannelKeys): Option[LocalNonce] = localCommit match { + case Left(_) => Some(NonceGenerator.verificationNonce(fundingTxId, localFundingKey(channelKeys), fundingParams.remoteFundingPubKey, localCommitIndex)) + case Right(_) => None + } + + /** Nonce for the next commitment, which our peer will need to sign our next commitment transaction. */ + def nextCommitNonce(channelKeys: ChannelKeys): LocalNonce = NonceGenerator.verificationNonce(fundingTxId, localFundingKey(channelKeys), fundingParams.remoteFundingPubKey, localCommitIndex + 1) + + def receiveCommitSig(channelParams: ChannelParams, channelKeys: ChannelKeys, remoteCommitSig: CommitSig, currentBlockHeight: BlockHeight)(implicit log: LoggingAdapter): Either[ChannelException, InteractiveTxSigningSession] = { localCommit match { case Left(unsignedLocalCommit) => - val channelKeyPath = nodeParams.channelKeyManager.keyPath(channelParams.localParams, channelParams.channelConfig) - val localPerCommitmentPoint = nodeParams.channelKeyManager.commitmentPoint(channelKeyPath, localCommitIndex) - LocalCommit.fromCommitSig(nodeParams.channelKeyManager, channelParams, fundingTx.txId, fundingTxIndex, fundingParams.remoteFundingPubKey, commitInput, remoteCommitSig, localCommitIndex, unsignedLocalCommit.spec, localPerCommitmentPoint).map { signedLocalCommit => + val fundingKey = localFundingKey(channelKeys) + val commitKeys = LocalCommitmentKeys(channelParams, channelKeys, localCommitIndex) + val fundingOutput = commitInput(fundingKey) + LocalCommit.fromCommitSig(channelParams, localCommitParams, commitKeys, fundingTx.txId, fundingKey, fundingParams.remoteFundingPubKey, fundingOutput, remoteCommitSig, localCommitIndex, unsignedLocalCommit.spec, fundingParams.commitmentFormat).map { signedLocalCommit => if (shouldSignFirst(fundingParams.isInitiator, channelParams, fundingTx.tx)) { - val fundingStatus = LocalFundingStatus.DualFundedUnconfirmedFundingTx(fundingTx, nodeParams.currentBlockHeight, fundingParams, liquidityPurchase_opt) - val commitment = Commitment(fundingTxIndex, remoteCommit.index, fundingParams.remoteFundingPubKey, fundingStatus, RemoteFundingStatus.NotLocked, signedLocalCommit, remoteCommit, None) + val fundingStatus = LocalFundingStatus.DualFundedUnconfirmedFundingTx(fundingTx, currentBlockHeight, fundingParams, liquidityPurchase_opt) + val commitment = Commitment(fundingTxIndex, remoteCommit.index, fundingOutput.outPoint, fundingParams.fundingAmount, fundingParams.remoteFundingPubKey, fundingStatus, RemoteFundingStatus.NotLocked, fundingParams.commitmentFormat, localCommitParams, signedLocalCommit, remoteCommitParams, remoteCommit, None) SendingSigs(fundingStatus, commitment, fundingTx.localSigs) } else { this.copy(localCommit = Right(signedLocalCommit)) @@ -1116,20 +1235,21 @@ object InteractiveTxSigningSession { } } - def receiveTxSigs(nodeParams: NodeParams, channelParams: ChannelParams, remoteTxSigs: TxSignatures)(implicit log: LoggingAdapter): Either[ChannelException, SendingSigs] = { + def receiveTxSigs(channelKeys: ChannelKeys, remoteTxSigs: TxSignatures, currentBlockHeight: BlockHeight)(implicit log: LoggingAdapter): Either[ChannelException, SendingSigs] = { localCommit match { case Left(_) => log.info("received tx_signatures before commit_sig") Left(UnexpectedFundingSignatures(fundingParams.channelId)) case Right(signedLocalCommit) => - addRemoteSigs(nodeParams.channelKeyManager, channelParams, fundingParams, fundingTx, remoteTxSigs) match { + addRemoteSigs(channelKeys, fundingParams, fundingTx, remoteTxSigs) match { case Left(f) => log.info("received invalid tx_signatures") Left(f) case Right(fullySignedTx) => log.info("interactive-tx fully signed with {} local inputs, {} remote inputs, {} local outputs and {} remote outputs", fullySignedTx.tx.localInputs.length, fullySignedTx.tx.remoteInputs.length, fullySignedTx.tx.localOutputs.length, fullySignedTx.tx.remoteOutputs.length) - val fundingStatus = LocalFundingStatus.DualFundedUnconfirmedFundingTx(fullySignedTx, nodeParams.currentBlockHeight, fundingParams, liquidityPurchase_opt) - val commitment = Commitment(fundingTxIndex, remoteCommit.index, fundingParams.remoteFundingPubKey, fundingStatus, RemoteFundingStatus.NotLocked, signedLocalCommit, remoteCommit, None) + val fundingOutput = commitInput(channelKeys) + val fundingStatus = LocalFundingStatus.DualFundedUnconfirmedFundingTx(fullySignedTx, currentBlockHeight, fundingParams, liquidityPurchase_opt) + val commitment = Commitment(fundingTxIndex, remoteCommit.index, fundingOutput.outPoint, fundingParams.fundingAmount, fundingParams.remoteFundingPubKey, fundingStatus, RemoteFundingStatus.NotLocked, fundingParams.commitmentFormat, localCommitParams, signedLocalCommit, remoteCommitParams, remoteCommit, None) Right(SendingSigs(fundingStatus, commitment, fullySignedTx.localSigs)) } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fund/InteractiveTxFunder.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fund/InteractiveTxFunder.scala index f5b22fd1de..d6935f0368 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fund/InteractiveTxFunder.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fund/InteractiveTxFunder.scala @@ -19,7 +19,7 @@ package fr.acinq.eclair.channel.fund import akka.actor.typed.scaladsl.{ActorContext, Behaviors} import akka.actor.typed.{ActorRef, Behavior} import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey -import fr.acinq.bitcoin.scalacompat.{KotlinUtils, OutPoint, Satoshi, SatoshiLong, Script, ScriptWitness, Transaction, TxIn, TxOut} +import fr.acinq.bitcoin.scalacompat.{OutPoint, Satoshi, SatoshiLong, Script, ScriptWitness, Transaction, TxIn, TxOut} import fr.acinq.eclair.blockchain.OnChainChannelFunder import fr.acinq.eclair.blockchain.fee.FeeratePerKw import fr.acinq.eclair.channel.fund.InteractiveTxBuilder._ @@ -75,7 +75,7 @@ object InteractiveTxFunder { */ def computeSpliceContribution(isInitiator: Boolean, sharedInput: SharedFundingInput, spliceInAmount: Satoshi, spliceOut: Seq[TxOut], targetFeerate: FeeratePerKw): Satoshi = { val fees = if (spliceInAmount == 0.sat) { - val spliceOutputsWeight = spliceOut.map(KotlinUtils.scala2kmp).map(_.weight()).sum + val spliceOutputsWeight = spliceOut.map(_.weight()).sum val weight = if (isInitiator) { // The initiator must add the shared input, the shared output and pay for the fees of the common transaction fields. val dummyTx = Transaction(2, Nil, Seq(sharedInput.info.txOut), 0) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/FinalTxPublisher.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/FinalTxPublisher.scala index 38da6d9ae5..2bdee8b37b 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/FinalTxPublisher.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/FinalTxPublisher.scala @@ -85,7 +85,7 @@ private class FinalTxPublisher(nodeParams: NodeParams, } } - def checkParentPublished(): Behavior[Command] = { + private def checkParentPublished(): Behavior[Command] = { cmd.parentTx_opt match { case Some(parentTxId) => context.self ! CheckParentTx diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/MempoolTxMonitor.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/MempoolTxMonitor.scala index cf380922d5..0470229637 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/MempoolTxMonitor.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/MempoolTxMonitor.scala @@ -134,7 +134,7 @@ private class MempoolTxMonitor(nodeParams: NodeParams, } } - def waitForConfirmation(): Behavior[Command] = { + private def waitForConfirmation(): Behavior[Command] = { context.system.eventStream ! EventStream.Subscribe(context.messageAdapter[CurrentBlockHeight](cbc => WrappedCurrentBlockHeight(cbc.blockHeight))) context.system.eventStream ! EventStream.Publish(TransactionPublished(txPublishContext.channelId_opt.getOrElse(ByteVector32.Zeroes), txPublishContext.remoteNodeId, cmd.tx, cmd.fee, cmd.desc)) Behaviors.receiveMessagePartial { @@ -192,7 +192,7 @@ private class MempoolTxMonitor(nodeParams: NodeParams, } } - def sendFinalResult(result: FinalTxResult): Behavior[Command] = { + private def sendFinalResult(result: FinalTxResult): Behavior[Command] = { cmd.replyTo ! result Behaviors.stopped } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/ReplaceableTxFunder.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/ReplaceableTxFunder.scala index 10301aaa8a..8d7acc3e8e 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/ReplaceableTxFunder.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/ReplaceableTxFunder.scala @@ -20,12 +20,11 @@ import akka.actor.typed.eventstream.EventStream import akka.actor.typed.scaladsl.{ActorContext, Behaviors} import akka.actor.typed.{ActorRef, Behavior} import fr.acinq.bitcoin.psbt.Psbt -import fr.acinq.bitcoin.scalacompat.{ByteVector32, OutPoint, Satoshi, Transaction, TxOut} +import fr.acinq.bitcoin.scalacompat.{Satoshi, SatoshiLong, Transaction, TxIn, TxOut} import fr.acinq.eclair.NotificationsLogger.NotifyNodeOperator import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinCoreClient import fr.acinq.eclair.blockchain.fee.{FeeratePerKw, FeeratesPerKw, OnChainFeeConf} import fr.acinq.eclair.channel.FullCommitment -import fr.acinq.eclair.channel.publish.ReplaceableTxPrePublisher._ import fr.acinq.eclair.channel.publish.TxPublisher.TxPublishContext import fr.acinq.eclair.transactions.Transactions import fr.acinq.eclair.transactions.Transactions._ @@ -47,20 +46,18 @@ object ReplaceableTxFunder { // @formatter:off sealed trait Command - case class FundTransaction(replyTo: ActorRef[FundingResult], cmd: TxPublisher.PublishReplaceableTx, tx: Either[FundedTx, ReplaceableTxWithWitnessData], targetFeerate: FeeratePerKw) extends Command + case class FundTransaction(replyTo: ActorRef[FundingResult], txInfo: Either[FundedTx, ForceCloseTransaction], commitTx: Transaction, commitment: FullCommitment, targetFeerate: FeeratePerKw) extends Command - private case class AddInputsOk(tx: ReplaceableTxWithWitnessData, totalAmountIn: Satoshi, walletUtxos: Map[OutPoint, TxOut]) extends Command + private case class AddInputsOk(walletInputs: WalletInputs) extends Command private case class AddInputsFailed(reason: Throwable) extends Command private case class SignWalletInputsOk(signedTx: Transaction) extends Command private case class SignWalletInputsFailed(reason: Throwable) extends Command private case object UtxosUnlocked extends Command // @formatter:on - case class FundedTx(signedTxWithWitnessData: ReplaceableTxWithWitnessData, totalAmountIn: Satoshi, feerate: FeeratePerKw, walletInputs: Map[OutPoint, TxOut]) { - require(signedTxWithWitnessData.txInfo.tx.txIn.nonEmpty, "funded transaction must have inputs") - require(signedTxWithWitnessData.txInfo.tx.txOut.nonEmpty, "funded transaction must have outputs") - val signedTx: Transaction = signedTxWithWitnessData.txInfo.tx - val fee: Satoshi = totalAmountIn - signedTx.txOut.map(_.amount).sum + case class FundedTx(txInfo: ForceCloseTransaction, walletInputs_opt: Option[WalletInputs], signedTx: Transaction, feerate: FeeratePerKw) { + val totalAmountIn: Satoshi = txInfo.amountIn + walletInputs_opt.map(_.amountIn).getOrElse(0 sat) + val fee: Satoshi = txInfo.fee + walletInputs_opt.map(_.fee).getOrElse(0 sat) } // @formatter:off @@ -73,11 +70,12 @@ object ReplaceableTxFunder { Behaviors.setup { context => Behaviors.withMdc(txPublishContext.mdc()) { Behaviors.receiveMessagePartial { - case FundTransaction(replyTo, cmd, tx, requestedFeerate) => - val targetFeerate = requestedFeerate.min(maxFeerate(cmd.txInfo, cmd.commitment, cmd.commitTx, nodeParams.currentBitcoinCoreFeerates, nodeParams.onChainFeeConf)) - val txFunder = new ReplaceableTxFunder(nodeParams, replyTo, cmd, bitcoinClient, context) - tx match { - case Right(txWithWitnessData) => txFunder.fund(txWithWitnessData, targetFeerate) + case cmd: FundTransaction => + val txInfo = cmd.txInfo.fold(fundedTx => fundedTx.txInfo, tx => tx) + val targetFeerate = cmd.targetFeerate.min(maxFeerate(txInfo, cmd.commitTx, cmd.commitment, nodeParams.currentBitcoinCoreFeerates, nodeParams.onChainFeeConf)) + val txFunder = new ReplaceableTxFunder(cmd.replyTo, cmd.commitTx, cmd.commitment, bitcoinClient, context) + cmd.txInfo match { + case Right(txInfo) => txFunder.fund(txInfo, targetFeerate) case Left(previousTx) => txFunder.bump(previousTx, targetFeerate) } } @@ -89,306 +87,223 @@ object ReplaceableTxFunder { * The on-chain feerate can be arbitrarily high, but it wouldn't make sense to pay more fees than the amount we're * trying to claim on-chain. We compute how much funds we have at risk and the feerate that matches this amount. */ - def maxFeerate(txInfo: ReplaceableTransactionWithInputInfo, commitment: FullCommitment, commitTx: Transaction, currentFeerates: FeeratesPerKw, feeConf: OnChainFeeConf): FeeratePerKw = { - // We don't want to pay more in fees than the amount at risk in untrimmed pending HTLCs. - val maxFee = txInfo match { - case tx: HtlcTx => tx.input.txOut.amount - case tx: ClaimHtlcTx => tx.input.txOut.amount - case _: ClaimLocalAnchorOutputTx => - val htlcBalance = commitment.localCommit.htlcTxsAndRemoteSigs.map(_.htlcTx.input.txOut.amount).sum + def maxFeerate(tx: ForceCloseTransaction, commitTx: Transaction, commitment: FullCommitment, currentFeerates: FeeratesPerKw, feeConf: OnChainFeeConf): FeeratePerKw = { + // We don't want to pay more in fees than the amount at risk in pending HTLCs. + val maxFee = tx match { + case _: ClaimAnchorTx => + val htlcBalance = commitment.localCommit.spec.htlcs.map(_.add.amountMsat).sum.truncateToSatoshi val mainBalance = commitment.localCommit.spec.toLocal.truncateToSatoshi // If there are no HTLCs or a low HTLC amount, we still want to get back our main balance. // In that case, we spend at most 5% of our balance in fees, with a hard cap configured by the node operator. val mainBalanceFee = (mainBalance * 5 / 100).min(feeConf.anchorWithoutHtlcsMaxFee) htlcBalance.max(mainBalanceFee) + case _ => tx.amountIn } // We cannot know beforehand how many wallet inputs will be added, but an estimation should be good enough. - val weight = txInfo match { - // For HTLC transactions, we add a p2wpkh input and a p2wpkh change output. - case _: HtlcSuccessTx => commitment.params.commitmentFormat.htlcSuccessWeight + Transactions.claimP2WPKHOutputWeight - case _: HtlcTimeoutTx => commitment.params.commitmentFormat.htlcTimeoutWeight + Transactions.claimP2WPKHOutputWeight - case _: ClaimHtlcSuccessTx => Transactions.claimHtlcSuccessWeight - case _: LegacyClaimHtlcSuccessTx => Transactions.claimHtlcSuccessWeight - case _: ClaimHtlcTimeoutTx => Transactions.claimHtlcTimeoutWeight - case _: ClaimLocalAnchorOutputTx => commitTx.weight() + Transactions.claimAnchorOutputMinWeight + val weight = tx match { + // When claiming our anchor output, it must pay for the weight of the commitment transaction. + // We usually add a wallet input and a change output. + case tx: ClaimAnchorTx => commitTx.weight() + tx.expectedWeight + Transactions.maxWalletInputWeight + Transactions.maxWalletOutputWeight + // For HTLC transactions, we usually add a wallet input and a change output. + case tx: SignedHtlcTx => tx.expectedWeight + Transactions.maxWalletInputWeight + Transactions.maxWalletOutputWeight + // Other transactions don't use any additional inputs or outputs. + case tx => tx.expectedWeight } - // It doesn't make sense to use a feerate that is much higher than the current feerate for inclusion into the next block. + // It doesn't make sense to use a feerate that is much higher than the current feerate for inclusion into the next block, + // so we restrict the weight-based feerate obtained. Transactions.fee2rate(maxFee, weight).min(currentFeerates.fastest * 1.25) } - /** - * Adjust the main output of a claim-htlc tx to match our target feerate. - * If the resulting output is too small, we skip the transaction. - */ - def adjustClaimHtlcTxOutput(claimHtlcTx: ClaimHtlcWithWitnessData, targetFeerate: FeeratePerKw, dustLimit: Satoshi): Either[TxGenerationSkipped, ClaimHtlcWithWitnessData] = { - require(claimHtlcTx.txInfo.tx.txIn.size == 1, "claim-htlc transaction should have a single input") - require(claimHtlcTx.txInfo.tx.txOut.size == 1, "claim-htlc transaction should have a single output") - val dummySignedTx = claimHtlcTx.txInfo match { - case tx: ClaimHtlcSuccessTx => addSigs(tx, PlaceHolderSig, ByteVector32.Zeroes) - case tx: ClaimHtlcTimeoutTx => addSigs(tx, PlaceHolderSig) - case tx: LegacyClaimHtlcSuccessTx => tx - } - val targetFee = weight2fee(targetFeerate, dummySignedTx.tx.weight()) - val outputAmount = claimHtlcTx.txInfo.amountIn - targetFee - if (outputAmount < dustLimit) { - Left(AmountBelowDustLimit) - } else { - val updatedClaimHtlcTx = claimHtlcTx match { - // NB: we don't modify legacy claim-htlc-success, it's already signed. - case legacyClaimHtlcSuccess: LegacyClaimHtlcSuccessWithWitnessData => legacyClaimHtlcSuccess - case _ => claimHtlcTx.updateTx(claimHtlcTx.txInfo.tx.copy(txOut = Seq(claimHtlcTx.txInfo.tx.txOut.head.copy(amount = outputAmount)))) - } - Right(updatedClaimHtlcTx) - } - } - - // @formatter:off - sealed trait AdjustPreviousTxOutputResult - object AdjustPreviousTxOutputResult { - case class Skip(reason: String) extends AdjustPreviousTxOutputResult - case class AddWalletInputs(previousTx: ReplaceableTxWithWalletInputs) extends AdjustPreviousTxOutputResult - case class TxOutputAdjusted(updatedTx: ReplaceableTxWithWitnessData) extends AdjustPreviousTxOutputResult - } - // @formatter:on - - /** - * Adjust the outputs of a transaction that was previously published at a lower feerate. - * If the current set of inputs doesn't let us to reach the target feerate, we should request new wallet inputs from bitcoind. - */ - def adjustPreviousTxOutput(previousTx: FundedTx, targetFeerate: FeeratePerKw, commitment: FullCommitment, commitTx: Transaction): AdjustPreviousTxOutputResult = { - val dustLimit = commitment.localParams.dustLimit - val targetFee = previousTx.signedTxWithWitnessData match { - case _: ClaimLocalAnchorWithWitnessData => - val commitFee = commitment.localCommit.commitTxAndRemoteSig.commitTx.fee - val totalWeight = previousTx.signedTx.weight() + commitTx.weight() - weight2fee(targetFeerate, totalWeight) - commitFee - case _ => - weight2fee(targetFeerate, previousTx.signedTx.weight()) - } - previousTx.signedTxWithWitnessData match { - case claimLocalAnchor: ClaimLocalAnchorWithWitnessData => - val changeAmount = previousTx.totalAmountIn - targetFee - if (changeAmount < dustLimit) { - AdjustPreviousTxOutputResult.AddWalletInputs(claimLocalAnchor) - } else { - val updatedTxOut = Seq(claimLocalAnchor.txInfo.tx.txOut.head.copy(amount = changeAmount)) - AdjustPreviousTxOutputResult.TxOutputAdjusted(claimLocalAnchor.updateTx(claimLocalAnchor.txInfo.tx.copy(txOut = updatedTxOut))) - } - case htlcTx: HtlcWithWitnessData => - if (htlcTx.txInfo.tx.txOut.length <= 1) { - // There is no change output, so we can't increase the fees without adding new inputs. - AdjustPreviousTxOutputResult.AddWalletInputs(htlcTx) - } else { - val htlcAmount = htlcTx.txInfo.tx.txOut.head.amount - val changeAmount = previousTx.totalAmountIn - targetFee - htlcAmount - if (dustLimit <= changeAmount) { - val updatedTxOut = Seq(htlcTx.txInfo.tx.txOut.head, htlcTx.txInfo.tx.txOut.last.copy(amount = changeAmount)) - AdjustPreviousTxOutputResult.TxOutputAdjusted(htlcTx.updateTx(htlcTx.txInfo.tx.copy(txOut = updatedTxOut))) - } else { - // We try removing the change output to see if it provides a high enough feerate. - val htlcTxNoChange = htlcTx.updateTx(htlcTx.txInfo.tx.copy(txOut = Seq(htlcTx.txInfo.tx.txOut.head))) - val fee = previousTx.totalAmountIn - htlcAmount - if (fee <= htlcAmount) { - val feerate = fee2rate(fee, htlcTxNoChange.txInfo.tx.weight()) - if (targetFeerate <= feerate) { - // Without the change output, we're able to reach our desired feerate. - AdjustPreviousTxOutputResult.TxOutputAdjusted(htlcTxNoChange) - } else { - // Even without the change output, the feerate is too low: we must add new wallet inputs. - AdjustPreviousTxOutputResult.AddWalletInputs(htlcTx) - } - } else { - AdjustPreviousTxOutputResult.Skip("fee higher than htlc amount") - } - } - } - case claimHtlcTx: ClaimHtlcWithWitnessData => - val updatedAmount = previousTx.totalAmountIn - targetFee - if (updatedAmount < dustLimit) { - AdjustPreviousTxOutputResult.Skip("fee higher than htlc amount") - } else { - val updatedTxOut = Seq(claimHtlcTx.txInfo.tx.txOut.head.copy(amount = updatedAmount)) - claimHtlcTx match { - // NB: we don't modify legacy claim-htlc-success, it's already signed. - case _: LegacyClaimHtlcSuccessWithWitnessData => AdjustPreviousTxOutputResult.Skip("legacy claim-htlc-success should not be updated") - case _ => AdjustPreviousTxOutputResult.TxOutputAdjusted(claimHtlcTx.updateTx(claimHtlcTx.txInfo.tx.copy(txOut = updatedTxOut))) - } - } - } - } - } -private class ReplaceableTxFunder(nodeParams: NodeParams, - replyTo: ActorRef[ReplaceableTxFunder.FundingResult], - cmd: TxPublisher.PublishReplaceableTx, +private class ReplaceableTxFunder(replyTo: ActorRef[ReplaceableTxFunder.FundingResult], + commitTx: Transaction, + commitment: FullCommitment, bitcoinClient: BitcoinCoreClient, context: ActorContext[ReplaceableTxFunder.Command])(implicit ec: ExecutionContext = ExecutionContext.Implicits.global) { import ReplaceableTxFunder._ - import nodeParams.{channelKeyManager => keyManager} + + private val dustLimit = commitment.localCommitParams.dustLimit + private val commitFee: Satoshi = commitment.capacity - commitTx.txOut.map(_.amount).sum private val log = context.log - def fund(txWithWitnessData: ReplaceableTxWithWitnessData, targetFeerate: FeeratePerKw): Behavior[Command] = { - log.info("funding {} tx (targetFeerate={})", txWithWitnessData.txInfo.desc, targetFeerate) - txWithWitnessData match { - case claimLocalAnchor: ClaimLocalAnchorWithWitnessData => - val commitFeerate = cmd.commitment.localCommit.spec.commitTxFeerate + def fund(tx: ForceCloseTransaction, targetFeerate: FeeratePerKw): Behavior[Command] = { + log.info("funding {} tx (targetFeerate={})", tx.desc, targetFeerate) + tx match { + case anchorTx: ClaimAnchorTx => + val commitFeerate = commitment.localCommit.spec.commitTxFeerate if (targetFeerate <= commitFeerate) { - log.info("skipping {}: commit feerate is high enough (feerate={})", cmd.desc, commitFeerate) + log.info("skipping {}: commit feerate is high enough (feerate={})", tx.desc, commitFeerate) // We set retry = true in case the on-chain feerate rises before the commit tx is confirmed: if that happens // we'll want to claim our anchor to raise the feerate of the commit tx and get it confirmed faster. replyTo ! FundingFailed(TxPublisher.TxRejectedReason.TxSkipped(retryNextBlock = true)) Behaviors.stopped } else { - addWalletInputs(claimLocalAnchor, targetFeerate) + addWalletInputs(anchorTx, targetFeerate) } - case htlcTx: HtlcWithWitnessData => - val htlcFeerate = cmd.commitment.localCommit.spec.htlcTxFeerate(cmd.commitment.params.commitmentFormat) + case htlcTx: SignedHtlcTx => + val htlcFeerate = commitment.localCommit.spec.htlcTxFeerate(htlcTx.commitmentFormat) if (targetFeerate <= htlcFeerate) { - log.debug("publishing {} without adding inputs: txid={}", cmd.desc, htlcTx.txInfo.tx.txid) - sign(txWithWitnessData, htlcFeerate, htlcTx.txInfo.amountIn, Map.empty) + log.debug("publishing {} without adding inputs: txid={}", tx.desc, htlcTx.tx.txid) + sign(htlcTx, htlcFeerate, walletInputs_opt = None) } else { addWalletInputs(htlcTx, targetFeerate) } - case claimHtlcTx: ClaimHtlcWithWitnessData => - adjustClaimHtlcTxOutput(claimHtlcTx, targetFeerate, cmd.commitment.localParams.dustLimit) match { + case _ => + Transactions.updateFee(tx, Transactions.weight2fee(targetFeerate, tx.expectedWeight), dustLimit) match { case Left(reason) => - // The htlc isn't economical to claim at the current feerate, but if the feerate goes down, we may want to claim it later. - log.warn("skipping {}: {} (feerate={})", cmd.desc, reason, targetFeerate) + // The output isn't economical to claim at the current feerate, but if the feerate goes down, we may want to claim it later. + log.warn("skipping {}: {} (feerate={})", tx.desc, reason.toString, targetFeerate) replyTo ! FundingFailed(TxPublisher.TxRejectedReason.TxSkipped(retryNextBlock = true)) Behaviors.stopped - case Right(updatedClaimHtlcTx) => - sign(updatedClaimHtlcTx, targetFeerate, updatedClaimHtlcTx.txInfo.amountIn, Map.empty) + case Right(updatedTx) => + sign(updatedTx, targetFeerate, walletInputs_opt = None) } } } private def bump(previousTx: FundedTx, targetFeerate: FeeratePerKw): Behavior[Command] = { - log.info("bumping {} tx (targetFeerate={})", previousTx.signedTxWithWitnessData.txInfo.desc, targetFeerate) - adjustPreviousTxOutput(previousTx, targetFeerate, cmd.commitment, cmd.commitTx) match { - case AdjustPreviousTxOutputResult.Skip(reason) => - log.warn("skipping {} fee bumping: {} (feerate={})", cmd.desc, reason, targetFeerate) - replyTo ! FundingFailed(TxPublisher.TxRejectedReason.TxSkipped(retryNextBlock = true)) - Behaviors.stopped - case AdjustPreviousTxOutputResult.TxOutputAdjusted(updatedTx) => - log.debug("bumping {} fees without adding new inputs: txid={}", cmd.desc, updatedTx.txInfo.tx.txid) - sign(updatedTx, targetFeerate, previousTx.totalAmountIn, previousTx.walletInputs) - case AdjustPreviousTxOutputResult.AddWalletInputs(tx) => - log.debug("bumping {} fees requires adding new inputs (feerate={})", cmd.desc, targetFeerate) - // We restore the original transaction (remove previous attempt's wallet inputs). - val resetTx = tx.updateTx(cmd.txInfo.tx) - addWalletInputs(resetTx, targetFeerate) + log.info("bumping {} tx (targetFeerate={})", previousTx.txInfo.desc, targetFeerate) + val targetFee = previousTx.txInfo match { + case _: ClaimAnchorTx => + val totalWeight = previousTx.signedTx.weight() + commitTx.weight() + weight2fee(targetFeerate, totalWeight) - commitFee + case _ => + weight2fee(targetFeerate, previousTx.signedTx.weight()) + } + // Whenever possible, we keep the previous wallet input(s) and simply update the change amount. + previousTx.txInfo match { + case anchorTx: ClaimAnchorTx => + val changeAmount = previousTx.totalAmountIn - targetFee + previousTx.walletInputs_opt match { + case Some(walletInputs) if changeAmount > dustLimit => sign(anchorTx, targetFeerate, Some(walletInputs.setChangeAmount(changeAmount))) + case _ => addWalletInputs(anchorTx, targetFeerate) + } + case htlcTx: SignedHtlcTx => + previousTx.walletInputs_opt match { + case Some(walletInputs) => + val htlcAmount = htlcTx.amountIn + val changeAmount = previousTx.totalAmountIn - targetFee - htlcAmount + if (changeAmount > dustLimit) { + // The existing wallet inputs are sufficient to pay the target feerate: no need to add new inputs. + sign(htlcTx, targetFeerate, Some(walletInputs.setChangeAmount(changeAmount))) + } else { + // We try removing the change output to see if it provides a high enough feerate. + val htlcTxNoChange = previousTx.signedTx.copy(txOut = previousTx.signedTx.txOut.take(1)) + val feeWithoutChange = previousTx.totalAmountIn - htlcAmount + if (feeWithoutChange <= htlcAmount) { + val feerate = Transactions.fee2rate(feeWithoutChange, htlcTxNoChange.weight()) + if (targetFeerate <= feerate) { + // Without the change output, we're able to reach our desired feerate. + sign(htlcTx, targetFeerate, Some(walletInputs.copy(changeOutput_opt = None))) + } else { + // Even without the change output, the feerate is too low: we must add new wallet inputs. + addWalletInputs(htlcTx, targetFeerate) + } + } else { + log.warn("skipping {} fee bumping: htlc amount too low (amount={} feerate={})", previousTx.txInfo.desc, htlcAmount, targetFeerate) + replyTo ! FundingFailed(TxPublisher.TxRejectedReason.TxSkipped(retryNextBlock = true)) + Behaviors.stopped + } + } + case None => addWalletInputs(htlcTx, targetFeerate) + } + case _ => + Transactions.updateFee(previousTx.txInfo, targetFee, dustLimit) match { + case Left(reason) => + log.warn("skipping {} fee bumping: {} (feerate={})", previousTx.txInfo.desc, reason.toString, targetFeerate) + replyTo ! FundingFailed(TxPublisher.TxRejectedReason.TxSkipped(retryNextBlock = true)) + Behaviors.stopped + case Right(updatedTx) => + sign(updatedTx, targetFeerate, walletInputs_opt = None) + } } } - private def addWalletInputs(txWithWitnessData: ReplaceableTxWithWalletInputs, targetFeerate: FeeratePerKw): Behavior[Command] = { - context.pipeToSelf(addInputs(txWithWitnessData, targetFeerate, cmd.commitment)) { - case Success((fundedTx, totalAmountIn, psbt)) => AddInputsOk(fundedTx, totalAmountIn, psbt) + private def addWalletInputs(tx: HasWalletInputs, targetFeerate: FeeratePerKw): Behavior[Command] = { + context.pipeToSelf(tx match { + case anchorTx: ClaimAnchorTx => addInputs(anchorTx, targetFeerate) + case htlcTx: SignedHtlcTx => addInputs(htlcTx, targetFeerate) + }) { + case Success(walletInputs) => AddInputsOk(walletInputs) case Failure(reason) => AddInputsFailed(reason) } Behaviors.receiveMessagePartial { - case AddInputsOk(fundedTx, totalAmountIn, walletUtxos) => - log.debug("added {} wallet input(s) and {} wallet output(s) to {}", fundedTx.txInfo.tx.txIn.length - 1, fundedTx.txInfo.tx.txOut.length - 1, cmd.desc) - sign(fundedTx, targetFeerate, totalAmountIn, walletUtxos) + case AddInputsOk(walletInputs) => + log.debug("added {} wallet input(s) and {} wallet output(s) to {}", walletInputs.inputs.size, walletInputs.txOut.size, tx.desc) + sign(tx, targetFeerate, Some(walletInputs)) case AddInputsFailed(reason) => if (reason.getMessage.contains("Insufficient funds")) { val nodeOperatorMessage = - s"""Insufficient funds in bitcoin wallet to set feerate=$targetFeerate for ${cmd.desc}. + s"""Insufficient funds in bitcoin wallet to set feerate=$targetFeerate for ${tx.desc}. |You should add more utxos to your bitcoin wallet to guarantee funds safety. |Attempts will be made periodically to re-publish this transaction. |""".stripMargin context.system.eventStream ! EventStream.Publish(NotifyNodeOperator(NotificationsLogger.Warning, nodeOperatorMessage)) - log.warn("cannot add inputs to {}: {}", cmd.desc, reason.getMessage) + log.warn("cannot add inputs to {}: {}", tx.desc, reason.getMessage) } else { - log.error(s"cannot add inputs to ${cmd.desc}: ", reason) + log.error(s"cannot add inputs to ${tx.desc}: ", reason) } replyTo ! FundingFailed(TxPublisher.TxRejectedReason.CouldNotFund) Behaviors.stopped } } - private def sign(fundedTx: ReplaceableTxWithWitnessData, txFeerate: FeeratePerKw, amountIn: Satoshi, walletUtxos: Map[OutPoint, TxOut]): Behavior[Command] = { - val channelKeyPath = keyManager.keyPath(cmd.commitment.localParams, cmd.commitment.params.channelConfig) - fundedTx match { - case claimAnchorTx: ClaimLocalAnchorWithWitnessData => - val localSig = keyManager.sign(claimAnchorTx.txInfo, keyManager.fundingPublicKey(cmd.commitment.localParams.fundingKeyPath, cmd.commitment.fundingTxIndex), TxOwner.Local, cmd.commitment.params.commitmentFormat, walletUtxos) - val signedTx = claimAnchorTx.copy(txInfo = addSigs(claimAnchorTx.txInfo, localSig)) - signWalletInputs(signedTx, txFeerate, amountIn, walletUtxos) - case htlcTx: HtlcWithWitnessData => - val localPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, cmd.commitment.localCommit.index) - val localHtlcBasepoint = keyManager.htlcPoint(channelKeyPath) - val localSig = keyManager.sign(htlcTx.txInfo, localHtlcBasepoint, localPerCommitmentPoint, TxOwner.Local, cmd.commitment.params.commitmentFormat, walletUtxos) - val signedTx = htlcTx match { - case htlcSuccess: HtlcSuccessWithWitnessData => htlcSuccess.copy(txInfo = addSigs(htlcSuccess.txInfo, localSig, htlcSuccess.remoteSig, htlcSuccess.preimage, cmd.commitment.params.commitmentFormat)) - case htlcTimeout: HtlcTimeoutWithWitnessData => htlcTimeout.copy(txInfo = addSigs(htlcTimeout.txInfo, localSig, htlcTimeout.remoteSig, cmd.commitment.params.commitmentFormat)) - } - val hasWalletInputs = htlcTx.txInfo.tx.txIn.size > 1 - if (hasWalletInputs) { - signWalletInputs(signedTx, txFeerate, amountIn, walletUtxos) - } else { - replyTo ! TransactionReady(FundedTx(signedTx, amountIn, txFeerate, walletUtxos)) - Behaviors.stopped - } - case claimHtlcTx: ClaimHtlcWithWitnessData => - val remotePerCommitmentPoint = cmd.commitment.nextRemoteCommit_opt match { - case Some(c) if claimHtlcTx.txInfo.input.outPoint.txid == c.commit.txid => c.commit.remotePerCommitmentPoint - case _ => cmd.commitment.remoteCommit.remotePerCommitmentPoint - } - val sig = keyManager.sign(claimHtlcTx.txInfo, keyManager.htlcPoint(channelKeyPath), remotePerCommitmentPoint, TxOwner.Local, cmd.commitment.params.commitmentFormat, walletUtxos) - val signedTx = claimHtlcTx match { - case claimSuccess: ClaimHtlcSuccessWithWitnessData => claimSuccess.copy(txInfo = addSigs(claimSuccess.txInfo, sig, claimSuccess.preimage)) - case legacyClaimHtlcSuccess: LegacyClaimHtlcSuccessWithWitnessData => legacyClaimHtlcSuccess - case claimTimeout: ClaimHtlcTimeoutWithWitnessData => claimTimeout.copy(txInfo = addSigs(claimTimeout.txInfo, sig)) - } - replyTo ! TransactionReady(FundedTx(signedTx, amountIn, txFeerate, walletUtxos)) + private def sign(tx: ForceCloseTransaction, txFeerate: FeeratePerKw, walletInputs_opt: Option[WalletInputs]): Behavior[Command] = { + (tx, walletInputs_opt) match { + case (tx: HasWalletInputs, Some(walletInputs)) => + val locallySignedTx = tx.sign(walletInputs) + signWalletInputs(tx, locallySignedTx, txFeerate, walletInputs) + case _ => + val signedTx = tx.sign() + replyTo ! TransactionReady(FundedTx(tx, None, signedTx, txFeerate)) Behaviors.stopped } } - private def signWalletInputs(locallySignedTx: ReplaceableTxWithWalletInputs, txFeerate: FeeratePerKw, amountIn: Satoshi, walletUtxos: Map[OutPoint, TxOut]): Behavior[Command] = { + private def signWalletInputs(tx: HasWalletInputs, locallySignedTx: Transaction, txFeerate: FeeratePerKw, walletInputs: WalletInputs): Behavior[Command] = { import fr.acinq.bitcoin.scalacompat.KotlinUtils._ // We create a PSBT with the non-wallet input already signed: - val witnessScript = locallySignedTx.txInfo.input match { - case InputInfo.SegwitInput(_, _, redeemScript) => fr.acinq.bitcoin.Script.parse(redeemScript) - case _: InputInfo.TaprootInput => null + val witnessScript = tx.redeemInfo match { + case redeemInfo: RedeemInfo.SegwitV0 => fr.acinq.bitcoin.Script.parse(redeemInfo.redeemScript) + case _: RedeemInfo.Taproot => null } - val sigHash = locallySignedTx.txInfo.sighash(TxOwner.Local, cmd.commitment.params.commitmentFormat) - val psbt = new Psbt(locallySignedTx.txInfo.tx) - .updateWitnessInput( - locallySignedTx.txInfo.input.outPoint, - locallySignedTx.txInfo.input.txOut, - null, - witnessScript, - sigHash, - java.util.Map.of(), - null, - null, - java.util.Map.of() - ).flatMap(_.finalizeWitnessInput(0, locallySignedTx.txInfo.tx.txIn.head.witness)) + val psbt = new Psbt(locallySignedTx).updateWitnessInput( + tx.input.outPoint, + tx.input.txOut, + null, + witnessScript, + tx.sighash, + java.util.Map.of(), + null, + null, + java.util.Map.of() + ).flatMap(_.finalizeWitnessInput(0, locallySignedTx.txIn.head.witness)) psbt match { case Left(failure) => - log.error(s"cannot sign ${cmd.desc}: $failure") - unlockAndStop(locallySignedTx.txInfo.tx, TxPublisher.TxRejectedReason.UnknownTxFailure) + log.error(s"cannot sign ${tx.desc}: $failure") + unlockAndStop(locallySignedTx, TxPublisher.TxRejectedReason.UnknownTxFailure) case Right(psbt1) => - // The transaction that we want to fund/replace has one input, the first one. Additional inputs are provided by our on-chain wallet. - val ourWalletInputs = locallySignedTx.txInfo.tx.txIn.indices.tail + // The transaction that we want to fund/replace has one input, the first one. + // Additional inputs are provided by our on-chain wallet. + val ourWalletInputs = locallySignedTx.txIn.indices.tail // For "claim anchor txs" there is a single change output that sends to our on-chain wallet. // For htlc txs the first output is the one we want to fund/bump, additional outputs send to our on-chain wallet. - val ourWalletOutputs = locallySignedTx match { - case _: ClaimLocalAnchorWithWitnessData => Seq(0) - case _: HtlcWithWitnessData => locallySignedTx.txInfo.tx.txOut.indices.tail + val ourWalletOutputs = tx match { + case _: ClaimAnchorTx => Seq(0) + case _: SignedHtlcTx => locallySignedTx.txOut.indices.tail } context.pipeToSelf(bitcoinClient.signPsbt(psbt1, ourWalletInputs, ourWalletOutputs)) { case Success(processPsbtResponse) => processPsbtResponse.finalTx_opt match { case Right(signedTx) => val actualFees = kmp2scala(processPsbtResponse.psbt.computeFees()) - val actualWeight = locallySignedTx match { - case _: ClaimLocalAnchorWithWitnessData => signedTx.weight() + cmd.commitTx.weight() - case _ => signedTx.weight() + val actualWeight = tx match { + case _: ClaimAnchorTx => signedTx.weight() + commitTx.weight() + case _: SignedHtlcTx => signedTx.weight() } val actualFeerate = Transactions.fee2rate(actualFees, actualWeight) if (actualFeerate >= txFeerate * 2) { @@ -402,14 +317,13 @@ private class ReplaceableTxFunder(nodeParams: NodeParams, } Behaviors.receiveMessagePartial { case SignWalletInputsOk(signedTx) => - val fullySignedTx = locallySignedTx.updateTx(signedTx) - replyTo ! TransactionReady(FundedTx(fullySignedTx, amountIn, txFeerate, walletUtxos)) + replyTo ! TransactionReady(FundedTx(tx, Some(walletInputs), signedTx, txFeerate)) Behaviors.stopped case SignWalletInputsFailed(reason) => - log.error(s"cannot sign ${cmd.desc}: ", reason) + log.error(s"cannot sign ${tx.desc}: ", reason) // We reply with the failure only once the utxos are unlocked, otherwise there is a risk that our parent stops // itself, which will automatically stop us before we had a chance to unlock them. - unlockAndStop(locallySignedTx.txInfo.tx, TxPublisher.TxRejectedReason.UnknownTxFailure) + unlockAndStop(locallySignedTx, TxPublisher.TxRejectedReason.UnknownTxFailure) } } } @@ -426,66 +340,56 @@ private class ReplaceableTxFunder(nodeParams: NodeParams, } } - private def getWalletUtxos(txInfo: TransactionWithInputInfo): Future[Map[OutPoint, TxOut]] = { - Future.sequence(txInfo.tx.txIn.filter(_.outPoint != txInfo.input.outPoint).map(txIn => { + private def getWalletUtxos(inputs: Seq[TxIn]): Future[Seq[WalletInput]] = { + Future.sequence(inputs.map(txIn => { bitcoinClient.getTransaction(txIn.outPoint.txid).flatMap { case inputTx if inputTx.txOut.size <= txIn.outPoint.index => Future.failed(new IllegalArgumentException(s"input ${inputTx.txid}:${txIn.outPoint.index} doesn't exist")) - case inputTx => Future.successful(txIn.outPoint -> inputTx.txOut(txIn.outPoint.index.toInt)) + case inputTx => Future.successful(WalletInput(txIn, inputTx.txOut(txIn.outPoint.index.toInt))) } - })).map(_.toMap) + })) } - private def addInputs(tx: ReplaceableTxWithWalletInputs, targetFeerate: FeeratePerKw, commitment: FullCommitment): Future[(ReplaceableTxWithWalletInputs, Satoshi, Map[OutPoint, TxOut])] = { - for { - (fundedTx, amountIn) <- tx match { - case anchorTx: ClaimLocalAnchorWithWitnessData => addInputs(anchorTx, targetFeerate, commitment) - case htlcTx: HtlcWithWitnessData => addInputs(htlcTx, targetFeerate, commitment) - } - spentUtxos <- getWalletUtxos(fundedTx.txInfo) - } yield (fundedTx, amountIn, spentUtxos) - } - - private def addInputs(anchorTx: ClaimLocalAnchorWithWitnessData, targetFeerate: FeeratePerKw, commitment: FullCommitment): Future[(ClaimLocalAnchorWithWitnessData, Satoshi)] = { + private def addInputs(anchorTx: ClaimAnchorTx, targetFeerate: FeeratePerKw): Future[WalletInputs] = { // We want to pay the commit fees using CPFP. Since the commit tx may not be in the mempool yet (its feerate may be // below the minimum acceptable mempool feerate), we cannot ask bitcoind to fund a transaction that spends that // commit tx: it would fail because it cannot find the input in the utxo set. So we instead ask bitcoind to fund an // empty transaction that pays the fees we must add to the transaction package, and we then add the input spending // the commit tx and adjust the change output. - val expectedCommitFee = Transactions.weight2fee(targetFeerate, cmd.commitTx.weight()) - val actualCommitFee = commitment.commitInput.txOut.amount - cmd.commitTx.txOut.map(_.amount).sum - val anchorInputFee = Transactions.weight2fee(targetFeerate, anchorInputWeight) - val missingFee = expectedCommitFee - actualCommitFee + anchorInputFee + val expectedCommitFee = Transactions.weight2fee(targetFeerate, commitTx.weight()) + val anchorFee = Transactions.weight2fee(targetFeerate, anchorTx.expectedWeight) + val missingFee = expectedCommitFee - commitFee + anchorFee for { changeScript <- bitcoinClient.getChangePublicKeyScript() - txNotFunded = Transaction(2, Nil, TxOut(commitment.localParams.dustLimit + missingFee, changeScript) :: Nil, 0) + txNotFunded = Transaction(2, Nil, TxOut(dustLimit + missingFee, changeScript) :: Nil, 0) // We only use confirmed inputs for anchor transactions to be able to leverage 1-parent-1-child package relay. fundTxResponse <- bitcoinClient.fundTransaction(txNotFunded, targetFeerate, minInputConfirmations_opt = Some(1)) + walletInputs <- getWalletUtxos(fundTxResponse.tx.txIn) } yield { // We merge our dummy change output with the one added by Bitcoin Core, if any, and adjust the change amount to // pay the expected package feerate. - val txIn = anchorTx.txInfo.tx.txIn ++ fundTxResponse.tx.txIn - val packageWeight = cmd.commitTx.weight() + anchorInputWeight + fundTxResponse.tx.weight() + val packageWeight = commitTx.weight() + anchorTx.commitmentFormat.anchorInputWeight + fundTxResponse.tx.weight() val expectedFee = Transactions.weight2fee(targetFeerate, packageWeight) - val currentFee = actualCommitFee + fundTxResponse.fee - val changeAmount = (fundTxResponse.tx.txOut.map(_.amount).sum - expectedFee + currentFee).max(commitment.localParams.dustLimit) - val changeOutput = fundTxResponse.changePosition match { - case Some(changePos) => fundTxResponse.tx.txOut(changePos).copy(amount = changeAmount) - case None => TxOut(changeAmount, changeScript) - } - val txSingleOutput = fundTxResponse.tx.copy(txIn = txIn, txOut = Seq(changeOutput)) - (anchorTx.updateTx(txSingleOutput), fundTxResponse.amountIn) + // Note that we haven't taken into account yet the amount of the anchor output, so we add it here. + val currentFee = commitFee + fundTxResponse.fee + anchorTx.input.txOut.amount + val changeAmount = (fundTxResponse.tx.txOut.map(_.amount).sum - expectedFee + currentFee).max(dustLimit) + WalletInputs(walletInputs, changeOutput_opt = Some(TxOut(changeAmount, changeScript))) } } - private def addInputs(htlcTx: HtlcWithWitnessData, targetFeerate: FeeratePerKw, commitment: FullCommitment): Future[(HtlcWithWitnessData, Satoshi)] = { - val htlcInputWeight = htlcTx.txInfo match { - case _: HtlcSuccessTx => commitment.params.commitmentFormat.htlcSuccessInputWeight.toLong - case _: HtlcTimeoutTx => commitment.params.commitmentFormat.htlcTimeoutInputWeight.toLong + private def addInputs(htlcTx: SignedHtlcTx, targetFeerate: FeeratePerKw): Future[WalletInputs] = { + val htlcInputWeight = htlcTx match { + case _: HtlcSuccessTx => htlcTx.commitmentFormat.htlcSuccessInputWeight.toLong + case _: HtlcTimeoutTx => htlcTx.commitmentFormat.htlcTimeoutInputWeight.toLong + } + for { + fundTxResponse <- bitcoinClient.fundTransaction(htlcTx.tx, targetFeerate, changePosition = Some(1), externalInputsWeight = Map(htlcTx.input.outPoint -> htlcInputWeight)) + walletInputs <- getWalletUtxos(fundTxResponse.tx.txIn.filter(_.outPoint != htlcTx.input.outPoint)) + } yield { + val changeOutput_opt = fundTxResponse.changePosition match { + case Some(changeIndex) if changeIndex < fundTxResponse.tx.txOut.size => Some(fundTxResponse.tx.txOut(changeIndex)) + case _ => None + } + WalletInputs(walletInputs, changeOutput_opt) } - bitcoinClient.fundTransaction(htlcTx.txInfo.tx, targetFeerate, changePosition = Some(1), externalInputsWeight = Map(htlcTx.txInfo.input.outPoint -> htlcInputWeight)).map(fundTxResponse => { - // Bitcoin Core may not preserve the order of inputs, we need to make sure the htlc is the first input. - val fundedTx = fundTxResponse.tx.copy(txIn = htlcTx.txInfo.tx.txIn ++ fundTxResponse.tx.txIn.filterNot(_.outPoint == htlcTx.txInfo.input.outPoint)) - (htlcTx.updateTx(fundedTx), fundTxResponse.amountIn) - }) } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPrePublisher.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPrePublisher.scala index e15119860e..368cf56d0d 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPrePublisher.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPrePublisher.scala @@ -18,14 +18,11 @@ package fr.acinq.eclair.channel.publish import akka.actor.typed.scaladsl.{ActorContext, Behaviors} import akka.actor.typed.{ActorRef, Behavior} -import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, Crypto, Transaction} +import fr.acinq.bitcoin.scalacompat.{OutPoint, Transaction, TxId} import fr.acinq.eclair.NodeParams import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinCoreClient import fr.acinq.eclair.channel.publish.TxPublisher.TxPublishContext -import fr.acinq.eclair.channel.{FullCommitment, HtlcTxAndRemoteSig} -import fr.acinq.eclair.transactions.Transactions import fr.acinq.eclair.transactions.Transactions._ -import fr.acinq.eclair.wire.protocol.UpdateFulfillHtlc import scala.concurrent.{ExecutionContext, Future} import scala.util.{Failure, Success} @@ -43,71 +40,38 @@ object ReplaceableTxPrePublisher { // @formatter:off sealed trait Command - case class CheckPreconditions(replyTo: ActorRef[PreconditionsResult], cmd: TxPublisher.PublishReplaceableTx) extends Command + case class CheckPreconditions(replyTo: ActorRef[PreconditionsResult], txInfo: ForceCloseTransaction, commitTx: Transaction, concurrentCommitTxs: Set[TxId]) extends Command private case object ParentTxOk extends Command - private case object FundingTxNotFound extends RuntimeException with Command - private case object CommitTxAlreadyConfirmed extends RuntimeException with Command - private case object LocalCommitTxConfirmed extends Command - private case object LocalCommitTxPublished extends Command - private case object RemoteCommitTxConfirmed extends Command - private case object RemoteCommitTxPublished extends RuntimeException with Command + private case object FundingTxNotFound extends Command + private case object CommitTxRecentlyConfirmed extends Command + private case object CommitTxDeeplyConfirmed extends Command + private case object ConcurrentCommitAvailable extends Command + private case object ConcurrentCommitRecentlyConfirmed extends Command + private case object ConcurrentCommitDeeplyConfirmed extends Command private case object HtlcOutputAlreadySpent extends Command private case class UnknownFailure(reason: Throwable) extends Command // @formatter:on // @formatter:off sealed trait PreconditionsResult - case class PreconditionsOk(txWithWitnessData: ReplaceableTxWithWitnessData) extends PreconditionsResult + case object PreconditionsOk extends PreconditionsResult case class PreconditionsFailed(reason: TxPublisher.TxRejectedReason) extends PreconditionsResult - - /** Replaceable transaction with all the witness data necessary to finalize. */ - sealed trait ReplaceableTxWithWitnessData { - def txInfo: ReplaceableTransactionWithInputInfo - def updateTx(tx: Transaction): ReplaceableTxWithWitnessData - } - /** Replaceable transaction for which we may need to add wallet inputs. */ - sealed trait ReplaceableTxWithWalletInputs extends ReplaceableTxWithWitnessData { - override def updateTx(tx: Transaction): ReplaceableTxWithWalletInputs - } - case class ClaimLocalAnchorWithWitnessData(txInfo: ClaimLocalAnchorOutputTx) extends ReplaceableTxWithWalletInputs { - override def updateTx(tx: Transaction): ClaimLocalAnchorWithWitnessData = this.copy(txInfo = this.txInfo.copy(tx = tx)) - } - sealed trait HtlcWithWitnessData extends ReplaceableTxWithWalletInputs { - override def txInfo: HtlcTx - override def updateTx(tx: Transaction): HtlcWithWitnessData - } - case class HtlcSuccessWithWitnessData(txInfo: HtlcSuccessTx, remoteSig: ByteVector64, preimage: ByteVector32) extends HtlcWithWitnessData { - override def updateTx(tx: Transaction): HtlcSuccessWithWitnessData = this.copy(txInfo = this.txInfo.copy(tx = tx)) - } - case class HtlcTimeoutWithWitnessData(txInfo: HtlcTimeoutTx, remoteSig: ByteVector64) extends HtlcWithWitnessData { - override def updateTx(tx: Transaction): HtlcTimeoutWithWitnessData = this.copy(txInfo = this.txInfo.copy(tx = tx)) - } - sealed trait ClaimHtlcWithWitnessData extends ReplaceableTxWithWitnessData { - override def txInfo: ClaimHtlcTx - override def updateTx(tx: Transaction): ClaimHtlcWithWitnessData - } - case class ClaimHtlcSuccessWithWitnessData(txInfo: ClaimHtlcSuccessTx, preimage: ByteVector32) extends ClaimHtlcWithWitnessData { - override def updateTx(tx: Transaction): ClaimHtlcSuccessWithWitnessData = this.copy(txInfo = this.txInfo.copy(tx = tx)) - } - case class LegacyClaimHtlcSuccessWithWitnessData(txInfo: LegacyClaimHtlcSuccessTx, preimage: ByteVector32) extends ClaimHtlcWithWitnessData { - override def updateTx(tx: Transaction): LegacyClaimHtlcSuccessWithWitnessData = this.copy(txInfo = this.txInfo.copy(tx = tx)) - } - case class ClaimHtlcTimeoutWithWitnessData(txInfo: ClaimHtlcTimeoutTx) extends ClaimHtlcWithWitnessData { - override def updateTx(tx: Transaction): ClaimHtlcTimeoutWithWitnessData = this.copy(txInfo = this.txInfo.copy(tx = tx)) - } // @formatter:on def apply(nodeParams: NodeParams, bitcoinClient: BitcoinCoreClient, txPublishContext: TxPublishContext): Behavior[Command] = { Behaviors.setup { context => Behaviors.withMdc(txPublishContext.mdc()) { Behaviors.receiveMessagePartial { - case CheckPreconditions(replyTo, cmd) => - val prePublisher = new ReplaceableTxPrePublisher(nodeParams, replyTo, cmd, bitcoinClient, context) - cmd.txInfo match { - case localAnchorTx: Transactions.ClaimLocalAnchorOutputTx => prePublisher.checkAnchorPreconditions(localAnchorTx) - case htlcTx: Transactions.HtlcTx => prePublisher.checkHtlcPreconditions(htlcTx) - case claimHtlcTx: Transactions.ClaimHtlcTx => prePublisher.checkClaimHtlcPreconditions(claimHtlcTx) + case CheckPreconditions(replyTo, txInfo, commitTx, concurrentCommitTxs) => + val prePublisher = new ReplaceableTxPrePublisher(nodeParams, replyTo, bitcoinClient, context) + txInfo match { + case _: ClaimLocalAnchorTx => prePublisher.checkLocalCommitAnchorPreconditions(commitTx) + case _: ClaimRemoteAnchorTx => prePublisher.checkRemoteCommitAnchorPreconditions(commitTx) + case _: SignedHtlcTx | _: ClaimHtlcTx => prePublisher.checkHtlcPreconditions(txInfo.desc, txInfo.input.outPoint, commitTx, concurrentCommitTxs) + case _ => + replyTo ! PreconditionsOk + Behaviors.stopped } } } @@ -118,7 +82,6 @@ object ReplaceableTxPrePublisher { private class ReplaceableTxPrePublisher(nodeParams: NodeParams, replyTo: ActorRef[ReplaceableTxPrePublisher.PreconditionsResult], - cmd: TxPublisher.PublishReplaceableTx, bitcoinClient: BitcoinCoreClient, context: ActorContext[ReplaceableTxPrePublisher.Command])(implicit ec: ExecutionContext = ExecutionContext.Implicits.global) { @@ -126,233 +89,188 @@ private class ReplaceableTxPrePublisher(nodeParams: NodeParams, private val log = context.log - private def checkAnchorPreconditions(localAnchorTx: ClaimLocalAnchorOutputTx): Behavior[Command] = { - // We verify that: - // - our commit is not confirmed (if it is, no need to claim our anchor) - // - their commit is not confirmed (if it is, no need to claim our anchor either) - val fundingOutpoint = cmd.commitment.commitInput.outPoint + /** + * We only claim our anchor output for our local commitment if: + * - our local commitment is unconfirmed + * - and we haven't seen a remote commitment (in which case it is more interesting to spend than the local commitment) + */ + private def checkLocalCommitAnchorPreconditions(commitTx: Transaction): Behavior[Command] = { + val fundingOutpoint = commitTx.txIn.head.outPoint context.pipeToSelf(bitcoinClient.getTxConfirmations(fundingOutpoint.txid).flatMap { case Some(_) => // The funding transaction was found, let's see if we can still spend it. - bitcoinClient.isTransactionOutputSpendable(fundingOutpoint.txid, fundingOutpoint.index.toInt, includeMempool = false).flatMap { - case false => Future.failed(CommitTxAlreadyConfirmed) - case true if cmd.isLocalCommitAnchor => - // We are trying to bump our local commitment. Let's check if the remote commitment is published: if it is, - // we will skip publishing our local commitment, because the remote commitment is more interesting (we don't - // have any CSV delays and don't need 2nd-stage HTLC transactions). - getRemoteCommitConfirmations(cmd.commitment).flatMap { - case Some(_) => Future.failed(RemoteCommitTxPublished) - // We're trying to bump the local commit tx: no need to do anything, we will publish it alongside the anchor transaction. - case None => Future.successful(cmd.commitTx.txid) - } + bitcoinClient.isTransactionOutputSpendable(fundingOutpoint.txid, fundingOutpoint.index.toInt, includeMempool = true).flatMap { case true => - // We're trying to bump a remote commitment: no need to do anything, we will publish it alongside the anchor transaction. - Future.successful(cmd.commitTx.txid) + // The funding output is unspent: let's publish our anchor transaction to get our local commit confirmed. + Future.successful(ParentTxOk) + case false => + // The funding output is spent: we check whether our local commit is confirmed or in our mempool. + bitcoinClient.getTxConfirmations(commitTx.txid).transformWith { + case Success(Some(confirmations)) if confirmations >= nodeParams.channelConf.minDepth => Future.successful(CommitTxDeeplyConfirmed) + case Success(Some(confirmations)) if confirmations > 0 => Future.successful(CommitTxRecentlyConfirmed) + case Success(Some(0)) => Future.successful(ParentTxOk) // our commit tx is unconfirmed, let's publish our anchor transaction + case _ => + // Our commit tx is unconfirmed and cannot be found in our mempool: this means that a remote commit is + // either confirmed or in our mempool. In that case, we don't want to use our local commit tx: the + // remote commit is more interesting to us because we won't have any CSV delays on our outputs. + Future.successful(ConcurrentCommitAvailable) + } } case None => // If the funding transaction cannot be found (e.g. when using 0-conf), we should retry later. - Future.failed(FundingTxNotFound) + Future.successful(FundingTxNotFound) }) { - case Success(_) => ParentTxOk - case Failure(FundingTxNotFound) => FundingTxNotFound - case Failure(CommitTxAlreadyConfirmed) => CommitTxAlreadyConfirmed - case Failure(RemoteCommitTxPublished) => RemoteCommitTxPublished - case Failure(reason) if reason.getMessage.contains("rejecting replacement") => RemoteCommitTxPublished + case Success(result) => result case Failure(reason) => UnknownFailure(reason) } Behaviors.receiveMessagePartial { case ParentTxOk => - replyTo ! PreconditionsOk(ClaimLocalAnchorWithWitnessData(localAnchorTx)) + replyTo ! PreconditionsOk Behaviors.stopped case FundingTxNotFound => log.debug("funding tx could not be found, we don't know yet if we need to claim our anchor") replyTo ! PreconditionsFailed(TxPublisher.TxRejectedReason.TxSkipped(retryNextBlock = true)) Behaviors.stopped - case CommitTxAlreadyConfirmed => - log.debug("commit tx is already confirmed, no need to claim our anchor") + case CommitTxRecentlyConfirmed => + log.debug("local commit tx was recently confirmed, let's check again later") + replyTo ! PreconditionsFailed(TxPublisher.TxRejectedReason.TxSkipped(retryNextBlock = true)) + Behaviors.stopped + case CommitTxDeeplyConfirmed => + log.debug("local commit tx is deeply confirmed, no need to claim our anchor") replyTo ! PreconditionsFailed(TxPublisher.TxRejectedReason.TxSkipped(retryNextBlock = false)) Behaviors.stopped - case RemoteCommitTxPublished => - log.warn("not publishing local commit tx: we're using the remote commit tx instead") + case ConcurrentCommitAvailable => + log.warn("not publishing local anchor for commitTxId={}: we will use the remote commit tx instead", commitTx.txid) replyTo ! PreconditionsFailed(TxPublisher.TxRejectedReason.TxSkipped(retryNextBlock = false)) Behaviors.stopped case UnknownFailure(reason) => - log.error(s"could not check ${cmd.desc} preconditions, proceeding anyway: ", reason) + log.error("could not check local anchor preconditions, proceeding anyway: ", reason) // If our checks fail, we don't want it to prevent us from trying to publish our commit tx. - replyTo ! PreconditionsOk(ClaimLocalAnchorWithWitnessData(localAnchorTx)) + replyTo ! PreconditionsOk Behaviors.stopped } } - private def getRemoteCommitConfirmations(commitment: FullCommitment): Future[Option[Int]] = { - bitcoinClient.getTxConfirmations(commitment.remoteCommit.txid).transformWith { - // NB: this handles the case where the remote commit is in the mempool because we will get Some(0). - case Success(Some(remoteCommitConfirmations)) => Future.successful(Some(remoteCommitConfirmations)) - case notFoundOrFailed => commitment.nextRemoteCommit_opt match { - case Some(nextRemoteCommit) => bitcoinClient.getTxConfirmations(nextRemoteCommit.commit.txid) - case None => Future.fromTry(notFoundOrFailed) - } - } - } - /** - * We verify that: - * - their commit is not confirmed: if it is, there is no need to publish our htlc transactions - * - the HTLC output isn't already spent by a confirmed transaction (race between HTLC-timeout and HTLC-success) + * We only claim our anchor output for a remote commitment if: + * - that remote commitment is unconfirmed + * - there is no other commitment that is already confirmed */ - private def checkHtlcOutput(commitment: FullCommitment, htlcTx: HtlcTx): Future[Command] = { - getRemoteCommitConfirmations(commitment).flatMap { - case Some(depth) if depth >= nodeParams.channelConf.minDepth => Future.successful(RemoteCommitTxConfirmed) - case Some(_) => Future.successful(RemoteCommitTxPublished) - case _ => bitcoinClient.isTransactionOutputSpent(htlcTx.input.outPoint.txid, htlcTx.input.outPoint.index.toInt).map { - case true => HtlcOutputAlreadySpent - case false => ParentTxOk - } - } - } - - private def checkHtlcPreconditions(htlcTx: HtlcTx): Behavior[Command] = { - context.pipeToSelf(checkHtlcOutput(cmd.commitment, htlcTx)) { + private def checkRemoteCommitAnchorPreconditions(commitTx: Transaction): Behavior[Command] = { + val fundingOutpoint = commitTx.txIn.head.outPoint + context.pipeToSelf(bitcoinClient.getTxConfirmations(fundingOutpoint.txid).flatMap { + case Some(_) => + // The funding transaction was found, let's see if we can still spend it. Note that in this case, we only look + // at *confirmed* spending transactions (unlike the local commit case). + bitcoinClient.isTransactionOutputSpendable(fundingOutpoint.txid, fundingOutpoint.index.toInt, includeMempool = false).flatMap { + case true => + // The funding output is unspent, or spent by an *unconfirmed* transaction: let's publish our anchor + // transaction, we may be able to replace our local commit with this (more interesting) remote commit. + Future.successful(ParentTxOk) + case false => + // The funding output is spent by a confirmed commit tx: we check the status of our anchor's commit tx. + bitcoinClient.getTxConfirmations(commitTx.txid).transformWith { + case Success(Some(confirmations)) if confirmations >= nodeParams.channelConf.minDepth => Future.successful(CommitTxDeeplyConfirmed) + case Success(_) => Future.successful(CommitTxRecentlyConfirmed) + // The spending tx is another commit tx: we can stop trying to publish this one. + case _ => Future.successful(ConcurrentCommitAvailable) + } + } + case None => + // If the funding transaction cannot be found (e.g. when using 0-conf), we should retry later. + Future.successful(FundingTxNotFound) + }) { case Success(result) => result case Failure(reason) => UnknownFailure(reason) } Behaviors.receiveMessagePartial { case ParentTxOk => - // We make sure that if this is an htlc-success transaction, we have the preimage. - extractHtlcWitnessData(htlcTx, cmd.commitment) match { - case Some(txWithWitnessData) => replyTo ! PreconditionsOk(txWithWitnessData) - case None => replyTo ! PreconditionsFailed(TxPublisher.TxRejectedReason.TxSkipped(retryNextBlock = false)) - } + replyTo ! PreconditionsOk Behaviors.stopped - case RemoteCommitTxPublished => - log.info("cannot publish {}: remote commit has been published", cmd.desc) - // We keep retrying until the remote commit reaches min-depth to protect against reorgs. + case FundingTxNotFound => + log.debug("funding tx could not be found, we don't know yet if we need to claim our anchor") replyTo ! PreconditionsFailed(TxPublisher.TxRejectedReason.TxSkipped(retryNextBlock = true)) Behaviors.stopped - case RemoteCommitTxConfirmed => - log.warn("cannot publish {}: remote commit has been confirmed", cmd.desc) - replyTo ! PreconditionsFailed(TxPublisher.TxRejectedReason.ConflictingTxConfirmed) + case CommitTxRecentlyConfirmed => + log.debug("remote commit tx was recently confirmed, let's check again later") + replyTo ! PreconditionsFailed(TxPublisher.TxRejectedReason.TxSkipped(retryNextBlock = true)) Behaviors.stopped - case HtlcOutputAlreadySpent => - log.warn("cannot publish {}: htlc output has already been spent", cmd.desc) - replyTo ! PreconditionsFailed(TxPublisher.TxRejectedReason.ConflictingTxConfirmed) + case CommitTxDeeplyConfirmed => + log.debug("remote commit tx is deeply confirmed, no need to claim our anchor") + replyTo ! PreconditionsFailed(TxPublisher.TxRejectedReason.TxSkipped(retryNextBlock = false)) + Behaviors.stopped + case ConcurrentCommitAvailable => + log.warn("not publishing remote anchor for commitTxId={}: a concurrent commit tx is confirmed", commitTx.txid) + replyTo ! PreconditionsFailed(TxPublisher.TxRejectedReason.TxSkipped(retryNextBlock = false)) Behaviors.stopped case UnknownFailure(reason) => - log.error(s"could not check ${cmd.desc} preconditions, proceeding anyway: ", reason) - // If our checks fail, we don't want it to prevent us from trying to publish our htlc transactions. - extractHtlcWitnessData(htlcTx, cmd.commitment) match { - case Some(txWithWitnessData) => replyTo ! PreconditionsOk(txWithWitnessData) - case None => replyTo ! PreconditionsFailed(TxPublisher.TxRejectedReason.TxSkipped(retryNextBlock = false)) - } + log.error("could not check remote anchor preconditions, proceeding anyway: ", reason) + // If our checks fail, we don't want it to prevent us from trying to publish our commit tx. + replyTo ! PreconditionsOk Behaviors.stopped } } - private def extractHtlcWitnessData(htlcTx: HtlcTx, commitment: FullCommitment): Option[ReplaceableTxWithWitnessData] = { - htlcTx match { - case tx: HtlcSuccessTx => - commitment.localCommit.htlcTxsAndRemoteSigs.collectFirst { - case HtlcTxAndRemoteSig(HtlcSuccessTx(input, _, _, _, _), remoteSig) if input.outPoint == tx.input.outPoint => remoteSig - } match { - case Some(remoteSig) => - commitment.changes.localChanges.all.collectFirst { - case u: UpdateFulfillHtlc if Crypto.sha256(u.paymentPreimage) == tx.paymentHash => u.paymentPreimage - } match { - case Some(preimage) => Some(HtlcSuccessWithWitnessData(tx, remoteSig, preimage)) - case None => - log.error(s"preimage not found for htlcId=${tx.htlcId}, skipping...") - None - } - case None => - log.error(s"remote signature not found for htlcId=${tx.htlcId}, skipping...") - None - } - case tx: HtlcTimeoutTx => - commitment.localCommit.htlcTxsAndRemoteSigs.collectFirst { - case HtlcTxAndRemoteSig(HtlcTimeoutTx(input, _, _, _), remoteSig) if input.outPoint == tx.input.outPoint => remoteSig - } match { - case Some(remoteSig) => Some(HtlcTimeoutWithWitnessData(tx, remoteSig)) - case None => - log.error(s"remote signature not found for htlcId=${tx.htlcId}, skipping...") - None - } - } - } - /** - * We verify that: - * - our commit is not confirmed: if it is, there is no need to publish our claim-htlc transactions - * - the HTLC output isn't already spent by a confirmed transaction (race between HTLC-timeout and HTLC-success) + * We first verify that the commit tx we're spending may confirm: if a conflicting commit tx is already confirmed, our + * HTLC transaction has become obsolete. Then we check that the HTLC output that we're spending isn't already spent + * by a confirmed transaction, which may happen in case of a race between HTLC-timeout and HTLC-success. */ - private def checkClaimHtlcOutput(commitment: FullCommitment, claimHtlcTx: ClaimHtlcTx): Future[Command] = { - bitcoinClient.getTxConfirmations(commitment.localCommit.commitTxAndRemoteSig.commitTx.tx.txid).flatMap { - case Some(depth) if depth >= nodeParams.channelConf.minDepth => Future.successful(LocalCommitTxConfirmed) - case Some(_) => Future.successful(LocalCommitTxPublished) - case _ => bitcoinClient.isTransactionOutputSpent(claimHtlcTx.input.outPoint.txid, claimHtlcTx.input.outPoint.index.toInt).map { - case true => HtlcOutputAlreadySpent - case false => ParentTxOk - } - } - } - - private def checkClaimHtlcPreconditions(claimHtlcTx: ClaimHtlcTx): Behavior[Command] = { - context.pipeToSelf(checkClaimHtlcOutput(cmd.commitment, claimHtlcTx)) { + private def checkHtlcPreconditions(desc: String, input: OutPoint, commitTx: Transaction, concurrentCommitTxs: Set[TxId]): Behavior[Command] = { + context.pipeToSelf(bitcoinClient.getTxConfirmations(commitTx.txid).flatMap { + case Some(_) => + // If the HTLC output is already spent by a confirmed transaction, there is no need for RBF: either this is one + // of our transactions (which thus has a high enough feerate), or it was a race with our peer and we lost. + bitcoinClient.isTransactionOutputSpent(input.txid, input.index.toInt).map { + case true => HtlcOutputAlreadySpent + case false => ParentTxOk + } + case None => + // The parent commitment is unconfirmed: we shouldn't try to publish this HTLC transaction if a concurrent + // commitment is deeply confirmed. + checkConcurrentCommits(concurrentCommitTxs.toSeq).map { + case Some(confirmations) if confirmations >= nodeParams.channelConf.minDepth => ConcurrentCommitDeeplyConfirmed + case Some(_) => ConcurrentCommitRecentlyConfirmed + case None => ParentTxOk + } + }) { case Success(result) => result case Failure(reason) => UnknownFailure(reason) } Behaviors.receiveMessagePartial { case ParentTxOk => - // We verify that if this is a claim-htlc-success transaction, we have the preimage. - extractClaimHtlcWitnessData(claimHtlcTx, cmd.commitment) match { - case Some(txWithWitnessData) => replyTo ! PreconditionsOk(txWithWitnessData) - case None => replyTo ! PreconditionsFailed(TxPublisher.TxRejectedReason.TxSkipped(retryNextBlock = false)) - } + replyTo ! PreconditionsOk Behaviors.stopped - case LocalCommitTxPublished => - log.info("cannot publish {}: local commit has been published", cmd.desc) - // We keep retrying until the local commit reaches min-depth to protect against reorgs. + case ConcurrentCommitRecentlyConfirmed => + log.debug("cannot publish {} spending commitTxId={}: concurrent commit tx was recently confirmed, let's check again later", desc, commitTx.txid) + // We keep retrying until the concurrent commit reaches min-depth to protect against reorgs. replyTo ! PreconditionsFailed(TxPublisher.TxRejectedReason.TxSkipped(retryNextBlock = true)) Behaviors.stopped - case LocalCommitTxConfirmed => - log.warn("cannot publish {}: local commit has been confirmed", cmd.desc) + case ConcurrentCommitDeeplyConfirmed => + log.warn("cannot publish {} spending commitTxId={}: concurrent commit is deeply confirmed", desc, commitTx.txid) replyTo ! PreconditionsFailed(TxPublisher.TxRejectedReason.ConflictingTxConfirmed) Behaviors.stopped case HtlcOutputAlreadySpent => - log.warn("cannot publish {}: htlc output has already been spent", cmd.desc) + log.warn("cannot publish {}: htlc output {} has already been spent", desc, input) replyTo ! PreconditionsFailed(TxPublisher.TxRejectedReason.ConflictingTxConfirmed) Behaviors.stopped case UnknownFailure(reason) => - log.error(s"could not check ${cmd.desc} preconditions, proceeding anyway: ", reason) + log.error(s"could not check $desc preconditions, proceeding anyway: ", reason) // If our checks fail, we don't want it to prevent us from trying to publish our htlc transactions. - extractClaimHtlcWitnessData(claimHtlcTx, cmd.commitment) match { - case Some(txWithWitnessData) => replyTo ! PreconditionsOk(txWithWitnessData) - case None => replyTo ! PreconditionsFailed(TxPublisher.TxRejectedReason.TxSkipped(retryNextBlock = false)) - } + replyTo ! PreconditionsOk Behaviors.stopped } } - private def extractClaimHtlcWitnessData(claimHtlcTx: ClaimHtlcTx, commitment: FullCommitment): Option[ReplaceableTxWithWitnessData] = { - claimHtlcTx match { - case tx: LegacyClaimHtlcSuccessTx => - commitment.changes.localChanges.all.collectFirst { - case u: UpdateFulfillHtlc if u.id == tx.htlcId => u.paymentPreimage - } match { - case Some(preimage) => Some(LegacyClaimHtlcSuccessWithWitnessData(tx, preimage)) - case None => - log.error(s"preimage not found for legacy htlcId=${tx.htlcId}, skipping...") - None - } - case tx: ClaimHtlcSuccessTx => - commitment.changes.localChanges.all.collectFirst { - case u: UpdateFulfillHtlc if Crypto.sha256(u.paymentPreimage) == tx.paymentHash => u.paymentPreimage - } match { - case Some(preimage) => Some(ClaimHtlcSuccessWithWitnessData(tx, preimage)) - case None => - log.error(s"preimage not found for htlcId=${tx.htlcId}, skipping...") - None + /** Check the confirmation status of concurrent commitment transactions. */ + private def checkConcurrentCommits(txIds: Seq[TxId]): Future[Option[Int]] = { + txIds.headOption match { + case Some(txId) => + bitcoinClient.getTxConfirmations(txId).transformWith { + case Success(Some(confirmations)) => Future.successful(Some(confirmations)) + case _ => checkConcurrentCommits(txIds.tail) } - case tx: ClaimHtlcTimeoutTx => Some(ClaimHtlcTimeoutWithWitnessData(tx)) + case None => Future.successful(None) } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisher.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisher.scala index 340bd62d36..c5a294a147 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisher.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisher.scala @@ -22,9 +22,8 @@ import fr.acinq.bitcoin.scalacompat.{SatoshiLong, Transaction} import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinCoreClient import fr.acinq.eclair.blockchain.fee.{ConfirmationTarget, FeeratePerKw, FeeratesPerKw} import fr.acinq.eclair.channel.publish.ReplaceableTxFunder.FundedTx -import fr.acinq.eclair.channel.publish.ReplaceableTxPrePublisher.{ClaimLocalAnchorWithWitnessData, ReplaceableTxWithWitnessData} import fr.acinq.eclair.channel.publish.TxPublisher.TxPublishContext -import fr.acinq.eclair.transactions.Transactions +import fr.acinq.eclair.transactions.Transactions.ClaimAnchorTx import fr.acinq.eclair.{BlockHeight, NodeParams} import scala.concurrent.duration.{DurationInt, DurationLong} @@ -115,16 +114,17 @@ private class ReplaceableTxPublisher(nodeParams: NodeParams, private val log = context.log - /** The confirmation target may be updated in some corner cases (e.g. for a htlc if we learn a payment preimage). */ - private var confirmationTarget: ConfirmationTarget = cmd.txInfo.confirmationTarget + /** The confirmation target may be updated in some corner cases (e.g. if we learn a payment preimage after initiating a force-close). */ + private var confirmationTarget: ConfirmationTarget = cmd.confirmationTarget - def checkPreconditions(): Behavior[Command] = { + private def checkPreconditions(): Behavior[Command] = { val prePublisher = context.spawn(ReplaceableTxPrePublisher(nodeParams, bitcoinClient, txPublishContext), "pre-publisher") - prePublisher ! ReplaceableTxPrePublisher.CheckPreconditions(context.messageAdapter[ReplaceableTxPrePublisher.PreconditionsResult](WrappedPreconditionsResult), cmd) + val concurrentCommitTxs = cmd.commitment.commitTxIds.txIds - cmd.commitTx.txid + prePublisher ! ReplaceableTxPrePublisher.CheckPreconditions(context.messageAdapter[ReplaceableTxPrePublisher.PreconditionsResult](WrappedPreconditionsResult), cmd.txInfo, cmd.commitTx, concurrentCommitTxs) Behaviors.receiveMessagePartial { case WrappedPreconditionsResult(result) => result match { - case ReplaceableTxPrePublisher.PreconditionsOk(txWithWitnessData) => checkTimeLocks(txWithWitnessData) + case ReplaceableTxPrePublisher.PreconditionsOk => checkTimeLocks() case ReplaceableTxPrePublisher.PreconditionsFailed(reason) => sendResult(TxPublisher.TxRejected(txPublishContext.id, cmd, reason), None) } case UpdateConfirmationTarget(target) => @@ -134,15 +134,15 @@ private class ReplaceableTxPublisher(nodeParams: NodeParams, } } - def checkTimeLocks(txWithWitnessData: ReplaceableTxWithWitnessData): Behavior[Command] = { - txWithWitnessData match { + def checkTimeLocks(): Behavior[Command] = { + cmd.txInfo match { // There are no time locks on anchor transactions, we can claim them right away. - case _: ClaimLocalAnchorWithWitnessData => chooseFeerate(txWithWitnessData) + case _: ClaimAnchorTx => chooseFeerate() case _ => val timeLocksChecker = context.spawn(TxTimeLocksMonitor(nodeParams, bitcoinClient, txPublishContext), "time-locks-monitor") timeLocksChecker ! TxTimeLocksMonitor.CheckTx(context.messageAdapter[TxTimeLocksMonitor.TimeLocksOk](_ => TimeLocksOk), cmd.txInfo.tx, cmd.desc) Behaviors.receiveMessagePartial { - case TimeLocksOk => chooseFeerate(txWithWitnessData) + case TimeLocksOk => chooseFeerate() case UpdateConfirmationTarget(target) => confirmationTarget = target Behaviors.same @@ -151,7 +151,7 @@ private class ReplaceableTxPublisher(nodeParams: NodeParams, } } - def chooseFeerate(txWithWitnessData: ReplaceableTxWithWitnessData): Behavior[Command] = { + private def chooseFeerate(): Behavior[Command] = { context.pipeToSelf(hasEnoughSafeUtxos(nodeParams.onChainFeeConf.safeUtxosThreshold)) { case Success(isSafe) => CheckUtxosResult(isSafe, nodeParams.currentBlockHeight) case Failure(_) => CheckUtxosResult(isSafe = false, nodeParams.currentBlockHeight) // if we can't check our utxos, we assume the worst @@ -159,7 +159,7 @@ private class ReplaceableTxPublisher(nodeParams: NodeParams, Behaviors.receiveMessagePartial { case CheckUtxosResult(isSafe, currentBlockHeight) => val targetFeerate = getFeerate(nodeParams.currentBitcoinCoreFeerates, confirmationTarget, currentBlockHeight, isSafe) - fund(txWithWitnessData, targetFeerate) + fund(targetFeerate) case UpdateConfirmationTarget(target) => confirmationTarget = target Behaviors.same @@ -167,9 +167,9 @@ private class ReplaceableTxPublisher(nodeParams: NodeParams, } } - def fund(txWithWitnessData: ReplaceableTxWithWitnessData, targetFeerate: FeeratePerKw): Behavior[Command] = { + def fund(targetFeerate: FeeratePerKw): Behavior[Command] = { val txFunder = context.spawn(ReplaceableTxFunder(nodeParams, bitcoinClient, txPublishContext), "tx-funder") - txFunder ! ReplaceableTxFunder.FundTransaction(context.messageAdapter[ReplaceableTxFunder.FundingResult](WrappedFundingResult), cmd, Right(txWithWitnessData), targetFeerate) + txFunder ! ReplaceableTxFunder.FundTransaction(context.messageAdapter[ReplaceableTxFunder.FundingResult](WrappedFundingResult), Right(cmd.txInfo), cmd.commitTx, cmd.commitment, targetFeerate) Behaviors.receiveMessagePartial { case WrappedFundingResult(result) => result match { @@ -181,7 +181,7 @@ private class ReplaceableTxPublisher(nodeParams: NodeParams, val txMonitor = context.spawn(MempoolTxMonitor(nodeParams, bitcoinClient, txPublishContext), s"mempool-tx-monitor-${tx.signedTx.txid}") val parentTx_opt = cmd.txInfo match { // Anchor output transactions are packaged with the corresponding commitment transaction. - case _: Transactions.ClaimAnchorOutputTx => Some(cmd.commitTx) + case _: ClaimAnchorTx => Some(cmd.commitTx) case _ => None } txMonitor ! MempoolTxMonitor.Publish(context.messageAdapter[MempoolTxMonitor.TxResult](WrappedTxResult), tx.signedTx, parentTx_opt, cmd.input, nodeParams.channelConf.minDepth, cmd.desc, tx.fee) @@ -201,14 +201,14 @@ private class ReplaceableTxPublisher(nodeParams: NodeParams, // Wait for our transaction to be confirmed or rejected from the mempool. // If we get close to the confirmation target and our transaction is stuck in the mempool, we will initiate an RBF attempt. - def wait(tx: FundedTx): Behavior[Command] = { + private def wait(tx: FundedTx): Behavior[Command] = { Behaviors.receiveMessagePartial { case WrappedTxResult(txResult) => txResult match { case MempoolTxMonitor.TxInMempool(_, currentBlockHeight, parentConfirmed) => val shouldRbf = cmd.txInfo match { - // Our commit tx was confirmed on its own, so there's no need to increase fees on the anchor tx. - case _: Transactions.ClaimLocalAnchorOutputTx if parentConfirmed => false + // We only need to increase fees on the anchor tx if the commit tx isn't confirmed. + case _: ClaimAnchorTx => !parentConfirmed case _ => true } if (shouldRbf) { @@ -259,10 +259,10 @@ private class ReplaceableTxPublisher(nodeParams: NodeParams, } // Fund a replacement transaction because our previous attempt seems to be stuck in the mempool. - def fundReplacement(targetFeerate: FeeratePerKw, previousTx: FundedTx): Behavior[Command] = { + private def fundReplacement(targetFeerate: FeeratePerKw, previousTx: FundedTx): Behavior[Command] = { log.info("bumping {} fees: previous feerate={}, next feerate={}", cmd.desc, previousTx.feerate, targetFeerate) val txFunder = context.spawn(ReplaceableTxFunder(nodeParams, bitcoinClient, txPublishContext), "tx-funder-rbf") - txFunder ! ReplaceableTxFunder.FundTransaction(context.messageAdapter[ReplaceableTxFunder.FundingResult](WrappedFundingResult), cmd, Left(previousTx), targetFeerate) + txFunder ! ReplaceableTxFunder.FundTransaction(context.messageAdapter[ReplaceableTxFunder.FundingResult](WrappedFundingResult), Left(previousTx), cmd.commitTx, cmd.commitment, targetFeerate) Behaviors.receiveMessagePartial { case WrappedFundingResult(result) => result match { @@ -290,11 +290,11 @@ private class ReplaceableTxPublisher(nodeParams: NodeParams, // Publish an RBF attempt. We then have two concurrent transactions: the previous one and the updated one. // Only one of them can be in the mempool, so we wait for the other to be rejected. Once that's done, we're back to a // situation where we have one transaction in the mempool and wait for it to confirm. - def publishReplacement(previousTx: FundedTx, bumpedTx: FundedTx): Behavior[Command] = { + private def publishReplacement(previousTx: FundedTx, bumpedTx: FundedTx): Behavior[Command] = { val txMonitor = context.spawn(MempoolTxMonitor(nodeParams, bitcoinClient, txPublishContext), s"mempool-tx-monitor-${bumpedTx.signedTx.txid}") val parentTx_opt = cmd.txInfo match { // Anchor output transactions are packaged with the corresponding commitment transaction. - case _: Transactions.ClaimAnchorOutputTx => Some(cmd.commitTx) + case _: ClaimAnchorTx => Some(cmd.commitTx) case _ => None } txMonitor ! MempoolTxMonitor.Publish(context.messageAdapter[MempoolTxMonitor.TxResult](WrappedTxResult), bumpedTx.signedTx, parentTx_opt, cmd.input, nodeParams.channelConf.minDepth, cmd.desc, bumpedTx.fee) @@ -333,7 +333,7 @@ private class ReplaceableTxPublisher(nodeParams: NodeParams, } // Clean up the failed transaction attempt. Once that's done, go back to the waiting state with the new transaction. - def cleanUpFailedTxAndWait(failedTx: Transaction, mempoolTx: FundedTx): Behavior[Command] = { + private def cleanUpFailedTxAndWait(failedTx: Transaction, mempoolTx: FundedTx): Behavior[Command] = { // Note that we don't need to filter inputs from the previous transaction, they have already been unlocked when the // previous transaction was published (but the bitcoin wallet ensures that they won't be double-spent). val toUnlock = failedTx.txIn.map(_.outPoint).toSet diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/TxPublisher.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/TxPublisher.scala index cd60df31d8..0e30819926 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/TxPublisher.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/TxPublisher.scala @@ -25,7 +25,7 @@ import fr.acinq.eclair.blockchain.CurrentBlockHeight import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinCoreClient import fr.acinq.eclair.blockchain.fee.ConfirmationTarget import fr.acinq.eclair.channel.FullCommitment -import fr.acinq.eclair.transactions.Transactions.{ClaimLocalAnchorOutputTx, ReplaceableTransactionWithInputInfo, TransactionWithInputInfo} +import fr.acinq.eclair.transactions.Transactions.ForceCloseTransaction import fr.acinq.eclair.{BlockHeight, Logs, NodeParams} import java.util.UUID @@ -80,29 +80,25 @@ object TxPublisher { * NB: the parent tx should only be provided when it's being concurrently published, it's unnecessary when it is * confirmed or when the tx has a relative delay. * - * @param amount amount we are claiming with this transaction. * @param fee the fee that we're actually paying: it must be set to the mining fee, unless our peer is paying it (in * which case it must be set to zero here). */ - case class PublishFinalTx(tx: Transaction, input: OutPoint, amount: Satoshi, desc: String, fee: Satoshi, parentTx_opt: Option[TxId]) extends PublishTx + case class PublishFinalTx(tx: Transaction, input: OutPoint, desc: String, fee: Satoshi, parentTx_opt: Option[TxId]) extends PublishTx object PublishFinalTx { - def apply(txInfo: TransactionWithInputInfo, fee: Satoshi, parentTx_opt: Option[TxId]): PublishFinalTx = PublishFinalTx(txInfo.tx, txInfo.input.outPoint, txInfo.amountIn, txInfo.desc, fee, parentTx_opt) + def apply(txInfo: ForceCloseTransaction, parentTx_opt: Option[TxId]): PublishFinalTx = PublishFinalTx(txInfo.sign(), txInfo.input.outPoint, txInfo.desc, txInfo.fee, parentTx_opt) } /** * Publish an unsigned transaction that can be RBF-ed. * - * @param commitTx commitment transaction that this transaction is spending. + * @param txInfo transaction to publish. + * @param commitTx signed commitment transaction from which [[txInfo]] is a descendant. + * @param commitment commitment matching the provided [[commitTx]]. + * @param confirmationTarget confirmation target for this transaction used to choose its feerate and RBF attempts. */ - case class PublishReplaceableTx(txInfo: ReplaceableTransactionWithInputInfo, commitment: FullCommitment, commitTx: Transaction) extends PublishTx { - override def input: OutPoint = txInfo.input.outPoint - override def desc: String = txInfo.desc - - /** True if we're trying to bump our local commit with an anchor transaction. */ - lazy val isLocalCommitAnchor = txInfo match { - case txInfo: ClaimLocalAnchorOutputTx => txInfo.input.outPoint.txid == commitment.localCommit.commitTxAndRemoteSig.commitTx.tx.txid - case _ => false - } + case class PublishReplaceableTx(txInfo: ForceCloseTransaction, commitTx: Transaction, commitment: FullCommitment, confirmationTarget: ConfirmationTarget) extends PublishTx { + override val input: OutPoint = txInfo.input.outPoint + override val desc: String = txInfo.desc } sealed trait PublishTxResult extends Command { def cmd: PublishTx } @@ -136,7 +132,7 @@ object TxPublisher { // @formatter:on // @formatter:off - case class ChannelContext(remoteNodeId: PublicKey, channelId_opt: Option[ByteVector32]) { + private case class ChannelContext(remoteNodeId: PublicKey, channelId_opt: Option[ByteVector32]) { def mdc(): Map[String, String] = Logs.mdc(remoteNodeId_opt = Some(remoteNodeId), channelId_opt = channelId_opt) } case class TxPublishContext(id: UUID, remoteNodeId: PublicKey, channelId_opt: Option[ByteVector32]) { @@ -232,13 +228,10 @@ private class TxPublisher(nodeParams: NodeParams, factory: TxPublisher.ChildFact } case cmd: PublishReplaceableTx => - val proposedConfirmationTarget = cmd.txInfo.confirmationTarget + val proposedConfirmationTarget = cmd.confirmationTarget val attempts = pending.getOrElse(cmd.input, PublishAttempts.empty) attempts.replaceableAttempt_opt match { case Some(currentAttempt) => - if (currentAttempt.cmd.txInfo.tx.txOut.headOption.map(_.publicKeyScript) != cmd.txInfo.tx.txOut.headOption.map(_.publicKeyScript)) { - log.error("replaceable {} sends to a different address than the previous attempt, this should not happen: proposed={}, previous={}", currentAttempt.cmd.desc, cmd.txInfo, currentAttempt.cmd.txInfo) - } val currentConfirmationTarget = currentAttempt.confirmationTarget def updateConfirmationTarget() = { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Generators.scala b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Generators.scala deleted file mode 100644 index c5dafa7bcd..0000000000 --- a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Generators.scala +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright 2019 ACINQ SAS - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package fr.acinq.eclair.crypto - -import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} -import fr.acinq.bitcoin.scalacompat.{ByteVector32, Crypto} -import scodec.bits.ByteVector - -/** - * Created by PM on 07/12/2016. - */ -object Generators { - - def fixSize(data: ByteVector): ByteVector32 = data.length match { - case 32 => ByteVector32(data) - case length if length < 32 => ByteVector32(data.padLeft(32)) - } - - def perCommitSecret(seed: ByteVector32, index: Long): PrivateKey = PrivateKey(ShaChain.shaChainFromSeed(seed, 0xFFFFFFFFFFFFL - index)) - - def perCommitPoint(seed: ByteVector32, index: Long): PublicKey = perCommitSecret(seed, index).publicKey - - def derivePrivKey(secret: PrivateKey, perCommitPoint: PublicKey): PrivateKey = { - // secretkey = basepoint-secret + SHA256(per-commitment-point || basepoint) - secret.add(PrivateKey(Crypto.sha256(perCommitPoint.value ++ secret.publicKey.value))) - } - - def derivePubKey(basePoint: PublicKey, perCommitPoint: PublicKey): PublicKey = { - //pubkey = basepoint + SHA256(per-commitment-point || basepoint)*G - val a = PrivateKey(Crypto.sha256(perCommitPoint.value ++ basePoint.value)) - basePoint.add(a.publicKey) - } - - def revocationPubKey(basePoint: PublicKey, perCommitPoint: PublicKey): PublicKey = { - val a = PrivateKey(Crypto.sha256(basePoint.value ++ perCommitPoint.value)) - val b = PrivateKey(Crypto.sha256(perCommitPoint.value ++ basePoint.value)) - basePoint.multiply(a).add(perCommitPoint.multiply(b)) - } - - def revocationPrivKey(secret: PrivateKey, perCommitSecret: PrivateKey): PrivateKey = { - val a = PrivateKey(Crypto.sha256(secret.publicKey.value ++ perCommitSecret.publicKey.value)) - val b = PrivateKey(Crypto.sha256(perCommitSecret.publicKey.value ++ secret.publicKey.value)) - secret.multiply(a).add(perCommitSecret.multiply(b)) - } - -} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Monitoring.scala b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Monitoring.scala index 020f6072e9..c59e48ad5e 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Monitoring.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Monitoring.scala @@ -22,21 +22,10 @@ import kamon.metric.MeasurementUnit object Monitoring { object Metrics { - val SignTxCount = Kamon.counter("crypto.keymanager.sign.count") - val SignTxDuration = Kamon.timer("crypto.keymanager.sign.duration") val MessageSize = Kamon.histogram("messages.size", MeasurementUnit.information.bytes) } object Tags { - val TxOwner = "txOwner" - val TxType = "txType" - - object TxTypes { - val CommitTx = "commit" - val HtlcTx = "htlc" - val RevokedTx = "revoked" - } - val MessageDirection = "direction" object MessageDirections { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/NonceGenerator.scala b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/NonceGenerator.scala new file mode 100644 index 0000000000..fd6c4588af --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/NonceGenerator.scala @@ -0,0 +1,31 @@ +package fr.acinq.eclair.crypto + +import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} +import fr.acinq.bitcoin.scalacompat.Musig2.LocalNonce +import fr.acinq.bitcoin.scalacompat.{ByteVector32, Musig2, TxId} +import fr.acinq.eclair.randomBytes32 +import grizzled.slf4j.Logging + +object NonceGenerator extends Logging { + + // When using single-funding, we don't have access to the funding tx and remote funding key when creating our first + // verification nonce, so we use placeholder values instead. Note that this is fixed with dual-funding. + val dummyFundingTxId: TxId = TxId(ByteVector32.Zeroes) + val dummyRemoteFundingPubKey: PublicKey = PrivateKey(ByteVector32.One.bytes).publicKey + + /** + * @return a deterministic nonce used to sign our local commit tx: its public part is sent to our peer. + */ + def verificationNonce(fundingTxId: TxId, fundingPrivKey: PrivateKey, remoteFundingPubKey: PublicKey, commitIndex: Long): LocalNonce = { + Musig2.generateNonceWithCounter(commitIndex, fundingPrivKey, Seq(fundingPrivKey.publicKey, remoteFundingPubKey), None, Some(fundingTxId.value)) + } + + /** + * @return a random nonce used to sign our peer's commit tx. + */ + def signingNonce(localFundingPubKey: PublicKey, remoteFundingPubKey: PublicKey, fundingTxId: TxId): LocalNonce = { + val sessionId = randomBytes32() + Musig2.generateNonce(sessionId, Right(localFundingPubKey), Seq(localFundingPubKey, remoteFundingPubKey), None, Some(fundingTxId.value)) + } + +} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala index 1fdfa1bab1..f79f02e976 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala @@ -23,8 +23,10 @@ import fr.acinq.eclair.wire.protocol._ import grizzled.slf4j.Logging import scodec.Attempt import scodec.bits.ByteVector +import scodec.codecs.uint32 import scala.annotation.tailrec +import scala.concurrent.duration.{DurationLong, FiniteDuration} import scala.util.{Failure, Success, Try} /** @@ -278,28 +280,33 @@ object Sphinx extends Logging { /** * The downstream failure could not be decrypted. * - * @param unwrapped encrypted failure packet after unwrapping using our shared secrets. + * @param unwrapped encrypted failure packet after unwrapping using our shared secrets. + * @param attribution_opt attribution data after unwrapping using our shared secrets */ - case class CannotDecryptFailurePacket(unwrapped: ByteVector) + case class CannotDecryptFailurePacket(unwrapped: ByteVector, attribution_opt: Option[ByteVector]) + + case class HoldTime(duration: FiniteDuration, remoteNodeId: PublicKey) + + case class HtlcFailure(holdTimes: Seq[HoldTime], failure: Either[CannotDecryptFailurePacket, DecryptedFailurePacket]) object FailurePacket { /** - * Create a failure packet that will be returned to the sender. + * Create a failure packet that needs to be wrapped before being returned to the sender. * Each intermediate hop will add a layer of encryption and forward to the previous hop. * Note that malicious intermediate hops may drop the packet or alter it (which breaks the mac). * * @param sharedSecret destination node's shared secret that was computed when the original onion for the HTLC * was created or forwarded: see OnionPacket.create() and OnionPacket.wrap(). * @param failure failure message. - * @return a failure packet that can be sent to the destination node. + * @return a failure packet that still needs to be wrapped before being sent to the destination node. */ def create(sharedSecret: ByteVector32, failure: FailureMessage): ByteVector = { val um = generateKey("um", sharedSecret) val packet = FailureMessageCodecs.failureOnionCodec(Hmac256(um)).encode(failure).require.toByteVector logger.debug(s"um key: $um") logger.debug(s"raw error packet: ${packet.toHex}") - wrap(packet, sharedSecret) + packet } /** @@ -322,25 +329,128 @@ object Sphinx extends Logging { * it was sent by the corresponding node. * Note that malicious nodes in the route may have altered the packet, triggering a decryption failure. * - * @param packet failure packet. - * @param sharedSecrets nodes shared secrets. + * @param packet failure packet. + * @param attribution_opt attribution data for this failure packet. + * @param sharedSecrets nodes shared secrets. * @return failure message if the origin of the packet could be identified and the packet decrypted, the unwrapped * failure packet otherwise. */ - @tailrec - def decrypt(packet: ByteVector, sharedSecrets: Seq[SharedSecret]): Either[CannotDecryptFailurePacket, DecryptedFailurePacket] = { + def decrypt(packet: ByteVector, attribution_opt: Option[ByteVector], sharedSecrets: Seq[SharedSecret]): HtlcFailure = { sharedSecrets match { - case Nil => Left(CannotDecryptFailurePacket(packet)) + case Nil => HtlcFailure(Nil, Left(CannotDecryptFailurePacket(packet, attribution_opt))) case ss :: tail => val packet1 = wrap(packet, ss.secret) + val attribution1_opt = attribution_opt.flatMap(Attribution.unwrap(_, packet1, ss.secret, sharedSecrets.length)) val um = generateKey("um", ss.secret) - FailureMessageCodecs.failureOnionCodec(Hmac256(um)).decode(packet1.toBitVector) match { - case Attempt.Successful(value) => Right(DecryptedFailurePacket(ss.remoteNodeId, value.value)) - case _ => decrypt(packet1, tail) + val HtlcFailure(downstreamHoldTimes, failure) = FailureMessageCodecs.failureOnionCodec(Hmac256(um)).decode(packet1.toBitVector) match { + case Attempt.Successful(value) => HtlcFailure(Nil, Right(DecryptedFailurePacket(ss.remoteNodeId, value.value))) + case _ => decrypt(packet1, attribution1_opt.map(_._2), tail) } + HtlcFailure(attribution1_opt.map(n => HoldTime(n._1, ss.remoteNodeId) +: downstreamHoldTimes).getOrElse(Nil), failure) } } + } + + /** + * Attribution data is added to the failure packet and prevents a node from evading responsibility for its failures. + * Nodes that relay attribution data can prove that they are not the erring node and in case the erring node tries + * to hide, there will only be at most two nodes that can be the erring node (the last one to send attribution data + * and the one after it). It also adds timing data for each node on the path. + * Attribution data can also be added to fulfilled HTLCs to provide timing data and allow choosing fast nodes for + * future payments. + * https://github.com/lightning/bolts/pull/1044 + */ + object Attribution { + val maxNumHops = 20 + val holdTimeLength = 4 + val hmacLength = 4 // HMACs are truncated to 4 bytes to save space + val totalLength = maxNumHops * holdTimeLength + maxNumHops * (maxNumHops + 1) / 2 * hmacLength // = 920 + + private def cipher(bytes: ByteVector, sharedSecret: ByteVector32): ByteVector = { + val key = generateKey("ammagext", sharedSecret) + val stream = generateStream(key, totalLength) + bytes xor stream + } + + /** + * Get the HMACs from the attribution data. + * The layout of the attribution data is as follows (using maxNumHops = 3 for conciseness): + * holdTime(0) ++ holdTime(1) ++ holdTime(2) ++ + * hmacs(0)(0) ++ hmacs(0)(1) ++ hmacs(0)(2) ++ + * hmacs(1)(0) ++ hmacs(1)(1) ++ + * hmacs(2)(0) + * + * Where `hmac(i)(j)` is the hmac added by node `i` (counted from the node that built the attribution data), + * assuming it is `maxNumHops - 1 - i - j` hops away from the erring node. + */ + private def getHmacs(bytes: ByteVector): Seq[Seq[ByteVector]] = + (0 until maxNumHops).map(i => (0 until (maxNumHops - i)).map(j => { + val start = maxNumHops * holdTimeLength + (maxNumHops * i - (i * (i - 1)) / 2 + j) * hmacLength + bytes.slice(start, start + hmacLength) + })) + + /** + * Computes the HMACs for the node that is `maxNumHops - remainingHops` hops away from us. Hence we only compute `remainingHops` HMACs. + * HMACs are truncated to 4 bytes to save space. An attacker has only one try to guess the HMAC so 4 bytes should be enough. + */ + private def computeHmacs(mac: Mac32, failurePacket: ByteVector, holdTimes: ByteVector, hmacs: Seq[Seq[ByteVector]], remainingHops: Int): Seq[ByteVector] = { + ((maxNumHops - remainingHops) until maxNumHops).map(i => { + val y = maxNumHops - i + mac.mac(failurePacket ++ + holdTimes.take(y * holdTimeLength) ++ + ByteVector.concat((0 until y - 1).map(j => hmacs(j)(i)))).bytes.take(hmacLength) + }) + } + /** + * Create attribution data to send when settling an HTLC (in both failure and success cases). + * + * @param failurePacket_opt the failure packet before being wrapped or `None` for fulfilled HTLCs. + */ + def create(previousAttribution_opt: Option[ByteVector], failurePacket_opt: Option[ByteVector], holdTime: FiniteDuration, sharedSecret: ByteVector32): ByteVector = { + val previousAttribution = previousAttribution_opt.getOrElse(ByteVector.low(totalLength)) + val previousHmacs = getHmacs(previousAttribution).dropRight(1).map(_.drop(1)) + val mac = Hmac256(generateKey("um", sharedSecret)) + val holdTimes = uint32.encode(holdTime.toMillis / 100).require.bytes ++ previousAttribution.take((maxNumHops - 1) * holdTimeLength) + val hmacs = computeHmacs(mac, failurePacket_opt.getOrElse(ByteVector.empty), holdTimes, previousHmacs, maxNumHops) +: previousHmacs + cipher(holdTimes ++ ByteVector.concat(hmacs.map(ByteVector.concat(_))), sharedSecret) + } + + /** + * Unwrap one hop of attribution data. + * + * @return a pair with the hold time for this hop and the attribution data for the next hop, or None if the attribution data was invalid. + */ + def unwrap(encrypted: ByteVector, failurePacket: ByteVector, sharedSecret: ByteVector32, remainingHops: Int): Option[(FiniteDuration, ByteVector)] = { + val bytes = cipher(encrypted, sharedSecret) + val holdTime = (uint32.decode(bytes.take(holdTimeLength).bits).require.value * 100).milliseconds + val hmacs = getHmacs(bytes) + val mac = Hmac256(generateKey("um", sharedSecret)) + if (computeHmacs(mac, failurePacket, bytes.take(maxNumHops * holdTimeLength), hmacs.drop(1), remainingHops) == hmacs.head.drop(maxNumHops - remainingHops)) { + val unwrapped = bytes.slice(holdTimeLength, maxNumHops * holdTimeLength) ++ ByteVector.low(holdTimeLength) ++ ByteVector.concat((hmacs.drop(1) :+ Seq()).map(s => ByteVector.low(hmacLength) ++ ByteVector.concat(s))) + Some(holdTime, unwrapped) + } else { + None + } + } + + case class UnwrappedAttribution(holdTimes: List[HoldTime], remaining_opt: Option[ByteVector]) + + /** + * Unwrap many hops of attribution data (e.g. used for fulfilled HTLCs). + */ + def unwrap(attribution: ByteVector, sharedSecrets: Seq[SharedSecret]): UnwrappedAttribution = { + sharedSecrets match { + case Nil => UnwrappedAttribution(Nil, Some(attribution)) + case ss :: tail => + unwrap(attribution, ByteVector.empty, ss.secret, sharedSecrets.length) match { + case Some((holdTime, nextAttribution)) => + val UnwrappedAttribution(holdTimes, remaining_opt) = unwrap(nextAttribution, tail) + UnwrappedAttribution(HoldTime(holdTime, ss.remoteNodeId) :: holdTimes, remaining_opt) + case None => UnwrappedAttribution(Nil, None) + } + } + } } /** diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/TransportHandler.scala b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/TransportHandler.scala index 32c8e965dd..74d9b2f349 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/TransportHandler.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/TransportHandler.scala @@ -27,7 +27,7 @@ import fr.acinq.eclair.Logs.LogCategory import fr.acinq.eclair.crypto.ChaCha20Poly1305.ChaCha20Poly1305Error import fr.acinq.eclair.crypto.Noise._ import fr.acinq.eclair.remote.EclairInternalsSerializer.RemoteTypes -import fr.acinq.eclair.wire.protocol.{AnnouncementSignatures, RoutingMessage} +import fr.acinq.eclair.wire.protocol._ import fr.acinq.eclair.{Diagnostics, FSMDiagnosticActorLogging, Logs, getSimpleClassName} import scodec.bits.ByteVector import scodec.{Attempt, Codec, DecodeResult} @@ -40,7 +40,7 @@ import scala.reflect.ClassTag /** * see BOLT #8 * This class handles the transport layer: - * - initial handshake. upon completion we will have a pair of cipher states (one for encryption, one for decryption) + * - initial handshake. upon completion we will have a pair of cipher states (one for encryption, one for decryption) * - encryption/decryption of messages * * Once the initial handshake has been completed successfully, the handler will create a listener actor with the @@ -50,23 +50,22 @@ import scala.reflect.ClassTag * @param rs remote node static public key (which must be known before we initiate communication) * @param connection actor that represents the other node's */ -class TransportHandler[T: ClassTag](keyPair: KeyPair, rs: Option[ByteVector], connection: ActorRef, codec: Codec[T]) extends Actor with FSMDiagnosticActorLogging[TransportHandler.State, TransportHandler.Data] { +class TransportHandler(keyPair: KeyPair, rs: Option[ByteVector], connection: ActorRef, codec: Codec[LightningMessage]) extends Actor with FSMDiagnosticActorLogging[TransportHandler.State, TransportHandler.Data] { // will hold the peer's public key once it is available (we don't know it right away in case of an incoming connection) var remoteNodeId_opt: Option[PublicKey] = rs.map(PublicKey(_)) - val wireLog = new BusLogging(context.system.eventStream, "", classOf[Diagnostics], context.system.asInstanceOf[ExtendedActorSystem].logFilter) with DiagnosticLoggingAdapter + private val wireLog = new BusLogging(context.system.eventStream, "", classOf[Diagnostics], context.system.asInstanceOf[ExtendedActorSystem].logFilter) with DiagnosticLoggingAdapter - def diag(message: T, direction: String): Unit = { - require(direction == "IN" || direction == "OUT") + private def logMessage(message: LightningMessage, direction: String): Unit = { val channelId_opt = Logs.channelId(message) wireLog.mdc(Logs.mdc(LogCategory(message), remoteNodeId_opt, channelId_opt)) if (channelId_opt.isDefined) { // channel-related messages are logged as info - wireLog.info(s"$direction msg={}", message) + wireLog.info("{} msg={}", direction, message) } else { // other messages (e.g. routing gossip) are logged as debug - wireLog.debug(s"$direction msg={}", message) + wireLog.debug("{} msg={}", direction, message) } wireLog.clearMDC() } @@ -79,11 +78,11 @@ class TransportHandler[T: ClassTag](keyPair: KeyPair, rs: Option[ByteVector], co def buf(message: ByteVector): ByteString = ByteString.fromArray(message.toArray) // it means we initiate the dialog - val isWriter = rs.isDefined + private val isWriter = rs.isDefined context.watch(connection) - val reader = if (isWriter) { + private val reader = if (isWriter) { val state = makeWriter(keyPair, rs.get) val (state1, message, None) = state.write(ByteVector.empty) log.debug(s"sending prefix + $message") @@ -93,13 +92,23 @@ class TransportHandler[T: ClassTag](keyPair: KeyPair, rs: Option[ByteVector], co makeReader(keyPair) } - def decodeAndSendToListener(listener: ActorRef, plaintextMessages: Seq[ByteVector]): Map[T, Int] = { + /** We keep track of pending pings to defend against ping flooding. */ + private var pendingPings = 0 + + private def decodeAndSendToListener(listener: ActorRef, plaintextMessages: Seq[ByteVector]): Map[LightningMessage, Int] = { log.debug("decoding {} plaintext messages", plaintextMessages.size) - var m: Map[T, Int] = Map() + var m = Map.empty[LightningMessage, Int] plaintextMessages.foreach(plaintext => codec.decode(plaintext.bits) match { case Attempt.Successful(DecodeResult(message, _)) => - diag(message, "IN") + logMessage(message, "IN") Monitoring.Metrics.MessageSize.withTag(Monitoring.Tags.MessageDirection, Monitoring.Tags.MessageDirections.IN).record(plaintext.size) + if (message.isInstanceOf[Ping]) { + pendingPings += 1 + if (pendingPings > 1) { + // We will kill the connection anyway, no need to process remaining messages + return m + } + } listener ! message m += (message -> (m.getOrElse(message, 0) + 1)) case Attempt.Failure(err) => @@ -109,6 +118,18 @@ class TransportHandler[T: ClassTag](keyPair: KeyPair, rs: Option[ByteVector], co m } + private def encodeAndSendToPeer(encryptor: Encryptor, t: LightningMessage): Encryptor = { + if (t.isInstanceOf[Pong]) { + pendingPings -= 1 + } + logMessage(t, "OUT") + val blob = codec.encode(t).require.toByteVector + Monitoring.Metrics.MessageSize.withTag(Monitoring.Tags.MessageDirection, Monitoring.Tags.MessageDirections.OUT).record(blob.size) + val (enc1, ciphertext) = encryptor.encrypt(blob) + connection ! Tcp.Write(buf(ciphertext), WriteAck) + enc1 + } + startWith(Handshake, HandshakeData(reader)) when(Handshake) { @@ -132,25 +153,22 @@ class TransportHandler[T: ClassTag](keyPair: KeyPair, rs: Option[ByteVector], co val nextStateData = WaitingForListenerData(Encryptor(ExtendedCipherState(enc, ck)), Decryptor(ExtendedCipherState(dec, ck), ciphertextLength = None, remainder)) goto(WaitingForListener) using nextStateData - case (writer, _, None) => { + case (writer, _, None) => writer.write(ByteVector.empty) match { - case (reader1, message, None) => { + case (reader1, message, None) => // we're still in the middle of the handshake process and the other end must first received our next // message before they can reply if (remainder.nonEmpty) throw UnexpectedDataDuringHandshake(ByteVector(remainder)) connection ! Tcp.Write(buf(TransportHandler.prefix +: message)) stay() using HandshakeData(reader1, remainder) - } - case (_, message, Some((enc, dec, ck))) => { + case (_, message, Some((enc, dec, ck))) => connection ! Tcp.Write(buf(TransportHandler.prefix +: message)) val remoteNodeId = PublicKey(writer.rs) remoteNodeId_opt = Some(remoteNodeId) context.parent ! HandshakeCompleted(remoteNodeId) val nextStateData = WaitingForListenerData(Encryptor(ExtendedCipherState(enc, ck)), Decryptor(ExtendedCipherState(dec, ck), ciphertextLength = None, remainder)) goto(WaitingForListener) using nextStateData - } } - } } } } @@ -165,27 +183,37 @@ class TransportHandler[T: ClassTag](keyPair: KeyPair, rs: Option[ByteVector], co context.watch(listener) val (dec1, plaintextMessages) = dec.decrypt() val unackedReceived1 = decodeAndSendToListener(listener, plaintextMessages) - if (unackedReceived1.isEmpty) { - log.debug("no decoded messages, resuming reading") - connection ! Tcp.ResumeReading + if (pendingPings > 1) { + log.warning("ping flood detected (pendingPings={}): closing connection", pendingPings) + stop(FSM.Normal) + } else { + if (unackedReceived1.isEmpty) { + log.debug("no decoded messages, resuming reading") + connection ! Tcp.ResumeReading + } + goto(Normal) using NormalData(d.encryptor, dec1, listener, sendBuffer = SendBuffer(Queue.empty[LightningMessage], Queue.empty[LightningMessage]), unackedReceived = unackedReceived1, unackedSent = None) } - goto(Normal) using NormalData(d.encryptor, dec1, listener, sendBuffer = SendBuffer(Queue.empty[T], Queue.empty[T]), unackedReceived = unackedReceived1, unackedSent = None) } } when(Normal) { handleExceptions { - case Event(Tcp.Received(data), d: NormalData[T @unchecked]) => + case Event(Tcp.Received(data), d: NormalData) => log.debug("received chunk of size={}", data.size) val (dec1, plaintextMessages) = d.decryptor.copy(buffer = d.decryptor.buffer ++ data).decrypt() val unackedReceived1 = decodeAndSendToListener(d.listener, plaintextMessages) - if (unackedReceived1.isEmpty) { - log.debug("no decoded messages, resuming reading") - connection ! Tcp.ResumeReading + if (pendingPings > 1) { + log.warning("ping flood detected (pendingPings={}): closing connection", pendingPings) + stop(FSM.Normal) + } else { + if (unackedReceived1.isEmpty) { + log.debug("no decoded messages, resuming reading") + connection ! Tcp.ResumeReading + } + stay() using d.copy(decryptor = dec1, unackedReceived = unackedReceived1) } - stay() using d.copy(decryptor = dec1, unackedReceived = unackedReceived1) - case Event(ReadAck(msg: T), d: NormalData[T @unchecked]) => + case Event(ReadAck(msg: LightningMessage), d: NormalData) => // how many occurrences of this message are still unacked? val remaining = d.unackedReceived.getOrElse(msg, 0) - 1 log.debug("acking message {}", msg) @@ -199,7 +227,7 @@ class TransportHandler[T: ClassTag](keyPair: KeyPair, rs: Option[ByteVector], co } stay() using d.copy(unackedReceived = unackedReceived1) - case Event(t: T, d: NormalData[T @unchecked]) => + case Event(t: LightningMessage, d: NormalData) => if (d.sendBuffer.normalPriority.size + d.sendBuffer.lowPriority.size >= MAX_BUFFERED) { log.warning("send buffer overrun, closing connection") connection ! PoisonPill @@ -213,32 +241,19 @@ class TransportHandler[T: ClassTag](keyPair: KeyPair, rs: Option[ByteVector], co } stay() using d.copy(sendBuffer = sendBuffer1) } else { - diag(t, "OUT") - val blob = codec.encode(t).require.toByteVector - Monitoring.Metrics.MessageSize.withTag(Monitoring.Tags.MessageDirection, Monitoring.Tags.MessageDirections.OUT).record(blob.size) - val (enc1, ciphertext) = d.encryptor.encrypt(blob) - connection ! Tcp.Write(buf(ciphertext), WriteAck) + val enc1 = encodeAndSendToPeer(d.encryptor, t) stay() using d.copy(encryptor = enc1, unackedSent = Some(t)) } - case Event(WriteAck, d: NormalData[T @unchecked]) => - def send(t: T) = { - diag(t, "OUT") - val blob = codec.encode(t).require.toByteVector - Monitoring.Metrics.MessageSize.withTag(Monitoring.Tags.MessageDirection, Monitoring.Tags.MessageDirections.OUT).record(blob.size) - val (enc1, ciphertext) = d.encryptor.encrypt(blob) - connection ! Tcp.Write(buf(ciphertext), WriteAck) - enc1 - } - + case Event(WriteAck, d: NormalData) => d.sendBuffer.normalPriority.dequeueOption match { case Some((t, normalPriority1)) => - val enc1 = send(t) + val enc1 = encodeAndSendToPeer(d.encryptor, t) stay() using d.copy(encryptor = enc1, sendBuffer = d.sendBuffer.copy(normalPriority = normalPriority1), unackedSent = Some(t)) case None => d.sendBuffer.lowPriority.dequeueOption match { case Some((t, lowPriority1)) => - val enc1 = send(t) + val enc1 = encodeAndSendToPeer(d.encryptor, t) stay() using d.copy(encryptor = enc1, sendBuffer = d.sendBuffer.copy(lowPriority = lowPriority1), unackedSent = Some(t)) case None => stay() using d.copy(unackedSent = None) @@ -260,7 +275,7 @@ class TransportHandler[T: ClassTag](keyPair: KeyPair, rs: Option[ByteVector], co case Event(msg, d) => d match { - case n: NormalData[_] => log.warning(s"unhandled message $msg in state normal unackedSent=${n.unackedSent.size} unackedReceived=${n.unackedReceived.size} sendBuffer.lowPriority=${n.sendBuffer.lowPriority.size} sendBuffer.normalPriority=${n.sendBuffer.normalPriority.size}") + case n: NormalData => log.warning(s"unhandled message $msg in state normal unackedSent=${n.unackedSent.size} unackedReceived=${n.unackedReceived.size} sendBuffer.lowPriority=${n.sendBuffer.lowPriority.size} sendBuffer.normalPriority=${n.sendBuffer.normalPriority.size}") case _ => log.warning(s"unhandled message $msg in state ${d.getClass.getSimpleName}") } stay() @@ -273,7 +288,7 @@ class TransportHandler[T: ClassTag](keyPair: KeyPair, rs: Option[ByteVector], co Logs.withMdc(diagLog)(Logs.mdc(category_opt = Some(Logs.LogCategory.CONNECTION), remoteNodeId_opt = remoteNodeId_opt)) { connection ! Tcp.Close // attempts to gracefully close the connection when dying stateData match { - case normal: NormalData[_] => + case normal: NormalData => // NB: we deduplicate on the class name: each class will appear once but there may be many instances (less verbose and gives debug hints) log.info("stopping (unackedReceived={} unackedSent={})", normal.unackedReceived.keys.map(getSimpleClassName).toSet.mkString(","), normal.unackedSent.map(getSimpleClassName)) case _ => @@ -297,8 +312,7 @@ class TransportHandler[T: ClassTag](keyPair: KeyPair, rs: Option[ByteVector], co case t: Throwable => t match { // for well known crypto error, we don't display the stack trace - case _: InvalidTransportPrefix => log.error(s"crypto error: ${t.getMessage}") - case _: ChaCha20Poly1305Error => log.error(s"crypto error: ${t.getMessage}") + case _: InvalidTransportPrefix | _: ChaCha20Poly1305Error => log.error(s"crypto error: ${t.getMessage}") case _ => log.error(t, "") } throw t @@ -309,19 +323,19 @@ class TransportHandler[T: ClassTag](keyPair: KeyPair, rs: Option[ByteVector], co object TransportHandler { - def props[T: ClassTag](keyPair: KeyPair, rs: Option[ByteVector], connection: ActorRef, codec: Codec[T]): Props = Props(new TransportHandler(keyPair, rs, connection, codec)) + def props[T: ClassTag](keyPair: KeyPair, rs: Option[ByteVector], connection: ActorRef, codec: Codec[LightningMessage]): Props = Props(new TransportHandler(keyPair, rs, connection, codec)) - val MAX_BUFFERED = 1000000L + private val MAX_BUFFERED = 1000000L // see BOLT #8 // this prefix is prepended to all Noise messages sent during the handshake phase val prefix: Byte = 0x00 + private val prologue = ByteVector.view("lightning".getBytes("UTF-8")) - case class InvalidTransportPrefix(buffer: ByteVector) extends RuntimeException(s"invalid transport prefix first64=${buffer.take(64).toHex}") - - case class UnexpectedDataDuringHandshake(buffer: ByteVector) extends RuntimeException(s"unexpected additional data received during handshake first64=${buffer.take(64).toHex}") - - val prologue = ByteVector.view("lightning".getBytes("UTF-8")) + // @formatter:off + private case class InvalidTransportPrefix(buffer: ByteVector) extends RuntimeException(s"invalid transport prefix first64=${buffer.take(64).toHex}") + private case class UnexpectedDataDuringHandshake(buffer: ByteVector) extends RuntimeException(s"unexpected additional data received during handshake first64=${buffer.take(64).toHex}") + // @formatter:on /** * See BOLT #8: during the handshake phase we are expecting 3 messages of 50, 50 and 66 bytes (including the prefix) @@ -329,17 +343,17 @@ object TransportHandler { * @param reader handshake state reader * @return the size of the message the reader is expecting */ - def expectedLength(reader: Noise.HandshakeStateReader) = reader.messages.length match { + private def expectedLength(reader: Noise.HandshakeStateReader): Int = reader.messages.length match { case 3 | 2 => 50 case 1 => 66 } - def makeWriter(localStatic: KeyPair, remoteStatic: ByteVector) = Noise.HandshakeState.initializeWriter( + private def makeWriter(localStatic: KeyPair, remoteStatic: ByteVector): HandshakeStateWriter = Noise.HandshakeState.initializeWriter( Noise.handshakePatternXK, prologue, localStatic, KeyPair(ByteVector.empty, ByteVector.empty), remoteStatic, ByteVector.empty, Noise.Secp256k1DHFunctions, Noise.Chacha20Poly1305CipherFunctions, Noise.SHA256HashFunctions) - def makeReader(localStatic: KeyPair) = Noise.HandshakeState.initializeReader( + private def makeReader(localStatic: KeyPair): HandshakeStateReader = Noise.HandshakeState.initializeReader( Noise.handshakePatternXK, prologue, localStatic, KeyPair(ByteVector.empty, ByteVector.empty), ByteVector.empty, ByteVector.empty, Noise.Secp256k1DHFunctions, Noise.Chacha20Poly1305CipherFunctions, Noise.SHA256HashFunctions) @@ -351,37 +365,32 @@ object TransportHandler { * @param ck chaining key */ case class ExtendedCipherState(cs: CipherState, ck: ByteVector) extends CipherState { - override def cipher: CipherFunctions = cs.cipher - - override def hasKey: Boolean = cs.hasKey + override val cipher: CipherFunctions = cs.cipher + override val hasKey: Boolean = cs.hasKey override def encryptWithAd(ad: ByteVector, plaintext: ByteVector): (CipherState, ByteVector) = { cs match { case UninitializedCipherState(_) => (this, plaintext) - case InitializedCipherState(k, n, _) if n == 999 => { + case InitializedCipherState(k, n, _) if n == 999 => val (_, ciphertext) = cs.encryptWithAd(ad, plaintext) val (ck1, k1) = SHA256HashFunctions.hkdf(ck, k) (this.copy(cs = cs.initializeKey(k1), ck = ck1), ciphertext) - } - case InitializedCipherState(_, n, _) => { + case _: InitializedCipherState => val (cs1, ciphertext) = cs.encryptWithAd(ad, plaintext) (this.copy(cs = cs1), ciphertext) - } } } override def decryptWithAd(ad: ByteVector, ciphertext: ByteVector): (CipherState, ByteVector) = { cs match { case UninitializedCipherState(_) => (this, ciphertext) - case InitializedCipherState(k, n, _) if n == 999 => { + case InitializedCipherState(k, n, _) if n == 999 => val (_, plaintext) = cs.decryptWithAd(ad, ciphertext) val (ck1, k1) = SHA256HashFunctions.hkdf(ck, k) (this.copy(cs = cs.initializeKey(k1), ck = ck1), plaintext) - } - case InitializedCipherState(_, n, _) => { + case _: InitializedCipherState => val (cs1, plaintext) = cs.decryptWithAd(ad, ciphertext) (this.copy(cs = cs1), plaintext) - } } } } @@ -438,23 +447,23 @@ object TransportHandler { // @formatter:off sealed trait State - case object Handshake extends State + private case object Handshake extends State case object WaitingForListener extends State case object Normal extends State sealed trait Data - case class HandshakeData(reader: Noise.HandshakeStateReader, buffer: ByteString = ByteString.empty) extends Data - case class WaitingForListenerData(encryptor: Encryptor, decryptor: Decryptor) extends Data - case class NormalData[T](encryptor: Encryptor, decryptor: Decryptor, listener: ActorRef, sendBuffer: SendBuffer[T], unackedReceived: Map[T, Int], unackedSent: Option[T]) extends Data + private case class HandshakeData(reader: Noise.HandshakeStateReader, buffer: ByteString = ByteString.empty) extends Data + private case class WaitingForListenerData(encryptor: Encryptor, decryptor: Decryptor) extends Data + private case class NormalData(encryptor: Encryptor, decryptor: Decryptor, listener: ActorRef, sendBuffer: SendBuffer, unackedReceived: Map[LightningMessage, Int], unackedSent: Option[LightningMessage]) extends Data - case class SendBuffer[T](normalPriority: Queue[T], lowPriority: Queue[T]) + private case class SendBuffer(normalPriority: Queue[LightningMessage], lowPriority: Queue[LightningMessage]) case class Listener(listener: ActorRef) case class HandshakeCompleted(remoteNodeId: PublicKey) case class ReadAck(msg: Any) extends RemoteTypes - case object WriteAck extends Tcp.Event + private case object WriteAck extends Tcp.Event // @formatter:on } \ No newline at end of file diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/WeakEntropyPool.scala b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/WeakEntropyPool.scala index 7f588cf692..cd34acc589 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/WeakEntropyPool.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/WeakEntropyPool.scala @@ -23,7 +23,7 @@ import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.bitcoin.scalacompat.{BlockId, ByteVector32, ByteVector64, Crypto} import fr.acinq.eclair.TimestampMilli import fr.acinq.eclair.blockchain.NewBlock -import fr.acinq.eclair.channel.ChannelSignatureReceived +import fr.acinq.eclair.channel.{ChannelSignatureReceived, ChannelSpendSignature} import fr.acinq.eclair.io.PeerConnected import fr.acinq.eclair.payment.ChannelPaymentRelayed import fr.acinq.eclair.router.NodeUpdated @@ -50,7 +50,7 @@ object WeakEntropyPool { private case class WrappedNewBlock(blockId: BlockId) extends Command private case class WrappedPaymentRelayed(paymentHash: ByteVector32, relayedAt: TimestampMilli) extends Command private case class WrappedPeerConnected(nodeId: PublicKey) extends Command - private case class WrappedChannelSignature(wtxid: ByteVector32) extends Command + private case class WrappedChannelSignature(sig: ChannelSpendSignature) extends Command private case class WrappedNodeUpdated(sig: ByteVector64) extends Command // @formatter:on @@ -60,7 +60,7 @@ object WeakEntropyPool { context.system.eventStream ! EventStream.Subscribe(context.messageAdapter[ChannelPaymentRelayed](e => WrappedPaymentRelayed(e.paymentHash, e.timestamp))) context.system.eventStream ! EventStream.Subscribe(context.messageAdapter[PeerConnected](e => WrappedPeerConnected(e.nodeId))) context.system.eventStream ! EventStream.Subscribe(context.messageAdapter[NodeUpdated](e => WrappedNodeUpdated(e.ann.signature))) - context.system.eventStream ! EventStream.Subscribe(context.messageAdapter[ChannelSignatureReceived](e => WrappedChannelSignature(e.commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx.wtxid))) + context.system.eventStream ! EventStream.Subscribe(context.messageAdapter[ChannelSignatureReceived](e => WrappedChannelSignature(e.commitments.latest.localCommit.remoteSig))) Behaviors.withTimers { timers => timers.startTimerWithFixedDelay(FlushEntropy, 30 seconds) collecting(collector, None) @@ -87,7 +87,10 @@ object WeakEntropyPool { case WrappedNodeUpdated(sig) => collecting(collector, collect(entropy_opt, sig ++ ByteVector.fromLong(System.currentTimeMillis()))) - case WrappedChannelSignature(wtxid) => collecting(collector, collect(entropy_opt, wtxid ++ ByteVector.fromLong(System.currentTimeMillis()))) + case WrappedChannelSignature(sig) => sig match { + case ChannelSpendSignature.IndividualSignature(sig) => collecting(collector, collect(entropy_opt, sig ++ ByteVector.fromLong(System.currentTimeMillis()))) + case ChannelSpendSignature.PartialSignatureWithNonce(partialSig, _) => collecting(collector, collect(entropy_opt, partialSig ++ ByteVector.fromLong(System.currentTimeMillis()))) + } } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/ChannelKeyManager.scala b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/ChannelKeyManager.scala index 78a2b981bb..510a49dfbe 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/ChannelKeyManager.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/ChannelKeyManager.scala @@ -17,95 +17,34 @@ package fr.acinq.eclair.crypto.keymanager import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} -import fr.acinq.bitcoin.scalacompat.DeterministicWallet.ExtendedPublicKey -import fr.acinq.bitcoin.scalacompat.{ByteVector64, Crypto, DeterministicWallet, OutPoint, Protocol, TxOut} -import fr.acinq.eclair.channel.{ChannelConfig, LocalParams} -import fr.acinq.eclair.transactions.Transactions.{CommitmentFormat, TransactionWithInputInfo, TxOwner} -import scodec.bits.ByteVector +import fr.acinq.bitcoin.scalacompat.{Crypto, DeterministicWallet, Protocol} +import fr.acinq.eclair.channel.ChannelConfig import java.io.ByteArrayInputStream import java.nio.ByteOrder trait ChannelKeyManager { - def fundingPublicKey(fundingKeyPath: DeterministicWallet.KeyPath, fundingTxIndex: Long): ExtendedPublicKey - - def revocationPoint(channelKeyPath: DeterministicWallet.KeyPath): ExtendedPublicKey - - def paymentPoint(channelKeyPath: DeterministicWallet.KeyPath): ExtendedPublicKey - - def delayedPaymentPoint(channelKeyPath: DeterministicWallet.KeyPath): ExtendedPublicKey - - def htlcPoint(channelKeyPath: DeterministicWallet.KeyPath): ExtendedPublicKey - - def commitmentSecret(channelKeyPath: DeterministicWallet.KeyPath, index: Long): Crypto.PrivateKey - - def commitmentPoint(channelKeyPath: DeterministicWallet.KeyPath, index: Long): Crypto.PublicKey - - def keyPath(localParams: LocalParams, channelConfig: ChannelConfig): DeterministicWallet.KeyPath = { - if (channelConfig.hasOption(ChannelConfig.FundingPubKeyBasedChannelKeyPath)) { - // deterministic mode: use the funding pubkey to compute the channel key path - ChannelKeyManager.keyPath(fundingPublicKey(localParams.fundingKeyPath, fundingTxIndex = 0)) - } else { - // legacy mode: we reuse the funding key path as our channel key path - localParams.fundingKeyPath - } - } - - /** - * @param isChannelOpener true if we initiated the channel open - * @return a partial key path for a new funding public key. This key path will be extended: - * - with a specific "chain" prefix - * - with a specific "funding pubkey" suffix - */ - def newFundingKeyPath(isChannelOpener: Boolean): DeterministicWallet.KeyPath - - /** - * @param tx input transaction - * @param publicKey extended public key - * @param txOwner owner of the transaction (local/remote) - * @param commitmentFormat format of the commitment tx - * @param extraUtxos extra outputs spent by this transaction (in addition to [[fr.acinq.eclair.transactions.Transactions.InputInfo]]) - * @return a signature generated with the private key that matches the input extended public key - */ - def sign(tx: TransactionWithInputInfo, publicKey: ExtendedPublicKey, txOwner: TxOwner, commitmentFormat: CommitmentFormat, extraUtxos: Map[OutPoint, TxOut]): ByteVector64 - /** - * This method is used to spend funds sent to htlc keys/delayed keys + * Create a BIP32 funding key path a new channel. + * This function must return a unique path every time it is called. + * This guarantees that unrelated channels use different BIP32 key paths and thus unrelated keys. * - * @param tx input transaction - * @param publicKey extended public key - * @param remotePoint remote point - * @param txOwner owner of the transaction (local/remote) - * @param commitmentFormat format of the commitment tx - * @param extraUtxos extra outputs spent by this transaction (in addition to [[fr.acinq.eclair.transactions.Transactions.InputInfo]]) - * @return a signature generated with a private key generated from the input key's matching private key and the remote point. + * @param isChannelOpener true if we initiated the channel open: this must be used to derive different key paths. */ - def sign(tx: TransactionWithInputInfo, publicKey: ExtendedPublicKey, remotePoint: PublicKey, txOwner: TxOwner, commitmentFormat: CommitmentFormat, extraUtxos: Map[OutPoint, TxOut]): ByteVector64 + def newFundingKeyPath(isChannelOpener: Boolean): DeterministicWallet.KeyPath /** - * Ths method is used to spend revoked transactions, with the corresponding revocation key - * - * @param tx input transaction - * @param publicKey extended public key - * @param remoteSecret remote secret - * @param txOwner owner of the transaction (local/remote) - * @param commitmentFormat format of the commitment tx - * @param extraUtxos extra outputs spent by this transaction (in addition to [[fr.acinq.eclair.transactions.Transactions.InputInfo]]) - * @return a signature generated with a private key generated from the input key's matching private key and the remote secret. + * Create channel keys based on a funding key path obtained using [[newFundingKeyPath]]. + * This function is deterministic: it must always return the same result when called with the same arguments. + * This allows re-creating the channel keys based on the seed and its main parameters. */ - def sign(tx: TransactionWithInputInfo, publicKey: ExtendedPublicKey, remoteSecret: PrivateKey, txOwner: TxOwner, commitmentFormat: CommitmentFormat, extraUtxos: Map[OutPoint, TxOut]): ByteVector64 + def channelKeys(channelConfig: ChannelConfig, fundingKeyPath: DeterministicWallet.KeyPath): ChannelKeys - /** - * Sign a channel announcement message - * - * @param witness channel announcement message - * @return the signature of the channel announcement with the channel's funding private key - */ - def signChannelAnnouncement(witness: ByteVector, fundingKeyPath: DeterministicWallet.KeyPath): ByteVector64 } object ChannelKeyManager { + /** * Create a BIP32 path from a public key. This path will be used to derive channel keys. * Having channel keys derived from the funding public keys makes it very easy to retrieve your funds when you've lost your data: @@ -116,7 +55,8 @@ object ChannelKeyManager { * @param fundingPubKey funding public key * @return a BIP32 path */ - def keyPath(fundingPubKey: PublicKey): DeterministicWallet.KeyPath = { + def keyPathFromPublicKey(fundingPubKey: PublicKey): DeterministicWallet.KeyPath = { + // We simply hash the public key, and then read the result in 4-bytes chunks to create a BIP32 path. val buffer = Crypto.sha256(fundingPubKey.value) val bis = new ByteArrayInputStream(buffer.toArray) @@ -125,5 +65,4 @@ object ChannelKeyManager { DeterministicWallet.KeyPath(Seq(next(), next(), next(), next(), next(), next(), next(), next())) } - def keyPath(fundingPubKey: DeterministicWallet.ExtendedPublicKey): DeterministicWallet.KeyPath = keyPath(fundingPubKey.publicKey) } \ No newline at end of file diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/ChannelKeys.scala b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/ChannelKeys.scala new file mode 100644 index 0000000000..7f5f44f061 --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/ChannelKeys.scala @@ -0,0 +1,114 @@ +/* + * Copyright 2019 ACINQ SAS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package fr.acinq.eclair.crypto.keymanager + +import com.google.common.cache.{CacheBuilder, CacheLoader, LoadingCache} +import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} +import fr.acinq.bitcoin.scalacompat.DeterministicWallet.{ExtendedPrivateKey, hardened} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, Crypto} +import fr.acinq.eclair.crypto.ShaChain + +/** + * Keys used for a specific channel instance: + * - funding keys (channel funding, splicing and closing) + * - commitment "base" keys, which are static for the channel lifetime + * - per-commitment keys, which change everytime we create a new commitment transaction: + * - derived from the commitment "base" keys + * - and tweaked with a per-commitment point + * + * WARNING: these private keys must never be stored on disk, in a database, or logged. + */ +case class ChannelKeys(private val fundingMasterKey: ExtendedPrivateKey, private val commitmentMasterKey: ExtendedPrivateKey) { + + private val fundingKeys: LoadingCache[Long, PrivateKey] = CacheBuilder.newBuilder() + .maximumSize(2) // we cache the current funding key and the funding key of a pending splice + .build[Long, PrivateKey](new CacheLoader[Long, PrivateKey] { + override def load(fundingTxIndex: Long): PrivateKey = fundingMasterKey.derivePrivateKey(hardened(fundingTxIndex)).privateKey + }) + + def fundingKey(fundingTxIndex: Long): PrivateKey = fundingKeys.get(fundingTxIndex) + + // Note that we use lazy values here to avoid deriving keys for all of our channels immediately after a restart. + lazy val revocationBaseSecret: PrivateKey = commitmentMasterKey.derivePrivateKey(hardened(1)).privateKey + lazy val revocationBasePoint: PublicKey = revocationBaseSecret.publicKey + lazy val paymentBaseSecret: PrivateKey = commitmentMasterKey.derivePrivateKey(hardened(2)).privateKey + lazy val paymentBasePoint: PublicKey = paymentBaseSecret.publicKey + lazy val delayedPaymentBaseSecret: PrivateKey = commitmentMasterKey.derivePrivateKey(hardened(3)).privateKey + lazy val delayedPaymentBasePoint: PublicKey = delayedPaymentBaseSecret.publicKey + lazy val htlcBaseSecret: PrivateKey = commitmentMasterKey.derivePrivateKey(hardened(4)).privateKey + lazy val htlcBasePoint: PublicKey = htlcBaseSecret.publicKey + + // @formatter:off + // Per-commitment keys are derived using a sha-chain, which provides efficient storage and retrieval mechanisms. + private lazy val shaSeed: ByteVector32 = Crypto.sha256(commitmentMasterKey.derivePrivateKey(hardened(5)).privateKey.value :+ 1.toByte) + def commitmentSecret(localCommitmentNumber: Long): PrivateKey = PrivateKey(ShaChain.shaChainFromSeed(shaSeed, 0xFFFFFFFFFFFFL - localCommitmentNumber)) + def commitmentPoint(localCommitmentNumber: Long): PublicKey = commitmentSecret(localCommitmentNumber).publicKey + // @formatter:on + + /** + * Derive our local payment key for our main output in the remote commitment transaction. + * Warning: when using option_staticremotekey or anchor_outputs, we must always use the base key instead of a per-commitment key. + */ + def paymentKey(commitmentPoint: PublicKey): PrivateKey = ChannelKeys.derivePerCommitmentKey(paymentBaseSecret, commitmentPoint) + + /** Derive our local delayed payment key for our main output in the local commitment transaction. */ + def delayedPaymentKey(commitmentPoint: PublicKey): PrivateKey = ChannelKeys.derivePerCommitmentKey(delayedPaymentBaseSecret, commitmentPoint) + + /** Derive our HTLC key for our HTLC transactions, in either the local or remote commitment transaction. */ + def htlcKey(commitmentPoint: PublicKey): PrivateKey = ChannelKeys.derivePerCommitmentKey(htlcBaseSecret, commitmentPoint) + + /** With the remote per-commitment secret, we can derive the private key to spend revoked commitments. */ + def revocationKey(remoteCommitmentSecret: PrivateKey): PrivateKey = ChannelKeys.revocationKey(revocationBaseSecret, remoteCommitmentSecret) + +} + +object ChannelKeys { + + /** Derive the local per-commitment key for the base key provided. */ + def derivePerCommitmentKey(baseSecret: PrivateKey, commitmentPoint: PublicKey): PrivateKey = { + // secretkey = basepoint-secret + SHA256(per-commitment-point || basepoint) + baseSecret + PrivateKey(Crypto.sha256(commitmentPoint.value ++ baseSecret.publicKey.value)) + } + + /** Derive the remote per-commitment key for the base point provided. */ + def remotePerCommitmentPublicKey(basePoint: PublicKey, commitmentPoint: PublicKey): PublicKey = { + // pubkey = basepoint + SHA256(per-commitment-point || basepoint)*G + basePoint + PrivateKey(Crypto.sha256(commitmentPoint.value ++ basePoint.value)).publicKey + } + + /** Derive the revocation private key from our local base revocation key and the remote per-commitment secret. */ + def revocationKey(baseKey: PrivateKey, remoteCommitmentSecret: PrivateKey): PrivateKey = { + val a = PrivateKey(Crypto.sha256(baseKey.publicKey.value ++ remoteCommitmentSecret.publicKey.value)) + val b = PrivateKey(Crypto.sha256(remoteCommitmentSecret.publicKey.value ++ baseKey.publicKey.value)) + (baseKey * a) + (remoteCommitmentSecret * b) + } + + /** + * We create two distinct revocation public keys: + * - one for the local commitment using the remote revocation base point and our local per-commitment point + * - one for the remote commitment using our revocation base point and the remote per-commitment point + * + * The owner of the commitment transaction is providing the per-commitment point, which ensures that they can revoke + * their previous commitment transactions by revealing the corresponding secret. + */ + def revocationPublicKey(revocationBasePoint: PublicKey, commitmentPoint: PublicKey): PublicKey = { + val a = PrivateKey(Crypto.sha256(revocationBasePoint.value ++ commitmentPoint.value)) + val b = PrivateKey(Crypto.sha256(commitmentPoint.value ++ revocationBasePoint.value)) + (revocationBasePoint * a) + (commitmentPoint * b) + } + +} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/CommitmentKeys.scala b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/CommitmentKeys.scala new file mode 100644 index 0000000000..dc07cfaa07 --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/CommitmentKeys.scala @@ -0,0 +1,116 @@ +/* + * Copyright 2025 ACINQ SAS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package fr.acinq.eclair.crypto.keymanager + +import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} +import fr.acinq.eclair.channel.ChannelParams + +/** + * Created by t-bast on 10/04/2025. + */ + +/** + * Each commitment transaction uses a set of many cryptographic keys for the various spending paths of all its outputs. + * Some of those keys are static for the channel lifetime, but others change every time we update the commitment + * transaction. + * + * This class can be used indifferently for the local or remote commitment transaction. Beware that when it applies to + * the remote transaction, the "local" prefix is for their keys and the "remote" prefix is for our keys. + * + * See Bolt 3 for more details. + * + * @param localDelayedPaymentPublicKey key used for delayed outputs of the transaction owner (main balance and outputs of HTLC transactions). + * @param remotePaymentPublicKey key used for the main balance of the transaction non-owner (not delayed). + * @param localHtlcPublicKey key used to sign HTLC transactions by the transaction owner. + * @param remoteHtlcPublicKey key used to sign HTLC transactions by the transaction non-owner. + * @param revocationPublicKey key used to revoke this commitment after signing the next one (by revealing the per-commitment secret). + */ +case class CommitmentPublicKeys(localDelayedPaymentPublicKey: PublicKey, + remotePaymentPublicKey: PublicKey, + localHtlcPublicKey: PublicKey, + remoteHtlcPublicKey: PublicKey, + revocationPublicKey: PublicKey) + +/** + * Keys used for our local commitment. + * WARNING: these private keys must never be stored on disk, in a database, or logged. + */ +case class LocalCommitmentKeys(ourDelayedPaymentKey: PrivateKey, + theirPaymentPublicKey: PublicKey, + ourPaymentBasePoint: PublicKey, + ourHtlcKey: PrivateKey, + theirHtlcPublicKey: PublicKey, + revocationPublicKey: PublicKey) { + val publicKeys: CommitmentPublicKeys = CommitmentPublicKeys( + localDelayedPaymentPublicKey = ourDelayedPaymentKey.publicKey, + remotePaymentPublicKey = theirPaymentPublicKey, + localHtlcPublicKey = ourHtlcKey.publicKey, + remoteHtlcPublicKey = theirHtlcPublicKey, + revocationPublicKey = revocationPublicKey + ) +} + +object LocalCommitmentKeys { + def apply(params: ChannelParams, channelKeys: ChannelKeys, localCommitIndex: Long): LocalCommitmentKeys = { + val localPerCommitmentPoint = channelKeys.commitmentPoint(localCommitIndex) + LocalCommitmentKeys( + ourDelayedPaymentKey = channelKeys.delayedPaymentKey(localPerCommitmentPoint), + theirPaymentPublicKey = params.remoteParams.paymentBasepoint, + ourPaymentBasePoint = channelKeys.paymentBasePoint, + ourHtlcKey = channelKeys.htlcKey(localPerCommitmentPoint), + theirHtlcPublicKey = ChannelKeys.remotePerCommitmentPublicKey(params.remoteParams.htlcBasepoint, localPerCommitmentPoint), + revocationPublicKey = ChannelKeys.revocationPublicKey(params.remoteParams.revocationBasepoint, localPerCommitmentPoint) + ) + } +} + +/** + * Keys used for the remote commitment. + * WARNING: these private keys must never be stored on disk, in a database, or logged. + * + * There is a subtlety for [[ourPaymentKey]]: when using option_static_remotekey, our output will directly send funds + * to a p2wpkh address created by our bitcoin node. We thus don't need the private key, as the output can immediately + * be spent by our bitcoin node (no need for 2nd-stage transactions to send it back to our wallet). + */ +case class RemoteCommitmentKeys(ourPaymentKey: PrivateKey, + theirDelayedPaymentPublicKey: PublicKey, + ourPaymentBasePoint: PublicKey, + ourHtlcKey: PrivateKey, + theirHtlcPublicKey: PublicKey, + revocationPublicKey: PublicKey) { + // Since this is the remote commitment, local is them and remote is us. + val publicKeys: CommitmentPublicKeys = CommitmentPublicKeys( + localDelayedPaymentPublicKey = theirDelayedPaymentPublicKey, + remotePaymentPublicKey = ourPaymentKey.publicKey, + localHtlcPublicKey = theirHtlcPublicKey, + remoteHtlcPublicKey = ourHtlcKey.publicKey, + revocationPublicKey = revocationPublicKey + ) +} + +object RemoteCommitmentKeys { + def apply(params: ChannelParams, channelKeys: ChannelKeys, remotePerCommitmentPoint: PublicKey): RemoteCommitmentKeys = { + RemoteCommitmentKeys( + ourPaymentKey = channelKeys.paymentBaseSecret, + theirDelayedPaymentPublicKey = ChannelKeys.remotePerCommitmentPublicKey(params.remoteParams.delayedPaymentBasepoint, remotePerCommitmentPoint), + ourPaymentBasePoint = channelKeys.paymentBasePoint, + ourHtlcKey = channelKeys.htlcKey(remotePerCommitmentPoint), + theirHtlcPublicKey = ChannelKeys.remotePerCommitmentPublicKey(params.remoteParams.htlcBasepoint, remotePerCommitmentPoint), + revocationPublicKey = ChannelKeys.revocationPublicKey(channelKeys.revocationBasePoint, remotePerCommitmentPoint) + ) + } +} \ No newline at end of file diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/LocalChannelKeyManager.scala b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/LocalChannelKeyManager.scala index 01be5a95c4..d97b8fd348 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/LocalChannelKeyManager.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/LocalChannelKeyManager.scala @@ -16,150 +16,61 @@ package fr.acinq.eclair.crypto.keymanager -import com.google.common.cache.{CacheBuilder, CacheLoader, LoadingCache} -import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin.scalacompat.DeterministicWallet._ -import fr.acinq.bitcoin.scalacompat.{Block, BlockHash, ByteVector32, ByteVector64, Crypto, DeterministicWallet, OutPoint, TxOut} -import fr.acinq.eclair.crypto.Generators -import fr.acinq.eclair.crypto.Monitoring.{Metrics, Tags} -import fr.acinq.eclair.router.Announcements -import fr.acinq.eclair.transactions.Transactions.{CommitmentFormat, TransactionWithInputInfo, TxOwner} -import fr.acinq.eclair.{KamonExt, randomLong} -import grizzled.slf4j.Logging -import kamon.tag.TagSet +import fr.acinq.bitcoin.scalacompat.{Block, BlockHash, DeterministicWallet} +import fr.acinq.eclair.channel.ChannelConfig +import fr.acinq.eclair.randomLong import scodec.bits.ByteVector -object LocalChannelKeyManager { - def keyBasePath(chainHash: BlockHash): List[Long] = (chainHash: @unchecked) match { - case Block.RegtestGenesisBlock.hash | Block.Testnet4GenesisBlock.hash | Block.Testnet3GenesisBlock.hash | Block.SignetGenesisBlock.hash => DeterministicWallet.hardened(46) :: DeterministicWallet.hardened(1) :: Nil - case Block.LivenetGenesisBlock.hash => DeterministicWallet.hardened(47) :: DeterministicWallet.hardened(1) :: Nil - } -} - /** - * An implementation of [[ChannelKeyManager]] that supports deterministic derivation of keys, based on the initial - * funding pubkey. - * - * Specifically, there are two paths both of length 8 (256 bits): - * - `fundingKeyPath`: chosen at random using `newFundingKeyPath()` - * - `channelKeyPath`: sha(fundingPubkey(0)) using `ChannelKeyManager.keyPath()` + * An implementation of [[ChannelKeyManager]] that supports deterministic derivation of keys, based on an initial + * random funding key. * * The resulting paths looks like so on mainnet: + * * {{{ * funding txs: - * 47' / 1' / / <1' or 0'> / ' + * 47' / 1' / / ' * - * others channel basepoint keys (payment, revocation, htlc, etc.): - * 47' / 1' / / <1'-5'> + * commitment basepoint keys (payment, revocation, htlc, etc.): + * 47' / 1' / / <1'-5'> * }}} * - * @param seed seed from which the channel keys will be derived + * Where the commitmentKeyPath is generated by hashing the funding key at index 0. + * + * @param seed seed from which the channel funding keys will be derived. */ -class LocalChannelKeyManager(seed: ByteVector, chainHash: BlockHash) extends ChannelKeyManager with Logging { +case class LocalChannelKeyManager(seed: ByteVector, chainHash: BlockHash) extends ChannelKeyManager { private val master = DeterministicWallet.generate(seed) - private val privateKeys: LoadingCache[KeyPath, ExtendedPrivateKey] = CacheBuilder.newBuilder() - .maximumSize(6 * 200) // 6 keys per channel * 200 channels - .build[KeyPath, ExtendedPrivateKey](new CacheLoader[KeyPath, ExtendedPrivateKey] { - override def load(keyPath: KeyPath): ExtendedPrivateKey = derivePrivateKey(master, keyPath) - }) - - private val publicKeys: LoadingCache[KeyPath, ExtendedPublicKey] = CacheBuilder.newBuilder() - .maximumSize(6 * 200) // 6 keys per channel * 200 channels - .build[KeyPath, ExtendedPublicKey](new CacheLoader[KeyPath, ExtendedPublicKey] { - override def load(keyPath: KeyPath): ExtendedPublicKey = publicKey(privateKeys.get(keyPath)) - }) - - private def internalKeyPath(keyPath: DeterministicWallet.KeyPath, index: Long): KeyPath = KeyPath((LocalChannelKeyManager.keyBasePath(chainHash) ++ keyPath.path) :+ index) + // We use the following prefix to derive channel funding keys from our seed. + private val basePath = chainHash match { + case Block.LivenetGenesisBlock.hash => DeterministicWallet.hardened(47) :: DeterministicWallet.hardened(1) :: Nil + case _ => DeterministicWallet.hardened(46) :: DeterministicWallet.hardened(1) :: Nil + } - override def newFundingKeyPath(isInitiator: Boolean): KeyPath = { - val last = DeterministicWallet.hardened(if (isInitiator) 1 else 0) + /** We always generate a random key path for new channels, which ensures that each channel uses a unique key path. */ + override def newFundingKeyPath(isChannelOpener: Boolean): KeyPath = { + val last = DeterministicWallet.hardened(if (isChannelOpener) 1 else 0) def next(): Long = randomLong() & 0xFFFFFFFFL DeterministicWallet.KeyPath(Seq(next(), next(), next(), next(), next(), next(), next(), next(), last)) } - override def fundingPublicKey(fundingKeyPath: DeterministicWallet.KeyPath, fundingTxIndex: Long): ExtendedPublicKey = { - val keyPath = internalKeyPath(fundingKeyPath, hardened(fundingTxIndex)) - publicKeys.get(keyPath) - } - - override def revocationPoint(channelKeyPath: DeterministicWallet.KeyPath): ExtendedPublicKey = publicKeys.get(internalKeyPath(channelKeyPath, hardened(1))) - - override def paymentPoint(channelKeyPath: DeterministicWallet.KeyPath): ExtendedPublicKey = publicKeys.get(internalKeyPath(channelKeyPath, hardened(2))) - - override def delayedPaymentPoint(channelKeyPath: DeterministicWallet.KeyPath): ExtendedPublicKey = publicKeys.get(internalKeyPath(channelKeyPath, hardened(3))) - - override def htlcPoint(channelKeyPath: DeterministicWallet.KeyPath): ExtendedPublicKey = publicKeys.get(internalKeyPath(channelKeyPath, hardened(4))) - - private def shaSeed(channelKeyPath: DeterministicWallet.KeyPath): ByteVector32 = Crypto.sha256(privateKeys.get(internalKeyPath(channelKeyPath, hardened(5))).privateKey.value :+ 1.toByte) - - override def commitmentSecret(channelKeyPath: DeterministicWallet.KeyPath, index: Long): PrivateKey = Generators.perCommitSecret(shaSeed(channelKeyPath), index) - - override def commitmentPoint(channelKeyPath: DeterministicWallet.KeyPath, index: Long): PublicKey = Generators.perCommitPoint(shaSeed(channelKeyPath), index) - - /** - * @param tx input transaction - * @param publicKey extended public key - * @param txOwner owner of the transaction (local/remote) - * @param commitmentFormat format of the commitment tx - * @param extraUtxos extra outputs spent by this transaction (in addition to [[fr.acinq.eclair.transactions.Transactions.InputInfo]]) - * @return a signature generated with the private key that matches the input extended public key - */ - override def sign(tx: TransactionWithInputInfo, publicKey: ExtendedPublicKey, txOwner: TxOwner, commitmentFormat: CommitmentFormat, extraUtxos: Map[OutPoint, TxOut]): ByteVector64 = { - // NB: not all those transactions are actually commit txs (especially during closing), but this is good enough for monitoring purposes - val tags = TagSet.Empty.withTag(Tags.TxOwner, txOwner.toString).withTag(Tags.TxType, Tags.TxTypes.CommitTx) - Metrics.SignTxCount.withTags(tags).increment() - KamonExt.time(Metrics.SignTxDuration.withTags(tags)) { - val privateKey = privateKeys.get(publicKey.path) - tx.sign(privateKey.privateKey, txOwner, commitmentFormat, extraUtxos) + override def channelKeys(channelConfig: ChannelConfig, fundingKeyPath: KeyPath): ChannelKeys = { + val fundingMasterKey = master.derivePrivateKey(basePath ++ fundingKeyPath.path) + val commitmentMasterKey = if (channelConfig.hasOption(ChannelConfig.FundingPubKeyBasedChannelKeyPath)) { + // Deterministic mode: use the funding public key itself to compute the key path for commitment keys. + val fundingPublicKey = fundingMasterKey.derivePrivateKey(hardened(0)).publicKey + val keyPath = ChannelKeyManager.keyPathFromPublicKey(fundingPublicKey) + derivePrivateKey(master, basePath ++ keyPath.path) + } else { + // Legacy mode: we simply reuse the funding key path as our channel key path. + // Note that this mode must never be used for new channels as it conflicts with splicing keys. + fundingMasterKey } + ChannelKeys(fundingMasterKey, commitmentMasterKey) } - /** - * This method is used to spend funds sent to htlc keys/delayed keys - * - * @param tx input transaction - * @param publicKey extended public key - * @param remotePoint remote point - * @param txOwner owner of the transaction (local/remote) - * @param commitmentFormat format of the commitment tx - * @param extraUtxos extra outputs spent by this transaction (in addition to [[fr.acinq.eclair.transactions.Transactions.InputInfo]]) - * @return a signature generated with a private key generated from the input key's matching private key and the remote point. - */ - override def sign(tx: TransactionWithInputInfo, publicKey: ExtendedPublicKey, remotePoint: PublicKey, txOwner: TxOwner, commitmentFormat: CommitmentFormat, extraUtxos: Map[OutPoint, TxOut]): ByteVector64 = { - // NB: not all those transactions are actually htlc txs (especially during closing), but this is good enough for monitoring purposes - val tags = TagSet.Empty.withTag(Tags.TxOwner, txOwner.toString).withTag(Tags.TxType, Tags.TxTypes.HtlcTx) - Metrics.SignTxCount.withTags(tags).increment() - KamonExt.time(Metrics.SignTxDuration.withTags(tags)) { - val privateKey = privateKeys.get(publicKey.path) - val currentKey = Generators.derivePrivKey(privateKey.privateKey, remotePoint) - tx.sign(currentKey, txOwner, commitmentFormat, extraUtxos) - } - } - - /** - * Ths method is used to spend revoked transactions, with the corresponding revocation key - * - * @param tx input transaction - * @param publicKey extended public key - * @param remoteSecret remote secret - * @param txOwner owner of the transaction (local/remote) - * @param commitmentFormat format of the commitment tx - * @param extraUtxos extra outputs spent by this transaction (in addition to [[fr.acinq.eclair.transactions.Transactions.InputInfo]]) - * @return a signature generated with a private key generated from the input key's matching private key and the remote secret. - */ - override def sign(tx: TransactionWithInputInfo, publicKey: ExtendedPublicKey, remoteSecret: PrivateKey, txOwner: TxOwner, commitmentFormat: CommitmentFormat, extraUtxos: Map[OutPoint, TxOut]): ByteVector64 = { - val tags = TagSet.Empty.withTag(Tags.TxOwner, txOwner.toString).withTag(Tags.TxType, Tags.TxTypes.RevokedTx) - Metrics.SignTxCount.withTags(tags).increment() - KamonExt.time(Metrics.SignTxDuration.withTags(tags)) { - val privateKey = privateKeys.get(publicKey.path) - val currentKey = Generators.revocationPrivKey(privateKey.privateKey, remoteSecret) - tx.sign(currentKey, txOwner, commitmentFormat, extraUtxos) - } - } - - override def signChannelAnnouncement(witness: ByteVector, fundingKeyPath: KeyPath): ByteVector64 = - Announcements.signChannelAnnouncement(witness, privateKeys.get(fundingKeyPath).privateKey) -} \ No newline at end of file +} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/LocalNodeKeyManager.scala b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/LocalNodeKeyManager.scala index a30d0f7174..968d735a5f 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/LocalNodeKeyManager.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/LocalNodeKeyManager.scala @@ -20,15 +20,14 @@ import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin.scalacompat.DeterministicWallet.ExtendedPrivateKey import fr.acinq.bitcoin.scalacompat.{Block, BlockHash, ByteVector32, ByteVector64, Crypto, DeterministicWallet} import fr.acinq.eclair.router.Announcements -import grizzled.slf4j.Logging import scodec.bits.ByteVector object LocalNodeKeyManager { // WARNING: if you change this path, you will change your node id even if the seed remains the same!!! // Note that the node path and the above channel path are on different branches so even if the // node key is compromised there is no way to retrieve the wallet keys - def keyBasePath(chainHash: BlockHash): List[Long] = (chainHash: @unchecked) match { - case Block.RegtestGenesisBlock.hash | Block.Testnet3GenesisBlock.hash | Block.Testnet4GenesisBlock.hash | Block.SignetGenesisBlock.hash => DeterministicWallet.hardened(46) :: DeterministicWallet.hardened(0) :: Nil + private def keyBasePath(chainHash: BlockHash): List[Long] = (chainHash: @unchecked) match { + case Block.RegtestGenesisBlock.hash | Block.Testnet3GenesisBlock.hash | Block.Testnet4GenesisBlock.hash | Block.SignetGenesisBlock.hash => DeterministicWallet.hardened(46) :: DeterministicWallet.hardened(0) :: Nil case Block.LivenetGenesisBlock.hash => DeterministicWallet.hardened(47) :: DeterministicWallet.hardened(0) :: Nil } } @@ -39,7 +38,7 @@ object LocalNodeKeyManager { * * @param seed seed from which the node key will be derived */ -class LocalNodeKeyManager(seed: ByteVector, chainHash: BlockHash) extends NodeKeyManager with Logging { +case class LocalNodeKeyManager(seed: ByteVector, chainHash: BlockHash) extends NodeKeyManager { private val master = DeterministicWallet.generate(seed) override val nodeKey: ExtendedPrivateKey = DeterministicWallet.derivePrivateKey(master, LocalNodeKeyManager.keyBasePath(chainHash)) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/LocalOnChainKeyManager.scala b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/LocalOnChainKeyManager.scala index f1dc83b732..8eafa1bc1b 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/LocalOnChainKeyManager.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/LocalOnChainKeyManager.scala @@ -17,7 +17,6 @@ package fr.acinq.eclair.crypto.keymanager import com.typesafe.config.ConfigFactory -import fr.acinq.bitcoin.ScriptTree import fr.acinq.bitcoin.psbt.Psbt import fr.acinq.bitcoin.scalacompat.DeterministicWallet._ import fr.acinq.bitcoin.scalacompat.{Block, BlockHash, ByteVector64, Crypto, DeterministicWallet, MnemonicCode, Satoshi, Script, computeBIP84Address} @@ -290,13 +289,13 @@ class LocalOnChainKeyManager(override val walletName: String, seed: ByteVector, s.getPsbt.finalizeWitnessInput(pos, Script.witnessPay2wpkh(pub, s.getSig)) }) match { case Right(psbt) => psbt - case Left(failure) => throw new RuntimeException(s"cannot sign psbt input, error = $failure") + case Left(failure) => throw new RuntimeException(s"cannot sign psbt input at position $pos, error = $failure") } } private def signPsbtInput86(psbt: Psbt, pos: Int): Psbt = { + import fr.acinq.bitcoin.SigHash import fr.acinq.bitcoin.scalacompat.KotlinUtils._ - import fr.acinq.bitcoin.{Script, SigHash} val input = psbt.getInput(pos) require(input != null, s"input $pos is missing from psbt: bitcoin core may be malicious") @@ -307,7 +306,7 @@ class LocalOnChainKeyManager(override val walletName: String, seed: ByteVector, val (pub, keypath) = input.getTaprootDerivationPaths.asScala.toSeq.head val priv = fr.acinq.bitcoin.DeterministicWallet.derivePrivateKey(master.priv, keypath.keyPath).getPrivateKey require(priv.publicKey().xOnly() == pub, s"derived public key doesn't match (expected=$pub actual=${priv.publicKey().xOnly()}): bitcoin core may be malicious") - val expectedScript = ByteVector(Script.write(Script.pay2tr(pub, null.asInstanceOf[ScriptTree]))) + val expectedScript = Script.write(Script.pay2tr(pub, None)) require(kmp2scala(input.getWitnessUtxo.publicKeyScript) == expectedScript, s"script mismatch (expected=$expectedScript, actual=${input.getWitnessUtxo.publicKeyScript}): bitcoin core may be malicious") // No need to update the input, we can directly sign and finalize. @@ -315,7 +314,7 @@ class LocalOnChainKeyManager(override val walletName: String, seed: ByteVector, .sign(priv, pos) .flatMap(s => s.getPsbt.finalizeWitnessInput(pos, Script.witnessKeyPathPay2tr(ByteVector64(s.getSig), SigHash.SIGHASH_DEFAULT))) match { case Right(psbt) => psbt - case Left(failure) => throw new RuntimeException(s"cannot sign psbt input, error = $failure") + case Left(failure) => throw new RuntimeException(s"cannot sign psbt input at position $pos, error = $failure") } } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/ChannelsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/ChannelsDb.scala index 6f1f34ecce..71c8ed8281 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/ChannelsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/ChannelsDb.scala @@ -18,7 +18,7 @@ package fr.acinq.eclair.db import fr.acinq.bitcoin.scalacompat.ByteVector32 import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey -import fr.acinq.eclair.channel.PersistentChannelData +import fr.acinq.eclair.channel.{DATA_CLOSED, PersistentChannelData} import fr.acinq.eclair.db.DbEventHandler.ChannelEvent import fr.acinq.eclair.{CltvExpiry, Paginated} @@ -30,8 +30,13 @@ trait ChannelsDb { def updateChannelMeta(channelId: ByteVector32, event: ChannelEvent.EventType): Unit - /** Mark a channel as closed, but keep it in the DB. */ - def removeChannel(channelId: ByteVector32): Unit + /** + * Remove a channel from our DB. + * + * @param channelId ID of the channel that should be removed. + * @param data_opt if provided, closing data will be stored in a dedicated table. + */ + def removeChannel(channelId: ByteVector32, data_opt: Option[DATA_CLOSED]): Unit /** Mark revoked HTLC information as obsolete. It will be removed from the DB once [[removeHtlcInfos]] is called. */ def markHtlcInfosForRemoval(channelId: ByteVector32, beforeCommitIndex: Long): Unit @@ -41,9 +46,10 @@ trait ChannelsDb { def listLocalChannels(): Seq[PersistentChannelData] - def listClosedChannels(remoteNodeId_opt: Option[PublicKey], paginated_opt: Option[Paginated]): Seq[PersistentChannelData] + def listClosedChannels(remoteNodeId_opt: Option[PublicKey], paginated_opt: Option[Paginated]): Seq[DATA_CLOSED] def addHtlcInfo(channelId: ByteVector32, commitmentNumber: Long, paymentHash: ByteVector32, cltvExpiry: CltvExpiry): Unit def listHtlcInfos(channelId: ByteVector32, commitmentNumber: Long): Seq[(ByteVector32, CltvExpiry)] + } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/Databases.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/Databases.scala index c7f929f572..e47b9fdb7e 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/Databases.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/Databases.scala @@ -21,7 +21,6 @@ import akka.actor.{ActorSystem, CoordinatedShutdown} import com.typesafe.config.Config import com.zaxxer.hikari.{HikariConfig, HikariDataSource} import fr.acinq.eclair.TimestampMilli -import fr.acinq.eclair.db.migration.{CompareDb, MigrateDb} import fr.acinq.eclair.db.pg.PgUtils.PgLock.LockFailureHandler import fr.acinq.eclair.db.pg.PgUtils._ import fr.acinq.eclair.db.pg._ @@ -80,6 +79,8 @@ object Databases extends Logging { object SqliteDatabases { def apply(auditJdbc: Connection, networkJdbc: Connection, eclairJdbc: Connection, jdbcUrlFile_opt: Option[File]): SqliteDatabases = { jdbcUrlFile_opt.foreach(checkIfDatabaseUrlIsUnchanged("sqlite", _)) + // We check whether the node operator needs to run an intermediate eclair version first. + using(eclairJdbc.createStatement(), inTransaction = true) { statement => checkChannelsDbVersion(statement, SqliteChannelsDb.DB_NAME, isSqlite = true) } SqliteDatabases( network = new SqliteNetworkDb(networkJdbc), liquidity = new SqliteLiquidityDb(eclairJdbc), @@ -154,6 +155,11 @@ object Databases extends Logging { } } + // We check whether the node operator needs to run an intermediate eclair version first. + PgUtils.inTransaction { connection => + using(connection.createStatement()) { statement => checkChannelsDbVersion(statement, PgChannelsDb.DB_NAME, isSqlite = false) } + } + val databases = PostgresDatabases( network = new PgNetworkDb, liquidity = new PgLiquidityDb, @@ -208,9 +214,8 @@ object Databases extends Logging { maxAge = initChecks.localChannelsMaxAge, sqlQuery = """ - |SELECT MAX(GREATEST(created_timestamp, last_payment_sent_timestamp, last_payment_received_timestamp, last_connected_timestamp, closed_timestamp)) - |FROM local.channels - |WHERE NOT is_closed""".stripMargin) + |SELECT MAX(GREATEST(created_timestamp, last_payment_sent_timestamp, last_payment_received_timestamp, last_connected_timestamp)) + |FROM local.channels""".stripMargin) checkMaxAge(name = "network node", maxAge = initChecks.networkNodesMaxAge, @@ -281,21 +286,6 @@ object Databases extends Logging { dbConfig.getString("driver") match { case "sqlite" => Databases.sqlite(chaindir, jdbcUrlFile_opt = Some(jdbcUrlFile)) case "postgres" => Databases.postgres(dbConfig, instanceId, chaindir, jdbcUrlFile_opt = Some(jdbcUrlFile)) - case dual@("dual-sqlite-primary" | "dual-postgres-primary") => - logger.info(s"using $dual database mode") - val sqlite = Databases.sqlite(chaindir, jdbcUrlFile_opt = None) - val postgres = Databases.postgres(dbConfig, instanceId, chaindir, jdbcUrlFile_opt = None) - val (primary, secondary) = if (dual == "dual-sqlite-primary") (sqlite, postgres) else (postgres, sqlite) - val dualDb = DualDatabases(primary, secondary) - if (primary == sqlite) { - if (dbConfig.getBoolean("dual.migrate-on-restart")) { - MigrateDb.migrateAll(dualDb) - } - if (dbConfig.getBoolean("dual.compare-on-restart")) { - CompareDb.compareAll(dualDb) - } - } - dualDb case driver => throw new RuntimeException(s"unknown database driver `$driver`") } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/DbEventHandler.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/DbEventHandler.scala index 9356799054..a7da4fb02b 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/DbEventHandler.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/DbEventHandler.scala @@ -125,7 +125,7 @@ class DbEventHandler(nodeParams: NodeParams) extends Actor with DiagnosticActorL case ChannelStateChanged(_, channelId, _, remoteNodeId, WAIT_FOR_CHANNEL_READY | WAIT_FOR_DUAL_FUNDING_READY, NORMAL, Some(commitments)) => ChannelMetrics.ChannelLifecycleEvents.withTag(ChannelTags.Event, ChannelTags.Events.Created).increment() val event = ChannelEvent.EventType.Created - auditDb.add(ChannelEvent(channelId, remoteNodeId, commitments.latest.capacity, commitments.params.localParams.isChannelOpener, !commitments.announceChannel, event)) + auditDb.add(ChannelEvent(channelId, remoteNodeId, commitments.latest.capacity, commitments.localChannelParams.isChannelOpener, !commitments.announceChannel, event)) channelsDb.updateChannelMeta(channelId, event) case ChannelStateChanged(_, _, _, _, WAIT_FOR_INIT_INTERNAL, _, _) => case ChannelStateChanged(_, channelId, _, _, OFFLINE, SYNCING, _) => @@ -139,7 +139,7 @@ class DbEventHandler(nodeParams: NodeParams) extends Actor with DiagnosticActorL ChannelMetrics.ChannelLifecycleEvents.withTag(ChannelTags.Event, ChannelTags.Events.Closed).increment() val event = ChannelEvent.EventType.Closed(e.closingType) val capacity = e.commitments.latest.capacity - auditDb.add(ChannelEvent(e.channelId, e.commitments.params.remoteParams.nodeId, capacity, e.commitments.params.localParams.isChannelOpener, !e.commitments.announceChannel, event)) + auditDb.add(ChannelEvent(e.channelId, e.commitments.remoteNodeId, capacity, e.commitments.localChannelParams.isChannelOpener, !e.commitments.announceChannel, event)) channelsDb.updateChannelMeta(e.channelId, event) case u: ChannelUpdateParametersChanged => diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/DualDatabases.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/DualDatabases.scala deleted file mode 100644 index ec2b77ba2f..0000000000 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/DualDatabases.scala +++ /dev/null @@ -1,523 +0,0 @@ -package fr.acinq.eclair.db - -import com.google.common.util.concurrent.ThreadFactoryBuilder -import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey -import fr.acinq.bitcoin.scalacompat.{ByteVector32, Crypto, Satoshi, TxId} -import fr.acinq.eclair.channel._ -import fr.acinq.eclair.db.AuditDb.PublishedTransaction -import fr.acinq.eclair.db.Databases.{FileBackup, PostgresDatabases, SqliteDatabases} -import fr.acinq.eclair.db.DbEventHandler.ChannelEvent -import fr.acinq.eclair.db.DualDatabases.runAsync -import fr.acinq.eclair.payment._ -import fr.acinq.eclair.payment.relay.OnTheFlyFunding -import fr.acinq.eclair.payment.relay.Relayer.RelayFees -import fr.acinq.eclair.router.Router -import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{CltvExpiry, Features, InitFeature, MilliSatoshi, Paginated, RealShortChannelId, ShortChannelId, TimestampMilli, TimestampSecond} -import grizzled.slf4j.Logging -import scodec.bits.ByteVector - -import java.io.File -import java.util.UUID -import java.util.concurrent.Executors -import scala.collection.immutable.SortedMap -import scala.concurrent.{ExecutionContext, Future} -import scala.util.{Failure, Success, Try} - -/** - * An implementation of [[Databases]] where there are two separate underlying db, one primary and one secondary. - * All calls to primary are replicated asynchronously to secondary. - * Calls to secondary are made asynchronously in a dedicated thread pool, so that it doesn't have any performance impact. - */ -case class DualDatabases(primary: Databases, secondary: Databases) extends Databases with FileBackup { - - override val network: NetworkDb = DualNetworkDb(primary.network, secondary.network) - override val audit: AuditDb = DualAuditDb(primary.audit, secondary.audit) - override val channels: ChannelsDb = DualChannelsDb(primary.channels, secondary.channels) - override val peers: PeersDb = DualPeersDb(primary.peers, secondary.peers) - override val payments: PaymentsDb = DualPaymentsDb(primary.payments, secondary.payments) - override val offers: OffersDb = DualOffersDb(primary.offers, secondary.offers) - override val pendingCommands: PendingCommandsDb = DualPendingCommandsDb(primary.pendingCommands, secondary.pendingCommands) - override val liquidity: LiquidityDb = DualLiquidityDb(primary.liquidity, secondary.liquidity) - - /** if one of the database supports file backup, we use it */ - override def backup(backupFile: File): Unit = (primary, secondary) match { - case (f: FileBackup, _) => f.backup(backupFile) - case (_, f: FileBackup) => f.backup(backupFile) - case _ => () - } -} - -object DualDatabases extends Logging { - - /** Run asynchronously and print errors */ - def runAsync[T](f: => T)(implicit ec: ExecutionContext): Future[T] = Future { - Try(f) match { - case Success(res) => res - case Failure(t) => - logger.error("postgres error:\n", t) - throw t - } - } - - def getDatabases(dualDatabases: DualDatabases): (SqliteDatabases, PostgresDatabases) = - (dualDatabases.primary, dualDatabases.secondary) match { - case (sqliteDb: SqliteDatabases, postgresDb: PostgresDatabases) => - (sqliteDb, postgresDb) - case (postgresDb: PostgresDatabases, sqliteDb: SqliteDatabases) => - (sqliteDb, postgresDb) - case _ => throw new IllegalArgumentException("there must be one sqlite and one postgres in dual db mode") - } -} - -case class DualNetworkDb(primary: NetworkDb, secondary: NetworkDb) extends NetworkDb { - - private implicit val ec: ExecutionContext = ExecutionContext.fromExecutor(Executors.newSingleThreadExecutor(new ThreadFactoryBuilder().setNameFormat("db-network").build())) - - override def addNode(n: NodeAnnouncement): Unit = { - runAsync(secondary.addNode(n)) - primary.addNode(n) - } - - override def updateNode(n: NodeAnnouncement): Unit = { - runAsync(secondary.updateNode(n)) - primary.updateNode(n) - } - - override def getNode(nodeId: Crypto.PublicKey): Option[NodeAnnouncement] = { - runAsync(secondary.getNode(nodeId)) - primary.getNode(nodeId) - } - - override def removeNode(nodeId: Crypto.PublicKey): Unit = { - runAsync(secondary.removeNode(nodeId)) - primary.removeNode(nodeId) - } - - override def listNodes(): Seq[NodeAnnouncement] = { - runAsync(secondary.listNodes()) - primary.listNodes() - } - - override def addChannel(c: ChannelAnnouncement, txid: TxId, capacity: Satoshi): Unit = { - runAsync(secondary.addChannel(c, txid, capacity)) - primary.addChannel(c, txid, capacity) - } - - override def updateChannel(u: ChannelUpdate): Unit = { - runAsync(secondary.updateChannel(u)) - primary.updateChannel(u) - } - - override def removeChannels(shortChannelIds: Iterable[ShortChannelId]): Unit = { - runAsync(secondary.removeChannels(shortChannelIds)) - primary.removeChannels(shortChannelIds) - } - - override def getChannel(shortChannelId: RealShortChannelId): Option[Router.PublicChannel] = { - runAsync(secondary.getChannel(shortChannelId)) - primary.getChannel(shortChannelId) - } - - override def listChannels(): SortedMap[RealShortChannelId, Router.PublicChannel] = { - runAsync(secondary.listChannels()) - primary.listChannels() - } - -} - -case class DualAuditDb(primary: AuditDb, secondary: AuditDb) extends AuditDb { - - private implicit val ec: ExecutionContext = ExecutionContext.fromExecutor(Executors.newSingleThreadExecutor(new ThreadFactoryBuilder().setNameFormat("db-audit").build())) - - override def add(channelLifecycle: DbEventHandler.ChannelEvent): Unit = { - runAsync(secondary.add(channelLifecycle)) - primary.add(channelLifecycle) - } - - override def add(paymentSent: PaymentSent): Unit = { - runAsync(secondary.add(paymentSent)) - primary.add(paymentSent) - } - - override def add(paymentReceived: PaymentReceived): Unit = { - runAsync(secondary.add(paymentReceived)) - primary.add(paymentReceived) - } - - override def add(paymentRelayed: PaymentRelayed): Unit = { - runAsync(secondary.add(paymentRelayed)) - primary.add(paymentRelayed) - } - - override def add(txPublished: TransactionPublished): Unit = { - runAsync(secondary.add(txPublished)) - primary.add(txPublished) - } - - override def add(txConfirmed: TransactionConfirmed): Unit = { - runAsync(secondary.add(txConfirmed)) - primary.add(txConfirmed) - } - - override def add(channelErrorOccurred: ChannelErrorOccurred): Unit = { - runAsync(secondary.add(channelErrorOccurred)) - primary.add(channelErrorOccurred) - } - - override def addChannelUpdate(channelUpdateParametersChanged: ChannelUpdateParametersChanged): Unit = { - runAsync(secondary.addChannelUpdate(channelUpdateParametersChanged)) - primary.addChannelUpdate(channelUpdateParametersChanged) - } - - override def addPathFindingExperimentMetrics(metrics: PathFindingExperimentMetrics): Unit = { - runAsync(secondary.addPathFindingExperimentMetrics(metrics)) - primary.addPathFindingExperimentMetrics(metrics) - } - - override def listPublished(channelId: ByteVector32): Seq[PublishedTransaction] = { - runAsync(secondary.listPublished(channelId)) - primary.listPublished(channelId) - } - - override def listSent(from: TimestampMilli, to: TimestampMilli, paginated_opt: Option[Paginated]): Seq[PaymentSent] = { - runAsync(secondary.listSent(from, to, paginated_opt)) - primary.listSent(from, to, paginated_opt) - } - - override def listReceived(from: TimestampMilli, to: TimestampMilli, paginated_opt: Option[Paginated]): Seq[PaymentReceived] = { - runAsync(secondary.listReceived(from, to, paginated_opt)) - primary.listReceived(from, to, paginated_opt) - } - - override def listRelayed(from: TimestampMilli, to: TimestampMilli, paginated_opt: Option[Paginated]): Seq[PaymentRelayed] = { - runAsync(secondary.listRelayed(from, to, paginated_opt)) - primary.listRelayed(from, to, paginated_opt) - } - - override def listNetworkFees(from: TimestampMilli, to: TimestampMilli): Seq[AuditDb.NetworkFee] = { - runAsync(secondary.listNetworkFees(from, to)) - primary.listNetworkFees(from, to) - } - - override def stats(from: TimestampMilli, to: TimestampMilli, paginated_opt: Option[Paginated]): Seq[AuditDb.Stats] = { - runAsync(secondary.stats(from, to, paginated_opt)) - primary.stats(from, to, paginated_opt) - } -} - -case class DualChannelsDb(primary: ChannelsDb, secondary: ChannelsDb) extends ChannelsDb { - - private implicit val ec: ExecutionContext = ExecutionContext.fromExecutor(Executors.newSingleThreadExecutor(new ThreadFactoryBuilder().setNameFormat("db-channels").build())) - - override def addOrUpdateChannel(data: PersistentChannelData): Unit = { - runAsync(secondary.addOrUpdateChannel(data)) - primary.addOrUpdateChannel(data) - } - - override def getChannel(channelId: ByteVector32): Option[PersistentChannelData] = { - runAsync(secondary.getChannel(channelId)) - primary.getChannel(channelId) - } - - override def updateChannelMeta(channelId: ByteVector32, event: ChannelEvent.EventType): Unit = { - runAsync(secondary.updateChannelMeta(channelId, event)) - primary.updateChannelMeta(channelId, event) - } - - override def removeChannel(channelId: ByteVector32): Unit = { - runAsync(secondary.removeChannel(channelId)) - primary.removeChannel(channelId) - } - - override def markHtlcInfosForRemoval(channelId: ByteVector32, beforeCommitIndex: Long): Unit = { - runAsync(secondary.markHtlcInfosForRemoval(channelId, beforeCommitIndex)) - primary.markHtlcInfosForRemoval(channelId, beforeCommitIndex) - } - - override def removeHtlcInfos(batchSize: Int): Unit = { - runAsync(secondary.removeHtlcInfos(batchSize)) - primary.removeHtlcInfos(batchSize) - } - - override def listLocalChannels(): Seq[PersistentChannelData] = { - runAsync(secondary.listLocalChannels()) - primary.listLocalChannels() - } - - override def listClosedChannels(remoteNodeId_opt: Option[PublicKey], paginated_opt: Option[Paginated]): Seq[PersistentChannelData] = { - runAsync(secondary.listClosedChannels(remoteNodeId_opt, paginated_opt)) - primary.listClosedChannels(remoteNodeId_opt, paginated_opt) - } - - override def addHtlcInfo(channelId: ByteVector32, commitmentNumber: Long, paymentHash: ByteVector32, cltvExpiry: CltvExpiry): Unit = { - runAsync(secondary.addHtlcInfo(channelId, commitmentNumber, paymentHash, cltvExpiry)) - primary.addHtlcInfo(channelId, commitmentNumber, paymentHash, cltvExpiry) - } - - override def listHtlcInfos(channelId: ByteVector32, commitmentNumber: Long): Seq[(ByteVector32, CltvExpiry)] = { - runAsync(secondary.listHtlcInfos(channelId, commitmentNumber)) - primary.listHtlcInfos(channelId, commitmentNumber) - } -} - -case class DualPeersDb(primary: PeersDb, secondary: PeersDb) extends PeersDb { - - private implicit val ec: ExecutionContext = ExecutionContext.fromExecutor(Executors.newSingleThreadExecutor(new ThreadFactoryBuilder().setNameFormat("db-peers").build())) - - override def addOrUpdatePeer(nodeId: Crypto.PublicKey, address: NodeAddress, features: Features[InitFeature]): Unit = { - runAsync(secondary.addOrUpdatePeer(nodeId, address, features)) - primary.addOrUpdatePeer(nodeId, address, features) - } - - override def addOrUpdatePeerFeatures(nodeId: Crypto.PublicKey, features: Features[InitFeature]): Unit = { - runAsync(secondary.addOrUpdatePeerFeatures(nodeId, features)) - primary.addOrUpdatePeerFeatures(nodeId, features) - } - - override def removePeer(nodeId: Crypto.PublicKey): Unit = { - runAsync(secondary.removePeer(nodeId)) - primary.removePeer(nodeId) - } - - override def getPeer(nodeId: Crypto.PublicKey): Option[NodeInfo] = { - runAsync(secondary.getPeer(nodeId)) - primary.getPeer(nodeId) - } - - override def listPeers(): Map[Crypto.PublicKey, NodeInfo] = { - runAsync(secondary.listPeers()) - primary.listPeers() - } - - override def addOrUpdateRelayFees(nodeId: Crypto.PublicKey, fees: RelayFees): Unit = { - runAsync(secondary.addOrUpdateRelayFees(nodeId, fees)) - primary.addOrUpdateRelayFees(nodeId, fees) - } - - override def getRelayFees(nodeId: Crypto.PublicKey): Option[RelayFees] = { - runAsync(secondary.getRelayFees(nodeId)) - primary.getRelayFees(nodeId) - } - - override def updateStorage(nodeId: PublicKey, data: ByteVector): Unit = { - runAsync(secondary.updateStorage(nodeId, data)) - primary.updateStorage(nodeId, data) - } - - override def getStorage(nodeId: PublicKey): Option[ByteVector] = { - runAsync(secondary.getStorage(nodeId)) - primary.getStorage(nodeId) - } - - override def removePeerStorage(peerRemovedBefore: TimestampSecond): Unit = { - runAsync(secondary.removePeerStorage(peerRemovedBefore)) - primary.removePeerStorage(peerRemovedBefore) - } -} - -case class DualPaymentsDb(primary: PaymentsDb, secondary: PaymentsDb) extends PaymentsDb { - - private implicit val ec: ExecutionContext = ExecutionContext.fromExecutor(Executors.newSingleThreadExecutor(new ThreadFactoryBuilder().setNameFormat("db-payments").build())) - - override def addIncomingPayment(pr: Bolt11Invoice, preimage: ByteVector32, paymentType: String): Unit = { - runAsync(secondary.addIncomingPayment(pr, preimage, paymentType)) - primary.addIncomingPayment(pr, preimage, paymentType) - } - - override def receiveIncomingPayment(paymentHash: ByteVector32, amount: MilliSatoshi, receivedAt: TimestampMilli): Boolean = { - runAsync(secondary.receiveIncomingPayment(paymentHash, amount, receivedAt)) - primary.receiveIncomingPayment(paymentHash, amount, receivedAt) - } - - override def receiveIncomingOfferPayment(pr: MinimalBolt12Invoice, preimage: ByteVector32, amount: MilliSatoshi, receivedAt: TimestampMilli, paymentType: String): Unit = { - runAsync(secondary.receiveIncomingOfferPayment(pr, preimage, amount, receivedAt, paymentType)) - primary.receiveIncomingOfferPayment(pr, preimage, amount, receivedAt, paymentType) - } - - override def getIncomingPayment(paymentHash: ByteVector32): Option[IncomingPayment] = { - runAsync(secondary.getIncomingPayment(paymentHash)) - primary.getIncomingPayment(paymentHash) - } - - override def removeIncomingPayment(paymentHash: ByteVector32): Try[Unit] = { - runAsync(secondary.removeIncomingPayment(paymentHash)) - primary.removeIncomingPayment(paymentHash) - } - - override def listIncomingPayments(from: TimestampMilli, to: TimestampMilli, paginated_opt: Option[Paginated]): Seq[IncomingPayment] = { - runAsync(secondary.listIncomingPayments(from, to, paginated_opt)) - primary.listIncomingPayments(from, to, paginated_opt) - } - - override def listPendingIncomingPayments(from: TimestampMilli, to: TimestampMilli, paginated_opt: Option[Paginated]): Seq[IncomingPayment] = { - runAsync(secondary.listPendingIncomingPayments(from, to, paginated_opt)) - primary.listPendingIncomingPayments(from, to, paginated_opt) - } - - override def listExpiredIncomingPayments(from: TimestampMilli, to: TimestampMilli, paginated_opt: Option[Paginated]): Seq[IncomingPayment] = { - runAsync(secondary.listExpiredIncomingPayments(from, to, paginated_opt)) - primary.listExpiredIncomingPayments(from, to, paginated_opt) - } - - override def listReceivedIncomingPayments(from: TimestampMilli, to: TimestampMilli, paginated_opt: Option[Paginated]): Seq[IncomingPayment] = { - runAsync(secondary.listReceivedIncomingPayments(from, to, paginated_opt)) - primary.listReceivedIncomingPayments(from, to, paginated_opt) - } - - override def addOutgoingPayment(outgoingPayment: OutgoingPayment): Unit = { - runAsync(secondary.addOutgoingPayment(outgoingPayment)) - primary.addOutgoingPayment(outgoingPayment) - } - - override def updateOutgoingPayment(paymentResult: PaymentSent): Unit = { - runAsync(secondary.updateOutgoingPayment(paymentResult)) - primary.updateOutgoingPayment(paymentResult) - } - - override def updateOutgoingPayment(paymentResult: PaymentFailed): Unit = { - runAsync(secondary.updateOutgoingPayment(paymentResult)) - primary.updateOutgoingPayment(paymentResult) - } - - override def getOutgoingPayment(id: UUID): Option[OutgoingPayment] = { - runAsync(secondary.getOutgoingPayment(id)) - primary.getOutgoingPayment(id) - } - - override def listOutgoingPayments(parentId: UUID): Seq[OutgoingPayment] = { - runAsync(secondary.listOutgoingPayments(parentId)) - primary.listOutgoingPayments(parentId) - } - - override def listOutgoingPayments(paymentHash: ByteVector32): Seq[OutgoingPayment] = { - runAsync(secondary.listOutgoingPayments(paymentHash)) - primary.listOutgoingPayments(paymentHash) - } - - override def listOutgoingPayments(from: TimestampMilli, to: TimestampMilli): Seq[OutgoingPayment] = { - runAsync(secondary.listOutgoingPayments(from, to)) - primary.listOutgoingPayments(from, to) - } - - override def listOutgoingPaymentsToOffer(offerId: ByteVector32): Seq[OutgoingPayment] = { - runAsync(secondary.listOutgoingPaymentsToOffer(offerId)) - primary.listOutgoingPaymentsToOffer(offerId) - } -} - -case class DualOffersDb(primary: OffersDb, secondary: OffersDb) extends OffersDb { - - private implicit val ec: ExecutionContext = ExecutionContext.fromExecutor(Executors.newSingleThreadExecutor(new ThreadFactoryBuilder().setNameFormat("db-offers").build())) - - override def addOffer(offer: OfferTypes.Offer, pathId_opt: Option[ByteVector32], createdAt: TimestampMilli = TimestampMilli.now()): Option[OfferData] = { - runAsync(secondary.addOffer(offer, pathId_opt, createdAt)) - primary.addOffer(offer, pathId_opt, createdAt) - } - - override def disableOffer(offer: OfferTypes.Offer, disabledAt: TimestampMilli = TimestampMilli.now()): Unit = { - runAsync(secondary.disableOffer(offer, disabledAt)) - primary.disableOffer(offer, disabledAt) - } - - override def listOffers(onlyActive: Boolean): Seq[OfferData] = { - runAsync(secondary.listOffers(onlyActive)) - primary.listOffers(onlyActive) - } -} - -case class DualPendingCommandsDb(primary: PendingCommandsDb, secondary: PendingCommandsDb) extends PendingCommandsDb { - - private implicit val ec: ExecutionContext = ExecutionContext.fromExecutor(Executors.newSingleThreadExecutor(new ThreadFactoryBuilder().setNameFormat("db-pending-commands").build())) - - override def addSettlementCommand(channelId: ByteVector32, cmd: HtlcSettlementCommand): Unit = { - runAsync(secondary.addSettlementCommand(channelId, cmd)) - primary.addSettlementCommand(channelId, cmd) - } - - override def removeSettlementCommand(channelId: ByteVector32, htlcId: Long): Unit = { - runAsync(secondary.removeSettlementCommand(channelId, htlcId)) - primary.removeSettlementCommand(channelId, htlcId) - } - - override def listSettlementCommands(channelId: ByteVector32): Seq[HtlcSettlementCommand] = { - runAsync(secondary.listSettlementCommands(channelId)) - primary.listSettlementCommands(channelId) - } - - override def listSettlementCommands(): Seq[(ByteVector32, HtlcSettlementCommand)] = { - runAsync(secondary.listSettlementCommands()) - primary.listSettlementCommands() - } -} - -case class DualLiquidityDb(primary: LiquidityDb, secondary: LiquidityDb) extends LiquidityDb { - - private implicit val ec: ExecutionContext = ExecutionContext.fromExecutor(Executors.newSingleThreadExecutor(new ThreadFactoryBuilder().setNameFormat("db-liquidity").build())) - - override def addPurchase(liquidityPurchase: ChannelLiquidityPurchased): Unit = { - runAsync(secondary.addPurchase(liquidityPurchase)) - primary.addPurchase(liquidityPurchase) - } - - override def setConfirmed(remoteNodeId: PublicKey, txId: TxId): Unit = { - runAsync(secondary.setConfirmed(remoteNodeId, txId)) - primary.setConfirmed(remoteNodeId, txId) - } - - override def listPurchases(remoteNodeId: PublicKey): Seq[LiquidityPurchase] = { - runAsync(secondary.listPurchases(remoteNodeId)) - primary.listPurchases(remoteNodeId) - } - - override def addPendingOnTheFlyFunding(remoteNodeId: PublicKey, pending: OnTheFlyFunding.Pending): Unit = { - runAsync(secondary.addPendingOnTheFlyFunding(remoteNodeId, pending)) - primary.addPendingOnTheFlyFunding(remoteNodeId, pending) - } - - override def removePendingOnTheFlyFunding(remoteNodeId: PublicKey, paymentHash: ByteVector32): Unit = { - runAsync(secondary.removePendingOnTheFlyFunding(remoteNodeId, paymentHash)) - primary.removePendingOnTheFlyFunding(remoteNodeId, paymentHash) - } - - override def listPendingOnTheFlyFunding(remoteNodeId: PublicKey): Map[ByteVector32, OnTheFlyFunding.Pending] = { - runAsync(secondary.listPendingOnTheFlyFunding(remoteNodeId)) - primary.listPendingOnTheFlyFunding(remoteNodeId) - } - - override def listPendingOnTheFlyFunding(): Map[PublicKey, Map[ByteVector32, OnTheFlyFunding.Pending]] = { - runAsync(secondary.listPendingOnTheFlyFunding()) - primary.listPendingOnTheFlyFunding() - } - - override def listPendingOnTheFlyPayments(): Map[PublicKey, Set[ByteVector32]] = { - runAsync(secondary.listPendingOnTheFlyPayments()) - primary.listPendingOnTheFlyPayments() - } - - override def addOnTheFlyFundingPreimage(preimage: ByteVector32): Unit = { - runAsync(secondary.addOnTheFlyFundingPreimage(preimage)) - primary.addOnTheFlyFundingPreimage(preimage) - } - - override def getOnTheFlyFundingPreimage(paymentHash: ByteVector32): Option[ByteVector32] = { - runAsync(secondary.getOnTheFlyFundingPreimage(paymentHash)) - primary.getOnTheFlyFundingPreimage(paymentHash) - } - - override def addFeeCredit(nodeId: PublicKey, amount: MilliSatoshi, receivedAt: TimestampMilli): MilliSatoshi = { - runAsync(secondary.addFeeCredit(nodeId, amount, receivedAt)) - primary.addFeeCredit(nodeId, amount, receivedAt) - } - - override def getFeeCredit(nodeId: PublicKey): MilliSatoshi = { - runAsync(secondary.getFeeCredit(nodeId)) - primary.getFeeCredit(nodeId) - } - - override def removeFeeCredit(nodeId: PublicKey, amountUsed: MilliSatoshi): MilliSatoshi = { - runAsync(secondary.removeFeeCredit(nodeId, amountUsed)) - primary.removeFeeCredit(nodeId, amountUsed) - } - -} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala index f5fcdfc3fa..53ce18f826 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala @@ -250,7 +250,7 @@ object FailureSummary { def apply(f: PaymentFailure): FailureSummary = f match { case LocalFailure(_, route, t) => FailureSummary(FailureType.LOCAL, t.getMessage, route.map(h => HopSummary(h)).toList, route.headOption.map(_.nodeId)) case RemoteFailure(_, route, e) => FailureSummary(FailureType.REMOTE, e.failureMessage.message, route.map(h => HopSummary(h)).toList, Some(e.originNode)) - case UnreadableRemoteFailure(_, route, _) => FailureSummary(FailureType.UNREADABLE_REMOTE, "could not decrypt failure onion", route.map(h => HopSummary(h)).toList, None) + case UnreadableRemoteFailure(_, route, _, _) => FailureSummary(FailureType.UNREADABLE_REMOTE, "could not decrypt failure onion", route.map(h => HopSummary(h)).toList, None) } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/jdbc/JdbcUtils.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/jdbc/JdbcUtils.scala index 272981fb7a..a6792a7439 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/jdbc/JdbcUtils.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/jdbc/JdbcUtils.scala @@ -79,6 +79,37 @@ trait JdbcUtils { .headOption } + /** + * We made some changes in the v0.13.0 and v0.13.1 releases to allow deprecating legacy channel data and channel types. + * It is thus not possible to directly upgrade from an eclair version earlier than v0.13.x without going through some + * data migration code. + * + * In the v0.13.0 release, we: + * - introduced channel codecs v5 + * - migrated all channels to this codec version + * - incremented the channels DB version to 7 (sqlite) and 11 (postgres) + * + * In the v0.13.1 release, we: + * - refused to start if the channels DB version wasn't 7 (sqlite) or 11 (postgres), to force node operators to + * run the v0.13.0 release first to migrate their channels to channel codecs v5 + * - removed support for older channel codecs + * - moved closed channels to a dedicated DB table, that doesn't have a dependency on legacy channel types, to + * allow deprecating channel types that aren't used anymore + * - incremented the channels DB version to 8 (sqlite) and 12 (postgres) + * + * We warn node operators that they must first run the v0.13.x releases to migrate their channel data and prevent + * eclair from starting. + */ + def checkChannelsDbVersion(statement: Statement, db_name: String, isSqlite: Boolean): Unit = { + val eclair130 = if (isSqlite) 7 else 11 + val eclair131 = if (isSqlite) 8 else 12 + getVersion(statement, db_name) match { + case Some(v) if v < eclair130 => throw new IllegalArgumentException("You are updating from a version of eclair older than v0.13.0: please update to the v0.13.0 release first to migrate your channel data, then to the v0.13.1 release to migrate your closed channels, and afterwards you'll be able to update to the latest version.") + case Some(v) if v < eclair131 => throw new IllegalArgumentException("You are updating from a version of eclair older than v0.13.1: please update to the v0.13.1 release first to migrate your closed channels, and afterwards you'll be able to update to the latest version.") + case _ => () + } + } + /** * Updates the version for a particular logical database, it will overwrite the previous version. * diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/CompareAuditDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/CompareAuditDb.scala deleted file mode 100644 index 5d532e70a8..0000000000 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/CompareAuditDb.scala +++ /dev/null @@ -1,280 +0,0 @@ -package fr.acinq.eclair.db.migration - -import fr.acinq.eclair.db.migration.CompareDb._ -import scodec.bits.ByteVector - -import java.sql.{Connection, ResultSet} - -object CompareAuditDb { - - private def compareSentTable(conn1: Connection, conn2: Connection): Boolean = { - val table1 = "sent" - val table2 = "audit.sent" - - def hash1(rs: ResultSet): ByteVector = { - long(rs, "amount_msat") ++ - long(rs, "fees_msat") ++ - long(rs, "recipient_amount_msat") ++ - string(rs, "payment_id") ++ - string(rs, "parent_payment_id") ++ - bytes(rs, "payment_hash") ++ - bytes(rs, "payment_preimage") ++ - bytes(rs, "recipient_node_id") ++ - bytes(rs, "to_channel_id") ++ - longts(rs, "timestamp") - } - - def hash2(rs: ResultSet): ByteVector = { - long(rs, "amount_msat") ++ - long(rs, "fees_msat") ++ - long(rs, "recipient_amount_msat") ++ - string(rs, "payment_id") ++ - string(rs, "parent_payment_id") ++ - hex(rs, "payment_hash") ++ - hex(rs, "payment_preimage") ++ - hex(rs, "recipient_node_id") ++ - hex(rs, "to_channel_id") ++ - ts(rs, "timestamp") - } - - compareTable(conn1, conn2, table1, table2, hash1, hash2) - } - - private def compareReceivedTable(conn1: Connection, conn2: Connection): Boolean = { - val table1 = "received" - val table2 = "audit.received" - - def hash1(rs: ResultSet): ByteVector = { - long(rs, "amount_msat") ++ - bytes(rs, "payment_hash") ++ - bytes(rs, "from_channel_id") ++ - longts(rs, "timestamp") - } - - def hash2(rs: ResultSet): ByteVector = { - long(rs, "amount_msat") ++ - hex(rs, "payment_hash") ++ - hex(rs, "from_channel_id") ++ - ts(rs, "timestamp") - } - - compareTable(conn1, conn2, table1, table2, hash1, hash2) - } - - private def compareRelayedTable(conn1: Connection, conn2: Connection): Boolean = { - val table1 = "relayed" - val table2 = "audit.relayed" - - def hash1(rs: ResultSet): ByteVector = { - bytes(rs, "payment_hash") ++ - long(rs, "amount_msat") ++ - bytes(rs, "channel_id") ++ - string(rs, "direction") ++ - string(rs, "relay_type") ++ - longts(rs, "timestamp") - } - - def hash2(rs: ResultSet): ByteVector = { - hex(rs, "payment_hash") ++ - long(rs, "amount_msat") ++ - hex(rs, "channel_id") ++ - string(rs, "direction") ++ - string(rs, "relay_type") ++ - ts(rs, "timestamp") - } - - compareTable(conn1, conn2, table1, table2, hash1, hash2) - } - - private def compareRelayedTrampolineTable(conn1: Connection, conn2: Connection): Boolean = { - val table1 = "relayed_trampoline" - val table2 = "audit.relayed_trampoline" - - def hash1(rs: ResultSet): ByteVector = { - bytes(rs, "payment_hash") ++ - long(rs, "amount_msat") ++ - bytes(rs, "next_node_id") ++ - longts(rs, "timestamp") - } - - def hash2(rs: ResultSet): ByteVector = { - hex(rs, "payment_hash") ++ - long(rs, "amount_msat") ++ - hex(rs, "next_node_id") ++ - ts(rs, "timestamp") - } - - compareTable(conn1, conn2, table1, table2, hash1, hash2) - } - - private def compareTransactionsPublishedTable(conn1: Connection, conn2: Connection): Boolean = { - val table1 = "transactions_published" - val table2 = "audit.transactions_published" - - def hash1(rs: ResultSet): ByteVector = { - bytes(rs, "tx_id") ++ - bytes(rs, "channel_id") ++ - bytes(rs, "node_id") ++ - long(rs, "mining_fee_sat") ++ - string(rs, "tx_type") ++ - longts(rs, "timestamp") - } - - def hash2(rs: ResultSet): ByteVector = { - hex(rs, "tx_id") ++ - hex(rs, "channel_id") ++ - hex(rs, "node_id") ++ - long(rs, "mining_fee_sat") ++ - string(rs, "tx_type") ++ - ts(rs, "timestamp") - } - - compareTable(conn1, conn2, table1, table2, hash1, hash2) - } - - private def compareTransactionsConfirmedTable(conn1: Connection, conn2: Connection): Boolean = { - val table1 = "transactions_confirmed" - val table2 = "audit.transactions_confirmed" - - def hash1(rs: ResultSet): ByteVector = { - bytes(rs, "tx_id") ++ - bytes(rs, "channel_id") ++ - bytes(rs, "node_id") ++ - longts(rs, "timestamp") - } - - def hash2(rs: ResultSet): ByteVector = { - hex(rs, "tx_id") ++ - hex(rs, "channel_id") ++ - hex(rs, "node_id") ++ - ts(rs, "timestamp") - } - - compareTable(conn1, conn2, table1, table2, hash1, hash2) - } - - private def compareChannelEventsTable(conn1: Connection, conn2: Connection): Boolean = { - val table1 = "channel_events" - val table2 = "audit.channel_events" - - def hash1(rs: ResultSet): ByteVector = { - bytes(rs, "channel_id") ++ - bytes(rs, "node_id") ++ - long(rs, "capacity_sat") ++ - bool(rs, "is_funder") ++ - bool(rs, "is_private") ++ - string(rs, "event") ++ - longts(rs, "timestamp") - } - - def hash2(rs: ResultSet): ByteVector = { - hex(rs, "channel_id") ++ - hex(rs, "node_id") ++ - long(rs, "capacity_sat") ++ - bool(rs, "is_funder") ++ - bool(rs, "is_private") ++ - string(rs, "event") ++ - ts(rs, "timestamp") - } - - compareTable(conn1, conn2, table1, table2, hash1, hash2) - } - - private def compareChannelErrorsTable(conn1: Connection, conn2: Connection): Boolean = { - val table1 = "channel_errors WHERE error_name <> 'CannotAffordFees'" - val table2 = "audit.channel_errors" - - def hash1(rs: ResultSet): ByteVector = { - bytes(rs, "channel_id") ++ - bytes(rs, "node_id") ++ - string(rs, "error_name") ++ - string(rs, "error_message") ++ - bool(rs, "is_fatal") ++ - longts(rs, "timestamp") - } - - def hash2(rs: ResultSet): ByteVector = { - hex(rs, "channel_id") ++ - hex(rs, "node_id") ++ - string(rs, "error_name") ++ - string(rs, "error_message") ++ - bool(rs, "is_fatal") ++ - ts(rs, "timestamp") - } - - compareTable(conn1, conn2, table1, table2, hash1, hash2) - } - - private def compareChannelUpdatesTable(conn1: Connection, conn2: Connection): Boolean = { - val table1 = "channel_updates" - val table2 = "audit.channel_updates" - - def hash1(rs: ResultSet): ByteVector = { - bytes(rs, "channel_id") ++ - bytes(rs, "node_id") ++ - long(rs, "fee_base_msat") ++ - long(rs, "fee_proportional_millionths") ++ - long(rs, "cltv_expiry_delta") ++ - long(rs, "htlc_minimum_msat") ++ - long(rs, "htlc_maximum_msat") ++ - longts(rs, "timestamp") - } - - def hash2(rs: ResultSet): ByteVector = { - hex(rs, "channel_id") ++ - hex(rs, "node_id") ++ - long(rs, "fee_base_msat") ++ - long(rs, "fee_proportional_millionths") ++ - long(rs, "cltv_expiry_delta") ++ - long(rs, "htlc_minimum_msat") ++ - long(rs, "htlc_maximum_msat") ++ - ts(rs, "timestamp") - } - - compareTable(conn1, conn2, table1, table2, hash1, hash2) - } - - private def comparePathFindingMetricsTable(conn1: Connection, conn2: Connection): Boolean = { - val table1 = "path_finding_metrics" - val table2 = "audit.path_finding_metrics" - - def hash1(rs: ResultSet): ByteVector = { - long(rs, "amount_msat") ++ - long(rs, "fees_msat") ++ - string(rs, "status") ++ - long(rs, "duration_ms") ++ - longts(rs, "timestamp") ++ - bool(rs, "is_mpp") ++ - string(rs, "experiment_name") ++ - bytes(rs, "recipient_node_id") - - } - - def hash2(rs: ResultSet): ByteVector = { - long(rs, "amount_msat") ++ - long(rs, "fees_msat") ++ - string(rs, "status") ++ - long(rs, "duration_ms") ++ - ts(rs, "timestamp") ++ - bool(rs, "is_mpp") ++ - string(rs, "experiment_name") ++ - hex(rs, "recipient_node_id") - } - - compareTable(conn1, conn2, table1, table2, hash1, hash2) - } - - def compareAllTables(conn1: Connection, conn2: Connection): Boolean = { - compareSentTable(conn1, conn2) && - compareReceivedTable(conn1, conn2) && - compareRelayedTable(conn1, conn2) && - compareRelayedTrampolineTable(conn1, conn2) && - compareTransactionsPublishedTable(conn1, conn2) && - compareTransactionsConfirmedTable(conn1, conn2) && - compareChannelEventsTable(conn1, conn2) && - compareChannelErrorsTable(conn1, conn2) && - compareChannelUpdatesTable(conn1, conn2) && - comparePathFindingMetricsTable(conn1, conn2) - } - -} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/CompareChannelsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/CompareChannelsDb.scala deleted file mode 100644 index 3905503b0f..0000000000 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/CompareChannelsDb.scala +++ /dev/null @@ -1,80 +0,0 @@ -package fr.acinq.eclair.db.migration - -import fr.acinq.eclair.BlockHeight -import fr.acinq.eclair.channel.{DATA_CLOSING, DATA_WAIT_FOR_FUNDING_CONFIRMED} -import fr.acinq.eclair.db.migration.CompareDb._ -import fr.acinq.eclair.wire.internal.channel.ChannelCodecs.channelDataCodec -import scodec.bits.ByteVector - -import java.sql.{Connection, ResultSet} - -object CompareChannelsDb { - - private def compareChannelsTable(conn1: Connection, conn2: Connection): Boolean = { - val table1 = "local_channels" - val table2 = "local.channels" - - def hash1(rs: ResultSet): ByteVector = { - val data = ByteVector(rs.getBytes("data")) - val data_modified = channelDataCodec.decode(data.bits).require.value match { - case c: DATA_WAIT_FOR_FUNDING_CONFIRMED => channelDataCodec.encode(c.copy(waitingSince = BlockHeight(0))).require.toByteVector - case c: DATA_CLOSING => channelDataCodec.encode(c.copy(waitingSince = BlockHeight(0))).require.toByteVector - case _ => data - } - bytes(rs, "channel_id") ++ - data_modified ++ - bool(rs, "is_closed") ++ - longtsnull(rs, "created_timestamp") ++ - longtsnull(rs, "last_payment_sent_timestamp") ++ - longtsnull(rs, "last_payment_received_timestamp") ++ - longtsnull(rs, "last_connected_timestamp") ++ - longtsnull(rs, "closed_timestamp") - } - - def hash2(rs: ResultSet): ByteVector = { - val data = ByteVector(rs.getBytes("data")) - val data_modified = channelDataCodec.decode(data.bits).require.value match { - case c: DATA_WAIT_FOR_FUNDING_CONFIRMED => channelDataCodec.encode(c.copy(waitingSince = BlockHeight(0))).require.toByteVector - case c: DATA_CLOSING => channelDataCodec.encode(c.copy(waitingSince = BlockHeight(0))).require.toByteVector - case _ => data - } - hex(rs, "channel_id") ++ - data_modified ++ - bool(rs, "is_closed") ++ - tsnull(rs, "created_timestamp") ++ - tsnull(rs, "last_payment_sent_timestamp") ++ - tsnull(rs, "last_payment_received_timestamp") ++ - tsnull(rs, "last_connected_timestamp") ++ - tsnull(rs, "closed_timestamp") - } - - compareTable(conn1, conn2, table1, table2, hash1, hash2) - } - - private def compareHtlcInfosTable(conn1: Connection, conn2: Connection): Boolean = { - val table1 = "htlc_infos" - val table2 = "local.htlc_infos" - - def hash1(rs: ResultSet): ByteVector = { - bytes(rs, "channel_id") ++ - long(rs, "commitment_number") ++ - bytes(rs, "payment_hash") ++ - long(rs, "cltv_expiry") - } - - def hash2(rs: ResultSet): ByteVector = { - hex(rs, "channel_id") ++ - long(rs, "commitment_number") ++ - hex(rs, "payment_hash") ++ - long(rs, "cltv_expiry") - } - - compareTable(conn1, conn2, table1, table2, hash1, hash2) - } - - def compareAllTables(conn1: Connection, conn2: Connection): Boolean = { - compareChannelsTable(conn1, conn2) && - compareHtlcInfosTable(conn1, conn2) - } - -} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/CompareDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/CompareDb.scala deleted file mode 100644 index 7403b3b5fa..0000000000 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/CompareDb.scala +++ /dev/null @@ -1,79 +0,0 @@ -package fr.acinq.eclair.db.migration - -import fr.acinq.eclair.db.Databases.{PostgresDatabases, SqliteDatabases} -import fr.acinq.eclair.db.DualDatabases -import fr.acinq.eclair.db.jdbc.JdbcUtils.using -import fr.acinq.eclair.db.pg.PgUtils -import grizzled.slf4j.Logging -import scodec.bits.ByteVector - -import java.sql.{Connection, ResultSet} - -object CompareDb extends Logging { - - def compareTable(conn1: Connection, - conn2: Connection, - table1: String, - table2: String, - hash1: ResultSet => ByteVector, - hash2: ResultSet => ByteVector): Boolean = { - var hashes1 = List.empty[ByteVector] - using(conn1.prepareStatement(s"SELECT * FROM $table1")) { statement => - val rs = statement.executeQuery() - while (rs.next()) hashes1 = hash1(rs) +: hashes1 - } - - var hashes2 = List.empty[ByteVector] - using(conn2.prepareStatement(s"SELECT * FROM $table2")) { statement => - val rs = statement.executeQuery() - while (rs.next()) hashes2 = hash2(rs) +: hashes2 - } - - if (hashes1.sorted == hashes2.sorted) { - logger.info(s"tables $table1/$table2 are identical") - true - } else { - val diff1 = hashes1 diff hashes2 - val diff2 = hashes2 diff hashes1 - logger.warn(s"tables $table1/$table2 are different diff1=${diff1.take(3).map(_.toHex.take(128))} diff2=${diff2.take(3).map(_.toHex.take(128))}") - false - } - } - - // @formatter:off - import fr.acinq.eclair.db.jdbc.JdbcUtils.ExtendedResultSet._ - def bytes(rs: ResultSet, columnName: String): ByteVector = rs.getByteVector(columnName) - def bytesnull(rs: ResultSet, columnName: String): ByteVector = rs.getByteVectorNullable(columnName).getOrElse(ByteVector.fromValidHex("deadbeef")) - def hex(rs: ResultSet, columnName: String): ByteVector = rs.getByteVectorFromHex(columnName) - def hexnull(rs: ResultSet, columnName: String): ByteVector = rs.getByteVectorFromHexNullable(columnName).getOrElse(ByteVector.fromValidHex("deadbeef")) - def string(rs: ResultSet, columnName: String): ByteVector = ByteVector(rs.getString(columnName).getBytes) - def stringnull(rs: ResultSet, columnName: String): ByteVector = ByteVector(rs.getStringNullable(columnName).getOrElse("").getBytes) - def bool(rs: ResultSet, columnName: String): ByteVector = ByteVector.fromByte(if (rs.getBoolean(columnName)) 1 else 0) - def long(rs: ResultSet, columnName: String): ByteVector = ByteVector.fromLong(rs.getLong(columnName)) - def longnull(rs: ResultSet, columnName: String): ByteVector = ByteVector.fromLong(rs.getLongNullable(columnName).getOrElse(42)) - def longts(rs: ResultSet, columnName: String): ByteVector = ByteVector.fromLong((rs.getLong(columnName).toDouble / 1_000_000).round) - def longtsnull(rs: ResultSet, columnName: String): ByteVector = ByteVector.fromLong(rs.getLongNullable(columnName).map(l => (l.toDouble/1_000_000).round).getOrElse(42)) - def int(rs: ResultSet, columnName: String): ByteVector = ByteVector.fromInt(rs.getInt(columnName)) - def ts(rs: ResultSet, columnName: String): ByteVector = ByteVector.fromLong((rs.getTimestamp(columnName).getTime.toDouble / 1_000_000).round) - def tsnull(rs: ResultSet, columnName: String): ByteVector = ByteVector.fromLong(rs.getTimestampNullable(columnName).map(t => (t.getTime.toDouble / 1_000_000).round).getOrElse(42)) - def tssec(rs: ResultSet, columnName: String): ByteVector = ByteVector.fromLong((rs.getTimestamp(columnName).toInstant.getEpochSecond.toDouble / 1_000_000).round) - def tssecnull(rs: ResultSet, columnName: String): ByteVector = ByteVector.fromLong(rs.getTimestampNullable(columnName).map(t => (t.toInstant.getEpochSecond.toDouble / 1_000_000).round).getOrElse(42)) - // @formatter:on - - def compareAll(dualDatabases: DualDatabases): Unit = { - logger.info("comparing all tables...") - val (sqliteDb: SqliteDatabases, postgresDb: PostgresDatabases) = DualDatabases.getDatabases(dualDatabases) - PgUtils.inTransaction { postgres => - val result = List( - CompareChannelsDb.compareAllTables(sqliteDb.channels.sqlite, postgres), - ComparePendingCommandsDb.compareAllTables(sqliteDb.pendingCommands.sqlite, postgres), - ComparePeersDb.compareAllTables(sqliteDb.peers.sqlite, postgres), - ComparePaymentsDb.compareAllTables(sqliteDb.payments.sqlite, postgres), - CompareNetworkDb.compareAllTables(sqliteDb.network.sqlite, postgres), - CompareAuditDb.compareAllTables(sqliteDb.audit.sqlite, postgres) - ).forall(_ == true) - logger.info(s"comparison complete identical=$result") - }(postgresDb.dataSource) - } - -} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/CompareNetworkDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/CompareNetworkDb.scala deleted file mode 100644 index 8b91bb31d4..0000000000 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/CompareNetworkDb.scala +++ /dev/null @@ -1,73 +0,0 @@ -package fr.acinq.eclair.db.migration - -import fr.acinq.eclair.db.migration.CompareDb._ -import scodec.bits.ByteVector - -import java.sql.{Connection, ResultSet} - -object CompareNetworkDb { - - private def compareNodesTable(conn1: Connection, conn2: Connection): Boolean = { - val table1 = "nodes" - val table2 = "network.nodes" - - def hash1(rs: ResultSet): ByteVector = { - bytes(rs, "node_id") ++ - bytes(rs, "data") - } - - def hash2(rs: ResultSet): ByteVector = { - hex(rs, "node_id") ++ - bytes(rs, "data") - } - - compareTable(conn1, conn2, table1, table2, hash1, hash2) - } - - private def compareChannelsTable(conn1: Connection, conn2: Connection): Boolean = { - val table1 = "channels" - val table2 = "network.public_channels" - - def hash1(rs: ResultSet): ByteVector = { - long(rs, "short_channel_id") ++ - string(rs, "txid") ++ - bytes(rs, "channel_announcement") ++ - long(rs, "capacity_sat") ++ - bytesnull(rs, "channel_update_1") ++ - bytesnull(rs, "channel_update_2") - } - - def hash2(rs: ResultSet): ByteVector = { - long(rs, "short_channel_id") ++ - string(rs, "txid") ++ - bytes(rs, "channel_announcement") ++ - long(rs, "capacity_sat") ++ - bytesnull(rs, "channel_update_1") ++ - bytesnull(rs, "channel_update_2") - } - - compareTable(conn1, conn2, table1, table2, hash1, hash2) - } - - private def comparePrunedTable(conn1: Connection, conn2: Connection): Boolean = { - val table1 = "pruned" - val table2 = "network.pruned_channels" - - def hash1(rs: ResultSet): ByteVector = { - long(rs, "short_channel_id") - } - - def hash2(rs: ResultSet): ByteVector = { - long(rs, "short_channel_id") - } - - compareTable(conn1, conn2, table1, table2, hash1, hash2) - } - - def compareAllTables(conn1: Connection, conn2: Connection): Boolean = { - compareNodesTable(conn1, conn2) && - compareChannelsTable(conn1, conn2) && - comparePrunedTable(conn1, conn2) - } - -} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/ComparePaymentsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/ComparePaymentsDb.scala deleted file mode 100644 index 17265931c0..0000000000 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/ComparePaymentsDb.scala +++ /dev/null @@ -1,87 +0,0 @@ -package fr.acinq.eclair.db.migration - -import fr.acinq.eclair.db.migration.CompareDb._ -import scodec.bits.ByteVector - -import java.sql.{Connection, ResultSet} - -object ComparePaymentsDb { - - private def compareReceivedPaymentsTable(conn1: Connection, conn2: Connection): Boolean = { - val table1 = "received_payments" - val table2 = "payments.received" - - def hash1(rs: ResultSet): ByteVector = { - bytes(rs, "payment_hash") ++ - string(rs, "payment_type") ++ - bytes(rs, "payment_preimage") ++ - string(rs, "payment_request") ++ - longnull(rs, "received_msat") ++ - longts(rs, "created_at") ++ - longts(rs, "expire_at") ++ - longtsnull(rs, "received_at") - } - - def hash2(rs: ResultSet): ByteVector = { - hex(rs, "payment_hash") ++ - string(rs, "payment_type") ++ - hex(rs, "payment_preimage") ++ - string(rs, "payment_request") ++ - longnull(rs, "received_msat") ++ - ts(rs, "created_at") ++ - ts(rs, "expire_at") ++ - tsnull(rs, "received_at") - } - - compareTable(conn1, conn2, table1, table2, hash1, hash2) - } - - private def compareSentPaymentsTable(conn1: Connection, conn2: Connection): Boolean = { - val table1 = "sent_payments" - val table2 = "payments.sent" - - def hash1(rs: ResultSet): ByteVector = { - string(rs, "id") ++ - string(rs, "parent_id") ++ - stringnull(rs, "external_id") ++ - bytes(rs, "payment_hash") ++ - bytesnull(rs, "payment_preimage") ++ - string(rs, "payment_type") ++ - long(rs, "amount_msat") ++ - longnull(rs, "fees_msat") ++ - long(rs, "recipient_amount_msat") ++ - bytes(rs, "recipient_node_id") ++ - stringnull(rs, "payment_request") ++ - bytesnull(rs, "payment_route") ++ - bytesnull(rs, "failures") ++ - longts(rs, "created_at") ++ - longtsnull(rs, "completed_at") - } - - def hash2(rs: ResultSet): ByteVector = { - string(rs, "id") ++ - string(rs, "parent_id") ++ - stringnull(rs, "external_id") ++ - hex(rs, "payment_hash") ++ - hexnull(rs, "payment_preimage") ++ - string(rs, "payment_type") ++ - long(rs, "amount_msat") ++ - longnull(rs, "fees_msat") ++ - long(rs, "recipient_amount_msat") ++ - hex(rs, "recipient_node_id") ++ - stringnull(rs, "payment_request") ++ - bytesnull(rs, "payment_route") ++ - bytesnull(rs, "failures") ++ - ts(rs, "created_at") ++ - tsnull(rs, "completed_at") - } - - compareTable(conn1, conn2, table1, table2, hash1, hash2) - } - - def compareAllTables(conn1: Connection, conn2: Connection): Boolean = { - compareReceivedPaymentsTable(conn1, conn2) && - compareSentPaymentsTable(conn1, conn2) - } - -} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/ComparePeersDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/ComparePeersDb.scala deleted file mode 100644 index c0c034fa97..0000000000 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/ComparePeersDb.scala +++ /dev/null @@ -1,51 +0,0 @@ -package fr.acinq.eclair.db.migration - -import fr.acinq.eclair.db.migration.CompareDb._ -import scodec.bits.ByteVector - -import java.sql.{Connection, ResultSet} - -object ComparePeersDb { - - private def comparePeersTable(conn1: Connection, conn2: Connection): Boolean = { - val table1 = "peers" - val table2 = "local.peers" - - def hash1(rs: ResultSet): ByteVector = { - bytes(rs, "node_id") ++ - bytes(rs, "data") - } - - def hash2(rs: ResultSet): ByteVector = { - hex(rs, "node_id") ++ - bytes(rs, "data") - } - - compareTable(conn1, conn2, table1, table2, hash1, hash2) - } - - private def compareRelayFeesTable(conn1: Connection, conn2: Connection): Boolean = { - val table1 = "relay_fees" - val table2 = "local.relay_fees" - - def hash1(rs: ResultSet): ByteVector = { - bytes(rs, "node_id") ++ - long(rs, "fee_base_msat") ++ - long(rs, "fee_proportional_millionths") - } - - def hash2(rs: ResultSet): ByteVector = { - hex(rs, "node_id") ++ - long(rs, "fee_base_msat") ++ - long(rs, "fee_proportional_millionths") - } - - compareTable(conn1, conn2, table1, table2, hash1, hash2) - } - - def compareAllTables(conn1: Connection, conn2: Connection): Boolean = { - comparePeersTable(conn1, conn2) && - compareRelayFeesTable(conn1, conn2) - } - -} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/ComparePendingCommandsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/ComparePendingCommandsDb.scala deleted file mode 100644 index eb099ceea4..0000000000 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/ComparePendingCommandsDb.scala +++ /dev/null @@ -1,33 +0,0 @@ -package fr.acinq.eclair.db.migration - -import fr.acinq.eclair.db.migration.CompareDb._ -import scodec.bits.ByteVector - -import java.sql.{Connection, ResultSet} - -object ComparePendingCommandsDb { - - private def comparePendingSettlementCommandsTable(conn1: Connection, conn2: Connection): Boolean = { - val table1 = "pending_settlement_commands" - val table2 = "local.pending_settlement_commands" - - def hash1(rs: ResultSet): ByteVector = { - bytes(rs, "channel_id") ++ - long(rs, "htlc_id") ++ - bytes(rs, "data") - } - - def hash2(rs: ResultSet): ByteVector = { - hex(rs, "channel_id") ++ - long(rs, "htlc_id") ++ - bytes(rs, "data") - } - - compareTable(conn1, conn2, table1, table2, hash1, hash2) - } - - def compareAllTables(conn1: Connection, conn2: Connection): Boolean = { - comparePendingSettlementCommandsTable(conn1, conn2) - } - -} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/MigrateAuditDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/MigrateAuditDb.scala deleted file mode 100644 index 28705cd272..0000000000 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/MigrateAuditDb.scala +++ /dev/null @@ -1,188 +0,0 @@ -package fr.acinq.eclair.db.migration - -import fr.acinq.eclair.db.jdbc.JdbcUtils.ExtendedResultSet._ -import fr.acinq.eclair.db.migration.MigrateDb.{checkVersions, migrateTable} - -import java.sql.{Connection, PreparedStatement, ResultSet, Timestamp} -import java.time.Instant - -object MigrateAuditDb { - - private def migrateSentTable(source: Connection, destination: Connection): Int = { - val sourceTable = "sent" - val insertSql = "INSERT INTO audit.sent (amount_msat, fees_msat, recipient_amount_msat, payment_id, parent_payment_id, payment_hash, payment_preimage, recipient_node_id, to_channel_id, timestamp) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)" - - def migrate(rs: ResultSet, insertStatement: PreparedStatement): Unit = { - insertStatement.setLong(1, rs.getLong("amount_msat")) - insertStatement.setLong(2, rs.getLong("fees_msat")) - insertStatement.setLong(3, rs.getLong("recipient_amount_msat")) - insertStatement.setString(4, rs.getString("payment_id")) - insertStatement.setString(5, rs.getString("parent_payment_id")) - insertStatement.setString(6, rs.getByteVector32("payment_hash").toHex) - insertStatement.setString(7, rs.getByteVector32("payment_preimage").toHex) - insertStatement.setString(8, rs.getByteVector("recipient_node_id").toHex) - insertStatement.setString(9, rs.getByteVector32("to_channel_id").toHex) - insertStatement.setTimestamp(10, Timestamp.from(Instant.ofEpochMilli(rs.getLong("timestamp")))) - } - - migrateTable(source, destination, sourceTable, insertSql, migrate) - } - - private def migrateReceivedTable(source: Connection, destination: Connection): Int = { - val sourceTable = "received" - val insertSql = "INSERT INTO audit.received (amount_msat, payment_hash, from_channel_id, timestamp) VALUES (?, ?, ?, ?)" - - def migrate(rs: ResultSet, insertStatement: PreparedStatement): Unit = { - insertStatement.setLong(1, rs.getLong("amount_msat")) - insertStatement.setString(2, rs.getByteVector32("payment_hash").toHex) - insertStatement.setString(3, rs.getByteVector32("from_channel_id").toHex) - insertStatement.setTimestamp(4, Timestamp.from(Instant.ofEpochMilli(rs.getLong("timestamp")))) - } - - migrateTable(source, destination, sourceTable, insertSql, migrate) - } - - private def migrateRelayedTable(source: Connection, destination: Connection): Int = { - val sourceTable = "relayed" - val insertSql = "INSERT INTO audit.relayed (payment_hash, amount_msat, channel_id, direction, relay_type, timestamp) VALUES (?, ?, ?, ?, ?, ?)" - - def migrate(rs: ResultSet, insertStatement: PreparedStatement): Unit = { - insertStatement.setString(1, rs.getByteVector32("payment_hash").toHex) - insertStatement.setLong(2, rs.getLong("amount_msat")) - insertStatement.setString(3, rs.getByteVector32("channel_id").toHex) - insertStatement.setString(4, rs.getString("direction")) - insertStatement.setString(5, rs.getString("relay_type")) - insertStatement.setTimestamp(6, Timestamp.from(Instant.ofEpochMilli(rs.getLong("timestamp")))) - } - - migrateTable(source, destination, sourceTable, insertSql, migrate) - } - - private def migrateRelayedTrampolineTable(source: Connection, destination: Connection): Int = { - val sourceTable = "relayed_trampoline" - val insertSql = "INSERT INTO audit.relayed_trampoline (payment_hash, amount_msat, next_node_id, timestamp) VALUES (?, ?, ?, ?)" - - def migrate(rs: ResultSet, insertStatement: PreparedStatement): Unit = { - insertStatement.setString(1, rs.getByteVector32("payment_hash").toHex) - insertStatement.setLong(2, rs.getLong("amount_msat")) - insertStatement.setString(3, rs.getByteVector("next_node_id").toHex) - insertStatement.setTimestamp(4, Timestamp.from(Instant.ofEpochMilli(rs.getLong("timestamp")))) - } - - migrateTable(source, destination, sourceTable, insertSql, migrate) - } - - private def migrateTransactionsPublishedTable(source: Connection, destination: Connection): Int = { - val sourceTable = "transactions_published" - val insertSql = "INSERT INTO audit.transactions_published (tx_id, channel_id, node_id, mining_fee_sat, tx_type, timestamp) VALUES (?, ?, ?, ?, ?, ?)" - - def migrate(rs: ResultSet, insertStatement: PreparedStatement): Unit = { - insertStatement.setString(1, rs.getByteVector32("tx_id").toHex) - insertStatement.setString(2, rs.getByteVector32("channel_id").toHex) - insertStatement.setString(3, rs.getByteVector("node_id").toHex) - insertStatement.setLong(4, rs.getLong("mining_fee_sat")) - insertStatement.setString(5, rs.getString("tx_type")) - insertStatement.setTimestamp(6, Timestamp.from(Instant.ofEpochMilli(rs.getLong("timestamp")))) - } - - migrateTable(source, destination, sourceTable, insertSql, migrate) - } - - private def migrateTransactionsConfirmedTable(source: Connection, destination: Connection): Int = { - val sourceTable = "transactions_confirmed" - val insertSql = "INSERT INTO audit.transactions_confirmed (tx_id, channel_id, node_id, timestamp) VALUES (?, ?, ?, ?)" - - def migrate(rs: ResultSet, insertStatement: PreparedStatement): Unit = { - insertStatement.setString(1, rs.getByteVector32("tx_id").toHex) - insertStatement.setString(2, rs.getByteVector32("channel_id").toHex) - insertStatement.setString(3, rs.getByteVector("node_id").toHex) - insertStatement.setTimestamp(4, Timestamp.from(Instant.ofEpochMilli(rs.getLong("timestamp")))) - } - - migrateTable(source, destination, sourceTable, insertSql, migrate) - } - - private def migrateChannelEventsTable(source: Connection, destination: Connection): Int = { - val sourceTable = "channel_events" - val insertSql = "INSERT INTO audit.channel_events (channel_id, node_id, capacity_sat, is_funder, is_private, event, timestamp) VALUES (?, ?, ?, ?, ?, ?, ?)" - - def migrate(rs: ResultSet, insertStatement: PreparedStatement): Unit = { - insertStatement.setString(1, rs.getByteVector32("channel_id").toHex) - insertStatement.setString(2, rs.getByteVector("node_id").toHex) - insertStatement.setLong(3, rs.getLong("capacity_sat")) - insertStatement.setBoolean(4, rs.getBoolean("is_funder")) - insertStatement.setBoolean(5, rs.getBoolean("is_private")) - insertStatement.setString(6, rs.getString("event")) - insertStatement.setTimestamp(7, Timestamp.from(Instant.ofEpochMilli(rs.getLong("timestamp")))) - } - - migrateTable(source, destination, sourceTable, insertSql, migrate) - } - - private def migrateChannelErrorsTable(source: Connection, destination: Connection): Int = { - val sourceTable = "channel_errors WHERE error_name <> 'CannotAffordFees'" - val insertSql = "INSERT INTO audit.channel_errors (channel_id, node_id, error_name, error_message, is_fatal, timestamp) VALUES (?, ?, ?, ?, ?, ?)" - - def migrate(rs: ResultSet, insertStatement: PreparedStatement): Unit = { - insertStatement.setString(1, rs.getByteVector32("channel_id").toHex) - insertStatement.setString(2, rs.getByteVector("node_id").toHex) - insertStatement.setString(3, rs.getString("error_name")) - insertStatement.setString(4, rs.getString("error_message")) - insertStatement.setBoolean(5, rs.getBoolean("is_fatal")) - insertStatement.setTimestamp(6, Timestamp.from(Instant.ofEpochMilli(rs.getLong("timestamp")))) - } - - migrateTable(source, destination, sourceTable, insertSql, migrate) - } - - private def migrateChannelUpdatesTable(source: Connection, destination: Connection): Int = { - val sourceTable = "channel_updates" - val insertSql = "INSERT INTO audit.channel_updates (channel_id, node_id, fee_base_msat, fee_proportional_millionths, cltv_expiry_delta, htlc_minimum_msat, htlc_maximum_msat, timestamp) VALUES (?, ?, ?, ?, ?, ?, ?, ?)" - - def migrate(rs: ResultSet, insertStatement: PreparedStatement): Unit = { - insertStatement.setString(1, rs.getByteVector32("channel_id").toHex) - insertStatement.setString(2, rs.getByteVector("node_id").toHex) - insertStatement.setLong(3, rs.getLong("fee_base_msat")) - insertStatement.setLong(4, rs.getLong("fee_proportional_millionths")) - insertStatement.setLong(5, rs.getLong("cltv_expiry_delta")) - insertStatement.setLong(6, rs.getLong("htlc_minimum_msat")) - insertStatement.setLong(7, rs.getLong("htlc_maximum_msat")) - insertStatement.setTimestamp(8, Timestamp.from(Instant.ofEpochMilli(rs.getLong("timestamp")))) - } - - migrateTable(source, destination, sourceTable, insertSql, migrate) - } - - private def migratePathFindingMetricsTable(source: Connection, destination: Connection): Int = { - val sourceTable = "path_finding_metrics" - val insertSql = "INSERT INTO audit.path_finding_metrics (amount_msat, fees_msat, status, duration_ms, timestamp, is_mpp, experiment_name, recipient_node_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?)" - - def migrate(rs: ResultSet, insertStatement: PreparedStatement): Unit = { - insertStatement.setLong(1, rs.getLong("amount_msat")) - insertStatement.setLong(2, rs.getLong("fees_msat")) - insertStatement.setString(3, rs.getString("status")) - insertStatement.setLong(4, rs.getLong("duration_ms")) - insertStatement.setTimestamp(5, Timestamp.from(Instant.ofEpochMilli(rs.getLong("timestamp")))) - insertStatement.setBoolean(6, rs.getBoolean("is_mpp")) - insertStatement.setString(7, rs.getString("experiment_name")) - insertStatement.setString(8, rs.getByteVector("recipient_node_id").toHex) - } - - migrateTable(source, destination, sourceTable, insertSql, migrate) - } - - def migrateAllTables(source: Connection, destination: Connection): Unit = { - checkVersions(source, destination, "audit", 8, 10) - migrateSentTable(source, destination) - migrateReceivedTable(source, destination) - migrateRelayedTable(source, destination) - migrateRelayedTrampolineTable(source, destination) - migrateTransactionsPublishedTable(source, destination) - migrateTransactionsConfirmedTable(source, destination) - migrateChannelEventsTable(source, destination) - migrateChannelErrorsTable(source, destination) - migrateChannelUpdatesTable(source, destination) - migratePathFindingMetricsTable(source, destination) - } - -} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/MigrateChannelsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/MigrateChannelsDb.scala deleted file mode 100644 index 2b4165daea..0000000000 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/MigrateChannelsDb.scala +++ /dev/null @@ -1,56 +0,0 @@ -package fr.acinq.eclair.db.migration - -import fr.acinq.eclair.db.jdbc.JdbcUtils.ExtendedResultSet._ -import fr.acinq.eclair.db.migration.MigrateDb.{checkVersions, migrateTable} -import fr.acinq.eclair.wire.internal.channel.ChannelCodecs.channelDataCodec -import scodec.bits.BitVector - -import java.sql.{Connection, PreparedStatement, ResultSet, Timestamp} -import java.time.Instant - -object MigrateChannelsDb { - - private def migrateChannelsTable(source: Connection, destination: Connection): Int = { - val sourceTable = "local_channels" - val insertSql = "INSERT INTO local.channels (channel_id, data, json, is_closed, created_timestamp, last_payment_sent_timestamp, last_payment_received_timestamp, last_connected_timestamp, closed_timestamp) VALUES (?, ?, ?::JSONB, ?, ?, ?, ?, ?, ?)" - - import fr.acinq.eclair.json.JsonSerializers._ - - def migrate(rs: ResultSet, insertStatement: PreparedStatement): Unit = { - insertStatement.setString(1, rs.getByteVector32("channel_id").toHex) - insertStatement.setBytes(2, rs.getBytes("data")) - val state = channelDataCodec.decode(BitVector(rs.getBytes("data"))).require.value - val json = serialization.write(state) - insertStatement.setString(3, json) - insertStatement.setBoolean(4, rs.getBoolean("is_closed")) - insertStatement.setTimestamp(5, rs.getLongNullable("created_timestamp").map(l => Timestamp.from(Instant.ofEpochMilli(l))).orNull) - insertStatement.setTimestamp(6, rs.getLongNullable("last_payment_sent_timestamp").map(l => Timestamp.from(Instant.ofEpochMilli(l))).orNull) - insertStatement.setTimestamp(7, rs.getLongNullable("last_payment_received_timestamp").map(l => Timestamp.from(Instant.ofEpochMilli(l))).orNull) - insertStatement.setTimestamp(8, rs.getLongNullable("last_connected_timestamp").map(l => Timestamp.from(Instant.ofEpochMilli(l))).orNull) - insertStatement.setTimestamp(9, rs.getLongNullable("closed_timestamp").map(l => Timestamp.from(Instant.ofEpochMilli(l))).orNull) - } - - migrateTable(source, destination, sourceTable, insertSql, migrate) - } - - private def migrateHtlcInfos(source: Connection, destination: Connection): Int = { - val sourceTable = "htlc_infos" - val insertSql = "INSERT INTO local.htlc_infos (channel_id, commitment_number, payment_hash, cltv_expiry) VALUES (?, ?, ?, ?)" - - def migrate(rs: ResultSet, insertStatement: PreparedStatement): Unit = { - insertStatement.setString(1, rs.getByteVector32("channel_id").toHex) - insertStatement.setLong(2, rs.getLong("commitment_number")) - insertStatement.setString(3, rs.getByteVector32("payment_hash").toHex) - insertStatement.setLong(4, rs.getLong("cltv_expiry")) - } - - migrateTable(source, destination, sourceTable, insertSql, migrate) - } - - def migrateAllTables(source: Connection, destination: Connection): Unit = { - checkVersions(source, destination, "channels", 4, 7) - migrateChannelsTable(source, destination) - migrateHtlcInfos(source, destination) - } - -} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/MigrateDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/MigrateDb.scala deleted file mode 100644 index 28d776a54f..0000000000 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/MigrateDb.scala +++ /dev/null @@ -1,54 +0,0 @@ -package fr.acinq.eclair.db.migration - -import fr.acinq.eclair.db.Databases.{PostgresDatabases, SqliteDatabases} -import fr.acinq.eclair.db.DualDatabases -import fr.acinq.eclair.db.jdbc.JdbcUtils -import fr.acinq.eclair.db.jdbc.JdbcUtils.using -import fr.acinq.eclair.db.pg.PgUtils -import grizzled.slf4j.Logging - -import java.sql.{Connection, PreparedStatement, ResultSet} - -object MigrateDb extends Logging { - - private def getVersion(conn: Connection, dbName: String): Int = { - using(conn.prepareStatement(s"SELECT version FROM versions WHERE db_name='$dbName'")) { statement => - val res = statement.executeQuery() - res.next() - res.getInt("version") - } - } - - def checkVersions(source: Connection, - destination: Connection, - dbName: String, - expectedSourceVersion: Int, - expectedDestinationVersion: Int): Unit = { - val actualSourceVersion = getVersion(source, dbName) - val actualDestinationVersion = getVersion(destination, dbName) - require(actualSourceVersion == expectedSourceVersion, s"unexpected version for source db=$dbName expected=$expectedSourceVersion actual=$actualSourceVersion") - require(actualDestinationVersion == expectedDestinationVersion, s"unexpected version for destination db=$dbName expected=$expectedDestinationVersion actual=$actualDestinationVersion") - } - - def migrateTable(source: Connection, - destination: Connection, - sourceTable: String, - insertSql: String, - migrate: (ResultSet, PreparedStatement) => Unit): Int = - JdbcUtils.migrateTable(source, destination, sourceTable, insertSql, migrate)(logger) - - def migrateAll(dualDatabases: DualDatabases): Unit = { - logger.info("migrating all tables...") - val (sqliteDb: SqliteDatabases, postgresDb: PostgresDatabases) = DualDatabases.getDatabases(dualDatabases) - PgUtils.inTransaction { postgres => - MigrateChannelsDb.migrateAllTables(sqliteDb.channels.sqlite, postgres) - MigratePendingCommandsDb.migrateAllTables(sqliteDb.pendingCommands.sqlite, postgres) - MigratePeersDb.migrateAllTables(sqliteDb.peers.sqlite, postgres) - MigratePaymentsDb.migrateAllTables(sqliteDb.payments.sqlite, postgres) - MigrateNetworkDb.migrateAllTables(sqliteDb.network.sqlite, postgres) - MigrateAuditDb.migrateAllTables(sqliteDb.audit.sqlite, postgres) - logger.info("migration complete") - }(postgresDb.dataSource) - } - -} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/MigrateNetworkDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/MigrateNetworkDb.scala deleted file mode 100644 index 0f2b4ccdf8..0000000000 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/MigrateNetworkDb.scala +++ /dev/null @@ -1,74 +0,0 @@ -package fr.acinq.eclair.db.migration - -import fr.acinq.eclair.db.jdbc.JdbcUtils.ExtendedResultSet._ -import fr.acinq.eclair.db.migration.MigrateDb.{checkVersions, migrateTable} -import fr.acinq.eclair.wire.protocol.LightningMessageCodecs.{channelAnnouncementCodec, channelUpdateCodec, nodeAnnouncementCodec} -import scodec.bits.BitVector - -import java.sql.{Connection, PreparedStatement, ResultSet} - -object MigrateNetworkDb { - - private def migrateNodesTable(source: Connection, destination: Connection): Int = { - val sourceTable = "nodes" - val insertSql = "INSERT INTO network.nodes (node_id, data, json) VALUES (?, ?, ?::JSONB)" - - import fr.acinq.eclair.json.JsonSerializers._ - - def migrate(rs: ResultSet, insertStatement: PreparedStatement): Unit = { - insertStatement.setString(1, rs.getByteVector("node_id").toHex) - insertStatement.setBytes(2, rs.getBytes("data")) - val state = nodeAnnouncementCodec.decode(BitVector(rs.getBytes("data"))).require.value - val json = serialization.write(state) - insertStatement.setString(3, json) - } - - migrateTable(source, destination, sourceTable, insertSql, migrate) - } - - private def migrateChannelsTable(source: Connection, destination: Connection): Int = { - val sourceTable = "channels" - val insertSql = "INSERT INTO network.public_channels (short_channel_id, txid, channel_announcement, capacity_sat, channel_update_1, channel_update_2, channel_announcement_json, channel_update_1_json, channel_update_2_json) VALUES (?, ?, ?, ?, ?, ?, ?::JSONB, ?::JSONB, ?::JSONB)" - - import fr.acinq.eclair.json.JsonSerializers._ - - def migrate(rs: ResultSet, insertStatement: PreparedStatement): Unit = { - insertStatement.setLong(1, rs.getLong("short_channel_id")) - insertStatement.setString(2, rs.getString("txid")) - insertStatement.setBytes(3, rs.getBytes("channel_announcement")) - insertStatement.setLong(4, rs.getLong("capacity_sat")) - insertStatement.setBytes(5, rs.getBytes("channel_update_1")) - insertStatement.setBytes(6, rs.getBytes("channel_update_2")) - val ann = channelAnnouncementCodec.decode(rs.getBitVectorOpt("channel_announcement").get).require.value - val channel_update_1_opt = rs.getBitVectorOpt("channel_update_1").map(channelUpdateCodec.decode(_).require.value) - val channel_update_2_opt = rs.getBitVectorOpt("channel_update_2").map(channelUpdateCodec.decode(_).require.value) - val json = serialization.write(ann) - val u1_json = channel_update_1_opt.map(serialization.write(_)).orNull - val u2_json = channel_update_2_opt.map(serialization.write(_)).orNull - insertStatement.setString(7, json) - insertStatement.setString(8, u1_json) - insertStatement.setString(9, u2_json) - } - - migrateTable(source, destination, sourceTable, insertSql, migrate) - } - - private def migratePrunedTable(source: Connection, destination: Connection): Int = { - val sourceTable = "pruned" - val insertSql = "INSERT INTO network.pruned_channels (short_channel_id) VALUES (?)" - - def migrate(rs: ResultSet, insertStatement: PreparedStatement): Unit = { - insertStatement.setLong(1, rs.getLong("short_channel_id")) - } - - migrateTable(source, destination, sourceTable, insertSql, migrate) - } - - def migrateAllTables(source: Connection, destination: Connection): Unit = { - checkVersions(source, destination, "network", 2, 4) - migrateNodesTable(source, destination) - migrateChannelsTable(source, destination) - migratePrunedTable(source, destination) - } - -} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/MigratePaymentsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/MigratePaymentsDb.scala deleted file mode 100644 index ac2e885bf3..0000000000 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/MigratePaymentsDb.scala +++ /dev/null @@ -1,60 +0,0 @@ -package fr.acinq.eclair.db.migration - -import fr.acinq.eclair.db.jdbc.JdbcUtils.ExtendedResultSet._ -import fr.acinq.eclair.db.migration.MigrateDb.{checkVersions, migrateTable} - -import java.sql.{Connection, PreparedStatement, ResultSet, Timestamp} -import java.time.Instant - -object MigratePaymentsDb { - - private def migrateReceivedPaymentsTable(source: Connection, destination: Connection): Int = { - val sourceTable = "received_payments" - val insertSql = "INSERT INTO payments.received (payment_hash, payment_type, payment_preimage, payment_request, received_msat, created_at, expire_at, received_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)" - - def migrate(rs: ResultSet, insertStatement: PreparedStatement): Unit = { - insertStatement.setString(1, rs.getByteVector("payment_hash").toHex) - insertStatement.setString(2, rs.getString("payment_type")) - insertStatement.setString(3, rs.getByteVector("payment_preimage").toHex) - insertStatement.setString(4, rs.getString("payment_request")) - insertStatement.setObject(5, rs.getLongNullable("received_msat").orNull) - insertStatement.setTimestamp(6, Timestamp.from(Instant.ofEpochMilli(rs.getLong("created_at")))) - insertStatement.setTimestamp(7, Timestamp.from(Instant.ofEpochMilli(rs.getLong("expire_at")))) - insertStatement.setObject(8, rs.getLongNullable("received_at").map(l => Timestamp.from(Instant.ofEpochMilli(l))).orNull) - } - - migrateTable(source, destination, sourceTable, insertSql, migrate) - } - - private def migrateSentPaymentsTable(source: Connection, destination: Connection): Int = { - val sourceTable = "sent_payments" - val insertSql = "INSERT INTO payments.sent (id, parent_id, external_id, payment_hash, payment_preimage, payment_type, amount_msat, fees_msat, recipient_amount_msat, recipient_node_id, payment_request, payment_route, failures, created_at, completed_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)" - - def migrate(rs: ResultSet, insertStatement: PreparedStatement): Unit = { - insertStatement.setString(1, rs.getString("id")) - insertStatement.setString(2, rs.getString("parent_id")) - insertStatement.setString(3, rs.getStringNullable("external_id").orNull) - insertStatement.setString(4, rs.getByteVector("payment_hash").toHex) - insertStatement.setString(5, rs.getByteVector32Nullable("payment_preimage").map(_.toHex).orNull) - insertStatement.setString(6, rs.getString("payment_type")) - insertStatement.setLong(7, rs.getLong("amount_msat")) - insertStatement.setObject(8, rs.getLongNullable("fees_msat").orNull) - insertStatement.setLong(9, rs.getLong("recipient_amount_msat")) - insertStatement.setString(10, rs.getByteVector("recipient_node_id").toHex) - insertStatement.setString(11, rs.getStringNullable("payment_request").orNull) - insertStatement.setBytes(12, rs.getBytes("payment_route")) - insertStatement.setBytes(13, rs.getBytes("failures")) - insertStatement.setTimestamp(14, Timestamp.from(Instant.ofEpochMilli(rs.getLong("created_at")))) - insertStatement.setObject(15, rs.getLongNullable("completed_at").map(l => Timestamp.from(Instant.ofEpochMilli(l))).orNull) - } - - migrateTable(source, destination, sourceTable, insertSql, migrate) - } - - def migrateAllTables(source: Connection, destination: Connection): Unit = { - checkVersions(source, destination, "payments", 4, 6) - migrateReceivedPaymentsTable(source, destination) - migrateSentPaymentsTable(source, destination) - } - -} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/MigratePeersDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/MigratePeersDb.scala deleted file mode 100644 index 77021f3a3d..0000000000 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/MigratePeersDb.scala +++ /dev/null @@ -1,41 +0,0 @@ -package fr.acinq.eclair.db.migration - -import fr.acinq.eclair.db.jdbc.JdbcUtils.ExtendedResultSet._ -import fr.acinq.eclair.db.migration.MigrateDb.{checkVersions, migrateTable} - -import java.sql.{Connection, PreparedStatement, ResultSet} - -object MigratePeersDb { - - private def migratePeersTable(source: Connection, destination: Connection): Int = { - val sourceTable = "peers" - val insertSql = "INSERT INTO local.peers (node_id, data) VALUES (?, ?)" - - def migrate(rs: ResultSet, insertStatement: PreparedStatement): Unit = { - insertStatement.setString(1, rs.getByteVector("node_id").toHex) - insertStatement.setBytes(2, rs.getBytes("data")) - } - - migrateTable(source, destination, sourceTable, insertSql, migrate) - } - - private def migrateRelayFeesTable(source: Connection, destination: Connection): Int = { - val sourceTable = "relay_fees" - val insertSql = "INSERT INTO local.relay_fees (node_id, fee_base_msat, fee_proportional_millionths) VALUES (?, ?, ?)" - - def migrate(rs: ResultSet, insertStatement: PreparedStatement): Unit = { - insertStatement.setString(1, rs.getByteVector("node_id").toHex) - insertStatement.setLong(2, rs.getLong("fee_base_msat")) - insertStatement.setLong(3, rs.getLong("fee_proportional_millionths")) - } - - migrateTable(source, destination, sourceTable, insertSql, migrate) - } - - def migrateAllTables(source: Connection, destination: Connection): Unit = { - checkVersions(source, destination, "peers", 2, 3) - migratePeersTable(source, destination) - migrateRelayFeesTable(source, destination) - } - -} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/MigratePendingCommandsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/MigratePendingCommandsDb.scala deleted file mode 100644 index 6a01208766..0000000000 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/MigratePendingCommandsDb.scala +++ /dev/null @@ -1,28 +0,0 @@ -package fr.acinq.eclair.db.migration - -import fr.acinq.eclair.db.jdbc.JdbcUtils.ExtendedResultSet._ -import fr.acinq.eclair.db.migration.MigrateDb.{checkVersions, migrateTable} - -import java.sql.{Connection, PreparedStatement, ResultSet} - -object MigratePendingCommandsDb { - - private def migratePendingSettlementCommandsTable(source: Connection, destination: Connection): Int = { - val sourceTable = "pending_settlement_commands" - val insertSql = "INSERT INTO local.pending_settlement_commands (channel_id, htlc_id, data) VALUES (?, ?, ?)" - - def migrate(rs: ResultSet, insertStatement: PreparedStatement): Unit = { - insertStatement.setString(1, rs.getByteVector("channel_id").toHex) - insertStatement.setLong(2, rs.getLong("htlc_id")) - insertStatement.setBytes(3, rs.getBytes("data")) - } - - migrateTable(source, destination, sourceTable, insertSql, migrate) - } - - def migrateAllTables(source: Connection, destination: Connection): Unit = { - checkVersions(source, destination, "pending_relay", 2, 3) - migratePendingSettlementCommandsTable(source, destination) - } - -} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgAuditDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgAuditDb.scala index 6efd253b5a..67e1c590f4 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgAuditDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgAuditDb.scala @@ -25,7 +25,6 @@ import fr.acinq.eclair.db.Monitoring.Metrics.withMetrics import fr.acinq.eclair.db.Monitoring.Tags.DbBackends import fr.acinq.eclair.db._ import fr.acinq.eclair.payment._ -import fr.acinq.eclair.transactions.Transactions.PlaceHolderPubKey import fr.acinq.eclair.{MilliSatoshi, MilliSatoshiLong, Paginated, TimestampMilli} import grizzled.slf4j.Logging @@ -392,7 +391,8 @@ class PgAuditDb(implicit ds: DataSource) extends AuditDb with Logging { rs.getByteVector32FromHex("payment_preimage"), MilliSatoshi(rs.getLong("recipient_amount_msat")), PublicKey(rs.getByteVectorFromHex("recipient_node_id")), - Seq(part)) + Seq(part), + None) } sentByParentId + (parentId -> sent) }.values.toSeq.sortBy(_.timestamp) @@ -466,9 +466,10 @@ class PgAuditDb(implicit ds: DataSource) extends AuditDb with Logging { case Some(RelayedPart(_, _, _, "channel", _)) => incoming.zip(outgoing).map { case (in, out) => ChannelPaymentRelayed(in.amount, out.amount, paymentHash, in.channelId, out.channelId, in.receivedAt, out.settledAt) } - case Some(RelayedPart(_, _, _, "trampoline", _)) => - val (nextTrampolineAmount, nextTrampolineNodeId) = trampolineByHash.getOrElse(paymentHash, (0 msat, PlaceHolderPubKey)) - TrampolinePaymentRelayed(paymentHash, incoming, outgoing, nextTrampolineNodeId, nextTrampolineAmount) :: Nil + case Some(RelayedPart(_, _, _, "trampoline", _)) => trampolineByHash.get(paymentHash) match { + case Some((nextTrampolineAmount, nextTrampolineNodeId)) => TrampolinePaymentRelayed(paymentHash, incoming, outgoing, nextTrampolineNodeId, nextTrampolineAmount) :: Nil + case None => Nil + } case Some(RelayedPart(_, _, _, "on-the-fly-funding", _)) => Seq(OnTheFlyFundingPaymentRelayed(paymentHash, incoming, outgoing)) case _ => Nil diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgChannelsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgChannelsDb.scala index 599c46db91..43f3d7617f 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgChannelsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgChannelsDb.scala @@ -17,26 +17,26 @@ package fr.acinq.eclair.db.pg import com.zaxxer.hikari.util.IsolationLevel -import fr.acinq.bitcoin.scalacompat.ByteVector32 import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey -import fr.acinq.eclair.channel.PersistentChannelData +import fr.acinq.bitcoin.scalacompat.{ByteVector32, Satoshi, TxId} +import fr.acinq.eclair.channel._ import fr.acinq.eclair.db.ChannelsDb import fr.acinq.eclair.db.DbEventHandler.ChannelEvent import fr.acinq.eclair.db.Monitoring.Metrics.withMetrics import fr.acinq.eclair.db.Monitoring.Tags.DbBackends import fr.acinq.eclair.db.pg.PgUtils.PgLock import fr.acinq.eclair.wire.internal.channel.ChannelCodecs.channelDataCodec -import fr.acinq.eclair.{CltvExpiry, Paginated} +import fr.acinq.eclair.{CltvExpiry, MilliSatoshi, Paginated} import grizzled.slf4j.Logging import scodec.bits.BitVector -import java.sql.{Connection, Statement, Timestamp} +import java.sql.{Connection, Timestamp} import java.time.Instant import javax.sql.DataSource object PgChannelsDb { val DB_NAME = "channels" - val CURRENT_VERSION = 10 + val CURRENT_VERSION = 12 } class PgChannelsDb(implicit ds: DataSource, lock: PgLock) extends ChannelsDb with Logging { @@ -49,89 +49,13 @@ class PgChannelsDb(implicit ds: DataSource, lock: PgLock) extends ChannelsDb wit inTransaction { pg => using(pg.createStatement()) { statement => - - def migration23(statement: Statement): Unit = { - statement.executeUpdate("ALTER TABLE local_channels ADD COLUMN created_timestamp BIGINT") - statement.executeUpdate("ALTER TABLE local_channels ADD COLUMN last_payment_sent_timestamp BIGINT") - statement.executeUpdate("ALTER TABLE local_channels ADD COLUMN last_payment_received_timestamp BIGINT") - statement.executeUpdate("ALTER TABLE local_channels ADD COLUMN last_connected_timestamp BIGINT") - statement.executeUpdate("ALTER TABLE local_channels ADD COLUMN closed_timestamp BIGINT") - } - - def migration34(statement: Statement): Unit = { - statement.executeUpdate("ALTER TABLE local_channels ALTER COLUMN created_timestamp SET DATA TYPE TIMESTAMP WITH TIME ZONE USING timestamp with time zone 'epoch' + created_timestamp * interval '1 millisecond'") - statement.executeUpdate("ALTER TABLE local_channels ALTER COLUMN last_payment_sent_timestamp SET DATA TYPE TIMESTAMP WITH TIME ZONE USING timestamp with time zone 'epoch' + last_payment_sent_timestamp * interval '1 millisecond'") - statement.executeUpdate("ALTER TABLE local_channels ALTER COLUMN last_payment_received_timestamp SET DATA TYPE TIMESTAMP WITH TIME ZONE USING timestamp with time zone 'epoch' + last_payment_received_timestamp * interval '1 millisecond'") - statement.executeUpdate("ALTER TABLE local_channels ALTER COLUMN last_connected_timestamp SET DATA TYPE TIMESTAMP WITH TIME ZONE USING timestamp with time zone 'epoch' + last_connected_timestamp * interval '1 millisecond'") - statement.executeUpdate("ALTER TABLE local_channels ALTER COLUMN closed_timestamp SET DATA TYPE TIMESTAMP WITH TIME ZONE USING timestamp with time zone 'epoch' + closed_timestamp * interval '1 millisecond'") - - statement.executeUpdate("ALTER TABLE htlc_infos ALTER COLUMN commitment_number SET DATA TYPE BIGINT USING commitment_number::BIGINT") - } - - def migration45(statement: Statement): Unit = { - statement.executeUpdate("ALTER TABLE local_channels ADD COLUMN json JSONB") - resetJsonColumns(pg, oldTableName = true) - statement.executeUpdate("ALTER TABLE local_channels ALTER COLUMN json SET NOT NULL") - statement.executeUpdate("CREATE INDEX local_channels_type_idx ON local_channels ((json->>'type'))") - statement.executeUpdate("CREATE INDEX local_channels_remote_node_id_idx ON local_channels ((json->'commitments'->'params'->'remoteParams'->>'nodeId'))") - } - - def migration56(statement: Statement): Unit = { - statement.executeUpdate("CREATE SCHEMA IF NOT EXISTS local") - statement.executeUpdate("ALTER TABLE local_channels SET SCHEMA local") - statement.executeUpdate("ALTER TABLE local.local_channels RENAME TO channels") - statement.executeUpdate("ALTER TABLE htlc_infos SET SCHEMA local") - } - - def migration67(): Unit = { - migrateTable(pg, pg, - "local.channels", - "UPDATE local.channels SET data=?, json=?::JSONB WHERE channel_id=?", - (rs, statement) => { - // This forces a re-serialization of the channel data with latest codecs, because as of codecs v3 we don't - // store local commitment signatures anymore, and we want to clean up existing data - val state = channelDataCodec.decode(BitVector(rs.getBytes("data"))).require.value - val data = channelDataCodec.encode(state).require.toByteArray - val json = serialization.write(state) - statement.setBytes(1, data) - statement.setString(2, json) - statement.setString(3, state.channelId.toHex) - } - )(logger) - } - - def migration78(statement: Statement): Unit = { - statement.executeUpdate("DROP INDEX IF EXISTS local.local_channels_remote_node_id_idx") - statement.executeUpdate("ALTER TABLE local.channels ADD COLUMN remote_node_id TEXT") - migrateTable(pg, pg, - "local.channels", - "UPDATE local.channels SET remote_node_id=? WHERE channel_id=?", - (rs, statement) => { - val state = channelDataCodec.decode(BitVector(rs.getBytes("data"))).require.value - statement.setString(1, state.remoteNodeId.toHex) - statement.setString(2, state.channelId.toHex) - })(logger) - statement.executeUpdate("ALTER TABLE local.channels ALTER COLUMN remote_node_id SET NOT NULL") - statement.executeUpdate("CREATE INDEX local_channels_remote_node_id_idx ON local.channels(remote_node_id)") - } - - def migration89(statement: Statement): Unit = { - statement.executeUpdate("CREATE TABLE local.htlc_infos_to_remove (channel_id TEXT NOT NULL PRIMARY KEY, before_commitment_number BIGINT NOT NULL)") - } - - def migration910(statement: Statement): Unit = { - // We're changing our composite index to two distinct indices to improve performance. - statement.executeUpdate("CREATE INDEX htlc_infos_channel_id_idx ON local.htlc_infos(channel_id)") - statement.executeUpdate("CREATE INDEX htlc_infos_commitment_number_idx ON local.htlc_infos(commitment_number)") - statement.executeUpdate("DROP INDEX IF EXISTS local.htlc_infos_idx") - } - getVersion(statement, DB_NAME) match { case None => statement.executeUpdate("CREATE SCHEMA IF NOT EXISTS local") - statement.executeUpdate("CREATE TABLE local.channels (channel_id TEXT NOT NULL PRIMARY KEY, remote_node_id TEXT NOT NULL, data BYTEA NOT NULL, json JSONB NOT NULL, is_closed BOOLEAN NOT NULL DEFAULT FALSE, created_timestamp TIMESTAMP WITH TIME ZONE, last_payment_sent_timestamp TIMESTAMP WITH TIME ZONE, last_payment_received_timestamp TIMESTAMP WITH TIME ZONE, last_connected_timestamp TIMESTAMP WITH TIME ZONE, closed_timestamp TIMESTAMP WITH TIME ZONE)") - statement.executeUpdate("CREATE TABLE local.htlc_infos (channel_id TEXT NOT NULL, commitment_number BIGINT NOT NULL, payment_hash TEXT NOT NULL, cltv_expiry BIGINT NOT NULL, FOREIGN KEY(channel_id) REFERENCES local.channels(channel_id))") + statement.executeUpdate("CREATE TABLE local.channels (channel_id TEXT NOT NULL PRIMARY KEY, remote_node_id TEXT NOT NULL, data BYTEA NOT NULL, json JSONB NOT NULL, created_timestamp TIMESTAMP WITH TIME ZONE, last_payment_sent_timestamp TIMESTAMP WITH TIME ZONE, last_payment_received_timestamp TIMESTAMP WITH TIME ZONE, last_connected_timestamp TIMESTAMP WITH TIME ZONE)") + statement.executeUpdate("CREATE TABLE local.channels_closed (channel_id TEXT NOT NULL PRIMARY KEY, remote_node_id TEXT NOT NULL, funding_txid TEXT NOT NULL, funding_output_index BIGINT NOT NULL, funding_tx_index BIGINT NOT NULL, funding_key_path TEXT NOT NULL, channel_features TEXT NOT NULL, is_channel_opener BOOLEAN NOT NULL, commitment_format TEXT NOT NULL, announced BOOLEAN NOT NULL, capacity_satoshis BIGINT NOT NULL, closing_txid TEXT NOT NULL, closing_type TEXT NOT NULL, closing_script TEXT NOT NULL, local_balance_msat BIGINT NOT NULL, remote_balance_msat BIGINT NOT NULL, closing_amount_satoshis BIGINT NOT NULL, created_at TIMESTAMP WITH TIME ZONE NOT NULL, closed_at TIMESTAMP WITH TIME ZONE NOT NULL)") + statement.executeUpdate("CREATE TABLE local.htlc_infos (channel_id TEXT NOT NULL, commitment_number BIGINT NOT NULL, payment_hash TEXT NOT NULL, cltv_expiry BIGINT NOT NULL)") statement.executeUpdate("CREATE TABLE local.htlc_infos_to_remove (channel_id TEXT NOT NULL PRIMARY KEY, before_commitment_number BIGINT NOT NULL)") statement.executeUpdate("CREATE INDEX local_channels_type_idx ON local.channels ((json->>'type'))") @@ -140,32 +64,7 @@ class PgChannelsDb(implicit ds: DataSource, lock: PgLock) extends ChannelsDb wit // This is more efficient because we're writing a lot to this table but only reading when a channel is force-closed. statement.executeUpdate("CREATE INDEX htlc_infos_channel_id_idx ON local.htlc_infos(channel_id)") statement.executeUpdate("CREATE INDEX htlc_infos_commitment_number_idx ON local.htlc_infos(commitment_number)") - case Some(v@(2 | 3 | 4 | 5 | 6 | 7 | 8 | 9)) => - logger.warn(s"migrating db $DB_NAME, found version=$v current=$CURRENT_VERSION") - if (v < 3) { - migration23(statement) - } - if (v < 4) { - migration34(statement) - } - if (v < 5) { - migration45(statement) - } - if (v < 6) { - migration56(statement) - } - if (v < 7) { - migration67() - } - if (v < 8) { - migration78(statement) - } - if (v < 9) { - migration89(statement) - } - if (v < 10) { - migration910(statement) - } + statement.executeUpdate("CREATE INDEX channels_closed_remote_node_id_idx ON local.channels_closed(remote_node_id)") case Some(CURRENT_VERSION) => () // table is up-to-date, nothing to do case Some(unknownVersion) => throw new RuntimeException(s"Unknown version of DB $DB_NAME found, version=$unknownVersion") } @@ -193,8 +92,8 @@ class PgChannelsDb(implicit ds: DataSource, lock: PgLock) extends ChannelsDb wit val encoded = channelDataCodec.encode(data).require.toByteArray using(pg.prepareStatement( """ - | INSERT INTO local.channels (channel_id, remote_node_id, data, json, created_timestamp, last_connected_timestamp, is_closed) - | VALUES (?, ?, ?, ?::JSONB, ?, ?, FALSE) + | INSERT INTO local.channels (channel_id, remote_node_id, data, json, created_timestamp, last_connected_timestamp) + | VALUES (?, ?, ?, ?::JSONB, ?, ?) | ON CONFLICT (channel_id) | DO UPDATE SET data = EXCLUDED.data, json = EXCLUDED.json ; | """.stripMargin)) { statement => @@ -211,7 +110,7 @@ class PgChannelsDb(implicit ds: DataSource, lock: PgLock) extends ChannelsDb wit override def getChannel(channelId: ByteVector32): Option[PersistentChannelData] = withMetrics("channels/get-channel", DbBackends.Postgres) { withLock { pg => - using(pg.prepareStatement("SELECT data FROM local.channels WHERE channel_id=? AND is_closed=FALSE")) { statement => + using(pg.prepareStatement("SELECT data FROM local.channels WHERE channel_id=?")) { statement => statement.setString(1, channelId.toHex) statement.executeQuery.mapCodec(channelDataCodec).lastOption } @@ -239,7 +138,7 @@ class PgChannelsDb(implicit ds: DataSource, lock: PgLock) extends ChannelsDb wit timestampColumn_opt.foreach(updateChannelMetaTimestampColumn(channelId, _)) } - override def removeChannel(channelId: ByteVector32): Unit = withMetrics("channels/remove-channel", DbBackends.Postgres) { + override def removeChannel(channelId: ByteVector32, data_opt: Option[DATA_CLOSED]): Unit = withMetrics("channels/remove-channel", DbBackends.Postgres) { withLock { pg => using(pg.prepareStatement("DELETE FROM local.pending_settlement_commands WHERE channel_id=?")) { statement => statement.setString(1, channelId.toHex) @@ -250,9 +149,39 @@ class PgChannelsDb(implicit ds: DataSource, lock: PgLock) extends ChannelsDb wit // We instead run an asynchronous job to clean up that data in small batches. markHtlcInfosForRemoval(channelId, Long.MaxValue) - using(pg.prepareStatement("UPDATE local.channels SET is_closed=TRUE, closed_timestamp=? WHERE channel_id=?")) { statement => - statement.setTimestamp(1, Timestamp.from(Instant.now())) - statement.setString(2, channelId.toHex) + // If we have useful closing data for this channel, we keep it in a dedicated table. + data_opt.foreach(data => { + val createdAt_opt = using(pg.prepareStatement("SELECT created_timestamp FROM local.channels WHERE channel_id=?")) { statement => + statement.setString(1, channelId.toHex) + statement.executeQuery().flatMap(rs => rs.getTimestampNullable("created_timestamp")).headOption + } + using(pg.prepareStatement("INSERT INTO local.channels_closed VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT DO NOTHING")) { statement => + statement.setString(1, channelId.toHex) + statement.setString(2, data.remoteNodeId.toHex) + statement.setString(3, data.fundingTxId.value.toHex) + statement.setLong(4, data.fundingOutputIndex) + statement.setLong(5, data.fundingTxIndex) + statement.setString(6, data.fundingKeyPath) + statement.setString(7, data.channelFeatures) + statement.setBoolean(8, data.isChannelOpener) + statement.setString(9, data.commitmentFormat) + statement.setBoolean(10, data.announced) + statement.setLong(11, data.capacity.toLong) + statement.setString(12, data.closingTxId.value.toHex) + statement.setString(13, data.closingType) + statement.setString(14, data.closingScript.toHex) + statement.setLong(15, data.localBalance.toLong) + statement.setLong(16, data.remoteBalance.toLong) + statement.setLong(17, data.closingAmount.toLong) + statement.setTimestamp(18, createdAt_opt.getOrElse(Timestamp.from(Instant.ofEpochMilli(0)))) + statement.setTimestamp(19, Timestamp.from(Instant.now())) + statement.executeUpdate() + } + }) + + // We can now remove this channel from the active channels table. + using(pg.prepareStatement("DELETE FROM local.channels WHERE channel_id=?")) { statement => + statement.setString(1, channelId.toHex) statement.executeUpdate() } } @@ -301,21 +230,40 @@ class PgChannelsDb(implicit ds: DataSource, lock: PgLock) extends ChannelsDb wit override def listLocalChannels(): Seq[PersistentChannelData] = withMetrics("channels/list-local-channels", DbBackends.Postgres) { withLock { pg => using(pg.createStatement) { statement => - statement.executeQuery("SELECT data FROM local.channels WHERE is_closed=FALSE") + statement.executeQuery("SELECT data FROM local.channels") .mapCodec(channelDataCodec).toSeq } } } - override def listClosedChannels(remoteNodeId_opt: Option[PublicKey], paginated_opt: Option[Paginated]): Seq[PersistentChannelData] = withMetrics("channels/list-closed-channels", DbBackends.Postgres) { + override def listClosedChannels(remoteNodeId_opt: Option[PublicKey], paginated_opt: Option[Paginated]): Seq[DATA_CLOSED] = withMetrics("channels/list-closed-channels", DbBackends.Postgres) { val sql = remoteNodeId_opt match { - case None => "SELECT data FROM local.channels WHERE is_closed=TRUE ORDER BY closed_timestamp DESC" - case Some(remoteNodeId) => s"SELECT data FROM local.channels WHERE is_closed=TRUE AND remote_node_id = '${remoteNodeId.toHex}' ORDER BY closed_timestamp DESC" + case Some(remoteNodeId) => s"SELECT * FROM local.channels_closed WHERE remote_node_id = '${remoteNodeId.toHex}' ORDER BY closed_at DESC" + case None => "SELECT * FROM local.channels_closed ORDER BY closed_at DESC" } withLock { pg => using(pg.prepareStatement(limited(sql, paginated_opt))) { statement => - statement.executeQuery() - .mapCodec(channelDataCodec).toSeq + statement.executeQuery().map { rs => + DATA_CLOSED( + channelId = rs.getByteVector32FromHex("channel_id"), + remoteNodeId = PublicKey(rs.getByteVectorFromHex("remote_node_id")), + fundingTxId = TxId(rs.getByteVector32FromHex("funding_txid")), + fundingOutputIndex = rs.getLong("funding_output_index"), + fundingTxIndex = rs.getLong("funding_tx_index"), + fundingKeyPath = rs.getString("funding_key_path"), + channelFeatures = rs.getString("channel_features"), + isChannelOpener = rs.getBoolean("is_channel_opener"), + commitmentFormat = rs.getString("commitment_format"), + announced = rs.getBoolean("announced"), + capacity = Satoshi(rs.getLong("capacity_satoshis")), + closingTxId = TxId(rs.getByteVector32FromHex("closing_txid")), + closingType = rs.getString("closing_type"), + closingScript = rs.getByteVectorFromHex("closing_script"), + localBalance = MilliSatoshi(rs.getLong("local_balance_msat")), + remoteBalance = MilliSatoshi(rs.getLong("remote_balance_msat")), + closingAmount = Satoshi(rs.getLong("closing_amount_satoshis")) + ) + }.toSeq } } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgPendingCommandsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgPendingCommandsDb.scala index 2ebd7d57b3..eed31da9c5 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgPendingCommandsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgPendingCommandsDb.scala @@ -56,7 +56,6 @@ class PgPendingCommandsDb(implicit ds: DataSource, lock: PgLock) extends Pending getVersion(statement, DB_NAME) match { case None => statement.executeUpdate("CREATE SCHEMA IF NOT EXISTS local") - // note: should we use a foreign key to local_channels table here? statement.executeUpdate("CREATE TABLE local.pending_settlement_commands (channel_id TEXT NOT NULL, htlc_id BIGINT NOT NULL, data BYTEA NOT NULL, PRIMARY KEY(channel_id, htlc_id))") case Some(v@(1 | 2)) => logger.warn(s"migrating db $DB_NAME, found version=$v current=$CURRENT_VERSION") diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteAuditDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteAuditDb.scala index 16e1944f3c..f118766e5b 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteAuditDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteAuditDb.scala @@ -25,7 +25,6 @@ import fr.acinq.eclair.db.Monitoring.Metrics.withMetrics import fr.acinq.eclair.db.Monitoring.Tags.DbBackends import fr.acinq.eclair.db._ import fr.acinq.eclair.payment._ -import fr.acinq.eclair.transactions.Transactions.PlaceHolderPubKey import fr.acinq.eclair.{MilliSatoshi, MilliSatoshiLong, Paginated, TimestampMilli} import grizzled.slf4j.Logging @@ -364,7 +363,8 @@ class SqliteAuditDb(val sqlite: Connection) extends AuditDb with Logging { rs.getByteVector32("payment_preimage"), MilliSatoshi(rs.getLong("recipient_amount_msat")), PublicKey(rs.getByteVector("recipient_node_id")), - Seq(part)) + Seq(part), + None) } sentByParentId + (parentId -> sent) }.values.toSeq.sortBy(_.timestamp) @@ -435,9 +435,10 @@ class SqliteAuditDb(val sqlite: Connection) extends AuditDb with Logging { case Some(RelayedPart(_, _, _, "channel", _)) => incoming.zip(outgoing).map { case (in, out) => ChannelPaymentRelayed(in.amount, out.amount, paymentHash, in.channelId, out.channelId, in.receivedAt, out.settledAt) } - case Some(RelayedPart(_, _, _, "trampoline", _)) => - val (nextTrampolineAmount, nextTrampolineNodeId) = trampolineByHash.getOrElse(paymentHash, (0 msat, PlaceHolderPubKey)) - TrampolinePaymentRelayed(paymentHash, incoming, outgoing, nextTrampolineNodeId, nextTrampolineAmount) :: Nil + case Some(RelayedPart(_, _, _, "trampoline", _)) => trampolineByHash.get(paymentHash) match { + case Some((nextTrampolineAmount, nextTrampolineNodeId)) => TrampolinePaymentRelayed(paymentHash, incoming, outgoing, nextTrampolineNodeId, nextTrampolineAmount) :: Nil + case None => Nil + } case Some(RelayedPart(_, _, _, "on-the-fly-funding", _)) => Seq(OnTheFlyFundingPaymentRelayed(paymentHash, incoming, outgoing)) case _ => Nil diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteChannelsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteChannelsDb.scala index f9c54cf2bf..f2bb46bba4 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteChannelsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteChannelsDb.scala @@ -16,23 +16,22 @@ package fr.acinq.eclair.db.sqlite -import fr.acinq.bitcoin.scalacompat.ByteVector32 import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey -import fr.acinq.eclair.channel.PersistentChannelData +import fr.acinq.bitcoin.scalacompat.{ByteVector32, Satoshi, TxId} +import fr.acinq.eclair.channel._ import fr.acinq.eclair.db.ChannelsDb import fr.acinq.eclair.db.DbEventHandler.ChannelEvent import fr.acinq.eclair.db.Monitoring.Metrics.withMetrics import fr.acinq.eclair.db.Monitoring.Tags.DbBackends import fr.acinq.eclair.wire.internal.channel.ChannelCodecs.channelDataCodec -import fr.acinq.eclair.{CltvExpiry, Paginated, TimestampMilli} +import fr.acinq.eclair.{CltvExpiry, MilliSatoshi, Paginated, TimestampMilli} import grizzled.slf4j.Logging -import scodec.bits.BitVector -import java.sql.{Connection, Statement} +import java.sql.Connection object SqliteChannelsDb { val DB_NAME = "channels" - val CURRENT_VERSION = 6 + val CURRENT_VERSION = 8 } class SqliteChannelsDb(val sqlite: Connection) extends ChannelsDb with Logging { @@ -51,71 +50,17 @@ class SqliteChannelsDb(val sqlite: Connection) extends ChannelsDb with Logging { } using(sqlite.createStatement(), inTransaction = true) { statement => - - def migration12(statement: Statement): Unit = { - statement.executeUpdate("ALTER TABLE local_channels ADD COLUMN is_closed BOOLEAN NOT NULL DEFAULT 0") - } - - def migration23(statement: Statement): Unit = { - statement.executeUpdate("ALTER TABLE local_channels ADD COLUMN created_timestamp INTEGER") - statement.executeUpdate("ALTER TABLE local_channels ADD COLUMN last_payment_sent_timestamp INTEGER") - statement.executeUpdate("ALTER TABLE local_channels ADD COLUMN last_payment_received_timestamp INTEGER") - statement.executeUpdate("ALTER TABLE local_channels ADD COLUMN last_connected_timestamp INTEGER") - statement.executeUpdate("ALTER TABLE local_channels ADD COLUMN closed_timestamp INTEGER") - } - - def migration34(): Unit = { - migrateTable(sqlite, sqlite, - "local_channels", - s"UPDATE local_channels SET data=? WHERE channel_id=?", - (rs, statement) => { - // This forces a re-serialization of the channel data with latest codecs, because as of codecs v3 we don't - // store local commitment signatures anymore, and we want to clean up existing data - val state = channelDataCodec.decode(BitVector(rs.getBytes("data"))).require.value - val data = channelDataCodec.encode(state).require.toByteArray - statement.setBytes(1, data) - statement.setBytes(2, state.channelId.toArray) - } - )(logger) - } - - def migration45(): Unit = { - statement.executeUpdate("CREATE TABLE htlc_infos_to_remove (channel_id BLOB NOT NULL PRIMARY KEY, before_commitment_number INTEGER NOT NULL)") - } - - def migration56(): Unit = { - // We're changing our composite index to two distinct indices to improve performance. - statement.executeUpdate("CREATE INDEX htlc_infos_channel_id_idx ON htlc_infos(channel_id)") - statement.executeUpdate("CREATE INDEX htlc_infos_commitment_number_idx ON htlc_infos(commitment_number)") - statement.executeUpdate("DROP INDEX IF EXISTS htlc_infos_idx") - } - getVersion(statement, DB_NAME) match { case None => - statement.executeUpdate("CREATE TABLE local_channels (channel_id BLOB NOT NULL PRIMARY KEY, data BLOB NOT NULL, is_closed BOOLEAN NOT NULL DEFAULT 0, created_timestamp INTEGER, last_payment_sent_timestamp INTEGER, last_payment_received_timestamp INTEGER, last_connected_timestamp INTEGER, closed_timestamp INTEGER)") - statement.executeUpdate("CREATE TABLE htlc_infos (channel_id BLOB NOT NULL, commitment_number INTEGER NOT NULL, payment_hash BLOB NOT NULL, cltv_expiry INTEGER NOT NULL, FOREIGN KEY(channel_id) REFERENCES local_channels(channel_id))") + statement.executeUpdate("CREATE TABLE local_channels (channel_id BLOB NOT NULL PRIMARY KEY, data BLOB NOT NULL, created_timestamp INTEGER, last_payment_sent_timestamp INTEGER, last_payment_received_timestamp INTEGER, last_connected_timestamp INTEGER)") + statement.executeUpdate("CREATE TABLE local_channels_closed (channel_id TEXT NOT NULL PRIMARY KEY, remote_node_id TEXT NOT NULL, funding_txid TEXT NOT NULL, funding_output_index INTEGER NOT NULL, funding_tx_index INTEGER NOT NULL, funding_key_path TEXT NOT NULL, channel_features TEXT NOT NULL, is_channel_opener BOOLEAN NOT NULL, commitment_format TEXT NOT NULL, announced BOOLEAN NOT NULL, capacity_satoshis INTEGER NOT NULL, closing_txid TEXT NOT NULL, closing_type TEXT NOT NULL, closing_script TEXT NOT NULL, local_balance_msat INTEGER NOT NULL, remote_balance_msat INTEGER NOT NULL, closing_amount_satoshis INTEGER NOT NULL, created_at INTEGER NOT NULL, closed_at INTEGER NOT NULL)") + statement.executeUpdate("CREATE TABLE htlc_infos (channel_id BLOB NOT NULL, commitment_number INTEGER NOT NULL, payment_hash BLOB NOT NULL, cltv_expiry INTEGER NOT NULL)") statement.executeUpdate("CREATE TABLE htlc_infos_to_remove (channel_id BLOB NOT NULL PRIMARY KEY, before_commitment_number INTEGER NOT NULL)") // Note that we use two distinct indices instead of a composite index on (channel_id, commitment_number). // This is more efficient because we're writing a lot to this table but only reading when a channel is force-closed. statement.executeUpdate("CREATE INDEX htlc_infos_channel_id_idx ON htlc_infos(channel_id)") statement.executeUpdate("CREATE INDEX htlc_infos_commitment_number_idx ON htlc_infos(commitment_number)") - case Some(v@(1 | 2 | 3 | 4 | 5)) => - logger.warn(s"migrating db $DB_NAME, found version=$v current=$CURRENT_VERSION") - if (v < 2) { - migration12(statement) - } - if (v < 3) { - migration23(statement) - } - if (v < 4) { - migration34() - } - if (v < 5) { - migration45() - } - if (v < 6) { - migration56() - } + statement.executeUpdate("CREATE INDEX local_channels_closed_remote_node_id_idx ON local_channels_closed(remote_node_id)") case Some(CURRENT_VERSION) => () // table is up-to-date, nothing to do case Some(unknownVersion) => throw new RuntimeException(s"Unknown version of DB $DB_NAME found, version=$unknownVersion") } @@ -128,7 +73,7 @@ class SqliteChannelsDb(val sqlite: Connection) extends ChannelsDb with Logging { update.setBytes(1, encoded) update.setBytes(2, data.channelId.toArray) if (update.executeUpdate() == 0) { - using(sqlite.prepareStatement("INSERT INTO local_channels (channel_id, data, created_timestamp, last_connected_timestamp, is_closed) VALUES (?, ?, ?, ?, 0)")) { statement => + using(sqlite.prepareStatement("INSERT INTO local_channels (channel_id, data, created_timestamp, last_connected_timestamp) VALUES (?, ?, ?, ?)")) { statement => statement.setBytes(1, data.channelId.toArray) statement.setBytes(2, encoded) statement.setLong(3, TimestampMilli.now().toLong) @@ -140,7 +85,7 @@ class SqliteChannelsDb(val sqlite: Connection) extends ChannelsDb with Logging { } override def getChannel(channelId: ByteVector32): Option[PersistentChannelData] = withMetrics("channels/get-channel", DbBackends.Sqlite) { - using(sqlite.prepareStatement("SELECT data FROM local_channels WHERE channel_id=? AND is_closed=0")) { statement => + using(sqlite.prepareStatement("SELECT data FROM local_channels WHERE channel_id=?")) { statement => statement.setBytes(1, channelId.toArray) statement.executeQuery.mapCodec(channelDataCodec).lastOption } @@ -167,7 +112,7 @@ class SqliteChannelsDb(val sqlite: Connection) extends ChannelsDb with Logging { timestampColumn_opt.foreach(updateChannelMetaTimestampColumn(channelId, _)) } - override def removeChannel(channelId: ByteVector32): Unit = withMetrics("channels/remove-channel", DbBackends.Sqlite) { + override def removeChannel(channelId: ByteVector32, data_opt: Option[DATA_CLOSED]): Unit = withMetrics("channels/remove-channel", DbBackends.Sqlite) { using(sqlite.prepareStatement("DELETE FROM pending_settlement_commands WHERE channel_id=?")) { statement => statement.setBytes(1, channelId.toArray) statement.executeUpdate() @@ -177,9 +122,39 @@ class SqliteChannelsDb(val sqlite: Connection) extends ChannelsDb with Logging { // We instead run an asynchronous job to clean up that data in small batches. markHtlcInfosForRemoval(channelId, Long.MaxValue) - using(sqlite.prepareStatement("UPDATE local_channels SET is_closed=1, closed_timestamp=? WHERE channel_id=?")) { statement => - statement.setLong(1, TimestampMilli.now().toLong) - statement.setBytes(2, channelId.toArray) + // If we have useful closing data for this channel, we keep it in a dedicated table. + data_opt.foreach(data => { + val createdAt_opt = using(sqlite.prepareStatement("SELECT created_timestamp FROM local_channels WHERE channel_id=?")) { statement => + statement.setBytes(1, channelId.toArray) + statement.executeQuery().flatMap(rs => rs.getLongNullable("created_timestamp")).headOption + } + using(sqlite.prepareStatement("INSERT OR IGNORE INTO local_channels_closed VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")) { statement => + statement.setString(1, channelId.toHex) + statement.setString(2, data.remoteNodeId.toHex) + statement.setString(3, data.fundingTxId.value.toHex) + statement.setLong(4, data.fundingOutputIndex) + statement.setLong(5, data.fundingTxIndex) + statement.setString(6, data.fundingKeyPath) + statement.setString(7, data.channelFeatures) + statement.setBoolean(8, data.isChannelOpener) + statement.setString(9, data.commitmentFormat) + statement.setBoolean(10, data.announced) + statement.setLong(11, data.capacity.toLong) + statement.setString(12, data.closingTxId.value.toHex) + statement.setString(13, data.closingType) + statement.setString(14, data.closingScript.toHex) + statement.setLong(15, data.localBalance.toLong) + statement.setLong(16, data.remoteBalance.toLong) + statement.setLong(17, data.closingAmount.toLong) + statement.setLong(18, createdAt_opt.getOrElse(0)) + statement.setLong(19, TimestampMilli.now().toLong) + statement.executeUpdate() + } + }) + + // We can now remove this channel from the active channels table. + using(sqlite.prepareStatement("DELETE FROM local_channels WHERE channel_id=?")) { statement => + statement.setBytes(1, channelId.toArray) statement.executeUpdate() } } @@ -228,29 +203,39 @@ class SqliteChannelsDb(val sqlite: Connection) extends ChannelsDb with Logging { override def listLocalChannels(): Seq[PersistentChannelData] = withMetrics("channels/list-local-channels", DbBackends.Sqlite) { using(sqlite.createStatement) { statement => - statement.executeQuery("SELECT data FROM local_channels WHERE is_closed=0") + statement.executeQuery("SELECT data FROM local_channels") .mapCodec(channelDataCodec).toSeq } } - - override def listClosedChannels(remoteNodeId_opt: Option[PublicKey], paginated_opt: Option[Paginated]): Seq[PersistentChannelData] = withMetrics("channels/list-closed-channels", DbBackends.Sqlite) { - val sql = "SELECT data FROM local_channels WHERE is_closed=1 ORDER BY closed_timestamp DESC" - remoteNodeId_opt match { - case None => - using(sqlite.prepareStatement(limited(sql, paginated_opt))) { statement => - statement.executeQuery().mapCodec(channelDataCodec).toSeq - } - case Some(nodeId) => - using(sqlite.prepareStatement(sql)) { statement => - val filtered = statement.executeQuery() - .mapCodec(channelDataCodec).filter(_.remoteNodeId == nodeId) - val limited = paginated_opt match { - case None => filtered - case Some(p) => filtered.slice(p.skip, p.skip + p.count) - } - limited.toSeq - } + override def listClosedChannels(remoteNodeId_opt: Option[PublicKey], paginated_opt: Option[Paginated]): Seq[DATA_CLOSED] = withMetrics("channels/list-closed-channels", DbBackends.Sqlite) { + val sql = remoteNodeId_opt match { + case Some(_) => "SELECT * FROM local_channels_closed WHERE remote_node_id=? ORDER BY closed_at DESC" + case None => "SELECT * FROM local_channels_closed ORDER BY closed_at DESC" + } + using(sqlite.prepareStatement(limited(sql, paginated_opt))) { statement => + remoteNodeId_opt.foreach(remoteNodeId => statement.setString(1, remoteNodeId.toHex)) + statement.executeQuery().map { rs => + DATA_CLOSED( + channelId = rs.getByteVector32FromHex("channel_id"), + remoteNodeId = PublicKey(rs.getByteVectorFromHex("remote_node_id")), + fundingTxId = TxId(rs.getByteVector32FromHex("funding_txid")), + fundingOutputIndex = rs.getLong("funding_output_index"), + fundingTxIndex = rs.getLong("funding_tx_index"), + fundingKeyPath = rs.getString("funding_key_path"), + channelFeatures = rs.getString("channel_features"), + isChannelOpener = rs.getBoolean("is_channel_opener"), + commitmentFormat = rs.getString("commitment_format"), + announced = rs.getBoolean("announced"), + capacity = Satoshi(rs.getLong("capacity_satoshis")), + closingTxId = TxId(rs.getByteVector32FromHex("closing_txid")), + closingType = rs.getString("closing_type"), + closingScript = rs.getByteVectorFromHex("closing_script"), + localBalance = MilliSatoshi(rs.getLong("local_balance_msat")), + remoteBalance = MilliSatoshi(rs.getLong("remote_balance_msat")), + closingAmount = Satoshi(rs.getLong("closing_amount_satoshis")) + ) + }.toSeq } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePendingCommandsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePendingCommandsDb.scala index 351a229e74..64a14e5104 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePendingCommandsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePendingCommandsDb.scala @@ -45,7 +45,6 @@ class SqlitePendingCommandsDb(val sqlite: Connection) extends PendingCommandsDb getVersion(statement, DB_NAME) match { case None => - // note: should we use a foreign key to local_channels table here? statement.executeUpdate("CREATE TABLE pending_settlement_commands (channel_id BLOB NOT NULL, htlc_id INTEGER NOT NULL, data BLOB NOT NULL, PRIMARY KEY(channel_id, htlc_id))") case Some(v@1) => logger.warn(s"migrating db $DB_NAME, found version=$v current=$CURRENT_VERSION") diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/OpenChannelInterceptor.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/OpenChannelInterceptor.scala index 009c99f234..8c50e398ee 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/OpenChannelInterceptor.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/OpenChannelInterceptor.scala @@ -22,16 +22,16 @@ import akka.actor.typed.scaladsl.adapter.TypedActorRefOps import akka.actor.typed.scaladsl.{ActorContext, Behaviors} import akka.actor.typed.{ActorRef, Behavior} import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey -import fr.acinq.bitcoin.scalacompat.{BtcDouble, ByteVector32, Satoshi, SatoshiLong, Script} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, Satoshi, SatoshiLong, Script} import fr.acinq.eclair.Features.Wumbo -import fr.acinq.eclair.blockchain.OnChainPubkeyCache +import fr.acinq.eclair.blockchain.OnChainAddressCache import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.io.Peer.{OpenChannelResponse, SpawnChannelNonInitiator} import fr.acinq.eclair.io.PendingChannelsRateLimiter.AddOrRejectChannel import fr.acinq.eclair.wire.protocol import fr.acinq.eclair.wire.protocol.{Error, LiquidityAds, NodeAddress} -import fr.acinq.eclair.{AcceptOpenChannel, CltvExpiryDelta, Features, InitFeature, InterceptOpenChannelPlugin, InterceptOpenChannelReceived, InterceptOpenChannelResponse, Logs, MilliSatoshi, NodeParams, RejectOpenChannel, ToMilliSatoshiConversion} +import fr.acinq.eclair.{AcceptOpenChannel, Features, InitFeature, InterceptOpenChannelPlugin, InterceptOpenChannelReceived, InterceptOpenChannelResponse, Logs, NodeParams, RejectOpenChannel} import scodec.bits.ByteVector import scala.concurrent.duration.{DurationInt, FiniteDuration} @@ -70,46 +70,21 @@ object OpenChannelInterceptor { private case object PluginTimeout extends QueryPluginCommands // @formatter:on - /** DefaultParams are a subset of ChannelData.LocalParams that can be modified by an InterceptOpenChannelPlugin */ - case class DefaultParams(dustLimit: Satoshi, - maxHtlcValueInFlightMsat: MilliSatoshi, - htlcMinimum: MilliSatoshi, - toSelfDelay: CltvExpiryDelta, - maxAcceptedHtlcs: Int) - - def apply(peer: ActorRef[Any], nodeParams: NodeParams, remoteNodeId: PublicKey, wallet: OnChainPubkeyCache, pendingChannelsRateLimiter: ActorRef[PendingChannelsRateLimiter.Command], pluginTimeout: FiniteDuration = 1 minute): Behavior[Command] = + def apply(peer: ActorRef[Any], nodeParams: NodeParams, remoteNodeId: PublicKey, wallet: OnChainAddressCache, pendingChannelsRateLimiter: ActorRef[PendingChannelsRateLimiter.Command], pluginTimeout: FiniteDuration = 1 minute): Behavior[Command] = Behaviors.setup { context => Behaviors.withMdc(Logs.mdc(remoteNodeId_opt = Some(remoteNodeId))) { new OpenChannelInterceptor(peer, pendingChannelsRateLimiter, pluginTimeout, nodeParams, wallet, context).waitForRequest() } } - private def computeMaxHtlcValueInFlight(nodeParams: NodeParams, fundingAmount: Satoshi, unlimitedMaxHtlcValueInFlight: Boolean): MilliSatoshi = { - if (unlimitedMaxHtlcValueInFlight) { - // We don't want to impose limits on the amount in flight, typically to allow fully emptying the channel. - 21e6.btc.toMilliSatoshi - } else { - // NB: when we're the initiator, we don't know yet if the remote peer will contribute to the funding amount, so - // the percentage-based value may be underestimated. That's ok, this is a security parameter so it makes sense to - // base it on the amount that we're contributing instead of the total funding amount. - nodeParams.channelConf.maxHtlcValueInFlightMsat.min(fundingAmount * nodeParams.channelConf.maxHtlcValueInFlightPercent / 100) - } - } - - def makeChannelParams(nodeParams: NodeParams, initFeatures: Features[InitFeature], upfrontShutdownScript_opt: Option[ByteVector], walletStaticPaymentBasepoint_opt: Option[PublicKey], isChannelOpener: Boolean, paysCommitTxFees: Boolean, dualFunded: Boolean, fundingAmount: Satoshi, unlimitedMaxHtlcValueInFlight: Boolean): LocalParams = { - LocalParams( + def makeChannelParams(nodeParams: NodeParams, initFeatures: Features[InitFeature], upfrontShutdownScript_opt: Option[ByteVector], isChannelOpener: Boolean, paysCommitTxFees: Boolean, dualFunded: Boolean, fundingAmount: Satoshi): LocalChannelParams = { + LocalChannelParams( nodeParams.nodeId, - nodeParams.channelKeyManager.newFundingKeyPath(isChannelOpener), // we make sure that opener and non-opener key paths end differently - dustLimit = nodeParams.channelConf.dustLimit, - maxHtlcValueInFlightMsat = computeMaxHtlcValueInFlight(nodeParams, fundingAmount, unlimitedMaxHtlcValueInFlight), - initialRequestedChannelReserve_opt = if (dualFunded) None else Some((fundingAmount * nodeParams.channelConf.reserveToFundingRatio).max(nodeParams.channelConf.dustLimit)), // BOLT #2: make sure that our reserve is above our dust limit - htlcMinimum = nodeParams.channelConf.htlcMinimum, - toSelfDelay = nodeParams.channelConf.toRemoteDelay, // we choose their delay - maxAcceptedHtlcs = nodeParams.channelConf.maxAcceptedHtlcs, + nodeParams.channelKeyManager.newFundingKeyPath(isChannelOpener), + initialRequestedChannelReserve_opt = if (dualFunded) None else Some((fundingAmount * nodeParams.channelConf.reserveToFundingRatio).max(nodeParams.channelConf.dustLimit)), // BOLT #2: make sure that our reserve is above our dust limit, isChannelOpener = isChannelOpener, paysCommitTxFees = paysCommitTxFees, upfrontShutdownScript_opt = upfrontShutdownScript_opt, - walletStaticPaymentBasepoint = walletStaticPaymentBasepoint_opt, initFeatures = initFeatures ) } @@ -120,7 +95,7 @@ private class OpenChannelInterceptor(peer: ActorRef[Any], pendingChannelsRateLimiter: ActorRef[PendingChannelsRateLimiter.Command], pluginTimeout: FiniteDuration, nodeParams: NodeParams, - wallet: OnChainPubkeyCache, + wallet: OnChainAddressCache, context: ActorContext[OpenChannelInterceptor.Command]) { import OpenChannelInterceptor._ @@ -139,30 +114,23 @@ private class OpenChannelInterceptor(peer: ActorRef[Any], } else if (request.open.fundingAmount >= Channel.MAX_FUNDING_WITHOUT_WUMBO && !request.remoteFeatures.hasFeature(Wumbo)) { request.replyTo ! OpenChannelResponse.Rejected(s"fundingAmount=${request.open.fundingAmount} is too big, the remote peer doesn't support wumbo") waitForRequest() + } else if (request.open.channelType_opt.isEmpty) { + request.replyTo ! OpenChannelResponse.Rejected("channel_type must be provided") + waitForRequest() } else { - // If a channel type was provided, we directly use it instead of computing it based on local and remote features. - val channelFlags = request.open.channelFlags_opt.getOrElse(nodeParams.channelConf.channelFlags) - val channelType = request.open.channelType_opt.getOrElse(ChannelTypes.defaultFromFeatures(request.localFeatures, request.remoteFeatures, channelFlags.announceChannel)) + val channelType = request.open.channelType_opt.get val dualFunded = Features.canUseFeature(request.localFeatures, request.remoteFeatures, Features.DualFunding) val upfrontShutdownScript = Features.canUseFeature(request.localFeatures, request.remoteFeatures, Features.UpfrontShutdownScript) // If we're purchasing liquidity, we expect our peer to contribute at least the amount we're purchasing, otherwise we'll cancel the funding attempt. val expectedFundingAmount = request.open.fundingAmount + request.open.requestFunding_opt.map(_.requestedAmount).getOrElse(0 sat) - val localParams = createLocalParams(nodeParams, request.localFeatures, upfrontShutdownScript, channelType, isChannelOpener = true, paysCommitTxFees = true, dualFunded = dualFunded, expectedFundingAmount, request.open.disableMaxHtlcValueInFlight) + val localParams = createLocalParams(nodeParams, request.localFeatures, upfrontShutdownScript, isChannelOpener = true, paysCommitTxFees = true, dualFunded = dualFunded, expectedFundingAmount) peer ! Peer.SpawnChannelInitiator(request.replyTo, request.open, ChannelConfig.standard, channelType, localParams) waitForRequest() } } private def sanityCheckNonInitiator(request: OpenChannelNonInitiator): Behavior[Command] = { - validateRemoteChannelType(request.temporaryChannelId, request.channelFlags, request.channelType_opt, request.localFeatures, request.remoteFeatures) match { - case Right(_: ChannelTypes.Standard) => - context.log.warn("rejecting incoming channel: anchor outputs must be used for new channels") - sendFailure("rejecting incoming channel: anchor outputs must be used for new channels", request) - waitForRequest() - case Right(_: ChannelTypes.StaticRemoteKey) if !nodeParams.channelConf.acceptIncomingStaticRemoteKeyChannels => - context.log.warn("rejecting static_remote_key incoming static_remote_key channels") - sendFailure("rejecting incoming static_remote_key channel: anchor outputs must be used for new channels", request) - waitForRequest() + ChannelTypes.areCompatible(request.temporaryChannelId, request.localFeatures, request.channelType_opt) match { case Right(channelType) => val dualFunded = Features.canUseFeature(request.localFeatures, request.remoteFeatures, Features.DualFunding) val upfrontShutdownScript = Features.canUseFeature(request.localFeatures, request.remoteFeatures, Features.UpfrontShutdownScript) @@ -176,12 +144,10 @@ private class OpenChannelInterceptor(peer: ActorRef[Any], nodeParams, request.localFeatures, upfrontShutdownScript, - channelType, isChannelOpener = false, paysCommitTxFees = nonInitiatorPaysCommitTxFees, dualFunded = dualFunded, - fundingAmount = request.fundingAmount, - disableMaxHtlcValueInFlight = false + fundingAmount = request.fundingAmount ) checkRateLimits(request, channelType, localParams) case Left(ex) => @@ -191,7 +157,7 @@ private class OpenChannelInterceptor(peer: ActorRef[Any], } } - private def checkRateLimits(request: OpenChannelNonInitiator, channelType: SupportedChannelType, localParams: LocalParams): Behavior[Command] = { + private def checkRateLimits(request: OpenChannelNonInitiator, channelType: SupportedChannelType, localParams: LocalChannelParams): Behavior[Command] = { val adapter = context.messageAdapter[PendingChannelsRateLimiter.Response](PendingChannelsRateLimiterResponse) pendingChannelsRateLimiter ! AddOrRejectChannel(adapter, request.remoteNodeId, request.temporaryChannelId) receiveCommandMessage[CheckRateLimitsCommands](context, "checkRateLimits") { @@ -208,17 +174,14 @@ private class OpenChannelInterceptor(peer: ActorRef[Any], * If an external plugin was configured, we forward the channel request for further analysis. Otherwise, we accept * the channel and honor the optional liquidity request only for on-the-fly funding where we enforce a single channel. */ - private def checkLiquidityAdsRequest(request: OpenChannelNonInitiator, channelType: SupportedChannelType, localParams: LocalParams): Behavior[Command] = { + private def checkLiquidityAdsRequest(request: OpenChannelNonInitiator, channelType: SupportedChannelType, localParams: LocalChannelParams): Behavior[Command] = { nodeParams.pluginOpenChannelInterceptor match { case Some(plugin) => queryPlugin(plugin, request, localParams, ChannelConfig.standard, channelType) case None => request.open.fold(_ => None, _.requestFunding_opt) match { case Some(requestFunding) if Features.canUseFeature(request.localFeatures, request.remoteFeatures, Features.OnTheFlyFunding) && localParams.paysCommitTxFees => val addFunding = LiquidityAds.AddFunding(requestFunding.requestedAmount, nodeParams.liquidityAdsConfig.rates_opt) - // Now that we know how much we'll contribute to the funding transaction, we update the maxHtlcValueInFlight. - val maxHtlcValueInFlight = localParams.maxHtlcValueInFlightMsat.max(computeMaxHtlcValueInFlight(nodeParams, request.fundingAmount + addFunding.fundingAmount, unlimitedMaxHtlcValueInFlight = false)) - val localParams1 = localParams.copy(maxHtlcValueInFlightMsat = maxHtlcValueInFlight) - val accept = SpawnChannelNonInitiator(request.open, ChannelConfig.standard, channelType, Some(addFunding), localParams1, request.peerConnection.toClassic) + val accept = SpawnChannelNonInitiator(request.open, ChannelConfig.standard, channelType, Some(addFunding), localParams, request.peerConnection.toClassic) checkNoExistingChannel(request, accept) case _ => // We don't honor liquidity ads for new channels: node operators should use plugin for that. @@ -247,16 +210,14 @@ private class OpenChannelInterceptor(peer: ActorRef[Any], } } - private def queryPlugin(plugin: InterceptOpenChannelPlugin, request: OpenChannelInterceptor.OpenChannelNonInitiator, localParams: LocalParams, channelConfig: ChannelConfig, channelType: SupportedChannelType): Behavior[Command] = + private def queryPlugin(plugin: InterceptOpenChannelPlugin, request: OpenChannelInterceptor.OpenChannelNonInitiator, localParams: LocalChannelParams, channelConfig: ChannelConfig, channelType: SupportedChannelType): Behavior[Command] = Behaviors.withTimers { timers => timers.startSingleTimer(PluginTimeout, pluginTimeout) val pluginResponseAdapter = context.messageAdapter[InterceptOpenChannelResponse](PluginOpenChannelResponse) - val defaultParams = DefaultParams(localParams.dustLimit, localParams.maxHtlcValueInFlightMsat, localParams.htlcMinimum, localParams.toSelfDelay, localParams.maxAcceptedHtlcs) - plugin.openChannelInterceptor ! InterceptOpenChannelReceived(pluginResponseAdapter, request, defaultParams) + plugin.openChannelInterceptor ! InterceptOpenChannelReceived(pluginResponseAdapter, request) receiveCommandMessage[QueryPluginCommands](context, "queryPlugin") { case PluginOpenChannelResponse(pluginResponse: AcceptOpenChannel) => - val localParams1 = updateLocalParams(localParams, pluginResponse.defaultParams) - peer ! SpawnChannelNonInitiator(request.open, channelConfig, channelType, pluginResponse.addFunding_opt, localParams1, request.peerConnection.toClassic) + peer ! SpawnChannelNonInitiator(request.open, channelConfig, channelType, pluginResponse.addFunding_opt, localParams, request.peerConnection.toClassic) timers.cancel(PluginTimeout) waitForRequest() case PluginOpenChannelResponse(pluginResponse: RejectOpenChannel) => @@ -285,6 +246,7 @@ private class OpenChannelInterceptor(peer: ActorRef[Any], case _: DATA_NEGOTIATING_SIMPLE => true case _: DATA_CLOSING => true case _: DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT => true + case _: ClosedData => true } } @@ -309,44 +271,19 @@ private class OpenChannelInterceptor(peer: ActorRef[Any], } } - private def validateRemoteChannelType(temporaryChannelId: ByteVector32, channelFlags: ChannelFlags, remoteChannelType_opt: Option[ChannelType], localFeatures: Features[InitFeature], remoteFeatures: Features[InitFeature]): Either[ChannelException, SupportedChannelType] = { - remoteChannelType_opt match { - // remote explicitly specifies a channel type: we check whether we want to allow it - case Some(remoteChannelType) => ChannelTypes.areCompatible(localFeatures, remoteChannelType) match { - case Some(acceptedChannelType) => Right(acceptedChannelType) - case None => Left(InvalidChannelType(temporaryChannelId, ChannelTypes.defaultFromFeatures(localFeatures, remoteFeatures, channelFlags.announceChannel), remoteChannelType)) - } - // Bolt 2: if `option_channel_type` is negotiated: MUST set `channel_type` - case None if Features.canUseFeature(localFeatures, remoteFeatures, Features.ChannelType) => Left(MissingChannelType(temporaryChannelId)) - // remote doesn't specify a channel type: we use spec-defined defaults - case None => Right(ChannelTypes.defaultFromFeatures(localFeatures, remoteFeatures, channelFlags.announceChannel)) - } - } - - private def createLocalParams(nodeParams: NodeParams, initFeatures: Features[InitFeature], upfrontShutdownScript: Boolean, channelType: SupportedChannelType, isChannelOpener: Boolean, paysCommitTxFees: Boolean, dualFunded: Boolean, fundingAmount: Satoshi, disableMaxHtlcValueInFlight: Boolean): LocalParams = { + private def createLocalParams(nodeParams: NodeParams, initFeatures: Features[InitFeature], upfrontShutdownScript: Boolean, isChannelOpener: Boolean, paysCommitTxFees: Boolean, dualFunded: Boolean, fundingAmount: Satoshi): LocalChannelParams = { makeChannelParams( - nodeParams, initFeatures, + nodeParams, + initFeatures, // Note that if our bitcoin node is configured to use taproot, this will generate a taproot script. // If our peer doesn't support option_shutdown_anysegwit, the channel open will fail. // This is fine: "serious" nodes should support option_shutdown_anysegwit, and if we want to use taproot, we // most likely don't want to open channels with nodes that don't support it. if (upfrontShutdownScript) Some(Script.write(wallet.getReceivePublicKeyScript(renew = true))) else None, - if (channelType.paysDirectlyToWallet) Some(wallet.getP2wpkhPubkey(renew = true)) else None, isChannelOpener = isChannelOpener, paysCommitTxFees = paysCommitTxFees, dualFunded = dualFunded, - fundingAmount, - disableMaxHtlcValueInFlight - ) - } - - private def updateLocalParams(localParams: LocalParams, defaultParams: DefaultParams): LocalParams = { - localParams.copy( - dustLimit = defaultParams.dustLimit, - maxHtlcValueInFlightMsat = defaultParams.maxHtlcValueInFlightMsat, - htlcMinimum = defaultParams.htlcMinimum, - toSelfDelay = defaultParams.toSelfDelay, - maxAcceptedHtlcs = defaultParams.maxAcceptedHtlcs + fundingAmount ) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala index 0c3d5b5bff..555a053002 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala @@ -29,10 +29,11 @@ import fr.acinq.eclair.NotificationsLogger.NotifyNodeOperator import fr.acinq.eclair._ import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher import fr.acinq.eclair.blockchain.fee.FeeratePerKw -import fr.acinq.eclair.blockchain.{CurrentBlockHeight, CurrentFeerates, OnChainChannelFunder, OnChainPubkeyCache} +import fr.acinq.eclair.blockchain.{CurrentBlockHeight, CurrentFeerates, OnChainAddressCache, OnChainChannelFunder} import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.crypto.Sphinx +import fr.acinq.eclair.crypto.keymanager.ChannelKeys import fr.acinq.eclair.db.PendingCommandsDb import fr.acinq.eclair.io.MessageRelay.Status import fr.acinq.eclair.io.Monitoring.{Metrics, Tags} @@ -60,7 +61,7 @@ import scodec.bits.ByteVector */ class Peer(val nodeParams: NodeParams, remoteNodeId: PublicKey, - wallet: OnChainPubkeyCache, + wallet: OnChainAddressCache, channelFactory: Peer.ChannelFactory, switchboard: ActorRef, register: ActorRef, @@ -82,7 +83,8 @@ class Peer(val nodeParams: NodeParams, case Event(init: Init, _) => pendingOnTheFlyFunding = init.pendingOnTheFlyFunding val channels = init.storedChannels.map { state => - val channel = spawnChannel() + val channelKeys = nodeParams.channelKeyManager.channelKeys(state.channelParams.channelConfig, state.channelParams.localParams.fundingKeyPath) + val channel = spawnChannel(channelKeys) channel ! INPUT_RESTORED(state) FinalChannelId(state.channelId) -> channel }.toMap @@ -206,19 +208,37 @@ class Peer(val nodeParams: NodeParams, stay() case Event(SpawnChannelInitiator(replyTo, c, channelConfig, channelType, localParams), d: ConnectedData) => - val channel = spawnChannel() + val channelKeys = nodeParams.channelKeyManager.channelKeys(channelConfig, localParams.fundingKeyPath) + val channel = spawnChannel(channelKeys) context.system.scheduler.scheduleOnce(c.timeout_opt.map(_.duration).getOrElse(nodeParams.channelConf.channelFundingTimeout), channel, Channel.TickChannelOpenTimeout)(context.dispatcher) val dualFunded = Features.canUseFeature(d.localFeatures, d.remoteFeatures, Features.DualFunding) val requireConfirmedInputs = c.requireConfirmedInputsOverride_opt.getOrElse(nodeParams.channelConf.requireConfirmedInputsForDualFunding) val temporaryChannelId = if (dualFunded) { - Helpers.dualFundedTemporaryChannelId(nodeParams, localParams, channelConfig) + Helpers.dualFundedTemporaryChannelId(channelKeys) } else { randomBytes32() } - val fundingTxFeerate = c.fundingTxFeerate_opt.getOrElse(nodeParams.onChainFeeConf.getFundingFeerate(nodeParams.currentFeeratesForFundingClosing)) - val commitTxFeerate = nodeParams.onChainFeeConf.getCommitmentFeerate(nodeParams.currentBitcoinCoreFeerates, remoteNodeId, channelType.commitmentFormat, c.fundingAmount) - log.info(s"requesting a new channel with type=$channelType fundingAmount=${c.fundingAmount} dualFunded=$dualFunded pushAmount=${c.pushAmount_opt} fundingFeerate=$fundingTxFeerate temporaryChannelId=$temporaryChannelId localParams=$localParams") - channel ! INPUT_INIT_CHANNEL_INITIATOR(temporaryChannelId, c.fundingAmount, dualFunded, commitTxFeerate, fundingTxFeerate, c.fundingTxFeeBudget_opt, c.pushAmount_opt, requireConfirmedInputs, c.requestFunding_opt, localParams, d.peerConnection, d.remoteInit, c.channelFlags_opt.getOrElse(nodeParams.channelConf.channelFlags), channelConfig, channelType, replyTo) + val init = INPUT_INIT_CHANNEL_INITIATOR( + temporaryChannelId = temporaryChannelId, + fundingAmount = c.fundingAmount, + dualFunded = dualFunded, + commitTxFeerate = nodeParams.onChainFeeConf.getCommitmentFeerate(nodeParams.currentBitcoinCoreFeerates, remoteNodeId, channelType.commitmentFormat), + fundingTxFeerate = c.fundingTxFeerate_opt.getOrElse(nodeParams.onChainFeeConf.getFundingFeerate(nodeParams.currentFeeratesForFundingClosing)), + fundingTxFeeBudget_opt = c.fundingTxFeeBudget_opt, + pushAmount_opt = c.pushAmount_opt, + requireConfirmedInputs = requireConfirmedInputs, + requestFunding_opt = c.requestFunding_opt, + localChannelParams = localParams, + proposedCommitParams = nodeParams.channelConf.commitParams(c.fundingAmount, unlimitedMaxHtlcValueInFlight = false), + remote = d.peerConnection, + remoteInit = d.remoteInit, + channelFlags = c.channelFlags_opt.getOrElse(nodeParams.channelConf.channelFlags), + channelConfig = channelConfig, + channelType = channelType, + replyTo = replyTo + ) + log.info(s"requesting a new channel with type=$channelType fundingAmount=${c.fundingAmount} dualFunded=$dualFunded pushAmount=${c.pushAmount_opt} fundingFeerate=${init.fundingTxFeerate} temporaryChannelId=$temporaryChannelId") + channel ! init stay() using d.copy(channels = d.channels + (TemporaryChannelId(temporaryChannelId) -> channel)) case Event(open: protocol.OpenChannel, d: ConnectedData) => @@ -260,16 +280,40 @@ class Peer(val nodeParams: NodeParams, context.system.eventStream.publish(ChannelAborted(ActorRef.noSender, remoteNodeId, temporaryChannelId)) stay() case accept: OnTheFlyFunding.ValidationResult.Accept => - val channel = spawnChannel() + val channelKeys = nodeParams.channelKeyManager.channelKeys(channelConfig, localParams.fundingKeyPath) + val channel = spawnChannel(channelKeys) context.system.scheduler.scheduleOnce(nodeParams.channelConf.channelFundingTimeout, channel, Channel.TickChannelOpenTimeout)(context.dispatcher) log.info(s"accepting a new channel with type=$channelType temporaryChannelId=$temporaryChannelId localParams=$localParams") open match { case Left(open) => - channel ! INPUT_INIT_CHANNEL_NON_INITIATOR(open.temporaryChannelId, None, dualFunded = false, None, requireConfirmedInputs = false, localParams, d.peerConnection, d.remoteInit, channelConfig, channelType) + val init = INPUT_INIT_CHANNEL_NON_INITIATOR( + temporaryChannelId = open.temporaryChannelId, + fundingContribution_opt = None, + dualFunded = false, + pushAmount_opt = None, + requireConfirmedInputs = false, + localChannelParams = localParams, + proposedCommitParams = nodeParams.channelConf.commitParams(open.fundingSatoshis, unlimitedMaxHtlcValueInFlight = false), + remote = d.peerConnection, + remoteInit = d.remoteInit, + channelConfig = channelConfig, + channelType = channelType) + channel ! init channel ! open case Right(open) => - val requireConfirmedInputs = nodeParams.channelConf.requireConfirmedInputsForDualFunding - channel ! INPUT_INIT_CHANNEL_NON_INITIATOR(open.temporaryChannelId, addFunding_opt, dualFunded = true, None, requireConfirmedInputs, localParams, d.peerConnection, d.remoteInit, channelConfig, channelType) + val init = INPUT_INIT_CHANNEL_NON_INITIATOR( + temporaryChannelId = open.temporaryChannelId, + fundingContribution_opt = addFunding_opt, + dualFunded = true, + pushAmount_opt = None, + requireConfirmedInputs = nodeParams.channelConf.requireConfirmedInputsForDualFunding, + localChannelParams = localParams, + proposedCommitParams = nodeParams.channelConf.commitParams(open.fundingAmount + addFunding_opt.map(_.fundingAmount).getOrElse(0 sat), unlimitedMaxHtlcValueInFlight = false), + remote = d.peerConnection, + remoteInit = d.remoteInit, + channelConfig = channelConfig, + channelType = channelType) + channel ! init accept.useFeeCredit_opt match { case Some(useFeeCredit) => channel ! open.copy(tlvStream = TlvStream(open.tlvStream.records + ChannelTlv.UseFeeCredit(useFeeCredit))) case None => channel ! open @@ -335,7 +379,7 @@ class Peer(val nodeParams: NodeParams, pending.proposed.find(_.htlc.id == msg.id) match { case Some(htlc) => val failure = msg match { - case msg: WillFailHtlc => FailureReason.EncryptedDownstreamFailure(msg.reason) + case msg: WillFailHtlc => FailureReason.EncryptedDownstreamFailure(msg.reason, msg.attribution_opt) case msg: WillFailMalformedHtlc => FailureReason.LocalFailure(createBadOnionFailure(msg.onionHash, msg.failureCode)) } htlc.createFailureCommands(Some(failure))(log).foreach { case (channelId, cmd) => PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, channelId, cmd) } @@ -423,10 +467,11 @@ class Peer(val nodeParams: NodeParams, case Event(msg: SpliceInit, d: ConnectedData) => d.channels.get(FinalChannelId(msg.channelId)) match { case Some(_) if msg.usesOnTheFlyFunding && !d.fundingFeerateOk(msg.feerate) => - log.info("rejecting open_channel2: feerate too low ({} < {})", msg.feerate, d.currentFeerates.fundingFeerate) + log.info("rejecting splice_init: feerate too low ({} < {})", msg.feerate, d.currentFeerates.fundingFeerate) self ! Peer.OutgoingMessage(TxAbort(msg.channelId, FundingFeerateTooLow(msg.channelId, msg.feerate, d.currentFeerates.fundingFeerate).getMessage), d.peerConnection) case Some(channel) => - OnTheFlyFunding.validateSplice(nodeParams.onTheFlyFundingConfig, msg, nodeParams.channelConf.htlcMinimum, pendingOnTheFlyFunding, feeCredit.getOrElse(0 msat)) match { + // We don't have access to the remote htlc_minimum here, so we hard-code the value Phoenix uses (1000 msat). + OnTheFlyFunding.validateSplice(nodeParams.onTheFlyFundingConfig, msg, nodeParams.channelConf.htlcMinimum.max(1000 msat), pendingOnTheFlyFunding, feeCredit.getOrElse(0 msat)) match { case reject: OnTheFlyFunding.ValidationResult.Reject => log.warning("rejecting on-the-fly splice: {}", reject.cancel.toAscii) self ! Peer.OutgoingMessage(reject.cancel, d.peerConnection) @@ -872,8 +917,8 @@ class Peer(val nodeParams: NodeParams, s(e) } - private def spawnChannel(): ActorRef = { - val channel = channelFactory.spawn(context, remoteNodeId) + private def spawnChannel(channelKeys: ChannelKeys): ActorRef = { + val channel = channelFactory.spawn(context, remoteNodeId, channelKeys) context.watchWith(channel, ChannelTerminated(channel.ref)) channel } @@ -976,15 +1021,15 @@ object Peer { val CHANNELID_ZERO: ByteVector32 = ByteVector32.Zeroes trait ChannelFactory { - def spawn(context: ActorContext, remoteNodeId: PublicKey): ActorRef + def spawn(context: ActorContext, remoteNodeId: PublicKey, channelKeys: ChannelKeys): ActorRef } - case class SimpleChannelFactory(nodeParams: NodeParams, watcher: typed.ActorRef[ZmqWatcher.Command], relayer: ActorRef, wallet: OnChainChannelFunder with OnChainPubkeyCache, txPublisherFactory: Channel.TxPublisherFactory) extends ChannelFactory { - override def spawn(context: ActorContext, remoteNodeId: PublicKey): ActorRef = - context.actorOf(Channel.props(nodeParams, wallet, remoteNodeId, watcher, relayer, txPublisherFactory)) + case class SimpleChannelFactory(nodeParams: NodeParams, watcher: typed.ActorRef[ZmqWatcher.Command], relayer: ActorRef, wallet: OnChainChannelFunder with OnChainAddressCache, txPublisherFactory: Channel.TxPublisherFactory) extends ChannelFactory { + override def spawn(context: ActorContext, remoteNodeId: PublicKey, channelKeys: ChannelKeys): ActorRef = + context.actorOf(Channel.props(nodeParams, channelKeys, wallet, remoteNodeId, watcher, relayer, txPublisherFactory)) } - def props(nodeParams: NodeParams, remoteNodeId: PublicKey, wallet: OnChainPubkeyCache, channelFactory: ChannelFactory, switchboard: ActorRef, register: ActorRef, router: typed.ActorRef[Router.GetNodeId], pendingChannelsRateLimiter: typed.ActorRef[PendingChannelsRateLimiter.Command]): Props = + def props(nodeParams: NodeParams, remoteNodeId: PublicKey, wallet: OnChainAddressCache, channelFactory: ChannelFactory, switchboard: ActorRef, register: ActorRef, router: typed.ActorRef[Router.GetNodeId], pendingChannelsRateLimiter: typed.ActorRef[PendingChannelsRateLimiter.Command]): Props = Props(new Peer(nodeParams, remoteNodeId, wallet, channelFactory, switchboard, register, router, pendingChannelsRateLimiter)) // @formatter:off @@ -1006,7 +1051,7 @@ object Peer { def peerStorage: PeerStorage } case object Nothing extends Data { - override def channels = Map.empty + override def channels: Map[_ <: ChannelId, ActorRef] = Map.empty override def activeChannels: Set[ByteVector32] = Set.empty override def peerStorage: PeerStorage = PeerStorage(None, written = true) } @@ -1067,15 +1112,15 @@ object Peer { * the channel has been opened. */ case class Created(channelId: ByteVector32, fundingTxId: TxId, fee: Satoshi) extends OpenChannelResponse { override def toString = s"created channel $channelId with fundingTxId=$fundingTxId and fees=$fee" } - case class Rejected(reason: String) extends OpenChannelResponse { override def toString = reason } + case class Rejected(reason: String) extends OpenChannelResponse { override def toString: String = reason } case object Cancelled extends OpenChannelResponse { override def toString = "channel creation cancelled" } case object Disconnected extends OpenChannelResponse { override def toString = "disconnected" } case object TimedOut extends OpenChannelResponse { override def toString = "open channel cancelled, took too long" } case class RemoteError(ascii: String) extends OpenChannelResponse { override def toString = s"peer aborted the channel funding flow: '$ascii'" } } - case class SpawnChannelInitiator(replyTo: akka.actor.typed.ActorRef[OpenChannelResponse], cmd: Peer.OpenChannel, channelConfig: ChannelConfig, channelType: SupportedChannelType, localParams: LocalParams) - case class SpawnChannelNonInitiator(open: Either[protocol.OpenChannel, protocol.OpenDualFundedChannel], channelConfig: ChannelConfig, channelType: SupportedChannelType, addFunding_opt: Option[LiquidityAds.AddFunding], localParams: LocalParams, peerConnection: ActorRef) + case class SpawnChannelInitiator(replyTo: akka.actor.typed.ActorRef[OpenChannelResponse], cmd: Peer.OpenChannel, channelConfig: ChannelConfig, channelType: SupportedChannelType, localParams: LocalChannelParams) + case class SpawnChannelNonInitiator(open: Either[protocol.OpenChannel, protocol.OpenDualFundedChannel], channelConfig: ChannelConfig, channelType: SupportedChannelType, addFunding_opt: Option[LiquidityAds.AddFunding], localParams: LocalChannelParams, peerConnection: ActorRef) /** If [[Features.OnTheFlyFunding]] is supported and we're connected, relay a funding proposal to our peer. */ case class ProposeOnTheFlyFunding(replyTo: typed.ActorRef[ProposeOnTheFlyFundingResponse], amount: MilliSatoshi, paymentHash: ByteVector32, expiry: CltvExpiry, onion: OnionRoutingPacket, onionSharedSecrets: Seq[Sphinx.SharedSecret], nextPathKey_opt: Option[PublicKey], upstream: Upstream.Hot) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/PeerConnection.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/PeerConnection.scala index ffcf92cac7..8f2fe0254c 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/PeerConnection.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/PeerConnection.scala @@ -18,8 +18,8 @@ package fr.acinq.eclair.io import akka.actor.{ActorRef, FSM, OneForOneStrategy, PoisonPill, Props, Stash, SupervisorStrategy, Terminated} import akka.event.Logging.MDC -import fr.acinq.bitcoin.scalacompat.{BlockHash, ByteVector32} import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.{BlockHash, ByteVector32} import fr.acinq.eclair.Logs.LogCategory import fr.acinq.eclair.crypto.Noise.KeyPair import fr.acinq.eclair.crypto.TransportHandler @@ -28,7 +28,7 @@ import fr.acinq.eclair.remote.EclairInternalsSerializer.RemoteTypes import fr.acinq.eclair.router.Router._ import fr.acinq.eclair.wire.protocol import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{FSMDiagnosticActorLogging, FeatureCompatibilityResult, Features, InitFeature, Logs, TimestampMilli, TimestampSecond} +import fr.acinq.eclair.{FSMDiagnosticActorLogging, Features, InitFeature, Logs, TimestampMilli, TimestampSecond} import scodec.Attempt import scodec.bits.ByteVector @@ -206,11 +206,13 @@ class PeerConnection(keyPair: KeyPair, conf: PeerConnection.Conf, switchboard: A stay() case Event(msg: LightningMessage, d: ConnectedData) if sender() != d.transport => // if the message doesn't originate from the transport, it is an outgoing message - d.transport forward msg + msg match { + case batch: CommitSigBatch => batch.messages.foreach(msg => d.transport forward msg) + case msg => d.transport forward msg + } msg match { // If we send any channel management message to this peer, the connection should be persistent. - case _: ChannelMessage if !d.isPersistent => - stay() using d.copy(isPersistent = true) + case _: ChannelMessage if !d.isPersistent => stay() using d.copy(isPersistent = true) case _ => stay() } @@ -341,10 +343,48 @@ class PeerConnection(keyPair: KeyPair, conf: PeerConnection.Conf, switchboard: A stay() case Event(msg: LightningMessage, d: ConnectedData) => - // we acknowledge and pass all other messages to the peer + // We immediately acknowledge all other messages. d.transport ! TransportHandler.ReadAck(msg) - d.peer ! msg - stay() + // We immediately forward messages to the peer, unless they are part of a batch, in which case we wait to + // receive the whole batch before forwarding. + msg match { + case msg: CommitSig => + msg.tlvStream.get[CommitSigTlv.BatchTlv].map(_.size) match { + case Some(batchSize) if batchSize > 25 => + log.warning("received legacy batch of commit_sig exceeding our threshold ({} > 25), processing messages individually", batchSize) + // We don't want peers to be able to exhaust our memory by sending batches of dummy messages that we keep in RAM. + d.peer ! msg + stay() + case Some(batchSize) if batchSize > 1 => + d.legacyCommitSigBatch_opt match { + case Some(pending) if pending.channelId != msg.channelId || pending.batchSize != batchSize => + log.warning("received invalid commit_sig batch while a different batch isn't complete") + // This should never happen, otherwise it will likely lead to a force-close. + d.peer ! CommitSigBatch(pending.received) + stay() using d.copy(legacyCommitSigBatch_opt = Some(PendingCommitSigBatch(msg.channelId, batchSize, Seq(msg)))) + case Some(pending) => + val received1 = pending.received :+ msg + if (received1.size == batchSize) { + log.debug("received last commit_sig in legacy batch for channel_id={}", msg.channelId) + d.peer ! CommitSigBatch(received1) + stay() using d.copy(legacyCommitSigBatch_opt = None) + } else { + log.debug("received commit_sig {}/{} in legacy batch for channel_id={}", received1.size, batchSize, msg.channelId) + stay() using d.copy(legacyCommitSigBatch_opt = Some(pending.copy(received = received1))) + } + case None => + log.debug("received first commit_sig in legacy batch of size {} for channel_id={}", batchSize, msg.channelId) + stay() using d.copy(legacyCommitSigBatch_opt = Some(PendingCommitSigBatch(msg.channelId, batchSize, Seq(msg)))) + } + case _ => + log.debug("received individual commit_sig for channel_id={}", msg.channelId) + d.peer ! msg + stay() + } + case _ => + d.peer ! msg + stay() + } case Event(readAck: TransportHandler.ReadAck, d: ConnectedData) => // we just forward acks to the transport (e.g. from the router) @@ -564,8 +604,19 @@ object PeerConnection { case class AuthenticatingData(pendingAuth: PendingAuth, transport: ActorRef, isPersistent: Boolean) extends Data with HasTransport case class BeforeInitData(remoteNodeId: PublicKey, pendingAuth: PendingAuth, transport: ActorRef, isPersistent: Boolean) extends Data with HasTransport case class InitializingData(chainHash: BlockHash, pendingAuth: PendingAuth, remoteNodeId: PublicKey, transport: ActorRef, peer: ActorRef, localInit: protocol.Init, doSync: Boolean, isPersistent: Boolean) extends Data with HasTransport - case class ConnectedData(chainHash: BlockHash, remoteNodeId: PublicKey, transport: ActorRef, peer: ActorRef, localInit: protocol.Init, remoteInit: protocol.Init, rebroadcastDelay: FiniteDuration, gossipTimestampFilter: Option[GossipTimestampFilter] = None, behavior: Behavior = Behavior(), expectedPong_opt: Option[ExpectedPong] = None, isPersistent: Boolean) extends Data with HasTransport - + case class ConnectedData(chainHash: BlockHash, + remoteNodeId: PublicKey, + transport: ActorRef, + peer: ActorRef, + localInit: protocol.Init, remoteInit: protocol.Init, + rebroadcastDelay: FiniteDuration, + gossipTimestampFilter: Option[GossipTimestampFilter] = None, + behavior: Behavior = Behavior(), + expectedPong_opt: Option[ExpectedPong] = None, + legacyCommitSigBatch_opt: Option[PendingCommitSigBatch] = None, + isPersistent: Boolean) extends Data with HasTransport + + case class PendingCommitSigBatch(channelId: ByteVector32, batchSize: Int, received: Seq[CommitSig]) case class ExpectedPong(ping: Ping, timestamp: TimestampMilli = TimestampMilli.now()) case class PingTimeout(ping: Ping) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/PendingChannelsRateLimiter.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/PendingChannelsRateLimiter.scala index a480cec083..44ccb6f366 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/PendingChannelsRateLimiter.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/PendingChannelsRateLimiter.scala @@ -54,11 +54,11 @@ object PendingChannelsRateLimiter { private[io] def filterPendingChannels(nodeParams: NodeParams, channels: Seq[PersistentChannelData]): Map[PublicKey, Seq[PersistentChannelData]] = { channels.filter { case p: PersistentChannelData if nodeParams.channelConf.channelOpenerWhitelist.contains(p.remoteNodeId) => false - case d: DATA_WAIT_FOR_FUNDING_CONFIRMED if !d.commitments.params.localParams.isChannelOpener => true + case d: DATA_WAIT_FOR_FUNDING_CONFIRMED if !d.commitments.localChannelParams.isChannelOpener => true case d: DATA_WAIT_FOR_DUAL_FUNDING_SIGNED if !d.channelParams.localParams.isChannelOpener => true - case d: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED if !d.commitments.params.localParams.isChannelOpener => true - case d: DATA_WAIT_FOR_CHANNEL_READY if !d.commitments.params.localParams.isChannelOpener => true - case d: DATA_WAIT_FOR_DUAL_FUNDING_READY if !d.commitments.params.localParams.isChannelOpener => true + case d: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED if !d.commitments.localChannelParams.isChannelOpener => true + case d: DATA_WAIT_FOR_CHANNEL_READY if !d.commitments.localChannelParams.isChannelOpener => true + case d: DATA_WAIT_FOR_DUAL_FUNDING_READY if !d.commitments.localChannelParams.isChannelOpener => true case _ => false }.groupBy(_.remoteNodeId) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/ReconnectionTask.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/ReconnectionTask.scala index 9d0b4ab662..c74177b61c 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/ReconnectionTask.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/ReconnectionTask.scala @@ -187,27 +187,32 @@ object ReconnectionTask { // @formatter:on def selectNodeAddress(nodeParams: NodeParams, nodeAddresses: Seq[NodeAddress]): Option[NodeAddress] = { - // it doesn't make sense to mix tor and clearnet addresses, so we separate them and decide whether we use one or the other + val selectedAddresses = selectNodeAddresses(nodeParams, nodeAddresses) + // we pick an address at random + if (selectedAddresses.nonEmpty) { + Some(selectedAddresses(Random.nextInt(selectedAddresses.size))) + } else { + None + } + } + + private[io] def selectNodeAddresses(nodeParams: NodeParams, nodeAddresses: Seq[NodeAddress]): Seq[NodeAddress] = { val torAddresses = nodeAddresses.collect { case o: OnionAddress => o } val clearnetAddresses = nodeAddresses diff torAddresses val selectedAddresses = nodeParams.socksProxy_opt match { - case Some(params) if clearnetAddresses.nonEmpty && params.useForTor && (!params.useForIPv4 || !params.useForIPv6) => - // Remote has clearnet (and possibly tor addresses), and we support tor, but we have configured it to only use - // tor when strictly necessary. In this case we will only connect over clearnet. + case Some(params) if clearnetAddresses.nonEmpty && (!params.useForIPv4 || !params.useForIPv6) => + // Remote has clearnet and possibly tor addresses, but we have configured our proxy to only be used + // for tor. In this case we will only connect over clearnet. clearnetAddresses - case Some(params) if torAddresses.nonEmpty && params.useForTor => - // In all other cases, if they have a tor address and we support tor, we use tor. - torAddresses + case Some(params) if params.useForTor => + // The SOCKS5 proxy is enabled, and specifically configured to handle tor addresses. + // This is the only case when we can connect to both tor and clearnet. + nodeAddresses case _ => - // Otherwise, if we don't support tor or they don't have a tor address, we use clearnet. + // Otherwise, if we don't support tor or remote doesn't have a tor address, we use clearnet. clearnetAddresses } - // finally, we pick an address at random - if (selectedAddresses.nonEmpty) { - Some(selectedAddresses(Random.nextInt(selectedAddresses.size))) - } else { - None - } + selectedAddresses } def getPeerAddressFromDb(nodeParams: NodeParams, remoteNodeId: PublicKey): Option[NodeAddress] = { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/Switchboard.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/Switchboard.scala index cfede0ffa5..c455056c66 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/Switchboard.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/Switchboard.scala @@ -20,9 +20,9 @@ import akka.actor.typed.receptionist.{Receptionist, ServiceKey} import akka.actor.typed.scaladsl.Behaviors import akka.actor.typed.scaladsl.adapter.{ClassicActorContextOps, ClassicActorRefOps, ClassicActorSystemOps, TypedActorRefOps} import akka.actor.{Actor, ActorContext, ActorLogging, ActorRef, OneForOneStrategy, Props, Stash, Status, SupervisorStrategy, typed} -import fr.acinq.bitcoin.scalacompat.{ByteVector32, Satoshi} +import fr.acinq.bitcoin.scalacompat.ByteVector32 import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey -import fr.acinq.eclair.blockchain.OnChainPubkeyCache +import fr.acinq.eclair.blockchain.OnChainAddressCache import fr.acinq.eclair.channel.Helpers.Closing import fr.acinq.eclair.channel._ import fr.acinq.eclair.io.IncomingConnectionsTracker.TrackIncomingConnection @@ -56,8 +56,12 @@ class Switchboard(nodeParams: NodeParams, peerFactory: Switchboard.PeerFactory) // Closed channels will be removed, other channels will be restored. val (channels, closedChannels) = init.channels.partition(c => Closing.isClosed(c, None).isEmpty) closedChannels.foreach(c => { - log.info(s"closing channel ${c.channelId}") - nodeParams.db.channels.removeChannel(c.channelId) + log.info("channel {} was closed before restarting, updating the DB", c.channelId) + val closingData_opt = (c, Closing.isClosed(c, None)) match { + case (c: DATA_CLOSING, Some(closingType)) => Some(DATA_CLOSED(c, closingType)) + case _ => None + } + nodeParams.db.channels.removeChannel(c.channelId, closingData_opt) }) val peersWithChannels = channels.groupBy(_.remoteNodeId) val peersWithOnTheFlyFunding = nodeParams.db.liquidity.listPendingOnTheFlyFunding() @@ -176,7 +180,7 @@ object Switchboard { def spawn(context: ActorContext, remoteNodeId: PublicKey): ActorRef } - case class SimplePeerFactory(nodeParams: NodeParams, wallet: OnChainPubkeyCache, channelFactory: Peer.ChannelFactory, pendingChannelsRateLimiter: typed.ActorRef[PendingChannelsRateLimiter.Command], register: ActorRef, router: typed.ActorRef[Router.GetNodeId]) extends PeerFactory { + case class SimplePeerFactory(nodeParams: NodeParams, wallet: OnChainAddressCache, channelFactory: Peer.ChannelFactory, pendingChannelsRateLimiter: typed.ActorRef[PendingChannelsRateLimiter.Command], register: ActorRef, router: typed.ActorRef[Router.GetNodeId]) extends PeerFactory { override def spawn(context: ActorContext, remoteNodeId: PublicKey): ActorRef = context.actorOf(Peer.props(nodeParams, remoteNodeId, wallet, channelFactory, context.self, register, router, pendingChannelsRateLimiter), name = peerActorName(remoteNodeId)) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala index 9cee99fabf..f2e6871fd0 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala @@ -19,9 +19,10 @@ package fr.acinq.eclair.json import com.google.common.net.HostAndPort import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin.scalacompat.DeterministicWallet.KeyPath +import fr.acinq.bitcoin.scalacompat.Musig2.IndividualNonce import fr.acinq.bitcoin.scalacompat.{BlockHash, BlockId, Btc, ByteVector32, ByteVector64, OutPoint, Satoshi, Transaction, TxId} import fr.acinq.eclair.balance.CheckBalance.{DetailedOnChainBalance, GlobalBalance, OffChainBalance} -import fr.acinq.eclair.blockchain.fee.{ConfirmationTarget, FeeratePerKw} +import fr.acinq.eclair.blockchain.fee.FeeratePerKw import fr.acinq.eclair.channel._ import fr.acinq.eclair.crypto.{ShaChain, Sphinx} import fr.acinq.eclair.db.FailureType.FailureType @@ -34,8 +35,9 @@ import fr.acinq.eclair.payment._ import fr.acinq.eclair.router.Router._ import fr.acinq.eclair.transactions.DirectedHtlc import fr.acinq.eclair.transactions.Transactions._ +import fr.acinq.eclair.wire.protocol.OfferTypes.Offer import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{Alias, BlockHeight, CltvExpiry, CltvExpiryDelta, EncodedNodeId, Feature, FeatureSupport, MilliSatoshi, RealShortChannelId, ShortChannelId, TimestampMilli, TimestampSecond, UInt64, UnknownFeature} +import fr.acinq.eclair.{Alias, BlockHeight, CltvExpiry, CltvExpiryDelta, EncodedNodeId, Feature, FeatureSupport, Features, MilliSatoshi, RealShortChannelId, ShortChannelId, TimestampMilli, TimestampSecond, UInt64, UnknownFeature} import org.json4s import org.json4s.JsonAST._ import org.json4s.jackson.Serialization @@ -150,6 +152,10 @@ object ByteVector64Serializer extends MinimalSerializer({ case x: ByteVector64 => JString(x.toHex) }) +object IndividualNonceSerializer extends MinimalSerializer({ + case x: IndividualNonce => JString(x.data.toHex) +}) + object UInt64Serializer extends MinimalSerializer({ case x: UInt64 => JInt(x.toBigInt) }) @@ -255,56 +261,40 @@ object KeyPathSerializer extends MinimalSerializer({ }) object TransactionWithInputInfoSerializer extends MinimalSerializer({ - case x: HtlcSuccessTx => JObject(List( + case x: HtlcTx => JObject(List( JField("txid", JString(x.tx.txid.value.toHex)), JField("tx", JString(x.tx.toString())), JField("paymentHash", JString(x.paymentHash.toString())), JField("htlcId", JLong(x.htlcId)), - JField("confirmBeforeBlock", JLong(x.confirmationTarget.confirmBefore.toLong)) - )) - case x: HtlcTimeoutTx => JObject(List( - JField("txid", JString(x.tx.txid.value.toHex)), - JField("tx", JString(x.tx.toString())), - JField("htlcId", JLong(x.htlcId)), - JField("confirmBeforeBlock", JLong(x.confirmationTarget.confirmBefore.toLong)) + JField("htlcExpiry", JLong(x.htlcExpiry.toLong)) )) - case x: ClaimHtlcSuccessTx => JObject(List( + case x: ClaimHtlcTx => JObject(List( JField("txid", JString(x.tx.txid.value.toHex)), JField("tx", JString(x.tx.toString())), JField("paymentHash", JString(x.paymentHash.toString())), JField("htlcId", JLong(x.htlcId)), - JField("confirmBeforeBlock", JLong(x.confirmationTarget.confirmBefore.toLong)) + JField("htlcExpiry", JLong(x.htlcExpiry.toLong)) )) - case x: ClaimHtlcTx => JObject(List( + case x: HtlcPenaltyTx => JObject(List( JField("txid", JString(x.tx.txid.value.toHex)), JField("tx", JString(x.tx.toString())), - JField("htlcId", JLong(x.htlcId)), - JField("confirmBeforeBlock", JLong(x.confirmationTarget.confirmBefore.toLong)) + JField("paymentHash", JString(x.paymentHash.toString())), + JField("htlcExpiry", JLong(x.htlcExpiry.toLong)) )) case x: ClosingTx => val txFields = List( JField("txid", JString(x.tx.txid.value.toHex)), JField("tx", JString(x.tx.toString())) ) - x.toLocalOutput match { + x.toLocalOutput_opt match { case Some(toLocal) => val toLocalField = JField("toLocalOutput", JObject(List( - JField("index", JLong(toLocal.index)), JField("amount", JLong(toLocal.amount.toLong)), JField("publicKeyScript", JString(toLocal.publicKeyScript.toHex)) ))) JObject(txFields :+ toLocalField) case None => JObject(txFields) } - case x: ReplaceableTransactionWithInputInfo => JObject(List( - JField("txid", JString(x.tx.txid.value.toHex)), - JField("tx", JString(x.tx.toString())), - x.confirmationTarget match { - case ConfirmationTarget.Absolute(confirmBefore) => JField("confirmBeforeBlock", JLong(confirmBefore.toLong)) - case ConfirmationTarget.Priority(priority) => JField("confirmPriority", JString(priority.toString)) - } - - )) case x: TransactionWithInputInfo => JObject(List( JField("txid", JString(x.tx.txid.value.toHex)), JField("tx", JString(x.tx.toString())) @@ -332,17 +322,6 @@ object ColorSerializer extends MinimalSerializer({ case c: Color => JString(c.toString) }) -// @formatter:off -private case class CommitTxAndRemoteSigJson(commitTx: CommitTx, remoteSig: ByteVector64) -private case class CommitTxAndRemotePartialSigJson(commitTx: CommitTx, remoteSig: RemoteSignature.PartialSignatureWithNonce) -object CommitTxAndRemoteSigSerializer extends ConvertClassSerializer[CommitTxAndRemoteSig]( - i => i.remoteSig match { - case f: RemoteSignature.FullSignature => CommitTxAndRemoteSigJson(i.commitTx, f.sig) - case p: RemoteSignature.PartialSignatureWithNonce => CommitTxAndRemotePartialSigJson(i.commitTx, p) - } -) -// @formatter:on - // @formatter:off private sealed trait HopJson private case class ChannelHopJson(nodeId: PublicKey, nextNodeId: PublicKey, source: HopRelayParams) extends HopJson @@ -492,6 +471,52 @@ object InvoiceSerializer extends MinimalSerializer({ JObject(fieldList) }) +private case class BlindedRouteJson(firstNodeId: EncodedNodeId, length: Int) +private case class OfferJson(chains: Option[Seq[String]], + amount: Option[String], + currency: Option[String], + description: Option[String], + expiry: Option[TimestampSecond], + issuer: Option[String], + nodeId: Option[PublicKey], + paths: Option[Seq[BlindedRouteJson]], + quantityMax: Option[Long], + features: Option[Features[Feature]], + metadata: Option[String], + unknownTlvs: Option[Map[String, String]]) +object OfferSerializer extends ConvertClassSerializer[Offer](o => { + val fractionDigits = o.records.get[OfferTypes.OfferCurrency].map(_.currency.getDefaultFractionDigits()).getOrElse(3) + OfferJson( + chains = o.records.get[OfferTypes.OfferChains].map(_.chains.map(_.toString())), + amount = o.records.get[OfferTypes.OfferAmount].map(a => + if (fractionDigits == 0) { + a.amount.toString + } else { + val one = scala.math.pow(10, fractionDigits).toInt + s"${a.amount / one}.%0${fractionDigits}d".format(a.amount % one) + } + ), + currency = if (o.records.get[OfferTypes.OfferAmount].isEmpty) { + None + } else { + Some(o.records.get[OfferTypes.OfferCurrency].map(_.currency.getCurrencyCode()).getOrElse("satoshi")) + }, + description = o.records.get[OfferTypes.OfferDescription].map(_.description), + expiry = o.records.get[OfferTypes.OfferAbsoluteExpiry].map(_.absoluteExpiry), + issuer = o.records.get[OfferTypes.OfferIssuer].map(_.issuer), + nodeId = o.records.get[OfferTypes.OfferNodeId].map(_.publicKey), + paths = o.records.get[OfferTypes.OfferPaths].map(_.paths.map(p => BlindedRouteJson(p.firstNodeId, p.blindedHops.length))), + quantityMax = o.records.get[OfferTypes.OfferQuantityMax].map(_.max), + features = o.records.get[OfferTypes.OfferFeatures].map(f => Features(f.features)), + metadata = o.records.get[OfferTypes.OfferMetadata].map(_.data.toHex), + unknownTlvs = if (o.records.unknown.isEmpty) { + None + } else { + Some(o.records.unknown.map(tlv => tlv.tag.toString -> tlv.value.toHex).toMap) + } + ) +}) + private case class OfferDataJson(amountMsat: Option[MilliSatoshi], description: Option[String], issuer: Option[String], @@ -565,7 +590,7 @@ object OriginSerializer extends MinimalSerializer({ JField("expiry", JLong(htlc.add.cltvExpiry.toLong)), JField("receivedAt", JLong(htlc.receivedAt.toLong)), ) - }.toList) + }) case o: Upstream.Cold.Channel => JObject( JField("channelId", JString(o.originChannelId.toHex)), JField("htlcId", JLong(o.originHtlcId)), @@ -577,18 +602,18 @@ object OriginSerializer extends MinimalSerializer({ JField("htlcId", JLong(htlc.originHtlcId)), JField("amount", JLong(htlc.amountIn.toLong)), ) - }.toList) + }) } }) // @formatter:off -case class CommitmentJson(fundingTxIndex: Long, fundingTx: InputInfo, localFunding: LocalFundingStatus, remoteFunding: RemoteFundingStatus, localCommit: LocalCommit, remoteCommit: RemoteCommit, nextRemoteCommit: Option[RemoteCommit]) -object CommitmentSerializer extends ConvertClassSerializer[Commitment](c => CommitmentJson(c.fundingTxIndex, c.commitInput, c.localFundingStatus, c.remoteFundingStatus, c.localCommit, c.remoteCommit, c.nextRemoteCommit_opt.map(_.commit))) +case class CommitmentJson(fundingTxIndex: Long, fundingInput: OutPoint, fundingAmount: Satoshi, localFunding: LocalFundingStatus, remoteFunding: RemoteFundingStatus, commitmentFormat: String, localCommitParams: CommitParams, localCommit: LocalCommit, remoteCommitParams: CommitParams, remoteCommit: RemoteCommit, nextRemoteCommit: Option[RemoteCommit]) +object CommitmentSerializer extends ConvertClassSerializer[Commitment](c => CommitmentJson(c.fundingTxIndex, c.fundingInput, c.fundingAmount, c.localFundingStatus, c.remoteFundingStatus, c.commitmentFormat.toString, c.localCommitParams, c.localCommit, c.remoteCommitParams, c.remoteCommit, c.nextRemoteCommit_opt)) // @formatter:on // @formatter:off -private case class DetailedOnChainBalanceJson(total: Btc, confirmed: Map[OutPoint, Btc], unconfirmed: Map[OutPoint, Btc]) -object DetailedOnChainBalanceSerializer extends ConvertClassSerializer[DetailedOnChainBalance](b => DetailedOnChainBalanceJson(b.total, confirmed = b.confirmed, unconfirmed = b.unconfirmed)) +private case class DetailedOnChainBalanceJson(total: Btc, deeplyConfirmed: Map[OutPoint, Btc], recentlyConfirmed: Map[OutPoint, Btc], unconfirmed: Map[OutPoint, Btc]) +object DetailedOnChainBalanceSerializer extends ConvertClassSerializer[DetailedOnChainBalance](b => DetailedOnChainBalanceJson(b.total, deeplyConfirmed = b.deeplyConfirmed, recentlyConfirmed = b.recentlyConfirmed, unconfirmed = b.unconfirmed)) private case class GlobalBalanceJson(total: Btc, onChain: DetailedOnChainBalance, offChain: OffChainBalance) object GlobalBalanceSerializer extends ConvertClassSerializer[GlobalBalance](b => GlobalBalanceJson(b.total, b.onChain, b.offChain)) @@ -710,6 +735,7 @@ object JsonSerializers { BlockIdSerializer + BlockHashSerializer + ByteVector64Serializer + + IndividualNonceSerializer + ChannelEventSerializer + UInt64Serializer + TimestampSecondSerializer + @@ -741,7 +767,6 @@ object JsonSerializers { OpenChannelResponseSerializer + CommandResponseSerializer + InputInfoSerializer + - CommitTxAndRemoteSigSerializer + ColorSerializer + ThrowableSerializer + FailureMessageSerializer + @@ -749,6 +774,7 @@ object JsonSerializers { NodeAddressSerializer + DirectedHtlcSerializer + InvoiceSerializer + + OfferSerializer + OfferDataSerializer + JavaUUIDSerializer + OriginSerializer + 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 8370de25e4..ab669d25fc 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,49 +47,27 @@ 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(Features.validateFeatureGraph(features).isEmpty, Features.validateFeatureGraph(features).map(_.message)) - { - val featuresErr = Features.validateFeatureGraph(features) - require(featuresErr.isEmpty, featuresErr.map(_.message)) - } - if (features.hasFeature(Features.PaymentSecret)) { - require(tags.collect { case _: Bolt11Invoice.PaymentSecret => }.size == 1, "there must be exactly one payment secret tag when feature bit is set") - } - - /** - * @return the payment hash - */ - lazy val paymentHash = tags.collectFirst { case p: Bolt11Invoice.PaymentHash => p.hash }.get + lazy val paymentHash: ByteVector32 = tags.collectFirst { case p: Bolt11Invoice.PaymentHash => p.hash }.get + lazy val paymentSecret: ByteVector32 = tags.collectFirst { case p: Bolt11Invoice.PaymentSecret => p.secret }.get - /** - * @return the payment secret - */ - lazy val paymentSecret = tags.collectFirst { case p: Bolt11Invoice.PaymentSecret => p.secret }.get - - /** - * @return the description of the payment, or its hash - */ + /** Description of the payment, or its hash. */ lazy val description: Either[String, ByteVector32] = tags.collectFirst { case Bolt11Invoice.Description(d) => Left(d) case Bolt11Invoice.DescriptionHash(h) => Right(h) }.get - /** - * @return metadata about the payment (see option_payment_metadata). - */ + /** Metadata about the payment (see option_payment_metadata). */ lazy val paymentMetadata: Option[ByteVector] = tags.collectFirst { case m: Bolt11Invoice.PaymentMetadata => m.data } - /** - * @return the fallback address if any. It could be a script address, pubkey address, .. - */ + /** Fallback on-chain address (if any). It could be a script address, pubkey address, etc. */ def fallbackAddress(): Option[String] = tags.collectFirst { case f: Bolt11Invoice.FallbackAddress => Bolt11Invoice.FallbackAddress.toAddress(f, prefix) } lazy val routingInfo: Seq[Seq[ExtraHop]] = tags.collect { case t: RoutingInfo => t.path } - lazy val extraEdges: Seq[Invoice.ExtraEdge] = routingInfo.flatMap(path => toExtraEdges(path, nodeId)) lazy val relativeExpiry: FiniteDuration = FiniteDuration(tags.collectFirst { case expiry: Bolt11Invoice.Expiry => expiry.toLong }.getOrElse(DEFAULT_EXPIRY_SECONDS), TimeUnit.SECONDS) - lazy val minFinalCltvExpiryDelta: CltvExpiryDelta = tags.collectFirst { case cltvExpiry: Bolt11Invoice.MinFinalCltvExpiry => cltvExpiry.toCltvExpiryDelta }.getOrElse(DEFAULT_MIN_CLTV_EXPIRY_DELTA) override lazy val features: Features[InvoiceFeature] = tags.collectFirst { case f: InvoiceFeatures => f.features.invoiceFeatures() }.getOrElse(Features.empty) @@ -172,7 +150,7 @@ object Bolt11Invoice { // We want to keep invoices as small as possible, so we explicitly remove unknown features. Some(InvoiceFeatures(features.copy(unknown = Set.empty).unscoped())) ).flatten - val routingInfoTags = extraHops.map(RoutingInfo) + val routingInfoTags = extraHops.filter(_.nonEmpty).map(RoutingInfo) defaultTags ++ routingInfoTags } Bolt11Invoice( @@ -269,7 +247,7 @@ object Bolt11Invoice { Try(fromBase58Address(address)).orElse(Try(fromBech32Address(address))).get } - def fromBase58Address(address: String): FallbackAddress = { + private def fromBase58Address(address: String): FallbackAddress = { val (prefix, hash) = { val decoded = Base58Check.decode(address) (decoded.getFirst.byteValue(), ByteVector.view(decoded.getSecond)) @@ -282,7 +260,7 @@ object Bolt11Invoice { } } - def fromBech32Address(address: String): FallbackAddress = { + private def fromBech32Address(address: String): FallbackAddress = { val (_, version, hash) = { val decoded = Bech32.decodeWitnessAddress(address) (decoded.getFirst, decoded.getSecond, ByteVector.view(decoded.getThird)) @@ -345,12 +323,16 @@ object Bolt11Invoice { * * @param path one or more entries containing extra routing information for a private route */ - case class RoutingInfo(path: List[ExtraHop]) extends TaggedField + case class RoutingInfo(path: List[ExtraHop]) extends TaggedField { + require(path.nonEmpty, "routing hint must contain one or more entries") + } /** * Expiry Date */ case class Expiry(bin: BitVector) extends TaggedField { + require(bin.size <= 64, "invoice expiry must be smaller than 2^64") + def toLong: Long = bin.toLong(signed = false) } @@ -365,7 +347,9 @@ object Bolt11Invoice { * Min final CLTV expiry */ case class MinFinalCltvExpiry(bin: BitVector) extends TaggedField { - def toCltvExpiryDelta = CltvExpiryDelta(bin.toInt(signed = false)) + require(bin.size <= 32, "invoice min_final_cltv_expiry_delta must be smaller than 2^32") + + def toCltvExpiryDelta: CltvExpiryDelta = CltvExpiryDelta(bin.toInt(signed = false)) } object MinFinalCltvExpiry { @@ -397,7 +381,7 @@ object Bolt11Invoice { ("cltv_expiry_delta" | cltvExpiryDelta) ).as[ExtraHop] - val extraHopsLengthCodec = Codec[Int]( + private val extraHopsLengthCodec = Codec[Int]( (_: Int) => Attempt.successful(BitVector.empty), // we don't encode the length (wire: BitVector) => Attempt.successful(DecodeResult(wire.size.toInt / 408, wire)) // we infer the number of items by the size of the data ) @@ -407,7 +391,7 @@ object Bolt11Invoice { (wire: BitVector) => (limitedSizeBits(wire.size - wire.size % 8, valueCodec) ~ constant(BitVector.fill(wire.size % 8)(high = false))).map(_._1).decode(wire) // the 'constant' codec ensures that padding is zero ) - val dataLengthCodec: Codec[Long] = uint(10).xmap(_ * 5, s => (s / 5 + (if (s % 5 == 0) 0 else 1)).toInt) + private val dataLengthCodec: Codec[Long] = uint(10).xmap(_ * 5, s => (s / 5 + (if (s % 5 == 0) 0 else 1)).toInt) def dataCodec[A](valueCodec: Codec[A], expectedLength: Option[Long] = None): Codec[A] = paddedVarAlignedBits( dataLengthCodec.narrow(l => if (expectedLength.getOrElse(l) == l) Attempt.successful(l) else Attempt.failure(Err(s"invalid length $l")), l => l), @@ -457,7 +441,7 @@ object Bolt11Invoice { .typecase(30, dataCodec(bits).as[UnknownTag30]) .typecase(31, dataCodec(bits).as[UnknownTag31]) - def fixedSizeTrailingCodec[A](codec: Codec[A], size: Int): Codec[A] = Codec[A]( + private def fixedSizeTrailingCodec[A](codec: Codec[A], size: Int): Codec[A] = Codec[A]( (data: A) => codec.encode(data), (wire: BitVector) => { val (head, tail) = wire.splitAt(wire.size - size) @@ -515,12 +499,12 @@ object Bolt11Invoice { } // char -> 5 bits value - val charToint5: Map[Char, BitVector] = Bech32.alphabet.zipWithIndex.toMap.view.mapValues(BitVector.fromInt(_, size = 5, ordering = ByteOrdering.BigEndian)).toMap + private val charToint5: Map[Char, BitVector] = Bech32.alphabet.zipWithIndex.toMap.view.mapValues(BitVector.fromInt(_, size = 5, ordering = ByteOrdering.BigEndian)).toMap // TODO: could be optimized by preallocating the resulting buffer def string2Bits(data: String): BitVector = data.map(charToint5).foldLeft(BitVector.empty)(_ ++ _) - val eight2fiveCodec: Codec[List[java.lang.Byte]] = list(ubyte(5).xmap[java.lang.Byte](b => b, b => b)) + private val eight2fiveCodec: Codec[List[java.lang.Byte]] = list(ubyte(5).xmap[java.lang.Byte](b => b, b => b)) /** * @param input bech32-encoded invoice diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Bolt12Invoice.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Bolt12Invoice.scala index 82ad8d9c87..ae65046c51 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Bolt12Invoice.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Bolt12Invoice.scala @@ -47,7 +47,7 @@ case class Bolt12Invoice(records: TlvStream[InvoiceTlv]) extends Invoice { val description: Option[String] = invoiceRequest.offer.description override val createdAt: TimestampSecond = records.get[InvoiceCreatedAt].get.timestamp override val relativeExpiry: FiniteDuration = FiniteDuration(records.get[InvoiceRelativeExpiry].map(_.seconds).getOrElse(DEFAULT_EXPIRY_SECONDS), TimeUnit.SECONDS) - override val features: Features[InvoiceFeature] = records.get[InvoiceFeatures].map(_.features.invoiceFeatures()).getOrElse(Features.empty) + override val features: Features[InvoiceFeature] = records.get[InvoiceFeatures].map(f => Features(f.features).invoiceFeatures()).getOrElse(Features.empty) val blindedPaths: Seq[PaymentBlindedRoute] = records.get[InvoicePaths].get.paths.zip(records.get[InvoiceBlindedPay].get.paymentInfo).map { case (route, info) => PaymentBlindedRoute(route, info) } val fallbacks: Option[Seq[FallbackAddress]] = records.get[InvoiceFallbacks].map(_.addresses) val signature: ByteVector64 = records.get[Signature].get.signature @@ -114,7 +114,7 @@ object Bolt12Invoice { Some(InvoiceRelativeExpiry(invoiceExpiry.toSeconds)), Some(InvoicePaymentHash(Crypto.sha256(preimage))), Some(InvoiceAmount(amount)), - if (!features.isEmpty) Some(InvoiceFeatures(features.unscoped())) else None, + if (!features.isEmpty) Some(InvoiceFeatures(features.unscoped().toByteVector)) else None, Some(InvoiceNodeId(nodeKey.publicKey)), ).flatten ++ additionalTlvs val signature = signSchnorr(signatureTag, rootHash(TlvStream(tlvs, request.records.unknown ++ customTlvs), OfferCodecs.invoiceTlvCodec), nodeKey) @@ -127,7 +127,7 @@ object Bolt12Invoice { _ -> () ) if (records.get[InvoiceAmount].isEmpty) return Left(MissingRequiredTlv(UInt64(170))) - if (records.get[InvoicePaths].forall(_.paths.isEmpty)) return Left(MissingRequiredTlv(UInt64(160))) + if (records.get[InvoicePaths].isEmpty) return Left(MissingRequiredTlv(UInt64(160))) if (records.get[InvoiceBlindedPay].map(_.paymentInfo.length) != records.get[InvoicePaths].map(_.paths.length)) return Left(MissingRequiredTlv(UInt64(162))) if (records.get[InvoiceNodeId].isEmpty) return Left(MissingRequiredTlv(UInt64(176))) if (records.get[InvoiceCreatedAt].isEmpty) return Left(MissingRequiredTlv(UInt64(164))) @@ -167,7 +167,7 @@ case class MinimalBolt12Invoice(records: TlvStream[InvoiceTlv]) extends Invoice val description: Option[String] = records.get[OfferDescription].map(_.description) override val createdAt: TimestampSecond = records.get[InvoiceCreatedAt].get.timestamp override val relativeExpiry: FiniteDuration = FiniteDuration(records.get[InvoiceRelativeExpiry].map(_.seconds).getOrElse(Bolt12Invoice.DEFAULT_EXPIRY_SECONDS), TimeUnit.SECONDS) - override val features: Features[InvoiceFeature] = records.get[InvoiceFeatures].map(_.features.invoiceFeatures()).getOrElse(Features[InvoiceFeature](Features.BasicMultiPartPayment -> FeatureSupport.Optional)) + override val features: Features[InvoiceFeature] = records.get[InvoiceFeatures].map(f => Features(f.features).invoiceFeatures()).getOrElse(Features[InvoiceFeature](Features.BasicMultiPartPayment -> FeatureSupport.Optional)) override def toString: String = { val data = OfferCodecs.invoiceTlvCodec.encode(records).require.bytes diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentEvents.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentEvents.scala index c070d9fb7e..37721d3ad6 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentEvents.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentEvents.scala @@ -19,6 +19,7 @@ package fr.acinq.eclair.payment import fr.acinq.bitcoin.scalacompat.ByteVector32 import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.eclair.crypto.Sphinx +import fr.acinq.eclair.crypto.Sphinx.HoldTime import fr.acinq.eclair.payment.Invoice.ExtraEdge import fr.acinq.eclair.payment.send.PaymentError.RetryExhausted import fr.acinq.eclair.payment.send.PaymentInitiator.SendPaymentConfig @@ -45,15 +46,16 @@ sealed trait PaymentEvent { /** * A payment was successfully sent and fulfilled. * - * @param id id of the whole payment attempt (if using multi-part, there will be multiple parts, each with - * a different id). - * @param paymentHash payment hash. - * @param paymentPreimage payment preimage (proof of payment). - * @param recipientAmount amount that has been received by the final recipient. - * @param recipientNodeId id of the final recipient. - * @param parts child payments (actual outgoing HTLCs). + * @param id id of the whole payment attempt (if using multi-part, there will be multiple parts, + * each with a different id). + * @param paymentHash payment hash. + * @param paymentPreimage payment preimage (proof of payment). + * @param recipientAmount amount that has been received by the final recipient. + * @param recipientNodeId id of the final recipient. + * @param parts child payments (actual outgoing HTLCs). + * @param remainingAttribution_opt for relayed trampoline payments, the attribution data that needs to be sent upstream */ -case class PaymentSent(id: UUID, paymentHash: ByteVector32, paymentPreimage: ByteVector32, recipientAmount: MilliSatoshi, recipientNodeId: PublicKey, parts: Seq[PaymentSent.PartialPayment]) extends PaymentEvent { +case class PaymentSent(id: UUID, paymentHash: ByteVector32, paymentPreimage: ByteVector32, recipientAmount: MilliSatoshi, recipientNodeId: PublicKey, parts: Seq[PaymentSent.PartialPayment], remainingAttribution_opt: Option[ByteVector]) extends PaymentEvent { require(parts.nonEmpty, "must have at least one payment part") val amountWithFees: MilliSatoshi = parts.map(_.amountWithFees).sum val feesPaid: MilliSatoshi = amountWithFees - recipientAmount // overall fees for this payment @@ -84,18 +86,18 @@ case class PaymentFailed(id: UUID, paymentHash: ByteVector32, failures: Seq[Paym sealed trait PaymentRelayed extends PaymentEvent { val amountIn: MilliSatoshi val amountOut: MilliSatoshi - val startedAt: TimestampMilli + val receivedAt: TimestampMilli val settledAt: TimestampMilli } -case class ChannelPaymentRelayed(amountIn: MilliSatoshi, amountOut: MilliSatoshi, paymentHash: ByteVector32, fromChannelId: ByteVector32, toChannelId: ByteVector32, startedAt: TimestampMilli, settledAt: TimestampMilli) extends PaymentRelayed { +case class ChannelPaymentRelayed(amountIn: MilliSatoshi, amountOut: MilliSatoshi, paymentHash: ByteVector32, fromChannelId: ByteVector32, toChannelId: ByteVector32, receivedAt: TimestampMilli, settledAt: TimestampMilli) extends PaymentRelayed { override val timestamp: TimestampMilli = settledAt } case class TrampolinePaymentRelayed(paymentHash: ByteVector32, incoming: PaymentRelayed.Incoming, outgoing: PaymentRelayed.Outgoing, nextTrampolineNodeId: PublicKey, nextTrampolineAmount: MilliSatoshi) extends PaymentRelayed { override val amountIn: MilliSatoshi = incoming.map(_.amount).sum override val amountOut: MilliSatoshi = outgoing.map(_.amount).sum - override val startedAt: TimestampMilli = incoming.map(_.receivedAt).minOption.getOrElse(TimestampMilli.now()) + override val receivedAt: TimestampMilli = incoming.map(_.receivedAt).minOption.getOrElse(TimestampMilli.now()) override val settledAt: TimestampMilli = outgoing.map(_.settledAt).maxOption.getOrElse(TimestampMilli.now()) override val timestamp: TimestampMilli = settledAt } @@ -103,7 +105,7 @@ case class TrampolinePaymentRelayed(paymentHash: ByteVector32, incoming: Payment case class OnTheFlyFundingPaymentRelayed(paymentHash: ByteVector32, incoming: PaymentRelayed.Incoming, outgoing: PaymentRelayed.Outgoing) extends PaymentRelayed { override val amountIn: MilliSatoshi = incoming.map(_.amount).sum override val amountOut: MilliSatoshi = outgoing.map(_.amount).sum - override val startedAt: TimestampMilli = incoming.map(_.receivedAt).minOption.getOrElse(TimestampMilli.now()) + override val receivedAt: TimestampMilli = incoming.map(_.receivedAt).minOption.getOrElse(TimestampMilli.now()) override val settledAt: TimestampMilli = outgoing.map(_.settledAt).maxOption.getOrElse(TimestampMilli.now()) override val timestamp: TimestampMilli = settledAt } @@ -150,7 +152,7 @@ case class LocalFailure(amount: MilliSatoshi, route: Seq[Hop], t: Throwable) ext case class RemoteFailure(amount: MilliSatoshi, route: Seq[Hop], e: Sphinx.DecryptedFailurePacket) extends PaymentFailure /** A remote node failed the payment but we couldn't decrypt the failure (e.g. a malicious node tampered with the message). */ -case class UnreadableRemoteFailure(amount: MilliSatoshi, route: Seq[Hop], failurePacket: ByteVector) extends PaymentFailure +case class UnreadableRemoteFailure(amount: MilliSatoshi, route: Seq[Hop], e: Sphinx.CannotDecryptFailurePacket, holdTimes: Seq[HoldTime]) extends PaymentFailure object PaymentFailure { @@ -235,13 +237,15 @@ object PaymentFailure { } case RemoteFailure(_, hops, Sphinx.DecryptedFailurePacket(nodeId, _)) => ignoreNodeOutgoingEdge(nodeId, hops, ignore) - case UnreadableRemoteFailure(_, hops, _) => + case UnreadableRemoteFailure(_, hops, _, holdTimes) => + // TODO: Once everyone supports attributable errors, we should only exclude two nodes: the last for which we have attribution data and the next one. // We don't know which node is sending garbage, let's blacklist all nodes except: + // - the nodes that returned attribution data (except the last one) // - the one we are directly connected to: it would be too restrictive for retries // - the final recipient: they have no incentive to send garbage since they want that payment // - the introduction point of a blinded route: we don't want a node before the blinded path to force us to ignore that blinded path // - the trampoline node: we don't want a node before the trampoline node to force us to ignore that trampoline node - val blacklist = hops.collect { case hop: ChannelHop => hop }.map(_.nextNodeId).drop(1).dropRight(1).toSet + val blacklist = hops.collect { case hop: ChannelHop => hop }.map(_.nextNodeId).drop(1 max (holdTimes.length - 1)).dropRight(1).toSet ignore ++ blacklist case LocalFailure(_, hops, _) => hops.headOption match { case Some(hop: ChannelHop) => diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentPacket.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentPacket.scala index 9c22b0ecc5..9a77355dcb 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentPacket.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentPacket.scala @@ -18,35 +18,39 @@ package fr.acinq.eclair.payment import fr.acinq.bitcoin.scalacompat.ByteVector32 import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} -import fr.acinq.eclair.channel.{CMD_ADD_HTLC, CMD_FAIL_HTLC, CannotExtractSharedSecret, Origin} +import fr.acinq.eclair.channel._ import fr.acinq.eclair.crypto.Sphinx import fr.acinq.eclair.payment.send.Recipient +import fr.acinq.eclair.reputation.Reputation import fr.acinq.eclair.router.Router.Route import fr.acinq.eclair.wire.protocol.OnionPaymentPayloadTlv.{InvoiceRoutingInfo, OutgoingBlindedPaths} import fr.acinq.eclair.wire.protocol.PaymentOnion.{FinalPayload, IntermediatePayload, PerHopPayload} import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, Feature, Features, MilliSatoshi, ShortChannelId, UInt64, randomBytes32, randomKey} +import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, Feature, Features, MilliSatoshi, ShortChannelId, TimestampMilli, UInt64, randomBytes32, randomKey} import scodec.bits.ByteVector import scodec.{Attempt, DecodeResult} +import scala.concurrent.duration.{DurationInt, FiniteDuration} import scala.util.{Failure, Success} /** * Created by t-bast on 08/10/2019. */ -sealed trait IncomingPaymentPacket +sealed trait IncomingPaymentPacket { + def receivedAt: TimestampMilli +} /** Helpers to handle incoming payment packets. */ object IncomingPaymentPacket { // @formatter:off /** We are the final recipient. */ - case class FinalPacket(add: UpdateAddHtlc, payload: FinalPayload) extends IncomingPaymentPacket + case class FinalPacket(add: UpdateAddHtlc, payload: FinalPayload, receivedAt: TimestampMilli) extends IncomingPaymentPacket /** We are an intermediate node. */ sealed trait RelayPacket extends IncomingPaymentPacket /** We must relay the payment to a direct peer. */ - case class ChannelRelayPacket(add: UpdateAddHtlc, payload: IntermediatePayload.ChannelRelay, nextPacket: OnionRoutingPacket) extends RelayPacket { + case class ChannelRelayPacket(add: UpdateAddHtlc, payload: IntermediatePayload.ChannelRelay, nextPacket: OnionRoutingPacket, receivedAt: TimestampMilli) extends RelayPacket { val amountToForward: MilliSatoshi = payload.amountToForward(add.amountMsat) val outgoingCltv: CltvExpiry = payload.outgoingCltv(add.cltvExpiry) val relayFeeMsat: MilliSatoshi = add.amountMsat - amountToForward @@ -58,9 +62,9 @@ object IncomingPaymentPacket { def outerPayload: FinalPayload.Standard def innerPayload: IntermediatePayload.NodeRelay } - case class RelayToTrampolinePacket(add: UpdateAddHtlc, outerPayload: FinalPayload.Standard, innerPayload: IntermediatePayload.NodeRelay.Standard, nextPacket: OnionRoutingPacket) extends NodeRelayPacket - case class RelayToNonTrampolinePacket(add: UpdateAddHtlc, outerPayload: FinalPayload.Standard, innerPayload: IntermediatePayload.NodeRelay.ToNonTrampoline) extends NodeRelayPacket - case class RelayToBlindedPathsPacket(add: UpdateAddHtlc, outerPayload: FinalPayload.Standard, innerPayload: IntermediatePayload.NodeRelay.ToBlindedPaths) extends NodeRelayPacket + case class RelayToTrampolinePacket(add: UpdateAddHtlc, outerPayload: FinalPayload.Standard, innerPayload: IntermediatePayload.NodeRelay.Standard, nextPacket: OnionRoutingPacket, receivedAt: TimestampMilli) extends NodeRelayPacket + case class RelayToNonTrampolinePacket(add: UpdateAddHtlc, outerPayload: FinalPayload.Standard, innerPayload: IntermediatePayload.NodeRelay.ToNonTrampoline, receivedAt: TimestampMilli) extends NodeRelayPacket + case class RelayToBlindedPathsPacket(add: UpdateAddHtlc, outerPayload: FinalPayload.Standard, innerPayload: IntermediatePayload.NodeRelay.ToBlindedPaths, receivedAt: TimestampMilli) extends NodeRelayPacket // @formatter:on case class DecodedOnionPacket(payload: TlvStream[OnionPaymentPayloadTlv], next_opt: Option[OnionRoutingPacket]) @@ -134,7 +138,7 @@ object IncomingPaymentPacket { decryptEncryptedRecipientData(add, privateKey, payload, encrypted.data).flatMap { case DecodedEncryptedRecipientData(blindedPayload, nextPathKey) => validateBlindedChannelRelayPayload(add, payload, blindedPayload, nextPathKey, nextPacket).flatMap { - case ChannelRelayPacket(_, payload, nextPacket) if payload.outgoing == Right(ShortChannelId.toSelf) => + case ChannelRelayPacket(_, payload, nextPacket, _) if payload.outgoing == Right(ShortChannelId.toSelf) => decrypt(add.copy(onionRoutingPacket = nextPacket, tlvStream = add.tlvStream.copy(records = Set(UpdateAddHtlcTlv.PathKey(nextPathKey)))), privateKey, features) case relayPacket => Right(relayPacket) } @@ -142,7 +146,7 @@ object IncomingPaymentPacket { case None if add.pathKey_opt.isDefined => Left(InvalidOnionBlinding(Sphinx.hash(add.onionRoutingPacket))) case None => // We are not inside a blinded path: channel relay information is directly available. - IntermediatePayload.ChannelRelay.Standard.validate(payload).left.map(_.failureMessage).map(payload => ChannelRelayPacket(add, payload, nextPacket)) + IntermediatePayload.ChannelRelay.Standard.validate(payload).left.map(_.failureMessage).map(payload => ChannelRelayPacket(add, payload, nextPacket, TimestampMilli.now())) } case DecodedOnionPacket(payload, None) => // We are the final node for the outer onion, so we are either: @@ -215,7 +219,7 @@ object IncomingPaymentPacket { case payload if add.amountMsat < payload.paymentRelayData.paymentConstraints.minAmount => Left(InvalidOnionBlinding(Sphinx.hash(add.onionRoutingPacket))) case payload if add.cltvExpiry > payload.paymentRelayData.paymentConstraints.maxCltvExpiry => Left(InvalidOnionBlinding(Sphinx.hash(add.onionRoutingPacket))) case payload if !Features.areCompatible(Features.empty, payload.paymentRelayData.allowedFeatures) => Left(InvalidOnionBlinding(Sphinx.hash(add.onionRoutingPacket))) - case payload => Right(ChannelRelayPacket(add, payload, nextPacket)) + case payload => Right(ChannelRelayPacket(add, payload, nextPacket, TimestampMilli.now())) } } @@ -223,7 +227,7 @@ object IncomingPaymentPacket { FinalPayload.Standard.validate(payload).left.map(_.failureMessage).flatMap { case payload if add.amountMsat < payload.amount => Left(FinalIncorrectHtlcAmount(add.amountMsat)) case payload if add.cltvExpiry < payload.expiry => Left(FinalIncorrectCltvExpiry(add.cltvExpiry)) - case payload => Right(FinalPacket(add, payload)) + case payload => Right(FinalPacket(add, payload, TimestampMilli.now())) } } @@ -233,7 +237,7 @@ object IncomingPaymentPacket { case payload if payload.paymentConstraints_opt.exists(c => c.maxCltvExpiry < add.cltvExpiry) => Left(InvalidOnionBlinding(Sphinx.hash(add.onionRoutingPacket))) case payload if !Features.areCompatible(Features.empty, payload.allowedFeatures) => Left(InvalidOnionBlinding(Sphinx.hash(add.onionRoutingPacket))) case payload if add.cltvExpiry < payload.expiry => Left(InvalidOnionBlinding(Sphinx.hash(add.onionRoutingPacket))) - case payload => Right(FinalPacket(add, payload)) + case payload => Right(FinalPacket(add, payload, TimestampMilli.now())) } } @@ -249,7 +253,7 @@ object IncomingPaymentPacket { // We merge contents from the outer and inner payloads. // We must use the inner payload's total amount and payment secret because the payment may be split between multiple trampoline payments (#reckless). val trampolinePacket = outerPayload.records.get[OnionPaymentPayloadTlv.TrampolineOnion].map(_.packet) - Right(FinalPacket(add, FinalPayload.Standard.createPayload(outerPayload.amount, innerPayload.totalAmount, innerPayload.expiry, innerPayload.paymentSecret, innerPayload.paymentMetadata, trampolinePacket))) + Right(FinalPacket(add, FinalPayload.Standard.createPayload(outerPayload.amount, innerPayload.totalAmount, innerPayload.expiry, innerPayload.paymentSecret, innerPayload.paymentMetadata, trampolinePacket), TimestampMilli.now())) } } } @@ -260,7 +264,7 @@ object IncomingPaymentPacket { IntermediatePayload.NodeRelay.Standard.validate(innerPayload).left.map(_.failureMessage).flatMap { case _ if add.amountMsat < outerPayload.amount => Left(FinalIncorrectHtlcAmount(add.amountMsat)) case _ if add.cltvExpiry != outerPayload.expiry => Left(FinalIncorrectCltvExpiry(add.cltvExpiry)) - case innerPayload => Right(RelayToTrampolinePacket(add, outerPayload, innerPayload, next)) + case innerPayload => Right(RelayToTrampolinePacket(add, outerPayload, innerPayload, next, TimestampMilli.now())) } } } @@ -270,7 +274,7 @@ object IncomingPaymentPacket { IntermediatePayload.NodeRelay.ToNonTrampoline.validate(innerPayload).left.map(_.failureMessage).flatMap { case _ if add.amountMsat < outerPayload.amount => Left(FinalIncorrectHtlcAmount(add.amountMsat)) case _ if add.cltvExpiry != outerPayload.expiry => Left(FinalIncorrectCltvExpiry(add.cltvExpiry)) - case innerPayload => Right(RelayToNonTrampolinePacket(add, outerPayload, innerPayload)) + case innerPayload => Right(RelayToNonTrampolinePacket(add, outerPayload, innerPayload, TimestampMilli.now())) } } } @@ -280,7 +284,7 @@ object IncomingPaymentPacket { IntermediatePayload.NodeRelay.ToBlindedPaths.validate(innerPayload).left.map(_.failureMessage).flatMap { case _ if add.amountMsat < outerPayload.amount => Left(FinalIncorrectHtlcAmount(add.amountMsat)) case _ if add.cltvExpiry != outerPayload.expiry => Left(FinalIncorrectCltvExpiry(add.cltvExpiry)) - case innerPayload => Right(RelayToBlindedPathsPacket(add, outerPayload, innerPayload)) + case innerPayload => Right(RelayToBlindedPathsPacket(add, outerPayload, innerPayload, TimestampMilli.now())) } } } @@ -335,22 +339,28 @@ object OutgoingPaymentPacket { } /** Build the command to add an HTLC for the given recipient using the provided route. */ - def buildOutgoingPayment(origin: Origin.Hot, paymentHash: ByteVector32, route: Route, recipient: Recipient, confidence: Double): Either[OutgoingPaymentError, OutgoingPaymentPacket] = { + def buildOutgoingPayment(origin: Origin.Hot, paymentHash: ByteVector32, route: Route, recipient: Recipient, reputationScore: Reputation.Score): Either[OutgoingPaymentError, OutgoingPaymentPacket] = { for { payment <- recipient.buildPayloads(paymentHash, route) onion <- buildOnion(payment.payloads, paymentHash, Some(PaymentOnionCodecs.paymentOnionPayloadLength)) // BOLT 2 requires that associatedData == paymentHash } yield { - val cmd = CMD_ADD_HTLC(origin.replyTo, payment.amount, paymentHash, payment.expiry, onion.packet, payment.outerPathKey_opt, confidence, fundingFee_opt = None, origin, commit = true) + val cmd = CMD_ADD_HTLC(origin.replyTo, payment.amount, paymentHash, payment.expiry, onion.packet, payment.outerPathKey_opt, reputationScore, fundingFee_opt = None, origin, commit = true) OutgoingPaymentPacket(cmd, route.hops.head.shortChannelId, onion.sharedSecrets) } } - private def buildHtlcFailure(nodeSecret: PrivateKey, reason: FailureReason, add: UpdateAddHtlc): Either[CannotExtractSharedSecret, ByteVector] = { + private def buildHtlcFailure(nodeSecret: PrivateKey, useAttributableFailures: Boolean, reason: FailureReason, add: UpdateAddHtlc, holdTime: FiniteDuration): Either[CannotExtractSharedSecret, (ByteVector, TlvStream[UpdateFailHtlcTlv])] = { extractSharedSecret(nodeSecret, add).map(sharedSecret => { - reason match { - case FailureReason.EncryptedDownstreamFailure(packet) => Sphinx.FailurePacket.wrap(packet, sharedSecret) - case FailureReason.LocalFailure(failure) => Sphinx.FailurePacket.create(sharedSecret, failure) + val (packet, attribution) = reason match { + case FailureReason.EncryptedDownstreamFailure(packet, attribution) => (packet, attribution) + case FailureReason.LocalFailure(failure) => (Sphinx.FailurePacket.create(sharedSecret, failure), None) + } + val tlvs: TlvStream[UpdateFailHtlcTlv] = if (useAttributableFailures) { + TlvStream(UpdateFailHtlcTlv.AttributionData(Sphinx.Attribution.create(attribution, Some(packet), holdTime, sharedSecret))) + } else { + TlvStream.empty } + (Sphinx.FailurePacket.wrap(packet, sharedSecret), tlvs) }) } @@ -367,15 +377,33 @@ object OutgoingPaymentPacket { } } - def buildHtlcFailure(nodeSecret: PrivateKey, cmd: CMD_FAIL_HTLC, add: UpdateAddHtlc): Either[CannotExtractSharedSecret, HtlcFailureMessage] = { + def buildHtlcFailure(nodeSecret: PrivateKey, useAttributableFailures: Boolean, cmd: CMD_FAIL_HTLC, add: UpdateAddHtlc, now: TimestampMilli = TimestampMilli.now()): Either[CannotExtractSharedSecret, HtlcFailureMessage] = { add.pathKey_opt match { case Some(_) => // We are part of a blinded route and we're not the introduction node. val failure = InvalidOnionBlinding(Sphinx.hash(add.onionRoutingPacket)) Right(UpdateFailMalformedHtlc(add.channelId, add.id, failure.onionHash, failure.code)) case None => - buildHtlcFailure(nodeSecret, cmd.reason, add).map(encryptedReason => UpdateFailHtlc(add.channelId, cmd.id, encryptedReason)) + // If the htlcReceivedAt was lost (because the node restarted), we use a hold time of 0 which should be ignored by the payer. + val holdTime = cmd.attribution_opt.map(now - _.htlcReceivedAt).getOrElse(0 millisecond) + buildHtlcFailure(nodeSecret, useAttributableFailures, cmd.reason, add, holdTime).map { + case (encryptedReason, tlvs) => UpdateFailHtlc(add.channelId, cmd.id, encryptedReason, tlvs) + } } } + def buildHtlcFulfill(nodeSecret: PrivateKey, useAttributionData: Boolean, cmd: CMD_FULFILL_HTLC, add: UpdateAddHtlc, now: TimestampMilli = TimestampMilli.now()): UpdateFulfillHtlc = { + // If we are part of a blinded route, we must not populate attribution data. + val tlvs: TlvStream[UpdateFulfillHtlcTlv] = if (useAttributionData && add.pathKey_opt.isEmpty) { + extractSharedSecret(nodeSecret, add) match { + case Left(_) => TlvStream.empty + case Right(sharedSecret) => + val holdTime = cmd.attribution_opt.map(now - _.htlcReceivedAt).getOrElse(0 millisecond) + TlvStream(UpdateFulfillHtlcTlv.AttributionData(Sphinx.Attribution.create(cmd.attribution_opt.flatMap(_.downstreamAttribution_opt), None, holdTime, sharedSecret))) + } + } else { + TlvStream.empty + } + UpdateFulfillHtlc(add.channelId, cmd.id, cmd.r, tlvs) + } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/offer/OfferCreator.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/offer/OfferCreator.scala index 9cfe7346a0..30c671c3ef 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/offer/OfferCreator.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/offer/OfferCreator.scala @@ -83,7 +83,7 @@ private class OfferCreator(context: ActorContext[OfferCreator.Command], } else { val tlvs: Set[OfferTlv] = Set( if (nodeParams.chainHash != Block.LivenetGenesisBlock.hash) Some(OfferChains(Seq(nodeParams.chainHash))) else None, - amount_opt.map(OfferAmount), + amount_opt.map(_.toLong).map(OfferAmount), description_opt.map(OfferDescription), expiry_opt.map(OfferAbsoluteExpiry), issuer_opt.map(OfferIssuer), diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartHandler.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartHandler.scala index c2dc342bba..6822aa721a 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartHandler.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartHandler.scala @@ -26,7 +26,7 @@ import fr.acinq.bitcoin.scalacompat.Crypto.PrivateKey import fr.acinq.bitcoin.scalacompat.{ByteVector32, Crypto} import fr.acinq.eclair.EncodedNodeId.ShortChannelIdDir import fr.acinq.eclair.Logs.LogCategory -import fr.acinq.eclair.channel.{CMD_FAIL_HTLC, CMD_FULFILL_HTLC, RES_SUCCESS} +import fr.acinq.eclair.channel._ import fr.acinq.eclair.crypto.Sphinx.RouteBlinding.BlindedRoute import fr.acinq.eclair.db._ import fr.acinq.eclair.payment.Bolt11Invoice.ExtraHop @@ -53,13 +53,13 @@ class MultiPartHandler(nodeParams: NodeParams, register: ActorRef, db: IncomingP // NB: this is safe because this handler will be called from within an actor private var pendingPayments: Map[ByteVector32, (IncomingPayment, ActorRef)] = Map.empty - private def addHtlcPart(ctx: ActorContext, add: UpdateAddHtlc, payload: FinalPayload, payment: IncomingPayment): Unit = { + private def addHtlcPart(ctx: ActorContext, add: UpdateAddHtlc, payload: FinalPayload, payment: IncomingPayment, receivedAt: TimestampMilli): Unit = { pendingPayments.get(add.paymentHash) match { case Some((_, handler)) => - handler ! MultiPartPaymentFSM.HtlcPart(payload.totalAmount, add) + handler ! MultiPartPaymentFSM.HtlcPart(payload.totalAmount, add, receivedAt) case None => val handler = ctx.actorOf(MultiPartPaymentFSM.props(nodeParams, add.paymentHash, payload.totalAmount, ctx.self)) - handler ! MultiPartPaymentFSM.HtlcPart(payload.totalAmount, add) + handler ! MultiPartPaymentFSM.HtlcPart(payload.totalAmount, add, receivedAt) pendingPayments = pendingPayments + (add.paymentHash -> (payment, handler)) } } @@ -86,10 +86,10 @@ class MultiPartHandler(nodeParams: NodeParams, register: ActorRef, db: IncomingP val child = ctx.spawnAnonymous(GetIncomingPaymentActor(nodeParams, p, offerManager)) child ! GetIncomingPaymentActor.GetIncomingPayment(ctx.self) - case ProcessPacket(add, payload, payment_opt) if doHandle(add.paymentHash) => + case ProcessPacket(add, payload, payment_opt, receivedAt) if doHandle(add.paymentHash) => Logs.withMdc(log)(Logs.mdc(paymentHash_opt = Some(add.paymentHash))) { payment_opt match { - case Some(payment) => validateStandardPayment(nodeParams, add, payload, payment) match { + case Some(payment) => validateStandardPayment(nodeParams, add, payload, payment, receivedAt) match { case Some(cmdFail) => Metrics.PaymentFailed.withTag(Tags.Direction, Tags.Directions.Received).withTag(Tags.Failure, Tags.FailureType(cmdFail)).increment() PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, add.channelId, cmdFail) @@ -100,7 +100,7 @@ class MultiPartHandler(nodeParams: NodeParams, register: ActorRef, db: IncomingP log.debug("received payment for amount={} totalAmount={} paymentMetadata={}", add.amountMsat, payload.totalAmount, payload.paymentMetadata.map(_.toHex).getOrElse("none")) Metrics.PaymentHtlcReceived.withTag(Tags.PaymentMetadataIncluded, payload.paymentMetadata.nonEmpty).increment() payload.paymentMetadata.foreach(metadata => ctx.system.eventStream.publish(PaymentMetadataReceived(add.paymentHash, metadata))) - addHtlcPart(ctx, add, payload, payment) + addHtlcPart(ctx, add, payload, payment, receivedAt) } case None => payload.paymentPreimage match { case Some(paymentPreimage) if nodeParams.features.hasFeature(Features.KeySend) => @@ -116,25 +116,26 @@ class MultiPartHandler(nodeParams: NodeParams, register: ActorRef, db: IncomingP val invoice = Bolt11Invoice(nodeParams.chainHash, amount, paymentHash, nodeParams.privateKey, desc, nodeParams.channelConf.minFinalExpiryDelta, paymentSecret = payload.paymentSecret, features = features) log.debug("generated fake invoice={} from amount={} (KeySend)", invoice.toString, amount) db.addIncomingPayment(invoice, paymentPreimage, PaymentType.KeySend) - ctx.self ! ProcessPacket(add, payload, Some(IncomingStandardPayment(invoice, paymentPreimage, PaymentType.KeySend, TimestampMilli.now(), IncomingPaymentStatus.Pending))) + ctx.self ! ProcessPacket(add, payload, Some(IncomingStandardPayment(invoice, paymentPreimage, PaymentType.KeySend, TimestampMilli.now(), IncomingPaymentStatus.Pending)), receivedAt) case _ => Metrics.PaymentFailed.withTag(Tags.Direction, Tags.Directions.Received).withTag(Tags.Failure, "InvoiceNotFound").increment() - val cmdFail = CMD_FAIL_HTLC(add.id, FailureReason.LocalFailure(IncorrectOrUnknownPaymentDetails(payload.totalAmount, nodeParams.currentBlockHeight)), commit = true) + val attribution = FailureAttributionData(htlcReceivedAt = receivedAt, trampolineReceivedAt_opt = None) + val cmdFail = CMD_FAIL_HTLC(add.id, FailureReason.LocalFailure(IncorrectOrUnknownPaymentDetails(payload.totalAmount, nodeParams.currentBlockHeight)), Some(attribution), commit = true) PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, add.channelId, cmdFail) } } } - case ProcessBlindedPacket(add, payload, payment, maxRecipientPathFees) if doHandle(add.paymentHash) => + case ProcessBlindedPacket(add, payload, payment, maxRecipientPathFees, receivedAt) if doHandle(add.paymentHash) => Logs.withMdc(log)(Logs.mdc(paymentHash_opt = Some(add.paymentHash))) { - validateBlindedPayment(nodeParams, add, payload, payment, maxRecipientPathFees) match { + validateBlindedPayment(nodeParams, add, payload, payment, maxRecipientPathFees, receivedAt) match { case Some(cmdFail) => Metrics.PaymentFailed.withTag(Tags.Direction, Tags.Directions.Received).withTag(Tags.Failure, Tags.FailureType(cmdFail)).increment() PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, add.channelId, cmdFail) case None => val recipientPathFees = payload.amount - add.amountMsat log.debug("received payment for amount={} recipientPathFees={} totalAmount={}", add.amountMsat, recipientPathFees, payload.totalAmount) - addHtlcPart(ctx, add, payload, payment) + addHtlcPart(ctx, add, payload, payment, receivedAt) if (recipientPathFees > 0.msat) { // We've opted into deducing the blinded paths fees from the amount we receive for this payment. // We add an artificial payment part for those fees, otherwise we will never reach the total amount. @@ -143,9 +144,10 @@ class MultiPartHandler(nodeParams: NodeParams, register: ActorRef, db: IncomingP } } - case RejectPacket(add, failure) if doHandle(add.paymentHash) => + case RejectPacket(add, failure, receivedAt) if doHandle(add.paymentHash) => Metrics.PaymentFailed.withTag(Tags.Direction, Tags.Directions.Received).withTag(Tags.Failure, failure.getClass.getSimpleName).increment() - val cmdFail = CMD_FAIL_HTLC(add.id, FailureReason.LocalFailure(failure), commit = true) + val attribution = FailureAttributionData(htlcReceivedAt = receivedAt, trampolineReceivedAt_opt = None) + val cmdFail = CMD_FAIL_HTLC(add.id, FailureReason.LocalFailure(failure), Some(attribution), commit = true) PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, add.channelId, cmdFail) case MultiPartPaymentFSM.MultiPartPaymentFailed(paymentHash, failure, parts) if doHandle(paymentHash) => @@ -154,7 +156,9 @@ class MultiPartHandler(nodeParams: NodeParams, register: ActorRef, db: IncomingP log.warning("payment with paidAmount={} failed ({})", parts.map(_.amount).sum, failure) pendingPayments.get(paymentHash).foreach { case (_, handler: ActorRef) => handler ! PoisonPill } parts.collect { - case p: MultiPartPaymentFSM.HtlcPart => PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, p.htlc.channelId, CMD_FAIL_HTLC(p.htlc.id, FailureReason.LocalFailure(failure), commit = true)) + case p: MultiPartPaymentFSM.HtlcPart => + val attribution = FailureAttributionData(htlcReceivedAt = p.receivedAt, trampolineReceivedAt_opt = None) + PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, p.htlc.channelId, CMD_FAIL_HTLC(p.htlc.id, FailureReason.LocalFailure(failure), Some(attribution), commit = true)) } pendingPayments = pendingPayments - paymentHash } @@ -174,7 +178,9 @@ class MultiPartHandler(nodeParams: NodeParams, register: ActorRef, db: IncomingP Logs.withMdc(log)(Logs.mdc(paymentHash_opt = Some(paymentHash))) { failure match { case Some(failure) => p match { - case p: MultiPartPaymentFSM.HtlcPart => PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, p.htlc.channelId, CMD_FAIL_HTLC(p.htlc.id, FailureReason.LocalFailure(failure), commit = true)) + case p: MultiPartPaymentFSM.HtlcPart => + val attribution = FailureAttributionData(htlcReceivedAt = p.receivedAt, trampolineReceivedAt_opt = None) + PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, p.htlc.channelId, CMD_FAIL_HTLC(p.htlc.id, FailureReason.LocalFailure(failure), Some(attribution), commit = true)) case _: MultiPartPaymentFSM.RecipientBlindedPathFeePart => () } case None => p match { @@ -183,10 +189,12 @@ class MultiPartHandler(nodeParams: NodeParams, register: ActorRef, db: IncomingP case p: MultiPartPaymentFSM.HtlcPart => db.getIncomingPayment(paymentHash).foreach(record => { val received = PaymentReceived(paymentHash, PaymentReceived.PartialPayment(p.amount, p.htlc.channelId) :: Nil) if (db.receiveIncomingPayment(paymentHash, p.amount, received.timestamp)) { - PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, p.htlc.channelId, CMD_FULFILL_HTLC(p.htlc.id, record.paymentPreimage, commit = true)) + val attribution = FulfillAttributionData(htlcReceivedAt = p.receivedAt, trampolineReceivedAt_opt = None, downstreamAttribution_opt = None) + PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, p.htlc.channelId, CMD_FULFILL_HTLC(p.htlc.id, record.paymentPreimage, Some(attribution), commit = true)) ctx.system.eventStream.publish(received) } else { - val cmdFail = CMD_FAIL_HTLC(p.htlc.id, FailureReason.LocalFailure(IncorrectOrUnknownPaymentDetails(received.amount, nodeParams.currentBlockHeight)), commit = true) + val attribution = FailureAttributionData(htlcReceivedAt = p.receivedAt, trampolineReceivedAt_opt = None) + val cmdFail = CMD_FAIL_HTLC(p.htlc.id, FailureReason.LocalFailure(IncorrectOrUnknownPaymentDetails(received.amount, nodeParams.currentBlockHeight)), Some(attribution), commit = true) PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, p.htlc.channelId, cmdFail) } }) @@ -213,7 +221,9 @@ class MultiPartHandler(nodeParams: NodeParams, register: ActorRef, db: IncomingP } if (recordedInDb) { parts.collect { - case p: MultiPartPaymentFSM.HtlcPart => PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, p.htlc.channelId, CMD_FULFILL_HTLC(p.htlc.id, payment.paymentPreimage, commit = true)) + case p: MultiPartPaymentFSM.HtlcPart => + val attribution = FulfillAttributionData(htlcReceivedAt = p.receivedAt, trampolineReceivedAt_opt = None, downstreamAttribution_opt = None) + PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, p.htlc.channelId, CMD_FULFILL_HTLC(p.htlc.id, payment.paymentPreimage, Some(attribution), commit = true)) } postFulfill(received) ctx.system.eventStream.publish(received) @@ -221,7 +231,8 @@ class MultiPartHandler(nodeParams: NodeParams, register: ActorRef, db: IncomingP parts.collect { case p: MultiPartPaymentFSM.HtlcPart => Metrics.PaymentFailed.withTag(Tags.Direction, Tags.Directions.Received).withTag(Tags.Failure, "InvoiceNotFound").increment() - val cmdFail = CMD_FAIL_HTLC(p.htlc.id, FailureReason.LocalFailure(IncorrectOrUnknownPaymentDetails(received.amount, nodeParams.currentBlockHeight)), commit = true) + val attribution = FailureAttributionData(htlcReceivedAt = p.receivedAt, trampolineReceivedAt_opt = None) + val cmdFail = CMD_FAIL_HTLC(p.htlc.id, FailureReason.LocalFailure(IncorrectOrUnknownPaymentDetails(received.amount, nodeParams.currentBlockHeight)), Some(attribution), commit = true) PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, p.htlc.channelId, cmdFail) } } @@ -237,9 +248,9 @@ class MultiPartHandler(nodeParams: NodeParams, register: ActorRef, db: IncomingP object MultiPartHandler { // @formatter:off - private case class ProcessPacket(add: UpdateAddHtlc, payload: FinalPayload.Standard, payment_opt: Option[IncomingStandardPayment]) - private case class ProcessBlindedPacket(add: UpdateAddHtlc, payload: FinalPayload.Blinded, payment: IncomingBlindedPayment, maxRecipientPathFees: MilliSatoshi) - private case class RejectPacket(add: UpdateAddHtlc, failure: FailureMessage) + private case class ProcessPacket(add: UpdateAddHtlc, payload: FinalPayload.Standard, payment_opt: Option[IncomingStandardPayment], receivedAt: TimestampMilli) + private case class ProcessBlindedPacket(add: UpdateAddHtlc, payload: FinalPayload.Blinded, payment: IncomingBlindedPayment, maxRecipientPathFees: MilliSatoshi, receivedAt: TimestampMilli) + private case class RejectPacket(add: UpdateAddHtlc, failure: FailureMessage, receivedAt: TimestampMilli) case class DoFulfill(payment: IncomingPayment, success: MultiPartPaymentFSM.MultiPartPaymentSucceeded) case object GetPendingPayments @@ -292,7 +303,7 @@ object MultiPartHandler { paymentPreimage: ByteVector32, additionalTlvs: Set[InvoiceTlv] = Set.empty, customTlvs: Set[GenericTlv] = Set.empty) extends ReceivePayment { - val amount = invoiceRequest.amount + val amount: MilliSatoshi = invoiceRequest.amount } object CreateInvoiceActor { @@ -371,28 +382,28 @@ object MultiPartHandler { nodeParams.db.payments.getIncomingPayment(packet.add.paymentHash) match { case Some(_: IncomingBlindedPayment) => context.log.info("rejecting non-blinded htlc #{} from channel {}: expected a blinded payment", packet.add.id, packet.add.channelId) - replyTo ! RejectPacket(packet.add, IncorrectOrUnknownPaymentDetails(payload.totalAmount, nodeParams.currentBlockHeight)) - case Some(payment: IncomingStandardPayment) => replyTo ! ProcessPacket(packet.add, payload, Some(payment)) - case None => replyTo ! ProcessPacket(packet.add, payload, None) + replyTo ! RejectPacket(packet.add, IncorrectOrUnknownPaymentDetails(payload.totalAmount, nodeParams.currentBlockHeight), packet.receivedAt) + case Some(payment: IncomingStandardPayment) => replyTo ! ProcessPacket(packet.add, payload, Some(payment), packet.receivedAt) + case None => replyTo ! ProcessPacket(packet.add, payload, None, packet.receivedAt) } Behaviors.stopped case payload: FinalPayload.Blinded => offerManager ! OfferManager.ReceivePayment(context.self, packet.add.paymentHash, payload, packet.add.amountMsat) - waitForPayment(context, nodeParams, replyTo, packet.add, payload) + waitForPayment(context, nodeParams, replyTo, packet.add, payload, packet.receivedAt) } } } } } - private def waitForPayment(context: typed.scaladsl.ActorContext[Command], nodeParams: NodeParams, replyTo: ActorRef, add: UpdateAddHtlc, payload: FinalPayload.Blinded): Behavior[Command] = { + private def waitForPayment(context: typed.scaladsl.ActorContext[Command], nodeParams: NodeParams, replyTo: ActorRef, add: UpdateAddHtlc, payload: FinalPayload.Blinded, packetReceivedAt: TimestampMilli): Behavior[Command] = { Behaviors.receiveMessagePartial { case ProcessPayment(payment, maxRecipientPathFees) => - replyTo ! ProcessBlindedPacket(add, payload, payment, maxRecipientPathFees) + replyTo ! ProcessBlindedPacket(add, payload, payment, maxRecipientPathFees, packetReceivedAt) Behaviors.stopped case RejectPayment(reason) => context.log.info("rejecting blinded htlc #{} from channel {}: {}", add.id, add.channelId, reason) - replyTo ! RejectPacket(add, IncorrectOrUnknownPaymentDetails(payload.totalAmount, nodeParams.currentBlockHeight)) + replyTo ! RejectPacket(add, IncorrectOrUnknownPaymentDetails(payload.totalAmount, nodeParams.currentBlockHeight), packetReceivedAt) Behaviors.stopped } } @@ -464,17 +475,19 @@ object MultiPartHandler { paymentAmountOk && paymentCltvOk && paymentStatusOk && paymentFeaturesOk } - private def validateStandardPayment(nodeParams: NodeParams, add: UpdateAddHtlc, payload: FinalPayload.Standard, record: IncomingStandardPayment)(implicit log: LoggingAdapter): Option[CMD_FAIL_HTLC] = { + private def validateStandardPayment(nodeParams: NodeParams, add: UpdateAddHtlc, payload: FinalPayload.Standard, record: IncomingStandardPayment, receivedAt: TimestampMilli)(implicit log: LoggingAdapter): Option[CMD_FAIL_HTLC] = { // We send the same error regardless of the failure to avoid probing attacks. - val cmdFail = CMD_FAIL_HTLC(add.id, FailureReason.LocalFailure(IncorrectOrUnknownPaymentDetails(payload.totalAmount, nodeParams.currentBlockHeight)), commit = true) + val attribution = FailureAttributionData(htlcReceivedAt = receivedAt, trampolineReceivedAt_opt = None) + val cmdFail = CMD_FAIL_HTLC(add.id, FailureReason.LocalFailure(IncorrectOrUnknownPaymentDetails(payload.totalAmount, nodeParams.currentBlockHeight)), Some(attribution), commit = true) val commonOk = validateCommon(nodeParams, add, payload, record) val secretOk = validatePaymentSecret(add, payload, record.invoice) if (commonOk && secretOk) None else Some(cmdFail) } - private def validateBlindedPayment(nodeParams: NodeParams, add: UpdateAddHtlc, payload: FinalPayload.Blinded, record: IncomingBlindedPayment, maxRecipientPathFees: MilliSatoshi)(implicit log: LoggingAdapter): Option[CMD_FAIL_HTLC] = { + private def validateBlindedPayment(nodeParams: NodeParams, add: UpdateAddHtlc, payload: FinalPayload.Blinded, record: IncomingBlindedPayment, maxRecipientPathFees: MilliSatoshi, receivedAt: TimestampMilli)(implicit log: LoggingAdapter): Option[CMD_FAIL_HTLC] = { // We send the same error regardless of the failure to avoid probing attacks. - val cmdFail = CMD_FAIL_HTLC(add.id, FailureReason.LocalFailure(IncorrectOrUnknownPaymentDetails(payload.totalAmount, nodeParams.currentBlockHeight)), commit = true) + val attribution = FailureAttributionData(htlcReceivedAt = receivedAt, trampolineReceivedAt_opt = None) + val cmdFail = CMD_FAIL_HTLC(add.id, FailureReason.LocalFailure(IncorrectOrUnknownPaymentDetails(payload.totalAmount, nodeParams.currentBlockHeight)), Some(attribution), commit = true) val commonOk = validateCommon(nodeParams, add, payload, record) // The payer isn't aware of the blinded path fees if we decided to hide them. The HTLC amount will thus be smaller // than the onion amount, but should match when re-adding the blinded path fees. diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartPaymentFSM.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartPaymentFSM.scala index 6bccb65f41..5e8f96f21f 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartPaymentFSM.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartPaymentFSM.scala @@ -134,7 +134,7 @@ object MultiPartPaymentFSM { def totalAmount: MilliSatoshi } /** An incoming HTLC. */ - case class HtlcPart(totalAmount: MilliSatoshi, htlc: UpdateAddHtlc) extends PaymentPart { + case class HtlcPart(totalAmount: MilliSatoshi, htlc: UpdateAddHtlc, receivedAt: TimestampMilli) extends PaymentPart { override def paymentHash: ByteVector32 = htlc.paymentHash override def amount: MilliSatoshi = htlc.amountMsat } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/ChannelRelay.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/ChannelRelay.scala index 72dd79ea51..d4c1ae46c3 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/ChannelRelay.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/ChannelRelay.scala @@ -16,7 +16,7 @@ package fr.acinq.eclair.payment.relay -import akka.actor.ActorRef +import akka.actor.{ActorRef, typed} import akka.actor.typed.Behavior import akka.actor.typed.eventstream.EventStream import akka.actor.typed.scaladsl.adapter.TypedActorRefOps @@ -31,6 +31,8 @@ import fr.acinq.eclair.io.{Peer, PeerReadyNotifier} import fr.acinq.eclair.payment.Monitoring.{Metrics, Tags} import fr.acinq.eclair.payment.relay.Relayer.{OutgoingChannel, OutgoingChannelParams} import fr.acinq.eclair.payment.{ChannelPaymentRelayed, IncomingPaymentPacket} +import fr.acinq.eclair.reputation.Reputation +import fr.acinq.eclair.reputation.ReputationRecorder.GetConfidence import fr.acinq.eclair.wire.protocol.FailureMessageCodecs.createBadOnionFailure import fr.acinq.eclair.wire.protocol.PaymentOnion.IntermediatePayload import fr.acinq.eclair.wire.protocol._ @@ -38,7 +40,7 @@ import fr.acinq.eclair.{EncodedNodeId, Features, InitFeature, Logs, NodeParams, import java.util.UUID import java.util.concurrent.TimeUnit -import scala.concurrent.duration.DurationLong +import scala.concurrent.duration.{DurationLong, FiniteDuration} import scala.util.Random object ChannelRelay { @@ -47,6 +49,7 @@ object ChannelRelay { sealed trait Command private case object DoRelay extends Command private case class WrappedPeerReadyResult(result: PeerReadyNotifier.Result) extends Command + private case class WrappedReputationScore(score: Reputation.Score) extends Command private case class WrappedForwardFailure(failure: Register.ForwardFailure[CMD_ADD_HTLC]) extends Command private case class WrappedAddResponse(res: CommandResponse[CMD_ADD_HTLC]) extends Command private case class WrappedOnTheFlyFundingResponse(result: Peer.ProposeOnTheFlyFundingResponse) extends Command @@ -61,19 +64,29 @@ object ChannelRelay { def apply(nodeParams: NodeParams, register: ActorRef, + reputationRecorder_opt: Option[typed.ActorRef[GetConfidence]], channels: Map[ByteVector32, Relayer.OutgoingChannel], originNode: PublicKey, relayId: UUID, - r: IncomingPaymentPacket.ChannelRelayPacket): Behavior[Command] = + r: IncomingPaymentPacket.ChannelRelayPacket, + incomingChannelOccupancy: Double): Behavior[Command] = Behaviors.setup { context => Behaviors.withMdc(Logs.mdc( category_opt = Some(Logs.LogCategory.PAYMENT), parentPaymentId_opt = Some(relayId), // for a channel relay, parent payment id = relay id paymentHash_opt = Some(r.add.paymentHash), nodeAlias_opt = Some(nodeParams.alias))) { - val upstream = Upstream.Hot.Channel(r.add.removeUnknownTlvs(), TimestampMilli.now(), originNode) - val confidence = (r.add.endorsement + 0.5) / 8 - new ChannelRelay(nodeParams, register, channels, r, upstream, confidence, context).start() + val upstream = Upstream.Hot.Channel(r.add.removeUnknownTlvs(), r.receivedAt, originNode, incomingChannelOccupancy) + reputationRecorder_opt match { + case Some(reputationRecorder) => + reputationRecorder ! GetConfidence(context.messageAdapter(WrappedReputationScore(_)), upstream, channels.values.headOption.map(_.nextNodeId), r.relayFeeMsat, nodeParams.currentBlockHeight, r.outgoingCltv) + case None => + context.self ! WrappedReputationScore(Reputation.Score.fromEndorsement(r.add.endorsement)) + } + Behaviors.receiveMessagePartial { + case WrappedReputationScore(score) => + new ChannelRelay(nodeParams, register, channels, r, upstream, score, context).start() + } } } @@ -97,13 +110,14 @@ object ChannelRelay { } } - def translateRelayFailure(originHtlcId: Long, fail: HtlcResult.Fail): CMD_FAIL_HTLC = { + def translateRelayFailure(originHtlcId: Long, fail: HtlcResult.Fail, htlcReceivedAt_opt: Option[TimestampMilli]): CMD_FAIL_HTLC = { + val attribution_opt = htlcReceivedAt_opt.map(receivedAt => FailureAttributionData(htlcReceivedAt = receivedAt, trampolineReceivedAt_opt = None)) fail match { - case f: HtlcResult.RemoteFail => CMD_FAIL_HTLC(originHtlcId, FailureReason.EncryptedDownstreamFailure(f.fail.reason), commit = true) - case f: HtlcResult.RemoteFailMalformed => CMD_FAIL_HTLC(originHtlcId, FailureReason.LocalFailure(createBadOnionFailure(f.fail.onionHash, f.fail.failureCode)), commit = true) - case _: HtlcResult.OnChainFail => CMD_FAIL_HTLC(originHtlcId, FailureReason.LocalFailure(PermanentChannelFailure()), commit = true) - case HtlcResult.ChannelFailureBeforeSigned => CMD_FAIL_HTLC(originHtlcId, FailureReason.LocalFailure(PermanentChannelFailure()), commit = true) - case f: HtlcResult.DisconnectedBeforeSigned => CMD_FAIL_HTLC(originHtlcId, FailureReason.LocalFailure(TemporaryChannelFailure(Some(f.channelUpdate))), commit = true) + case f: HtlcResult.RemoteFail => CMD_FAIL_HTLC(originHtlcId, FailureReason.EncryptedDownstreamFailure(f.fail.reason, f.fail.attribution_opt), attribution_opt, commit = true) + case f: HtlcResult.RemoteFailMalformed => CMD_FAIL_HTLC(originHtlcId, FailureReason.LocalFailure(createBadOnionFailure(f.fail.onionHash, f.fail.failureCode)), attribution_opt, commit = true) + case _: HtlcResult.OnChainFail => CMD_FAIL_HTLC(originHtlcId, FailureReason.LocalFailure(PermanentChannelFailure()), attribution_opt, commit = true) + case HtlcResult.ChannelFailureBeforeSigned => CMD_FAIL_HTLC(originHtlcId, FailureReason.LocalFailure(PermanentChannelFailure()), attribution_opt, commit = true) + case f: HtlcResult.DisconnectedBeforeSigned => CMD_FAIL_HTLC(originHtlcId, FailureReason.LocalFailure(TemporaryChannelFailure(Some(f.channelUpdate))), attribution_opt, commit = true) } } @@ -117,7 +131,7 @@ class ChannelRelay private(nodeParams: NodeParams, channels: Map[ByteVector32, Relayer.OutgoingChannel], r: IncomingPaymentPacket.ChannelRelayPacket, upstream: Upstream.Hot.Channel, - confidence: Double, + reputationScore: Reputation.Score, context: ActorContext[ChannelRelay.Command]) { import ChannelRelay._ @@ -166,7 +180,7 @@ class ChannelRelay private(nodeParams: NodeParams, case WrappedPeerReadyResult(_: PeerReadyNotifier.PeerUnavailable) => Metrics.recordPaymentRelayFailed(Tags.FailureType.WakeUp, Tags.RelayType.Channel) context.log.info("rejecting htlc: failed to wake-up remote peer") - safeSendAndStop(r.add.channelId, CMD_FAIL_HTLC(r.add.id, FailureReason.LocalFailure(UnknownNextPeer()), commit = true)) + safeSendAndStop(r.add.channelId, makeCmdFailHtlc(r.add.id, UnknownNextPeer())) case WrappedPeerReadyResult(r: PeerReadyNotifier.PeerReady) => context.self ! DoRelay relay(Some(r.remoteFeatures), Seq.empty) @@ -203,7 +217,7 @@ class ChannelRelay private(nodeParams: NodeParams, Behaviors.receiveMessagePartial { case WrappedForwardFailure(Register.ForwardFailure(Register.Forward(_, channelId, _))) => context.log.warn(s"couldn't resolve downstream channel $channelId, failing htlc #${upstream.add.id}") - val cmdFail = CMD_FAIL_HTLC(upstream.add.id, FailureReason.LocalFailure(UnknownNextPeer()), commit = true) + val cmdFail = makeCmdFailHtlc(upstream.add.id, UnknownNextPeer()) Metrics.recordPaymentRelayFailed(Tags.FailureType(cmdFail), Tags.RelayType.Channel) safeSendAndStop(upstream.add.channelId, cmdFail) @@ -220,18 +234,23 @@ class ChannelRelay private(nodeParams: NodeParams, private def waitForAddSettled(): Behavior[Command] = Behaviors.receiveMessagePartial { case WrappedAddResponse(RES_ADD_SETTLED(_, htlc, fulfill: HtlcResult.Fulfill)) => - context.log.info("relaying fulfill to upstream, startedAt={}, endedAt={}, confidence={}, originNode={}, outgoingChannel={}", upstream.receivedAt, TimestampMilli.now(), confidence, upstream.receivedFrom, htlc.channelId) - Metrics.relayFulfill(confidence) - val cmd = CMD_FULFILL_HTLC(upstream.add.id, fulfill.paymentPreimage, commit = true) - context.system.eventStream ! EventStream.Publish(ChannelPaymentRelayed(upstream.amountIn, htlc.amountMsat, htlc.paymentHash, upstream.add.channelId, htlc.channelId, upstream.receivedAt, TimestampMilli.now())) + context.log.info("relaying fulfill to upstream, receivedAt={}, endedAt={}, confidence={}, originNode={}, outgoingChannel={}", upstream.receivedAt, r.receivedAt, reputationScore.incomingConfidence, upstream.receivedFrom, htlc.channelId) + Metrics.relayFulfill(reputationScore.incomingConfidence) + val downstreamAttribution_opt = fulfill match { + case HtlcResult.RemoteFulfill(fulfill) => fulfill.attribution_opt + case HtlcResult.OnChainFulfill(_) => None + } + val attribution = FulfillAttributionData(htlcReceivedAt = upstream.receivedAt, trampolineReceivedAt_opt = None, downstreamAttribution_opt = downstreamAttribution_opt) + val cmd = CMD_FULFILL_HTLC(upstream.add.id, fulfill.paymentPreimage, Some(attribution), commit = true) + context.system.eventStream ! EventStream.Publish(ChannelPaymentRelayed(upstream.amountIn, htlc.amountMsat, htlc.paymentHash, upstream.add.channelId, htlc.channelId, upstream.receivedAt, r.receivedAt)) recordRelayDuration(isSuccess = true) safeSendAndStop(upstream.add.channelId, cmd) case WrappedAddResponse(RES_ADD_SETTLED(_, htlc, fail: HtlcResult.Fail)) => - context.log.info("relaying fail to upstream, startedAt={}, endedAt={}, confidence={}, originNode={}, outgoingChannel={}", upstream.receivedAt, TimestampMilli.now(), confidence, upstream.receivedFrom, htlc.channelId) - Metrics.relayFail(confidence) + context.log.info("relaying fail to upstream, receivedAt={}, endedAt={}, confidence={}, originNode={}, outgoingChannel={}", upstream.receivedAt, r.receivedAt, reputationScore.incomingConfidence, upstream.receivedFrom, htlc.channelId) + Metrics.relayFail(reputationScore.incomingConfidence) Metrics.recordPaymentRelayFailed(Tags.FailureType.Remote, Tags.RelayType.Channel) - val cmd = translateRelayFailure(upstream.add.id, fail) + val cmd = translateRelayFailure(upstream.add.id, fail, Some(upstream.receivedAt)) recordRelayDuration(isSuccess = false) safeSendAndStop(upstream.add.channelId, cmd) } @@ -265,7 +284,7 @@ class ChannelRelay private(nodeParams: NodeParams, cmd match { // However, when the failure comes from us, we don't want to leak the unannounced channel by revealing // its channel_update: in that case, we always return a temporary node failure instead. - case cmd@CMD_FAIL_HTLC(_, FailureReason.LocalFailure(_: Update), _, _, _) => cmd.copy(reason = FailureReason.LocalFailure(TemporaryNodeFailure())) + case cmd@CMD_FAIL_HTLC(_, FailureReason.LocalFailure(_: Update), _, _, _, _) => cmd.copy(reason = FailureReason.LocalFailure(TemporaryNodeFailure())) case _ => cmd } case None => @@ -275,7 +294,7 @@ class ChannelRelay private(nodeParams: NodeParams, case Some(_) => // We are the introduction node: we add a delay to make it look like it could come from further downstream. val delay = Some(Random.nextLong(1000).millis) - CMD_FAIL_HTLC(cmd.id, FailureReason.LocalFailure(failure), delay, commit = true) + makeCmdFailHtlc(cmd.id, failure, delay) case None => // We are not the introduction node. CMD_FAIL_MALFORMED_HTLC(cmd.id, failure.onionHash, failure.code, commit = true) @@ -309,9 +328,9 @@ class ChannelRelay private(nodeParams: NodeParams, // Otherwise we return the error for the first channel tried. .getOrElse(previousFailures.head) .failure - CMD_FAIL_HTLC(r.add.id, FailureReason.LocalFailure(translateLocalError(error.t, error.channelUpdate)), commit = true) + makeCmdFailHtlc(r.add.id, translateLocalError(error.t, error.channelUpdate)) } else { - CMD_FAIL_HTLC(r.add.id, FailureReason.LocalFailure(UnknownNextPeer()), commit = true) + makeCmdFailHtlc(r.add.id, UnknownNextPeer()) } walletNodeId_opt match { case Some(walletNodeId) if shouldAttemptOnTheFlyFunding(remoteFeatures_opt, previousFailures) => RelayNeedsFunding(walletNodeId, cmdFail) @@ -342,7 +361,7 @@ class ChannelRelay private(nodeParams: NodeParams, channel.channelUpdate, relayResult match { case _: RelaySuccess => "success" - case RelayFailure(CMD_FAIL_HTLC(_, FailureReason.LocalFailure(failureReason), _, _, _)) => failureReason + case RelayFailure(CMD_FAIL_HTLC(_, FailureReason.LocalFailure(failureReason), _, _, _, _)) => failureReason case other => other }) (channel, relayResult) @@ -389,10 +408,10 @@ class ChannelRelay private(nodeParams: NodeParams, case Some(fail) => RelayFailure(fail) case None if !update.channelFlags.isEnabled => - RelayFailure(CMD_FAIL_HTLC(r.add.id, FailureReason.LocalFailure(ChannelDisabled(update.messageFlags, update.channelFlags, Some(update))), commit = true)) + RelayFailure(makeCmdFailHtlc(r.add.id, ChannelDisabled(update.messageFlags, update.channelFlags, Some(update)))) case None => val origin = Origin.Hot(addResponseAdapter.toClassic, upstream) - RelaySuccess(outgoingChannel.channelId, CMD_ADD_HTLC(addResponseAdapter.toClassic, r.amountToForward, r.add.paymentHash, r.outgoingCltv, r.nextPacket, nextPathKey_opt, confidence, fundingFee_opt = None, origin, commit = true)) + RelaySuccess(outgoingChannel.channelId, CMD_ADD_HTLC(addResponseAdapter.toClassic, r.amountToForward, r.add.paymentHash, r.outgoingCltv, r.nextPacket, nextPathKey_opt, reputationScore, fundingFee_opt = None, origin, commit = true)) } } @@ -405,11 +424,11 @@ class ChannelRelay private(nodeParams: NodeParams, val expiryDeltaOk = update.cltvExpiryDelta <= r.expiryDelta || prevUpdate_opt.exists(_.cltvExpiryDelta <= r.expiryDelta) val feesOk = nodeFee(update.relayFees, r.amountToForward) <= r.relayFeeMsat || prevUpdate_opt.exists(u => nodeFee(u.relayFees, r.amountToForward) <= r.relayFeeMsat) if (!htlcMinimumOk) { - Some(CMD_FAIL_HTLC(r.add.id, FailureReason.LocalFailure(AmountBelowMinimum(r.amountToForward, Some(update))), commit = true)) + Some(makeCmdFailHtlc(r.add.id, AmountBelowMinimum(r.amountToForward, Some(update)))) } else if (!expiryDeltaOk) { - Some(CMD_FAIL_HTLC(r.add.id, FailureReason.LocalFailure(IncorrectCltvExpiry(r.outgoingCltv, Some(update))), commit = true)) + Some(makeCmdFailHtlc(r.add.id, IncorrectCltvExpiry(r.outgoingCltv, Some(update)))) } else if (!feesOk) { - Some(CMD_FAIL_HTLC(r.add.id, FailureReason.LocalFailure(FeeInsufficient(r.add.amountMsat, Some(update))), commit = true)) + Some(makeCmdFailHtlc(r.add.id, FeeInsufficient(r.add.amountMsat, Some(update)))) } else { None } @@ -429,6 +448,11 @@ class ChannelRelay private(nodeParams: NodeParams, featureOk && liquidityIssue && relayParamsOk } + private def makeCmdFailHtlc(originHtlcId: Long, failure: FailureMessage, delay_opt: Option[FiniteDuration] = None): CMD_FAIL_HTLC = { + val attribution = FailureAttributionData(htlcReceivedAt = upstream.receivedAt, trampolineReceivedAt_opt = None) + CMD_FAIL_HTLC(originHtlcId, FailureReason.LocalFailure(failure), Some(attribution), delay_opt, commit = true) + } + private def recordRelayDuration(isSuccess: Boolean): Unit = Metrics.RelayedPaymentDuration .withTag(Tags.Relay, Tags.RelayType.Channel) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/ChannelRelayer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/ChannelRelayer.scala index 53b94e4ae3..ed25d79b6f 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/ChannelRelayer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/ChannelRelayer.scala @@ -16,14 +16,15 @@ package fr.acinq.eclair.payment.relay -import akka.actor.ActorRef import akka.actor.typed.Behavior import akka.actor.typed.eventstream.EventStream import akka.actor.typed.scaladsl.Behaviors +import akka.actor.{ActorRef, typed} import fr.acinq.bitcoin.scalacompat.ByteVector32 import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.eclair.channel._ import fr.acinq.eclair.payment.IncomingPaymentPacket +import fr.acinq.eclair.reputation.ReputationRecorder import fr.acinq.eclair.{Logs, NodeParams, ShortChannelId, SubscriptionsComplete} import java.util.UUID @@ -42,7 +43,7 @@ object ChannelRelayer { // @formatter:off sealed trait Command case class GetOutgoingChannels(replyTo: ActorRef, getOutgoingChannels: Relayer.GetOutgoingChannels) extends Command - case class Relay(channelRelayPacket: IncomingPaymentPacket.ChannelRelayPacket, originNode: PublicKey) extends Command + case class Relay(channelRelayPacket: IncomingPaymentPacket.ChannelRelayPacket, originNode: PublicKey, incomingChannelOccupancy: Double) extends Command private[payment] case class WrappedLocalChannelUpdate(localChannelUpdate: LocalChannelUpdate) extends Command private[payment] case class WrappedLocalChannelDown(localChannelDown: LocalChannelDown) extends Command private[payment] case class WrappedAvailableBalanceChanged(availableBalanceChanged: AvailableBalanceChanged) extends Command @@ -58,6 +59,7 @@ object ChannelRelayer { def apply(nodeParams: NodeParams, register: ActorRef, + reputationRecorder_opt: Option[typed.ActorRef[ReputationRecorder.GetConfidence]], channels: Map[ByteVector32, Relayer.OutgoingChannel] = Map.empty, scid2channels: Map[ShortChannelId, ByteVector32] = Map.empty, node2channels: mutable.MultiDict[PublicKey, ByteVector32] = mutable.MultiDict.empty): Behavior[Command] = @@ -68,7 +70,7 @@ object ChannelRelayer { context.system.eventStream ! EventStream.Publish(SubscriptionsComplete(this.getClass)) Behaviors.withMdc(Logs.mdc(category_opt = Some(Logs.LogCategory.PAYMENT), nodeAlias_opt = Some(nodeParams.alias)), mdc) { Behaviors.receiveMessage { - case Relay(channelRelayPacket, originNode) => + case Relay(channelRelayPacket, originNode, incomingChannelOccupancy) => val relayId = UUID.randomUUID() val nextNodeId_opt: Option[PublicKey] = channelRelayPacket.payload.outgoing match { case Left(outgoingNodeId) => Some(outgoingNodeId.publicKey) @@ -82,7 +84,7 @@ object ChannelRelayer { case None => Map.empty } context.log.debug(s"spawning a new handler with relayId=$relayId to nextNodeId={} with channels={}", nextNodeId_opt.getOrElse(""), nextChannels.keys.mkString(",")) - context.spawn(ChannelRelay.apply(nodeParams, register, nextChannels, originNode, relayId, channelRelayPacket), name = relayId.toString) + context.spawn(ChannelRelay.apply(nodeParams, register, reputationRecorder_opt, nextChannels, originNode, relayId, channelRelayPacket, incomingChannelOccupancy), name = relayId.toString) Behaviors.same case GetOutgoingChannels(replyTo, Relayer.GetOutgoingChannels(enabledOnly)) => @@ -103,14 +105,14 @@ object ChannelRelayer { context.log.debug("adding mappings={} to channelId={}", mappings.keys.mkString(","), channelId) val scid2channels1 = scid2channels ++ mappings val node2channels1 = node2channels.addOne(remoteNodeId, channelId) - apply(nodeParams, register, channels1, scid2channels1, node2channels1) + apply(nodeParams, register, reputationRecorder_opt, channels1, scid2channels1, node2channels1) case WrappedLocalChannelDown(LocalChannelDown(_, channelId, realScids, aliases, remoteNodeId)) => context.log.debug("removed local channel info for channelId={} localAlias={}", channelId, aliases.localAlias) val channels1 = channels - channelId val scid2Channels1 = scid2channels - aliases.localAlias -- realScids val node2channels1 = node2channels.subtractOne(remoteNodeId, channelId) - apply(nodeParams, register, channels1, scid2Channels1, node2channels1) + apply(nodeParams, register, reputationRecorder_opt, channels1, scid2Channels1, node2channels1) case WrappedAvailableBalanceChanged(AvailableBalanceChanged(_, channelId, aliases, commitments, _)) => val channels1 = channels.get(channelId) match { @@ -119,7 +121,7 @@ object ChannelRelayer { channels + (channelId -> c.copy(commitments = commitments)) case None => channels // we only consider the balance if we have the channel_update } - apply(nodeParams, register, channels1, scid2channels, node2channels) + apply(nodeParams, register, reputationRecorder_opt, channels1, scid2channels, node2channels) } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/NodeRelay.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/NodeRelay.scala index b976970999..07fb03c030 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/NodeRelay.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/NodeRelay.scala @@ -37,11 +37,14 @@ import fr.acinq.eclair.payment.send.MultiPartPaymentLifecycle.{PreimageReceived, import fr.acinq.eclair.payment.send.PaymentInitiator.SendPaymentConfig import fr.acinq.eclair.payment.send.PaymentLifecycle.SendPaymentToNode import fr.acinq.eclair.payment.send._ +import fr.acinq.eclair.reputation.Reputation +import fr.acinq.eclair.reputation.ReputationRecorder.GetConfidence import fr.acinq.eclair.router.Router.{ChannelHop, HopRelayParams, Route, RouteParams} import fr.acinq.eclair.router.{BalanceTooLow, RouteNotFound} import fr.acinq.eclair.wire.protocol.PaymentOnion.IntermediatePayload import fr.acinq.eclair.wire.protocol._ import fr.acinq.eclair.{Alias, CltvExpiry, CltvExpiryDelta, EncodedNodeId, Features, InitFeature, Logs, MilliSatoshi, MilliSatoshiLong, NodeParams, TimestampMilli, UInt64, nodeFee, randomBytes32} +import scodec.bits.ByteVector import java.util.UUID import java.util.concurrent.TimeUnit @@ -55,7 +58,7 @@ object NodeRelay { // @formatter:off sealed trait Command - case class Relay(nodeRelayPacket: IncomingPaymentPacket.NodeRelayPacket, originNode: PublicKey) extends Command + case class Relay(nodeRelayPacket: IncomingPaymentPacket.NodeRelayPacket, originNode: PublicKey, incomingChannelOccupancy: Double) extends Command case object Stop extends Command private case class WrappedMultiPartExtraPaymentReceived(mppExtraReceived: MultiPartPaymentFSM.ExtraPaymentReceived[HtlcPart]) extends Command private case class WrappedMultiPartPaymentFailed(mppFailed: MultiPartPaymentFSM.MultiPartPaymentFailed) extends Command @@ -73,14 +76,14 @@ object NodeRelay { def spawnOutgoingPayFSM(context: ActorContext[NodeRelay.Command], cfg: SendPaymentConfig, multiPart: Boolean): ActorRef } - case class SimpleOutgoingPaymentFactory(nodeParams: NodeParams, router: ActorRef, register: ActorRef) extends OutgoingPaymentFactory { + case class SimpleOutgoingPaymentFactory(nodeParams: NodeParams, router: ActorRef, register: ActorRef, reputationRecorder_opt: Option[typed.ActorRef[GetConfidence]]) extends OutgoingPaymentFactory { val paymentFactory: PaymentInitiator.SimplePaymentFactory = PaymentInitiator.SimplePaymentFactory(nodeParams, router, register) override def spawnOutgoingPayFSM(context: ActorContext[Command], cfg: SendPaymentConfig, multiPart: Boolean): ActorRef = { if (multiPart) { context.toClassic.actorOf(MultiPartPaymentLifecycle.props(nodeParams, cfg, publishPreimage = true, router, paymentFactory)) } else { - context.toClassic.actorOf(PaymentLifecycle.props(nodeParams, cfg, router, register)) + context.toClassic.actorOf(PaymentLifecycle.props(nodeParams, cfg, router, register, reputationRecorder_opt)) } } } @@ -107,7 +110,7 @@ object NodeRelay { }.toClassic val incomingPaymentHandler = context.actorOf(MultiPartPaymentFSM.props(nodeParams, paymentHash, totalAmountIn, mppFsmAdapters)) val nextPacket_opt = nodeRelayPacket match { - case IncomingPaymentPacket.RelayToTrampolinePacket(_, _, _, nextPacket) => Some(nextPacket) + case IncomingPaymentPacket.RelayToTrampolinePacket(_, _, _, nextPacket, _) => Some(nextPacket) case _: IncomingPaymentPacket.RelayToNonTrampolinePacket => None case _: IncomingPaymentPacket.RelayToBlindedPathsPacket => None } @@ -220,15 +223,15 @@ class NodeRelay private(nodeParams: NodeParams, */ private def receiving(htlcs: Queue[Upstream.Hot.Channel], nextPayload: IntermediatePayload.NodeRelay, nextPacket_opt: Option[OnionRoutingPacket], handler: ActorRef): Behavior[Command] = Behaviors.receiveMessagePartial { - case Relay(packet: IncomingPaymentPacket.NodeRelayPacket, originNode) => + case Relay(packet: IncomingPaymentPacket.NodeRelayPacket, originNode, incomingChannelOccupancy) => require(packet.outerPayload.paymentSecret == paymentSecret, "payment secret mismatch") context.log.debug("forwarding incoming htlc #{} from channel {} to the payment FSM", packet.add.id, packet.add.channelId) - handler ! MultiPartPaymentFSM.HtlcPart(packet.outerPayload.totalAmount, packet.add) - receiving(htlcs :+ Upstream.Hot.Channel(packet.add.removeUnknownTlvs(), TimestampMilli.now(), originNode), nextPayload, nextPacket_opt, handler) + handler ! MultiPartPaymentFSM.HtlcPart(packet.outerPayload.totalAmount, packet.add, packet.receivedAt) + receiving(htlcs :+ Upstream.Hot.Channel(packet.add.removeUnknownTlvs(), packet.receivedAt, originNode, incomingChannelOccupancy), nextPayload, nextPacket_opt, handler) case WrappedMultiPartPaymentFailed(MultiPartPaymentFSM.MultiPartPaymentFailed(_, failure, parts)) => context.log.warn("could not complete incoming multi-part payment (parts={} paidAmount={} failure={})", parts.size, parts.map(_.amount).sum, failure) Metrics.recordPaymentRelayFailed(failure.getClass.getSimpleName, Tags.RelayType.Trampoline) - parts.collect { case p: MultiPartPaymentFSM.HtlcPart => rejectHtlc(p.htlc.id, p.htlc.channelId, p.amount, Some(failure)) } + parts.collect { case p: MultiPartPaymentFSM.HtlcPart => rejectHtlc(p.htlc.id, p.htlc.channelId, p.amount, p.receivedAt, None, Some(failure)) } stopping() case WrappedMultiPartPaymentSucceeded(MultiPartPaymentFSM.MultiPartPaymentSucceeded(_, parts)) => context.log.info("completed incoming multi-part payment with parts={} paidAmount={}", parts.size, parts.map(_.amount).sum) @@ -335,10 +338,9 @@ class NodeRelay private(nodeParams: NodeParams, val amountOut = outgoingAmount(upstream, payloadOut) val expiryOut = outgoingExpiry(upstream, payloadOut) context.log.debug("relaying trampoline payment (amountIn={} expiryIn={} amountOut={} expiryOut={} isWallet={})", upstream.amountIn, upstream.expiryIn, amountOut, expiryOut, walletNodeId_opt.isDefined) - val confidence = (upstream.received.map(_.add.endorsement).min + 0.5) / 8 // We only make one try when it's a direct payment to a wallet. val maxPaymentAttempts = if (walletNodeId_opt.isDefined) 1 else nodeParams.maxPaymentAttempts - val paymentCfg = SendPaymentConfig(relayId, relayId, None, paymentHash, recipient.nodeId, upstream, None, None, storeInDb = false, publishEvent = false, recordPathFindingMetrics = true, confidence) + val paymentCfg = SendPaymentConfig(relayId, relayId, None, paymentHash, recipient.nodeId, upstream, None, None, storeInDb = false, publishEvent = false, recordPathFindingMetrics = true) val routeParams = computeRouteParams(nodeParams, upstream.amountIn, upstream.expiryIn, amountOut, expiryOut) // If the next node is using trampoline, we assume that they support MPP. val useMultiPart = recipient.features.hasFeature(Features.BasicMultiPartPayment) || packetOut_opt.nonEmpty @@ -374,11 +376,11 @@ class NodeRelay private(nodeParams: NodeParams, Behaviors.receiveMessagePartial { rejectExtraHtlcPartialFunction orElse { // this is the fulfill that arrives from downstream channels - case WrappedPreimageReceived(PreimageReceived(_, paymentPreimage)) => + case WrappedPreimageReceived(PreimageReceived(_, paymentPreimage, attribution_opt)) => if (!fulfilledUpstream) { // We want to fulfill upstream as soon as we receive the preimage (even if not all HTLCs have fulfilled downstream). context.log.debug("got preimage from downstream") - fulfillPayment(upstream, paymentPreimage) + fulfillPayment(upstream, paymentPreimage, attribution_opt) sending(upstream, recipient, walletNodeId_opt, recipientFeatures_opt, nextPayload, startedAt, fulfilledUpstream = true) } else { // we don't want to fulfill multiple times @@ -419,7 +421,7 @@ class NodeRelay private(nodeParams: NodeParams, case r: BlindedRecipient => r.blindedHops.headOption } val dummyRoute = Route(amountOut, Seq(dummyHop), finalHop_opt) - OutgoingPaymentPacket.buildOutgoingPayment(Origin.Hot(ActorRef.noSender, upstream), paymentHash, dummyRoute, recipient, 1.0) match { + OutgoingPaymentPacket.buildOutgoingPayment(Origin.Hot(ActorRef.noSender, upstream), paymentHash, dummyRoute, recipient, Reputation.Score.max) match { case Left(f) => context.log.warn("could not create payment onion for on-the-fly funding: {}", f.getMessage) rejectPayment(upstream, translateError(nodeParams, failures, upstream, nextPayload)) @@ -463,52 +465,54 @@ class NodeRelay private(nodeParams: NodeParams, } private def rejectExtraHtlcPartialFunction: PartialFunction[Command, Behavior[Command]] = { - case Relay(nodeRelayPacket, _) => - rejectExtraHtlc(nodeRelayPacket.add) + case Relay(nodeRelayPacket, _, _) => + rejectExtraHtlc(nodeRelayPacket.add, nodeRelayPacket.receivedAt) Behaviors.same // NB: this message would be sent from the payment FSM which we stopped before going to this state, but all this is asynchronous. // We always fail extraneous HTLCs. They are a spec violation from the sender, but harmless in the relay case. // By failing them fast (before the payment has reached the final recipient) there's a good chance the sender won't lose any money. // We don't expect to relay pay-to-open payments. case WrappedMultiPartExtraPaymentReceived(extraPaymentReceived) => - rejectExtraHtlc(extraPaymentReceived.payment.htlc) + rejectExtraHtlc(extraPaymentReceived.payment.htlc, extraPaymentReceived.payment.receivedAt) Behaviors.same } - private def rejectExtraHtlc(add: UpdateAddHtlc): Unit = { + private def rejectExtraHtlc(add: UpdateAddHtlc, htlcReceivedAt: TimestampMilli): Unit = { context.log.warn("rejecting extra htlc #{} from channel {}", add.id, add.channelId) - rejectHtlc(add.id, add.channelId, add.amountMsat) + rejectHtlc(add.id, add.channelId, add.amountMsat, htlcReceivedAt, trampolineReceivedAt_opt = None) } - private def rejectHtlc(htlcId: Long, channelId: ByteVector32, amount: MilliSatoshi, failure: Option[FailureMessage] = None): Unit = { + private def rejectHtlc(htlcId: Long, channelId: ByteVector32, amount: MilliSatoshi, htlcReceivedAt: TimestampMilli, trampolineReceivedAt_opt: Option[TimestampMilli], failure: Option[FailureMessage] = None): Unit = { val failureMessage = failure.getOrElse(IncorrectOrUnknownPaymentDetails(amount, nodeParams.currentBlockHeight)) - val cmd = CMD_FAIL_HTLC(htlcId, FailureReason.LocalFailure(failureMessage), commit = true) + val attribution = FailureAttributionData(htlcReceivedAt, trampolineReceivedAt_opt) + val cmd = CMD_FAIL_HTLC(htlcId, FailureReason.LocalFailure(failureMessage), Some(attribution), commit = true) PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, channelId, cmd) } private def rejectPayment(upstream: Upstream.Hot.Trampoline, failure: Option[FailureMessage]): Unit = { Metrics.recordPaymentRelayFailed(failure.map(_.getClass.getSimpleName).getOrElse("Unknown"), Tags.RelayType.Trampoline) - upstream.received.foreach(r => rejectHtlc(r.add.id, r.add.channelId, upstream.amountIn, failure)) + upstream.received.foreach(r => rejectHtlc(r.add.id, r.add.channelId, upstream.amountIn, r.receivedAt, Some(upstream.receivedAt), failure)) } - private def fulfillPayment(upstream: Upstream.Hot.Trampoline, paymentPreimage: ByteVector32): Unit = upstream.received.foreach(r => { - val cmd = CMD_FULFILL_HTLC(r.add.id, paymentPreimage, commit = true) + private def fulfillPayment(upstream: Upstream.Hot.Trampoline, paymentPreimage: ByteVector32, downstreamAttribution_opt: Option[ByteVector]): Unit = upstream.received.foreach(r => { + val attribution = FulfillAttributionData(r.receivedAt, Some(upstream.receivedAt), downstreamAttribution_opt) + val cmd = CMD_FULFILL_HTLC(r.add.id, paymentPreimage, Some(attribution), commit = true) PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, r.add.channelId, cmd) }) private def success(upstream: Upstream.Hot.Trampoline, fulfilledUpstream: Boolean, paymentSent: PaymentSent): Unit = { // We may have already fulfilled upstream, but we can now emit an accurate relayed event and clean-up resources. if (!fulfilledUpstream) { - fulfillPayment(upstream, paymentSent.paymentPreimage) + fulfillPayment(upstream, paymentSent.paymentPreimage, paymentSent.remainingAttribution_opt) } val incoming = upstream.received.map(r => PaymentRelayed.IncomingPart(r.add.amountMsat, r.add.channelId, r.receivedAt)) val outgoing = paymentSent.parts.map(part => PaymentRelayed.OutgoingPart(part.amountWithFees, part.toChannelId, part.timestamp)) context.system.eventStream ! EventStream.Publish(TrampolinePaymentRelayed(paymentHash, incoming, outgoing, paymentSent.recipientNodeId, paymentSent.recipientAmount)) } - private def recordRelayDuration(startedAt: TimestampMilli, isSuccess: Boolean): Unit = + private def recordRelayDuration(receivedAt: TimestampMilli, isSuccess: Boolean): Unit = Metrics.RelayedPaymentDuration .withTag(Tags.Relay, Tags.RelayType.Trampoline) .withTag(Tags.Success, isSuccess) - .record((TimestampMilli.now() - startedAt).toMillis, TimeUnit.MILLISECONDS) + .record((TimestampMilli.now() - receivedAt).toMillis, TimeUnit.MILLISECONDS) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/NodeRelayer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/NodeRelayer.scala index 75bb545c89..82857c544f 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/NodeRelayer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/NodeRelayer.scala @@ -38,7 +38,7 @@ object NodeRelayer { // @formatter:off sealed trait Command - case class Relay(nodeRelayPacket: IncomingPaymentPacket.NodeRelayPacket, originNode: PublicKey) extends Command + case class Relay(nodeRelayPacket: IncomingPaymentPacket.NodeRelayPacket, originNode: PublicKey, incomingChannelOccupancy: Double) extends Command case class RelayComplete(childHandler: ActorRef[NodeRelay.Command], paymentHash: ByteVector32, paymentSecret: ByteVector32) extends Command private[relay] case class GetPendingPayments(replyTo: akka.actor.ActorRef) extends Command // @formatter:on @@ -61,20 +61,20 @@ object NodeRelayer { Behaviors.setup { context => Behaviors.withMdc(Logs.mdc(category_opt = Some(Logs.LogCategory.PAYMENT)), mdc) { Behaviors.receiveMessage { - case Relay(nodeRelayPacket, originNode) => + case Relay(nodeRelayPacket, originNode, incomingChannelOccupancy) => val htlcIn = nodeRelayPacket.add val childKey = PaymentKey(htlcIn.paymentHash, nodeRelayPacket.outerPayload.paymentSecret) children.get(childKey) match { case Some(handler) => context.log.debug("forwarding incoming htlc #{} from channel {} to existing handler", htlcIn.id, htlcIn.channelId) - handler ! NodeRelay.Relay(nodeRelayPacket, originNode) + handler ! NodeRelay.Relay(nodeRelayPacket, originNode, incomingChannelOccupancy) Behaviors.same case None => val relayId = UUID.randomUUID() context.log.debug(s"spawning a new handler with relayId=$relayId") val handler = context.spawn(NodeRelay.apply(nodeParams, context.self, register, relayId, nodeRelayPacket, outgoingPaymentFactory, router), relayId.toString) context.log.debug("forwarding incoming htlc #{} from channel {} to new handler", htlcIn.id, htlcIn.channelId) - handler ! NodeRelay.Relay(nodeRelayPacket, originNode) + handler ! NodeRelay.Relay(nodeRelayPacket, originNode, incomingChannelOccupancy) apply(nodeParams, register, outgoingPaymentFactory, router, children + (childKey -> handler)) } case RelayComplete(childHandler, paymentHash, paymentSecret) => diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/OnTheFlyFunding.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/OnTheFlyFunding.scala index 5b1b308f8c..ed791b7f00 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/OnTheFlyFunding.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/OnTheFlyFunding.scala @@ -27,6 +27,7 @@ import fr.acinq.eclair.blockchain.fee.FeeratePerKw import fr.acinq.eclair.channel._ import fr.acinq.eclair.crypto.Sphinx import fr.acinq.eclair.payment.Monitoring.Metrics +import fr.acinq.eclair.reputation.Reputation import fr.acinq.eclair.wire.protocol.LiquidityAds.PaymentDetails import fr.acinq.eclair.wire.protocol._ import fr.acinq.eclair.{Logs, MilliSatoshi, MilliSatoshiLong, NodeParams, TimestampMilli, ToMilliSatoshiConversion} @@ -90,7 +91,7 @@ object OnTheFlyFunding { /** An on-the-fly funding proposal sent to our peer. */ case class Proposal(htlc: WillAddHtlc, upstream: Upstream.Hot, onionSharedSecrets: Seq[Sphinx.SharedSecret]) { /** Maximum fees that can be collected from this HTLC. */ - def maxFees(htlcMinimum: MilliSatoshi): MilliSatoshi = htlc.amount - htlcMinimum + def maxFees(htlcMinimum: MilliSatoshi): MilliSatoshi = (htlc.amount - htlcMinimum).max(0 msat) /** Create commands to fail all upstream HTLCs. */ def createFailureCommands(failure_opt: Option[FailureReason])(implicit log: LoggingAdapter): Seq[(ByteVector32, CMD_FAIL_HTLC)] = upstream match { @@ -100,15 +101,16 @@ object OnTheFlyFunding { // That's because we are directly connected to the wallet: the blinded path doesn't contain any other public nodes, // so we don't need to protect against probing. This allows us to return a more meaningful failure to the payer. val failure = failure_opt.getOrElse(FailureReason.LocalFailure(UnknownNextPeer())) - Seq(u.add.channelId -> CMD_FAIL_HTLC(u.add.id, failure, commit = true)) + val attribution = FailureAttributionData(htlcReceivedAt = u.receivedAt, trampolineReceivedAt_opt = None) + Seq(u.add.channelId -> CMD_FAIL_HTLC(u.add.id, failure, Some(attribution), commit = true)) case u: Upstream.Hot.Trampoline => val failure = failure_opt match { case Some(f) => f match { case f: FailureReason.EncryptedDownstreamFailure => // In the trampoline case, we currently ignore downstream failures: we should add dedicated failures to // the BOLTs to better handle those cases. - Sphinx.FailurePacket.decrypt(f.packet, onionSharedSecrets) match { - case Left(Sphinx.CannotDecryptFailurePacket(_)) => + Sphinx.FailurePacket.decrypt(f.packet, f.attribution_opt, onionSharedSecrets).failure match { + case Left(Sphinx.CannotDecryptFailurePacket(_, _)) => log.warning("couldn't decrypt downstream on-the-fly funding failure") case Right(f) => log.warning("downstream on-the-fly funding failure: {}", f.failureMessage.message) @@ -118,14 +120,20 @@ object OnTheFlyFunding { } case None => FailureReason.LocalFailure(UnknownNextPeer()) } - u.received.map(_.add).map(add => add.channelId -> CMD_FAIL_HTLC(add.id, failure, commit = true)) + u.received.map(c => { + val attribution = FailureAttributionData(htlcReceivedAt = c.receivedAt, trampolineReceivedAt_opt = Some(u.receivedAt)) + c.add.channelId -> CMD_FAIL_HTLC(c.add.id, failure, Some(attribution), commit = true) + }) } /** Create commands to fulfill all upstream HTLCs. */ def createFulfillCommands(preimage: ByteVector32): Seq[(ByteVector32, CMD_FULFILL_HTLC)] = upstream match { case _: Upstream.Local => Nil - case u: Upstream.Hot.Channel => Seq(u.add.channelId -> CMD_FULFILL_HTLC(u.add.id, preimage, commit = true)) - case u: Upstream.Hot.Trampoline => u.received.map(_.add).map(add => add.channelId -> CMD_FULFILL_HTLC(add.id, preimage, commit = true)) + case u: Upstream.Hot.Channel => Seq(u.add.channelId -> CMD_FULFILL_HTLC(u.add.id, preimage, Some(FulfillAttributionData(htlcReceivedAt = u.receivedAt, trampolineReceivedAt_opt = None, downstreamAttribution_opt = None)), commit = true)) + case u: Upstream.Hot.Trampoline => u.received.map(c => { + val attribution = FulfillAttributionData(htlcReceivedAt = c.receivedAt, trampolineReceivedAt_opt = Some(u.receivedAt), downstreamAttribution_opt = None) + c.add.channelId -> CMD_FULFILL_HTLC(c.add.id, preimage, Some(attribution), commit = true) + }) } } @@ -293,7 +301,7 @@ object OnTheFlyFunding { private def relay(data: DATA_NORMAL): Behavior[Command] = { context.log.debug("relaying {} on-the-fly HTLCs that have been funded", cmd.proposed.size) - val htlcMinimum = data.commitments.params.remoteParams.htlcMinimum + val htlcMinimum = data.commitments.latest.remoteCommitParams.htlcMinimum val cmdAdapter = context.messageAdapter[CommandResponse[CMD_ADD_HTLC]](WrappedCommandResponse) val htlcSettledAdapter = context.messageAdapter[RES_ADD_SETTLED[Origin.Hot, HtlcResult]](WrappedHtlcSettled) cmd.proposed.foldLeft(cmd.status.remainingFees) { @@ -302,7 +310,7 @@ object OnTheFlyFunding { // This lets us detect that this HTLC is an on-the-fly funded HTLC. val htlcFees = LiquidityAds.FundingFee(remainingFees.min(p.maxFees(htlcMinimum)), cmd.status.txId) val origin = Origin.Hot(htlcSettledAdapter.toClassic, p.upstream) - val add = CMD_ADD_HTLC(cmdAdapter.toClassic, p.htlc.amount - htlcFees.amount, paymentHash, p.htlc.expiry, p.htlc.finalPacket, p.htlc.pathKey_opt, 1.0, Some(htlcFees), origin, commit = true) + val add = CMD_ADD_HTLC(cmdAdapter.toClassic, p.htlc.amount - htlcFees.amount, paymentHash, p.htlc.expiry, p.htlc.finalPacket, p.htlc.pathKey_opt, Reputation.Score.max, Some(htlcFees), origin, commit = true) cmd.channel ! add remainingFees - htlcFees.amount } @@ -355,13 +363,17 @@ object OnTheFlyFunding { import scodec.codecs._ private val upstreamLocal: Codec[Upstream.Local] = uuid.as[Upstream.Local] - private val upstreamChannel: Codec[Upstream.Hot.Channel] = (lengthDelimited(updateAddHtlcCodec) :: uint64overflow.as[TimestampMilli] :: publicKey).as[Upstream.Hot.Channel] + private val upstreamChannel: Codec[Upstream.Hot.Channel] = (lengthDelimited(updateAddHtlcCodec) :: uint64overflow.as[TimestampMilli] :: publicKey :: double).as[Upstream.Hot.Channel] private val upstreamTrampoline: Codec[Upstream.Hot.Trampoline] = listOfN(uint16, upstreamChannel).as[Upstream.Hot.Trampoline] + private val legacyUpstreamChannel: Codec[Upstream.Hot.Channel] = (lengthDelimited(updateAddHtlcCodec) :: uint64overflow.as[TimestampMilli] :: publicKey :: provide(0.0)).as[Upstream.Hot.Channel] + private val legacyUpstreamTrampoline: Codec[Upstream.Hot.Trampoline] = listOfN(uint16, legacyUpstreamChannel).as[Upstream.Hot.Trampoline] val upstream: Codec[Upstream.Hot] = discriminated[Upstream.Hot].by(uint16) .typecase(0x00, upstreamLocal) - .typecase(0x01, upstreamChannel) - .typecase(0x02, upstreamTrampoline) + .typecase(0x03, upstreamChannel) + .typecase(0x04, upstreamTrampoline) + .typecase(0x01, legacyUpstreamChannel) + .typecase(0x02, legacyUpstreamTrampoline) val proposal: Codec[Proposal] = ( ("willAddHtlc" | lengthDelimited(willAddHtlcCodec)) :: diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/PostRestartHtlcCleaner.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/PostRestartHtlcCleaner.scala index b8f9c5810f..8c51b41677 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/PostRestartHtlcCleaner.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/PostRestartHtlcCleaner.scala @@ -30,6 +30,7 @@ import fr.acinq.eclair.payment.{ChannelPaymentRelayed, IncomingPaymentPacket, Pa import fr.acinq.eclair.transactions.DirectedHtlc.outgoing import fr.acinq.eclair.wire.protocol._ import fr.acinq.eclair.{CustomCommitmentsPlugin, Feature, Features, Logs, MilliSatoshiLong, NodeParams, TimestampMilli} +import kamon.metric.{Gauge, Metric} import scala.concurrent.Promise import scala.util.Try @@ -72,7 +73,7 @@ class PostRestartHtlcCleaner(nodeParams: NodeParams, register: ActorRef, initial val nonStandardIncomingHtlcs: Seq[IncomingHtlc] = nodeParams.pluginParams.collect { case p: CustomCommitmentsPlugin => p.getIncomingHtlcs(nodeParams, log) }.flatten val htlcsIn: Seq[IncomingHtlc] = getIncomingHtlcs(channels, nodeParams.db.payments, nodeParams.privateKey, nodeParams.features) ++ nonStandardIncomingHtlcs val nonStandardRelayedOutHtlcs: Map[Origin.Cold, Set[(ByteVector32, Long)]] = nodeParams.pluginParams.collect { case p: CustomCommitmentsPlugin => p.getHtlcsRelayedOut(htlcsIn, nodeParams, log) }.flatten.toMap - val relayedOut: Map[Origin.Cold, Set[(ByteVector32, Long)]] = getHtlcsRelayedOut(channels, htlcsIn) ++ nonStandardRelayedOutHtlcs + val relayedOut: Map[Origin.Cold, Set[(ByteVector32, Long)]] = getHtlcsRelayedOut(nodeParams, channels, htlcsIn) ++ nonStandardRelayedOutHtlcs val settledHtlcs: Set[(ByteVector32, Long)] = nodeParams.db.pendingCommands.listSettlementCommands().map { case (channelId, cmd) => (channelId, cmd.id) }.toSet val notRelayed = htlcsIn.filterNot(htlcIn => { @@ -117,7 +118,7 @@ class PostRestartHtlcCleaner(nodeParams: NodeParams, register: ActorRef, initial Metrics.Resolved.withTag(Tags.Success, value = true).withTag(Metrics.Relayed, value = false).increment() if (e.currentState != CLOSED) { log.info(s"fulfilling broken htlc=$htlc") - channel ! CMD_FULFILL_HTLC(htlc.id, preimage, commit = true) + channel ! CMD_FULFILL_HTLC(htlc.id, preimage, None, commit = true) } else { log.info(s"got preimage but upstream channel is closed for htlc=$htlc") } @@ -136,7 +137,7 @@ class PostRestartHtlcCleaner(nodeParams: NodeParams, register: ActorRef, initial val failure = InvalidOnionBlinding(ByteVector32.Zeroes) CMD_FAIL_MALFORMED_HTLC(htlc.id, failure.onionHash, failure.code, commit = true) case None => - CMD_FAIL_HTLC(htlc.id, FailureReason.LocalFailure(TemporaryNodeFailure()), commit = true) + CMD_FAIL_HTLC(htlc.id, FailureReason.LocalFailure(TemporaryNodeFailure()), None, commit = true) } channel ! cmd } else { @@ -177,7 +178,7 @@ class PostRestartHtlcCleaner(nodeParams: NodeParams, register: ActorRef, initial val feesPaid = 0.msat // fees are unknown since we lost the reference to the payment nodeParams.db.payments.getOutgoingPayment(id) match { case Some(p) => - nodeParams.db.payments.updateOutgoingPayment(PaymentSent(p.parentId, fulfilledHtlc.paymentHash, paymentPreimage, p.recipientAmount, p.recipientNodeId, PaymentSent.PartialPayment(id, fulfilledHtlc.amountMsat, feesPaid, fulfilledHtlc.channelId, None) :: Nil)) + nodeParams.db.payments.updateOutgoingPayment(PaymentSent(p.parentId, fulfilledHtlc.paymentHash, paymentPreimage, p.recipientAmount, p.recipientNodeId, PaymentSent.PartialPayment(id, fulfilledHtlc.amountMsat, feesPaid, fulfilledHtlc.channelId, None) :: Nil, None)) // If all downstream HTLCs are now resolved, we can emit the payment event. val payments = nodeParams.db.payments.listOutgoingPayments(p.parentId) if (!payments.exists(p => p.status == OutgoingPaymentStatus.Pending)) { @@ -185,7 +186,7 @@ class PostRestartHtlcCleaner(nodeParams: NodeParams, register: ActorRef, initial case OutgoingPayment(id, _, _, _, _, amount, _, _, _, _, _, OutgoingPaymentStatus.Succeeded(_, feesPaid, _, completedAt)) => PaymentSent.PartialPayment(id, amount, feesPaid, ByteVector32.Zeroes, None, completedAt) } - val sent = PaymentSent(p.parentId, fulfilledHtlc.paymentHash, paymentPreimage, p.recipientAmount, p.recipientNodeId, succeeded) + val sent = PaymentSent(p.parentId, fulfilledHtlc.paymentHash, paymentPreimage, p.recipientAmount, p.recipientNodeId, succeeded, None) log.info(s"payment id=${sent.id} paymentHash=${sent.paymentHash} successfully sent (amount=${sent.recipientAmount})") context.system.eventStream.publish(sent) } @@ -196,7 +197,7 @@ class PostRestartHtlcCleaner(nodeParams: NodeParams, register: ActorRef, initial val dummyFinalAmount = fulfilledHtlc.amountMsat val dummyNodeId = nodeParams.nodeId nodeParams.db.payments.addOutgoingPayment(OutgoingPayment(id, id, None, fulfilledHtlc.paymentHash, PaymentType.Standard, fulfilledHtlc.amountMsat, dummyFinalAmount, dummyNodeId, TimestampMilli.now(), None, None, OutgoingPaymentStatus.Pending)) - nodeParams.db.payments.updateOutgoingPayment(PaymentSent(id, fulfilledHtlc.paymentHash, paymentPreimage, dummyFinalAmount, dummyNodeId, PaymentSent.PartialPayment(id, fulfilledHtlc.amountMsat, feesPaid, fulfilledHtlc.channelId, None) :: Nil)) + nodeParams.db.payments.updateOutgoingPayment(PaymentSent(id, fulfilledHtlc.paymentHash, paymentPreimage, dummyFinalAmount, dummyNodeId, PaymentSent.PartialPayment(id, fulfilledHtlc.amountMsat, feesPaid, fulfilledHtlc.channelId, None) :: Nil, None)) } // There can never be more than one pending downstream HTLC for a given local origin (a multi-part payment is // instead spread across multiple local origins) so we can now forget this origin. @@ -207,7 +208,7 @@ class PostRestartHtlcCleaner(nodeParams: NodeParams, register: ActorRef, initial if (relayedOut != Set((fulfilledHtlc.channelId, fulfilledHtlc.id))) { log.error(s"unexpected channel relay downstream HTLCs: expected (${fulfilledHtlc.channelId},${fulfilledHtlc.id}), found $relayedOut") } - PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, originChannelId, CMD_FULFILL_HTLC(originHtlcId, paymentPreimage, commit = true)) + PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, originChannelId, CMD_FULFILL_HTLC(originHtlcId, paymentPreimage, None, commit = true)) // We don't know when we received this HTLC so we just pretend that we received it just now. context.system.eventStream.publish(ChannelPaymentRelayed(amountIn, fulfilledHtlc.amountMsat, fulfilledHtlc.paymentHash, originChannelId, fulfilledHtlc.channelId, TimestampMilli.now(), TimestampMilli.now())) Metrics.PendingRelayedOut.decrement() @@ -218,7 +219,7 @@ class PostRestartHtlcCleaner(nodeParams: NodeParams, register: ActorRef, initial log.info(s"received preimage for paymentHash=${fulfilledHtlc.paymentHash}: fulfilling ${originHtlcs.length} HTLCs upstream") originHtlcs.foreach { case Upstream.Cold.Channel(channelId, htlcId, _) => Metrics.Resolved.withTag(Tags.Success, value = true).withTag(Metrics.Relayed, value = true).increment() - PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, channelId, CMD_FULFILL_HTLC(htlcId, paymentPreimage, commit = true)) + PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, channelId, CMD_FULFILL_HTLC(htlcId, paymentPreimage, None, commit = true)) } } val relayedOut1 = relayedOut diff Set((fulfilledHtlc.channelId, fulfilledHtlc.id)) @@ -269,7 +270,7 @@ class PostRestartHtlcCleaner(nodeParams: NodeParams, register: ActorRef, initial val failure = InvalidOnionBlinding(ByteVector32.Zeroes) CMD_FAIL_MALFORMED_HTLC(originHtlcId, failure.onionHash, failure.code, commit = true) case None => - ChannelRelay.translateRelayFailure(originHtlcId, fail) + ChannelRelay.translateRelayFailure(originHtlcId, fail, None) } PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, originChannelId, cmd) case Upstream.Cold.Trampoline(originHtlcs) => @@ -278,7 +279,7 @@ class PostRestartHtlcCleaner(nodeParams: NodeParams, register: ActorRef, initial Metrics.Resolved.withTag(Tags.Success, value = false).withTag(Metrics.Relayed, value = true).increment() // We don't bother decrypting the downstream failure to forward a more meaningful error upstream, it's // very likely that it won't be actionable anyway because of our node restart. - PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, channelId, CMD_FAIL_HTLC(htlcId, FailureReason.LocalFailure(TemporaryNodeFailure()), commit = true)) + PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, channelId, CMD_FAIL_HTLC(htlcId, FailureReason.LocalFailure(TemporaryNodeFailure()), None, commit = true)) } } } @@ -306,7 +307,7 @@ class PostRestartHtlcCleaner(nodeParams: NodeParams, register: ActorRef, initial object PostRestartHtlcCleaner { - def props(nodeParams: NodeParams, register: ActorRef, initialized: Option[Promise[Done]] = None) = Props(new PostRestartHtlcCleaner(nodeParams, register, initialized)) + def props(nodeParams: NodeParams, register: ActorRef, initialized: Option[Promise[Done]] = None): Props = Props(new PostRestartHtlcCleaner(nodeParams, register, initialized)) case class Init(channels: Seq[PersistentChannelData]) @@ -320,10 +321,10 @@ object PostRestartHtlcCleaner { val Hint = "hint" private val pending = Kamon.gauge("payment.broken-htlcs.pending", "Broken HTLCs because of a node restart") - val PendingNotRelayed = pending.withTag(Relayed, value = false) - val PendingRelayedOut = pending.withTag(Relayed, value = true) - val Resolved = Kamon.gauge("payment.broken-htlcs.resolved", "Broken HTLCs resolved after a node restart") - val Unhandled = Kamon.gauge("payment.broken-htlcs.unhandled", "Broken HTLCs that we don't know how to handle") + val PendingNotRelayed: Gauge = pending.withTag(Relayed, value = false) + val PendingRelayedOut: Gauge = pending.withTag(Relayed, value = true) + val Resolved: Metric.Gauge = Kamon.gauge("payment.broken-htlcs.resolved", "Broken HTLCs resolved after a node restart") + val Unhandled: Metric.Gauge = Kamon.gauge("payment.broken-htlcs.unhandled", "Broken HTLCs that we don't know how to handle") } @@ -403,7 +404,7 @@ object PostRestartHtlcCleaner { .toMap /** @return pending outgoing HTLCs, grouped by their upstream origin. */ - private def getHtlcsRelayedOut(channels: Seq[PersistentChannelData], htlcsIn: Seq[IncomingHtlc])(implicit log: LoggingAdapter): Map[Origin.Cold, Set[(ByteVector32, Long)]] = { + private def getHtlcsRelayedOut(nodeParams: NodeParams, channels: Seq[PersistentChannelData], htlcsIn: Seq[IncomingHtlc])(implicit log: LoggingAdapter): Map[Origin.Cold, Set[(ByteVector32, Long)]] = { val htlcsOut = channels .collect { case c: ChannelDataWithCommitments => c } .flatMap { c => @@ -427,10 +428,10 @@ object PostRestartHtlcCleaner { case Some(_: Closing.MutualClose) => Set.empty case None => Set.empty } - val params = d.commitments.params + val channelKeys = nodeParams.channelKeyManager.channelKeys(d.commitments.channelParams.channelConfig, d.commitments.localChannelParams.fundingKeyPath) val timedOutHtlcs: Set[Long] = (closingType_opt match { - case Some(c: Closing.LocalClose) => confirmedTxs.flatMap(tx => Closing.trimmedOrTimedOutHtlcs(params.commitmentFormat, c.localCommit, c.localCommitPublished, params.localParams.dustLimit, tx)) - case Some(c: Closing.RemoteClose) => confirmedTxs.flatMap(tx => Closing.trimmedOrTimedOutHtlcs(params.commitmentFormat, c.remoteCommit, c.remoteCommitPublished, params.remoteParams.dustLimit, tx)) + case Some(c: Closing.LocalClose) => confirmedTxs.flatMap(tx => Closing.trimmedOrTimedOutHtlcs(channelKeys, d.commitments.latest, c.localCommit, tx)) + case Some(c: Closing.RemoteClose) => confirmedTxs.flatMap(tx => Closing.trimmedOrTimedOutHtlcs(channelKeys, d.commitments.latest, c.remoteCommit, tx)) case Some(_: Closing.RevokedClose) => Set.empty // revoked commitments are handled using [[overriddenOutgoingHtlcs]] above case Some(_: Closing.RecoveryClose) => Set.empty // we lose htlc outputs in dataloss protection scenarios (future remote commit) case Some(_: Closing.MutualClose) => Set.empty diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/Relayer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/Relayer.scala index dd706ecf6e..920274be82 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/Relayer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/Relayer.scala @@ -28,8 +28,9 @@ import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.eclair.channel._ import fr.acinq.eclair.db.PendingCommandsDb import fr.acinq.eclair.payment._ +import fr.acinq.eclair.reputation.{Reputation, ReputationRecorder} import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{CltvExpiryDelta, Logs, MilliSatoshi, NodeParams, RealShortChannelId} +import fr.acinq.eclair.{CltvExpiryDelta, Logs, MilliSatoshi, NodeParams, RealShortChannelId, TimestampMilli} import grizzled.slf4j.Logging import scala.concurrent.Promise @@ -49,7 +50,7 @@ import scala.util.Random * It also receives channel HTLC events (fulfill / failed) and relays those to the appropriate handlers. * It also maintains an up-to-date view of local channel balances. */ -class Relayer(nodeParams: NodeParams, router: ActorRef, register: ActorRef, paymentHandler: ActorRef, initialized: Option[Promise[Done]] = None) extends Actor with DiagnosticActorLogging { +class Relayer(nodeParams: NodeParams, router: ActorRef, register: ActorRef, paymentHandler: ActorRef, reputationRecorder_opt: Option[typed.ActorRef[ReputationRecorder.Command]], initialized: Option[Promise[Done]] = None) extends Actor with DiagnosticActorLogging { import Relayer._ @@ -57,25 +58,26 @@ class Relayer(nodeParams: NodeParams, router: ActorRef, register: ActorRef, paym implicit def implicitLog: LoggingAdapter = log private val postRestartCleaner = context.actorOf(PostRestartHtlcCleaner.props(nodeParams, register, initialized), "post-restart-htlc-cleaner") - private val channelRelayer = context.spawn(Behaviors.supervise(ChannelRelayer(nodeParams, register)).onFailure(SupervisorStrategy.resume), "channel-relayer") - private val nodeRelayer = context.spawn(Behaviors.supervise(NodeRelayer(nodeParams, register, NodeRelay.SimpleOutgoingPaymentFactory(nodeParams, router, register), router)).onFailure(SupervisorStrategy.resume), name = "node-relayer") + private val channelRelayer = context.spawn(Behaviors.supervise(ChannelRelayer(nodeParams, register, reputationRecorder_opt)).onFailure(SupervisorStrategy.resume), "channel-relayer") + private val nodeRelayer = context.spawn(Behaviors.supervise(NodeRelayer(nodeParams, register, NodeRelay.SimpleOutgoingPaymentFactory(nodeParams, router, register, reputationRecorder_opt), router)).onFailure(SupervisorStrategy.resume), name = "node-relayer") def receive: Receive = { case init: PostRestartHtlcCleaner.Init => postRestartCleaner forward init - case RelayForward(add, originNode) => + case RelayForward(add, originNode, incomingChannelOccupancy) => log.debug(s"received forwarding request for htlc #${add.id} from channelId=${add.channelId}") IncomingPaymentPacket.decrypt(add, nodeParams.privateKey, nodeParams.features) match { case Right(p: IncomingPaymentPacket.FinalPacket) => log.debug(s"forwarding htlc #${add.id} to payment-handler") paymentHandler forward p case Right(r: IncomingPaymentPacket.ChannelRelayPacket) => - channelRelayer ! ChannelRelayer.Relay(r, originNode) + channelRelayer ! ChannelRelayer.Relay(r, originNode, incomingChannelOccupancy) case Right(r: IncomingPaymentPacket.NodeRelayPacket) => if (!nodeParams.enableTrampolinePayment) { log.warning(s"rejecting htlc #${add.id} from channelId=${add.channelId} reason=trampoline disabled") - PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, add.channelId, CMD_FAIL_HTLC(add.id, FailureReason.LocalFailure(RequiredNodeFeatureMissing()), commit = true)) + val attribution = FailureAttributionData(htlcReceivedAt = r.receivedAt, trampolineReceivedAt_opt = None) + PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, add.channelId, CMD_FAIL_HTLC(add.id, FailureReason.LocalFailure(RequiredNodeFeatureMissing()), Some(attribution), commit = true)) } else { - nodeRelayer ! NodeRelayer.Relay(r, originNode) + nodeRelayer ! NodeRelayer.Relay(r, originNode, incomingChannelOccupancy) } case Left(badOnion: BadOnion) => log.warning(s"couldn't parse onion: reason=${badOnion.message}") @@ -84,7 +86,8 @@ class Relayer(nodeParams: NodeParams, router: ActorRef, register: ActorRef, paym // We are the introduction point of a blinded path: we add a non-negligible delay to make it look like it // could come from a downstream node. val delay = Some(500.millis + Random.nextLong(1500).millis) - CMD_FAIL_HTLC(add.id, FailureReason.LocalFailure(InvalidOnionBlinding(badOnion.onionHash)), delay, commit = true) + val attribution = FailureAttributionData(htlcReceivedAt = TimestampMilli.now(), trampolineReceivedAt_opt = None) + CMD_FAIL_HTLC(add.id, FailureReason.LocalFailure(InvalidOnionBlinding(badOnion.onionHash)), Some(attribution), delay, commit = true) case _ => CMD_FAIL_MALFORMED_HTLC(add.id, badOnion.onionHash, badOnion.code, commit = true) } @@ -92,7 +95,8 @@ class Relayer(nodeParams: NodeParams, router: ActorRef, register: ActorRef, paym PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, add.channelId, cmdFail) case Left(failure) => log.warning(s"rejecting htlc #${add.id} from channelId=${add.channelId} reason=$failure") - val cmdFail = CMD_FAIL_HTLC(add.id, FailureReason.LocalFailure(failure), commit = true) + val attribution = FailureAttributionData(htlcReceivedAt = TimestampMilli.now(), trampolineReceivedAt_opt = None) + val cmdFail = CMD_FAIL_HTLC(add.id, FailureReason.LocalFailure(failure), Some(attribution), commit = true) PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, add.channelId, cmdFail) } @@ -113,7 +117,7 @@ class Relayer(nodeParams: NodeParams, router: ActorRef, register: ActorRef, paym override def mdc(currentMessage: Any): MDC = { val paymentHash_opt = currentMessage match { - case RelayForward(add, _) => Some(add.paymentHash) + case RelayForward(add, _, _) => Some(add.paymentHash) case addFailed: RES_ADD_FAILED[_] => Some(addFailed.c.paymentHash) case addCompleted: RES_ADD_SETTLED[_, _] => Some(addCompleted.htlc.paymentHash) case _ => None @@ -125,8 +129,8 @@ class Relayer(nodeParams: NodeParams, router: ActorRef, register: ActorRef, paym object Relayer extends Logging { - def props(nodeParams: NodeParams, router: ActorRef, register: ActorRef, paymentHandler: ActorRef, initialized: Option[Promise[Done]] = None): Props = - Props(new Relayer(nodeParams, router, register, paymentHandler, initialized)) + def props(nodeParams: NodeParams, router: ActorRef, register: ActorRef, paymentHandler: ActorRef, reputationRecorder_opt: Option[typed.ActorRef[ReputationRecorder.Command]], initialized: Option[Promise[Done]] = None): Props = + Props(new Relayer(nodeParams, router, register, paymentHandler, reputationRecorder_opt, initialized)) // @formatter:off case class RelayFees(feeBase: MilliSatoshi, feeProportionalMillionths: Long) @@ -141,7 +145,8 @@ object Relayer extends Logging { privateChannelFees: RelayFees, minTrampolineFees: RelayFees, enforcementDelay: FiniteDuration, - asyncPaymentsParams: AsyncPaymentsParams) { + asyncPaymentsParams: AsyncPaymentsParams, + peerReputationConfig: Reputation.Config) { def defaultFees(announceChannel: Boolean): RelayFees = { if (announceChannel) { publicChannelFees @@ -151,7 +156,7 @@ object Relayer extends Logging { } } - case class RelayForward(add: UpdateAddHtlc, originNode: PublicKey) + case class RelayForward(add: UpdateAddHtlc, originNode: PublicKey, incomingChannelOccupancy: Double) case class ChannelBalance(remoteNodeId: PublicKey, realScid: Option[RealShortChannelId], aliases: ShortIdAliases, canSend: MilliSatoshi, canReceive: MilliSatoshi, isPublic: Boolean, isEnabled: Boolean) sealed trait OutgoingChannelParams { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/MultiPartPaymentLifecycle.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/MultiPartPaymentLifecycle.scala index 14e8669098..b486e22f2c 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/MultiPartPaymentLifecycle.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/MultiPartPaymentLifecycle.scala @@ -27,8 +27,10 @@ import fr.acinq.eclair.payment.PaymentSent.PartialPayment import fr.acinq.eclair.payment._ import fr.acinq.eclair.payment.send.PaymentInitiator.SendPaymentConfig import fr.acinq.eclair.payment.send.PaymentLifecycle.SendPaymentToRoute +import fr.acinq.eclair.router.Router.MultiPartParams.FullCapacity import fr.acinq.eclair.router.Router._ import fr.acinq.eclair.{FSMDiagnosticActorLogging, Logs, MilliSatoshiLong, NodeParams, TimestampMilli} +import scodec.bits.ByteVector import java.util.UUID import java.util.concurrent.TimeUnit @@ -56,7 +58,7 @@ class MultiPartPaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, when(WAIT_FOR_PAYMENT_REQUEST) { case Event(r: SendMultiPartPayment, _) => - val routeParams = r.routeParams.copy(randomize = false) // we don't randomize the first attempt, regardless of configuration choices + val routeParams = r.routeParams.copy(randomize = false, mpp = r.routeParams.mpp.copy(splittingStrategy = FullCapacity)) // we don't randomize the first attempt, regardless of configuration choices log.debug("sending {} with maximum fee {}", r.recipient.totalAmount, r.routeParams.getMaxFee(r.recipient.totalAmount)) val d = PaymentProgress(r, r.maxAttempts, Map.empty, Ignore.empty, retryRouteRequest = false, failures = Nil) router ! createRouteRequest(self, nodeParams, routeParams, d, cfg) @@ -118,7 +120,7 @@ class MultiPartPaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, case Event(ps: PaymentSent, d: PaymentProgress) => require(ps.parts.length == 1, "child payment must contain only one part") // As soon as we get the preimage we can consider that the whole payment succeeded (we have a proof of payment). - gotoSucceededOrStop(PaymentSucceeded(d.request, ps.paymentPreimage, ps.parts, d.pending.keySet - ps.parts.head.id)) + gotoSucceededOrStop(PaymentSucceeded(d.request, ps.paymentPreimage, ps.parts, d.pending.keySet - ps.parts.head.id, ps.remainingAttribution_opt)) } when(PAYMENT_IN_PROGRESS) { @@ -144,7 +146,7 @@ class MultiPartPaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, require(ps.parts.length == 1, "child payment must contain only one part") // As soon as we get the preimage we can consider that the whole payment succeeded (we have a proof of payment). Metrics.PaymentAttempt.withTag(Tags.MultiPart, value = true).record(d.request.maxAttempts - d.remainingAttempts) - gotoSucceededOrStop(PaymentSucceeded(d.request, ps.paymentPreimage, ps.parts, d.pending.keySet - ps.parts.head.id)) + gotoSucceededOrStop(PaymentSucceeded(d.request, ps.paymentPreimage, ps.parts, d.pending.keySet - ps.parts.head.id, ps.remainingAttribution_opt)) } when(PAYMENT_ABORTED) { @@ -162,7 +164,7 @@ class MultiPartPaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, case Event(ps: PaymentSent, d: PaymentAborted) => require(ps.parts.length == 1, "child payment must contain only one part") log.warning(s"payment recipient fulfilled incomplete multi-part payment (id=${ps.parts.head.id})") - gotoSucceededOrStop(PaymentSucceeded(d.request, ps.paymentPreimage, ps.parts, d.pending - ps.parts.head.id)) + gotoSucceededOrStop(PaymentSucceeded(d.request, ps.paymentPreimage, ps.parts, d.pending - ps.parts.head.id, ps.remainingAttribution_opt)) case Event(_: RouteResponse, _) => stay() case Event(_: PaymentRouteNotFound, _) => stay() @@ -174,7 +176,7 @@ class MultiPartPaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, val parts = d.parts ++ ps.parts val pending = d.pending - ps.parts.head.id if (pending.isEmpty) { - myStop(d.request, Right(cfg.createPaymentSent(d.request.recipient, d.preimage, parts))) + myStop(d.request, Right(cfg.createPaymentSent(d.request.recipient, d.preimage, parts, d.remainingAttribution_opt))) } else { stay() using d.copy(parts = parts, pending = pending) } @@ -185,7 +187,7 @@ class MultiPartPaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, log.warning(s"payment succeeded but partial payment failed (id=${pf.id})") val pending = d.pending - pf.id if (pending.isEmpty) { - myStop(d.request, Right(cfg.createPaymentSent(d.request.recipient, d.preimage, d.parts))) + myStop(d.request, Right(cfg.createPaymentSent(d.request.recipient, d.preimage, d.parts, d.remainingAttribution_opt))) } else { stay() using d.copy(pending = pending) } @@ -212,10 +214,10 @@ class MultiPartPaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, private def gotoSucceededOrStop(d: PaymentSucceeded): State = { if (publishPreimage) { - d.request.replyTo ! PreimageReceived(paymentHash, d.preimage) + d.request.replyTo ! PreimageReceived(paymentHash, d.preimage, d.remainingAttribution_opt) } if (d.pending.isEmpty) { - myStop(d.request, Right(cfg.createPaymentSent(d.request.recipient, d.preimage, d.parts))) + myStop(d.request, Right(cfg.createPaymentSent(d.request.recipient, d.preimage, d.parts, d.remainingAttribution_opt))) } else goto(PAYMENT_SUCCEEDED) using d } @@ -310,7 +312,7 @@ object MultiPartPaymentLifecycle { * The payment FSM will wait for all child payments to settle before emitting payment events, but the preimage will be * shared as soon as it's received to unblock other actors that may need it. */ - case class PreimageReceived(paymentHash: ByteVector32, paymentPreimage: ByteVector32) + case class PreimageReceived(paymentHash: ByteVector32, paymentPreimage: ByteVector32, remainingAttribution_opt: Option[ByteVector]) // @formatter:off sealed trait State @@ -367,7 +369,7 @@ object MultiPartPaymentLifecycle { * @param parts fulfilled child payments. * @param pending pending child payments (we are waiting for them to be fulfilled downstream). */ - case class PaymentSucceeded(request: SendMultiPartPayment, preimage: ByteVector32, parts: Seq[PartialPayment], pending: Set[UUID]) extends Data + case class PaymentSucceeded(request: SendMultiPartPayment, preimage: ByteVector32, parts: Seq[PartialPayment], pending: Set[UUID], remainingAttribution_opt: Option[ByteVector]) extends Data private def createRouteRequest(replyTo: ActorRef, nodeParams: NodeParams, routeParams: RouteParams, d: PaymentProgress, cfg: SendPaymentConfig): RouteRequest = { RouteRequest(replyTo.toTyped, nodeParams.nodeId, d.request.recipient, routeParams, d.ignore, allowMultiPart = true, d.pending.values.toSeq, Some(cfg.paymentContext)) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentInitiator.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentInitiator.scala index d691d8c5ff..f7ed6d6233 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentInitiator.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentInitiator.scala @@ -29,6 +29,7 @@ import fr.acinq.eclair.payment.send.PaymentError._ import fr.acinq.eclair.router.Router._ import fr.acinq.eclair.wire.protocol._ import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, Features, MilliSatoshi, MilliSatoshiLong, NodeParams} +import scodec.bits.ByteVector import java.util.UUID @@ -49,7 +50,7 @@ class PaymentInitiator(nodeParams: NodeParams, outgoingPaymentFactory: PaymentIn // Immediately return the paymentId replyTo ! paymentId } - val paymentCfg = SendPaymentConfig(paymentId, paymentId, r.externalId, r.paymentHash, r.invoice.nodeId, Upstream.Local(paymentId), Some(r.invoice), r.payerKey_opt, storeInDb = true, publishEvent = true, recordPathFindingMetrics = true, confidence = 1.0) + val paymentCfg = SendPaymentConfig(paymentId, paymentId, r.externalId, r.paymentHash, r.invoice.nodeId, Upstream.Local(paymentId), Some(r.invoice), r.payerKey_opt, storeInDb = true, publishEvent = true, recordPathFindingMetrics = true) val finalExpiry = r.finalExpiry(nodeParams) val recipient = r.invoice match { case invoice: Bolt11Invoice => ClearRecipient(invoice, r.recipientAmount, finalExpiry, r.userCustomTlvs) @@ -70,7 +71,7 @@ class PaymentInitiator(nodeParams: NodeParams, outgoingPaymentFactory: PaymentIn case r: SendSpontaneousPayment => val paymentId = UUID.randomUUID() sender() ! paymentId - val paymentCfg = SendPaymentConfig(paymentId, paymentId, r.externalId, r.paymentHash, r.recipientNodeId, Upstream.Local(paymentId), None, None, storeInDb = true, publishEvent = true, recordPathFindingMetrics = r.recordPathFindingMetrics, confidence = 1.0) + val paymentCfg = SendPaymentConfig(paymentId, paymentId, r.externalId, r.paymentHash, r.recipientNodeId, Upstream.Local(paymentId), None, None, storeInDb = true, publishEvent = true, recordPathFindingMetrics = r.recordPathFindingMetrics) val finalExpiry = nodeParams.paymentFinalExpiry.computeFinalExpiry(nodeParams.currentBlockHeight, Channel.MIN_CLTV_EXPIRY_DELTA) val recipient = SpontaneousRecipient(r.recipientNodeId, r.recipientAmount, finalExpiry, r.paymentPreimage, r.userCustomTlvs) val fsm = outgoingPaymentFactory.spawnOutgoingPayment(context, paymentCfg) @@ -98,7 +99,7 @@ class PaymentInitiator(nodeParams: NodeParams, outgoingPaymentFactory: PaymentIn sender() ! PaymentFailed(paymentId, r.paymentHash, LocalFailure(r.recipientAmount, Nil, UnsupportedFeatures(r.invoice.features)) :: Nil) } else { sender() ! SendPaymentToRouteResponse(paymentId, parentPaymentId) - val paymentCfg = SendPaymentConfig(paymentId, parentPaymentId, r.externalId, r.paymentHash, r.recipientNodeId, Upstream.Local(paymentId), Some(r.invoice), None, storeInDb = true, publishEvent = true, recordPathFindingMetrics = false, confidence = 1.0) + val paymentCfg = SendPaymentConfig(paymentId, parentPaymentId, r.externalId, r.paymentHash, r.recipientNodeId, Upstream.Local(paymentId), Some(r.invoice), None, storeInDb = true, publishEvent = true, recordPathFindingMetrics = false) val finalExpiry = r.finalExpiry(nodeParams) val recipient = r.invoice match { case invoice: Bolt11Invoice => ClearRecipient(invoice, r.recipientAmount, finalExpiry, Set.empty) @@ -162,7 +163,7 @@ object PaymentInitiator { case class SimplePaymentFactory(nodeParams: NodeParams, router: ActorRef, register: ActorRef) extends MultiPartPaymentFactory { override def spawnOutgoingPayment(context: ActorContext, cfg: SendPaymentConfig): ActorRef = { - context.actorOf(PaymentLifecycle.props(nodeParams, cfg, router, register)) + context.actorOf(PaymentLifecycle.props(nodeParams, cfg, router, register, None)) } override def spawnOutgoingMultiPartPayment(context: ActorContext, cfg: SendPaymentConfig, publishPreimage: Boolean): ActorRef = { @@ -315,7 +316,6 @@ object PaymentInitiator { * @param publishEvent whether to publish a [[fr.acinq.eclair.payment.PaymentEvent]] on success/failure (e.g. for * multi-part child payments, we don't want to emit events for each child, only for the whole payment). * @param recordPathFindingMetrics We don't record metrics for payments that don't use path finding or that are a part of a bigger payment. - * @param confidence How confident we are that this payment will succeed. Used to set the outgoing endorsement value. */ case class SendPaymentConfig(id: UUID, parentId: UUID, @@ -327,15 +327,14 @@ object PaymentInitiator { payerKey_opt: Option[PrivateKey], storeInDb: Boolean, // e.g. for trampoline we don't want to store in the DB when we're relaying payments publishEvent: Boolean, - recordPathFindingMetrics: Boolean, - confidence: Double) { + recordPathFindingMetrics: Boolean) { val paymentContext: PaymentContext = PaymentContext(id, parentId, paymentHash) val paymentType = invoice match { case Some(_: Bolt12Invoice) => PaymentType.Blinded case _ => PaymentType.Standard } - def createPaymentSent(recipient: Recipient, preimage: ByteVector32, parts: Seq[PaymentSent.PartialPayment]) = PaymentSent(parentId, paymentHash, preimage, recipient.totalAmount, recipient.nodeId, parts) + def createPaymentSent(recipient: Recipient, preimage: ByteVector32, parts: Seq[PaymentSent.PartialPayment], remainingAttribution_opt: Option[ByteVector]) = PaymentSent(parentId, paymentHash, preimage, recipient.totalAmount, recipient.nodeId, parts, remainingAttribution_opt) } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentLifecycle.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentLifecycle.scala index ab37eeaff0..aa564e0798 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentLifecycle.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentLifecycle.scala @@ -17,11 +17,11 @@ package fr.acinq.eclair.payment.send import akka.actor.typed.scaladsl.adapter._ -import akka.actor.{ActorRef, FSM, Props, Status} +import akka.actor.{ActorRef, FSM, Props, typed} import akka.event.Logging.MDC -import fr.acinq.bitcoin.scalacompat.ByteVector32 import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.eclair._ +import fr.acinq.eclair.channel.Upstream.Hot import fr.acinq.eclair.channel._ import fr.acinq.eclair.crypto.{Sphinx, TransportHandler} import fr.acinq.eclair.db.{OutgoingPayment, OutgoingPaymentStatus} @@ -31,6 +31,8 @@ import fr.acinq.eclair.payment.PaymentSent.PartialPayment import fr.acinq.eclair.payment._ import fr.acinq.eclair.payment.send.PaymentInitiator.SendPaymentConfig import fr.acinq.eclair.payment.send.PaymentLifecycle._ +import fr.acinq.eclair.reputation.Reputation +import fr.acinq.eclair.reputation.ReputationRecorder.GetConfidence import fr.acinq.eclair.router.Router._ import fr.acinq.eclair.router._ import fr.acinq.eclair.wire.protocol._ @@ -41,7 +43,7 @@ import java.util.concurrent.TimeUnit * Created by PM on 26/08/2016. */ -class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: ActorRef, register: ActorRef) extends FSMDiagnosticActorLogging[PaymentLifecycle.State, PaymentLifecycle.Data] { +class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: ActorRef, register: ActorRef, reputationRecorder_opt: Option[typed.ActorRef[GetConfidence]]) extends FSMDiagnosticActorLogging[PaymentLifecycle.State, PaymentLifecycle.Data] { private val id = cfg.id private val paymentHash = cfg.paymentHash @@ -74,7 +76,28 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A when(WAITING_FOR_ROUTE) { case Event(RouteResponse(route +: _), WaitingForRoute(request, failures, ignore)) => log.info(s"route found: attempt=${failures.size + 1}/${request.maxAttempts} route=${route.printNodes()} channels=${route.printChannels()}") - OutgoingPaymentPacket.buildOutgoingPayment(Origin.Hot(self, cfg.upstream), paymentHash, route, request.recipient, cfg.confidence) match { + reputationRecorder_opt match { + case Some(reputationRecorder) => + val cltvExpiry = route.fullRoute.map(_.cltvExpiryDelta).foldLeft(request.recipient.expiry)(_ + _) + reputationRecorder ! GetConfidence(self, cfg.upstream, Some(route.hops.head.nextNodeId), route.hops.head.fee(request.amount), nodeParams.currentBlockHeight, cltvExpiry) + case None => + val endorsement = cfg.upstream match { + case Hot.Channel(add, _, _, _) => add.endorsement + case Hot.Trampoline(received) => received.map(_.add.endorsement).min + case Upstream.Local(_) => Reputation.maxEndorsement + } + self ! Reputation.Score.fromEndorsement(endorsement) + } + goto(WAITING_FOR_CONFIDENCE) using WaitingForConfidence(request, failures, ignore, route) + + case Event(PaymentRouteNotFound(t), WaitingForRoute(request, failures, _)) => + Metrics.PaymentError.withTag(Tags.Failure, Tags.FailureType(LocalFailure(request.amount, Nil, t))).increment() + myStop(request, Left(PaymentFailed(id, paymentHash, failures :+ LocalFailure(request.amount, Nil, t)))) + } + + when(WAITING_FOR_CONFIDENCE) { + case Event(score: Reputation.Score, WaitingForConfidence(request, failures, ignore, route)) => + OutgoingPaymentPacket.buildOutgoingPayment(Origin.Hot(self, cfg.upstream), paymentHash, route, request.recipient, score) match { case Right(payment) => register ! Register.ForwardShortId(self.toTyped[Register.ForwardShortIdFailure[CMD_ADD_HTLC]], payment.outgoingChannel, payment.cmd) goto(WAITING_FOR_PAYMENT_COMPLETE) using WaitingForComplete(request, payment.cmd, failures, payment.sharedSecrets, ignore, route) @@ -83,10 +106,6 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A Metrics.PaymentError.withTag(Tags.Failure, Tags.FailureType(LocalFailure(request.amount, route.fullRoute, error))).increment() myStop(request, Left(PaymentFailed(id, paymentHash, failures :+ LocalFailure(request.amount, route.fullRoute, error)))) } - - case Event(PaymentRouteNotFound(t), WaitingForRoute(request, failures, _)) => - Metrics.PaymentError.withTag(Tags.Failure, Tags.FailureType(LocalFailure(request.amount, Nil, t))).increment() - myStop(request, Left(PaymentFailed(id, paymentHash, failures :+ LocalFailure(request.amount, Nil, t)))) } when(WAITING_FOR_PAYMENT_COMPLETE) { @@ -102,7 +121,20 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A router ! Router.RouteDidRelay(d.route) Metrics.PaymentAttempt.withTag(Tags.MultiPart, value = false).record(d.failures.size + 1) val p = PartialPayment(id, d.request.amount, d.cmd.amount - d.request.amount, htlc.channelId, Some(d.route.fullRoute)) - myStop(d.request, Right(cfg.createPaymentSent(d.recipient, fulfill.paymentPreimage, p :: Nil))) + val remainingAttribution_opt = fulfill match { + case HtlcResult.RemoteFulfill(updateFulfill) => + updateFulfill.attribution_opt match { + case Some(attribution) => + val unwrapped = Sphinx.Attribution.unwrap(attribution, d.sharedSecrets) + if (unwrapped.holdTimes.nonEmpty) { + context.system.eventStream.publish(Router.ReportedHoldTimes(unwrapped.holdTimes)) + } + unwrapped.remaining_opt + case None => None + } + case _: HtlcResult.OnChainFulfill => None + } + myStop(d.request, Right(cfg.createPaymentSent(d.recipient, fulfill.paymentPreimage, p :: Nil, remainingAttribution_opt))) case Event(RES_ADD_SETTLED(_, _, fail: HtlcResult.Fail), d: WaitingForComplete) => fail match { @@ -164,12 +196,16 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A private def handleRemoteFail(d: WaitingForComplete, fail: UpdateFailHtlc) = { import d._ - ((Sphinx.FailurePacket.decrypt(fail.reason, sharedSecrets) match { + val htlcFailure = Sphinx.FailurePacket.decrypt(fail.reason, fail.attribution_opt, sharedSecrets) + if (htlcFailure.holdTimes.nonEmpty) { + context.system.eventStream.publish(Router.ReportedHoldTimes(htlcFailure.holdTimes)) + } + ((htlcFailure.failure match { case success@Right(e) => Metrics.PaymentError.withTag(Tags.Failure, Tags.FailureType(RemoteFailure(request.amount, Nil, e))).increment() success case failure@Left(e) => - Metrics.PaymentError.withTag(Tags.Failure, Tags.FailureType(UnreadableRemoteFailure(request.amount, Nil, e.unwrapped))).increment() + Metrics.PaymentError.withTag(Tags.Failure, Tags.FailureType(UnreadableRemoteFailure(request.amount, Nil, e, htlcFailure.holdTimes))).increment() failure }) match { case res@Right(Sphinx.DecryptedFailurePacket(nodeId, failureMessage)) => @@ -215,15 +251,15 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A case _ => } RemoteFailure(request.amount, route.fullRoute, e) - case Left(Sphinx.CannotDecryptFailurePacket(unwrapped)) => + case Left(e@Sphinx.CannotDecryptFailurePacket(unwrapped, _)) => log.warning(s"cannot parse returned error ${fail.reason.toHex} with sharedSecrets=$sharedSecrets: unwrapped=$unwrapped") - UnreadableRemoteFailure(request.amount, route.fullRoute, unwrapped) + UnreadableRemoteFailure(request.amount, route.fullRoute, e, htlcFailure.holdTimes) } log.warning(s"too many failed attempts, failing the payment") myStop(request, Left(PaymentFailed(id, paymentHash, failures :+ failure))) - case Left(Sphinx.CannotDecryptFailurePacket(unwrapped)) => + case Left(e@Sphinx.CannotDecryptFailurePacket(unwrapped, _)) => log.warning(s"cannot parse returned error: unwrapped=$unwrapped, route=${route.printNodes()}") - val failure = UnreadableRemoteFailure(request.amount, route.fullRoute, unwrapped) + val failure = UnreadableRemoteFailure(request.amount, route.fullRoute, e, htlcFailure.holdTimes) retry(failure, d) case Right(e@Sphinx.DecryptedFailurePacket(nodeId, failureMessage: Node)) => log.info(s"received 'Node' type error message from nodeId=$nodeId, trying to route around it (failure=$failureMessage)") @@ -430,7 +466,7 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A object PaymentLifecycle { - def props(nodeParams: NodeParams, cfg: SendPaymentConfig, router: ActorRef, register: ActorRef) = Props(new PaymentLifecycle(nodeParams, cfg, router, register)) + def props(nodeParams: NodeParams, cfg: SendPaymentConfig, router: ActorRef, register: ActorRef, reputationRecorder_opt: Option[typed.ActorRef[GetConfidence]]) = Props(new PaymentLifecycle(nodeParams, cfg, router, register, reputationRecorder_opt)) sealed trait SendPayment { // @formatter:off @@ -476,6 +512,7 @@ object PaymentLifecycle { sealed trait Data case object WaitingForRequest extends Data case class WaitingForRoute(request: SendPayment, failures: Seq[PaymentFailure], ignore: Ignore) extends Data + case class WaitingForConfidence(request: SendPayment, failures: Seq[PaymentFailure], ignore: Ignore, route: Route) extends Data case class WaitingForComplete(request: SendPayment, cmd: CMD_ADD_HTLC, failures: Seq[PaymentFailure], sharedSecrets: Seq[Sphinx.SharedSecret], ignore: Ignore, route: Route) extends Data { val recipient = request.recipient } @@ -483,6 +520,7 @@ object PaymentLifecycle { sealed trait State case object WAITING_FOR_REQUEST extends State case object WAITING_FOR_ROUTE extends State + case object WAITING_FOR_CONFIDENCE extends State case object WAITING_FOR_PAYMENT_COMPLETE extends State /** custom exceptions to handle corner cases */ diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/TrampolinePaymentLifecycle.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/TrampolinePaymentLifecycle.scala index 6f55c60b66..bf99fb55b3 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/TrampolinePaymentLifecycle.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/TrampolinePaymentLifecycle.scala @@ -16,6 +16,7 @@ package fr.acinq.eclair.payment.send +import akka.actor.typed.eventstream.EventStream import akka.actor.typed.scaladsl.adapter._ import akka.actor.typed.scaladsl.{ActorContext, Behaviors} import akka.actor.typed.{ActorRef, Behavior} @@ -27,6 +28,9 @@ import fr.acinq.eclair.io.Peer import fr.acinq.eclair.payment.OutgoingPaymentPacket.{NodePayload, buildOnion} import fr.acinq.eclair.payment.PaymentSent.PartialPayment import fr.acinq.eclair.payment._ +import fr.acinq.eclair.payment.send.TrampolinePayment.{buildOutgoingPayment, computeFees} +import fr.acinq.eclair.reputation.Reputation +import fr.acinq.eclair.router.Router import fr.acinq.eclair.router.Router.RouteParams import fr.acinq.eclair.wire.protocol.{PaymentOnion, PaymentOnionCodecs} import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, Features, Logs, MilliSatoshi, NodeParams, randomBytes32} @@ -54,9 +58,9 @@ object TrampolinePaymentLifecycle { require(invoice.amount_opt.nonEmpty, "amount-less invoices are not supported in trampoline tests") } private case class TrampolinePeerNotFound(trampolineNodeId: PublicKey) extends Command + private case class CouldntAddHtlc(failure: Throwable) extends Command + private case class HtlcSettled(result: HtlcResult, part: PartialPayment, holdTimes: Seq[Sphinx.HoldTime]) extends Command private case class WrappedPeerChannels(channels: Seq[Peer.ChannelInfo]) extends Command - private case class WrappedAddHtlcResponse(response: CommandResponse[CMD_ADD_HTLC]) extends Command - private case class WrappedHtlcSettled(result: RES_ADD_SETTLED[Origin.Hot, HtlcResult]) extends Command // @formatter:on def apply(nodeParams: NodeParams, register: ActorRef[Register.ForwardNodeId[Peer.GetPeerChannels]]): Behavior[Command] = @@ -75,6 +79,78 @@ object TrampolinePaymentLifecycle { } } + object PartHandler { + sealed trait Command + + private case class WrappedAddHtlcResponse(response: CommandResponse[CMD_ADD_HTLC]) extends Command + + private case class WrappedHtlcSettled(result: RES_ADD_SETTLED[Origin.Hot, HtlcResult]) extends Command + + def apply(parent: ActorRef[TrampolinePaymentLifecycle.Command], + cmd: TrampolinePaymentLifecycle.SendPayment, + amount: MilliSatoshi, + channelInfo: Peer.ChannelInfo, + expiry: CltvExpiry, + trampolinePaymentSecret: ByteVector32, + attemptNumber: Int): Behavior[Command] = + Behaviors.setup { context => + new PartHandler(context, parent, cmd).start(amount, channelInfo, expiry, trampolinePaymentSecret, attemptNumber) + } + } + + class PartHandler(context: ActorContext[PartHandler.Command], parent: ActorRef[Command], cmd: TrampolinePaymentLifecycle.SendPayment) { + + import PartHandler._ + + private val paymentHash = cmd.invoice.paymentHash + + private val addHtlcAdapter = context.messageAdapter[CommandResponse[CMD_ADD_HTLC]](WrappedAddHtlcResponse) + private val htlcSettledAdapter = context.messageAdapter[RES_ADD_SETTLED[Origin.Hot, HtlcResult]](WrappedHtlcSettled) + + def start(amount: MilliSatoshi, channelInfo: Peer.ChannelInfo, expiry: CltvExpiry, trampolinePaymentSecret: ByteVector32, attemptNumber: Int): Behavior[PartHandler.Command] = { + val origin = Origin.Hot(htlcSettledAdapter.toClassic, Upstream.Local(cmd.paymentId)) + val outgoing = buildOutgoingPayment(cmd.trampolineNodeId, cmd.invoice, amount, expiry, Some(trampolinePaymentSecret), attemptNumber) + val add = CMD_ADD_HTLC(addHtlcAdapter.toClassic, outgoing.trampolineAmount, paymentHash, outgoing.trampolineExpiry, outgoing.onion.packet, None, Reputation.Score.max, None, origin, commit = true) + channelInfo.channel ! add + val channelId = channelInfo.data.asInstanceOf[DATA_NORMAL].channelId + val part = PartialPayment(cmd.paymentId, amount, computeFees(amount, attemptNumber), channelId, None) + waitForSettlement(part, outgoing.onion.sharedSecrets, outgoing.trampolineOnion.sharedSecrets) + } + + def waitForSettlement(part: PartialPayment, outerOnionSecrets: Seq[Sphinx.SharedSecret], trampolineOnionSecrets: Seq[Sphinx.SharedSecret]): Behavior[PartHandler.Command] = { + Behaviors.receiveMessagePartial { + case WrappedAddHtlcResponse(response) => response match { + case _: CommandSuccess[_] => + // HTLC was correctly sent out. + Behaviors.same + case failure: CommandFailure[_, Throwable] => + parent ! CouldntAddHtlc(failure.t) + Behaviors.stopped + } + case WrappedHtlcSettled(result) => result.result match { + case fulfill: HtlcResult.Fulfill => + val holdTimes = fulfill match { + case HtlcResult.RemoteFulfill(updateFulfill) => + updateFulfill.attribution_opt match { + case Some(attribution) => Sphinx.Attribution.unwrap(attribution, outerOnionSecrets).holdTimes + case None => Nil + } + case _: HtlcResult.OnChainFulfill => Nil + } + parent ! HtlcSettled(fulfill, part, holdTimes) + Behaviors.stopped + case fail: HtlcResult.Fail => + val holdTimes = fail match { + case HtlcResult.RemoteFail(updateFail) => + Sphinx.FailurePacket.decrypt(updateFail.reason, updateFail.attribution_opt, outerOnionSecrets).holdTimes + case _ => Nil + } + parent ! HtlcSettled(fail, part, holdTimes) + Behaviors.stopped + } + } + } + } } class TrampolinePaymentLifecycle private(nodeParams: NodeParams, @@ -90,8 +166,6 @@ class TrampolinePaymentLifecycle private(nodeParams: NodeParams, private val forwardNodeIdFailureAdapter = context.messageAdapter[Register.ForwardNodeIdFailure[Peer.GetPeerChannels]](_ => TrampolinePeerNotFound(cmd.trampolineNodeId)) private val peerChannelsResponseAdapter = context.messageAdapter[Peer.PeerChannels](c => WrappedPeerChannels(c.channels)) - private val addHtlcAdapter = context.messageAdapter[CommandResponse[CMD_ADD_HTLC]](WrappedAddHtlcResponse) - private val htlcSettledAdapter = context.messageAdapter[RES_ADD_SETTLED[Origin.Hot, HtlcResult]](WrappedHtlcSettled) def start(): Behavior[Command] = listChannels(attemptNumber = 0) @@ -117,7 +191,6 @@ class TrampolinePaymentLifecycle private(nodeParams: NodeParams, case _ => None } }) - val origin = Origin.Hot(htlcSettledAdapter.toClassic, Upstream.Local(cmd.paymentId)) val expiry = CltvExpiry(nodeParams.currentBlockHeight) + CltvExpiryDelta(36) if (filtered.isEmpty) { context.log.warn("no usable channel with trampoline node {}", cmd.trampolineNodeId) @@ -131,54 +204,49 @@ class TrampolinePaymentLifecycle private(nodeParams: NodeParams, // We generate a random secret to avoid leaking the invoice secret to the trampoline node. val trampolinePaymentSecret = randomBytes32() context.log.info("sending trampoline payment parts: {}->{}, {}->{}", channel1.data.channelId, amount1, channel2.data.channelId, amount2) - val parts = Seq((amount1, channel1), (amount2, channel2)).map { case (amount, channelInfo) => - val outgoing = buildOutgoingPayment(cmd.trampolineNodeId, cmd.invoice, amount, expiry, Some(trampolinePaymentSecret), attemptNumber) - val add = CMD_ADD_HTLC(addHtlcAdapter.toClassic, outgoing.trampolineAmount, paymentHash, outgoing.trampolineExpiry, outgoing.onion.packet, None, 1.0, None, origin, commit = true) - channelInfo.channel ! add - val channelId = channelInfo.data.asInstanceOf[DATA_NORMAL].channelId - PartialPayment(cmd.paymentId, amount, computeFees(amount, attemptNumber), channelId, None) + Seq((amount1, channel1), (amount2, channel2)).foreach { case (amount, channelInfo) => + context.spawnAnonymous(PartHandler(context.self, cmd, amount, channelInfo, expiry, trampolinePaymentSecret, attemptNumber)) } - waitForSettlement(remaining = 2, attemptNumber, parts) + waitForSettlement(remaining = 2, attemptNumber, Nil) } } - private def waitForSettlement(remaining: Int, attemptNumber: Int, parts: Seq[PartialPayment]): Behavior[Command] = { + private def waitForSettlement(remaining: Int, attemptNumber: Int, fulfilledParts: Seq[PartialPayment]): Behavior[Command] = { Behaviors.receiveMessagePartial { - case WrappedAddHtlcResponse(response) => response match { - case _: CommandSuccess[_] => - // HTLC was correctly sent out. - Behaviors.same - case failure: CommandFailure[_, Throwable] => - context.log.warn("HTLC could not be sent: {}", failure.t.getMessage) - if (remaining > 1) { - context.log.info("waiting for remaining HTLCs to complete") - waitForSettlement(remaining - 1, attemptNumber, parts) - } else { - context.log.warn("trampoline payment failed") - cmd.replyTo ! PaymentFailed(cmd.paymentId, paymentHash, LocalFailure(totalAmount, Nil, failure.t) :: Nil) - Behaviors.stopped - } - } - case WrappedHtlcSettled(result) => result.result match { - case fulfill: HtlcResult.Fulfill => - context.log.info("HTLC was fulfilled") - if (remaining > 1) { - context.log.info("waiting for remaining HTLCs to be fulfilled") - waitForSettlement(remaining - 1, attemptNumber, parts) - } else { - context.log.info("trampoline payment succeeded") - cmd.replyTo ! PaymentSent(cmd.paymentId, paymentHash, fulfill.paymentPreimage, totalAmount, cmd.invoice.nodeId, parts) - Behaviors.stopped - } - case fail: HtlcResult.Fail => - context.log.warn("received HTLC failure: {}", fail) - if (remaining > 1) { - context.log.info("waiting for remaining HTLCs to be failed") - waitForSettlement(remaining - 1, attemptNumber, parts) - } else { - retryOrStop(attemptNumber + 1) - } - } + case CouldntAddHtlc(failure) => + context.log.warn("HTLC could not be sent: {}", failure.getMessage) + if (remaining > 1) { + context.log.info("waiting for remaining HTLCs to complete") + waitForSettlement(remaining - 1, attemptNumber, fulfilledParts) + } else { + context.log.warn("trampoline payment failed") + cmd.replyTo ! PaymentFailed(cmd.paymentId, paymentHash, LocalFailure(totalAmount, Nil, failure) :: Nil) + Behaviors.stopped + } + case HtlcSettled(result: HtlcResult, part, holdTimes) => + if (holdTimes.nonEmpty) { + context.system.eventStream ! EventStream.Publish(Router.ReportedHoldTimes(holdTimes)) + } + result match { + case fulfill: HtlcResult.Fulfill => + context.log.info("HTLC was fulfilled") + if (remaining > 1) { + context.log.info("waiting for remaining HTLCs to be fulfilled") + waitForSettlement(remaining - 1, attemptNumber, part +: fulfilledParts) + } else { + context.log.info("trampoline payment succeeded") + cmd.replyTo ! PaymentSent(cmd.paymentId, paymentHash, fulfill.paymentPreimage, totalAmount, cmd.invoice.nodeId, part +: fulfilledParts, None) + Behaviors.stopped + } + case fail: HtlcResult.Fail => + context.log.warn("received HTLC failure: {}", fail) + if (remaining > 1) { + context.log.info("waiting for remaining HTLCs to be failed") + waitForSettlement(remaining - 1, attemptNumber, fulfilledParts) + } else { + retryOrStop(attemptNumber + 1) + } + } } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/remote/EclairInternalsSerializer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/remote/EclairInternalsSerializer.scala index 4538eed438..305b1410ee 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/remote/EclairInternalsSerializer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/remote/EclairInternalsSerializer.scala @@ -24,7 +24,7 @@ import fr.acinq.eclair.io.Peer.PeerRoutingMessage import fr.acinq.eclair.io.Switchboard.RouterPeerConf import fr.acinq.eclair.io.{ClientSpawner, Peer, PeerConnection, Switchboard} import fr.acinq.eclair.payment.relay.Relayer.RelayFees -import fr.acinq.eclair.router.Graph.{HeuristicsConstants, PaymentPathWeight, PaymentWeightRatios, WeightRatios} +import fr.acinq.eclair.router.Graph.{HeuristicsConstants, PaymentPathWeight, WeightRatios} import fr.acinq.eclair.router.Router._ import fr.acinq.eclair.router._ import fr.acinq.eclair.wire.protocol.CommonCodecs._ @@ -57,27 +57,24 @@ object EclairInternalsSerializer { ("feeBase" | millisatoshi) :: ("feeProportionalMillionths" | int64)).as[RelayFees] - val paymentWeightRatiosCodec: Codec[PaymentWeightRatios] = ( - ("baseFactor" | double) :: - ("cltvDeltaFactor" | double) :: - ("ageFactor" | double) :: - ("capacityFactor" | double) :: - ("hopCost" | relayFeesCodec)).as[PaymentWeightRatios] - val heuristicsConstantsCodec: Codec[HeuristicsConstants] = ( ("lockedFundsRisk" | double) :: ("failureCost" | relayFeesCodec) :: ("hopCost" | relayFeesCodec) :: - ("useLogProbability" | bool(8))).as[HeuristicsConstants] + ("useLogProbability" | bool(8)) :: + ("usePastRelaysData" | bool(8))).as[HeuristicsConstants] - val weightRatiosCodec: Codec[WeightRatios[PaymentPathWeight]] = - discriminated[WeightRatios[PaymentPathWeight]].by(uint8) - .typecase(0x00, paymentWeightRatiosCodec) + val weightRatiosCodec: Codec[HeuristicsConstants] = + discriminated[HeuristicsConstants].by(uint8) .typecase(0xff, heuristicsConstantsCodec) val multiPartParamsCodec: Codec[MultiPartParams] = ( ("minPartAmount" | millisatoshi) :: - ("maxParts" | int32)).as[MultiPartParams] + ("maxParts" | int32) :: + ("splittingStrategy" | discriminated[MultiPartParams.SplittingStrategy].by(uint8) + .typecase(0, provide(MultiPartParams.FullCapacity)) + .typecase(1, provide(MultiPartParams.Randomize)) + .typecase(2, provide(MultiPartParams.MaxExpectedAmount)))).as[MultiPartParams] val pathFindingConfCodec: Codec[PathFindingConf] = ( ("randomize" | bool(8)) :: @@ -109,6 +106,7 @@ object EclairInternalsSerializer { val routerConfCodec: Codec[RouterConf] = ( ("watchSpentWindow" | finiteDurationCodec) :: + ("channelSpentSpliceDelay" | int32) :: ("channelExcludeDuration" | finiteDurationCodec) :: ("routerBroadcastInterval" | finiteDurationCodec) :: ("syncConf" | syncConfCodec) :: diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/reputation/Reputation.scala b/eclair-core/src/main/scala/fr/acinq/eclair/reputation/Reputation.scala new file mode 100644 index 0000000000..43b8816e3c --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/reputation/Reputation.scala @@ -0,0 +1,187 @@ +/* + * Copyright 2025 ACINQ SAS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package fr.acinq.eclair.reputation + +import fr.acinq.bitcoin.scalacompat.ByteVector32 +import fr.acinq.eclair.channel._ +import fr.acinq.eclair.transactions.DirectedHtlc +import fr.acinq.eclair.wire.protocol.UpdateAddHtlc +import fr.acinq.eclair.{BlockHeight, CltvExpiry, MilliSatoshi, TimestampMilli} + +import scala.concurrent.duration.{DurationInt, FiniteDuration} + +/** + * Reputation score per endorsement level. + * + * @param weight How much fees we would have collected in the past if all HTLCs had succeeded (exponential moving average). + * @param score How much fees we have collected in the past (exponential moving average). + * @param lastSettlementAt Timestamp of the last recorded HTLC settlement. + */ +case class PastScore(weight: Double, score: Double, lastSettlementAt: TimestampMilli) + +/** We're relaying that HTLC and are waiting for it to settle. */ +case class PendingHtlc(fee: MilliSatoshi, endorsement: Int, startedAt: TimestampMilli, expiry: CltvExpiry) { + def weight(now: TimestampMilli, minDuration: FiniteDuration, currentBlockHeight: BlockHeight): Double = { + val alreadyPending = now - startedAt + val untilExpiry = (expiry.toLong - currentBlockHeight.toLong) * 10.minutes + val duration = alreadyPending + untilExpiry + fee.toLong.toDouble * (duration / minDuration) + } +} + +case class HtlcId(channelId: ByteVector32, id: Long) + +case object HtlcId { + def apply(add: UpdateAddHtlc): HtlcId = HtlcId(add.channelId, add.id) +} + +/** + * Local reputation for a given node. + * + * @param pastScores Scores from past HTLCs for each endorsement level. + * @param pending Set of pending HTLCs. + * @param halfLife Half life for the exponential moving average. + * @param maxRelayDuration Duration after which HTLCs are penalized for staying pending too long. + */ +case class Reputation(pastScores: Map[Int, PastScore], pending: Map[HtlcId, PendingHtlc], halfLife: FiniteDuration, maxRelayDuration: FiniteDuration) { + private def decay(now: TimestampMilli, lastSettlementAt: TimestampMilli): Double = scala.math.pow(0.5, (now - lastSettlementAt) / halfLife) + + /** + * Estimate the confidence that a payment will succeed. + */ + def getConfidence(fee: MilliSatoshi, endorsement: Int, currentBlockHeight: BlockHeight, expiry: CltvExpiry, now: TimestampMilli = TimestampMilli.now()): Double = { + val weights = Array.fill(Reputation.endorsementLevels)(0.0) + val scores = Array.fill(Reputation.endorsementLevels)(0.0) + for (e <- 0 until Reputation.endorsementLevels) { + val d = decay(now, pastScores(e).lastSettlementAt) + weights(e) += d * pastScores(e).weight + scores(e) += d * pastScores(e).score + } + for (p <- pending.values) { + weights(p.endorsement) += p.weight(now, maxRelayDuration, currentBlockHeight) + } + weights(endorsement) += PendingHtlc(fee, endorsement, now, expiry).weight(now, maxRelayDuration, currentBlockHeight) + /* + Higher endorsement buckets may have fewer payments which makes the weight of pending payments disproportionately + important. To counter this effect, we try adding payments from the lower buckets to see if it gives us a higher + confidence score. + It is acceptable to use payments with lower endorsements to increase the confidence score but not payments with + higher endorsements. + */ + var score = scores(endorsement) + var weight = weights(endorsement) + var confidence = score / weight + for (e <- Range.inclusive(endorsement - 1, 0, step = -1)) { + score += scores(e) + weight += weights(e) + confidence = confidence.max(score / weight) + } + confidence + } + + /** + * Register a pending relay. + */ + def addPendingHtlc(add: UpdateAddHtlc, fee: MilliSatoshi, endorsement: Int, now: TimestampMilli = TimestampMilli.now()): Reputation = + copy(pending = pending + (HtlcId(add) -> PendingHtlc(fee, endorsement, now, add.cltvExpiry))) + + /** + * When a HTLC is settled, we record whether it succeeded and how long it took. + */ + def settlePendingHtlc(htlcId: HtlcId, isSuccess: Boolean, now: TimestampMilli = TimestampMilli.now()): Reputation = { + val newScores = pending.get(htlcId).map(p => { + val d = decay(now, pastScores(p.endorsement).lastSettlementAt) + val duration = now - p.startedAt + val (weight, score) = if (isSuccess) { + (p.fee.toLong.toDouble * (duration / maxRelayDuration).max(1.0), p.fee.toLong.toDouble) + } else { + (p.fee.toLong.toDouble * (duration / maxRelayDuration), 0.0) + } + val newWeight = d * pastScores(p.endorsement).weight + weight + val newScore = d * pastScores(p.endorsement).score + score + pastScores + (p.endorsement -> PastScore(newWeight, newScore, now)) + }).getOrElse(pastScores) + copy(pending = pending - htlcId, pastScores = newScores) + } +} + +object Reputation { + private val endorsementLevels = 8 + val maxEndorsement: Int = endorsementLevels - 1 + + case class Config(enabled: Boolean, halfLife: FiniteDuration, maxRelayDuration: FiniteDuration) + + def init(config: Config): Reputation = Reputation(Map.empty.withDefaultValue(PastScore(0.0, 0.0, TimestampMilli.min)), Map.empty, config.halfLife, config.maxRelayDuration) + + /** + * @param incomingConfidence Confidence that the outgoing HTLC will succeed given the reputation of the incoming peer + */ + case class Score(incomingConfidence: Double, outgoingConfidence: Double) { + val endorsement: Int = toEndorsement(incomingConfidence) + + def checkOutgoingChannelOccupancy(channelId: ByteVector32, commitment: Commitment, outgoingHtlcs: Seq[UpdateAddHtlc]): Either[ChannelJammingException, Unit] = { + val maxAcceptedHtlcs = Seq(commitment.localCommitParams.maxAcceptedHtlcs, commitment.remoteCommitParams.maxAcceptedHtlcs).min + + for ((amountMsat, i) <- outgoingHtlcs.map(_.amountMsat).sorted.zipWithIndex) { + // We want to allow some small HTLCs but still keep slots for larger ones. + // We never want to reject HTLCs of size above `maxHtlcAmount / maxAcceptedHtlcs` as too small because we want to allow filling all the slots with HTLCs of equal sizes. + // We use exponentially spaced thresholds in between. + if (amountMsat.toLong < 1 || amountMsat.toLong.toDouble < math.pow(commitment.maxHtlcValueInFlight.toLong.toDouble / maxAcceptedHtlcs, i / maxAcceptedHtlcs)) { + return Left(TooManySmallHtlcs(channelId, number = i + 1, below = amountMsat)) + } + } + + val htlcValueInFlight = outgoingHtlcs.map(_.amountMsat).sum + val slotsOccupancy = outgoingHtlcs.size.toDouble / maxAcceptedHtlcs + val valueOccupancy = htlcValueInFlight.toLong.toDouble / commitment.maxHtlcValueInFlight.toLong.toDouble + val occupancy = slotsOccupancy max valueOccupancy + // Because there are only 8 endorsement levels, the highest endorsement corresponds to a confidence between 87.5% and 100%. + // So even for well-behaved peers setting the highest endorsement we still expect a confidence of less than 93.75%. + // To compensate for that we add a tolerance of 10% that's also useful for nodes without history. + if (incomingConfidence + 0.1 < occupancy) { + return Left(IncomingConfidenceTooLow(channelId, incomingConfidence, occupancy)) + } + + Right(()) + } + + def checkIncomingChannelOccupancy(incomingChannelOccupancy: Double, outgoingChannelId: ByteVector32): Either[ChannelJammingException, Unit] = { + if (outgoingConfidence + 0.1 < incomingChannelOccupancy) { + return Left(OutgoingConfidenceTooLow(outgoingChannelId, incomingConfidence, incomingChannelOccupancy)) + } + Right(()) + } + } + + case object Score { + val max: Score = Score(1.0, 1.0) + + def fromEndorsement(endorsement: Int): Score = Score((endorsement + 0.5) / 8, 1.0) + } + + def toEndorsement(confidence: Double): Int = (confidence * endorsementLevels).toInt.min(maxEndorsement) + + def incomingOccupancy(commitments: Commitments): Double = { + commitments.active.map(commitment => { + val incomingHtlcs = commitment.localCommit.spec.htlcs.collect(DirectedHtlc.incoming) + val slotsOccupancy = commitments.active.map(c => incomingHtlcs.size.toDouble / (c.localCommitParams.maxAcceptedHtlcs min c.remoteCommitParams.maxAcceptedHtlcs)).max + val htlcValueInFlight = incomingHtlcs.toSeq.map(_.amountMsat).sum + val valueOccupancy = commitments.active.map(c => htlcValueInFlight.toLong.toDouble / c.maxHtlcValueInFlight.toLong.toDouble).max + slotsOccupancy max valueOccupancy + }).max + } +} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/reputation/ReputationRecorder.scala b/eclair-core/src/main/scala/fr/acinq/eclair/reputation/ReputationRecorder.scala new file mode 100644 index 0000000000..58224a50ca --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/reputation/ReputationRecorder.scala @@ -0,0 +1,132 @@ +/* + * Copyright 2025 ACINQ SAS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package fr.acinq.eclair.reputation + +import akka.actor.typed.eventstream.EventStream +import akka.actor.typed.scaladsl.Behaviors +import akka.actor.typed.{ActorRef, Behavior} +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey +import fr.acinq.eclair.{BlockHeight, CltvExpiry, MilliSatoshi} +import fr.acinq.eclair.channel.Upstream.Hot +import fr.acinq.eclair.channel.{OutgoingHtlcAdded, OutgoingHtlcFailed, OutgoingHtlcFulfilled, OutgoingHtlcSettled, Upstream} +import fr.acinq.eclair.reputation.ReputationRecorder._ +import fr.acinq.eclair.wire.protocol.{UpdateAddHtlc, UpdateFailHtlc, UpdateFailMalformedHtlc} + +import scala.collection.mutable + +object ReputationRecorder { + // @formatter:off + sealed trait Command + case class GetConfidence(replyTo: ActorRef[Reputation.Score], upstream: Upstream.Hot, downstream_opt: Option[PublicKey], fee: MilliSatoshi, currentBlockHeight: BlockHeight, expiry: CltvExpiry) extends Command + case class WrappedOutgoingHtlcAdded(added: OutgoingHtlcAdded) extends Command + case class WrappedOutgoingHtlcSettled(settled: OutgoingHtlcSettled) extends Command + // @formatter:on + + def apply(config: Reputation.Config): Behavior[Command] = + Behaviors.setup(context => { + context.system.eventStream ! EventStream.Subscribe(context.messageAdapter(WrappedOutgoingHtlcSettled)) + context.system.eventStream ! EventStream.Subscribe(context.messageAdapter(WrappedOutgoingHtlcAdded)) + new ReputationRecorder(config).run() + }) + + /** + * A pending outgoing HTLC. + * + * @param add UpdateAddHtlc that contains an id for the HTLC and an endorsement value. + * @param upstream The incoming node or nodes. + * @param downstream The outgoing node. + */ + case class PendingHtlc(add: UpdateAddHtlc, upstream: Upstream.Hot, downstream: PublicKey) +} + +class ReputationRecorder(config: Reputation.Config) { + private val incomingReputations: mutable.Map[PublicKey, Reputation] = mutable.HashMap.empty.withDefaultValue(Reputation.init(config)) + private val outgoingReputations: mutable.Map[PublicKey, Reputation] = mutable.HashMap.empty.withDefaultValue(Reputation.init(config)) + private val pending: mutable.Map[HtlcId, PendingHtlc] = mutable.HashMap.empty + + def run(): Behavior[Command] = + Behaviors.receiveMessage { + case GetConfidence(replyTo, _: Upstream.Local, _, _, _, _) => + replyTo ! Reputation.Score.max + Behaviors.same + + case GetConfidence(replyTo, upstream: Upstream.Hot.Channel, downstream_opt, fee, currentBlockHeight, expiry) => + val incomingConfidence = incomingReputations.get(upstream.receivedFrom).map(_.getConfidence(fee, upstream.add.endorsement, currentBlockHeight, expiry)).getOrElse(0.0) + val outgoingConfidence = downstream_opt.flatMap(outgoingReputations.get).map(_.getConfidence(fee, Reputation.toEndorsement(incomingConfidence), currentBlockHeight, expiry)).getOrElse(0.0) + replyTo ! Reputation.Score(incomingConfidence, outgoingConfidence) + Behaviors.same + + case GetConfidence(replyTo, upstream: Upstream.Hot.Trampoline, downstream_opt, totalFee, currentBlockHeight, expiry) => + val incomingConfidence = + upstream.received + .groupMapReduce(_.receivedFrom)(r => (r.add.amountMsat, r.add.endorsement)) { + case ((amount1, endorsement1), (amount2, endorsement2)) => (amount1 + amount2, endorsement1 min endorsement2) + } + .map { + case (nodeId, (amount, endorsement)) => + val fee = amount * totalFee.toLong / upstream.amountIn.toLong + incomingReputations.get(nodeId).map(_.getConfidence(fee, endorsement, currentBlockHeight, expiry)).getOrElse(0.0) + } + .min + val outgoingConfidence = downstream_opt.flatMap(outgoingReputations.get).map(_.getConfidence(totalFee, Reputation.toEndorsement(incomingConfidence), currentBlockHeight, expiry)).getOrElse(0.0) + replyTo ! Reputation.Score(incomingConfidence, outgoingConfidence) + Behaviors.same + + case WrappedOutgoingHtlcAdded(OutgoingHtlcAdded(add, remoteNodeId, upstream, fee)) => + val htlcId = HtlcId(add) + upstream match { + case channel: Hot.Channel => + incomingReputations(channel.receivedFrom) = incomingReputations(channel.receivedFrom).addPendingHtlc(add, fee, channel.add.endorsement) + case trampoline: Hot.Trampoline => + trampoline.received + .groupMapReduce(_.receivedFrom)(r => (r.add.amountMsat, r.add.endorsement)) { + case ((amount1, endorsement1), (amount2, endorsement2)) => (amount1 + amount2, endorsement1 min endorsement2) + } + .foreach { case (nodeId, (amount, endorsement)) => + incomingReputations(nodeId) = incomingReputations(nodeId).addPendingHtlc(add, fee * amount.toLong / trampoline.amountIn.toLong, endorsement) + } + case _: Upstream.Local => () + } + outgoingReputations(remoteNodeId) = outgoingReputations(remoteNodeId).addPendingHtlc(add, fee, add.endorsement) + pending(htlcId) = PendingHtlc(add, upstream, remoteNodeId) + Behaviors.same + + case WrappedOutgoingHtlcSettled(settled) => + val htlcId = settled match { + case OutgoingHtlcFailed(UpdateFailHtlc(channelId, id, _, _)) => HtlcId(channelId, id) + case OutgoingHtlcFailed(UpdateFailMalformedHtlc(channelId, id, _, _, _)) => HtlcId(channelId, id) + case OutgoingHtlcFulfilled(fulfill) => HtlcId(fulfill.channelId, fulfill.id) + } + val isSuccess = settled match { + case _: OutgoingHtlcFailed => false + case _: OutgoingHtlcFulfilled => true + } + pending.remove(htlcId).foreach(p => { + p.upstream match { + case Hot.Channel(_, _, receivedFrom, _) => + incomingReputations(receivedFrom) = incomingReputations(receivedFrom).settlePendingHtlc(htlcId, isSuccess) + case Hot.Trampoline(received) => + received.foreach(channel => + incomingReputations(channel.receivedFrom) = incomingReputations(channel.receivedFrom).settlePendingHtlc(htlcId, isSuccess) + ) + case _: Upstream.Local => () + } + outgoingReputations(p.downstream) = outgoingReputations(p.downstream).settlePendingHtlc(htlcId, isSuccess) + }) + Behaviors.same + } +} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/Announcements.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/Announcements.scala index 80aee61d55..1809e7263b 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/Announcements.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/Announcements.scala @@ -18,10 +18,8 @@ package fr.acinq.eclair.router import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey, sha256, verifySignature} import fr.acinq.bitcoin.scalacompat.{BlockHash, ByteVector64, Crypto, LexicographicalOrdering} -import fr.acinq.eclair.channel.ChannelParams -import fr.acinq.eclair.payment.relay.Relayer.RelayFees import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{CltvExpiryDelta, Feature, Features, MilliSatoshi, NodeFeature, NodeParams, RealShortChannelId, ShortChannelId, TimestampSecond, TimestampSecondLong, serializationResult} +import fr.acinq.eclair.{CltvExpiryDelta, Feature, Features, MilliSatoshi, NodeFeature, RealShortChannelId, ShortChannelId, TimestampSecond, TimestampSecondLong, serializationResult} import scodec.bits.ByteVector import shapeless.HNil @@ -122,10 +120,6 @@ object Announcements { u1.htlcMinimumMsat == u2.htlcMinimumMsat && u1.htlcMaximumMsat == u2.htlcMaximumMsat - def makeChannelUpdate(nodeParams: NodeParams, remoteNodeId: PublicKey, scid: ShortChannelId, params: ChannelParams, relayFees: RelayFees, maxHtlcAmount: MilliSatoshi, enable: Boolean): ChannelUpdate = { - makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, scid, nodeParams.channelConf.expiryDelta, params.remoteParams.htlcMinimum, relayFees.feeBase, relayFees.feeProportionalMillionths, maxHtlcAmount, isPrivate = !params.announceChannel, enable) - } - def makeChannelUpdate(chainHash: BlockHash, nodeSecret: PrivateKey, remoteNodeId: PublicKey, shortChannelId: ShortChannelId, cltvExpiryDelta: CltvExpiryDelta, htlcMinimumMsat: MilliSatoshi, feeBaseMsat: MilliSatoshi, feeProportionalMillionths: Long, htlcMaximumMsat: MilliSatoshi, isPrivate: Boolean = false, enable: Boolean = true, timestamp: TimestampSecond = TimestampSecond.now()): ChannelUpdate = { val messageFlags = ChannelUpdate.MessageFlags(isPrivate) val channelFlags = ChannelUpdate.ChannelFlags(isNode1 = isNode1(nodeSecret.publicKey, remoteNodeId), isEnabled = enable) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/BalanceEstimate.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/BalanceEstimate.scala index 21e86b0af7..f59a003a29 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/BalanceEstimate.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/BalanceEstimate.scala @@ -24,7 +24,7 @@ import fr.acinq.eclair.router.Router.{ChannelDesc, ChannelHop, Route} import fr.acinq.eclair.wire.protocol.NodeAnnouncement import fr.acinq.eclair.{MilliSatoshi, MilliSatoshiLong, RealShortChannelId, ShortChannelId, TimestampSecond, TimestampSecondLong, ToMilliSatoshiConversion} -import scala.concurrent.duration.{DurationInt, FiniteDuration} +import scala.concurrent.duration.FiniteDuration /** * Estimates the balance between a pair of nodes @@ -216,7 +216,7 @@ case class BalanceEstimate private(low: MilliSatoshi, * - probability that it can relay a payment of high is decay(high, 0, highTimestamp) which is close to 0 if highTimestamp is recent * - probability that it can relay a payment of maxCapacity is 0 */ - def canSend(amount: MilliSatoshi, now: TimestampSecond)(implicit log: LoggingAdapter): Double = { + def canSendAndDerivative(amount: MilliSatoshi, now: TimestampSecond): (Double, Double) = { val a = amount.toLong.toDouble val l = low.toLong.toDouble val h = high.toLong.toDouble @@ -226,22 +226,18 @@ case class BalanceEstimate private(low: MilliSatoshi, val pLow = decay(low, 1, lowTimestamp, now) val pHigh = decay(high, 0, highTimestamp, now) - val estimate = if (amount < low) { - (l - a * (1.0 - pLow)) / l + if (amount < low) { + ((l - a * (1.0 - pLow)) / l, (pLow - 1.0) / l) } else if (amount < high) { - ((h - a) * pLow + (a - l) * pHigh) / (h - l) + (((h - a) * pLow + (a - l) * pHigh) / (h - l), (pHigh - pLow) / (h - l)) } else if (h < c) { - ((c - a) * pHigh) / (c - h) + (((c - a) * pHigh) / (c - h), (-pHigh) / (c - h)) } else { - 0 + (0.0, 0.0) } - - if (estimate < 0 || estimate > 1) { - log.error("Could not estimate balance: this={}, amount={}, now={}", this, amount, now) - } - - estimate } + + def canSend(amount: MilliSatoshi, now: TimestampSecond): Double = canSendAndDerivative(amount, now)._1 } object BalanceEstimate { @@ -254,6 +250,8 @@ object BalanceEstimate { case class BalancesEstimates(balances: Map[(PublicKey, PublicKey), BalanceEstimate], defaultHalfLife: FiniteDuration) { private def get(a: PublicKey, b: PublicKey): Option[BalanceEstimate] = balances.get((a, b)) + def get(edge: GraphEdge): BalanceEstimate = get(edge.desc.a, edge.desc.b).getOrElse(BalanceEstimate.empty(defaultHalfLife).addEdge(edge)) + def addEdge(edge: GraphEdge): BalancesEstimates = BalancesEstimates( balances.updatedWith((edge.desc.a, edge.desc.b))(balance => Some(balance.getOrElse(BalanceEstimate.empty(defaultHalfLife)).addEdge(edge)) @@ -314,7 +312,7 @@ case class BalancesEstimates(balances: Map[(PublicKey, PublicKey), BalanceEstima } -case class GraphWithBalanceEstimates(graph: DirectedGraph, private val balances: BalancesEstimates) { +case class GraphWithBalanceEstimates(graph: DirectedGraph, balances: BalancesEstimates) { def addOrUpdateVertex(ann: NodeAnnouncement): GraphWithBalanceEstimates = GraphWithBalanceEstimates(graph.addOrUpdateVertex(ann), balances) def addEdge(edge: GraphEdge): GraphWithBalanceEstimates = GraphWithBalanceEstimates(graph.addEdge(edge), balances.addEdge(edge)) @@ -356,13 +354,6 @@ case class GraphWithBalanceEstimates(graph: DirectedGraph, private val balances: def channelCouldNotSend(hop: ChannelHop, amount: MilliSatoshi)(implicit log: LoggingAdapter): GraphWithBalanceEstimates = { GraphWithBalanceEstimates(graph, balances.channelCouldNotSend(hop, amount)) } - - def canSend(amount: MilliSatoshi, edge: GraphEdge)(implicit log: LoggingAdapter): Double = { - balances.balances.get((edge.desc.a, edge.desc.b)) match { - case Some(estimate) => estimate.canSend(amount, TimestampSecond.now()) - case None => BalanceEstimate.empty(1 hour).addEdge(edge).canSend(amount, TimestampSecond.now()) - } - } } object GraphWithBalanceEstimates { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/BlindedRouteCreation.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/BlindedRouteCreation.scala index 8e2e2696c8..7b07aba128 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/BlindedRouteCreation.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/BlindedRouteCreation.scala @@ -28,7 +28,7 @@ object BlindedRouteCreation { /** Compute aggregated fees and expiry for a given route. */ def aggregatePaymentInfo(amount: MilliSatoshi, hops: Seq[ChannelHop], minFinalCltvExpiryDelta: CltvExpiryDelta): PaymentInfo = { - val zeroPaymentInfo = PaymentInfo(0 msat, 0, minFinalCltvExpiryDelta, 0 msat, amount, Features.empty) + val zeroPaymentInfo = PaymentInfo(0 msat, 0, minFinalCltvExpiryDelta, 0 msat, amount, ByteVector.empty) hops.foldRight(zeroPaymentInfo) { case (channel, payInfo) => val newFeeBase = MilliSatoshi((channel.params.relayFees.feeBase.toLong * 1_000_000 + payInfo.feeBase.toLong * (1_000_000 + channel.params.relayFees.feeProportionalMillionths) + 1_000_000 - 1) / 1_000_000) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/Graph.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/Graph.scala index 832fc0d11a..13d98e2c8a 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/Graph.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/Graph.scala @@ -17,11 +17,11 @@ package fr.acinq.eclair.router import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey -import fr.acinq.bitcoin.scalacompat.{Btc, BtcDouble, MilliBtc, Satoshi} +import fr.acinq.bitcoin.scalacompat.{Btc, MilliBtc, Satoshi} import fr.acinq.eclair._ import fr.acinq.eclair.payment.Invoice import fr.acinq.eclair.payment.relay.Relayer.RelayFees -import fr.acinq.eclair.router.Graph.GraphStructure.{DirectedGraph, GraphEdge} +import fr.acinq.eclair.router.Graph.GraphStructure.GraphEdge import fr.acinq.eclair.router.Router._ import fr.acinq.eclair.wire.protocol.{ChannelUpdate, NodeAnnouncement} @@ -84,58 +84,7 @@ object Graph { * @param currentBlockHeight the height of the chain tip (latest block). * @param includeLocalChannelCost if the path is for relaying and we need to include the cost of the local channel */ - def addEdgeWeight(sender: PublicKey, edge: GraphEdge, prev: RichWeight, currentBlockHeight: BlockHeight, includeLocalChannelCost: Boolean): RichWeight - } - - /** - * We use heuristics to calculate the weight of an edge based on channel age, cltv delta, capacity and a virtual hop cost to keep routes short. - * We favor older channels, with bigger capacity and small cltv delta. - */ - case class PaymentWeightRatios(baseFactor: Double, cltvDeltaFactor: Double, ageFactor: Double, capacityFactor: Double, hopFees: RelayFees) extends WeightRatios[PaymentPathWeight] { - require(baseFactor + cltvDeltaFactor + ageFactor + capacityFactor == 1, "The sum of heuristics ratios must be 1") - require(baseFactor >= 0.0, "ratio-base must be nonnegative") - require(cltvDeltaFactor >= 0.0, "ratio-cltv must be nonnegative") - require(ageFactor >= 0.0, "ratio-channel-age must be nonnegative") - require(capacityFactor >= 0.0, "ratio-channel-capacity must be nonnegative") - - override def addEdgeWeight(sender: PublicKey, edge: GraphEdge, prev: PaymentPathWeight, currentBlockHeight: BlockHeight, includeLocalChannelCost: Boolean): PaymentPathWeight = { - val totalAmount = if (edge.desc.a == sender && !includeLocalChannelCost) prev.amount else addEdgeFees(edge, prev.amount) - val fee = totalAmount - prev.amount - val totalFees = prev.fees + fee - val cltv = if (edge.desc.a == sender && !includeLocalChannelCost) CltvExpiryDelta(0) else edge.params.cltvExpiryDelta - val totalCltv = prev.cltv + cltv - val hopCost = if (edge.desc.a == sender) 0 msat else nodeFee(hopFees, prev.amount) - import RoutingHeuristics._ - - // Every edge is weighted by funding block height where older blocks add less weight. The window considered is 1 year. - val ageFactor = edge.desc.shortChannelId match { - case real: RealShortChannelId => normalize(real.blockHeight.toDouble, min = (currentBlockHeight - BLOCK_TIME_ONE_YEAR).toDouble, max = currentBlockHeight.toDouble) - // for local channels or route hints we don't easily have access to the channel block height, but we want to - // give them the best score anyway - case _: Alias => 1 - case _: UnspecifiedShortChannelId => 1 - } - - // Every edge is weighted by channel capacity, larger channels add less weight - val edgeMaxCapacity = edge.capacity.toMilliSatoshi - val capFactor = - if (edge.balance_opt.isDefined) 0 // If we know the balance of the channel we treat it as if it had the maximum capacity. - else 1 - normalize(edgeMaxCapacity.toLong.toDouble, CAPACITY_CHANNEL_LOW.toLong.toDouble, CAPACITY_CHANNEL_HIGH.toLong.toDouble) - - // Every edge is weighted by its cltv-delta value, normalized - val cltvFactor = normalize(edge.params.cltvExpiryDelta.toInt, CLTV_LOW, CLTV_HIGH) - - // NB we're guaranteed to have weightRatios and factors > 0 - val factor = baseFactor + (cltvFactor * this.cltvDeltaFactor) + (ageFactor * this.ageFactor) + (capFactor * this.capacityFactor) - val totalWeight = prev.weight + (fee + hopCost).toLong * factor - val richWeight = PaymentPathWeight(totalAmount, prev.length + 1, totalCltv, 1.0, totalFees, 0 msat, totalWeight) - if (edge.desc.a == sender) { - // If this is a local channel it shouldn't add any weight. We always prefer local channels. - richWeight.copy(weight = prev.weight) - } else { - richWeight - } - } + def addEdgeWeight(sender: PublicKey, edge: GraphEdge, balance: BalanceEstimate, prev: RichWeight, currentBlockHeight: BlockHeight, includeLocalChannelCost: Boolean): RichWeight } /** @@ -143,28 +92,35 @@ object Graph { * The fee for a failed attempt and the fee per hop are never actually spent, they are used to incentivize shorter * paths or path with higher success probability. * - * @param lockedFundsRisk cost of having funds locked in htlc in msat per msat per block - * @param failureFees fee for a failed attempt - * @param hopFees virtual fee per hop (how much we're willing to pay to make the route one hop shorter) + * @param lockedFundsRisk cost of having funds locked in htlc in msat per msat per block + * @param failureFees fee for a failed attempt + * @param hopFees virtual fee per hop (how much we're willing to pay to make the route one hop shorter) + * @param usePastRelaysData use data from past relays to estimate the balance of the channels */ - case class HeuristicsConstants(lockedFundsRisk: Double, failureFees: RelayFees, hopFees: RelayFees, useLogProbability: Boolean) extends WeightRatios[PaymentPathWeight] { - override def addEdgeWeight(sender: PublicKey, edge: GraphEdge, prev: PaymentPathWeight, currentBlockHeight: BlockHeight, includeLocalChannelCost: Boolean): PaymentPathWeight = { + case class HeuristicsConstants(lockedFundsRisk: Double, failureFees: RelayFees, hopFees: RelayFees, useLogProbability: Boolean, usePastRelaysData: Boolean) extends WeightRatios[PaymentPathWeight] { + override def addEdgeWeight(sender: PublicKey, edge: GraphEdge, balance: BalanceEstimate, prev: PaymentPathWeight, currentBlockHeight: BlockHeight, includeLocalChannelCost: Boolean): PaymentPathWeight = { val totalAmount = if (edge.desc.a == sender && !includeLocalChannelCost) prev.amount else addEdgeFees(edge, prev.amount) val fee = totalAmount - prev.amount val totalFees = prev.fees + fee - val cltv = if (edge.desc.a == sender && !includeLocalChannelCost) CltvExpiryDelta(0) else edge.params.cltvExpiryDelta - val totalCltv = prev.cltv + cltv + val totalCltv = prev.cltv + edge.params.cltvExpiryDelta val hopCost = nodeFee(hopFees, prev.amount) val totalHopsCost = prev.virtualFees + hopCost // If we know the balance of the channel, then we will check separately that it can relay the payment. - val successProbability = if (edge.balance_opt.nonEmpty) 1.0 else 1.0 - prev.amount.toLong.toDouble / edge.capacity.toMilliSatoshi.toLong.toDouble + val successProbability = + if (edge.balance_opt.nonEmpty) { + 1.0 + } else if (usePastRelaysData) { + balance.canSend(prev.amount, TimestampSecond.now()) + } else { + 1.0 - prev.amount.toLong.toDouble / edge.capacity.toMilliSatoshi.toLong.toDouble + } if (successProbability < 0) { throw NegativeProbability(edge, prev, this) } val totalSuccessProbability = prev.successProbability * successProbability val failureCost = nodeFee(failureFees, totalAmount) val richWeight = if (useLogProbability) { - val riskCost = totalAmount.toLong * cltv.toInt * lockedFundsRisk + val riskCost = totalAmount.toLong * edge.params.cltvExpiryDelta.toInt * lockedFundsRisk val weight = prev.weight + fee.toLong + hopCost.toLong + riskCost - failureCost.toLong * math.log(successProbability) PaymentPathWeight(totalAmount, prev.length + 1, totalCltv, totalSuccessProbability, totalFees, totalHopsCost, weight) } else { @@ -187,7 +143,7 @@ object Graph { require(ageFactor >= 0.0, "ratio-channel-age must be nonnegative") require(capacityFactor >= 0.0, "ratio-channel-capacity must be nonnegative") - override def addEdgeWeight(sender: PublicKey, edge: GraphEdge, prev: MessagePathWeight, currentBlockHeight: BlockHeight, includeLocalChannelCost: Boolean): MessagePathWeight = { + override def addEdgeWeight(sender: PublicKey, edge: GraphEdge, balance: BalanceEstimate, prev: MessagePathWeight, currentBlockHeight: BlockHeight, includeLocalChannelCost: Boolean): MessagePathWeight = { import RoutingHeuristics._ // Every edge is weighted by funding block height where older blocks add less weight. The window considered is 1 year. @@ -245,7 +201,7 @@ object Graph { * @param boundaries a predicate function that can be used to impose limits on the outcome of the search * @param includeLocalChannelCost if the path is for relaying and we need to include the cost of the local channel */ - def yenKshortestPaths(graph: DirectedGraph, + def yenKshortestPaths(g: GraphWithBalanceEstimates, sourceNode: PublicKey, targetNode: PublicKey, amount: MilliSatoshi, @@ -259,7 +215,7 @@ object Graph { includeLocalChannelCost: Boolean): Seq[WeightedPath[PaymentPathWeight]] = { // find the shortest path (k = 0) val targetWeight = PaymentPathWeight(amount) - dijkstraShortestPath(graph, sourceNode, targetNode, ignoredEdges, ignoredVertices, extraEdges, targetWeight, boundaries, Features.empty, currentBlockHeight, wr, includeLocalChannelCost) match { + dijkstraShortestPath(g, sourceNode, targetNode, ignoredEdges, ignoredVertices, extraEdges, targetWeight, boundaries, Features.empty, currentBlockHeight, wr, includeLocalChannelCost) match { case None => Seq.empty // if we can't even find a single path, avoid returning a Seq(Seq.empty) case Some(shortestPath) => @@ -270,7 +226,7 @@ object Graph { var allSpurPathsFound = false val shortestPaths = new mutable.Queue[PathWithSpur] - shortestPaths.enqueue(PathWithSpur(WeightedPath(shortestPath, pathWeight(sourceNode, shortestPath, amount, currentBlockHeight, wr, includeLocalChannelCost)), 0)) + shortestPaths.enqueue(PathWithSpur(WeightedPath(shortestPath, pathWeight(g.balances, sourceNode, shortestPath, amount, currentBlockHeight, wr, includeLocalChannelCost)), 0)) // stores the candidates for the k-th shortest path, sorted by path cost val candidates = new mutable.PriorityQueue[PathWithSpur] @@ -295,12 +251,12 @@ object Graph { val alreadyExploredEdges = shortestPaths.collect { case p if p.p.path.takeRight(i) == rootPathEdges => p.p.path(p.p.path.length - 1 - i).desc }.toSet // we also want to ignore any vertex on the root path to prevent loops val alreadyExploredVertices = rootPathEdges.map(_.desc.b).toSet - val rootPathWeight = pathWeight(sourceNode, rootPathEdges, amount, currentBlockHeight, wr, includeLocalChannelCost) + val rootPathWeight = pathWeight(g.balances, sourceNode, rootPathEdges, amount, currentBlockHeight, wr, includeLocalChannelCost) // find the "spur" path, a sub-path going from the spur node to the target avoiding previously found sub-paths - dijkstraShortestPath(graph, sourceNode, spurNode, ignoredEdges ++ alreadyExploredEdges, ignoredVertices ++ alreadyExploredVertices, extraEdges, rootPathWeight, boundaries, Features.empty, currentBlockHeight, wr, includeLocalChannelCost) match { + dijkstraShortestPath(g, sourceNode, spurNode, ignoredEdges ++ alreadyExploredEdges, ignoredVertices ++ alreadyExploredVertices, extraEdges, rootPathWeight, boundaries, Features.empty, currentBlockHeight, wr, includeLocalChannelCost) match { case Some(spurPath) => val completePath = spurPath ++ rootPathEdges - val candidatePath = WeightedPath(completePath, pathWeight(sourceNode, completePath, amount, currentBlockHeight, wr, includeLocalChannelCost)) + val candidatePath = WeightedPath(completePath, pathWeight(g.balances, sourceNode, completePath, amount, currentBlockHeight, wr, includeLocalChannelCost)) candidates.enqueue(PathWithSpur(candidatePath, i)) case None => () } @@ -338,7 +294,7 @@ object Graph { * @param wr ratios used to 'weight' edges when searching for the shortest path * @param includeLocalChannelCost if the path is for relaying and we need to include the cost of the local channel */ - private def dijkstraShortestPath[RichWeight <: PathWeight](g: DirectedGraph, + private def dijkstraShortestPath[RichWeight <: PathWeight](g: GraphWithBalanceEstimates, sourceNode: PublicKey, targetNode: PublicKey, ignoredEdges: Set[ChannelDesc], @@ -351,8 +307,8 @@ object Graph { wr: WeightRatios[RichWeight], includeLocalChannelCost: Boolean): Option[Seq[GraphEdge]] = { // the graph does not contain source/destination nodes - val sourceNotInGraph = !g.containsVertex(sourceNode) && !extraEdges.exists(_.desc.a == sourceNode) - val targetNotInGraph = !g.containsVertex(targetNode) && !extraEdges.exists(_.desc.b == targetNode) + val sourceNotInGraph = !g.graph.containsVertex(sourceNode) && !extraEdges.exists(_.desc.a == sourceNode) + val targetNotInGraph = !g.graph.containsVertex(targetNode) && !extraEdges.exists(_.desc.b == targetNode) if (sourceNotInGraph || targetNotInGraph) { return None } @@ -381,17 +337,17 @@ object Graph { val neighborEdges = { val extraNeighbors = extraEdges.filter(_.desc.b == current.key) // the resulting set must have only one element per shortChannelId; we prioritize extra edges - g.getIncomingEdgesOf(current.key).collect{case e: GraphEdge if !extraNeighbors.exists(_.desc.shortChannelId == e.desc.shortChannelId) => e} ++ extraNeighbors + g.graph.getIncomingEdgesOf(current.key).collect { case e: GraphEdge if !extraNeighbors.exists(_.desc.shortChannelId == e.desc.shortChannelId) => e } ++ extraNeighbors } neighborEdges.foreach { edge => val neighbor = edge.desc.a if (current.weight.canUseEdge(edge) && !ignoredEdges.contains(edge.desc) && !ignoredVertices.contains(neighbor) && - (neighbor == sourceNode || g.getVertexFeatures(neighbor).areSupported(nodeFeatures))) { + (neighbor == sourceNode || g.graph.getVertexFeatures(neighbor).areSupported(nodeFeatures))) { // NB: this contains the amount (including fees) that will need to be sent to `neighbor`, but the amount that // will be relayed through that edge is the one in `currentWeight`. - val neighborWeight = wr.addEdgeWeight(sourceNode, edge, current.weight, currentBlockHeight, includeLocalChannelCost) + val neighborWeight = wr.addEdgeWeight(sourceNode, edge, g.balances.get(edge), current.weight, currentBlockHeight, includeLocalChannelCost) if (boundaries(neighborWeight)) { val previousNeighborWeight = bestWeights.get(neighbor) // if this path between neighbor and the target has a shorter distance than previously known, we select it @@ -425,7 +381,7 @@ object Graph { } } - def dijkstraMessagePath(g: DirectedGraph, + def dijkstraMessagePath(g: GraphWithBalanceEstimates, sourceNode: PublicKey, targetNode: PublicKey, ignoredVertices: Set[PublicKey], @@ -440,7 +396,7 @@ object Graph { * * @param pathsToFind Number of paths to find. We may return fewer paths if we couldn't find more non-overlapping ones. */ - def routeBlindingPaths(graph: DirectedGraph, + def routeBlindingPaths(g: GraphWithBalanceEstimates, sourceNode: PublicKey, targetNode: PublicKey, amount: MilliSatoshi, @@ -454,9 +410,9 @@ object Graph { val verticesToIgnore = new mutable.HashSet[PublicKey]() verticesToIgnore.addAll(ignoredVertices) for (_ <- 1 to pathsToFind) { - dijkstraShortestPath(graph, sourceNode, targetNode, ignoredEdges, verticesToIgnore.toSet, extraEdges = Set.empty, PaymentPathWeight(amount), boundaries, Features(Features.RouteBlinding -> FeatureSupport.Mandatory), currentBlockHeight, wr, includeLocalChannelCost = true) match { + dijkstraShortestPath(g, sourceNode, targetNode, ignoredEdges, verticesToIgnore.toSet, extraEdges = Set.empty, PaymentPathWeight(amount), boundaries, Features(Features.RouteBlinding -> FeatureSupport.Mandatory), currentBlockHeight, wr, includeLocalChannelCost = true) match { case Some(path) => - val weight = pathWeight(sourceNode, path, amount, currentBlockHeight, wr, includeLocalChannelCost = true) + val weight = pathWeight(g.balances, sourceNode, path, amount, currentBlockHeight, wr, includeLocalChannelCost = true) paths += WeightedPath(path, weight) // Additional paths must keep using the source and target nodes, but shouldn't use any of the same intermediate nodes. verticesToIgnore.addAll(path.drop(1).map(_.desc.a)) @@ -503,24 +459,24 @@ object Graph { * @param wr ratios used to 'weight' edges when searching for the shortest path * @param includeLocalChannelCost if the path is for relaying and we need to include the cost of the local channel */ - def pathWeight(sender: PublicKey, path: Seq[GraphEdge], amount: MilliSatoshi, currentBlockHeight: BlockHeight, wr: WeightRatios[PaymentPathWeight], includeLocalChannelCost: Boolean): PaymentPathWeight = { + def pathWeight(balances: BalancesEstimates, sender: PublicKey, path: Seq[GraphEdge], amount: MilliSatoshi, currentBlockHeight: BlockHeight, wr: WeightRatios[PaymentPathWeight], includeLocalChannelCost: Boolean): PaymentPathWeight = { path.foldRight(PaymentPathWeight(amount)) { (edge, prev) => - wr.addEdgeWeight(sender, edge, prev, currentBlockHeight, includeLocalChannelCost) + wr.addEdgeWeight(sender, edge, balances.get(edge), prev, currentBlockHeight, includeLocalChannelCost) } } object RoutingHeuristics { // Number of blocks in one year - val BLOCK_TIME_ONE_YEAR = 365 * 24 * 6 + val BLOCK_TIME_ONE_YEAR: Int = 365 * 24 * 6 // Low/High bound for channel capacity - val CAPACITY_CHANNEL_LOW = MilliBtc(1).toMilliSatoshi - val CAPACITY_CHANNEL_HIGH = Btc(1).toMilliSatoshi + val CAPACITY_CHANNEL_LOW: MilliSatoshi = MilliBtc(1).toMilliSatoshi + val CAPACITY_CHANNEL_HIGH: MilliSatoshi = Btc(1).toMilliSatoshi // Low/High bound for CLTV channel value - val CLTV_LOW = 9 - val CLTV_HIGH = 2016 + val CLTV_LOW: Int = 9 + val CLTV_HIGH: Int = 2016 /** * Normalize the given value between (0, 1). If the @param value is outside the min/max window we flatten it to something very close to the @@ -545,13 +501,6 @@ object Graph { * @param balance_opt (optional) available balance that can be sent through this edge */ case class GraphEdge private(desc: ChannelDesc, params: HopRelayParams, capacity: Satoshi, balance_opt: Option[MilliSatoshi]) { - - def maxHtlcAmount(reservedCapacity: MilliSatoshi): MilliSatoshi = Seq( - balance_opt.map(balance => balance - reservedCapacity), - params.htlcMaximum_opt, - Some(capacity.toMilliSatoshi - reservedCapacity) - ).flatten.min.max(0 msat) - def fee(amount: MilliSatoshi): MilliSatoshi = params.fee(amount) } @@ -571,12 +520,11 @@ object Graph { ) def apply(e: Invoice.ExtraEdge): GraphEdge = { - val maxBtc = 21e6.btc GraphEdge( desc = ChannelDesc(e.shortChannelId, e.sourceNodeId, e.targetNodeId), params = HopRelayParams.FromHint(e), // Routing hints don't include the channel's capacity, so we assume it's big enough. - capacity = maxBtc.toSatoshi, + capacity = MilliSatoshi.MaxMoney.truncateToSatoshi, balance_opt = None, ) } @@ -648,9 +596,9 @@ object Graph { * Update the shortChannelId and capacity of edges corresponding to the given channel-desc, * both edges (corresponding to both directions) are updated. * - * @param desc the channel description for the channel to update + * @param desc the channel description for the channel to update * @param newShortChannelId the new shortChannelId for this channel - * @param newCapacity the new capacity of the channel + * @param newCapacity the new capacity of the channel * @return a new graph with updated vertexes */ def updateChannel(desc: ChannelDesc, newShortChannelId: RealShortChannelId, newCapacity: Satoshi): DirectedGraph = { @@ -698,8 +646,6 @@ object Graph { DirectedGraph(removeChannels(channels).vertices - key) } - def removeVertices(nodeIds: Iterable[PublicKey]): DirectedGraph = nodeIds.foldLeft(this)((acc, nodeId) => acc.removeVertex(nodeId)) - /** * Adds a new vertex to the graph, starting with no edges. * Or update the node features if the vertex is already present. diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/RouteCalculation.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/RouteCalculation.scala index c3df92971c..cba5f42684 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/RouteCalculation.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/RouteCalculation.scala @@ -198,9 +198,9 @@ object RouteCalculation { val tags = TagSet.Empty.withTag(Tags.MultiPart, r.allowMultiPart).withTag(Tags.Amount, Tags.amountBucket(amountToSend)) KamonExt.time(Metrics.FindRouteDuration.withTags(tags.withTag(Tags.NumberOfRoutes, routesToFind.toLong))) { val result = if (r.allowMultiPart) { - findMultiPartRoute(d.graphWithBalances.graph, r.source, targetNodeId, amountToSend, maxFee, extraEdges, ignoredEdges, r.ignore.nodes, r.pendingPayments, r.routeParams, currentBlockHeight) + findMultiPartRoute(d.graphWithBalances, r.source, targetNodeId, amountToSend, maxFee, extraEdges, ignoredEdges, r.ignore.nodes, r.pendingPayments, r.routeParams, currentBlockHeight) } else { - findRoute(d.graphWithBalances.graph, r.source, targetNodeId, amountToSend, maxFee, routesToFind, extraEdges, ignoredEdges, r.ignore.nodes, r.routeParams, currentBlockHeight) + findRoute(d.graphWithBalances, r.source, targetNodeId, amountToSend, maxFee, routesToFind, extraEdges, ignoredEdges, r.ignore.nodes, r.routeParams, currentBlockHeight) } result.map(routes => addFinalHop(r.target, routes)) match { case Success(routes) => @@ -236,7 +236,7 @@ object RouteCalculation { weight.length <= ROUTE_MAX_LENGTH && weight.cltv <= r.routeParams.boundaries.maxCltv } - val routes = Graph.routeBlindingPaths(d.graphWithBalances.graph, r.source, r.target, r.amount, r.ignore.channels, r.ignore.nodes, r.pathsToFind, r.routeParams.heuristics, currentBlockHeight, boundaries) + val routes = Graph.routeBlindingPaths(d.graphWithBalances, r.source, r.target, r.amount, r.ignore.channels, r.ignore.nodes, r.pathsToFind, r.routeParams.heuristics, currentBlockHeight, boundaries) if (routes.isEmpty) { r.replyTo ! PaymentRouteNotFound(RouteNotFound) } else { @@ -250,7 +250,7 @@ object RouteCalculation { weight.length <= routeParams.maxRouteLength && weight.length <= ROUTE_MAX_LENGTH } log.info("finding route for onion messages {} -> {}", r.source, r.target) - Graph.dijkstraMessagePath(d.graphWithBalances.graph, r.source, r.target, r.ignoredNodes, boundaries, currentBlockHeight, routeParams.ratios) match { + Graph.dijkstraMessagePath(d.graphWithBalances, r.source, r.target, r.ignoredNodes, boundaries, currentBlockHeight, routeParams.ratios) match { case Some(path) => val intermediateNodes = path.map(_.desc.a).drop(1) log.info("found route for onion messages {}", (r.source +: intermediateNodes :+ r.target).mkString(" -> ")) @@ -300,7 +300,7 @@ object RouteCalculation { * @param routeParams a set of parameters that can restrict the route search * @return the computed routes to the destination @param targetNodeId */ - def findRoute(g: DirectedGraph, + def findRoute(g: GraphWithBalanceEstimates, localNodeId: PublicKey, targetNodeId: PublicKey, amount: MilliSatoshi, @@ -318,7 +318,7 @@ object RouteCalculation { } @tailrec - private def findRouteInternal(g: DirectedGraph, + private def findRouteInternal(g: GraphWithBalanceEstimates, localNodeId: PublicKey, targetNodeId: PublicKey, amount: MilliSatoshi, @@ -379,7 +379,7 @@ object RouteCalculation { * @param routeParams a set of parameters that can restrict the route search * @return a set of disjoint routes to the destination @param targetNodeId with the payment amount split between them */ - def findMultiPartRoute(g: DirectedGraph, + def findMultiPartRoute(g: GraphWithBalanceEstimates, localNodeId: PublicKey, targetNodeId: PublicKey, amount: MilliSatoshi, @@ -390,20 +390,13 @@ object RouteCalculation { pendingHtlcs: Seq[Route] = Nil, routeParams: RouteParams, currentBlockHeight: BlockHeight): Try[Seq[Route]] = Try { - val result = findMultiPartRouteInternal(g, localNodeId, targetNodeId, amount, maxFee, extraEdges, ignoredEdges, ignoredVertices, pendingHtlcs, routeParams, currentBlockHeight) match { - case Right(routes) => Right(routes) - case Left(RouteNotFound) if routeParams.randomize => - // If we couldn't find a randomized solution, fallback to a deterministic one. - findMultiPartRouteInternal(g, localNodeId, targetNodeId, amount, maxFee, extraEdges, ignoredEdges, ignoredVertices, pendingHtlcs, routeParams.copy(randomize = false), currentBlockHeight) - case Left(ex) => Left(ex) - } - result match { + findMultiPartRouteInternal(g, localNodeId, targetNodeId, amount, maxFee, extraEdges, ignoredEdges, ignoredVertices, pendingHtlcs, routeParams, currentBlockHeight) match { case Right(routes) => routes case Left(ex) => return Failure(ex) } } - private def findMultiPartRouteInternal(g: DirectedGraph, + private def findMultiPartRouteInternal(g: GraphWithBalanceEstimates, localNodeId: PublicKey, targetNodeId: PublicKey, amount: MilliSatoshi, @@ -413,12 +406,13 @@ object RouteCalculation { ignoredVertices: Set[PublicKey] = Set.empty, pendingHtlcs: Seq[Route] = Nil, routeParams: RouteParams, - currentBlockHeight: BlockHeight): Either[RouterException, Seq[Route]] = { + currentBlockHeight: BlockHeight, + now: TimestampSecond = TimestampSecond.now()): Either[RouterException, Seq[Route]] = { // We use Yen's k-shortest paths to find many paths for chunks of the total amount. // When the recipient is a direct peer, we have complete visibility on our local channels so we can use more accurate MPP parameters. val routeParams1 = { case class DirectChannel(balance: MilliSatoshi, isEmpty: Boolean) - val directChannels = g.getEdgesBetween(localNodeId, targetNodeId).collect { + val directChannels = g.graph.getEdgesBetween(localNodeId, targetNodeId).collect { // We should always have balance information available for local channels. // NB: htlcMinimumMsat is set by our peer and may be 0 msat (even though it's not recommended). case GraphEdge(_, params, _, Some(balance)) => DirectChannel(balance, balance <= 0.msat || balance < params.htlcMinimum) @@ -429,63 +423,124 @@ object RouteCalculation { // We want to ensure that the set of routes we find have enough capacity to allow sending the total amount, // without excluding routes with small capacity when the total amount is small. val minPartAmount = routeParams.mpp.minPartAmount.max(amount / numRoutes).min(amount) - routeParams.copy(mpp = MultiPartParams(minPartAmount, numRoutes)) + routeParams.copy(mpp = MultiPartParams(minPartAmount, numRoutes, routeParams.mpp.splittingStrategy)) } findRouteInternal(g, localNodeId, targetNodeId, routeParams1.mpp.minPartAmount, maxFee, routeParams1.mpp.maxParts, extraEdges, ignoredEdges, ignoredVertices, routeParams1, currentBlockHeight) match { - case Right(routes) => + case Right(paths) => // We use these shortest paths to find a set of non-conflicting HTLCs that send the total amount. - split(amount, mutable.Queue(routes: _*), initializeUsedCapacity(pendingHtlcs), routeParams1) match { + split(amount, mutable.Queue(paths: _*), initializeUsedCapacity(pendingHtlcs), routeParams1, g.balances, now) match { case Right(routes) if validateMultiPartRoute(amount, maxFee, routes, routeParams.includeLocalChannelCost) => Right(routes) + case Right(_) if routeParams.randomize => + // We've found a multipart route, but it's too expensive. We try again without randomization to prioritize cheaper paths. + val sortedPaths = paths.sortBy(_.weight.weight) + split(amount, mutable.Queue(sortedPaths: _*), initializeUsedCapacity(pendingHtlcs), routeParams1, g.balances, now) match { + case Right(routes) if validateMultiPartRoute(amount, maxFee, routes, routeParams.includeLocalChannelCost) => Right(routes) + case _ => Left(RouteNotFound) + } case _ => Left(RouteNotFound) } case Left(ex) => Left(ex) } } - @tailrec - private def split(amount: MilliSatoshi, paths: mutable.Queue[WeightedPath[PaymentPathWeight]], usedCapacity: mutable.Map[ShortChannelId, MilliSatoshi], routeParams: RouteParams, selectedRoutes: Seq[Route] = Nil): Either[RouterException, Seq[Route]] = { - if (amount == 0.msat) { - Right(selectedRoutes) - } else if (paths.isEmpty) { - Left(RouteNotFound) - } else { + private case class CandidateRoute(route: Route, maxAmount: MilliSatoshi) + + private def split(amount: MilliSatoshi, paths: mutable.Queue[WeightedPath[PaymentPathWeight]], usedCapacity: mutable.Map[ShortChannelId, MilliSatoshi], routeParams: RouteParams, balances: BalancesEstimates, now: TimestampSecond): Either[RouterException, Seq[Route]] = { + var amountLeft = amount + var candidates: List[CandidateRoute] = Nil + // We build some candidate route but may adjust the amounts later if the don't cover the full amount we need to send. + while(paths.nonEmpty && amountLeft > 0.msat) { val current = paths.dequeue() val candidate = computeRouteMaxAmount(current.path, usedCapacity) - if (candidate.amount < routeParams.mpp.minPartAmount.min(amount)) { - // this route doesn't have enough capacity left: we remove it and continue. - split(amount, paths, usedCapacity, routeParams, selectedRoutes) - } else { - val route = if (routeParams.randomize) { - // randomly choose the amount to be between 20% and 100% of the available capacity. - val randomizedAmount = candidate.amount * ((20d + Random.nextInt(81)) / 100) - if (randomizedAmount < routeParams.mpp.minPartAmount) { - candidate.copy(amount = routeParams.mpp.minPartAmount.min(amount)) - } else { - candidate.copy(amount = randomizedAmount.min(amount)) - } + if (candidate.amount >= routeParams.mpp.minPartAmount.min(amountLeft)) { + val chosenAmount = routeParams.mpp.splittingStrategy match { + case MultiPartParams.Randomize => + // randomly choose the amount to be between 20% and 100% of the available capacity. + val randomizedAmount = candidate.amount * ((20d + Random.nextInt(81)) / 100) + randomizedAmount.max(routeParams.mpp.minPartAmount).min(amountLeft) + case MultiPartParams.MaxExpectedAmount => + val bestAmount = optimizeExpectedValue(current.path, candidate.amount, usedCapacity, routeParams.heuristics.usePastRelaysData, balances, now) + bestAmount.max(routeParams.mpp.minPartAmount).min(amountLeft) + case MultiPartParams.FullCapacity => + candidate.amount.min(amountLeft) + } + // We update the route with our chosen amount, which is always smaller than the maximum amount. + val chosenRoute = CandidateRoute(candidate.copy(amount = chosenAmount), candidate.amount) + // But we use the route with its maximum amount when updating the used capacity, because we may use more funds below when adjusting the amounts. + updateUsedCapacity(candidate, usedCapacity) + candidates = chosenRoute :: candidates + amountLeft = amountLeft - chosenAmount + paths.enqueue(current) + } + } + // We adjust the amounts to send through each route. + val totalMaximum = candidates.map(_.maxAmount).sum + if (amountLeft == 0.msat) { + Right(candidates.map(_.route)) + } else if (totalMaximum < amount) { + Left(RouteNotFound) + } else { + val totalChosen = candidates.map(_.route.amount).sum + val additionalFraction = (amount - totalChosen).toLong.toDouble / (totalMaximum - totalChosen).toLong.toDouble + var routes: List[Route] = Nil + var amountLeft = amount + candidates.foreach { case CandidateRoute(route, maxAmount) => + if (amountLeft > 0.msat) { + val additionalAmount = MilliSatoshi(((maxAmount - route.amount).toLong * additionalFraction).ceil.toLong) + val amountToSend = (route.amount + additionalAmount).min(amountLeft) + routes = route.copy(amount = amountToSend) :: routes + amountLeft = amountLeft - amountToSend + } + } + Right(routes) + } + } + + private def optimizeExpectedValue(route: Seq[GraphEdge], capacity: MilliSatoshi, usedCapacity: mutable.Map[ShortChannelId, MilliSatoshi], usePastRelaysData: Boolean, balances: BalancesEstimates, now: TimestampSecond): MilliSatoshi = { + // We search the maximum value of a polynomial between its two smallest roots (0 and the minimum channel capacity on the path). + // We use binary search to find where the derivative changes sign. + var low = 1L + var high = capacity.toLong + while (high - low > 1L) { + val x = (high + low) / 2 + val d = route.drop(1).foldLeft(1.0 / x.toDouble) { case (total, edge) => + // We compute the success probability `p` for this edge, and its derivative `dp`. + val (p, dp) = if (usePastRelaysData) { + balances.get(edge).canSendAndDerivative(MilliSatoshi(x), now) } else { - candidate.copy(amount = candidate.amount.min(amount)) + // If not using past relays data, we assume that balances are uniformly distributed between 0 and the full capacity of the channel. + val c = (edge.capacity - usedCapacity.getOrElse(edge.desc.shortChannelId, 0 msat)).toLong.toDouble + (1.0 - x.toDouble / c, -1.0 / c) } - updateUsedCapacity(route, usedCapacity) - // NB: we re-enqueue the current path, it may still have capacity for a second HTLC. - split(amount - route.amount, paths.enqueue(current), usedCapacity, routeParams, route +: selectedRoutes) + total + dp / p + } + if (d > 0.0) { + low = x + } else { + high = x } } + MilliSatoshi(high) } /** Compute the maximum amount that we can send through the given route. */ private def computeRouteMaxAmount(route: Seq[GraphEdge], usedCapacity: mutable.Map[ShortChannelId, MilliSatoshi]): Route = { - val firstHopMaxAmount = route.head.maxHtlcAmount(usedCapacity.getOrElse(route.head.desc.shortChannelId, 0 msat)) + val firstHopMaxAmount = maxEdgeAmount(route.head, usedCapacity.getOrElse(route.head.desc.shortChannelId, 0 msat)) val amount = route.drop(1).foldLeft(firstHopMaxAmount) { case (amount, edge) => // We compute fees going forward instead of backwards. That means we will slightly overestimate the fees of some // edges, but we will always stay inside the capacity bounds we computed. val amountMinusFees = amount - edge.fee(amount) - val edgeMaxAmount = edge.maxHtlcAmount(usedCapacity.getOrElse(edge.desc.shortChannelId, 0 msat)) + val edgeMaxAmount = maxEdgeAmount(edge, usedCapacity.getOrElse(edge.desc.shortChannelId, 0 msat)) amountMinusFees.min(edgeMaxAmount) } Route(amount.max(0 msat), route.map(graphEdgeToHop), None) } + private def maxEdgeAmount(edge: GraphEdge, usedCapacity: MilliSatoshi): MilliSatoshi = { + val maxBalance = edge.balance_opt.getOrElse(edge.params.htlcMaximum_opt.getOrElse(edge.capacity.toMilliSatoshi)) + Seq(Some(maxBalance - usedCapacity), edge.params.htlcMaximum_opt).flatten.min.max(0 msat) + } + /** Initialize known used capacity based on pending HTLCs. */ private def initializeUsedCapacity(pendingHtlcs: Seq[Route]): mutable.Map[ShortChannelId, MilliSatoshi] = { val usedCapacity = mutable.Map.empty[ShortChannelId, MilliSatoshi] @@ -497,7 +552,14 @@ object RouteCalculation { /** Update used capacity by taking into account an HTLC sent to the given route. */ private def updateUsedCapacity(route: Route, usedCapacity: mutable.Map[ShortChannelId, MilliSatoshi]): Unit = { - route.hops.foldRight(route.amount) { case (hop, amount) => + val finalHopAmount = route.finalHop_opt.map(hop => { + hop match { + case BlindedHop(dummyId, _) => usedCapacity.updateWith(dummyId)(previous => Some(route.amount + previous.getOrElse(0 msat))) + case _: NodeHop => () + } + route.amount + hop.fee(route.amount) + }).getOrElse(route.amount) + route.hops.foldRight(finalHopAmount) { case (hop, amount) => usedCapacity.updateWith(hop.shortChannelId)(previous => Some(amount + previous.getOrElse(0 msat))) amount + hop.fee(amount) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala index ce67a7c96a..28b8e95b43 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala @@ -28,7 +28,7 @@ import fr.acinq.eclair._ import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ import fr.acinq.eclair.channel._ -import fr.acinq.eclair.crypto.TransportHandler +import fr.acinq.eclair.crypto.{Sphinx, TransportHandler} import fr.acinq.eclair.db.NetworkDb import fr.acinq.eclair.io.Peer.PeerRoutingMessage import fr.acinq.eclair.payment.Invoice.ExtraEdge @@ -38,7 +38,7 @@ import fr.acinq.eclair.payment.send.Recipient import fr.acinq.eclair.payment.{Bolt11Invoice, Invoice} import fr.acinq.eclair.remote.EclairInternalsSerializer.RemoteTypes import fr.acinq.eclair.router.Graph.GraphStructure.DirectedGraph -import fr.acinq.eclair.router.Graph.MessageWeightRatios +import fr.acinq.eclair.router.Graph.{HeuristicsConstants, MessageWeightRatios} import fr.acinq.eclair.router.Monitoring.Metrics import fr.acinq.eclair.wire.protocol._ @@ -266,7 +266,7 @@ class Router(val nodeParams: NodeParams, watcher: typed.ActorRef[ZmqWatcher.Comm case Event(WatchExternalChannelSpentTriggered(shortChannelId, spendingTx), d) if d.channels.contains(shortChannelId) || d.prunedChannels.contains(shortChannelId) => val fundingTxId = d.channels.get(shortChannelId).orElse(d.prunedChannels.get(shortChannelId)).get.fundingTxId log.info("funding tx txId={} of channelId={} has been spent by txId={}: waiting for the spending tx to have enough confirmations before removing the channel from the graph", fundingTxId, shortChannelId, spendingTx.txid) - watcher ! WatchTxConfirmed(self, spendingTx.txid, ANNOUNCEMENTS_MINCONF * 2) + watcher ! WatchTxConfirmed(self, spendingTx.txid, nodeParams.routerConf.channelSpentSpliceDelay) stay() using d.copy(spentChannels = d.spentChannels.updated(spendingTx.txid, d.spentChannels.getOrElse(spendingTx.txid, Set.empty) + shortChannelId)) case Event(WatchTxConfirmedTriggered(_, _, spendingTx), d) => @@ -345,9 +345,6 @@ class Router(val nodeParams: NodeParams, watcher: typed.ActorRef[ZmqWatcher.Comm object Router { - // see https://github.com/lightningnetwork/lightning-rfc/blob/master/07-routing-gossip.md#requirements - val ANNOUNCEMENTS_MINCONF = 6 - def props(nodeParams: NodeParams, watcher: typed.ActorRef[ZmqWatcher.Command], initialized: Option[Promise[Done]] = None) = Props(new Router(nodeParams, watcher, initialized)) case class SearchBoundaries(maxFeeFlat: MilliSatoshi, @@ -357,7 +354,7 @@ object Router { case class PathFindingConf(randomize: Boolean, boundaries: SearchBoundaries, - heuristics: Graph.WeightRatios[Graph.PaymentPathWeight], + heuristics: HeuristicsConstants, mpp: MultiPartParams, experimentName: String, experimentPercentage: Int) { @@ -382,6 +379,7 @@ object Router { } case class RouterConf(watchSpentWindow: FiniteDuration, + channelSpentSpliceDelay: Int, channelExcludeDuration: FiniteDuration, routerBroadcastInterval: FiniteDuration, syncConf: SyncConf, @@ -579,11 +577,24 @@ object Router { override def fee(amount: MilliSatoshi): MilliSatoshi = fee } - case class MultiPartParams(minPartAmount: MilliSatoshi, maxParts: Int) + object MultiPartParams { + sealed trait SplittingStrategy + + /** Send the full capacity of the route */ + object FullCapacity extends SplittingStrategy + + /** Send between 20% and 100% of the capacity of the route */ + object Randomize extends SplittingStrategy + + /** Maximize the expected delivered amount */ + object MaxExpectedAmount extends SplittingStrategy + } + + case class MultiPartParams(minPartAmount: MilliSatoshi, maxParts: Int, splittingStrategy: MultiPartParams.SplittingStrategy) case class RouteParams(randomize: Boolean, boundaries: SearchBoundaries, - heuristics: Graph.WeightRatios[Graph.PaymentPathWeight], + heuristics: HeuristicsConstants, mpp: MultiPartParams, experimentName: String, includeLocalChannelCost: Boolean) { @@ -832,4 +843,6 @@ object Router { /** We have tried to relay this amount from this channel and it failed. */ case class ChannelCouldNotRelay(amount: MilliSatoshi, hop: ChannelHop) + + case class ReportedHoldTimes(holdTimes: Seq[Sphinx.HoldTime]) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/Sync.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/Sync.scala index 569d9b7795..ee7f64d02c 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/Sync.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/Sync.scala @@ -31,6 +31,7 @@ import shapeless.HNil import scala.annotation.tailrec import scala.collection.SortedSet import scala.collection.immutable.SortedMap +import scala.concurrent.duration.DurationInt import scala.util.Random object Sync { @@ -55,10 +56,10 @@ object Sync { s.to ! query // we also set a pass-all filter for now (we can update it later) for the future gossip messages, by setting - // the first_timestamp field to the current date/time and timestamp_range to the maximum value + // the first_timestamp field to the current date/time minus one minute and timestamp_range to the maximum value // NB: we can't just set firstTimestamp to 0, because in that case peer would send us all past messages matching // that (i.e. the whole routing table) - val filter = GossipTimestampFilter(s.chainHash, firstTimestamp = TimestampSecond.now(), timestampRange = Int.MaxValue) + val filter = GossipTimestampFilter(s.chainHash, firstTimestamp = TimestampSecond.now() - 1.minute, timestampRange = Int.MaxValue) // the one minute buffer ensures that we don't miss recent gossip that hasn't reached our peer yet s.to ! filter // reset our sync state for this peer: we create an entry to ensure we reject duplicate queries and unsolicited reply_channel_range diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/transactions/CommitmentSpec.scala b/eclair-core/src/main/scala/fr/acinq/eclair/transactions/CommitmentSpec.scala index 88f43c6149..8793de795e 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/transactions/CommitmentSpec.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/transactions/CommitmentSpec.scala @@ -16,27 +16,46 @@ package fr.acinq.eclair.transactions -import fr.acinq.bitcoin.scalacompat.SatoshiLong +import fr.acinq.bitcoin.scalacompat.{LexicographicalOrdering, SatoshiLong, TxOut} import fr.acinq.eclair.MilliSatoshi import fr.acinq.eclair.blockchain.fee.FeeratePerKw -import fr.acinq.eclair.transactions.Transactions.{CommitmentFormat, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat} +import fr.acinq.eclair.transactions.Transactions._ import fr.acinq.eclair.wire.protocol._ /** * Created by PM on 07/12/2016. */ -sealed trait CommitmentOutput +sealed trait CommitmentOutput { + val txOut: TxOut +} object CommitmentOutput { // @formatter:off - case object ToLocal extends CommitmentOutput - case object ToRemote extends CommitmentOutput - case object ToLocalAnchor extends CommitmentOutput - case object ToRemoteAnchor extends CommitmentOutput - case class InHtlc(incomingHtlc: IncomingHtlc) extends CommitmentOutput - case class OutHtlc(outgoingHtlc: OutgoingHtlc) extends CommitmentOutput + case class ToLocal(txOut: TxOut) extends CommitmentOutput + case class ToRemote(txOut: TxOut) extends CommitmentOutput + case class ToLocalAnchor(txOut: TxOut) extends CommitmentOutput + case class ToRemoteAnchor(txOut: TxOut) extends CommitmentOutput + // If there is an output for an HTLC in the commit tx, there is also a 2nd-level HTLC tx. + case class InHtlc(htlc: IncomingHtlc, txOut: TxOut, htlcDelayedOutput: TxOut) extends CommitmentOutput + case class OutHtlc(htlc: OutgoingHtlc, txOut: TxOut, htlcDelayedOutput: TxOut) extends CommitmentOutput // @formatter:on + + def isLessThan(a: CommitmentOutput, b: CommitmentOutput): Boolean = (a, b) match { + // Outgoing HTLCs that have the same payment_hash will have the same script. If they also have the same amount, they + // will produce exactly the same output: in that case, we must sort them using their expiry (see Bolt 3). + // If they also have the same expiry, it doesn't really matter how we sort them, but in order to provide a fully + // deterministic ordering (which is useful for tests), we sort them by htlc_id, which cannot be equal. + case (a: OutHtlc, b: OutHtlc) if a.txOut == b.txOut && a.htlc.add.cltvExpiry == b.htlc.add.cltvExpiry => a.htlc.add.id <= b.htlc.add.id + case (a: OutHtlc, b: OutHtlc) if a.txOut == b.txOut => a.htlc.add.cltvExpiry <= b.htlc.add.cltvExpiry + // Incoming HTLCs that have the same payment_hash *and* expiry will have the same script. If they also have the same + // amount, they will produce exactly the same output: just like offered HTLCs, it doesn't really matter how we sort + // them, but we use the htlc_id to provide a fully deterministic ordering. Note that the expiry is included in the + // script, so HTLCs with different expiries will have different scripts, and will thus be sorted by script as required + // by Bolt 3. + case (a: InHtlc, b: InHtlc) if a.txOut == b.txOut => a.htlc.add.id <= b.htlc.add.id + case _ => LexicographicalOrdering.isLessThan(a.txOut, b.txOut) + } } sealed trait DirectedHtlc { @@ -74,8 +93,8 @@ case class OutgoingHtlc(add: UpdateAddHtlc) extends DirectedHtlc final case class CommitmentSpec(htlcs: Set[DirectedHtlc], commitTxFeerate: FeeratePerKw, toLocal: MilliSatoshi, toRemote: MilliSatoshi) { def htlcTxFeerate(commitmentFormat: CommitmentFormat): FeeratePerKw = commitmentFormat match { - case ZeroFeeHtlcTxAnchorOutputsCommitmentFormat => FeeratePerKw(0 sat) - case _ => commitTxFeerate + case ZeroFeeHtlcTxAnchorOutputsCommitmentFormat | ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat => FeeratePerKw(0 sat) + case UnsafeLegacyAnchorOutputsCommitmentFormat | PhoenixSimpleTaprootChannelCommitmentFormat => commitTxFeerate } def findIncomingHtlcById(id: Long): Option[IncomingHtlc] = htlcs.collectFirst { case htlc: IncomingHtlc if htlc.add.id == id => htlc } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Scripts.scala b/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Scripts.scala index 6137d9e7f0..b64c842ec7 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Scripts.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Scripts.scala @@ -17,17 +17,17 @@ package fr.acinq.eclair.transactions import fr.acinq.bitcoin.Script.LOCKTIME_THRESHOLD -import fr.acinq.bitcoin.ScriptTree import fr.acinq.bitcoin.SigHash._ import fr.acinq.bitcoin.TxIn.{SEQUENCE_LOCKTIME_DISABLE_FLAG, SEQUENCE_LOCKTIME_MASK, SEQUENCE_LOCKTIME_TYPE_FLAG} import fr.acinq.bitcoin.scalacompat.Crypto.{PublicKey, XonlyPublicKey} import fr.acinq.bitcoin.scalacompat.Script._ import fr.acinq.bitcoin.scalacompat._ -import fr.acinq.eclair.transactions.Transactions.{AnchorOutputsCommitmentFormat, CommitmentFormat, DefaultCommitmentFormat} +import fr.acinq.eclair.crypto.keymanager.{CommitmentPublicKeys, LocalCommitmentKeys, RemoteCommitmentKeys} +import fr.acinq.eclair.transactions.Transactions._ import fr.acinq.eclair.{BlockHeight, CltvExpiry, CltvExpiryDelta} import scodec.bits.ByteVector -import scala.jdk.CollectionConverters.SeqHasAsJava +import scala.util.{Success, Try} /** * Created by PM on 02/12/2016. @@ -43,8 +43,7 @@ object Scripts { def der(sig: ByteVector64, sighashType: Int = SIGHASH_ALL): ByteVector = Crypto.compact2der(sig) :+ sighashType.toByte private def htlcRemoteSighash(commitmentFormat: CommitmentFormat): Int = commitmentFormat match { - case DefaultCommitmentFormat => SIGHASH_ALL - case _: AnchorOutputsCommitmentFormat => SIGHASH_SINGLE | SIGHASH_ANYONECANPAY + case _: AnchorOutputsCommitmentFormat | _: SimpleTaprootChannelCommitmentFormat => SIGHASH_SINGLE | SIGHASH_ANYONECANPAY } /** Sort public keys using lexicographic ordering. */ @@ -70,13 +69,34 @@ object Scripts { * @param n input number * @return a script element that represents n */ - def encodeNumber(n: Long): ScriptElt = n match { + private def encodeNumber(n: Long): ScriptElt = n match { case 0 => OP_0 case -1 => OP_1NEGATE case x if x >= 1 && x <= 16 => ScriptElt.code2elt((ScriptElt.elt2code(OP_1) + x - 1).toInt).get case _ => OP_PUSHDATA(Script.encodeNumber(n)) } + /** As defined in https://github.com/lightning/bolts/blob/master/03-transactions.md#dust-limits */ + def dustLimit(scriptPubKey: ByteVector): Satoshi = { + Try(Script.parse(scriptPubKey)) match { + case Success(OP_DUP :: OP_HASH160 :: OP_PUSHDATA(pubkeyHash, _) :: OP_EQUALVERIFY :: OP_CHECKSIG :: Nil) if pubkeyHash.size == 20 => 546.sat + case Success(OP_HASH160 :: OP_PUSHDATA(scriptHash, _) :: OP_EQUAL :: Nil) if scriptHash.size == 20 => 540.sat + case Success(OP_0 :: OP_PUSHDATA(pubkeyHash, _) :: Nil) if pubkeyHash.size == 20 => 294.sat + case Success(OP_0 :: OP_PUSHDATA(scriptHash, _) :: Nil) if scriptHash.size == 32 => 330.sat + case Success((OP_1 | OP_2 | OP_3 | OP_4 | OP_5 | OP_6 | OP_7 | OP_8 | OP_9 | OP_10 | OP_11 | OP_12 | OP_13 | OP_14 | OP_15 | OP_16) :: OP_PUSHDATA(program, _) :: Nil) if 2 <= program.length && program.length <= 40 => 354.sat + case Success(OP_RETURN :: _) => 0.sat // OP_RETURN is never dust + case _ => 546.sat + } + } + + /** Checks if the given script is an OP_RETURN. */ + def isOpReturn(scriptPubKey: ByteVector): Boolean = { + Try(Script.parse(scriptPubKey)) match { + case Success(OP_RETURN :: _) => true + case _ => false + } + } + /** * This function interprets the locktime for the given transaction, and returns the block height before which this tx cannot be published. * By convention in bitcoin, depending of the value of locktime it might be a number of blocks or a number of seconds since epoch. @@ -89,8 +109,7 @@ object Scripts { if (tx.lockTime <= LOCKTIME_THRESHOLD) { // locktime is a number of blocks BlockHeight(tx.lockTime) - } - else { + } else { // locktime is a unix epoch timestamp require(tx.lockTime <= 0x20FFFFFF, "locktime should be lesser than 0x20FFFFFF") // since locktime is very well in the past (0x20FFFFFF is in 1987), it is equivalent to no locktime at all @@ -125,13 +144,13 @@ object Scripts { } } - def toLocalDelayed(revocationPubkey: PublicKey, toSelfDelay: CltvExpiryDelta, localDelayedPaymentPubkey: PublicKey): Seq[ScriptElt] = { + def toLocalDelayed(keys: CommitmentPublicKeys, toSelfDelay: CltvExpiryDelta): Seq[ScriptElt] = { // @formatter:off OP_IF :: - OP_PUSHDATA(revocationPubkey) :: + OP_PUSHDATA(keys.revocationPublicKey) :: OP_ELSE :: encodeNumber(toSelfDelay.toInt) :: OP_CHECKSEQUENCEVERIFY :: OP_DROP :: - OP_PUSHDATA(localDelayedPaymentPubkey) :: + OP_PUSHDATA(keys.localDelayedPaymentPublicKey) :: OP_ENDIF :: OP_CHECKSIG :: Nil // @formatter:on @@ -140,37 +159,37 @@ object Scripts { /** * This witness script spends a [[toLocalDelayed]] output using a local sig after a delay */ - def witnessToLocalDelayedAfterDelay(localSig: ByteVector64, toLocalDelayedScript: ByteVector) = + def witnessToLocalDelayedAfterDelay(localSig: ByteVector64, toLocalDelayedScript: ByteVector): ScriptWitness = ScriptWitness(der(localSig) :: ByteVector.empty :: toLocalDelayedScript :: Nil) /** * This witness script spends (steals) a [[toLocalDelayed]] output using a revocation key as a punishment * for having published a revoked transaction */ - def witnessToLocalDelayedWithRevocationSig(revocationSig: ByteVector64, toLocalScript: ByteVector) = + def witnessToLocalDelayedWithRevocationSig(revocationSig: ByteVector64, toLocalScript: ByteVector): ScriptWitness = ScriptWitness(der(revocationSig) :: ByteVector(1) :: toLocalScript :: Nil) /** * With the anchor outputs format, the to_remote output is delayed with a CSV 1 to allow CPFP carve-out on anchors. */ - def toRemoteDelayed(remotePaymentPubkey: PublicKey): Seq[ScriptElt] = { - OP_PUSHDATA(remotePaymentPubkey) :: OP_CHECKSIGVERIFY :: OP_1 :: OP_CHECKSEQUENCEVERIFY :: Nil + def toRemoteDelayed(keys: CommitmentPublicKeys): Seq[ScriptElt] = { + OP_PUSHDATA(keys.remotePaymentPublicKey) :: OP_CHECKSIGVERIFY :: OP_1 :: OP_CHECKSEQUENCEVERIFY :: Nil } /** * If remote publishes its commit tx where there was a to_remote delayed output (anchor outputs format), then local * uses this script to claim its funds (consumes to_remote script from commit tx). */ - def witnessClaimToRemoteDelayedFromCommitTx(localSig: ByteVector64, toRemoteDelayedScript: ByteVector) = + def witnessClaimToRemoteDelayedFromCommitTx(localSig: ByteVector64, toRemoteDelayedScript: ByteVector): ScriptWitness = ScriptWitness(der(localSig) :: toRemoteDelayedScript :: Nil) /** * Each participant has its own anchor output that locks to their funding key. This allows using CPFP carve-out (see * https://github.com/bitcoin/bitcoin/pull/15681) to speed up confirmation of a commitment transaction. */ - def anchor(fundingPubkey: PublicKey): Seq[ScriptElt] = { + def anchor(anchorKey: PublicKey): Seq[ScriptElt] = { // @formatter:off - OP_PUSHDATA(fundingPubkey) :: OP_CHECKSIG :: OP_IFDUP :: + OP_PUSHDATA(anchorKey) :: OP_CHECKSIG :: OP_IFDUP :: OP_NOTIF :: OP_16 :: OP_CHECKSEQUENCEVERIFY :: OP_ENDIF :: Nil @@ -180,30 +199,24 @@ object Scripts { /** * This witness script spends a local [[anchor]] output using a local sig. */ - def witnessAnchor(localSig: ByteVector64, anchorScript: ByteVector) = ScriptWitness(der(localSig) :: anchorScript :: Nil) - - /** - * This witness script spends either a local or remote [[anchor]] output after its CSV delay. - */ - def witnessAnchorAfterDelay(anchorScript: ByteVector) = ScriptWitness(ByteVector.empty :: anchorScript :: Nil) + def witnessAnchor(localSig: ByteVector64, anchorScript: ByteVector): ScriptWitness = ScriptWitness(der(localSig) :: anchorScript :: Nil) - def htlcOffered(localHtlcPubkey: PublicKey, remoteHtlcPubkey: PublicKey, revocationPubKey: PublicKey, paymentHash: ByteVector, commitmentFormat: CommitmentFormat): Seq[ScriptElt] = { + def htlcOffered(keys: CommitmentPublicKeys, paymentHash: ByteVector32, commitmentFormat: CommitmentFormat): Seq[ScriptElt] = { val addCsvDelay = commitmentFormat match { - case DefaultCommitmentFormat => false - case _: AnchorOutputsCommitmentFormat => true + case _: AnchorOutputsCommitmentFormat | _: SimpleTaprootChannelCommitmentFormat => true } // @formatter:off // To you with revocation key - OP_DUP :: OP_HASH160 :: OP_PUSHDATA(revocationPubKey.hash160) :: OP_EQUAL :: + OP_DUP :: OP_HASH160 :: OP_PUSHDATA(keys.revocationPublicKey.hash160) :: OP_EQUAL :: OP_IF :: OP_CHECKSIG :: OP_ELSE :: - OP_PUSHDATA(remoteHtlcPubkey) :: OP_SWAP :: OP_SIZE :: encodeNumber(32) :: OP_EQUAL :: + OP_PUSHDATA(keys.remoteHtlcPublicKey) :: OP_SWAP :: OP_SIZE :: encodeNumber(32) :: OP_EQUAL :: OP_NOTIF :: // To me via HTLC-timeout transaction (timelocked). - OP_DROP :: OP_2 :: OP_SWAP :: OP_PUSHDATA(localHtlcPubkey) :: OP_2 :: OP_CHECKMULTISIG :: + OP_DROP :: OP_2 :: OP_SWAP :: OP_PUSHDATA(keys.localHtlcPublicKey) :: OP_2 :: OP_CHECKMULTISIG :: OP_ELSE :: - OP_HASH160 :: OP_PUSHDATA(paymentHash) :: OP_EQUALVERIFY :: + OP_HASH160 :: OP_PUSHDATA(Crypto.ripemd160(paymentHash)) :: OP_EQUALVERIFY :: OP_CHECKSIG :: OP_ENDIF :: (if (addCsvDelay) { @@ -218,12 +231,13 @@ object Scripts { /** * This is the witness script of the 2nd-stage HTLC Success transaction (consumes htlcOffered script from commit tx) */ - def witnessHtlcSuccess(localSig: ByteVector64, remoteSig: ByteVector64, paymentPreimage: ByteVector32, htlcOfferedScript: ByteVector, commitmentFormat: CommitmentFormat) = + def witnessHtlcSuccess(localSig: ByteVector64, remoteSig: ByteVector64, paymentPreimage: ByteVector32, htlcOfferedScript: ByteVector, commitmentFormat: CommitmentFormat): ScriptWitness = ScriptWitness(ByteVector.empty :: der(remoteSig, htlcRemoteSighash(commitmentFormat)) :: der(localSig) :: paymentPreimage.bytes :: htlcOfferedScript :: Nil) /** Extract the payment preimage from a 2nd-stage HTLC Success transaction's witness script */ def extractPreimageFromHtlcSuccess: PartialFunction[ScriptWitness, ByteVector32] = { case ScriptWitness(Seq(ByteVector.empty, _, _, paymentPreimage, _)) if paymentPreimage.size == 32 => ByteVector32(paymentPreimage) + case ScriptWitness(Seq(_, _, paymentPreimage, _, _)) if paymentPreimage.size == 32 => ByteVector32(paymentPreimage) } /** Extract payment preimages from a (potentially batched) 2nd-stage HTLC transaction's witnesses. */ @@ -233,33 +247,33 @@ object Scripts { * If remote publishes its commit tx where there was a remote->local htlc, then local uses this script to * claim its funds using a payment preimage (consumes htlcOffered script from commit tx) */ - def witnessClaimHtlcSuccessFromCommitTx(localSig: ByteVector64, paymentPreimage: ByteVector32, htlcOffered: ByteVector) = + def witnessClaimHtlcSuccessFromCommitTx(localSig: ByteVector64, paymentPreimage: ByteVector32, htlcOffered: ByteVector): ScriptWitness = ScriptWitness(der(localSig) :: paymentPreimage.bytes :: htlcOffered :: Nil) /** Extract the payment preimage from from a fulfilled offered htlc. */ def extractPreimageFromClaimHtlcSuccess: PartialFunction[ScriptWitness, ByteVector32] = { case ScriptWitness(Seq(_, paymentPreimage, _)) if paymentPreimage.size == 32 => ByteVector32(paymentPreimage) + case ScriptWitness(Seq(_, paymentPreimage, _, _)) if paymentPreimage.size == 32 => ByteVector32(paymentPreimage) } /** Extract payment preimages from a (potentially batched) claim HTLC transaction's witnesses. */ def extractPreimagesFromClaimHtlcSuccess(tx: Transaction): Set[ByteVector32] = tx.txIn.map(_.witness).collect(extractPreimageFromClaimHtlcSuccess).toSet - def htlcReceived(localHtlcPubkey: PublicKey, remoteHtlcPubkey: PublicKey, revocationPubKey: PublicKey, paymentHash: ByteVector, lockTime: CltvExpiry, commitmentFormat: CommitmentFormat): Seq[ScriptElt] = { + def htlcReceived(keys: CommitmentPublicKeys, paymentHash: ByteVector32, lockTime: CltvExpiry, commitmentFormat: CommitmentFormat): Seq[ScriptElt] = { val addCsvDelay = commitmentFormat match { - case DefaultCommitmentFormat => false - case _: AnchorOutputsCommitmentFormat => true + case _: AnchorOutputsCommitmentFormat | _: SimpleTaprootChannelCommitmentFormat => true } // @formatter:off // To you with revocation key - OP_DUP :: OP_HASH160 :: OP_PUSHDATA(revocationPubKey.hash160) :: OP_EQUAL :: + OP_DUP :: OP_HASH160 :: OP_PUSHDATA(keys.revocationPublicKey.hash160) :: OP_EQUAL :: OP_IF :: OP_CHECKSIG :: OP_ELSE :: - OP_PUSHDATA(remoteHtlcPubkey) :: OP_SWAP :: OP_SIZE :: encodeNumber(32) :: OP_EQUAL :: + OP_PUSHDATA(keys.remoteHtlcPublicKey) :: OP_SWAP :: OP_SIZE :: encodeNumber(32) :: OP_EQUAL :: OP_IF :: // To me via HTLC-success transaction. - OP_HASH160 :: OP_PUSHDATA(paymentHash) :: OP_EQUALVERIFY :: - OP_2 :: OP_SWAP :: OP_PUSHDATA(localHtlcPubkey) :: OP_2 :: OP_CHECKMULTISIG :: + OP_HASH160 :: OP_PUSHDATA(Crypto.ripemd160(paymentHash)) :: OP_EQUALVERIFY :: + OP_2 :: OP_SWAP :: OP_PUSHDATA(keys.localHtlcPublicKey) :: OP_2 :: OP_CHECKMULTISIG :: OP_ELSE :: // To you after timeout. OP_DROP :: encodeNumber(lockTime.toLong) :: OP_CHECKLOCKTIMEVERIFY :: OP_DROP :: @@ -277,36 +291,32 @@ object Scripts { /** * This is the witness script of the 2nd-stage HTLC Timeout transaction (consumes htlcOffered script from commit tx) */ - def witnessHtlcTimeout(localSig: ByteVector64, remoteSig: ByteVector64, htlcOfferedScript: ByteVector, commitmentFormat: CommitmentFormat) = + def witnessHtlcTimeout(localSig: ByteVector64, remoteSig: ByteVector64, htlcOfferedScript: ByteVector, commitmentFormat: CommitmentFormat): ScriptWitness = ScriptWitness(ByteVector.empty :: der(remoteSig, htlcRemoteSighash(commitmentFormat)) :: der(localSig) :: ByteVector.empty :: htlcOfferedScript :: Nil) /** * If remote publishes its commit tx where there was a local->remote htlc, then local uses this script to * claim its funds after timeout (consumes htlcReceived script from commit tx) */ - def witnessClaimHtlcTimeoutFromCommitTx(localSig: ByteVector64, htlcReceivedScript: ByteVector) = + def witnessClaimHtlcTimeoutFromCommitTx(localSig: ByteVector64, htlcReceivedScript: ByteVector): ScriptWitness = ScriptWitness(der(localSig) :: ByteVector.empty :: htlcReceivedScript :: Nil) /** * This witness script spends (steals) a [[htlcOffered]] or [[htlcReceived]] output using a revocation key as a punishment * for having published a revoked transaction */ - def witnessHtlcWithRevocationSig(revocationSig: ByteVector64, revocationPubkey: PublicKey, htlcScript: ByteVector) = - ScriptWitness(der(revocationSig) :: revocationPubkey.value :: htlcScript :: Nil) + def witnessHtlcWithRevocationSig(keys: RemoteCommitmentKeys, revocationSig: ByteVector64, htlcScript: ByteVector): ScriptWitness = + ScriptWitness(der(revocationSig) :: keys.revocationPublicKey.value :: htlcScript :: Nil) /** * Specific scripts for taproot channels */ object Taproot { - import KotlinUtils._ - - implicit def scala2kmpscript(input: Seq[fr.acinq.bitcoin.scalacompat.ScriptElt]): java.util.List[fr.acinq.bitcoin.ScriptElt] = input.map(e => scala2kmp(e)).asJava - /** * Taproot signatures are usually 64 bytes, unless a non-default sighash is used, in which case it is appended. */ - def encodeSig(sig: ByteVector64, sighashType: Int = SIGHASH_DEFAULT): ByteVector = sighashType match { + private def encodeSig(sig: ByteVector64, sighashType: Int = SIGHASH_DEFAULT): ByteVector = sighashType match { case SIGHASH_DEFAULT | SIGHASH_ALL => sig case _ => sig :+ sighashType.toByte } @@ -324,19 +334,18 @@ object Scripts { /** * "Nothing Up My Sleeve" point, for which there is no known private key. */ - val NUMS_POINT = PublicKey(ByteVector.fromValidHex("02dca094751109d0bd055d03565874e8276dd53e926b44e3bd1bb6bf4bc130a279")) + val NUMS_POINT: PublicKey = PublicKey(ByteVector.fromValidHex("02dca094751109d0bd055d03565874e8276dd53e926b44e3bd1bb6bf4bc130a279")) // miniscript: older(16) private val anchorScript: Seq[ScriptElt] = OP_16 :: OP_CHECKSEQUENCEVERIFY :: Nil - val anchorScriptTree = new ScriptTree.Leaf(anchorScript) + val anchorScriptTree = ScriptTree.Leaf(anchorScript) /** * Script used for local or remote anchor outputs. - * - * @param paymentPubkey local or remote payment key. + * The key used matches the key for the matching node's main output. */ - def anchor(paymentPubkey: PublicKey): Seq[ScriptElt] = { - Script.pay2tr(paymentPubkey.xOnly, Some(anchorScriptTree)) + def anchor(anchorKey: PublicKey): Seq[ScriptElt] = { + Script.pay2tr(anchorKey.xOnly, Some(anchorScriptTree)) } /** @@ -345,12 +354,10 @@ object Scripts { * * miniscript: this is not miniscript compatible * - * @param localDelayedPaymentPubkey local delayed key - * @param revocationPubkey revocation key * @return a script that will be used to add a "revocation" leaf to a script tree */ - private def toRevocationKey(localDelayedPaymentPubkey: PublicKey, revocationPubkey: PublicKey): Seq[ScriptElt] = { - OP_PUSHDATA(localDelayedPaymentPubkey.xOnly) :: OP_DROP :: OP_PUSHDATA(revocationPubkey.xOnly) :: OP_CHECKSIG :: Nil + private def toRevocationKey(keys: CommitmentPublicKeys): Seq[ScriptElt] = { + OP_PUSHDATA(keys.localDelayedPaymentPublicKey.xOnly) :: OP_DROP :: OP_PUSHDATA(keys.revocationPublicKey.xOnly) :: OP_CHECKSIG :: Nil } /** @@ -358,33 +365,31 @@ object Scripts { * * miniscript: and_v(v:pk(delayed_key),older(delay)) * - * @param localDelayedPaymentPubkey delayed payment key - * @param toSelfDelay to-self CSV delay * @return a script that will be used to add a "to local key" leaf to a script tree */ - private def toLocalDelayed(localDelayedPaymentPubkey: PublicKey, toSelfDelay: CltvExpiryDelta): Seq[ScriptElt] = { - OP_PUSHDATA(localDelayedPaymentPubkey.xOnly) :: OP_CHECKSIGVERIFY :: Scripts.encodeNumber(toSelfDelay.toInt) :: OP_CHECKSEQUENCEVERIFY :: Nil + private def toLocalDelayed(keys: CommitmentPublicKeys, toSelfDelay: CltvExpiryDelta): Seq[ScriptElt] = { + OP_PUSHDATA(keys.localDelayedPaymentPublicKey.xOnly) :: OP_CHECKSIGVERIFY :: Scripts.encodeNumber(toSelfDelay.toInt) :: OP_CHECKSEQUENCEVERIFY :: Nil + } + + case class ToLocalScriptTree(localDelayed: ScriptTree.Leaf, revocation: ScriptTree.Leaf) { + val scriptTree: ScriptTree.Branch = ScriptTree.Branch(localDelayed, revocation) } /** - * - * @param revocationPubkey revocation key - * @param toSelfDelay to-self CSV delay - * @param localDelayedPaymentPubkey local delayed payment key * @return a script tree with two leaves (to self with delay, and to revocation key) */ - def toLocalScriptTree(revocationPubkey: PublicKey, toSelfDelay: CltvExpiryDelta, localDelayedPaymentPubkey: PublicKey): ScriptTree.Branch = { - new ScriptTree.Branch( - new ScriptTree.Leaf(toLocalDelayed(localDelayedPaymentPubkey, toSelfDelay)), - new ScriptTree.Leaf(toRevocationKey(localDelayedPaymentPubkey, revocationPubkey)), + def toLocalScriptTree(keys: CommitmentPublicKeys, toSelfDelay: CltvExpiryDelta): ToLocalScriptTree = { + ToLocalScriptTree( + ScriptTree.Leaf(toLocalDelayed(keys, toSelfDelay)), + ScriptTree.Leaf(toRevocationKey(keys)), ) } /** * Script used for the main balance of the owner of the commitment transaction. */ - def toLocal(localDelayedPaymentPubkey: PublicKey, toSelfDelay: CltvExpiryDelta, revocationPubkey: PublicKey): Seq[ScriptElt] = { - Script.pay2tr(NUMS_POINT.xOnly, Some(toLocalScriptTree(revocationPubkey, toSelfDelay, localDelayedPaymentPubkey))) + def toLocal(keys: CommitmentPublicKeys, toSelfDelay: CltvExpiryDelta): Seq[ScriptElt] = { + Script.pay2tr(NUMS_POINT.xOnly, Some(toLocalScriptTree(keys, toSelfDelay).scriptTree)) } /** @@ -392,29 +397,27 @@ object Scripts { * * miniscript: and_v(v:pk(remote_key),older(1)) * - * @param remotePaymentPubkey remote payment key * @return a script that will be used to add a "to remote key" leaf to a script tree */ - private def toRemoteDelayed(remotePaymentPubkey: PublicKey): Seq[ScriptElt] = { - OP_PUSHDATA(remotePaymentPubkey.xOnly) :: OP_CHECKSIGVERIFY :: OP_1 :: OP_CHECKSEQUENCEVERIFY :: Nil + private def toRemoteDelayed(keys: CommitmentPublicKeys): Seq[ScriptElt] = { + OP_PUSHDATA(keys.remotePaymentPublicKey.xOnly) :: OP_CHECKSIGVERIFY :: OP_1 :: OP_CHECKSEQUENCEVERIFY :: Nil } /** * Script tree used for the main balance of the remote node in our commitment transaction. * Note that there is no need for a revocation leaf in that case. * - * @param remotePaymentPubkey remote key * @return a script tree with a single leaf (to remote key, with a 1-block CSV delay) */ - def toRemoteScriptTree(remotePaymentPubkey: PublicKey): ScriptTree.Leaf = { - new ScriptTree.Leaf(toRemoteDelayed(remotePaymentPubkey)) + def toRemoteScriptTree(keys: CommitmentPublicKeys): ScriptTree.Leaf = { + ScriptTree.Leaf(toRemoteDelayed(keys)) } /** * Script used for the main balance of the remote node in our commitment transaction. */ - def toRemote(remotePaymentPubkey: PublicKey): Seq[ScriptElt] = { - Script.pay2tr(NUMS_POINT.xOnly, Some(toRemoteScriptTree(remotePaymentPubkey))) + def toRemote(keys: CommitmentPublicKeys): Seq[ScriptElt] = { + Script.pay2tr(NUMS_POINT.xOnly, Some(toRemoteScriptTree(keys))) } /** @@ -423,12 +426,10 @@ object Scripts { * * miniscript: and_v(v:pk(local_htlc_key),pk(remote_htlc_key)) * - * @param localHtlcPubkey local HTLC key - * @param remoteHtlcPubkey remote HTLC key * @return a script used to create a "HTLC timeout" leaf in a script tree */ - private def offeredHtlcTimeout(localHtlcPubkey: PublicKey, remoteHtlcPubkey: PublicKey): Seq[ScriptElt] = { - OP_PUSHDATA(localHtlcPubkey.xOnly) :: OP_CHECKSIGVERIFY :: OP_PUSHDATA(remoteHtlcPubkey.xOnly) :: OP_CHECKSIG :: Nil + private def offeredHtlcTimeout(keys: CommitmentPublicKeys): Seq[ScriptElt] = { + OP_PUSHDATA(keys.localHtlcPublicKey.xOnly) :: OP_CHECKSIGVERIFY :: OP_PUSHDATA(keys.remoteHtlcPublicKey.xOnly) :: OP_CHECKSIG :: Nil } /** @@ -437,34 +438,37 @@ object Scripts { * * miniscript: and_v(v:hash160(H),and_v(v:pk(remote_htlc_key),older(1))) * - * @param remoteHtlcPubkey remote HTLC key - * @param paymentHash payment hash * @return a script used to create a "spend offered HTLC" leaf in a script tree */ - private def offeredHtlcSuccess(remoteHtlcPubkey: PublicKey, paymentHash: ByteVector32): Seq[ScriptElt] = { + private def offeredHtlcSuccess(keys: CommitmentPublicKeys, paymentHash: ByteVector32): Seq[ScriptElt] = { // @formatter:off OP_SIZE :: encodeNumber(32) :: OP_EQUALVERIFY :: OP_HASH160 :: OP_PUSHDATA(Crypto.ripemd160(paymentHash)) :: OP_EQUALVERIFY :: - OP_PUSHDATA(remoteHtlcPubkey.xOnly) :: OP_CHECKSIGVERIFY :: + OP_PUSHDATA(keys.remoteHtlcPublicKey.xOnly) :: OP_CHECKSIGVERIFY :: OP_1 :: OP_CHECKSEQUENCEVERIFY :: Nil // @formatter:on } - /** - * Script tree used for offered HTLCs. - */ - def offeredHtlcScriptTree(localHtlcPubkey: PublicKey, remoteHtlcPubkey: PublicKey, paymentHash: ByteVector32): ScriptTree.Branch = { - new ScriptTree.Branch( - new ScriptTree.Leaf(offeredHtlcTimeout(localHtlcPubkey, remoteHtlcPubkey)), - new ScriptTree.Leaf(offeredHtlcSuccess(remoteHtlcPubkey, paymentHash)), - ) + case class OfferedHtlcScriptTree(timeout: ScriptTree.Leaf, success: ScriptTree.Leaf) { + val scriptTree: ScriptTree.Branch = ScriptTree.Branch(timeout, success) + + def witnessTimeout(commitKeys: LocalCommitmentKeys, localSig: ByteVector64, remoteSig: ByteVector64): ScriptWitness = { + Script.witnessScriptPathPay2tr(commitKeys.revocationPublicKey.xOnly, timeout, ScriptWitness(Seq(Taproot.encodeSig(remoteSig, htlcRemoteSighash(ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat)), localSig)), scriptTree) + } + + def witnessSuccess(commitKeys: RemoteCommitmentKeys, localSig: ByteVector64, paymentPreimage: ByteVector32): ScriptWitness = { + Script.witnessScriptPathPay2tr(commitKeys.revocationPublicKey.xOnly, success, ScriptWitness(Seq(localSig, paymentPreimage)), scriptTree) + } } /** - * Script used for offered HTLCs. + * Script tree used for offered HTLCs. */ - def offeredHtlc(localHtlcPubkey: PublicKey, remoteHtlcPubkey: PublicKey, paymentHash: ByteVector32, revocationPubkey: PublicKey): Seq[ScriptElt] = { - Script.pay2tr(revocationPubkey.xOnly, Some(offeredHtlcScriptTree(localHtlcPubkey, remoteHtlcPubkey, paymentHash))) + def offeredHtlcScriptTree(keys: CommitmentPublicKeys, paymentHash: ByteVector32): OfferedHtlcScriptTree = { + OfferedHtlcScriptTree( + ScriptTree.Leaf(offeredHtlcTimeout(keys)), + ScriptTree.Leaf(offeredHtlcSuccess(keys, paymentHash)), + ) } /** @@ -472,15 +476,12 @@ object Scripts { * It is spent using a signature from the receiving node after an absolute delay and a 1-block relative delay. * * miniscript: and_v(v:pk(remote_htlc_key),and_v(v:older(1),after(delay))) - * - * @param remoteHtlcPubkey remote HTLC key - * @param lockTime HTLC expiry */ - private def receivedHtlcTimeout(remoteHtlcPubkey: PublicKey, lockTime: CltvExpiry): Seq[ScriptElt] = { + private def receivedHtlcTimeout(keys: CommitmentPublicKeys, expiry: CltvExpiry): Seq[ScriptElt] = { // @formatter:off - OP_PUSHDATA(remoteHtlcPubkey.xOnly) :: OP_CHECKSIGVERIFY :: + OP_PUSHDATA(keys.remoteHtlcPublicKey.xOnly) :: OP_CHECKSIGVERIFY :: OP_1 :: OP_CHECKSEQUENCEVERIFY :: OP_VERIFY :: - encodeNumber(lockTime.toLong) :: OP_CHECKLOCKTIMEVERIFY :: Nil + encodeNumber(expiry.toLong) :: OP_CHECKLOCKTIMEVERIFY :: Nil // @formatter:on } @@ -489,53 +490,50 @@ object Scripts { * It is spent using a pre-signed HTLC transaction signed with both keys and the preimage. * * miniscript: and_v(v:hash160(H),and_v(v:pk(local_key),pk(remote_key))) - * - * @param localHtlcPubkey local HTLC key - * @param remoteHtlcPubkey remote HTLC key - * @param paymentHash payment hash */ - private def receivedHtlcSuccess(localHtlcPubkey: PublicKey, remoteHtlcPubkey: PublicKey, paymentHash: ByteVector32): Seq[ScriptElt] = { + private def receivedHtlcSuccess(keys: CommitmentPublicKeys, paymentHash: ByteVector32): Seq[ScriptElt] = { // @formatter:off OP_SIZE :: encodeNumber(32) :: OP_EQUALVERIFY :: OP_HASH160 :: OP_PUSHDATA(Crypto.ripemd160(paymentHash)) :: OP_EQUALVERIFY :: - OP_PUSHDATA(localHtlcPubkey.xOnly) :: OP_CHECKSIGVERIFY :: - OP_PUSHDATA(remoteHtlcPubkey.xOnly) :: OP_CHECKSIG :: Nil + OP_PUSHDATA(keys.localHtlcPublicKey.xOnly) :: OP_CHECKSIGVERIFY :: + OP_PUSHDATA(keys.remoteHtlcPublicKey.xOnly) :: OP_CHECKSIG :: Nil // @formatter:on } - /** - * Script tree used for received HTLCs. - */ - def receivedHtlcScriptTree(localHtlcPubkey: PublicKey, remoteHtlcPubkey: PublicKey, paymentHash: ByteVector32, lockTime: CltvExpiry): ScriptTree.Branch = { - new ScriptTree.Branch( - new ScriptTree.Leaf(receivedHtlcTimeout(remoteHtlcPubkey, lockTime)), - new ScriptTree.Leaf(receivedHtlcSuccess(localHtlcPubkey, remoteHtlcPubkey, paymentHash)), - ) + case class ReceivedHtlcScriptTree(timeout: ScriptTree.Leaf, success: ScriptTree.Leaf) { + val scriptTree = ScriptTree.Branch(timeout, success) + + def witnessSuccess(commitKeys: LocalCommitmentKeys, localSig: ByteVector64, remoteSig: ByteVector64, paymentPreimage: ByteVector32): ScriptWitness = { + Script.witnessScriptPathPay2tr(commitKeys.revocationPublicKey.xOnly, success, ScriptWitness(Seq(Taproot.encodeSig(remoteSig, htlcRemoteSighash(ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat)), localSig, paymentPreimage)), scriptTree) + } + + def witnessTimeout(commitKeys: RemoteCommitmentKeys, localSig: ByteVector64): ScriptWitness = { + Script.witnessScriptPathPay2tr(commitKeys.revocationPublicKey.xOnly, timeout, ScriptWitness(Seq(localSig)), scriptTree) + } } /** - * Script used for received HTLCs. + * Script tree used for received HTLCs. */ - def receivedHtlc(localHtlcPubkey: PublicKey, remoteHtlcPubkey: PublicKey, paymentHash: ByteVector32, lockTime: CltvExpiry, revocationPubkey: PublicKey): Seq[ScriptElt] = { - Script.pay2tr(revocationPubkey.xOnly, Some(receivedHtlcScriptTree(localHtlcPubkey, remoteHtlcPubkey, paymentHash, lockTime))) + def receivedHtlcScriptTree(keys: CommitmentPublicKeys, paymentHash: ByteVector32, expiry: CltvExpiry): ReceivedHtlcScriptTree = { + ReceivedHtlcScriptTree( + ScriptTree.Leaf(receivedHtlcTimeout(keys, expiry)), + ScriptTree.Leaf(receivedHtlcSuccess(keys, paymentHash)), + ) } /** * Script tree used for the output of pre-signed HTLC 2nd-stage transactions. */ - def htlcDelayedScriptTree(localDelayedPaymentPubkey: PublicKey, toSelfDelay: CltvExpiryDelta): ScriptTree.Leaf = { - new ScriptTree.Leaf(toLocalDelayed(localDelayedPaymentPubkey, toSelfDelay)) + def htlcDelayedScriptTree(keys: CommitmentPublicKeys, toSelfDelay: CltvExpiryDelta): ScriptTree.Leaf = { + ScriptTree.Leaf(toLocalDelayed(keys, toSelfDelay)) } /** * Script used for the output of pre-signed HTLC 2nd-stage transactions. - * - * @param localDelayedPaymentPubkey local delayed payment key - * @param toSelfDelay to-self CSV delay - * @param revocationPubkey revocation key */ - def htlcDelayed(localDelayedPaymentPubkey: PublicKey, toSelfDelay: CltvExpiryDelta, revocationPubkey: PublicKey): Seq[ScriptElt] = { - Script.pay2tr(revocationPubkey.xOnly, Some(htlcDelayedScriptTree(localDelayedPaymentPubkey, toSelfDelay))) + def htlcDelayed(keys: CommitmentPublicKeys, toSelfDelay: CltvExpiryDelta): Seq[ScriptElt] = { + Script.pay2tr(keys.revocationPublicKey.xOnly, Some(htlcDelayedScriptTree(keys, toSelfDelay))) } } } \ No newline at end of file diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala b/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala index 9cb1ea9194..2375315af0 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala @@ -18,19 +18,25 @@ package fr.acinq.eclair.transactions import fr.acinq.bitcoin.SigHash._ import fr.acinq.bitcoin.SigVersion._ -import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey, XonlyPublicKey, ripemd160} -import fr.acinq.bitcoin.scalacompat.Script._ +import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey, XonlyPublicKey} +import fr.acinq.bitcoin.scalacompat.KotlinUtils._ import fr.acinq.bitcoin.scalacompat._ -import fr.acinq.bitcoin.{ScriptFlags, ScriptTree} +import fr.acinq.bitcoin.ScriptFlags +import fr.acinq.bitcoin.scalacompat.Musig2.{IndividualNonce, LocalNonce} import fr.acinq.eclair._ -import fr.acinq.eclair.blockchain.fee.{ConfirmationTarget, FeeratePerKw} +import fr.acinq.eclair.blockchain.fee.FeeratePerKw +import fr.acinq.eclair.channel.ChannelSpendSignature +import fr.acinq.eclair.channel.ChannelSpendSignature._ +import fr.acinq.eclair.crypto.NonceGenerator +import fr.acinq.eclair.crypto.keymanager.{CommitmentPublicKeys, LocalCommitmentKeys, RemoteCommitmentKeys} import fr.acinq.eclair.transactions.CommitmentOutput._ +import fr.acinq.eclair.transactions.Scripts.Taproot.NUMS_POINT import fr.acinq.eclair.transactions.Scripts._ import fr.acinq.eclair.wire.protocol.UpdateAddHtlc import scodec.bits.ByteVector import java.nio.ByteOrder -import scala.util.{Success, Try} +import scala.util.Try /** * Created by PM on 15/12/2016. @@ -39,44 +45,86 @@ object Transactions { val MAX_STANDARD_TX_WEIGHT = 400_000 + /** Weight of a fully signed p2wpkh input (using a 73 bytes signature). */ + val p2wpkhInputWeight = 273 + /** Weight of a fully signed p2tr wallet input. */ + val p2trInputWeight = 230 + /** Weight of an additional p2wpkh output added to a transaction. */ + val p2wpkhOutputWeight = 124 + /** Weight of an additional p2tr wallet output added to a transaction. */ + val p2trOutputWeight = 172 + + val maxWalletInputWeight: Int = p2wpkhInputWeight.max(p2trInputWeight) + val maxWalletOutputWeight: Int = p2wpkhOutputWeight.max(p2trOutputWeight) + sealed trait CommitmentFormat { // @formatter:off + /** Weight of a fully signed channel output, when spent by a [[ChannelSpendTransaction]]. */ + def fundingInputWeight: Int + /** Weight of a fully signed [[CommitTx]] transaction without any HTLCs. */ def commitWeight: Int + /** Weight of a fully signed [[ClaimLocalAnchorTx]] or [[ClaimRemoteAnchorTx]] input. */ + def anchorInputWeight: Int + /** Weight of an additional HTLC output added to a [[CommitTx]]. */ def htlcOutputWeight: Int - def htlcTimeoutWeight: Int - def htlcSuccessWeight: Int + /** Weight of the fully signed [[HtlcTimeoutTx]] input. */ def htlcTimeoutInputWeight: Int + /** Weight of a fully signed [[HtlcTimeoutTx]] transaction without additional wallet inputs. */ + def htlcTimeoutWeight: Int + /** Weight of the fully signed [[HtlcSuccessTx]] input. */ def htlcSuccessInputWeight: Int + /** Weight of a fully signed [[HtlcSuccessTx]] transaction without additional wallet inputs. */ + def htlcSuccessWeight: Int + /** Weight of a fully signed [[ClaimHtlcSuccessTx]] transaction. */ + def claimHtlcSuccessWeight: Int + /** Weight of a fully signed [[ClaimHtlcTimeoutTx]] transaction. */ + def claimHtlcTimeoutWeight: Int + /** Weight of a fully signed [[ClaimLocalDelayedOutputTx]] transaction. */ + def toLocalDelayedWeight: Int + /** Weight of a fully signed [[ClaimRemoteDelayedOutputTx]] transaction. */ + def toRemoteWeight: Int + /** Weight of a fully signed [[HtlcDelayedTx]] 3rd-stage transaction (spending the output of an [[HtlcTx]]). */ + def htlcDelayedWeight: Int + /** Weight of a fully signed [[MainPenaltyTx]] transaction. */ + def mainPenaltyWeight: Int + /** Weight of a fully signed [[HtlcPenaltyTx]] transaction for an offered HTLC. */ + def htlcOfferedPenaltyWeight: Int + /** Weight of a fully signed [[HtlcPenaltyTx]] transaction for a received HTLC. */ + def htlcReceivedPenaltyWeight: Int + /** Weight of a fully signed [[ClaimHtlcDelayedOutputPenaltyTx]] transaction. */ + def claimHtlcPenaltyWeight: Int // @formatter:on } - /** - * Commitment format as defined in the v1.0 specification (https://github.com/lightningnetwork/lightning-rfc/tree/v1.0). - */ - case object DefaultCommitmentFormat extends CommitmentFormat { - override val commitWeight = 724 - override val htlcOutputWeight = 172 - override val htlcTimeoutWeight = 663 - override val htlcSuccessWeight = 703 - override val htlcTimeoutInputWeight = 449 - override val htlcSuccessInputWeight = 488 + sealed trait SegwitV0CommitmentFormat extends CommitmentFormat { + override val fundingInputWeight = 384 } /** * Commitment format that adds anchor outputs to the commitment transaction and uses custom sighash flags for HTLC * transactions to allow unilateral fee bumping (https://github.com/lightningnetwork/lightning-rfc/pull/688). */ - sealed trait AnchorOutputsCommitmentFormat extends CommitmentFormat { + sealed trait AnchorOutputsCommitmentFormat extends SegwitV0CommitmentFormat { override val commitWeight = 1124 + override val anchorInputWeight = 279 override val htlcOutputWeight = 172 - override val htlcTimeoutWeight = 666 - override val htlcSuccessWeight = 706 override val htlcTimeoutInputWeight = 452 + override val htlcTimeoutWeight = 666 override val htlcSuccessInputWeight = 491 + override val htlcSuccessWeight = 706 + override val claimHtlcSuccessWeight = 574 + override val claimHtlcTimeoutWeight = 547 + override val toLocalDelayedWeight = 483 + override val toRemoteWeight = 442 + override val htlcDelayedWeight = 483 + override val mainPenaltyWeight = 483 + override val htlcOfferedPenaltyWeight = 575 + override val htlcReceivedPenaltyWeight = 580 + override val claimHtlcPenaltyWeight = 483 } object AnchorOutputsCommitmentFormat { - val anchorAmount = Satoshi(330) + val anchorAmount: Satoshi = Satoshi(330) } /** @@ -84,196 +132,1093 @@ object Transactions { * Don't use this commitment format unless you know what you're doing! * See https://lists.linuxfoundation.org/pipermail/lightning-dev/2020-September/002796.html for details. */ - case object UnsafeLegacyAnchorOutputsCommitmentFormat extends AnchorOutputsCommitmentFormat + case object UnsafeLegacyAnchorOutputsCommitmentFormat extends AnchorOutputsCommitmentFormat { + override def toString: String = "unsafe_anchor_outputs" + } /** * This commitment format removes the fees from the pre-signed 2nd-stage htlc transactions to fix the fee inflating * attack against [[UnsafeLegacyAnchorOutputsCommitmentFormat]]. */ - case object ZeroFeeHtlcTxAnchorOutputsCommitmentFormat extends AnchorOutputsCommitmentFormat + case object ZeroFeeHtlcTxAnchorOutputsCommitmentFormat extends AnchorOutputsCommitmentFormat { + override def toString: String = "anchor_outputs" + } - // @formatter:off - case class OutputInfo(index: Long, amount: Satoshi, publicKeyScript: ByteVector) + sealed trait TaprootCommitmentFormat extends CommitmentFormat - sealed trait InputInfo { - val outPoint: OutPoint - val txOut: TxOut + sealed trait SimpleTaprootChannelCommitmentFormat extends TaprootCommitmentFormat { + // Weights for taproot transactions are deterministic since signatures are encoded as 64 bytes and not in variable + // length DER format like ECDSA (which is used for segwit v0 commitment formats). + override val fundingInputWeight = 230 + // Note that the commit weight assumes that the number of outputs is encoded using 3 bytes, to handle the case + // where we have a lot of HTLCs pending. + override val commitWeight = 968 + override val anchorInputWeight = 230 + override val htlcOutputWeight = 172 + override val htlcTimeoutWeight = 645 + override val htlcSuccessWeight = 705 + override val htlcTimeoutInputWeight = 431 + override val htlcSuccessInputWeight = 491 + override val claimHtlcSuccessWeight = 559 + override val claimHtlcTimeoutWeight = 504 + override val toLocalDelayedWeight = 501 + override val toRemoteWeight = 467 + override val htlcDelayedWeight = 469 + override val mainPenaltyWeight = 531 + override val htlcOfferedPenaltyWeight = 396 + override val htlcReceivedPenaltyWeight = 396 + override val claimHtlcPenaltyWeight = 396 } - object InputInfo { - case class SegwitInput(outPoint: OutPoint, txOut: TxOut, redeemScript: ByteVector) extends InputInfo - case class TaprootInput(outPoint: OutPoint, txOut: TxOut, internalKey: XonlyPublicKey, scriptTree_opt: Option[ScriptTree]) extends InputInfo { - val publicKeyScript: ByteVector = Script.write(Script.pay2tr(internalKey, scriptTree_opt)) - } + /** For Phoenix users we sign HTLC transactions with the same feerate as the commit tx to allow broadcasting without wallet inputs. */ + case object PhoenixSimpleTaprootChannelCommitmentFormat extends SimpleTaprootChannelCommitmentFormat { + override def toString: String = "simple_taproot_phoenix" + } - def apply(outPoint: OutPoint, txOut: TxOut, redeemScript: ByteVector): SegwitInput = SegwitInput(outPoint, txOut, redeemScript) - def apply(outPoint: OutPoint, txOut: TxOut, redeemScript: Seq[ScriptElt]): SegwitInput = SegwitInput(outPoint, txOut, Script.write(redeemScript)) - def apply(outPoint: OutPoint, txOut: TxOut, internalKey: XonlyPublicKey, scriptTree_opt: Option[ScriptTree]): TaprootInput = TaprootInput(outPoint, txOut, internalKey, scriptTree_opt) + case object ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat extends SimpleTaprootChannelCommitmentFormat { + override def toString: String = "simple_taproot" } - /** Owner of a given transaction (local/remote). */ - sealed trait TxOwner - object TxOwner { - case object Local extends TxOwner - case object Remote extends TxOwner + case class InputInfo(outPoint: OutPoint, txOut: TxOut) + + // @formatter:off + /** This trait contains redeem information necessary to spend different types of segwit inputs. */ + sealed trait RedeemInfo { + def pubkeyScript: ByteVector } + object RedeemInfo { + sealed trait SegwitV0 extends RedeemInfo { def redeemScript: ByteVector } + /** @param redeemScript the actual script must be known to redeem pay2wsh inputs. */ + case class P2wsh(redeemScript: ByteVector) extends SegwitV0 { + override val pubkeyScript: ByteVector = Script.write(Script.pay2wsh(redeemScript)) + } + object P2wsh { + def apply(script: Seq[ScriptElt]): P2wsh = P2wsh(Script.write(script)) + } + + sealed trait Taproot extends RedeemInfo + /** + * @param internalKey the private key associated with this public key will be used to sign. + * @param scriptTree_opt the script tree must be known if there is one, even when spending via the key path. + */ + case class TaprootKeyPath(internalKey: XonlyPublicKey, scriptTree_opt: Option[ScriptTree]) extends Taproot { + override val pubkeyScript: ByteVector = Script.write(Script.pay2tr(internalKey, scriptTree_opt)) + } + /** + * @param internalKey we need the internal key, even if we don't have the private key, to spend via a script path. + * @param scriptTree we need the complete script tree to spend taproot inputs. + * @param leafHash hash of the leaf script we're spending (must belong to the tree). + */ + case class TaprootScriptPath(internalKey: XonlyPublicKey, scriptTree: ScriptTree, leafHash: ByteVector32) extends Taproot { + val leaf: ScriptTree.Leaf = scriptTree.findScript(leafHash).getOrElse(throw new IllegalArgumentException("script tree must contain the provided leaf")) + val redeemScript: ByteVector = leaf.getScript + override val pubkeyScript: ByteVector = Script.write(Script.pay2tr(internalKey, Some(scriptTree))) + } + } + // @formatter:on sealed trait TransactionWithInputInfo { + // @formatter:off def input: InputInfo def desc: String def tx: Transaction def amountIn: Satoshi = input.txOut.amount def fee: Satoshi = amountIn - tx.txOut.map(_.amount).sum - def minRelayFee: Satoshi = { - val vsize = (tx.weight() + 3) / 4 - Satoshi(FeeratePerKw.MinimumRelayFeeRate * vsize / 1000) - } - /** Sighash flags to use when signing the transaction. */ - def sighash(txOwner: TxOwner, commitmentFormat: CommitmentFormat): Int = SIGHASH_ALL + def inputIndex: Int = tx.txIn.indexWhere(_.outPoint == input.outPoint) + // @formatter:on - /** - * @param extraUtxos extra outputs spent by this transaction (in addition to the main [[input]]). - */ - def sign(key: PrivateKey, txOwner: TxOwner, commitmentFormat: CommitmentFormat, extraUtxos: Map[OutPoint, TxOut]): ByteVector64 = { - sign(key, sighash(txOwner, commitmentFormat), extraUtxos) + protected def buildSpentOutputs(extraUtxos: Map[OutPoint, TxOut]): Seq[TxOut] = { + // Callers don't except this function to throw. + // But we want to ensure that we're correctly providing input details, otherwise our signature will silently be + // invalid when using taproot. We verify this in all cases, even when using segwit v0, to ensure that we have as + // many tests as possible that exercise this codepath. + val inputsMap = extraUtxos + (input.outPoint -> input.txOut) + tx.txIn.foreach(txIn => require(inputsMap.contains(txIn.outPoint), s"cannot sign $desc with txId=${tx.txid}: missing input details for ${txIn.outPoint}")) + tx.txIn.map(txIn => inputsMap(txIn.outPoint)) } - def sign(key: PrivateKey, sighashType: Int, extraUtxos: Map[OutPoint, TxOut]): ByteVector64 = { - val inputsMap = extraUtxos + (input.outPoint -> input.txOut) - tx.txIn.foreach(txIn => { - // Note that using a require here is dangerous, because callers don't except this function to throw. - // But we want to ensure that we're correctly providing input details, otherwise our signature will silently be - // invalid when using taproot. We verify this in all cases, even when using segwit v0, to ensure that we have as - // many tests as possible that exercise this codepath. - require(inputsMap.contains(txIn.outPoint), s"cannot sign $desc with txId=${tx.txid}: missing input details for ${txIn.outPoint}") - }) - input match { - case InputInfo.SegwitInput(outPoint, txOut, redeemScript) => - // NB: the tx may have multiple inputs, we will only sign the one provided in txinfo.input. Bear in mind that the - // signature will be invalidated if other inputs are added *afterwards* and sighashType was SIGHASH_ALL. - val inputIndex = tx.txIn.indexWhere(_.outPoint == outPoint) - val sigDER = Transaction.signInput(tx, inputIndex, redeemScript, sighashType, txOut.amount, SIGVERSION_WITNESS_V0, key) + protected def sign(key: PrivateKey, sighash: Int, redeemInfo: RedeemInfo, extraUtxos: Map[OutPoint, TxOut]): ByteVector64 = { + val spentOutputs = buildSpentOutputs(extraUtxos) + // NB: the tx may have multiple inputs, we will only sign the one provided in our input. Bear in mind that the + // signature will be invalidated if other inputs are added *afterwards* and sighash was SIGHASH_ALL. + redeemInfo match { + case redeemInfo: RedeemInfo.SegwitV0 => + val sigDER = Transaction.signInput(tx, inputIndex, redeemInfo.redeemScript, sighash, input.txOut.amount, SIGVERSION_WITNESS_V0, key) Crypto.der2compact(sigDER) - case _: InputInfo.TaprootInput => ??? + case t: RedeemInfo.TaprootKeyPath => + Transaction.signInputTaprootKeyPath(key, tx, inputIndex, spentOutputs, sighash, t.scriptTree_opt) + case s: RedeemInfo.TaprootScriptPath => + Transaction.signInputTaprootScriptPath(key, tx, inputIndex, spentOutputs, sighash, s.leafHash) } } - def checkSig(sig: ByteVector64, pubKey: PublicKey, txOwner: TxOwner, commitmentFormat: CommitmentFormat): Boolean = input match { - case _: InputInfo.TaprootInput => false - case InputInfo.SegwitInput(outPoint, txOut, redeemScript) => - val sighash = this.sighash(txOwner, commitmentFormat) - val inputIndex = tx.txIn.indexWhere(_.outPoint == outPoint) - if (inputIndex >= 0) { - val data = Transaction.hashForSigning(tx, inputIndex, redeemScript, sighash, txOut.amount, SIGVERSION_WITNESS_V0) - Crypto.verifySignature(data, sig, pubKey) - } else { - false + protected def checkSig(sig: ByteVector64, publicKey: PublicKey, sighash: Int, redeemInfo: RedeemInfo): Boolean = { + if (inputIndex >= 0) { + redeemInfo match { + case redeemInfo: RedeemInfo.SegwitV0 => + val data = Transaction.hashForSigning(tx, inputIndex, redeemInfo.redeemScript, sighash, input.txOut.amount, SIGVERSION_WITNESS_V0) + Crypto.verifySignature(data, sig, publicKey) + case _: RedeemInfo.TaprootKeyPath => + val data = Transaction.hashForSigningTaprootKeyPath(tx, inputIndex, Seq(input.txOut), sighash) + Crypto.verifySignatureSchnorr(data, sig, publicKey.xOnly) + case s: RedeemInfo.TaprootScriptPath => + val data = Transaction.hashForSigningTaprootScriptPath(tx, inputIndex, Seq(input.txOut), sighash, s.leafHash) + Crypto.verifySignatureSchnorr(data, sig, publicKey.xOnly) } + } else { + false + } + } + + /** Check that this transaction is correctly signed. */ + def validate(extraUtxos: Map[OutPoint, TxOut]): Boolean = { + val inputsMap = extraUtxos + (input.outPoint -> input.txOut) + val allInputsProvided = tx.txIn.forall(txIn => inputsMap.contains(txIn.outPoint)) + val witnessesOk = Try(Transaction.correctlySpends(tx, inputsMap, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)).isSuccess + allInputsProvided && witnessesOk + } + } + + /** + * Transactions spending the channel funding output: [[CommitTx]], [[SpliceTx]] and [[ClosingTx]]. + * Those transactions always require two signatures, one from each channel participant. + */ + sealed trait ChannelSpendTransaction extends TransactionWithInputInfo { + /** Sign the channel's 2-of-2 funding output when using a [[SegwitV0CommitmentFormat]]. */ + def sign(localFundingKey: PrivateKey, remoteFundingPubkey: PublicKey, extraUtxos: Map[OutPoint, TxOut]): ChannelSpendSignature.IndividualSignature = { + val redeemScript = Script.write(Scripts.multiSig2of2(localFundingKey.publicKey, remoteFundingPubkey)) + val sig = sign(localFundingKey, SIGHASH_ALL, RedeemInfo.P2wsh(redeemScript), extraUtxos) + ChannelSpendSignature.IndividualSignature(sig) + } + + /** Create a partial transaction for the channel's musig2 funding output when using a [[TaprootCommitmentFormat]]. */ + def partialSign(localFundingKey: PrivateKey, remoteFundingPubkey: PublicKey, extraUtxos: Map[OutPoint, TxOut], localNonce: LocalNonce, publicNonces: Seq[IndividualNonce]): Either[Throwable, ChannelSpendSignature.PartialSignatureWithNonce] = { + val spentOutputs = buildSpentOutputs(extraUtxos) + for { + partialSig <- Musig2.signTaprootInput(localFundingKey, tx, inputIndex, spentOutputs, Scripts.sort(Seq(localFundingKey.publicKey, remoteFundingPubkey)), localNonce.secretNonce, publicNonces, None) + } yield ChannelSpendSignature.PartialSignatureWithNonce(partialSig, localNonce.publicNonce) } + + /** Verify a signature received from the remote channel participant. */ + def checkRemoteSig(localFundingPubkey: PublicKey, remoteFundingPubkey: PublicKey, remoteSig: ChannelSpendSignature.IndividualSignature): Boolean = { + val redeemScript = Script.write(Scripts.multiSig2of2(localFundingPubkey, remoteFundingPubkey)) + checkSig(remoteSig.sig, remoteFundingPubkey, SIGHASH_ALL, RedeemInfo.P2wsh(redeemScript)) + } + + def checkRemotePartialSignature(localFundingPubKey: PublicKey, remoteFundingPubKey: PublicKey, remoteSig: PartialSignatureWithNonce, localNonce: IndividualNonce): Boolean = { + Musig2.verifyTaprootSignature(remoteSig.partialSig, remoteSig.nonce, remoteFundingPubKey, tx, inputIndex, Seq(input.txOut), Scripts.sort(Seq(localFundingPubKey, remoteFundingPubKey)), Seq(localNonce, remoteSig.nonce), scriptTree_opt = None) + } + + /** Aggregate local and remote channel spending signatures for a [[SegwitV0CommitmentFormat]]. */ + def aggregateSigs(localFundingPubkey: PublicKey, remoteFundingPubkey: PublicKey, localSig: IndividualSignature, remoteSig: IndividualSignature): Transaction = { + val witness = Scripts.witness2of2(localSig.sig, remoteSig.sig, localFundingPubkey, remoteFundingPubkey) + tx.updateWitness(inputIndex, witness) + } + + /** Aggregate local and remote channel spending partial signatures for a [[TaprootCommitmentFormat]]. */ + def aggregateSigs(localFundingPubkey: PublicKey, remoteFundingPubkey: PublicKey, localSig: PartialSignatureWithNonce, remoteSig: PartialSignatureWithNonce, extraUtxos: Map[OutPoint, TxOut]): Either[Throwable, Transaction] = { + val spentOutputs = buildSpentOutputs(extraUtxos) + for { + aggregatedSignature <- Musig2.aggregateTaprootSignatures(Seq(localSig.partialSig, remoteSig.partialSig), tx, inputIndex, spentOutputs, sort(Seq(localFundingPubkey, remoteFundingPubkey)), Seq(localSig.nonce, remoteSig.nonce), None) + witness = Script.witnessKeyPathPay2tr(aggregatedSignature) + } yield tx.updateWitness(inputIndex, witness) + } + } + + /** This transaction collaboratively spends the channel funding output to change its capacity. */ + case class SpliceTx(input: InputInfo, tx: Transaction) extends ChannelSpendTransaction { + override val desc: String = "splice-tx" + } + + /** This transaction unilaterally spends the channel funding output (force-close). */ + case class CommitTx(input: InputInfo, tx: Transaction) extends ChannelSpendTransaction { + override val desc: String = "commit-tx" + + def sign(localFundingKey: PrivateKey, remoteFundingPubkey: PublicKey): ChannelSpendSignature.IndividualSignature = sign(localFundingKey, remoteFundingPubkey, extraUtxos = Map.empty) + + def partialSign(localFundingKey: PrivateKey, remoteFundingPubkey: PublicKey, localNonce: LocalNonce, publicNonces: Seq[IndividualNonce]): Either[Throwable, ChannelSpendSignature.PartialSignatureWithNonce] = partialSign(localFundingKey, remoteFundingPubkey, extraUtxos = Map.empty, localNonce, publicNonces) + + def aggregateSigs(localFundingPubkey: PublicKey, remoteFundingPubkey: PublicKey, localSig: PartialSignatureWithNonce, remoteSig: PartialSignatureWithNonce): Either[Throwable, Transaction] = aggregateSigs(localFundingPubkey, remoteFundingPubkey, localSig, remoteSig, extraUtxos = Map.empty) + } + + /** This transaction collaboratively spends the channel funding output (mutual-close). */ + case class ClosingTx(input: InputInfo, tx: Transaction, toLocalOutputIndex_opt: Option[Long]) extends ChannelSpendTransaction { + override val desc: String = "closing-tx" + val toLocalOutput_opt: Option[TxOut] = toLocalOutputIndex_opt.map(i => tx.txOut(i.toInt)) + + def sign(localFundingKey: PrivateKey, remoteFundingPubkey: PublicKey): ChannelSpendSignature.IndividualSignature = sign(localFundingKey, remoteFundingPubkey, extraUtxos = Map.empty) + + def partialSign(localFundingKey: PrivateKey, remoteFundingPubkey: PublicKey, localNonce: LocalNonce, publicNonces: Seq[IndividualNonce]): Either[Throwable, ChannelSpendSignature.PartialSignatureWithNonce] = partialSign(localFundingKey, remoteFundingPubkey, extraUtxos = Map.empty, localNonce, publicNonces) + + def aggregateSigs(localFundingPubkey: PublicKey, remoteFundingPubkey: PublicKey, localSig: PartialSignatureWithNonce, remoteSig: PartialSignatureWithNonce): Either[Throwable, Transaction] = aggregateSigs(localFundingPubkey, remoteFundingPubkey, localSig, remoteSig, extraUtxos = Map.empty) + } + + object ClosingTx { + def createUnsignedTx(input: InputInfo, localScriptPubKey: ByteVector, remoteScriptPubKey: ByteVector, localPaysClosingFees: Boolean, dustLimit: Satoshi, closingFee: Satoshi, spec: CommitmentSpec): ClosingTx = { + require(spec.htlcs.isEmpty, "there shouldn't be any pending htlcs") + + val (toLocalAmount: Satoshi, toRemoteAmount: Satoshi) = if (localPaysClosingFees) { + (spec.toLocal.truncateToSatoshi - closingFee, spec.toRemote.truncateToSatoshi) + } else { + (spec.toLocal.truncateToSatoshi, spec.toRemote.truncateToSatoshi - closingFee) + } // NB: we don't care if values are < 0, they will be trimmed if they are < dust limit anyway + + val toLocalOutput_opt = if (toLocalAmount >= dustLimit) Some(TxOut(toLocalAmount, localScriptPubKey)) else None + val toRemoteOutput_opt = if (toRemoteAmount >= dustLimit) Some(TxOut(toRemoteAmount, remoteScriptPubKey)) else None + + val tx = LexicographicalOrdering.sort(Transaction( + version = 2, + txIn = TxIn(input.outPoint, ByteVector.empty, sequence = 0xffffffffL) :: Nil, + txOut = toLocalOutput_opt.toSeq ++ toRemoteOutput_opt.toSeq ++ Nil, + lockTime = 0 + )) + val toLocalOutput = findPubKeyScriptIndex(tx, localScriptPubKey).map(_.toLong).toOption + ClosingTx(input, tx, toLocalOutput) + } + } + + /** + * @param txIn wallet input. + * @param spentOutput utxo spent by this wallet input. + */ + case class WalletInput(txIn: TxIn, spentOutput: TxOut) { + val amount: Satoshi = spentOutput.amount + } + + /** + * Whenever possible, [[ForceCloseTransaction]]s pay on-chain fees by lowering their output amount. + * When this isn't possible, we add wallet inputs to allow paying on-chain fees. + * + * @param inputs inputs added by our bitcoin wallet. + * @param changeOutput_opt change output added by our bitcoin wallet, if any. + */ + case class WalletInputs(inputs: Seq[WalletInput], changeOutput_opt: Option[TxOut]) { + val amountIn: Satoshi = inputs.map(_.amount).sum + val fee: Satoshi = amountIn - changeOutput_opt.map(_.amount).getOrElse(0 sat) + val txIn: Seq[TxIn] = inputs.map(_.txIn) + val txOut: Seq[TxOut] = changeOutput_opt.toSeq + val spentUtxos: Map[OutPoint, TxOut] = inputs.map(i => i.txIn.outPoint -> i.spentOutput).toMap + + /** Set the change output. */ + def setChangeOutput(amount: Satoshi, changeScript: ByteVector): WalletInputs = { + val changeOutput = TxOut(amount, changeScript) + this.copy(changeOutput_opt = Some(changeOutput)) + } + + /** Set the change output amount. */ + def setChangeAmount(amount: Satoshi): WalletInputs = { + this.copy(changeOutput_opt = changeOutput_opt.map(_.copy(amount = amount))) + } + } + + /** Transactions spending a [[CommitTx]] or one of its descendants. */ + sealed trait ForceCloseTransaction extends TransactionWithInputInfo { + // @formatter:off + def commitmentFormat: CommitmentFormat + def expectedWeight: Int + // @formatter:on + + def sign(): Transaction + + /** Sighash flags to use when signing the transaction. */ + def sighash: Int = commitmentFormat match { + case _: SegwitV0CommitmentFormat => SIGHASH_ALL + case _: SimpleTaprootChannelCommitmentFormat => SIGHASH_DEFAULT + } + } + + /** Some force-close transactions require wallet inputs to pay on-chain fees. */ + sealed trait HasWalletInputs extends ForceCloseTransaction { + /** Create redeem information for this transaction, based on the commitment format used. */ + def redeemInfo: RedeemInfo + + /** Sign the transaction combined with the wallet inputs provided. */ + def sign(walletInputs: WalletInputs): Transaction + + override def sign(): Transaction = sign(WalletInputs(Nil, None)) + + protected def setWalletInputs(walletInputs: WalletInputs): Transaction = { + // Note that we always keep the channel input in first position for simplicity. + val txIn = tx.txIn.take(1) ++ walletInputs.txIn + val txOut = tx.txOut.headOption.toSeq ++ walletInputs.changeOutput_opt.toSeq + tx.copy(txIn = txIn, txOut = txOut) + } + } + + /** + * Transactions spending a local [[CommitTx]] or one of its descendants: + * - [[ClaimLocalDelayedOutputTx]] spends the to-local output of [[CommitTx]] after a delay + * - When using anchor outputs, [[ClaimLocalAnchorTx]] spends the to-local anchor of [[CommitTx]] + * - [[HtlcSuccessTx]] spends received htlc outputs of [[CommitTx]] for which we have the preimage + * - [[HtlcDelayedTx]] spends [[HtlcSuccessTx]] after a delay + * - [[HtlcTimeoutTx]] spends sent htlc outputs of [[CommitTx]] after a timeout + * - [[HtlcDelayedTx]] spends [[HtlcTimeoutTx]] after a delay + */ + sealed trait LocalCommitForceCloseTransaction extends ForceCloseTransaction { + def commitKeys: LocalCommitmentKeys } - sealed trait ReplaceableTransactionWithInputInfo extends TransactionWithInputInfo { - /** Block before which the transaction must be confirmed. */ - def confirmationTarget: ConfirmationTarget + /** + * Transactions spending a remote [[CommitTx]] or one of its descendants. + * + * When a current remote [[CommitTx]] is published: + * - When using anchor outputs, [[ClaimRemoteDelayedOutputTx]] spends the to-local output of [[CommitTx]] + * - When using anchor outputs, [[ClaimRemoteAnchorTx]] spends the to-local anchor of [[CommitTx]] + * - [[ClaimHtlcSuccessTx]] spends received htlc outputs of [[CommitTx]] for which we have the preimage + * - [[ClaimHtlcTimeoutTx]] spends sent htlc outputs of [[CommitTx]] after a timeout + * + * When a revoked remote [[CommitTx]] is published: + * - When using anchor outputs, [[ClaimRemoteDelayedOutputTx]] spends the to-local output of [[CommitTx]] + * - [[MainPenaltyTx]] spends the remote main output using the revocation secret + * - [[HtlcPenaltyTx]] spends all htlc outputs using the revocation secret (and competes with [[HtlcSuccessTx]] and [[HtlcTimeoutTx]] published by the remote node) + * - [[ClaimHtlcDelayedOutputPenaltyTx]] spends [[HtlcSuccessTx]] transactions published by the remote node using the revocation secret + * - [[ClaimHtlcDelayedOutputPenaltyTx]] spends [[HtlcTimeoutTx]] transactions published by the remote node using the revocation secret + */ + sealed trait RemoteCommitForceCloseTransaction extends ForceCloseTransaction { + def commitKeys: RemoteCommitmentKeys } - case class SpliceTx(input: InputInfo, tx: Transaction) extends TransactionWithInputInfo { override def desc: String = "splice-tx" } + // @formatter:off + /** Owner of a given HTLC transaction (local/remote). */ + sealed trait TxOwner + private object TxOwner { + case object Local extends TxOwner + case object Remote extends TxOwner + } + // @formatter:on - case class CommitTx(input: InputInfo, tx: Transaction) extends TransactionWithInputInfo { override def desc: String = "commit-tx" } /** - * It's important to note that htlc transactions with the default commitment format are not actually replaceable: only - * anchor outputs htlc transactions are replaceable. We should have used different types for these different kinds of - * htlc transactions, but we introduced that before implementing the replacement strategy. - * Unfortunately, if we wanted to change that, we would have to update the codecs and implement a migration of channel - * data, which isn't trivial, so we chose to temporarily live with that inconsistency (and have the transaction - * replacement logic abort when non-anchor outputs htlc transactions are provided). - * Ideally, we'd like to implement a dynamic commitment format upgrade mechanism and depreciate the pre-anchor outputs - * format soon, which will get rid of this inconsistency. - * The next time we introduce a new type of commitment, we should avoid repeating that mistake and define separate - * types right from the start. + * HTLC transactions require local and remote signatures and can be spent using two distinct script paths: + * - the success path by revealing the payment preimage + * - the timeout path after a predefined block height + * + * The success path must be used before the timeout is reached, otherwise there is a race where both channel + * participants may claim the output. + * + * Once confirmed, HTLC transactions need to be spent by an [[HtlcDelayedTx]] after a relative delay to get the funds + * back into our bitcoin wallet. */ - sealed trait HtlcTx extends ReplaceableTransactionWithInputInfo { + sealed trait HtlcTx extends TransactionWithInputInfo { + // @formatter:off def htlcId: Long - override def sighash(txOwner: TxOwner, commitmentFormat: CommitmentFormat): Int = commitmentFormat match { - case DefaultCommitmentFormat => SIGHASH_ALL + def paymentHash: ByteVector32 + def htlcExpiry: CltvExpiry + def commitmentFormat: CommitmentFormat + // @formatter:on + + def sighash(txOwner: TxOwner): Int = commitmentFormat match { case _: AnchorOutputsCommitmentFormat => txOwner match { case TxOwner.Local => SIGHASH_ALL case TxOwner.Remote => SIGHASH_SINGLE | SIGHASH_ANYONECANPAY } + case _: SimpleTaprootChannelCommitmentFormat => txOwner match { + case TxOwner.Local => SIGHASH_DEFAULT + case TxOwner.Remote => SIGHASH_SINGLE | SIGHASH_ANYONECANPAY + } + } + } + + /** + * We first create unsigned HTLC transactions based on the [[CommitTx]]: this lets us produce our local signature, + * which we need to send to our peer for their commitment. + */ + sealed trait UnsignedHtlcTx extends HtlcTx { + /** Create redeem information for this HTLC transaction, based on the commitment format used. */ + def redeemInfo(commitKeys: CommitmentPublicKeys): RedeemInfo + + /** Sign an HTLC transaction for the remote commitment. */ + def localSig(commitKeys: RemoteCommitmentKeys): ByteVector64 = { + sign(commitKeys.ourHtlcKey, sighash(TxOwner.Remote), redeemInfo(commitKeys.publicKeys), extraUtxos = Map.empty) + } + + /** This is a function only used in tests to produce signatures with a different sighash. */ + def localSigWithInvalidSighash(commitKeys: RemoteCommitmentKeys, sighash: Int): ByteVector64 = { + sign(commitKeys.ourHtlcKey, sighash, redeemInfo(commitKeys.publicKeys), extraUtxos = Map.empty) + } + + def checkRemoteSig(commitKeys: LocalCommitmentKeys, remoteSig: ByteVector64): Boolean = { + // The transaction was signed by our remote for us: from their point of view, we're a remote owner. + val remoteSighash = sighash(TxOwner.Remote) + checkSig(remoteSig, commitKeys.theirHtlcPublicKey, remoteSighash, redeemInfo(commitKeys.publicKeys)) + } + } + + /** + * Once we've received valid signatures from our peer and the payment preimage for incoming HTLCs, we can create fully + * signed HTLC transactions for our local [[CommitTx]]. + */ + sealed trait SignedHtlcTx extends HtlcTx with LocalCommitForceCloseTransaction with HasWalletInputs { + /** Sign an HTLC transaction spending our local commitment. */ + def localSig(walletInputs: WalletInputs): ByteVector64 = { + sign(commitKeys.ourHtlcKey, sighash(TxOwner.Local), redeemInfo, walletInputs.spentUtxos) + } + } + + /** This transaction spends a received (incoming) HTLC from a local or remote commitment by revealing the payment preimage. */ + case class HtlcSuccessTx(commitKeys: LocalCommitmentKeys, input: InputInfo, tx: Transaction, htlcId: Long, htlcExpiry: CltvExpiry, preimage: ByteVector32, remoteSig: ByteVector64, commitmentFormat: CommitmentFormat) extends SignedHtlcTx { + override val desc: String = "htlc-success" + override val paymentHash: ByteVector32 = Crypto.sha256(preimage) + override val redeemInfo: RedeemInfo = HtlcSuccessTx.redeemInfo(commitKeys.publicKeys, paymentHash, htlcExpiry, commitmentFormat) + override val expectedWeight: Int = commitmentFormat.htlcSuccessWeight + + override def sign(walletInputs: WalletInputs): Transaction = { + val toSign = copy(tx = setWalletInputs(walletInputs)) + val sig = toSign.localSig(walletInputs) + val witness = redeemInfo match { + case redeemInfo: RedeemInfo.SegwitV0 => + witnessHtlcSuccess(sig, remoteSig, preimage, redeemInfo.redeemScript, commitmentFormat) + case _: RedeemInfo.Taproot => + val receivedHtlcTree = Taproot.receivedHtlcScriptTree(commitKeys.publicKeys, paymentHash, htlcExpiry) + receivedHtlcTree.witnessSuccess(commitKeys, sig, remoteSig, preimage) + } + toSign.tx.updateWitness(toSign.inputIndex, witness) + } + } + + case class UnsignedHtlcSuccessTx(input: InputInfo, tx: Transaction, paymentHash: ByteVector32, htlcId: Long, htlcExpiry: CltvExpiry, commitmentFormat: CommitmentFormat) extends UnsignedHtlcTx { + override val desc: String = "htlc-success" + + override def redeemInfo(commitKeys: CommitmentPublicKeys): RedeemInfo = HtlcSuccessTx.redeemInfo(commitKeys, paymentHash, htlcExpiry, commitmentFormat) + + def addRemoteSig(commitKeys: LocalCommitmentKeys, remoteSig: ByteVector64, preimage: ByteVector32): HtlcSuccessTx = HtlcSuccessTx(commitKeys, input, tx, htlcId, htlcExpiry, preimage, remoteSig, commitmentFormat) + } + + object HtlcSuccessTx { + def createUnsignedTx(commitTx: Transaction, + output: InHtlc, + outputIndex: Int, + commitmentFormat: CommitmentFormat): UnsignedHtlcSuccessTx = { + val htlc = output.htlc.add + val input = InputInfo(OutPoint(commitTx, outputIndex), commitTx.txOut(outputIndex)) + val tx = Transaction( + version = 2, + txIn = TxIn(input.outPoint, ByteVector.empty, getHtlcTxInputSequence(commitmentFormat)) :: Nil, + txOut = output.htlcDelayedOutput :: Nil, + lockTime = 0 + ) + UnsignedHtlcSuccessTx(input, tx, htlc.paymentHash, htlc.id, htlc.cltvExpiry, commitmentFormat) + } + + def redeemInfo(commitKeys: CommitmentPublicKeys, paymentHash: ByteVector32, htlcExpiry: CltvExpiry, commitmentFormat: CommitmentFormat): RedeemInfo = commitmentFormat match { + case _: AnchorOutputsCommitmentFormat => + val redeemScript = Script.write(htlcReceived(commitKeys, paymentHash, htlcExpiry, commitmentFormat)) + RedeemInfo.P2wsh(redeemScript) + case _: SimpleTaprootChannelCommitmentFormat => + val receivedHtlcTree = Taproot.receivedHtlcScriptTree(commitKeys, paymentHash, htlcExpiry) + RedeemInfo.TaprootScriptPath(commitKeys.revocationPublicKey.xOnly, receivedHtlcTree.scriptTree, receivedHtlcTree.success.hash()) + } + } + + /** This transaction spends an offered (outgoing) HTLC from a local or remote commitment after its expiry. */ + case class HtlcTimeoutTx(commitKeys: LocalCommitmentKeys, input: InputInfo, tx: Transaction, paymentHash: ByteVector32, htlcId: Long, htlcExpiry: CltvExpiry, remoteSig: ByteVector64, commitmentFormat: CommitmentFormat) extends SignedHtlcTx { + override val desc: String = "htlc-timeout" + override val redeemInfo: RedeemInfo = HtlcTimeoutTx.redeemInfo(commitKeys.publicKeys, paymentHash, commitmentFormat) + override val expectedWeight: Int = commitmentFormat.htlcTimeoutWeight + + def sign(walletInputs: WalletInputs): Transaction = { + val toSign = copy(tx = setWalletInputs(walletInputs)) + val sig = toSign.localSig(walletInputs) + val witness = redeemInfo match { + case redeemInfo: RedeemInfo.SegwitV0 => + witnessHtlcTimeout(sig, remoteSig, redeemInfo.redeemScript, commitmentFormat) + case _: RedeemInfo.Taproot => + val offeredHtlcTree = Taproot.offeredHtlcScriptTree(commitKeys.publicKeys, paymentHash) + offeredHtlcTree.witnessTimeout(commitKeys, sig, remoteSig) + } + toSign.tx.updateWitness(toSign.inputIndex, witness) + } + } + + case class UnsignedHtlcTimeoutTx(input: InputInfo, tx: Transaction, paymentHash: ByteVector32, htlcId: Long, htlcExpiry: CltvExpiry, commitmentFormat: CommitmentFormat) extends UnsignedHtlcTx { + override val desc: String = "htlc-timeout" + + override def redeemInfo(commitKeys: CommitmentPublicKeys): RedeemInfo = HtlcTimeoutTx.redeemInfo(commitKeys, paymentHash, commitmentFormat) + + def addRemoteSig(commitKeys: LocalCommitmentKeys, remoteSig: ByteVector64): HtlcTimeoutTx = HtlcTimeoutTx(commitKeys, input, tx, paymentHash, htlcId, htlcExpiry, remoteSig, commitmentFormat) + } + + object HtlcTimeoutTx { + def createUnsignedTx(commitTx: Transaction, + output: OutHtlc, + outputIndex: Int, + commitmentFormat: CommitmentFormat): UnsignedHtlcTimeoutTx = { + val htlc = output.htlc.add + val input = InputInfo(OutPoint(commitTx, outputIndex), commitTx.txOut(outputIndex)) + val tx = Transaction( + version = 2, + txIn = TxIn(input.outPoint, ByteVector.empty, getHtlcTxInputSequence(commitmentFormat)) :: Nil, + txOut = output.htlcDelayedOutput :: Nil, + lockTime = htlc.cltvExpiry.toLong + ) + UnsignedHtlcTimeoutTx(input, tx, htlc.paymentHash, htlc.id, htlc.cltvExpiry, commitmentFormat) + } + + def redeemInfo(commitKeys: CommitmentPublicKeys, paymentHash: ByteVector32, commitmentFormat: CommitmentFormat): RedeemInfo = commitmentFormat match { + case _: AnchorOutputsCommitmentFormat => + val redeemScript = Script.write(htlcOffered(commitKeys, paymentHash, commitmentFormat)) + RedeemInfo.P2wsh(redeemScript) + case _: SimpleTaprootChannelCommitmentFormat => + val offeredHtlcTree = Taproot.offeredHtlcScriptTree(commitKeys, paymentHash) + RedeemInfo.TaprootScriptPath(commitKeys.revocationPublicKey.xOnly, offeredHtlcTree.scriptTree, offeredHtlcTree.timeout.hash()) + } + } + + /** This transaction spends the output of a local [[HtlcTx]] after a to_self_delay relative delay. */ + case class HtlcDelayedTx(commitKeys: LocalCommitmentKeys, input: InputInfo, tx: Transaction, toLocalDelay: CltvExpiryDelta, commitmentFormat: CommitmentFormat) extends LocalCommitForceCloseTransaction { + override val desc: String = "htlc-delayed" + override val expectedWeight: Int = commitmentFormat.htlcDelayedWeight + + override def sign(): Transaction = { + val witness = commitmentFormat match { + case _: AnchorOutputsCommitmentFormat => + val redeemScript = Script.write(toLocalDelayed(commitKeys.publicKeys, toLocalDelay)) + val sig = sign(commitKeys.ourDelayedPaymentKey, sighash, RedeemInfo.P2wsh(redeemScript), extraUtxos = Map.empty) + witnessToLocalDelayedAfterDelay(sig, redeemScript) + case _: SimpleTaprootChannelCommitmentFormat => + val scriptTree: ScriptTree.Leaf = Taproot.htlcDelayedScriptTree(commitKeys.publicKeys, toLocalDelay) + val redeemInfo = RedeemInfo.TaprootScriptPath(commitKeys.revocationPublicKey.xOnly, scriptTree, scriptTree.hash()) + val sig = sign(commitKeys.ourDelayedPaymentKey, sighash, redeemInfo, extraUtxos = Map.empty) + Script.witnessScriptPathPay2tr(commitKeys.revocationPublicKey.xOnly, scriptTree, ScriptWitness(Seq(sig)), scriptTree) + } + tx.updateWitness(inputIndex, witness) } - override def confirmationTarget: ConfirmationTarget.Absolute } - case class HtlcSuccessTx(input: InputInfo, tx: Transaction, paymentHash: ByteVector32, htlcId: Long, confirmationTarget: ConfirmationTarget.Absolute) extends HtlcTx { override def desc: String = "htlc-success" } - case class HtlcTimeoutTx(input: InputInfo, tx: Transaction, htlcId: Long, confirmationTarget: ConfirmationTarget.Absolute) extends HtlcTx { override def desc: String = "htlc-timeout" } - case class HtlcDelayedTx(input: InputInfo, tx: Transaction) extends TransactionWithInputInfo { override def desc: String = "htlc-delayed" } - sealed trait ClaimHtlcTx extends ReplaceableTransactionWithInputInfo { + + object HtlcDelayedTx { + def createUnsignedTx(commitKeys: LocalCommitmentKeys, htlcTx: Transaction, localDustLimit: Satoshi, toLocalDelay: CltvExpiryDelta, localFinalScriptPubKey: ByteVector, feerate: FeeratePerKw, commitmentFormat: CommitmentFormat): Either[TxGenerationSkipped, HtlcDelayedTx] = { + val pubkeyScript = redeemInfo(commitKeys.publicKeys, toLocalDelay, commitmentFormat).pubkeyScript + findPubKeyScriptIndex(htlcTx, pubkeyScript) match { + case Left(skip) => Left(skip) + case Right(outputIndex) => + val input = InputInfo(OutPoint(htlcTx, outputIndex), htlcTx.txOut(outputIndex)) + val amount = input.txOut.amount - weight2fee(feerate, commitmentFormat.htlcDelayedWeight) + val tx = Transaction( + version = 2, + txIn = TxIn(input.outPoint, ByteVector.empty, toLocalDelay.toInt) :: Nil, + txOut = TxOut(amount, localFinalScriptPubKey) :: Nil, + lockTime = 0 + ) + val unsignedTx = HtlcDelayedTx(commitKeys, input, tx, toLocalDelay, commitmentFormat) + skipTxIfBelowDust(unsignedTx, localDustLimit) + } + } + + def redeemInfo(commitKeys: CommitmentPublicKeys, toLocalDelay: CltvExpiryDelta, commitmentFormat: CommitmentFormat): RedeemInfo = commitmentFormat match { + case _: AnchorOutputsCommitmentFormat => + val redeemScript = Script.write(toLocalDelayed(commitKeys, toLocalDelay)) + RedeemInfo.P2wsh(redeemScript) + case _: SimpleTaprootChannelCommitmentFormat => + val scriptTree: ScriptTree.Leaf = Taproot.htlcDelayedScriptTree(commitKeys, toLocalDelay) + RedeemInfo.TaprootScriptPath(commitKeys.revocationPublicKey.xOnly, scriptTree, scriptTree.hash()) + } + } + + sealed trait ClaimHtlcTx extends RemoteCommitForceCloseTransaction { + // @formatter:off def htlcId: Long - override def confirmationTarget: ConfirmationTarget.Absolute - } - case class LegacyClaimHtlcSuccessTx(input: InputInfo, tx: Transaction, htlcId: Long, confirmationTarget: ConfirmationTarget.Absolute) extends ClaimHtlcTx { override def desc: String = "claim-htlc-success" } - case class ClaimHtlcSuccessTx(input: InputInfo, tx: Transaction, paymentHash: ByteVector32, htlcId: Long, confirmationTarget: ConfirmationTarget.Absolute) extends ClaimHtlcTx { override def desc: String = "claim-htlc-success" } - case class ClaimHtlcTimeoutTx(input: InputInfo, tx: Transaction, htlcId: Long, confirmationTarget: ConfirmationTarget.Absolute) extends ClaimHtlcTx { override def desc: String = "claim-htlc-timeout" } - sealed trait ClaimAnchorOutputTx extends TransactionWithInputInfo - case class ClaimLocalAnchorOutputTx(input: InputInfo, tx: Transaction, confirmationTarget: ConfirmationTarget) extends ClaimAnchorOutputTx with ReplaceableTransactionWithInputInfo { override def desc: String = "local-anchor" } - case class ClaimRemoteAnchorOutputTx(input: InputInfo, tx: Transaction) extends ClaimAnchorOutputTx { override def desc: String = "remote-anchor" } - sealed trait ClaimRemoteCommitMainOutputTx extends TransactionWithInputInfo - case class ClaimP2WPKHOutputTx(input: InputInfo, tx: Transaction) extends ClaimRemoteCommitMainOutputTx { override def desc: String = "remote-main" } - case class ClaimRemoteDelayedOutputTx(input: InputInfo, tx: Transaction) extends ClaimRemoteCommitMainOutputTx { override def desc: String = "remote-main-delayed" } - case class ClaimLocalDelayedOutputTx(input: InputInfo, tx: Transaction) extends TransactionWithInputInfo { override def desc: String = "local-main-delayed" } - case class MainPenaltyTx(input: InputInfo, tx: Transaction) extends TransactionWithInputInfo { override def desc: String = "main-penalty" } - case class HtlcPenaltyTx(input: InputInfo, tx: Transaction) extends TransactionWithInputInfo { override def desc: String = "htlc-penalty" } - case class ClaimHtlcDelayedOutputPenaltyTx(input: InputInfo, tx: Transaction) extends TransactionWithInputInfo { override def desc: String = "htlc-delayed-penalty" } - case class ClosingTx(input: InputInfo, tx: Transaction, toLocalOutput: Option[OutputInfo]) extends TransactionWithInputInfo { override def desc: String = "closing" } + def paymentHash: ByteVector32 + def htlcExpiry: CltvExpiry + // @formatter:on + } + + /** This transaction spends an HTLC we received by revealing the payment preimage, from the remote commitment. */ + case class ClaimHtlcSuccessTx(commitKeys: RemoteCommitmentKeys, input: InputInfo, tx: Transaction, preimage: ByteVector32, htlcId: Long, htlcExpiry: CltvExpiry, commitmentFormat: CommitmentFormat) extends ClaimHtlcTx { + override val desc: String = "claim-htlc-success" + override val paymentHash: ByteVector32 = Crypto.sha256(preimage) + override val expectedWeight: Int = commitmentFormat.claimHtlcSuccessWeight + + override def sign(): Transaction = { + // Note that in/out HTLCs are inverted in the remote commitment: from their point of view it's an offered (outgoing) HTLC. + val witness = commitmentFormat match { + case _: AnchorOutputsCommitmentFormat => + val redeemScript = Script.write(htlcOffered(commitKeys.publicKeys, paymentHash, commitmentFormat)) + val sig = sign(commitKeys.ourHtlcKey, sighash, RedeemInfo.P2wsh(redeemScript), extraUtxos = Map.empty) + witnessClaimHtlcSuccessFromCommitTx(sig, preimage, redeemScript) + case _: SimpleTaprootChannelCommitmentFormat => + val offeredTree = Taproot.offeredHtlcScriptTree(commitKeys.publicKeys, paymentHash) + val redeemInfo = RedeemInfo.TaprootScriptPath(commitKeys.revocationPublicKey.xOnly, offeredTree.scriptTree, offeredTree.success.hash()) + val sig = sign(commitKeys.ourHtlcKey, sighash, redeemInfo, extraUtxos = Map.empty) + offeredTree.witnessSuccess(commitKeys, sig, preimage) + } + tx.updateWitness(inputIndex, witness) + } + } + + object ClaimHtlcSuccessTx { + /** + * Find the output of the commitment transaction matching this HTLC. + * Note that we match on a specific HTLC, because we may have multiple HTLCs with the same payment_hash, expiry + * and amount and thus the same pubkeyScript, and we must make sure we claim them all. + */ + def findInput(commitTx: Transaction, outputs: Seq[CommitmentOutput], htlc: UpdateAddHtlc): Option[InputInfo] = { + outputs.zipWithIndex.collectFirst { + case (OutHtlc(outgoingHtlc, _, _), outputIndex) if outgoingHtlc.add.id == htlc.id => + InputInfo(OutPoint(commitTx, outputIndex), commitTx.txOut(outputIndex)) + } + } + + def createUnsignedTx(commitKeys: RemoteCommitmentKeys, + commitTx: Transaction, + dustLimit: Satoshi, + outputs: Seq[CommitmentOutput], + localFinalScriptPubKey: ByteVector, + htlc: UpdateAddHtlc, + preimage: ByteVector32, + feerate: FeeratePerKw, + commitmentFormat: CommitmentFormat): Either[TxGenerationSkipped, ClaimHtlcSuccessTx] = { + findInput(commitTx, outputs, htlc) match { + case Some(input) => + val amount = input.txOut.amount - weight2fee(feerate, commitmentFormat.claimHtlcSuccessWeight) + val tx = Transaction( + version = 2, + txIn = TxIn(input.outPoint, ByteVector.empty, getHtlcTxInputSequence(commitmentFormat)) :: Nil, + txOut = TxOut(amount, localFinalScriptPubKey) :: Nil, + lockTime = 0 + ) + val unsignedTx = ClaimHtlcSuccessTx(commitKeys, input, tx, preimage, htlc.id, htlc.cltvExpiry, commitmentFormat) + skipTxIfBelowDust(unsignedTx, dustLimit) + case None => Left(OutputNotFound) + } + } + } + + /** This transaction spends an HTLC we sent after its expiry, from the remote commitment. */ + case class ClaimHtlcTimeoutTx(commitKeys: RemoteCommitmentKeys, input: InputInfo, tx: Transaction, paymentHash: ByteVector32, htlcId: Long, htlcExpiry: CltvExpiry, commitmentFormat: CommitmentFormat) extends ClaimHtlcTx { + override val desc: String = "claim-htlc-timeout" + override val expectedWeight: Int = commitmentFormat.claimHtlcTimeoutWeight + + override def sign(): Transaction = { + // Note that in/out HTLCs are inverted in the remote commitment: from their point of view it's a received (incoming) HTLC. + val witness = commitmentFormat match { + case _: AnchorOutputsCommitmentFormat => + val redeemScript = Script.write(htlcReceived(commitKeys.publicKeys, paymentHash, htlcExpiry, commitmentFormat)) + val sig = sign(commitKeys.ourHtlcKey, sighash, RedeemInfo.P2wsh(redeemScript), extraUtxos = Map.empty) + witnessClaimHtlcTimeoutFromCommitTx(sig, redeemScript) + case _: SimpleTaprootChannelCommitmentFormat => + val offeredTree = Taproot.receivedHtlcScriptTree(commitKeys.publicKeys, paymentHash, htlcExpiry) + val redeemInfo = RedeemInfo.TaprootScriptPath(commitKeys.revocationPublicKey.xOnly, offeredTree.scriptTree, offeredTree.timeout.hash()) + val sig = sign(commitKeys.ourHtlcKey, sighash, redeemInfo, extraUtxos = Map.empty) + offeredTree.witnessTimeout(commitKeys, sig) + } + tx.updateWitness(inputIndex, witness) + } + } + + object ClaimHtlcTimeoutTx { + /** + * Find the output of the commitment transaction matching this HTLC. + * Note that we match on a specific HTLC, because we may have multiple HTLCs with the same payment_hash, expiry + * and amount and thus the same pubkeyScript, and we must make sure we claim them all. + */ + def findInput(commitTx: Transaction, outputs: Seq[CommitmentOutput], htlc: UpdateAddHtlc): Option[InputInfo] = { + outputs.zipWithIndex.collectFirst { + case (InHtlc(incomingHtlc, _, _), outputIndex) if incomingHtlc.add.id == htlc.id => + InputInfo(OutPoint(commitTx, outputIndex), commitTx.txOut(outputIndex)) + } + } + def createUnsignedTx(commitKeys: RemoteCommitmentKeys, + commitTx: Transaction, + dustLimit: Satoshi, + outputs: Seq[CommitmentOutput], + localFinalScriptPubKey: ByteVector, + htlc: UpdateAddHtlc, + feerate: FeeratePerKw, + commitmentFormat: CommitmentFormat): Either[TxGenerationSkipped, ClaimHtlcTimeoutTx] = { + findInput(commitTx, outputs, htlc) match { + case Some(input) => + val amount = input.txOut.amount - weight2fee(feerate, commitmentFormat.claimHtlcTimeoutWeight) + val tx = Transaction( + version = 2, + txIn = TxIn(input.outPoint, ByteVector.empty, getHtlcTxInputSequence(commitmentFormat)) :: Nil, + txOut = TxOut(amount, localFinalScriptPubKey) :: Nil, + lockTime = htlc.cltvExpiry.toLong + ) + val unsignedTx = ClaimHtlcTimeoutTx(commitKeys, input, tx, htlc.paymentHash, htlc.id, htlc.cltvExpiry, commitmentFormat) + skipTxIfBelowDust(unsignedTx, dustLimit) + case None => Left(OutputNotFound) + } + } + } + + /** This transaction claims our anchor output to CPFP the parent commitment transaction and get it confirmed. */ + sealed trait ClaimAnchorTx extends ForceCloseTransaction with HasWalletInputs { + // On top of the anchor input, the weight includes the nVersion field, nLockTime and other shared fields. + override def expectedWeight: Int = commitmentFormat.anchorInputWeight + 42 + } + + object ClaimAnchorTx { + def redeemInfo(fundingKey: PublicKey, paymentKey: PublicKey, commitmentFormat: CommitmentFormat): RedeemInfo = { + commitmentFormat match { + case _: AnchorOutputsCommitmentFormat => RedeemInfo.P2wsh(anchor(fundingKey)) + case _: SimpleTaprootChannelCommitmentFormat => RedeemInfo.TaprootKeyPath(paymentKey.xOnly, Some(Taproot.anchorScriptTree)) + } + } + + def createUnsignedTx(input: InputInfo): Transaction = { + Transaction( + version = 2, + txIn = TxIn(input.outPoint, ByteVector.empty, 0) :: Nil, + txOut = Nil, // anchor is only used to bump fees, the output will be added later depending on available inputs + lockTime = 0 + ) + } + } + + /** This transaction claims our anchor output in our local commitment. */ + case class ClaimLocalAnchorTx(fundingKey: PrivateKey, commitKeys: LocalCommitmentKeys, input: InputInfo, tx: Transaction, commitmentFormat: CommitmentFormat) extends ClaimAnchorTx with LocalCommitForceCloseTransaction { + override val desc: String = "local-anchor" + override val redeemInfo: RedeemInfo = ClaimLocalAnchorTx.redeemInfo(fundingKey.publicKey, commitKeys.publicKeys, commitmentFormat) + + override def sign(walletInputs: WalletInputs): Transaction = { + val toSign = copy(tx = setWalletInputs(walletInputs)) + val witness = commitmentFormat match { + case _: AnchorOutputsCommitmentFormat => + val redeemScript = Script.write(anchor(fundingKey.publicKey)) + val sig = toSign.sign(fundingKey, sighash, RedeemInfo.P2wsh(redeemScript), walletInputs.spentUtxos) + witnessAnchor(sig, redeemScript) + case _: SimpleTaprootChannelCommitmentFormat => + val anchorKey = commitKeys.ourDelayedPaymentKey + val redeemInfo = RedeemInfo.TaprootKeyPath(anchorKey.xOnlyPublicKey(), Some(Taproot.anchorScriptTree)) + val sig = toSign.sign(anchorKey, sighash, redeemInfo, walletInputs.spentUtxos) + Script.witnessKeyPathPay2tr(sig) + } + toSign.tx.updateWitness(toSign.inputIndex, witness) + } + } + + object ClaimLocalAnchorTx { + def redeemInfo(fundingKey: PublicKey, commitKeys: CommitmentPublicKeys, commitmentFormat: CommitmentFormat): RedeemInfo = { + ClaimAnchorTx.redeemInfo(fundingKey, commitKeys.localDelayedPaymentPublicKey, commitmentFormat) + } + + def findInput(commitTx: Transaction, fundingKey: PrivateKey, commitKeys: LocalCommitmentKeys, commitmentFormat: CommitmentFormat): Either[TxGenerationSkipped, InputInfo] = { + val pubKeyScript = redeemInfo(fundingKey.publicKey, commitKeys.publicKeys, commitmentFormat).pubkeyScript + findPubKeyScriptIndex(commitTx, pubKeyScript).map(outputIndex => InputInfo(OutPoint(commitTx, outputIndex), commitTx.txOut(outputIndex))) + } + + def createUnsignedTx(fundingKey: PrivateKey, commitKeys: LocalCommitmentKeys, commitTx: Transaction, commitmentFormat: CommitmentFormat): Either[TxGenerationSkipped, ClaimLocalAnchorTx] = { + findInput(commitTx, fundingKey, commitKeys, commitmentFormat).map(input => ClaimLocalAnchorTx(fundingKey, commitKeys, input, ClaimAnchorTx.createUnsignedTx(input), commitmentFormat)) + } + } + + /** This transaction claims our anchor output in a remote commitment. */ + case class ClaimRemoteAnchorTx(fundingKey: PrivateKey, commitKeys: RemoteCommitmentKeys, input: InputInfo, tx: Transaction, commitmentFormat: CommitmentFormat) extends ClaimAnchorTx with RemoteCommitForceCloseTransaction { + override val desc: String = "remote-anchor" + override val redeemInfo: RedeemInfo = ClaimRemoteAnchorTx.redeemInfo(fundingKey.publicKey, commitKeys.publicKeys, commitmentFormat) + + override def sign(walletInputs: WalletInputs): Transaction = { + val toSign = copy(tx = setWalletInputs(walletInputs)) + val witness = commitmentFormat match { + case _: AnchorOutputsCommitmentFormat => + val redeemScript = Script.write(anchor(fundingKey.publicKey)) + val sig = toSign.sign(fundingKey, sighash, RedeemInfo.P2wsh(redeemScript), walletInputs.spentUtxos) + witnessAnchor(sig, redeemScript) + case _: SimpleTaprootChannelCommitmentFormat => + val redeemInfo = RedeemInfo.TaprootKeyPath(commitKeys.ourPaymentKey.xOnlyPublicKey(), Some(Taproot.anchorScriptTree)) + val sig = toSign.sign(commitKeys.ourPaymentKey, sighash, redeemInfo, walletInputs.spentUtxos) + Script.witnessKeyPathPay2tr(sig) + } + toSign.tx.updateWitness(toSign.inputIndex, witness) + } + } + + object ClaimRemoteAnchorTx { + def redeemInfo(fundingKey: PublicKey, commitKeys: CommitmentPublicKeys, commitmentFormat: CommitmentFormat): RedeemInfo = { + ClaimAnchorTx.redeemInfo(fundingKey, commitKeys.remotePaymentPublicKey, commitmentFormat) + } + + def findInput(commitTx: Transaction, fundingKey: PrivateKey, commitKeys: RemoteCommitmentKeys, commitmentFormat: CommitmentFormat): Either[TxGenerationSkipped, InputInfo] = { + val pubKeyScript = redeemInfo(fundingKey.publicKey, commitKeys.publicKeys, commitmentFormat).pubkeyScript + findPubKeyScriptIndex(commitTx, pubKeyScript).map(outputIndex => InputInfo(OutPoint(commitTx, outputIndex), commitTx.txOut(outputIndex))) + } + + def createUnsignedTx(fundingKey: PrivateKey, commitKeys: RemoteCommitmentKeys, commitTx: Transaction, commitmentFormat: CommitmentFormat): Either[TxGenerationSkipped, ClaimRemoteAnchorTx] = { + findInput(commitTx, fundingKey, commitKeys, commitmentFormat).map(input => ClaimRemoteAnchorTx(fundingKey, commitKeys, input, ClaimAnchorTx.createUnsignedTx(input), commitmentFormat)) + } + } + + /** This transaction spends our main balance from the remote commitment with a 1-block relative delay. */ + case class ClaimRemoteDelayedOutputTx(commitKeys: RemoteCommitmentKeys, input: InputInfo, tx: Transaction, commitmentFormat: CommitmentFormat) extends RemoteCommitForceCloseTransaction { + override val desc: String = "remote-main-delayed" + override val expectedWeight: Int = commitmentFormat.toRemoteWeight + + override def sign(): Transaction = { + val witness = commitmentFormat match { + case _: AnchorOutputsCommitmentFormat => + val redeemScript = Script.write(toRemoteDelayed(commitKeys.publicKeys)) + val sig = sign(commitKeys.ourPaymentKey, sighash, RedeemInfo.P2wsh(redeemScript), extraUtxos = Map.empty) + witnessClaimToRemoteDelayedFromCommitTx(sig, redeemScript) + case _: SimpleTaprootChannelCommitmentFormat => + val scriptTree: ScriptTree.Leaf = Taproot.toRemoteScriptTree(commitKeys.publicKeys) + val redeemInfo = RedeemInfo.TaprootScriptPath(NUMS_POINT.xOnly, scriptTree, scriptTree.hash()) + val sig = sign(commitKeys.ourPaymentKey, sighash, redeemInfo, extraUtxos = Map.empty) + Script.witnessScriptPathPay2tr(redeemInfo.internalKey, scriptTree, ScriptWitness(Seq(sig)), scriptTree) + } + tx.updateWitness(inputIndex, witness) + } + } + + object ClaimRemoteDelayedOutputTx { + def createUnsignedTx(commitKeys: RemoteCommitmentKeys, commitTx: Transaction, localDustLimit: Satoshi, localFinalScriptPubKey: ByteVector, feerate: FeeratePerKw, commitmentFormat: CommitmentFormat): Either[TxGenerationSkipped, ClaimRemoteDelayedOutputTx] = { + val redeemInfo = commitmentFormat match { + case _: AnchorOutputsCommitmentFormat => + val redeemScript = Script.write(toRemoteDelayed(commitKeys.publicKeys)) + RedeemInfo.P2wsh(redeemScript) + case _: SimpleTaprootChannelCommitmentFormat => + val scriptTree: ScriptTree.Leaf = Taproot.toRemoteScriptTree(commitKeys.publicKeys) + RedeemInfo.TaprootScriptPath(NUMS_POINT.xOnly, scriptTree, scriptTree.hash()) + } + findPubKeyScriptIndex(commitTx, redeemInfo.pubkeyScript) match { + case Left(skip) => Left(skip) + case Right(outputIndex) => + val input = InputInfo(OutPoint(commitTx, outputIndex), commitTx.txOut(outputIndex)) + val amount = input.txOut.amount - weight2fee(feerate, commitmentFormat.toRemoteWeight) + val tx = Transaction( + version = 2, + txIn = TxIn(input.outPoint, ByteVector.empty, 1) :: Nil, + txOut = TxOut(amount, localFinalScriptPubKey) :: Nil, + lockTime = 0 + ) + val unsignedTx = ClaimRemoteDelayedOutputTx(commitKeys, input, tx, commitmentFormat) + skipTxIfBelowDust(unsignedTx, localDustLimit) + } + } + } + + /** This transaction spends our main balance from our commitment after a to_self_delay relative delay. */ + case class ClaimLocalDelayedOutputTx(commitKeys: LocalCommitmentKeys, input: InputInfo, tx: Transaction, toLocalDelay: CltvExpiryDelta, commitmentFormat: CommitmentFormat) extends LocalCommitForceCloseTransaction { + override val desc: String = "local-main-delayed" + override val expectedWeight: Int = commitmentFormat.toLocalDelayedWeight + + override def sign(): Transaction = { + val witness = commitmentFormat match { + case _: AnchorOutputsCommitmentFormat => + val redeemScript = Script.write(toLocalDelayed(commitKeys.publicKeys, toLocalDelay)) + val sig = sign(commitKeys.ourDelayedPaymentKey, sighash, RedeemInfo.P2wsh(redeemScript), extraUtxos = Map.empty) + witnessToLocalDelayedAfterDelay(sig, redeemScript) + case _: SimpleTaprootChannelCommitmentFormat => + val toLocalTree = Taproot.toLocalScriptTree(commitKeys.publicKeys, toLocalDelay) + val redeemInfo = RedeemInfo.TaprootScriptPath(NUMS_POINT.xOnly, toLocalTree.scriptTree, toLocalTree.localDelayed.hash()) + val sig = sign(commitKeys.ourDelayedPaymentKey, sighash, redeemInfo, extraUtxos = Map.empty) + Script.witnessScriptPathPay2tr(redeemInfo.internalKey, redeemInfo.leaf, ScriptWitness(Seq(sig)), toLocalTree.scriptTree) + } + tx.updateWitness(inputIndex, witness) + } + } + + object ClaimLocalDelayedOutputTx { + def createUnsignedTx(commitKeys: LocalCommitmentKeys, commitTx: Transaction, localDustLimit: Satoshi, toLocalDelay: CltvExpiryDelta, localFinalScriptPubKey: ByteVector, feerate: FeeratePerKw, commitmentFormat: CommitmentFormat): Either[TxGenerationSkipped, ClaimLocalDelayedOutputTx] = { + val redeemInfo = commitmentFormat match { + case _: AnchorOutputsCommitmentFormat => + val redeemScript = Script.write(toLocalDelayed(commitKeys.publicKeys, toLocalDelay)) + RedeemInfo.P2wsh(redeemScript) + case _: SimpleTaprootChannelCommitmentFormat => + val toLocalTree = Taproot.toLocalScriptTree(commitKeys.publicKeys, toLocalDelay) + RedeemInfo.TaprootScriptPath(NUMS_POINT.xOnly, toLocalTree.scriptTree, toLocalTree.localDelayed.hash()) + } + findPubKeyScriptIndex(commitTx, redeemInfo.pubkeyScript) match { + case Left(skip) => Left(skip) + case Right(outputIndex) => + val input = InputInfo(OutPoint(commitTx, outputIndex), commitTx.txOut(outputIndex)) + val amount = input.txOut.amount - weight2fee(feerate, commitmentFormat.toLocalDelayedWeight) + val tx = Transaction( + version = 2, + txIn = TxIn(input.outPoint, ByteVector.empty, toLocalDelay.toInt) :: Nil, + txOut = TxOut(amount, localFinalScriptPubKey) :: Nil, + lockTime = 0 + ) + val unsignedTx = ClaimLocalDelayedOutputTx(commitKeys, input, tx, toLocalDelay, commitmentFormat) + skipTxIfBelowDust(unsignedTx, localDustLimit) + } + } + } + + /** This transaction spends the remote main balance from one of their revoked commitments. */ + case class MainPenaltyTx(commitKeys: RemoteCommitmentKeys, revocationKey: PrivateKey, input: InputInfo, tx: Transaction, toRemoteDelay: CltvExpiryDelta, commitmentFormat: CommitmentFormat) extends RemoteCommitForceCloseTransaction { + override val desc: String = "main-penalty" + override val expectedWeight: Int = commitmentFormat.mainPenaltyWeight + + override def sign(): Transaction = { + val witness = commitmentFormat match { + case _: AnchorOutputsCommitmentFormat => + val redeemScript = Script.write(toLocalDelayed(commitKeys.publicKeys, toRemoteDelay)) + val sig = sign(revocationKey, sighash, RedeemInfo.P2wsh(redeemScript), extraUtxos = Map.empty) + Scripts.witnessToLocalDelayedWithRevocationSig(sig, redeemScript) + case _: SimpleTaprootChannelCommitmentFormat => + val toLocalTree = Taproot.toLocalScriptTree(commitKeys.publicKeys, toRemoteDelay) + val redeemInfo = RedeemInfo.TaprootScriptPath(NUMS_POINT.xOnly, toLocalTree.scriptTree, toLocalTree.revocation.hash()) + val sig = sign(revocationKey, sighash, redeemInfo, extraUtxos = Map.empty) + Script.witnessScriptPathPay2tr(redeemInfo.internalKey, redeemInfo.leaf, ScriptWitness(Seq(sig)), toLocalTree.scriptTree) + } + tx.updateWitness(inputIndex, witness) + } + } + + object MainPenaltyTx { + def createUnsignedTx(commitKeys: RemoteCommitmentKeys, revocationKey: PrivateKey, commitTx: Transaction, localDustLimit: Satoshi, localFinalScriptPubKey: ByteVector, toRemoteDelay: CltvExpiryDelta, feerate: FeeratePerKw, commitmentFormat: CommitmentFormat): Either[TxGenerationSkipped, MainPenaltyTx] = { + val redeemInfo = commitmentFormat match { + case _: AnchorOutputsCommitmentFormat => + val redeemScript = Script.write(toLocalDelayed(commitKeys.publicKeys, toRemoteDelay)) + RedeemInfo.P2wsh(redeemScript) + case _: SimpleTaprootChannelCommitmentFormat => + val toLocalTree = Taproot.toLocalScriptTree(commitKeys.publicKeys, toRemoteDelay) + RedeemInfo.TaprootScriptPath(NUMS_POINT.xOnly, toLocalTree.scriptTree, toLocalTree.revocation.hash()) + } + findPubKeyScriptIndex(commitTx, redeemInfo.pubkeyScript) match { + case Left(skip) => Left(skip) + case Right(outputIndex) => + val input = InputInfo(OutPoint(commitTx, outputIndex), commitTx.txOut(outputIndex)) + val amount = input.txOut.amount - weight2fee(feerate, commitmentFormat.mainPenaltyWeight) + val tx = Transaction( + version = 2, + txIn = TxIn(input.outPoint, ByteVector.empty, 0xffffffffL) :: Nil, + txOut = TxOut(amount, localFinalScriptPubKey) :: Nil, + lockTime = 0 + ) + val unsignedTx = MainPenaltyTx(commitKeys, revocationKey, input, tx, toRemoteDelay, commitmentFormat) + skipTxIfBelowDust(unsignedTx, localDustLimit) + } + } + } + + private case class HtlcPenaltyRedeemDetails(redeemInfo: RedeemInfo, paymentHash: ByteVector32, htlcExpiry: CltvExpiry, weight: Int) + + /** This transaction spends an HTLC output from one of the remote revoked commitments. */ + case class HtlcPenaltyTx(commitKeys: RemoteCommitmentKeys, revocationKey: PrivateKey, redeemInfo: RedeemInfo, input: InputInfo, tx: Transaction, paymentHash: ByteVector32, htlcExpiry: CltvExpiry, commitmentFormat: CommitmentFormat) extends RemoteCommitForceCloseTransaction { + override val desc: String = "htlc-penalty" + // We don't know if this is an incoming or outgoing HTLC, so we just use the bigger one (they are very close anyway). + override val expectedWeight: Int = commitmentFormat.htlcOfferedPenaltyWeight.max(commitmentFormat.htlcReceivedPenaltyWeight) + + override def sign(): Transaction = { + val sig = sign(revocationKey, sighash, redeemInfo, extraUtxos = Map.empty) + val witness = redeemInfo match { + case RedeemInfo.P2wsh(redeemScript) => Scripts.witnessHtlcWithRevocationSig(commitKeys, sig, redeemScript) + case _: RedeemInfo.TaprootKeyPath => Script.witnessKeyPathPay2tr(sig, sighash) + case s: RedeemInfo.TaprootScriptPath => Script.witnessScriptPathPay2tr(s.internalKey, s.leaf, ScriptWitness(Seq(sig)), s.scriptTree) + } + tx.updateWitness(inputIndex, witness) + } + } + + object HtlcPenaltyTx { + def createUnsignedTxs(commitKeys: RemoteCommitmentKeys, + revocationKey: PrivateKey, + commitTx: Transaction, + htlcs: Seq[(ByteVector32, CltvExpiry)], + localDustLimit: Satoshi, + localFinalScriptPubKey: ByteVector, + feerate: FeeratePerKw, + commitmentFormat: CommitmentFormat): Seq[Either[TxGenerationSkipped, HtlcPenaltyTx]] = { + // We create the output scripts for the corresponding HTLCs. + val redeemInfos: Map[ByteVector, HtlcPenaltyRedeemDetails] = htlcs.flatMap { + case (paymentHash, htlcExpiry) => + // We don't know if this was an incoming or outgoing HTLC, so we try both cases. + val (offered, received) = commitmentFormat match { + case _: AnchorOutputsCommitmentFormat => + (RedeemInfo.P2wsh(Script.write(htlcOffered(commitKeys.publicKeys, paymentHash, commitmentFormat))), + RedeemInfo.P2wsh(Script.write(htlcReceived(commitKeys.publicKeys, paymentHash, htlcExpiry, commitmentFormat)))) + case _: SimpleTaprootChannelCommitmentFormat => + (RedeemInfo.TaprootKeyPath(commitKeys.revocationPublicKey.xOnly, Some(Taproot.offeredHtlcScriptTree(commitKeys.publicKeys, paymentHash).scriptTree)), + RedeemInfo.TaprootKeyPath(commitKeys.revocationPublicKey.xOnly, Some(Taproot.receivedHtlcScriptTree(commitKeys.publicKeys, paymentHash, htlcExpiry).scriptTree))) + } + Seq( + offered.pubkeyScript -> HtlcPenaltyRedeemDetails(offered, paymentHash, htlcExpiry, commitmentFormat.htlcOfferedPenaltyWeight), + received.pubkeyScript -> HtlcPenaltyRedeemDetails(received, paymentHash, htlcExpiry, commitmentFormat.htlcReceivedPenaltyWeight), + ) + }.toMap + // We check every output of the commitment transaction, and create an HTLC-penalty transaction if it is an HTLC output. + commitTx.txOut.zipWithIndex.collect { + case (txOut, outputIndex) if redeemInfos.contains(txOut.publicKeyScript) => + val Some(redeemInfo) = redeemInfos.get(txOut.publicKeyScript) + createUnsignedTx(commitKeys, revocationKey, commitTx, outputIndex, redeemInfo, localDustLimit, localFinalScriptPubKey, feerate, commitmentFormat) + } + } + + private def createUnsignedTx(commitKeys: RemoteCommitmentKeys, + revocationKey: PrivateKey, + commitTx: Transaction, + htlcOutputIndex: Int, + redeemDetails: HtlcPenaltyRedeemDetails, + localDustLimit: Satoshi, + localFinalScriptPubKey: ByteVector, + feerate: FeeratePerKw, + commitmentFormat: CommitmentFormat): Either[TxGenerationSkipped, HtlcPenaltyTx] = { + val input = InputInfo(OutPoint(commitTx, htlcOutputIndex), commitTx.txOut(htlcOutputIndex)) + val amount = input.txOut.amount - weight2fee(feerate, redeemDetails.weight) + val tx = Transaction( + version = 2, + txIn = TxIn(input.outPoint, ByteVector.empty, 0xffffffffL) :: Nil, + txOut = TxOut(amount, localFinalScriptPubKey) :: Nil, + lockTime = 0 + ) + val unsignedTx = HtlcPenaltyTx(commitKeys, revocationKey, redeemDetails.redeemInfo, input, tx, redeemDetails.paymentHash, redeemDetails.htlcExpiry, commitmentFormat) + skipTxIfBelowDust(unsignedTx, localDustLimit) + } + } + + /** This transaction spends a remote [[HtlcTx]] from one of their revoked commitments. */ + case class ClaimHtlcDelayedOutputPenaltyTx(commitKeys: RemoteCommitmentKeys, revocationKey: PrivateKey, input: InputInfo, tx: Transaction, toRemoteDelay: CltvExpiryDelta, commitmentFormat: CommitmentFormat) extends RemoteCommitForceCloseTransaction { + override val desc: String = "htlc-delayed-penalty" + override val expectedWeight: Int = commitmentFormat.claimHtlcPenaltyWeight + + override def sign(): Transaction = { + val witness = commitmentFormat match { + case _: AnchorOutputsCommitmentFormat => + val redeemScript = Script.write(toLocalDelayed(commitKeys.publicKeys, toRemoteDelay)) + val sig = sign(revocationKey, sighash, RedeemInfo.P2wsh(redeemScript), extraUtxos = Map.empty) + Scripts.witnessToLocalDelayedWithRevocationSig(sig, redeemScript) + case _: SimpleTaprootChannelCommitmentFormat => + val redeemInfo = RedeemInfo.TaprootKeyPath(commitKeys.revocationPublicKey.xOnly, Some(Taproot.htlcDelayedScriptTree(commitKeys.publicKeys, toRemoteDelay))) + val sig = sign(revocationKey, sighash, redeemInfo, extraUtxos = Map.empty) + Script.witnessKeyPathPay2tr(sig) + } + tx.updateWitness(inputIndex, witness) + } + } + + object ClaimHtlcDelayedOutputPenaltyTx { + def createUnsignedTxs(commitKeys: RemoteCommitmentKeys, + revocationKey: PrivateKey, + htlcTx: Transaction, + localDustLimit: Satoshi, + toRemoteDelay: CltvExpiryDelta, + localFinalScriptPubKey: ByteVector, + feerate: FeeratePerKw, + commitmentFormat: CommitmentFormat): Seq[Either[TxGenerationSkipped, ClaimHtlcDelayedOutputPenaltyTx]] = { + val redeemInfo = commitmentFormat match { + case _: AnchorOutputsCommitmentFormat => + val redeemScript = Script.write(toLocalDelayed(commitKeys.publicKeys, toRemoteDelay)) + RedeemInfo.P2wsh(redeemScript) + case _: SimpleTaprootChannelCommitmentFormat => + RedeemInfo.TaprootKeyPath(commitKeys.revocationPublicKey.xOnly, Some(Taproot.htlcDelayedScriptTree(commitKeys.publicKeys, toRemoteDelay))) + } + // Note that we check *all* outputs of the tx, because it could spend a batch of HTLC outputs from the commit tx. + htlcTx.txOut.zipWithIndex.collect { + case (txOut, outputIndex) if txOut.publicKeyScript == redeemInfo.pubkeyScript => + val input = InputInfo(OutPoint(htlcTx, outputIndex), htlcTx.txOut(outputIndex)) + val amount = input.txOut.amount - weight2fee(feerate, commitmentFormat.claimHtlcPenaltyWeight) + val tx = Transaction( + version = 2, + txIn = TxIn(input.outPoint, ByteVector.empty, 0xffffffffL) :: Nil, + txOut = TxOut(amount, localFinalScriptPubKey) :: Nil, + lockTime = 0 + ) + val unsignedTx = ClaimHtlcDelayedOutputPenaltyTx(commitKeys, revocationKey, input, tx, toRemoteDelay, commitmentFormat) + skipTxIfBelowDust(unsignedTx, localDustLimit) + } + } + } + + // @formatter:off sealed trait TxGenerationSkipped case object OutputNotFound extends TxGenerationSkipped { override def toString = "output not found (probably trimmed)" } + private case object OutputAlreadyInWallet extends TxGenerationSkipped { override def toString = "output doesn't need to be claimed, it belongs to our bitcoin wallet (p2wpkh or p2tr)" } case object AmountBelowDustLimit extends TxGenerationSkipped { override def toString = "amount is below dust limit" } + private case class CannotUpdateFee(txInfo: ForceCloseTransaction) extends TxGenerationSkipped { override def toString = s"cannot update fee for ${txInfo.desc} transactions" } // @formatter:on - /** - * When *local* *current* [[CommitTx]] is published: - * - [[ClaimLocalDelayedOutputTx]] spends to-local output of [[CommitTx]] after a delay - * - When using anchor outputs, [[ClaimLocalAnchorOutputTx]] spends to-local anchor of [[CommitTx]] - * - [[HtlcSuccessTx]] spends htlc-received outputs of [[CommitTx]] for which we have the preimage - * - [[HtlcDelayedTx]] spends [[HtlcSuccessTx]] after a delay - * - [[HtlcTimeoutTx]] spends htlc-sent outputs of [[CommitTx]] after a timeout - * - [[HtlcDelayedTx]] spends [[HtlcTimeoutTx]] after a delay - * - * When *remote* *current* [[CommitTx]] is published: - * - When using the default commitment format, [[ClaimP2WPKHOutputTx]] spends to-local output of [[CommitTx]] - * - When using anchor outputs, [[ClaimRemoteDelayedOutputTx]] spends to-local output of [[CommitTx]] - * - When using anchor outputs, [[ClaimLocalAnchorOutputTx]] spends to-local anchor of [[CommitTx]] - * - [[ClaimHtlcSuccessTx]] spends htlc-received outputs of [[CommitTx]] for which we have the preimage - * - [[ClaimHtlcTimeoutTx]] spends htlc-sent outputs of [[CommitTx]] after a timeout - * - * When *remote* *revoked* [[CommitTx]] is published: - * - When using the default commitment format, [[ClaimP2WPKHOutputTx]] spends to-local output of [[CommitTx]] - * - When using anchor outputs, [[ClaimRemoteDelayedOutputTx]] spends to-local output of [[CommitTx]] - * - When using anchor outputs, [[ClaimLocalAnchorOutputTx]] spends to-local anchor of [[CommitTx]] - * - [[MainPenaltyTx]] spends remote main output using the per-commitment secret - * - [[HtlcSuccessTx]] spends htlc-sent outputs of [[CommitTx]] for which they have the preimage (published by remote) - * - [[ClaimHtlcDelayedOutputPenaltyTx]] spends [[HtlcSuccessTx]] using the revocation secret (published by local) - * - [[HtlcTimeoutTx]] spends htlc-received outputs of [[CommitTx]] after a timeout (published by remote) - * - [[ClaimHtlcDelayedOutputPenaltyTx]] spends [[HtlcTimeoutTx]] using the revocation secret (published by local) - * - [[HtlcPenaltyTx]] spends competes with [[HtlcSuccessTx]] and [[HtlcTimeoutTx]] for the same outputs (published by local) - */ - - /** - * these values are specific to us (not defined in the specification) and used to estimate fees - */ - val claimP2WPKHOutputWeight = 438 - val anchorInputWeight = 279 - // The smallest transaction that spends an anchor contains 2 inputs (the commit tx output and a wallet input to set the feerate) - // and 1 output (change). If we're using P2WPKH wallet inputs/outputs with 72 bytes signatures, this results in a weight of 717. - // We round it down to 700 to allow for some error margin (e.g. signatures smaller than 72 bytes). - val claimAnchorOutputMinWeight = 700 - val htlcDelayedWeight = 483 - val claimHtlcSuccessWeight = 571 - val claimHtlcTimeoutWeight = 545 - val mainPenaltyWeight = 484 - val htlcPenaltyWeight = 578 // based on spending an HTLC-Success output (would be 571 with HTLC-Timeout) - - def weight2feeMsat(feeratePerKw: FeeratePerKw, weight: Int): MilliSatoshi = MilliSatoshi(feeratePerKw.toLong * weight) + private def weight2feeMsat(feeratePerKw: FeeratePerKw, weight: Int): MilliSatoshi = MilliSatoshi(feeratePerKw.toLong * weight) def weight2fee(feeratePerKw: FeeratePerKw, weight: Int): Satoshi = weight2feeMsat(feeratePerKw, weight).truncateToSatoshi @@ -284,34 +1229,13 @@ object Transactions { */ def fee2rate(fee: Satoshi, weight: Int): FeeratePerKw = FeeratePerKw((fee * 1000L) / weight) - /** As defined in https://github.com/lightning/bolts/blob/master/03-transactions.md#dust-limits */ - def dustLimit(scriptPubKey: ByteVector): Satoshi = { - Try(Script.parse(scriptPubKey)) match { - case Success(OP_DUP :: OP_HASH160 :: OP_PUSHDATA(pubkeyHash, _) :: OP_EQUALVERIFY :: OP_CHECKSIG :: Nil) if pubkeyHash.size == 20 => 546.sat - case Success(OP_HASH160 :: OP_PUSHDATA(scriptHash, _) :: OP_EQUAL :: Nil) if scriptHash.size == 20 => 540.sat - case Success(OP_0 :: OP_PUSHDATA(pubkeyHash, _) :: Nil) if pubkeyHash.size == 20 => 294.sat - case Success(OP_0 :: OP_PUSHDATA(scriptHash, _) :: Nil) if scriptHash.size == 32 => 330.sat - case Success((OP_1 | OP_2 | OP_3 | OP_4 | OP_5 | OP_6 | OP_7 | OP_8 | OP_9 | OP_10 | OP_11 | OP_12 | OP_13 | OP_14 | OP_15 | OP_16) :: OP_PUSHDATA(program, _) :: Nil) if 2 <= program.length && program.length <= 40 => 354.sat - case Success(OP_RETURN :: _) => 0.sat // OP_RETURN is never dust - case _ => 546.sat - } - } - - /** When an output is using OP_RETURN, we usually want to make sure its amount is 0, otherwise bitcoind won't accept it. */ - def isOpReturn(scriptPubKey: ByteVector): Boolean = { - Try(Script.parse(scriptPubKey)) match { - case Success(OP_RETURN :: _) => true - case _ => false - } - } - /** Offered HTLCs below this amount will be trimmed. */ def offeredHtlcTrimThreshold(dustLimit: Satoshi, spec: CommitmentSpec, commitmentFormat: CommitmentFormat): Satoshi = dustLimit + weight2fee(spec.htlcTxFeerate(commitmentFormat), commitmentFormat.htlcTimeoutWeight) def offeredHtlcTrimThreshold(dustLimit: Satoshi, feerate: FeeratePerKw, commitmentFormat: CommitmentFormat): Satoshi = { commitmentFormat match { - case ZeroFeeHtlcTxAnchorOutputsCommitmentFormat => dustLimit + case ZeroFeeHtlcTxAnchorOutputsCommitmentFormat | ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat => dustLimit case _ => dustLimit + weight2fee(feerate, commitmentFormat.htlcTimeoutWeight) } } @@ -329,7 +1253,7 @@ object Transactions { def receivedHtlcTrimThreshold(dustLimit: Satoshi, feerate: FeeratePerKw, commitmentFormat: CommitmentFormat): Satoshi = { commitmentFormat match { - case ZeroFeeHtlcTxAnchorOutputsCommitmentFormat => dustLimit + case ZeroFeeHtlcTxAnchorOutputsCommitmentFormat | ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat => dustLimit case _ => dustLimit + weight2fee(feerate, commitmentFormat.htlcSuccessWeight) } } @@ -364,8 +1288,7 @@ object Transactions { // When using anchor outputs, the channel initiator pays for *both* anchors all the time, even if only one anchor is present. // This is not technically a fee (it doesn't go to miners) but it also has to be deduced from the channel initiator's main output. val anchorsCost = commitmentFormat match { - case DefaultCommitmentFormat => Satoshi(0) - case _: AnchorOutputsCommitmentFormat => AnchorOutputsCommitmentFormat.anchorAmount * 2 + case _: AnchorOutputsCommitmentFormat | _: SimpleTaprootChannelCommitmentFormat => AnchorOutputsCommitmentFormat.anchorAmount * 2 } txFee + anchorsCost } @@ -417,100 +1340,79 @@ object Transactions { def decodeTxNumber(sequence: Long, locktime: Long): Long = ((sequence & 0xffffffL) << 24) + (locktime & 0xffffffL) - def getHtlcTxInputSequence(commitmentFormat: CommitmentFormat): Long = commitmentFormat match { - case DefaultCommitmentFormat => 0 // htlc txs immediately spend the commit tx - case _: AnchorOutputsCommitmentFormat => 1 // htlc txs have a 1-block delay to allow CPFP carve-out on anchors - } - - /** - * Represent a link between a commitment spec item (to-local, to-remote, anchors, htlc) and the actual output in the commit tx - * - * @param output transaction output - * @param redeemScript redeem script that matches this output (most of them are p2wsh) - * @param commitmentOutput commitment spec item this output is built from - */ - case class CommitmentOutputLink[T <: CommitmentOutput](output: TxOut, redeemScript: Seq[ScriptElt], commitmentOutput: T) - - /** Type alias for a collection of commitment output links */ - type CommitmentOutputs = Seq[CommitmentOutputLink[CommitmentOutput]] - - object CommitmentOutputLink { - /** - * We sort HTLC outputs according to BIP69 + CLTV as tie-breaker for offered HTLC, we do this only for the outgoing - * HTLC because we must agree with the remote on the order of HTLC-Timeout transactions even for identical HTLC outputs. - * See https://github.com/lightningnetwork/lightning-rfc/issues/448#issuecomment-432074187. - */ - def sort(a: CommitmentOutputLink[CommitmentOutput], b: CommitmentOutputLink[CommitmentOutput]): Boolean = (a.commitmentOutput, b.commitmentOutput) match { - case (OutHtlc(OutgoingHtlc(htlcA)), OutHtlc(OutgoingHtlc(htlcB))) if htlcA.paymentHash == htlcB.paymentHash && htlcA.amountMsat.truncateToSatoshi == htlcB.amountMsat.truncateToSatoshi => - htlcA.cltvExpiry <= htlcB.cltvExpiry - case _ => LexicographicalOrdering.isLessThan(a.output, b.output) - } + private def getHtlcTxInputSequence(commitmentFormat: CommitmentFormat): Long = commitmentFormat match { + case _: AnchorOutputsCommitmentFormat | _: SimpleTaprootChannelCommitmentFormat => 1 // htlc txs have a 1-block delay to allow CPFP carve-out on anchors } - def makeCommitTxOutputs(localPaysCommitTxFees: Boolean, - localDustLimit: Satoshi, - localRevocationPubkey: PublicKey, - toLocalDelay: CltvExpiryDelta, - localDelayedPaymentPubkey: PublicKey, - remotePaymentPubkey: PublicKey, - localHtlcPubkey: PublicKey, - remoteHtlcPubkey: PublicKey, - localFundingPubkey: PublicKey, - remoteFundingPubkey: PublicKey, + def makeCommitTxOutputs(localFundingPublicKey: PublicKey, + remoteFundingPublicKey: PublicKey, + commitmentKeys: CommitmentPublicKeys, + payCommitTxFees: Boolean, + dustLimit: Satoshi, + toSelfDelay: CltvExpiryDelta, spec: CommitmentSpec, - commitmentFormat: CommitmentFormat): CommitmentOutputs = { - val outputs = collection.mutable.ArrayBuffer.empty[CommitmentOutputLink[CommitmentOutput]] - - trimOfferedHtlcs(localDustLimit, spec, commitmentFormat).foreach { htlc => - val redeemScript = htlcOffered(localHtlcPubkey, remoteHtlcPubkey, localRevocationPubkey, ripemd160(htlc.add.paymentHash.bytes), commitmentFormat) - outputs.append(CommitmentOutputLink(TxOut(htlc.add.amountMsat.truncateToSatoshi, pay2wsh(redeemScript)), redeemScript, OutHtlc(htlc))) + commitmentFormat: CommitmentFormat): Seq[CommitmentOutput] = { + val outputs = collection.mutable.ArrayBuffer.empty[CommitmentOutput] + + trimOfferedHtlcs(dustLimit, spec, commitmentFormat).foreach { htlc => + val fee = weight2fee(spec.htlcTxFeerate(commitmentFormat), commitmentFormat.htlcTimeoutWeight) + val amountAfterFees = htlc.add.amountMsat.truncateToSatoshi - fee + val redeemInfo = HtlcTimeoutTx.redeemInfo(commitmentKeys, htlc.add.paymentHash, commitmentFormat) + val htlcDelayedRedeemInfo = HtlcDelayedTx.redeemInfo(commitmentKeys, toSelfDelay, commitmentFormat) + outputs.append(OutHtlc(htlc, TxOut(htlc.add.amountMsat.truncateToSatoshi, redeemInfo.pubkeyScript), TxOut(amountAfterFees, htlcDelayedRedeemInfo.pubkeyScript))) } - trimReceivedHtlcs(localDustLimit, spec, commitmentFormat).foreach { htlc => - val redeemScript = htlcReceived(localHtlcPubkey, remoteHtlcPubkey, localRevocationPubkey, ripemd160(htlc.add.paymentHash.bytes), htlc.add.cltvExpiry, commitmentFormat) - outputs.append(CommitmentOutputLink(TxOut(htlc.add.amountMsat.truncateToSatoshi, pay2wsh(redeemScript)), redeemScript, InHtlc(htlc))) + trimReceivedHtlcs(dustLimit, spec, commitmentFormat).foreach { htlc => + val fee = weight2fee(spec.htlcTxFeerate(commitmentFormat), commitmentFormat.htlcSuccessWeight) + val amountAfterFees = htlc.add.amountMsat.truncateToSatoshi - fee + val redeemInfo = HtlcSuccessTx.redeemInfo(commitmentKeys, htlc.add.paymentHash, htlc.add.cltvExpiry, commitmentFormat) + val htlcDelayedRedeemInfo = HtlcDelayedTx.redeemInfo(commitmentKeys, toSelfDelay, commitmentFormat) + outputs.append(InHtlc(htlc, TxOut(htlc.add.amountMsat.truncateToSatoshi, redeemInfo.pubkeyScript), TxOut(amountAfterFees, htlcDelayedRedeemInfo.pubkeyScript))) } val hasHtlcs = outputs.nonEmpty - val (toLocalAmount: Satoshi, toRemoteAmount: Satoshi) = if (localPaysCommitTxFees) { - (spec.toLocal.truncateToSatoshi - commitTxTotalCost(localDustLimit, spec, commitmentFormat), spec.toRemote.truncateToSatoshi) + val (toLocalAmount: Satoshi, toRemoteAmount: Satoshi) = if (payCommitTxFees) { + (spec.toLocal.truncateToSatoshi - commitTxTotalCost(dustLimit, spec, commitmentFormat), spec.toRemote.truncateToSatoshi) } else { - (spec.toLocal.truncateToSatoshi, spec.toRemote.truncateToSatoshi - commitTxTotalCost(localDustLimit, spec, commitmentFormat)) + (spec.toLocal.truncateToSatoshi, spec.toRemote.truncateToSatoshi - commitTxTotalCost(dustLimit, spec, commitmentFormat)) } // NB: we don't care if values are < 0, they will be trimmed if they are < dust limit anyway - if (toLocalAmount >= localDustLimit) { - outputs.append(CommitmentOutputLink( - TxOut(toLocalAmount, pay2wsh(toLocalDelayed(localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey))), - toLocalDelayed(localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey), - ToLocal)) + if (toLocalAmount >= dustLimit) { + val redeemInfo = commitmentFormat match { + case _: AnchorOutputsCommitmentFormat => + RedeemInfo.P2wsh(toLocalDelayed(commitmentKeys, toSelfDelay)) + case _: SimpleTaprootChannelCommitmentFormat => + val toLocalTree = Taproot.toLocalScriptTree(commitmentKeys, toSelfDelay) + RedeemInfo.TaprootScriptPath(NUMS_POINT.xOnly, toLocalTree.scriptTree, toLocalTree.localDelayed.hash()) + } + outputs.append(ToLocal(TxOut(toLocalAmount, redeemInfo.pubkeyScript))) } - if (toRemoteAmount >= localDustLimit) { - commitmentFormat match { - case DefaultCommitmentFormat => outputs.append(CommitmentOutputLink( - TxOut(toRemoteAmount, pay2wpkh(remotePaymentPubkey)), - pay2pkh(remotePaymentPubkey), - ToRemote)) - case _: AnchorOutputsCommitmentFormat => outputs.append(CommitmentOutputLink( - TxOut(toRemoteAmount, pay2wsh(toRemoteDelayed(remotePaymentPubkey))), - toRemoteDelayed(remotePaymentPubkey), - ToRemote)) + if (toRemoteAmount >= dustLimit) { + val redeemInfo = commitmentFormat match { + case _: AnchorOutputsCommitmentFormat => + RedeemInfo.P2wsh(toRemoteDelayed(commitmentKeys)) + case _: SimpleTaprootChannelCommitmentFormat => + val scripTree = Taproot.toRemoteScriptTree(commitmentKeys) + RedeemInfo.TaprootScriptPath(NUMS_POINT.xOnly, scripTree, scripTree.hash()) } + outputs.append(ToRemote(TxOut(toRemoteAmount, redeemInfo.pubkeyScript))) } commitmentFormat match { - case _: AnchorOutputsCommitmentFormat => - if (toLocalAmount >= localDustLimit || hasHtlcs) { - outputs.append(CommitmentOutputLink(TxOut(AnchorOutputsCommitmentFormat.anchorAmount, pay2wsh(anchor(localFundingPubkey))), anchor(localFundingPubkey), ToLocalAnchor)) + case _: AnchorOutputsCommitmentFormat | _: SimpleTaprootChannelCommitmentFormat => + if (toLocalAmount >= dustLimit || hasHtlcs) { + val redeemInfo = ClaimLocalAnchorTx.redeemInfo(localFundingPublicKey, commitmentKeys, commitmentFormat) + outputs.append(ToLocalAnchor(TxOut(AnchorOutputsCommitmentFormat.anchorAmount, redeemInfo.pubkeyScript))) } - if (toRemoteAmount >= localDustLimit || hasHtlcs) { - outputs.append(CommitmentOutputLink(TxOut(AnchorOutputsCommitmentFormat.anchorAmount, pay2wsh(anchor(remoteFundingPubkey))), anchor(remoteFundingPubkey), ToRemoteAnchor)) + if (toRemoteAmount >= dustLimit || hasHtlcs) { + val redeemInfo = ClaimRemoteAnchorTx.redeemInfo(remoteFundingPublicKey, commitmentKeys, commitmentFormat) + outputs.append(ToRemoteAnchor(TxOut(AnchorOutputsCommitmentFormat.anchorAmount, redeemInfo.pubkeyScript))) } - case _ => } - outputs.sortWith(CommitmentOutputLink.sort).toSeq + outputs.sortWith(CommitmentOutput.isLessThan).toSeq } def makeCommitTx(commitTxInput: InputInfo, @@ -518,374 +1420,38 @@ object Transactions { localPaymentBasePoint: PublicKey, remotePaymentBasePoint: PublicKey, localIsChannelOpener: Boolean, - outputs: CommitmentOutputs): CommitTx = { + outputs: Seq[CommitmentOutput]): CommitTx = { val txNumber = obscuredCommitTxNumber(commitTxNumber, localIsChannelOpener, localPaymentBasePoint, remotePaymentBasePoint) val (sequence, lockTime) = encodeTxNumber(txNumber) - val tx = Transaction( version = 2, txIn = TxIn(commitTxInput.outPoint, ByteVector.empty, sequence = sequence) :: Nil, - txOut = outputs.map(_.output), - lockTime = lockTime) - + txOut = outputs.map(_.txOut), + lockTime = lockTime + ) CommitTx(commitTxInput, tx) } - def makeHtlcTimeoutTx(commitTx: Transaction, - output: CommitmentOutputLink[OutHtlc], - outputIndex: Int, - localDustLimit: Satoshi, - localRevocationPubkey: PublicKey, - toLocalDelay: CltvExpiryDelta, - localDelayedPaymentPubkey: PublicKey, - feeratePerKw: FeeratePerKw, - commitmentFormat: CommitmentFormat): Either[TxGenerationSkipped, HtlcTimeoutTx] = { - val fee = weight2fee(feeratePerKw, commitmentFormat.htlcTimeoutWeight) - val redeemScript = output.redeemScript - val htlc = output.commitmentOutput.outgoingHtlc.add - val amount = htlc.amountMsat.truncateToSatoshi - fee - if (amount < localDustLimit) { - Left(AmountBelowDustLimit) - } else { - val input = InputInfo(OutPoint(commitTx, outputIndex), commitTx.txOut(outputIndex), write(redeemScript)) - val tx = Transaction( - version = 2, - txIn = TxIn(input.outPoint, ByteVector.empty, getHtlcTxInputSequence(commitmentFormat)) :: Nil, - txOut = TxOut(amount, pay2wsh(toLocalDelayed(localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey))) :: Nil, - lockTime = htlc.cltvExpiry.toLong - ) - Right(HtlcTimeoutTx(input, tx, htlc.id, ConfirmationTarget.Absolute(BlockHeight(htlc.cltvExpiry.toLong)))) - } - } - - def makeHtlcSuccessTx(commitTx: Transaction, - output: CommitmentOutputLink[InHtlc], - outputIndex: Int, - localDustLimit: Satoshi, - localRevocationPubkey: PublicKey, - toLocalDelay: CltvExpiryDelta, - localDelayedPaymentPubkey: PublicKey, - feeratePerKw: FeeratePerKw, - commitmentFormat: CommitmentFormat): Either[TxGenerationSkipped, HtlcSuccessTx] = { - val fee = weight2fee(feeratePerKw, commitmentFormat.htlcSuccessWeight) - val redeemScript = output.redeemScript - val htlc = output.commitmentOutput.incomingHtlc.add - val amount = htlc.amountMsat.truncateToSatoshi - fee - if (amount < localDustLimit) { - Left(AmountBelowDustLimit) - } else { - val input = InputInfo(OutPoint(commitTx, outputIndex), commitTx.txOut(outputIndex), write(redeemScript)) - val tx = Transaction( - version = 2, - txIn = TxIn(input.outPoint, ByteVector.empty, getHtlcTxInputSequence(commitmentFormat)) :: Nil, - txOut = TxOut(amount, pay2wsh(toLocalDelayed(localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey))) :: Nil, - lockTime = 0 - ) - Right(HtlcSuccessTx(input, tx, htlc.paymentHash, htlc.id, ConfirmationTarget.Absolute(BlockHeight(htlc.cltvExpiry.toLong)))) - } - } - def makeHtlcTxs(commitTx: Transaction, - localDustLimit: Satoshi, - localRevocationPubkey: PublicKey, - toLocalDelay: CltvExpiryDelta, - localDelayedPaymentPubkey: PublicKey, - feeratePerKw: FeeratePerKw, - outputs: CommitmentOutputs, - commitmentFormat: CommitmentFormat): Seq[HtlcTx] = { - val htlcTimeoutTxs = outputs.zipWithIndex.collect { - case (CommitmentOutputLink(o, s, OutHtlc(ou)), outputIndex) => - val co = CommitmentOutputLink(o, s, OutHtlc(ou)) - makeHtlcTimeoutTx(commitTx, co, outputIndex, localDustLimit, localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey, feeratePerKw, commitmentFormat) - }.collect { case Right(htlcTimeoutTx) => htlcTimeoutTx } - val htlcSuccessTxs = outputs.zipWithIndex.collect { - case (CommitmentOutputLink(o, s, InHtlc(in)), outputIndex) => - val co = CommitmentOutputLink(o, s, InHtlc(in)) - makeHtlcSuccessTx(commitTx, co, outputIndex, localDustLimit, localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey, feeratePerKw, commitmentFormat) - }.collect { case Right(htlcSuccessTx) => htlcSuccessTx } - htlcTimeoutTxs ++ htlcSuccessTxs - } - - def makeClaimHtlcSuccessTx(commitTx: Transaction, - outputs: CommitmentOutputs, - localDustLimit: Satoshi, - localHtlcPubkey: PublicKey, - remoteHtlcPubkey: PublicKey, - remoteRevocationPubkey: PublicKey, - localFinalScriptPubKey: ByteVector, - htlc: UpdateAddHtlc, - feeratePerKw: FeeratePerKw, - commitmentFormat: CommitmentFormat): Either[TxGenerationSkipped, ClaimHtlcSuccessTx] = { - val redeemScript = htlcOffered(remoteHtlcPubkey, localHtlcPubkey, remoteRevocationPubkey, ripemd160(htlc.paymentHash.bytes), commitmentFormat) - outputs.zipWithIndex.collectFirst { - case (CommitmentOutputLink(_, _, OutHtlc(OutgoingHtlc(outgoingHtlc))), outIndex) if outgoingHtlc.id == htlc.id => outIndex - } match { - case Some(outputIndex) => - val input = InputInfo(OutPoint(commitTx, outputIndex), commitTx.txOut(outputIndex), write(redeemScript)) - // unsigned tx - val tx = Transaction( - version = 2, - txIn = TxIn(input.outPoint, ByteVector.empty, getHtlcTxInputSequence(commitmentFormat)) :: Nil, - txOut = TxOut(Satoshi(0), localFinalScriptPubKey) :: Nil, - lockTime = 0) - val weight = addSigs(ClaimHtlcSuccessTx(input, tx, htlc.paymentHash, htlc.id, ConfirmationTarget.Absolute(BlockHeight(htlc.cltvExpiry.toLong))), PlaceHolderSig, ByteVector32.Zeroes).tx.weight() - val fee = weight2fee(feeratePerKw, weight) - val amount = input.txOut.amount - fee - if (amount < localDustLimit) { - Left(AmountBelowDustLimit) - } else { - val tx1 = tx.copy(txOut = tx.txOut.head.copy(amount = amount) :: Nil) - Right(ClaimHtlcSuccessTx(input, tx1, htlc.paymentHash, htlc.id, ConfirmationTarget.Absolute(BlockHeight(htlc.cltvExpiry.toLong)))) - } - case None => Left(OutputNotFound) - } - } - - def makeClaimHtlcTimeoutTx(commitTx: Transaction, - outputs: CommitmentOutputs, - localDustLimit: Satoshi, - localHtlcPubkey: PublicKey, - remoteHtlcPubkey: PublicKey, - remoteRevocationPubkey: PublicKey, - localFinalScriptPubKey: ByteVector, - htlc: UpdateAddHtlc, - feeratePerKw: FeeratePerKw, - commitmentFormat: CommitmentFormat): Either[TxGenerationSkipped, ClaimHtlcTimeoutTx] = { - val redeemScript = htlcReceived(remoteHtlcPubkey, localHtlcPubkey, remoteRevocationPubkey, ripemd160(htlc.paymentHash.bytes), htlc.cltvExpiry, commitmentFormat) - outputs.zipWithIndex.collectFirst { - case (CommitmentOutputLink(_, _, InHtlc(IncomingHtlc(incomingHtlc))), outIndex) if incomingHtlc.id == htlc.id => outIndex - } match { - case Some(outputIndex) => - val input = InputInfo(OutPoint(commitTx, outputIndex), commitTx.txOut(outputIndex), write(redeemScript)) - // unsigned tx - val tx = Transaction( - version = 2, - txIn = TxIn(input.outPoint, ByteVector.empty, getHtlcTxInputSequence(commitmentFormat)) :: Nil, - txOut = TxOut(Satoshi(0), localFinalScriptPubKey) :: Nil, - lockTime = htlc.cltvExpiry.toLong) - val weight = addSigs(ClaimHtlcTimeoutTx(input, tx, htlc.id, ConfirmationTarget.Absolute(BlockHeight(htlc.cltvExpiry.toLong))), PlaceHolderSig).tx.weight() - val fee = weight2fee(feeratePerKw, weight) - val amount = input.txOut.amount - fee - if (amount < localDustLimit) { - Left(AmountBelowDustLimit) - } else { - val tx1 = tx.copy(txOut = tx.txOut.head.copy(amount = amount) :: Nil) - Right(ClaimHtlcTimeoutTx(input, tx1, htlc.id, ConfirmationTarget.Absolute(BlockHeight(htlc.cltvExpiry.toLong)))) - } - case None => Left(OutputNotFound) - } - } - - def makeClaimP2WPKHOutputTx(commitTx: Transaction, localDustLimit: Satoshi, localPaymentPubkey: PublicKey, localFinalScriptPubKey: ByteVector, feeratePerKw: FeeratePerKw): Either[TxGenerationSkipped, ClaimP2WPKHOutputTx] = { - val redeemScript = Script.pay2pkh(localPaymentPubkey) - val pubkeyScript = write(pay2wpkh(localPaymentPubkey)) - findPubKeyScriptIndex(commitTx, pubkeyScript) match { - case Left(skip) => Left(skip) - case Right(outputIndex) => - val input = InputInfo(OutPoint(commitTx, outputIndex), commitTx.txOut(outputIndex), write(redeemScript)) - // unsigned tx - val tx = Transaction( - version = 2, - txIn = TxIn(input.outPoint, ByteVector.empty, 0x00000000L) :: Nil, - txOut = TxOut(Satoshi(0), localFinalScriptPubKey) :: Nil, - lockTime = 0) - // compute weight with a dummy 73 bytes signature (the largest you can get) and a dummy 33 bytes pubkey - val weight = addSigs(ClaimP2WPKHOutputTx(input, tx), PlaceHolderPubKey, PlaceHolderSig).tx.weight() - val fee = weight2fee(feeratePerKw, weight) - val amount = input.txOut.amount - fee - if (amount < localDustLimit) { - Left(AmountBelowDustLimit) - } else { - val tx1 = tx.copy(txOut = tx.txOut.head.copy(amount = amount) :: Nil) - Right(ClaimP2WPKHOutputTx(input, tx1)) - } - } - } - - def makeClaimRemoteDelayedOutputTx(commitTx: Transaction, localDustLimit: Satoshi, localPaymentPubkey: PublicKey, localFinalScriptPubKey: ByteVector, feeratePerKw: FeeratePerKw): Either[TxGenerationSkipped, ClaimRemoteDelayedOutputTx] = { - val redeemScript = toRemoteDelayed(localPaymentPubkey) - val pubkeyScript = write(pay2wsh(redeemScript)) - findPubKeyScriptIndex(commitTx, pubkeyScript) match { - case Left(skip) => Left(skip) - case Right(outputIndex) => - val input = InputInfo(OutPoint(commitTx, outputIndex), commitTx.txOut(outputIndex), write(redeemScript)) - // unsigned transaction - val tx = Transaction( - version = 2, - txIn = TxIn(input.outPoint, ByteVector.empty, 1) :: Nil, - txOut = TxOut(Satoshi(0), localFinalScriptPubKey) :: Nil, - lockTime = 0) - // compute weight with a dummy 73 bytes signature (the largest you can get) - val weight = addSigs(ClaimRemoteDelayedOutputTx(input, tx), PlaceHolderSig).tx.weight() - val fee = weight2fee(feeratePerKw, weight) - val amount = input.txOut.amount - fee - if (amount < localDustLimit) { - Left(AmountBelowDustLimit) - } else { - val tx1 = tx.copy(txOut = tx.txOut.head.copy(amount = amount) :: Nil) - Right(ClaimRemoteDelayedOutputTx(input, tx1)) - } + outputs: Seq[CommitmentOutput], + commitmentFormat: CommitmentFormat): Seq[UnsignedHtlcTx] = { + outputs.zipWithIndex.collect { + case (o: OutHtlc, outputIndex) => HtlcTimeoutTx.createUnsignedTx(commitTx, o, outputIndex, commitmentFormat) + case (i: InHtlc, outputIndex) => HtlcSuccessTx.createUnsignedTx(commitTx, i, outputIndex, commitmentFormat) } } - def makeHtlcDelayedTx(htlcTx: Transaction, localDustLimit: Satoshi, localRevocationPubkey: PublicKey, toLocalDelay: CltvExpiryDelta, localDelayedPaymentPubkey: PublicKey, localFinalScriptPubKey: ByteVector, feeratePerKw: FeeratePerKw): Either[TxGenerationSkipped, HtlcDelayedTx] = { - makeLocalDelayedOutputTx(htlcTx, localDustLimit, localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey, localFinalScriptPubKey, feeratePerKw).map { - case (input, tx) => HtlcDelayedTx(input, tx) - } - } - - def makeClaimLocalDelayedOutputTx(commitTx: Transaction, localDustLimit: Satoshi, localRevocationPubkey: PublicKey, toLocalDelay: CltvExpiryDelta, localDelayedPaymentPubkey: PublicKey, localFinalScriptPubKey: ByteVector, feeratePerKw: FeeratePerKw): Either[TxGenerationSkipped, ClaimLocalDelayedOutputTx] = { - makeLocalDelayedOutputTx(commitTx, localDustLimit, localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey, localFinalScriptPubKey, feeratePerKw).map { - case (input, tx) => ClaimLocalDelayedOutputTx(input, tx) - } - } - - private def makeLocalDelayedOutputTx(parentTx: Transaction, localDustLimit: Satoshi, localRevocationPubkey: PublicKey, toLocalDelay: CltvExpiryDelta, localDelayedPaymentPubkey: PublicKey, localFinalScriptPubKey: ByteVector, feeratePerKw: FeeratePerKw): Either[TxGenerationSkipped, (InputInfo, Transaction)] = { - val redeemScript = toLocalDelayed(localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey) - val pubkeyScript = write(pay2wsh(redeemScript)) - findPubKeyScriptIndex(parentTx, pubkeyScript) match { - case Left(skip) => Left(skip) - case Right(outputIndex) => - val input = InputInfo(OutPoint(parentTx, outputIndex), parentTx.txOut(outputIndex), write(redeemScript)) - // unsigned transaction - val tx = Transaction( - version = 2, - txIn = TxIn(input.outPoint, ByteVector.empty, toLocalDelay.toInt) :: Nil, - txOut = TxOut(Satoshi(0), localFinalScriptPubKey) :: Nil, - lockTime = 0) - // compute weight with a dummy 73 bytes signature (the largest you can get) - val weight = addSigs(ClaimLocalDelayedOutputTx(input, tx), PlaceHolderSig).tx.weight() - val fee = weight2fee(feeratePerKw, weight) - val amount = input.txOut.amount - fee - if (amount < localDustLimit) { - Left(AmountBelowDustLimit) - } else { - val tx1 = tx.copy(txOut = tx.txOut.head.copy(amount = amount) :: Nil) - Right(input, tx1) - } - } - } - - private def makeClaimAnchorOutputTx(commitTx: Transaction, fundingPubkey: PublicKey): Either[TxGenerationSkipped, (InputInfo, Transaction)] = { - val redeemScript = anchor(fundingPubkey) - val pubkeyScript = write(pay2wsh(redeemScript)) - findPubKeyScriptIndex(commitTx, pubkeyScript) match { - case Left(skip) => Left(skip) - case Right(outputIndex) => - val input = InputInfo(OutPoint(commitTx, outputIndex), commitTx.txOut(outputIndex), write(redeemScript)) - // unsigned transaction - val tx = Transaction( - version = 2, - txIn = TxIn(input.outPoint, ByteVector.empty, 0) :: Nil, - txOut = Nil, // anchor is only used to bump fees, the output will be added later depending on available inputs - lockTime = 0) - Right((input, tx)) - } - } - - def makeClaimLocalAnchorOutputTx(commitTx: Transaction, localFundingPubkey: PublicKey, confirmationTarget: ConfirmationTarget): Either[TxGenerationSkipped, ClaimLocalAnchorOutputTx] = { - makeClaimAnchorOutputTx(commitTx, localFundingPubkey).map { case (input, tx) => ClaimLocalAnchorOutputTx(input, tx, confirmationTarget) } - } - - def makeClaimRemoteAnchorOutputTx(commitTx: Transaction, remoteFundingPubkey: PublicKey): Either[TxGenerationSkipped, ClaimRemoteAnchorOutputTx] = { - makeClaimAnchorOutputTx(commitTx, remoteFundingPubkey).map { case (input, tx) => ClaimRemoteAnchorOutputTx(input, tx) } - } - - def makeClaimHtlcDelayedOutputPenaltyTxs(htlcTx: Transaction, localDustLimit: Satoshi, localRevocationPubkey: PublicKey, toLocalDelay: CltvExpiryDelta, localDelayedPaymentPubkey: PublicKey, localFinalScriptPubKey: ByteVector, feeratePerKw: FeeratePerKw): Seq[Either[TxGenerationSkipped, ClaimHtlcDelayedOutputPenaltyTx]] = { - val redeemScript = toLocalDelayed(localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey) - val pubkeyScript = write(pay2wsh(redeemScript)) - findPubKeyScriptIndexes(htlcTx, pubkeyScript) match { - case Left(skip) => Seq(Left(skip)) - case Right(outputIndexes) => outputIndexes.map(outputIndex => { - val input = InputInfo(OutPoint(htlcTx, outputIndex), htlcTx.txOut(outputIndex), write(redeemScript)) - // unsigned transaction - val tx = Transaction( - version = 2, - txIn = TxIn(input.outPoint, ByteVector.empty, 0xffffffffL) :: Nil, - txOut = TxOut(Satoshi(0), localFinalScriptPubKey) :: Nil, - lockTime = 0) - // compute weight with a dummy 73 bytes signature (the largest you can get) - val weight = addSigs(ClaimHtlcDelayedOutputPenaltyTx(input, tx), PlaceHolderSig).tx.weight() - val fee = weight2fee(feeratePerKw, weight) - val amount = input.txOut.amount - fee - if (amount < localDustLimit) { - Left(AmountBelowDustLimit) - } else { - val tx1 = tx.copy(txOut = tx.txOut.head.copy(amount = amount) :: Nil) - Right(ClaimHtlcDelayedOutputPenaltyTx(input, tx1)) - } - }) - } - } - - def makeMainPenaltyTx(commitTx: Transaction, localDustLimit: Satoshi, remoteRevocationPubkey: PublicKey, localFinalScriptPubKey: ByteVector, toRemoteDelay: CltvExpiryDelta, remoteDelayedPaymentPubkey: PublicKey, feeratePerKw: FeeratePerKw): Either[TxGenerationSkipped, MainPenaltyTx] = { - val redeemScript = toLocalDelayed(remoteRevocationPubkey, toRemoteDelay, remoteDelayedPaymentPubkey) - val pubkeyScript = write(pay2wsh(redeemScript)) - findPubKeyScriptIndex(commitTx, pubkeyScript) match { - case Left(skip) => Left(skip) - case Right(outputIndex) => - val input = InputInfo(OutPoint(commitTx, outputIndex), commitTx.txOut(outputIndex), write(redeemScript)) - // unsigned transaction - val tx = Transaction( - version = 2, - txIn = TxIn(input.outPoint, ByteVector.empty, 0xffffffffL) :: Nil, - txOut = TxOut(Satoshi(0), localFinalScriptPubKey) :: Nil, - lockTime = 0) - // compute weight with a dummy 73 bytes signature (the largest you can get) - val weight = addSigs(MainPenaltyTx(input, tx), PlaceHolderSig).tx.weight() - val fee = weight2fee(feeratePerKw, weight) - val amount = input.txOut.amount - fee - if (amount < localDustLimit) { - Left(AmountBelowDustLimit) - } else { - val tx1 = tx.copy(txOut = tx.txOut.head.copy(amount = amount) :: Nil) - Right(MainPenaltyTx(input, tx1)) - } - } - } - - /** - * We already have the redeemScript, no need to build it - */ - def makeHtlcPenaltyTx(commitTx: Transaction, htlcOutputIndex: Int, redeemScript: ByteVector, localDustLimit: Satoshi, localFinalScriptPubKey: ByteVector, feeratePerKw: FeeratePerKw): Either[TxGenerationSkipped, HtlcPenaltyTx] = { - val input = InputInfo(OutPoint(commitTx, htlcOutputIndex), commitTx.txOut(htlcOutputIndex), redeemScript) - // unsigned transaction - val tx = Transaction( - version = 2, - txIn = TxIn(input.outPoint, ByteVector.empty, 0xffffffffL) :: Nil, - txOut = TxOut(Satoshi(0), localFinalScriptPubKey) :: Nil, - lockTime = 0) - // compute weight with a dummy 73 bytes signature (the largest you can get) - val weight = addSigs(MainPenaltyTx(input, tx), PlaceHolderSig).tx.weight() - val fee = weight2fee(feeratePerKw, weight) - val amount = input.txOut.amount - fee - if (amount < localDustLimit) { - Left(AmountBelowDustLimit) - } else { - val tx1 = tx.copy(txOut = tx.txOut.head.copy(amount = amount) :: Nil) - Right(HtlcPenaltyTx(input, tx1)) + def makeFundingScript(localFundingKey: PublicKey, remoteFundingKey: PublicKey, commitmentFormat: CommitmentFormat): RedeemInfo = { + commitmentFormat match { + case _: SegwitV0CommitmentFormat => RedeemInfo.P2wsh(Script.write(multiSig2of2(localFundingKey, remoteFundingKey))) + case _: SimpleTaprootChannelCommitmentFormat => RedeemInfo.TaprootKeyPath(Taproot.musig2Aggregate(localFundingKey, remoteFundingKey), None) } } - def makeClosingTx(commitTxInput: InputInfo, localScriptPubKey: ByteVector, remoteScriptPubKey: ByteVector, localPaysClosingFees: Boolean, dustLimit: Satoshi, closingFee: Satoshi, spec: CommitmentSpec): ClosingTx = { - require(spec.htlcs.isEmpty, "there shouldn't be any pending htlcs") - - val (toLocalAmount: Satoshi, toRemoteAmount: Satoshi) = if (localPaysClosingFees) { - (spec.toLocal.truncateToSatoshi - closingFee, spec.toRemote.truncateToSatoshi) - } else { - (spec.toLocal.truncateToSatoshi, spec.toRemote.truncateToSatoshi - closingFee) - } // NB: we don't care if values are < 0, they will be trimmed if they are < dust limit anyway - - val toLocalOutput_opt = if (toLocalAmount >= dustLimit) Some(TxOut(toLocalAmount, localScriptPubKey)) else None - val toRemoteOutput_opt = if (toRemoteAmount >= dustLimit) Some(TxOut(toRemoteAmount, remoteScriptPubKey)) else None - - val tx = LexicographicalOrdering.sort(Transaction( - version = 2, - txIn = TxIn(commitTxInput.outPoint, ByteVector.empty, sequence = 0xffffffffL) :: Nil, - txOut = toLocalOutput_opt.toSeq ++ toRemoteOutput_opt.toSeq ++ Nil, - lockTime = 0)) - val toLocalOutput = findPubKeyScriptIndex(tx, localScriptPubKey).map(index => OutputInfo(index, toLocalAmount, localScriptPubKey)).toOption - ClosingTx(commitTxInput, tx, toLocalOutput) + def makeFundingInputInfo(fundingTxId: TxId, fundingOutputIndex: Int, fundingAmount: Satoshi, localFundingKey: PublicKey, remoteFundingKey: PublicKey, commitmentFormat: CommitmentFormat): InputInfo = { + val redeemInfo = makeFundingScript(localFundingKey, remoteFundingKey, commitmentFormat) + val fundingTxOut = TxOut(fundingAmount, redeemInfo.pubkeyScript) + InputInfo(OutPoint(fundingTxId, fundingOutputIndex), fundingTxOut) } // @formatter:off @@ -897,6 +1463,21 @@ object Transactions { } // @formatter:on + /** + * When sending [[fr.acinq.eclair.wire.protocol.ClosingComplete]], we use a different nonce for each closing transaction we create. + * We generate nonces for all variants of the closing transaction for simplicity, even though we never use them all. + */ + case class CloserNonces(localAndRemote: LocalNonce, localOnly: LocalNonce, remoteOnly: LocalNonce) + + object CloserNonces { + /** Generate a set of random signing nonces for our closing transactions. */ + def generate(localFundingKey: PublicKey, remoteFundingKey: PublicKey, fundingTxId: TxId): CloserNonces = CloserNonces( + NonceGenerator.signingNonce(localFundingKey, remoteFundingKey, fundingTxId), + NonceGenerator.signingNonce(localFundingKey, remoteFundingKey, fundingTxId), + NonceGenerator.signingNonce(localFundingKey, remoteFundingKey, fundingTxId), + ) + } + /** Each closing attempt can result in multiple potential closing transactions, depending on which outputs are included. */ case class ClosingTxs(localAndRemote_opt: Option[ClosingTx], localOnly_opt: Option[ClosingTx], remoteOnly_opt: Option[ClosingTx]) { /** Preferred closing transaction for this closing attempt. */ @@ -918,7 +1499,7 @@ object Transactions { case SimpleClosingTxFee.PaidByThem(fee) => (spec.toLocal.truncateToSatoshi, spec.toRemote.truncateToSatoshi - fee) } - // An OP_RETURN script may be provided, but only when burning all of the peer's balance to fees. + // An OP_RETURN script may be provided, but only when burning all of the peer's balance to fees, otherwise bitcoind won't accept it. val toLocalOutput_opt = if (toLocalAmount >= dustLimit(localScriptPubKey)) { val amount = if (isOpReturn(localScriptPubKey)) 0.sat else toLocalAmount Some(TxOut(amount, localScriptPubKey)) @@ -936,17 +1517,16 @@ object Transactions { (toLocalOutput_opt, toRemoteOutput_opt) match { case (Some(toLocalOutput), Some(toRemoteOutput)) => val txLocalAndRemote = LexicographicalOrdering.sort(txNoOutput.copy(txOut = Seq(toLocalOutput, toRemoteOutput))) - val Right(toLocalOutputInfo) = findPubKeyScriptIndex(txLocalAndRemote, localScriptPubKey).map(index => OutputInfo(index, toLocalOutput.amount, localScriptPubKey)) ClosingTxs( - localAndRemote_opt = Some(ClosingTx(input, txLocalAndRemote, Some(toLocalOutputInfo))), + localAndRemote_opt = Some(ClosingTx(input, txLocalAndRemote, findPubKeyScriptIndex(txLocalAndRemote, localScriptPubKey).map(_.toLong).toOption)), // We also provide a version of the transaction without the remote output, which they may want to omit if not economical to spend. - localOnly_opt = Some(ClosingTx(input, txNoOutput.copy(txOut = Seq(toLocalOutput)), Some(OutputInfo(0, toLocalOutput.amount, localScriptPubKey)))), + localOnly_opt = Some(ClosingTx(input, txNoOutput.copy(txOut = Seq(toLocalOutput)), Some(0))), remoteOnly_opt = None ) case (Some(toLocalOutput), None) => ClosingTxs( localAndRemote_opt = None, - localOnly_opt = Some(ClosingTx(input, txNoOutput.copy(txOut = Seq(toLocalOutput)), Some(OutputInfo(0, toLocalOutput.amount, localScriptPubKey)))), + localOnly_opt = Some(ClosingTx(input, txNoOutput.copy(txOut = Seq(toLocalOutput)), Some(0))), remoteOnly_opt = None ) case (None, Some(toRemoteOutput)) => @@ -959,6 +1539,14 @@ object Transactions { } } + /** We skip creating transactions spending commitment outputs when the remaining amount is below dust. */ + private def skipTxIfBelowDust[T <: TransactionWithInputInfo](txInfo: T, dustLimit: Satoshi): Either[TxGenerationSkipped, T] = { + txInfo.tx.txOut.headOption match { + case Some(txOut) if txOut.amount < dustLimit => Left(AmountBelowDustLimit) + case _ => Right(txInfo) + } + } + def findPubKeyScriptIndex(tx: Transaction, pubkeyScript: ByteVector): Either[TxGenerationSkipped, Int] = { val outputIndex = tx.txOut.indexWhere(_.publicKeyScript == pubkeyScript) if (outputIndex >= 0) { @@ -968,123 +1556,33 @@ object Transactions { } } - def findPubKeyScriptIndexes(tx: Transaction, pubkeyScript: ByteVector): Either[TxGenerationSkipped, Seq[Int]] = { - val outputIndexes = tx.txOut.zipWithIndex.collect { - case (txOut, index) if txOut.publicKeyScript == pubkeyScript => index - } - if (outputIndexes.nonEmpty) { - Right(outputIndexes) + /** Update the on-chain fee paid by this transaction by lowering its output amount, if possible. */ + def updateFee(txInfo: ForceCloseTransaction, fee: Satoshi, dustLimit: Satoshi): Either[TxGenerationSkipped, ForceCloseTransaction] = { + if (txInfo.amountIn < fee + dustLimit) { + Left(AmountBelowDustLimit) } else { - Left(OutputNotFound) + val updatedTx = txInfo.tx.copy(txOut = txInfo.tx.txOut.headOption.map(_.copy(amount = txInfo.amountIn - fee)).toSeq) + txInfo match { + case txInfo: ClaimLocalDelayedOutputTx => Right(txInfo.copy(tx = updatedTx)) + case txInfo: ClaimRemoteDelayedOutputTx => Right(txInfo.copy(tx = updatedTx)) + // Anchor transaction don't have any output: wallet inputs must be used to pay fees. + case txInfo: ClaimAnchorTx => Left(CannotUpdateFee(txInfo)) + // HTLC transactions are pre-signed, we can't update their fee by lowering the output amount. + case txInfo: SignedHtlcTx => Left(CannotUpdateFee(txInfo)) + case txInfo: ClaimHtlcSuccessTx => Right(txInfo.copy(tx = updatedTx)) + case txInfo: ClaimHtlcTimeoutTx => Right(txInfo.copy(tx = updatedTx)) + case txInfo: HtlcDelayedTx => Right(txInfo.copy(tx = updatedTx)) + case txInfo: MainPenaltyTx => Right(txInfo.copy(tx = updatedTx)) + case txInfo: HtlcPenaltyTx => Right(txInfo.copy(tx = updatedTx)) + case txInfo: ClaimHtlcDelayedOutputPenaltyTx => Right(txInfo.copy(tx = updatedTx)) + } } } - /** - * Default public key used for fee estimation - */ - val PlaceHolderPubKey = PrivateKey(ByteVector32.One).publicKey - /** * This default sig takes 72B when encoded in DER (incl. 1B for the trailing sig hash), it is used for fee estimation * It is 72 bytes because our signatures are normalized (low-s) and will take up 72 bytes at most in DER format */ - val PlaceHolderSig = ByteVector64(ByteVector.fill(64)(0xaa)) + val PlaceHolderSig: ByteVector64 = ByteVector64(ByteVector.fill(64)(0xaa)) assert(der(PlaceHolderSig).size == 72) - - def addSigs(commitTx: CommitTx, localFundingPubkey: PublicKey, remoteFundingPubkey: PublicKey, localSig: ByteVector64, remoteSig: ByteVector64): CommitTx = { - val witness = Scripts.witness2of2(localSig, remoteSig, localFundingPubkey, remoteFundingPubkey) - commitTx.copy(tx = commitTx.tx.updateWitness(0, witness)) - } - - def addSigs(mainPenaltyTx: MainPenaltyTx, revocationSig: ByteVector64): MainPenaltyTx = mainPenaltyTx.input match { - case InputInfo.SegwitInput(_, _, redeemScript) => - val witness = Scripts.witnessToLocalDelayedWithRevocationSig(revocationSig, redeemScript) - mainPenaltyTx.copy(tx = mainPenaltyTx.tx.updateWitness(0, witness)) - case _: InputInfo.TaprootInput => mainPenaltyTx - } - - def addSigs(htlcPenaltyTx: HtlcPenaltyTx, revocationSig: ByteVector64, revocationPubkey: PublicKey): HtlcPenaltyTx = htlcPenaltyTx.input match { - case InputInfo.SegwitInput(_, _, redeemScript) => - val witness = Scripts.witnessHtlcWithRevocationSig(revocationSig, revocationPubkey, redeemScript) - htlcPenaltyTx.copy(tx = htlcPenaltyTx.tx.updateWitness(0, witness)) - case _: InputInfo.TaprootInput => htlcPenaltyTx - } - - def addSigs(htlcSuccessTx: HtlcSuccessTx, localSig: ByteVector64, remoteSig: ByteVector64, paymentPreimage: ByteVector32, commitmentFormat: CommitmentFormat): HtlcSuccessTx = htlcSuccessTx.input match { - case InputInfo.SegwitInput(_, _, redeemScript) => - val witness = witnessHtlcSuccess(localSig, remoteSig, paymentPreimage, redeemScript, commitmentFormat) - htlcSuccessTx.copy(tx = htlcSuccessTx.tx.updateWitness(0, witness)) - case _: InputInfo.TaprootInput => htlcSuccessTx - } - - def addSigs(htlcTimeoutTx: HtlcTimeoutTx, localSig: ByteVector64, remoteSig: ByteVector64, commitmentFormat: CommitmentFormat): HtlcTimeoutTx = htlcTimeoutTx.input match { - case InputInfo.SegwitInput(_, _, redeemScript) => - val witness = witnessHtlcTimeout(localSig, remoteSig, redeemScript, commitmentFormat) - htlcTimeoutTx.copy(tx = htlcTimeoutTx.tx.updateWitness(0, witness)) - case _: InputInfo.TaprootInput => htlcTimeoutTx - } - - def addSigs(claimHtlcSuccessTx: ClaimHtlcSuccessTx, localSig: ByteVector64, paymentPreimage: ByteVector32): ClaimHtlcSuccessTx = claimHtlcSuccessTx.input match { - case InputInfo.SegwitInput(_, _, redeemScript) => - val witness = witnessClaimHtlcSuccessFromCommitTx(localSig, paymentPreimage, redeemScript) - claimHtlcSuccessTx.copy(tx = claimHtlcSuccessTx.tx.updateWitness(0, witness)) - case _: InputInfo.TaprootInput => claimHtlcSuccessTx - } - - def addSigs(claimHtlcTimeoutTx: ClaimHtlcTimeoutTx, localSig: ByteVector64): ClaimHtlcTimeoutTx = claimHtlcTimeoutTx.input match { - case InputInfo.SegwitInput(_, _, redeemScript) => - val witness = witnessClaimHtlcTimeoutFromCommitTx(localSig, redeemScript) - claimHtlcTimeoutTx.copy(tx = claimHtlcTimeoutTx.tx.updateWitness(0, witness)) - case _: InputInfo.TaprootInput => claimHtlcTimeoutTx - } - - def addSigs(claimP2WPKHOutputTx: ClaimP2WPKHOutputTx, localPaymentPubkey: PublicKey, localSig: ByteVector64): ClaimP2WPKHOutputTx = { - val witness = ScriptWitness(Seq(der(localSig), localPaymentPubkey.value)) - claimP2WPKHOutputTx.copy(tx = claimP2WPKHOutputTx.tx.updateWitness(0, witness)) - } - - def addSigs(claimRemoteDelayedOutputTx: ClaimRemoteDelayedOutputTx, localSig: ByteVector64): ClaimRemoteDelayedOutputTx = claimRemoteDelayedOutputTx.input match { - case InputInfo.SegwitInput(_, _, redeemScript) => - val witness = witnessClaimToRemoteDelayedFromCommitTx(localSig, redeemScript) - claimRemoteDelayedOutputTx.copy(tx = claimRemoteDelayedOutputTx.tx.updateWitness(0, witness)) - case _: InputInfo.TaprootInput => claimRemoteDelayedOutputTx - } - - def addSigs(claimDelayedOutputTx: ClaimLocalDelayedOutputTx, localSig: ByteVector64): ClaimLocalDelayedOutputTx = claimDelayedOutputTx.input match { - case InputInfo.SegwitInput(_, _, redeemScript) => - val witness = witnessToLocalDelayedAfterDelay(localSig, redeemScript) - claimDelayedOutputTx.copy(tx = claimDelayedOutputTx.tx.updateWitness(0, witness)) - case _: InputInfo.TaprootInput => claimDelayedOutputTx - } - - def addSigs(htlcDelayedTx: HtlcDelayedTx, localSig: ByteVector64): HtlcDelayedTx = htlcDelayedTx.input match { - case InputInfo.SegwitInput(_, _, redeemScript) => - val witness = witnessToLocalDelayedAfterDelay(localSig, redeemScript) - htlcDelayedTx.copy(tx = htlcDelayedTx.tx.updateWitness(0, witness)) - case _: InputInfo.TaprootInput => htlcDelayedTx - } - - def addSigs(claimAnchorOutputTx: ClaimLocalAnchorOutputTx, localSig: ByteVector64): ClaimLocalAnchorOutputTx = claimAnchorOutputTx.input match { - case InputInfo.SegwitInput(_, _, redeemScript) => - val witness = witnessAnchor(localSig, redeemScript) - claimAnchorOutputTx.copy(tx = claimAnchorOutputTx.tx.updateWitness(0, witness)) - case _: InputInfo.TaprootInput => claimAnchorOutputTx - } - - def addSigs(claimHtlcDelayedPenalty: ClaimHtlcDelayedOutputPenaltyTx, revocationSig: ByteVector64): ClaimHtlcDelayedOutputPenaltyTx = claimHtlcDelayedPenalty.input match { - case InputInfo.SegwitInput(_, _, redeemScript) => - val witness = Scripts.witnessToLocalDelayedWithRevocationSig(revocationSig, redeemScript) - claimHtlcDelayedPenalty.copy(tx = claimHtlcDelayedPenalty.tx.updateWitness(0, witness)) - case _: InputInfo.TaprootInput => claimHtlcDelayedPenalty - } - - def addSigs(closingTx: ClosingTx, localFundingPubkey: PublicKey, remoteFundingPubkey: PublicKey, localSig: ByteVector64, remoteSig: ByteVector64): ClosingTx = { - val witness = Scripts.witness2of2(localSig, remoteSig, localFundingPubkey, remoteFundingPubkey) - closingTx.copy(tx = closingTx.tx.updateWitness(0, witness)) - } - - def checkSpendable(txinfo: TransactionWithInputInfo): Try[Unit] = { - // NB: we don't verify the other inputs as they should only be wallet inputs used to RBF the transaction - Try(Transaction.correctlySpends(txinfo.tx, Map(txinfo.input.outPoint -> txinfo.input.txOut), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) - } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/CommandCodecs.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/CommandCodecs.scala index 8f9d8d5cfa..0d056ac68d 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/CommandCodecs.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/CommandCodecs.scala @@ -17,12 +17,15 @@ package fr.acinq.eclair.wire.internal import akka.actor.ActorRef +import fr.acinq.eclair.TimestampMilli import fr.acinq.eclair.channel._ +import fr.acinq.eclair.crypto.Sphinx import fr.acinq.eclair.wire.protocol.CommonCodecs._ import fr.acinq.eclair.wire.protocol.FailureMessageCodecs._ import fr.acinq.eclair.wire.protocol._ import scodec.Codec import scodec.codecs._ +import shapeless.{::, HNil} import scala.concurrent.duration.FiniteDuration @@ -34,21 +37,47 @@ object CommandCodecs { (("id" | int64) :: ("reason" | either(bool, varsizebinarydata, provide(TemporaryNodeFailure()).upcast[FailureMessage]).xmap[FailureReason]( { - case Left(packet) => FailureReason.EncryptedDownstreamFailure(packet) + case Left(packet) => FailureReason.EncryptedDownstreamFailure(packet, None) case Right(f) => FailureReason.LocalFailure(f) }, { - case FailureReason.EncryptedDownstreamFailure(packet) => Left(packet) + case FailureReason.EncryptedDownstreamFailure(packet, _) => Left(packet) case FailureReason.LocalFailure(f) => Right(f) } )) :: + ("attribution_opt" | provide(Option.empty[FailureAttributionData])) :: ("delay_opt" | provide(Option.empty[FiniteDuration])) :: ("commit" | provide(false)) :: ("replyTo_opt" | provide(Option.empty[ActorRef]))).as[CMD_FAIL_HTLC] - private val cmdFulfillCodec: Codec[CMD_FULFILL_HTLC] = + private val cmdFulfillWithPartialAttributionCodec: Codec[CMD_FULFILL_HTLC] = (("id" | int64) :: ("r" | bytes32) :: + ("downstreamAttribution_opt" | optional(bool8, bytes(Sphinx.Attribution.totalLength))) :: + ("htlcReceivedAt_opt" | optional(bool8, uint64overflow.as[TimestampMilli])) :: + ("commit" | provide(false)) :: + ("replyTo_opt" | provide(Option.empty[ActorRef]))).map { + case id :: r :: downstreamAttribution_opt :: htlcReceivedAt_opt :: commit :: replyTo_opt :: HNil => + val attribution_opt = htlcReceivedAt_opt.map(receivedAt => FulfillAttributionData(receivedAt, None, downstreamAttribution_opt)) + CMD_FULFILL_HTLC(id, r, attribution_opt, commit, replyTo_opt) + }.decodeOnly + + private val cmdFulfillWithoutAttributionCodec: Codec[CMD_FULFILL_HTLC] = + (("id" | int64) :: + ("r" | bytes32) :: + ("attribution_opt" | provide(Option.empty[FulfillAttributionData])) :: + ("commit" | provide(false)) :: + ("replyTo_opt" | provide(Option.empty[ActorRef]))).as[CMD_FULFILL_HTLC] + + private val fulfillAttributionCodec: Codec[FulfillAttributionData] = + (("htlcReceivedAt" | uint64overflow.as[TimestampMilli]) :: + ("trampolineReceivedAt_opt" | optional(bool8, uint64overflow.as[TimestampMilli])) :: + ("downstreamAttribution_opt" | optional(bool8, bytes(Sphinx.Attribution.totalLength)))).as[FulfillAttributionData] + + private val cmdFullfillCodec: Codec[CMD_FULFILL_HTLC] = + (("id" | int64) :: + ("r" | bytes32) :: + ("attribution_opt" | optional(bool8, fulfillAttributionCodec)) :: ("commit" | provide(false)) :: ("replyTo_opt" | provide(Option.empty[ActorRef]))).as[CMD_FULFILL_HTLC] @@ -57,22 +86,50 @@ object CommandCodecs { (("id" | int64) :: ("reason" | either(bool8, varsizebinarydata, variableSizeBytes(uint16, failureMessageCodec)).xmap[FailureReason]( { - case Left(packet) => FailureReason.EncryptedDownstreamFailure(packet) + case Left(packet) => FailureReason.EncryptedDownstreamFailure(packet, None) case Right(f) => FailureReason.LocalFailure(f) }, { - case FailureReason.EncryptedDownstreamFailure(packet) => Left(packet) + case FailureReason.EncryptedDownstreamFailure(packet, _) => Left(packet) case FailureReason.LocalFailure(f) => Right(f) } )) :: + ("attribution_opt" | provide(Option.empty[FailureAttributionData])) :: + // No need to delay commands after a restart, we've been offline which already created a random delay. + ("delay_opt" | provide(Option.empty[FiniteDuration])) :: + ("commit" | provide(false)) :: + ("replyTo_opt" | provide(Option.empty[ActorRef]))).as[CMD_FAIL_HTLC] + + private val cmdFailWithoutAttributionCodec: Codec[CMD_FAIL_HTLC] = + (("id" | int64) :: + ("reason" | failureReasonCodec) :: + ("attribution_opt" | provide(Option.empty[FailureAttributionData])) :: // No need to delay commands after a restart, we've been offline which already created a random delay. ("delay_opt" | provide(Option.empty[FiniteDuration])) :: ("commit" | provide(false)) :: ("replyTo_opt" | provide(Option.empty[ActorRef]))).as[CMD_FAIL_HTLC] + private val cmdFailWithPartialAttributionCodec: Codec[CMD_FAIL_HTLC] = + (("id" | int64) :: + ("reason" | failureReasonCodec) :: + ("htlcReceivedAt_opt" | optional(bool8, uint64overflow.as[TimestampMilli])) :: + // No need to delay commands after a restart, we've been offline which already created a random delay. + ("delay_opt" | provide(Option.empty[FiniteDuration])) :: + ("commit" | provide(false)) :: + ("replyTo_opt" | provide(Option.empty[ActorRef]))).map { + case id :: reason :: htlcReceivedAt_opt :: delay_opt :: commit :: replyTo_opt :: HNil => + val attribution_opt = htlcReceivedAt_opt.map(receivedAt => FailureAttributionData(receivedAt, None)) + CMD_FAIL_HTLC(id, reason, attribution_opt, delay_opt, commit, replyTo_opt) + }.decodeOnly + + private val failureAttributionCodec: Codec[FailureAttributionData] = + (("htlcReceivedAt" | uint64overflow.as[TimestampMilli]) :: + ("trampolineReceivedAt_opt" | optional(bool8, uint64overflow.as[TimestampMilli]))).as[FailureAttributionData] + private val cmdFailCodec: Codec[CMD_FAIL_HTLC] = (("id" | int64) :: ("reason" | failureReasonCodec) :: + ("attribution_opt" | optional(bool8, failureAttributionCodec)) :: // No need to delay commands after a restart, we've been offline which already created a random delay. ("delay_opt" | provide(Option.empty[FiniteDuration])) :: ("commit" | provide(false)) :: @@ -87,10 +144,14 @@ object CommandCodecs { val cmdCodec: Codec[HtlcSettlementCommand] = discriminated[HtlcSettlementCommand].by(uint16) // NB: order matters! - .typecase(4, cmdFailCodec) + .typecase(8, cmdFullfillCodec) + .typecase(7, cmdFailCodec) + .typecase(6, cmdFulfillWithPartialAttributionCodec) + .typecase(5, cmdFailWithPartialAttributionCodec) + .typecase(4, cmdFailWithoutAttributionCodec) .typecase(3, cmdFailEitherCodec) .typecase(2, cmdFailMalformedCodec) .typecase(1, cmdFailWithoutLengthCodec) - .typecase(0, cmdFulfillCodec) + .typecase(0, cmdFulfillWithoutAttributionCodec) } \ No newline at end of file diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/ChannelCodecs.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/ChannelCodecs.scala index 779490fdab..91c132bfa7 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/ChannelCodecs.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/ChannelCodecs.scala @@ -17,16 +17,11 @@ package fr.acinq.eclair.wire.internal.channel import fr.acinq.eclair.channel.PersistentChannelData -import fr.acinq.eclair.wire.internal.channel.version0.ChannelCodecs0 -import fr.acinq.eclair.wire.internal.channel.version1.ChannelCodecs1 -import fr.acinq.eclair.wire.internal.channel.version2.ChannelCodecs2 -import fr.acinq.eclair.wire.internal.channel.version3.ChannelCodecs3 -import fr.acinq.eclair.wire.internal.channel.version4.ChannelCodecs4 +import fr.acinq.eclair.wire.internal.channel.version5.ChannelCodecs5 import grizzled.slf4j.Logging -import scodec.Codec -import scodec.codecs.{byte, discriminated} +import scodec.codecs.{byte, discriminated, fail} +import scodec.{Codec, Err} -// @formatter:off /** * Codecs used to store the internal channel data. * @@ -36,28 +31,33 @@ import scodec.codecs.{byte, discriminated} * 1) [[ChannelCodecs]] is the only publicly accessible class. It handles compatibility between different versions * of the codecs. * - * 2) Each codec version must be in its separate package, and have the following structure: + * 2) Each codec version must be in its separate package (version0, version1, etc), and have the following structure: * {{{ - * private[channel] object ChannelCodecs0 { - - private[version0] object Codecs { - - // internal codecs - - } - - val channelDataCodec: Codec[PersistentChannelData] = ... + * private[channel] object ChannelCodecsN { + * + * private[versionN] object Codecs { + * + * // internal codecs + * + * } + * + * val channelDataCodec: Codec[PersistentChannelData] = ... * }}} * - * Notice that the outer class has a visibility restricted to package [[fr.acinq.eclair.wire.internal.channel]], while the inner class has a - * visibility restricted to package [[version0]]. This guarantees that we strictly segregate each codec version, - * while still allowing unitary testing. + * Notice that the outer class has a visibility restricted to package [[fr.acinq.eclair.wire.internal.channel]], while + * the inner class has a visibility restricted to package [[versionN]]. This guarantees that we strictly segregate each + * codec version, while still allowing unitary testing. * * Created by PM on 02/06/2017. */ -// @formatter:on object ChannelCodecs extends Logging { + /** + * Codecs v0 to v4 have been removed after the eclair v0.13.0 release. + * Users on older version will need to first run the v0.13.0 release before updating to a newer version. + */ + private val pre013FailingCodec: Codec[PersistentChannelData] = fail(Err("You are updating from a version of eclair older than v0.13.0: please update to the v0.13.0 release first to migrate your channel data, and afterwards you'll be able to update to the latest version.")) + /** * Order matters!! * @@ -67,10 +67,11 @@ object ChannelCodecs extends Logging { * More info here: https://github.com/scodec/scodec/issues/122 */ val channelDataCodec: Codec[PersistentChannelData] = discriminated[PersistentChannelData].by(byte) - .typecase(4, ChannelCodecs4.channelDataCodec) - .typecase(3, ChannelCodecs3.channelDataCodec.decodeOnly) - .typecase(2, ChannelCodecs2.channelDataCodec.decodeOnly) - .typecase(1, ChannelCodecs1.channelDataCodec.decodeOnly) - .typecase(0, ChannelCodecs0.channelDataCodec.decodeOnly) + .typecase(5, ChannelCodecs5.channelDataCodec) + .typecase(4, pre013FailingCodec) + .typecase(3, pre013FailingCodec) + .typecase(2, pre013FailingCodec) + .typecase(1, pre013FailingCodec) + .typecase(0, pre013FailingCodec) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelCodecs0.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelCodecs0.scala deleted file mode 100644 index 281e3d7d8b..0000000000 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelCodecs0.scala +++ /dev/null @@ -1,499 +0,0 @@ -/* - * Copyright 2019 ACINQ SAS - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package fr.acinq.eclair.wire.internal.channel.version0 - -import fr.acinq.bitcoin.scalacompat.DeterministicWallet.KeyPath -import fr.acinq.bitcoin.scalacompat.{ByteVector64, Crypto, OutPoint, Transaction, TxId, TxOut} -import fr.acinq.eclair.blockchain.fee.ConfirmationTarget -import fr.acinq.eclair.channel.LocalFundingStatus.SingleFundedUnconfirmedFundingTx -import fr.acinq.eclair.channel._ -import fr.acinq.eclair.crypto.ShaChain -import fr.acinq.eclair.transactions.Transactions._ -import fr.acinq.eclair.transactions._ -import fr.acinq.eclair.wire.internal.channel.version0.ChannelTypes0.{HtlcTxAndSigs, PublishableTxs} -import fr.acinq.eclair.wire.protocol.CommonCodecs._ -import fr.acinq.eclair.wire.protocol.LightningMessageCodecs.{channelAnnouncementCodec, channelUpdateCodec, combinedFeaturesCodec} -import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{Alias, BlockHeight, MilliSatoshiLong, TimestampSecond} -import scodec.Codec -import scodec.bits.{BitVector, ByteVector} -import scodec.codecs._ -import shapeless.{::, HNil} - -import java.util.UUID - -/** - * Those codecs are here solely for backward compatibility reasons. - * - * Created by PM on 02/06/2017. - */ -private[channel] object ChannelCodecs0 { - - private[version0] object Codecs { - - val keyPathCodec: Codec[KeyPath] = ("path" | listOfN(uint16, uint32)).xmap[KeyPath](l => KeyPath(l), keyPath => keyPath.path.toList).as[KeyPath].decodeOnly - - val channelVersionCodec: Codec[ChannelTypes0.ChannelVersion] = discriminatorWithDefault[ChannelTypes0.ChannelVersion]( - discriminator = discriminated[ChannelTypes0.ChannelVersion].by(byte) - .typecase(0x01, bits(ChannelTypes0.ChannelVersion.LENGTH_BITS).as[ChannelTypes0.ChannelVersion]) - // NB: 0x02 and 0x03 are *reserved* for backward compatibility reasons - , - fallback = provide(ChannelTypes0.ChannelVersion.ZEROES) // README: DO NOT CHANGE THIS !! old channels don't have a channel version - // field and don't support additional features which is why all bits are set to 0. - ) - - def localParamsCodec(channelVersion: ChannelTypes0.ChannelVersion): Codec[LocalParams] = ( - ("nodeId" | publicKey) :: - ("channelPath" | keyPathCodec) :: - ("dustLimit" | satoshi) :: - ("maxHtlcValueInFlightMsat" | millisatoshi) :: - ("channelReserve" | conditional(included = true, satoshi)) :: - ("htlcMinimum" | millisatoshi) :: - ("toSelfDelay" | cltvExpiryDelta) :: - ("maxAcceptedHtlcs" | uint16) :: - ("isInitiator" | bool) :: - ("upfrontShutdownScript_opt" | varsizebinarydata.map(Option(_)).decodeOnly) :: - ("walletStaticPaymentBasepoint" | optional(provide(channelVersion.paysDirectlyToWallet), publicKey)) :: - ("features" | combinedFeaturesCodec)).map { - case nodeId :: channelPath :: dustLimit :: maxHtlcValueInFlightMsat :: channelReserve :: htlcMinimum :: toSelfDelay :: maxAcceptedHtlcs :: isInitiator :: upfrontShutdownScript_opt :: walletStaticPaymentBasepoint :: features :: HNil => - LocalParams(nodeId, channelPath, dustLimit, maxHtlcValueInFlightMsat, channelReserve, htlcMinimum, toSelfDelay, maxAcceptedHtlcs, isInitiator, isInitiator, upfrontShutdownScript_opt, walletStaticPaymentBasepoint, features) - }.decodeOnly - - val remoteParamsCodec: Codec[ChannelTypes0.RemoteParams] = ( - ("nodeId" | publicKey) :: - ("dustLimit" | satoshi) :: - ("maxHtlcValueInFlightMsat" | uint64) :: - ("channelReserve" | conditional(included = true, satoshi)) :: - ("htlcMinimum" | millisatoshi) :: - ("toSelfDelay" | cltvExpiryDelta) :: - ("maxAcceptedHtlcs" | uint16) :: - ("fundingPubKey" | publicKey) :: - ("revocationBasepoint" | publicKey) :: - ("paymentBasepoint" | publicKey) :: - ("delayedPaymentBasepoint" | publicKey) :: - ("htlcBasepoint" | publicKey) :: - ("features" | combinedFeaturesCodec) :: - ("shutdownScript" | provide[Option[ByteVector]](None))).as[ChannelTypes0.RemoteParams].decodeOnly - - val updateAddHtlcCodec: Codec[UpdateAddHtlc] = ( - ("channelId" | bytes32) :: - ("id" | uint64overflow) :: - ("amountMsat" | millisatoshi) :: - ("paymentHash" | bytes32) :: - ("expiry" | cltvExpiry) :: - ("onionRoutingPacket" | PaymentOnionCodecs.paymentOnionPacketCodec) :: - ("tlvStream" | provide(TlvStream.empty[UpdateAddHtlcTlv]))).as[UpdateAddHtlc] - - val htlcCodec: Codec[DirectedHtlc] = discriminated[DirectedHtlc].by(bool) - .typecase(true, updateAddHtlcCodec.as[IncomingHtlc]) - .typecase(false, updateAddHtlcCodec.as[OutgoingHtlc]) - - def setCodec[T](codec: Codec[T]): Codec[Set[T]] = Codec[Set[T]]( - (elems: Set[T]) => listOfN(uint16, codec).encode(elems.toList), - (wire: BitVector) => listOfN(uint16, codec).decode(wire).map(_.map(_.toSet)) - ) - - val commitmentSpecCodec: Codec[CommitmentSpec] = ( - ("htlcs" | setCodec(htlcCodec)) :: - ("feeratePerKw" | feeratePerKw) :: - ("toLocal" | millisatoshi) :: - ("toRemote" | millisatoshi)).as[CommitmentSpec].decodeOnly - - val outPointCodec: Codec[OutPoint] = variableSizeBytes(uint16, bytes.xmap(d => OutPoint.read(d.toArray), d => OutPoint.write(d))) - - val txOutCodec: Codec[TxOut] = variableSizeBytes(uint16, bytes.xmap(d => TxOut.read(d.toArray), d => TxOut.write(d))) - - val txCodec: Codec[Transaction] = variableSizeBytes(uint16, bytes.xmap(d => Transaction.read(d.toArray), d => Transaction.write(d))) - - val closingTxCodec: Codec[ClosingTx] = txCodec.decodeOnly.xmap( - tx => ChannelTypes0.migrateClosingTx(tx), - closingTx => closingTx.tx - ) - - val inputInfoCodec: Codec[InputInfo] = ( - ("outPoint" | outPointCodec) :: - ("txOut" | txOutCodec) :: - ("redeemScript" | varsizebinarydata)).as[InputInfo.SegwitInput].upcast[InputInfo].decodeOnly - - private val defaultConfirmationTarget: Codec[ConfirmationTarget.Absolute] = provide(ConfirmationTarget.Absolute(BlockHeight(0))) - - // We can safely set htlcId = 0 for htlc txs. This information is only used to find upstream htlcs to fail when a - // downstream htlc times out, and `Helpers.Closing.timedOutHtlcs` explicitly handles the case where htlcId is missing. - // We can also safely set confirmBefore = 0: we will simply use a high feerate to make these transactions confirm - // as quickly as possible. It's very unlikely that nodes will run into this, so it's a good trade-off between code - // complexity and real world impact. - val txWithInputInfoCodec: Codec[TransactionWithInputInfo] = discriminated[TransactionWithInputInfo].by(uint16) - .typecase(0x01, (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[CommitTx]) - .typecase(0x02, (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec) :: ("paymentHash" | bytes32) :: ("htlcId" | provide(0L)) :: ("confirmationTarget" | defaultConfirmationTarget)).as[HtlcSuccessTx]) - .typecase(0x03, (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec) :: ("htlcId" | provide(0L)) :: ("confirmBefore" | defaultConfirmationTarget)).as[HtlcTimeoutTx]) - .typecase(0x04, (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec) :: ("htlcId" | provide(0L)) :: ("confirmBefore" | defaultConfirmationTarget)).as[LegacyClaimHtlcSuccessTx]) - .typecase(0x05, (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec) :: ("htlcId" | provide(0L)) :: ("confirmBefore" | defaultConfirmationTarget)).as[ClaimHtlcTimeoutTx]) - .typecase(0x06, (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[ClaimP2WPKHOutputTx]) - .typecase(0x07, (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[ClaimLocalDelayedOutputTx]) - .typecase(0x08, (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[MainPenaltyTx]) - .typecase(0x09, (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[HtlcPenaltyTx]) - .typecase(0x10, (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec) :: ("outputIndex" | provide(Option.empty[OutputInfo]))).as[ClosingTx]) - - // this is a backward compatible codec (we used to store the sig as DER encoded), now we store it as 64-bytes - val sig64OrDERCodec: Codec[ByteVector64] = Codec[ByteVector64]( - (value: ByteVector64) => bytes(64).encode(value), - (wire: BitVector) => bytes.decode(wire).map(_.map { - case bin64 if bin64.size == 64 => ByteVector64(bin64) - case der => Crypto.der2compact(der) - }) - ) - - val htlcTxAndSigsCodec: Codec[HtlcTxAndSigs] = ( - ("txinfo" | txWithInputInfoCodec.downcast[HtlcTx]) :: - ("localSig" | variableSizeBytes(uint16, sig64OrDERCodec)) :: // we store as variable length for historical purposes (we used to store as DER encoded) - ("remoteSig" | variableSizeBytes(uint16, sig64OrDERCodec))).as[HtlcTxAndSigs].decodeOnly - - val publishableTxsCodec: Codec[PublishableTxs] = ( - ("commitTx" | (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[CommitTx]) :: - ("htlcTxsAndSigs" | listOfN(uint16, htlcTxAndSigsCodec))).as[PublishableTxs].decodeOnly - - val localCommitCodec: Codec[ChannelTypes0.LocalCommit] = ( - ("index" | uint64overflow) :: - ("spec" | commitmentSpecCodec) :: - ("publishableTxs" | publishableTxsCodec)).as[ChannelTypes0.LocalCommit].decodeOnly - - val remoteCommitCodec: Codec[RemoteCommit] = ( - ("index" | uint64overflow) :: - ("spec" | commitmentSpecCodec) :: - ("txid" | txId) :: - ("remotePerCommitmentPoint" | publicKey)).as[RemoteCommit].decodeOnly - - val updateFulfillHtlcCodec: Codec[UpdateFulfillHtlc] = ( - ("channelId" | bytes32) :: - ("id" | uint64overflow) :: - ("paymentPreimage" | bytes32) :: - ("tlvStream" | provide(TlvStream.empty[UpdateFulfillHtlcTlv]))).as[UpdateFulfillHtlc] - - val updateFailHtlcCodec: Codec[UpdateFailHtlc] = ( - ("channelId" | bytes32) :: - ("id" | uint64overflow) :: - ("reason" | varsizebinarydata) :: - ("tlvStream" | provide(TlvStream.empty[UpdateFailHtlcTlv]))).as[UpdateFailHtlc] - - val updateFailMalformedHtlcCodec: Codec[UpdateFailMalformedHtlc] = ( - ("channelId" | bytes32) :: - ("id" | uint64overflow) :: - ("onionHash" | bytes32) :: - ("failureCode" | uint16) :: - ("tlvStream" | provide(TlvStream.empty[UpdateFailMalformedHtlcTlv]))).as[UpdateFailMalformedHtlc] - - val updateFeeCodec: Codec[UpdateFee] = ( - ("channelId" | bytes32) :: - ("feeratePerKw" | feeratePerKw) :: - ("tlvStream" | provide(TlvStream.empty[UpdateFeeTlv]))).as[UpdateFee] - - val updateMessageCodec: Codec[UpdateMessage] = discriminated[UpdateMessage].by(uint16) - .typecase(128, updateAddHtlcCodec) - .typecase(130, updateFulfillHtlcCodec) - .typecase(131, updateFailHtlcCodec) - .typecase(134, updateFeeCodec) - .typecase(135, updateFailMalformedHtlcCodec) - - val localChangesCodec: Codec[LocalChanges] = ( - ("proposed" | listOfN(uint16, updateMessageCodec)) :: - ("signed" | listOfN(uint16, updateMessageCodec)) :: - ("acked" | listOfN(uint16, updateMessageCodec))).as[LocalChanges].decodeOnly - - val remoteChangesCodec: Codec[RemoteChanges] = ( - ("proposed" | listOfN(uint16, updateMessageCodec)) :: - ("acked" | listOfN(uint16, updateMessageCodec)) :: - ("signed" | listOfN(uint16, updateMessageCodec))).as[RemoteChanges].decodeOnly - - val commitSigCodec: Codec[CommitSig] = ( - ("channelId" | bytes32) :: - ("signature" | bytes64) :: - ("htlcSignatures" | listofsignatures) :: - ("tlvStream" | provide(TlvStream.empty[CommitSigTlv]))).as[CommitSig] - - val waitingForRevocationCodec: Codec[ChannelTypes0.WaitingForRevocation] = ( - ("nextRemoteCommit" | remoteCommitCodec) :: - ("sent" | commitSigCodec) :: - ("sentAfterLocalCommitIndex" | uint64overflow) :: - ("reSignAsap" | ignore(1))).as[ChannelTypes0.WaitingForRevocation].decodeOnly - - val upstreamLocalCodec: Codec[Upstream.Local] = ("id" | uuid).as[Upstream.Local] - - val upstreamChannelCodec: Codec[Upstream.Cold.Channel] = ( - ("originChannelId" | bytes32) :: - ("originHtlcId" | int64) :: - ("amountIn" | millisatoshi) :: - ("amountOut" | ignore(64))).as[Upstream.Cold.Channel] - - val upstreamChannelWithoutAmountCodec: Codec[Upstream.Cold.Channel] = ( - ("originChannelId" | bytes32) :: - ("originHtlcId" | int64) :: - ("amountIn" | provide(0 msat))).as[Upstream.Cold.Channel] - - val upstreamTrampolineCodec: Codec[Upstream.Cold.Trampoline] = listOfN(uint16, upstreamChannelWithoutAmountCodec).as[Upstream.Cold.Trampoline] - - // this is for backward compatibility to handle legacy payments that didn't have identifiers - val UNKNOWN_UUID: UUID = UUID.fromString("00000000-0000-0000-0000-000000000000") - - val coldUpstreamCodec: Codec[Upstream.Cold] = discriminated[Upstream.Cold].by(uint16) - .typecase(0x03, upstreamLocalCodec) // backward compatible - .typecase(0x01, provide(Upstream.Local(UNKNOWN_UUID))) - .typecase(0x02, upstreamChannelCodec) - .typecase(0x04, upstreamTrampolineCodec) - - val originCodec: Codec[Origin] = coldUpstreamCodec.xmap[Origin]( - upstream => Origin.Cold(upstream), - { - case Origin.Hot(_, upstream) => Upstream.Cold(upstream) - case Origin.Cold(upstream) => upstream - } - ) - - val originsListCodec: Codec[List[(Long, Origin)]] = listOfN(uint16, int64 ~ originCodec) - - val originsMapCodec: Codec[Map[Long, Origin]] = Codec[Map[Long, Origin]]( - (map: Map[Long, Origin]) => originsListCodec.encode(map.toList), - (wire: BitVector) => originsListCodec.decode(wire).map(_.map(_.toMap)) - ) - - val spentListCodec: Codec[List[(OutPoint, TxId)]] = listOfN(uint16, outPointCodec ~ txId) - - val spentMapCodec: Codec[Map[OutPoint, TxId]] = Codec[Map[OutPoint, TxId]]( - (map: Map[OutPoint, TxId]) => spentListCodec.encode(map.toList), - (wire: BitVector) => spentListCodec.decode(wire).map(_.map(_.toMap)) - ) - - val commitmentsCodec: Codec[Commitments] = ( - ("channelVersion" | channelVersionCodec) >>:~ { channelVersion => - ("localParams" | localParamsCodec(channelVersion)) :: - ("remoteParams" | remoteParamsCodec) :: - ("channelFlags" | channelflags) :: - ("localCommit" | localCommitCodec) :: - ("remoteCommit" | remoteCommitCodec) :: - ("localChanges" | localChangesCodec) :: - ("remoteChanges" | remoteChangesCodec) :: - ("localNextHtlcId" | uint64overflow) :: - ("remoteNextHtlcId" | uint64overflow) :: - ("originChannels" | originsMapCodec) :: - ("remoteNextCommitInfo" | either(bool, waitingForRevocationCodec, publicKey)) :: - ("commitInput" | inputInfoCodec) :: - ("remotePerCommitmentSecrets" | ShaChain.shaChainCodec) :: - ("channelId" | bytes32) - }).as[ChannelTypes0.Commitments].decodeOnly.map[Commitments](_.migrate()).decodeOnly - - val closingSignedCodec: Codec[ClosingSigned] = ( - ("channelId" | bytes32) :: - ("feeSatoshis" | satoshi) :: - ("signature" | bytes64) :: - ("tlvStream" | provide(TlvStream.empty[ClosingSignedTlv]))).as[ClosingSigned] - - val closingTxProposedCodec: Codec[ClosingTxProposed] = ( - ("unsignedTx" | closingTxCodec) :: - ("localClosingSigned" | closingSignedCodec)).as[ClosingTxProposed].decodeOnly - - val localCommitPublishedCodec: Codec[LocalCommitPublished] = ( - ("commitTx" | txCodec) :: - ("claimMainDelayedOutputTx" | optional(bool, txCodec)) :: - ("htlcSuccessTxs" | listOfN(uint16, txCodec)) :: - ("htlcTimeoutTxs" | listOfN(uint16, txCodec)) :: - ("claimHtlcDelayedTx" | listOfN(uint16, txCodec)) :: - ("spent" | spentMapCodec)).as[ChannelTypes0.LocalCommitPublished].decodeOnly.map[LocalCommitPublished](_.migrate()).decodeOnly - - val remoteCommitPublishedCodec: Codec[RemoteCommitPublished] = ( - ("commitTx" | txCodec) :: - ("claimMainOutputTx" | optional(bool, txCodec)) :: - ("claimHtlcSuccessTxs" | listOfN(uint16, txCodec)) :: - ("claimHtlcTimeoutTxs" | listOfN(uint16, txCodec)) :: - ("spent" | spentMapCodec)).as[ChannelTypes0.RemoteCommitPublished].decodeOnly.map[RemoteCommitPublished](_.migrate()).decodeOnly - - val revokedCommitPublishedCodec: Codec[RevokedCommitPublished] = ( - ("commitTx" | txCodec) :: - ("claimMainOutputTx" | optional(bool, txCodec)) :: - ("mainPenaltyTx" | optional(bool, txCodec)) :: - ("htlcPenaltyTxs" | listOfN(uint16, txCodec)) :: - ("claimHtlcDelayedPenaltyTxs" | listOfN(uint16, txCodec)) :: - ("spent" | spentMapCodec)).as[ChannelTypes0.RevokedCommitPublished].decodeOnly.map[RevokedCommitPublished](_.migrate()).decodeOnly - - // All channel_announcement's written prior to supporting unknown trailing fields had the same fixed size, because - // those are the announcements that *we* created and we always used an empty features field, which was the only - // variable-length field. - val noUnknownFieldsChannelAnnouncementSizeCodec: Codec[Int] = provide(430) - - // We used to ignore unknown trailing fields, and assume that channel_update size was known. This is not true anymore, - // so we need to tell the codec where to stop, otherwise all the remaining part of the data will be decoded as unknown - // fields. Fortunately, we can easily tell what size the channel_update will be. - val noUnknownFieldsChannelUpdateSizeCodec: Codec[Int] = peek( // we need to take a peek at a specific byte to know what size the message will be, and then rollback to read the full message - ignore(8 * (64 + 32 + 8 + 4)) ~> // we skip the first fields: signature + chain_hash + short_channel_id + timestamp - byte // this is the messageFlags byte - ) - .map(messageFlags => if ((messageFlags & 1) != 0) 136 else 128) // depending on the value of option_channel_htlc_max, size will be 128B or 136B - .decodeOnly // this is for compat, we only need to decode - - val fundingCreatedCodec: Codec[FundingCreated] = ( - ("temporaryChannelId" | bytes32) :: - ("fundingTxHash" | txIdAsHash) :: - ("fundingOutputIndex" | uint16) :: - ("signature" | bytes64) :: - ("tlvStream" | provide(TlvStream.empty[FundingCreatedTlv]))).as[FundingCreated] - - val fundingSignedCodec: Codec[FundingSigned] = ( - ("channelId" | bytes32) :: - ("signature" | bytes64) :: - ("tlvStream" | provide(TlvStream.empty[FundingSignedTlv]))).as[FundingSigned] - - val channelReadyCodec: Codec[ChannelReady] = ( - ("channelId" | bytes32) :: - ("nextPerCommitmentPoint" | publicKey) :: - ("tlvStream" | provide(TlvStream.empty[ChannelReadyTlv]))).as[ChannelReady] - - // this is a decode-only codec compatible with versions 997acee and below, with placeholders for new fields - val DATA_WAIT_FOR_FUNDING_CONFIRMED_01_Codec: Codec[DATA_WAIT_FOR_FUNDING_CONFIRMED] = ( - ("commitments" | commitmentsCodec) :: - ("fundingTx_opt" | provide[Option[Transaction]](None)) :: - ("waitingSince" | provide(BlockHeight(TimestampSecond.now().toLong))) :: - ("deferred" | optional(bool, channelReadyCodec)) :: - ("lastSent" | either(bool, fundingCreatedCodec, fundingSignedCodec))).map { - case commitments :: fundingTx :: waitingSince :: deferred :: lastSent :: HNil => - val commitments1 = ChannelTypes0.setFundingStatus(commitments, SingleFundedUnconfirmedFundingTx(fundingTx)) - DATA_WAIT_FOR_FUNDING_CONFIRMED(commitments1, waitingSince, deferred, lastSent) - }.decodeOnly - - val DATA_WAIT_FOR_FUNDING_CONFIRMED_08_Codec: Codec[DATA_WAIT_FOR_FUNDING_CONFIRMED] = ( - ("commitments" | commitmentsCodec) :: - ("fundingTx_opt" | optional(bool, txCodec)) :: - ("waitingSince" | int64.as[BlockHeight]) :: - ("deferred" | optional(bool, channelReadyCodec)) :: - ("lastSent" | either(bool, fundingCreatedCodec, fundingSignedCodec))).map { - case commitments :: fundingTx :: waitingSince :: deferred :: lastSent :: HNil => - val commitments1 = ChannelTypes0.setFundingStatus(commitments, SingleFundedUnconfirmedFundingTx(fundingTx)) - DATA_WAIT_FOR_FUNDING_CONFIRMED(commitments1, waitingSince, deferred, lastSent) - }.decodeOnly - - val DATA_WAIT_FOR_CHANNEL_READY_02_Codec: Codec[DATA_WAIT_FOR_CHANNEL_READY] = ( - ("commitments" | commitmentsCodec) :: - ("shortChannelId" | realshortchannelid) :: - ("lastSent" | channelReadyCodec)).map { - case commitments :: shortChannelId :: _ :: HNil => - DATA_WAIT_FOR_CHANNEL_READY(commitments, aliases = ShortIdAliases(localAlias = Alias(shortChannelId.toLong), remoteAlias_opt = None)) - }.decodeOnly - - val shutdownCodec: Codec[Shutdown] = ( - ("channelId" | bytes32) :: - ("scriptPubKey" | varsizebinarydata) :: - ("tlvStream" | provide(TlvStream.empty[ShutdownTlv]))).as[Shutdown] - - // this is a decode-only codec compatible with versions 9afb26e and below - val DATA_NORMAL_03_Codec: Codec[DATA_NORMAL] = ( - ("commitments" | commitmentsCodec) :: - ("shortChannelId" | realshortchannelid) :: - ("buried" | bool) :: - ("channelAnnouncement" | optional(bool, variableSizeBytes(noUnknownFieldsChannelAnnouncementSizeCodec, channelAnnouncementCodec))) :: - ("channelUpdate" | variableSizeBytes(noUnknownFieldsChannelUpdateSizeCodec, channelUpdateCodec)) :: - ("localShutdown" | optional(bool, shutdownCodec)) :: - ("remoteShutdown" | optional(bool, shutdownCodec)) :: - ("closeStatus" | provide(Option.empty[CloseStatus]))).map { - case commitments :: shortChannelId :: _ :: channelAnnouncement :: channelUpdate :: localShutdown :: remoteShutdown :: closingFeerates :: HNil => - val aliases = ShortIdAliases(localAlias = Alias(shortChannelId.toLong), remoteAlias_opt = None) - DATA_NORMAL(commitments, aliases, channelAnnouncement, channelUpdate, localShutdown, remoteShutdown, closingFeerates, SpliceStatus.NoSplice) - }.decodeOnly - - val DATA_NORMAL_10_Codec: Codec[DATA_NORMAL] = ( - ("commitments" | commitmentsCodec) :: - ("shortChannelId" | realshortchannelid) :: - ("buried" | bool) :: - ("channelAnnouncement" | optional(bool, variableSizeBytes(uint16, channelAnnouncementCodec))) :: - ("channelUpdate" | variableSizeBytes(uint16, channelUpdateCodec)) :: - ("localShutdown" | optional(bool, shutdownCodec)) :: - ("remoteShutdown" | optional(bool, shutdownCodec)) :: - ("closeStatus" | provide(Option.empty[CloseStatus]))).map { - case commitments :: shortChannelId :: _ :: channelAnnouncement :: channelUpdate :: localShutdown :: remoteShutdown :: closingFeerates :: HNil => - val aliases = ShortIdAliases(localAlias = Alias(shortChannelId.toLong), remoteAlias_opt = None) - DATA_NORMAL(commitments, aliases, channelAnnouncement, channelUpdate, localShutdown, remoteShutdown, closingFeerates, SpliceStatus.NoSplice) - }.decodeOnly - - val DATA_SHUTDOWN_04_Codec: Codec[DATA_SHUTDOWN] = ( - ("commitments" | commitmentsCodec) :: - ("localShutdown" | shutdownCodec) :: - ("remoteShutdown" | shutdownCodec) :: - ("closeStatus" | provide[CloseStatus](CloseStatus.Initiator(None)))).as[DATA_SHUTDOWN].decodeOnly - - val DATA_NEGOTIATING_05_Codec: Codec[DATA_NEGOTIATING] = ( - ("commitments" | commitmentsCodec) :: - ("localShutdown" | shutdownCodec) :: - ("remoteShutdown" | shutdownCodec) :: - ("closingTxProposed" | listOfN(uint16, listOfN(uint16, closingTxProposedCodec))) :: - ("bestUnpublishedClosingTx_opt" | optional(bool, closingTxCodec))).as[DATA_NEGOTIATING].decodeOnly - - // this is a decode-only codec compatible with versions 818199e and below, with placeholders for new fields - val DATA_CLOSING_06_Codec: Codec[DATA_CLOSING] = ( - ("commitments" | commitmentsCodec) :: - ("fundingTx_opt" | provide[Option[Transaction]](None)) :: - ("waitingSince" | provide(BlockHeight(TimestampSecond.now().toLong))) :: - ("mutualCloseProposed" | listOfN(uint16, closingTxCodec)) :: - ("mutualClosePublished" | listOfN(uint16, closingTxCodec)) :: - ("localCommitPublished" | optional(bool, localCommitPublishedCodec)) :: - ("remoteCommitPublished" | optional(bool, remoteCommitPublishedCodec)) :: - ("nextRemoteCommitPublished" | optional(bool, remoteCommitPublishedCodec)) :: - ("futureRemoteCommitPublished" | optional(bool, remoteCommitPublishedCodec)) :: - ("revokedCommitPublished" | listOfN(uint16, revokedCommitPublishedCodec))).map { - case commitments :: fundingTx_opt :: waitingSince :: mutualCloseProposed :: mutualClosePublished :: localCommitPublished :: remoteCommitPublished :: nextRemoteCommitPublished :: futureRemoteCommitPublished :: revokedCommitPublished :: HNil => - val commitments1 = ChannelTypes0.setFundingStatus(commitments, SingleFundedUnconfirmedFundingTx(fundingTx_opt)) - DATA_CLOSING(commitments1, waitingSince, commitments1.params.localParams.upfrontShutdownScript_opt.get, mutualCloseProposed, mutualClosePublished, localCommitPublished, remoteCommitPublished, nextRemoteCommitPublished, futureRemoteCommitPublished, revokedCommitPublished) - }.decodeOnly - - val DATA_CLOSING_09_Codec: Codec[DATA_CLOSING] = ( - ("commitments" | commitmentsCodec) :: - ("fundingTx_opt" | optional(bool, txCodec)) :: - ("waitingSince" | int64.as[BlockHeight]) :: - ("mutualCloseProposed" | listOfN(uint16, closingTxCodec)) :: - ("mutualClosePublished" | listOfN(uint16, closingTxCodec)) :: - ("localCommitPublished" | optional(bool, localCommitPublishedCodec)) :: - ("remoteCommitPublished" | optional(bool, remoteCommitPublishedCodec)) :: - ("nextRemoteCommitPublished" | optional(bool, remoteCommitPublishedCodec)) :: - ("futureRemoteCommitPublished" | optional(bool, remoteCommitPublishedCodec)) :: - ("revokedCommitPublished" | listOfN(uint16, revokedCommitPublishedCodec))).map { - case commitments :: fundingTx_opt :: waitingSince :: mutualCloseProposed :: mutualClosePublished :: localCommitPublished :: remoteCommitPublished :: nextRemoteCommitPublished :: futureRemoteCommitPublished :: revokedCommitPublished :: HNil => - val commitments1 = ChannelTypes0.setFundingStatus(commitments, SingleFundedUnconfirmedFundingTx(fundingTx_opt)) - DATA_CLOSING(commitments1, waitingSince, commitments1.params.localParams.upfrontShutdownScript_opt.get, mutualCloseProposed, mutualClosePublished, localCommitPublished, remoteCommitPublished, nextRemoteCommitPublished, futureRemoteCommitPublished, revokedCommitPublished) - }.decodeOnly - - val channelReestablishCodec: Codec[ChannelReestablish] = ( - ("channelId" | bytes32) :: - ("nextLocalCommitmentNumber" | uint64overflow) :: - ("nextRemoteRevocationNumber" | uint64overflow) :: - ("yourLastPerCommitmentSecret" | privateKey) :: - ("myCurrentPerCommitmentPoint" | publicKey) :: - ("tlvStream" | provide(TlvStream.empty[ChannelReestablishTlv]))).as[ChannelReestablish] - - val DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT_07_Codec: Codec[DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT] = ( - ("commitments" | commitmentsCodec) :: - ("remoteChannelReestablish" | channelReestablishCodec)).as[DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT].decodeOnly - } - - // Order matters! - val channelDataCodec: Codec[PersistentChannelData] = discriminated[PersistentChannelData].by(uint16) - .typecase(0x10, Codecs.DATA_NORMAL_10_Codec) - .typecase(0x09, Codecs.DATA_CLOSING_09_Codec) - .typecase(0x08, Codecs.DATA_WAIT_FOR_FUNDING_CONFIRMED_08_Codec) - .typecase(0x01, Codecs.DATA_WAIT_FOR_FUNDING_CONFIRMED_01_Codec) - .typecase(0x02, Codecs.DATA_WAIT_FOR_CHANNEL_READY_02_Codec) - .typecase(0x03, Codecs.DATA_NORMAL_03_Codec) - .typecase(0x04, Codecs.DATA_SHUTDOWN_04_Codec) - .typecase(0x05, Codecs.DATA_NEGOTIATING_05_Codec) - .typecase(0x06, Codecs.DATA_CLOSING_06_Codec) - .typecase(0x07, Codecs.DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT_07_Codec) - -} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelTypes0.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelTypes0.scala deleted file mode 100644 index f11978187c..0000000000 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelTypes0.scala +++ /dev/null @@ -1,269 +0,0 @@ -/* - * Copyright 2021 ACINQ SAS - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package fr.acinq.eclair.wire.internal.channel.version0 - -import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey -import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, Crypto, OP_CHECKMULTISIG, OP_PUSHDATA, OutPoint, Satoshi, Script, ScriptWitness, Transaction, TxId, TxOut} -import fr.acinq.eclair.blockchain.fee.ConfirmationTarget -import fr.acinq.eclair.channel._ -import fr.acinq.eclair.crypto.ShaChain -import fr.acinq.eclair.transactions.CommitmentSpec -import fr.acinq.eclair.transactions.Transactions._ -import fr.acinq.eclair.wire.protocol.CommitSig -import fr.acinq.eclair.{BlockHeight, CltvExpiryDelta, Features, InitFeature, MilliSatoshi, UInt64, channel} -import scodec.bits.{BitVector, ByteVector} - -private[channel] object ChannelTypes0 { - - // The format of the XxxCommitPublished types was changed in version2 to work with anchor outputs channels. - // Before that, all closing txs were generated once (when we detected the force-close) and never updated afterwards - // (with the exception of 3rd-stage penalty transactions for revoked commitments when one of their htlc txs wins the - // race against our htlc-penalty tx, but if that happens a `WatchSpent` will be triggered and we will claim it correctly). - // When migrating from these previous types, we can safely set dummy values in the following fields: - // - we only use the `tx` field of `TransactionWithInputInfo` -> no need to completely fill the `InputInfo` - // - `irrevocablySpent` now contains the whole transaction (previously only the txid): we can easily set these when - // one of *our* transactions confirmed, but not when a *remote* transaction confirms. This can only happen for HTLC - // outputs and in these cases we simply remove the entry in `irrevocablySpent`: the channel will set a `WatchSpent` - // which will immediately be triggered and that will let us store the information in `irrevocablySpent`. - // - the `htlcId` in htlc txs is used to detect timed out htlcs and relay them upstream, but it can be safely set to - // 0 because the `timedOutHtlcs` in `Helpers.scala` explicitly handle the case where this information is unavailable. - - private def getPartialInputInfo(parentTx: Transaction, childTx: Transaction): InputInfo = { - // When using the default commitment format, spending txs have a single input. These txs are fully signed and never - // modified: we don't use the InputInfo in closing business logic, so we don't need to fill everything (this part - // assumes that we only have standard channels, no anchor output channels - which was the case before version2). - val input = childTx.txIn.head.outPoint - InputInfo(input, parentTx.txOut(input.index.toInt), Nil) - } - - case class LocalCommitPublished(commitTx: Transaction, claimMainDelayedOutputTx: Option[Transaction], htlcSuccessTxs: List[Transaction], htlcTimeoutTxs: List[Transaction], claimHtlcDelayedTxs: List[Transaction], irrevocablySpent: Map[OutPoint, TxId]) { - def migrate(): channel.LocalCommitPublished = { - val htlcTxs = htlcSuccessTxs ++ htlcTimeoutTxs - val knownTxs: Map[TxId, Transaction] = (commitTx :: claimMainDelayedOutputTx.toList ::: htlcTxs ::: claimHtlcDelayedTxs).map(tx => tx.txid -> tx).toMap - // NB: irrevocablySpent may contain transactions that belong to our peer: we will drop them in this migration but - // the channel will put a watch at start-up which will make us fetch the spending transaction. - val irrevocablySpentNew = irrevocablySpent.collect { case (outpoint, txid) if knownTxs.contains(txid) => (outpoint, knownTxs(txid)) } - val claimMainDelayedOutputTxNew = claimMainDelayedOutputTx.map(tx => ClaimLocalDelayedOutputTx(getPartialInputInfo(commitTx, tx), tx)) - val htlcSuccessTxsNew = htlcSuccessTxs.map(tx => HtlcSuccessTx(getPartialInputInfo(commitTx, tx), tx, ByteVector32.Zeroes, 0, ConfirmationTarget.Absolute(BlockHeight(0)))) - val htlcTimeoutTxsNew = htlcTimeoutTxs.map(tx => HtlcTimeoutTx(getPartialInputInfo(commitTx, tx), tx, 0, ConfirmationTarget.Absolute(BlockHeight(0)))) - val htlcTxsNew = (htlcSuccessTxsNew ++ htlcTimeoutTxsNew).map(tx => tx.input.outPoint -> Some(tx)).toMap - val claimHtlcDelayedTxsNew = claimHtlcDelayedTxs.map(tx => { - val htlcTx = htlcTxs.find(_.txid == tx.txIn.head.outPoint.txid) - require(htlcTx.nonEmpty, s"3rd-stage htlc tx doesn't spend one of our htlc txs: claim-htlc-tx=$tx, htlc-txs=${htlcTxs.mkString(",")}") - HtlcDelayedTx(getPartialInputInfo(htlcTx.get, tx), tx) - }) - channel.LocalCommitPublished(commitTx, claimMainDelayedOutputTxNew, htlcTxsNew, claimHtlcDelayedTxsNew, Nil, irrevocablySpentNew) - } - } - - case class RemoteCommitPublished(commitTx: Transaction, claimMainOutputTx: Option[Transaction], claimHtlcSuccessTxs: List[Transaction], claimHtlcTimeoutTxs: List[Transaction], irrevocablySpent: Map[OutPoint, TxId]) { - def migrate(): channel.RemoteCommitPublished = { - val claimHtlcTxs = claimHtlcSuccessTxs ::: claimHtlcTimeoutTxs - val knownTxs: Map[TxId, Transaction] = (commitTx :: claimMainOutputTx.toList ::: claimHtlcTxs).map(tx => tx.txid -> tx).toMap - // NB: irrevocablySpent may contain transactions that belong to our peer: we will drop them in this migration but - // the channel will put a watch at start-up which will make us fetch the spending transaction. - val irrevocablySpentNew = irrevocablySpent.collect { case (outpoint, txid) if knownTxs.contains(txid) => (outpoint, knownTxs(txid)) } - val claimMainOutputTxNew = claimMainOutputTx.map(tx => ClaimP2WPKHOutputTx(getPartialInputInfo(commitTx, tx), tx)) - val claimHtlcSuccessTxsNew = claimHtlcSuccessTxs.map(tx => LegacyClaimHtlcSuccessTx(getPartialInputInfo(commitTx, tx), tx, 0, ConfirmationTarget.Absolute(BlockHeight(0)))) - val claimHtlcTimeoutTxsNew = claimHtlcTimeoutTxs.map(tx => ClaimHtlcTimeoutTx(getPartialInputInfo(commitTx, tx), tx, 0, ConfirmationTarget.Absolute(BlockHeight(0)))) - val claimHtlcTxsNew = (claimHtlcSuccessTxsNew ++ claimHtlcTimeoutTxsNew).map(tx => tx.input.outPoint -> Some(tx)).toMap - channel.RemoteCommitPublished(commitTx, claimMainOutputTxNew, claimHtlcTxsNew, Nil, irrevocablySpentNew) - } - } - - case class RevokedCommitPublished(commitTx: Transaction, claimMainOutputTx: Option[Transaction], mainPenaltyTx: Option[Transaction], htlcPenaltyTxs: List[Transaction], claimHtlcDelayedPenaltyTxs: List[Transaction], irrevocablySpent: Map[OutPoint, TxId]) { - def migrate(): channel.RevokedCommitPublished = { - val knownTxs: Map[TxId, Transaction] = (commitTx :: claimMainOutputTx.toList ::: mainPenaltyTx.toList ::: htlcPenaltyTxs ::: claimHtlcDelayedPenaltyTxs).map(tx => tx.txid -> tx).toMap - // NB: irrevocablySpent may contain transactions that belong to our peer: we will drop them in this migration but - // the channel will put a watch at start-up which will make us fetch the spending transaction. - val irrevocablySpentNew = irrevocablySpent.collect { case (outpoint, txid) if knownTxs.contains(txid) => (outpoint, knownTxs(txid)) } - val claimMainOutputTxNew = claimMainOutputTx.map(tx => ClaimP2WPKHOutputTx(getPartialInputInfo(commitTx, tx), tx)) - val mainPenaltyTxNew = mainPenaltyTx.map(tx => MainPenaltyTx(getPartialInputInfo(commitTx, tx), tx)) - val htlcPenaltyTxsNew = htlcPenaltyTxs.map(tx => HtlcPenaltyTx(getPartialInputInfo(commitTx, tx), tx)) - val claimHtlcDelayedPenaltyTxsNew = claimHtlcDelayedPenaltyTxs.map(tx => { - // We don't have all the `InputInfo` data, but it's ok: we only use the tx that is fully signed. - ClaimHtlcDelayedOutputPenaltyTx(InputInfo(tx.txIn.head.outPoint, TxOut(Satoshi(0), Nil), Nil), tx) - }) - channel.RevokedCommitPublished(commitTx, claimMainOutputTxNew, mainPenaltyTxNew, htlcPenaltyTxsNew, claimHtlcDelayedPenaltyTxsNew, irrevocablySpentNew) - } - } - - def setFundingStatus(commitments: fr.acinq.eclair.channel.Commitments, status: LocalFundingStatus): fr.acinq.eclair.channel.Commitments = { - commitments.copy( - active = commitments.active.head.copy(localFundingStatus = status) +: commitments.active.tail - ) - } - - /** - * Starting with version2, we store a complete ClosingTx object for mutual close scenarios instead of simply storing - * the raw transaction. It provides more information for auditing but is not used for business logic, so we can safely - * put dummy values in the migration. - */ - def migrateClosingTx(tx: Transaction): ClosingTx = ClosingTx(InputInfo(tx.txIn.head.outPoint, TxOut(Satoshi(0), Nil), Nil), tx, None) - - case class HtlcTxAndSigs(txinfo: HtlcTx, localSig: ByteVector64, remoteSig: ByteVector64) - - case class PublishableTxs(commitTx: CommitTx, htlcTxsAndSigs: List[HtlcTxAndSigs]) - - // Before version3, we stored fully signed local transactions (commit tx and htlc txs). It meant that someone gaining - // access to the database could publish revoked commit txs, so we changed that to only store unsigned txs and remote - // signatures. - case class LocalCommit(index: Long, spec: CommitmentSpec, publishableTxs: PublishableTxs) { - def migrate(remoteFundingPubKey: PublicKey): channel.LocalCommit = { - val remoteSig = extractRemoteSig(publishableTxs.commitTx, remoteFundingPubKey) - val unsignedCommitTx = publishableTxs.commitTx.copy(tx = removeWitnesses(publishableTxs.commitTx.tx)) - val commitTxAndRemoteSig = CommitTxAndRemoteSig(unsignedCommitTx, remoteSig) - val htlcTxsAndRemoteSigs = publishableTxs.htlcTxsAndSigs map { - case HtlcTxAndSigs(htlcTx: HtlcSuccessTx, _, remoteSig) => - val unsignedHtlcTx = htlcTx.copy(tx = removeWitnesses(htlcTx.tx)) - HtlcTxAndRemoteSig(unsignedHtlcTx, remoteSig) - case HtlcTxAndSigs(htlcTx: HtlcTimeoutTx, _, remoteSig) => - val unsignedHtlcTx = htlcTx.copy(tx = removeWitnesses(htlcTx.tx)) - HtlcTxAndRemoteSig(unsignedHtlcTx, remoteSig) - } - channel.LocalCommit(index, spec, commitTxAndRemoteSig, htlcTxsAndRemoteSigs) - } - - private def extractRemoteSig(commitTx: CommitTx, remoteFundingPubKey: PublicKey): ByteVector64 = { - require(commitTx.tx.txIn.size == 1, s"commit tx must have exactly one input, found ${commitTx.tx.txIn.size}") - val ScriptWitness(Seq(_, sig1, sig2, redeemScript)) = commitTx.tx.txIn.head.witness - val _ :: OP_PUSHDATA(pub1, _) :: OP_PUSHDATA(pub2, _) :: _ :: OP_CHECKMULTISIG :: Nil = Script.parse(redeemScript) - require(pub1 == remoteFundingPubKey.value || pub2 == remoteFundingPubKey.value, "unrecognized funding pubkey") - if (pub1 == remoteFundingPubKey.value) { - Crypto.der2compact(sig1) - } else { - Crypto.der2compact(sig2) - } - } - } - - private def removeWitnesses(tx: Transaction): Transaction = tx.copy(txIn = tx.txIn.map(_.copy(witness = ScriptWitness.empty))) - - // Before version3, we had a ChannelVersion field describing what channel features were activated. It was mixing - // official features (static_remotekey, anchor_outputs) and internal features (channel key derivation scheme). - // We separated this into two separate fields in version3: - // - a channel type field containing the channel Bolt 9 features - // - an internal channel configuration field - case class ChannelVersion(bits: BitVector) { - // @formatter:off - def isSet(bit: Int): Boolean = bits.reverse.get(bit) - def |(other: ChannelVersion): ChannelVersion = ChannelVersion(bits | other.bits) - - def hasPubkeyKeyPath: Boolean = isSet(ChannelVersion.USE_PUBKEY_KEYPATH_BIT) - def hasStaticRemotekey: Boolean = isSet(ChannelVersion.USE_STATIC_REMOTEKEY_BIT) - def hasAnchorOutputs: Boolean = isSet(ChannelVersion.USE_ANCHOR_OUTPUTS_BIT) - def paysDirectlyToWallet: Boolean = hasStaticRemotekey && !hasAnchorOutputs - // @formatter:on - } - - object ChannelVersion { - - import scodec.bits._ - - val LENGTH_BITS: Int = 4 * 8 - - private val USE_PUBKEY_KEYPATH_BIT = 0 // bit numbers start at 0 - private val USE_STATIC_REMOTEKEY_BIT = 1 - private val USE_ANCHOR_OUTPUTS_BIT = 2 - - def fromBit(bit: Int): ChannelVersion = ChannelVersion(BitVector.low(LENGTH_BITS).set(bit).reverse) - - val ZEROES = ChannelVersion(bin"00000000000000000000000000000000") - val STANDARD = ZEROES | fromBit(USE_PUBKEY_KEYPATH_BIT) - val STATIC_REMOTEKEY = STANDARD | fromBit(USE_STATIC_REMOTEKEY_BIT) // PUBKEY_KEYPATH + STATIC_REMOTEKEY - val ANCHOR_OUTPUTS = STATIC_REMOTEKEY | fromBit(USE_ANCHOR_OUTPUTS_BIT) // PUBKEY_KEYPATH + STATIC_REMOTEKEY + ANCHOR_OUTPUTS - } - - case class RemoteParams(nodeId: PublicKey, - dustLimit: Satoshi, - maxHtlcValueInFlightMsat: UInt64, // this is not MilliSatoshi because it can exceed the total amount of MilliSatoshi - requestedChannelReserve_opt: Option[Satoshi], - htlcMinimum: MilliSatoshi, - toSelfDelay: CltvExpiryDelta, - maxAcceptedHtlcs: Int, - fundingPubKey: PublicKey, - revocationBasepoint: PublicKey, - paymentBasepoint: PublicKey, - delayedPaymentBasepoint: PublicKey, - htlcBasepoint: PublicKey, - initFeatures: Features[InitFeature], - upfrontShutdownScript_opt: Option[ByteVector]) { - def migrate(): channel.RemoteParams = channel.RemoteParams( - nodeId = nodeId, - dustLimit = dustLimit, - maxHtlcValueInFlightMsat = maxHtlcValueInFlightMsat, - initialRequestedChannelReserve_opt = requestedChannelReserve_opt, - htlcMinimum = htlcMinimum, - toSelfDelay = toSelfDelay, - maxAcceptedHtlcs = maxAcceptedHtlcs, - revocationBasepoint = revocationBasepoint, - paymentBasepoint = paymentBasepoint, - delayedPaymentBasepoint = delayedPaymentBasepoint, - htlcBasepoint = htlcBasepoint, - initFeatures = initFeatures, - upfrontShutdownScript_opt = upfrontShutdownScript_opt - ) - } - - case class WaitingForRevocation(nextRemoteCommit: RemoteCommit, sent: CommitSig, sentAfterLocalCommitIndex: Long) - - case class Commitments(channelVersion: ChannelVersion, - localParams: LocalParams, remoteParams: RemoteParams, - channelFlags: ChannelFlags, - localCommit: LocalCommit, remoteCommit: RemoteCommit, - localChanges: LocalChanges, remoteChanges: RemoteChanges, - localNextHtlcId: Long, remoteNextHtlcId: Long, - originChannels: Map[Long, Origin], - remoteNextCommitInfo: Either[WaitingForRevocation, PublicKey], - commitInput: InputInfo, - remotePerCommitmentSecrets: ShaChain, channelId: ByteVector32) { - def migrate(): channel.Commitments = { - val channelConfig = if (channelVersion.hasPubkeyKeyPath) { - ChannelConfig(ChannelConfig.FundingPubKeyBasedChannelKeyPath) - } else { - ChannelConfig() - } - val channelFeatures = if (channelVersion.hasAnchorOutputs) { - ChannelFeatures(Features.StaticRemoteKey, Features.AnchorOutputs) - } else if (channelVersion.hasStaticRemotekey) { - ChannelFeatures(Features.StaticRemoteKey) - } else { - ChannelFeatures() - } - val commitment = Commitment( - fundingTxIndex = 0, - firstRemoteCommitIndex = 0, - remoteFundingPubKey = remoteParams.fundingPubKey, - // We set an empty funding tx, even if it may be confirmed already (and the channel fully operational). We could - // have set a specific Unknown status, but it would have forced us to keep it forever. We will retrieve the - // funding tx when the channel is instantiated, and update the status (possibly immediately if it was confirmed). - LocalFundingStatus.SingleFundedUnconfirmedFundingTx(None), RemoteFundingStatus.Locked, - localCommit.migrate(remoteParams.fundingPubKey), remoteCommit, remoteNextCommitInfo.left.toOption.map(w => NextRemoteCommit(w.sent, w.nextRemoteCommit)) - ) - channel.Commitments( - ChannelParams(channelId, channelConfig, channelFeatures, localParams, remoteParams.migrate(), channelFlags), - CommitmentChanges(localChanges, remoteChanges, localNextHtlcId, remoteNextHtlcId), - Seq(commitment), - inactive = Nil, - remoteNextCommitInfo.fold(w => Left(WaitForRev(w.sentAfterLocalCommitIndex)), remotePerCommitmentPoint => Right(remotePerCommitmentPoint)), - remotePerCommitmentSecrets, - originChannels - ) - } - } - -} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1.scala deleted file mode 100644 index 323774a41f..0000000000 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1.scala +++ /dev/null @@ -1,313 +0,0 @@ -/* - * Copyright 2021 ACINQ SAS - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package fr.acinq.eclair.wire.internal.channel.version1 - -import fr.acinq.bitcoin.scalacompat.DeterministicWallet.KeyPath -import fr.acinq.bitcoin.scalacompat.{OutPoint, Transaction, TxId, TxOut} -import fr.acinq.eclair.blockchain.fee.ConfirmationTarget -import fr.acinq.eclair.channel.LocalFundingStatus.SingleFundedUnconfirmedFundingTx -import fr.acinq.eclair.channel._ -import fr.acinq.eclair.crypto.ShaChain -import fr.acinq.eclair.transactions.Transactions._ -import fr.acinq.eclair.transactions.{CommitmentSpec, DirectedHtlc, IncomingHtlc, OutgoingHtlc} -import fr.acinq.eclair.wire.internal.channel.version0.ChannelTypes0 -import fr.acinq.eclair.wire.internal.channel.version0.ChannelTypes0.{HtlcTxAndSigs, PublishableTxs} -import fr.acinq.eclair.wire.protocol.CommonCodecs._ -import fr.acinq.eclair.wire.protocol.LightningMessageCodecs._ -import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{Alias, BlockHeight, MilliSatoshiLong} -import scodec.bits.ByteVector -import scodec.codecs._ -import scodec.{Attempt, Codec} -import shapeless.{::, HNil} - -private[channel] object ChannelCodecs1 { - - private[version1] object Codecs { - - val keyPathCodec: Codec[KeyPath] = ("path" | listOfN(uint16, uint32)).xmap[KeyPath](l => KeyPath(l), keyPath => keyPath.path.toList).as[KeyPath] - - val channelVersionCodec: Codec[ChannelTypes0.ChannelVersion] = bits(ChannelTypes0.ChannelVersion.LENGTH_BITS).as[ChannelTypes0.ChannelVersion] - - def localParamsCodec(channelVersion: ChannelTypes0.ChannelVersion): Codec[LocalParams] = ( - ("nodeId" | publicKey) :: - ("channelPath" | keyPathCodec) :: - ("dustLimit" | satoshi) :: - ("maxHtlcValueInFlightMsat" | millisatoshi) :: - ("channelReserve" | conditional(included = true, satoshi)) :: - ("htlcMinimum" | millisatoshi) :: - ("toSelfDelay" | cltvExpiryDelta) :: - ("maxAcceptedHtlcs" | uint16) :: - ("isChannelOpener" | bool) :: ("paysCommitTxFees" | bool) :: ignore(6) :: - ("upfrontShutdownScript_opt" | lengthDelimited(bytes).map(Option(_)).decodeOnly) :: - ("walletStaticPaymentBasepoint" | optional(provide(channelVersion.paysDirectlyToWallet), publicKey)) :: - ("features" | combinedFeaturesCodec)).as[LocalParams].decodeOnly - - val remoteParamsCodec: Codec[ChannelTypes0.RemoteParams] = ( - ("nodeId" | publicKey) :: - ("dustLimit" | satoshi) :: - ("maxHtlcValueInFlightMsat" | uint64) :: - ("channelReserve" | conditional(included = true, satoshi)) :: - ("htlcMinimum" | millisatoshi) :: - ("toSelfDelay" | cltvExpiryDelta) :: - ("maxAcceptedHtlcs" | uint16) :: - ("fundingPubKey" | publicKey) :: - ("revocationBasepoint" | publicKey) :: - ("paymentBasepoint" | publicKey) :: - ("delayedPaymentBasepoint" | publicKey) :: - ("htlcBasepoint" | publicKey) :: - ("features" | combinedFeaturesCodec) :: - ("shutdownScript" | provide[Option[ByteVector]](None))).as[ChannelTypes0.RemoteParams] - - def setCodec[T](codec: Codec[T]): Codec[Set[T]] = listOfN(uint16, codec).xmap(_.toSet, _.toList) - - val htlcCodec: Codec[DirectedHtlc] = discriminated[DirectedHtlc].by(bool8) - .typecase(true, lengthDelimited(updateAddHtlcCodec).as[IncomingHtlc]) - .typecase(false, lengthDelimited(updateAddHtlcCodec).as[OutgoingHtlc]) - - val commitmentSpecCodec: Codec[CommitmentSpec] = ( - ("htlcs" | setCodec(htlcCodec)) :: - ("feeratePerKw" | feeratePerKw) :: - ("toLocal" | millisatoshi) :: - ("toRemote" | millisatoshi)).as[CommitmentSpec] - - val outPointCodec: Codec[OutPoint] = lengthDelimited(bytes.xmap(d => OutPoint.read(d.toArray), d => OutPoint.write(d))) - - val txOutCodec: Codec[TxOut] = lengthDelimited(bytes.xmap(d => TxOut.read(d.toArray), d => TxOut.write(d))) - - val txCodec: Codec[Transaction] = lengthDelimited(bytes.xmap(d => Transaction.read(d.toArray), d => Transaction.write(d))) - - val closingTxCodec: Codec[ClosingTx] = txCodec.decodeOnly.xmap( - tx => ChannelTypes0.migrateClosingTx(tx), - closingTx => closingTx.tx - ) - - val inputInfoCodec: Codec[InputInfo] = ( - ("outPoint" | outPointCodec) :: - ("txOut" | txOutCodec) :: - ("redeemScript" | lengthDelimited(bytes))).as[InputInfo.SegwitInput].upcast[InputInfo].decodeOnly - - private val defaultConfirmationTarget: Codec[ConfirmationTarget.Absolute] = provide(ConfirmationTarget.Absolute(BlockHeight(0))) - - // NB: we can safely set htlcId = 0 for htlc txs. This information is only used to find upstream htlcs to fail when a - // downstream htlc times out, and `Helpers.Closing.timedOutHtlcs` explicitly handles the case where htlcId is missing. - val txWithInputInfoCodec: Codec[TransactionWithInputInfo] = discriminated[TransactionWithInputInfo].by(uint16) - .typecase(0x01, (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[CommitTx]) - .typecase(0x02, (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec) :: ("paymentHash" | bytes32) :: ("htlcId" | provide(0L)) :: ("confirmationTargetBefore" | defaultConfirmationTarget)).as[HtlcSuccessTx]) - .typecase(0x03, (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec) :: ("htlcId" | provide(0L)) :: ("confirmBefore" | defaultConfirmationTarget)).as[HtlcTimeoutTx]) - .typecase(0x04, (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec) :: ("htlcId" | provide(0L)) :: ("confirmBefore" | defaultConfirmationTarget)).as[LegacyClaimHtlcSuccessTx]) - .typecase(0x05, (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec) :: ("htlcId" | provide(0L)) :: ("confirmBefore" | defaultConfirmationTarget)).as[ClaimHtlcTimeoutTx]) - .typecase(0x06, (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[ClaimP2WPKHOutputTx]) - .typecase(0x07, (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[ClaimLocalDelayedOutputTx]) - .typecase(0x08, (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[MainPenaltyTx]) - .typecase(0x09, (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[HtlcPenaltyTx]) - .typecase(0x10, (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec) :: ("outputIndex" | provide(Option.empty[OutputInfo]))).as[ClosingTx]) - - val htlcTxAndSigsCodec: Codec[HtlcTxAndSigs] = ( - ("txinfo" | txWithInputInfoCodec.downcast[HtlcTx]) :: - ("localSig" | lengthDelimited(bytes64)) :: // we store as variable length for historical purposes (we used to store as DER encoded) - ("remoteSig" | lengthDelimited(bytes64))).as[HtlcTxAndSigs] - - val publishableTxsCodec: Codec[PublishableTxs] = ( - ("commitTx" | (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[CommitTx]) :: - ("htlcTxsAndSigs" | listOfN(uint16, htlcTxAndSigsCodec))).as[PublishableTxs] - - val localCommitCodec: Codec[ChannelTypes0.LocalCommit] = ( - ("index" | uint64overflow) :: - ("spec" | commitmentSpecCodec) :: - ("publishableTxs" | publishableTxsCodec)).as[ChannelTypes0.LocalCommit].decodeOnly - - val remoteCommitCodec: Codec[RemoteCommit] = ( - ("index" | uint64overflow) :: - ("spec" | commitmentSpecCodec) :: - ("txid" | txId) :: - ("remotePerCommitmentPoint" | publicKey)).as[RemoteCommit] - - val updateMessageCodec: Codec[UpdateMessage] = lengthDelimited(lightningMessageCodec.narrow[UpdateMessage](f => Attempt.successful(f.asInstanceOf[UpdateMessage]), g => g)) - - val localChangesCodec: Codec[LocalChanges] = ( - ("proposed" | listOfN(uint16, updateMessageCodec)) :: - ("signed" | listOfN(uint16, updateMessageCodec)) :: - ("acked" | listOfN(uint16, updateMessageCodec))).as[LocalChanges] - - val remoteChangesCodec: Codec[RemoteChanges] = ( - ("proposed" | listOfN(uint16, updateMessageCodec)) :: - ("acked" | listOfN(uint16, updateMessageCodec)) :: - ("signed" | listOfN(uint16, updateMessageCodec))).as[RemoteChanges] - - val waitingForRevocationCodec: Codec[ChannelTypes0.WaitingForRevocation] = ( - ("nextRemoteCommit" | remoteCommitCodec) :: - ("sent" | lengthDelimited(commitSigCodec)) :: - ("sentAfterLocalCommitIndex" | uint64overflow) :: - ("reSignAsap" | ignore(8))).as[ChannelTypes0.WaitingForRevocation] - - val upstreamLocalCodec: Codec[Upstream.Local] = ("id" | uuid).as[Upstream.Local] - - val upstreamChannelCodec: Codec[Upstream.Cold.Channel] = ( - ("originChannelId" | bytes32) :: - ("originHtlcId" | int64) :: - ("amountIn" | millisatoshi) :: - ("amountOut" | ignore(64))).as[Upstream.Cold.Channel] - - val upstreamChannelWithoutAmountCodec: Codec[Upstream.Cold.Channel] = ( - ("originChannelId" | bytes32) :: - ("originHtlcId" | int64) :: - ("amountIn" | provide(0 msat))).as[Upstream.Cold.Channel] - - val upstreamTrampolineCodec: Codec[Upstream.Cold.Trampoline] = listOfN(uint16, upstreamChannelWithoutAmountCodec).as[Upstream.Cold.Trampoline] - - val coldUpstreamCodec: Codec[Upstream.Cold] = discriminated[Upstream.Cold].by(uint16) - .typecase(0x02, upstreamChannelCodec) - .typecase(0x03, upstreamLocalCodec) - .typecase(0x04, upstreamTrampolineCodec) - - val originCodec: Codec[Origin] = coldUpstreamCodec.xmap[Origin]( - upstream => Origin.Cold(upstream), - { - case Origin.Hot(_, upstream) => Upstream.Cold(upstream) - case Origin.Cold(upstream) => upstream - } - ) - - def mapCodec[K, V](keyCodec: Codec[K], valueCodec: Codec[V]): Codec[Map[K, V]] = listOfN(uint16, keyCodec ~ valueCodec).xmap(_.toMap, _.toList) - - val originsMapCodec: Codec[Map[Long, Origin]] = mapCodec(int64, originCodec) - - val spentMapCodec: Codec[Map[OutPoint, TxId]] = mapCodec(outPointCodec, txId) - - val commitmentsCodec: Codec[Commitments] = ( - ("channelVersion" | channelVersionCodec) >>:~ { channelVersion => - ("localParams" | localParamsCodec(channelVersion)) :: - ("remoteParams" | remoteParamsCodec) :: - ("channelFlags" | channelflags) :: - ("localCommit" | localCommitCodec) :: - ("remoteCommit" | remoteCommitCodec) :: - ("localChanges" | localChangesCodec) :: - ("remoteChanges" | remoteChangesCodec) :: - ("localNextHtlcId" | uint64overflow) :: - ("remoteNextHtlcId" | uint64overflow) :: - ("originChannels" | originsMapCodec) :: - ("remoteNextCommitInfo" | either(bool8, waitingForRevocationCodec, publicKey)) :: - ("commitInput" | inputInfoCodec) :: - ("remotePerCommitmentSecrets" | byteAligned(ShaChain.shaChainCodec)) :: - ("channelId" | bytes32) - }).as[ChannelTypes0.Commitments].decodeOnly.map[Commitments](_.migrate()).decodeOnly - - val closingTxProposedCodec: Codec[ClosingTxProposed] = ( - ("unsignedTx" | closingTxCodec) :: - ("localClosingSigned" | lengthDelimited(closingSignedCodec))).as[ClosingTxProposed] - - val localCommitPublishedCodec: Codec[LocalCommitPublished] = ( - ("commitTx" | txCodec) :: - ("claimMainDelayedOutputTx" | optional(bool8, txCodec)) :: - ("htlcSuccessTxs" | listOfN(uint16, txCodec)) :: - ("htlcTimeoutTxs" | listOfN(uint16, txCodec)) :: - ("claimHtlcDelayedTx" | listOfN(uint16, txCodec)) :: - ("spent" | spentMapCodec)).as[ChannelTypes0.LocalCommitPublished].decodeOnly.map[LocalCommitPublished](_.migrate()).decodeOnly - - val remoteCommitPublishedCodec: Codec[RemoteCommitPublished] = ( - ("commitTx" | txCodec) :: - ("claimMainOutputTx" | optional(bool8, txCodec)) :: - ("claimHtlcSuccessTxs" | listOfN(uint16, txCodec)) :: - ("claimHtlcTimeoutTxs" | listOfN(uint16, txCodec)) :: - ("spent" | spentMapCodec)).as[ChannelTypes0.RemoteCommitPublished].decodeOnly.map[RemoteCommitPublished](_.migrate()).decodeOnly - - val revokedCommitPublishedCodec: Codec[RevokedCommitPublished] = ( - ("commitTx" | txCodec) :: - ("claimMainOutputTx" | optional(bool8, txCodec)) :: - ("mainPenaltyTx" | optional(bool8, txCodec)) :: - ("htlcPenaltyTxs" | listOfN(uint16, txCodec)) :: - ("claimHtlcDelayedPenaltyTxs" | listOfN(uint16, txCodec)) :: - ("spent" | spentMapCodec)).as[ChannelTypes0.RevokedCommitPublished].decodeOnly.map[RevokedCommitPublished](_.migrate()).decodeOnly - - val DATA_WAIT_FOR_FUNDING_CONFIRMED_20_Codec: Codec[DATA_WAIT_FOR_FUNDING_CONFIRMED] = ( - ("commitments" | commitmentsCodec) :: - ("fundingTx_opt" | optional(bool8, txCodec)) :: - ("waitingSince" | int64.as[BlockHeight]) :: - ("deferred" | optional(bool8, lengthDelimited(channelReadyCodec))) :: - ("lastSent" | either(bool8, lengthDelimited(fundingCreatedCodec), lengthDelimited(fundingSignedCodec)))).map { - case commitments :: fundingTx :: waitingSince :: deferred :: lastSent :: HNil => - val commitments1 = ChannelTypes0.setFundingStatus(commitments, SingleFundedUnconfirmedFundingTx(fundingTx)) - DATA_WAIT_FOR_FUNDING_CONFIRMED(commitments1, waitingSince, deferred, lastSent) - }.decodeOnly - - val DATA_WAIT_FOR_CHANNEL_READY_21_Codec: Codec[DATA_WAIT_FOR_CHANNEL_READY] = ( - ("commitments" | commitmentsCodec) :: - ("shortChannelId" | realshortchannelid) :: - ("lastSent" | lengthDelimited(channelReadyCodec))).map { - case commitments :: shortChannelId :: _ :: HNil => - DATA_WAIT_FOR_CHANNEL_READY(commitments, aliases = ShortIdAliases(localAlias = Alias(shortChannelId.toLong), remoteAlias_opt = None)) - }.decodeOnly - - val DATA_NORMAL_22_Codec: Codec[DATA_NORMAL] = ( - ("commitments" | commitmentsCodec) :: - ("shortChannelId" | realshortchannelid) :: - ("buried" | bool8) :: - ("channelAnnouncement" | optional(bool8, lengthDelimited(channelAnnouncementCodec))) :: - ("channelUpdate" | lengthDelimited(channelUpdateCodec)) :: - ("localShutdown" | optional(bool8, lengthDelimited(shutdownCodec))) :: - ("remoteShutdown" | optional(bool8, lengthDelimited(shutdownCodec))) :: - ("closeStatus" | provide(Option.empty[CloseStatus]))).map { - case commitments :: shortChannelId :: _ :: channelAnnouncement :: channelUpdate :: localShutdown :: remoteShutdown :: closeStatus :: HNil => - val aliases = ShortIdAliases(localAlias = Alias(shortChannelId.toLong), remoteAlias_opt = None) - DATA_NORMAL(commitments, aliases, channelAnnouncement, channelUpdate, localShutdown, remoteShutdown, closeStatus, SpliceStatus.NoSplice) - }.decodeOnly - - val DATA_SHUTDOWN_23_Codec: Codec[DATA_SHUTDOWN] = ( - ("commitments" | commitmentsCodec) :: - ("localShutdown" | lengthDelimited(shutdownCodec)) :: - ("remoteShutdown" | lengthDelimited(shutdownCodec)) :: - ("closeStatus" | provide[CloseStatus](CloseStatus.Initiator(None)))).as[DATA_SHUTDOWN] - - val DATA_NEGOTIATING_24_Codec: Codec[DATA_NEGOTIATING] = ( - ("commitments" | commitmentsCodec) :: - ("localShutdown" | lengthDelimited(shutdownCodec)) :: - ("remoteShutdown" | lengthDelimited(shutdownCodec)) :: - ("closingTxProposed" | listOfN(uint16, listOfN(uint16, lengthDelimited(closingTxProposedCodec)))) :: - ("bestUnpublishedClosingTx_opt" | optional(bool8, closingTxCodec))).as[DATA_NEGOTIATING] - - val DATA_CLOSING_25_Codec: Codec[DATA_CLOSING] = ( - ("commitments" | commitmentsCodec) :: - ("fundingTx_opt" | optional(bool8, txCodec)) :: - ("waitingSince" | int64.as[BlockHeight]) :: - ("mutualCloseProposed" | listOfN(uint16, closingTxCodec)) :: - ("mutualClosePublished" | listOfN(uint16, closingTxCodec)) :: - ("localCommitPublished" | optional(bool8, localCommitPublishedCodec)) :: - ("remoteCommitPublished" | optional(bool8, remoteCommitPublishedCodec)) :: - ("nextRemoteCommitPublished" | optional(bool8, remoteCommitPublishedCodec)) :: - ("futureRemoteCommitPublished" | optional(bool8, remoteCommitPublishedCodec)) :: - ("revokedCommitPublished" | listOfN(uint16, revokedCommitPublishedCodec))).map { - case commitments :: fundingTx_opt :: waitingSince :: mutualCloseProposed :: mutualClosePublished :: localCommitPublished :: remoteCommitPublished :: nextRemoteCommitPublished :: futureRemoteCommitPublished :: revokedCommitPublished :: HNil => - val commitments1 = ChannelTypes0.setFundingStatus(commitments, SingleFundedUnconfirmedFundingTx(fundingTx_opt)) - DATA_CLOSING(commitments1, waitingSince, commitments1.params.localParams.upfrontShutdownScript_opt.get, mutualCloseProposed, mutualClosePublished, localCommitPublished, remoteCommitPublished, nextRemoteCommitPublished, futureRemoteCommitPublished, revokedCommitPublished) - }.decodeOnly - - val DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT_26_Codec: Codec[DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT] = ( - ("commitments" | commitmentsCodec) :: - ("remoteChannelReestablish" | channelReestablishCodec)).as[DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT] - } - - // Order matters! - val channelDataCodec: Codec[PersistentChannelData] = discriminated[PersistentChannelData].by(uint16) - .typecase(0x20, Codecs.DATA_WAIT_FOR_FUNDING_CONFIRMED_20_Codec) - .typecase(0x21, Codecs.DATA_WAIT_FOR_CHANNEL_READY_21_Codec) - .typecase(0x22, Codecs.DATA_NORMAL_22_Codec) - .typecase(0x23, Codecs.DATA_SHUTDOWN_23_Codec) - .typecase(0x24, Codecs.DATA_NEGOTIATING_24_Codec) - .typecase(0x25, Codecs.DATA_CLOSING_25_Codec) - .typecase(0x26, Codecs.DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT_26_Codec) -} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version2/ChannelCodecs2.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version2/ChannelCodecs2.scala deleted file mode 100644 index 93d405f7cf..0000000000 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version2/ChannelCodecs2.scala +++ /dev/null @@ -1,340 +0,0 @@ -/* - * Copyright 2021 ACINQ SAS - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package fr.acinq.eclair.wire.internal.channel.version2 - -import fr.acinq.bitcoin.scalacompat.DeterministicWallet.{ExtendedPrivateKey, KeyPath} -import fr.acinq.bitcoin.scalacompat.{OutPoint, Transaction, TxOut} -import fr.acinq.eclair.blockchain.fee.ConfirmationTarget -import fr.acinq.eclair.channel.LocalFundingStatus.SingleFundedUnconfirmedFundingTx -import fr.acinq.eclair.channel._ -import fr.acinq.eclair.crypto.ShaChain -import fr.acinq.eclair.transactions.Transactions._ -import fr.acinq.eclair.transactions.{CommitmentSpec, DirectedHtlc, IncomingHtlc, OutgoingHtlc} -import fr.acinq.eclair.wire.internal.channel.version0.ChannelTypes0 -import fr.acinq.eclair.wire.internal.channel.version0.ChannelTypes0.{HtlcTxAndSigs, PublishableTxs} -import fr.acinq.eclair.wire.protocol.CommonCodecs._ -import fr.acinq.eclair.wire.protocol.LightningMessageCodecs._ -import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{Alias, BlockHeight, MilliSatoshiLong} -import scodec.bits.ByteVector -import scodec.codecs._ -import scodec.{Attempt, Codec} -import shapeless.{::, HNil} - -private[channel] object ChannelCodecs2 { - - private[version2] object Codecs { - - val keyPathCodec: Codec[KeyPath] = ("path" | listOfN(uint16, uint32)).xmap[KeyPath](l => KeyPath(l), keyPath => keyPath.path.toList).as[KeyPath] - - val extendedPrivateKeyCodec: Codec[ExtendedPrivateKey] = ( - ("secretkeybytes" | bytes32) :: - ("chaincode" | bytes32) :: - ("depth" | uint16) :: - ("path" | keyPathCodec) :: - ("parent" | int64)) - .map { case a :: b :: c :: d :: e :: HNil => ExtendedPrivateKey(a, b, c, d, e) } - .decodeOnly - - val channelVersionCodec: Codec[ChannelTypes0.ChannelVersion] = bits(ChannelTypes0.ChannelVersion.LENGTH_BITS).as[ChannelTypes0.ChannelVersion] - - def localParamsCodec(channelVersion: ChannelTypes0.ChannelVersion): Codec[LocalParams] = ( - ("nodeId" | publicKey) :: - ("channelPath" | keyPathCodec) :: - ("dustLimit" | satoshi) :: - ("maxHtlcValueInFlightMsat" | millisatoshi) :: - ("channelReserve" | conditional(included = true, satoshi)) :: - ("htlcMinimum" | millisatoshi) :: - ("toSelfDelay" | cltvExpiryDelta) :: - ("maxAcceptedHtlcs" | uint16) :: - ("isChannelOpener" | bool) :: ("paysCommitTxFees" | bool) :: ignore(6) :: - ("upfrontShutdownScript_opt" | lengthDelimited(bytes).map(Option(_)).decodeOnly) :: - ("walletStaticPaymentBasepoint" | optional(provide(channelVersion.paysDirectlyToWallet), publicKey)) :: - ("features" | combinedFeaturesCodec)).as[LocalParams] - - val remoteParamsCodec: Codec[ChannelTypes0.RemoteParams] = ( - ("nodeId" | publicKey) :: - ("dustLimit" | satoshi) :: - ("maxHtlcValueInFlightMsat" | uint64) :: - ("channelReserve" | conditional(included = true, satoshi)) :: - ("htlcMinimum" | millisatoshi) :: - ("toSelfDelay" | cltvExpiryDelta) :: - ("maxAcceptedHtlcs" | uint16) :: - ("fundingPubKey" | publicKey) :: - ("revocationBasepoint" | publicKey) :: - ("paymentBasepoint" | publicKey) :: - ("delayedPaymentBasepoint" | publicKey) :: - ("htlcBasepoint" | publicKey) :: - ("features" | combinedFeaturesCodec) :: - ("shutdownScript" | provide[Option[ByteVector]](None))).as[ChannelTypes0.RemoteParams] - - def setCodec[T](codec: Codec[T]): Codec[Set[T]] = listOfN(uint16, codec).xmap(_.toSet, _.toList) - - val htlcCodec: Codec[DirectedHtlc] = discriminated[DirectedHtlc].by(bool8) - .typecase(true, lengthDelimited(updateAddHtlcCodec).as[IncomingHtlc]) - .typecase(false, lengthDelimited(updateAddHtlcCodec).as[OutgoingHtlc]) - - val commitmentSpecCodec: Codec[CommitmentSpec] = ( - ("htlcs" | setCodec(htlcCodec)) :: - ("feeratePerKw" | feeratePerKw) :: - ("toLocal" | millisatoshi) :: - ("toRemote" | millisatoshi)).as[CommitmentSpec] - - val outPointCodec: Codec[OutPoint] = lengthDelimited(bytes.xmap(d => OutPoint.read(d.toArray), d => OutPoint.write(d))) - - val txOutCodec: Codec[TxOut] = lengthDelimited(bytes.xmap(d => TxOut.read(d.toArray), d => TxOut.write(d))) - - val txCodec: Codec[Transaction] = lengthDelimited(bytes.xmap(d => Transaction.read(d.toArray), d => Transaction.write(d))) - - val inputInfoCodec: Codec[InputInfo] = ( - ("outPoint" | outPointCodec) :: - ("txOut" | txOutCodec) :: - ("redeemScript" | lengthDelimited(bytes))).as[InputInfo.SegwitInput].upcast[InputInfo].decodeOnly - - val outputInfoCodec: Codec[OutputInfo] = ( - ("index" | uint32) :: - ("amount" | satoshi) :: - ("scriptPubKey" | lengthDelimited(bytes))).as[OutputInfo] - - private val defaultConfirmationTarget: Codec[ConfirmationTarget.Absolute] = provide(ConfirmationTarget.Absolute(BlockHeight(0))) - - val commitTxCodec: Codec[CommitTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[CommitTx] - val htlcSuccessTxCodec: Codec[HtlcSuccessTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec) :: ("paymentHash" | bytes32) :: ("htlcId" | uint64overflow) :: ("confirmationTarget" | defaultConfirmationTarget)).as[HtlcSuccessTx] - val htlcTimeoutTxCodec: Codec[HtlcTimeoutTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec) :: ("htlcId" | uint64overflow) :: ("confirmationTarget" | defaultConfirmationTarget)).as[HtlcTimeoutTx] - val htlcDelayedTxCodec: Codec[HtlcDelayedTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[HtlcDelayedTx] - val claimHtlcSuccessTxCodec: Codec[LegacyClaimHtlcSuccessTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec) :: ("htlcId" | uint64overflow) :: ("confirmationTarget" | defaultConfirmationTarget)).as[LegacyClaimHtlcSuccessTx] - val claimHtlcTimeoutTxCodec: Codec[ClaimHtlcTimeoutTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec) :: ("htlcId" | uint64overflow) :: ("confirmationTarget" | defaultConfirmationTarget)).as[ClaimHtlcTimeoutTx] - val claimLocalDelayedOutputTxCodec: Codec[ClaimLocalDelayedOutputTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[ClaimLocalDelayedOutputTx] - val claimP2WPKHOutputTxCodec: Codec[ClaimP2WPKHOutputTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[ClaimP2WPKHOutputTx] - val claimRemoteDelayedOutputTxCodec: Codec[ClaimRemoteDelayedOutputTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[ClaimRemoteDelayedOutputTx] - val mainPenaltyTxCodec: Codec[MainPenaltyTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[MainPenaltyTx] - val htlcPenaltyTxCodec: Codec[HtlcPenaltyTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[HtlcPenaltyTx] - val claimHtlcDelayedOutputPenaltyTxCodec: Codec[ClaimHtlcDelayedOutputPenaltyTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[ClaimHtlcDelayedOutputPenaltyTx] - val claimLocalAnchorOutputTxCodec: Codec[ClaimLocalAnchorOutputTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec) :: ("confirmationTarget" | defaultConfirmationTarget.upcast[ConfirmationTarget])).as[ClaimLocalAnchorOutputTx] - val claimRemoteAnchorOutputTxCodec: Codec[ClaimRemoteAnchorOutputTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[ClaimRemoteAnchorOutputTx] - val closingTxCodec: Codec[ClosingTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec) :: ("outputIndex" | optional(bool8, outputInfoCodec))).as[ClosingTx] - - val claimRemoteCommitMainOutputTxCodec: Codec[ClaimRemoteCommitMainOutputTx] = discriminated[ClaimRemoteCommitMainOutputTx].by(uint8) - .typecase(0x01, claimP2WPKHOutputTxCodec) - .typecase(0x02, claimRemoteDelayedOutputTxCodec) - - val claimAnchorOutputTxCodec: Codec[ClaimAnchorOutputTx] = discriminated[ClaimAnchorOutputTx].by(uint8) - .typecase(0x01, claimLocalAnchorOutputTxCodec) - .typecase(0x02, claimRemoteAnchorOutputTxCodec) - - val htlcTxCodec: Codec[HtlcTx] = discriminated[HtlcTx].by(uint8) - .typecase(0x01, htlcSuccessTxCodec) - .typecase(0x02, htlcTimeoutTxCodec) - - val claimHtlcTxCodec: Codec[ClaimHtlcTx] = discriminated[ClaimHtlcTx].by(uint8) - .typecase(0x01, claimHtlcSuccessTxCodec) - .typecase(0x02, claimHtlcTimeoutTxCodec) - - val htlcTxAndSigsCodec: Codec[HtlcTxAndSigs] = ( - ("txinfo" | htlcTxCodec) :: - ("localSig" | lengthDelimited(bytes64)) :: // we store as variable length for historical purposes (we used to store as DER encoded) - ("remoteSig" | lengthDelimited(bytes64))).as[HtlcTxAndSigs] - - val publishableTxsCodec: Codec[PublishableTxs] = ( - ("commitTx" | (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[CommitTx]) :: - ("htlcTxsAndSigs" | listOfN(uint16, htlcTxAndSigsCodec))).as[PublishableTxs] - - val localCommitCodec: Codec[ChannelTypes0.LocalCommit] = ( - ("index" | uint64overflow) :: - ("spec" | commitmentSpecCodec) :: - ("publishableTxs" | publishableTxsCodec)).as[ChannelTypes0.LocalCommit].decodeOnly - - val remoteCommitCodec: Codec[RemoteCommit] = ( - ("index" | uint64overflow) :: - ("spec" | commitmentSpecCodec) :: - ("txid" | txId) :: - ("remotePerCommitmentPoint" | publicKey)).as[RemoteCommit] - - val updateMessageCodec: Codec[UpdateMessage] = lengthDelimited(lightningMessageCodec.narrow[UpdateMessage](f => Attempt.successful(f.asInstanceOf[UpdateMessage]), g => g)) - - val localChangesCodec: Codec[LocalChanges] = ( - ("proposed" | listOfN(uint16, updateMessageCodec)) :: - ("signed" | listOfN(uint16, updateMessageCodec)) :: - ("acked" | listOfN(uint16, updateMessageCodec))).as[LocalChanges] - - val remoteChangesCodec: Codec[RemoteChanges] = ( - ("proposed" | listOfN(uint16, updateMessageCodec)) :: - ("acked" | listOfN(uint16, updateMessageCodec)) :: - ("signed" | listOfN(uint16, updateMessageCodec))).as[RemoteChanges] - - val waitingForRevocationCodec: Codec[ChannelTypes0.WaitingForRevocation] = ( - ("nextRemoteCommit" | remoteCommitCodec) :: - ("sent" | lengthDelimited(commitSigCodec)) :: - ("sentAfterLocalCommitIndex" | uint64overflow) :: - ("reSignAsap" | ignore(8))).as[ChannelTypes0.WaitingForRevocation] - - val upstreamLocalCodec: Codec[Upstream.Local] = ("id" | uuid).as[Upstream.Local] - - val upstreamChannelCodec: Codec[Upstream.Cold.Channel] = ( - ("originChannelId" | bytes32) :: - ("originHtlcId" | int64) :: - ("amountIn" | millisatoshi) :: - ("amountOut" | ignore(64))).as[Upstream.Cold.Channel] - - val upstreamChannelWithoutAmountCodec: Codec[Upstream.Cold.Channel] = ( - ("originChannelId" | bytes32) :: - ("originHtlcId" | int64) :: - ("amountIn" | provide(0 msat))).as[Upstream.Cold.Channel] - - val upstreamTrampolineCodec: Codec[Upstream.Cold.Trampoline] = listOfN(uint16, upstreamChannelWithoutAmountCodec).as[Upstream.Cold.Trampoline] - - val coldUpstreamCodec: Codec[Upstream.Cold] = discriminated[Upstream.Cold].by(uint16) - .typecase(0x02, upstreamChannelCodec) - .typecase(0x03, upstreamLocalCodec) - .typecase(0x04, upstreamTrampolineCodec) - - val originCodec: Codec[Origin] = coldUpstreamCodec.xmap[Origin]( - upstream => Origin.Cold(upstream), - { - case Origin.Hot(_, upstream) => Upstream.Cold(upstream) - case Origin.Cold(upstream) => upstream - } - ) - - def mapCodec[K, V](keyCodec: Codec[K], valueCodec: Codec[V]): Codec[Map[K, V]] = listOfN(uint16, keyCodec ~ valueCodec).xmap(_.toMap, _.toList) - - val originsMapCodec: Codec[Map[Long, Origin]] = mapCodec(int64, originCodec) - - val spentMapCodec: Codec[Map[OutPoint, Transaction]] = mapCodec(outPointCodec, txCodec) - - val commitmentsCodec: Codec[Commitments] = ( - ("channelVersion" | channelVersionCodec) >>:~ { channelVersion => - ("localParams" | localParamsCodec(channelVersion)) :: - ("remoteParams" | remoteParamsCodec) :: - ("channelFlags" | channelflags) :: - ("localCommit" | localCommitCodec) :: - ("remoteCommit" | remoteCommitCodec) :: - ("localChanges" | localChangesCodec) :: - ("remoteChanges" | remoteChangesCodec) :: - ("localNextHtlcId" | uint64overflow) :: - ("remoteNextHtlcId" | uint64overflow) :: - ("originChannels" | originsMapCodec) :: - ("remoteNextCommitInfo" | either(bool8, waitingForRevocationCodec, publicKey)) :: - ("commitInput" | inputInfoCodec) :: - ("remotePerCommitmentSecrets" | byteAligned(ShaChain.shaChainCodec)) :: - ("channelId" | bytes32) - }).as[ChannelTypes0.Commitments].decodeOnly.map[Commitments](_.migrate()).decodeOnly - - val closingTxProposedCodec: Codec[ClosingTxProposed] = ( - ("unsignedTx" | closingTxCodec) :: - ("localClosingSigned" | lengthDelimited(closingSignedCodec))).as[ClosingTxProposed] - - val localCommitPublishedCodec: Codec[LocalCommitPublished] = ( - ("commitTx" | txCodec) :: - ("claimMainDelayedOutputTx" | optional(bool8, claimLocalDelayedOutputTxCodec)) :: - ("htlcTxs" | mapCodec(outPointCodec, optional(bool8, htlcTxCodec))) :: - ("claimHtlcDelayedTx" | listOfN(uint16, htlcDelayedTxCodec)) :: - ("claimAnchorTxs" | listOfN(uint16, claimAnchorOutputTxCodec)) :: - ("spent" | spentMapCodec)).as[LocalCommitPublished] - - val remoteCommitPublishedCodec: Codec[RemoteCommitPublished] = ( - ("commitTx" | txCodec) :: - ("claimMainOutputTx" | optional(bool8, claimRemoteCommitMainOutputTxCodec)) :: - ("claimHtlcTxs" | mapCodec(outPointCodec, optional(bool8, claimHtlcTxCodec))) :: - ("claimAnchorTxs" | listOfN(uint16, claimAnchorOutputTxCodec)) :: - ("spent" | spentMapCodec)).as[RemoteCommitPublished] - - val revokedCommitPublishedCodec: Codec[RevokedCommitPublished] = ( - ("commitTx" | txCodec) :: - ("claimMainOutputTx" | optional(bool8, claimRemoteCommitMainOutputTxCodec)) :: - ("mainPenaltyTx" | optional(bool8, mainPenaltyTxCodec)) :: - ("htlcPenaltyTxs" | listOfN(uint16, htlcPenaltyTxCodec)) :: - ("claimHtlcDelayedPenaltyTxs" | listOfN(uint16, claimHtlcDelayedOutputPenaltyTxCodec)) :: - ("spent" | spentMapCodec)).as[RevokedCommitPublished] - - val DATA_WAIT_FOR_FUNDING_CONFIRMED_00_Codec: Codec[DATA_WAIT_FOR_FUNDING_CONFIRMED] = ( - ("commitments" | commitmentsCodec) :: - ("fundingTx_opt" | optional(bool8, txCodec)) :: - ("waitingSince" | int64.as[BlockHeight]) :: - ("deferred" | optional(bool8, lengthDelimited(channelReadyCodec))) :: - ("lastSent" | either(bool8, lengthDelimited(fundingCreatedCodec), lengthDelimited(fundingSignedCodec)))).map { - case commitments :: fundingTx :: waitingSince :: deferred :: lastSent :: HNil => - val commitments1 = ChannelTypes0.setFundingStatus(commitments, SingleFundedUnconfirmedFundingTx(fundingTx)) - DATA_WAIT_FOR_FUNDING_CONFIRMED(commitments1, waitingSince, deferred, lastSent) - }.decodeOnly - - val DATA_WAIT_FOR_CHANNEL_READY_01_Codec: Codec[DATA_WAIT_FOR_CHANNEL_READY] = ( - ("commitments" | commitmentsCodec) :: - ("shortChannelId" | realshortchannelid) :: - ("lastSent" | lengthDelimited(channelReadyCodec))).map { - case commitments :: shortChannelId :: _ :: HNil => - DATA_WAIT_FOR_CHANNEL_READY(commitments, aliases = ShortIdAliases(localAlias = Alias(shortChannelId.toLong), remoteAlias_opt = None)) - }.decodeOnly - - val DATA_NORMAL_02_Codec: Codec[DATA_NORMAL] = ( - ("commitments" | commitmentsCodec) :: - ("shortChannelId" | realshortchannelid) :: - ("buried" | bool8) :: - ("channelAnnouncement" | optional(bool8, lengthDelimited(channelAnnouncementCodec))) :: - ("channelUpdate" | lengthDelimited(channelUpdateCodec)) :: - ("localShutdown" | optional(bool8, lengthDelimited(shutdownCodec))) :: - ("remoteShutdown" | optional(bool8, lengthDelimited(shutdownCodec))) :: - ("closeStatus" | provide(Option.empty[CloseStatus]))).map { - case commitments :: shortChannelId :: _ :: channelAnnouncement :: channelUpdate :: localShutdown :: remoteShutdown :: closeStatus :: HNil => - val aliases = ShortIdAliases(localAlias = Alias(shortChannelId.toLong), remoteAlias_opt = None) - DATA_NORMAL(commitments, aliases, channelAnnouncement, channelUpdate, localShutdown, remoteShutdown, closeStatus, SpliceStatus.NoSplice) - }.decodeOnly - - val DATA_SHUTDOWN_03_Codec: Codec[DATA_SHUTDOWN] = ( - ("commitments" | commitmentsCodec) :: - ("localShutdown" | lengthDelimited(shutdownCodec)) :: - ("remoteShutdown" | lengthDelimited(shutdownCodec)) :: - ("closeStatus" | provide[CloseStatus](CloseStatus.Initiator(None)))).as[DATA_SHUTDOWN] - - val DATA_NEGOTIATING_04_Codec: Codec[DATA_NEGOTIATING] = ( - ("commitments" | commitmentsCodec) :: - ("localShutdown" | lengthDelimited(shutdownCodec)) :: - ("remoteShutdown" | lengthDelimited(shutdownCodec)) :: - ("closingTxProposed" | listOfN(uint16, listOfN(uint16, lengthDelimited(closingTxProposedCodec)))) :: - ("bestUnpublishedClosingTx_opt" | optional(bool8, closingTxCodec))).as[DATA_NEGOTIATING] - - val DATA_CLOSING_05_Codec: Codec[DATA_CLOSING] = ( - ("commitments" | commitmentsCodec) :: - ("fundingTx_opt" | optional(bool8, txCodec)) :: - ("waitingSince" | int64.as[BlockHeight]) :: - ("mutualCloseProposed" | listOfN(uint16, closingTxCodec)) :: - ("mutualClosePublished" | listOfN(uint16, closingTxCodec)) :: - ("localCommitPublished" | optional(bool8, localCommitPublishedCodec)) :: - ("remoteCommitPublished" | optional(bool8, remoteCommitPublishedCodec)) :: - ("nextRemoteCommitPublished" | optional(bool8, remoteCommitPublishedCodec)) :: - ("futureRemoteCommitPublished" | optional(bool8, remoteCommitPublishedCodec)) :: - ("revokedCommitPublished" | listOfN(uint16, revokedCommitPublishedCodec))).map { - case commitments :: fundingTx_opt :: waitingSince :: mutualCloseProposed :: mutualClosePublished :: localCommitPublished :: remoteCommitPublished :: nextRemoteCommitPublished :: futureRemoteCommitPublished :: revokedCommitPublished :: HNil => - val commitments1 = ChannelTypes0.setFundingStatus(commitments, SingleFundedUnconfirmedFundingTx(fundingTx_opt)) - DATA_CLOSING(commitments1, waitingSince, commitments1.params.localParams.upfrontShutdownScript_opt.get, mutualCloseProposed, mutualClosePublished, localCommitPublished, remoteCommitPublished, nextRemoteCommitPublished, futureRemoteCommitPublished, revokedCommitPublished) - }.decodeOnly - - val DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT_06_Codec: Codec[DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT] = ( - ("commitments" | commitmentsCodec) :: - ("remoteChannelReestablish" | channelReestablishCodec)).as[DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT] - } - - val channelDataCodec: Codec[PersistentChannelData] = discriminated[PersistentChannelData].by(uint16) - .typecase(0x00, Codecs.DATA_WAIT_FOR_FUNDING_CONFIRMED_00_Codec) - .typecase(0x01, Codecs.DATA_WAIT_FOR_CHANNEL_READY_01_Codec) - .typecase(0x02, Codecs.DATA_NORMAL_02_Codec) - .typecase(0x03, Codecs.DATA_SHUTDOWN_03_Codec) - .typecase(0x04, Codecs.DATA_NEGOTIATING_04_Codec) - .typecase(0x05, Codecs.DATA_CLOSING_05_Codec) - .typecase(0x06, Codecs.DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT_06_Codec) - -} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3.scala deleted file mode 100644 index e9aa69650e..0000000000 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3.scala +++ /dev/null @@ -1,490 +0,0 @@ -/* - * Copyright 2021 ACINQ SAS - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package fr.acinq.eclair.wire.internal.channel.version3 - -import com.softwaremill.quicklens.{ModifyPimp, QuicklensAt} -import fr.acinq.bitcoin.scalacompat.DeterministicWallet.KeyPath -import fr.acinq.bitcoin.scalacompat.{OutPoint, Transaction, TxOut} -import fr.acinq.eclair.blockchain.fee.ConfirmationTarget -import fr.acinq.eclair.channel.LocalFundingStatus._ -import fr.acinq.eclair.channel._ -import fr.acinq.eclair.channel.fund.InteractiveTxBuilder._ -import fr.acinq.eclair.crypto.ShaChain -import fr.acinq.eclair.transactions.Transactions._ -import fr.acinq.eclair.transactions.{CommitmentSpec, DirectedHtlc, IncomingHtlc, OutgoingHtlc} -import fr.acinq.eclair.wire.internal.channel.version0.ChannelTypes0 -import fr.acinq.eclair.wire.protocol.CommonCodecs._ -import fr.acinq.eclair.wire.protocol.LightningMessageCodecs._ -import fr.acinq.eclair.wire.protocol.UpdateMessage -import fr.acinq.eclair.{Alias, BlockHeight, FeatureSupport, Features, MilliSatoshiLong, PermanentChannelFeature} -import scodec.bits.{BitVector, ByteVector} -import scodec.codecs._ -import scodec.{Attempt, Codec, Err} -import shapeless.{::, HNil} - -private[channel] object ChannelCodecs3 { - - private[version3] object Codecs { - - val keyPathCodec: Codec[KeyPath] = ("path" | listOfN(uint16, uint32)).xmap[KeyPath](l => KeyPath(l), keyPath => keyPath.path.toList).as[KeyPath] - - val channelConfigCodec: Codec[ChannelConfig] = lengthDelimited(bytes).xmap(b => { - val activated: Set[ChannelConfigOption] = b.bits.toIndexedSeq.reverse.zipWithIndex.collect { - case (true, 0) => ChannelConfig.FundingPubKeyBasedChannelKeyPath - }.toSet - ChannelConfig(activated) - }, cfg => { - val indices = cfg.options.map(_.supportBit) - if (indices.isEmpty) { - ByteVector.empty - } else { - // NB: when converting from BitVector to ByteVector, scodec pads right instead of left, so we make sure we pad to bytes *before* setting bits. - var buffer = BitVector.fill(indices.max + 1)(high = false).bytes.bits - indices.foreach(i => buffer = buffer.set(i)) - buffer.reverse.bytes - } - }) - - /** We use the same encoding as init features, even if we don't need the distinction between mandatory and optional */ - val channelFeaturesCodec: Codec[ChannelFeatures] = lengthDelimited(bytes).xmap( - (b: ByteVector) => ChannelFeatures(Features(b).activated.keySet.collect { case f: PermanentChannelFeature => f }), // we make no difference between mandatory/optional, both are considered activated - (cf: ChannelFeatures) => Features(cf.features.map(f => f -> FeatureSupport.Mandatory).toMap).toByteVector // we encode features as mandatory, by convention - ) - - def localParamsCodec(channelFeatures: ChannelFeatures): Codec[LocalParams] = ( - ("nodeId" | publicKey) :: - ("channelPath" | keyPathCodec) :: - ("dustLimit" | satoshi) :: - ("maxHtlcValueInFlightMsat" | millisatoshi) :: - ("channelReserve" | conditional(!channelFeatures.hasFeature(Features.DualFunding), satoshi)) :: - ("htlcMinimum" | millisatoshi) :: - ("toSelfDelay" | cltvExpiryDelta) :: - ("maxAcceptedHtlcs" | uint16) :: - ("isChannelOpener" | bool) :: ("paysCommitTxFees" | bool) :: ignore(6) :: - ("upfrontShutdownScript_opt" | lengthDelimited(bytes).map(Option(_)).decodeOnly) :: - ("walletStaticPaymentBasepoint" | optional(provide(channelFeatures.paysDirectlyToWallet), publicKey)) :: - ("features" | combinedFeaturesCodec)).as[LocalParams] - - def remoteParamsCodec(channelFeatures: ChannelFeatures): Codec[ChannelTypes0.RemoteParams] = ( - ("nodeId" | publicKey) :: - ("dustLimit" | satoshi) :: - ("maxHtlcValueInFlightMsat" | uint64) :: - ("channelReserve" | conditional(!channelFeatures.hasFeature(Features.DualFunding), satoshi)) :: - ("htlcMinimum" | millisatoshi) :: - ("toSelfDelay" | cltvExpiryDelta) :: - ("maxAcceptedHtlcs" | uint16) :: - ("fundingPubKey" | publicKey) :: - ("revocationBasepoint" | publicKey) :: - ("paymentBasepoint" | publicKey) :: - ("delayedPaymentBasepoint" | publicKey) :: - ("htlcBasepoint" | publicKey) :: - ("features" | combinedFeaturesCodec) :: - ("shutdownScript" | optional(bool8, lengthDelimited(bytes)))).as[ChannelTypes0.RemoteParams] - - def setCodec[T](codec: Codec[T]): Codec[Set[T]] = listOfN(uint16, codec).xmap(_.toSet, _.toList) - - val htlcCodec: Codec[DirectedHtlc] = discriminated[DirectedHtlc].by(bool8) - .typecase(true, lengthDelimited(updateAddHtlcCodec).as[IncomingHtlc]) - .typecase(false, lengthDelimited(updateAddHtlcCodec).as[OutgoingHtlc]) - - val commitmentSpecCodec: Codec[CommitmentSpec] = ( - ("htlcs" | setCodec(htlcCodec)) :: - ("feeratePerKw" | feeratePerKw) :: - ("toLocal" | millisatoshi) :: - ("toRemote" | millisatoshi)).as[CommitmentSpec] - - val outPointCodec: Codec[OutPoint] = lengthDelimited(bytes.xmap(d => OutPoint.read(d.toArray), d => OutPoint.write(d))) - - val txOutCodec: Codec[TxOut] = lengthDelimited(bytes.xmap(d => TxOut.read(d.toArray), d => TxOut.write(d))) - - val txCodec: Codec[Transaction] = lengthDelimited(bytes.xmap(d => Transaction.read(d.toArray), d => Transaction.write(d))) - - val inputInfoCodec: Codec[InputInfo] = ( - ("outPoint" | outPointCodec) :: - ("txOut" | txOutCodec) :: - ("redeemScript" | lengthDelimited(bytes))).as[InputInfo.SegwitInput].upcast[InputInfo].decodeOnly - - val outputInfoCodec: Codec[OutputInfo] = ( - ("index" | uint32) :: - ("amount" | satoshi) :: - ("scriptPubKey" | lengthDelimited(bytes))).as[OutputInfo] - - private val defaultConfirmationTarget: Codec[ConfirmationTarget.Absolute] = provide(ConfirmationTarget.Absolute(BlockHeight(0))) - private val blockHeightConfirmationTarget: Codec[ConfirmationTarget.Absolute] = blockHeight.map(ConfirmationTarget.Absolute).decodeOnly - - val commitTxCodec: Codec[CommitTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[CommitTx] - val htlcSuccessTxCodec: Codec[HtlcSuccessTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec) :: ("paymentHash" | bytes32) :: ("htlcId" | uint64overflow) :: ("confirmationTarget" | blockHeightConfirmationTarget)).as[HtlcSuccessTx] - val htlcTimeoutTxCodec: Codec[HtlcTimeoutTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec) :: ("htlcId" | uint64overflow) :: ("confirmationTarget" | blockHeightConfirmationTarget)).as[HtlcTimeoutTx] - private val htlcSuccessTxNoConfirmCodec: Codec[HtlcSuccessTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec) :: ("paymentHash" | bytes32) :: ("htlcId" | uint64overflow) :: ("confirmationTarget" | defaultConfirmationTarget)).as[HtlcSuccessTx] - private val htlcTimeoutTxNoConfirmCodec: Codec[HtlcTimeoutTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec) :: ("htlcId" | uint64overflow) :: ("confirmationTarget" | defaultConfirmationTarget)).as[HtlcTimeoutTx] - val htlcDelayedTxCodec: Codec[HtlcDelayedTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[HtlcDelayedTx] - private val legacyClaimHtlcSuccessTxCodec: Codec[LegacyClaimHtlcSuccessTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec) :: ("htlcId" | uint64overflow) :: ("confirmationTarget" | defaultConfirmationTarget)).as[LegacyClaimHtlcSuccessTx] - val claimHtlcSuccessTxCodec: Codec[ClaimHtlcSuccessTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec) :: ("paymentHash" | bytes32) :: ("htlcId" | uint64overflow) :: ("confirmationTarget" | blockHeightConfirmationTarget)).as[ClaimHtlcSuccessTx] - val claimHtlcTimeoutTxCodec: Codec[ClaimHtlcTimeoutTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec) :: ("htlcId" | uint64overflow) :: ("confirmationTarget" | blockHeightConfirmationTarget)).as[ClaimHtlcTimeoutTx] - private val claimHtlcSuccessTxNoConfirmCodec: Codec[ClaimHtlcSuccessTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec) :: ("paymentHash" | bytes32) :: ("htlcId" | uint64overflow) :: ("confirmationTarget" | defaultConfirmationTarget)).as[ClaimHtlcSuccessTx] - private val claimHtlcTimeoutTxNoConfirmCodec: Codec[ClaimHtlcTimeoutTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec) :: ("htlcId" | uint64overflow) :: ("confirmationTarget" | defaultConfirmationTarget)).as[ClaimHtlcTimeoutTx] - val claimLocalDelayedOutputTxCodec: Codec[ClaimLocalDelayedOutputTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[ClaimLocalDelayedOutputTx] - val claimP2WPKHOutputTxCodec: Codec[ClaimP2WPKHOutputTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[ClaimP2WPKHOutputTx] - val claimRemoteDelayedOutputTxCodec: Codec[ClaimRemoteDelayedOutputTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[ClaimRemoteDelayedOutputTx] - val mainPenaltyTxCodec: Codec[MainPenaltyTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[MainPenaltyTx] - val htlcPenaltyTxCodec: Codec[HtlcPenaltyTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[HtlcPenaltyTx] - val claimHtlcDelayedOutputPenaltyTxCodec: Codec[ClaimHtlcDelayedOutputPenaltyTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[ClaimHtlcDelayedOutputPenaltyTx] - val claimLocalAnchorOutputTxCodec: Codec[ClaimLocalAnchorOutputTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec) :: ("confirmationTarget" | blockHeightConfirmationTarget).upcast[ConfirmationTarget]).as[ClaimLocalAnchorOutputTx] - private val claimLocalAnchorOutputTxNoConfirmCodec: Codec[ClaimLocalAnchorOutputTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec) :: ("confirmationTarget" | defaultConfirmationTarget).upcast[ConfirmationTarget]).as[ClaimLocalAnchorOutputTx] - val claimRemoteAnchorOutputTxCodec: Codec[ClaimRemoteAnchorOutputTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[ClaimRemoteAnchorOutputTx] - val closingTxCodec: Codec[ClosingTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec) :: ("outputIndex" | optional(bool8, outputInfoCodec))).as[ClosingTx] - - val txWithInputInfoCodec: Codec[TransactionWithInputInfo] = discriminated[TransactionWithInputInfo].by(uint16) - // Important: order matters! - .typecase(0x20, claimLocalAnchorOutputTxCodec) - .typecase(0x21, htlcSuccessTxCodec) - .typecase(0x22, htlcTimeoutTxCodec) - .typecase(0x23, claimHtlcSuccessTxCodec) - .typecase(0x24, claimHtlcTimeoutTxCodec) - .typecase(0x01, commitTxCodec) - .typecase(0x02, htlcSuccessTxNoConfirmCodec) - .typecase(0x03, htlcTimeoutTxNoConfirmCodec) - .typecase(0x04, legacyClaimHtlcSuccessTxCodec) - .typecase(0x05, claimHtlcTimeoutTxNoConfirmCodec) - .typecase(0x06, claimP2WPKHOutputTxCodec) - .typecase(0x07, claimLocalDelayedOutputTxCodec) - .typecase(0x08, mainPenaltyTxCodec) - .typecase(0x09, htlcPenaltyTxCodec) - .typecase(0x10, closingTxCodec) - .typecase(0x11, claimLocalAnchorOutputTxNoConfirmCodec) - .typecase(0x12, claimRemoteAnchorOutputTxCodec) - .typecase(0x13, claimRemoteDelayedOutputTxCodec) - .typecase(0x14, claimHtlcDelayedOutputPenaltyTxCodec) - .typecase(0x15, htlcDelayedTxCodec) - .typecase(0x16, claimHtlcSuccessTxNoConfirmCodec) - - val claimRemoteCommitMainOutputTxCodec: Codec[ClaimRemoteCommitMainOutputTx] = discriminated[ClaimRemoteCommitMainOutputTx].by(uint8) - .typecase(0x01, claimP2WPKHOutputTxCodec) - .typecase(0x02, claimRemoteDelayedOutputTxCodec) - - val claimAnchorOutputTxCodec: Codec[ClaimAnchorOutputTx] = discriminated[ClaimAnchorOutputTx].by(uint8) - // Important: order matters! - .typecase(0x11, claimLocalAnchorOutputTxCodec) - .typecase(0x01, claimLocalAnchorOutputTxNoConfirmCodec) - .typecase(0x02, claimRemoteAnchorOutputTxCodec) - - val htlcTxCodec: Codec[HtlcTx] = discriminated[HtlcTx].by(uint8) - // Important: order matters! - .typecase(0x11, htlcSuccessTxCodec) - .typecase(0x12, htlcTimeoutTxCodec) - .typecase(0x01, htlcSuccessTxNoConfirmCodec) - .typecase(0x02, htlcTimeoutTxNoConfirmCodec) - - val claimHtlcTxCodec: Codec[ClaimHtlcTx] = discriminated[ClaimHtlcTx].by(uint8) - // Important: order matters! - .typecase(0x22, claimHtlcTimeoutTxCodec) - .typecase(0x23, claimHtlcSuccessTxCodec) - .typecase(0x01, legacyClaimHtlcSuccessTxCodec) - .typecase(0x02, claimHtlcTimeoutTxNoConfirmCodec) - .typecase(0x03, claimHtlcSuccessTxNoConfirmCodec) - - val htlcTxsAndRemoteSigsCodec: Codec[HtlcTxAndRemoteSig] = ( - ("txinfo" | htlcTxCodec) :: - ("remoteSig" | bytes64)).as[HtlcTxAndRemoteSig] - - val commitTxAndRemoteSigCodec: Codec[CommitTxAndRemoteSig] = ( - ("commitTx" | commitTxCodec) :: - ("remoteSig" | bytes64.as[RemoteSignature.FullSignature].upcast[RemoteSignature])).as[CommitTxAndRemoteSig] - - val localCommitCodec: Codec[LocalCommit] = ( - ("index" | uint64overflow) :: - ("spec" | commitmentSpecCodec) :: - ("commitTxAndRemoteSig" | commitTxAndRemoteSigCodec) :: - ("htlcTxsAndRemoteSigs" | listOfN(uint16, htlcTxsAndRemoteSigsCodec))).as[LocalCommit] - - val remoteCommitCodec: Codec[RemoteCommit] = ( - ("index" | uint64overflow) :: - ("spec" | commitmentSpecCodec) :: - ("txid" | txId) :: - ("remotePerCommitmentPoint" | publicKey)).as[RemoteCommit] - - val updateMessageCodec: Codec[UpdateMessage] = lengthDelimited(lightningMessageCodec.narrow[UpdateMessage](f => Attempt.successful(f.asInstanceOf[UpdateMessage]), g => g)) - - val localChangesCodec: Codec[LocalChanges] = ( - ("proposed" | listOfN(uint16, updateMessageCodec)) :: - ("signed" | listOfN(uint16, updateMessageCodec)) :: - ("acked" | listOfN(uint16, updateMessageCodec))).as[LocalChanges] - - val remoteChangesCodec: Codec[RemoteChanges] = ( - ("proposed" | listOfN(uint16, updateMessageCodec)) :: - ("acked" | listOfN(uint16, updateMessageCodec)) :: - ("signed" | listOfN(uint16, updateMessageCodec))).as[RemoteChanges] - - val waitingForRevocationCodec: Codec[ChannelTypes3.WaitingForRevocation] = ( - ("nextRemoteCommit" | remoteCommitCodec) :: - ("sent" | lengthDelimited(commitSigCodec)) :: - ("sentAfterLocalCommitIndex" | uint64overflow) :: - ("reSignAsap" | ignore(8))).as[ChannelTypes3.WaitingForRevocation] - - val upstreamLocalCodec: Codec[Upstream.Local] = ("id" | uuid).as[Upstream.Local] - - val upstreamChannelCodec: Codec[Upstream.Cold.Channel] = ( - ("originChannelId" | bytes32) :: - ("originHtlcId" | int64) :: - ("amountIn" | millisatoshi) :: - ("amountOut" | ignore(64))).as[Upstream.Cold.Channel] - - val upstreamChannelWithoutAmountCodec: Codec[Upstream.Cold.Channel] = ( - ("originChannelId" | bytes32) :: - ("originHtlcId" | int64) :: - ("amountIn" | provide(0 msat))).as[Upstream.Cold.Channel] - - val upstreamTrampolineCodec: Codec[Upstream.Cold.Trampoline] = listOfN(uint16, upstreamChannelWithoutAmountCodec).as[Upstream.Cold.Trampoline] - - val coldUpstreamCodec: Codec[Upstream.Cold] = discriminated[Upstream.Cold].by(uint16) - .typecase(0x02, upstreamChannelCodec) - .typecase(0x03, upstreamLocalCodec) - .typecase(0x04, upstreamTrampolineCodec) - - val originCodec: Codec[Origin] = coldUpstreamCodec.xmap[Origin]( - upstream => Origin.Cold(upstream), - { - case Origin.Hot(_, upstream) => Upstream.Cold(upstream) - case Origin.Cold(upstream) => upstream - } - ) - - def mapCodec[K, V](keyCodec: Codec[K], valueCodec: Codec[V]): Codec[Map[K, V]] = listOfN(uint16, keyCodec ~ valueCodec).xmap(_.toMap, _.toList) - - val originsMapCodec: Codec[Map[Long, Origin]] = mapCodec(int64, originCodec) - - val spentMapCodec: Codec[Map[OutPoint, Transaction]] = mapCodec(outPointCodec, txCodec) - - val commitmentsCodec: Codec[Commitments] = ( - ("channelId" | bytes32) :: - ("channelConfig" | channelConfigCodec) :: - (("channelFeatures" | channelFeaturesCodec) >>:~ { channelFeatures => - ("localParams" | localParamsCodec(channelFeatures)) :: - ("remoteParams" | remoteParamsCodec(channelFeatures)) :: - ("channelFlags" | channelflags) :: - ("localCommit" | localCommitCodec) :: - ("remoteCommit" | remoteCommitCodec) :: - ("localChanges" | localChangesCodec) :: - ("remoteChanges" | remoteChangesCodec) :: - ("localNextHtlcId" | uint64overflow) :: - ("remoteNextHtlcId" | uint64overflow) :: - ("originChannels" | originsMapCodec) :: - ("remoteNextCommitInfo" | either(bool8, waitingForRevocationCodec, publicKey)) :: - ("commitInput" | inputInfoCodec.map(_ => ()).decodeOnly) :: - ("fundingTxStatus" | provide(SingleFundedUnconfirmedFundingTx(None)).upcast[LocalFundingStatus]) :: - ("remoteFundingTxStatus" | provide(RemoteFundingStatus.Locked).upcast[RemoteFundingStatus]) :: - ("remotePerCommitmentSecrets" | byteAligned(ShaChain.shaChainCodec)) - })).as[ChannelTypes3.Commitments].decodeOnly.map[Commitments](_.migrate()).decodeOnly - - /** Once a dual funding tx has been signed, we must remember the associated commitments. */ - case class DualFundingTx(fundingTx: SignedSharedTransaction, commitments: ChannelTypes3.Commitments) - - private val dualFundingTxCodec: Codec[DualFundingTx] = fail[DualFundingTx](Err("you have been running dual funding before it was officially released! contact developers")) - - val closingFeeratesCodec: Codec[ClosingFeerates] = ( - ("preferred" | feeratePerKw) :: - ("min" | feeratePerKw) :: - ("max" | feeratePerKw)).as[ClosingFeerates] - - /** If there are closing fees defined we consider ourselves to be the closing initiator. */ - val closeStatusCompatCodec: Codec[Option[CloseStatus]] = optional(bool8, closingFeeratesCodec).map(feerates_opt => Some(CloseStatus.Initiator(feerates_opt))).decodeOnly - - val closingTxProposedCodec: Codec[ClosingTxProposed] = ( - ("unsignedTx" | closingTxCodec) :: - ("localClosingSigned" | lengthDelimited(closingSignedCodec))).as[ClosingTxProposed] - - val localCommitPublishedCodec: Codec[LocalCommitPublished] = ( - ("commitTx" | txCodec) :: - ("claimMainDelayedOutputTx" | optional(bool8, claimLocalDelayedOutputTxCodec)) :: - ("htlcTxs" | mapCodec(outPointCodec, optional(bool8, htlcTxCodec))) :: - ("claimHtlcDelayedTx" | listOfN(uint16, htlcDelayedTxCodec)) :: - ("claimAnchorTxs" | listOfN(uint16, claimAnchorOutputTxCodec)) :: - ("spent" | spentMapCodec)).as[LocalCommitPublished] - - val remoteCommitPublishedCodec: Codec[RemoteCommitPublished] = ( - ("commitTx" | txCodec) :: - ("claimMainOutputTx" | optional(bool8, claimRemoteCommitMainOutputTxCodec)) :: - ("claimHtlcTxs" | mapCodec(outPointCodec, optional(bool8, claimHtlcTxCodec))) :: - ("claimAnchorTxs" | listOfN(uint16, claimAnchorOutputTxCodec)) :: - ("spent" | spentMapCodec)).as[RemoteCommitPublished] - - val revokedCommitPublishedCodec: Codec[RevokedCommitPublished] = ( - ("commitTx" | txCodec) :: - ("claimMainOutputTx" | optional(bool8, claimRemoteCommitMainOutputTxCodec)) :: - ("mainPenaltyTx" | optional(bool8, mainPenaltyTxCodec)) :: - ("htlcPenaltyTxs" | listOfN(uint16, htlcPenaltyTxCodec)) :: - ("claimHtlcDelayedPenaltyTxs" | listOfN(uint16, claimHtlcDelayedOutputPenaltyTxCodec)) :: - ("spent" | spentMapCodec)).as[RevokedCommitPublished] - - private val shortids: Codec[ShortIdAliases] = ( - ("real_opt" | optional(bool8, realshortchannelid)) :: - ("localAlias" | discriminated[Alias].by(uint16).typecase(1, alias)) :: - ("remoteAlias_opt" | optional(bool8, alias)) - ).map { - case _ :: localAlias :: remoteAlias_opt :: HNil => ShortIdAliases(localAlias, remoteAlias_opt) - }.decodeOnly - - val DATA_WAIT_FOR_FUNDING_CONFIRMED_00_Codec: Codec[DATA_WAIT_FOR_FUNDING_CONFIRMED] = ( - ("commitments" | commitmentsCodec) :: - ("fundingTx_opt" | optional(bool8, txCodec)) :: - ("waitingSince" | int64.as[BlockHeight]) :: - ("deferred" | optional(bool8, lengthDelimited(channelReadyCodec))) :: - ("lastSent" | either(bool8, lengthDelimited(fundingCreatedCodec), lengthDelimited(fundingSignedCodec)))).map { - case commitments :: fundingTx :: waitingSince :: deferred :: lastSent :: HNil => - val commitments1 = ChannelTypes0.setFundingStatus(commitments, SingleFundedUnconfirmedFundingTx(fundingTx)) - DATA_WAIT_FOR_FUNDING_CONFIRMED(commitments1, waitingSince, deferred, lastSent) - }.decodeOnly - - val DATA_WAIT_FOR_CHANNEL_READY_01_Codec: Codec[DATA_WAIT_FOR_CHANNEL_READY] = ( - ("commitments" | commitmentsCodec) :: - ("shortChannelId" | realshortchannelid) :: - ("lastSent" | lengthDelimited(channelReadyCodec))).map { - case commitments :: shortChannelId :: _ :: HNil => - DATA_WAIT_FOR_CHANNEL_READY(commitments, aliases = ShortIdAliases(localAlias = Alias(shortChannelId.toLong), remoteAlias_opt = None)) - }.decodeOnly - - val DATA_WAIT_FOR_CHANNEL_READY_0a_Codec: Codec[DATA_WAIT_FOR_CHANNEL_READY] = ( - ("commitments" | commitmentsCodec) :: - ("shortIds" | shortids)).as[DATA_WAIT_FOR_CHANNEL_READY] - - val DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED_0b_Codec: Codec[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED] = fail(Err("you have been running dual funding before it was officially released! contact developers")) - - val DATA_WAIT_FOR_DUAL_FUNDING_READY_0c_Codec: Codec[DATA_WAIT_FOR_DUAL_FUNDING_READY] = ( - ("commitments" | commitmentsCodec) :: - ("shortIds" | shortids)).as[DATA_WAIT_FOR_DUAL_FUNDING_READY] - .decodeOnly - - val DATA_NORMAL_02_Codec: Codec[DATA_NORMAL] = ( - ("commitments" | commitmentsCodec) :: - ("shortChannelId" | realshortchannelid) :: - ("buried" | bool8) :: - ("channelAnnouncement" | optional(bool8, lengthDelimited(channelAnnouncementCodec))) :: - ("channelUpdate" | lengthDelimited(channelUpdateCodec)) :: - ("localShutdown" | optional(bool8, lengthDelimited(shutdownCodec))) :: - ("remoteShutdown" | optional(bool8, lengthDelimited(shutdownCodec))) :: - ("closeStatus" | provide(Option.empty[CloseStatus]))).map { - case commitments :: shortChannelId :: _ :: channelAnnouncement :: channelUpdate :: localShutdown :: remoteShutdown :: closeStatus :: HNil => - val aliases = ShortIdAliases(localAlias = Alias(shortChannelId.toLong), remoteAlias_opt = None) - DATA_NORMAL(commitments, aliases, channelAnnouncement, channelUpdate, localShutdown, remoteShutdown, closeStatus, SpliceStatus.NoSplice) - }.decodeOnly - - val DATA_NORMAL_07_Codec: Codec[DATA_NORMAL] = ( - ("commitments" | commitmentsCodec) :: - ("shortChannelId" | realshortchannelid) :: - ("buried" | bool8) :: - ("channelAnnouncement" | optional(bool8, lengthDelimited(channelAnnouncementCodec))) :: - ("channelUpdate" | lengthDelimited(channelUpdateCodec)) :: - ("localShutdown" | optional(bool8, lengthDelimited(shutdownCodec))) :: - ("remoteShutdown" | optional(bool8, lengthDelimited(shutdownCodec))) :: - ("closeStatus" | closeStatusCompatCodec)).map { - case commitments :: shortChannelId :: _ :: channelAnnouncement :: channelUpdate :: localShutdown :: remoteShutdown :: closeStatus :: HNil => - val aliases = ShortIdAliases(localAlias = Alias(shortChannelId.toLong), remoteAlias_opt = None) - DATA_NORMAL(commitments, aliases, channelAnnouncement, channelUpdate, localShutdown, remoteShutdown, closeStatus, SpliceStatus.NoSplice) - }.decodeOnly - - val DATA_NORMAL_09_Codec: Codec[DATA_NORMAL] = ( - ("commitments" | commitmentsCodec) :: - ("shortids" | shortids) :: - ("channelAnnouncement" | optional(bool8, lengthDelimited(channelAnnouncementCodec))) :: - ("channelUpdate" | lengthDelimited(channelUpdateCodec)) :: - ("localShutdown" | optional(bool8, lengthDelimited(shutdownCodec))) :: - ("remoteShutdown" | optional(bool8, lengthDelimited(shutdownCodec))) :: - ("closeStatus" | closeStatusCompatCodec) :: - ("spliceStatus" | provide[SpliceStatus](SpliceStatus.NoSplice))).map { - case commitments :: shortIds :: channelAnnouncement :: channelUpdate :: localShutdown :: remoteShutdown :: closeStatus :: spliceStatus :: HNil => - DATA_NORMAL(commitments, shortIds, channelAnnouncement, channelUpdate, localShutdown, remoteShutdown, closeStatus, spliceStatus) - }.decodeOnly - - val DATA_SHUTDOWN_03_Codec: Codec[DATA_SHUTDOWN] = ( - ("commitments" | commitmentsCodec) :: - ("localShutdown" | lengthDelimited(shutdownCodec)) :: - ("remoteShutdown" | lengthDelimited(shutdownCodec)) :: - ("closeStatus" | provide[CloseStatus](CloseStatus.Initiator(None)))).as[DATA_SHUTDOWN] - - val DATA_SHUTDOWN_08_Codec: Codec[DATA_SHUTDOWN] = ( - ("commitments" | commitmentsCodec) :: - ("localShutdown" | lengthDelimited(shutdownCodec)) :: - ("remoteShutdown" | lengthDelimited(shutdownCodec)) :: - ("closingFeerates" | optional(bool8, closingFeeratesCodec).map[CloseStatus](feerates_opt => CloseStatus.Initiator(feerates_opt)).decodeOnly)).as[DATA_SHUTDOWN] - - val DATA_NEGOTIATING_04_Codec: Codec[DATA_NEGOTIATING] = ( - ("commitments" | commitmentsCodec) :: - ("localShutdown" | lengthDelimited(shutdownCodec)) :: - ("remoteShutdown" | lengthDelimited(shutdownCodec)) :: - ("closingTxProposed" | listOfN(uint16, listOfN(uint16, lengthDelimited(closingTxProposedCodec)))) :: - ("bestUnpublishedClosingTx_opt" | optional(bool8, closingTxCodec))).as[DATA_NEGOTIATING] - - val DATA_CLOSING_05_Codec: Codec[DATA_CLOSING] = ( - ("commitments" | commitmentsCodec) :: - ("fundingTx_opt" | optional(bool8, txCodec)) :: - ("waitingSince" | int64.as[BlockHeight]) :: - ("mutualCloseProposed" | listOfN(uint16, closingTxCodec)) :: - ("mutualClosePublished" | listOfN(uint16, closingTxCodec)) :: - ("localCommitPublished" | optional(bool8, localCommitPublishedCodec)) :: - ("remoteCommitPublished" | optional(bool8, remoteCommitPublishedCodec)) :: - ("nextRemoteCommitPublished" | optional(bool8, remoteCommitPublishedCodec)) :: - ("futureRemoteCommitPublished" | optional(bool8, remoteCommitPublishedCodec)) :: - ("revokedCommitPublished" | listOfN(uint16, revokedCommitPublishedCodec))).map { - case commitments :: fundingTx_opt :: waitingSince :: mutualCloseProposed :: mutualClosePublished :: localCommitPublished :: remoteCommitPublished :: nextRemoteCommitPublished :: futureRemoteCommitPublished :: revokedCommitPublished :: HNil => - val commitments1 = ChannelTypes0.setFundingStatus(commitments, SingleFundedUnconfirmedFundingTx(fundingTx_opt)) - DATA_CLOSING(commitments1, waitingSince, commitments1.params.localParams.upfrontShutdownScript_opt.get, mutualCloseProposed, mutualClosePublished, localCommitPublished, remoteCommitPublished, nextRemoteCommitPublished, futureRemoteCommitPublished, revokedCommitPublished) - }.decodeOnly - - val unconfirmedFundingTxCodec: Codec[UnconfirmedFundingTx] = discriminated[UnconfirmedFundingTx].by(uint8) - .typecase(0x01, txCodec.map(tx => SingleFundedUnconfirmedFundingTx(Some(tx))).decodeOnly) - .typecase(0x02, fail[DualFundedUnconfirmedFundingTx](Err("you have been running dual funding before it was officially released! contact developers"))) - - val DATA_CLOSING_0d_Codec: Codec[DATA_CLOSING] = ( - ("commitments" | commitmentsCodec) :: - ("fundingTx_opt" | optional(bool8, unconfirmedFundingTxCodec)) :: - ("waitingSince" | blockHeight) :: - ("alternativeCommitments" | listOfN(uint16, dualFundingTxCodec)) :: - ("mutualCloseProposed" | listOfN(uint16, closingTxCodec)) :: - ("mutualClosePublished" | listOfN(uint16, closingTxCodec)) :: - ("localCommitPublished" | optional(bool8, localCommitPublishedCodec)) :: - ("remoteCommitPublished" | optional(bool8, remoteCommitPublishedCodec)) :: - ("nextRemoteCommitPublished" | optional(bool8, remoteCommitPublishedCodec)) :: - ("futureRemoteCommitPublished" | optional(bool8, remoteCommitPublishedCodec)) :: - ("revokedCommitPublished" | listOfN(uint16, revokedCommitPublishedCodec))).map { - case commitments :: fundingTx_opt :: waitingSince :: _ :: mutualCloseProposed :: mutualClosePublished :: localCommitPublished :: remoteCommitPublished :: nextRemoteCommitPublished :: futureRemoteCommitPublished :: revokedCommitPublished :: HNil => - val commitments1 = ChannelTypes0.setFundingStatus(commitments, SingleFundedUnconfirmedFundingTx(fundingTx_opt.flatMap(_.signedTx_opt))) - DATA_CLOSING(commitments1, waitingSince, commitments.params.localParams.upfrontShutdownScript_opt.get, mutualCloseProposed, mutualClosePublished, localCommitPublished, remoteCommitPublished, nextRemoteCommitPublished, futureRemoteCommitPublished, revokedCommitPublished) - }.decodeOnly - - val DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT_06_Codec: Codec[DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT] = ( - ("commitments" | commitmentsCodec) :: - ("remoteChannelReestablish" | channelReestablishCodec)).as[DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT] - } - - // Order matters! - val channelDataCodec: Codec[PersistentChannelData] = discriminated[PersistentChannelData].by(uint16) - .typecase(0x0d, Codecs.DATA_CLOSING_0d_Codec) - .typecase(0x0c, Codecs.DATA_WAIT_FOR_DUAL_FUNDING_READY_0c_Codec) - .typecase(0x0b, Codecs.DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED_0b_Codec) - .typecase(0x0a, Codecs.DATA_WAIT_FOR_CHANNEL_READY_0a_Codec) - .typecase(0x09, Codecs.DATA_NORMAL_09_Codec) - .typecase(0x08, Codecs.DATA_SHUTDOWN_08_Codec) - .typecase(0x07, Codecs.DATA_NORMAL_07_Codec) - .typecase(0x06, Codecs.DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT_06_Codec) - .typecase(0x05, Codecs.DATA_CLOSING_05_Codec) - .typecase(0x04, Codecs.DATA_NEGOTIATING_04_Codec) - .typecase(0x03, Codecs.DATA_SHUTDOWN_03_Codec) - .typecase(0x02, Codecs.DATA_NORMAL_02_Codec) - .typecase(0x01, Codecs.DATA_WAIT_FOR_CHANNEL_READY_01_Codec) - .typecase(0x00, Codecs.DATA_WAIT_FOR_FUNDING_CONFIRMED_00_Codec) - -} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelTypes3.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelTypes3.scala deleted file mode 100644 index ce14345e8e..0000000000 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelTypes3.scala +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright 2023 ACINQ SAS - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package fr.acinq.eclair.wire.internal.channel.version3 - -import fr.acinq.bitcoin.scalacompat.ByteVector32 -import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey -import fr.acinq.eclair.channel -import fr.acinq.eclair.channel._ -import fr.acinq.eclair.crypto.ShaChain -import fr.acinq.eclair.wire.internal.channel.version0.ChannelTypes0 -import fr.acinq.eclair.wire.protocol.CommitSig - -private[channel] object ChannelTypes3 { - - case class WaitingForRevocation(nextRemoteCommit: RemoteCommit, sent: CommitSig, sentAfterLocalCommitIndex: Long) - - // Before version4, we didn't support multiple active commitments, which were later introduced by dual funding and splicing. - case class Commitments(channelId: ByteVector32, - channelConfig: ChannelConfig, - channelFeatures: ChannelFeatures, - localParams: LocalParams, remoteParams: ChannelTypes0.RemoteParams, - channelFlags: ChannelFlags, - localCommit: LocalCommit, remoteCommit: RemoteCommit, - localChanges: LocalChanges, remoteChanges: RemoteChanges, - localNextHtlcId: Long, remoteNextHtlcId: Long, - originChannels: Map[Long, Origin], - remoteNextCommitInfo: Either[WaitingForRevocation, PublicKey], - localFundingStatus: LocalFundingStatus, - remoteFundingStatus: RemoteFundingStatus, - remotePerCommitmentSecrets: ShaChain) { - def migrate(): channel.Commitments = channel.Commitments( - ChannelParams(channelId, channelConfig, channelFeatures, localParams, remoteParams.migrate(), channelFlags), - CommitmentChanges(localChanges, remoteChanges, localNextHtlcId, remoteNextHtlcId), - Seq(Commitment(fundingTxIndex = 0, firstRemoteCommitIndex = 0, remoteFundingPubKey = remoteParams.fundingPubKey, localFundingStatus, remoteFundingStatus, localCommit, remoteCommit, remoteNextCommitInfo.left.toOption.map(w => NextRemoteCommit(w.sent, w.nextRemoteCommit)))), - inactive = Nil, - remoteNextCommitInfo.fold(w => Left(WaitForRev(w.sentAfterLocalCommitIndex)), remotePerCommitmentPoint => Right(remotePerCommitmentPoint)), - remotePerCommitmentSecrets, - originChannels - ) - } - -} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version4/ChannelCodecs4.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version4/ChannelCodecs4.scala deleted file mode 100644 index 2169e640b8..0000000000 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version4/ChannelCodecs4.scala +++ /dev/null @@ -1,881 +0,0 @@ -package fr.acinq.eclair.wire.internal.channel.version4 - -import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey -import fr.acinq.bitcoin.scalacompat.DeterministicWallet.KeyPath -import fr.acinq.bitcoin.scalacompat.{OutPoint, ScriptWitness, Transaction, TxOut} -import fr.acinq.eclair.blockchain.fee.{ConfirmationPriority, ConfirmationTarget} -import fr.acinq.eclair.channel.LocalFundingStatus._ -import fr.acinq.eclair.channel._ -import fr.acinq.eclair.channel.fund.InteractiveTxBuilder.{FullySignedSharedTransaction, PartiallySignedSharedTransaction} -import fr.acinq.eclair.channel.fund.InteractiveTxSigningSession.UnsignedLocalCommit -import fr.acinq.eclair.channel.fund.{InteractiveTxBuilder, InteractiveTxSigningSession} -import fr.acinq.eclair.crypto.ShaChain -import fr.acinq.eclair.transactions.Transactions._ -import fr.acinq.eclair.transactions.{CommitmentSpec, DirectedHtlc, IncomingHtlc, OutgoingHtlc} -import fr.acinq.eclair.wire.protocol.CommonCodecs._ -import fr.acinq.eclair.wire.protocol.LightningMessageCodecs._ -import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{Alias, BlockHeight, FeatureSupport, Features, MilliSatoshiLong, PermanentChannelFeature, RealShortChannelId, channel} -import scodec.bits.{BitVector, ByteVector} -import scodec.codecs._ -import scodec.{Attempt, Codec} - -private[channel] object ChannelCodecs4 { - - private[version4] object Codecs { - - val keyPathCodec: Codec[KeyPath] = ("path" | listOfN(uint16, uint32)).xmap[KeyPath](l => KeyPath(l), keyPath => keyPath.path.toList).as[KeyPath] - - val channelConfigCodec: Codec[ChannelConfig] = lengthDelimited(bytes).xmap(b => { - val activated: Set[ChannelConfigOption] = b.bits.toIndexedSeq.reverse.zipWithIndex.collect { - case (true, 0) => ChannelConfig.FundingPubKeyBasedChannelKeyPath - }.toSet - ChannelConfig(activated) - }, cfg => { - val indices = cfg.options.map(_.supportBit) - if (indices.isEmpty) { - ByteVector.empty - } else { - // NB: when converting from BitVector to ByteVector, scodec pads right instead of left, so we make sure we pad to bytes *before* setting bits. - var buffer = BitVector.fill(indices.max + 1)(high = false).bytes.bits - indices.foreach(i => buffer = buffer.set(i)) - buffer.reverse.bytes - } - }) - - /** We use the same encoding as init features, even if we don't need the distinction between mandatory and optional */ - val channelFeaturesCodec: Codec[ChannelFeatures] = lengthDelimited(bytes).xmap( - (b: ByteVector) => ChannelFeatures(Features(b).activated.keySet.collect { case f: PermanentChannelFeature => f }), // we make no difference between mandatory/optional, both are considered activated - (cf: ChannelFeatures) => Features(cf.features.map(f => f -> FeatureSupport.Mandatory).toMap).toByteVector // we encode features as mandatory, by convention - ) - - def localParamsCodec(channelFeatures: ChannelFeatures): Codec[LocalParams] = ( - ("nodeId" | publicKey) :: - ("channelPath" | keyPathCodec) :: - ("dustLimit" | satoshi) :: - ("maxHtlcValueInFlightMsat" | millisatoshi) :: - ("channelReserve" | conditional(!channelFeatures.hasFeature(Features.DualFunding), satoshi)) :: - ("htlcMinimum" | millisatoshi) :: - ("toSelfDelay" | cltvExpiryDelta) :: - ("maxAcceptedHtlcs" | uint16) :: - // We pad to keep codecs byte-aligned. - ("isChannelOpener" | bool) :: ("paysCommitTxFees" | bool) :: ignore(6) :: - ("upfrontShutdownScript_opt" | optional(bool8, lengthDelimited(bytes))) :: - ("walletStaticPaymentBasepoint" | optional(provide(channelFeatures.paysDirectlyToWallet), publicKey)) :: - ("features" | combinedFeaturesCodec)).as[LocalParams] - - def remoteParamsCodec(channelFeatures: ChannelFeatures): Codec[RemoteParams] = ( - ("nodeId" | publicKey) :: - ("dustLimit" | satoshi) :: - ("maxHtlcValueInFlightMsat" | uint64) :: - ("channelReserve" | conditional(!channelFeatures.hasFeature(Features.DualFunding), satoshi)) :: - ("htlcMinimum" | millisatoshi) :: - ("toSelfDelay" | cltvExpiryDelta) :: - ("maxAcceptedHtlcs" | uint16) :: - ("revocationBasepoint" | publicKey) :: - ("paymentBasepoint" | publicKey) :: - ("delayedPaymentBasepoint" | publicKey) :: - ("htlcBasepoint" | publicKey) :: - ("features" | combinedFeaturesCodec) :: - ("shutdownScript" | optional(bool8, lengthDelimited(bytes)))).as[RemoteParams] - - def setCodec[T](codec: Codec[T]): Codec[Set[T]] = listOfN(uint16, codec).xmap(_.toSet, _.toList) - - val htlcCodec: Codec[DirectedHtlc] = discriminated[DirectedHtlc].by(bool8) - .typecase(true, lengthDelimited(updateAddHtlcCodec).as[IncomingHtlc]) - .typecase(false, lengthDelimited(updateAddHtlcCodec).as[OutgoingHtlc]) - - def minimalHtlcCodec(htlcs: Set[UpdateAddHtlc]): Codec[UpdateAddHtlc] = uint64overflow.xmap[UpdateAddHtlc](id => htlcs.find(_.id == id).get, _.id) - - def minimalDirectedHtlcCodec(htlcs: Set[DirectedHtlc]): Codec[DirectedHtlc] = discriminated[DirectedHtlc].by(bool8) - .typecase(true, minimalHtlcCodec(htlcs.collect(DirectedHtlc.incoming)).as[IncomingHtlc]) - .typecase(false, minimalHtlcCodec(htlcs.collect(DirectedHtlc.outgoing)).as[OutgoingHtlc]) - - private def baseCommitmentSpecCodec(directedHtlcCodec: Codec[DirectedHtlc]): Codec[CommitmentSpec] = ( - ("htlcs" | setCodec(directedHtlcCodec)) :: - ("feeratePerKw" | feeratePerKw) :: - ("toLocal" | millisatoshi) :: - ("toRemote" | millisatoshi)).as[CommitmentSpec] - - /** HTLCs are stored separately to avoid duplicating data. */ - def minimalCommitmentSpecCodec(htlcs: Set[DirectedHtlc]): Codec[CommitmentSpec] = baseCommitmentSpecCodec(minimalDirectedHtlcCodec(htlcs)) - - /** HTLCs are stored in full, the codec is stateless but creates duplication between local/remote commitment, and across commitments. */ - val commitmentSpecCodec: Codec[CommitmentSpec] = baseCommitmentSpecCodec(htlcCodec) - - val outPointCodec: Codec[OutPoint] = lengthDelimited(bytes.xmap(d => OutPoint.read(d.toArray), d => OutPoint.write(d))) - - val txOutCodec: Codec[TxOut] = lengthDelimited(bytes.xmap(d => TxOut.read(d.toArray), d => TxOut.write(d))) - - val txCodec: Codec[Transaction] = lengthDelimited(bytes.xmap(d => Transaction.read(d.toArray), d => Transaction.write(d))) - - // all v4-encoded channels use segwit inputs, support for taproot inputs will be added later in v5 codecs - val inputInfoCodec: Codec[InputInfo] = ( - ("outPoint" | outPointCodec) :: - ("txOut" | txOutCodec) :: - ("redeemScript" | lengthDelimited(bytes))).as[InputInfo.SegwitInput].upcast[InputInfo] - - val outputInfoCodec: Codec[OutputInfo] = ( - ("index" | uint32) :: - ("amount" | satoshi) :: - ("scriptPubKey" | lengthDelimited(bytes))).as[OutputInfo] - - private val defaultConfirmationTarget: Codec[ConfirmationTarget.Absolute] = provide(ConfirmationTarget.Absolute(BlockHeight(0))) - private val blockHeightConfirmationTarget: Codec[ConfirmationTarget.Absolute] = blockHeight.xmap(ConfirmationTarget.Absolute, _.confirmBefore) - private val confirmationPriority: Codec[ConfirmationPriority] = discriminated[ConfirmationPriority].by(uint8) - .typecase(0x01, provide(ConfirmationPriority.Slow)) - .typecase(0x02, provide(ConfirmationPriority.Medium)) - .typecase(0x03, provide(ConfirmationPriority.Fast)) - private val priorityConfirmationTarget: Codec[ConfirmationTarget.Priority] = confirmationPriority.xmap(ConfirmationTarget.Priority, _.priority) - private val confirmationTarget: Codec[ConfirmationTarget] = discriminated[ConfirmationTarget].by(uint8) - .typecase(0x00, blockHeightConfirmationTarget) - .typecase(0x01, priorityConfirmationTarget) - - val commitTxCodec: Codec[CommitTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[CommitTx] - val htlcSuccessTxCodec: Codec[HtlcSuccessTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec) :: ("paymentHash" | bytes32) :: ("htlcId" | uint64overflow) :: ("confirmationTarget" | blockHeightConfirmationTarget)).as[HtlcSuccessTx] - val htlcTimeoutTxCodec: Codec[HtlcTimeoutTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec) :: ("htlcId" | uint64overflow) :: ("confirmationTarget" | blockHeightConfirmationTarget)).as[HtlcTimeoutTx] - private val htlcSuccessTxNoConfirmCodec: Codec[HtlcSuccessTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec) :: ("paymentHash" | bytes32) :: ("htlcId" | uint64overflow) :: ("confirmationTarget" | defaultConfirmationTarget)).as[HtlcSuccessTx] - private val htlcTimeoutTxNoConfirmCodec: Codec[HtlcTimeoutTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec) :: ("htlcId" | uint64overflow) :: ("confirmationTarget" | defaultConfirmationTarget)).as[HtlcTimeoutTx] - val htlcDelayedTxCodec: Codec[HtlcDelayedTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[HtlcDelayedTx] - private val legacyClaimHtlcSuccessTxCodec: Codec[LegacyClaimHtlcSuccessTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec) :: ("htlcId" | uint64overflow) :: ("confirmationTarget" | defaultConfirmationTarget)).as[LegacyClaimHtlcSuccessTx] - val claimHtlcSuccessTxCodec: Codec[ClaimHtlcSuccessTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec) :: ("paymentHash" | bytes32) :: ("htlcId" | uint64overflow) :: ("confirmationTarget" | blockHeightConfirmationTarget)).as[ClaimHtlcSuccessTx] - val claimHtlcTimeoutTxCodec: Codec[ClaimHtlcTimeoutTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec) :: ("htlcId" | uint64overflow) :: ("confirmationTarget" | blockHeightConfirmationTarget)).as[ClaimHtlcTimeoutTx] - private val claimHtlcSuccessTxNoConfirmCodec: Codec[ClaimHtlcSuccessTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec) :: ("paymentHash" | bytes32) :: ("htlcId" | uint64overflow) :: ("confirmationTarget" | defaultConfirmationTarget)).as[ClaimHtlcSuccessTx] - private val claimHtlcTimeoutTxNoConfirmCodec: Codec[ClaimHtlcTimeoutTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec) :: ("htlcId" | uint64overflow) :: ("confirmationTarget" | defaultConfirmationTarget)).as[ClaimHtlcTimeoutTx] - val claimLocalDelayedOutputTxCodec: Codec[ClaimLocalDelayedOutputTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[ClaimLocalDelayedOutputTx] - val claimP2WPKHOutputTxCodec: Codec[ClaimP2WPKHOutputTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[ClaimP2WPKHOutputTx] - val claimRemoteDelayedOutputTxCodec: Codec[ClaimRemoteDelayedOutputTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[ClaimRemoteDelayedOutputTx] - val mainPenaltyTxCodec: Codec[MainPenaltyTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[MainPenaltyTx] - val htlcPenaltyTxCodec: Codec[HtlcPenaltyTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[HtlcPenaltyTx] - val claimHtlcDelayedOutputPenaltyTxCodec: Codec[ClaimHtlcDelayedOutputPenaltyTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[ClaimHtlcDelayedOutputPenaltyTx] - val claimLocalAnchorOutputTxCodec: Codec[ClaimLocalAnchorOutputTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec) :: ("confirmationTarget" | confirmationTarget)).as[ClaimLocalAnchorOutputTx] - private val claimLocalAnchorOutputTxBlockHeightConfirmCodec: Codec[ClaimLocalAnchorOutputTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec) :: ("confirmationTarget" | blockHeightConfirmationTarget).upcast[ConfirmationTarget]).as[ClaimLocalAnchorOutputTx] - private val claimLocalAnchorOutputTxNoConfirmCodec: Codec[ClaimLocalAnchorOutputTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec) :: ("confirmationTarget" | defaultConfirmationTarget).upcast[ConfirmationTarget]).as[ClaimLocalAnchorOutputTx] - private val claimRemoteAnchorOutputTxCodec: Codec[ClaimRemoteAnchorOutputTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[ClaimRemoteAnchorOutputTx] - val closingTxCodec: Codec[ClosingTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec) :: ("outputIndex" | optional(bool8, outputInfoCodec))).as[ClosingTx] - - val claimRemoteCommitMainOutputTxCodec: Codec[ClaimRemoteCommitMainOutputTx] = discriminated[ClaimRemoteCommitMainOutputTx].by(uint8) - .typecase(0x01, claimP2WPKHOutputTxCodec) - .typecase(0x02, claimRemoteDelayedOutputTxCodec) - - val claimAnchorOutputTxCodec: Codec[ClaimAnchorOutputTx] = discriminated[ClaimAnchorOutputTx].by(uint8) - // Important: order matters! - .typecase(0x12, claimLocalAnchorOutputTxCodec) - .typecase(0x11, claimLocalAnchorOutputTxBlockHeightConfirmCodec) - .typecase(0x01, claimLocalAnchorOutputTxNoConfirmCodec) - .typecase(0x02, claimRemoteAnchorOutputTxCodec) - - val htlcTxCodec: Codec[HtlcTx] = discriminated[HtlcTx].by(uint8) - // Important: order matters! - .typecase(0x11, htlcSuccessTxCodec) - .typecase(0x12, htlcTimeoutTxCodec) - .typecase(0x01, htlcSuccessTxNoConfirmCodec) - .typecase(0x02, htlcTimeoutTxNoConfirmCodec) - - val claimHtlcTxCodec: Codec[ClaimHtlcTx] = discriminated[ClaimHtlcTx].by(uint8) - // Important: order matters! - .typecase(0x22, claimHtlcTimeoutTxCodec) - .typecase(0x23, claimHtlcSuccessTxCodec) - .typecase(0x01, legacyClaimHtlcSuccessTxCodec) - .typecase(0x02, claimHtlcTimeoutTxNoConfirmCodec) - .typecase(0x03, claimHtlcSuccessTxNoConfirmCodec) - - val htlcTxsAndRemoteSigsCodec: Codec[HtlcTxAndRemoteSig] = ( - ("txinfo" | htlcTxCodec) :: - ("remoteSig" | bytes64)).as[HtlcTxAndRemoteSig] - - val commitTxAndRemoteSigCodec: Codec[CommitTxAndRemoteSig] = ( - ("commitTx" | commitTxCodec) :: - ("remoteSig" | bytes64.as[RemoteSignature.FullSignature].upcast[RemoteSignature])).as[CommitTxAndRemoteSig] - - val updateMessageCodec: Codec[UpdateMessage] = lengthDelimited(lightningMessageCodec.narrow[UpdateMessage](f => Attempt.successful(f.asInstanceOf[UpdateMessage]), g => g)) - - val localChangesCodec: Codec[LocalChanges] = ( - ("proposed" | listOfN(uint16, updateMessageCodec)) :: - ("signed" | listOfN(uint16, updateMessageCodec)) :: - ("acked" | listOfN(uint16, updateMessageCodec))).as[LocalChanges] - - val remoteChangesCodec: Codec[RemoteChanges] = ( - ("proposed" | listOfN(uint16, updateMessageCodec)) :: - ("acked" | listOfN(uint16, updateMessageCodec)) :: - ("signed" | listOfN(uint16, updateMessageCodec))).as[RemoteChanges] - - val upstreamLocalCodec: Codec[Upstream.Local] = ("id" | uuid).as[Upstream.Local] - - val upstreamChannelCodec: Codec[Upstream.Cold.Channel] = ( - ("originChannelId" | bytes32) :: - ("originHtlcId" | int64) :: - ("amountIn" | millisatoshi)).as[Upstream.Cold.Channel] - - val legacyUpstreamChannelCodec: Codec[Upstream.Cold.Channel] = ( - ("originChannelId" | bytes32) :: - ("originHtlcId" | int64) :: - ("amountIn" | millisatoshi) :: - ("amountOut" | ignore(64))).as[Upstream.Cold.Channel] - - val upstreamChannelWithoutAmountCodec: Codec[Upstream.Cold.Channel] = ( - ("originChannelId" | bytes32) :: - ("originHtlcId" | int64) :: - ("amountIn" | provide(0 msat))).as[Upstream.Cold.Channel] - - val legacyUpstreamTrampolineCodec: Codec[Upstream.Cold.Trampoline] = listOfN(uint16, upstreamChannelWithoutAmountCodec).as[Upstream.Cold.Trampoline] - - val upstreamTrampolineCodec: Codec[Upstream.Cold.Trampoline] = listOfN(uint16, upstreamChannelCodec).as[Upstream.Cold.Trampoline] - - val coldUpstreamCodec: Codec[Upstream.Cold] = discriminated[Upstream.Cold].by(uint16) - // NB: order matters! - .typecase(0x06, upstreamChannelCodec) - .typecase(0x05, upstreamTrampolineCodec) - .typecase(0x04, legacyUpstreamTrampolineCodec) - .typecase(0x03, upstreamLocalCodec) - .typecase(0x02, legacyUpstreamChannelCodec) - - val originCodec: Codec[Origin] = coldUpstreamCodec.xmap[Origin]( - upstream => Origin.Cold(upstream), - { - case Origin.Hot(_, upstream) => Upstream.Cold(upstream) - case Origin.Cold(upstream) => upstream - } - ) - - def mapCodec[K, V](keyCodec: Codec[K], valueCodec: Codec[V]): Codec[Map[K, V]] = listOfN(uint16, keyCodec ~ valueCodec).xmap(_.toMap, _.toList) - - val originsMapCodec: Codec[Map[Long, Origin]] = mapCodec(int64, originCodec) - - val spentMapCodec: Codec[Map[OutPoint, Transaction]] = mapCodec(outPointCodec, txCodec) - - private val multisig2of2InputCodec: Codec[InteractiveTxBuilder.Multisig2of2Input] = ( - ("info" | inputInfoCodec) :: - ("fundingTxIndex" | uint32) :: - ("remoteFundingPubkey" | publicKey)).as[InteractiveTxBuilder.Multisig2of2Input] - - private val sharedFundingInputCodec: Codec[InteractiveTxBuilder.SharedFundingInput] = discriminated[InteractiveTxBuilder.SharedFundingInput].by(uint16) - .typecase(0x01, multisig2of2InputCodec) - - private val requireConfirmedInputsCodec: Codec[InteractiveTxBuilder.RequireConfirmedInputs] = (("forLocal" | bool8) :: ("forRemote" | bool8)).as[InteractiveTxBuilder.RequireConfirmedInputs] - - private val fundingParamsCodec: Codec[InteractiveTxBuilder.InteractiveTxParams] = ( - ("channelId" | bytes32) :: - ("isInitiator" | bool8) :: - ("localContribution" | satoshiSigned) :: - ("remoteContribution" | satoshiSigned) :: - ("sharedInput_opt" | optional(bool8, sharedFundingInputCodec)) :: - ("remoteFundingPubKey" | publicKey) :: - ("localOutputs" | listOfN(uint16, txOutCodec)) :: - ("lockTime" | uint32) :: - ("dustLimit" | satoshi) :: - ("targetFeerate" | feeratePerKw) :: - ("requireConfirmedInputs" | requireConfirmedInputsCodec)).as[InteractiveTxBuilder.InteractiveTxParams] - - // This codec was used by a first prototype version of splicing that only worked without HTLCs. - private val sharedInteractiveTxInputWithoutHtlcsCodec: Codec[InteractiveTxBuilder.Input.Shared] = ( - ("serialId" | uint64) :: - ("outPoint" | outPointCodec) :: - ("publicKeyScript" | provide(ByteVector.empty)) :: - ("sequence" | uint32) :: - ("localAmount" | millisatoshi) :: - ("remoteAmount" | millisatoshi) :: - ("htlcAmount" | provide(0 msat))).as[InteractiveTxBuilder.Input.Shared] - - private val sharedInteractiveTxInputWithHtlcsCodec: Codec[InteractiveTxBuilder.Input.Shared] = ( - ("serialId" | uint64) :: - ("outPoint" | outPointCodec) :: - ("publicKeyScript" | provide(ByteVector.empty)) :: - ("sequence" | uint32) :: - ("localAmount" | millisatoshi) :: - ("remoteAmount" | millisatoshi) :: - ("htlcAmount" | millisatoshi)).as[InteractiveTxBuilder.Input.Shared] - - private val sharedInteractiveTxInputWithHtlcsAndPubkeyScriptCodec: Codec[InteractiveTxBuilder.Input.Shared] = ( - ("serialId" | uint64) :: - ("outPoint" | outPointCodec) :: - ("publicKeyScript" | lengthDelimited(bytes)) :: - ("sequence" | uint32) :: - ("localAmount" | millisatoshi) :: - ("remoteAmount" | millisatoshi) :: - ("htlcAmount" | millisatoshi)).as[InteractiveTxBuilder.Input.Shared] - - private val sharedInteractiveTxInputCodec: Codec[InteractiveTxBuilder.Input.Shared] = discriminated[InteractiveTxBuilder.Input.Shared].by(byte) - .typecase(0x03, sharedInteractiveTxInputWithHtlcsAndPubkeyScriptCodec) - .typecase(0x02, sharedInteractiveTxInputWithHtlcsCodec) - .typecase(0x01, sharedInteractiveTxInputWithoutHtlcsCodec) - - private val sharedInteractiveTxOutputWithoutHtlcsCodec: Codec[InteractiveTxBuilder.Output.Shared] = ( - ("serialId" | uint64) :: - ("scriptPubKey" | lengthDelimited(bytes)) :: - ("localAmount" | millisatoshi) :: - ("remoteAmount" | millisatoshi) :: - ("htlcAmount" | provide(0 msat))).as[InteractiveTxBuilder.Output.Shared] - - private val sharedInteractiveTxOutputWithHtlcsCodec: Codec[InteractiveTxBuilder.Output.Shared] = ( - ("serialId" | uint64) :: - ("scriptPubKey" | lengthDelimited(bytes)) :: - ("localAmount" | millisatoshi) :: - ("remoteAmount" | millisatoshi) :: - ("htlcAmount" | millisatoshi)).as[InteractiveTxBuilder.Output.Shared] - - private val sharedInteractiveTxOutputCodec: Codec[InteractiveTxBuilder.Output.Shared] = discriminated[InteractiveTxBuilder.Output.Shared].by(byte) - .typecase(0x02, sharedInteractiveTxOutputWithHtlcsCodec) - .typecase(0x01, sharedInteractiveTxOutputWithoutHtlcsCodec) - - private val localOnlyInteractiveTxInputCodec: Codec[InteractiveTxBuilder.Input.Local] = ( - ("serialId" | uint64) :: - ("previousTx" | txCodec) :: - ("previousTxOutput" | uint32) :: - ("sequence" | uint32)).as[InteractiveTxBuilder.Input.Local] - - private val localInteractiveTxInputCodec: Codec[InteractiveTxBuilder.Input.Local] = discriminated[InteractiveTxBuilder.Input.Local].by(byte) - .typecase(0x01, localOnlyInteractiveTxInputCodec) - - private val remoteOnlyInteractiveTxInputCodec: Codec[InteractiveTxBuilder.Input.Remote] = ( - ("serialId" | uint64) :: - ("outPoint" | outPointCodec) :: - ("txOut" | txOutCodec) :: - ("sequence" | uint32)).as[InteractiveTxBuilder.Input.Remote] - - private val remoteInteractiveTxInputCodec: Codec[InteractiveTxBuilder.Input.Remote] = discriminated[InteractiveTxBuilder.Input.Remote].by(byte) - .typecase(0x01, remoteOnlyInteractiveTxInputCodec) - - private val localInteractiveTxChangeOutputCodec: Codec[InteractiveTxBuilder.Output.Local.Change] = ( - ("serialId" | uint64) :: - ("amount" | satoshi) :: - ("scriptPubKey" | lengthDelimited(bytes))).as[InteractiveTxBuilder.Output.Local.Change] - - private val localInteractiveTxNonChangeOutputCodec: Codec[InteractiveTxBuilder.Output.Local.NonChange] = ( - ("serialId" | uint64) :: - ("amount" | satoshi) :: - ("scriptPubKey" | lengthDelimited(bytes))).as[InteractiveTxBuilder.Output.Local.NonChange] - - private val localInteractiveTxOutputCodec: Codec[InteractiveTxBuilder.Output.Local] = discriminated[InteractiveTxBuilder.Output.Local].by(byte) - .typecase(0x01, localInteractiveTxChangeOutputCodec) - .typecase(0x02, localInteractiveTxNonChangeOutputCodec) - - private val remoteStandardInteractiveTxOutputCodec: Codec[InteractiveTxBuilder.Output.Remote] = ( - ("serialId" | uint64) :: - ("amount" | satoshi) :: - ("scriptPubKey" | lengthDelimited(bytes))).as[InteractiveTxBuilder.Output.Remote] - - private val remoteInteractiveTxOutputCodec: Codec[InteractiveTxBuilder.Output.Remote] = discriminated[InteractiveTxBuilder.Output.Remote].by(byte) - .typecase(0x01, remoteStandardInteractiveTxOutputCodec) - - private val sharedTransactionCodec: Codec[InteractiveTxBuilder.SharedTransaction] = ( - ("sharedInput" | optional(bool8, sharedInteractiveTxInputCodec)) :: - ("sharedOutput" | sharedInteractiveTxOutputCodec) :: - ("localInputs" | listOfN(uint16, localInteractiveTxInputCodec)) :: - ("remoteInputs" | listOfN(uint16, remoteInteractiveTxInputCodec)) :: - ("localOutputs" | listOfN(uint16, localInteractiveTxOutputCodec)) :: - ("remoteOutputs" | listOfN(uint16, remoteInteractiveTxOutputCodec)) :: - ("lockTime" | uint32)).as[InteractiveTxBuilder.SharedTransaction] - - private val partiallySignedSharedTransactionCodec: Codec[InteractiveTxBuilder.PartiallySignedSharedTransaction] = ( - ("sharedTx" | sharedTransactionCodec) :: - ("localSigs" | lengthDelimited(txSignaturesCodec))).as[InteractiveTxBuilder.PartiallySignedSharedTransaction] - - private val scriptWitnessCodec: Codec[ScriptWitness] = listOfN(uint16, lengthDelimited(bytes)).xmap(s => ScriptWitness(s.toSeq), w => w.stack.toList) - - private val fullySignedSharedTransactionCodec: Codec[InteractiveTxBuilder.FullySignedSharedTransaction] = ( - ("sharedTx" | sharedTransactionCodec) :: - ("localSigs" | lengthDelimited(txSignaturesCodec)) :: - ("remoteSigs" | lengthDelimited(txSignaturesCodec)) :: - ("sharedSigs_opt" | optional(bool8, scriptWitnessCodec))).as[InteractiveTxBuilder.FullySignedSharedTransaction] - - private val signedSharedTransactionCodec: Codec[InteractiveTxBuilder.SignedSharedTransaction] = discriminated[InteractiveTxBuilder.SignedSharedTransaction].by(uint16) - .typecase(0x01, partiallySignedSharedTransactionCodec) - .typecase(0x02, fullySignedSharedTransactionCodec) - - private val liquidityFeesCodec: Codec[LiquidityAds.Fees] = (("miningFees" | satoshi) :: ("serviceFees" | satoshi)).as[LiquidityAds.Fees] - - private val liquidityPurchaseCodec: Codec[LiquidityAds.PurchaseBasicInfo] = ( - ("isBuyer" | bool8) :: - ("amount" | satoshi) :: - ("fees" | liquidityFeesCodec)).as[LiquidityAds.PurchaseBasicInfo] - - private val dualFundedUnconfirmedFundingTxWithoutLiquidityPurchaseCodec: Codec[DualFundedUnconfirmedFundingTx] = ( - ("sharedTx" | signedSharedTransactionCodec) :: - ("createdAt" | blockHeight) :: - ("fundingParams" | fundingParamsCodec) :: - ("liquidityPurchase" | provide(Option.empty[LiquidityAds.PurchaseBasicInfo]))).as[DualFundedUnconfirmedFundingTx].xmap( - dfu => fillSharedInputScript(dfu), - dfu => dfu - ) - - private val dualFundedUnconfirmedFundingTxCodec: Codec[DualFundedUnconfirmedFundingTx] = ( - ("sharedTx" | signedSharedTransactionCodec) :: - ("createdAt" | blockHeight) :: - ("fundingParams" | fundingParamsCodec) :: - ("liquidityPurchase" | optional(bool8, liquidityPurchaseCodec))).as[DualFundedUnconfirmedFundingTx].xmap( - dfu => fillSharedInputScript(dfu), - dfu => dfu - ) - - // When decoding interactive-tx from older codecs, we fill the shared input publicKeyScript if necessary. - private def fillSharedInputScript(dfu: DualFundedUnconfirmedFundingTx): DualFundedUnconfirmedFundingTx = { - (dfu.sharedTx.tx.sharedInput_opt, dfu.fundingParams.sharedInput_opt) match { - case (Some(sharedTxInput), Some(sharedFundingParamsInput)) if sharedTxInput.publicKeyScript.isEmpty => - val sharedTxInput1 = sharedTxInput.copy(publicKeyScript = sharedFundingParamsInput.info.txOut.publicKeyScript) - val sharedTx1 = dfu.sharedTx.tx.copy(sharedInput_opt = Some(sharedTxInput1)) - val dfu1 = dfu.sharedTx match { - case pt: PartiallySignedSharedTransaction => dfu.copy(sharedTx = pt.copy(tx = sharedTx1)) - case ft: FullySignedSharedTransaction => dfu.copy(sharedTx = ft.copy(tx = sharedTx1)) - } - dfu1 - case _ => dfu - } - } - - val fundingTxStatusCodec: Codec[LocalFundingStatus] = discriminated[LocalFundingStatus].by(uint8) - .typecase(0x0a, (txCodec :: realshortchannelid :: optional(bool8, lengthDelimited(txSignaturesCodec)) :: optional(bool8, liquidityPurchaseCodec)).as[ConfirmedFundingTx]) - .typecase(0x01, optional(bool8, txCodec).as[SingleFundedUnconfirmedFundingTx]) - .typecase(0x07, dualFundedUnconfirmedFundingTxCodec) - .typecase(0x08, (txCodec :: optional(bool8, lengthDelimited(txSignaturesCodec)) :: optional(bool8, liquidityPurchaseCodec)).as[ZeroconfPublishedFundingTx]) - .typecase(0x09, (txCodec :: provide(RealShortChannelId(0)) :: optional(bool8, lengthDelimited(txSignaturesCodec)) :: optional(bool8, liquidityPurchaseCodec)).as[ConfirmedFundingTx]) - .typecase(0x02, dualFundedUnconfirmedFundingTxWithoutLiquidityPurchaseCodec) - .typecase(0x05, (txCodec :: optional(bool8, lengthDelimited(txSignaturesCodec)) :: provide(Option.empty[LiquidityAds.PurchaseBasicInfo])).as[ZeroconfPublishedFundingTx]) - .typecase(0x06, (txCodec :: provide(RealShortChannelId(0)) :: optional(bool8, lengthDelimited(txSignaturesCodec)) :: provide(Option.empty[LiquidityAds.PurchaseBasicInfo])).as[ConfirmedFundingTx]) - .typecase(0x03, (txCodec :: provide(Option.empty[TxSignatures]) :: provide(Option.empty[LiquidityAds.PurchaseBasicInfo])).as[ZeroconfPublishedFundingTx]) - .typecase(0x04, (txCodec :: provide(RealShortChannelId(0)) :: provide(Option.empty[TxSignatures]) :: provide(Option.empty[LiquidityAds.PurchaseBasicInfo])).as[ConfirmedFundingTx]) - - val remoteFundingStatusCodec: Codec[RemoteFundingStatus] = discriminated[RemoteFundingStatus].by(uint8) - .typecase(0x01, provide(RemoteFundingStatus.NotLocked)) - .typecase(0x02, provide(RemoteFundingStatus.Locked)) - - val paramsCodec: Codec[ChannelParams] = ( - ("channelId" | bytes32) :: - ("channelConfig" | channelConfigCodec) :: - (("channelFeatures" | channelFeaturesCodec) >>:~ { channelFeatures => - ("localParams" | localParamsCodec(channelFeatures)) :: - ("remoteParams" | remoteParamsCodec(channelFeatures)) :: - ("channelFlags" | channelflags) - })).as[ChannelParams] - - val waitForRevCodec: Codec[WaitForRev] = ("sentAfterLocalCommitIndex" | uint64overflow).as[WaitForRev] - - val changesCodec: Codec[CommitmentChanges] = ( - ("localChanges" | localChangesCodec) :: - ("remoteChanges" | remoteChangesCodec) :: - ("localNextHtlcId" | uint64overflow) :: - ("remoteNextHtlcId" | uint64overflow)).as[CommitmentChanges] - - private def localCommitCodec(commitmentSpecCodec: Codec[CommitmentSpec]): Codec[LocalCommit] = ( - ("index" | uint64overflow) :: - ("spec" | commitmentSpecCodec) :: - ("commitTxAndRemoteSig" | commitTxAndRemoteSigCodec) :: - ("htlcTxsAndRemoteSigs" | listOfN(uint16, htlcTxsAndRemoteSigsCodec))).as[LocalCommit] - - private def remoteCommitCodec(commitmentSpecCodec: Codec[CommitmentSpec]): Codec[RemoteCommit] = ( - ("index" | uint64overflow) :: - ("spec" | commitmentSpecCodec) :: - ("txid" | txId) :: - ("remotePerCommitmentPoint" | publicKey)).as[RemoteCommit] - - private def nextRemoteCommitCodec(commitmentSpecCodec: Codec[CommitmentSpec]): Codec[NextRemoteCommit] = ( - ("sig" | lengthDelimited(commitSigCodec)) :: - ("commit" | remoteCommitCodec(commitmentSpecCodec))).as[NextRemoteCommit] - - private def commitmentCodecWithoutFirstRemoteCommitIndex(htlcs: Set[DirectedHtlc]): Codec[Commitment] = ( - ("fundingTxIndex" | uint32) :: - ("firstRemoteCommitIndex" | provide(0L)) :: - ("fundingPubKey" | publicKey) :: - ("fundingTxStatus" | fundingTxStatusCodec) :: - ("remoteFundingStatus" | remoteFundingStatusCodec) :: - ("localCommit" | localCommitCodec(minimalCommitmentSpecCodec(htlcs))) :: - ("remoteCommit" | remoteCommitCodec(minimalCommitmentSpecCodec(htlcs.map(_.opposite)))) :: - ("nextRemoteCommit_opt" | optional(bool8, nextRemoteCommitCodec(minimalCommitmentSpecCodec(htlcs.map(_.opposite)))))).as[Commitment] - - private def commitmentCodec(htlcs: Set[DirectedHtlc]): Codec[Commitment] = ( - ("fundingTxIndex" | uint32) :: - ("firstRemoteCommitIndex" | uint64overflow) :: - ("fundingPubKey" | publicKey) :: - ("fundingTxStatus" | fundingTxStatusCodec) :: - ("remoteFundingStatus" | remoteFundingStatusCodec) :: - ("localCommit" | localCommitCodec(minimalCommitmentSpecCodec(htlcs))) :: - ("remoteCommit" | remoteCommitCodec(minimalCommitmentSpecCodec(htlcs.map(_.opposite)))) :: - ("nextRemoteCommit_opt" | optional(bool8, nextRemoteCommitCodec(minimalCommitmentSpecCodec(htlcs.map(_.opposite)))))).as[Commitment] - - /** - * When multiple commitments are active, htlcs are shared between all of these commitments. - * There may be up to 2 * 483 = 966 htlcs, and every htlc uses at least 1452 bytes and at most 65536 bytes. - * The resulting htlc set size is thus between 1,4 MB and 64 MB, which can be pretty large. - * To avoid writing that htlc set multiple times to disk, we encode it separately. - */ - case class EncodedCommitments(params: ChannelParams, - changes: CommitmentChanges, - // The direction we use is from our local point of view. - htlcs: Set[DirectedHtlc], - active: List[Commitment], - inactive: List[Commitment], - remoteNextCommitInfo: Either[WaitForRev, PublicKey], - remotePerCommitmentSecrets: ShaChain, - originChannels: Map[Long, Origin], - remoteChannelData_opt: Option[ByteVector]) { - def toCommitments: Commitments = { - Commitments( - params = params, - changes = changes, - active = active, - inactive = inactive, - remoteNextCommitInfo, - remotePerCommitmentSecrets, - originChannels, - remoteChannelData_opt - ) - } - } - - object EncodedCommitments { - def apply(commitments: Commitments): EncodedCommitments = { - // The direction we use is from our local point of view: we use sets, which deduplicates htlcs that are in both - // local and remote commitments. - // All active commitments have the same htlc set, but each inactive commitment may have a distinct htlc set - val commitmentsSet = (commitments.active.head +: commitments.inactive).toSet - val htlcs = commitmentsSet.flatMap(_.localCommit.spec.htlcs) ++ - commitmentsSet.flatMap(_.remoteCommit.spec.htlcs.map(_.opposite)) ++ - commitmentsSet.flatMap(_.nextRemoteCommit_opt.toList.flatMap(_.commit.spec.htlcs.map(_.opposite))) - EncodedCommitments( - params = commitments.params, - changes = commitments.changes, - htlcs = htlcs, - active = commitments.active.toList, - inactive = commitments.inactive.toList, - remoteNextCommitInfo = commitments.remoteNextCommitInfo, - remotePerCommitmentSecrets = commitments.remotePerCommitmentSecrets, - originChannels = commitments.originChannels, - remoteChannelData_opt = commitments.remoteChannelData_opt - ) - } - } - - val commitmentsCodecWithoutFirstRemoteCommitIndex: Codec[Commitments] = ( - ("params" | paramsCodec) :: - ("changes" | changesCodec) :: - (("htlcs" | setCodec(htlcCodec)) >>:~ { htlcs => - ("active" | listOfN(uint16, commitmentCodecWithoutFirstRemoteCommitIndex(htlcs))) :: - ("inactive" | listOfN(uint16, commitmentCodecWithoutFirstRemoteCommitIndex(htlcs))) :: - ("remoteNextCommitInfo" | either(bool8, waitForRevCodec, publicKey)) :: - ("remotePerCommitmentSecrets" | byteAligned(ShaChain.shaChainCodec)) :: - ("originChannels" | originsMapCodec) :: - ("remoteChannelData_opt" | optional(bool8, varsizebinarydata)) - })).as[EncodedCommitments].xmap( - encoded => encoded.toCommitments, - commitments => EncodedCommitments(commitments) - ) - - val commitmentsCodec: Codec[Commitments] = ( - ("params" | paramsCodec) :: - ("changes" | changesCodec) :: - (("htlcs" | setCodec(htlcCodec)) >>:~ { htlcs => - ("active" | listOfN(uint16, commitmentCodec(htlcs))) :: - ("inactive" | listOfN(uint16, commitmentCodec(htlcs))) :: - ("remoteNextCommitInfo" | either(bool8, waitForRevCodec, publicKey)) :: - ("remotePerCommitmentSecrets" | byteAligned(ShaChain.shaChainCodec)) :: - ("originChannels" | originsMapCodec) :: - ("remoteChannelData_opt" | optional(bool8, varsizebinarydata)) - })).as[EncodedCommitments].xmap( - encoded => encoded.toCommitments, - commitments => EncodedCommitments(commitments) - ) - - val versionedCommitmentsCodec: Codec[Commitments] = discriminated[Commitments].by(uint8) - .typecase(0x01, commitmentsCodec) - - val closingFeeratesCodec: Codec[ClosingFeerates] = ( - ("preferred" | feeratePerKw) :: - ("min" | feeratePerKw) :: - ("max" | feeratePerKw)).as[ClosingFeerates] - - val closeStatusCodec: Codec[CloseStatus] = discriminated[CloseStatus].by(uint8) - .typecase(0x01, optional(bool8, closingFeeratesCodec).as[CloseStatus.Initiator]) - .typecase(0x02, optional(bool8, closingFeeratesCodec).as[CloseStatus.NonInitiator]) - - val closingTxProposedCodec: Codec[ClosingTxProposed] = ( - ("unsignedTx" | closingTxCodec) :: - ("localClosingSigned" | lengthDelimited(closingSignedCodec))).as[ClosingTxProposed] - - val localCommitPublishedCodec: Codec[LocalCommitPublished] = ( - ("commitTx" | txCodec) :: - ("claimMainDelayedOutputTx" | optional(bool8, claimLocalDelayedOutputTxCodec)) :: - ("htlcTxs" | mapCodec(outPointCodec, optional(bool8, htlcTxCodec))) :: - ("claimHtlcDelayedTx" | listOfN(uint16, htlcDelayedTxCodec)) :: - ("claimAnchorTxs" | listOfN(uint16, claimAnchorOutputTxCodec)) :: - ("spent" | spentMapCodec)).as[LocalCommitPublished] - - val remoteCommitPublishedCodec: Codec[RemoteCommitPublished] = ( - ("commitTx" | txCodec) :: - ("claimMainOutputTx" | optional(bool8, claimRemoteCommitMainOutputTxCodec)) :: - ("claimHtlcTxs" | mapCodec(outPointCodec, optional(bool8, claimHtlcTxCodec))) :: - ("claimAnchorTxs" | listOfN(uint16, claimAnchorOutputTxCodec)) :: - ("spent" | spentMapCodec)).as[RemoteCommitPublished] - - val revokedCommitPublishedCodec: Codec[RevokedCommitPublished] = ( - ("commitTx" | txCodec) :: - ("claimMainOutputTx" | optional(bool8, claimRemoteCommitMainOutputTxCodec)) :: - ("mainPenaltyTx" | optional(bool8, mainPenaltyTxCodec)) :: - ("htlcPenaltyTxs" | listOfN(uint16, htlcPenaltyTxCodec)) :: - ("claimHtlcDelayedPenaltyTxs" | listOfN(uint16, claimHtlcDelayedOutputPenaltyTxCodec)) :: - ("spent" | spentMapCodec)).as[RevokedCommitPublished] - - // We don't bother removing the duplication across HTLCs: this is a short-lived state during which the channel - // cannot be used for payments. - private val (interactiveTxWaitingForSigsWithoutLiquidityPurchaseCodec, interactiveTxWaitingForSigsCodec): (Codec[InteractiveTxSigningSession.WaitingForSigs], Codec[InteractiveTxSigningSession.WaitingForSigs]) = { - val unsignedLocalCommitCodec: Codec[UnsignedLocalCommit] = ( - ("index" | uint64overflow) :: - ("spec" | commitmentSpecCodec) :: - ("commitTx" | commitTxCodec) :: - ("htlcTxs" | listOfN(uint16, htlcTxCodec))).as[UnsignedLocalCommit] - - val waitingForSigsWithoutLiquidityPurchaseCodec: Codec[InteractiveTxSigningSession.WaitingForSigs] = ( - ("fundingParams" | fundingParamsCodec) :: - ("fundingTxIndex" | uint32) :: - ("fundingTx" | partiallySignedSharedTransactionCodec) :: - ("localCommit" | either(bool8, unsignedLocalCommitCodec, localCommitCodec(commitmentSpecCodec))) :: - ("remoteCommit" | remoteCommitCodec(commitmentSpecCodec)) :: - ("liquidityPurchase" | provide(Option.empty[LiquidityAds.PurchaseBasicInfo]))).as[InteractiveTxSigningSession.WaitingForSigs] - - val waitingForSigsCodec: Codec[InteractiveTxSigningSession.WaitingForSigs] = ( - ("fundingParams" | fundingParamsCodec) :: - ("fundingTxIndex" | uint32) :: - ("fundingTx" | partiallySignedSharedTransactionCodec) :: - ("localCommit" | either(bool8, unsignedLocalCommitCodec, localCommitCodec(commitmentSpecCodec))) :: - ("remoteCommit" | remoteCommitCodec(commitmentSpecCodec)) :: - ("liquidityPurchase" | optional(bool8, liquidityPurchaseCodec))).as[InteractiveTxSigningSession.WaitingForSigs] - - (waitingForSigsWithoutLiquidityPurchaseCodec, waitingForSigsCodec) - } - - val dualFundingStatusCodec: Codec[DualFundingStatus] = discriminated[DualFundingStatus].by(uint8) - .\(0x01) { case status: DualFundingStatus if !status.isInstanceOf[DualFundingStatus.RbfWaitingForSigs] => DualFundingStatus.WaitingForConfirmations }(provide(DualFundingStatus.WaitingForConfirmations)) - .\(0x03) { case status: DualFundingStatus.RbfWaitingForSigs => status }(interactiveTxWaitingForSigsCodec.as[DualFundingStatus.RbfWaitingForSigs]) - .\(0x02) { case status: DualFundingStatus.RbfWaitingForSigs => status }(interactiveTxWaitingForSigsWithoutLiquidityPurchaseCodec.as[DualFundingStatus.RbfWaitingForSigs]) - - val spliceStatusCodec: Codec[SpliceStatus] = discriminated[SpliceStatus].by(uint8) - .\(0x01) { case status: SpliceStatus if !status.isInstanceOf[SpliceStatus.SpliceWaitingForSigs] => SpliceStatus.NoSplice }(provide(SpliceStatus.NoSplice)) - .\(0x03) { case status: SpliceStatus.SpliceWaitingForSigs => status }(interactiveTxWaitingForSigsCodec.as[channel.SpliceStatus.SpliceWaitingForSigs]) - .\(0x02) { case status: SpliceStatus.SpliceWaitingForSigs => status }(interactiveTxWaitingForSigsWithoutLiquidityPurchaseCodec.as[channel.SpliceStatus.SpliceWaitingForSigs]) - - private val shortids: Codec[ChannelTypes4.ShortIds] = ( - ("real_opt" | optional(bool8, realshortchannelid)) :: - ("localAlias" | discriminated[Alias].by(uint16).typecase(1, alias)) :: - ("remoteAlias_opt" | optional(bool8, alias)) - ).as[ChannelTypes4.ShortIds].decodeOnly - - val DATA_WAIT_FOR_FUNDING_CONFIRMED_00_Codec: Codec[DATA_WAIT_FOR_FUNDING_CONFIRMED] = ( - ("commitments" | commitmentsCodecWithoutFirstRemoteCommitIndex) :: - ("waitingSince" | blockHeight) :: - ("deferred" | optional(bool8, lengthDelimited(channelReadyCodec))) :: - ("lastSent" | either(bool8, lengthDelimited(fundingCreatedCodec), lengthDelimited(fundingSignedCodec)))).as[DATA_WAIT_FOR_FUNDING_CONFIRMED] - - val DATA_WAIT_FOR_FUNDING_CONFIRMED_0a_Codec: Codec[DATA_WAIT_FOR_FUNDING_CONFIRMED] = ( - ("commitments" | versionedCommitmentsCodec) :: - ("waitingSince" | blockHeight) :: - ("deferred" | optional(bool8, lengthDelimited(channelReadyCodec))) :: - ("lastSent" | either(bool8, lengthDelimited(fundingCreatedCodec), lengthDelimited(fundingSignedCodec)))).as[DATA_WAIT_FOR_FUNDING_CONFIRMED] - - val DATA_WAIT_FOR_CHANNEL_READY_01_Codec: Codec[DATA_WAIT_FOR_CHANNEL_READY] = ( - ("commitments" | commitmentsCodecWithoutFirstRemoteCommitIndex) :: - ("shortIds" | shortids)).as[ChannelTypes4.DATA_WAIT_FOR_CHANNEL_READY_0b].map(_.migrate()).decodeOnly - - val DATA_WAIT_FOR_CHANNEL_READY_0b_Codec: Codec[DATA_WAIT_FOR_CHANNEL_READY] = ( - ("commitments" | versionedCommitmentsCodec) :: - ("shortIds" | shortids)).as[ChannelTypes4.DATA_WAIT_FOR_CHANNEL_READY_0b].map(_.migrate()).decodeOnly - - val DATA_WAIT_FOR_CHANNEL_READY_15_Codec: Codec[DATA_WAIT_FOR_CHANNEL_READY] = ( - ("commitments" | versionedCommitmentsCodec) :: - ("aliases" | aliases)).as[DATA_WAIT_FOR_CHANNEL_READY] - - val DATA_WAIT_FOR_DUAL_FUNDING_SIGNED_09_Codec: Codec[DATA_WAIT_FOR_DUAL_FUNDING_SIGNED] = ( - ("channelParams" | paramsCodec) :: - ("secondRemotePerCommitmentPoint" | publicKey) :: - ("localPushAmount" | millisatoshi) :: - ("remotePushAmount" | millisatoshi) :: - ("status" | interactiveTxWaitingForSigsWithoutLiquidityPurchaseCodec) :: - ("remoteChannelData_opt" | optional(bool8, varsizebinarydata))).as[DATA_WAIT_FOR_DUAL_FUNDING_SIGNED] - - val DATA_WAIT_FOR_DUAL_FUNDING_SIGNED_13_Codec: Codec[DATA_WAIT_FOR_DUAL_FUNDING_SIGNED] = ( - ("channelParams" | paramsCodec) :: - ("secondRemotePerCommitmentPoint" | publicKey) :: - ("localPushAmount" | millisatoshi) :: - ("remotePushAmount" | millisatoshi) :: - ("status" | interactiveTxWaitingForSigsCodec) :: - ("remoteChannelData_opt" | optional(bool8, varsizebinarydata))).as[DATA_WAIT_FOR_DUAL_FUNDING_SIGNED] - - val DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED_02_Codec: Codec[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED] = ( - ("commitments" | commitmentsCodecWithoutFirstRemoteCommitIndex) :: - ("localPushAmount" | millisatoshi) :: - ("remotePushAmount" | millisatoshi) :: - ("waitingSince" | blockHeight) :: - ("lastChecked" | blockHeight) :: - ("status" | dualFundingStatusCodec) :: - ("deferred" | optional(bool8, lengthDelimited(channelReadyCodec)))).as[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED] - - val DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED_0c_Codec: Codec[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED] = ( - ("commitments" | versionedCommitmentsCodec) :: - ("localPushAmount" | millisatoshi) :: - ("remotePushAmount" | millisatoshi) :: - ("waitingSince" | blockHeight) :: - ("lastChecked" | blockHeight) :: - ("status" | dualFundingStatusCodec) :: - ("deferred" | optional(bool8, lengthDelimited(channelReadyCodec)))).as[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED] - - val DATA_WAIT_FOR_DUAL_FUNDING_READY_03_Codec: Codec[DATA_WAIT_FOR_DUAL_FUNDING_READY] = ( - ("commitments" | commitmentsCodecWithoutFirstRemoteCommitIndex) :: - ("shortIds" | shortids)).as[ChannelTypes4.DATA_WAIT_FOR_DUAL_FUNDING_READY_0d].map(_.migrate()).decodeOnly - - val DATA_WAIT_FOR_DUAL_FUNDING_READY_0d_Codec: Codec[DATA_WAIT_FOR_DUAL_FUNDING_READY] = ( - ("commitments" | versionedCommitmentsCodec) :: - ("shortIds" | shortids)).as[ChannelTypes4.DATA_WAIT_FOR_DUAL_FUNDING_READY_0d].map(_.migrate()).decodeOnly - - val DATA_WAIT_FOR_DUAL_FUNDING_READY_16_Codec: Codec[DATA_WAIT_FOR_DUAL_FUNDING_READY] = ( - ("commitments" | versionedCommitmentsCodec) :: - ("aliases" | aliases)).as[DATA_WAIT_FOR_DUAL_FUNDING_READY] - - val DATA_NORMAL_04_Codec: Codec[DATA_NORMAL] = ( - ("commitments" | commitmentsCodecWithoutFirstRemoteCommitIndex) :: - ("shortids" | shortids) :: - ("channelAnnouncement" | optional(bool8, lengthDelimited(channelAnnouncementCodec))) :: - ("channelUpdate" | lengthDelimited(channelUpdateCodec)) :: - ("localShutdown" | optional(bool8, lengthDelimited(shutdownCodec))) :: - ("remoteShutdown" | optional(bool8, lengthDelimited(shutdownCodec))) :: - ("closingFeerates" | optional(bool8, closingFeeratesCodec)) :: - ("spliceStatus" | spliceStatusCodec)).as[ChannelTypes4.DATA_NORMAL_0e].map(_.migrate()).decodeOnly - - val DATA_NORMAL_0e_Codec: Codec[DATA_NORMAL] = ( - ("commitments" | versionedCommitmentsCodec) :: - ("shortids" | shortids) :: - ("channelAnnouncement" | optional(bool8, lengthDelimited(channelAnnouncementCodec))) :: - ("channelUpdate" | lengthDelimited(channelUpdateCodec)) :: - ("localShutdown" | optional(bool8, lengthDelimited(shutdownCodec))) :: - ("remoteShutdown" | optional(bool8, lengthDelimited(shutdownCodec))) :: - ("closingFeerates" | optional(bool8, closingFeeratesCodec)) :: - ("spliceStatus" | spliceStatusCodec)).as[ChannelTypes4.DATA_NORMAL_0e].map(_.migrate()).decodeOnly - - val DATA_NORMAL_14_Codec: Codec[DATA_NORMAL] = ( - ("commitments" | versionedCommitmentsCodec) :: - ("aliases" | aliases) :: - ("channelAnnouncement" | optional(bool8, lengthDelimited(channelAnnouncementCodec))) :: - ("channelUpdate" | lengthDelimited(channelUpdateCodec)) :: - ("localShutdown" | optional(bool8, lengthDelimited(shutdownCodec))) :: - ("remoteShutdown" | optional(bool8, lengthDelimited(shutdownCodec))) :: - // If there are closing fees defined we consider ourselves to be the closing initiator. - ("closingFeerates" | optional(bool8, closingFeeratesCodec).map[Option[CloseStatus]](feerates_opt => Some(CloseStatus.Initiator(feerates_opt))).decodeOnly) :: - ("spliceStatus" | spliceStatusCodec)).as[DATA_NORMAL] - - val DATA_NORMAL_18_Codec: Codec[DATA_NORMAL] = ( - ("commitments" | versionedCommitmentsCodec) :: - ("aliases" | aliases) :: - ("channelAnnouncement" | optional(bool8, lengthDelimited(channelAnnouncementCodec))) :: - ("channelUpdate" | lengthDelimited(channelUpdateCodec)) :: - ("localShutdown" | optional(bool8, lengthDelimited(shutdownCodec))) :: - ("remoteShutdown" | optional(bool8, lengthDelimited(shutdownCodec))) :: - ("closeStatus" | optional(bool8, closeStatusCodec)) :: - ("spliceStatus" | spliceStatusCodec)).as[DATA_NORMAL] - - val DATA_SHUTDOWN_05_Codec: Codec[DATA_SHUTDOWN] = ( - ("commitments" | commitmentsCodecWithoutFirstRemoteCommitIndex) :: - ("localShutdown" | lengthDelimited(shutdownCodec)) :: - ("remoteShutdown" | lengthDelimited(shutdownCodec)) :: - // If there are closing fees defined we consider ourselves to be the closing initiator. - ("closingFeerates" | optional(bool8, closingFeeratesCodec).map[CloseStatus](feerates_opt => CloseStatus.Initiator(feerates_opt)).decodeOnly)).as[DATA_SHUTDOWN] - - val DATA_SHUTDOWN_0f_Codec: Codec[DATA_SHUTDOWN] = ( - ("commitments" | versionedCommitmentsCodec) :: - ("localShutdown" | lengthDelimited(shutdownCodec)) :: - ("remoteShutdown" | lengthDelimited(shutdownCodec)) :: - // If there are closing fees defined we consider ourselves to be the closing initiator. - ("closingFeerates" | optional(bool8, closingFeeratesCodec).map[CloseStatus](feerates_opt => CloseStatus.Initiator(feerates_opt)).decodeOnly)).as[DATA_SHUTDOWN] - - val DATA_SHUTDOWN_19_Codec: Codec[DATA_SHUTDOWN] = ( - ("commitments" | versionedCommitmentsCodec) :: - ("localShutdown" | lengthDelimited(shutdownCodec)) :: - ("remoteShutdown" | lengthDelimited(shutdownCodec)) :: - ("closeStatus" | closeStatusCodec)).as[DATA_SHUTDOWN] - - val DATA_NEGOTIATING_06_Codec: Codec[DATA_NEGOTIATING] = ( - ("commitments" | commitmentsCodecWithoutFirstRemoteCommitIndex) :: - ("localShutdown" | lengthDelimited(shutdownCodec)) :: - ("remoteShutdown" | lengthDelimited(shutdownCodec)) :: - ("closingTxProposed" | listOfN(uint16, listOfN(uint16, lengthDelimited(closingTxProposedCodec)))) :: - ("bestUnpublishedClosingTx_opt" | optional(bool8, closingTxCodec))).as[DATA_NEGOTIATING] - - val DATA_NEGOTIATING_10_Codec: Codec[DATA_NEGOTIATING] = ( - ("commitments" | versionedCommitmentsCodec) :: - ("localShutdown" | lengthDelimited(shutdownCodec)) :: - ("remoteShutdown" | lengthDelimited(shutdownCodec)) :: - ("closingTxProposed" | listOfN(uint16, listOfN(uint16, lengthDelimited(closingTxProposedCodec)))) :: - ("bestUnpublishedClosingTx_opt" | optional(bool8, closingTxCodec))).as[DATA_NEGOTIATING] - - private val closingTxsCodec: Codec[ClosingTxs] = ( - ("localAndRemote_opt" | optional(bool8, closingTxCodec)) :: - ("localOnly_opt" | optional(bool8, closingTxCodec)) :: - ("remoteOnly_opt" | optional(bool8, closingTxCodec))).as[ClosingTxs] - - val DATA_NEGOTIATING_SIMPLE_17_Codec: Codec[DATA_NEGOTIATING_SIMPLE] = ( - ("commitments" | commitmentsCodec) :: - ("lastClosingFeerate" | feeratePerKw) :: - ("localScriptPubKey" | varsizebinarydata) :: - ("remoteScriptPubKey" | varsizebinarydata) :: - ("proposedClosingTxs" | listOfN(uint16, closingTxsCodec)) :: - ("publishedClosingTxs" | listOfN(uint16, closingTxCodec))).as[DATA_NEGOTIATING_SIMPLE] - - val DATA_CLOSING_07_Codec: Codec[DATA_CLOSING] = ( - ("commitments" | commitmentsCodecWithoutFirstRemoteCommitIndex) :: - ("waitingSince" | blockHeight) :: - ("finalScriptPubKey" | lengthDelimited(bytes)) :: - ("mutualCloseProposed" | listOfN(uint16, closingTxCodec)) :: - ("mutualClosePublished" | listOfN(uint16, closingTxCodec)) :: - ("localCommitPublished" | optional(bool8, localCommitPublishedCodec)) :: - ("remoteCommitPublished" | optional(bool8, remoteCommitPublishedCodec)) :: - ("nextRemoteCommitPublished" | optional(bool8, remoteCommitPublishedCodec)) :: - ("futureRemoteCommitPublished" | optional(bool8, remoteCommitPublishedCodec)) :: - ("revokedCommitPublished" | listOfN(uint16, revokedCommitPublishedCodec))).as[DATA_CLOSING] - - val DATA_CLOSING_11_Codec: Codec[DATA_CLOSING] = ( - ("commitments" | versionedCommitmentsCodec) :: - ("waitingSince" | blockHeight) :: - ("finalScriptPubKey" | lengthDelimited(bytes)) :: - ("mutualCloseProposed" | listOfN(uint16, closingTxCodec)) :: - ("mutualClosePublished" | listOfN(uint16, closingTxCodec)) :: - ("localCommitPublished" | optional(bool8, localCommitPublishedCodec)) :: - ("remoteCommitPublished" | optional(bool8, remoteCommitPublishedCodec)) :: - ("nextRemoteCommitPublished" | optional(bool8, remoteCommitPublishedCodec)) :: - ("futureRemoteCommitPublished" | optional(bool8, remoteCommitPublishedCodec)) :: - ("revokedCommitPublished" | listOfN(uint16, revokedCommitPublishedCodec))).as[DATA_CLOSING] - - val DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT_08_Codec: Codec[DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT] = ( - ("commitments" | commitmentsCodecWithoutFirstRemoteCommitIndex) :: - ("remoteChannelReestablish" | channelReestablishCodec)).as[DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT] - - val DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT_12_Codec: Codec[DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT] = ( - ("commitments" | versionedCommitmentsCodec) :: - ("remoteChannelReestablish" | channelReestablishCodec)).as[DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT] - } - - // Order matters! - val channelDataCodec: Codec[PersistentChannelData] = discriminated[PersistentChannelData].by(uint16) - .typecase(0x19, Codecs.DATA_SHUTDOWN_19_Codec) - .typecase(0x18, Codecs.DATA_NORMAL_18_Codec) - .typecase(0x17, Codecs.DATA_NEGOTIATING_SIMPLE_17_Codec) - .typecase(0x16, Codecs.DATA_WAIT_FOR_DUAL_FUNDING_READY_16_Codec) - .typecase(0x15, Codecs.DATA_WAIT_FOR_CHANNEL_READY_15_Codec) - .typecase(0x14, Codecs.DATA_NORMAL_14_Codec) - .typecase(0x13, Codecs.DATA_WAIT_FOR_DUAL_FUNDING_SIGNED_13_Codec) - .typecase(0x12, Codecs.DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT_12_Codec) - .typecase(0x11, Codecs.DATA_CLOSING_11_Codec) - .typecase(0x10, Codecs.DATA_NEGOTIATING_10_Codec) - .typecase(0x0f, Codecs.DATA_SHUTDOWN_0f_Codec) - .typecase(0x0e, Codecs.DATA_NORMAL_0e_Codec) - .typecase(0x0d, Codecs.DATA_WAIT_FOR_DUAL_FUNDING_READY_0d_Codec) - .typecase(0x0c, Codecs.DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED_0c_Codec) - .typecase(0x0b, Codecs.DATA_WAIT_FOR_CHANNEL_READY_0b_Codec) - .typecase(0x0a, Codecs.DATA_WAIT_FOR_FUNDING_CONFIRMED_0a_Codec) - .typecase(0x09, Codecs.DATA_WAIT_FOR_DUAL_FUNDING_SIGNED_09_Codec) - .typecase(0x08, Codecs.DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT_08_Codec) - .typecase(0x07, Codecs.DATA_CLOSING_07_Codec) - .typecase(0x06, Codecs.DATA_NEGOTIATING_06_Codec) - .typecase(0x05, Codecs.DATA_SHUTDOWN_05_Codec) - .typecase(0x04, Codecs.DATA_NORMAL_04_Codec) - .typecase(0x03, Codecs.DATA_WAIT_FOR_DUAL_FUNDING_READY_03_Codec) - .typecase(0x02, Codecs.DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED_02_Codec) - .typecase(0x01, Codecs.DATA_WAIT_FOR_CHANNEL_READY_01_Codec) - .typecase(0x00, Codecs.DATA_WAIT_FOR_FUNDING_CONFIRMED_00_Codec) - -} \ No newline at end of file diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version4/ChannelTypes4.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version4/ChannelTypes4.scala deleted file mode 100644 index 55973eef7a..0000000000 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version4/ChannelTypes4.scala +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright 2024 ACINQ SAS - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package fr.acinq.eclair.wire.internal.channel.version4 - -import fr.acinq.eclair.channel.LocalFundingStatus.ConfirmedFundingTx -import fr.acinq.eclair.channel._ -import fr.acinq.eclair.wire.protocol.{ChannelAnnouncement, ChannelUpdate, Shutdown} -import fr.acinq.eclair.{Alias, RealShortChannelId} - -private[channel] object ChannelTypes4 { - - // We moved the real scid inside each commitment object when adding DATA_NORMAL_14_Codec. - case class ShortIds(real_opt: Option[RealShortChannelId], localAlias: Alias, remoteAlias_opt: Option[Alias]) - - // We moved the channel_announcement inside each commitment object when adding DATA_NORMAL_14_Codec. - case class DATA_NORMAL_0e(commitments: Commitments, - shortIds: ShortIds, - channelAnnouncement: Option[ChannelAnnouncement], - channelUpdate: ChannelUpdate, - localShutdown: Option[Shutdown], - remoteShutdown: Option[Shutdown], - closingFeerates: Option[ClosingFeerates], - spliceStatus: SpliceStatus) { - def migrate(): DATA_NORMAL = { - val commitments1 = commitments.copy( - active = commitments.active.map(c => setScidIfMatches(c, shortIds)), - inactive = commitments.inactive.map(c => setScidIfMatches(c, shortIds)), - ) - val aliases = ShortIdAliases(shortIds.localAlias, shortIds.remoteAlias_opt) - val closeStatus_opt = if (localShutdown.nonEmpty) { - Some(CloseStatus.Initiator(closingFeerates)) - } else if (remoteShutdown.nonEmpty) { - Some(CloseStatus.NonInitiator(closingFeerates)) - } else None - DATA_NORMAL(commitments1, aliases, channelAnnouncement, channelUpdate, localShutdown, remoteShutdown, closeStatus_opt, spliceStatus) - } - } - - case class DATA_WAIT_FOR_CHANNEL_READY_0b(commitments: Commitments, shortIds: ShortIds) { - def migrate(): DATA_WAIT_FOR_CHANNEL_READY = { - val commitments1 = commitments.copy( - active = commitments.active.map(c => setScidIfMatches(c, shortIds)), - inactive = commitments.inactive.map(c => setScidIfMatches(c, shortIds)), - ) - val aliases = ShortIdAliases(shortIds.localAlias, shortIds.remoteAlias_opt) - DATA_WAIT_FOR_CHANNEL_READY(commitments1, aliases) - } - } - - case class DATA_WAIT_FOR_DUAL_FUNDING_READY_0d(commitments: Commitments, shortIds: ShortIds) { - def migrate(): DATA_WAIT_FOR_DUAL_FUNDING_READY = { - val commitments1 = commitments.copy( - active = commitments.active.map(c => setScidIfMatches(c, shortIds)), - inactive = commitments.inactive.map(c => setScidIfMatches(c, shortIds)), - ) - val aliases = ShortIdAliases(shortIds.localAlias, shortIds.remoteAlias_opt) - DATA_WAIT_FOR_DUAL_FUNDING_READY(commitments1, aliases) - } - } - - private def setScidIfMatches(c: Commitment, shortIds: ShortIds): Commitment = { - c.localFundingStatus match { - // We didn't support splicing on public channels in this version: the scid (if available) is for the initial - // funding transaction. For private channels we don't care about the real scid, it will be set correctly after - // the next splice. - case f: ConfirmedFundingTx if c.fundingTxIndex == 0 => - val scid = shortIds.real_opt.getOrElse(f.shortChannelId) - c.copy(localFundingStatus = f.copy(shortChannelId = scid)) - case _ => c - } - } - -} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version5/ChannelCodecs5.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version5/ChannelCodecs5.scala new file mode 100644 index 0000000000..82a9cd32a9 --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version5/ChannelCodecs5.scala @@ -0,0 +1,529 @@ +/* + * Copyright 2025 ACINQ SAS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package fr.acinq.eclair.wire.internal.channel.version5 + +import fr.acinq.bitcoin.scalacompat.DeterministicWallet.KeyPath +import fr.acinq.bitcoin.scalacompat.{OutPoint, ScriptWitness, Transaction, TxOut} +import fr.acinq.eclair.channel._ +import fr.acinq.eclair.channel.fund.{InteractiveTxBuilder, InteractiveTxSigningSession} +import fr.acinq.eclair.crypto.ShaChain +import fr.acinq.eclair.transactions.Transactions.{ClosingTx, ClosingTxs, InputInfo} +import fr.acinq.eclair.transactions._ +import fr.acinq.eclair.wire.protocol.CommonCodecs._ +import fr.acinq.eclair.wire.protocol.LightningMessageCodecs._ +import fr.acinq.eclair.wire.protocol.{LiquidityAds, UpdateAddHtlc, UpdateMessage} +import fr.acinq.eclair.{FeatureSupport, Features, PermanentChannelFeature} +import scodec.bits.{BitVector, ByteVector} +import scodec.codecs._ +import scodec.{Attempt, Codec, Err} + +/** + * Created by t-bast on 18/06/2025. + */ + +private[channel] object ChannelCodecs5 { + + private[version5] object Codecs { + private val keyPathCodec: Codec[KeyPath] = ("path" | listOfN(uint16, uint32)).xmap[KeyPath](l => KeyPath(l), keyPath => keyPath.path.toList).as[KeyPath] + private val outPointCodec: Codec[OutPoint] = lengthDelimited(bytes.xmap(d => OutPoint.read(d.toArray), d => OutPoint.write(d))) + private val txOutCodec: Codec[TxOut] = lengthDelimited(bytes.xmap(d => TxOut.read(d.toArray), d => TxOut.write(d))) + private val txCodec: Codec[Transaction] = lengthDelimited(bytes.xmap(d => Transaction.read(d.toArray), d => Transaction.write(d))) + private val scriptWitnessCodec: Codec[ScriptWitness] = listOfN(uint16, lengthDelimited(bytes)).xmap(s => ScriptWitness(s.toSeq), w => w.stack.toList) + + private val inputInfoCodec: Codec[InputInfo] = (("outPoint" | outPointCodec) :: ("txOut" | txOutCodec)).as[InputInfo] + + private val channelSpendSignatureCodec: Codec[ChannelSpendSignature] = discriminated[ChannelSpendSignature].by(uint8) + .typecase(0x01, bytes64.as[ChannelSpendSignature.IndividualSignature]) + .typecase(0x02, partialSignatureWithNonce) + + private def setCodec[T](codec: Codec[T]): Codec[Set[T]] = listOfN(uint16, codec).xmap(_.toSet, _.toList) + + private def mapCodec[K, V](keyCodec: Codec[K], valueCodec: Codec[V]): Codec[Map[K, V]] = listOfN(uint16, keyCodec ~ valueCodec).xmap(_.toMap, _.toList) + + private val channelConfigCodec: Codec[ChannelConfig] = lengthDelimited(bytes).xmap(b => { + val activated: Set[ChannelConfigOption] = b.bits.toIndexedSeq.reverse.zipWithIndex.collect { + case (true, 0) => ChannelConfig.FundingPubKeyBasedChannelKeyPath + }.toSet + ChannelConfig(activated) + }, cfg => { + val indices = cfg.options.map(_.supportBit) + if (indices.isEmpty) { + ByteVector.empty + } else { + // NB: when converting from BitVector to ByteVector, scodec pads right instead of left, so we make sure we pad to bytes *before* setting bits. + var buffer = BitVector.fill(indices.max + 1)(high = false).bytes.bits + indices.foreach(i => buffer = buffer.set(i)) + buffer.reverse.bytes + } + }) + + /** We use the same encoding as init features, even if we don't need the distinction between mandatory and optional */ + private val channelFeaturesCodec: Codec[ChannelFeatures] = lengthDelimited(bytes).xmap( + (b: ByteVector) => ChannelFeatures(Features(b).activated.keySet.collect { case f: PermanentChannelFeature => f }), // we make no difference between mandatory/optional, both are considered activated + (cf: ChannelFeatures) => Features(cf.features.map(f => f -> FeatureSupport.Mandatory).toMap).toByteVector // we encode features as mandatory, by convention + ) + + private val commitmentFormatCodec: Codec[Transactions.CommitmentFormat] = discriminated[Transactions.CommitmentFormat].by(uint8) + .typecase(0x01, provide(Transactions.UnsafeLegacyAnchorOutputsCommitmentFormat)) + .typecase(0x02, provide(Transactions.ZeroFeeHtlcTxAnchorOutputsCommitmentFormat)) + .typecase(0x03, provide(Transactions.PhoenixSimpleTaprootChannelCommitmentFormat)) + .typecase(0x04, provide(Transactions.ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat)) + // 0x00 was used for pre-anchor channels, which have been deprecated after eclair v0.13.1. + .typecase(0x00, fail[Transactions.CommitmentFormat](Err("some of your channels are not using anchor outputs: you must restart with your previous eclair version and close those channels before updating to this version of eclair (see the release notes for more details)"))) + + // The walletStaticPaymentBasepoint field was used for static_remotekey channels, which have been deprecated: we can + // thus safely ignore that field, and encode as if it wasn't provided. + // By keeping this codec, we can potentially re-introduce that field in the future if necessary for future channel + // types, without breaking backwards-compatibility. + private val ignoreWalletStaticPaymentBasepoint: Codec[Unit] = optional(bool8, publicKey).xmap[Unit](_ => (), _ => None) + + private val localChannelParamsCodec: Codec[LocalChannelParams] = ( + ("nodeId" | publicKey) :: + ("channelPath" | keyPathCodec) :: + ("channelReserve" | optional(bool8, satoshi)) :: + // We pad to keep codecs byte-aligned. + ("isChannelOpener" | bool) :: ("paysCommitTxFees" | bool) :: ignore(6) :: + ("upfrontShutdownScript_opt" | optional(bool8, lengthDelimited(bytes))) :: + ("walletStaticPaymentBasepoint" | ignoreWalletStaticPaymentBasepoint) :: + ("features" | combinedFeaturesCodec)).as[LocalChannelParams] + + val remoteChannelParamsCodec: Codec[RemoteChannelParams] = ( + ("nodeId" | publicKey) :: + ("channelReserve" | optional(bool8, satoshi)) :: + ("revocationBasepoint" | publicKey) :: + ("paymentBasepoint" | publicKey) :: + ("delayedPaymentBasepoint" | publicKey) :: + ("htlcBasepoint" | publicKey) :: + ("features" | combinedFeaturesCodec) :: + ("shutdownScript" | optional(bool8, lengthDelimited(bytes)))).as[RemoteChannelParams] + + private val channelParamsCodec: Codec[ChannelParams] = ( + ("channelId" | bytes32) :: + ("channelConfig" | channelConfigCodec) :: + ("channelFeatures" | channelFeaturesCodec) :: + ("localParams" | localChannelParamsCodec) :: + ("remoteParams" | remoteChannelParamsCodec) :: + ("channelFlags" | channelflags)).as[ChannelParams] + + private val commitParamsCodec: Codec[CommitParams] = ( + ("dustLimit" | satoshi) :: + ("htlcMinimum" | millisatoshi) :: + ("maxHtlcValueInFlight" | uint64) :: + ("maxAcceptedHtlcs" | uint16) :: + ("toSelfDelay" | cltvExpiryDelta)).as[CommitParams] + + private val interactiveTxSharedFundingInputCodec: Codec[InteractiveTxBuilder.SharedFundingInput] = ( + ("info" | inputInfoCodec) :: + ("fundingTxIndex" | uint32) :: + ("remoteFundingPubkey" | publicKey) :: + ("commitmentFormat" | commitmentFormatCodec)).as[InteractiveTxBuilder.SharedFundingInput] + + private val sharedFundingInputCodec: Codec[InteractiveTxBuilder.SharedFundingInput] = discriminated[InteractiveTxBuilder.SharedFundingInput].by(uint16) + .typecase(0x01, interactiveTxSharedFundingInputCodec) + + private val requireConfirmedInputsCodec: Codec[InteractiveTxBuilder.RequireConfirmedInputs] = (("forLocal" | bool8) :: ("forRemote" | bool8)).as[InteractiveTxBuilder.RequireConfirmedInputs] + + private val fundingParamsCodec: Codec[InteractiveTxBuilder.InteractiveTxParams] = ( + ("channelId" | bytes32) :: + ("isInitiator" | bool8) :: + ("localContribution" | satoshiSigned) :: + ("remoteContribution" | satoshiSigned) :: + ("sharedInput_opt" | optional(bool8, sharedFundingInputCodec)) :: + ("remoteFundingPubKey" | publicKey) :: + ("localOutputs" | listOfN(uint16, txOutCodec)) :: + ("commitmentFormat" | commitmentFormatCodec) :: + ("lockTime" | uint32) :: + ("dustLimit" | satoshi) :: + ("targetFeerate" | feeratePerKw) :: + ("requireConfirmedInputs" | requireConfirmedInputsCodec)).as[InteractiveTxBuilder.InteractiveTxParams] + + private val liquidityFeesCodec: Codec[LiquidityAds.Fees] = (("miningFees" | satoshi) :: ("serviceFees" | satoshi)).as[LiquidityAds.Fees] + private val liquidityPurchaseCodec: Codec[LiquidityAds.PurchaseBasicInfo] = (("isBuyer" | bool8) :: ("amount" | satoshi) :: ("fees" | liquidityFeesCodec)).as[LiquidityAds.PurchaseBasicInfo] + + private val sharedInteractiveTxInputCodec: Codec[InteractiveTxBuilder.Input.Shared] = ( + ("serialId" | uint64) :: + ("outPoint" | outPointCodec) :: + ("publicKeyScript" | lengthDelimited(bytes)) :: + ("sequence" | uint32) :: + ("localAmount" | millisatoshi) :: + ("remoteAmount" | millisatoshi) :: + ("htlcAmount" | millisatoshi)).as[InteractiveTxBuilder.Input.Shared] + + private val sharedInteractiveTxOutputCodec: Codec[InteractiveTxBuilder.Output.Shared] = ( + ("serialId" | uint64) :: + ("scriptPubKey" | lengthDelimited(bytes)) :: + ("localAmount" | millisatoshi) :: + ("remoteAmount" | millisatoshi) :: + ("htlcAmount" | millisatoshi)).as[InteractiveTxBuilder.Output.Shared] + + private val localOnlyInteractiveTxInputCodec: Codec[InteractiveTxBuilder.Input.Local] = ( + ("serialId" | uint64) :: + ("previousTx" | txCodec) :: + ("previousTxOutput" | uint32) :: + ("sequence" | uint32)).as[InteractiveTxBuilder.Input.Local] + + private val localInteractiveTxInputCodec: Codec[InteractiveTxBuilder.Input.Local] = discriminated[InteractiveTxBuilder.Input.Local].by(byte) + .typecase(0x01, localOnlyInteractiveTxInputCodec) + + private val remoteOnlyInteractiveTxInputCodec: Codec[InteractiveTxBuilder.Input.Remote] = ( + ("serialId" | uint64) :: + ("outPoint" | outPointCodec) :: + ("txOut" | txOutCodec) :: + ("sequence" | uint32)).as[InteractiveTxBuilder.Input.Remote] + + private val remoteInteractiveTxInputCodec: Codec[InteractiveTxBuilder.Input.Remote] = discriminated[InteractiveTxBuilder.Input.Remote].by(byte) + .typecase(0x01, remoteOnlyInteractiveTxInputCodec) + + private val localInteractiveTxChangeOutputCodec: Codec[InteractiveTxBuilder.Output.Local.Change] = ( + ("serialId" | uint64) :: + ("amount" | satoshi) :: + ("scriptPubKey" | lengthDelimited(bytes))).as[InteractiveTxBuilder.Output.Local.Change] + + private val localInteractiveTxNonChangeOutputCodec: Codec[InteractiveTxBuilder.Output.Local.NonChange] = ( + ("serialId" | uint64) :: + ("amount" | satoshi) :: + ("scriptPubKey" | lengthDelimited(bytes))).as[InteractiveTxBuilder.Output.Local.NonChange] + + private val localInteractiveTxOutputCodec: Codec[InteractiveTxBuilder.Output.Local] = discriminated[InteractiveTxBuilder.Output.Local].by(byte) + .typecase(0x01, localInteractiveTxChangeOutputCodec) + .typecase(0x02, localInteractiveTxNonChangeOutputCodec) + + private val remoteStandardInteractiveTxOutputCodec: Codec[InteractiveTxBuilder.Output.Remote] = ( + ("serialId" | uint64) :: + ("amount" | satoshi) :: + ("scriptPubKey" | lengthDelimited(bytes))).as[InteractiveTxBuilder.Output.Remote] + + private val remoteInteractiveTxOutputCodec: Codec[InteractiveTxBuilder.Output.Remote] = discriminated[InteractiveTxBuilder.Output.Remote].by(byte) + .typecase(0x01, remoteStandardInteractiveTxOutputCodec) + + private val sharedTransactionCodec: Codec[InteractiveTxBuilder.SharedTransaction] = ( + ("sharedInput" | optional(bool8, sharedInteractiveTxInputCodec)) :: + ("sharedOutput" | sharedInteractiveTxOutputCodec) :: + ("localInputs" | listOfN(uint16, localInteractiveTxInputCodec)) :: + ("remoteInputs" | listOfN(uint16, remoteInteractiveTxInputCodec)) :: + ("localOutputs" | listOfN(uint16, localInteractiveTxOutputCodec)) :: + ("remoteOutputs" | listOfN(uint16, remoteInteractiveTxOutputCodec)) :: + ("lockTime" | uint32)).as[InteractiveTxBuilder.SharedTransaction] + + private val partiallySignedSharedTransactionCodec: Codec[InteractiveTxBuilder.PartiallySignedSharedTransaction] = ( + ("sharedTx" | sharedTransactionCodec) :: + ("localSigs" | lengthDelimited(txSignaturesCodec))).as[InteractiveTxBuilder.PartiallySignedSharedTransaction] + + private val fullySignedSharedTransactionCodec: Codec[InteractiveTxBuilder.FullySignedSharedTransaction] = ( + ("sharedTx" | sharedTransactionCodec) :: + ("localSigs" | lengthDelimited(txSignaturesCodec)) :: + ("remoteSigs" | lengthDelimited(txSignaturesCodec)) :: + ("sharedSigs_opt" | optional(bool8, scriptWitnessCodec))).as[InteractiveTxBuilder.FullySignedSharedTransaction] + + private val signedSharedTransactionCodec: Codec[InteractiveTxBuilder.SignedSharedTransaction] = discriminated[InteractiveTxBuilder.SignedSharedTransaction].by(uint16) + .typecase(0x01, partiallySignedSharedTransactionCodec) + .typecase(0x02, fullySignedSharedTransactionCodec) + + private val spentInputscodec: Codec[Seq[OutPoint]] = listOfN(uint16, outPointCodec).xmap(_.toSeq, _.toList) + + private val localFundingStatusCodec: Codec[LocalFundingStatus] = discriminated[LocalFundingStatus].by(uint8) + .typecase(0x04, (spentInputscodec :: txOutCodec :: realshortchannelid :: optional(bool8, lengthDelimited(txSignaturesCodec)) :: optional(bool8, liquidityPurchaseCodec)).as[LocalFundingStatus.ConfirmedFundingTx]) + .typecase(0x03, (txCodec :: optional(bool8, lengthDelimited(txSignaturesCodec)) :: optional(bool8, liquidityPurchaseCodec)).as[LocalFundingStatus.ZeroconfPublishedFundingTx]) + .typecase(0x02, (signedSharedTransactionCodec :: blockHeight :: fundingParamsCodec :: optional(bool8, liquidityPurchaseCodec)).as[LocalFundingStatus.DualFundedUnconfirmedFundingTx]) + .typecase(0x01, optional(bool8, txCodec).as[LocalFundingStatus.SingleFundedUnconfirmedFundingTx]) + + private val remoteFundingStatusCodec: Codec[RemoteFundingStatus] = discriminated[RemoteFundingStatus].by(uint8) + .typecase(0x01, provide(RemoteFundingStatus.NotLocked)) + .typecase(0x02, provide(RemoteFundingStatus.Locked)) + + private val htlcCodec: Codec[DirectedHtlc] = discriminated[DirectedHtlc].by(uint8) + .typecase(0x01, lengthDelimited(updateAddHtlcCodec).as[IncomingHtlc]) + .typecase(0x02, lengthDelimited(updateAddHtlcCodec).as[OutgoingHtlc]) + + private def minimalHtlcCodec(htlcs: Set[UpdateAddHtlc]): Codec[UpdateAddHtlc] = uint64overflow.xmap[UpdateAddHtlc](id => htlcs.find(_.id == id).get, _.id) + + private def minimalDirectedHtlcCodec(htlcs: Set[DirectedHtlc]): Codec[DirectedHtlc] = discriminated[DirectedHtlc].by(uint8) + .typecase(0x01, minimalHtlcCodec(htlcs.collect(DirectedHtlc.incoming)).as[IncomingHtlc]) + .typecase(0x02, minimalHtlcCodec(htlcs.collect(DirectedHtlc.outgoing)).as[OutgoingHtlc]) + + private def baseCommitmentSpecCodec(directedHtlcCodec: Codec[DirectedHtlc]): Codec[CommitmentSpec] = ( + ("htlcs" | setCodec(directedHtlcCodec)) :: + ("feeratePerKw" | feeratePerKw) :: + ("toLocal" | millisatoshi) :: + ("toRemote" | millisatoshi)).as[CommitmentSpec] + + /** HTLCs are stored separately to avoid duplicating data. */ + private def minimalCommitmentSpecCodec(htlcs: Set[DirectedHtlc]): Codec[CommitmentSpec] = baseCommitmentSpecCodec(minimalDirectedHtlcCodec(htlcs)) + + /** HTLCs are stored in full, the codec is stateless but creates duplication between local/remote commitment, and across commitments. */ + private val commitmentSpecCodec: Codec[CommitmentSpec] = baseCommitmentSpecCodec(htlcCodec) + + // Note that we use the default commitmentSpec codec that fully encodes HTLCs. This creates some duplication, but + // it's fine because this is a short-lived state during which the channel cannot be used for payments. + private val unsignedLocalCommitCodec: Codec[InteractiveTxSigningSession.UnsignedLocalCommit] = ( + ("index" | uint64overflow) :: + ("spec" | commitmentSpecCodec) :: + ("txId" | txId)).as[InteractiveTxSigningSession.UnsignedLocalCommit] + + private def localCommitCodec(commitmentSpecCodec: Codec[CommitmentSpec]): Codec[LocalCommit] = ( + ("index" | uint64overflow) :: + ("spec" | commitmentSpecCodec) :: + ("txId" | txId) :: + ("remoteSig" | channelSpendSignatureCodec) :: + ("htlcRemoteSigs" | listOfN(uint16, bytes64))).as[LocalCommit] + + private def remoteCommitCodec(commitmentSpecCodec: Codec[CommitmentSpec]): Codec[RemoteCommit] = ( + ("index" | uint64overflow) :: + ("spec" | commitmentSpecCodec) :: + ("txid" | txId) :: + ("remotePerCommitmentPoint" | publicKey)).as[RemoteCommit] + + // We previously stored our commit_sig for the next remote commit and retransmitted it on reconnection. + // For taproot channels, we need to re-sign because the remote nonce may have changed, so we stopped storing those. + private case class NextRemoteCommitWithSig(commitSig: ByteVector, commit: RemoteCommit) + + private def nextRemoteCommitWithSigCodec(commitmentSpecCodec: Codec[CommitmentSpec]): Codec[NextRemoteCommitWithSig] = ( + ("sig" | lengthDelimited(bytes)) :: + ("commit" | remoteCommitCodec(commitmentSpecCodec))).as[NextRemoteCommitWithSig] + + private def nextRemoteCommitCodec(commitmentSpecCodec: Codec[CommitmentSpec]): Codec[RemoteCommit] = discriminatorWithDefault( + discriminator = discriminated[RemoteCommit].by(varintoverflow).typecase(0L, remoteCommitCodec(commitmentSpecCodec)), + fallback = nextRemoteCommitWithSigCodec(commitmentSpecCodec).xmap(c => c.commit, c => NextRemoteCommitWithSig(ByteVector.empty, c)) + ) + + private def commitmentCodec(htlcs: Set[DirectedHtlc]): Codec[Commitment] = ( + ("fundingTxIndex" | uint32) :: + ("firstRemoteCommitIndex" | uint64overflow) :: + ("fundingInput" | outPointCodec) :: + ("fundingAmount" | satoshi) :: + ("remoteFundingPubKey" | publicKey) :: + ("fundingTxStatus" | localFundingStatusCodec) :: + ("remoteFundingStatus" | remoteFundingStatusCodec) :: + ("commitmentFormat" | commitmentFormatCodec) :: + ("localCommitParams" | commitParamsCodec) :: + ("localCommit" | localCommitCodec(minimalCommitmentSpecCodec(htlcs))) :: + ("remoteCommitParams" | commitParamsCodec) :: + ("remoteCommit" | remoteCommitCodec(minimalCommitmentSpecCodec(htlcs.map(_.opposite)))) :: + ("nextRemoteCommit_opt" | optional(bool8, nextRemoteCommitCodec(minimalCommitmentSpecCodec(htlcs.map(_.opposite)))))).as[Commitment] + + private val waitForRevCodec: Codec[WaitForRev] = ("sentAfterLocalCommitIndex" | uint64overflow).as[WaitForRev] + + private val updateMessageCodec: Codec[UpdateMessage] = lengthDelimited(lightningMessageCodec.narrow[UpdateMessage](f => Attempt.successful(f.asInstanceOf[UpdateMessage]), g => g)) + + private val localChangesCodec: Codec[LocalChanges] = ( + ("proposed" | listOfN(uint16, updateMessageCodec)) :: + ("signed" | listOfN(uint16, updateMessageCodec)) :: + ("acked" | listOfN(uint16, updateMessageCodec))).as[LocalChanges] + + private val remoteChangesCodec: Codec[RemoteChanges] = ( + ("proposed" | listOfN(uint16, updateMessageCodec)) :: + ("acked" | listOfN(uint16, updateMessageCodec)) :: + ("signed" | listOfN(uint16, updateMessageCodec))).as[RemoteChanges] + + private val changesCodec: Codec[CommitmentChanges] = ( + ("localChanges" | localChangesCodec) :: + ("remoteChanges" | remoteChangesCodec) :: + ("localNextHtlcId" | uint64overflow) :: + ("remoteNextHtlcId" | uint64overflow)).as[CommitmentChanges] + + private val upstreamChannelCodec: Codec[Upstream.Cold.Channel] = ( + ("originChannelId" | bytes32) :: + ("originHtlcId" | int64) :: + ("amountIn" | millisatoshi)).as[Upstream.Cold.Channel] + + private val coldUpstreamCodec: Codec[Upstream.Cold] = discriminated[Upstream.Cold].by(uint16) + // NB: order matters! + .typecase(0x03, upstreamChannelCodec) + .typecase(0x02, listOfN(uint16, upstreamChannelCodec).as[Upstream.Cold.Trampoline]) + .typecase(0x01, ("id" | uuid).as[Upstream.Local]) + + private val originCodec: Codec[Origin] = coldUpstreamCodec.xmap[Origin]( + upstream => Origin.Cold(upstream), + { + case Origin.Hot(_, upstream) => Upstream.Cold(upstream) + case Origin.Cold(upstream) => upstream + } + ) + + private val originsMapCodec: Codec[Map[Long, Origin]] = mapCodec(int64, originCodec) + + private val commitmentsCodec: Codec[ChannelTypes5.EncodedCommitments] = ( + ("params" | channelParamsCodec) :: + ("changes" | changesCodec) :: + (("htlcs" | setCodec(htlcCodec)) >>:~ { htlcs => + ("active" | listOfN(uint16, commitmentCodec(htlcs))) :: + ("inactive" | listOfN(uint16, commitmentCodec(htlcs))) :: + ("remoteNextCommitInfo" | either(bool8, waitForRevCodec, publicKey)) :: + ("remotePerCommitmentSecrets" | byteAligned(ShaChain.shaChainCodec)) :: + ("originChannels" | originsMapCodec) :: + ("remoteChannelData_opt" | optional(bool8, varsizebinarydata)) + })).as[ChannelTypes5.EncodedCommitments] + + private val versionedCommitmentsCodec: Codec[Commitments] = discriminated[Commitments].by(uint8) + .typecase(0x01, commitmentsCodec.xmap(_.toCommitments, c => ChannelTypes5.EncodedCommitments.fromCommitments(c))) + + // Note that we use the default commitmentSpec codec that fully encodes HTLCs. This creates some duplication, but + // it's fine because this is a short-lived state during which the channel cannot be used for payments. + private val interactiveTxWaitingForSigsCodec: Codec[InteractiveTxSigningSession.WaitingForSigs] = ( + ("fundingParams" | fundingParamsCodec) :: + ("fundingTxIndex" | uint32) :: + ("fundingTx" | partiallySignedSharedTransactionCodec) :: + ("localCommitParams" | commitParamsCodec) :: + ("localCommit" | either(bool8, unsignedLocalCommitCodec, localCommitCodec(commitmentSpecCodec))) :: + ("remoteCommitParams" | commitParamsCodec) :: + ("remoteCommit" | remoteCommitCodec(commitmentSpecCodec)) :: + ("liquidityPurchase" | optional(bool8, liquidityPurchaseCodec))).as[InteractiveTxSigningSession.WaitingForSigs] + + val dualFundingStatusCodec: Codec[DualFundingStatus] = discriminated[DualFundingStatus].by(uint8) + .\(0x01) { case status: DualFundingStatus if !status.isInstanceOf[DualFundingStatus.RbfWaitingForSigs] => DualFundingStatus.WaitingForConfirmations }(provide(DualFundingStatus.WaitingForConfirmations)) + .\(0x02) { case status: DualFundingStatus.RbfWaitingForSigs => status }(interactiveTxWaitingForSigsCodec.as[DualFundingStatus.RbfWaitingForSigs]) + + private val spliceStatusCodec: Codec[SpliceStatus] = discriminated[SpliceStatus].by(uint8) + .\(0x01) { case status: SpliceStatus if !status.isInstanceOf[SpliceStatus.SpliceWaitingForSigs] => SpliceStatus.NoSplice }(provide(SpliceStatus.NoSplice)) + .\(0x02) { case status: SpliceStatus.SpliceWaitingForSigs => status }(interactiveTxWaitingForSigsCodec.as[SpliceStatus.SpliceWaitingForSigs]) + + private val closingFeeratesCodec: Codec[ClosingFeerates] = (("preferred" | feeratePerKw) :: ("min" | feeratePerKw) :: ("max" | feeratePerKw)).as[ClosingFeerates] + + private val closeStatusCodec: Codec[CloseStatus] = discriminated[CloseStatus].by(uint8) + .typecase(0x01, optional(bool8, closingFeeratesCodec).as[CloseStatus.Initiator]) + .typecase(0x02, optional(bool8, closingFeeratesCodec).as[CloseStatus.NonInitiator]) + + private val closingTxCodec: Codec[ClosingTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec) :: ("outputIndex" | optional(bool8, uint32))).as[ClosingTx] + + private val closingTxsCodec: Codec[ClosingTxs] = ( + ("localAndRemote_opt" | optional(bool8, closingTxCodec)) :: + ("localOnly_opt" | optional(bool8, closingTxCodec)) :: + ("remoteOnly_opt" | optional(bool8, closingTxCodec))).as[ClosingTxs] + + private val closingTxProposedCodec: Codec[ClosingTxProposed] = (("unsignedTx" | closingTxCodec) :: ("localClosingSigned" | lengthDelimited(closingSignedCodec))).as[ClosingTxProposed] + + private val spentMapCodec: Codec[Map[OutPoint, Transaction]] = mapCodec(outPointCodec, txCodec) + + private val localCommitPublishedCodec: Codec[LocalCommitPublished] = ( + ("commitTx" | txCodec) :: + ("localOutput_opt" | optional(bool8, outPointCodec)) :: + ("anchorOutput_opt" | optional(bool8, outPointCodec)) :: + ("incomingHtlcs" | mapCodec(outPointCodec, uint64overflow)) :: + ("outgoingHtlcs" | mapCodec(outPointCodec, uint64overflow)) :: + ("htlcDelayedOutputs" | setCodec(outPointCodec)) :: + ("irrevocablySpent" | spentMapCodec)).as[LocalCommitPublished] + + private val remoteCommitPublishedCodec: Codec[RemoteCommitPublished] = ( + ("commitTx" | txCodec) :: + ("localOutput_opt" | optional(bool8, outPointCodec)) :: + ("anchorOutput_opt" | optional(bool8, outPointCodec)) :: + ("incomingHtlcs" | mapCodec(outPointCodec, uint64overflow)) :: + ("outgoingHtlcs" | mapCodec(outPointCodec, uint64overflow)) :: + ("irrevocablySpent" | spentMapCodec)).as[RemoteCommitPublished] + + private val revokedCommitPublishedCodec: Codec[RevokedCommitPublished] = ( + ("commitTx" | txCodec) :: + ("localOutput_opt" | optional(bool8, outPointCodec)) :: + ("remoteOutput_opt" | optional(bool8, outPointCodec)) :: + ("htlcOutputs" | setCodec(outPointCodec)) :: + ("htlcDelayedOutputs" | setCodec(outPointCodec)) :: + ("irrevocablySpent" | spentMapCodec)).as[RevokedCommitPublished] + + val DATA_WAIT_FOR_FUNDING_CONFIRMED_Codec: Codec[DATA_WAIT_FOR_FUNDING_CONFIRMED] = ( + ("commitments" | versionedCommitmentsCodec) :: + ("waitingSince" | blockHeight) :: + ("deferred" | optional(bool8, lengthDelimited(channelReadyCodec))) :: + ("lastSent" | either(bool8, lengthDelimited(fundingCreatedCodec), lengthDelimited(fundingSignedCodec)))).as[DATA_WAIT_FOR_FUNDING_CONFIRMED] + + val DATA_WAIT_FOR_CHANNEL_READY_Codec: Codec[DATA_WAIT_FOR_CHANNEL_READY] = ( + ("commitments" | versionedCommitmentsCodec) :: + ("aliases" | aliases)).as[DATA_WAIT_FOR_CHANNEL_READY] + + val DATA_WAIT_FOR_DUAL_FUNDING_SIGNED_Codec: Codec[DATA_WAIT_FOR_DUAL_FUNDING_SIGNED] = ( + ("channelParams" | channelParamsCodec) :: + ("secondRemotePerCommitmentPoint" | publicKey) :: + ("localPushAmount" | millisatoshi) :: + ("remotePushAmount" | millisatoshi) :: + ("status" | interactiveTxWaitingForSigsCodec)).as[DATA_WAIT_FOR_DUAL_FUNDING_SIGNED] + + val DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED_Codec: Codec[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED] = ( + ("commitments" | versionedCommitmentsCodec) :: + ("localPushAmount" | millisatoshi) :: + ("remotePushAmount" | millisatoshi) :: + ("waitingSince" | blockHeight) :: + ("lastChecked" | blockHeight) :: + ("status" | dualFundingStatusCodec) :: + ("deferred" | optional(bool8, lengthDelimited(channelReadyCodec)))).as[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED] + + val DATA_WAIT_FOR_DUAL_FUNDING_READY_Codec: Codec[DATA_WAIT_FOR_DUAL_FUNDING_READY] = ( + ("commitments" | versionedCommitmentsCodec) :: + ("aliases" | aliases)).as[DATA_WAIT_FOR_DUAL_FUNDING_READY] + + val DATA_NORMAL_Codec: Codec[DATA_NORMAL] = ( + ("commitments" | versionedCommitmentsCodec) :: + ("aliases" | aliases) :: + ("channelAnnouncement" | optional(bool8, lengthDelimited(channelAnnouncementCodec))) :: + ("channelUpdate" | lengthDelimited(channelUpdateCodec)) :: + ("spliceStatus" | spliceStatusCodec) :: + ("localShutdown" | optional(bool8, lengthDelimited(shutdownCodec))) :: + ("remoteShutdown" | optional(bool8, lengthDelimited(shutdownCodec))) :: + ("closeStatus" | optional(bool8, closeStatusCodec))).as[DATA_NORMAL] + + val DATA_SHUTDOWN_Codec: Codec[DATA_SHUTDOWN] = ( + ("commitments" | versionedCommitmentsCodec) :: + ("localShutdown" | lengthDelimited(shutdownCodec)) :: + ("remoteShutdown" | lengthDelimited(shutdownCodec)) :: + ("closeStatus" | closeStatusCodec)).as[DATA_SHUTDOWN] + + val DATA_NEGOTIATING_Codec: Codec[DATA_NEGOTIATING] = ( + ("commitments" | versionedCommitmentsCodec) :: + ("localShutdown" | lengthDelimited(shutdownCodec)) :: + ("remoteShutdown" | lengthDelimited(shutdownCodec)) :: + ("closingTxProposed" | listOfN(uint16, listOfN(uint16, lengthDelimited(closingTxProposedCodec)))) :: + ("bestUnpublishedClosingTx_opt" | optional(bool8, closingTxCodec))).as[DATA_NEGOTIATING] + + val DATA_NEGOTIATING_SIMPLE_Codec: Codec[DATA_NEGOTIATING_SIMPLE] = ( + ("commitments" | versionedCommitmentsCodec) :: + ("lastClosingFeerate" | feeratePerKw) :: + ("localScriptPubKey" | varsizebinarydata) :: + ("remoteScriptPubKey" | varsizebinarydata) :: + ("proposedClosingTxs" | listOfN(uint16, closingTxsCodec)) :: + ("publishedClosingTxs" | listOfN(uint16, closingTxCodec))).as[DATA_NEGOTIATING_SIMPLE] + + val DATA_CLOSING_Codec: Codec[DATA_CLOSING] = ( + ("commitments" | versionedCommitmentsCodec) :: + ("waitingSince" | blockHeight) :: + ("finalScriptPubKey" | lengthDelimited(bytes)) :: + ("mutualCloseProposed" | listOfN(uint16, closingTxCodec)) :: + ("mutualClosePublished" | listOfN(uint16, closingTxCodec)) :: + ("localCommitPublished" | optional(bool8, localCommitPublishedCodec)) :: + ("remoteCommitPublished" | optional(bool8, remoteCommitPublishedCodec)) :: + ("nextRemoteCommitPublished" | optional(bool8, remoteCommitPublishedCodec)) :: + ("futureRemoteCommitPublished" | optional(bool8, remoteCommitPublishedCodec)) :: + ("revokedCommitPublished" | listOfN(uint16, revokedCommitPublishedCodec)) :: + ("maxClosingFeerate" | optional(bool8, feeratePerKw))).as[DATA_CLOSING] + + val DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT_Codec: Codec[DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT] = ( + ("commitments" | versionedCommitmentsCodec) :: + ("remoteChannelReestablish" | channelReestablishCodec)).as[DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT] + } + + // Order matters! + val channelDataCodec: Codec[PersistentChannelData] = discriminated[PersistentChannelData].by(uint16) + .typecase(0x0b, Codecs.DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT_Codec) + .typecase(0x0a, Codecs.DATA_CLOSING_Codec) + .typecase(0x09, Codecs.DATA_NEGOTIATING_SIMPLE_Codec) + .typecase(0x08, Codecs.DATA_NEGOTIATING_Codec) + .typecase(0x07, Codecs.DATA_SHUTDOWN_Codec) + .typecase(0x06, Codecs.DATA_NORMAL_Codec) + .typecase(0x05, Codecs.DATA_WAIT_FOR_DUAL_FUNDING_READY_Codec) + .typecase(0x04, Codecs.DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED_Codec) + .typecase(0x03, Codecs.DATA_WAIT_FOR_DUAL_FUNDING_SIGNED_Codec) + .typecase(0x02, Codecs.DATA_WAIT_FOR_CHANNEL_READY_Codec) + .typecase(0x01, Codecs.DATA_WAIT_FOR_FUNDING_CONFIRMED_Codec) + +} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version5/ChannelTypes5.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version5/ChannelTypes5.scala new file mode 100644 index 0000000000..d23645d62b --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version5/ChannelTypes5.scala @@ -0,0 +1,84 @@ +/* + * Copyright 2025 ACINQ SAS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package fr.acinq.eclair.wire.internal.channel.version5 + +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey +import fr.acinq.eclair.channel._ +import fr.acinq.eclair.crypto.ShaChain +import fr.acinq.eclair.transactions.DirectedHtlc +import scodec.bits.ByteVector + +/** + * Created by t-bast on 18/06/2025. + */ + +private[channel] object ChannelTypes5 { + + /** + * When multiple commitments are active, htlcs are shared between all of these commitments. + * There may be up to 2 * 483 = 966 htlcs, and every htlc uses at least 1452 bytes and at most 65536 bytes. + * The resulting htlc set size is thus between 1,4 MB and 64 MB, which can be pretty large. + * To avoid writing that htlc set multiple times to disk, we encode it separately. + */ + case class EncodedCommitments(channelParams: ChannelParams, + changes: CommitmentChanges, + // The direction we use is from our local point of view. + htlcs: Set[DirectedHtlc], + active: List[Commitment], + inactive: List[Commitment], + remoteNextCommitInfo: Either[WaitForRev, PublicKey], + remotePerCommitmentSecrets: ShaChain, + originChannels: Map[Long, Origin], + remoteChannelData_opt: Option[ByteVector]) { + def toCommitments: Commitments = { + Commitments( + channelParams = channelParams, + changes = changes, + active = active, + inactive = inactive, + remoteNextCommitInfo = remoteNextCommitInfo, + remotePerCommitmentSecrets = remotePerCommitmentSecrets, + originChannels = originChannels, + remoteChannelData_opt = remoteChannelData_opt + ) + } + } + + object EncodedCommitments { + def fromCommitments(commitments: Commitments): EncodedCommitments = { + // The direction we use is from our local point of view: we use sets, which deduplicates htlcs that are in both + // local and remote commitments. All active commitments have the same htlc set, but each inactive commitment may + // have a distinct htlc set. + val commitmentsSet = commitments.active.head +: commitments.inactive + val htlcs = commitmentsSet.flatMap(_.localCommit.spec.htlcs).toSet ++ + commitmentsSet.flatMap(_.remoteCommit.spec.htlcs.map(_.opposite)).toSet ++ + commitmentsSet.flatMap(_.nextRemoteCommit_opt.toList.flatMap(_.spec.htlcs.map(_.opposite))).toSet + EncodedCommitments( + channelParams = commitments.channelParams, + changes = commitments.changes, + htlcs = htlcs, + active = commitments.active.toList, + inactive = commitments.inactive.toList, + remoteNextCommitInfo = commitments.remoteNextCommitInfo, + remotePerCommitmentSecrets = commitments.remotePerCommitmentSecrets, + originChannels = commitments.originChannels, + remoteChannelData_opt = commitments.remoteChannelData_opt + ) + } + } + +} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/ChannelTlv.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/ChannelTlv.scala index 7b561d3fa1..ac0f921e50 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/ChannelTlv.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/ChannelTlv.scala @@ -16,7 +16,9 @@ package fr.acinq.eclair.wire.protocol -import fr.acinq.bitcoin.scalacompat.{ByteVector64, Satoshi, TxId} +import fr.acinq.bitcoin.scalacompat.Musig2.IndividualNonce +import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, Satoshi, TxId} +import fr.acinq.eclair.channel.ChannelSpendSignature.PartialSignatureWithNonce import fr.acinq.eclair.channel.{ChannelType, ChannelTypes} import fr.acinq.eclair.wire.protocol.CommonCodecs._ import fr.acinq.eclair.wire.protocol.TlvCodecs.{tlvField, tlvStream, tmillisatoshi} @@ -53,7 +55,7 @@ object ChannelTlv { val upfrontShutdownScriptCodec: Codec[UpfrontShutdownScriptTlv] = tlvField(bytes) /** A channel type is a set of even feature bits that represent persistent features which affect channel operations. */ - case class ChannelTypeTlv(channelType: ChannelType) extends OpenChannelTlv with AcceptChannelTlv with OpenDualFundedChannelTlv with AcceptDualFundedChannelTlv + case class ChannelTypeTlv(channelType: ChannelType) extends OpenChannelTlv with AcceptChannelTlv with OpenDualFundedChannelTlv with AcceptDualFundedChannelTlv with SpliceInitTlv with SpliceAckTlv val channelTypeCodec: Codec[ChannelTypeTlv] = tlvField(bytes.xmap[ChannelTypeTlv]( b => ChannelTypeTlv(ChannelTypes.fromFeatures(Features(b).initFeatures())), @@ -89,6 +91,16 @@ object ChannelTlv { */ case class UseFeeCredit(amount: MilliSatoshi) extends OpenDualFundedChannelTlv with SpliceInitTlv + /** Verification nonce used for the next commitment transaction that will be signed (when using taproot channels). */ + case class NextLocalNonceTlv(nonce: IndividualNonce) extends OpenChannelTlv with AcceptChannelTlv with ChannelReadyTlv with ClosingTlv + + val nextLocalNonceCodec: Codec[NextLocalNonceTlv] = tlvField(publicNonce) + + /** Partial signature along with the signer's nonce, which is usually randomly created at signing time (when using taproot channels). */ + case class PartialSignatureWithNonceTlv(partialSigWithNonce: PartialSignatureWithNonce) extends FundingCreatedTlv with FundingSignedTlv with ClosingTlv + + val partialSignatureWithNonceCodec: Codec[PartialSignatureWithNonceTlv] = tlvField(partialSignatureWithNonce) + } object OpenChannelTlv { @@ -98,6 +110,7 @@ object OpenChannelTlv { val openTlvCodec: Codec[TlvStream[OpenChannelTlv]] = tlvStream(discriminated[OpenChannelTlv].by(varint) .typecase(UInt64(0), upfrontShutdownScriptCodec) .typecase(UInt64(1), channelTypeCodec) + .typecase(UInt64(4), nextLocalNonceCodec) ) } @@ -109,6 +122,7 @@ object AcceptChannelTlv { val acceptTlvCodec: Codec[TlvStream[AcceptChannelTlv]] = tlvStream(discriminated[AcceptChannelTlv].by(varint) .typecase(UInt64(0), upfrontShutdownScriptCodec) .typecase(UInt64(1), channelTypeCodec) + .typecase(UInt64(4), nextLocalNonceCodec) ) } @@ -169,6 +183,7 @@ object SpliceInitTlv { // We use a temporary TLV while the spec is being reviewed. .typecase(UInt64(1339), requestFundingCodec) .typecase(UInt64(0x47000007), tlvField(tmillisatoshi.as[PushAmountTlv])) + .typecase(UInt64(0x47000011), channelTypeCodec.as[ChannelTypeTlv]) ) } @@ -182,6 +197,7 @@ object SpliceAckTlv { .typecase(UInt64(1339), provideFundingCodec) .typecase(UInt64(41042), feeCreditUsedCodec) .typecase(UInt64(0x47000007), tlvField(tmillisatoshi.as[PushAmountTlv])) + .typecase(UInt64(0x47000011), channelTypeCodec.as[ChannelTypeTlv]) ) } @@ -208,13 +224,17 @@ object AcceptDualFundedChannelTlv { sealed trait FundingCreatedTlv extends Tlv object FundingCreatedTlv { - val fundingCreatedTlvCodec: Codec[TlvStream[FundingCreatedTlv]] = tlvStream(discriminated[FundingCreatedTlv].by(varint)) + val fundingCreatedTlvCodec: Codec[TlvStream[FundingCreatedTlv]] = tlvStream(discriminated[FundingCreatedTlv].by(varint) + .typecase(UInt64(2), ChannelTlv.partialSignatureWithNonceCodec) + ) } sealed trait FundingSignedTlv extends Tlv object FundingSignedTlv { - val fundingSignedTlvCodec: Codec[TlvStream[FundingSignedTlv]] = tlvStream(discriminated[FundingSignedTlv].by(varint)) + val fundingSignedTlvCodec: Codec[TlvStream[FundingSignedTlv]] = tlvStream(discriminated[FundingSignedTlv].by(varint) + .typecase(UInt64(2), ChannelTlv.partialSignatureWithNonceCodec) + ) } sealed trait ChannelReadyTlv extends Tlv @@ -227,6 +247,7 @@ object ChannelReadyTlv { val channelReadyTlvCodec: Codec[TlvStream[ChannelReadyTlv]] = tlvStream(discriminated[ChannelReadyTlv].by(varint) .typecase(UInt64(1), channelAliasTlvCodec) + .typecase(UInt64(4), ChannelTlv.nextLocalNonceCodec) ) } @@ -238,6 +259,19 @@ object ChannelReestablishTlv { case class YourLastFundingLockedTlv(txId: TxId) extends ChannelReestablishTlv case class MyCurrentFundingLockedTlv(txId: TxId) extends ChannelReestablishTlv + /** + * When disconnected during an interactive tx session, we'll include a verification nonce for our *current* commitment + * which our peer will need to re-send a commit sig for our current commitment transaction spending the interactive tx. + */ + case class CurrentCommitNonceTlv(nonce: IndividualNonce) extends ChannelReestablishTlv + + /** + * Verification nonces used for the next commitment transaction, when using taproot channels. + * There must be a nonce for each active commitment (when there are pending splices or RBF attempts), indexed by the + * corresponding fundingTxId. + */ + case class NextLocalNoncesTlv(nonces: Seq[(TxId, IndividualNonce)]) extends ChannelReestablishTlv + object NextFundingTlv { val codec: Codec[NextFundingTlv] = tlvField(txIdAsHash) } @@ -245,14 +279,25 @@ object ChannelReestablishTlv { object YourLastFundingLockedTlv { val codec: Codec[YourLastFundingLockedTlv] = tlvField("your_last_funding_locked_txid" | txIdAsHash) } + object MyCurrentFundingLockedTlv { val codec: Codec[MyCurrentFundingLockedTlv] = tlvField("my_current_funding_locked_txid" | txIdAsHash) } + object CurrentCommitNonceTlv { + val codec: Codec[CurrentCommitNonceTlv] = tlvField("current_commit_nonce" | publicNonce) + } + + object NextLocalNoncesTlv { + val codec: Codec[NextLocalNoncesTlv] = tlvField(list(txIdAsHash ~ publicNonce).xmap[Seq[(TxId, IndividualNonce)]](_.toSeq, _.toList)) + } + val channelReestablishTlvCodec: Codec[TlvStream[ChannelReestablishTlv]] = tlvStream(discriminated[ChannelReestablishTlv].by(varint) .typecase(UInt64(0), NextFundingTlv.codec) .typecase(UInt64(1), YourLastFundingLockedTlv.codec) .typecase(UInt64(3), MyCurrentFundingLockedTlv.codec) + .typecase(UInt64(22), NextLocalNoncesTlv.codec) + .typecase(UInt64(24), CurrentCommitNonceTlv.codec) ) } @@ -265,7 +310,14 @@ object UpdateFeeTlv { sealed trait ShutdownTlv extends Tlv object ShutdownTlv { - val shutdownTlvCodec: Codec[TlvStream[ShutdownTlv]] = tlvStream(discriminated[ShutdownTlv].by(varint)) + /** When closing taproot channels, local nonce that will be used to sign the remote closing transaction. */ + case class ShutdownNonce(nonce: IndividualNonce) extends ShutdownTlv + + private val shutdownNonceCodec: Codec[ShutdownNonce] = tlvField(publicNonce) + + val shutdownTlvCodec: Codec[TlvStream[ShutdownTlv]] = tlvStream(discriminated[ShutdownTlv].by(varint) + .typecase(UInt64(8), shutdownNonceCodec) + ) } sealed trait ClosingSignedTlv extends Tlv @@ -286,18 +338,60 @@ sealed trait ClosingTlv extends Tlv object ClosingTlv { /** Signature for a closing transaction containing only the closer's output. */ - case class CloserOutputOnly(sig: ByteVector64) extends ClosingTlv + case class CloserOutputOnly(sig: ByteVector64) extends ClosingTlv with ClosingCompleteTlv with ClosingSigTlv /** Signature for a closing transaction containing only the closee's output. */ - case class CloseeOutputOnly(sig: ByteVector64) extends ClosingTlv + case class CloseeOutputOnly(sig: ByteVector64) extends ClosingTlv with ClosingCompleteTlv with ClosingSigTlv /** Signature for a closing transaction containing the closer and closee's outputs. */ - case class CloserAndCloseeOutputs(sig: ByteVector64) extends ClosingTlv + case class CloserAndCloseeOutputs(sig: ByteVector64) extends ClosingTlv with ClosingCompleteTlv with ClosingSigTlv +} + +sealed trait ClosingCompleteTlv extends ClosingTlv + +object ClosingCompleteTlv { + /** When closing taproot channels, partial signature for a closing transaction containing only the closer's output. */ + case class CloserOutputOnlyPartialSignature(partialSignature: PartialSignatureWithNonce) extends ClosingCompleteTlv + + /** When closing taproot channels, partial signature for a closing transaction containing only the closee's output. */ + case class CloseeOutputOnlyPartialSignature(partialSignature: PartialSignatureWithNonce) extends ClosingCompleteTlv + + /** When closing taproot channels, partial signature for a closing transaction containing the closer and closee's outputs. */ + case class CloserAndCloseeOutputsPartialSignature(partialSignature: PartialSignatureWithNonce) extends ClosingCompleteTlv - val closingTlvCodec: Codec[TlvStream[ClosingTlv]] = tlvStream(discriminated[ClosingTlv].by(varint) - .typecase(UInt64(1), tlvField(bytes64.as[CloserOutputOnly])) - .typecase(UInt64(2), tlvField(bytes64.as[CloseeOutputOnly])) - .typecase(UInt64(3), tlvField(bytes64.as[CloserAndCloseeOutputs])) + val closingCompleteTlvCodec: Codec[TlvStream[ClosingCompleteTlv]] = tlvStream(discriminated[ClosingCompleteTlv].by(varint) + .typecase(UInt64(1), tlvField(bytes64.as[ClosingTlv.CloserOutputOnly])) + .typecase(UInt64(2), tlvField(bytes64.as[ClosingTlv.CloseeOutputOnly])) + .typecase(UInt64(3), tlvField(bytes64.as[ClosingTlv.CloserAndCloseeOutputs])) + .typecase(UInt64(5), tlvField(partialSignatureWithNonce.as[CloserOutputOnlyPartialSignature])) + .typecase(UInt64(6), tlvField(partialSignatureWithNonce.as[CloseeOutputOnlyPartialSignature])) + .typecase(UInt64(7), tlvField(partialSignatureWithNonce.as[CloserAndCloseeOutputsPartialSignature])) ) +} + +sealed trait ClosingSigTlv extends ClosingTlv + +object ClosingSigTlv { + /** When closing taproot channels, partial signature for a closing transaction containing only the closer's output. */ + case class CloserOutputOnlyPartialSignature(partialSignature: ByteVector32) extends ClosingSigTlv + + /** When closing taproot channels, partial signature for a closing transaction containing only the closee's output. */ + case class CloseeOutputOnlyPartialSignature(partialSignature: ByteVector32) extends ClosingSigTlv + /** When closing taproot channels, partial signature for a closing transaction containing the closer and closee's outputs. */ + case class CloserAndCloseeOutputsPartialSignature(partialSignature: ByteVector32) extends ClosingSigTlv + + /** When closing taproot channels, local nonce that will be used to sign the next remote closing transaction. */ + case class NextCloseeNonce(nonce: IndividualNonce) extends ClosingSigTlv + + val closingSigTlvCodec: Codec[TlvStream[ClosingSigTlv]] = tlvStream(discriminated[ClosingSigTlv].by(varint) + .typecase(UInt64(1), tlvField(bytes64.as[ClosingTlv.CloserOutputOnly])) + .typecase(UInt64(2), tlvField(bytes64.as[ClosingTlv.CloseeOutputOnly])) + .typecase(UInt64(3), tlvField(bytes64.as[ClosingTlv.CloserAndCloseeOutputs])) + .typecase(UInt64(5), tlvField(bytes32.as[CloserOutputOnlyPartialSignature])) + .typecase(UInt64(6), tlvField(bytes32.as[CloseeOutputOnlyPartialSignature])) + .typecase(UInt64(7), tlvField(bytes32.as[CloserAndCloseeOutputsPartialSignature])) + .typecase(UInt64(22), tlvField(publicNonce.as[NextCloseeNonce])) + ) } + diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/CommonCodecs.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/CommonCodecs.scala index 3018c02032..9feb0093ed 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/CommonCodecs.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/CommonCodecs.scala @@ -17,11 +17,14 @@ package fr.acinq.eclair.wire.protocol import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} +import fr.acinq.bitcoin.scalacompat.Musig2.IndividualNonce import fr.acinq.bitcoin.scalacompat.{BlockHash, ByteVector32, ByteVector64, Satoshi, Transaction, TxHash, TxId} import fr.acinq.eclair.blockchain.fee.FeeratePerKw +import fr.acinq.eclair.channel.ChannelSpendSignature.PartialSignatureWithNonce import fr.acinq.eclair.channel.{ChannelFlags, ShortIdAliases} import fr.acinq.eclair.crypto.Mac32 import fr.acinq.eclair.{Alias, BlockHeight, CltvExpiry, CltvExpiryDelta, Feature, Features, InitFeature, MilliSatoshi, RealShortChannelId, ShortChannelId, TimestampSecond, UInt64, UnspecifiedShortChannelId} +import fr.acinq.secp256k1.Secp256k1 import org.apache.commons.codec.binary.Base32 import scodec.bits.{BitVector, ByteVector} import scodec.codecs._ @@ -156,6 +159,13 @@ object CommonCodecs { val publicKey: Codec[PublicKey] = catchAllCodec(bytes(33).xmap(bin => PublicKey(bin), pub => pub.value)) + val publicNonce: Codec[IndividualNonce] = Codec[IndividualNonce]( + (pub: IndividualNonce) => bytes(Secp256k1.MUSIG2_PUBLIC_NONCE_SIZE).encode(pub.data), + (wire: BitVector) => bytes(Secp256k1.MUSIG2_PUBLIC_NONCE_SIZE).decode(wire).map(_.map(IndividualNonce(_))) + ) + + val partialSignatureWithNonce: Codec[PartialSignatureWithNonce] = (bytes32 :: publicNonce).as[PartialSignatureWithNonce] + val rgb: Codec[Color] = bytes(3).xmap(buf => Color(buf(0), buf(1), buf(2)), t => ByteVector(t.r, t.g, t.b)) val txCodec: Codec[Transaction] = bytes.xmap(d => Transaction.read(d.toArray), d => Transaction.write(d)) @@ -195,4 +205,12 @@ object CommonCodecs { (bits: BitVector) => Attempt.fromTry(Try(codec.decode(bits))).flatten ) + def nonEmptyList[A](codec: Codec[A], name: String): Codec[Seq[A]] = + list(codec).narrow(l => { + if (l.nonEmpty) { + Attempt.successful(l.toSeq) + } else { + Attempt.failure(Err(s"$name must not be empty")) + } + }, _.toList) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/FailureMessage.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/FailureMessage.scala index 25aa721788..563fcce0ae 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/FailureMessage.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/FailureMessage.scala @@ -17,7 +17,7 @@ package fr.acinq.eclair.wire.protocol import fr.acinq.bitcoin.scalacompat.ByteVector32 -import fr.acinq.eclair.crypto.Mac32 +import fr.acinq.eclair.crypto.{Mac32, Sphinx} import fr.acinq.eclair.wire.protocol.CommonCodecs._ import fr.acinq.eclair.wire.protocol.FailureMessageCodecs.failureMessageCodec import fr.acinq.eclair.wire.protocol.LightningMessageCodecs.{channelFlagsCodec, channelUpdateCodec, messageFlagsCodec, meteredLightningMessageCodec} @@ -36,7 +36,7 @@ import scodec.{Attempt, Codec, Err} sealed trait FailureReason object FailureReason { /** An encrypted failure coming from downstream which we should re-encrypt and forward upstream. */ - case class EncryptedDownstreamFailure(packet: ByteVector) extends FailureReason + case class EncryptedDownstreamFailure(packet: ByteVector, attribution_opt: Option[ByteVector]) extends FailureReason /** A local failure that should be encrypted for the node that created the payment onion. */ case class LocalFailure(failure: FailureMessage) extends FailureReason } @@ -168,8 +168,14 @@ object FailureMessageCodecs { fallback = unknownFailureMessageCodec.upcast[FailureMessage] ) + private val encryptedDownstreamFailure: Codec[FailureReason.EncryptedDownstreamFailure] = + (("packet" | varsizebinarydata) :: + ("attribution_opt" | optional(bool8, bytes(Sphinx.Attribution.totalLength)))).as[FailureReason.EncryptedDownstreamFailure] + val failureReasonCodec: Codec[FailureReason] = discriminated[FailureReason].by(uint8) - .typecase(0, varsizebinarydata.as[FailureReason.EncryptedDownstreamFailure]) + // Order matters: latest codec comes first, then old codecs for backward compatibility + .typecase(2, encryptedDownstreamFailure) + .typecase(0, (varsizebinarydata :: provide[Option[ByteVector]](None)).as[FailureReason.EncryptedDownstreamFailure]) .typecase(1, variableSizeBytes(uint16, failureMessageCodec).as[FailureReason.LocalFailure]) private def failureOnionPayload(payloadAndPadLength: Int): Codec[FailureMessage] = Codec( @@ -187,9 +193,9 @@ object FailureMessageCodecs { /** * An onion-encrypted failure from an intermediate node: - * +----------------+----------------------------------+-----------------+----------------------+-----+ + * * | HMAC(32 bytes) | failure message length (2 bytes) | failure message | pad length (2 bytes) | pad | - * +----------------+----------------------------------+-----------------+----------------------+-----+ + * * Bolt 4: SHOULD set pad such that the failure_len plus pad_len is equal to 256: by always using the same size we * ensure error messages are indistinguishable. */ diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/HtlcTlv.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/HtlcTlv.scala index 7e608d36e5..2a62118e40 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/HtlcTlv.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/HtlcTlv.scala @@ -17,12 +17,16 @@ package fr.acinq.eclair.wire.protocol import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.Musig2.IndividualNonce +import fr.acinq.bitcoin.scalacompat.TxId import fr.acinq.eclair.UInt64 +import fr.acinq.eclair.channel.ChannelSpendSignature.PartialSignatureWithNonce +import fr.acinq.eclair.crypto.Sphinx import fr.acinq.eclair.wire.protocol.CommonCodecs._ import fr.acinq.eclair.wire.protocol.TlvCodecs.{tlvField, tlvStream, tu16} -import scodec.{Attempt, Codec, Err} -import scodec.bits.HexStringSyntax +import scodec.bits.{ByteVector, HexStringSyntax} import scodec.codecs._ +import scodec.{Attempt, Codec, Err} /** * Created by t-bast on 19/07/2021. @@ -55,13 +59,25 @@ object UpdateAddHtlcTlv { sealed trait UpdateFulfillHtlcTlv extends Tlv object UpdateFulfillHtlcTlv { - val updateFulfillHtlcTlvCodec: Codec[TlvStream[UpdateFulfillHtlcTlv]] = tlvStream(discriminated[UpdateFulfillHtlcTlv].by(varint)) + case class AttributionData(data: ByteVector) extends UpdateFulfillHtlcTlv + + private val attributionData: Codec[AttributionData] = (("length" | constant(hex"fd0398")) :: ("data" | bytes(Sphinx.Attribution.totalLength))).as[AttributionData] + + val updateFulfillHtlcTlvCodec: Codec[TlvStream[UpdateFulfillHtlcTlv]] = tlvStream(discriminated[UpdateFulfillHtlcTlv].by(varint) + .typecase(UInt64(1), attributionData) + ) } sealed trait UpdateFailHtlcTlv extends Tlv object UpdateFailHtlcTlv { - val updateFailHtlcTlvCodec: Codec[TlvStream[UpdateFailHtlcTlv]] = tlvStream(discriminated[UpdateFailHtlcTlv].by(varint)) + case class AttributionData(data: ByteVector) extends UpdateFailHtlcTlv + + private val attributionData: Codec[AttributionData] = (("length" | constant(hex"fd0398")) :: ("data" | bytes(Sphinx.Attribution.totalLength))).as[AttributionData] + + val updateFailHtlcTlvCodec: Codec[TlvStream[UpdateFailHtlcTlv]] = tlvStream(discriminated[UpdateFailHtlcTlv].by(varint) + .typecase(UInt64(1), attributionData) + ) } sealed trait UpdateFailMalformedHtlcTlv extends Tlv @@ -81,7 +97,15 @@ object CommitSigTlv { val codec: Codec[BatchTlv] = tlvField(tu16) } + /** Partial signature signature for the current commitment transaction, along with the signing nonce used (when using taproot channels). */ + case class PartialSignatureWithNonceTlv(partialSigWithNonce: PartialSignatureWithNonce) extends CommitSigTlv + + object PartialSignatureWithNonceTlv { + val codec: Codec[PartialSignatureWithNonceTlv] = tlvField(partialSignatureWithNonce) + } + val commitSigTlvCodec: Codec[TlvStream[CommitSigTlv]] = tlvStream(discriminated[CommitSigTlv].by(varint) + .typecase(UInt64(2), PartialSignatureWithNonceTlv.codec) .typecase(UInt64(0x47010005), BatchTlv.codec) ) @@ -90,5 +114,19 @@ object CommitSigTlv { sealed trait RevokeAndAckTlv extends Tlv object RevokeAndAckTlv { - val revokeAndAckTlvCodec: Codec[TlvStream[RevokeAndAckTlv]] = tlvStream(discriminated[RevokeAndAckTlv].by(varint)) + + /** + * Verification nonces used for the next commitment transaction, when using taproot channels. + * There must be a nonce for each active commitment (when there are pending splices or RBF attempts), indexed by the + * corresponding fundingTxId. + */ + case class NextLocalNoncesTlv(nonces: Seq[(TxId, IndividualNonce)]) extends RevokeAndAckTlv + + object NextLocalNoncesTlv { + val codec: Codec[NextLocalNoncesTlv] = tlvField(list(txIdAsHash ~ publicNonce).xmap[Seq[(TxId, IndividualNonce)]](_.toSeq, _.toList)) + } + + val revokeAndAckTlvCodec: Codec[TlvStream[RevokeAndAckTlv]] = tlvStream(discriminated[RevokeAndAckTlv].by(varint) + .typecase(UInt64(22), NextLocalNoncesTlv.codec) + ) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/InteractiveTxTlv.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/InteractiveTxTlv.scala index 96696d8356..f8ae3d15a8 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/InteractiveTxTlv.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/InteractiveTxTlv.scala @@ -16,12 +16,15 @@ package fr.acinq.eclair.wire.protocol -import fr.acinq.bitcoin.scalacompat.{ByteVector64, TxId} +import fr.acinq.bitcoin.scalacompat.Musig2.IndividualNonce +import fr.acinq.bitcoin.scalacompat.{ByteVector64, Satoshi, TxId} import fr.acinq.eclair.UInt64 -import fr.acinq.eclair.wire.protocol.CommonCodecs.{bytes64, txIdAsHash, varint} +import fr.acinq.eclair.channel.ChannelSpendSignature.PartialSignatureWithNonce +import fr.acinq.eclair.wire.protocol.CommonCodecs._ import fr.acinq.eclair.wire.protocol.TlvCodecs.{tlvField, tlvStream} import scodec.Codec -import scodec.codecs.discriminated +import scodec.bits.ByteVector +import scodec.codecs._ /** * Created by t-bast on 08/04/2022. @@ -33,9 +36,21 @@ object TxAddInputTlv { /** When doing a splice, the initiator must provide the previous funding txId instead of the whole transaction. */ case class SharedInputTxId(txId: TxId) extends TxAddInputTlv + /** + * When creating an interactive-tx where both participants sign a taproot input, we don't need to provide the entire + * previous transaction in [[TxAddInput]]: signatures will commit to the txOut of *all* of the transaction's inputs, + * which ensures that nodes cannot cheat and downgrade to a non-segwit input. + */ + case class PrevTxOut(txId: TxId, amount: Satoshi, publicKeyScript: ByteVector) extends TxAddInputTlv + + object PrevTxOut { + val codec: Codec[PrevTxOut] = tlvField((txIdAsHash :: satoshi :: bytes).as[PrevTxOut]) + } + val txAddInputTlvCodec: Codec[TlvStream[TxAddInputTlv]] = tlvStream(discriminated[TxAddInputTlv].by(varint) // Note that we actually encode as a tx_hash to be consistent with other lightning messages. .typecase(UInt64(1105), tlvField(txIdAsHash.as[SharedInputTxId])) + .typecase(UInt64(1111), PrevTxOut.codec) ) } @@ -60,7 +75,22 @@ object TxRemoveOutputTlv { sealed trait TxCompleteTlv extends Tlv object TxCompleteTlv { - val txCompleteTlvCodec: Codec[TlvStream[TxCompleteTlv]] = tlvStream(discriminated[TxCompleteTlv].by(varint)) + /** + * Musig2 nonces for the commitment transaction(s), exchanged during an interactive tx session, when using a taproot + * channel or upgrading a channel to use taproot. + * + * @param commitNonce the sender's verification nonce for the current commit tx spending the interactive tx. + * @param nextCommitNonce the sender's verification nonce for the next commit tx spending the interactive tx. + */ + case class CommitNonces(commitNonce: IndividualNonce, nextCommitNonce: IndividualNonce) extends TxCompleteTlv + + /** When splicing a taproot channel, the sender's random signing nonce for the previous funding output. */ + case class FundingInputNonce(nonce: IndividualNonce) extends TxCompleteTlv + + val txCompleteTlvCodec: Codec[TlvStream[TxCompleteTlv]] = tlvStream(discriminated[TxCompleteTlv].by(varint) + .typecase(UInt64(4), tlvField[CommitNonces, CommitNonces]((publicNonce :: publicNonce).as[CommitNonces])) + .typecase(UInt64(6), tlvField[FundingInputNonce, FundingInputNonce](publicNonce.as[FundingInputNonce])) + ) } sealed trait TxSignaturesTlv extends Tlv @@ -69,7 +99,11 @@ object TxSignaturesTlv { /** When doing a splice, each peer must provide their signature for the previous 2-of-2 funding output. */ case class PreviousFundingTxSig(sig: ByteVector64) extends TxSignaturesTlv + /** When doing a splice for a taproot channel, each peer must provide their partial signature for the previous musig2 funding output. */ + case class PreviousFundingTxPartialSig(partialSigWithNonce: PartialSignatureWithNonce) extends TxSignaturesTlv + val txSignaturesTlvCodec: Codec[TlvStream[TxSignaturesTlv]] = tlvStream(discriminated[TxSignaturesTlv].by(varint) + .typecase(UInt64(2), tlvField(partialSignatureWithNonce.as[PreviousFundingTxPartialSig])) .typecase(UInt64(601), tlvField(bytes64.as[PreviousFundingTxSig])) ) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecs.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecs.scala index 0928d8ae35..f506a3cc57 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecs.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecs.scala @@ -17,6 +17,7 @@ package fr.acinq.eclair.wire.protocol import fr.acinq.bitcoin.scalacompat.ScriptWitness +import fr.acinq.eclair.channel.ChannelSpendSignature import fr.acinq.eclair.wire.Monitoring.{Metrics, Tags} import fr.acinq.eclair.wire.protocol.CommonCodecs._ import fr.acinq.eclair.{Features, InitFeature, KamonExt} @@ -233,7 +234,7 @@ object LightningMessageCodecs { ("closeeScriptPubKey" | varsizebinarydata) :: ("fees" | satoshi) :: ("lockTime" | uint32) :: - ("tlvStream" | ClosingTlv.closingTlvCodec)).as[ClosingComplete] + ("tlvStream" | ClosingCompleteTlv.closingCompleteTlvCodec)).as[ClosingComplete] val closingSigCodec: Codec[ClosingSig] = ( ("channelId" | bytes32) :: @@ -241,7 +242,7 @@ object LightningMessageCodecs { ("closeeScriptPubKey" | varsizebinarydata) :: ("fees" | satoshi) :: ("lockTime" | uint32) :: - ("tlvStream" | ClosingTlv.closingTlvCodec)).as[ClosingSig] + ("tlvStream" | ClosingSigTlv.closingSigTlvCodec)).as[ClosingSig] val updateAddHtlcCodec: Codec[UpdateAddHtlc] = ( ("channelId" | bytes32) :: @@ -273,7 +274,7 @@ object LightningMessageCodecs { val commitSigCodec: Codec[CommitSig] = ( ("channelId" | bytes32) :: - ("signature" | bytes64) :: + ("signature" | bytes64.as[ChannelSpendSignature.IndividualSignature]) :: ("htlcSignatures" | listofsignatures) :: ("tlvStream" | CommitSigTlv.commitSigTlvCodec)).as[CommitSig] @@ -471,7 +472,8 @@ object LightningMessageCodecs { val willFailHtlcCodec: Codec[WillFailHtlc] = ( ("id" | bytes32) :: ("paymentHash" | bytes32) :: - ("reason" | varsizebinarydata)).as[WillFailHtlc] + ("reason" | varsizebinarydata) :: + ("tlvStream" | UpdateFailHtlcTlv.updateFailHtlcTlvCodec)).as[WillFailHtlc] val willFailMalformedHtlcCodec: Codec[WillFailMalformedHtlc] = ( ("id" | bytes32) :: diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala index 11034b42d0..ba4d08fe57 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala @@ -19,10 +19,13 @@ package fr.acinq.eclair.wire.protocol import com.google.common.base.Charsets import com.google.common.net.InetAddresses import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} -import fr.acinq.bitcoin.scalacompat.{BlockHash, ByteVector32, ByteVector64, OutPoint, Satoshi, SatoshiLong, ScriptWitness, Transaction, TxId} +import fr.acinq.bitcoin.scalacompat.Musig2.IndividualNonce +import fr.acinq.bitcoin.scalacompat.{BlockHash, ByteVector32, ByteVector64, OutPoint, Satoshi, SatoshiLong, ScriptWitness, Transaction, TxId, TxOut} import fr.acinq.eclair.blockchain.fee.FeeratePerKw -import fr.acinq.eclair.channel.{ChannelFlags, ChannelType} +import fr.acinq.eclair.channel.ChannelSpendSignature.{IndividualSignature, PartialSignatureWithNonce} +import fr.acinq.eclair.channel.{ChannelFlags, ChannelSpendSignature, ChannelType} import fr.acinq.eclair.payment.relay.Relayer +import fr.acinq.eclair.transactions.Transactions.InputInfo import fr.acinq.eclair.wire.protocol.ChannelReadyTlv.ShortChannelIdTlv import fr.acinq.eclair.{Alias, BlockHeight, CltvExpiry, CltvExpiryDelta, Feature, Features, InitFeature, MilliSatoshi, MilliSatoshiLong, RealShortChannelId, ShortChannelId, TimestampSecond, UInt64, isAsciiPrintable} import scodec.bits.ByteVector @@ -58,9 +61,9 @@ sealed trait HtlcFailureMessage extends HtlcSettlementMessage // <- not in the s // @formatter:on case class Init(features: Features[InitFeature], tlvStream: TlvStream[InitTlv] = TlvStream.empty) extends SetupMessage { - val networks = tlvStream.get[InitTlv.Networks].map(_.chainHashes).getOrElse(Nil) - val remoteAddress_opt = tlvStream.get[InitTlv.RemoteAddress].map(_.address) - val fundingRates_opt = tlvStream.get[InitTlv.OptionWillFund].map(_.rates) + val networks: Seq[BlockHash] = tlvStream.get[InitTlv.Networks].map(_.chainHashes).getOrElse(Nil) + val remoteAddress_opt: Option[NodeAddress] = tlvStream.get[InitTlv.RemoteAddress].map(_.address) + val fundingRates_opt: Option[LiquidityAds.WillFundRates] = tlvStream.get[InitTlv.OptionWillFund].map(_.rates) } case class Warning(channelId: ByteVector32, data: ByteVector, tlvStream: TlvStream[WarningTlv] = TlvStream.empty) extends SetupMessage with HasChannelId { @@ -92,6 +95,8 @@ case class TxAddInput(channelId: ByteVector32, previousTxOutput: Long, sequence: Long, tlvStream: TlvStream[TxAddInputTlv] = TlvStream.empty) extends InteractiveTxConstructionMessage with HasChannelId with HasSerialId { + /** This field may replace [[previousTx_opt]] when using taproot. */ + val previousTxOut_opt: Option[InputInfo] = tlvStream.get[TxAddInputTlv.PrevTxOut].map(tlv => InputInfo(OutPoint(tlv.txId, previousTxOutput), TxOut(tlv.amount, tlv.publicKeyScript))) val sharedInput_opt: Option[OutPoint] = tlvStream.get[TxAddInputTlv.SharedInputTxId].map(i => OutPoint(i.txId, previousTxOutput)) } @@ -116,18 +121,38 @@ case class TxRemoveOutput(channelId: ByteVector32, tlvStream: TlvStream[TxRemoveOutputTlv] = TlvStream.empty) extends InteractiveTxConstructionMessage with HasChannelId with HasSerialId case class TxComplete(channelId: ByteVector32, - tlvStream: TlvStream[TxCompleteTlv] = TlvStream.empty) extends InteractiveTxConstructionMessage with HasChannelId + tlvStream: TlvStream[TxCompleteTlv] = TlvStream.empty) extends InteractiveTxConstructionMessage with HasChannelId { + val commitNonces_opt: Option[TxCompleteTlv.CommitNonces] = tlvStream.get[TxCompleteTlv.CommitNonces] + val fundingNonce_opt: Option[IndividualNonce] = tlvStream.get[TxCompleteTlv.FundingInputNonce].map(_.nonce) +} + +object TxComplete { + def apply(channelId: ByteVector32, commitNonce: IndividualNonce, nextCommitNonce: IndividualNonce, fundingNonce_opt: Option[IndividualNonce]): TxComplete = { + val tlvs = Set( + Some(TxCompleteTlv.CommitNonces(commitNonce, nextCommitNonce)), + fundingNonce_opt.map(TxCompleteTlv.FundingInputNonce(_)), + ).flatten[TxCompleteTlv] + TxComplete(channelId, TlvStream(tlvs)) + } +} case class TxSignatures(channelId: ByteVector32, txId: TxId, witnesses: Seq[ScriptWitness], tlvStream: TlvStream[TxSignaturesTlv] = TlvStream.empty) extends InteractiveTxMessage with HasChannelId { val previousFundingTxSig_opt: Option[ByteVector64] = tlvStream.get[TxSignaturesTlv.PreviousFundingTxSig].map(_.sig) + val previousFundingTxPartialSig_opt: Option[PartialSignatureWithNonce] = tlvStream.get[TxSignaturesTlv.PreviousFundingTxPartialSig].map(_.partialSigWithNonce) } object TxSignatures { - def apply(channelId: ByteVector32, tx: Transaction, witnesses: Seq[ScriptWitness], previousFundingSig_opt: Option[ByteVector64]): TxSignatures = { - TxSignatures(channelId, tx.txid, witnesses, TlvStream(previousFundingSig_opt.map(TxSignaturesTlv.PreviousFundingTxSig).toSet[TxSignaturesTlv])) + def apply(channelId: ByteVector32, tx: Transaction, witnesses: Seq[ScriptWitness], previousFundingSig_opt: Option[ChannelSpendSignature]): TxSignatures = { + val tlvs: Set[TxSignaturesTlv] = Set( + previousFundingSig_opt.map { + case IndividualSignature(sig) => TxSignaturesTlv.PreviousFundingTxSig(sig) + case partialSig: PartialSignatureWithNonce => TxSignaturesTlv.PreviousFundingTxPartialSig(partialSig) + } + ).flatten + TxSignatures(channelId, tx.txid, witnesses, TlvStream(tlvs)) } } @@ -187,6 +212,8 @@ case class ChannelReestablish(channelId: ByteVector32, val nextFundingTxId_opt: Option[TxId] = tlvStream.get[ChannelReestablishTlv.NextFundingTlv].map(_.txId) val myCurrentFundingLocked_opt: Option[TxId] = tlvStream.get[ChannelReestablishTlv.MyCurrentFundingLockedTlv].map(_.txId) val yourLastFundingLocked_opt: Option[TxId] = tlvStream.get[ChannelReestablishTlv.YourLastFundingLockedTlv].map(_.txId) + val nextCommitNonces: Map[TxId, IndividualNonce] = tlvStream.get[ChannelReestablishTlv.NextLocalNoncesTlv].map(_.nonces.toMap).getOrElse(Map.empty) + val currentCommitNonce_opt: Option[IndividualNonce] = tlvStream.get[ChannelReestablishTlv.CurrentCommitNonceTlv].map(_.nonce) } case class OpenChannel(chainHash: BlockHash, @@ -210,6 +237,7 @@ case class OpenChannel(chainHash: BlockHash, tlvStream: TlvStream[OpenChannelTlv] = TlvStream.empty) extends ChannelMessage with HasTemporaryChannelId with HasChainHash { val upfrontShutdownScript_opt: Option[ByteVector] = tlvStream.get[ChannelTlv.UpfrontShutdownScriptTlv].map(_.script) val channelType_opt: Option[ChannelType] = tlvStream.get[ChannelTlv.ChannelTypeTlv].map(_.channelType) + val commitNonce_opt: Option[IndividualNonce] = tlvStream.get[ChannelTlv.NextLocalNonceTlv].map(_.nonce) } case class AcceptChannel(temporaryChannelId: ByteVector32, @@ -229,6 +257,7 @@ case class AcceptChannel(temporaryChannelId: ByteVector32, tlvStream: TlvStream[AcceptChannelTlv] = TlvStream.empty) extends ChannelMessage with HasTemporaryChannelId { val upfrontShutdownScript_opt: Option[ByteVector] = tlvStream.get[ChannelTlv.UpfrontShutdownScriptTlv].map(_.script) val channelType_opt: Option[ChannelType] = tlvStream.get[ChannelTlv.ChannelTypeTlv].map(_.channelType) + val commitNonce_opt: Option[IndividualNonce] = tlvStream.get[ChannelTlv.NextLocalNonceTlv].map(_.nonce) } // NB: this message is named open_channel2 in the specification. @@ -289,16 +318,64 @@ case class FundingCreated(temporaryChannelId: ByteVector32, fundingTxId: TxId, fundingOutputIndex: Int, signature: ByteVector64, - tlvStream: TlvStream[FundingCreatedTlv] = TlvStream.empty) extends ChannelMessage with HasTemporaryChannelId + tlvStream: TlvStream[FundingCreatedTlv] = TlvStream.empty) extends ChannelMessage with HasTemporaryChannelId { + val sigOrPartialSig: ChannelSpendSignature = tlvStream.get[ChannelTlv.PartialSignatureWithNonceTlv].map(_.partialSigWithNonce).getOrElse(IndividualSignature(signature)) +} + +object FundingCreated { + def apply(temporaryChannelId: ByteVector32, fundingTxId: TxId, fundingOutputIndex: Int, sig: ChannelSpendSignature): FundingCreated = { + val individualSig = sig match { + case IndividualSignature(sig) => sig + case _: PartialSignatureWithNonce => ByteVector64.Zeroes + } + val tlvs = sig match { + case _: IndividualSignature => TlvStream.empty[FundingCreatedTlv] + case psig: PartialSignatureWithNonce => TlvStream[FundingCreatedTlv](ChannelTlv.PartialSignatureWithNonceTlv(psig)) + } + FundingCreated(temporaryChannelId, fundingTxId, fundingOutputIndex, individualSig, tlvs) + } +} case class FundingSigned(channelId: ByteVector32, signature: ByteVector64, - tlvStream: TlvStream[FundingSignedTlv] = TlvStream.empty) extends ChannelMessage with HasChannelId + tlvStream: TlvStream[FundingSignedTlv] = TlvStream.empty) extends ChannelMessage with HasChannelId { + val sigOrPartialSig: ChannelSpendSignature = tlvStream.get[ChannelTlv.PartialSignatureWithNonceTlv].map(_.partialSigWithNonce).getOrElse(IndividualSignature(signature)) +} + +object FundingSigned { + def apply(channelId: ByteVector32, sig: ChannelSpendSignature): FundingSigned = { + val individualSig = sig match { + case IndividualSignature(sig) => sig + case _: PartialSignatureWithNonce => ByteVector64.Zeroes + } + val tlvs = sig match { + case _: IndividualSignature => TlvStream.empty[FundingSignedTlv] + case psig: PartialSignatureWithNonce => TlvStream[FundingSignedTlv](ChannelTlv.PartialSignatureWithNonceTlv(psig)) + } + FundingSigned(channelId, individualSig, tlvs) + } +} case class ChannelReady(channelId: ByteVector32, nextPerCommitmentPoint: PublicKey, tlvStream: TlvStream[ChannelReadyTlv] = TlvStream.empty) extends ChannelMessage with HasChannelId { val alias_opt: Option[Alias] = tlvStream.get[ShortChannelIdTlv].map(_.alias) + val nextCommitNonce_opt: Option[IndividualNonce] = tlvStream.get[ChannelTlv.NextLocalNonceTlv].map(_.nonce) +} + +object ChannelReady { + def apply(channelId: ByteVector32, nextPerCommitmentPoint: PublicKey, alias: Alias): ChannelReady = { + val tlvs = TlvStream[ChannelReadyTlv](ChannelReadyTlv.ShortChannelIdTlv(alias)) + ChannelReady(channelId, nextPerCommitmentPoint, tlvs) + } + + def apply(channelId: ByteVector32, nextPerCommitmentPoint: PublicKey, alias: Alias, nextCommitNonce: IndividualNonce): ChannelReady = { + val tlvs = TlvStream[ChannelReadyTlv]( + ChannelReadyTlv.ShortChannelIdTlv(alias), + ChannelTlv.NextLocalNonceTlv(nextCommitNonce), + ) + ChannelReady(channelId, nextPerCommitmentPoint, tlvs) + } } case class Stfu(channelId: ByteVector32, initiator: Boolean) extends SetupMessage with HasChannelId @@ -314,17 +391,22 @@ case class SpliceInit(channelId: ByteVector32, val usesOnTheFlyFunding: Boolean = requestFunding_opt.exists(_.paymentDetails.paymentType.isInstanceOf[LiquidityAds.OnTheFlyFundingPaymentType]) val useFeeCredit_opt: Option[MilliSatoshi] = tlvStream.get[ChannelTlv.UseFeeCredit].map(_.amount) val pushAmount: MilliSatoshi = tlvStream.get[ChannelTlv.PushAmountTlv].map(_.amount).getOrElse(0 msat) + val channelType_opt: Option[ChannelType] = tlvStream.get[ChannelTlv.ChannelTypeTlv].map(_.channelType) } object SpliceInit { - def apply(channelId: ByteVector32, fundingContribution: Satoshi, lockTime: Long, feerate: FeeratePerKw, fundingPubKey: PublicKey, pushAmount: MilliSatoshi, requireConfirmedInputs: Boolean, requestFunding_opt: Option[LiquidityAds.RequestFunding]): SpliceInit = { + def apply(channelId: ByteVector32, fundingContribution: Satoshi, lockTime: Long, feerate: FeeratePerKw, fundingPubKey: PublicKey, pushAmount: MilliSatoshi, requireConfirmedInputs: Boolean, requestFunding_opt: Option[LiquidityAds.RequestFunding], channelType_opt: Option[ChannelType]): SpliceInit = { val tlvs: Set[SpliceInitTlv] = Set( if (pushAmount > 0.msat) Some(ChannelTlv.PushAmountTlv(pushAmount)) else None, if (requireConfirmedInputs) Some(ChannelTlv.RequireConfirmedInputsTlv()) else None, - requestFunding_opt.map(ChannelTlv.RequestFundingTlv) + requestFunding_opt.map(ChannelTlv.RequestFundingTlv), + channelType_opt.map(ChannelTlv.ChannelTypeTlv), ).flatten SpliceInit(channelId, fundingContribution, feerate, lockTime, fundingPubKey, TlvStream(tlvs)) } + + def apply(channelId: ByteVector32, fundingContribution: Satoshi, lockTime: Long, feerate: FeeratePerKw, fundingPubKey: PublicKey, pushAmount: MilliSatoshi, requireConfirmedInputs: Boolean, requestFunding_opt: Option[LiquidityAds.RequestFunding]): SpliceInit = + apply(channelId, fundingContribution, lockTime, feerate, fundingPubKey, pushAmount, requireConfirmedInputs, requestFunding_opt, None) } case class SpliceAck(channelId: ByteVector32, @@ -334,18 +416,23 @@ case class SpliceAck(channelId: ByteVector32, val requireConfirmedInputs: Boolean = tlvStream.get[ChannelTlv.RequireConfirmedInputsTlv].nonEmpty val willFund_opt: Option[LiquidityAds.WillFund] = tlvStream.get[ChannelTlv.ProvideFundingTlv].map(_.willFund) val pushAmount: MilliSatoshi = tlvStream.get[ChannelTlv.PushAmountTlv].map(_.amount).getOrElse(0 msat) + val channelType_opt: Option[ChannelType] = tlvStream.get[ChannelTlv.ChannelTypeTlv].map(_.channelType) } object SpliceAck { - def apply(channelId: ByteVector32, fundingContribution: Satoshi, fundingPubKey: PublicKey, pushAmount: MilliSatoshi, requireConfirmedInputs: Boolean, willFund_opt: Option[LiquidityAds.WillFund], feeCreditUsed_opt: Option[MilliSatoshi]): SpliceAck = { + def apply(channelId: ByteVector32, fundingContribution: Satoshi, fundingPubKey: PublicKey, pushAmount: MilliSatoshi, requireConfirmedInputs: Boolean, willFund_opt: Option[LiquidityAds.WillFund], feeCreditUsed_opt: Option[MilliSatoshi], channelType_opt: Option[ChannelType]): SpliceAck = { val tlvs: Set[SpliceAckTlv] = Set( if (pushAmount > 0.msat) Some(ChannelTlv.PushAmountTlv(pushAmount)) else None, if (requireConfirmedInputs) Some(ChannelTlv.RequireConfirmedInputsTlv()) else None, willFund_opt.map(ChannelTlv.ProvideFundingTlv), feeCreditUsed_opt.map(ChannelTlv.FeeCreditUsedTlv), + channelType_opt.map(ChannelTlv.ChannelTypeTlv), ).flatten SpliceAck(channelId, fundingContribution, fundingPubKey, TlvStream(tlvs)) } + + def apply(channelId: ByteVector32, fundingContribution: Satoshi, fundingPubKey: PublicKey, pushAmount: MilliSatoshi, requireConfirmedInputs: Boolean, willFund_opt: Option[LiquidityAds.WillFund], feeCreditUsed_opt: Option[MilliSatoshi]): SpliceAck = + apply(channelId, fundingContribution, fundingPubKey, pushAmount, requireConfirmedInputs, willFund_opt, feeCreditUsed_opt, None) } case class SpliceLocked(channelId: ByteVector32, @@ -355,25 +442,38 @@ case class SpliceLocked(channelId: ByteVector32, case class Shutdown(channelId: ByteVector32, scriptPubKey: ByteVector, - tlvStream: TlvStream[ShutdownTlv] = TlvStream.empty) extends ChannelMessage with HasChannelId with ForbiddenMessageWhenQuiescent + tlvStream: TlvStream[ShutdownTlv] = TlvStream.empty) extends ChannelMessage with HasChannelId with ForbiddenMessageWhenQuiescent { + val closeeNonce_opt: Option[IndividualNonce] = tlvStream.get[ShutdownTlv.ShutdownNonce].map(_.nonce) +} + +object Shutdown { + def apply(channelId: ByteVector32, scriptPubKey: ByteVector, closeeNonce: IndividualNonce): Shutdown = Shutdown(channelId, scriptPubKey, TlvStream[ShutdownTlv](ShutdownTlv.ShutdownNonce(closeeNonce))) +} case class ClosingSigned(channelId: ByteVector32, feeSatoshis: Satoshi, signature: ByteVector64, tlvStream: TlvStream[ClosingSignedTlv] = TlvStream.empty) extends ChannelMessage with HasChannelId { - val feeRange_opt = tlvStream.get[ClosingSignedTlv.FeeRange] + val feeRange_opt: Option[ClosingSignedTlv.FeeRange] = tlvStream.get[ClosingSignedTlv.FeeRange] } -case class ClosingComplete(channelId: ByteVector32, closerScriptPubKey: ByteVector, closeeScriptPubKey: ByteVector, fees: Satoshi, lockTime: Long, tlvStream: TlvStream[ClosingTlv] = TlvStream.empty) extends ChannelMessage with HasChannelId { +case class ClosingComplete(channelId: ByteVector32, closerScriptPubKey: ByteVector, closeeScriptPubKey: ByteVector, fees: Satoshi, lockTime: Long, tlvStream: TlvStream[ClosingCompleteTlv] = TlvStream.empty) extends ChannelMessage with HasChannelId { val closerOutputOnlySig_opt: Option[ByteVector64] = tlvStream.get[ClosingTlv.CloserOutputOnly].map(_.sig) val closeeOutputOnlySig_opt: Option[ByteVector64] = tlvStream.get[ClosingTlv.CloseeOutputOnly].map(_.sig) val closerAndCloseeOutputsSig_opt: Option[ByteVector64] = tlvStream.get[ClosingTlv.CloserAndCloseeOutputs].map(_.sig) + val closerOutputOnlyPartialSig_opt: Option[PartialSignatureWithNonce] = tlvStream.get[ClosingCompleteTlv.CloserOutputOnlyPartialSignature].map(_.partialSignature) + val closeeOutputOnlyPartialSig_opt: Option[PartialSignatureWithNonce] = tlvStream.get[ClosingCompleteTlv.CloseeOutputOnlyPartialSignature].map(_.partialSignature) + val closerAndCloseeOutputsPartialSig_opt: Option[PartialSignatureWithNonce] = tlvStream.get[ClosingCompleteTlv.CloserAndCloseeOutputsPartialSignature].map(_.partialSignature) } -case class ClosingSig(channelId: ByteVector32, closerScriptPubKey: ByteVector, closeeScriptPubKey: ByteVector, fees: Satoshi, lockTime: Long, tlvStream: TlvStream[ClosingTlv] = TlvStream.empty) extends ChannelMessage with HasChannelId { +case class ClosingSig(channelId: ByteVector32, closerScriptPubKey: ByteVector, closeeScriptPubKey: ByteVector, fees: Satoshi, lockTime: Long, tlvStream: TlvStream[ClosingSigTlv] = TlvStream.empty) extends ChannelMessage with HasChannelId { val closerOutputOnlySig_opt: Option[ByteVector64] = tlvStream.get[ClosingTlv.CloserOutputOnly].map(_.sig) val closeeOutputOnlySig_opt: Option[ByteVector64] = tlvStream.get[ClosingTlv.CloseeOutputOnly].map(_.sig) val closerAndCloseeOutputsSig_opt: Option[ByteVector64] = tlvStream.get[ClosingTlv.CloserAndCloseeOutputs].map(_.sig) + val closerOutputOnlyPartialSig_opt: Option[ByteVector32] = tlvStream.get[ClosingSigTlv.CloserOutputOnlyPartialSignature].map(_.partialSignature) + val closeeOutputOnlyPartialSig_opt: Option[ByteVector32] = tlvStream.get[ClosingSigTlv.CloseeOutputOnlyPartialSignature].map(_.partialSignature) + val closerAndCloseeOutputsPartialSig_opt: Option[ByteVector32] = tlvStream.get[ClosingSigTlv.CloserAndCloseeOutputsPartialSignature].map(_.partialSignature) + val nextCloseeNonce_opt: Option[IndividualNonce] = tlvStream.get[ClosingSigTlv.NextCloseeNonce].map(_.nonce) } case class UpdateAddHtlc(channelId: ByteVector32, @@ -400,12 +500,12 @@ object UpdateAddHtlc { cltvExpiry: CltvExpiry, onionRoutingPacket: OnionRoutingPacket, pathKey_opt: Option[PublicKey], - confidence: Double, + endorsement: Int, fundingFee_opt: Option[LiquidityAds.FundingFee]): UpdateAddHtlc = { val tlvs = Set( pathKey_opt.map(UpdateAddHtlcTlv.PathKey), fundingFee_opt.map(UpdateAddHtlcTlv.FundingFeeTlv), - Some(UpdateAddHtlcTlv.Endorsement((confidence * 7.999).toInt)), + Some(UpdateAddHtlcTlv.Endorsement(endorsement)), ).flatten[UpdateAddHtlcTlv] UpdateAddHtlc(channelId, id, amountMsat, paymentHash, cltvExpiry, onionRoutingPacket, TlvStream(tlvs)) } @@ -414,12 +514,16 @@ object UpdateAddHtlc { case class UpdateFulfillHtlc(channelId: ByteVector32, id: Long, paymentPreimage: ByteVector32, - tlvStream: TlvStream[UpdateFulfillHtlcTlv] = TlvStream.empty) extends HtlcMessage with UpdateMessage with HasChannelId with HtlcSettlementMessage + tlvStream: TlvStream[UpdateFulfillHtlcTlv] = TlvStream.empty) extends HtlcMessage with UpdateMessage with HasChannelId with HtlcSettlementMessage { + val attribution_opt: Option[ByteVector] = tlvStream.get[UpdateFulfillHtlcTlv.AttributionData].map(_.data) +} case class UpdateFailHtlc(channelId: ByteVector32, id: Long, reason: ByteVector, - tlvStream: TlvStream[UpdateFailHtlcTlv] = TlvStream.empty) extends HtlcMessage with UpdateMessage with HasChannelId with HtlcFailureMessage + tlvStream: TlvStream[UpdateFailHtlcTlv] = TlvStream.empty) extends HtlcMessage with UpdateMessage with HasChannelId with HtlcFailureMessage { + val attribution_opt: Option[ByteVector] = tlvStream.get[UpdateFailHtlcTlv.AttributionData].map(_.data) +} case class UpdateFailMalformedHtlc(channelId: ByteVector32, id: Long, @@ -427,17 +531,59 @@ case class UpdateFailMalformedHtlc(channelId: ByteVector32, failureCode: Int, tlvStream: TlvStream[UpdateFailMalformedHtlcTlv] = TlvStream.empty) extends HtlcMessage with UpdateMessage with HasChannelId with HtlcFailureMessage +/** + * [[CommitSig]] can either be sent individually or as part of a batch. When sent in a batch (which happens when there + * are pending splice transactions), we treat the whole batch as a single lightning message and group them on the wire. + */ +sealed trait CommitSigs extends HtlcMessage with HasChannelId + +object CommitSigs { + def apply(sigs: Seq[CommitSig]): CommitSigs = if (sigs.size == 1) sigs.head else CommitSigBatch(sigs) +} + case class CommitSig(channelId: ByteVector32, - signature: ByteVector64, + signature: IndividualSignature, htlcSignatures: List[ByteVector64], - tlvStream: TlvStream[CommitSigTlv] = TlvStream.empty) extends HtlcMessage with HasChannelId { - val batchSize: Int = tlvStream.get[CommitSigTlv.BatchTlv].map(_.size).getOrElse(1) + tlvStream: TlvStream[CommitSigTlv] = TlvStream.empty) extends CommitSigs { + val partialSignature_opt: Option[PartialSignatureWithNonce] = tlvStream.get[CommitSigTlv.PartialSignatureWithNonceTlv].map(_.partialSigWithNonce) + val sigOrPartialSig: ChannelSpendSignature = partialSignature_opt.getOrElse(signature) +} + +object CommitSig { + def apply(channelId: ByteVector32, signature: ChannelSpendSignature, htlcSignatures: List[ByteVector64], batchSize: Int): CommitSig = { + val (individualSig, partialSig_opt) = signature match { + case sig: IndividualSignature => (sig, None) + case psig: PartialSignatureWithNonce => (IndividualSignature(ByteVector64.Zeroes), Some(psig)) + } + val tlvs = Set( + if (batchSize > 1) Some(CommitSigTlv.BatchTlv(batchSize)) else None, + partialSig_opt.map(CommitSigTlv.PartialSignatureWithNonceTlv(_)) + ).flatten[CommitSigTlv] + CommitSig(channelId, individualSig, htlcSignatures, TlvStream(tlvs)) + } +} + +case class CommitSigBatch(messages: Seq[CommitSig]) extends CommitSigs { + require(messages.map(_.channelId).toSet.size == 1, "commit_sig messages in a batch must be for the same channel") + val channelId: ByteVector32 = messages.head.channelId + val batchSize: Int = messages.size } case class RevokeAndAck(channelId: ByteVector32, perCommitmentSecret: PrivateKey, nextPerCommitmentPoint: PublicKey, - tlvStream: TlvStream[RevokeAndAckTlv] = TlvStream.empty) extends HtlcMessage with HasChannelId + tlvStream: TlvStream[RevokeAndAckTlv] = TlvStream.empty) extends HtlcMessage with HasChannelId { + val nextCommitNonces: Map[TxId, IndividualNonce] = tlvStream.get[RevokeAndAckTlv.NextLocalNoncesTlv].map(_.nonces.toMap).getOrElse(Map.empty) +} + +object RevokeAndAck { + def apply(channelId: ByteVector32, perCommitmentSecret: PrivateKey, nextPerCommitmentPoint: PublicKey, nextCommitNonces: Seq[(TxId, IndividualNonce)]): RevokeAndAck = { + val tlvs = Set( + if (nextCommitNonces.nonEmpty) Some(RevokeAndAckTlv.NextLocalNoncesTlv(nextCommitNonces)) else None + ).flatten[RevokeAndAckTlv] + RevokeAndAck(channelId, perCommitmentSecret, nextPerCommitmentPoint, TlvStream(tlvs)) + } +} case class UpdateFee(channelId: ByteVector32, feeratePerKw: FeeratePerKw, @@ -533,7 +679,7 @@ case class NodeAnnouncement(signature: ByteVector64, alias: String, addresses: List[NodeAddress], tlvStream: TlvStream[NodeAnnouncementTlv] = TlvStream.empty) extends RoutingMessage with AnnouncementMessage with HasTimestamp { - val fundingRates_opt = tlvStream.get[NodeAnnouncementTlv.OptionWillFund].map(_.rates) + val fundingRates_opt: Option[LiquidityAds.WillFundRates] = tlvStream.get[NodeAnnouncementTlv.OptionWillFund].map(_.rates) val validAddresses: List[NodeAddress] = { // if port is equal to 0, SHOULD ignore ipv6_addr OR ipv4_addr OR hostname; SHOULD ignore Tor v2 onion services. val validAddresses = addresses.filter(address => address.port != 0 || address.isInstanceOf[Tor3]).filterNot(address => address.isInstanceOf[Tor2]) @@ -685,7 +831,9 @@ object WillAddHtlc { } /** This message is similar to [[UpdateFailHtlc]], but for [[WillAddHtlc]]. */ -case class WillFailHtlc(id: ByteVector32, paymentHash: ByteVector32, reason: ByteVector) extends OnTheFlyFundingFailureMessage +case class WillFailHtlc(id: ByteVector32, paymentHash: ByteVector32, reason: ByteVector, tlvStream: TlvStream[UpdateFailHtlcTlv] = TlvStream.empty) extends OnTheFlyFundingFailureMessage { + val attribution_opt: Option[ByteVector] = tlvStream.get[UpdateFailHtlcTlv.AttributionData].map(_.data) +} /** This message is similar to [[UpdateFailMalformedHtlc]], but for [[WillAddHtlc]]. */ case class WillFailMalformedHtlc(id: ByteVector32, paymentHash: ByteVector32, onionHash: ByteVector32, failureCode: Int) extends OnTheFlyFundingFailureMessage diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/OfferCodecs.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/OfferCodecs.scala index 5b8604bc26..73e3d1e37e 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/OfferCodecs.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/OfferCodecs.scala @@ -16,28 +16,35 @@ package fr.acinq.eclair.wire.protocol -import fr.acinq.bitcoin.scalacompat.BlockHash import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.eclair.crypto.Sphinx.RouteBlinding.{BlindedHop, BlindedRoute} import fr.acinq.eclair.wire.protocol.CommonCodecs._ import fr.acinq.eclair.wire.protocol.OfferTypes._ import fr.acinq.eclair.wire.protocol.TlvCodecs.{tlvField, tmillisatoshi, tu32, tu64overflow} import fr.acinq.eclair.{EncodedNodeId, TimestampSecond, UInt64} -import scodec.Codec +import scodec.{Attempt, Codec} import scodec.codecs._ +import java.util.Currency +import scala.util.Try + object OfferCodecs { - private val offerChains: Codec[OfferChains] = tlvField(list(blockHash).xmap[Seq[BlockHash]](_.toSeq, _.toList)) + private val offerChains: Codec[OfferChains] = tlvField(nonEmptyList(blockHash, "offer_chains")) private val offerMetadata: Codec[OfferMetadata] = tlvField(bytes) - private val offerCurrency: Codec[OfferCurrency] = tlvField(utf8) + val offerCurrency: Codec[OfferCurrency] = + tlvField(utf8.narrow[Currency](s => Attempt.fromTry(Try{ + val c = Currency.getInstance(s) + require(c.getDefaultFractionDigits() >= 0) // getDefaultFractionDigits may return -1 for things that are not currencies + c + }), _.getCurrencyCode())) - private val offerAmount: Codec[OfferAmount] = tlvField(tmillisatoshi) + private val offerAmount: Codec[OfferAmount] = tlvField(tu64overflow) private val offerDescription: Codec[OfferDescription] = tlvField(utf8) - private val offerFeatures: Codec[OfferFeatures] = tlvField(featuresCodec) + private val offerFeatures: Codec[OfferFeatures] = tlvField(bytes) private val offerAbsoluteExpiry: Codec[OfferAbsoluteExpiry] = tlvField(tu64overflow.as[TimestampSecond]) @@ -68,7 +75,7 @@ object OfferCodecs { ("firstPathKey" | publicKey) :: ("path" | blindedNodesCodec)).as[BlindedRoute] - private val offerPaths: Codec[OfferPaths] = tlvField(list(blindedRouteCodec).xmap[Seq[BlindedRoute]](_.toSeq, _.toList)) + private val offerPaths: Codec[OfferPaths] = tlvField(nonEmptyList(blindedRouteCodec, "offer_paths")) private val offerIssuer: Codec[OfferIssuer] = tlvField(utf8) @@ -96,7 +103,7 @@ object OfferCodecs { private val invoiceRequestAmount: Codec[InvoiceRequestAmount] = tlvField(tmillisatoshi) - private val invoiceRequestFeatures: Codec[InvoiceRequestFeatures] = tlvField(featuresCodec) + private val invoiceRequestFeatures: Codec[InvoiceRequestFeatures] = tlvField(bytes) private val invoiceRequestQuantity: Codec[InvoiceRequestQuantity] = tlvField(tu64overflow) @@ -130,7 +137,7 @@ object OfferCodecs { .typecase(UInt64(240), signature) ).complete) - private val invoicePaths: Codec[InvoicePaths] = tlvField(list(blindedRouteCodec).xmap[Seq[BlindedRoute]](_.toSeq, _.toList)) + private val invoicePaths: Codec[InvoicePaths] = tlvField(nonEmptyList(blindedRouteCodec, "invoice_paths")) val paymentInfo: Codec[PaymentInfo] = (("fee_base_msat" | millisatoshi32) :: @@ -138,9 +145,9 @@ object OfferCodecs { ("cltv_expiry_delta" | cltvExpiryDelta) :: ("htlc_minimum_msat" | millisatoshi) :: ("htlc_maximum_msat" | millisatoshi) :: - ("features" | lengthPrefixedFeaturesCodec)).as[PaymentInfo] + ("features" | variableSizeBytes(uint16, bytes))).as[PaymentInfo] - private val invoiceBlindedPay: Codec[InvoiceBlindedPay] = tlvField(list(paymentInfo).xmap[Seq[PaymentInfo]](_.toSeq, _.toList)) + private val invoiceBlindedPay: Codec[InvoiceBlindedPay] = tlvField(nonEmptyList(paymentInfo, "invoice_blindedpay")) private val invoiceCreatedAt: Codec[InvoiceCreatedAt] = tlvField(tu64overflow.as[TimestampSecond]) @@ -154,7 +161,7 @@ object OfferCodecs { private val invoiceFallbacks: Codec[InvoiceFallbacks] = tlvField(list(fallbackAddress).xmap[Seq[FallbackAddress]](_.toSeq, _.toList)) - private val invoiceFeatures: Codec[InvoiceFeatures] = tlvField(featuresCodec) + private val invoiceFeatures: Codec[InvoiceFeatures] = tlvField(bytes) private val invoiceNodeId: Codec[InvoiceNodeId] = tlvField(publicKey) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/OfferTypes.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/OfferTypes.scala index 519ae90e1b..aa64f183e7 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/OfferTypes.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/OfferTypes.scala @@ -17,17 +17,18 @@ package fr.acinq.eclair.wire.protocol import fr.acinq.bitcoin.Bech32 -import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey, XonlyPublicKey} +import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey, SchnorrTweak, XonlyPublicKey} import fr.acinq.bitcoin.scalacompat.{Block, BlockHash, ByteVector32, ByteVector64, Crypto, LexicographicalOrdering} import fr.acinq.eclair.crypto.Sphinx.RouteBlinding.BlindedRoute import fr.acinq.eclair.wire.protocol.CommonCodecs.varint import fr.acinq.eclair.wire.protocol.OnionRoutingCodecs.{ForbiddenTlv, InvalidTlvPayload, MissingRequiredTlv} import fr.acinq.eclair.wire.protocol.TlvCodecs.genericTlv -import fr.acinq.eclair.{Bolt12Feature, CltvExpiryDelta, Feature, Features, MilliSatoshi, TimestampSecond, UInt64, nodeFee, randomBytes32} +import fr.acinq.eclair.{Bolt12Feature, CltvExpiryDelta, Features, MilliSatoshi, MilliSatoshiLong, TimestampSecond, UInt64, nodeFee, randomBytes32} import scodec.Codec import scodec.bits.ByteVector import scodec.codecs.vector +import java.util.Currency import scala.util.{Failure, Try} /** @@ -71,12 +72,12 @@ object OfferTypes { /** * Three-letter code of the currency the offer is denominated in. If empty, bitcoin is implied. */ - case class OfferCurrency(iso4217: String) extends OfferTlv + case class OfferCurrency(currency: Currency) extends OfferTlv /** - * Amount to pay per item. As we only support bitcoin, the amount is in msat. + * Amount to pay per item. */ - case class OfferAmount(amount: MilliSatoshi) extends OfferTlv + case class OfferAmount(amount: Long) extends OfferTlv /** * Description of the purpose of the payment. @@ -86,7 +87,7 @@ object OfferTypes { /** * Features supported to pay the offer. */ - case class OfferFeatures(features: Features[Feature]) extends OfferTlv + case class OfferFeatures(features: ByteVector) extends OfferTlv /** * Time after which the offer is no longer valid. @@ -135,7 +136,7 @@ object OfferTypes { /** * Features supported by the sender to pay the offer. */ - case class InvoiceRequestFeatures(features: Features[Feature]) extends InvoiceRequestTlv + case class InvoiceRequestFeatures(features: ByteVector) extends InvoiceRequestTlv /** * Number of items to purchase. Only use if the offer supports purchasing multiple items at once. @@ -163,7 +164,7 @@ object OfferTypes { cltvExpiryDelta: CltvExpiryDelta, minHtlc: MilliSatoshi, maxHtlc: MilliSatoshi, - allowedFeatures: Features[Feature]) { + allowedFeatures: ByteVector) { def fee(amount: MilliSatoshi): MilliSatoshi = nodeFee(feeBase, feeProportionalMillionths, amount) } @@ -202,7 +203,7 @@ object OfferTypes { /** * Features supported to pay the invoice. */ - case class InvoiceFeatures(features: Features[Feature]) extends InvoiceTlv + case class InvoiceFeatures(features: ByteVector) extends InvoiceTlv /** * Public key of the invoice recipient. @@ -238,9 +239,9 @@ object OfferTypes { case class Offer(records: TlvStream[OfferTlv]) { val chains: Seq[BlockHash] = records.get[OfferChains].map(_.chains).getOrElse(Seq(Block.LivenetGenesisBlock.hash)) val metadata: Option[ByteVector] = records.get[OfferMetadata].map(_.data) - val amount: Option[MilliSatoshi] = records.get[OfferAmount].map(_.amount) + val amount: Option[MilliSatoshi] = if (records.get[OfferCurrency].isEmpty) records.get[OfferAmount].map(_.amount.msat) else None val description: Option[String] = records.get[OfferDescription].map(_.description) - val features: Features[Bolt12Feature] = records.get[OfferFeatures].map(_.features.bolt12Features()).getOrElse(Features.empty) + val features: Features[Bolt12Feature] = records.get[OfferFeatures].map(f => Features(f.features).bolt12Features()).getOrElse(Features.empty) val expiry: Option[TimestampSecond] = records.get[OfferAbsoluteExpiry].map(_.absoluteExpiry) private val paths: Option[Seq[BlindedPath]] = records.get[OfferPaths].map(_.paths.map(BlindedPath)) val issuer: Option[String] = records.get[OfferIssuer].map(_.issuer) @@ -279,9 +280,9 @@ object OfferTypes { require(amount_opt.isEmpty || description_opt.nonEmpty) val tlvs: Set[OfferTlv] = Set( if (chain != Block.LivenetGenesisBlock.hash) Some(OfferChains(Seq(chain))) else None, - amount_opt.map(OfferAmount), + amount_opt.map(_.toLong).map(OfferAmount), description_opt.map(OfferDescription), - if (!features.isEmpty) Some(OfferFeatures(features.unscoped())) else None, + if (!features.isEmpty) Some(OfferFeatures(features.unscoped().toByteVector)) else None, Some(OfferNodeId(nodeId)), ).flatten ++ additionalTlvs Offer(TlvStream(tlvs, customTlvs)) @@ -297,9 +298,9 @@ object OfferTypes { require(amount_opt.isEmpty || description_opt.nonEmpty) val tlvs: Set[OfferTlv] = Set( if (chain != Block.LivenetGenesisBlock.hash) Some(OfferChains(Seq(chain))) else None, - amount_opt.map(OfferAmount), + amount_opt.map(_.toLong).map(OfferAmount), description_opt.map(OfferDescription), - if (!features.isEmpty) Some(OfferFeatures(features.unscoped())) else None, + if (!features.isEmpty) Some(OfferFeatures(features.unscoped().toByteVector)) else None, Some(OfferPaths(paths)) ).flatten ++ additionalTlvs Offer(TlvStream(tlvs, customTlvs)) @@ -307,15 +308,15 @@ object OfferTypes { def validate(records: TlvStream[OfferTlv]): Either[InvalidTlvPayload, Offer] = { if (records.get[OfferDescription].isEmpty && records.get[OfferAmount].nonEmpty) return Left(MissingRequiredTlv(UInt64(10))) - if (records.get[OfferNodeId].isEmpty && records.get[OfferPaths].forall(_.paths.isEmpty)) return Left(MissingRequiredTlv(UInt64(22))) - // Currency conversion isn't supported yet. - if (records.get[OfferCurrency].nonEmpty) return Left(ForbiddenTlv(UInt64(6))) + if (records.get[OfferNodeId].isEmpty && records.get[OfferPaths].isEmpty) return Left(MissingRequiredTlv(UInt64(22))) + if (records.get[OfferCurrency].nonEmpty && records.get[OfferAmount].isEmpty) return Left(MissingRequiredTlv(UInt64(8))) if (records.unknown.exists(!isOfferTlv(_))) return Left(ForbiddenTlv(records.unknown.find(!isOfferTlv(_)).get.tag)) Right(Offer(records)) } /** * 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. + * * @return a lowercase string with '+' and whitespaces removed */ private def validateFormat(s: String): String = { @@ -348,7 +349,7 @@ object OfferTypes { val metadata: ByteVector = records.get[InvoiceRequestMetadata].get.data val chain: BlockHash = records.get[InvoiceRequestChain].map(_.hash).getOrElse(Block.LivenetGenesisBlock.hash) private val amount_opt: Option[MilliSatoshi] = records.get[InvoiceRequestAmount].map(_.amount) - val features: Features[Bolt12Feature] = records.get[InvoiceRequestFeatures].map(_.features.bolt12Features()).getOrElse(Features.empty) + val features: Features[Bolt12Feature] = records.get[InvoiceRequestFeatures].map(f => Features(f.features).bolt12Features()).getOrElse(Features.empty) val quantity_opt: Option[Long] = records.get[InvoiceRequestQuantity].map(_.quantity) val quantity: Long = quantity_opt.getOrElse(1) private val baseInvoiceAmount_opt = offer.amount.map(_ * quantity) @@ -409,7 +410,7 @@ object OfferTypes { Some(InvoiceRequestChain(chain)), Some(InvoiceRequestAmount(amount)), if (offer.quantityMax.nonEmpty) Some(InvoiceRequestQuantity(quantity)) else None, - if (!features.isEmpty) Some(InvoiceRequestFeatures(features.unscoped())) else None, + if (!features.isEmpty) Some(InvoiceRequestFeatures(features.unscoped().toByteVector)) else None, Some(InvoiceRequestPayerId(payerKey.publicKey)), ).flatten ++ additionalTlvs val signature = signSchnorr(signatureTag, rootHash(TlvStream(tlvs, offer.records.unknown ++ customTlvs), OfferCodecs.invoiceRequestTlvCodec), payerKey) @@ -445,7 +446,7 @@ object OfferTypes { } case class InvoiceError(records: TlvStream[InvoiceErrorTlv]) { - val error = records.get[Error].get.message + val error: String = records.get[Error].get.message } object InvoiceError { @@ -499,7 +500,7 @@ object OfferTypes { def signSchnorr(tag: ByteVector, msg: ByteVector32, key: PrivateKey): ByteVector64 = { val h = hash(tag, msg) // NB: we don't add auxiliary random data to keep signatures deterministic. - Crypto.signSchnorr(h, key, fr.acinq.bitcoin.Crypto.SchnorrTweak.NoTweak.INSTANCE) + Crypto.signSchnorr(h, key, SchnorrTweak.NoTweak) } def verifySchnorr(tag: ByteVector, msg: ByteVector32, signature: ByteVector64, publicKey: PublicKey): Boolean = { diff --git a/eclair-core/src/test/resources/bolt3-tx-test-vectors-default-commitment-format.txt b/eclair-core/src/test/resources/bolt3-tx-test-vectors-default-commitment-format.txt deleted file mode 100644 index e4c7148f9e..0000000000 --- a/eclair-core/src/test/resources/bolt3-tx-test-vectors-default-commitment-format.txt +++ /dev/null @@ -1,367 +0,0 @@ - name: simple commitment tx with no HTLCs - to_local_msat: 7000000000 - to_remote_msat: 3000000000 - local_feerate_per_kw: 15000 - # base commitment transaction fee = 10860 - # actual commitment transaction fee = 10860 - # to_local amount 6989140 wscript 63210212a140cd0c6539d07cd08dfe09984dec3251ea808b892efeac3ede9402bf2b1967029000b2752103fd5960528dc152014952efdb702a88f71e3c1653b2314431701ec77e57fde83c68ac - # to_remote amount 3000000 P2WPKH(0394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b) - remote_signature = 3045022100f51d2e566a70ba740fc5d8c0f07b9b93d2ed741c3c0860c613173de7d39e7968022041376d520e9c0e1ad52248ddf4b22e12be8763007df977253ef45a4ca3bdb7c0 - # local_signature = 3044022051b75c73198c6deee1a875871c3961832909acd297c6b908d59e3319e5185a46022055c419379c5051a78d00dbbce11b5b664a0c22815fbcc6fcef6b1937c3836939 - output commit_tx: 02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b8002c0c62d0000000000160014ccf1af2f2aabee14bb40fa3851ab2301de84311054a56a00000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0400473044022051b75c73198c6deee1a875871c3961832909acd297c6b908d59e3319e5185a46022055c419379c5051a78d00dbbce11b5b664a0c22815fbcc6fcef6b1937c383693901483045022100f51d2e566a70ba740fc5d8c0f07b9b93d2ed741c3c0860c613173de7d39e7968022041376d520e9c0e1ad52248ddf4b22e12be8763007df977253ef45a4ca3bdb7c001475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220 - num_htlcs: 0 - - name: commitment tx with all five HTLCs untrimmed (minimum feerate) - to_local_msat: 6988000000 - to_remote_msat: 3000000000 - local_feerate_per_kw: 0 - # base commitment transaction fee = 0 - # actual commitment transaction fee = 0 - # HTLC #2 offered amount 2000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a914b43e1b38138a41b37f7cd9a1d274bc63e3a9b5d188ac6868 - # HTLC #3 offered amount 3000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9148a486ff2e31d6158bf39e2608864d63fefd09d5b88ac6868 - # HTLC #0 received amount 1000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a914b8bcb07f6344b42ab04250c86a6e8b75d3fdbbc688527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f401b175ac6868 - # HTLC #1 received amount 2000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a9144b6b2e5444c2639cc0fb7bcea5afba3f3cdce23988527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f501b175ac6868 - # HTLC #4 received amount 4000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac6868 - # to_local amount 6988000 wscript 63210212a140cd0c6539d07cd08dfe09984dec3251ea808b892efeac3ede9402bf2b1967029000b2752103fd5960528dc152014952efdb702a88f71e3c1653b2314431701ec77e57fde83c68ac - # to_remote amount 3000000 P2WPKH(0394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b) - remote_signature = 304402204fd4928835db1ccdfc40f5c78ce9bd65249b16348df81f0c44328dcdefc97d630220194d3869c38bc732dd87d13d2958015e2fc16829e74cd4377f84d215c0b70606 - # local_signature = 30440220275b0c325a5e9355650dc30c0eccfbc7efb23987c24b556b9dfdd40effca18d202206caceb2c067836c51f296740c7ae807ffcbfbf1dd3a0d56b6de9a5b247985f06 - output commit_tx: 02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b8007e80300000000000022002052bfef0479d7b293c27e0f1eb294bea154c63a3294ef092c19af51409bce0e2ad007000000000000220020403d394747cae42e98ff01734ad5c08f82ba123d3d9a620abda88989651e2ab5d007000000000000220020748eba944fedc8827f6b06bc44678f93c0f9e6078b35c6331ed31e75f8ce0c2db80b000000000000220020c20b5d1f8584fd90443e7b7b720136174fa4b9333c261d04dbbd012635c0f419a00f0000000000002200208c48d15160397c9731df9bc3b236656efb6665fbfe92b4a6878e88a499f741c4c0c62d0000000000160014ccf1af2f2aabee14bb40fa3851ab2301de843110e0a06a00000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e04004730440220275b0c325a5e9355650dc30c0eccfbc7efb23987c24b556b9dfdd40effca18d202206caceb2c067836c51f296740c7ae807ffcbfbf1dd3a0d56b6de9a5b247985f060147304402204fd4928835db1ccdfc40f5c78ce9bd65249b16348df81f0c44328dcdefc97d630220194d3869c38bc732dd87d13d2958015e2fc16829e74cd4377f84d215c0b7060601475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220 - num_htlcs: 5 - # signature for output #0 (htlc-success for htlc #0) - remote_htlc_signature = 304402206a6e59f18764a5bf8d4fa45eebc591566689441229c918b480fb2af8cc6a4aeb02205248f273be447684b33e3c8d1d85a8e0ca9fa0bae9ae33f0527ada9c162919a6 - # local_htlc_signature = 304402207cb324fa0de88f452ffa9389678127ebcf4cabe1dd848b8e076c1a1962bf34720220116ed922b12311bd602d67e60d2529917f21c5b82f25ff6506c0f87886b4dfd5 - htlc_success_tx (htlc #0): 020000000001018154ecccf11a5fb56c39654c4deb4d2296f83c69268280b94d021370c94e219700000000000000000001e8030000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e050047304402206a6e59f18764a5bf8d4fa45eebc591566689441229c918b480fb2af8cc6a4aeb02205248f273be447684b33e3c8d1d85a8e0ca9fa0bae9ae33f0527ada9c162919a60147304402207cb324fa0de88f452ffa9389678127ebcf4cabe1dd848b8e076c1a1962bf34720220116ed922b12311bd602d67e60d2529917f21c5b82f25ff6506c0f87886b4dfd5012000000000000000000000000000000000000000000000000000000000000000008a76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a914b8bcb07f6344b42ab04250c86a6e8b75d3fdbbc688527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f401b175ac686800000000 - # signature for output #1 (htlc-timeout for htlc #2) - remote_htlc_signature = 3045022100d5275b3619953cb0c3b5aa577f04bc512380e60fa551762ce3d7a1bb7401cff9022037237ab0dac3fe100cde094e82e2bed9ba0ed1bb40154b48e56aa70f259e608b - # local_htlc_signature = 3045022100c89172099507ff50f4c925e6c5150e871fb6e83dd73ff9fbb72f6ce829a9633f02203a63821d9162e99f9be712a68f9e589483994feae2661e4546cd5b6cec007be5 - htlc_timeout_tx (htlc #2): 020000000001018154ecccf11a5fb56c39654c4deb4d2296f83c69268280b94d021370c94e219701000000000000000001d0070000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100d5275b3619953cb0c3b5aa577f04bc512380e60fa551762ce3d7a1bb7401cff9022037237ab0dac3fe100cde094e82e2bed9ba0ed1bb40154b48e56aa70f259e608b01483045022100c89172099507ff50f4c925e6c5150e871fb6e83dd73ff9fbb72f6ce829a9633f02203a63821d9162e99f9be712a68f9e589483994feae2661e4546cd5b6cec007be501008576a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a914b43e1b38138a41b37f7cd9a1d274bc63e3a9b5d188ac6868f6010000 - # signature for output #2 (htlc-success for htlc #1) - remote_htlc_signature = 304402201b63ec807771baf4fdff523c644080de17f1da478989308ad13a58b51db91d360220568939d38c9ce295adba15665fa68f51d967e8ed14a007b751540a80b325f202 - # local_htlc_signature = 3045022100def389deab09cee69eaa1ec14d9428770e45bcbe9feb46468ecf481371165c2f022015d2e3c46600b2ebba8dcc899768874cc6851fd1ecb3fffd15db1cc3de7e10da - htlc_success_tx (htlc #1): 020000000001018154ecccf11a5fb56c39654c4deb4d2296f83c69268280b94d021370c94e219702000000000000000001d0070000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e050047304402201b63ec807771baf4fdff523c644080de17f1da478989308ad13a58b51db91d360220568939d38c9ce295adba15665fa68f51d967e8ed14a007b751540a80b325f20201483045022100def389deab09cee69eaa1ec14d9428770e45bcbe9feb46468ecf481371165c2f022015d2e3c46600b2ebba8dcc899768874cc6851fd1ecb3fffd15db1cc3de7e10da012001010101010101010101010101010101010101010101010101010101010101018a76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a9144b6b2e5444c2639cc0fb7bcea5afba3f3cdce23988527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f501b175ac686800000000 - # signature for output #3 (htlc-timeout for htlc #3) - remote_htlc_signature = 3045022100daee1808f9861b6c3ecd14f7b707eca02dd6bdfc714ba2f33bc8cdba507bb182022026654bf8863af77d74f51f4e0b62d461a019561bb12acb120d3f7195d148a554 - # local_htlc_signature = 30440220643aacb19bbb72bd2b635bc3f7375481f5981bace78cdd8319b2988ffcc6704202203d27784ec8ad51ed3bd517a05525a5139bb0b755dd719e0054332d186ac08727 - htlc_timeout_tx (htlc #3): 020000000001018154ecccf11a5fb56c39654c4deb4d2296f83c69268280b94d021370c94e219703000000000000000001b80b0000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100daee1808f9861b6c3ecd14f7b707eca02dd6bdfc714ba2f33bc8cdba507bb182022026654bf8863af77d74f51f4e0b62d461a019561bb12acb120d3f7195d148a554014730440220643aacb19bbb72bd2b635bc3f7375481f5981bace78cdd8319b2988ffcc6704202203d27784ec8ad51ed3bd517a05525a5139bb0b755dd719e0054332d186ac0872701008576a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9148a486ff2e31d6158bf39e2608864d63fefd09d5b88ac6868f7010000 - # signature for output #4 (htlc-success for htlc #4) - remote_htlc_signature = 304402207e0410e45454b0978a623f36a10626ef17b27d9ad44e2760f98cfa3efb37924f0220220bd8acd43ecaa916a80bd4f919c495a2c58982ce7c8625153f8596692a801d - # local_htlc_signature = 30440220549e80b4496803cbc4a1d09d46df50109f546d43fbbf86cd90b174b1484acd5402205f12a4f995cb9bded597eabfee195a285986aa6d93ae5bb72507ebc6a4e2349e - htlc_success_tx (htlc #4): 020000000001018154ecccf11a5fb56c39654c4deb4d2296f83c69268280b94d021370c94e219704000000000000000001a00f0000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e050047304402207e0410e45454b0978a623f36a10626ef17b27d9ad44e2760f98cfa3efb37924f0220220bd8acd43ecaa916a80bd4f919c495a2c58982ce7c8625153f8596692a801d014730440220549e80b4496803cbc4a1d09d46df50109f546d43fbbf86cd90b174b1484acd5402205f12a4f995cb9bded597eabfee195a285986aa6d93ae5bb72507ebc6a4e2349e012004040404040404040404040404040404040404040404040404040404040404048a76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac686800000000 - - name: commitment tx with seven outputs untrimmed (maximum feerate) - to_local_msat: 6988000000 - to_remote_msat: 3000000000 - local_feerate_per_kw: 647 - # base commitment transaction fee = 1024 - # actual commitment transaction fee = 1024 - # HTLC #2 offered amount 2000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a914b43e1b38138a41b37f7cd9a1d274bc63e3a9b5d188ac6868 - # HTLC #3 offered amount 3000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9148a486ff2e31d6158bf39e2608864d63fefd09d5b88ac6868 - # HTLC #0 received amount 1000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a914b8bcb07f6344b42ab04250c86a6e8b75d3fdbbc688527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f401b175ac6868 - # HTLC #1 received amount 2000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a9144b6b2e5444c2639cc0fb7bcea5afba3f3cdce23988527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f501b175ac6868 - # HTLC #4 received amount 4000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac6868 - # to_local amount 6986976 wscript 63210212a140cd0c6539d07cd08dfe09984dec3251ea808b892efeac3ede9402bf2b1967029000b2752103fd5960528dc152014952efdb702a88f71e3c1653b2314431701ec77e57fde83c68ac - # to_remote amount 3000000 P2WPKH(0394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b) - remote_signature = 3045022100a5c01383d3ec646d97e40f44318d49def817fcd61a0ef18008a665b3e151785502203e648efddd5838981ef55ec954be69c4a652d021e6081a100d034de366815e9b - # local_signature = 304502210094bfd8f5572ac0157ec76a9551b6c5216a4538c07cd13a51af4a54cb26fa14320220768efce8ce6f4a5efac875142ff19237c011343670adf9c7ac69704a120d1163 - output commit_tx: 02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b8007e80300000000000022002052bfef0479d7b293c27e0f1eb294bea154c63a3294ef092c19af51409bce0e2ad007000000000000220020403d394747cae42e98ff01734ad5c08f82ba123d3d9a620abda88989651e2ab5d007000000000000220020748eba944fedc8827f6b06bc44678f93c0f9e6078b35c6331ed31e75f8ce0c2db80b000000000000220020c20b5d1f8584fd90443e7b7b720136174fa4b9333c261d04dbbd012635c0f419a00f0000000000002200208c48d15160397c9731df9bc3b236656efb6665fbfe92b4a6878e88a499f741c4c0c62d0000000000160014ccf1af2f2aabee14bb40fa3851ab2301de843110e09c6a00000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e040048304502210094bfd8f5572ac0157ec76a9551b6c5216a4538c07cd13a51af4a54cb26fa14320220768efce8ce6f4a5efac875142ff19237c011343670adf9c7ac69704a120d116301483045022100a5c01383d3ec646d97e40f44318d49def817fcd61a0ef18008a665b3e151785502203e648efddd5838981ef55ec954be69c4a652d021e6081a100d034de366815e9b01475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220 - num_htlcs: 5 - # signature for output #0 (htlc-success for htlc #0) - remote_htlc_signature = 30440220385a5afe75632f50128cbb029ee95c80156b5b4744beddc729ad339c9ca432c802202ba5f48550cad3379ac75b9b4fedb86a35baa6947f16ba5037fb8b11ab343740 - # local_htlc_signature = 304402205999590b8a79fa346e003a68fd40366397119b2b0cdf37b149968d6bc6fbcc4702202b1e1fb5ab7864931caed4e732c359e0fe3d86a548b557be2246efb1708d579a - htlc_success_tx (htlc #0): 020000000001018323148ce2419f21ca3d6780053747715832e18ac780931a514b187768882bb60000000000000000000122020000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e05004730440220385a5afe75632f50128cbb029ee95c80156b5b4744beddc729ad339c9ca432c802202ba5f48550cad3379ac75b9b4fedb86a35baa6947f16ba5037fb8b11ab3437400147304402205999590b8a79fa346e003a68fd40366397119b2b0cdf37b149968d6bc6fbcc4702202b1e1fb5ab7864931caed4e732c359e0fe3d86a548b557be2246efb1708d579a012000000000000000000000000000000000000000000000000000000000000000008a76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a914b8bcb07f6344b42ab04250c86a6e8b75d3fdbbc688527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f401b175ac686800000000 - # signature for output #1 (htlc-timeout for htlc #2) - remote_htlc_signature = 304402207ceb6678d4db33d2401fdc409959e57c16a6cb97a30261d9c61f29b8c58d34b90220084b4a17b4ca0e86f2d798b3698ca52de5621f2ce86f80bed79afa66874511b0 - # local_htlc_signature = 304402207ff03eb0127fc7c6cae49cc29e2a586b98d1e8969cf4a17dfa50b9c2647720b902205e2ecfda2252956c0ca32f175080e75e4e390e433feb1f8ce9f2ba55648a1dac - htlc_timeout_tx (htlc #2): 020000000001018323148ce2419f21ca3d6780053747715832e18ac780931a514b187768882bb60100000000000000000124060000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e050047304402207ceb6678d4db33d2401fdc409959e57c16a6cb97a30261d9c61f29b8c58d34b90220084b4a17b4ca0e86f2d798b3698ca52de5621f2ce86f80bed79afa66874511b00147304402207ff03eb0127fc7c6cae49cc29e2a586b98d1e8969cf4a17dfa50b9c2647720b902205e2ecfda2252956c0ca32f175080e75e4e390e433feb1f8ce9f2ba55648a1dac01008576a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a914b43e1b38138a41b37f7cd9a1d274bc63e3a9b5d188ac6868f6010000 - # signature for output #2 (htlc-success for htlc #1) - remote_htlc_signature = 304402206a401b29a0dff0d18ec903502c13d83e7ec019450113f4a7655a4ce40d1f65ba0220217723a084e727b6ca0cc8b6c69c014a7e4a01fcdcba3e3993f462a3c574d833 - # local_htlc_signature = 3045022100d50d067ca625d54e62df533a8f9291736678d0b86c28a61bb2a80cf42e702d6e02202373dde7e00218eacdafb9415fe0e1071beec1857d1af3c6a201a44cbc47c877 - htlc_success_tx (htlc #1): 020000000001018323148ce2419f21ca3d6780053747715832e18ac780931a514b187768882bb6020000000000000000010a060000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e050047304402206a401b29a0dff0d18ec903502c13d83e7ec019450113f4a7655a4ce40d1f65ba0220217723a084e727b6ca0cc8b6c69c014a7e4a01fcdcba3e3993f462a3c574d83301483045022100d50d067ca625d54e62df533a8f9291736678d0b86c28a61bb2a80cf42e702d6e02202373dde7e00218eacdafb9415fe0e1071beec1857d1af3c6a201a44cbc47c877012001010101010101010101010101010101010101010101010101010101010101018a76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a9144b6b2e5444c2639cc0fb7bcea5afba3f3cdce23988527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f501b175ac686800000000 - # signature for output #3 (htlc-timeout for htlc #3) - remote_htlc_signature = 30450221009b1c987ba599ee3bde1dbca776b85481d70a78b681a8d84206723e2795c7cac002207aac84ad910f8598c4d1c0ea2e3399cf6627a4e3e90131315bc9f038451ce39d - # local_htlc_signature = 3045022100db9dc65291077a52728c622987e9895b7241d4394d6dcb916d7600a3e8728c22022036ee3ee717ba0bb5c45ee84bc7bbf85c0f90f26ae4e4a25a6b4241afa8a3f1cb - htlc_timeout_tx (htlc #3): 020000000001018323148ce2419f21ca3d6780053747715832e18ac780931a514b187768882bb6030000000000000000010c0a0000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e05004830450221009b1c987ba599ee3bde1dbca776b85481d70a78b681a8d84206723e2795c7cac002207aac84ad910f8598c4d1c0ea2e3399cf6627a4e3e90131315bc9f038451ce39d01483045022100db9dc65291077a52728c622987e9895b7241d4394d6dcb916d7600a3e8728c22022036ee3ee717ba0bb5c45ee84bc7bbf85c0f90f26ae4e4a25a6b4241afa8a3f1cb01008576a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9148a486ff2e31d6158bf39e2608864d63fefd09d5b88ac6868f7010000 - # signature for output #4 (htlc-success for htlc #4) - remote_htlc_signature = 3045022100cc28030b59f0914f45b84caa983b6f8effa900c952310708c2b5b00781117022022027ba2ccdf94d03c6d48b327f183f6e28c8a214d089b9227f94ac4f85315274f0 - # local_htlc_signature = 304402202d1a3c0d31200265d2a2def2753ead4959ae20b4083e19553acfffa5dfab60bf022020ede134149504e15b88ab261a066de49848411e15e70f9e6a5462aec2949f8f - htlc_success_tx (htlc #4): 020000000001018323148ce2419f21ca3d6780053747715832e18ac780931a514b187768882bb604000000000000000001da0d0000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100cc28030b59f0914f45b84caa983b6f8effa900c952310708c2b5b00781117022022027ba2ccdf94d03c6d48b327f183f6e28c8a214d089b9227f94ac4f85315274f00147304402202d1a3c0d31200265d2a2def2753ead4959ae20b4083e19553acfffa5dfab60bf022020ede134149504e15b88ab261a066de49848411e15e70f9e6a5462aec2949f8f012004040404040404040404040404040404040404040404040404040404040404048a76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac686800000000 - - name: commitment tx with six outputs untrimmed (minimum feerate) - to_local_msat: 6988000000 - to_remote_msat: 3000000000 - local_feerate_per_kw: 648 - # base commitment transaction fee = 914 - # actual commitment transaction fee = 1914 - # HTLC #2 offered amount 2000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a914b43e1b38138a41b37f7cd9a1d274bc63e3a9b5d188ac6868 - # HTLC #3 offered amount 3000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9148a486ff2e31d6158bf39e2608864d63fefd09d5b88ac6868 - # HTLC #1 received amount 2000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a9144b6b2e5444c2639cc0fb7bcea5afba3f3cdce23988527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f501b175ac6868 - # HTLC #4 received amount 4000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac6868 - # to_local amount 6987086 wscript 63210212a140cd0c6539d07cd08dfe09984dec3251ea808b892efeac3ede9402bf2b1967029000b2752103fd5960528dc152014952efdb702a88f71e3c1653b2314431701ec77e57fde83c68ac - # to_remote amount 3000000 P2WPKH(0394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b) - remote_signature = 3044022072714e2fbb93cdd1c42eb0828b4f2eff143f717d8f26e79d6ada4f0dcb681bbe02200911be4e5161dd6ebe59ff1c58e1997c4aea804f81db6b698821db6093d7b057 - # local_signature = 3045022100a2270d5950c89ae0841233f6efea9c951898b301b2e89e0adbd2c687b9f32efa02207943d90f95b9610458e7c65a576e149750ff3accaacad004cd85e70b235e27de - output commit_tx: 02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b8006d007000000000000220020403d394747cae42e98ff01734ad5c08f82ba123d3d9a620abda88989651e2ab5d007000000000000220020748eba944fedc8827f6b06bc44678f93c0f9e6078b35c6331ed31e75f8ce0c2db80b000000000000220020c20b5d1f8584fd90443e7b7b720136174fa4b9333c261d04dbbd012635c0f419a00f0000000000002200208c48d15160397c9731df9bc3b236656efb6665fbfe92b4a6878e88a499f741c4c0c62d0000000000160014ccf1af2f2aabee14bb40fa3851ab2301de8431104e9d6a00000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0400483045022100a2270d5950c89ae0841233f6efea9c951898b301b2e89e0adbd2c687b9f32efa02207943d90f95b9610458e7c65a576e149750ff3accaacad004cd85e70b235e27de01473044022072714e2fbb93cdd1c42eb0828b4f2eff143f717d8f26e79d6ada4f0dcb681bbe02200911be4e5161dd6ebe59ff1c58e1997c4aea804f81db6b698821db6093d7b05701475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220 - num_htlcs: 4 - # signature for output #0 (htlc-timeout for htlc #2) - remote_htlc_signature = 3044022062ef2e77591409d60d7817d9bb1e71d3c4a2931d1a6c7c8307422c84f001a251022022dad9726b0ae3fe92bda745a06f2c00f92342a186d84518588cf65f4dfaada8 - # local_htlc_signature = 3045022100a4c574f00411dd2f978ca5cdc1b848c311cd7849c087ad2f21a5bce5e8cc5ae90220090ae39a9bce2fb8bc879d7e9f9022df249f41e25e51f1a9bf6447a9eeffc098 - htlc_timeout_tx (htlc #2): 02000000000101579c183eca9e8236a5d7f5dcd79cfec32c497fdc0ec61533cde99ecd436cadd10000000000000000000123060000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500473044022062ef2e77591409d60d7817d9bb1e71d3c4a2931d1a6c7c8307422c84f001a251022022dad9726b0ae3fe92bda745a06f2c00f92342a186d84518588cf65f4dfaada801483045022100a4c574f00411dd2f978ca5cdc1b848c311cd7849c087ad2f21a5bce5e8cc5ae90220090ae39a9bce2fb8bc879d7e9f9022df249f41e25e51f1a9bf6447a9eeffc09801008576a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a914b43e1b38138a41b37f7cd9a1d274bc63e3a9b5d188ac6868f6010000 - # signature for output #1 (htlc-success for htlc #1) - remote_htlc_signature = 3045022100e968cbbb5f402ed389fdc7f6cd2a80ed650bb42c79aeb2a5678444af94f6c78502204b47a1cb24ab5b0b6fe69fe9cfc7dba07b9dd0d8b95f372c1d9435146a88f8d4 - # local_htlc_signature = 304402207679cf19790bea76a733d2fa0672bd43ab455687a068f815a3d237581f57139a0220683a1a799e102071c206b207735ca80f627ab83d6616b4bcd017c5d79ef3e7d0 - htlc_success_tx (htlc #1): 02000000000101579c183eca9e8236a5d7f5dcd79cfec32c497fdc0ec61533cde99ecd436cadd10100000000000000000109060000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100e968cbbb5f402ed389fdc7f6cd2a80ed650bb42c79aeb2a5678444af94f6c78502204b47a1cb24ab5b0b6fe69fe9cfc7dba07b9dd0d8b95f372c1d9435146a88f8d40147304402207679cf19790bea76a733d2fa0672bd43ab455687a068f815a3d237581f57139a0220683a1a799e102071c206b207735ca80f627ab83d6616b4bcd017c5d79ef3e7d0012001010101010101010101010101010101010101010101010101010101010101018a76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a9144b6b2e5444c2639cc0fb7bcea5afba3f3cdce23988527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f501b175ac686800000000 - # signature for output #2 (htlc-timeout for htlc #3) - remote_htlc_signature = 3045022100aa91932e305292cf9969cc23502bbf6cef83a5df39c95ad04a707c4f4fed5c7702207099fc0f3a9bfe1e7683c0e9aa5e76c5432eb20693bf4cb182f04d383dc9c8c2 - # local_htlc_signature = 304402200df76fea718745f3c529bac7fd37923e7309ce38b25c0781e4cf514dd9ef8dc802204172295739dbae9fe0474dcee3608e3433b4b2af3a2e6787108b02f894dcdda3 - htlc_timeout_tx (htlc #3): 02000000000101579c183eca9e8236a5d7f5dcd79cfec32c497fdc0ec61533cde99ecd436cadd1020000000000000000010b0a0000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100aa91932e305292cf9969cc23502bbf6cef83a5df39c95ad04a707c4f4fed5c7702207099fc0f3a9bfe1e7683c0e9aa5e76c5432eb20693bf4cb182f04d383dc9c8c20147304402200df76fea718745f3c529bac7fd37923e7309ce38b25c0781e4cf514dd9ef8dc802204172295739dbae9fe0474dcee3608e3433b4b2af3a2e6787108b02f894dcdda301008576a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9148a486ff2e31d6158bf39e2608864d63fefd09d5b88ac6868f7010000 - # signature for output #3 (htlc-success for htlc #4) - remote_htlc_signature = 3044022035cac88040a5bba420b1c4257235d5015309113460bc33f2853cd81ca36e632402202fc94fd3e81e9d34a9d01782a0284f3044370d03d60f3fc041e2da088d2de58f - # local_htlc_signature = 304402200daf2eb7afd355b4caf6fb08387b5f031940ea29d1a9f35071288a839c9039e4022067201b562456e7948616c13acb876b386b511599b58ac1d94d127f91c50463a6 - htlc_success_tx (htlc #4): 02000000000101579c183eca9e8236a5d7f5dcd79cfec32c497fdc0ec61533cde99ecd436cadd103000000000000000001d90d0000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500473044022035cac88040a5bba420b1c4257235d5015309113460bc33f2853cd81ca36e632402202fc94fd3e81e9d34a9d01782a0284f3044370d03d60f3fc041e2da088d2de58f0147304402200daf2eb7afd355b4caf6fb08387b5f031940ea29d1a9f35071288a839c9039e4022067201b562456e7948616c13acb876b386b511599b58ac1d94d127f91c50463a6012004040404040404040404040404040404040404040404040404040404040404048a76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac686800000000 - - name: commitment tx with six outputs untrimmed (maximum feerate) - to_local_msat: 6988000000 - to_remote_msat: 3000000000 - local_feerate_per_kw: 2069 - # base commitment transaction fee = 2921 - # actual commitment transaction fee = 3921 - # HTLC #2 offered amount 2000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a914b43e1b38138a41b37f7cd9a1d274bc63e3a9b5d188ac6868 - # HTLC #3 offered amount 3000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9148a486ff2e31d6158bf39e2608864d63fefd09d5b88ac6868 - # HTLC #1 received amount 2000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a9144b6b2e5444c2639cc0fb7bcea5afba3f3cdce23988527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f501b175ac6868 - # HTLC #4 received amount 4000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac6868 - # to_local amount 6985079 wscript 63210212a140cd0c6539d07cd08dfe09984dec3251ea808b892efeac3ede9402bf2b1967029000b2752103fd5960528dc152014952efdb702a88f71e3c1653b2314431701ec77e57fde83c68ac - # to_remote amount 3000000 P2WPKH(0394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b) - remote_signature = 3044022001d55e488b8b035b2dd29d50b65b530923a416d47f377284145bc8767b1b6a75022019bb53ddfe1cefaf156f924777eaaf8fdca1810695a7d0a247ad2afba8232eb4 - # local_signature = 304402203ca8f31c6a47519f83255dc69f1894d9a6d7476a19f498d31eaf0cd3a85eeb63022026fd92dc752b33905c4c838c528b692a8ad4ced959990b5d5ee2ff940fa90eea - output commit_tx: 02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b8006d007000000000000220020403d394747cae42e98ff01734ad5c08f82ba123d3d9a620abda88989651e2ab5d007000000000000220020748eba944fedc8827f6b06bc44678f93c0f9e6078b35c6331ed31e75f8ce0c2db80b000000000000220020c20b5d1f8584fd90443e7b7b720136174fa4b9333c261d04dbbd012635c0f419a00f0000000000002200208c48d15160397c9731df9bc3b236656efb6665fbfe92b4a6878e88a499f741c4c0c62d0000000000160014ccf1af2f2aabee14bb40fa3851ab2301de84311077956a00000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e040047304402203ca8f31c6a47519f83255dc69f1894d9a6d7476a19f498d31eaf0cd3a85eeb63022026fd92dc752b33905c4c838c528b692a8ad4ced959990b5d5ee2ff940fa90eea01473044022001d55e488b8b035b2dd29d50b65b530923a416d47f377284145bc8767b1b6a75022019bb53ddfe1cefaf156f924777eaaf8fdca1810695a7d0a247ad2afba8232eb401475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220 - num_htlcs: 4 - # signature for output #0 (htlc-timeout for htlc #2) - remote_htlc_signature = 3045022100d1cf354de41c1369336cf85b225ed033f1f8982a01be503668df756a7e668b66022001254144fb4d0eecc61908fccc3388891ba17c5d7a1a8c62bdd307e5a513f992 - # local_htlc_signature = 3044022056eb1af429660e45a1b0b66568cb8c4a3aa7e4c9c292d5d6c47f86ebf2c8838f022065c3ac4ebe980ca7a41148569be4ad8751b0a724a41405697ec55035dae66402 - htlc_timeout_tx (htlc #2): 02000000000101ca94a9ad516ebc0c4bdd7b6254871babfa978d5accafb554214137d398bfcf6a0000000000000000000175020000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100d1cf354de41c1369336cf85b225ed033f1f8982a01be503668df756a7e668b66022001254144fb4d0eecc61908fccc3388891ba17c5d7a1a8c62bdd307e5a513f99201473044022056eb1af429660e45a1b0b66568cb8c4a3aa7e4c9c292d5d6c47f86ebf2c8838f022065c3ac4ebe980ca7a41148569be4ad8751b0a724a41405697ec55035dae6640201008576a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a914b43e1b38138a41b37f7cd9a1d274bc63e3a9b5d188ac6868f6010000 - # signature for output #1 (htlc-success for htlc #1) - remote_htlc_signature = 3045022100d065569dcb94f090345402736385efeb8ea265131804beac06dd84d15dd2d6880220664feb0b4b2eb985fadb6ec7dc58c9334ea88ce599a9be760554a2d4b3b5d9f4 - # local_htlc_signature = 3045022100914bb232cd4b2690ee3d6cb8c3713c4ac9c4fb925323068d8b07f67c8541f8d9022057152f5f1615b793d2d45aac7518989ae4fe970f28b9b5c77504799d25433f7f - htlc_success_tx (htlc #1): 02000000000101ca94a9ad516ebc0c4bdd7b6254871babfa978d5accafb554214137d398bfcf6a0100000000000000000122020000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100d065569dcb94f090345402736385efeb8ea265131804beac06dd84d15dd2d6880220664feb0b4b2eb985fadb6ec7dc58c9334ea88ce599a9be760554a2d4b3b5d9f401483045022100914bb232cd4b2690ee3d6cb8c3713c4ac9c4fb925323068d8b07f67c8541f8d9022057152f5f1615b793d2d45aac7518989ae4fe970f28b9b5c77504799d25433f7f012001010101010101010101010101010101010101010101010101010101010101018a76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a9144b6b2e5444c2639cc0fb7bcea5afba3f3cdce23988527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f501b175ac686800000000 - # signature for output #2 (htlc-timeout for htlc #3) - remote_htlc_signature = 3045022100d4e69d363de993684eae7b37853c40722a4c1b4a7b588ad7b5d8a9b5006137a102207a069c628170ee34be5612747051bdcc087466dbaa68d5756ea81c10155aef18 - # local_htlc_signature = 304402200e362443f7af830b419771e8e1614fc391db3a4eb799989abfc5ab26d6fcd032022039ab0cad1c14dfbe9446bf847965e56fe016e0cbcf719fd18c1bfbf53ecbd9f9 - htlc_timeout_tx (htlc #3): 02000000000101ca94a9ad516ebc0c4bdd7b6254871babfa978d5accafb554214137d398bfcf6a020000000000000000015d060000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100d4e69d363de993684eae7b37853c40722a4c1b4a7b588ad7b5d8a9b5006137a102207a069c628170ee34be5612747051bdcc087466dbaa68d5756ea81c10155aef180147304402200e362443f7af830b419771e8e1614fc391db3a4eb799989abfc5ab26d6fcd032022039ab0cad1c14dfbe9446bf847965e56fe016e0cbcf719fd18c1bfbf53ecbd9f901008576a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9148a486ff2e31d6158bf39e2608864d63fefd09d5b88ac6868f7010000 - # signature for output #3 (htlc-success for htlc #4) - remote_htlc_signature = 30450221008ec888e36e4a4b3dc2ed6b823319855b2ae03006ca6ae0d9aa7e24bfc1d6f07102203b0f78885472a67ff4fe5916c0bb669487d659527509516fc3a08e87a2cc0a7c - # local_htlc_signature = 304402202c3e14282b84b02705dfd00a6da396c9fe8a8bcb1d3fdb4b20a4feba09440e8b02202b058b39aa9b0c865b22095edcd9ff1f71bbfe20aa4993755e54d042755ed0d5 - htlc_success_tx (htlc #4): 02000000000101ca94a9ad516ebc0c4bdd7b6254871babfa978d5accafb554214137d398bfcf6a03000000000000000001f2090000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e05004830450221008ec888e36e4a4b3dc2ed6b823319855b2ae03006ca6ae0d9aa7e24bfc1d6f07102203b0f78885472a67ff4fe5916c0bb669487d659527509516fc3a08e87a2cc0a7c0147304402202c3e14282b84b02705dfd00a6da396c9fe8a8bcb1d3fdb4b20a4feba09440e8b02202b058b39aa9b0c865b22095edcd9ff1f71bbfe20aa4993755e54d042755ed0d5012004040404040404040404040404040404040404040404040404040404040404048a76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac686800000000 - - name: commitment tx with five outputs untrimmed (minimum feerate) - to_local_msat: 6988000000 - to_remote_msat: 3000000000 - local_feerate_per_kw: 2070 - # base commitment transaction fee = 2566 - # actual commitment transaction fee = 5566 - # HTLC #2 offered amount 2000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a914b43e1b38138a41b37f7cd9a1d274bc63e3a9b5d188ac6868 - # HTLC #3 offered amount 3000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9148a486ff2e31d6158bf39e2608864d63fefd09d5b88ac6868 - # HTLC #4 received amount 4000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac6868 - # to_local amount 6985434 wscript 63210212a140cd0c6539d07cd08dfe09984dec3251ea808b892efeac3ede9402bf2b1967029000b2752103fd5960528dc152014952efdb702a88f71e3c1653b2314431701ec77e57fde83c68ac - # to_remote amount 3000000 P2WPKH(0394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b) - remote_signature = 3045022100f2377f7a67b7fc7f4e2c0c9e3a7de935c32417f5668eda31ea1db401b7dc53030220415fdbc8e91d0f735e70c21952342742e25249b0d062d43efbfc564499f37526 - # local_signature = 30440220443cb07f650aebbba14b8bc8d81e096712590f524c5991ac0ed3bbc8fd3bd0c7022028a635f548e3ca64b19b69b1ea00f05b22752f91daf0b6dab78e62ba52eb7fd0 - output commit_tx: 02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b8005d007000000000000220020403d394747cae42e98ff01734ad5c08f82ba123d3d9a620abda88989651e2ab5b80b000000000000220020c20b5d1f8584fd90443e7b7b720136174fa4b9333c261d04dbbd012635c0f419a00f0000000000002200208c48d15160397c9731df9bc3b236656efb6665fbfe92b4a6878e88a499f741c4c0c62d0000000000160014ccf1af2f2aabee14bb40fa3851ab2301de843110da966a00000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e04004730440220443cb07f650aebbba14b8bc8d81e096712590f524c5991ac0ed3bbc8fd3bd0c7022028a635f548e3ca64b19b69b1ea00f05b22752f91daf0b6dab78e62ba52eb7fd001483045022100f2377f7a67b7fc7f4e2c0c9e3a7de935c32417f5668eda31ea1db401b7dc53030220415fdbc8e91d0f735e70c21952342742e25249b0d062d43efbfc564499f3752601475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220 - num_htlcs: 3 - # signature for output #0 (htlc-timeout for htlc #2) - remote_htlc_signature = 3045022100eed143b1ee4bed5dc3cde40afa5db3e7354cbf9c44054b5f713f729356f08cf7022077161d171c2bbd9badf3c9934de65a4918de03bbac1450f715275f75b103f891 - # local_htlc_signature = 3045022100a0d043ed533e7fb1911e0553d31a8e2f3e6de19dbc035257f29d747c5e02f1f5022030cd38d8e84282175d49c1ebe0470db3ebd59768cf40780a784e248a43904fb8 - htlc_timeout_tx (htlc #2): 0200000000010140a83ce364747ff277f4d7595d8d15f708418798922c40bc2b056aca5485a2180000000000000000000174020000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100eed143b1ee4bed5dc3cde40afa5db3e7354cbf9c44054b5f713f729356f08cf7022077161d171c2bbd9badf3c9934de65a4918de03bbac1450f715275f75b103f89101483045022100a0d043ed533e7fb1911e0553d31a8e2f3e6de19dbc035257f29d747c5e02f1f5022030cd38d8e84282175d49c1ebe0470db3ebd59768cf40780a784e248a43904fb801008576a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a914b43e1b38138a41b37f7cd9a1d274bc63e3a9b5d188ac6868f6010000 - # signature for output #1 (htlc-timeout for htlc #3) - remote_htlc_signature = 3044022071e9357619fd8d29a411dc053b326a5224c5d11268070e88ecb981b174747c7a02202b763ae29a9d0732fa8836dd8597439460b50472183f420021b768981b4f7cf6 - # local_htlc_signature = 3045022100adb1d679f65f96178b59f23ed37d3b70443118f345224a07ecb043eee2acc157022034d24524fe857144a3bcfff3065a9994d0a6ec5f11c681e49431d573e242612d - htlc_timeout_tx (htlc #3): 0200000000010140a83ce364747ff277f4d7595d8d15f708418798922c40bc2b056aca5485a218010000000000000000015c060000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500473044022071e9357619fd8d29a411dc053b326a5224c5d11268070e88ecb981b174747c7a02202b763ae29a9d0732fa8836dd8597439460b50472183f420021b768981b4f7cf601483045022100adb1d679f65f96178b59f23ed37d3b70443118f345224a07ecb043eee2acc157022034d24524fe857144a3bcfff3065a9994d0a6ec5f11c681e49431d573e242612d01008576a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9148a486ff2e31d6158bf39e2608864d63fefd09d5b88ac6868f7010000 - # signature for output #2 (htlc-success for htlc #4) - remote_htlc_signature = 3045022100c9458a4d2cbb741705577deb0a890e5cb90ee141be0400d3162e533727c9cb2102206edcf765c5dc5e5f9b976ea8149bf8607b5a0efb30691138e1231302b640d2a4 - # local_htlc_signature = 304402200831422aa4e1ee6d55e0b894201770a8f8817a189356f2d70be76633ffa6a6f602200dd1b84a4855dc6727dd46c98daae43dfc70889d1ba7ef0087529a57c06e5e04 - htlc_success_tx (htlc #4): 0200000000010140a83ce364747ff277f4d7595d8d15f708418798922c40bc2b056aca5485a21802000000000000000001f1090000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100c9458a4d2cbb741705577deb0a890e5cb90ee141be0400d3162e533727c9cb2102206edcf765c5dc5e5f9b976ea8149bf8607b5a0efb30691138e1231302b640d2a40147304402200831422aa4e1ee6d55e0b894201770a8f8817a189356f2d70be76633ffa6a6f602200dd1b84a4855dc6727dd46c98daae43dfc70889d1ba7ef0087529a57c06e5e04012004040404040404040404040404040404040404040404040404040404040404048a76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac686800000000 - - name: commitment tx with five outputs untrimmed (maximum feerate) - to_local_msat: 6988000000 - to_remote_msat: 3000000000 - local_feerate_per_kw: 2194 - # base commitment transaction fee = 2720 - # actual commitment transaction fee = 5720 - # HTLC #2 offered amount 2000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a914b43e1b38138a41b37f7cd9a1d274bc63e3a9b5d188ac6868 - # HTLC #3 offered amount 3000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9148a486ff2e31d6158bf39e2608864d63fefd09d5b88ac6868 - # HTLC #4 received amount 4000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac6868 - # to_local amount 6985280 wscript 63210212a140cd0c6539d07cd08dfe09984dec3251ea808b892efeac3ede9402bf2b1967029000b2752103fd5960528dc152014952efdb702a88f71e3c1653b2314431701ec77e57fde83c68ac - # to_remote amount 3000000 P2WPKH(0394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b) - remote_signature = 3045022100d33c4e541aa1d255d41ea9a3b443b3b822ad8f7f86862638aac1f69f8f760577022007e2a18e6931ce3d3a804b1c78eda1de17dbe1fb7a95488c9a4ec86203953348 - # local_signature = 304402203b1b010c109c2ecbe7feb2d259b9c4126bd5dc99ee693c422ec0a5781fe161ba0220571fe4e2c649dea9c7aaf7e49b382962f6a3494963c97d80fef9a430ca3f7061 - output commit_tx: 02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b8005d007000000000000220020403d394747cae42e98ff01734ad5c08f82ba123d3d9a620abda88989651e2ab5b80b000000000000220020c20b5d1f8584fd90443e7b7b720136174fa4b9333c261d04dbbd012635c0f419a00f0000000000002200208c48d15160397c9731df9bc3b236656efb6665fbfe92b4a6878e88a499f741c4c0c62d0000000000160014ccf1af2f2aabee14bb40fa3851ab2301de84311040966a00000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e040047304402203b1b010c109c2ecbe7feb2d259b9c4126bd5dc99ee693c422ec0a5781fe161ba0220571fe4e2c649dea9c7aaf7e49b382962f6a3494963c97d80fef9a430ca3f706101483045022100d33c4e541aa1d255d41ea9a3b443b3b822ad8f7f86862638aac1f69f8f760577022007e2a18e6931ce3d3a804b1c78eda1de17dbe1fb7a95488c9a4ec8620395334801475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220 - num_htlcs: 3 - # signature for output #0 (htlc-timeout for htlc #2) - remote_htlc_signature = 30450221009ed2f0a67f99e29c3c8cf45c08207b765980697781bb727fe0b1416de0e7622902206052684229bc171419ed290f4b615c943f819c0262414e43c5b91dcf72ddcf44 - # local_htlc_signature = 3044022004ad5f04ae69c71b3b141d4db9d0d4c38d84009fb3cfeeae6efdad414487a9a0022042d3fe1388c1ff517d1da7fb4025663d372c14728ed52dc88608363450ff6a2f - htlc_timeout_tx (htlc #2): 02000000000101fb824d4e4dafc0f567789dee3a6bce8d411fe80f5563d8cdfdcc7d7e4447d43a0000000000000000000122020000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e05004830450221009ed2f0a67f99e29c3c8cf45c08207b765980697781bb727fe0b1416de0e7622902206052684229bc171419ed290f4b615c943f819c0262414e43c5b91dcf72ddcf4401473044022004ad5f04ae69c71b3b141d4db9d0d4c38d84009fb3cfeeae6efdad414487a9a0022042d3fe1388c1ff517d1da7fb4025663d372c14728ed52dc88608363450ff6a2f01008576a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a914b43e1b38138a41b37f7cd9a1d274bc63e3a9b5d188ac6868f6010000 - # signature for output #1 (htlc-timeout for htlc #3) - remote_htlc_signature = 30440220155d3b90c67c33a8321996a9be5b82431b0c126613be751d400669da9d5c696702204318448bcd48824439d2c6a70be6e5747446be47ff45977cf41672bdc9b6b12d - # local_htlc_signature = 304402201707050c870c1f77cc3ed58d6d71bf281de239e9eabd8ef0955bad0d7fe38dcc02204d36d80d0019b3a71e646a08fa4a5607761d341ae8be371946ebe437c289c915 - htlc_timeout_tx (htlc #3): 02000000000101fb824d4e4dafc0f567789dee3a6bce8d411fe80f5563d8cdfdcc7d7e4447d43a010000000000000000010a060000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e05004730440220155d3b90c67c33a8321996a9be5b82431b0c126613be751d400669da9d5c696702204318448bcd48824439d2c6a70be6e5747446be47ff45977cf41672bdc9b6b12d0147304402201707050c870c1f77cc3ed58d6d71bf281de239e9eabd8ef0955bad0d7fe38dcc02204d36d80d0019b3a71e646a08fa4a5607761d341ae8be371946ebe437c289c91501008576a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9148a486ff2e31d6158bf39e2608864d63fefd09d5b88ac6868f7010000 - # signature for output #2 (htlc-success for htlc #4) - remote_htlc_signature = 3045022100a12a9a473ece548584aabdd051779025a5ed4077c4b7aa376ec7a0b1645e5a48022039490b333f53b5b3e2ddde1d809e492cba2b3e5fc3a436cd3ffb4cd3d500fa5a - # local_htlc_signature = 3045022100ff200bc934ab26ce9a559e998ceb0aee53bc40368e114ab9d3054d9960546e2802202496856ca163ac12c143110b6b3ac9d598df7254f2e17b3b94c3ab5301f4c3b0 - htlc_success_tx (htlc #4): 02000000000101fb824d4e4dafc0f567789dee3a6bce8d411fe80f5563d8cdfdcc7d7e4447d43a020000000000000000019a090000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100a12a9a473ece548584aabdd051779025a5ed4077c4b7aa376ec7a0b1645e5a48022039490b333f53b5b3e2ddde1d809e492cba2b3e5fc3a436cd3ffb4cd3d500fa5a01483045022100ff200bc934ab26ce9a559e998ceb0aee53bc40368e114ab9d3054d9960546e2802202496856ca163ac12c143110b6b3ac9d598df7254f2e17b3b94c3ab5301f4c3b0012004040404040404040404040404040404040404040404040404040404040404048a76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac686800000000 - - name: commitment tx with four outputs untrimmed (minimum feerate) - to_local_msat: 6988000000 - to_remote_msat: 3000000000 - local_feerate_per_kw: 2195 - # base commitment transaction fee = 2344 - # actual commitment transaction fee = 7344 - # HTLC #3 offered amount 3000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9148a486ff2e31d6158bf39e2608864d63fefd09d5b88ac6868 - # HTLC #4 received amount 4000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac6868 - # to_local amount 6985656 wscript 63210212a140cd0c6539d07cd08dfe09984dec3251ea808b892efeac3ede9402bf2b1967029000b2752103fd5960528dc152014952efdb702a88f71e3c1653b2314431701ec77e57fde83c68ac - # to_remote amount 3000000 P2WPKH(0394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b) - remote_signature = 304402205e2f76d4657fb732c0dfc820a18a7301e368f5799e06b7828007633741bda6df0220458009ae59d0c6246065c419359e05eb2a4b4ef4a1b310cc912db44eb7924298 - # local_signature = 304402203b12d44254244b8ff3bb4129b0920fd45120ab42f553d9976394b099d500c99e02205e95bb7a3164852ef0c48f9e0eaf145218f8e2c41251b231f03cbdc4f29a5429 - output commit_tx: 02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b8004b80b000000000000220020c20b5d1f8584fd90443e7b7b720136174fa4b9333c261d04dbbd012635c0f419a00f0000000000002200208c48d15160397c9731df9bc3b236656efb6665fbfe92b4a6878e88a499f741c4c0c62d0000000000160014ccf1af2f2aabee14bb40fa3851ab2301de843110b8976a00000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e040047304402203b12d44254244b8ff3bb4129b0920fd45120ab42f553d9976394b099d500c99e02205e95bb7a3164852ef0c48f9e0eaf145218f8e2c41251b231f03cbdc4f29a54290147304402205e2f76d4657fb732c0dfc820a18a7301e368f5799e06b7828007633741bda6df0220458009ae59d0c6246065c419359e05eb2a4b4ef4a1b310cc912db44eb792429801475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220 - num_htlcs: 2 - # signature for output #0 (htlc-timeout for htlc #3) - remote_htlc_signature = 3045022100a8a78fa1016a5c5c3704f2e8908715a3cef66723fb95f3132ec4d2d05cd84fb4022025ac49287b0861ec21932405f5600cbce94313dbde0e6c5d5af1b3366d8afbfc - # local_htlc_signature = 3045022100be6ae1977fd7b630a53623f3f25c542317ccfc2b971782802a4f1ef538eb22b402207edc4d0408f8f38fd3c7365d1cfc26511b7cd2d4fecd8b005fba3cd5bc704390 - htlc_timeout_tx (htlc #3): 020000000001014e16c488fa158431c1a82e8f661240ec0a71ba0ce92f2721a6538c510226ad5c0000000000000000000109060000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100a8a78fa1016a5c5c3704f2e8908715a3cef66723fb95f3132ec4d2d05cd84fb4022025ac49287b0861ec21932405f5600cbce94313dbde0e6c5d5af1b3366d8afbfc01483045022100be6ae1977fd7b630a53623f3f25c542317ccfc2b971782802a4f1ef538eb22b402207edc4d0408f8f38fd3c7365d1cfc26511b7cd2d4fecd8b005fba3cd5bc70439001008576a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9148a486ff2e31d6158bf39e2608864d63fefd09d5b88ac6868f7010000 - # signature for output #1 (htlc-success for htlc #4) - remote_htlc_signature = 3045022100e769cb156aa2f7515d126cef7a69968629620ce82afcaa9e210969de6850df4602200b16b3f3486a229a48aadde520dbee31ae340dbadaffae74fbb56681fef27b92 - # local_htlc_signature = 30440220665b9cb4a978c09d1ca8977a534999bc8a49da624d0c5439451dd69cde1a003d022070eae0620f01f3c1bd029cc1488da13fb40fdab76f396ccd335479a11c5276d8 - htlc_success_tx (htlc #4): 020000000001014e16c488fa158431c1a82e8f661240ec0a71ba0ce92f2721a6538c510226ad5c0100000000000000000199090000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100e769cb156aa2f7515d126cef7a69968629620ce82afcaa9e210969de6850df4602200b16b3f3486a229a48aadde520dbee31ae340dbadaffae74fbb56681fef27b92014730440220665b9cb4a978c09d1ca8977a534999bc8a49da624d0c5439451dd69cde1a003d022070eae0620f01f3c1bd029cc1488da13fb40fdab76f396ccd335479a11c5276d8012004040404040404040404040404040404040404040404040404040404040404048a76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac686800000000 - - name: commitment tx with four outputs untrimmed (maximum feerate) - to_local_msat: 6988000000 - to_remote_msat: 3000000000 - local_feerate_per_kw: 3702 - # base commitment transaction fee = 3953 - # actual commitment transaction fee = 8953 - # HTLC #3 offered amount 3000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9148a486ff2e31d6158bf39e2608864d63fefd09d5b88ac6868 - # HTLC #4 received amount 4000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac6868 - # to_local amount 6984047 wscript 63210212a140cd0c6539d07cd08dfe09984dec3251ea808b892efeac3ede9402bf2b1967029000b2752103fd5960528dc152014952efdb702a88f71e3c1653b2314431701ec77e57fde83c68ac - # to_remote amount 3000000 P2WPKH(0394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b) - remote_signature = 3045022100c1a3b0b60ca092ed5080121f26a74a20cec6bdee3f8e47bae973fcdceb3eda5502207d467a9873c939bf3aa758014ae67295fedbca52412633f7e5b2670fc7c381c1 - # local_signature = 304402200e930a43c7951162dc15a2b7344f48091c74c70f7024e7116e900d8bcfba861c022066fa6cbda3929e21daa2e7e16a4b948db7e8919ef978402360d1095ffdaff7b0 - output commit_tx: 02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b8004b80b000000000000220020c20b5d1f8584fd90443e7b7b720136174fa4b9333c261d04dbbd012635c0f419a00f0000000000002200208c48d15160397c9731df9bc3b236656efb6665fbfe92b4a6878e88a499f741c4c0c62d0000000000160014ccf1af2f2aabee14bb40fa3851ab2301de8431106f916a00000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e040047304402200e930a43c7951162dc15a2b7344f48091c74c70f7024e7116e900d8bcfba861c022066fa6cbda3929e21daa2e7e16a4b948db7e8919ef978402360d1095ffdaff7b001483045022100c1a3b0b60ca092ed5080121f26a74a20cec6bdee3f8e47bae973fcdceb3eda5502207d467a9873c939bf3aa758014ae67295fedbca52412633f7e5b2670fc7c381c101475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220 - num_htlcs: 2 - # signature for output #0 (htlc-timeout for htlc #3) - remote_htlc_signature = 3045022100dfb73b4fe961b31a859b2bb1f4f15cabab9265016dd0272323dc6a9e85885c54022059a7b87c02861ee70662907f25ce11597d7b68d3399443a831ae40e777b76bdb - # local_htlc_signature = 304402202765b9c9ece4f127fa5407faf66da4c5ce2719cdbe47cd3175fc7d48b482e43d02205605125925e07bad1e41c618a4b434d72c88a164981c4b8af5eaf4ee9142ec3a - htlc_timeout_tx (htlc #3): 02000000000101b8de11eb51c22498fe39722c7227b6e55ff1a94146cf638458cb9bc6a060d3a30000000000000000000122020000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100dfb73b4fe961b31a859b2bb1f4f15cabab9265016dd0272323dc6a9e85885c54022059a7b87c02861ee70662907f25ce11597d7b68d3399443a831ae40e777b76bdb0147304402202765b9c9ece4f127fa5407faf66da4c5ce2719cdbe47cd3175fc7d48b482e43d02205605125925e07bad1e41c618a4b434d72c88a164981c4b8af5eaf4ee9142ec3a01008576a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9148a486ff2e31d6158bf39e2608864d63fefd09d5b88ac6868f7010000 - # signature for output #1 (htlc-success for htlc #4) - remote_htlc_signature = 3045022100ea9dc2a7c3c3640334dab733bb4e036e32a3106dc707b24227874fa4f7da746802204d672f7ac0fe765931a8df10b81e53a3242dd32bd9dc9331eb4a596da87954e9 - # local_htlc_signature = 30440220048a41c660c4841693de037d00a407810389f4574b3286afb7bc392a438fa3f802200401d71fa87c64fe621b49ac07e3bf85157ac680acb977124da28652cc7f1a5c - htlc_success_tx (htlc #4): 02000000000101b8de11eb51c22498fe39722c7227b6e55ff1a94146cf638458cb9bc6a060d3a30100000000000000000176050000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100ea9dc2a7c3c3640334dab733bb4e036e32a3106dc707b24227874fa4f7da746802204d672f7ac0fe765931a8df10b81e53a3242dd32bd9dc9331eb4a596da87954e9014730440220048a41c660c4841693de037d00a407810389f4574b3286afb7bc392a438fa3f802200401d71fa87c64fe621b49ac07e3bf85157ac680acb977124da28652cc7f1a5c012004040404040404040404040404040404040404040404040404040404040404048a76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac686800000000 - - name: commitment tx with three outputs untrimmed (minimum feerate) - to_local_msat: 6988000000 - to_remote_msat: 3000000000 - local_feerate_per_kw: 3703 - # base commitment transaction fee = 3317 - # actual commitment transaction fee = 11317 - # HTLC #4 received amount 4000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac6868 - # to_local amount 6984683 wscript 63210212a140cd0c6539d07cd08dfe09984dec3251ea808b892efeac3ede9402bf2b1967029000b2752103fd5960528dc152014952efdb702a88f71e3c1653b2314431701ec77e57fde83c68ac - # to_remote amount 3000000 P2WPKH(0394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b) - remote_signature = 30450221008b7c191dd46893b67b628e618d2dc8e81169d38bade310181ab77d7c94c6675e02203b4dd131fd7c9deb299560983dcdc485545c98f989f7ae8180c28289f9e6bdb0 - # local_signature = 3044022047305531dd44391dce03ae20f8735005c615eb077a974edb0059ea1a311857d602202e0ed6972fbdd1e8cb542b06e0929bc41b2ddf236e04cb75edd56151f4197506 - output commit_tx: 02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b8003a00f0000000000002200208c48d15160397c9731df9bc3b236656efb6665fbfe92b4a6878e88a499f741c4c0c62d0000000000160014ccf1af2f2aabee14bb40fa3851ab2301de843110eb936a00000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0400473044022047305531dd44391dce03ae20f8735005c615eb077a974edb0059ea1a311857d602202e0ed6972fbdd1e8cb542b06e0929bc41b2ddf236e04cb75edd56151f4197506014830450221008b7c191dd46893b67b628e618d2dc8e81169d38bade310181ab77d7c94c6675e02203b4dd131fd7c9deb299560983dcdc485545c98f989f7ae8180c28289f9e6bdb001475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220 - num_htlcs: 1 - # signature for output #0 (htlc-success for htlc #4) - remote_htlc_signature = 3044022044f65cf833afdcb9d18795ca93f7230005777662539815b8a601eeb3e57129a902206a4bf3e53392affbba52640627defa8dc8af61c958c9e827b2798ab45828abdd - # local_htlc_signature = 3045022100b94d931a811b32eeb885c28ddcf999ae1981893b21dd1329929543fe87ce793002206370107fdd151c5f2384f9ceb71b3107c69c74c8ed5a28a94a4ab2d27d3b0724 - htlc_success_tx (htlc #4): 020000000001011c076aa7fb3d7460d10df69432c904227ea84bbf3134d4ceee5fb0f135ef206d0000000000000000000175050000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500473044022044f65cf833afdcb9d18795ca93f7230005777662539815b8a601eeb3e57129a902206a4bf3e53392affbba52640627defa8dc8af61c958c9e827b2798ab45828abdd01483045022100b94d931a811b32eeb885c28ddcf999ae1981893b21dd1329929543fe87ce793002206370107fdd151c5f2384f9ceb71b3107c69c74c8ed5a28a94a4ab2d27d3b0724012004040404040404040404040404040404040404040404040404040404040404048a76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac686800000000 - - name: commitment tx with three outputs untrimmed (maximum feerate) - to_local_msat: 6988000000 - to_remote_msat: 3000000000 - local_feerate_per_kw: 4914 - # base commitment transaction fee = 4402 - # actual commitment transaction fee = 12402 - # HTLC #4 received amount 4000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac6868 - # to_local amount 6983598 wscript 63210212a140cd0c6539d07cd08dfe09984dec3251ea808b892efeac3ede9402bf2b1967029000b2752103fd5960528dc152014952efdb702a88f71e3c1653b2314431701ec77e57fde83c68ac - # to_remote amount 3000000 P2WPKH(0394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b) - remote_signature = 304402206d6cb93969d39177a09d5d45b583f34966195b77c7e585cf47ac5cce0c90cefb022031d71ae4e33a4e80df7f981d696fbdee517337806a3c7138b7491e2cbb077a0e - # local_signature = 304402206a2679efa3c7aaffd2a447fd0df7aba8792858b589750f6a1203f9259173198a022008d52a0e77a99ab533c36206cb15ad7aeb2aa72b93d4b571e728cb5ec2f6fe26 - output commit_tx: 02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b8003a00f0000000000002200208c48d15160397c9731df9bc3b236656efb6665fbfe92b4a6878e88a499f741c4c0c62d0000000000160014ccf1af2f2aabee14bb40fa3851ab2301de843110ae8f6a00000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e040047304402206a2679efa3c7aaffd2a447fd0df7aba8792858b589750f6a1203f9259173198a022008d52a0e77a99ab533c36206cb15ad7aeb2aa72b93d4b571e728cb5ec2f6fe260147304402206d6cb93969d39177a09d5d45b583f34966195b77c7e585cf47ac5cce0c90cefb022031d71ae4e33a4e80df7f981d696fbdee517337806a3c7138b7491e2cbb077a0e01475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220 - num_htlcs: 1 - # signature for output #0 (htlc-success for htlc #4) - remote_htlc_signature = 3045022100fcb38506bfa11c02874092a843d0cc0a8613c23b639832564a5f69020cb0f6ba02206508b9e91eaa001425c190c68ee5f887e1ad5b1b314002e74db9dbd9e42dbecf - # local_htlc_signature = 304502210086e76b460ddd3cea10525fba298405d3fe11383e56966a5091811368362f689a02200f72ee75657915e0ede89c28709acd113ede9e1b7be520e3bc5cda425ecd6e68 - htlc_success_tx (htlc #4): 0200000000010110a3fdcbcd5db477cd3ad465e7f501ffa8c437e8301f00a6061138590add757f0000000000000000000122020000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100fcb38506bfa11c02874092a843d0cc0a8613c23b639832564a5f69020cb0f6ba02206508b9e91eaa001425c190c68ee5f887e1ad5b1b314002e74db9dbd9e42dbecf0148304502210086e76b460ddd3cea10525fba298405d3fe11383e56966a5091811368362f689a02200f72ee75657915e0ede89c28709acd113ede9e1b7be520e3bc5cda425ecd6e68012004040404040404040404040404040404040404040404040404040404040404048a76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac686800000000 - - name: commitment tx with two outputs untrimmed (minimum feerate) - to_local_msat: 6988000000 - to_remote_msat: 3000000000 - local_feerate_per_kw: 4915 - # base commitment transaction fee = 3558 - # actual commitment transaction fee = 15558 - # to_local amount 6984442 wscript 63210212a140cd0c6539d07cd08dfe09984dec3251ea808b892efeac3ede9402bf2b1967029000b2752103fd5960528dc152014952efdb702a88f71e3c1653b2314431701ec77e57fde83c68ac - # to_remote amount 3000000 P2WPKH(0394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b) - remote_signature = 304402200769ba89c7330dfa4feba447b6e322305f12ac7dac70ec6ba997ed7c1b598d0802204fe8d337e7fee781f9b7b1a06e580b22f4f79d740059560191d7db53f8765552 - # local_signature = 3045022100a012691ba6cea2f73fa8bac37750477e66363c6d28813b0bb6da77c8eb3fb0270220365e99c51304b0b1a6ab9ea1c8500db186693e39ec1ad5743ee231b0138384b9 - output commit_tx: 02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b8002c0c62d0000000000160014ccf1af2f2aabee14bb40fa3851ab2301de843110fa926a00000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0400483045022100a012691ba6cea2f73fa8bac37750477e66363c6d28813b0bb6da77c8eb3fb0270220365e99c51304b0b1a6ab9ea1c8500db186693e39ec1ad5743ee231b0138384b90147304402200769ba89c7330dfa4feba447b6e322305f12ac7dac70ec6ba997ed7c1b598d0802204fe8d337e7fee781f9b7b1a06e580b22f4f79d740059560191d7db53f876555201475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220 - num_htlcs: 0 - - name: commitment tx with two outputs untrimmed (maximum feerate) - to_local_msat: 6988000000 - to_remote_msat: 3000000000 - local_feerate_per_kw: 9651180 - # base commitment transaction fee = 6987454 - # actual commitment transaction fee = 6999454 - # to_local amount 546 wscript 63210212a140cd0c6539d07cd08dfe09984dec3251ea808b892efeac3ede9402bf2b1967029000b2752103fd5960528dc152014952efdb702a88f71e3c1653b2314431701ec77e57fde83c68ac - # to_remote amount 3000000 P2WPKH(0394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b) - remote_signature = 3044022037f83ff00c8e5fb18ae1f918ffc24e54581775a20ff1ae719297ef066c71caa9022039c529cccd89ff6c5ed1db799614533844bd6d101da503761c45c713996e3bbd - # local_signature = 30440220514f977bf7edc442de8ce43ace9686e5ebdc0f893033f13e40fb46c8b8c6e1f90220188006227d175f5c35da0b092c57bea82537aed89f7778204dc5bacf4f29f2b9 - output commit_tx: 02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b800222020000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80ec0c62d0000000000160014ccf1af2f2aabee14bb40fa3851ab2301de84311004004730440220514f977bf7edc442de8ce43ace9686e5ebdc0f893033f13e40fb46c8b8c6e1f90220188006227d175f5c35da0b092c57bea82537aed89f7778204dc5bacf4f29f2b901473044022037f83ff00c8e5fb18ae1f918ffc24e54581775a20ff1ae719297ef066c71caa9022039c529cccd89ff6c5ed1db799614533844bd6d101da503761c45c713996e3bbd01475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220 - num_htlcs: 0 - - name: commitment tx with one output untrimmed (minimum feerate) - to_local_msat: 6988000000 - to_remote_msat: 3000000000 - local_feerate_per_kw: 9651181 - # base commitment transaction fee = 6987455 - # actual commitment transaction fee = 7000000 - # to_remote amount 3000000 P2WPKH(0394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b) - remote_signature = 3044022064901950be922e62cbe3f2ab93de2b99f37cff9fc473e73e394b27f88ef0731d02206d1dfa227527b4df44a07599289e207d6fd9cca60c0365682dcd3deaf739567e - # local_signature = 3044022031a82b51bd014915fe68928d1abf4b9885353fb896cac10c3fdd88d7f9c7f2e00220716bda819641d2c63e65d3549b6120112e1aeaf1742eed94a471488e79e206b1 - output commit_tx: 02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b8001c0c62d0000000000160014ccf1af2f2aabee14bb40fa3851ab2301de8431100400473044022031a82b51bd014915fe68928d1abf4b9885353fb896cac10c3fdd88d7f9c7f2e00220716bda819641d2c63e65d3549b6120112e1aeaf1742eed94a471488e79e206b101473044022064901950be922e62cbe3f2ab93de2b99f37cff9fc473e73e394b27f88ef0731d02206d1dfa227527b4df44a07599289e207d6fd9cca60c0365682dcd3deaf739567e01475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220 - num_htlcs: 0 - - name: commitment tx with fee greater than funder amount - to_local_msat: 6988000000 - to_remote_msat: 3000000000 - local_feerate_per_kw: 9651936 - # base commitment transaction fee = 6988001 - # actual commitment transaction fee = 7000000 - # to_remote amount 3000000 P2WPKH(0394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b) - remote_signature = 3044022064901950be922e62cbe3f2ab93de2b99f37cff9fc473e73e394b27f88ef0731d02206d1dfa227527b4df44a07599289e207d6fd9cca60c0365682dcd3deaf739567e - # local_signature = 3044022031a82b51bd014915fe68928d1abf4b9885353fb896cac10c3fdd88d7f9c7f2e00220716bda819641d2c63e65d3549b6120112e1aeaf1742eed94a471488e79e206b1 - output commit_tx: 02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b8001c0c62d0000000000160014ccf1af2f2aabee14bb40fa3851ab2301de8431100400473044022031a82b51bd014915fe68928d1abf4b9885353fb896cac10c3fdd88d7f9c7f2e00220716bda819641d2c63e65d3549b6120112e1aeaf1742eed94a471488e79e206b101473044022064901950be922e62cbe3f2ab93de2b99f37cff9fc473e73e394b27f88ef0731d02206d1dfa227527b4df44a07599289e207d6fd9cca60c0365682dcd3deaf739567e01475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220 - num_htlcs: 0 - - name: commitment tx with 3 htlc outputs, 2 offered having the same amount and preimage - to_local_msat: 6987999999 - to_remote_msat: 3000000000 - local_feerate_per_kw: 253 - # HTLC #1 received amount 2000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a9144b6b2e5444c2639cc0fb7bcea5afba3f3cdce23988527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f501b175ac6868 - # HTLC #5 offered amount 5000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9142002cc93ebefbb1b73f0af055dcc27a0b504ad7688ac6868 - # HTLC #6 offered amount 5000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9142002cc93ebefbb1b73f0af055dcc27a0b504ad7688ac6868 - # HTLC #5 and 6 have CLTV 506 and 505, respectively, and preimage 0505050505050505050505050505050505050505050505050505050505050505 - remote_signature = 304402206cda85b2811a211aa70fb74abf23303d87c4355ccf2c2c7954d4137c4fb26a830220719402ab3fef1cbaf42ba42fe437e9bed1e45f84547d603bf7af3fb88f501933 - # local_signature = 3045022100d25455151be075bae8b3400d0825341a3c25a1a5258b84ad2546c09539a83bc602203c1a4ac19c3ac415af7f6a98348f8d7e94fc0ab82dbed54c3c40134f465d027f - output commit_tx: 02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b8005d007000000000000220020748eba944fedc8827f6b06bc44678f93c0f9e6078b35c6331ed31e75f8ce0c2d8813000000000000220020305c12e1a0bc21e283c131cea1c66d68857d28b7b2fce0a6fbc40c164852121b8813000000000000220020305c12e1a0bc21e283c131cea1c66d68857d28b7b2fce0a6fbc40c164852121bc0c62d0000000000160014ccf1af2f2aabee14bb40fa3851ab2301de843110a69f6a00000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0400483045022100d25455151be075bae8b3400d0825341a3c25a1a5258b84ad2546c09539a83bc602203c1a4ac19c3ac415af7f6a98348f8d7e94fc0ab82dbed54c3c40134f465d027f0147304402206cda85b2811a211aa70fb74abf23303d87c4355ccf2c2c7954d4137c4fb26a830220719402ab3fef1cbaf42ba42fe437e9bed1e45f84547d603bf7af3fb88f50193301475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220 - num_htlcs: 3 - # signature for output #0 (htlc-success for htlc #1) - remote_htlc_signature = 30450221008117770f1d750a8af7a23f27c51a22f66386ea8f074115d0feeac7fe0f077f6102203d4d8dab53ef695b634786224ae5138c81d223b0493fd4637a448e31f734bb30 - # local_htlc_signature = 3045022100f2868e4380ac389960b96a8d01bd0d7ae845d24ad67993f1680e207864d5d3ae0220779ad0a943f73951d628a4f931c3044c51d393de1ab6f8283df41b68485f2b1b - htlc_success_tx (htlc #1): 0200000000010129253160416b9b2a2ecc303421b7fd1dee52d2e0b08d1a697f3979608334dbb9000000000000000000011f070000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e05004830450221008117770f1d750a8af7a23f27c51a22f66386ea8f074115d0feeac7fe0f077f6102203d4d8dab53ef695b634786224ae5138c81d223b0493fd4637a448e31f734bb3001483045022100f2868e4380ac389960b96a8d01bd0d7ae845d24ad67993f1680e207864d5d3ae0220779ad0a943f73951d628a4f931c3044c51d393de1ab6f8283df41b68485f2b1b012001010101010101010101010101010101010101010101010101010101010101018a76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a9144b6b2e5444c2639cc0fb7bcea5afba3f3cdce23988527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f501b175ac686800000000 - # signature for output #1 (htlc-timeout for htlc #6) - remote_htlc_signature = 30440220620b37379e587447e7ee9c8eed9e71b2d2ec29cadc89a4b5411ee6f9b013ca1b02201aaf26d1a03d901425fc44562bff77d5645cb54498e4d9a56c39456825974a3d - # local_htlc_signature = 304402204db4a1266aa8f0df66d35cd8b4269feef47f1cccebf7fdbb2b64cef35d72d79f022021b6d270a6d623cfec4cd0186096596d8deb17505d8c1c9785c719e0dbfa7e84 - htlc_timeout_tx (htlc #6): 0200000000010129253160416b9b2a2ecc303421b7fd1dee52d2e0b08d1a697f3979608334dbb901000000000000000001e1120000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e05004730440220620b37379e587447e7ee9c8eed9e71b2d2ec29cadc89a4b5411ee6f9b013ca1b02201aaf26d1a03d901425fc44562bff77d5645cb54498e4d9a56c39456825974a3d0147304402204db4a1266aa8f0df66d35cd8b4269feef47f1cccebf7fdbb2b64cef35d72d79f022021b6d270a6d623cfec4cd0186096596d8deb17505d8c1c9785c719e0dbfa7e8401008576a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9142002cc93ebefbb1b73f0af055dcc27a0b504ad7688ac6868f9010000 - # signature for output #2 (htlc-timeout for htlc #5) - remote_htlc_signature = 30450221009353b1d7e6313dc57a3781671b7ed9dde668b30e6f438ff08b13580937264de202200654599d2b9839fe9c88553f7c164a517fc8f0892e942b615460325fee7e0747 - # local_htlc_signature = 3045022100c11a3f26356524b556bdfaeeae80f3cdef2cdb1c42c49047014a56af6916e2cb0220230d0667dc5b86015056a2ae5158ff29cbaf9cde419677e058d23c59a0a07d31 - htlc_timeout_tx (htlc #5): 0200000000010129253160416b9b2a2ecc303421b7fd1dee52d2e0b08d1a697f3979608334dbb902000000000000000001e1120000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e05004830450221009353b1d7e6313dc57a3781671b7ed9dde668b30e6f438ff08b13580937264de202200654599d2b9839fe9c88553f7c164a517fc8f0892e942b615460325fee7e074701483045022100c11a3f26356524b556bdfaeeae80f3cdef2cdb1c42c49047014a56af6916e2cb0220230d0667dc5b86015056a2ae5158ff29cbaf9cde419677e058d23c59a0a07d3101008576a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9142002cc93ebefbb1b73f0af055dcc27a0b504ad7688ac6868fa010000 diff --git a/eclair-core/src/test/resources/bolt3-tx-test-vectors-static-remotekey-format.txt b/eclair-core/src/test/resources/bolt3-tx-test-vectors-static-remotekey-format.txt deleted file mode 100644 index 52a450bb5a..0000000000 --- a/eclair-core/src/test/resources/bolt3-tx-test-vectors-static-remotekey-format.txt +++ /dev/null @@ -1,367 +0,0 @@ - name: simple commitment tx with no HTLCs - to_local_msat: 7000000000 - to_remote_msat: 3000000000 - local_feerate_per_kw: 15000 - # base commitment transaction fee = 10860 - # actual commitment transaction fee = 10860 - # to_local amount 6989140 wscript 63210212a140cd0c6539d07cd08dfe09984dec3251ea808b892efeac3ede9402bf2b1967029000b2752103fd5960528dc152014952efdb702a88f71e3c1653b2314431701ec77e57fde83c68ac - # to_remote amount 3000000 P2WPKH(032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991) - remote_signature = 3045022100c3127b33dcc741dd6b05b1e63cbd1a9a7d816f37af9b6756fa2376b056f032370220408b96279808fe57eb7e463710804cdf4f108388bc5cf722d8c848d2c7f9f3b0 - # local_signature = 30440220616210b2cc4d3afb601013c373bbd8aac54febd9f15400379a8cb65ce7deca60022034236c010991beb7ff770510561ae8dc885b8d38d1947248c38f2ae055647142 - output commit_tx: 02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b8002c0c62d0000000000160014cc1b07838e387deacd0e5232e1e8b49f4c29e48454a56a00000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e04004730440220616210b2cc4d3afb601013c373bbd8aac54febd9f15400379a8cb65ce7deca60022034236c010991beb7ff770510561ae8dc885b8d38d1947248c38f2ae05564714201483045022100c3127b33dcc741dd6b05b1e63cbd1a9a7d816f37af9b6756fa2376b056f032370220408b96279808fe57eb7e463710804cdf4f108388bc5cf722d8c848d2c7f9f3b001475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220 - num_htlcs: 0 - - name: commitment tx with all five HTLCs untrimmed (minimum feerate) - to_local_msat: 6988000000 - to_remote_msat: 3000000000 - local_feerate_per_kw: 0 - # base commitment transaction fee = 0 - # actual commitment transaction fee = 0 - # HTLC #2 offered amount 2000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a914b43e1b38138a41b37f7cd9a1d274bc63e3a9b5d188ac6868 - # HTLC #3 offered amount 3000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9148a486ff2e31d6158bf39e2608864d63fefd09d5b88ac6868 - # HTLC #0 received amount 1000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a914b8bcb07f6344b42ab04250c86a6e8b75d3fdbbc688527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f401b175ac6868 - # HTLC #1 received amount 2000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a9144b6b2e5444c2639cc0fb7bcea5afba3f3cdce23988527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f501b175ac6868 - # HTLC #4 received amount 4000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac6868 - # to_local amount 6988000 wscript 63210212a140cd0c6539d07cd08dfe09984dec3251ea808b892efeac3ede9402bf2b1967029000b2752103fd5960528dc152014952efdb702a88f71e3c1653b2314431701ec77e57fde83c68ac - # to_remote amount 3000000 P2WPKH(032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991) - remote_signature = 3044022009b048187705a8cbc9ad73adbe5af148c3d012e1f067961486c822c7af08158c022006d66f3704cfab3eb2dc49dae24e4aa22a6910fc9b424007583204e3621af2e5 - # local_signature = 304402206fc2d1f10ea59951eefac0b4b7c396a3c3d87b71ff0b019796ef4535beaf36f902201765b0181e514d04f4c8ad75659d7037be26cdb3f8bb6f78fe61decef484c3ea - output commit_tx: 02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b8007e80300000000000022002052bfef0479d7b293c27e0f1eb294bea154c63a3294ef092c19af51409bce0e2ad007000000000000220020403d394747cae42e98ff01734ad5c08f82ba123d3d9a620abda88989651e2ab5d007000000000000220020748eba944fedc8827f6b06bc44678f93c0f9e6078b35c6331ed31e75f8ce0c2db80b000000000000220020c20b5d1f8584fd90443e7b7b720136174fa4b9333c261d04dbbd012635c0f419a00f0000000000002200208c48d15160397c9731df9bc3b236656efb6665fbfe92b4a6878e88a499f741c4c0c62d0000000000160014cc1b07838e387deacd0e5232e1e8b49f4c29e484e0a06a00000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e040047304402206fc2d1f10ea59951eefac0b4b7c396a3c3d87b71ff0b019796ef4535beaf36f902201765b0181e514d04f4c8ad75659d7037be26cdb3f8bb6f78fe61decef484c3ea01473044022009b048187705a8cbc9ad73adbe5af148c3d012e1f067961486c822c7af08158c022006d66f3704cfab3eb2dc49dae24e4aa22a6910fc9b424007583204e3621af2e501475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220 - num_htlcs: 5 - # signature for output #0 (htlc-success for htlc #0) - remote_htlc_signature = 3045022100d9e29616b8f3959f1d3d7f7ce893ffedcdc407717d0de8e37d808c91d3a7c50d022078c3033f6d00095c8720a4bc943c1b45727818c082e4e3ddbc6d3116435b624b - # local_htlc_signature = 30440220636de5682ef0c5b61f124ec74e8aa2461a69777521d6998295dcea36bc3338110220165285594b23c50b28b82df200234566628a27bcd17f7f14404bd865354eb3ce - htlc_success_tx (htlc #0): 02000000000101ab84ff284f162cfbfef241f853b47d4368d171f9e2a1445160cd591c4c7d882b00000000000000000001e8030000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100d9e29616b8f3959f1d3d7f7ce893ffedcdc407717d0de8e37d808c91d3a7c50d022078c3033f6d00095c8720a4bc943c1b45727818c082e4e3ddbc6d3116435b624b014730440220636de5682ef0c5b61f124ec74e8aa2461a69777521d6998295dcea36bc3338110220165285594b23c50b28b82df200234566628a27bcd17f7f14404bd865354eb3ce012000000000000000000000000000000000000000000000000000000000000000008a76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a914b8bcb07f6344b42ab04250c86a6e8b75d3fdbbc688527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f401b175ac686800000000 - # signature for output #1 (htlc-timeout for htlc #2) - remote_htlc_signature = 30440220649fe8b20e67e46cbb0d09b4acea87dbec001b39b08dee7bdd0b1f03922a8640022037c462dff79df501cecfdb12ea7f4de91f99230bb544726f6e04527b1f896004 - # local_htlc_signature = 3045022100803159dee7935dba4a1d36a61055ce8fd62caa528573cc221ae288515405a252022029c59e7cffce374fe860100a4a63787e105c3cf5156d40b12dd53ff55ac8cf3f - htlc_timeout_tx (htlc #2): 02000000000101ab84ff284f162cfbfef241f853b47d4368d171f9e2a1445160cd591c4c7d882b01000000000000000001d0070000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e05004730440220649fe8b20e67e46cbb0d09b4acea87dbec001b39b08dee7bdd0b1f03922a8640022037c462dff79df501cecfdb12ea7f4de91f99230bb544726f6e04527b1f89600401483045022100803159dee7935dba4a1d36a61055ce8fd62caa528573cc221ae288515405a252022029c59e7cffce374fe860100a4a63787e105c3cf5156d40b12dd53ff55ac8cf3f01008576a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a914b43e1b38138a41b37f7cd9a1d274bc63e3a9b5d188ac6868f6010000 - # signature for output #2 (htlc-success for htlc #1) - remote_htlc_signature = 30440220770fc321e97a19f38985f2e7732dd9fe08d16a2efa4bcbc0429400a447faf49102204d40b417f3113e1b0944ae0986f517564ab4acd3d190503faf97a6e420d43352 - # local_htlc_signature = 3045022100a437cc2ce77400ecde441b3398fea3c3ad8bdad8132be818227fe3c5b8345989022069d45e7fa0ae551ec37240845e2c561ceb2567eacf3076a6a43a502d05865faa - htlc_success_tx (htlc #1): 02000000000101ab84ff284f162cfbfef241f853b47d4368d171f9e2a1445160cd591c4c7d882b02000000000000000001d0070000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e05004730440220770fc321e97a19f38985f2e7732dd9fe08d16a2efa4bcbc0429400a447faf49102204d40b417f3113e1b0944ae0986f517564ab4acd3d190503faf97a6e420d4335201483045022100a437cc2ce77400ecde441b3398fea3c3ad8bdad8132be818227fe3c5b8345989022069d45e7fa0ae551ec37240845e2c561ceb2567eacf3076a6a43a502d05865faa012001010101010101010101010101010101010101010101010101010101010101018a76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a9144b6b2e5444c2639cc0fb7bcea5afba3f3cdce23988527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f501b175ac686800000000 - # signature for output #3 (htlc-timeout for htlc #3) - remote_htlc_signature = 304402207bcbf4f60a9829b05d2dbab84ed593e0291836be715dc7db6b72a64caf646af802201e489a5a84f7c5cc130398b841d138d031a5137ac8f4c49c770a4959dc3c1363 - # local_htlc_signature = 304402203121d9b9c055f354304b016a36662ee99e1110d9501cb271b087ddb6f382c2c80220549882f3f3b78d9c492de47543cb9a697cecc493174726146536c5954dac7487 - htlc_timeout_tx (htlc #3): 02000000000101ab84ff284f162cfbfef241f853b47d4368d171f9e2a1445160cd591c4c7d882b03000000000000000001b80b0000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e050047304402207bcbf4f60a9829b05d2dbab84ed593e0291836be715dc7db6b72a64caf646af802201e489a5a84f7c5cc130398b841d138d031a5137ac8f4c49c770a4959dc3c13630147304402203121d9b9c055f354304b016a36662ee99e1110d9501cb271b087ddb6f382c2c80220549882f3f3b78d9c492de47543cb9a697cecc493174726146536c5954dac748701008576a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9148a486ff2e31d6158bf39e2608864d63fefd09d5b88ac6868f7010000 - # signature for output #4 (htlc-success for htlc #4) - remote_htlc_signature = 3044022076dca5cb81ba7e466e349b7128cdba216d4d01659e29b96025b9524aaf0d1899022060de85697b88b21c749702b7d2cfa7dfeaa1f472c8f1d7d9c23f2bf968464b87 - # local_htlc_signature = 3045022100d9080f103cc92bac15ec42464a95f070c7fb6925014e673ee2ea1374d36a7f7502200c65294d22eb20d48564954d5afe04a385551919d8b2ddb4ae2459daaeee1d95 - htlc_success_tx (htlc #4): 02000000000101ab84ff284f162cfbfef241f853b47d4368d171f9e2a1445160cd591c4c7d882b04000000000000000001a00f0000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500473044022076dca5cb81ba7e466e349b7128cdba216d4d01659e29b96025b9524aaf0d1899022060de85697b88b21c749702b7d2cfa7dfeaa1f472c8f1d7d9c23f2bf968464b8701483045022100d9080f103cc92bac15ec42464a95f070c7fb6925014e673ee2ea1374d36a7f7502200c65294d22eb20d48564954d5afe04a385551919d8b2ddb4ae2459daaeee1d95012004040404040404040404040404040404040404040404040404040404040404048a76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac686800000000 - - name: commitment tx with seven outputs untrimmed (maximum feerate) - to_local_msat: 6988000000 - to_remote_msat: 3000000000 - local_feerate_per_kw: 647 - # base commitment transaction fee = 1024 - # actual commitment transaction fee = 1024 - # HTLC #2 offered amount 2000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a914b43e1b38138a41b37f7cd9a1d274bc63e3a9b5d188ac6868 - # HTLC #3 offered amount 3000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9148a486ff2e31d6158bf39e2608864d63fefd09d5b88ac6868 - # HTLC #0 received amount 1000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a914b8bcb07f6344b42ab04250c86a6e8b75d3fdbbc688527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f401b175ac6868 - # HTLC #1 received amount 2000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a9144b6b2e5444c2639cc0fb7bcea5afba3f3cdce23988527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f501b175ac6868 - # HTLC #4 received amount 4000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac6868 - # to_local amount 6986976 wscript 63210212a140cd0c6539d07cd08dfe09984dec3251ea808b892efeac3ede9402bf2b1967029000b2752103fd5960528dc152014952efdb702a88f71e3c1653b2314431701ec77e57fde83c68ac - # to_remote amount 3000000 P2WPKH(032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991) - remote_signature = 3045022100a135f9e8a5ed25f7277446c67956b00ce6f610ead2bdec2c2f686155b7814772022059f1f6e1a8b336a68efcc1af3fe4d422d4827332b5b067501b099c47b7b5b5ee - # local_signature = 30450221009ec15c687898bb4da8b3a833e5ab8bfc51ec6e9202aaa8e66611edfd4a85ed1102203d7183e45078b9735c93450bc3415d3e5a8c576141a711ec6ddcb4a893926bb7 - output commit_tx: 02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b8007e80300000000000022002052bfef0479d7b293c27e0f1eb294bea154c63a3294ef092c19af51409bce0e2ad007000000000000220020403d394747cae42e98ff01734ad5c08f82ba123d3d9a620abda88989651e2ab5d007000000000000220020748eba944fedc8827f6b06bc44678f93c0f9e6078b35c6331ed31e75f8ce0c2db80b000000000000220020c20b5d1f8584fd90443e7b7b720136174fa4b9333c261d04dbbd012635c0f419a00f0000000000002200208c48d15160397c9731df9bc3b236656efb6665fbfe92b4a6878e88a499f741c4c0c62d0000000000160014cc1b07838e387deacd0e5232e1e8b49f4c29e484e09c6a00000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e04004830450221009ec15c687898bb4da8b3a833e5ab8bfc51ec6e9202aaa8e66611edfd4a85ed1102203d7183e45078b9735c93450bc3415d3e5a8c576141a711ec6ddcb4a893926bb701483045022100a135f9e8a5ed25f7277446c67956b00ce6f610ead2bdec2c2f686155b7814772022059f1f6e1a8b336a68efcc1af3fe4d422d4827332b5b067501b099c47b7b5b5ee01475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220 - num_htlcs: 5 - # signature for output #0 (htlc-success for htlc #0) - remote_htlc_signature = 30450221008437627f9ad84ac67052e2a414a4367b8556fd1f94d8b02590f89f50525cd33502205b9c21ff6e7fc864f2352746ad8ba59182510819acb644e25b8a12fc37bbf24f - # local_htlc_signature = 30440220344b0deb055230d01703e6c7acd45853c4af2328b49b5d8af4f88a060733406602202ea64f2a43d5751edfe75503cbc35a62e3141b5ed032fa03360faf4ca66f670b - htlc_success_tx (htlc #0): 020000000001012cfb3e4788c206881d38f2996b6cb2109b5935acb527d14bdaa7b908afa9b2fe0000000000000000000122020000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e05004830450221008437627f9ad84ac67052e2a414a4367b8556fd1f94d8b02590f89f50525cd33502205b9c21ff6e7fc864f2352746ad8ba59182510819acb644e25b8a12fc37bbf24f014730440220344b0deb055230d01703e6c7acd45853c4af2328b49b5d8af4f88a060733406602202ea64f2a43d5751edfe75503cbc35a62e3141b5ed032fa03360faf4ca66f670b012000000000000000000000000000000000000000000000000000000000000000008a76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a914b8bcb07f6344b42ab04250c86a6e8b75d3fdbbc688527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f401b175ac686800000000 - # signature for output #1 (htlc-timeout for htlc #2) - remote_htlc_signature = 304402205a67f92bf6845cf2892b48d874ac1daf88a36495cf8a06f93d83180d930a6f75022031da1621d95c3f335cc06a3056cf960199dae600b7cf89088f65fc53cdbef28c - # local_htlc_signature = 30450221009e5e3822b0185c6799a95288c597b671d6cc69ab80f43740f00c6c3d0752bdda02206da947a74bd98f3175324dc56fdba86cc783703a120a6f0297537e60632f4c7f - htlc_timeout_tx (htlc #2): 020000000001012cfb3e4788c206881d38f2996b6cb2109b5935acb527d14bdaa7b908afa9b2fe0100000000000000000124060000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e050047304402205a67f92bf6845cf2892b48d874ac1daf88a36495cf8a06f93d83180d930a6f75022031da1621d95c3f335cc06a3056cf960199dae600b7cf89088f65fc53cdbef28c014830450221009e5e3822b0185c6799a95288c597b671d6cc69ab80f43740f00c6c3d0752bdda02206da947a74bd98f3175324dc56fdba86cc783703a120a6f0297537e60632f4c7f01008576a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a914b43e1b38138a41b37f7cd9a1d274bc63e3a9b5d188ac6868f6010000 - # signature for output #2 (htlc-success for htlc #1) - remote_htlc_signature = 30440220437e21766054a3eef7f65690c5bcfa9920babbc5af92b819f772f6ea96df6c7402207173622024bd97328cfb26c6665e25c2f5d67c319443ccdc60c903217005d8c8 - # local_htlc_signature = 3045022100fcfc47e36b712624677626cef3dc1d67f6583bd46926a6398fe6b00b0c9a37760220525788257b187fc775c6370d04eadf34d06f3650a63f8df851cee0ecb47a1673 - htlc_success_tx (htlc #1): 020000000001012cfb3e4788c206881d38f2996b6cb2109b5935acb527d14bdaa7b908afa9b2fe020000000000000000010a060000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e05004730440220437e21766054a3eef7f65690c5bcfa9920babbc5af92b819f772f6ea96df6c7402207173622024bd97328cfb26c6665e25c2f5d67c319443ccdc60c903217005d8c801483045022100fcfc47e36b712624677626cef3dc1d67f6583bd46926a6398fe6b00b0c9a37760220525788257b187fc775c6370d04eadf34d06f3650a63f8df851cee0ecb47a1673012001010101010101010101010101010101010101010101010101010101010101018a76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a9144b6b2e5444c2639cc0fb7bcea5afba3f3cdce23988527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f501b175ac686800000000 - # signature for output #3 (htlc-timeout for htlc #3) - remote_htlc_signature = 304402207436e10737e4df499fc051686d3e11a5bb2310e4d1f1e691d287cef66514791202207cb58e71a6b7a42dd001b7e3ae672ea4f71ea3e1cd412b742e9124abb0739c64 - # local_htlc_signature = 3045022100e78211b8409afb7255ffe37337da87f38646f1faebbdd61bc1920d69e3ead67a02201a626305adfcd16bfb7e9340928d9b6305464eab4aa4c4a3af6646e9b9f69dee - htlc_timeout_tx (htlc #3): 020000000001012cfb3e4788c206881d38f2996b6cb2109b5935acb527d14bdaa7b908afa9b2fe030000000000000000010c0a0000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e050047304402207436e10737e4df499fc051686d3e11a5bb2310e4d1f1e691d287cef66514791202207cb58e71a6b7a42dd001b7e3ae672ea4f71ea3e1cd412b742e9124abb0739c6401483045022100e78211b8409afb7255ffe37337da87f38646f1faebbdd61bc1920d69e3ead67a02201a626305adfcd16bfb7e9340928d9b6305464eab4aa4c4a3af6646e9b9f69dee01008576a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9148a486ff2e31d6158bf39e2608864d63fefd09d5b88ac6868f7010000 - # signature for output #4 (htlc-success for htlc #4) - remote_htlc_signature = 30450221009acd6a827a76bfee50806178dfe0495cd4e1d9c58279c194c7b01520fe68cb8d022024d439047c368883e570997a7d40f0b430cb5a742f507965e7d3063ae3feccca - # local_htlc_signature = 3044022048762cf546bbfe474f1536365ea7c416e3c0389d60558bc9412cb148fb6ab68202207215d7083b75c96ff9d2b08c59c34e287b66820f530b486a9aa4cdd9c347d5b9 - htlc_success_tx (htlc #4): 020000000001012cfb3e4788c206881d38f2996b6cb2109b5935acb527d14bdaa7b908afa9b2fe04000000000000000001da0d0000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e05004830450221009acd6a827a76bfee50806178dfe0495cd4e1d9c58279c194c7b01520fe68cb8d022024d439047c368883e570997a7d40f0b430cb5a742f507965e7d3063ae3feccca01473044022048762cf546bbfe474f1536365ea7c416e3c0389d60558bc9412cb148fb6ab68202207215d7083b75c96ff9d2b08c59c34e287b66820f530b486a9aa4cdd9c347d5b9012004040404040404040404040404040404040404040404040404040404040404048a76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac686800000000 - - name: commitment tx with six outputs untrimmed (minimum feerate) - to_local_msat: 6988000000 - to_remote_msat: 3000000000 - local_feerate_per_kw: 648 - # base commitment transaction fee = 914 - # actual commitment transaction fee = 1914 - # HTLC #2 offered amount 2000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a914b43e1b38138a41b37f7cd9a1d274bc63e3a9b5d188ac6868 - # HTLC #3 offered amount 3000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9148a486ff2e31d6158bf39e2608864d63fefd09d5b88ac6868 - # HTLC #1 received amount 2000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a9144b6b2e5444c2639cc0fb7bcea5afba3f3cdce23988527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f501b175ac6868 - # HTLC #4 received amount 4000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac6868 - # to_local amount 6987086 wscript 63210212a140cd0c6539d07cd08dfe09984dec3251ea808b892efeac3ede9402bf2b1967029000b2752103fd5960528dc152014952efdb702a88f71e3c1653b2314431701ec77e57fde83c68ac - # to_remote amount 3000000 P2WPKH(032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991) - remote_signature = 304402203948f900a5506b8de36a4d8502f94f21dd84fd9c2314ab427d52feaa7a0a19f2022059b6a37a4adaa2c5419dc8aea63c6e2a2ec4c4bde46207f6dc1fcd22152fc6e5 - # local_signature = 3045022100b15f72908ba3382a34ca5b32519240a22300cc6015b6f9418635fb41f3d01d8802207adb331b9ed1575383dca0f2355e86c173802feecf8298fbea53b9d4610583e9 - output commit_tx: 02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b8006d007000000000000220020403d394747cae42e98ff01734ad5c08f82ba123d3d9a620abda88989651e2ab5d007000000000000220020748eba944fedc8827f6b06bc44678f93c0f9e6078b35c6331ed31e75f8ce0c2db80b000000000000220020c20b5d1f8584fd90443e7b7b720136174fa4b9333c261d04dbbd012635c0f419a00f0000000000002200208c48d15160397c9731df9bc3b236656efb6665fbfe92b4a6878e88a499f741c4c0c62d0000000000160014cc1b07838e387deacd0e5232e1e8b49f4c29e4844e9d6a00000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0400483045022100b15f72908ba3382a34ca5b32519240a22300cc6015b6f9418635fb41f3d01d8802207adb331b9ed1575383dca0f2355e86c173802feecf8298fbea53b9d4610583e90147304402203948f900a5506b8de36a4d8502f94f21dd84fd9c2314ab427d52feaa7a0a19f2022059b6a37a4adaa2c5419dc8aea63c6e2a2ec4c4bde46207f6dc1fcd22152fc6e501475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220 - num_htlcs: 4 - # signature for output #0 (htlc-timeout for htlc #2) - remote_htlc_signature = 3045022100a031202f3be94678f0e998622ee95ebb6ada8da1e9a5110228b5e04a747351e4022010ca6a21e18314ed53cfaae3b1f51998552a61a468e596368829a50ce40110e0 - # local_htlc_signature = 304502210097e1873b57267730154595187a34949d3744f52933070c74757005e61ce2112e02204ecfba2aa42d4f14bdf8bad4206bb97217b702e6c433e0e1b0ce6587e6d46ec6 - htlc_timeout_tx (htlc #2): 020000000001010f44041fdfba175987cf4e6135ba2a154e3b7fb96483dc0ed5efc0678e5b6bf10000000000000000000123060000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100a031202f3be94678f0e998622ee95ebb6ada8da1e9a5110228b5e04a747351e4022010ca6a21e18314ed53cfaae3b1f51998552a61a468e596368829a50ce40110e00148304502210097e1873b57267730154595187a34949d3744f52933070c74757005e61ce2112e02204ecfba2aa42d4f14bdf8bad4206bb97217b702e6c433e0e1b0ce6587e6d46ec601008576a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a914b43e1b38138a41b37f7cd9a1d274bc63e3a9b5d188ac6868f6010000 - # signature for output #1 (htlc-success for htlc #1) - remote_htlc_signature = 304402202361012a634aee7835c5ecdd6413dcffa8f404b7e77364c792cff984e4ee71e90220715c5e90baa08daa45a7439b1ee4fa4843ed77b19c058240b69406606d384124 - # local_htlc_signature = 3044022019de73b00f1d818fb388e83b2c8c31f6bce35ac624e215bc12f88f9dc33edf48022006ff814bb9f700ee6abc3294e146fac3efd4f13f0005236b41c0a946ee00c9ae - htlc_success_tx (htlc #1): 020000000001010f44041fdfba175987cf4e6135ba2a154e3b7fb96483dc0ed5efc0678e5b6bf10100000000000000000109060000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e050047304402202361012a634aee7835c5ecdd6413dcffa8f404b7e77364c792cff984e4ee71e90220715c5e90baa08daa45a7439b1ee4fa4843ed77b19c058240b69406606d38412401473044022019de73b00f1d818fb388e83b2c8c31f6bce35ac624e215bc12f88f9dc33edf48022006ff814bb9f700ee6abc3294e146fac3efd4f13f0005236b41c0a946ee00c9ae012001010101010101010101010101010101010101010101010101010101010101018a76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a9144b6b2e5444c2639cc0fb7bcea5afba3f3cdce23988527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f501b175ac686800000000 - # signature for output #2 (htlc-timeout for htlc #3) - remote_htlc_signature = 304402207e8e82cd71ed4febeb593732c260456836e97d81896153ecd2b3cf320ca6861702202dd4a30f68f98ced7cc56a36369ac1fdd978248c5ff4ed204fc00cc625532989 - # local_htlc_signature = 3045022100bd0be6100c4fd8f102ec220e1b053e4c4e2ecca25615490150007b40d314dc3902201a1e0ea266965b43164d9e6576f58fa6726d42883dd1c3996d2925c2e2260796 - htlc_timeout_tx (htlc #3): 020000000001010f44041fdfba175987cf4e6135ba2a154e3b7fb96483dc0ed5efc0678e5b6bf1020000000000000000010b0a0000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e050047304402207e8e82cd71ed4febeb593732c260456836e97d81896153ecd2b3cf320ca6861702202dd4a30f68f98ced7cc56a36369ac1fdd978248c5ff4ed204fc00cc62553298901483045022100bd0be6100c4fd8f102ec220e1b053e4c4e2ecca25615490150007b40d314dc3902201a1e0ea266965b43164d9e6576f58fa6726d42883dd1c3996d2925c2e226079601008576a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9148a486ff2e31d6158bf39e2608864d63fefd09d5b88ac6868f7010000 - # signature for output #3 (htlc-success for htlc #4) - remote_htlc_signature = 3044022024cd52e4198c8ae0e414a86d86b5a65ea7450f2eb4e783096736d93395eca5ce022078f0094745b45be4d4b2b04dd5978c9e66ba49109e5704403e84aaf5f387d6be - # local_htlc_signature = 3045022100bbfb9d0a946d420807c86e985d636cceb16e71c3694ed186316251a00cbd807202207773223f9a337e145f64673825be9b30d07ef1542c82188b264bedcf7cda78c6 - htlc_success_tx (htlc #4): 020000000001010f44041fdfba175987cf4e6135ba2a154e3b7fb96483dc0ed5efc0678e5b6bf103000000000000000001d90d0000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500473044022024cd52e4198c8ae0e414a86d86b5a65ea7450f2eb4e783096736d93395eca5ce022078f0094745b45be4d4b2b04dd5978c9e66ba49109e5704403e84aaf5f387d6be01483045022100bbfb9d0a946d420807c86e985d636cceb16e71c3694ed186316251a00cbd807202207773223f9a337e145f64673825be9b30d07ef1542c82188b264bedcf7cda78c6012004040404040404040404040404040404040404040404040404040404040404048a76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac686800000000 - - name: commitment tx with six outputs untrimmed (maximum feerate) - to_local_msat: 6988000000 - to_remote_msat: 3000000000 - local_feerate_per_kw: 2069 - # base commitment transaction fee = 2921 - # actual commitment transaction fee = 3921 - # HTLC #2 offered amount 2000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a914b43e1b38138a41b37f7cd9a1d274bc63e3a9b5d188ac6868 - # HTLC #3 offered amount 3000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9148a486ff2e31d6158bf39e2608864d63fefd09d5b88ac6868 - # HTLC #1 received amount 2000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a9144b6b2e5444c2639cc0fb7bcea5afba3f3cdce23988527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f501b175ac6868 - # HTLC #4 received amount 4000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac6868 - # to_local amount 6985079 wscript 63210212a140cd0c6539d07cd08dfe09984dec3251ea808b892efeac3ede9402bf2b1967029000b2752103fd5960528dc152014952efdb702a88f71e3c1653b2314431701ec77e57fde83c68ac - # to_remote amount 3000000 P2WPKH(032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991) - remote_signature = 304502210090b96a2498ce0c0f2fadbec2aab278fed54c1a7838df793ec4d2c78d96ec096202204fdd439c50f90d483baa7b68feeef4bd33bc277695405447bcd0bfb2ca34d7bc - # local_signature = 3045022100ad9a9bbbb75d506ca3b716b336ee3cf975dd7834fcf129d7dd188146eb58a8b4022061a759ee417339f7fe2ea1e8deb83abb6a74db31a09b7648a932a639cda23e33 - output commit_tx: 02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b8006d007000000000000220020403d394747cae42e98ff01734ad5c08f82ba123d3d9a620abda88989651e2ab5d007000000000000220020748eba944fedc8827f6b06bc44678f93c0f9e6078b35c6331ed31e75f8ce0c2db80b000000000000220020c20b5d1f8584fd90443e7b7b720136174fa4b9333c261d04dbbd012635c0f419a00f0000000000002200208c48d15160397c9731df9bc3b236656efb6665fbfe92b4a6878e88a499f741c4c0c62d0000000000160014cc1b07838e387deacd0e5232e1e8b49f4c29e48477956a00000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0400483045022100ad9a9bbbb75d506ca3b716b336ee3cf975dd7834fcf129d7dd188146eb58a8b4022061a759ee417339f7fe2ea1e8deb83abb6a74db31a09b7648a932a639cda23e330148304502210090b96a2498ce0c0f2fadbec2aab278fed54c1a7838df793ec4d2c78d96ec096202204fdd439c50f90d483baa7b68feeef4bd33bc277695405447bcd0bfb2ca34d7bc01475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220 - num_htlcs: 4 - # signature for output #0 (htlc-timeout for htlc #2) - remote_htlc_signature = 3045022100f33513ee38abf1c582876f921f8fddc06acff48e04515532a32d3938de938ffd02203aa308a2c1863b7d6fdf53159a1465bf2e115c13152546cc5d74483ceaa7f699 - # local_htlc_signature = 3045022100a637902a5d4c9ba9e7c472a225337d5aac9e2e3f6744f76e237132e7619ba0400220035c60d784a031c0d9f6df66b7eab8726a5c25397399ee4aa960842059eb3f9d - htlc_timeout_tx (htlc #2): 02000000000101adbe717a63fb658add30ada1e6e12ed257637581898abe475c11d7bbcd65bd4d0000000000000000000175020000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100f33513ee38abf1c582876f921f8fddc06acff48e04515532a32d3938de938ffd02203aa308a2c1863b7d6fdf53159a1465bf2e115c13152546cc5d74483ceaa7f69901483045022100a637902a5d4c9ba9e7c472a225337d5aac9e2e3f6744f76e237132e7619ba0400220035c60d784a031c0d9f6df66b7eab8726a5c25397399ee4aa960842059eb3f9d01008576a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a914b43e1b38138a41b37f7cd9a1d274bc63e3a9b5d188ac6868f6010000 - # signature for output #1 (htlc-success for htlc #1) - remote_htlc_signature = 3045022100ce07682cf4b90093c22dc2d9ab2a77ad6803526b655ef857221cc96af5c9e0bf02200f501cee22e7a268af40b555d15a8237c9f36ad67ef1841daf9f6a0267b1e6df - # local_htlc_signature = 3045022100e57e46234f8782d3ff7aa593b4f7446fb5316c842e693dc63ee324fd49f6a1c302204a2f7b44c48bd26e1554422afae13153eb94b29d3687b733d18930615fb2db61 - htlc_success_tx (htlc #1): 02000000000101adbe717a63fb658add30ada1e6e12ed257637581898abe475c11d7bbcd65bd4d0100000000000000000122020000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100ce07682cf4b90093c22dc2d9ab2a77ad6803526b655ef857221cc96af5c9e0bf02200f501cee22e7a268af40b555d15a8237c9f36ad67ef1841daf9f6a0267b1e6df01483045022100e57e46234f8782d3ff7aa593b4f7446fb5316c842e693dc63ee324fd49f6a1c302204a2f7b44c48bd26e1554422afae13153eb94b29d3687b733d18930615fb2db61012001010101010101010101010101010101010101010101010101010101010101018a76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a9144b6b2e5444c2639cc0fb7bcea5afba3f3cdce23988527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f501b175ac686800000000 - # signature for output #2 (htlc-timeout for htlc #3) - remote_htlc_signature = 3045022100e3e35492e55f82ec0bc2f317ffd7a486d1f7024330fe9743c3559fc39f32ef0c02203d1d4db651fc388a91d5ad8ecdd8e83673063bc8eefe27cfd8c189090e3a23e0 - # local_htlc_signature = 3044022068613fb1b98eb3aec7f44c5b115b12343c2f066c4277c82b5f873dfe68f37f50022028109b4650f3f528ca4bfe9a467aff2e3e43893b61b5159157119d5d95cf1c18 - htlc_timeout_tx (htlc #3): 02000000000101adbe717a63fb658add30ada1e6e12ed257637581898abe475c11d7bbcd65bd4d020000000000000000015d060000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100e3e35492e55f82ec0bc2f317ffd7a486d1f7024330fe9743c3559fc39f32ef0c02203d1d4db651fc388a91d5ad8ecdd8e83673063bc8eefe27cfd8c189090e3a23e001473044022068613fb1b98eb3aec7f44c5b115b12343c2f066c4277c82b5f873dfe68f37f50022028109b4650f3f528ca4bfe9a467aff2e3e43893b61b5159157119d5d95cf1c1801008576a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9148a486ff2e31d6158bf39e2608864d63fefd09d5b88ac6868f7010000 - # signature for output #3 (htlc-success for htlc #4) - remote_htlc_signature = 304402207475aeb0212ef9bf5130b60937817ad88c9a87976988ef1f323f026148cc4a850220739fea17ad3257dcad72e509c73eebe86bee30b178467b9fdab213d631b109df - # local_htlc_signature = 3045022100d315522e09e7d53d2a659a79cb67fef56d6c4bddf3f46df6772d0d20a7beb7c8022070bcc17e288607b6a72be0bd83368bb6d53488db266c1cdb4d72214e4f02ac33 - htlc_success_tx (htlc #4): 02000000000101adbe717a63fb658add30ada1e6e12ed257637581898abe475c11d7bbcd65bd4d03000000000000000001f2090000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e050047304402207475aeb0212ef9bf5130b60937817ad88c9a87976988ef1f323f026148cc4a850220739fea17ad3257dcad72e509c73eebe86bee30b178467b9fdab213d631b109df01483045022100d315522e09e7d53d2a659a79cb67fef56d6c4bddf3f46df6772d0d20a7beb7c8022070bcc17e288607b6a72be0bd83368bb6d53488db266c1cdb4d72214e4f02ac33012004040404040404040404040404040404040404040404040404040404040404048a76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac686800000000 - - name: commitment tx with five outputs untrimmed (minimum feerate) - to_local_msat: 6988000000 - to_remote_msat: 3000000000 - local_feerate_per_kw: 2070 - # base commitment transaction fee = 2566 - # actual commitment transaction fee = 5566 - # HTLC #2 offered amount 2000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a914b43e1b38138a41b37f7cd9a1d274bc63e3a9b5d188ac6868 - # HTLC #3 offered amount 3000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9148a486ff2e31d6158bf39e2608864d63fefd09d5b88ac6868 - # HTLC #4 received amount 4000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac6868 - # to_local amount 6985434 wscript 63210212a140cd0c6539d07cd08dfe09984dec3251ea808b892efeac3ede9402bf2b1967029000b2752103fd5960528dc152014952efdb702a88f71e3c1653b2314431701ec77e57fde83c68ac - # to_remote amount 3000000 P2WPKH(032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991) - remote_signature = 304402204ca1ba260dee913d318271d86e10ca0f5883026fb5653155cff600fb40895223022037b145204b7054a40e08bb1fefbd826f827b40838d3e501423bcc57924bcb50c - # local_signature = 3044022001014419b5ba00e083ac4e0a85f19afc848aacac2d483b4b525d15e2ae5adbfe022015ebddad6ee1e72b47cb09f3e78459da5be01ccccd95dceca0e056a00cc773c1 - output commit_tx: 02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b8005d007000000000000220020403d394747cae42e98ff01734ad5c08f82ba123d3d9a620abda88989651e2ab5b80b000000000000220020c20b5d1f8584fd90443e7b7b720136174fa4b9333c261d04dbbd012635c0f419a00f0000000000002200208c48d15160397c9731df9bc3b236656efb6665fbfe92b4a6878e88a499f741c4c0c62d0000000000160014cc1b07838e387deacd0e5232e1e8b49f4c29e484da966a00000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0400473044022001014419b5ba00e083ac4e0a85f19afc848aacac2d483b4b525d15e2ae5adbfe022015ebddad6ee1e72b47cb09f3e78459da5be01ccccd95dceca0e056a00cc773c10147304402204ca1ba260dee913d318271d86e10ca0f5883026fb5653155cff600fb40895223022037b145204b7054a40e08bb1fefbd826f827b40838d3e501423bcc57924bcb50c01475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220 - num_htlcs: 3 - # signature for output #0 (htlc-timeout for htlc #2) - remote_htlc_signature = 304402205f6b6d12d8d2529fb24f4445630566cf4abbd0f9330ab6c2bdb94222d6a2a0c502202f556258ae6f05b193749e4c541dfcc13b525a5422f6291f073f15617ba8579b - # local_htlc_signature = 30440220150b11069454da70caf2492ded9e0065c9a57f25ac2a4c52657b1d15b6c6ed85022068a38833b603c8892717206383611bad210f1cbb4b1f87ea29c6c65b9e1cb3e5 - htlc_timeout_tx (htlc #2): 02000000000101403ad7602b43293497a3a2235a12ecefda4f3a1f1d06e49b1786d945685de1ff0000000000000000000174020000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e050047304402205f6b6d12d8d2529fb24f4445630566cf4abbd0f9330ab6c2bdb94222d6a2a0c502202f556258ae6f05b193749e4c541dfcc13b525a5422f6291f073f15617ba8579b014730440220150b11069454da70caf2492ded9e0065c9a57f25ac2a4c52657b1d15b6c6ed85022068a38833b603c8892717206383611bad210f1cbb4b1f87ea29c6c65b9e1cb3e501008576a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a914b43e1b38138a41b37f7cd9a1d274bc63e3a9b5d188ac6868f6010000 - # signature for output #1 (htlc-timeout for htlc #3) - remote_htlc_signature = 3045022100f960dfb1c9aee7ce1437efa65b523e399383e8149790e05d8fed27ff6e42fe0002202fe8613e062ffe0b0c518cc4101fba1c6de70f64a5bcc7ae663f2efae43b8546 - # local_htlc_signature = 30450221009a6ed18e6873bc3644332a6ee21c152a5b102821865350df7a8c74451a51f9f2022050d801fb4895d7d7fbf452824c0168347f5c0cbe821cf6a97a63af5b8b2563c6 - htlc_timeout_tx (htlc #3): 02000000000101403ad7602b43293497a3a2235a12ecefda4f3a1f1d06e49b1786d945685de1ff010000000000000000015c060000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100f960dfb1c9aee7ce1437efa65b523e399383e8149790e05d8fed27ff6e42fe0002202fe8613e062ffe0b0c518cc4101fba1c6de70f64a5bcc7ae663f2efae43b8546014830450221009a6ed18e6873bc3644332a6ee21c152a5b102821865350df7a8c74451a51f9f2022050d801fb4895d7d7fbf452824c0168347f5c0cbe821cf6a97a63af5b8b2563c601008576a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9148a486ff2e31d6158bf39e2608864d63fefd09d5b88ac6868f7010000 - # signature for output #2 (htlc-success for htlc #4) - remote_htlc_signature = 3045022100ae5fc7717ae684bc1fcf9020854e5dbe9842c9e7472879ac06ff95ac2bb10e4e022057728ada4c00083a3e65493fb5d50a232165948a1a0f530ef63185c2c8c56504 - # local_htlc_signature = 30440220408ad3009827a8fccf774cb285587686bfb2ed041f89a89453c311ce9c8ee0f902203c7392d9f8306d3a46522a66bd2723a7eb2628cb2d9b34d4c104f1766bf37502 - htlc_success_tx (htlc #4): 02000000000101403ad7602b43293497a3a2235a12ecefda4f3a1f1d06e49b1786d945685de1ff02000000000000000001f1090000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100ae5fc7717ae684bc1fcf9020854e5dbe9842c9e7472879ac06ff95ac2bb10e4e022057728ada4c00083a3e65493fb5d50a232165948a1a0f530ef63185c2c8c56504014730440220408ad3009827a8fccf774cb285587686bfb2ed041f89a89453c311ce9c8ee0f902203c7392d9f8306d3a46522a66bd2723a7eb2628cb2d9b34d4c104f1766bf37502012004040404040404040404040404040404040404040404040404040404040404048a76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac686800000000 - - name: commitment tx with five outputs untrimmed (maximum feerate) - to_local_msat: 6988000000 - to_remote_msat: 3000000000 - local_feerate_per_kw: 2194 - # base commitment transaction fee = 2720 - # actual commitment transaction fee = 5720 - # HTLC #2 offered amount 2000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a914b43e1b38138a41b37f7cd9a1d274bc63e3a9b5d188ac6868 - # HTLC #3 offered amount 3000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9148a486ff2e31d6158bf39e2608864d63fefd09d5b88ac6868 - # HTLC #4 received amount 4000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac6868 - # to_local amount 6985280 wscript 63210212a140cd0c6539d07cd08dfe09984dec3251ea808b892efeac3ede9402bf2b1967029000b2752103fd5960528dc152014952efdb702a88f71e3c1653b2314431701ec77e57fde83c68ac - # to_remote amount 3000000 P2WPKH(032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991) - remote_signature = 304402204bb3d6e279d71d9da414c82de42f1f954267c762b2e2eb8b76bc3be4ea07d4b0022014febc009c5edc8c3fc5d94015de163200f780046f1c293bfed8568f08b70fb3 - # local_signature = 3044022072c2e2b1c899b2242656a537dde2892fa3801be0d6df0a87836c550137acde8302201654aa1974d37a829083c3ba15088689f30b56d6a4f6cb14c7bad0ee3116d398 - output commit_tx: 02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b8005d007000000000000220020403d394747cae42e98ff01734ad5c08f82ba123d3d9a620abda88989651e2ab5b80b000000000000220020c20b5d1f8584fd90443e7b7b720136174fa4b9333c261d04dbbd012635c0f419a00f0000000000002200208c48d15160397c9731df9bc3b236656efb6665fbfe92b4a6878e88a499f741c4c0c62d0000000000160014cc1b07838e387deacd0e5232e1e8b49f4c29e48440966a00000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0400473044022072c2e2b1c899b2242656a537dde2892fa3801be0d6df0a87836c550137acde8302201654aa1974d37a829083c3ba15088689f30b56d6a4f6cb14c7bad0ee3116d3980147304402204bb3d6e279d71d9da414c82de42f1f954267c762b2e2eb8b76bc3be4ea07d4b0022014febc009c5edc8c3fc5d94015de163200f780046f1c293bfed8568f08b70fb301475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220 - num_htlcs: 3 - # signature for output #0 (htlc-timeout for htlc #2) - remote_htlc_signature = 3045022100939726680351a7856c1bc386d4a1f422c7d29bd7b56afc139570f508474e6c40022023175a799ccf44c017fbaadb924c40b2a12115a5b7d0dfd3228df803a2de8450 - # local_htlc_signature = 304502210099c98c2edeeee6ec0fb5f3bea8b79bb016a2717afa9b5072370f34382de281d302206f5e2980a995e045cf90a547f0752a7ee99d48547bc135258fe7bc07e0154301 - htlc_timeout_tx (htlc #2): 02000000000101153cd825fdb3aa624bfe513e8031d5d08c5e582fb3d1d1fe8faf27d3eed410cd0000000000000000000122020000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100939726680351a7856c1bc386d4a1f422c7d29bd7b56afc139570f508474e6c40022023175a799ccf44c017fbaadb924c40b2a12115a5b7d0dfd3228df803a2de84500148304502210099c98c2edeeee6ec0fb5f3bea8b79bb016a2717afa9b5072370f34382de281d302206f5e2980a995e045cf90a547f0752a7ee99d48547bc135258fe7bc07e015430101008576a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a914b43e1b38138a41b37f7cd9a1d274bc63e3a9b5d188ac6868f6010000 - # signature for output #1 (htlc-timeout for htlc #3) - remote_htlc_signature = 3044022021bb883bf324553d085ba2e821cad80c28ef8b303dbead8f98e548783c02d1600220638f9ef2a9bba25869afc923f4b5dc38be3bb459f9efa5d869392d5f7779a4a0 - # local_htlc_signature = 3045022100fd85bd7697b89c08ec12acc8ba89b23090637d83abd26ca37e01ae93e67c367302202b551fe69386116c47f984aab9c8dfd25d864dcde5d3389cfbef2447a85c4b77 - htlc_timeout_tx (htlc #3): 02000000000101153cd825fdb3aa624bfe513e8031d5d08c5e582fb3d1d1fe8faf27d3eed410cd010000000000000000010a060000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500473044022021bb883bf324553d085ba2e821cad80c28ef8b303dbead8f98e548783c02d1600220638f9ef2a9bba25869afc923f4b5dc38be3bb459f9efa5d869392d5f7779a4a001483045022100fd85bd7697b89c08ec12acc8ba89b23090637d83abd26ca37e01ae93e67c367302202b551fe69386116c47f984aab9c8dfd25d864dcde5d3389cfbef2447a85c4b7701008576a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9148a486ff2e31d6158bf39e2608864d63fefd09d5b88ac6868f7010000 - # signature for output #2 (htlc-success for htlc #4) - remote_htlc_signature = 3045022100c9e6f0454aa598b905a35e641a70cc9f67b5f38cc4b00843a041238c4a9f1c4a0220260a2822a62da97e44583e837245995ca2e36781769c52f19e498efbdcca262b - # local_htlc_signature = 30450221008a9f2ea24cd455c2b64c1472a5fa83865b0a5f49a62b661801e884cf2849af8302204d44180e50bf6adfcf1c1e581d75af91aba4e28681ce4a5ee5f3cbf65eca10f3 - htlc_success_tx (htlc #4): 02000000000101153cd825fdb3aa624bfe513e8031d5d08c5e582fb3d1d1fe8faf27d3eed410cd020000000000000000019a090000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100c9e6f0454aa598b905a35e641a70cc9f67b5f38cc4b00843a041238c4a9f1c4a0220260a2822a62da97e44583e837245995ca2e36781769c52f19e498efbdcca262b014830450221008a9f2ea24cd455c2b64c1472a5fa83865b0a5f49a62b661801e884cf2849af8302204d44180e50bf6adfcf1c1e581d75af91aba4e28681ce4a5ee5f3cbf65eca10f3012004040404040404040404040404040404040404040404040404040404040404048a76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac686800000000 - - name: commitment tx with four outputs untrimmed (minimum feerate) - to_local_msat: 6988000000 - to_remote_msat: 3000000000 - local_feerate_per_kw: 2195 - # base commitment transaction fee = 2344 - # actual commitment transaction fee = 7344 - # HTLC #3 offered amount 3000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9148a486ff2e31d6158bf39e2608864d63fefd09d5b88ac6868 - # HTLC #4 received amount 4000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac6868 - # to_local amount 6985656 wscript 63210212a140cd0c6539d07cd08dfe09984dec3251ea808b892efeac3ede9402bf2b1967029000b2752103fd5960528dc152014952efdb702a88f71e3c1653b2314431701ec77e57fde83c68ac - # to_remote amount 3000000 P2WPKH(032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991) - remote_signature = 304402201a8c1b1f9671cd9e46c7323a104d7047cc48d3ee80d40d4512e0c72b8dc65666022066d7f9a2ce18c9eb22d2739ffcce05721c767f9b607622a31b6ea5793ddce403 - # local_signature = 3044022044d592025b610c0d678f65032e87035cdfe89d1598c522cc32524ae8172417c30220749fef9d5b2ae8cdd91ece442ba8809bc891efedae2291e578475f97715d1767 - output commit_tx: 02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b8004b80b000000000000220020c20b5d1f8584fd90443e7b7b720136174fa4b9333c261d04dbbd012635c0f419a00f0000000000002200208c48d15160397c9731df9bc3b236656efb6665fbfe92b4a6878e88a499f741c4c0c62d0000000000160014cc1b07838e387deacd0e5232e1e8b49f4c29e484b8976a00000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0400473044022044d592025b610c0d678f65032e87035cdfe89d1598c522cc32524ae8172417c30220749fef9d5b2ae8cdd91ece442ba8809bc891efedae2291e578475f97715d17670147304402201a8c1b1f9671cd9e46c7323a104d7047cc48d3ee80d40d4512e0c72b8dc65666022066d7f9a2ce18c9eb22d2739ffcce05721c767f9b607622a31b6ea5793ddce40301475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220 - num_htlcs: 2 - # signature for output #0 (htlc-timeout for htlc #3) - remote_htlc_signature = 3045022100e57b845066a06ee7c2cbfc29eabffe52daa9bf6f6de760066d04df9f9b250e0002202ffb197f0e6e0a77a75a9aff27014bd3de83b7f748d7efef986abe655e1dd50e - # local_htlc_signature = 3045022100ecc8c6529d0b2316d046f0f0757c1e1c25a636db168ec4f3aa1b9278df685dc0022067ae6b65e936f1337091f7b18a15935b608c5f2cdddb2f892ed0babfdd376d76 - htlc_timeout_tx (htlc #3): 020000000001018130a10f09b13677ba2885a8bca32860f3a952e5912b829a473639b5a2c07b900000000000000000000109060000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100e57b845066a06ee7c2cbfc29eabffe52daa9bf6f6de760066d04df9f9b250e0002202ffb197f0e6e0a77a75a9aff27014bd3de83b7f748d7efef986abe655e1dd50e01483045022100ecc8c6529d0b2316d046f0f0757c1e1c25a636db168ec4f3aa1b9278df685dc0022067ae6b65e936f1337091f7b18a15935b608c5f2cdddb2f892ed0babfdd376d7601008576a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9148a486ff2e31d6158bf39e2608864d63fefd09d5b88ac6868f7010000 - # signature for output #1 (htlc-success for htlc #4) - remote_htlc_signature = 3045022100d193b7ecccad8057571620a0b1ffa6c48e9483311723b59cf536043b20bc51550220546d4bd37b3b101ecda14f6c907af46ec391abce1cd9c7ce22b1a62b534f2f2a - # local_htlc_signature = 3044022014d66f11f9cacf923807eba49542076c5fe5cccf252fb08fe98c78ef3ca6ab5402201b290dbe043cc512d9d78de074a5a129b8759bc6a6c546b190d120b690bd6e82 - htlc_success_tx (htlc #4): 020000000001018130a10f09b13677ba2885a8bca32860f3a952e5912b829a473639b5a2c07b900100000000000000000199090000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100d193b7ecccad8057571620a0b1ffa6c48e9483311723b59cf536043b20bc51550220546d4bd37b3b101ecda14f6c907af46ec391abce1cd9c7ce22b1a62b534f2f2a01473044022014d66f11f9cacf923807eba49542076c5fe5cccf252fb08fe98c78ef3ca6ab5402201b290dbe043cc512d9d78de074a5a129b8759bc6a6c546b190d120b690bd6e82012004040404040404040404040404040404040404040404040404040404040404048a76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac686800000000 - - name: commitment tx with four outputs untrimmed (maximum feerate) - to_local_msat: 6988000000 - to_remote_msat: 3000000000 - local_feerate_per_kw: 3702 - # base commitment transaction fee = 3953 - # actual commitment transaction fee = 8953 - # HTLC #3 offered amount 3000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9148a486ff2e31d6158bf39e2608864d63fefd09d5b88ac6868 - # HTLC #4 received amount 4000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac6868 - # to_local amount 6984047 wscript 63210212a140cd0c6539d07cd08dfe09984dec3251ea808b892efeac3ede9402bf2b1967029000b2752103fd5960528dc152014952efdb702a88f71e3c1653b2314431701ec77e57fde83c68ac - # to_remote amount 3000000 P2WPKH(032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991) - remote_signature = 304502210092a587aeb777f869e7ff0d7898ea619ee26a3dacd1f3672b945eea600be431100220077ee9eae3528d15251f2a52b607b189820e57a6ccfac8d1af502b132ee40169 - # local_signature = 3045022100e5efb73c32d32da2d79702299b6317de6fb24a60476e3855926d78484dd1b3c802203557cb66a42c944ef06e00bcc4da35a5bcb2f185aab0f8e403e519e1d66aaf75 - output commit_tx: 02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b8004b80b000000000000220020c20b5d1f8584fd90443e7b7b720136174fa4b9333c261d04dbbd012635c0f419a00f0000000000002200208c48d15160397c9731df9bc3b236656efb6665fbfe92b4a6878e88a499f741c4c0c62d0000000000160014cc1b07838e387deacd0e5232e1e8b49f4c29e4846f916a00000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0400483045022100e5efb73c32d32da2d79702299b6317de6fb24a60476e3855926d78484dd1b3c802203557cb66a42c944ef06e00bcc4da35a5bcb2f185aab0f8e403e519e1d66aaf750148304502210092a587aeb777f869e7ff0d7898ea619ee26a3dacd1f3672b945eea600be431100220077ee9eae3528d15251f2a52b607b189820e57a6ccfac8d1af502b132ee4016901475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220 - num_htlcs: 2 - # signature for output #0 (htlc-timeout for htlc #3) - remote_htlc_signature = 304402206fa54c11f98c3bae1e93df43fc7affeb05b476bf8060c03e29c377c69bc08e8b0220672701cce50d5c379ff45a5d2cfe48ac44973adb066ac32608e21221d869bb89 - # local_htlc_signature = 304402206e36c683ebf2cb16bcef3d5439cf8b53cd97280a365ed8acd7abb85a8ba5f21c02206e8621edfc2a5766cbc96eb67fd501127ff163eb6b85518a39f7d4974aef126f - htlc_timeout_tx (htlc #3): 020000000001018db483bff65c70ee71d8282aeec5a880e2e2b39e45772bda5460403095c62e3f0000000000000000000122020000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e050047304402206fa54c11f98c3bae1e93df43fc7affeb05b476bf8060c03e29c377c69bc08e8b0220672701cce50d5c379ff45a5d2cfe48ac44973adb066ac32608e21221d869bb890147304402206e36c683ebf2cb16bcef3d5439cf8b53cd97280a365ed8acd7abb85a8ba5f21c02206e8621edfc2a5766cbc96eb67fd501127ff163eb6b85518a39f7d4974aef126f01008576a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9148a486ff2e31d6158bf39e2608864d63fefd09d5b88ac6868f7010000 - # signature for output #1 (htlc-success for htlc #4) - remote_htlc_signature = 3044022057649739b0eb74d541ead0dfdb3d4b2c15aa192720031044c3434c67812e5ca902201e5ede42d960ae551707f4a6b34b09393cf4dee2418507daa022e3550dbb5817 - # local_htlc_signature = 304402207faad26678c8850e01b4a0696d60841f7305e1832b786110ee9075cb92ed14a30220516ef8ee5dfa80824ea28cbcec0dd95f8b847146257c16960db98507db15ffdc - htlc_success_tx (htlc #4): 020000000001018db483bff65c70ee71d8282aeec5a880e2e2b39e45772bda5460403095c62e3f0100000000000000000176050000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500473044022057649739b0eb74d541ead0dfdb3d4b2c15aa192720031044c3434c67812e5ca902201e5ede42d960ae551707f4a6b34b09393cf4dee2418507daa022e3550dbb58170147304402207faad26678c8850e01b4a0696d60841f7305e1832b786110ee9075cb92ed14a30220516ef8ee5dfa80824ea28cbcec0dd95f8b847146257c16960db98507db15ffdc012004040404040404040404040404040404040404040404040404040404040404048a76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac686800000000 - - name: commitment tx with three outputs untrimmed (minimum feerate) - to_local_msat: 6988000000 - to_remote_msat: 3000000000 - local_feerate_per_kw: 3703 - # base commitment transaction fee = 3317 - # actual commitment transaction fee = 11317 - # HTLC #4 received amount 4000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac6868 - # to_local amount 6984683 wscript 63210212a140cd0c6539d07cd08dfe09984dec3251ea808b892efeac3ede9402bf2b1967029000b2752103fd5960528dc152014952efdb702a88f71e3c1653b2314431701ec77e57fde83c68ac - # to_remote amount 3000000 P2WPKH(032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991) - remote_signature = 3045022100b495d239772a237ff2cf354b1b11be152fd852704cb184e7356d13f2fb1e5e430220723db5cdb9cbd6ead7bfd3deb419cf41053a932418cbb22a67b581f40bc1f13e - # local_signature = 304402201b736d1773a124c745586217a75bed5f66c05716fbe8c7db4fdb3c3069741cdd02205083f39c321c1bcadfc8d97e3c791a66273d936abac0c6a2fde2ed46019508e1 - output commit_tx: 02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b8003a00f0000000000002200208c48d15160397c9731df9bc3b236656efb6665fbfe92b4a6878e88a499f741c4c0c62d0000000000160014cc1b07838e387deacd0e5232e1e8b49f4c29e484eb936a00000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e040047304402201b736d1773a124c745586217a75bed5f66c05716fbe8c7db4fdb3c3069741cdd02205083f39c321c1bcadfc8d97e3c791a66273d936abac0c6a2fde2ed46019508e101483045022100b495d239772a237ff2cf354b1b11be152fd852704cb184e7356d13f2fb1e5e430220723db5cdb9cbd6ead7bfd3deb419cf41053a932418cbb22a67b581f40bc1f13e01475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220 - num_htlcs: 1 - # signature for output #0 (htlc-success for htlc #4) - remote_htlc_signature = 3045022100c34c61735f93f2e324cc873c3b248111ccf8f6db15d5969583757010d4ad2b4602207867bb919b2ddd6387873e425345c9b7fd18d1d66aba41f3607bc2896ef3c30a - # local_htlc_signature = 3045022100988c143e2110067117d2321bdd4bd16ca1734c98b29290d129384af0962b634e02206c1b02478878c5f547018b833986578f90c3e9be669fe5788ad0072a55acbb05 - htlc_success_tx (htlc #4): 0200000000010120060e4a29579d429f0f27c17ee5f1ee282f20d706d6f90b63d35946d8f3029a0000000000000000000175050000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100c34c61735f93f2e324cc873c3b248111ccf8f6db15d5969583757010d4ad2b4602207867bb919b2ddd6387873e425345c9b7fd18d1d66aba41f3607bc2896ef3c30a01483045022100988c143e2110067117d2321bdd4bd16ca1734c98b29290d129384af0962b634e02206c1b02478878c5f547018b833986578f90c3e9be669fe5788ad0072a55acbb05012004040404040404040404040404040404040404040404040404040404040404048a76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac686800000000 - - name: commitment tx with three outputs untrimmed (maximum feerate) - to_local_msat: 6988000000 - to_remote_msat: 3000000000 - local_feerate_per_kw: 4914 - # base commitment transaction fee = 4402 - # actual commitment transaction fee = 12402 - # HTLC #4 received amount 4000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac6868 - # to_local amount 6983598 wscript 63210212a140cd0c6539d07cd08dfe09984dec3251ea808b892efeac3ede9402bf2b1967029000b2752103fd5960528dc152014952efdb702a88f71e3c1653b2314431701ec77e57fde83c68ac - # to_remote amount 3000000 P2WPKH(032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991) - remote_signature = 3045022100b4b16d5f8cc9fc4c1aff48831e832a0d8990e133978a66e302c133550954a44d022073573ce127e2200d316f6b612803a5c0c97b8d20e1e44dbe2ac0dd2fb8c95244 - # local_signature = 3045022100d72638bc6308b88bb6d45861aae83e5b9ff6e10986546e13bce769c70036e2620220320be7c6d66d22f30b9fcd52af66531505b1310ca3b848c19285b38d8a1a8c19 - output commit_tx: 02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b8003a00f0000000000002200208c48d15160397c9731df9bc3b236656efb6665fbfe92b4a6878e88a499f741c4c0c62d0000000000160014cc1b07838e387deacd0e5232e1e8b49f4c29e484ae8f6a00000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0400483045022100d72638bc6308b88bb6d45861aae83e5b9ff6e10986546e13bce769c70036e2620220320be7c6d66d22f30b9fcd52af66531505b1310ca3b848c19285b38d8a1a8c1901483045022100b4b16d5f8cc9fc4c1aff48831e832a0d8990e133978a66e302c133550954a44d022073573ce127e2200d316f6b612803a5c0c97b8d20e1e44dbe2ac0dd2fb8c9524401475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220 - num_htlcs: 1 - # signature for output #0 (htlc-success for htlc #4) - remote_htlc_signature = 3045022100f43591c156038ba217756006bb3c55f7d113a325cdd7d9303c82115372858d68022016355b5aadf222bc8d12e426c75f4a03423917b2443a103eb2a498a3a2234374 - # local_htlc_signature = 30440220585dee80fafa264beac535c3c0bb5838ac348b156fdc982f86adc08dfc9bfd250220130abb82f9f295cc9ef423dcfef772fde2acd85d9df48cc538981d26a10a9c10 - htlc_success_tx (htlc #4): 02000000000101a9172908eace869cc35128c31fc2ab502f72e4dff31aab23e0244c4b04b11ab00000000000000000000122020000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100f43591c156038ba217756006bb3c55f7d113a325cdd7d9303c82115372858d68022016355b5aadf222bc8d12e426c75f4a03423917b2443a103eb2a498a3a2234374014730440220585dee80fafa264beac535c3c0bb5838ac348b156fdc982f86adc08dfc9bfd250220130abb82f9f295cc9ef423dcfef772fde2acd85d9df48cc538981d26a10a9c10012004040404040404040404040404040404040404040404040404040404040404048a76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac686800000000 - - name: commitment tx with two outputs untrimmed (minimum feerate) - to_local_msat: 6988000000 - to_remote_msat: 3000000000 - local_feerate_per_kw: 4915 - # base commitment transaction fee = 3558 - # actual commitment transaction fee = 15558 - # to_local amount 6984442 wscript 63210212a140cd0c6539d07cd08dfe09984dec3251ea808b892efeac3ede9402bf2b1967029000b2752103fd5960528dc152014952efdb702a88f71e3c1653b2314431701ec77e57fde83c68ac - # to_remote amount 3000000 P2WPKH(032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991) - remote_signature = 304402203a286936e74870ca1459c700c71202af0381910a6bfab687ef494ef1bc3e02c902202506c362d0e3bee15e802aa729bf378e051644648253513f1c085b264cc2a720 - # local_signature = 30450221008a953551f4d67cb4df3037207fc082ddaf6be84d417b0bd14c80aab66f1b01a402207508796dc75034b2dee876fe01dc05a08b019f3e5d689ac8842ade2f1befccf5 - output commit_tx: 02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b8002c0c62d0000000000160014cc1b07838e387deacd0e5232e1e8b49f4c29e484fa926a00000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e04004830450221008a953551f4d67cb4df3037207fc082ddaf6be84d417b0bd14c80aab66f1b01a402207508796dc75034b2dee876fe01dc05a08b019f3e5d689ac8842ade2f1befccf50147304402203a286936e74870ca1459c700c71202af0381910a6bfab687ef494ef1bc3e02c902202506c362d0e3bee15e802aa729bf378e051644648253513f1c085b264cc2a72001475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220 - num_htlcs: 0 - - name: commitment tx with two outputs untrimmed (maximum feerate) - to_local_msat: 6988000000 - to_remote_msat: 3000000000 - local_feerate_per_kw: 9651180 - # base commitment transaction fee = 6987454 - # actual commitment transaction fee = 6999454 - # to_local amount 546 wscript 63210212a140cd0c6539d07cd08dfe09984dec3251ea808b892efeac3ede9402bf2b1967029000b2752103fd5960528dc152014952efdb702a88f71e3c1653b2314431701ec77e57fde83c68ac - # to_remote amount 3000000 P2WPKH(032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991) - remote_signature = 304402200a8544eba1d216f5c5e530597665fa9bec56943c0f66d98fc3d028df52d84f7002201e45fa5c6bc3a506cc2553e7d1c0043a9811313fc39c954692c0d47cfce2bbd3 - # local_signature = 3045022100e11b638c05c650c2f63a421d36ef8756c5ce82f2184278643520311cdf50aa200220259565fb9c8e4a87ccaf17f27a3b9ca4f20625754a0920d9c6c239d8156a11de - output commit_tx: 02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b800222020000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80ec0c62d0000000000160014cc1b07838e387deacd0e5232e1e8b49f4c29e4840400483045022100e11b638c05c650c2f63a421d36ef8756c5ce82f2184278643520311cdf50aa200220259565fb9c8e4a87ccaf17f27a3b9ca4f20625754a0920d9c6c239d8156a11de0147304402200a8544eba1d216f5c5e530597665fa9bec56943c0f66d98fc3d028df52d84f7002201e45fa5c6bc3a506cc2553e7d1c0043a9811313fc39c954692c0d47cfce2bbd301475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220 - num_htlcs: 0 - - name: commitment tx with one output untrimmed (minimum feerate) - to_local_msat: 6988000000 - to_remote_msat: 3000000000 - local_feerate_per_kw: 9651181 - # base commitment transaction fee = 6987455 - # actual commitment transaction fee = 7000000 - # to_remote amount 3000000 P2WPKH(032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991) - remote_signature = 304402202ade0142008309eb376736575ad58d03e5b115499709c6db0b46e36ff394b492022037b63d78d66404d6504d4c4ac13be346f3d1802928a6d3ad95a6a944227161a2 - # local_signature = 304402207e8d51e0c570a5868a78414f4e0cbfaed1106b171b9581542c30718ee4eb95ba02203af84194c97adf98898c9afe2f2ed4a7f8dba05a2dfab28ac9d9c604aa49a379 - output commit_tx: 02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b8001c0c62d0000000000160014cc1b07838e387deacd0e5232e1e8b49f4c29e484040047304402207e8d51e0c570a5868a78414f4e0cbfaed1106b171b9581542c30718ee4eb95ba02203af84194c97adf98898c9afe2f2ed4a7f8dba05a2dfab28ac9d9c604aa49a3790147304402202ade0142008309eb376736575ad58d03e5b115499709c6db0b46e36ff394b492022037b63d78d66404d6504d4c4ac13be346f3d1802928a6d3ad95a6a944227161a201475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220 - num_htlcs: 0 - - name: commitment tx with fee greater than funder amount - to_local_msat: 6988000000 - to_remote_msat: 3000000000 - local_feerate_per_kw: 9651936 - # base commitment transaction fee = 6988001 - # actual commitment transaction fee = 7000000 - # to_remote amount 3000000 P2WPKH(032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991) - remote_signature = 304402202ade0142008309eb376736575ad58d03e5b115499709c6db0b46e36ff394b492022037b63d78d66404d6504d4c4ac13be346f3d1802928a6d3ad95a6a944227161a2 - # local_signature = 304402207e8d51e0c570a5868a78414f4e0cbfaed1106b171b9581542c30718ee4eb95ba02203af84194c97adf98898c9afe2f2ed4a7f8dba05a2dfab28ac9d9c604aa49a379 - output commit_tx: 02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b8001c0c62d0000000000160014cc1b07838e387deacd0e5232e1e8b49f4c29e484040047304402207e8d51e0c570a5868a78414f4e0cbfaed1106b171b9581542c30718ee4eb95ba02203af84194c97adf98898c9afe2f2ed4a7f8dba05a2dfab28ac9d9c604aa49a3790147304402202ade0142008309eb376736575ad58d03e5b115499709c6db0b46e36ff394b492022037b63d78d66404d6504d4c4ac13be346f3d1802928a6d3ad95a6a944227161a201475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220 - num_htlcs: 0 - - name: commitment tx with 3 htlc outputs, 2 offered having the same amount and preimage - to_local_msat: 6987999999 - to_remote_msat: 3000000000 - local_feerate_per_kw: 253 - # HTLC #1 received amount 2000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a9144b6b2e5444c2639cc0fb7bcea5afba3f3cdce23988527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f501b175ac6868 - # HTLC #5 offered amount 5000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9142002cc93ebefbb1b73f0af055dcc27a0b504ad7688ac6868 - # HTLC #6 offered amount 5000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9142002cc93ebefbb1b73f0af055dcc27a0b504ad7688ac6868 - # HTLC #5 and 6 have CLTV 506 and 505, respectively, and preimage 0505050505050505050505050505050505050505050505050505050505050505 - remote_signature = 304402207d0870964530f97b62497b11153c551dca0a1e226815ef0a336651158da0f82402200f5378beee0e77759147b8a0a284decd11bfd2bc55c8fafa41c134fe996d43c8 - # local_signature = 304402200d10bf5bc5397fc59d7188ae438d80c77575595a2d488e41bd6363a810cc8d72022012b57e714fbbfdf7a28c47d5b370cb8ac37c8545f596216e5b21e9b236ef457c - output commit_tx: 02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b8005d007000000000000220020748eba944fedc8827f6b06bc44678f93c0f9e6078b35c6331ed31e75f8ce0c2d8813000000000000220020305c12e1a0bc21e283c131cea1c66d68857d28b7b2fce0a6fbc40c164852121b8813000000000000220020305c12e1a0bc21e283c131cea1c66d68857d28b7b2fce0a6fbc40c164852121bc0c62d0000000000160014cc1b07838e387deacd0e5232e1e8b49f4c29e484a69f6a00000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e040047304402200d10bf5bc5397fc59d7188ae438d80c77575595a2d488e41bd6363a810cc8d72022012b57e714fbbfdf7a28c47d5b370cb8ac37c8545f596216e5b21e9b236ef457c0147304402207d0870964530f97b62497b11153c551dca0a1e226815ef0a336651158da0f82402200f5378beee0e77759147b8a0a284decd11bfd2bc55c8fafa41c134fe996d43c801475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220 - num_htlcs: 3 - # signature for output #0 (htlc-success for htlc #1) - remote_htlc_signature = 3045022100b470fe12e5b7fea9eccb8cbff1972cea4f96758041898982a02bcc7f9d56d50b0220338a75b2afaab4ec00cdd2d9273c68c7581ff5a28bcbb40c4d138b81f1d45ce5 - # local_htlc_signature = 3044022017b90c65207522a907fb6a137f9dd528b3389465a8ae72308d9e1d564f512cf402204fc917b4f0e88604a3e994f85bfae7c7c1f9d9e9f78e8cd112e0889720d9405b - htlc_success_tx (htlc #1): 020000000001014bdccf28653066a2c554cafeffdfe1e678e64a69b056684deb0c4fba909423ec000000000000000000011f070000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100b470fe12e5b7fea9eccb8cbff1972cea4f96758041898982a02bcc7f9d56d50b0220338a75b2afaab4ec00cdd2d9273c68c7581ff5a28bcbb40c4d138b81f1d45ce501473044022017b90c65207522a907fb6a137f9dd528b3389465a8ae72308d9e1d564f512cf402204fc917b4f0e88604a3e994f85bfae7c7c1f9d9e9f78e8cd112e0889720d9405b012001010101010101010101010101010101010101010101010101010101010101018a76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a9144b6b2e5444c2639cc0fb7bcea5afba3f3cdce23988527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f501b175ac686800000000 - # signature for output #1 (htlc-timeout for htlc #6) - remote_htlc_signature = 3045022100b575379f6d8743cb0087648f81cfd82d17a97fbf8f67e058c65ce8b9d25df9500220554a210d65b02d9f36c6adf0f639430ca8293196ba5089bf67cc3a9813b7b00a - # local_htlc_signature = 3045022100ee2e16b90930a479b13f8823a7f14b600198c838161160b9436ed086d3fc57e002202a66fa2324f342a17129949c640bfe934cbc73a869ba7c06aa25c5a3d0bfb53d - htlc_timeout_tx (htlc #6): 020000000001014bdccf28653066a2c554cafeffdfe1e678e64a69b056684deb0c4fba909423ec01000000000000000001e1120000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100b575379f6d8743cb0087648f81cfd82d17a97fbf8f67e058c65ce8b9d25df9500220554a210d65b02d9f36c6adf0f639430ca8293196ba5089bf67cc3a9813b7b00a01483045022100ee2e16b90930a479b13f8823a7f14b600198c838161160b9436ed086d3fc57e002202a66fa2324f342a17129949c640bfe934cbc73a869ba7c06aa25c5a3d0bfb53d01008576a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9142002cc93ebefbb1b73f0af055dcc27a0b504ad7688ac6868f9010000 - # signature for output #2 (htlc-timeout for htlc #5) - remote_htlc_signature = 30440220471c9f3ad92e49b13b7b8059f43ecf8f7887b0dccbb9fdb54bfe23d62a8ae332022024bd22fae0740e86a44228c35330da9526fd7306dffb2b9dc362d5e78abef7cc - # local_htlc_signature = 304402207157f452f2506d73c315192311893800cfb3cc235cc1185b1cfcc136b55230db022014be242dbc6c5da141fec4034e7f387f74d6ff1899453d72ba957467540e1ecb - htlc_timeout_tx (htlc #5): 020000000001014bdccf28653066a2c554cafeffdfe1e678e64a69b056684deb0c4fba909423ec02000000000000000001e1120000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e05004730440220471c9f3ad92e49b13b7b8059f43ecf8f7887b0dccbb9fdb54bfe23d62a8ae332022024bd22fae0740e86a44228c35330da9526fd7306dffb2b9dc362d5e78abef7cc0147304402207157f452f2506d73c315192311893800cfb3cc235cc1185b1cfcc136b55230db022014be242dbc6c5da141fec4034e7f387f74d6ff1899453d72ba957467540e1ecb01008576a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9142002cc93ebefbb1b73f0af055dcc27a0b504ad7688ac6868fa010000 diff --git a/eclair-core/src/test/resources/integration/bitcoin.conf b/eclair-core/src/test/resources/integration/bitcoin.conf index 370f1cbfcb..5145259591 100644 --- a/eclair-core/src/test/resources/integration/bitcoin.conf +++ b/eclair-core/src/test/resources/integration/bitcoin.conf @@ -1,5 +1,7 @@ regtest=1 -noprinttoconsole=1 +printtoconsole=1 +debug=rpc +debug=http server=1 rpcuser=foo rpcpassword=bar diff --git a/eclair-core/src/test/resources/nonreg/codecs/000003-DATA_NORMAL/fundee/data.bin b/eclair-core/src/test/resources/nonreg/codecs/000003-DATA_NORMAL/fundee/data.bin deleted file mode 100644 index e47310bccf..0000000000 --- a/eclair-core/src/test/resources/nonreg/codecs/000003-DATA_NORMAL/fundee/data.bin +++ /dev/null @@ -1 +0,0 @@ -00000303933884AAF1D6B108397E5EFE5C86BCF2D8CA8D2F700EDA99DB9214FC2712B134000456E4167E3C0EB8C856C79CA31C97C0AA0000000000000222000000012A05F2000000000000028F5C000000000000000102D0001E000BD48A2402E80B723C42EE3E42938866EC6686ABB7ABF64380000000C501A7F2974C5074E9E10DBB3F0D9B8C40932EC63ABC610FAD7EB6B21C6D081A459B000000000000011E80000001EEFFFE5C00000000000147AE00000000000001F403F000F18146779F781067ED04B4957E14F1C5623AB653039B2B1D49910240848E4E682DB20131AD64F76FAF90CD7DE26892F1BDAB82FB9E02EF6538D82FF4204B5348F02AE081A5388E9474769D69C4F60A763AE0CCDB5228A06281DE64408871A927297FDFD8818B6383985ABD4F0AC22E73791CF3A4D63C592FA2648242D34B8334B1539E823381BB1F1404C37D9C2318F5FC6B1BF7ECF5E6835B779E3BE09BADCF6DF1F51DCFBC80000000C0808000000000000EFD80000000007F00000000061A0A4880000001EDE5F3C3801203B9C79160B5123CADE575E370480B57B0C816EB35DD7B27372EDA858622EB1E808000000015FFFFFF800000000011001029DFB814F6502A68D6F83B6049E3D2948A2080084083750626532FDB437169C20023A9108146779F781067ED04B4957E14F1C5623AB653039B2B1D49910240848E4E682DB21081B30694071254D8B3B9537320C014B8CB1052E5514F5EFC19CF2EB806308D5CF1A95700AD0100000000008083B9C79160B5123CADE575E370480B57B0C816EB35DD7B27372EDA858622EB1E80800000001961B4C001618F8180000000001100102E648BA30998A28C02C2DFD9DDCD0E0BA064DA199C55186485AFAB296B94E704426FFE00000000000B000A67D9B9FAADB91650E0146B1F742E5C16006708890200239822011026A6925C659D006FEB42D639F1E42DD13224EE49AA34E71B612CF96DB66A8CD4011032C22F653C54CC5E41098227427650644266D80DED45B7387AE0FFC10E529C4680A418228110807CB47D9C1A14CB832FB361C398EA672C9542F34A90BAD4288FA6AC5FC9E9845C01101CF71CAE9252D389135D8C606225DCF1E0333CCDF1FAE84B74FC5D3D440C25F880A3A9108146779F781067ED04B4957E14F1C5623AB653039B2B1D49910240848E4E682DB21081B30694071254D8B3B9537320C014B8CB1052E5514F5EFC19CF2EB806308D5CF1A9573D7C531000000000000000000F3180000000007F00000001EDE5F3C380000000061A0A48D64CA627B243AD5915A2E5D0BAD026762028DDF3304992B83A26D6C11735FC5F01ED56D769BDE7F6A068AF1A4BCFDF950321F3A4744B01B1DDC7498677F112AE1A80000000000000000000000000000000000000658000000000000819800040D37301C10C9419287E9A3B704EB6D7F45CC145DD77DCE8A63B0A47C8AB67467D800901DCE3C8B05A891E56F2BAF1B82405ABD8640B759AEEBD939B976D42C311758F40400000000AFFFFFFC00000000008800814EFDC0A7B2815346B7C1DB024F1E94A451040042041BA83132997EDA1B8B4E10011D48840A33BCFBC0833F6825A4ABF0A78E2B11D5B2981CD958EA4C881204247273416D90840D9834A03892A6C59DCA9B990600A5C65882972A8A7AF7E0CE7975C031846AE78D4AB8002000EC0003FFFFFFFF86801076D98A575A4CDFD0E3F44D1BB3CD3BBAF3BD04C38FED439ED90D88DF932A9296801A80007FFFFFFFF4008136A9D5896669E8724C5120FB6B36C241EF3CEF68AE0316161F04A9EE3EAFF36000FC0003FFFFFFFF86780106E4B5CC4155733A2427082907338051A5DA1E7CA6432840A5528ECAFFA3FB628801B80007FFFFFFFF10020CA4E125E9126107745D4354D4187ABCDE323117857A1DCEB7CCF60B2AAFA80C6003A0000FFFFFFFFE1C0080981575FD981A73A848CC0243CB467BF451F6811DAF4D71CAD8CE8B1E96DB190C01000003FFFFFFFF867400814C747E0FD8290BE8A3B8B3F73015A261479A71780CD3A0A9270234E4B394409C00D80003FFFFFFFF90020E1B9C9B10A97F15F5E1BB27FC8AC670DF8DADEAE4EDFAFB23BDD0AC705FDF51600340000FFFFFFFFF0020AD2581F3494A17B0BE3F63516D53F028A204FD3156D8B21AA4E57A8738D2062080007FFFFFFFF0CE83B9C79160B5123CADE575E370480B57B0C816EB35DD7B27372EDA858622EB1E0B8C1E00000B8000FA46CC2C7E9AB4A37C64216CD65C944E6D73998419D1A1AD2827AB6BC85B32280230764E374064EC82A3751E789607E23BEAE93FB0EDDD5E7FA803767079662E80EAEF384E2AFCB68049D9DC246119E77BD2ED4112330760CAB6CD3671CFCE006C584B9C95E0B554261E00154D40806EA694F44751B328A9291BAD124EFD5664280936EC92D27B242737E7E3E83B4704BA367B7DA5108F2F6EDFB1C38EE721A369E77EED71B12090BAEAAAC322C1457E31AB0C4DE5D9351943F10FD747742616A1AABD09F680B37D4105A8872695EE9B97FAB8985FAA9D747D45046229BF265CEEB300A40FE23040C5F335E0515496C58EE47418B72331FCC6F47A31A9B33B8E000008692FFAFF04D2AE211E9461FB39D875D74F32E4109D21D5A03D46612000000002E307800002E0002069FCA5D3141D3A78436ECFC366E31024CBB18EAF1843EB5FADAC871B42069166C0726710955E3AD621072FCBDFCB90D79E5B1951A5EE01DB533B72429F84E2562680519DE7DE0419FB412D255F853C71588EAD94C0E6CAC7526440902123939A0B6C806CC1A501C495362CEE54DCC830052E32C414B95453D7BF0673CBAE018C23573C69C694A8F88483050257A7366B838489731E5776B6FA0F02573401176D3E7FAEEF11E95A671420586631255F51A0EC2CF4D4D9F69D587712070FE1FB9316B71868692FFAFF04D2AE211E9461FB39D875D74F32E4109D21D5A03D46612000000002E307800002E0002BA11BBBA0202012000000000000007D0000007D0000000C800000007CFFFF83000 \ No newline at end of file diff --git a/eclair-core/src/test/resources/nonreg/codecs/000003-DATA_NORMAL/fundee/data.json b/eclair-core/src/test/resources/nonreg/codecs/000003-DATA_NORMAL/fundee/data.json deleted file mode 100644 index 0021f36d05..0000000000 --- a/eclair-core/src/test/resources/nonreg/codecs/000003-DATA_NORMAL/fundee/data.json +++ /dev/null @@ -1,156 +0,0 @@ -{ - "type" : "DATA_NORMAL", - "commitments" : { - "params" : { - "channelId" : "07738f22c16a24795bcaebc6e09016af61902dd66bbaf64e6e5db50b0c45d63c", - "channelConfig" : [ ], - "channelFeatures" : [ ], - "localParams" : { - "nodeId" : "03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134", - "fundingKeyPath" : [ 1457788542, 1007597768, 1455922339, 479707306 ], - "dustLimit" : 546, - "maxHtlcValueInFlightMsat" : 5000000000, - "initialRequestedChannelReserve_opt" : 167772, - "htlcMinimum" : 1, - "toSelfDelay" : 720, - "maxAcceptedHtlcs" : 30, - "isChannelOpener" : false, - "paysCommitTxFees" : false, - "upfrontShutdownScript_opt" : "a9144805d016e47885dc7c852710cdd8cd0d576f57ec87", - "initFeatures" : { - "activated" : { - "option_data_loss_protect" : "optional", - "initial_routing_sync" : "optional", - "gossip_queries" : "optional" - }, - "unknown" : [ ] - } - }, - "remoteParams" : { - "nodeId" : "034fe52e98a0e9d3c21b767e1b371881265d8c7578c21f5afd6d6438da10348b36", - "dustLimit" : 573, - "maxHtlcValueInFlightMsat" : 16609443000, - "initialRequestedChannelReserve_opt" : 167772, - "htlcMinimum" : 1000, - "toSelfDelay" : 2016, - "maxAcceptedHtlcs" : 483, - "revocationBasepoint" : "02635ac9eedf5f219afbc4d125e37b5705f73c05deca71b05fe84096a691e055c1", - "paymentBasepoint" : "034a711d28e8ed3ad389ec14ec75c199b6a45140c503bcc88110e3524e52ffbfb1", - "delayedPaymentBasepoint" : "0316c70730b57a9e15845ce6f239e749ac78b25f44c90485a697066962a73d0467", - "htlcBasepoint" : "03763e280986fb384631ebf8d637efd9ebcd06b6ef3c77c1375b9edbe3ea3b9f79", - "initFeatures" : { - "activated" : { - "option_data_loss_protect" : "mandatory", - "gossip_queries" : "optional" - }, - "unknown" : [ ] - } - }, - "channelFlags" : { - "nonInitiatorPaysCommitFees" : false, - "announceChannel" : true - } - }, - "changes" : { - "localChanges" : { - "proposed" : [ ], - "signed" : [ ], - "acked" : [ ] - }, - "remoteChanges" : { - "proposed" : [ ], - "acked" : [ ], - "signed" : [ ] - }, - "localNextHtlcId" : 203, - "remoteNextHtlcId" : 4147 - }, - "active" : [ { - "fundingTxIndex" : 0, - "fundingTx" : { - "outPoint" : "3dd6450c0bb55d6e4ef6ba6bd62d9061af1690e0c6ebca5b79246ac1228f7307:1", - "amountSatoshis" : 16777215 - }, - "localFunding" : { - "status" : "unconfirmed" - }, - "remoteFunding" : { - "status" : "locked" - }, - "localCommit" : { - "index" : 7675, - "spec" : { - "htlcs" : [ ], - "commitTxFeerate" : 254, - "toLocal" : 204739729, - "toRemote" : 16572475271 - }, - "commitTxAndRemoteSig" : { - "commitTx" : { - "txid" : "e25a866b79212015e01e155e530fb547abc8276869f8740a9948e52ca231f1e4", - "tx" : "020000000107738f22c16a24795bcaebc6e09016af61902dd66bbaf64e6e5db50b0c45d63d010000000032c3698002c31f0300000000002200205cc91746133145180585bfb3bb9a1c1740c9b43338aa30c90b5f5652d729ce0884dffc0000000000160014cfb373f55b722ca1c028d63ee85cb82c00ce11127af8a620" - }, - "remoteSig" : "4d4d24b8cb3a00dfd685ac73e3c85ba26449dc935469ce36c259f2db6cd519a865845eca78a998bc8213044e84eca0c884cdb01bda8b6e70f5c1ff821ca5388d" - }, - "htlcTxsAndRemoteSigs" : [ ] - }, - "remoteCommit" : { - "index" : 7779, - "spec" : { - "htlcs" : [ ], - "commitTxFeerate" : 254, - "toLocal" : 16572475271, - "toRemote" : 204739729 - }, - "txid" : "ac994c4f64875ab22b45cba175a04cec4051bbe660932570744dad822e6bf8be", - "remotePerCommitmentPoint" : "03daadaed37bcfed40d15e34979fbf2a0643e748e8960363bb8e930cefe2255c35" - } - } ], - "inactive" : [ ], - "remoteNextCommitInfo" : "034dcc0704325064a1fa68edc13adb5fd173051775df73a298ec291f22ad9d19f6", - "remotePerCommitmentSecrets" : null, - "originChannels" : { } - }, - "aliases" : { - "localAlias" : "0x17183c0000170001" - }, - "lastAnnouncement_opt" : { - "nodeSignature1" : "d2366163f4d5a51be3210b66b2e4a2736b9ccc20ce8d0d69413d5b5e42d991401183b271ba032764151ba8f3c4b03f11df5749fd876eeaf3fd401bb383cb3174", - "nodeSignature2" : "075779c27157e5b4024ecee12308cf3bde976a0891983b0655b669b38e7e700362c25ce4af05aaa130f000aa6a04037534a7a23a8d99454948dd689277eab321", - "bitcoinSignature1" : "4049b7649693d92139bf3f1f41da3825d1b3dbed2884797b76fd8e1c77390d1b4f3bf76b8d890485d7555619160a2bf18d58626f2ec9a8ca1f887eba3ba130b5", - "bitcoinSignature2" : "0d55e84fb4059bea082d443934af74dcbfd5c4c2fd54eba3ea2823114df932e7759805207f1182062f99af028aa4b62c7723a0c5b9198fe637a3d18d4d99dc70", - "features" : { - "activated" : { }, - "unknown" : [ ] - }, - "chainHash" : "43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000", - "shortChannelId" : "1513532x23x1", - "nodeId1" : "034fe52e98a0e9d3c21b767e1b371881265d8c7578c21f5afd6d6438da10348b36", - "nodeId2" : "03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134", - "bitcoinKey1" : "028cef3ef020cfda09692afc29e38ac4756ca60736563a93220481091c9cd05b64", - "bitcoinKey2" : "03660d280e24a9b16772a6e6418029719620a5caa29ebdf8339e5d700c611ab9e3", - "tlvStream" : { } - }, - "channelUpdate" : { - "signature" : "4e34a547c424182812bd39b35c1c244b98f2bbb5b7d07812b9a008bb69f3fd77788f4ad338a102c331892afa8d076167a6a6cfb4eac3b890387f0fdc98b5b8c3", - "chainHash" : "43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000", - "shortChannelId" : "1513532x23x1", - "timestamp" : { - "iso" : "2019-06-18T12:49:33Z", - "unix" : 1560862173 - }, - "messageFlags" : { - "dontForward" : false - }, - "channelFlags" : { - "isEnabled" : true, - "isNode1" : false - }, - "cltvExpiryDelta" : 144, - "htlcMinimumMsat" : 1000, - "feeBaseMsat" : 1000, - "feeProportionalMillionths" : 100, - "htlcMaximumMsat" : 16777215000, - "tlvStream" : { } - } -} \ No newline at end of file diff --git a/eclair-core/src/test/resources/nonreg/codecs/000003-DATA_NORMAL/funder/data.bin b/eclair-core/src/test/resources/nonreg/codecs/000003-DATA_NORMAL/funder/data.bin deleted file mode 100644 index cace3d5deb..0000000000 --- a/eclair-core/src/test/resources/nonreg/codecs/000003-DATA_NORMAL/funder/data.bin +++ /dev/null @@ -1 +0,0 @@ -00000303933884AAF1D6B108397E5EFE5C86BCF2D8CA8D2F700EDA99DB9214FC2712B1340004D443ECE9D9C43A11A19B554BAAA6AD150000000000000222000000003B9ACA0000000000000249F000000000000000010090001E800BD48A22F4C80A42CC8BB29A764DBAEFC95674931FBE9A4380000000C50134D4A745996002F219B5FDBA1E045374DF589ECA06ABE23CECAE47343E65EDCF800000000000011E80000001BA90824000000000000124F800000000000001F4038500F1810AE1AF8A1D6F56F80855E26705F191BB07CD4E2434BC5BB1698E7E5880E2266201E8BFEEEEED725775B8116F6F82CF8E87835A5B45B184E56F272AD70D6078118601E06212B8C8F2E25B73EE7974FDCDF007E389B437BBFE238CCC3F3BF7121B6C5E81AA8589D21E9584B24A11F3ABBA5DAD48D121DD63C57A69CD767119C05DA159CB81A649D8CC0E136EB8DFBD2268B69DCA86F8CE4A604235A03D9D37AE7B07FC563F80000000C080800000000000271C000000000177000000002808B14600000001970039BA00123767F0F4F00D5E9FDF24177EF2872343D9F8FAEC65D3048BA575E70E00A0AB08800000000015E070F20000000000110010584241B5FB364208F6E64A80D1166DAD866186B10C015ED0283FF1C308C2105A0023A910810AE1AF8A1D6F56F80855E26705F191BB07CD4E2434BC5BB1698E7E5880E226621081DE8ADFA110DC8A94D8B9E9EF616BAE8598287C8F82AFDF0FC068697D570266FDA95700AD81000000000080B767F0F4F00D5E9FDF24177EF2872343D9F8FAEC65D3048BA575E70E00A0AB0880000000003E7AEDC0011ABE8A00000000001100101A9CE4B6AEF469590BC7BCC51DCEEAE9C86084055A63CC01E443C733FBE400B9B5B16800000000000B000A5E5700106D1A7097E4DE87EBAF1F8F2773842FA482002418228110805E84989A81F51ABD9D11889AE43E68FAD93659DEC019F1B8C0ADBF15A57B118B81101DCC1256F9306439AD3962C043FC47A5179CAAA001CCB23342BE0E8D92E4022780A4182281108074F306DA3751B84EC5FFB155BDCA7B8E02208BBDBC8D4F3327ABA557BF27CD1701102EF4AC8CC92F469DA9642D4D4162BC545F8B34ADE15B7D6F99808AA22B086B0180A3A910810AE1AF8A1D6F56F80855E26705F191BB07CD4E2434BC5BB1698E7E5880E226621081DE8ADFA110DC8A94D8B9E9EF616BAE8598287C8F82AFDF0FC068697D570266FDA9576F8099900000000000000000271C00000000017700000001970039BA000000002808B14648CE00AE97051EE10A3C361263F81A98165CE4AA7BA076933D4266E533585F24815C15DEACF0691332B38ECF23EC39982C5C978C748374A01BA9B30D501EE4F26E8000000000000000000000000000000000001224000000000000004B800040A911C460F1467952E3B99BED072F81BFB4454FF389636DCB399FE6A78113C28580091BB3F87A7806AF4FEF920BBF794391A1ECFC7D7632E98245D2BAF3870050558440000000000AF0387900000000000880082C2120DAFD9B21047B732540688B36D6C330C3588600AF68141FF8E18461082D0011D488408570D7C50EB7AB7C042AF13382F8C8DD83E6A7121A5E2DD8B4C73F2C407113310840EF456FD0886E454A6C5CF4F7B0B5D742CC143E47C157EF87E03434BEAB81337ED4AB8001C00F40003FFFFFFFEC7200403248A1D44DFA3AC9EC237D452C936400CAA86E9517CCCF2A8F77B7493CD70B6A00780001FFFFFFFF63A0041826829646B907A97FBD1455EA8673A12B8E7AA6EA790F7802E955CE3B69DE57E006E0001FFFFFFFF640081E51EB1F91218821E680B50E4B22DF8B094385BD33ACAE36BFC9E8C2F5AD2DA5400EC0003FFFFFFFEC7801047C26AD5435658D063EBCF73A5D0EEFE73ED6B73426246E8DFB3A21D1C4C7465001900007FFFFFFFE0040B115AC58BAAA900195893EA3B2AB408D2AD348AD047E3B6CB15E599625E38608006A0001FFFFFFFF7002033C39A21A38BB61F6FB33623771A9356D8885B7C12C939C770C939EF826286C200360000FFFFFFFFB4008104EF4271064A0973B053727C3E67352D00E25CAEED944F50782449CEAE8F50960001FFFFFFFF6390DD9FC3D3C0357A7F7C905DFBCA1C8D0F67E3EBB1974C122E95D79C380282AC222B21FA0007920001295AA1FB77029F7620A90EF7AE6A6CD31E4588B93264A7ADB76152D535C52E90B9E1B7C2376DABA316A6290F1A9730D4E5E44D0B1CB0EE6A795702E6A6BCDFCDA1A4BFEBFC134AB8847A5187ECE761D75D3CCB904274875680F51984800000000AC87E8001E480002E884D2A8080804800000000000001F4000001F40000003200000001BF08EB000 \ No newline at end of file diff --git a/eclair-core/src/test/resources/nonreg/codecs/000003-DATA_NORMAL/funder/data.json b/eclair-core/src/test/resources/nonreg/codecs/000003-DATA_NORMAL/funder/data.json deleted file mode 100644 index d5fe938497..0000000000 --- a/eclair-core/src/test/resources/nonreg/codecs/000003-DATA_NORMAL/funder/data.json +++ /dev/null @@ -1,139 +0,0 @@ -{ - "type" : "DATA_NORMAL", - "commitments" : { - "params" : { - "channelId" : "6ecfe1e9e01abd3fbe482efde50e4687b3f1f5d8cba609174aebce1c01415611", - "channelConfig" : [ ], - "channelFeatures" : [ ], - "localParams" : { - "nodeId" : "03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134", - "fundingKeyPath" : [ 3561221353, 3653515793, 2711311691, 2863050005 ], - "dustLimit" : 546, - "maxHtlcValueInFlightMsat" : 1000000000, - "initialRequestedChannelReserve_opt" : 150000, - "htlcMinimum" : 1, - "toSelfDelay" : 144, - "maxAcceptedHtlcs" : 30, - "isChannelOpener" : true, - "paysCommitTxFees" : true, - "upfrontShutdownScript_opt" : "a91445e990148599176534ec9b75df92ace9263f7d3487", - "initFeatures" : { - "activated" : { - "option_data_loss_protect" : "optional", - "initial_routing_sync" : "optional", - "gossip_queries" : "optional" - }, - "unknown" : [ ] - } - }, - "remoteParams" : { - "nodeId" : "0269a94e8b32c005e4336bfb743c08a6e9beb13d940d57c479d95c8e687ccbdb9f", - "dustLimit" : 573, - "maxHtlcValueInFlightMsat" : 14850000000, - "initialRequestedChannelReserve_opt" : 150000, - "htlcMinimum" : 1000, - "toSelfDelay" : 1802, - "maxAcceptedHtlcs" : 483, - "revocationBasepoint" : "03d17fdddddae4aeeb7022dedf059f1d0f06b4b68b6309cade4e55ae1ac0f0230c", - "paymentBasepoint" : "03c0c4257191e5c4b6e7dcf2e9fb9be00fc713686f77fc4719987e77ee2436d8bd", - "delayedPaymentBasepoint" : "03550b13a43d2b09649423e75774bb5a91a243bac78af4d39aece23380bb42b397", - "htlcBasepoint" : "034c93b1981c26dd71bf7a44d16d3b950df19c94c0846b407b3a6f5cf60ff8ac7f", - "initFeatures" : { - "activated" : { - "option_data_loss_protect" : "mandatory", - "gossip_queries" : "optional" - }, - "unknown" : [ ] - } - }, - "channelFlags" : { - "nonInitiatorPaysCommitFees" : false, - "announceChannel" : true - } - }, - "changes" : { - "localChanges" : { - "proposed" : [ ], - "signed" : [ ], - "acked" : [ ] - }, - "remoteChanges" : { - "proposed" : [ ], - "acked" : [ ], - "signed" : [ ] - }, - "localNextHtlcId" : 9288, - "remoteNextHtlcId" : 151 - }, - "active" : [ { - "fundingTxIndex" : 0, - "fundingTx" : { - "outPoint" : "115641011cceeb4a1709a6cbd8f5f1b387460ee5fd2e48be3fbd1ae0e9e1cf6e:0", - "amountSatoshis" : 15000000 - }, - "localFunding" : { - "status" : "unconfirmed" - }, - "remoteFunding" : { - "status" : "locked" - }, - "localCommit" : { - "index" : 20024, - "spec" : { - "htlcs" : [ ], - "commitTxFeerate" : 750, - "toLocal" : 1343316620, - "toRemote" : 13656683380 - }, - "commitTxAndRemoteSig" : { - "commitTx" : { - "txid" : "65fe0b1f079fa763448df3ab8d94b1ad7d377c061121376be90b9c0c1bb0cd43", - "tx" : "02000000016ecfe1e9e01abd3fbe482efde50e4687b3f1f5d8cba609174aebce1c0141561100000000007cf5db8002357d1400000000002200203539c96d5de8d2b2178f798a3b9dd5d390c1080ab4c79803c8878e67f7c801736b62d00000000000160014bcae0020da34e12fc9bd0fd75e3f1e4ee7085f49df013320" - }, - "remoteSig" : "bd09313503ea357b3a231135c87cd1f5b26cb3bd8033e371815b7e2b4af623173b9824adf260c8735a72c58087f88f4a2f39554003996466857c1d1b25c8044f" - }, - "htlcTxsAndRemoteSigs" : [ ] - }, - "remoteCommit" : { - "index" : 20024, - "spec" : { - "htlcs" : [ ], - "commitTxFeerate" : 750, - "toLocal" : 13656683380, - "toRemote" : 1343316620 - }, - "txid" : "919c015d2e0a3dc214786c24c7f035302cb9c954f740ed267a84cdca66b0be49", - "remotePerCommitmentPoint" : "02b82bbd59e0d22665671d9e47d8733058b92f18e906e9403753661aa03dc9e4dd" - } - } ], - "inactive" : [ ], - "remoteNextCommitInfo" : "02a4471183c519e54b8ee66fb41cbe06fed1153fce258db72ce67f9a9e044f0a16", - "remotePerCommitmentSecrets" : null, - "originChannels" : { } - }, - "aliases" : { - "localAlias" : "0x1590fd0003c90000" - }, - "channelUpdate" : { - "signature" : "52b543f6ee053eec41521def5cd4d9a63c8b117264c94f5b6ec2a5aa6b8a5d2173c36f846edb57462d4c521e352e61a9cbc89a163961dcd4f2ae05cd4d79bf9b", - "chainHash" : "43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000", - "shortChannelId" : "1413373x969x0", - "timestamp" : { - "iso" : "2019-06-24T09:39:33Z", - "unix" : 1561369173 - }, - "messageFlags" : { - "dontForward" : false - }, - "channelFlags" : { - "isEnabled" : true, - "isNode1" : false - }, - "cltvExpiryDelta" : 144, - "htlcMinimumMsat" : 1000, - "feeBaseMsat" : 1000, - "feeProportionalMillionths" : 100, - "htlcMaximumMsat" : 15000000000, - "tlvStream" : { } - } -} \ No newline at end of file diff --git a/eclair-core/src/test/resources/nonreg/codecs/020002-DATA_NORMAL/funder/data.bin b/eclair-core/src/test/resources/nonreg/codecs/020002-DATA_NORMAL/funder/data.bin deleted file mode 100644 index fbdcec643a..0000000000 --- a/eclair-core/src/test/resources/nonreg/codecs/020002-DATA_NORMAL/funder/data.bin +++ /dev/null @@ -1 +0,0 @@ -0200020000000303933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b13400098c4b989bbdced820a77a7186c2320e7d176a5c8b5c16d6ac2af3889d6bc8bf8080000001000000000000022200000004a817c80000000000000249f0000000000000000102d0001eff1600148061b7fbd2d84ed1884177ea785faecb2080b10302e56c8eca8d4f00df84ac34c23f49c006d57d316b7ada5c346e9d4211e11604b300000004080aa982027455aef8453d92f4706b560b61527cc217ddf14da41770e8ed6607190a1851b8000000000000023d000000037521048000000000000249f00000000000000001070a01e302eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b0343bf4bfbaea5c100f1f2bf1cdf82a0ef97c9a0069a2aec631e7c3084ba929b7503c54e7d5ccfc13f1a6c7a441ffcfac86248574d1bc0fe9773836f4c724ea7b2bd03765aaac2e8fa6dbce7de5143072e9d9d5e96a1fd451d02fe4ff803f413f303f8022f3b055b0d35cde31dec5263a8ed638433e3424a4e197c06d94053985a364a5700000004808a52a1010000000000000004000000001046000000037e11d6000000000000000000245986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b000000002bc0e1e40000000000220020690fb50de412adf9b20a7fc6c8fb86f1bfd4ebc1ef8e2d96a5a196560798d944475221023ced05ed1ab328b67477376d68a69ecd0f371a9d5843c6c3be4d31498d516d8d2102eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b52aefd013b020000000001015986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b0000000000c2d6178001f8d5e4000000000022002080f1dfe71a865b605593e169677c952aaa1196fc2f541ef7d21c3b1006527b61040047304402207f8c1936d0a50671c993890f887c78c6019abc2a2e8018899dcdc0e891fd2b090220046b56afa2cb7e9470073c238654ecf584bcf5c00b96b91e38335a70e2739ec901483045022100871afd240e20a171b9cba46f20555f848c5850f94ec7da7b33b9eeaf6af6653c0220119cda8cbf5f80986d6a4f0db2590c734d1de399a7060a477b5d94df0183625b01475221023ced05ed1ab328b67477376d68a69ecd0f371a9d5843c6c3be4d31498d516d8d2102eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b52aed7782c20000000000000000000040000000010460000000000000000000000037e11d600b5f2287b2d5edf4df5602a3c287db3b938c3f1a943e40715886db5bd400f95d802e7e1abac1feb54ee3ac2172c9e2231f77765df57664fb44a6dc2e4aa9e6a9a6a000000000000000000000000000000000000000000000000000000000000ff03fd10fe44564e2d7e1550099785c2c1bad32a5ae0feeef6e27f0c108d18b4931d245986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b000000002bc0e1e40000000000220020690fb50de412adf9b20a7fc6c8fb86f1bfd4ebc1ef8e2d96a5a196560798d944475221023ced05ed1ab328b67477376d68a69ecd0f371a9d5843c6c3be4d31498d516d8d2102eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b52ae0001003e0000fffffffffffc0080474b8cf7bb98217dd8dc475cb7c057a3465d466728978bbb909d0a05d4ae7bbe0001fffffffffff85986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b1eedce0000010000fffffd01ae98d7a81bc1aa92fcfb74ced2213e85e0d92ae8ac622bf294b3551c7c27f6f84f782f3b318e4d0eb2c67ac719a7c65afcf85bf159f6ceea9427be54920134196992f6ed0e059db72105a13ec0e799bb08896cad8b4feb7e9ec7283c309b5f43123af1bd9e913fc2db018edadde8932d6992408f10c1ad020504361972dfa7fef09bbc2b568cef3c8c006f7860106fd5984bcc271ff06c4829db2a665e59b7c0b22c311a340ff2ab9bcb74a50db10ed85503ad2d248d95af8151aca8ef96248e8f84b3075922385fbaf012f057e7ee84ecbc14c84880520b26d6fd22ab5f107db606a906efdcf0f88ffbe32dc6ecc10131e1ff0dc8d68dad89c98562557f00448b000043497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea3309000000001eedce0000010000027455aef8453d92f4706b560b61527cc217ddf14da41770e8ed6607190a1851b803933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b13402eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b023ced05ed1ab328b67477376d68a69ecd0f371a9d5843c6c3be4d31498d516d8d88710d73875607575f3d84bb507dd87cca5b85f0cdac84f4ccecce7af3a55897525a45070fe26c0ea43e9580d4ea4cfa62ee3273e5546911145cba6bbf56e59d8e43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea3309000000001eedce000001000060e6eb14010100900000000000000001000003e800000064000000037e11d6000000 \ No newline at end of file diff --git a/eclair-core/src/test/resources/nonreg/codecs/020002-DATA_NORMAL/funder/data.json b/eclair-core/src/test/resources/nonreg/codecs/020002-DATA_NORMAL/funder/data.json deleted file mode 100644 index b723107096..0000000000 --- a/eclair-core/src/test/resources/nonreg/codecs/020002-DATA_NORMAL/funder/data.json +++ /dev/null @@ -1,170 +0,0 @@ -{ - "type" : "DATA_NORMAL", - "commitments" : { - "params" : { - "channelId" : "5986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b", - "channelConfig" : [ "funding_pubkey_based_channel_keypath" ], - "channelFeatures" : [ "option_static_remotekey" ], - "localParams" : { - "nodeId" : "03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134", - "fundingKeyPath" : [ 2353764507, 3184449568, 2809819526, 3258060413, 392846475, 1545000620, 720603293, 1808318336, 2147483649 ], - "dustLimit" : 546, - "maxHtlcValueInFlightMsat" : 20000000000, - "initialRequestedChannelReserve_opt" : 150000, - "htlcMinimum" : 1, - "toSelfDelay" : 720, - "maxAcceptedHtlcs" : 30, - "isChannelOpener" : true, - "paysCommitTxFees" : true, - "upfrontShutdownScript_opt" : "00148061b7fbd2d84ed1884177ea785faecb2080b103", - "walletStaticPaymentBasepoint" : "02e56c8eca8d4f00df84ac34c23f49c006d57d316b7ada5c346e9d4211e11604b3", - "initFeatures" : { - "activated" : { - "option_support_large_channel" : "optional", - "gossip_queries_ex" : "optional", - "option_data_loss_protect" : "optional", - "var_onion_optin" : "mandatory", - "option_static_remotekey" : "optional", - "payment_secret" : "optional", - "option_shutdown_anysegwit" : "optional", - "basic_mpp" : "optional", - "gossip_queries" : "optional" - }, - "unknown" : [ ] - } - }, - "remoteParams" : { - "nodeId" : "027455aef8453d92f4706b560b61527cc217ddf14da41770e8ed6607190a1851b8", - "dustLimit" : 573, - "maxHtlcValueInFlightMsat" : 14850000000, - "initialRequestedChannelReserve_opt" : 150000, - "htlcMinimum" : 1, - "toSelfDelay" : 1802, - "maxAcceptedHtlcs" : 483, - "revocationBasepoint" : "0343bf4bfbaea5c100f1f2bf1cdf82a0ef97c9a0069a2aec631e7c3084ba929b75", - "paymentBasepoint" : "03c54e7d5ccfc13f1a6c7a441ffcfac86248574d1bc0fe9773836f4c724ea7b2bd", - "delayedPaymentBasepoint" : "03765aaac2e8fa6dbce7de5143072e9d9d5e96a1fd451d02fe4ff803f413f303f8", - "htlcBasepoint" : "022f3b055b0d35cde31dec5263a8ed638433e3424a4e197c06d94053985a364a57", - "initFeatures" : { - "activated" : { - "option_upfront_shutdown_script" : "optional", - "payment_secret" : "mandatory", - "option_data_loss_protect" : "mandatory", - "var_onion_optin" : "optional", - "option_static_remotekey" : "mandatory", - "option_support_large_channel" : "optional", - "option_anchors_zero_fee_htlc_tx" : "optional", - "basic_mpp" : "optional", - "gossip_queries" : "optional" - }, - "unknown" : [ 31 ] - } - }, - "channelFlags" : { - "nonInitiatorPaysCommitFees" : false, - "announceChannel" : true - } - }, - "changes" : { - "localChanges" : { - "proposed" : [ ], - "signed" : [ ], - "acked" : [ ] - }, - "remoteChanges" : { - "proposed" : [ ], - "acked" : [ ], - "signed" : [ ] - }, - "localNextHtlcId" : 0, - "remoteNextHtlcId" : 0 - }, - "active" : [ { - "fundingTxIndex" : 0, - "fundingTx" : { - "outPoint" : "1bade1718aaf98ab1f91a97ed5b34ab47bfb78085e384f67c156793544f68659:0", - "amountSatoshis" : 15000000 - }, - "localFunding" : { - "status" : "unconfirmed" - }, - "remoteFunding" : { - "status" : "locked" - }, - "localCommit" : { - "index" : 4, - "spec" : { - "htlcs" : [ ], - "commitTxFeerate" : 4166, - "toLocal" : 15000000000, - "toRemote" : 0 - }, - "commitTxAndRemoteSig" : { - "commitTx" : { - "txid" : "fa747ecb6f718c6831cc7148cf8d65c3468d2bb6c202605e2b82d2277491222f", - "tx" : "02000000015986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b0000000000c2d6178001f8d5e4000000000022002080f1dfe71a865b605593e169677c952aaa1196fc2f541ef7d21c3b1006527b61d7782c20" - }, - "remoteSig" : "871afd240e20a171b9cba46f20555f848c5850f94ec7da7b33b9eeaf6af6653c119cda8cbf5f80986d6a4f0db2590c734d1de399a7060a477b5d94df0183625b" - }, - "htlcTxsAndRemoteSigs" : [ ] - }, - "remoteCommit" : { - "index" : 4, - "spec" : { - "htlcs" : [ ], - "commitTxFeerate" : 4166, - "toLocal" : 0, - "toRemote" : 15000000000 - }, - "txid" : "b5f2287b2d5edf4df5602a3c287db3b938c3f1a943e40715886db5bd400f95d8", - "remotePerCommitmentPoint" : "02e7e1abac1feb54ee3ac2172c9e2231f77765df57664fb44a6dc2e4aa9e6a9a6a" - } - } ], - "inactive" : [ ], - "remoteNextCommitInfo" : "03fd10fe44564e2d7e1550099785c2c1bad32a5ae0feeef6e27f0c108d18b4931d", - "remotePerCommitmentSecrets" : null, - "originChannels" : { } - }, - "aliases" : { - "localAlias" : "0x1eedce0000010000" - }, - "lastAnnouncement_opt" : { - "nodeSignature1" : "98d7a81bc1aa92fcfb74ced2213e85e0d92ae8ac622bf294b3551c7c27f6f84f782f3b318e4d0eb2c67ac719a7c65afcf85bf159f6ceea9427be549201341969", - "nodeSignature2" : "92f6ed0e059db72105a13ec0e799bb08896cad8b4feb7e9ec7283c309b5f43123af1bd9e913fc2db018edadde8932d6992408f10c1ad020504361972dfa7fef0", - "bitcoinSignature1" : "9bbc2b568cef3c8c006f7860106fd5984bcc271ff06c4829db2a665e59b7c0b22c311a340ff2ab9bcb74a50db10ed85503ad2d248d95af8151aca8ef96248e8f", - "bitcoinSignature2" : "84b3075922385fbaf012f057e7ee84ecbc14c84880520b26d6fd22ab5f107db606a906efdcf0f88ffbe32dc6ecc10131e1ff0dc8d68dad89c98562557f00448b", - "features" : { - "activated" : { }, - "unknown" : [ ] - }, - "chainHash" : "43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000", - "shortChannelId" : "2026958x1x0", - "nodeId1" : "027455aef8453d92f4706b560b61527cc217ddf14da41770e8ed6607190a1851b8", - "nodeId2" : "03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134", - "bitcoinKey1" : "02eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b", - "bitcoinKey2" : "023ced05ed1ab328b67477376d68a69ecd0f371a9d5843c6c3be4d31498d516d8d", - "tlvStream" : { } - }, - "channelUpdate" : { - "signature" : "710d73875607575f3d84bb507dd87cca5b85f0cdac84f4ccecce7af3a55897525a45070fe26c0ea43e9580d4ea4cfa62ee3273e5546911145cba6bbf56e59d8e", - "chainHash" : "43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000", - "shortChannelId" : "2026958x1x0", - "timestamp" : { - "iso" : "2021-07-08T12:09:56Z", - "unix" : 1625746196 - }, - "messageFlags" : { - "dontForward" : false - }, - "channelFlags" : { - "isEnabled" : true, - "isNode1" : false - }, - "cltvExpiryDelta" : 144, - "htlcMinimumMsat" : 1, - "feeBaseMsat" : 1000, - "feeProportionalMillionths" : 100, - "htlcMaximumMsat" : 15000000000, - "tlvStream" : { } - } -} \ No newline at end of file diff --git a/eclair-core/src/test/resources/nonreg/codecs/030000-DATA_WAIT_FOR_FUNDING_CONFIRMED/funder/data.json b/eclair-core/src/test/resources/nonreg/codecs/030000-DATA_WAIT_FOR_FUNDING_CONFIRMED/funder/data.json deleted file mode 100644 index 677a22cfdf..0000000000 --- a/eclair-core/src/test/resources/nonreg/codecs/030000-DATA_WAIT_FOR_FUNDING_CONFIRMED/funder/data.json +++ /dev/null @@ -1,130 +0,0 @@ -{ - "type" : "DATA_WAIT_FOR_FUNDING_CONFIRMED", - "commitments" : { - "params" : { - "channelId" : "e917adc681383fe00f779c6144a1bd91135ba2c9862ad1bc5aa8a14d37bae3f4", - "channelConfig" : [ "funding_pubkey_based_channel_keypath" ], - "channelFeatures" : [ ], - "localParams" : { - "nodeId" : "02aaaa00ce2f18a967dc4f25f414e671ba6585f8ef0b8c5fb812c21064f55a2eaa", - "fundingKeyPath" : [ 4092535092, 4227137620, 3959690417, 2298849496, 2106263857, 1090614243, 1495530077, 1280982866, 2147483649 ], - "dustLimit" : 1100, - "maxHtlcValueInFlightMsat" : 500000000, - "initialRequestedChannelReserve_opt" : 10000, - "htlcMinimum" : 0, - "toSelfDelay" : 144, - "maxAcceptedHtlcs" : 100, - "isChannelOpener" : true, - "paysCommitTxFees" : true, - "upfrontShutdownScript_opt" : "0014fec406ef7a0258cb503fe1f1803787d971eeb4d1", - "initFeatures" : { - "activated" : { - "payment_secret" : "mandatory", - "gossip_queries_ex" : "optional", - "option_data_loss_protect" : "optional", - "var_onion_optin" : "mandatory", - "basic_mpp" : "optional", - "gossip_queries" : "optional" - }, - "unknown" : [ 50001 ] - } - }, - "remoteParams" : { - "nodeId" : "02bbbb671d15145722fb8c28d732cddb249bcc6652ed2b297ff1f77a18371b1e63", - "dustLimit" : 1000, - "maxHtlcValueInFlightMsat" : 18446744073709551615, - "initialRequestedChannelReserve_opt" : 20000, - "htlcMinimum" : 1000, - "toSelfDelay" : 144, - "maxAcceptedHtlcs" : 30, - "revocationBasepoint" : "02f7c3bf47cdc640304eda4c761a26dfebfee561de15ba106f3d9982d3ef3fbe10", - "paymentBasepoint" : "02be361b7bf1bdfb283cff3f83bf16f6f8fb67d3f480b541e76518939f667ab834", - "delayedPaymentBasepoint" : "03fcaec5443dd423f160c9b77a48b2585b186f2d850147f57210d3f8a8c8d754a7", - "htlcBasepoint" : "021eecb7915d4a3f2b391b9b1fbccbaad2006a1e67a0a03aa041721a817e9a0874", - "initFeatures" : { - "activated" : { - "payment_secret" : "mandatory", - "gossip_queries_ex" : "optional", - "option_data_loss_protect" : "optional", - "var_onion_optin" : "mandatory", - "basic_mpp" : "optional", - "gossip_queries" : "optional" - }, - "unknown" : [ ] - } - }, - "channelFlags" : { - "nonInitiatorPaysCommitFees" : false, - "announceChannel" : false - } - }, - "changes" : { - "localChanges" : { - "proposed" : [ ], - "signed" : [ ], - "acked" : [ ] - }, - "remoteChanges" : { - "proposed" : [ ], - "acked" : [ ], - "signed" : [ ] - }, - "localNextHtlcId" : 0, - "remoteNextHtlcId" : 0 - }, - "active" : [ { - "fundingTxIndex" : 0, - "fundingTx" : { - "outPoint" : "f4e3ba374da1a85abcd12a86c9a25b1391bda144619c770fe03f3881c6ad17e9:0", - "amountSatoshis" : 1000000 - }, - "localFunding" : { - "status" : "unconfirmed", - "txid" : "f4e3ba374da1a85abcd12a86c9a25b1391bda144619c770fe03f3881c6ad17e9" - }, - "remoteFunding" : { - "status" : "locked" - }, - "localCommit" : { - "index" : 0, - "spec" : { - "htlcs" : [ ], - "commitTxFeerate" : 10000, - "toLocal" : 800000000, - "toRemote" : 200000000 - }, - "commitTxAndRemoteSig" : { - "commitTx" : { - "txid" : "c6fe6bc0a5a9c149a03a907d2351714aa27fc98a485e981343cea08a1904ee26", - "tx" : "0200000001e917adc681383fe00f779c6144a1bd91135ba2c9862ad1bc5aa8a14d37bae3f40000000000fd11418002400d0300000000001600142276cff9d96f4696d6e504568db62088428706e0b8180c0000000000220020ad1b593fc0780225407ba65c612b974b4b66610e129f21064cb8ada0ffe4c8d2b7262120" - }, - "remoteSig" : "1ea9cffd2af82f6c14251bd59ffa3e876178c7b263f16d12790c33fc093eaed053dd9e0d49f79558aeb3c2fe7fe84f95a91eecbea2679fb65916ad9632df07b4" - }, - "htlcTxsAndRemoteSigs" : [ ] - }, - "remoteCommit" : { - "index" : 0, - "spec" : { - "htlcs" : [ ], - "commitTxFeerate" : 10000, - "toLocal" : 200000000, - "toRemote" : 800000000 - }, - "txid" : "10e4205393672b61bbde4261441a2cf8d08ae50fdb813df8efaac6c6090ef290", - "remotePerCommitmentPoint" : "032a992c123095216f7937a8b0baf442211eeb57942d586854a61a0dc6b01ca6ee" - } - } ], - "inactive" : [ ], - "remoteNextCommitInfo" : "030af74aa1e98668a504d50fe6f664aff3fbdb5c8681f0667c34cdb80024fb950f", - "remotePerCommitmentSecrets" : null, - "originChannels" : { } - }, - "waitingSince" : 400000, - "lastSent" : { - "temporaryChannelId" : "0000000000000000000000000000000000000000000000000000000000000000", - "fundingTxId" : "f4e3ba374da1a85abcd12a86c9a25b1391bda144619c770fe03f3881c6ad17e9", - "fundingOutputIndex" : 0, - "signature" : "bd55d8660f54a1be6e123e2ed9cd6669d90b830d99dc6f9addd9ae65c447ea6b657e2fe39841f66c1d6a2fc80ad59e6f3d9bfaf177f4e95a579f0683bf3e9790", - "tlvStream" : { } - } -} \ No newline at end of file diff --git a/eclair-core/src/test/resources/nonreg/codecs/03000a-DATA_WAIT_FOR_CHANNEL_READY/funder/data.json b/eclair-core/src/test/resources/nonreg/codecs/03000a-DATA_WAIT_FOR_CHANNEL_READY/funder/data.json deleted file mode 100644 index bf125aac53..0000000000 --- a/eclair-core/src/test/resources/nonreg/codecs/03000a-DATA_WAIT_FOR_CHANNEL_READY/funder/data.json +++ /dev/null @@ -1,126 +0,0 @@ -{ - "type" : "DATA_WAIT_FOR_CHANNEL_READY", - "commitments" : { - "params" : { - "channelId" : "83a6dea8adf975b60df17738937034a2f96cb8b784da01e2934e9e172244317d", - "channelConfig" : [ "funding_pubkey_based_channel_keypath" ], - "channelFeatures" : [ ], - "localParams" : { - "nodeId" : "02aaaa00ce2f18a967dc4f25f414e671ba6585f8ef0b8c5fb812c21064f55a2eaa", - "fundingKeyPath" : [ 303987973, 3198768511, 3783619274, 2277156978, 1699864653, 63358126, 3265052696, 516813756, 2147483649 ], - "dustLimit" : 1100, - "maxHtlcValueInFlightMsat" : 500000000, - "initialRequestedChannelReserve_opt" : 10000, - "htlcMinimum" : 0, - "toSelfDelay" : 144, - "maxAcceptedHtlcs" : 100, - "isChannelOpener" : true, - "paysCommitTxFees" : true, - "upfrontShutdownScript_opt" : "0014c59265957886e166f37c863dca15b49aa42d75b4", - "initFeatures" : { - "activated" : { - "option_route_blinding" : "optional", - "payment_secret" : "mandatory", - "gossip_queries_ex" : "optional", - "option_data_loss_protect" : "optional", - "var_onion_optin" : "mandatory", - "basic_mpp" : "optional", - "gossip_queries" : "optional" - }, - "unknown" : [ 50001 ] - } - }, - "remoteParams" : { - "nodeId" : "02bbbb671d15145722fb8c28d732cddb249bcc6652ed2b297ff1f77a18371b1e63", - "dustLimit" : 1000, - "maxHtlcValueInFlightMsat" : 1000000000, - "initialRequestedChannelReserve_opt" : 20000, - "htlcMinimum" : 1000, - "toSelfDelay" : 144, - "maxAcceptedHtlcs" : 30, - "revocationBasepoint" : "033b164d14b54f08a951f9e40fb84e637021f376c9ae2907975f9ff0795431205d", - "paymentBasepoint" : "03759da3f6bdcc2f595e1a98eb4803729743ab8608e28788cfe96726b5328c214c", - "delayedPaymentBasepoint" : "03abc3ae99fac104e94f79cafcd70247cc336cdc21a53d4c3a4c321b54e20902e7", - "htlcBasepoint" : "02d457910d53f6a735571f09b85a25797f5cb06df20be8b3c39412b3fe18a0f6f9", - "initFeatures" : { - "activated" : { - "option_route_blinding" : "optional", - "payment_secret" : "mandatory", - "gossip_queries_ex" : "optional", - "option_data_loss_protect" : "optional", - "var_onion_optin" : "mandatory", - "basic_mpp" : "optional", - "gossip_queries" : "optional" - }, - "unknown" : [ ] - } - }, - "channelFlags" : { - "nonInitiatorPaysCommitFees" : false, - "announceChannel" : false - } - }, - "changes" : { - "localChanges" : { - "proposed" : [ ], - "signed" : [ ], - "acked" : [ ] - }, - "remoteChanges" : { - "proposed" : [ ], - "acked" : [ ], - "signed" : [ ] - }, - "localNextHtlcId" : 0, - "remoteNextHtlcId" : 0 - }, - "active" : [ { - "fundingTxIndex" : 0, - "fundingTx" : { - "outPoint" : "7d314422179e4e93e201da84b7b86cf9a23470933877f10db675f9ada8dea683:0", - "amountSatoshis" : 1000000 - }, - "localFunding" : { - "status" : "unconfirmed" - }, - "remoteFunding" : { - "status" : "locked" - }, - "localCommit" : { - "index" : 0, - "spec" : { - "htlcs" : [ ], - "commitTxFeerate" : 10000, - "toLocal" : 800000000, - "toRemote" : 200000000 - }, - "commitTxAndRemoteSig" : { - "commitTx" : { - "txid" : "a591186503570022767b96fc4e02448e2c2d95f9e83f06c526f810886b02dedd", - "tx" : "020000000183a6dea8adf975b60df17738937034a2f96cb8b784da01e2934e9e172244317d0000000000fc10bb8002400d03000000000016001434b58af5c6bf8472898ef7ed44f65bce2e4a6101b8180c00000000002200206bf6eeedf2e93dee452c92c0c1e2421cc064bf8f70b432c6e36459215576d24decab0320" - }, - "remoteSig" : "8ccca40bbf6c859ceb2d5288153801426e19f8b119c2d1d4ba6d82511c7816aa0eaa8e41eafb179818d1ba7b0cfdce2da5fa002921df00a1fb4e1458d0b9042c" - }, - "htlcTxsAndRemoteSigs" : [ ] - }, - "remoteCommit" : { - "index" : 0, - "spec" : { - "htlcs" : [ ], - "commitTxFeerate" : 10000, - "toLocal" : 200000000, - "toRemote" : 800000000 - }, - "txid" : "839b92423077adb38eeaa118dd6ba0e6daf8c1e0add666ddd18f57b517a2d750", - "remotePerCommitmentPoint" : "0324b50221ad635b97f597802fbe5b2d6414fdf41f224ac1869d3772314e9fbfa5" - } - } ], - "inactive" : [ ], - "remoteNextCommitInfo" : "0209317c45de4cff05adbf9d69edbc334a1c89325bade86f4194c6665336b7e9f8", - "remotePerCommitmentSecrets" : null, - "originChannels" : { } - }, - "aliases" : { - "localAlias" : "0x3dab1f7dc6942fd" - } -} \ No newline at end of file diff --git a/eclair-core/src/test/resources/nonreg/codecs/03000c-DATA_WAIT_FOR_DUAL_FUNDING_READY/funder/data.json b/eclair-core/src/test/resources/nonreg/codecs/03000c-DATA_WAIT_FOR_DUAL_FUNDING_READY/funder/data.json deleted file mode 100644 index 702dd3c5b5..0000000000 --- a/eclair-core/src/test/resources/nonreg/codecs/03000c-DATA_WAIT_FOR_DUAL_FUNDING_READY/funder/data.json +++ /dev/null @@ -1,132 +0,0 @@ -{ - "type" : "DATA_WAIT_FOR_DUAL_FUNDING_READY", - "commitments" : { - "params" : { - "channelId" : "f5cafea10bc83c2fe9de16d04bb73c1ddaed2ce9d600dd91301a7d995f7b9134", - "channelConfig" : [ "funding_pubkey_based_channel_keypath" ], - "channelFeatures" : [ "option_static_remotekey", "option_anchors_zero_fee_htlc_tx", "option_dual_fund" ], - "localParams" : { - "nodeId" : "02aaaa00ce2f18a967dc4f25f414e671ba6585f8ef0b8c5fb812c21064f55a2eaa", - "fundingKeyPath" : [ 3109590638, 2571039769, 2759029525, 192289746, 3879532998, 1343053922, 3645251601, 1767821717, 2147483649 ], - "dustLimit" : 1100, - "maxHtlcValueInFlightMsat" : 500000000, - "htlcMinimum" : 0, - "toSelfDelay" : 144, - "maxAcceptedHtlcs" : 100, - "isChannelOpener" : true, - "paysCommitTxFees" : true, - "upfrontShutdownScript_opt" : "0014fe2baa428e1f6b4b4134d444b3de42a86cffbabc", - "initFeatures" : { - "activated" : { - "option_anchors_zero_fee_htlc_tx" : "optional", - "option_route_blinding" : "optional", - "option_dual_fund" : "optional", - "payment_secret" : "mandatory", - "gossip_queries_ex" : "optional", - "option_anchor_outputs" : "optional", - "option_data_loss_protect" : "optional", - "var_onion_optin" : "mandatory", - "option_static_remotekey" : "optional", - "basic_mpp" : "optional", - "gossip_queries" : "optional" - }, - "unknown" : [ 50001 ] - } - }, - "remoteParams" : { - "nodeId" : "02bbbb671d15145722fb8c28d732cddb249bcc6652ed2b297ff1f77a18371b1e63", - "dustLimit" : 1000, - "maxHtlcValueInFlightMsat" : 1000000000, - "htlcMinimum" : 1000, - "toSelfDelay" : 144, - "maxAcceptedHtlcs" : 30, - "revocationBasepoint" : "03626342f0af6e87ab41715bd6d1db7d6eee5e95a2a835680b42d9b357003c1c6a", - "paymentBasepoint" : "02ed6013749b4e3a820e7c003d3d3c7c5f16dd0d25ce72d3de99e544d4011c0a67", - "delayedPaymentBasepoint" : "038a15a3dcd087135509b8e91d95c4fb788be11735f5c1a9ac24283d3cecbc76d2", - "htlcBasepoint" : "036d77c307c066c5308618e885d18475ea18fde69887df29ae3c2dfdffd2dfbbb1", - "initFeatures" : { - "activated" : { - "option_anchors_zero_fee_htlc_tx" : "optional", - "option_route_blinding" : "optional", - "option_dual_fund" : "optional", - "payment_secret" : "mandatory", - "gossip_queries_ex" : "optional", - "option_anchor_outputs" : "optional", - "option_data_loss_protect" : "optional", - "var_onion_optin" : "mandatory", - "option_static_remotekey" : "optional", - "basic_mpp" : "optional", - "gossip_queries" : "optional" - }, - "unknown" : [ ] - } - }, - "channelFlags" : { - "nonInitiatorPaysCommitFees" : false, - "announceChannel" : false - } - }, - "changes" : { - "localChanges" : { - "proposed" : [ ], - "signed" : [ ], - "acked" : [ ] - }, - "remoteChanges" : { - "proposed" : [ ], - "acked" : [ ], - "signed" : [ ] - }, - "localNextHtlcId" : 0, - "remoteNextHtlcId" : 0 - }, - "active" : [ { - "fundingTxIndex" : 0, - "fundingTx" : { - "outPoint" : "7443277377ab5ca44330a332d79e6ff33d21a3b8889559f54894982af47e1cdb:0", - "amountSatoshis" : 1500000 - }, - "localFunding" : { - "status" : "unconfirmed" - }, - "remoteFunding" : { - "status" : "locked" - }, - "localCommit" : { - "index" : 0, - "spec" : { - "htlcs" : [ ], - "commitTxFeerate" : 2500, - "toLocal" : 1000000000, - "toRemote" : 500000000 - }, - "commitTxAndRemoteSig" : { - "commitTx" : { - "txid" : "b543f86a2a9a06f80b7339f9aaa169a779a7754d3523c28845d10dad9b9d7bc9", - "tx" : "0200000001db1c7ef42a989448f5599588b8a3213df36f9ed732a33043a45cab7773274374000000000058951280044a010000000000002200203a52179d307a973d00b2b102a0df754258ba7f0d1011a2a5d9e2844004b76d8a4a01000000000000220020a8b562e803e0523f85ff90ff8f90571a03c016bb61f387016d16db99f539c54e20a107000000000022002084bbfa5dee3d11a165c2ec7c0ce8b66e055fd7b6d90be6f0162dd4887cc25226b2340f00000000002200207fafaeecce02b12fad250d513a7493fc21bc9f4b82c7bbc115de3038cf984f933e9c0820" - }, - "remoteSig" : "5d65b42b0a8f05ca2793360a6b34bd5c1b9d960cf62c300c0e493f9cd83d2ca41f46a663d4fc11891ef268c0200b8fea8fb454d9055d35c2d6f5ba0b14c78b2a" - }, - "htlcTxsAndRemoteSigs" : [ ] - }, - "remoteCommit" : { - "index" : 0, - "spec" : { - "htlcs" : [ ], - "commitTxFeerate" : 2500, - "toLocal" : 500000000, - "toRemote" : 1000000000 - }, - "txid" : "990138002f6fc36ae3ea48b88e2302a80eb925c33668e59638a82840f1e633ad", - "remotePerCommitmentPoint" : "037d0b91e7bf58eec2eddf033d457b17140a341533808a346c869ada9ecea0cec0" - } - } ], - "inactive" : [ ], - "remoteNextCommitInfo" : "02a7d9d163632731c7211ced4ee21ae181bb0dfa73f5538607c081dd63d89f9820", - "remotePerCommitmentSecrets" : null, - "originChannels" : { } - }, - "aliases" : { - "localAlias" : "0x2d1583d8409d217" - } -} \ No newline at end of file diff --git a/eclair-core/src/test/resources/nonreg/codecs/04000e-DATA_NORMAL/announced/data.bin b/eclair-core/src/test/resources/nonreg/codecs/04000e-DATA_NORMAL/announced/data.bin deleted file mode 100644 index f7bb4696c7..0000000000 --- a/eclair-core/src/test/resources/nonreg/codecs/04000e-DATA_NORMAL/announced/data.bin +++ /dev/null @@ -1 +0,0 @@ -04000e01c380aa11700db0a6d797dfd0be8aecfadad9397e4975f0fa8c9d10db71feac38010102100002aaaa00ce2f18a967dc4f25f414e671ba6585f8ef0b8c5fb812c21064f55a2eaa00093f0bc6821d13ef53b6de934ca894a123e1f5241e8e5767effee1562c0cfbdc9b80000001000000000000044c000000001dcd65000000000000002710000000000000000000900064c000028feba10d0eafd0fad8fe20e6d9206e6bd30242826de05c63f459a00aced24b120000186b02000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000808020a598202bbbb671d15145722fb8c28d732cddb249bcc6652ed2b297ff1f77a18371b1e6300000000000003e8000000003b9aca000000000000004e2000000000000003e80090001e02f943c4f199d1425fc6e52f160be536d526e9643af1430cfed4ce63f88beebd2f028feba10d0eafd0fad8fe20e6d9206e6bd30242826de05c63f459a00aced24b1203f3b6c8919d313d5bd14a2e567649ea80ed1dd299ebc72766775a1f4fdc3cc1b5027ebc1b165dfe376d2a5b7bfba6419af4670b1b89b3aabf8bcbc2b5f33f0db2740000001408000000000000000000000000000008028a59820001000000000000000000000000000000000000000000000000000000000000000100000000000000000000000003613ca8550a9ce1252ed80d39dce2e56f5befdedd70019e195bc0e1448e48b4d6095e020000000101010101010101010101010101010101010101010101010101010101010101012a00000000ffffffff0140420f00000000002200202f7e4f9d9557aa3819fba995db85e76bf85bda1fb209d429121c3209394e379b000000000000010000000000000000000000002710000000002faf0800000000000bebc20024c380aa11700db0a6d797dfd0be8aecfadad9397e4975f0fa8c9d10db71feac38000000002b40420f00000000002200202f7e4f9d9557aa3819fba995db85e76bf85bda1fb209d429121c3209394e379b475221029fbe1c2fe0d86e562a09aedaca23dabee01159d288ef3d6ea85ed107dd51db8a2103613ca8550a9ce1252ed80d39dce2e56f5befdedd70019e195bc0e1448e48b4d652ae7d0200000001c380aa11700db0a6d797dfd0be8aecfadad9397e4975f0fa8c9d10db71feac38000000000036a2ab8002400d030000000000160014761879f7b274ce995f87150a02e75cc0c037e8e3b8180c000000000022002010a0e90d648e62c6aa0d9b9883ee391449f1a1ce28926eb26c3bbdf76c25f631ac67892062de565629126afb02a76e67ddf18dfc883cf93bfeb7c807826dd6c8d1bd1f4d55528717f0d2303dc254b4512e32ce82dc7947d5d8e4d38647c2407fc53b874a00000000000000000000000000002710000000000bebc200000000002faf0800287a0627974d69a85199233f5cb8e86e7d04d8afdbc0faa4e56587833838c5af03293b6fcd6474a6c8dff63eeed1ca704c15d5bb80177025c2c45a4f995723393d000000ff021492cff600e90bf43cfa885be167e2596428e3ab962ecc1f86df9d0ab6d0440c00000000000002061a8000002a000000010037ca53d75c1340ff0189a8cb29e4b168fffd01aec5a72f183a9fc823c5f5e28c2803d2042c563f291aafa2fb5e97b3cad51014b50aafbad94e32c6326bf2865a4a3d5f2cd57c9eff6173b4328dae116ed5a04e389d6e2543f51f5cee25d2bfdf2273f278362f09935f4749433cf81ca1b8004a223d7196b8c8e241f8b723d7e47124557d977b0cb40fffef57d8123d0a633e24529f91b0bacabbf1008077f58571453ba25bf9b1bf01c3511e255f533eb10776e34a04390a4ad8539825346f9a122016c3b3cc8e176ed5235e27ec1fa7ff0b38997fc5d33308e2a870ff1feb1cad3271801da93aed362fe13ea9f33f543e2a372a5f4de2566bba858b95e3df8d7ba9c10647a6acea455598a9b84d21a876525de9000006226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f061a8000002a000002aaaa00ce2f18a967dc4f25f414e671ba6585f8ef0b8c5fb812c21064f55a2eaa02bbbb671d15145722fb8c28d732cddb249bcc6652ed2b297ff1f77a18371b1e63029fbe1c2fe0d86e562a09aedaca23dabee01159d288ef3d6ea85ed107dd51db8a03613ca8550a9ce1252ed80d39dce2e56f5befdedd70019e195bc0e1448e48b4d68861051293e9bb63a6e2182bce3dba766e0b0f69d546dcc059aee70e81fac8db1e56d2b10e4ecee3b99aaad8751fd396dbe9a6ba68d8c153dbc5e862afafd31daa06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f061a8000002a0000677415e20100009000000000000003e8000854d00000000a000000001dcd650000000001 \ No newline at end of file diff --git a/eclair-core/src/test/resources/nonreg/codecs/04000e-DATA_NORMAL/splicing-private/data.bin b/eclair-core/src/test/resources/nonreg/codecs/04000e-DATA_NORMAL/splicing-private/data.bin deleted file mode 100644 index 185f019b7b..0000000000 --- a/eclair-core/src/test/resources/nonreg/codecs/04000e-DATA_NORMAL/splicing-private/data.bin +++ /dev/null @@ -1 +0,0 @@ -04000e019c5655e99c1ef6d219eb093ca9f29e642167b4c776928df11c73e2293086b7490101041000100002bbbb671d15145722fb8c28d732cddb249bcc6652ed2b297ff1f77a18371b1e630009a15be736720f5de917bc0cf677c11e997e365fc71204be7b9f84db1964c606c68000000000000000000003e87fffffffffffffff00000000000003e80090001e0000038bff1253b7b8e40532508a53d31b95b295df31edf0d067d734479a680bd395de0000001408000000000000000000000000000008228a598202aaaa00ce2f18a967dc4f25f414e671ba6585f8ef0b8c5fb812c21064f55a2eaa000000000000044c7fffffffffffffff00000000000000000090006402fab0332aecd8006f4937b220ea073b0e814a21e36f4d21de9bb3e5165c3ffa02038bff1253b7b8e40532508a53d31b95b295df31edf0d067d734479a680bd395de023a5359a4e12c66e5b6a1847161f837695ab6e7591e67cb5b1da82706b491ea12032d7ab7162c56d1d273b09e67312833bcc8891c76b73fc6f69843b0f053e15b7c0000186b02000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000808220a59820000000000000000000000000000000000000000000000000000000000000000000300000002000000000000000003084360f5fd9a8a731db06c8060fa34ddc870170f715df692e65fbe0b5563cd58070002ff0300000000000000022405303d3f65cce13af3e5908692cefadae10b3f1205ac9c23eb381088f56c559100000000220020e9f2700aede07216deb9f641022813be41e24bcfb0d5e8efbc3f6b9ea345ec11fffffffd0000000029b92700000000004d7c6d0000000000000000000200000000000000042200204c2426cb19933167c0355daa79ea6197c8d398eeec5d07faa754d8ce333f3e5a0000000029b92700000000006b49d2000000000000000000000000010100000000000000002498c64318f88c4d85b1b9b2d92007d2added8f36dc03278d03956a7a73f9d33bf000000001f40ac270000000000160014b8a6877e8036939b79fcebe6c478d4bb234656b4000000000000000101000000000000000600000000001fe41a160014b8a6877e8036939b79fcebe6c478d4bb234656b400061a80869c5655e99c1ef6d219eb093ca9f29e642167b4c776928df11c73e2293086b74901310d33721f5dab9805cd15529a750a0e1d335e986f165306e9ac782b3883880000fd0259401ace8e75f9487f6e00dbb26c5ec9f420b8380e281464fbfe58d29e0a3dd4cdc148e35525f636c70ac2f0634cbda7e95f674e7f107e41730192024e9f7b994ac6f39c5655e99c1ef6d219eb093ca9f29e642167b4c776928df11c73e2293086b74901310d33721f5dab9805cd15529a750a0e1d335e986f165306e9ac782b3883880001006b0247304402206ed7fd184681abb89def87afc33ea27f2cbc5f6fd3457ead53d7540a7178820602207ffb13cbc41138468fab9ebcb698111fcb25dacb2395768415c27d05fb9de8550121038bff1253b7b8e40532508a53d31b95b295df31edf0d067d734479a680bd395defd025940412f04a65198e74a39557a3ab2da2e670b48a66fc72c3fd09995bf8267a978cc59789b066fc9b563b5be62c2b94c97fe9836231f65facb00acf218a5adea092eff0004004730440220412f04a65198e74a39557a3ab2da2e670b48a66fc72c3fd09995bf8267a978cc022059789b066fc9b563b5be62c2b94c97fe9836231f65facb00acf218a5adea092e0147304402201ace8e75f9487f6e00dbb26c5ec9f420b8380e281464fbfe58d29e0a3dd4cdc1022048e35525f636c70ac2f0634cbda7e95f674e7f107e41730192024e9f7b994ac601475221035f9474da22bcc3d52832f7722e0a00bfc904806c2906324c19c233c0d827df96210376f97a39e29a8868982625e60950ec5575f84cee4357994e0c47fd44140e2b3952ae00061a809c5655e99c1ef6d219eb093ca9f29e642167b4c776928df11c73e2293086b749000000000000000000000000000007a120ff00012405303d3f65cce13af3e5908692cefadae10b3f1205ac9c23eb381088f56c5591000000002b80841e0000000000220020e9f2700aede07216deb9f641022813be41e24bcfb0d5e8efbc3f6b9ea345ec11475221035f9474da22bcc3d52832f7722e0a00bfc904806c2906324c19c233c0d827df96210376f97a39e29a8868982625e60950ec5575f84cee4357994e0c47fd44140e2b3952ae00000001035f9474da22bcc3d52832f7722e0a00bfc904806c2906324c19c233c0d827df9603084360f5fd9a8a731db06c8060fa34ddc870170f715df692e65fbe0b5563cd58000000061a80000000000000044c000027100000000100000000000000000000000027100000000029b92700000000006b49d2002401310d33721f5dab9805cd15529a750a0e1d335e986f165306e9ac782b388388000000002ba0252600000000002200204c2426cb19933167c0355daa79ea6197c8d398eeec5d07faa754d8ce333f3e5a47522102c3a1c666d7ec45a951b7b171aef56f1ff7b97e35cb45cd29be70b5d9c36a18762103084360f5fd9a8a731db06c8060fa34ddc870170f715df692e65fbe0b5563cd5852ae7d020000000101310d33721f5dab9805cd15529a750a0e1d335e986f165306e9ac782b3883880000000000310038800260ae0a0000000000220020a1ca24b5ac3c96e066fa7466d34505e7e62bfe75017116d988328f97f4358160f85a1b0000000000160014b8a6877e8036939b79fcebe6c478d4bb234656b4a0f8562052ea4ba9421f4b4ad8a9a1e3041a940b13751504faf7d79338d73bbc1187fb551d9340de809849d6d7dcd745b813e01c07809cb8eacc57a211990b0c021d4c4500000000000000000000000000002710000000006b49d2000000000029b92700ac57fb7fa3b5493412ad83d7d056f269f54133e69026306d34a9ede545ef145c0362028d59fc5fd2598f1fad1df3933b64367fd6a16ba88a90d31d396b588eac2a00000000010000000000000000035f9474da22bcc3d52832f7722e0a00bfc904806c2906324c19c233c0d827df9609fd01ee0200000000010261e4d19b417c9bd9dfd56c23def17f2a9a0785c595bd1f181be2e84a7936d56b0000000000000000005f212d2dd5363ebcf4ff99b4e490fef6436b983182d65fd3129d9a7ccf9812420200000000fdffffff0280841e0000000000220020e9f2700aede07216deb9f641022813be41e24bcfb0d5e8efbc3f6b9ea345ec11fa42180000000000160014b8a6877e8036939b79fcebe6c478d4bb234656b40247304402206fee3390250c13cafe49ac68785603e8e7594eb8164286c7fa9540b03472e6a202207db90ec355e56b21c3595b909cbac668a38e1f8968920664bb9402b8bf51c66c0121038bff1253b7b8e40532508a53d31b95b295df31edf0d067d734479a680bd395de040048304502210093eb389e5a9980159a59ff72b614ed3ee0ea7b0e87f914813be91247c45df4e50220355ec43f1a73d290e6a55a0c4b589fa54f361fef754e13fb889a77e10e463a5f0147304402207a816162df6c49beb46b7ad77a86fc0c13f086b9030bbf8b61d8f439895a20fa02202e9d006e343d736314dfb8d986274081b6b8f1dfde44ad2396ee9336dd3ac0290147522103b59ad43139d2731d309ed31a6cdb22fb3420b0772aed954c52fd7738ffceb5552103d2c4e54adf97874aa2e311d1297614d2c1ff5a4c4a18c693018f07b2c449840752ae801a0600ff869c5655e99c1ef6d219eb093ca9f29e642167b4c776928df11c73e2293086b74905303d3f65cce13af3e5908692cefadae10b3f1205ac9c23eb381088f56c55910000fd02594093eb389e5a9980159a59ff72b614ed3ee0ea7b0e87f914813be91247c45df4e5355ec43f1a73d290e6a55a0c4b589fa54f361fef754e13fb889a77e10e463a5f000100000000000000000000000027100000000029b92700000000004d7c6d002405303d3f65cce13af3e5908692cefadae10b3f1205ac9c23eb381088f56c5591000000002b80841e0000000000220020e9f2700aede07216deb9f641022813be41e24bcfb0d5e8efbc3f6b9ea345ec11475221035f9474da22bcc3d52832f7722e0a00bfc904806c2906324c19c233c0d827df96210376f97a39e29a8868982625e60950ec5575f84cee4357994e0c47fd44140e2b3952ae7d020000000105303d3f65cce13af3e5908692cefadae10b3f1205ac9c23eb381088f56c55910000000000310038800260ae0a0000000000220020a1ca24b5ac3c96e066fa7466d34505e7e62bfe75017116d988328f97f4358160d8b9130000000000160014b8a6877e8036939b79fcebe6c478d4bb234656b4a0f8562032a246840dd67eb72b59fc170303c508c77ee8666ac6be36abced02bf79dbfbe114cdd4e56600ae6e3573e6e10d3517ddc857fbd89c00e2cbcdf5186c6592f2600000000000000000000000000002710000000004d7c6d000000000029b927001f1392e4cee2d3fc5f4fed4afed2b962ea8df1a802dc9274043f337e8767838e0362028d59fc5fd2598f1fad1df3933b64367fd6a16ba88a90d31d396b588eac2a0000000000000000000000000003d2c4e54adf97874aa2e311d1297614d2c1ff5a4c4a18c693018f07b2c449840709fd019f02000000000102858cd8aefb21344c1de13837213f24fe6f7ef16ccd245b6f791c5f3972433c48000000000000000000f7f92c73570683d9b37a0a708ccd508d2a4316c67b3a7fba5dcaa2120ad78cb200000000000000000003c26e010000000000160014b8a6877e8036939b79fcebe6c478d4bb234656b40a77010000000000160014b8a6877e8036939b79fcebe6c478d4bb234656b460e3160000000000220020f500ba71f33a6674177518acf627cd1f4a192f6fd6251c66117b8ca09a902a1302483045022100ff54d5e967cb70ee61b958c4f3477395901f4ba07afe0bf7784c060370ffa12c02204dd7f2014a53138805f86e7673878b1cc5e9b051190917a8342d910ff29b30730121038bff1253b7b8e40532508a53d31b95b295df31edf0d067d734479a680bd395de02483045022100b5c1d0b2a599ef06b678af4234f2f8220ab6532d6312944a57eefafcc8ae7dcf0220739feb9f64e83d8aaed92e3ccd57ec5c4dcb25c23435742694ed179cf5f091260121038bff1253b7b8e40532508a53d31b95b295df31edf0d067d734479a680bd395de801a0600ffb09c5655e99c1ef6d219eb093ca9f29e642167b4c776928df11c73e2293086b7495f212d2dd5363ebcf4ff99b4e490fef6436b983182d65fd3129d9a7ccf9812420001006c02483045022100b5c1d0b2a599ef06b678af4234f2f8220ab6532d6312944a57eefafcc8ae7dcf0220739feb9f64e83d8aaed92e3ccd57ec5c4dcb25c23435742694ed179cf5f091260121038bff1253b7b8e40532508a53d31b95b295df31edf0d067d734479a680bd395de000100000000000000000000000027100000000029b92700000000002faf0800245f212d2dd5363ebcf4ff99b4e490fef6436b983182d65fd3129d9a7ccf981242020000002b60e3160000000000220020f500ba71f33a6674177518acf627cd1f4a192f6fd6251c66117b8ca09a902a1347522103b59ad43139d2731d309ed31a6cdb22fb3420b0772aed954c52fd7738ffceb5552103d2c4e54adf97874aa2e311d1297614d2c1ff5a4c4a18c693018f07b2c449840752ae7d02000000015f212d2dd5363ebcf4ff99b4e490fef6436b983182d65fd3129d9a7ccf9812420200000000310038800260ae0a0000000000220020a1ca24b5ac3c96e066fa7466d34505e7e62bfe75017116d988328f97f4358160b8180c0000000000160014b8a6877e8036939b79fcebe6c478d4bb234656b4a0f8562047f525be7304ef4fa4d868eccfa2389fe1d8f058d08305b0f1792cebbd14ec7b6b43f98d0f3aa22e60d7ef024785f3576d0c1d74b666c7c5d2da88c7e7f7bd1800000000000000000000000000002710000000002faf08000000000029b92700ea2430e6f9d234b9ca79d5edbdd0edbe429aa31026ad79ecc207299a62151d330362028d59fc5fd2598f1fad1df3933b64367fd6a16ba88a90d31d396b588eac2a000000ff02c8a6e5a10f60d65a8d9aec86137898f4293bf8692917fd59a676efdc7956df2700000000000002061a8000002a0002000101e9544574043065ff010644a8d60b0c1100881f06e865a73a5f7ca2cd4f4df80110ffb72623fab4a3b3edaab2cbd14886f94f1e8d7c1586a668a5e0c7acb17a6fdf9539b63be05a2a650eca95f79d9af23ee106226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f01e95445740430656774180a030100900000000000000000000858b8000000147fffffffffffffff00000001 \ No newline at end of file diff --git a/eclair-core/src/test/resources/nonreg/codecs/04000e-DATA_NORMAL/splicing-private/data.json b/eclair-core/src/test/resources/nonreg/codecs/04000e-DATA_NORMAL/splicing-private/data.json deleted file mode 100644 index 29fb6d0eb3..0000000000 --- a/eclair-core/src/test/resources/nonreg/codecs/04000e-DATA_NORMAL/splicing-private/data.json +++ /dev/null @@ -1,244 +0,0 @@ -{ - "type" : "DATA_NORMAL", - "commitments" : { - "params" : { - "channelId" : "9c5655e99c1ef6d219eb093ca9f29e642167b4c776928df11c73e2293086b749", - "channelConfig" : [ "funding_pubkey_based_channel_keypath" ], - "channelFeatures" : [ "option_static_remotekey", "option_dual_fund" ], - "localParams" : { - "nodeId" : "02bbbb671d15145722fb8c28d732cddb249bcc6652ed2b297ff1f77a18371b1e63", - "fundingKeyPath" : [ 2707154742, 1913609705, 398200054, 2009144985, 2117492679, 302300795, 2676284185, 1690699462, 2147483648 ], - "dustLimit" : 1000, - "maxHtlcValueInFlightMsat" : 9223372036854775807, - "htlcMinimum" : 1000, - "toSelfDelay" : 144, - "maxAcceptedHtlcs" : 30, - "isChannelOpener" : false, - "paysCommitTxFees" : false, - "walletStaticPaymentBasepoint" : "038bff1253b7b8e40532508a53d31b95b295df31edf0d067d734479a680bd395de", - "initFeatures" : { - "activated" : { - "option_route_blinding" : "optional", - "option_dual_fund" : "optional", - "splice_prototype" : "optional", - "payment_secret" : "mandatory", - "gossip_queries_ex" : "optional", - "option_quiesce" : "optional", - "option_data_loss_protect" : "optional", - "var_onion_optin" : "mandatory", - "option_static_remotekey" : "mandatory", - "option_support_large_channel" : "optional", - "option_anchors_zero_fee_htlc_tx" : "optional", - "basic_mpp" : "optional", - "gossip_queries" : "optional" - }, - "unknown" : [ ] - } - }, - "remoteParams" : { - "nodeId" : "02aaaa00ce2f18a967dc4f25f414e671ba6585f8ef0b8c5fb812c21064f55a2eaa", - "dustLimit" : 1100, - "maxHtlcValueInFlightMsat" : 9223372036854775807, - "htlcMinimum" : 0, - "toSelfDelay" : 144, - "maxAcceptedHtlcs" : 100, - "revocationBasepoint" : "02fab0332aecd8006f4937b220ea073b0e814a21e36f4d21de9bb3e5165c3ffa02", - "paymentBasepoint" : "038bff1253b7b8e40532508a53d31b95b295df31edf0d067d734479a680bd395de", - "delayedPaymentBasepoint" : "023a5359a4e12c66e5b6a1847161f837695ab6e7591e67cb5b1da82706b491ea12", - "htlcBasepoint" : "032d7ab7162c56d1d273b09e67312833bcc8891c76b73fc6f69843b0f053e15b7c", - "initFeatures" : { - "activated" : { - "option_support_large_channel" : "optional", - "option_route_blinding" : "optional", - "option_provide_storage" : "optional", - "option_dual_fund" : "optional", - "splice_prototype" : "optional", - "payment_secret" : "mandatory", - "gossip_queries_ex" : "optional", - "option_quiesce" : "optional", - "option_data_loss_protect" : "optional", - "var_onion_optin" : "mandatory", - "option_static_remotekey" : "mandatory", - "basic_mpp" : "optional", - "gossip_queries" : "optional" - }, - "unknown" : [ 50001 ] - } - }, - "channelFlags" : { - "nonInitiatorPaysCommitFees" : false, - "announceChannel" : false - } - }, - "changes" : { - "localChanges" : { - "proposed" : [ ], - "signed" : [ ], - "acked" : [ ] - }, - "remoteChanges" : { - "proposed" : [ ], - "acked" : [ ], - "signed" : [ ] - }, - "localNextHtlcId" : 0, - "remoteNextHtlcId" : 0 - }, - "active" : [ { - "fundingTxIndex" : 2, - "fundingTx" : { - "outPoint" : "8883382b78ace90653166f985e331d0e0a759a5215cd0598ab5d1f72330d3101:0", - "amountSatoshis" : 2500000 - }, - "localFunding" : { - "status" : "unconfirmed", - "txid" : "8883382b78ace90653166f985e331d0e0a759a5215cd0598ab5d1f72330d3101" - }, - "remoteFunding" : { - "status" : "not-locked" - }, - "localCommit" : { - "index" : 0, - "spec" : { - "htlcs" : [ ], - "commitTxFeerate" : 10000, - "toLocal" : 700000000, - "toRemote" : 1800000000 - }, - "commitTxAndRemoteSig" : { - "commitTx" : { - "txid" : "ec1fdbdffd12f20851f33319fb3ab6ecb8528a34a9a2c76e53aaf91cf4a18da4", - "tx" : "020000000101310d33721f5dab9805cd15529a750a0e1d335e986f165306e9ac782b3883880000000000310038800260ae0a0000000000220020a1ca24b5ac3c96e066fa7466d34505e7e62bfe75017116d988328f97f4358160f85a1b0000000000160014b8a6877e8036939b79fcebe6c478d4bb234656b4a0f85620" - }, - "remoteSig" : "52ea4ba9421f4b4ad8a9a1e3041a940b13751504faf7d79338d73bbc1187fb551d9340de809849d6d7dcd745b813e01c07809cb8eacc57a211990b0c021d4c45" - }, - "htlcTxsAndRemoteSigs" : [ ] - }, - "remoteCommit" : { - "index" : 0, - "spec" : { - "htlcs" : [ ], - "commitTxFeerate" : 10000, - "toLocal" : 1800000000, - "toRemote" : 700000000 - }, - "txid" : "ac57fb7fa3b5493412ad83d7d056f269f54133e69026306d34a9ede545ef145c", - "remotePerCommitmentPoint" : "0362028d59fc5fd2598f1fad1df3933b64367fd6a16ba88a90d31d396b588eac2a" - } - }, { - "fundingTxIndex" : 1, - "fundingTx" : { - "outPoint" : "91556cf5881038eb239cac05123f0be1daface928690e5f33ae1cc653f3d3005:0", - "amountSatoshis" : 2000000 - }, - "localFunding" : { - "status" : "confirmed", - "txid" : "91556cf5881038eb239cac05123f0be1daface928690e5f33ae1cc653f3d3005", - "shortChannelId" : "0x0x0" - }, - "remoteFunding" : { - "status" : "not-locked" - }, - "localCommit" : { - "index" : 0, - "spec" : { - "htlcs" : [ ], - "commitTxFeerate" : 10000, - "toLocal" : 700000000, - "toRemote" : 1300000000 - }, - "commitTxAndRemoteSig" : { - "commitTx" : { - "txid" : "27f7218158a68f5bb23c9d8a280f85bfca32d7636529e1db2ba23172241161f4", - "tx" : "020000000105303d3f65cce13af3e5908692cefadae10b3f1205ac9c23eb381088f56c55910000000000310038800260ae0a0000000000220020a1ca24b5ac3c96e066fa7466d34505e7e62bfe75017116d988328f97f4358160d8b9130000000000160014b8a6877e8036939b79fcebe6c478d4bb234656b4a0f85620" - }, - "remoteSig" : "32a246840dd67eb72b59fc170303c508c77ee8666ac6be36abced02bf79dbfbe114cdd4e56600ae6e3573e6e10d3517ddc857fbd89c00e2cbcdf5186c6592f26" - }, - "htlcTxsAndRemoteSigs" : [ ] - }, - "remoteCommit" : { - "index" : 0, - "spec" : { - "htlcs" : [ ], - "commitTxFeerate" : 10000, - "toLocal" : 1300000000, - "toRemote" : 700000000 - }, - "txid" : "1f1392e4cee2d3fc5f4fed4afed2b962ea8df1a802dc9274043f337e8767838e", - "remotePerCommitmentPoint" : "0362028d59fc5fd2598f1fad1df3933b64367fd6a16ba88a90d31d396b588eac2a" - } - }, { - "fundingTxIndex" : 0, - "fundingTx" : { - "outPoint" : "421298cf7c9a9d12d35fd68231986b43f6fe90e4b499fff4bc3e36d52d2d215f:2", - "amountSatoshis" : 1500000 - }, - "localFunding" : { - "status" : "confirmed", - "txid" : "421298cf7c9a9d12d35fd68231986b43f6fe90e4b499fff4bc3e36d52d2d215f", - "shortChannelId" : "400000x42x2" - }, - "remoteFunding" : { - "status" : "not-locked" - }, - "localCommit" : { - "index" : 0, - "spec" : { - "htlcs" : [ ], - "commitTxFeerate" : 10000, - "toLocal" : 700000000, - "toRemote" : 800000000 - }, - "commitTxAndRemoteSig" : { - "commitTx" : { - "txid" : "381ee5a54f127b776114b197b2695c39f2d9b81cb967628a32e12766cdf8f1b5", - "tx" : "02000000015f212d2dd5363ebcf4ff99b4e490fef6436b983182d65fd3129d9a7ccf9812420200000000310038800260ae0a0000000000220020a1ca24b5ac3c96e066fa7466d34505e7e62bfe75017116d988328f97f4358160b8180c0000000000160014b8a6877e8036939b79fcebe6c478d4bb234656b4a0f85620" - }, - "remoteSig" : "47f525be7304ef4fa4d868eccfa2389fe1d8f058d08305b0f1792cebbd14ec7b6b43f98d0f3aa22e60d7ef024785f3576d0c1d74b666c7c5d2da88c7e7f7bd18" - }, - "htlcTxsAndRemoteSigs" : [ ] - }, - "remoteCommit" : { - "index" : 0, - "spec" : { - "htlcs" : [ ], - "commitTxFeerate" : 10000, - "toLocal" : 800000000, - "toRemote" : 700000000 - }, - "txid" : "ea2430e6f9d234b9ca79d5edbdd0edbe429aa31026ad79ecc207299a62151d33", - "remotePerCommitmentPoint" : "0362028d59fc5fd2598f1fad1df3933b64367fd6a16ba88a90d31d396b588eac2a" - } - } ], - "inactive" : [ ], - "remoteNextCommitInfo" : "02c8a6e5a10f60d65a8d9aec86137898f4293bf8692917fd59a676efdc7956df27", - "remotePerCommitmentSecrets" : null, - "originChannels" : { } - }, - "aliases" : { - "localAlias" : "0x1e9544574043065", - "remoteAlias" : "0x10644a8d60b0c11" - }, - "channelUpdate" : { - "signature" : "1f06e865a73a5f7ca2cd4f4df80110ffb72623fab4a3b3edaab2cbd14886f94f1e8d7c1586a668a5e0c7acb17a6fdf9539b63be05a2a650eca95f79d9af23ee1", - "chainHash" : "06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f", - "shortChannelId" : "125268x4551684x12389", - "timestamp" : { - "iso" : "2024-12-31T16:12:58Z", - "unix" : 1735661578 - }, - "messageFlags" : { - "dontForward" : true - }, - "channelFlags" : { - "isEnabled" : true, - "isNode1" : false - }, - "cltvExpiryDelta" : 144, - "htlcMinimumMsat" : 0, - "feeBaseMsat" : 547000, - "feeProportionalMillionths" : 20, - "htlcMaximumMsat" : 9223372036854775807, - "tlvStream" : { } - } -} \ No newline at end of file diff --git a/eclair-core/src/test/resources/nonreg/codecs/050001-DATA_WAIT_FOR_FUNDING_CONFIRMED/fundee/data.bin b/eclair-core/src/test/resources/nonreg/codecs/050001-DATA_WAIT_FOR_FUNDING_CONFIRMED/fundee/data.bin new file mode 100644 index 0000000000..10f818304b --- /dev/null +++ b/eclair-core/src/test/resources/nonreg/codecs/050001-DATA_WAIT_FOR_FUNDING_CONFIRMED/fundee/data.bin @@ -0,0 +1 @@ +0500010154e64a242e3ddebf0ae861177c9527cab2590753ff06e083447058c14e52df1601010002bbbb671d15145722fb8c28d732cddb249bcc6652ed2b297ff1f77a18371b1e63000964b73db83d0bc558887b321edd69c878af1d5c7011f7225cda4b5d4d222ee35780000000ff0000000000004e20000000000000140800000000000000000000000000100802aa698202aaaa00ce2f18a967dc4f25f414e671ba6585f8ef0b8c5fb812c21064f55a2eaaff000000000000271003b128cb7add2eea5c2e72e6ba86eaf850a0616ae3980353c6ed96cb18004839a8020e2002e56b8744f03c2c70ef8d943cb2757475dfcb03010966f61050c10e1d39034bfec4fcacd91d43a3142c490b76b9e76923edc270942f757a5db2ce8e0b9f5102eb3fb4c7dc3f500600ef9ae770a07215a8abc159a183923c261a63a4903e0edc0000186b0200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000180802aa6982000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000002454e64a242e3ddebf0ae861177c9527cab2590753ff06e083447058c14e52df160000000000000000000f42400215e3a3ee05e852e9e48fabd610a9a0b1e59c8b0412f4629b4da00dbd002d51220100010200000000000003e800000000000003e8000000003b9aca00001e009000000000000000000000000009c4000000000bebc200000000002faf0800b10e91e8391c08080c344403bc16d96d6d812b928c6269eb6492fe101fcc0241012510bfd05abddacfe165981318528c0ef7a5ab69f2a9146068550ebbc629eb906ec81eb888aac11575f8a58f522d79f5b36c85ecbbe9ec6a9379c5f257370e760000000000000000044c0000000000000000000000001dcd6500006402d000000000000000000000000009c4000000002faf0800000000000bebc200a52fbdd06acaf7dc29ad4ef1bcbf0ff99f7e27a89b5bf55d86b857937b5cd1280276fa864098136818422eda69b26a498ac3e534bc5673b6ceaa0acc72706d7b8c000000ff023cdb83eaac5d236885bbbbe6a366b077ea6cf51a099aafc57b3b07201821531c00000000000000061a8000ff6054e64a242e3ddebf0ae861177c9527cab2590753ff06e083447058c14e52df1631b9111f33d0905f65b7b2f8fc879af706334c78c0e38b981717ef28674cade56b0cd9af2269e139a087cb1ffbca9db5366f69b9aff2b801df0b1ea1017d32b7 \ No newline at end of file diff --git a/eclair-core/src/test/resources/nonreg/codecs/04000b-DATA_WAIT_FOR_CHANNEL_READY/fundee/data.json b/eclair-core/src/test/resources/nonreg/codecs/050001-DATA_WAIT_FOR_FUNDING_CONFIRMED/fundee/data.json similarity index 55% rename from eclair-core/src/test/resources/nonreg/codecs/04000b-DATA_WAIT_FOR_CHANNEL_READY/fundee/data.json rename to eclair-core/src/test/resources/nonreg/codecs/050001-DATA_WAIT_FOR_FUNDING_CONFIRMED/fundee/data.json index 7e8e183d49..5aa1929ddc 100644 --- a/eclair-core/src/test/resources/nonreg/codecs/04000b-DATA_WAIT_FOR_CHANNEL_READY/fundee/data.json +++ b/eclair-core/src/test/resources/nonreg/codecs/050001-DATA_WAIT_FOR_FUNDING_CONFIRMED/fundee/data.json @@ -1,34 +1,30 @@ { - "type" : "DATA_WAIT_FOR_CHANNEL_READY", + "type" : "DATA_WAIT_FOR_FUNDING_CONFIRMED", "commitments" : { - "params" : { - "channelId" : "7d975ecb75e1497076150f745b83dace95a189eecb6172c20a9a4fe0f91b8d1d", + "channelParams" : { + "channelId" : "54e64a242e3ddebf0ae861177c9527cab2590753ff06e083447058c14e52df16", "channelConfig" : [ "funding_pubkey_based_channel_keypath" ], - "channelFeatures" : [ "option_static_remotekey" ], + "channelFeatures" : [ ], "localParams" : { "nodeId" : "02bbbb671d15145722fb8c28d732cddb249bcc6652ed2b297ff1f77a18371b1e63", - "fundingKeyPath" : [ 3266606892, 3454308995, 379409286, 2058386039, 150235166, 1337553882, 292124276, 1286028724, 2147483648 ], - "dustLimit" : 1000, - "maxHtlcValueInFlightMsat" : 1000000000, + "fundingKeyPath" : [ 1689730488, 1024181592, 2289775134, 3714697336, 2937937008, 301408860, 3662372173, 573498199, 2147483648 ], "initialRequestedChannelReserve_opt" : 20000, - "htlcMinimum" : 1000, - "toSelfDelay" : 144, - "maxAcceptedHtlcs" : 30, "isChannelOpener" : false, "paysCommitTxFees" : false, - "walletStaticPaymentBasepoint" : "028feba10d0eafd0fad8fe20e6d9206e6bd30242826de05c63f459a00aced24b12", "initFeatures" : { "activated" : { "option_route_blinding" : "optional", "splice_prototype" : "optional", "payment_secret" : "mandatory", "gossip_queries_ex" : "optional", + "option_anchor_outputs" : "optional", "option_quiesce" : "optional", "option_data_loss_protect" : "optional", "var_onion_optin" : "mandatory", - "option_static_remotekey" : "mandatory", + "option_static_remotekey" : "optional", "option_support_large_channel" : "optional", "option_anchors_zero_fee_htlc_tx" : "optional", + "option_channel_type" : "mandatory", "basic_mpp" : "optional", "gossip_queries" : "optional" }, @@ -37,28 +33,26 @@ }, "remoteParams" : { "nodeId" : "02aaaa00ce2f18a967dc4f25f414e671ba6585f8ef0b8c5fb812c21064f55a2eaa", - "dustLimit" : 1100, - "maxHtlcValueInFlightMsat" : 500000000, "initialRequestedChannelReserve_opt" : 10000, - "htlcMinimum" : 0, - "toSelfDelay" : 144, - "maxAcceptedHtlcs" : 100, - "revocationBasepoint" : "02e1a7010650cd5cd6fbf7505f5f213b36e5cc0d127064c74619c83dfa7434ce25", - "paymentBasepoint" : "028feba10d0eafd0fad8fe20e6d9206e6bd30242826de05c63f459a00aced24b12", - "delayedPaymentBasepoint" : "031723f6ffc8e552694f09d067844851f14357fb2a294fd94fd0b596ee808d2a17", - "htlcBasepoint" : "031c3b7172c024396dd7931cd835d50e8eaef23c80e5b47a6c35fcf5c0ea89c6ab", + "revocationBasepoint" : "03b128cb7add2eea5c2e72e6ba86eaf850a0616ae3980353c6ed96cb18004839a8", + "paymentBasepoint" : "020e2002e56b8744f03c2c70ef8d943cb2757475dfcb03010966f61050c10e1d39", + "delayedPaymentBasepoint" : "034bfec4fcacd91d43a3142c490b76b9e76923edc270942f757a5db2ce8e0b9f51", + "htlcBasepoint" : "02eb3fb4c7dc3f500600ef9ae770a07215a8abc159a183923c261a63a4903e0edc", "initFeatures" : { "activated" : { - "option_support_large_channel" : "optional", "option_route_blinding" : "optional", "option_provide_storage" : "optional", "splice_prototype" : "optional", "payment_secret" : "mandatory", "gossip_queries_ex" : "optional", + "option_anchor_outputs" : "optional", "option_quiesce" : "optional", "option_data_loss_protect" : "optional", "var_onion_optin" : "mandatory", - "option_static_remotekey" : "mandatory", + "option_static_remotekey" : "optional", + "option_support_large_channel" : "optional", + "option_anchors_zero_fee_htlc_tx" : "optional", + "option_channel_type" : "mandatory", "basic_mpp" : "optional", "gossip_queries" : "optional" }, @@ -86,53 +80,64 @@ }, "active" : [ { "fundingTxIndex" : 0, - "fundingTx" : { - "outPoint" : "1d8d1bf9e04f9a0ac27261cbee89a195ceda835b740f15767049e175cb5e977d:0", - "amountSatoshis" : 1000000 - }, + "fundingInput" : "16df524ec158704483e006ff530759b2ca27957c1761e80abfde3d2e244ae654:0", + "fundingAmount" : 1000000, "localFunding" : { - "status" : "confirmed", - "txid" : "1d8d1bf9e04f9a0ac27261cbee89a195ceda835b740f15767049e175cb5e977d", - "shortChannelId" : "400000x42x0" + "status" : "unconfirmed" }, "remoteFunding" : { "status" : "not-locked" }, + "commitmentFormat" : "anchor_outputs", + "localCommitParams" : { + "dustLimit" : 1000, + "htlcMinimum" : 1000, + "maxHtlcValueInFlight" : 1000000000, + "maxAcceptedHtlcs" : 30, + "toSelfDelay" : 144 + }, "localCommit" : { "index" : 0, "spec" : { "htlcs" : [ ], - "commitTxFeerate" : 10000, + "commitTxFeerate" : 2500, "toLocal" : 200000000, "toRemote" : 800000000 }, - "commitTxAndRemoteSig" : { - "commitTx" : { - "txid" : "d873d760728d9cf12d61c43360c383ab83799c9153f38c6012d9d6f6e6026df1", - "tx" : "02000000017d975ecb75e1497076150f745b83dace95a189eecb6172c20a9a4fe0f91b8d1d000000000036a2ab8002400d0300000000002200207d4136c71e1c702d6e56ea300e1a56298b70fc8e96d67e72cfab3a27d39c9bf2b8180c0000000000160014761879f7b274ce995f87150a02e75cc0c037e8e3ac678920" - }, - "remoteSig" : "02b8ce0a760ae6052a0d6c5770ae626ae71ea1b64853f97bb2c784839264054c64dc8717789f804342cbd9c5a364b32f4cbd9e9a4f91f48127a58f95014f6663" + "txId" : "b10e91e8391c08080c344403bc16d96d6d812b928c6269eb6492fe101fcc0241", + "remoteSig" : { + "sig" : "2510bfd05abddacfe165981318528c0ef7a5ab69f2a9146068550ebbc629eb906ec81eb888aac11575f8a58f522d79f5b36c85ecbbe9ec6a9379c5f257370e76" }, - "htlcTxsAndRemoteSigs" : [ ] + "htlcRemoteSigs" : [ ] + }, + "remoteCommitParams" : { + "dustLimit" : 1100, + "htlcMinimum" : 0, + "maxHtlcValueInFlight" : 500000000, + "maxAcceptedHtlcs" : 100, + "toSelfDelay" : 720 }, "remoteCommit" : { "index" : 0, "spec" : { "htlcs" : [ ], - "commitTxFeerate" : 10000, + "commitTxFeerate" : 2500, "toLocal" : 800000000, "toRemote" : 200000000 }, - "txid" : "ad28d946d13d331539d08da9d131311d1bec7308ddf072a59b07b2dc22779c3f", - "remotePerCommitmentPoint" : "02e813fa8f4480fcbfa92d65037fe2d5d99cb72d6987381cf253d13f071a45f049" + "txId" : "a52fbdd06acaf7dc29ad4ef1bcbf0ff99f7e27a89b5bf55d86b857937b5cd128", + "remotePerCommitmentPoint" : "0276fa864098136818422eda69b26a498ac3e534bc5673b6ceaa0acc72706d7b8c" } } ], "inactive" : [ ], - "remoteNextCommitInfo" : "02305a6b82bd468d5eb1d80d36117119d1144f0f7439b88e64c8e164f43d9ea969", + "remoteNextCommitInfo" : "023cdb83eaac5d236885bbbbe6a366b077ea6cf51a099aafc57b3b07201821531c", "remotePerCommitmentSecrets" : null, "originChannels" : { } }, - "aliases" : { - "localAlias" : "0x2a7224ea3627762" + "waitingSince" : 400000, + "lastSent" : { + "channelId" : "54e64a242e3ddebf0ae861177c9527cab2590753ff06e083447058c14e52df16", + "signature" : "31b9111f33d0905f65b7b2f8fc879af706334c78c0e38b981717ef28674cade56b0cd9af2269e139a087cb1ffbca9db5366f69b9aff2b801df0b1ea1017d32b7", + "tlvStream" : { } } } \ No newline at end of file diff --git a/eclair-core/src/test/resources/nonreg/codecs/04000b-DATA_WAIT_FOR_CHANNEL_READY/fundee/data.bin b/eclair-core/src/test/resources/nonreg/codecs/050001-DATA_WAIT_FOR_FUNDING_CONFIRMED/funder/data.bin similarity index 84% rename from eclair-core/src/test/resources/nonreg/codecs/04000b-DATA_WAIT_FOR_CHANNEL_READY/fundee/data.bin rename to eclair-core/src/test/resources/nonreg/codecs/050001-DATA_WAIT_FOR_FUNDING_CONFIRMED/funder/data.bin index e6eb0b9b6e..32a642f773 100644 --- a/eclair-core/src/test/resources/nonreg/codecs/04000b-DATA_WAIT_FOR_CHANNEL_READY/fundee/data.bin +++ b/eclair-core/src/test/resources/nonreg/codecs/050001-DATA_WAIT_FOR_FUNDING_CONFIRMED/funder/data.bin @@ -1 +1 @@ -04000b017d975ecb75e1497076150f745b83dace95a189eecb6172c20a9a4fe0f91b8d1d010102100002bbbb671d15145722fb8c28d732cddb249bcc6652ed2b297ff1f77a18371b1e630009c2b4772ccde49283169d53867ab07a7708f4681e4fb973da116976744ca73db48000000000000000000003e8000000003b9aca000000000000004e2000000000000003e80090001e0000028feba10d0eafd0fad8fe20e6d9206e6bd30242826de05c63f459a00aced24b120000001408000000000000000000000000000008028a598202aaaa00ce2f18a967dc4f25f414e671ba6585f8ef0b8c5fb812c21064f55a2eaa000000000000044c000000001dcd6500000000000000271000000000000000000090006402e1a7010650cd5cd6fbf7505f5f213b36e5cc0d127064c74619c83dfa7434ce25028feba10d0eafd0fad8fe20e6d9206e6bd30242826de05c63f459a00aced24b12031723f6ffc8e552694f09d067844851f14357fb2a294fd94fd0b596ee808d2a17031c3b7172c024396dd7931cd835d50e8eaef23c80e5b47a6c35fcf5c0ea89c6ab0000186b02000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000808020a5982000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000386cbbe2cb46c4ce728b18f4405b0d51f2d648b9edf2b79faf2134e5776b672b6095e020000000101010101010101010101010101010101010101010101010101010101010101012a00000000ffffffff0140420f0000000000220020002cc0186fb2a7959231475a7b15726531dbc3f0c52da973a8d3b41969c9a393000000000000010000000000000000000000002710000000000bebc200000000002faf0800247d975ecb75e1497076150f745b83dace95a189eecb6172c20a9a4fe0f91b8d1d000000002b40420f0000000000220020002cc0186fb2a7959231475a7b15726531dbc3f0c52da973a8d3b41969c9a393475221032862b5ef046f63319d2acc6750db140ff9a12122add71a136f6601eb6e26b0c9210386cbbe2cb46c4ce728b18f4405b0d51f2d648b9edf2b79faf2134e5776b672b652ae7d02000000017d975ecb75e1497076150f745b83dace95a189eecb6172c20a9a4fe0f91b8d1d000000000036a2ab8002400d0300000000002200207d4136c71e1c702d6e56ea300e1a56298b70fc8e96d67e72cfab3a27d39c9bf2b8180c0000000000160014761879f7b274ce995f87150a02e75cc0c037e8e3ac67892002b8ce0a760ae6052a0d6c5770ae626ae71ea1b64853f97bb2c784839264054c64dc8717789f804342cbd9c5a364b32f4cbd9e9a4f91f48127a58f95014f666300000000000000000000000000002710000000002faf0800000000000bebc200ad28d946d13d331539d08da9d131311d1bec7308ddf072a59b07b2dc22779c3f02e813fa8f4480fcbfa92d65037fe2d5d99cb72d6987381cf253d13f071a45f049000000ff02305a6b82bd468d5eb1d80d36117119d1144f0f7439b88e64c8e164f43d9ea96900000000000001061a8000002a0000000102a7224ea362776200 \ No newline at end of file +0500010154e64a242e3ddebf0ae861177c9527cab2590753ff06e083447058c14e52df1601010002aaaa00ce2f18a967dc4f25f414e671ba6585f8ef0b8c5fb812c21064f55a2eaa00094f800d5346eb068ababf1bb8f7a0bf72289de08f0b3959e5297679249b64651380000001ff0000000000002710c000000000186b0200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000180802aa698202bbbb671d15145722fb8c28d732cddb249bcc6652ed2b297ff1f77a18371b1e63ff0000000000004e200219a94ed17ed28b7fb8a78805e841d42044bf49c330eab4ac2d051c5a1e51e68f020a814ad2030bef9d42eb6d29bafa4c22d17f1114f6c9b8651e91d848de4f6a7d032b01af2224e34163fb0b1fe4523cad273818dda1ed7189dfb27e407e70e99afd0219e66b4f4dec1ff6644b8ab1196834e1d401dc52bf99bda8b50df8c231e729f5000000140800000000000000000000000000100802aa6982000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000002454e64a242e3ddebf0ae861177c9527cab2590753ff06e083447058c14e52df160000000000000000000f4240025e0b518e680bc70ebca69f5b1a794f6c5ffa78246b03691bb99e9e136af2c24a01ff5e020000000101010101010101010101010101010101010101010101010101010101010101012a00000000ffffffff0140420f00000000002200200879d9983ee4063ea1701b5ea9c98e53be174ba964397592690b627926c3a642000000000102000000000000044c0000000000000000000000001dcd6500006402d000000000000000000000000009c4000000002faf0800000000000bebc200a52fbdd06acaf7dc29ad4ef1bcbf0ff99f7e27a89b5bf55d86b857937b5cd1280131b9111f33d0905f65b7b2f8fc879af706334c78c0e38b981717ef28674cade56b0cd9af2269e139a087cb1ffbca9db5366f69b9aff2b801df0b1ea1017d32b7000000000000000003e800000000000003e8000000003b9aca00001e009000000000000000000000000009c4000000000bebc200000000002faf0800b10e91e8391c08080c344403bc16d96d6d812b928c6269eb6492fe101fcc0241034f07167b088c401ce0092c7e441ca19b39fb14a003324616adcac78a2f44c9db000000ff038e89411e0d79c6ef7e693d93b3653cffc3fd7b5e055703bea9d3ef19becc820100000000000000061a80000082000000000000000000000000000000000000000000000000000000000000000054e64a242e3ddebf0ae861177c9527cab2590753ff06e083447058c14e52df1600002510bfd05abddacfe165981318528c0ef7a5ab69f2a9146068550ebbc629eb906ec81eb888aac11575f8a58f522d79f5b36c85ecbbe9ec6a9379c5f257370e76 \ No newline at end of file diff --git a/eclair-core/src/test/resources/nonreg/codecs/04000d-DATA_WAIT_FOR_DUAL_FUNDING_READY/fundee/data.json b/eclair-core/src/test/resources/nonreg/codecs/050001-DATA_WAIT_FOR_FUNDING_CONFIRMED/funder/data.json similarity index 52% rename from eclair-core/src/test/resources/nonreg/codecs/04000d-DATA_WAIT_FOR_DUAL_FUNDING_READY/fundee/data.json rename to eclair-core/src/test/resources/nonreg/codecs/050001-DATA_WAIT_FOR_FUNDING_CONFIRMED/funder/data.json index 8ef01bebf6..6d01ae414d 100644 --- a/eclair-core/src/test/resources/nonreg/codecs/04000d-DATA_WAIT_FOR_DUAL_FUNDING_READY/fundee/data.json +++ b/eclair-core/src/test/resources/nonreg/codecs/050001-DATA_WAIT_FOR_FUNDING_CONFIRMED/funder/data.json @@ -1,24 +1,20 @@ { - "type" : "DATA_WAIT_FOR_DUAL_FUNDING_READY", + "type" : "DATA_WAIT_FOR_FUNDING_CONFIRMED", "commitments" : { - "params" : { - "channelId" : "ae58e31828b115b8a900a9d20b060ef2b2da2fcfe18991aff34168579d246b54", + "channelParams" : { + "channelId" : "54e64a242e3ddebf0ae861177c9527cab2590753ff06e083447058c14e52df16", "channelConfig" : [ "funding_pubkey_based_channel_keypath" ], - "channelFeatures" : [ "option_static_remotekey", "option_anchors_zero_fee_htlc_tx", "option_dual_fund" ], + "channelFeatures" : [ ], "localParams" : { - "nodeId" : "02bbbb671d15145722fb8c28d732cddb249bcc6652ed2b297ff1f77a18371b1e63", - "fundingKeyPath" : [ 2566431519, 2591595317, 1320214673, 2312083324, 3942626068, 2510065287, 1961707336, 236707474, 2147483648 ], - "dustLimit" : 1000, - "maxHtlcValueInFlightMsat" : 1000000000, - "htlcMinimum" : 1000, - "toSelfDelay" : 144, - "maxAcceptedHtlcs" : 30, - "isChannelOpener" : false, - "paysCommitTxFees" : false, + "nodeId" : "02aaaa00ce2f18a967dc4f25f414e671ba6585f8ef0b8c5fb812c21064f55a2eaa", + "fundingKeyPath" : [ 1333792083, 1189807754, 3133086648, 4154507122, 681435279, 188307941, 695630116, 2607047955, 2147483649 ], + "initialRequestedChannelReserve_opt" : 10000, + "isChannelOpener" : true, + "paysCommitTxFees" : true, "initFeatures" : { "activated" : { "option_route_blinding" : "optional", - "option_dual_fund" : "optional", + "option_provide_storage" : "optional", "splice_prototype" : "optional", "payment_secret" : "mandatory", "gossip_queries_ex" : "optional", @@ -29,28 +25,23 @@ "option_static_remotekey" : "optional", "option_support_large_channel" : "optional", "option_anchors_zero_fee_htlc_tx" : "optional", + "option_channel_type" : "mandatory", "basic_mpp" : "optional", "gossip_queries" : "optional" }, - "unknown" : [ ] + "unknown" : [ 50001 ] } }, "remoteParams" : { - "nodeId" : "02aaaa00ce2f18a967dc4f25f414e671ba6585f8ef0b8c5fb812c21064f55a2eaa", - "dustLimit" : 1100, - "maxHtlcValueInFlightMsat" : 500000000, - "htlcMinimum" : 0, - "toSelfDelay" : 144, - "maxAcceptedHtlcs" : 100, - "revocationBasepoint" : "038039cb4b63c81052cf0893e27f7be58a5808453dffd44c662b1a75aebb1fc74e", - "paymentBasepoint" : "0394e61db71e79c20f2ef131e99121996c7ae04330c4c50dae6bccc5afcf493661", - "delayedPaymentBasepoint" : "029a15b644f2800a507565666956446c05ef8dd7b60023852607c97471d233d62e", - "htlcBasepoint" : "02c633358670a7235d82a5b732d3f7c21e010ea411c805f0f319d922300df453c0", + "nodeId" : "02bbbb671d15145722fb8c28d732cddb249bcc6652ed2b297ff1f77a18371b1e63", + "initialRequestedChannelReserve_opt" : 20000, + "revocationBasepoint" : "0219a94ed17ed28b7fb8a78805e841d42044bf49c330eab4ac2d051c5a1e51e68f", + "paymentBasepoint" : "020a814ad2030bef9d42eb6d29bafa4c22d17f1114f6c9b8651e91d848de4f6a7d", + "delayedPaymentBasepoint" : "032b01af2224e34163fb0b1fe4523cad273818dda1ed7189dfb27e407e70e99afd", + "htlcBasepoint" : "0219e66b4f4dec1ff6644b8ab1196834e1d401dc52bf99bda8b50df8c231e729f5", "initFeatures" : { "activated" : { "option_route_blinding" : "optional", - "option_provide_storage" : "optional", - "option_dual_fund" : "optional", "splice_prototype" : "optional", "payment_secret" : "mandatory", "gossip_queries_ex" : "optional", @@ -61,10 +52,11 @@ "option_static_remotekey" : "optional", "option_support_large_channel" : "optional", "option_anchors_zero_fee_htlc_tx" : "optional", + "option_channel_type" : "mandatory", "basic_mpp" : "optional", "gossip_queries" : "optional" }, - "unknown" : [ 50001 ] + "unknown" : [ ] } }, "channelFlags" : { @@ -88,53 +80,67 @@ }, "active" : [ { "fundingTxIndex" : 0, - "fundingTx" : { - "outPoint" : "e0052c9a6cebcf139248732fcc7c563b022c2ae2d665a670f09389e4ff514393:0", - "amountSatoshis" : 1500000 - }, + "fundingInput" : "16df524ec158704483e006ff530759b2ca27957c1761e80abfde3d2e244ae654:0", + "fundingAmount" : 1000000, "localFunding" : { - "status" : "confirmed", - "txid" : "e0052c9a6cebcf139248732fcc7c563b022c2ae2d665a670f09389e4ff514393", - "shortChannelId" : "400000x42x0" + "status" : "unconfirmed", + "txid" : "16df524ec158704483e006ff530759b2ca27957c1761e80abfde3d2e244ae654" }, "remoteFunding" : { "status" : "not-locked" }, + "commitmentFormat" : "anchor_outputs", + "localCommitParams" : { + "dustLimit" : 1100, + "htlcMinimum" : 0, + "maxHtlcValueInFlight" : 500000000, + "maxAcceptedHtlcs" : 100, + "toSelfDelay" : 720 + }, "localCommit" : { "index" : 0, "spec" : { "htlcs" : [ ], "commitTxFeerate" : 2500, - "toLocal" : 500000000, - "toRemote" : 1000000000 + "toLocal" : 800000000, + "toRemote" : 200000000 }, - "commitTxAndRemoteSig" : { - "commitTx" : { - "txid" : "800a5eab3f585d54f19d6e574b611d94fa7414af651e17104310bf004ccd5cd6", - "tx" : "0200000001934351ffe48993f070a665d6e22a2c023b567ccc2f73489213cfeb6c9a2c05e000000000001a741d80044a010000000000002200209698cbfb709ad73804217ae67e86ea857e8c1b317c82e7740670eb6def38ea334a01000000000000220020d468d18ef4bd63800e3e59bbb204a2921dc75cec9786e363ba2d33d467ae98e820a10700000000002200200e84b31c024b1498353109b6127546c1c1999edf47edb4c772c2108edeb524b4b2340f0000000000220020c38ddef3ceb25118b96983889833cbecd3fdeaf190083eef04ee451b70b47e04a71aae20" - }, - "remoteSig" : "c70d3fc3e720cc4fd1a2e07b97d454392ecfbe27c0ef2eed401538466924f6c24accb834eed784536fe5d30656eb8308a8b0f48bdba2ccc9812ce49a12b9dd80" + "txId" : "a52fbdd06acaf7dc29ad4ef1bcbf0ff99f7e27a89b5bf55d86b857937b5cd128", + "remoteSig" : { + "sig" : "31b9111f33d0905f65b7b2f8fc879af706334c78c0e38b981717ef28674cade56b0cd9af2269e139a087cb1ffbca9db5366f69b9aff2b801df0b1ea1017d32b7" }, - "htlcTxsAndRemoteSigs" : [ ] + "htlcRemoteSigs" : [ ] + }, + "remoteCommitParams" : { + "dustLimit" : 1000, + "htlcMinimum" : 1000, + "maxHtlcValueInFlight" : 1000000000, + "maxAcceptedHtlcs" : 30, + "toSelfDelay" : 144 }, "remoteCommit" : { "index" : 0, "spec" : { "htlcs" : [ ], "commitTxFeerate" : 2500, - "toLocal" : 1000000000, - "toRemote" : 500000000 + "toLocal" : 200000000, + "toRemote" : 800000000 }, - "txid" : "c98fac755c41c1688a55f820a3ac9c4c8b2adde1d6687b2a9d843b364211965f", - "remotePerCommitmentPoint" : "02df0aedf5ee7eba9dbe640dfd3b2da6e108cf52568e4de3e492ed4366355a33c0" + "txId" : "b10e91e8391c08080c344403bc16d96d6d812b928c6269eb6492fe101fcc0241", + "remotePerCommitmentPoint" : "034f07167b088c401ce0092c7e441ca19b39fb14a003324616adcac78a2f44c9db" } } ], "inactive" : [ ], - "remoteNextCommitInfo" : "028a7d253bc83df4357f3c1c89d9040cc55bc5a64da5828e523840a50832543629", + "remoteNextCommitInfo" : "038e89411e0d79c6ef7e693d93b3653cffc3fd7b5e055703bea9d3ef19becc8201", "remotePerCommitmentSecrets" : null, "originChannels" : { } }, - "aliases" : { - "localAlias" : "0x15e6c55cb4a946e" + "waitingSince" : 400000, + "lastSent" : { + "temporaryChannelId" : "0000000000000000000000000000000000000000000000000000000000000000", + "fundingTxId" : "16df524ec158704483e006ff530759b2ca27957c1761e80abfde3d2e244ae654", + "fundingOutputIndex" : 0, + "signature" : "2510bfd05abddacfe165981318528c0ef7a5ab69f2a9146068550ebbc629eb906ec81eb888aac11575f8a58f522d79f5b36c85ecbbe9ec6a9379c5f257370e76", + "tlvStream" : { } } } \ No newline at end of file diff --git a/eclair-core/src/test/resources/nonreg/codecs/04000b-DATA_WAIT_FOR_CHANNEL_READY/funder/data.bin b/eclair-core/src/test/resources/nonreg/codecs/050002-DATA_WAIT_FOR_CHANNEL_READY/funder/data.bin similarity index 84% rename from eclair-core/src/test/resources/nonreg/codecs/04000b-DATA_WAIT_FOR_CHANNEL_READY/funder/data.bin rename to eclair-core/src/test/resources/nonreg/codecs/050002-DATA_WAIT_FOR_CHANNEL_READY/funder/data.bin index 75d7740d4c..58c99441cb 100644 --- a/eclair-core/src/test/resources/nonreg/codecs/04000b-DATA_WAIT_FOR_CHANNEL_READY/funder/data.bin +++ b/eclair-core/src/test/resources/nonreg/codecs/050002-DATA_WAIT_FOR_CHANNEL_READY/funder/data.bin @@ -1 +1 @@ -04000b017d975ecb75e1497076150f745b83dace95a189eecb6172c20a9a4fe0f91b8d1d010102100002aaaa00ce2f18a967dc4f25f414e671ba6585f8ef0b8c5fb812c21064f55a2eaa00090d3df100e4b0d615bfcded915d4edcc9aa98e2e3d88be57e97d451e7f170b3f180000001000000000000044c000000001dcd65000000000000002710000000000000000000900064c000028feba10d0eafd0fad8fe20e6d9206e6bd30242826de05c63f459a00aced24b120000186b02000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000808020a598202bbbb671d15145722fb8c28d732cddb249bcc6652ed2b297ff1f77a18371b1e6300000000000003e8000000003b9aca000000000000004e2000000000000003e80090001e0201dd773d2108f3ed8f59669240c7f098d3083963bef5b36ebf940f4bc40724c8028feba10d0eafd0fad8fe20e6d9206e6bd30242826de05c63f459a00aced24b120243f77d02fbb07f25d6fe14f9c7ee904e1a19442a4aea0dc1ce959d5d2caea9b402e3ae52529ae89f6d7ab478474e0ebe1845d3f7a36967209caad616b0d43353f50000001408000000000000000000000000000008028a598200000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000032862b5ef046f63319d2acc6750db140ff9a12122add71a136f6601eb6e26b0c9095e020000000101010101010101010101010101010101010101010101010101010101010101012a00000000ffffffff0140420f0000000000220020002cc0186fb2a7959231475a7b15726531dbc3f0c52da973a8d3b41969c9a393000000000000010000000000000000000000002710000000002faf0800000000000bebc200247d975ecb75e1497076150f745b83dace95a189eecb6172c20a9a4fe0f91b8d1d000000002b40420f0000000000220020002cc0186fb2a7959231475a7b15726531dbc3f0c52da973a8d3b41969c9a393475221032862b5ef046f63319d2acc6750db140ff9a12122add71a136f6601eb6e26b0c9210386cbbe2cb46c4ce728b18f4405b0d51f2d648b9edf2b79faf2134e5776b672b652ae7d02000000017d975ecb75e1497076150f745b83dace95a189eecb6172c20a9a4fe0f91b8d1d000000000036a2ab8002400d030000000000160014761879f7b274ce995f87150a02e75cc0c037e8e3b8180c000000000022002029b18fc4cf2f2b4634802a1b950d9e27e93b9be9ce638351143432fd0db09163ac6789208d3c57514dd1c01073b3d3704eddcfd8ca6ac3bb58f145f2c2dd921b77f238e842dce706a711b69b250e9ec3b827a9d9e144720a41fadce820bb18280667bffd00000000000000000000000000002710000000000bebc200000000002faf0800d873d760728d9cf12d61c43360c383ab83799c9153f38c6012d9d6f6e6026df1026b84590050c242d572dd2a45b873ce1e108303e1a54a6492e641f3ec1a70a7aa000000ff02449092e08f529ffd0b45361f70f877e865f045b23b80e7658d050690c228012500000000000001061a8000002a0000000101955fc8674b761600 \ No newline at end of file +05000201000923eae31f2c2d205c40d6268926e415e9ab64c4fb034c762127c1e69a68b801010002aaaa00ce2f18a967dc4f25f414e671ba6585f8ef0b8c5fb812c21064f55a2eaa0009799641ad2f302f1974bad79a55ba627e330e43e978e5b92c6e8e117f4cb1081f80000001ff0000000000002710c000000000186b0200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000180802aa698202bbbb671d15145722fb8c28d732cddb249bcc6652ed2b297ff1f77a18371b1e63ff0000000000004e2003131f5a3054e8a0e4b8452169ae4f133c0d37cae987319d4242e3bb3f474fc32e02860e11fc1ce48e9863e1b7c96dcef28e87deda13c814392f7cc7f1240b1d2a7c02e11e1168875cd1793246f54cc19e1c001b94c3fd237bba23c09ee235def98798022f8b16b3b58ced8caaeb4c25a4fee77a77d789a40b3407ca73848dd114c39281000000140800000000000000000000000000100802aa69820000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000024000923eae31f2c2d205c40d6268926e415e9ab64c4fb034c762127c1e69a68b80000000000000000000f42400336cf6283b0a068391d10ef1d97dcb877b40b8270f3fa1f72479e737db92de1990400012401010101010101010101010101010101010101010101010101010101010101012a0000002b40420f0000000000220020ffa48de769e3b5cee1d7facc8fed8bbd4d5e21308740a949aed8729db5c53809061a8000002a000000000102000000000000044c0000000000000000000000001dcd6500006402d000000000000000000000000009c4000000002faf0800000000000bebc2009ece3263aa27791d84aa7af12d968b4ceef337d4a98d9e94f96c91bea62e608901fa8fd3eb502cfa75014da7656410f0d3a7d1b27052c522edb69f5df1f487ea062d270119d5e55c5146e47879fe0d4ff18a67da9dfbe161c7fa4b9234e0371513000000000000000003e800000000000003e8000000003b9aca00001e009000000000000000000000000009c4000000000bebc200000000002faf0800ab0f4b3984356e59f4c60c6515f4f1ed83654ddf80ea6b8b039b0a2d825e95530306cf422411b55494026f160c21203873815da2cc22593fa54255794bce737da5000000ff036e61f20d145451813cc94d455c7178eaf4bfa6c2cd1658c9ac96bb0d97b87d1f0000000000000001023c70817b9d4b8d00 \ No newline at end of file diff --git a/eclair-core/src/test/resources/nonreg/codecs/04000b-DATA_WAIT_FOR_CHANNEL_READY/funder/data.json b/eclair-core/src/test/resources/nonreg/codecs/050002-DATA_WAIT_FOR_CHANNEL_READY/funder/data.json similarity index 57% rename from eclair-core/src/test/resources/nonreg/codecs/04000b-DATA_WAIT_FOR_CHANNEL_READY/funder/data.json rename to eclair-core/src/test/resources/nonreg/codecs/050002-DATA_WAIT_FOR_CHANNEL_READY/funder/data.json index c190f0a5be..8e4b03ca79 100644 --- a/eclair-core/src/test/resources/nonreg/codecs/04000b-DATA_WAIT_FOR_CHANNEL_READY/funder/data.json +++ b/eclair-core/src/test/resources/nonreg/codecs/050002-DATA_WAIT_FOR_CHANNEL_READY/funder/data.json @@ -1,34 +1,31 @@ { "type" : "DATA_WAIT_FOR_CHANNEL_READY", "commitments" : { - "params" : { - "channelId" : "7d975ecb75e1497076150f745b83dace95a189eecb6172c20a9a4fe0f91b8d1d", + "channelParams" : { + "channelId" : "000923eae31f2c2d205c40d6268926e415e9ab64c4fb034c762127c1e69a68b8", "channelConfig" : [ "funding_pubkey_based_channel_keypath" ], - "channelFeatures" : [ "option_static_remotekey" ], + "channelFeatures" : [ ], "localParams" : { "nodeId" : "02aaaa00ce2f18a967dc4f25f414e671ba6585f8ef0b8c5fb812c21064f55a2eaa", - "fundingKeyPath" : [ 222163200, 3836794389, 3217943953, 1565449417, 2862146275, 3633046910, 2547274215, 4050695153, 2147483649 ], - "dustLimit" : 1100, - "maxHtlcValueInFlightMsat" : 500000000, + "fundingKeyPath" : [ 2039890349, 791686937, 1958401946, 1438278270, 856572905, 2028321068, 1854804351, 1286670367, 2147483649 ], "initialRequestedChannelReserve_opt" : 10000, - "htlcMinimum" : 0, - "toSelfDelay" : 144, - "maxAcceptedHtlcs" : 100, "isChannelOpener" : true, "paysCommitTxFees" : true, - "walletStaticPaymentBasepoint" : "028feba10d0eafd0fad8fe20e6d9206e6bd30242826de05c63f459a00aced24b12", "initFeatures" : { "activated" : { - "option_support_large_channel" : "optional", "option_route_blinding" : "optional", "option_provide_storage" : "optional", "splice_prototype" : "optional", "payment_secret" : "mandatory", "gossip_queries_ex" : "optional", + "option_anchor_outputs" : "optional", "option_quiesce" : "optional", "option_data_loss_protect" : "optional", "var_onion_optin" : "mandatory", - "option_static_remotekey" : "mandatory", + "option_static_remotekey" : "optional", + "option_support_large_channel" : "optional", + "option_anchors_zero_fee_htlc_tx" : "optional", + "option_channel_type" : "mandatory", "basic_mpp" : "optional", "gossip_queries" : "optional" }, @@ -37,28 +34,25 @@ }, "remoteParams" : { "nodeId" : "02bbbb671d15145722fb8c28d732cddb249bcc6652ed2b297ff1f77a18371b1e63", - "dustLimit" : 1000, - "maxHtlcValueInFlightMsat" : 1000000000, "initialRequestedChannelReserve_opt" : 20000, - "htlcMinimum" : 1000, - "toSelfDelay" : 144, - "maxAcceptedHtlcs" : 30, - "revocationBasepoint" : "0201dd773d2108f3ed8f59669240c7f098d3083963bef5b36ebf940f4bc40724c8", - "paymentBasepoint" : "028feba10d0eafd0fad8fe20e6d9206e6bd30242826de05c63f459a00aced24b12", - "delayedPaymentBasepoint" : "0243f77d02fbb07f25d6fe14f9c7ee904e1a19442a4aea0dc1ce959d5d2caea9b4", - "htlcBasepoint" : "02e3ae52529ae89f6d7ab478474e0ebe1845d3f7a36967209caad616b0d43353f5", + "revocationBasepoint" : "03131f5a3054e8a0e4b8452169ae4f133c0d37cae987319d4242e3bb3f474fc32e", + "paymentBasepoint" : "02860e11fc1ce48e9863e1b7c96dcef28e87deda13c814392f7cc7f1240b1d2a7c", + "delayedPaymentBasepoint" : "02e11e1168875cd1793246f54cc19e1c001b94c3fd237bba23c09ee235def98798", + "htlcBasepoint" : "022f8b16b3b58ced8caaeb4c25a4fee77a77d789a40b3407ca73848dd114c39281", "initFeatures" : { "activated" : { "option_route_blinding" : "optional", "splice_prototype" : "optional", "payment_secret" : "mandatory", "gossip_queries_ex" : "optional", + "option_anchor_outputs" : "optional", "option_quiesce" : "optional", "option_data_loss_protect" : "optional", "var_onion_optin" : "mandatory", - "option_static_remotekey" : "mandatory", + "option_static_remotekey" : "optional", "option_support_large_channel" : "optional", "option_anchors_zero_fee_htlc_tx" : "optional", + "option_channel_type" : "mandatory", "basic_mpp" : "optional", "gossip_queries" : "optional" }, @@ -86,53 +80,62 @@ }, "active" : [ { "fundingTxIndex" : 0, - "fundingTx" : { - "outPoint" : "1d8d1bf9e04f9a0ac27261cbee89a195ceda835b740f15767049e175cb5e977d:0", - "amountSatoshis" : 1000000 - }, + "fundingInput" : "b8689ae6c12721764c03fbc464abe915e4268926d6405c202d2c1fe3ea230900:0", + "fundingAmount" : 1000000, "localFunding" : { "status" : "confirmed", - "txid" : "1d8d1bf9e04f9a0ac27261cbee89a195ceda835b740f15767049e175cb5e977d", "shortChannelId" : "400000x42x0" }, "remoteFunding" : { "status" : "not-locked" }, + "commitmentFormat" : "anchor_outputs", + "localCommitParams" : { + "dustLimit" : 1100, + "htlcMinimum" : 0, + "maxHtlcValueInFlight" : 500000000, + "maxAcceptedHtlcs" : 100, + "toSelfDelay" : 720 + }, "localCommit" : { "index" : 0, "spec" : { "htlcs" : [ ], - "commitTxFeerate" : 10000, + "commitTxFeerate" : 2500, "toLocal" : 800000000, "toRemote" : 200000000 }, - "commitTxAndRemoteSig" : { - "commitTx" : { - "txid" : "ad28d946d13d331539d08da9d131311d1bec7308ddf072a59b07b2dc22779c3f", - "tx" : "02000000017d975ecb75e1497076150f745b83dace95a189eecb6172c20a9a4fe0f91b8d1d000000000036a2ab8002400d030000000000160014761879f7b274ce995f87150a02e75cc0c037e8e3b8180c000000000022002029b18fc4cf2f2b4634802a1b950d9e27e93b9be9ce638351143432fd0db09163ac678920" - }, - "remoteSig" : "8d3c57514dd1c01073b3d3704eddcfd8ca6ac3bb58f145f2c2dd921b77f238e842dce706a711b69b250e9ec3b827a9d9e144720a41fadce820bb18280667bffd" + "txId" : "9ece3263aa27791d84aa7af12d968b4ceef337d4a98d9e94f96c91bea62e6089", + "remoteSig" : { + "sig" : "fa8fd3eb502cfa75014da7656410f0d3a7d1b27052c522edb69f5df1f487ea062d270119d5e55c5146e47879fe0d4ff18a67da9dfbe161c7fa4b9234e0371513" }, - "htlcTxsAndRemoteSigs" : [ ] + "htlcRemoteSigs" : [ ] + }, + "remoteCommitParams" : { + "dustLimit" : 1000, + "htlcMinimum" : 1000, + "maxHtlcValueInFlight" : 1000000000, + "maxAcceptedHtlcs" : 30, + "toSelfDelay" : 144 }, "remoteCommit" : { "index" : 0, "spec" : { "htlcs" : [ ], - "commitTxFeerate" : 10000, + "commitTxFeerate" : 2500, "toLocal" : 200000000, "toRemote" : 800000000 }, - "txid" : "d873d760728d9cf12d61c43360c383ab83799c9153f38c6012d9d6f6e6026df1", - "remotePerCommitmentPoint" : "026b84590050c242d572dd2a45b873ce1e108303e1a54a6492e641f3ec1a70a7aa" + "txId" : "ab0f4b3984356e59f4c60c6515f4f1ed83654ddf80ea6b8b039b0a2d825e9553", + "remotePerCommitmentPoint" : "0306cf422411b55494026f160c21203873815da2cc22593fa54255794bce737da5" } } ], "inactive" : [ ], - "remoteNextCommitInfo" : "02449092e08f529ffd0b45361f70f877e865f045b23b80e7658d050690c2280125", + "remoteNextCommitInfo" : "036e61f20d145451813cc94d455c7178eaf4bfa6c2cd1658c9ac96bb0d97b87d1f", "remotePerCommitmentSecrets" : null, "originChannels" : { } }, "aliases" : { - "localAlias" : "0x1955fc8674b7616" + "localAlias" : "0x23c70817b9d4b8d" } } \ No newline at end of file diff --git a/eclair-core/src/test/resources/nonreg/codecs/03000c-DATA_WAIT_FOR_DUAL_FUNDING_READY/funder/data.bin b/eclair-core/src/test/resources/nonreg/codecs/050003-DATA_WAIT_FOR_DUAL_FUNDING_SIGNED/fundee/data.bin similarity index 82% rename from eclair-core/src/test/resources/nonreg/codecs/03000c-DATA_WAIT_FOR_DUAL_FUNDING_READY/funder/data.bin rename to eclair-core/src/test/resources/nonreg/codecs/050003-DATA_WAIT_FOR_DUAL_FUNDING_SIGNED/fundee/data.bin index 600f678a37..e7138dab10 100644 --- a/eclair-core/src/test/resources/nonreg/codecs/03000c-DATA_WAIT_FOR_DUAL_FUNDING_READY/funder/data.bin +++ b/eclair-core/src/test/resources/nonreg/codecs/050003-DATA_WAIT_FOR_DUAL_FUNDING_SIGNED/fundee/data.bin @@ -1 +1 @@ -03000cf5cafea10bc83c2fe9de16d04bb73c1ddaed2ce9d600dd91301a7d995f7b91340101041040100002aaaa00ce2f18a967dc4f25f414e671ba6585f8ef0b8c5fb812c21064f55a2eaa0009b958966e993ef419a47373150b761bd2e73cf9c6500d6062d9462011695ed19580000001000000000000044c000000001dcd6500000000000000000000900064ff160014fe2baa428e1f6b4b4134d444b3de42a86cffbabc0000186b0200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000022a2698202bbbb671d15145722fb8c28d732cddb249bcc6652ed2b297ff1f77a18371b1e6300000000000003e8000000003b9aca0000000000000003e80090001e032e093165a2e7c96d5f362e08c51d46f931032513569fdfbb107781e09c6183ab03626342f0af6e87ab41715bd6d1db7d6eee5e95a2a835680b42d9b357003c1c6a02ed6013749b4e3a820e7c003d3d3c7c5f16dd0d25ce72d3de99e544d4011c0a67038a15a3dcd087135509b8e91d95c4fb788be11735f5c1a9ac24283d3cecbc76d2036d77c307c066c5308618e885d18475ea18fde69887df29ae3c2dfdffd2dfbbb10000000422a26982000000000000000000000000000009c4000000003b9aca00000000001dcd650024db1c7ef42a989448f5599588b8a3213df36f9ed732a33043a45cab7773274374000000002b60e316000000000022002021c3887e1a9225682a57bf0285c1a0ed0cd4361fc71aaa7785614dc89d58ab2e47522102bfad58b344c5d8954b937f2a66d48ee070b8d249de24cbde336587daeb2d497721032e093165a2e7c96d5f362e08c51d46f931032513569fdfbb107781e09c6183ab52aedf0200000001db1c7ef42a989448f5599588b8a3213df36f9ed732a33043a45cab7773274374000000000058951280044a010000000000002200203a52179d307a973d00b2b102a0df754258ba7f0d1011a2a5d9e2844004b76d8a4a01000000000000220020a8b562e803e0523f85ff90ff8f90571a03c016bb61f387016d16db99f539c54e20a107000000000022002084bbfa5dee3d11a165c2ec7c0ce8b66e055fd7b6d90be6f0162dd4887cc25226b2340f00000000002200207fafaeecce02b12fad250d513a7493fc21bc9f4b82c7bbc115de3038cf984f933e9c08205d65b42b0a8f05ca2793360a6b34bd5c1b9d960cf62c300c0e493f9cd83d2ca41f46a663d4fc11891ef268c0200b8fea8fb454d9055d35c2d6f5ba0b14c78b2a000000000000000000000000000009c4000000001dcd6500000000003b9aca00990138002f6fc36ae3ea48b88e2302a80eb925c33668e59638a82840f1e633ad037d0b91e7bf58eec2eddf033d457b17140a341533808a346c869ada9ecea0cec0000000000000000000000000000000000000000000000000000000000000ff02a7d9d163632731c7211ced4ee21ae181bb0dfa73f5538607c081dd63d89f982024db1c7ef42a989448f5599588b8a3213df36f9ed732a33043a45cab7773274374000000002b60e316000000000022002021c3887e1a9225682a57bf0285c1a0ed0cd4361fc71aaa7785614dc89d58ab2e47522102bfad58b344c5d8954b937f2a66d48ee070b8d249de24cbde336587daeb2d497721032e093165a2e7c96d5f362e08c51d46f931032513569fdfbb107781e09c6183ab52ae00000001061a8000002a0000000102d1583d8409d217004bf5cafea10bc83c2fe9de16d04bb73c1ddaed2ce9d600dd91301a7d995f7b913403d5b6576ef060f70de70e5c4ffc73f7fde7b9e8a7874fd56d4371e7974eff9fd3010802d1583d8409d217 \ No newline at end of file +050003f9a841690da209150ad3eea75da5766fd80ba8bbe98a348c0d1f06192e9966b30101041000000002bbbb671d15145722fb8c28d732cddb249bcc6652ed2b297ff1f77a18371b1e630009dc6ea8cbc65a6b3b4329c31d9d622a08e11b26bed1966d35ce425c1379bbca008000000000000000000000140800000000000000000000000000100822aa698202aaaa00ce2f18a967dc4f25f414e671ba6585f8ef0b8c5fb812c21064f55a2eaa00032776bc3dddf0f65d91492db9ec2c2c65ba04dafa1115b47edd4931d46cb69d0a03ef6b2a5fcde344869a3a295a54353ceaf4d77baf1e5ff75279fcc86429826b8c0362172c6c6ce4fab010b204abe117531d6064615ad20950ede6665abaf67f970a02d19759c7ffb955b349cb43530f3ad0d14101c5423064793a474ee62299bb44290000186b0200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000180822aa69820000027c9a7f54cfaebf911633e48b3604513592363d909341e9b87acebaa78d8fe4fe00000000000000000000000000000000f9a841690da209150ad3eea75da5766fd80ba8bbe98a348c0d1f06192e9966b300000000000007a12000000000000f42400002e73872855a7a4d3ce04feae605f06259146bb3f2d2f67957579a46e7b92dd1d300000200061a80000000000000044c000027100000000000000000000000000000022200201be4326e0cb81be6bebfc2435f3db50dedec75fef4539333e6f92ac4a873cb20000000001dcd6500000000003b9aca0000000000000000000001010000000000000001520200000001d630e8d571838cf351e72df96020cd9bd333070a907e6da21ac500be64f8d42e01000000000000000001c027090000000000160014469aab81cd82c84aeeceee6e8152231e660e392300000000000000000000000000010100000000000000002455e60d949a07dd629cc375e853ddd7553e5adbfe7485550be3e6b7c437b75d98000000002be0c810000000000022512086502ec56d407c524b2952a88fb489c1d664bc3ec2c0781f02a288a34a832a1c000000000001010000000000000003000000000001770a160014469aab81cd82c84aeeceee6e8152231e660e392300010100000000000000040000000000016e9022512086502ec56d407c524b2952a88fb489c1d664bc3ec2c0781f02a288a34a832a1c00061a80b0f9a841690da209150ad3eea75da5766fd80ba8bbe98a348c0d1f06192e9966b3cc8e08b3ce0c7233a29dc4257afb20a52b70e02076ed85ebadc687d4b68c00110001006c02483045022100acf179f4b82fa9dddf3e7259516447ff12797acc3d367e3e6975651d435c7475022010fcac540579523ed844db9aad1f84751cb107660144494c26cff625ec9a2707012102cd108f37e0d9da60adfcb2710a7e0453a372613d68406a5e9125eea7c014327500000000000003e800000000000003e8000000003b9aca00001e00900000000000000000000000000009c4000000001dcd6500000000003b9aca00503e2b7aa61c4e8ecbc9c03cd7418cb2ea00633cbb120ab3e2090030bcef6c1e000000000000044c0000000000000000000000001dcd6500006402d000000000000000000000000009c4000000003b9aca00000000001dcd650051747ac3f424543fa6577c50dca579fe03a5edf23d2a7f79348514e7720482910257e7b83fa5c0711996cb718969b835b18eee9def51081061d9c49ba354e131f000 \ No newline at end of file diff --git a/eclair-core/src/test/resources/nonreg/codecs/050003-DATA_WAIT_FOR_DUAL_FUNDING_SIGNED/fundee/data.json b/eclair-core/src/test/resources/nonreg/codecs/050003-DATA_WAIT_FOR_DUAL_FUNDING_SIGNED/fundee/data.json new file mode 100644 index 0000000000..5e71bd01d4 --- /dev/null +++ b/eclair-core/src/test/resources/nonreg/codecs/050003-DATA_WAIT_FOR_DUAL_FUNDING_SIGNED/fundee/data.json @@ -0,0 +1,171 @@ +{ + "type" : "DATA_WAIT_FOR_DUAL_FUNDING_SIGNED", + "channelParams" : { + "channelId" : "f9a841690da209150ad3eea75da5766fd80ba8bbe98a348c0d1f06192e9966b3", + "channelConfig" : [ "funding_pubkey_based_channel_keypath" ], + "channelFeatures" : [ "option_dual_fund" ], + "localParams" : { + "nodeId" : "02bbbb671d15145722fb8c28d732cddb249bcc6652ed2b297ff1f77a18371b1e63", + "fundingKeyPath" : [ 3698239691, 3327814459, 1126810397, 2640456200, 3776652990, 3516296501, 3460455443, 2042350080, 2147483648 ], + "isChannelOpener" : false, + "paysCommitTxFees" : false, + "initFeatures" : { + "activated" : { + "option_route_blinding" : "optional", + "option_dual_fund" : "optional", + "splice_prototype" : "optional", + "payment_secret" : "mandatory", + "gossip_queries_ex" : "optional", + "option_anchor_outputs" : "optional", + "option_quiesce" : "optional", + "option_data_loss_protect" : "optional", + "var_onion_optin" : "mandatory", + "option_static_remotekey" : "optional", + "option_support_large_channel" : "optional", + "option_anchors_zero_fee_htlc_tx" : "optional", + "option_channel_type" : "mandatory", + "basic_mpp" : "optional", + "gossip_queries" : "optional" + }, + "unknown" : [ ] + } + }, + "remoteParams" : { + "nodeId" : "02aaaa00ce2f18a967dc4f25f414e671ba6585f8ef0b8c5fb812c21064f55a2eaa", + "revocationBasepoint" : "032776bc3dddf0f65d91492db9ec2c2c65ba04dafa1115b47edd4931d46cb69d0a", + "paymentBasepoint" : "03ef6b2a5fcde344869a3a295a54353ceaf4d77baf1e5ff75279fcc86429826b8c", + "delayedPaymentBasepoint" : "0362172c6c6ce4fab010b204abe117531d6064615ad20950ede6665abaf67f970a", + "htlcBasepoint" : "02d19759c7ffb955b349cb43530f3ad0d14101c5423064793a474ee62299bb4429", + "initFeatures" : { + "activated" : { + "option_route_blinding" : "optional", + "option_provide_storage" : "optional", + "option_dual_fund" : "optional", + "splice_prototype" : "optional", + "payment_secret" : "mandatory", + "gossip_queries_ex" : "optional", + "option_anchor_outputs" : "optional", + "option_quiesce" : "optional", + "option_data_loss_protect" : "optional", + "var_onion_optin" : "mandatory", + "option_static_remotekey" : "optional", + "option_support_large_channel" : "optional", + "option_anchors_zero_fee_htlc_tx" : "optional", + "option_channel_type" : "mandatory", + "basic_mpp" : "optional", + "gossip_queries" : "optional" + }, + "unknown" : [ 50001 ] + } + }, + "channelFlags" : { + "nonInitiatorPaysCommitFees" : false, + "announceChannel" : false + } + }, + "secondRemotePerCommitmentPoint" : "027c9a7f54cfaebf911633e48b3604513592363d909341e9b87acebaa78d8fe4fe", + "localPushAmount" : 0, + "remotePushAmount" : 0, + "signingSession" : { + "fundingParams" : { + "channelId" : "f9a841690da209150ad3eea75da5766fd80ba8bbe98a348c0d1f06192e9966b3", + "isInitiator" : false, + "localContribution" : 500000, + "remoteContribution" : 1000000, + "remoteFundingPubKey" : "02e73872855a7a4d3ce04feae605f06259146bb3f2d2f67957579a46e7b92dd1d3", + "localOutputs" : [ ], + "commitmentFormat" : { }, + "lockTime" : 400000, + "dustLimit" : 1100, + "targetFeerate" : 10000, + "requireConfirmedInputs" : { + "forLocal" : false, + "forRemote" : false + } + }, + "fundingTxIndex" : 0, + "fundingTx" : { + "tx" : { + "sharedOutput" : { + "serialId" : 2, + "pubkeyScript" : "00201be4326e0cb81be6bebfc2435f3db50dedec75fef4539333e6f92ac4a873cb20", + "localAmount" : 500000000, + "remoteAmount" : 1000000000, + "htlcAmount" : 0 + }, + "localInputs" : [ { + "serialId" : 1, + "previousTx" : { + "txid" : "30d77c8dd0145cbb8acce3e239024449417099d7458ec90661131c97d41cdb30", + "tx" : "0200000001d630e8d571838cf351e72df96020cd9bd333070a907e6da21ac500be64f8d42e01000000000000000001c027090000000000160014469aab81cd82c84aeeceee6e8152231e660e392300000000" + }, + "previousTxOutput" : 0, + "sequence" : 0 + } ], + "remoteInputs" : [ { + "serialId" : 0, + "outPoint" : "985db737c4b7e6e30b558574fedb5a3e55d7dd53e875c39c62dd079a940de655:0", + "txOut" : { + "amount" : 1100000, + "publicKeyScript" : "512086502ec56d407c524b2952a88fb489c1d664bc3ec2c0781f02a288a34a832a1c" + }, + "sequence" : 0 + } ], + "localOutputs" : [ { + "serialId" : 3, + "amount" : 96010, + "pubkeyScript" : "0014469aab81cd82c84aeeceee6e8152231e660e3923" + } ], + "remoteOutputs" : [ { + "serialId" : 4, + "amount" : 93840, + "pubkeyScript" : "512086502ec56d407c524b2952a88fb489c1d664bc3ec2c0781f02a288a34a832a1c" + } ], + "lockTime" : 400000 + }, + "localSigs" : { + "channelId" : "f9a841690da209150ad3eea75da5766fd80ba8bbe98a348c0d1f06192e9966b3", + "txId" : "11008cb6d487c6adeb85ed7620e0702ba520fb7a25c49da233720cceb3088ecc", + "witnesses" : [ { + "stack" : [ "3045022100acf179f4b82fa9dddf3e7259516447ff12797acc3d367e3e6975651d435c7475022010fcac540579523ed844db9aad1f84751cb107660144494c26cff625ec9a270701", "02cd108f37e0d9da60adfcb2710a7e0453a372613d68406a5e9125eea7c0143275" ] + } ], + "tlvStream" : { } + } + }, + "localCommitParams" : { + "dustLimit" : 1000, + "htlcMinimum" : 1000, + "maxHtlcValueInFlight" : 1000000000, + "maxAcceptedHtlcs" : 30, + "toSelfDelay" : 144 + }, + "localCommit" : { + "index" : 0, + "spec" : { + "htlcs" : [ ], + "commitTxFeerate" : 2500, + "toLocal" : 500000000, + "toRemote" : 1000000000 + }, + "txId" : "503e2b7aa61c4e8ecbc9c03cd7418cb2ea00633cbb120ab3e2090030bcef6c1e" + }, + "remoteCommitParams" : { + "dustLimit" : 1100, + "htlcMinimum" : 0, + "maxHtlcValueInFlight" : 500000000, + "maxAcceptedHtlcs" : 100, + "toSelfDelay" : 720 + }, + "remoteCommit" : { + "index" : 0, + "spec" : { + "htlcs" : [ ], + "commitTxFeerate" : 2500, + "toLocal" : 1000000000, + "toRemote" : 500000000 + }, + "txId" : "51747ac3f424543fa6577c50dca579fe03a5edf23d2a7f79348514e772048291", + "remotePerCommitmentPoint" : "0257e7b83fa5c0711996cb718969b835b18eee9def51081061d9c49ba354e131f0" + } + } +} \ No newline at end of file diff --git a/eclair-core/src/test/resources/nonreg/codecs/050003-DATA_WAIT_FOR_DUAL_FUNDING_SIGNED/funder/data.bin b/eclair-core/src/test/resources/nonreg/codecs/050003-DATA_WAIT_FOR_DUAL_FUNDING_SIGNED/funder/data.bin new file mode 100644 index 0000000000..45e5d6b77d --- /dev/null +++ b/eclair-core/src/test/resources/nonreg/codecs/050003-DATA_WAIT_FOR_DUAL_FUNDING_SIGNED/funder/data.bin @@ -0,0 +1 @@ +050003f9a841690da209150ad3eea75da5766fd80ba8bbe98a348c0d1f06192e9966b30101041000000002aaaa00ce2f18a967dc4f25f414e671ba6585f8ef0b8c5fb812c21064f55a2eaa000993a0d264fde0eea09589912e85914841b49e9e44ae32130e302f8c88dfb3f1b38000000100c000000000186b0200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000180822aa698202bbbb671d15145722fb8c28d732cddb249bcc6652ed2b297ff1f77a18371b1e630003e2a80108b554d09221227b27e9539b40c2467a8697857689595a7cde2b49b91002f58bdf5ec331de694917466969c586f2b1d049244903b641003829008771b5b2028dd357d5fbd3cc8826d294bcd516b420a58363b01ee0548a4b99c8331c691820037be0c20c6eddb50d5a848359dad825bfb2cefc982adee1fc42a15dc8006b5e62000000140800000000000000000000000000100822aa698200000224e765a3b0cc9046df2c93046cda43bafde997807eb5dd2662ed1101b4fed07f00000000000000000000000000000000f9a841690da209150ad3eea75da5766fd80ba8bbe98a348c0d1f06192e9966b3ff00000000000f4240000000000007a12000022f25f60e5675c111a8752672da9e428d73453fb30d30022201e43d2b63e0187000000200061a80000000000000044c000027100000000000000000000000000000022200201be4326e0cb81be6bebfc2435f3db50dedec75fef4539333e6f92ac4a873cb20000000003b9aca00000000001dcd6500000000000000000000010100000000000000005e02000000013d66403ac1c2adef02f159ebf320f74a3ece8aed8cc05d77194882833c147c6501000000000000000001e0c810000000000022512086502ec56d407c524b2952a88fb489c1d664bc3ec2c0781f02a288a34a832a1c00000000000000000000000000010100000000000000012430db1cd4971c136106c98e45d799704149440239e2e3cc8abb5c14d08d7cd730000000001fc027090000000000160014469aab81cd82c84aeeceee6e8152231e660e39230000000000010100000000000000040000000000016e9022512086502ec56d407c524b2952a88fb489c1d664bc3ec2c0781f02a288a34a832a1c0001010000000000000003000000000001770a160014469aab81cd82c84aeeceee6e8152231e660e392300061a8086f9a841690da209150ad3eea75da5766fd80ba8bbe98a348c0d1f06192e9966b3cc8e08b3ce0c7233a29dc4257afb20a52b70e02076ed85ebadc687d4b68c001100010042014006644f46eaa79ed3ab78ecb51d16b194d98c5d5c310faec68b69551f0cdd63cdbee88bd98b353d9a209e5bfe02e601cb41fd105a9a2a368533320f5009c91e01000000000000044c0000000000000000000000001dcd6500006402d0ff00000000000000000000000009c4000000003b9aca00000000001dcd650051747ac3f424543fa6577c50dca579fe03a5edf23d2a7f79348514e772048291011aba7bc42cb220e8b7b15b360bb93724aaa7d7095ec90a89f7307b22d1f654ea29deb3fcf4c17fd1e9fb387b2eb7bdaf21d788109376643bcbd4d7b78607b6d3000000000000000003e800000000000003e8000000003b9aca00001e009000000000000000000000000009c4000000001dcd6500000000003b9aca00503e2b7aa61c4e8ecbc9c03cd7418cb2ea00633cbb120ab3e2090030bcef6c1e03bab000830e59155885a54ec35603e94ca7a380d98b61e988cc6430982335b10000 \ No newline at end of file diff --git a/eclair-core/src/test/resources/nonreg/codecs/050003-DATA_WAIT_FOR_DUAL_FUNDING_SIGNED/funder/data.json b/eclair-core/src/test/resources/nonreg/codecs/050003-DATA_WAIT_FOR_DUAL_FUNDING_SIGNED/funder/data.json new file mode 100644 index 0000000000..7d6286ed80 --- /dev/null +++ b/eclair-core/src/test/resources/nonreg/codecs/050003-DATA_WAIT_FOR_DUAL_FUNDING_SIGNED/funder/data.json @@ -0,0 +1,175 @@ +{ + "type" : "DATA_WAIT_FOR_DUAL_FUNDING_SIGNED", + "channelParams" : { + "channelId" : "f9a841690da209150ad3eea75da5766fd80ba8bbe98a348c0d1f06192e9966b3", + "channelConfig" : [ "funding_pubkey_based_channel_keypath" ], + "channelFeatures" : [ "option_dual_fund" ], + "localParams" : { + "nodeId" : "02aaaa00ce2f18a967dc4f25f414e671ba6585f8ef0b8c5fb812c21064f55a2eaa", + "fundingKeyPath" : [ 2476790372, 4259376800, 2508820782, 2240890945, 3030294084, 2922517262, 808422536, 3753111987, 2147483649 ], + "isChannelOpener" : true, + "paysCommitTxFees" : true, + "initFeatures" : { + "activated" : { + "option_route_blinding" : "optional", + "option_provide_storage" : "optional", + "option_dual_fund" : "optional", + "splice_prototype" : "optional", + "payment_secret" : "mandatory", + "gossip_queries_ex" : "optional", + "option_anchor_outputs" : "optional", + "option_quiesce" : "optional", + "option_data_loss_protect" : "optional", + "var_onion_optin" : "mandatory", + "option_static_remotekey" : "optional", + "option_support_large_channel" : "optional", + "option_anchors_zero_fee_htlc_tx" : "optional", + "option_channel_type" : "mandatory", + "basic_mpp" : "optional", + "gossip_queries" : "optional" + }, + "unknown" : [ 50001 ] + } + }, + "remoteParams" : { + "nodeId" : "02bbbb671d15145722fb8c28d732cddb249bcc6652ed2b297ff1f77a18371b1e63", + "revocationBasepoint" : "03e2a80108b554d09221227b27e9539b40c2467a8697857689595a7cde2b49b910", + "paymentBasepoint" : "02f58bdf5ec331de694917466969c586f2b1d049244903b641003829008771b5b2", + "delayedPaymentBasepoint" : "028dd357d5fbd3cc8826d294bcd516b420a58363b01ee0548a4b99c8331c691820", + "htlcBasepoint" : "037be0c20c6eddb50d5a848359dad825bfb2cefc982adee1fc42a15dc8006b5e62", + "initFeatures" : { + "activated" : { + "option_route_blinding" : "optional", + "option_dual_fund" : "optional", + "splice_prototype" : "optional", + "payment_secret" : "mandatory", + "gossip_queries_ex" : "optional", + "option_anchor_outputs" : "optional", + "option_quiesce" : "optional", + "option_data_loss_protect" : "optional", + "var_onion_optin" : "mandatory", + "option_static_remotekey" : "optional", + "option_support_large_channel" : "optional", + "option_anchors_zero_fee_htlc_tx" : "optional", + "option_channel_type" : "mandatory", + "basic_mpp" : "optional", + "gossip_queries" : "optional" + }, + "unknown" : [ ] + } + }, + "channelFlags" : { + "nonInitiatorPaysCommitFees" : false, + "announceChannel" : false + } + }, + "secondRemotePerCommitmentPoint" : "0224e765a3b0cc9046df2c93046cda43bafde997807eb5dd2662ed1101b4fed07f", + "localPushAmount" : 0, + "remotePushAmount" : 0, + "signingSession" : { + "fundingParams" : { + "channelId" : "f9a841690da209150ad3eea75da5766fd80ba8bbe98a348c0d1f06192e9966b3", + "isInitiator" : true, + "localContribution" : 1000000, + "remoteContribution" : 500000, + "remoteFundingPubKey" : "022f25f60e5675c111a8752672da9e428d73453fb30d30022201e43d2b63e01870", + "localOutputs" : [ ], + "commitmentFormat" : { }, + "lockTime" : 400000, + "dustLimit" : 1100, + "targetFeerate" : 10000, + "requireConfirmedInputs" : { + "forLocal" : false, + "forRemote" : false + } + }, + "fundingTxIndex" : 0, + "fundingTx" : { + "tx" : { + "sharedOutput" : { + "serialId" : 2, + "pubkeyScript" : "00201be4326e0cb81be6bebfc2435f3db50dedec75fef4539333e6f92ac4a873cb20", + "localAmount" : 1000000000, + "remoteAmount" : 500000000, + "htlcAmount" : 0 + }, + "localInputs" : [ { + "serialId" : 0, + "previousTx" : { + "txid" : "985db737c4b7e6e30b558574fedb5a3e55d7dd53e875c39c62dd079a940de655", + "tx" : "02000000013d66403ac1c2adef02f159ebf320f74a3ece8aed8cc05d77194882833c147c6501000000000000000001e0c810000000000022512086502ec56d407c524b2952a88fb489c1d664bc3ec2c0781f02a288a34a832a1c00000000" + }, + "previousTxOutput" : 0, + "sequence" : 0 + } ], + "remoteInputs" : [ { + "serialId" : 1, + "outPoint" : "30d77c8dd0145cbb8acce3e239024449417099d7458ec90661131c97d41cdb30:0", + "txOut" : { + "amount" : 600000, + "publicKeyScript" : "0014469aab81cd82c84aeeceee6e8152231e660e3923" + }, + "sequence" : 0 + } ], + "localOutputs" : [ { + "serialId" : 4, + "amount" : 93840, + "pubkeyScript" : "512086502ec56d407c524b2952a88fb489c1d664bc3ec2c0781f02a288a34a832a1c" + } ], + "remoteOutputs" : [ { + "serialId" : 3, + "amount" : 96010, + "pubkeyScript" : "0014469aab81cd82c84aeeceee6e8152231e660e3923" + } ], + "lockTime" : 400000 + }, + "localSigs" : { + "channelId" : "f9a841690da209150ad3eea75da5766fd80ba8bbe98a348c0d1f06192e9966b3", + "txId" : "11008cb6d487c6adeb85ed7620e0702ba520fb7a25c49da233720cceb3088ecc", + "witnesses" : [ { + "stack" : [ "06644f46eaa79ed3ab78ecb51d16b194d98c5d5c310faec68b69551f0cdd63cdbee88bd98b353d9a209e5bfe02e601cb41fd105a9a2a368533320f5009c91e01" ] + } ], + "tlvStream" : { } + } + }, + "localCommitParams" : { + "dustLimit" : 1100, + "htlcMinimum" : 0, + "maxHtlcValueInFlight" : 500000000, + "maxAcceptedHtlcs" : 100, + "toSelfDelay" : 720 + }, + "localCommit" : { + "index" : 0, + "spec" : { + "htlcs" : [ ], + "commitTxFeerate" : 2500, + "toLocal" : 1000000000, + "toRemote" : 500000000 + }, + "txId" : "51747ac3f424543fa6577c50dca579fe03a5edf23d2a7f79348514e772048291", + "remoteSig" : { + "sig" : "1aba7bc42cb220e8b7b15b360bb93724aaa7d7095ec90a89f7307b22d1f654ea29deb3fcf4c17fd1e9fb387b2eb7bdaf21d788109376643bcbd4d7b78607b6d3" + }, + "htlcRemoteSigs" : [ ] + }, + "remoteCommitParams" : { + "dustLimit" : 1000, + "htlcMinimum" : 1000, + "maxHtlcValueInFlight" : 1000000000, + "maxAcceptedHtlcs" : 30, + "toSelfDelay" : 144 + }, + "remoteCommit" : { + "index" : 0, + "spec" : { + "htlcs" : [ ], + "commitTxFeerate" : 2500, + "toLocal" : 500000000, + "toRemote" : 1000000000 + }, + "txId" : "503e2b7aa61c4e8ecbc9c03cd7418cb2ea00633cbb120ab3e2090030bcef6c1e", + "remotePerCommitmentPoint" : "03bab000830e59155885a54ec35603e94ca7a380d98b61e988cc6430982335b100" + } + } +} \ No newline at end of file diff --git a/eclair-core/src/test/resources/nonreg/codecs/050004-DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED/fundee/data.bin b/eclair-core/src/test/resources/nonreg/codecs/050004-DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED/fundee/data.bin new file mode 100644 index 0000000000..fcfbe96e77 --- /dev/null +++ b/eclair-core/src/test/resources/nonreg/codecs/050004-DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED/fundee/data.bin @@ -0,0 +1 @@ +050004018b5505fe64ed55ab7d7505338be5e2895453eb113d68f0d9ca8ace01d1c2610a0101041000000002bbbb671d15145722fb8c28d732cddb249bcc6652ed2b297ff1f77a18371b1e6300097f030f827c089bb6a31adba66dfeccac6ac4a74e9fbc02d02ab1990059ca367e8000000000000000000000140800000000000000000000000000100822aa698202aaaa00ce2f18a967dc4f25f414e671ba6585f8ef0b8c5fb812c21064f55a2eaa00021b51b8d0d785d9c368fd445f9ed729c60f1402f41381ec088708c77b1dfa62aa028b32b2d2fc2dc23ed3af743c966a39039108ec1a4f68d863f9d9d989ef48b81003d5c9a694d89647ae561a4de51d831467f08a7cc4f43d432ef88094e6b2f97c150250882801587f6c1f3aa3109867e1e84ca7ec9465a65665e7786b47a47fa39d880000186b0200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000180822aa69820000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000024dfd62bb4858b9aa317728abbdce9c09b253a1b2500b915c2d788bf8cd2e079800000000000000000001312d003f71472cd0c2d74395079547236e963cf354f002d6857295a9fd0a65b9104f830020002000000000000000008220020b2a49083d8b4305389aa873a3de7f9c94523aa79e31da0198766702e09216cd7000000000ee6b280000000003b9aca0000000000000000000004010000000000000001520200000001689da343165e0aa12c34d8aba62e6eb0d851704b2e90d9f4ea5ee81ee3a0c20a0100000000000000000150f80c0000000000160014211e67f245e9aa786722bbf8541ae98d3c102d5c0000000000000000000000000100000000000000035e0200000001105b2d4e190cfe3bf6699a375d4cb7add361a8a97de89d9ff905d10683befac401000000000000000001305705000000000022512089bf33c5f3bab05cc845434013f55ecca01e191579d9067384ef4416dad70cbc000000000000000000000000010000000000000005520200000001ad771840e3275292a22a15fdfbd843c2837a1368a91aa76f756a280b8494bd2101000000000000000001c027090000000000160014211e67f245e9aa786722bbf8541ae98d3c102d5c000000000000000000000000010000000000000007520200000001b250a3d0f7d978517958291c17ed6d596635d9cfa25d1ebe8ab45bbf9689537601000000000000000001c027090000000000160014211e67f245e9aa786722bbf8541ae98d3c102d5c0000000000000000000000000004010000000000000000245893bf2927ac626567c8ec77db1ee05d3b648897af58c069adcbffe69538064b000000002be0c810000000000022512089bf33c5f3bab05cc845434013f55ecca01e191579d9067384ef4416dad70cbc000000000100000000000000022424b1de45db7f50866a4b23de5bb2ffcc6af814b165d43442ac2f4fb0a3b08564000000002be0c810000000000022512089bf33c5f3bab05cc845434013f55ecca01e191579d9067384ef4416dad70cbc00000000010000000000000004249501c025be40bd04a95cce7538642ad6d190957b6028a7e9548b31c1350d8fd9000000002be0c810000000000022512089bf33c5f3bab05cc845434013f55ecca01e191579d9067384ef4416dad70cbc00000000010000000000000006242713c75150fcb2ca9b479b2aa8b2e5ee9dad9492fcd1213833549ac0d9f448f4000000001fe0c8100000000000160014211e67f245e9aa786722bbf8541ae98d3c102d5c0000000000010100000000000000090000000000207ad622512089bf33c5f3bab05cc845434013f55ecca01e191579d9067384ef4416dad70cbc000101000000000000000a000000000033850922512089bf33c5f3bab05cc845434013f55ecca01e191579d9067384ef4416dad70cbc00000000fd01ce8b5505fe64ed55ab7d7505338be5e2895453eb113d68f0d9ca8ace01d1c2610adfd62bb4858b9aa317728abbdce9c09b253a1b2500b915c2d788bf8cd2e079800004006c024830450221009c1ce43e6e45d6d9c62db6fdc91be66c079de576bf69f339efdee5f7402335ba022009ff6d3fcaa362c9fefe6fb6cc611885344cbf68a5794ff66d4c772b3cb3fb440121032ca9aaa209ab7f512c8839b5a4bc88e66050b5094f0d9d62d5005bfe1101d62d0042014038a913277fe7b7a850a613936a0925f370051ec44775054e002f2a9fe29086b5b04f4e2023bead7816f90e9b721b03e2d19bb32befe3b7a3d7759a5113c282d6006b0247304402207c88d1c47313985efce945dac01e24c11d23bd984b16b48365915d1ecc260b9502205574e7cdbd1cf468bfbd1c2e24def19afbd612e47ad4d29e37e853db3ccd43b20121032ca9aaa209ab7f512c8839b5a4bc88e66050b5094f0d9d62d5005bfe1101d62d006b0247304402203dc933f3b98dfa7b9cbd70260d8d87db2c172ad71b54211152fa55152d3e2dac0220096064163ffafba04713fb168f12228c57e96d7349fc13319b8d5f1deb690ef70121032ca9aaa209ab7f512c8839b5a4bc88e66050b5094f0d9d62d5005bfe1101d62dfd017c8b5505fe64ed55ab7d7505338be5e2895453eb113d68f0d9ca8ace01d1c2610adfd62bb4858b9aa317728abbdce9c09b253a1b2500b915c2d788bf8cd2e0798000040042014030f02589752f32946a071fce36fc2648cbc1d4c0bfe3323095c264fd1a2f655b17697b12c3a947461dcfff008fdeae7dc6716fbf130321890d224466ebdebd7c00420140bde210774504d5ed0086bc22013ca2f0841791f68bd4804efa22c613fc9828ee76b0a252d9b5511110244e36d2b605c1daeae65a2c4182557b8033e158d79c40004201409e8d6557db46292249061d3804ae07435cb217bf45b5126bfaa4fdec649aa82b7e14fd0dab64fe74e9c3e2726542c344445e93a8022f88708cb53a262e8cab41006c024830450221008be06a8f7568ba5a526f4fd155d65b8276819b230f5a83897e5ce8b36486227102207c98e0156cae652cd258e93d6f8a243d39dae110625b30156dd726b0cf27940e0121032ca9aaa209ab7f512c8839b5a4bc88e66050b5094f0d9d62d5005bfe1101d62d0000061a808b5505fe64ed55ab7d7505338be5e2895453eb113d68f0d9ca8ace01d1c2610a00000000000003d09000000000000f42400003f71472cd0c2d74395079547236e963cf354f002d6857295a9fd0a65b9104f83000000200000000000000000000044c0000445c0000ff00000000000003d090000000000000222e0000000000000e10010200000000000003e800000000000003e8000000003b9aca00001e009000000000000000000000000009c4000000000fa324b0000000003ade57d0d7c4038d94da90de0f6d7a545b90333485229bb2dad55a182c94bb6921221569013b191ca883c1becfd540ec44a6be968a0b7d99130624df5fbcda0d3f57b77eb4776f581f94406c8480149ef7d7fb5aa843d2cd43d2013e231602086eaf677eb30000000000000000044c0000000000000000000000001dcd6500006402d000000000000000000000000009c4000000003ade57d0000000000fa324b0ba5e83eafb7f06b6f343b963fd07b4e58e0e94d1fed90be388e6a6646186c46d03041d0ccce284f7e89da8ef93b566ac3561768db5bd0ba6ca69f58ee8d45eea830000000000000000000000000024e3dcd51af888fd545bbba1b4d04d49059bcf5d01775ea231486d7228abfdcfaf0200000000000000001ab3f003f71472cd0c2d74395079547236e963cf354f002d6857295a9fd0a65b9104f830020002000000000000000008220020b2a49083d8b4305389aa873a3de7f9c94523aa79e31da0198766702e09216cd7000000002cb41780000000003b9aca0000000000000000000003010000000000000001520200000001ad771840e3275292a22a15fdfbd843c2837a1368a91aa76f756a280b8494bd2101000000000000000001c027090000000000160014211e67f245e9aa786722bbf8541ae98d3c102d5c000000000000000000000000010000000000000003520200000001b250a3d0f7d978517958291c17ed6d596635d9cfa25d1ebe8ab45bbf9689537601000000000000000001c027090000000000160014211e67f245e9aa786722bbf8541ae98d3c102d5c000000000000000000000000010000000000000005520200000001689da343165e0aa12c34d8aba62e6eb0d851704b2e90d9f4ea5ee81ee3a0c20a0100000000000000000150f80c0000000000160014211e67f245e9aa786722bbf8541ae98d3c102d5c00000000000000000000000000030100000000000000002424b1de45db7f50866a4b23de5bb2ffcc6af814b165d43442ac2f4fb0a3b08564000000002be0c810000000000022512089bf33c5f3bab05cc845434013f55ecca01e191579d9067384ef4416dad70cbc00000000010000000000000002242713c75150fcb2ca9b479b2aa8b2e5ee9dad9492fcd1213833549ac0d9f448f4000000001fe0c8100000000000160014211e67f245e9aa786722bbf8541ae98d3c102d5c00000000010000000000000004245893bf2927ac626567c8ec77db1ee05d3b648897af58c069adcbffe69538064b000000002be0c810000000000022512089bf33c5f3bab05cc845434013f55ecca01e191579d9067384ef4416dad70cbc0000000000010100000000000000070000000000139ec1160014211e67f245e9aa786722bbf8541ae98d3c102d5c0001010000000000000006000000000022d6cf22512089bf33c5f3bab05cc845434013f55ecca01e191579d9067384ef4416dad70cbc00000000fd018a8b5505fe64ed55ab7d7505338be5e2895453eb113d68f0d9ca8ace01d1c2610ae3dcd51af888fd545bbba1b4d04d49059bcf5d01775ea231486d7228abfdcfaf0003006b0247304402200f7994ebe9e143d818b4ba09332509253cbb34e96f207d27aa179105df611b3d022062a1ca089197faa6122fee7a9c601a5b839ccd3eb493c5900c864f0449dec91d0121032ca9aaa209ab7f512c8839b5a4bc88e66050b5094f0d9d62d5005bfe1101d62d006c02483045022100a9f976454dd706ac99758e5436f3adc43e6b70390fd5dbab0eb3e1dd59ccb5cf02202477a7fda904388c54ab102f237070cc50f91db616d1c15e9e8611c3a78930560121032ca9aaa209ab7f512c8839b5a4bc88e66050b5094f0d9d62d5005bfe1101d62d006b024730440220769934b760840adae7acd94644c12315b2da5d98380f7e8b912b2bd7a69b3e170220284ebbf8770c66ae8405eacf37110d3cca88ea5b9968523d3026aa0f3b2945eb0121032ca9aaa209ab7f512c8839b5a4bc88e66050b5094f0d9d62d5005bfe1101d62dfd01388b5505fe64ed55ab7d7505338be5e2895453eb113d68f0d9ca8ace01d1c2610ae3dcd51af888fd545bbba1b4d04d49059bcf5d01775ea231486d7228abfdcfaf0003004201403a50d5912fb88e5a3478b8deabe192c9124b86062d39b57d40201c619bd58c566d422aab04b3d7c9917b6db3f87ee05c61b3b50ea9ec28a32228c960fd96fba4006c0248304502210099f5de689296514a0430dd4aa096c4e56a54c19d1fcd69bc3b8e35f7a735b40f0220264e7fc46ff4b2c4a8cf8526ef2f3a2f97f8f12ba1ceb961b8210370fe42a0ca0121032ca9aaa209ab7f512c8839b5a4bc88e66050b5094f0d9d62d5005bfe1101d62d00420140cab9ce20db34bc913f4ff6671e8c55b2b589294b7106747e5ba15e3354553f742deedbac3e9c2423458848395dbd5f303a3009ce3aa6918e41a5464acc4471a20000061a808b5505fe64ed55ab7d7505338be5e2895453eb113d68f0d9ca8ace01d1c2610a0000000000000b71b000000000000f42400003f71472cd0c2d74395079547236e963cf354f002d6857295a9fd0a65b9104f83000000200000000000000000000044c00003a980000ff0000000000000b71b00000000000001d4c0000000000002198010200000000000003e800000000000003e8000000003b9aca00001e009000000000000000000000000009c4000000002da9c220000000003aa51f60d19ab71cca21e7bbc60949b9003491aa95db1b928ff58eaac7a9c601ab7a6b8e019fa2f9aeba09f73f5f48f916e7271e2d69fc8bdd2cd093d8cc43b820db6fc7653f5a7c429d741cf15ba9102247fb53c0453a8e257514daf1b35b1fd7a2c8e5de0000000000000000044c0000000000000000000000001dcd6500006402d000000000000000000000000009c4000000003aa51f60000000002da9c220021a43ae2d2d01dbb79d9dbf3860da3c669fd1cfc800598847fc8de55eafd6ce03041d0ccce284f7e89da8ef93b566ac3561768db5bd0ba6ca69f58ee8d45eea830000000000000000000000000024e47d8dab3baf300aac3f9c183827b7e6faa7cd95dc3609c4608e4bede72e706f00000000000000000016e36003f71472cd0c2d74395079547236e963cf354f002d6857295a9fd0a65b9104f830020002000000000000000004220020b2a49083d8b4305389aa873a3de7f9c94523aa79e31da0198766702e09216cd7000000001dcd6500000000003b9aca0000000000000000000002010000000000000001520200000001ad771840e3275292a22a15fdfbd843c2837a1368a91aa76f756a280b8494bd2101000000000000000001c027090000000000160014211e67f245e9aa786722bbf8541ae98d3c102d5c000000000000000000000000010000000000000003520200000001b250a3d0f7d978517958291c17ed6d596635d9cfa25d1ebe8ab45bbf9689537601000000000000000001c027090000000000160014211e67f245e9aa786722bbf8541ae98d3c102d5c0000000000000000000000000002010000000000000000245893bf2927ac626567c8ec77db1ee05d3b648897af58c069adcbffe69538064b000000002be0c810000000000022512089bf33c5f3bab05cc845434013f55ecca01e191579d9067384ef4416dad70cbc00000000010000000000000002242713c75150fcb2ca9b479b2aa8b2e5ee9dad9492fcd1213833549ac0d9f448f4000000001fe0c8100000000000160014211e67f245e9aa786722bbf8541ae98d3c102d5c00000000000101000000000000000500000000000a8d90160014211e67f245e9aa786722bbf8541ae98d3c102d5c0001010000000000000006000000000012241822512089bf33c5f3bab05cc845434013f55ecca01e191579d9067384ef4416dad70cbc00000000fd011e8b5505fe64ed55ab7d7505338be5e2895453eb113d68f0d9ca8ace01d1c2610ae47d8dab3baf300aac3f9c183827b7e6faa7cd95dc3609c4608e4bede72e706f0002006c02483045022100dded09d52b6ec25ce729d758e66c01c5f72383e193bdd58807a175a62983c75e022004332808ab1bab2d39248823f7ec760576c4d64d9bf1ff333acddbeb9b0e8fb50121032ca9aaa209ab7f512c8839b5a4bc88e66050b5094f0d9d62d5005bfe1101d62d006c024830450221009fde5a1de38b8bce5206207136a675bcb568c259c6f925dfc273956be8b671e902201a83903e497aea812e7991a2a514185ea195cdd3c0e78159eef055da220265030121032ca9aaa209ab7f512c8839b5a4bc88e66050b5094f0d9d62d5005bfe1101d62df38b5505fe64ed55ab7d7505338be5e2895453eb113d68f0d9ca8ace01d1c2610ae47d8dab3baf300aac3f9c183827b7e6faa7cd95dc3609c4608e4bede72e706f000200420140c18bd07ab32f5c2ee6dcbc4e6fc097060c6339579ebba7086c402b01cccb6aa34f396691edfa5ba60d0e3bac30589b257d12e428b40ef1309bdb0373bff03015006b024730440220466b46139edab96c0721dd803b11cc60a0289697ecdf2041baeeea3597a95c60022072b4beebca1f4b33367f8e436c50581ca08e0a3a15d1cd1c741651741216ac510121032ca9aaa209ab7f512c8839b5a4bc88e66050b5094f0d9d62d5005bfe1101d62d0000061a808b5505fe64ed55ab7d7505338be5e2895453eb113d68f0d9ca8ace01d1c2610a00000000000007a12000000000000f42400003f71472cd0c2d74395079547236e963cf354f002d6857295a9fd0a65b9104f83000000200000000000000000000044c000030d40000ff00000000000007a120000000000000186a00000000000017d4010200000000000003e800000000000003e8000000003b9aca00001e009000000000000000000000000009c4000000001e89d730000000003ade57d0624b66b127057abbc7c74fd70c4a66de86be773e4ef25500881e9f31c897d1ff0183a1d18b9e30c353513ec1d7228876eb89e97051e295521615c3afbcee5ae99a0c04770f4f909646afee1aaaaed34286355f05fbc7eb554c23666e133e82891a0000000000000000044c0000000000000000000000001dcd6500006402d000000000000000000000000009c4000000003ade57d0000000001e89d73056768c50777aa0a60664aeab43f6bba0a1410eeab680669dfbda438f217ba81603041d0ccce284f7e89da8ef93b566ac3561768db5bd0ba6ca69f58ee8d45eea83000000000000000000000000002452fadbb700e2090f7bcba950c1d891c41107edfd42ac98836bff8c3b3647d87200000000000000000016e36003f71472cd0c2d74395079547236e963cf354f002d6857295a9fd0a65b9104f830020002000000000000000002220020b2a49083d8b4305389aa873a3de7f9c94523aa79e31da0198766702e09216cd7000000001dcd6500000000003b9aca0000000000000000000001010000000000000001520200000001ad771840e3275292a22a15fdfbd843c2837a1368a91aa76f756a280b8494bd2101000000000000000001c027090000000000160014211e67f245e9aa786722bbf8541ae98d3c102d5c0000000000000000000000000001010000000000000000242713c75150fcb2ca9b479b2aa8b2e5ee9dad9492fcd1213833549ac0d9f448f4000000001fe0c8100000000000160014211e67f245e9aa786722bbf8541ae98d3c102d5c000000000001010000000000000003000000000001770a160014211e67f245e9aa786722bbf8541ae98d3c102d5c00010100000000000000040000000000016ec2160014211e67f245e9aa786722bbf8541ae98d3c102d5c00061a80b08b5505fe64ed55ab7d7505338be5e2895453eb113d68f0d9ca8ace01d1c2610a52fadbb700e2090f7bcba950c1d891c41107edfd42ac98836bff8c3b3647d8720001006c02483045022100b7728ca8313b75ad74a597e2424d3d064621dbe753051d031c7054194389b0a002200dd0f2598ad26a8eb01c343b68404986ac7568d3e04cbb7e972bf60d816b30030121032ca9aaa209ab7f512c8839b5a4bc88e66050b5094f0d9d62d5005bfe1101d62daf8b5505fe64ed55ab7d7505338be5e2895453eb113d68f0d9ca8ace01d1c2610a52fadbb700e2090f7bcba950c1d891c41107edfd42ac98836bff8c3b3647d8720001006b0247304402206f944130e33be429f292b5b523166365bc995cee7e787cca4b3c91788ee5a1bb022024f8e7dc22eed3a55b60ebbbf082355d2a6e837136176f6cad22e2f42167e84a0121032ca9aaa209ab7f512c8839b5a4bc88e66050b5094f0d9d62d5005bfe1101d62d0000061a808b5505fe64ed55ab7d7505338be5e2895453eb113d68f0d9ca8ace01d1c2610a00000000000007a12000000000000f42400003f71472cd0c2d74395079547236e963cf354f002d6857295a9fd0a65b9104f83000000200061a80000000000000044c000027100000ff00000000000007a120000000000000138800000000000017d4010200000000000003e800000000000003e8000000003b9aca00001e009000000000000000000000000009c4000000001e76c460000000003af16aa0d21bfaa4fd823795583f4760ec87d8fcdab7eef75dd7e8a88b45bde9b5d6bf130175430684a3cd0bd03132328da55c77aebaca125f403cfac9dbc6d774b9bbbd7e334addf9703208c96c301cc7cf67a460bd629fd05ae992aaf5a8c9d1cc50822b0000000000000000044c0000000000000000000000001dcd6500006402d000000000000000000000000009c4000000003af16aa0000000001e76c460283ecb8bd6b29edbbae369f17c996dae91ff2cc85c42ad7c3f9f7ec8896577ff03041d0ccce284f7e89da8ef93b566ac3561768db5bd0ba6ca69f58ee8d45eea83000000ff038019cc4175a000ffac250cab41e9eda28e8aecc628a770a7146784ad51f20d2b0000000000000000000000000000000000000000000000061a8000061a800100 \ No newline at end of file diff --git a/eclair-core/src/test/resources/nonreg/codecs/050004-DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED/fundee/data.json b/eclair-core/src/test/resources/nonreg/codecs/050004-DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED/fundee/data.json new file mode 100644 index 0000000000..ebbbb2e890 --- /dev/null +++ b/eclair-core/src/test/resources/nonreg/codecs/050004-DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED/fundee/data.json @@ -0,0 +1,296 @@ +{ + "type" : "DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED", + "commitments" : { + "channelParams" : { + "channelId" : "8b5505fe64ed55ab7d7505338be5e2895453eb113d68f0d9ca8ace01d1c2610a", + "channelConfig" : [ "funding_pubkey_based_channel_keypath" ], + "channelFeatures" : [ "option_dual_fund" ], + "localParams" : { + "nodeId" : "02bbbb671d15145722fb8c28d732cddb249bcc6652ed2b297ff1f77a18371b1e63", + "fundingKeyPath" : [ 2130907010, 2080938934, 2736446374, 1845415084, 1791272782, 2679898832, 716282112, 1506424446, 2147483648 ], + "isChannelOpener" : false, + "paysCommitTxFees" : false, + "initFeatures" : { + "activated" : { + "option_route_blinding" : "optional", + "option_dual_fund" : "optional", + "splice_prototype" : "optional", + "payment_secret" : "mandatory", + "gossip_queries_ex" : "optional", + "option_anchor_outputs" : "optional", + "option_quiesce" : "optional", + "option_data_loss_protect" : "optional", + "var_onion_optin" : "mandatory", + "option_static_remotekey" : "optional", + "option_support_large_channel" : "optional", + "option_anchors_zero_fee_htlc_tx" : "optional", + "option_channel_type" : "mandatory", + "basic_mpp" : "optional", + "gossip_queries" : "optional" + }, + "unknown" : [ ] + } + }, + "remoteParams" : { + "nodeId" : "02aaaa00ce2f18a967dc4f25f414e671ba6585f8ef0b8c5fb812c21064f55a2eaa", + "revocationBasepoint" : "021b51b8d0d785d9c368fd445f9ed729c60f1402f41381ec088708c77b1dfa62aa", + "paymentBasepoint" : "028b32b2d2fc2dc23ed3af743c966a39039108ec1a4f68d863f9d9d989ef48b810", + "delayedPaymentBasepoint" : "03d5c9a694d89647ae561a4de51d831467f08a7cc4f43d432ef88094e6b2f97c15", + "htlcBasepoint" : "0250882801587f6c1f3aa3109867e1e84ca7ec9465a65665e7786b47a47fa39d88", + "initFeatures" : { + "activated" : { + "option_route_blinding" : "optional", + "option_provide_storage" : "optional", + "option_dual_fund" : "optional", + "splice_prototype" : "optional", + "payment_secret" : "mandatory", + "gossip_queries_ex" : "optional", + "option_anchor_outputs" : "optional", + "option_quiesce" : "optional", + "option_data_loss_protect" : "optional", + "var_onion_optin" : "mandatory", + "option_static_remotekey" : "optional", + "option_support_large_channel" : "optional", + "option_anchors_zero_fee_htlc_tx" : "optional", + "option_channel_type" : "mandatory", + "basic_mpp" : "optional", + "gossip_queries" : "optional" + }, + "unknown" : [ 50001 ] + } + }, + "channelFlags" : { + "nonInitiatorPaysCommitFees" : false, + "announceChannel" : false + } + }, + "changes" : { + "localChanges" : { + "proposed" : [ ], + "signed" : [ ], + "acked" : [ ] + }, + "remoteChanges" : { + "proposed" : [ ], + "acked" : [ ], + "signed" : [ ] + }, + "localNextHtlcId" : 0, + "remoteNextHtlcId" : 0 + }, + "active" : [ { + "fundingTxIndex" : 0, + "fundingInput" : "8079e0d28cbf88d7c215b900251b3a259bc0e9dcbb8a7217a39a8b85b42bd6df:0", + "fundingAmount" : 1250000, + "localFunding" : { + "status" : "unconfirmed", + "txid" : "8079e0d28cbf88d7c215b900251b3a259bc0e9dcbb8a7217a39a8b85b42bd6df" + }, + "remoteFunding" : { + "status" : "not-locked" + }, + "commitmentFormat" : "anchor_outputs", + "localCommitParams" : { + "dustLimit" : 1000, + "htlcMinimum" : 1000, + "maxHtlcValueInFlight" : 1000000000, + "maxAcceptedHtlcs" : 30, + "toSelfDelay" : 144 + }, + "localCommit" : { + "index" : 0, + "spec" : { + "htlcs" : [ ], + "commitTxFeerate" : 2500, + "toLocal" : 262350000, + "toRemote" : 987650000 + }, + "txId" : "d7c4038d94da90de0f6d7a545b90333485229bb2dad55a182c94bb6921221569", + "remoteSig" : { + "sig" : "3b191ca883c1becfd540ec44a6be968a0b7d99130624df5fbcda0d3f57b77eb4776f581f94406c8480149ef7d7fb5aa843d2cd43d2013e231602086eaf677eb3" + }, + "htlcRemoteSigs" : [ ] + }, + "remoteCommitParams" : { + "dustLimit" : 1100, + "htlcMinimum" : 0, + "maxHtlcValueInFlight" : 500000000, + "maxAcceptedHtlcs" : 100, + "toSelfDelay" : 720 + }, + "remoteCommit" : { + "index" : 0, + "spec" : { + "htlcs" : [ ], + "commitTxFeerate" : 2500, + "toLocal" : 987650000, + "toRemote" : 262350000 + }, + "txId" : "ba5e83eafb7f06b6f343b963fd07b4e58e0e94d1fed90be388e6a6646186c46d", + "remotePerCommitmentPoint" : "03041d0ccce284f7e89da8ef93b566ac3561768db5bd0ba6ca69f58ee8d45eea83" + } + }, { + "fundingTxIndex" : 0, + "fundingInput" : "afcffdab28726d4831a25e77015dcf9b05494dd0b4a1bb5b54fd88f81ad5dce3:2", + "fundingAmount" : 1750000, + "localFunding" : { + "status" : "unconfirmed", + "txid" : "afcffdab28726d4831a25e77015dcf9b05494dd0b4a1bb5b54fd88f81ad5dce3" + }, + "remoteFunding" : { + "status" : "not-locked" + }, + "commitmentFormat" : "anchor_outputs", + "localCommitParams" : { + "dustLimit" : 1000, + "htlcMinimum" : 1000, + "maxHtlcValueInFlight" : 1000000000, + "maxAcceptedHtlcs" : 30, + "toSelfDelay" : 144 + }, + "localCommit" : { + "index" : 0, + "spec" : { + "htlcs" : [ ], + "commitTxFeerate" : 2500, + "toLocal" : 766100000, + "toRemote" : 983900000 + }, + "txId" : "d19ab71cca21e7bbc60949b9003491aa95db1b928ff58eaac7a9c601ab7a6b8e", + "remoteSig" : { + "sig" : "9fa2f9aeba09f73f5f48f916e7271e2d69fc8bdd2cd093d8cc43b820db6fc7653f5a7c429d741cf15ba9102247fb53c0453a8e257514daf1b35b1fd7a2c8e5de" + }, + "htlcRemoteSigs" : [ ] + }, + "remoteCommitParams" : { + "dustLimit" : 1100, + "htlcMinimum" : 0, + "maxHtlcValueInFlight" : 500000000, + "maxAcceptedHtlcs" : 100, + "toSelfDelay" : 720 + }, + "remoteCommit" : { + "index" : 0, + "spec" : { + "htlcs" : [ ], + "commitTxFeerate" : 2500, + "toLocal" : 983900000, + "toRemote" : 766100000 + }, + "txId" : "021a43ae2d2d01dbb79d9dbf3860da3c669fd1cfc800598847fc8de55eafd6ce", + "remotePerCommitmentPoint" : "03041d0ccce284f7e89da8ef93b566ac3561768db5bd0ba6ca69f58ee8d45eea83" + } + }, { + "fundingTxIndex" : 0, + "fundingInput" : "6f702ee7ed4b8e60c40936dc95cda7fae6b72738189c3fac0a30af3bab8d7de4:0", + "fundingAmount" : 1500000, + "localFunding" : { + "status" : "unconfirmed", + "txid" : "6f702ee7ed4b8e60c40936dc95cda7fae6b72738189c3fac0a30af3bab8d7de4" + }, + "remoteFunding" : { + "status" : "not-locked" + }, + "commitmentFormat" : "anchor_outputs", + "localCommitParams" : { + "dustLimit" : 1000, + "htlcMinimum" : 1000, + "maxHtlcValueInFlight" : 1000000000, + "maxAcceptedHtlcs" : 30, + "toSelfDelay" : 144 + }, + "localCommit" : { + "index" : 0, + "spec" : { + "htlcs" : [ ], + "commitTxFeerate" : 2500, + "toLocal" : 512350000, + "toRemote" : 987650000 + }, + "txId" : "624b66b127057abbc7c74fd70c4a66de86be773e4ef25500881e9f31c897d1ff", + "remoteSig" : { + "sig" : "83a1d18b9e30c353513ec1d7228876eb89e97051e295521615c3afbcee5ae99a0c04770f4f909646afee1aaaaed34286355f05fbc7eb554c23666e133e82891a" + }, + "htlcRemoteSigs" : [ ] + }, + "remoteCommitParams" : { + "dustLimit" : 1100, + "htlcMinimum" : 0, + "maxHtlcValueInFlight" : 500000000, + "maxAcceptedHtlcs" : 100, + "toSelfDelay" : 720 + }, + "remoteCommit" : { + "index" : 0, + "spec" : { + "htlcs" : [ ], + "commitTxFeerate" : 2500, + "toLocal" : 987650000, + "toRemote" : 512350000 + }, + "txId" : "56768c50777aa0a60664aeab43f6bba0a1410eeab680669dfbda438f217ba816", + "remotePerCommitmentPoint" : "03041d0ccce284f7e89da8ef93b566ac3561768db5bd0ba6ca69f58ee8d45eea83" + } + }, { + "fundingTxIndex" : 0, + "fundingInput" : "72d847363b8cff6b8398ac42fded0711c491d8c150a9cb7b0f09e200b7dbfa52:0", + "fundingAmount" : 1500000, + "localFunding" : { + "status" : "unconfirmed", + "txid" : "72d847363b8cff6b8398ac42fded0711c491d8c150a9cb7b0f09e200b7dbfa52" + }, + "remoteFunding" : { + "status" : "not-locked" + }, + "commitmentFormat" : "anchor_outputs", + "localCommitParams" : { + "dustLimit" : 1000, + "htlcMinimum" : 1000, + "maxHtlcValueInFlight" : 1000000000, + "maxAcceptedHtlcs" : 30, + "toSelfDelay" : 144 + }, + "localCommit" : { + "index" : 0, + "spec" : { + "htlcs" : [ ], + "commitTxFeerate" : 2500, + "toLocal" : 511100000, + "toRemote" : 988900000 + }, + "txId" : "d21bfaa4fd823795583f4760ec87d8fcdab7eef75dd7e8a88b45bde9b5d6bf13", + "remoteSig" : { + "sig" : "75430684a3cd0bd03132328da55c77aebaca125f403cfac9dbc6d774b9bbbd7e334addf9703208c96c301cc7cf67a460bd629fd05ae992aaf5a8c9d1cc50822b" + }, + "htlcRemoteSigs" : [ ] + }, + "remoteCommitParams" : { + "dustLimit" : 1100, + "htlcMinimum" : 0, + "maxHtlcValueInFlight" : 500000000, + "maxAcceptedHtlcs" : 100, + "toSelfDelay" : 720 + }, + "remoteCommit" : { + "index" : 0, + "spec" : { + "htlcs" : [ ], + "commitTxFeerate" : 2500, + "toLocal" : 988900000, + "toRemote" : 511100000 + }, + "txId" : "283ecb8bd6b29edbbae369f17c996dae91ff2cc85c42ad7c3f9f7ec8896577ff", + "remotePerCommitmentPoint" : "03041d0ccce284f7e89da8ef93b566ac3561768db5bd0ba6ca69f58ee8d45eea83" + } + } ], + "inactive" : [ ], + "remoteNextCommitInfo" : "038019cc4175a000ffac250cab41e9eda28e8aecc628a770a7146784ad51f20d2b", + "remotePerCommitmentSecrets" : null, + "originChannels" : { } + }, + "localPushAmount" : 0, + "remotePushAmount" : 0, + "waitingSince" : 400000, + "lastChecked" : 400000, + "status" : { } +} \ No newline at end of file diff --git a/eclair-core/src/test/resources/nonreg/codecs/050004-DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED/funder/data.bin b/eclair-core/src/test/resources/nonreg/codecs/050004-DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED/funder/data.bin new file mode 100644 index 0000000000..5ac0a96d1b --- /dev/null +++ b/eclair-core/src/test/resources/nonreg/codecs/050004-DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED/funder/data.bin @@ -0,0 +1 @@ +050004018b5505fe64ed55ab7d7505338be5e2895453eb113d68f0d9ca8ace01d1c2610a0101041000000002aaaa00ce2f18a967dc4f25f414e671ba6585f8ef0b8c5fb812c21064f55a2eaa000975a245c226f5be811ebf27c01e7f6353477b2a8905789634272c5342f95b5d3a8000000100c000000000186b0200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000180822aa698202bbbb671d15145722fb8c28d732cddb249bcc6652ed2b297ff1f77a18371b1e630002c152c7a43051740ef1b8fa5936f4866bca5ca740b4589b202f60692b6c30c114021485935cf03bb4f568e2d33e23fefe36cb6ad1d2ff57a50a609a43e36d484df7039c9096f7f6bda74572664aacdacd04ff7dbeb20bfa48ac169271724fb9aafb390357bf0ceb7a3fc5cf65d076910235a0d9abcce1087414865ad3374a08bcbd023d000000140800000000000000000000000000100822aa69820000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000024dfd62bb4858b9aa317728abbdce9c09b253a1b2500b915c2d788bf8cd2e079800000000000000000001312d0039fba7bef02ba45a17b5d10799430c9b68181b764ceb7f32c9f0c59c0af521e28020002000000000000000008220020b2a49083d8b4305389aa873a3de7f9c94523aa79e31da0198766702e09216cd7000000003b9aca00000000000ee6b280000000000000000000040100000000000000005e02000000012431730de5f724b1609e576f23b4d4755ee8771219a46064650c89a2f10db1c201000000000000000001e0c810000000000022512089bf33c5f3bab05cc845434013f55ecca01e191579d9067384ef4416dad70cbc0000000000000000000000000100000000000000025e0200000001d916ac482ea1dd1a734a8f04a68347b05a4a21f4df69b3ff7331a66c6e2705fc01000000000000000001e0c810000000000022512089bf33c5f3bab05cc845434013f55ecca01e191579d9067384ef4416dad70cbc0000000000000000000000000100000000000000045e02000000019c5db1026c6788a69901b5bbbacac7e8fa94ebd20818933a57a78ef7739f87f801000000000000000001e0c810000000000022512089bf33c5f3bab05cc845434013f55ecca01e191579d9067384ef4416dad70cbc000000000000000000000000010000000000000006520200000001658a8f58c53d1dbd8059d208b23d162e4ae22057c9da8639ce9220715dd1029601000000000000000001e0c8100000000000160014211e67f245e9aa786722bbf8541ae98d3c102d5c000000000000000000000000000401000000000000000124004a7b606284e503f5ae8fd18761903e9d5d140f5079197cbb9adc25e8a07fca000000001f50f80c0000000000160014211e67f245e9aa786722bbf8541ae98d3c102d5c0000000001000000000000000324433ad01cc871d52c5552c77ac593a9d05ddc55afc15421acef59c27ff281717c000000002b305705000000000022512089bf33c5f3bab05cc845434013f55ecca01e191579d9067384ef4416dad70cbc0000000001000000000000000524e2ed2c90027959c4f0d54df6da2372a0ed3bd078444e6967412cc3d11b7b6e0a000000001fc027090000000000160014211e67f245e9aa786722bbf8541ae98d3c102d5c0000000001000000000000000724a2b2dd4e22a391be5e29d9a263881b4b50f25720a4c01a37c8211f4a6f24125d000000001fc027090000000000160014211e67f245e9aa786722bbf8541ae98d3c102d5c00000000000101000000000000000a000000000033850922512089bf33c5f3bab05cc845434013f55ecca01e191579d9067384ef4416dad70cbc00010100000000000000090000000000207ad622512089bf33c5f3bab05cc845434013f55ecca01e191579d9067384ef4416dad70cbc00000000fd017c8b5505fe64ed55ab7d7505338be5e2895453eb113d68f0d9ca8ace01d1c2610adfd62bb4858b9aa317728abbdce9c09b253a1b2500b915c2d788bf8cd2e0798000040042014030f02589752f32946a071fce36fc2648cbc1d4c0bfe3323095c264fd1a2f655b17697b12c3a947461dcfff008fdeae7dc6716fbf130321890d224466ebdebd7c00420140bde210774504d5ed0086bc22013ca2f0841791f68bd4804efa22c613fc9828ee76b0a252d9b5511110244e36d2b605c1daeae65a2c4182557b8033e158d79c40004201409e8d6557db46292249061d3804ae07435cb217bf45b5126bfaa4fdec649aa82b7e14fd0dab64fe74e9c3e2726542c344445e93a8022f88708cb53a262e8cab41006c024830450221008be06a8f7568ba5a526f4fd155d65b8276819b230f5a83897e5ce8b36486227102207c98e0156cae652cd258e93d6f8a243d39dae110625b30156dd726b0cf27940e0121032ca9aaa209ab7f512c8839b5a4bc88e66050b5094f0d9d62d5005bfe1101d62dfd01ce8b5505fe64ed55ab7d7505338be5e2895453eb113d68f0d9ca8ace01d1c2610adfd62bb4858b9aa317728abbdce9c09b253a1b2500b915c2d788bf8cd2e079800004006c024830450221009c1ce43e6e45d6d9c62db6fdc91be66c079de576bf69f339efdee5f7402335ba022009ff6d3fcaa362c9fefe6fb6cc611885344cbf68a5794ff66d4c772b3cb3fb440121032ca9aaa209ab7f512c8839b5a4bc88e66050b5094f0d9d62d5005bfe1101d62d0042014038a913277fe7b7a850a613936a0925f370051ec44775054e002f2a9fe29086b5b04f4e2023bead7816f90e9b721b03e2d19bb32befe3b7a3d7759a5113c282d6006b0247304402207c88d1c47313985efce945dac01e24c11d23bd984b16b48365915d1ecc260b9502205574e7cdbd1cf468bfbd1c2e24def19afbd612e47ad4d29e37e853db3ccd43b20121032ca9aaa209ab7f512c8839b5a4bc88e66050b5094f0d9d62d5005bfe1101d62d006b0247304402203dc933f3b98dfa7b9cbd70260d8d87db2c172ad71b54211152fa55152d3e2dac0220096064163ffafba04713fb168f12228c57e96d7349fc13319b8d5f1deb690ef70121032ca9aaa209ab7f512c8839b5a4bc88e66050b5094f0d9d62d5005bfe1101d62d0000061a808b5505fe64ed55ab7d7505338be5e2895453eb113d68f0d9ca8ace01d1c2610aff00000000000f4240000000000003d09000039fba7bef02ba45a17b5d10799430c9b68181b764ceb7f32c9f0c59c0af521e2800000200000000000000000000044c0000445c0000ffff000000000003d090000000000000222e0000000000000e100102000000000000044c0000000000000000000000001dcd6500006402d000000000000000000000000009c4000000003ade57d0000000000fa324b0ba5e83eafb7f06b6f343b963fd07b4e58e0e94d1fed90be388e6a6646186c46d0193c5f1ce1caf79cd2f314572562d4e8eefecf7559aa953a034c88f70ebce6e0242032a4e5787591197974af6ed0a99113f5ce9e469425dfe2f5cab5507c732b8000000000000000003e800000000000003e8000000003b9aca00001e009000000000000000000000000009c4000000000fa324b0000000003ade57d0d7c4038d94da90de0f6d7a545b90333485229bb2dad55a182c94bb692122156902bde57fb815af918e1373c8443239667255e14fd3a1d7484647c51268a01bc1330000000000000000000000000024e3dcd51af888fd545bbba1b4d04d49059bcf5d01775ea231486d7228abfdcfaf0200000000000000001ab3f0039fba7bef02ba45a17b5d10799430c9b68181b764ceb7f32c9f0c59c0af521e28020002000000000000000008220020b2a49083d8b4305389aa873a3de7f9c94523aa79e31da0198766702e09216cd7000000003b9aca00000000002cb41780000000000000000000030100000000000000005e0200000001d916ac482ea1dd1a734a8f04a68347b05a4a21f4df69b3ff7331a66c6e2705fc01000000000000000001e0c810000000000022512089bf33c5f3bab05cc845434013f55ecca01e191579d9067384ef4416dad70cbc000000000000000000000000010000000000000002520200000001658a8f58c53d1dbd8059d208b23d162e4ae22057c9da8639ce9220715dd1029601000000000000000001e0c8100000000000160014211e67f245e9aa786722bbf8541ae98d3c102d5c0000000000000000000000000100000000000000045e02000000012431730de5f724b1609e576f23b4d4755ee8771219a46064650c89a2f10db1c201000000000000000001e0c810000000000022512089bf33c5f3bab05cc845434013f55ecca01e191579d9067384ef4416dad70cbc000000000000000000000000000301000000000000000124e2ed2c90027959c4f0d54df6da2372a0ed3bd078444e6967412cc3d11b7b6e0a000000001fc027090000000000160014211e67f245e9aa786722bbf8541ae98d3c102d5c0000000001000000000000000324a2b2dd4e22a391be5e29d9a263881b4b50f25720a4c01a37c8211f4a6f24125d000000001fc027090000000000160014211e67f245e9aa786722bbf8541ae98d3c102d5c0000000001000000000000000524004a7b606284e503f5ae8fd18761903e9d5d140f5079197cbb9adc25e8a07fca000000001f50f80c0000000000160014211e67f245e9aa786722bbf8541ae98d3c102d5c000000000001010000000000000006000000000022d6cf22512089bf33c5f3bab05cc845434013f55ecca01e191579d9067384ef4416dad70cbc00010100000000000000070000000000139ec1160014211e67f245e9aa786722bbf8541ae98d3c102d5c00000000fd01388b5505fe64ed55ab7d7505338be5e2895453eb113d68f0d9ca8ace01d1c2610ae3dcd51af888fd545bbba1b4d04d49059bcf5d01775ea231486d7228abfdcfaf0003004201403a50d5912fb88e5a3478b8deabe192c9124b86062d39b57d40201c619bd58c566d422aab04b3d7c9917b6db3f87ee05c61b3b50ea9ec28a32228c960fd96fba4006c0248304502210099f5de689296514a0430dd4aa096c4e56a54c19d1fcd69bc3b8e35f7a735b40f0220264e7fc46ff4b2c4a8cf8526ef2f3a2f97f8f12ba1ceb961b8210370fe42a0ca0121032ca9aaa209ab7f512c8839b5a4bc88e66050b5094f0d9d62d5005bfe1101d62d00420140cab9ce20db34bc913f4ff6671e8c55b2b589294b7106747e5ba15e3354553f742deedbac3e9c2423458848395dbd5f303a3009ce3aa6918e41a5464acc4471a2fd018a8b5505fe64ed55ab7d7505338be5e2895453eb113d68f0d9ca8ace01d1c2610ae3dcd51af888fd545bbba1b4d04d49059bcf5d01775ea231486d7228abfdcfaf0003006b0247304402200f7994ebe9e143d818b4ba09332509253cbb34e96f207d27aa179105df611b3d022062a1ca089197faa6122fee7a9c601a5b839ccd3eb493c5900c864f0449dec91d0121032ca9aaa209ab7f512c8839b5a4bc88e66050b5094f0d9d62d5005bfe1101d62d006c02483045022100a9f976454dd706ac99758e5436f3adc43e6b70390fd5dbab0eb3e1dd59ccb5cf02202477a7fda904388c54ab102f237070cc50f91db616d1c15e9e8611c3a78930560121032ca9aaa209ab7f512c8839b5a4bc88e66050b5094f0d9d62d5005bfe1101d62d006b024730440220769934b760840adae7acd94644c12315b2da5d98380f7e8b912b2bd7a69b3e170220284ebbf8770c66ae8405eacf37110d3cca88ea5b9968523d3026aa0f3b2945eb0121032ca9aaa209ab7f512c8839b5a4bc88e66050b5094f0d9d62d5005bfe1101d62d0000061a808b5505fe64ed55ab7d7505338be5e2895453eb113d68f0d9ca8ace01d1c2610aff00000000000f424000000000000b71b000039fba7bef02ba45a17b5d10799430c9b68181b764ceb7f32c9f0c59c0af521e2800000200000000000000000000044c00003a980000ffff00000000000b71b00000000000001d4c00000000000021980102000000000000044c0000000000000000000000001dcd6500006402d000000000000000000000000009c4000000003aa51f60000000002da9c220021a43ae2d2d01dbb79d9dbf3860da3c669fd1cfc800598847fc8de55eafd6ce01e11b7a3726e1bfc570ffb4ac9bb4e531576a100c0a8267898b2ad0215ad179a400aed248c419209a768f6ced19dbbb8598a68695c2cb310e05c734dce8f891b7000000000000000003e800000000000003e8000000003b9aca00001e009000000000000000000000000009c4000000002da9c220000000003aa51f60d19ab71cca21e7bbc60949b9003491aa95db1b928ff58eaac7a9c601ab7a6b8e02bde57fb815af918e1373c8443239667255e14fd3a1d7484647c51268a01bc1330000000000000000000000000024e47d8dab3baf300aac3f9c183827b7e6faa7cd95dc3609c4608e4bede72e706f00000000000000000016e360039fba7bef02ba45a17b5d10799430c9b68181b764ceb7f32c9f0c59c0af521e28020002000000000000000004220020b2a49083d8b4305389aa873a3de7f9c94523aa79e31da0198766702e09216cd7000000003b9aca00000000001dcd6500000000000000000000020100000000000000005e02000000012431730de5f724b1609e576f23b4d4755ee8771219a46064650c89a2f10db1c201000000000000000001e0c810000000000022512089bf33c5f3bab05cc845434013f55ecca01e191579d9067384ef4416dad70cbc000000000000000000000000010000000000000002520200000001658a8f58c53d1dbd8059d208b23d162e4ae22057c9da8639ce9220715dd1029601000000000000000001e0c8100000000000160014211e67f245e9aa786722bbf8541ae98d3c102d5c000000000000000000000000000201000000000000000124e2ed2c90027959c4f0d54df6da2372a0ed3bd078444e6967412cc3d11b7b6e0a000000001fc027090000000000160014211e67f245e9aa786722bbf8541ae98d3c102d5c0000000001000000000000000324a2b2dd4e22a391be5e29d9a263881b4b50f25720a4c01a37c8211f4a6f24125d000000001fc027090000000000160014211e67f245e9aa786722bbf8541ae98d3c102d5c000000000001010000000000000006000000000012241822512089bf33c5f3bab05cc845434013f55ecca01e191579d9067384ef4416dad70cbc000101000000000000000500000000000a8d90160014211e67f245e9aa786722bbf8541ae98d3c102d5c00000000f38b5505fe64ed55ab7d7505338be5e2895453eb113d68f0d9ca8ace01d1c2610ae47d8dab3baf300aac3f9c183827b7e6faa7cd95dc3609c4608e4bede72e706f000200420140c18bd07ab32f5c2ee6dcbc4e6fc097060c6339579ebba7086c402b01cccb6aa34f396691edfa5ba60d0e3bac30589b257d12e428b40ef1309bdb0373bff03015006b024730440220466b46139edab96c0721dd803b11cc60a0289697ecdf2041baeeea3597a95c60022072b4beebca1f4b33367f8e436c50581ca08e0a3a15d1cd1c741651741216ac510121032ca9aaa209ab7f512c8839b5a4bc88e66050b5094f0d9d62d5005bfe1101d62dfd011e8b5505fe64ed55ab7d7505338be5e2895453eb113d68f0d9ca8ace01d1c2610ae47d8dab3baf300aac3f9c183827b7e6faa7cd95dc3609c4608e4bede72e706f0002006c02483045022100dded09d52b6ec25ce729d758e66c01c5f72383e193bdd58807a175a62983c75e022004332808ab1bab2d39248823f7ec760576c4d64d9bf1ff333acddbeb9b0e8fb50121032ca9aaa209ab7f512c8839b5a4bc88e66050b5094f0d9d62d5005bfe1101d62d006c024830450221009fde5a1de38b8bce5206207136a675bcb568c259c6f925dfc273956be8b671e902201a83903e497aea812e7991a2a514185ea195cdd3c0e78159eef055da220265030121032ca9aaa209ab7f512c8839b5a4bc88e66050b5094f0d9d62d5005bfe1101d62d0000061a808b5505fe64ed55ab7d7505338be5e2895453eb113d68f0d9ca8ace01d1c2610aff00000000000f4240000000000007a12000039fba7bef02ba45a17b5d10799430c9b68181b764ceb7f32c9f0c59c0af521e2800000200000000000000000000044c000030d40000ffff000000000007a120000000000000186a00000000000017d40102000000000000044c0000000000000000000000001dcd6500006402d000000000000000000000000009c4000000003ade57d0000000001e89d73056768c50777aa0a60664aeab43f6bba0a1410eeab680669dfbda438f217ba8160167c8658ae646dfdd33704ad3df8becfb2d8d916f0523c3b0a0ee4a40043d2e37206e347656b0dd54eeb114221bc7a64b0e94ff3bc01dd39ed8694aee664fd58a000000000000000003e800000000000003e8000000003b9aca00001e009000000000000000000000000009c4000000001e89d730000000003ade57d0624b66b127057abbc7c74fd70c4a66de86be773e4ef25500881e9f31c897d1ff02bde57fb815af918e1373c8443239667255e14fd3a1d7484647c51268a01bc133000000000000000000000000002452fadbb700e2090f7bcba950c1d891c41107edfd42ac98836bff8c3b3647d87200000000000000000016e360039fba7bef02ba45a17b5d10799430c9b68181b764ceb7f32c9f0c59c0af521e28020002000000000000000002220020b2a49083d8b4305389aa873a3de7f9c94523aa79e31da0198766702e09216cd7000000003b9aca00000000001dcd650000000000000000000001010000000000000000520200000001658a8f58c53d1dbd8059d208b23d162e4ae22057c9da8639ce9220715dd1029601000000000000000001e0c8100000000000160014211e67f245e9aa786722bbf8541ae98d3c102d5c000000000000000000000000000101000000000000000124e2ed2c90027959c4f0d54df6da2372a0ed3bd078444e6967412cc3d11b7b6e0a000000001fc027090000000000160014211e67f245e9aa786722bbf8541ae98d3c102d5c0000000000010100000000000000040000000000016ec2160014211e67f245e9aa786722bbf8541ae98d3c102d5c0001010000000000000003000000000001770a160014211e67f245e9aa786722bbf8541ae98d3c102d5c00061a80af8b5505fe64ed55ab7d7505338be5e2895453eb113d68f0d9ca8ace01d1c2610a52fadbb700e2090f7bcba950c1d891c41107edfd42ac98836bff8c3b3647d8720001006b0247304402206f944130e33be429f292b5b523166365bc995cee7e787cca4b3c91788ee5a1bb022024f8e7dc22eed3a55b60ebbbf082355d2a6e837136176f6cad22e2f42167e84a0121032ca9aaa209ab7f512c8839b5a4bc88e66050b5094f0d9d62d5005bfe1101d62db08b5505fe64ed55ab7d7505338be5e2895453eb113d68f0d9ca8ace01d1c2610a52fadbb700e2090f7bcba950c1d891c41107edfd42ac98836bff8c3b3647d8720001006c02483045022100b7728ca8313b75ad74a597e2424d3d064621dbe753051d031c7054194389b0a002200dd0f2598ad26a8eb01c343b68404986ac7568d3e04cbb7e972bf60d816b30030121032ca9aaa209ab7f512c8839b5a4bc88e66050b5094f0d9d62d5005bfe1101d62d0000061a808b5505fe64ed55ab7d7505338be5e2895453eb113d68f0d9ca8ace01d1c2610aff00000000000f4240000000000007a12000039fba7bef02ba45a17b5d10799430c9b68181b764ceb7f32c9f0c59c0af521e2800000200061a80000000000000044c000027100000ffff000000000007a120000000000000138800000000000017d40102000000000000044c0000000000000000000000001dcd6500006402d000000000000000000000000009c4000000003af16aa0000000001e76c460283ecb8bd6b29edbbae369f17c996dae91ff2cc85c42ad7c3f9f7ec8896577ff01385a666eebaaeed3b5d69bbc1c3749315f0f4f1e3e87ef72da66245e4a0463105d8f9cb8907f018ad0c4d7cf215871f62d5648e4fa7a09121035d6b037d1a1ac000000000000000003e800000000000003e8000000003b9aca00001e009000000000000000000000000009c4000000001e76c460000000003af16aa0d21bfaa4fd823795583f4760ec87d8fcdab7eef75dd7e8a88b45bde9b5d6bf1302bde57fb815af918e1373c8443239667255e14fd3a1d7484647c51268a01bc133000000ff033ba69a8b77b43130f2cd875a804dab53d9f4f678bc797a70098727a419663e740000000000000000000000000000000000000000000000061a8000061a800100 \ No newline at end of file diff --git a/eclair-core/src/test/resources/nonreg/codecs/050004-DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED/funder/data.json b/eclair-core/src/test/resources/nonreg/codecs/050004-DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED/funder/data.json new file mode 100644 index 0000000000..52f5323644 --- /dev/null +++ b/eclair-core/src/test/resources/nonreg/codecs/050004-DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED/funder/data.json @@ -0,0 +1,296 @@ +{ + "type" : "DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED", + "commitments" : { + "channelParams" : { + "channelId" : "8b5505fe64ed55ab7d7505338be5e2895453eb113d68f0d9ca8ace01d1c2610a", + "channelConfig" : [ "funding_pubkey_based_channel_keypath" ], + "channelFeatures" : [ "option_dual_fund" ], + "localParams" : { + "nodeId" : "02aaaa00ce2f18a967dc4f25f414e671ba6585f8ef0b8c5fb812c21064f55a2eaa", + "fundingKeyPath" : [ 1973568962, 653639297, 515844032, 511664979, 1199254153, 91788852, 657216322, 4183514426, 2147483649 ], + "isChannelOpener" : true, + "paysCommitTxFees" : true, + "initFeatures" : { + "activated" : { + "option_route_blinding" : "optional", + "option_provide_storage" : "optional", + "option_dual_fund" : "optional", + "splice_prototype" : "optional", + "payment_secret" : "mandatory", + "gossip_queries_ex" : "optional", + "option_anchor_outputs" : "optional", + "option_quiesce" : "optional", + "option_data_loss_protect" : "optional", + "var_onion_optin" : "mandatory", + "option_static_remotekey" : "optional", + "option_support_large_channel" : "optional", + "option_anchors_zero_fee_htlc_tx" : "optional", + "option_channel_type" : "mandatory", + "basic_mpp" : "optional", + "gossip_queries" : "optional" + }, + "unknown" : [ 50001 ] + } + }, + "remoteParams" : { + "nodeId" : "02bbbb671d15145722fb8c28d732cddb249bcc6652ed2b297ff1f77a18371b1e63", + "revocationBasepoint" : "02c152c7a43051740ef1b8fa5936f4866bca5ca740b4589b202f60692b6c30c114", + "paymentBasepoint" : "021485935cf03bb4f568e2d33e23fefe36cb6ad1d2ff57a50a609a43e36d484df7", + "delayedPaymentBasepoint" : "039c9096f7f6bda74572664aacdacd04ff7dbeb20bfa48ac169271724fb9aafb39", + "htlcBasepoint" : "0357bf0ceb7a3fc5cf65d076910235a0d9abcce1087414865ad3374a08bcbd023d", + "initFeatures" : { + "activated" : { + "option_route_blinding" : "optional", + "option_dual_fund" : "optional", + "splice_prototype" : "optional", + "payment_secret" : "mandatory", + "gossip_queries_ex" : "optional", + "option_anchor_outputs" : "optional", + "option_quiesce" : "optional", + "option_data_loss_protect" : "optional", + "var_onion_optin" : "mandatory", + "option_static_remotekey" : "optional", + "option_support_large_channel" : "optional", + "option_anchors_zero_fee_htlc_tx" : "optional", + "option_channel_type" : "mandatory", + "basic_mpp" : "optional", + "gossip_queries" : "optional" + }, + "unknown" : [ ] + } + }, + "channelFlags" : { + "nonInitiatorPaysCommitFees" : false, + "announceChannel" : false + } + }, + "changes" : { + "localChanges" : { + "proposed" : [ ], + "signed" : [ ], + "acked" : [ ] + }, + "remoteChanges" : { + "proposed" : [ ], + "acked" : [ ], + "signed" : [ ] + }, + "localNextHtlcId" : 0, + "remoteNextHtlcId" : 0 + }, + "active" : [ { + "fundingTxIndex" : 0, + "fundingInput" : "8079e0d28cbf88d7c215b900251b3a259bc0e9dcbb8a7217a39a8b85b42bd6df:0", + "fundingAmount" : 1250000, + "localFunding" : { + "status" : "unconfirmed", + "txid" : "8079e0d28cbf88d7c215b900251b3a259bc0e9dcbb8a7217a39a8b85b42bd6df" + }, + "remoteFunding" : { + "status" : "not-locked" + }, + "commitmentFormat" : "anchor_outputs", + "localCommitParams" : { + "dustLimit" : 1100, + "htlcMinimum" : 0, + "maxHtlcValueInFlight" : 500000000, + "maxAcceptedHtlcs" : 100, + "toSelfDelay" : 720 + }, + "localCommit" : { + "index" : 0, + "spec" : { + "htlcs" : [ ], + "commitTxFeerate" : 2500, + "toLocal" : 987650000, + "toRemote" : 262350000 + }, + "txId" : "ba5e83eafb7f06b6f343b963fd07b4e58e0e94d1fed90be388e6a6646186c46d", + "remoteSig" : { + "sig" : "93c5f1ce1caf79cd2f314572562d4e8eefecf7559aa953a034c88f70ebce6e0242032a4e5787591197974af6ed0a99113f5ce9e469425dfe2f5cab5507c732b8" + }, + "htlcRemoteSigs" : [ ] + }, + "remoteCommitParams" : { + "dustLimit" : 1000, + "htlcMinimum" : 1000, + "maxHtlcValueInFlight" : 1000000000, + "maxAcceptedHtlcs" : 30, + "toSelfDelay" : 144 + }, + "remoteCommit" : { + "index" : 0, + "spec" : { + "htlcs" : [ ], + "commitTxFeerate" : 2500, + "toLocal" : 262350000, + "toRemote" : 987650000 + }, + "txId" : "d7c4038d94da90de0f6d7a545b90333485229bb2dad55a182c94bb6921221569", + "remotePerCommitmentPoint" : "02bde57fb815af918e1373c8443239667255e14fd3a1d7484647c51268a01bc133" + } + }, { + "fundingTxIndex" : 0, + "fundingInput" : "afcffdab28726d4831a25e77015dcf9b05494dd0b4a1bb5b54fd88f81ad5dce3:2", + "fundingAmount" : 1750000, + "localFunding" : { + "status" : "unconfirmed", + "txid" : "afcffdab28726d4831a25e77015dcf9b05494dd0b4a1bb5b54fd88f81ad5dce3" + }, + "remoteFunding" : { + "status" : "not-locked" + }, + "commitmentFormat" : "anchor_outputs", + "localCommitParams" : { + "dustLimit" : 1100, + "htlcMinimum" : 0, + "maxHtlcValueInFlight" : 500000000, + "maxAcceptedHtlcs" : 100, + "toSelfDelay" : 720 + }, + "localCommit" : { + "index" : 0, + "spec" : { + "htlcs" : [ ], + "commitTxFeerate" : 2500, + "toLocal" : 983900000, + "toRemote" : 766100000 + }, + "txId" : "021a43ae2d2d01dbb79d9dbf3860da3c669fd1cfc800598847fc8de55eafd6ce", + "remoteSig" : { + "sig" : "e11b7a3726e1bfc570ffb4ac9bb4e531576a100c0a8267898b2ad0215ad179a400aed248c419209a768f6ced19dbbb8598a68695c2cb310e05c734dce8f891b7" + }, + "htlcRemoteSigs" : [ ] + }, + "remoteCommitParams" : { + "dustLimit" : 1000, + "htlcMinimum" : 1000, + "maxHtlcValueInFlight" : 1000000000, + "maxAcceptedHtlcs" : 30, + "toSelfDelay" : 144 + }, + "remoteCommit" : { + "index" : 0, + "spec" : { + "htlcs" : [ ], + "commitTxFeerate" : 2500, + "toLocal" : 766100000, + "toRemote" : 983900000 + }, + "txId" : "d19ab71cca21e7bbc60949b9003491aa95db1b928ff58eaac7a9c601ab7a6b8e", + "remotePerCommitmentPoint" : "02bde57fb815af918e1373c8443239667255e14fd3a1d7484647c51268a01bc133" + } + }, { + "fundingTxIndex" : 0, + "fundingInput" : "6f702ee7ed4b8e60c40936dc95cda7fae6b72738189c3fac0a30af3bab8d7de4:0", + "fundingAmount" : 1500000, + "localFunding" : { + "status" : "unconfirmed", + "txid" : "6f702ee7ed4b8e60c40936dc95cda7fae6b72738189c3fac0a30af3bab8d7de4" + }, + "remoteFunding" : { + "status" : "not-locked" + }, + "commitmentFormat" : "anchor_outputs", + "localCommitParams" : { + "dustLimit" : 1100, + "htlcMinimum" : 0, + "maxHtlcValueInFlight" : 500000000, + "maxAcceptedHtlcs" : 100, + "toSelfDelay" : 720 + }, + "localCommit" : { + "index" : 0, + "spec" : { + "htlcs" : [ ], + "commitTxFeerate" : 2500, + "toLocal" : 987650000, + "toRemote" : 512350000 + }, + "txId" : "56768c50777aa0a60664aeab43f6bba0a1410eeab680669dfbda438f217ba816", + "remoteSig" : { + "sig" : "67c8658ae646dfdd33704ad3df8becfb2d8d916f0523c3b0a0ee4a40043d2e37206e347656b0dd54eeb114221bc7a64b0e94ff3bc01dd39ed8694aee664fd58a" + }, + "htlcRemoteSigs" : [ ] + }, + "remoteCommitParams" : { + "dustLimit" : 1000, + "htlcMinimum" : 1000, + "maxHtlcValueInFlight" : 1000000000, + "maxAcceptedHtlcs" : 30, + "toSelfDelay" : 144 + }, + "remoteCommit" : { + "index" : 0, + "spec" : { + "htlcs" : [ ], + "commitTxFeerate" : 2500, + "toLocal" : 512350000, + "toRemote" : 987650000 + }, + "txId" : "624b66b127057abbc7c74fd70c4a66de86be773e4ef25500881e9f31c897d1ff", + "remotePerCommitmentPoint" : "02bde57fb815af918e1373c8443239667255e14fd3a1d7484647c51268a01bc133" + } + }, { + "fundingTxIndex" : 0, + "fundingInput" : "72d847363b8cff6b8398ac42fded0711c491d8c150a9cb7b0f09e200b7dbfa52:0", + "fundingAmount" : 1500000, + "localFunding" : { + "status" : "unconfirmed", + "txid" : "72d847363b8cff6b8398ac42fded0711c491d8c150a9cb7b0f09e200b7dbfa52" + }, + "remoteFunding" : { + "status" : "not-locked" + }, + "commitmentFormat" : "anchor_outputs", + "localCommitParams" : { + "dustLimit" : 1100, + "htlcMinimum" : 0, + "maxHtlcValueInFlight" : 500000000, + "maxAcceptedHtlcs" : 100, + "toSelfDelay" : 720 + }, + "localCommit" : { + "index" : 0, + "spec" : { + "htlcs" : [ ], + "commitTxFeerate" : 2500, + "toLocal" : 988900000, + "toRemote" : 511100000 + }, + "txId" : "283ecb8bd6b29edbbae369f17c996dae91ff2cc85c42ad7c3f9f7ec8896577ff", + "remoteSig" : { + "sig" : "385a666eebaaeed3b5d69bbc1c3749315f0f4f1e3e87ef72da66245e4a0463105d8f9cb8907f018ad0c4d7cf215871f62d5648e4fa7a09121035d6b037d1a1ac" + }, + "htlcRemoteSigs" : [ ] + }, + "remoteCommitParams" : { + "dustLimit" : 1000, + "htlcMinimum" : 1000, + "maxHtlcValueInFlight" : 1000000000, + "maxAcceptedHtlcs" : 30, + "toSelfDelay" : 144 + }, + "remoteCommit" : { + "index" : 0, + "spec" : { + "htlcs" : [ ], + "commitTxFeerate" : 2500, + "toLocal" : 511100000, + "toRemote" : 988900000 + }, + "txId" : "d21bfaa4fd823795583f4760ec87d8fcdab7eef75dd7e8a88b45bde9b5d6bf13", + "remotePerCommitmentPoint" : "02bde57fb815af918e1373c8443239667255e14fd3a1d7484647c51268a01bc133" + } + } ], + "inactive" : [ ], + "remoteNextCommitInfo" : "033ba69a8b77b43130f2cd875a804dab53d9f4f678bc797a70098727a419663e74", + "remotePerCommitmentSecrets" : null, + "originChannels" : { } + }, + "localPushAmount" : 0, + "remotePushAmount" : 0, + "waitingSince" : 400000, + "lastChecked" : 400000, + "status" : { } +} \ No newline at end of file diff --git a/eclair-core/src/test/resources/nonreg/codecs/03000a-DATA_WAIT_FOR_CHANNEL_READY/funder/data.bin b/eclair-core/src/test/resources/nonreg/codecs/050005-DATA_WAIT_FOR_DUAL_FUNDING_READY/funder/data.bin similarity index 83% rename from eclair-core/src/test/resources/nonreg/codecs/03000a-DATA_WAIT_FOR_CHANNEL_READY/funder/data.bin rename to eclair-core/src/test/resources/nonreg/codecs/050005-DATA_WAIT_FOR_DUAL_FUNDING_READY/funder/data.bin index ec62657d45..36ff46481e 100644 --- a/eclair-core/src/test/resources/nonreg/codecs/03000a-DATA_WAIT_FOR_CHANNEL_READY/funder/data.bin +++ b/eclair-core/src/test/resources/nonreg/codecs/050005-DATA_WAIT_FOR_DUAL_FUNDING_READY/funder/data.bin @@ -1 +1 @@ -03000a83a6dea8adf975b60df17738937034a2f96cb8b784da01e2934e9e172244317d01010002aaaa00ce2f18a967dc4f25f414e671ba6585f8ef0b8c5fb812c21064f55a2eaa0009121e7d05bea9557fe18572ca87baa8726551e04d03c6c4aec29cc0181ecdf3bc80000001000000000000044c000000001dcd65000000000000002710000000000000000000900064ff160014c59265957886e166f37c863dca15b49aa42d75b40000186b020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000202498202bbbb671d15145722fb8c28d732cddb249bcc6652ed2b297ff1f77a18371b1e6300000000000003e8000000003b9aca000000000000004e2000000000000003e80090001e02b9da90e1ed391bd7b419b509f9499d09c52156c5deba6c927791c0074c1d18ff033b164d14b54f08a951f9e40fb84e637021f376c9ae2907975f9ff0795431205d03759da3f6bdcc2f595e1a98eb4803729743ab8608e28788cfe96726b5328c214c03abc3ae99fac104e94f79cafcd70247cc336cdc21a53d4c3a4c321b54e20902e702d457910d53f6a735571f09b85a25797f5cb06df20be8b3c39412b3fe18a0f6f9000000040202498200000000000000000000000000002710000000002faf0800000000000bebc2002483a6dea8adf975b60df17738937034a2f96cb8b784da01e2934e9e172244317d000000002b40420f00000000002200207da990cedd741f9f5dc35fae5919582e8baf6c04ec7244e1553a654018a3929f475221025ab0800cdb8b9664753e51c5e22755a86e439e66eaf8e5c1b743d0bc4b3b076b2102b9da90e1ed391bd7b419b509f9499d09c52156c5deba6c927791c0074c1d18ff52ae7d020000000183a6dea8adf975b60df17738937034a2f96cb8b784da01e2934e9e172244317d0000000000fc10bb8002400d03000000000016001434b58af5c6bf8472898ef7ed44f65bce2e4a6101b8180c00000000002200206bf6eeedf2e93dee452c92c0c1e2421cc064bf8f70b432c6e36459215576d24decab03208ccca40bbf6c859ceb2d5288153801426e19f8b119c2d1d4ba6d82511c7816aa0eaa8e41eafb179818d1ba7b0cfdce2da5fa002921df00a1fb4e1458d0b9042c00000000000000000000000000002710000000000bebc200000000002faf0800839b92423077adb38eeaa118dd6ba0e6daf8c1e0add666ddd18f57b517a2d7500324b50221ad635b97f597802fbe5b2d6414fdf41f224ac1869d3772314e9fbfa5000000000000000000000000000000000000000000000000000000000000ff0209317c45de4cff05adbf9d69edbc334a1c89325bade86f4194c6665336b7e9f82483a6dea8adf975b60df17738937034a2f96cb8b784da01e2934e9e172244317d000000002b40420f00000000002200207da990cedd741f9f5dc35fae5919582e8baf6c04ec7244e1553a654018a3929f475221025ab0800cdb8b9664753e51c5e22755a86e439e66eaf8e5c1b743d0bc4b3b076b2102b9da90e1ed391bd7b419b509f9499d09c52156c5deba6c927791c0074c1d18ff52ae00000001061a8000002a0000000103dab1f7dc6942fd004b83a6dea8adf975b60df17738937034a2f96cb8b784da01e2934e9e172244317d02c059863c54784cf8c84965a370a4698002688239802f605894b0c690502388ae010803dab1f7dc6942fd \ No newline at end of file +0500050153befcd6f2049374f803f765adc794f7b5c43978c31b225659a5a85797ad07300101041000000002aaaa00ce2f18a967dc4f25f414e671ba6585f8ef0b8c5fb812c21064f55a2eaa0009a836ed30703f2227ee010b05223e99b7266b5eba5e6ba0da82e0c05a193ce04f8000000100c000000000186b0200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000180822aa698202bbbb671d15145722fb8c28d732cddb249bcc6652ed2b297ff1f77a18371b1e63000294eb03c0fb94b572d4488e367496af7c583c3a91a6b779cb8dc89a9801f0990702d1aa755ef89b975b151ed0161e01df06d0b993009e4bcf166c4310ae7349ba34021026827860f06ec11a860cef2ad4493eecbdacb082eb426e854c159e2836381c033a2eb85e7b76af367225e9f8f8f34d0e19bc33c7d4f7c9b94e7ec39fcc2ec53c000000140800000000000000000000000000100822aa6982000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000002421b51b29b10d62d8d778d4b650e50394e184da0ba1b0c1fbc38bede8622354b302000000000000000016e36002c87e802c96700dec5f8d345383d95c189235b43c958211e85c61e567cfbfa6e104000224facfdde44454e90a91d2deed1de02cb2b49d8fd559be87212f99438df7cea50d000000002416dcfdb70667a39cc3aa3de81737e4992f62503673a0d83b1beec8033e1801cf000000002b60e31600000000002200208f8fbbb52a762e6954ce27780e3c3b1feb542843e38d8ccb049e4ef577e556c4061a8000002a0002ffb053befcd6f2049374f803f765adc794f7b5c43978c31b225659a5a85797ad073021b51b29b10d62d8d778d4b650e50394e184da0ba1b0c1fbc38bede8622354b30001006c02483045022100e73d6beaaeaab5cb0adb7d4da3364f5063bf9106c138b3f2dbd5eaf83cadf6bf02200493407b832481579e1c997f5ef46c532bc9fd9632ee4460a4a8f509e45a12940121020fd564dc1b02e39667103e45b0769e4c032884c82c8da72c3e9cac36674f12f2000102000000000000044c0000000000000000000000001dcd6500006402d000000000000000000000000009c4000000003b9aca00000000001dcd6500ef3e5bb8e68f6cc3edbef0f63546679204b93d89e72a1ef305f1d302ec91179201de15babcc6ed94cd9221952cfd47154e9450e061827b68e0e692871a8ce5c4005ffbca12b217885c4a476b34e94265b9db80e711986e0a51fe7a793397c6877b000000000000000003e800000000000003e8000000003b9aca00001e009000000000000000000000000009c4000000001dcd6500000000003b9aca0056d3f5823d83d308887fe61e4aeba828725a2d4d745f4257c18de65edaaee9d602d0c96ba289fa5ad21a528fc9905457f27bebb989c0631f1e3dbd4bb163955836000000ff03ed2f5a37317543756a3958e8e6e8f367e714fbec63eae76eb53db0fd9f20d1df0000000000000001001853569ad7464300 \ No newline at end of file diff --git a/eclair-core/src/test/resources/nonreg/codecs/04000d-DATA_WAIT_FOR_DUAL_FUNDING_READY/funder/data.json b/eclair-core/src/test/resources/nonreg/codecs/050005-DATA_WAIT_FOR_DUAL_FUNDING_READY/funder/data.json similarity index 59% rename from eclair-core/src/test/resources/nonreg/codecs/04000d-DATA_WAIT_FOR_DUAL_FUNDING_READY/funder/data.json rename to eclair-core/src/test/resources/nonreg/codecs/050005-DATA_WAIT_FOR_DUAL_FUNDING_READY/funder/data.json index 1d8b1d5a7e..6e0084590b 100644 --- a/eclair-core/src/test/resources/nonreg/codecs/04000d-DATA_WAIT_FOR_DUAL_FUNDING_READY/funder/data.json +++ b/eclair-core/src/test/resources/nonreg/codecs/050005-DATA_WAIT_FOR_DUAL_FUNDING_READY/funder/data.json @@ -1,18 +1,13 @@ { "type" : "DATA_WAIT_FOR_DUAL_FUNDING_READY", "commitments" : { - "params" : { - "channelId" : "ae58e31828b115b8a900a9d20b060ef2b2da2fcfe18991aff34168579d246b54", + "channelParams" : { + "channelId" : "53befcd6f2049374f803f765adc794f7b5c43978c31b225659a5a85797ad0730", "channelConfig" : [ "funding_pubkey_based_channel_keypath" ], - "channelFeatures" : [ "option_static_remotekey", "option_anchors_zero_fee_htlc_tx", "option_dual_fund" ], + "channelFeatures" : [ "option_dual_fund" ], "localParams" : { "nodeId" : "02aaaa00ce2f18a967dc4f25f414e671ba6585f8ef0b8c5fb812c21064f55a2eaa", - "fundingKeyPath" : [ 461130578, 2441458723, 3315200959, 1624559903, 421875145, 1108846792, 2605590614, 1621284536, 2147483649 ], - "dustLimit" : 1100, - "maxHtlcValueInFlightMsat" : 500000000, - "htlcMinimum" : 0, - "toSelfDelay" : 144, - "maxAcceptedHtlcs" : 100, + "fundingKeyPath" : [ 2822171952, 1883185703, 3993045765, 574527927, 644570810, 1584111834, 2195767386, 423419983, 2147483649 ], "isChannelOpener" : true, "paysCommitTxFees" : true, "initFeatures" : { @@ -30,6 +25,7 @@ "option_static_remotekey" : "optional", "option_support_large_channel" : "optional", "option_anchors_zero_fee_htlc_tx" : "optional", + "option_channel_type" : "mandatory", "basic_mpp" : "optional", "gossip_queries" : "optional" }, @@ -38,15 +34,10 @@ }, "remoteParams" : { "nodeId" : "02bbbb671d15145722fb8c28d732cddb249bcc6652ed2b297ff1f77a18371b1e63", - "dustLimit" : 1000, - "maxHtlcValueInFlightMsat" : 1000000000, - "htlcMinimum" : 1000, - "toSelfDelay" : 144, - "maxAcceptedHtlcs" : 30, - "revocationBasepoint" : "031a9b266c6a1f5d05f6f2b040b0eea59819c2fb5715945628541c58c2605e5c42", - "paymentBasepoint" : "02cb3e21fe0825663cfa1d8a8accc93dedbdb906b0ec4b779bb41b0f030b38fd33", - "delayedPaymentBasepoint" : "0370c28e0286bfcf6bfb18c2b98d5b265594ec363822cbb323e7f7451ca60b0330", - "htlcBasepoint" : "03fac293804f27e6823ba11b41063d35f20a844864de7cc2f9586445af26b1a03e", + "revocationBasepoint" : "0294eb03c0fb94b572d4488e367496af7c583c3a91a6b779cb8dc89a9801f09907", + "paymentBasepoint" : "02d1aa755ef89b975b151ed0161e01df06d0b993009e4bcf166c4310ae7349ba34", + "delayedPaymentBasepoint" : "021026827860f06ec11a860cef2ad4493eecbdacb082eb426e854c159e2836381c", + "htlcBasepoint" : "033a2eb85e7b76af367225e9f8f8f34d0e19bc33c7d4f7c9b94e7ec39fcc2ec53c", "initFeatures" : { "activated" : { "option_route_blinding" : "optional", @@ -61,6 +52,7 @@ "option_static_remotekey" : "optional", "option_support_large_channel" : "optional", "option_anchors_zero_fee_htlc_tx" : "optional", + "option_channel_type" : "mandatory", "basic_mpp" : "optional", "gossip_queries" : "optional" }, @@ -88,18 +80,23 @@ }, "active" : [ { "fundingTxIndex" : 0, - "fundingTx" : { - "outPoint" : "e0052c9a6cebcf139248732fcc7c563b022c2ae2d665a670f09389e4ff514393:0", - "amountSatoshis" : 1500000 - }, + "fundingInput" : "b3542362e8ed8bc3fbc1b0a10bda84e19403e550b6d478d7d8620db1291bb521:2", + "fundingAmount" : 1500000, "localFunding" : { "status" : "confirmed", - "txid" : "e0052c9a6cebcf139248732fcc7c563b022c2ae2d665a670f09389e4ff514393", - "shortChannelId" : "400000x42x0" + "shortChannelId" : "400000x42x2" }, "remoteFunding" : { "status" : "not-locked" }, + "commitmentFormat" : "anchor_outputs", + "localCommitParams" : { + "dustLimit" : 1100, + "htlcMinimum" : 0, + "maxHtlcValueInFlight" : 500000000, + "maxAcceptedHtlcs" : 100, + "toSelfDelay" : 720 + }, "localCommit" : { "index" : 0, "spec" : { @@ -108,14 +105,18 @@ "toLocal" : 1000000000, "toRemote" : 500000000 }, - "commitTxAndRemoteSig" : { - "commitTx" : { - "txid" : "c98fac755c41c1688a55f820a3ac9c4c8b2adde1d6687b2a9d843b364211965f", - "tx" : "0200000001934351ffe48993f070a665d6e22a2c023b567ccc2f73489213cfeb6c9a2c05e000000000001a741d80044a010000000000002200209698cbfb709ad73804217ae67e86ea857e8c1b317c82e7740670eb6def38ea334a01000000000000220020d468d18ef4bd63800e3e59bbb204a2921dc75cec9786e363ba2d33d467ae98e820a1070000000000220020fbc40c7329e9ed94bb1a94bed8d66eba59e96aba45dc5d59150b6ac6ab24436ab2340f00000000002200201e1e972764d9ad2836c50f22bcb80b031dd989239b89db8d3c62c83e14ad88c0a71aae20" - }, - "remoteSig" : "353c51755a0c852a4c5dd9e2d33d4a9c0934e9a07a43da89f061a3b4499f374d1857dfa513ed714287baf4ac3f4375f06bf435577962f121182ad9ce4b19f549" + "txId" : "ef3e5bb8e68f6cc3edbef0f63546679204b93d89e72a1ef305f1d302ec911792", + "remoteSig" : { + "sig" : "de15babcc6ed94cd9221952cfd47154e9450e061827b68e0e692871a8ce5c4005ffbca12b217885c4a476b34e94265b9db80e711986e0a51fe7a793397c6877b" }, - "htlcTxsAndRemoteSigs" : [ ] + "htlcRemoteSigs" : [ ] + }, + "remoteCommitParams" : { + "dustLimit" : 1000, + "htlcMinimum" : 1000, + "maxHtlcValueInFlight" : 1000000000, + "maxAcceptedHtlcs" : 30, + "toSelfDelay" : 144 }, "remoteCommit" : { "index" : 0, @@ -125,16 +126,16 @@ "toLocal" : 500000000, "toRemote" : 1000000000 }, - "txid" : "800a5eab3f585d54f19d6e574b611d94fa7414af651e17104310bf004ccd5cd6", - "remotePerCommitmentPoint" : "03a59c87106cab1b24df7f9a3843a728a45bca74680f65118ca6c5a3d45c1934e6" + "txId" : "56d3f5823d83d308887fe61e4aeba828725a2d4d745f4257c18de65edaaee9d6", + "remotePerCommitmentPoint" : "02d0c96ba289fa5ad21a528fc9905457f27bebb989c0631f1e3dbd4bb163955836" } } ], "inactive" : [ ], - "remoteNextCommitInfo" : "035642c8d53e45f57ffdbcfd1bdb3c931479649d6c3c3a4ffe766c1a3f8835212d", + "remoteNextCommitInfo" : "03ed2f5a37317543756a3958e8e6e8f367e714fbec63eae76eb53db0fd9f20d1df", "remotePerCommitmentSecrets" : null, "originChannels" : { } }, "aliases" : { - "localAlias" : "0x1277132bb4ca09a" + "localAlias" : "0x1853569ad74643" } } \ No newline at end of file diff --git a/eclair-core/src/test/resources/nonreg/codecs/050006-DATA_NORMAL/announced-splice/data.bin b/eclair-core/src/test/resources/nonreg/codecs/050006-DATA_NORMAL/announced-splice/data.bin new file mode 100644 index 0000000000..8dd78e8b95 --- /dev/null +++ b/eclair-core/src/test/resources/nonreg/codecs/050006-DATA_NORMAL/announced-splice/data.bin @@ -0,0 +1 @@ +0500060140aeef92e20152a240778be1efc1c21b5aad9845af64bd847192ebc542fd86ae0101041000000002aaaa00ce2f18a967dc4f25f414e671ba6585f8ef0b8c5fb812c21064f55a2eaa0009d6b9c092aaebb75def82d314d00da1692aab1f0380138cfc4f73cefb9eafd6a68000000100c000000000186b0200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000180822aa698202bbbb671d15145722fb8c28d732cddb249bcc6652ed2b297ff1f77a18371b1e630002d1adfda1fb776dee79bb47dfbef1e9aabd84f9b52069ef667ca7dda5e9095d140345f2d1602b1028e2ecdcbb26d3536ffa58b437305da69c823c70bd14a18ebcf802b7b0a53ed7e69c53551ab61be3ec157f2cb4e3394d38ee37a412a70b995929ea03723fcadf475548206a498b5214289b5301fc83561b91c218ad3bf6f6d2aef5da000000140800000000000000000000000000100822aa6982000100000000000000000000000000000000000000000000000000000000000000010000000200000000000000002418def7f6049f2267cdaf9ff07c0897d0f65085b16b139430ce579e5cce3567f5000000000000000000200b2003ce9233425de39effe766fcf94adb84c56bc31915f52dc82d038372fdd36f63460400022453a6778183da859a4d756a765bc7ed64098b371a30d150b68e22411e46a60d62000000002493790747c4d7d21297ee1cee1f9ffabdf2b3176e50696ed586e7ff2fd26382b5010000002b200b200000000000220020768856f5179d40fe0df8904ea6cece595b44cdfe83dfead88ab92a4fd1ab35730006c100001b0000ffca40aeef92e20152a240778be1efc1c21b5aad9845af64bd847192ebc542fd86ae18def7f6049f2267cdaf9ff07c0897d0f65085b16b139430ce579e5cce3567f5000100420140b03372b5ac38b8e109b7a08b2a1aa20a3d6df3facadf24deb3c8834078c3e0dcd245932d32dc3b2175d9144105ccde47bc38189b70d7511383f43cf5a0362a33fd025940036f8a32cb0b9c83a1007a151fba78a52dac2e070b45b39681bcfecad8e11c544149d4596d87d06c53a0cda4833353e07483fce4826ec32ee37eec7ef5cef7ea000202000000000000044c0000000000000000000000001dcd6500006402d000000000000000000000000009c40000000053724e000000000029b92700129afb31943781baadeda527158d17c9cba6654dc4576c2fb3cbb6c5537164dd0104de3a1e42ea04f9aca0c167623150ef86dc28c1bac69272c90574bb272e365f2ce1e09872ed0e3c97cbdf62a0494c38317d227d45fb8836585ecd9bf4439dde000000000000000003e800000000000003e8000000003b9aca00001e009000000000000000000000000009c40000000029b927000000000053724e00300c847b813a3dba6ff6eaa6a149ee88931f79699cd037070a79b59a0fddf7a202078accc2dbdccefc30355c2bbf2482d49c825f8a3c9be0c6948ec9fc06b0a3750000020000000100000000000000002493790747c4d7d21297ee1cee1f9ffabdf2b3176e50696ed586e7ff2fd26382b50100000000000000001e8480029bbed42a2a075a2096b988965e34301fc8c155a57e0bad7e331c5bc7eb793e2e040002246030bf11e728ed1779e89e22d08650f52a8d0bc64f0889d0fdee7a072354ef3300000000242576b910377cb1d0d5fd286d9934d1ca746933916eb8a38b6bfad7eb07ef6117000000002b80841e00000000002200203e8896d47bcde2c99116cc088c597e7b1e5fe01ec14320894ba0c631358855ea0004510000250001fff440aeef92e20152a240778be1efc1c21b5aad9845af64bd847192ebc542fd86ae93790747c4d7d21297ee1cee1f9ffabdf2b3176e50696ed586e7ff2fd26382b50001006c02483045022100de9ae2fdd569e5ebbcadabad7d817e73ea902e1ff57bd04cb125e39ddfa1c11402204552c441ce9c89f005aa2ee18c21819a17f0aa8f33044a956d28588fbbbca1580121034af1784ee2af8a0243316844c4a7430b92d796fa09af545edb26cd7726d4c94dfd0259409766e7da45a6e00ce71b0be1d60ee873683aa9603430e4039dde3df66f9595ff3b8f7e91e87080773fedd8963145b92e324ea721b9e2b8c5cc7599d5e49a101e000202000000000000044c0000000000000000000000001dcd6500006402d000000000000000000000000009c4000000004d7c6d000000000029b92700fa3b8ebdce81796a56592f34f2eca3fc9561f2d389f14cc12d57cb91894f69e9017f858ab4d1214ee849fb49f271dda4e413d5b09eef481cbeee3d2ecfbbc49f984dc657184b9a05add35cc14200578c73a78fdb4f6c753399551c5688bb2698ea000000000000000003e800000000000003e8000000003b9aca00001e009000000000000000000000000009c40000000029b92700000000004d7c6d0014485a091401962722c69a3faa6c46a3983599b0b81bb02f984a4a95f7cbf4d902078accc2dbdccefc30355c2bbf2482d49c825f8a3c9be0c6948ec9fc06b0a37500000000000000000000000000246030bf11e728ed1779e89e22d08650f52a8d0bc64f0889d0fdee7a072354ef3300000000000000000016e36003c0b536754fdd60b52b87c23410a2ddc099e5a61cd481a4c42136ed515f51cad004000224cba5f28ae75551bb16851b38645b6ea28c8cf88352209a8479af4806a3e7da130000000024d236e3f15f797930268a3d9ccb31277258dad7d33e5d32d21ede9281640408a7000000002b60e3160000000000220020ed16fef9a91946291745695ddb4b5ec210e54ebbcff0f6314c7e0f417ba47f3a061a8000002a0000ff8640aeef92e20152a240778be1efc1c21b5aad9845af64bd847192ebc542fd86ae6030bf11e728ed1779e89e22d08650f52a8d0bc64f0889d0fdee7a072354ef330001004201404656a711d6294197d32a4e347b4de4701e3b02fa7577895103d10b98a037abe8b13de1297f136341b1711068ad2449cf4c430a2629dd89766086edad48ecee8e000202000000000000044c0000000000000000000000001dcd6500006402d000000000000000000000000009c4000000002faf08000000000029b927003fd0ad57b0ba68c68e403498a47befa62f0af2add84d6bcb44c6f6de6e8e76bd01578e73b9d3e6486935735797fc26083621faa2dc715580c24f48e1cd4f67fad1524967f32403ce30e8ea1019c9e72b532c8aeabf0fd9a8a896ef4f69cb562022000000000000000003e800000000000003e8000000003b9aca00001e009000000000000000000000000009c40000000029b92700000000002faf0800e381da618dac2b7259c7ed0933f1a3f6eff44014b86cc7636129b41c718af34802078accc2dbdccefc30355c2bbf2482d49c825f8a3c9be0c6948ec9fc06b0a37500ff030e5de3e6b7f7eee32b8beefbe40fdbffdfa7428ccf1b0fd94292db4576ab0a3e00000000000000010201163ba9b2d775ff038ac563ce60e886fffd01ae7e1b49b6cb40bb1babb9dc9b11cfe10712bb6102356537f22a36e6d4a70f233917a9ed4979054007f68ed8119c1676c7a79e95fb55ff31ccb63ca784487b264fb13a1062961ec5361f84f6703594bb940901722c31e0f769007cb41c65772c020d7837076c1a516b1fc11cb93d4344ebf9d3ef007d481bb4c09656b26231236bc354ffd0de3b1ea69cf09ea3918bacf65dc208f059bf5ebe2433328cb6aae8f54d7f4ea787a9eb137f945f30afca3846f0c84febfb02fa9bab390a1bd413fa09ab0bdff42031f002f3b2adf4facf1493dbfc6ea7e5f463c557bf1d252a9f1f331442bde73a7a0472c6b54195cc00921c2f7e492d80523467d6dabdaae50e19e4000006226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f061a8000002a000002aaaa00ce2f18a967dc4f25f414e671ba6585f8ef0b8c5fb812c21064f55a2eaa02bbbb671d15145722fb8c28d732cddb249bcc6652ed2b297ff1f77a18371b1e63028539aa68248b7d478cc9dde8ca4eac0d6d429df4e6c3413f2d75b98954668ea603c0b536754fdd60b52b87c23410a2ddc099e5a61cd481a4c42136ed515f51cad088e8f087397502818833701813105c3a4effb1563759067f27cd775f9accad82c64dd6d3c98d9db3845269a1c104f4c14b4263e75c95584b4e27f048bd740a3b7906226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f061a8000002a000068ba944e0100009000000000000003e8000854d00000000a000000001dcd650001000000 \ No newline at end of file diff --git a/eclair-core/src/test/resources/nonreg/codecs/050006-DATA_NORMAL/announced-splice/data.json b/eclair-core/src/test/resources/nonreg/codecs/050006-DATA_NORMAL/announced-splice/data.json new file mode 100644 index 0000000000..069f8d6a60 --- /dev/null +++ b/eclair-core/src/test/resources/nonreg/codecs/050006-DATA_NORMAL/announced-splice/data.json @@ -0,0 +1,283 @@ +{ + "type" : "DATA_NORMAL", + "commitments" : { + "channelParams" : { + "channelId" : "40aeef92e20152a240778be1efc1c21b5aad9845af64bd847192ebc542fd86ae", + "channelConfig" : [ "funding_pubkey_based_channel_keypath" ], + "channelFeatures" : [ "option_dual_fund" ], + "localParams" : { + "nodeId" : "02aaaa00ce2f18a967dc4f25f414e671ba6585f8ef0b8c5fb812c21064f55a2eaa", + "fundingKeyPath" : [ 3602497682, 2867574621, 4018328340, 3490554217, 715857667, 2148764924, 1332989691, 2662323878, 2147483649 ], + "isChannelOpener" : true, + "paysCommitTxFees" : true, + "initFeatures" : { + "activated" : { + "option_route_blinding" : "optional", + "option_provide_storage" : "optional", + "option_dual_fund" : "optional", + "splice_prototype" : "optional", + "payment_secret" : "mandatory", + "gossip_queries_ex" : "optional", + "option_anchor_outputs" : "optional", + "option_quiesce" : "optional", + "option_data_loss_protect" : "optional", + "var_onion_optin" : "mandatory", + "option_static_remotekey" : "optional", + "option_support_large_channel" : "optional", + "option_anchors_zero_fee_htlc_tx" : "optional", + "option_channel_type" : "mandatory", + "basic_mpp" : "optional", + "gossip_queries" : "optional" + }, + "unknown" : [ 50001 ] + } + }, + "remoteParams" : { + "nodeId" : "02bbbb671d15145722fb8c28d732cddb249bcc6652ed2b297ff1f77a18371b1e63", + "revocationBasepoint" : "02d1adfda1fb776dee79bb47dfbef1e9aabd84f9b52069ef667ca7dda5e9095d14", + "paymentBasepoint" : "0345f2d1602b1028e2ecdcbb26d3536ffa58b437305da69c823c70bd14a18ebcf8", + "delayedPaymentBasepoint" : "02b7b0a53ed7e69c53551ab61be3ec157f2cb4e3394d38ee37a412a70b995929ea", + "htlcBasepoint" : "03723fcadf475548206a498b5214289b5301fc83561b91c218ad3bf6f6d2aef5da", + "initFeatures" : { + "activated" : { + "option_route_blinding" : "optional", + "option_dual_fund" : "optional", + "splice_prototype" : "optional", + "payment_secret" : "mandatory", + "gossip_queries_ex" : "optional", + "option_anchor_outputs" : "optional", + "option_quiesce" : "optional", + "option_data_loss_protect" : "optional", + "var_onion_optin" : "mandatory", + "option_static_remotekey" : "optional", + "option_support_large_channel" : "optional", + "option_anchors_zero_fee_htlc_tx" : "optional", + "option_channel_type" : "mandatory", + "basic_mpp" : "optional", + "gossip_queries" : "optional" + }, + "unknown" : [ ] + } + }, + "channelFlags" : { + "nonInitiatorPaysCommitFees" : false, + "announceChannel" : true + } + }, + "changes" : { + "localChanges" : { + "proposed" : [ ], + "signed" : [ ], + "acked" : [ ] + }, + "remoteChanges" : { + "proposed" : [ ], + "acked" : [ ], + "signed" : [ ] + }, + "localNextHtlcId" : 0, + "remoteNextHtlcId" : 0 + }, + "active" : [ { + "fundingTxIndex" : 2, + "fundingInput" : "f56735ce5c9e57ce3094136bb18550f6d097087cf09fafcd67229f04f6f7de18:0", + "fundingAmount" : 2100000, + "localFunding" : { + "status" : "confirmed", + "shortChannelId" : "1729x27x0" + }, + "remoteFunding" : { + "status" : "locked" + }, + "commitmentFormat" : "anchor_outputs", + "localCommitParams" : { + "dustLimit" : 1100, + "htlcMinimum" : 0, + "maxHtlcValueInFlight" : 500000000, + "maxAcceptedHtlcs" : 100, + "toSelfDelay" : 720 + }, + "localCommit" : { + "index" : 0, + "spec" : { + "htlcs" : [ ], + "commitTxFeerate" : 2500, + "toLocal" : 1400000000, + "toRemote" : 700000000 + }, + "txId" : "129afb31943781baadeda527158d17c9cba6654dc4576c2fb3cbb6c5537164dd", + "remoteSig" : { + "sig" : "04de3a1e42ea04f9aca0c167623150ef86dc28c1bac69272c90574bb272e365f2ce1e09872ed0e3c97cbdf62a0494c38317d227d45fb8836585ecd9bf4439dde" + }, + "htlcRemoteSigs" : [ ] + }, + "remoteCommitParams" : { + "dustLimit" : 1000, + "htlcMinimum" : 1000, + "maxHtlcValueInFlight" : 1000000000, + "maxAcceptedHtlcs" : 30, + "toSelfDelay" : 144 + }, + "remoteCommit" : { + "index" : 0, + "spec" : { + "htlcs" : [ ], + "commitTxFeerate" : 2500, + "toLocal" : 700000000, + "toRemote" : 1400000000 + }, + "txId" : "300c847b813a3dba6ff6eaa6a149ee88931f79699cd037070a79b59a0fddf7a2", + "remotePerCommitmentPoint" : "02078accc2dbdccefc30355c2bbf2482d49c825f8a3c9be0c6948ec9fc06b0a375" + } + } ], + "inactive" : [ { + "fundingTxIndex" : 1, + "fundingInput" : "b58263d22fffe786d56e69506e17b3f2bdfa9f1fee1cee9712d2d7c447077993:1", + "fundingAmount" : 2000000, + "localFunding" : { + "status" : "confirmed", + "shortChannelId" : "1105x37x1" + }, + "remoteFunding" : { + "status" : "locked" + }, + "commitmentFormat" : "anchor_outputs", + "localCommitParams" : { + "dustLimit" : 1100, + "htlcMinimum" : 0, + "maxHtlcValueInFlight" : 500000000, + "maxAcceptedHtlcs" : 100, + "toSelfDelay" : 720 + }, + "localCommit" : { + "index" : 0, + "spec" : { + "htlcs" : [ ], + "commitTxFeerate" : 2500, + "toLocal" : 1300000000, + "toRemote" : 700000000 + }, + "txId" : "fa3b8ebdce81796a56592f34f2eca3fc9561f2d389f14cc12d57cb91894f69e9", + "remoteSig" : { + "sig" : "7f858ab4d1214ee849fb49f271dda4e413d5b09eef481cbeee3d2ecfbbc49f984dc657184b9a05add35cc14200578c73a78fdb4f6c753399551c5688bb2698ea" + }, + "htlcRemoteSigs" : [ ] + }, + "remoteCommitParams" : { + "dustLimit" : 1000, + "htlcMinimum" : 1000, + "maxHtlcValueInFlight" : 1000000000, + "maxAcceptedHtlcs" : 30, + "toSelfDelay" : 144 + }, + "remoteCommit" : { + "index" : 0, + "spec" : { + "htlcs" : [ ], + "commitTxFeerate" : 2500, + "toLocal" : 700000000, + "toRemote" : 1300000000 + }, + "txId" : "14485a091401962722c69a3faa6c46a3983599b0b81bb02f984a4a95f7cbf4d9", + "remotePerCommitmentPoint" : "02078accc2dbdccefc30355c2bbf2482d49c825f8a3c9be0c6948ec9fc06b0a375" + } + }, { + "fundingTxIndex" : 0, + "fundingInput" : "33ef5423077aeefdd089084fc60b8d2af55086d0229ee87917ed28e711bf3060:0", + "fundingAmount" : 1500000, + "localFunding" : { + "status" : "confirmed", + "shortChannelId" : "400000x42x0" + }, + "remoteFunding" : { + "status" : "locked" + }, + "commitmentFormat" : "anchor_outputs", + "localCommitParams" : { + "dustLimit" : 1100, + "htlcMinimum" : 0, + "maxHtlcValueInFlight" : 500000000, + "maxAcceptedHtlcs" : 100, + "toSelfDelay" : 720 + }, + "localCommit" : { + "index" : 0, + "spec" : { + "htlcs" : [ ], + "commitTxFeerate" : 2500, + "toLocal" : 800000000, + "toRemote" : 700000000 + }, + "txId" : "3fd0ad57b0ba68c68e403498a47befa62f0af2add84d6bcb44c6f6de6e8e76bd", + "remoteSig" : { + "sig" : "578e73b9d3e6486935735797fc26083621faa2dc715580c24f48e1cd4f67fad1524967f32403ce30e8ea1019c9e72b532c8aeabf0fd9a8a896ef4f69cb562022" + }, + "htlcRemoteSigs" : [ ] + }, + "remoteCommitParams" : { + "dustLimit" : 1000, + "htlcMinimum" : 1000, + "maxHtlcValueInFlight" : 1000000000, + "maxAcceptedHtlcs" : 30, + "toSelfDelay" : 144 + }, + "remoteCommit" : { + "index" : 0, + "spec" : { + "htlcs" : [ ], + "commitTxFeerate" : 2500, + "toLocal" : 700000000, + "toRemote" : 800000000 + }, + "txId" : "e381da618dac2b7259c7ed0933f1a3f6eff44014b86cc7636129b41c718af348", + "remotePerCommitmentPoint" : "02078accc2dbdccefc30355c2bbf2482d49c825f8a3c9be0c6948ec9fc06b0a375" + } + } ], + "remoteNextCommitInfo" : "030e5de3e6b7f7eee32b8beefbe40fdbffdfa7428ccf1b0fd94292db4576ab0a3e", + "remotePerCommitmentSecrets" : null, + "originChannels" : { } + }, + "aliases" : { + "localAlias" : "0x201163ba9b2d775", + "remoteAlias" : "0x38ac563ce60e886" + }, + "lastAnnouncement_opt" : { + "nodeSignature1" : "7e1b49b6cb40bb1babb9dc9b11cfe10712bb6102356537f22a36e6d4a70f233917a9ed4979054007f68ed8119c1676c7a79e95fb55ff31ccb63ca784487b264f", + "nodeSignature2" : "b13a1062961ec5361f84f6703594bb940901722c31e0f769007cb41c65772c020d7837076c1a516b1fc11cb93d4344ebf9d3ef007d481bb4c09656b26231236b", + "bitcoinSignature1" : "c354ffd0de3b1ea69cf09ea3918bacf65dc208f059bf5ebe2433328cb6aae8f54d7f4ea787a9eb137f945f30afca3846f0c84febfb02fa9bab390a1bd413fa09", + "bitcoinSignature2" : "ab0bdff42031f002f3b2adf4facf1493dbfc6ea7e5f463c557bf1d252a9f1f331442bde73a7a0472c6b54195cc00921c2f7e492d80523467d6dabdaae50e19e4", + "features" : { + "activated" : { }, + "unknown" : [ ] + }, + "chainHash" : "06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f", + "shortChannelId" : "400000x42x0", + "nodeId1" : "02aaaa00ce2f18a967dc4f25f414e671ba6585f8ef0b8c5fb812c21064f55a2eaa", + "nodeId2" : "02bbbb671d15145722fb8c28d732cddb249bcc6652ed2b297ff1f77a18371b1e63", + "bitcoinKey1" : "028539aa68248b7d478cc9dde8ca4eac0d6d429df4e6c3413f2d75b98954668ea6", + "bitcoinKey2" : "03c0b536754fdd60b52b87c23410a2ddc099e5a61cd481a4c42136ed515f51cad0", + "tlvStream" : { } + }, + "channelUpdate" : { + "signature" : "e8f087397502818833701813105c3a4effb1563759067f27cd775f9accad82c64dd6d3c98d9db3845269a1c104f4c14b4263e75c95584b4e27f048bd740a3b79", + "chainHash" : "06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f", + "shortChannelId" : "400000x42x0", + "timestamp" : { + "iso" : "2025-09-05T07:42:06Z", + "unix" : 1757058126 + }, + "messageFlags" : { + "dontForward" : false + }, + "channelFlags" : { + "isEnabled" : true, + "isNode1" : true + }, + "cltvExpiryDelta" : 144, + "htlcMinimumMsat" : 1000, + "feeBaseMsat" : 546000, + "feeProportionalMillionths" : 10, + "htlcMaximumMsat" : 500000000, + "tlvStream" : { } + } +} \ No newline at end of file diff --git a/eclair-core/src/test/resources/nonreg/codecs/030000-DATA_WAIT_FOR_FUNDING_CONFIRMED/funder/data.bin b/eclair-core/src/test/resources/nonreg/codecs/050006-DATA_NORMAL/announced/data.bin similarity index 80% rename from eclair-core/src/test/resources/nonreg/codecs/030000-DATA_WAIT_FOR_FUNDING_CONFIRMED/funder/data.bin rename to eclair-core/src/test/resources/nonreg/codecs/050006-DATA_NORMAL/announced/data.bin index 0f0ba269a5..d96d928a61 100644 --- a/eclair-core/src/test/resources/nonreg/codecs/030000-DATA_WAIT_FOR_FUNDING_CONFIRMED/funder/data.bin +++ b/eclair-core/src/test/resources/nonreg/codecs/050006-DATA_NORMAL/announced/data.bin @@ -1 +1 @@ -030000e917adc681383fe00f779c6144a1bd91135ba2c9862ad1bc5aa8a14d37bae3f401010002aaaa00ce2f18a967dc4f25f414e671ba6585f8ef0b8c5fb812c21064f55a2eaa0009f3ef2134fbf50054ec0414b18905a8d87d8b0931410173e35923fa5d4c5a3f5280000001000000000000044c000000001dcd65000000000000002710000000000000000000900064ff160014fec406ef7a0258cb503fe1f1803787d971eeb4d10000186b020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002498202bbbb671d15145722fb8c28d732cddb249bcc6652ed2b297ff1f77a18371b1e6300000000000003e8ffffffffffffffff0000000000004e2000000000000003e80090001e02e3048a4918587b33fb380e3061b7ac38ef4038551c0f098850d43e45ab1cb28302f7c3bf47cdc640304eda4c761a26dfebfee561de15ba106f3d9982d3ef3fbe1002be361b7bf1bdfb283cff3f83bf16f6f8fb67d3f480b541e76518939f667ab83403fcaec5443dd423f160c9b77a48b2585b186f2d850147f57210d3f8a8c8d754a7021eecb7915d4a3f2b391b9b1fbccbaad2006a1e67a0a03aa041721a817e9a08740000000302498200000000000000000000000000002710000000002faf0800000000000bebc20024e917adc681383fe00f779c6144a1bd91135ba2c9862ad1bc5aa8a14d37bae3f4000000002b40420f0000000000220020a5fb8f636bb73759b4c7e5edd55fd8e9e8e5467c7079709cd22fb519c79ab8b347522102e3048a4918587b33fb380e3061b7ac38ef4038551c0f098850d43e45ab1cb2832103e89d5ecf56f19b5080f2c71629a9da8f6bd027ee282b94d615b82d9f7be14cf952ae7d0200000001e917adc681383fe00f779c6144a1bd91135ba2c9862ad1bc5aa8a14d37bae3f40000000000fd11418002400d0300000000001600142276cff9d96f4696d6e504568db62088428706e0b8180c0000000000220020ad1b593fc0780225407ba65c612b974b4b66610e129f21064cb8ada0ffe4c8d2b72621201ea9cffd2af82f6c14251bd59ffa3e876178c7b263f16d12790c33fc093eaed053dd9e0d49f79558aeb3c2fe7fe84f95a91eecbea2679fb65916ad9632df07b400000000000000000000000000002710000000000bebc200000000002faf080010e4205393672b61bbde4261441a2cf8d08ae50fdb813df8efaac6c6090ef290032a992c123095216f7937a8b0baf442211eeb57942d586854a61a0dc6b01ca6ee000000000000000000000000000000000000000000000000000000000000ff030af74aa1e98668a504d50fe6f664aff3fbdb5c8681f0667c34cdb80024fb950f24e917adc681383fe00f779c6144a1bd91135ba2c9862ad1bc5aa8a14d37bae3f4000000002b40420f0000000000220020a5fb8f636bb73759b4c7e5edd55fd8e9e8e5467c7079709cd22fb519c79ab8b347522102e3048a4918587b33fb380e3061b7ac38ef4038551c0f098850d43e45ab1cb2832103e89d5ecf56f19b5080f2c71629a9da8f6bd027ee282b94d615b82d9f7be14cf952ae000000ff5e020000000101010101010101010101010101010101010101010101010101010101010101012a00000000ffffffff0140420f0000000000220020a5fb8f636bb73759b4c7e5edd55fd8e9e8e5467c7079709cd22fb519c79ab8b3000000000000000000061a800000820000000000000000000000000000000000000000000000000000000000000000e917adc681383fe00f779c6144a1bd91135ba2c9862ad1bc5aa8a14d37bae3f40000bd55d8660f54a1be6e123e2ed9cd6669d90b830d99dc6f9addd9ae65c447ea6b657e2fe39841f66c1d6a2fc80ad59e6f3d9bfaf177f4e95a579f0683bf3e9790 \ No newline at end of file +05000601635f355f559285ba7df0603df45ecf2a4d2e19dc5025cbfced52ae7d3518402101010002aaaa00ce2f18a967dc4f25f414e671ba6585f8ef0b8c5fb812c21064f55a2eaa0009f1be4fa2ef24fbc4722591a2c93ebf23eeaacf0e917275156fbcecfd3f4d2e7980000001ff0000000000002710c000000000186b0200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000180802aa698202bbbb671d15145722fb8c28d732cddb249bcc6652ed2b297ff1f77a18371b1e63ff0000000000004e20034a9fcdb3b656feca23e62dec62f971f3da8841cfb596991db1e94b4fa31bd75a03bcb4056f62387ef42a2eb855b79a94054c114aa815e3d3f8cc7f3f61e5027d2803cd767fc0acd4f4e974ba30599c55d2ece63b974aad10d6e30424769eeb1f265103e21f886691747790cde23b2c65a95aaf8b5a3d488187a114343e0c0ad3a72912000000140800000000000000000000000000100802aa69820001000000000000000000000000000000000000000000000000000000000000000100000000000000000000000024635f355f559285ba7df0603df45ecf2a4d2e19dc5025cbfced52ae7d351840210000000000000000000f424002c344d288048d49ce0ac2fb71a40ff0a128c9a301061e69f4949da90fbb25ab6b0400012401010101010101010101010101010101010101010101010101010101010101012a0000002b40420f0000000000220020dcda7a0f7ed8d5f0794b7337b1f32d0849906aa11b8326aa74735a82a555957f061a8000002a000000000202000000000000044c0000000000000000000000001dcd6500006402d000000000000000000000000009c4000000002faf0800000000000bebc2004c1ff676a17a9ddd84b1f1955d0ca6d2d9593a0c05ec4fbf97d7eb979752e9e40194a03db75a8577dd24c7e4e15a5c90b078d8e09e90590f9b3a29e1dec217aee94cadc472f5b379ac31bef8dd4b46b3d4c4838142176f65ea37327317de6053da000000000000000003e800000000000003e8000000003b9aca00001e009000000000000000000000000009c4000000000bebc200000000002faf0800fb278db08cea85146fd04fbf3533cb1dc3c995c0a2356dc97b48da6a0831286e02976ca9b96fb46faeedbcc9be78577da3d6a55aa84248abfa89ad0815eff9dcd3000000ff024e5ac7d7df6e211ff7a7a0a0c7c5bc338b5ca5fdafa693fc6fb212fbe778aade000000000000000103a57caafcb7e674ff01014796cf9418e3fffd01ae74496b7ab70410b6315053f0b9bab6169d3f2f518102acf70eb108e4f9720f5305eceb19c643667f20351e7f5108bdb594d71d4de15c1f3a838edbe32e0e79be60b30057c47fbe6b37896e350bcd4c039f3f6e5a83ce1ad4cc991914fd7df244387593ab9762d1309116cfcd4c93359d240c7014769e56dd6e949782042e3fd5c5c3a3c582ae450844574694311cc8fda45445246b1c28be736bb306db70afc02172b3562b1b827f37290e8c113cd5617c6bddfa32328d3607845da71ba2905d9786d785aa174393b9aecc32605d89b81ae4f7f7c727f2ee455f774176c0893a2eb330de6ed8b03882459d2734b03653b84636a0b8bcf57d8c1a09c010de4309000006226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f061a8000002a000002aaaa00ce2f18a967dc4f25f414e671ba6585f8ef0b8c5fb812c21064f55a2eaa02bbbb671d15145722fb8c28d732cddb249bcc6652ed2b297ff1f77a18371b1e6302c8739e013b1b3de3a3ec74f3e3c28f137b7cac16f85e89c6a0f6ab70276c79cd02c344d288048d49ce0ac2fb71a40ff0a128c9a301061e69f4949da90fbb25ab6b8866ef30667c0e95b4f5c3b959e2fa4d7c3f5dc72a0099c53a82e54c35dcd60db723c3b1582fea00d611ab7ca34a3c56dbf0d8f4362af752e192355238f2746c2006226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f061a8000002a000068ba93080100009000000000000003e8000854d00000000a000000001dcd650001000000 \ No newline at end of file diff --git a/eclair-core/src/test/resources/nonreg/codecs/04000e-DATA_NORMAL/announced/data.json b/eclair-core/src/test/resources/nonreg/codecs/050006-DATA_NORMAL/announced/data.json similarity index 53% rename from eclair-core/src/test/resources/nonreg/codecs/04000e-DATA_NORMAL/announced/data.json rename to eclair-core/src/test/resources/nonreg/codecs/050006-DATA_NORMAL/announced/data.json index 40bc72d116..7a3a08b754 100644 --- a/eclair-core/src/test/resources/nonreg/codecs/04000e-DATA_NORMAL/announced/data.json +++ b/eclair-core/src/test/resources/nonreg/codecs/050006-DATA_NORMAL/announced/data.json @@ -1,34 +1,31 @@ { "type" : "DATA_NORMAL", "commitments" : { - "params" : { - "channelId" : "c380aa11700db0a6d797dfd0be8aecfadad9397e4975f0fa8c9d10db71feac38", + "channelParams" : { + "channelId" : "635f355f559285ba7df0603df45ecf2a4d2e19dc5025cbfced52ae7d35184021", "channelConfig" : [ "funding_pubkey_based_channel_keypath" ], - "channelFeatures" : [ "option_static_remotekey" ], + "channelFeatures" : [ ], "localParams" : { "nodeId" : "02aaaa00ce2f18a967dc4f25f414e671ba6585f8ef0b8c5fb812c21064f55a2eaa", - "fundingKeyPath" : [ 1057736322, 487845715, 3068040012, 2828312867, 3790939166, 2388092911, 4276180524, 217832603, 2147483649 ], - "dustLimit" : 1100, - "maxHtlcValueInFlightMsat" : 500000000, + "fundingKeyPath" : [ 4055781282, 4012178372, 1915064738, 3376332579, 4004171534, 2440197397, 1874652413, 1062022777, 2147483649 ], "initialRequestedChannelReserve_opt" : 10000, - "htlcMinimum" : 0, - "toSelfDelay" : 144, - "maxAcceptedHtlcs" : 100, "isChannelOpener" : true, "paysCommitTxFees" : true, - "walletStaticPaymentBasepoint" : "028feba10d0eafd0fad8fe20e6d9206e6bd30242826de05c63f459a00aced24b12", "initFeatures" : { "activated" : { - "option_support_large_channel" : "optional", "option_route_blinding" : "optional", "option_provide_storage" : "optional", "splice_prototype" : "optional", "payment_secret" : "mandatory", "gossip_queries_ex" : "optional", + "option_anchor_outputs" : "optional", "option_quiesce" : "optional", "option_data_loss_protect" : "optional", "var_onion_optin" : "mandatory", - "option_static_remotekey" : "mandatory", + "option_static_remotekey" : "optional", + "option_support_large_channel" : "optional", + "option_anchors_zero_fee_htlc_tx" : "optional", + "option_channel_type" : "mandatory", "basic_mpp" : "optional", "gossip_queries" : "optional" }, @@ -37,28 +34,25 @@ }, "remoteParams" : { "nodeId" : "02bbbb671d15145722fb8c28d732cddb249bcc6652ed2b297ff1f77a18371b1e63", - "dustLimit" : 1000, - "maxHtlcValueInFlightMsat" : 1000000000, "initialRequestedChannelReserve_opt" : 20000, - "htlcMinimum" : 1000, - "toSelfDelay" : 144, - "maxAcceptedHtlcs" : 30, - "revocationBasepoint" : "02f943c4f199d1425fc6e52f160be536d526e9643af1430cfed4ce63f88beebd2f", - "paymentBasepoint" : "028feba10d0eafd0fad8fe20e6d9206e6bd30242826de05c63f459a00aced24b12", - "delayedPaymentBasepoint" : "03f3b6c8919d313d5bd14a2e567649ea80ed1dd299ebc72766775a1f4fdc3cc1b5", - "htlcBasepoint" : "027ebc1b165dfe376d2a5b7bfba6419af4670b1b89b3aabf8bcbc2b5f33f0db274", + "revocationBasepoint" : "034a9fcdb3b656feca23e62dec62f971f3da8841cfb596991db1e94b4fa31bd75a", + "paymentBasepoint" : "03bcb4056f62387ef42a2eb855b79a94054c114aa815e3d3f8cc7f3f61e5027d28", + "delayedPaymentBasepoint" : "03cd767fc0acd4f4e974ba30599c55d2ece63b974aad10d6e30424769eeb1f2651", + "htlcBasepoint" : "03e21f886691747790cde23b2c65a95aaf8b5a3d488187a114343e0c0ad3a72912", "initFeatures" : { "activated" : { "option_route_blinding" : "optional", "splice_prototype" : "optional", "payment_secret" : "mandatory", "gossip_queries_ex" : "optional", + "option_anchor_outputs" : "optional", "option_quiesce" : "optional", "option_data_loss_protect" : "optional", "var_onion_optin" : "mandatory", - "option_static_remotekey" : "mandatory", + "option_static_remotekey" : "optional", "option_support_large_channel" : "optional", "option_anchors_zero_fee_htlc_tx" : "optional", + "option_channel_type" : "mandatory", "basic_mpp" : "optional", "gossip_queries" : "optional" }, @@ -86,61 +80,70 @@ }, "active" : [ { "fundingTxIndex" : 0, - "fundingTx" : { - "outPoint" : "38acfe71db109d8cfaf075497e39d9dafaec8abed0df97d7a6b00d7011aa80c3:0", - "amountSatoshis" : 1000000 - }, + "fundingInput" : "214018357dae52edfccb2550dc192e4d2acf5ef43d60f07dba8592555f355f63:0", + "fundingAmount" : 1000000, "localFunding" : { "status" : "confirmed", - "txid" : "38acfe71db109d8cfaf075497e39d9dafaec8abed0df97d7a6b00d7011aa80c3", "shortChannelId" : "400000x42x0" }, "remoteFunding" : { - "status" : "not-locked" + "status" : "locked" + }, + "commitmentFormat" : "anchor_outputs", + "localCommitParams" : { + "dustLimit" : 1100, + "htlcMinimum" : 0, + "maxHtlcValueInFlight" : 500000000, + "maxAcceptedHtlcs" : 100, + "toSelfDelay" : 720 }, "localCommit" : { "index" : 0, "spec" : { "htlcs" : [ ], - "commitTxFeerate" : 10000, + "commitTxFeerate" : 2500, "toLocal" : 800000000, "toRemote" : 200000000 }, - "commitTxAndRemoteSig" : { - "commitTx" : { - "txid" : "41418383164935b0753fa1f9b128dff2f1b3608e6b50e8beea4525edf7c3959d", - "tx" : "0200000001c380aa11700db0a6d797dfd0be8aecfadad9397e4975f0fa8c9d10db71feac38000000000036a2ab8002400d030000000000160014761879f7b274ce995f87150a02e75cc0c037e8e3b8180c000000000022002010a0e90d648e62c6aa0d9b9883ee391449f1a1ce28926eb26c3bbdf76c25f631ac678920" - }, - "remoteSig" : "62de565629126afb02a76e67ddf18dfc883cf93bfeb7c807826dd6c8d1bd1f4d55528717f0d2303dc254b4512e32ce82dc7947d5d8e4d38647c2407fc53b874a" + "txId" : "4c1ff676a17a9ddd84b1f1955d0ca6d2d9593a0c05ec4fbf97d7eb979752e9e4", + "remoteSig" : { + "sig" : "94a03db75a8577dd24c7e4e15a5c90b078d8e09e90590f9b3a29e1dec217aee94cadc472f5b379ac31bef8dd4b46b3d4c4838142176f65ea37327317de6053da" }, - "htlcTxsAndRemoteSigs" : [ ] + "htlcRemoteSigs" : [ ] + }, + "remoteCommitParams" : { + "dustLimit" : 1000, + "htlcMinimum" : 1000, + "maxHtlcValueInFlight" : 1000000000, + "maxAcceptedHtlcs" : 30, + "toSelfDelay" : 144 }, "remoteCommit" : { "index" : 0, "spec" : { "htlcs" : [ ], - "commitTxFeerate" : 10000, + "commitTxFeerate" : 2500, "toLocal" : 200000000, "toRemote" : 800000000 }, - "txid" : "287a0627974d69a85199233f5cb8e86e7d04d8afdbc0faa4e56587833838c5af", - "remotePerCommitmentPoint" : "03293b6fcd6474a6c8dff63eeed1ca704c15d5bb80177025c2c45a4f995723393d" + "txId" : "fb278db08cea85146fd04fbf3533cb1dc3c995c0a2356dc97b48da6a0831286e", + "remotePerCommitmentPoint" : "02976ca9b96fb46faeedbcc9be78577da3d6a55aa84248abfa89ad0815eff9dcd3" } } ], "inactive" : [ ], - "remoteNextCommitInfo" : "021492cff600e90bf43cfa885be167e2596428e3ab962ecc1f86df9d0ab6d0440c", + "remoteNextCommitInfo" : "024e5ac7d7df6e211ff7a7a0a0c7c5bc338b5ca5fdafa693fc6fb212fbe778aade", "remotePerCommitmentSecrets" : null, "originChannels" : { } }, "aliases" : { - "localAlias" : "0x37ca53d75c1340", - "remoteAlias" : "0x189a8cb29e4b168" + "localAlias" : "0x3a57caafcb7e674", + "remoteAlias" : "0x1014796cf9418e3" }, "lastAnnouncement_opt" : { - "nodeSignature1" : "c5a72f183a9fc823c5f5e28c2803d2042c563f291aafa2fb5e97b3cad51014b50aafbad94e32c6326bf2865a4a3d5f2cd57c9eff6173b4328dae116ed5a04e38", - "nodeSignature2" : "9d6e2543f51f5cee25d2bfdf2273f278362f09935f4749433cf81ca1b8004a223d7196b8c8e241f8b723d7e47124557d977b0cb40fffef57d8123d0a633e2452", - "bitcoinSignature1" : "9f91b0bacabbf1008077f58571453ba25bf9b1bf01c3511e255f533eb10776e34a04390a4ad8539825346f9a122016c3b3cc8e176ed5235e27ec1fa7ff0b3899", - "bitcoinSignature2" : "7fc5d33308e2a870ff1feb1cad3271801da93aed362fe13ea9f33f543e2a372a5f4de2566bba858b95e3df8d7ba9c10647a6acea455598a9b84d21a876525de9", + "nodeSignature1" : "74496b7ab70410b6315053f0b9bab6169d3f2f518102acf70eb108e4f9720f5305eceb19c643667f20351e7f5108bdb594d71d4de15c1f3a838edbe32e0e79be", + "nodeSignature2" : "60b30057c47fbe6b37896e350bcd4c039f3f6e5a83ce1ad4cc991914fd7df244387593ab9762d1309116cfcd4c93359d240c7014769e56dd6e949782042e3fd5", + "bitcoinSignature1" : "c5c3a3c582ae450844574694311cc8fda45445246b1c28be736bb306db70afc02172b3562b1b827f37290e8c113cd5617c6bddfa32328d3607845da71ba2905d", + "bitcoinSignature2" : "9786d785aa174393b9aecc32605d89b81ae4f7f7c727f2ee455f774176c0893a2eb330de6ed8b03882459d2734b03653b84636a0b8bcf57d8c1a09c010de4309", "features" : { "activated" : { }, "unknown" : [ ] @@ -149,17 +152,17 @@ "shortChannelId" : "400000x42x0", "nodeId1" : "02aaaa00ce2f18a967dc4f25f414e671ba6585f8ef0b8c5fb812c21064f55a2eaa", "nodeId2" : "02bbbb671d15145722fb8c28d732cddb249bcc6652ed2b297ff1f77a18371b1e63", - "bitcoinKey1" : "029fbe1c2fe0d86e562a09aedaca23dabee01159d288ef3d6ea85ed107dd51db8a", - "bitcoinKey2" : "03613ca8550a9ce1252ed80d39dce2e56f5befdedd70019e195bc0e1448e48b4d6", + "bitcoinKey1" : "02c8739e013b1b3de3a3ec74f3e3c28f137b7cac16f85e89c6a0f6ab70276c79cd", + "bitcoinKey2" : "02c344d288048d49ce0ac2fb71a40ff0a128c9a301061e69f4949da90fbb25ab6b", "tlvStream" : { } }, "channelUpdate" : { - "signature" : "61051293e9bb63a6e2182bce3dba766e0b0f69d546dcc059aee70e81fac8db1e56d2b10e4ecee3b99aaad8751fd396dbe9a6ba68d8c153dbc5e862afafd31daa", + "signature" : "66ef30667c0e95b4f5c3b959e2fa4d7c3f5dc72a0099c53a82e54c35dcd60db723c3b1582fea00d611ab7ca34a3c56dbf0d8f4362af752e192355238f2746c20", "chainHash" : "06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f", "shortChannelId" : "400000x42x0", "timestamp" : { - "iso" : "2024-12-31T16:03:46Z", - "unix" : 1735661026 + "iso" : "2025-09-05T07:36:40Z", + "unix" : 1757057800 }, "messageFlags" : { "dontForward" : false diff --git a/eclair-core/src/test/resources/nonreg/codecs/04000d-DATA_WAIT_FOR_DUAL_FUNDING_READY/fundee/data.bin b/eclair-core/src/test/resources/nonreg/codecs/050006-DATA_NORMAL/fundee/data.bin similarity index 59% rename from eclair-core/src/test/resources/nonreg/codecs/04000d-DATA_WAIT_FOR_DUAL_FUNDING_READY/fundee/data.bin rename to eclair-core/src/test/resources/nonreg/codecs/050006-DATA_NORMAL/fundee/data.bin index d715289a13..dc999dacd4 100644 --- a/eclair-core/src/test/resources/nonreg/codecs/04000d-DATA_WAIT_FOR_DUAL_FUNDING_READY/fundee/data.bin +++ b/eclair-core/src/test/resources/nonreg/codecs/050006-DATA_NORMAL/fundee/data.bin @@ -1 +1 @@ -04000d01ae58e31828b115b8a900a9d20b060ef2b2da2fcfe18991aff34168579d246b540101041040100002bbbb671d15145722fb8c28d732cddb249bcc6652ed2b297ff1f77a18371b1e63000998f8a31f9a789b354eb0e09189cf977ceaffb314959c8e8774ed47480e1bde928000000000000000000003e8000000003b9aca0000000000000003e80090001e0000000000140800000000000000000000000000000822aa698202aaaa00ce2f18a967dc4f25f414e671ba6585f8ef0b8c5fb812c21064f55a2eaa000000000000044c000000001dcd6500000000000000000000900064038039cb4b63c81052cf0893e27f7be58a5808453dffd44c662b1a75aebb1fc74e0394e61db71e79c20f2ef131e99121996c7ae04330c4c50dae6bccc5afcf493661029a15b644f2800a507565666956446c05ef8dd7b60023852607c97471d233d62e02c633358670a7235d82a5b732d3f7c21e010ea411c805f0f319d922300df453c00000186b0200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000080822aa69820000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000003a570258bd99dfc5f235fcd03cad5b91533b1e156e38ee8638577ade1eb02954309fd019e020000000001023e98556e317c32c22b43d8f4aa7e31466f70b367ac61aaf4887a492df854b2800000000000000000001e3142211e1eb2114afd8a91d6a240d6a701bc01954e9f906206767e2a927d0d0000000000000000000360e31600000000002200204f6effc7a2d2d74b28ca1dcc262651c170c809a10d41bf5ab54f2c250ae53f6c0a770100000000001600142b08f4f677adeacc5dd403c01d77aade558f4d84c26e0100000000001600142b08f4f677adeacc5dd403c01d77aade558f4d840247304402201fc1bc68327665b99df0ed4f2deaba3481e04d2636b8abb3cbb7f27b8d381b0e02204308de7d6ac92419939feb2e69b780cc06f2f979104fc4ada04968de7e49f9db012103ec0636475c0250cae58d86fb4876f516ef3ba2052f9216e88816f4043fb1a25502483045022100a508a1276bfc7ebb2c23c89a25f4f463e5876e91a43ee55a04bb4d433c929f2f02200901a48fa1f05d4a6ef6d01e8c065fe59ccc129fce36eb2df49c719537159b34012103ec0636475c0250cae58d86fb4876f516ef3ba2052f9216e88816f4043fb1a255801a0600ffb0ae58e31828b115b8a900a9d20b060ef2b2da2fcfe18991aff34168579d246b54934351ffe48993f070a665d6e22a2c023b567ccc2f73489213cfeb6c9a2c05e00001006c02483045022100a508a1276bfc7ebb2c23c89a25f4f463e5876e91a43ee55a04bb4d433c929f2f02200901a48fa1f05d4a6ef6d01e8c065fe59ccc129fce36eb2df49c719537159b34012103ec0636475c0250cae58d86fb4876f516ef3ba2052f9216e88816f4043fb1a255000100000000000000000000000009c4000000001dcd6500000000003b9aca0024934351ffe48993f070a665d6e22a2c023b567ccc2f73489213cfeb6c9a2c05e0000000002b60e31600000000002200204f6effc7a2d2d74b28ca1dcc262651c170c809a10d41bf5ab54f2c250ae53f6c47522103a570258bd99dfc5f235fcd03cad5b91533b1e156e38ee8638577ade1eb0295432103e03c071ae90c35685c88048da0b1900948446ddbace3e41cd7a142967d584e8552aedf0200000001934351ffe48993f070a665d6e22a2c023b567ccc2f73489213cfeb6c9a2c05e000000000001a741d80044a010000000000002200209698cbfb709ad73804217ae67e86ea857e8c1b317c82e7740670eb6def38ea334a01000000000000220020d468d18ef4bd63800e3e59bbb204a2921dc75cec9786e363ba2d33d467ae98e820a10700000000002200200e84b31c024b1498353109b6127546c1c1999edf47edb4c772c2108edeb524b4b2340f0000000000220020c38ddef3ceb25118b96983889833cbecd3fdeaf190083eef04ee451b70b47e04a71aae20c70d3fc3e720cc4fd1a2e07b97d454392ecfbe27c0ef2eed401538466924f6c24accb834eed784536fe5d30656eb8308a8b0f48bdba2ccc9812ce49a12b9dd80000000000000000000000000000009c4000000003b9aca00000000001dcd6500c98fac755c41c1688a55f820a3ac9c4c8b2adde1d6687b2a9d843b364211965f02df0aedf5ee7eba9dbe640dfd3b2da6e108cf52568e4de3e492ed4366355a33c0000000ff028a7d253bc83df4357f3c1c89d9040cc55bc5a64da5828e523840a5083254362900000000000001061a8000002a00000001015e6c55cb4a946e00 \ No newline at end of file +05000601641269d5601610b37f6bcb6bb2f9ddff20d8310d25713a56818d4830b239442501010002bbbb671d15145722fb8c28d732cddb249bcc6652ed2b297ff1f77a18371b1e6300091818586c2396348164e248e80e14dee3c2289e406ce830e9cca7e0aa5ddaed3580000000ff0000000000004e20000000000000140800000000000000000000000000100802aa698202aaaa00ce2f18a967dc4f25f414e671ba6585f8ef0b8c5fb812c21064f55a2eaaff0000000000002710028b35568172f857fe22740da6b32d1b3e33ad0bbf545fa5db54ffbea1c1b2328f020be394219f4cf2822dc57de788d764e2caf266bdcade9547ff125caf2222d0af0274b0a98730861d77c1ec13de8d08a5147c52ac43eae03f9eabf2ac2c3e240f1e02ee6464db4797a42c961fd4c9def7959bd4bcef4689930e091360aad0cb9942e20000186b0200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000180802aa6982000000000000000000000000000000000000000000000000000000000002000201fd05b1641269d5601610b37f6bcb6bb2f9ddff20d8310d25713a56818d4830b239442500000000000000000000000002faf080c3e4d0788c4dd8e68b6f04411797d215f3baacfd15246f77285e299e12e4859c00061b1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fe0001a147010701fd05b1641269d5601610b37f6bcb6bb2f9ddff20d8310d25713a56818d4830b239442500000000000000010000000002faf080c3e4d0788c4dd8e68b6f04411797d215f3baacfd15246f77285e299e12e4859c00061b1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fe0001a1470107000100000000000000000000000024641269d5601610b37f6bcb6bb2f9ddff20d8310d25713a56818d4830b23944250000000000000000000f424002c12fbaf73b241d613ccc2f9973bc39e6fa9c9ed1ac91aef8c09cca8ec0cc8eee0400012401010101010101010101010101010101010101010101010101010101010101012a0000002b40420f0000000000220020e8fb4596b74e3a8f040d133b039629ed2f2ce7aaf4094c76451bedfe18615ce2061a8000002a00000000020200000000000003e800000000000003e8000000003b9aca00001e009000000000000000010002010000000000000000010000000000000001000009c4000000000bebc2000000000029b927009b01bf0040b0e0845e6878622ef79f99ab28512ba6c04d7f1c0b8d0594b9e2bb01d63a6d40f1206b8b1c2eaddba4952055f37ca157ad6c582ab32785ad124f381417d562a736ed9303267d6be893f8cd30394e50d391ab0a83709514ba3f26b63700022fefa2d3c471ce7af1b43e92f8717cbaca09115dfa030c19a21060d7813c3a5443a4989c63d238eda5fe1aa546c1206b0be05be76155f53d251a9991fd67eba0c8453d2548cea33c0d74ebf5eadc88d239b4078a80bca36f2a88f3c77e94f5185ec362b6373f2676e8ec3f447565097e77480d1dd42e155c1f6d7c609b6f5a83000000000000044c0000000000000000000000001dcd6500006402d000000000000000010002020000000000000000020000000000000001000009c40000000029b92700000000000bebc200631a6d71b946b2c12cd12506d6821db5620ae95348ebdaf0222d99608283402502010336d6fae904025cdb686da76f5a76ea15910930a53dae79672e2af01829ab000000ff0212b4e0c9439ca0eaecb6316dff57b46434faea68e8fb7b4f99a1f94a71329122000100400000ffffffffffff0020d0e37bd5d4335f739c764537a1bcb93efcec3002907978733cc91410d71065f380007fffffffffff80000000000100b7b90ade22611dff002175ef9c3ed6b10088f8f68d17353dbe6c8270bd045c73dfbf61caf5de0a834bce60dfc0400e0c06d31b0dbfde6f7be272f6f3f4489a03369ece3690e42bec7f5e9eae37a80e62039106226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f00b7b90ade22611d68ba92a1030100900000000000000000000858b800000014000000001dcd650001000000 \ No newline at end of file diff --git a/eclair-core/src/test/resources/nonreg/codecs/050006-DATA_NORMAL/fundee/data.json b/eclair-core/src/test/resources/nonreg/codecs/050006-DATA_NORMAL/fundee/data.json new file mode 100644 index 0000000000..0117c91e38 --- /dev/null +++ b/eclair-core/src/test/resources/nonreg/codecs/050006-DATA_NORMAL/fundee/data.json @@ -0,0 +1,188 @@ +{ + "type" : "DATA_NORMAL", + "commitments" : { + "channelParams" : { + "channelId" : "641269d5601610b37f6bcb6bb2f9ddff20d8310d25713a56818d4830b2394425", + "channelConfig" : [ "funding_pubkey_based_channel_keypath" ], + "channelFeatures" : [ ], + "localParams" : { + "nodeId" : "02bbbb671d15145722fb8c28d732cddb249bcc6652ed2b297ff1f77a18371b1e63", + "fundingKeyPath" : [ 404248684, 597046401, 1692551400, 236248803, 3257441856, 1827156201, 3433554090, 1574628661, 2147483648 ], + "initialRequestedChannelReserve_opt" : 20000, + "isChannelOpener" : false, + "paysCommitTxFees" : false, + "initFeatures" : { + "activated" : { + "option_route_blinding" : "optional", + "splice_prototype" : "optional", + "payment_secret" : "mandatory", + "gossip_queries_ex" : "optional", + "option_anchor_outputs" : "optional", + "option_quiesce" : "optional", + "option_data_loss_protect" : "optional", + "var_onion_optin" : "mandatory", + "option_static_remotekey" : "optional", + "option_support_large_channel" : "optional", + "option_anchors_zero_fee_htlc_tx" : "optional", + "option_channel_type" : "mandatory", + "basic_mpp" : "optional", + "gossip_queries" : "optional" + }, + "unknown" : [ ] + } + }, + "remoteParams" : { + "nodeId" : "02aaaa00ce2f18a967dc4f25f414e671ba6585f8ef0b8c5fb812c21064f55a2eaa", + "initialRequestedChannelReserve_opt" : 10000, + "revocationBasepoint" : "028b35568172f857fe22740da6b32d1b3e33ad0bbf545fa5db54ffbea1c1b2328f", + "paymentBasepoint" : "020be394219f4cf2822dc57de788d764e2caf266bdcade9547ff125caf2222d0af", + "delayedPaymentBasepoint" : "0274b0a98730861d77c1ec13de8d08a5147c52ac43eae03f9eabf2ac2c3e240f1e", + "htlcBasepoint" : "02ee6464db4797a42c961fd4c9def7959bd4bcef4689930e091360aad0cb9942e2", + "initFeatures" : { + "activated" : { + "option_route_blinding" : "optional", + "option_provide_storage" : "optional", + "splice_prototype" : "optional", + "payment_secret" : "mandatory", + "gossip_queries_ex" : "optional", + "option_anchor_outputs" : "optional", + "option_quiesce" : "optional", + "option_data_loss_protect" : "optional", + "var_onion_optin" : "mandatory", + "option_static_remotekey" : "optional", + "option_support_large_channel" : "optional", + "option_anchors_zero_fee_htlc_tx" : "optional", + "option_channel_type" : "mandatory", + "basic_mpp" : "optional", + "gossip_queries" : "optional" + }, + "unknown" : [ 50001 ] + } + }, + "channelFlags" : { + "nonInitiatorPaysCommitFees" : false, + "announceChannel" : false + } + }, + "changes" : { + "localChanges" : { + "proposed" : [ ], + "signed" : [ ], + "acked" : [ ] + }, + "remoteChanges" : { + "proposed" : [ ], + "acked" : [ ], + "signed" : [ ] + }, + "localNextHtlcId" : 0, + "remoteNextHtlcId" : 2 + }, + "active" : [ { + "fundingTxIndex" : 0, + "fundingInput" : "254439b230488d81563a71250d31d820ffddf9b26bcb6b7fb3101660d5691264:0", + "fundingAmount" : 1000000, + "localFunding" : { + "status" : "confirmed", + "shortChannelId" : "400000x42x0" + }, + "remoteFunding" : { + "status" : "locked" + }, + "commitmentFormat" : "anchor_outputs", + "localCommitParams" : { + "dustLimit" : 1000, + "htlcMinimum" : 1000, + "maxHtlcValueInFlight" : 1000000000, + "maxAcceptedHtlcs" : 30, + "toSelfDelay" : 144 + }, + "localCommit" : { + "index" : 1, + "spec" : { + "htlcs" : [ { + "direction" : "IN", + "id" : 0, + "amountMsat" : 50000000, + "paymentHash" : "c3e4d0788c4dd8e68b6f04411797d215f3baacfd15246f77285e299e12e4859c", + "cltvExpiry" : 400144 + }, { + "direction" : "IN", + "id" : 1, + "amountMsat" : 50000000, + "paymentHash" : "c3e4d0788c4dd8e68b6f04411797d215f3baacfd15246f77285e299e12e4859c", + "cltvExpiry" : 400144 + } ], + "commitTxFeerate" : 2500, + "toLocal" : 200000000, + "toRemote" : 700000000 + }, + "txId" : "9b01bf0040b0e0845e6878622ef79f99ab28512ba6c04d7f1c0b8d0594b9e2bb", + "remoteSig" : { + "sig" : "d63a6d40f1206b8b1c2eaddba4952055f37ca157ad6c582ab32785ad124f381417d562a736ed9303267d6be893f8cd30394e50d391ab0a83709514ba3f26b637" + }, + "htlcRemoteSigs" : [ "2fefa2d3c471ce7af1b43e92f8717cbaca09115dfa030c19a21060d7813c3a5443a4989c63d238eda5fe1aa546c1206b0be05be76155f53d251a9991fd67eba0", "c8453d2548cea33c0d74ebf5eadc88d239b4078a80bca36f2a88f3c77e94f5185ec362b6373f2676e8ec3f447565097e77480d1dd42e155c1f6d7c609b6f5a83" ] + }, + "remoteCommitParams" : { + "dustLimit" : 1100, + "htlcMinimum" : 0, + "maxHtlcValueInFlight" : 500000000, + "maxAcceptedHtlcs" : 100, + "toSelfDelay" : 720 + }, + "remoteCommit" : { + "index" : 1, + "spec" : { + "htlcs" : [ { + "direction" : "OUT", + "id" : 0, + "amountMsat" : 50000000, + "paymentHash" : "c3e4d0788c4dd8e68b6f04411797d215f3baacfd15246f77285e299e12e4859c", + "cltvExpiry" : 400144 + }, { + "direction" : "OUT", + "id" : 1, + "amountMsat" : 50000000, + "paymentHash" : "c3e4d0788c4dd8e68b6f04411797d215f3baacfd15246f77285e299e12e4859c", + "cltvExpiry" : 400144 + } ], + "commitTxFeerate" : 2500, + "toLocal" : 700000000, + "toRemote" : 200000000 + }, + "txId" : "631a6d71b946b2c12cd12506d6821db5620ae95348ebdaf0222d996082834025", + "remotePerCommitmentPoint" : "02010336d6fae904025cdb686da76f5a76ea15910930a53dae79672e2af01829ab" + } + } ], + "inactive" : [ ], + "remoteNextCommitInfo" : "0212b4e0c9439ca0eaecb6316dff57b46434faea68e8fb7b4f99a1f94a71329122", + "remotePerCommitmentSecrets" : null, + "originChannels" : { } + }, + "aliases" : { + "localAlias" : "0xb7b90ade22611d", + "remoteAlias" : "0x2175ef9c3ed6b1" + }, + "channelUpdate" : { + "signature" : "f8f68d17353dbe6c8270bd045c73dfbf61caf5de0a834bce60dfc0400e0c06d31b0dbfde6f7be272f6f3f4489a03369ece3690e42bec7f5e9eae37a80e620391", + "chainHash" : "06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f", + "shortChannelId" : "47033x712226x24861", + "timestamp" : { + "iso" : "2025-09-05T07:34:57Z", + "unix" : 1757057697 + }, + "messageFlags" : { + "dontForward" : true + }, + "channelFlags" : { + "isEnabled" : true, + "isNode1" : false + }, + "cltvExpiryDelta" : 144, + "htlcMinimumMsat" : 0, + "feeBaseMsat" : 547000, + "feeProportionalMillionths" : 20, + "htlcMaximumMsat" : 500000000, + "tlvStream" : { } + } +} \ No newline at end of file diff --git a/eclair-core/src/test/resources/nonreg/codecs/050006-DATA_NORMAL/funder/data.bin b/eclair-core/src/test/resources/nonreg/codecs/050006-DATA_NORMAL/funder/data.bin new file mode 100644 index 0000000000..da175a30e3 --- /dev/null +++ b/eclair-core/src/test/resources/nonreg/codecs/050006-DATA_NORMAL/funder/data.bin @@ -0,0 +1 @@ +050006014b851a62adaa0c0c003027706666d9e860253463d81971eb22089bb534f9b52601010002aaaa00ce2f18a967dc4f25f414e671ba6585f8ef0b8c5fb812c21064f55a2eaa000907c224dbd80c5d85408e72eb785d9fe6a5294413d2a6ea3d5193b963a533ed3e80000001ff0000000000002710c000000000186b0200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000180802aa698202bbbb671d15145722fb8c28d732cddb249bcc6652ed2b297ff1f77a18371b1e63ff0000000000004e2003368d06fa8bac9366977e1ac914109191f54ab3f0152476a59bf928304300ba6b03b4b6c6e2e0760495c464df937402832c845ec00a32988496747f1606fc46b6260361234d1f650157b084405c89580afe97cbda284a116e09003eb188786a5b02e302ebb64a426889e183e74c45e0c5fd187f6d99d4dc155b4cb4ca063cd0fd5c97d2000000140800000000000000000000000000100802aa6982000000000004fd05b300804b851a62adaa0c0c003027706666d9e860253463d81971eb22089bb534f9b52600000000000000000000000002faf0804206184c2a00aa7b17f33936aaff2b237528a7f1034df562057f9c904b3f34ac00061b1000030db149d7518d85d195295b7f9d64ccf5a393f27ecb2779e800ac4b3a7c5fe1811aea7beaecaca26a4a94eb5b8a26a02f6057c911873f8be25c0de1d7dffa7dc4fc5c3b5c58b4c6611a5c9e10da0764c106e8697763662e55a663224a3a5ed51f3b3ce907b26cac6c00bd7d60b136f1613955221af43ab6634a6a0e3de840d3cb9e776fa9b4e19b1fb264947f3edca39372f31a0cf722af23b5421cf6d9c493de30d6dc57e199798243d23407c0d96c2d7a406203284e8ce5113fc23c7e0b6e4d97882adcaafe15e02a5eb610ff56e5d880680c0afcb00c967e36d4e218c50c386a206f87bd3f68ae29eeeb470695c21e751ceff7f7d66cc64b6d8963e97290f7cdcdc283f6fba3d7057ecbf879d26a211cab7a7787e88ba3031911777c6b43e6433e25b458c262b66249f97e379ef7f722d7541b3ab4a0b762f7ba3dc45bf61bc00003d0246dcf32b77574785549619524425f113fc3e8090f9393401349c752d1fa9ddc399321a0055ae84131a8b8228e381349079ee9adf6976de1dd0cd6e8fd0e248abe382ccb5e465f5c6274dd9d4bbd8c07a9a3f40a3deb21c628831a084dbd5cbc0f923fa753ad51bafa80d800622f59a197062e83897344df59f7a114d46cc6bdc98e77f8e3d1b9c01a5700ac520bf97511763fbcc6f797811f71fe1022422f3b725a1f54da9a056f26ac4bfef17ab0565c37c1183cda6f600eabeafbafcb0183658f6882f02d1a9721fae69d3489969ce30b95d89fd2a5a32c9a8ee5868fe8ac0dcc1713d0bb8f41d8f5aac102d26ffd16993fdcf4239925f9a03d1dea77feb463bdde80cf17c260762f40e65370ea220178b38ab8770a6554d619d5dc4ef9fd21fae392b4e37d03a8cf9cfd556ba449b7f623700a5da2ec30a6f15a54e3fde0705521b21fbedecefc7c7233396f0e18182a6f7840027013a104136b4e46879d3aa73b8aa0d4202da1f5cc448a1b4b1f3ce55168e35e1c094c1db76fa3f93620e2ed2d431bb5f64de1843cfa5c579a54fe74c8376c138bc2b53ac84bd16bc09639db07fef50959970cede04cfae9548eaf8fbc2dd9d745aa4eaabe7278a7b37c77025313ab74b5e7cd892a081d46b47faaf40bd6709882c66c0b28525ee0b38094b4ed3684e022a1cd9aa078b75586aa176a8218b6a019d4909372f5e2ac837d40e779e2eae51f30965581250ea557929b2a6598ebd18e3ed796e59841c5d684cc2134bd40a87d0772c8f8a6f8eb6771294567d213a95cd42db22ce7f10b9efb2c1e58b0759f50d522406f8699721e5fcd6531754593ebd90fa18d9db4fef3a76d5e8c8b129347228ac27ad8fca774c20a2fb300ef4a3778540d2901f4a53dd0d0b1976f594a029b259572ee4ccf7aea11b48ba27b67f58ec1ff9c12c2ff0bb02322c1eba9147fe124165d96b1088068d835f8195ff45ef379b4cc48f14e887d779de7e723e50833bb44b340664c5907d52cccac676df96299645d0d3eb1443fac71abdaeb95a0c440584105aa8c88b5aa57b897125008039d42e094a47b9851c4af2dea480234423e9e8ed86eecc23ad459d051a1244c204e2335d2b613b85629a79efa02ce782776e99c35751d5fdf6dafc30bf994e8077e6b471200a511e0f0aeb5cee775215e8c385bc5122b6fc7ba717162a80a22b73ed4253cd7e6b2cf8cd504f84a939c4b8a1848ad4e93c4bac898a83d0549070b700236377acbc270efb0900cb732fbfa0ed39a7b0037195ba056e9a732a3388179b63166f03327cb307defa3e98a0eedec78778a48a32a4758e4cf9bafe8fa746746d9d4dc75805fe4edcdd37fcd35f9ea7ec8075178ef0fed8166d69d109046ca48c7ad2725a4361b79cf0052717527065a43831fd0b94d5ee09d8f0b0ae937a4307b4c7dfeb299a15d09a15fd3f0998867ddf1f6d042fafe0001a1470107fd05b300804b851a62adaa0c0c003027706666d9e860253463d81971eb22089bb534f9b5260000000000000001000000000010c8e0b8335cabd041e3f8024bd0b5b10e401868d31ff594b98f2bb79dc28693c7788f00061b10000230caae9f655304aa6eaf036c4d1839012b5b63cd8041bb0c70d1f9dead16a16c67ad62b6d988a208ff75b4b2b17d5594e071360dd7118259f8b855e1af695394edcc0cd67d28c94f63bc686e214bb3b6537d9fbf6afc14e1a68e8ab3d202feedceab7c01a02e15426672f8b0434b5f319c8230df38096a28c4c05cc4cde86ba3c0a15d5892b7ec10711d4983122bd525e9376b373c9e2055b95b4980b180be8dee61a81180668678b610c81cce8df7a07a8e60171b826a823afd77125876a40c5bf7a2cfca6f11a69f1bc6d2770af53daa28b6ea58c60275f53b3985289c2cf8793dab4759c3bfc0cbec4ef15bd8710984ef66117b4852398e5dfef28ae650279d3d5f8b7e8d55506d4dfba3cc3123b66965483a9bb0eb9e6f114cb2ee6f69dca6c53d917e8f0c27f704bed1677338b15a6b8625434c77f284c8711b4983b19134692ce7d3f85559ead3aeaa09e99f3c2a74b157a4768f27856600734b0983b233a222c9c20c2e0ebe3108d9069e62742d270c02a198d5d037488cfccc14dc56b0ca1ddac50e133a217093d598b1a7bbeb6190ee9a85ef4ccc7432dee503aa481f2cb11adf434eb514f8f28ad02a1680aeeb9a743cb350a88402765e1ade1cc0251da42841f1f7de85915c63a42d13942c9d2b1d4d2762f705383f099d6f1f01615fc721266ef45fa20a859e06aea7d44f154be97e4f906096eead6f2c4bff6027d82228265aeba458c6361682ee9cbfb2fac1a179de45b3598444002537393f058c3cff0272c06e18694e280d275616723b7019a540b6c75a6c182696fe5dd89a16a54783d3b533df13c703d081b7b33da3158a0da3cb628393b866a181145337797271e4437d49b0370d50b18d5fc031f35e9c5ddc872f3c47b08c1a9e5c465a59cab419da0da0a2340075b8f71df38c022d97cb97a231898a53fd4d5c20fb2877aa8dba9fdef88936ab36eaccfd31ca674958d2677acfc9c99067000b9a8688723ee3ecea593dc12454bbfd10b73c68abd4c8d099eb55e41ebd8d6a390ca536a7a5fb5370f1a1e3e31fbbe7b256db29b403a94179ad1698d1a7e7503e3744d11bbd349f01d790431b747533272d09217aed55101cded1eabfa705b3c0d6689441f0a03d33fd39bf5ee3dc8896276355906e87c2faf5b9cd74823285f2d58d70db120ab53da6af532900387c107b2803becc793ef0c31c09a5b2c2612652b38910c19238b198c2a11e2f7a2bd51cff9ab01d6d0af3b9c4581b1d7246cbd1f1a10a5b81c0530dcbc8098c2ce283ceca438c0cf4abe734ca8faa0ae8b4849509f445b212e7e1af229db978e67d1c531cd2d9d3c8489c46b8277ddaf260cb727c4a34bda98753007ef1b90e2f83aaa7c5139c3f1c3192acb0ded1cc41170827ccb5fde144bbbb68f139883bfa611af6446a46486fa7c3a5fa9785d87fa229a8503c1e74069dc994f3c284561abfb245c2e7bdbb88d7a8f85ced0df0fb2dd7677878049d0ebd53cbe6abc26b3f34d9e9bb0a1c02812359fc3618060431e1696dc5ca6c157deefc2fbc85a60f6b4ca43c5edb822d55a57591e5632f42f860359f30e3b84f7151d0a2651b2dae1c1a408b2fc2af9f667acd6d2e19ef4f654b48de61b60d13dd011547ec7200cd2a74f26e96e2512e962e073b703300868b6c86a46718c543f896fbaa368e65746bf5b1a4da04b59cd446e38f5c5b244fbfc879b49517c95522547e9a756074c171a43e20d5439803dca0bb0f87589bcb6171992565e1e7f9324b0fd0e025f36dcdada2d14237c1e79ec12497c420c99275f637cdd5c3910f62c8e42ccfa627893b46ae91e13d3b54d1d98408894247c803233fba1d8493aa10e2560b0d2fb4ae5a9881259fdce09e8b5cf7762cbc8ece0a15ba576d4b1d1910b376f2d19f524671275e6e4be33b4d8dfe0001a1470107fd05b300804b851a62adaa0c0c003027706666d9e860253463d81971eb22089bb534f9b5260000000000000002000000000098968053358f633c280126a51c2c7430662b669f24f035b3995d5bb0d57b88cb06799300061b10000279e6e6dcba2ada19aa78346b56df3df3bfc1820ccc9d3138635f4dde381f968e2e0070f136ec8b10359b398b151c464035e95ffac2d4f5ffb9e3bb4cefb8c472ab51a75645be45b2067b5f6abe28ed5b8c96ca5cc5e69a5b6e7c1846344778294285f46f70db900fc519ddf416d940fb10c6f0ad85c9ea803569affb9bf9de13dcefd8111d25b9f62e54b3f536901c84f8ac0ce1a51f514cc917aa32e2a14c0c48253bee78c4086320c73ef3053717e42b3d79426e898ba6b3fee3065eec0750f1f6c1edab4e96baec2306c49a63692ca7c318f647f2a1500cb5b0292752bae3f10da3ee6b35d049e76897515b42ef2b4445da80f5931cb9131d525106d21ba30dadfefb65b25333cb213cd856c91f28f42b88879904201a452bf996b131eef06be945abe1f29d20ff4de145757dca2b51891badd1b0ec756f28aaa4b6f1ca24bd9af228ab76269469dc4798d29fccea61825ccab164f6353f24b198ca98106c639c23fb1b88c45a51fb710316449436641887d077b23b81b8f3bc663d98106ad214da1f51235e4c7119c1074f39e7bfca123c9a665b39a9de36ee84cf3fbf22c3a934768395eb02f3e5b74c642048d799363daf6e9a663433f8ca01d08ce08e711fac40de70b920140bfa762a4b5740062d8e79d20a1e0003bd12412f51995263d4c65faf4912ded001fc7169c5f84e585435f60595550b57c895795a3bb2b3ab7f3a9eda479f96c232b51774c775ce14818fed5e404c3ecbf9ed767ae4e0722e13b703fee6ffd2a3f78eec0cbe80f84297fde055aaed54d4199c66ceef3adbe819876a48ffebe337cec4f222912f4159ba51ec4ab239ba349d40dd7a8d7d15654bc91605d7ff7acecd3cb5781996dd896f70200a6236d3175f0f34fa4c84229a2f76297e45ad925aab2808b898c0712f9c7bebf886308e1879b15deb24efccdf2d97797321a4ce3d46688375376b540c1db049384157da71e3ca534253f4f8e3007996ad80c416b5df9b06fe4f73d3dbac45a95d532792d0ebba2649afd721241eb2ce07a291bffdb77cf9d083d239fce425e9c919fa6d3586368db2abc4eea2dd03bec12e60d8d19f25d873505f04064c2bf591e98b44fb1b3f0b1d1971b335a5d7cf10045bd1008aa40d930f675e3d1ae3b3342de4ec87b3c7cf5ef883bb30e3dd0d63283c22530e4ad469a1e95d9813d6a26f0d518de096eaf39a01862aad09fd99b195b6d8bfae4c096fcf57de19efc032c859bd9800fc1064c36cf43dd1698e86a657e12b36c7cf01d7a24d85a7e1a6196171ac9e62ceca81627180ad76196873d2eb15b2717eb2dcd4f28b87169139ad6a17405e5e095f949781b0ecd5a339683d0f01e6af2125b20098f2678b0be00ae4dcbaabc4e36943e92372a4a1a2600e9a7f113fe0a3669839709986f9366b254a5b67849a5d7af091b016579de6f4a30a49c4723fa470a55fbce52387f1b929ed6933008003d962351fe1f1a5ad1c66fbd7ab81ee06a7e1b884b5b1816b89575da49618f56de6dc7566f263b5e7749463ea40aa3c7dc417bc5bf616b6f1a8f80e83cf95caa9e060214e2ca6bb8bddef8011a18134616fa7c2c1c957737a556f204a3d9945ef6275b532c8064bb711220291a4c628d34982bec5185f437e12ef168d76213a3654feef53b22e976b1af10998218871673e6b69fd7d84834dd4926a1f4b7b3f9189b2e6cfe061eb44e1caf7e95efddb26d26f2ccf736b64fdede50825e9df37ead839d2d6b8b6079092c2bc63724952ab0779ac6e05dc41fb014a4fe1a69ed8ae4e5879462f00dcddccdd5d2a11b15ec953fd80408d8a5a60824c2f9e500591e14cff01887b8c8aaed69ff3df2185b2018f9192cd98f70debf692e56ad161c160fecc76fd4668459464898800acbb4e99c2f96219380189b059bdfe0001a1470107fd05b300804b851a62adaa0c0c003027706666d9e860253463d81971eb22089bb534f9b526000000000000000300000000000f423ffe8a4ea0583ce6bd25f2459fd886caaf8bb548ca73f6131bced1e987ca1c018700061b1000027ebd8fbd32ba770c9645f58014cc8e4655fbd0dc6e010c972a52da0557434004754648e2a3cbb18451d3e2e5673078ca399d5f12eb08e32ffe30d9ebb532e0737380e1fd7182daa76cc97ecfe69413592e4e0dd89879bede59627fad297aa4b3a04b56026bb88fcb313289499837041f9cee053316fc093274b97797b55a944068f06e361300edc8f82c07fde4f4994e46798591900b0e054bc6e34ad3a87fd8d095d713fb3acd9c5a3fa39209368e6e929619da3b095aea8a51a4389508f29626a3fb7dedfd6341c7c82fb1000c0bba364c61a73c13b1a830b931af119c36ad71ae58fa5279050ffce5c7ee93a6913d961ee5473be0e63df6074923b2b64c292a2eab005d5b7ea194e8208de33d6ac6281235aebf99ee3d50a007aca83cc14ce98d8e8bcd7f2cc351e82e16c3eae77ff0dc2a852781892940c58b5e2e184fcd01a59d113509d6687ae412257d6a217416c00b13f8c909dc35aaa635a238126acdae9761ecd47edfa38d809a403f67487920f5771f253805ca5bdc372942c995f9ac6752f69414e47bbd4fba34b2039bdc617ebeb971baaf2a7715ecd4157c1fa04c77651cab9521cd42741e46949e1459d643e3a73030c3b5ceab540a8437522569392a4a7b8157722c99daa63845c28e27cfaf381c297bc675e20d3efdaa45f59bd865b517839210cd92c76bc4630481854a7d8ceb2519f82f0471989e3fcaa092b89dbcb7d64cdbae0794f307f2393ef3bd744648d3d16609802d304d1f93c20d028d2a531192a96cdc562cbca9e46b3e1d68bf18594ff34f2f84c01514a6d530fcd05cbbaf1126bc12ef4a75fc8676e440ff4ea41bec0ba16f8377692b758fbdbe56b273a514ec85244b3fedb8021f8645e2744c467781ddedcb2b0a56b589938fc411bcc8a115bfb382d53634b2a26325b299bf1ddce1729fe33be4589fcb74c013c6e05affc63d95f0da9f8a138c0f2236f789de16c20a284ef2bc265a358412b95fb6a105c8500bd85a59cf5789fc8fa775232d98276a17022bebbf631c258b6846d35cf112bcab37c4690e6abc9f24dd918653584056beccec1efe2aeab9664b7efff28aab69b740ae6c4f380a9c71b93556f7317715785b1d90fa0d26f6013ee3fe5a51c32249b2d841dd4bee5a4a4bf10ee899f923acb3ea3537d1396c1bd4598767973743c3454b3bb795a39f83e2d44966a607037f1a73ac1fef6f8e5a8441bc692b8c762b52d2bbe953c374a2cad1db87f2116f9a23ed4bdcc16588bf03ff1c051b4586821b0c3ffbfe84bab3feae31f3999a544e64366a0972c0ba3598e9116b1a70746e2fa1a94d57cec0a5eb14b15e55782d649200f0ddcb862330571e0ca3c59feb81d1d174e2267af235b8490061095e96be48343615c7b4219136dbdc5617c1fbc6f6066453f864a2c6fe9c0701781d9e51c627f1b527e6ff256773c02484ebcfd61c95828203b56ac07819824f4ffe32ffe961fa739036e02b534ca39587194431e3ac98c52bf9d64a0458528f23b2eb40ff0c0097f7efbcb0d608e0a8589ad5fa2c4a4263a803f507503f3b3c55422abcd8116fa04b9e024317958f6863711f7f6d2698ce77fe77eaf4a66cb9e1ae1eb1ffd5ed1d0930fff86b03ec4664ef03dec919778dc93e53d4a722bde1a00d1adabab97116a9c9665187976968ad0f26c45b6c6406eae2ac00cc94fffd198c1f44dacb164d2167ac1dae701aa20edba4ec004b62714eaf858dec1fe50b723b7cadfab51f08b49d0fc47636a207590f2b729b812fa357f3231a97774091bb981235c32c2f10e52e6f886b0473ed590f9977e4d28dd74599419835a398a5556133bc1cfb9fe537355bcc35bb3698c44b4b62d19cde005b3ad74006f489b7419edba57764791019ee303acc27b6a6171c1bc4d296576186c47e2249fe0001a147010700000003fd05b300804b851a62adaa0c0c003027706666d9e860253463d81971eb22089bb534f9b526000000000000000000000000000f423f2034580a9321047b4059618250f0b657d77ff81588b27c231271e8a76dc86ee300061b100002c924fb4342efb18b87c288548d5f7aace82ea3e8e62eb6a6c29a6c8c451723668baa0dc7eb2767e1cd77c59e091e954e3fec2f41d48ba8d4a7483dee5651509f7a17e1d66034ea272e4c665c5c6e2d2631edd213d983f2295d74d30755b6035284bb4e7dd87cebd74ac7e3d019d6ef5a832f98547a168a168307c23ddd5f1ef412c24d710fb249056a4c171410ba0182f9b056b03fdd8d7ca5b2d430831af6b3ffcb4926373566f1c4f749ff53ecbeca24143ef6754fa45eeb1483af8dc4c447fabdce44f1a65bb6c99afdb44ac1c63891cecc0893aadb4df9ad70eb576b91f6591b78f19b5c8d6a8273c98c6084051c0b512684108bb2bbe361507e5f9607bfd15844cd403a9c4f63830ef5d06bd1374fcd490b54c6a3c202f2fd88971413b8baa3b440f6151e0a46c6d4fa583ad90483ab5831ac95f1b5c21176c25353f9e873cadd2e59f3d6f99430f65db2c9599a3a74afe6a64dbe1f56dd9dbba358ced0056706ca21a8ec6d59d6ead3db03a8508531279160adcb36fef941229ff6c4a7ed8ace26eaf22eb35e30e69c479daceca7aedd462ce1bb61060de7158a6d2a3c14052009f6de408f73f63d69cacec595d195833a1885d0d56c9ba74ea735371869bc62d1c79b2835f8b7783f27943164fb6b38e3a077dbf9d3d41a45721214ed14658c78bee759e1625e173b8d8f2b166a89f4efb4e52c96c9eb4d7f2cc4eafbc20bc13de49dfcfdf90a266d083663da7a8a4c414ff2addd48114830b2fc77a74aed25d9c2d57c86afea4087493249ab57b38f5e9cfacd623861bd658871ccb33c25e4538dfdb46df3f78ac4c5c2525620bc3ebaf520f7f805c84213662b44021ab0d32c9a004b985cc685d93784e0cf91876989469a5b8960a3b8d097ae6dc3107b49b82dcccdf7c3b7ad20fc0f56d4c1b9882241b7cf70c36f70a16660e244b2765227f3f0ed48e2e495e2b211a5bb2f5a2384a06a2a6c06fd0a42a82127971b927bc0256730e82c6f36fae2b15daed25501ac7c3b963596ed83d17e9b0bc42fc5409d8f525088d522ad627a415adf34ad000cc20cf8e62e19d31ec4d389415fa0574a01123555579c964cc0a0c952419fd92f393a7ec0b8feb43f7fe4b8f9acd6f943470c1ca421a80aa28ac8138ad45fca84b57aac0fdedfb583d76e830fddf85eb0705e44ddf044eeb7a814f1d912fcf14c4dc104e2ed3eb661151071fcdd8cd2e51ec60838cf9a3aed9ac10b6e11707d808ff2bc817c255051b8eb79c90c413d514a9256f24c355128e17b866fe65d5e36f082c0714327e8e7914b1775dcb0dc76efc5d5d3a02083a3fc4cd5ee1988d26319868e494fd4eae0099300bf7932a348b4beb680a93d50937c9f4967ab901d297938b241b9d9b5b992f6603801b80eb30a5f172716cd9f1a9a5e580f826d6f639356c6aa7086bf66a5612aabcdc8980073c6f331056673efd49df8bf1cf9d36f71de931c773ddd3ce89ceae8c71017ac7cd21a5a6d38f328f77802ecc84d49fe6f144049ec6a63a4d59b99ad51250d2fa4357002436d42e69b82e10b1b0eac3ac2e7c38a46ab5a09ece95948bf180fab8284c594ff6c264f7e4ca9a5ea0b015e9e58f091b7adce4ca34a132613612463842aad112ff218bab88007d97e245c759c62947bf0ab37efecbe6fbd0ddc3a2b7bf2c9d8f7cdfc01504094e811e20831934db121fb15c9533d40c3d4b9e63911241f3b74d1657057285303d11173170fe245c4b612247e8e477328dd09f26736cdda4148749f449a9712c5dc6ceef950868110d621e8bc1c84ce28d21375fe0d62363a0ed2089d32db666435dd343f3e673d44d8092c46c9c0e3f2ab2100bde4c7b96bbe527229ce83dad1660083c4d4fc171d9f300e254a9ffa2b3a23eae54cf8bc03f7ab239142469b11179cefd72dfe0001a1470107fd05b300804b851a62adaa0c0c003027706666d9e860253463d81971eb22089bb534f9b52600000000000000010000000002faf080d341f3e367059875c56147a7148e6be30e96bb9c7c74151758feb7d5757ec01a00061b1000020c0c35bb44e606d10d250c64c47be0d010c6dd2246b6179460f79236f92de91f6c9690bf306f23a63ee91713b5842efed429b8da5d31109b574e796624ac50a1cb964d734a5b4bed9d159fe4d68005a044ad0bc5fe3e5fc50e6fa5c6f57ea38f49d9a8ba2907b53fb7b986be9742dfb083c742fbde63a2c1f9f3651569a715ca76a4061f543e3d58cbf54307e29d75b3192ae4934e53d078c86637bd9d62db281586171acbdea15f3dbbb0871b07523dce005ef277c8ae26dfedf442245ef9579cfcca53532d948ccb90956d21b293ac0f2e3ba89fdf05feb2fd11880e366ba5104cde531d0074157c67617996d5f409d46ab2f4d566ed59f2a47c4d4e0420cf5b8d2a6feeb5d502102f8e5e1c53083eec0f7161ea782793433eefe9c444e8c2b940fd9b2bd3f343964d0f4e4a785b0205584da57b844fa92f05db2a4bf0cdd814464214fac5e0ff8cc1c14001dbf85c50fc9b818c233b4f59c5f9e5bfa23275d81e4449dc68f70fb47c343472552b83f725d3ff65a76384b1c4c14e6126afa74c256d7a0a9d53af72ce7da8c6cc2b4a7987cf78211d27e487e0d78f040653e89e326636733bf39a13661ae307b43e15c0a5aabaea428b1d1081630a17a94ca53ef27ae06d80756b7ba5d5720a8e947e684fc7d9bb90ba8114e106c9b86b4a138dfacdde4513e4c038d7a0477ef6ad0b503e0afbfe8308706f84f9557e7097b2550bd36808e83b87926a09f917e57bfdff5819215e322844832795a59ea0d3a4d5a277708f39f8965e33fba3f042e56a089ffadd6be06a9cbc943100672b6072ab0aa04d51f6cd0a48ba929ecfb30494e3b9b3fe8dae70874f6f16b997959afe850da93a9ee268e276863666ea576dfbd3607390a566c1dd14f7359e39efd07eac54aa2b51ae75bb1b73e9268065d2c45a9c354f38943e4975dc45d0212ad2a35906ebedd905d8d608f9575eece992508b441375b05c41b7346a4e3d99a2d93854e628ecc8a1239980fd8c841071642402d2eb1abf88cad195220d41ae26be0a9c21195472d42cd5fcbf8be23ddb4efee26d1c6f4c7206bee26959dee969daee351d7c62b33dc8d49d1b42dfb379225179f997dc93be8b1b264b788d277178b385d72de24c5e24db781de4ca625b5847ddd1305f647aa3271bf45bb397fda817f223b0b635e8c071a0e9067fb8c0c6ade6f3f41355b8e6d80475254606239cecb8819e7ec54979c868667bc0c867e2cba7c2586a2e5a930cb11edb66b6958b41f3e73c35f51486ca03b1f77214bee7ccad7d2de529ef10ab87457cc8711ed3844cf9cf4e1368ccf3ee36cabc82df5fe86000fc62f8e3e0cd6d80c04d11f6b7f60d0b020f3c78be0cbab6e9aaddb0170cf2c1ecec2975e0b9df69f57f56a300e35638a9759d8063b0d21b1fb30dbc28d0515bebc27e57ff5ee10990ce38ada46f29e15b1f4c41bf144b4ddb99ebf5e6ae7fccad1d177bcdbff4e585dd41bb5ebd4ad8d9bd05e14e67b6a2780d5197986df284b3a361f0090b4cc55dd6998ade2afb7ce5ed1ee119a28548ed816503f81646f439964a0df66242117bafceabc89a45b20ae305689db80c877b7093feb136188a40715fd69cac4965bf58cd1966ffe9d73a05dd69868cb9201b11d48925e738000b94565ef6ffc451fd8652a1194bf04a210065476a3a3a5bbb048414dd89fd2d3e6f8fdb964b91103f86c289232db9d5d37cd95cc0709cf0237a72a43b7fdb5f8daa574985d74f3048c6bff9874187d739f95db8f3d315a7e767a0e148cb6610569a3552e877abaf71b016a8fe7c02885f70799ec7ff5b9922f6021d8ca931dfa24ce79e50965b3312279a7f6ecf63cd655b62526a6267753ecf5c2c99c32f00779bad3279099f3968fee2b1cc21d383e3771116b9e334ef766049b43d782a7fbe51fe0001a1470107fd05b300804b851a62adaa0c0c003027706666d9e860253463d81971eb22089bb534f9b5260000000000000002000000000010c8e05f56b89fecac2a2630fcd0c7f1a6befe5280b53767a909be33868385e1bf51c900061b100003b2a1d1a2a49ad936a74cb4a9a711f743b5a050454d3b13f70b21cb2a182e5cc3a57e300b854f537d24f86c2d475504fa5d7c127b1f64ec93d72fd8bc1f7315ce94ab7677437051fb5c41314ea10f3c89ff5a355074e9b8731b9fdb2f713f1e5d3a8f6030022a791e5fe7c2d33b2ed24c43604b8d15859b86338beb8d9bedf98f40c9e0faf128e9f4e8534c731a1099088c0af7ff4e76e558b0bcd3f3dc4d30043b340518c132efa90fb5eab3daf91418575a910ad790c1ebdcaf1c781657cf62f62c3fbba3e1896c89eadf2caec95f9db22ba2a99f6ef8d4c8dc66d8333193e39736432ebf5b9052b4af8cc2541d0abd83c6b0a4816a7784620e95ff07b9bc6eb268e5500562dcb6d410d6946d4df8c2511142ab2ca96d0c2b8ab37c1f9047b638a1fc43b10b39436d256976cbe042548d64ebea357358a2ffd13a37d3c7dec9242e30d99255889d261e4cd3cfefe59ce2e1141b68161d9b75af6bf4c1419e14ccfb27e74be29d03ef10d74d014e4cc78a5c831234b0059f60e41a897a11d247a386d0f72dd7b19868dee5f47030145972ff683de626cea4bfb184cbbc77f7f8226e5f0b16f576a065a830dbf76f31a10ab8ce9332fa1f26432a7d13d8885ecf34239aca7198ba88c8d84bcf9c966a8649d58da5c69a3139bbc475aef243e5c7ce0825fdf34a81c8566c2ee313f2e6e19ebb818ddf182684dd49ffc2f97bd29e5ef90787458dae49ca1c53bfd34f9a0ff7bd69858511239224ede8d4ca6979f14fae06a2d07ee9fe27a15474b4a83778293e122a140c9718b3974d9f77d99062f9499267b5006108d7040f3c285892c8eedd7226b9fda611edb1ae5c13306e284e6f0a41e39b30f61535b93a41e80aa114092df635409842e69da6f01de516f5b603e8066eb9acdd391d299c0648093703fb33a8ab76d6b08f691e44783e44bffc42278fff06b2e115532e9a26e26c675f2c1694222c3a9b290fb91c0c1ac85ecb253525b7bff3fc793af3a78deb6023e9ca228253f6bf40be04135f29c3533a7b8916558959251e77a918bf02ff1e73e72ba41788f524df1681afcbb598927d69f587812b2f5870d9d75210219f751d5a6039debc7a750a6be046415b8e2d065f56d46de0ef1007f24480bf8915daaa6bc807bc8231aa797d3c8698ef4d87feb0c0ba9ce4d1b5f9de1618b93df02a2f5d13eededc4e32a0dbf6004888dd6dc4908e37a77d722d2c9b3515b204634c7849bc623e9c16f7549b80c54205861021bedf964f6914cb39bc63debca84d7c7b1272d3d0d0d3ec691791a7a6ee5592fdf2605e8e02c1e83d0abebdf15dfe1c682ff82cc5136cdbbc60f3e680bb0913013886bcfb688ebab2837e9223cb5890d75d7b6a32506fc0a891c5c06d12858277861459c75b45794fcc4af5a8753cc9a5de60e703c455e4966528b9514e4545e124f1feec5583b61c90c03ba7d0a002601171ddc29a10922fdea5589d554ab690243829e80c94180af921605cc122d1cd9d8f561fa5bd6cc4479cbc8bb6c5f2d04380fde6ef06267b8555dc5b7447325a775be9cd04fc604672eec88abb16503c23b47d990a40aa91d3be0d451857b9ad6892a21c8d7d7ef228509626b4e2be32c36be8faddf3b84ad8d29120d52109a2ac2bfecc1949ff176ef5218f13748df023015c304dac20b7729ffb4c6d1bfa5cb9dfd433676b98627fcf6d6475026653f6954e5c127af229328d87aae4cd34450a8ce3912e3938a2bc877e1a7ee7e51fb05d15a5a96c235755a72c136198d0588d7fa96f7ef5703e6877ca9ac9ba5b3781d9a46428617e44eb29ecceaf90a6656db572d638ff28728203bb275c51d29dc876b241e4178c02920142ed4002caef62c6b60e710e9ee11b2463fe48ee38f0f144dc2f750e5ed7598e722e5894008361093522fe0001a14701070000000000000000000000040000000000000003000402fd05b14b851a62adaa0c0c003027706666d9e860253463d81971eb22089bb534f9b52600000000000000000000000002faf0804206184c2a00aa7b17f33936aaff2b237528a7f1034df562057f9c904b3f34ac00061b1000030db149d7518d85d195295b7f9d64ccf5a393f27ecb2779e800ac4b3a7c5fe1811aea7beaecaca26a4a94eb5b8a26a02f6057c911873f8be25c0de1d7dffa7dc4fc5c3b5c58b4c6611a5c9e10da0764c106e8697763662e55a663224a3a5ed51f3b3ce907b26cac6c00bd7d60b136f1613955221af43ab6634a6a0e3de840d3cb9e776fa9b4e19b1fb264947f3edca39372f31a0cf722af23b5421cf6d9c493de30d6dc57e199798243d23407c0d96c2d7a406203284e8ce5113fc23c7e0b6e4d97882adcaafe15e02a5eb610ff56e5d880680c0afcb00c967e36d4e218c50c386a206f87bd3f68ae29eeeb470695c21e751ceff7f7d66cc64b6d8963e97290f7cdcdc283f6fba3d7057ecbf879d26a211cab7a7787e88ba3031911777c6b43e6433e25b458c262b66249f97e379ef7f722d7541b3ab4a0b762f7ba3dc45bf61bc00003d0246dcf32b77574785549619524425f113fc3e8090f9393401349c752d1fa9ddc399321a0055ae84131a8b8228e381349079ee9adf6976de1dd0cd6e8fd0e248abe382ccb5e465f5c6274dd9d4bbd8c07a9a3f40a3deb21c628831a084dbd5cbc0f923fa753ad51bafa80d800622f59a197062e83897344df59f7a114d46cc6bdc98e77f8e3d1b9c01a5700ac520bf97511763fbcc6f797811f71fe1022422f3b725a1f54da9a056f26ac4bfef17ab0565c37c1183cda6f600eabeafbafcb0183658f6882f02d1a9721fae69d3489969ce30b95d89fd2a5a32c9a8ee5868fe8ac0dcc1713d0bb8f41d8f5aac102d26ffd16993fdcf4239925f9a03d1dea77feb463bdde80cf17c260762f40e65370ea220178b38ab8770a6554d619d5dc4ef9fd21fae392b4e37d03a8cf9cfd556ba449b7f623700a5da2ec30a6f15a54e3fde0705521b21fbedecefc7c7233396f0e18182a6f7840027013a104136b4e46879d3aa73b8aa0d4202da1f5cc448a1b4b1f3ce55168e35e1c094c1db76fa3f93620e2ed2d431bb5f64de1843cfa5c579a54fe74c8376c138bc2b53ac84bd16bc09639db07fef50959970cede04cfae9548eaf8fbc2dd9d745aa4eaabe7278a7b37c77025313ab74b5e7cd892a081d46b47faaf40bd6709882c66c0b28525ee0b38094b4ed3684e022a1cd9aa078b75586aa176a8218b6a019d4909372f5e2ac837d40e779e2eae51f30965581250ea557929b2a6598ebd18e3ed796e59841c5d684cc2134bd40a87d0772c8f8a6f8eb6771294567d213a95cd42db22ce7f10b9efb2c1e58b0759f50d522406f8699721e5fcd6531754593ebd90fa18d9db4fef3a76d5e8c8b129347228ac27ad8fca774c20a2fb300ef4a3778540d2901f4a53dd0d0b1976f594a029b259572ee4ccf7aea11b48ba27b67f58ec1ff9c12c2ff0bb02322c1eba9147fe124165d96b1088068d835f8195ff45ef379b4cc48f14e887d779de7e723e50833bb44b340664c5907d52cccac676df96299645d0d3eb1443fac71abdaeb95a0c440584105aa8c88b5aa57b897125008039d42e094a47b9851c4af2dea480234423e9e8ed86eecc23ad459d051a1244c204e2335d2b613b85629a79efa02ce782776e99c35751d5fdf6dafc30bf994e8077e6b471200a511e0f0aeb5cee775215e8c385bc5122b6fc7ba717162a80a22b73ed4253cd7e6b2cf8cd504f84a939c4b8a1848ad4e93c4bac898a83d0549070b700236377acbc270efb0900cb732fbfa0ed39a7b0037195ba056e9a732a3388179b63166f03327cb307defa3e98a0eedec78778a48a32a4758e4cf9bafe8fa746746d9d4dc75805fe4edcdd37fcd35f9ea7ec8075178ef0fed8166d69d109046ca48c7ad2725a4361b79cf0052717527065a43831fd0b94d5ee09d8f0b0ae937a4307b4c7dfeb299a15d09a15fd3f0998867ddf1f6d042fafe0001a147010702fd05b14b851a62adaa0c0c003027706666d9e860253463d81971eb22089bb534f9b5260000000000000001000000000010c8e0b8335cabd041e3f8024bd0b5b10e401868d31ff594b98f2bb79dc28693c7788f00061b10000230caae9f655304aa6eaf036c4d1839012b5b63cd8041bb0c70d1f9dead16a16c67ad62b6d988a208ff75b4b2b17d5594e071360dd7118259f8b855e1af695394edcc0cd67d28c94f63bc686e214bb3b6537d9fbf6afc14e1a68e8ab3d202feedceab7c01a02e15426672f8b0434b5f319c8230df38096a28c4c05cc4cde86ba3c0a15d5892b7ec10711d4983122bd525e9376b373c9e2055b95b4980b180be8dee61a81180668678b610c81cce8df7a07a8e60171b826a823afd77125876a40c5bf7a2cfca6f11a69f1bc6d2770af53daa28b6ea58c60275f53b3985289c2cf8793dab4759c3bfc0cbec4ef15bd8710984ef66117b4852398e5dfef28ae650279d3d5f8b7e8d55506d4dfba3cc3123b66965483a9bb0eb9e6f114cb2ee6f69dca6c53d917e8f0c27f704bed1677338b15a6b8625434c77f284c8711b4983b19134692ce7d3f85559ead3aeaa09e99f3c2a74b157a4768f27856600734b0983b233a222c9c20c2e0ebe3108d9069e62742d270c02a198d5d037488cfccc14dc56b0ca1ddac50e133a217093d598b1a7bbeb6190ee9a85ef4ccc7432dee503aa481f2cb11adf434eb514f8f28ad02a1680aeeb9a743cb350a88402765e1ade1cc0251da42841f1f7de85915c63a42d13942c9d2b1d4d2762f705383f099d6f1f01615fc721266ef45fa20a859e06aea7d44f154be97e4f906096eead6f2c4bff6027d82228265aeba458c6361682ee9cbfb2fac1a179de45b3598444002537393f058c3cff0272c06e18694e280d275616723b7019a540b6c75a6c182696fe5dd89a16a54783d3b533df13c703d081b7b33da3158a0da3cb628393b866a181145337797271e4437d49b0370d50b18d5fc031f35e9c5ddc872f3c47b08c1a9e5c465a59cab419da0da0a2340075b8f71df38c022d97cb97a231898a53fd4d5c20fb2877aa8dba9fdef88936ab36eaccfd31ca674958d2677acfc9c99067000b9a8688723ee3ecea593dc12454bbfd10b73c68abd4c8d099eb55e41ebd8d6a390ca536a7a5fb5370f1a1e3e31fbbe7b256db29b403a94179ad1698d1a7e7503e3744d11bbd349f01d790431b747533272d09217aed55101cded1eabfa705b3c0d6689441f0a03d33fd39bf5ee3dc8896276355906e87c2faf5b9cd74823285f2d58d70db120ab53da6af532900387c107b2803becc793ef0c31c09a5b2c2612652b38910c19238b198c2a11e2f7a2bd51cff9ab01d6d0af3b9c4581b1d7246cbd1f1a10a5b81c0530dcbc8098c2ce283ceca438c0cf4abe734ca8faa0ae8b4849509f445b212e7e1af229db978e67d1c531cd2d9d3c8489c46b8277ddaf260cb727c4a34bda98753007ef1b90e2f83aaa7c5139c3f1c3192acb0ded1cc41170827ccb5fde144bbbb68f139883bfa611af6446a46486fa7c3a5fa9785d87fa229a8503c1e74069dc994f3c284561abfb245c2e7bdbb88d7a8f85ced0df0fb2dd7677878049d0ebd53cbe6abc26b3f34d9e9bb0a1c02812359fc3618060431e1696dc5ca6c157deefc2fbc85a60f6b4ca43c5edb822d55a57591e5632f42f860359f30e3b84f7151d0a2651b2dae1c1a408b2fc2af9f667acd6d2e19ef4f654b48de61b60d13dd011547ec7200cd2a74f26e96e2512e962e073b703300868b6c86a46718c543f896fbaa368e65746bf5b1a4da04b59cd446e38f5c5b244fbfc879b49517c95522547e9a756074c171a43e20d5439803dca0bb0f87589bcb6171992565e1e7f9324b0fd0e025f36dcdada2d14237c1e79ec12497c420c99275f637cdd5c3910f62c8e42ccfa627893b46ae91e13d3b54d1d98408894247c803233fba1d8493aa10e2560b0d2fb4ae5a9881259fdce09e8b5cf7762cbc8ece0a15ba576d4b1d1910b376f2d19f524671275e6e4be33b4d8dfe0001a147010702fd05b14b851a62adaa0c0c003027706666d9e860253463d81971eb22089bb534f9b5260000000000000002000000000098968053358f633c280126a51c2c7430662b669f24f035b3995d5bb0d57b88cb06799300061b10000279e6e6dcba2ada19aa78346b56df3df3bfc1820ccc9d3138635f4dde381f968e2e0070f136ec8b10359b398b151c464035e95ffac2d4f5ffb9e3bb4cefb8c472ab51a75645be45b2067b5f6abe28ed5b8c96ca5cc5e69a5b6e7c1846344778294285f46f70db900fc519ddf416d940fb10c6f0ad85c9ea803569affb9bf9de13dcefd8111d25b9f62e54b3f536901c84f8ac0ce1a51f514cc917aa32e2a14c0c48253bee78c4086320c73ef3053717e42b3d79426e898ba6b3fee3065eec0750f1f6c1edab4e96baec2306c49a63692ca7c318f647f2a1500cb5b0292752bae3f10da3ee6b35d049e76897515b42ef2b4445da80f5931cb9131d525106d21ba30dadfefb65b25333cb213cd856c91f28f42b88879904201a452bf996b131eef06be945abe1f29d20ff4de145757dca2b51891badd1b0ec756f28aaa4b6f1ca24bd9af228ab76269469dc4798d29fccea61825ccab164f6353f24b198ca98106c639c23fb1b88c45a51fb710316449436641887d077b23b81b8f3bc663d98106ad214da1f51235e4c7119c1074f39e7bfca123c9a665b39a9de36ee84cf3fbf22c3a934768395eb02f3e5b74c642048d799363daf6e9a663433f8ca01d08ce08e711fac40de70b920140bfa762a4b5740062d8e79d20a1e0003bd12412f51995263d4c65faf4912ded001fc7169c5f84e585435f60595550b57c895795a3bb2b3ab7f3a9eda479f96c232b51774c775ce14818fed5e404c3ecbf9ed767ae4e0722e13b703fee6ffd2a3f78eec0cbe80f84297fde055aaed54d4199c66ceef3adbe819876a48ffebe337cec4f222912f4159ba51ec4ab239ba349d40dd7a8d7d15654bc91605d7ff7acecd3cb5781996dd896f70200a6236d3175f0f34fa4c84229a2f76297e45ad925aab2808b898c0712f9c7bebf886308e1879b15deb24efccdf2d97797321a4ce3d46688375376b540c1db049384157da71e3ca534253f4f8e3007996ad80c416b5df9b06fe4f73d3dbac45a95d532792d0ebba2649afd721241eb2ce07a291bffdb77cf9d083d239fce425e9c919fa6d3586368db2abc4eea2dd03bec12e60d8d19f25d873505f04064c2bf591e98b44fb1b3f0b1d1971b335a5d7cf10045bd1008aa40d930f675e3d1ae3b3342de4ec87b3c7cf5ef883bb30e3dd0d63283c22530e4ad469a1e95d9813d6a26f0d518de096eaf39a01862aad09fd99b195b6d8bfae4c096fcf57de19efc032c859bd9800fc1064c36cf43dd1698e86a657e12b36c7cf01d7a24d85a7e1a6196171ac9e62ceca81627180ad76196873d2eb15b2717eb2dcd4f28b87169139ad6a17405e5e095f949781b0ecd5a339683d0f01e6af2125b20098f2678b0be00ae4dcbaabc4e36943e92372a4a1a2600e9a7f113fe0a3669839709986f9366b254a5b67849a5d7af091b016579de6f4a30a49c4723fa470a55fbce52387f1b929ed6933008003d962351fe1f1a5ad1c66fbd7ab81ee06a7e1b884b5b1816b89575da49618f56de6dc7566f263b5e7749463ea40aa3c7dc417bc5bf616b6f1a8f80e83cf95caa9e060214e2ca6bb8bddef8011a18134616fa7c2c1c957737a556f204a3d9945ef6275b532c8064bb711220291a4c628d34982bec5185f437e12ef168d76213a3654feef53b22e976b1af10998218871673e6b69fd7d84834dd4926a1f4b7b3f9189b2e6cfe061eb44e1caf7e95efddb26d26f2ccf736b64fdede50825e9df37ead839d2d6b8b6079092c2bc63724952ab0779ac6e05dc41fb014a4fe1a69ed8ae4e5879462f00dcddccdd5d2a11b15ec953fd80408d8a5a60824c2f9e500591e14cff01887b8c8aaed69ff3df2185b2018f9192cd98f70debf692e56ad161c160fecc76fd4668459464898800acbb4e99c2f96219380189b059bdfe0001a147010702fd05b14b851a62adaa0c0c003027706666d9e860253463d81971eb22089bb534f9b526000000000000000300000000000f423ffe8a4ea0583ce6bd25f2459fd886caaf8bb548ca73f6131bced1e987ca1c018700061b1000027ebd8fbd32ba770c9645f58014cc8e4655fbd0dc6e010c972a52da0557434004754648e2a3cbb18451d3e2e5673078ca399d5f12eb08e32ffe30d9ebb532e0737380e1fd7182daa76cc97ecfe69413592e4e0dd89879bede59627fad297aa4b3a04b56026bb88fcb313289499837041f9cee053316fc093274b97797b55a944068f06e361300edc8f82c07fde4f4994e46798591900b0e054bc6e34ad3a87fd8d095d713fb3acd9c5a3fa39209368e6e929619da3b095aea8a51a4389508f29626a3fb7dedfd6341c7c82fb1000c0bba364c61a73c13b1a830b931af119c36ad71ae58fa5279050ffce5c7ee93a6913d961ee5473be0e63df6074923b2b64c292a2eab005d5b7ea194e8208de33d6ac6281235aebf99ee3d50a007aca83cc14ce98d8e8bcd7f2cc351e82e16c3eae77ff0dc2a852781892940c58b5e2e184fcd01a59d113509d6687ae412257d6a217416c00b13f8c909dc35aaa635a238126acdae9761ecd47edfa38d809a403f67487920f5771f253805ca5bdc372942c995f9ac6752f69414e47bbd4fba34b2039bdc617ebeb971baaf2a7715ecd4157c1fa04c77651cab9521cd42741e46949e1459d643e3a73030c3b5ceab540a8437522569392a4a7b8157722c99daa63845c28e27cfaf381c297bc675e20d3efdaa45f59bd865b517839210cd92c76bc4630481854a7d8ceb2519f82f0471989e3fcaa092b89dbcb7d64cdbae0794f307f2393ef3bd744648d3d16609802d304d1f93c20d028d2a531192a96cdc562cbca9e46b3e1d68bf18594ff34f2f84c01514a6d530fcd05cbbaf1126bc12ef4a75fc8676e440ff4ea41bec0ba16f8377692b758fbdbe56b273a514ec85244b3fedb8021f8645e2744c467781ddedcb2b0a56b589938fc411bcc8a115bfb382d53634b2a26325b299bf1ddce1729fe33be4589fcb74c013c6e05affc63d95f0da9f8a138c0f2236f789de16c20a284ef2bc265a358412b95fb6a105c8500bd85a59cf5789fc8fa775232d98276a17022bebbf631c258b6846d35cf112bcab37c4690e6abc9f24dd918653584056beccec1efe2aeab9664b7efff28aab69b740ae6c4f380a9c71b93556f7317715785b1d90fa0d26f6013ee3fe5a51c32249b2d841dd4bee5a4a4bf10ee899f923acb3ea3537d1396c1bd4598767973743c3454b3bb795a39f83e2d44966a607037f1a73ac1fef6f8e5a8441bc692b8c762b52d2bbe953c374a2cad1db87f2116f9a23ed4bdcc16588bf03ff1c051b4586821b0c3ffbfe84bab3feae31f3999a544e64366a0972c0ba3598e9116b1a70746e2fa1a94d57cec0a5eb14b15e55782d649200f0ddcb862330571e0ca3c59feb81d1d174e2267af235b8490061095e96be48343615c7b4219136dbdc5617c1fbc6f6066453f864a2c6fe9c0701781d9e51c627f1b527e6ff256773c02484ebcfd61c95828203b56ac07819824f4ffe32ffe961fa739036e02b534ca39587194431e3ac98c52bf9d64a0458528f23b2eb40ff0c0097f7efbcb0d608e0a8589ad5fa2c4a4263a803f507503f3b3c55422abcd8116fa04b9e024317958f6863711f7f6d2698ce77fe77eaf4a66cb9e1ae1eb1ffd5ed1d0930fff86b03ec4664ef03dec919778dc93e53d4a722bde1a00d1adabab97116a9c9665187976968ad0f26c45b6c6406eae2ac00cc94fffd198c1f44dacb164d2167ac1dae701aa20edba4ec004b62714eaf858dec1fe50b723b7cadfab51f08b49d0fc47636a207590f2b729b812fa357f3231a97774091bb981235c32c2f10e52e6f886b0473ed590f9977e4d28dd74599419835a398a5556133bc1cfb9fe537355bcc35bb3698c44b4b62d19cde005b3ad74006f489b7419edba57764791019ee303acc27b6a6171c1bc4d296576186c47e2249fe0001a14701070001000000000000000000000000244b851a62adaa0c0c003027706666d9e860253463d81971eb22089bb534f9b5260000000000000000000f42400369d0304f60362ca0545e7b01d866440add0601f345cca08e2ba0bf8d589e47540400012401010101010101010101010101010101010101010101010101010101010101012a0000002b40420f0000000000220020f6e5fd71924ec575d84188309b7ab7549900b14ca35f24f7528b976f4718660d061a8000002a000000000202000000000000044c0000000000000000000000001dcd6500006402d000000000000000000000000009c4000000002faf0800000000000bebc200c6ea335bd980cbb54eaa76aac45078a042997878fe848c29b829b083d075b83a012a8df13046f5a8d8ad87d5d681f38d512768420d8ec23704e7372f2f0490e8c60a88920d8c98973fe078b8b3929e084cc195f8ad956b92a5ec3c1b8ae7c59c47000000000000000003e800000000000003e8000000003b9aca00001e009000000000000000000000000009c4000000000bebc200000000002faf08004042c9a5ea1d9ac2ba846ab65a6a1c9ed7464f68d8cdd57ff4d001f8589bd068038a55e743334350ad9f60528552fd0209ab44a31927c477d693933937c70dc016fffd01224b851a62adaa0c0c003027706666d9e860253463d81971eb22089bb534f9b5261a25d281260487de0db9b1086bde8e26da3dbbe7cb7ffa3c8d86a1d7251c4d124d8d3106e9fac038e209da1f952590556a11bd56845d980269cd711d0815e8330003370d43f6d7b92c5d8bb4368e81acc3060e15ce3d141e4859ba1cf7b7bd13bfee5355bd7556250c52094f8b7183f6bfe3aacfa777d5ffc1435930ad3b17b17dc4b3a2560f4d087bb1eaf1424df790b7163f11921e5a0569add51f4f7593ce8d8a7689c0c12abcf9b6623f9b129f177e419a7b06f2b9b73732149bf9359033547f45218581956c38f365cfc8df021e99d631162fba7dc3e5eb9e3e9e6136d8e63f5bd7a845ccdff022e896f31958c0fb70594d6dc34b2e6b08f2069152fa4f90d600000000000000010004010000000000000000010000000000000001010000000000000002010000000000000003000009c4000000000bebc200000000002bfb75e14cafd62677e479a252daccd9a372b3effe908619c29941a9a703caf5ddfff53c0340990e09f3cad846dd40c129dc14d32653286dfe2ea0ba17bde9f58e56e916200000000000000000000000000000000400000000000000000001b703ae5c0193492aac3fb37c3a58dd2b00000000000000010001fefc2c75cde2413686de5c4624b1384500000000000000020001ba0a2a33526c49fc90a54bfbd7f1197b00000000000000030001734ad7d4b2c243439293c6b499e2729200000103c38c9947786cbcff005d1848c8993027008811517df536c7e4917efe15149285bc6c67bb596a97aa2e7b10173be1977b08d61deab3baf8dc69722229d862d79714338d093742008228b848f1aac069608ef706226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f03c38c9947786cbc68ba91e00300009000000000000003e8000858b800000014000000001dcd650001000000 \ No newline at end of file diff --git a/eclair-core/src/test/resources/nonreg/codecs/050006-DATA_NORMAL/funder/data.json b/eclair-core/src/test/resources/nonreg/codecs/050006-DATA_NORMAL/funder/data.json new file mode 100644 index 0000000000..bca7cfcc95 --- /dev/null +++ b/eclair-core/src/test/resources/nonreg/codecs/050006-DATA_NORMAL/funder/data.json @@ -0,0 +1,333 @@ +{ + "type" : "DATA_NORMAL", + "commitments" : { + "channelParams" : { + "channelId" : "4b851a62adaa0c0c003027706666d9e860253463d81971eb22089bb534f9b526", + "channelConfig" : [ "funding_pubkey_based_channel_keypath" ], + "channelFeatures" : [ ], + "localParams" : { + "nodeId" : "02aaaa00ce2f18a967dc4f25f414e671ba6585f8ef0b8c5fb812c21064f55a2eaa", + "fundingKeyPath" : [ 130163931, 3624689029, 1083077355, 2019401702, 2770945043, 3534154301, 1368635747, 2771643710, 2147483649 ], + "initialRequestedChannelReserve_opt" : 10000, + "isChannelOpener" : true, + "paysCommitTxFees" : true, + "initFeatures" : { + "activated" : { + "option_route_blinding" : "optional", + "option_provide_storage" : "optional", + "splice_prototype" : "optional", + "payment_secret" : "mandatory", + "gossip_queries_ex" : "optional", + "option_anchor_outputs" : "optional", + "option_quiesce" : "optional", + "option_data_loss_protect" : "optional", + "var_onion_optin" : "mandatory", + "option_static_remotekey" : "optional", + "option_support_large_channel" : "optional", + "option_anchors_zero_fee_htlc_tx" : "optional", + "option_channel_type" : "mandatory", + "basic_mpp" : "optional", + "gossip_queries" : "optional" + }, + "unknown" : [ 50001 ] + } + }, + "remoteParams" : { + "nodeId" : "02bbbb671d15145722fb8c28d732cddb249bcc6652ed2b297ff1f77a18371b1e63", + "initialRequestedChannelReserve_opt" : 20000, + "revocationBasepoint" : "03368d06fa8bac9366977e1ac914109191f54ab3f0152476a59bf928304300ba6b", + "paymentBasepoint" : "03b4b6c6e2e0760495c464df937402832c845ec00a32988496747f1606fc46b626", + "delayedPaymentBasepoint" : "0361234d1f650157b084405c89580afe97cbda284a116e09003eb188786a5b02e3", + "htlcBasepoint" : "02ebb64a426889e183e74c45e0c5fd187f6d99d4dc155b4cb4ca063cd0fd5c97d2", + "initFeatures" : { + "activated" : { + "option_route_blinding" : "optional", + "splice_prototype" : "optional", + "payment_secret" : "mandatory", + "gossip_queries_ex" : "optional", + "option_anchor_outputs" : "optional", + "option_quiesce" : "optional", + "option_data_loss_protect" : "optional", + "var_onion_optin" : "mandatory", + "option_static_remotekey" : "optional", + "option_support_large_channel" : "optional", + "option_anchors_zero_fee_htlc_tx" : "optional", + "option_channel_type" : "mandatory", + "basic_mpp" : "optional", + "gossip_queries" : "optional" + }, + "unknown" : [ ] + } + }, + "channelFlags" : { + "nonInitiatorPaysCommitFees" : false, + "announceChannel" : false + } + }, + "changes" : { + "localChanges" : { + "proposed" : [ ], + "signed" : [ { + "channelId" : "4b851a62adaa0c0c003027706666d9e860253463d81971eb22089bb534f9b526", + "id" : 0, + "amountMsat" : 50000000, + "paymentHash" : "4206184c2a00aa7b17f33936aaff2b237528a7f1034df562057f9c904b3f34ac", + "cltvExpiry" : 400144, + "onionRoutingPacket" : { + "version" : 0, + "publicKey" : "030db149d7518d85d195295b7f9d64ccf5a393f27ecb2779e800ac4b3a7c5fe181", + "payload" : "1aea7beaecaca26a4a94eb5b8a26a02f6057c911873f8be25c0de1d7dffa7dc4fc5c3b5c58b4c6611a5c9e10da0764c106e8697763662e55a663224a3a5ed51f3b3ce907b26cac6c00bd7d60b136f1613955221af43ab6634a6a0e3de840d3cb9e776fa9b4e19b1fb264947f3edca39372f31a0cf722af23b5421cf6d9c493de30d6dc57e199798243d23407c0d96c2d7a406203284e8ce5113fc23c7e0b6e4d97882adcaafe15e02a5eb610ff56e5d880680c0afcb00c967e36d4e218c50c386a206f87bd3f68ae29eeeb470695c21e751ceff7f7d66cc64b6d8963e97290f7cdcdc283f6fba3d7057ecbf879d26a211cab7a7787e88ba3031911777c6b43e6433e25b458c262b66249f97e379ef7f722d7541b3ab4a0b762f7ba3dc45bf61bc00003d0246dcf32b77574785549619524425f113fc3e8090f9393401349c752d1fa9ddc399321a0055ae84131a8b8228e381349079ee9adf6976de1dd0cd6e8fd0e248abe382ccb5e465f5c6274dd9d4bbd8c07a9a3f40a3deb21c628831a084dbd5cbc0f923fa753ad51bafa80d800622f59a197062e83897344df59f7a114d46cc6bdc98e77f8e3d1b9c01a5700ac520bf97511763fbcc6f797811f71fe1022422f3b725a1f54da9a056f26ac4bfef17ab0565c37c1183cda6f600eabeafbafcb0183658f6882f02d1a9721fae69d3489969ce30b95d89fd2a5a32c9a8ee5868fe8ac0dcc1713d0bb8f41d8f5aac102d26ffd16993fdcf4239925f9a03d1dea77feb463bdde80cf17c260762f40e65370ea220178b38ab8770a6554d619d5dc4ef9fd21fae392b4e37d03a8cf9cfd556ba449b7f623700a5da2ec30a6f15a54e3fde0705521b21fbedecefc7c7233396f0e18182a6f7840027013a104136b4e46879d3aa73b8aa0d4202da1f5cc448a1b4b1f3ce55168e35e1c094c1db76fa3f93620e2ed2d431bb5f64de1843cfa5c579a54fe74c8376c138bc2b53ac84bd16bc09639db07fef50959970cede04cfae9548eaf8fbc2dd9d745aa4eaabe7278a7b37c77025313ab74b5e7cd892a081d46b47faaf40bd6709882c66c0b28525ee0b38094b4ed3684e022a1cd9aa078b75586aa176a8218b6a019d4909372f5e2ac837d40e779e2eae51f30965581250ea557929b2a6598ebd18e3ed796e59841c5d684cc2134bd40a87d0772c8f8a6f8eb6771294567d213a95cd42db22ce7f10b9efb2c1e58b0759f50d522406f8699721e5fcd6531754593ebd90fa18d9db4fef3a76d5e8c8b129347228ac27ad8fca774c20a2fb300ef4a3778540d2901f4a53dd0d0b1976f594a029b259572ee4ccf7aea11b48ba27b67f58ec1ff9c12c2ff0bb02322c1eba9147fe124165d96b1088068d835f8195ff45ef379b4cc48f14e887d779de7e723e50833bb44b340664c5907d52cccac676df96299645d0d3eb1443fac71abdaeb95a0c440584105aa8c88b5aa57b897125008039d42e094a47b9851c4af2dea480234423e9e8ed86eecc23ad459d051a1244c204e2335d2b613b85629a79efa02ce782776e99c35751d5fdf6dafc30bf994e8077e6b471200a511e0f0aeb5cee775215e8c385bc5122b6fc7ba717162a80a22b73ed4253cd7e6b2cf8cd504f84a939c4b8a1848ad4e93c4bac898a83d0549070b700236377acbc270efb0900cb732fbfa0ed39a7b0037195ba056e9a732a3388179b63166f03327cb307defa3e98a0eedec78778a48a32a4758e4cf9bafe8fa746746d9d4dc75805fe4edcdd37fcd35f9ea7ec8075178ef0fed8166d69d109046ca48c7ad2725a4361b79cf0052717527065a43831fd0b94d", + "hmac" : "5ee09d8f0b0ae937a4307b4c7dfeb299a15d09a15fd3f0998867ddf1f6d042fa" + }, + "tlvStream" : { + "Endorsement" : { + "level" : 7 + } + } + }, { + "channelId" : "4b851a62adaa0c0c003027706666d9e860253463d81971eb22089bb534f9b526", + "id" : 1, + "amountMsat" : 1100000, + "paymentHash" : "b8335cabd041e3f8024bd0b5b10e401868d31ff594b98f2bb79dc28693c7788f", + "cltvExpiry" : 400144, + "onionRoutingPacket" : { + "version" : 0, + "publicKey" : "0230caae9f655304aa6eaf036c4d1839012b5b63cd8041bb0c70d1f9dead16a16c", + "payload" : "67ad62b6d988a208ff75b4b2b17d5594e071360dd7118259f8b855e1af695394edcc0cd67d28c94f63bc686e214bb3b6537d9fbf6afc14e1a68e8ab3d202feedceab7c01a02e15426672f8b0434b5f319c8230df38096a28c4c05cc4cde86ba3c0a15d5892b7ec10711d4983122bd525e9376b373c9e2055b95b4980b180be8dee61a81180668678b610c81cce8df7a07a8e60171b826a823afd77125876a40c5bf7a2cfca6f11a69f1bc6d2770af53daa28b6ea58c60275f53b3985289c2cf8793dab4759c3bfc0cbec4ef15bd8710984ef66117b4852398e5dfef28ae650279d3d5f8b7e8d55506d4dfba3cc3123b66965483a9bb0eb9e6f114cb2ee6f69dca6c53d917e8f0c27f704bed1677338b15a6b8625434c77f284c8711b4983b19134692ce7d3f85559ead3aeaa09e99f3c2a74b157a4768f27856600734b0983b233a222c9c20c2e0ebe3108d9069e62742d270c02a198d5d037488cfccc14dc56b0ca1ddac50e133a217093d598b1a7bbeb6190ee9a85ef4ccc7432dee503aa481f2cb11adf434eb514f8f28ad02a1680aeeb9a743cb350a88402765e1ade1cc0251da42841f1f7de85915c63a42d13942c9d2b1d4d2762f705383f099d6f1f01615fc721266ef45fa20a859e06aea7d44f154be97e4f906096eead6f2c4bff6027d82228265aeba458c6361682ee9cbfb2fac1a179de45b3598444002537393f058c3cff0272c06e18694e280d275616723b7019a540b6c75a6c182696fe5dd89a16a54783d3b533df13c703d081b7b33da3158a0da3cb628393b866a181145337797271e4437d49b0370d50b18d5fc031f35e9c5ddc872f3c47b08c1a9e5c465a59cab419da0da0a2340075b8f71df38c022d97cb97a231898a53fd4d5c20fb2877aa8dba9fdef88936ab36eaccfd31ca674958d2677acfc9c99067000b9a8688723ee3ecea593dc12454bbfd10b73c68abd4c8d099eb55e41ebd8d6a390ca536a7a5fb5370f1a1e3e31fbbe7b256db29b403a94179ad1698d1a7e7503e3744d11bbd349f01d790431b747533272d09217aed55101cded1eabfa705b3c0d6689441f0a03d33fd39bf5ee3dc8896276355906e87c2faf5b9cd74823285f2d58d70db120ab53da6af532900387c107b2803becc793ef0c31c09a5b2c2612652b38910c19238b198c2a11e2f7a2bd51cff9ab01d6d0af3b9c4581b1d7246cbd1f1a10a5b81c0530dcbc8098c2ce283ceca438c0cf4abe734ca8faa0ae8b4849509f445b212e7e1af229db978e67d1c531cd2d9d3c8489c46b8277ddaf260cb727c4a34bda98753007ef1b90e2f83aaa7c5139c3f1c3192acb0ded1cc41170827ccb5fde144bbbb68f139883bfa611af6446a46486fa7c3a5fa9785d87fa229a8503c1e74069dc994f3c284561abfb245c2e7bdbb88d7a8f85ced0df0fb2dd7677878049d0ebd53cbe6abc26b3f34d9e9bb0a1c02812359fc3618060431e1696dc5ca6c157deefc2fbc85a60f6b4ca43c5edb822d55a57591e5632f42f860359f30e3b84f7151d0a2651b2dae1c1a408b2fc2af9f667acd6d2e19ef4f654b48de61b60d13dd011547ec7200cd2a74f26e96e2512e962e073b703300868b6c86a46718c543f896fbaa368e65746bf5b1a4da04b59cd446e38f5c5b244fbfc879b49517c95522547e9a756074c171a43e20d5439803dca0bb0f87589bcb6171992565e1e7f9324b0fd0e025f36dcdada2d14237c1e79ec12497c420c99275f637cdd5c3910f62c8e42ccfa627893b46ae91e13d3b54d1d98408894247c803233fba1d8493aa10e2560b0d2fb4ae5a9881259fdce09e8b", + "hmac" : "5cf7762cbc8ece0a15ba576d4b1d1910b376f2d19f524671275e6e4be33b4d8d" + }, + "tlvStream" : { + "Endorsement" : { + "level" : 7 + } + } + }, { + "channelId" : "4b851a62adaa0c0c003027706666d9e860253463d81971eb22089bb534f9b526", + "id" : 2, + "amountMsat" : 10000000, + "paymentHash" : "53358f633c280126a51c2c7430662b669f24f035b3995d5bb0d57b88cb067993", + "cltvExpiry" : 400144, + "onionRoutingPacket" : { + "version" : 0, + "publicKey" : "0279e6e6dcba2ada19aa78346b56df3df3bfc1820ccc9d3138635f4dde381f968e", + "payload" : "2e0070f136ec8b10359b398b151c464035e95ffac2d4f5ffb9e3bb4cefb8c472ab51a75645be45b2067b5f6abe28ed5b8c96ca5cc5e69a5b6e7c1846344778294285f46f70db900fc519ddf416d940fb10c6f0ad85c9ea803569affb9bf9de13dcefd8111d25b9f62e54b3f536901c84f8ac0ce1a51f514cc917aa32e2a14c0c48253bee78c4086320c73ef3053717e42b3d79426e898ba6b3fee3065eec0750f1f6c1edab4e96baec2306c49a63692ca7c318f647f2a1500cb5b0292752bae3f10da3ee6b35d049e76897515b42ef2b4445da80f5931cb9131d525106d21ba30dadfefb65b25333cb213cd856c91f28f42b88879904201a452bf996b131eef06be945abe1f29d20ff4de145757dca2b51891badd1b0ec756f28aaa4b6f1ca24bd9af228ab76269469dc4798d29fccea61825ccab164f6353f24b198ca98106c639c23fb1b88c45a51fb710316449436641887d077b23b81b8f3bc663d98106ad214da1f51235e4c7119c1074f39e7bfca123c9a665b39a9de36ee84cf3fbf22c3a934768395eb02f3e5b74c642048d799363daf6e9a663433f8ca01d08ce08e711fac40de70b920140bfa762a4b5740062d8e79d20a1e0003bd12412f51995263d4c65faf4912ded001fc7169c5f84e585435f60595550b57c895795a3bb2b3ab7f3a9eda479f96c232b51774c775ce14818fed5e404c3ecbf9ed767ae4e0722e13b703fee6ffd2a3f78eec0cbe80f84297fde055aaed54d4199c66ceef3adbe819876a48ffebe337cec4f222912f4159ba51ec4ab239ba349d40dd7a8d7d15654bc91605d7ff7acecd3cb5781996dd896f70200a6236d3175f0f34fa4c84229a2f76297e45ad925aab2808b898c0712f9c7bebf886308e1879b15deb24efccdf2d97797321a4ce3d46688375376b540c1db049384157da71e3ca534253f4f8e3007996ad80c416b5df9b06fe4f73d3dbac45a95d532792d0ebba2649afd721241eb2ce07a291bffdb77cf9d083d239fce425e9c919fa6d3586368db2abc4eea2dd03bec12e60d8d19f25d873505f04064c2bf591e98b44fb1b3f0b1d1971b335a5d7cf10045bd1008aa40d930f675e3d1ae3b3342de4ec87b3c7cf5ef883bb30e3dd0d63283c22530e4ad469a1e95d9813d6a26f0d518de096eaf39a01862aad09fd99b195b6d8bfae4c096fcf57de19efc032c859bd9800fc1064c36cf43dd1698e86a657e12b36c7cf01d7a24d85a7e1a6196171ac9e62ceca81627180ad76196873d2eb15b2717eb2dcd4f28b87169139ad6a17405e5e095f949781b0ecd5a339683d0f01e6af2125b20098f2678b0be00ae4dcbaabc4e36943e92372a4a1a2600e9a7f113fe0a3669839709986f9366b254a5b67849a5d7af091b016579de6f4a30a49c4723fa470a55fbce52387f1b929ed6933008003d962351fe1f1a5ad1c66fbd7ab81ee06a7e1b884b5b1816b89575da49618f56de6dc7566f263b5e7749463ea40aa3c7dc417bc5bf616b6f1a8f80e83cf95caa9e060214e2ca6bb8bddef8011a18134616fa7c2c1c957737a556f204a3d9945ef6275b532c8064bb711220291a4c628d34982bec5185f437e12ef168d76213a3654feef53b22e976b1af10998218871673e6b69fd7d84834dd4926a1f4b7b3f9189b2e6cfe061eb44e1caf7e95efddb26d26f2ccf736b64fdede50825e9df37ead839d2d6b8b6079092c2bc63724952ab0779ac6e05dc41fb014a4fe1a69ed8ae4e5879462f00dcddccdd5d2a11b15ec953fd80408d8a5a60824c2f9e500591e14cff01887b8c8aaed69ff3df2185b2018f9192cd98f70debf692", + "hmac" : "e56ad161c160fecc76fd4668459464898800acbb4e99c2f96219380189b059bd" + }, + "tlvStream" : { + "Endorsement" : { + "level" : 7 + } + } + }, { + "channelId" : "4b851a62adaa0c0c003027706666d9e860253463d81971eb22089bb534f9b526", + "id" : 3, + "amountMsat" : 999999, + "paymentHash" : "fe8a4ea0583ce6bd25f2459fd886caaf8bb548ca73f6131bced1e987ca1c0187", + "cltvExpiry" : 400144, + "onionRoutingPacket" : { + "version" : 0, + "publicKey" : "027ebd8fbd32ba770c9645f58014cc8e4655fbd0dc6e010c972a52da0557434004", + "payload" : "754648e2a3cbb18451d3e2e5673078ca399d5f12eb08e32ffe30d9ebb532e0737380e1fd7182daa76cc97ecfe69413592e4e0dd89879bede59627fad297aa4b3a04b56026bb88fcb313289499837041f9cee053316fc093274b97797b55a944068f06e361300edc8f82c07fde4f4994e46798591900b0e054bc6e34ad3a87fd8d095d713fb3acd9c5a3fa39209368e6e929619da3b095aea8a51a4389508f29626a3fb7dedfd6341c7c82fb1000c0bba364c61a73c13b1a830b931af119c36ad71ae58fa5279050ffce5c7ee93a6913d961ee5473be0e63df6074923b2b64c292a2eab005d5b7ea194e8208de33d6ac6281235aebf99ee3d50a007aca83cc14ce98d8e8bcd7f2cc351e82e16c3eae77ff0dc2a852781892940c58b5e2e184fcd01a59d113509d6687ae412257d6a217416c00b13f8c909dc35aaa635a238126acdae9761ecd47edfa38d809a403f67487920f5771f253805ca5bdc372942c995f9ac6752f69414e47bbd4fba34b2039bdc617ebeb971baaf2a7715ecd4157c1fa04c77651cab9521cd42741e46949e1459d643e3a73030c3b5ceab540a8437522569392a4a7b8157722c99daa63845c28e27cfaf381c297bc675e20d3efdaa45f59bd865b517839210cd92c76bc4630481854a7d8ceb2519f82f0471989e3fcaa092b89dbcb7d64cdbae0794f307f2393ef3bd744648d3d16609802d304d1f93c20d028d2a531192a96cdc562cbca9e46b3e1d68bf18594ff34f2f84c01514a6d530fcd05cbbaf1126bc12ef4a75fc8676e440ff4ea41bec0ba16f8377692b758fbdbe56b273a514ec85244b3fedb8021f8645e2744c467781ddedcb2b0a56b589938fc411bcc8a115bfb382d53634b2a26325b299bf1ddce1729fe33be4589fcb74c013c6e05affc63d95f0da9f8a138c0f2236f789de16c20a284ef2bc265a358412b95fb6a105c8500bd85a59cf5789fc8fa775232d98276a17022bebbf631c258b6846d35cf112bcab37c4690e6abc9f24dd918653584056beccec1efe2aeab9664b7efff28aab69b740ae6c4f380a9c71b93556f7317715785b1d90fa0d26f6013ee3fe5a51c32249b2d841dd4bee5a4a4bf10ee899f923acb3ea3537d1396c1bd4598767973743c3454b3bb795a39f83e2d44966a607037f1a73ac1fef6f8e5a8441bc692b8c762b52d2bbe953c374a2cad1db87f2116f9a23ed4bdcc16588bf03ff1c051b4586821b0c3ffbfe84bab3feae31f3999a544e64366a0972c0ba3598e9116b1a70746e2fa1a94d57cec0a5eb14b15e55782d649200f0ddcb862330571e0ca3c59feb81d1d174e2267af235b8490061095e96be48343615c7b4219136dbdc5617c1fbc6f6066453f864a2c6fe9c0701781d9e51c627f1b527e6ff256773c02484ebcfd61c95828203b56ac07819824f4ffe32ffe961fa739036e02b534ca39587194431e3ac98c52bf9d64a0458528f23b2eb40ff0c0097f7efbcb0d608e0a8589ad5fa2c4a4263a803f507503f3b3c55422abcd8116fa04b9e024317958f6863711f7f6d2698ce77fe77eaf4a66cb9e1ae1eb1ffd5ed1d0930fff86b03ec4664ef03dec919778dc93e53d4a722bde1a00d1adabab97116a9c9665187976968ad0f26c45b6c6406eae2ac00cc94fffd198c1f44dacb164d2167ac1dae701aa20edba4ec004b62714eaf858dec1fe50b723b7cadfab51f08b49d0fc47636a207590f2b729b812fa357f3231a97774091bb981235c32c2f10e52e6f886b0473ed590f9977e4d28dd74599419835a398a5556133bc1cfb9fe537355bcc35bb3698c44b4b62d19cde005b3ad74006", + "hmac" : "f489b7419edba57764791019ee303acc27b6a6171c1bc4d296576186c47e2249" + }, + "tlvStream" : { + "Endorsement" : { + "level" : 7 + } + } + } ], + "acked" : [ ] + }, + "remoteChanges" : { + "proposed" : [ { + "channelId" : "4b851a62adaa0c0c003027706666d9e860253463d81971eb22089bb534f9b526", + "id" : 0, + "amountMsat" : 999999, + "paymentHash" : "2034580a9321047b4059618250f0b657d77ff81588b27c231271e8a76dc86ee3", + "cltvExpiry" : 400144, + "onionRoutingPacket" : { + "version" : 0, + "publicKey" : "02c924fb4342efb18b87c288548d5f7aace82ea3e8e62eb6a6c29a6c8c45172366", + "payload" : "8baa0dc7eb2767e1cd77c59e091e954e3fec2f41d48ba8d4a7483dee5651509f7a17e1d66034ea272e4c665c5c6e2d2631edd213d983f2295d74d30755b6035284bb4e7dd87cebd74ac7e3d019d6ef5a832f98547a168a168307c23ddd5f1ef412c24d710fb249056a4c171410ba0182f9b056b03fdd8d7ca5b2d430831af6b3ffcb4926373566f1c4f749ff53ecbeca24143ef6754fa45eeb1483af8dc4c447fabdce44f1a65bb6c99afdb44ac1c63891cecc0893aadb4df9ad70eb576b91f6591b78f19b5c8d6a8273c98c6084051c0b512684108bb2bbe361507e5f9607bfd15844cd403a9c4f63830ef5d06bd1374fcd490b54c6a3c202f2fd88971413b8baa3b440f6151e0a46c6d4fa583ad90483ab5831ac95f1b5c21176c25353f9e873cadd2e59f3d6f99430f65db2c9599a3a74afe6a64dbe1f56dd9dbba358ced0056706ca21a8ec6d59d6ead3db03a8508531279160adcb36fef941229ff6c4a7ed8ace26eaf22eb35e30e69c479daceca7aedd462ce1bb61060de7158a6d2a3c14052009f6de408f73f63d69cacec595d195833a1885d0d56c9ba74ea735371869bc62d1c79b2835f8b7783f27943164fb6b38e3a077dbf9d3d41a45721214ed14658c78bee759e1625e173b8d8f2b166a89f4efb4e52c96c9eb4d7f2cc4eafbc20bc13de49dfcfdf90a266d083663da7a8a4c414ff2addd48114830b2fc77a74aed25d9c2d57c86afea4087493249ab57b38f5e9cfacd623861bd658871ccb33c25e4538dfdb46df3f78ac4c5c2525620bc3ebaf520f7f805c84213662b44021ab0d32c9a004b985cc685d93784e0cf91876989469a5b8960a3b8d097ae6dc3107b49b82dcccdf7c3b7ad20fc0f56d4c1b9882241b7cf70c36f70a16660e244b2765227f3f0ed48e2e495e2b211a5bb2f5a2384a06a2a6c06fd0a42a82127971b927bc0256730e82c6f36fae2b15daed25501ac7c3b963596ed83d17e9b0bc42fc5409d8f525088d522ad627a415adf34ad000cc20cf8e62e19d31ec4d389415fa0574a01123555579c964cc0a0c952419fd92f393a7ec0b8feb43f7fe4b8f9acd6f943470c1ca421a80aa28ac8138ad45fca84b57aac0fdedfb583d76e830fddf85eb0705e44ddf044eeb7a814f1d912fcf14c4dc104e2ed3eb661151071fcdd8cd2e51ec60838cf9a3aed9ac10b6e11707d808ff2bc817c255051b8eb79c90c413d514a9256f24c355128e17b866fe65d5e36f082c0714327e8e7914b1775dcb0dc76efc5d5d3a02083a3fc4cd5ee1988d26319868e494fd4eae0099300bf7932a348b4beb680a93d50937c9f4967ab901d297938b241b9d9b5b992f6603801b80eb30a5f172716cd9f1a9a5e580f826d6f639356c6aa7086bf66a5612aabcdc8980073c6f331056673efd49df8bf1cf9d36f71de931c773ddd3ce89ceae8c71017ac7cd21a5a6d38f328f77802ecc84d49fe6f144049ec6a63a4d59b99ad51250d2fa4357002436d42e69b82e10b1b0eac3ac2e7c38a46ab5a09ece95948bf180fab8284c594ff6c264f7e4ca9a5ea0b015e9e58f091b7adce4ca34a132613612463842aad112ff218bab88007d97e245c759c62947bf0ab37efecbe6fbd0ddc3a2b7bf2c9d8f7cdfc01504094e811e20831934db121fb15c9533d40c3d4b9e63911241f3b74d1657057285303d11173170fe245c4b612247e8e477328dd09f26736cdda4148749f449a9712c5dc6ceef950868110d621e8bc1c84ce28d21375fe0d62363a0ed2089d32db666435dd343f3e673d44d8092c46c9c0e3f2ab2100bde4c7b96bbe527229ce83dad1660083c4d4", + "hmac" : "fc171d9f300e254a9ffa2b3a23eae54cf8bc03f7ab239142469b11179cefd72d" + }, + "tlvStream" : { + "Endorsement" : { + "level" : 7 + } + } + }, { + "channelId" : "4b851a62adaa0c0c003027706666d9e860253463d81971eb22089bb534f9b526", + "id" : 1, + "amountMsat" : 50000000, + "paymentHash" : "d341f3e367059875c56147a7148e6be30e96bb9c7c74151758feb7d5757ec01a", + "cltvExpiry" : 400144, + "onionRoutingPacket" : { + "version" : 0, + "publicKey" : "020c0c35bb44e606d10d250c64c47be0d010c6dd2246b6179460f79236f92de91f", + "payload" : "6c9690bf306f23a63ee91713b5842efed429b8da5d31109b574e796624ac50a1cb964d734a5b4bed9d159fe4d68005a044ad0bc5fe3e5fc50e6fa5c6f57ea38f49d9a8ba2907b53fb7b986be9742dfb083c742fbde63a2c1f9f3651569a715ca76a4061f543e3d58cbf54307e29d75b3192ae4934e53d078c86637bd9d62db281586171acbdea15f3dbbb0871b07523dce005ef277c8ae26dfedf442245ef9579cfcca53532d948ccb90956d21b293ac0f2e3ba89fdf05feb2fd11880e366ba5104cde531d0074157c67617996d5f409d46ab2f4d566ed59f2a47c4d4e0420cf5b8d2a6feeb5d502102f8e5e1c53083eec0f7161ea782793433eefe9c444e8c2b940fd9b2bd3f343964d0f4e4a785b0205584da57b844fa92f05db2a4bf0cdd814464214fac5e0ff8cc1c14001dbf85c50fc9b818c233b4f59c5f9e5bfa23275d81e4449dc68f70fb47c343472552b83f725d3ff65a76384b1c4c14e6126afa74c256d7a0a9d53af72ce7da8c6cc2b4a7987cf78211d27e487e0d78f040653e89e326636733bf39a13661ae307b43e15c0a5aabaea428b1d1081630a17a94ca53ef27ae06d80756b7ba5d5720a8e947e684fc7d9bb90ba8114e106c9b86b4a138dfacdde4513e4c038d7a0477ef6ad0b503e0afbfe8308706f84f9557e7097b2550bd36808e83b87926a09f917e57bfdff5819215e322844832795a59ea0d3a4d5a277708f39f8965e33fba3f042e56a089ffadd6be06a9cbc943100672b6072ab0aa04d51f6cd0a48ba929ecfb30494e3b9b3fe8dae70874f6f16b997959afe850da93a9ee268e276863666ea576dfbd3607390a566c1dd14f7359e39efd07eac54aa2b51ae75bb1b73e9268065d2c45a9c354f38943e4975dc45d0212ad2a35906ebedd905d8d608f9575eece992508b441375b05c41b7346a4e3d99a2d93854e628ecc8a1239980fd8c841071642402d2eb1abf88cad195220d41ae26be0a9c21195472d42cd5fcbf8be23ddb4efee26d1c6f4c7206bee26959dee969daee351d7c62b33dc8d49d1b42dfb379225179f997dc93be8b1b264b788d277178b385d72de24c5e24db781de4ca625b5847ddd1305f647aa3271bf45bb397fda817f223b0b635e8c071a0e9067fb8c0c6ade6f3f41355b8e6d80475254606239cecb8819e7ec54979c868667bc0c867e2cba7c2586a2e5a930cb11edb66b6958b41f3e73c35f51486ca03b1f77214bee7ccad7d2de529ef10ab87457cc8711ed3844cf9cf4e1368ccf3ee36cabc82df5fe86000fc62f8e3e0cd6d80c04d11f6b7f60d0b020f3c78be0cbab6e9aaddb0170cf2c1ecec2975e0b9df69f57f56a300e35638a9759d8063b0d21b1fb30dbc28d0515bebc27e57ff5ee10990ce38ada46f29e15b1f4c41bf144b4ddb99ebf5e6ae7fccad1d177bcdbff4e585dd41bb5ebd4ad8d9bd05e14e67b6a2780d5197986df284b3a361f0090b4cc55dd6998ade2afb7ce5ed1ee119a28548ed816503f81646f439964a0df66242117bafceabc89a45b20ae305689db80c877b7093feb136188a40715fd69cac4965bf58cd1966ffe9d73a05dd69868cb9201b11d48925e738000b94565ef6ffc451fd8652a1194bf04a210065476a3a3a5bbb048414dd89fd2d3e6f8fdb964b91103f86c289232db9d5d37cd95cc0709cf0237a72a43b7fdb5f8daa574985d74f3048c6bff9874187d739f95db8f3d315a7e767a0e148cb6610569a3552e877abaf71b016a8fe7c02885f70799ec7ff5b9922f6021d8ca931dfa24ce79e50965b3312279a7f6ecf63cd655b62526a6267753ecf5c2c99c32f00779b", + "hmac" : "ad3279099f3968fee2b1cc21d383e3771116b9e334ef766049b43d782a7fbe51" + }, + "tlvStream" : { + "Endorsement" : { + "level" : 7 + } + } + }, { + "channelId" : "4b851a62adaa0c0c003027706666d9e860253463d81971eb22089bb534f9b526", + "id" : 2, + "amountMsat" : 1100000, + "paymentHash" : "5f56b89fecac2a2630fcd0c7f1a6befe5280b53767a909be33868385e1bf51c9", + "cltvExpiry" : 400144, + "onionRoutingPacket" : { + "version" : 0, + "publicKey" : "03b2a1d1a2a49ad936a74cb4a9a711f743b5a050454d3b13f70b21cb2a182e5cc3", + "payload" : "a57e300b854f537d24f86c2d475504fa5d7c127b1f64ec93d72fd8bc1f7315ce94ab7677437051fb5c41314ea10f3c89ff5a355074e9b8731b9fdb2f713f1e5d3a8f6030022a791e5fe7c2d33b2ed24c43604b8d15859b86338beb8d9bedf98f40c9e0faf128e9f4e8534c731a1099088c0af7ff4e76e558b0bcd3f3dc4d30043b340518c132efa90fb5eab3daf91418575a910ad790c1ebdcaf1c781657cf62f62c3fbba3e1896c89eadf2caec95f9db22ba2a99f6ef8d4c8dc66d8333193e39736432ebf5b9052b4af8cc2541d0abd83c6b0a4816a7784620e95ff07b9bc6eb268e5500562dcb6d410d6946d4df8c2511142ab2ca96d0c2b8ab37c1f9047b638a1fc43b10b39436d256976cbe042548d64ebea357358a2ffd13a37d3c7dec9242e30d99255889d261e4cd3cfefe59ce2e1141b68161d9b75af6bf4c1419e14ccfb27e74be29d03ef10d74d014e4cc78a5c831234b0059f60e41a897a11d247a386d0f72dd7b19868dee5f47030145972ff683de626cea4bfb184cbbc77f7f8226e5f0b16f576a065a830dbf76f31a10ab8ce9332fa1f26432a7d13d8885ecf34239aca7198ba88c8d84bcf9c966a8649d58da5c69a3139bbc475aef243e5c7ce0825fdf34a81c8566c2ee313f2e6e19ebb818ddf182684dd49ffc2f97bd29e5ef90787458dae49ca1c53bfd34f9a0ff7bd69858511239224ede8d4ca6979f14fae06a2d07ee9fe27a15474b4a83778293e122a140c9718b3974d9f77d99062f9499267b5006108d7040f3c285892c8eedd7226b9fda611edb1ae5c13306e284e6f0a41e39b30f61535b93a41e80aa114092df635409842e69da6f01de516f5b603e8066eb9acdd391d299c0648093703fb33a8ab76d6b08f691e44783e44bffc42278fff06b2e115532e9a26e26c675f2c1694222c3a9b290fb91c0c1ac85ecb253525b7bff3fc793af3a78deb6023e9ca228253f6bf40be04135f29c3533a7b8916558959251e77a918bf02ff1e73e72ba41788f524df1681afcbb598927d69f587812b2f5870d9d75210219f751d5a6039debc7a750a6be046415b8e2d065f56d46de0ef1007f24480bf8915daaa6bc807bc8231aa797d3c8698ef4d87feb0c0ba9ce4d1b5f9de1618b93df02a2f5d13eededc4e32a0dbf6004888dd6dc4908e37a77d722d2c9b3515b204634c7849bc623e9c16f7549b80c54205861021bedf964f6914cb39bc63debca84d7c7b1272d3d0d0d3ec691791a7a6ee5592fdf2605e8e02c1e83d0abebdf15dfe1c682ff82cc5136cdbbc60f3e680bb0913013886bcfb688ebab2837e9223cb5890d75d7b6a32506fc0a891c5c06d12858277861459c75b45794fcc4af5a8753cc9a5de60e703c455e4966528b9514e4545e124f1feec5583b61c90c03ba7d0a002601171ddc29a10922fdea5589d554ab690243829e80c94180af921605cc122d1cd9d8f561fa5bd6cc4479cbc8bb6c5f2d04380fde6ef06267b8555dc5b7447325a775be9cd04fc604672eec88abb16503c23b47d990a40aa91d3be0d451857b9ad6892a21c8d7d7ef228509626b4e2be32c36be8faddf3b84ad8d29120d52109a2ac2bfecc1949ff176ef5218f13748df023015c304dac20b7729ffb4c6d1bfa5cb9dfd433676b98627fcf6d6475026653f6954e5c127af229328d87aae4cd34450a8ce3912e3938a2bc877e1a7ee7e51fb05d15a5a96c235755a72c136198d0588d7fa96f7ef5703e6877ca9ac9ba5b3781d9a46428617e44eb29ecceaf90a6656db572d638ff28728203bb275c51d29dc876b241e4178c02920142ed4002caef62c6b60e", + "hmac" : "710e9ee11b2463fe48ee38f0f144dc2f750e5ed7598e722e5894008361093522" + }, + "tlvStream" : { + "Endorsement" : { + "level" : 7 + } + } + } ], + "acked" : [ ], + "signed" : [ ] + }, + "localNextHtlcId" : 4, + "remoteNextHtlcId" : 3 + }, + "active" : [ { + "fundingTxIndex" : 0, + "fundingInput" : "26b5f934b59b0822eb7119d863342560e8d96666702730000c0caaad621a854b:0", + "fundingAmount" : 1000000, + "localFunding" : { + "status" : "confirmed", + "shortChannelId" : "400000x42x0" + }, + "remoteFunding" : { + "status" : "locked" + }, + "commitmentFormat" : "anchor_outputs", + "localCommitParams" : { + "dustLimit" : 1100, + "htlcMinimum" : 0, + "maxHtlcValueInFlight" : 500000000, + "maxAcceptedHtlcs" : 100, + "toSelfDelay" : 720 + }, + "localCommit" : { + "index" : 0, + "spec" : { + "htlcs" : [ ], + "commitTxFeerate" : 2500, + "toLocal" : 800000000, + "toRemote" : 200000000 + }, + "txId" : "c6ea335bd980cbb54eaa76aac45078a042997878fe848c29b829b083d075b83a", + "remoteSig" : { + "sig" : "2a8df13046f5a8d8ad87d5d681f38d512768420d8ec23704e7372f2f0490e8c60a88920d8c98973fe078b8b3929e084cc195f8ad956b92a5ec3c1b8ae7c59c47" + }, + "htlcRemoteSigs" : [ ] + }, + "remoteCommitParams" : { + "dustLimit" : 1000, + "htlcMinimum" : 1000, + "maxHtlcValueInFlight" : 1000000000, + "maxAcceptedHtlcs" : 30, + "toSelfDelay" : 144 + }, + "remoteCommit" : { + "index" : 0, + "spec" : { + "htlcs" : [ ], + "commitTxFeerate" : 2500, + "toLocal" : 200000000, + "toRemote" : 800000000 + }, + "txId" : "4042c9a5ea1d9ac2ba846ab65a6a1c9ed7464f68d8cdd57ff4d001f8589bd068", + "remotePerCommitmentPoint" : "038a55e743334350ad9f60528552fd0209ab44a31927c477d693933937c70dc016" + }, + "nextRemoteCommit" : { + "index" : 1, + "spec" : { + "htlcs" : [ { + "direction" : "IN", + "id" : 0, + "amountMsat" : 50000000, + "paymentHash" : "4206184c2a00aa7b17f33936aaff2b237528a7f1034df562057f9c904b3f34ac", + "cltvExpiry" : 400144 + }, { + "direction" : "IN", + "id" : 1, + "amountMsat" : 1100000, + "paymentHash" : "b8335cabd041e3f8024bd0b5b10e401868d31ff594b98f2bb79dc28693c7788f", + "cltvExpiry" : 400144 + }, { + "direction" : "IN", + "id" : 2, + "amountMsat" : 10000000, + "paymentHash" : "53358f633c280126a51c2c7430662b669f24f035b3995d5bb0d57b88cb067993", + "cltvExpiry" : 400144 + }, { + "direction" : "IN", + "id" : 3, + "amountMsat" : 999999, + "paymentHash" : "fe8a4ea0583ce6bd25f2459fd886caaf8bb548ca73f6131bced1e987ca1c0187", + "cltvExpiry" : 400144 + } ], + "commitTxFeerate" : 2500, + "toLocal" : 200000000, + "toRemote" : 737900001 + }, + "txId" : "4cafd62677e479a252daccd9a372b3effe908619c29941a9a703caf5ddfff53c", + "remotePerCommitmentPoint" : "0340990e09f3cad846dd40c129dc14d32653286dfe2ea0ba17bde9f58e56e91620" + } + } ], + "inactive" : [ ], + "remoteNextCommitInfo" : { + "sentAfterLocalCommitIndex" : 0 + }, + "remotePerCommitmentSecrets" : null, + "originChannels" : { + "0" : { + "paymentId" : "b703ae5c-0193-492a-ac3f-b37c3a58dd2b" + }, + "1" : { + "paymentId" : "fefc2c75-cde2-4136-86de-5c4624b13845" + }, + "2" : { + "paymentId" : "ba0a2a33-526c-49fc-90a5-4bfbd7f1197b" + }, + "3" : { + "paymentId" : "734ad7d4-b2c2-4343-9293-c6b499e27292" + } + } + }, + "aliases" : { + "localAlias" : "0x3c38c9947786cbc", + "remoteAlias" : "0x5d1848c8993027" + }, + "channelUpdate" : { + "signature" : "11517df536c7e4917efe15149285bc6c67bb596a97aa2e7b10173be1977b08d61deab3baf8dc69722229d862d79714338d093742008228b848f1aac069608ef7", + "chainHash" : "06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f", + "shortChannelId" : "246668x10045304x27836", + "timestamp" : { + "iso" : "2025-09-05T07:31:44Z", + "unix" : 1757057504 + }, + "messageFlags" : { + "dontForward" : true + }, + "channelFlags" : { + "isEnabled" : true, + "isNode1" : true + }, + "cltvExpiryDelta" : 144, + "htlcMinimumMsat" : 1000, + "feeBaseMsat" : 547000, + "feeProportionalMillionths" : 20, + "htlcMaximumMsat" : 500000000, + "tlvStream" : { } + } +} \ No newline at end of file diff --git a/eclair-core/src/test/resources/nonreg/codecs/050006-DATA_NORMAL/splice-commitment-upgrade/data.bin b/eclair-core/src/test/resources/nonreg/codecs/050006-DATA_NORMAL/splice-commitment-upgrade/data.bin new file mode 100644 index 0000000000..a0796e6826 --- /dev/null +++ b/eclair-core/src/test/resources/nonreg/codecs/050006-DATA_NORMAL/splice-commitment-upgrade/data.bin @@ -0,0 +1 @@ +0500060103947f7d4afc6052752add3a000801a91cd0e549ee67032e57a9ec1f9dc5a5690101041000000002aaaa00ce2f18a967dc4f25f414e671ba6585f8ef0b8c5fb812c21064f55a2eaa0009e27eaf4ad67df152b1d06db4e6ffab0cd238d78d4563a452e74d54607227d7178000000100c000000000186b0200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000180822aa698202bbbb671d15145722fb8c28d732cddb249bcc6652ed2b297ff1f77a18371b1e630002c586d173691ef2ab086741471c5aa0279775e1f0538d305e222b735af69610bf0337df270640e947e898084b1859603d62a2ce8ed7a30b1cce4f53647554d4cd1203ebe3dfdc25a1ddc5fc98abe3db5a0e18c70cf9b2a47c0b596bbbc81f8e8d933c03e66ffa803a927d55a99347fc86f2e23434b218bba6c9775c25b74b2c2adf7c7c000000140800000000000000000000000000100822aa6982000000000000000000000000000000000000000000020000000000000002000402fd05b103947f7d4afc6052752add3a000801a91cd0e549ee67032e57a9ec1f9dc5a56900000000000000000000000000e4e1c066267cd731c4a7ead922867566db3b7b22db847e4f8d2d3c07b9f7c44285399300061b100002e992f6a71186fc8b9cd2cc871a3f5b5ea02cc657c01688f6c347fddd5ecc08d3ee386ee85dfb97b21bf2ac32a19e291fe75546d67d580fde35b6ba1eda83159dc1fbe280ee77f5eb6e3541c4cf121e3754aaf188c6e6288ca6cebbe738f90fc38ab9d3e6d0d98df92173e04604724f5a5ac4f0c6b28d430c0c336ed95408509c4a640d6add299870918aa73b269279db1fcb2427cdef6ce7088c66a6f52df7b694d51400a17dfc763099aaa6fe7d1c98893b0f31aa37119591d1c9718e4be920f3b08599647729691bae6569767a44ed50ba67a74c7deb03301d5f113767704fec54f1c9c753c5e9e484812a90bb7e51bf6d7ccc3d9ede117b0718147b9400f341b007878ed5fdb8822983ac94c4553e15615277ad6fe356c7261a1f13f989e3a3194518a4d4bcd55fa5e2bd660060a2429cbc8769b670dcc4e80796f6c5a59c23fc8ad415136085d886b99b87eef79e523b5d813c311c68fe271e2120401e7a5f01497577e2f032f2f2bccffd5fafbda134b2ffc764643a1c5015a9f26e44d2ee8e79969fe21b9f8d3b2d3e3682ca25506d777bed6ea4e97a4cd1b3c487e96b2fd851cfbc1a2302d2fe0ce2071bac4508f75a20009e36791b013b68eccda101621bc72ff9db7e6b5f5045e2a04906a9263e11ae4fdb464ce42c12ca79c335b4ceb43f50e67eb698253b99ae38376c3764928ff98f8cc6a8352205f8d845df9e7dd4f248976db315c9cfa6fa2b6aab4b3a5188b3abf582257f32faeb3b5bfb268d43e536943f71f95969a3708107af520147840581f4ddde66863fa23a54df93a922dc896b966d523e510d747c78f9d51aa8b5e6f311653654d94a39f08e4b476f8d816abb5c40121049d2be70c39ae8e0c9ba3389c12b14fa04def6c4356e1a541ae27165fa4acbb3a84fca3e3db63445eff72d666255dede42bdbc41f67f58eb553e7648f3af32e5515a3557f1dcc22532b8c13322b1362a37f034854275fe6f7983d9c46af013ae5ad3872e070e8bc8e45a5bd6ced8de16150054c93a607aa4cf8d7163817aaece126ada529e3b8a7cd8191237d4509e0ef6026811b5b6fbf6882570b00f6dc19d85b2983557d9a195ad29185f1732e74636016404d7cd6405fef1dded1269360da63b1bda5f855927757fc4e849bcc9739208e761e4ef08f9aeec861965923c1748281ddb47269265566dce73f3297c4a8196ef80bea6fab9ea8893876f3b8dcc002d91ab636dd854d88820ceace8aa6dcd140bc1a5308747db58e1b282fb23c9d825ee5b1ba689abd5a529259243eb1183c86c9714c6a6f714afbe7b25023d17198127b870624251110c2541d786e71ef01ad8f9c4c174c41e5bbfa8ed61345212111231f488d4c856cc82869536c66366a337b7a47c6bd5473fe5ca0eb48157b33908df883fa25c021d3c025e7693ae3fe8ce7e3e4e40d3b885f0a51d956d5f1d5b03e2a0ced34da88c54afb3a6f115b0c78ea40c522322e1ce9bfcc36a7ac4f606e8f666f76d8b3fb0dfaf2c7fcbed8402bd64d15fa904be71bd1773952ad9a5d02bdbf87af24db4c6e0b20db4ac6a4592e8718a074571e5406e81f3d6ce0f4b954ddf41794b840e6e52c4e3c7f6c263cfe4d0892f1cbbc16596f34d71d28aecfe9fe5c2d24367c4cb8454b736595b342e05d547d62dfd7bb99a63217a3edc61dd52106fe7c83f93ee0869f7278c27b822ad75ed4516a077fe697e7cdf36ab96cde98a7e44b0038104eff9937dbd78dc3b7f22c68b2d60d2be3bcea9bc5cf39b83e5a79dc611e54c038e755722304005dbdfb79347cbebcfbfde110090023df0cb0bed5c9fbec3d5161331c1c3a40464fce5d10000c118048cbddd9e42ce2c0e9e5c598df48aae983fc0245fee37d213569328c36cd6dad3aa9794a149efa236a0f816ac4dce892523acfe0001a147010702fd05b103947f7d4afc6052752add3a000801a91cd0e549ee67032e57a9ec1f9dc5a56900000000000000010000000000e4e1c03cccb3fea4565e9d90f7cb9067a2444b804459d2149d302379dc63cb3cea7fea00061b1000021a6c3a41aeb87cb65b34a3cb6e45c7962879e274c27c8c4bd9c50029ad382018b3eb40c4fa2d1f33e3246c2b1b8ae6dd4c9e6885540778ce00344db55fbaf4ae3dcda0381ca894797d3abe9a4b2d22b2521461cb6dd15e6984feb764330e06b13d09d08eb6a3b6de4e4d1f80781e96a574c00da822e7836d3ac5b9b9bf2c2e751734676bfe2fc7ce6b9146e77e097eddb7575e2409d81753c93552672b1bc2c410510a2685092e1b69af46c652278ae69ed0bf824c34f83399a20371613b819e303b50825215a923c7e9089bb3edfe9f387944f0c93ee8f84ad1fd5c27dadab84ef587b2c256855fa241c2ac51cf71b36081a5111b397c5211f78b87f4bb10503917b119efb7909a47c95fd9379e5f127a38739ca1e479369088ab0c96e9d1be399865ab54923f9867b1796d845e6f0db7d972a260c09ae1cb060e6a68363faf3599515664c44d13dd51f352a3f8d677e6045b5574080b3fa7c90f6eb917a9c1590f3d8f430d7cd81fc88a2dc89f2962b4548f8de5c91ceeb0af45de66e8bccf27ecca44dfb8de686f27623ce88a0cf06645af8e7ac3dbfeca8113aeda8d29f0d5bada0a015d38b0d5f1cfceff63e4df1a302918c008a9a9f250d4c1a1aaa7091743f4994cee0bf5b5655462ba16bb982579a4b022c93588428f1349fc230674d8a0d58156c5f1e6f9d8f96a091f57acbc598cb68494e04503b43e5cc3e9d3f975fcff274bde1469a060aea86b8b86bb3400d66bcaad6d0e6618074e1ba6c7d3438c6c5204814e01fa92eae7b4760840c36903df305415bd14fd842e0ec03b05f1ccb9b0008c69dc7f17fbc2a67cfffeeabb91e5f9f771dd1cda90b0720ea48e2d0a598c556965812c1ee1ccc93674386f1f1a513b55bfe527341c35fac5746b9d335a6ea4e064f9053f1b1f512376b8b20858e26c91abec8a4b40ebe593ac33544543f3c5bd45566470f6544d74d866400a096762c9a7b351e82463cecf9e649802085523e683ab31517183f9b5552629f09f8e71e072e67ef3744aa1aec2498c4ec2da619ed59697ddfe661bed9f6fd4d5dd9b0f75349b7f1dcb1e6f49af6d327d25b810a00461f03fa230e92b4c1580c0068748e7abcdc8da2830dc98652ad2d9b091fe12be3e32e98a8251680648695b67861654cb0f203db81ea660a5b6fda153323eaa9042a6e0ae24f1c308b199e59eda68c5d92f399570f2b3ab290290c77301239ec0a3c2f6ac6eca74a7e0f4513d4a5a3a894ff7d6af925e87511865d933121517a5c93c7761dc100fd1888253fb00d05ee2cd43185778ba965b460b08d9a1caced877015d4e525548b94b255f4f204035a16325bb5ea9971624b787feef6de5507c49076bc3b3c1cea383d7d1965803c86fdc451b6be58cca63d8fddaf7847cbf0305468aa73284e4ac6a2196043b589d90a305113826e603a51e4df6bb5156cb44f80ae7016d2ea4132c17e2e8011bba519bce0c50cf5885e012fbad09c33b641c1ed0dbedb4feb85654bc37faa8469602fed9f359c60541d384317ccf56eec544b8fec0aef30428e57f8c51c91e02f012547e7c308f0fa74f5bb366454b3d4f6de513045e9ab43d0490acede07d4395538cc1b62060c9d1b700263dd75160e9f4ee7c5ac5b7c224ad026814a37bbefcd0f6cef95936f224938742499c4627cc7f9fffe33710cedea05fd26bdfb2a55ae8c236ea8493faece38bcc243639049273f123ae6c9bd57e26ab949a811392c21100826e2144e2f6ce534f882f835c937a257f212801e4fd9b3f0e96dd1833d10ccfa97f4edca59ed770d51af75eabd1f54aa34d61b4c57d7927b7dd7c887c4c4b56e4422f7fa4f0a4d32dc010428f13fdc362cd27c8639cd069803e6dbcb9d4352e1ee83709af1e16229a3d29c5ef9a14582487f18953037fa7a9889e89fe0001a147010701fd05b103947f7d4afc6052752add3a000801a91cd0e549ee67032e57a9ec1f9dc5a56900000000000000000000000001312d008dd8363e26d6261500aef9ffffc7ba6c9813dee204016705e919c2190e3af6ea00061b100003b235d27dc5b6f95f4cb0e70b18a4e764f56775a3890ecf14608849bec6adcc0b8cc6fa4be71d75f142b08f234b763eb1742318b774bda14139adcb742cc93558616cb1a8c2c767ba7b4323597fe0c2592c5b348cdbe19a092d106a89cabce9d26550ff258310b2cecfdbcda14612d0f8093b9494e69f870dac9f8016331d972eaf74926c3e7c8d2ae521f80f8fecc797db69fd5f425374c3a2b30e3b7390f9da19d422acb34660499d39dfed5749d171836d603efb1706de01c5c46f5f958795bda1f5183ed65f4fc1420879dce3d34877016a6d7ef6349cfd65d00667dd42f080f4f402b22a67b03e22b5dc565c7f88ff183892b0d74ec93356c9d43d931a3d8ad8ceb51232d047e2f00b3da0bc574cd154f6520df8ed3ff5899a7e3123f96f9bf881dfe428c0f0ffa738366f24e86da6821986481bf6e7eed7673841104e0bf7054343f6f0f24b7f4d5911bcea6a80c3d7459762faba36c4406d47f640b8359a76b5a8e8d89231ac58423266c679c2443becfefb5f5ee34836fdb33d62af8de9285d5d94fcf4de5f6d1fff6c2ff302e8b671e3dd7323e33f0f3bab549675913e48806a197514fef03f50f2d045a0e1f8fbb201b6ce7962e04343ed9d47054d1818d8952dc824f5a2d8fbbda9f9a517a6a04157d4dc8d0dd7032d5b5c25d70dd2028de78d49960f6e6f0f2e8da6c6d67b2d710cdf859c09b3f697c1a2f5a2f7f3e2b1895519595aac1d731b952cbef2416ab6cb77e02cb4129e12e912f3c6ada2f92f7e391435af0457b94458398dac9e582fa6f7fa5a9fbc9740d90b7ced528bc8cd2462316b9e71b1c7a98f7b9078c4b65cb672a431833a0c0c3aa0afcfda57879bf863d1cf57ede3a42a24c34b5b66dc6fa3d619bcab246419a4d46acb78473d4874d300f25ab2f17f4d850c2ce5c3e0393d4a18c3db1829ef970ebe3eb2426e171e1424608b519cf7f679c3b16a64fe0145d0fd252ce2001ef9ef73d2a9d9dcfec249728638f4423277093c63ff495116dd68e1f654f2af7aadd8194a883ad187a505aab826e11d3a82781a08e2a4e9e99351c91ddbb89466e27c5ece1e94badb73feee0647734ce2415b87b0d404e17bd6fcbb2116ccddf145ee3b7685995e4077fa5c4327416b06e5472628ed4021fefff8e94faee07201cdc1d7625850214dfa7545943aa0e4c993dca249d9fd2b2ea40ab6a2cb31b6bdac8c4a509fc8afde140c0acc1c02bc32ccc35b3f7f177e924a56f1c50a01e9ac304dbc4ba2a6959c377c884fa0478be833edf57042e1414b513d896cbdf17d1b4cbcd10a3ab5afb485913c997d6d56062d4cc06ac85517a15bd5f8e20c8c56c8afee5b1ca0deaaf0e7d3e5088016c45f6fe85bfa05f4b80164788f4006a64db38c3c704d6350768db66e8a0542cd3e8d4701388281d3647fd8d2ffb708d3c522e2930b2157630b688bcd5bd502bdeb2a02a1614891f4f3fa4e7029dd5f77f01c07c7d109e6263815c5c466f28b1321e3119d17bc889b5be56eb8cfc2f4aa3dc521dd52a35a4d1134d0db055d8d67bcd4f31f2e707eeac14e24d14c927ddf1fe0f2c4c9f014f7d1e140a8eee31a8cf2d40ed4c337c2c71b2a5065e9f602d13c94c76a2d23bfe7ef00bfdbf5c9f85bc8e5608cfb51f6612b820cc80cf3a6e8c19c0178a42a603f8541735f8a27e750b93826d32802a0922e45c950f82ce93a4cd1f68a2c95a81a87a010a82a1c73881451a5d33c41a80a2d0fed9b4ef7d96bbbf89b0132bc5ecd1b7944e1c5e30007bacbd536171c85e45f6b34adcfc24a2e70c7e74fb2402482075944c5b0363790cd9c556e1cb887866b687066fe702c7d0f0e27ea1be4d15ad2091db448c9dbfe61721e29eea71045f61612cf70d74aedf37b216e0b986579785555344d10f9698dab44c65bccd8fc12e568fe0001a147010701fd05b103947f7d4afc6052752add3a000801a91cd0e549ee67032e57a9ec1f9dc5a56900000000000000010000000000e4e1c01c9c1acc94053ef1636062d0cc12e3f4e1eb937fd886e197725ce04f55e8bd3700061b100002375f1762cee2e6ecfd3697d122455453586903b0e39b10ad58449ba5d92b0f383f73e86607ec4d55356838286c86f12cda4c1b05e824f1480a221a3d347681e67fe8013833770b4f2df269d7680b4019e50097d1797cd3c34dc57ef0ea686e7671b30ac4e7a84333ac030cb2d102cb82902091bde495e5c9836b22a3f27a1c07ba3d96afd068bf461576011133b3236dbf0f750fb3e3604c2470158ef445532293f9f3c087b240905ad675ef1dab400cb81a287e59a4b78bc166e1fff8e66145ca0e32d3f652444f9db1b9005f8f837d97fe7d07663359e0be47fcd3b9e53f3ea02767ca0df2b17dee4a6a34c9c1cd9bce8d23b1db7ff7e4fb950517e5d46c6acccb114334cd3df6a10df5a35af0200fd210090f6b82fc45e4c0cdcf77a16178bd04a4cc64d439f7affff041557daf220158ca62bbac28842496b798224ac1317be3912fb2513c612fae8f4e23beae40c369c5be7933f55f9a68300376a14a0ef47894c665d13a1eef8572cd1f7c2f23d742613d0fbe33dff57c45ea1cbfb2d50aa8367af5b4fdd21d1ac7357d6603b35eaded95d2b146fbdecf9fb9d4838f4971a5abba8de7360517ad954a27e60116974fc5990533d577ddbcbe487e5b7edb03dcef24090b44cdc5f42978324c73ec5a0e4f78f0c8f1ec9b61914908aeb8b4bd2ba540c5b4dbd6d138c49f0ff0236f2a10742401afc1d8b5849d8c16af98025a5212ee7fa5197a7e72587c5e46317640fa62170ab1d61b51e1023f809fb7619719e1620ccae2f0d959249c5adf6513ca848db0c143b624ebce1fa82451b86658a4b71ada6acc6e1770e7953bca2b872a9d6df29a6a8f55b64cddb7ecf18683ae09a6790dd111b0a4b13b658db1e261d36d2edba2925e01ad9906be66d98482d6de76261f0740aa69d28d580df42597988c509723e0d03bb450c8d64f6d5117da84bd324153db870ac9ac551fe0a684e0ede3eef879832a0371b3029eef474b9d11c9e760ee1384a0d5c6096f4d3f906c270a8ba95e3472fc9b20805a0dc4d1bf611bdfc493e80f840087e7e6bd47fff393a2d1448b79594c5febe8a7ae4ea293ee746fa8fec79c3184a61466cbf2c2b283d1afcaf206831f86f2a90e1ec00f8dc5232e428fcfc6a1ccd5369d1e26741a9142163a2b964b8405c264b896d92cdf58f11447d849e1450b06837b3ad7f57e1e2423e36c325ec29fbd6b28648c1d8cf107095af49645b0be7fb6d7b4ff7e4c8d6ff756367f98b127eb66389a1ec3f172e274ab04382cbbda8f1b4a5d0c376d535e7a62c8bb45debe600fe567ff7dd8518d9f68fb90a0e29c1e9d6619de23aca8564fe3ea804673e871788fa3f31109475e32df67f66ca94b03c3b783ed34a2b3c88f47fbff3aea7472b4b52ce4fa46c21bd75e7c4c31175355ad77b6018f86fc073b00d48ed3b59bf96bdace68a039f42fd2fad1186e2225fc4d8926daf53bc1f37883de0bba564d389be3abfc9c044db80741a8dfcee768141e6a8a66573082fbf7484f7c726c8bfdaf56c3bbad227c44aa34333366fb850f620c9f8e2a48bca1648994af38f6c7977f5e6e56bd8948d2f7e5aa4f1a1bfd04fa0736bca8d12f52b3c9c87743c63e3d4bdad59c224837ba9432223edf5f1bf42f62347df4bc1fc25006e6ff985307476b02762597238bd80627ccf8b760a6183cae949ea589bb4fc1f52c57627d7ee467364dfb6f21962d5d6eb12d17734894a341e987bb55bc7dcd8bbf6f73dd603def09dc40ad8dfbf8bacfb1f8609e8e89ae18637b555682481523b045268256f5eda707fa087c04e75618eaabada8044e982e715d088573032b1c61e4764d6bf5e6e58dcab10ac00e9988d9de2ffc6afaac94ca3a72fb15a048f78e7c7767435892ac3771c2a334ab3c68d72dcca1b7891a8563343278959a26fe0001a14701070002000000010000000000000002248346fa527bb258661702039084591a18946d57b0aebe228304e0b0f2f3ab926c0000000000000000001cfde003310a9389dfedb76570658d85f68e29ca418f8942e48c25e06c5ed3595d3435ab020002ff0000000000000000240662bad2f83c6a7a46d10267b06896364643664c458d23fe45a0df12cf9f6083020000002200205b9ea9211c6fc1167c620e0582ce115f22490f8f316e585802628b29bb57d107fffffffd000000002de544800000000027a318400000000003dfd24000000000000000042251204d112974f2088c84041b48d57ec6226d02a337893ab2d3fef65e4d3541d0e71c0000000045bcc8800000000027a318400000000003dfd24000010100000000000000025e02000000010a6f8248fda4042d10658d723f0d2c0334d600b7d29d56d24c7a161bd6056dba0100000000000000000180841e000000000022512036ae43b00b6c906d4d3cc9532eb10d021322a1c6fc26b9942fe17e74ab2128120000000000000000000000000000000101000000000000000600000000001842f022512036ae43b00b6c906d4d3cc9532eb10d021322a1c6fc26b9942fe17e74ab212812000000061a80ca03947f7d4afc6052752add3a000801a91cd0e549ee67032e57a9ec1f9dc5a5698346fa527bb258661702039084591a18946d57b0aebe228304e0b0f2f3ab926c0001004201405fce19bbdeccc77c5a2b5d960253a2990a105ea75e6df6d874b1fdbe49e2a839b94377623ea69501d9683a72a5561b9b122c1b7fc6b7372251b0c2bc8fa253cafd025940a94b8803857f1bd969267312409e3bbbf2cb2b20f285cd32453d780c8db4c7d255b20d1f4f6278d5ff40a8a4532958b092821a670ef62ad320a7dc7deabcb8fc8603947f7d4afc6052752add3a000801a91cd0e549ee67032e57a9ec1f9dc5a5698346fa527bb258661702039084591a18946d57b0aebe228304e0b0f2f3ab926c0000fd0259408f03c7ff7af90e5243e0b2e78de336015f27f5123510d0eef48457e80fe4af3e5f2b5cb0223f425aed297dfa6825369f0a195917a88cb2133cfd3e418eabc001ff000400483045022100a94b8803857f1bd969267312409e3bbbf2cb2b20f285cd32453d780c8db4c7d2022055b20d1f4f6278d5ff40a8a4532958b092821a670ef62ad320a7dc7deabcb8fc014830450221008f03c7ff7af90e5243e0b2e78de336015f27f5123510d0eef48457e80fe4af3e02205f2b5cb0223f425aed297dfa6825369f0a195917a88cb2133cfd3e418eabc00101475221020fbda26e3e9105d441fb40fa1b27475484eb061132402835590cacb7cc1b073d21039e41e0025176c0a197982a635cc7d5c90d0c5a0e3ef662bcf73fb3260ca4e02952ae00061a8003947f7d4afc6052752add3a000801a91cd0e549ee67032e57a9ec1f9dc5a569ff0000000000061a800000000000000000ff0001240662bad2f83c6a7a46d10267b06896364643664c458d23fe45a0df12cf9f6083020000002b60e31600000000002200205b9ea9211c6fc1167c620e0582ce115f22490f8f316e585802628b29bb57d10700000000039e41e0025176c0a197982a635cc7d5c90d0c5a0e3ef662bcf73fb3260ca4e0290203310a9389dfedb76570658d85f68e29ca418f8942e48c25e06c5ed3595d3435ab00000300061a80000000000000044c000027100000000103000000000000044c0000000000000000000000001dcd6500006402d000000000000000010004020000000000000000020000000000000001010000000000000000010000000000000001000009c40000000045bcc8800000000027a3184069220aea10ba4836314adcd4371eb8b877a2e7f5dfd58c75c167fdecefc66a6202ea256ab27e52a7a678eb5dcfddcacb5cf2a51392d5ac40eb1041943ce216b16d02e01c2043e085619539e50fe106aab30ce393683c7ffeaa463f10c5ea5831320a0215a04ac7bcb14f457acb138809092ff8840468d9bd378559cecc502e5264dd030004851d7dc9fb8029058be73b74cba375131cfaed787a03eb4473b51951837a9b4267739e298e6905e1fc8809a179b5d20f7142c49edae5441ce74db44cc3fae3f2edd4dbbb99ebc435c6040ae84511c129df4ccefd4dadc51c718a90a8654d1b0d6a09956f9f1358bffdd4def002ce95d7637d97a0179dfcbb099b6761e6d9d984dc18bfc179905406ef55a4e10e399a2807fef78b054972f77b7b0c42b5d4febf01ca32550bb1f82fe2622b5cffc188ea460d4b49608b93fac389099d31ae8fed8a0c32156fe17248f6cd77fe705426c5ead8900e490a6db285fef0a8a043f6ac62c1212af7a5baaddf4d6dc0655d85787f09252d37c861bfaa64b86bd429d28400000000000003e800000000000003e8000000003b9aca00001e009000000000000000020004010000000000000000010000000000000001020000000000000000020000000000000001000009c40000000027a318400000000045bcc8805380f10dafe447571255ad55280b0a8d0d3b9628417d3d606eccc72f2bba1e5703243355a76851b43d8e72bd236f72dfb5f488f0dd28f4925fd8672a082c873c2800000000000000000000000000240662bad2f83c6a7a46d10267b06896364643664c458d23fe45a0df12cf9f608302000000000000000016e360039e41e0025176c0a197982a635cc7d5c90d0c5a0e3ef662bcf73fb3260ca4e02904000224ff7fcbbf49d681ef8f4f7c67abeee6a68e126e63f0cae0c8eb0c9c29e0717b580000000024bc8e2330ced6b078f9275befc69a9d1590cd2f3b99a4d61c577b9a08b85e2d88000000002b60e31600000000002200205b9ea9211c6fc1167c620e0582ce115f22490f8f316e585802628b29bb57d107061a8000002a0002ff8603947f7d4afc6052752add3a000801a91cd0e549ee67032e57a9ec1f9dc5a5690662bad2f83c6a7a46d10267b06896364643664c458d23fe45a0df12cf9f6083000100420140e38b7ae573ed0bf0978d18ae0a442bf9bf80cae61294830719aafe07b5f932d9fa6c2632762c87c58cf9eb8834a19c0f2bea5e42c1851c7d9324551d7e56b535000202000000000000044c0000000000000000000000001dcd6500006402d000000000000000010004020000000000000000020000000000000001010000000000000000010000000000000001000009c4000000002de544800000000027a318400d6e0f1494288354cbee9f060785ab924b2ed80a4cf6ee3f99c2cf8e9060c4c501d3b6898476f3aee65e9c622f70e4c1ee208cbc8a734a93c4d9b1e329940d47a7107b959113671d7a3d40ab15bb93414ae8f8570c11c4c9a26786baab9703f6cb0004b1a03d82e35539fcf2a55f0682a47600c550db10b962e901733f1f3ee8546cd87e7d541d5dbec51edda8c4430e219db922898d4eb4dcf6914f23126cede8ef04e1fe2e08e75e3edae655636dc68565db2c91f2448cfb9928320660d4bb9f21d938a2211d8ae004f832ebc1ea4b9db0c9ec12c9212ca80f6d0aa66b6814cb13ec89b8f276f096d1f5ec8358b3ba41d17bf73b2e9b4d2bfcbf580896cdd0c51bb1532d5866016b1e23981d5844a02796de69b0bfa44a047506a819905c5f4d3cc59dc2a4790b26381cc255a8d0d538b5c56d0a0bc237b1b9bbfe106cd23c72a3372ece493e55995f0181ff400ee9b516f2106258be7809e8b5fb5d87a11fee878a00000000000003e800000000000003e8000000003b9aca00001e009000000000000000020004010000000000000000010000000000000001020000000000000000020000000000000001000009c40000000027a31840000000002de544800160e22ca71ed9a7a881feea81dae3b37b5427b25cd6562d8d26efef67fd40f703243355a76851b43d8e72bd236f72dfb5f488f0dd28f4925fd8672a082c873c28000000ff038de4dbf9360ed2528932b97f4f3526965ab5ce08506477f88722111b36c68f810001003f0000fffffffffffe004120fe586f0273e21df186cfc84c935fcc2aa567280da00e3ede3a7f84f116cb730000fffffffffffe0002000000000000000000010c7098c58b604fe7bf03d5f195443c5600000000000000010001aa6ad10faf8e47aeaf98188ebcfdd56f0000010304c812155fa803ff0214858924a8ac3f0088bdc9eeee5f7d65891b987c18554028d98152887bc764849716d7b78df924e19542912aa4b5f49aa5c5cc0b94592ad6729c36a5d71c86495a1a22966bbd3d2e5f06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f0304c812155fa80368ba93bb0300009000000000000003e8000858b800000014000000001dcd650001000000 \ No newline at end of file diff --git a/eclair-core/src/test/resources/nonreg/codecs/050006-DATA_NORMAL/splice-commitment-upgrade/data.json b/eclair-core/src/test/resources/nonreg/codecs/050006-DATA_NORMAL/splice-commitment-upgrade/data.json new file mode 100644 index 0000000000..7abd5d5000 --- /dev/null +++ b/eclair-core/src/test/resources/nonreg/codecs/050006-DATA_NORMAL/splice-commitment-upgrade/data.json @@ -0,0 +1,319 @@ +{ + "type" : "DATA_NORMAL", + "commitments" : { + "channelParams" : { + "channelId" : "03947f7d4afc6052752add3a000801a91cd0e549ee67032e57a9ec1f9dc5a569", + "channelConfig" : [ "funding_pubkey_based_channel_keypath" ], + "channelFeatures" : [ "option_dual_fund" ], + "localParams" : { + "nodeId" : "02aaaa00ce2f18a967dc4f25f414e671ba6585f8ef0b8c5fb812c21064f55a2eaa", + "fundingKeyPath" : [ 3799953226, 3598578002, 2983226804, 3875515148, 3526940557, 1164158034, 3880604768, 1915213591, 2147483649 ], + "isChannelOpener" : true, + "paysCommitTxFees" : true, + "initFeatures" : { + "activated" : { + "option_route_blinding" : "optional", + "option_provide_storage" : "optional", + "option_dual_fund" : "optional", + "splice_prototype" : "optional", + "payment_secret" : "mandatory", + "gossip_queries_ex" : "optional", + "option_anchor_outputs" : "optional", + "option_quiesce" : "optional", + "option_data_loss_protect" : "optional", + "var_onion_optin" : "mandatory", + "option_static_remotekey" : "optional", + "option_support_large_channel" : "optional", + "option_anchors_zero_fee_htlc_tx" : "optional", + "option_channel_type" : "mandatory", + "basic_mpp" : "optional", + "gossip_queries" : "optional" + }, + "unknown" : [ 50001 ] + } + }, + "remoteParams" : { + "nodeId" : "02bbbb671d15145722fb8c28d732cddb249bcc6652ed2b297ff1f77a18371b1e63", + "revocationBasepoint" : "02c586d173691ef2ab086741471c5aa0279775e1f0538d305e222b735af69610bf", + "paymentBasepoint" : "0337df270640e947e898084b1859603d62a2ce8ed7a30b1cce4f53647554d4cd12", + "delayedPaymentBasepoint" : "03ebe3dfdc25a1ddc5fc98abe3db5a0e18c70cf9b2a47c0b596bbbc81f8e8d933c", + "htlcBasepoint" : "03e66ffa803a927d55a99347fc86f2e23434b218bba6c9775c25b74b2c2adf7c7c", + "initFeatures" : { + "activated" : { + "option_route_blinding" : "optional", + "option_dual_fund" : "optional", + "splice_prototype" : "optional", + "payment_secret" : "mandatory", + "gossip_queries_ex" : "optional", + "option_anchor_outputs" : "optional", + "option_quiesce" : "optional", + "option_data_loss_protect" : "optional", + "var_onion_optin" : "mandatory", + "option_static_remotekey" : "optional", + "option_support_large_channel" : "optional", + "option_anchors_zero_fee_htlc_tx" : "optional", + "option_channel_type" : "mandatory", + "basic_mpp" : "optional", + "gossip_queries" : "optional" + }, + "unknown" : [ ] + } + }, + "channelFlags" : { + "nonInitiatorPaysCommitFees" : false, + "announceChannel" : false + } + }, + "changes" : { + "localChanges" : { + "proposed" : [ ], + "signed" : [ ], + "acked" : [ ] + }, + "remoteChanges" : { + "proposed" : [ ], + "acked" : [ ], + "signed" : [ ] + }, + "localNextHtlcId" : 2, + "remoteNextHtlcId" : 2 + }, + "active" : [ { + "fundingTxIndex" : 1, + "fundingInput" : "6c92abf3f2b0e0048322beaeb0576d94181a5984900302176658b27b52fa4683:0", + "fundingAmount" : 1900000, + "localFunding" : { + "status" : "unconfirmed", + "txid" : "6c92abf3f2b0e0048322beaeb0576d94181a5984900302176658b27b52fa4683" + }, + "remoteFunding" : { + "status" : "not-locked" + }, + "commitmentFormat" : "simple_taproot_phoenix", + "localCommitParams" : { + "dustLimit" : 1100, + "htlcMinimum" : 0, + "maxHtlcValueInFlight" : 500000000, + "maxAcceptedHtlcs" : 100, + "toSelfDelay" : 720 + }, + "localCommit" : { + "index" : 1, + "spec" : { + "htlcs" : [ { + "direction" : "OUT", + "id" : 0, + "amountMsat" : 15000000, + "paymentHash" : "66267cd731c4a7ead922867566db3b7b22db847e4f8d2d3c07b9f7c442853993", + "cltvExpiry" : 400144 + }, { + "direction" : "OUT", + "id" : 1, + "amountMsat" : 15000000, + "paymentHash" : "3cccb3fea4565e9d90f7cb9067a2444b804459d2149d302379dc63cb3cea7fea", + "cltvExpiry" : 400144 + }, { + "direction" : "IN", + "id" : 0, + "amountMsat" : 20000000, + "paymentHash" : "8dd8363e26d6261500aef9ffffc7ba6c9813dee204016705e919c2190e3af6ea", + "cltvExpiry" : 400144 + }, { + "direction" : "IN", + "id" : 1, + "amountMsat" : 15000000, + "paymentHash" : "1c9c1acc94053ef1636062d0cc12e3f4e1eb937fd886e197725ce04f55e8bd37", + "cltvExpiry" : 400144 + } ], + "commitTxFeerate" : 2500, + "toLocal" : 1170000000, + "toRemote" : 665000000 + }, + "txId" : "69220aea10ba4836314adcd4371eb8b877a2e7f5dfd58c75c167fdecefc66a62", + "remoteSig" : { + "partialSig" : "ea256ab27e52a7a678eb5dcfddcacb5cf2a51392d5ac40eb1041943ce216b16d", + "nonce" : "02e01c2043e085619539e50fe106aab30ce393683c7ffeaa463f10c5ea5831320a0215a04ac7bcb14f457acb138809092ff8840468d9bd378559cecc502e5264dd03" + }, + "htlcRemoteSigs" : [ "851d7dc9fb8029058be73b74cba375131cfaed787a03eb4473b51951837a9b4267739e298e6905e1fc8809a179b5d20f7142c49edae5441ce74db44cc3fae3f2", "edd4dbbb99ebc435c6040ae84511c129df4ccefd4dadc51c718a90a8654d1b0d6a09956f9f1358bffdd4def002ce95d7637d97a0179dfcbb099b6761e6d9d984", "dc18bfc179905406ef55a4e10e399a2807fef78b054972f77b7b0c42b5d4febf01ca32550bb1f82fe2622b5cffc188ea460d4b49608b93fac389099d31ae8fed", "8a0c32156fe17248f6cd77fe705426c5ead8900e490a6db285fef0a8a043f6ac62c1212af7a5baaddf4d6dc0655d85787f09252d37c861bfaa64b86bd429d284" ] + }, + "remoteCommitParams" : { + "dustLimit" : 1000, + "htlcMinimum" : 1000, + "maxHtlcValueInFlight" : 1000000000, + "maxAcceptedHtlcs" : 30, + "toSelfDelay" : 144 + }, + "remoteCommit" : { + "index" : 2, + "spec" : { + "htlcs" : [ { + "direction" : "IN", + "id" : 0, + "amountMsat" : 15000000, + "paymentHash" : "66267cd731c4a7ead922867566db3b7b22db847e4f8d2d3c07b9f7c442853993", + "cltvExpiry" : 400144 + }, { + "direction" : "IN", + "id" : 1, + "amountMsat" : 15000000, + "paymentHash" : "3cccb3fea4565e9d90f7cb9067a2444b804459d2149d302379dc63cb3cea7fea", + "cltvExpiry" : 400144 + }, { + "direction" : "OUT", + "id" : 0, + "amountMsat" : 20000000, + "paymentHash" : "8dd8363e26d6261500aef9ffffc7ba6c9813dee204016705e919c2190e3af6ea", + "cltvExpiry" : 400144 + }, { + "direction" : "OUT", + "id" : 1, + "amountMsat" : 15000000, + "paymentHash" : "1c9c1acc94053ef1636062d0cc12e3f4e1eb937fd886e197725ce04f55e8bd37", + "cltvExpiry" : 400144 + } ], + "commitTxFeerate" : 2500, + "toLocal" : 665000000, + "toRemote" : 1170000000 + }, + "txId" : "5380f10dafe447571255ad55280b0a8d0d3b9628417d3d606eccc72f2bba1e57", + "remotePerCommitmentPoint" : "03243355a76851b43d8e72bd236f72dfb5f488f0dd28f4925fd8672a082c873c28" + } + }, { + "fundingTxIndex" : 0, + "fundingInput" : "83609fcf12dfa045fe238d454c664346369668b06702d1467a6a3cf8d2ba6206:2", + "fundingAmount" : 1500000, + "localFunding" : { + "status" : "confirmed", + "shortChannelId" : "400000x42x2" + }, + "remoteFunding" : { + "status" : "locked" + }, + "commitmentFormat" : "anchor_outputs", + "localCommitParams" : { + "dustLimit" : 1100, + "htlcMinimum" : 0, + "maxHtlcValueInFlight" : 500000000, + "maxAcceptedHtlcs" : 100, + "toSelfDelay" : 720 + }, + "localCommit" : { + "index" : 1, + "spec" : { + "htlcs" : [ { + "direction" : "OUT", + "id" : 0, + "amountMsat" : 15000000, + "paymentHash" : "66267cd731c4a7ead922867566db3b7b22db847e4f8d2d3c07b9f7c442853993", + "cltvExpiry" : 400144 + }, { + "direction" : "OUT", + "id" : 1, + "amountMsat" : 15000000, + "paymentHash" : "3cccb3fea4565e9d90f7cb9067a2444b804459d2149d302379dc63cb3cea7fea", + "cltvExpiry" : 400144 + }, { + "direction" : "IN", + "id" : 0, + "amountMsat" : 20000000, + "paymentHash" : "8dd8363e26d6261500aef9ffffc7ba6c9813dee204016705e919c2190e3af6ea", + "cltvExpiry" : 400144 + }, { + "direction" : "IN", + "id" : 1, + "amountMsat" : 15000000, + "paymentHash" : "1c9c1acc94053ef1636062d0cc12e3f4e1eb937fd886e197725ce04f55e8bd37", + "cltvExpiry" : 400144 + } ], + "commitTxFeerate" : 2500, + "toLocal" : 770000000, + "toRemote" : 665000000 + }, + "txId" : "0d6e0f1494288354cbee9f060785ab924b2ed80a4cf6ee3f99c2cf8e9060c4c5", + "remoteSig" : { + "sig" : "d3b6898476f3aee65e9c622f70e4c1ee208cbc8a734a93c4d9b1e329940d47a7107b959113671d7a3d40ab15bb93414ae8f8570c11c4c9a26786baab9703f6cb" + }, + "htlcRemoteSigs" : [ "b1a03d82e35539fcf2a55f0682a47600c550db10b962e901733f1f3ee8546cd87e7d541d5dbec51edda8c4430e219db922898d4eb4dcf6914f23126cede8ef04", "e1fe2e08e75e3edae655636dc68565db2c91f2448cfb9928320660d4bb9f21d938a2211d8ae004f832ebc1ea4b9db0c9ec12c9212ca80f6d0aa66b6814cb13ec", "89b8f276f096d1f5ec8358b3ba41d17bf73b2e9b4d2bfcbf580896cdd0c51bb1532d5866016b1e23981d5844a02796de69b0bfa44a047506a819905c5f4d3cc5", "9dc2a4790b26381cc255a8d0d538b5c56d0a0bc237b1b9bbfe106cd23c72a3372ece493e55995f0181ff400ee9b516f2106258be7809e8b5fb5d87a11fee878a" ] + }, + "remoteCommitParams" : { + "dustLimit" : 1000, + "htlcMinimum" : 1000, + "maxHtlcValueInFlight" : 1000000000, + "maxAcceptedHtlcs" : 30, + "toSelfDelay" : 144 + }, + "remoteCommit" : { + "index" : 2, + "spec" : { + "htlcs" : [ { + "direction" : "IN", + "id" : 0, + "amountMsat" : 15000000, + "paymentHash" : "66267cd731c4a7ead922867566db3b7b22db847e4f8d2d3c07b9f7c442853993", + "cltvExpiry" : 400144 + }, { + "direction" : "IN", + "id" : 1, + "amountMsat" : 15000000, + "paymentHash" : "3cccb3fea4565e9d90f7cb9067a2444b804459d2149d302379dc63cb3cea7fea", + "cltvExpiry" : 400144 + }, { + "direction" : "OUT", + "id" : 0, + "amountMsat" : 20000000, + "paymentHash" : "8dd8363e26d6261500aef9ffffc7ba6c9813dee204016705e919c2190e3af6ea", + "cltvExpiry" : 400144 + }, { + "direction" : "OUT", + "id" : 1, + "amountMsat" : 15000000, + "paymentHash" : "1c9c1acc94053ef1636062d0cc12e3f4e1eb937fd886e197725ce04f55e8bd37", + "cltvExpiry" : 400144 + } ], + "commitTxFeerate" : 2500, + "toLocal" : 665000000, + "toRemote" : 770000000 + }, + "txId" : "0160e22ca71ed9a7a881feea81dae3b37b5427b25cd6562d8d26efef67fd40f7", + "remotePerCommitmentPoint" : "03243355a76851b43d8e72bd236f72dfb5f488f0dd28f4925fd8672a082c873c28" + } + } ], + "inactive" : [ ], + "remoteNextCommitInfo" : "038de4dbf9360ed2528932b97f4f3526965ab5ce08506477f88722111b36c68f81", + "remotePerCommitmentSecrets" : null, + "originChannels" : { + "0" : { + "paymentId" : "0c7098c5-8b60-4fe7-bf03-d5f195443c56" + }, + "1" : { + "paymentId" : "aa6ad10f-af8e-47ae-af98-188ebcfdd56f" + } + } + }, + "aliases" : { + "localAlias" : "0x304c812155fa803", + "remoteAlias" : "0x214858924a8ac3f" + }, + "channelUpdate" : { + "signature" : "bdc9eeee5f7d65891b987c18554028d98152887bc764849716d7b78df924e19542912aa4b5f49aa5c5cc0b94592ad6729c36a5d71c86495a1a22966bbd3d2e5f", + "chainHash" : "06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f", + "shortChannelId" : "197832x1185119x43011", + "timestamp" : { + "iso" : "2025-09-05T07:39:39Z", + "unix" : 1757057979 + }, + "messageFlags" : { + "dontForward" : true + }, + "channelFlags" : { + "isEnabled" : true, + "isNode1" : true + }, + "cltvExpiryDelta" : 144, + "htlcMinimumMsat" : 1000, + "feeBaseMsat" : 547000, + "feeProportionalMillionths" : 20, + "htlcMaximumMsat" : 500000000, + "tlvStream" : { } + } +} \ No newline at end of file diff --git a/eclair-core/src/test/resources/nonreg/codecs/050007-DATA_SHUTDOWN/anchor-outputs/data.bin b/eclair-core/src/test/resources/nonreg/codecs/050007-DATA_SHUTDOWN/anchor-outputs/data.bin new file mode 100644 index 0000000000..089c9245b1 --- /dev/null +++ b/eclair-core/src/test/resources/nonreg/codecs/050007-DATA_SHUTDOWN/anchor-outputs/data.bin @@ -0,0 +1 @@ +050007012ef2c0b2095e3e9131dc501a0c98e5bdd2cf2ac43561fea5a268475e5f37db0001010002aaaa00ce2f18a967dc4f25f414e671ba6585f8ef0b8c5fb812c21064f55a2eaa0009b24bcf77abf6881a270dfb46ae2bc02b3e37d09a687bf42a4665090edc3491d880000001ff0000000000002710c000000000186b0200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000180802aa698202bbbb671d15145722fb8c28d732cddb249bcc6652ed2b297ff1f77a18371b1e63ff0000000000004e2003f0285c07046e02a67017e9094e04c5958c4894c59de7fb525e8848b06c33b71f02b12174865e2776d03ea17440cc0793b6e18401b87f9d675d3fc06a4a1c57a6b202ecd1e11cd7fc82a559567b19448e8023e5ae6c8960fdd7cecbb1ea5bc7d39fc503995345f0570ae8563976e70810bece1297a3c46db73675416d646482af784bbd000000140800000000000000000000000000100802aa6982000000000000000000000000000000000000000000020000000000000000000102fd05b12ef2c0b2095e3e9131dc501a0c98e5bdd2cf2ac43561fea5a268475e5f37db0000000000000000000000000011e1a300c829f6cc2a9b97272924872878a38c89436ac184bd890cdb3c03cb1b1030bb1900061b100003b7a341904a7325229f40e7e730700cad5dec73fdbf9ac439f5fccab34f0cf92e358a4203d0671599024681ca68360773643762f685dfdf5acbd1d386ef767219f51353e472bb9e6de2f0cf9ae73d323c13b489cdcf3d3dfacf43990bed4a6dc9e4260ab126270d556ff9dc93d347311a92632e8e3cc36139a13a2698ca802b4f1dc9cbda8bcfd76ed51ab556540b1afb86f75b963837d3e021ed2f4d83caee2bf160b89df97a0494f7c5b4b90ff5d0d69ea2bb152876a187e6a790ad0a4a03510b2dd8ba0a17aa8200661c9001c4f7fd3d1b959f367184ea37634725b343d6f474ea0f326abde8f1bab94da9fca54e2af3769197a9866862507ec79bc1dc0ec7a368a02fe2d1cf514692267854cc5d17aa8ad07a9ecd2d0a9e9f6d50646f1940ad444332105a170e1324b077010b3bf38a8ca7ae461cbf766e7af6c6f53a884eed01cc6fcad3c36962049eaa29f6c7a7324b084aabe64e32b812ffccbf7fdf3d9d6c4b6db1ee68e22efb94b6b29b1987b3a13e294c05cc437469d5898e45e8281fe3797212bf7e4c432abec13f9d20d801dd4d6aa02b24fcac38bf2448f4b4f3ff53a050b9562bfdb9a8d4d674328187bb94ffa383c4c36dab29f59ef26d1f4f1d7769bfddc0a54aef286b6432c7a91956571b440307b3db986e1b8c8f57acfc227a149122c63b4cfb15a1cabc88e089261e77808ba6e57f04789c5b76ecfd3bf48bf8175c08fe504c0ead6d85b077629127a31293e5caed20ba3db44750f5ba459efa2c71af0fa536b21de0ef5aae1bbc869687193c9796b4a86dc47d06c04411e71a10bac27f53889f0b287673fc85f5b1f7a842a8e7d82e77926a1b979dc12143a9729f5c9cddda49e56cc9767328e212326f4218485f633e27531e26dcbeb49459935de3a95d5f90cc8342d0774ae0003a43f2263b326a38d2bda2a467a4c58c2059078e501f766460732ead1296668ab8504f94f856150ec620241d270f3a1f73ceb2d565f88a6b1b1bc7c0439258a1d80dc39fd27333134d4590330af99679c8dc2f19a662b6daa035cff8da4aa318847551ef6b3b33149b150ac35d7fd151f44c970bd99b1bdef4c4266f5b1ea4dcf486757306ca256afb23808959619771261f1e934b69bff9a13ee2796ca7628a4b864cb765401b3b6d3355516208cff9f401cd1f9974055d5278e07411e1c4a0feebbef7e87be9f2c28cbd2d9e4d019c0cf34bdfb0cb71bb19255ee3b57153b747a5953d5a83613b843b550f9e8e4f6f9689c80c41e1ba5313373b4893897cbf37d1435061e9e6855c2aaa579e19b4088083ef78119a03a74e0857a0c8279d6ea86ec1ebb7fdb41a1afa436c9fb91226322902965fc6a79c54b21c1493f0bb9f692511dd90d101449f637b3e605dd425d08167dfa8aecd35d6945e74857e30d64fdcc3767586d2792faef4b1b15dfd6f683c490ad640fd32b955edf862b6f1d1c09fb531b142b08b72f355f3d2971e9667c3322d21e791225453c797c66e4b0710461a314bb20e098b6b64076bb508ebb70c12116f1db5168c946090a83735a4367ab7d0917a1647122c1ac47c8f18368a7ede37ade9531cd69df43504a327281bae1826cef8e960e2d3651d32d21f594b66ac022a1dcdd28027da271256bb1e43a630f1f0315b4728b449c4468035c063b9923fecc2bff863f080ffe3d9c49dd73a2b10498509beed2f38d6cbaeaf7974627e6a28d383d79acd380b0d9ed6fd12d6709294586067ee68fd67a071665d3c9e5be13478c67cad78683ded8d3ec8fe7db736e8c50bc4a3762839782886f007db05afa46aa6c480a81f8c54d6444d053849af8199e3a502bf77649d72e03662d29bac6b448ea4a91deb627d8936df7dd06f204063b67ac03d04fb6dcc8add741fe16675e2ce187f267e911c6b2f42f3f8fe0001a14701070001000000000000000000000000242ef2c0b2095e3e9131dc501a0c98e5bdd2cf2ac43561fea5a268475e5f37db000000000000000000000f424002ed9f70ce496242e1c79388e2a176adc7f98f8a1e0377a5f38c7cea688e414c8e0400012401010101010101010101010101010101010101010101010101010101010101012a0000002b40420f00000000002200203f1e1ebc1c015ee48121698baa1c9ff86a5dae6754c1778d8c940e4858e07fb3061a8000002a000000000202000000000000044c0000000000000000000000001dcd6500006402d000000000000000020001020000000000000000000009c40000000011e1a3000000000017d7840067d9164bf0ed90c5e76ed1786334530f9a89b57dd8645d61a9d88bf328e9ac8b0151db1af176a59269ea456d589fed9ea79c0cabddad5902f877dbf4f82c30562a60748f3b2f5fd86db13a8c3f61d92c6960ebecb610211209978c5496676729850001392ca4a5200406a05b9e90815d84fab4082e273620be56a2af5447b8687df1d976aae1ad300d88c5017ccc9ec7fa135a4396762f94879641471568d8eda054a700000000000003e800000000000003e8000000003b9aca00001e009000000000000000020001010000000000000000000009c40000000017d784000000000011e1a3009737dfe5d0c76ba8da73730db86b30b7c749cec84c20c8c82b1f983a6875bfdc02d6c263c2da69cd9c05468823f3016634104e05cb02380fbc959a3d44e52a7a55000000ff02828d2753c7955e13a3b1109c215e8920e34ac565bf4aa61c974e1c480ed450560001003f0000fffffffffffe0041a4772678ca4b2a3539b2c521e3ac1dbe572cee66236d20d420b7dd17e96c11b10000fffffffffffe0001000000000000000000013f0e212ced22456cbbfbd4c61e30a81800382ef2c0b2095e3e9131dc501a0c98e5bdd2cf2ac43561fea5a268475e5f37db0000160014761879f7b274ce995f87150a02e75cc0c037e8e3382ef2c0b2095e3e9131dc501a0c98e5bdd2cf2ac43561fea5a268475e5f37db0000160014761879f7b274ce995f87150a02e75cc0c037e8e30100 \ No newline at end of file diff --git a/eclair-core/src/test/resources/nonreg/codecs/050007-DATA_SHUTDOWN/anchor-outputs/data.json b/eclair-core/src/test/resources/nonreg/codecs/050007-DATA_SHUTDOWN/anchor-outputs/data.json new file mode 100644 index 0000000000..607511d322 --- /dev/null +++ b/eclair-core/src/test/resources/nonreg/codecs/050007-DATA_SHUTDOWN/anchor-outputs/data.json @@ -0,0 +1,165 @@ +{ + "type" : "DATA_SHUTDOWN", + "commitments" : { + "channelParams" : { + "channelId" : "2ef2c0b2095e3e9131dc501a0c98e5bdd2cf2ac43561fea5a268475e5f37db00", + "channelConfig" : [ "funding_pubkey_based_channel_keypath" ], + "channelFeatures" : [ ], + "localParams" : { + "nodeId" : "02aaaa00ce2f18a967dc4f25f414e671ba6585f8ef0b8c5fb812c21064f55a2eaa", + "fundingKeyPath" : [ 2991312759, 2885060634, 655227718, 2922102827, 1043845274, 1752953898, 1181026574, 3694432728, 2147483649 ], + "initialRequestedChannelReserve_opt" : 10000, + "isChannelOpener" : true, + "paysCommitTxFees" : true, + "initFeatures" : { + "activated" : { + "option_route_blinding" : "optional", + "option_provide_storage" : "optional", + "splice_prototype" : "optional", + "payment_secret" : "mandatory", + "gossip_queries_ex" : "optional", + "option_anchor_outputs" : "optional", + "option_quiesce" : "optional", + "option_data_loss_protect" : "optional", + "var_onion_optin" : "mandatory", + "option_static_remotekey" : "optional", + "option_support_large_channel" : "optional", + "option_anchors_zero_fee_htlc_tx" : "optional", + "option_channel_type" : "mandatory", + "basic_mpp" : "optional", + "gossip_queries" : "optional" + }, + "unknown" : [ 50001 ] + } + }, + "remoteParams" : { + "nodeId" : "02bbbb671d15145722fb8c28d732cddb249bcc6652ed2b297ff1f77a18371b1e63", + "initialRequestedChannelReserve_opt" : 20000, + "revocationBasepoint" : "03f0285c07046e02a67017e9094e04c5958c4894c59de7fb525e8848b06c33b71f", + "paymentBasepoint" : "02b12174865e2776d03ea17440cc0793b6e18401b87f9d675d3fc06a4a1c57a6b2", + "delayedPaymentBasepoint" : "02ecd1e11cd7fc82a559567b19448e8023e5ae6c8960fdd7cecbb1ea5bc7d39fc5", + "htlcBasepoint" : "03995345f0570ae8563976e70810bece1297a3c46db73675416d646482af784bbd", + "initFeatures" : { + "activated" : { + "option_route_blinding" : "optional", + "splice_prototype" : "optional", + "payment_secret" : "mandatory", + "gossip_queries_ex" : "optional", + "option_anchor_outputs" : "optional", + "option_quiesce" : "optional", + "option_data_loss_protect" : "optional", + "var_onion_optin" : "mandatory", + "option_static_remotekey" : "optional", + "option_support_large_channel" : "optional", + "option_anchors_zero_fee_htlc_tx" : "optional", + "option_channel_type" : "mandatory", + "basic_mpp" : "optional", + "gossip_queries" : "optional" + }, + "unknown" : [ ] + } + }, + "channelFlags" : { + "nonInitiatorPaysCommitFees" : false, + "announceChannel" : false + } + }, + "changes" : { + "localChanges" : { + "proposed" : [ ], + "signed" : [ ], + "acked" : [ ] + }, + "remoteChanges" : { + "proposed" : [ ], + "acked" : [ ], + "signed" : [ ] + }, + "localNextHtlcId" : 2, + "remoteNextHtlcId" : 0 + }, + "active" : [ { + "fundingTxIndex" : 0, + "fundingInput" : "00db375f5e4768a2a5fe6135c42acfd2bde5980c1a50dc31913e5e09b2c0f22e:0", + "fundingAmount" : 1000000, + "localFunding" : { + "status" : "confirmed", + "shortChannelId" : "400000x42x0" + }, + "remoteFunding" : { + "status" : "locked" + }, + "commitmentFormat" : "anchor_outputs", + "localCommitParams" : { + "dustLimit" : 1100, + "htlcMinimum" : 0, + "maxHtlcValueInFlight" : 500000000, + "maxAcceptedHtlcs" : 100, + "toSelfDelay" : 720 + }, + "localCommit" : { + "index" : 2, + "spec" : { + "htlcs" : [ { + "direction" : "OUT", + "id" : 0, + "amountMsat" : 300000000, + "paymentHash" : "c829f6cc2a9b97272924872878a38c89436ac184bd890cdb3c03cb1b1030bb19", + "cltvExpiry" : 400144 + } ], + "commitTxFeerate" : 2500, + "toLocal" : 300000000, + "toRemote" : 400000000 + }, + "txId" : "67d9164bf0ed90c5e76ed1786334530f9a89b57dd8645d61a9d88bf328e9ac8b", + "remoteSig" : { + "sig" : "51db1af176a59269ea456d589fed9ea79c0cabddad5902f877dbf4f82c30562a60748f3b2f5fd86db13a8c3f61d92c6960ebecb610211209978c549667672985" + }, + "htlcRemoteSigs" : [ "392ca4a5200406a05b9e90815d84fab4082e273620be56a2af5447b8687df1d976aae1ad300d88c5017ccc9ec7fa135a4396762f94879641471568d8eda054a7" ] + }, + "remoteCommitParams" : { + "dustLimit" : 1000, + "htlcMinimum" : 1000, + "maxHtlcValueInFlight" : 1000000000, + "maxAcceptedHtlcs" : 30, + "toSelfDelay" : 144 + }, + "remoteCommit" : { + "index" : 2, + "spec" : { + "htlcs" : [ { + "direction" : "IN", + "id" : 0, + "amountMsat" : 300000000, + "paymentHash" : "c829f6cc2a9b97272924872878a38c89436ac184bd890cdb3c03cb1b1030bb19", + "cltvExpiry" : 400144 + } ], + "commitTxFeerate" : 2500, + "toLocal" : 400000000, + "toRemote" : 300000000 + }, + "txId" : "9737dfe5d0c76ba8da73730db86b30b7c749cec84c20c8c82b1f983a6875bfdc", + "remotePerCommitmentPoint" : "02d6c263c2da69cd9c05468823f3016634104e05cb02380fbc959a3d44e52a7a55" + } + } ], + "inactive" : [ ], + "remoteNextCommitInfo" : "02828d2753c7955e13a3b1109c215e8920e34ac565bf4aa61c974e1c480ed45056", + "remotePerCommitmentSecrets" : null, + "originChannels" : { + "0" : { + "paymentId" : "3f0e212c-ed22-456c-bbfb-d4c61e30a818" + } + } + }, + "localShutdown" : { + "channelId" : "2ef2c0b2095e3e9131dc501a0c98e5bdd2cf2ac43561fea5a268475e5f37db00", + "scriptPubKey" : "0014761879f7b274ce995f87150a02e75cc0c037e8e3", + "tlvStream" : { } + }, + "remoteShutdown" : { + "channelId" : "2ef2c0b2095e3e9131dc501a0c98e5bdd2cf2ac43561fea5a268475e5f37db00", + "scriptPubKey" : "0014761879f7b274ce995f87150a02e75cc0c037e8e3", + "tlvStream" : { } + }, + "closeStatus" : { } +} \ No newline at end of file diff --git a/eclair-core/src/test/resources/nonreg/codecs/050007-DATA_SHUTDOWN/taproot/data.bin b/eclair-core/src/test/resources/nonreg/codecs/050007-DATA_SHUTDOWN/taproot/data.bin new file mode 100644 index 0000000000..030adef58d --- /dev/null +++ b/eclair-core/src/test/resources/nonreg/codecs/050007-DATA_SHUTDOWN/taproot/data.bin @@ -0,0 +1 @@ +05000701e8ae5d9e18da0563038a7d089d5211edfb508060cc4bff99bf1e4f381d039c4e01010002bbbb671d15145722fb8c28d732cddb249bcc6652ed2b297ff1f77a18371b1e63000964df7b8b39259cf64398c860385cd178118246424f67c0af08829fa8312d5c0080000000ff0000000000004e200000000000004720000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000020001008028a598202aaaa00ce2f18a967dc4f25f414e671ba6585f8ef0b8c5fb812c21064f55a2eaaff00000000000027100331fb0c0375e15a02366a6d3d48f6d61a2c424af7ac5eab1372a17a4881097c00027fb3280f1e548f4d805dfdb3f6de2a5e5cf0c96a3b6441baf28a0ef101a847ec028fe2f6350ead9d1dd1f8bd86abf8be43e5592d4ab3883083c336ab1d28d2825a02a3346f122555685c5b2267182c40bb8b774f502af0c877e230d7e898957cfb240000186b02000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000020001808020a59820000000000014a0082e8ae5d9e18da0563038a7d089d5211edfb508060cc4bff99bf1e4f381d039c4e0000000000000000a0c60915dfca3bb24a8a221b0427ff592686d6967d5c4413215864d20649471c000000000000000000000000000000000000000000000002000201fd05b1e8ae5d9e18da0563038a7d089d5211edfb508060cc4bff99bf1e4f381d039c4e00000000000000000000000011e1a300200c93d62ce9375b8862f962a0d1631db53eddc988600e27f388883567a28ea100061b1000037b782a293eb0fbe9171477377591820197d0cb5217f852c30c54adfac191b79f14a214e27d5f9d9fb54d1153d6a36890e18ea3451c2b6e63395779da6a0b9f5d40aa9626b0ebbb8d9990bd95302339ed559939b5dea91a6e89ccbf224f80790e524be9ecf02d1b70ebd5ae229fd17aaa860b9358d4cc6ea130848488bc9801cff3018cef276a9b69db548e87fc9264104bddcb25c32c8fd1f8202e6d9a943a2e5b62ab9a095859d805a2f043a33ee64429c8b57bf987794e31ce1e9aa7c45c627f091c38f7c1d969dffb89a894827f032fb246191e21ee5f55b1cbbd9b91639b6a74df689af33dc5759ae1470f6ba2da89280a6381e4508b4693bda5275e3e8d6d71d3479938fed3a5d4ffc0e343b32599f8a0ef04c88a1a748c167e811d2019c3c538dacfbe196cc8ad071d56de8838150247e72b8661470e294d6f4d064cc50cdfb0b999eec3aaf4c98431d5d9844ad976375c8602184db65e74888c0f643e51387ec51f318c269c9332b4b5780a5de6182a6f5ab7e7237d43ffc9e2bdd23b4068bacacc1d1953f55a4cb24677fa3951ee08f03fd7013aaf4a39f91d58f8db87655700149bfe7a425808f1428ecfd8595258be278e79243d478cacd5a3a6a9dc79aa41c8ef251f362e57aacdf1f72e04382629d78bbac303f3380053d274ce43f04312781c3691de3f0c55e75e3adf89ba3b77b5d5ab5aea5db1a3ff0bccf64e585ee33d146f4984ca33b85155e0d93848310f63d76e7d386bb8bf2f0089c6457028956560dd25d8d475725d840847da4e52cfeee196d30eb343a9c3a1aac2b96feb07d73c93c430f9eca9bbf6e0a380384f5630229eb0f459ccd06dd329c58aee85462e369e4add13d4e6162ccee27f3581c53f9b63cd40a9d3161277b91c4b8a25692b8a72bf384c89d65a61ade714d35b18f50f30033da1274442890b724d9ecb0fe553d56a982688987e0aef186991631acdc31b4392dd65706f69fbca7dcf514f73129764821d1c382089db812f20a749591ba90355ae75728a909f9c4ebf06fb989a0d64dbc0f774ae0112659d196159f83f9a2819967fb734c53f6a63f06c81482a707a9b09616ccf9de48ee9404656da3b0f6d07de4fe790da61e69f57b75ea6131ea3ed14212d3a769e38f731bf824851b26db60eeb14efa8bd164bcd5a22e5baabe104b2507a4ab3e901810083959c6d8bb6f2b0ae52c912ff4c9298f3b307a3430562d3c456ccbdf1450e6c9dc9a58d2ecdc9b19828ad501c6af0f7f90321e3fd5f5c5b40c649f0cec8cc29962014576d4dd7f5df07c0176c0a9382b90cc73bc5a0f42a3b68c03fffdeaad6fa47c9d53b2ca4fd39b479a57d169e42f08fac20e1349d37e3eed153c8103b3d875e3fdc94e113fac61a2d91207b01aef94d2c6fdd81254f66da75f3110d5c211d7cd42bce7c5834326f20734a7d8bfa44bc7476e449a2867c01998fb2dfab5182dce77268cf1d87f536b74d29b11679b46fbda03ef5733d2d20a152e9c3ce7d70250c99c5b62303ef39d39cd1921f961154a7f94a2fd80a6a481454f0b310100d8ba4c054ca5c6d4d0439d28a3b750bcaa134319ae05e5528fd16500df598fe3098f95522e38299a09bb09b4c913191a68133ac6e3a05ad670bef4803470c5735b0c1340a9a3cb369f09708e016cf563248cf3bd740fcdd8b5f0033d0e4e37f038133fd6e41d5bc0922f5e5bbe1f1ce811fc75344cf4371cfbb95cc43d2eaafe845c78cb094b4057e64dabc590e424e130f5bb60363b79f18a30345b4657710f92323a6f803f5992b8b10b4067234a5fcadd211b3b606383f99c9b8983a70af93e6f0034773cb3d963d22a290e18961a7b6e2de9a5194c240b16d3630138891a243c3552c8a6e6f205fd1e29bbff0f6b5ab4a6167d086919810d82bca4a39936058fe0001a147010701fd05b1e8ae5d9e18da0563038a7d089d5211edfb508060cc4bff99bf1e4f381d039c4e0000000000000001000000000bebc200ebcadc0c561cc439e1c20a0b8ac15ac7c185c93027cf37bf684beb521553d22e00061b1000030ee465e3e4b86d3325e5a429b7d8c5e13d59e2aab7f078be8093097793d232ef5754a0a4621591d490bb6e2ce2fadf5b9c5e709b569fafc720414ead9be70181cb9cacb5ad707cff0114dc72ab8b1d7c6c90600125d5167ad27c87a233a3967f744e52ea1a6d189902c1c547274ea511a12791f6b08993dadf1f37221d7f8a94dad0a0c366e58e199aea4a1ad8df21dbd715cf00ee48f9a96147a00f362e151639e1ffb0193b1e90e2e686a6ec1b438b6315c8a08c2c1be2cfc717c437aa6121f76b03ef041fc3842ef4401384f336cc5594d2a236d7ac0b4b5ec7816e961a4e14f1d71862a3b1542bebb434007535927ab27ab576acd48f3a2ae77af0f0cae3c76693ea15bdecf150ba83f94bae6a498e2906b2e735e25b726b9ffc999e736c8039b3b4f587873b6c33d987531f3f16f8e9d088e081228a191cc92702d3a3483ffef5fe00718354db3c0648790815692a4cb28215d29934de140ec381bc311a9d65f54056f9b84705fc3196cbabc603fc83bb13591fbd1cc6299422350ae3774506e1ae8dbbc1da7f544313648f1f719ec87734a5fc34451951762df3e21c26fd38e29c413d842d6abe012749e2993c22922782d96364b82f5b6bf3ffd32e10f6a705c35d2a90ccc01eea080464ed1aad44912efc16b09ef56b0436e171bae808c4f10c6b58ec6152db0ef016578d98fb4dba4bbd27b27f0e55cb0a6e9a31084e4bc961af48456ec62e4375f0d771e0832a1e2ddc52754215e1ad69583112a6798c14faaf9a10994d55e703b7bfa08fa8fa0a0f0f123652397ba17dcca426d14b4f6a13748633e736a1870cb07da371d62f8f4d036b8ae2a274f7979f55eb642a63b566688b9aabc08c8b23d01a8275c36cf2d86ef8a9132b6cce7c2909b99a9c325f00d169f70d889ea160e6d9a506e6cd4667944ab8d2c871b51bc78ffc638c82c3411cc7f3b2d06d24f3e8312ec68855b9cdb3dc7a282f4037a869846b6199432fe5892af5e6f3671ace742a3a8de20161e1017e46b3b658f61ecee0d783ee0f9510fe82f3f559211b635ad588b719a95c0f1aa33bb3addc59757cca1045883a14616420df0571daa27c160a1bb9c318d528f1b1aa1468e4380febbbecb689b0a28b49fa5a71188c9fde9faf187f7f2fec61565c997c9622da865a6d9d4a1c85ecc28137e45121f2e3a62f1121fe684a14f43fa3fcbc3e0ea806dee859e4a7b679acdee62f6f04cb571449d60d93b7ea1197b46194e0af4f96f65045ae71519c3227d0a40647f51c10b141929a65012ae16529c87056154547ed9c077ea566da82a4fb0a99f7311a1f41d508be68b6cede75d9822f670e501832ae1e3c6521f4935c0969e9a14e8546a85ef8db9b9b5bd31e06ca817f7f86d09462483a97954a8534b842fa00334ceb2d55d6e93acd1bcbcce9ff34058a297c346f83f3e51faec0bb7d7fc134629fd140433d8aa3c47c5374c1e4eedcf331830babb883d7ea0b4013cf6166921c4ae6fdd26bc8ed76bd76c0a0d4b52cebd3b7fbd969e872a015539e569fd3a7e92123d33d728cdf9fbe87778e1f53621034309f7f3b4446c3ba1e26517964f74874ee701966c142ad23e1b3ad43817279a47d37f43ed9dc6195fb9a1eede793d5b95661cec52456f4c676be07f72ed3c13c798a37472a9c1376511c7d336cc056251a744b0fa9b92a0997c3ec5765a70be3d926306d18eaf3e9d0879e0f12812aeacab9bb8bad6fc1e8f2a9753c03dcd62a0a70481b9cb9a23cec05a28ac7242a2cfd075cf5842a7a277f1c48bb8c88da8a395620e256f00f18d6cd9eb92bdf164d84dd528686f20abe64789deec9f17dba0b56783d44f9f1aa3afb7e823bc87d46c1e18ee252c1d66bf42dc25dc57b0a6b8b0c51e9f6354d3173c5b663e02f2b9207651f7a00a6af2cfb64fe0001a1470107000100000000000000000000000024e8ae5d9e18da0563038a7d089d5211edfb508060cc4bff99bf1e4f381d039c4e0000000000000000000f42400356311b1bdc04673995a91bf6bbd7dd20b9dbb3b29acb099ed3273192951e3b050400012401010101010101010101010101010101010101010101010101010101010101012a0000002b40420f00000000002251200e982ce4d7465299d820e22fdb27ca1b8cd87289240f92e87a0fa72b2a570264061a8000002a00000000020300000000000003e800000000000003e8000000003b9aca00001e009000000000000000010002010000000000000000010000000000000001000009c4000000000bebc2000000000011e1a30030bbcb2766a592860e836c3dc093e78e5392fc351e840d2d5cd3bc14eba09258027b152de4f72844830b125d897287d257bd8081f8c3c22abf46eecf3c0eb23ff8025eb602b798b41d3f0e2f71e2b4c43c00a755faa89d736b2c7848f88f2157527703f4021a5b09f0578cba827eb12202600b30772f4b1be0d7b1392c2e587c781ec000022367cd8ac316305d019b9016171926d7ae8cd43af23cabd99420549d9482d9e9adcb2c5addf01f8eee196abcc441cc428f72321958ca4611da877e947f288f80bc5dcd63f30ecaab75dd327a6977d90c163a55dffdb73b60c4973699e42eea70d4abfa3f9505c76fa2d0fa94224d61f32ae88c8a40a50189718acee7d28118dd000000000000044c0000000000000000000000001dcd6500006402d000000000000000010002020000000000000000020000000000000001000009c40000000011e1a300000000000bebc200d136df4c62a78cec95967023c33fc0534d61482f2457c8a434bede9ef75b211d03d8267557b3d73846072ee5f6e5acb9c27f25088a695e62b60c9e117c6d222021fffd0106e8ae5d9e18da0563038a7d089d5211edfb508060cc4bff99bf1e4f381d039c4e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000190945de524f760fa2c4858113f0675a6fc8f012137988c3d17ea49acd981a6cab4724142ba77ccb9196b8deaf41e6d035cd81738b82d5970589c9589c1246574026257932c46cb9d9d828ccba0e498a7e361c4ea81879f0a000195beec311ade951403f79275ca695a99ac0cc7ada98aefab44ff9ee7cfc866924337eb3c3872d994cb02684e45f678e70cd9802cc94cc771414e50ee44c49a59a02862cc37e2925131f000000000000000020001020000000000000001000009c40000000011e1a300000000001dcd650007c8a5cb379ee61d9312c300db594f543b848bb8e9ec3d027de615e8f2de62e2020360035e021651b01c4471a014d3b7cdc5c86e2ed915f915817fa466093973060000000000000000000001000100400000ffffffffffff0020014543ec6c7709ea81199fc320cc5bd5bf288b08fd38f31502f7b8fd5b9a197c80007fffffffffff800000007ce8ae5d9e18da0563038a7d089d5211edfb508060cc4bff99bf1e4f381d039c4e00160014761879f7b274ce995f87150a02e75cc0c037e8e3084203bc0cb276be08751de656b8a712bdd8635e037316f1820dd30877dbbb159263820288218519982ce2eae32123f5b2a868b50bdae61bcd918d0a096bff9de31cb6ab7ce8ae5d9e18da0563038a7d089d5211edfb508060cc4bff99bf1e4f381d039c4e00160014761879f7b274ce995f87150a02e75cc0c037e8e3084202a8e6a615c3782f5cd2db2e6188888f9525e13df215f1693e62cde511633cd6980239caecb127656cdba3e080f73e4c2e9734efd4a05a82ed2e4203fc76265a6c630200 \ No newline at end of file diff --git a/eclair-core/src/test/resources/nonreg/codecs/050007-DATA_SHUTDOWN/taproot/data.json b/eclair-core/src/test/resources/nonreg/codecs/050007-DATA_SHUTDOWN/taproot/data.json new file mode 100644 index 0000000000..0bd1a05109 --- /dev/null +++ b/eclair-core/src/test/resources/nonreg/codecs/050007-DATA_SHUTDOWN/taproot/data.json @@ -0,0 +1,207 @@ +{ + "type" : "DATA_SHUTDOWN", + "commitments" : { + "channelParams" : { + "channelId" : "e8ae5d9e18da0563038a7d089d5211edfb508060cc4bff99bf1e4f381d039c4e", + "channelConfig" : [ "funding_pubkey_based_channel_keypath" ], + "channelFeatures" : [ ], + "localParams" : { + "nodeId" : "02bbbb671d15145722fb8c28d732cddb249bcc6652ed2b297ff1f77a18371b1e63", + "fundingKeyPath" : [ 1692367755, 958766326, 1134086240, 945607032, 293750338, 1332199599, 142778280, 825056256, 2147483648 ], + "initialRequestedChannelReserve_opt" : 20000, + "isChannelOpener" : false, + "paysCommitTxFees" : false, + "initFeatures" : { + "activated" : { + "option_simple_close" : "optional", + "option_simple_taproot_phoenix" : "optional", + "option_route_blinding" : "optional", + "splice_prototype" : "optional", + "payment_secret" : "mandatory", + "gossip_queries_ex" : "optional", + "option_quiesce" : "optional", + "option_data_loss_protect" : "optional", + "var_onion_optin" : "mandatory", + "option_static_remotekey" : "mandatory", + "option_support_large_channel" : "optional", + "option_anchors_zero_fee_htlc_tx" : "optional", + "option_channel_type" : "mandatory", + "basic_mpp" : "optional", + "gossip_queries" : "optional" + }, + "unknown" : [ ] + } + }, + "remoteParams" : { + "nodeId" : "02aaaa00ce2f18a967dc4f25f414e671ba6585f8ef0b8c5fb812c21064f55a2eaa", + "initialRequestedChannelReserve_opt" : 10000, + "revocationBasepoint" : "0331fb0c0375e15a02366a6d3d48f6d61a2c424af7ac5eab1372a17a4881097c00", + "paymentBasepoint" : "027fb3280f1e548f4d805dfdb3f6de2a5e5cf0c96a3b6441baf28a0ef101a847ec", + "delayedPaymentBasepoint" : "028fe2f6350ead9d1dd1f8bd86abf8be43e5592d4ab3883083c336ab1d28d2825a", + "htlcBasepoint" : "02a3346f122555685c5b2267182c40bb8b774f502af0c877e230d7e898957cfb24", + "initFeatures" : { + "activated" : { + "option_simple_close" : "optional", + "option_support_large_channel" : "optional", + "option_simple_taproot_phoenix" : "optional", + "option_route_blinding" : "optional", + "option_provide_storage" : "optional", + "splice_prototype" : "optional", + "payment_secret" : "mandatory", + "gossip_queries_ex" : "optional", + "option_quiesce" : "optional", + "option_data_loss_protect" : "optional", + "var_onion_optin" : "mandatory", + "option_static_remotekey" : "mandatory", + "option_channel_type" : "mandatory", + "basic_mpp" : "optional", + "gossip_queries" : "optional" + }, + "unknown" : [ 50001 ] + } + }, + "channelFlags" : { + "nonInitiatorPaysCommitFees" : false, + "announceChannel" : false + } + }, + "changes" : { + "localChanges" : { + "proposed" : [ ], + "signed" : [ { + "channelId" : "e8ae5d9e18da0563038a7d089d5211edfb508060cc4bff99bf1e4f381d039c4e", + "id" : 0, + "paymentPreimage" : "a0c60915dfca3bb24a8a221b0427ff592686d6967d5c4413215864d20649471c", + "tlvStream" : { } + } ], + "acked" : [ ] + }, + "remoteChanges" : { + "proposed" : [ ], + "acked" : [ ], + "signed" : [ ] + }, + "localNextHtlcId" : 0, + "remoteNextHtlcId" : 2 + }, + "active" : [ { + "fundingTxIndex" : 0, + "fundingInput" : "4e9c031d384f1ebf99ff4bcc608050fbed11529d087d8a036305da189e5daee8:0", + "fundingAmount" : 1000000, + "localFunding" : { + "status" : "confirmed", + "shortChannelId" : "400000x42x0" + }, + "remoteFunding" : { + "status" : "locked" + }, + "commitmentFormat" : "simple_taproot_phoenix", + "localCommitParams" : { + "dustLimit" : 1000, + "htlcMinimum" : 1000, + "maxHtlcValueInFlight" : 1000000000, + "maxAcceptedHtlcs" : 30, + "toSelfDelay" : 144 + }, + "localCommit" : { + "index" : 1, + "spec" : { + "htlcs" : [ { + "direction" : "IN", + "id" : 0, + "amountMsat" : 300000000, + "paymentHash" : "200c93d62ce9375b8862f962a0d1631db53eddc988600e27f388883567a28ea1", + "cltvExpiry" : 400144 + }, { + "direction" : "IN", + "id" : 1, + "amountMsat" : 200000000, + "paymentHash" : "ebcadc0c561cc439e1c20a0b8ac15ac7c185c93027cf37bf684beb521553d22e", + "cltvExpiry" : 400144 + } ], + "commitTxFeerate" : 2500, + "toLocal" : 200000000, + "toRemote" : 300000000 + }, + "txId" : "30bbcb2766a592860e836c3dc093e78e5392fc351e840d2d5cd3bc14eba09258", + "remoteSig" : { + "partialSig" : "7b152de4f72844830b125d897287d257bd8081f8c3c22abf46eecf3c0eb23ff8", + "nonce" : "025eb602b798b41d3f0e2f71e2b4c43c00a755faa89d736b2c7848f88f2157527703f4021a5b09f0578cba827eb12202600b30772f4b1be0d7b1392c2e587c781ec0" + }, + "htlcRemoteSigs" : [ "2367cd8ac316305d019b9016171926d7ae8cd43af23cabd99420549d9482d9e9adcb2c5addf01f8eee196abcc441cc428f72321958ca4611da877e947f288f80", "bc5dcd63f30ecaab75dd327a6977d90c163a55dffdb73b60c4973699e42eea70d4abfa3f9505c76fa2d0fa94224d61f32ae88c8a40a50189718acee7d28118dd" ] + }, + "remoteCommitParams" : { + "dustLimit" : 1100, + "htlcMinimum" : 0, + "maxHtlcValueInFlight" : 500000000, + "maxAcceptedHtlcs" : 100, + "toSelfDelay" : 720 + }, + "remoteCommit" : { + "index" : 1, + "spec" : { + "htlcs" : [ { + "direction" : "OUT", + "id" : 0, + "amountMsat" : 300000000, + "paymentHash" : "200c93d62ce9375b8862f962a0d1631db53eddc988600e27f388883567a28ea1", + "cltvExpiry" : 400144 + }, { + "direction" : "OUT", + "id" : 1, + "amountMsat" : 200000000, + "paymentHash" : "ebcadc0c561cc439e1c20a0b8ac15ac7c185c93027cf37bf684beb521553d22e", + "cltvExpiry" : 400144 + } ], + "commitTxFeerate" : 2500, + "toLocal" : 300000000, + "toRemote" : 200000000 + }, + "txId" : "d136df4c62a78cec95967023c33fc0534d61482f2457c8a434bede9ef75b211d", + "remotePerCommitmentPoint" : "03d8267557b3d73846072ee5f6e5acb9c27f25088a695e62b60c9e117c6d222021" + }, + "nextRemoteCommit" : { + "index" : 2, + "spec" : { + "htlcs" : [ { + "direction" : "OUT", + "id" : 1, + "amountMsat" : 200000000, + "paymentHash" : "ebcadc0c561cc439e1c20a0b8ac15ac7c185c93027cf37bf684beb521553d22e", + "cltvExpiry" : 400144 + } ], + "commitTxFeerate" : 2500, + "toLocal" : 300000000, + "toRemote" : 500000000 + }, + "txId" : "07c8a5cb379ee61d9312c300db594f543b848bb8e9ec3d027de615e8f2de62e2", + "remotePerCommitmentPoint" : "020360035e021651b01c4471a014d3b7cdc5c86e2ed915f915817fa46609397306" + } + } ], + "inactive" : [ ], + "remoteNextCommitInfo" : { + "sentAfterLocalCommitIndex" : 1 + }, + "remotePerCommitmentSecrets" : null, + "originChannels" : { } + }, + "localShutdown" : { + "channelId" : "e8ae5d9e18da0563038a7d089d5211edfb508060cc4bff99bf1e4f381d039c4e", + "scriptPubKey" : "0014761879f7b274ce995f87150a02e75cc0c037e8e3", + "tlvStream" : { + "ShutdownNonce" : { + "nonce" : "03bc0cb276be08751de656b8a712bdd8635e037316f1820dd30877dbbb159263820288218519982ce2eae32123f5b2a868b50bdae61bcd918d0a096bff9de31cb6ab" + } + } + }, + "remoteShutdown" : { + "channelId" : "e8ae5d9e18da0563038a7d089d5211edfb508060cc4bff99bf1e4f381d039c4e", + "scriptPubKey" : "0014761879f7b274ce995f87150a02e75cc0c037e8e3", + "tlvStream" : { + "ShutdownNonce" : { + "nonce" : "02a8e6a615c3782f5cd2db2e6188888f9525e13df215f1693e62cde511633cd6980239caecb127656cdba3e080f73e4c2e9734efd4a05a82ed2e4203fc76265a6c63" + } + } + }, + "closeStatus" : { } +} \ No newline at end of file diff --git a/eclair-core/src/test/resources/nonreg/codecs/050008-DATA_NEGOTIATING/fundee/data.bin b/eclair-core/src/test/resources/nonreg/codecs/050008-DATA_NEGOTIATING/fundee/data.bin new file mode 100644 index 0000000000..11abd460d7 --- /dev/null +++ b/eclair-core/src/test/resources/nonreg/codecs/050008-DATA_NEGOTIATING/fundee/data.bin @@ -0,0 +1 @@ +05000801dca86c28f42d8793ae0e65a6ec747983e252fc9c93ea042f510dc9938bf6b43e01010002bbbb671d15145722fb8c28d732cddb249bcc6652ed2b297ff1f77a18371b1e630009681017da40f48f9b5fcd904ea23a0e16291341ab605e77bbbf3905af11eb17d180000000ff0000000000004e20000000000000140800000000000000000000000000100802aa698202aaaa00ce2f18a967dc4f25f414e671ba6585f8ef0b8c5fb812c21064f55a2eaaff0000000000002710021ad1d4b30a4f9139aff5627f426b8ff04f4d1cf4cfd58781e7174dbf8a76acb003e3711dd21be2f65fad17ec131d75d8eeda649d496d0ebc2f91b26520dcf26982027dd7a5313e51856a13efb6383bd67faa923fe20ad96223c18ea555a8f5d3ba3002f4aaf2516efe365c0a3cf14f898ca593a278ad855745d640751180104fd73feb0000186b0200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000180802aa69820000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000024dca86c28f42d8793ae0e65a6ec747983e252fc9c93ea042f510dc9938bf6b43e0000000000000000000f424003f3841b40e4a5046ed410f8eb1fb79c59bc6ddc968f73eb92f87286c9b9d204470400012401010101010101010101010101010101010101010101010101010101010101012a0000002b40420f0000000000220020c4e7d23983470367c25e869bd7b18432ef51e87e017363d7bc59d22b0280c2cf061a8000002a00000000020200000000000003e800000000000003e8000000003b9aca00001e009000000000000000000000000009c4000000000bebc200000000002faf0800f2789e59bd755935d9b2edb1c8922f6643be2ce8d8639cbb6729d0b2f4716527017b988f15ee147358fc4f9292e356f630610bd0f357870ed25bb3cb1259db834154aa6539cb6e8b2835d9cc0a7553b9e8015c1977c187a5142e958660d20c13420000000000000000044c0000000000000000000000001dcd6500006402d000000000000000000000000009c4000000002faf0800000000000bebc200095b196f66547fd545611cae4c4820b8e6d4508b1d920b0abb0ff7471c3a7a42021f8171d635e4f51bd1928f8858fddbc3b478af680a9face1412ee2fd36452346000000ff028f1bb0bb97608dca87a17a922b1db19029efd8d91ae756ba2107abe3230953eb00000000000038dca86c28f42d8793ae0e65a6ec747983e252fc9c93ea042f510dc9938bf6b43e00160014761879f7b274ce995f87150a02e75cc0c037e8e338dca86c28f42d8793ae0e65a6ec747983e252fc9c93ea042f510dc9938bf6b43e00160014761879f7b274ce995f87150a02e75cc0c037e8e300010003fd014324dca86c28f42d8793ae0e65a6ec747983e252fc9c93ea042f510dc9938bf6b43e000000002b40420f0000000000220020c4e7d23983470367c25e869bd7b18432ef51e87e017363d7bc59d22b0280c2cf710200000001dca86c28f42d8793ae0e65a6ec747983e252fc9c93ea042f510dc9938bf6b43e0000000000ffffffff02400d030000000000160014761879f7b274ce995f87150a02e75cc0c037e8e3dc260c0000000000160014761879f7b274ce995f87150a02e75cc0c037e8e300000000ff000000007adca86c28f42d8793ae0e65a6ec747983e252fc9c93ea042f510dc9938bf6b43e0000000000000e2463e56c7056bd78cb27a28bb76e22fa67a1737012b2175c0dc5992ffe75247c9e167490658a73c4f2be18e99dbf83c6194c691f009eb9ab753f73596e246dc5c301100000000000001a5400000000000034a8fd014324dca86c28f42d8793ae0e65a6ec747983e252fc9c93ea042f510dc9938bf6b43e000000002b40420f0000000000220020c4e7d23983470367c25e869bd7b18432ef51e87e017363d7bc59d22b0280c2cf710200000001dca86c28f42d8793ae0e65a6ec747983e252fc9c93ea042f510dc9938bf6b43e0000000000ffffffff02400d030000000000160014761879f7b274ce995f87150a02e75cc0c037e8e3782c0c0000000000160014761879f7b274ce995f87150a02e75cc0c037e8e300000000ff000000007adca86c28f42d8793ae0e65a6ec747983e252fc9c93ea042f510dc9938bf6b43e00000000000008885cee0b9b066cc86016de90d618da7b7f16349d7994b7232b80fc1cd3bc326b5a007d61da1c6bf8071b6ae1a5a8c29ea1c9b9ee777c59b0dd8268d6b57cb237eb01100000000000001a5400000000000034a8fd014324dca86c28f42d8793ae0e65a6ec747983e252fc9c93ea042f510dc9938bf6b43e000000002b40420f0000000000220020c4e7d23983470367c25e869bd7b18432ef51e87e017363d7bc59d22b0280c2cf710200000001dca86c28f42d8793ae0e65a6ec747983e252fc9c93ea042f510dc9938bf6b43e0000000000ffffffff02400d030000000000160014761879f7b274ce995f87150a02e75cc0c037e8e3c82e0c0000000000160014761879f7b274ce995f87150a02e75cc0c037e8e300000000ff000000007adca86c28f42d8793ae0e65a6ec747983e252fc9c93ea042f510dc9938bf6b43e0000000000000638e1c5909ac828b7fcad839afdcbb08085f529b96ce803db98cfe1242eda97f3b4778731d114e9490cf07e81cb687dc90d9a6ccd3433128af54d87b81586000ad201100000000000001a5400000000000034a8ff24dca86c28f42d8793ae0e65a6ec747983e252fc9c93ea042f510dc9938bf6b43e000000002b40420f0000000000220020c4e7d23983470367c25e869bd7b18432ef51e87e017363d7bc59d22b0280c2cffd014d02000000000101dca86c28f42d8793ae0e65a6ec747983e252fc9c93ea042f510dc9938bf6b43e0000000000ffffffff02400d030000000000160014761879f7b274ce995f87150a02e75cc0c037e8e318310c0000000000160014761879f7b274ce995f87150a02e75cc0c037e8e3040047304402204c28eab56bf1f1477d8d9ac7b02a4b1e582608c2dc1da72f3060b65f95d2510b02204e55ed9444d39a542cdb0e270d3758e935bd122a6fddff297bda2209c5aa38f901473044022046dfba01365cd0e8989980ff558fa0a1ef649262f7d37522ebd143792115fcd10220342d658ec27efa39627c3fb55abce8b850179843f00da872be4a47a359b33f8f014752210376510328f91bf430fc444048aa4f204b7d04b208826b07e76e99c6290879991e2103f3841b40e4a5046ed410f8eb1fb79c59bc6ddc968f73eb92f87286c9b9d2044752ae00000000ff00000000 \ No newline at end of file diff --git a/eclair-core/src/test/resources/nonreg/codecs/050008-DATA_NEGOTIATING/fundee/data.json b/eclair-core/src/test/resources/nonreg/codecs/050008-DATA_NEGOTIATING/fundee/data.json new file mode 100644 index 0000000000..869b350741 --- /dev/null +++ b/eclair-core/src/test/resources/nonreg/codecs/050008-DATA_NEGOTIATING/fundee/data.json @@ -0,0 +1,217 @@ +{ + "type" : "DATA_NEGOTIATING", + "commitments" : { + "channelParams" : { + "channelId" : "dca86c28f42d8793ae0e65a6ec747983e252fc9c93ea042f510dc9938bf6b43e", + "channelConfig" : [ "funding_pubkey_based_channel_keypath" ], + "channelFeatures" : [ ], + "localParams" : { + "nodeId" : "02bbbb671d15145722fb8c28d732cddb249bcc6652ed2b297ff1f77a18371b1e63", + "fundingKeyPath" : [ 1745885146, 1089769371, 1607307342, 2721713686, 689127851, 1616803771, 3208185263, 300619729, 2147483648 ], + "initialRequestedChannelReserve_opt" : 20000, + "isChannelOpener" : false, + "paysCommitTxFees" : false, + "initFeatures" : { + "activated" : { + "option_route_blinding" : "optional", + "splice_prototype" : "optional", + "payment_secret" : "mandatory", + "gossip_queries_ex" : "optional", + "option_anchor_outputs" : "optional", + "option_quiesce" : "optional", + "option_data_loss_protect" : "optional", + "var_onion_optin" : "mandatory", + "option_static_remotekey" : "optional", + "option_support_large_channel" : "optional", + "option_anchors_zero_fee_htlc_tx" : "optional", + "option_channel_type" : "mandatory", + "basic_mpp" : "optional", + "gossip_queries" : "optional" + }, + "unknown" : [ ] + } + }, + "remoteParams" : { + "nodeId" : "02aaaa00ce2f18a967dc4f25f414e671ba6585f8ef0b8c5fb812c21064f55a2eaa", + "initialRequestedChannelReserve_opt" : 10000, + "revocationBasepoint" : "021ad1d4b30a4f9139aff5627f426b8ff04f4d1cf4cfd58781e7174dbf8a76acb0", + "paymentBasepoint" : "03e3711dd21be2f65fad17ec131d75d8eeda649d496d0ebc2f91b26520dcf26982", + "delayedPaymentBasepoint" : "027dd7a5313e51856a13efb6383bd67faa923fe20ad96223c18ea555a8f5d3ba30", + "htlcBasepoint" : "02f4aaf2516efe365c0a3cf14f898ca593a278ad855745d640751180104fd73feb", + "initFeatures" : { + "activated" : { + "option_route_blinding" : "optional", + "option_provide_storage" : "optional", + "splice_prototype" : "optional", + "payment_secret" : "mandatory", + "gossip_queries_ex" : "optional", + "option_anchor_outputs" : "optional", + "option_quiesce" : "optional", + "option_data_loss_protect" : "optional", + "var_onion_optin" : "mandatory", + "option_static_remotekey" : "optional", + "option_support_large_channel" : "optional", + "option_anchors_zero_fee_htlc_tx" : "optional", + "option_channel_type" : "mandatory", + "basic_mpp" : "optional", + "gossip_queries" : "optional" + }, + "unknown" : [ 50001 ] + } + }, + "channelFlags" : { + "nonInitiatorPaysCommitFees" : false, + "announceChannel" : false + } + }, + "changes" : { + "localChanges" : { + "proposed" : [ ], + "signed" : [ ], + "acked" : [ ] + }, + "remoteChanges" : { + "proposed" : [ ], + "acked" : [ ], + "signed" : [ ] + }, + "localNextHtlcId" : 0, + "remoteNextHtlcId" : 0 + }, + "active" : [ { + "fundingTxIndex" : 0, + "fundingInput" : "3eb4f68b93c90d512f04ea939cfc52e2837974eca6650eae93872df4286ca8dc:0", + "fundingAmount" : 1000000, + "localFunding" : { + "status" : "confirmed", + "shortChannelId" : "400000x42x0" + }, + "remoteFunding" : { + "status" : "locked" + }, + "commitmentFormat" : "anchor_outputs", + "localCommitParams" : { + "dustLimit" : 1000, + "htlcMinimum" : 1000, + "maxHtlcValueInFlight" : 1000000000, + "maxAcceptedHtlcs" : 30, + "toSelfDelay" : 144 + }, + "localCommit" : { + "index" : 0, + "spec" : { + "htlcs" : [ ], + "commitTxFeerate" : 2500, + "toLocal" : 200000000, + "toRemote" : 800000000 + }, + "txId" : "f2789e59bd755935d9b2edb1c8922f6643be2ce8d8639cbb6729d0b2f4716527", + "remoteSig" : { + "sig" : "7b988f15ee147358fc4f9292e356f630610bd0f357870ed25bb3cb1259db834154aa6539cb6e8b2835d9cc0a7553b9e8015c1977c187a5142e958660d20c1342" + }, + "htlcRemoteSigs" : [ ] + }, + "remoteCommitParams" : { + "dustLimit" : 1100, + "htlcMinimum" : 0, + "maxHtlcValueInFlight" : 500000000, + "maxAcceptedHtlcs" : 100, + "toSelfDelay" : 720 + }, + "remoteCommit" : { + "index" : 0, + "spec" : { + "htlcs" : [ ], + "commitTxFeerate" : 2500, + "toLocal" : 800000000, + "toRemote" : 200000000 + }, + "txId" : "095b196f66547fd545611cae4c4820b8e6d4508b1d920b0abb0ff7471c3a7a42", + "remotePerCommitmentPoint" : "021f8171d635e4f51bd1928f8858fddbc3b478af680a9face1412ee2fd36452346" + } + } ], + "inactive" : [ ], + "remoteNextCommitInfo" : "028f1bb0bb97608dca87a17a922b1db19029efd8d91ae756ba2107abe3230953eb", + "remotePerCommitmentSecrets" : null, + "originChannels" : { } + }, + "localShutdown" : { + "channelId" : "dca86c28f42d8793ae0e65a6ec747983e252fc9c93ea042f510dc9938bf6b43e", + "scriptPubKey" : "0014761879f7b274ce995f87150a02e75cc0c037e8e3", + "tlvStream" : { } + }, + "remoteShutdown" : { + "channelId" : "dca86c28f42d8793ae0e65a6ec747983e252fc9c93ea042f510dc9938bf6b43e", + "scriptPubKey" : "0014761879f7b274ce995f87150a02e75cc0c037e8e3", + "tlvStream" : { } + }, + "closingTxProposed" : [ [ { + "unsignedTx" : { + "txid" : "ca8534133fc815b47e03c12bd46de29c914ea288a07185794fad903d9a97a2aa", + "tx" : "0200000001dca86c28f42d8793ae0e65a6ec747983e252fc9c93ea042f510dc9938bf6b43e0000000000ffffffff02400d030000000000160014761879f7b274ce995f87150a02e75cc0c037e8e3dc260c0000000000160014761879f7b274ce995f87150a02e75cc0c037e8e300000000", + "toLocalOutput" : { + "amount" : 200000, + "publicKeyScript" : "0014761879f7b274ce995f87150a02e75cc0c037e8e3" + } + }, + "localClosingSigned" : { + "channelId" : "dca86c28f42d8793ae0e65a6ec747983e252fc9c93ea042f510dc9938bf6b43e", + "feeSatoshis" : 3620, + "signature" : "63e56c7056bd78cb27a28bb76e22fa67a1737012b2175c0dc5992ffe75247c9e167490658a73c4f2be18e99dbf83c6194c691f009eb9ab753f73596e246dc5c3", + "tlvStream" : { + "FeeRange" : { + "min" : 6740, + "max" : 13480 + } + } + } + }, { + "unsignedTx" : { + "txid" : "e3ba1938c9a50b59f74a05f0c022297facd0589abec2ba08bd7851fd6aac422c", + "tx" : "0200000001dca86c28f42d8793ae0e65a6ec747983e252fc9c93ea042f510dc9938bf6b43e0000000000ffffffff02400d030000000000160014761879f7b274ce995f87150a02e75cc0c037e8e3782c0c0000000000160014761879f7b274ce995f87150a02e75cc0c037e8e300000000", + "toLocalOutput" : { + "amount" : 200000, + "publicKeyScript" : "0014761879f7b274ce995f87150a02e75cc0c037e8e3" + } + }, + "localClosingSigned" : { + "channelId" : "dca86c28f42d8793ae0e65a6ec747983e252fc9c93ea042f510dc9938bf6b43e", + "feeSatoshis" : 2184, + "signature" : "5cee0b9b066cc86016de90d618da7b7f16349d7994b7232b80fc1cd3bc326b5a007d61da1c6bf8071b6ae1a5a8c29ea1c9b9ee777c59b0dd8268d6b57cb237eb", + "tlvStream" : { + "FeeRange" : { + "min" : 6740, + "max" : 13480 + } + } + } + }, { + "unsignedTx" : { + "txid" : "f3478d7bbf9d0a3f2a745f75f21336829bc491d49b978e8c7bb4b94223041c7f", + "tx" : "0200000001dca86c28f42d8793ae0e65a6ec747983e252fc9c93ea042f510dc9938bf6b43e0000000000ffffffff02400d030000000000160014761879f7b274ce995f87150a02e75cc0c037e8e3c82e0c0000000000160014761879f7b274ce995f87150a02e75cc0c037e8e300000000", + "toLocalOutput" : { + "amount" : 200000, + "publicKeyScript" : "0014761879f7b274ce995f87150a02e75cc0c037e8e3" + } + }, + "localClosingSigned" : { + "channelId" : "dca86c28f42d8793ae0e65a6ec747983e252fc9c93ea042f510dc9938bf6b43e", + "feeSatoshis" : 1592, + "signature" : "e1c5909ac828b7fcad839afdcbb08085f529b96ce803db98cfe1242eda97f3b4778731d114e9490cf07e81cb687dc90d9a6ccd3433128af54d87b81586000ad2", + "tlvStream" : { + "FeeRange" : { + "min" : 6740, + "max" : 13480 + } + } + } + } ] ], + "bestUnpublishedClosingTx_opt" : { + "txid" : "73670abbca5447e89670270e54425af0825a971e6094f57218198b0a54d3f8cb", + "tx" : "02000000000101dca86c28f42d8793ae0e65a6ec747983e252fc9c93ea042f510dc9938bf6b43e0000000000ffffffff02400d030000000000160014761879f7b274ce995f87150a02e75cc0c037e8e318310c0000000000160014761879f7b274ce995f87150a02e75cc0c037e8e3040047304402204c28eab56bf1f1477d8d9ac7b02a4b1e582608c2dc1da72f3060b65f95d2510b02204e55ed9444d39a542cdb0e270d3758e935bd122a6fddff297bda2209c5aa38f901473044022046dfba01365cd0e8989980ff558fa0a1ef649262f7d37522ebd143792115fcd10220342d658ec27efa39627c3fb55abce8b850179843f00da872be4a47a359b33f8f014752210376510328f91bf430fc444048aa4f204b7d04b208826b07e76e99c6290879991e2103f3841b40e4a5046ed410f8eb1fb79c59bc6ddc968f73eb92f87286c9b9d2044752ae00000000", + "toLocalOutput" : { + "amount" : 200000, + "publicKeyScript" : "0014761879f7b274ce995f87150a02e75cc0c037e8e3" + } + } +} \ No newline at end of file diff --git a/eclair-core/src/test/resources/nonreg/codecs/050009-DATA_NEGOTIATING_SIMPLE/anchor-outputs/data.bin b/eclair-core/src/test/resources/nonreg/codecs/050009-DATA_NEGOTIATING_SIMPLE/anchor-outputs/data.bin new file mode 100644 index 0000000000..8adfd11216 --- /dev/null +++ b/eclair-core/src/test/resources/nonreg/codecs/050009-DATA_NEGOTIATING_SIMPLE/anchor-outputs/data.bin @@ -0,0 +1 @@ +05000901d140d9192056abb1eb3cd4fab1c658e00d480256862b1b3aac179bb344a62a5d01010002bbbb671d15145722fb8c28d732cddb249bcc6652ed2b297ff1f77a18371b1e630009c9041c722c6b7aac20cb0cd43dd239d8a3fd6d3e306f1cdbbdf71843a1f7f1b480000000ff0000000000004e20000000000000140800000000000000000000002000100802aa698202aaaa00ce2f18a967dc4f25f414e671ba6585f8ef0b8c5fb812c21064f55a2eaaff000000000000271003970fc468240f7269c6ea3340468cf6c8f3ebded529f5cde0769987fa7b82ab3603a01edb1460bf62d68b90b8d5cff9837b2ba425a5aab391f3771ea44dd18aac2b02cc2f85c3de3578dbb34724dc3c6eb19e4a809b4d3a5468630f5f23d3fab6562e034ea352d4ce042fbaa08c02a7831be67e7c58402930a2962242407fcbece8c1090000186b0200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000002000180802aa69820000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000024d140d9192056abb1eb3cd4fab1c658e00d480256862b1b3aac179bb344a62a5d0000000000000000000f424002d571d16072a66bd4bbc9f425eed490893eabe60126728e919a23b69513c90c9a0400012401010101010101010101010101010101010101010101010101010101010101012a0000002b40420f0000000000220020708d468dcd15ba69b187fcb42b1de49172e137f6b793c8b16355809e68df114f061a8000002a00000000020200000000000003e800000000000003e8000000003b9aca00001e009000000000000000000000000009c4000000000bebc200000000002faf08008758dbf63c228973c5e00946120993d6b5aba15f5fbbc7008241ad888150382601ce4fa6feeb54c7ae66fa0f513f27ffdc9a1d904d1e0b038f73570cce52579e0c1814241eaadeec4b4a8110fa06dc4e8bcdd639c34fd519ebd69355c8feeae4040000000000000000044c0000000000000000000000001dcd6500006402d000000000000000000000000009c4000000002faf0800000000000bebc200647051acccba343fa8201fe3931315053c2339f5ea6031c97a42e1d1cca25f80035a1ebb061ea7d8ea91b0ef7ef4a6cab7bd2b2b89d5f6120807ccb6f2f2ac6034000000ff0237da0e963609124a77d37f2e132e1ed79a4ff42f072620d50dbd8513b42cfc140000000000000000271000160014761879f7b274ce995f87150a02e75cc0c037e8e300160014761879f7b274ce995f87150a02e75cc0c037e8e30001ff24d140d9192056abb1eb3cd4fab1c658e00d480256862b1b3aac179bb344a62a5d000000002b40420f0000000000220020708d468dcd15ba69b187fcb42b1de49172e137f6b793c8b16355809e68df114f710200000001d140d9192056abb1eb3cd4fab1c658e00d480256862b1b3aac179bb344a62a5d0000000000fdffffff02ecf2020000000000160014761879f7b274ce995f87150a02e75cc0c037e8e300350c0000000000160014761879f7b274ce995f87150a02e75cc0c037e8e3801a0600ff00000000ff24d140d9192056abb1eb3cd4fab1c658e00d480256862b1b3aac179bb344a62a5d000000002b40420f0000000000220020708d468dcd15ba69b187fcb42b1de49172e137f6b793c8b16355809e68df114f520200000001d140d9192056abb1eb3cd4fab1c658e00d480256862b1b3aac179bb344a62a5d0000000000fdffffff01ecf2020000000000160014761879f7b274ce995f87150a02e75cc0c037e8e3801a0600ff0000000000000124d140d9192056abb1eb3cd4fab1c658e00d480256862b1b3aac179bb344a62a5d000000002b40420f0000000000220020708d468dcd15ba69b187fcb42b1de49172e137f6b793c8b16355809e68df114ffd014d02000000000101d140d9192056abb1eb3cd4fab1c658e00d480256862b1b3aac179bb344a62a5d0000000000fdffffff02400d030000000000160014761879f7b274ce995f87150a02e75cc0c037e8e3ac1a0c0000000000160014761879f7b274ce995f87150a02e75cc0c037e8e304004730440220545ea4567257d78dd484e0d93e79adb98b59bef80dceaf4f1f753aab5c0d1f170220047732512997b79ab613326b419de6f7dde0e003aa9b91bd750c1306ed9f6096014730440220672a30519847599e2466243f815d17fd5525cdacd02783209d21910f090f567302204409c342638216a505769a5278e7643d71f8fbce596562ce8d485133c55772d30147522102d571d16072a66bd4bbc9f425eed490893eabe60126728e919a23b69513c90c9a2103d5751f1f29e5117ec524020084f42eaf73aa3eabcbfd32fbb3702a9eab12d67d52ae801a0600ff00000000 \ No newline at end of file diff --git a/eclair-core/src/test/resources/nonreg/codecs/050009-DATA_NEGOTIATING_SIMPLE/anchor-outputs/data.json b/eclair-core/src/test/resources/nonreg/codecs/050009-DATA_NEGOTIATING_SIMPLE/anchor-outputs/data.json new file mode 100644 index 0000000000..5ced7c1191 --- /dev/null +++ b/eclair-core/src/test/resources/nonreg/codecs/050009-DATA_NEGOTIATING_SIMPLE/anchor-outputs/data.json @@ -0,0 +1,169 @@ +{ + "type" : "DATA_NEGOTIATING_SIMPLE", + "commitments" : { + "channelParams" : { + "channelId" : "d140d9192056abb1eb3cd4fab1c658e00d480256862b1b3aac179bb344a62a5d", + "channelConfig" : [ "funding_pubkey_based_channel_keypath" ], + "channelFeatures" : [ ], + "localParams" : { + "nodeId" : "02bbbb671d15145722fb8c28d732cddb249bcc6652ed2b297ff1f77a18371b1e63", + "fundingKeyPath" : [ 3372489842, 745241260, 550178004, 1037187544, 2751294782, 812588251, 3187087427, 2717381044, 2147483648 ], + "initialRequestedChannelReserve_opt" : 20000, + "isChannelOpener" : false, + "paysCommitTxFees" : false, + "initFeatures" : { + "activated" : { + "option_simple_close" : "optional", + "option_route_blinding" : "optional", + "splice_prototype" : "optional", + "payment_secret" : "mandatory", + "gossip_queries_ex" : "optional", + "option_anchor_outputs" : "optional", + "option_quiesce" : "optional", + "option_data_loss_protect" : "optional", + "var_onion_optin" : "mandatory", + "option_static_remotekey" : "optional", + "option_support_large_channel" : "optional", + "option_anchors_zero_fee_htlc_tx" : "optional", + "option_channel_type" : "mandatory", + "basic_mpp" : "optional", + "gossip_queries" : "optional" + }, + "unknown" : [ ] + } + }, + "remoteParams" : { + "nodeId" : "02aaaa00ce2f18a967dc4f25f414e671ba6585f8ef0b8c5fb812c21064f55a2eaa", + "initialRequestedChannelReserve_opt" : 10000, + "revocationBasepoint" : "03970fc468240f7269c6ea3340468cf6c8f3ebded529f5cde0769987fa7b82ab36", + "paymentBasepoint" : "03a01edb1460bf62d68b90b8d5cff9837b2ba425a5aab391f3771ea44dd18aac2b", + "delayedPaymentBasepoint" : "02cc2f85c3de3578dbb34724dc3c6eb19e4a809b4d3a5468630f5f23d3fab6562e", + "htlcBasepoint" : "034ea352d4ce042fbaa08c02a7831be67e7c58402930a2962242407fcbece8c109", + "initFeatures" : { + "activated" : { + "option_simple_close" : "optional", + "option_route_blinding" : "optional", + "option_provide_storage" : "optional", + "splice_prototype" : "optional", + "payment_secret" : "mandatory", + "gossip_queries_ex" : "optional", + "option_anchor_outputs" : "optional", + "option_quiesce" : "optional", + "option_data_loss_protect" : "optional", + "var_onion_optin" : "mandatory", + "option_static_remotekey" : "optional", + "option_support_large_channel" : "optional", + "option_anchors_zero_fee_htlc_tx" : "optional", + "option_channel_type" : "mandatory", + "basic_mpp" : "optional", + "gossip_queries" : "optional" + }, + "unknown" : [ 50001 ] + } + }, + "channelFlags" : { + "nonInitiatorPaysCommitFees" : false, + "announceChannel" : false + } + }, + "changes" : { + "localChanges" : { + "proposed" : [ ], + "signed" : [ ], + "acked" : [ ] + }, + "remoteChanges" : { + "proposed" : [ ], + "acked" : [ ], + "signed" : [ ] + }, + "localNextHtlcId" : 0, + "remoteNextHtlcId" : 0 + }, + "active" : [ { + "fundingTxIndex" : 0, + "fundingInput" : "5d2aa644b39b17ac3a1b2b865602480de058c6b1fad43cebb1ab562019d940d1:0", + "fundingAmount" : 1000000, + "localFunding" : { + "status" : "confirmed", + "shortChannelId" : "400000x42x0" + }, + "remoteFunding" : { + "status" : "locked" + }, + "commitmentFormat" : "anchor_outputs", + "localCommitParams" : { + "dustLimit" : 1000, + "htlcMinimum" : 1000, + "maxHtlcValueInFlight" : 1000000000, + "maxAcceptedHtlcs" : 30, + "toSelfDelay" : 144 + }, + "localCommit" : { + "index" : 0, + "spec" : { + "htlcs" : [ ], + "commitTxFeerate" : 2500, + "toLocal" : 200000000, + "toRemote" : 800000000 + }, + "txId" : "8758dbf63c228973c5e00946120993d6b5aba15f5fbbc7008241ad8881503826", + "remoteSig" : { + "sig" : "ce4fa6feeb54c7ae66fa0f513f27ffdc9a1d904d1e0b038f73570cce52579e0c1814241eaadeec4b4a8110fa06dc4e8bcdd639c34fd519ebd69355c8feeae404" + }, + "htlcRemoteSigs" : [ ] + }, + "remoteCommitParams" : { + "dustLimit" : 1100, + "htlcMinimum" : 0, + "maxHtlcValueInFlight" : 500000000, + "maxAcceptedHtlcs" : 100, + "toSelfDelay" : 720 + }, + "remoteCommit" : { + "index" : 0, + "spec" : { + "htlcs" : [ ], + "commitTxFeerate" : 2500, + "toLocal" : 800000000, + "toRemote" : 200000000 + }, + "txId" : "647051acccba343fa8201fe3931315053c2339f5ea6031c97a42e1d1cca25f80", + "remotePerCommitmentPoint" : "035a1ebb061ea7d8ea91b0ef7ef4a6cab7bd2b2b89d5f6120807ccb6f2f2ac6034" + } + } ], + "inactive" : [ ], + "remoteNextCommitInfo" : "0237da0e963609124a77d37f2e132e1ed79a4ff42f072620d50dbd8513b42cfc14", + "remotePerCommitmentSecrets" : null, + "originChannels" : { } + }, + "lastClosingFeerate" : 10000, + "localScriptPubKey" : "0014761879f7b274ce995f87150a02e75cc0c037e8e3", + "remoteScriptPubKey" : "0014761879f7b274ce995f87150a02e75cc0c037e8e3", + "proposedClosingTxs" : [ { + "localAndRemote_opt" : { + "txid" : "96184f5c7ba4abc65a188b0c15306afd8ff51708a96ba309b8ef9751f6b3e64f", + "tx" : "0200000001d140d9192056abb1eb3cd4fab1c658e00d480256862b1b3aac179bb344a62a5d0000000000fdffffff02ecf2020000000000160014761879f7b274ce995f87150a02e75cc0c037e8e300350c0000000000160014761879f7b274ce995f87150a02e75cc0c037e8e3801a0600", + "toLocalOutput" : { + "amount" : 193260, + "publicKeyScript" : "0014761879f7b274ce995f87150a02e75cc0c037e8e3" + } + }, + "localOnly_opt" : { + "txid" : "9eef1646e92d55426ac9149f18bcdf46bba9f838e7e8c66d6a3698162a3395cf", + "tx" : "0200000001d140d9192056abb1eb3cd4fab1c658e00d480256862b1b3aac179bb344a62a5d0000000000fdffffff01ecf2020000000000160014761879f7b274ce995f87150a02e75cc0c037e8e3801a0600", + "toLocalOutput" : { + "amount" : 193260, + "publicKeyScript" : "0014761879f7b274ce995f87150a02e75cc0c037e8e3" + } + } + } ], + "publishedClosingTxs" : [ { + "txid" : "e12d095d42f2ec84fe04dccc953bac08bd877635fa862ba1a0f205fc9d0568c7", + "tx" : "02000000000101d140d9192056abb1eb3cd4fab1c658e00d480256862b1b3aac179bb344a62a5d0000000000fdffffff02400d030000000000160014761879f7b274ce995f87150a02e75cc0c037e8e3ac1a0c0000000000160014761879f7b274ce995f87150a02e75cc0c037e8e304004730440220545ea4567257d78dd484e0d93e79adb98b59bef80dceaf4f1f753aab5c0d1f170220047732512997b79ab613326b419de6f7dde0e003aa9b91bd750c1306ed9f6096014730440220672a30519847599e2466243f815d17fd5525cdacd02783209d21910f090f567302204409c342638216a505769a5278e7643d71f8fbce596562ce8d485133c55772d30147522102d571d16072a66bd4bbc9f425eed490893eabe60126728e919a23b69513c90c9a2103d5751f1f29e5117ec524020084f42eaf73aa3eabcbfd32fbb3702a9eab12d67d52ae801a0600", + "toLocalOutput" : { + "amount" : 200000, + "publicKeyScript" : "0014761879f7b274ce995f87150a02e75cc0c037e8e3" + } + } ] +} \ No newline at end of file diff --git a/eclair-core/src/test/resources/nonreg/codecs/04000d-DATA_WAIT_FOR_DUAL_FUNDING_READY/funder/data.bin b/eclair-core/src/test/resources/nonreg/codecs/050009-DATA_NEGOTIATING_SIMPLE/taproot/data.bin similarity index 78% rename from eclair-core/src/test/resources/nonreg/codecs/04000d-DATA_WAIT_FOR_DUAL_FUNDING_READY/funder/data.bin rename to eclair-core/src/test/resources/nonreg/codecs/050009-DATA_NEGOTIATING_SIMPLE/taproot/data.bin index 5e0852ff26..2188260716 100644 --- a/eclair-core/src/test/resources/nonreg/codecs/04000d-DATA_WAIT_FOR_DUAL_FUNDING_READY/funder/data.bin +++ b/eclair-core/src/test/resources/nonreg/codecs/050009-DATA_NEGOTIATING_SIMPLE/taproot/data.bin @@ -1 +1 @@ -04000d01ae58e31828b115b8a900a9d20b060ef2b2da2fcfe18991aff34168579d246b540101041040100002aaaa00ce2f18a967dc4f25f414e671ba6585f8ef0b8c5fb812c21064f55a2eaa00091b7c4b529185b423c599f3bf60d4d11f19254dc94217a8c89b4e285660a2d6b880000001000000000000044c000000001dcd6500000000000000000000900064c0000000186b0200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000080822aa698202bbbb671d15145722fb8c28d732cddb249bcc6652ed2b297ff1f77a18371b1e6300000000000003e8000000003b9aca0000000000000003e80090001e031a9b266c6a1f5d05f6f2b040b0eea59819c2fb5715945628541c58c2605e5c4202cb3e21fe0825663cfa1d8a8accc93dedbdb906b0ec4b779bb41b0f030b38fd330370c28e0286bfcf6bfb18c2b98d5b265594ec363822cbb323e7f7451ca60b033003fac293804f27e6823ba11b41063d35f20a844864de7cc2f9586445af26b1a03e000000140800000000000000000000000000000822aa69820000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000003e03c071ae90c35685c88048da0b1900948446ddbace3e41cd7a142967d584e8509fd019e020000000001023e98556e317c32c22b43d8f4aa7e31466f70b367ac61aaf4887a492df854b2800000000000000000001e3142211e1eb2114afd8a91d6a240d6a701bc01954e9f906206767e2a927d0d0000000000000000000360e31600000000002200204f6effc7a2d2d74b28ca1dcc262651c170c809a10d41bf5ab54f2c250ae53f6c0a770100000000001600142b08f4f677adeacc5dd403c01d77aade558f4d84c26e0100000000001600142b08f4f677adeacc5dd403c01d77aade558f4d840247304402201fc1bc68327665b99df0ed4f2deaba3481e04d2636b8abb3cbb7f27b8d381b0e02204308de7d6ac92419939feb2e69b780cc06f2f979104fc4ada04968de7e49f9db012103ec0636475c0250cae58d86fb4876f516ef3ba2052f9216e88816f4043fb1a25502483045022100a508a1276bfc7ebb2c23c89a25f4f463e5876e91a43ee55a04bb4d433c929f2f02200901a48fa1f05d4a6ef6d01e8c065fe59ccc129fce36eb2df49c719537159b34012103ec0636475c0250cae58d86fb4876f516ef3ba2052f9216e88816f4043fb1a255801a0600ffafae58e31828b115b8a900a9d20b060ef2b2da2fcfe18991aff34168579d246b54934351ffe48993f070a665d6e22a2c023b567ccc2f73489213cfeb6c9a2c05e00001006b0247304402201fc1bc68327665b99df0ed4f2deaba3481e04d2636b8abb3cbb7f27b8d381b0e02204308de7d6ac92419939feb2e69b780cc06f2f979104fc4ada04968de7e49f9db012103ec0636475c0250cae58d86fb4876f516ef3ba2052f9216e88816f4043fb1a255000100000000000000000000000009c4000000003b9aca00000000001dcd650024934351ffe48993f070a665d6e22a2c023b567ccc2f73489213cfeb6c9a2c05e0000000002b60e31600000000002200204f6effc7a2d2d74b28ca1dcc262651c170c809a10d41bf5ab54f2c250ae53f6c47522103a570258bd99dfc5f235fcd03cad5b91533b1e156e38ee8638577ade1eb0295432103e03c071ae90c35685c88048da0b1900948446ddbace3e41cd7a142967d584e8552aedf0200000001934351ffe48993f070a665d6e22a2c023b567ccc2f73489213cfeb6c9a2c05e000000000001a741d80044a010000000000002200209698cbfb709ad73804217ae67e86ea857e8c1b317c82e7740670eb6def38ea334a01000000000000220020d468d18ef4bd63800e3e59bbb204a2921dc75cec9786e363ba2d33d467ae98e820a1070000000000220020fbc40c7329e9ed94bb1a94bed8d66eba59e96aba45dc5d59150b6ac6ab24436ab2340f00000000002200201e1e972764d9ad2836c50f22bcb80b031dd989239b89db8d3c62c83e14ad88c0a71aae20353c51755a0c852a4c5dd9e2d33d4a9c0934e9a07a43da89f061a3b4499f374d1857dfa513ed714287baf4ac3f4375f06bf435577962f121182ad9ce4b19f549000000000000000000000000000009c4000000001dcd6500000000003b9aca00800a5eab3f585d54f19d6e574b611d94fa7414af651e17104310bf004ccd5cd603a59c87106cab1b24df7f9a3843a728a45bca74680f65118ca6c5a3d45c1934e6000000ff035642c8d53e45f57ffdbcfd1bdb3c931479649d6c3c3a4ffe766c1a3f8835212d00000000000001061a8000002a0000000101277132bb4ca09a00 \ No newline at end of file +05000901b086632eeb3280df5e60bc306c6355def2cf9fc76c57dec9543382852c2f6d9201010002aaaa00ce2f18a967dc4f25f414e671ba6585f8ef0b8c5fb812c21064f55a2eaa0009d00443a9fb70eb03eadf0ec8e7cc767902bc82b0127622161a9077638700df7080000001ff0000000000002710c000000000186b02000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000020001808020a598202bbbb671d15145722fb8c28d732cddb249bcc6652ed2b297ff1f77a18371b1e63ff0000000000004e200251e9ab08a96f12fe9767d431dce13bf4b01197fa1dd39137aa3403187e6c4e3b03a9c92fac03161730055d3244f669af06b5d017dd5b5612bc26ea9d954eb45f4e0312a9c6d2e78af602bdd0f4ad41df383b59b1243e7028b55e1788f442efed21570217551864c4fe2e42dd4bb9c2a0adfda3a013b9097e1ba1e4c670cf55506a609d0000004720000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000020001008028a59820000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000024b086632eeb3280df5e60bc306c6355def2cf9fc76c57dec9543382852c2f6d920000000000000000000f424002c5945d1e006744de21fbbe01788fc26ff44bb9bd012cc73647bbccd5e4a536d50400012401010101010101010101010101010101010101010101010101010101010101012a0000002b40420f0000000000225120d34fc1744f52f3459163e2cae5d4a412cf99fc412a5df73d3d8c3397c6952b24061a8000002a000000000203000000000000044c0000000000000000000000001dcd6500006402d000000000000000000000000009c4000000002faf0800000000000bebc2003f8b5472801539a522353220cfb0553669554ae6fb441506e01a0c530dcfafdc02ff2d9514870a5e6937fe1da6cb79dcd2c1f00ea2b15df3b552065a234be0986b020774aae4ecdb7e0763c2abf8ee6371a73bfb4bdb0655462ddd7650d0b99f7b1602a6aaedfe4b8c8c8b3567a9269e9265b98968bc39888f2ac17a777ace8ac4ca71000000000000000003e800000000000003e8000000003b9aca00001e009000000000000000000000000009c4000000000bebc200000000002faf0800a14c0f066e8676e8c0c35323901e8641f8af832e5dc11229f80ed0a6baa20af40222505fadfbf46d575ac16bec2d61c846e11d4cae0aca3a7f920d7c8b44b690ee000000ff024816e502b782e4e8c7b5d5b7ea07af34a877e32fc2aa2ba7dca2d02f0dfdc0290000000000000000271000160014761879f7b274ce995f87150a02e75cc0c037e8e300160014761879f7b274ce995f87150a02e75cc0c037e8e30001ff24b086632eeb3280df5e60bc306c6355def2cf9fc76c57dec9543382852c2f6d92000000002b40420f0000000000225120d34fc1744f52f3459163e2cae5d4a412cf99fc412a5df73d3d8c3397c6952b24710200000001b086632eeb3280df5e60bc306c6355def2cf9fc76c57dec9543382852c2f6d920000000000fdffffff02400d030000000000160014761879f7b274ce995f87150a02e75cc0c037e8e3b0200c0000000000160014761879f7b274ce995f87150a02e75cc0c037e8e3801a0600ff00000000ff24b086632eeb3280df5e60bc306c6355def2cf9fc76c57dec9543382852c2f6d92000000002b40420f0000000000225120d34fc1744f52f3459163e2cae5d4a412cf99fc412a5df73d3d8c3397c6952b24520200000001b086632eeb3280df5e60bc306c6355def2cf9fc76c57dec9543382852c2f6d920000000000fdffffff01b0200c0000000000160014761879f7b274ce995f87150a02e75cc0c037e8e3801a0600ff0000000000000124b086632eeb3280df5e60bc306c6355def2cf9fc76c57dec9543382852c2f6d92000000002b40420f0000000000225120d34fc1744f52f3459163e2cae5d4a412cf99fc412a5df73d3d8c3397c6952b24b502000000000101b086632eeb3280df5e60bc306c6355def2cf9fc76c57dec9543382852c2f6d920000000000fdffffff02400d030000000000160014761879f7b274ce995f87150a02e75cc0c037e8e3b0200c0000000000160014761879f7b274ce995f87150a02e75cc0c037e8e30140ecb62d69d8c6c7a39ffa6a399025a9e2339b9c23ae493bdf9ae47bef0d685f0a517dca879cbc8b532b6ae055210ded97c6ef08a66e3af1b02a2d178b6a016e14801a0600ff00000000 \ No newline at end of file diff --git a/eclair-core/src/test/resources/nonreg/codecs/050009-DATA_NEGOTIATING_SIMPLE/taproot/data.json b/eclair-core/src/test/resources/nonreg/codecs/050009-DATA_NEGOTIATING_SIMPLE/taproot/data.json new file mode 100644 index 0000000000..7feae98792 --- /dev/null +++ b/eclair-core/src/test/resources/nonreg/codecs/050009-DATA_NEGOTIATING_SIMPLE/taproot/data.json @@ -0,0 +1,169 @@ +{ + "type" : "DATA_NEGOTIATING_SIMPLE", + "commitments" : { + "channelParams" : { + "channelId" : "b086632eeb3280df5e60bc306c6355def2cf9fc76c57dec9543382852c2f6d92", + "channelConfig" : [ "funding_pubkey_based_channel_keypath" ], + "channelFeatures" : [ ], + "localParams" : { + "nodeId" : "02aaaa00ce2f18a967dc4f25f414e671ba6585f8ef0b8c5fb812c21064f55a2eaa", + "fundingKeyPath" : [ 3489940393, 4218481411, 3940486856, 3888936569, 45908656, 309731862, 445675363, 2264981360, 2147483649 ], + "initialRequestedChannelReserve_opt" : 10000, + "isChannelOpener" : true, + "paysCommitTxFees" : true, + "initFeatures" : { + "activated" : { + "option_simple_close" : "optional", + "option_support_large_channel" : "optional", + "option_simple_taproot_phoenix" : "optional", + "option_route_blinding" : "optional", + "option_provide_storage" : "optional", + "splice_prototype" : "optional", + "payment_secret" : "mandatory", + "gossip_queries_ex" : "optional", + "option_quiesce" : "optional", + "option_data_loss_protect" : "optional", + "var_onion_optin" : "mandatory", + "option_static_remotekey" : "mandatory", + "option_channel_type" : "mandatory", + "basic_mpp" : "optional", + "gossip_queries" : "optional" + }, + "unknown" : [ 50001 ] + } + }, + "remoteParams" : { + "nodeId" : "02bbbb671d15145722fb8c28d732cddb249bcc6652ed2b297ff1f77a18371b1e63", + "initialRequestedChannelReserve_opt" : 20000, + "revocationBasepoint" : "0251e9ab08a96f12fe9767d431dce13bf4b01197fa1dd39137aa3403187e6c4e3b", + "paymentBasepoint" : "03a9c92fac03161730055d3244f669af06b5d017dd5b5612bc26ea9d954eb45f4e", + "delayedPaymentBasepoint" : "0312a9c6d2e78af602bdd0f4ad41df383b59b1243e7028b55e1788f442efed2157", + "htlcBasepoint" : "0217551864c4fe2e42dd4bb9c2a0adfda3a013b9097e1ba1e4c670cf55506a609d", + "initFeatures" : { + "activated" : { + "option_simple_close" : "optional", + "option_simple_taproot_phoenix" : "optional", + "option_route_blinding" : "optional", + "splice_prototype" : "optional", + "payment_secret" : "mandatory", + "gossip_queries_ex" : "optional", + "option_quiesce" : "optional", + "option_data_loss_protect" : "optional", + "var_onion_optin" : "mandatory", + "option_static_remotekey" : "mandatory", + "option_support_large_channel" : "optional", + "option_anchors_zero_fee_htlc_tx" : "optional", + "option_channel_type" : "mandatory", + "basic_mpp" : "optional", + "gossip_queries" : "optional" + }, + "unknown" : [ ] + } + }, + "channelFlags" : { + "nonInitiatorPaysCommitFees" : false, + "announceChannel" : false + } + }, + "changes" : { + "localChanges" : { + "proposed" : [ ], + "signed" : [ ], + "acked" : [ ] + }, + "remoteChanges" : { + "proposed" : [ ], + "acked" : [ ], + "signed" : [ ] + }, + "localNextHtlcId" : 0, + "remoteNextHtlcId" : 0 + }, + "active" : [ { + "fundingTxIndex" : 0, + "fundingInput" : "926d2f2c85823354c9de576cc79fcff2de55636c30bc605edf8032eb2e6386b0:0", + "fundingAmount" : 1000000, + "localFunding" : { + "status" : "confirmed", + "shortChannelId" : "400000x42x0" + }, + "remoteFunding" : { + "status" : "locked" + }, + "commitmentFormat" : "simple_taproot_phoenix", + "localCommitParams" : { + "dustLimit" : 1100, + "htlcMinimum" : 0, + "maxHtlcValueInFlight" : 500000000, + "maxAcceptedHtlcs" : 100, + "toSelfDelay" : 720 + }, + "localCommit" : { + "index" : 0, + "spec" : { + "htlcs" : [ ], + "commitTxFeerate" : 2500, + "toLocal" : 800000000, + "toRemote" : 200000000 + }, + "txId" : "3f8b5472801539a522353220cfb0553669554ae6fb441506e01a0c530dcfafdc", + "remoteSig" : { + "partialSig" : "ff2d9514870a5e6937fe1da6cb79dcd2c1f00ea2b15df3b552065a234be0986b", + "nonce" : "020774aae4ecdb7e0763c2abf8ee6371a73bfb4bdb0655462ddd7650d0b99f7b1602a6aaedfe4b8c8c8b3567a9269e9265b98968bc39888f2ac17a777ace8ac4ca71" + }, + "htlcRemoteSigs" : [ ] + }, + "remoteCommitParams" : { + "dustLimit" : 1000, + "htlcMinimum" : 1000, + "maxHtlcValueInFlight" : 1000000000, + "maxAcceptedHtlcs" : 30, + "toSelfDelay" : 144 + }, + "remoteCommit" : { + "index" : 0, + "spec" : { + "htlcs" : [ ], + "commitTxFeerate" : 2500, + "toLocal" : 200000000, + "toRemote" : 800000000 + }, + "txId" : "a14c0f066e8676e8c0c35323901e8641f8af832e5dc11229f80ed0a6baa20af4", + "remotePerCommitmentPoint" : "0222505fadfbf46d575ac16bec2d61c846e11d4cae0aca3a7f920d7c8b44b690ee" + } + } ], + "inactive" : [ ], + "remoteNextCommitInfo" : "024816e502b782e4e8c7b5d5b7ea07af34a877e32fc2aa2ba7dca2d02f0dfdc029", + "remotePerCommitmentSecrets" : null, + "originChannels" : { } + }, + "lastClosingFeerate" : 10000, + "localScriptPubKey" : "0014761879f7b274ce995f87150a02e75cc0c037e8e3", + "remoteScriptPubKey" : "0014761879f7b274ce995f87150a02e75cc0c037e8e3", + "proposedClosingTxs" : [ { + "localAndRemote_opt" : { + "txid" : "a528f9f47d08e852e6688b07b4cb9d8fe486a74b90a81b39d609a757590e6fd2", + "tx" : "0200000001b086632eeb3280df5e60bc306c6355def2cf9fc76c57dec9543382852c2f6d920000000000fdffffff02400d030000000000160014761879f7b274ce995f87150a02e75cc0c037e8e3b0200c0000000000160014761879f7b274ce995f87150a02e75cc0c037e8e3801a0600", + "toLocalOutput" : { + "amount" : 200000, + "publicKeyScript" : "0014761879f7b274ce995f87150a02e75cc0c037e8e3" + } + }, + "localOnly_opt" : { + "txid" : "bf7b78c57528f111315ed968859f284a010ff8118403642a4393739abc89fb95", + "tx" : "0200000001b086632eeb3280df5e60bc306c6355def2cf9fc76c57dec9543382852c2f6d920000000000fdffffff01b0200c0000000000160014761879f7b274ce995f87150a02e75cc0c037e8e3801a0600", + "toLocalOutput" : { + "amount" : 794800, + "publicKeyScript" : "0014761879f7b274ce995f87150a02e75cc0c037e8e3" + } + } + } ], + "publishedClosingTxs" : [ { + "txid" : "a528f9f47d08e852e6688b07b4cb9d8fe486a74b90a81b39d609a757590e6fd2", + "tx" : "02000000000101b086632eeb3280df5e60bc306c6355def2cf9fc76c57dec9543382852c2f6d920000000000fdffffff02400d030000000000160014761879f7b274ce995f87150a02e75cc0c037e8e3b0200c0000000000160014761879f7b274ce995f87150a02e75cc0c037e8e30140ecb62d69d8c6c7a39ffa6a399025a9e2339b9c23ae493bdf9ae47bef0d685f0a517dca879cbc8b532b6ae055210ded97c6ef08a66e3af1b02a2d178b6a016e14801a0600", + "toLocalOutput" : { + "amount" : 200000, + "publicKeyScript" : "0014761879f7b274ce995f87150a02e75cc0c037e8e3" + } + } ] +} \ No newline at end of file diff --git a/eclair-core/src/test/resources/nonreg/codecs/05000a-DATA_CLOSING/local/data.bin b/eclair-core/src/test/resources/nonreg/codecs/05000a-DATA_CLOSING/local/data.bin new file mode 100644 index 0000000000..b54473c001 --- /dev/null +++ b/eclair-core/src/test/resources/nonreg/codecs/05000a-DATA_CLOSING/local/data.bin @@ -0,0 +1 @@ +05000a012f2eb1b005627f7bfa0a75f9a58264e76b3e5556a0c0390c3391d7bbcb01dbb501010002aaaa00ce2f18a967dc4f25f414e671ba6585f8ef0b8c5fb812c21064f55a2eaa0009fb06f68c2377f76291612ec4a90a34399180c41c3eeb5130d4593c2648aaebf380000001ff0000000000002710c000000000186b0200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000180802aa698202bbbb671d15145722fb8c28d732cddb249bcc6652ed2b297ff1f77a18371b1e63ff0000000000004e200319b253943833df9299411fd93035b7c7907ca7af1c522ada034aa860c7aaf117034cda59ef0c36106fef2ef99769b553ba116916c64c8d7e17619e0cedf4d327d30353418e63b7891f578f6caa7fe041815c19ba3afcd70b01955fbafd0fe08f06dd02b244d38d0ced96d8282c4006d44e9f3eb34e80acb0d612cd3a7970d626a74186000000140800000000000000000000000000100802aa69820000000000014a00822f2eb1b005627f7bfa0a75f9a58264e76b3e5556a0c0390c3391d7bbcb01dbb500000000000000006e582d0c2714b4495f83b884257b0560d2141bd6266867cb073306dd9e14f43e000000000000000000000000000000030000000000000001000101fd05b12f2eb1b005627f7bfa0a75f9a58264e76b3e5556a0c0390c3391d7bbcb01dbb5000000000000000000000000068e7780f13140ed218f8882803ee844374172569e40df747c52cb1b36aad3b645f99b9000061b100002ce8864be51a66ca5c6eb0a230e56f4dcfcfa3b293df870745e2290896d1458b1c7963d6b246f5f4e025759adcf2066fe86cc2faf5667bee06ee6224074222234af87155870dc44b51c553acf2dfcb15bd914991732e1a7379f85fb120e17993fa335ef07c3032b96393d37fd993d8f9a6318d538c912a30768fee364457bde1f252e783dfb3a5a0f19f4753cefe3ed343b1e346bcdeb16efa3e0b54fd87265708dbca839a36b5e04ff9bf4118c7829c79aaac2d6507cb964751d65bada9389a64cf2c18758cc30aede626c4edef10da1b33a7372838087fa4a02ba3a1db8b24f7748528e71bde418b35876f16bac6c4230b6f03cb3b0349eda81ed1a427ce423fc47db0bffd67ae4065d9c93f494bb9c4932d5a4ddfa7c83c4bffe381873b2946293d0cd44500760a947f27ec91161a1ccf48c0b5f83e9123fecd9b1ca1294a7bf20b35b42c7fd1d66973679d819d4025104c845e972d826165de4df315197b0e53caf66276437c0b4c37ab4c8b93f2abd522a8f28f60bf01cfe3d0d0e97dc281ace63888dabaea00fbd7af4718d0f752210bf84b6a947f8314d6966115b7da9cfe219fa8e9fbdafd7129a7ffd183ba436173548e707db5233e6616bd94ae24fc841e6dfd2b9eba1202a4ed06fa09c0010d82dc2d225f117ec122d1c9458fc92333af73ac40206fb201366f113485d0cebb12163cfde5479793335b9d5e742a1e48d0645c9184d6a2509d9ee41bf2f1192ea19e0b36ea913ab8d00d4dfd532a42e240db95e709b8fd6df30eeaf7788abc8856ef4414d6262c534727116d622b88376cf405c69d44d5332fc9174a914f4c3324b5f3107f81a4389caf2d2bbded2fdfe5ddad8c82dafbf52aedfb14226a306521aeab9bdd4036fa8c8a8ea92c843c1b9542562ffb5afd5c49de0f70d77794f11737621eaab43803761592bb56b711248756816a92edea6e445aba84381bfb3c42de2893aacddeaec90f47159927538ebb12aab84b5060109b25316ba59f87a25058645a7e3a4175d5a915c46cc5962efce2504d40efdab3d7cec04019cb06b162136a093d8ee62e3142839f4c465c4236c331573d3f44bc7057498baff80a480ba1aef97bbd4ee5324ced9ffe9cec175c334b41246177efacd5c6d3ad4ffe8dc346509e562ee59049285b863f81c8f4e6f031c571e6244cbe4bb83ac9b183aaea3f8d1bc048dcff42b10ef2df17c3d8798f9e14ad7ef94f0c95fff06841d9aae4a196ba9a089fa4031d26f463377558670cf8c1e1169b34846e407d0e92722d2a94d0a9a600db4bc44eff4194e1133c312de59818b5eca63a0d838c67053d10e5c8245b7855f1773867bfb05ea9c7b3941f7900f9e31ee959b4de8b694ff00306b64bac1e7939356e4bb354bcb78b94bb85da84427347daea704df10c78ffa639c32f747f575cd8273d23b9141275e38d073988463590fd3e9af73499e7fce8e873f23dc579df19f205472534db58e97781b78783d9be95dde62aaeb70a07627a58137cc908e26ff1b9191f005c5a4e470db1fb8d288fe09a6938dff43cac3694f390be5a40839e3e535f73c82711b1005d7e30da9d04efd5af1839e37d75d29c799928cc3cac114b701b160d990382d3d036f943b15fb54f92f12373f96a97ff194f57d5ecf2ec544190613f059f88658e642e6cbc2d8e7fe9bb65a720714d79b07ea3477efba4babb11c5be1550577884af16304561ee156cea61b58780d88a1620bce4d25dd345aece78e8d46ec674cfbc2a7e670399a2fee1a20679994c98c3e59c97a1d7e983c0b53cb5e7020a0bcbc9032c059bab48b4a0d37bb95573f083c6feab01502d3ddffaed478b4349acecb69c6f9a9267d51a807d8d9ec2dfce6eb734028208b28a3f4942c402314f34a692d47a6341ac6de75da8b535ecf16fff5fe0001a14701070001000000000000000000000000242f2eb1b005627f7bfa0a75f9a58264e76b3e5556a0c0390c3391d7bbcb01dbb50000000000000000000f4240038e5944509edcc47eabb32e68747f92c61bbea857b1e122649df2acb4112bec600400012401010101010101010101010101010101010101010101010101010101010101012a0000002b40420f0000000000220020c45bdb7d812241b0d1697b60708b02eab5c0f0f9ac4e5764bb9885995643e37a061a8000002a000000000202000000000000044c0000000000000000000000001dcd6500006402d000000000000000070001010000000000000000000009c4000000000bebc2000000000029209080b6d9cb30fefe3c2ed5f9dd0a8c3ed9ccf54d620b701c56410e4ed78fcefca9490174dae76c12d1f3faf5efb7f292840af187d0ea0d008cdb8f46e0efc6ecfdb8b542431cca69c265250c3a3658d3a8b02cedd971cfbb11cd28f65a9ffcc6d5a64400013837689fab474750fc8e9666bb5471d22d1d52ace1214e99e21edbbf6941e9ad7760e73a34b1c1d81a444af0fd52ea77ffb17a6d548684c93b70512a8b18ccc000000000000003e800000000000003e8000000003b9aca00001e009000000000000000070001020000000000000000000009c40000000029209080000000000bebc2002d88d75ca245aedaee7709732088573676471c227727a98b06400a2061846799022bdcbdf3c0ff1e21548b7d455e884599b19f0d57318bf14d56fde08084ec550cff622f2eb1b005627f7bfa0a75f9a58264e76b3e5556a0c0390c3391d7bbcb01dbb5a44461f6b1d094ed5897fd8df1b4783e64bbea90fca7b57c462324918f9d980e24790d7ed23e5bde59de2c46f985473228d923ed2b82996afe4dec7f98890628000000000000000000080000000009c4000000002920908000000000127a3980bf359514f86a18ff97933d96c98066162b1242becb32a7052d3f03ba6f819b67035e30811486ba863d85ac010fa0933784b127b5131a62b2350627e01e93dd052c00000000000000000000070003003e0000fffffffffffc008099021981b18487b451d600bb56646a67d4907a3488c272d433d89a3cf653926400fc0003ffffffffffe80101e785c8322c3b362fc88469cca7a3b9106603b25da1a49e200bd6f55680402c5802000007ffffffffffc80105973e8039fc5967d85026e102f6e5eb92a0f9cc2de954df456b3f280cbab1d1040003ffffffffffe400000000061a80160014761879f7b274ce995f87150a02e75cc0c037e8e300000000fffd01e7020000000001012f2eb1b005627f7bfa0a75f9a58264e76b3e5556a0c0390c3391d7bbcb01dbb50000000000a33aee80054a01000000000000220020387286e7d751204df392696a71984ea6477974810a5e4b2995f4643e429c624b4a01000000000000220020fd9d83d03e8d5299369d43466aa0d0f63942791c2fb9081d06f6170dc58b8dd7b0ad010000000000220020c678813094320ce2737c4a64d5a969022f41c530b4f10aa8e67c5d03e1efdd4804fe0200000000002200202f6616b0cd17eb00350ba0ec9c533de947891504d1540dfc6b76760c2d35252b50870a0000000000220020aa5617da3371dfa43aa83633223bb74554051a5419158bde45ff7df812ddbf690400483045022100c3df79bff08d2d1d253621523eb1d0d8318ec5113ad313ed38a117fae15f6f830220292be1fc199f6ab0c36deb3dab91f45f4d70b7933458187cc005a2db36911c3b01473044022074dae76c12d1f3faf5efb7f292840af187d0ea0d008cdb8f46e0efc6ecfdb8b5022042431cca69c265250c3a3658d3a8b02cedd971cfbb11cd28f65a9ffcc6d5a6440147522102efc3254975667057c8d8ee43cff68662eea27bb06b21cfd6aaf7fe14e33277be21038e5944509edcc47eabb32e68747f92c61bbea857b1e122649df2acb4112bec6052ae9e52f120ff2449a9fcce8fd74e0e41561c700b624df5ccd93e8c0addf9d52e3cfefe30cbd9b603000000ff2449a9fcce8fd74e0e41561c700b624df5ccd93e8c0addf9d52e3cfefe30cbd9b60000000000012449a9fcce8fd74e0e41561c700b624df5ccd93e8c0addf9d52e3cfefe30cbd9b6020000000000000000000000000000000000000000000000 \ No newline at end of file diff --git a/eclair-core/src/test/resources/nonreg/codecs/05000a-DATA_CLOSING/local/data.json b/eclair-core/src/test/resources/nonreg/codecs/05000a-DATA_CLOSING/local/data.json new file mode 100644 index 0000000000..96ab01a0f0 --- /dev/null +++ b/eclair-core/src/test/resources/nonreg/codecs/05000a-DATA_CLOSING/local/data.json @@ -0,0 +1,187 @@ +{ + "type" : "DATA_CLOSING", + "commitments" : { + "channelParams" : { + "channelId" : "2f2eb1b005627f7bfa0a75f9a58264e76b3e5556a0c0390c3391d7bbcb01dbb5", + "channelConfig" : [ "funding_pubkey_based_channel_keypath" ], + "channelFeatures" : [ ], + "localParams" : { + "nodeId" : "02aaaa00ce2f18a967dc4f25f414e671ba6585f8ef0b8c5fb812c21064f55a2eaa", + "fundingKeyPath" : [ 4211537548, 595064674, 2439065284, 2836018233, 2441135132, 1055609136, 3562617894, 1219161075, 2147483649 ], + "initialRequestedChannelReserve_opt" : 10000, + "isChannelOpener" : true, + "paysCommitTxFees" : true, + "initFeatures" : { + "activated" : { + "option_route_blinding" : "optional", + "option_provide_storage" : "optional", + "splice_prototype" : "optional", + "payment_secret" : "mandatory", + "gossip_queries_ex" : "optional", + "option_anchor_outputs" : "optional", + "option_quiesce" : "optional", + "option_data_loss_protect" : "optional", + "var_onion_optin" : "mandatory", + "option_static_remotekey" : "optional", + "option_support_large_channel" : "optional", + "option_anchors_zero_fee_htlc_tx" : "optional", + "option_channel_type" : "mandatory", + "basic_mpp" : "optional", + "gossip_queries" : "optional" + }, + "unknown" : [ 50001 ] + } + }, + "remoteParams" : { + "nodeId" : "02bbbb671d15145722fb8c28d732cddb249bcc6652ed2b297ff1f77a18371b1e63", + "initialRequestedChannelReserve_opt" : 20000, + "revocationBasepoint" : "0319b253943833df9299411fd93035b7c7907ca7af1c522ada034aa860c7aaf117", + "paymentBasepoint" : "034cda59ef0c36106fef2ef99769b553ba116916c64c8d7e17619e0cedf4d327d3", + "delayedPaymentBasepoint" : "0353418e63b7891f578f6caa7fe041815c19ba3afcd70b01955fbafd0fe08f06dd", + "htlcBasepoint" : "02b244d38d0ced96d8282c4006d44e9f3eb34e80acb0d612cd3a7970d626a74186", + "initFeatures" : { + "activated" : { + "option_route_blinding" : "optional", + "splice_prototype" : "optional", + "payment_secret" : "mandatory", + "gossip_queries_ex" : "optional", + "option_anchor_outputs" : "optional", + "option_quiesce" : "optional", + "option_data_loss_protect" : "optional", + "var_onion_optin" : "mandatory", + "option_static_remotekey" : "optional", + "option_support_large_channel" : "optional", + "option_anchors_zero_fee_htlc_tx" : "optional", + "option_channel_type" : "mandatory", + "basic_mpp" : "optional", + "gossip_queries" : "optional" + }, + "unknown" : [ ] + } + }, + "channelFlags" : { + "nonInitiatorPaysCommitFees" : false, + "announceChannel" : false + } + }, + "changes" : { + "localChanges" : { + "proposed" : [ ], + "signed" : [ { + "channelId" : "2f2eb1b005627f7bfa0a75f9a58264e76b3e5556a0c0390c3391d7bbcb01dbb5", + "id" : 0, + "paymentPreimage" : "6e582d0c2714b4495f83b884257b0560d2141bd6266867cb073306dd9e14f43e", + "tlvStream" : { } + } ], + "acked" : [ ] + }, + "remoteChanges" : { + "proposed" : [ ], + "acked" : [ ], + "signed" : [ ] + }, + "localNextHtlcId" : 3, + "remoteNextHtlcId" : 1 + }, + "active" : [ { + "fundingTxIndex" : 0, + "fundingInput" : "b5db01cbbbd791330c39c0a056553e6be76482a5f9750afa7b7f6205b0b12e2f:0", + "fundingAmount" : 1000000, + "localFunding" : { + "status" : "confirmed", + "shortChannelId" : "400000x42x0" + }, + "remoteFunding" : { + "status" : "locked" + }, + "commitmentFormat" : "anchor_outputs", + "localCommitParams" : { + "dustLimit" : 1100, + "htlcMinimum" : 0, + "maxHtlcValueInFlight" : 500000000, + "maxAcceptedHtlcs" : 100, + "toSelfDelay" : 720 + }, + "localCommit" : { + "index" : 7, + "spec" : { + "htlcs" : [ { + "direction" : "IN", + "id" : 0, + "amountMsat" : 110000000, + "paymentHash" : "f13140ed218f8882803ee844374172569e40df747c52cb1b36aad3b645f99b90", + "cltvExpiry" : 400144 + } ], + "commitTxFeerate" : 2500, + "toLocal" : 200000000, + "toRemote" : 690000000 + }, + "txId" : "b6d9cb30fefe3c2ed5f9dd0a8c3ed9ccf54d620b701c56410e4ed78fcefca949", + "remoteSig" : { + "sig" : "74dae76c12d1f3faf5efb7f292840af187d0ea0d008cdb8f46e0efc6ecfdb8b542431cca69c265250c3a3658d3a8b02cedd971cfbb11cd28f65a9ffcc6d5a644" + }, + "htlcRemoteSigs" : [ "3837689fab474750fc8e9666bb5471d22d1d52ace1214e99e21edbbf6941e9ad7760e73a34b1c1d81a444af0fd52ea77ffb17a6d548684c93b70512a8b18ccc0" ] + }, + "remoteCommitParams" : { + "dustLimit" : 1000, + "htlcMinimum" : 1000, + "maxHtlcValueInFlight" : 1000000000, + "maxAcceptedHtlcs" : 30, + "toSelfDelay" : 144 + }, + "remoteCommit" : { + "index" : 7, + "spec" : { + "htlcs" : [ { + "direction" : "OUT", + "id" : 0, + "amountMsat" : 110000000, + "paymentHash" : "f13140ed218f8882803ee844374172569e40df747c52cb1b36aad3b645f99b90", + "cltvExpiry" : 400144 + } ], + "commitTxFeerate" : 2500, + "toLocal" : 690000000, + "toRemote" : 200000000 + }, + "txId" : "2d88d75ca245aedaee7709732088573676471c227727a98b06400a2061846799", + "remotePerCommitmentPoint" : "022bdcbdf3c0ff1e21548b7d455e884599b19f0d57318bf14d56fde08084ec550c" + }, + "nextRemoteCommit" : { + "index" : 8, + "spec" : { + "htlcs" : [ ], + "commitTxFeerate" : 2500, + "toLocal" : 690000000, + "toRemote" : 310000000 + }, + "txId" : "bf359514f86a18ff97933d96c98066162b1242becb32a7052d3f03ba6f819b67", + "remotePerCommitmentPoint" : "035e30811486ba863d85ac010fa0933784b127b5131a62b2350627e01e93dd052c" + } + } ], + "inactive" : [ ], + "remoteNextCommitInfo" : { + "sentAfterLocalCommitIndex" : 7 + }, + "remotePerCommitmentSecrets" : null, + "originChannels" : { } + }, + "waitingSince" : 400000, + "finalScriptPubKey" : "0014761879f7b274ce995f87150a02e75cc0c037e8e3", + "mutualCloseProposed" : [ ], + "mutualClosePublished" : [ ], + "localCommitPublished" : { + "commitTx" : { + "txid" : "b6d9cb30fefe3c2ed5f9dd0a8c3ed9ccf54d620b701c56410e4ed78fcefca949", + "tx" : "020000000001012f2eb1b005627f7bfa0a75f9a58264e76b3e5556a0c0390c3391d7bbcb01dbb50000000000a33aee80054a01000000000000220020387286e7d751204df392696a71984ea6477974810a5e4b2995f4643e429c624b4a01000000000000220020fd9d83d03e8d5299369d43466aa0d0f63942791c2fb9081d06f6170dc58b8dd7b0ad010000000000220020c678813094320ce2737c4a64d5a969022f41c530b4f10aa8e67c5d03e1efdd4804fe0200000000002200202f6616b0cd17eb00350ba0ec9c533de947891504d1540dfc6b76760c2d35252b50870a0000000000220020aa5617da3371dfa43aa83633223bb74554051a5419158bde45ff7df812ddbf690400483045022100c3df79bff08d2d1d253621523eb1d0d8318ec5113ad313ed38a117fae15f6f830220292be1fc199f6ab0c36deb3dab91f45f4d70b7933458187cc005a2db36911c3b01473044022074dae76c12d1f3faf5efb7f292840af187d0ea0d008cdb8f46e0efc6ecfdb8b5022042431cca69c265250c3a3658d3a8b02cedd971cfbb11cd28f65a9ffcc6d5a6440147522102efc3254975667057c8d8ee43cff68662eea27bb06b21cfd6aaf7fe14e33277be21038e5944509edcc47eabb32e68747f92c61bbea857b1e122649df2acb4112bec6052ae9e52f120" + }, + "localOutput_opt" : "b6d9cb30fefe3c2ed5f9dd0a8c3ed9ccf54d620b701c56410e4ed78fcefca949:3", + "anchorOutput_opt" : "b6d9cb30fefe3c2ed5f9dd0a8c3ed9ccf54d620b701c56410e4ed78fcefca949:0", + "incomingHtlcs" : { + "b6d9cb30fefe3c2ed5f9dd0a8c3ed9ccf54d620b701c56410e4ed78fcefca949:2" : 0 + }, + "outgoingHtlcs" : { }, + "htlcDelayedOutputs" : [ ], + "irrevocablySpent" : { } + }, + "revokedCommitPublished" : [ ] +} \ No newline at end of file diff --git a/eclair-core/src/test/resources/nonreg/codecs/05000a-DATA_CLOSING/next-remote/data.bin b/eclair-core/src/test/resources/nonreg/codecs/05000a-DATA_CLOSING/next-remote/data.bin new file mode 100644 index 0000000000..5e59a7b2f1 --- /dev/null +++ b/eclair-core/src/test/resources/nonreg/codecs/05000a-DATA_CLOSING/next-remote/data.bin @@ -0,0 +1 @@ +05000a015c2c7737036235a6889a24f7800f762660d84273f5926940b4452af3618f592c01010002aaaa00ce2f18a967dc4f25f414e671ba6585f8ef0b8c5fb812c21064f55a2eaa00090b1d7d62b967d6f895091353b343e630aeb804577a9513033622bea85e1dcd9180000001ff0000000000002710c000000000186b0200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000180802aa698202bbbb671d15145722fb8c28d732cddb249bcc6652ed2b297ff1f77a18371b1e63ff0000000000004e200395e320d7df69bfbc0b2d1f559010bcc3569b166b5219a69d0722c51dbde0b3b202588a7e8f90381590f3defcf757807e0aded5cf93d500b2e0e9939f86c9fd8c3f02771ae3d3cb51eeca4470412e54aab30e2ce629204562f5baeee1380f46da2374023c4c494f182e78a74d9ca0233cc8db3841c6c4fcba75c736d15f8da2bd305bc8000000140800000000000000000000000000100802aa6982000000024a00825c2c7737036235a6889a24f7800f762660d84273f5926940b4452af3618f592c0000000000000000a906e024f5cf088b1b4115cc38d30e8f24f8465bb00cc0f8f145ec3bd638b9122c00835c2c7737036235a6889a24f7800f762660d84273f5926940b4452af3618f592c000000000000000100000001fd05b300805c2c7737036235a6889a24f7800f762660d84273f5926940b4452af3618f592c00000000000000030000000005a995c0d1e829925392fdaa8e472d7262d40a076af9bfbafb46d04c86715cfa017e0ca900061aa00002065ea89fa5a1b90013656bd306dae4c0b94155a98f35fe853a418e512bbbaa2aea4ceae2e8235c468f78bb7c7e9dd286246b5e27276c3c7ebc7ed54c5359265eb9bb8b80e4bcd9cfbaf7f2a923d3d906f2bcc75b12113873352f8bc9f195bf9dd2ffcf04173b428dbfa501f72a9ae75015fcc7bf8bba95e2f2734492b1f7df70887471413fed6e9e688b96cc1605344b6104122f5e6bc9c9ae779a66d026f3e069304d939fee334f79313d916a665a3e57ab7a72ea988743b0ce552c02194fef1f8870f79bc972c31aaa47ae61bb0a39af33f6ed1407398b379db153f46934b6158dfb19c6c5896afb89bc01740de868bcb7c7c60afd7bed6acf38de73e45f1db62164c41af45d89eb605373de2dd85733dec745a31624cec2b45a15d41822f4618ee986d1db96c02a1d3e1dc02c66880245f522c2dce55a54847bd370634237c39ffdd466e7151f57986fc32448172859e7ed6b9f0132c98429e622370e446a0a954b8e6e11012de38e0661844ea0df4854355eb8ae8b8027020e24131eff0761871f71d09d5f5d56615a5eecd061b3cf8c4230f6efde1d15556850d0d356aac04db5898e71a5496235a8ad8b60c509f473b104b05c60f40e0c8a0f7bb2fe8c5f896765fea4a53bd015ab9ea643584a51648155ed0f90f0f2ba7d9ad5cb52bdfb60a8553d626c7a28795df2dd08e012d370fefa8c7144d8df62ee2f643a8d767265c6da396418be04771cb1a602f752205593e81ae6f6a338129fa7dcf7f9af440ab24b7e01973fcb733ec794ffb2ff308c398bc50b089df4d4f3cb69649df5ded4f679821b4dd37623bd65853da74e6ac5ad754005095d401c07996c4aa7b6937d4cf7834952ca691488dd5b4b831e7042a17acbe0a6ab853d4f2012e6960528ef54762d37a3bd651507732c38b26efd83019a0461a5e36ea06969ea94ec2ec2da608c7e3cfd40a6d5ad03527027aacb938e4820e6390dc19b3d6e27c47d058c2e328b6dfc56d811befdd8abe919f972dbd9320b91d0b134d018f2dace97549f10055758994f81613f37fedfc8116e62e67be952083b736f55c1488af1ab591dea8dd18e64605149280cd2c45e86c4f0566678a84dac554d2e100cbc1edc87637a1c0f8e8914e19daf7e123c3eda58f6b6677fbcd4b67f0b650b8eb03f061854c0d1d2e595546f7dc887bac5ab2c83d26391a87611d828e34be55ec2cf577acae1cc477e9c3b161c899be80016890524551e6309bde6f5d66ba264f57d6d50aeeb0bf80a23e2c8902aebea29238f70f2cc45a519e49cd59aef6cdb216f9b0d62733b5e15b41ebcffc94e93fcdc683d2f826fb7fffd671a8e3951b37f4f86d187520e3307a0ba76ee17167f3aa036806a4a0175bd2667a4b89522f71cccac33245ad08aea005b3701faecbd8f2ea6557610c62660748019f683b01fcd4144a6db868ad17c4bd6615f64d3ebbb6ce9cce3d268cc335a562c7a9aaa55c65361a89da437167e0a0611a49859f70b638fa4020c28b10d2a3f35abb87e6cac847d99c69e05417d96cc6b8ed237a7399c256aa106930c4307f76bde0fdad1796cbcc6d3e767ccecd257e1887fbfcd8ff60f6b3c7dd8758c8082ae9e7f5578eb9802fde18b07739672529d680bd197947e2114c24746d2fff6f718d7e8e67067da65ab02ab2700957fd907397b92bbcb407685a9e1063f5d0ded6aa9f24c55d353c7ff1fae9c152f7510e414e6e48c0d4023ad488925302ef7b2d89e4f6e2882c7178407b543f36cb2400900e3e636f450b4249712d2c38f9348360e4a0045b3908b9af2e89fc35942957ef1d9385b0180fb8bd00de51b257f3f6883d59ad99600f5ed87a4efe503ec4774eb08c08353565fedfbb6dc283ae1ea4bbe67becc89c99d333c245e44e8d94be0e29248a1c27d0fc9d1f0b004fe0001a1470107000000000000000000000000000000040000000000000002000301fd05b15c2c7737036235a6889a24f7800f762660d84273f5926940b4452af3618f592c000000000000000000000000068e778004245ebd50c1ad4f0af1ebe00d8ac493f92150325ccd88ca4eab02e16b5db3c600061ac0000381b28a2c6a9495a2a99c32dc5942d9c5647825d9efa9c29fd27beaf354630deb2666b55e227cb5d288fa80c9ab12e89fe665b933f3dfaf6adc6d159de50367e26041046d128ffc47362a9e10bb65994e985371b4d39aa19e979061453982cd481cee6569fbc38caea4291c2e20c850e889ae05b683dd881b440290675ebdcf6a0dcef6661f47bc6b060ecb037f17e3532aa6e54f6831b3dd318d21dac159516d61a28bb249d47bebcafb4b367bed63c9655d7abb150b5886b2a7e1e00192f5e6b167e27fa813a34e21c27fef1d64b5bc569d5da70d5abc6f1811b8cb7e293cc803470305a1835fe5dce8be5320c27f9fd34180a5d2a8a9e8d92b2c5e865dc6134d9de653fceb8c9a1521a93a5521ada9c06ac6d5f7fa903d916ef1d0cd30ca30ac1674747a49eb1083cc2e44a33dbcb3c7ce98b9f3fa7e249a2f8e84721b28cd523467afe0b291dfc911868e254a276e89ef79729e47a9a1b94a93f91f1728033ef4f272b60fe9332132df1ee25afa62e02b0034d11135f187963b4857fb75b32f5216f9317c7af82b99d224394bfef1f94ac77fbf99eeb34ffa32f4dc03204ef0ad0f869d5f27e73e2e861bdcfd110d83f2e670ba9ebd827fa41819417c756b1124999c99f0457ce3e7312674b3e16042e33fc8b9d1c4463cf568a827a69240ab60cbffefb6426e70ce5fbf9d1eea0a732f5b39324ee7ae338be680b472eb5349564105e3c087a617d9233bc1553f85bd613b6f48a3d601b02c37a750bdf59c19b7c6b478605fa667be37d346cbc322cba4f8a3aa6ac3c6245ce58b3bac3dec9e214ef751215f81d6174a21dc857bc7ed057ac1e3e6af54b1bda046c931f3da33dd85a40958dbe0d712e9d4e827ca9dee08a7d40a6b789e286c4e781b40bf7e7fe9f6b431468308249a41f252dfcd5f74fb1838a87f7e272d6f496c4ce0bc546d05356a66e8009e05308ab705ea69b37a8539e7c31b62576d1e0d4d1fa0152a36918460f635b50176d14e842f9833df45d098ca847695b980b639ff06f0c06e2807e482ed934343acb48e6b2b7a69156397a7313ead352eb5078350001dbbbc56432147f0ed6485a0f9a5c4bd8f8024d0522327a47c2694914a098347783963c5cb665fda7e7cb1fc98b0ab93c99489d691c98bcaa8358be299468a0aaf4d0a3779b805c7023a30671a9c09841aa93575682ab74e2ecfa5699de8ec5932ee60bc8e898eeca15b7e3d007711b1ad9b702cef1858f79fe604fad9a02b8bb78b7ce8f8fe725c6d40ff466c43a5002d46512071a0d7400893a33bfbef7d1427593bb9329ad81d144bb2cf59845be056e4493ba0cc34db4c7a5701f8c37b248d0e2f91bbbbaee16bb712909dcdb681a44c96760b1e1c9b96d19ea8f773bb5f0703343ed5734452e396b2c93d9b62555961aa4ea693e0b065d0d870b4d11a104cd5b47cc44fa279ea9bd99934c69946daf1c340cc0161cf735a13a95eebc3aa9a9dee4c0e288d733af72a17e21616f5c1b9dec10ac51c7198703abb065025551b8f962739ea0f79e8ebb5881ba712ce46e838a0378f5a77bf68d73f6e8e4e0802b53488186e5f1edefa5bc0471a1c0073872d298dbf1b3edd5a00fa4ef3ce5e77dabb419024fb7b2d9a2a9b63dc60d8b07f3b990505f221163a09ec86a3f53b3545dd3b723ed61bb826cba04e2cfe4fc5be3ad0831b41b7560e7b8e1b2b3bbd1a020240e1fc371be88fa9b916758b83b66a1f7c97154e75501c533465d71125134728002dfcd5e5abbafc266afc4d7a11bd0bd7b57d16bc6253d0dd15c8915629468b3e5d74b72dc1009003717edbc9774aa6b1f98d7d9a6fb3295bb5b8bf25d2e32f56b38449b6edb9f9438effc2338f51be1c761fa1fc914191442fd02f2e66e3156eed3fa9604e60f03ce4e23c57e8ebc8c28eef1bfe0001a147010701fd05b15c2c7737036235a6889a24f7800f762660d84273f5926940b4452af3618f592c000000000000000100000000042c1d80c26f35564fc94a766748f4caec35780ca53dcd2733cafd6e8df7c72d2bf1e23300061ae000028e624e0fc1b9235c5d3495cbf6c4df9a276d66f58d34efd15795ab337d41485324f09b63dd4ee8313cb4762cd781c7fcacdf97e01e6ca57fe04705aafda4e6d66a20b431204adcb6074d16916a4ceaf9c1489345e3979065059fe85488a9a1a9d44e3b8c278d8e9a2f5b0f18464f96211bb8393e198ea8a5b1d8bc170e4813be8707b3d885414b2759c055fd8e82fbf7d1fa49884357730b97f6596dac8042574fa3c2d25ae5db6d59be1838994b452e48e84a2c53cb44f15d75031b38fdcd2fb2c98caa7240fbf99302773b65967ac906ef24a9df5f7ce251568fbdca07f23b6996adfa6f9e87c495a04f5b01bd1f7cbde83fa5912069f40ae5f203fff8696b4459215e6873d72d23fb2ac91452e065bd68c69cb02f55bf581f59dc77c8a19ddb234e549fe4ae1864227a3fea6bc2f2d18dc429795d275acd4435c7d2e8a8db6814f28385cbd158c0a15eebbee9ff1020721957faefe463a1062c599d59dd2e01643cca443633cf5cae89a37ca44b51a1a3c38c426598c0ed6ccbd6fab91177c1b7d1b3dd4d4ffe931fc04e8e98f1181e8524199d0c858c2b717e22c10d99f3feebda4d5a8bda90edecd4bd25ee24d5a339a1269b5419287da71c140686f7d56664aa04c9e14c0ba895004fad753e1602068cf9b07029c76ae501d2aff758111e9d36251e9ec8edcb516b7e63a41db808f55975685245f55a3553593094826af437f8c3d469aa51fc1c3e9c4dd1b0f2bf61b94c9056cf6b4630a37a2a128d493c03b521331e77829d27e4874b8ce87a504d78cd9e914830055baa45a34d204a79a6ced1fc7018276c1b2d813804e5f379505e9667bb5cdc550d2b4a93082b1ed488cba30851469d0deda5bdf7d7b7641031508dc44e12e943cf8e3b03858dc15cde71acbb01e9a48a09b5519cbff82f80202177b5bfc81ac6325e1f5d0cce972edada51ae26b3f6863028f4c5d5ef92caabcde2e10a666885eccc107d95160df4d449cbcc49e9d0cf66dcea562b69086682ede3324bd354043e57f209052bbe40cb93a8c956aa9630dd2bf0a3e8ae537bbd90f9d6f5044268d2c19772e86738402990b3112ce638f0b796f87be59477260a22321c279876c0e2c782da291f9cf4602b0b9710e1c70705d7e6f428d980d803ec5430c3bbc4398369e4dff4de84750d8310d0c27483aa9b4140e52b9a19d31c1592b68bc88d2519365ded2353fa83c8a1fabd4764b332a0979c262fcb1375aa6d4abba213657a57c42680445c6e068cb85cdfefa340d531faafe553161cb3b648c93417b77ac03ebcfed7d90a7b84a0a64720e8565943b357480c97c886ff2812d4f69cebfadd788cf43df4d43980d746aad28a61e7991e208185cc4609ed8b4d6bff93b78329b91860c133bbd576a2a4e66b55e3aab8444cd32d05f0ec063e01c306737336988309750dcb0bf43b5b963a8026f78fc74c722940f8e497255bcfece6efa397badabbf44f84b90fe3072b952d8b090777acec2733f9ba47294349d562b041ab622cadc0cd6ae63dde109e8af48638a9f0c481b30a4070a48ea7e3a195c4677f8d635f31007bc20cc5aba4d98b55145b564432126c6bd9f96bddb5d1ea12fbd4c5da1eac5ff2e20fb7f09b0dd52739c65557e6403f03dce377fde94d3ca5155b62174e94f28397dfa238b4ef2adb5c7c79f3f6d623b2d1f2d4362ca5c05305eea5e8c95753400b55701465d19e9ea95e8cbf3c3a0186eca09b4f5dcb3d15d1f8314c1903734a6a5310e695e3e91c6ba43fe55397bb7914b5c467c015a1db67d88ffcf3d809d2669190f2aaca2632cec272d506607e7413c86ab2d7913b59c4a4ec9544b413997215b9358cc4f286c550a569a3b4e6a553761c64a30fc08cd46b6212041887b2971df4bc988334a829ee92717bc3dbf8aec71bbda32efe0001a147010702fd05b15c2c7737036235a6889a24f7800f762660d84273f5926940b4452af3618f592c00000000000000030000000005a995c0d1e829925392fdaa8e472d7262d40a076af9bfbafb46d04c86715cfa017e0ca900061aa00002065ea89fa5a1b90013656bd306dae4c0b94155a98f35fe853a418e512bbbaa2aea4ceae2e8235c468f78bb7c7e9dd286246b5e27276c3c7ebc7ed54c5359265eb9bb8b80e4bcd9cfbaf7f2a923d3d906f2bcc75b12113873352f8bc9f195bf9dd2ffcf04173b428dbfa501f72a9ae75015fcc7bf8bba95e2f2734492b1f7df70887471413fed6e9e688b96cc1605344b6104122f5e6bc9c9ae779a66d026f3e069304d939fee334f79313d916a665a3e57ab7a72ea988743b0ce552c02194fef1f8870f79bc972c31aaa47ae61bb0a39af33f6ed1407398b379db153f46934b6158dfb19c6c5896afb89bc01740de868bcb7c7c60afd7bed6acf38de73e45f1db62164c41af45d89eb605373de2dd85733dec745a31624cec2b45a15d41822f4618ee986d1db96c02a1d3e1dc02c66880245f522c2dce55a54847bd370634237c39ffdd466e7151f57986fc32448172859e7ed6b9f0132c98429e622370e446a0a954b8e6e11012de38e0661844ea0df4854355eb8ae8b8027020e24131eff0761871f71d09d5f5d56615a5eecd061b3cf8c4230f6efde1d15556850d0d356aac04db5898e71a5496235a8ad8b60c509f473b104b05c60f40e0c8a0f7bb2fe8c5f896765fea4a53bd015ab9ea643584a51648155ed0f90f0f2ba7d9ad5cb52bdfb60a8553d626c7a28795df2dd08e012d370fefa8c7144d8df62ee2f643a8d767265c6da396418be04771cb1a602f752205593e81ae6f6a338129fa7dcf7f9af440ab24b7e01973fcb733ec794ffb2ff308c398bc50b089df4d4f3cb69649df5ded4f679821b4dd37623bd65853da74e6ac5ad754005095d401c07996c4aa7b6937d4cf7834952ca691488dd5b4b831e7042a17acbe0a6ab853d4f2012e6960528ef54762d37a3bd651507732c38b26efd83019a0461a5e36ea06969ea94ec2ec2da608c7e3cfd40a6d5ad03527027aacb938e4820e6390dc19b3d6e27c47d058c2e328b6dfc56d811befdd8abe919f972dbd9320b91d0b134d018f2dace97549f10055758994f81613f37fedfc8116e62e67be952083b736f55c1488af1ab591dea8dd18e64605149280cd2c45e86c4f0566678a84dac554d2e100cbc1edc87637a1c0f8e8914e19daf7e123c3eda58f6b6677fbcd4b67f0b650b8eb03f061854c0d1d2e595546f7dc887bac5ab2c83d26391a87611d828e34be55ec2cf577acae1cc477e9c3b161c899be80016890524551e6309bde6f5d66ba264f57d6d50aeeb0bf80a23e2c8902aebea29238f70f2cc45a519e49cd59aef6cdb216f9b0d62733b5e15b41ebcffc94e93fcdc683d2f826fb7fffd671a8e3951b37f4f86d187520e3307a0ba76ee17167f3aa036806a4a0175bd2667a4b89522f71cccac33245ad08aea005b3701faecbd8f2ea6557610c62660748019f683b01fcd4144a6db868ad17c4bd6615f64d3ebbb6ce9cce3d268cc335a562c7a9aaa55c65361a89da437167e0a0611a49859f70b638fa4020c28b10d2a3f35abb87e6cac847d99c69e05417d96cc6b8ed237a7399c256aa106930c4307f76bde0fdad1796cbcc6d3e767ccecd257e1887fbfcd8ff60f6b3c7dd8758c8082ae9e7f5578eb9802fde18b07739672529d680bd197947e2114c24746d2fff6f718d7e8e67067da65ab02ab2700957fd907397b92bbcb407685a9e1063f5d0ded6aa9f24c55d353c7ff1fae9c152f7510e414e6e48c0d4023ad488925302ef7b2d89e4f6e2882c7178407b543f36cb2400900e3e636f450b4249712d2c38f9348360e4a0045b3908b9af2e89fc35942957ef1d9385b0180fb8bd00de51b257f3f6883d59ad99600f5ed87a4efe503ec4774eb08c08353565fedfbb6dc283ae1ea4bbe67becc89c99d333c245e44e8d94be0e29248a1c27d0fc9d1f0b004fe0001a14701070001000000000000000000000000245c2c7737036235a6889a24f7800f762660d84273f5926940b4452af3618f592c0000000000000000000f424003225d493749e2dbe358ca150eeba2b0aec52e759b8348b1525860bff4104fe88b0400012401010101010101010101010101010101010101010101010101010101010101012a0000002b40420f00000000002200204a51884029b41316673ca69eede0a3b20e0b7f42bb50d7229bcb9d23ea50ba26061a8000002a000000000202000000000000044c0000000000000000000000001dcd6500006402d000000000000000070002010000000000000000010000000000000001000009c4000000000bebc2000000000024f473006b1c3e0ad998f0a14b2248ca1c8ceb6144e2c823f222c2aa7b0b33a10dfb0ab7019e03ac3db96ecd8a79523c185e0d82db3888caf3a6583c055cdc78186ffc7d406041853bf8377a6b50c7a9dd678430402bc8dd6c84841427521df1b8df7f4ce50002ed24442ed6b5700dd1dbe76f59a916d443ff6ece1a1c685a984aff68082a81543cf5698d01a1b162248493c728747c56536960ef806e95cad89a8cb770130888024e450eebfe166eeb1f651a8e6f36c2249b665b0d6b7dc2423fb1b184759d0426204939ce5393237f1d6abb0663fff253efa21fc652a7e88891ed556233d53a00000000000003e800000000000003e8000000003b9aca00001e009000000000000000070002020000000000000000020000000000000001000009c40000000024f47300000000000bebc2005f43968d71b783dc165183a86b556a5c27bfb98ab9e336a95a84782d784d8e03035633114affbbf4e73ef2ac151b85b2a5243c7e415adba3226993fb8abff2c0dffffd01225c2c7737036235a6889a24f7800f762660d84273f5926940b4452af3618f592cc529e2e61df2eb2b4c3d3643d9eb943418f80e823e69ed3f5c63ae59133bfa2a380435ea9497c29f203be09b0f91a686643304fe6af81aac7866746bd7609eeb0003c7c26d3863d250cad6a2254caf3a7f64abb9ae4b1e625be36351141a657c688b798ac175a1d08f3178dcda8116329836b02f005457558bf475674757602b4b77c3de6bb5351e35f19b6eaeb9d908a8a8cb6ec8535292b21dd9d5c2e27afb46992c5b35e16947641671c7663da11d8f3feedda9e7edbdff8d9f5e784a6ae23d3fc845d12e41e66fa06bccb50d46a0d2eff9bac3efb9cf8a4d574da6887229f0a07bf72946898b66de21e359ba1bca8292de0549ac01d307d58d65eb02ddfdfd5600000000000000080003020000000000000000020000000000000001010000000000000003000009c40000000024f473000000000006422c4072f6afa3783bab27ca0a19973ef43f850a396785f2bf8e8e96f9307667df1a430292edee13bca46cf7456ed9b3f4d13d867c985a5e02a2ccdfe8c28d45d61f487400000000000000000000070003003e0000fffffffffffc00826ee2502d3cb343e3085b9e2f9c4df2cf30e9ee5071ad70b731577e8c31950b1400fc0003ffffffffffe8010425254644b36119794a6829740f5d16f64727b35d213aafd9ace115e71b1a10c002000007ffffffffffc80107cf8fa14ad1ec30b4f3e95f1e1d28eea17773d0fdcc518deed1096f0120c999240003ffffffffffe40001000000000000000300012fc0fae746fc44e2aa3d2e320e914edf0000061a80160014761879f7b274ce995f87150a02e75cc0c037e8e3000000000000fffd023e020000000001015c2c7737036235a6889a24f7800f762660d84273f5926940b4452af3618f592c00000000003aded680074a0100000000000022002093e3333850fc25f89d3b12559c85a59e019696be79850060d32496b0bb965fa24a01000000000000220020a2a4341b53b559d097075985ed615f5dde505ca6e5499e60aca8b1e7afc0a8eb7011010000000000220020ff11b843ca0f23499ea981b499a692eb5710d8f1b81dfcf8be63794aab3ecb9d1873010000000000220020fca6db057c1604c90cbaaba41d259ddd7ae59c4dfc6fac7d1012c73bc52517e890870100000000002200209ddc805a7fb981f4817f167988e962ebfa6aacf24709ddd981f35efce0ad3f16b0ad010000000000220020c1c5d3e73ebf3afd25e94098cabd8029f0026a7da71b8c738c4dc16d34e3df41e075090000000000220020d0713fc025cc90b7a040bb24de693a02d8af7c92e205ff7365d3c0253a2d3a8c0400483045022100c529e2e61df2eb2b4c3d3643d9eb943418f80e823e69ed3f5c63ae59133bfa2a0220380435ea9497c29f203be09b0f91a686643304fe6af81aac7866746bd7609eeb01483045022100917e62cc460a695e3a20679c295367e3f4fc66039231ef2842946848a1ae029402205ab9872085b94076367e6c6ec370debedd4282be400a6446bbab1e1f226429cb01475221027ed31ec818a08099c8d1eb9e35a52462559aedea40addd4dbb2995a6811fe3dc2103225d493749e2dbe358ca150eeba2b0aec52e759b8348b1525860bff4104fe88b52aea2362b20ff24431adf677630f9968e8ebff28567390a853ff43e97190aca27ab3b78a3aff67204000000ff24431adf677630f9968e8ebff28567390a853ff43e97190aca27ab3b78a3aff67200000000000124431adf677630f9968e8ebff28567390a853ff43e97190aca27ab3b78a3aff672050000000000000000000000000124431adf677630f9968e8ebff28567390a853ff43e97190aca27ab3b78a3aff6720300000000000000000000030003245c2c7737036235a6889a24f7800f762660d84273f5926940b4452af3618f592c00000000fd023e020000000001015c2c7737036235a6889a24f7800f762660d84273f5926940b4452af3618f592c00000000003aded680074a0100000000000022002093e3333850fc25f89d3b12559c85a59e019696be79850060d32496b0bb965fa24a01000000000000220020a2a4341b53b559d097075985ed615f5dde505ca6e5499e60aca8b1e7afc0a8eb7011010000000000220020ff11b843ca0f23499ea981b499a692eb5710d8f1b81dfcf8be63794aab3ecb9d1873010000000000220020fca6db057c1604c90cbaaba41d259ddd7ae59c4dfc6fac7d1012c73bc52517e890870100000000002200209ddc805a7fb981f4817f167988e962ebfa6aacf24709ddd981f35efce0ad3f16b0ad010000000000220020c1c5d3e73ebf3afd25e94098cabd8029f0026a7da71b8c738c4dc16d34e3df41e075090000000000220020d0713fc025cc90b7a040bb24de693a02d8af7c92e205ff7365d3c0253a2d3a8c0400483045022100c529e2e61df2eb2b4c3d3643d9eb943418f80e823e69ed3f5c63ae59133bfa2a0220380435ea9497c29f203be09b0f91a686643304fe6af81aac7866746bd7609eeb01483045022100917e62cc460a695e3a20679c295367e3f4fc66039231ef2842946848a1ae029402205ab9872085b94076367e6c6ec370debedd4282be400a6446bbab1e1f226429cb01475221027ed31ec818a08099c8d1eb9e35a52462559aedea40addd4dbb2995a6811fe3dc2103225d493749e2dbe358ca150eeba2b0aec52e759b8348b1525860bff4104fe88b52aea2362b2024431adf677630f9968e8ebff28567390a853ff43e97190aca27ab3b78a3aff67200000000330200000001431adf677630f9968e8ebff28567390a853ff43e97190aca27ab3b78a3aff672000000000000000000000000000024431adf677630f9968e8ebff28567390a853ff43e97190aca27ab3b78a3aff67204000000c302000000000101431adf677630f9968e8ebff28567390a853ff43e97190aca27ab3b78a3aff672040000000001000000014c76010000000000160014761879f7b274ce995f87150a02e75cc0c037e8e30247304402204f334829ddc5b302159133c5a84dd9ead6bbc418e44cf334fd558acd3ea4a956022048cd5779bb4eaf929d504e6eaed3c418420fc3c655312cc63076df0ccc33cdfa01252103272e55246b81e20bd899c510f5fc7678ecbc764687b8a2ded3a2a7a9212c453fad51b20000000000000000 \ No newline at end of file diff --git a/eclair-core/src/test/resources/nonreg/codecs/05000a-DATA_CLOSING/next-remote/data.json b/eclair-core/src/test/resources/nonreg/codecs/05000a-DATA_CLOSING/next-remote/data.json new file mode 100644 index 0000000000..8a2b872db9 --- /dev/null +++ b/eclair-core/src/test/resources/nonreg/codecs/05000a-DATA_CLOSING/next-remote/data.json @@ -0,0 +1,257 @@ +{ + "type" : "DATA_CLOSING", + "commitments" : { + "channelParams" : { + "channelId" : "5c2c7737036235a6889a24f7800f762660d84273f5926940b4452af3618f592c", + "channelConfig" : [ "funding_pubkey_based_channel_keypath" ], + "channelFeatures" : [ ], + "localParams" : { + "nodeId" : "02aaaa00ce2f18a967dc4f25f414e671ba6585f8ef0b8c5fb812c21064f55a2eaa", + "fundingKeyPath" : [ 186482018, 3110590200, 2500399955, 3007571504, 2931295319, 2056590083, 908246696, 1579011473, 2147483649 ], + "initialRequestedChannelReserve_opt" : 10000, + "isChannelOpener" : true, + "paysCommitTxFees" : true, + "initFeatures" : { + "activated" : { + "option_route_blinding" : "optional", + "option_provide_storage" : "optional", + "splice_prototype" : "optional", + "payment_secret" : "mandatory", + "gossip_queries_ex" : "optional", + "option_anchor_outputs" : "optional", + "option_quiesce" : "optional", + "option_data_loss_protect" : "optional", + "var_onion_optin" : "mandatory", + "option_static_remotekey" : "optional", + "option_support_large_channel" : "optional", + "option_anchors_zero_fee_htlc_tx" : "optional", + "option_channel_type" : "mandatory", + "basic_mpp" : "optional", + "gossip_queries" : "optional" + }, + "unknown" : [ 50001 ] + } + }, + "remoteParams" : { + "nodeId" : "02bbbb671d15145722fb8c28d732cddb249bcc6652ed2b297ff1f77a18371b1e63", + "initialRequestedChannelReserve_opt" : 20000, + "revocationBasepoint" : "0395e320d7df69bfbc0b2d1f559010bcc3569b166b5219a69d0722c51dbde0b3b2", + "paymentBasepoint" : "02588a7e8f90381590f3defcf757807e0aded5cf93d500b2e0e9939f86c9fd8c3f", + "delayedPaymentBasepoint" : "02771ae3d3cb51eeca4470412e54aab30e2ce629204562f5baeee1380f46da2374", + "htlcBasepoint" : "023c4c494f182e78a74d9ca0233cc8db3841c6c4fcba75c736d15f8da2bd305bc8", + "initFeatures" : { + "activated" : { + "option_route_blinding" : "optional", + "splice_prototype" : "optional", + "payment_secret" : "mandatory", + "gossip_queries_ex" : "optional", + "option_anchor_outputs" : "optional", + "option_quiesce" : "optional", + "option_data_loss_protect" : "optional", + "var_onion_optin" : "mandatory", + "option_static_remotekey" : "optional", + "option_support_large_channel" : "optional", + "option_anchors_zero_fee_htlc_tx" : "optional", + "option_channel_type" : "mandatory", + "basic_mpp" : "optional", + "gossip_queries" : "optional" + }, + "unknown" : [ ] + } + }, + "channelFlags" : { + "nonInitiatorPaysCommitFees" : false, + "announceChannel" : false + } + }, + "changes" : { + "localChanges" : { + "proposed" : [ { + "channelId" : "5c2c7737036235a6889a24f7800f762660d84273f5926940b4452af3618f592c", + "id" : 0, + "paymentPreimage" : "a906e024f5cf088b1b4115cc38d30e8f24f8465bb00cc0f8f145ec3bd638b912", + "tlvStream" : { } + }, { + "channelId" : "5c2c7737036235a6889a24f7800f762660d84273f5926940b4452af3618f592c", + "id" : 1, + "reason" : "", + "tlvStream" : { } + } ], + "signed" : [ { + "channelId" : "5c2c7737036235a6889a24f7800f762660d84273f5926940b4452af3618f592c", + "id" : 3, + "amountMsat" : 95000000, + "paymentHash" : "d1e829925392fdaa8e472d7262d40a076af9bfbafb46d04c86715cfa017e0ca9", + "cltvExpiry" : 400032, + "onionRoutingPacket" : { + "version" : 0, + "publicKey" : "02065ea89fa5a1b90013656bd306dae4c0b94155a98f35fe853a418e512bbbaa2a", + "payload" : "ea4ceae2e8235c468f78bb7c7e9dd286246b5e27276c3c7ebc7ed54c5359265eb9bb8b80e4bcd9cfbaf7f2a923d3d906f2bcc75b12113873352f8bc9f195bf9dd2ffcf04173b428dbfa501f72a9ae75015fcc7bf8bba95e2f2734492b1f7df70887471413fed6e9e688b96cc1605344b6104122f5e6bc9c9ae779a66d026f3e069304d939fee334f79313d916a665a3e57ab7a72ea988743b0ce552c02194fef1f8870f79bc972c31aaa47ae61bb0a39af33f6ed1407398b379db153f46934b6158dfb19c6c5896afb89bc01740de868bcb7c7c60afd7bed6acf38de73e45f1db62164c41af45d89eb605373de2dd85733dec745a31624cec2b45a15d41822f4618ee986d1db96c02a1d3e1dc02c66880245f522c2dce55a54847bd370634237c39ffdd466e7151f57986fc32448172859e7ed6b9f0132c98429e622370e446a0a954b8e6e11012de38e0661844ea0df4854355eb8ae8b8027020e24131eff0761871f71d09d5f5d56615a5eecd061b3cf8c4230f6efde1d15556850d0d356aac04db5898e71a5496235a8ad8b60c509f473b104b05c60f40e0c8a0f7bb2fe8c5f896765fea4a53bd015ab9ea643584a51648155ed0f90f0f2ba7d9ad5cb52bdfb60a8553d626c7a28795df2dd08e012d370fefa8c7144d8df62ee2f643a8d767265c6da396418be04771cb1a602f752205593e81ae6f6a338129fa7dcf7f9af440ab24b7e01973fcb733ec794ffb2ff308c398bc50b089df4d4f3cb69649df5ded4f679821b4dd37623bd65853da74e6ac5ad754005095d401c07996c4aa7b6937d4cf7834952ca691488dd5b4b831e7042a17acbe0a6ab853d4f2012e6960528ef54762d37a3bd651507732c38b26efd83019a0461a5e36ea06969ea94ec2ec2da608c7e3cfd40a6d5ad03527027aacb938e4820e6390dc19b3d6e27c47d058c2e328b6dfc56d811befdd8abe919f972dbd9320b91d0b134d018f2dace97549f10055758994f81613f37fedfc8116e62e67be952083b736f55c1488af1ab591dea8dd18e64605149280cd2c45e86c4f0566678a84dac554d2e100cbc1edc87637a1c0f8e8914e19daf7e123c3eda58f6b6677fbcd4b67f0b650b8eb03f061854c0d1d2e595546f7dc887bac5ab2c83d26391a87611d828e34be55ec2cf577acae1cc477e9c3b161c899be80016890524551e6309bde6f5d66ba264f57d6d50aeeb0bf80a23e2c8902aebea29238f70f2cc45a519e49cd59aef6cdb216f9b0d62733b5e15b41ebcffc94e93fcdc683d2f826fb7fffd671a8e3951b37f4f86d187520e3307a0ba76ee17167f3aa036806a4a0175bd2667a4b89522f71cccac33245ad08aea005b3701faecbd8f2ea6557610c62660748019f683b01fcd4144a6db868ad17c4bd6615f64d3ebbb6ce9cce3d268cc335a562c7a9aaa55c65361a89da437167e0a0611a49859f70b638fa4020c28b10d2a3f35abb87e6cac847d99c69e05417d96cc6b8ed237a7399c256aa106930c4307f76bde0fdad1796cbcc6d3e767ccecd257e1887fbfcd8ff60f6b3c7dd8758c8082ae9e7f5578eb9802fde18b07739672529d680bd197947e2114c24746d2fff6f718d7e8e67067da65ab02ab2700957fd907397b92bbcb407685a9e1063f5d0ded6aa9f24c55d353c7ff1fae9c152f7510e414e6e48c0d4023ad488925302ef7b2d89e4f6e2882c7178407b543f36cb2400900e3e636f450b4249712d2c38f9348360e4a0045b3908b9af2e89fc35942957ef1d9385b0180fb8bd00de51b257f3f6883d59ad99600f5ed87a4efe503ec4774eb08c08353565fedfbb6dc28", + "hmac" : "3ae1ea4bbe67becc89c99d333c245e44e8d94be0e29248a1c27d0fc9d1f0b004" + }, + "tlvStream" : { + "Endorsement" : { + "level" : 7 + } + } + } ], + "acked" : [ ] + }, + "remoteChanges" : { + "proposed" : [ ], + "acked" : [ ], + "signed" : [ ] + }, + "localNextHtlcId" : 4, + "remoteNextHtlcId" : 2 + }, + "active" : [ { + "fundingTxIndex" : 0, + "fundingInput" : "2c598f61f32a45b4406992f57342d86026760f80f7249a88a635620337772c5c:0", + "fundingAmount" : 1000000, + "localFunding" : { + "status" : "confirmed", + "shortChannelId" : "400000x42x0" + }, + "remoteFunding" : { + "status" : "locked" + }, + "commitmentFormat" : "anchor_outputs", + "localCommitParams" : { + "dustLimit" : 1100, + "htlcMinimum" : 0, + "maxHtlcValueInFlight" : 500000000, + "maxAcceptedHtlcs" : 100, + "toSelfDelay" : 720 + }, + "localCommit" : { + "index" : 7, + "spec" : { + "htlcs" : [ { + "direction" : "IN", + "id" : 0, + "amountMsat" : 110000000, + "paymentHash" : "04245ebd50c1ad4f0af1ebe00d8ac493f92150325ccd88ca4eab02e16b5db3c6", + "cltvExpiry" : 400064 + }, { + "direction" : "IN", + "id" : 1, + "amountMsat" : 70000000, + "paymentHash" : "c26f35564fc94a766748f4caec35780ca53dcd2733cafd6e8df7c72d2bf1e233", + "cltvExpiry" : 400096 + } ], + "commitTxFeerate" : 2500, + "toLocal" : 200000000, + "toRemote" : 620000000 + }, + "txId" : "6b1c3e0ad998f0a14b2248ca1c8ceb6144e2c823f222c2aa7b0b33a10dfb0ab7", + "remoteSig" : { + "sig" : "9e03ac3db96ecd8a79523c185e0d82db3888caf3a6583c055cdc78186ffc7d406041853bf8377a6b50c7a9dd678430402bc8dd6c84841427521df1b8df7f4ce5" + }, + "htlcRemoteSigs" : [ "ed24442ed6b5700dd1dbe76f59a916d443ff6ece1a1c685a984aff68082a81543cf5698d01a1b162248493c728747c56536960ef806e95cad89a8cb770130888", "024e450eebfe166eeb1f651a8e6f36c2249b665b0d6b7dc2423fb1b184759d0426204939ce5393237f1d6abb0663fff253efa21fc652a7e88891ed556233d53a" ] + }, + "remoteCommitParams" : { + "dustLimit" : 1000, + "htlcMinimum" : 1000, + "maxHtlcValueInFlight" : 1000000000, + "maxAcceptedHtlcs" : 30, + "toSelfDelay" : 144 + }, + "remoteCommit" : { + "index" : 7, + "spec" : { + "htlcs" : [ { + "direction" : "OUT", + "id" : 0, + "amountMsat" : 110000000, + "paymentHash" : "04245ebd50c1ad4f0af1ebe00d8ac493f92150325ccd88ca4eab02e16b5db3c6", + "cltvExpiry" : 400064 + }, { + "direction" : "OUT", + "id" : 1, + "amountMsat" : 70000000, + "paymentHash" : "c26f35564fc94a766748f4caec35780ca53dcd2733cafd6e8df7c72d2bf1e233", + "cltvExpiry" : 400096 + } ], + "commitTxFeerate" : 2500, + "toLocal" : 620000000, + "toRemote" : 200000000 + }, + "txId" : "5f43968d71b783dc165183a86b556a5c27bfb98ab9e336a95a84782d784d8e03", + "remotePerCommitmentPoint" : "035633114affbbf4e73ef2ac151b85b2a5243c7e415adba3226993fb8abff2c0df" + }, + "nextRemoteCommit" : { + "index" : 8, + "spec" : { + "htlcs" : [ { + "direction" : "OUT", + "id" : 0, + "amountMsat" : 110000000, + "paymentHash" : "04245ebd50c1ad4f0af1ebe00d8ac493f92150325ccd88ca4eab02e16b5db3c6", + "cltvExpiry" : 400064 + }, { + "direction" : "OUT", + "id" : 1, + "amountMsat" : 70000000, + "paymentHash" : "c26f35564fc94a766748f4caec35780ca53dcd2733cafd6e8df7c72d2bf1e233", + "cltvExpiry" : 400096 + }, { + "direction" : "IN", + "id" : 3, + "amountMsat" : 95000000, + "paymentHash" : "d1e829925392fdaa8e472d7262d40a076af9bfbafb46d04c86715cfa017e0ca9", + "cltvExpiry" : 400032 + } ], + "commitTxFeerate" : 2500, + "toLocal" : 620000000, + "toRemote" : 105000000 + }, + "txId" : "72f6afa3783bab27ca0a19973ef43f850a396785f2bf8e8e96f9307667df1a43", + "remotePerCommitmentPoint" : "0292edee13bca46cf7456ed9b3f4d13d867c985a5e02a2ccdfe8c28d45d61f4874" + } + } ], + "inactive" : [ ], + "remoteNextCommitInfo" : { + "sentAfterLocalCommitIndex" : 7 + }, + "remotePerCommitmentSecrets" : null, + "originChannels" : { + "3" : { + "paymentId" : "2fc0fae7-46fc-44e2-aa3d-2e320e914edf" + } + } + }, + "waitingSince" : 400000, + "finalScriptPubKey" : "0014761879f7b274ce995f87150a02e75cc0c037e8e3", + "mutualCloseProposed" : [ ], + "mutualClosePublished" : [ ], + "nextRemoteCommitPublished" : { + "commitTx" : { + "txid" : "72f6afa3783bab27ca0a19973ef43f850a396785f2bf8e8e96f9307667df1a43", + "tx" : "020000000001015c2c7737036235a6889a24f7800f762660d84273f5926940b4452af3618f592c00000000003aded680074a0100000000000022002093e3333850fc25f89d3b12559c85a59e019696be79850060d32496b0bb965fa24a01000000000000220020a2a4341b53b559d097075985ed615f5dde505ca6e5499e60aca8b1e7afc0a8eb7011010000000000220020ff11b843ca0f23499ea981b499a692eb5710d8f1b81dfcf8be63794aab3ecb9d1873010000000000220020fca6db057c1604c90cbaaba41d259ddd7ae59c4dfc6fac7d1012c73bc52517e890870100000000002200209ddc805a7fb981f4817f167988e962ebfa6aacf24709ddd981f35efce0ad3f16b0ad010000000000220020c1c5d3e73ebf3afd25e94098cabd8029f0026a7da71b8c738c4dc16d34e3df41e075090000000000220020d0713fc025cc90b7a040bb24de693a02d8af7c92e205ff7365d3c0253a2d3a8c0400483045022100c529e2e61df2eb2b4c3d3643d9eb943418f80e823e69ed3f5c63ae59133bfa2a0220380435ea9497c29f203be09b0f91a686643304fe6af81aac7866746bd7609eeb01483045022100917e62cc460a695e3a20679c295367e3f4fc66039231ef2842946848a1ae029402205ab9872085b94076367e6c6ec370debedd4282be400a6446bbab1e1f226429cb01475221027ed31ec818a08099c8d1eb9e35a52462559aedea40addd4dbb2995a6811fe3dc2103225d493749e2dbe358ca150eeba2b0aec52e759b8348b1525860bff4104fe88b52aea2362b20" + }, + "localOutput_opt" : "72f6afa3783bab27ca0a19973ef43f850a396785f2bf8e8e96f9307667df1a43:4", + "anchorOutput_opt" : "72f6afa3783bab27ca0a19973ef43f850a396785f2bf8e8e96f9307667df1a43:0", + "incomingHtlcs" : { + "72f6afa3783bab27ca0a19973ef43f850a396785f2bf8e8e96f9307667df1a43:5" : 0 + }, + "outgoingHtlcs" : { + "72f6afa3783bab27ca0a19973ef43f850a396785f2bf8e8e96f9307667df1a43:3" : 3 + }, + "irrevocablySpent" : { + "2c598f61f32a45b4406992f57342d86026760f80f7249a88a635620337772c5c:0" : { + "txid" : "72f6afa3783bab27ca0a19973ef43f850a396785f2bf8e8e96f9307667df1a43", + "tx" : "020000000001015c2c7737036235a6889a24f7800f762660d84273f5926940b4452af3618f592c00000000003aded680074a0100000000000022002093e3333850fc25f89d3b12559c85a59e019696be79850060d32496b0bb965fa24a01000000000000220020a2a4341b53b559d097075985ed615f5dde505ca6e5499e60aca8b1e7afc0a8eb7011010000000000220020ff11b843ca0f23499ea981b499a692eb5710d8f1b81dfcf8be63794aab3ecb9d1873010000000000220020fca6db057c1604c90cbaaba41d259ddd7ae59c4dfc6fac7d1012c73bc52517e890870100000000002200209ddc805a7fb981f4817f167988e962ebfa6aacf24709ddd981f35efce0ad3f16b0ad010000000000220020c1c5d3e73ebf3afd25e94098cabd8029f0026a7da71b8c738c4dc16d34e3df41e075090000000000220020d0713fc025cc90b7a040bb24de693a02d8af7c92e205ff7365d3c0253a2d3a8c0400483045022100c529e2e61df2eb2b4c3d3643d9eb943418f80e823e69ed3f5c63ae59133bfa2a0220380435ea9497c29f203be09b0f91a686643304fe6af81aac7866746bd7609eeb01483045022100917e62cc460a695e3a20679c295367e3f4fc66039231ef2842946848a1ae029402205ab9872085b94076367e6c6ec370debedd4282be400a6446bbab1e1f226429cb01475221027ed31ec818a08099c8d1eb9e35a52462559aedea40addd4dbb2995a6811fe3dc2103225d493749e2dbe358ca150eeba2b0aec52e759b8348b1525860bff4104fe88b52aea2362b20" + }, + "72f6afa3783bab27ca0a19973ef43f850a396785f2bf8e8e96f9307667df1a43:0" : { + "txid" : "56f981183ef8612132e35563e9be6107baf2d55a7e053d0e12de65f5c2214109", + "tx" : "0200000001431adf677630f9968e8ebff28567390a853ff43e97190aca27ab3b78a3aff6720000000000000000000000000000" + }, + "72f6afa3783bab27ca0a19973ef43f850a396785f2bf8e8e96f9307667df1a43:4" : { + "txid" : "89a392cf2d36015f177a9ddf04edc74d0802f5accfad072b955a61d8a534a7e7", + "tx" : "02000000000101431adf677630f9968e8ebff28567390a853ff43e97190aca27ab3b78a3aff672040000000001000000014c76010000000000160014761879f7b274ce995f87150a02e75cc0c037e8e30247304402204f334829ddc5b302159133c5a84dd9ead6bbc418e44cf334fd558acd3ea4a956022048cd5779bb4eaf929d504e6eaed3c418420fc3c655312cc63076df0ccc33cdfa01252103272e55246b81e20bd899c510f5fc7678ecbc764687b8a2ded3a2a7a9212c453fad51b200000000" + } + } + }, + "revokedCommitPublished" : [ ] +} \ No newline at end of file diff --git a/eclair-core/src/test/resources/nonreg/codecs/05000a-DATA_CLOSING/remote/data.bin b/eclair-core/src/test/resources/nonreg/codecs/05000a-DATA_CLOSING/remote/data.bin new file mode 100644 index 0000000000..be5ea6d724 --- /dev/null +++ b/eclair-core/src/test/resources/nonreg/codecs/05000a-DATA_CLOSING/remote/data.bin @@ -0,0 +1 @@ +05000a01517d2e413b334da125c59240368a0cf601d3b661f0bb14841900cfb34c5ced3101010002aaaa00ce2f18a967dc4f25f414e671ba6585f8ef0b8c5fb812c21064f55a2eaa000992ef887f0fd1dd4d1be6dd1189524eff9c976fd02840fbe53e95ff21faa7df6380000001ff0000000000002710c000000000186b0200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000180802aa698202bbbb671d15145722fb8c28d732cddb249bcc6652ed2b297ff1f77a18371b1e63ff0000000000004e2002c3c29dc57978b69aa8f3d483b54ce7e9e08fef3a556f854ab7f93d9c56d504e503cbfd24887a5fb7f8d8e6ca0c652a70a809f4044faf58ee39a02a3a98acc50253023f449e1a43e11ead00b327e72088c6d8632c0b4a84ec8d80e237474c8d8401310315ded9d5e06c7b544580b893ee1a136437cf307e59ed48f6a4e9f8d4c9200d39000000140800000000000000000000000000100802aa6982000000000000000000000000000000000000000000060000000000000000000302fd05b1517d2e413b334da125c59240368a0cf601d3b661f0bb14841900cfb34c5ced3100000000000000030000000000e4e1c0e9a8e03598461419045c46113c29e561e9c94cb9d2539359bf31730e65047cf800061b100003bb1fbbd68ea267573beebe1b0939b5b0eae4ea2f09e84fcdbf325ff46458cfab3821fc04e9478c91c4fbeae7b1ca5e2ebc77fd1f2effa6c7dac4678675b2cfd319d326b91125ffda09ab67754849c7652e46dd695dd75c14eb1c09ac834a81fe23622bdf510745ee74400a45055461529a9d8c78cdbf0edb535418e13038647d09546e413c555e93d8d64df5c3fcf5da96efea47a2c275e564ee801b70e8ef6756a6cd0c2dafda538d24f649225efaba92e1713c0b68e5465dc97f867168e08d83074eb5bb48f5f6aea45e76cbc79f4aa0bf8e490877ab98375f1908d95b14627d772e64d6c3b02039b788333913e07e5a021f7303f1e5614e1414c1d67103e23e51a87968b051dd65b53045606bb7b0bf27521d4e9bfdec98179eb79c95e0c9a49e24fa3e248e88bb1e94a439bc5f38dc7688e576519156addfdf468ecce8acdbb979a39302f170c8c73cd466c9e6a39be8c75ebfd6c74f3e73e212d1c9c7869bdd50661d9bb1ae98e2f90f4de9b43976036740378612839a1b9b282ff9b4da2c9313da19051d7854a2fdec7cebab9a7422dae3241124b2c4d2dfc0a96b4e9b7b3cd9c094e7679473b0ada8c01e725ec9221c48497ae2c0e68509867f24c95b3faf3d6a6aea77f705ea5d121cc4672aedc1901d917f3d7a0ed3ab2a37a7fac762c6e0d64f075b7c1a6a897571633b5fc9bb9971f26c54adfc032f80dc35b6b93ed98c89af3f2270b847ed3e11779c16fbf919b149230cfb8e7fc3e13dc67a737e28afbd4c1c399666949e2f72c754643fae5983250e588d34ed190dec6d26ee4ca7ab2069d0894db162678d6b6c3e28fd12db2e048cd434f0aaa155f769b770369671839861f35814603f3526b526441aefb50bdba2db32ef54d5706752e57a4fb2ca5936c19b3653f7a1b83cd8d188404d6b2e3033550d62801dd580f51b5ef76632820d5d3635a76735674f5c9f9207390ed2cee80b19a4a451d4e98b9247485c95174eaed30531b95f80323ec48ffdaaf7096d38330f8bf46a4b1dd12b8ed2fe26a983192ea5f913361fde646ed8619b98136181466bddb82d4a33361aad8ed03fabdf32f51d55985ff26e0bc7096557c63dfd425aed6f268d2974c59235b5c837bb77060bb09dc02584f861dcdd0556411a5e1a7a557bf5245695254faefb9a8387e683f4b874f437c6d073f1b106acdfc58b11e495ed84cf2fd4e7d03128ec0f720bea07971cdc8d3bf2ad283b970a579a7a280533f38bd831dddc4d41ec73cd42f0736b65d6a933e7a728a435067f29cd02a65e6c3eeb97a434af664d52e37113f7fb78ea77b8c390095bd6d6027444b4e4ac0cfabc0764d26ca16993d0bd833dd561e1d0ab62b090ca626ab49ec111f24703a755c675f33a73d7167e7f6e74ca924dae425da11db3542f2e99aadd39b7aca804de7944cf073baae38ffc82c98c0bea84efb7f269506da9146f011e83bd243d0f9c948ec76c2e76d003a1196dfd5c681f8cd6b02d2c83cc75e52680a983c0490eb76e83c1047120b28fa4e659b4f0c14423b9548e6e2b6d2d0f89e95c194f933841f079a0b6b6e04292bdfa16ef45a811abfd8e957224ac1049c3070a89ad2f996a50d6f0466b9ec8785d9f41d74905a386e43e3922493f1dbc2c7aa4a8cba7d1754854f9b2afd6a349c2b0216024b6dbaa7615239db219850f54694f1a84cd9a7d459823f1c9e0ab13d8cdb59bd4cf2b50520c27ccfe1d71a00649933c1a2d5af14a5cd37fbd6403cb698489c6d0ab0d2bce6399a2dc5103c181c48db538d73bd961774b7daf4f9948070131d3611dd9901c420e86d36567e9a139b65a71de38d37b921813e4eb56dbb2046069017bbcc495987888820b0282280b3e6dd120c29ec33201f04db23eb047660b7aeb0c42518a059ae68d6be6f89f259aa3fe0001a147010702fd05b1517d2e413b334da125c59240368a0cf601d3b661f0bb14841900cfb34c5ced3100000000000000040000000000e4e1c0e9a8e03598461419045c46113c29e561e9c94cb9d2539359bf31730e65047cf800061b100002c2a2e358ff493e14552a590cc5480dbd6c65a8d38012dd7a063b83e1628d9fb9fc6892efdb30e02d662e5682669402a46c0452a9cf55703beece192c78e238b4f096a6b0cf062a248eebe47530abe0b284ab655045e1cd729fc983d8a3301d17dd6b88914b18f22058322c8ebbbf797086662642929ac21a0f67d1faa544c28ab231529006baf3a212b828ce591129be0c552f54c0fae02b024c487f6bd343f2338c86e063b2b0364a875169eb0ad2ebd63539f8985988e9597e4c4daa63578e9f3182692df5dd012085f2fa264ec37f66affb8f2b2ef30f8224d7c39333dc453310a19a3cb583a019479721d7be72fa1253e2252c1bc8b16c1515a851a795f158a172c61ab03d5a40a2f5d0d1335f5a859def1f6ef6d084b3c5a697146a47a419f4f6c1e62492f3cf68addd260ea6383d44f21f71dc62a4dcf6b7298abfdbc01e78edef8ebdb8d62c1a9730fd06bde56167d7439767b0232bd54d9f7d570b343a63517b13e904a380ff3153135a614b84102f485623ebeb2cd1f82b4c09271d215e31dcb5e932e8693e64f1643d5a678b27bea2cf78255ce04871fbd6c5d8df1b88b8ff1070469a68f573dd9535dce68dc6ac7a6aff2bd1e46a1527ba9468ed88b2ad6c7044ad311ccdf184444e1bd03a7694fa59f190440de037daae5d5c9eddbb446795fc86fd6b3d362ac4ced76b2f2e9f356f319563ada03f8e8955d21c18951093899c4cff78aced9e4bb44f3a6ca00ad0c029bb27c4d32250e903f5afc9f40482d1a01caf5f6927201e3992c9c49782c04ebf351548f0391b091172e5c0e2c85ff8efbc3400e086ea1f5532b32abbc7dc3a3332098fe65935ef008d19b7a1ba8c0e5c92833ee5c336046a1b718dd862fecd21d52a207ddaf9fb101d4435357e2fed3e7e2b8e15cf5862a3fd5cb54e48097b335420fbef075411cd4c13d5412aeae1d4a1fe44c8188d60f5847d23b203b5716e66fc9137588ae360e60a8f34da36b4006550cd20f27a9c0bfb1dfa047874784dfc70ea7e83491211f1269b4f3a514f4e47b13c99560814a906694de1bebeb1ae895b06cfdeefafb72460611c5f188af92a6630af230c708b9fcd6e1aebe14a8ca8a7f936064fe8ffca14e2e94ca016c3818696a105dc19846958b8d2479a04775205ec366e467d9b97daa6632281e49f24452fcc986abcc448565d9a0fda14cefb36c949209e75db3720293419fd628aa0ded7cf073f4c582c95ca1d06403dccbe0e411f9d53ff9cd1f4cff3d11afd79f40d3563c519b580778885d9bc02dd11f770cad75bd616d1302c995578763c41a84222ce1d0abc34b72912a809e6e6e4fd21fff013533135d2a18cb080c758f5f5ac5831bea997fe85b1bd744850abbe8d401fd22ede0b00fd148269f58513e317ccf51483810f0c44626a8865ba84ba24d4d8a3d802fa88afb60cbfa5c2910c8a5f5f2fb6ddb22fd5711a11ff510c6f726aebdc8bd26e117336d863b5b806b70330bcb0bc2356acb809baf846d2394fcf999f3bce2d7ba73b2b88070a8a98ae610a87c1814c8b84fa05286e5f8c0f66cf2465008f2f9f554de6d1fb79e336ab10aab433692f10843c8bb6329decf6e2415c1773da001e1897270ce0d5c49bd5ec575d63c5831a88b8a549858246ea61a6859dda024b36d98a789d7d3961b6057853d947dcbee3cbb8218ee80a91cf25e0a9afc2797b903f052a7b9018867d2bf6c50d82b6b53b4abd7d15ddb09e49cf75d1278b5729658c1667399acee8b37b6c114ae6479f6c0f5f2b4148bf3339a5903f0d45dc3253649d2265e617a6436d94c1dee01acbc1a2d6ff2bd364d87e48826466a00bfbbe3bfb581cbd4f7714ed93ccef5043ce09ba5a7ae29aeea048f21ae4939e39b52d0dc6dc1319b2f69512fb90ed6c170463c3e1945b4c754cfe0001a147010702fd05b1517d2e413b334da125c59240368a0cf601d3b661f0bb14841900cfb34c5ced3100000000000000050000000001312d00e9a8e03598461419045c46113c29e561e9c94cb9d2539359bf31730e65047cf800061b0f0003ff60ae4b0f90b4094c16b24a41189874278026dad5ae615249c98061153490c75d168e55ebde72c562b23e94c04bb1fe3e4efa4a3e3747a6d5fd4afb49181734ef9f05f9371ef76323c1d86a42b06cc448e183588a18f6471d6ae6d4801f8fe9e6de2a86ccee9d7199980a3eb39c14b1b0ef1b427fcca4c5bddb2597a5f2ff33ce189dc3be47c9f8db4b3f5f981ecfacb4f501b8369ed0e5055e2855bf1a5f6d1f60c032a6a5e614ff7cff1dfc3953f40d5900881e30d73516440543452e981ed9f7e7cbfbf43015e2e58eb0d5e328978b7cfb86f06ede0a4ef2188b444b0f0b0a67b13097d1efdc163cd80c5b308ae386e37c98260d07f1b4e9798dda2964a48ed759bf089e05ee96a2085b006f17960fc2ffebac3fa32d96bcecb85552f9f7074920f251bb10fdfd54bfa63b2f2089c71b27c4eca890190a6cbf3452a7f4695f46576d366aa1577ff1efaf1928d812b2c75be3173d2bf7d7ee651b0c1e834d415ff23637fdf11963eeeb31ab65291f7e0e4b85e79bd5e8cf4073c8d32d427c33cfc6cf519207002147b8efc80151e426cbf67f7a1d88876aae794a9de7b075961ee26ec0f827c00a2221e0806f06e53aaacfa2f96e5a73f61ff66fc0f0c72a5dfeba86680288a51e4bf3054bc28aae46690f0b8ab1b86e1cc9897ba5fc142ee042f9a7dc555cd5ccc00b17e4e967b1cb95f875ad7b804c2345d69c13b4af93d359c22b3277b87b986b92e38df8b265220ba522018edf1b529edf52639b704ae29f7b9318814798d26dacb6cc1f53a749d03a753193bc38dd5f3f3dfde431843618b51a699b36ab925b22df59804490447f0649e23e7a3c6bdee21757343088936bf38de46a9fe7399901debdd0ba0a2df801eb50baf0864a22c3c7ab998389c01724d21af0c5ba82f550ad349008b42ff30b28b585d7f30efa56d921d65f110a1c349577c4cfefc7c8134b8fd23bf029ce332bc6f015b1add992c714e207f3ed2be16feaed660f0e5b8e5cd27e01f2d5ee660607f88fc76ddac51943d45cbac814fced5ce9417c369d4d310329e2fd0f9e3465fad492c5be7b8c4f8e02a5088f85641481f8e0c83f0715e39a3760670d947e53e5284752753b9b87b87c83a1daf72c9e7e811c1f227a58f8b8e2dfcce67d787eaa541f89727802be483d0c4f6564ae547c10689d9da0469a806b9bf995fe880c0d36bdcdc7b6fe30fa96588a775b47f1d1c49d1ebfa323dcbd8e679a2681635459ea5aa214e625bc4737569d3fc71ee72320f7f9299b7396f7fc493d06fe479c5b3639c546f51b09c610593b8aa94501e7fa94f6a6b961719af53f137a89cf3a82899492e8b5240f43a74a3631f7bf1ee60f5b93df3d7896faf6a944ad2df3546fcafafa562653bf7d9e1bfa9e14bfec7d372d881d1f43edb75854fe1653ffd1569f9f01ef0d3ca0345f3814a4bd0f5d5aafc943966846f693890f969e0249494b5329aeb3a1ba42bf95437cafa458b97ebd0ef44e3c590138f3271e961615a62d48f7895c35ddba51135531bf7d83b900853d91381ea38d9a57f9bfe064b45257aa7b9f1f33e102bf6370eae14f60226d59208e520cc41307f5da8e3f5ae6d3b59c5f9de502ea0fb7d99461e7dceefc91867f693d8d99177d8dd395bacd5889a15dfc8eb3aa16651e39272afcef4d43440304c62a16078e08368cc836288349bfbaf99e9829e31da981431b60289830a818b899e39f5051b27af0230b3f6a11210423840c2138c41dd917e1b73b9e231afcce986b03cf91e074fd2f4f41797d86254da5fc33fc3895b8b19b68e93f662c7458d3950a8e913db358d455cb17d24ad17e0223bf6a6bf77748e2fc7df61f5b6f28a70a840390f2e1215aad0c94ae221080db08439b2a3176ace06711bd00b0056b414a6cbe6cd02cae7b21624dedfe0001a1470107000100000000000000000000000024517d2e413b334da125c59240368a0cf601d3b661f0bb14841900cfb34c5ced310000000000000000000f424002f9061287def08210bb486b2d7da13a892dd617ac9cab05c4b0cf5df7690012630400012401010101010101010101010101010101010101010101010101010101010101012a0000002b40420f0000000000220020a509b20870df3d86385de226de6d6c00bbe06306aaf9a65c6784f4443f1d8c96061a8000002a000000000202000000000000044c0000000000000000000000001dcd6500006402d000000000000000070003020000000000000003020000000000000004020000000000000005000009c40000000008f0d180000000002faf0800417afe669ec45cc974ead3d5a450cee95071f0181b30e79b4cac0b7bca5bde8f0175c470a01e387085420921021ff96c15e52d92c38ff7fc001b39d788124041596ad205bb702803ca7bf5b6fb400855b44f60885120a1b5dd8d4127a592e0e1ee000302a77bdecaff8ba889a600bd0c6ff275fbb4c653fd384dcc74ef83c0a489f0cb4fae0232ba93a4199979214cc04b6674f4c71c2160ea730f12984a505bf5e3b2e67109b0f66f783a9a81440f5efa94d5e24e3b94ad7ba416299c7a73e13207654a9400f345144eeae4416323e681c1b54f0fa6d4b8b114f7d147e6752655c6cf67449349d9dc68f08bf6ac6cd843a069659b95473b896cad80aa8f4d569b87986f03c32d8e3823ae81816c2d0700fcd159bea42bccd65f432e00f4714a310d8800000000000003e800000000000003e8000000003b9aca00001e009000000000000000070003010000000000000003010000000000000004010000000000000005000009c4000000002faf08000000000008f0d180c07f56129b0acc8016f3b2b92177d2f0b4f5c3f2062da6d2ffa9fbbbb169c7f40257defec7cc49ca82a1a35763ad8062f18b2efd6544352824bf46955fc125faae000000ff03a3d68883581ae02971963997762b8a87f7e4d3436726c08e68227833ac97522c0003003e0000fffffffffffc00819683302bccd824e277fe42ab94b7ddde6de92819354b769d5aa2f9e32959af0800fc0003ffffffffffe801050801ced4de8f29ad19ce8d89ea41d663c69e8991904bb485068683a74ed8574002000007ffffffffffc801072f24eca6f738a235010736b50b3e2c93e79c0e292b503098db66d60e58bc47fc0003ffffffffffe40003000000000000000300013ad2926707b24337b4b8a4807f29ea3200000000000000040001864f8fbba78c43e79fad189132c58dac000000000000000500013b6819795b5d43c09e67dedfa8e5b0900000061a80160014761879f7b274ce995f87150a02e75cc0c037e8e30000000000fffd023d02000000000101517d2e413b334da125c59240368a0cf601d3b661f0bb14841900cfb34c5ced310000000000ac411680074a010000000000002200203aed4774869098eee46a59020a36e77e001549a3a4fa5adca7182289d9c3cfb84a01000000000000220020afd285c7863ed45cb5c369755b800bef1a93922b68ac12476163b2a2cda7f301983a000000000000220020dce9b7bf8b7b37b3d193e649cfbcb90b9141761e7cdd84fe822cec65d87465f6983a000000000000220020dce9b7bf8b7b37b3d193e649cfbcb90b9141761e7cdd84fe822cec65d87465f6204e0000000000002200205f70ee0c30e5d2107ec6826c0806b7793e6c95a3153a3c0f116f06a2f1d12f6d5837020000000000220020cfe0882d0bb9ed2d0b2a7ca9d1982f4660ea04e76bd6c25fc02fd6b678a1a42a00350c000000000022002074e7dd4e6c440f206647694be395adfdd416731ebaa1e4d4a70121e5674c7b9d040047304402204e8649d2b4c18df6a009a7cb1ca7eba52f5e9aab0b5ca03f9f0eff3438b5d94102207e639b145cc353495ea583b3d38212a9d45c79e816e1289bb060fd522c4caafd01483045022100c37dd0d8f0f77248bb3044561470d41e1e3f1a3a96abfbae083db1bab3c3ee5802206c8dc44a9dde0a63dc354da22898de5aba52fbb21950ad3a64968b06ffd71094014752210275ddbe228db6b90c9e2a3350e0048a44e04fcfeb680ed425e2bdf7964368f83d2102f9061287def08210bb486b2d7da13a892dd617ac9cab05c4b0cf5df76900126352ae87498120ff24f4c769b1bbfba9ffd2a62d06f2c3f5b4f0d27721b9b2f31680cc0a9b12567fc005000000ff24f4c769b1bbfba9ffd2a62d06f2c3f5b4f0d27721b9b2f31680cc0a9b12567fc0000000000000000324f4c769b1bbfba9ffd2a62d06f2c3f5b4f0d27721b9b2f31680cc0a9b12567fc002000000000000000000000324f4c769b1bbfba9ffd2a62d06f2c3f5b4f0d27721b9b2f31680cc0a9b12567fc003000000000000000000000424f4c769b1bbfba9ffd2a62d06f2c3f5b4f0d27721b9b2f31680cc0a9b12567fc0040000000000000000000005000424517d2e413b334da125c59240368a0cf601d3b661f0bb14841900cfb34c5ced3100000000fd023d02000000000101517d2e413b334da125c59240368a0cf601d3b661f0bb14841900cfb34c5ced310000000000ac411680074a010000000000002200203aed4774869098eee46a59020a36e77e001549a3a4fa5adca7182289d9c3cfb84a01000000000000220020afd285c7863ed45cb5c369755b800bef1a93922b68ac12476163b2a2cda7f301983a000000000000220020dce9b7bf8b7b37b3d193e649cfbcb90b9141761e7cdd84fe822cec65d87465f6983a000000000000220020dce9b7bf8b7b37b3d193e649cfbcb90b9141761e7cdd84fe822cec65d87465f6204e0000000000002200205f70ee0c30e5d2107ec6826c0806b7793e6c95a3153a3c0f116f06a2f1d12f6d5837020000000000220020cfe0882d0bb9ed2d0b2a7ca9d1982f4660ea04e76bd6c25fc02fd6b678a1a42a00350c000000000022002074e7dd4e6c440f206647694be395adfdd416731ebaa1e4d4a70121e5674c7b9d040047304402204e8649d2b4c18df6a009a7cb1ca7eba52f5e9aab0b5ca03f9f0eff3438b5d94102207e639b145cc353495ea583b3d38212a9d45c79e816e1289bb060fd522c4caafd01483045022100c37dd0d8f0f77248bb3044561470d41e1e3f1a3a96abfbae083db1bab3c3ee5802206c8dc44a9dde0a63dc354da22898de5aba52fbb21950ad3a64968b06ffd71094014752210275ddbe228db6b90c9e2a3350e0048a44e04fcfeb680ed425e2bdf7964368f83d2102f9061287def08210bb486b2d7da13a892dd617ac9cab05c4b0cf5df76900126352ae8749812024f4c769b1bbfba9ffd2a62d06f2c3f5b4f0d27721b9b2f31680cc0a9b12567fc005000000c302000000000101f4c769b1bbfba9ffd2a62d06f2c3f5b4f0d27721b9b2f31680cc0a9b12567fc0050000000001000000011426020000000000160014761879f7b274ce995f87150a02e75cc0c037e8e30247304402206779ed6341471bc350a476eb7ed93118337e19f85128c5c9c806355f811aac4702200128ac254b38260987611eeba5bea32717b412ac2c67f47a4c2071c972959f57012521038babd383821d5e104eba86044151807a5bc08294e436a26817f1865104fbe457ad51b20000000024f4c769b1bbfba9ffd2a62d06f2c3f5b4f0d27721b9b2f31680cc0a9b12567fc002000000fd012e02000000000101f4c769b1bbfba9ffd2a62d06f2c3f5b4f0d27721b9b2f31680cc0a9b12567fc0020000000001000000010e3a000000000000160014761879f7b274ce995f87150a02e75cc0c037e8e303483045022100f7ce5624964ede4c2b47f13550f8e9e41877ea079dbc3020f5dcede34407efcd022065782d162aa8cef61d7e203f1f78344992b52d830d3f7858517e79f50e9f3cf401008e76a9149c9ff72dc1e2dffe43b9ce2e36ab45f552b217b98763ac672103da09db658d1955bc6e80a6f994e70db971fff30ab497e95f2b08cef650d80e0b7c8201208763a914039d026f03732badf6719bd217778462a3e19a9088527c21027a3c9c9f92a2ea7a3016e25453677908519dffc0ae863f49b9bc5b9d7022287452ae677503101b06b175ac6851b27568101b060024f4c769b1bbfba9ffd2a62d06f2c3f5b4f0d27721b9b2f31680cc0a9b12567fc003000000fd012e02000000000101f4c769b1bbfba9ffd2a62d06f2c3f5b4f0d27721b9b2f31680cc0a9b12567fc0030000000001000000010e3a000000000000160014761879f7b274ce995f87150a02e75cc0c037e8e303483045022100c5ca68ad6ca3abac56919cfdae84e6c95130a5b90fd8852668454f599bd60c880220248c9789caa074cf785c3eadd6a2d133dd4887c30fdf0281669b257f66ba0c3e01008e76a9149c9ff72dc1e2dffe43b9ce2e36ab45f552b217b98763ac672103da09db658d1955bc6e80a6f994e70db971fff30ab497e95f2b08cef650d80e0b7c8201208763a914039d026f03732badf6719bd217778462a3e19a9088527c21027a3c9c9f92a2ea7a3016e25453677908519dffc0ae863f49b9bc5b9d7022287452ae677503101b06b175ac6851b27568101b06000000000000 \ No newline at end of file diff --git a/eclair-core/src/test/resources/nonreg/codecs/05000a-DATA_CLOSING/remote/data.json b/eclair-core/src/test/resources/nonreg/codecs/05000a-DATA_CLOSING/remote/data.json new file mode 100644 index 0000000000..603860aefc --- /dev/null +++ b/eclair-core/src/test/resources/nonreg/codecs/05000a-DATA_CLOSING/remote/data.json @@ -0,0 +1,221 @@ +{ + "type" : "DATA_CLOSING", + "commitments" : { + "channelParams" : { + "channelId" : "517d2e413b334da125c59240368a0cf601d3b661f0bb14841900cfb34c5ced31", + "channelConfig" : [ "funding_pubkey_based_channel_keypath" ], + "channelFeatures" : [ ], + "localParams" : { + "nodeId" : "02aaaa00ce2f18a967dc4f25f414e671ba6585f8ef0b8c5fb812c21064f55a2eaa", + "fundingKeyPath" : [ 2465171583, 265411917, 468114705, 2303872767, 2627170256, 675347429, 1050017569, 4205305699, 2147483649 ], + "initialRequestedChannelReserve_opt" : 10000, + "isChannelOpener" : true, + "paysCommitTxFees" : true, + "initFeatures" : { + "activated" : { + "option_route_blinding" : "optional", + "option_provide_storage" : "optional", + "splice_prototype" : "optional", + "payment_secret" : "mandatory", + "gossip_queries_ex" : "optional", + "option_anchor_outputs" : "optional", + "option_quiesce" : "optional", + "option_data_loss_protect" : "optional", + "var_onion_optin" : "mandatory", + "option_static_remotekey" : "optional", + "option_support_large_channel" : "optional", + "option_anchors_zero_fee_htlc_tx" : "optional", + "option_channel_type" : "mandatory", + "basic_mpp" : "optional", + "gossip_queries" : "optional" + }, + "unknown" : [ 50001 ] + } + }, + "remoteParams" : { + "nodeId" : "02bbbb671d15145722fb8c28d732cddb249bcc6652ed2b297ff1f77a18371b1e63", + "initialRequestedChannelReserve_opt" : 20000, + "revocationBasepoint" : "02c3c29dc57978b69aa8f3d483b54ce7e9e08fef3a556f854ab7f93d9c56d504e5", + "paymentBasepoint" : "03cbfd24887a5fb7f8d8e6ca0c652a70a809f4044faf58ee39a02a3a98acc50253", + "delayedPaymentBasepoint" : "023f449e1a43e11ead00b327e72088c6d8632c0b4a84ec8d80e237474c8d840131", + "htlcBasepoint" : "0315ded9d5e06c7b544580b893ee1a136437cf307e59ed48f6a4e9f8d4c9200d39", + "initFeatures" : { + "activated" : { + "option_route_blinding" : "optional", + "splice_prototype" : "optional", + "payment_secret" : "mandatory", + "gossip_queries_ex" : "optional", + "option_anchor_outputs" : "optional", + "option_quiesce" : "optional", + "option_data_loss_protect" : "optional", + "var_onion_optin" : "mandatory", + "option_static_remotekey" : "optional", + "option_support_large_channel" : "optional", + "option_anchors_zero_fee_htlc_tx" : "optional", + "option_channel_type" : "mandatory", + "basic_mpp" : "optional", + "gossip_queries" : "optional" + }, + "unknown" : [ ] + } + }, + "channelFlags" : { + "nonInitiatorPaysCommitFees" : false, + "announceChannel" : false + } + }, + "changes" : { + "localChanges" : { + "proposed" : [ ], + "signed" : [ ], + "acked" : [ ] + }, + "remoteChanges" : { + "proposed" : [ ], + "acked" : [ ], + "signed" : [ ] + }, + "localNextHtlcId" : 6, + "remoteNextHtlcId" : 0 + }, + "active" : [ { + "fundingTxIndex" : 0, + "fundingInput" : "31ed5c4cb3cf00198414bbf061b6d301f60c8a364092c525a14d333b412e7d51:0", + "fundingAmount" : 1000000, + "localFunding" : { + "status" : "confirmed", + "shortChannelId" : "400000x42x0" + }, + "remoteFunding" : { + "status" : "locked" + }, + "commitmentFormat" : "anchor_outputs", + "localCommitParams" : { + "dustLimit" : 1100, + "htlcMinimum" : 0, + "maxHtlcValueInFlight" : 500000000, + "maxAcceptedHtlcs" : 100, + "toSelfDelay" : 720 + }, + "localCommit" : { + "index" : 7, + "spec" : { + "htlcs" : [ { + "direction" : "OUT", + "id" : 3, + "amountMsat" : 15000000, + "paymentHash" : "e9a8e03598461419045c46113c29e561e9c94cb9d2539359bf31730e65047cf8", + "cltvExpiry" : 400144 + }, { + "direction" : "OUT", + "id" : 4, + "amountMsat" : 15000000, + "paymentHash" : "e9a8e03598461419045c46113c29e561e9c94cb9d2539359bf31730e65047cf8", + "cltvExpiry" : 400144 + }, { + "direction" : "OUT", + "id" : 5, + "amountMsat" : 20000000, + "paymentHash" : "e9a8e03598461419045c46113c29e561e9c94cb9d2539359bf31730e65047cf8", + "cltvExpiry" : 400143 + } ], + "commitTxFeerate" : 2500, + "toLocal" : 150000000, + "toRemote" : 800000000 + }, + "txId" : "417afe669ec45cc974ead3d5a450cee95071f0181b30e79b4cac0b7bca5bde8f", + "remoteSig" : { + "sig" : "75c470a01e387085420921021ff96c15e52d92c38ff7fc001b39d788124041596ad205bb702803ca7bf5b6fb400855b44f60885120a1b5dd8d4127a592e0e1ee" + }, + "htlcRemoteSigs" : [ "02a77bdecaff8ba889a600bd0c6ff275fbb4c653fd384dcc74ef83c0a489f0cb4fae0232ba93a4199979214cc04b6674f4c71c2160ea730f12984a505bf5e3b2", "e67109b0f66f783a9a81440f5efa94d5e24e3b94ad7ba416299c7a73e13207654a9400f345144eeae4416323e681c1b54f0fa6d4b8b114f7d147e6752655c6cf", "67449349d9dc68f08bf6ac6cd843a069659b95473b896cad80aa8f4d569b87986f03c32d8e3823ae81816c2d0700fcd159bea42bccd65f432e00f4714a310d88" ] + }, + "remoteCommitParams" : { + "dustLimit" : 1000, + "htlcMinimum" : 1000, + "maxHtlcValueInFlight" : 1000000000, + "maxAcceptedHtlcs" : 30, + "toSelfDelay" : 144 + }, + "remoteCommit" : { + "index" : 7, + "spec" : { + "htlcs" : [ { + "direction" : "IN", + "id" : 3, + "amountMsat" : 15000000, + "paymentHash" : "e9a8e03598461419045c46113c29e561e9c94cb9d2539359bf31730e65047cf8", + "cltvExpiry" : 400144 + }, { + "direction" : "IN", + "id" : 4, + "amountMsat" : 15000000, + "paymentHash" : "e9a8e03598461419045c46113c29e561e9c94cb9d2539359bf31730e65047cf8", + "cltvExpiry" : 400144 + }, { + "direction" : "IN", + "id" : 5, + "amountMsat" : 20000000, + "paymentHash" : "e9a8e03598461419045c46113c29e561e9c94cb9d2539359bf31730e65047cf8", + "cltvExpiry" : 400143 + } ], + "commitTxFeerate" : 2500, + "toLocal" : 800000000, + "toRemote" : 150000000 + }, + "txId" : "c07f56129b0acc8016f3b2b92177d2f0b4f5c3f2062da6d2ffa9fbbbb169c7f4", + "remotePerCommitmentPoint" : "0257defec7cc49ca82a1a35763ad8062f18b2efd6544352824bf46955fc125faae" + } + } ], + "inactive" : [ ], + "remoteNextCommitInfo" : "03a3d68883581ae02971963997762b8a87f7e4d3436726c08e68227833ac97522c", + "remotePerCommitmentSecrets" : null, + "originChannels" : { + "3" : { + "paymentId" : "3ad29267-07b2-4337-b4b8-a4807f29ea32" + }, + "4" : { + "paymentId" : "864f8fbb-a78c-43e7-9fad-189132c58dac" + }, + "5" : { + "paymentId" : "3b681979-5b5d-43c0-9e67-dedfa8e5b090" + } + } + }, + "waitingSince" : 400000, + "finalScriptPubKey" : "0014761879f7b274ce995f87150a02e75cc0c037e8e3", + "mutualCloseProposed" : [ ], + "mutualClosePublished" : [ ], + "remoteCommitPublished" : { + "commitTx" : { + "txid" : "c07f56129b0acc8016f3b2b92177d2f0b4f5c3f2062da6d2ffa9fbbbb169c7f4", + "tx" : "02000000000101517d2e413b334da125c59240368a0cf601d3b661f0bb14841900cfb34c5ced310000000000ac411680074a010000000000002200203aed4774869098eee46a59020a36e77e001549a3a4fa5adca7182289d9c3cfb84a01000000000000220020afd285c7863ed45cb5c369755b800bef1a93922b68ac12476163b2a2cda7f301983a000000000000220020dce9b7bf8b7b37b3d193e649cfbcb90b9141761e7cdd84fe822cec65d87465f6983a000000000000220020dce9b7bf8b7b37b3d193e649cfbcb90b9141761e7cdd84fe822cec65d87465f6204e0000000000002200205f70ee0c30e5d2107ec6826c0806b7793e6c95a3153a3c0f116f06a2f1d12f6d5837020000000000220020cfe0882d0bb9ed2d0b2a7ca9d1982f4660ea04e76bd6c25fc02fd6b678a1a42a00350c000000000022002074e7dd4e6c440f206647694be395adfdd416731ebaa1e4d4a70121e5674c7b9d040047304402204e8649d2b4c18df6a009a7cb1ca7eba52f5e9aab0b5ca03f9f0eff3438b5d94102207e639b145cc353495ea583b3d38212a9d45c79e816e1289bb060fd522c4caafd01483045022100c37dd0d8f0f77248bb3044561470d41e1e3f1a3a96abfbae083db1bab3c3ee5802206c8dc44a9dde0a63dc354da22898de5aba52fbb21950ad3a64968b06ffd71094014752210275ddbe228db6b90c9e2a3350e0048a44e04fcfeb680ed425e2bdf7964368f83d2102f9061287def08210bb486b2d7da13a892dd617ac9cab05c4b0cf5df76900126352ae87498120" + }, + "localOutput_opt" : "c07f56129b0acc8016f3b2b92177d2f0b4f5c3f2062da6d2ffa9fbbbb169c7f4:5", + "anchorOutput_opt" : "c07f56129b0acc8016f3b2b92177d2f0b4f5c3f2062da6d2ffa9fbbbb169c7f4:0", + "incomingHtlcs" : { }, + "outgoingHtlcs" : { + "c07f56129b0acc8016f3b2b92177d2f0b4f5c3f2062da6d2ffa9fbbbb169c7f4:2" : 3, + "c07f56129b0acc8016f3b2b92177d2f0b4f5c3f2062da6d2ffa9fbbbb169c7f4:3" : 4, + "c07f56129b0acc8016f3b2b92177d2f0b4f5c3f2062da6d2ffa9fbbbb169c7f4:4" : 5 + }, + "irrevocablySpent" : { + "31ed5c4cb3cf00198414bbf061b6d301f60c8a364092c525a14d333b412e7d51:0" : { + "txid" : "c07f56129b0acc8016f3b2b92177d2f0b4f5c3f2062da6d2ffa9fbbbb169c7f4", + "tx" : "02000000000101517d2e413b334da125c59240368a0cf601d3b661f0bb14841900cfb34c5ced310000000000ac411680074a010000000000002200203aed4774869098eee46a59020a36e77e001549a3a4fa5adca7182289d9c3cfb84a01000000000000220020afd285c7863ed45cb5c369755b800bef1a93922b68ac12476163b2a2cda7f301983a000000000000220020dce9b7bf8b7b37b3d193e649cfbcb90b9141761e7cdd84fe822cec65d87465f6983a000000000000220020dce9b7bf8b7b37b3d193e649cfbcb90b9141761e7cdd84fe822cec65d87465f6204e0000000000002200205f70ee0c30e5d2107ec6826c0806b7793e6c95a3153a3c0f116f06a2f1d12f6d5837020000000000220020cfe0882d0bb9ed2d0b2a7ca9d1982f4660ea04e76bd6c25fc02fd6b678a1a42a00350c000000000022002074e7dd4e6c440f206647694be395adfdd416731ebaa1e4d4a70121e5674c7b9d040047304402204e8649d2b4c18df6a009a7cb1ca7eba52f5e9aab0b5ca03f9f0eff3438b5d94102207e639b145cc353495ea583b3d38212a9d45c79e816e1289bb060fd522c4caafd01483045022100c37dd0d8f0f77248bb3044561470d41e1e3f1a3a96abfbae083db1bab3c3ee5802206c8dc44a9dde0a63dc354da22898de5aba52fbb21950ad3a64968b06ffd71094014752210275ddbe228db6b90c9e2a3350e0048a44e04fcfeb680ed425e2bdf7964368f83d2102f9061287def08210bb486b2d7da13a892dd617ac9cab05c4b0cf5df76900126352ae87498120" + }, + "c07f56129b0acc8016f3b2b92177d2f0b4f5c3f2062da6d2ffa9fbbbb169c7f4:5" : { + "txid" : "aa4ee5b0f8cfdaa256f2c45771602eeb990e05916ab4eb2ab25ce57b8c8cbd96", + "tx" : "02000000000101f4c769b1bbfba9ffd2a62d06f2c3f5b4f0d27721b9b2f31680cc0a9b12567fc0050000000001000000011426020000000000160014761879f7b274ce995f87150a02e75cc0c037e8e30247304402206779ed6341471bc350a476eb7ed93118337e19f85128c5c9c806355f811aac4702200128ac254b38260987611eeba5bea32717b412ac2c67f47a4c2071c972959f57012521038babd383821d5e104eba86044151807a5bc08294e436a26817f1865104fbe457ad51b200000000" + }, + "c07f56129b0acc8016f3b2b92177d2f0b4f5c3f2062da6d2ffa9fbbbb169c7f4:2" : { + "txid" : "c3e834e9a14a7e76d4ba837d561011893a5e7182dfb61f0f3dd822d2fc2bf3ac", + "tx" : "02000000000101f4c769b1bbfba9ffd2a62d06f2c3f5b4f0d27721b9b2f31680cc0a9b12567fc0020000000001000000010e3a000000000000160014761879f7b274ce995f87150a02e75cc0c037e8e303483045022100f7ce5624964ede4c2b47f13550f8e9e41877ea079dbc3020f5dcede34407efcd022065782d162aa8cef61d7e203f1f78344992b52d830d3f7858517e79f50e9f3cf401008e76a9149c9ff72dc1e2dffe43b9ce2e36ab45f552b217b98763ac672103da09db658d1955bc6e80a6f994e70db971fff30ab497e95f2b08cef650d80e0b7c8201208763a914039d026f03732badf6719bd217778462a3e19a9088527c21027a3c9c9f92a2ea7a3016e25453677908519dffc0ae863f49b9bc5b9d7022287452ae677503101b06b175ac6851b27568101b0600" + }, + "c07f56129b0acc8016f3b2b92177d2f0b4f5c3f2062da6d2ffa9fbbbb169c7f4:3" : { + "txid" : "3da048e73602307642a52e7ebf2ac7fdf6010380ee2518512817d1114464f643", + "tx" : "02000000000101f4c769b1bbfba9ffd2a62d06f2c3f5b4f0d27721b9b2f31680cc0a9b12567fc0030000000001000000010e3a000000000000160014761879f7b274ce995f87150a02e75cc0c037e8e303483045022100c5ca68ad6ca3abac56919cfdae84e6c95130a5b90fd8852668454f599bd60c880220248c9789caa074cf785c3eadd6a2d133dd4887c30fdf0281669b257f66ba0c3e01008e76a9149c9ff72dc1e2dffe43b9ce2e36ab45f552b217b98763ac672103da09db658d1955bc6e80a6f994e70db971fff30ab497e95f2b08cef650d80e0b7c8201208763a914039d026f03732badf6719bd217778462a3e19a9088527c21027a3c9c9f92a2ea7a3016e25453677908519dffc0ae863f49b9bc5b9d7022287452ae677503101b06b175ac6851b27568101b0600" + } + } + }, + "revokedCommitPublished" : [ ] +} \ No newline at end of file diff --git a/eclair-core/src/test/resources/nonreg/codecs/05000a-DATA_CLOSING/revoked/data.bin b/eclair-core/src/test/resources/nonreg/codecs/05000a-DATA_CLOSING/revoked/data.bin new file mode 100644 index 0000000000..fd411fea94 --- /dev/null +++ b/eclair-core/src/test/resources/nonreg/codecs/05000a-DATA_CLOSING/revoked/data.bin @@ -0,0 +1 @@ +05000a014aae85cfe6079bf8622300701611cba1043b31bc34dc145c3d313b73217b975d01010002aaaa00ce2f18a967dc4f25f414e671ba6585f8ef0b8c5fb812c21064f55a2eaa0009d9004e3d37fca9468d44bceb4d79d21fc5100bf2755ae6ccafa16350c6a92ac480000001ff0000000000002710c000000000186b0200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000180802aa698202bbbb671d15145722fb8c28d732cddb249bcc6652ed2b297ff1f77a18371b1e63ff0000000000004e2003b449a5e5211504532d220acf96b254874e61c2f3f74c7b00c75f2d5ed22d6a0e031cbc0b96b0b0261f01c231b035483aeea0ab5f3308c744f1a5ee49e34be14c4102179e78d06c189b23b652034a9d0e58d2ac20610874089aa68612e3cec909bb720310d0c71ecc4f0d9bb37637ea28b155c4e8d5f96152954fc2f2149657e38a9574000000140800000000000000000000000000100802aa698200000000000000000000000000000000000000000005000000000000000200000001000000000000000000000000244aae85cfe6079bf8622300701611cba1043b31bc34dc145c3d313b73217b975d0000000000000000000f424002b2db12e8a2214f0ce18bed22e3804d4f66f8d54a51b2db494a4fae208646eba30400012401010101010101010101010101010101010101010101010101010101010101012a0000002b40420f0000000000220020dbd6b017f6487771e6e13bb607cfc21a62bbecab07d9e97ca534ff045d8de3db061a8000002a000000000202000000000000044c0000000000000000000000001dcd6500006402d0000000000000000b0000000009c4000000000bebc200000000002faf0800828ca4cec26d2a637bfc95b30f3492ba5dbb742c51efe2acbeb0e8bed369ac4c01bfebcc6507efb9fdffcf9bc88d03aab6b860fe3b10c806228396327dc3b2bf435638f630bc5b48cd10fb09555956ec293c17766d3e9fac5e76c5c1aae0c54560000000000000000003e800000000000003e8000000003b9aca00001e0090000000000000000c0000000009c4000000002faf0800000000000bebc2003efcff03a249d4793731b0873e9292c99dd744614f35a418d9cba5402207933f03805fdee9bae4ab5071dcd77531b164716a0b4399330d2bd2067fa60bb12ee85e000000ff0305bd1fc007f88a0211e93b43b631cf38dfd21e1668466aff1e29dfb2727bf0480002003d0000fffffffffff80103190772875654c5e99e9f578b0bfa1236696f1cdedbd7b98fc77b9d27648c03a801f00007ffffffffffa0040d0b5019052b1c41ac5019fe870e4251e0ad6c756a714bd2d63bfc15b5c33643b0000fffffffffff4000000000061a80160014761879f7b274ce995f87150a02e75cc0c037e8e300000000000000000001fd0268020000000001014aae85cfe6079bf8622300701611cba1043b31bc34dc145c3d313b73217b975d0000000000d600b680084a010000000000002200206843890682f05f877f4867aa7af467d6e94eb31c1fd80976f2a8c97fa95a9c6f4a01000000000000220020d27b5f1c3423e4d9ea86e57f88f61cb41225f80e865027b980719d6a6ad1150c5046000000000000220020d6fa299994adf089b0666721ce3800189884f63fa26c5c65825038e3fde0739a204e000000000000220020c85c2c90929348cabba661bce2faa8952659fb4e89fd314228c333f609cf5124a861000000000000220020655a4d87a9b88816c1a539190ee898be212a6b6fc265c1086bf94b8712768714b888000000000000220020988a30ffd1122da838ac8053aa4c528606ba80bfd01645db4982567718617bf39a0e0200000000002200205b95cec225d92369d10b0505cbee422b5fadedbceafb00b11119120da4a5da4b90a00b0000000000220020ab68e29d0d7f7e2d7e4bfdff40b71e856ae4e7c6ac2a4216644a838b486eea710400483045022100b048d2d1e247a2ad5655e8e9a2f06aa2546bd0ffff297e49e51e9541c1606c5502203f27e791dcb43d3e039113a2d08938d5db4cf69fddcbedac7f8979c43b91908101473044022021335f53ee0da33e9806b36a9dd0ed0d223ae16900d831a9b952c835921219ef0220136e50f2ca304f950548c1360fa3cf933e6a8a63acd0ac3d014d05bd20e82dea0147522102b2db12e8a2214f0ce18bed22e3804d4f66f8d54a51b2db494a4fae208646eba3210312a33052470d42f3639af368c670b9a85367c66913c1e4f8d1cff51dfb793d1852aedfbdb420ff241a14da37e63a7717b5730c78e742275eff4156a091f7524d64615b649a0c636d06000000ff241a14da37e63a7717b5730c78e742275eff4156a091f7524d64615b649a0c636d070000000004241a14da37e63a7717b5730c78e742275eff4156a091f7524d64615b649a0c636d02000000241a14da37e63a7717b5730c78e742275eff4156a091f7524d64615b649a0c636d03000000241a14da37e63a7717b5730c78e742275eff4156a091f7524d64615b649a0c636d04000000241a14da37e63a7717b5730c78e742275eff4156a091f7524d64615b649a0c636d050000000004249f5570244acfe1d2b3a5e1669bdbecdf1930c90b82f6529dc6ee3e1f10b504dc01000000249f5570244acfe1d2b3a5e1669bdbecdf1930c90b82f6529dc6ee3e1f10b504dc0300000024a3919e0eb605906d0b1984239ce36c4a55ac6eade7e016e9a0c4dd77be1449180000000024a3919e0eb605906d0b1984239ce36c4a55ac6eade7e016e9a0c4dd77be144918010000000007241a14da37e63a7717b5730c78e742275eff4156a091f7524d64615b649a0c636d07000000ed020000000001011a14da37e63a7717b5730c78e742275eff4156a091f7524d64615b649a0c636d0700000000ffffffff0121970b0000000000160014761879f7b274ce995f87150a02e75cc0c037e8e30347304402201f9b2335f2b32daacf539b8e000473299a5c79d041a5c2a99b5cf136b8968b86022070bbee1a0e3d340da44760be12787ecf770032bc76182d366e4544d8180229e80101014d6321031f2653e9dc5d6897dd1960d85a1ca55947e5daaeef0e00b67bb275215c43ae9167029000b2752103f461c4dabdc712bc13e82e531db63aa6c14b777412c5bbab7a2faf2d9328e5d768ac00000000241a14da37e63a7717b5730c78e742275eff4156a091f7524d64615b649a0c636d05000000fa02000000031a14da37e63a7717b5730c78e742275eff4156a091f7524d64615b649a0c636d0400000000010000001a14da37e63a7717b5730c78e742275eff4156a091f7524d64615b649a0c636d050000000001000000f2b1edcee612f9e6e789e89d89e90b6f1eacf4ff4f1761384243c1370ad2416200000000000100000003a861000000000000220020ab68e29d0d7f7e2d7e4bfdff40b71e856ae4e7c6ac2a4216644a838b486eea71b888000000000000220020ab68e29d0d7f7e2d7e4bfdff40b71e856ae4e7c6ac2a4216644a838b486eea71204e0000000000001600142867f2e206aeb0a25d195e2f9d1f9633870e7c6000000000241a14da37e63a7717b5730c78e742275eff4156a091f7524d64615b649a0c636d04000000fa02000000031a14da37e63a7717b5730c78e742275eff4156a091f7524d64615b649a0c636d0400000000010000001a14da37e63a7717b5730c78e742275eff4156a091f7524d64615b649a0c636d050000000001000000f2b1edcee612f9e6e789e89d89e90b6f1eacf4ff4f1761384243c1370ad2416200000000000100000003a861000000000000220020ab68e29d0d7f7e2d7e4bfdff40b71e856ae4e7c6ac2a4216644a838b486eea71b888000000000000220020ab68e29d0d7f7e2d7e4bfdff40b71e856ae4e7c6ac2a4216644a838b486eea71204e0000000000001600142867f2e206aeb0a25d195e2f9d1f9633870e7c6000000000244aae85cfe6079bf8622300701611cba1043b31bc34dc145c3d313b73217b975d00000000fd0268020000000001014aae85cfe6079bf8622300701611cba1043b31bc34dc145c3d313b73217b975d0000000000d600b680084a010000000000002200206843890682f05f877f4867aa7af467d6e94eb31c1fd80976f2a8c97fa95a9c6f4a01000000000000220020d27b5f1c3423e4d9ea86e57f88f61cb41225f80e865027b980719d6a6ad1150c5046000000000000220020d6fa299994adf089b0666721ce3800189884f63fa26c5c65825038e3fde0739a204e000000000000220020c85c2c90929348cabba661bce2faa8952659fb4e89fd314228c333f609cf5124a861000000000000220020655a4d87a9b88816c1a539190ee898be212a6b6fc265c1086bf94b8712768714b888000000000000220020988a30ffd1122da838ac8053aa4c528606ba80bfd01645db4982567718617bf39a0e0200000000002200205b95cec225d92369d10b0505cbee422b5fadedbceafb00b11119120da4a5da4b90a00b0000000000220020ab68e29d0d7f7e2d7e4bfdff40b71e856ae4e7c6ac2a4216644a838b486eea710400483045022100b048d2d1e247a2ad5655e8e9a2f06aa2546bd0ffff297e49e51e9541c1606c5502203f27e791dcb43d3e039113a2d08938d5db4cf69fddcbedac7f8979c43b91908101473044022021335f53ee0da33e9806b36a9dd0ed0d223ae16900d831a9b952c835921219ef0220136e50f2ca304f950548c1360fa3cf933e6a8a63acd0ac3d014d05bd20e82dea0147522102b2db12e8a2214f0ce18bed22e3804d4f66f8d54a51b2db494a4fae208646eba3210312a33052470d42f3639af368c670b9a85367c66913c1e4f8d1cff51dfb793d1852aedfbdb420241a14da37e63a7717b5730c78e742275eff4156a091f7524d64615b649a0c636d02000000fd01420200000004b11bba597c3c7106491d72f86e8117663e31e15b5ce2834d569f7507e8e730e80400000000010000001a14da37e63a7717b5730c78e742275eff4156a091f7524d64615b649a0c636d020000000001000000611c0e2921268db9b2fe0e51ac35ea72e5e96efa09b63548ab6181047aa1218d0400000000010000001a14da37e63a7717b5730c78e742275eff4156a091f7524d64615b649a0c636d03000000000100000004102700000000000016001442524c9d6f386bd3ce7d365b16f55d844f3e4ac75046000000000000220020ab68e29d0d7f7e2d7e4bfdff40b71e856ae4e7c6ac2a4216644a838b486eea71983a0000000000001600149d42dd01768a4837fa132fd6a8775d1c5b1956ad204e000000000000220020ab68e29d0d7f7e2d7e4bfdff40b71e856ae4e7c6ac2a4216644a838b486eea7100000000241a14da37e63a7717b5730c78e742275eff4156a091f7524d64615b649a0c636d06000000c4020000000001011a14da37e63a7717b5730c78e742275eff4156a091f7524d64615b649a0c636d06000000000100000001490a020000000000160014761879f7b274ce995f87150a02e75cc0c037e8e3024830450221008e72ddf230df3cd402ed27f3e63639b1411890f2c5effeb103541c44473d03c80220227f5a38d2d06724a41bbb0181f57112ee78af5c0709e5daa311e8f3d1b8db4d012521025ee8a9761dd7db6b48cd97e760692b03af498caa737743488be1f5911d91b943ad51b200000000241a14da37e63a7717b5730c78e742275eff4156a091f7524d64615b649a0c636d03000000fd01420200000004b11bba597c3c7106491d72f86e8117663e31e15b5ce2834d569f7507e8e730e80400000000010000001a14da37e63a7717b5730c78e742275eff4156a091f7524d64615b649a0c636d020000000001000000611c0e2921268db9b2fe0e51ac35ea72e5e96efa09b63548ab6181047aa1218d0400000000010000001a14da37e63a7717b5730c78e742275eff4156a091f7524d64615b649a0c636d03000000000100000004102700000000000016001442524c9d6f386bd3ce7d365b16f55d844f3e4ac75046000000000000220020ab68e29d0d7f7e2d7e4bfdff40b71e856ae4e7c6ac2a4216644a838b486eea71983a0000000000001600149d42dd01768a4837fa132fd6a8775d1c5b1956ad204e000000000000220020ab68e29d0d7f7e2d7e4bfdff40b71e856ae4e7c6ac2a4216644a838b486eea710000000000 \ No newline at end of file diff --git a/eclair-core/src/test/resources/nonreg/codecs/05000a-DATA_CLOSING/revoked/data.json b/eclair-core/src/test/resources/nonreg/codecs/05000a-DATA_CLOSING/revoked/data.json new file mode 100644 index 0000000000..6243f0f317 --- /dev/null +++ b/eclair-core/src/test/resources/nonreg/codecs/05000a-DATA_CLOSING/revoked/data.json @@ -0,0 +1,182 @@ +{ + "type" : "DATA_CLOSING", + "commitments" : { + "channelParams" : { + "channelId" : "4aae85cfe6079bf8622300701611cba1043b31bc34dc145c3d313b73217b975d", + "channelConfig" : [ "funding_pubkey_based_channel_keypath" ], + "channelFeatures" : [ ], + "localParams" : { + "nodeId" : "02aaaa00ce2f18a967dc4f25f414e671ba6585f8ef0b8c5fb812c21064f55a2eaa", + "fundingKeyPath" : [ 3640675901, 939305286, 2370092267, 1299829279, 3306163186, 1968891596, 2946589520, 3332975300, 2147483649 ], + "initialRequestedChannelReserve_opt" : 10000, + "isChannelOpener" : true, + "paysCommitTxFees" : true, + "initFeatures" : { + "activated" : { + "option_route_blinding" : "optional", + "option_provide_storage" : "optional", + "splice_prototype" : "optional", + "payment_secret" : "mandatory", + "gossip_queries_ex" : "optional", + "option_anchor_outputs" : "optional", + "option_quiesce" : "optional", + "option_data_loss_protect" : "optional", + "var_onion_optin" : "mandatory", + "option_static_remotekey" : "optional", + "option_support_large_channel" : "optional", + "option_anchors_zero_fee_htlc_tx" : "optional", + "option_channel_type" : "mandatory", + "basic_mpp" : "optional", + "gossip_queries" : "optional" + }, + "unknown" : [ 50001 ] + } + }, + "remoteParams" : { + "nodeId" : "02bbbb671d15145722fb8c28d732cddb249bcc6652ed2b297ff1f77a18371b1e63", + "initialRequestedChannelReserve_opt" : 20000, + "revocationBasepoint" : "03b449a5e5211504532d220acf96b254874e61c2f3f74c7b00c75f2d5ed22d6a0e", + "paymentBasepoint" : "031cbc0b96b0b0261f01c231b035483aeea0ab5f3308c744f1a5ee49e34be14c41", + "delayedPaymentBasepoint" : "02179e78d06c189b23b652034a9d0e58d2ac20610874089aa68612e3cec909bb72", + "htlcBasepoint" : "0310d0c71ecc4f0d9bb37637ea28b155c4e8d5f96152954fc2f2149657e38a9574", + "initFeatures" : { + "activated" : { + "option_route_blinding" : "optional", + "splice_prototype" : "optional", + "payment_secret" : "mandatory", + "gossip_queries_ex" : "optional", + "option_anchor_outputs" : "optional", + "option_quiesce" : "optional", + "option_data_loss_protect" : "optional", + "var_onion_optin" : "mandatory", + "option_static_remotekey" : "optional", + "option_support_large_channel" : "optional", + "option_anchors_zero_fee_htlc_tx" : "optional", + "option_channel_type" : "mandatory", + "basic_mpp" : "optional", + "gossip_queries" : "optional" + }, + "unknown" : [ ] + } + }, + "channelFlags" : { + "nonInitiatorPaysCommitFees" : false, + "announceChannel" : false + } + }, + "changes" : { + "localChanges" : { + "proposed" : [ ], + "signed" : [ ], + "acked" : [ ] + }, + "remoteChanges" : { + "proposed" : [ ], + "acked" : [ ], + "signed" : [ ] + }, + "localNextHtlcId" : 5, + "remoteNextHtlcId" : 2 + }, + "active" : [ { + "fundingTxIndex" : 0, + "fundingInput" : "5d977b21733b313d5c14dc34bc313b04a1cb111670002362f89b07e6cf85ae4a:0", + "fundingAmount" : 1000000, + "localFunding" : { + "status" : "confirmed", + "shortChannelId" : "400000x42x0" + }, + "remoteFunding" : { + "status" : "locked" + }, + "commitmentFormat" : "anchor_outputs", + "localCommitParams" : { + "dustLimit" : 1100, + "htlcMinimum" : 0, + "maxHtlcValueInFlight" : 500000000, + "maxAcceptedHtlcs" : 100, + "toSelfDelay" : 720 + }, + "localCommit" : { + "index" : 11, + "spec" : { + "htlcs" : [ ], + "commitTxFeerate" : 2500, + "toLocal" : 200000000, + "toRemote" : 800000000 + }, + "txId" : "828ca4cec26d2a637bfc95b30f3492ba5dbb742c51efe2acbeb0e8bed369ac4c", + "remoteSig" : { + "sig" : "bfebcc6507efb9fdffcf9bc88d03aab6b860fe3b10c806228396327dc3b2bf435638f630bc5b48cd10fb09555956ec293c17766d3e9fac5e76c5c1aae0c54560" + }, + "htlcRemoteSigs" : [ ] + }, + "remoteCommitParams" : { + "dustLimit" : 1000, + "htlcMinimum" : 1000, + "maxHtlcValueInFlight" : 1000000000, + "maxAcceptedHtlcs" : 30, + "toSelfDelay" : 144 + }, + "remoteCommit" : { + "index" : 12, + "spec" : { + "htlcs" : [ ], + "commitTxFeerate" : 2500, + "toLocal" : 800000000, + "toRemote" : 200000000 + }, + "txId" : "3efcff03a249d4793731b0873e9292c99dd744614f35a418d9cba5402207933f", + "remotePerCommitmentPoint" : "03805fdee9bae4ab5071dcd77531b164716a0b4399330d2bd2067fa60bb12ee85e" + } + } ], + "inactive" : [ ], + "remoteNextCommitInfo" : "0305bd1fc007f88a0211e93b43b631cf38dfd21e1668466aff1e29dfb2727bf048", + "remotePerCommitmentSecrets" : null, + "originChannels" : { } + }, + "waitingSince" : 400000, + "finalScriptPubKey" : "0014761879f7b274ce995f87150a02e75cc0c037e8e3", + "mutualCloseProposed" : [ ], + "mutualClosePublished" : [ ], + "revokedCommitPublished" : [ { + "commitTx" : { + "txid" : "6d630c9a645b61644d52f791a05641ff5e2742e7780c73b517773ae637da141a", + "tx" : "020000000001014aae85cfe6079bf8622300701611cba1043b31bc34dc145c3d313b73217b975d0000000000d600b680084a010000000000002200206843890682f05f877f4867aa7af467d6e94eb31c1fd80976f2a8c97fa95a9c6f4a01000000000000220020d27b5f1c3423e4d9ea86e57f88f61cb41225f80e865027b980719d6a6ad1150c5046000000000000220020d6fa299994adf089b0666721ce3800189884f63fa26c5c65825038e3fde0739a204e000000000000220020c85c2c90929348cabba661bce2faa8952659fb4e89fd314228c333f609cf5124a861000000000000220020655a4d87a9b88816c1a539190ee898be212a6b6fc265c1086bf94b8712768714b888000000000000220020988a30ffd1122da838ac8053aa4c528606ba80bfd01645db4982567718617bf39a0e0200000000002200205b95cec225d92369d10b0505cbee422b5fadedbceafb00b11119120da4a5da4b90a00b0000000000220020ab68e29d0d7f7e2d7e4bfdff40b71e856ae4e7c6ac2a4216644a838b486eea710400483045022100b048d2d1e247a2ad5655e8e9a2f06aa2546bd0ffff297e49e51e9541c1606c5502203f27e791dcb43d3e039113a2d08938d5db4cf69fddcbedac7f8979c43b91908101473044022021335f53ee0da33e9806b36a9dd0ed0d223ae16900d831a9b952c835921219ef0220136e50f2ca304f950548c1360fa3cf933e6a8a63acd0ac3d014d05bd20e82dea0147522102b2db12e8a2214f0ce18bed22e3804d4f66f8d54a51b2db494a4fae208646eba3210312a33052470d42f3639af368c670b9a85367c66913c1e4f8d1cff51dfb793d1852aedfbdb420" + }, + "localOutput_opt" : "6d630c9a645b61644d52f791a05641ff5e2742e7780c73b517773ae637da141a:6", + "remoteOutput_opt" : "6d630c9a645b61644d52f791a05641ff5e2742e7780c73b517773ae637da141a:7", + "htlcOutputs" : [ "6d630c9a645b61644d52f791a05641ff5e2742e7780c73b517773ae637da141a:2", "6d630c9a645b61644d52f791a05641ff5e2742e7780c73b517773ae637da141a:3", "6d630c9a645b61644d52f791a05641ff5e2742e7780c73b517773ae637da141a:4", "6d630c9a645b61644d52f791a05641ff5e2742e7780c73b517773ae637da141a:5" ], + "htlcDelayedOutputs" : [ "dc04b5101f3eeec69d52f6820bc93019dfecdb9b66e1a5b3d2e1cf4a2470559f:1", "dc04b5101f3eeec69d52f6820bc93019dfecdb9b66e1a5b3d2e1cf4a2470559f:3", "184914be77ddc4a0e916e0e7ad6eac554a6ce39c2384190b6d9005b60e9e91a3:0", "184914be77ddc4a0e916e0e7ad6eac554a6ce39c2384190b6d9005b60e9e91a3:1" ], + "irrevocablySpent" : { + "6d630c9a645b61644d52f791a05641ff5e2742e7780c73b517773ae637da141a:7" : { + "txid" : "a5f2d3c0c85e29dd97cbd0d492d3ccd75350092d9eff5cb155e103804c4120d9", + "tx" : "020000000001011a14da37e63a7717b5730c78e742275eff4156a091f7524d64615b649a0c636d0700000000ffffffff0121970b0000000000160014761879f7b274ce995f87150a02e75cc0c037e8e30347304402201f9b2335f2b32daacf539b8e000473299a5c79d041a5c2a99b5cf136b8968b86022070bbee1a0e3d340da44760be12787ecf770032bc76182d366e4544d8180229e80101014d6321031f2653e9dc5d6897dd1960d85a1ca55947e5daaeef0e00b67bb275215c43ae9167029000b2752103f461c4dabdc712bc13e82e531db63aa6c14b777412c5bbab7a2faf2d9328e5d768ac00000000" + }, + "6d630c9a645b61644d52f791a05641ff5e2742e7780c73b517773ae637da141a:5" : { + "txid" : "184914be77ddc4a0e916e0e7ad6eac554a6ce39c2384190b6d9005b60e9e91a3", + "tx" : "02000000031a14da37e63a7717b5730c78e742275eff4156a091f7524d64615b649a0c636d0400000000010000001a14da37e63a7717b5730c78e742275eff4156a091f7524d64615b649a0c636d050000000001000000f2b1edcee612f9e6e789e89d89e90b6f1eacf4ff4f1761384243c1370ad2416200000000000100000003a861000000000000220020ab68e29d0d7f7e2d7e4bfdff40b71e856ae4e7c6ac2a4216644a838b486eea71b888000000000000220020ab68e29d0d7f7e2d7e4bfdff40b71e856ae4e7c6ac2a4216644a838b486eea71204e0000000000001600142867f2e206aeb0a25d195e2f9d1f9633870e7c6000000000" + }, + "6d630c9a645b61644d52f791a05641ff5e2742e7780c73b517773ae637da141a:4" : { + "txid" : "184914be77ddc4a0e916e0e7ad6eac554a6ce39c2384190b6d9005b60e9e91a3", + "tx" : "02000000031a14da37e63a7717b5730c78e742275eff4156a091f7524d64615b649a0c636d0400000000010000001a14da37e63a7717b5730c78e742275eff4156a091f7524d64615b649a0c636d050000000001000000f2b1edcee612f9e6e789e89d89e90b6f1eacf4ff4f1761384243c1370ad2416200000000000100000003a861000000000000220020ab68e29d0d7f7e2d7e4bfdff40b71e856ae4e7c6ac2a4216644a838b486eea71b888000000000000220020ab68e29d0d7f7e2d7e4bfdff40b71e856ae4e7c6ac2a4216644a838b486eea71204e0000000000001600142867f2e206aeb0a25d195e2f9d1f9633870e7c6000000000" + }, + "5d977b21733b313d5c14dc34bc313b04a1cb111670002362f89b07e6cf85ae4a:0" : { + "txid" : "6d630c9a645b61644d52f791a05641ff5e2742e7780c73b517773ae637da141a", + "tx" : "020000000001014aae85cfe6079bf8622300701611cba1043b31bc34dc145c3d313b73217b975d0000000000d600b680084a010000000000002200206843890682f05f877f4867aa7af467d6e94eb31c1fd80976f2a8c97fa95a9c6f4a01000000000000220020d27b5f1c3423e4d9ea86e57f88f61cb41225f80e865027b980719d6a6ad1150c5046000000000000220020d6fa299994adf089b0666721ce3800189884f63fa26c5c65825038e3fde0739a204e000000000000220020c85c2c90929348cabba661bce2faa8952659fb4e89fd314228c333f609cf5124a861000000000000220020655a4d87a9b88816c1a539190ee898be212a6b6fc265c1086bf94b8712768714b888000000000000220020988a30ffd1122da838ac8053aa4c528606ba80bfd01645db4982567718617bf39a0e0200000000002200205b95cec225d92369d10b0505cbee422b5fadedbceafb00b11119120da4a5da4b90a00b0000000000220020ab68e29d0d7f7e2d7e4bfdff40b71e856ae4e7c6ac2a4216644a838b486eea710400483045022100b048d2d1e247a2ad5655e8e9a2f06aa2546bd0ffff297e49e51e9541c1606c5502203f27e791dcb43d3e039113a2d08938d5db4cf69fddcbedac7f8979c43b91908101473044022021335f53ee0da33e9806b36a9dd0ed0d223ae16900d831a9b952c835921219ef0220136e50f2ca304f950548c1360fa3cf933e6a8a63acd0ac3d014d05bd20e82dea0147522102b2db12e8a2214f0ce18bed22e3804d4f66f8d54a51b2db494a4fae208646eba3210312a33052470d42f3639af368c670b9a85367c66913c1e4f8d1cff51dfb793d1852aedfbdb420" + }, + "6d630c9a645b61644d52f791a05641ff5e2742e7780c73b517773ae637da141a:2" : { + "txid" : "dc04b5101f3eeec69d52f6820bc93019dfecdb9b66e1a5b3d2e1cf4a2470559f", + "tx" : "0200000004b11bba597c3c7106491d72f86e8117663e31e15b5ce2834d569f7507e8e730e80400000000010000001a14da37e63a7717b5730c78e742275eff4156a091f7524d64615b649a0c636d020000000001000000611c0e2921268db9b2fe0e51ac35ea72e5e96efa09b63548ab6181047aa1218d0400000000010000001a14da37e63a7717b5730c78e742275eff4156a091f7524d64615b649a0c636d03000000000100000004102700000000000016001442524c9d6f386bd3ce7d365b16f55d844f3e4ac75046000000000000220020ab68e29d0d7f7e2d7e4bfdff40b71e856ae4e7c6ac2a4216644a838b486eea71983a0000000000001600149d42dd01768a4837fa132fd6a8775d1c5b1956ad204e000000000000220020ab68e29d0d7f7e2d7e4bfdff40b71e856ae4e7c6ac2a4216644a838b486eea7100000000" + }, + "6d630c9a645b61644d52f791a05641ff5e2742e7780c73b517773ae637da141a:6" : { + "txid" : "555b854c21781834eac4a24d1e673dc24f1a50cec856a324876ae9fdbc09a860", + "tx" : "020000000001011a14da37e63a7717b5730c78e742275eff4156a091f7524d64615b649a0c636d06000000000100000001490a020000000000160014761879f7b274ce995f87150a02e75cc0c037e8e3024830450221008e72ddf230df3cd402ed27f3e63639b1411890f2c5effeb103541c44473d03c80220227f5a38d2d06724a41bbb0181f57112ee78af5c0709e5daa311e8f3d1b8db4d012521025ee8a9761dd7db6b48cd97e760692b03af498caa737743488be1f5911d91b943ad51b200000000" + }, + "6d630c9a645b61644d52f791a05641ff5e2742e7780c73b517773ae637da141a:3" : { + "txid" : "dc04b5101f3eeec69d52f6820bc93019dfecdb9b66e1a5b3d2e1cf4a2470559f", + "tx" : "0200000004b11bba597c3c7106491d72f86e8117663e31e15b5ce2834d569f7507e8e730e80400000000010000001a14da37e63a7717b5730c78e742275eff4156a091f7524d64615b649a0c636d020000000001000000611c0e2921268db9b2fe0e51ac35ea72e5e96efa09b63548ab6181047aa1218d0400000000010000001a14da37e63a7717b5730c78e742275eff4156a091f7524d64615b649a0c636d03000000000100000004102700000000000016001442524c9d6f386bd3ce7d365b16f55d844f3e4ac75046000000000000220020ab68e29d0d7f7e2d7e4bfdff40b71e856ae4e7c6ac2a4216644a838b486eea71983a0000000000001600149d42dd01768a4837fa132fd6a8775d1c5b1956ad204e000000000000220020ab68e29d0d7f7e2d7e4bfdff40b71e856ae4e7c6ac2a4216644a838b486eea7100000000" + } + } + } ] +} \ No newline at end of file diff --git a/eclair-core/src/test/resources/offers-test.json b/eclair-core/src/test/resources/offers-test.json index 891ed5673f..237ac08918 100644 --- a/eclair-core/src/test/resources/offers-test.json +++ b/eclair-core/src/test/resources/offers-test.json @@ -489,10 +489,15 @@ "bolt12": "lno1qcqcqzs9g9xyjs69zcssyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqsz" }, { - "description": "Malformed: invalid currency UTF-8", + "description": "Malformed: invalid currency UTF-8 (2-letter code)", "valid": false, "bolt12": "lno1qcpgqsg2q4q5cj2rg5tzzqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqg" }, + { + "description": "Malformed: invalid currency UTF-8 (3-letter code)", + "valid": false, + "bolt12": "lno1qcplllhapqpq86q2q4qkc6trv5tzzq6muh550qsfva9fdes0ruph7ctk2s8aqq06r4jxj3msc448wzwy9s" + }, { "description": "Malformed: truncated description UTF-8", "valid": false, @@ -573,6 +578,11 @@ "valid": false, "bolt12": "lno1pqpzwyqkyypwa3eyt44h6txtxquqh7lz5djge4afgfjn7k4rgrkuag0jsd5xvxg" }, + { + "description": "Missing offer_amount with offer_currency", + "valid": false, + "bolt12": "lno1qcp4256ypgx9getnwss8vetrw3hhyuckyypwa3eyt44h6txtxquqh7lz5djge4afgfjn7k4rgrkuag0jsd5xvxg" + }, { "description": "Missing offer_issuer_id and no offer_path", "valid": false, diff --git a/eclair-core/src/test/resources/scenarii/01-offer1.script b/eclair-core/src/test/resources/scenarii/01-offer1.script deleted file mode 100644 index 9e43b0f8af..0000000000 --- a/eclair-core/src/test/resources/scenarii/01-offer1.script +++ /dev/null @@ -1,15 +0,0 @@ -# Simple test that we can commit an HTLC -# Initial state: A=1000000 sat, B=1000000 sat, both fee rates=10000 sat -A:offer 1000000,9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08 -B:recvoffer -A:commit -B:recvcommit -A:recvrevoke -B:commit -A:recvcommit -B:recvrevoke -checksync -echo ***A*** -A:dump -echo ***B*** -B:dump diff --git a/eclair-core/src/test/resources/scenarii/01-offer1.script.expected b/eclair-core/src/test/resources/scenarii/01-offer1.script.expected deleted file mode 100644 index 0fc65dcbb7..0000000000 --- a/eclair-core/src/test/resources/scenarii/01-offer1.script.expected +++ /dev/null @@ -1,30 +0,0 @@ -***A*** -LOCAL COMMITS: - Commit 1: - Offered htlcs: (0,1000000 msat) - Received htlcs: - Balance us: 999000000 msat - Balance them: 1000000000 msat - Fee rate: 10000 -REMOTE COMMITS: - Commit 1: - Offered htlcs: - Received htlcs: (0,1000000 msat) - Balance us: 1000000000 msat - Balance them: 999000000 msat - Fee rate: 10000 -***B*** -LOCAL COMMITS: - Commit 1: - Offered htlcs: - Received htlcs: (0,1000000 msat) - Balance us: 1000000000 msat - Balance them: 999000000 msat - Fee rate: 10000 -REMOTE COMMITS: - Commit 1: - Offered htlcs: (0,1000000 msat) - Received htlcs: - Balance us: 999000000 msat - Balance them: 1000000000 msat - Fee rate: 10000 diff --git a/eclair-core/src/test/resources/scenarii/02-offer2.script b/eclair-core/src/test/resources/scenarii/02-offer2.script deleted file mode 100644 index 2a7da7929b..0000000000 --- a/eclair-core/src/test/resources/scenarii/02-offer2.script +++ /dev/null @@ -1,18 +0,0 @@ -# Simple test that we can commit two HTLCs -# Initial state: A=1000000 sat, B=1000000 sat, both fee rates=10000 sat -A:offer 1000000,7b3d979ca8330a94fa7e9e1b466d8b99e0bcdea1ec90596c0dcc8d7ef6b4300c -A:offer 2000000,6016bcc377c93692f2fe19fbad47eee6fb8f4cc98c56e935db5edb69806d84f6 -A:commit -B:recvoffer -B:recvoffer -B:recvcommit -A:recvrevoke -B:commit -A:recvcommit -B:recvrevoke -checksync -echo ***A*** -A:dump -echo ***B*** -B:dump - diff --git a/eclair-core/src/test/resources/scenarii/02-offer2.script.expected b/eclair-core/src/test/resources/scenarii/02-offer2.script.expected deleted file mode 100644 index d31361786e..0000000000 --- a/eclair-core/src/test/resources/scenarii/02-offer2.script.expected +++ /dev/null @@ -1,30 +0,0 @@ -***A*** -LOCAL COMMITS: - Commit 1: - Offered htlcs: (0,1000000 msat) (1,2000000 msat) - Received htlcs: - Balance us: 997000000 msat - Balance them: 1000000000 msat - Fee rate: 10000 -REMOTE COMMITS: - Commit 1: - Offered htlcs: - Received htlcs: (0,1000000 msat) (1,2000000 msat) - Balance us: 1000000000 msat - Balance them: 997000000 msat - Fee rate: 10000 -***B*** -LOCAL COMMITS: - Commit 1: - Offered htlcs: - Received htlcs: (0,1000000 msat) (1,2000000 msat) - Balance us: 1000000000 msat - Balance them: 997000000 msat - Fee rate: 10000 -REMOTE COMMITS: - Commit 1: - Offered htlcs: (0,1000000 msat) (1,2000000 msat) - Received htlcs: - Balance us: 997000000 msat - Balance them: 1000000000 msat - Fee rate: 10000 diff --git a/eclair-core/src/test/resources/scenarii/03-fulfill1.script b/eclair-core/src/test/resources/scenarii/03-fulfill1.script deleted file mode 100644 index 9d3cd3fea1..0000000000 --- a/eclair-core/src/test/resources/scenarii/03-fulfill1.script +++ /dev/null @@ -1,25 +0,0 @@ -# Simple test that we can fulfill (remove) an HTLC after fully settled. -# Initial state: A=1000000 sat, B=1000000 sat, both fee rates=10000 sat -A:offer 1000000,b8928207364d445daa42f4ba8be0ef661b8d7190206c01f6ee8281566b3dec0a -B:recvoffer -A:commit -B:recvcommit -A:recvrevoke -B:commit -A:recvcommit -B:recvrevoke - -B:fulfill 0,60303ae22b998861bce3b28f33eec1be758a213c86c93c076dbe9f558c11c752 -B:commit -A:recvremove -A:recvcommit -B:recvrevoke -A:commit -B:recvcommit -A:recvrevoke - -checksync -echo ***A*** -A:dump -echo ***B*** -B:dump diff --git a/eclair-core/src/test/resources/scenarii/03-fulfill1.script.expected b/eclair-core/src/test/resources/scenarii/03-fulfill1.script.expected deleted file mode 100644 index a81561bc8f..0000000000 --- a/eclair-core/src/test/resources/scenarii/03-fulfill1.script.expected +++ /dev/null @@ -1,30 +0,0 @@ -***A*** -LOCAL COMMITS: - Commit 2: - Offered htlcs: - Received htlcs: - Balance us: 999000000 msat - Balance them: 1001000000 msat - Fee rate: 10000 -REMOTE COMMITS: - Commit 2: - Offered htlcs: - Received htlcs: - Balance us: 1001000000 msat - Balance them: 999000000 msat - Fee rate: 10000 -***B*** -LOCAL COMMITS: - Commit 2: - Offered htlcs: - Received htlcs: - Balance us: 1001000000 msat - Balance them: 999000000 msat - Fee rate: 10000 -REMOTE COMMITS: - Commit 2: - Offered htlcs: - Received htlcs: - Balance us: 999000000 msat - Balance them: 1001000000 msat - Fee rate: 10000 diff --git a/eclair-core/src/test/resources/scenarii/04-two-commits-onedir.script b/eclair-core/src/test/resources/scenarii/04-two-commits-onedir.script deleted file mode 100644 index 0feb2e8819..0000000000 --- a/eclair-core/src/test/resources/scenarii/04-two-commits-onedir.script +++ /dev/null @@ -1,22 +0,0 @@ -# Test A can commit twice; queueing two commits onto B's queue. -# Initial state: A=1000000 sat, B=0, both fee rates=10000 sat -A:offer 1000000,7b3d979ca8330a94fa7e9e1b466d8b99e0bcdea1ec90596c0dcc8d7ef6b4300c -B:recvoffer -A:commit -B:recvcommit -A:recvrevoke - -A:offer 2000000,6016bcc377c93692f2fe19fbad47eee6fb8f4cc98c56e935db5edb69806d84f6 -B:recvoffer -A:commit -B:recvcommit -A:recvrevoke - -B:commit -A:recvcommit -B:recvrevoke -checksync -echo ***A*** -A:dump -echo ***B*** -B:dump diff --git a/eclair-core/src/test/resources/scenarii/04-two-commits-onedir.script.expected b/eclair-core/src/test/resources/scenarii/04-two-commits-onedir.script.expected deleted file mode 100644 index c309a14e41..0000000000 --- a/eclair-core/src/test/resources/scenarii/04-two-commits-onedir.script.expected +++ /dev/null @@ -1,30 +0,0 @@ -***A*** -LOCAL COMMITS: - Commit 1: - Offered htlcs: (0,1000000) (1,2000000) - Received htlcs: - Balance us: 997000000 msat - Balance them: 1000000000 msat - Fee rate: 10000 -REMOTE COMMITS: - Commit 2: - Offered htlcs: - Received htlcs: (0,1000000) (1,2000000) - Balance us: 1000000000 msat - Balance them: 997000000 msat - Fee rate: 10000 -***B*** -LOCAL COMMITS: - Commit 2: - Offered htlcs: - Received htlcs: (0,1000000) (1,2000000) - Balance us: 1000000000 msat - Balance them: 997000000 msat - Fee rate: 10000 -REMOTE COMMITS: - Commit 1: - Offered htlcs: (0,1000000) (1,2000000) - Received htlcs: - Balance us: 997000000 msat - Balance them: 1000000000 msat - Fee rate: 10000 diff --git a/eclair-core/src/test/resources/scenarii/05-two-commits-in-flight.script b/eclair-core/src/test/resources/scenarii/05-two-commits-in-flight.script deleted file mode 100644 index 0feb2fab8f..0000000000 --- a/eclair-core/src/test/resources/scenarii/05-two-commits-in-flight.script +++ /dev/null @@ -1,20 +0,0 @@ -# Test committing before receiving previous revocation. -A:nocommitwait -A:offer 1 -A:commit -A:offer 3 -A:commit -B:recvoffer -B:recvcommit -B:recvoffer -B:recvcommit -A:recvrevoke -A:recvrevoke -B:commit -A:recvcommit -B:recvrevoke -checksync -echo ***A*** -A:dump -echo ***B*** -B:dump diff --git a/eclair-core/src/test/resources/scenarii/05-two-commits-in-flight.script.expected b/eclair-core/src/test/resources/scenarii/05-two-commits-in-flight.script.expected deleted file mode 100644 index 69503312d6..0000000000 --- a/eclair-core/src/test/resources/scenarii/05-two-commits-in-flight.script.expected +++ /dev/null @@ -1,22 +0,0 @@ -***A*** -LOCAL COMMITS: - Commit 1: - Offered htlcs: 1 3 - Received htlcs: - SIGNED -REMOTE COMMITS: - Commit 2: - Offered htlcs: - Received htlcs: 1 3 - SIGNED -***B*** -LOCAL COMMITS: - Commit 2: - Offered htlcs: - Received htlcs: 1 3 - SIGNED -REMOTE COMMITS: - Commit 1: - Offered htlcs: 1 3 - Received htlcs: - SIGNED diff --git a/eclair-core/src/test/resources/scenarii/10-offers-crossover.script b/eclair-core/src/test/resources/scenarii/10-offers-crossover.script deleted file mode 100644 index cbb893a4bf..0000000000 --- a/eclair-core/src/test/resources/scenarii/10-offers-crossover.script +++ /dev/null @@ -1,24 +0,0 @@ -# Offers which cross over still get resolved. -# Initial state: A=1000000 sat, B=1000000, both fee rates=10000 sat - -A:offer 1000000,7b3d979ca8330a94fa7e9e1b466d8b99e0bcdea1ec90596c0dcc8d7ef6b4300c -A:commit -B:offer 2000000,6016bcc377c93692f2fe19fbad47eee6fb8f4cc98c56e935db5edb69806d84f6 -B:recvoffer -B:recvcommit -B:commit - -A:recvoffer -A:recvrevoke -A:recvcommit -B:recvrevoke - -A:commit -B:recvcommit -A:recvrevoke - -checksync -echo ***A*** -A:dump -echo ***B*** -B:dump diff --git a/eclair-core/src/test/resources/scenarii/10-offers-crossover.script.expected b/eclair-core/src/test/resources/scenarii/10-offers-crossover.script.expected deleted file mode 100644 index a259e358d1..0000000000 --- a/eclair-core/src/test/resources/scenarii/10-offers-crossover.script.expected +++ /dev/null @@ -1,30 +0,0 @@ -***A*** -LOCAL COMMITS: - Commit 1: - Offered htlcs: (0,1000000 msat) - Received htlcs: (0,2000000 msat) - Balance us: 999000000 msat - Balance them: 998000000 msat - Fee rate: 10000 -REMOTE COMMITS: - Commit 2: - Offered htlcs: (0,2000000 msat) - Received htlcs: (0,1000000 msat) - Balance us: 998000000 msat - Balance them: 999000000 msat - Fee rate: 10000 -***B*** -LOCAL COMMITS: - Commit 2: - Offered htlcs: (0,2000000 msat) - Received htlcs: (0,1000000 msat) - Balance us: 998000000 msat - Balance them: 999000000 msat - Fee rate: 10000 -REMOTE COMMITS: - Commit 1: - Offered htlcs: (0,1000000 msat) - Received htlcs: (0,2000000 msat) - Balance us: 999000000 msat - Balance them: 998000000 msat - Fee rate: 10000 diff --git a/eclair-core/src/test/resources/scenarii/11-commits-crossover.script b/eclair-core/src/test/resources/scenarii/11-commits-crossover.script deleted file mode 100644 index ccd72ca84c..0000000000 --- a/eclair-core/src/test/resources/scenarii/11-commits-crossover.script +++ /dev/null @@ -1,29 +0,0 @@ -# Commits which cross over still get resolved. -# Initial state: A=1000000 sat, B=1000000, both fee rates=10000 sat - -A:offer 1000000,7b3d979ca8330a94fa7e9e1b466d8b99e0bcdea1ec90596c0dcc8d7ef6b4300c -B:offer 2000000,6016bcc377c93692f2fe19fbad47eee6fb8f4cc98c56e935db5edb69806d84f6 -A:commit -B:commit - -A:recvoffer -B:recvoffer -A:recvcommit -B:recvcommit - -A:recvrevoke -B:recvrevoke - -# They've got to come into sync eventually! -A:commit -B:commit -A:recvcommit -B:recvcommit -A:recvrevoke -B:recvrevoke - -checksync -echo ***A*** -A:dump -echo ***B*** -B:dump diff --git a/eclair-core/src/test/resources/scenarii/11-commits-crossover.script.expected b/eclair-core/src/test/resources/scenarii/11-commits-crossover.script.expected deleted file mode 100644 index f47357ea01..0000000000 --- a/eclair-core/src/test/resources/scenarii/11-commits-crossover.script.expected +++ /dev/null @@ -1,30 +0,0 @@ -***A*** -LOCAL COMMITS: - Commit 2: - Offered htlcs: (0,1000000 msat) - Received htlcs: (0,2000000 msat) - Balance us: 999000000 msat - Balance them: 998000000 msat - Fee rate: 10000 -REMOTE COMMITS: - Commit 2: - Offered htlcs: (0,2000000 msat) - Received htlcs: (0,1000000 msat) - Balance us: 998000000 msat - Balance them: 999000000 msat - Fee rate: 10000 -***B*** -LOCAL COMMITS: - Commit 2: - Offered htlcs: (0,2000000 msat) - Received htlcs: (0,1000000 msat) - Balance us: 998000000 msat - Balance them: 999000000 msat - Fee rate: 10000 -REMOTE COMMITS: - Commit 2: - Offered htlcs: (0,1000000 msat) - Received htlcs: (0,2000000 msat) - Balance us: 999000000 msat - Balance them: 998000000 msat - Fee rate: 10000 diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala index b449d431e3..6ae9b7dc87 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala @@ -107,10 +107,10 @@ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with I assert(open.fundingTxFeerate_opt.contains(FeeratePerKw(1250 sat))) // check that minimum fee rate of 253 sat/bw is used - eclair.open(nodeId, fundingAmount = 10000000L sat, pushAmount_opt = None, channelType_opt = Some(ChannelTypes.StaticRemoteKey()), fundingFeerate_opt = Some(FeeratePerByte(1 sat)), fundingFeeBudget_opt = None, announceChannel_opt = None, openTimeout_opt = None) + eclair.open(nodeId, fundingAmount = 10000000L sat, pushAmount_opt = None, channelType_opt = Some(ChannelTypes.AnchorOutputsZeroFeeHtlcTx()), fundingFeerate_opt = Some(FeeratePerByte(1 sat)), fundingFeeBudget_opt = None, announceChannel_opt = None, openTimeout_opt = None) val open1 = switchboard.expectMsgType[OpenChannel] assert(open1.fundingTxFeerate_opt.contains(FeeratePerKw.MinimumFeeratePerKw)) - assert(open1.channelType_opt.contains(ChannelTypes.StaticRemoteKey())) + assert(open1.channelType_opt.contains(ChannelTypes.AnchorOutputsZeroFeeHtlcTx())) } test("call send with passing correct arguments") { f => @@ -231,11 +231,11 @@ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with I assert(Announcements.isNode1(b, c)) val channels = SortedMap(Seq( - (ann_ab, makeUpdateShort(ShortChannelId(1L), a, b, feeBase = 0 msat, 0, minHtlc = 0 msat, maxHtlc = None, cltvDelta = CltvExpiryDelta(13))), - (ann_ae, makeUpdateShort(ShortChannelId(4L), a, e, feeBase = 0 msat, 0, minHtlc = 0 msat, maxHtlc = None, cltvDelta = CltvExpiryDelta(12))), - (ann_bc, makeUpdateShort(ShortChannelId(2L), b, c, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, cltvDelta = CltvExpiryDelta(500))), - (ann_cd, makeUpdateShort(ShortChannelId(3L), c, d, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, cltvDelta = CltvExpiryDelta(500))), - (ann_ec, makeUpdateShort(ShortChannelId(7L), e, c, feeBase = 2 msat, 0, minHtlc = 0 msat, maxHtlc = None, cltvDelta = CltvExpiryDelta(12))) + (ann_ab, makeUpdateShort(ShortChannelId(1L), a, b, feeBase = 0 msat, 0, minHtlc = 0 msat, maxHtlc = 100000 msat, cltvDelta = CltvExpiryDelta(13))), + (ann_ae, makeUpdateShort(ShortChannelId(4L), a, e, feeBase = 0 msat, 0, minHtlc = 0 msat, maxHtlc = 100000 msat, cltvDelta = CltvExpiryDelta(12))), + (ann_bc, makeUpdateShort(ShortChannelId(2L), b, c, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = 100000 msat, cltvDelta = CltvExpiryDelta(500))), + (ann_cd, makeUpdateShort(ShortChannelId(3L), c, d, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = 100000 msat, cltvDelta = CltvExpiryDelta(500))), + (ann_ec, makeUpdateShort(ShortChannelId(7L), e, c, feeBase = 2 msat, 0, minHtlc = 0 msat, maxHtlc = 100000 msat, cltvDelta = CltvExpiryDelta(12))) ).map { case (ann, update) => update.shortChannelId -> PublicChannel(ann, TxId(ByteVector32.Zeroes), 100 sat, Some(update.copy(channelFlags = ChannelUpdate.ChannelFlags.DUMMY)), None, None) }: _*) @@ -271,19 +271,19 @@ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with I val eclair = new EclairImpl(kit) - eclair.forceClose(Left(ByteVector32.Zeroes) :: Nil) + eclair.forceClose(Left(ByteVector32.Zeroes) :: Nil, None) register.expectMsg(Register.Forward(null, ByteVector32.Zeroes, CMD_FORCECLOSE(ActorRef.noSender))) eclair.bumpForceCloseFee(Left(ByteVector32.Zeroes) :: Nil, ConfirmationTarget.Priority(ConfirmationPriority.Medium)) register.expectMsgType[Register.Forward[CMD_BUMP_FORCE_CLOSE_FEE]] - eclair.forceClose(Right(ShortChannelId.fromCoordinates("568749x2597x0").success.value) :: Nil) + eclair.forceClose(Right(ShortChannelId.fromCoordinates("568749x2597x0").success.value) :: Nil, None) register.expectMsg(Register.ForwardShortId(null, ShortChannelId.fromCoordinates("568749x2597x0").success.value, CMD_FORCECLOSE(ActorRef.noSender))) eclair.bumpForceCloseFee(Right(ShortChannelId.fromCoordinates("568749x2597x0").success.value) :: Nil, ConfirmationTarget.Priority(ConfirmationPriority.Fast)) register.expectMsgType[Register.ForwardShortId[CMD_BUMP_FORCE_CLOSE_FEE]] - eclair.forceClose(Left(ByteVector32.Zeroes) :: Right(ShortChannelId.fromCoordinates("568749x2597x0").success.value) :: Nil) + eclair.forceClose(Left(ByteVector32.Zeroes) :: Right(ShortChannelId.fromCoordinates("568749x2597x0").success.value) :: Nil, None) register.expectMsgAllOf( Register.Forward(null, ByteVector32.Zeroes, CMD_FORCECLOSE(ActorRef.noSender)), Register.ForwardShortId(null, ShortChannelId.fromCoordinates("568749x2597x0").success.value, CMD_FORCECLOSE(ActorRef.noSender)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/StartupSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/StartupSpec.scala index acf2157cd8..805f9f22e0 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/StartupSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/StartupSpec.scala @@ -39,8 +39,8 @@ class StartupSpec extends AnyFunSuite { def makeNodeParamsWithDefaults(conf: Config): NodeParams = { val blockCount = new AtomicLong(0) val feerates = new AtomicReference(FeeratesPerKw.single(feeratePerKw)) - val nodeKeyManager = new LocalNodeKeyManager(randomBytes32(), chainHash = Block.Testnet3GenesisBlock.hash) - val channelKeyManager = new LocalChannelKeyManager(randomBytes32(), chainHash = Block.Testnet3GenesisBlock.hash) + val nodeKeyManager = LocalNodeKeyManager(randomBytes32(), chainHash = Block.Testnet3GenesisBlock.hash) + val channelKeyManager = LocalChannelKeyManager(randomBytes32(), chainHash = Block.Testnet3GenesisBlock.hash) val db = TestDatabases.inMemoryDb() NodeParams.makeNodeParams(conf, UUID.fromString("01234567-0123-4567-89ab-0123456789ab"), nodeKeyManager, channelKeyManager, None, None, db, blockCount, feerates) } @@ -340,9 +340,9 @@ class StartupSpec extends AnyFunSuite { ) val nodeParams = makeNodeParamsWithDefaults(perNodeConf.withFallback(defaultConf)) - assert(nodeParams.onChainFeeConf.feerateToleranceFor(PublicKey(hex"031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f")) == FeerateTolerance(0.1, 15.0, FeeratePerKw(FeeratePerByte(15 sat)), DustTolerance(25_000 sat, closeOnUpdateFeeOverflow = true))) - assert(nodeParams.onChainFeeConf.feerateToleranceFor(PublicKey(hex"03462779ad4aad39514614751a71085f2f10e1c7a593e4e030efb5b8721ce55b0b")) == FeerateTolerance(0.75, 5.0, FeeratePerKw(FeeratePerByte(5 sat)), DustTolerance(40_000 sat, closeOnUpdateFeeOverflow = false))) - assert(nodeParams.onChainFeeConf.feerateToleranceFor(PublicKey(hex"0362c0a046dacce86ddd0343c6d3c7c79c2208ba0d9c9cf24a6d046d21d21f90f7")) == FeerateTolerance(0.5, 10.0, FeeratePerKw(FeeratePerByte(10 sat)), DustTolerance(50_000 sat, closeOnUpdateFeeOverflow = false))) + assert(nodeParams.onChainFeeConf.feerateToleranceFor(PublicKey(hex"031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f")) == FeerateTolerance(0.1, 15.0, FeeratePerByte(15 sat).perKw, DustTolerance(25_000 sat, closeOnUpdateFeeOverflow = true))) + assert(nodeParams.onChainFeeConf.feerateToleranceFor(PublicKey(hex"03462779ad4aad39514614751a71085f2f10e1c7a593e4e030efb5b8721ce55b0b")) == FeerateTolerance(0.75, 5.0, FeeratePerByte(5 sat).perKw, DustTolerance(40_000 sat, closeOnUpdateFeeOverflow = false))) + assert(nodeParams.onChainFeeConf.feerateToleranceFor(PublicKey(hex"0362c0a046dacce86ddd0343c6d3c7c79c2208ba0d9c9cf24a6d046d21d21f90f7")) == FeerateTolerance(0.5, 10.0, FeeratePerByte(10 sat).perKw, DustTolerance(50_000 sat, closeOnUpdateFeeOverflow = false))) } test("NodeParams should fail if htlc-minimum-msat is set to 0") { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala index f1ac1f0c67..97b8a51464 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala @@ -19,9 +19,9 @@ package fr.acinq.eclair import akka.actor.ActorRef import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, Satoshi, SatoshiLong} import fr.acinq.eclair.blockchain.fee._ +import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.fsm.Channel.{ChannelConf, RemoteRbfLimits, UnhandledExceptionStrategy} -import fr.acinq.eclair.channel.{ChannelFlags, LocalParams, Origin, Upstream} -import fr.acinq.eclair.crypto.keymanager.{LocalChannelKeyManager, LocalNodeKeyManager} +import fr.acinq.eclair.crypto.keymanager._ import fr.acinq.eclair.db.RevokedHtlcInfoCleaner import fr.acinq.eclair.io.MessageRelay.RelayAll import fr.acinq.eclair.io.{OpenChannelInterceptor, PeerConnection, PeerReadyNotifier} @@ -29,7 +29,8 @@ import fr.acinq.eclair.message.OnionMessages.OnionMessageConfig import fr.acinq.eclair.payment.offer.OffersConfig import fr.acinq.eclair.payment.relay.OnTheFlyFunding import fr.acinq.eclair.payment.relay.Relayer.{AsyncPaymentsParams, RelayFees, RelayParams} -import fr.acinq.eclair.router.Graph.{MessageWeightRatios, PaymentWeightRatios} +import fr.acinq.eclair.reputation.Reputation +import fr.acinq.eclair.router.Graph.{HeuristicsConstants, MessageWeightRatios} import fr.acinq.eclair.router.Router._ import fr.acinq.eclair.router.{PathFindingExperimentConf, Router} import fr.acinq.eclair.wire.protocol._ @@ -52,6 +53,7 @@ object TestConstants { val nonInitiatorPushAmount: MilliSatoshi = 100_000_000L msat val feeratePerKw: FeeratePerKw = FeeratePerKw(10_000 sat) val anchorOutputsFeeratePerKw: FeeratePerKw = FeeratePerKw(2_500 sat) + val phoenixCommitFeeratePerKw: FeeratePerKw = FeeratePerByte(1 sat).perKw val defaultLiquidityRates: LiquidityAds.WillFundRates = LiquidityAds.WillFundRates( fundingRates = LiquidityAds.FundingRate(100_000 sat, 10_000_000 sat, 500, 100, 100 sat, 1000 sat) :: Nil, paymentTypes = Set(LiquidityAds.PaymentType.FromChannelBalance) @@ -81,8 +83,11 @@ object TestConstants { object Alice { val seed: ByteVector32 = ByteVector32(hex"b4acd47335b25ab7b84b8c020997b12018592bb4631b868762154d77fa8b93a3") // 02aaaa... - val nodeKeyManager = new LocalNodeKeyManager(seed, Block.RegtestGenesisBlock.hash) - val channelKeyManager = new LocalChannelKeyManager(seed, Block.RegtestGenesisBlock.hash) + val nodeKeyManager: NodeKeyManager = LocalNodeKeyManager(seed, Block.RegtestGenesisBlock.hash) + val channelKeyManager: ChannelKeyManager = LocalChannelKeyManager(seed, Block.RegtestGenesisBlock.hash) + private val fundingKeyPath = channelKeyManager.newFundingKeyPath(isChannelOpener = true) + + def channelKeys(channelConfig: ChannelConfig = ChannelConfig.standard): ChannelKeys = channelKeyManager.channelKeys(channelConfig, fundingKeyPath) // This is a function, and not a val! When called will return a new NodeParams def nodeParams: NodeParams = NodeParams( @@ -106,7 +111,9 @@ object TestConstants { Features.Wumbo -> FeatureSupport.Optional, Features.PaymentMetadata -> FeatureSupport.Optional, Features.RouteBlinding -> FeatureSupport.Optional, + Features.ShutdownAnySegwit -> FeatureSupport.Optional, Features.StaticRemoteKey -> FeatureSupport.Mandatory, + Features.AnchorOutputsZeroFeeHtlcTx -> FeatureSupport.Optional, Features.Quiescence -> FeatureSupport.Optional, Features.SplicePrototype -> FeatureSupport.Optional, Features.ProvideStorage -> FeatureSupport.Optional, @@ -151,10 +158,10 @@ object TestConstants { quiescenceTimeout = 2 minutes, balanceThresholds = Nil, minTimeBetweenUpdates = 0 hours, - acceptIncomingStaticRemoteKeyChannels = false ), onChainFeeConf = OnChainFeeConf( feeTargets = FeeTargets(funding = ConfirmationPriority.Medium, closing = ConfirmationPriority.Medium), + maxClosingFeerate = FeeratePerKw(15_000 sat), safeUtxosThreshold = 0, spendAnchorWithoutHtlcs = true, anchorWithoutHtlcsMaxFee = 100_000.sat, @@ -174,7 +181,9 @@ object TestConstants { feeBase = 548000 msat, feeProportionalMillionths = 30), enforcementDelay = 10 minutes, - asyncPaymentsParams = AsyncPaymentsParams(1008, CltvExpiryDelta(144))), + asyncPaymentsParams = AsyncPaymentsParams(1008, CltvExpiryDelta(144)), + peerReputationConfig = Reputation.Config(enabled = true, 1 day, 10 minutes), + ), db = TestDatabases.inMemoryDb(), autoReconnect = false, initialRandomReconnectDelay = 5 seconds, @@ -196,6 +205,7 @@ object TestConstants { ), routerConf = RouterConf( watchSpentWindow = 1 second, + channelSpentSpliceDelay = 12, channelExcludeDuration = 60 seconds, routerBroadcastInterval = 1 day, // "disables" rebroadcast syncConf = Router.SyncConf( @@ -213,16 +223,17 @@ object TestConstants { maxFeeProportional = 0.03, maxCltv = CltvExpiryDelta(2016), maxRouteLength = 20), - heuristics = PaymentWeightRatios( - baseFactor = 1.0, - cltvDeltaFactor = 0.0, - ageFactor = 0.0, - capacityFactor = 0.0, + heuristics = HeuristicsConstants( + lockedFundsRisk = 0, + failureFees = RelayFees(0 msat, 0), hopFees = RelayFees(0 msat, 0), + useLogProbability = false, + usePastRelaysData = false ), mpp = MultiPartParams( minPartAmount = 15000000 msat, maxParts = 10, + splittingStrategy = MultiPartParams.FullCapacity ), experimentName = "alice-test-experiment", experimentPercentage = 100))), @@ -240,7 +251,7 @@ object TestConstants { onionMessageConfig = OnionMessageConfig( relayPolicy = RelayAll, minIntermediateHops = 9, - timeout = 200 millis, + timeout = 40 seconds, maxAttempts = 2, ), purgeInvoicesInterval = None, @@ -252,25 +263,27 @@ object TestConstants { offersConfig = OffersConfig(messagePathMinLength = 2, paymentPathCount = 2, paymentPathLength = 4, paymentPathCltvExpiryDelta = CltvExpiryDelta(500)), ) - def channelParams: LocalParams = OpenChannelInterceptor.makeChannelParams( + def channelParams: LocalChannelParams = OpenChannelInterceptor.makeChannelParams( nodeParams, nodeParams.features.initFeatures(), None, - None, isChannelOpener = true, paysCommitTxFees = true, dualFunded = false, - fundingSatoshis, - unlimitedMaxHtlcValueInFlight = false, + fundingSatoshis ).copy( + fundingKeyPath = fundingKeyPath, initialRequestedChannelReserve_opt = Some(10_000 sat) // Bob will need to keep that much satoshis in his balance ) } object Bob { val seed: ByteVector32 = ByteVector32(hex"7620226fec887b0b2ebe76492e5a3fd3eb0e47cd3773263f6a81b59a704dc492") // 02bbbb... - val nodeKeyManager = new LocalNodeKeyManager(seed, Block.RegtestGenesisBlock.hash) - val channelKeyManager = new LocalChannelKeyManager(seed, Block.RegtestGenesisBlock.hash) + val nodeKeyManager: NodeKeyManager = LocalNodeKeyManager(seed, Block.RegtestGenesisBlock.hash) + val channelKeyManager: ChannelKeyManager = LocalChannelKeyManager(seed, Block.RegtestGenesisBlock.hash) + private val fundingKeyPath = channelKeyManager.newFundingKeyPath(isChannelOpener = false) + + def channelKeys(channelConfig: ChannelConfig = ChannelConfig.standard): ChannelKeys = channelKeyManager.channelKeys(channelConfig, fundingKeyPath) def nodeParams: NodeParams = NodeParams( nodeKeyManager, @@ -292,6 +305,7 @@ object TestConstants { Features.Wumbo -> FeatureSupport.Optional, Features.PaymentMetadata -> FeatureSupport.Optional, Features.RouteBlinding -> FeatureSupport.Optional, + Features.ShutdownAnySegwit -> FeatureSupport.Optional, Features.StaticRemoteKey -> FeatureSupport.Mandatory, Features.AnchorOutputsZeroFeeHtlcTx -> FeatureSupport.Optional, Features.Quiescence -> FeatureSupport.Optional, @@ -317,8 +331,8 @@ object TestConstants { maxChannelSpentRescanBlocks = 144, htlcMinimum = 1000 msat, minDepth = 3, - toRemoteDelay = CltvExpiryDelta(144), - maxToLocalDelay = CltvExpiryDelta(1000), + toRemoteDelay = CltvExpiryDelta(720), + maxToLocalDelay = CltvExpiryDelta(2016), reserveToFundingRatio = 0.01, // note: not used (overridden below) maxReserveToFundingRatio = 0.05, unhandledExceptionStrategy = UnhandledExceptionStrategy.LocalClose, @@ -335,10 +349,10 @@ object TestConstants { quiescenceTimeout = 2 minutes, balanceThresholds = Nil, minTimeBetweenUpdates = 0 hour, - acceptIncomingStaticRemoteKeyChannels = false ), onChainFeeConf = OnChainFeeConf( feeTargets = FeeTargets(funding = ConfirmationPriority.Medium, closing = ConfirmationPriority.Medium), + maxClosingFeerate = FeeratePerKw(15_000 sat), safeUtxosThreshold = 0, spendAnchorWithoutHtlcs = true, anchorWithoutHtlcsMaxFee = 100_000.sat, @@ -358,7 +372,9 @@ object TestConstants { feeBase = 548000 msat, feeProportionalMillionths = 30), enforcementDelay = 10 minutes, - asyncPaymentsParams = AsyncPaymentsParams(1008, CltvExpiryDelta(144))), + asyncPaymentsParams = AsyncPaymentsParams(1008, CltvExpiryDelta(144)), + peerReputationConfig = Reputation.Config(enabled = true, 2 day, 20 minutes), + ), db = TestDatabases.inMemoryDb(), autoReconnect = false, initialRandomReconnectDelay = 5 seconds, @@ -380,6 +396,7 @@ object TestConstants { ), routerConf = RouterConf( watchSpentWindow = 1 second, + channelSpentSpliceDelay = 12, channelExcludeDuration = 60 seconds, routerBroadcastInterval = 1 day, // "disables" rebroadcast syncConf = Router.SyncConf( @@ -397,16 +414,17 @@ object TestConstants { maxFeeProportional = 0.03, maxCltv = CltvExpiryDelta(2016), maxRouteLength = 20), - heuristics = PaymentWeightRatios( - baseFactor = 1.0, - cltvDeltaFactor = 0.0, - ageFactor = 0.0, - capacityFactor = 0.0, + heuristics = HeuristicsConstants( + lockedFundsRisk = 0, + failureFees = RelayFees(0 msat, 0), hopFees = RelayFees(0 msat, 0), + useLogProbability = false, + usePastRelaysData = false ), mpp = MultiPartParams( minPartAmount = 15000000 msat, maxParts = 10, + splittingStrategy = MultiPartParams.FullCapacity ), experimentName = "bob-test-experiment", experimentPercentage = 100))), @@ -424,7 +442,7 @@ object TestConstants { onionMessageConfig = OnionMessageConfig( relayPolicy = RelayAll, minIntermediateHops = 8, - timeout = 100 millis, + timeout = 30 seconds, maxAttempts = 2, ), purgeInvoicesInterval = None, @@ -436,17 +454,16 @@ object TestConstants { offersConfig = OffersConfig(messagePathMinLength = 2, paymentPathCount = 2, paymentPathLength = 4, paymentPathCltvExpiryDelta = CltvExpiryDelta(500)), ) - def channelParams: LocalParams = OpenChannelInterceptor.makeChannelParams( + def channelParams: LocalChannelParams = OpenChannelInterceptor.makeChannelParams( nodeParams, nodeParams.features.initFeatures(), None, - None, isChannelOpener = false, paysCommitTxFees = false, dualFunded = false, - fundingSatoshis, - unlimitedMaxHtlcValueInFlight = false, + fundingSatoshis ).copy( + fundingKeyPath = fundingKeyPath, initialRequestedChannelReserve_opt = Some(20_000 sat) // Alice will need to keep that much satoshis in her balance ) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/TestDatabases.scala b/eclair-core/src/test/scala/fr/acinq/eclair/TestDatabases.scala index dbb4775563..99b1c7def8 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/TestDatabases.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/TestDatabases.scala @@ -110,7 +110,6 @@ object TestDatabases { } case class TestPgDatabases() extends TestDatabases { - val datasource: DataSource = getNewDatabase() val hikariConfig = new HikariConfig hikariConfig.setDataSource(datasource) @@ -165,8 +164,7 @@ object TestDatabases { initializeTables: Connection => Unit, dbName: String, targetVersion: Int, - postCheck: Connection => Unit - ): Unit = { + postCheck: Connection => Unit): Unit = { val connection = dbs.connection // initialize the database to a previous version and populate data initializeTables(connection) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/balance/CheckBalanceSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/balance/CheckBalanceSpec.scala index e380760364..a7fa658f78 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/balance/CheckBalanceSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/balance/CheckBalanceSpec.scala @@ -1,31 +1,26 @@ package fr.acinq.eclair.balance -import akka.pattern.pipe -import akka.testkit.TestProbe -import fr.acinq.bitcoin.scalacompat.{ByteVector32, OutPoint, SatoshiLong, TxId} -import fr.acinq.eclair.TestUtils.randomTxId -import fr.acinq.eclair.balance.CheckBalance.{ClosingBalance, MainAndHtlcBalance, OffChainBalance, PossiblyPublishedMainAndHtlcBalance, PossiblyPublishedMainBalance} +import fr.acinq.bitcoin.scalacompat.SatoshiLong +import fr.acinq.eclair.balance.CheckBalance.{MainAndHtlcBalance, OffChainBalance} import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.{apply => _, _} -import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinCoreClient -import fr.acinq.eclair.channel.Helpers.Closing.{CurrentRemoteClose, LocalClose} -import fr.acinq.eclair.channel.publish.TxPublisher.{PublishFinalTx, PublishReplaceableTx} +import fr.acinq.eclair.channel._ +import fr.acinq.eclair.channel.publish.TxPublisher.PublishReplaceableTx +import fr.acinq.eclair.channel.states.ChannelStateTestsBase.PimpTestFSM import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags} -import fr.acinq.eclair.channel.{CLOSING, CMD_SIGN, DATA_CLOSING, DATA_NORMAL, Upstream} import fr.acinq.eclair.db.jdbc.JdbcUtils.ExtendedResultSet._ import fr.acinq.eclair.db.pg.PgUtils.using +import fr.acinq.eclair.testutils.PimpTestProbe.convert +import fr.acinq.eclair.transactions.Transactions.{ClaimHtlcSuccessTx, ClaimHtlcTimeoutTx, ClaimRemoteAnchorTx} import fr.acinq.eclair.wire.internal.channel.ChannelCodecs.channelDataCodec -import fr.acinq.eclair.wire.protocol.{CommitSig, Error, RevokeAndAck, TlvStream, UpdateAddHtlc, UpdateAddHtlcTlv} -import fr.acinq.eclair.{BlockHeight, CltvExpiry, CltvExpiryDelta, MilliSatoshiLong, TestConstants, TestKitBaseClass, TimestampMilli, ToMilliSatoshiConversion, randomBytes32} -import org.scalatest.{Outcome, Tag} +import fr.acinq.eclair.wire.protocol.{CommitSig, RevokeAndAck} +import fr.acinq.eclair.{BlockHeight, MilliSatoshiLong, TestConstants, TestKitBaseClass, ToMilliSatoshiConversion} import org.scalatest.funsuite.FixtureAnyFunSuiteLike +import org.scalatest.{Outcome, Tag} import org.sqlite.SQLiteConfig import java.io.File import java.sql.DriverManager -import scala.collection.immutable.Queue -import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.duration.DurationInt -import scala.concurrent.{ExecutionContext, Future} class CheckBalanceSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with ChannelStateTestsBase { @@ -42,18 +37,29 @@ class CheckBalanceSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with test("do not deduplicate htlc amounts") { f => import f._ - // We add 3 identical htlcs Alice -> Bob + // We add 3 identical outgoing htlcs Alice -> Bob. addHtlc(10_000_000 msat, alice, bob, alice2bob, bob2alice) addHtlc(10_000_000 msat, alice, bob, alice2bob, bob2alice) addHtlc(10_000_000 msat, alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) - - assert(CheckBalance.computeOffChainBalance(Seq(alice.stateData.asInstanceOf[DATA_NORMAL]), knownPreimages = Set.empty).normal == - MainAndHtlcBalance( - toLocal = (TestConstants.fundingSatoshis - TestConstants.initiatorPushAmount - 30_000_000.msat).truncateToSatoshi, - htlcs = 30_000.sat - ) + val expected1 = MainAndHtlcBalance( + toLocal = (TestConstants.fundingSatoshis - TestConstants.initiatorPushAmount - 30_000_000.msat).truncateToSatoshi, + htlcs = 0 sat, // outgoing HTLCs are considered paid ) + assert(CheckBalance.computeOffChainBalance(Seq(alice.stateData.asInstanceOf[DATA_NORMAL]), recentlySpentInputs = Set.empty).normal == expected1) + + // We add 3 identical incoming htlcs Bob -> Alice. + addHtlc(20_000_000 msat, bob, alice, bob2alice, alice2bob) + addHtlc(20_000_000 msat, bob, alice, bob2alice, alice2bob) + addHtlc(20_000_000 msat, bob, alice, bob2alice, alice2bob) + crossSign(bob, alice, bob2alice, alice2bob) + val expected2 = expected1.copy(htlcs = 60_000 sat) + assert(CheckBalance.computeOffChainBalance(Seq(alice.stateData.asInstanceOf[DATA_NORMAL]), recentlySpentInputs = Set.empty).normal == expected2) + + // We add our balance to an existing off-chain balance. + val previous = OffChainBalance(normal = MainAndHtlcBalance(toLocal = 100_000 sat, htlcs = 25_000 sat)) + val expected3 = expected2.copy(toLocal = expected2.toLocal + 100_000.sat, htlcs = 85_000 sat) + assert(previous.addChannelBalance(alice.stateData.asInstanceOf[DATA_NORMAL], recentlySpentInputs = Set.empty).normal == expected3) } test("take in-flight signed fulfills into account") { f => @@ -65,168 +71,219 @@ class CheckBalanceSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with bob ! CMD_SIGN() bob2alice.expectMsgType[CommitSig] - assert(CheckBalance.computeOffChainBalance(Seq(bob.stateData.asInstanceOf[DATA_NORMAL]), knownPreimages = Set.empty).normal == - MainAndHtlcBalance( - toLocal = TestConstants.initiatorPushAmount.truncateToSatoshi, - htlcs = htlc.amountMsat.truncateToSatoshi - ) + val expected = MainAndHtlcBalance( + toLocal = TestConstants.initiatorPushAmount.truncateToSatoshi, + htlcs = htlc.amountMsat.truncateToSatoshi + ) + assert(CheckBalance.computeOffChainBalance(Seq(bob.stateData.asInstanceOf[DATA_NORMAL]), recentlySpentInputs = Set.empty).normal == expected) + } + + test("channel closing with unpublished closing tx", Tag(ChannelStateTestsTags.SimpleClose)) { f => + import f._ + + mutualClose(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain) + val expected = MainAndHtlcBalance( + toLocal = (TestConstants.fundingSatoshis - TestConstants.initiatorPushAmount).truncateToSatoshi, + htlcs = 0 sat, ) + assert(CheckBalance.computeOffChainBalance(Seq(alice.stateData.asInstanceOf[DATA_NEGOTIATING_SIMPLE]), recentlySpentInputs = Set.empty).negotiating == expected) + } + + test("channel closing with published closing tx", Tag(ChannelStateTestsTags.SimpleClose)) { f => + import f._ + + mutualClose(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain) + val closingTxInput = alice.stateData.asInstanceOf[DATA_NEGOTIATING_SIMPLE].commitments.latest.fundingInput + val expected = MainAndHtlcBalance(toLocal = 0 sat, htlcs = 0 sat) + assert(CheckBalance.computeOffChainBalance(Seq(alice.stateData.asInstanceOf[DATA_NEGOTIATING_SIMPLE]), recentlySpentInputs = Set(closingTxInput)).negotiating == expected) + } + + test("channel closing with published closing tx (without option_simple_close)") { f => + import f._ + + mutualClose(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain) + assert(alice.stateData.asInstanceOf[DATA_CLOSING].mutualClosePublished.nonEmpty) + val closingTxInput = alice.stateData.asInstanceOf[DATA_CLOSING].commitments.latest.fundingInput + val expected = MainAndHtlcBalance(toLocal = 0 sat, htlcs = 0 sat) + assert(CheckBalance.computeOffChainBalance(Seq(alice.stateData.asInstanceOf[DATA_CLOSING]), recentlySpentInputs = Set(closingTxInput)).closing == expected) } - test("take published remote commit tx into account", Tag(ChannelStateTestsTags.StaticRemoteKey), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + test("channel closed with remote commit tx") { f => import f._ // We add 3 htlcs Alice -> Bob (one of them below dust) and 2 htlcs Bob -> Alice - addHtlc(250000000 msat, alice, bob, alice2bob, bob2alice) - val (ra2, htlca2) = addHtlc(100000000 msat, alice, bob, alice2bob, bob2alice) - val (_, htlca3) = addHtlc(10000 msat, alice, bob, alice2bob, bob2alice) - val (rb1, htlcb1) = addHtlc(50000000 msat, bob, alice, bob2alice, alice2bob) - val (_, htlcb2) = addHtlc(55000000 msat, bob, alice, bob2alice, alice2bob) + addHtlc(250_000_000 msat, alice, bob, alice2bob, bob2alice) + val (ra, htlca) = addHtlc(100_000_000 msat, alice, bob, alice2bob, bob2alice) + addHtlc(10_000 msat, alice, bob, alice2bob, bob2alice) + val (rb, htlcb) = addHtlc(50_000_000 msat, bob, alice, bob2alice, alice2bob) + addHtlc(55_000_000 msat, bob, alice, bob2alice, alice2bob) crossSign(alice, bob, alice2bob, bob2alice) - // And fulfill one htlc in each direction without signing a new commit tx - fulfillHtlc(htlca2.id, ra2, bob, alice, bob2alice, alice2bob) - fulfillHtlc(htlcb1.id, rb1, alice, bob, alice2bob, bob2alice) + // And fulfill one htlc in each direction without signing a new commit tx. + fulfillHtlc(htlca.id, ra, bob, alice, bob2alice, alice2bob) + fulfillHtlc(htlcb.id, rb, alice, bob, alice2bob, bob2alice) - // bob publishes his current commit tx - val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + // Bob publishes his current commit tx. + val bobCommitTx = bob.signCommitTx() assert(bobCommitTx.txOut.size == 8) // two anchor outputs, two main outputs and 4 pending htlcs alice ! WatchFundingSpentTriggered(bobCommitTx) - // in response to that, alice publishes her claim txs - alice2blockchain.expectMsgType[PublishReplaceableTx] // claim-anchor - alice2blockchain.expectMsgType[PublishFinalTx] // claim-main - val claimHtlcTxs = (1 to 3).map(_ => alice2blockchain.expectMsgType[PublishReplaceableTx].txInfo.tx) - - val commitments = alice.stateData.asInstanceOf[DATA_CLOSING].commitments - val remoteCommitPublished = alice.stateData.asInstanceOf[DATA_CLOSING].remoteCommitPublished.get - val knownPreimages = Set((commitments.channelId, htlcb1.id)) - assert(CheckBalance.computeRemoteCloseBalance(commitments, CurrentRemoteClose(commitments.active.last.remoteCommit, remoteCommitPublished), knownPreimages) == - PossiblyPublishedMainAndHtlcBalance( - toLocal = Map(OutPoint(remoteCommitPublished.claimMainOutputTx.get.tx.txid, 0) -> remoteCommitPublished.claimMainOutputTx.get.tx.txOut.head.amount), - htlcs = claimHtlcTxs.map(claimTx => OutPoint(claimTx.txid, 0) -> claimTx.txOut.head.amount.toBtc).toMap, - htlcsUnpublished = htlca3.amountMsat.truncateToSatoshi - )) - // assuming alice gets the preimage for the 2nd htlc - val knownPreimages1 = Set((commitments.channelId, htlcb1.id), (commitments.channelId, htlcb2.id)) - assert(CheckBalance.computeRemoteCloseBalance(commitments, CurrentRemoteClose(commitments.active.last.remoteCommit, remoteCommitPublished), knownPreimages1) == - PossiblyPublishedMainAndHtlcBalance( - toLocal = Map(OutPoint(remoteCommitPublished.claimMainOutputTx.get.tx.txid, 0) -> remoteCommitPublished.claimMainOutputTx.get.tx.txOut.head.amount), - htlcs = claimHtlcTxs.map(claimTx => OutPoint(claimTx.txid, 0) -> claimTx.txOut.head.amount.toBtc).toMap, - htlcsUnpublished = htlca3.amountMsat.truncateToSatoshi + htlcb2.amountMsat.truncateToSatoshi - )) + // In response to that, alice publishes her claim txs. + alice2blockchain.expectReplaceableTxPublished[ClaimRemoteAnchorTx] + val claimMain = alice2blockchain.expectFinalTxPublished("remote-main-delayed") + val mainAmount = bobCommitTx.txOut(claimMain.input.index.toInt).amount + val claimHtlcTxs = (1 to 3).map(_ => alice2blockchain.expectMsgType[PublishReplaceableTx].txInfo) + assert(claimHtlcTxs.collect { case tx: ClaimHtlcSuccessTx => tx }.size == 1) + assert(claimHtlcTxs.collect { case tx: ClaimHtlcTimeoutTx => tx }.size == 2) + alice ! WatchTxConfirmedTriggered(BlockHeight(600_000), 5, bobCommitTx) + awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].remoteCommitPublished.exists(_.isConfirmed)) + + // We already have an off-chain balance from other channels. + val balance = OffChainBalance( + normal = MainAndHtlcBalance(toLocal = 100_000_000 sat, htlcs = 250_000 sat), + closing = MainAndHtlcBalance(toLocal = 50_000_000 sat, htlcs = 100_000 sat), + ) + // We add our main balance and the amount of the incoming HTLCs. + val expected1 = balance.copy(closing = MainAndHtlcBalance(toLocal = 50_000_000.sat + mainAmount, htlcs = 100_000.sat + 50_000.sat + 55_000.sat)) + val balance1 = balance.addChannelBalance(alice.stateData.asInstanceOf[DATA_CLOSING], recentlySpentInputs = Set.empty) + assert(balance1 == expected1) + // When our transactions are in the mempool or recently confirmed, we stop including them in our off-chain balance. + val expected2 = balance.copy(closing = MainAndHtlcBalance(toLocal = 50_000_000.sat, htlcs = 100_000.sat + 50_000.sat + 55_000.sat)) + val balance2 = balance.addChannelBalance(alice.stateData.asInstanceOf[DATA_CLOSING], recentlySpentInputs = Set(claimMain.input)) + assert(balance2 == expected2) + val htlcOutpoint = claimHtlcTxs.collectFirst { case tx: ClaimHtlcSuccessTx if tx.htlcId == htlcb.id => tx.input.outPoint }.get + val expected3 = balance.copy(closing = MainAndHtlcBalance(toLocal = 50_000_000.sat, htlcs = 100_000.sat + 55_000.sat)) + val balance3 = balance.addChannelBalance(alice.stateData.asInstanceOf[DATA_CLOSING], recentlySpentInputs = Set(claimMain.input, htlcOutpoint)) + assert(balance3 == expected3) + // When our HTLC transaction confirms, we stop including it in our off-chain balance: it appears in our on-chain balance. + alice ! WatchTxConfirmedTriggered(BlockHeight(601_000), 2, claimHtlcTxs.collectFirst { case tx: ClaimHtlcSuccessTx => tx }.get.tx) + awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].remoteCommitPublished.get.irrevocablySpent.size == 2) + val expected4 = balance.copy(closing = MainAndHtlcBalance(toLocal = 50_000_000.sat + mainAmount, htlcs = 100_000.sat + 55_000.sat)) + val balance4 = balance.addChannelBalance(alice.stateData.asInstanceOf[DATA_CLOSING], recentlySpentInputs = Set.empty) + assert(balance4 == expected4) } - test("take published next remote commit tx into account", Tag(ChannelStateTestsTags.StaticRemoteKey), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + test("channel closed with next remote commit tx") { f => import f._ - // We add 3 htlcs Alice -> Bob (one of them below dust) and 2 htlcs Bob -> Alice - addHtlc(250000000 msat, alice, bob, alice2bob, bob2alice) - val (ra2, htlca2) = addHtlc(100000000 msat, alice, bob, alice2bob, bob2alice) - val (_, htlca3) = addHtlc(10000 msat, alice, bob, alice2bob, bob2alice) - val (rb1, htlcb1) = addHtlc(50000000 msat, bob, alice, bob2alice, alice2bob) - val (_, htlcb2) = addHtlc(55000000 msat, bob, alice, bob2alice, alice2bob) + // We add 3 htlcs Alice -> Bob (one of them below dust) and 2 htlcs Bob -> Alice. + addHtlc(250_000_000 msat, alice, bob, alice2bob, bob2alice) + addHtlc(100_000_000 msat, alice, bob, alice2bob, bob2alice) + addHtlc(10_000 msat, alice, bob, alice2bob, bob2alice) + addHtlc(50_000_000 msat, bob, alice, bob2alice, alice2bob) + val (_, htlcb) = addHtlc(55_000_000 msat, bob, alice, bob2alice, alice2bob) crossSign(alice, bob, alice2bob, bob2alice) - // And fulfill one htlc in each direction - fulfillHtlc(htlca2.id, ra2, bob, alice, bob2alice, alice2bob) - fulfillHtlc(htlcb1.id, rb1, alice, bob, alice2bob, bob2alice) - // alice signs but we intercept bob's revocation + // Alice fails one of her incoming htlcs. + failHtlc(htlcb.id, alice, bob, alice2bob, bob2alice) + // Alice signs but we intercept bob's revocation. alice ! CMD_SIGN() alice2bob.expectMsgType[CommitSig] alice2bob.forward(bob) bob2alice.expectMsgType[RevokeAndAck] - // as far as alice knows, bob currently has two valid unrevoked commitment transactions - // bob publishes his current commit tx - val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.active.last.localCommit.commitTxAndRemoteSig.commitTx.tx + // Bob publishes his next commit tx. + val bobCommitTx = bob.signCommitTx() assert(bobCommitTx.txOut.size == 7) // two anchor outputs, two main outputs and 3 pending htlcs alice ! WatchFundingSpentTriggered(bobCommitTx) - - // in response to that, alice publishes her claim txs - alice2blockchain.expectMsgType[PublishReplaceableTx] // claim-anchor - alice2blockchain.expectMsgType[PublishFinalTx] // claim-main - val claimHtlcTxs = (1 to 2).map(_ => alice2blockchain.expectMsgType[PublishReplaceableTx].txInfo.tx) - - val commitments = alice.stateData.asInstanceOf[DATA_CLOSING].commitments - val remoteCommitPublished = alice.stateData.asInstanceOf[DATA_CLOSING].nextRemoteCommitPublished.get - val knownPreimages = Set((commitments.channelId, htlcb1.id)) - assert(CheckBalance.computeRemoteCloseBalance(commitments, CurrentRemoteClose(commitments.active.last.nextRemoteCommit_opt.get.commit, remoteCommitPublished), knownPreimages) == - PossiblyPublishedMainAndHtlcBalance( - toLocal = Map(OutPoint(remoteCommitPublished.claimMainOutputTx.get.tx.txid, 0) -> remoteCommitPublished.claimMainOutputTx.get.tx.txOut.head.amount), - htlcs = claimHtlcTxs.map(claimTx => OutPoint(claimTx.txid, 0) -> claimTx.txOut.head.amount.toBtc).toMap, - htlcsUnpublished = htlca3.amountMsat.truncateToSatoshi - )) - // assuming alice gets the preimage for the 2nd htlc - val knownPreimages1 = Set((commitments.channelId, htlcb1.id), (commitments.channelId, htlcb2.id)) - assert(CheckBalance.computeRemoteCloseBalance(commitments, CurrentRemoteClose(commitments.active.last.nextRemoteCommit_opt.get.commit, remoteCommitPublished), knownPreimages1) == - PossiblyPublishedMainAndHtlcBalance( - toLocal = Map(OutPoint(remoteCommitPublished.claimMainOutputTx.get.tx.txid, 0) -> remoteCommitPublished.claimMainOutputTx.get.tx.txOut.head.amount), - htlcs = claimHtlcTxs.map(claimTx => OutPoint(claimTx.txid, 0) -> claimTx.txOut.head.amount.toBtc).toMap, - htlcsUnpublished = htlca3.amountMsat.truncateToSatoshi + htlcb2.amountMsat.truncateToSatoshi - )) + // In response to that, alice publishes her claim txs + alice2blockchain.expectReplaceableTxPublished[ClaimRemoteAnchorTx] + val claimMain = alice2blockchain.expectFinalTxPublished("remote-main-delayed") + val mainAmount = bobCommitTx.txOut(claimMain.input.index.toInt).amount + (1 to 2).map(_ => alice2blockchain.expectReplaceableTxPublished[ClaimHtlcTimeoutTx]) + alice ! WatchTxConfirmedTriggered(BlockHeight(600_000), 5, bobCommitTx) + awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].nextRemoteCommitPublished.exists(_.isConfirmed)) + + // We already have an off-chain balance from other channels. + val balance = OffChainBalance( + negotiating = MainAndHtlcBalance(toLocal = 200_000_000 sat, htlcs = 150_000 sat), + closing = MainAndHtlcBalance(toLocal = 20_000_000 sat, htlcs = 0 sat), + ) + // We add our main balance and the amount of the remaining incoming HTLC. + val expected1 = balance.copy(closing = MainAndHtlcBalance(toLocal = 20_000_000.sat + mainAmount, htlcs = 50_000.sat)) + val balance1 = balance.addChannelBalance(alice.stateData.asInstanceOf[DATA_CLOSING], recentlySpentInputs = Set.empty) + assert(balance1 == expected1) + // We deduplicate our main balance with our on-chain balance. + val expected2 = balance.copy(closing = MainAndHtlcBalance(toLocal = 20_000_000.sat, htlcs = 50_000.sat)) + val balance2 = balance.addChannelBalance(alice.stateData.asInstanceOf[DATA_CLOSING], recentlySpentInputs = Set(claimMain.input)) + assert(balance2 == expected2) } - test("take published local commit tx into account") { f => + test("channel closed with local commit tx") { f => import f._ - // We add 4 htlcs Alice -> Bob (one of them below dust) and 2 htlcs Bob -> Alice - val (_, htlca1) = addHtlc(250000000 msat, alice, bob, alice2bob, bob2alice) - val (ra2, htlca2) = addHtlc(100000000 msat, alice, bob, alice2bob, bob2alice) - val (_, htlca3) = addHtlc(10000 msat, alice, bob, alice2bob, bob2alice) - // for this one we set a non-local upstream to simulate a relayed payment - val (_, htlca4) = addHtlc(30000000 msat, CltvExpiryDelta(144), alice, bob, alice2bob, bob2alice, upstream = Upstream.Hot.Trampoline(Upstream.Hot.Channel(UpdateAddHtlc(randomBytes32(), 42, 30003000 msat, randomBytes32(), CltvExpiry(144), TestConstants.emptyOnionPacket, TlvStream.empty[UpdateAddHtlcTlv]), TimestampMilli(1687345927000L), TestConstants.Alice.nodeParams.nodeId) :: Nil), replyTo = TestProbe().ref) - val (rb1, htlcb1) = addHtlc(50000000 msat, bob, alice, bob2alice, alice2bob) - val (_, _) = addHtlc(55000000 msat, bob, alice, bob2alice, alice2bob) + // We add 2 htlcs Alice -> Bob and 4 htlcs Bob -> Alice (one of them below dust). + addHtlc(50_000_000 msat, alice, bob, alice2bob, bob2alice) + addHtlc(60_000_000 msat, alice, bob, alice2bob, bob2alice) + val (_, htlcb1) = addHtlc(35_000_000 msat, bob, alice, bob2alice, alice2bob) + val (rb1, htlcb2) = addHtlc(30_000_000 msat, bob, alice, bob2alice, alice2bob) + addHtlc(50_000 msat, bob, alice, bob2alice, alice2bob) + val (rb2, htlcb3) = addHtlc(25_000_000 msat, bob, alice, bob2alice, alice2bob) crossSign(alice, bob, alice2bob, bob2alice) - // And fulfill one htlc in each direction without signing a new commit tx - fulfillHtlc(htlca2.id, ra2, bob, alice, bob2alice, alice2bob) - fulfillHtlc(htlcb1.id, rb1, alice, bob, alice2bob, bob2alice) - - // alice publishes her commit tx - val aliceCommitTx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.active.last.localCommit.commitTxAndRemoteSig.commitTx.tx - alice ! Error(ByteVector32.Zeroes, "oops") - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == aliceCommitTx.txid) - assert(aliceCommitTx.txOut.size == 7) // two main outputs and 5 pending htlcs (one is dust) - awaitCond(alice.stateName == CLOSING) - assert(alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.isDefined) - val commitments = alice.stateData.asInstanceOf[DATA_CLOSING].commitments - val localCommitPublished = alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.get - val knownPreimages = Set((commitments.channelId, htlcb1.id)) - assert(CheckBalance.computeLocalCloseBalance(commitments.changes, LocalClose(commitments.active.last.localCommit, localCommitPublished), commitments.originChannels, knownPreimages) == - PossiblyPublishedMainAndHtlcBalance( - toLocal = Map(OutPoint(localCommitPublished.claimMainDelayedOutputTx.get.tx.txid, 0) -> localCommitPublished.claimMainDelayedOutputTx.get.tx.txOut.head.amount), - htlcs = Map.empty, - htlcsUnpublished = htlca4.amountMsat.truncateToSatoshi + htlcb1.amountMsat.truncateToSatoshi - )) - - alice2blockchain.expectMsgType[PublishFinalTx] // claim-main - val htlcTx1 = alice2blockchain.expectMsgType[PublishFinalTx] - val htlcTx2 = alice2blockchain.expectMsgType[PublishFinalTx] - val htlcTx3 = alice2blockchain.expectMsgType[PublishFinalTx] - val htlcTx4 = alice2blockchain.expectMsgType[PublishFinalTx] - alice2blockchain.expectMsgType[WatchTxConfirmed] // commit tx - alice2blockchain.expectMsgType[WatchTxConfirmed] // main-delayed - alice2blockchain.expectMsgType[WatchOutputSpent] // htlc 1 - alice2blockchain.expectMsgType[WatchOutputSpent] // htlc 2 - alice2blockchain.expectMsgType[WatchOutputSpent] // htlc 3 - alice2blockchain.expectMsgType[WatchOutputSpent] // htlc 4 - alice2blockchain.expectMsgType[WatchOutputSpent] // htlc 5 - - // 3rd-stage txs are published when htlc-timeout txs confirm - val claimHtlcDelayedTxs = Seq(htlcTx1, htlcTx2, htlcTx3, htlcTx4).map { htlcTimeoutTx => - alice ! WatchOutputSpentTriggered(htlcTimeoutTx.amount, htlcTimeoutTx.tx) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == htlcTimeoutTx.tx.txid) - alice ! WatchTxConfirmedTriggered(BlockHeight(2701), 3, htlcTimeoutTx.tx) - val claimHtlcDelayedTx = alice2blockchain.expectMsgType[PublishFinalTx] - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == claimHtlcDelayedTx.tx.txid) - claimHtlcDelayedTx - } - awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.get.claimHtlcDelayedTxs.length == 4) - - assert(CheckBalance.computeLocalCloseBalance(commitments.changes, LocalClose(commitments.active.last.localCommit, alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.get), commitments.originChannels, knownPreimages) == - PossiblyPublishedMainAndHtlcBalance( - toLocal = Map(OutPoint(localCommitPublished.claimMainDelayedOutputTx.get.tx.txid, 0) -> localCommitPublished.claimMainDelayedOutputTx.get.tx.txOut.head.amount), - htlcs = claimHtlcDelayedTxs.map(claimTx => OutPoint(claimTx.tx.txid, 0) -> claimTx.tx.txOut.head.amount.toBtc).toMap, - htlcsUnpublished = 0.sat - )) + // Alice has the preimage for 2 of her incoming HTLCs. + fulfillHtlc(htlcb2.id, rb1, alice, bob, alice2bob, bob2alice) + fulfillHtlc(htlcb3.id, rb2, alice, bob, alice2bob, bob2alice) + + // Alice publishes her commit tx. + val (localCommitPublished, localClosingTxs) = localClose(alice, alice2blockchain, htlcSuccessCount = 2, htlcTimeoutCount = 2) + alice ! WatchTxConfirmedTriggered(BlockHeight(750_000), 1, localCommitPublished.commitTx) + awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.exists(_.isConfirmed)) + val mainTx = localClosingTxs.mainTx_opt.get + val mainBalance = localCommitPublished.localOutput_opt.map(o => localCommitPublished.commitTx.txOut(o.index.toInt).amount).get + + // We already have an off-chain balance from other channels. + val balance = OffChainBalance( + waitForChannelReady = 500_000 sat, + shutdown = MainAndHtlcBalance(toLocal = 100_000_000 sat, htlcs = 0 sat), + closing = MainAndHtlcBalance(toLocal = 250_000 sat, htlcs = 50_000 sat), + ) + // We add our main balance and the amount of the incoming HTLCs. + val expected1 = balance.copy(closing = MainAndHtlcBalance(toLocal = 250_000.sat + mainBalance, htlcs = 50_000.sat + 35_000.sat + 30_000.sat + 25_000.sat)) + val balance1 = balance.addChannelBalance(alice.stateData.asInstanceOf[DATA_CLOSING], recentlySpentInputs = Set.empty) + assert(balance1 == expected1) + // If our main transaction is published, we don't include it. + val expected1b = balance.copy(closing = MainAndHtlcBalance(toLocal = 250_000.sat, htlcs = 50_000.sat + 35_000.sat + 30_000.sat + 25_000.sat)) + val balance1b = balance.addChannelBalance(alice.stateData.asInstanceOf[DATA_CLOSING], recentlySpentInputs = mainTx.txIn.map(_.outPoint).toSet) + assert(balance1b == expected1b) + + // The incoming HTLCs for which Alice has the preimage confirm: we keep including them until the 3rd-stage transaction confirms. + assert(localClosingTxs.htlcSuccessTxs.size == 2) + val htlcDelayedTxs = localClosingTxs.htlcSuccessTxs.map(tx => { + alice ! WatchTxConfirmedTriggered(BlockHeight(760_000), 3, tx) + val htlcDelayedTx = alice2blockchain.expectFinalTxPublished("htlc-delayed") + alice2blockchain.expectWatchOutputSpent(htlcDelayedTx.input) + htlcDelayedTx + }) + awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.exists(_.htlcDelayedOutputs.size == 2)) + assert(balance.addChannelBalance(alice.stateData.asInstanceOf[DATA_CLOSING], recentlySpentInputs = Set.empty) == expected1) + assert(balance.addChannelBalance(alice.stateData.asInstanceOf[DATA_CLOSING], recentlySpentInputs = mainTx.txIn.map(_.outPoint).toSet) == expected1b) + + // Bob claims the remaining incoming HTLC using his HTLC-timeout transaction: we remove it from our balance. + val (remoteCommitPublished, remoteClosingTxs) = remoteClose(localCommitPublished.commitTx, bob, bob2blockchain, htlcTimeoutCount = 3) + val bobHtlcTimeoutTx = remoteCommitPublished.outgoingHtlcs + .collectFirst { case (outpoint, htlcId) if htlcId == htlcb1.id => outpoint } + .flatMap(outpoint => remoteClosingTxs.htlcTimeoutTxs.find(_.txIn.head.outPoint == outpoint)) + .get + alice ! WatchTxConfirmedTriggered(BlockHeight(760_010), 0, bobHtlcTimeoutTx) + awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.exists(_.irrevocablySpent.contains(bobHtlcTimeoutTx.txIn.head.outPoint))) + val expected2 = balance.copy(closing = MainAndHtlcBalance(toLocal = 250_000.sat + mainBalance, htlcs = 50_000.sat + 30_000.sat + 25_000.sat)) + val balance2 = balance.addChannelBalance(alice.stateData.asInstanceOf[DATA_CLOSING], recentlySpentInputs = Set.empty) + assert(balance2 == expected2) + + // Alice's 3rd-stage transactions are published in our mempool: we stop including them in our off-chain balance, they will appear in our on-chain balance. + val expected3 = balance.copy(closing = MainAndHtlcBalance(toLocal = 250_000.sat + mainBalance, htlcs = 50_000.sat)) + val balance3 = balance.addChannelBalance(alice.stateData.asInstanceOf[DATA_CLOSING], recentlySpentInputs = htlcDelayedTxs.map(_.input).toSet) + assert(balance3 == expected3) + + // Alice's 3rd-stage transactions confirm: we stop including them in our off-chain balance, they will appear in our on-chain balance. + htlcDelayedTxs.foreach(txInfo => alice ! WatchTxConfirmedTriggered(BlockHeight(765_000), 3, txInfo.tx)) + awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.exists(lcp => htlcDelayedTxs.map(_.input).forall(o => lcp.irrevocablySpent.contains(o)))) + val expected4 = balance.copy(closing = MainAndHtlcBalance(toLocal = 250_000.sat + mainBalance, htlcs = 50_000.sat)) + val balance4 = balance.addChannelBalance(alice.stateData.asInstanceOf[DATA_CLOSING], recentlySpentInputs = Set.empty) + assert(balance4 == expected4) + + // Alice's main transaction confirms: we stop including it in our off-chain balance, it will appear in our on-chain balance. + alice ! WatchTxConfirmedTriggered(BlockHeight(765_100), 2, mainTx) + awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.exists(_.irrevocablySpent.contains(mainTx.txIn.head.outPoint))) + val balance5 = balance.addChannelBalance(alice.stateData.asInstanceOf[DATA_CLOSING], recentlySpentInputs = Set.empty) + assert(balance5 == balance) } ignore("compute from eclair.sqlite") { _ => @@ -238,88 +295,9 @@ class CheckBalanceSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with statement.executeQuery("SELECT data FROM local_channels WHERE is_closed=0") .mapCodec(channelDataCodec) } - val knownPreimages: Set[(ByteVector32, Long)] = using(sqlite.prepareStatement("SELECT channel_id, htlc_id FROM pending_relay")) { statement => - val rs = statement.executeQuery() - var q: Queue[(ByteVector32, Long)] = Queue() - while (rs.next()) { - q = q :+ (rs.getByteVector32("channel_id"), rs.getLong("htlc_id")) - } - q.toSet - } - val res = CheckBalance.computeOffChainBalance(channels, knownPreimages) + val res = CheckBalance.computeOffChainBalance(channels, recentlySpentInputs = Set.empty) println(res) println(res.total) } - test("tx pruning") { () => - val outPoints = (for (_ <- 0 until 20) yield OutPoint(randomTxId(), 0)).toList - val knownTxids = Set(outPoints(1).txid, outPoints(3).txid, outPoints(4).txid, outPoints(6).txid, outPoints(9).txid, outPoints(12).txid, outPoints(13).txid) - - val bitcoinClient = new BitcoinCoreClient(null) { - /** Get the number of confirmations of a given transaction. */ - override def getTxConfirmations(txid: TxId)(implicit ec: ExecutionContext): Future[Option[Int]] = - Future.successful(if (knownTxids.contains(txid)) Some(42) else None) - } - - val bal1 = OffChainBalance( - closing = ClosingBalance( - localCloseBalance = PossiblyPublishedMainAndHtlcBalance( - toLocal = Map( - outPoints(0) -> 1000.sat, - outPoints(1) -> 1000.sat, - outPoints(2) -> 1000.sat), - htlcs = Map( - outPoints(3) -> 1000.sat, - outPoints(4) -> 1000.sat, - outPoints(5) -> 1000.sat) - ), - remoteCloseBalance = PossiblyPublishedMainAndHtlcBalance( - toLocal = Map( - outPoints(6) -> 1000.sat, - outPoints(7) -> 1000.sat, - outPoints(8) -> 1000.sat, - outPoints(9) -> 1000.sat), - htlcs = Map( - outPoints(10) -> 1000.sat, - outPoints(11) -> 1000.sat, - outPoints(12) -> 1000.sat), - ), - mutualCloseBalance = PossiblyPublishedMainBalance( - toLocal = Map( - outPoints(13) -> 1000.sat, - outPoints(14) -> 1000.sat - ) - ) - ) - ) - - val sender = TestProbe() - CheckBalance.prunePublishedTransactions(bal1, bitcoinClient).pipeTo(sender.ref) - val bal2 = sender.expectMsgType[OffChainBalance] - - assert(bal2 == OffChainBalance( - closing = ClosingBalance( - localCloseBalance = PossiblyPublishedMainAndHtlcBalance( - toLocal = Map( - outPoints(0) -> 1000.sat, - outPoints(2) -> 1000.sat), - htlcs = Map( - outPoints(5) -> 1000.sat) - ), - remoteCloseBalance = PossiblyPublishedMainAndHtlcBalance( - toLocal = Map( - outPoints(7) -> 1000.sat, - outPoints(8) -> 1000.sat), - htlcs = Map( - outPoints(10) -> 1000.sat, - outPoints(11) -> 1000.sat), - ), - mutualCloseBalance = PossiblyPublishedMainBalance( - toLocal = Map( - outPoints(14) -> 1000.sat - ) - ))) - ) - } - } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/DummyOnChainWallet.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/DummyOnChainWallet.scala index b02a3ee034..df2afcd2b4 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/DummyOnChainWallet.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/DummyOnChainWallet.scala @@ -20,7 +20,7 @@ import fr.acinq.bitcoin.TxIn.SEQUENCE_FINAL import fr.acinq.bitcoin.psbt.{KeyPathWithMaster, Psbt, TaprootBip32DerivationPath} import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.bitcoin.scalacompat.DeterministicWallet.KeyPath -import fr.acinq.bitcoin.scalacompat.{Block, ByteVector64, Crypto, KotlinUtils, OutPoint, Satoshi, SatoshiLong, Script, ScriptElt, ScriptWitness, Transaction, TxId, TxIn, TxOut} +import fr.acinq.bitcoin.scalacompat.{Block, ByteVector64, KotlinUtils, OutPoint, Satoshi, SatoshiLong, Script, ScriptElt, ScriptWitness, Transaction, TxId, TxIn, TxOut} import fr.acinq.eclair.TestUtils.randomTxId import fr.acinq.eclair.blockchain.OnChainWallet.{FundTransactionResponse, MakeFundingTxResponse, OnChainBalance, ProcessPsbtResponse} import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinCoreClient.AddressType @@ -30,13 +30,13 @@ import fr.acinq.eclair.transactions.Transactions import fr.acinq.eclair.{TimestampSecond, randomBytes32} import scodec.bits._ -import scala.concurrent.{ExecutionContext, Future, Promise} +import scala.concurrent.{ExecutionContext, Future} import scala.util.{Failure, Random, Success} /** * Created by PM on 06/07/2017. */ -class DummyOnChainWallet extends OnChainWallet with OnChainPubkeyCache { +class DummyOnChainWallet extends OnChainWallet with OnChainAddressCache { import DummyOnChainWallet._ @@ -52,8 +52,6 @@ class DummyOnChainWallet extends OnChainWallet with OnChainPubkeyCache { case _ => Script.pay2wpkh(dummyReceivePubkey) }) - override def getP2wpkhPubkey()(implicit ec: ExecutionContext): Future[Crypto.PublicKey] = Future.successful(dummyReceivePubkey) - override def fundTransaction(tx: Transaction, feeRate: FeeratePerKw, replaceable: Boolean, changePosition: Option[Int], externalInputsWeight: Map[OutPoint, Long], minInputConfirmations_opt: Option[Int], feeBudget_opt: Option[Satoshi])(implicit ec: ExecutionContext): Future[FundTransactionResponse] = { funded += (tx.txid -> tx) Future.successful(FundTransactionResponse(tx, 0 sat, None)) @@ -92,62 +90,10 @@ class DummyOnChainWallet extends OnChainWallet with OnChainPubkeyCache { override def doubleSpent(tx: Transaction)(implicit ec: ExecutionContext): Future[Boolean] = Future.successful(false) - override def getP2wpkhPubkey(renew: Boolean): PublicKey = dummyReceivePubkey - override def getReceivePublicKeyScript(renew: Boolean): Seq[ScriptElt] = Script.pay2tr(dummyReceivePubkey.xOnly) } -class NoOpOnChainWallet extends OnChainWallet with OnChainPubkeyCache { - - import DummyOnChainWallet._ - - var rolledback = Seq.empty[Transaction] - var doubleSpent = Set.empty[TxId] - var abandoned = Set.empty[TxId] - - override def onChainBalance()(implicit ec: ExecutionContext): Future[OnChainBalance] = Future.successful(OnChainBalance(1105 sat, 561 sat)) - - override def getReceivePublicKeyScript(addressType: Option[AddressType] = None)(implicit ec: ExecutionContext): Future[Seq[ScriptElt]] = Future.successful(addressType match { - case Some(AddressType.P2tr) => Script.pay2tr(dummyReceivePubkey.xOnly) - case _ => Script.pay2wpkh(dummyReceivePubkey) - }) - - override def getP2wpkhPubkey()(implicit ec: ExecutionContext): Future[Crypto.PublicKey] = Future.successful(dummyReceivePubkey) - - override def fundTransaction(tx: Transaction, feeRate: FeeratePerKw, replaceable: Boolean, changePosition: Option[Int], externalInputsWeight: Map[OutPoint, Long], minInputConfirmations_opt: Option[Int], feeBudget_opt: Option[Satoshi])(implicit ec: ExecutionContext): Future[FundTransactionResponse] = Promise().future // will never be completed - - override def signPsbt(psbt: Psbt, ourInputs: Seq[Int], ourOutputs: Seq[Int])(implicit ec: ExecutionContext): Future[ProcessPsbtResponse] = Promise().future // will never be completed - - override def publishTransaction(tx: Transaction)(implicit ec: ExecutionContext): Future[TxId] = Future.successful(tx.txid) - - override def makeFundingTx(pubkeyScript: ByteVector, amount: Satoshi, feeRatePerKw: FeeratePerKw, feeBudget_opt: Option[Satoshi])(implicit ec: ExecutionContext): Future[MakeFundingTxResponse] = Promise().future // will never be completed - - override def commit(tx: Transaction)(implicit ec: ExecutionContext): Future[Boolean] = Future.successful(true) - - override def getTransaction(txId: TxId)(implicit ec: ExecutionContext): Future[Transaction] = Promise().future // will never be completed - - override def getTxConfirmations(txid: TxId)(implicit ec: ExecutionContext): Future[Option[Int]] = Promise().future // will never be completed - - override def isTransactionOutputSpendable(txid: TxId, outputIndex: Int, includeMempool: Boolean)(implicit ec: ExecutionContext): Future[Boolean] = Future.successful(true) - - override def rollback(tx: Transaction)(implicit ec: ExecutionContext): Future[Boolean] = { - rolledback = rolledback :+ tx - Future.successful(true) - } - - override def abandon(txId: TxId)(implicit ec: ExecutionContext): Future[Boolean] = { - abandoned = abandoned + txId - Future.successful(true) - } - - override def doubleSpent(tx: Transaction)(implicit ec: ExecutionContext): Future[Boolean] = Future.successful(doubleSpent.contains(tx.txid)) - - override def getP2wpkhPubkey(renew: Boolean): PublicKey = dummyReceivePubkey - - override def getReceivePublicKeyScript(renew: Boolean): Seq[ScriptElt] = Script.pay2tr(dummyReceivePubkey.xOnly) -} - -class SingleKeyOnChainWallet extends OnChainWallet with OnChainPubkeyCache { +class SingleKeyOnChainWallet extends OnChainWallet with OnChainAddressCache { import fr.acinq.bitcoin.scalacompat.KotlinUtils._ @@ -174,8 +120,6 @@ class SingleKeyOnChainWallet extends OnChainWallet with OnChainPubkeyCache { case _ => p2trScript }) - override def getP2wpkhPubkey()(implicit ec: ExecutionContext): Future[Crypto.PublicKey] = Future.successful(p2wpkhPublicKey) - override def fundTransaction(tx: Transaction, feeRate: FeeratePerKw, replaceable: Boolean, changePosition: Option[Int], externalInputsWeight: Map[OutPoint, Long], minInputConfirmations_opt: Option[Int], feeBudget_opt: Option[Satoshi])(implicit ec: ExecutionContext): Future[FundTransactionResponse] = synchronized { val currentAmountIn = tx.txIn.flatMap(txIn => inputs.find(_.txid == txIn.outPoint.txid).flatMap(_.txOut.lift(txIn.outPoint.index.toInt))).map(_.amount).sum val amountOut = tx.txOut.map(_.amount).sum @@ -281,8 +225,6 @@ class SingleKeyOnChainWallet extends OnChainWallet with OnChainPubkeyCache { override def doubleSpent(tx: Transaction)(implicit ec: ExecutionContext): Future[Boolean] = Future.successful(doubleSpent.contains(tx.txid)) - override def getP2wpkhPubkey(renew: Boolean): PublicKey = p2wpkhPublicKey - override def getReceivePublicKeyScript(renew: Boolean): Seq[ScriptElt] = p2trScript } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreClientSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreClientSpec.scala index 44c71fdfff..f59e6ccf72 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreClientSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreClientSpec.scala @@ -25,6 +25,7 @@ import fr.acinq.bitcoin.scalacompat.Crypto.{PublicKey, der2compact} import fr.acinq.bitcoin.scalacompat.{Block, BlockHash, BlockId, Btc, BtcDouble, Crypto, DeterministicWallet, KotlinUtils, MilliBtcDouble, MnemonicCode, OP_DROP, OP_PUSHDATA, OutPoint, Satoshi, SatoshiLong, Script, ScriptWitness, Transaction, TxId, TxIn, TxOut, addressToPublicKeyScript, computeBIP84Address, computeP2WpkhAddress} import fr.acinq.bitcoin.{Bech32, SigHash, SigVersion} import fr.acinq.eclair.TestUtils.randomTxId +import fr.acinq.eclair.balance.CheckBalance import fr.acinq.eclair.blockchain.OnChainWallet.{FundTransactionResponse, MakeFundingTxResponse, OnChainBalance, ProcessPsbtResponse} import fr.acinq.eclair.blockchain.WatcherSpec.{createSpendManyP2WPKH, createSpendP2WPKH} import fr.acinq.eclair.blockchain.bitcoind.BitcoindService.{BitcoinReq, SignTransactionResponse} @@ -34,6 +35,7 @@ import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinJsonRPCAuthMethod.UserPass import fr.acinq.eclair.blockchain.bitcoind.rpc.{BasicBitcoinJsonRPCClient, BitcoinCoreClient, BitcoinJsonRPCClient, JsonRPCError} import fr.acinq.eclair.blockchain.fee.{FeeratePerByte, FeeratePerKw} import fr.acinq.eclair.crypto.keymanager.LocalOnChainKeyManager +import fr.acinq.eclair.transactions.Transactions.ZeroFeeHtlcTxAnchorOutputsCommitmentFormat import fr.acinq.eclair.transactions.{Scripts, Transactions} import fr.acinq.eclair.{BlockHeight, TestConstants, TestKitBaseClass, TimestampSecond, randomBytes32, randomKey} import grizzled.slf4j.Logging @@ -52,7 +54,7 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A implicit val formats: Formats = DefaultFormats - val defaultAddressType_opt: Option[String] = Some("bech32") + val defaultAddressType_opt: Option[String] = Some("bech32m") override def beforeAll(): Unit = { // Note that we don't specify a default change address type, allowing bitcoind to choose between p2wpkh and p2tr. @@ -88,10 +90,10 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A val sender = TestProbe() val bitcoinClient = makeBitcoinCoreClient() - // wallet is configured with address_type=bech32 + // wallet is configured with address_type=bech32m bitcoinClient.getReceiveAddress(None).pipeTo(sender.ref) val address = sender.expectMsgType[String] - assert(addressToPublicKeyScript(Block.RegtestGenesisBlock.hash, address).map(Script.isPay2wpkh).contains(true)) + assert(addressToPublicKeyScript(Block.RegtestGenesisBlock.hash, address).map(Script.isPay2tr).contains(true)) bitcoinClient.getReceiveAddress(Some(AddressType.P2wpkh)).pipeTo(sender.ref) val address1 = sender.expectMsgType[String] @@ -106,10 +108,10 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A val sender = TestProbe() val bitcoinClient = makeBitcoinCoreClient() - // wallet is configured with address_type=bech32 + // wallet is configured with address_type=bech32m bitcoinClient.getChangeAddress(None).pipeTo(sender.ref) val address = sender.expectMsgType[String] - assert(addressToPublicKeyScript(Block.RegtestGenesisBlock.hash, address).map(Script.isPay2wpkh).contains(true)) + assert(addressToPublicKeyScript(Block.RegtestGenesisBlock.hash, address).map(Script.isPay2tr).contains(true)) bitcoinClient.getChangeAddress(Some(AddressType.P2wpkh)).pipeTo(sender.ref) val address1 = sender.expectMsgType[String] @@ -247,7 +249,7 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A val pubkeyScript = Script.write(addressToPublicKeyScript(Block.RegtestGenesisBlock.hash, address).toOption.get) // We first receive some confirmed funds. - miner.sendToPubkeyScript(pubkeyScript, 150_000 sat, FeeratePerKw(FeeratePerByte(5 sat))).pipeTo(sender.ref) + miner.sendToPubkeyScript(pubkeyScript, 150_000 sat, FeeratePerByte(5 sat).perKw).pipeTo(sender.ref) val externalTxId = sender.expectMsgType[TxId] generateBlocks(1) @@ -292,7 +294,7 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A Seq(25 millibtc, 15 millibtc, 20 millibtc).foreach(amount => { walletExternalFunds.getReceiveAddress().pipeTo(sender.ref) val walletAddress = sender.expectMsgType[String] - defaultWallet.sendToPubkeyScript(Script.write(addressToPublicKeyScript(Block.RegtestGenesisBlock.hash, walletAddress).toOption.get), amount, FeeratePerKw(FeeratePerByte(3.sat))).pipeTo(sender.ref) + defaultWallet.sendToPubkeyScript(Script.write(addressToPublicKeyScript(Block.RegtestGenesisBlock.hash, walletAddress).toOption.get), amount, FeeratePerByte(3.sat).perKw).pipeTo(sender.ref) sender.expectMsgType[TxId] }) @@ -440,7 +442,7 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A // When sending to a p2wpkh, bitcoin core should add a p2wpkh change output. val pubkeyScript = Script.pay2wpkh(pubKey) val unsignedTx = Transaction(version = 2, Nil, Seq(TxOut(150_000 sat, pubkeyScript)), lockTime = 0) - bitcoinClient.fundTransaction(unsignedTx, feeRate = FeeratePerKw(FeeratePerByte(3 sat)), changePosition = Some(1)).pipeTo(sender.ref) + bitcoinClient.fundTransaction(unsignedTx, feeRate = FeeratePerByte(3 sat).perKw, changePosition = Some(1)).pipeTo(sender.ref) val tx = sender.expectMsgType[FundTransactionResponse].tx // We have a change output. assert(tx.txOut.length == 2) @@ -458,7 +460,7 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A // When sending to a p2tr, bitcoin core should add a p2tr change output. val pubkeyScript = Script.pay2tr(pubKey.xOnly) val unsignedTx = Transaction(version = 2, Nil, Seq(TxOut(150_000 sat, pubkeyScript)), lockTime = 0) - bitcoinClient.fundTransaction(unsignedTx, feeRate = FeeratePerKw(FeeratePerByte(3 sat)), changePosition = Some(1)).pipeTo(sender.ref) + bitcoinClient.fundTransaction(unsignedTx, feeRate = FeeratePerByte(3 sat).perKw, changePosition = Some(1)).pipeTo(sender.ref) val tx = sender.expectMsgType[FundTransactionResponse].tx // We have a change output. assert(tx.txOut.length == 2) @@ -708,7 +710,7 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A } val commitOutpoint = OutPoint(commitTx, commitTx.txOut.indexWhere(_.publicKeyScript == Script.write(Script.pay2wpkh(priv.publicKey)))) val Seq(anchorTx1, anchorTx2) = Seq(FeeratePerKw(1000 sat), FeeratePerKw(2000 sat)).map { feerate => - val externalInput = Map(commitOutpoint -> Transactions.claimP2WPKHOutputWeight.toLong) + val externalInput = Map(commitOutpoint -> (Transactions.p2wpkhInputWeight + Transactions.p2wpkhOutputWeight).toLong) val txNotFunded = Transaction(2, Seq(TxIn(commitOutpoint, Nil, 0)), Seq(TxOut(200_000 sat, Script.pay2wpkh(priv.publicKey))), 0) wallet.fundTransaction(txNotFunded, feerate, externalInputsWeight = externalInput).pipeTo(sender.ref) signTransaction(wallet, sender.expectMsgType[FundTransactionResponse].tx).pipeTo(sender.ref) @@ -779,7 +781,7 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A } val commitOutpoint = OutPoint(commitTx, commitTx.txOut.indexWhere(_.publicKeyScript == Script.write(Script.pay2wpkh(priv.publicKey)))) val Seq(htlcSuccessTx, htlcTimeoutTx) = Seq((wallet1, FeeratePerKw(1000 sat)), (wallet2, FeeratePerKw(2000 sat))).map { case (wallet, feerate) => - val externalInput = Map(commitOutpoint -> Transactions.claimP2WPKHOutputWeight.toLong) + val externalInput = Map(commitOutpoint -> (Transactions.p2wpkhInputWeight + Transactions.p2wpkhOutputWeight).toLong) val txNotFunded = Transaction(2, Seq(TxIn(commitOutpoint, Nil, 0)), Seq(TxOut(200_000 sat, Script.pay2wpkh(priv.publicKey))), 0) wallet.fundTransaction(txNotFunded, feerate, externalInputsWeight = externalInput).pipeTo(sender.ref) signTransaction(wallet, sender.expectMsgType[FundTransactionResponse].tx).pipeTo(sender.ref) @@ -818,7 +820,7 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A val bitcoinClient = makeBitcoinCoreClient() val txNotFunded = Transaction(2, Nil, Seq(TxOut(200_000 sat, Script.pay2wpkh(randomKey().publicKey))), 0) - bitcoinClient.fundTransaction(txNotFunded, FeeratePerKw(FeeratePerByte(1 sat)), replaceable = true).pipeTo(sender.ref) + bitcoinClient.fundTransaction(txNotFunded, FeeratePerByte(1 sat).perKw, replaceable = true).pipeTo(sender.ref) val txFunded1 = sender.expectMsgType[FundTransactionResponse].tx assert(txFunded1.txIn.nonEmpty) bitcoinClient.signPsbt(new Psbt(txFunded1), txFunded1.txIn.indices, Nil).pipeTo(sender.ref) @@ -831,7 +833,7 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A sender.expectMsg(txFunded1.txIn.map(_.outPoint).toSet) // we double-spend the inputs, which unlocks them - bitcoinClient.fundTransaction(txFunded1, FeeratePerKw(FeeratePerByte(5 sat)), replaceable = true).pipeTo(sender.ref) + bitcoinClient.fundTransaction(txFunded1, FeeratePerByte(5 sat).perKw, replaceable = true).pipeTo(sender.ref) val txFunded2 = sender.expectMsgType[FundTransactionResponse].tx assert(txFunded2.txid != txFunded1.txid) txFunded1.txIn.foreach(txIn => assert(txFunded2.txIn.map(_.outPoint).contains(txIn.outPoint))) @@ -1177,6 +1179,73 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A assert(tx2.confirmations == 1) } + test("compute detailed on-chain balance") { + assume(!useEclairSigner) + + val sender = TestProbe() + val miner = makeBitcoinCoreClient() + val wallet = new BitcoinCoreClient(createWallet("detailed_on_chain_balance", sender)) + wallet.getReceiveAddress().pipeTo(sender.ref) + val address = sender.expectMsgType[String] + + // We receive an unconfirmed transaction. + miner.sendToAddress(address, 200_000 sat, 1).pipeTo(sender.ref) + val txId1 = sender.expectMsgType[TxId] + awaitAssert({ + CheckBalance.computeOnChainBalance(wallet, minDepth = 2).pipeTo(sender.ref) + val balance = sender.expectMsgType[CheckBalance.DetailedOnChainBalance] + assert(balance.deeplyConfirmed.isEmpty && balance.recentlyConfirmed.isEmpty) + assert(balance.unconfirmed.keySet.map(_.txid) == Set(txId1)) + assert(balance.totalUnconfirmed.toSatoshi == 200_000.sat) + assert(!balance.recentlySpentInputs.map(_.txid).contains(txId1)) + }) + + // Our received transaction confirms. + generateBlocks(1) + awaitAssert({ + CheckBalance.computeOnChainBalance(wallet, minDepth = 2).pipeTo(sender.ref) + val balance = sender.expectMsgType[CheckBalance.DetailedOnChainBalance] + assert(balance.deeplyConfirmed.isEmpty && balance.unconfirmed.isEmpty) + assert(balance.recentlyConfirmed.keySet.map(_.txid) == Set(txId1)) + assert(balance.totalRecentlyConfirmed.toSatoshi == 200_000.sat) + assert(!balance.recentlySpentInputs.map(_.txid).contains(txId1)) + }) + + // We spend our received transaction before it deeply confirms. + wallet.sendToAddress(address, 150_000 sat, 1).pipeTo(sender.ref) + val txId2 = sender.expectMsgType[TxId] + awaitAssert({ + CheckBalance.computeOnChainBalance(wallet, minDepth = 2).pipeTo(sender.ref) + val balance = sender.expectMsgType[CheckBalance.DetailedOnChainBalance] + assert(balance.deeplyConfirmed.isEmpty && balance.recentlyConfirmed.isEmpty) + assert(balance.unconfirmed.keySet.map(_.txid) == Set(txId2)) + assert(190_000.sat < balance.totalUnconfirmed.toSatoshi && balance.totalUnconfirmed.toSatoshi < 200_000.sat) + assert(balance.recentlySpentInputs.map(_.txid).contains(txId1)) + }) + + // Our transaction deeply confirms. + generateBlocks(2) + awaitAssert({ + CheckBalance.computeOnChainBalance(wallet, minDepth = 2).pipeTo(sender.ref) + val balance = sender.expectMsgType[CheckBalance.DetailedOnChainBalance] + assert(balance.recentlyConfirmed.isEmpty && balance.unconfirmed.isEmpty) + assert(balance.deeplyConfirmed.keySet.map(_.txid) == Set(txId2)) + assert(190_000.sat < balance.totalDeeplyConfirmed.toSatoshi && balance.totalDeeplyConfirmed.toSatoshi < 200_000.sat) + assert(balance.recentlySpentInputs.map(_.txid).contains(txId1)) + }) + + // With more confirmations, the input isn't included in our recently spent inputs, but stays in our balance. + generateBlocks(3) + awaitAssert({ + CheckBalance.computeOnChainBalance(wallet, minDepth = 2).pipeTo(sender.ref) + val balance = sender.expectMsgType[CheckBalance.DetailedOnChainBalance] + assert(balance.recentlyConfirmed.isEmpty && balance.unconfirmed.isEmpty) + assert(balance.deeplyConfirmed.keySet.map(_.txid) == Set(txId2)) + assert(190_000.sat < balance.totalDeeplyConfirmed.toSatoshi && balance.totalDeeplyConfirmed.toSatoshi < 200_000.sat) + assert(!balance.recentlySpentInputs.map(_.txid).contains(txId1)) + }) + } + test("get mempool transaction") { import fr.acinq.bitcoin.scalacompat.KotlinUtils._ @@ -1677,11 +1746,11 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A val sender = TestProbe() val bitcoinClient = makeBitcoinCoreClient() - // eclair on-chain key manager does not yet support taproot descriptors + // eclair on-chain key manager supports taproot descriptors bitcoinClient.getReceiveAddress().pipeTo(sender.ref) val defaultAddress = sender.expectMsgType[String] val decoded = Bech32.decodeWitnessAddress(defaultAddress) - assert(decoded.getSecond == 0) + assert(decoded.getSecond == 1) // But we can explicitly use segwit v0 addresses. bitcoinClient.getP2wpkhPubkey().pipeTo(sender.ref) @@ -1817,7 +1886,7 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A } val localAnchorTx = { val txNotFunded = Transaction(2, Seq(TxIn(OutPoint(localCommitTx, 1), Nil, 0)), Seq(TxOut(300_000 sat, Script.pay2wpkh(randomKey().publicKey))), 0) - val externalWeight = Map(txNotFunded.txIn.head.outPoint -> Transactions.anchorInputWeight.toLong) + val externalWeight = Map(txNotFunded.txIn.head.outPoint -> ZeroFeeHtlcTxAnchorOutputsCommitmentFormat.anchorInputWeight.toLong) wallet.fundTransaction(txNotFunded, FeeratePerKw(2500 sat), externalInputsWeight = externalWeight).pipeTo(sender.ref) signTransaction(wallet, sender.expectMsgType[FundTransactionResponse].tx).pipeTo(sender.ref) val partiallySignedTx = sender.expectMsgType[SignTransactionResponse].tx @@ -1946,13 +2015,13 @@ class BitcoinCoreClientWithEclairSignerSpec extends BitcoinCoreClientSpec { assert(wallet.onChainKeyManager_opt.get.masterPubKey(0, AddressType.P2wpkh) == accountXPub) (0 to 10).foreach { _ => - wallet.getReceiveAddress().pipeTo(sender.ref) + wallet.getReceiveAddress(Some(AddressType.P2wpkh)).pipeTo(sender.ref) val address = sender.expectMsgType[String] val bip32path = getBip32Path(wallet, address, sender) assert(bip32path.path.length == 5 && bip32path.toString().startsWith("m/84'/1'/0'/0")) assert(computeBIP84Address(master.derivePrivateKey(bip32path).publicKey, Block.RegtestGenesisBlock.hash) == address) - wallet.getChangeAddress().pipeTo(sender.ref) + wallet.getChangeAddress(Some(AddressType.P2wpkh)).pipeTo(sender.ref) val changeAddress = sender.expectMsgType[String] val bip32ChangePath = getBip32Path(wallet, changeAddress, sender) assert(bip32ChangePath.path.length == 5 && bip32ChangePath.toString().startsWith("m/84'/1'/0'/1")) @@ -2009,7 +2078,7 @@ class BitcoinCoreClientWithEclairSignerSpec extends BitcoinCoreClientSpec { val error = sender.expectMsgType[Failure] assert(error.cause.getMessage.contains("Private keys are disabled for this wallet")) - wallet.sendToPubkeyScript(addressToPublicKeyScript(Block.RegtestGenesisBlock.hash, address).toOption.get, 50_000.sat, FeeratePerKw(FeeratePerByte(5.sat))).pipeTo(sender.ref) + wallet.sendToPubkeyScript(addressToPublicKeyScript(Block.RegtestGenesisBlock.hash, address).toOption.get, 50_000.sat, FeeratePerByte(5.sat).perKw).pipeTo(sender.ref) sender.expectMsgType[TxId] } } @@ -2043,7 +2112,7 @@ class BitcoinCoreClientWithEclairSignerSpec extends BitcoinCoreClientSpec { wallet.getReceiveAddress(Some(P2tr)).pipeTo(sender.ref) val address = sender.expectMsgType[String] - val tx = Transaction(version = 2, + val tx = Transaction(version = 2, txIn = TxIn(utxo1, Nil, TxIn.SEQUENCE_FINAL) :: TxIn(utxo2, Nil, TxIn.SEQUENCE_FINAL) :: Nil, txOut = TxOut(199_000.sat, addressToPublicKeyScript(Block.RegtestGenesisBlock.hash, address).toOption.get) :: Nil, lockTime = 0) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoindService.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoindService.scala index 0e285c5d34..7b6d3b8666 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoindService.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoindService.scala @@ -62,7 +62,7 @@ trait BitcoindService extends Logging { val PATH_BITCOIND = sys.env.get("BITCOIND_DIR") match { case Some(customBitcoinDir) => new File(customBitcoinDir, "bitcoind") - case None => new File(TestUtils.BUILD_DIRECTORY, "bitcoin-28.1/bin/bitcoind") + case None => new File(TestUtils.BUILD_DIRECTORY, "bitcoin-29.2/bin/bitcoind") } logger.info(s"using bitcoind: $PATH_BITCOIND") val PATH_BITCOIND_DATADIR = new File(INTEGRATION_TMP_DIR, "datadir-bitcoin") @@ -89,8 +89,8 @@ trait BitcoindService extends Logging { val onChainKeyManager = new LocalOnChainKeyManager("eclair", MnemonicCode.toSeed(mnemonics, passphrase), TimestampSecond.now(), Block.RegtestGenesisBlock.hash) def startBitcoind(useCookie: Boolean = false, - defaultAddressType_opt: Option[String] = None, - changeAddressType_opt: Option[String] = None, + defaultAddressType_opt: Option[String] = Some("bech32m"), + changeAddressType_opt: Option[String] = Some("bech32m"), mempoolSize_opt: Option[Int] = None, // mempool size in MB mempoolMinFeerate_opt: Option[FeeratePerByte] = None, // transactions below this feerate won't be accepted in the mempool startupFlags: String = ""): Unit = { @@ -106,7 +106,7 @@ trait BitcoindService extends Logging { .appendedAll(defaultAddressType_opt.map(addressType => s"addresstype=$addressType\n").getOrElse("")) .appendedAll(changeAddressType_opt.map(addressType => s"changetype=$addressType\n").getOrElse("")) .appendedAll(mempoolSize_opt.map(mempoolSize => s"maxmempool=$mempoolSize\n").getOrElse("")) - .appendedAll(mempoolMinFeerate_opt.map(mempoolMinFeerate => s"minrelaytxfee=${FeeratePerKB(mempoolMinFeerate).feerate.toBtc.toBigDecimal}\n").getOrElse("")) + .appendedAll(mempoolMinFeerate_opt.map(mempoolMinFeerate => s"minrelaytxfee=${mempoolMinFeerate.perKB.feerate.toBtc.toBigDecimal}\n").getOrElse("")) if (useCookie) { defaultConf .replace("rpcuser=foo", "") @@ -199,7 +199,7 @@ trait BitcoindService extends Logging { val addressToUse = address match { case Some(addr) => addr case None => - sender.send(bitcoincli, BitcoinReq("getnewaddress", "", "bech32")) + sender.send(bitcoincli, BitcoinReq("getnewaddress", "", "bech32m")) val JString(address) = sender.expectMsgType[JValue](timeout) address } @@ -248,7 +248,7 @@ trait BitcoindService extends Logging { val tx = Transaction(version = 2, Nil, TxOut(amountSat, addressToPublicKeyScript(Block.RegtestGenesisBlock.hash, address).toOption.get) :: Nil, lockTime = 0) val client = makeBitcoinCoreClient() val f = for { - funded <- client.fundTransaction(tx, FeeratePerKw(FeeratePerByte(Satoshi(10))), replaceable = true) + funded <- client.fundTransaction(tx, FeeratePerByte(Satoshi(10)).perKw, replaceable = true) signed <- client.signPsbt(new Psbt(funded.tx), funded.tx.txIn.indices, Nil) txid <- client.publishTransaction(signed.finalTx_opt.toOption.get) tx <- client.getTransaction(txid) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/OnChainAddressRefresherSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/OnChainAddressRefresherSpec.scala index 59d999aca3..818eba4456 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/OnChainAddressRefresherSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/OnChainAddressRefresherSpec.scala @@ -1,8 +1,7 @@ package fr.acinq.eclair.blockchain.bitcoind import akka.actor.typed.scaladsl.adapter.ClassicActorSystemOps -import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey -import fr.acinq.bitcoin.scalacompat.{Crypto, Script, ScriptElt} +import fr.acinq.bitcoin.scalacompat.{Script, ScriptElt} import fr.acinq.eclair.blockchain.OnChainAddressGenerator import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinCoreClient.AddressType import fr.acinq.eclair.{TestKitBaseClass, randomKey} @@ -15,48 +14,31 @@ import scala.concurrent.{ExecutionContext, Future} class OnChainAddressRefresherSpec extends TestKitBaseClass with AnyFunSuiteLike { test("renew on-chain addresses") { - val finalPubkey = new AtomicReference[PublicKey](randomKey().publicKey) val finalPubkeyScript = new AtomicReference[Seq[ScriptElt]](Script.pay2tr(randomKey().xOnlyPublicKey())) - val renewedPublicKeyCount = new AtomicInteger(0) - val renewedPublicKeyScriptCount = new AtomicInteger(0) + val renewedCount = new AtomicInteger(0) val generator = new OnChainAddressGenerator { override def getReceivePublicKeyScript(addressType: Option[AddressType] = None)(implicit ec: ExecutionContext): Future[Seq[ScriptElt]] = { - renewedPublicKeyScriptCount.incrementAndGet() + renewedCount.incrementAndGet() Future.successful(addressType match { case Some(AddressType.P2tr) => Script.pay2tr(randomKey().xOnlyPublicKey()) case _ => Script.pay2wpkh(randomKey().publicKey) }) } - - override def getP2wpkhPubkey()(implicit ec: ExecutionContext): Future[Crypto.PublicKey] = { - renewedPublicKeyCount.incrementAndGet() - Future.successful(randomKey().publicKey) - } } - val manager = system.spawnAnonymous(OnChainAddressRefresher(generator, finalPubkey, finalPubkeyScript, 1 second)) - - // We send a batch of requests to renew our public key. - val publicKey1 = finalPubkey.get() - (1 to 7).foreach(_ => manager ! OnChainAddressRefresher.RenewPubkey) - awaitCond(finalPubkey.get() != publicKey1) - assert(renewedPublicKeyCount.get() == 1) + val manager = system.spawnAnonymous(OnChainAddressRefresher(generator, finalPubkeyScript, 1 second)) // We send a batch of requests to renew our public key script. val script1 = finalPubkeyScript.get() (1 to 5).foreach(_ => manager ! OnChainAddressRefresher.RenewPubkeyScript) awaitCond(finalPubkeyScript.get() != script1) - assert(renewedPublicKeyScriptCount.get() == 1) + assert(renewedCount.get() == 1) - // If we mix the two types of renew requests, only the first one will be renewed. - val publicKey2 = finalPubkey.get() + // We send another request to renew our public key script. val script2 = finalPubkeyScript.get() manager ! OnChainAddressRefresher.RenewPubkeyScript - manager ! OnChainAddressRefresher.RenewPubkey awaitCond(finalPubkeyScript.get() != script2) - assert(renewedPublicKeyCount.get() == 1) - assert(renewedPublicKeyScriptCount.get() == 2) - assert(finalPubkey.get() == publicKey2) + assert(renewedCount.get() == 2) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/ZmqWatcherSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/ZmqWatcherSpec.scala index 768cf132dd..b5833de43a 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/ZmqWatcherSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/ZmqWatcherSpec.scala @@ -245,39 +245,6 @@ class ZmqWatcherSpec extends TestKitBaseClass with AnyFunSuiteLike with Bitcoind }) } - test("watch for confirmed transactions with relative delay") { - withWatcher(f => { - import f._ - - // We simulate a transaction with a 3-blocks CSV delay. - val (priv, address) = createExternalAddress() - val parentTx = sendToAddress(address, 50.millibtc, probe) - val tx = createSpendP2WPKH(parentTx, priv, priv.publicKey, 5_000 sat, 3, 0) - val delay = RelativeDelay(parentTx.txid, 3) - - watcher ! WatchTxConfirmed(probe.ref, tx.txid, 6, Some(delay)) - probe.expectNoMessage(100 millis) - - // We make the parent tx confirm to satisfy the CSV delay and publish the delayed transaction. - generateBlocks(3) - bitcoinClient.publishTransaction(tx).pipeTo(probe.ref) - probe.expectMsg(tx.txid) - probe.expectNoMessage(100 millis) - - // The delayed transaction confirms, but hasn't reached its minimum depth. - generateBlocks(3) - probe.expectNoMessage(100 millis) - - // The delayed transaction reaches its minimum depth. - generateBlocks(3) - assert(probe.expectMsgType[WatchTxConfirmedTriggered].tx.txid == tx.txid) - - // If we watch the transaction when it's already confirmed, we immediately receive the WatchConfirmedTriggered event. - watcher ! WatchTxConfirmed(probe.ref, tx.txid, 3, Some(delay.copy(delay = 720))) - assert(probe.expectMsgType[WatchTxConfirmedTriggered].tx.txid == tx.txid) - }) - } - test("watch for spent transactions") { withWatcher(f => { import f._ diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/BitcoinCoreFeeProviderSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/BitcoinCoreFeeProviderSpec.scala index 9031cf5a6f..ab71ba2367 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/BitcoinCoreFeeProviderSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/BitcoinCoreFeeProviderSpec.scala @@ -95,7 +95,7 @@ class BitcoinCoreFeeProviderSpec extends TestKitBaseClass with BitcoindService w regtestProvider.mempoolMinFee().pipeTo(sender.ref) val mempoolMinFee = sender.expectMsgType[FeeratePerKB] // The regtest provider doesn't have any transaction in its mempool, so it defaults to the min_relay_fee. - assert(mempoolMinFee.feerate.toLong == FeeratePerKw.MinimumRelayFeeRate) + assert(mempoolMinFee.feerate.toLong == 100) // 0.1 sat/byte } private def createMockBitcoinClient(fees: Map[Int, FeeratePerKB], mempoolMinFee: FeeratePerKB): BasicBitcoinJsonRPCClient = { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/FeeProviderSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/FeeProviderSpec.scala index 73c4d4548f..220eb225cf 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/FeeProviderSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/FeeProviderSpec.scala @@ -26,14 +26,14 @@ class FeeProviderSpec extends AnyFunSuite { assert(FeeratePerByte(FeeratePerKw(2000 sat)) == FeeratePerByte(8 sat)) assert(FeeratePerKB(FeeratePerByte(10 sat)) == FeeratePerKB(10000 sat)) assert(FeeratePerKB(FeeratePerKw(25 sat)) == FeeratePerKB(100 sat)) - assert(FeeratePerKw(FeeratePerKB(10000 sat)) == FeeratePerKw(2500 sat)) - assert(FeeratePerKw(FeeratePerByte(10 sat)) == FeeratePerKw(2500 sat)) + assert(FeeratePerKB(10000 sat).perKw == FeeratePerKw(2500 sat)) + assert(FeeratePerByte(10 sat).perKw == FeeratePerKw(2500 sat)) } test("enforce a minimum feerate-per-kw") { - assert(FeeratePerKw(FeeratePerKB(1000 sat)) == MinimumFeeratePerKw) - assert(FeeratePerKw(FeeratePerKB(500 sat)) == MinimumFeeratePerKw) - assert(FeeratePerKw(FeeratePerByte(1 sat)) == MinimumFeeratePerKw) + assert(FeeratePerKB(1000 sat).perKw == MinimumFeeratePerKw) + assert(FeeratePerKB(500 sat).perKw == MinimumFeeratePerKw) + assert(FeeratePerByte(1 sat).perKw == MinimumFeeratePerKw) } test("compare feerates") { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/OnChainFeeConfSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/OnChainFeeConfSpec.scala index 1ad0af7664..446ae06d46 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/OnChainFeeConfSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/OnChainFeeConfSpec.scala @@ -18,91 +18,88 @@ package fr.acinq.eclair.blockchain.fee import fr.acinq.bitcoin.scalacompat.SatoshiLong import fr.acinq.eclair.randomKey -import fr.acinq.eclair.transactions.Transactions.{DefaultCommitmentFormat, UnsafeLegacyAnchorOutputsCommitmentFormat, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat} +import fr.acinq.eclair.transactions.Transactions.{PhoenixSimpleTaprootChannelCommitmentFormat, UnsafeLegacyAnchorOutputsCommitmentFormat, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat, ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat} import org.scalatest.funsuite.AnyFunSuite class OnChainFeeConfSpec extends AnyFunSuite { private val defaultFeeTargets = FeeTargets(funding = ConfirmationPriority.Medium, closing = ConfirmationPriority.Medium) + private val defaultMaxClosingFeerate = FeeratePerKw(10_000 sat) private val defaultFeerateTolerance = FeerateTolerance(0.5, 2.0, FeeratePerKw(2500 sat), DustTolerance(15000 sat, closeOnUpdateFeeOverflow = false)) test("should update fee when diff ratio exceeded") { - val feeConf = OnChainFeeConf(defaultFeeTargets, safeUtxosThreshold = 0, spendAnchorWithoutHtlcs = true, anchorWithoutHtlcsMaxFee = 10_000.sat, closeOnOfflineMismatch = true, updateFeeMinDiffRatio = 0.1, defaultFeerateTolerance, Map.empty) - assert(!feeConf.shouldUpdateFee(FeeratePerKw(1000 sat), FeeratePerKw(1000 sat))) - assert(!feeConf.shouldUpdateFee(FeeratePerKw(1000 sat), FeeratePerKw(900 sat))) - assert(!feeConf.shouldUpdateFee(FeeratePerKw(1000 sat), FeeratePerKw(1100 sat))) - assert(feeConf.shouldUpdateFee(FeeratePerKw(1000 sat), FeeratePerKw(899 sat))) - assert(feeConf.shouldUpdateFee(FeeratePerKw(1000 sat), FeeratePerKw(1101 sat))) + val feeConf = OnChainFeeConf(defaultFeeTargets, defaultMaxClosingFeerate, safeUtxosThreshold = 0, spendAnchorWithoutHtlcs = true, anchorWithoutHtlcsMaxFee = 10_000.sat, closeOnOfflineMismatch = true, updateFeeMinDiffRatio = 0.1, defaultFeerateTolerance, Map.empty) + assert(!feeConf.shouldUpdateFee(FeeratePerKw(1000 sat), FeeratePerKw(1000 sat), ZeroFeeHtlcTxAnchorOutputsCommitmentFormat)) + assert(!feeConf.shouldUpdateFee(FeeratePerKw(1000 sat), FeeratePerKw(900 sat), ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat)) + assert(!feeConf.shouldUpdateFee(FeeratePerKw(1000 sat), FeeratePerKw(1100 sat), ZeroFeeHtlcTxAnchorOutputsCommitmentFormat)) + assert(feeConf.shouldUpdateFee(FeeratePerKw(1000 sat), FeeratePerKw(899 sat), ZeroFeeHtlcTxAnchorOutputsCommitmentFormat)) + assert(feeConf.shouldUpdateFee(FeeratePerKw(1000 sat), FeeratePerKw(1101 sat), ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat)) } - test("get commitment feerate") { - val commitmentFormat = DefaultCommitmentFormat - val feeConf = OnChainFeeConf(defaultFeeTargets, safeUtxosThreshold = 0, spendAnchorWithoutHtlcs = true, anchorWithoutHtlcsMaxFee = 10_000.sat, closeOnOfflineMismatch = true, updateFeeMinDiffRatio = 0.1, defaultFeerateTolerance, Map.empty) - - val feerates1 = FeeratesPerKw.single(FeeratePerKw(10000 sat)).copy(fast = FeeratePerKw(5000 sat)) - assert(feeConf.getCommitmentFeerate(feerates1, randomKey().publicKey, commitmentFormat, 100000 sat) == FeeratePerKw(5000 sat)) - - val feerates2 = FeeratesPerKw.single(FeeratePerKw(10000 sat)).copy(fast = FeeratePerKw(4000 sat)) - assert(feeConf.getCommitmentFeerate(feerates2, randomKey().publicKey, commitmentFormat, 100000 sat) == FeeratePerKw(4000 sat)) + test("should update fee to set to 1 sat/byte") { + val feeConf = OnChainFeeConf(defaultFeeTargets, defaultMaxClosingFeerate, safeUtxosThreshold = 0, spendAnchorWithoutHtlcs = true, anchorWithoutHtlcsMaxFee = 10_000.sat, closeOnOfflineMismatch = true, updateFeeMinDiffRatio = 0.1, defaultFeerateTolerance, Map.empty) + // We always use 1 sat/byte for mobile wallet commitment formats, regardless of the current feerate. + val feerates = FeeratesPerKw.single(FeeratePerKw(FeeratePerByte(20 sat))) + assert(feeConf.getCommitmentFeerate(feerates, randomKey().publicKey, UnsafeLegacyAnchorOutputsCommitmentFormat) == FeeratePerKw(FeeratePerByte(1 sat))) + assert(feeConf.getCommitmentFeerate(feerates, randomKey().publicKey, PhoenixSimpleTaprootChannelCommitmentFormat) == FeeratePerKw(FeeratePerByte(1 sat))) + // If we're not already using 1 sat/byte, we update the feerate. + assert(feeConf.shouldUpdateFee(FeeratePerKw(300 sat), FeeratePerKw(FeeratePerByte(1 sat)), UnsafeLegacyAnchorOutputsCommitmentFormat)) + assert(feeConf.shouldUpdateFee(FeeratePerKw(300 sat), FeeratePerKw(FeeratePerByte(1 sat)), PhoenixSimpleTaprootChannelCommitmentFormat)) } - test("get commitment feerate (anchor outputs)") { + test("get commitment feerate") { val defaultNodeId = randomKey().publicKey val defaultMaxCommitFeerate = defaultFeerateTolerance.anchorOutputMaxCommitFeerate val overrideNodeId = randomKey().publicKey val overrideMaxCommitFeerate = defaultMaxCommitFeerate * 2 - val feeConf = OnChainFeeConf(defaultFeeTargets, safeUtxosThreshold = 0, spendAnchorWithoutHtlcs = true, anchorWithoutHtlcsMaxFee = 10_000.sat, closeOnOfflineMismatch = true, updateFeeMinDiffRatio = 0.1, defaultFeerateTolerance, Map(overrideNodeId -> defaultFeerateTolerance.copy(anchorOutputMaxCommitFeerate = overrideMaxCommitFeerate))) + val feeConf = OnChainFeeConf(defaultFeeTargets, defaultMaxClosingFeerate, safeUtxosThreshold = 0, spendAnchorWithoutHtlcs = true, anchorWithoutHtlcsMaxFee = 10_000.sat, closeOnOfflineMismatch = true, updateFeeMinDiffRatio = 0.1, defaultFeerateTolerance, Map(overrideNodeId -> defaultFeerateTolerance.copy(anchorOutputMaxCommitFeerate = overrideMaxCommitFeerate))) val feerates1 = FeeratesPerKw.single(FeeratePerKw(10000 sat)).copy(fast = defaultMaxCommitFeerate / 2, minimum = FeeratePerKw(250 sat)) - assert(feeConf.getCommitmentFeerate(feerates1, defaultNodeId, UnsafeLegacyAnchorOutputsCommitmentFormat, 100000 sat) == defaultMaxCommitFeerate / 2) - assert(feeConf.getCommitmentFeerate(feerates1, defaultNodeId, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat, 100000 sat) == defaultMaxCommitFeerate / 2) + assert(feeConf.getCommitmentFeerate(feerates1, defaultNodeId, ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat) == defaultMaxCommitFeerate / 2) + assert(feeConf.getCommitmentFeerate(feerates1, defaultNodeId, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) == defaultMaxCommitFeerate / 2) val feerates2 = FeeratesPerKw.single(FeeratePerKw(10000 sat)).copy(fast = defaultMaxCommitFeerate * 2, minimum = FeeratePerKw(250 sat)) - assert(feeConf.getCommitmentFeerate(feerates2, defaultNodeId, UnsafeLegacyAnchorOutputsCommitmentFormat, 100000 sat) == defaultMaxCommitFeerate) - assert(feeConf.getCommitmentFeerate(feerates2, defaultNodeId, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat, 100000 sat) == defaultMaxCommitFeerate) - assert(feeConf.getCommitmentFeerate(feerates2, overrideNodeId, UnsafeLegacyAnchorOutputsCommitmentFormat, 100000 sat) == overrideMaxCommitFeerate) - assert(feeConf.getCommitmentFeerate(feerates2, overrideNodeId, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat, 100000 sat) == overrideMaxCommitFeerate) + assert(feeConf.getCommitmentFeerate(feerates2, defaultNodeId, ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat) == defaultMaxCommitFeerate) + assert(feeConf.getCommitmentFeerate(feerates2, defaultNodeId, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) == defaultMaxCommitFeerate) + assert(feeConf.getCommitmentFeerate(feerates2, overrideNodeId, ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat) == overrideMaxCommitFeerate) + assert(feeConf.getCommitmentFeerate(feerates2, overrideNodeId, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) == overrideMaxCommitFeerate) val feerates3 = FeeratesPerKw.single(FeeratePerKw(10000 sat)).copy(fast = defaultMaxCommitFeerate / 2, minimum = FeeratePerKw(250 sat)) - assert(feeConf.getCommitmentFeerate(feerates3, defaultNodeId, UnsafeLegacyAnchorOutputsCommitmentFormat, 100000 sat) == defaultMaxCommitFeerate / 2) - assert(feeConf.getCommitmentFeerate(feerates3, defaultNodeId, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat, 100000 sat) == defaultMaxCommitFeerate / 2) + assert(feeConf.getCommitmentFeerate(feerates3, defaultNodeId, ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat) == defaultMaxCommitFeerate / 2) + assert(feeConf.getCommitmentFeerate(feerates3, defaultNodeId, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) == defaultMaxCommitFeerate / 2) val feerates4 = FeeratesPerKw.single(FeeratePerKw(10000 sat)).copy(fast = defaultMaxCommitFeerate * 1.5, minimum = FeeratePerKw(250 sat)) - assert(feeConf.getCommitmentFeerate(feerates4, defaultNodeId, UnsafeLegacyAnchorOutputsCommitmentFormat, 100000 sat) == defaultMaxCommitFeerate) - assert(feeConf.getCommitmentFeerate(feerates4, defaultNodeId, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat, 100000 sat) == defaultMaxCommitFeerate) + assert(feeConf.getCommitmentFeerate(feerates4, defaultNodeId, ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat) == defaultMaxCommitFeerate) + assert(feeConf.getCommitmentFeerate(feerates4, defaultNodeId, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) == defaultMaxCommitFeerate) val feerates5 = FeeratesPerKw.single(FeeratePerKw(25000 sat)).copy(minimum = FeeratePerKw(10000 sat)) - assert(feeConf.getCommitmentFeerate(feerates5, defaultNodeId, UnsafeLegacyAnchorOutputsCommitmentFormat, 100000 sat) == FeeratePerKw(10000 sat) * 1.25) - assert(feeConf.getCommitmentFeerate(feerates5, defaultNodeId, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat, 100000 sat) == FeeratePerKw(10000 sat) * 1.25) - assert(feeConf.getCommitmentFeerate(feerates5, overrideNodeId, UnsafeLegacyAnchorOutputsCommitmentFormat, 100000 sat) == FeeratePerKw(10000 sat) * 1.25) - assert(feeConf.getCommitmentFeerate(feerates5, overrideNodeId, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat, 100000 sat) == FeeratePerKw(10000 sat) * 1.25) + assert(feeConf.getCommitmentFeerate(feerates5, defaultNodeId, ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat) == FeeratePerKw(10000 sat) * 1.25) + assert(feeConf.getCommitmentFeerate(feerates5, defaultNodeId, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) == FeeratePerKw(10000 sat) * 1.25) + assert(feeConf.getCommitmentFeerate(feerates5, overrideNodeId, ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat) == FeeratePerKw(10000 sat) * 1.25) + assert(feeConf.getCommitmentFeerate(feerates5, overrideNodeId, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) == FeeratePerKw(10000 sat) * 1.25) val feerates6 = FeeratesPerKw.single(FeeratePerKw(25000 sat)).copy(minimum = FeeratePerKw(10000 sat)) - assert(feeConf.getCommitmentFeerate(feerates6, defaultNodeId, UnsafeLegacyAnchorOutputsCommitmentFormat, 100000 sat) == FeeratePerKw(10000 sat) * 1.25) - assert(feeConf.getCommitmentFeerate(feerates6, defaultNodeId, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat, 100000 sat) == FeeratePerKw(10000 sat) * 1.25) - assert(feeConf.getCommitmentFeerate(feerates6, overrideNodeId, UnsafeLegacyAnchorOutputsCommitmentFormat, 100000 sat) == FeeratePerKw(10000 sat) * 1.25) - assert(feeConf.getCommitmentFeerate(feerates6, overrideNodeId, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat, 100000 sat) == FeeratePerKw(10000 sat) * 1.25) + assert(feeConf.getCommitmentFeerate(feerates6, defaultNodeId, ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat) == FeeratePerKw(10000 sat) * 1.25) + assert(feeConf.getCommitmentFeerate(feerates6, defaultNodeId, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) == FeeratePerKw(10000 sat) * 1.25) + assert(feeConf.getCommitmentFeerate(feerates6, overrideNodeId, ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat) == FeeratePerKw(10000 sat) * 1.25) + assert(feeConf.getCommitmentFeerate(feerates6, overrideNodeId, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) == FeeratePerKw(10000 sat) * 1.25) } - test("fee difference too high") { - val tolerance = FeerateTolerance(ratioLow = 0.5, ratioHigh = 4.0, anchorOutputMaxCommitFeerate = FeeratePerKw(2500 sat), DustTolerance(25000 sat, closeOnUpdateFeeOverflow = false)) - val testCases = Seq( - (FeeratePerKw(500 sat), FeeratePerKw(500 sat), false), - (FeeratePerKw(500 sat), FeeratePerKw(250 sat), false), - (FeeratePerKw(500 sat), FeeratePerKw(249 sat), true), - (FeeratePerKw(500 sat), FeeratePerKw(200 sat), true), - (FeeratePerKw(249 sat), FeeratePerKw(500 sat), false), - (FeeratePerKw(250 sat), FeeratePerKw(500 sat), false), - (FeeratePerKw(250 sat), FeeratePerKw(1000 sat), false), - (FeeratePerKw(250 sat), FeeratePerKw(1001 sat), true), - (FeeratePerKw(250 sat), FeeratePerKw(1500 sat), true), - ) - testCases.foreach { case (networkFeerate, proposedFeerate, expected) => - assert(tolerance.isFeeDiffTooHigh(DefaultCommitmentFormat, networkFeerate, proposedFeerate) == expected) - } + test("get closing feerate") { + val maxClosingFeerate = FeeratePerKw(2500 sat) + val feeTargets = FeeTargets(funding = ConfirmationPriority.Medium, closing = ConfirmationPriority.Fast) + val feeConf = OnChainFeeConf(feeTargets, maxClosingFeerate, safeUtxosThreshold = 0, spendAnchorWithoutHtlcs = true, anchorWithoutHtlcsMaxFee = 10_000.sat, closeOnOfflineMismatch = true, updateFeeMinDiffRatio = 0.1, defaultFeerateTolerance, Map.empty) + val feerates1 = FeeratesPerKw.single(FeeratePerKw(1000 sat)).copy(fast = FeeratePerKw(1500 sat)) + assert(feeConf.getClosingFeerate(feerates1, None) == FeeratePerKw(1500 sat)) + val feerates2 = FeeratesPerKw.single(FeeratePerKw(1000 sat)).copy(fast = FeeratePerKw(500 sat)) + assert(feeConf.getClosingFeerate(feerates2, None) == FeeratePerKw(500 sat)) + val feerates3 = FeeratesPerKw.single(FeeratePerKw(1000 sat)).copy(fast = FeeratePerKw(3000 sat)) + assert(feeConf.getClosingFeerate(feerates3, None) == maxClosingFeerate) + assert(feeConf.getClosingFeerate(feerates3, maxClosingFeerateOverride_opt = Some(FeeratePerKw(2600 sat))) == FeeratePerKw(2600 sat)) + assert(feeConf.getClosingFeerate(feerates3, maxClosingFeerateOverride_opt = Some(FeeratePerKw(2400 sat))) == FeeratePerKw(2400 sat)) } - test("fee difference too high (anchor outputs)") { + test("fee difference too high") { val tolerance = FeerateTolerance(ratioLow = 0.5, ratioHigh = 4.0, anchorOutputMaxCommitFeerate = FeeratePerKw(2500 sat), DustTolerance(25000 sat, closeOnUpdateFeeOverflow = false)) val testCases = Seq( (FeeratePerKw(500 sat), FeeratePerKw(500 sat), false), @@ -116,8 +113,7 @@ class OnChainFeeConfSpec extends AnyFunSuite { (FeeratePerKw(1000 sat), FeeratePerKw(500 sat), false), ) testCases.foreach { case (networkFeerate, proposedFeerate, expected) => - assert(tolerance.isFeeDiffTooHigh(UnsafeLegacyAnchorOutputsCommitmentFormat, networkFeerate, proposedFeerate) == expected) - assert(tolerance.isFeeDiffTooHigh(ZeroFeeHtlcTxAnchorOutputsCommitmentFormat, networkFeerate, proposedFeerate) == expected) + assert(tolerance.isProposedCommitFeerateTooHigh(networkFeerate, proposedFeerate) == expected) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelDataSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelDataSpec.scala deleted file mode 100644 index 5fd196fe17..0000000000 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelDataSpec.scala +++ /dev/null @@ -1,618 +0,0 @@ -/* - * Copyright 2019 ACINQ SAS - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package fr.acinq.eclair.channel - -import akka.testkit.{TestFSMRef, TestProbe} -import fr.acinq.bitcoin.scalacompat.{ByteVector32, OutPoint, SatoshiLong, Transaction, TxIn, TxOut} -import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.WatchFundingSpentTriggered -import fr.acinq.eclair.channel.Helpers.Closing -import fr.acinq.eclair.channel.fsm.Channel -import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags} -import fr.acinq.eclair.transactions.Transactions._ -import fr.acinq.eclair.wire.protocol.{CommitSig, FailureReason, RevokeAndAck, UnknownNextPeer, UpdateAddHtlc} -import fr.acinq.eclair.{MilliSatoshiLong, NodeParams, TestKitBaseClass} -import org.scalatest.funsuite.AnyFunSuiteLike -import scodec.bits.ByteVector - -class ChannelDataSpec extends TestKitBaseClass with AnyFunSuiteLike with ChannelStateTestsBase { - - implicit val log: akka.event.LoggingAdapter = akka.event.NoLogging - - case class HtlcWithPreimage(preimage: ByteVector32, htlc: UpdateAddHtlc) - - case class Fixture(alice: TestFSMRef[ChannelState, ChannelData, Channel], alicePendingHtlc: HtlcWithPreimage, bob: TestFSMRef[ChannelState, ChannelData, Channel], bobPendingHtlc: HtlcWithPreimage, probe: TestProbe) - - private def setupClosingChannel(testTags: Set[String] = Set.empty): Fixture = { - val probe = TestProbe() - val setup = init() - reachNormal(setup, testTags) - import setup._ - awaitCond(alice.stateName == NORMAL) - awaitCond(bob.stateName == NORMAL) - val (ra1, htlca1) = addHtlc(15_000_000 msat, alice, bob, alice2bob, bob2alice) - val (ra2, htlca2) = addHtlc(16_000_000 msat, alice, bob, alice2bob, bob2alice) - addHtlc(500_000 msat, alice, bob, alice2bob, bob2alice) // below dust - crossSign(alice, bob, alice2bob, bob2alice) - val (rb1, htlcb1) = addHtlc(17_000_000 msat, bob, alice, bob2alice, alice2bob) - val (rb2, htlcb2) = addHtlc(18_000_000 msat, bob, alice, bob2alice, alice2bob) - addHtlc(400_000 msat, bob, alice, bob2alice, alice2bob) // below dust - crossSign(bob, alice, bob2alice, alice2bob) - - // Alice and Bob both know the preimage for only one of the two HTLCs they received. - alice ! CMD_FULFILL_HTLC(htlcb1.id, rb1, replyTo_opt = Some(probe.ref)) - probe.expectMsgType[CommandSuccess[CMD_FULFILL_HTLC]] - bob ! CMD_FULFILL_HTLC(htlca1.id, ra1, replyTo_opt = Some(probe.ref)) - probe.expectMsgType[CommandSuccess[CMD_FULFILL_HTLC]] - - // Alice publishes her commitment. - alice ! CMD_FORCECLOSE(probe.ref) - probe.expectMsgType[CommandSuccess[CMD_FORCECLOSE]] - awaitCond(alice.stateName == CLOSING) - - // Bob detects it. - bob ! WatchFundingSpentTriggered(alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.get.commitTx) - awaitCond(bob.stateName == CLOSING) - - Fixture(alice, HtlcWithPreimage(rb2, htlcb2), bob, HtlcWithPreimage(ra2, htlca2), TestProbe()) - } - - case class LocalFixture(nodeParams: NodeParams, alice: TestFSMRef[ChannelState, ChannelData, Channel], alicePendingHtlc: HtlcWithPreimage, remainingHtlcOutpoint: OutPoint, lcp: LocalCommitPublished, rcp: RemoteCommitPublished, htlcTimeoutTxs: Seq[HtlcTimeoutTx], htlcSuccessTxs: Seq[HtlcSuccessTx], probe: TestProbe) { - val aliceClosing = alice.stateData.asInstanceOf[DATA_CLOSING] - } - - private def setupClosingChannelForLocalClose(): LocalFixture = { - val f = setupClosingChannel() - import f._ - - val nodeParams = alice.underlyingActor.nodeParams - val aliceClosing = alice.stateData.asInstanceOf[DATA_CLOSING] - assert(aliceClosing.localCommitPublished.nonEmpty) - val lcp = aliceClosing.localCommitPublished.get - assert(lcp.commitTx.txOut.length == 6) - assert(lcp.claimMainDelayedOutputTx.nonEmpty) - assert(lcp.htlcTxs.size == 4) // we have one entry for each non-dust htlc - val htlcTimeoutTxs = getHtlcTimeoutTxs(lcp) - assert(htlcTimeoutTxs.length == 2) - val htlcSuccessTxs = getHtlcSuccessTxs(lcp) - assert(htlcSuccessTxs.length == 1) // we only have the preimage for 1 of the 2 non-dust htlcs - val remainingHtlcOutpoint = lcp.htlcTxs.collect { case (outpoint, None) => outpoint }.head - assert(lcp.claimHtlcDelayedTxs.length == 0) // we will publish 3rd-stage txs once htlc txs confirm - assert(!lcp.isConfirmed) - assert(!lcp.isDone) - - // Commit tx has been confirmed. - val lcp1 = Closing.updateLocalCommitPublished(lcp, lcp.commitTx) - assert(lcp1.irrevocablySpent.nonEmpty) - assert(lcp1.isConfirmed) - assert(!lcp1.isDone) - - // Main output has been confirmed. - val lcp2 = Closing.updateLocalCommitPublished(lcp1, lcp.claimMainDelayedOutputTx.get.tx) - assert(lcp2.isConfirmed) - assert(!lcp2.isDone) - - val bobClosing = bob.stateData.asInstanceOf[DATA_CLOSING] - assert(bobClosing.remoteCommitPublished.nonEmpty) - val rcp = bobClosing.remoteCommitPublished.get - - LocalFixture(nodeParams, f.alice, alicePendingHtlc, remainingHtlcOutpoint, lcp2, rcp, htlcTimeoutTxs, htlcSuccessTxs, probe) - } - - test("local commit published (our HTLC txs are confirmed, they claim the remaining HTLC)") { - val f = setupClosingChannelForLocalClose() - import f._ - - val lcp3 = (htlcSuccessTxs.map(_.tx) ++ htlcTimeoutTxs.map(_.tx)).foldLeft(lcp) { - case (current, tx) => - val (current1, Some(_)) = Closing.LocalClose.claimHtlcDelayedOutput(current, nodeParams.channelKeyManager, aliceClosing.commitments.latest, tx, nodeParams.currentBitcoinCoreFeerates, nodeParams.onChainFeeConf, aliceClosing.finalScriptPubKey) - Closing.updateLocalCommitPublished(current1, tx) - } - assert(!lcp3.isDone) - assert(lcp3.claimHtlcDelayedTxs.length == 3) - - val lcp4 = lcp3.claimHtlcDelayedTxs.map(_.tx).foldLeft(lcp3) { - case (current, tx) => Closing.updateLocalCommitPublished(current, tx) - } - assert(!lcp4.isDone) - - val theirClaimHtlcTimeout = rcp.claimHtlcTxs(remainingHtlcOutpoint) - assert(theirClaimHtlcTimeout !== None) - val lcp5 = Closing.updateLocalCommitPublished(lcp4, theirClaimHtlcTimeout.get.tx) - assert(lcp5.isDone) - } - - test("local commit published (our HTLC txs are confirmed and we claim the remaining HTLC)") { - val f = setupClosingChannelForLocalClose() - import f._ - - val lcp3 = (htlcSuccessTxs.map(_.tx) ++ htlcTimeoutTxs.map(_.tx)).foldLeft(lcp) { - case (current, tx) => - val (current1, Some(_)) = Closing.LocalClose.claimHtlcDelayedOutput(current, nodeParams.channelKeyManager, aliceClosing.commitments.latest, tx, nodeParams.currentBitcoinCoreFeerates, nodeParams.onChainFeeConf, aliceClosing.finalScriptPubKey) - Closing.updateLocalCommitPublished(current1, tx) - } - assert(!lcp3.isDone) - assert(lcp3.claimHtlcDelayedTxs.length == 3) - - val lcp4 = lcp3.claimHtlcDelayedTxs.map(_.tx).foldLeft(lcp3) { - case (current, tx) => Closing.updateLocalCommitPublished(current, tx) - } - assert(!lcp4.isDone) - - alice ! CMD_FULFILL_HTLC(alicePendingHtlc.htlc.id, alicePendingHtlc.preimage, replyTo_opt = Some(probe.ref)) - probe.expectMsgType[CommandSuccess[CMD_FULFILL_HTLC]] - val aliceClosing1 = alice.stateData.asInstanceOf[DATA_CLOSING] - val lcp5 = aliceClosing1.localCommitPublished.get.copy(irrevocablySpent = lcp4.irrevocablySpent, claimHtlcDelayedTxs = lcp4.claimHtlcDelayedTxs) - assert(lcp5.htlcTxs(remainingHtlcOutpoint) !== None) - assert(lcp5.claimHtlcDelayedTxs.length == 3) - - val newHtlcSuccessTx = lcp5.htlcTxs(remainingHtlcOutpoint).get.tx - val (lcp6, Some(newClaimHtlcDelayedTx)) = Closing.LocalClose.claimHtlcDelayedOutput(lcp5, nodeParams.channelKeyManager, aliceClosing.commitments.latest, newHtlcSuccessTx, nodeParams.currentBitcoinCoreFeerates, nodeParams.onChainFeeConf, aliceClosing.finalScriptPubKey) - assert(lcp6.claimHtlcDelayedTxs.length == 4) - - val lcp7 = Closing.updateLocalCommitPublished(lcp6, newHtlcSuccessTx) - assert(!lcp7.isDone) - - val lcp8 = Closing.updateLocalCommitPublished(lcp7, newClaimHtlcDelayedTx.tx) - assert(lcp8.isDone) - } - - test("local commit published (they fulfill one of the HTLCs we sent them)") { - val f = setupClosingChannelForLocalClose() - import f._ - - val remoteHtlcSuccess = rcp.claimHtlcTxs.values.collectFirst { case Some(tx: ClaimHtlcSuccessTx) => tx }.get - val lcp3 = (htlcSuccessTxs.map(_.tx) ++ Seq(remoteHtlcSuccess.tx)).foldLeft(lcp) { - case (current, tx) => - val (current1, _) = Closing.LocalClose.claimHtlcDelayedOutput(current, nodeParams.channelKeyManager, aliceClosing.commitments.latest, tx, nodeParams.currentBitcoinCoreFeerates, nodeParams.onChainFeeConf, aliceClosing.finalScriptPubKey) - Closing.updateLocalCommitPublished(current1, tx) - } - assert(lcp3.claimHtlcDelayedTxs.length == 1) - assert(!lcp3.isDone) - - val lcp4 = Closing.updateLocalCommitPublished(lcp3, lcp3.claimHtlcDelayedTxs.head.tx) - assert(!lcp4.isDone) - - val remainingHtlcTimeoutTxs = htlcTimeoutTxs.filter(_.input.outPoint != remoteHtlcSuccess.input.outPoint) - assert(remainingHtlcTimeoutTxs.length == 1) - val (lcp5, Some(remainingClaimHtlcTx)) = Closing.LocalClose.claimHtlcDelayedOutput(lcp4, nodeParams.channelKeyManager, aliceClosing.commitments.latest, remainingHtlcTimeoutTxs.head.tx, nodeParams.currentBitcoinCoreFeerates, nodeParams.onChainFeeConf, aliceClosing.finalScriptPubKey) - assert(lcp5.claimHtlcDelayedTxs.length == 2) - - val lcp6 = (remainingHtlcTimeoutTxs.map(_.tx) ++ Seq(remainingClaimHtlcTx.tx)).foldLeft(lcp5) { - case (current, tx) => Closing.updateLocalCommitPublished(current, tx) - } - assert(!lcp6.isDone) - - val theirClaimHtlcTimeout = rcp.claimHtlcTxs(remainingHtlcOutpoint) - val lcp7 = Closing.updateLocalCommitPublished(lcp6, theirClaimHtlcTimeout.get.tx) - assert(lcp7.isDone) - } - - test("local commit published (they get back the HTLCs they sent us)") { - val f = setupClosingChannelForLocalClose() - import f._ - - val lcp3 = htlcTimeoutTxs.map(_.tx).foldLeft(lcp) { - case (current, tx) => - val (current1, Some(_)) = Closing.LocalClose.claimHtlcDelayedOutput(current, nodeParams.channelKeyManager, aliceClosing.commitments.latest, tx, nodeParams.currentBitcoinCoreFeerates, nodeParams.onChainFeeConf, aliceClosing.finalScriptPubKey) - Closing.updateLocalCommitPublished(current1, tx) - } - assert(!lcp3.isDone) - assert(lcp3.claimHtlcDelayedTxs.length == 2) - - val lcp4 = lcp3.claimHtlcDelayedTxs.map(_.tx).foldLeft(lcp3) { - case (current, tx) => Closing.updateLocalCommitPublished(current, tx) - } - assert(!lcp4.isDone) - - val remoteHtlcTimeoutTxs = getClaimHtlcTimeoutTxs(rcp).map(_.tx) - assert(remoteHtlcTimeoutTxs.length == 2) - val lcp5 = Closing.updateLocalCommitPublished(lcp4, remoteHtlcTimeoutTxs.head) - assert(!lcp5.isDone) - - val lcp6 = Closing.updateLocalCommitPublished(lcp5, remoteHtlcTimeoutTxs.last) - assert(lcp6.isDone) - } - - test("local commit published (our HTLC txs are confirmed and the remaining HTLC is failed)") { - val f = setupClosingChannelForLocalClose() - import f._ - - val lcp3 = (htlcSuccessTxs.map(_.tx) ++ htlcTimeoutTxs.map(_.tx)).foldLeft(lcp) { - case (current, tx) => - val (current1, Some(_)) = Closing.LocalClose.claimHtlcDelayedOutput(current, nodeParams.channelKeyManager, aliceClosing.commitments.latest, tx, nodeParams.currentBitcoinCoreFeerates, nodeParams.onChainFeeConf, aliceClosing.finalScriptPubKey) - Closing.updateLocalCommitPublished(current1, tx) - } - - assert(!lcp3.isDone) - assert(lcp3.claimHtlcDelayedTxs.length == 3) - - val lcp4 = lcp3.claimHtlcDelayedTxs.map(_.tx).foldLeft(lcp3) { - case (current, tx) => Closing.updateLocalCommitPublished(current, tx) - } - assert(!lcp4.isDone) - - // at this point the pending incoming htlc is waiting for a preimage - assert(lcp4.htlcTxs(remainingHtlcOutpoint) == None) - - alice ! CMD_FAIL_HTLC(1, FailureReason.LocalFailure(UnknownNextPeer()), replyTo_opt = Some(probe.ref)) - probe.expectMsgType[CommandSuccess[CMD_FAIL_HTLC]] - val aliceClosing1 = alice.stateData.asInstanceOf[DATA_CLOSING] - val lcp5 = aliceClosing1.localCommitPublished.get.copy(irrevocablySpent = lcp4.irrevocablySpent, claimHtlcDelayedTxs = lcp4.claimHtlcDelayedTxs) - assert(!lcp5.htlcTxs.contains(remainingHtlcOutpoint)) - assert(lcp5.claimHtlcDelayedTxs.length == 3) - - assert(lcp5.isDone) - } - - case class RemoteFixture(bob: TestFSMRef[ChannelState, ChannelData, Channel], bobPendingHtlc: HtlcWithPreimage, remainingHtlcOutpoint: OutPoint, lcp: LocalCommitPublished, rcp: RemoteCommitPublished, claimHtlcTimeoutTxs: Seq[ClaimHtlcTimeoutTx], claimHtlcSuccessTxs: Seq[ClaimHtlcSuccessTx], probe: TestProbe) - - private def setupClosingChannelForRemoteClose(): RemoteFixture = { - val f = setupClosingChannel(Set(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs, ChannelStateTestsTags.StaticRemoteKey)) - import f._ - - val bobClosing = bob.stateData.asInstanceOf[DATA_CLOSING] - assert(bobClosing.remoteCommitPublished.nonEmpty) - val rcp = bobClosing.remoteCommitPublished.get - assert(rcp.commitTx.txOut.length == 8) - assert(rcp.claimMainOutputTx.nonEmpty) - assert(rcp.claimHtlcTxs.size == 4) // we have one entry for each non-dust htlc - val claimHtlcTimeoutTxs = getClaimHtlcTimeoutTxs(rcp) - assert(claimHtlcTimeoutTxs.length == 2) - val claimHtlcSuccessTxs = getClaimHtlcSuccessTxs(rcp) - assert(claimHtlcSuccessTxs.length == 1) // we only have the preimage for 1 of the 2 non-dust htlcs - val remainingHtlcOutpoint = rcp.claimHtlcTxs.collect { case (outpoint, None) => outpoint }.head - assert(!rcp.isConfirmed) - assert(!rcp.isDone) - - // Commit tx has been confirmed. - val rcp1 = Closing.updateRemoteCommitPublished(rcp, rcp.commitTx) - assert(rcp1.irrevocablySpent.nonEmpty) - assert(rcp1.isConfirmed) - assert(!rcp1.isDone) - - // Main output has been confirmed. - val rcp2 = Closing.updateRemoteCommitPublished(rcp1, rcp.claimMainOutputTx.get.tx) - assert(rcp2.isConfirmed) - assert(!rcp2.isDone) - - val aliceClosing = alice.stateData.asInstanceOf[DATA_CLOSING] - assert(aliceClosing.localCommitPublished.nonEmpty) - val lcp = aliceClosing.localCommitPublished.get - - RemoteFixture(f.bob, f.bobPendingHtlc, remainingHtlcOutpoint, lcp, rcp2, claimHtlcTimeoutTxs, claimHtlcSuccessTxs, probe) - } - - test("remote commit published (our claim-HTLC txs are confirmed, they claim the remaining HTLC)") { - val f = setupClosingChannelForRemoteClose() - import f._ - - val rcp3 = (claimHtlcSuccessTxs ++ claimHtlcTimeoutTxs).map(_.tx).foldLeft(rcp) { - case (current, tx) => Closing.updateRemoteCommitPublished(current, tx) - } - assert(!rcp3.isDone) - - val theirHtlcTimeout = lcp.htlcTxs(remainingHtlcOutpoint) - assert(theirHtlcTimeout !== None) - val rcp4 = Closing.updateRemoteCommitPublished(rcp3, theirHtlcTimeout.get.tx) - assert(rcp4.isDone) - } - - test("remote commit published (our claim-HTLC txs are confirmed and we claim the remaining HTLC)") { - val f = setupClosingChannelForRemoteClose() - import f._ - - val rcp3 = (claimHtlcSuccessTxs ++ claimHtlcTimeoutTxs).map(_.tx).foldLeft(rcp) { - case (current, tx) => Closing.updateRemoteCommitPublished(current, tx) - } - assert(!rcp3.isDone) - - bob ! CMD_FULFILL_HTLC(bobPendingHtlc.htlc.id, bobPendingHtlc.preimage, replyTo_opt = Some(probe.ref)) - probe.expectMsgType[CommandSuccess[CMD_FULFILL_HTLC]] - val bobClosing1 = bob.stateData.asInstanceOf[DATA_CLOSING] - val rcp4 = bobClosing1.remoteCommitPublished.get.copy(irrevocablySpent = rcp3.irrevocablySpent) - assert(rcp4.claimHtlcTxs(remainingHtlcOutpoint) !== None) - val newClaimHtlcSuccessTx = rcp4.claimHtlcTxs(remainingHtlcOutpoint).get - - val rcp5 = Closing.updateRemoteCommitPublished(rcp4, newClaimHtlcSuccessTx.tx) - assert(rcp5.isDone) - } - - test("remote commit published (they fulfill one of the HTLCs we sent them)") { - val f = setupClosingChannelForRemoteClose() - import f._ - - val remoteHtlcSuccess = lcp.htlcTxs.values.collectFirst { case Some(tx: HtlcSuccessTx) => tx }.get - val rcp3 = (remoteHtlcSuccess.tx +: claimHtlcSuccessTxs.map(_.tx)).foldLeft(rcp) { - case (current, tx) => Closing.updateRemoteCommitPublished(current, tx) - } - assert(!rcp3.isDone) - - val remainingClaimHtlcTimeoutTx = claimHtlcTimeoutTxs.filter(_.input.outPoint != remoteHtlcSuccess.input.outPoint) - assert(remainingClaimHtlcTimeoutTx.length == 1) - val rcp4 = Closing.updateRemoteCommitPublished(rcp3, remainingClaimHtlcTimeoutTx.head.tx) - assert(!rcp4.isDone) - - val theirHtlcTimeout = lcp.htlcTxs(remainingHtlcOutpoint) - assert(theirHtlcTimeout !== None) - val rcp5 = Closing.updateRemoteCommitPublished(rcp4, theirHtlcTimeout.get.tx) - assert(rcp5.isDone) - } - - test("remote commit published (they get back the HTLCs they sent us)") { - val f = setupClosingChannelForRemoteClose() - import f._ - - val rcp3 = claimHtlcTimeoutTxs.map(_.tx).foldLeft(rcp) { - case (current, tx) => Closing.updateRemoteCommitPublished(current, tx) - } - assert(!rcp3.isDone) - - val htlcTimeoutTxs = getHtlcTimeoutTxs(lcp).map(_.tx) - val rcp4 = Closing.updateRemoteCommitPublished(rcp3, htlcTimeoutTxs.head) - assert(!rcp4.isDone) - - val rcp5 = Closing.updateRemoteCommitPublished(rcp4, htlcTimeoutTxs.last) - assert(rcp5.isDone) - } - - test("remote commit published (our claim-HTLC txs are confirmed and the remaining one is failed)") { - val f = setupClosingChannelForRemoteClose() - import f._ - - val rcp3 = (claimHtlcSuccessTxs ++ claimHtlcTimeoutTxs).map(_.tx).foldLeft(rcp) { - case (current, tx) => Closing.updateRemoteCommitPublished(current, tx) - } - assert(!rcp3.isDone) - - bob ! CMD_FAIL_HTLC(bobPendingHtlc.htlc.id, FailureReason.LocalFailure(UnknownNextPeer()), replyTo_opt = Some(probe.ref)) - probe.expectMsgType[CommandSuccess[CMD_FAIL_HTLC]] - val bobClosing1 = bob.stateData.asInstanceOf[DATA_CLOSING] - val rcp4 = bobClosing1.remoteCommitPublished.get.copy(irrevocablySpent = rcp3.irrevocablySpent) - assert(!rcp4.claimHtlcTxs.contains(remainingHtlcOutpoint)) - assert(rcp4.claimHtlcTxs.size == 3) - assert(getClaimHtlcSuccessTxs(rcp4).size == 1) - assert(getClaimHtlcTimeoutTxs(rcp4).size == 2) - - assert(rcp4.isDone) - } - - private def setupClosingChannelForNextRemoteClose(tags: Set[String] = Set.empty): RemoteFixture = { - val probe = TestProbe() - val setup = init(tags = tags) - reachNormal(setup, tags = tags) - import setup._ - awaitCond(alice.stateName == NORMAL) - awaitCond(bob.stateName == NORMAL) - val (ra1, htlca1) = addHtlc(15_000_000 msat, alice, bob, alice2bob, bob2alice) - val (ra2, htlca2) = addHtlc(16_000_000 msat, alice, bob, alice2bob, bob2alice) - addHtlc(500_000 msat, alice, bob, alice2bob, bob2alice) // below dust - crossSign(alice, bob, alice2bob, bob2alice) - val (rb1, htlcb1) = addHtlc(17_000_000 msat, bob, alice, bob2alice, alice2bob) - addHtlc(400_000 msat, bob, alice, bob2alice, alice2bob) // below dust - crossSign(bob, alice, bob2alice, alice2bob) - addHtlc(18_000_000 msat, bob, alice, bob2alice, alice2bob) - bob ! CMD_SIGN(Some(probe.ref)) - probe.expectMsgType[CommandSuccess[CMD_SIGN]] - bob2alice.expectMsgType[CommitSig] - bob2alice.forward(alice) - alice2bob.expectMsgType[RevokeAndAck] - - // Alice and Bob both know the preimage for only one of the two HTLCs they received. - alice ! CMD_FULFILL_HTLC(htlcb1.id, rb1, replyTo_opt = Some(probe.ref)) - probe.expectMsgType[CommandSuccess[CMD_FULFILL_HTLC]] - bob ! CMD_FULFILL_HTLC(htlca1.id, ra1, replyTo_opt = Some(probe.ref)) - probe.expectMsgType[CommandSuccess[CMD_FULFILL_HTLC]] - - // Alice publishes her last commitment. - alice ! CMD_FORCECLOSE(probe.ref) - probe.expectMsgType[CommandSuccess[CMD_FORCECLOSE]] - awaitCond(alice.stateName == CLOSING) - val aliceClosing = alice.stateData.asInstanceOf[DATA_CLOSING] - val lcp = aliceClosing.localCommitPublished.get - - // Bob detects it. - bob ! WatchFundingSpentTriggered(alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.get.commitTx) - awaitCond(bob.stateName == CLOSING) - - val bobClosing = bob.stateData.asInstanceOf[DATA_CLOSING] - assert(bobClosing.nextRemoteCommitPublished.nonEmpty) - val rcp = bobClosing.nextRemoteCommitPublished.get - assert(rcp.commitTx.txOut.length == 8) - assert(rcp.claimMainOutputTx.nonEmpty) - assert(rcp.claimHtlcTxs.size == 4) // we have one entry for each non-dust htlc - val claimHtlcTimeoutTxs = getClaimHtlcTimeoutTxs(rcp) - assert(claimHtlcTimeoutTxs.length == 2) - val claimHtlcSuccessTxs = getClaimHtlcSuccessTxs(rcp) - assert(claimHtlcSuccessTxs.length == 1) // we only have the preimage for 1 of the 2 non-dust htlcs - val remainingHtlcOutpoint = rcp.claimHtlcTxs.collect { case (outpoint, None) => outpoint }.head - assert(!rcp.isConfirmed) - assert(!rcp.isDone) - - // Commit tx has been confirmed. - val rcp1 = Closing.updateRemoteCommitPublished(rcp, rcp.commitTx) - assert(rcp1.irrevocablySpent.nonEmpty) - assert(rcp1.isConfirmed) - assert(!rcp1.isDone) - - // Main output has been confirmed. - val rcp2 = Closing.updateRemoteCommitPublished(rcp1, rcp.claimMainOutputTx.get.tx) - assert(rcp2.isConfirmed) - assert(!rcp2.isDone) - - val bobPendingHtlc = HtlcWithPreimage(ra2, htlca2) - - RemoteFixture(bob, bobPendingHtlc, remainingHtlcOutpoint, lcp, rcp2, claimHtlcTimeoutTxs, claimHtlcSuccessTxs, probe) - } - - test("next remote commit published (our claim-HTLC txs are confirmed, they claim the remaining HTLC)") { - val f = setupClosingChannelForNextRemoteClose(Set(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs, ChannelStateTestsTags.StaticRemoteKey)) - import f._ - - val rcp3 = (claimHtlcSuccessTxs ++ claimHtlcTimeoutTxs).map(_.tx).foldLeft(rcp) { - case (current, tx) => Closing.updateRemoteCommitPublished(current, tx) - } - assert(!rcp3.isDone) - - val theirHtlcTimeout = lcp.htlcTxs(remainingHtlcOutpoint) - assert(theirHtlcTimeout !== None) - val rcp4 = Closing.updateRemoteCommitPublished(rcp3, theirHtlcTimeout.get.tx) - assert(rcp4.isDone) - } - - test("next remote commit published (our claim-HTLC txs are confirmed and we claim the remaining HTLC)") { - val f = setupClosingChannelForNextRemoteClose(Set(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs, ChannelStateTestsTags.StaticRemoteKey)) - import f._ - - val rcp3 = (claimHtlcSuccessTxs ++ claimHtlcTimeoutTxs).map(_.tx).foldLeft(rcp) { - case (current, tx) => Closing.updateRemoteCommitPublished(current, tx) - } - assert(!rcp3.isDone) - - bob ! CMD_FULFILL_HTLC(bobPendingHtlc.htlc.id, bobPendingHtlc.preimage, replyTo_opt = Some(probe.ref)) - probe.expectMsgType[CommandSuccess[CMD_FULFILL_HTLC]] - val bobClosing1 = bob.stateData.asInstanceOf[DATA_CLOSING] - val rcp4 = bobClosing1.nextRemoteCommitPublished.get.copy(irrevocablySpent = rcp3.irrevocablySpent) - assert(rcp4.claimHtlcTxs(remainingHtlcOutpoint) !== None) - val newClaimHtlcSuccessTx = rcp4.claimHtlcTxs(remainingHtlcOutpoint).get - - val rcp5 = Closing.updateRemoteCommitPublished(rcp4, newClaimHtlcSuccessTx.tx) - assert(rcp5.isDone) - } - - test("next remote commit published (they fulfill one of the HTLCs we sent them)") { - val f = setupClosingChannelForNextRemoteClose(Set(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs, ChannelStateTestsTags.StaticRemoteKey)) - import f._ - - val remoteHtlcSuccess = lcp.htlcTxs.values.collectFirst { case Some(tx: HtlcSuccessTx) => tx }.get - val rcp3 = (remoteHtlcSuccess.tx +: claimHtlcSuccessTxs.map(_.tx)).foldLeft(rcp) { - case (current, tx) => Closing.updateRemoteCommitPublished(current, tx) - } - assert(!rcp3.isDone) - - val remainingClaimHtlcTimeoutTx = claimHtlcTimeoutTxs.filter(_.input.outPoint != remoteHtlcSuccess.input.outPoint) - assert(remainingClaimHtlcTimeoutTx.length == 1) - val rcp4 = Closing.updateRemoteCommitPublished(rcp3, remainingClaimHtlcTimeoutTx.head.tx) - assert(!rcp4.isDone) - - val theirHtlcTimeout = lcp.htlcTxs(remainingHtlcOutpoint) - assert(theirHtlcTimeout !== None) - val rcp5 = Closing.updateRemoteCommitPublished(rcp4, theirHtlcTimeout.get.tx) - assert(rcp5.isDone) - } - - test("next remote commit published (they get back the HTLCs they sent us)") { - val f = setupClosingChannelForNextRemoteClose(Set(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs, ChannelStateTestsTags.StaticRemoteKey)) - import f._ - - val rcp3 = claimHtlcTimeoutTxs.map(_.tx).foldLeft(rcp) { - case (current, tx) => Closing.updateRemoteCommitPublished(current, tx) - } - assert(!rcp3.isDone) - - val htlcTimeoutTxs = getHtlcTimeoutTxs(lcp).map(_.tx) - val rcp4 = Closing.updateRemoteCommitPublished(rcp3, htlcTimeoutTxs.head) - assert(!rcp4.isDone) - - val rcp5 = Closing.updateRemoteCommitPublished(rcp4, htlcTimeoutTxs.last) - assert(rcp5.isDone) - } - - test("revoked commit published") { - val setup = init(tags = Set(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs, ChannelStateTestsTags.StaticRemoteKey)) - reachNormal(setup, tags = Set(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs, ChannelStateTestsTags.StaticRemoteKey)) - import setup._ - awaitCond(alice.stateName == NORMAL) - awaitCond(bob.stateName == NORMAL) - val (ra1, htlca1) = addHtlc(15_000_000 msat, alice, bob, alice2bob, bob2alice) - addHtlc(16_000_000 msat, alice, bob, alice2bob, bob2alice) - addHtlc(500_000 msat, alice, bob, alice2bob, bob2alice) // below dust - crossSign(alice, bob, alice2bob, bob2alice) - addHtlc(17_000_000 msat, bob, alice, bob2alice, alice2bob) - addHtlc(18_000_000 msat, bob, alice, bob2alice, alice2bob) - addHtlc(400_000 msat, bob, alice, bob2alice, alice2bob) // below dust - crossSign(bob, alice, bob2alice, alice2bob) - val revokedCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx - fulfillHtlc(htlca1.id, ra1, bob, alice, bob2alice, alice2bob) - crossSign(bob, alice, bob2alice, alice2bob) - - alice ! WatchFundingSpentTriggered(revokedCommitTx) - awaitCond(alice.stateName == CLOSING) - val aliceClosing = alice.stateData.asInstanceOf[DATA_CLOSING] - assert(aliceClosing.revokedCommitPublished.length == 1) - val rvk = aliceClosing.revokedCommitPublished.head - assert(rvk.claimMainOutputTx.nonEmpty) - assert(rvk.mainPenaltyTx.nonEmpty) - assert(rvk.htlcPenaltyTxs.length == 4) - assert(rvk.claimHtlcDelayedPenaltyTxs.isEmpty) - assert(!rvk.isDone) - - // Commit tx has been confirmed. - val rvk1 = Closing.updateRevokedCommitPublished(rvk, rvk.commitTx) - assert(rvk1.irrevocablySpent.nonEmpty) - assert(!rvk1.isDone) - - // Main output has been confirmed. - val rvk2 = Closing.updateRevokedCommitPublished(rvk1, rvk.claimMainOutputTx.get.tx) - assert(!rvk2.isDone) - - // Two of our htlc penalty txs have been confirmed. - val rvk3 = rvk.htlcPenaltyTxs.map(_.tx).take(2).foldLeft(rvk2) { - case (current, tx) => Closing.updateRevokedCommitPublished(current, tx) - } - assert(!rvk3.isDone) - - // Scenario 1: the remaining penalty txs have been confirmed. - { - val rvk4a = rvk.htlcPenaltyTxs.map(_.tx).drop(2).foldLeft(rvk3) { - case (current, tx) => Closing.updateRevokedCommitPublished(current, tx) - } - assert(!rvk4a.isDone) - - val rvk4b = Closing.updateRevokedCommitPublished(rvk4a, rvk.mainPenaltyTx.get.tx) - assert(rvk4b.isDone) - } - - // Scenario 2: they claim the remaining outputs. - { - val remoteMainOutput = rvk.mainPenaltyTx.get.tx.copy(txOut = Seq(TxOut(35_000 sat, ByteVector.empty))) - val rvk4a = Closing.updateRevokedCommitPublished(rvk3, remoteMainOutput) - assert(!rvk4a.isDone) - - val htlcSuccess = rvk.htlcPenaltyTxs(2).tx.copy(txOut = Seq(TxOut(3_000 sat, ByteVector.empty), TxOut(2_500 sat, ByteVector.empty))) - val htlcTimeout = rvk.htlcPenaltyTxs(3).tx.copy(txOut = Seq(TxOut(3_500 sat, ByteVector.empty), TxOut(3_100 sat, ByteVector.empty))) - // When Bob claims these outputs, the channel should call Helpers.claimRevokedHtlcTxOutputs to punish them by claiming the output of their htlc tx. - // This is tested in ClosingStateSpec. - val rvk4b = Seq(htlcSuccess, htlcTimeout).foldLeft(rvk4a) { - case (current, tx) => Closing.updateRevokedCommitPublished(current, tx) - }.copy( - claimHtlcDelayedPenaltyTxs = List( - ClaimHtlcDelayedOutputPenaltyTx(InputInfo(OutPoint(htlcSuccess, 0), TxOut(2_500 sat, Nil), Nil), Transaction(2, Seq(TxIn(OutPoint(htlcSuccess, 0), ByteVector.empty, 0)), Seq(TxOut(5_000 sat, ByteVector.empty)), 0)), - ClaimHtlcDelayedOutputPenaltyTx(InputInfo(OutPoint(htlcTimeout, 0), TxOut(3_000 sat, Nil), Nil), Transaction(2, Seq(TxIn(OutPoint(htlcTimeout, 0), ByteVector.empty, 0)), Seq(TxOut(6_000 sat, ByteVector.empty)), 0)) - ) - ) - assert(!rvk4b.isDone) - - // We claim one of the remaining outputs, they claim the other. - val rvk5a = Closing.updateRevokedCommitPublished(rvk4b, rvk4b.claimHtlcDelayedPenaltyTxs.head.tx) - assert(!rvk5a.isDone) - val theirClaimHtlcTimeout = rvk4b.claimHtlcDelayedPenaltyTxs(1).tx.copy(txOut = Seq(TxOut(1_500.sat, ByteVector.empty), TxOut(2_500.sat, ByteVector.empty))) - val rvk5b = Closing.updateRevokedCommitPublished(rvk5a, theirClaimHtlcTimeout) - assert(rvk5b.isDone) - } - } - -} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelFeaturesSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelFeaturesSpec.scala index b9185cffee..405223d820 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelFeaturesSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelFeaturesSpec.scala @@ -19,82 +19,15 @@ package fr.acinq.eclair.channel import fr.acinq.eclair.FeatureSupport._ import fr.acinq.eclair.Features._ import fr.acinq.eclair.channel.states.ChannelStateTestsBase -import fr.acinq.eclair.transactions.Transactions -import fr.acinq.eclair.{Feature, Features, InitFeature, TestKitBaseClass} +import fr.acinq.eclair.{Features, InitFeature, PermanentChannelFeature, TestKitBaseClass} import org.scalatest.funsuite.AnyFunSuiteLike class ChannelFeaturesSpec extends TestKitBaseClass with AnyFunSuiteLike with ChannelStateTestsBase { - test("channel features determines commitment format") { - val standardChannel = ChannelFeatures() - val staticRemoteKeyChannel = ChannelFeatures(Features.StaticRemoteKey) - val anchorOutputsChannel = ChannelFeatures(Features.StaticRemoteKey, Features.AnchorOutputs) - val anchorOutputsZeroFeeHtlcsChannel = ChannelFeatures(Features.StaticRemoteKey, Features.AnchorOutputsZeroFeeHtlcTx) - - assert(!standardChannel.hasFeature(Features.StaticRemoteKey)) - assert(!standardChannel.hasFeature(Features.AnchorOutputs)) - assert(standardChannel.commitmentFormat == Transactions.DefaultCommitmentFormat) - assert(!standardChannel.paysDirectlyToWallet) - - assert(staticRemoteKeyChannel.hasFeature(Features.StaticRemoteKey)) - assert(!staticRemoteKeyChannel.hasFeature(Features.AnchorOutputs)) - assert(staticRemoteKeyChannel.commitmentFormat == Transactions.DefaultCommitmentFormat) - assert(staticRemoteKeyChannel.paysDirectlyToWallet) - - assert(anchorOutputsChannel.hasFeature(Features.StaticRemoteKey)) - assert(anchorOutputsChannel.hasFeature(Features.AnchorOutputs)) - assert(!anchorOutputsChannel.hasFeature(Features.AnchorOutputsZeroFeeHtlcTx)) - assert(anchorOutputsChannel.commitmentFormat == Transactions.UnsafeLegacyAnchorOutputsCommitmentFormat) - assert(!anchorOutputsChannel.paysDirectlyToWallet) - - assert(anchorOutputsZeroFeeHtlcsChannel.hasFeature(Features.StaticRemoteKey)) - assert(anchorOutputsZeroFeeHtlcsChannel.hasFeature(Features.AnchorOutputsZeroFeeHtlcTx)) - assert(!anchorOutputsZeroFeeHtlcsChannel.hasFeature(Features.AnchorOutputs)) - assert(anchorOutputsZeroFeeHtlcsChannel.commitmentFormat == Transactions.ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) - assert(!anchorOutputsZeroFeeHtlcsChannel.paysDirectlyToWallet) - } - - test("pick channel type based on local and remote features") { - case class TestCase(localFeatures: Features[InitFeature], remoteFeatures: Features[InitFeature], announceChannel: Boolean, expectedChannelType: ChannelType) - val testCases = Seq( - TestCase(Features.empty, Features.empty, announceChannel = true, ChannelTypes.Standard()), - TestCase(Features(ScidAlias -> Optional), Features(ScidAlias -> Optional), announceChannel = false, ChannelTypes.Standard(scidAlias = true)), - TestCase(Features(StaticRemoteKey -> Optional), Features.empty, announceChannel = true, ChannelTypes.Standard()), - TestCase(Features.empty, Features(StaticRemoteKey -> Optional), announceChannel = true, ChannelTypes.Standard()), - TestCase(Features.empty, Features(StaticRemoteKey -> Mandatory), announceChannel = true, ChannelTypes.Standard()), - TestCase(Features(StaticRemoteKey -> Optional), Features(StaticRemoteKey -> Optional), announceChannel = true, ChannelTypes.StaticRemoteKey()), - TestCase(Features(StaticRemoteKey -> Optional), Features(StaticRemoteKey -> Mandatory), announceChannel = true, ChannelTypes.StaticRemoteKey()), - TestCase(Features(StaticRemoteKey -> Optional, ScidAlias -> Mandatory), Features(StaticRemoteKey -> Mandatory, ScidAlias -> Mandatory), announceChannel = false, ChannelTypes.StaticRemoteKey(scidAlias = true)), - TestCase(Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional), Features(StaticRemoteKey -> Optional), announceChannel = true, ChannelTypes.StaticRemoteKey()), - TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputsZeroFeeHtlcTx -> Optional), Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional), announceChannel = true, ChannelTypes.StaticRemoteKey()), - TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Optional), Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional), announceChannel = true, ChannelTypes.AnchorOutputs()), - TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Optional, ScidAlias -> Optional, ZeroConf -> Optional), Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional, ScidAlias -> Mandatory, ZeroConf -> Optional), announceChannel = false, ChannelTypes.AnchorOutputs(scidAlias = true, zeroConf = true)), - TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Optional), Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional), announceChannel = true, ChannelTypes.AnchorOutputs()), - TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional), Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional), announceChannel = true, ChannelTypes.AnchorOutputsZeroFeeHtlcTx()), - TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional), Features(StaticRemoteKey -> Optional, AnchorOutputs -> Mandatory, AnchorOutputsZeroFeeHtlcTx -> Optional), announceChannel = true, ChannelTypes.AnchorOutputsZeroFeeHtlcTx()), - TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputsZeroFeeHtlcTx -> Optional), Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Mandatory), announceChannel = true, ChannelTypes.AnchorOutputsZeroFeeHtlcTx()), - TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional, Features.ScidAlias -> Optional), Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional), announceChannel = true, ChannelTypes.AnchorOutputsZeroFeeHtlcTx()), - TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional, Features.ScidAlias -> Optional), Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional, Features.ScidAlias -> Optional), announceChannel = true, ChannelTypes.AnchorOutputsZeroFeeHtlcTx()), - TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional, Features.ScidAlias -> Optional), Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional, Features.ScidAlias -> Optional), announceChannel = false, ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = true)), - TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional, Features.ScidAlias -> Optional), Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional, Features.ZeroConf -> Optional), announceChannel = true, ChannelTypes.AnchorOutputsZeroFeeHtlcTx()), - TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional, Features.ZeroConf -> Optional), Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional, Features.ZeroConf -> Optional), announceChannel = true, ChannelTypes.AnchorOutputsZeroFeeHtlcTx(zeroConf = true)), - TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional, Features.ScidAlias -> Mandatory, Features.ZeroConf -> Optional), Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional, Features.ScidAlias -> Optional, Features.ZeroConf -> Optional), announceChannel = true, ChannelTypes.AnchorOutputsZeroFeeHtlcTx(zeroConf = true)), - TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional, Features.ScidAlias -> Mandatory, Features.ZeroConf -> Optional), Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional, Features.ScidAlias -> Optional, Features.ZeroConf -> Optional), announceChannel = false, ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = true, zeroConf = true)), - ) - - for (testCase <- testCases) { - assert(ChannelTypes.defaultFromFeatures(testCase.localFeatures, testCase.remoteFeatures, announceChannel = testCase.announceChannel) == testCase.expectedChannelType, s"localFeatures=${testCase.localFeatures} remoteFeatures=${testCase.remoteFeatures}") - } - } - test("create channel type from features") { case class TestCase(features: Features[InitFeature], expectedChannelType: ChannelType) val validChannelTypes = Seq( - TestCase(Features.empty, ChannelTypes.Standard()), - TestCase(Features(ScidAlias -> Mandatory), ChannelTypes.Standard(scidAlias = true)), - TestCase(Features(StaticRemoteKey -> Mandatory), ChannelTypes.StaticRemoteKey()), - TestCase(Features(StaticRemoteKey -> Mandatory, ScidAlias -> Mandatory), ChannelTypes.StaticRemoteKey(scidAlias = true)), TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Mandatory), ChannelTypes.AnchorOutputs()), TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Mandatory, ScidAlias -> Mandatory, ZeroConf -> Mandatory), ChannelTypes.AnchorOutputs(scidAlias = true, zeroConf = true)), TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputsZeroFeeHtlcTx -> Mandatory), ChannelTypes.AnchorOutputsZeroFeeHtlcTx()), @@ -123,22 +56,17 @@ class ChannelFeaturesSpec extends TestKitBaseClass with AnyFunSuiteLike with Cha } test("enrich channel type with optional permanent channel features") { - case class TestCase(channelType: SupportedChannelType, localFeatures: Features[InitFeature], remoteFeatures: Features[InitFeature], announceChannel: Boolean, expected: Set[Feature]) + case class TestCase(channelType: SupportedChannelType, localFeatures: Features[InitFeature], remoteFeatures: Features[InitFeature], announceChannel: Boolean, expected: Set[PermanentChannelFeature]) val testCases = Seq( - TestCase(ChannelTypes.Standard(), Features(UpfrontShutdownScript -> Optional), Features.empty, announceChannel = true, Set.empty), - TestCase(ChannelTypes.Standard(), Features(UpfrontShutdownScript -> Optional), Features(UpfrontShutdownScript -> Optional), announceChannel = true, Set(UpfrontShutdownScript)), - TestCase(ChannelTypes.Standard(), Features(UpfrontShutdownScript -> Mandatory), Features(UpfrontShutdownScript -> Optional), announceChannel = true, Set(UpfrontShutdownScript)), - TestCase(ChannelTypes.StaticRemoteKey(), Features(UpfrontShutdownScript -> Optional), Features.empty, announceChannel = true, Set(StaticRemoteKey)), - TestCase(ChannelTypes.StaticRemoteKey(), Features(UpfrontShutdownScript -> Optional), Features(UpfrontShutdownScript -> Optional), announceChannel = true, Set(StaticRemoteKey, UpfrontShutdownScript)), - TestCase(ChannelTypes.AnchorOutputs(), Features.empty, Features(UpfrontShutdownScript -> Optional), announceChannel = true, Set(StaticRemoteKey, AnchorOutputs)), - TestCase(ChannelTypes.AnchorOutputs(), Features(UpfrontShutdownScript -> Optional), Features(UpfrontShutdownScript -> Mandatory), announceChannel = true, Set(StaticRemoteKey, AnchorOutputs, UpfrontShutdownScript)), - TestCase(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(), Features.empty, Features(UpfrontShutdownScript -> Optional), announceChannel = true, Set(StaticRemoteKey, AnchorOutputsZeroFeeHtlcTx)), - TestCase(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(), Features(ScidAlias -> Optional, ZeroConf -> Optional), Features(ScidAlias -> Optional, ZeroConf -> Optional), announceChannel = true, Set(StaticRemoteKey, AnchorOutputsZeroFeeHtlcTx, ZeroConf)), - TestCase(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(), Features(ScidAlias -> Optional, ZeroConf -> Optional), Features(ScidAlias -> Optional, ZeroConf -> Optional), announceChannel = false, Set(StaticRemoteKey, AnchorOutputsZeroFeeHtlcTx, ScidAlias, ZeroConf)), - TestCase(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = true), Features.empty, Features(UpfrontShutdownScript -> Optional), announceChannel = false, Set(StaticRemoteKey, AnchorOutputsZeroFeeHtlcTx, ScidAlias)), - TestCase(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = true, zeroConf = true), Features.empty, Features(UpfrontShutdownScript -> Optional), announceChannel = false, Set(StaticRemoteKey, AnchorOutputsZeroFeeHtlcTx, ScidAlias, ZeroConf)), - TestCase(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(), Features(UpfrontShutdownScript -> Optional), Features(UpfrontShutdownScript -> Mandatory), announceChannel = true, Set(StaticRemoteKey, AnchorOutputsZeroFeeHtlcTx, UpfrontShutdownScript)), - TestCase(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(), Features(DualFunding -> Optional, UpfrontShutdownScript -> Optional), Features(DualFunding -> Optional, UpfrontShutdownScript -> Optional), announceChannel = true, Set(StaticRemoteKey, AnchorOutputsZeroFeeHtlcTx, UpfrontShutdownScript, DualFunding)), + TestCase(ChannelTypes.AnchorOutputs(), Features.empty, Features(UpfrontShutdownScript -> Optional), announceChannel = true, Set.empty), + TestCase(ChannelTypes.AnchorOutputs(), Features(UpfrontShutdownScript -> Optional), Features(UpfrontShutdownScript -> Mandatory), announceChannel = true, Set(UpfrontShutdownScript)), + TestCase(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(), Features.empty, Features(UpfrontShutdownScript -> Optional), announceChannel = true, Set.empty), + TestCase(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(), Features(ScidAlias -> Optional, ZeroConf -> Optional), Features(ScidAlias -> Optional, ZeroConf -> Optional), announceChannel = true, Set(ZeroConf)), + TestCase(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(), Features(ScidAlias -> Optional, ZeroConf -> Optional), Features(ScidAlias -> Optional, ZeroConf -> Optional), announceChannel = false, Set(ScidAlias, ZeroConf)), + TestCase(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = true), Features.empty, Features(UpfrontShutdownScript -> Optional), announceChannel = false, Set(ScidAlias)), + TestCase(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = true, zeroConf = true), Features.empty, Features(UpfrontShutdownScript -> Optional), announceChannel = false, Set(ScidAlias, ZeroConf)), + TestCase(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(), Features(UpfrontShutdownScript -> Optional), Features(UpfrontShutdownScript -> Mandatory), announceChannel = true, Set(UpfrontShutdownScript)), + TestCase(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(), Features(DualFunding -> Optional, UpfrontShutdownScript -> Optional), Features(DualFunding -> Optional, UpfrontShutdownScript -> Optional), announceChannel = true, Set(UpfrontShutdownScript, DualFunding)), ) testCases.foreach(t => assert(ChannelFeatures(t.channelType, t.localFeatures, t.remoteFeatures, t.announceChannel).features == t.expected, s"channelType=${t.channelType} localFeatures=${t.localFeatures} remoteFeatures=${t.remoteFeatures}")) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/CommitmentsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/CommitmentsSpec.scala index 4448fb9993..3aaa7f2cf1 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/CommitmentsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/CommitmentsSpec.scala @@ -17,16 +17,16 @@ package fr.acinq.eclair.channel import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey -import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, ByteVector64, DeterministicWallet, OutPoint, Satoshi, SatoshiLong, Transaction, TxOut} +import fr.acinq.bitcoin.scalacompat.{ByteVector64, DeterministicWallet, OutPoint, Satoshi, SatoshiLong, TxOut} import fr.acinq.eclair.TestUtils.randomTxId import fr.acinq.eclair._ import fr.acinq.eclair.blockchain.fee._ -import fr.acinq.eclair.channel.Helpers.Funding +import fr.acinq.eclair.channel.ChannelSpendSignature.IndividualSignature import fr.acinq.eclair.channel.states.ChannelStateTestsBase import fr.acinq.eclair.crypto.ShaChain -import fr.acinq.eclair.crypto.keymanager.LocalChannelKeyManager -import fr.acinq.eclair.transactions.Transactions.CommitTx -import fr.acinq.eclair.transactions.{CommitmentSpec, Scripts, Transactions} +import fr.acinq.eclair.reputation.Reputation +import fr.acinq.eclair.transactions.Transactions.ZeroFeeHtlcTxAnchorOutputsCommitmentFormat +import fr.acinq.eclair.transactions.{CommitmentSpec, Transactions} import fr.acinq.eclair.wire.protocol._ import org.scalatest.funsuite.FixtureAnyFunSuiteLike import org.scalatest.{Outcome, Tag} @@ -40,9 +40,9 @@ class CommitmentsSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with implicit val log: akka.event.LoggingAdapter = akka.event.NoLogging - private val feerates = FeeratesPerKw.single(TestConstants.feeratePerKw) private val feeConfNoMismatch = OnChainFeeConf( feeTargets = FeeTargets(funding = ConfirmationPriority.Medium, closing = ConfirmationPriority.Medium), + maxClosingFeerate = FeeratePerKw(10_000 sat), safeUtxosThreshold = 0, spendAnchorWithoutHtlcs = true, anchorWithoutHtlcsMaxFee = 10_000.sat, @@ -66,10 +66,10 @@ class CommitmentsSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with test("correct values for availableForSend/availableForReceive (success case)") { f => import f._ - val a = 758640000 msat // initial balance alice + val a = 772000000 msat // initial balance alice val b = 190000000 msat // initial balance bob val p = 42000000 msat // a->b payment - val htlcOutputFee = 2 * 1720000 msat // fee due to the additional htlc output; we count it twice because we keep a reserve for a x2 feerate increase + val htlcOutputFee = 860000 msat val maxDustExposure = 500000 sat val ac0 = alice.stateData.asInstanceOf[DATA_NORMAL].commitments @@ -82,19 +82,19 @@ class CommitmentsSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(bc0.availableBalanceForReceive == a) val (payment_preimage, cmdAdd) = makeCmdAdd(p, bob.underlyingActor.nodeParams.nodeId, currentBlockHeight) - val Right((ac1, add)) = ac0.sendAdd(cmdAdd, currentBlockHeight, alice.underlyingActor.nodeParams.channelConf, alice.underlyingActor.nodeParams.currentBitcoinCoreFeerates, alice.underlyingActor.nodeParams.onChainFeeConf) + val Right((ac1, add)) = ac0.sendAdd(cmdAdd, currentBlockHeight, alice.underlyingActor.nodeParams.channelConf, alice.underlyingActor.nodeParams.onChainFeeConf) assert(ac1.availableBalanceForSend == a - p - htlcOutputFee) // as soon as htlc is sent, alice sees its balance decrease (more than the payment amount because of the commitment fees) assert(ac1.availableBalanceForReceive == b) - val Right(bc1) = bc0.receiveAdd(add, bob.underlyingActor.nodeParams.currentBitcoinCoreFeerates, bob.underlyingActor.nodeParams.onChainFeeConf) + val Right(bc1) = bc0.receiveAdd(add) assert(bc1.availableBalanceForSend == b) assert(bc1.availableBalanceForReceive == a - p - htlcOutputFee) - val Right((ac2, commit1)) = ac1.sendCommit(alice.underlyingActor.nodeParams.channelKeyManager) + val Right((ac2, commit1)) = ac1.sendCommit(alice.underlyingActor.channelKeys, Map.empty) assert(ac2.availableBalanceForSend == a - p - htlcOutputFee) assert(ac2.availableBalanceForReceive == b) - val Right((bc2, revocation1)) = bc1.receiveCommit(commit1, bob.underlyingActor.nodeParams.channelKeyManager) + val Right((bc2, revocation1)) = bc1.receiveCommit(commit1, bob.underlyingActor.channelKeys) assert(bc2.availableBalanceForSend == b) assert(bc2.availableBalanceForReceive == a - p - htlcOutputFee) @@ -102,11 +102,11 @@ class CommitmentsSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(ac3.availableBalanceForSend == a - p - htlcOutputFee) assert(ac3.availableBalanceForReceive == b) - val Right((bc3, commit2)) = bc2.sendCommit(bob.underlyingActor.nodeParams.channelKeyManager) + val Right((bc3, commit2)) = bc2.sendCommit(bob.underlyingActor.channelKeys, Map.empty) assert(bc3.availableBalanceForSend == b) assert(bc3.availableBalanceForReceive == a - p - htlcOutputFee) - val Right((ac4, revocation2)) = ac3.receiveCommit(commit2, alice.underlyingActor.nodeParams.channelKeyManager) + val Right((ac4, revocation2)) = ac3.receiveCommit(commit2, alice.underlyingActor.channelKeys) assert(ac4.availableBalanceForSend == a - p - htlcOutputFee) assert(ac4.availableBalanceForReceive == b) @@ -114,8 +114,8 @@ class CommitmentsSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(bc4.availableBalanceForSend == b) assert(bc4.availableBalanceForReceive == a - p - htlcOutputFee) - val cmdFulfill = CMD_FULFILL_HTLC(0, payment_preimage) - val Right((bc5, fulfill)) = bc4.sendFulfill(cmdFulfill) + val cmdFulfill = CMD_FULFILL_HTLC(0, payment_preimage, None) + val Right((bc5, fulfill)) = bc4.sendFulfill(cmdFulfill, bob.underlyingActor.nodeParams.privateKey, useAttributionData = false) assert(bc5.availableBalanceForSend == b + p) // as soon as we have the fulfill, the balance increases assert(bc5.availableBalanceForReceive == a - p - htlcOutputFee) @@ -123,11 +123,11 @@ class CommitmentsSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(ac5.availableBalanceForSend == a - p - htlcOutputFee) assert(ac5.availableBalanceForReceive == b + p) - val Right((bc6, commit3)) = bc5.sendCommit(bob.underlyingActor.nodeParams.channelKeyManager) + val Right((bc6, commit3)) = bc5.sendCommit(bob.underlyingActor.channelKeys, Map.empty) assert(bc6.availableBalanceForSend == b + p) assert(bc6.availableBalanceForReceive == a - p - htlcOutputFee) - val Right((ac6, revocation3)) = ac5.receiveCommit(commit3, alice.underlyingActor.nodeParams.channelKeyManager) + val Right((ac6, revocation3)) = ac5.receiveCommit(commit3, alice.underlyingActor.channelKeys) assert(ac6.availableBalanceForSend == a - p) assert(ac6.availableBalanceForReceive == b + p) @@ -135,11 +135,11 @@ class CommitmentsSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(bc7.availableBalanceForSend == b + p) assert(bc7.availableBalanceForReceive == a - p) - val Right((ac7, commit4)) = ac6.sendCommit(alice.underlyingActor.nodeParams.channelKeyManager) + val Right((ac7, commit4)) = ac6.sendCommit(alice.underlyingActor.channelKeys, Map.empty) assert(ac7.availableBalanceForSend == a - p) assert(ac7.availableBalanceForReceive == b + p) - val Right((bc8, revocation4)) = bc7.receiveCommit(commit4, bob.underlyingActor.nodeParams.channelKeyManager) + val Right((bc8, revocation4)) = bc7.receiveCommit(commit4, bob.underlyingActor.channelKeys) assert(bc8.availableBalanceForSend == b + p) assert(bc8.availableBalanceForReceive == a - p) @@ -151,10 +151,10 @@ class CommitmentsSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with test("correct values for availableForSend/availableForReceive (failure case)") { f => import f._ - val a = 758640000 msat // initial balance alice + val a = 772000000 msat // initial balance alice val b = 190000000 msat // initial balance bob val p = 42000000 msat // a->b payment - val htlcOutputFee = 2 * 1720000 msat // fee due to the additional htlc output; we count it twice because we keep a reserve for a x2 feerate increase + val htlcOutputFee = 860000 msat val maxDustExposure = 500000 sat val ac0 = alice.stateData.asInstanceOf[DATA_NORMAL].commitments @@ -167,19 +167,19 @@ class CommitmentsSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(bc0.availableBalanceForReceive == a) val (_, cmdAdd) = makeCmdAdd(p, bob.underlyingActor.nodeParams.nodeId, currentBlockHeight) - val Right((ac1, add)) = ac0.sendAdd(cmdAdd, currentBlockHeight, alice.underlyingActor.nodeParams.channelConf, alice.underlyingActor.nodeParams.currentBitcoinCoreFeerates, alice.underlyingActor.nodeParams.onChainFeeConf) + val Right((ac1, add)) = ac0.sendAdd(cmdAdd, currentBlockHeight, alice.underlyingActor.nodeParams.channelConf, alice.underlyingActor.nodeParams.onChainFeeConf) assert(ac1.availableBalanceForSend == a - p - htlcOutputFee) // as soon as htlc is sent, alice sees its balance decrease (more than the payment amount because of the commitment fees) assert(ac1.availableBalanceForReceive == b) - val Right(bc1) = bc0.receiveAdd(add, bob.underlyingActor.nodeParams.currentBitcoinCoreFeerates, bob.underlyingActor.nodeParams.onChainFeeConf) + val Right(bc1) = bc0.receiveAdd(add) assert(bc1.availableBalanceForSend == b) assert(bc1.availableBalanceForReceive == a - p - htlcOutputFee) - val Right((ac2, commit1)) = ac1.sendCommit(alice.underlyingActor.nodeParams.channelKeyManager) + val Right((ac2, commit1)) = ac1.sendCommit(alice.underlyingActor.channelKeys, Map.empty) assert(ac2.availableBalanceForSend == a - p - htlcOutputFee) assert(ac2.availableBalanceForReceive == b) - val Right((bc2, revocation1)) = bc1.receiveCommit(commit1, bob.underlyingActor.nodeParams.channelKeyManager) + val Right((bc2, revocation1)) = bc1.receiveCommit(commit1, bob.underlyingActor.channelKeys) assert(bc2.availableBalanceForSend == b) assert(bc2.availableBalanceForReceive == a - p - htlcOutputFee) @@ -187,11 +187,11 @@ class CommitmentsSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(ac3.availableBalanceForSend == a - p - htlcOutputFee) assert(ac3.availableBalanceForReceive == b) - val Right((bc3, commit2)) = bc2.sendCommit(bob.underlyingActor.nodeParams.channelKeyManager) + val Right((bc3, commit2)) = bc2.sendCommit(bob.underlyingActor.channelKeys, Map.empty) assert(bc3.availableBalanceForSend == b) assert(bc3.availableBalanceForReceive == a - p - htlcOutputFee) - val Right((ac4, revocation2)) = ac3.receiveCommit(commit2, alice.underlyingActor.nodeParams.channelKeyManager) + val Right((ac4, revocation2)) = ac3.receiveCommit(commit2, alice.underlyingActor.channelKeys) assert(ac4.availableBalanceForSend == a - p - htlcOutputFee) assert(ac4.availableBalanceForReceive == b) @@ -199,8 +199,8 @@ class CommitmentsSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(bc4.availableBalanceForSend == b) assert(bc4.availableBalanceForReceive == a - p - htlcOutputFee) - val cmdFail = CMD_FAIL_HTLC(0, FailureReason.LocalFailure(IncorrectOrUnknownPaymentDetails(p, BlockHeight(42)))) - val Right((bc5, fail: UpdateFailHtlc)) = bc4.sendFail(cmdFail, bob.underlyingActor.nodeParams.privateKey) + val cmdFail = CMD_FAIL_HTLC(0, FailureReason.LocalFailure(IncorrectOrUnknownPaymentDetails(p, BlockHeight(42))), None) + val Right((bc5, fail: UpdateFailHtlc)) = bc4.sendFail(cmdFail, bob.underlyingActor.nodeParams.privateKey, useAttributableFailures = false) assert(bc5.availableBalanceForSend == b) assert(bc5.availableBalanceForReceive == a - p - htlcOutputFee) // a's balance won't return to previous before she acknowledges the fail @@ -208,11 +208,11 @@ class CommitmentsSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(ac5.availableBalanceForSend == a - p - htlcOutputFee) assert(ac5.availableBalanceForReceive == b) - val Right((bc6, commit3)) = bc5.sendCommit(bob.underlyingActor.nodeParams.channelKeyManager) + val Right((bc6, commit3)) = bc5.sendCommit(bob.underlyingActor.channelKeys, Map.empty) assert(bc6.availableBalanceForSend == b) assert(bc6.availableBalanceForReceive == a - p - htlcOutputFee) - val Right((ac6, revocation3)) = ac5.receiveCommit(commit3, alice.underlyingActor.nodeParams.channelKeyManager) + val Right((ac6, revocation3)) = ac5.receiveCommit(commit3, alice.underlyingActor.channelKeys) assert(ac6.availableBalanceForSend == a) assert(ac6.availableBalanceForReceive == b) @@ -220,11 +220,11 @@ class CommitmentsSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(bc7.availableBalanceForSend == b) assert(bc7.availableBalanceForReceive == a) - val Right((ac7, commit4)) = ac6.sendCommit(alice.underlyingActor.nodeParams.channelKeyManager) + val Right((ac7, commit4)) = ac6.sendCommit(alice.underlyingActor.channelKeys, Map.empty) assert(ac7.availableBalanceForSend == a) assert(ac7.availableBalanceForReceive == b) - val Right((bc8, revocation4)) = bc7.receiveCommit(commit4, bob.underlyingActor.nodeParams.channelKeyManager) + val Right((bc8, revocation4)) = bc7.receiveCommit(commit4, bob.underlyingActor.channelKeys) assert(bc8.availableBalanceForSend == b) assert(bc8.availableBalanceForReceive == a) @@ -236,12 +236,12 @@ class CommitmentsSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with test("correct values for availableForSend/availableForReceive (multiple htlcs)") { f => import f._ - val a = 758640000 msat // initial balance alice + val a = 772000000 msat // initial balance alice val b = 190000000 msat // initial balance bob val p1 = 18000000 msat // a->b payment val p2 = 20000000 msat // a->b payment val p3 = 40000000 msat // b->a payment - val htlcOutputFee = 2 * 1720000 msat // fee due to the additional htlc output; we count it twice because we keep a reserve for a x2 feerate increase + val htlcOutputFee = 860000 msat val maxDustExposure = 500000 sat val ac0 = alice.stateData.asInstanceOf[DATA_NORMAL].commitments @@ -255,37 +255,37 @@ class CommitmentsSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(bc0.availableBalanceForReceive == a) val (payment_preimage1, cmdAdd1) = makeCmdAdd(p1, bob.underlyingActor.nodeParams.nodeId, currentBlockHeight) - val Right((ac1, add1)) = ac0.sendAdd(cmdAdd1, currentBlockHeight, alice.underlyingActor.nodeParams.channelConf, alice.underlyingActor.nodeParams.currentBitcoinCoreFeerates, alice.underlyingActor.nodeParams.onChainFeeConf) + val Right((ac1, add1)) = ac0.sendAdd(cmdAdd1, currentBlockHeight, alice.underlyingActor.nodeParams.channelConf, alice.underlyingActor.nodeParams.onChainFeeConf) assert(ac1.availableBalanceForSend == a - p1 - htlcOutputFee) // as soon as htlc is sent, alice sees its balance decrease (more than the payment amount because of the commitment fees) assert(ac1.availableBalanceForReceive == b) val (_, cmdAdd2) = makeCmdAdd(p2, bob.underlyingActor.nodeParams.nodeId, currentBlockHeight) - val Right((ac2, add2)) = ac1.sendAdd(cmdAdd2, currentBlockHeight, alice.underlyingActor.nodeParams.channelConf, alice.underlyingActor.nodeParams.currentBitcoinCoreFeerates, alice.underlyingActor.nodeParams.onChainFeeConf) + val Right((ac2, add2)) = ac1.sendAdd(cmdAdd2, currentBlockHeight, alice.underlyingActor.nodeParams.channelConf, alice.underlyingActor.nodeParams.onChainFeeConf) assert(ac2.availableBalanceForSend == a - p1 - htlcOutputFee - p2 - htlcOutputFee) // as soon as htlc is sent, alice sees its balance decrease (more than the payment amount because of the commitment fees) assert(ac2.availableBalanceForReceive == b) val (payment_preimage3, cmdAdd3) = makeCmdAdd(p3, alice.underlyingActor.nodeParams.nodeId, currentBlockHeight) - val Right((bc1, add3)) = bc0.sendAdd(cmdAdd3, currentBlockHeight, bob.underlyingActor.nodeParams.channelConf, bob.underlyingActor.nodeParams.currentBitcoinCoreFeerates, bob.underlyingActor.nodeParams.onChainFeeConf) + val Right((bc1, add3)) = bc0.sendAdd(cmdAdd3, currentBlockHeight, bob.underlyingActor.nodeParams.channelConf, bob.underlyingActor.nodeParams.onChainFeeConf) assert(bc1.availableBalanceForSend == b - p3) // bob doesn't pay the fee assert(bc1.availableBalanceForReceive == a) - val Right(bc2) = bc1.receiveAdd(add1, bob.underlyingActor.nodeParams.currentBitcoinCoreFeerates, bob.underlyingActor.nodeParams.onChainFeeConf) + val Right(bc2) = bc1.receiveAdd(add1) assert(bc2.availableBalanceForSend == b - p3) assert(bc2.availableBalanceForReceive == a - p1 - htlcOutputFee) - val Right(bc3) = bc2.receiveAdd(add2, bob.underlyingActor.nodeParams.currentBitcoinCoreFeerates, bob.underlyingActor.nodeParams.onChainFeeConf) + val Right(bc3) = bc2.receiveAdd(add2) assert(bc3.availableBalanceForSend == b - p3) assert(bc3.availableBalanceForReceive == a - p1 - htlcOutputFee - p2 - htlcOutputFee) - val Right(ac3) = ac2.receiveAdd(add3, alice.underlyingActor.nodeParams.currentBitcoinCoreFeerates, alice.underlyingActor.nodeParams.onChainFeeConf) + val Right(ac3) = ac2.receiveAdd(add3) assert(ac3.availableBalanceForSend == a - p1 - htlcOutputFee - p2 - htlcOutputFee) assert(ac3.availableBalanceForReceive == b - p3) - val Right((ac4, commit1)) = ac3.sendCommit(alice.underlyingActor.nodeParams.channelKeyManager) + val Right((ac4, commit1)) = ac3.sendCommit(alice.underlyingActor.channelKeys, Map.empty) assert(ac4.availableBalanceForSend == a - p1 - htlcOutputFee - p2 - htlcOutputFee) assert(ac4.availableBalanceForReceive == b - p3) - val Right((bc4, revocation1)) = bc3.receiveCommit(commit1, bob.underlyingActor.nodeParams.channelKeyManager) + val Right((bc4, revocation1)) = bc3.receiveCommit(commit1, bob.underlyingActor.channelKeys) assert(bc4.availableBalanceForSend == b - p3) assert(bc4.availableBalanceForReceive == a - p1 - htlcOutputFee - p2 - htlcOutputFee) @@ -293,11 +293,11 @@ class CommitmentsSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(ac5.availableBalanceForSend == a - p1 - htlcOutputFee - p2 - htlcOutputFee) assert(ac5.availableBalanceForReceive == b - p3) - val Right((bc5, commit2)) = bc4.sendCommit(bob.underlyingActor.nodeParams.channelKeyManager) + val Right((bc5, commit2)) = bc4.sendCommit(bob.underlyingActor.channelKeys, Map.empty) assert(bc5.availableBalanceForSend == b - p3) assert(bc5.availableBalanceForReceive == a - p1 - htlcOutputFee - p2 - htlcOutputFee) - val Right((ac6, revocation2)) = ac5.receiveCommit(commit2, alice.underlyingActor.nodeParams.channelKeyManager) + val Right((ac6, revocation2)) = ac5.receiveCommit(commit2, alice.underlyingActor.channelKeys) assert(ac6.availableBalanceForSend == a - p1 - htlcOutputFee - p2 - htlcOutputFee - htlcOutputFee) // alice has acknowledged b's hltc so it needs to pay the fee for it assert(ac6.availableBalanceForReceive == b - p3) @@ -305,11 +305,11 @@ class CommitmentsSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(bc6.availableBalanceForSend == b - p3) assert(bc6.availableBalanceForReceive == a - p1 - htlcOutputFee - p2 - htlcOutputFee - htlcOutputFee) - val Right((ac7, commit3)) = ac6.sendCommit(alice.underlyingActor.nodeParams.channelKeyManager) + val Right((ac7, commit3)) = ac6.sendCommit(alice.underlyingActor.channelKeys, Map.empty) assert(ac7.availableBalanceForSend == a - p1 - htlcOutputFee - p2 - htlcOutputFee - htlcOutputFee) assert(ac7.availableBalanceForReceive == b - p3) - val Right((bc7, revocation3)) = bc6.receiveCommit(commit3, bob.underlyingActor.nodeParams.channelKeyManager) + val Right((bc7, revocation3)) = bc6.receiveCommit(commit3, bob.underlyingActor.channelKeys) assert(bc7.availableBalanceForSend == b - p3) assert(bc7.availableBalanceForReceive == a - p1 - htlcOutputFee - p2 - htlcOutputFee - htlcOutputFee) @@ -317,18 +317,18 @@ class CommitmentsSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(ac8.availableBalanceForSend == a - p1 - htlcOutputFee - p2 - htlcOutputFee - htlcOutputFee) assert(ac8.availableBalanceForReceive == b - p3) - val cmdFulfill1 = CMD_FULFILL_HTLC(0, payment_preimage1) - val Right((bc8, fulfill1)) = bc7.sendFulfill(cmdFulfill1) + val cmdFulfill1 = CMD_FULFILL_HTLC(0, payment_preimage1, None) + val Right((bc8, fulfill1)) = bc7.sendFulfill(cmdFulfill1, bob.underlyingActor.nodeParams.privateKey, useAttributionData = false) assert(bc8.availableBalanceForSend == b + p1 - p3) // as soon as we have the fulfill, the balance increases assert(bc8.availableBalanceForReceive == a - p1 - htlcOutputFee - p2 - htlcOutputFee - htlcOutputFee) - val cmdFail2 = CMD_FAIL_HTLC(1, FailureReason.LocalFailure(IncorrectOrUnknownPaymentDetails(p2, BlockHeight(42)))) - val Right((bc9, fail2: UpdateFailHtlc)) = bc8.sendFail(cmdFail2, bob.underlyingActor.nodeParams.privateKey) + val cmdFail2 = CMD_FAIL_HTLC(1, FailureReason.LocalFailure(IncorrectOrUnknownPaymentDetails(p2, BlockHeight(42))), None) + val Right((bc9, fail2: UpdateFailHtlc)) = bc8.sendFail(cmdFail2, bob.underlyingActor.nodeParams.privateKey, useAttributableFailures = false) assert(bc9.availableBalanceForSend == b + p1 - p3) assert(bc9.availableBalanceForReceive == a - p1 - htlcOutputFee - p2 - htlcOutputFee - htlcOutputFee) // a's balance won't return to previous before she acknowledges the fail - val cmdFulfill3 = CMD_FULFILL_HTLC(0, payment_preimage3) - val Right((ac9, fulfill3)) = ac8.sendFulfill(cmdFulfill3) + val cmdFulfill3 = CMD_FULFILL_HTLC(0, payment_preimage3, None) + val Right((ac9, fulfill3)) = ac8.sendFulfill(cmdFulfill3, alice.underlyingActor.nodeParams.privateKey, useAttributionData = false) assert(ac9.availableBalanceForSend == a - p1 - htlcOutputFee - p2 - htlcOutputFee + p3) // as soon as we have the fulfill, the balance increases assert(ac9.availableBalanceForReceive == b - p3) @@ -344,11 +344,11 @@ class CommitmentsSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(bc10.availableBalanceForSend == b + p1 - p3) assert(bc10.availableBalanceForReceive == a - p1 - htlcOutputFee - p2 - htlcOutputFee + p3) // the fee for p3 disappears - val Right((ac12, commit4)) = ac11.sendCommit(alice.underlyingActor.nodeParams.channelKeyManager) + val Right((ac12, commit4)) = ac11.sendCommit(alice.underlyingActor.channelKeys, Map.empty) assert(ac12.availableBalanceForSend == a - p1 - htlcOutputFee - p2 - htlcOutputFee + p3) assert(ac12.availableBalanceForReceive == b + p1 - p3) - val Right((bc11, revocation4)) = bc10.receiveCommit(commit4, bob.underlyingActor.nodeParams.channelKeyManager) + val Right((bc11, revocation4)) = bc10.receiveCommit(commit4, bob.underlyingActor.channelKeys) assert(bc11.availableBalanceForSend == b + p1 - p3) assert(bc11.availableBalanceForReceive == a - p1 - htlcOutputFee - p2 - htlcOutputFee + p3) @@ -356,11 +356,11 @@ class CommitmentsSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(ac13.availableBalanceForSend == a - p1 - htlcOutputFee - p2 - htlcOutputFee + p3) assert(ac13.availableBalanceForReceive == b + p1 - p3) - val Right((bc12, commit5)) = bc11.sendCommit(bob.underlyingActor.nodeParams.channelKeyManager) + val Right((bc12, commit5)) = bc11.sendCommit(bob.underlyingActor.channelKeys, Map.empty) assert(bc12.availableBalanceForSend == b + p1 - p3) assert(bc12.availableBalanceForReceive == a - p1 - htlcOutputFee - p2 - htlcOutputFee + p3) - val Right((ac14, revocation5)) = ac13.receiveCommit(commit5, alice.underlyingActor.nodeParams.channelKeyManager) + val Right((ac14, revocation5)) = ac13.receiveCommit(commit5, alice.underlyingActor.channelKeys) assert(ac14.availableBalanceForSend == a - p1 + p3) assert(ac14.availableBalanceForReceive == b + p1 - p3) @@ -368,11 +368,11 @@ class CommitmentsSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(bc13.availableBalanceForSend == b + p1 - p3) assert(bc13.availableBalanceForReceive == a - p1 + p3) - val Right((ac15, commit6)) = ac14.sendCommit(alice.underlyingActor.nodeParams.channelKeyManager) + val Right((ac15, commit6)) = ac14.sendCommit(alice.underlyingActor.channelKeys, Map.empty) assert(ac15.availableBalanceForSend == a - p1 + p3) assert(ac15.availableBalanceForReceive == b + p1 - p3) - val Right((bc14, revocation6)) = bc13.receiveCommit(commit6, bob.underlyingActor.nodeParams.channelKeyManager) + val Right((bc14, revocation6)) = bc13.receiveCommit(commit6, bob.underlyingActor.channelKeys) assert(bc14.availableBalanceForSend == b + p1 - p3) assert(bc14.availableBalanceForReceive == a - p1 + p3) @@ -386,7 +386,7 @@ class CommitmentsSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val isInitiator = true val c = CommitmentsSpec.makeCommitments(100000000 msat, 50000000 msat, FeeratePerKw(2500 sat), 546 sat, isInitiator) val (_, cmdAdd) = makeCmdAdd(c.availableBalanceForSend, randomKey().publicKey, f.currentBlockHeight) - val Right((c1, _)) = c.sendAdd(cmdAdd, f.currentBlockHeight, TestConstants.Alice.nodeParams.channelConf, feerates, feeConfNoMismatch) + val Right((c1, _)) = c.sendAdd(cmdAdd, f.currentBlockHeight, TestConstants.Alice.nodeParams.channelConf, feeConfNoMismatch) assert(c1.availableBalanceForSend == 0.msat) // We should be able to handle a fee increase. @@ -394,14 +394,14 @@ class CommitmentsSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // Now we shouldn't be able to send until we receive enough to handle the updated commit tx fee (even trimmed HTLCs shouldn't be sent). val (_, cmdAdd1) = makeCmdAdd(100 msat, randomKey().publicKey, f.currentBlockHeight) - val Left(_: InsufficientFunds) = c2.sendAdd(cmdAdd1, f.currentBlockHeight, TestConstants.Alice.nodeParams.channelConf, feerates, feeConfNoMismatch) + val Left(_: InsufficientFunds) = c2.sendAdd(cmdAdd1, f.currentBlockHeight, TestConstants.Alice.nodeParams.channelConf, feeConfNoMismatch) } test("can send availableForSend") { f => for (isInitiator <- Seq(true, false)) { val c = CommitmentsSpec.makeCommitments(702000000 msat, 52000000 msat, FeeratePerKw(2679 sat), 546 sat, isInitiator) val (_, cmdAdd) = makeCmdAdd(c.availableBalanceForSend, randomKey().publicKey, f.currentBlockHeight) - val result = c.sendAdd(cmdAdd, f.currentBlockHeight, TestConstants.Alice.nodeParams.channelConf, feerates, feeConfNoMismatch) + val result = c.sendAdd(cmdAdd, f.currentBlockHeight, TestConstants.Alice.nodeParams.channelConf, feeConfNoMismatch) assert(result.isRight, result) } } @@ -409,8 +409,8 @@ class CommitmentsSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with test("can receive availableForReceive") { f => for (isInitiator <- Seq(true, false)) { val c = CommitmentsSpec.makeCommitments(31000000 msat, 702000000 msat, FeeratePerKw(2679 sat), 546 sat, isInitiator) - val add = UpdateAddHtlc(randomBytes32(), c.changes.remoteNextHtlcId, c.availableBalanceForReceive, randomBytes32(), CltvExpiry(f.currentBlockHeight), TestConstants.emptyOnionPacket, None, 1.0, None) - c.receiveAdd(add, feerates, feeConfNoMismatch) + val add = UpdateAddHtlc(randomBytes32(), c.changes.remoteNextHtlcId, c.availableBalanceForReceive, randomBytes32(), CltvExpiry(f.currentBlockHeight), TestConstants.emptyOnionPacket, None, Reputation.maxEndorsement, None) + c.receiveAdd(add) } } @@ -431,14 +431,14 @@ class CommitmentsSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with for (_ <- 1 to t.pendingHtlcs) { val amount = Random.nextInt(maxPendingHtlcAmount.toLong.toInt).msat.max(1 msat) val (_, cmdAdd) = makeCmdAdd(amount, randomKey().publicKey, f.currentBlockHeight) - c.sendAdd(cmdAdd, f.currentBlockHeight, TestConstants.Alice.nodeParams.channelConf, feerates, feeConfNoMismatch) match { + c.sendAdd(cmdAdd, f.currentBlockHeight, TestConstants.Alice.nodeParams.channelConf, feeConfNoMismatch) match { case Right((cc, _)) => c = cc case Left(e) => ignore(s"$t -> could not setup initial htlcs: $e") } } if (c.availableBalanceForSend > 0.msat) { val (_, cmdAdd) = makeCmdAdd(c.availableBalanceForSend, randomKey().publicKey, f.currentBlockHeight) - val result = c.sendAdd(cmdAdd, f.currentBlockHeight, TestConstants.Alice.nodeParams.channelConf, feerates, feeConfNoMismatch) + val result = c.sendAdd(cmdAdd, f.currentBlockHeight, TestConstants.Alice.nodeParams.channelConf, feeConfNoMismatch) assert(result.isRight, s"$t -> $result") } } @@ -460,15 +460,15 @@ class CommitmentsSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // Add some initial HTLCs to the pending list (bigger commit tx). for (_ <- 1 to t.pendingHtlcs) { val amount = Random.nextInt(maxPendingHtlcAmount.toLong.toInt).msat.max(1 msat) - val add = UpdateAddHtlc(randomBytes32(), c.changes.remoteNextHtlcId, amount, randomBytes32(), CltvExpiry(f.currentBlockHeight), TestConstants.emptyOnionPacket, None, 1.0, None) - c.receiveAdd(add, feerates, feeConfNoMismatch) match { + val add = UpdateAddHtlc(randomBytes32(), c.changes.remoteNextHtlcId, amount, randomBytes32(), CltvExpiry(f.currentBlockHeight), TestConstants.emptyOnionPacket, None, Reputation.maxEndorsement, None) + c.receiveAdd(add) match { case Right(cc) => c = cc case Left(e) => ignore(s"$t -> could not setup initial htlcs: $e") } } if (c.availableBalanceForReceive > 0.msat) { - val add = UpdateAddHtlc(randomBytes32(), c.changes.remoteNextHtlcId, c.availableBalanceForReceive, randomBytes32(), CltvExpiry(f.currentBlockHeight), TestConstants.emptyOnionPacket, None, 1.0, None) - c.receiveAdd(add, feerates, feeConfNoMismatch) match { + val add = UpdateAddHtlc(randomBytes32(), c.changes.remoteNextHtlcId, c.availableBalanceForReceive, randomBytes32(), CltvExpiry(f.currentBlockHeight), TestConstants.emptyOnionPacket, None, Reputation.maxEndorsement, None) + c.receiveAdd(add) match { case Right(_) => () case Left(e) => fail(s"$t -> $e") } @@ -478,8 +478,10 @@ class CommitmentsSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with test("check if channel seed has been modified") { f => val commitments = f.alice.stateData.asInstanceOf[DATA_NORMAL].commitments - assert(commitments.validateSeed(TestConstants.Alice.channelKeyManager)) - assert(!commitments.validateSeed(new LocalChannelKeyManager(ByteVector32.fromValidHex("42" * 32), Block.RegtestGenesisBlock.hash))) + val aliceChannelKeys = f.alice.underlyingActor.channelKeys + assert(commitments.validateSeed(aliceChannelKeys)) + val otherChannelKeys = TestConstants.Alice.channelKeyManager.channelKeys(ChannelConfig.standard, TestConstants.Alice.channelKeyManager.newFundingKeyPath(isChannelOpener = true)) + assert(!commitments.validateSeed(otherChannelKeys)) } } @@ -487,22 +489,22 @@ object CommitmentsSpec { def makeCommitments(toLocal: MilliSatoshi, toRemote: MilliSatoshi, feeRatePerKw: FeeratePerKw = FeeratePerKw(0 sat), dustLimit: Satoshi = 0 sat, isOpener: Boolean = true, announcement_opt: Option[ChannelAnnouncement] = None): Commitments = { val channelReserve = (toLocal + toRemote).truncateToSatoshi * 0.01 - val localParams = LocalParams(randomKey().publicKey, DeterministicWallet.KeyPath(Seq(42L)), dustLimit, Long.MaxValue.msat, Some(channelReserve), 1 msat, CltvExpiryDelta(144), 50, isOpener, isOpener, None, None, Features.empty) - val remoteParams = RemoteParams(randomKey().publicKey, dustLimit, UInt64.MaxValue, Some(channelReserve), 1 msat, CltvExpiryDelta(144), 50, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, Features.empty, None) + val localChannelParams = LocalChannelParams(randomKey().publicKey, DeterministicWallet.KeyPath(Seq(42L)), Some(channelReserve), isOpener, isOpener, None, Features.empty) + val remoteChannelParams = RemoteChannelParams(randomKey().publicKey, Some(channelReserve), randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, Features.empty, None) + val commitParams = CommitParams(dustLimit, 1 msat, UInt64.MaxValue, 50, CltvExpiryDelta(144)) val localFundingPubKey = randomKey().publicKey val remoteFundingPubKey = randomKey().publicKey - val fundingTx = Transaction(2, Nil, Seq(TxOut((toLocal + toRemote).truncateToSatoshi, Funding.makeFundingPubKeyScript(localFundingPubKey, remoteFundingPubKey))), 0) - val commitmentInput = Transactions.InputInfo(OutPoint(fundingTx, 0), fundingTx.txOut.head, Scripts.multiSig2of2(localFundingPubKey, remoteFundingPubKey)) - val localCommit = LocalCommit(0, CommitmentSpec(Set.empty, feeRatePerKw, toLocal, toRemote), CommitTxAndRemoteSig(CommitTx(commitmentInput, Transaction(2, Nil, Nil, 0)), ByteVector64.Zeroes), Nil) + val fundingTxOut = TxOut((toLocal + toRemote).truncateToSatoshi, Transactions.makeFundingScript(localFundingPubKey, remoteFundingPubKey, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat).pubkeyScript) + val localCommit = LocalCommit(0, CommitmentSpec(Set.empty, feeRatePerKw, toLocal, toRemote), randomTxId(), IndividualSignature(ByteVector64.Zeroes), Nil) val remoteCommit = RemoteCommit(0, CommitmentSpec(Set.empty, feeRatePerKw, toRemote, toLocal), randomTxId(), randomKey().publicKey) val localFundingStatus = announcement_opt match { - case Some(ann) => LocalFundingStatus.ConfirmedFundingTx(fundingTx, ann.shortChannelId, None, None) + case Some(ann) => LocalFundingStatus.ConfirmedFundingTx(Nil, fundingTxOut, ann.shortChannelId, None, None) case None => LocalFundingStatus.SingleFundedUnconfirmedFundingTx(None) } Commitments( - ChannelParams(randomBytes32(), ChannelConfig.standard, ChannelFeatures(), localParams, remoteParams, ChannelFlags(announceChannel = announcement_opt.nonEmpty)), + ChannelParams(randomBytes32(), ChannelConfig.standard, ChannelFeatures(), localChannelParams, remoteChannelParams, ChannelFlags(announceChannel = announcement_opt.nonEmpty)), CommitmentChanges(LocalChanges(Nil, Nil, Nil), RemoteChanges(Nil, Nil, Nil), localNextHtlcId = 1, remoteNextHtlcId = 1), - List(Commitment(0, 0, remoteFundingPubKey, localFundingStatus, RemoteFundingStatus.Locked, localCommit, remoteCommit, None)), + List(Commitment(0, 0, OutPoint(randomTxId(), 0), fundingTxOut.amount, remoteFundingPubKey, localFundingStatus, RemoteFundingStatus.Locked, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat, commitParams, localCommit, commitParams, remoteCommit, None)), inactive = Nil, Right(randomKey().publicKey), ShaChain.init, @@ -512,22 +514,22 @@ object CommitmentsSpec { def makeCommitments(toLocal: MilliSatoshi, toRemote: MilliSatoshi, localNodeId: PublicKey, remoteNodeId: PublicKey, announcement_opt: Option[ChannelAnnouncement]): Commitments = { val channelReserve = (toLocal + toRemote).truncateToSatoshi * 0.01 - val localParams = LocalParams(localNodeId, DeterministicWallet.KeyPath(Seq(42L)), 0 sat, Long.MaxValue.msat, Some(channelReserve), 1 msat, CltvExpiryDelta(144), 50, isChannelOpener = true, paysCommitTxFees = true, None, None, Features.empty) - val remoteParams = RemoteParams(remoteNodeId, 0 sat, UInt64.MaxValue, Some(channelReserve), 1 msat, CltvExpiryDelta(144), 50, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, Features.empty, None) + val localChannelParams = LocalChannelParams(localNodeId, DeterministicWallet.KeyPath(Seq(42L)), Some(channelReserve), isChannelOpener = true, paysCommitTxFees = true, None, Features.empty) + val remoteChannelParams = RemoteChannelParams(remoteNodeId, Some(channelReserve), randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, Features.empty, None) + val commitParams = CommitParams(0 sat, 1 msat, UInt64.MaxValue, 50, CltvExpiryDelta(144)) val localFundingPubKey = randomKey().publicKey val remoteFundingPubKey = randomKey().publicKey - val fundingTx = Transaction(2, Nil, Seq(TxOut((toLocal + toRemote).truncateToSatoshi, Funding.makeFundingPubKeyScript(localFundingPubKey, remoteFundingPubKey))), 0) - val commitmentInput = Transactions.InputInfo(OutPoint(fundingTx, 0), fundingTx.txOut.head, Scripts.multiSig2of2(localFundingPubKey, remoteFundingPubKey)) - val localCommit = LocalCommit(0, CommitmentSpec(Set.empty, FeeratePerKw(0 sat), toLocal, toRemote), CommitTxAndRemoteSig(CommitTx(commitmentInput, Transaction(2, Nil, Nil, 0)), ByteVector64.Zeroes), Nil) + val fundingTxOut = TxOut((toLocal + toRemote).truncateToSatoshi, Transactions.makeFundingScript(localFundingPubKey, remoteFundingPubKey, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat).pubkeyScript) + val localCommit = LocalCommit(0, CommitmentSpec(Set.empty, FeeratePerKw(0 sat), toLocal, toRemote), randomTxId(), IndividualSignature(ByteVector64.Zeroes), Nil) val remoteCommit = RemoteCommit(0, CommitmentSpec(Set.empty, FeeratePerKw(0 sat), toRemote, toLocal), randomTxId(), randomKey().publicKey) val localFundingStatus = announcement_opt match { - case Some(ann) => LocalFundingStatus.ConfirmedFundingTx(fundingTx, ann.shortChannelId, None, None) + case Some(ann) => LocalFundingStatus.ConfirmedFundingTx(Nil, fundingTxOut, ann.shortChannelId, None, None) case None => LocalFundingStatus.SingleFundedUnconfirmedFundingTx(None) } Commitments( - ChannelParams(randomBytes32(), ChannelConfig.standard, ChannelFeatures(), localParams, remoteParams, ChannelFlags(announceChannel = announcement_opt.nonEmpty)), + ChannelParams(randomBytes32(), ChannelConfig.standard, ChannelFeatures(), localChannelParams, remoteChannelParams, ChannelFlags(announceChannel = announcement_opt.nonEmpty)), CommitmentChanges(LocalChanges(Nil, Nil, Nil), RemoteChanges(Nil, Nil, Nil), localNextHtlcId = 1, remoteNextHtlcId = 1), - List(Commitment(0, 0, remoteFundingPubKey, localFundingStatus, RemoteFundingStatus.Locked, localCommit, remoteCommit, None)), + List(Commitment(0, 0, OutPoint(randomTxId(), 0), fundingTxOut.amount, remoteFundingPubKey, localFundingStatus, RemoteFundingStatus.Locked, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat, commitParams, localCommit, commitParams, remoteCommit, None)), inactive = Nil, Right(randomKey().publicKey), ShaChain.init, diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/DustExposureSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/DustExposureSpec.scala index 7bd438bc1b..6c94ae473e 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/DustExposureSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/DustExposureSpec.scala @@ -18,6 +18,7 @@ package fr.acinq.eclair.channel import fr.acinq.bitcoin.scalacompat.{ByteVector32, SatoshiLong} import fr.acinq.eclair.blockchain.fee.{FeeratePerByte, FeeratePerKw} +import fr.acinq.eclair.reputation.Reputation import fr.acinq.eclair.transactions._ import fr.acinq.eclair.wire.protocol.UpdateAddHtlc import fr.acinq.eclair.{CltvExpiry, MilliSatoshi, MilliSatoshiLong, TestConstants, ToMilliSatoshiConversion, randomBytes32} @@ -26,7 +27,7 @@ import org.scalatest.funsuite.AnyFunSuiteLike class DustExposureSpec extends AnyFunSuiteLike { def createHtlc(id: Long, amount: MilliSatoshi): UpdateAddHtlc = { - UpdateAddHtlc(ByteVector32.Zeroes, id, amount, randomBytes32(), CltvExpiry(500), TestConstants.emptyOnionPacket, None, 1.0, None) + UpdateAddHtlc(ByteVector32.Zeroes, id, amount, randomBytes32(), CltvExpiry(500), TestConstants.emptyOnionPacket, None, Reputation.maxEndorsement, None) } test("compute dust exposure") { @@ -41,46 +42,15 @@ class DustExposureSpec extends AnyFunSuiteLike { IncomingHtlc(createHtlc(3, 500.sat.toMilliSatoshi)), OutgoingHtlc(createHtlc(3, 500.sat.toMilliSatoshi)), ) - val spec = CommitmentSpec(htlcs, FeeratePerKw(FeeratePerByte(50 sat)), 50000 msat, 75000 msat) + val spec = CommitmentSpec(htlcs, FeeratePerByte(50 sat).perKw, 50000 msat, 75000 msat) assert(DustExposure.computeExposure(spec, 450 sat, Transactions.ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) == 898.sat.toMilliSatoshi) assert(DustExposure.computeExposure(spec, 500 sat, Transactions.ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) == 2796.sat.toMilliSatoshi) assert(DustExposure.computeExposure(spec, 500 sat, Transactions.UnsafeLegacyAnchorOutputsCommitmentFormat) == 3796.sat.toMilliSatoshi) } - { - // Low feerate: buffer adds 10 sat/byte - val dustLimit = 500.sat - val feerate = FeeratePerKw(FeeratePerByte(10 sat)) - assert(Transactions.receivedHtlcTrimThreshold(dustLimit, feerate, Transactions.DefaultCommitmentFormat) == 2257.sat) - assert(Transactions.offeredHtlcTrimThreshold(dustLimit, feerate, Transactions.DefaultCommitmentFormat) == 2157.sat) - assert(Transactions.receivedHtlcTrimThreshold(dustLimit, feerate * 2, Transactions.DefaultCommitmentFormat) == 4015.sat) - assert(Transactions.offeredHtlcTrimThreshold(dustLimit, feerate * 2, Transactions.DefaultCommitmentFormat) == 3815.sat) - val htlcs = Set[DirectedHtlc]( - // Below the dust limit. - IncomingHtlc(createHtlc(0, 450.sat.toMilliSatoshi)), - OutgoingHtlc(createHtlc(0, 450.sat.toMilliSatoshi)), - // Above the dust limit, trimmed at 10 sat/byte - IncomingHtlc(createHtlc(1, 2250.sat.toMilliSatoshi)), - OutgoingHtlc(createHtlc(1, 2150.sat.toMilliSatoshi)), - // Above the dust limit, trimmed at 20 sat/byte - IncomingHtlc(createHtlc(2, 4010.sat.toMilliSatoshi)), - OutgoingHtlc(createHtlc(2, 3810.sat.toMilliSatoshi)), - // Above the dust limit, untrimmed at 20 sat/byte - IncomingHtlc(createHtlc(3, 4020.sat.toMilliSatoshi)), - OutgoingHtlc(createHtlc(3, 3820.sat.toMilliSatoshi)), - ) - val spec = CommitmentSpec(htlcs, feerate, 50000 msat, 75000 msat) - val expected = 450.sat + 450.sat + 2250.sat + 2150.sat + 4010.sat + 3810.sat - assert(DustExposure.computeExposure(spec, dustLimit, Transactions.DefaultCommitmentFormat) == expected.toMilliSatoshi) - assert(DustExposure.computeExposure(spec, feerate * 2, dustLimit, Transactions.DefaultCommitmentFormat) == DustExposure.computeExposure(spec, dustLimit, Transactions.DefaultCommitmentFormat)) - assert(DustExposure.contributesToDustExposure(IncomingHtlc(createHtlc(4, 4010.sat.toMilliSatoshi)), spec, dustLimit, Transactions.DefaultCommitmentFormat)) - assert(DustExposure.contributesToDustExposure(OutgoingHtlc(createHtlc(4, 3810.sat.toMilliSatoshi)), spec, dustLimit, Transactions.DefaultCommitmentFormat)) - assert(!DustExposure.contributesToDustExposure(IncomingHtlc(createHtlc(5, 4020.sat.toMilliSatoshi)), spec, dustLimit, Transactions.DefaultCommitmentFormat)) - assert(!DustExposure.contributesToDustExposure(OutgoingHtlc(createHtlc(5, 3820.sat.toMilliSatoshi)), spec, dustLimit, Transactions.DefaultCommitmentFormat)) - } { // High feerate: buffer adds 25% val dustLimit = 1000.sat - val feerate = FeeratePerKw(FeeratePerByte(80 sat)) + val feerate = FeeratePerByte(80 sat).perKw assert(Transactions.receivedHtlcTrimThreshold(dustLimit, feerate, Transactions.UnsafeLegacyAnchorOutputsCommitmentFormat) == 15120.sat) assert(Transactions.offeredHtlcTrimThreshold(dustLimit, feerate, Transactions.UnsafeLegacyAnchorOutputsCommitmentFormat) == 14320.sat) assert(Transactions.receivedHtlcTrimThreshold(dustLimit, feerate * 1.25, Transactions.UnsafeLegacyAnchorOutputsCommitmentFormat) == 18650.sat) @@ -102,7 +72,6 @@ class DustExposureSpec extends AnyFunSuiteLike { val spec = CommitmentSpec(htlcs, feerate, 50000 msat, 75000 msat) val expected = 900.sat + 900.sat + 15000.sat + 14000.sat + 18000.sat + 17000.sat assert(DustExposure.computeExposure(spec, dustLimit, Transactions.UnsafeLegacyAnchorOutputsCommitmentFormat) == expected.toMilliSatoshi) - assert(DustExposure.computeExposure(spec, feerate * 1.25, dustLimit, Transactions.DefaultCommitmentFormat) == DustExposure.computeExposure(spec, dustLimit, Transactions.DefaultCommitmentFormat)) assert(DustExposure.contributesToDustExposure(IncomingHtlc(createHtlc(4, 18000.sat.toMilliSatoshi)), spec, dustLimit, Transactions.UnsafeLegacyAnchorOutputsCommitmentFormat)) assert(DustExposure.contributesToDustExposure(OutgoingHtlc(createHtlc(4, 17000.sat.toMilliSatoshi)), spec, dustLimit, Transactions.UnsafeLegacyAnchorOutputsCommitmentFormat)) assert(!DustExposure.contributesToDustExposure(IncomingHtlc(createHtlc(5, 19000.sat.toMilliSatoshi)), spec, dustLimit, Transactions.UnsafeLegacyAnchorOutputsCommitmentFormat)) @@ -113,22 +82,22 @@ class DustExposureSpec extends AnyFunSuiteLike { test("filter incoming htlcs before forwarding") { val dustLimit = 1000.sat val initialSpec = CommitmentSpec(Set.empty, FeeratePerKw(10000 sat), 0 msat, 0 msat) - assert(DustExposure.computeExposure(initialSpec, dustLimit, Transactions.DefaultCommitmentFormat) == 0.msat) - assert(DustExposure.contributesToDustExposure(IncomingHtlc(createHtlc(0, 9000.sat.toMilliSatoshi)), initialSpec, dustLimit, Transactions.DefaultCommitmentFormat)) - assert(DustExposure.contributesToDustExposure(OutgoingHtlc(createHtlc(0, 9000.sat.toMilliSatoshi)), initialSpec, dustLimit, Transactions.DefaultCommitmentFormat)) + assert(DustExposure.computeExposure(initialSpec, dustLimit, Transactions.UnsafeLegacyAnchorOutputsCommitmentFormat) == 0.msat) + assert(DustExposure.contributesToDustExposure(IncomingHtlc(createHtlc(0, 9000.sat.toMilliSatoshi)), initialSpec, dustLimit, Transactions.UnsafeLegacyAnchorOutputsCommitmentFormat)) + assert(DustExposure.contributesToDustExposure(OutgoingHtlc(createHtlc(0, 9000.sat.toMilliSatoshi)), initialSpec, dustLimit, Transactions.UnsafeLegacyAnchorOutputsCommitmentFormat)) // NB: HTLC-success transactions are bigger than HTLC-timeout transactions: that means incoming htlcs have a higher // dust threshold than outgoing htlcs in our commitment. - assert(DustExposure.contributesToDustExposure(IncomingHtlc(createHtlc(0, 9500.sat.toMilliSatoshi)), initialSpec, dustLimit, Transactions.DefaultCommitmentFormat)) - assert(!DustExposure.contributesToDustExposure(OutgoingHtlc(createHtlc(0, 9500.sat.toMilliSatoshi)), initialSpec, dustLimit, Transactions.DefaultCommitmentFormat)) - assert(!DustExposure.contributesToDustExposure(IncomingHtlc(createHtlc(0, 10000.sat.toMilliSatoshi)), initialSpec, dustLimit, Transactions.DefaultCommitmentFormat)) - assert(!DustExposure.contributesToDustExposure(OutgoingHtlc(createHtlc(0, 10000.sat.toMilliSatoshi)), initialSpec, dustLimit, Transactions.DefaultCommitmentFormat)) + assert(DustExposure.contributesToDustExposure(IncomingHtlc(createHtlc(0, 9500.sat.toMilliSatoshi)), initialSpec, dustLimit, Transactions.UnsafeLegacyAnchorOutputsCommitmentFormat)) + assert(!DustExposure.contributesToDustExposure(OutgoingHtlc(createHtlc(0, 9500.sat.toMilliSatoshi)), initialSpec, dustLimit, Transactions.UnsafeLegacyAnchorOutputsCommitmentFormat)) + assert(!DustExposure.contributesToDustExposure(OutgoingHtlc(createHtlc(0, 10000.sat.toMilliSatoshi)), initialSpec, dustLimit, Transactions.UnsafeLegacyAnchorOutputsCommitmentFormat)) + assert(!DustExposure.contributesToDustExposure(OutgoingHtlc(createHtlc(0, 10000.sat.toMilliSatoshi)), initialSpec, dustLimit, Transactions.UnsafeLegacyAnchorOutputsCommitmentFormat)) val updatedSpec = initialSpec.copy(htlcs = Set( OutgoingHtlc(createHtlc(2, 9000.sat.toMilliSatoshi)), OutgoingHtlc(createHtlc(3, 9500.sat.toMilliSatoshi)), IncomingHtlc(createHtlc(4, 9500.sat.toMilliSatoshi)), )) - assert(DustExposure.computeExposure(updatedSpec, dustLimit, Transactions.DefaultCommitmentFormat) == 18500.sat.toMilliSatoshi) + assert(DustExposure.computeExposure(updatedSpec, dustLimit, Transactions.UnsafeLegacyAnchorOutputsCommitmentFormat) == 18500.sat.toMilliSatoshi) val receivedHtlcs = Seq( createHtlc(5, 9500.sat.toMilliSatoshi), @@ -138,7 +107,7 @@ class DustExposureSpec extends AnyFunSuiteLike { createHtlc(9, 400.sat.toMilliSatoshi), createHtlc(10, 50000.sat.toMilliSatoshi), ) - val (accepted, rejected) = DustExposure.filterBeforeForward(25000 sat, updatedSpec, dustLimit, 10000.sat.toMilliSatoshi, initialSpec, dustLimit, 15000.sat.toMilliSatoshi, receivedHtlcs, Transactions.DefaultCommitmentFormat) + val (accepted, rejected) = DustExposure.filterBeforeForward(25000 sat, updatedSpec, dustLimit, 10000.sat.toMilliSatoshi, initialSpec, dustLimit, 15000.sat.toMilliSatoshi, receivedHtlcs, Transactions.UnsafeLegacyAnchorOutputsCommitmentFormat) assert(accepted.map(_.id).toSet == Set(5, 6, 8, 10)) assert(rejected.map(_.id).toSet == Set(7, 9)) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/FuzzySpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/FuzzySpec.scala index 506f8ca31a..0c48fa8555 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/FuzzySpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/FuzzySpec.scala @@ -19,7 +19,7 @@ package fr.acinq.eclair.channel import akka.actor.typed.scaladsl.adapter.actorRefAdapter import akka.actor.{Actor, ActorLogging, ActorRef, Props} import akka.testkit.{TestFSMRef, TestProbe} -import fr.acinq.bitcoin.scalacompat.ByteVector32 +import fr.acinq.bitcoin.scalacompat.{ByteVector32, SatoshiLong} import fr.acinq.eclair.TestConstants.{Alice, Bob} import fr.acinq.eclair._ import fr.acinq.eclair.blockchain.DummyOnChainWallet @@ -33,6 +33,7 @@ import fr.acinq.eclair.payment.receive.MultiPartHandler.ReceiveStandardPayment import fr.acinq.eclair.payment.receive.PaymentHandler import fr.acinq.eclair.payment.relay.Relayer import fr.acinq.eclair.payment.send.ClearRecipient +import fr.acinq.eclair.reputation.Reputation import fr.acinq.eclair.wire.protocol._ import grizzled.slf4j.Logging import org.scalatest.funsuite.FixtureAnyFunSuiteLike @@ -55,6 +56,9 @@ class FuzzySpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Channe val pipe = system.actorOf(Props(new FuzzyPipe(fuzzy))) val aliceParams = Alice.nodeParams val bobParams = Bob.nodeParams + val aliceChannelParams = Alice.channelParams + val bobChannelParams = Bob.channelParams + val commitParams = ProposedCommitParams(1000 sat, 1 msat, UInt64.MaxValue, 30, CltvExpiryDelta(720)) val channelFlags = ChannelFlags(announceChannel = false) val alicePeer = TestProbe() val bobPeer = TestProbe() @@ -66,20 +70,20 @@ class FuzzySpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Channe val bobRegister = system.actorOf(Props(new TestRegister())) val alicePaymentHandler = system.actorOf(Props(new PaymentHandler(aliceParams, aliceRegister, TestProbe().ref))) val bobPaymentHandler = system.actorOf(Props(new PaymentHandler(bobParams, bobRegister, TestProbe().ref))) - val aliceRelayer = system.actorOf(Relayer.props(aliceParams, TestProbe().ref, aliceRegister, alicePaymentHandler)) - val bobRelayer = system.actorOf(Relayer.props(bobParams, TestProbe().ref, bobRegister, bobPaymentHandler)) + val aliceRelayer = system.actorOf(Relayer.props(aliceParams, TestProbe().ref, aliceRegister, alicePaymentHandler, None)) + val bobRelayer = system.actorOf(Relayer.props(bobParams, TestProbe().ref, bobRegister, bobPaymentHandler, None)) val wallet = new DummyOnChainWallet() - val alice: TestFSMRef[ChannelState, ChannelData, Channel] = TestFSMRef(new Channel(aliceParams, wallet, bobParams.nodeId, alice2blockchain.ref, aliceRelayer, FakeTxPublisherFactory(alice2blockchain)), alicePeer.ref) - val bob: TestFSMRef[ChannelState, ChannelData, Channel] = TestFSMRef(new Channel(bobParams, wallet, aliceParams.nodeId, bob2blockchain.ref, bobRelayer, FakeTxPublisherFactory(bob2blockchain)), bobPeer.ref) + val alice: TestFSMRef[ChannelState, ChannelData, Channel] = TestFSMRef(new Channel(aliceParams, Alice.channelKeys(), wallet, bobParams.nodeId, alice2blockchain.ref, aliceRelayer, FakeTxPublisherFactory(alice2blockchain)), alicePeer.ref) + val bob: TestFSMRef[ChannelState, ChannelData, Channel] = TestFSMRef(new Channel(bobParams, Bob.channelKeys(), wallet, aliceParams.nodeId, bob2blockchain.ref, bobRelayer, FakeTxPublisherFactory(bob2blockchain)), bobPeer.ref) within(30 seconds) { - val aliceInit = Init(Alice.channelParams.initFeatures) - val bobInit = Init(Bob.channelParams.initFeatures) + val aliceInit = Init(aliceChannelParams.initFeatures) + val bobInit = Init(bobChannelParams.initFeatures) aliceRegister ! alice bobRegister ! bob // no announcements - alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, TestConstants.fundingSatoshis, dualFunded = false, TestConstants.feeratePerKw, TestConstants.feeratePerKw, fundingTxFeeBudget_opt = None, Some(TestConstants.initiatorPushAmount), requireConfirmedInputs = false, requestFunding_opt = None, Alice.channelParams, pipe, bobInit, channelFlags, ChannelConfig.standard, ChannelTypes.Standard(), replyTo = system.deadLetters) + alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, TestConstants.fundingSatoshis, dualFunded = false, TestConstants.anchorOutputsFeeratePerKw, TestConstants.feeratePerKw, fundingTxFeeBudget_opt = None, Some(TestConstants.initiatorPushAmount), requireConfirmedInputs = false, requestFunding_opt = None, aliceChannelParams, commitParams, pipe, bobInit, channelFlags, ChannelConfig.standard, ChannelTypes.AnchorOutputsZeroFeeHtlcTx(), replyTo = system.deadLetters) alice2blockchain.expectMsgType[TxPublisher.SetChannelId] - bob ! INPUT_INIT_CHANNEL_NON_INITIATOR(ByteVector32.Zeroes, None, dualFunded = false, None, requireConfirmedInputs = false, Bob.channelParams, pipe, aliceInit, ChannelConfig.standard, ChannelTypes.Standard()) + bob ! INPUT_INIT_CHANNEL_NON_INITIATOR(ByteVector32.Zeroes, None, dualFunded = false, None, requireConfirmedInputs = false, bobChannelParams, commitParams, pipe, aliceInit, ChannelConfig.standard, ChannelTypes.AnchorOutputsZeroFeeHtlcTx()) bob2blockchain.expectMsgType[TxPublisher.SetChannelId] pipe ! (alice, bob) alice2blockchain.expectMsgType[TxPublisher.SetChannelId] @@ -118,7 +122,7 @@ class FuzzySpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Channe // allow overpaying (no more than 2 times the required amount) val amount = requiredAmount + Random.nextInt(requiredAmount.toLong.toInt).msat val expiry = (Channel.MIN_CLTV_EXPIRY_DELTA + 1).toCltvExpiry(currentBlockHeight = BlockHeight(400000)) - val Right(payment) = OutgoingPaymentPacket.buildOutgoingPayment(localOrigin(self), invoice.paymentHash, makeSingleHopRoute(amount, invoice.nodeId), ClearRecipient(invoice, amount, expiry, Set.empty), 1.0) + val Right(payment) = OutgoingPaymentPacket.buildOutgoingPayment(localOrigin(self), invoice.paymentHash, makeSingleHopRoute(amount, invoice.nodeId), ClearRecipient(invoice, amount, expiry, Set.empty), Reputation.Score.max) payment.cmd } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/HelpersSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/HelpersSpec.scala index e511ba7030..d0234a8ff4 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/HelpersSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/HelpersSpec.scala @@ -23,7 +23,10 @@ import fr.acinq.eclair.TestUtils.NoLoggingDiagnostics import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.WatchFundingSpentTriggered import fr.acinq.eclair.channel.Helpers.Closing import fr.acinq.eclair.channel.fsm.Channel +import fr.acinq.eclair.channel.publish.TxPublisher.PublishReplaceableTx import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags} +import fr.acinq.eclair.reputation.Reputation +import fr.acinq.eclair.testutils.PimpTestProbe.convert import fr.acinq.eclair.transactions.Transactions._ import fr.acinq.eclair.wire.protocol.UpdateAddHtlc import fr.acinq.eclair.{BlockHeight, MilliSatoshiLong, TestKitBaseClass, TimestampSecond, TimestampSecondLong, randomKey} @@ -47,7 +50,15 @@ class HelpersSpec extends TestKitBaseClass with AnyFunSuiteLike with ChannelStat Helpers.nextChannelUpdateRefresh(TimestampSecond.now()).toSeconds should equal(10 * 24 * 3600L +- 100) } - case class Fixture(alice: TestFSMRef[ChannelState, ChannelData, Channel], aliceCommitPublished: LocalCommitPublished, aliceHtlcs: Set[UpdateAddHtlc], bob: TestFSMRef[ChannelState, ChannelData, Channel], bobCommitPublished: RemoteCommitPublished, bobHtlcs: Set[UpdateAddHtlc], probe: TestProbe) + case class Fixture(alice: TestFSMRef[ChannelState, ChannelData, Channel], + aliceHtlcs: Set[UpdateAddHtlc], + aliceHtlcSuccessTxs: Seq[HtlcSuccessTx], + aliceHtlcTimeoutTxs: Seq[HtlcTimeoutTx], + bob: TestFSMRef[ChannelState, ChannelData, Channel], + bobHtlcs: Set[UpdateAddHtlc], + bobClaimHtlcSuccessTxs: Seq[ClaimHtlcSuccessTx], + bobClaimHtlcTimeoutTxs: Seq[ClaimHtlcTimeoutTx], + probe: TestProbe) def setupHtlcs(testTags: Set[String] = Set.empty): Fixture = { val probe = TestProbe() @@ -58,108 +69,117 @@ class HelpersSpec extends TestKitBaseClass with AnyFunSuiteLike with ChannelStat awaitCond(bob.stateName == NORMAL) // We have two identical HTLCs (MPP): val (_, htlca1a) = addHtlc(15_000_000 msat, alice, bob, alice2bob, bob2alice) - val aliceMppCmd = CMD_ADD_HTLC(TestProbe().ref, 15_000_000 msat, htlca1a.paymentHash, htlca1a.cltvExpiry, htlca1a.onionRoutingPacket, None, 1.0, None, Origin.Hot(TestProbe().ref, Upstream.Local(UUID.randomUUID()))) + val aliceMppCmd = CMD_ADD_HTLC(TestProbe().ref, 15_000_000 msat, htlca1a.paymentHash, htlca1a.cltvExpiry, htlca1a.onionRoutingPacket, None, Reputation.Score.max, None, Origin.Hot(TestProbe().ref, Upstream.Local(UUID.randomUUID()))) val htlca1b = addHtlc(aliceMppCmd, alice, bob, alice2bob, bob2alice) val (ra2, htlca2) = addHtlc(16_000_000 msat, alice, bob, alice2bob, bob2alice) addHtlc(500_000 msat, alice, bob, alice2bob, bob2alice) // below dust crossSign(alice, bob, alice2bob, bob2alice) // We have two identical HTLCs (MPP): val (_, htlcb1a) = addHtlc(17_000_000 msat, bob, alice, bob2alice, alice2bob) - val bobMppCmd = CMD_ADD_HTLC(TestProbe().ref, 17_000_000 msat, htlcb1a.paymentHash, htlcb1a.cltvExpiry, htlcb1a.onionRoutingPacket, None, 1.0, None, Origin.Hot(TestProbe().ref, Upstream.Local(UUID.randomUUID()))) + val bobMppCmd = CMD_ADD_HTLC(TestProbe().ref, 17_000_000 msat, htlcb1a.paymentHash, htlcb1a.cltvExpiry, htlcb1a.onionRoutingPacket, None, Reputation.Score.max, None, Origin.Hot(TestProbe().ref, Upstream.Local(UUID.randomUUID()))) val htlcb1b = addHtlc(bobMppCmd, bob, alice, bob2alice, alice2bob) val (rb2, htlcb2) = addHtlc(18_000_000 msat, bob, alice, bob2alice, alice2bob) addHtlc(400_000 msat, bob, alice, bob2alice, alice2bob) // below dust crossSign(bob, alice, bob2alice, alice2bob) // Alice and Bob both know the preimage for only one of the two HTLCs they received. - alice ! CMD_FULFILL_HTLC(htlcb2.id, rb2, replyTo_opt = Some(probe.ref)) + alice ! CMD_FULFILL_HTLC(htlcb2.id, rb2, None, replyTo_opt = Some(probe.ref)) probe.expectMsgType[CommandSuccess[CMD_FULFILL_HTLC]] - bob ! CMD_FULFILL_HTLC(htlca2.id, ra2, replyTo_opt = Some(probe.ref)) + bob ! CMD_FULFILL_HTLC(htlca2.id, ra2, None, replyTo_opt = Some(probe.ref)) probe.expectMsgType[CommandSuccess[CMD_FULFILL_HTLC]] // Alice publishes her commitment. alice ! CMD_FORCECLOSE(probe.ref) probe.expectMsgType[CommandSuccess[CMD_FORCECLOSE]] + val commitTx = alice2blockchain.expectFinalTxPublished("commit-tx") awaitCond(alice.stateName == CLOSING) - // Bob detects it. - bob ! WatchFundingSpentTriggered(alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.get.commitTx) - awaitCond(bob.stateName == CLOSING) - val lcp = alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.get - assert(lcp.htlcTxs.size == 6) - val htlcTimeoutTxs = getHtlcTimeoutTxs(lcp) + lcp.anchorOutput_opt.foreach(_ => alice2blockchain.expectReplaceableTxPublished[ClaimLocalAnchorTx]) + lcp.localOutput_opt.foreach(_ => alice2blockchain.expectFinalTxPublished("local-main-delayed")) + // Alice is missing the preimage for 2 of the HTLCs she received. + assert(lcp.htlcOutputs.size == 6) + val htlcTxs = (0 until 4).map(_ => alice2blockchain.expectMsgType[PublishReplaceableTx]).map(_.txInfo).collect { case tx: SignedHtlcTx => tx } + alice2blockchain.expectWatchTxConfirmed(commitTx.tx.txid) + val htlcTimeoutTxs = htlcTxs.collect { case tx: HtlcTimeoutTx => tx } assert(htlcTimeoutTxs.length == 3) - val htlcSuccessTxs = getHtlcSuccessTxs(lcp) + assert(lcp.outgoingHtlcs.values.toSet == htlcTimeoutTxs.map(_.htlcId).toSet) + val htlcSuccessTxs = htlcTxs.collect { case tx: HtlcSuccessTx => tx } assert(htlcSuccessTxs.length == 1) + assert(lcp.incomingHtlcs.values.toSet.contains(htlcSuccessTxs.head.htlcId)) + + // Bob detects Alice's force-close. + bob ! WatchFundingSpentTriggered(commitTx.tx) + awaitCond(bob.stateName == CLOSING) val rcp = bob.stateData.asInstanceOf[DATA_CLOSING].remoteCommitPublished.get - assert(rcp.claimHtlcTxs.size == 6) - val claimHtlcTimeoutTxs = getClaimHtlcTimeoutTxs(rcp) + rcp.anchorOutput_opt.foreach(_ => bob2blockchain.expectReplaceableTxPublished[ClaimRemoteAnchorTx]) + rcp.localOutput_opt.foreach(_ => bob2blockchain.expectFinalTxPublished("remote-main-delayed")) + // Bob is missing the preimage for 2 of the HTLCs she received. + assert(rcp.htlcOutputs.size == 6) + val claimHtlcTxs = (0 until 4).map(_ => bob2blockchain.expectMsgType[PublishReplaceableTx]) + bob2blockchain.expectWatchTxConfirmed(commitTx.tx.txid) + val claimHtlcTimeoutTxs = claimHtlcTxs.map(_.txInfo).collect { case tx: ClaimHtlcTimeoutTx => tx } assert(claimHtlcTimeoutTxs.length == 3) - val claimHtlcSuccessTxs = getClaimHtlcSuccessTxs(rcp) + assert(rcp.outgoingHtlcs.values.toSet == claimHtlcTimeoutTxs.map(_.htlcId).toSet) + val claimHtlcSuccessTxs = claimHtlcTxs.map(_.txInfo).collect { case tx: ClaimHtlcSuccessTx => tx } assert(claimHtlcSuccessTxs.length == 1) + assert(rcp.incomingHtlcs.values.toSet.contains(claimHtlcSuccessTxs.head.htlcId)) - Fixture(alice, lcp, Set(htlca1a, htlca1b, htlca2), bob, rcp, Set(htlcb1a, htlcb1b, htlcb2), probe) + Fixture(alice, Set(htlca1a, htlca1b, htlca2), htlcSuccessTxs, htlcTimeoutTxs, bob, Set(htlcb1a, htlcb1b, htlcb2), claimHtlcSuccessTxs, claimHtlcTimeoutTxs, probe) } def findTimedOutHtlcs(f: Fixture): Unit = { import f._ - val dustLimit = alice.underlyingActor.nodeParams.channelConf.dustLimit - val commitmentFormat = alice.stateData.asInstanceOf[DATA_CLOSING].commitments.params.commitmentFormat - val localCommit = alice.stateData.asInstanceOf[DATA_CLOSING].commitments.latest.localCommit - val remoteCommit = bob.stateData.asInstanceOf[DATA_CLOSING].commitments.latest.remoteCommit + val localKeys = alice.underlyingActor.channelKeys + val localCommitment = alice.stateData.asInstanceOf[DATA_CLOSING].commitments.latest + val localCommit = localCommitment.localCommit + val remoteKeys = bob.underlyingActor.channelKeys + val remoteCommitment = bob.stateData.asInstanceOf[DATA_CLOSING].commitments.latest + val remoteCommit = remoteCommitment.remoteCommit - val htlcTimeoutTxs = getHtlcTimeoutTxs(aliceCommitPublished) - val htlcSuccessTxs = getHtlcSuccessTxs(aliceCommitPublished) // Claim-HTLC txs can be modified to pay more (or less) fees by changing the output amount. - val claimHtlcTimeoutTxs = getClaimHtlcTimeoutTxs(bobCommitPublished) - val claimHtlcTimeoutTxsModifiedFees = claimHtlcTimeoutTxs.map(tx => tx.modify(_.tx.txOut).setTo(Seq(tx.tx.txOut.head.copy(amount = 5000 sat)))) - val claimHtlcSuccessTxs = getClaimHtlcSuccessTxs(bobCommitPublished) - val claimHtlcSuccessTxsModifiedFees = claimHtlcSuccessTxs.map(tx => tx.modify(_.tx.txOut).setTo(Seq(tx.tx.txOut.head.copy(amount = 5000 sat)))) + val bobClaimHtlcTimeoutTxsModifiedFees = bobClaimHtlcTimeoutTxs.map(tx => tx.modify(_.tx.txOut).setTo(Seq(tx.tx.txOut.head.copy(amount = 5000 sat)))) + val bobClaimHtlcSuccessTxsModifiedFees = bobClaimHtlcSuccessTxs.map(tx => tx.modify(_.tx.txOut).setTo(Seq(tx.tx.txOut.head.copy(amount = 5000 sat)))) - val aliceTimedOutHtlcs = htlcTimeoutTxs.map(htlcTimeout => { - val timedOutHtlcs = Closing.trimmedOrTimedOutHtlcs(commitmentFormat, localCommit, aliceCommitPublished, dustLimit, htlcTimeout.tx) + val aliceTimedOutHtlcs = aliceHtlcTimeoutTxs.map(htlcTimeout => { + val timedOutHtlcs = Closing.trimmedOrTimedOutHtlcs(localKeys, localCommitment, localCommit, htlcTimeout.tx) assert(timedOutHtlcs.size == 1) timedOutHtlcs.head }) assert(aliceTimedOutHtlcs.toSet == aliceHtlcs) - val bobTimedOutHtlcs = claimHtlcTimeoutTxs.map(claimHtlcTimeout => { - val timedOutHtlcs = Closing.trimmedOrTimedOutHtlcs(commitmentFormat, remoteCommit, bobCommitPublished, dustLimit, claimHtlcTimeout.tx) + val bobTimedOutHtlcs = bobClaimHtlcTimeoutTxs.map(claimHtlcTimeout => { + val timedOutHtlcs = Closing.trimmedOrTimedOutHtlcs(remoteKeys, remoteCommitment, remoteCommit, claimHtlcTimeout.tx) assert(timedOutHtlcs.size == 1) timedOutHtlcs.head }) assert(bobTimedOutHtlcs.toSet == bobHtlcs) - val bobTimedOutHtlcs2 = claimHtlcTimeoutTxsModifiedFees.map(claimHtlcTimeout => { - val timedOutHtlcs = Closing.trimmedOrTimedOutHtlcs(commitmentFormat, remoteCommit, bobCommitPublished, dustLimit, claimHtlcTimeout.tx) + val bobTimedOutHtlcs2 = bobClaimHtlcTimeoutTxsModifiedFees.map(claimHtlcTimeout => { + val timedOutHtlcs = Closing.trimmedOrTimedOutHtlcs(remoteKeys, remoteCommitment, remoteCommit, claimHtlcTimeout.tx) assert(timedOutHtlcs.size == 1) timedOutHtlcs.head }) assert(bobTimedOutHtlcs2.toSet == bobHtlcs) - htlcSuccessTxs.foreach(htlcSuccess => assert(Closing.trimmedOrTimedOutHtlcs(commitmentFormat, localCommit, aliceCommitPublished, dustLimit, htlcSuccess.tx).isEmpty)) - htlcSuccessTxs.foreach(htlcSuccess => assert(Closing.trimmedOrTimedOutHtlcs(commitmentFormat, remoteCommit, bobCommitPublished, dustLimit, htlcSuccess.tx).isEmpty)) - claimHtlcSuccessTxs.foreach(claimHtlcSuccess => assert(Closing.trimmedOrTimedOutHtlcs(commitmentFormat, localCommit, aliceCommitPublished, dustLimit, claimHtlcSuccess.tx).isEmpty)) - claimHtlcSuccessTxsModifiedFees.foreach(claimHtlcSuccess => assert(Closing.trimmedOrTimedOutHtlcs(commitmentFormat, localCommit, aliceCommitPublished, dustLimit, claimHtlcSuccess.tx).isEmpty)) - claimHtlcSuccessTxs.foreach(claimHtlcSuccess => assert(Closing.trimmedOrTimedOutHtlcs(commitmentFormat, remoteCommit, bobCommitPublished, dustLimit, claimHtlcSuccess.tx).isEmpty)) - claimHtlcSuccessTxsModifiedFees.foreach(claimHtlcSuccess => assert(Closing.trimmedOrTimedOutHtlcs(commitmentFormat, remoteCommit, bobCommitPublished, dustLimit, claimHtlcSuccess.tx).isEmpty)) - htlcTimeoutTxs.foreach(htlcTimeout => assert(Closing.trimmedOrTimedOutHtlcs(commitmentFormat, remoteCommit, bobCommitPublished, dustLimit, htlcTimeout.tx).isEmpty)) - claimHtlcTimeoutTxs.foreach(claimHtlcTimeout => assert(Closing.trimmedOrTimedOutHtlcs(commitmentFormat, localCommit, aliceCommitPublished, dustLimit, claimHtlcTimeout.tx).isEmpty)) + aliceHtlcSuccessTxs.foreach(htlcSuccess => assert(Closing.trimmedOrTimedOutHtlcs(localKeys, localCommitment, localCommit, htlcSuccess.sign()).isEmpty)) + aliceHtlcSuccessTxs.foreach(htlcSuccess => assert(Closing.trimmedOrTimedOutHtlcs(remoteKeys, remoteCommitment, remoteCommit, htlcSuccess.sign()).isEmpty)) + bobClaimHtlcSuccessTxs.foreach(claimHtlcSuccess => assert(Closing.trimmedOrTimedOutHtlcs(localKeys, localCommitment, localCommit, claimHtlcSuccess.sign()).isEmpty)) + bobClaimHtlcSuccessTxsModifiedFees.foreach(claimHtlcSuccess => assert(Closing.trimmedOrTimedOutHtlcs(localKeys, localCommitment, localCommit, claimHtlcSuccess.sign()).isEmpty)) + bobClaimHtlcSuccessTxs.foreach(claimHtlcSuccess => assert(Closing.trimmedOrTimedOutHtlcs(remoteKeys, remoteCommitment, remoteCommit, claimHtlcSuccess.sign()).isEmpty)) + bobClaimHtlcSuccessTxsModifiedFees.foreach(claimHtlcSuccess => assert(Closing.trimmedOrTimedOutHtlcs(remoteKeys, remoteCommitment, remoteCommit, claimHtlcSuccess.sign()).isEmpty)) + aliceHtlcTimeoutTxs.foreach(htlcTimeout => assert(Closing.trimmedOrTimedOutHtlcs(remoteKeys, remoteCommitment, remoteCommit, htlcTimeout.sign()).isEmpty)) + bobClaimHtlcTimeoutTxs.foreach(claimHtlcTimeout => assert(Closing.trimmedOrTimedOutHtlcs(localKeys, localCommitment, localCommit, claimHtlcTimeout.sign()).isEmpty)) } test("find timed out htlcs") { findTimedOutHtlcs(setupHtlcs()) } - test("find timed out htlcs (anchor outputs)", Tag(ChannelStateTestsTags.AnchorOutputs)) { - findTimedOutHtlcs(setupHtlcs(Set(ChannelStateTestsTags.AnchorOutputs))) - } - - test("find timed out htlcs (anchor outputs zero fee htlc txs)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { - findTimedOutHtlcs(setupHtlcs(Set(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs))) + test("find timed out htlcs (anchor outputs phoenix)", Tag(ChannelStateTestsTags.AnchorOutputsPhoenix)) { + findTimedOutHtlcs(setupHtlcs(Set(ChannelStateTestsTags.AnchorOutputsPhoenix))) } test("check closing tx amounts above dust") { @@ -178,7 +198,7 @@ class HelpersSpec extends TestKitBaseClass with AnyFunSuiteLike with ChannelStat ) def toClosingTx(txOut: Seq[TxOut]): ClosingTx = { - ClosingTx(InputInfo(OutPoint(TxId(ByteVector32.Zeroes), 0), TxOut(1000 sat, Nil), Nil), Transaction(2, Nil, txOut, 0), None) + ClosingTx(InputInfo(OutPoint(TxId(ByteVector32.Zeroes), 0), TxOut(1000 sat, Nil)), Transaction(2, Nil, txOut, 0), None) } assert(Closing.MutualClose.checkClosingDustAmounts(toClosingTx(allOutputsAboveDust))) @@ -198,7 +218,7 @@ class HelpersSpec extends TestKitBaseClass with AnyFunSuiteLike with ChannelStat Transaction.read("0200000001c8a8934fb38a44b969528252bc37be66ee166c7897c57384d1e561449e110c93010000006b483045022100dc6c50f445ed53d2fb41067fdcb25686fe79492d90e6e5db43235726ace247210220773d35228af0800c257970bee9cf75175d75217de09a8ecd83521befd040c4ca012102082b751372fe7e3b012534afe0bb8d1f2f09c724b1a10a813ce704e5b9c217ccfdffffff0247ba2300000000001976a914f97a7641228e6b17d4b0b08252ae75bd62a95fe788ace3de24000000000017a914a9fefd4b9a9282a1d7a17d2f14ac7d1eb88141d287f7d50800"), Transaction.read("010000000235a2f5c4fd48672534cce1ac063047edc38683f43c5a883f815d6026cb5f8321020000006a47304402206be5fd61b1702599acf51941560f0a1e1965aa086634b004967747f79788bd6e022002f7f719a45b8b5e89129c40a9d15e4a8ee1e33be3a891cf32e859823ecb7a510121024756c5adfbc0827478b0db042ce09d9b98e21ad80d036e73bd8e7f0ecbc254a2ffffffffb2387d3125bb8c84a2da83f4192385ce329283661dfc70191f4112c67ce7b4d0000000006b483045022100a2c737eab1c039f79238767ccb9bb3e81160e965ef0fc2ea79e8360c61b7c9f702202348b0f2c0ea2a757e25d375d9be183200ce0a79ec81d6a4ebb2ae4dc31bc3c9012102db16a822e2ec3706c58fc880c08a3617c61d8ef706cc8830cfe4561d9a5d52f0ffffffff01808d5b00000000001976a9141210c32def6b64d0d77ba8d99adeb7e9f91158b988ac00000000"), Transaction.read("0100000001b14ba6952c83f6f8c382befbf4e44270f13e479d5a5ff3862ac3a112f103ff2a010000006b4830450221008b097fd69bfa3715fc5e119a891933c091c55eabd3d1ddae63a1c2cc36dc9a3e02205666d5299fa403a393bcbbf4b05f9c0984480384796cdebcf69171674d00809c01210335b592484a59a44f40998d65a94f9e2eecca47e8d1799342112a59fc96252830ffffffff024bf308000000000017a914440668d018e5e0ba550d6e042abcf726694f515c8798dd1801000000001976a91453a503fe151dd32e0503bd9a2fbdbf4f9a3af1da88ac00000000") - ).map(tx => ClosingTx(InputInfo(tx.txIn.head.outPoint, TxOut(10_000 sat, Nil), Nil), tx, None)) + ).map(tx => ClosingTx(InputInfo(tx.txIn.head.outPoint, TxOut(10_000 sat, Nil)), tx, None)) // only mutual close assert(Closing.isClosingTypeAlreadyKnown( @@ -225,10 +245,11 @@ class HelpersSpec extends TestKitBaseClass with AnyFunSuiteLike with ChannelStat mutualClosePublished = tx1 :: Nil, localCommitPublished = Some(LocalCommitPublished( commitTx = tx2.tx, - claimMainDelayedOutputTx = Some(ClaimLocalDelayedOutputTx(tx3.input, tx3.tx)), - htlcTxs = Map.empty, - claimHtlcDelayedTxs = Nil, - claimAnchorTxs = Nil, + localOutput_opt = Some(tx3.input.outPoint), + anchorOutput_opt = None, + incomingHtlcs = Map.empty, + outgoingHtlcs = Map.empty, + htlcDelayedOutputs = Set.empty, irrevocablySpent = Map.empty )), remoteCommitPublished = None, @@ -247,10 +268,11 @@ class HelpersSpec extends TestKitBaseClass with AnyFunSuiteLike with ChannelStat mutualClosePublished = tx1 :: Nil, localCommitPublished = Some(LocalCommitPublished( commitTx = tx2.tx, - claimMainDelayedOutputTx = Some(ClaimLocalDelayedOutputTx(tx3.input, tx3.tx)), - htlcTxs = Map.empty, - claimHtlcDelayedTxs = Nil, - claimAnchorTxs = Nil, + localOutput_opt = Some(tx3.input.outPoint), + anchorOutput_opt = None, + incomingHtlcs = Map.empty, + outgoingHtlcs = Map.empty, + htlcDelayedOutputs = Set.empty, irrevocablySpent = Map(tx2.input.outPoint -> tx2.tx) )), remoteCommitPublished = None, @@ -269,17 +291,19 @@ class HelpersSpec extends TestKitBaseClass with AnyFunSuiteLike with ChannelStat mutualClosePublished = Nil, localCommitPublished = Some(LocalCommitPublished( commitTx = tx2.tx, - claimMainDelayedOutputTx = None, - htlcTxs = Map.empty, - claimHtlcDelayedTxs = Nil, - claimAnchorTxs = Nil, + localOutput_opt = None, + anchorOutput_opt = None, + incomingHtlcs = Map.empty, + outgoingHtlcs = Map.empty, + htlcDelayedOutputs = Set.empty, irrevocablySpent = Map.empty )), remoteCommitPublished = Some(RemoteCommitPublished( commitTx = tx3.tx, - claimMainOutputTx = None, - claimHtlcTxs = Map.empty, - claimAnchorTxs = Nil, + localOutput_opt = None, + anchorOutput_opt = None, + incomingHtlcs = Map.empty, + outgoingHtlcs = Map.empty, irrevocablySpent = Map.empty )), nextRemoteCommitPublished = None, @@ -297,17 +321,19 @@ class HelpersSpec extends TestKitBaseClass with AnyFunSuiteLike with ChannelStat mutualClosePublished = tx1 :: Nil, localCommitPublished = Some(LocalCommitPublished( commitTx = tx2.tx, - claimMainDelayedOutputTx = None, - htlcTxs = Map.empty, - claimHtlcDelayedTxs = Nil, - claimAnchorTxs = Nil, + localOutput_opt = None, + anchorOutput_opt = None, + incomingHtlcs = Map.empty, + outgoingHtlcs = Map.empty, + htlcDelayedOutputs = Set.empty, irrevocablySpent = Map.empty )), remoteCommitPublished = Some(RemoteCommitPublished( commitTx = tx3.tx, - claimMainOutputTx = None, - claimHtlcTxs = Map.empty, - claimAnchorTxs = Nil, + localOutput_opt = None, + anchorOutput_opt = None, + incomingHtlcs = Map.empty, + outgoingHtlcs = Map.empty, irrevocablySpent = Map(tx3.input.outPoint -> tx3.tx) )), nextRemoteCommitPublished = None, @@ -319,7 +345,7 @@ class HelpersSpec extends TestKitBaseClass with AnyFunSuiteLike with ChannelStat assert(Closing.isClosingTypeAlreadyKnown( DATA_CLOSING( commitments = commitments - .modify(_.active.at(0).nextRemoteCommit_opt).setTo(Some(NextRemoteCommit(null, commitments.active.head.remoteCommit))) + .modify(_.active.at(0).nextRemoteCommit_opt).setTo(Some(commitments.active.head.remoteCommit)) .modify(_.remoteNextCommitInfo).setTo(Left(WaitForRev(7))), waitingSince = BlockHeight(0), finalScriptPubKey = Script.write(Script.pay2wpkh(randomKey().publicKey)), @@ -327,24 +353,27 @@ class HelpersSpec extends TestKitBaseClass with AnyFunSuiteLike with ChannelStat mutualClosePublished = tx1 :: Nil, localCommitPublished = Some(LocalCommitPublished( commitTx = tx2.tx, - claimMainDelayedOutputTx = None, - htlcTxs = Map.empty, - claimHtlcDelayedTxs = Nil, - claimAnchorTxs = Nil, + localOutput_opt = None, + anchorOutput_opt = None, + incomingHtlcs = Map.empty, + outgoingHtlcs = Map.empty, + htlcDelayedOutputs = Set.empty, irrevocablySpent = Map.empty )), remoteCommitPublished = Some(RemoteCommitPublished( commitTx = tx3.tx, - claimMainOutputTx = None, - claimHtlcTxs = Map.empty, - claimAnchorTxs = Nil, + localOutput_opt = None, + anchorOutput_opt = None, + incomingHtlcs = Map.empty, + outgoingHtlcs = Map.empty, irrevocablySpent = Map.empty )), nextRemoteCommitPublished = Some(RemoteCommitPublished( commitTx = tx4.tx, - claimMainOutputTx = Some(ClaimP2WPKHOutputTx(tx5.input, tx5.tx)), - claimHtlcTxs = Map.empty, - claimAnchorTxs = Nil, + localOutput_opt = Some(tx5.input.outPoint), + anchorOutput_opt = None, + incomingHtlcs = Map.empty, + outgoingHtlcs = Map.empty, irrevocablySpent = Map(tx4.input.outPoint -> tx4.tx) )), futureRemoteCommitPublished = None, @@ -364,9 +393,10 @@ class HelpersSpec extends TestKitBaseClass with AnyFunSuiteLike with ChannelStat nextRemoteCommitPublished = None, futureRemoteCommitPublished = Some(RemoteCommitPublished( commitTx = tx4.tx, - claimMainOutputTx = Some(ClaimRemoteDelayedOutputTx(tx5.input, tx5.tx)), - claimHtlcTxs = Map.empty, - claimAnchorTxs = Nil, + localOutput_opt = Some(tx5.input.outPoint), + anchorOutput_opt = None, + incomingHtlcs = Map.empty, + outgoingHtlcs = Map.empty, irrevocablySpent = Map.empty )), revokedCommitPublished = Nil) @@ -385,9 +415,10 @@ class HelpersSpec extends TestKitBaseClass with AnyFunSuiteLike with ChannelStat nextRemoteCommitPublished = None, futureRemoteCommitPublished = Some(RemoteCommitPublished( commitTx = tx4.tx, - claimMainOutputTx = Some(ClaimP2WPKHOutputTx(tx5.input, tx5.tx)), - claimHtlcTxs = Map.empty, - claimAnchorTxs = Nil, + localOutput_opt = Some(tx5.input.outPoint), + anchorOutput_opt = None, + incomingHtlcs = Map.empty, + outgoingHtlcs = Map.empty, irrevocablySpent = Map(tx4.input.outPoint -> tx4.tx) )), revokedCommitPublished = Nil) @@ -403,10 +434,11 @@ class HelpersSpec extends TestKitBaseClass with AnyFunSuiteLike with ChannelStat mutualClosePublished = Nil, localCommitPublished = Some(LocalCommitPublished( commitTx = tx1.tx, - claimMainDelayedOutputTx = None, - htlcTxs = Map.empty, - claimHtlcDelayedTxs = Nil, - claimAnchorTxs = Nil, + localOutput_opt = None, + anchorOutput_opt = None, + incomingHtlcs = Map.empty, + outgoingHtlcs = Map.empty, + htlcDelayedOutputs = Set.empty, irrevocablySpent = Map.empty )), remoteCommitPublished = None, @@ -415,26 +447,26 @@ class HelpersSpec extends TestKitBaseClass with AnyFunSuiteLike with ChannelStat revokedCommitPublished = RevokedCommitPublished( commitTx = tx2.tx, - claimMainOutputTx = Some(ClaimP2WPKHOutputTx(tx3.input, tx3.tx)), - mainPenaltyTx = None, - htlcPenaltyTxs = Nil, - claimHtlcDelayedPenaltyTxs = Nil, + localOutput_opt = Some(tx3.input.outPoint), + remoteOutput_opt = None, + htlcOutputs = Set.empty, + htlcDelayedOutputs = Set.empty, irrevocablySpent = Map.empty ) :: RevokedCommitPublished( commitTx = tx4.tx, - claimMainOutputTx = Some(ClaimP2WPKHOutputTx(tx5.input, tx5.tx)), - mainPenaltyTx = None, - htlcPenaltyTxs = Nil, - claimHtlcDelayedPenaltyTxs = Nil, + localOutput_opt = Some(tx5.input.outPoint), + remoteOutput_opt = None, + htlcOutputs = Set.empty, + htlcDelayedOutputs = Set.empty, irrevocablySpent = Map.empty ) :: RevokedCommitPublished( commitTx = tx6.tx, - claimMainOutputTx = None, - mainPenaltyTx = None, - htlcPenaltyTxs = Nil, - claimHtlcDelayedPenaltyTxs = Nil, + localOutput_opt = None, + remoteOutput_opt = None, + htlcOutputs = Set.empty, + htlcDelayedOutputs = Set.empty, irrevocablySpent = Map.empty ) :: Nil ) @@ -450,10 +482,11 @@ class HelpersSpec extends TestKitBaseClass with AnyFunSuiteLike with ChannelStat mutualClosePublished = Nil, localCommitPublished = Some(LocalCommitPublished( commitTx = tx1.tx, - claimMainDelayedOutputTx = None, - htlcTxs = Map.empty, - claimHtlcDelayedTxs = Nil, - claimAnchorTxs = Nil, + localOutput_opt = None, + anchorOutput_opt = None, + incomingHtlcs = Map.empty, + outgoingHtlcs = Map.empty, + htlcDelayedOutputs = Set.empty, irrevocablySpent = Map.empty )), remoteCommitPublished = None, @@ -462,26 +495,26 @@ class HelpersSpec extends TestKitBaseClass with AnyFunSuiteLike with ChannelStat revokedCommitPublished = RevokedCommitPublished( commitTx = tx2.tx, - claimMainOutputTx = Some(ClaimP2WPKHOutputTx(tx3.input, tx3.tx)), - mainPenaltyTx = None, - htlcPenaltyTxs = Nil, - claimHtlcDelayedPenaltyTxs = Nil, + localOutput_opt = Some(tx3.input.outPoint), + remoteOutput_opt = None, + htlcOutputs = Set.empty, + htlcDelayedOutputs = Set.empty, irrevocablySpent = Map.empty ) :: RevokedCommitPublished( commitTx = tx4.tx, - claimMainOutputTx = Some(ClaimP2WPKHOutputTx(tx5.input, tx5.tx)), - mainPenaltyTx = None, - htlcPenaltyTxs = Nil, - claimHtlcDelayedPenaltyTxs = Nil, + localOutput_opt = Some(tx5.input.outPoint), + remoteOutput_opt = None, + htlcOutputs = Set.empty, + htlcDelayedOutputs = Set.empty, irrevocablySpent = Map(tx4.input.outPoint -> tx4.tx) ) :: RevokedCommitPublished( commitTx = tx6.tx, - claimMainOutputTx = None, - mainPenaltyTx = None, - htlcPenaltyTxs = Nil, - claimHtlcDelayedPenaltyTxs = Nil, + localOutput_opt = None, + remoteOutput_opt = None, + htlcOutputs = Set.empty, + htlcDelayedOutputs = Set.empty, irrevocablySpent = Map.empty ) :: Nil ) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/InteractiveTxBuilderSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/InteractiveTxBuilderSpec.scala index 446d5a0a51..bcddf81d43 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/InteractiveTxBuilderSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/InteractiveTxBuilderSpec.scala @@ -31,13 +31,15 @@ import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinCoreClient.{MempoolTx, Utx import fr.acinq.eclair.blockchain.bitcoind.rpc.{BitcoinCoreClient, BitcoinJsonRPCClient} import fr.acinq.eclair.blockchain.fee.{FeeratePerByte, FeeratePerKw} import fr.acinq.eclair.blockchain.{OnChainWallet, SingleKeyOnChainWallet} +import fr.acinq.eclair.channel.ChannelSpendSignature.{IndividualSignature, PartialSignatureWithNonce} import fr.acinq.eclair.channel.fund.InteractiveTxBuilder._ import fr.acinq.eclair.channel.fund.{InteractiveTxBuilder, InteractiveTxSigningSession} +import fr.acinq.eclair.crypto.keymanager.ChannelKeys import fr.acinq.eclair.io.OpenChannelInterceptor.makeChannelParams -import fr.acinq.eclair.transactions.Transactions.InputInfo +import fr.acinq.eclair.transactions.Transactions.{CommitmentFormat, InputInfo, PhoenixSimpleTaprootChannelCommitmentFormat, UnsafeLegacyAnchorOutputsCommitmentFormat} import fr.acinq.eclair.transactions.{Scripts, Transactions} import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{Feature, FeatureSupport, Features, InitFeature, MilliSatoshiLong, NodeParams, TestConstants, TestKitBaseClass, ToMilliSatoshiConversion, UInt64, randomBytes32, randomKey} +import fr.acinq.eclair.{Feature, FeatureSupport, Features, MilliSatoshiLong, NodeParams, TestConstants, TestKitBaseClass, ToMilliSatoshiConversion, UInt64, randomBytes32, randomKey} import org.scalatest.BeforeAndAfterAll import org.scalatest.funsuite.AnyFunSuiteLike import scodec.bits.{ByteVector, HexStringSyntax} @@ -66,7 +68,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit val tx = Transaction(version = 2, Nil, TxOut(amount, addressToPublicKeyScript(Block.RegtestGenesisBlock.hash, walletAddress).toOption.get) :: Nil, lockTime = 0) val client = makeBitcoinCoreClient() val f = for { - funded <- client.fundTransaction(tx, FeeratePerKw(FeeratePerByte(10.sat)), replaceable = true) + funded <- client.fundTransaction(tx, FeeratePerByte(10.sat).perKw) signed <- client.signPsbt(new Psbt(funded.tx), funded.tx.txIn.indices, Nil) txid <- client.publishTransaction(signed.finalTx_opt.toOption.get) } yield txid @@ -75,7 +77,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit } private def createInput(channelId: ByteVector32, serialId: UInt64, amount: Satoshi): TxAddInput = { - val changeScript = Script.write(Script.pay2wpkh(randomKey().publicKey)) + val changeScript = Script.write(Script.pay2tr(randomKey().publicKey.xOnly)) val previousTx = Transaction(2, Nil, Seq(TxOut(amount, changeScript), TxOut(amount, changeScript), TxOut(amount, changeScript)), 0) TxAddInput(channelId, serialId, Some(previousTx), 1, 0) } @@ -85,95 +87,99 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit case None => input.sharedInput_opt.get } - private def sharedInputs(commitmentA: Commitment, commitmentB: Commitment): (SharedFundingInput, SharedFundingInput) = { - val sharedInputA = Multisig2of2Input(commitmentA) - val sharedInputB = Multisig2of2Input(commitmentB) - (sharedInputA, sharedInputB) - } - case class FixtureParams(fundingParamsA: InteractiveTxParams, nodeParamsA: NodeParams, channelParamsA: ChannelParams, + commitParamsA: CommitParams, fundingParamsB: InteractiveTxParams, nodeParamsB: NodeParams, channelParamsB: ChannelParams, - channelFeatures: ChannelFeatures) { + commitParamsB: CommitParams) { val channelId: ByteVector32 = fundingParamsA.channelId val commitFeerate: FeeratePerKw = TestConstants.anchorOutputsFeeratePerKw + val channelKeysA: ChannelKeys = nodeParamsA.channelKeyManager.channelKeys(channelParamsA.channelConfig, channelParamsA.localParams.fundingKeyPath) + val channelKeysB: ChannelKeys = nodeParamsB.channelKeyManager.channelKeys(channelParamsB.channelConfig, channelParamsB.localParams.fundingKeyPath) - private val firstPerCommitmentPointA = nodeParamsA.channelKeyManager.commitmentPoint(nodeParamsA.channelKeyManager.keyPath(channelParamsA.localParams, ChannelConfig.standard), 0) - private val firstPerCommitmentPointB = nodeParamsB.channelKeyManager.commitmentPoint(nodeParamsB.channelKeyManager.keyPath(channelParamsB.localParams, ChannelConfig.standard), 0) - val fundingPubkeyScript: ByteVector = Script.write(Script.pay2wsh(Scripts.multiSig2of2(fundingParamsB.remoteFundingPubKey, fundingParamsA.remoteFundingPubKey))) + private val firstPerCommitmentPointA = channelKeysA.commitmentPoint(0) + private val firstPerCommitmentPointB = channelKeysB.commitmentPoint(0) + val fundingPubkeyScript: ByteVector = Transactions.makeFundingScript(fundingParamsB.remoteFundingPubKey, fundingParamsA.remoteFundingPubKey, fundingParamsA.commitmentFormat).pubkeyScript + + def sharedInputs(commitmentA: Commitment, commitmentB: Commitment): (SharedFundingInput, SharedFundingInput) = { + val sharedInputA = SharedFundingInput(channelKeysA, commitmentA) + val sharedInputB = SharedFundingInput(channelKeysB, commitmentB) + (sharedInputA, sharedInputB) + } def dummySharedInputB(amount: Satoshi): SharedFundingInput = { - val inputInfo = InputInfo(OutPoint(randomTxId(), 3), TxOut(amount, fundingPubkeyScript), Nil) + val inputInfo = InputInfo(OutPoint(randomTxId(), 3), TxOut(amount, fundingPubkeyScript)) val fundingTxIndex = fundingParamsA.sharedInput_opt match { - case Some(input: Multisig2of2Input) => input.fundingTxIndex + 1 + case Some(input) => input.fundingTxIndex + 1 case _ => 0 } - Multisig2of2Input(inputInfo, fundingTxIndex, fundingParamsA.remoteFundingPubKey) + SharedFundingInput(inputInfo, fundingTxIndex, fundingParamsA.remoteFundingPubKey, fundingParamsA.commitmentFormat) } - def createSpliceFixtureParams(fundingTxIndex: Long, fundingAmountA: Satoshi, fundingAmountB: Satoshi, targetFeerate: FeeratePerKw, dustLimit: Satoshi, lockTime: Long, sharedInputA: SharedFundingInput, sharedInputB: SharedFundingInput, spliceOutputsA: List[TxOut] = Nil, spliceOutputsB: List[TxOut] = Nil, requireConfirmedInputs: RequireConfirmedInputs = RequireConfirmedInputs(forLocal = false, forRemote = false)): FixtureParams = { - val fundingPubKeyA = nodeParamsA.channelKeyManager.fundingPublicKey(channelParamsA.localParams.fundingKeyPath, fundingTxIndex).publicKey - val fundingPubKeyB = nodeParamsB.channelKeyManager.fundingPublicKey(channelParamsB.localParams.fundingKeyPath, fundingTxIndex).publicKey - val fundingParamsA = InteractiveTxParams(channelId, isInitiator = true, fundingAmountA, fundingAmountB, Some(sharedInputA), fundingPubKeyB, spliceOutputsA, lockTime, dustLimit, targetFeerate, requireConfirmedInputs) - val fundingParamsB = InteractiveTxParams(channelId, isInitiator = false, fundingAmountB, fundingAmountA, Some(sharedInputB), fundingPubKeyA, spliceOutputsB, lockTime, dustLimit, targetFeerate, requireConfirmedInputs) - copy(fundingParamsA = fundingParamsA, fundingParamsB = fundingParamsB) + def createSpliceFixtureParams(fundingTxIndex: Long, fundingAmountA: Satoshi, fundingAmountB: Satoshi, targetFeerate: FeeratePerKw, dustLimit: Satoshi, lockTime: Long, sharedInputA: SharedFundingInput, sharedInputB: SharedFundingInput, nextCommitmentFormat_opt: Option[CommitmentFormat] = None, spliceOutputsA: List[TxOut] = Nil, spliceOutputsB: List[TxOut] = Nil, requireConfirmedInputs: RequireConfirmedInputs = RequireConfirmedInputs(forLocal = false, forRemote = false)): FixtureParams = { + val fundingPubKeyA = channelKeysA.fundingKey(fundingTxIndex).publicKey + val fundingPubKeyB = channelKeysB.fundingKey(fundingTxIndex).publicKey + val nextCommitmentFormat = nextCommitmentFormat_opt.getOrElse(fundingParamsA.commitmentFormat) + val fundingParamsA1 = InteractiveTxParams(channelId, isInitiator = true, fundingAmountA, fundingAmountB, Some(sharedInputA), fundingPubKeyB, spliceOutputsA, nextCommitmentFormat, lockTime, dustLimit, targetFeerate, requireConfirmedInputs) + val fundingParamsB1 = InteractiveTxParams(channelId, isInitiator = false, fundingAmountB, fundingAmountA, Some(sharedInputB), fundingPubKeyA, spliceOutputsB, nextCommitmentFormat, lockTime, dustLimit, targetFeerate, requireConfirmedInputs) + copy(fundingParamsA = fundingParamsA1, fundingParamsB = fundingParamsB1) } def spawnTxBuilderAlice(wallet: OnChainWallet, fundingParams: InteractiveTxParams = fundingParamsA, liquidityPurchase_opt: Option[LiquidityAds.Purchase] = None): ActorRef[InteractiveTxBuilder.Command] = system.spawnAnonymous(InteractiveTxBuilder( ByteVector32.Zeroes, - nodeParamsA, fundingParams, channelParamsA, + nodeParamsA, fundingParams, channelParamsA, commitParamsA, commitParamsB, channelKeysA, FundingTx(commitFeerate, firstPerCommitmentPointB, feeBudget_opt = None), 0 msat, 0 msat, liquidityPurchase_opt, wallet)) def spawnTxBuilderRbfAlice(fundingParams: InteractiveTxParams, commitment: Commitment, previousTransactions: Seq[InteractiveTxBuilder.SignedSharedTransaction], wallet: OnChainWallet): ActorRef[InteractiveTxBuilder.Command] = system.spawnAnonymous(InteractiveTxBuilder( ByteVector32.Zeroes, - nodeParamsA, fundingParams, channelParamsA, + nodeParamsA, fundingParams, channelParamsA, commitParamsA, commitParamsB, channelKeysA, FundingTxRbf(commitment, previousTransactions, feeBudget_opt = None), 0 msat, 0 msat, None, wallet)) def spawnTxBuilderSpliceAlice(fundingParams: InteractiveTxParams, commitment: Commitment, wallet: OnChainWallet, liquidityPurchase_opt: Option[LiquidityAds.Purchase] = None): ActorRef[InteractiveTxBuilder.Command] = system.spawnAnonymous(InteractiveTxBuilder( ByteVector32.Zeroes, - nodeParamsA, fundingParams, channelParamsA, + nodeParamsA, fundingParams, channelParamsA, commitParamsA, commitParamsB, channelKeysA, SpliceTx(commitment, CommitmentChanges.init()), 0 msat, 0 msat, liquidityPurchase_opt, wallet)) def spawnTxBuilderSpliceRbfAlice(fundingParams: InteractiveTxParams, parentCommitment: Commitment, latestFundingTx: LocalFundingStatus.DualFundedUnconfirmedFundingTx, previousTransactions: Seq[InteractiveTxBuilder.SignedSharedTransaction], wallet: OnChainWallet): ActorRef[InteractiveTxBuilder.Command] = system.spawnAnonymous(InteractiveTxBuilder( ByteVector32.Zeroes, - nodeParamsA, fundingParams, channelParamsA, + nodeParamsA, fundingParams, channelParamsA, commitParamsA, commitParamsB, channelKeysA, SpliceTxRbf(parentCommitment, CommitmentChanges.init(), latestFundingTx, previousTransactions, feeBudget_opt = None), 0 msat, 0 msat, None, wallet)) def spawnTxBuilderBob(wallet: OnChainWallet, fundingParams: InteractiveTxParams = fundingParamsB, liquidityPurchase_opt: Option[LiquidityAds.Purchase] = None): ActorRef[InteractiveTxBuilder.Command] = system.spawnAnonymous(InteractiveTxBuilder( ByteVector32.Zeroes, - nodeParamsB, fundingParams, channelParamsB, + nodeParamsB, fundingParams, channelParamsB, commitParamsB, commitParamsA, channelKeysB, FundingTx(commitFeerate, firstPerCommitmentPointA, feeBudget_opt = None), 0 msat, 0 msat, liquidityPurchase_opt, wallet)) def spawnTxBuilderRbfBob(fundingParams: InteractiveTxParams, commitment: Commitment, previousTransactions: Seq[InteractiveTxBuilder.SignedSharedTransaction], wallet: OnChainWallet): ActorRef[InteractiveTxBuilder.Command] = system.spawnAnonymous(InteractiveTxBuilder( ByteVector32.Zeroes, - nodeParamsB, fundingParams, channelParamsB, + nodeParamsB, fundingParams, channelParamsB, commitParamsB, commitParamsA, channelKeysB, FundingTxRbf(commitment, previousTransactions, feeBudget_opt = None), 0 msat, 0 msat, None, wallet)) def spawnTxBuilderSpliceBob(fundingParams: InteractiveTxParams, commitment: Commitment, wallet: OnChainWallet, liquidityPurchase_opt: Option[LiquidityAds.Purchase] = None): ActorRef[InteractiveTxBuilder.Command] = system.spawnAnonymous(InteractiveTxBuilder( ByteVector32.Zeroes, - nodeParamsB, fundingParams, channelParamsB, + nodeParamsB, fundingParams, channelParamsB, commitParamsB, commitParamsA, channelKeysB, SpliceTx(commitment, CommitmentChanges.init()), 0 msat, 0 msat, liquidityPurchase_opt, wallet)) def spawnTxBuilderSpliceRbfBob(fundingParams: InteractiveTxParams, parentCommitment: Commitment, latestFundingTx: LocalFundingStatus.DualFundedUnconfirmedFundingTx, previousTransactions: Seq[InteractiveTxBuilder.SignedSharedTransaction], wallet: OnChainWallet): ActorRef[InteractiveTxBuilder.Command] = system.spawnAnonymous(InteractiveTxBuilder( ByteVector32.Zeroes, - nodeParamsB, fundingParams, channelParamsB, + nodeParamsB, fundingParams, channelParamsB, commitParamsB, commitParamsA, channelKeysB, SpliceTxRbf(parentCommitment, CommitmentChanges.init(), latestFundingTx, previousTransactions, feeBudget_opt = None), 0 msat, 0 msat, None, wallet)) @@ -181,65 +187,67 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit def exchangeSigsAliceFirst(fundingParams: InteractiveTxParams, successA: InteractiveTxBuilder.Succeeded, successB: InteractiveTxBuilder.Succeeded): (FullySignedSharedTransaction, Commitment, FullySignedSharedTransaction, Commitment) = { implicit val log: akka.event.LoggingAdapter = akka.event.NoLogging // Alice --- commit_sig --> Bob - val Right(signingSessionB2: InteractiveTxSigningSession.WaitingForSigs) = successB.signingSession.receiveCommitSig(nodeParamsB, channelParamsB, successA.commitSig) + val Right(signingSessionB2: InteractiveTxSigningSession.WaitingForSigs) = successB.signingSession.receiveCommitSig(channelParamsB, channelKeysB, successA.commitSig, nodeParamsB.currentBlockHeight) // Alice <-- commit_sig --- Bob - val Right(sigsA: InteractiveTxSigningSession.SendingSigs) = successA.signingSession.receiveCommitSig(nodeParamsA, channelParamsA, successB.commitSig) + val Right(sigsA: InteractiveTxSigningSession.SendingSigs) = successA.signingSession.receiveCommitSig(channelParamsA, channelKeysA, successB.commitSig, nodeParamsA.currentBlockHeight) assert(sigsA.fundingTx.sharedTx.isInstanceOf[PartiallySignedSharedTransaction]) // Alice --- tx_signatures --> Bob - val Right(sigsB) = signingSessionB2.receiveTxSigs(nodeParamsB, channelParamsB, sigsA.localSigs) + val Right(sigsB) = signingSessionB2.receiveTxSigs(channelKeysB, sigsA.localSigs, nodeParamsB.currentBlockHeight) assert(sigsB.fundingTx.sharedTx.isInstanceOf[FullySignedSharedTransaction]) val txB = sigsB.fundingTx.sharedTx.asInstanceOf[FullySignedSharedTransaction] // Alice <-- tx_signatures --- Bob - val Right(txA) = InteractiveTxSigningSession.addRemoteSigs(nodeParamsA.channelKeyManager, channelParamsA, fundingParams, sigsA.fundingTx.sharedTx.asInstanceOf[PartiallySignedSharedTransaction], sigsB.localSigs) + val Right(txA) = InteractiveTxSigningSession.addRemoteSigs(channelKeysA, fundingParams, sigsA.fundingTx.sharedTx.asInstanceOf[PartiallySignedSharedTransaction], sigsB.localSigs) (txA, sigsA.commitment, txB, sigsB.commitment) } def exchangeSigsBobFirst(fundingParams: InteractiveTxParams, successA: InteractiveTxBuilder.Succeeded, successB: InteractiveTxBuilder.Succeeded): (FullySignedSharedTransaction, Commitment, FullySignedSharedTransaction, Commitment) = { implicit val log: akka.event.LoggingAdapter = akka.event.NoLogging // Alice <-- commit_sig --- Bob - val Right(signingSessionA2: InteractiveTxSigningSession.WaitingForSigs) = successA.signingSession.receiveCommitSig(nodeParamsA, channelParamsA, successB.commitSig) + val Right(signingSessionA2: InteractiveTxSigningSession.WaitingForSigs) = successA.signingSession.receiveCommitSig(channelParamsA, channelKeysA, successB.commitSig, nodeParamsA.currentBlockHeight) // Alice --- commit_sig --> Bob - val Right(sigsB: InteractiveTxSigningSession.SendingSigs) = successB.signingSession.receiveCommitSig(nodeParamsB, channelParamsB, successA.commitSig) + val Right(sigsB: InteractiveTxSigningSession.SendingSigs) = successB.signingSession.receiveCommitSig(channelParamsB, channelKeysB, successA.commitSig, nodeParamsB.currentBlockHeight) assert(sigsB.fundingTx.sharedTx.isInstanceOf[PartiallySignedSharedTransaction]) // Alice <-- tx_signatures --- Bob - val Right(sigsA) = signingSessionA2.receiveTxSigs(nodeParamsA, channelParamsA, sigsB.localSigs) + val Right(sigsA) = signingSessionA2.receiveTxSigs(channelKeysA, sigsB.localSigs, nodeParamsA.currentBlockHeight) assert(sigsA.fundingTx.sharedTx.isInstanceOf[FullySignedSharedTransaction]) val txA = sigsA.fundingTx.sharedTx.asInstanceOf[FullySignedSharedTransaction] // Alice --- tx_signatures --> Bob - val Right(txB) = InteractiveTxSigningSession.addRemoteSigs(nodeParamsB.channelKeyManager, channelParamsB, fundingParams, sigsB.fundingTx.sharedTx.asInstanceOf[PartiallySignedSharedTransaction], sigsA.localSigs) + val Right(txB) = InteractiveTxSigningSession.addRemoteSigs(channelKeysB, fundingParams, sigsB.fundingTx.sharedTx.asInstanceOf[PartiallySignedSharedTransaction], sigsA.localSigs) (txA, sigsA.commitment, txB, sigsB.commitment) } } - private def createFixtureParams(fundingAmountA: Satoshi, fundingAmountB: Satoshi, targetFeerate: FeeratePerKw, dustLimit: Satoshi, lockTime: Long, requireConfirmedInputs: RequireConfirmedInputs = RequireConfirmedInputs(forLocal = false, forRemote = false), nonInitiatorPaysCommitTxFees: Boolean = false): FixtureParams = { - val channelFeatures = ChannelFeatures(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(), Features[InitFeature](Features.DualFunding -> FeatureSupport.Optional), Features[InitFeature](Features.DualFunding -> FeatureSupport.Optional), announceChannel = true) - val Seq(nodeParamsA, nodeParamsB) = Seq(TestConstants.Alice.nodeParams, TestConstants.Bob.nodeParams).map(_.copy(features = Features(channelFeatures.features.map(f => f -> FeatureSupport.Optional).toMap[Feature, FeatureSupport]))) - val localParamsA = makeChannelParams(nodeParamsA, nodeParamsA.features.initFeatures(), None, None, isChannelOpener = true, paysCommitTxFees = !nonInitiatorPaysCommitTxFees, dualFunded = true, fundingAmountA, unlimitedMaxHtlcValueInFlight = false) - val localParamsB = makeChannelParams(nodeParamsB, nodeParamsB.features.initFeatures(), None, None, isChannelOpener = false, paysCommitTxFees = nonInitiatorPaysCommitTxFees, dualFunded = true, fundingAmountB, unlimitedMaxHtlcValueInFlight = false) - - val Seq(remoteParamsA, remoteParamsB) = Seq((nodeParamsA, localParamsA), (nodeParamsB, localParamsB)).map { - case (nodeParams, localParams) => - val channelKeyPath = nodeParams.channelKeyManager.keyPath(localParams, ChannelConfig.standard) - RemoteParams( + private def createFixtureParams(channelType: SupportedChannelType, fundingAmountA: Satoshi, fundingAmountB: Satoshi, targetFeerate: FeeratePerKw, dustLimit: Satoshi, lockTime: Long, requireConfirmedInputs: RequireConfirmedInputs = RequireConfirmedInputs(forLocal = false, forRemote = false), nonInitiatorPaysCommitTxFees: Boolean = false): FixtureParams = { + val Seq(nodeParamsA, nodeParamsB) = Seq(TestConstants.Alice.nodeParams, TestConstants.Bob.nodeParams).map(_.copy(features = Features(channelType.features.map(f => f -> FeatureSupport.Optional).toMap[Feature, FeatureSupport]))) + val localChannelParamsA = makeChannelParams(nodeParamsA, nodeParamsA.features.initFeatures(), None, isChannelOpener = true, paysCommitTxFees = !nonInitiatorPaysCommitTxFees, dualFunded = true, fundingAmountA) + val commitParamsA = CommitParams(nodeParamsA.channelConf.dustLimit, nodeParamsA.channelConf.htlcMinimum, nodeParamsA.channelConf.maxHtlcValueInFlight(fundingAmountA + fundingAmountB, unlimited = false), nodeParamsA.channelConf.maxAcceptedHtlcs, nodeParamsB.channelConf.toRemoteDelay) + val localChannelParamsB = makeChannelParams(nodeParamsB, nodeParamsB.features.initFeatures(), None, isChannelOpener = false, paysCommitTxFees = nonInitiatorPaysCommitTxFees, dualFunded = true, fundingAmountB) + val commitParamsB = CommitParams(nodeParamsB.channelConf.dustLimit, nodeParamsB.channelConf.htlcMinimum, nodeParamsB.channelConf.maxHtlcValueInFlight(fundingAmountA + fundingAmountB, unlimited = false), nodeParamsB.channelConf.maxAcceptedHtlcs, nodeParamsA.channelConf.toRemoteDelay) + val channelKeysA = nodeParamsA.channelKeyManager.channelKeys(ChannelConfig.standard, localChannelParamsA.fundingKeyPath) + val channelKeysB = nodeParamsB.channelKeyManager.channelKeys(ChannelConfig.standard, localChannelParamsB.fundingKeyPath) + + val Seq(remoteChannelParamsA, remoteChannelParamsB) = Seq((nodeParamsA, localChannelParamsA, channelKeysA), (nodeParamsB, localChannelParamsB, channelKeysB)).map { + case (nodeParams, localParams, channelKeys) => + RemoteChannelParams( nodeParams.nodeId, - localParams.dustLimit, UInt64(localParams.maxHtlcValueInFlightMsat.toLong), None, localParams.htlcMinimum, localParams.toSelfDelay, localParams.maxAcceptedHtlcs, - nodeParams.channelKeyManager.revocationPoint(channelKeyPath).publicKey, - nodeParams.channelKeyManager.paymentPoint(channelKeyPath).publicKey, - nodeParams.channelKeyManager.delayedPaymentPoint(channelKeyPath).publicKey, - nodeParams.channelKeyManager.htlcPoint(channelKeyPath).publicKey, + None, + channelKeys.revocationBasePoint, + channelKeys.paymentBasePoint, + channelKeys.delayedPaymentBasePoint, + channelKeys.htlcBasePoint, localParams.initFeatures, None) } val channelId = randomBytes32() - val fundingPubKeyA = nodeParamsA.channelKeyManager.fundingPublicKey(localParamsA.fundingKeyPath, fundingTxIndex = 0).publicKey - val fundingPubKeyB = nodeParamsB.channelKeyManager.fundingPublicKey(localParamsB.fundingKeyPath, fundingTxIndex = 0).publicKey - val fundingParamsA = InteractiveTxParams(channelId, isInitiator = true, fundingAmountA, fundingAmountB, None, fundingPubKeyB, Nil, lockTime, dustLimit, targetFeerate, requireConfirmedInputs) - val fundingParamsB = InteractiveTxParams(channelId, isInitiator = false, fundingAmountB, fundingAmountA, None, fundingPubKeyA, Nil, lockTime, dustLimit, targetFeerate, requireConfirmedInputs) - val channelParamsA = ChannelParams(channelId, ChannelConfig.standard, channelFeatures, localParamsA, remoteParamsB, ChannelFlags(announceChannel = true)) - val channelParamsB = ChannelParams(channelId, ChannelConfig.standard, channelFeatures, localParamsB, remoteParamsA, ChannelFlags(announceChannel = true)) - - FixtureParams(fundingParamsA, nodeParamsA, channelParamsA, fundingParamsB, nodeParamsB, channelParamsB, channelFeatures) + val fundingPubKeyA = channelKeysA.fundingKey(fundingTxIndex = 0).publicKey + val fundingPubKeyB = channelKeysB.fundingKey(fundingTxIndex = 0).publicKey + val fundingParamsA = InteractiveTxParams(channelId, isInitiator = true, fundingAmountA, fundingAmountB, None, fundingPubKeyB, Nil, channelType.commitmentFormat, lockTime, dustLimit, targetFeerate, requireConfirmedInputs) + val fundingParamsB = InteractiveTxParams(channelId, isInitiator = false, fundingAmountB, fundingAmountA, None, fundingPubKeyA, Nil, channelType.commitmentFormat, lockTime, dustLimit, targetFeerate, requireConfirmedInputs) + val channelParamsA = ChannelParams(channelId, ChannelConfig.standard, ChannelFeatures(Features.DualFunding), localChannelParamsA, remoteChannelParamsB, ChannelFlags(announceChannel = true)) + val channelParamsB = ChannelParams(channelId, ChannelConfig.standard, ChannelFeatures(Features.DualFunding), localChannelParamsB, remoteChannelParamsA, ChannelFlags(announceChannel = true)) + + FixtureParams(fundingParamsA, nodeParamsA, channelParamsA, commitParamsA, fundingParamsB, nodeParamsB, channelParamsB, commitParamsB) } case class Fixture(alice: ActorRef[InteractiveTxBuilder.Command], @@ -276,7 +284,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit } } - private def withFixture(fundingAmountA: Satoshi, utxosA: Seq[Satoshi], fundingAmountB: Satoshi, utxosB: Seq[Satoshi], targetFeerate: FeeratePerKw, dustLimit: Satoshi, lockTime: Long, requireConfirmedInputs: RequireConfirmedInputs, liquidityPurchase_opt: Option[LiquidityAds.Purchase] = None)(testFun: Fixture => Any): Unit = { + private def withFixture(channelType: SupportedChannelType, fundingAmountA: Satoshi, utxosA: Seq[Satoshi], fundingAmountB: Satoshi, utxosB: Seq[Satoshi], targetFeerate: FeeratePerKw, dustLimit: Satoshi, lockTime: Long, requireConfirmedInputs: RequireConfirmedInputs, liquidityPurchase_opt: Option[LiquidityAds.Purchase] = None)(testFun: Fixture => Any): Unit = { // Initialize wallets with a few confirmed utxos. val probe = TestProbe() val rpcClientA = createWallet(UUID.randomUUID().toString) @@ -287,7 +295,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit utxosB.foreach(amount => addUtxo(walletB, amount, probe)) generateBlocks(1) - val fixtureParams = createFixtureParams(fundingAmountA, fundingAmountB, targetFeerate, dustLimit, lockTime, requireConfirmedInputs, nonInitiatorPaysCommitTxFees = liquidityPurchase_opt.nonEmpty) + val fixtureParams = createFixtureParams(channelType, fundingAmountA, fundingAmountB, targetFeerate, dustLimit, lockTime, requireConfirmedInputs, nonInitiatorPaysCommitTxFees = liquidityPurchase_opt.nonEmpty) val alice = fixtureParams.spawnTxBuilderAlice(walletA, liquidityPurchase_opt = liquidityPurchase_opt) val bob = fixtureParams.spawnTxBuilderBob(walletB, liquidityPurchase_opt = liquidityPurchase_opt) testFun(Fixture(alice, bob, fixtureParams, walletA, rpcClientA, walletB, rpcClientB, TestProbe(), TestProbe())) @@ -299,7 +307,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit val utxosA = Seq(50_000 sat, 35_000 sat, 60_000 sat) val fundingB = 40_000 sat val utxosB = Seq(100_000 sat) - withFixture(fundingA, utxosA, fundingB, utxosB, targetFeerate, 660 sat, 42, RequireConfirmedInputs(forLocal = false, forRemote = false)) { f => + withFixture(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(), fundingA, utxosA, fundingB, utxosB, targetFeerate, 660 sat, 42, RequireConfirmedInputs(forLocal = false, forRemote = false)) { f => import f._ alice ! Start(alice2bob.ref) @@ -376,7 +384,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit val utxosA = Seq(50_000 sat) val fundingB = 50_000 sat val utxosB = Seq(80_000 sat) - withFixture(fundingA, utxosA, fundingB, utxosB, targetFeerate, 660 sat, 0, RequireConfirmedInputs(forLocal = true, forRemote = true)) { f => + withFixture(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(), fundingA, utxosA, fundingB, utxosB, targetFeerate, 660 sat, 0, RequireConfirmedInputs(forLocal = true, forRemote = true)) { f => import f._ alice ! Start(alice2bob.ref) @@ -432,7 +440,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit val utxosA = Seq(150_000 sat) val fundingB = 50_000 sat val utxosB = Seq(200_000 sat) - withFixture(fundingA, utxosA, fundingB, utxosB, targetFeerate, 660 sat, 42, RequireConfirmedInputs(forLocal = false, forRemote = false)) { f => + withFixture(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(), fundingA, utxosA, fundingB, utxosB, targetFeerate, 660 sat, 42, RequireConfirmedInputs(forLocal = false, forRemote = false)) { f => import f._ alice ! Start(alice2bob.ref) @@ -471,7 +479,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit val targetFeerate = FeeratePerKw(2500 sat) val fundingA = 150_000 sat val utxosA = Seq(80_000 sat, 120_000 sat) - withFixture(fundingA, utxosA, 0 sat, Nil, targetFeerate, 660 sat, 0, RequireConfirmedInputs(forLocal = false, forRemote = false)) { f => + withFixture(ChannelTypes.SimpleTaprootChannelsStaging(), fundingA, utxosA, 0 sat, Nil, targetFeerate, 660 sat, 0, RequireConfirmedInputs(forLocal = false, forRemote = false)) { f => import f._ alice ! Start(alice2bob.ref) @@ -484,17 +492,25 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit // Alice --- tx_add_input --> Bob fwd.forwardAlice2Bob[TxAddInput] // Alice <-- tx_complete --- Bob - fwd.forwardBob2Alice[TxComplete] + val txCompleteB1 = fwd.forwardBob2Alice[TxComplete] // Alice --- tx_add_output --> Bob val outputA1 = fwd.forwardAlice2Bob[TxAddOutput] // Alice <-- tx_complete --- Bob - fwd.forwardBob2Alice[TxComplete] + val txCompleteB2 = fwd.forwardBob2Alice[TxComplete] // Alice --- tx_add_output --> Bob val outputA2 = fwd.forwardAlice2Bob[TxAddOutput] // Alice <-- tx_complete --- Bob - fwd.forwardBob2Alice[TxComplete] + val txCompleteB3 = fwd.forwardBob2Alice[TxComplete] // Alice --- tx_complete --> Bob - fwd.forwardAlice2Bob[TxComplete] + val txCompleteA = fwd.forwardAlice2Bob[TxComplete] + assert(txCompleteA.commitNonces_opt.nonEmpty) + assert(txCompleteA.fundingNonce_opt.isEmpty) + Seq(txCompleteB1, txCompleteB2, txCompleteB3).foreach(txCompleteB => { + assert(txCompleteB.commitNonces_opt.nonEmpty) + assert(txCompleteB.fundingNonce_opt.isEmpty) + }) + // Nonces change every time the shared transaction changes. + assert(Seq(txCompleteB1, txCompleteB2, txCompleteB3).flatMap(_.commitNonces_opt).flatMap(n => Seq(n.commitNonce, n.nextCommitNonce)).toSet.size == 6) // Alice is responsible for adding the shared output. assert(aliceParams.fundingAmount == fundingA) @@ -503,8 +519,12 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit // Bob sends signatures first as he did not contribute at all. val successA = alice2bob.expectMsgType[Succeeded] + assert(successA.commitSig.sigOrPartialSig.isInstanceOf[PartialSignatureWithNonce]) val successB = bob2alice.expectMsgType[Succeeded] + assert(successB.commitSig.sigOrPartialSig.isInstanceOf[PartialSignatureWithNonce]) val (txA, _, txB, _) = fixtureParams.exchangeSigsBobFirst(bobParams, successA, successB) + assert(successA.nextRemoteCommitNonce_opt.contains((txA.txId, txCompleteB3.commitNonces_opt.get.nextCommitNonce))) + assert(successB.nextRemoteCommitNonce_opt.contains((txB.txId, txCompleteA.commitNonces_opt.get.nextCommitNonce))) // The resulting transaction is valid and has the right feerate. assert(txA.txId == txB.txId) assert(txA.signedTx.lockTime == aliceParams.lockTime) @@ -523,7 +543,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit } test("initiator uses unconfirmed inputs") { - withFixture(100_000 sat, Seq(170_000 sat), 0 sat, Nil, FeeratePerKw(2500 sat), 660 sat, 0, RequireConfirmedInputs(forLocal = false, forRemote = false)) { f => + withFixture(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(), 100_000 sat, Seq(170_000 sat), 0 sat, Nil, FeeratePerKw(2500 sat), 660 sat, 0, RequireConfirmedInputs(forLocal = false, forRemote = false)) { f => import f._ // Alice's inputs are all unconfirmed: we spent her only confirmed input to create two unconfirmed outputs. @@ -571,7 +591,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit // When on-the-fly funding is used, the initiator may not contribute to the funding transaction. // It will receive HTLCs later that use the purchased inbound liquidity, and liquidity fees will be deduced from those HTLCs. val purchase = LiquidityAds.Purchase.Standard(fundingB, LiquidityAds.Fees(2500 sat, 7500 sat), LiquidityAds.PaymentDetails.FromFutureHtlc(Nil)) - withFixture(0 sat, Nil, fundingB, utxosB, targetFeerate, 330 sat, 0, RequireConfirmedInputs(forLocal = false, forRemote = false), Some(purchase)) { f => + withFixture(ChannelTypes.AnchorOutputs(), 0 sat, Nil, fundingB, utxosB, targetFeerate, 330 sat, 0, RequireConfirmedInputs(forLocal = false, forRemote = false), Some(purchase)) { f => import f._ alice ! Start(alice2bob.ref) @@ -625,7 +645,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit val utxosB = Seq(200_000 sat) // The initiator contributes a small amount, and pays the remaining liquidity fees from its fee credit. val purchase = LiquidityAds.Purchase.WithFeeCredit(fundingB, LiquidityAds.Fees(2500 sat, 7500 sat), 7_500_000 msat, LiquidityAds.PaymentDetails.FromChannelBalanceForFutureHtlc(Nil)) - withFixture(fundingA, utxosA, fundingB, utxosB, targetFeerate, 330 sat, 0, RequireConfirmedInputs(forLocal = false, forRemote = false), Some(purchase)) { f => + withFixture(ChannelTypes.AnchorOutputs(), fundingA, utxosA, fundingB, utxosB, targetFeerate, 330 sat, 0, RequireConfirmedInputs(forLocal = false, forRemote = false), Some(purchase)) { f => import f._ // Alice has enough fee credit. @@ -676,7 +696,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit val utxosB = Seq(200_000 sat) // The initiator wants to pay the liquidity fees from their fee credit, but they don't have enough of it. val purchase = LiquidityAds.Purchase.WithFeeCredit(fundingB, LiquidityAds.Fees(2500 sat, 7500 sat), 10_000_000 msat, LiquidityAds.PaymentDetails.FromChannelBalanceForFutureHtlc(Nil)) - withFixture(0 sat, Nil, fundingB, utxosB, targetFeerate, 330 sat, 0, RequireConfirmedInputs(forLocal = false, forRemote = false), Some(purchase)) { f => + withFixture(ChannelTypes.AnchorOutputs(), 0 sat, Nil, fundingB, utxosB, targetFeerate, 330 sat, 0, RequireConfirmedInputs(forLocal = false, forRemote = false), Some(purchase)) { f => import f._ // Alice doesn't have enough fee credit. @@ -712,7 +732,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit val utxosA = Seq(380_000 sat, 380_000 sat) val fundingB1 = 100_000 sat val utxosB = Seq(350_000 sat, 350_000 sat) - withFixture(fundingA1, utxosA, fundingB1, utxosB, targetFeerate, 660 sat, 0, RequireConfirmedInputs(forLocal = true, forRemote = true)) { f => + withFixture(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(), fundingA1, utxosA, fundingB1, utxosB, targetFeerate, 660 sat, 0, RequireConfirmedInputs(forLocal = true, forRemote = true)) { f => import f._ val probe = TestProbe() @@ -743,7 +763,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit // Alice and Bob decide to splice additional funds in the channel. val additionalFundingA2 = 30_000.sat val additionalFundingB2 = 25_000.sat - val (sharedInputA, sharedInputB) = sharedInputs(commitmentA1, commitmentB1) + val (sharedInputA, sharedInputB) = fixtureParams.sharedInputs(commitmentA1, commitmentB1) val spliceFixtureParams = fixtureParams.createSpliceFixtureParams(fundingTxIndex = 1, fundingAmountA = additionalFundingA2, fundingAmountB = additionalFundingB2, aliceParams.targetFeerate, aliceParams.dustLimit, aliceParams.lockTime, sharedInputA = sharedInputA, sharedInputB = sharedInputB, requireConfirmedInputs = aliceParams.requireConfirmedInputs) val aliceSplice = fixtureParams.spawnTxBuilderSpliceAlice(spliceFixtureParams.fundingParamsA, commitmentA1, walletA) val bobSplice = fixtureParams.spawnTxBuilderSpliceBob(spliceFixtureParams.fundingParamsB, commitmentB1, walletB) @@ -779,7 +799,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit // Bob has more balance than Alice in the shared input, so its total contribution is greater than Alice. // But Bob still signs first, because we don't split the shared input's balance when deciding who signs first. assert(spliceTxA.tx.localAmountIn < spliceTxA.tx.remoteAmountIn) - assert(spliceTxA.signedTx.txIn.exists(_.outPoint == commitmentA1.commitInput.outPoint)) + assert(spliceTxA.signedTx.txIn.exists(_.outPoint == commitmentA1.fundingInput)) assert(0.msat < spliceTxA.tx.localFees) assert(0.msat < spliceTxA.tx.remoteFees) assert(spliceTxB.tx.localFees == spliceTxA.tx.remoteFees) @@ -805,7 +825,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit val utxosA = Seq(150_000 sat) val fundingB1 = 90_000 sat val utxosB = Seq(130_000 sat) - withFixture(fundingA1, utxosA, fundingB1, utxosB, FeeratePerKw(1000 sat), 660 sat, 0, RequireConfirmedInputs(forLocal = true, forRemote = true)) { f => + withFixture(ChannelTypes.SimpleTaprootChannelsPhoenix, fundingA1, utxosA, fundingB1, utxosB, FeeratePerKw(1000 sat), 660 sat, 0, RequireConfirmedInputs(forLocal = true, forRemote = true)) { f => import f._ val probe = TestProbe() @@ -828,17 +848,21 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit fwd.forwardAlice2Bob[TxComplete] val successA1 = alice2bob.expectMsgType[Succeeded] + assert(successA1.nextRemoteCommitNonce_opt.nonEmpty) val successB1 = bob2alice.expectMsgType[Succeeded] + assert(successB1.nextRemoteCommitNonce_opt.nonEmpty) val (txA1, commitmentA1, _, commitmentB1) = fixtureParams.exchangeSigsBobFirst(bobParams, successA1, successB1) walletA.publishTransaction(txA1.signedTx).pipeTo(probe.ref) probe.expectMsg(txA1.txId) // Alice and Bob decide to splice funds out of the channel, and deduce on-chain fees from their new channel contribution. - val spliceOutputsA = List(TxOut(50_000 sat, Script.pay2wpkh(randomKey().publicKey))) - val spliceOutputsB = List(TxOut(30_000 sat, Script.pay2wpkh(randomKey().publicKey))) + val spliceOutputsA = List(TxOut(50_000 sat, Script.pay2tr(randomKey().xOnlyPublicKey()))) + val spliceOutputsB = List(TxOut(30_000 sat, Script.pay2tr(randomKey().xOnlyPublicKey()))) val subtractedFundingA = spliceOutputsA.map(_.amount).sum + 1_000.sat val subtractedFundingB = spliceOutputsB.map(_.amount).sum + 500.sat - val (sharedInputA, sharedInputB) = sharedInputs(commitmentA1, commitmentB1) + val (sharedInputA, sharedInputB) = fixtureParams.sharedInputs(commitmentA1, commitmentB1) + assert(sharedInputA.commitmentFormat == PhoenixSimpleTaprootChannelCommitmentFormat) + assert(sharedInputB.commitmentFormat == PhoenixSimpleTaprootChannelCommitmentFormat) val spliceFixtureParams = fixtureParams.createSpliceFixtureParams(fundingTxIndex = 1, fundingAmountA = -subtractedFundingA, fundingAmountB = -subtractedFundingB, aliceParams.targetFeerate, aliceParams.dustLimit, aliceParams.lockTime, sharedInputA = sharedInputA, sharedInputB = sharedInputB, spliceOutputsA = spliceOutputsA, spliceOutputsB = spliceOutputsB, requireConfirmedInputs = aliceParams.requireConfirmedInputs) val aliceSplice = fixtureParams.spawnTxBuilderSpliceAlice(spliceFixtureParams.fundingParamsA, commitmentA1, walletA) @@ -851,7 +875,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit // Alice --- tx_add_input --> Bob val sharedInput = fwdSplice.forwardAlice2Bob[TxAddInput] assert(sharedInput.previousTx_opt.isEmpty) - assert(sharedInput.sharedInput_opt.contains(commitmentA1.commitInput.outPoint)) + assert(sharedInput.sharedInput_opt.contains(commitmentA1.fundingInput)) // Alice <-- tx_add_output --- Bob val outputB = fwdSplice.forwardBob2Alice[TxAddOutput] // Alice --- tx_add_output --> Bob @@ -866,9 +890,11 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit fwdSplice.forwardAlice2Bob[TxComplete] val successA2 = alice2bob.expectMsgType[Succeeded] - assert(successA2.signingSession.fundingTx.localSigs.previousFundingTxSig_opt.nonEmpty) + assert(successA2.signingSession.fundingTx.localSigs.previousFundingTxSig_opt.isEmpty) + assert(successA2.signingSession.fundingTx.localSigs.previousFundingTxPartialSig_opt.nonEmpty) val successB2 = bob2alice.expectMsgType[Succeeded] - assert(successB2.signingSession.fundingTx.localSigs.previousFundingTxSig_opt.nonEmpty) + assert(successB2.signingSession.fundingTx.localSigs.previousFundingTxSig_opt.isEmpty) + assert(successB2.signingSession.fundingTx.localSigs.previousFundingTxPartialSig_opt.nonEmpty) val (spliceTxA, commitmentA2, spliceTxB, commitmentB2) = fixtureParams.exchangeSigsBobFirst(spliceFixtureParams.fundingParamsB, successA2, successB2) assert(spliceTxA.tx.localFees == 1_000_000.msat) assert(spliceTxB.tx.localFees == 500_000.msat) @@ -893,7 +919,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit val utxosA = Seq(200_000 sat) val fundingB1 = 100_000 sat val utxosB = Seq(150_000 sat) - withFixture(fundingA1, utxosA, fundingB1, utxosB, FeeratePerKw(1000 sat), 660 sat, 0, RequireConfirmedInputs(forLocal = true, forRemote = true)) { f => + withFixture(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(), fundingA1, utxosA, fundingB1, utxosB, FeeratePerKw(1000 sat), 660 sat, 0, RequireConfirmedInputs(forLocal = true, forRemote = true)) { f => import f._ val probe = TestProbe() @@ -926,7 +952,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit val spliceOutputsB = List(25_000 sat, 15_000 sat).map(amount => TxOut(amount, Script.pay2wpkh(randomKey().publicKey))) val subtractedFundingA = spliceOutputsA.map(_.amount).sum + 1_000.sat val subtractedFundingB = spliceOutputsB.map(_.amount).sum + 500.sat - val (sharedInputA, sharedInputB) = sharedInputs(commitmentA1, commitmentB1) + val (sharedInputA, sharedInputB) = fixtureParams.sharedInputs(commitmentA1, commitmentB1) val spliceFixtureParams = fixtureParams.createSpliceFixtureParams(fundingTxIndex = 1, fundingAmountA = -subtractedFundingA, fundingAmountB = -subtractedFundingB, aliceParams.targetFeerate, aliceParams.dustLimit, aliceParams.lockTime, sharedInputA = sharedInputA, sharedInputB = sharedInputB, spliceOutputsA = spliceOutputsA, spliceOutputsB = spliceOutputsB, requireConfirmedInputs = aliceParams.requireConfirmedInputs) val aliceSplice = fixtureParams.spawnTxBuilderSpliceAlice(spliceFixtureParams.fundingParamsA, commitmentA1, walletA) val bobSplice = fixtureParams.spawnTxBuilderSpliceBob(spliceFixtureParams.fundingParamsB, commitmentB1, walletB) @@ -938,7 +964,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit // Alice --- tx_add_input --> Bob val sharedInput = fwdSplice.forwardAlice2Bob[TxAddInput] assert(sharedInput.previousTx_opt.isEmpty) - assert(sharedInput.sharedInput_opt.contains(commitmentA1.commitInput.outPoint)) + assert(sharedInput.sharedInput_opt.contains(commitmentA1.fundingInput)) // Alice <-- tx_add_output --- Bob val outputB1 = fwdSplice.forwardBob2Alice[TxAddOutput] // Alice --- tx_add_output --> Bob @@ -989,7 +1015,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit val utxosA = Seq(480_000 sat, 130_000 sat) val fundingB1 = 100_000 sat val utxosB = Seq(340_000 sat, 70_000 sat) - withFixture(fundingA1, utxosA, fundingB1, utxosB, targetFeerate, 660 sat, 0, RequireConfirmedInputs(forLocal = true, forRemote = true)) { f => + withFixture(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(), fundingA1, utxosA, fundingB1, utxosB, targetFeerate, 660 sat, 0, RequireConfirmedInputs(forLocal = true, forRemote = true)) { f => import f._ val probe = TestProbe() @@ -1023,7 +1049,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit val additionalFundingB = 15_000.sat val spliceOutputsA = List(TxOut(30_000 sat, Script.pay2wpkh(randomKey().publicKey))) val spliceOutputsB = List(TxOut(10_000 sat, Script.pay2wpkh(randomKey().publicKey))) - val (sharedInputA, sharedInputB) = sharedInputs(commitmentA1, commitmentB1) + val (sharedInputA, sharedInputB) = fixtureParams.sharedInputs(commitmentA1, commitmentB1) val spliceFixtureParams = fixtureParams.createSpliceFixtureParams(fundingTxIndex = 1, fundingAmountA = additionalFundingA, fundingAmountB = additionalFundingB, aliceParams.targetFeerate, aliceParams.dustLimit, aliceParams.lockTime, sharedInputA = sharedInputA, sharedInputB = sharedInputB, spliceOutputsA = spliceOutputsA, spliceOutputsB = spliceOutputsB, requireConfirmedInputs = aliceParams.requireConfirmedInputs) val aliceSplice = fixtureParams.spawnTxBuilderSpliceAlice(spliceFixtureParams.fundingParamsA, commitmentA1, walletA) val bobSplice = fixtureParams.spawnTxBuilderSpliceBob(spliceFixtureParams.fundingParamsB, commitmentB1, walletB) @@ -1079,8 +1105,116 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit } } + test("initiator upgrades to taproot while splicing-in") { + val targetFeerate = FeeratePerKw(2000 sat) + val fundingA1 = 150_000 sat + val utxosA = Seq(480_000 sat, 130_000 sat) + val fundingB1 = 0 sat + val utxosB = Seq(70_000 sat) + withFixture(ChannelTypes.AnchorOutputs(), fundingA1, utxosA, fundingB1, utxosB, targetFeerate, 750 sat, 0, RequireConfirmedInputs(forLocal = true, forRemote = true)) { f => + import f._ + + val probe = TestProbe() + alice ! Start(alice2bob.ref) + bob ! Start(bob2alice.ref) + + // Alice --- tx_add_input --> Bob + fwd.forwardAlice2Bob[TxAddInput] + // Alice <-- tx_complete --- Bob + fwd.forwardBob2Alice[TxComplete] + // Alice --- tx_add_output --> Bob + fwd.forwardAlice2Bob[TxAddOutput] + // Alice <-- tx_complete --- Bob + fwd.forwardBob2Alice[TxComplete] + // Alice --- tx_add_output --> Bob + fwd.forwardAlice2Bob[TxAddOutput] + // Alice <-- tx_complete --- Bob + fwd.forwardBob2Alice[TxComplete] + // Alice --- tx_complete --> Bob + fwd.forwardAlice2Bob[TxComplete] + + val successA1 = alice2bob.expectMsgType[Succeeded] + val successB1 = bob2alice.expectMsgType[Succeeded] + val (txA1, commitmentA1, _, commitmentB1) = fixtureParams.exchangeSigsBobFirst(bobParams, successA1, successB1) + assert(commitmentA1.commitmentFormat == UnsafeLegacyAnchorOutputsCommitmentFormat) + assert(commitmentB1.commitmentFormat == UnsafeLegacyAnchorOutputsCommitmentFormat) + walletA.publishTransaction(txA1.signedTx).pipeTo(probe.ref) + probe.expectMsg(txA1.txId) + + // Alice decides to splice funds in the channel and upgrade to taproot. + // Bob uses this opportunity to also splice some funds in the channel. + val additionalFundingA2 = 80_000.sat + val additionalFundingB2 = 55_000.sat + val (sharedInputA, sharedInputB) = fixtureParams.sharedInputs(commitmentA1, commitmentB1) + assert(sharedInputA.commitmentFormat == UnsafeLegacyAnchorOutputsCommitmentFormat) + assert(sharedInputB.commitmentFormat == UnsafeLegacyAnchorOutputsCommitmentFormat) + val spliceFixtureParams = fixtureParams.createSpliceFixtureParams(fundingTxIndex = 1, fundingAmountA = additionalFundingA2, fundingAmountB = additionalFundingB2, aliceParams.targetFeerate, aliceParams.dustLimit, aliceParams.lockTime, sharedInputA = sharedInputA, sharedInputB = sharedInputB, nextCommitmentFormat_opt = Some(PhoenixSimpleTaprootChannelCommitmentFormat), requireConfirmedInputs = aliceParams.requireConfirmedInputs) + val aliceSplice = fixtureParams.spawnTxBuilderSpliceAlice(spliceFixtureParams.fundingParamsA, commitmentA1, walletA) + val bobSplice = fixtureParams.spawnTxBuilderSpliceBob(spliceFixtureParams.fundingParamsB, commitmentB1, walletB) + val fwdSplice = TypeCheckedForwarder(aliceSplice, bobSplice, alice2bob, bob2alice) + + aliceSplice ! Start(alice2bob.ref) + bobSplice ! Start(bob2alice.ref) + + // Alice --- tx_add_input --> Bob + fwdSplice.forwardAlice2Bob[TxAddInput] + // Alice <-- tx_add_input --- Bob + fwdSplice.forwardBob2Alice[TxAddInput] + // Alice --- tx_add_input --> Bob + fwdSplice.forwardAlice2Bob[TxAddInput] + // Alice <-- tx_add_output --- Bob + fwdSplice.forwardBob2Alice[TxAddOutput] + // Alice --- tx_add_output --> Bob + fwdSplice.forwardAlice2Bob[TxAddOutput] + // Alice <-- tx_complete --- Bob + fwdSplice.forwardBob2Alice[TxComplete] + // Alice --- tx_add_output --> Bob + fwdSplice.forwardAlice2Bob[TxAddOutput] + // Alice <-- tx_complete --- Bob + val txCompleteB = fwdSplice.forwardBob2Alice[TxComplete] + // Alice --- tx_complete --> Bob + val txCompleteA = fwdSplice.forwardAlice2Bob[TxComplete] + Seq(txCompleteA, txCompleteB).foreach(txComplete => { + assert(txComplete.commitNonces_opt.nonEmpty) + assert(txComplete.fundingNonce_opt.isEmpty) // the previous commitment didn't use taproot + assert(txComplete.commitNonces_opt.map(n => Seq(n.commitNonce, n.nextCommitNonce)).get.size == 2) + }) + + val successA2 = alice2bob.expectMsgType[Succeeded] + assert(successA2.signingSession.fundingTx.localSigs.previousFundingTxSig_opt.nonEmpty) + assert(successA2.signingSession.fundingTx.localSigs.previousFundingTxPartialSig_opt.isEmpty) + assert(successA2.commitSig.sigOrPartialSig.isInstanceOf[PartialSignatureWithNonce]) + val successB2 = bob2alice.expectMsgType[Succeeded] + assert(successB2.signingSession.fundingTx.localSigs.previousFundingTxSig_opt.nonEmpty) + assert(successB2.signingSession.fundingTx.localSigs.previousFundingTxPartialSig_opt.isEmpty) + assert(successB2.commitSig.sigOrPartialSig.isInstanceOf[PartialSignatureWithNonce]) + val (spliceTxA, commitmentA2, spliceTxB, commitmentB2) = fixtureParams.exchangeSigsBobFirst(spliceFixtureParams.fundingParamsB, successA2, successB2) + assert(successA2.nextRemoteCommitNonce_opt.contains((spliceTxA.txId, txCompleteB.commitNonces_opt.get.nextCommitNonce))) + assert(successB2.nextRemoteCommitNonce_opt.contains((spliceTxB.txId, txCompleteA.commitNonces_opt.get.nextCommitNonce))) + assert(spliceTxA.tx.localAmountIn > spliceTxA.tx.remoteAmountIn) + assert(spliceTxA.signedTx.txIn.exists(_.outPoint == commitmentA1.fundingInput)) + assert(0.msat < spliceTxA.tx.localFees) + assert(0.msat < spliceTxA.tx.remoteFees) + assert(spliceTxB.tx.localFees == spliceTxA.tx.remoteFees) + assert(spliceTxA.tx.sharedOutput.amount == fundingA1 + fundingB1 + additionalFundingA2 + additionalFundingB2) + + assert(commitmentA2.localCommit.spec.toLocal == (fundingA1 + additionalFundingA2).toMilliSatoshi) + assert(commitmentA2.localCommit.spec.toRemote == (fundingB1 + additionalFundingB2).toMilliSatoshi) + assert(commitmentB2.localCommit.spec.toLocal == (fundingB1 + additionalFundingB2).toMilliSatoshi) + assert(commitmentB2.localCommit.spec.toRemote == (fundingA1 + additionalFundingA2).toMilliSatoshi) + + // The resulting transaction is valid and has the right feerate. + walletA.publishTransaction(spliceTxA.signedTx).pipeTo(probe.ref) + probe.expectMsg(spliceTxA.txId) + walletA.getMempoolTx(spliceTxA.txId).pipeTo(probe.ref) + val mempoolTx = probe.expectMsgType[MempoolTx] + assert(mempoolTx.fees == spliceTxA.tx.fees) + assert(targetFeerate <= spliceTxA.feerate && spliceTxA.feerate <= targetFeerate * 1.25, s"unexpected feerate (target=$targetFeerate actual=${spliceTxA.feerate})") + } + } + test("remove input/output") { - withFixture(100_000 sat, Seq(150_000 sat), 0 sat, Nil, FeeratePerKw(2500 sat), 330 sat, 0, RequireConfirmedInputs(forLocal = false, forRemote = false)) { f => + withFixture(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(), 100_000 sat, Seq(150_000 sat), 0 sat, Nil, FeeratePerKw(2500 sat), 330 sat, 0, RequireConfirmedInputs(forLocal = false, forRemote = false)) { f => import f._ alice ! Start(alice2bob.ref) @@ -1124,7 +1258,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit } test("not enough funds (unconfirmed utxos not allowed)") { - withFixture(100_000 sat, Seq(250_000 sat), 0 sat, Nil, FeeratePerKw(2500 sat), 660 sat, 0, RequireConfirmedInputs(forLocal = true, forRemote = true)) { f => + withFixture(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(), 100_000 sat, Seq(250_000 sat), 0 sat, Nil, FeeratePerKw(2500 sat), 660 sat, 0, RequireConfirmedInputs(forLocal = true, forRemote = true)) { f => import f._ // Alice's inputs are all unconfirmed. @@ -1150,7 +1284,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit test("not enough funds (unusable utxos)") { val fundingA = 140_000 sat val utxosA = Seq(75_000 sat, 60_000 sat) - withFixture(fundingA, utxosA, 0 sat, Nil, FeeratePerKw(5000 sat), 660 sat, 0, RequireConfirmedInputs(forLocal = false, forRemote = false)) { f => + withFixture(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(), fundingA, utxosA, 0 sat, Nil, FeeratePerKw(5000 sat), 660 sat, 0, RequireConfirmedInputs(forLocal = false, forRemote = false)) { f => import f._ import fr.acinq.bitcoin.scalacompat.KotlinUtils._ @@ -1167,7 +1301,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit val publicKey = probe.expectMsgType[PublicKey] val tx = Transaction(2, Nil, TxOut(100_000 sat, Script.pay2wpkh(publicKey)) +: (1 to 2500).map(_ => TxOut(5000 sat, Script.pay2wpkh(randomKey().publicKey))), 0) val minerWallet = makeBitcoinCoreClient() - minerWallet.fundTransaction(tx, FeeratePerKw(500 sat), replaceable = true).pipeTo(probe.ref) + minerWallet.fundTransaction(tx, FeeratePerKw(500 sat)).pipeTo(probe.ref) val unsignedTx = probe.expectMsgType[FundTransactionResponse].tx minerWallet.signPsbt(new Psbt(unsignedTx), unsignedTx.txIn.indices, Nil).pipeTo(probe.ref) val Right(signedTx) = probe.expectMsgType[ProcessPsbtResponse].finalTx_opt @@ -1198,7 +1332,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit test("skip unusable utxos") { val fundingA = 140_000 sat val utxosA = Seq(55_000 sat, 65_000 sat, 50_000 sat) - withFixture(fundingA, utxosA, 0 sat, Nil, FeeratePerKw(5000 sat), 660 sat, 0, RequireConfirmedInputs(forLocal = false, forRemote = false)) { f => + withFixture(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(), fundingA, utxosA, 0 sat, Nil, FeeratePerKw(5000 sat), 660 sat, 0, RequireConfirmedInputs(forLocal = false, forRemote = false)) { f => import f._ // Add some unusable utxos to Alice's wallet. @@ -1263,7 +1397,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit val targetFeerate = FeeratePerKw(7500 sat) val fundingA = 85_000 sat val utxosA = Seq(120_000 sat) - withFixture(fundingA, utxosA, 0 sat, Nil, targetFeerate, 660 sat, 0, RequireConfirmedInputs(forLocal = false, forRemote = false)) { f => + withFixture(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(), fundingA, utxosA, 0 sat, Nil, targetFeerate, 660 sat, 0, RequireConfirmedInputs(forLocal = false, forRemote = false)) { f => import f._ alice ! Start(alice2bob.ref) @@ -1329,10 +1463,10 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit } test("fund transaction with previous inputs (with new inputs)") { - val targetFeerate = FeeratePerKw(10_000 sat) + val targetFeerate = FeeratePerKw(11_000 sat) val fundingA = 100_000 sat val utxosA = Seq(55_000 sat, 55_000 sat, 55_000 sat) - withFixture(fundingA, utxosA, 0 sat, Nil, targetFeerate, 660 sat, 0, RequireConfirmedInputs(forLocal = false, forRemote = false)) { f => + withFixture(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(), fundingA, utxosA, 0 sat, Nil, targetFeerate, 660 sat, 0, RequireConfirmedInputs(forLocal = false, forRemote = false)) { f => import f._ alice ! Start(alice2bob.ref) @@ -1413,8 +1547,8 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit val fundingA = 100_000 sat val utxosA = Seq(70_000 sat, 60_000 sat) val fundingB = 25_000 sat - val utxosB = Seq(27_500 sat) - withFixture(fundingA, utxosA, fundingB, utxosB, initialFeerate, 660 sat, 0, RequireConfirmedInputs(forLocal = false, forRemote = false)) { f => + val utxosB = Seq(27_250 sat) + withFixture(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(), fundingA, utxosA, fundingB, utxosB, initialFeerate, 660 sat, 0, RequireConfirmedInputs(forLocal = false, forRemote = false)) { f => import f._ alice ! Start(alice2bob.ref) @@ -1498,7 +1632,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit val utxosA = Seq(480_000 sat, 75_000 sat) val fundingB1 = 100_000 sat val utxosB = Seq(325_000 sat, 60_000 sat) - withFixture(fundingA1, utxosA, fundingB1, utxosB, targetFeerate, 660 sat, 0, RequireConfirmedInputs(forLocal = false, forRemote = false)) { f => + withFixture(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(), fundingA1, utxosA, fundingB1, utxosB, targetFeerate, 660 sat, 0, RequireConfirmedInputs(forLocal = false, forRemote = false)) { f => import f._ val probe = TestProbe() @@ -1532,7 +1666,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit val additionalFundingB = 5_000.sat val spliceOutputsA = List(TxOut(20_000 sat, Script.pay2wpkh(randomKey().publicKey))) val spliceOutputsB = List(TxOut(10_000 sat, Script.pay2wpkh(randomKey().publicKey))) - val (sharedInputA, sharedInputB) = sharedInputs(commitmentA1, commitmentB1) + val (sharedInputA, sharedInputB) = fixtureParams.sharedInputs(commitmentA1, commitmentB1) val spliceFixtureParams = fixtureParams.createSpliceFixtureParams(fundingTxIndex = 1, fundingAmountA = additionalFundingA, fundingAmountB = additionalFundingB, aliceParams.targetFeerate, aliceParams.dustLimit, aliceParams.lockTime, sharedInputA = sharedInputA, sharedInputB = sharedInputB, spliceOutputsA = spliceOutputsA, spliceOutputsB = spliceOutputsB, requireConfirmedInputs = aliceParams.requireConfirmedInputs) val aliceSplice = fixtureParams.spawnTxBuilderSpliceAlice(spliceFixtureParams.fundingParamsA, commitmentA1, walletA) val bobSplice = fixtureParams.spawnTxBuilderSpliceBob(spliceFixtureParams.fundingParamsB, commitmentB1, walletB) @@ -1625,7 +1759,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit val utxosA = Seq(340_000 sat, 40_000 sat, 35_000 sat) val fundingB1 = 80_000 sat val utxosB = Seq(280_000 sat, 20_000 sat, 15_000 sat) - withFixture(fundingA1, utxosA, fundingB1, utxosB, targetFeerate, 660 sat, 0, RequireConfirmedInputs(forLocal = false, forRemote = false)) { f => + withFixture(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(), fundingA1, utxosA, fundingB1, utxosB, targetFeerate, 660 sat, 0, RequireConfirmedInputs(forLocal = false, forRemote = false)) { f => import f._ val probe = TestProbe() @@ -1659,7 +1793,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit val additionalFundingB = 5_000.sat val spliceOutputsA = List(TxOut(20_000 sat, Script.pay2wpkh(randomKey().publicKey))) val spliceOutputsB = List(TxOut(10_000 sat, Script.pay2wpkh(randomKey().publicKey))) - val (sharedInputA, sharedInputB) = sharedInputs(commitmentA1, commitmentB1) + val (sharedInputA, sharedInputB) = fixtureParams.sharedInputs(commitmentA1, commitmentB1) val spliceFixtureParams = fixtureParams.createSpliceFixtureParams(fundingTxIndex = 1, fundingAmountA = additionalFundingA, fundingAmountB = additionalFundingB, aliceParams.targetFeerate, aliceParams.dustLimit, aliceParams.lockTime, sharedInputA = sharedInputA, sharedInputB = sharedInputB, spliceOutputsA = spliceOutputsA, spliceOutputsB = spliceOutputsB, requireConfirmedInputs = aliceParams.requireConfirmedInputs) val fundingParamsA1 = spliceFixtureParams.fundingParamsA val fundingParamsB1 = spliceFixtureParams.fundingParamsB @@ -1758,9 +1892,9 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit val targetFeerate = FeeratePerKw(10_000 sat) val fundingA = 100_000 sat val utxosA = Seq(150_000 sat) - val fundingB = 92_000 sat + val fundingB = 93_000 sat val utxosB = Seq(50_000 sat, 50_000 sat, 50_000 sat, 50_000 sat) - withFixture(fundingA, utxosA, fundingB, utxosB, targetFeerate, 660 sat, 0, RequireConfirmedInputs(forLocal = false, forRemote = false)) { f => + withFixture(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(), fundingA, utxosA, fundingB, utxosB, targetFeerate, 660 sat, 0, RequireConfirmedInputs(forLocal = false, forRemote = false)) { f => import f._ val probe = TestProbe() @@ -1796,15 +1930,16 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit val purchase = LiquidityAds.Purchase.Standard(50_000 sat, LiquidityAds.Fees(1000 sat, 1500 sat), LiquidityAds.PaymentDetails.FromChannelBalance) // Alice pays fees for the common fields of the transaction, by decreasing her balance in the shared output. val spliceFeeA = { + val dummyWitness = Scripts.witness2of2(Transactions.PlaceHolderSig, Transactions.PlaceHolderSig, randomKey().publicKey, randomKey().publicKey) val dummySpliceTx = Transaction( version = 2, - txIn = Seq(TxIn(commitmentA1.commitInput.outPoint, ByteVector.empty, 0, Scripts.witness2of2(Transactions.PlaceHolderSig, Transactions.PlaceHolderSig, Transactions.PlaceHolderPubKey, Transactions.PlaceHolderPubKey))), - txOut = Seq(commitmentA1.commitInput.txOut), + txIn = Seq(TxIn(commitmentA1.fundingInput, ByteVector.empty, 0, dummyWitness)), + txOut = Seq(commitmentA1.commitInput(fixtureParams.channelKeysA).txOut), lockTime = 0 ) Transactions.weight2fee(targetFeerate, dummySpliceTx.weight()) } - val (sharedInputA, sharedInputB) = sharedInputs(commitmentA1, commitmentB1) + val (sharedInputA, sharedInputB) = fixtureParams.sharedInputs(commitmentA1, commitmentB1) val spliceFixtureParams = fixtureParams.createSpliceFixtureParams(fundingTxIndex = 1, fundingAmountA = -spliceFeeA, fundingAmountB = fundingB, targetFeerate, aliceParams.dustLimit, aliceParams.lockTime, sharedInputA = sharedInputA, sharedInputB = sharedInputB, spliceOutputsA = Nil, spliceOutputsB = Nil, requireConfirmedInputs = aliceParams.requireConfirmedInputs) val fundingParamsA1 = spliceFixtureParams.fundingParamsA val fundingParamsB1 = spliceFixtureParams.fundingParamsB @@ -1846,13 +1981,22 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit } } + private def replacePrevTxWithPrevTxOut(input: TxAddInput): TxAddInput = { + input.previousTx_opt match { + case None => input + case Some(tx) => + val txOut = tx.txOut(input.previousTxOutput.toInt) + input.copy(previousTx_opt = None, tlvStream = TlvStream(TxAddInputTlv.PrevTxOut(tx.txid, txOut.amount, txOut.publicKeyScript))) + } + } + test("fund splice transaction with previous inputs (different balance)") { val targetFeerate = FeeratePerKw(2_500 sat) val fundingA1 = 100_000 sat val utxosA = Seq(340_000 sat, 40_000 sat, 35_000 sat) val fundingB1 = 80_000 sat val utxosB = Seq(290_000 sat, 20_000 sat, 15_000 sat) - withFixture(fundingA1, utxosA, fundingB1, utxosB, targetFeerate, 660 sat, 0, RequireConfirmedInputs(forLocal = false, forRemote = false)) { f => + withFixture(ChannelTypes.SimpleTaprootChannelsStaging(), fundingA1, utxosA, fundingB1, utxosB, targetFeerate, 660 sat, 0, RequireConfirmedInputs(forLocal = false, forRemote = false)) { f => import f._ val probe = TestProbe() @@ -1883,7 +2027,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit // Alice splices some funds in, which requires using an additional input. val additionalFundingA1 = 25_000.sat - val (sharedInputA, sharedInputB) = sharedInputs(commitmentA1, commitmentB1) + val (sharedInputA, sharedInputB) = fixtureParams.sharedInputs(commitmentA1, commitmentB1) val spliceFixtureParams = fixtureParams.createSpliceFixtureParams(fundingTxIndex = 1, fundingAmountA = additionalFundingA1, fundingAmountB = 0 sat, aliceParams.targetFeerate, aliceParams.dustLimit, aliceParams.lockTime, sharedInputA = sharedInputA, sharedInputB = sharedInputB, requireConfirmedInputs = aliceParams.requireConfirmedInputs) val fundingParamsA1 = spliceFixtureParams.fundingParamsA val fundingParamsB1 = spliceFixtureParams.fundingParamsB @@ -1894,12 +2038,15 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit aliceSplice ! Start(alice2bob.ref) bobSplice ! Start(bob2alice.ref) + // Since we're splicing a taproot channel, we can replace the entire previous transaction by only its txOut. // Alice --- tx_add_input --> Bob - fwdSplice.forwardAlice2Bob[TxAddInput] + val input1 = alice2bob.expectMsgType[SendMessage].msg.asInstanceOf[TxAddInput] + bobSplice ! ReceiveMessage(replacePrevTxWithPrevTxOut(input1)) // Alice <-- tx_complete --- Bob fwdSplice.forwardBob2Alice[TxComplete] // Alice --- tx_add_input --> Bob - fwdSplice.forwardAlice2Bob[TxAddInput] + val input2 = alice2bob.expectMsgType[SendMessage].msg.asInstanceOf[TxAddInput] + bobSplice ! ReceiveMessage(replacePrevTxWithPrevTxOut(input2)) // Alice <-- tx_complete --- Bob fwdSplice.forwardBob2Alice[TxComplete] // Alice --- tx_add_output --> Bob @@ -2006,7 +2153,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit val targetFeerate = FeeratePerKw(10_000 sat) val fundingA = 80_000 sat val utxosA = Seq(85_000 sat) - withFixture(fundingA, utxosA, 0 sat, Nil, targetFeerate, 660 sat, 0, RequireConfirmedInputs(forLocal = false, forRemote = false)) { f => + withFixture(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(), fundingA, utxosA, 0 sat, Nil, targetFeerate, 660 sat, 0, RequireConfirmedInputs(forLocal = false, forRemote = false)) { f => import f._ alice ! Start(alice2bob.ref) @@ -2035,13 +2182,13 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit } test("allow unconfirmed remote inputs") { - withFixture(120_000 sat, Seq(150_000 sat), 50_000 sat, Seq(100_000 sat), FeeratePerKw(4000 sat), 660 sat, 0, RequireConfirmedInputs(forLocal = false, forRemote = false)) { f => + withFixture(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(), 120_000 sat, Seq(150_000 sat), 50_000 sat, Seq(100_000 sat), FeeratePerKw(4000 sat), 660 sat, 0, RequireConfirmedInputs(forLocal = false, forRemote = false)) { f => import f._ // Bob's available utxo is unconfirmed. val probe = TestProbe() walletB.getP2wpkhPubkey().pipeTo(probe.ref) - walletB.sendToPubkeyScript(Script.write(Script.pay2wpkh(probe.expectMsgType[PublicKey])), 75_000 sat, FeeratePerKw(FeeratePerByte(1.sat))).pipeTo(probe.ref) + walletB.sendToPubkeyScript(Script.write(Script.pay2wpkh(probe.expectMsgType[PublicKey])), 75_000 sat, FeeratePerByte(1.sat).perKw).pipeTo(probe.ref) probe.expectMsgType[TxId] alice ! Start(alice2bob.ref) @@ -2071,7 +2218,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit } test("reject unconfirmed remote inputs") { - withFixture(120_000 sat, Seq(150_000 sat), 50_000 sat, Seq(100_000 sat), FeeratePerKw(4000 sat), 660 sat, 0, RequireConfirmedInputs(forLocal = false, forRemote = true)) { f => + withFixture(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(), 120_000 sat, Seq(150_000 sat), 50_000 sat, Seq(100_000 sat), FeeratePerKw(4000 sat), 660 sat, 0, RequireConfirmedInputs(forLocal = false, forRemote = true)) { f => import f._ // Bob's available utxo is unconfirmed. @@ -2103,7 +2250,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit } test("funding amount drops below reserve") { - withFixture(500_000 sat, Seq(600_000 sat), 400_000 sat, Seq(450_000 sat), FeeratePerKw(1000 sat), 330 sat, 0, RequireConfirmedInputs(forLocal = false, forRemote = false)) { f => + withFixture(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(), 500_000 sat, Seq(600_000 sat), 400_000 sat, Seq(450_000 sat), FeeratePerKw(1000 sat), 330 sat, 0, RequireConfirmedInputs(forLocal = false, forRemote = false)) { f => import f._ val probe = TestProbe() @@ -2136,7 +2283,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit val subtractedFundingB = 398_000 sat val spliceOutputsA = List(TxOut(99_000 sat, Script.pay2wpkh(randomKey().publicKey))) val spliceOutputsB = List(TxOut(397_000 sat, Script.pay2wpkh(randomKey().publicKey))) - val (sharedInputA, sharedInputB) = sharedInputs(commitmentA, commitmentB) + val (sharedInputA, sharedInputB) = fixtureParams.sharedInputs(commitmentA, commitmentB) val fundingParamsA1 = aliceParams.copy(localContribution = -subtractedFundingA, remoteContribution = -subtractedFundingB, sharedInput_opt = Some(sharedInputA), localOutputs = spliceOutputsA) val fundingParamsB1 = bobParams.copy(localContribution = -subtractedFundingB, remoteContribution = -subtractedFundingA, sharedInput_opt = Some(sharedInputB), localOutputs = spliceOutputsB) val aliceSplice = fixtureParams.spawnTxBuilderSpliceAlice(fundingParamsA1, commitmentA, walletA) @@ -2165,8 +2312,8 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit } } - test("invalid tx_signatures (missing shared input signature)") { - withFixture(150_000 sat, Seq(200_000 sat), 0 sat, Nil, FeeratePerKw(1000 sat), 330 sat, 0, RequireConfirmedInputs(forLocal = false, forRemote = false)) { f => + private def testTxSignaturesMissingSharedInputSigs(channelType: SupportedChannelType): Unit = { + withFixture(channelType, 150_000 sat, Seq(200_000 sat), 0 sat, Nil, FeeratePerKw(1000 sat), 330 sat, 0, RequireConfirmedInputs(forLocal = false, forRemote = false)) { f => import f._ val probe = TestProbe() @@ -2197,7 +2344,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit // Alice splices some funds out, which creates two outputs (a shared output and a splice output). val subtractedFundingA = 30_000 sat val spliceOutputsA = List(TxOut(25_000 sat, Script.pay2wpkh(randomKey().publicKey))) - val (sharedInputA, sharedInputB) = sharedInputs(commitmentA, commitmentB) + val (sharedInputA, sharedInputB) = fixtureParams.sharedInputs(commitmentA, commitmentB) val spliceFixtureParams = fixtureParams.createSpliceFixtureParams(fundingTxIndex = 1, fundingAmountA = -subtractedFundingA, fundingAmountB = 0 sat, aliceParams.targetFeerate, aliceParams.dustLimit, aliceParams.lockTime, sharedInputA = sharedInputA, sharedInputB = sharedInputB, spliceOutputsA = spliceOutputsA, spliceOutputsB = Nil, requireConfirmedInputs = aliceParams.requireConfirmedInputs) val fundingParamsA1 = spliceFixtureParams.fundingParamsA val fundingParamsB1 = spliceFixtureParams.fundingParamsB @@ -2225,15 +2372,23 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit val successA2 = alice2bob.expectMsgType[Succeeded] val successB2 = bob2alice.expectMsgType[Succeeded] // Alice <-- commit_sig --- Bob - val Right(signingA3: InteractiveTxSigningSession.WaitingForSigs) = successA2.signingSession.receiveCommitSig(fixtureParams.nodeParamsA, fixtureParams.channelParamsA, successB2.commitSig)(akka.event.NoLogging) + val Right(signingA3: InteractiveTxSigningSession.WaitingForSigs) = successA2.signingSession.receiveCommitSig(fixtureParams.channelParamsA, fixtureParams.channelKeysA, successB2.commitSig, fixtureParams.nodeParamsA.currentBlockHeight)(akka.event.NoLogging) // Alice <-- tx_signatures --- Bob - val Left(error) = signingA3.receiveTxSigs(fixtureParams.nodeParamsA, fixtureParams.channelParamsA, successB2.signingSession.fundingTx.localSigs.copy(tlvStream = TlvStream.empty))(akka.event.NoLogging) + val Left(error) = signingA3.receiveTxSigs(fixtureParams.channelKeysA, successB2.signingSession.fundingTx.localSigs.copy(tlvStream = TlvStream.empty), fixtureParams.nodeParamsA.currentBlockHeight)(akka.event.NoLogging) assert(error == InvalidFundingSignature(bobParams.channelId, Some(successA2.signingSession.fundingTx.txId))) } } + test("invalid tx_signatures (missing shared input signature)") { + testTxSignaturesMissingSharedInputSigs(ChannelTypes.AnchorOutputsZeroFeeHtlcTx()) + } + + test("invalid tx_signatures (missing shared input signature, taproot)") { + testTxSignaturesMissingSharedInputSigs(ChannelTypes.SimpleTaprootChannelsStaging()) + } + test("invalid commitment index") { - withFixture(150_000 sat, Seq(200_000 sat), 0 sat, Nil, FeeratePerKw(1000 sat), 330 sat, 0, RequireConfirmedInputs(forLocal = false, forRemote = false)) { f => + withFixture(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(), 150_000 sat, Seq(200_000 sat), 0 sat, Nil, FeeratePerKw(1000 sat), 330 sat, 0, RequireConfirmedInputs(forLocal = false, forRemote = false)) { f => import f._ val probe = TestProbe() @@ -2264,7 +2419,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit // Alice splices some funds out, but she doesn't have the same commitment index than Bob. val subtractedFundingA = 30_000 sat val spliceOutputsA = List(TxOut(25_000 sat, Script.pay2wpkh(randomKey().publicKey))) - val (sharedInputA, sharedInputB) = sharedInputs(commitmentA, commitmentB) + val (sharedInputA, sharedInputB) = fixtureParams.sharedInputs(commitmentA, commitmentB) val spliceFixtureParams = fixtureParams.createSpliceFixtureParams(fundingTxIndex = 1, fundingAmountA = -subtractedFundingA, fundingAmountB = 0 sat, aliceParams.targetFeerate, aliceParams.dustLimit, aliceParams.lockTime, sharedInputA = sharedInputA, sharedInputB = sharedInputB, spliceOutputsA = spliceOutputsA, spliceOutputsB = Nil, requireConfirmedInputs = aliceParams.requireConfirmedInputs) val fundingParamsA1 = spliceFixtureParams.fundingParamsA val fundingParamsB1 = spliceFixtureParams.fundingParamsB @@ -2296,9 +2451,9 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit val successA2 = alice2bob.expectMsgType[Succeeded] val successB2 = bob2alice.expectMsgType[Succeeded] // Alice <-- commit_sig --- Bob - val Left(failureA) = successA2.signingSession.receiveCommitSig(fixtureParams.nodeParamsA, fixtureParams.channelParamsA, successB2.commitSig)(akka.event.NoLogging) + val Left(failureA) = successA2.signingSession.receiveCommitSig(fixtureParams.channelParamsA, fixtureParams.channelKeysA, successB2.commitSig, fixtureParams.nodeParamsA.currentBlockHeight)(akka.event.NoLogging) // Alice --- commit_sig --> Bob - val Left(failureB) = successB2.signingSession.receiveCommitSig(fixtureParams.nodeParamsB, fixtureParams.channelParamsB, successA2.commitSig)(akka.event.NoLogging) + val Left(failureB) = successB2.signingSession.receiveCommitSig(fixtureParams.channelParamsB, fixtureParams.channelKeysB, successA2.commitSig, fixtureParams.nodeParamsB.currentBlockHeight)(akka.event.NoLogging) assert(failureA.isInstanceOf[InvalidCommitmentSignature]) assert(failureB.isInstanceOf[InvalidCommitmentSignature]) assert(failureA.asInstanceOf[InvalidCommitmentSignature].fundingTxId == failureB.asInstanceOf[InvalidCommitmentSignature].fundingTxId) @@ -2309,7 +2464,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit test("invalid funding contributions") { val probe = TestProbe() val wallet = new SingleKeyOnChainWallet() - val params = createFixtureParams(75_000 sat, 25_000 sat, FeeratePerKw(5000 sat), 500 sat, 0) + val params = createFixtureParams(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(), 75_000 sat, 25_000 sat, FeeratePerKw(5000 sat), 500 sat, 0) val previousCommitment = CommitmentsSpec.makeCommitments(25_000_000 msat, 75_000_000 msat).active.head val sharedInput = params.dummySharedInputB(100_000 sat) val testCases = Seq( @@ -2329,7 +2484,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit val probe = TestProbe() val wallet = new SingleKeyOnChainWallet() val purchase = LiquidityAds.Purchase.Standard(500_000 sat, LiquidityAds.Fees(5000 sat, 20_000 sat), LiquidityAds.PaymentDetails.FromChannelBalance) - val params = createFixtureParams(24_000 sat, 500_000 sat, FeeratePerKw(5000 sat), 500 sat, 0) + val params = createFixtureParams(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(), 24_000 sat, 500_000 sat, FeeratePerKw(5000 sat), 500 sat, 0) // Bob will reject Alice's proposal, since she doesn't have enough funds to pay the liquidity fees. val bob = params.spawnTxBuilderBob(wallet, params.fundingParamsB, Some(purchase)) bob ! Start(probe.ref) @@ -2366,7 +2521,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit ) val previousTx = Transaction(2, Nil, previousOutputs, 0) val wallet = new SingleKeyOnChainWallet() - val params = createFixtureParams(100_000 sat, 0 sat, FeeratePerKw(5000 sat), 330 sat, 0) + val params = createFixtureParams(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(), 100_000 sat, 0 sat, FeeratePerKw(5000 sat), 330 sat, 0) val testCases = Seq( TxAddInput(params.channelId, UInt64(0), Some(previousTx), 0, 0) -> InvalidSerialId(params.channelId, UInt64(0)), TxAddInput(params.channelId, UInt64(1), Some(previousTx), 0, 0) -> DuplicateSerialId(params.channelId, UInt64(1)), @@ -2375,6 +2530,8 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit TxAddInput(params.channelId, UInt64(7), Some(previousTx), 1, 0) -> NonSegwitInput(params.channelId, UInt64(7), previousTx.txid, 1), TxAddInput(params.channelId, UInt64(9), Some(previousTx), 2, 0xfffffffeL) -> NonReplaceableInput(params.channelId, UInt64(9), previousTx.txid, 2, 0xfffffffeL), TxAddInput(params.channelId, UInt64(9), Some(previousTx), 2, 0xffffffffL) -> NonReplaceableInput(params.channelId, UInt64(9), previousTx.txid, 2, 0xffffffffL), + // Replacing the previousTx field with previousTxOut is only allowed for splices on taproot channels. + TxAddInput(params.channelId, UInt64(5), None, 0, 0, TlvStream(TxAddInputTlv.PrevTxOut(previousTx.txid, previousOutputs(0).amount, previousOutputs(0).publicKeyScript))) -> PreviousTxMissing(params.channelId, UInt64(5)) ) testCases.foreach { case (input, expected) => @@ -2395,7 +2552,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit test("allow standard output types") { val probe = TestProbe() val wallet = new SingleKeyOnChainWallet() - val params = createFixtureParams(100_000 sat, 0 sat, FeeratePerKw(5000 sat), 330 sat, 0) + val params = createFixtureParams(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(), 100_000 sat, 0 sat, FeeratePerKw(5000 sat), 330 sat, 0) val testCases = Seq( TxAddOutput(params.channelId, UInt64(1), 25_000 sat, Script.write(Script.pay2pkh(randomKey().publicKey))), TxAddOutput(params.channelId, UInt64(1), 25_000 sat, Script.write(Script.pay2sh(OP_1 :: Nil))), @@ -2418,7 +2575,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit test("invalid output") { val probe = TestProbe() val wallet = new SingleKeyOnChainWallet() - val params = createFixtureParams(100_000 sat, 0 sat, FeeratePerKw(5000 sat), 330 sat, 0) + val params = createFixtureParams(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(), 100_000 sat, 0 sat, FeeratePerKw(5000 sat), 330 sat, 0) val validScript = Script.write(Script.pay2wpkh(randomKey().publicKey)) val testCases = Seq( TxAddOutput(params.channelId, UInt64(0), 25_000 sat, validScript) -> InvalidSerialId(params.channelId, UInt64(0)), @@ -2444,7 +2601,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit test("remove unknown input/output") { val probe = TestProbe() val wallet = new SingleKeyOnChainWallet() - val params = createFixtureParams(100_000 sat, 0 sat, FeeratePerKw(5000 sat), 330 sat, 0) + val params = createFixtureParams(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(), 100_000 sat, 0 sat, FeeratePerKw(5000 sat), 330 sat, 0) val testCases = Seq( TxRemoveOutput(params.channelId, UInt64(53)) -> UnknownSerialId(params.channelId, UInt64(53)), TxRemoveInput(params.channelId, UInt64(57)) -> UnknownSerialId(params.channelId, UInt64(57)), @@ -2464,7 +2621,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit test("too many protocol rounds") { val probe = TestProbe() val wallet = new SingleKeyOnChainWallet() - val params = createFixtureParams(100_000 sat, 0 sat, FeeratePerKw(5000 sat), 330 sat, 0) + val params = createFixtureParams(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(), 100_000 sat, 0 sat, FeeratePerKw(5000 sat), 330 sat, 0) val validScript = Script.write(Script.pay2wpkh(randomKey().publicKey)) val alice = params.spawnTxBuilderAlice(wallet) alice ! Start(probe.ref) @@ -2482,7 +2639,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit test("too many inputs") { val probe = TestProbe() val wallet = new SingleKeyOnChainWallet() - val params = createFixtureParams(100_000 sat, 0 sat, FeeratePerKw(5000 sat), 330 sat, 0) + val params = createFixtureParams(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(), 100_000 sat, 0 sat, FeeratePerKw(5000 sat), 330 sat, 0) val alice = params.spawnTxBuilderAlice(wallet) alice ! Start(probe.ref) (1 to 252).foreach(i => { @@ -2499,7 +2656,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit test("too many outputs") { val probe = TestProbe() val wallet = new SingleKeyOnChainWallet() - val params = createFixtureParams(100_000 sat, 0 sat, FeeratePerKw(5000 sat), 330 sat, 0) + val params = createFixtureParams(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(), 100_000 sat, 0 sat, FeeratePerKw(5000 sat), 330 sat, 0) val validScript = Script.write(Script.pay2wpkh(randomKey().publicKey)) val alice = params.spawnTxBuilderAlice(wallet) alice ! Start(probe.ref) @@ -2517,7 +2674,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit test("missing funding output") { val probe = TestProbe() val wallet = new SingleKeyOnChainWallet() - val params = createFixtureParams(100_000 sat, 0 sat, FeeratePerKw(5000 sat), 330 sat, 0) + val params = createFixtureParams(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(), 100_000 sat, 0 sat, FeeratePerKw(5000 sat), 330 sat, 0) val validScript = Script.write(Script.pay2wpkh(randomKey().publicKey)) val bob = params.spawnTxBuilderBob(wallet) bob ! Start(probe.ref) @@ -2537,7 +2694,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit test("multiple funding outputs") { val probe = TestProbe() val wallet = new SingleKeyOnChainWallet() - val params = createFixtureParams(100_000 sat, 0 sat, FeeratePerKw(5000 sat), 330 sat, 0) + val params = createFixtureParams(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(), 100_000 sat, 0 sat, FeeratePerKw(5000 sat), 330 sat, 0) val bob = params.spawnTxBuilderBob(wallet) bob ! Start(probe.ref) // Alice --- tx_add_input --> Bob @@ -2560,7 +2717,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit test("missing shared input") { val probe = TestProbe() val wallet = new SingleKeyOnChainWallet() - val params = createFixtureParams(100_000 sat, 0 sat, FeeratePerKw(1000 sat), 330 sat, 0) + val params = createFixtureParams(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(), 100_000 sat, 0 sat, FeeratePerKw(1000 sat), 330 sat, 0) val commitment = CommitmentsSpec.makeCommitments(250_000_000 msat, 150_000_000 msat).active.head val fundingParamsB = params.fundingParamsB.copy(sharedInput_opt = Some(params.dummySharedInputB(commitment.capacity))) val bob = params.spawnTxBuilderSpliceBob(fundingParamsB, commitment, wallet) @@ -2581,7 +2738,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit test("invalid funding amount") { val probe = TestProbe() val wallet = new SingleKeyOnChainWallet() - val params = createFixtureParams(100_000 sat, 0 sat, FeeratePerKw(5000 sat), 330 sat, 0) + val params = createFixtureParams(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(), 100_000 sat, 0 sat, FeeratePerKw(5000 sat), 330 sat, 0) val bob = params.spawnTxBuilderBob(wallet) bob ! Start(probe.ref) // Alice --- tx_add_input --> Bob @@ -2596,9 +2753,9 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit test("missing previous tx") { val probe = TestProbe() val wallet = new SingleKeyOnChainWallet() - val params = createFixtureParams(100_000 sat, 0 sat, FeeratePerKw(5000 sat), 330 sat, 0) + val params = createFixtureParams(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(), 100_000 sat, 0 sat, FeeratePerKw(5000 sat), 330 sat, 0) val previousCommitment = CommitmentsSpec.makeCommitments(25_000_000 msat, 50_000_000 msat).active.head - val fundingParams = params.fundingParamsB.copy(sharedInput_opt = Some(Multisig2of2Input(previousCommitment.commitInput, 0, randomKey().publicKey))) + val fundingParams = params.fundingParamsB.copy(sharedInput_opt = Some(SharedFundingInput(previousCommitment.commitInput(params.channelKeysB), 0, randomKey().publicKey, previousCommitment.commitmentFormat))) val bob = params.spawnTxBuilderSpliceBob(fundingParams, previousCommitment, wallet) bob ! Start(probe.ref) // Alice --- tx_add_input --> Bob @@ -2608,13 +2765,27 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit assert(probe.expectMsgType[RemoteFailure].cause == PreviousTxMissing(params.channelId, UInt64(0))) } + test("previous txOut not allowed for non-taproot channels") { + val probe = TestProbe() + val wallet = new SingleKeyOnChainWallet() + val params = createFixtureParams(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(), 100_000 sat, 0 sat, FeeratePerKw(5000 sat), 330 sat, 0) + val previousCommitment = CommitmentsSpec.makeCommitments(25_000_000 msat, 50_000_000 msat).active.head + val fundingParams = params.fundingParamsB.copy(sharedInput_opt = Some(SharedFundingInput(previousCommitment.commitInput(params.channelKeysB), 0, randomKey().publicKey, previousCommitment.commitmentFormat))) + val bob = params.spawnTxBuilderSpliceBob(fundingParams, previousCommitment, wallet) + bob ! Start(probe.ref) + // Alice --- tx_add_input --> Bob + // The input only includes the previous txOut which is only allowed for taproot channels. + bob ! ReceiveMessage(TxAddInput(params.channelId, UInt64(0), None, 0, 0, TlvStream(TxAddInputTlv.PrevTxOut(randomTxId(), 100_000 sat, Script.write(Script.pay2tr(randomKey().xOnlyPublicKey())))))) + assert(probe.expectMsgType[RemoteFailure].cause == PreviousTxMissing(params.channelId, UInt64(0))) + } + test("invalid shared input") { val probe = TestProbe() val wallet = new SingleKeyOnChainWallet() - val params = createFixtureParams(100_000 sat, 0 sat, FeeratePerKw(5000 sat), 330 sat, 0) + val params = createFixtureParams(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(), 100_000 sat, 0 sat, FeeratePerKw(5000 sat), 330 sat, 0) val previousCommitment = CommitmentsSpec.makeCommitments(25_000_000 msat, 50_000_000 msat).active.head val fundingTx = Transaction(2, Nil, Seq(TxOut(50_000 sat, Script.pay2wpkh(randomKey().publicKey)), TxOut(20_000 sat, Script.pay2wpkh(randomKey().publicKey))), 0) - val sharedInput = Multisig2of2Input(InputInfo(OutPoint(fundingTx, 0), fundingTx.txOut.head, Nil), 0, randomKey().publicKey) + val sharedInput = SharedFundingInput(InputInfo(OutPoint(fundingTx, 0), fundingTx.txOut.head), 0, randomKey().publicKey, previousCommitment.commitmentFormat) val bob = params.spawnTxBuilderSpliceBob(params.fundingParamsB.copy(sharedInput_opt = Some(sharedInput)), previousCommitment, wallet) bob ! Start(probe.ref) // Alice --- tx_add_input --> Bob @@ -2627,7 +2798,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit test("total input amount too low") { val probe = TestProbe() val wallet = new SingleKeyOnChainWallet() - val params = createFixtureParams(100_000 sat, 0 sat, FeeratePerKw(5000 sat), 330 sat, 0) + val params = createFixtureParams(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(), 100_000 sat, 0 sat, FeeratePerKw(5000 sat), 330 sat, 0) val validScript = Script.write(Script.pay2wpkh(randomKey().publicKey)) val bob = params.spawnTxBuilderBob(wallet) bob ! Start(probe.ref) @@ -2651,7 +2822,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit test("minimum fee not met") { val probe = TestProbe() val wallet = new SingleKeyOnChainWallet() - val params = createFixtureParams(100_000 sat, 0 sat, FeeratePerKw(5000 sat), 330 sat, 0) + val params = createFixtureParams(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(), 100_000 sat, 0 sat, FeeratePerKw(5000 sat), 330 sat, 0) val validScript = Script.write(Script.pay2wpkh(randomKey().publicKey)) val bob = params.spawnTxBuilderBob(wallet) bob ! Start(probe.ref) @@ -2676,7 +2847,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit val targetFeerate = FeeratePerKw(7500 sat) val fundingA = 85_000 sat val utxosA = Seq(120_000 sat) - withFixture(fundingA, utxosA, 0 sat, Nil, targetFeerate, 660 sat, 0, RequireConfirmedInputs(forLocal = false, forRemote = false)) { f => + withFixture(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(), fundingA, utxosA, 0 sat, Nil, targetFeerate, 660 sat, 0, RequireConfirmedInputs(forLocal = false, forRemote = false)) { f => import f._ alice ! Start(alice2bob.ref) @@ -2735,32 +2906,63 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit } test("invalid commit_sig") { - val probe = TestProbe() + val (alice2bob, bob2alice) = (TestProbe(), TestProbe()) val wallet = new SingleKeyOnChainWallet() - val params = createFixtureParams(100_000 sat, 0 sat, FeeratePerKw(5000 sat), 330 sat, 0) + val params = createFixtureParams(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(), 100_000 sat, 25_000 sat, FeeratePerKw(5000 sat), 330 sat, 0) val alice = params.spawnTxBuilderAlice(wallet) - alice ! Start(probe.ref) + val bob = params.spawnTxBuilderBob(wallet) + alice ! Start(alice2bob.ref) + bob ! Start(bob2alice.ref) // Alice --- tx_add_input --> Bob - probe.expectMsgType[SendMessage] - alice ! ReceiveMessage(TxComplete(params.channelId)) + bob ! ReceiveMessage(alice2bob.expectMsgType[SendMessage].msg.asInstanceOf[TxAddInput]) + alice ! ReceiveMessage(bob2alice.expectMsgType[SendMessage].msg.asInstanceOf[TxAddInput]) // Alice --- tx_add_output --> Bob - probe.expectMsgType[SendMessage] - alice ! ReceiveMessage(TxComplete(params.channelId)) + bob ! ReceiveMessage(alice2bob.expectMsgType[SendMessage].msg.asInstanceOf[TxAddOutput]) + alice ! ReceiveMessage(bob2alice.expectMsgType[SendMessage].msg.asInstanceOf[TxAddOutput]) // Alice --- tx_add_output --> Bob - probe.expectMsgType[SendMessage] - alice ! ReceiveMessage(TxComplete(params.channelId)) + bob ! ReceiveMessage(alice2bob.expectMsgType[SendMessage].msg.asInstanceOf[TxAddOutput]) + alice ! ReceiveMessage(bob2alice.expectMsgType[SendMessage].msg.asInstanceOf[TxComplete]) + // Alice --- tx_complete --> Bob + bob ! ReceiveMessage(alice2bob.expectMsgType[SendMessage].msg.asInstanceOf[TxComplete]) + // Alice <-- commit_sig --- Bob + val successA1 = alice2bob.expectMsgType[Succeeded] + val invalidCommitSig = CommitSig(params.channelId, IndividualSignature(ByteVector64.Zeroes), Nil) + val Left(error) = successA1.signingSession.receiveCommitSig(params.channelParamsA, params.channelKeysA, invalidCommitSig, params.nodeParamsA.currentBlockHeight)(akka.event.NoLogging) + assert(error.isInstanceOf[InvalidCommitmentSignature]) + } + + test("invalid commit_sig (taproot)") { + val (alice2bob, bob2alice) = (TestProbe(), TestProbe()) + val wallet = new SingleKeyOnChainWallet() + val params = createFixtureParams(ChannelTypes.SimpleTaprootChannelsPhoenix, 100_000 sat, 25_000 sat, FeeratePerKw(5000 sat), 330 sat, 0) + val alice = params.spawnTxBuilderAlice(wallet) + val bob = params.spawnTxBuilderBob(wallet) + alice ! Start(alice2bob.ref) + bob ! Start(bob2alice.ref) + // Alice --- tx_add_input --> Bob + bob ! ReceiveMessage(alice2bob.expectMsgType[SendMessage].msg.asInstanceOf[TxAddInput]) + alice ! ReceiveMessage(bob2alice.expectMsgType[SendMessage].msg.asInstanceOf[TxAddInput]) + // Alice --- tx_add_output --> Bob + bob ! ReceiveMessage(alice2bob.expectMsgType[SendMessage].msg.asInstanceOf[TxAddOutput]) + alice ! ReceiveMessage(bob2alice.expectMsgType[SendMessage].msg.asInstanceOf[TxAddOutput]) + // Alice --- tx_add_output --> Bob + bob ! ReceiveMessage(alice2bob.expectMsgType[SendMessage].msg.asInstanceOf[TxAddOutput]) + val txCompleteBob = bob2alice.expectMsgType[SendMessage].msg.asInstanceOf[TxComplete] + assert(txCompleteBob.commitNonces_opt.nonEmpty) + alice ! ReceiveMessage(txCompleteBob) // Alice --- tx_complete --> Bob - assert(probe.expectMsgType[SendMessage].msg.isInstanceOf[TxComplete]) + bob ! ReceiveMessage(alice2bob.expectMsgType[SendMessage].msg.asInstanceOf[TxComplete]) // Alice <-- commit_sig --- Bob - val signingA = probe.expectMsgType[Succeeded].signingSession - val Left(error) = signingA.receiveCommitSig(params.nodeParamsA, params.channelParamsA, CommitSig(params.channelId, ByteVector64.Zeroes, Nil))(akka.event.NoLogging) + val successA1 = alice2bob.expectMsgType[Succeeded] + val invalidCommitSig = CommitSig(params.channelId, PartialSignatureWithNonce(randomBytes32(), txCompleteBob.commitNonces_opt.get.commitNonce), Nil, batchSize = 1) + val Left(error) = successA1.signingSession.receiveCommitSig(params.channelParamsA, params.channelKeysA, invalidCommitSig, params.nodeParamsA.currentBlockHeight)(akka.event.NoLogging) assert(error.isInstanceOf[InvalidCommitmentSignature]) } test("receive tx_signatures before commit_sig") { val (alice2bob, bob2alice) = (TestProbe(), TestProbe()) val wallet = new SingleKeyOnChainWallet() - val params = createFixtureParams(100_000 sat, 0 sat, FeeratePerKw(5000 sat), 330 sat, 0) + val params = createFixtureParams(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(), 100_000 sat, 0 sat, FeeratePerKw(5000 sat), 330 sat, 0) val alice = params.spawnTxBuilderAlice(wallet) val bob = params.spawnTxBuilderBob(wallet) alice ! Start(alice2bob.ref) @@ -2779,14 +2981,14 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit // Alice <-- tx_signatures --- Bob val signingA = alice2bob.expectMsgType[Succeeded].signingSession val signingB = bob2alice.expectMsgType[Succeeded].signingSession - val Left(error) = signingA.receiveTxSigs(params.nodeParamsA, params.channelParamsA, signingB.fundingTx.localSigs)(akka.event.NoLogging) + val Left(error) = signingA.receiveTxSigs(params.channelKeysA, signingB.fundingTx.localSigs, params.nodeParamsA.currentBlockHeight)(akka.event.NoLogging) assert(error == UnexpectedFundingSignatures(params.channelId)) } test("invalid tx_signatures") { val (alice2bob, bob2alice) = (TestProbe(), TestProbe()) val wallet = new SingleKeyOnChainWallet() - val params = createFixtureParams(100_000 sat, 25_000 sat, FeeratePerKw(5000 sat), 330 sat, 0) + val params = createFixtureParams(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(), 100_000 sat, 25_000 sat, FeeratePerKw(5000 sat), 330 sat, 0) val alice = params.spawnTxBuilderAlice(wallet) val bob = params.spawnTxBuilderBob(wallet) alice ! Start(alice2bob.ref) @@ -2805,9 +3007,9 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit // Alice <-- commit_sig --- Bob val successA1 = alice2bob.expectMsgType[Succeeded] val successB1 = bob2alice.expectMsgType[Succeeded] - val Right(signingA2: InteractiveTxSigningSession.WaitingForSigs) = successA1.signingSession.receiveCommitSig(params.nodeParamsA, params.channelParamsA, successB1.commitSig)(akka.event.NoLogging) + val Right(signingA2: InteractiveTxSigningSession.WaitingForSigs) = successA1.signingSession.receiveCommitSig(params.channelParamsA, params.channelKeysA, successB1.commitSig, params.nodeParamsA.currentBlockHeight)(akka.event.NoLogging) // Alice <-- tx_signatures --- Bob - val Left(error) = signingA2.receiveTxSigs(params.nodeParamsA, params.channelParamsA, successB1.signingSession.fundingTx.localSigs.copy(witnesses = Seq(Script.witnessPay2wpkh(randomKey().publicKey, ByteVector.fill(73)(0)))))(akka.event.NoLogging) + val Left(error) = signingA2.receiveTxSigs(params.channelKeysA, successB1.signingSession.fundingTx.localSigs.copy(witnesses = Seq(Script.witnessPay2wpkh(randomKey().publicKey, ByteVector.fill(73)(0)))), params.nodeParamsA.currentBlockHeight)(akka.event.NoLogging) assert(error.isInstanceOf[InvalidFundingSignature]) } @@ -2856,4 +3058,4 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit class InteractiveTxBuilderWithEclairSignerSpec extends InteractiveTxBuilderSpec { override def useEclairSigner = true -} \ No newline at end of file +} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/RestoreSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/RestoreSpec.scala index d8e8c9acec..db69a0d1c6 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/RestoreSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/RestoreSpec.scala @@ -4,16 +4,14 @@ import akka.actor.ActorRef import akka.actor.typed.scaladsl.adapter.actorRefAdapter import akka.testkit.{TestActor, TestFSMRef, TestProbe} import com.softwaremill.quicklens.ModifyPimp -import fr.acinq.bitcoin.scalacompat._ import fr.acinq.eclair.TestConstants.{Alice, Bob} -import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.WatchFundingSpentTriggered import fr.acinq.eclair.channel.fsm.Channel -import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags} +import fr.acinq.eclair.channel.states.ChannelStateTestsBase import fr.acinq.eclair.channel.states.ChannelStateTestsBase.FakeTxPublisherFactory import fr.acinq.eclair.router.Announcements -import fr.acinq.eclair.wire.protocol.{ChannelReestablish, ChannelUpdate, CommitSig, Error, Init, RevokeAndAck} +import fr.acinq.eclair.wire.protocol.{ChannelReestablish, ChannelUpdate, Init} import fr.acinq.eclair.{TestKitBaseClass, _} -import org.scalatest.{Outcome, Tag} +import org.scalatest.Outcome import org.scalatest.funsuite.FixtureAnyFunSuiteLike import scala.concurrent.duration._ @@ -30,71 +28,8 @@ class RestoreSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Chan } } - private def aliceInit = Init(Alice.nodeParams.features.initFeatures()) - - private def bobInit = Init(Bob.nodeParams.features.initFeatures()) - - test("use funding pubkeys from publish commitment to spend our output", Tag(ChannelStateTestsTags.StaticRemoteKey)) { f => - import f._ - val sender = TestProbe() - - // we start by storing the current state - val oldStateData = alice.stateData.asInstanceOf[PersistentChannelData] - // then we add an htlc and sign it - addHtlc(250000000 msat, alice, bob, alice2bob, bob2alice) - sender.send(alice, CMD_SIGN()) - alice2bob.expectMsgType[CommitSig] - alice2bob.forward(bob) - // alice will receive neither the revocation nor the commit sig - bob2alice.expectMsgType[RevokeAndAck] - bob2alice.expectMsgType[CommitSig] - - // we simulate a disconnection - sender.send(alice, INPUT_DISCONNECTED) - sender.send(bob, INPUT_DISCONNECTED) - awaitCond(alice.stateName == OFFLINE) - awaitCond(bob.stateName == OFFLINE) - - // and we terminate Alice - alice.stop() - - // we restart Alice - val newAlice: TestFSMRef[ChannelState, ChannelData, Channel] = TestFSMRef(new Channel(Alice.nodeParams, wallet, Bob.nodeParams.nodeId, alice2blockchain.ref, alice2relayer.ref, FakeTxPublisherFactory(alice2blockchain)), alicePeer.ref) - newAlice ! INPUT_RESTORED(oldStateData) - - // then we reconnect them - sender.send(newAlice, INPUT_RECONNECTED(alice2bob.ref, aliceInit, bobInit)) - sender.send(bob, INPUT_RECONNECTED(bob2alice.ref, bobInit, aliceInit)) - - // peers exchange channel_reestablish messages - alice2bob.expectMsgType[ChannelReestablish] - val ce = bob2alice.expectMsgType[ChannelReestablish] - - // alice then realizes it has an old state... - bob2alice.forward(newAlice) - // ... and ask bob to publish its current commitment - val error = alice2bob.expectMsgType[Error] - assert(new String(error.data.toArray) == PleasePublishYourCommitment(channelId(newAlice)).getMessage) - - // alice now waits for bob to publish its commitment - awaitCond(newAlice.stateName == WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT) - - // bob is nice and publishes its commitment - val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.fullySignedLocalCommitTx(bob.underlyingActor.nodeParams.channelKeyManager).tx - - // actual tests starts here: let's see what we can do with Bob's commit tx - sender.send(newAlice, WatchFundingSpentTriggered(bobCommitTx)) - - // from Bob's commit tx we can extract both funding public keys - val OP_2 :: OP_PUSHDATA(pub1, _) :: OP_PUSHDATA(pub2, _) :: OP_2 :: OP_CHECKMULTISIG :: Nil = Script.parse(bobCommitTx.txIn(0).witness.stack.last) - // from Bob's commit tx we can also extract our p2wpkh output - val ourOutput = bobCommitTx.txOut.find(_.publicKeyScript.length == 22).get - val OP_0 :: OP_PUSHDATA(pubKeyHash, _) :: Nil = Script.parse(ourOutput.publicKeyScript) - - // check that our output in Bob's commit tx sends to our static payment point - val Some(ourStaticPaymentPoint) = oldStateData.asInstanceOf[DATA_NORMAL].commitments.params.localParams.walletStaticPaymentBasepoint - assert(pubKeyHash == ourStaticPaymentPoint.hash160) - } + private val aliceInit = Init(Alice.nodeParams.features.initFeatures()) + private val bobInit = Init(Bob.nodeParams.features.initFeatures()) /** We are only interested in channel updates from Alice, we use the channel flag to discriminate */ def aliceChannelUpdateListener(channelUpdateListener: TestProbe): TestProbe = { @@ -134,7 +69,7 @@ class RestoreSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Chan alice.stop() // we restart Alice - val newAlice: TestFSMRef[ChannelState, ChannelData, Channel] = TestFSMRef(new Channel(Alice.nodeParams, wallet, Bob.nodeParams.nodeId, alice2blockchain.ref, alice2relayer.ref, FakeTxPublisherFactory(alice2blockchain)), alicePeer.ref) + val newAlice: TestFSMRef[ChannelState, ChannelData, Channel] = TestFSMRef(new Channel(Alice.nodeParams, Alice.channelKeys(), aliceWallet, Bob.nodeParams.nodeId, alice2blockchain.ref, alice2relayer.ref, FakeTxPublisherFactory(alice2blockchain)), alicePeer.ref) newAlice ! INPUT_RESTORED(oldStateData) newAlice ! INPUT_RECONNECTED(alice2bob.ref, aliceInit, bobInit) @@ -186,8 +121,7 @@ class RestoreSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Chan .modify(_.relayParams.privateChannelFees.feeProportionalMillionths).setTo(2345) .modify(_.channelConf.expiryDelta).setTo(CltvExpiryDelta(147)), ) foreach { newConfig => - - val newAlice: TestFSMRef[ChannelState, ChannelData, Channel] = TestFSMRef(new Channel(newConfig, wallet, Bob.nodeParams.nodeId, alice2blockchain.ref, alice2relayer.ref, FakeTxPublisherFactory(alice2blockchain)), alicePeer.ref) + val newAlice: TestFSMRef[ChannelState, ChannelData, Channel] = TestFSMRef(new Channel(newConfig, Alice.channelKeys(), aliceWallet, Bob.nodeParams.nodeId, alice2blockchain.ref, alice2relayer.ref, FakeTxPublisherFactory(alice2blockchain)), alicePeer.ref) newAlice ! INPUT_RESTORED(oldStateData) val u1 = channelUpdateListener.expectMsgType[ChannelUpdateParametersChanged] diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/FinalTxPublisherSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/FinalTxPublisherSpec.scala index 9bcd0441eb..b97e28e399 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/FinalTxPublisherSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/FinalTxPublisherSpec.scala @@ -79,7 +79,7 @@ class FinalTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike with Bi createBlocks(5, probe) val tx = createSpendP2WPKH(parentTx, priv, priv.publicKey, 2_500 sat, sequence = 5, lockTime = 0) - val cmd = PublishFinalTx(tx, tx.txIn.head.outPoint, 125_000 sat, "tx-time-locks", 0 sat, None) + val cmd = PublishFinalTx(tx, tx.txIn.head.outPoint, "tx-time-locks", 0 sat, None) publisher ! Publish(probe.ref, cmd) // Time locks are satisfied, the transaction should be published: @@ -103,7 +103,7 @@ class FinalTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike with Bi val ancestorTx = sendToAddress(address, 125_000 sat, probe) val parentTx = createSpendP2WPKH(ancestorTx, priv, priv.publicKey, 2_500 sat, 0, 0) val tx = createSpendP2WPKH(parentTx, priv, priv.publicKey, 2_000 sat, 0, 0) - val cmd = PublishFinalTx(tx, tx.txIn.head.outPoint, 125_000 sat, "tx-with-parent", 10 sat, Some(parentTx.txid)) + val cmd = PublishFinalTx(tx, tx.txIn.head.outPoint, "tx-with-parent", 10 sat, Some(parentTx.txid)) publisher ! Publish(probe.ref, cmd) // Since the parent is not published yet, we can't publish the child tx either: @@ -125,7 +125,7 @@ class FinalTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike with Bi val (priv, address) = createExternalAddress() val parentTx = sendToAddress(address, 125_000 sat, probe) val tx1 = createSpendP2WPKH(parentTx, priv, priv.publicKey, 2_500 sat, 0, 0) - val cmd = PublishFinalTx(tx1, tx1.txIn.head.outPoint, 125_000 sat, "tx-time-locks", 10 sat, None) + val cmd = PublishFinalTx(tx1, tx1.txIn.head.outPoint, "tx-time-locks", 10 sat, None) publisher ! Publish(probe.ref, cmd) waitTxInMempool(bitcoinClient, tx1.txid, probe) @@ -150,7 +150,7 @@ class FinalTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike with Bi import f._ val tx = sendToAddress(getNewAddress(probe), 125_000 sat, probe) - val cmd = PublishFinalTx(tx, tx.txIn.head.outPoint, 125_000 sat, "final-tx", 10 sat, None) + val cmd = PublishFinalTx(tx, tx.txIn.head.outPoint, "final-tx", 10 sat, None) publisher ! Publish(probe.ref, cmd) probe.watch(publisher.toClassic) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxFunderSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxFunderSpec.scala deleted file mode 100644 index 40d369925e..0000000000 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxFunderSpec.scala +++ /dev/null @@ -1,254 +0,0 @@ -/* - * Copyright 2021 ACINQ SAS - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package fr.acinq.eclair.channel.publish - -import fr.acinq.bitcoin.scalacompat.{Crypto, OutPoint, SatoshiLong, Script, Transaction, TxIn, TxOut} -import fr.acinq.eclair.TestUtils.randomTxId -import fr.acinq.eclair.blockchain.fee.{ConfirmationTarget, FeeratePerKw} -import fr.acinq.eclair.channel.Helpers.Funding -import fr.acinq.eclair.channel._ -import fr.acinq.eclair.channel.publish.ReplaceableTxFunder.AdjustPreviousTxOutputResult.{AddWalletInputs, TxOutputAdjusted} -import fr.acinq.eclair.channel.publish.ReplaceableTxFunder._ -import fr.acinq.eclair.channel.publish.ReplaceableTxPrePublisher._ -import fr.acinq.eclair.transactions.Scripts -import fr.acinq.eclair.transactions.Transactions._ -import fr.acinq.eclair.{BlockHeight, CltvExpiry, TestKitBaseClass, randomBytes32} -import org.mockito.IdiomaticMockito.StubbingOps -import org.mockito.MockitoSugar.mock -import org.scalatest.Tag -import org.scalatest.funsuite.AnyFunSuiteLike -import scodec.bits.ByteVector - -import scala.util.Random - -class ReplaceableTxFunderSpec extends TestKitBaseClass with AnyFunSuiteLike { - - private def createAnchorTx(): (CommitTx, ClaimLocalAnchorOutputTx) = { - val anchorScript = Scripts.anchor(PlaceHolderPubKey) - val commitInput = Funding.makeFundingInputInfo(randomTxId(), 1, 500 sat, PlaceHolderPubKey, PlaceHolderPubKey) - val commitTx = Transaction( - 2, - Seq(TxIn(commitInput.outPoint, commitInput.redeemScript, 0, Scripts.witness2of2(PlaceHolderSig, PlaceHolderSig, PlaceHolderPubKey, PlaceHolderPubKey))), - Seq(TxOut(330 sat, Script.pay2wsh(anchorScript))), - 0 - ) - val anchorTx = ClaimLocalAnchorOutputTx( - InputInfo(OutPoint(commitTx, 0), commitTx.txOut.head, anchorScript), - Transaction(2, Seq(TxIn(OutPoint(commitTx, 0), ByteVector.empty, 0)), Nil, 0), - ConfirmationTarget.Absolute(BlockHeight(0)) - ) - (CommitTx(commitInput, commitTx), anchorTx) - } - - private def createHtlcTxs(): (Transaction, HtlcSuccessWithWitnessData, HtlcTimeoutWithWitnessData) = { - val preimage = randomBytes32() - val paymentHash = Crypto.sha256(preimage) - val htlcSuccessScript = Scripts.htlcReceived(PlaceHolderPubKey, PlaceHolderPubKey, PlaceHolderPubKey, paymentHash, CltvExpiry(0), ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) - val htlcTimeoutScript = Scripts.htlcOffered(PlaceHolderPubKey, PlaceHolderPubKey, PlaceHolderPubKey, randomBytes32(), ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) - val commitTx = Transaction( - 2, - Seq(TxIn(OutPoint(randomTxId(), 1), Script.write(Script.pay2wpkh(PlaceHolderPubKey)), 0, Script.witnessPay2wpkh(PlaceHolderPubKey, PlaceHolderSig))), - Seq(TxOut(5000 sat, Script.pay2wsh(htlcSuccessScript)), TxOut(4000 sat, Script.pay2wsh(htlcTimeoutScript))), - 0 - ) - val htlcSuccess = HtlcSuccessWithWitnessData(HtlcSuccessTx( - InputInfo(OutPoint(commitTx, 0), commitTx.txOut.head, htlcSuccessScript), - Transaction(2, Seq(TxIn(OutPoint(commitTx, 0), ByteVector.empty, 0)), Seq(TxOut(5000 sat, Script.pay2wpkh(PlaceHolderPubKey))), 0), - paymentHash, - 17, - ConfirmationTarget.Absolute(BlockHeight(0)) - ), PlaceHolderSig, preimage) - val htlcTimeout = HtlcTimeoutWithWitnessData(HtlcTimeoutTx( - InputInfo(OutPoint(commitTx, 1), commitTx.txOut.last, htlcTimeoutScript), - Transaction(2, Seq(TxIn(OutPoint(commitTx, 1), ByteVector.empty, 0)), Seq(TxOut(4000 sat, Script.pay2wpkh(PlaceHolderPubKey))), 0), - 12, - ConfirmationTarget.Absolute(BlockHeight(0)) - ), PlaceHolderSig) - (commitTx, htlcSuccess, htlcTimeout) - } - - private def createClaimHtlcTx(): (Transaction, ClaimHtlcSuccessWithWitnessData, ClaimHtlcTimeoutWithWitnessData) = { - val preimage = randomBytes32() - val paymentHash = Crypto.sha256(preimage) - val htlcSuccessScript = Scripts.htlcReceived(PlaceHolderPubKey, PlaceHolderPubKey, PlaceHolderPubKey, paymentHash, CltvExpiry(0), ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) - val htlcTimeoutScript = Scripts.htlcOffered(PlaceHolderPubKey, PlaceHolderPubKey, PlaceHolderPubKey, randomBytes32(), ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) - val commitTx = Transaction( - 2, - Seq(TxIn(OutPoint(randomTxId(), 1), Script.write(Script.pay2wpkh(PlaceHolderPubKey)), 0, Script.witnessPay2wpkh(PlaceHolderPubKey, PlaceHolderSig))), - Seq(TxOut(5000 sat, Script.pay2wsh(htlcSuccessScript)), TxOut(5000 sat, Script.pay2wsh(htlcTimeoutScript))), - 0 - ) - val claimHtlcSuccess = ClaimHtlcSuccessWithWitnessData(ClaimHtlcSuccessTx( - InputInfo(OutPoint(commitTx, 0), commitTx.txOut.head, htlcSuccessScript), - Transaction(2, Seq(TxIn(OutPoint(commitTx, 0), ByteVector.empty, 0)), Seq(TxOut(5000 sat, Script.pay2wpkh(PlaceHolderPubKey))), 0), - paymentHash, - 5, - ConfirmationTarget.Absolute(BlockHeight(0)) - ), preimage) - val claimHtlcTimeout = ClaimHtlcTimeoutWithWitnessData(ClaimHtlcTimeoutTx( - InputInfo(OutPoint(commitTx, 1), commitTx.txOut.last, htlcTimeoutScript), - Transaction(2, Seq(TxIn(OutPoint(commitTx, 1), ByteVector.empty, 0)), Seq(TxOut(5000 sat, Script.pay2wpkh(PlaceHolderPubKey))), 0), - 7, - ConfirmationTarget.Absolute(BlockHeight(0)) - )) - (commitTx, claimHtlcSuccess, claimHtlcTimeout) - } - - test("adjust claim htlc tx change amount") { - val dustLimit = 750 sat - val (_, claimHtlcSuccess, claimHtlcTimeout) = createClaimHtlcTx() - for (claimHtlc <- Seq(claimHtlcSuccess, claimHtlcTimeout)) { - var previousAmount = claimHtlc.txInfo.tx.txOut.head.amount - for (i <- 1 to 100) { - val targetFeerate = FeeratePerKw(250 * i sat) - adjustClaimHtlcTxOutput(claimHtlc, targetFeerate, dustLimit) match { - case Left(_) => assert(targetFeerate >= FeeratePerKw(7000 sat)) - case Right(updatedClaimHtlc) => - assert(updatedClaimHtlc.txInfo.tx.txIn.length == 1) - assert(updatedClaimHtlc.txInfo.tx.txOut.length == 1) - assert(updatedClaimHtlc.txInfo.tx.txOut.head.amount < previousAmount) - previousAmount = updatedClaimHtlc.txInfo.tx.txOut.head.amount - val signedTx = updatedClaimHtlc match { - case ClaimHtlcSuccessWithWitnessData(txInfo, preimage) => addSigs(txInfo, PlaceHolderSig, preimage) - case ClaimHtlcTimeoutWithWitnessData(txInfo) => addSigs(txInfo, PlaceHolderSig) - case _: LegacyClaimHtlcSuccessWithWitnessData => fail("legacy claim htlc success not supported") - } - val txFeerate = fee2rate(signedTx.fee, signedTx.tx.weight()) - assert(targetFeerate * 0.9 <= txFeerate && txFeerate <= targetFeerate * 1.1, s"actualFeerate=$txFeerate targetFeerate=$targetFeerate") - } - } - } - } - - test("adjust previous anchor transaction outputs") { - val (commitTx, initialAnchorTx) = createAnchorTx() - val previousAnchorTx = ClaimLocalAnchorWithWitnessData(initialAnchorTx).updateTx(initialAnchorTx.tx.copy( - txIn = Seq( - initialAnchorTx.tx.txIn.head, - // The previous funding attempt added two wallet inputs: - TxIn(OutPoint(randomTxId(), 3), ByteVector.empty, 0, Script.witnessPay2wpkh(PlaceHolderPubKey, PlaceHolderSig)), - TxIn(OutPoint(randomTxId(), 1), ByteVector.empty, 0, Script.witnessPay2wpkh(PlaceHolderPubKey, PlaceHolderSig)) - ), - // And a change output: - txOut = Seq(TxOut(5000 sat, Script.pay2wpkh(PlaceHolderPubKey))) - )) - - val commitment = mock[FullCommitment] - val localParams = mock[LocalParams] - localParams.dustLimit.returns(1000 sat) - commitment.localParams.returns(localParams) - val localCommit = mock[LocalCommit] - localCommit.commitTxAndRemoteSig.returns(CommitTxAndRemoteSig(commitTx, PlaceHolderSig)) - commitment.localCommit.returns(localCommit) - - // We can handle a small feerate update by lowering the change output. - val TxOutputAdjusted(feerateUpdate1) = adjustPreviousTxOutput(FundedTx(previousAnchorTx, 12000 sat, FeeratePerKw(2500 sat), Map.empty), FeeratePerKw(5000 sat), commitment, commitTx.tx) - assert(feerateUpdate1.txInfo.tx.txIn == previousAnchorTx.txInfo.tx.txIn) - assert(feerateUpdate1.txInfo.tx.txOut.length == 1) - val TxOutputAdjusted(feerateUpdate2) = adjustPreviousTxOutput(FundedTx(previousAnchorTx, 12000 sat, FeeratePerKw(2500 sat), Map.empty), FeeratePerKw(6000 sat), commitment, commitTx.tx) - assert(feerateUpdate2.txInfo.tx.txIn == previousAnchorTx.txInfo.tx.txIn) - assert(feerateUpdate2.txInfo.tx.txOut.length == 1) - assert(feerateUpdate2.txInfo.tx.txOut.head.amount < feerateUpdate1.txInfo.tx.txOut.head.amount) - - // But if the feerate increase is too large, we must add new wallet inputs. - val AddWalletInputs(previousTx) = adjustPreviousTxOutput(FundedTx(previousAnchorTx, 12000 sat, FeeratePerKw(2500 sat), Map.empty), FeeratePerKw(10000 sat), commitment, commitTx.tx) - assert(previousTx == previousAnchorTx) - } - - test("adjust previous htlc transaction outputs", Tag("fuzzy")) { - val commitment = mock[FullCommitment] - val localParams = mock[LocalParams] - localParams.dustLimit.returns(600 sat) - commitment.localParams.returns(localParams) - val (commitTx, initialHtlcSuccess, initialHtlcTimeout) = createHtlcTxs() - for (initialHtlcTx <- Seq(initialHtlcSuccess, initialHtlcTimeout)) { - val previousTx = initialHtlcTx.updateTx(initialHtlcTx.txInfo.tx.copy( - txIn = Seq( - initialHtlcTx.txInfo.tx.txIn.head, - // The previous funding attempt added three wallet inputs: - TxIn(OutPoint(randomTxId(), 3), ByteVector.empty, 0, Script.witnessPay2wpkh(PlaceHolderPubKey, PlaceHolderSig)), - TxIn(OutPoint(randomTxId(), 1), ByteVector.empty, 0, Script.witnessPay2wpkh(PlaceHolderPubKey, PlaceHolderSig)), - TxIn(OutPoint(randomTxId(), 5), ByteVector.empty, 0, Script.witnessPay2wpkh(PlaceHolderPubKey, PlaceHolderSig)) - ), - txOut = Seq( - initialHtlcTx.txInfo.tx.txOut.head, - // And one change output: - TxOut(5000 sat, Script.pay2wpkh(PlaceHolderPubKey)) - ) - )) - - // We can handle a small feerate update by lowering the change output. - val TxOutputAdjusted(feerateUpdate1) = adjustPreviousTxOutput(FundedTx(previousTx, 15000 sat, FeeratePerKw(2500 sat), Map.empty), FeeratePerKw(5000 sat), commitment, commitTx) - assert(feerateUpdate1.txInfo.tx.txIn == previousTx.txInfo.tx.txIn) - assert(feerateUpdate1.txInfo.tx.txOut.length == 2) - assert(feerateUpdate1.txInfo.tx.txOut.head == previousTx.txInfo.tx.txOut.head) - val TxOutputAdjusted(feerateUpdate2) = adjustPreviousTxOutput(FundedTx(previousTx, 15000 sat, FeeratePerKw(2500 sat), Map.empty), FeeratePerKw(6000 sat), commitment, commitTx) - assert(feerateUpdate2.txInfo.tx.txIn == previousTx.txInfo.tx.txIn) - assert(feerateUpdate2.txInfo.tx.txOut.length == 2) - assert(feerateUpdate2.txInfo.tx.txOut.head == previousTx.txInfo.tx.txOut.head) - assert(feerateUpdate2.txInfo.tx.txOut.last.amount < feerateUpdate1.txInfo.tx.txOut.last.amount) - - // If the previous funding attempt didn't add a change output, we must add new wallet inputs. - val previousTxNoChange = previousTx.updateTx(previousTx.txInfo.tx.copy(txOut = Seq(previousTx.txInfo.tx.txOut.head))) - val AddWalletInputs(tx) = adjustPreviousTxOutput(FundedTx(previousTxNoChange, 25000 sat, FeeratePerKw(2500 sat), Map.empty), FeeratePerKw(5000 sat), commitment, commitTx) - assert(tx == previousTxNoChange) - - for (_ <- 1 to 100) { - val amountIn = Random.nextInt(25_000_000).sat - val changeAmount = Random.nextInt(amountIn.toLong.toInt).sat - val fuzzyPreviousTx = previousTx.updateTx(previousTx.txInfo.tx.copy(txOut = Seq( - initialHtlcTx.txInfo.tx.txOut.head, - TxOut(changeAmount, Script.pay2wpkh(PlaceHolderPubKey)) - ))) - val targetFeerate = FeeratePerKw(2500 sat) + FeeratePerKw(Random.nextInt(20000).sat) - adjustPreviousTxOutput(FundedTx(fuzzyPreviousTx, amountIn, FeeratePerKw(2500 sat), Map.empty), targetFeerate, commitment, commitTx) match { - case AdjustPreviousTxOutputResult.Skip(_) => // nothing do check - case AddWalletInputs(tx) => assert(tx == fuzzyPreviousTx) - case TxOutputAdjusted(updatedTx) => - assert(updatedTx.txInfo.tx.txIn == fuzzyPreviousTx.txInfo.tx.txIn) - assert(Set(1, 2).contains(updatedTx.txInfo.tx.txOut.length)) - assert(updatedTx.txInfo.tx.txOut.head == fuzzyPreviousTx.txInfo.tx.txOut.head) - assert(updatedTx.txInfo.tx.txOut.last.amount >= 600.sat) - } - } - } - } - - test("adjust previous claim htlc transaction outputs") { - val commitment = mock[FullCommitment] - val localParams = mock[LocalParams] - localParams.dustLimit.returns(500 sat) - commitment.localParams.returns(localParams) - val (commitTx, claimHtlcSuccess, claimHtlcTimeout) = createClaimHtlcTx() - for (claimHtlc <- Seq(claimHtlcSuccess, claimHtlcTimeout)) { - var previousAmount = claimHtlc.txInfo.tx.txOut.head.amount - for (i <- 1 to 100) { - val targetFeerate = FeeratePerKw(250 * i sat) - adjustPreviousTxOutput(FundedTx(claimHtlc, claimHtlc.txInfo.amountIn, FeeratePerKw(2500 sat), Map.empty), targetFeerate, commitment, commitTx) match { - case AdjustPreviousTxOutputResult.Skip(_) => assert(targetFeerate >= FeeratePerKw(10000 sat)) - case AddWalletInputs(_) => fail("shouldn't add wallet inputs to claim-htlc-tx") - case TxOutputAdjusted(updatedTx) => - assert(updatedTx.txInfo.tx.txIn == claimHtlc.txInfo.tx.txIn) - assert(updatedTx.txInfo.tx.txOut.length == 1) - assert(updatedTx.txInfo.tx.txOut.head.amount < previousAmount) - previousAmount = updatedTx.txInfo.tx.txOut.head.amount - } - } - } - } - -} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisherSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisherSpec.scala index 64fe89219a..e3f8222782 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisherSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisherSpec.scala @@ -21,7 +21,6 @@ import akka.actor.typed.scaladsl.adapter.{ClassicActorSystemOps, actorRefAdapter import akka.pattern.pipe import akka.testkit.{TestFSMRef, TestProbe} import com.softwaremill.quicklens.ModifyPimp -import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.bitcoin.scalacompat.{Block, BtcAmount, MilliBtcDouble, MnemonicCode, OutPoint, SatoshiLong, ScriptElt, Transaction, TxId} import fr.acinq.eclair.NotificationsLogger.NotifyNodeOperator import fr.acinq.eclair.blockchain.bitcoind.BitcoindService @@ -29,14 +28,16 @@ import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinCoreClient.MempoolTx import fr.acinq.eclair.blockchain.bitcoind.rpc.{BasicBitcoinJsonRPCClient, BitcoinCoreClient, BitcoinJsonRPCClient} import fr.acinq.eclair.blockchain.fee.{ConfirmationTarget, FeeratePerKw, FeeratesPerKw} -import fr.acinq.eclair.blockchain.{CurrentBlockHeight, OnChainPubkeyCache} +import fr.acinq.eclair.blockchain.{CurrentBlockHeight, OnChainAddressCache} import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.channel.publish.ReplaceableTxPublisher.{Publish, Stop, UpdateConfirmationTarget} import fr.acinq.eclair.channel.publish.TxPublisher.TxRejectedReason._ import fr.acinq.eclair.channel.publish.TxPublisher._ +import fr.acinq.eclair.channel.states.ChannelStateTestsBase.PimpTestFSM import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags} import fr.acinq.eclair.crypto.keymanager.LocalOnChainKeyManager +import fr.acinq.eclair.testutils.PimpTestProbe.convert import fr.acinq.eclair.transactions.Transactions import fr.acinq.eclair.transactions.Transactions._ import fr.acinq.eclair.wire.protocol.{CommitSig, RevokeAndAck, UpdateFee} @@ -124,18 +125,12 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w def createTestWallet(walletName: String) = { val walletRpcClient = createWallet(walletName) val probe = TestProbe() - val walletClient = new BitcoinCoreClient(walletRpcClient) with OnChainPubkeyCache { - val pubkey = { - getP2wpkhPubkey().pipeTo(probe.ref) - probe.expectMsgType[PublicKey] - } - val pubkeyScript = { + val walletClient = new BitcoinCoreClient(walletRpcClient) with OnChainAddressCache { + private val pubkeyScript: Seq[ScriptElt] = { getReceivePublicKeyScript(None).pipeTo(probe.ref) probe.expectMsgType[Seq[ScriptElt]] } - override def getP2wpkhPubkey(renew: Boolean): PublicKey = pubkey - override def getReceivePublicKeyScript(renew: Boolean): Seq[ScriptElt] = pubkeyScript } @@ -161,11 +156,9 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w val blockHeight = new AtomicLong() blockHeight.set(currentBlockHeight(probe).toLong) val aliceNodeParams = TestConstants.Alice.nodeParams.copy(blockHeight = blockHeight) - val setup = init(aliceNodeParams, TestConstants.Bob.nodeParams.copy(blockHeight = blockHeight), wallet_opt = Some(walletClient)) + val setup = init(aliceNodeParams, TestConstants.Bob.nodeParams.copy(blockHeight = blockHeight), walletA_opt = Some(walletClient)) val testTags = channelType match { - case _: ChannelTypes.AnchorOutputsZeroFeeHtlcTx => Set(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs) - case _: ChannelTypes.AnchorOutputs => Set(ChannelStateTestsTags.AnchorOutputs) - case _: ChannelTypes.StaticRemoteKey => Set(ChannelStateTestsTags.StaticRemoteKey) + case _: ChannelTypes.AnchorOutputs => Set(ChannelStateTestsTags.AnchorOutputsPhoenix) case _ => Set.empty[String] } reachNormal(setup, testTags) @@ -187,36 +180,36 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w def closeChannelWithoutHtlcs(f: Fixture, overrideCommitTarget: BlockHeight): (PublishFinalTx, PublishReplaceableTx) = { import f._ - val commitTx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.fullySignedLocalCommitTx(alice.underlyingActor.nodeParams.channelKeyManager) + val commitTx = alice.signCommitTx() + val commitment = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest + val commitFee = commitment.capacity - commitTx.txOut.map(_.amount).sum probe.send(alice, CMD_FORCECLOSE(probe.ref)) probe.expectMsgType[CommandSuccess[CMD_FORCECLOSE]] // Forward the commit tx to the publisher. - val publishCommitTx = alice2blockchain.expectMsg(PublishFinalTx(commitTx, commitTx.fee, None)) + val publishCommitTx = alice2blockchain.expectMsg(PublishFinalTx(commitTx, commitment.fundingInput, "commit-tx", commitFee, None)) // Forward the anchor tx to the publisher. - val publishAnchor = alice2blockchain.expectMsgType[PublishReplaceableTx] - assert(publishAnchor.txInfo.input.outPoint.txid == commitTx.tx.txid) - assert(publishAnchor.txInfo.isInstanceOf[ClaimLocalAnchorOutputTx]) - val anchorTx = publishAnchor.txInfo.asInstanceOf[ClaimLocalAnchorOutputTx].copy(confirmationTarget = ConfirmationTarget.Absolute(overrideCommitTarget)) + val publishAnchor = alice2blockchain.expectMsgType[PublishReplaceableTx].copy(confirmationTarget = ConfirmationTarget.Absolute(overrideCommitTarget)) + assert(publishAnchor.commitTx == commitTx) + assert(publishAnchor.txInfo.isInstanceOf[ClaimLocalAnchorTx]) - (publishCommitTx, publishAnchor.copy(txInfo = anchorTx)) + (publishCommitTx, publishAnchor) } def remoteCloseChannelWithoutHtlcs(f: Fixture, overrideCommitTarget: BlockHeight): (Transaction, PublishReplaceableTx) = { import f._ - val commitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.fullySignedLocalCommitTx(bob.underlyingActor.nodeParams.channelKeyManager).tx + val commitTx = bob.signCommitTx() wallet.publishTransaction(commitTx).pipeTo(probe.ref) probe.expectMsg(commitTx.txid) probe.send(alice, WatchFundingSpentTriggered(commitTx)) // Forward the anchor tx to the publisher. - val publishAnchor = alice2blockchain.expectMsgType[PublishReplaceableTx] - assert(publishAnchor.txInfo.input.outPoint.txid == commitTx.txid) - assert(publishAnchor.txInfo.isInstanceOf[ClaimLocalAnchorOutputTx]) - val anchorTx = publishAnchor.txInfo.asInstanceOf[ClaimLocalAnchorOutputTx].copy(confirmationTarget = ConfirmationTarget.Absolute(overrideCommitTarget)) + val publishAnchor = alice2blockchain.expectMsgType[PublishReplaceableTx].copy(confirmationTarget = ConfirmationTarget.Absolute(overrideCommitTarget)) + assert(publishAnchor.commitTx == commitTx) + assert(publishAnchor.txInfo.isInstanceOf[ClaimRemoteAnchorTx]) - (commitTx, publishAnchor.copy(txInfo = anchorTx)) + (commitTx, publishAnchor) } test("commit tx feerate high enough, not spending anchor output (local commit)") { @@ -249,7 +242,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } } - test("commit tx confirmed, not spending anchor output") { + test("commit tx recently confirmed, not spending anchor output") { withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx()) { f => import f._ @@ -260,9 +253,30 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w setFeerate(FeeratePerKw(10_000 sat)) publisher ! Publish(probe.ref, anchorTx) - val result = probe.expectMsgType[TxRejected] - assert(result.cmd == anchorTx) - assert(result.reason == TxSkipped(retryNextBlock = false)) + inside(probe.expectMsgType[TxRejected]) { result => + assert(result.cmd == anchorTx) + // The commit tx isn't deeply confirmed yet: we will check again later. + assert(result.reason == TxSkipped(retryNextBlock = true)) + } + } + } + + test("commit tx deeply confirmed, not spending anchor output") { + withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx()) { f => + import f._ + + val (commitTx, anchorTx) = closeChannelWithoutHtlcs(f, aliceBlockHeight() + 12) + wallet.publishTransaction(commitTx.tx).pipeTo(probe.ref) + probe.expectMsg(commitTx.tx.txid) + generateBlocks(6) + + setFeerate(FeeratePerKw(10_000 sat)) + publisher ! Publish(probe.ref, anchorTx) + inside(probe.expectMsgType[TxRejected]) { result => + assert(result.cmd == anchorTx) + // The commit tx is deeply confirmed: we don't need to retry again. + assert(result.reason == TxSkipped(retryNextBlock = false)) + } } } @@ -275,7 +289,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w val (commitTx, anchorTx) = closeChannelWithoutHtlcs(f, aliceBlockHeight() + 6) wallet.publishTransaction(commitTx.tx).pipeTo(probe.ref) probe.expectMsg(commitTx.tx.txid) - generateBlocks(1) + generateBlocks(6) publisher ! Publish(probe.ref, anchorTx) val result = probe.expectMsgType[TxRejected] @@ -288,11 +302,11 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx()) { f => import f._ - val remoteCommit = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.fullySignedLocalCommitTx(bob.underlyingActor.nodeParams.channelKeyManager) - assert(remoteCommit.tx.txOut.length == 4) // 2 main outputs + 2 anchor outputs + val remoteCommit = bob.signCommitTx() + assert(remoteCommit.txOut.length == 4) // 2 main outputs + 2 anchor outputs val (_, anchorTx) = closeChannelWithoutHtlcs(f, aliceBlockHeight() + 12) - wallet.publishTransaction(remoteCommit.tx).pipeTo(probe.ref) - probe.expectMsg(remoteCommit.tx.txid) + wallet.publishTransaction(remoteCommit).pipeTo(probe.ref) + probe.expectMsg(remoteCommit.txid) generateBlocks(1) setFeerate(FeeratePerKw(10_000 sat)) @@ -316,14 +330,14 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w bob2alice.expectMsgType[RevokeAndAck] bob2alice.expectMsgType[CommitSig] assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.nextRemoteCommit_opt.nonEmpty) - val nextRemoteCommitTxId = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.nextRemoteCommit_opt.get.commit.txid + val nextRemoteCommitTxId = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.nextRemoteCommit_opt.get.txId - val nextRemoteCommit = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.fullySignedLocalCommitTx(bob.underlyingActor.nodeParams.channelKeyManager) - assert(nextRemoteCommit.tx.txid == nextRemoteCommitTxId) - assert(nextRemoteCommit.tx.txOut.length == 5) // 2 main outputs + 2 anchor outputs + 1 htlc + val nextRemoteCommit = bob.signCommitTx() + assert(nextRemoteCommit.txid == nextRemoteCommitTxId) + assert(nextRemoteCommit.txOut.length == 5) // 2 main outputs + 2 anchor outputs + 1 htlc val (_, anchorTx) = closeChannelWithoutHtlcs(f, aliceBlockHeight() + 12) - wallet.publishTransaction(nextRemoteCommit.tx).pipeTo(probe.ref) - probe.expectMsg(nextRemoteCommit.tx.txid) + wallet.publishTransaction(nextRemoteCommit).pipeTo(probe.ref) + probe.expectMsg(nextRemoteCommit.txid) generateBlocks(1) setFeerate(FeeratePerKw(10_000 sat)) @@ -338,10 +352,10 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx()) { f => import f._ - val remoteCommit = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.fullySignedLocalCommitTx(bob.underlyingActor.nodeParams.channelKeyManager) + val remoteCommit = bob.signCommitTx() val (_, anchorTx) = closeChannelWithoutHtlcs(f, aliceBlockHeight() + 12) - wallet.publishTransaction(remoteCommit.tx).pipeTo(probe.ref) - probe.expectMsg(remoteCommit.tx.txid) + wallet.publishTransaction(remoteCommit).pipeTo(probe.ref) + probe.expectMsg(remoteCommit.txid) setFeerate(FeeratePerKw(10_000 sat)) publisher ! Publish(probe.ref, anchorTx) @@ -355,7 +369,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx()) { f => import f._ - val remoteCommit = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.fullySignedLocalCommitTx(bob.underlyingActor.nodeParams.channelKeyManager) + val remoteCommit = bob.signCommitTx() assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.spec.commitTxFeerate == FeeratePerKw(2500 sat)) // We lower the feerate to make it easy to replace our commit tx by theirs in the mempool. @@ -369,8 +383,8 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w assert(mempoolTxs.map(_.txid).contains(localCommit.tx.txid)) // Our commit tx is replaced by theirs. - wallet.publishTransaction(remoteCommit.tx).pipeTo(probe.ref) - probe.expectMsg(remoteCommit.tx.txid) + wallet.publishTransaction(remoteCommit).pipeTo(probe.ref) + probe.expectMsg(remoteCommit.txid) generateBlocks(1) system.eventStream.publish(CurrentBlockHeight(currentBlockHeight(probe))) @@ -464,7 +478,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w if (nextCommit) { assert(alice.stateData.asInstanceOf[DATA_CLOSING].commitments.latest.nextRemoteCommit_opt.nonEmpty) - assert(alice.stateData.asInstanceOf[DATA_CLOSING].commitments.latest.nextRemoteCommit_opt.map(_.commit.txid).contains(commitTx.txid)) + assert(alice.stateData.asInstanceOf[DATA_CLOSING].commitments.latest.nextRemoteCommit_opt.map(_.txId).contains(commitTx.txid)) } val targetFeerate = FeeratePerKw(3000 sat) @@ -508,7 +522,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w probe.expectMsg(commitTx.tx.txid) assert(getMempool().length == 1) - val maxFeerate = ReplaceableTxFunder.maxFeerate(anchorTx.txInfo, anchorTx.commitment, anchorTx.commitTx, alice.underlyingActor.nodeParams.currentBitcoinCoreFeerates, alice.underlyingActor.nodeParams.onChainFeeConf) + val maxFeerate = ReplaceableTxFunder.maxFeerate(anchorTx.txInfo, anchorTx.commitTx, anchorTx.commitment, alice.underlyingActor.nodeParams.currentBitcoinCoreFeerates, alice.underlyingActor.nodeParams.onChainFeeConf) val targetFeerate = FeeratePerKw(50_000 sat) assert(maxFeerate <= targetFeerate / 2) setFeerate(targetFeerate, blockTarget = 12) @@ -587,17 +601,17 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx()) { f => import f._ - val commitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.fullySignedLocalCommitTx(bob.underlyingActor.nodeParams.channelKeyManager).tx + val commitTx = bob.signCommitTx() // Note that we don't publish the remote commit, to simulate the case where the watch triggers but the remote commit is then evicted from our mempool. probe.send(alice, WatchFundingSpentTriggered(commitTx)) val publishAnchor = alice2blockchain.expectMsgType[PublishReplaceableTx] assert(publishAnchor.commitTx == commitTx) assert(publishAnchor.txInfo.input.outPoint.txid == commitTx.txid) - assert(publishAnchor.txInfo.isInstanceOf[ClaimLocalAnchorOutputTx]) + assert(publishAnchor.txInfo.isInstanceOf[ClaimRemoteAnchorTx]) val targetFeerate = FeeratePerKw(3000 sat) setFeerate(targetFeerate) - val anchorTx = publishAnchor.copy(txInfo = publishAnchor.txInfo.asInstanceOf[ClaimLocalAnchorOutputTx].copy(confirmationTarget = ConfirmationTarget.Absolute(aliceBlockHeight() + 6))) + val anchorTx = publishAnchor.copy(confirmationTarget = ConfirmationTarget.Absolute(aliceBlockHeight() + 6)) publisher ! Publish(probe.ref, anchorTx) // wait for the commit tx and anchor tx to be published val mempoolTxs = getMempoolTxs(2) @@ -932,14 +946,14 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w crossSign(alice, bob, alice2bob, bob2alice) val (r, htlc) = addHtlc(4_000_000 msat, bob, alice, bob2alice, alice2bob) crossSign(bob, alice, bob2alice, alice2bob) - probe.send(alice, CMD_FULFILL_HTLC(htlc.id, r, replyTo_opt = Some(probe.ref))) + probe.send(alice, CMD_FULFILL_HTLC(htlc.id, r, None, replyTo_opt = Some(probe.ref))) probe.expectMsgType[CommandSuccess[CMD_FULFILL_HTLC]] // Force-close channel. probe.send(alice, CMD_FORCECLOSE(probe.ref)) probe.expectMsgType[CommandSuccess[CMD_FORCECLOSE]] alice2blockchain.expectMsgType[PublishFinalTx] - assert(alice2blockchain.expectMsgType[PublishReplaceableTx].txInfo.isInstanceOf[ClaimLocalAnchorOutputTx]) + assert(alice2blockchain.expectMsgType[PublishReplaceableTx].txInfo.isInstanceOf[ClaimLocalAnchorTx]) alice2blockchain.expectMsgType[PublishFinalTx] // claim main output val htlcSuccess = alice2blockchain.expectMsgType[PublishReplaceableTx] assert(htlcSuccess.txInfo.isInstanceOf[HtlcSuccessTx]) @@ -947,9 +961,9 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w assert(htlcTimeout.txInfo.isInstanceOf[HtlcTimeoutTx]) // The remote commit tx has a few confirmations, but isn't deeply confirmed yet. - val remoteCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.fullySignedLocalCommitTx(bob.underlyingActor.nodeParams.channelKeyManager) - wallet.publishTransaction(remoteCommitTx.tx).pipeTo(probe.ref) - probe.expectMsg(remoteCommitTx.tx.txid) + val remoteCommitTx = bob.signCommitTx() + wallet.publishTransaction(remoteCommitTx).pipeTo(probe.ref) + probe.expectMsg(remoteCommitTx.txid) generateBlocks(2) // Verify that HTLC transactions aren't published, but are retried in case a reorg makes the local commit confirm. @@ -1005,23 +1019,23 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w bob2alice.expectMsgType[RevokeAndAck] bob2alice.expectMsgType[CommitSig] assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.nextRemoteCommit_opt.nonEmpty) - val nextRemoteCommitTxId = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.nextRemoteCommit_opt.get.commit.txid + val nextRemoteCommitTxId = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.nextRemoteCommit_opt.get.txId // Force-close channel. probe.send(alice, CMD_FORCECLOSE(probe.ref)) probe.expectMsgType[CommandSuccess[CMD_FORCECLOSE]] alice2blockchain.expectMsgType[PublishFinalTx] - assert(alice2blockchain.expectMsgType[PublishReplaceableTx].txInfo.isInstanceOf[ClaimLocalAnchorOutputTx]) + assert(alice2blockchain.expectMsgType[PublishReplaceableTx].txInfo.isInstanceOf[ClaimLocalAnchorTx]) alice2blockchain.expectMsgType[PublishFinalTx] // claim main output val htlcTimeout = alice2blockchain.expectMsgType[PublishReplaceableTx] assert(htlcTimeout.txInfo.isInstanceOf[HtlcTimeoutTx]) // Ensure remote commit tx confirms. - val nextRemoteCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.fullySignedLocalCommitTx(bob.underlyingActor.nodeParams.channelKeyManager) - assert(nextRemoteCommitTx.tx.txid == nextRemoteCommitTxId) - assert(nextRemoteCommitTx.tx.txOut.length == 6) // 2 main outputs + 2 anchor outputs + 2 htlcs - wallet.publishTransaction(nextRemoteCommitTx.tx).pipeTo(probe.ref) - probe.expectMsg(nextRemoteCommitTx.tx.txid) + val nextRemoteCommitTx = bob.signCommitTx() + assert(nextRemoteCommitTx.txid == nextRemoteCommitTxId) + assert(nextRemoteCommitTx.txOut.length == 6) // 2 main outputs + 2 anchor outputs + 2 htlcs + wallet.publishTransaction(nextRemoteCommitTx).pipeTo(probe.ref) + probe.expectMsg(nextRemoteCommitTx.txid) generateBlocks(6) // Verify that HTLC transactions immediately fail to publish. @@ -1043,38 +1057,34 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w crossSign(alice, bob, alice2bob, bob2alice) val (r, htlc) = addHtlc(incomingHtlcAmount, bob, alice, bob2alice, alice2bob) crossSign(bob, alice, bob2alice, alice2bob) - probe.send(alice, CMD_FULFILL_HTLC(htlc.id, r, replyTo_opt = Some(probe.ref))) + probe.send(alice, CMD_FULFILL_HTLC(htlc.id, r, None, replyTo_opt = Some(probe.ref))) probe.expectMsgType[CommandSuccess[CMD_FULFILL_HTLC]] // Force-close channel and verify txs sent to watcher. - val commitTx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.fullySignedLocalCommitTx(alice.underlyingActor.nodeParams.channelKeyManager) - assert(commitTx.tx.txOut.size == 6) + val commitTx = alice.signCommitTx() + val commitment = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest + val commitFee = commitment.capacity - commitTx.txOut.map(_.amount).sum + assert(commitTx.txOut.size == 6) probe.send(alice, CMD_FORCECLOSE(probe.ref)) probe.expectMsgType[CommandSuccess[CMD_FORCECLOSE]] // We make the commit tx confirm because htlc txs have a relative delay. - alice2blockchain.expectMsg(PublishFinalTx(commitTx, commitTx.fee, None)) - wallet.publishTransaction(commitTx.tx).pipeTo(probe.ref) - probe.expectMsg(commitTx.tx.txid) + alice2blockchain.expectMsg(PublishFinalTx(commitTx, commitment.fundingInput, "commit-tx", commitFee, None)) + wallet.publishTransaction(commitTx).pipeTo(probe.ref) + probe.expectMsg(commitTx.txid) generateBlocks(1) - assert(alice2blockchain.expectMsgType[PublishReplaceableTx].txInfo.isInstanceOf[ClaimLocalAnchorOutputTx]) - alice2blockchain.expectMsgType[PublishFinalTx] // claim main output - val htlcSuccess = alice2blockchain.expectMsgType[PublishReplaceableTx] + val anchor = alice2blockchain.expectReplaceableTxPublished[ClaimLocalAnchorTx] + val main = alice2blockchain.expectFinalTxPublished("local-main-delayed") + val htlcSuccess = alice2blockchain.expectMsgType[PublishReplaceableTx].copy(confirmationTarget = ConfirmationTarget.Absolute(overrideHtlcTarget)) assert(htlcSuccess.txInfo.isInstanceOf[HtlcSuccessTx]) - val htlcSuccessTx = htlcSuccess.txInfo.asInstanceOf[HtlcSuccessTx].copy(confirmationTarget = ConfirmationTarget.Absolute(overrideHtlcTarget)) - val htlcTimeout = alice2blockchain.expectMsgType[PublishReplaceableTx] + val htlcTimeout = alice2blockchain.expectMsgType[PublishReplaceableTx].copy(confirmationTarget = ConfirmationTarget.Absolute(overrideHtlcTarget)) assert(htlcTimeout.txInfo.isInstanceOf[HtlcTimeoutTx]) - val htlcTimeoutTx = htlcTimeout.txInfo.asInstanceOf[HtlcTimeoutTx].copy(confirmationTarget = ConfirmationTarget.Absolute(overrideHtlcTarget)) - - alice2blockchain.expectMsgType[WatchTxConfirmed] // commit tx - alice2blockchain.expectMsgType[WatchTxConfirmed] // claim main output - alice2blockchain.expectMsgType[WatchOutputSpent] // claim-anchor tx - alice2blockchain.expectMsgType[WatchOutputSpent] // htlc-success tx - alice2blockchain.expectMsgType[WatchOutputSpent] // htlc-timeout tx + alice2blockchain.expectWatchTxConfirmed(commitTx.txid) + alice2blockchain.expectWatchOutputsSpent(Seq(main.input, anchor.input.outPoint, htlcSuccess.input, htlcTimeout.input)) alice2blockchain.expectNoMessage(100 millis) - (commitTx.tx, htlcSuccess.copy(txInfo = htlcSuccessTx), htlcTimeout.copy(txInfo = htlcTimeoutTx)) + (commitTx, htlcSuccess, htlcTimeout) } test("not enough funds to increase htlc tx feerate") { @@ -1100,7 +1110,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w htlcSuccessPublisher ! Publish(probe.ref, htlcSuccess) val htlcSuccessTx = getMempoolTxs(1).head val htlcSuccessTargetFee = Transactions.weight2fee(targetFeerate, htlcSuccessTx.weight.toInt) - assert(htlcSuccessTargetFee * 0.9 <= htlcSuccessTx.fees && htlcSuccessTx.fees <= htlcSuccessTargetFee * 1.2, s"actualFee=${htlcSuccessTx.fees} targetFee=$htlcSuccessTargetFee") + assert(htlcSuccessTargetFee * 0.9 <= htlcSuccessTx.fees && htlcSuccessTx.fees <= htlcSuccessTargetFee * 1.1, s"actualFee=${htlcSuccessTx.fees} targetFee=$htlcSuccessTargetFee") assert(htlcSuccessTx.fees <= htlcSuccess.txInfo.amountIn) generateBlocks(6) @@ -1128,7 +1138,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w setFeerate(targetFeerate) // the feerate is higher than what it was when the channel force-closed val htlcTimeoutTx = getMempoolTxs(1).head val htlcTimeoutTargetFee = Transactions.weight2fee(targetFeerate, htlcTimeoutTx.weight.toInt) - assert(htlcTimeoutTargetFee * 0.9 <= htlcTimeoutTx.fees && htlcTimeoutTx.fees <= htlcTimeoutTargetFee * 1.2, s"actualFee=${htlcTimeoutTx.fees} targetFee=$htlcTimeoutTargetFee") + assert(htlcTimeoutTargetFee * 0.9 <= htlcTimeoutTx.fees && htlcTimeoutTx.fees <= htlcTimeoutTargetFee * 1.1, s"actualFee=${htlcTimeoutTx.fees} targetFee=$htlcTimeoutTargetFee") assert(htlcTimeoutTx.fees <= htlcTimeout.txInfo.amountIn) generateBlocks(6) @@ -1214,12 +1224,12 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w val (commitTx, htlcSuccess, htlcTimeout) = closeChannelWithHtlcs(f, aliceBlockHeight() + 30, outgoingHtlcAmount = 5_000_000 msat, incomingHtlcAmount = 4_000_000 msat) setFeerate(targetFeerate, blockTarget = 12) assert(htlcSuccess.txInfo.fee == 0.sat) - val htlcSuccessMaxFeerate = ReplaceableTxFunder.maxFeerate(htlcSuccess.txInfo, htlcSuccess.commitment, htlcSuccess.commitTx, alice.underlyingActor.nodeParams.currentBitcoinCoreFeerates, alice.underlyingActor.nodeParams.onChainFeeConf) + val htlcSuccessMaxFeerate = ReplaceableTxFunder.maxFeerate(htlcSuccess.txInfo, commitTx, htlcSuccess.commitment, alice.underlyingActor.nodeParams.currentBitcoinCoreFeerates, alice.underlyingActor.nodeParams.onChainFeeConf) assert(htlcSuccessMaxFeerate < targetFeerate / 2) val htlcSuccessTx = testPublishHtlcSuccess(f, commitTx, htlcSuccess, htlcSuccessMaxFeerate) assert(htlcSuccessTx.txIn.length > 1) assert(htlcTimeout.txInfo.fee == 0.sat) - val htlcTimeoutMaxFeerate = ReplaceableTxFunder.maxFeerate(htlcTimeout.txInfo, htlcTimeout.commitment, htlcTimeout.commitTx, alice.underlyingActor.nodeParams.currentBitcoinCoreFeerates, alice.underlyingActor.nodeParams.onChainFeeConf) + val htlcTimeoutMaxFeerate = ReplaceableTxFunder.maxFeerate(htlcTimeout.txInfo, commitTx, htlcTimeout.commitment, alice.underlyingActor.nodeParams.currentBitcoinCoreFeerates, alice.underlyingActor.nodeParams.onChainFeeConf) assert(htlcTimeoutMaxFeerate < targetFeerate / 2) val htlcTimeoutTx = testPublishHtlcTimeout(f, commitTx, htlcTimeout, htlcTimeoutMaxFeerate) assert(htlcTimeoutTx.txIn.length > 1) @@ -1245,7 +1255,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w withFixture(utxos, ChannelTypes.AnchorOutputsZeroFeeHtlcTx()) { f => import f._ - val targetFeerate = FeeratePerKw(8_000 sat) + val targetFeerate = FeeratePerKw(10_000 sat) val (commitTx, htlcSuccess, htlcTimeout) = closeChannelWithHtlcs(f, aliceBlockHeight() + 30) // NB: we try to get transactions confirmed *before* their confirmation target, so we aim for a more aggressive block target than what's provided. setFeerate(targetFeerate, blockTarget = 12) @@ -1341,7 +1351,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w bob2blockchain.expectMsgType[PublishFinalTx] // claim main output val claimHtlcTimeout = bob2blockchain.expectMsgType[PublishReplaceableTx] // claim-htlc-timeout assert(claimHtlcTimeout.txInfo.isInstanceOf[ClaimHtlcTimeoutTx]) - wallet.publishTransaction(claimHtlcTimeout.txInfo.tx).pipeTo(probe.ref) + wallet.publishTransaction(claimHtlcTimeout.txInfo.sign()).pipeTo(probe.ref) probe.expectMsg(claimHtlcTimeout.txInfo.tx.txid) generateBlocks(1) @@ -1455,6 +1465,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w // we try to publish the htlc-success again (can be caused by a node restart): it will fail to replace the existing // one in the mempool but we must ensure we don't leave some utxos locked. + setFeerate(targetFeerate * 0.9) val publisher2 = createPublisher() publisher2 ! Publish(probe.ref, htlcSuccess) val result = probe.expectMsgType[TxRejected] @@ -1503,24 +1514,24 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w crossSign(alice, bob, alice2bob, bob2alice) val (r, htlc) = addHtlc(20_000_000 msat, bob, alice, bob2alice, alice2bob) crossSign(bob, alice, bob2alice, alice2bob) - probe.send(alice, CMD_FULFILL_HTLC(htlc.id, r, replyTo_opt = Some(probe.ref))) + probe.send(alice, CMD_FULFILL_HTLC(htlc.id, r, None, replyTo_opt = Some(probe.ref))) probe.expectMsgType[CommandSuccess[CMD_FULFILL_HTLC]] // Force-close channel. - val localCommitTx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.fullySignedLocalCommitTx(alice.underlyingActor.nodeParams.channelKeyManager) - val remoteCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.fullySignedLocalCommitTx(bob.underlyingActor.nodeParams.channelKeyManager) - assert(remoteCommitTx.tx.txOut.size == 6) - probe.send(alice, WatchFundingSpentTriggered(remoteCommitTx.tx)) + val localCommitTx = alice.signCommitTx() + val remoteCommitTx = bob.signCommitTx() + assert(remoteCommitTx.txOut.size == 6) + probe.send(alice, WatchFundingSpentTriggered(remoteCommitTx)) alice2blockchain.expectMsgType[PublishReplaceableTx] // claim anchor alice2blockchain.expectMsgType[PublishFinalTx] // claim main output - val claimHtlcTimeout = alice2blockchain.expectMsgType[PublishReplaceableTx] - assert(claimHtlcTimeout.txInfo.isInstanceOf[ClaimHtlcTimeoutTx]) val claimHtlcSuccess = alice2blockchain.expectMsgType[PublishReplaceableTx] assert(claimHtlcSuccess.txInfo.isInstanceOf[ClaimHtlcSuccessTx]) + val claimHtlcTimeout = alice2blockchain.expectMsgType[PublishReplaceableTx] + assert(claimHtlcTimeout.txInfo.isInstanceOf[ClaimHtlcTimeoutTx]) // The local commit tx has a few confirmations, but isn't deeply confirmed yet. - wallet.publishTransaction(localCommitTx.tx).pipeTo(probe.ref) - probe.expectMsg(localCommitTx.tx.txid) + wallet.publishTransaction(localCommitTx).pipeTo(probe.ref) + probe.expectMsg(localCommitTx.txid) generateBlocks(3) // Verify that Claim-HTLC transactions aren't published, but are retried in case a reorg makes the remote commit confirm. @@ -1578,41 +1589,30 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } else { crossSign(alice, bob, alice2bob, bob2alice) } - probe.send(alice, CMD_FULFILL_HTLC(htlc.id, r, replyTo_opt = Some(probe.ref))) + probe.send(alice, CMD_FULFILL_HTLC(htlc.id, r, None, replyTo_opt = Some(probe.ref))) probe.expectMsgType[CommandSuccess[CMD_FULFILL_HTLC]] // Force-close channel and verify txs sent to watcher. - val remoteCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.fullySignedLocalCommitTx(bob.underlyingActor.nodeParams.channelKeyManager) - bob.stateData.asInstanceOf[DATA_NORMAL].commitments.params.commitmentFormat match { - case Transactions.DefaultCommitmentFormat => assert(remoteCommitTx.tx.txOut.size == 4) - case _: AnchorOutputsCommitmentFormat => assert(remoteCommitTx.tx.txOut.size == 6) - } - probe.send(alice, WatchFundingSpentTriggered(remoteCommitTx.tx)) + val remoteCommitTx = bob.signCommitTx() + assert(remoteCommitTx.txOut.size == 6) + probe.send(alice, WatchFundingSpentTriggered(remoteCommitTx)) // We make the commit tx confirm because claim-htlc txs have a relative delay when using anchor outputs. - wallet.publishTransaction(remoteCommitTx.tx).pipeTo(probe.ref) - probe.expectMsg(remoteCommitTx.tx.txid) + wallet.publishTransaction(remoteCommitTx).pipeTo(probe.ref) + probe.expectMsg(remoteCommitTx.txid) generateBlocks(1) - bob.stateData.asInstanceOf[DATA_NORMAL].commitments.params.commitmentFormat match { - case Transactions.DefaultCommitmentFormat => () - case _: AnchorOutputsCommitmentFormat => alice2blockchain.expectMsgType[PublishReplaceableTx] // claim anchor - } - if (!bob.stateData.asInstanceOf[DATA_NORMAL].commitments.params.channelFeatures.paysDirectlyToWallet) alice2blockchain.expectMsgType[PublishFinalTx] // claim main output - val claimHtlcSuccess = alice2blockchain.expectMsgType[PublishReplaceableTx] + val anchorTx = alice2blockchain.expectReplaceableTxPublished[ClaimRemoteAnchorTx] + val mainTx = alice2blockchain.expectFinalTxPublished("remote-main-delayed") + val claimHtlcSuccess = alice2blockchain.expectMsgType[PublishReplaceableTx].copy(confirmationTarget = ConfirmationTarget.Absolute(overrideHtlcTarget)) assert(claimHtlcSuccess.txInfo.isInstanceOf[ClaimHtlcSuccessTx]) - val claimHtlcSuccessTx = claimHtlcSuccess.txInfo.asInstanceOf[ClaimHtlcSuccessTx].copy(confirmationTarget = ConfirmationTarget.Absolute(overrideHtlcTarget)) - val claimHtlcTimeout = alice2blockchain.expectMsgType[PublishReplaceableTx] + val claimHtlcTimeout = alice2blockchain.expectMsgType[PublishReplaceableTx].copy(confirmationTarget = ConfirmationTarget.Absolute(overrideHtlcTarget)) assert(claimHtlcTimeout.txInfo.isInstanceOf[ClaimHtlcTimeoutTx]) - val claimHtlcTimeoutTx = claimHtlcTimeout.txInfo.asInstanceOf[ClaimHtlcTimeoutTx].copy(confirmationTarget = ConfirmationTarget.Absolute(overrideHtlcTarget)) - - alice2blockchain.expectMsgType[WatchTxConfirmed] // commit tx - if (!bob.stateData.asInstanceOf[DATA_NORMAL].commitments.params.channelFeatures.paysDirectlyToWallet) alice2blockchain.expectMsgType[WatchTxConfirmed] // claim main output - alice2blockchain.expectMsgType[WatchOutputSpent] // claim-htlc-success tx - alice2blockchain.expectMsgType[WatchOutputSpent] // claim-htlc-timeout tx + alice2blockchain.expectWatchTxConfirmed(remoteCommitTx.txid) + alice2blockchain.expectWatchOutputsSpent(Seq(mainTx.input, anchorTx.input.outPoint) ++ Seq(claimHtlcSuccess.input, claimHtlcTimeout.input)) alice2blockchain.expectNoMessage(100 millis) - (remoteCommitTx.tx, claimHtlcSuccess.copy(txInfo = claimHtlcSuccessTx), claimHtlcTimeout.copy(txInfo = claimHtlcTimeoutTx)) + (remoteCommitTx, claimHtlcSuccess, claimHtlcTimeout) } private def testPublishClaimHtlcSuccess(f: Fixture, remoteCommitTx: Transaction, claimHtlcSuccess: PublishReplaceableTx, targetFeerate: FeeratePerKw): Transaction = { @@ -1675,7 +1675,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } } - def testClaimHtlcTxFeerateTooLowAnchors(nextCommit: Boolean): Unit = { + def testClaimHtlcTxFeerateTooLow(nextCommit: Boolean): Unit = { withFixture(Seq(11 millibtc), ChannelTypes.AnchorOutputs()) { f => import f._ @@ -1695,59 +1695,11 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("claim htlc tx feerate too low, lowering output amount") { - testClaimHtlcTxFeerateTooLowAnchors(nextCommit = false) + testClaimHtlcTxFeerateTooLow(nextCommit = false) } test("claim htlc tx feerate too low, lowering output amount (next remote commit)") { - testClaimHtlcTxFeerateTooLowAnchors(nextCommit = true) - } - - def testClaimHtlcTxFeerateTooLowStandard(nextCommit: Boolean): Unit = { - withFixture(Seq(11 millibtc), ChannelTypes.Standard()) { f => - import f._ - - val targetFeerate = FeeratePerKw(15_000 sat) - val (remoteCommitTx, claimHtlcSuccess, claimHtlcTimeout) = remoteCloseChannelWithHtlcs(f, aliceBlockHeight() + 300, nextCommit) - - // The Claim-HTLC-success tx will be immediately published. - setFeerate(targetFeerate) - val claimHtlcSuccessPublisher = createPublisher() - claimHtlcSuccessPublisher ! Publish(probe.ref, claimHtlcSuccess) - val claimHtlcSuccessTx = getMempoolTxs(1).head - val claimHtlcSuccessTargetFee = Transactions.weight2fee(targetFeerate, claimHtlcSuccessTx.weight.toInt) - assert(claimHtlcSuccessTargetFee * 0.9 <= claimHtlcSuccessTx.fees && claimHtlcSuccessTx.fees <= claimHtlcSuccessTargetFee * 1.1, s"actualFee=${claimHtlcSuccessTx.fees} targetFee=$claimHtlcSuccessTargetFee") - generateBlocks(6) - system.eventStream.publish(CurrentBlockHeight(currentBlockHeight(probe))) - val claimHtlcSuccessResult = probe.expectMsgType[TxConfirmed] - assert(claimHtlcSuccessResult.cmd == claimHtlcSuccess) - assert(claimHtlcSuccessResult.tx.txIn.map(_.outPoint.txid).contains(remoteCommitTx.txid)) - claimHtlcSuccessPublisher ! Stop - - // The Claim-HTLC-timeout will be published after the timeout. - val claimHtlcTimeoutPublisher = createPublisher() - claimHtlcTimeoutPublisher ! Publish(probe.ref, claimHtlcTimeout) - alice2blockchain.expectNoMessage(100 millis) - generateBlocks(144) - system.eventStream.publish(CurrentBlockHeight(currentBlockHeight(probe))) - val claimHtlcTimeoutTx = getMempoolTxs(1).head - val claimHtlcTimeoutTargetFee = Transactions.weight2fee(targetFeerate, claimHtlcTimeoutTx.weight.toInt) - assert(claimHtlcTimeoutTargetFee * 0.9 <= claimHtlcTimeoutTx.fees && claimHtlcTimeoutTx.fees <= claimHtlcTimeoutTargetFee * 1.1, s"actualFee=${claimHtlcTimeoutTx.fees} targetFee=$claimHtlcTimeoutTargetFee") - - generateBlocks(6) - system.eventStream.publish(CurrentBlockHeight(currentBlockHeight(probe))) - val claimHtlcTimeoutResult = probe.expectMsgType[TxConfirmed] - assert(claimHtlcTimeoutResult.cmd == claimHtlcTimeout) - assert(claimHtlcTimeoutResult.tx.txIn.map(_.outPoint.txid).contains(remoteCommitTx.txid)) - claimHtlcTimeoutPublisher ! Stop - } - } - - test("claim htlc tx feerate too low, lowering output amount (standard commitment format)") { - testClaimHtlcTxFeerateTooLowStandard(nextCommit = false) - } - - test("claim htlc tx feerate too low, lowering output amount (next remote commit, standard commitment format)") { - testClaimHtlcTxFeerateTooLowStandard(nextCommit = true) + testClaimHtlcTxFeerateTooLow(nextCommit = true) } test("claim htlc tx feerate way too low, skipping output") { @@ -1775,65 +1727,6 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } } - test("claim htlc tx not confirming, lowering output amount again (standard commitment format)") { - withFixture(Seq(11 millibtc), ChannelTypes.Standard()) { f => - import f._ - - val initialFeerate = FeeratePerKw(15_000 sat) - val targetFeerate = FeeratePerKw(20_000 sat) - - val (remoteCommitTx, claimHtlcSuccess, claimHtlcTimeout) = remoteCloseChannelWithHtlcs(f, aliceBlockHeight() + 144, nextCommit = false) - - val listener = TestProbe() - system.eventStream.subscribe(listener.ref, classOf[TransactionPublished]) - - // The Claim-HTLC-success tx will be immediately published. - setFeerate(initialFeerate, fastest = targetFeerate) - val claimHtlcSuccessPublisher = createPublisher() - claimHtlcSuccessPublisher ! Publish(probe.ref, claimHtlcSuccess) - val claimHtlcSuccessTx1 = getMempoolTxs(1).head - assert(listener.expectMsgType[TransactionPublished].tx.txid == claimHtlcSuccessTx1.txid) - - setFeerate(targetFeerate, fastest = targetFeerate) - system.eventStream.publish(CurrentBlockHeight(aliceBlockHeight() + 5)) - val claimHtlcSuccessTxId2 = listener.expectMsgType[TransactionPublished].tx.txid - assert(!isInMempool(claimHtlcSuccessTx1.txid)) - val claimHtlcSuccessTx2 = getMempoolTxs(1).head - assert(claimHtlcSuccessTx2.txid == claimHtlcSuccessTxId2) - assert(claimHtlcSuccessTx1.fees < claimHtlcSuccessTx2.fees) - val targetHtlcSuccessFee = Transactions.weight2fee(targetFeerate, claimHtlcSuccessTx2.weight.toInt) - assert(targetHtlcSuccessFee * 0.9 <= claimHtlcSuccessTx2.fees && claimHtlcSuccessTx2.fees <= targetHtlcSuccessFee * 1.1, s"actualFee=${claimHtlcSuccessTx2.fees} targetFee=$targetHtlcSuccessFee") - val finalHtlcSuccessTx = getMempool().head - assert(finalHtlcSuccessTx.txIn.length == 1) - assert(finalHtlcSuccessTx.txOut.length == 1) - assert(finalHtlcSuccessTx.txIn.head.outPoint.txid == remoteCommitTx.txid) - - // The Claim-HTLC-timeout will be published after the timeout. - setFeerate(initialFeerate, fastest = targetFeerate) - val claimHtlcTimeoutPublisher = createPublisher() - claimHtlcTimeoutPublisher ! Publish(probe.ref, claimHtlcTimeout) - generateBlocks(144) - system.eventStream.publish(CurrentBlockHeight(aliceBlockHeight() + 144)) - assert(probe.expectMsgType[TxConfirmed].tx.txid == finalHtlcSuccessTx.txid) // the claim-htlc-success is now confirmed - val claimHtlcTimeoutTx1 = getMempoolTxs(1).head - assert(listener.expectMsgType[TransactionPublished].tx.txid == claimHtlcTimeoutTx1.txid) - - setFeerate(targetFeerate, fastest = targetFeerate) - system.eventStream.publish(CurrentBlockHeight(aliceBlockHeight() + 145)) - val claimHtlcTimeoutTxId2 = listener.expectMsgType[TransactionPublished].tx.txid - assert(!isInMempool(claimHtlcTimeoutTx1.txid)) - val claimHtlcTimeoutTx2 = getMempoolTxs(1).head - assert(claimHtlcTimeoutTx2.txid == claimHtlcTimeoutTxId2) - assert(claimHtlcTimeoutTx1.fees < claimHtlcTimeoutTx2.fees) - val targetHtlcTimeoutFee = Transactions.weight2fee(targetFeerate, claimHtlcTimeoutTx2.weight.toInt) - assert(targetHtlcTimeoutFee * 0.9 <= claimHtlcTimeoutTx2.fees && claimHtlcTimeoutTx2.fees <= targetHtlcTimeoutFee * 1.1, s"actualFee=${claimHtlcTimeoutTx2.fees} targetFee=$targetHtlcTimeoutFee") - val finalHtlcTimeoutTx = getMempool().head - assert(finalHtlcTimeoutTx.txIn.length == 1) - assert(finalHtlcTimeoutTx.txOut.length == 1) - assert(finalHtlcTimeoutTx.txIn.head.outPoint.txid == remoteCommitTx.txid) - } - } - test("claim htlc tx not confirming, but cannot lower output amount again") { withFixture(Seq(11 millibtc), ChannelTypes.AnchorOutputs()) { f => import f._ @@ -1868,18 +1761,12 @@ class ReplaceableTxPublisherWithEclairSignerSpec extends ReplaceableTxPublisherS val seed = MnemonicCode.toSeed(MnemonicCode.toMnemonics(entropy), walletName) val keyManager = new LocalOnChainKeyManager(walletName, seed, TimestampSecond.now(), Block.RegtestGenesisBlock.hash) val walletRpcClient = new BasicBitcoinJsonRPCClient(Block.RegtestGenesisBlock.hash, rpcAuthMethod = bitcoinrpcauthmethod, host = "localhost", port = bitcoindRpcPort, wallet = Some(walletName)) - val walletClient = new BitcoinCoreClient(walletRpcClient, onChainKeyManager_opt = Some(keyManager)) with OnChainPubkeyCache { - lazy val pubkey = { - getP2wpkhPubkey().pipeTo(probe.ref) - probe.expectMsgType[PublicKey] - } - lazy val pubkeyScript = { + val walletClient = new BitcoinCoreClient(walletRpcClient, onChainKeyManager_opt = Some(keyManager)) with OnChainAddressCache { + lazy val pubkeyScript: Seq[ScriptElt] = { getReceivePublicKeyScript(None).pipeTo(probe.ref) probe.expectMsgType[Seq[ScriptElt]] } - override def getP2wpkhPubkey(renew: Boolean): PublicKey = pubkey - override def getReceivePublicKeyScript(renew: Boolean): Seq[ScriptElt] = pubkeyScript } createEclairBackedWallet(walletRpcClient, keyManager) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/TxPublisherSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/TxPublisherSpec.scala index 1f0647aa4d..3226996a10 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/TxPublisherSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/TxPublisherSpec.scala @@ -20,6 +20,7 @@ import akka.actor.typed.ActorRef import akka.actor.typed.scaladsl.ActorContext import akka.actor.typed.scaladsl.adapter.{ClassicActorSystemOps, TypedActorRefOps, actorRefAdapter} import akka.testkit.TestProbe +import fr.acinq.bitcoin.scalacompat.Crypto.PrivateKey import fr.acinq.bitcoin.scalacompat.{OutPoint, SatoshiLong, Transaction, TxIn, TxOut} import fr.acinq.eclair.TestUtils.randomTxId import fr.acinq.eclair.blockchain.CurrentBlockHeight @@ -27,16 +28,22 @@ import fr.acinq.eclair.blockchain.fee.{ConfirmationPriority, ConfirmationTarget} import fr.acinq.eclair.channel.publish import fr.acinq.eclair.channel.publish.TxPublisher.TxRejectedReason._ import fr.acinq.eclair.channel.publish.TxPublisher._ -import fr.acinq.eclair.transactions.Transactions.{ClaimLocalAnchorOutputTx, HtlcSuccessTx, InputInfo} -import fr.acinq.eclair.{BlockHeight, NodeParams, TestConstants, TestKitBaseClass, randomBytes32, randomKey} +import fr.acinq.eclair.crypto.keymanager.{LocalCommitmentKeys, RemoteCommitmentKeys} +import fr.acinq.eclair.transactions.Transactions._ +import fr.acinq.eclair.{BlockHeight, CltvExpiry, NodeParams, TestConstants, TestKitBaseClass, randomBytes32, randomBytes64, randomKey} import org.scalatest.Outcome import org.scalatest.funsuite.FixtureAnyFunSuiteLike +import scodec.bits.ByteVector import java.util.UUID import scala.concurrent.duration.DurationInt class TxPublisherSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike { + private val fundingKey: PrivateKey = randomKey() + private val localCommitKeys: LocalCommitmentKeys = LocalCommitmentKeys(randomKey(), randomKey().publicKey, randomKey().publicKey, randomKey(), randomKey().publicKey, randomKey().publicKey) + private val remoteCommitKeys: RemoteCommitmentKeys = RemoteCommitmentKeys(randomKey(), randomKey().publicKey, randomKey().publicKey, randomKey(), randomKey().publicKey, randomKey().publicKey) + case class FixtureParam(nodeParams: NodeParams, txPublisher: ActorRef[TxPublisher.Command], factory: TestProbe, probe: TestProbe) override def withFixture(test: OneArgTest): Outcome = { @@ -74,7 +81,7 @@ class TxPublisherSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike { import f._ val tx = Transaction(2, TxIn(OutPoint(randomTxId(), 1), Nil, 0) :: Nil, Nil, 0) - val cmd = PublishFinalTx(tx, tx.txIn.head.outPoint, 100_000 sat, "final-tx", 5 sat, None) + val cmd = PublishFinalTx(tx, tx.txIn.head.outPoint, "final-tx", 5 sat, None) txPublisher ! cmd val child = factory.expectMsgType[FinalTxPublisherSpawned].actor assert(child.expectMsgType[FinalTxPublisher.Publish].cmd == cmd) @@ -85,7 +92,7 @@ class TxPublisherSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike { val input = OutPoint(randomTxId(), 1) val tx1 = Transaction(2, TxIn(input, Nil, 0) :: Nil, Nil, 0) - val cmd1 = PublishFinalTx(tx1, input, 100_000 sat, "final-tx", 10 sat, None) + val cmd1 = PublishFinalTx(tx1, input, "final-tx", 10 sat, None) txPublisher ! cmd1 factory.expectMsgType[FinalTxPublisherSpawned] @@ -95,7 +102,7 @@ class TxPublisherSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike { // But a different tx spending the same main input is allowed: val tx2 = tx1.copy(txIn = tx1.txIn ++ Seq(TxIn(OutPoint(randomTxId(), 0), Nil, 0))) - val cmd2 = PublishFinalTx(tx2, input, 100_000 sat, "another-final-tx", 0 sat, None) + val cmd2 = PublishFinalTx(tx2, input, "another-final-tx", 0 sat, None) txPublisher ! cmd2 factory.expectMsgType[FinalTxPublisherSpawned] } @@ -105,7 +112,8 @@ class TxPublisherSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike { val confirmBefore = ConfirmationTarget.Absolute(nodeParams.currentBlockHeight + 12) val input = OutPoint(randomTxId(), 3) - val cmd = PublishReplaceableTx(ClaimLocalAnchorOutputTx(InputInfo(input, TxOut(25_000 sat, Nil), Nil), Transaction(2, TxIn(input, Nil, 0) :: Nil, Nil, 0), confirmBefore), null, null) + val anchorTx = ClaimLocalAnchorTx(fundingKey, localCommitKeys, InputInfo(input, TxOut(25_000 sat, Nil)), Transaction(2, TxIn(input, Nil, 0) :: Nil, Nil, 0), ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) + val cmd = PublishReplaceableTx(anchorTx, null, null, confirmBefore) txPublisher ! cmd val child = factory.expectMsgType[ReplaceableTxPublisherSpawned].actor val p = child.expectMsgType[ReplaceableTxPublisher.Publish] @@ -117,43 +125,43 @@ class TxPublisherSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike { val confirmBefore = nodeParams.currentBlockHeight + 12 val input = OutPoint(randomTxId(), 3) - val anchorTx = ClaimLocalAnchorOutputTx(InputInfo(input, TxOut(25_000 sat, Nil), Nil), Transaction(2, TxIn(input, Nil, 0) :: Nil, Nil, 0), ConfirmationTarget.Priority(ConfirmationPriority.Medium)) - val cmd = PublishReplaceableTx(anchorTx, null, null) + val anchorTx = ClaimLocalAnchorTx(fundingKey, localCommitKeys, InputInfo(input, TxOut(25_000 sat, Nil)), Transaction(2, TxIn(input, Nil, 0) :: Nil, Nil, 0), ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) + val cmd = PublishReplaceableTx(anchorTx, null, null, ConfirmationTarget.Priority(ConfirmationPriority.Medium)) txPublisher ! cmd val child = factory.expectMsgType[ReplaceableTxPublisherSpawned].actor assert(child.expectMsgType[ReplaceableTxPublisher.Publish].cmd == cmd) // We ignore duplicates that don't use a more aggressive priority: - txPublisher ! PublishReplaceableTx(anchorTx.copy(confirmationTarget = ConfirmationTarget.Priority(ConfirmationPriority.Slow)), null, null) + txPublisher ! cmd.copy(confirmationTarget = ConfirmationTarget.Priority(ConfirmationPriority.Slow)) child.expectNoMessage(100 millis) factory.expectNoMessage(100 millis) // But we allow duplicates with a more aggressive priority: - val cmdHigherPriority = cmd.copy(txInfo = anchorTx.copy(confirmationTarget = ConfirmationTarget.Priority(ConfirmationPriority.Fast))) + val cmdHigherPriority = cmd.copy(confirmationTarget = ConfirmationTarget.Priority(ConfirmationPriority.Fast)) txPublisher ! cmdHigherPriority child.expectMsg(ReplaceableTxPublisher.UpdateConfirmationTarget(ConfirmationTarget.Priority(ConfirmationPriority.Fast))) factory.expectNoMessage(100 millis) // Absolute confirmation targets replace relative priorities: - val cmdAbsoluteTarget = cmd.copy(txInfo = anchorTx.copy(confirmationTarget = ConfirmationTarget.Absolute(confirmBefore))) + val cmdAbsoluteTarget = cmd.copy(confirmationTarget = ConfirmationTarget.Absolute(confirmBefore)) txPublisher ! cmdAbsoluteTarget child.expectMsg(ReplaceableTxPublisher.UpdateConfirmationTarget(ConfirmationTarget.Absolute(confirmBefore))) factory.expectNoMessage(100 millis) // We ignore duplicates with a less aggressive confirmation target: - val cmdHigherTarget = cmd.copy(txInfo = anchorTx.copy(confirmationTarget = ConfirmationTarget.Absolute(confirmBefore + 1))) + val cmdHigherTarget = cmd.copy(confirmationTarget = ConfirmationTarget.Absolute(confirmBefore + 1)) txPublisher ! cmdHigherTarget child.expectNoMessage(100 millis) factory.expectNoMessage(100 millis) // But we update the confirmation target when it is more aggressive than previous attempts: - val cmdLowerTarget = cmd.copy(txInfo = anchorTx.copy(confirmationTarget = ConfirmationTarget.Absolute(confirmBefore - 6))) + val cmdLowerTarget = cmd.copy(confirmationTarget = ConfirmationTarget.Absolute(confirmBefore - 6)) txPublisher ! cmdLowerTarget child.expectMsg(ReplaceableTxPublisher.UpdateConfirmationTarget(ConfirmationTarget.Absolute(confirmBefore - 6))) factory.expectNoMessage(100 millis) // And we update our internal threshold accordingly: - val cmdInBetween = cmd.copy(txInfo = anchorTx.copy(confirmationTarget = ConfirmationTarget.Absolute(confirmBefore - 3))) + val cmdInBetween = cmd.copy(confirmationTarget = ConfirmationTarget.Absolute(confirmBefore - 3)) txPublisher ! cmdInBetween child.expectNoMessage(100 millis) factory.expectNoMessage(100 millis) @@ -164,18 +172,19 @@ class TxPublisherSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike { val input = OutPoint(randomTxId(), 3) val tx1 = Transaction(2, TxIn(input, Nil, 0) :: Nil, Nil, 0) - val cmd1 = PublishFinalTx(tx1, input, 100_000 sat, "final-tx-1", 5 sat, None) + val cmd1 = PublishFinalTx(tx1, input, "final-tx-1", 5 sat, None) txPublisher ! cmd1 val attempt1 = factory.expectMsgType[FinalTxPublisherSpawned].actor attempt1.expectMsgType[FinalTxPublisher.Publish] val tx2 = Transaction(2, TxIn(input, Nil, 0) :: TxIn(OutPoint(randomTxId(), 0), Nil, 3) :: Nil, Nil, 0) - val cmd2 = PublishFinalTx(tx2, input, 100_000 sat, "final-tx-2", 15 sat, None) + val cmd2 = PublishFinalTx(tx2, input, "final-tx-2", 15 sat, None) txPublisher ! cmd2 val attempt2 = factory.expectMsgType[FinalTxPublisherSpawned].actor attempt2.expectMsgType[FinalTxPublisher.Publish] - val cmd3 = PublishReplaceableTx(ClaimLocalAnchorOutputTx(InputInfo(input, TxOut(25_000 sat, Nil), Nil), Transaction(2, TxIn(input, Nil, 0) :: Nil, TxOut(20_000 sat, Nil) :: Nil, 0), ConfirmationTarget.Absolute(nodeParams.currentBlockHeight)), null, null) + val anchorTx = ClaimLocalAnchorTx(fundingKey, localCommitKeys, InputInfo(input, TxOut(25_000 sat, Nil)), Transaction(2, TxIn(input, Nil, 0) :: Nil, TxOut(20_000 sat, Nil) :: Nil, 0), ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) + val cmd3 = PublishReplaceableTx(anchorTx, null, null, ConfirmationTarget.Absolute(nodeParams.currentBlockHeight)) txPublisher ! cmd3 val attempt3 = factory.expectMsgType[ReplaceableTxPublisherSpawned].actor attempt3.expectMsgType[ReplaceableTxPublisher.Publish] @@ -192,12 +201,13 @@ class TxPublisherSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike { val input = OutPoint(randomTxId(), 3) val tx1 = Transaction(2, TxIn(input, Nil, 0) :: Nil, Nil, 0) - val cmd1 = PublishFinalTx(tx1, input, 100_000 sat, "final-tx-1", 0 sat, None) + val cmd1 = PublishFinalTx(tx1, input, "final-tx-1", 0 sat, None) txPublisher ! cmd1 val attempt1 = factory.expectMsgType[FinalTxPublisherSpawned] attempt1.actor.expectMsgType[FinalTxPublisher.Publish] - val cmd2 = PublishReplaceableTx(ClaimLocalAnchorOutputTx(InputInfo(input, TxOut(25_000 sat, Nil), Nil), Transaction(2, TxIn(input, Nil, 0) :: Nil, TxOut(20_000 sat, Nil) :: Nil, 0), ConfirmationTarget.Absolute(nodeParams.currentBlockHeight)), null, null) + val anchorTx = ClaimRemoteAnchorTx(fundingKey, remoteCommitKeys, InputInfo(input, TxOut(25_000 sat, Nil)), Transaction(2, TxIn(input, Nil, 0) :: Nil, TxOut(20_000 sat, Nil) :: Nil, 0), ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) + val cmd2 = PublishReplaceableTx(anchorTx, null, null, ConfirmationTarget.Absolute(nodeParams.currentBlockHeight)) txPublisher ! cmd2 val attempt2 = factory.expectMsgType[ReplaceableTxPublisherSpawned] attempt2.actor.expectMsgType[ReplaceableTxPublisher.Publish] @@ -216,7 +226,7 @@ class TxPublisherSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike { val input = OutPoint(randomTxId(), 3) val tx = Transaction(2, TxIn(input, Nil, 0) :: Nil, Nil, 0) - val cmd = PublishFinalTx(tx, input, 100_000 sat, "final-tx", 0 sat, None) + val cmd = PublishFinalTx(tx, input, "final-tx", 0 sat, None) txPublisher ! cmd val attempt1 = factory.expectMsgType[FinalTxPublisherSpawned] attempt1.actor.expectMsgType[FinalTxPublisher.Publish] @@ -234,10 +244,12 @@ class TxPublisherSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike { test("publishing attempt fails (not enough funds)") { f => import f._ - val target = nodeParams.currentBlockHeight + 12 + val expiry = CltvExpiry(nodeParams.currentBlockHeight + 12) val input = OutPoint(randomTxId(), 7) - val paymentHash = randomBytes32() - val cmd = PublishReplaceableTx(HtlcSuccessTx(InputInfo(input, TxOut(25_000 sat, Nil), Nil), Transaction(2, TxIn(input, Nil, 0) :: Nil, Nil, 0), paymentHash, 3, ConfirmationTarget.Absolute(target)), null, null) + val preimage = randomBytes32() + val remoteSig = randomBytes64() + val htlcTx = HtlcSuccessTx(localCommitKeys, InputInfo(input, TxOut(25_000 sat, Nil)), Transaction(2, TxIn(input, Nil, 0) :: Nil, Nil, 0), 3, expiry, preimage, remoteSig, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) + val cmd = PublishReplaceableTx(htlcTx, null, null, ConfirmationTarget.Absolute(expiry.blockHeight)) txPublisher ! cmd val attempt1 = factory.expectMsgType[ReplaceableTxPublisherSpawned] attempt1.actor.expectMsgType[ReplaceableTxPublisher.Publish] @@ -256,13 +268,13 @@ class TxPublisherSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike { import f._ val tx1 = Transaction(2, TxIn(OutPoint(randomTxId(), 1), Nil, 0) :: Nil, Nil, 0) - val cmd1 = PublishFinalTx(tx1, tx1.txIn.head.outPoint, 100_000 sat, "final-tx-1", 0 sat, None) + val cmd1 = PublishFinalTx(tx1, tx1.txIn.head.outPoint, "final-tx-1", 0 sat, None) txPublisher ! cmd1 val attempt1 = factory.expectMsgType[FinalTxPublisherSpawned] attempt1.actor.expectMsgType[FinalTxPublisher.Publish] val tx2 = Transaction(2, TxIn(OutPoint(randomTxId(), 0), Nil, 0) :: Nil, Nil, 0) - val cmd2 = PublishFinalTx(tx2, tx2.txIn.head.outPoint, 100_000 sat, "final-tx-2", 5 sat, None) + val cmd2 = PublishFinalTx(tx2, tx2.txIn.head.outPoint, "final-tx-2", 5 sat, None) txPublisher ! cmd2 val attempt2 = factory.expectMsgType[FinalTxPublisherSpawned] attempt2.actor.expectMsgType[FinalTxPublisher.Publish] @@ -283,7 +295,7 @@ class TxPublisherSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike { import f._ val tx = Transaction(2, TxIn(OutPoint(randomTxId(), 1), Nil, 0) :: Nil, Nil, 0) - val cmd = PublishFinalTx(tx, tx.txIn.head.outPoint, 100_000 sat, "final-tx", 5 sat, None) + val cmd = PublishFinalTx(tx, tx.txIn.head.outPoint, "final-tx", 5 sat, None) txPublisher ! cmd val attempt = factory.expectMsgType[FinalTxPublisherSpawned] attempt.actor.expectMsgType[FinalTxPublisher.Publish] @@ -301,7 +313,9 @@ class TxPublisherSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike { val input = OutPoint(randomTxId(), 7) val paymentHash = randomBytes32() - val cmd = PublishReplaceableTx(HtlcSuccessTx(InputInfo(input, TxOut(25_000 sat, Nil), Nil), Transaction(2, TxIn(input, Nil, 0) :: Nil, Nil, 0), paymentHash, 3, ConfirmationTarget.Absolute(nodeParams.currentBlockHeight)), null, null) + val remoteSig = randomBytes64() + val htlcTx = HtlcTimeoutTx(localCommitKeys, InputInfo(input, TxOut(25_000 sat, Nil)), Transaction(2, TxIn(input, Nil, 0) :: Nil, Nil, 0), paymentHash, 3, CltvExpiry(nodeParams.currentBlockHeight), remoteSig, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) + val cmd = PublishReplaceableTx(htlcTx, null, null, ConfirmationTarget.Absolute(nodeParams.currentBlockHeight)) txPublisher ! cmd val attempt1 = factory.expectMsgType[ReplaceableTxPublisherSpawned] attempt1.actor.expectMsgType[ReplaceableTxPublisher.Publish] @@ -320,7 +334,7 @@ class TxPublisherSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike { import f._ val tx = Transaction(2, TxIn(OutPoint(randomTxId(), 1), Nil, 0) :: Nil, Nil, 0) - val cmd = PublishFinalTx(tx, tx.txIn.head.outPoint, 100_000 sat, "final-tx", 5 sat, None) + val cmd = PublishFinalTx(tx, tx.txIn.head.outPoint, "final-tx", 5 sat, None) txPublisher ! cmd val attempt = factory.expectMsgType[FinalTxPublisherSpawned] attempt.actor.expectMsgType[FinalTxPublisher.Publish] @@ -337,7 +351,7 @@ class TxPublisherSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike { import f._ val tx = Transaction(2, TxIn(OutPoint(randomTxId(), 1), Nil, 0) :: Nil, Nil, 0) - val cmd = PublishFinalTx(tx, tx.txIn.head.outPoint, 100_000 sat, "final-tx", 5 sat, None) + val cmd = PublishFinalTx(tx, tx.txIn.head.outPoint, "final-tx", 5 sat, None) txPublisher ! cmd val attempt = factory.expectMsgType[FinalTxPublisherSpawned] attempt.actor.expectMsgType[FinalTxPublisher.Publish] diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/TxTimeLocksMonitorSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/TxTimeLocksMonitorSpec.scala index bebd58cade..2f77b3e53e 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/TxTimeLocksMonitorSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/TxTimeLocksMonitorSpec.scala @@ -20,7 +20,7 @@ import akka.actor.typed.ActorRef import akka.actor.typed.scaladsl.adapter.{ClassicActorSystemOps, actorRefAdapter} import akka.testkit.TestProbe import fr.acinq.bitcoin.scalacompat.{OutPoint, SatoshiLong, Script, Transaction, TxId, TxIn, TxOut} -import fr.acinq.eclair.blockchain.NoOpOnChainWallet +import fr.acinq.eclair.blockchain.DummyOnChainWallet import fr.acinq.eclair.channel.publish.TxPublisher.TxPublishContext import fr.acinq.eclair.channel.publish.TxTimeLocksMonitor.{CheckTx, TimeLocksOk, WrappedCurrentBlockHeight} import fr.acinq.eclair.{NodeParams, TestConstants, TestKitBaseClass, randomKey} @@ -36,7 +36,7 @@ class TxTimeLocksMonitorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik case class FixtureParam(nodeParams: NodeParams, monitor: ActorRef[TxTimeLocksMonitor.Command], bitcoinClient: BitcoinTestClient, probe: TestProbe) - case class BitcoinTestClient() extends NoOpOnChainWallet { + case class BitcoinTestClient() extends DummyOnChainWallet { private val requests = collection.concurrent.TrieMap.empty[TxId, Promise[Option[Int]]] override def getTxConfirmations(txId: TxId)(implicit ec: ExecutionContext): Future[Option[Int]] = { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala index 063ff34a1b..a2aa6ae263 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala @@ -22,22 +22,22 @@ import akka.testkit.{TestFSMRef, TestKit, TestProbe} import com.softwaremill.quicklens.ModifyPimp import fr.acinq.bitcoin.ScriptFlags import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey -import fr.acinq.bitcoin.scalacompat.{ByteVector32, Crypto, SatoshiLong, Script, Transaction} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, Crypto, Satoshi, SatoshiLong, Script, Transaction} import fr.acinq.eclair.TestConstants.{Alice, Bob} import fr.acinq.eclair._ import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ import fr.acinq.eclair.blockchain.fee.{FeeratePerKw, FeeratesPerKw} -import fr.acinq.eclair.blockchain.{DummyOnChainWallet, OnChainWallet, OnChainPubkeyCache, SingleKeyOnChainWallet} +import fr.acinq.eclair.blockchain.{OnChainAddressCache, OnChainWallet, SingleKeyOnChainWallet} import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.fsm.Channel -import fr.acinq.eclair.channel.publish.TxPublisher import fr.acinq.eclair.channel.publish.TxPublisher.{PublishFinalTx, PublishReplaceableTx} -import fr.acinq.eclair.channel.states.ChannelStateTestsBase.FakeTxPublisherFactory +import fr.acinq.eclair.channel.publish._ +import fr.acinq.eclair.channel.states.ChannelStateTestsBase.{FakeTxPublisherFactory, PimpTestFSM} import fr.acinq.eclair.payment.send.SpontaneousRecipient import fr.acinq.eclair.payment.{Invoice, OutgoingPaymentPacket} +import fr.acinq.eclair.reputation.Reputation import fr.acinq.eclair.router.Router.{ChannelHop, HopRelayParams, Route} import fr.acinq.eclair.testutils.PimpTestProbe.convert -import fr.acinq.eclair.transactions.Transactions import fr.acinq.eclair.transactions.Transactions._ import fr.acinq.eclair.wire.protocol._ import org.scalatest.Assertions @@ -54,14 +54,6 @@ object ChannelStateTestsTags { val DualFunding = "dual_funding" /** If set, a liquidity ads will be used when opening a channel. */ val LiquidityAds = "liquidity_ads" - /** If set, channels will use option_static_remotekey. */ - val StaticRemoteKey = "static_remotekey" - /** If set, channels will use option_anchor_outputs. */ - val AnchorOutputs = "anchor_outputs" - /** If set, channels will use option_anchors_zero_fee_htlc_tx. */ - val AnchorOutputsZeroFeeHtlcTxs = "anchor_outputs_zero_fee_htlc_tx" - /** If set, channels will use option_shutdown_anysegwit. */ - val ShutdownAnySegwit = "shutdown_anysegwit" /** If set, channels will be public (otherwise we don't announce them by default). */ val ChannelsPublic = "channels_public" /** If set, initial announcement_signatures and channel_updates will not be intercepted and ignored. */ @@ -92,12 +84,20 @@ object ChannelStateTestsTags { val RejectRbfAttempts = "reject_rbf_attempts" /** If set, the non-initiator will require a 1-block delay between RBF attempts. */ val DelayRbfAttempts = "delay_rbf_attempts" + /** If set, the non-initiator will not enforce any restriction between RBF attempts. */ + val UnlimitedRbfAttempts = "unlimited_rbf_attempts" /** If set, channels will adapt their max HTLC amount to the available balance. */ val AdaptMaxHtlcAmount = "adapt_max_htlc_amount" /** If set, closing will use option_simple_close. */ val SimpleClose = "option_simple_close" /** If set, disable option_splice for one node. */ val DisableSplice = "disable_splice" + /** If set, channels will use the anchor outputs format where HTLC txs have a non-0 feerate, which is used for pre-taproot Phoenix channels. */ + val AnchorOutputsPhoenix = "anchor_outputs_phoenix" + /** If set, channels will use taproot like Phoenix does. */ + val OptionSimpleTaprootPhoenix = "option_simple_taproot_phoenix" + /** If set, channels will use taproot. */ + val OptionSimpleTaproot = "option_simple_taproot" } trait ChannelStateTestsBase extends Assertions with Eventually { @@ -113,12 +113,84 @@ trait ChannelStateTestsBase extends Assertions with Eventually { alice2relayer: TestProbe, bob2relayer: TestProbe, channelUpdateListener: TestProbe, - wallet: OnChainWallet with OnChainPubkeyCache, + aliceWallet: OnChainWallet with OnChainAddressCache, + bobWallet: OnChainWallet with OnChainAddressCache, alicePeer: TestProbe, bobPeer: TestProbe) { def currentBlockHeight: BlockHeight = alice.underlyingActor.nodeParams.currentBlockHeight } + case class ChannelParamsFixture(aliceChannelParams: LocalChannelParams, + aliceCommitParams: CommitParams, + aliceInitFeatures: Features[InitFeature], + bobChannelParams: LocalChannelParams, + bobCommitParams: CommitParams, + bobInitFeatures: Features[InitFeature], + channelType: SupportedChannelType, + alice2bob: TestProbe, + bob2alice: TestProbe, + aliceOpenReplyTo: TestProbe) { + def initChannelAlice(fundingAmount: Satoshi, + dualFunded: Boolean = false, + requireConfirmedInputs: Boolean = false, + channelFlags: ChannelFlags = ChannelFlags(announceChannel = false), + requestFunding_opt: Option[LiquidityAds.RequestFunding] = None, + pushAmount_opt: Option[MilliSatoshi] = None): INPUT_INIT_CHANNEL_INITIATOR = { + INPUT_INIT_CHANNEL_INITIATOR( + temporaryChannelId = ByteVector32.Zeroes, + fundingAmount = fundingAmount, + dualFunded = dualFunded, + commitTxFeerate = channelType match { + case _: ChannelTypes.AnchorOutputs | ChannelTypes.SimpleTaprootChannelsPhoenix => TestConstants.phoenixCommitFeeratePerKw + case _ => TestConstants.anchorOutputsFeeratePerKw + }, + fundingTxFeerate = TestConstants.feeratePerKw, + fundingTxFeeBudget_opt = None, + pushAmount_opt = pushAmount_opt, + requireConfirmedInputs = requireConfirmedInputs, + requestFunding_opt = requestFunding_opt, + localChannelParams = aliceChannelParams, + proposedCommitParams = ProposedCommitParams( + localDustLimit = aliceCommitParams.dustLimit, + localHtlcMinimum = aliceCommitParams.htlcMinimum, + localMaxHtlcValueInFlight = aliceCommitParams.maxHtlcValueInFlight, + localMaxAcceptedHtlcs = aliceCommitParams.maxAcceptedHtlcs, + toRemoteDelay = bobCommitParams.toSelfDelay + ), + remote = alice2bob.ref, + remoteInit = Init(bobInitFeatures), + channelFlags = channelFlags, + channelConfig = ChannelConfig.standard, + channelType = channelType, + replyTo = aliceOpenReplyTo.ref.toTyped + ) + } + + def initChannelBob(fundingContribution_opt: Option[LiquidityAds.AddFunding] = None, + dualFunded: Boolean = false, + requireConfirmedInputs: Boolean = false, + pushAmount_opt: Option[MilliSatoshi] = None): INPUT_INIT_CHANNEL_NON_INITIATOR = { + INPUT_INIT_CHANNEL_NON_INITIATOR( + temporaryChannelId = ByteVector32.Zeroes, + fundingContribution_opt = fundingContribution_opt, + dualFunded = dualFunded, + pushAmount_opt = pushAmount_opt, + requireConfirmedInputs = requireConfirmedInputs, + localChannelParams = bobChannelParams, + proposedCommitParams = ProposedCommitParams( + localDustLimit = bobCommitParams.dustLimit, + localHtlcMinimum = bobCommitParams.htlcMinimum, + localMaxHtlcValueInFlight = bobCommitParams.maxHtlcValueInFlight, + localMaxAcceptedHtlcs = bobCommitParams.maxAcceptedHtlcs, + toRemoteDelay = aliceCommitParams.toSelfDelay + ), + remote = bob2alice.ref, + remoteInit = Init(aliceInitFeatures), + channelConfig = ChannelConfig.standard, + channelType = channelType) + } + } + implicit val system: ActorSystem val systemA: ActorSystem = ActorSystem("system-alice") val systemB: ActorSystem = ActorSystem("system-bob") @@ -126,7 +198,7 @@ trait ChannelStateTestsBase extends Assertions with Eventually { system.registerOnTermination(TestKit.shutdownActorSystem(systemA)) system.registerOnTermination(TestKit.shutdownActorSystem(systemB)) - def init(nodeParamsA: NodeParams = TestConstants.Alice.nodeParams, nodeParamsB: NodeParams = TestConstants.Bob.nodeParams, wallet_opt: Option[OnChainWallet with OnChainPubkeyCache] = None, tags: Set[String] = Set.empty): SetupFixture = { + def init(nodeParamsA: NodeParams = TestConstants.Alice.nodeParams, nodeParamsB: NodeParams = TestConstants.Bob.nodeParams, walletA_opt: Option[OnChainWallet with OnChainAddressCache] = None, walletB_opt: Option[OnChainWallet with OnChainAddressCache] = None, tags: Set[String] = Set.empty): SetupFixture = { val aliceOpenReplyTo = TestProbe() val alice2bob = TestProbe() val bob2alice = TestProbe() @@ -150,6 +222,8 @@ trait ChannelStateTestsBase extends Assertions with Eventually { .modify(_.channelConf.dustLimit).setToIf(tags.contains(ChannelStateTestsTags.HighDustLimitDifferenceBobAlice))(1000 sat) .modify(_.channelConf.maxRemoteDustLimit).setToIf(tags.contains(ChannelStateTestsTags.HighDustLimitDifferenceAliceBob))(10000 sat) .modify(_.channelConf.maxRemoteDustLimit).setToIf(tags.contains(ChannelStateTestsTags.HighDustLimitDifferenceBobAlice))(10000 sat) + .modify(_.channelConf.remoteRbfLimits.maxAttempts).setToIf(tags.contains(ChannelStateTestsTags.UnlimitedRbfAttempts))(100) + .modify(_.channelConf.remoteRbfLimits.attemptDeltaBlocks).setToIf(tags.contains(ChannelStateTestsTags.UnlimitedRbfAttempts))(0) .modify(_.onChainFeeConf.defaultFeerateTolerance.ratioLow).setToIf(tags.contains(ChannelStateTestsTags.HighFeerateMismatchTolerance))(0.000001) .modify(_.onChainFeeConf.defaultFeerateTolerance.ratioHigh).setToIf(tags.contains(ChannelStateTestsTags.HighFeerateMismatchTolerance))(1000000) .modify(_.onChainFeeConf.spendAnchorWithoutHtlcs).setToIf(tags.contains(ChannelStateTestsTags.DontSpendAnchorWithoutHtlcs))(false) @@ -160,97 +234,107 @@ trait ChannelStateTestsBase extends Assertions with Eventually { .modify(_.channelConf.maxRemoteDustLimit).setToIf(tags.contains(ChannelStateTestsTags.HighDustLimitDifferenceAliceBob))(10000 sat) .modify(_.channelConf.maxRemoteDustLimit).setToIf(tags.contains(ChannelStateTestsTags.HighDustLimitDifferenceBobAlice))(10000 sat) .modify(_.channelConf.remoteRbfLimits.maxAttempts).setToIf(tags.contains(ChannelStateTestsTags.RejectRbfAttempts))(0) + .modify(_.channelConf.remoteRbfLimits.maxAttempts).setToIf(tags.contains(ChannelStateTestsTags.UnlimitedRbfAttempts))(100) .modify(_.channelConf.remoteRbfLimits.attemptDeltaBlocks).setToIf(tags.contains(ChannelStateTestsTags.DelayRbfAttempts))(1) + .modify(_.channelConf.remoteRbfLimits.attemptDeltaBlocks).setToIf(tags.contains(ChannelStateTestsTags.UnlimitedRbfAttempts))(0) .modify(_.onChainFeeConf.defaultFeerateTolerance.ratioLow).setToIf(tags.contains(ChannelStateTestsTags.HighFeerateMismatchTolerance))(0.000001) .modify(_.onChainFeeConf.defaultFeerateTolerance.ratioHigh).setToIf(tags.contains(ChannelStateTestsTags.HighFeerateMismatchTolerance))(1000000) .modify(_.onChainFeeConf.spendAnchorWithoutHtlcs).setToIf(tags.contains(ChannelStateTestsTags.DontSpendAnchorWithoutHtlcs))(false) .modify(_.channelConf.balanceThresholds).setToIf(tags.contains(ChannelStateTestsTags.AdaptMaxHtlcAmount))(Seq(Channel.BalanceThreshold(1_000 sat, 0 sat), Channel.BalanceThreshold(5_000 sat, 1_000 sat), Channel.BalanceThreshold(10_000 sat, 5_000 sat))) - val wallet = wallet_opt match { - case Some(wallet) => wallet - case None => if (tags.contains(ChannelStateTestsTags.DualFunding)) new SingleKeyOnChainWallet() else new DummyOnChainWallet() - } + val aliceWallet = walletA_opt.getOrElse(new SingleKeyOnChainWallet()) val alice: TestFSMRef[ChannelState, ChannelData, Channel] = { implicit val system: ActorSystem = systemA - TestFSMRef(new Channel(finalNodeParamsA, wallet, finalNodeParamsB.nodeId, alice2blockchain.ref, alice2relayer.ref, FakeTxPublisherFactory(alice2blockchain)), alicePeer.ref) + TestFSMRef(new Channel(finalNodeParamsA, TestConstants.Alice.channelKeys(), aliceWallet, finalNodeParamsB.nodeId, alice2blockchain.ref, alice2relayer.ref, FakeTxPublisherFactory(alice2blockchain)), alicePeer.ref) } + val bobWallet = walletB_opt.getOrElse(new SingleKeyOnChainWallet()) val bob: TestFSMRef[ChannelState, ChannelData, Channel] = { implicit val system: ActorSystem = systemB - TestFSMRef(new Channel(finalNodeParamsB, wallet, finalNodeParamsA.nodeId, bob2blockchain.ref, bob2relayer.ref, FakeTxPublisherFactory(bob2blockchain)), bobPeer.ref) + TestFSMRef(new Channel(finalNodeParamsB, TestConstants.Bob.channelKeys(), bobWallet, finalNodeParamsA.nodeId, bob2blockchain.ref, bob2relayer.ref, FakeTxPublisherFactory(bob2blockchain)), bobPeer.ref) } - SetupFixture(alice, bob, aliceOpenReplyTo, alice2bob, bob2alice, alice2blockchain, bob2blockchain, router, alice2relayer, bob2relayer, channelUpdateListener, wallet, alicePeer, bobPeer) + SetupFixture(alice, bob, aliceOpenReplyTo, alice2bob, bob2alice, alice2blockchain, bob2blockchain, router, alice2relayer, bob2relayer, channelUpdateListener, aliceWallet, bobWallet, alicePeer, bobPeer) } def updateInitFeatures(nodeParamsA: NodeParams, nodeParamsB: NodeParams, tags: Set[String]): (NodeParams, NodeParams) = { val nodeParamsA1 = nodeParamsA.copy(features = nodeParamsA.features .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.DisableWumbo))(_.removed(Features.Wumbo)) - .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.StaticRemoteKey))(_.updated(Features.StaticRemoteKey, FeatureSupport.Optional)) - .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.AnchorOutputs))(_.updated(Features.StaticRemoteKey, FeatureSupport.Optional).updated(Features.AnchorOutputs, FeatureSupport.Optional)) - .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs))(_.updated(Features.StaticRemoteKey, FeatureSupport.Optional).updated(Features.AnchorOutputs, FeatureSupport.Optional).updated(Features.AnchorOutputsZeroFeeHtlcTx, FeatureSupport.Optional)) - .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.ShutdownAnySegwit))(_.updated(Features.ShutdownAnySegwit, FeatureSupport.Optional)) .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.UpfrontShutdownScript))(_.updated(Features.UpfrontShutdownScript, FeatureSupport.Optional)) .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.ZeroConf))(_.updated(Features.ZeroConf, FeatureSupport.Optional)) .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.ScidAlias))(_.updated(Features.ScidAlias, FeatureSupport.Optional)) .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.DualFunding))(_.updated(Features.DualFunding, FeatureSupport.Optional)) .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.SimpleClose))(_.updated(Features.SimpleClose, FeatureSupport.Optional)) + .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.AnchorOutputsPhoenix))(_.removed(Features.AnchorOutputsZeroFeeHtlcTx).updated(Features.AnchorOutputs, FeatureSupport.Optional)) + .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.OptionSimpleTaprootPhoenix))(_.removed(Features.SimpleTaprootChannelsStaging).updated(Features.SimpleTaprootChannelsPhoenix, FeatureSupport.Optional).updated(Features.PhoenixZeroReserve, FeatureSupport.Optional)) + .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.OptionSimpleTaproot))(_.updated(Features.SimpleTaprootChannelsStaging, FeatureSupport.Optional)) ) val nodeParamsB1 = nodeParamsB.copy(features = nodeParamsB.features .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.DisableWumbo))(_.removed(Features.Wumbo)) - .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.StaticRemoteKey))(_.updated(Features.StaticRemoteKey, FeatureSupport.Optional)) - .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.AnchorOutputs))(_.updated(Features.StaticRemoteKey, FeatureSupport.Optional).updated(Features.AnchorOutputs, FeatureSupport.Optional)) - .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs))(_.updated(Features.StaticRemoteKey, FeatureSupport.Optional).updated(Features.AnchorOutputs, FeatureSupport.Optional).updated(Features.AnchorOutputsZeroFeeHtlcTx, FeatureSupport.Optional)) - .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.ShutdownAnySegwit))(_.updated(Features.ShutdownAnySegwit, FeatureSupport.Optional)) .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.UpfrontShutdownScript))(_.updated(Features.UpfrontShutdownScript, FeatureSupport.Optional)) .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.ZeroConf))(_.updated(Features.ZeroConf, FeatureSupport.Optional)) .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.ScidAlias))(_.updated(Features.ScidAlias, FeatureSupport.Optional)) .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.DualFunding))(_.updated(Features.DualFunding, FeatureSupport.Optional)) .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.SimpleClose))(_.updated(Features.SimpleClose, FeatureSupport.Optional)) .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.DisableSplice))(_.removed(Features.SplicePrototype)) + .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.AnchorOutputsPhoenix))(_.removed(Features.AnchorOutputsZeroFeeHtlcTx).updated(Features.AnchorOutputs, FeatureSupport.Optional)) + .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.OptionSimpleTaprootPhoenix))(_.removed(Features.SimpleTaprootChannelsStaging).updated(Features.SimpleTaprootChannelsPhoenix, FeatureSupport.Optional).updated(Features.PhoenixZeroReserve, FeatureSupport.Optional)) + .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.OptionSimpleTaproot))(_.updated(Features.SimpleTaprootChannelsStaging, FeatureSupport.Optional)) ) (nodeParamsA1, nodeParamsB1) } - def computeFeatures(setup: SetupFixture, tags: Set[String], channelFlags: ChannelFlags): (LocalParams, LocalParams, SupportedChannelType) = { + /** Pick the channel type based on local and remote feature bits. */ + def channelTypeFromFeatures(localFeatures: Features[InitFeature], remoteFeatures: Features[InitFeature], announceChannel: Boolean): SupportedChannelType = { + def canUse(feature: InitFeature): Boolean = Features.canUseFeature(localFeatures, remoteFeatures, feature) + + val scidAlias = canUse(Features.ScidAlias) && !announceChannel // alias feature is incompatible with public channel + val zeroConf = canUse(Features.ZeroConf) + if (canUse(Features.SimpleTaprootChannelsStaging)) { + ChannelTypes.SimpleTaprootChannelsStaging(scidAlias, zeroConf) + } else if (canUse(Features.SimpleTaprootChannelsPhoenix)) { + ChannelTypes.SimpleTaprootChannelsPhoenix + } else if (canUse(Features.AnchorOutputsZeroFeeHtlcTx)) { + ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias, zeroConf) + } else if (canUse(Features.AnchorOutputs)) { + ChannelTypes.AnchorOutputs(scidAlias, zeroConf) + } else { + fail("cannot figure out channel_type from features") + } + } + + def computeChannelParams(setup: SetupFixture, tags: Set[String], channelFlags: ChannelFlags = ChannelFlags(announceChannel = false)): ChannelParamsFixture = { import setup._ val (nodeParamsA, nodeParamsB) = updateInitFeatures(alice.underlyingActor.nodeParams, bob.underlyingActor.nodeParams, tags) val aliceInitFeatures = nodeParamsA.features.initFeatures() val bobInitFeatures = nodeParamsB.features.initFeatures() - - val channelType = ChannelTypes.defaultFromFeatures(aliceInitFeatures, bobInitFeatures, announceChannel = channelFlags.announceChannel) - - // those features can only be enabled with AnchorOutputsZeroFeeHtlcTxs, this is to prevent incompatible test configurations - if (tags.contains(ChannelStateTestsTags.ZeroConf)) assert(tags.contains(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), "invalid test configuration") - if (tags.contains(ChannelStateTestsTags.ScidAlias)) assert(tags.contains(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), "invalid test configuration") + val channelType = channelTypeFromFeatures(aliceInitFeatures, bobInitFeatures, announceChannel = channelFlags.announceChannel) implicit val ec: scala.concurrent.ExecutionContext = scala.concurrent.ExecutionContext.global - val aliceParams = Alice.channelParams + val aliceChannelParams = Alice.channelParams .modify(_.initFeatures).setTo(aliceInitFeatures) - .modify(_.walletStaticPaymentBasepoint).setToIf(channelType.paysDirectlyToWallet)(Some(Await.result(wallet.getP2wpkhPubkey(), 10 seconds))) - .modify(_.maxHtlcValueInFlightMsat).setToIf(tags.contains(ChannelStateTestsTags.NoMaxHtlcValueInFlight))(Long.MaxValue.msat) - .modify(_.maxHtlcValueInFlightMsat).setToIf(tags.contains(ChannelStateTestsTags.AliceLowMaxHtlcValueInFlight))(150_000_000 msat) + .modify(_.initialRequestedChannelReserve_opt).setToIf(tags.contains(ChannelStateTestsTags.DualFunding))(None) + .modify(_.upfrontShutdownScript_opt).setToIf(tags.contains(ChannelStateTestsTags.UpfrontShutdownScript))(Some(Script.write(Await.result(aliceWallet.getReceivePublicKeyScript(), 10 seconds)))) + val aliceCommitParams = CommitParams(nodeParamsA.channelConf.dustLimit, nodeParamsA.channelConf.htlcMinimum, nodeParamsA.channelConf.maxHtlcValueInFlight(TestConstants.fundingSatoshis, unlimited = false), nodeParamsA.channelConf.maxAcceptedHtlcs, nodeParamsB.channelConf.toRemoteDelay) + .modify(_.maxHtlcValueInFlight).setToIf(tags.contains(ChannelStateTestsTags.NoMaxHtlcValueInFlight))(UInt64.MaxValue) + .modify(_.maxHtlcValueInFlight).setToIf(tags.contains(ChannelStateTestsTags.AliceLowMaxHtlcValueInFlight))(UInt64(150_000_000)) .modify(_.dustLimit).setToIf(tags.contains(ChannelStateTestsTags.HighDustLimitDifferenceAliceBob))(5000 sat) .modify(_.dustLimit).setToIf(tags.contains(ChannelStateTestsTags.HighDustLimitDifferenceBobAlice))(1000 sat) - .modify(_.initialRequestedChannelReserve_opt).setToIf(tags.contains(ChannelStateTestsTags.DualFunding))(None) - .modify(_.upfrontShutdownScript_opt).setToIf(tags.contains(ChannelStateTestsTags.UpfrontShutdownScript))(Some(Script.write(Script.pay2wpkh(Await.result(wallet.getP2wpkhPubkey(), 10 seconds))))) - val bobParams = Bob.channelParams + val bobChannelParams = Bob.channelParams .modify(_.initFeatures).setTo(bobInitFeatures) - .modify(_.walletStaticPaymentBasepoint).setToIf(channelType.paysDirectlyToWallet)(Some(Await.result(wallet.getP2wpkhPubkey(), 10 seconds))) - .modify(_.maxHtlcValueInFlightMsat).setToIf(tags.contains(ChannelStateTestsTags.NoMaxHtlcValueInFlight))(Long.MaxValue.msat) + .modify(_.initialRequestedChannelReserve_opt).setToIf(tags.contains(ChannelStateTestsTags.DualFunding))(None) + .modify(_.upfrontShutdownScript_opt).setToIf(tags.contains(ChannelStateTestsTags.UpfrontShutdownScript))(Some(Script.write(Await.result(bobWallet.getReceivePublicKeyScript(), 10 seconds)))) + val bobCommitParams = CommitParams(nodeParamsB.channelConf.dustLimit, nodeParamsB.channelConf.htlcMinimum, nodeParamsB.channelConf.maxHtlcValueInFlight(TestConstants.fundingSatoshis, unlimited = false), nodeParamsB.channelConf.maxAcceptedHtlcs, nodeParamsA.channelConf.toRemoteDelay) + .modify(_.maxHtlcValueInFlight).setToIf(tags.contains(ChannelStateTestsTags.NoMaxHtlcValueInFlight))(UInt64.MaxValue) .modify(_.dustLimit).setToIf(tags.contains(ChannelStateTestsTags.HighDustLimitDifferenceAliceBob))(1000 sat) .modify(_.dustLimit).setToIf(tags.contains(ChannelStateTestsTags.HighDustLimitDifferenceBobAlice))(5000 sat) - .modify(_.initialRequestedChannelReserve_opt).setToIf(tags.contains(ChannelStateTestsTags.DualFunding))(None) - .modify(_.upfrontShutdownScript_opt).setToIf(tags.contains(ChannelStateTestsTags.UpfrontShutdownScript))(Some(Script.write(Script.pay2wpkh(Await.result(wallet.getP2wpkhPubkey(), 10 seconds))))) - (aliceParams, bobParams, channelType) + ChannelParamsFixture(aliceChannelParams, aliceCommitParams, aliceInitFeatures, bobChannelParams, bobCommitParams, bobInitFeatures, channelType, alice2bob, bob2alice, aliceOpenReplyTo) } def reachNormal(setup: SetupFixture, tags: Set[String] = Set.empty): Transaction = { import setup._ - val channelConfig = ChannelConfig.standard val channelFlags = ChannelFlags(announceChannel = tags.contains(ChannelStateTestsTags.ChannelsPublic)) - val (aliceParams, bobParams, channelType) = computeFeatures(setup, tags, channelFlags) - val commitTxFeerate = if (tags.contains(ChannelStateTestsTags.AnchorOutputs) || tags.contains(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) TestConstants.anchorOutputsFeeratePerKw else TestConstants.feeratePerKw + val channelParams = computeChannelParams(setup, tags, channelFlags) val fundingAmount = TestConstants.fundingSatoshis val initiatorPushAmount = if (tags.contains(ChannelStateTestsTags.NoPushAmount)) None else Some(TestConstants.initiatorPushAmount) val nonInitiatorPushAmount = if (tags.contains(ChannelStateTestsTags.NonInitiatorPushAmount)) Some(TestConstants.nonInitiatorPushAmount) else None @@ -271,11 +355,9 @@ trait ChannelStateTestsBase extends Assertions with Eventually { val eventListener = TestProbe() systemA.eventStream.subscribe(eventListener.ref, classOf[TransactionPublished]) - val aliceInit = Init(aliceParams.initFeatures) - val bobInit = Init(bobParams.initFeatures) - alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, fundingAmount, dualFunded, commitTxFeerate, TestConstants.feeratePerKw, fundingTxFeeBudget_opt = None, initiatorPushAmount, requireConfirmedInputs = false, requestFunds_opt, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType, replyTo = aliceOpenReplyTo.ref.toTyped) + alice ! channelParams.initChannelAlice(fundingAmount, dualFunded, pushAmount_opt = initiatorPushAmount, requestFunding_opt = requestFunds_opt, channelFlags = channelFlags) assert(alice2blockchain.expectMsgType[TxPublisher.SetChannelId].channelId == ByteVector32.Zeroes) - bob ! INPUT_INIT_CHANNEL_NON_INITIATOR(ByteVector32.Zeroes, nonInitiatorFunding_opt, dualFunded, nonInitiatorPushAmount, requireConfirmedInputs = false, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) + bob ! channelParams.initChannelBob(nonInitiatorFunding_opt, dualFunded, pushAmount_opt = nonInitiatorPushAmount) assert(bob2blockchain.expectMsgType[TxPublisher.SetChannelId].channelId == ByteVector32.Zeroes) val fundingTx = if (!dualFunded) { @@ -292,7 +374,7 @@ trait ChannelStateTestsBase extends Assertions with Eventually { val fundingTx = eventListener.expectMsgType[TransactionPublished].tx eventually(assert(alice.stateName == WAIT_FOR_FUNDING_CONFIRMED)) eventually(assert(bob.stateName == WAIT_FOR_FUNDING_CONFIRMED)) - if (channelType.features.contains(Features.ZeroConf)) { + if (channelParams.channelType.features.contains(Features.ZeroConf) || alice.commitments.channelParams.zeroConf) { alice2blockchain.expectMsgType[WatchPublished] bob2blockchain.expectMsgType[WatchPublished] alice ! WatchPublishedTriggered(fundingTx) @@ -346,7 +428,7 @@ trait ChannelStateTestsBase extends Assertions with Eventually { val fundingTx = eventListener.expectMsgType[TransactionPublished].tx eventually(assert(alice.stateName == WAIT_FOR_DUAL_FUNDING_CONFIRMED)) eventually(assert(bob.stateName == WAIT_FOR_DUAL_FUNDING_CONFIRMED)) - if (channelType.features.contains(Features.ZeroConf)) { + if (channelParams.channelType.features.contains(Features.ZeroConf) || alice.commitments.channelParams.zeroConf) { alice2blockchain.expectMsgType[WatchPublished] bob2blockchain.expectMsgType[WatchPublished] alice ! WatchPublishedTriggered(fundingTx) @@ -369,7 +451,7 @@ trait ChannelStateTestsBase extends Assertions with Eventually { } if (!tags.contains(ChannelStateTestsTags.DoNotInterceptGossip)) { - if (tags.contains(ChannelStateTestsTags.ChannelsPublic) && !channelType.features.contains(Features.ZeroConf)) { + if (tags.contains(ChannelStateTestsTags.ChannelsPublic) && !channelParams.channelType.features.contains(Features.ZeroConf)) { alice2bob.expectMsgType[AnnouncementSignatures] bob2alice.expectMsgType[AnnouncementSignatures] } @@ -386,6 +468,8 @@ trait ChannelStateTestsBase extends Assertions with Eventually { fundingTx } + def channelId(a: TestFSMRef[ChannelState, ChannelData, Channel]): ByteVector32 = a.stateData.channelId + def localOrigin(replyTo: ActorRef): Origin.Hot = Origin.Hot(replyTo, localUpstream()) def localUpstream(): Upstream.Local = Upstream.Local(UUID.randomUUID()) @@ -406,7 +490,7 @@ trait ChannelStateTestsBase extends Assertions with Eventually { val paymentHash = Crypto.sha256(paymentPreimage) val expiry = cltvExpiryDelta.toCltvExpiry(currentBlockHeight) val recipient = SpontaneousRecipient(destination, amount, expiry, paymentPreimage) - val Right(payment) = OutgoingPaymentPacket.buildOutgoingPayment(Origin.Hot(replyTo, upstream), paymentHash, makeSingleHopRoute(amount, destination), recipient, 1.0) + val Right(payment) = OutgoingPaymentPacket.buildOutgoingPayment(Origin.Hot(replyTo, upstream), paymentHash, makeSingleHopRoute(amount, destination), recipient, Reputation.Score.max) (paymentPreimage, payment.cmd.copy(commit = false)) } @@ -443,14 +527,14 @@ trait ChannelStateTestsBase extends Assertions with Eventually { } def fulfillHtlc(id: Long, preimage: ByteVector32, s: TestFSMRef[ChannelState, ChannelData, Channel], r: TestFSMRef[ChannelState, ChannelData, Channel], s2r: TestProbe, r2s: TestProbe): Unit = { - s ! CMD_FULFILL_HTLC(id, preimage) + s ! CMD_FULFILL_HTLC(id, preimage, None) val fulfill = s2r.expectMsgType[UpdateFulfillHtlc] s2r.forward(r) eventually(assert(r.stateData.asInstanceOf[ChannelDataWithCommitments].commitments.changes.remoteChanges.proposed.contains(fulfill))) } def failHtlc(id: Long, s: TestFSMRef[ChannelState, ChannelData, Channel], r: TestFSMRef[ChannelState, ChannelData, Channel], s2r: TestProbe, r2s: TestProbe): Unit = { - s ! CMD_FAIL_HTLC(id, FailureReason.LocalFailure(TemporaryNodeFailure())) + s ! CMD_FAIL_HTLC(id, FailureReason.LocalFailure(TemporaryNodeFailure()), None) val fail = s2r.expectMsgType[UpdateFailHtlc] s2r.forward(r) eventually(assert(r.stateData.asInstanceOf[ChannelDataWithCommitments].commitments.changes.remoteChanges.proposed.contains(fail))) @@ -463,31 +547,17 @@ trait ChannelStateTestsBase extends Assertions with Eventually { val rHasChanges = r.stateData.asInstanceOf[ChannelDataWithCommitments].commitments.changes.localHasChanges s ! CMD_SIGN(Some(sender.ref)) sender.expectMsgType[RES_SUCCESS[CMD_SIGN]] - var sigs2r = 0 - var batchSize = 0 - do { - val sig = s2r.expectMsgType[CommitSig] - s2r.forward(r) - sigs2r += 1 - batchSize = sig.batchSize - } while (sigs2r < batchSize) + s2r.expectMsgType[CommitSigs] + s2r.forward(r) r2s.expectMsgType[RevokeAndAck] r2s.forward(s) - var sigr2s = 0 - do { - r2s.expectMsgType[CommitSig] - r2s.forward(s) - sigr2s += 1 - } while (sigr2s < batchSize) + r2s.expectMsgType[CommitSigs] + r2s.forward(s) s2r.expectMsgType[RevokeAndAck] s2r.forward(r) if (rHasChanges) { - sigs2r = 0 - do { - s2r.expectMsgType[CommitSig] - s2r.forward(r) - sigs2r += 1 - } while (sigs2r < batchSize) + s2r.expectMsgType[CommitSigs] + s2r.forward(r) r2s.expectMsgType[RevokeAndAck] r2s.forward(s) eventually { @@ -529,7 +599,7 @@ trait ChannelStateTestsBase extends Assertions with Eventually { s2r.forward(r) r2s.expectMsgType[Shutdown] r2s.forward(s) - if (s.stateData.asInstanceOf[ChannelDataWithCommitments].commitments.params.localParams.initFeatures.hasFeature(Features.SimpleClose)) { + if (s.stateData.asInstanceOf[ChannelDataWithCommitments].commitments.localChannelParams.initFeatures.hasFeature(Features.SimpleClose)) { s2r.expectMsgType[ClosingComplete] s2r.forward(r) r2s.expectMsgType[ClosingComplete] @@ -566,62 +636,71 @@ trait ChannelStateTestsBase extends Assertions with Eventually { } } - def localClose(s: TestFSMRef[ChannelState, ChannelData, Channel], s2blockchain: TestProbe): LocalCommitPublished = { - // an error occurs and s publishes its commit tx - val localCommit = s.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit - // check that we store the local txs without sigs - localCommit.commitTxAndRemoteSig.commitTx.tx.txIn.foreach(txIn => assert(txIn.witness.isNull)) - localCommit.htlcTxsAndRemoteSigs.foreach(_.htlcTx.tx.txIn.foreach(txIn => assert(txIn.witness.isNull))) + case class PublishedForceCloseTxs(mainTx_opt: Option[Transaction], anchorTx: Transaction, htlcSuccessTxs: Seq[Transaction], htlcTimeoutTxs: Seq[Transaction]) { + val htlcTxs: Seq[Transaction] = htlcSuccessTxs ++ htlcTimeoutTxs + } - val commitTx = localCommit.commitTxAndRemoteSig.commitTx.tx + def localClose(s: TestFSMRef[ChannelState, ChannelData, Channel], s2blockchain: TestProbe, htlcSuccessCount: Int = 0, htlcTimeoutCount: Int = 0): (LocalCommitPublished, PublishedForceCloseTxs) = { s ! Error(ByteVector32.Zeroes, "oops") eventually(assert(s.stateName == CLOSING)) val closingState = s.stateData.asInstanceOf[DATA_CLOSING] assert(closingState.localCommitPublished.isDefined) val localCommitPublished = closingState.localCommitPublished.get - - val publishedLocalCommitTx = s2blockchain.expectMsgType[TxPublisher.PublishFinalTx].tx - assert(publishedLocalCommitTx.txid == commitTx.txid) - val commitInput = closingState.commitments.latest.commitInput - Transaction.correctlySpends(publishedLocalCommitTx, Map(commitInput.outPoint -> commitInput.txOut), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - if (closingState.commitments.params.commitmentFormat.isInstanceOf[Transactions.AnchorOutputsCommitmentFormat]) { - assert(s2blockchain.expectMsgType[TxPublisher.PublishReplaceableTx].txInfo.isInstanceOf[ClaimLocalAnchorOutputTx]) - } + // It may be strictly greater if we're waiting for preimages for some of our HTLC-success txs, or if we're ignoring + // HTLCs that where failed downstream or not relayed. + assert(localCommitPublished.incomingHtlcs.size >= htlcSuccessCount) + assert(localCommitPublished.outgoingHtlcs.size == htlcTimeoutCount) + + val commitTx = s2blockchain.expectFinalTxPublished("commit-tx").tx + assert(commitTx.txid == closingState.commitments.latest.localCommit.txId) + val commitInput = closingState.commitments.latest.commitInput(s.underlyingActor.channelKeys) + Transaction.correctlySpends(commitTx, Map(commitInput.outPoint -> commitInput.txOut), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + val publishedAnchorTx = s2blockchain.expectReplaceableTxPublished[ClaimLocalAnchorTx].tx // if s has a main output in the commit tx (when it has a non-dust balance), it should be claimed - localCommitPublished.claimMainDelayedOutputTx.foreach(tx => s2blockchain.expectMsg(TxPublisher.PublishFinalTx(tx, tx.fee, None))) - closingState.commitments.params.commitmentFormat match { - case Transactions.DefaultCommitmentFormat => - // all htlcs success/timeout should be published as-is, without claiming their outputs - s2blockchain.expectMsgAllOf(localCommitPublished.htlcTxs.values.toSeq.collect { case Some(tx) => TxPublisher.PublishFinalTx(tx, tx.fee, Some(commitTx.txid)) }: _*) - assert(localCommitPublished.claimHtlcDelayedTxs.isEmpty) - case _: Transactions.AnchorOutputsCommitmentFormat => - // all htlcs success/timeout should be published as replaceable txs, without claiming their outputs - val htlcTxs = localCommitPublished.htlcTxs.values.collect { case Some(tx: HtlcTx) => tx } - val publishedTxs = htlcTxs.map(_ => s2blockchain.expectMsgType[TxPublisher.PublishReplaceableTx]) - assert(publishedTxs.map(_.input).toSet == htlcTxs.map(_.input.outPoint).toSet) - assert(localCommitPublished.claimHtlcDelayedTxs.isEmpty) + val publishedMainTx_opt = localCommitPublished.localOutput_opt.map(_ => s2blockchain.expectFinalTxPublished("local-main-delayed").tx) + // all htlcs success/timeout should be published as replaceable txs + val publishedHtlcTxs = (0 until htlcSuccessCount + htlcTimeoutCount).map { _ => + val htlcTx = s2blockchain.expectMsgType[PublishReplaceableTx] + assert(htlcTx.commitTx == commitTx) + assert(localCommitPublished.htlcOutputs.contains(htlcTx.txInfo.input.outPoint)) + htlcTx.txInfo + } + // the publisher actors will sign the HTLC transactions, so we sign them here to test witness validity + val publishedHtlcSuccessTxs = publishedHtlcTxs.collect { case tx: HtlcSuccessTx => + assert(localCommitPublished.incomingHtlcs.get(tx.input.outPoint).contains(tx.htlcId)) + tx.sign() + } + val publishedHtlcTimeoutTxs = publishedHtlcTxs.collect { case tx: HtlcTimeoutTx => + assert(localCommitPublished.outgoingHtlcs.get(tx.input.outPoint).contains(tx.htlcId)) + tx.sign() } + assert(publishedHtlcSuccessTxs.size == htlcSuccessCount) + assert(publishedHtlcTimeoutTxs.size == htlcTimeoutCount) + (publishedHtlcSuccessTxs ++ publishedHtlcTimeoutTxs).foreach(htlcTx => Transaction.correctlySpends(htlcTx, commitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) + // we're not claiming the outputs of htlc txs yet + assert(localCommitPublished.htlcDelayedOutputs.isEmpty) + + // we watch the confirmation of the commitment transaction + s2blockchain.expectWatchTxConfirmed(commitTx.txid) + + // we watch outputs of the commitment tx that we want to claim + localCommitPublished.localOutput_opt.foreach(outpoint => s2blockchain.expectWatchOutputSpent(outpoint)) + localCommitPublished.anchorOutput_opt.foreach(outpoint => s2blockchain.expectWatchOutputSpent(outpoint)) + s2blockchain.expectWatchOutputsSpent(localCommitPublished.htlcOutputs.toSeq) + s2blockchain.expectNoMessage(100 millis) - // we watch the confirmation of the "final" transactions that send funds to our wallets (main delayed output and 2nd stage htlc transactions) - assert(s2blockchain.expectMsgType[WatchTxConfirmed].txId == commitTx.txid) - localCommitPublished.claimMainDelayedOutputTx.foreach(claimMain => { - val watchConfirmed = s2blockchain.expectMsgType[WatchTxConfirmed] - assert(watchConfirmed.txId == claimMain.tx.txid) - assert(watchConfirmed.delay_opt.map(_.parentTxId).contains(publishedLocalCommitTx.txid)) + // once our closing transactions are published, we watch for their confirmation + (publishedMainTx_opt ++ Seq(publishedAnchorTx) ++ publishedHtlcSuccessTxs ++ publishedHtlcTimeoutTxs).foreach(tx => { + s ! WatchOutputSpentTriggered(tx.txOut.headOption.map(_.amount).getOrElse(330 sat), tx) + s2blockchain.expectWatchTxConfirmed(tx.txid) }) - // we watch outputs of the commitment tx that both parties may spend and anchor outputs - val watchedOutputIndexes = localCommitPublished.htlcTxs.keySet.map(_.index) ++ localCommitPublished.claimAnchorTxs.collect { case tx: ClaimLocalAnchorOutputTx => tx.input.outPoint.index } - val spentWatches = watchedOutputIndexes.map(_ => s2blockchain.expectMsgType[WatchOutputSpent]) - spentWatches.foreach(ws => assert(ws.txId == commitTx.txid)) - assert(spentWatches.map(_.outputIndex) == watchedOutputIndexes) - s2blockchain.expectNoMessage(100 millis) - // s is now in CLOSING state with txs pending for confirmation before going in CLOSED state - closingState.localCommitPublished.get + val publishedTxs = PublishedForceCloseTxs(publishedMainTx_opt, publishedAnchorTx, publishedHtlcSuccessTxs, publishedHtlcTimeoutTxs) + (localCommitPublished, publishedTxs) } - def remoteClose(rCommitTx: Transaction, s: TestFSMRef[ChannelState, ChannelData, Channel], s2blockchain: TestProbe): RemoteCommitPublished = { + def remoteClose(rCommitTx: Transaction, s: TestFSMRef[ChannelState, ChannelData, Channel], s2blockchain: TestProbe, htlcSuccessCount: Int = 0, htlcTimeoutCount: Int = 0): (RemoteCommitPublished, PublishedForceCloseTxs) = { // we make s believe r unilaterally closed the channel s ! WatchFundingSpentTriggered(rCommitTx) eventually(assert(s.stateName == CLOSING)) @@ -630,49 +709,55 @@ trait ChannelStateTestsBase extends Assertions with Eventually { assert(remoteCommitPublished_opt.isDefined) assert(closingData.localCommitPublished.isEmpty) val remoteCommitPublished = remoteCommitPublished_opt.get + // It may be strictly greater if we're waiting for preimages for some of our HTLC-success txs, or if we're ignoring + // HTLCs that where failed downstream or not relayed. Note that since this is the remote commit, IN/OUT are inverted. + assert(remoteCommitPublished.incomingHtlcs.size >= htlcSuccessCount) + assert(remoteCommitPublished.outgoingHtlcs.size == htlcTimeoutCount) // If anchor outputs is used, we use the anchor output to bump the fees if necessary. - closingData.commitments.params.commitmentFormat match { - case _: AnchorOutputsCommitmentFormat => - val anchorTx = s2blockchain.expectMsgType[PublishReplaceableTx] - assert(anchorTx.txInfo.isInstanceOf[ClaimLocalAnchorOutputTx]) - case Transactions.DefaultCommitmentFormat => () - } + val publishedAnchorTx = s2blockchain.expectReplaceableTxPublished[ClaimRemoteAnchorTx].tx // if s has a main output in the commit tx (when it has a non-dust balance), it should be claimed - remoteCommitPublished.claimMainOutputTx.foreach(claimMain => { - Transaction.correctlySpends(claimMain.tx, rCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - s2blockchain.expectMsg(TxPublisher.PublishFinalTx(claimMain, claimMain.fee, None)) - }) + val publishedMainTx_opt = remoteCommitPublished.localOutput_opt.map(_ => s2blockchain.expectFinalTxPublished("remote-main-delayed").tx) + publishedMainTx_opt.foreach(tx => Transaction.correctlySpends(tx, rCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) // all htlcs success/timeout should be claimed - val claimHtlcTxs = remoteCommitPublished.claimHtlcTxs.values.collect { case Some(tx: ClaimHtlcTx) => tx }.toSeq - claimHtlcTxs.foreach(claimHtlc => Transaction.correctlySpends(claimHtlc.tx, rCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) - val publishedClaimHtlcTxs = claimHtlcTxs.map(_ => s2blockchain.expectMsgType[PublishReplaceableTx]) - assert(publishedClaimHtlcTxs.map(_.input).toSet == claimHtlcTxs.map(_.input.outPoint).toSet) - - // we watch the confirmation of the "final" transactions that send funds to our wallets (main delayed output and 2nd stage htlc transactions) - assert(s2blockchain.expectMsgType[WatchTxConfirmed].txId == rCommitTx.txid) - remoteCommitPublished.claimMainOutputTx.foreach(claimMain => assert(s2blockchain.expectMsgType[WatchTxConfirmed].txId == claimMain.tx.txid)) - - // we watch outputs of the commitment tx that both parties may spend - val htlcOutputIndexes = remoteCommitPublished.claimHtlcTxs.keySet.map(_.index) - val spentWatches = htlcOutputIndexes.map(_ => s2blockchain.expectMsgType[WatchOutputSpent]) - spentWatches.foreach(ws => assert(ws.txId == rCommitTx.txid)) - assert(spentWatches.map(_.outputIndex) == htlcOutputIndexes) - s2blockchain.expectNoMessage(100 millis) - - // s is now in CLOSING state with txs pending for confirmation before going in CLOSED state - remoteCommitPublished - } - - def channelId(a: TestFSMRef[ChannelState, ChannelData, Channel]): ByteVector32 = a.stateData.channelId + val publishedClaimHtlcTxs = (0 until htlcSuccessCount + htlcTimeoutCount).map { _ => + val claimHtlcTx = s2blockchain.expectMsgType[PublishReplaceableTx] + assert(claimHtlcTx.commitTx == rCommitTx) + assert(remoteCommitPublished.htlcOutputs.contains(claimHtlcTx.input)) + claimHtlcTx.txInfo + } + // the publisher actors will sign the HTLC transactions, so we sign them here to test witness validity + val publishedHtlcSuccessTxs = publishedClaimHtlcTxs.collect { case tx: ClaimHtlcSuccessTx => + assert(remoteCommitPublished.incomingHtlcs.get(tx.input.outPoint).contains(tx.htlcId)) + tx.sign() + } + assert(publishedHtlcSuccessTxs.size == htlcSuccessCount) + val publishedHtlcTimeoutTxs = publishedClaimHtlcTxs.collect { case tx: ClaimHtlcTimeoutTx => + assert(remoteCommitPublished.outgoingHtlcs.get(tx.input.outPoint).contains(tx.htlcId)) + tx.sign() + } + assert(publishedHtlcTimeoutTxs.size == htlcTimeoutCount) + (publishedHtlcSuccessTxs ++ publishedHtlcTimeoutTxs).foreach(htlcTx => Transaction.correctlySpends(htlcTx, rCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) - def getHtlcSuccessTxs(lcp: LocalCommitPublished): Seq[HtlcSuccessTx] = lcp.htlcTxs.values.collect { case Some(tx: HtlcSuccessTx) => tx }.toSeq + // we watch the confirmation of the commitment transaction + s2blockchain.expectWatchTxConfirmed(rCommitTx.txid) - def getHtlcTimeoutTxs(lcp: LocalCommitPublished): Seq[HtlcTimeoutTx] = lcp.htlcTxs.values.collect { case Some(tx: HtlcTimeoutTx) => tx }.toSeq + // we watch outputs of the commitment tx that we want to claim + remoteCommitPublished.localOutput_opt.foreach(outpoint => s2blockchain.expectWatchOutputSpent(outpoint)) + remoteCommitPublished.anchorOutput_opt.foreach(outpoint => s2blockchain.expectWatchOutputSpent(outpoint)) + s2blockchain.expectWatchOutputsSpent(remoteCommitPublished.htlcOutputs.toSeq) + s2blockchain.expectNoMessage(100 millis) - def getClaimHtlcSuccessTxs(rcp: RemoteCommitPublished): Seq[ClaimHtlcSuccessTx] = rcp.claimHtlcTxs.values.collect { case Some(tx: ClaimHtlcSuccessTx) => tx }.toSeq + // once our closing transactions are published, we watch for their confirmation + (publishedMainTx_opt ++ Seq(publishedAnchorTx) ++ publishedHtlcSuccessTxs ++ publishedHtlcTimeoutTxs).foreach(tx => { + s ! WatchOutputSpentTriggered(tx.txOut.headOption.map(_.amount).getOrElse(330 sat), tx) + s2blockchain.expectWatchTxConfirmed(tx.txid) + }) - def getClaimHtlcTimeoutTxs(rcp: RemoteCommitPublished): Seq[ClaimHtlcTimeoutTx] = rcp.claimHtlcTxs.values.collect { case Some(tx: ClaimHtlcTimeoutTx) => tx }.toSeq + // s is now in CLOSING state with txs pending for confirmation before going in CLOSED state + val publishedTxs = PublishedForceCloseTxs(publishedMainTx_opt, publishedAnchorTx, publishedHtlcSuccessTxs, publishedHtlcTimeoutTxs) + (remoteCommitPublished, publishedTxs) + } } @@ -683,9 +768,14 @@ object ChannelStateTestsBase { } implicit class PimpTestFSM(private val channel: TestFSMRef[ChannelState, ChannelData, Channel]) { - val nodeParams: NodeParams = channel.underlyingActor.nodeParams + def commitments: Commitments = channel.stateData.asInstanceOf[ChannelDataWithCommitments].commitments + + def signCommitTx(): Transaction = commitments.latest.fullySignedLocalCommitTx(channel.underlyingActor.channelKeys) + + def htlcTxs(): Seq[UnsignedHtlcTx] = commitments.latest.htlcTxs(channel.underlyingActor.channelKeys).map(_._1) + def setBitcoinCoreFeerates(feerates: FeeratesPerKw): Unit = channel.underlyingActor.nodeParams.setBitcoinCoreFeerates(feerates) def setBitcoinCoreFeerate(feerate: FeeratePerKw): Unit = setBitcoinCoreFeerates(FeeratesPerKw.single(feerate)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptChannelStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptChannelStateSpec.scala index b66f872891..07b371dcca 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptChannelStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptChannelStateSpec.scala @@ -16,20 +16,18 @@ package fr.acinq.eclair.channel.states.a -import akka.actor.typed.scaladsl.adapter.ClassicActorRefOps import akka.testkit.{TestFSMRef, TestProbe} import com.softwaremill.quicklens.ModifyPimp -import fr.acinq.bitcoin.scalacompat.{ByteVector32, SatoshiLong} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, SatoshiLong, TxId} import fr.acinq.eclair.TestConstants.{Alice, Bob} -import fr.acinq.eclair.blockchain.NoOpOnChainWallet import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.channel.fsm.Channel.TickChannelOpenTimeout import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags} import fr.acinq.eclair.io.Peer.OpenChannelResponse -import fr.acinq.eclair.transactions.Transactions.{DefaultCommitmentFormat, UnsafeLegacyAnchorOutputsCommitmentFormat, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat} -import fr.acinq.eclair.wire.protocol.{AcceptChannel, ChannelTlv, Error, Init, OpenChannel, TlvStream} -import fr.acinq.eclair.{CltvExpiryDelta, FeatureSupport, Features, TestConstants, TestKitBaseClass} +import fr.acinq.eclair.transactions.Transactions.{PhoenixSimpleTaprootChannelCommitmentFormat, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat} +import fr.acinq.eclair.wire.protocol.{AcceptChannel, ChannelTlv, Error, OpenChannel, TlvStream} +import fr.acinq.eclair.{CltvExpiryDelta, TestConstants, TestKitBaseClass} import org.scalatest.funsuite.FixtureAnyFunSuiteLike import org.scalatest.{Outcome, Tag} import scodec.bits.ByteVector @@ -43,7 +41,6 @@ import scala.concurrent.duration._ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with ChannelStateTestsBase { private val HighRemoteDustLimit = "high_remote_dust_limit" - private val StandardChannelType = "standard_channel_type" case class FixtureParam(alice: TestFSMRef[ChannelState, ChannelData, Channel], bob: TestFSMRef[ChannelState, ChannelData, Channel], aliceOpenReplyTo: TestProbe, alice2bob: TestProbe, bob2alice: TestProbe, alice2blockchain: TestProbe, listener: TestProbe) @@ -51,22 +48,15 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS import com.softwaremill.quicklens._ val aliceNodeParams = Alice.nodeParams.modify(_.channelConf.maxRemoteDustLimit).setToIf(test.tags.contains(HighRemoteDustLimit))(15_000 sat) - - val setup = init(aliceNodeParams, Bob.nodeParams, wallet_opt = Some(new NoOpOnChainWallet()), test.tags) + val setup = init(aliceNodeParams, Bob.nodeParams, tags = test.tags) import setup._ - val channelConfig = ChannelConfig.standard - val channelFlags = ChannelFlags(announceChannel = false) - val (aliceParams, bobParams, defaultChannelType) = computeFeatures(setup, test.tags, channelFlags) - val channelType = if (test.tags.contains(StandardChannelType)) ChannelTypes.Standard() else defaultChannelType - val commitTxFeerate = if (channelType.isInstanceOf[ChannelTypes.AnchorOutputs] || channelType.isInstanceOf[ChannelTypes.AnchorOutputsZeroFeeHtlcTx]) TestConstants.anchorOutputsFeeratePerKw else TestConstants.feeratePerKw - val aliceInit = Init(aliceParams.initFeatures) - val bobInit = Init(bobParams.initFeatures) + val channelParams = computeChannelParams(setup, test.tags) val listener = TestProbe() within(30 seconds) { alice.underlying.system.eventStream.subscribe(listener.ref, classOf[ChannelAborted]) - alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, TestConstants.fundingSatoshis, dualFunded = false, commitTxFeerate, TestConstants.feeratePerKw, fundingTxFeeBudget_opt = None, Some(TestConstants.initiatorPushAmount), requireConfirmedInputs = false, requestFunding_opt = None, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType, replyTo = aliceOpenReplyTo.ref.toTyped) - bob ! INPUT_INIT_CHANNEL_NON_INITIATOR(ByteVector32.Zeroes, None, dualFunded = false, None, requireConfirmedInputs = false, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) + alice ! channelParams.initChannelAlice(TestConstants.fundingSatoshis, pushAmount_opt = Some(TestConstants.initiatorPushAmount)) + bob ! channelParams.initChannelBob() alice2bob.expectMsgType[OpenChannel] alice2bob.forward(bob) awaitCond(alice.stateName == WAIT_FOR_ACCEPT_CHANNEL) @@ -74,48 +64,52 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS } } - test("recv AcceptChannel") { f => + test("recv AcceptChannel (anchor outputs zero fee htlc txs)") { f => import f._ val accept = bob2alice.expectMsgType[AcceptChannel] // Since https://github.com/lightningnetwork/lightning-rfc/pull/714 we must include an empty upfront_shutdown_script. assert(accept.upfrontShutdownScript_opt.contains(ByteVector.empty)) - assert(accept.channelType_opt.contains(ChannelTypes.StaticRemoteKey())) + assert(accept.channelType_opt.contains(ChannelTypes.AnchorOutputsZeroFeeHtlcTx())) bob2alice.forward(alice) awaitCond(alice.stateName == WAIT_FOR_FUNDING_INTERNAL) + assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_INTERNAL].commitmentFormat == ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) aliceOpenReplyTo.expectNoMessage() } - test("recv AcceptChannel (anchor outputs)", Tag(ChannelStateTestsTags.AnchorOutputs)) { f => + test("recv AcceptChannel (anchor outputs zero fee htlc txs and scid alias)", Tag(ChannelStateTestsTags.ScidAlias)) { f => import f._ val accept = bob2alice.expectMsgType[AcceptChannel] - assert(accept.channelType_opt.contains(ChannelTypes.AnchorOutputs())) + assert(accept.channelType_opt.contains(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = true))) bob2alice.forward(alice) awaitCond(alice.stateName == WAIT_FOR_FUNDING_INTERNAL) - assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_INTERNAL].params.commitmentFormat == UnsafeLegacyAnchorOutputsCommitmentFormat) + assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_INTERNAL].commitmentFormat == ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) aliceOpenReplyTo.expectNoMessage() } - test("recv AcceptChannel (anchor outputs zero fee htlc txs)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + test("recv AcceptChannel (simple taproot channels phoenix)", Tag(ChannelStateTestsTags.OptionSimpleTaprootPhoenix)) { f => import f._ val accept = bob2alice.expectMsgType[AcceptChannel] - assert(accept.channelType_opt.contains(ChannelTypes.AnchorOutputsZeroFeeHtlcTx())) + assert(accept.channelType_opt.contains(ChannelTypes.SimpleTaprootChannelsPhoenix)) + assert(accept.commitNonce_opt.isDefined) bob2alice.forward(alice) awaitCond(alice.stateName == WAIT_FOR_FUNDING_INTERNAL) - assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_INTERNAL].params.commitmentFormat == ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) + assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_INTERNAL].commitmentFormat == PhoenixSimpleTaprootChannelCommitmentFormat) aliceOpenReplyTo.expectNoMessage() } - test("recv AcceptChannel (anchor outputs zero fee htlc txs and scid alias)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), Tag(ChannelStateTestsTags.ScidAlias)) { f => + test("recv AcceptChannel (simple taproot channels outputs, missing nonce)", Tag(ChannelStateTestsTags.OptionSimpleTaprootPhoenix)) { f => import f._ val accept = bob2alice.expectMsgType[AcceptChannel] - assert(accept.channelType_opt.contains(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = true))) - bob2alice.forward(alice) - awaitCond(alice.stateName == WAIT_FOR_FUNDING_INTERNAL) - assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_INTERNAL].params.commitmentFormat == ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) - aliceOpenReplyTo.expectNoMessage() + assert(accept.channelType_opt.contains(ChannelTypes.SimpleTaprootChannelsPhoenix)) + assert(accept.commitNonce_opt.isDefined) + bob2alice.forward(alice, accept.copy(tlvStream = accept.tlvStream.copy(records = accept.tlvStream.records.filterNot(_.isInstanceOf[ChannelTlv.NextLocalNonceTlv])))) + alice2bob.expectMsg(Error(accept.temporaryChannelId, MissingCommitNonce(accept.temporaryChannelId, TxId(ByteVector32.Zeroes), 0).getMessage)) + listener.expectMsgType[ChannelAborted] + awaitCond(alice.stateName == CLOSED) + aliceOpenReplyTo.expectMsgType[OpenChannelResponse.Rejected] } - test("recv AcceptChannel (channel type not set but feature bit set)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + test("recv AcceptChannel (channel type not set)") { f => import f._ val accept = bob2alice.expectMsgType[AcceptChannel] assert(accept.channelType_opt.contains(ChannelTypes.AnchorOutputsZeroFeeHtlcTx())) @@ -126,46 +120,13 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS aliceOpenReplyTo.expectMsgType[OpenChannelResponse.Rejected] } - test("recv AcceptChannel (non-default channel type)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), Tag(StandardChannelType)) { f => - import f._ - val accept = bob2alice.expectMsgType[AcceptChannel] - // Alice asked for a standard channel whereas they both support anchor outputs. - assert(accept.channelType_opt.contains(ChannelTypes.Standard())) - bob2alice.forward(alice, accept) - awaitCond(alice.stateName == WAIT_FOR_FUNDING_INTERNAL) - assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_INTERNAL].params.commitmentFormat == DefaultCommitmentFormat) - aliceOpenReplyTo.expectNoMessage() - } - - test("recv AcceptChannel (anchor outputs channel type without enabling the feature)") { () => - val setup = init(Alice.nodeParams, Bob.nodeParams, wallet_opt = Some(new NoOpOnChainWallet())) - import setup._ - - val channelConfig = ChannelConfig.standard - val channelFlags = ChannelFlags(announceChannel = false) - // Bob advertises support for anchor outputs, but Alice doesn't. - val aliceParams = Alice.channelParams - val bobParams = Bob.channelParams.copy(initFeatures = Features(Features.StaticRemoteKey -> FeatureSupport.Optional, Features.AnchorOutputs -> FeatureSupport.Optional, Features.ChannelType -> FeatureSupport.Mandatory)) - alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, TestConstants.fundingSatoshis, dualFunded = false, TestConstants.anchorOutputsFeeratePerKw, TestConstants.feeratePerKw, fundingTxFeeBudget_opt = None, Some(TestConstants.initiatorPushAmount), requireConfirmedInputs = false, requestFunding_opt = None, aliceParams, alice2bob.ref, Init(bobParams.initFeatures), channelFlags, channelConfig, ChannelTypes.AnchorOutputs(), replyTo = aliceOpenReplyTo.ref.toTyped) - bob ! INPUT_INIT_CHANNEL_NON_INITIATOR(ByteVector32.Zeroes, None, dualFunded = false, None, requireConfirmedInputs = false, bobParams, bob2alice.ref, Init(bobParams.initFeatures), channelConfig, ChannelTypes.AnchorOutputs()) - val open = alice2bob.expectMsgType[OpenChannel] - assert(open.channelType_opt.contains(ChannelTypes.AnchorOutputs())) - alice2bob.forward(bob, open) - val accept = bob2alice.expectMsgType[AcceptChannel] - assert(accept.channelType_opt.contains(ChannelTypes.AnchorOutputs())) - bob2alice.forward(alice, accept) - awaitCond(alice.stateName == WAIT_FOR_FUNDING_INTERNAL) - assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_INTERNAL].params.commitmentFormat == UnsafeLegacyAnchorOutputsCommitmentFormat) - aliceOpenReplyTo.expectNoMessage() - } - test("recv AcceptChannel (invalid channel type)") { f => import f._ val accept = bob2alice.expectMsgType[AcceptChannel] - assert(accept.channelType_opt.contains(ChannelTypes.StaticRemoteKey())) + assert(accept.channelType_opt.contains(ChannelTypes.AnchorOutputsZeroFeeHtlcTx())) val invalidAccept = accept.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(ByteVector.empty), ChannelTlv.ChannelTypeTlv(ChannelTypes.AnchorOutputs()))) bob2alice.forward(alice, invalidAccept) - alice2bob.expectMsg(Error(accept.temporaryChannelId, "invalid channel_type=anchor_outputs, expected channel_type=static_remotekey")) + alice2bob.expectMsg(Error(accept.temporaryChannelId, "invalid channel_type=anchor_outputs")) listener.expectMsgType[ChannelAborted] awaitCond(alice.stateName == CLOSED) aliceOpenReplyTo.expectMsgType[OpenChannelResponse.Rejected] @@ -275,24 +236,23 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS test("recv AcceptChannel (upfront shutdown script)", Tag(ChannelStateTestsTags.UpfrontShutdownScript)) { f => import f._ val accept = bob2alice.expectMsgType[AcceptChannel] - assert(accept.upfrontShutdownScript_opt.contains(bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CREATED].params.localParams.upfrontShutdownScript_opt.get)) + assert(accept.upfrontShutdownScript_opt.contains(bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CREATED].channelParams.localParams.upfrontShutdownScript_opt.get)) bob2alice.forward(alice, accept) awaitCond(alice.stateName == WAIT_FOR_FUNDING_INTERNAL) - assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_INTERNAL].params.remoteParams.upfrontShutdownScript_opt == accept.upfrontShutdownScript_opt) + assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_INTERNAL].channelParams.remoteParams.upfrontShutdownScript_opt == accept.upfrontShutdownScript_opt) aliceOpenReplyTo.expectNoMessage() } test("recv AcceptChannel (empty upfront shutdown script)", Tag(ChannelStateTestsTags.UpfrontShutdownScript)) { f => import f._ val accept = bob2alice.expectMsgType[AcceptChannel] - assert(accept.upfrontShutdownScript_opt.contains(bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CREATED].params.localParams.upfrontShutdownScript_opt.get)) + assert(accept.upfrontShutdownScript_opt.contains(bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CREATED].channelParams.localParams.upfrontShutdownScript_opt.get)) val accept1 = accept .modify(_.tlvStream.records).using(_.filterNot(_.isInstanceOf[ChannelTlv.UpfrontShutdownScriptTlv])) .modify(_.tlvStream.records).using(_ + ChannelTlv.UpfrontShutdownScriptTlv(ByteVector.empty)) bob2alice.forward(alice, accept1) - alice2bob.expectNoMessage(100 millis) awaitCond(alice.stateName == WAIT_FOR_FUNDING_INTERNAL) - assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_INTERNAL].params.remoteParams.upfrontShutdownScript_opt.isEmpty) + assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_INTERNAL].channelParams.remoteParams.upfrontShutdownScript_opt.isEmpty) aliceOpenReplyTo.expectNoMessage() } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptDualFundedChannelStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptDualFundedChannelStateSpec.scala index 4f90628095..0ab2f06274 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptDualFundedChannelStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptDualFundedChannelStateSpec.scala @@ -16,7 +16,6 @@ package fr.acinq.eclair.channel.states.a -import akka.actor.typed.scaladsl.adapter.ClassicActorRefOps import akka.testkit.{TestFSMRef, TestProbe} import com.softwaremill.quicklens.ModifyPimp import fr.acinq.bitcoin.scalacompat.{ByteVector32, SatoshiLong} @@ -26,7 +25,7 @@ import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.channel.fsm.Channel.TickChannelOpenTimeout import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags} import fr.acinq.eclair.io.Peer.OpenChannelResponse -import fr.acinq.eclair.wire.protocol.{AcceptDualFundedChannel, ChannelTlv, Error, Init, LiquidityAds, OpenDualFundedChannel} +import fr.acinq.eclair.wire.protocol.{AcceptDualFundedChannel, ChannelTlv, Error, LiquidityAds, OpenDualFundedChannel} import fr.acinq.eclair.{MilliSatoshiLong, TestConstants, TestKitBaseClass, randomBytes64} import org.scalatest.funsuite.FixtureAnyFunSuiteLike import org.scalatest.{Outcome, Tag} @@ -49,11 +48,7 @@ class WaitForAcceptDualFundedChannelStateSpec extends TestKitBaseClass with Fixt val setup = init(nodeParamsB = bobNodeParams, tags = test.tags) import setup._ - val channelConfig = ChannelConfig.standard - val channelFlags = ChannelFlags(announceChannel = false) - val (aliceParams, bobParams, channelType) = computeFeatures(setup, test.tags, channelFlags) - val aliceInit = Init(aliceParams.initFeatures) - val bobInit = Init(bobParams.initFeatures) + val channelParams = computeChannelParams(setup, test.tags) val nonInitiatorContribution = if (test.tags.contains(ChannelStateTestsTags.LiquidityAds)) Some(LiquidityAds.AddFunding(TestConstants.nonInitiatorFundingSatoshis, Some(TestConstants.defaultLiquidityRates))) else None val requestFunds_opt = if (test.tags.contains(ChannelStateTestsTags.LiquidityAds)) { Some(LiquidityAds.RequestFunding(TestConstants.nonInitiatorFundingSatoshis, TestConstants.defaultLiquidityRates.fundingRates.head, LiquidityAds.PaymentDetails.FromChannelBalance)) @@ -64,8 +59,8 @@ class WaitForAcceptDualFundedChannelStateSpec extends TestKitBaseClass with Fixt val listener = TestProbe() within(30 seconds) { alice.underlying.system.eventStream.subscribe(listener.ref, classOf[ChannelAborted]) - alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, TestConstants.fundingSatoshis, dualFunded = true, TestConstants.anchorOutputsFeeratePerKw, TestConstants.feeratePerKw, fundingTxFeeBudget_opt = None, None, requireConfirmedInputs = false, requestFunds_opt, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType, replyTo = aliceOpenReplyTo.ref.toTyped) - bob ! INPUT_INIT_CHANNEL_NON_INITIATOR(ByteVector32.Zeroes, nonInitiatorContribution, dualFunded = true, nonInitiatorPushAmount, requireConfirmedInputs = test.tags.contains(bobRequiresConfirmedInputs), bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) + alice ! channelParams.initChannelAlice(TestConstants.fundingSatoshis, dualFunded = true, requestFunding_opt = requestFunds_opt) + bob ! channelParams.initChannelBob(nonInitiatorContribution, dualFunded = true, pushAmount_opt = nonInitiatorPushAmount, requireConfirmedInputs = test.tags.contains(bobRequiresConfirmedInputs)) val open = alice2bob.expectMsgType[OpenDualFundedChannel] alice2bob.forward(bob, open) awaitCond(alice.stateName == WAIT_FOR_ACCEPT_DUAL_FUNDED_CHANNEL) @@ -73,7 +68,7 @@ class WaitForAcceptDualFundedChannelStateSpec extends TestKitBaseClass with Fixt } } - test("recv AcceptDualFundedChannel", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + test("recv AcceptDualFundedChannel", Tag(ChannelStateTestsTags.DualFunding)) { f => import f._ val accept = bob2alice.expectMsgType[AcceptDualFundedChannel] @@ -92,7 +87,7 @@ class WaitForAcceptDualFundedChannelStateSpec extends TestKitBaseClass with Fixt aliceOpenReplyTo.expectNoMessage() } - test("recv AcceptDualFundedChannel (with liquidity ads)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.LiquidityAds), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + test("recv AcceptDualFundedChannel (with liquidity ads)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.LiquidityAds)) { f => import f._ val accept = bob2alice.expectMsgType[AcceptDualFundedChannel] @@ -105,7 +100,7 @@ class WaitForAcceptDualFundedChannelStateSpec extends TestKitBaseClass with Fixt awaitCond(alice.stateName == WAIT_FOR_DUAL_FUNDING_CREATED) } - test("recv AcceptDualFundedChannel (with invalid liquidity ads sig)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.LiquidityAds), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + test("recv AcceptDualFundedChannel (with invalid liquidity ads sig)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.LiquidityAds)) { f => import f._ val accept = bob2alice.expectMsgType[AcceptDualFundedChannel] @@ -118,7 +113,7 @@ class WaitForAcceptDualFundedChannelStateSpec extends TestKitBaseClass with Fixt awaitCond(alice.stateName == CLOSED) } - test("recv AcceptDualFundedChannel (with invalid liquidity ads amount)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.LiquidityAds), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + test("recv AcceptDualFundedChannel (with invalid liquidity ads amount)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.LiquidityAds)) { f => import f._ val accept = bob2alice.expectMsgType[AcceptDualFundedChannel].copy(fundingAmount = TestConstants.nonInitiatorFundingSatoshis / 2) @@ -127,7 +122,7 @@ class WaitForAcceptDualFundedChannelStateSpec extends TestKitBaseClass with Fixt awaitCond(alice.stateName == CLOSED) } - test("recv AcceptDualFundedChannel (without liquidity ads response)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.LiquidityAds), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + test("recv AcceptDualFundedChannel (without liquidity ads response)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.LiquidityAds)) { f => import f._ val accept = bob2alice.expectMsgType[AcceptDualFundedChannel] @@ -137,7 +132,7 @@ class WaitForAcceptDualFundedChannelStateSpec extends TestKitBaseClass with Fixt awaitCond(alice.stateName == CLOSED) } - test("recv AcceptDualFundedChannel (with push amount)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.LiquidityAds), Tag(ChannelStateTestsTags.NonInitiatorPushAmount), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + test("recv AcceptDualFundedChannel (with push amount)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.LiquidityAds), Tag(ChannelStateTestsTags.NonInitiatorPushAmount)) { f => import f._ val accept = bob2alice.expectMsgType[AcceptDualFundedChannel] @@ -149,7 +144,7 @@ class WaitForAcceptDualFundedChannelStateSpec extends TestKitBaseClass with Fixt awaitCond(alice.stateName == WAIT_FOR_DUAL_FUNDING_CREATED) } - test("recv AcceptDualFundedChannel (require confirmed inputs)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.LiquidityAds), Tag(bobRequiresConfirmedInputs), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + test("recv AcceptDualFundedChannel (require confirmed inputs)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.LiquidityAds), Tag(bobRequiresConfirmedInputs)) { f => import f._ val accept = bob2alice.expectMsgType[AcceptDualFundedChannel] @@ -160,7 +155,7 @@ class WaitForAcceptDualFundedChannelStateSpec extends TestKitBaseClass with Fixt awaitCond(alice.stateName == WAIT_FOR_DUAL_FUNDING_CREATED) } - test("recv AcceptDualFundedChannel (negative funding amount)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.LiquidityAds), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + test("recv AcceptDualFundedChannel (negative funding amount)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.LiquidityAds)) { f => import f._ val accept = bob2alice.expectMsgType[AcceptDualFundedChannel] @@ -171,7 +166,7 @@ class WaitForAcceptDualFundedChannelStateSpec extends TestKitBaseClass with Fixt aliceOpenReplyTo.expectMsgType[OpenChannelResponse.Rejected] } - test("recv AcceptDualFundedChannel (invalid push amount)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.LiquidityAds), Tag(ChannelStateTestsTags.NonInitiatorPushAmount), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + test("recv AcceptDualFundedChannel (invalid push amount)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.LiquidityAds), Tag(ChannelStateTestsTags.NonInitiatorPushAmount)) { f => import f._ val accept = bob2alice.expectMsgType[AcceptDualFundedChannel] @@ -183,7 +178,7 @@ class WaitForAcceptDualFundedChannelStateSpec extends TestKitBaseClass with Fixt aliceOpenReplyTo.expectMsgType[OpenChannelResponse.Rejected] } - test("recv AcceptDualFundedChannel (invalid max accepted htlcs)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + test("recv AcceptDualFundedChannel (invalid max accepted htlcs)", Tag(ChannelStateTestsTags.DualFunding)) { f => import f._ val accept = bob2alice.expectMsgType[AcceptDualFundedChannel] val invalidMaxAcceptedHtlcs = Channel.MAX_ACCEPTED_HTLCS + 1 @@ -195,7 +190,7 @@ class WaitForAcceptDualFundedChannelStateSpec extends TestKitBaseClass with Fixt aliceOpenReplyTo.expectMsgType[OpenChannelResponse.Rejected] } - test("recv AcceptDualFundedChannel (dust limit too low)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + test("recv AcceptDualFundedChannel (dust limit too low)", Tag(ChannelStateTestsTags.DualFunding)) { f => import f._ val accept = bob2alice.expectMsgType[AcceptDualFundedChannel] val lowDustLimit = Channel.MIN_DUST_LIMIT - 1.sat @@ -207,7 +202,7 @@ class WaitForAcceptDualFundedChannelStateSpec extends TestKitBaseClass with Fixt aliceOpenReplyTo.expectMsgType[OpenChannelResponse.Rejected] } - test("recv AcceptDualFundedChannel (dust limit too high)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + test("recv AcceptDualFundedChannel (dust limit too high)", Tag(ChannelStateTestsTags.DualFunding)) { f => import f._ val accept = bob2alice.expectMsgType[AcceptDualFundedChannel] val highDustLimit = Alice.nodeParams.channelConf.maxRemoteDustLimit + 1.sat @@ -219,7 +214,7 @@ class WaitForAcceptDualFundedChannelStateSpec extends TestKitBaseClass with Fixt aliceOpenReplyTo.expectMsgType[OpenChannelResponse.Rejected] } - test("recv AcceptDualFundedChannel (to_self_delay too high)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + test("recv AcceptDualFundedChannel (to_self_delay too high)", Tag(ChannelStateTestsTags.DualFunding)) { f => import f._ val accept = bob2alice.expectMsgType[AcceptDualFundedChannel] val delayTooHigh = Alice.nodeParams.channelConf.maxToLocalDelay + 1 @@ -231,7 +226,7 @@ class WaitForAcceptDualFundedChannelStateSpec extends TestKitBaseClass with Fixt aliceOpenReplyTo.expectMsgType[OpenChannelResponse.Rejected] } - test("recv Error", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + test("recv Error", Tag(ChannelStateTestsTags.DualFunding)) { f => import f._ alice ! Error(ByteVector32.Zeroes, "dual funding not supported") listener.expectMsgType[ChannelAborted] @@ -239,7 +234,7 @@ class WaitForAcceptDualFundedChannelStateSpec extends TestKitBaseClass with Fixt aliceOpenReplyTo.expectMsgType[OpenChannelResponse.RemoteError] } - test("recv CMD_CLOSE", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + test("recv CMD_CLOSE", Tag(ChannelStateTestsTags.DualFunding)) { f => import f._ val sender = TestProbe() val c = CMD_CLOSE(sender.ref, None, None) @@ -250,7 +245,7 @@ class WaitForAcceptDualFundedChannelStateSpec extends TestKitBaseClass with Fixt aliceOpenReplyTo.expectMsg(OpenChannelResponse.Cancelled) } - test("recv INPUT_DISCONNECTED", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + test("recv INPUT_DISCONNECTED", Tag(ChannelStateTestsTags.DualFunding)) { f => import f._ alice ! INPUT_DISCONNECTED listener.expectMsgType[ChannelAborted] @@ -258,7 +253,7 @@ class WaitForAcceptDualFundedChannelStateSpec extends TestKitBaseClass with Fixt aliceOpenReplyTo.expectMsg(OpenChannelResponse.Disconnected) } - test("recv TickChannelOpenTimeout", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + test("recv TickChannelOpenTimeout", Tag(ChannelStateTestsTags.DualFunding)) { f => import f._ alice ! TickChannelOpenTimeout listener.expectMsgType[ChannelAborted] diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForOpenChannelStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForOpenChannelStateSpec.scala index fb09112a72..3248890918 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForOpenChannelStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForOpenChannelStateSpec.scala @@ -16,16 +16,15 @@ package fr.acinq.eclair.channel.states.a -import akka.actor.typed.scaladsl.adapter.ClassicActorRefOps import akka.testkit.{TestFSMRef, TestProbe} -import fr.acinq.bitcoin.scalacompat.{Block, Btc, ByteVector32, SatoshiLong} -import fr.acinq.eclair.TestConstants.{Alice, Bob} +import fr.acinq.bitcoin.scalacompat.{Block, Btc, ByteVector32, SatoshiLong, TxId} +import fr.acinq.eclair.TestConstants.Bob import fr.acinq.eclair.blockchain.fee.FeeratePerKw import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags} -import fr.acinq.eclair.transactions.Transactions.{DefaultCommitmentFormat, UnsafeLegacyAnchorOutputsCommitmentFormat, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat} -import fr.acinq.eclair.wire.protocol.{AcceptChannel, ChannelTlv, Error, Init, OpenChannel, TlvStream} +import fr.acinq.eclair.transactions.Transactions.{ZeroFeeHtlcTxAnchorOutputsCommitmentFormat, ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat} +import fr.acinq.eclair.wire.protocol.{AcceptChannel, ChannelTlv, Error, OpenChannel, TlvStream} import fr.acinq.eclair.{CltvExpiryDelta, MilliSatoshiLong, TestConstants, TestKitBaseClass, ToMilliSatoshiConversion} import org.scalatest.funsuite.FixtureAnyFunSuiteLike import org.scalatest.{Outcome, Tag} @@ -39,77 +38,63 @@ import scala.concurrent.duration._ class WaitForOpenChannelStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with ChannelStateTestsBase { - private val StandardChannelType = "standard_channel_type" - case class FixtureParam(alice: TestFSMRef[ChannelState, ChannelData, Channel], bob: TestFSMRef[ChannelState, ChannelData, Channel], alice2bob: TestProbe, bob2alice: TestProbe, bob2blockchain: TestProbe, listener: TestProbe) override def withFixture(test: OneArgTest): Outcome = { val setup = init(tags = test.tags) import setup._ - val channelConfig = ChannelConfig.standard - val channelFlags = ChannelFlags(announceChannel = false) - val (aliceParams, bobParams, defaultChannelType) = computeFeatures(setup, test.tags, channelFlags) - val channelType = if (test.tags.contains(StandardChannelType)) ChannelTypes.Standard() else defaultChannelType - val commitTxFeerate = if (channelType.isInstanceOf[ChannelTypes.AnchorOutputs] || channelType.isInstanceOf[ChannelTypes.AnchorOutputsZeroFeeHtlcTx]) TestConstants.anchorOutputsFeeratePerKw else TestConstants.feeratePerKw - val aliceInit = Init(aliceParams.initFeatures) - val bobInit = Init(bobParams.initFeatures) + val channelParams = computeChannelParams(setup, test.tags) val listener = TestProbe() within(30 seconds) { bob.underlying.system.eventStream.subscribe(listener.ref, classOf[ChannelAborted]) - alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, TestConstants.fundingSatoshis, dualFunded = false, commitTxFeerate, TestConstants.feeratePerKw, fundingTxFeeBudget_opt = None, Some(TestConstants.initiatorPushAmount), requireConfirmedInputs = false, requestFunding_opt = None, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType, replyTo = aliceOpenReplyTo.ref.toTyped) - bob ! INPUT_INIT_CHANNEL_NON_INITIATOR(ByteVector32.Zeroes, None, dualFunded = false, None, requireConfirmedInputs = false, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) + alice ! channelParams.initChannelAlice(TestConstants.fundingSatoshis, pushAmount_opt = Some(TestConstants.initiatorPushAmount)) + bob ! channelParams.initChannelBob() awaitCond(bob.stateName == WAIT_FOR_OPEN_CHANNEL) withFixture(test.toNoArgTest(FixtureParam(alice, bob, alice2bob, bob2alice, bob2blockchain, listener))) } } - test("recv OpenChannel") { f => + test("recv OpenChannel (anchor outputs zero fee htlc txs)") { f => import f._ val open = alice2bob.expectMsgType[OpenChannel] // Since https://github.com/lightningnetwork/lightning-rfc/pull/714 we must include an empty upfront_shutdown_script. assert(open.upfrontShutdownScript_opt.contains(ByteVector.empty)) - // We always send a channel type, even for standard channels. - assert(open.channelType_opt.contains(ChannelTypes.StaticRemoteKey())) - alice2bob.forward(bob) - awaitCond(bob.stateName == WAIT_FOR_FUNDING_CREATED) - assert(bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CREATED].params.commitmentFormat == DefaultCommitmentFormat) - } - - test("recv OpenChannel (anchor outputs)", Tag(ChannelStateTestsTags.AnchorOutputs)) { f => - import f._ - val open = alice2bob.expectMsgType[OpenChannel] - assert(open.channelType_opt.contains(ChannelTypes.AnchorOutputs())) + assert(open.channelType_opt.contains(ChannelTypes.AnchorOutputsZeroFeeHtlcTx())) alice2bob.forward(bob) awaitCond(bob.stateName == WAIT_FOR_FUNDING_CREATED) - assert(bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CREATED].params.commitmentFormat == UnsafeLegacyAnchorOutputsCommitmentFormat) + assert(bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CREATED].commitmentFormat == ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) } - test("recv OpenChannel (anchor outputs zero fee htlc txs)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + test("recv OpenChannel (anchor outputs zero fee htlc txs and scid alias)", Tag(ChannelStateTestsTags.ScidAlias)) { f => import f._ val open = alice2bob.expectMsgType[OpenChannel] - assert(open.channelType_opt.contains(ChannelTypes.AnchorOutputsZeroFeeHtlcTx())) + assert(open.channelType_opt.contains(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = true))) alice2bob.forward(bob) awaitCond(bob.stateName == WAIT_FOR_FUNDING_CREATED) - assert(bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CREATED].params.commitmentFormat == ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) + assert(bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CREATED].commitmentFormat == ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) } - test("recv OpenChannel (anchor outputs zero fee htlc txs and scid alias)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), Tag(ChannelStateTestsTags.ScidAlias)) { f => + test("recv OpenChannel (simple taproot channels)", Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => import f._ val open = alice2bob.expectMsgType[OpenChannel] - assert(open.channelType_opt.contains(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = true))) + assert(open.channelType_opt.contains(ChannelTypes.SimpleTaprootChannelsStaging())) alice2bob.forward(bob) awaitCond(bob.stateName == WAIT_FOR_FUNDING_CREATED) - assert(bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CREATED].params.commitmentFormat == ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) + assert(bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CREATED].commitmentFormat == ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat) + assert(open.commitNonce_opt.isDefined) } - test("recv OpenChannel (non-default channel type)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), Tag(StandardChannelType)) { f => + test("recv OpenChannel (simple taproot channels, missing nonce)", Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => import f._ val open = alice2bob.expectMsgType[OpenChannel] - assert(open.channelType_opt.contains(ChannelTypes.Standard())) - alice2bob.forward(bob) - awaitCond(bob.stateName == WAIT_FOR_FUNDING_CREATED) - assert(bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CREATED].params.commitmentFormat == DefaultCommitmentFormat) + assert(open.channelType_opt.contains(ChannelTypes.SimpleTaprootChannelsStaging())) + assert(open.commitNonce_opt.isDefined) + alice2bob.forward(bob, open.copy(tlvStream = open.tlvStream.copy(records = open.tlvStream.records.filterNot(_.isInstanceOf[ChannelTlv.NextLocalNonceTlv])))) + val error = bob2alice.expectMsgType[Error] + assert(error == Error(open.temporaryChannelId, MissingCommitNonce(open.temporaryChannelId, TxId(ByteVector32.Zeroes), 0).getMessage)) + listener.expectMsgType[ChannelAborted] + awaitCond(bob.stateName == CLOSED) } test("recv OpenChannel (invalid chain)") { f => @@ -187,7 +172,7 @@ class WaitForOpenChannelStateSpec extends TestKitBaseClass with FixtureAnyFunSui val delayTooHigh = CltvExpiryDelta(10000) bob ! open.copy(toSelfDelay = delayTooHigh) val error = bob2alice.expectMsgType[Error] - assert(error == Error(open.temporaryChannelId, ToSelfDelayTooHigh(open.temporaryChannelId, delayTooHigh, Alice.nodeParams.channelConf.maxToLocalDelay).getMessage)) + assert(error == Error(open.temporaryChannelId, ToSelfDelayTooHigh(open.temporaryChannelId, delayTooHigh, Bob.nodeParams.channelConf.maxToLocalDelay).getMessage)) listener.expectMsgType[ChannelAborted] awaitCond(bob.stateName == CLOSED) } @@ -204,19 +189,6 @@ class WaitForOpenChannelStateSpec extends TestKitBaseClass with FixtureAnyFunSui awaitCond(bob.stateName == CLOSED) } - test("recv OpenChannel (fee too low, but still valid)") { f => - import f._ - val open = alice2bob.expectMsgType[OpenChannel] - // set a very small fee - val tinyFee = FeeratePerKw(253 sat) - bob ! open.copy(feeratePerKw = tinyFee) - val error = bob2alice.expectMsgType[Error] - // we check that the error uses the temporary channel id - assert(error == Error(open.temporaryChannelId, "local/remote feerates are too different: remoteFeeratePerKw=253 localFeeratePerKw=10000")) - listener.expectMsgType[ChannelAborted] - awaitCond(bob.stateName == CLOSED) - } - test("recv OpenChannel (fee below absolute valid minimum)") { f => import f._ val open = alice2bob.expectMsgType[OpenChannel] @@ -278,19 +250,19 @@ class WaitForOpenChannelStateSpec extends TestKitBaseClass with FixtureAnyFunSui test("recv OpenChannel (upfront shutdown script)", Tag(ChannelStateTestsTags.UpfrontShutdownScript)) { f => import f._ val open = alice2bob.expectMsgType[OpenChannel] - assert(open.upfrontShutdownScript_opt.contains(alice.stateData.asInstanceOf[DATA_WAIT_FOR_ACCEPT_CHANNEL].initFunder.localParams.upfrontShutdownScript_opt.get)) + assert(open.upfrontShutdownScript_opt.contains(alice.stateData.asInstanceOf[DATA_WAIT_FOR_ACCEPT_CHANNEL].initFunder.localChannelParams.upfrontShutdownScript_opt.get)) alice2bob.forward(bob, open) awaitCond(bob.stateName == WAIT_FOR_FUNDING_CREATED) - assert(bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CREATED].params.remoteParams.upfrontShutdownScript_opt == open.upfrontShutdownScript_opt) + assert(bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CREATED].channelParams.remoteParams.upfrontShutdownScript_opt == open.upfrontShutdownScript_opt) } test("recv OpenChannel (empty upfront shutdown script)", Tag(ChannelStateTestsTags.UpfrontShutdownScript)) { f => import f._ val open = alice2bob.expectMsgType[OpenChannel] - val open1 = open.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(ByteVector.empty))) + val open1 = open.copy(tlvStream = TlvStream(open.tlvStream.records.filterNot(_.isInstanceOf[ChannelTlv.UpfrontShutdownScriptTlv]) + ChannelTlv.UpfrontShutdownScriptTlv(ByteVector.empty))) alice2bob.forward(bob, open1) awaitCond(bob.stateName == WAIT_FOR_FUNDING_CREATED) - assert(bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CREATED].params.remoteParams.upfrontShutdownScript_opt.isEmpty) + assert(bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CREATED].channelParams.remoteParams.upfrontShutdownScript_opt.isEmpty) } test("recv OpenChannel (invalid upfront shutdown script)", Tag(ChannelStateTestsTags.UpfrontShutdownScript)) { f => @@ -302,7 +274,7 @@ class WaitForOpenChannelStateSpec extends TestKitBaseClass with FixtureAnyFunSui awaitCond(bob.stateName == CLOSED) } - test("recv OpenChannel (zeroconf)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), Tag(ChannelStateTestsTags.ZeroConf)) { f => + test("recv OpenChannel (zeroconf)", Tag(ChannelStateTestsTags.ZeroConf)) { f => import f._ val open = alice2bob.expectMsgType[OpenChannel] alice2bob.forward(bob, open) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForOpenDualFundedChannelStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForOpenDualFundedChannelStateSpec.scala index 246a6fc34c..07b7ae96fb 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForOpenDualFundedChannelStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForOpenDualFundedChannelStateSpec.scala @@ -16,14 +16,13 @@ package fr.acinq.eclair.channel.states.a -import akka.actor.typed.scaladsl.adapter.ClassicActorRefOps import akka.testkit.{TestFSMRef, TestProbe} import fr.acinq.bitcoin.scalacompat.{Block, BlockHash, ByteVector32, SatoshiLong} -import fr.acinq.eclair.TestConstants.{Alice, Bob} +import fr.acinq.eclair.TestConstants.Bob import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags} -import fr.acinq.eclair.wire.protocol.{AcceptDualFundedChannel, ChannelTlv, Error, Init, LiquidityAds, OpenDualFundedChannel} +import fr.acinq.eclair.wire.protocol.{AcceptDualFundedChannel, ChannelTlv, Error, LiquidityAds, OpenDualFundedChannel} import fr.acinq.eclair.{MilliSatoshiLong, TestConstants, TestKitBaseClass, randomBytes32} import org.scalatest.funsuite.FixtureAnyFunSuiteLike import org.scalatest.{Outcome, Tag} @@ -49,23 +48,19 @@ class WaitForOpenDualFundedChannelStateSpec extends TestKitBaseClass with Fixtur bob.underlyingActor.context.system.eventStream.subscribe(bobListener.ref, classOf[ChannelIdAssigned]) bob.underlyingActor.context.system.eventStream.subscribe(bobListener.ref, classOf[ChannelAborted]) - val channelConfig = ChannelConfig.standard - val channelFlags = ChannelFlags(announceChannel = false) val pushAmount = if (test.tags.contains(ChannelStateTestsTags.NoPushAmount)) None else Some(TestConstants.initiatorPushAmount) val nonInitiatorContribution = if (test.tags.contains(ChannelStateTestsTags.LiquidityAds)) Some(LiquidityAds.AddFunding(TestConstants.nonInitiatorFundingSatoshis, Some(TestConstants.defaultLiquidityRates))) else None - val (aliceParams, bobParams, channelType) = computeFeatures(setup, test.tags, channelFlags) - val aliceInit = Init(aliceParams.initFeatures) - val bobInit = Init(bobParams.initFeatures) + val channelParams = computeChannelParams(setup, test.tags) val requireConfirmedInputs = test.tags.contains(aliceRequiresConfirmedInputs) within(30 seconds) { - alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, TestConstants.fundingSatoshis, dualFunded = true, TestConstants.anchorOutputsFeeratePerKw, TestConstants.feeratePerKw, fundingTxFeeBudget_opt = None, pushAmount, requireConfirmedInputs, None, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType, replyTo = aliceOpenReplyTo.ref.toTyped) - bob ! INPUT_INIT_CHANNEL_NON_INITIATOR(ByteVector32.Zeroes, nonInitiatorContribution, dualFunded = true, None, requireConfirmedInputs = false, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) + alice ! channelParams.initChannelAlice(TestConstants.fundingSatoshis, dualFunded = true, requireConfirmedInputs = requireConfirmedInputs, pushAmount_opt = pushAmount) + bob ! channelParams.initChannelBob(nonInitiatorContribution, dualFunded = true) awaitCond(bob.stateName == WAIT_FOR_OPEN_DUAL_FUNDED_CHANNEL) withFixture(test.toNoArgTest(FixtureParam(alice, bob, alice2bob, bob2alice, aliceListener, bobListener))) } } - test("recv OpenDualFundedChannel", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.NoPushAmount), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + test("recv OpenDualFundedChannel", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.NoPushAmount)) { f => import f._ val open = alice2bob.expectMsgType[OpenDualFundedChannel] @@ -96,7 +91,7 @@ class WaitForOpenDualFundedChannelStateSpec extends TestKitBaseClass with Fixtur awaitCond(bob.stateName == WAIT_FOR_DUAL_FUNDING_CREATED) } - test("recv OpenDualFundedChannel (with liquidity ads)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.LiquidityAds), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + test("recv OpenDualFundedChannel (with liquidity ads)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.LiquidityAds)) { f => import f._ val open = alice2bob.expectMsgType[OpenDualFundedChannel] @@ -108,7 +103,7 @@ class WaitForOpenDualFundedChannelStateSpec extends TestKitBaseClass with Fixtur assert(accept.willFund_opt.nonEmpty) } - test("recv OpenDualFundedChannel (with liquidity ads and fee credit)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.LiquidityAds), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + test("recv OpenDualFundedChannel (with liquidity ads and fee credit)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.LiquidityAds)) { f => import f._ val open = alice2bob.expectMsgType[OpenDualFundedChannel] @@ -121,7 +116,7 @@ class WaitForOpenDualFundedChannelStateSpec extends TestKitBaseClass with Fixtur assert(accept.tlvStream.get[ChannelTlv.FeeCreditUsedTlv].map(_.amount).contains(2_500_000 msat)) } - test("recv OpenDualFundedChannel (with push amount)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + test("recv OpenDualFundedChannel (with push amount)", Tag(ChannelStateTestsTags.DualFunding)) { f => import f._ val open = alice2bob.expectMsgType[OpenDualFundedChannel] @@ -132,7 +127,7 @@ class WaitForOpenDualFundedChannelStateSpec extends TestKitBaseClass with Fixtur awaitCond(bob.stateName == WAIT_FOR_DUAL_FUNDING_CREATED) } - test("recv OpenDualFundedChannel (require confirmed inputs)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), Tag(aliceRequiresConfirmedInputs)) { f => + test("recv OpenDualFundedChannel (require confirmed inputs)", Tag(ChannelStateTestsTags.DualFunding), Tag(aliceRequiresConfirmedInputs)) { f => import f._ val open = alice2bob.expectMsgType[OpenDualFundedChannel] @@ -143,7 +138,7 @@ class WaitForOpenDualFundedChannelStateSpec extends TestKitBaseClass with Fixtur awaitCond(bob.stateName == WAIT_FOR_DUAL_FUNDING_CREATED) } - test("recv OpenDualFundedChannel (invalid chain)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + test("recv OpenDualFundedChannel (invalid chain)", Tag(ChannelStateTestsTags.DualFunding)) { f => import f._ val open = alice2bob.expectMsgType[OpenDualFundedChannel] val chain = BlockHash(randomBytes32()) @@ -154,7 +149,7 @@ class WaitForOpenDualFundedChannelStateSpec extends TestKitBaseClass with Fixtur awaitCond(bob.stateName == CLOSED) } - test("recv OpenDualFundedChannel (funding too low)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.NoPushAmount), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + test("recv OpenDualFundedChannel (funding too low)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.NoPushAmount)) { f => import f._ val open = alice2bob.expectMsgType[OpenDualFundedChannel] bob ! open.copy(fundingAmount = 100 sat) @@ -164,7 +159,7 @@ class WaitForOpenDualFundedChannelStateSpec extends TestKitBaseClass with Fixtur awaitCond(bob.stateName == CLOSED) } - test("recv OpenDualFundedChannel (invalid push amount)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.NoPushAmount), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + test("recv OpenDualFundedChannel (invalid push amount)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.NoPushAmount)) { f => import f._ val open = alice2bob.expectMsgType[OpenDualFundedChannel] bob ! open.copy(fundingAmount = 50_000 sat, tlvStream = open.tlvStream.copy(records = open.tlvStream.records + ChannelTlv.PushAmountTlv(50_000_001 msat))) @@ -174,7 +169,7 @@ class WaitForOpenDualFundedChannelStateSpec extends TestKitBaseClass with Fixtur awaitCond(bob.stateName == CLOSED) } - test("recv OpenDualFundedChannel (invalid max accepted htlcs)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + test("recv OpenDualFundedChannel (invalid max accepted htlcs)", Tag(ChannelStateTestsTags.DualFunding)) { f => import f._ val open = alice2bob.expectMsgType[OpenDualFundedChannel] val invalidMaxAcceptedHtlcs = Channel.MAX_ACCEPTED_HTLCS + 1 @@ -185,18 +180,18 @@ class WaitForOpenDualFundedChannelStateSpec extends TestKitBaseClass with Fixtur awaitCond(bob.stateName == CLOSED) } - test("recv OpenDualFundedChannel (to_self_delay too high)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + test("recv OpenDualFundedChannel (to_self_delay too high)", Tag(ChannelStateTestsTags.DualFunding)) { f => import f._ val open = alice2bob.expectMsgType[OpenDualFundedChannel] - val delayTooHigh = Alice.nodeParams.channelConf.maxToLocalDelay + 1 + val delayTooHigh = Bob.nodeParams.channelConf.maxToLocalDelay + 1 bob ! open.copy(toSelfDelay = delayTooHigh) val error = bob2alice.expectMsgType[Error] - assert(error == Error(open.temporaryChannelId, ToSelfDelayTooHigh(open.temporaryChannelId, delayTooHigh, Alice.nodeParams.channelConf.maxToLocalDelay).getMessage)) + assert(error == Error(open.temporaryChannelId, ToSelfDelayTooHigh(open.temporaryChannelId, delayTooHigh, Bob.nodeParams.channelConf.maxToLocalDelay).getMessage)) bobListener.expectMsgType[ChannelAborted] awaitCond(bob.stateName == CLOSED) } - test("recv OpenDualFundedChannel (dust limit too high)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + test("recv OpenDualFundedChannel (dust limit too high)", Tag(ChannelStateTestsTags.DualFunding)) { f => import f._ val open = alice2bob.expectMsgType[OpenDualFundedChannel] val dustLimitTooHigh = Bob.nodeParams.channelConf.maxRemoteDustLimit + 1.sat @@ -207,7 +202,7 @@ class WaitForOpenDualFundedChannelStateSpec extends TestKitBaseClass with Fixtur awaitCond(bob.stateName == CLOSED) } - test("recv OpenDualFundedChannel (dust limit too small)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + test("recv OpenDualFundedChannel (dust limit too small)", Tag(ChannelStateTestsTags.DualFunding)) { f => import f._ val open = alice2bob.expectMsgType[OpenDualFundedChannel] val dustLimitTooSmall = Channel.MIN_DUST_LIMIT - 1.sat @@ -218,14 +213,14 @@ class WaitForOpenDualFundedChannelStateSpec extends TestKitBaseClass with Fixtur awaitCond(bob.stateName == CLOSED) } - test("recv Error", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + test("recv Error", Tag(ChannelStateTestsTags.DualFunding)) { f => import f._ bob ! Error(ByteVector32.Zeroes, "dual funding not supported") bobListener.expectMsgType[ChannelAborted] awaitCond(bob.stateName == CLOSED) } - test("recv CMD_CLOSE", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + test("recv CMD_CLOSE", Tag(ChannelStateTestsTags.DualFunding)) { f => import f._ val sender = TestProbe() val cmd = CMD_CLOSE(sender.ref, None, None) @@ -235,7 +230,7 @@ class WaitForOpenDualFundedChannelStateSpec extends TestKitBaseClass with Fixtur awaitCond(bob.stateName == CLOSED) } - test("recv INPUT_DISCONNECTED", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + test("recv INPUT_DISCONNECTED", Tag(ChannelStateTestsTags.DualFunding)) { f => import f._ bob ! INPUT_DISCONNECTED bobListener.expectMsgType[ChannelAborted] diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForDualFundingCreatedStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForDualFundingCreatedStateSpec.scala index ad5feb7184..ed356036ee 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForDualFundingCreatedStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForDualFundingCreatedStateSpec.scala @@ -16,9 +16,8 @@ package fr.acinq.eclair.channel.states.b -import akka.actor.typed.scaladsl.adapter.ClassicActorRefOps import akka.testkit.{TestFSMRef, TestProbe} -import fr.acinq.bitcoin.scalacompat.{ByteVector32, SatoshiLong, Script} +import fr.acinq.bitcoin.scalacompat.{SatoshiLong, Script} import fr.acinq.eclair.blockchain.SingleKeyOnChainWallet import fr.acinq.eclair.blockchain.fee.FeeratePerKw import fr.acinq.eclair.channel._ @@ -27,7 +26,7 @@ import fr.acinq.eclair.channel.fsm.Channel.TickChannelOpenTimeout import fr.acinq.eclair.channel.publish.TxPublisher import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags} import fr.acinq.eclair.io.Peer.OpenChannelResponse -import fr.acinq.eclair.wire.protocol.{AcceptDualFundedChannel, ChannelReestablish, CommitSig, Error, Init, LiquidityAds, OpenDualFundedChannel, TxAbort, TxAckRbf, TxAddInput, TxAddOutput, TxComplete, TxInitRbf, Warning} +import fr.acinq.eclair.wire.protocol.{AcceptDualFundedChannel, ChannelReestablish, CommitSig, Error, Init, LiquidityAds, OpenDualFundedChannel, TxAbort, TxAckRbf, TxAddInput, TxAddOutput, TxComplete, TxCompleteTlv, TxInitRbf, Warning} import fr.acinq.eclair.{TestConstants, TestKitBaseClass, UInt64, randomKey} import org.scalatest.funsuite.FixtureAnyFunSuiteLike import org.scalatest.{Outcome, Tag} @@ -37,24 +36,21 @@ import scala.concurrent.duration.DurationInt class WaitForDualFundingCreatedStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with ChannelStateTestsBase { - case class FixtureParam(alice: TestFSMRef[ChannelState, ChannelData, Channel], bob: TestFSMRef[ChannelState, ChannelData, Channel], aliceOpenReplyTo: TestProbe, alice2bob: TestProbe, bob2alice: TestProbe, alice2blockchain: TestProbe, bob2blockchain: TestProbe, wallet: SingleKeyOnChainWallet, aliceListener: TestProbe, bobListener: TestProbe) + case class FixtureParam(alice: TestFSMRef[ChannelState, ChannelData, Channel], bob: TestFSMRef[ChannelState, ChannelData, Channel], aliceOpenReplyTo: TestProbe, alice2bob: TestProbe, bob2alice: TestProbe, alice2blockchain: TestProbe, bob2blockchain: TestProbe, aliceWallet: SingleKeyOnChainWallet, bobWallet: SingleKeyOnChainWallet, aliceListener: TestProbe, bobListener: TestProbe) override def withFixture(test: OneArgTest): Outcome = { - val wallet = new SingleKeyOnChainWallet() - val setup = init(wallet_opt = Some(wallet), tags = test.tags) + val aliceWallet = new SingleKeyOnChainWallet() + val bobWallet = new SingleKeyOnChainWallet() + val setup = init(walletA_opt = Some(aliceWallet), walletB_opt = Some(bobWallet), tags = test.tags) import setup._ - val channelConfig = ChannelConfig.standard - val channelFlags = ChannelFlags(announceChannel = false) - val (aliceParams, bobParams, channelType) = computeFeatures(setup, test.tags, channelFlags) - val aliceInit = Init(aliceParams.initFeatures) - val bobInit = Init(bobParams.initFeatures) + val channelParams = computeChannelParams(setup, test.tags) val aliceListener = TestProbe() val bobListener = TestProbe() within(30 seconds) { alice.underlying.system.eventStream.subscribe(aliceListener.ref, classOf[ChannelAborted]) bob.underlying.system.eventStream.subscribe(bobListener.ref, classOf[ChannelAborted]) - alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, TestConstants.fundingSatoshis, dualFunded = true, TestConstants.feeratePerKw, TestConstants.feeratePerKw, fundingTxFeeBudget_opt = None, None, requireConfirmedInputs = false, requestFunding_opt = None, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType, replyTo = aliceOpenReplyTo.ref.toTyped) - bob ! INPUT_INIT_CHANNEL_NON_INITIATOR(ByteVector32.Zeroes, Some(LiquidityAds.AddFunding(TestConstants.nonInitiatorFundingSatoshis, None)), dualFunded = true, None, requireConfirmedInputs = false, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) + alice ! channelParams.initChannelAlice(TestConstants.fundingSatoshis, dualFunded = true) + bob ! channelParams.initChannelBob(Some(LiquidityAds.AddFunding(TestConstants.nonInitiatorFundingSatoshis, None)), dualFunded = true) alice2blockchain.expectMsgType[TxPublisher.SetChannelId] // temporary channel id bob2blockchain.expectMsgType[TxPublisher.SetChannelId] // temporary channel id alice2bob.expectMsgType[OpenDualFundedChannel] @@ -65,7 +61,7 @@ class WaitForDualFundingCreatedStateSpec extends TestKitBaseClass with FixtureAn bob2blockchain.expectMsgType[TxPublisher.SetChannelId] // final channel id awaitCond(alice.stateName == WAIT_FOR_DUAL_FUNDING_CREATED) awaitCond(bob.stateName == WAIT_FOR_DUAL_FUNDING_CREATED) - withFixture(test.toNoArgTest(FixtureParam(alice, bob, aliceOpenReplyTo, alice2bob, bob2alice, alice2blockchain, bob2blockchain, wallet, aliceListener, bobListener))) + withFixture(test.toNoArgTest(FixtureParam(alice, bob, aliceOpenReplyTo, alice2bob, bob2alice, alice2blockchain, bob2blockchain, aliceWallet, bobWallet, aliceListener, bobListener))) } } @@ -102,31 +98,51 @@ class WaitForDualFundingCreatedStateSpec extends TestKitBaseClass with FixtureAn // Invalid serial_id. alice2bob.forward(bob, inputA.copy(serialId = UInt64(1))) bob2alice.expectMsgType[TxAbort] - awaitCond(wallet.rolledback.length == 1) + awaitCond(bobWallet.rolledback.length == 1) bobListener.expectMsgType[ChannelAborted] awaitCond(bob.stateName == CLOSED) // Below dust. bob2alice.forward(alice, TxAddOutput(channelId(bob), UInt64(1), 150 sat, Script.write(Script.pay2wpkh(randomKey().publicKey)))) alice2bob.expectMsgType[TxAbort] - awaitCond(wallet.rolledback.length == 2) + awaitCond(aliceWallet.rolledback.length == 1) aliceListener.expectMsgType[ChannelAborted] awaitCond(alice.stateName == CLOSED) aliceOpenReplyTo.expectMsgType[OpenChannelResponse.Rejected] } + test("recv tx_complete without nonces (taproot)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => + import f._ + + alice2bob.expectMsgType[TxAddInput] + alice2bob.forward(bob) + bob2alice.expectMsgType[TxAddInput] + bob2alice.forward(alice) + alice2bob.expectMsgType[TxAddOutput] + alice2bob.forward(bob) + bob2alice.expectMsgType[TxAddOutput] + bob2alice.forward(alice) + alice2bob.expectMsgType[TxAddOutput] + alice2bob.forward(bob) + val txComplete = bob2alice.expectMsgType[TxComplete] + assert(txComplete.commitNonces_opt.isDefined) + bob2alice.forward(alice, txComplete.copy(tlvStream = txComplete.tlvStream.copy(records = txComplete.tlvStream.records.filterNot(_.isInstanceOf[TxCompleteTlv.CommitNonces])))) + aliceListener.expectMsgType[ChannelAborted] + awaitCond(alice.stateName == CLOSED) + } + test("recv TxAbort", Tag(ChannelStateTestsTags.DualFunding)) { f => import f._ alice2bob.expectMsgType[TxAddInput] alice2bob.forward(bob, TxAbort(channelId(alice), hex"deadbeef")) val bobTxAbort = bob2alice.expectMsgType[TxAbort] - awaitCond(wallet.rolledback.size == 1) + awaitCond(bobWallet.rolledback.size == 1) bobListener.expectMsgType[ChannelAborted] awaitCond(bob.stateName == CLOSED) bob2alice.forward(alice, bobTxAbort) - awaitCond(wallet.rolledback.size == 2) + awaitCond(aliceWallet.rolledback.size == 1) aliceListener.expectMsgType[ChannelAborted] awaitCond(alice.stateName == CLOSED) aliceOpenReplyTo.expectMsgType[OpenChannelResponse.RemoteError] @@ -144,7 +160,7 @@ class WaitForDualFundingCreatedStateSpec extends TestKitBaseClass with FixtureAn alice2bob.expectMsgType[Warning] assert(alice.stateName == WAIT_FOR_DUAL_FUNDING_CREATED) aliceOpenReplyTo.expectNoMessage(100 millis) - assert(wallet.rolledback.isEmpty) + assert(aliceWallet.rolledback.isEmpty) } test("recv TxAckRbf", Tag(ChannelStateTestsTags.DualFunding)) { f => @@ -159,7 +175,7 @@ class WaitForDualFundingCreatedStateSpec extends TestKitBaseClass with FixtureAn alice2bob.expectMsgType[Warning] assert(alice.stateName == WAIT_FOR_DUAL_FUNDING_CREATED) aliceOpenReplyTo.expectNoMessage(100 millis) - assert(wallet.rolledback.isEmpty) + assert(aliceWallet.rolledback.isEmpty) } test("recv Error", Tag(ChannelStateTestsTags.DualFunding)) { f => @@ -167,13 +183,13 @@ class WaitForDualFundingCreatedStateSpec extends TestKitBaseClass with FixtureAn val finalChannelId = channelId(alice) alice ! Error(finalChannelId, "oops") - awaitCond(wallet.rolledback.size == 1) + awaitCond(aliceWallet.rolledback.size == 1) aliceListener.expectMsgType[ChannelAborted] awaitCond(alice.stateName == CLOSED) aliceOpenReplyTo.expectMsgType[OpenChannelResponse.RemoteError] bob ! Error(finalChannelId, "oops") - awaitCond(wallet.rolledback.size == 2) + awaitCond(bobWallet.rolledback.size == 1) bobListener.expectMsgType[ChannelAborted] awaitCond(bob.stateName == CLOSED) } @@ -187,14 +203,14 @@ class WaitForDualFundingCreatedStateSpec extends TestKitBaseClass with FixtureAn alice ! c sender.expectMsg(RES_SUCCESS(c, finalChannelId)) - awaitCond(wallet.rolledback.size == 1) + awaitCond(aliceWallet.rolledback.size == 1) aliceListener.expectMsgType[ChannelAborted] awaitCond(alice.stateName == CLOSED) aliceOpenReplyTo.expectMsg(OpenChannelResponse.Cancelled) bob ! c sender.expectMsg(RES_SUCCESS(c, finalChannelId)) - awaitCond(wallet.rolledback.size == 2) + awaitCond(bobWallet.rolledback.size == 1) bobListener.expectMsgType[ChannelAborted] awaitCond(bob.stateName == CLOSED) } @@ -203,13 +219,13 @@ class WaitForDualFundingCreatedStateSpec extends TestKitBaseClass with FixtureAn import f._ alice ! INPUT_DISCONNECTED - awaitCond(wallet.rolledback.size == 1) + awaitCond(aliceWallet.rolledback.size == 1) aliceListener.expectMsgType[ChannelAborted] awaitCond(alice.stateName == CLOSED) aliceOpenReplyTo.expectMsg(OpenChannelResponse.Disconnected) bob ! INPUT_DISCONNECTED - awaitCond(wallet.rolledback.size == 2) + awaitCond(bobWallet.rolledback.size == 1) bobListener.expectMsgType[ChannelAborted] awaitCond(bob.stateName == CLOSED) } @@ -255,10 +271,10 @@ class WaitForDualFundingCreatedStateSpec extends TestKitBaseClass with FixtureAn import f._ alice ! TickChannelOpenTimeout - awaitCond(wallet.rolledback.size == 1) + awaitCond(aliceWallet.rolledback.size == 1) aliceListener.expectMsgType[ChannelAborted] awaitCond(alice.stateName == CLOSED) aliceOpenReplyTo.expectMsg(OpenChannelResponse.TimedOut) } -} +} \ No newline at end of file diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForDualFundingSignedStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForDualFundingSignedStateSpec.scala index 0e23ffa827..04c6a37709 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForDualFundingSignedStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForDualFundingSignedStateSpec.scala @@ -16,22 +16,23 @@ package fr.acinq.eclair.channel.states.b -import akka.actor.typed.scaladsl.adapter.ClassicActorRefOps import akka.testkit.{TestFSMRef, TestProbe} -import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, SatoshiLong, TxId} +import fr.acinq.bitcoin.scalacompat.{ByteVector64, SatoshiLong, TxId} import fr.acinq.eclair.TestUtils.randomTxId -import fr.acinq.eclair.blockchain.SingleKeyOnChainWallet import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.{WatchFundingConfirmed, WatchPublished, WatchPublishedTriggered} import fr.acinq.eclair.blockchain.fee.FeeratePerKw +import fr.acinq.eclair.blockchain.{NewTransaction, SingleKeyOnChainWallet} +import fr.acinq.eclair.channel.ChannelSpendSignature.{IndividualSignature, PartialSignatureWithNonce} import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.channel.fund.InteractiveTxBuilder.{FullySignedSharedTransaction, PartiallySignedSharedTransaction} import fr.acinq.eclair.channel.publish.TxPublisher import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags} +import fr.acinq.eclair.crypto.NonceGenerator import fr.acinq.eclair.io.Peer.{LiquidityPurchaseSigned, OpenChannelResponse} -import fr.acinq.eclair.transactions.Transactions +import fr.acinq.eclair.transactions.Transactions._ import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{Features, MilliSatoshiLong, TestConstants, TestKitBaseClass, ToMilliSatoshiConversion} +import fr.acinq.eclair.{Features, MilliSatoshiLong, TestConstants, TestKitBaseClass, ToMilliSatoshiConversion, randomBytes32, randomKey} import org.scalatest.funsuite.FixtureAnyFunSuiteLike import org.scalatest.{Outcome, Tag} @@ -39,31 +40,24 @@ import scala.concurrent.duration.DurationInt class WaitForDualFundingSignedStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with ChannelStateTestsBase { - case class FixtureParam(alice: TestFSMRef[ChannelState, ChannelData, Channel], bob: TestFSMRef[ChannelState, ChannelData, Channel], alicePeer: TestProbe, bobPeer: TestProbe, alice2bob: TestProbe, bob2alice: TestProbe, alice2blockchain: TestProbe, bob2blockchain: TestProbe, wallet: SingleKeyOnChainWallet, aliceListener: TestProbe, bobListener: TestProbe) + case class FixtureParam(alice: TestFSMRef[ChannelState, ChannelData, Channel], bob: TestFSMRef[ChannelState, ChannelData, Channel], alicePeer: TestProbe, bobPeer: TestProbe, alice2bob: TestProbe, bob2alice: TestProbe, alice2blockchain: TestProbe, bob2blockchain: TestProbe, aliceWallet: SingleKeyOnChainWallet, bobWallet: SingleKeyOnChainWallet, aliceListener: TestProbe, bobListener: TestProbe) override def withFixture(test: OneArgTest): Outcome = { - val wallet = new SingleKeyOnChainWallet() - val setup = init(wallet_opt = Some(wallet), tags = test.tags) + val aliceWallet = new SingleKeyOnChainWallet() + val bobWallet = new SingleKeyOnChainWallet() + val setup = init(walletA_opt = Some(aliceWallet), walletB_opt = Some(bobWallet), tags = test.tags) import setup._ - val channelConfig = ChannelConfig.standard - val channelFlags = ChannelFlags(announceChannel = false) - val (aliceParams, bobParams, channelType) = computeFeatures(setup, test.tags, channelFlags) - val aliceInit = Init(aliceParams.initFeatures) - val bobInit = Init(bobParams.initFeatures) - val bobContribution = if (channelType.features.contains(Features.ZeroConf)) None else Some(LiquidityAds.AddFunding(TestConstants.nonInitiatorFundingSatoshis, Some(TestConstants.defaultLiquidityRates))) + val channelParams = computeChannelParams(setup, test.tags) + val bobContribution = if (channelParams.channelType.features.contains(Features.ZeroConf)) None else Some(LiquidityAds.AddFunding(TestConstants.nonInitiatorFundingSatoshis, Some(TestConstants.defaultLiquidityRates))) val requestFunding_opt = if (test.tags.contains(ChannelStateTestsTags.LiquidityAds)) Some(LiquidityAds.RequestFunding(TestConstants.nonInitiatorFundingSatoshis, TestConstants.defaultLiquidityRates.fundingRates.head, LiquidityAds.PaymentDetails.FromChannelBalance)) else None val (initiatorPushAmount, nonInitiatorPushAmount) = if (test.tags.contains("both_push_amount")) (Some(TestConstants.initiatorPushAmount), Some(TestConstants.nonInitiatorPushAmount)) else (None, None) - val commitFeerate = channelType.commitmentFormat match { - case Transactions.DefaultCommitmentFormat => TestConstants.feeratePerKw - case _: Transactions.AnchorOutputsCommitmentFormat => TestConstants.anchorOutputsFeeratePerKw - } val aliceListener = TestProbe() val bobListener = TestProbe() within(30 seconds) { alice.underlying.system.eventStream.subscribe(aliceListener.ref, classOf[ChannelAborted]) bob.underlying.system.eventStream.subscribe(bobListener.ref, classOf[ChannelAborted]) - alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, TestConstants.fundingSatoshis, dualFunded = true, commitFeerate, TestConstants.feeratePerKw, fundingTxFeeBudget_opt = None, initiatorPushAmount, requireConfirmedInputs = false, requestFunding_opt, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType, replyTo = aliceOpenReplyTo.ref.toTyped) - bob ! INPUT_INIT_CHANNEL_NON_INITIATOR(ByteVector32.Zeroes, bobContribution, dualFunded = true, nonInitiatorPushAmount, requireConfirmedInputs = false, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) + alice ! channelParams.initChannelAlice(TestConstants.fundingSatoshis, dualFunded = true, requestFunding_opt = requestFunding_opt, pushAmount_opt = initiatorPushAmount) + bob ! channelParams.initChannelBob(bobContribution, dualFunded = true, pushAmount_opt = nonInitiatorPushAmount) alice2blockchain.expectMsgType[TxPublisher.SetChannelId] // temporary channel id bob2blockchain.expectMsgType[TxPublisher.SetChannelId] // temporary channel id alice2bob.expectMsgType[OpenDualFundedChannel] @@ -96,7 +90,7 @@ class WaitForDualFundingSignedStateSpec extends TestKitBaseClass with FixtureAny aliceOpenReplyTo.expectMsgType[OpenChannelResponse.Created] awaitCond(alice.stateName == WAIT_FOR_DUAL_FUNDING_SIGNED) awaitCond(bob.stateName == WAIT_FOR_DUAL_FUNDING_SIGNED) - withFixture(test.toNoArgTest(FixtureParam(alice, bob, alicePeer, bobPeer, alice2bob, bob2alice, alice2blockchain, bob2blockchain, wallet, aliceListener, bobListener))) + withFixture(test.toNoArgTest(FixtureParam(alice, bob, alicePeer, bobPeer, alice2bob, bob2alice, alice2blockchain, bob2blockchain, aliceWallet, bobWallet, aliceListener, bobListener))) } } @@ -115,7 +109,7 @@ class WaitForDualFundingSignedStateSpec extends TestKitBaseClass with FixtureAny bob2alice.expectMsgType[TxSignatures] awaitCond(bob.stateName == WAIT_FOR_DUAL_FUNDING_CONFIRMED) val bobData = bob.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED] - assert(bobData.commitments.params.channelFeatures.hasFeature(Features.DualFunding)) + assert(bobData.commitments.channelParams.channelFeatures.hasFeature(Features.DualFunding)) assert(bobData.latestFundingTx.sharedTx.isInstanceOf[PartiallySignedSharedTransaction]) val fundingTxId = bobData.latestFundingTx.sharedTx.asInstanceOf[PartiallySignedSharedTransaction].txId assert(bob2blockchain.expectMsgType[WatchFundingConfirmed].txId == fundingTxId) @@ -127,16 +121,17 @@ class WaitForDualFundingSignedStateSpec extends TestKitBaseClass with FixtureAny alice2bob.expectMsgType[TxSignatures] awaitCond(alice.stateName == WAIT_FOR_DUAL_FUNDING_CONFIRMED) val aliceData = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED] - assert(aliceData.commitments.params.channelFeatures.hasFeature(Features.DualFunding)) + assert(aliceData.commitments.channelParams.channelFeatures.hasFeature(Features.DualFunding)) assert(aliceData.latestFundingTx.sharedTx.isInstanceOf[FullySignedSharedTransaction]) assert(aliceData.latestFundingTx.sharedTx.asInstanceOf[FullySignedSharedTransaction].signedTx.txid == fundingTxId) } - test("complete interactive-tx protocol (zero-conf)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.ZeroConf), Tag(ChannelStateTestsTags.ScidAlias), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + test("complete interactive-tx protocol (zero-conf)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.ZeroConf), Tag(ChannelStateTestsTags.ScidAlias)) { f => import f._ val listener = TestProbe() alice.underlyingActor.context.system.eventStream.subscribe(listener.ref, classOf[TransactionPublished]) + alice.underlyingActor.context.system.eventStream.subscribe(listener.ref, classOf[NewTransaction]) bob2alice.expectMsgType[CommitSig] bob2alice.forward(alice) @@ -147,7 +142,7 @@ class WaitForDualFundingSignedStateSpec extends TestKitBaseClass with FixtureAny bob2alice.expectMsgType[TxSignatures] awaitCond(bob.stateName == WAIT_FOR_DUAL_FUNDING_CONFIRMED) val bobData = bob.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED] - assert(bobData.commitments.params.channelFeatures.hasFeature(Features.DualFunding)) + assert(bobData.commitments.channelParams.channelFeatures.hasFeature(Features.DualFunding)) assert(bobData.latestFundingTx.sharedTx.isInstanceOf[PartiallySignedSharedTransaction]) val fundingTxId = bobData.latestFundingTx.sharedTx.asInstanceOf[PartiallySignedSharedTransaction].tx.buildUnsignedTx().txid assert(bob2blockchain.expectMsgType[WatchPublished].txId == fundingTxId) @@ -156,25 +151,28 @@ class WaitForDualFundingSignedStateSpec extends TestKitBaseClass with FixtureAny // Alice receives Bob's signatures and sends her own signatures. bob2alice.forward(alice) assert(listener.expectMsgType[TransactionPublished].tx.txid == fundingTxId) + assert(listener.expectMsgType[NewTransaction].tx.txid == fundingTxId) assert(alice2blockchain.expectMsgType[WatchPublished].txId == fundingTxId) alice2bob.expectMsgType[TxSignatures] awaitCond(alice.stateName == WAIT_FOR_DUAL_FUNDING_CONFIRMED) val aliceData = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED] - assert(aliceData.commitments.params.channelFeatures.hasFeature(Features.DualFunding)) + assert(aliceData.commitments.channelParams.channelFeatures.hasFeature(Features.DualFunding)) assert(aliceData.latestFundingTx.sharedTx.isInstanceOf[FullySignedSharedTransaction]) assert(aliceData.latestFundingTx.sharedTx.asInstanceOf[FullySignedSharedTransaction].signedTx.txid == fundingTxId) } - test("complete interactive-tx protocol (with push amount)", Tag(ChannelStateTestsTags.DualFunding), Tag("both_push_amount")) { f => + test("complete interactive-tx protocol (with push amount, taproot)", Tag(ChannelStateTestsTags.DualFunding), Tag("both_push_amount"), Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => import f._ val listener = TestProbe() alice.underlyingActor.context.system.eventStream.subscribe(listener.ref, classOf[TransactionPublished]) - bob2alice.expectMsgType[CommitSig] - bob2alice.forward(alice) - alice2bob.expectMsgType[CommitSig] - alice2bob.forward(bob) + val commitSigB = bob2alice.expectMsgType[CommitSig] + assert(commitSigB.sigOrPartialSig.isInstanceOf[PartialSignatureWithNonce]) + bob2alice.forward(alice, commitSigB) + val commitSigA = alice2bob.expectMsgType[CommitSig] + assert(commitSigA.sigOrPartialSig.isInstanceOf[PartialSignatureWithNonce]) + alice2bob.forward(bob, commitSigA) val expectedBalanceAlice = TestConstants.fundingSatoshis.toMilliSatoshi + TestConstants.nonInitiatorPushAmount - TestConstants.initiatorPushAmount assert(expectedBalanceAlice == 900_000_000.msat) @@ -241,15 +239,38 @@ class WaitForDualFundingSignedStateSpec extends TestKitBaseClass with FixtureAny val bobCommitSig = bob2alice.expectMsgType[CommitSig] val aliceCommitSig = alice2bob.expectMsgType[CommitSig] - bob2alice.forward(alice, bobCommitSig.copy(signature = ByteVector64.Zeroes)) + bob2alice.forward(alice, bobCommitSig.copy(signature = IndividualSignature(ByteVector64.Zeroes))) + alice2bob.expectMsgType[Error] + awaitCond(aliceWallet.rolledback.length == 1) + aliceListener.expectMsgType[ChannelAborted] + awaitCond(alice.stateName == CLOSED) + + alice2bob.forward(bob, aliceCommitSig.copy(signature = IndividualSignature(ByteVector64.Zeroes))) + bob2alice.expectMsgType[Error] + awaitCond(bobWallet.rolledback.length == 1) + bobListener.expectMsgType[ChannelAborted] + awaitCond(bob.stateName == CLOSED) + } + + test("recv invalid CommitSig (taproot)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => + import f._ + + val bobCommitSig = bob2alice.expectMsgType[CommitSig] + assert(bobCommitSig.partialSignature_opt.nonEmpty) + val aliceCommitSig = alice2bob.expectMsgType[CommitSig] + assert(aliceCommitSig.partialSignature_opt.nonEmpty) + + val invalidSigBob = bobCommitSig.partialSignature_opt.get.copy(partialSig = randomBytes32()) + bob2alice.forward(alice, bobCommitSig.copy(tlvStream = TlvStream(CommitSigTlv.PartialSignatureWithNonceTlv(invalidSigBob)))) alice2bob.expectMsgType[Error] - awaitCond(wallet.rolledback.length == 1) + awaitCond(aliceWallet.rolledback.length == 1) aliceListener.expectMsgType[ChannelAborted] awaitCond(alice.stateName == CLOSED) - alice2bob.forward(bob, aliceCommitSig.copy(signature = ByteVector64.Zeroes)) + val invalidSigAlice = aliceCommitSig.partialSignature_opt.get.copy(nonce = NonceGenerator.signingNonce(randomKey().publicKey, randomKey().publicKey, randomTxId()).publicNonce) + alice2bob.forward(bob, aliceCommitSig.copy(tlvStream = TlvStream(CommitSigTlv.PartialSignatureWithNonceTlv(invalidSigAlice)))) bob2alice.expectMsgType[Error] - awaitCond(wallet.rolledback.length == 2) + awaitCond(bobWallet.rolledback.length == 1) bobListener.expectMsgType[ChannelAborted] awaitCond(bob.stateName == CLOSED) } @@ -266,7 +287,7 @@ class WaitForDualFundingSignedStateSpec extends TestKitBaseClass with FixtureAny bob2blockchain.expectMsgType[WatchFundingConfirmed] bob2alice.forward(alice, bobSigs.copy(txId = randomTxId(), witnesses = Nil)) alice2bob.expectMsgType[Error] - awaitCond(wallet.rolledback.size == 1) + awaitCond(aliceWallet.rolledback.size == 1) aliceListener.expectMsgType[ChannelAborted] awaitCond(alice.stateName == CLOSED) @@ -274,6 +295,7 @@ class WaitForDualFundingSignedStateSpec extends TestKitBaseClass with FixtureAny alice2bob.forward(bob, TxSignatures(channelId(alice), randomTxId(), Nil)) bob2alice.expectMsgType[Error] bob2blockchain.expectNoMessage(100 millis) + assert(bobWallet.rolledback.isEmpty) assert(bob.stateName == WAIT_FOR_DUAL_FUNDING_CONFIRMED) } @@ -290,7 +312,7 @@ class WaitForDualFundingSignedStateSpec extends TestKitBaseClass with FixtureAny bob2alice.forward(alice, TxInitRbf(channelId(bob), 0, FeeratePerKw(15_000 sat))) alice2bob.expectMsgType[Warning] assert(alice.stateName == WAIT_FOR_DUAL_FUNDING_SIGNED) - assert(wallet.rolledback.isEmpty) + assert(aliceWallet.rolledback.isEmpty) } test("recv TxAckRbf", Tag(ChannelStateTestsTags.DualFunding)) { f => @@ -306,7 +328,7 @@ class WaitForDualFundingSignedStateSpec extends TestKitBaseClass with FixtureAny bob2alice.forward(alice, TxAckRbf(channelId(bob))) alice2bob.expectMsgType[Warning] assert(alice.stateName == WAIT_FOR_DUAL_FUNDING_SIGNED) - assert(wallet.rolledback.isEmpty) + assert(aliceWallet.rolledback.isEmpty) } test("recv Error", Tag(ChannelStateTestsTags.DualFunding)) { f => @@ -314,12 +336,12 @@ class WaitForDualFundingSignedStateSpec extends TestKitBaseClass with FixtureAny val finalChannelId = channelId(alice) alice ! Error(finalChannelId, "oops") - awaitCond(wallet.rolledback.size == 1) + awaitCond(aliceWallet.rolledback.size == 1) aliceListener.expectMsgType[ChannelAborted] awaitCond(alice.stateName == CLOSED) bob ! Error(finalChannelId, "oops") - awaitCond(wallet.rolledback.size == 2) + awaitCond(bobWallet.rolledback.size == 1) bobListener.expectMsgType[ChannelAborted] awaitCond(bob.stateName == CLOSED) } @@ -333,13 +355,38 @@ class WaitForDualFundingSignedStateSpec extends TestKitBaseClass with FixtureAny alice ! c sender.expectMsg(RES_SUCCESS(c, finalChannelId)) - awaitCond(wallet.rolledback.size == 1) + awaitCond(aliceWallet.rolledback.size == 1) + aliceListener.expectMsgType[ChannelAborted] + awaitCond(alice.stateName == CLOSED) + + bob ! c + sender.expectMsg(RES_SUCCESS(c, finalChannelId)) + awaitCond(bobWallet.rolledback.size == 1) + bobListener.expectMsgType[ChannelAborted] + awaitCond(bob.stateName == CLOSED) + } + + test("recv CMD_FORCECLOSE (offline)", Tag(ChannelStateTestsTags.DualFunding)) { f => + import f._ + + alice ! INPUT_DISCONNECTED + awaitCond(alice.stateName == OFFLINE) + bob ! INPUT_DISCONNECTED + awaitCond(bob.stateName == OFFLINE) + + val finalChannelId = channelId(alice) + val sender = TestProbe() + val c = CMD_FORCECLOSE(sender.ref) + + alice ! c + sender.expectMsg(RES_SUCCESS(c, finalChannelId)) + awaitCond(aliceWallet.rolledback.size == 1) aliceListener.expectMsgType[ChannelAborted] awaitCond(alice.stateName == CLOSED) bob ! c sender.expectMsg(RES_SUCCESS(c, finalChannelId)) - awaitCond(wallet.rolledback.size == 2) + awaitCond(bobWallet.rolledback.size == 1) bobListener.expectMsgType[ChannelAborted] awaitCond(bob.stateName == CLOSED) } @@ -358,10 +405,35 @@ class WaitForDualFundingSignedStateSpec extends TestKitBaseClass with FixtureAny awaitCond(bob.stateName == OFFLINE) assert(bob.stateData == bobData) bobListener.expectNoMessage(100 millis) - assert(wallet.rolledback.isEmpty) + assert(bobWallet.rolledback.isEmpty) + } + + def testReconnectCommitSigNotReceived(f: FixtureParam, commitmentFormat: CommitmentFormat): Unit = { + import f._ + + val fundingTxId = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_SIGNED].signingSession.fundingTx.txId + alice2bob.expectMsgType[CommitSig] // Bob doesn't receive Alice's commit_sig + bob2alice.expectMsgType[CommitSig] // Alice doesn't receive Bob's commit_sig + awaitCond(alice.stateName == WAIT_FOR_DUAL_FUNDING_SIGNED) + awaitCond(bob.stateName == WAIT_FOR_DUAL_FUNDING_SIGNED) + + alice ! INPUT_DISCONNECTED + awaitCond(alice.stateName == OFFLINE) + bob ! INPUT_DISCONNECTED + awaitCond(bob.stateName == OFFLINE) + + reconnect(f, fundingTxId, commitmentFormat, aliceExpectsCommitSig = true, bobExpectsCommitSig = true) } test("recv INPUT_DISCONNECTED (commit_sig not received)", Tag(ChannelStateTestsTags.DualFunding)) { f => + testReconnectCommitSigNotReceived(f, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) + } + + test("recv INPUT_DISCONNECTED (commit_sig not received, taproot)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => + testReconnectCommitSigNotReceived(f, ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat) + } + + test("recv INPUT_DISCONNECTED (commit_sig not received, missing taproot commit nonce)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => import f._ val fundingTxId = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_SIGNED].signingSession.fundingTx.txId @@ -375,10 +447,35 @@ class WaitForDualFundingSignedStateSpec extends TestKitBaseClass with FixtureAny bob ! INPUT_DISCONNECTED awaitCond(bob.stateName == OFFLINE) - reconnect(f, fundingTxId, aliceExpectsCommitSig = true, bobExpectsCommitSig = true) + val aliceInit = Init(alice.underlyingActor.nodeParams.features.initFeatures()) + val bobInit = Init(bob.underlyingActor.nodeParams.features.initFeatures()) + + alice ! INPUT_RECONNECTED(bob, aliceInit, bobInit) + val channelReestablishAlice = alice2bob.expectMsgType[ChannelReestablish] + assert(channelReestablishAlice.nextLocalCommitmentNumber == 0) + assert(channelReestablishAlice.currentCommitNonce_opt.nonEmpty) + assert(channelReestablishAlice.nextCommitNonces.contains(fundingTxId)) + + bob ! INPUT_RECONNECTED(alice, bobInit, aliceInit) + val channelReestablishBob = bob2alice.expectMsgType[ChannelReestablish] + assert(channelReestablishBob.nextLocalCommitmentNumber == 0) + assert(channelReestablishBob.currentCommitNonce_opt.nonEmpty) + assert(channelReestablishBob.nextCommitNonces.contains(fundingTxId)) + + // If Alice doesn't include her current commit nonce, Bob won't be able to retransmit commit_sig. + val channelReestablishAlice1 = channelReestablishAlice.copy(tlvStream = TlvStream(channelReestablishAlice.tlvStream.records.filterNot(_.isInstanceOf[ChannelReestablishTlv.CurrentCommitNonceTlv]))) + alice2bob.forward(bob, channelReestablishAlice1) + assert(bob2alice.expectMsgType[Error].toAscii == MissingCommitNonce(channelReestablishBob.channelId, fundingTxId, commitmentNumber = 0).getMessage) + awaitCond(bob.stateName == CLOSED) + + // If Bob doesn't include nonces for this next commit, Alice won't be able to update the channel. + val channelReestablishBob1 = channelReestablishBob.copy(tlvStream = TlvStream(channelReestablishBob.tlvStream.records.filterNot(_.isInstanceOf[ChannelReestablishTlv.NextLocalNoncesTlv]))) + bob2alice.forward(alice, channelReestablishBob1) + assert(alice2bob.expectMsgType[Error].toAscii == MissingCommitNonce(channelReestablishBob.channelId, fundingTxId, commitmentNumber = 1).getMessage) + awaitCond(bob.stateName == CLOSED) } - test("recv INPUT_DISCONNECTED (commit_sig received by Alice)", Tag(ChannelStateTestsTags.DualFunding)) { f => + def testReconnectCommitSigReceivedByAlice(f: FixtureParam, commitmentFormat: CommitmentFormat): Unit = { import f._ val fundingTxId = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_SIGNED].signingSession.fundingTx.txId @@ -393,7 +490,15 @@ class WaitForDualFundingSignedStateSpec extends TestKitBaseClass with FixtureAny bob ! INPUT_DISCONNECTED awaitCond(bob.stateName == OFFLINE) - reconnect(f, fundingTxId, aliceExpectsCommitSig = false, bobExpectsCommitSig = true) + reconnect(f, fundingTxId, commitmentFormat, aliceExpectsCommitSig = false, bobExpectsCommitSig = true) + } + + test("recv INPUT_DISCONNECTED (commit_sig received by Alice)", Tag(ChannelStateTestsTags.DualFunding)) { f => + testReconnectCommitSigReceivedByAlice(f, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) + } + + test("recv INPUT_DISCONNECTED (commit_sig received by Alice, taproot)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => + testReconnectCommitSigReceivedByAlice(f, ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat) } test("recv INPUT_DISCONNECTED (commit_sig received by Bob)", Tag(ChannelStateTestsTags.DualFunding)) { f => @@ -412,10 +517,10 @@ class WaitForDualFundingSignedStateSpec extends TestKitBaseClass with FixtureAny bob ! INPUT_DISCONNECTED awaitCond(bob.stateName == OFFLINE) - reconnect(f, fundingTxId, aliceExpectsCommitSig = true, bobExpectsCommitSig = false) + reconnect(f, fundingTxId, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat, aliceExpectsCommitSig = true, bobExpectsCommitSig = false) } - test("recv INPUT_DISCONNECTED (commit_sig received by Bob, zero-conf)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.ZeroConf), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + test("recv INPUT_DISCONNECTED (commit_sig received by Bob, zero-conf)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.ZeroConf), Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => import f._ alice2bob.expectMsgType[CommitSig] @@ -431,7 +536,8 @@ class WaitForDualFundingSignedStateSpec extends TestKitBaseClass with FixtureAny assert(bob2blockchain.expectMsgType[WatchPublished].txId == fundingTx.txid) bob ! WatchPublishedTriggered(fundingTx) assert(bob2blockchain.expectMsgType[WatchFundingConfirmed].txId == fundingTx.txid) - bob2alice.expectMsgType[ChannelReady] + val channelReadyB = bob2alice.expectMsgType[ChannelReady] + assert(channelReadyB.nextCommitNonce_opt.nonEmpty) awaitCond(bob.stateName == WAIT_FOR_DUAL_FUNDING_READY) alice ! INPUT_DISCONNECTED @@ -447,15 +553,20 @@ class WaitForDualFundingSignedStateSpec extends TestKitBaseClass with FixtureAny alice ! INPUT_RECONNECTED(bob, aliceInit, bobInit) bob ! INPUT_RECONNECTED(alice, bobInit, aliceInit) val channelReestablishAlice = alice2bob.expectMsgType[ChannelReestablish] + assert(channelReestablishAlice.currentCommitNonce_opt.nonEmpty) + assert(channelReestablishAlice.nextCommitNonces.contains(fundingTx.txid)) + assert(channelReestablishAlice.nextCommitNonces.get(fundingTx.txid) != channelReestablishAlice.currentCommitNonce_opt) assert(channelReestablishAlice.nextFundingTxId_opt.contains(fundingTx.txid)) assert(channelReestablishAlice.nextLocalCommitmentNumber == 0) alice2bob.forward(bob, channelReestablishAlice) val channelReestablishBob = bob2alice.expectMsgType[ChannelReestablish] + assert(channelReestablishBob.currentCommitNonce_opt.isEmpty) + assert(channelReestablishBob.nextCommitNonces.get(fundingTx.txid) == channelReadyB.nextCommitNonce_opt) assert(channelReestablishBob.nextFundingTxId_opt.isEmpty) assert(channelReestablishBob.nextLocalCommitmentNumber == 1) bob2alice.forward(alice, channelReestablishBob) - bob2alice.expectMsgType[CommitSig] + assert(bob2alice.expectMsgType[CommitSig].partialSignature_opt.nonEmpty) bob2alice.forward(alice) bob2alice.expectMsgType[TxSignatures] bob2alice.forward(alice) @@ -485,7 +596,7 @@ class WaitForDualFundingSignedStateSpec extends TestKitBaseClass with FixtureAny bob ! INPUT_DISCONNECTED awaitCond(bob.stateName == OFFLINE) - reconnect(f, fundingTxId, aliceExpectsCommitSig = false, bobExpectsCommitSig = false) + reconnect(f, fundingTxId, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat, aliceExpectsCommitSig = false, bobExpectsCommitSig = false) } test("recv INPUT_DISCONNECTED (tx_signatures received)", Tag(ChannelStateTestsTags.DualFunding)) { f => @@ -525,7 +636,7 @@ class WaitForDualFundingSignedStateSpec extends TestKitBaseClass with FixtureAny assert(listener.expectMsgType[TransactionPublished].tx.txid == fundingTxId) } - test("recv INPUT_DISCONNECTED (tx_signatures received, zero-conf)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.ZeroConf), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + test("recv INPUT_DISCONNECTED (tx_signatures received, zero-conf)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.ZeroConf), Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => import f._ val listener = TestProbe() @@ -545,7 +656,8 @@ class WaitForDualFundingSignedStateSpec extends TestKitBaseClass with FixtureAny assert(alice2blockchain.expectMsgType[WatchPublished].txId == fundingTx.txid) alice ! WatchPublishedTriggered(fundingTx) assert(alice2blockchain.expectMsgType[WatchFundingConfirmed].txId == fundingTx.txid) - alice2bob.expectMsgType[ChannelReady] + val channelReadyA1 = alice2bob.expectMsgType[ChannelReady] + assert(channelReadyA1.nextCommitNonce_opt.nonEmpty) awaitCond(alice.stateName == WAIT_FOR_DUAL_FUNDING_READY) alice ! INPUT_DISCONNECTED @@ -558,19 +670,26 @@ class WaitForDualFundingSignedStateSpec extends TestKitBaseClass with FixtureAny alice ! INPUT_RECONNECTED(bob, aliceInit, bobInit) bob ! INPUT_RECONNECTED(alice, bobInit, aliceInit) - assert(alice2bob.expectMsgType[ChannelReestablish].nextFundingTxId_opt.isEmpty) - alice2bob.forward(bob) - assert(bob2alice.expectMsgType[ChannelReestablish].nextFundingTxId_opt.contains(fundingTx.txid)) - bob2alice.forward(alice) + val channelReestablishA = alice2bob.expectMsgType[ChannelReestablish] + assert(channelReestablishA.nextFundingTxId_opt.isEmpty) + assert(channelReestablishA.currentCommitNonce_opt.isEmpty) + assert(channelReestablishA.nextCommitNonces.get(fundingTx.txid) == channelReadyA1.nextCommitNonce_opt) + alice2bob.forward(bob, channelReestablishA) + val channelReestablishB = bob2alice.expectMsgType[ChannelReestablish] + assert(channelReestablishB.nextFundingTxId_opt.contains(fundingTx.txid)) + assert(channelReestablishA.currentCommitNonce_opt.isEmpty) + assert(channelReestablishA.nextCommitNonces.contains(fundingTx.txid)) + bob2alice.forward(alice, channelReestablishB) alice2bob.expectMsgType[TxSignatures] alice2bob.forward(bob) - alice2bob.expectMsgType[ChannelReady] - alice2bob.forward(bob) + val channelReadyA2 = alice2bob.expectMsgType[ChannelReady] + assert(channelReadyA2.nextCommitNonce_opt == channelReadyA1.nextCommitNonce_opt) + alice2bob.forward(bob, channelReadyA2) assert(bob2blockchain.expectMsgType[WatchPublished].txId == fundingTx.txid) assert(listener.expectMsgType[TransactionPublished].tx.txid == fundingTx.txid) } - private def reconnect(f: FixtureParam, fundingTxId: TxId, aliceExpectsCommitSig: Boolean, bobExpectsCommitSig: Boolean): Unit = { + private def reconnect(f: FixtureParam, fundingTxId: TxId, commitmentFormat: CommitmentFormat, aliceExpectsCommitSig: Boolean, bobExpectsCommitSig: Boolean): Unit = { import f._ val listener = TestProbe() @@ -591,13 +710,38 @@ class WaitForDualFundingSignedStateSpec extends TestKitBaseClass with FixtureAny assert(channelReestablishBob.nextLocalCommitmentNumber == nextLocalCommitmentNumberBob) bob2alice.forward(alice, channelReestablishBob) + // When using taproot, we must provide nonces for the partial signatures. + commitmentFormat match { + case _: SegwitV0CommitmentFormat => () + case _: SimpleTaprootChannelCommitmentFormat => + Seq((channelReestablishAlice, aliceExpectsCommitSig), (channelReestablishBob, bobExpectsCommitSig)).foreach { + case (channelReestablish, expectCommitSig) => + assert(channelReestablish.nextCommitNonces.size == 1) + assert(channelReestablish.nextCommitNonces.contains(fundingTxId)) + if (expectCommitSig) { + assert(channelReestablish.currentCommitNonce_opt.nonEmpty) + assert(channelReestablish.currentCommitNonce_opt != channelReestablish.nextCommitNonces.get(fundingTxId)) + } else { + assert(channelReestablish.currentCommitNonce_opt.isEmpty) + } + } + } + if (aliceExpectsCommitSig) { - bob2alice.expectMsgType[CommitSig] - bob2alice.forward(alice) + val commitSigBob = bob2alice.expectMsgType[CommitSig] + commitmentFormat match { + case _: SegwitV0CommitmentFormat => assert(commitSigBob.partialSignature_opt.isEmpty) + case _: SimpleTaprootChannelCommitmentFormat => assert(commitSigBob.partialSignature_opt.nonEmpty) + } + bob2alice.forward(alice, commitSigBob) } if (bobExpectsCommitSig) { - alice2bob.expectMsgType[CommitSig] - alice2bob.forward(bob) + val commitSigAlice = alice2bob.expectMsgType[CommitSig] + commitmentFormat match { + case _: SegwitV0CommitmentFormat => assert(commitSigAlice.partialSignature_opt.isEmpty) + case _: SimpleTaprootChannelCommitmentFormat => assert(commitSigAlice.partialSignature_opt.nonEmpty) + } + alice2bob.forward(bob, commitSigAlice) } bob2alice.expectMsgType[TxSignatures] bob2alice.forward(alice) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingCreatedStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingCreatedStateSpec.scala index de687186b1..c4a77c07d7 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingCreatedStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingCreatedStateSpec.scala @@ -17,7 +17,6 @@ package fr.acinq.eclair.channel.states.b import akka.actor.ActorRef -import akka.actor.typed.scaladsl.adapter.ClassicActorRefOps import akka.testkit.{TestFSMRef, TestProbe} import fr.acinq.bitcoin.scalacompat.{ByteVector32, SatoshiLong} import fr.acinq.eclair.TestConstants.{Alice, Bob} @@ -26,7 +25,6 @@ import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.channel.publish.TxPublisher import fr.acinq.eclair.channel.states.ChannelStateTestsBase -import fr.acinq.eclair.transactions.Transactions import fr.acinq.eclair.wire.protocol._ import fr.acinq.eclair.{TestConstants, TestKitBaseClass, ToMilliSatoshiConversion} import org.scalatest.funsuite.FixtureAnyFunSuiteLike @@ -48,23 +46,18 @@ class WaitForFundingCreatedStateSpec extends TestKitBaseClass with FixtureAnyFun val setup = init(Alice.nodeParams, Bob.nodeParams, tags = test.tags) import setup._ - val (fundingSatoshis, pushMsat) = if (test.tags.contains(FunderBelowCommitFees)) { + val channelParams = computeChannelParams(setup, test.tags) + val (fundingAmount, pushAmount) = if (test.tags.contains(FunderBelowCommitFees)) { (1_000_100 sat, (1_000_000 sat).toMilliSatoshi) // toLocal = 100 satoshis } else { (TestConstants.fundingSatoshis, TestConstants.initiatorPushAmount) } - - val channelConfig = ChannelConfig.standard - val channelFlags = ChannelFlags(announceChannel = false) - val (aliceParams, bobParams, channelType) = computeFeatures(setup, test.tags, channelFlags) - val aliceInit = Init(aliceParams.initFeatures) - val bobInit = Init(bobParams.initFeatures) val listener = TestProbe() within(30 seconds) { bob.underlying.system.eventStream.subscribe(listener.ref, classOf[ChannelAborted]) - alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, fundingSatoshis, dualFunded = false, TestConstants.feeratePerKw, TestConstants.feeratePerKw, fundingTxFeeBudget_opt = None, Some(pushMsat), requireConfirmedInputs = false, requestFunding_opt = None, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType, replyTo = aliceOpenReplyTo.ref.toTyped) + alice ! channelParams.initChannelAlice(fundingAmount, pushAmount_opt = Some(pushAmount)) alice2blockchain.expectMsgType[TxPublisher.SetChannelId] - bob ! INPUT_INIT_CHANNEL_NON_INITIATOR(ByteVector32.Zeroes, None, dualFunded = false, None, requireConfirmedInputs = false, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) + bob ! channelParams.initChannelBob() bob2blockchain.expectMsgType[TxPublisher.SetChannelId] alice2bob.expectMsgType[OpenChannel] alice2bob.forward(bob) @@ -88,12 +81,10 @@ class WaitForFundingCreatedStateSpec extends TestKitBaseClass with FixtureAnyFun test("recv FundingCreated (funder can't pay fees)", Tag(FunderBelowCommitFees)) { f => import f._ - val fees = Transactions.weight2fee(TestConstants.feeratePerKw, Transactions.DefaultCommitmentFormat.commitWeight) - val missing = fees - 100.sat val fundingCreated = alice2bob.expectMsgType[FundingCreated] alice2bob.forward(bob) val error = bob2alice.expectMsgType[Error] - assert(error == Error(fundingCreated.temporaryChannelId, CannotAffordFirstCommitFees(fundingCreated.temporaryChannelId, missing, fees).getMessage)) + assert(error == Error(fundingCreated.temporaryChannelId, CannotAffordFirstCommitFees(fundingCreated.temporaryChannelId, 3370 sat, 3470 sat).getMessage)) awaitCond(bob.stateName == CLOSED) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingInternalStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingInternalStateSpec.scala index 2c6a39d236..dc831d481d 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingInternalStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingInternalStateSpec.scala @@ -17,10 +17,8 @@ package fr.acinq.eclair.channel.states.b import akka.actor.Status -import akka.actor.typed.scaladsl.adapter.ClassicActorRefOps import akka.testkit.{TestFSMRef, TestProbe} import fr.acinq.bitcoin.scalacompat.ByteVector32 -import fr.acinq.eclair.blockchain.NoOpOnChainWallet import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.channel.fsm.Channel.TickChannelOpenTimeout @@ -42,18 +40,14 @@ class WaitForFundingInternalStateSpec extends TestKitBaseClass with FixtureAnyFu case class FixtureParam(alice: TestFSMRef[ChannelState, ChannelData, Channel], aliceOpenReplyTo: TestProbe, alice2bob: TestProbe, bob2alice: TestProbe, alice2blockchain: TestProbe, listener: TestProbe) override def withFixture(test: OneArgTest): Outcome = { - val setup = init(wallet_opt = Some(new NoOpOnChainWallet()), tags = test.tags) + val setup = init(tags = test.tags) import setup._ - val channelConfig = ChannelConfig.standard - val channelFlags = ChannelFlags(announceChannel = false) - val (aliceParams, bobParams, channelType) = computeFeatures(setup, test.tags, channelFlags) - val aliceInit = Init(aliceParams.initFeatures) - val bobInit = Init(bobParams.initFeatures) + val channelParams = computeChannelParams(setup, test.tags) val listener = TestProbe() within(30 seconds) { alice.underlying.system.eventStream.subscribe(listener.ref, classOf[ChannelAborted]) - alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, TestConstants.fundingSatoshis, dualFunded = false, TestConstants.feeratePerKw, TestConstants.feeratePerKw, fundingTxFeeBudget_opt = None, Some(TestConstants.initiatorPushAmount), requireConfirmedInputs = false, requestFunding_opt = None, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType, replyTo = aliceOpenReplyTo.ref.toTyped) - bob ! INPUT_INIT_CHANNEL_NON_INITIATOR(ByteVector32.Zeroes, None, dualFunded = false, None, requireConfirmedInputs = false, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) + alice ! channelParams.initChannelAlice(TestConstants.fundingSatoshis, pushAmount_opt = Some(TestConstants.initiatorPushAmount)) + bob ! channelParams.initChannelBob() alice2bob.expectMsgType[OpenChannel] alice2bob.forward(bob) bob2alice.expectMsgType[AcceptChannel] @@ -84,7 +78,7 @@ class WaitForFundingInternalStateSpec extends TestKitBaseClass with FixtureAnyFu val sender = TestProbe() val c = CMD_CLOSE(sender.ref, None, None) alice ! c - sender.expectMsg(RES_SUCCESS(c, ByteVector32.Zeroes)) + assert(sender.expectMsgType[RES_SUCCESS[CMD_CLOSE]].cmd == c) listener.expectMsgType[ChannelAborted] awaitCond(alice.stateName == CLOSED) aliceOpenReplyTo.expectMsg(OpenChannelResponse.Cancelled) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingSignedStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingSignedStateSpec.scala index fd778546c9..08fc290bf5 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingSignedStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingSignedStateSpec.scala @@ -16,21 +16,22 @@ package fr.acinq.eclair.channel.states.b -import akka.actor.typed.scaladsl.adapter.ClassicActorRefOps import akka.testkit.{TestFSMRef, TestProbe} import fr.acinq.bitcoin.scalacompat.{Btc, ByteVector32, ByteVector64, SatoshiLong} import fr.acinq.eclair.TestConstants.{Alice, Bob} -import fr.acinq.eclair.blockchain.DummyOnChainWallet +import fr.acinq.eclair.TestUtils.randomTxId +import fr.acinq.eclair.blockchain.SingleKeyOnChainWallet import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ +import fr.acinq.eclair.channel.ChannelSpendSignature.PartialSignatureWithNonce import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.channel.fsm.Channel.TickChannelOpenTimeout import fr.acinq.eclair.channel.publish.TxPublisher import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags} +import fr.acinq.eclair.crypto.NonceGenerator import fr.acinq.eclair.io.Peer.OpenChannelResponse -import fr.acinq.eclair.transactions.Transactions -import fr.acinq.eclair.wire.protocol.{AcceptChannel, Error, FundingCreated, FundingSigned, Init, OpenChannel} -import fr.acinq.eclair.{TestConstants, TestKitBaseClass} +import fr.acinq.eclair.wire.protocol.{AcceptChannel, Error, FundingCreated, FundingSigned, OpenChannel} +import fr.acinq.eclair.{TestConstants, TestKitBaseClass, randomBytes32, randomKey} import org.scalatest.funsuite.FixtureAnyFunSuiteLike import org.scalatest.{Outcome, Tag} @@ -50,27 +51,18 @@ class WaitForFundingSignedStateSpec extends TestKitBaseClass with FixtureAnyFunS val setup = init(Alice.nodeParams, Bob.nodeParams, tags = test.tags) import setup._ - val (fundingSatoshis, pushMsat) = if (test.tags.contains(LargeChannel)) { + val channelParams = computeChannelParams(setup, test.tags) + val (fundingAmount, pushAmount) = if (test.tags.contains(LargeChannel)) { (Btc(5).toSatoshi, TestConstants.initiatorPushAmount) } else { (TestConstants.fundingSatoshis, TestConstants.initiatorPushAmount) } - - val channelConfig = ChannelConfig.standard - val channelFlags = ChannelFlags(announceChannel = false) - val (aliceParams, bobParams, channelType) = computeFeatures(setup, test.tags, channelFlags) - val commitFeerate = channelType.commitmentFormat match { - case Transactions.DefaultCommitmentFormat => TestConstants.feeratePerKw - case _: Transactions.AnchorOutputsCommitmentFormat => TestConstants.anchorOutputsFeeratePerKw - } - val aliceInit = Init(aliceParams.initFeatures) - val bobInit = Init(bobParams.initFeatures) val listener = TestProbe() within(30 seconds) { alice.underlying.system.eventStream.subscribe(listener.ref, classOf[ChannelAborted]) - alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, fundingSatoshis, dualFunded = false, commitFeerate, TestConstants.feeratePerKw, fundingTxFeeBudget_opt = None, Some(pushMsat), requireConfirmedInputs = false, requestFunding_opt = None, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType, replyTo = aliceOpenReplyTo.ref.toTyped) + alice ! channelParams.initChannelAlice(fundingAmount, pushAmount_opt = Some(pushAmount)) alice2blockchain.expectMsgType[TxPublisher.SetChannelId] - bob ! INPUT_INIT_CHANNEL_NON_INITIATOR(ByteVector32.Zeroes, None, dualFunded = false, None, requireConfirmedInputs = false, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) + bob ! channelParams.initChannelBob() bob2blockchain.expectMsgType[TxPublisher.SetChannelId] alice2bob.expectMsgType[OpenChannel] alice2bob.forward(bob) @@ -100,7 +92,7 @@ class WaitForFundingSignedStateSpec extends TestKitBaseClass with FixtureAnyFunS aliceOpenReplyTo.expectMsgType[OpenChannelResponse.Created] } - test("recv FundingSigned with valid signature (zero-conf)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), Tag(ChannelStateTestsTags.ZeroConf)) { f => + test("recv FundingSigned with valid signature (zero-conf)", Tag(ChannelStateTestsTags.ZeroConf)) { f => import f._ bob2alice.expectMsgType[FundingSigned] bob2alice.forward(alice) @@ -111,6 +103,23 @@ class WaitForFundingSignedStateSpec extends TestKitBaseClass with FixtureAnyFunS aliceOpenReplyTo.expectMsgType[OpenChannelResponse.Created] } + test("recv FundingSigned with valid signature (simple taproot channels)", Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => + import f._ + val listener = TestProbe() + alice.underlying.system.eventStream.subscribe(listener.ref, classOf[TransactionPublished]) + val fundingSigned = bob2alice.expectMsgType[FundingSigned] + assert(fundingSigned.sigOrPartialSig.isInstanceOf[PartialSignatureWithNonce]) + bob2alice.forward(alice, fundingSigned) + awaitCond(alice.stateName == WAIT_FOR_FUNDING_CONFIRMED) + val watchConfirmed = alice2blockchain.expectMsgType[WatchFundingConfirmed] + val fundingTxId = watchConfirmed.txId + assert(watchConfirmed.minDepth == 6) + val txPublished = listener.expectMsgType[TransactionPublished] + assert(txPublished.tx.txid == fundingTxId) + assert(txPublished.miningFee > 0.sat) + aliceOpenReplyTo.expectMsgType[OpenChannelResponse.Created] + } + test("recv FundingSigned with valid signature (wumbo)", Tag(LargeChannel)) { f => import f._ bob2alice.expectMsgType[FundingSigned] @@ -131,12 +140,23 @@ class WaitForFundingSignedStateSpec extends TestKitBaseClass with FixtureAnyFunS listener.expectMsgType[ChannelAborted] } + test("recv FundingSigned with invalid signature (simple taproot channels)", Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => + import f._ + // sending an invalid partial sig + alice ! FundingSigned(ByteVector32.Zeroes, PartialSignatureWithNonce(randomBytes32(), NonceGenerator.signingNonce(randomKey().publicKey, randomKey().publicKey, randomTxId()).publicNonce)) + awaitCond(alice.stateName == CLOSED) + alice2bob.expectMsgType[Error] + aliceOpenReplyTo.expectMsgType[OpenChannelResponse.Rejected] + listener.expectMsgType[ChannelAborted] + } + test("recv CMD_CLOSE") { f => import f._ val sender = TestProbe() + val channelId = alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_SIGNED].channelId val c = CMD_CLOSE(sender.ref, None, None) alice ! c - sender.expectMsg(RES_SUCCESS(c, alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_SIGNED].channelId)) + sender.expectMsg(RES_SUCCESS(c, channelId)) awaitCond(alice.stateName == CLOSED) aliceOpenReplyTo.expectMsg(OpenChannelResponse.Cancelled) listener.expectMsgType[ChannelAborted] @@ -154,10 +174,10 @@ class WaitForFundingSignedStateSpec extends TestKitBaseClass with FixtureAnyFunS test("recv INPUT_DISCONNECTED") { f => import f._ val fundingTx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_SIGNED].fundingTx - assert(alice.underlyingActor.wallet.asInstanceOf[DummyOnChainWallet].rolledback.isEmpty) + assert(alice.underlyingActor.wallet.asInstanceOf[SingleKeyOnChainWallet].rolledback.isEmpty) alice ! INPUT_DISCONNECTED awaitCond(alice.stateName == CLOSED) - assert(alice.underlyingActor.wallet.asInstanceOf[DummyOnChainWallet].rolledback.contains(fundingTx)) + assert(alice.underlyingActor.wallet.asInstanceOf[SingleKeyOnChainWallet].rolledback.contains(fundingTx)) aliceOpenReplyTo.expectMsg(OpenChannelResponse.Disconnected) listener.expectMsgType[ChannelAborted] } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForChannelReadyStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForChannelReadyStateSpec.scala index 7c7bf41f5a..51fbdd8840 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForChannelReadyStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForChannelReadyStateSpec.scala @@ -16,7 +16,6 @@ package fr.acinq.eclair.channel.states.c -import akka.actor.typed.scaladsl.adapter.ClassicActorRefOps import akka.testkit.{TestFSMRef, TestProbe} import com.softwaremill.quicklens.ModifyPimp import fr.acinq.bitcoin.scalacompat.{ByteVector32, Transaction} @@ -24,10 +23,11 @@ import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.channel.publish.TxPublisher +import fr.acinq.eclair.channel.states.ChannelStateTestsBase.PimpTestFSM import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags} import fr.acinq.eclair.payment.relay.Relayer.RelayFees import fr.acinq.eclair.router.Announcements -import fr.acinq.eclair.transactions.Transactions +import fr.acinq.eclair.testutils.PimpTestProbe.convert import fr.acinq.eclair.wire.protocol._ import fr.acinq.eclair.{BlockHeight, MilliSatoshiLong, TestConstants, TestKitBaseClass} import org.scalatest.funsuite.FixtureAnyFunSuiteLike @@ -48,26 +48,18 @@ class WaitForChannelReadyStateSpec extends TestKitBaseClass with FixtureAnyFunSu override def withFixture(test: OneArgTest): Outcome = { val setup = init(tags = test.tags) import setup._ - val channelConfig = ChannelConfig.standard val channelFlags = ChannelFlags(announceChannel = test.tags.contains(ChannelStateTestsTags.ChannelsPublic)) - val (aliceParams, bobParams, channelType) = computeFeatures(setup, test.tags, channelFlags) - val commitFeerate = channelType.commitmentFormat match { - case Transactions.DefaultCommitmentFormat => TestConstants.feeratePerKw - case _: Transactions.AnchorOutputsCommitmentFormat => TestConstants.anchorOutputsFeeratePerKw - } - val pushMsat = if (test.tags.contains(ChannelStateTestsTags.NoPushAmount)) None else Some(TestConstants.initiatorPushAmount) - val aliceInit = Init(aliceParams.initFeatures) - val bobInit = Init(bobParams.initFeatures) + val channelParams = computeChannelParams(setup, test.tags, channelFlags) + val pushAmount = if (test.tags.contains(ChannelStateTestsTags.NoPushAmount)) None else Some(TestConstants.initiatorPushAmount) val aliceListener = TestProbe() val bobListener = TestProbe() - within(30 seconds) { alice.underlying.system.eventStream.subscribe(aliceListener.ref, classOf[ChannelAborted]) bob.underlying.system.eventStream.subscribe(bobListener.ref, classOf[ChannelAborted]) - alice.underlyingActor.nodeParams.db.peers.addOrUpdateRelayFees(bobParams.nodeId, relayFees) - alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, TestConstants.fundingSatoshis, dualFunded = false, commitFeerate, TestConstants.feeratePerKw, fundingTxFeeBudget_opt = None, pushMsat, requireConfirmedInputs = false, requestFunding_opt = None, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType, replyTo = aliceOpenReplyTo.ref.toTyped) + alice.underlyingActor.nodeParams.db.peers.addOrUpdateRelayFees(channelParams.bobChannelParams.nodeId, relayFees) + alice ! channelParams.initChannelAlice(TestConstants.fundingSatoshis, pushAmount_opt = pushAmount, channelFlags = channelFlags) alice2blockchain.expectMsgType[TxPublisher.SetChannelId] - bob ! INPUT_INIT_CHANNEL_NON_INITIATOR(ByteVector32.Zeroes, None, dualFunded = false, None, requireConfirmedInputs = false, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) + bob ! channelParams.initChannelBob() bob2blockchain.expectMsgType[TxPublisher.SetChannelId] alice2bob.expectMsgType[OpenChannel] alice2bob.forward(bob) @@ -156,7 +148,7 @@ class WaitForChannelReadyStateSpec extends TestKitBaseClass with FixtureAnyFunSu awaitCond(alice.stateName == NORMAL) } - test("recv ChannelReady (zero-conf)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), Tag(ChannelStateTestsTags.ZeroConf)) { f => + test("recv ChannelReady (zero-conf)", Tag(ChannelStateTestsTags.ZeroConf)) { f => import f._ // zero-conf channel: we don't have a real scid val aliceIds = alice.stateData.asInstanceOf[DATA_WAIT_FOR_CHANNEL_READY].aliases @@ -181,7 +173,7 @@ class WaitForChannelReadyStateSpec extends TestKitBaseClass with FixtureAnyFunSu awaitCond(alice.stateName == NORMAL) } - test("recv ChannelReady (zero-conf, no alias)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), Tag(ChannelStateTestsTags.ZeroConf)) { f => + test("recv ChannelReady (zero-conf, no alias)", Tag(ChannelStateTestsTags.ZeroConf)) { f => import f._ // zero-conf channel: we don't have a real scid val aliceIds = alice.stateData.asInstanceOf[DATA_WAIT_FOR_CHANNEL_READY].aliases @@ -206,9 +198,9 @@ class WaitForChannelReadyStateSpec extends TestKitBaseClass with FixtureAnyFunSu import f._ // we have a real scid at this stage, because this isn't a zero-conf channel val aliceIds = alice.stateData.asInstanceOf[DATA_WAIT_FOR_CHANNEL_READY].aliases - assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_CHANNEL_READY].commitments.params.channelFlags.announceChannel) + assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_CHANNEL_READY].commitments.announceChannel) val bobIds = bob.stateData.asInstanceOf[DATA_WAIT_FOR_CHANNEL_READY].aliases - assert(bob.stateData.asInstanceOf[DATA_WAIT_FOR_CHANNEL_READY].commitments.params.channelFlags.announceChannel) + assert(bob.stateData.asInstanceOf[DATA_WAIT_FOR_CHANNEL_READY].commitments.announceChannel) val channelReady = bob2alice.expectMsgType[ChannelReady] assert(channelReady.alias_opt.contains(bobIds.localAlias)) bob2alice.forward(alice) @@ -227,7 +219,7 @@ class WaitForChannelReadyStateSpec extends TestKitBaseClass with FixtureAnyFunSu awaitCond(alice.stateName == NORMAL) } - test("recv ChannelReady (public, zero-conf)", Tag(ChannelStateTestsTags.ChannelsPublic), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), Tag(ChannelStateTestsTags.ZeroConf)) { f => + test("recv ChannelReady (public, zero-conf)", Tag(ChannelStateTestsTags.ChannelsPublic), Tag(ChannelStateTestsTags.ZeroConf)) { f => import f._ // zero-conf channel: we don't have a real scid val aliceIds = alice.stateData.asInstanceOf[DATA_WAIT_FOR_CHANNEL_READY].aliases @@ -249,10 +241,10 @@ class WaitForChannelReadyStateSpec extends TestKitBaseClass with FixtureAnyFunSu awaitCond(alice.stateName == NORMAL) } - test("recv WatchFundingSpentTriggered (remote commit)", Tag(ChannelStateTestsTags.StaticRemoteKey), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + test("recv WatchFundingSpentTriggered (remote commit)") { f => import f._ // bob publishes his commitment tx - val tx = bob.stateData.asInstanceOf[DATA_WAIT_FOR_CHANNEL_READY].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val tx = bob.signCommitTx() alice ! WatchFundingSpentTriggered(tx) alice2blockchain.expectMsgType[TxPublisher.PublishReplaceableTx] alice2blockchain.expectMsgType[TxPublisher.PublishTx] @@ -270,18 +262,16 @@ class WaitForChannelReadyStateSpec extends TestKitBaseClass with FixtureAnyFunSu test("recv Error") { f => import f._ - val tx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_CHANNEL_READY].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val tx = alice.signCommitTx() alice ! Error(ByteVector32.Zeroes, "oops") aliceListener.expectMsgType[ChannelAborted] awaitCond(alice.stateName == CLOSING) - assert(alice2blockchain.expectMsgType[TxPublisher.PublishFinalTx].tx.txid == tx.txid) - alice2blockchain.expectMsgType[TxPublisher.PublishTx] - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == tx.txid) + alice2blockchain.expectFinalTxPublished(tx.txid) } test("recv Error (nothing at stake)", Tag(ChannelStateTestsTags.NoPushAmount)) { f => import f._ - val tx = bob.stateData.asInstanceOf[DATA_WAIT_FOR_CHANNEL_READY].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val tx = bob.signCommitTx() bob ! Error(ByteVector32.Zeroes, "funding double-spent") bobListener.expectMsgType[ChannelAborted] awaitCond(bob.stateName == CLOSING) @@ -300,12 +290,11 @@ class WaitForChannelReadyStateSpec extends TestKitBaseClass with FixtureAnyFunSu test("recv CMD_FORCECLOSE") { f => import f._ val sender = TestProbe() - val tx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_CHANNEL_READY].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val tx = alice.signCommitTx() alice ! CMD_FORCECLOSE(sender.ref) aliceListener.expectMsgType[ChannelAborted] awaitCond(alice.stateName == CLOSING) - assert(alice2blockchain.expectMsgType[TxPublisher.PublishFinalTx].tx.txid == tx.txid) - alice2blockchain.expectMsgType[TxPublisher.PublishTx] - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == tx.txid) + alice2blockchain.expectFinalTxPublished(tx.txid) } + } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForDualFundingConfirmedStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForDualFundingConfirmedStateSpec.scala index bc671235d3..7566087c06 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForDualFundingConfirmedStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForDualFundingConfirmedStateSpec.scala @@ -16,9 +16,10 @@ package fr.acinq.eclair.channel.states.c -import akka.actor.typed.scaladsl.adapter.{ClassicActorRefOps, actorRefAdapter} +import akka.actor.typed.scaladsl.adapter.actorRefAdapter import akka.testkit.{TestFSMRef, TestProbe} import com.softwaremill.quicklens.{ModifyPimp, QuicklensAt} +import fr.acinq.bitcoin.ScriptFlags import fr.acinq.bitcoin.scalacompat.{ByteVector32, SatoshiLong, Transaction} import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ import fr.acinq.eclair.blockchain.fee.FeeratePerKw @@ -29,11 +30,11 @@ import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.channel.fsm.Channel.ProcessCurrentBlockHeight import fr.acinq.eclair.channel.fund.InteractiveTxBuilder.FullySignedSharedTransaction import fr.acinq.eclair.channel.publish.TxPublisher -import fr.acinq.eclair.channel.publish.TxPublisher.{PublishFinalTx, SetChannelId} -import fr.acinq.eclair.channel.states.ChannelStateTestsBase.FakeTxPublisherFactory +import fr.acinq.eclair.channel.publish.TxPublisher.SetChannelId +import fr.acinq.eclair.channel.states.ChannelStateTestsBase.{FakeTxPublisherFactory, PimpTestFSM} import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags} -import fr.acinq.eclair.transactions.Transactions -import fr.acinq.eclair.transactions.Transactions.ClaimLocalAnchorOutputTx +import fr.acinq.eclair.testutils.PimpTestProbe.convert +import fr.acinq.eclair.transactions.Transactions._ import fr.acinq.eclair.wire.protocol._ import fr.acinq.eclair.{BlockHeight, MilliSatoshiLong, TestConstants, TestKitBaseClass, ToMilliSatoshiConversion} import org.scalatest.funsuite.FixtureAnyFunSuiteLike @@ -47,11 +48,12 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture val noFundingContribution = "no_funding_contribution" val liquidityPurchase = "liquidity_purchase" - case class FixtureParam(alice: TestFSMRef[ChannelState, ChannelData, Channel], bob: TestFSMRef[ChannelState, ChannelData, Channel], alice2bob: TestProbe, bob2alice: TestProbe, alice2blockchain: TestProbe, bob2blockchain: TestProbe, aliceListener: TestProbe, bobListener: TestProbe, wallet: SingleKeyOnChainWallet) + case class FixtureParam(alice: TestFSMRef[ChannelState, ChannelData, Channel], bob: TestFSMRef[ChannelState, ChannelData, Channel], alice2bob: TestProbe, bob2alice: TestProbe, alice2blockchain: TestProbe, bob2blockchain: TestProbe, aliceListener: TestProbe, bobListener: TestProbe, aliceWallet: SingleKeyOnChainWallet, bobWallet: SingleKeyOnChainWallet) override def withFixture(test: OneArgTest): Outcome = { - val wallet = new SingleKeyOnChainWallet() - val setup = init(wallet_opt = Some(wallet), tags = test.tags) + val aliceWallet = new SingleKeyOnChainWallet() + val bobWallet = new SingleKeyOnChainWallet() + val setup = init(walletA_opt = Some(aliceWallet), walletB_opt = Some(bobWallet), tags = test.tags) import setup._ val aliceListener = TestProbe() @@ -67,15 +69,7 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture bob.underlying.system.eventStream.subscribe(bobListener.ref, classOf[ChannelAborted]) bob.underlying.system.eventStream.subscribe(bobListener.ref, classOf[ChannelClosed]) - val channelConfig = ChannelConfig.standard - val channelFlags = ChannelFlags(announceChannel = false) - val (aliceParams, bobParams, channelType) = computeFeatures(setup, test.tags, channelFlags) - val commitFeerate = channelType.commitmentFormat match { - case Transactions.DefaultCommitmentFormat => TestConstants.feeratePerKw - case _: Transactions.AnchorOutputsCommitmentFormat => TestConstants.anchorOutputsFeeratePerKw - } - val aliceInit = Init(aliceParams.initFeatures) - val bobInit = Init(bobParams.initFeatures) + val channelParams = computeChannelParams(setup, test.tags) val (requestFunding_opt, bobContribution) = if (test.tags.contains(noFundingContribution)) { (None, None) } else if (test.tags.contains(liquidityPurchase)) { @@ -88,8 +82,8 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture } val (initiatorPushAmount, nonInitiatorPushAmount) = if (test.tags.contains(bothPushAmount)) (Some(TestConstants.initiatorPushAmount), Some(TestConstants.nonInitiatorPushAmount)) else (None, None) within(30 seconds) { - alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, TestConstants.fundingSatoshis, dualFunded = true, commitFeerate, TestConstants.feeratePerKw, fundingTxFeeBudget_opt = None, initiatorPushAmount, requireConfirmedInputs = false, requestFunding_opt, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType, replyTo = aliceOpenReplyTo.ref.toTyped) - bob ! INPUT_INIT_CHANNEL_NON_INITIATOR(ByteVector32.Zeroes, bobContribution, dualFunded = true, nonInitiatorPushAmount, requireConfirmedInputs = false, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) + alice ! channelParams.initChannelAlice(TestConstants.fundingSatoshis, dualFunded = true, pushAmount_opt = initiatorPushAmount, requestFunding_opt = requestFunding_opt) + bob ! channelParams.initChannelBob(bobContribution, dualFunded = true, pushAmount_opt = nonInitiatorPushAmount) alice2blockchain.expectMsgType[SetChannelId] // temporary channel id bob2blockchain.expectMsgType[SetChannelId] // temporary channel id alice2bob.expectMsgType[OpenDualFundedChannel] @@ -150,7 +144,7 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture val expectedBalanceBob = bobContribution.map(_.fundingAmount).getOrElse(0 sat) + liquidityFees.total + initiatorPushAmount.getOrElse(0 msat) - nonInitiatorPushAmount.getOrElse(0 msat) - bobReserve assert(bob.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].commitments.availableBalanceForSend == expectedBalanceBob) } - withFixture(test.toNoArgTest(FixtureParam(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain, aliceListener, bobListener, wallet))) + withFixture(test.toNoArgTest(FixtureParam(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain, aliceListener, bobListener, aliceWallet, bobWallet))) } } @@ -188,7 +182,7 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture assert(bob.stateName == WAIT_FOR_DUAL_FUNDING_CONFIRMED) } - test("recv WatchPublishedTriggered (initiator)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.ZeroConf), Tag(ChannelStateTestsTags.ScidAlias), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + test("recv WatchPublishedTriggered (initiator)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.ZeroConf), Tag(ChannelStateTestsTags.ScidAlias)) { f => import f._ val fundingTx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].latestFundingTx.sharedTx.asInstanceOf[FullySignedSharedTransaction].signedTx alice ! WatchPublishedTriggered(fundingTx) @@ -199,7 +193,7 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture awaitCond(alice.stateName == WAIT_FOR_DUAL_FUNDING_READY) } - test("recv WatchPublishedTriggered (non-initiator)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.ZeroConf), Tag(ChannelStateTestsTags.ScidAlias), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + test("recv WatchPublishedTriggered (non-initiator)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.ZeroConf), Tag(ChannelStateTestsTags.ScidAlias)) { f => import f._ val fundingTx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].latestFundingTx.sharedTx.asInstanceOf[FullySignedSharedTransaction].signedTx bob ! WatchPublishedTriggered(fundingTx) @@ -210,7 +204,7 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture awaitCond(bob.stateName == WAIT_FOR_DUAL_FUNDING_READY) } - test("recv WatchPublishedTriggered (offline)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.ZeroConf), Tag(ChannelStateTestsTags.ScidAlias), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + test("recv WatchPublishedTriggered (offline)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.ZeroConf), Tag(ChannelStateTestsTags.ScidAlias)) { f => import f._ val fundingTx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].latestFundingTx.sharedTx.asInstanceOf[FullySignedSharedTransaction].signedTx alice ! INPUT_DISCONNECTED @@ -332,7 +326,7 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture assert(alice2.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_READY].commitments.active.size == 1) assert(alice2.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_READY].commitments.inactive.isEmpty) assert(alice2.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_READY].commitments.latest.fundingTxId == fundingTx1.signedTx.txid) - testUnusedInputsUnlocked(wallet, Seq(fundingTx2)) + testUnusedInputsUnlocked(aliceWallet, Seq(fundingTx2)) bob2 ! WatchFundingConfirmedTriggered(BlockHeight(42000), 42, fundingTx1.signedTx) assert(bobListener.expectMsgType[TransactionConfirmed].tx == fundingTx1.signedTx) @@ -345,49 +339,61 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture } def testBumpFundingFees(f: FixtureParam, feerate_opt: Option[FeeratePerKw] = None, requestFunding_opt: Option[LiquidityAds.RequestFunding] = None): FullySignedSharedTransaction = { + testBumpFundingFees(f, f.alice, f.bob, f.alice2bob, f.bob2alice, feerate_opt, requestFunding_opt) + } + + def testBumpFundingFees(f: FixtureParam, s: TestFSMRef[ChannelState, ChannelData, Channel], r: TestFSMRef[ChannelState, ChannelData, Channel], s2r: TestProbe, r2s: TestProbe, feerate_opt: Option[FeeratePerKw], requestFunding_opt: Option[LiquidityAds.RequestFunding]): FullySignedSharedTransaction = { import f._ val probe = TestProbe() - val currentFundingTx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].latestFundingTx.sharedTx.asInstanceOf[FullySignedSharedTransaction] - val previousFundingTxs = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].previousFundingTxs - alice ! CMD_BUMP_FUNDING_FEE(probe.ref, feerate_opt.getOrElse(currentFundingTx.feerate * 1.1), fundingFeeBudget = 100_000.sat, 0, requestFunding_opt) - assert(alice2bob.expectMsgType[TxInitRbf].fundingContribution == TestConstants.fundingSatoshis) - alice2bob.forward(bob) - val txAckRbf = bob2alice.expectMsgType[TxAckRbf] - assert(txAckRbf.fundingContribution == requestFunding_opt.map(_.requestedAmount).getOrElse(TestConstants.nonInitiatorFundingSatoshis)) + val currentFundingParams = s.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].latestFundingTx.fundingParams + val currentFundingTx = s.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].latestFundingTx.sharedTx.asInstanceOf[FullySignedSharedTransaction] + val previousFundingTxs = s.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].previousFundingTxs + s ! CMD_BUMP_FUNDING_FEE(probe.ref, feerate_opt.getOrElse(currentFundingTx.feerate * 1.1), fundingFeeBudget = 100_000.sat, 0, requestFunding_opt) + assert(s2r.expectMsgType[TxInitRbf].fundingContribution == currentFundingParams.localContribution) + s2r.forward(r) + val txAckRbf = r2s.expectMsgType[TxAckRbf] + assert(txAckRbf.fundingContribution == requestFunding_opt.map(_.requestedAmount).getOrElse(currentFundingParams.remoteContribution)) requestFunding_opt.foreach(_ => assert(txAckRbf.willFund_opt.nonEmpty)) - bob2alice.forward(alice) + r2s.forward(s) // Alice and Bob build a new version of the funding transaction, with one new input every time. val inputCount = previousFundingTxs.length + 2 (1 to inputCount).foreach(_ => { - alice2bob.expectMsgType[TxAddInput] - alice2bob.forward(bob) - bob2alice.expectMsgType[TxAddInput] - bob2alice.forward(alice) + s2r.expectMsgType[TxAddInput] + s2r.forward(r) + r2s.expectMsgType[TxAddInput] + r2s.forward(s) }) - alice2bob.expectMsgType[TxAddOutput] - alice2bob.forward(bob) - bob2alice.expectMsgType[TxAddOutput] - bob2alice.forward(alice) - alice2bob.expectMsgType[TxAddOutput] - alice2bob.forward(bob) - bob2alice.expectMsgType[TxComplete] - bob2alice.forward(alice) - alice2bob.expectMsgType[TxComplete] - alice2bob.forward(bob) - bob2alice.expectMsgType[CommitSig] - bob2alice.forward(alice) - alice2bob.expectMsgType[CommitSig] - alice2bob.forward(bob) - bob2alice.expectMsgType[TxSignatures] - bob2alice.forward(alice) - alice2bob.expectMsgType[TxSignatures] - alice2bob.forward(bob) + s2r.expectMsgType[TxAddOutput] + s2r.forward(r) + r2s.expectMsgType[TxAddOutput] + r2s.forward(s) + s2r.expectMsgType[TxAddOutput] + s2r.forward(r) + r2s.expectMsgType[TxComplete] + r2s.forward(s) + s2r.expectMsgType[TxComplete] + s2r.forward(r) + r2s.expectMsgType[CommitSig] + r2s.forward(s) + s2r.expectMsgType[CommitSig] + s2r.forward(r) + if (currentFundingParams.localContribution < currentFundingParams.remoteContribution) { + s2r.expectMsgType[TxSignatures] + s2r.forward(r) + r2s.expectMsgType[TxSignatures] + r2s.forward(s) + } else { + r2s.expectMsgType[TxSignatures] + r2s.forward(s) + s2r.expectMsgType[TxSignatures] + s2r.forward(r) + } probe.expectMsgType[RES_BUMP_FUNDING_FEE] - val nextFundingTx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].latestFundingTx.sharedTx.asInstanceOf[FullySignedSharedTransaction] + val nextFundingTx = s.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].latestFundingTx.sharedTx.asInstanceOf[FullySignedSharedTransaction] assert(aliceListener.expectMsgType[TransactionPublished].tx.txid == nextFundingTx.signedTx.txid) assert(alice2blockchain.expectMsgType[WatchFundingConfirmed].txId == nextFundingTx.signedTx.txid) assert(bobListener.expectMsgType[TransactionPublished].tx.txid == nextFundingTx.signedTx.txid) @@ -396,12 +402,12 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture assert(currentFundingTx.feerate < nextFundingTx.feerate) // The new transaction double-spends previous inputs. currentFundingTx.signedTx.txIn.map(_.outPoint).foreach(o => assert(nextFundingTx.signedTx.txIn.exists(_.outPoint == o))) - assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].previousFundingTxs.length == previousFundingTxs.length + 1) - assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].previousFundingTxs.head.sharedTx == currentFundingTx) + assert(s.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].previousFundingTxs.length == previousFundingTxs.length + 1) + assert(s.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].previousFundingTxs.head.sharedTx == currentFundingTx) nextFundingTx } - test("recv CMD_BUMP_FUNDING_FEE", Tag(ChannelStateTestsTags.DualFunding)) { f => + test("recv CMD_BUMP_FUNDING_FEE", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.UnlimitedRbfAttempts)) { f => import f._ // Bob contributed to the funding transaction. @@ -427,9 +433,21 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture assert(FeeratePerKw(15_000 sat) <= fundingTx3.feerate && fundingTx3.feerate < FeeratePerKw(15_700 sat)) assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].previousFundingTxs.length == 2) - // The initial funding transaction confirms + // Bob RBFs the funding transaction: Alice keeps contributing the same amount. + val feerate4 = FeeratePerKw(20_000 sat) + testBumpFundingFees(f, bob, alice, bob2alice, alice2bob, Some(feerate4), requestFunding_opt = None) + val balanceBob4 = bob.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].commitments.latest.localCommit.spec.toLocal + assert(balanceBob4 == TestConstants.nonInitiatorFundingSatoshis.toMilliSatoshi) + val balanceAlice4 = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].commitments.latest.localCommit.spec.toLocal + assert(balanceAlice4 == TestConstants.fundingSatoshis.toMilliSatoshi) + val fundingTx4 = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].commitments.latest.localFundingStatus.asInstanceOf[DualFundedUnconfirmedFundingTx].sharedTx.asInstanceOf[FullySignedSharedTransaction] + assert(FeeratePerKw(20_000 sat) <= fundingTx4.feerate && fundingTx4.feerate < FeeratePerKw(20_500 sat)) + assert(bob.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].previousFundingTxs.length == 3) + assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].previousFundingTxs.length == 3) + + // The initial funding transaction confirms: we rollback unused inputs. alice ! WatchFundingConfirmedTriggered(BlockHeight(42000), 42, fundingTx1.signedTx) - testUnusedInputsUnlocked(wallet, Seq(fundingTx2, fundingTx3)) + testUnusedInputsUnlocked(aliceWallet, Seq(fundingTx2, fundingTx3, fundingTx4)) } test("recv CMD_BUMP_FUNDING_FEE (liquidity ads)", Tag(ChannelStateTestsTags.DualFunding), Tag(liquidityPurchase)) { f => @@ -485,6 +503,10 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture bob2alice.forward(alice) alice2bob.expectMsgType[TxAbort] alice2bob.forward(bob) + + // Bob tries to RBF: this is disabled because it would override Alice's liquidity purchase. + bob ! CMD_BUMP_FUNDING_FEE(sender.ref, FeeratePerKw(20_000 sat), 100_000 sat, 0, requestFunding_opt = None) + assert(sender.expectMsgType[RES_FAILURE[_, ChannelException]].t.isInstanceOf[InvalidRbfOverridesLiquidityPurchase]) } test("recv CMD_BUMP_FUNDING_FEE (aborted)", Tag(ChannelStateTestsTags.DualFunding)) { f => @@ -616,11 +638,11 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture import f._ val fundingTx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].latestFundingTx.sharedTx.asInstanceOf[FullySignedSharedTransaction].signedTx val currentBlock = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].waitingSince + 10 - wallet.doubleSpent = Set(fundingTx.txid) + aliceWallet.doubleSpent = Set(fundingTx.txid) alice ! ProcessCurrentBlockHeight(CurrentBlockHeight(currentBlock)) alice2bob.expectMsgType[Error] alice2blockchain.expectNoMessage(100 millis) - awaitCond(wallet.rolledback.map(_.txid) == Seq(fundingTx.txid)) + awaitCond(aliceWallet.rolledback.map(_.txid) == Seq(fundingTx.txid)) aliceListener.expectMsgType[ChannelAborted] awaitCond(alice.stateName == CLOSED) } @@ -631,11 +653,11 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture val currentBlock = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].waitingSince + 10 alice ! INPUT_DISCONNECTED awaitCond(alice.stateName == OFFLINE) - wallet.doubleSpent = Set(fundingTx.txid) + aliceWallet.doubleSpent = Set(fundingTx.txid) alice ! ProcessCurrentBlockHeight(CurrentBlockHeight(currentBlock)) alice2bob.expectMsgType[Error] alice2blockchain.expectNoMessage(100 millis) - awaitCond(wallet.rolledback.map(_.txid) == Seq(fundingTx.txid)) + awaitCond(aliceWallet.rolledback.map(_.txid) == Seq(fundingTx.txid)) aliceListener.expectMsgType[ChannelAborted] awaitCond(alice.stateName == CLOSED) } @@ -733,7 +755,7 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture awaitCond(alice.stateName == WAIT_FOR_DUAL_FUNDING_READY) } - test("recv WatchFundingSpentTriggered while offline (remote commit)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.StaticRemoteKey), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + test("recv WatchFundingSpentTriggered while offline (remote commit)", Tag(ChannelStateTestsTags.DualFunding)) { f => import f._ val fundingTx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].latestFundingTx.sharedTx.asInstanceOf[FullySignedSharedTransaction].signedTx alice ! INPUT_DISCONNECTED @@ -748,26 +770,26 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture awaitCond(alice.stateData.isInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_READY]) assert(alice.stateName == OFFLINE) // Bob broadcasts his commit tx. - val bobCommitTx = bob.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx - alice ! WatchFundingSpentTriggered(bobCommitTx.tx) + val bobCommitTx = bob.signCommitTx() + alice ! WatchFundingSpentTriggered(bobCommitTx) aliceListener.expectMsgType[TransactionPublished] - assert(alice2blockchain.expectMsgType[TxPublisher.PublishReplaceableTx].txInfo.isInstanceOf[ClaimLocalAnchorOutputTx]) - val claimMain = alice2blockchain.expectMsgType[TxPublisher.PublishFinalTx] - assert(claimMain.input.txid == bobCommitTx.tx.txid) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == bobCommitTx.tx.txid) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == claimMain.tx.txid) + alice2blockchain.expectReplaceableTxPublished[ClaimRemoteAnchorTx] + val claimMain = alice2blockchain.expectFinalTxPublished("remote-main-delayed") + Transaction.correctlySpends(claimMain.tx, Seq(bobCommitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + alice2blockchain.expectWatchTxConfirmed(bobCommitTx.txid) + alice2blockchain.expectWatchOutputSpent(claimMain.input) aliceListener.expectMsgType[ChannelAborted] awaitCond(alice.stateName == CLOSING) } - test("recv WatchFundingSpentTriggered while offline (previous tx)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.StaticRemoteKey), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + test("recv WatchFundingSpentTriggered while offline (previous tx)", Tag(ChannelStateTestsTags.DualFunding)) { f => import f._ val fundingTx1 = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].latestFundingTx.sharedTx.asInstanceOf[FullySignedSharedTransaction].signedTx - val bobCommitTx1 = bob.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val bobCommitTx1 = bob.signCommitTx() val fundingTx2 = testBumpFundingFees(f) assert(fundingTx1.txid != fundingTx2.signedTx.txid) - val bobCommitTx2 = bob.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val bobCommitTx2 = bob.signCommitTx() assert(bobCommitTx1.txid != bobCommitTx2.txid) alice ! INPUT_DISCONNECTED @@ -782,60 +804,62 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture alice2bob.expectNoMessage(100 millis) awaitCond(alice.stateData.isInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_READY]) assert(alice.stateName == OFFLINE) - testUnusedInputsUnlocked(wallet, Seq(fundingTx2)) + testUnusedInputsUnlocked(aliceWallet, Seq(fundingTx2)) // Bob broadcasts his commit tx. alice ! WatchFundingSpentTriggered(bobCommitTx1) assert(aliceListener.expectMsgType[TransactionPublished].tx.txid == bobCommitTx1.txid) - assert(alice2blockchain.expectMsgType[TxPublisher.PublishReplaceableTx].txInfo.isInstanceOf[ClaimLocalAnchorOutputTx]) - val claimMain = alice2blockchain.expectMsgType[TxPublisher.PublishFinalTx] - assert(claimMain.input.txid == bobCommitTx1.txid) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == bobCommitTx1.txid) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == claimMain.tx.txid) + alice2blockchain.expectReplaceableTxPublished[ClaimRemoteAnchorTx] + val claimMain = alice2blockchain.expectFinalTxPublished("remote-main-delayed") + Transaction.correctlySpends(claimMain.tx, Seq(bobCommitTx1), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + alice2blockchain.expectWatchTxConfirmed(bobCommitTx1.txid) + alice2blockchain.expectWatchOutputSpent(claimMain.input) aliceListener.expectMsgType[ChannelAborted] awaitCond(alice.stateName == CLOSING) } - test("recv WatchFundingSpentTriggered after restart (remote commit)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.StaticRemoteKey), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + test("recv WatchFundingSpentTriggered after restart (remote commit)", Tag(ChannelStateTestsTags.DualFunding)) { f => import f._ val aliceData = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED] val bobData = bob.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED] val fundingTx = aliceData.latestFundingTx.sharedTx.asInstanceOf[FullySignedSharedTransaction].signedTx + val aliceCommitTx = alice.signCommitTx() + val bobCommitTx = bob.signCommitTx() val (alice2, bob2) = restartNodes(f, aliceData, bobData) alice2 ! WatchFundingConfirmedTriggered(BlockHeight(42000), 42, fundingTx) assert(aliceListener.expectMsgType[TransactionConfirmed].tx == fundingTx) assert(alice2blockchain.expectMsgType[WatchFundingSpent].txId == fundingTx.txid) - alice2 ! WatchFundingSpentTriggered(bobData.commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx) - assert(alice2blockchain.expectMsgType[TxPublisher.PublishReplaceableTx].txInfo.isInstanceOf[ClaimLocalAnchorOutputTx]) - val claimMainAlice = alice2blockchain.expectMsgType[TxPublisher.PublishFinalTx] - assert(claimMainAlice.input.txid == bobData.commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx.txid) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == bobData.commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx.txid) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == claimMainAlice.tx.txid) + alice2 ! WatchFundingSpentTriggered(bobCommitTx) + alice2blockchain.expectReplaceableTxPublished[ClaimRemoteAnchorTx] + val claimMainAlice = alice2blockchain.expectFinalTxPublished("remote-main-delayed") + Transaction.correctlySpends(claimMainAlice.tx, Seq(bobCommitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + alice2blockchain.expectWatchTxConfirmed(bobCommitTx.txid) + alice2blockchain.expectWatchOutputSpent(claimMainAlice.input) awaitCond(alice2.stateName == CLOSING) bob2 ! WatchFundingConfirmedTriggered(BlockHeight(42000), 42, fundingTx) assert(bobListener.expectMsgType[TransactionConfirmed].tx == fundingTx) assert(bob2blockchain.expectMsgType[WatchFundingSpent].txId == fundingTx.txid) - bob2 ! WatchFundingSpentTriggered(aliceData.commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx) - assert(bob2blockchain.expectMsgType[TxPublisher.PublishReplaceableTx].txInfo.isInstanceOf[ClaimLocalAnchorOutputTx]) - val claimMainBob = bob2blockchain.expectMsgType[TxPublisher.PublishFinalTx] - assert(claimMainBob.input.txid == aliceData.commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx.txid) - assert(bob2blockchain.expectMsgType[WatchTxConfirmed].txId == aliceData.commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx.txid) - assert(bob2blockchain.expectMsgType[WatchTxConfirmed].txId == claimMainBob.tx.txid) + bob2 ! WatchFundingSpentTriggered(aliceCommitTx) + bob2blockchain.expectReplaceableTxPublished[ClaimRemoteAnchorTx] + val claimMainBob = bob2blockchain.expectFinalTxPublished("remote-main-delayed") + Transaction.correctlySpends(claimMainBob.tx, Seq(aliceCommitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + bob2blockchain.expectWatchTxConfirmed(aliceCommitTx.txid) + bob2blockchain.expectWatchOutputSpent(claimMainBob.input) awaitCond(bob2.stateName == CLOSING) } - test("recv WatchFundingSpentTriggered after restart (previous tx)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.StaticRemoteKey), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + test("recv WatchFundingSpentTriggered after restart (previous tx)", Tag(ChannelStateTestsTags.DualFunding)) { f => import f._ val fundingTx1 = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].latestFundingTx.sharedTx.asInstanceOf[FullySignedSharedTransaction].signedTx - val aliceCommitTx1 = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx - val bobCommitTx1 = bob.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val aliceCommitTx1 = alice.signCommitTx() + val bobCommitTx1 = bob.signCommitTx() val fundingTx2 = testBumpFundingFees(f) assert(fundingTx1.txid != fundingTx2.signedTx.txid) - val bobCommitTx2 = bob.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val bobCommitTx2 = bob.signCommitTx() assert(bobCommitTx1.txid != bobCommitTx2.txid) val aliceData = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED] @@ -847,24 +871,24 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture assert(alice2blockchain.expectMsgType[WatchFundingSpent].txId == fundingTx1.txid) alice2blockchain.expectMsg(UnwatchTxConfirmed(fundingTx2.txId)) alice2 ! WatchFundingSpentTriggered(bobCommitTx1) - assert(alice2blockchain.expectMsgType[TxPublisher.PublishReplaceableTx].txInfo.isInstanceOf[ClaimLocalAnchorOutputTx]) - val claimMainAlice = alice2blockchain.expectMsgType[TxPublisher.PublishFinalTx] - assert(claimMainAlice.input.txid == bobCommitTx1.txid) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == bobCommitTx1.txid) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == claimMainAlice.tx.txid) + alice2blockchain.expectReplaceableTxPublished[ClaimRemoteAnchorTx] + val claimMainAlice = alice2blockchain.expectFinalTxPublished("remote-main-delayed") + Transaction.correctlySpends(claimMainAlice.tx, Seq(bobCommitTx1), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + alice2blockchain.expectWatchTxConfirmed(bobCommitTx1.txid) + alice2blockchain.expectWatchOutputSpent(claimMainAlice.input) awaitCond(alice2.stateName == CLOSING) - testUnusedInputsUnlocked(wallet, Seq(fundingTx2)) + testUnusedInputsUnlocked(aliceWallet, Seq(fundingTx2)) bob2 ! WatchFundingConfirmedTriggered(BlockHeight(42000), 42, fundingTx1) assert(bobListener.expectMsgType[TransactionConfirmed].tx == fundingTx1) assert(bob2blockchain.expectMsgType[WatchFundingSpent].txId == fundingTx1.txid) bob2blockchain.expectMsg(UnwatchTxConfirmed(fundingTx2.txId)) bob2 ! WatchFundingSpentTriggered(aliceCommitTx1) - assert(bob2blockchain.expectMsgType[TxPublisher.PublishReplaceableTx].txInfo.isInstanceOf[ClaimLocalAnchorOutputTx]) - val claimMainBob = bob2blockchain.expectMsgType[TxPublisher.PublishFinalTx] - assert(claimMainBob.input.txid == aliceCommitTx1.txid) - assert(bob2blockchain.expectMsgType[WatchTxConfirmed].txId == aliceCommitTx1.txid) - assert(bob2blockchain.expectMsgType[WatchTxConfirmed].txId == claimMainBob.tx.txid) + bob2blockchain.expectReplaceableTxPublished[ClaimRemoteAnchorTx] + val claimMainBob = bob2blockchain.expectFinalTxPublished("remote-main-delayed") + Transaction.correctlySpends(claimMainBob.tx, Seq(aliceCommitTx1), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + bob2blockchain.expectWatchTxConfirmed(aliceCommitTx1.txid) + bob2blockchain.expectWatchOutputSpent(claimMainBob.input) awaitCond(bob2.stateName == CLOSING) } @@ -875,7 +899,7 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture assert(alice.stateName == WAIT_FOR_DUAL_FUNDING_CONFIRMED) } - private def initiateRbf(f: FixtureParam): Unit = { + private def initiateRbf(f: FixtureParam): TxComplete = { import f._ alice ! CMD_BUMP_FUNDING_FEE(TestProbe().ref, TestConstants.feeratePerKw * 1.1, fundingFeeBudget = 100_000.sat, 0, None) @@ -897,8 +921,9 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture bob2alice.forward(alice) alice2bob.expectMsgType[TxAddOutput] alice2bob.forward(bob) - bob2alice.expectMsgType[TxComplete] + val txCompleteBob = bob2alice.expectMsgType[TxComplete] bob2alice.forward(alice) + txCompleteBob } private def reconnectRbf(f: FixtureParam): (ChannelReestablish, ChannelReestablish) = { @@ -920,7 +945,7 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture (channelReestablishAlice, channelReestablishBob) } - test("recv INPUT_DISCONNECTED (unsigned rbf attempt)", Tag(ChannelStateTestsTags.DualFunding)) { f => + def testDisconnectUnsignedRbfAttempt(f: FixtureParam, commitmentFormat: CommitmentFormat): Unit = { import f._ initiateRbf(f) @@ -928,14 +953,24 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture alice2bob.expectMsgType[CommitSig] // bob doesn't receive alice's commit_sig awaitCond(alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].status.isInstanceOf[DualFundingStatus.RbfWaitingForSigs]) + val fundingTxId = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].latestFundingTx.sharedTx.txId val rbfTxId = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].status.asInstanceOf[DualFundingStatus.RbfWaitingForSigs].signingSession.fundingTx.txId assert(bob.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].status.isInstanceOf[DualFundingStatus.RbfInProgress]) + assert(fundingTxId != rbfTxId) val (channelReestablishAlice, channelReestablishBob) = reconnectRbf(f) assert(channelReestablishAlice.nextFundingTxId_opt.contains(rbfTxId)) assert(channelReestablishAlice.nextLocalCommitmentNumber == 0) assert(channelReestablishBob.nextFundingTxId_opt.isEmpty) assert(channelReestablishBob.nextLocalCommitmentNumber == 1) + commitmentFormat match { + case _: SegwitV0CommitmentFormat => () + case _: TaprootCommitmentFormat => + assert(channelReestablishAlice.currentCommitNonce_opt.nonEmpty) + assert(channelReestablishBob.currentCommitNonce_opt.isEmpty) + assert(channelReestablishAlice.nextCommitNonces.contains(fundingTxId)) + assert(channelReestablishBob.nextCommitNonces.contains(fundingTxId)) + } // Bob detects that Alice stored an old RBF attempt and tells her to abort. bob2alice.expectMsgType[TxAbort] @@ -948,24 +983,48 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture bob2alice.expectNoMessage(100 millis) } - test("recv INPUT_DISCONNECTED (rbf commit_sig received by Alice)", Tag(ChannelStateTestsTags.DualFunding)) { f => + test("recv INPUT_DISCONNECTED (unsigned rbf attempt)", Tag(ChannelStateTestsTags.DualFunding)) { f => + testDisconnectUnsignedRbfAttempt(f, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) + } + + test("recv INPUT_DISCONNECTED (unsigned rbf attempt, taproot)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => + testDisconnectUnsignedRbfAttempt(f, ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat) + } + + def testDisconnectRbfCommitSigReceivedAlice(f: FixtureParam, commitmentFormat: CommitmentFormat): Unit = { import f._ - initiateRbf(f) - alice2bob.expectMsgType[TxComplete] + val txCompleteBob = initiateRbf(f) + val txCompleteAlice = alice2bob.expectMsgType[TxComplete] alice2bob.forward(bob) bob2alice.expectMsgType[CommitSig] bob2alice.forward(alice) alice2bob.expectMsgType[CommitSig] // Bob doesn't receive Alice's commit_sig awaitCond(alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].status.isInstanceOf[DualFundingStatus.RbfWaitingForSigs]) awaitCond(bob.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].status.isInstanceOf[DualFundingStatus.RbfWaitingForSigs]) + val fundingTxId = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].latestFundingTx.sharedTx.txId val rbfTxId = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].status.asInstanceOf[DualFundingStatus.RbfWaitingForSigs].signingSession.fundingTx.txId + assert(fundingTxId != rbfTxId) val (channelReestablishAlice, channelReestablishBob) = reconnectRbf(f) assert(channelReestablishAlice.nextFundingTxId_opt.contains(rbfTxId)) assert(channelReestablishAlice.nextLocalCommitmentNumber == 1) assert(channelReestablishBob.nextFundingTxId_opt.contains(rbfTxId)) assert(channelReestablishBob.nextLocalCommitmentNumber == 0) + commitmentFormat match { + case _: SegwitV0CommitmentFormat => () + case _: TaprootCommitmentFormat => + assert(channelReestablishAlice.currentCommitNonce_opt.isEmpty) + assert(channelReestablishBob.currentCommitNonce_opt.nonEmpty) + Seq(channelReestablishAlice, channelReestablishBob).foreach(channelReestablish => { + assert(channelReestablish.nextCommitNonces.size == 2) + assert(channelReestablish.nextCommitNonces.values.toSet.size == 2) + assert(channelReestablish.nextCommitNonces.contains(fundingTxId)) + assert(channelReestablish.nextCommitNonces.contains(rbfTxId)) + }) + assert(channelReestablishAlice.nextCommitNonces.get(rbfTxId).contains(txCompleteAlice.commitNonces_opt.get.nextCommitNonce)) + assert(channelReestablishBob.nextCommitNonces.get(rbfTxId).contains(txCompleteBob.commitNonces_opt.get.nextCommitNonce)) + } // Alice retransmits commit_sig, and they exchange tx_signatures afterwards. bob2alice.expectNoMessage(100 millis) @@ -984,11 +1043,19 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture awaitCond(bob.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].status == DualFundingStatus.WaitingForConfirmations) } - test("recv INPUT_DISCONNECTED (rbf commit_sig received by Bob)", Tag(ChannelStateTestsTags.DualFunding)) { f => + test("recv INPUT_DISCONNECTED (rbf commit_sig received by Alice)", Tag(ChannelStateTestsTags.DualFunding)) { f => + testDisconnectRbfCommitSigReceivedAlice(f, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) + } + + test("recv INPUT_DISCONNECTED (rbf commit_sig received by Alice, taproot)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => + testDisconnectRbfCommitSigReceivedAlice(f, ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat) + } + + def testDisconnectRbfCommitSigReceivedBob(f: FixtureParam, commitmentFormat: CommitmentFormat): Unit = { import f._ - initiateRbf(f) - alice2bob.expectMsgType[TxComplete] + val txCompleteBob = initiateRbf(f) + val txCompleteAlice = alice2bob.expectMsgType[TxComplete] alice2bob.forward(bob) alice2bob.expectMsgType[CommitSig] alice2bob.forward(bob) @@ -996,13 +1063,29 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture bob2alice.expectMsgType[TxSignatures] // Alice doesn't receive Bob's tx_signatures awaitCond(alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].status.isInstanceOf[DualFundingStatus.RbfWaitingForSigs]) awaitCond(bob.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].status == DualFundingStatus.WaitingForConfirmations) + val fundingTxId = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].latestFundingTx.sharedTx.txId val rbfTxId = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].status.asInstanceOf[DualFundingStatus.RbfWaitingForSigs].signingSession.fundingTx.txId + assert(fundingTxId != rbfTxId) val (channelReestablishAlice, channelReestablishBob) = reconnectRbf(f) assert(channelReestablishAlice.nextFundingTxId_opt.contains(rbfTxId)) assert(channelReestablishAlice.nextLocalCommitmentNumber == 0) assert(channelReestablishBob.nextFundingTxId_opt.contains(rbfTxId)) assert(channelReestablishBob.nextLocalCommitmentNumber == 1) + commitmentFormat match { + case _: SegwitV0CommitmentFormat => () + case _: TaprootCommitmentFormat => + assert(channelReestablishAlice.currentCommitNonce_opt.nonEmpty) + assert(channelReestablishBob.currentCommitNonce_opt.isEmpty) + Seq(channelReestablishAlice, channelReestablishBob).foreach(channelReestablish => { + assert(channelReestablish.nextCommitNonces.size == 2) + assert(channelReestablish.nextCommitNonces.values.toSet.size == 2) + assert(channelReestablish.nextCommitNonces.contains(fundingTxId)) + assert(channelReestablish.nextCommitNonces.contains(rbfTxId)) + }) + assert(channelReestablishAlice.nextCommitNonces.get(rbfTxId).contains(txCompleteAlice.commitNonces_opt.get.nextCommitNonce)) + assert(channelReestablishBob.nextCommitNonces.get(rbfTxId).contains(txCompleteBob.commitNonces_opt.get.nextCommitNonce)) + } // Bob retransmits commit_sig and tx_signatures, then Alice sends her tx_signatures. bob2alice.expectMsgType[CommitSig] @@ -1021,7 +1104,15 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture awaitCond(bob.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].status == DualFundingStatus.WaitingForConfirmations) } - test("recv INPUT_DISCONNECTED (rbf commit_sig received)", Tag(ChannelStateTestsTags.DualFunding)) { f => + test("recv INPUT_DISCONNECTED (rbf commit_sig received by Bob)", Tag(ChannelStateTestsTags.DualFunding)) { f => + testDisconnectRbfCommitSigReceivedBob(f, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) + } + + test("recv INPUT_DISCONNECTED (rbf commit_sig received by Bob, taproot)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => + testDisconnectRbfCommitSigReceivedBob(f, ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat) + } + + test("recv INPUT_DISCONNECTED (rbf commit_sig received by Bob, taproot, missing current commit nonce)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => import f._ initiateRbf(f) @@ -1029,18 +1120,67 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture alice2bob.forward(bob) alice2bob.expectMsgType[CommitSig] alice2bob.forward(bob) + bob2alice.expectMsgType[CommitSig] // Alice doesn't receive Bob's commit_sig + bob2alice.expectMsgType[TxSignatures] // Alice doesn't receive Bob's tx_signatures + awaitCond(alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].status.isInstanceOf[DualFundingStatus.RbfWaitingForSigs]) + awaitCond(bob.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].status == DualFundingStatus.WaitingForConfirmations) + + alice ! INPUT_DISCONNECTED + awaitCond(alice.stateName == OFFLINE) + bob ! INPUT_DISCONNECTED + awaitCond(bob.stateName == OFFLINE) + + // Alice is buggy and doesn't include her current commit nonce in channel_reestablish. + val aliceInit = Init(alice.underlyingActor.nodeParams.features.initFeatures()) + val bobInit = Init(bob.underlyingActor.nodeParams.features.initFeatures()) + alice ! INPUT_RECONNECTED(bob, aliceInit, bobInit) + bob ! INPUT_RECONNECTED(alice, bobInit, aliceInit) + val channelReestablishAlice = alice2bob.expectMsgType[ChannelReestablish] + assert(channelReestablishAlice.nextFundingTxId_opt.nonEmpty) + assert(channelReestablishAlice.nextLocalCommitmentNumber == 0) + assert(channelReestablishAlice.currentCommitNonce_opt.nonEmpty) + bob2alice.expectMsgType[ChannelReestablish] + alice2bob.forward(bob, channelReestablishAlice.copy(tlvStream = TlvStream(channelReestablishAlice.tlvStream.records.filterNot(_.isInstanceOf[ChannelReestablishTlv.CurrentCommitNonceTlv])))) + bob2alice.expectMsgType[Error] + awaitCond(bob.stateName == CLOSING) + } + + def testDisconnectRbfCommitSigReceived(f: FixtureParam, commitmentFormat: CommitmentFormat): Unit = { + import f._ + + val txCompleteBob = initiateRbf(f) + val txCompleteAlice = alice2bob.expectMsgType[TxComplete] + alice2bob.forward(bob) + alice2bob.expectMsgType[CommitSig] + alice2bob.forward(bob) bob2alice.expectMsgType[CommitSig] bob2alice.forward(alice) bob2alice.expectMsgType[TxSignatures] // Alice doesn't receive Bob's tx_signatures awaitCond(alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].status.isInstanceOf[DualFundingStatus.RbfWaitingForSigs]) awaitCond(bob.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].status == DualFundingStatus.WaitingForConfirmations) + val fundingTxId = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].latestFundingTx.sharedTx.txId val rbfTx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].status.asInstanceOf[DualFundingStatus.RbfWaitingForSigs].signingSession.fundingTx + assert(fundingTxId != rbfTx.txId) val (channelReestablishAlice, channelReestablishBob) = reconnectRbf(f) assert(channelReestablishAlice.nextFundingTxId_opt.contains(rbfTx.txId)) + assert(channelReestablishAlice.currentCommitNonce_opt.isEmpty) assert(channelReestablishAlice.nextLocalCommitmentNumber == 1) assert(channelReestablishBob.nextFundingTxId_opt.contains(rbfTx.txId)) + assert(channelReestablishBob.currentCommitNonce_opt.isEmpty) assert(channelReestablishBob.nextLocalCommitmentNumber == 1) + commitmentFormat match { + case _: SegwitV0CommitmentFormat => () + case _: TaprootCommitmentFormat => + Seq(channelReestablishAlice, channelReestablishBob).foreach(channelReestablish => { + assert(channelReestablish.nextCommitNonces.size == 2) + assert(channelReestablish.nextCommitNonces.values.toSet.size == 2) + assert(channelReestablish.nextCommitNonces.contains(fundingTxId)) + assert(channelReestablish.nextCommitNonces.contains(rbfTx.txId)) + }) + assert(channelReestablishAlice.nextCommitNonces.get(rbfTx.txId).contains(txCompleteAlice.commitNonces_opt.get.nextCommitNonce)) + assert(channelReestablishBob.nextCommitNonces.get(rbfTx.txId).contains(txCompleteBob.commitNonces_opt.get.nextCommitNonce)) + } // Alice and Bob exchange tx_signatures and complete the RBF attempt. bob2alice.expectMsgType[TxSignatures] @@ -1056,10 +1196,17 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture awaitCond(bob.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].status == DualFundingStatus.WaitingForConfirmations) } - test("recv INPUT_DISCONNECTED (rbf tx_signatures partially received)", Tag(ChannelStateTestsTags.DualFunding)) { f => + test("recv INPUT_DISCONNECTED (rbf commit_sig received)", Tag(ChannelStateTestsTags.DualFunding)) { f => + testDisconnectRbfCommitSigReceived(f, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) + } + + test("recv INPUT_DISCONNECTED (rbf commit_sig received, taproot)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => + testDisconnectRbfCommitSigReceived(f, ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat) + } + + test("recv INPUT_DISCONNECTED (rbf commit_sig received, taproot, missing next commit nonce)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => import f._ - val currentFundingTxId = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].latestFundingTx.sharedTx.txId initiateRbf(f) alice2bob.expectMsgType[TxComplete] alice2bob.forward(bob) @@ -1067,6 +1214,49 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture alice2bob.forward(bob) bob2alice.expectMsgType[CommitSig] bob2alice.forward(alice) + bob2alice.expectMsgType[TxSignatures] // Alice doesn't receive Bob's tx_signatures + awaitCond(alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].status.isInstanceOf[DualFundingStatus.RbfWaitingForSigs]) + awaitCond(bob.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].status == DualFundingStatus.WaitingForConfirmations) + val fundingTxId = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].latestFundingTx.sharedTx.txId + val rbfTxId = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].status.asInstanceOf[DualFundingStatus.RbfWaitingForSigs].signingSession.fundingTx.txId + assert(fundingTxId != rbfTxId) + + alice ! INPUT_DISCONNECTED + awaitCond(alice.stateName == OFFLINE) + bob ! INPUT_DISCONNECTED + awaitCond(bob.stateName == OFFLINE) + + val aliceInit = Init(alice.underlyingActor.nodeParams.features.initFeatures()) + val bobInit = Init(bob.underlyingActor.nodeParams.features.initFeatures()) + alice ! INPUT_RECONNECTED(bob, aliceInit, bobInit) + bob ! INPUT_RECONNECTED(alice, bobInit, aliceInit) + // Alice is buggy and doesn't include her next commit nonce for the initial funding tx. + val channelReestablishAlice = alice2bob.expectMsgType[ChannelReestablish] + val aliceNonces = ChannelReestablishTlv.NextLocalNoncesTlv((channelReestablishAlice.nextCommitNonces - fundingTxId).toSeq) + val channelReestablishAlice1 = channelReestablishAlice.copy(tlvStream = TlvStream(channelReestablishAlice.tlvStream.records.filterNot(_.isInstanceOf[ChannelReestablishTlv.NextLocalNoncesTlv]) + aliceNonces)) + // Bob is buggy and doesn't include his next commit nonce for the RBF tx. + val channelReestablishBob = bob2alice.expectMsgType[ChannelReestablish] + val bobNonces = ChannelReestablishTlv.NextLocalNoncesTlv((channelReestablishBob.nextCommitNonces - rbfTxId).toSeq) + val channelReestablishBob1 = channelReestablishBob.copy(tlvStream = TlvStream(channelReestablishBob.tlvStream.records.filterNot(_.isInstanceOf[ChannelReestablishTlv.NextLocalNoncesTlv]) + bobNonces)) + alice2bob.forward(bob, channelReestablishAlice1) + assert(bob2alice.expectMsgType[Error].toAscii == MissingCommitNonce(channelReestablishBob.channelId, fundingTxId, commitmentNumber = 1).getMessage) + awaitCond(bob.stateName == CLOSING) + bob2alice.forward(alice, channelReestablishBob1) + assert(alice2bob.expectMsgType[Error].toAscii == MissingCommitNonce(channelReestablishAlice.channelId, rbfTxId, commitmentNumber = 1).getMessage) + awaitCond(alice.stateName == CLOSING) + } + + def testDisconnectTxSigsPartiallyReceived(f: FixtureParam, commitmentFormat: CommitmentFormat): Unit = { + import f._ + + val currentFundingTxId = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].latestFundingTx.sharedTx.txId + val txCompleteBob = initiateRbf(f) + val txCompleteAlice = alice2bob.expectMsgType[TxComplete] + alice2bob.forward(bob) + alice2bob.expectMsgType[CommitSig] + alice2bob.forward(bob) + bob2alice.expectMsgType[CommitSig] + bob2alice.forward(alice) bob2alice.expectMsgType[TxSignatures] bob2alice.forward(alice) alice2bob.expectMsgType[TxSignatures] // Bob doesn't receive Alice's tx_signatures @@ -1077,9 +1267,23 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture val (channelReestablishAlice, channelReestablishBob) = reconnectRbf(f) assert(channelReestablishAlice.nextFundingTxId_opt.isEmpty) + assert(channelReestablishAlice.currentCommitNonce_opt.isEmpty) assert(channelReestablishAlice.nextLocalCommitmentNumber == 1) assert(channelReestablishBob.nextFundingTxId_opt.contains(rbfTxId)) + assert(channelReestablishBob.currentCommitNonce_opt.isEmpty) assert(channelReestablishBob.nextLocalCommitmentNumber == 1) + commitmentFormat match { + case _: SegwitV0CommitmentFormat => () + case _: TaprootCommitmentFormat => + Seq(channelReestablishAlice, channelReestablishBob).foreach(channelReestablish => { + assert(channelReestablish.nextCommitNonces.size == 2) + assert(channelReestablish.nextCommitNonces.values.toSet.size == 2) + assert(channelReestablish.nextCommitNonces.contains(currentFundingTxId)) + assert(channelReestablish.nextCommitNonces.contains(rbfTxId)) + }) + assert(channelReestablishAlice.nextCommitNonces.get(rbfTxId).contains(txCompleteAlice.commitNonces_opt.get.nextCommitNonce)) + assert(channelReestablishBob.nextCommitNonces.get(rbfTxId).contains(txCompleteBob.commitNonces_opt.get.nextCommitNonce)) + } // Alice and Bob exchange signatures and complete the RBF attempt. bob2alice.expectNoMessage(100 millis) @@ -1094,97 +1298,103 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture awaitCond(bob.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].status == DualFundingStatus.WaitingForConfirmations) } + test("recv INPUT_DISCONNECTED (rbf tx_signatures partially received)", Tag(ChannelStateTestsTags.DualFunding)) { f => + testDisconnectTxSigsPartiallyReceived(f, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) + } + + test("recv INPUT_DISCONNECTED (rbf tx_signatures partially received, taproot)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => + testDisconnectTxSigsPartiallyReceived(f, ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat) + } + test("recv Error", Tag(ChannelStateTestsTags.DualFunding)) { f => import f._ - val tx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val tx = alice.signCommitTx() alice ! Error(ByteVector32.Zeroes, "dual funding d34d") awaitCond(alice.stateName == CLOSING) - assert(alice2blockchain.expectMsgType[TxPublisher.PublishFinalTx].tx.txid == tx.txid) - alice2blockchain.expectMsgType[TxPublisher.PublishTx] // claim-main-delayed - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == tx.txid) + alice2blockchain.expectFinalTxPublished(tx.txid) } - test("recv Error (remote commit published)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.StaticRemoteKey), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + test("recv Error (remote commit published)", Tag(ChannelStateTestsTags.DualFunding)) { f => import f._ - val aliceCommitTx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val aliceCommitTx = alice.signCommitTx() alice ! Error(ByteVector32.Zeroes, "force-closing channel, bye-bye") awaitCond(alice.stateName == CLOSING) aliceListener.expectMsgType[ChannelAborted] - assert(alice2blockchain.expectMsgType[TxPublisher.PublishFinalTx].tx.txid == aliceCommitTx.txid) - assert(alice2blockchain.expectMsgType[TxPublisher.PublishReplaceableTx].txInfo.isInstanceOf[ClaimLocalAnchorOutputTx]) - val claimMainLocal = alice2blockchain.expectMsgType[TxPublisher.PublishFinalTx] - assert(claimMainLocal.input.txid == aliceCommitTx.txid) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == aliceCommitTx.txid) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == claimMainLocal.tx.txid) + alice2blockchain.expectFinalTxPublished(aliceCommitTx.txid) + val anchorTxLocal = alice2blockchain.expectReplaceableTxPublished[ClaimLocalAnchorTx] + val claimMainLocal = alice2blockchain.expectFinalTxPublished("local-main-delayed") + Transaction.correctlySpends(claimMainLocal.tx, Seq(aliceCommitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + alice2blockchain.expectWatchTxConfirmed(aliceCommitTx.txid) + alice2blockchain.expectWatchOutputSpent(claimMainLocal.input) + alice2blockchain.expectWatchOutputSpent(anchorTxLocal.input.outPoint) // Bob broadcasts his commit tx as well. - val bobCommitTx = bob.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val bobCommitTx = bob.signCommitTx() alice ! WatchFundingSpentTriggered(bobCommitTx) - alice2blockchain.expectMsgType[WatchOutputSpent] - assert(alice2blockchain.expectMsgType[TxPublisher.PublishReplaceableTx].txInfo.isInstanceOf[ClaimLocalAnchorOutputTx]) - val claimMainRemote = alice2blockchain.expectMsgType[TxPublisher.PublishFinalTx] - assert(claimMainRemote.input.txid == bobCommitTx.txid) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == bobCommitTx.txid) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == claimMainRemote.tx.txid) + val anchorTxRemote = alice2blockchain.expectReplaceableTxPublished[ClaimRemoteAnchorTx] + val claimMainRemote = alice2blockchain.expectFinalTxPublished("remote-main-delayed") + Transaction.correctlySpends(claimMainRemote.tx, Seq(bobCommitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + alice2blockchain.expectWatchTxConfirmed(bobCommitTx.txid) + alice2blockchain.expectWatchOutputSpent(claimMainRemote.input) + alice2blockchain.expectWatchOutputSpent(anchorTxRemote.input.outPoint) + alice2blockchain.expectNoMessage(100 millis) } - test("recv Error (previous tx confirms)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.StaticRemoteKey), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + test("recv Error (previous tx confirms)", Tag(ChannelStateTestsTags.DualFunding)) { f => import f._ val fundingTx1 = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].latestFundingTx.sharedTx.asInstanceOf[FullySignedSharedTransaction].signedTx - val aliceCommitTx1 = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx - val bobCommitTx1 = bob.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx - assert(aliceCommitTx1.input.outPoint.txid == fundingTx1.txid) - assert(bobCommitTx1.input.outPoint.txid == fundingTx1.txid) + val aliceCommitTx1 = alice.signCommitTx() + val bobCommitTx1 = bob.signCommitTx() + assert(aliceCommitTx1.txIn.head.outPoint.txid == fundingTx1.txid) + assert(bobCommitTx1.txIn.head.outPoint.txid == fundingTx1.txid) val fundingTx2 = testBumpFundingFees(f) assert(fundingTx1.txid != fundingTx2.signedTx.txid) - val aliceCommitTx2 = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx - assert(aliceCommitTx2.input.outPoint.txid == fundingTx2.signedTx.txid) + val aliceCommitTx2 = alice.signCommitTx() + assert(aliceCommitTx2.txIn.head.outPoint.txid == fundingTx2.signedTx.txid) // Alice receives an error and force-closes using the latest funding transaction. alice ! Error(ByteVector32.Zeroes, "dual funding d34d") awaitCond(alice.stateName == CLOSING) aliceListener.expectMsgType[ChannelAborted] - assert(alice2blockchain.expectMsgType[TxPublisher.PublishFinalTx].tx.txid == aliceCommitTx2.tx.txid) - assert(alice2blockchain.expectMsgType[TxPublisher.PublishReplaceableTx].txInfo.isInstanceOf[ClaimLocalAnchorOutputTx]) - val claimMain2 = alice2blockchain.expectMsgType[TxPublisher.PublishFinalTx] - assert(claimMain2.input.txid == aliceCommitTx2.tx.txid) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == aliceCommitTx2.tx.txid) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == claimMain2.tx.txid) + alice2blockchain.expectFinalTxPublished(aliceCommitTx2.txid) + val anchorTx2 = alice2blockchain.expectReplaceableTxPublished[ClaimLocalAnchorTx] + val claimMain2 = alice2blockchain.expectFinalTxPublished("local-main-delayed") + Transaction.correctlySpends(claimMain2.tx, Seq(aliceCommitTx2), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + alice2blockchain.expectWatchTxConfirmed(aliceCommitTx2.txid) + alice2blockchain.expectWatchOutputsSpent(Seq(anchorTx2.input.outPoint, claimMain2.input)) // A previous funding transaction confirms, so Alice publishes the corresponding commit tx. alice ! WatchFundingConfirmedTriggered(BlockHeight(42000), 42, fundingTx1) assert(aliceListener.expectMsgType[TransactionConfirmed].tx == fundingTx1) - alice2blockchain.expectMsgType[WatchOutputSpent] assert(alice2blockchain.expectMsgType[WatchFundingSpent].txId == fundingTx1.txid) alice2blockchain.expectMsg(UnwatchTxConfirmed(fundingTx2.txId)) - assert(alice2blockchain.expectMsgType[TxPublisher.PublishFinalTx].tx.txid == aliceCommitTx1.tx.txid) - assert(alice2blockchain.expectMsgType[TxPublisher.PublishReplaceableTx].txInfo.isInstanceOf[ClaimLocalAnchorOutputTx]) - val claimMain1 = alice2blockchain.expectMsgType[TxPublisher.PublishFinalTx] - assert(claimMain1.input.txid == aliceCommitTx1.tx.txid) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == aliceCommitTx1.tx.txid) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == claimMain1.tx.txid) - testUnusedInputsUnlocked(wallet, Seq(fundingTx2)) + alice2blockchain.expectFinalTxPublished(aliceCommitTx1.txid) + val anchorTx1 = alice2blockchain.expectReplaceableTxPublished[ClaimLocalAnchorTx] + val claimMain1 = alice2blockchain.expectFinalTxPublished("local-main-delayed") + Transaction.correctlySpends(claimMain1.tx, Seq(aliceCommitTx1), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + alice2blockchain.expectWatchTxConfirmed(aliceCommitTx1.txid) + alice2blockchain.expectWatchOutputsSpent(Seq(anchorTx1.input.outPoint, claimMain1.input)) + testUnusedInputsUnlocked(aliceWallet, Seq(fundingTx2)) // Bob publishes his commit tx, Alice reacts by spending her remote main output. - alice ! WatchFundingSpentTriggered(bobCommitTx1.tx) - alice2blockchain.expectMsgType[WatchOutputSpent] - assert(alice2blockchain.expectMsgType[TxPublisher.PublishReplaceableTx].txInfo.isInstanceOf[ClaimLocalAnchorOutputTx]) - val claimMainRemote = alice2blockchain.expectMsgType[TxPublisher.PublishFinalTx] - assert(claimMainRemote.input.txid == bobCommitTx1.tx.txid) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == bobCommitTx1.tx.txid) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == claimMainRemote.tx.txid) + alice ! WatchFundingSpentTriggered(bobCommitTx1) + val anchorRemote = alice2blockchain.expectReplaceableTxPublished[ClaimRemoteAnchorTx] + val claimMainRemote = alice2blockchain.expectFinalTxPublished("remote-main-delayed") + Transaction.correctlySpends(claimMainRemote.tx, Seq(bobCommitTx1), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + alice2blockchain.expectWatchTxConfirmed(bobCommitTx1.txid) + alice2blockchain.expectWatchOutputsSpent(Seq(anchorRemote.input.outPoint, claimMainRemote.input)) assert(alice.stateName == CLOSING) } test("recv Error (nothing at stake)", Tag(ChannelStateTestsTags.DualFunding), Tag(noFundingContribution)) { f => import f._ - val commitTx = bob.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val commitTx = bob.signCommitTx() bob ! Error(ByteVector32.Zeroes, "please help me recover my funds") // We have nothing at stake, but we publish our commitment to help our peer recover their funds more quickly. awaitCond(bob.stateName == CLOSING) bobListener.expectMsgType[ChannelAborted] - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == commitTx.txid) - assert(bob2blockchain.expectMsgType[WatchTxConfirmed].txId == commitTx.txid) + bob2blockchain.expectFinalTxPublished(commitTx.txid) + bob2blockchain.expectWatchTxConfirmed(commitTx.txid) bob ! WatchTxConfirmedTriggered(BlockHeight(42), 1, commitTx) bobListener.expectMsgType[TransactionConfirmed] awaitCond(bob.stateName == CLOSED) @@ -1202,34 +1412,35 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture test("recv CMD_FORCECLOSE", Tag(ChannelStateTestsTags.DualFunding)) { f => import f._ val sender = TestProbe() - val commitTx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val commitTx = alice.signCommitTx() alice ! CMD_FORCECLOSE(sender.ref) awaitCond(alice.stateName == CLOSING) aliceListener.expectMsgType[ChannelAborted] - assert(alice2blockchain.expectMsgType[TxPublisher.PublishFinalTx].tx.txid == commitTx.txid) - val claimMain = alice2blockchain.expectMsgType[TxPublisher.PublishFinalTx] - assert(claimMain.input.txid == commitTx.txid) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == commitTx.txid) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == claimMain.tx.txid) + alice2blockchain.expectFinalTxPublished(commitTx.txid) + alice2blockchain.expectReplaceableTxPublished[ClaimLocalAnchorTx] + val claimMain = alice2blockchain.expectFinalTxPublished("local-main-delayed") + Transaction.correctlySpends(claimMain.tx, Seq(commitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + alice2blockchain.expectWatchTxConfirmed(commitTx.txid) } def restartNodes(f: FixtureParam, aliceData: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED, bobData: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED): (TestFSMRef[ChannelState, ChannelData, Channel], TestFSMRef[ChannelState, ChannelData, Channel]) = { import f._ val (aliceNodeParams, bobNodeParams) = (alice.underlyingActor.nodeParams, bob.underlyingActor.nodeParams) + val (aliceKeys, bobKeys) = (alice.underlyingActor.channelKeys, bob.underlyingActor.channelKeys) val (alicePeer, bobPeer) = (alice.getParent, bob.getParent) alice.stop() bob.stop() - val alice2 = TestFSMRef(new Channel(aliceNodeParams, wallet, bobNodeParams.nodeId, alice2blockchain.ref, TestProbe().ref, FakeTxPublisherFactory(alice2blockchain)), alicePeer) + val alice2 = TestFSMRef(new Channel(aliceNodeParams, aliceKeys, aliceWallet, bobNodeParams.nodeId, alice2blockchain.ref, TestProbe().ref, FakeTxPublisherFactory(alice2blockchain)), alicePeer) alice2 ! INPUT_RESTORED(aliceData) alice2blockchain.expectMsgType[SetChannelId] // When restoring, we watch confirmation of all potential funding transactions to detect offline force-closes. aliceData.allFundingTxs.foreach(f => alice2blockchain.expectMsgType[WatchFundingConfirmed].txId == f.sharedTx.txId) awaitCond(alice2.stateName == OFFLINE) - val bob2 = TestFSMRef(new Channel(bobNodeParams, wallet, aliceNodeParams.nodeId, bob2blockchain.ref, TestProbe().ref, FakeTxPublisherFactory(bob2blockchain)), bobPeer) + val bob2 = TestFSMRef(new Channel(bobNodeParams, bobKeys, bobWallet, aliceNodeParams.nodeId, bob2blockchain.ref, TestProbe().ref, FakeTxPublisherFactory(bob2blockchain)), bobPeer) bob2 ! INPUT_RESTORED(bobData) bob2blockchain.expectMsgType[SetChannelId] assert(bob2blockchain.expectMsgType[WatchFundingConfirmed].txId == bobData.commitments.latest.fundingTxId) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForDualFundingReadyStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForDualFundingReadyStateSpec.scala index e416cbaf20..25a7022584 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForDualFundingReadyStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForDualFundingReadyStateSpec.scala @@ -16,7 +16,6 @@ package fr.acinq.eclair.channel.states.c -import akka.actor.typed.scaladsl.adapter.ClassicActorRefOps import akka.testkit.{TestFSMRef, TestProbe} import fr.acinq.bitcoin.scalacompat.{ByteVector32, Transaction} import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ @@ -25,9 +24,11 @@ import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.channel.fund.InteractiveTxBuilder.FullySignedSharedTransaction import fr.acinq.eclair.channel.publish.TxPublisher import fr.acinq.eclair.channel.publish.TxPublisher.SetChannelId +import fr.acinq.eclair.channel.states.ChannelStateTestsBase.PimpTestFSM import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags} import fr.acinq.eclair.payment.relay.Relayer.RelayFees -import fr.acinq.eclair.transactions.Transactions.ClaimLocalAnchorOutputTx +import fr.acinq.eclair.testutils.PimpTestProbe.convert +import fr.acinq.eclair.transactions.Transactions.ClaimRemoteAnchorTx import fr.acinq.eclair.wire.protocol._ import fr.acinq.eclair.{BlockHeight, MilliSatoshiLong, TestConstants, TestKitBaseClass} import org.scalatest.OptionValues.convertOptionToValuable @@ -44,16 +45,13 @@ class WaitForDualFundingReadyStateSpec extends TestKitBaseClass with FixtureAnyF val setup = init(tags = test.tags) import setup._ - val channelConfig = ChannelConfig.standard val channelFlags = ChannelFlags(announceChannel = test.tags.contains(ChannelStateTestsTags.ChannelsPublic)) - val (aliceParams, bobParams, channelType) = computeFeatures(setup, test.tags, channelFlags) - val aliceInit = Init(aliceParams.initFeatures) - val bobInit = Init(bobParams.initFeatures) + val channelParams = computeChannelParams(setup, test.tags, channelFlags) val listener = TestProbe() within(30 seconds) { alice.underlying.system.eventStream.subscribe(listener.ref, classOf[ChannelAborted]) - alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, TestConstants.fundingSatoshis, dualFunded = true, TestConstants.anchorOutputsFeeratePerKw, TestConstants.feeratePerKw, fundingTxFeeBudget_opt = None, None, requireConfirmedInputs = false, requestFunding_opt = None, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType, replyTo = aliceOpenReplyTo.ref.toTyped) - bob ! INPUT_INIT_CHANNEL_NON_INITIATOR(ByteVector32.Zeroes, Some(LiquidityAds.AddFunding(TestConstants.nonInitiatorFundingSatoshis, None)), dualFunded = true, None, requireConfirmedInputs = false, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) + alice ! channelParams.initChannelAlice(TestConstants.fundingSatoshis, dualFunded = true, channelFlags = channelFlags) + bob ! channelParams.initChannelBob(Some(LiquidityAds.AddFunding(TestConstants.nonInitiatorFundingSatoshis, None)), dualFunded = true) alice2blockchain.expectMsgType[SetChannelId] // temporary channel id bob2blockchain.expectMsgType[SetChannelId] // temporary channel id alice2bob.expectMsgType[OpenDualFundedChannel] @@ -109,7 +107,7 @@ class WaitForDualFundingReadyStateSpec extends TestKitBaseClass with FixtureAnyF } } - test("recv ChannelReady", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + test("recv ChannelReady", Tag(ChannelStateTestsTags.DualFunding)) { f => import f._ alice.underlyingActor.nodeParams.db.peers.addOrUpdateRelayFees(bob.underlyingActor.nodeParams.nodeId, RelayFees(20 msat, 125)) @@ -153,7 +151,7 @@ class WaitForDualFundingReadyStateSpec extends TestKitBaseClass with FixtureAnyF assert(aliceUpdate.shortChannelId == aliceChannelReady.alias_opt.value) assert(aliceUpdate.feeBaseMsat == 20.msat) assert(aliceUpdate.feeProportionalMillionths == 125) - assert(aliceCommitments.localChannelReserve == aliceCommitments.commitInput.txOut.amount / 100) + assert(aliceCommitments.localChannelReserve == aliceCommitments.capacity / 100) assert(aliceCommitments.localChannelReserve == aliceCommitments.remoteChannelReserve) val bobCommitments = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest assert(bobCommitments.commitment.shortChannelId_opt.nonEmpty) @@ -170,7 +168,7 @@ class WaitForDualFundingReadyStateSpec extends TestKitBaseClass with FixtureAnyF bob2alice.expectNoMessage(100 millis) } - test("recv ChannelReady (zero-conf)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), Tag(ChannelStateTestsTags.ZeroConf)) { f => + test("recv ChannelReady (zero-conf)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.ZeroConf)) { f => import f._ val listenerA = TestProbe() @@ -192,7 +190,7 @@ class WaitForDualFundingReadyStateSpec extends TestKitBaseClass with FixtureAnyF val aliceCommitments = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest assert(aliceCommitments.commitment.shortChannelId_opt.isEmpty) assert(alice.stateData.asInstanceOf[DATA_NORMAL].channelUpdate.shortChannelId == aliceChannelReady.alias_opt.value) - assert(aliceCommitments.localChannelReserve == aliceCommitments.commitInput.txOut.amount / 100) + assert(aliceCommitments.localChannelReserve == aliceCommitments.capacity / 100) assert(aliceCommitments.localChannelReserve == aliceCommitments.remoteChannelReserve) val bobCommitments = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest assert(bobCommitments.commitment.shortChannelId_opt.isEmpty) @@ -206,7 +204,7 @@ class WaitForDualFundingReadyStateSpec extends TestKitBaseClass with FixtureAnyF bob2alice.expectNoMessage(100 millis) } - test("recv ChannelReady (public channel)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), Tag(ChannelStateTestsTags.ChannelsPublic)) { f => + test("recv ChannelReady (public channel)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.ChannelsPublic)) { f => import f._ val aliceChannelReady = alice2bob.expectMsgType[ChannelReady] @@ -257,7 +255,7 @@ class WaitForDualFundingReadyStateSpec extends TestKitBaseClass with FixtureAnyF awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].lastAnnouncement_opt.nonEmpty) } - test("recv TxInitRbf", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + test("recv TxInitRbf", Tag(ChannelStateTestsTags.DualFunding)) { f => import f._ alice2bob.expectMsgType[ChannelReady] alice ! TxInitRbf(channelId(alice), 0, TestConstants.feeratePerKw * 1.1) @@ -265,19 +263,19 @@ class WaitForDualFundingReadyStateSpec extends TestKitBaseClass with FixtureAnyF assert(alice.stateName == WAIT_FOR_DUAL_FUNDING_READY) } - test("recv WatchFundingSpentTriggered (remote commit)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + test("recv WatchFundingSpentTriggered (remote commit)", Tag(ChannelStateTestsTags.DualFunding)) { f => import f._ // bob publishes his commitment tx - val bobCommitTx = bob.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_READY].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val bobCommitTx = bob.signCommitTx() alice ! WatchFundingSpentTriggered(bobCommitTx) - assert(alice2blockchain.expectMsgType[TxPublisher.PublishReplaceableTx].txInfo.isInstanceOf[ClaimLocalAnchorOutputTx]) + alice2blockchain.expectReplaceableTxPublished[ClaimRemoteAnchorTx] alice2blockchain.expectMsgType[TxPublisher.PublishTx] assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == bobCommitTx.txid) listener.expectMsgType[ChannelAborted] awaitCond(alice.stateName == CLOSING) } - test("recv WatchFundingSpentTriggered (unrecognized commit)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + test("recv WatchFundingSpentTriggered (unrecognized commit)", Tag(ChannelStateTestsTags.DualFunding)) { f => import f._ alice2bob.expectMsgType[ChannelReady] alice ! WatchFundingSpentTriggered(Transaction(0, Nil, Nil, 0)) @@ -285,9 +283,9 @@ class WaitForDualFundingReadyStateSpec extends TestKitBaseClass with FixtureAnyF assert(alice.stateName == WAIT_FOR_DUAL_FUNDING_READY) } - test("recv Error", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + test("recv Error", Tag(ChannelStateTestsTags.DualFunding)) { f => import f._ - val commitTx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_READY].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val commitTx = alice.signCommitTx() alice ! Error(ByteVector32.Zeroes, "dual funding failure") listener.expectMsgType[ChannelAborted] awaitCond(alice.stateName == CLOSING) @@ -297,7 +295,7 @@ class WaitForDualFundingReadyStateSpec extends TestKitBaseClass with FixtureAnyF assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == commitTx.txid) } - test("recv CMD_CLOSE", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + test("recv CMD_CLOSE", Tag(ChannelStateTestsTags.DualFunding)) { f => import f._ val sender = TestProbe() val c = CMD_CLOSE(sender.ref, None, None) @@ -305,10 +303,10 @@ class WaitForDualFundingReadyStateSpec extends TestKitBaseClass with FixtureAnyF sender.expectMsg(RES_FAILURE(c, CommandUnavailableInThisState(channelId(alice), "close", WAIT_FOR_DUAL_FUNDING_READY))) } - test("recv CMD_FORCECLOSE", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + test("recv CMD_FORCECLOSE", Tag(ChannelStateTestsTags.DualFunding)) { f => import f._ val sender = TestProbe() - val commitTx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_READY].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val commitTx = alice.signCommitTx() alice ! CMD_FORCECLOSE(sender.ref) listener.expectMsgType[ChannelAborted] awaitCond(alice.stateName == CLOSING) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingConfirmedStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingConfirmedStateSpec.scala index d35d398660..81f33b5b6f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingConfirmedStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingConfirmedStateSpec.scala @@ -16,7 +16,6 @@ package fr.acinq.eclair.channel.states.c -import akka.actor.typed.scaladsl.adapter.ClassicActorRefOps import akka.testkit.{TestFSMRef, TestProbe} import fr.acinq.bitcoin.scalacompat.{ByteVector32, SatoshiLong, Script, Transaction} import fr.acinq.eclair.blockchain.CurrentBlockHeight @@ -26,9 +25,11 @@ import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.channel.fsm.Channel.{BITCOIN_FUNDING_PUBLISH_FAILED, BITCOIN_FUNDING_TIMEOUT} import fr.acinq.eclair.channel.publish.TxPublisher import fr.acinq.eclair.channel.publish.TxPublisher.PublishFinalTx +import fr.acinq.eclair.channel.states.ChannelStateTestsBase.PimpTestFSM import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags} +import fr.acinq.eclair.testutils.PimpTestProbe.convert import fr.acinq.eclair.transactions.Scripts.multiSig2of2 -import fr.acinq.eclair.wire.protocol.{AcceptChannel, ChannelReady, Error, FundingCreated, FundingSigned, Init, OpenChannel, TlvStream} +import fr.acinq.eclair.wire.protocol.{AcceptChannel, ChannelReady, Error, FundingCreated, FundingSigned, OpenChannel, TlvStream} import fr.acinq.eclair.{BlockHeight, MilliSatoshiLong, TestConstants, TestKitBaseClass, randomKey} import org.scalatest.funsuite.FixtureAnyFunSuiteLike import org.scalatest.{Outcome, Tag} @@ -46,13 +47,8 @@ class WaitForFundingConfirmedStateSpec extends TestKitBaseClass with FixtureAnyF override def withFixture(test: OneArgTest): Outcome = { val setup = init(tags = test.tags) import setup._ - val channelConfig = ChannelConfig.standard - val channelFlags = ChannelFlags(announceChannel = false) - val (aliceParams, bobParams, channelType) = computeFeatures(setup, test.tags, channelFlags) - val pushMsat = if (test.tags.contains(ChannelStateTestsTags.NoPushAmount)) 0.msat else TestConstants.initiatorPushAmount - val aliceInit = Init(aliceParams.initFeatures) - val bobInit = Init(bobParams.initFeatures) - + val channelParams = computeChannelParams(setup, test.tags) + val pushAmount = if (test.tags.contains(ChannelStateTestsTags.NoPushAmount)) 0.msat else TestConstants.initiatorPushAmount within(30 seconds) { val listener = TestProbe() alice.underlying.system.eventStream.subscribe(listener.ref, classOf[TransactionPublished]) @@ -60,10 +56,9 @@ class WaitForFundingConfirmedStateSpec extends TestKitBaseClass with FixtureAnyF alice.underlying.system.eventStream.subscribe(listener.ref, classOf[ChannelClosed]) bob.underlying.system.eventStream.subscribe(listener.ref, classOf[ChannelAborted]) bob.underlying.system.eventStream.subscribe(listener.ref, classOf[ChannelClosed]) - val commitTxFeerate = if (test.tags.contains(ChannelStateTestsTags.AnchorOutputs) || test.tags.contains(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) TestConstants.anchorOutputsFeeratePerKw else TestConstants.feeratePerKw - alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, TestConstants.fundingSatoshis, dualFunded = false, commitTxFeerate, TestConstants.feeratePerKw, fundingTxFeeBudget_opt = None, Some(pushMsat), requireConfirmedInputs = false, requestFunding_opt = None, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType, replyTo = aliceOpenReplyTo.ref.toTyped) + alice ! channelParams.initChannelAlice(TestConstants.fundingSatoshis, pushAmount_opt = Some(pushAmount)) alice2blockchain.expectMsgType[TxPublisher.SetChannelId] - bob ! INPUT_INIT_CHANNEL_NON_INITIATOR(ByteVector32.Zeroes, None, dualFunded = false, None, requireConfirmedInputs = false, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) + bob ! channelParams.initChannelBob() bob2blockchain.expectMsgType[TxPublisher.SetChannelId] alice2bob.expectMsgType[OpenChannel] alice2bob.forward(bob) @@ -230,10 +225,10 @@ class WaitForFundingConfirmedStateSpec extends TestKitBaseClass with FixtureAnyF awaitCond(bob.stateName == CLOSED) } - test("recv WatchFundingSpentTriggered (remote commit)", Tag(ChannelStateTestsTags.StaticRemoteKey), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + test("recv WatchFundingSpentTriggered (remote commit)") { f => import f._ // bob publishes his commitment tx - val tx = bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val tx = bob.signCommitTx() alice ! WatchFundingSpentTriggered(tx) assert(listener.expectMsgType[TransactionPublished].tx == tx) alice2blockchain.expectMsgType[TxPublisher.PublishReplaceableTx] @@ -252,18 +247,16 @@ class WaitForFundingConfirmedStateSpec extends TestKitBaseClass with FixtureAnyF test("recv Error") { f => import f._ - val tx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val tx = alice.signCommitTx() alice ! Error(ByteVector32.Zeroes, "oops") awaitCond(alice.stateName == CLOSING) listener.expectMsgType[ChannelAborted] - assert(alice2blockchain.expectMsgType[TxPublisher.PublishFinalTx].tx.txid == tx.txid) - alice2blockchain.expectMsgType[TxPublisher.PublishTx] // claim-main-delayed - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == tx.txid) + alice2blockchain.expectFinalTxPublished(tx.txid) } test("recv Error (nothing at stake)", Tag(ChannelStateTestsTags.NoPushAmount)) { f => import f._ - val tx = bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val tx = bob.signCommitTx() bob ! Error(ByteVector32.Zeroes, "please help me recover my funds") // We have nothing at stake, but we publish our commitment to help our peer recover their funds more quickly. awaitCond(bob.stateName == CLOSING) @@ -286,13 +279,11 @@ class WaitForFundingConfirmedStateSpec extends TestKitBaseClass with FixtureAnyF test("recv CMD_FORCECLOSE") { f => import f._ val sender = TestProbe() - val tx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val tx = alice.signCommitTx() alice ! CMD_FORCECLOSE(sender.ref) awaitCond(alice.stateName == CLOSING) listener.expectMsgType[ChannelAborted] - assert(alice2blockchain.expectMsgType[TxPublisher.PublishFinalTx].tx.txid == tx.txid) - alice2blockchain.expectMsgType[TxPublisher.PublishTx] // claim-main-delayed - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == tx.txid) + alice2blockchain.expectFinalTxPublished(tx.txid) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalQuiescentStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalQuiescentStateSpec.scala index 0804373785..668f445eef 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalQuiescentStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalQuiescentStateSpec.scala @@ -22,15 +22,17 @@ import akka.testkit.{TestFSMRef, TestProbe} import fr.acinq.bitcoin.scalacompat.{SatoshiLong, Script} import fr.acinq.eclair.TestConstants.Bob import fr.acinq.eclair.blockchain.CurrentBlockHeight -import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ import fr.acinq.eclair.blockchain.fee.FeeratePerKw import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.channel.fund.InteractiveTxBuilder -import fr.acinq.eclair.channel.publish.TxPublisher.{PublishFinalTx, PublishTx} +import fr.acinq.eclair.channel.states.ChannelStateTestsBase.PimpTestFSM import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags} import fr.acinq.eclair.io.Peer import fr.acinq.eclair.payment.relay.Relayer.RelayForward +import fr.acinq.eclair.reputation.Reputation +import fr.acinq.eclair.testutils.PimpTestProbe.convert +import fr.acinq.eclair.transactions.Transactions._ import fr.acinq.eclair.wire.protocol._ import fr.acinq.eclair.{channel, _} import org.scalatest.Outcome @@ -67,8 +69,8 @@ class NormalQuiescentStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteL private def reconnect(f: FixtureParam): Unit = { import f._ - val aliceInit = Init(alice.stateData.asInstanceOf[ChannelDataWithCommitments].commitments.params.localParams.initFeatures) - val bobInit = Init(bob.stateData.asInstanceOf[ChannelDataWithCommitments].commitments.params.localParams.initFeatures) + val aliceInit = Init(alice.commitments.localChannelParams.initFeatures) + val bobInit = Init(bob.commitments.localChannelParams.initFeatures) alice ! INPUT_RECONNECTED(alice2bob.ref, aliceInit, bobInit) bob ! INPUT_RECONNECTED(bob2alice.ref, bobInit, aliceInit) alice2bob.expectMsgType[ChannelReestablish] @@ -95,7 +97,7 @@ class NormalQuiescentStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteL val sender = TestProbe() val scriptPubKey = Script.write(Script.pay2wpkh(randomKey().publicKey)) - val cmd = CMD_SPLICE(sender.ref, spliceIn_opt = Some(SpliceIn(500_000 sat)), spliceOut_opt = Some(SpliceOut(100_000 sat, scriptPubKey)), requestFunding_opt = None) + val cmd = CMD_SPLICE(sender.ref, spliceIn_opt = Some(SpliceIn(500_000 sat)), spliceOut_opt = Some(SpliceOut(100_000 sat, scriptPubKey)), requestFunding_opt = None, channelType_opt = None) alice ! cmd alice2bob.expectMsgType[Stfu] if (!sendInitialStfu) { @@ -115,7 +117,7 @@ class NormalQuiescentStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteL import f._ // we have an unsigned htlc in our local changes addHtlc(50_000_000 msat, alice, bob, alice2bob, bob2alice) - alice ! CMD_SPLICE(TestProbe().ref, spliceIn_opt = Some(SpliceIn(50_000 sat)), spliceOut_opt = None, requestFunding_opt = None) + alice ! CMD_SPLICE(TestProbe().ref, spliceIn_opt = Some(SpliceIn(50_000 sat)), spliceOut_opt = None, requestFunding_opt = None, channelType_opt = None) alice2bob.expectNoMessage(100 millis) crossSign(alice, bob, alice2bob, bob2alice) alice2bob.expectMsgType[Stfu] @@ -146,7 +148,7 @@ class NormalQuiescentStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteL // initiator should reject commands that change the commitment once it became quiescent val sender1, sender2, sender3 = TestProbe() val cmds = Seq( - CMD_ADD_HTLC(sender1.ref, 1_000_000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, 1.0, None, localOrigin(sender1.ref)), + CMD_ADD_HTLC(sender1.ref, 1_000_000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, Reputation.Score.max, None, localOrigin(sender1.ref)), CMD_UPDATE_FEE(FeeratePerKw(100 sat), replyTo_opt = Some(sender2.ref)), CMD_CLOSE(sender3.ref, None, None) ) @@ -162,7 +164,7 @@ class NormalQuiescentStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteL // both should reject commands that change the commitment while quiescent val sender1, sender2, sender3 = TestProbe() val cmds = Seq( - CMD_ADD_HTLC(sender1.ref, 1_000_000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, 1.0, None, localOrigin(sender1.ref)), + CMD_ADD_HTLC(sender1.ref, 1_000_000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, Reputation.Score.max, None, localOrigin(sender1.ref)), CMD_UPDATE_FEE(FeeratePerKw(100 sat), replyTo_opt = Some(sender2.ref)), CMD_CLOSE(sender3.ref, None, None) ) @@ -188,8 +190,8 @@ class NormalQuiescentStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteL val (preimage, add) = addHtlc(50_000_000 msat, bob, alice, bob2alice, alice2bob) val cmd = c match { - case FulfillHtlc => CMD_FULFILL_HTLC(add.id, preimage) - case FailHtlc => CMD_FAIL_HTLC(add.id, FailureReason.EncryptedDownstreamFailure(randomBytes(252))) + case FulfillHtlc => CMD_FULFILL_HTLC(add.id, preimage, None) + case FailHtlc => CMD_FAIL_HTLC(add.id, FailureReason.EncryptedDownstreamFailure(randomBytes(252), None), None) } crossSign(bob, alice, bob2alice, alice2bob) val sender = initiateQuiescence(f, sendInitialStfu) @@ -309,7 +311,7 @@ class NormalQuiescentStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteL import f._ val (preimage, add) = addHtlc(50_000_000 msat, bob, alice, bob2alice, alice2bob) crossSign(bob, alice, bob2alice, alice2bob) - alice2relayer.expectMsg(RelayForward(add, TestConstants.Bob.nodeParams.nodeId)) + alice2relayer.expectMsg(RelayForward(add, TestConstants.Bob.nodeParams.nodeId, 0.1)) initiateQuiescence(f, sendInitialStfu = true) val forbiddenMsg = UpdateFulfillHtlc(channelId(bob), add.id, preimage) // both parties will respond to a forbidden msg while quiescent with a warning (and disconnect) @@ -350,7 +352,7 @@ class NormalQuiescentStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteL import f._ initiateQuiescence(f, sendInitialStfu = true) // have to build a htlc manually because eclair would refuse to accept this command as it's forbidden - val forbiddenMsg = UpdateAddHtlc(channelId = randomBytes32(), id = 5656, amountMsat = 50000000 msat, cltvExpiry = CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), paymentHash = randomBytes32(), onionRoutingPacket = TestConstants.emptyOnionPacket, pathKey_opt = None, confidence = 1.0, fundingFee_opt = None) + val forbiddenMsg = UpdateAddHtlc(channelId = randomBytes32(), id = 5656, amountMsat = 50000000 msat, cltvExpiry = CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), paymentHash = randomBytes32(), onionRoutingPacket = TestConstants.emptyOnionPacket, pathKey_opt = None, endorsement = Reputation.maxEndorsement, fundingFee_opt = None) // both parties will respond to a forbidden msg while quiescent with a warning (and disconnect) bob2alice.forward(alice, forbiddenMsg) alice2bob.expectMsg(Warning(channelId(alice), ForbiddenDuringSplice(channelId(alice), "UpdateAddHtlc").getMessage)) @@ -388,7 +390,7 @@ class NormalQuiescentStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteL import f._ val sender = TestProbe() - val cmd = CMD_SPLICE(sender.ref, spliceIn_opt = Some(SpliceIn(500_000 sat, pushAmount = 0 msat)), spliceOut_opt = None, requestFunding_opt = None) + val cmd = CMD_SPLICE(sender.ref, spliceIn_opt = Some(SpliceIn(500_000 sat, pushAmount = 0 msat)), spliceOut_opt = None, requestFunding_opt = None, channelType_opt = None) alice ! cmd alice2bob.expectMsgType[Stfu] bob ! cmd @@ -405,7 +407,7 @@ class NormalQuiescentStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteL addHtlc(50_000_000 msat, alice, bob, alice2bob, bob2alice) val sender = TestProbe() - val cmd = CMD_SPLICE(sender.ref, spliceIn_opt = Some(SpliceIn(500_000 sat, pushAmount = 0 msat)), spliceOut_opt = None, requestFunding_opt = None) + val cmd = CMD_SPLICE(sender.ref, spliceIn_opt = Some(SpliceIn(500_000 sat, pushAmount = 0 msat)), spliceOut_opt = None, requestFunding_opt = None, channelType_opt = None) alice ! cmd alice2bob.expectNoMessage(100 millis) // alice isn't quiescent yet bob ! cmd @@ -424,7 +426,7 @@ class NormalQuiescentStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteL addHtlc(10_000 msat, bob, alice, bob2alice, alice2bob) val sender = TestProbe() - val cmd = CMD_SPLICE(sender.ref, spliceIn_opt = Some(SpliceIn(500_000 sat, pushAmount = 0 msat)), spliceOut_opt = None, requestFunding_opt = None) + val cmd = CMD_SPLICE(sender.ref, spliceIn_opt = Some(SpliceIn(500_000 sat, pushAmount = 0 msat)), spliceOut_opt = None, requestFunding_opt = None, channelType_opt = None) alice ! cmd alice2bob.expectMsgType[Stfu] bob ! cmd @@ -444,19 +446,20 @@ class NormalQuiescentStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteL crossSign(alice, bob, alice2bob, bob2alice) initiateQuiescence(f, sendInitialStfu = true) - val aliceCommit = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit - val commitTx = aliceCommit.commitTxAndRemoteSig.commitTx.tx - assert(aliceCommit.htlcTxsAndRemoteSigs.size == 1) - val htlcTimeoutTx = aliceCommit.htlcTxsAndRemoteSigs.head.htlcTx.tx + val commitTx = alice.signCommitTx() + val htlcTxs = alice.htlcTxs() + assert(htlcTxs.size == 1) + val htlcTimeoutTx = htlcTxs.head + assert(htlcTimeoutTx.isInstanceOf[UnsignedHtlcTimeoutTx]) // the HTLC times out, alice needs to close the channel alice ! CurrentBlockHeight(add.cltvExpiry.blockHeight) - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == commitTx.txid) - alice2blockchain.expectMsgType[PublishTx] // main delayed - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == htlcTimeoutTx.txid) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == commitTx.txid) - alice2blockchain.expectMsgType[WatchTxConfirmed] // main delayed - alice2blockchain.expectMsgType[WatchOutputSpent] // htlc output + alice2blockchain.expectFinalTxPublished(commitTx.txid) + val anchorTx = alice2blockchain.expectReplaceableTxPublished[ClaimLocalAnchorTx] + val mainDelayedTx = alice2blockchain.expectFinalTxPublished("local-main-delayed") + assert(alice2blockchain.expectReplaceableTxPublished[HtlcTimeoutTx].input == htlcTimeoutTx.input) + alice2blockchain.expectWatchTxConfirmed(commitTx.txid) + alice2blockchain.expectWatchOutputsSpent(Seq(mainDelayedTx.input, anchorTx.input.outPoint, htlcTimeoutTx.input.outPoint)) alice2blockchain.expectNoMessage(100 millis) channelUpdateListener.expectMsgType[LocalChannelDown] @@ -468,35 +471,29 @@ class NormalQuiescentStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteL crossSign(alice, bob, alice2bob, bob2alice) initiateQuiescence(f, sendInitialStfu = true) - val bobCommit = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit - val commitTx = bobCommit.commitTxAndRemoteSig.commitTx.tx - assert(bobCommit.htlcTxsAndRemoteSigs.size == 1) - val htlcSuccessTx = bobCommit.htlcTxsAndRemoteSigs.head.htlcTx.tx + val commitTx = bob.signCommitTx() + val htlcTxs = bob.htlcTxs() + assert(htlcTxs.size == 1) + val htlcSuccessTx = htlcTxs.head + assert(htlcSuccessTx.isInstanceOf[UnsignedHtlcSuccessTx]) // bob does not force-close unless there is a pending preimage for the incoming htlc bob ! CurrentBlockHeight(add.cltvExpiry.blockHeight - Bob.nodeParams.channelConf.fulfillSafetyBeforeTimeout.toInt) bob2blockchain.expectNoMessage(100 millis) // bob receives the fulfill for htlc, which is ignored because the channel is quiescent - val fulfillHtlc = CMD_FULFILL_HTLC(add.id, preimage) + val fulfillHtlc = CMD_FULFILL_HTLC(add.id, preimage, None) safeSend(bob, Seq(fulfillHtlc)) // the HTLC timeout from alice is near, bob needs to close the channel to avoid an on-chain race condition bob ! CurrentBlockHeight(add.cltvExpiry.blockHeight - Bob.nodeParams.channelConf.fulfillSafetyBeforeTimeout.toInt) - // bob publishes a first set of force-close transactions - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == commitTx.txid) - bob2blockchain.expectMsgType[PublishTx] // main delayed - assert(bob2blockchain.expectMsgType[WatchTxConfirmed].txId == commitTx.txid) - bob2blockchain.expectMsgType[WatchTxConfirmed] - bob2blockchain.expectMsgType[WatchOutputSpent] // htlc output - - // when transitioning to the closing state, bob checks the pending commands DB and replays the HTLC fulfill - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == commitTx.txid) - bob2blockchain.expectMsgType[PublishTx] // main delayed - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == htlcSuccessTx.txid) - assert(bob2blockchain.expectMsgType[WatchTxConfirmed].txId == commitTx.txid) - bob2blockchain.expectMsgType[WatchTxConfirmed] // main delayed - bob2blockchain.expectMsgType[WatchOutputSpent] // htlc output + // bob publishes a set of force-close transactions, including the HTLC-success using the received preimage + bob2blockchain.expectFinalTxPublished(commitTx.txid) + val anchorTx = bob2blockchain.expectReplaceableTxPublished[ClaimLocalAnchorTx] + val mainDelayedTx = bob2blockchain.expectFinalTxPublished("local-main-delayed") + bob2blockchain.expectWatchTxConfirmed(commitTx.txid) + bob2blockchain.expectWatchOutputsSpent(Seq(mainDelayedTx.input, anchorTx.input.outPoint, htlcSuccessTx.input.outPoint)) + assert(bob2blockchain.expectReplaceableTxPublished[HtlcSuccessTx].input == htlcSuccessTx.input) bob2blockchain.expectNoMessage(100 millis) channelUpdateListener.expectMsgType[LocalChannelDown] diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalSplicesStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalSplicesStateSpec.scala index 2329b46a8e..9f997cb960 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalSplicesStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalSplicesStateSpec.scala @@ -22,25 +22,26 @@ import akka.testkit.{TestFSMRef, TestProbe} import com.softwaremill.quicklens.ModifyPimp import fr.acinq.bitcoin.ScriptFlags import fr.acinq.bitcoin.scalacompat.NumericSatoshi.abs -import fr.acinq.bitcoin.scalacompat.{ByteVector32, Satoshi, SatoshiLong, Transaction} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, Crypto, Satoshi, SatoshiLong, Transaction} import fr.acinq.eclair._ -import fr.acinq.eclair.blockchain.SingleKeyOnChainWallet import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ import fr.acinq.eclair.blockchain.fee.FeeratePerKw -import fr.acinq.eclair.channel.Helpers.Closing.{LocalClose, RemoteClose, RevokedClose} +import fr.acinq.eclair.blockchain.{NewTransaction, SingleKeyOnChainWallet} import fr.acinq.eclair.channel.LocalFundingStatus.DualFundedUnconfirmedFundingTx import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.channel.fund.InteractiveTxBuilder.FullySignedSharedTransaction -import fr.acinq.eclair.channel.publish.TxPublisher.{PublishFinalTx, PublishReplaceableTx, PublishTx, SetChannelId} +import fr.acinq.eclair.channel.publish.TxPublisher.SetChannelId import fr.acinq.eclair.channel.states.ChannelStateTestsBase.{FakeTxPublisherFactory, PimpTestFSM} import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags} import fr.acinq.eclair.db.RevokedHtlcInfoCleaner.ForgetHtlcInfos import fr.acinq.eclair.io.Peer.LiquidityPurchaseSigned import fr.acinq.eclair.payment.relay.Relayer +import fr.acinq.eclair.reputation.Reputation import fr.acinq.eclair.testutils.PimpTestProbe.convert import fr.acinq.eclair.transactions.DirectedHtlc.{incoming, outgoing} import fr.acinq.eclair.transactions.Transactions +import fr.acinq.eclair.transactions.Transactions._ import fr.acinq.eclair.wire.protocol._ import org.scalatest.Inside.inside import org.scalatest.funsuite.FixtureAnyFunSuiteLike @@ -72,9 +73,9 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik private val defaultSpliceOutScriptPubKey = hex"0020aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" - private def initiateSpliceWithoutSigs(s: TestFSMRef[ChannelState, ChannelData, Channel], r: TestFSMRef[ChannelState, ChannelData, Channel], s2r: TestProbe, r2s: TestProbe, spliceIn_opt: Option[SpliceIn], spliceOut_opt: Option[SpliceOut]): TestProbe = { + private def initiateSpliceWithoutSigs(s: TestFSMRef[ChannelState, ChannelData, Channel], r: TestFSMRef[ChannelState, ChannelData, Channel], s2r: TestProbe, r2s: TestProbe, spliceIn_opt: Option[SpliceIn], spliceOut_opt: Option[SpliceOut], channelType_opt: Option[ChannelType], sendTxComplete: Boolean): TestProbe = { val sender = TestProbe() - val cmd = CMD_SPLICE(sender.ref, spliceIn_opt, spliceOut_opt, None) + val cmd = CMD_SPLICE(sender.ref, spliceIn_opt, spliceOut_opt, None, channelType_opt) s ! cmd exchangeStfu(s, r, s2r, r2s) s2r.expectMsgType[SpliceInit] @@ -104,14 +105,18 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik } s2r.expectMsgType[TxAddOutput] s2r.forward(r) - r2s.expectMsgType[TxComplete] - r2s.forward(s) - s2r.expectMsgType[TxComplete] - s2r.forward(r) + if (sendTxComplete) { + r2s.expectMsgType[TxComplete] + r2s.forward(s) + s2r.expectMsgType[TxComplete] + s2r.forward(r) + } sender } - private def initiateSpliceWithoutSigs(f: FixtureParam, spliceIn_opt: Option[SpliceIn] = None, spliceOut_opt: Option[SpliceOut] = None): TestProbe = initiateSpliceWithoutSigs(f.alice, f.bob, f.alice2bob, f.bob2alice, spliceIn_opt, spliceOut_opt) + private def initiateSpliceWithoutSigs(f: FixtureParam, spliceIn_opt: Option[SpliceIn] = None, spliceOut_opt: Option[SpliceOut] = None, channelType_opt: Option[ChannelType] = None, sendTxComplete: Boolean = true): TestProbe = { + initiateSpliceWithoutSigs(f.alice, f.bob, f.alice2bob, f.bob2alice, spliceIn_opt, spliceOut_opt, channelType_opt, sendTxComplete) + } private def initiateRbfWithoutSigs(s: TestFSMRef[ChannelState, ChannelData, Channel], r: TestFSMRef[ChannelState, ChannelData, Channel], s2r: TestProbe, r2s: TestProbe, feerate: FeeratePerKw, sInputsCount: Int, sOutputsCount: Int, rInputsCount: Int, rOutputsCount: Int): TestProbe = { val sender = TestProbe() @@ -211,12 +216,14 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik private def exchangeSpliceSigs(f: FixtureParam, sender: TestProbe): Transaction = exchangeSpliceSigs(f.alice, f.bob, f.alice2bob, f.bob2alice, sender) - private def initiateSplice(s: TestFSMRef[ChannelState, ChannelData, Channel], r: TestFSMRef[ChannelState, ChannelData, Channel], s2r: TestProbe, r2s: TestProbe, spliceIn_opt: Option[SpliceIn], spliceOut_opt: Option[SpliceOut]): Transaction = { - val sender = initiateSpliceWithoutSigs(s, r, s2r, r2s, spliceIn_opt, spliceOut_opt) + private def initiateSplice(s: TestFSMRef[ChannelState, ChannelData, Channel], r: TestFSMRef[ChannelState, ChannelData, Channel], s2r: TestProbe, r2s: TestProbe, spliceIn_opt: Option[SpliceIn], spliceOut_opt: Option[SpliceOut], channelType_opt: Option[ChannelType]): Transaction = { + val sender = initiateSpliceWithoutSigs(s, r, s2r, r2s, spliceIn_opt, spliceOut_opt, channelType_opt, sendTxComplete = true) exchangeSpliceSigs(s, r, s2r, r2s, sender) } - private def initiateSplice(f: FixtureParam, spliceIn_opt: Option[SpliceIn] = None, spliceOut_opt: Option[SpliceOut] = None): Transaction = initiateSplice(f.alice, f.bob, f.alice2bob, f.bob2alice, spliceIn_opt, spliceOut_opt) + private def initiateSplice(f: FixtureParam, spliceIn_opt: Option[SpliceIn] = None, spliceOut_opt: Option[SpliceOut] = None, channelType_opt: Option[ChannelType] = None): Transaction = { + initiateSplice(f.alice, f.bob, f.alice2bob, f.bob2alice, spliceIn_opt, spliceOut_opt, channelType_opt) + } private def initiateRbf(f: FixtureParam, feerate: FeeratePerKw, sInputsCount: Int, sOutputsCount: Int): Transaction = { val sender = initiateRbfWithoutSigs(f, feerate, sInputsCount, sOutputsCount) @@ -241,6 +248,15 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik bob2blockchain.expectNoMessage(100 millis) } + private def checkWatchPublished(f: FixtureParam, spliceTx: Transaction): Unit = { + import f._ + + alice2blockchain.expectWatchPublished(spliceTx.txid) + alice2blockchain.expectNoMessage(100 millis) + bob2blockchain.expectWatchPublished(spliceTx.txid) + bob2blockchain.expectNoMessage(100 millis) + } + private def confirmSpliceTx(f: FixtureParam, spliceTx: Transaction): Unit = { import f._ @@ -264,6 +280,9 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik private def setupHtlcs(f: FixtureParam): TestHtlcs = { import f._ + val localBalance = alice.commitments.latest.localCommit.spec.toLocal + val remoteBalance = alice.commitments.latest.localCommit.spec.toRemote + // Concurrently add htlcs in both directions so that commit indices don't match. val adda1 = addHtlc(15_000_000 msat, alice, bob, alice2bob, bob2alice) val adda2 = addHtlc(15_000_000 msat, alice, bob, alice2bob, bob2alice) @@ -283,11 +302,9 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik bob2alice.expectMsgType[RevokeAndAck] bob2alice.forward(alice) - val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] - assert(initialState.commitments.localCommitIndex != initialState.commitments.remoteCommitIndex) - assert(initialState.commitments.latest.capacity == 1_500_000.sat) - assert(initialState.commitments.latest.localCommit.spec.toLocal == 770_000_000.msat) - assert(initialState.commitments.latest.localCommit.spec.toRemote == 665_000_000.msat) + assert(alice.commitments.localCommitIndex != alice.commitments.remoteCommitIndex) + assert(alice.commitments.latest.localCommit.spec.toLocal == localBalance - 30_000_000.msat) + assert(alice.commitments.latest.localCommit.spec.toRemote == remoteBalance - 35_000_000.msat) alice2relayer.expectMsgType[Relayer.RelayForward] alice2relayer.expectMsgType[Relayer.RelayForward] @@ -297,11 +314,11 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik TestHtlcs(Seq(adda1, adda2), Seq(addb1, addb2)) } - def spliceOutFee(f: FixtureParam, capacity: Satoshi): Satoshi = { + def spliceOutFee(f: FixtureParam, capacity: Satoshi, signedTx_opt: Option[Transaction] = None): Satoshi = { import f._ // When we only splice-out, the fees are paid by deducing them from the next funding amount. - val fundingTx = alice.stateData.asInstanceOf[ChannelDataWithCommitments].commitments.latest.localFundingStatus.signedTx_opt.get + val fundingTx = signedTx_opt.getOrElse(alice.stateData.asInstanceOf[ChannelDataWithCommitments].commitments.latest.localFundingStatus.signedTx_opt.get) val feerate = alice.nodeParams.onChainFeeConf.getFundingFeerate(alice.nodeParams.currentBitcoinCoreFeerates) val expectedMiningFee = Transactions.weight2fee(feerate, fundingTx.weight()) val actualMiningFee = capacity - alice.stateData.asInstanceOf[ChannelDataWithCommitments].commitments.latest.capacity @@ -358,7 +375,13 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik assert(initialState.commitments.latest.localCommit.spec.toLocal == 800_000_000.msat) assert(initialState.commitments.latest.localCommit.spec.toRemote == 700_000_000.msat) - initiateSplice(f, spliceIn_opt = Some(SpliceIn(500_000 sat))) + val listener = TestProbe() + alice.underlyingActor.context.system.eventStream.subscribe(listener.ref, classOf[TransactionPublished]) + alice.underlyingActor.context.system.eventStream.subscribe(listener.ref, classOf[NewTransaction]) + + val spliceTx = initiateSplice(f, spliceIn_opt = Some(SpliceIn(500_000 sat))) + assert(listener.expectMsgType[TransactionPublished].tx == spliceTx) + assert(listener.expectMsgType[NewTransaction].tx == spliceTx) assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.capacity == 2_000_000.sat) assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.spec.toLocal == 1_300_000_000.msat) @@ -366,7 +389,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik } test("recv CMD_SPLICE (splice-in, non dual-funded channel)") { () => - val f = init(tags = Set.empty, wallet_opt = Some(new SingleKeyOnChainWallet())) + val f = init(tags = Set.empty, walletA_opt = Some(new SingleKeyOnChainWallet()), walletB_opt = Some(new SingleKeyOnChainWallet())) import f._ reachNormal(f, tags = Set.empty) // we open a non dual-funded channel @@ -374,7 +397,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik bob2alice.ignoreMsg { case _: ChannelUpdate => true } awaitCond(alice.stateName == NORMAL && bob.stateName == NORMAL) val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] - assert(!initialState.commitments.params.channelFeatures.hasFeature(Features.DualFunding)) + assert(!initialState.commitments.channelParams.channelFeatures.hasFeature(Features.DualFunding)) assert(initialState.commitments.latest.capacity == 1_000_000.sat) assert(initialState.commitments.latest.localCommit.spec.toLocal == 800_000_000.msat) assert(initialState.commitments.latest.localCommit.spec.toRemote == 200_000_000.msat) @@ -385,7 +408,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik // We can splice on top of a non dual-funded channel. initiateSplice(f, spliceIn_opt = Some(SpliceIn(500_000 sat))) val postSpliceState = alice.stateData.asInstanceOf[DATA_NORMAL] - assert(!postSpliceState.commitments.params.channelFeatures.hasFeature(Features.DualFunding)) + assert(!postSpliceState.commitments.channelParams.channelFeatures.hasFeature(Features.DualFunding)) assert(postSpliceState.commitments.latest.capacity == 1_500_000.sat) assert(postSpliceState.commitments.latest.localCommit.spec.toLocal == 1_300_000_000.msat) assert(postSpliceState.commitments.latest.localCommit.spec.toRemote == 200_000_000.msat) @@ -399,7 +422,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik val sender = TestProbe() val fundingRequest = LiquidityAds.RequestFunding(400_000 sat, TestConstants.defaultLiquidityRates.fundingRates.head, LiquidityAds.PaymentDetails.FromChannelBalance) - val cmd = CMD_SPLICE(sender.ref, Some(SpliceIn(500_000 sat)), None, Some(fundingRequest)) + val cmd = CMD_SPLICE(sender.ref, Some(SpliceIn(500_000 sat)), None, Some(fundingRequest), None) alice ! cmd exchangeStfu(alice, bob, alice2bob, bob2alice) @@ -448,7 +471,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik val sender = TestProbe() val fundingRequest = LiquidityAds.RequestFunding(400_000 sat, TestConstants.defaultLiquidityRates.fundingRates.head, LiquidityAds.PaymentDetails.FromChannelBalance) - val cmd = CMD_SPLICE(sender.ref, Some(SpliceIn(500_000 sat)), None, Some(fundingRequest)) + val cmd = CMD_SPLICE(sender.ref, Some(SpliceIn(500_000 sat)), None, Some(fundingRequest), None) alice ! cmd exchangeStfu(alice, bob, alice2bob, bob2alice) @@ -475,7 +498,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik val sender = TestProbe() val fundingRequest = LiquidityAds.RequestFunding(5_000 sat, TestConstants.defaultLiquidityRates.fundingRates.head, LiquidityAds.PaymentDetails.FromChannelBalance) - val cmd = CMD_SPLICE(sender.ref, Some(SpliceIn(500_000 sat)), None, Some(fundingRequest)) + val cmd = CMD_SPLICE(sender.ref, Some(SpliceIn(500_000 sat)), None, Some(fundingRequest), None) alice ! cmd exchangeStfu(alice, bob, alice2bob, bob2alice) @@ -492,7 +515,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik val sender = TestProbe() val fundingRequest = LiquidityAds.RequestFunding(100_000 sat, LiquidityAds.FundingRate(10_000 sat, 200_000 sat, 0, 0, 0 sat, 0 sat), LiquidityAds.PaymentDetails.FromChannelBalance) - val cmd = CMD_SPLICE(sender.ref, Some(SpliceIn(500_000 sat)), None, Some(fundingRequest)) + val cmd = CMD_SPLICE(sender.ref, Some(SpliceIn(500_000 sat)), None, Some(fundingRequest), None) alice ! cmd exchangeStfu(alice, bob, alice2bob, bob2alice) @@ -511,7 +534,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik // Alice requests a lot of funding, but she doesn't have enough balance to pay the corresponding fee. assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.spec.toLocal == 800_000_000.msat) val fundingRequest = LiquidityAds.RequestFunding(5_000_000 sat, TestConstants.defaultLiquidityRates.fundingRates.head, LiquidityAds.PaymentDetails.FromChannelBalance) - val cmd = CMD_SPLICE(sender.ref, None, Some(SpliceOut(750_000 sat, defaultSpliceOutScriptPubKey)), Some(fundingRequest)) + val cmd = CMD_SPLICE(sender.ref, None, Some(SpliceOut(750_000 sat, defaultSpliceOutScriptPubKey)), Some(fundingRequest), None) alice ! cmd exchangeStfu(alice, bob, alice2bob, bob2alice) @@ -595,11 +618,11 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik val commitment = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest assert(commitment.localCommit.spec.toLocal == 770_000_000.msat) assert(commitment.localChannelReserve == 15_000.sat) - val commitFees = Transactions.commitTxTotalCost(commitment.remoteParams.dustLimit, commitment.remoteCommit.spec, commitment.params.commitmentFormat) + val commitFees = Transactions.commitTxTotalCost(commitment.remoteCommitParams.dustLimit, commitment.remoteCommit.spec, commitment.commitmentFormat) assert(commitFees < 15_000.sat) val sender = TestProbe() - val cmd = CMD_SPLICE(sender.ref, spliceIn_opt = None, Some(SpliceOut(760_000 sat, defaultSpliceOutScriptPubKey)), requestFunding_opt = None) + val cmd = CMD_SPLICE(sender.ref, spliceIn_opt = None, Some(SpliceOut(760_000 sat, defaultSpliceOutScriptPubKey)), requestFunding_opt = None, channelType_opt = None) alice ! cmd exchangeStfu(f) sender.expectMsgType[RES_FAILURE[_, _]] @@ -615,11 +638,11 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik val commitment = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest assert(commitment.localCommit.spec.toLocal == 650_000_000.msat) assert(commitment.localChannelReserve == 15_000.sat) - val commitFees = Transactions.commitTxTotalCost(commitment.remoteParams.dustLimit, commitment.remoteCommit.spec, commitment.params.commitmentFormat) - assert(commitFees > 20_000.sat) + val commitFees = Transactions.commitTxTotalCost(commitment.remoteCommitParams.dustLimit, commitment.remoteCommit.spec, commitment.commitmentFormat) + assert(commitFees > 7_000.sat) val sender = TestProbe() - val cmd = CMD_SPLICE(sender.ref, spliceIn_opt = None, Some(SpliceOut(630_000 sat, defaultSpliceOutScriptPubKey)), requestFunding_opt = None) + val cmd = CMD_SPLICE(sender.ref, spliceIn_opt = None, Some(SpliceOut(643_000 sat, defaultSpliceOutScriptPubKey)), requestFunding_opt = None, channelType_opt = None) alice ! cmd exchangeStfu(f) sender.expectMsgType[RES_FAILURE[_, _]] @@ -629,7 +652,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik import f._ val sender = TestProbe() - val cmd = CMD_SPLICE(sender.ref, spliceIn_opt = Some(SpliceIn(500_000 sat)), spliceOut_opt = None, requestFunding_opt = None) + val cmd = CMD_SPLICE(sender.ref, spliceIn_opt = Some(SpliceIn(500_000 sat)), spliceOut_opt = None, requestFunding_opt = None, channelType_opt = None) alice ! cmd exchangeStfu(f) // we tweak the feerate @@ -650,7 +673,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik val sender = TestProbe() val bobBalance = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.spec.toLocal - alice ! CMD_SPLICE(sender.ref, spliceIn_opt = Some(SpliceIn(100_000 sat)), spliceOut_opt = None, requestFunding_opt = None) + alice ! CMD_SPLICE(sender.ref, spliceIn_opt = Some(SpliceIn(100_000 sat)), spliceOut_opt = None, requestFunding_opt = None, channelType_opt = None) exchangeStfu(f) val spliceInit = alice2bob.expectMsgType[SpliceInit] alice2bob.forward(bob, spliceInit) @@ -675,7 +698,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik crossSign(bob, alice, bob2alice, alice2bob) // Bob makes a large splice: Alice doesn't meet the new reserve requirements, but she met the previous one, so we allow this. - initiateSplice(bob, alice, bob2alice, alice2bob, spliceIn_opt = Some(SpliceIn(4_000_000 sat)), spliceOut_opt = None) + initiateSplice(bob, alice, bob2alice, alice2bob, spliceIn_opt = Some(SpliceIn(4_000_000 sat)), spliceOut_opt = None, channelType_opt = None) val postSpliceState = alice.stateData.asInstanceOf[DATA_NORMAL] assert(postSpliceState.commitments.latest.localCommit.spec.toLocal < postSpliceState.commitments.latest.localChannelReserve) @@ -703,7 +726,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik initiateRbf(f, FeeratePerKw(15_000 sat), sInputsCount = 2, sOutputsCount = 1) val probe = TestProbe() - alice ! CMD_SPLICE(probe.ref, Some(SpliceIn(250_000 sat)), None, None) + alice ! CMD_SPLICE(probe.ref, Some(SpliceIn(250_000 sat)), None, None, None) assert(probe.expectMsgType[RES_FAILURE[_, ChannelException]].t.isInstanceOf[InvalidSpliceWithUnconfirmedTx]) bob2alice.forward(alice, Stfu(alice.stateData.channelId, initiator = true)) @@ -721,7 +744,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik // We allow initiating such splice... val probe = TestProbe() - alice ! CMD_SPLICE(probe.ref, Some(SpliceIn(250_000 sat)), None, None) + alice ! CMD_SPLICE(probe.ref, Some(SpliceIn(250_000 sat)), None, None, None) alice2bob.expectMsgType[Stfu] alice2bob.forward(bob) bob2alice.expectMsgType[Stfu] @@ -739,6 +762,26 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik resolveHtlcs(f, htlcs) } + test("recv CMD_SPLICE (accepting upgrade channel to taproot)", Tag(ChannelStateTestsTags.AnchorOutputsPhoenix)) { f => + import f._ + + val htlcs = setupHtlcs(f) + initiateSplice(f, spliceIn_opt = Some(SpliceIn(400_000 sat)), channelType_opt = Some(ChannelTypes.SimpleTaprootChannelsPhoenix)) + assert(alice.commitments.active.head.commitmentFormat == PhoenixSimpleTaprootChannelCommitmentFormat) + assert(alice.commitments.active.last.commitmentFormat == UnsafeLegacyAnchorOutputsCommitmentFormat) + resolveHtlcs(f, htlcs) + } + + test("recv CMD_SPLICE (rejecting upgrade channel to taproot)") { f => + import f._ + + val htlcs = setupHtlcs(f) + initiateSplice(f, spliceIn_opt = Some(SpliceIn(400_000 sat)), channelType_opt = Some(ChannelTypes.SimpleTaprootChannelsPhoenix)) + assert(alice.commitments.active.head.commitmentFormat == ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) + assert(alice.commitments.active.last.commitmentFormat == ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) + resolveHtlcs(f, htlcs) + } + test("recv CMD_BUMP_FUNDING_FEE (splice-in + splice-out)") { f => import f._ @@ -787,7 +830,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik } // We can keep doing more splice transactions now that one of the previous transactions confirmed. - initiateSplice(bob, alice, bob2alice, alice2bob, Some(SpliceIn(100_000 sat)), None) + initiateSplice(bob, alice, bob2alice, alice2bob, Some(SpliceIn(100_000 sat)), None, None) } test("recv CMD_BUMP_FUNDING_FEE (splice-in + splice-out from non-initiator)") { f => @@ -798,7 +841,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik confirmSpliceTx(f, spliceTx1) // Bob initiates a second splice that spends the first splice. - val spliceTx2 = initiateSplice(bob, alice, bob2alice, alice2bob, spliceIn_opt = Some(SpliceIn(50_000 sat)), spliceOut_opt = Some(SpliceOut(25_000 sat, defaultSpliceOutScriptPubKey))) + val spliceTx2 = initiateSplice(bob, alice, bob2alice, alice2bob, spliceIn_opt = Some(SpliceIn(50_000 sat)), spliceOut_opt = Some(SpliceOut(25_000 sat, defaultSpliceOutScriptPubKey)), channelType_opt = None) assert(spliceTx2.txIn.exists(_.outPoint.txid == spliceTx1.txid)) // Alice cannot RBF her first splice, so she RBFs Bob's splice instead. @@ -814,7 +857,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik // Alice initiates a splice-in with a liquidity purchase. val sender = TestProbe() val fundingRequest = LiquidityAds.RequestFunding(400_000 sat, TestConstants.defaultLiquidityRates.fundingRates.head, LiquidityAds.PaymentDetails.FromChannelBalance) - alice ! CMD_SPLICE(sender.ref, Some(SpliceIn(500_000 sat)), None, Some(fundingRequest)) + alice ! CMD_SPLICE(sender.ref, Some(SpliceIn(500_000 sat)), None, Some(fundingRequest), None) exchangeStfu(alice, bob, alice2bob, bob2alice) inside(alice2bob.expectMsgType[SpliceInit]) { msg => assert(msg.fundingContribution == 500_000.sat) @@ -964,7 +1007,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik assert(alice2bob.expectMsgType[TxAbort].toAscii.contains("transaction is already confirmed")) } - test("recv CMD_BUMP_FUNDING_FEE (transaction is using 0-conf)", Tag(ChannelStateTestsTags.ZeroConf), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + test("recv CMD_BUMP_FUNDING_FEE (transaction is using 0-conf)", Tag(ChannelStateTestsTags.ZeroConf)) { f => import f._ val spliceTx = initiateSplice(f, spliceIn_opt = Some(SpliceIn(500_000 sat))) @@ -985,7 +1028,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik import f._ val sender = TestProbe() - alice ! CMD_SPLICE(sender.ref, spliceIn_opt = None, spliceOut_opt = Some(SpliceOut(50_000 sat, defaultSpliceOutScriptPubKey)), requestFunding_opt = None) + alice ! CMD_SPLICE(sender.ref, spliceIn_opt = None, spliceOut_opt = Some(SpliceOut(50_000 sat, defaultSpliceOutScriptPubKey)), requestFunding_opt = None, channelType_opt = None) exchangeStfu(f) alice2bob.expectMsgType[SpliceInit] alice2bob.forward(bob) @@ -1010,7 +1053,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik import f._ val sender = TestProbe() - alice ! CMD_SPLICE(sender.ref, spliceIn_opt = None, spliceOut_opt = Some(SpliceOut(50_000 sat, defaultSpliceOutScriptPubKey)), requestFunding_opt = None) + alice ! CMD_SPLICE(sender.ref, spliceIn_opt = None, spliceOut_opt = Some(SpliceOut(50_000 sat, defaultSpliceOutScriptPubKey)), requestFunding_opt = None, channelType_opt = None) exchangeStfu(f) alice2bob.expectMsgType[SpliceInit] alice2bob.forward(bob) @@ -1046,7 +1089,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik import f._ val sender = TestProbe() - alice ! CMD_SPLICE(sender.ref, spliceIn_opt = Some(SpliceIn(50_000 sat)), spliceOut_opt = None, requestFunding_opt = None) + alice ! CMD_SPLICE(sender.ref, spliceIn_opt = Some(SpliceIn(50_000 sat)), spliceOut_opt = None, requestFunding_opt = None, channelType_opt = None) exchangeStfu(f) alice2bob.expectMsgType[SpliceInit] alice2bob.forward(bob) @@ -1096,7 +1139,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik val sender = TestProbe() // command for a large payment (larger than local balance pre-slice) - val cmd = CMD_ADD_HTLC(sender.ref, 1_000_000_000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, 1.0, None, localOrigin(sender.ref)) + val cmd = CMD_ADD_HTLC(sender.ref, 1_000_000_000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, Reputation.Score.max, None, localOrigin(sender.ref)) // first attempt at payment fails (not enough balance) alice ! cmd sender.expectMsgType[RES_ADD_FAILED[_]] @@ -1140,8 +1183,8 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik val fundingTx1 = initiateSplice(f, spliceIn_opt = Some(SpliceIn(500_000 sat))) checkWatchConfirmed(f, fundingTx1) - val commitAlice1 = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.active.head.localCommit.commitTxAndRemoteSig.commitTx.tx - val commitBob1 = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.active.head.localCommit.commitTxAndRemoteSig.commitTx.tx + val commitAlice1 = alice.signCommitTx() + val commitBob1 = bob.signCommitTx() // Bob sees the first splice confirm, but Alice doesn't. bob ! WatchFundingConfirmedTriggered(BlockHeight(400000), 42, fundingTx1) @@ -1152,8 +1195,8 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik // Alice creates another splice spending the first splice. val fundingTx2 = initiateSplice(f, spliceIn_opt = Some(SpliceIn(500_000 sat))) checkWatchConfirmed(f, fundingTx2) - val commitAlice2 = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.active.head.localCommit.commitTxAndRemoteSig.commitTx.tx - val commitBob2 = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.active.head.localCommit.commitTxAndRemoteSig.commitTx.tx + val commitAlice2 = alice.signCommitTx() + val commitBob2 = bob.signCommitTx() assert(commitAlice1.txid != commitAlice2.txid) assert(commitBob1.txid != commitBob2.txid) @@ -1179,7 +1222,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.inactive.map(_.fundingTxIndex) == Seq.empty) } - test("splice local/remote locking (zero-conf)", Tag(ChannelStateTestsTags.NoMaxHtlcValueInFlight), Tag(ChannelStateTestsTags.ZeroConf), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + test("splice local/remote locking (zero-conf)", Tag(ChannelStateTestsTags.NoMaxHtlcValueInFlight), Tag(ChannelStateTestsTags.ZeroConf)) { f => import f._ val fundingTx1 = initiateSplice(f, spliceIn_opt = Some(SpliceIn(250_000 sat))) @@ -1236,8 +1279,8 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik // Alice creates another splice spending the first splice. val fundingTx2 = initiateSplice(f, spliceIn_opt = Some(SpliceIn(500_000 sat))) checkWatchConfirmed(f, fundingTx2) - val commitAlice2 = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.active.head.localCommit.commitTxAndRemoteSig.commitTx.tx - val commitBob2 = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.active.head.localCommit.commitTxAndRemoteSig.commitTx.tx + val commitAlice2 = alice.signCommitTx() + val commitBob2 = bob.signCommitTx() // Alice sees the second splice confirm. alice ! WatchFundingConfirmedTriggered(BlockHeight(400000), 42, fundingTx2) @@ -1348,7 +1391,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik } } - test("recv announcement_signatures", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), Tag(ChannelStateTestsTags.ChannelsPublic), Tag(ChannelStateTestsTags.DoNotInterceptGossip)) { f => + test("recv announcement_signatures", Tag(ChannelStateTestsTags.ChannelsPublic), Tag(ChannelStateTestsTags.DoNotInterceptGossip)) { f => import f._ val aliceListener = TestProbe() @@ -1441,7 +1484,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik assert(alice.stateData.asInstanceOf[DATA_NORMAL].lastAnnouncement_opt.contains(spliceAnn2)) } - test("recv announcement_signatures (after restart)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), Tag(ChannelStateTestsTags.ChannelsPublic), Tag(ChannelStateTestsTags.DoNotInterceptGossip)) { f => + test("recv announcement_signatures (after restart)", Tag(ChannelStateTestsTags.ChannelsPublic), Tag(ChannelStateTestsTags.DoNotInterceptGossip)) { f => import f._ val aliceListener = TestProbe() @@ -1512,71 +1555,92 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik import f._ initiateSplice(f, spliceIn_opt = Some(SpliceIn(500_000 sat))) val sender = TestProbe() - alice ! CMD_ADD_HTLC(sender.ref, 500_000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, 1.0, None, localOrigin(sender.ref)) + alice ! CMD_ADD_HTLC(sender.ref, 500_000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, Reputation.Score.max, None, localOrigin(sender.ref)) sender.expectMsgType[RES_SUCCESS[CMD_ADD_HTLC]] alice2bob.expectMsgType[UpdateAddHtlc] alice2bob.forward(bob) alice ! CMD_SIGN() - val sigA1 = alice2bob.expectMsgType[CommitSig] - assert(sigA1.batchSize == 2) - alice2bob.forward(bob) - val sigA2 = alice2bob.expectMsgType[CommitSig] - assert(sigA2.batchSize == 2) - alice2bob.forward(bob) + val sigsA = alice2bob.expectMsgType[CommitSigBatch] + assert(sigsA.batchSize == 2) + alice2bob.forward(bob, sigsA) bob2alice.expectMsgType[RevokeAndAck] bob2alice.forward(alice) - val sigB1 = bob2alice.expectMsgType[CommitSig] - assert(sigB1.batchSize == 2) - bob2alice.forward(alice) - val sigB2 = bob2alice.expectMsgType[CommitSig] - assert(sigB2.batchSize == 2) - bob2alice.forward(alice) + val sigsB = bob2alice.expectMsgType[CommitSigBatch] + assert(sigsB.batchSize == 2) + bob2alice.forward(alice, sigsB) alice2bob.expectMsgType[RevokeAndAck] alice2bob.forward(bob) awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.active.forall(_.localCommit.spec.htlcs.size == 1)) awaitCond(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.active.forall(_.localCommit.spec.htlcs.size == 1)) } - test("recv CMD_ADD_HTLC with multiple commitments and reconnect") { f => + test("recv CMD_ADD_HTLC with multiple commitments (missing nonces)", Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => import f._ - initiateSplice(f, spliceIn_opt = Some(SpliceIn(500_000 sat))) + val spliceTx = initiateSplice(f, spliceIn_opt = Some(SpliceIn(500_000 sat))) + bob2blockchain.expectWatchFundingConfirmed(spliceTx.txid) val sender = TestProbe() - alice ! CMD_ADD_HTLC(sender.ref, 500_000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, 1.0, None, localOrigin(sender.ref)) + alice ! CMD_ADD_HTLC(sender.ref, 500_000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, Reputation.Score.max, None, localOrigin(sender.ref)) sender.expectMsgType[RES_SUCCESS[CMD_ADD_HTLC]] alice2bob.expectMsgType[UpdateAddHtlc] alice2bob.forward(bob) alice ! CMD_SIGN() - assert(alice2bob.expectMsgType[CommitSig].batchSize == 2) - assert(alice2bob.expectMsgType[CommitSig].batchSize == 2) + val sigsA = alice2bob.expectMsgType[CommitSigBatch] + assert(sigsA.batchSize == 2) + alice2bob.forward(bob, sigsA) + bob2alice.expectMsgType[RevokeAndAck] + bob2alice.forward(alice) + val sigsB = bob2alice.expectMsgType[CommitSigBatch] + assert(sigsB.batchSize == 2) + bob2alice.forward(alice, sigsB) + val revA = alice2bob.expectMsgType[RevokeAndAck] + assert(revA.nextCommitNonces.size == 2) + val missingNonce = RevokeAndAckTlv.NextLocalNoncesTlv(revA.nextCommitNonces.toSeq.take(1)) + alice2bob.forward(bob, revA.copy(tlvStream = TlvStream(revA.tlvStream.records.filterNot(_.isInstanceOf[RevokeAndAckTlv.NextLocalNoncesTlv]) + missingNonce))) + bob2alice.expectMsgType[Error] + val commitTx = bob2blockchain.expectFinalTxPublished("commit-tx").tx + Transaction.correctlySpends(commitTx, Seq(spliceTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + } + + test("recv CMD_ADD_HTLC with multiple commitments and reconnect", Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => + import f._ + initiateSplice(f, spliceIn_opt = Some(SpliceIn(500_000 sat))) + val sender = TestProbe() + val preimage = randomBytes32() + alice ! CMD_ADD_HTLC(sender.ref, 500_000 msat, Crypto.sha256(preimage), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, Reputation.Score.max, None, localOrigin(sender.ref)) + sender.expectMsgType[RES_SUCCESS[CMD_ADD_HTLC]] + val add = alice2bob.expectMsgType[UpdateAddHtlc] + alice2bob.forward(bob) + alice ! CMD_SIGN() + assert(alice2bob.expectMsgType[CommitSigBatch].batchSize == 2) // Bob disconnects before receiving Alice's commit_sig. disconnect(f) reconnect(f) alice2bob.expectMsgType[UpdateAddHtlc] alice2bob.forward(bob) - assert(alice2bob.expectMsgType[CommitSig].batchSize == 2) - alice2bob.forward(bob) - assert(alice2bob.expectMsgType[CommitSig].batchSize == 2) - alice2bob.forward(bob) - bob2alice.expectMsgType[RevokeAndAck] + val sigsA = alice2bob.expectMsgType[CommitSigBatch] + assert(sigsA.batchSize == 2) + alice2bob.forward(bob, sigsA) + assert(bob2alice.expectMsgType[RevokeAndAck].nextCommitNonces.size == 2) bob2alice.forward(alice) - assert(bob2alice.expectMsgType[CommitSig].batchSize == 2) - bob2alice.forward(alice) - assert(bob2alice.expectMsgType[CommitSig].batchSize == 2) - bob2alice.forward(alice) - alice2bob.expectMsgType[RevokeAndAck] + val sigsB = bob2alice.expectMsgType[CommitSigBatch] + assert(sigsB.batchSize == 2) + bob2alice.forward(alice, sigsB) + assert(alice2bob.expectMsgType[RevokeAndAck].nextCommitNonces.size == 2) alice2bob.forward(bob) awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.active.forall(_.localCommit.spec.htlcs.size == 1)) awaitCond(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.active.forall(_.localCommit.spec.htlcs.size == 1)) + fulfillHtlc(add.id, preimage, bob, alice, bob2alice, alice2bob) + crossSign(bob, alice, bob2alice, alice2bob) } test("recv CMD_ADD_HTLC while a splice is requested") { f => import f._ val sender = TestProbe() - val cmd = CMD_SPLICE(sender.ref, spliceIn_opt = Some(SpliceIn(500_000 sat, pushAmount = 0 msat)), spliceOut_opt = None, requestFunding_opt = None) + val cmd = CMD_SPLICE(sender.ref, spliceIn_opt = Some(SpliceIn(500_000 sat, pushAmount = 0 msat)), spliceOut_opt = None, requestFunding_opt = None, channelType_opt = None) alice ! cmd exchangeStfu(f) alice2bob.expectMsgType[SpliceInit] - alice ! CMD_ADD_HTLC(sender.ref, 500000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, 1.0, None, localOrigin(sender.ref)) + alice ! CMD_ADD_HTLC(sender.ref, 500000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, Reputation.Score.max, None, localOrigin(sender.ref)) sender.expectMsgType[RES_ADD_FAILED[_]] alice2bob.expectNoMessage(100 millis) } @@ -1584,7 +1648,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik test("recv CMD_ADD_HTLC while a splice is in progress") { f => import f._ val sender = TestProbe() - val cmd = CMD_SPLICE(sender.ref, spliceIn_opt = Some(SpliceIn(500_000 sat, pushAmount = 0 msat)), spliceOut_opt = None, requestFunding_opt = None) + val cmd = CMD_SPLICE(sender.ref, spliceIn_opt = Some(SpliceIn(500_000 sat, pushAmount = 0 msat)), spliceOut_opt = None, requestFunding_opt = None, channelType_opt = None) alice ! cmd exchangeStfu(f) alice2bob.expectMsgType[SpliceInit] @@ -1592,7 +1656,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik bob2alice.expectMsgType[SpliceAck] bob2alice.forward(alice) alice2bob.expectMsgType[TxAddInput] - alice ! CMD_ADD_HTLC(sender.ref, 500000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, 1.0, None, localOrigin(sender.ref)) + alice ! CMD_ADD_HTLC(sender.ref, 500000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, Reputation.Score.max, None, localOrigin(sender.ref)) sender.expectMsgType[RES_ADD_FAILED[_]] alice2bob.expectNoMessage(100 millis) } @@ -1600,7 +1664,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik test("recv UpdateAddHtlc while a splice is in progress") { f => import f._ val sender = TestProbe() - val cmd = CMD_SPLICE(sender.ref, spliceIn_opt = Some(SpliceIn(500_000 sat, pushAmount = 0 msat)), spliceOut_opt = None, requestFunding_opt = None) + val cmd = CMD_SPLICE(sender.ref, spliceIn_opt = Some(SpliceIn(500_000 sat, pushAmount = 0 msat)), spliceOut_opt = None, requestFunding_opt = None, channelType_opt = None) alice ! cmd exchangeStfu(f) alice2bob.expectMsgType[SpliceInit] @@ -1610,7 +1674,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik alice2bob.expectMsgType[TxAddInput] // have to build a htlc manually because eclair would refuse to accept this command as it's forbidden - val fakeHtlc = UpdateAddHtlc(channelId = randomBytes32(), id = 5656, amountMsat = 50000000 msat, cltvExpiry = CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), paymentHash = randomBytes32(), onionRoutingPacket = TestConstants.emptyOnionPacket, pathKey_opt = None, confidence = 1.0, fundingFee_opt = None) + val fakeHtlc = UpdateAddHtlc(channelId = randomBytes32(), id = 5656, amountMsat = 50000000 msat, cltvExpiry = CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), paymentHash = randomBytes32(), onionRoutingPacket = TestConstants.emptyOnionPacket, pathKey_opt = None, endorsement = Reputation.maxEndorsement, fundingFee_opt = None) bob2alice.forward(alice, fakeHtlc) // alice returns a warning and schedules a disconnect after receiving UpdateAddHtlc alice2bob.expectMsg(Warning(channelId(alice), ForbiddenDuringSplice(channelId(alice), "UpdateAddHtlc").getMessage)) @@ -1618,7 +1682,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik assert(!alice.stateData.asInstanceOf[DATA_NORMAL].commitments.hasPendingOrProposedHtlcs) } - test("recv UpdateAddHtlc before splice confirms (zero-conf)", Tag(ChannelStateTestsTags.ZeroConf), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + test("recv UpdateAddHtlc before splice confirms (zero-conf)", Tag(ChannelStateTestsTags.ZeroConf)) { f => import f._ val spliceTx = initiateSplice(f, spliceOut_opt = Some(SpliceOut(50_000 sat, defaultSpliceOutScriptPubKey))) @@ -1642,7 +1706,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.inactive.head.localCommit.spec.htlcs.size == 1) } - test("recv UpdateAddHtlc while splice is being locked", Tag(ChannelStateTestsTags.ZeroConf), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + test("recv UpdateAddHtlc while splice is being locked", Tag(ChannelStateTestsTags.ZeroConf)) { f => import f._ val spliceTx1 = initiateSplice(f, spliceOut_opt = Some(SpliceOut(50_000 sat, defaultSpliceOutScriptPubKey))) @@ -1679,17 +1743,16 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik alice2bob.forward(bob, spliceLockedAlice) val (preimage, htlc) = addHtlc(20_000_000 msat, alice, bob, alice2bob, bob2alice) alice ! CMD_SIGN() - val commitSigsAlice = (1 to 3).map(_ => alice2bob.expectMsgType[CommitSig]) - alice2bob.forward(bob, commitSigsAlice(0)) + val commitSigsAlice = alice2bob.expectMsgType[CommitSigBatch] + assert(commitSigsAlice.batchSize == 3) bob ! WatchPublishedTriggered(spliceTx2) val spliceLockedBob = bob2alice.expectMsgType[SpliceLocked] assert(spliceLockedBob.fundingTxId == spliceTx2.txid) bob2alice.forward(alice, spliceLockedBob) - alice2bob.forward(bob, commitSigsAlice(1)) - alice2bob.forward(bob, commitSigsAlice(2)) + alice2bob.forward(bob, commitSigsAlice) bob2alice.expectMsgType[RevokeAndAck] bob2alice.forward(alice) - assert(bob2alice.expectMsgType[CommitSig].batchSize == 1) + bob2alice.expectMsgType[CommitSig] bob2alice.forward(alice) alice2bob.expectMsgType[RevokeAndAck] alice2bob.forward(bob) @@ -1722,8 +1785,8 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik private def reconnect(f: FixtureParam, sendReestablish: Boolean = true): (ChannelReestablish, ChannelReestablish) = { import f._ - val aliceInit = Init(alice.stateData.asInstanceOf[ChannelDataWithCommitments].commitments.params.localParams.initFeatures) - val bobInit = Init(bob.stateData.asInstanceOf[ChannelDataWithCommitments].commitments.params.localParams.initFeatures) + val aliceInit = Init(alice.commitments.localChannelParams.initFeatures) + val bobInit = Init(bob.commitments.localChannelParams.initFeatures) alice ! INPUT_RECONNECTED(alice2bob.ref, aliceInit, bobInit) bob ! INPUT_RECONNECTED(bob2alice.ref, bobInit, aliceInit) val channelReestablishAlice = alice2bob.expectMsgType[ChannelReestablish] @@ -1733,11 +1796,45 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik (channelReestablishAlice, channelReestablishBob) } + test("disconnect (tx_complete not received)") { f => + import f._ + // Disconnection with one side sending commit_sig + // alice bob + // | ... | + // | | + // |<----- tx_complete ----| + // |------ tx_complete --X | + // |------ commit_sig ---X | + // | | + // | | + // | | + // |<------ tx_abort ------| + // |------- tx_abort ----->| + + val sender = initiateSpliceWithoutSigs(f, spliceIn_opt = Some(SpliceIn(500_000 sat)), spliceOut_opt = Some(SpliceOut(100_000 sat, defaultSpliceOutScriptPubKey)), sendTxComplete = false) + bob2alice.expectMsgType[TxComplete] + bob2alice.forward(alice) + alice2bob.expectMsgType[TxComplete] // Bob doesn't receive Alice's tx_complete + alice2bob.expectMsgType[CommitSig] // Bob doesn't receive Alice's commit_sig + sender.expectMsgType[RES_SPLICE] // TODO: we should exchange tx_signatures before returning RES_SPLICE, see issue #3093 + + disconnect(f) + reconnect(f) + + // Bob and Alice will exchange tx_abort because Bob did not receive Alice's tx_complete before the disconnect. + bob2alice.expectMsgType[TxAbort] + bob2alice.forward(alice) + alice2bob.expectMsgType[TxAbort] + alice2bob.forward(bob) + awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].spliceStatus == SpliceStatus.NoSplice) + awaitCond(bob.stateData.asInstanceOf[DATA_NORMAL].spliceStatus == SpliceStatus.NoSplice) + } + test("disconnect (commit_sig not sent)") { f => import f._ val sender = TestProbe() - val cmd = CMD_SPLICE(sender.ref, spliceIn_opt = Some(SpliceIn(500_000 sat, pushAmount = 0 msat)), spliceOut_opt = None, requestFunding_opt = None) + val cmd = CMD_SPLICE(sender.ref, spliceIn_opt = Some(SpliceIn(500_000 sat, pushAmount = 0 msat)), spliceOut_opt = None, requestFunding_opt = None, channelType_opt = None) alice ! cmd exchangeStfu(f) alice2bob.expectMsgType[SpliceInit] @@ -1795,7 +1892,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik assert(channelReestablishBob2.nextFundingTxId_opt.contains(spliceStatus.signingSession.fundingTx.txId)) assert(channelReestablishBob2.nextLocalCommitmentNumber == bobCommitIndex) - // Alice and Bob retransmit commit_sig and tx_signatures. + // Alice retransmits commit_sig and both retransmit tx_signatures. alice2bob.expectMsgType[CommitSig] alice2bob.forward(bob) bob2alice.expectMsgType[TxSignatures] @@ -1819,12 +1916,107 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik resolveHtlcs(f, htlcs) } - test("disconnect (commit_sig received by alice)") { f => + test("disconnect (commit_sig not received, missing current nonce)", Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => + import f._ + + setupHtlcs(f) + val bobCommitIndex = bob.commitments.localCommitIndex + initiateSpliceWithoutSigs(f, spliceIn_opt = Some(SpliceIn(500_000 sat)), spliceOut_opt = Some(SpliceOut(100_000 sat, defaultSpliceOutScriptPubKey))) + alice2bob.expectMsgType[CommitSig] // Bob doesn't receive Alice's commit_sig + bob2alice.expectMsgType[CommitSig] + bob2alice.forward(alice) + awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].spliceStatus.isInstanceOf[SpliceStatus.SpliceWaitingForSigs]) + val spliceTxId = alice.stateData.asInstanceOf[DATA_NORMAL].spliceStatus.asInstanceOf[SpliceStatus.SpliceWaitingForSigs].signingSession.fundingTxId + + disconnect(f) + + val aliceInit = Init(alice.commitments.localChannelParams.initFeatures) + val bobInit = Init(bob.commitments.localChannelParams.initFeatures) + + alice ! INPUT_RECONNECTED(alice2bob.ref, aliceInit, bobInit) + val channelReestablishAlice = alice2bob.expectMsgType[ChannelReestablish] + assert(channelReestablishAlice.nextFundingTxId_opt.contains(spliceTxId)) + assert(channelReestablishAlice.currentCommitNonce_opt.isEmpty) + + bob ! INPUT_RECONNECTED(bob2alice.ref, bobInit, aliceInit) + val channelReestablishBob = bob2alice.expectMsgType[ChannelReestablish] + assert(channelReestablishBob.nextFundingTxId_opt.contains(spliceTxId)) + assert(channelReestablishBob.currentCommitNonce_opt.nonEmpty) + + // If Bob doesn't provide a nonce for Alice to retransmit her commit_sig, she cannot sign. + // We sent a warning and wait for Bob to fix his node instead of force-closing. + bob2alice.forward(alice, channelReestablishBob.copy(tlvStream = TlvStream(channelReestablishBob.tlvStream.records.filterNot(_.isInstanceOf[ChannelReestablishTlv.CurrentCommitNonceTlv])))) + assert(alice2bob.expectMsgType[Warning].toAscii == MissingCommitNonce(channelReestablishBob.channelId, spliceTxId, bobCommitIndex).getMessage) + alice2bob.expectNoMessage(100 millis) + assert(alice.stateName == NORMAL) + } + + test("disconnect (commit_sig not received, missing next nonce)", Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => + import f._ + + setupHtlcs(f) + val aliceCommitIndex = alice.commitments.localCommitIndex + val bobCommitIndex = bob.commitments.localCommitIndex + val fundingTxId = alice.commitments.latest.fundingTxId + initiateSpliceWithoutSigs(f, spliceIn_opt = Some(SpliceIn(500_000 sat)), spliceOut_opt = Some(SpliceOut(100_000 sat, defaultSpliceOutScriptPubKey))) + alice2bob.expectMsgType[CommitSig] // Bob doesn't receive Alice's commit_sig + bob2alice.expectMsgType[CommitSig] // Alice doesn't receive Bob's commit_sig + awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].spliceStatus.isInstanceOf[SpliceStatus.SpliceWaitingForSigs]) + val spliceTxId = alice.stateData.asInstanceOf[DATA_NORMAL].spliceStatus.asInstanceOf[SpliceStatus.SpliceWaitingForSigs].signingSession.fundingTxId + + disconnect(f) + + val aliceInit = Init(alice.commitments.localChannelParams.initFeatures) + val bobInit = Init(bob.commitments.localChannelParams.initFeatures) + val aliceCommitTx = alice.signCommitTx() + val bobCommitTx = bob.signCommitTx() + + alice ! INPUT_RECONNECTED(alice2bob.ref, aliceInit, bobInit) + val channelReestablishAlice = alice2bob.expectMsgType[ChannelReestablish] + assert(channelReestablishAlice.nextFundingTxId_opt.contains(spliceTxId)) + assert(channelReestablishAlice.currentCommitNonce_opt.nonEmpty) + + bob ! INPUT_RECONNECTED(bob2alice.ref, bobInit, aliceInit) + val channelReestablishBob = bob2alice.expectMsgType[ChannelReestablish] + assert(channelReestablishBob.nextFundingTxId_opt.contains(spliceTxId)) + assert(channelReestablishBob.currentCommitNonce_opt.nonEmpty) + + // If Alice doesn't include a nonce for the previous funding transaction, Bob must force-close. + val noncesAlice1 = ChannelReestablishTlv.NextLocalNoncesTlv((channelReestablishAlice.nextCommitNonces - fundingTxId).toSeq) + val channelReestablishAlice1 = channelReestablishAlice.copy(tlvStream = TlvStream(channelReestablishAlice.tlvStream.records.filterNot(_.isInstanceOf[ChannelReestablishTlv.NextLocalNoncesTlv]) + noncesAlice1)) + alice2bob.forward(bob, channelReestablishAlice1) + assert(bob2alice.expectMsgType[Error].toAscii == MissingCommitNonce(channelReestablishAlice.channelId, fundingTxId, aliceCommitIndex + 1).getMessage) + bob2blockchain.expectFinalTxPublished(bobCommitTx.txid) + + // If Bob doesn't include a nonce for the splice transaction, Alice must force-close. + val noncesBob1 = ChannelReestablishTlv.NextLocalNoncesTlv((channelReestablishBob.nextCommitNonces - spliceTxId).toSeq) + val channelReestablishBob1 = channelReestablishBob.copy(tlvStream = TlvStream(channelReestablishBob.tlvStream.records.filterNot(_.isInstanceOf[ChannelReestablishTlv.NextLocalNoncesTlv]) + noncesBob1)) + bob2alice.forward(alice, channelReestablishBob1) + assert(alice2bob.expectMsgType[Error].toAscii == MissingCommitNonce(channelReestablishBob.channelId, spliceTxId, bobCommitIndex + 1).getMessage) + alice2blockchain.expectFinalTxPublished(aliceCommitTx.txid) + } + + def disconnectCommitSigReceivedAlice(f: FixtureParam, commitmentFormat: CommitmentFormat): Unit = { import f._ + // Disconnection with both sides sending commit_sig + // alice bob + // | ... | + // | | + // |<----- tx_complete ----| + // |------ tx_complete --->| + // |------ commit_sig ---X | + // |<------ commit_sig ----| + // | | + // | | + // | | + // |------ commit_sig ---->| + // |<---- tx_signatures ---| + // |----- tx_signatures -->| val htlcs = setupHtlcs(f) - val aliceCommitIndex = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommitIndex - val bobCommitIndex = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommitIndex + val aliceCommitIndex = alice.commitments.localCommitIndex + val bobCommitIndex = bob.commitments.localCommitIndex + val fundingTxId = alice.commitments.latest.fundingTxId assert(aliceCommitIndex != bobCommitIndex) val sender = initiateSpliceWithoutSigs(f, spliceIn_opt = Some(SpliceIn(500_000 sat)), spliceOut_opt = Some(SpliceOut(100_000 sat, defaultSpliceOutScriptPubKey))) @@ -1836,10 +2028,21 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik disconnect(f) val (channelReestablishAlice, channelReestablishBob) = reconnect(f) - assert(channelReestablishAlice.nextFundingTxId_opt.contains(spliceStatus.signingSession.fundingTx.txId)) + assert(channelReestablishAlice.nextFundingTxId_opt.contains(spliceStatus.signingSession.fundingTxId)) assert(channelReestablishAlice.nextLocalCommitmentNumber == aliceCommitIndex + 1) - assert(channelReestablishBob.nextFundingTxId_opt.contains(spliceStatus.signingSession.fundingTx.txId)) + assert(channelReestablishBob.nextFundingTxId_opt.contains(spliceStatus.signingSession.fundingTxId)) assert(channelReestablishBob.nextLocalCommitmentNumber == bobCommitIndex) + commitmentFormat match { + case _: SegwitV0CommitmentFormat => () + case _: SimpleTaprootChannelCommitmentFormat => + assert(channelReestablishAlice.currentCommitNonce_opt.isEmpty) + assert(channelReestablishBob.currentCommitNonce_opt.nonEmpty) + Seq(channelReestablishAlice, channelReestablishBob).foreach { channelReestablish => + assert(channelReestablish.nextCommitNonces.size == 2) + assert(channelReestablish.nextCommitNonces.contains(fundingTxId)) + assert(channelReestablish.nextCommitNonces.contains(spliceStatus.signingSession.fundingTxId)) + } + } // Alice retransmits commit_sig, and they exchange tx_signatures afterwards. bob2alice.expectNoMessage(100 millis) @@ -1852,7 +2055,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik alice2bob.forward(bob) sender.expectMsgType[RES_SPLICE] - val spliceTx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localFundingStatus.signedTx_opt.get + val spliceTx = alice.commitments.latest.localFundingStatus.signedTx_opt.get alice2blockchain.expectWatchFundingConfirmed(spliceTx.txid) bob2blockchain.expectWatchFundingConfirmed(spliceTx.txid) alice ! WatchFundingConfirmedTriggered(BlockHeight(42), 0, spliceTx) @@ -1867,12 +2070,21 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik resolveHtlcs(f, htlcs) } - test("disconnect (commit_sig received by bob)") { f => + test("disconnect (commit_sig received by alice)") { f => + disconnectCommitSigReceivedAlice(f, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) + } + + test("disconnect (commit_sig received by alice, taproot)", Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => + disconnectCommitSigReceivedAlice(f, ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat) + } + + def disconnectCommitSigReceivedBob(f: FixtureParam, commitmentFormat: CommitmentFormat): Unit = { import f._ val htlcs = setupHtlcs(f) - val aliceCommitIndex = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommitIndex - val bobCommitIndex = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommitIndex + val aliceCommitIndex = alice.commitments.localCommitIndex + val bobCommitIndex = bob.commitments.localCommitIndex + val fundingTxId = alice.commitments.latest.fundingTxId assert(aliceCommitIndex != bobCommitIndex) val sender = initiateSpliceWithoutSigs(f, spliceIn_opt = Some(SpliceIn(500_000 sat)), spliceOut_opt = Some(SpliceOut(100_000 sat, defaultSpliceOutScriptPubKey))) @@ -1890,6 +2102,17 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik assert(channelReestablishAlice.nextLocalCommitmentNumber == aliceCommitIndex) assert(channelReestablishBob.nextFundingTxId_opt.contains(spliceStatus.signingSession.fundingTx.txId)) assert(channelReestablishBob.nextLocalCommitmentNumber == bobCommitIndex + 1) + commitmentFormat match { + case _: SegwitV0CommitmentFormat => () + case _: SimpleTaprootChannelCommitmentFormat => + assert(channelReestablishAlice.currentCommitNonce_opt.nonEmpty) + assert(channelReestablishBob.currentCommitNonce_opt.isEmpty) + Seq(channelReestablishAlice, channelReestablishBob).foreach { channelReestablish => + assert(channelReestablish.nextCommitNonces.size == 2) + assert(channelReestablish.nextCommitNonces.contains(fundingTxId)) + assert(channelReestablish.nextCommitNonces.contains(spliceStatus.signingSession.fundingTxId)) + } + } // Bob retransmit commit_sig and tx_signatures, Alice sends tx_signatures afterwards. bob2alice.expectMsgType[CommitSig] @@ -1901,7 +2124,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik alice2bob.forward(bob) sender.expectMsgType[RES_SPLICE] - val spliceTx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localFundingStatus.signedTx_opt.get + val spliceTx = alice.commitments.latest.localFundingStatus.signedTx_opt.get alice2blockchain.expectWatchFundingConfirmed(spliceTx.txid) bob2blockchain.expectWatchFundingConfirmed(spliceTx.txid) alice ! WatchFundingConfirmedTriggered(BlockHeight(42), 0, spliceTx) @@ -1916,12 +2139,21 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik resolveHtlcs(f, htlcs) } - test("disconnect (commit_sig received)") { f => + test("disconnect (commit_sig received by bob)") { f => + disconnectCommitSigReceivedBob(f, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) + } + + test("disconnect (commit_sig received by bob, taproot)", Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => + disconnectCommitSigReceivedBob(f, ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat) + } + + def disconnectCommitSigReceived(f: FixtureParam, commitmentFormat: CommitmentFormat): Unit = { import f._ val htlcs = setupHtlcs(f) - val aliceCommitIndex = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommitIndex - val bobCommitIndex = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommitIndex + val aliceCommitIndex = alice.commitments.localCommitIndex + val bobCommitIndex = bob.commitments.localCommitIndex + val fundingTxId = alice.commitments.latest.fundingTxId val sender = initiateSpliceWithoutSigs(f, spliceIn_opt = Some(SpliceIn(500_000 sat)), spliceOut_opt = Some(SpliceOut(100_000 sat, defaultSpliceOutScriptPubKey))) alice2bob.expectMsgType[CommitSig] @@ -1935,8 +2167,18 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik val (channelReestablishAlice, channelReestablishBob) = reconnect(f) assert(channelReestablishAlice.nextFundingTxId_opt.contains(spliceTxId)) assert(channelReestablishAlice.nextLocalCommitmentNumber == aliceCommitIndex + 1) + assert(channelReestablishAlice.currentCommitNonce_opt.isEmpty) assert(channelReestablishBob.nextFundingTxId_opt.contains(spliceTxId)) assert(channelReestablishBob.nextLocalCommitmentNumber == bobCommitIndex + 1) + assert(channelReestablishBob.currentCommitNonce_opt.isEmpty) + commitmentFormat match { + case _: SegwitV0CommitmentFormat => () + case _: SimpleTaprootChannelCommitmentFormat => Seq(channelReestablishAlice, channelReestablishBob).foreach { channelReestablish => + assert(channelReestablish.nextCommitNonces.size == 2) + assert(channelReestablish.nextCommitNonces.contains(fundingTxId)) + assert(channelReestablish.nextCommitNonces.contains(spliceTxId)) + } + } bob2blockchain.expectWatchFundingConfirmed(spliceTxId) // Alice and Bob retransmit tx_signatures. @@ -1947,7 +2189,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik alice2bob.forward(bob) sender.expectMsgType[RES_SPLICE] - val spliceTx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localFundingStatus.signedTx_opt.get + val spliceTx = alice.commitments.latest.localFundingStatus.signedTx_opt.get alice2blockchain.expectWatchFundingConfirmed(spliceTx.txid) alice ! WatchFundingConfirmedTriggered(BlockHeight(42), 0, spliceTx) alice2bob.expectMsgType[SpliceLocked] @@ -1955,18 +2197,41 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik bob ! WatchFundingConfirmedTriggered(BlockHeight(42), 0, spliceTx) bob2alice.expectMsgType[SpliceLocked] bob2alice.forward(alice) - awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.active.size == 1) - awaitCond(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.active.size == 1) + awaitCond(alice.commitments.active.size == 1) + awaitCond(bob.commitments.active.size == 1) resolveHtlcs(f, htlcs) } - test("disconnect (tx_signatures received by alice)") { f => + test("disconnect (commit_sig received)") { f => + disconnectCommitSigReceived(f, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) + } + + test("disconnect (commit_sig received, taproot)", Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => + disconnectCommitSigReceived(f, ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat) + } + + def disconnectTxSigsReceivedAlice(f: FixtureParam, commitmentFormat: CommitmentFormat): Unit = { import f._ + // Disconnection with both sides sending tx_signatures + // alice bob + // | ... | + // | | + // |<----- tx_complete ----| + // |------ tx_complete --->| + // |------ commit_sig ---->| + // |<------ commit_sig ----| + // |<---- tx_signatures ---| + // |----- tx_signatures --X| + // | | + // | | + // | | + // |----- tx_signatures -->| val htlcs = setupHtlcs(f) - val aliceCommitIndex = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommitIndex - val bobCommitIndex = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommitIndex + val aliceCommitIndex = alice.commitments.localCommitIndex + val bobCommitIndex = bob.commitments.localCommitIndex + val fundingTxId = alice.commitments.latest.fundingTxId initiateSpliceWithoutSigs(f, spliceIn_opt = Some(SpliceIn(500_000 sat)), spliceOut_opt = Some(SpliceOut(100_000 sat, defaultSpliceOutScriptPubKey))) alice2bob.expectMsgType[CommitSig] @@ -1983,14 +2248,24 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik val (channelReestablishAlice, channelReestablishBob) = reconnect(f) assert(channelReestablishAlice.nextFundingTxId_opt.isEmpty) assert(channelReestablishAlice.nextLocalCommitmentNumber == aliceCommitIndex + 1) + assert(channelReestablishAlice.currentCommitNonce_opt.isEmpty) assert(channelReestablishBob.nextFundingTxId_opt.contains(spliceTxId)) assert(channelReestablishBob.nextLocalCommitmentNumber == bobCommitIndex + 1) + assert(channelReestablishBob.currentCommitNonce_opt.isEmpty) + commitmentFormat match { + case _: SegwitV0CommitmentFormat => () + case _: SimpleTaprootChannelCommitmentFormat => Seq(channelReestablishAlice, channelReestablishBob).foreach { channelReestablish => + assert(channelReestablish.nextCommitNonces.size == 2) + assert(channelReestablish.nextCommitNonces.contains(fundingTxId)) + assert(channelReestablish.nextCommitNonces.contains(spliceTxId)) + } + } alice2blockchain.expectWatchFundingConfirmed(spliceTxId) bob2blockchain.expectWatchFundingConfirmed(spliceTxId) - assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.active.size == 2) - assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.active.size == 2) - val spliceTx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localFundingStatus.signedTx_opt.get + assert(alice.commitments.active.size == 2) + assert(bob.commitments.active.size == 2) + val spliceTx = alice.commitments.latest.localFundingStatus.signedTx_opt.get // Alice retransmits tx_signatures. alice2bob.expectMsgType[TxSignatures] @@ -2001,13 +2276,21 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik bob ! WatchFundingConfirmedTriggered(BlockHeight(42), 0, spliceTx) bob2alice.expectMsgType[SpliceLocked] bob2alice.forward(alice) - awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.active.size == 1) - awaitCond(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.active.size == 1) + awaitCond(alice.commitments.active.size == 1) + awaitCond(bob.commitments.active.size == 1) resolveHtlcs(f, htlcs) } - test("disconnect (tx_signatures received by alice, zero-conf)", Tag(ChannelStateTestsTags.ZeroConf), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + test("disconnect (tx_signatures received by alice)") { f => + disconnectTxSigsReceivedAlice(f, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) + } + + test("disconnect (tx_signatures received by alice, taproot)", Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => + disconnectTxSigsReceivedAlice(f, ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat) + } + + test("disconnect (tx_signatures received by alice, zero-conf)", Tag(ChannelStateTestsTags.ZeroConf)) { f => import f._ val htlcs = setupHtlcs(f) @@ -2222,10 +2505,11 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik resolveHtlcs(f, htlcs) } - test("disconnect (RBF commit_sig received by bob)") { f => + test("disconnect (RBF commit_sig received by bob)", Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => import f._ val htlcs = setupHtlcs(f) + val fundingTxId = alice.commitments.latest.fundingTxId val spliceTx = initiateSplice(f, spliceIn_opt = Some(SpliceIn(500_000 sat)), spliceOut_opt = Some(SpliceOut(100_000 sat, defaultSpliceOutScriptPubKey))) assert(alice2blockchain.expectMsgType[WatchFundingConfirmed].txId == spliceTx.txid) @@ -2251,8 +2535,17 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik val (channelReestablishAlice, channelReestablishBob) = reconnect(f) assert(channelReestablishAlice.nextFundingTxId_opt.contains(rbfTxId)) assert(channelReestablishAlice.nextLocalCommitmentNumber == aliceCommitIndex) + assert(channelReestablishAlice.currentCommitNonce_opt.nonEmpty) assert(channelReestablishBob.nextFundingTxId_opt.contains(rbfTxId)) assert(channelReestablishBob.nextLocalCommitmentNumber == bobCommitIndex + 1) + assert(channelReestablishBob.currentCommitNonce_opt.isEmpty) + Seq(channelReestablishAlice, channelReestablishBob).foreach(channelReestablish => { + assert(channelReestablish.nextCommitNonces.size == 3) + assert(channelReestablish.nextCommitNonces.contains(fundingTxId)) + assert(channelReestablish.nextCommitNonces.contains(spliceTx.txid)) + assert(channelReestablish.nextCommitNonces.contains(rbfTxId)) + assert(channelReestablish.nextCommitNonces.values.toSet.size == 3) + }) bob2blockchain.expectWatchFundingConfirmed(spliceTx.txid) // Bob retransmits commit_sig, and they exchange tx_signatures afterwards. @@ -2318,7 +2611,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik resolveHtlcs(f, htlcs) } - test("don't resend splice_locked when zero-conf channel confirms", Tag(ChannelStateTestsTags.ZeroConf), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + test("don't resend splice_locked when zero-conf channel confirms", Tag(ChannelStateTestsTags.ZeroConf)) { f => import f._ val fundingTx = initiateSplice(f, spliceIn_opt = Some(SpliceIn(500_000 sat, pushAmount = 0 msat))) @@ -2333,7 +2626,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik alice2blockchain.expectWatchFundingSpent(fundingTx.txid) } - test("re-send splice_locked on reconnection") { f => + def resendSpliceLockedOnReconnection(f: FixtureParam): Unit = { import f._ val fundingTx1 = initiateSplice(f, spliceIn_opt = Some(SpliceIn(500_000 sat, pushAmount = 0 msat))) @@ -2421,6 +2714,86 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik } } + test("re-send splice_locked on reconnection") { f => + resendSpliceLockedOnReconnection(f) + } + + test("re-send splice_locked on reconnection (taproot channels)", Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => + resendSpliceLockedOnReconnection(f) + } + + test("disconnect before channel update and tx_signatures are received") { f => + import f._ + // Disconnection with both sides sending tx_signatures and channel updates + // alice bob + // | ... | + // | | + // |<----- tx_complete ----| + // |------ tx_complete --->| + // |------ commit_sig ---->| + // |<------ commit_sig ----| + // |<---- tx_signatures ---| + // |----- tx_signatures --X| + // |--- update_add_htlc --X| + // |------ start_batch ---X| batch_size = 2 + // |------ commit_sig ----X| funding_txid = FundingTx + // |------ commit_sig ----X| funding_txid = SpliceFundingTx + // | | + // | | + // | | + // |----- tx_signatures -->| + // |--- update_add_htlc -->| + // |------ start_batch --->| batch_size = 2 + // |------ commit_sig ---->| funding_txid = FundingTx + // |------ commit_sig ---->| funding_txid = SpliceFundingTx + // |<--- revoke_and_ack ---| + // |<----- commit_sig -----| + // |<----- commit_sig -----| + // |---- revoke_and_ack -->| + + initiateSpliceWithoutSigs(f, spliceIn_opt = Some(SpliceIn(500_000 sat)), spliceOut_opt = Some(SpliceOut(100_000 sat, defaultSpliceOutScriptPubKey))) + alice2bob.expectMsgType[CommitSig] + alice2bob.forward(bob) + bob2alice.expectMsgType[CommitSig] + bob2alice.forward(alice) + bob2alice.expectMsgType[TxSignatures] + bob2alice.forward(alice) + alice2bob.expectMsgType[TxSignatures] // Bob doesn't receive Alice's tx_signatures + + awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].spliceStatus == SpliceStatus.NoSplice) + val (_, cmd) = makeCmdAdd(25_000_000 msat, bob.nodeParams.nodeId, bob.nodeParams.currentBlockHeight) + alice ! cmd.copy(commit = true) + alice2bob.expectMsgType[UpdateAddHtlc] // Bob doesn't receive Alice's update_add_htlc + inside(alice2bob.expectMsgType[CommitSigBatch]) { batch => // Bob doesn't receive Alice's commit_sigs + assert(batch.batchSize == 2) + } + alice2bob.expectNoMessage(100 millis) + bob2alice.expectNoMessage(100 millis) + + // Bob will not receive Alice's tx_signatures, update_add_htlc or commit_sigs before disconnecting. + disconnect(f) + reconnect(f) + + // Alice must retransmit her tx_signatures, update_add_htlc and commit_sigs first. + alice2bob.expectMsgType[TxSignatures] + alice2bob.forward(bob) + alice2bob.expectMsgType[UpdateAddHtlc] + alice2bob.forward(bob) + inside(alice2bob.expectMsgType[CommitSigBatch]) { batch => + assert(batch.batchSize == 2) + alice2bob.forward(bob) + } + bob2alice.expectMsgType[RevokeAndAck] + bob2alice.forward(alice) + inside(bob2alice.expectMsgType[CommitSigBatch]) { batch => + assert(batch.batchSize == 2) + bob2alice.forward(alice) + } + alice2bob.expectMsgType[RevokeAndAck] + alice2bob.forward(bob) + bob2alice.expectNoMessage(100 millis) + } + test("disconnect and update channel before receiving final splice_locked") { f => import f._ @@ -2492,6 +2865,248 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik } } + test("disconnect while updating channel before receiving splice_locked", Tag(ChannelStateTestsTags.OptionSimpleTaproot), Tag(ChannelStateTestsTags.ZeroConf)) { f => + import f._ + + val spliceTx = initiateSplice(f, spliceIn_opt = Some(SpliceIn(500_000 sat, pushAmount = 0 msat))) + checkWatchPublished(f, spliceTx) + + alice2bob.ignoreMsg { case _: ChannelUpdate => true } + bob2alice.ignoreMsg { case _: ChannelUpdate => true } + + // The splice confirms on Alice's side. + alice ! WatchFundingConfirmedTriggered(BlockHeight(400000), 42, spliceTx) + alice2blockchain.expectMsgTypeHaving[WatchFundingSpent](_.txId == spliceTx.txid) + alice2bob.expectMsgTypeHaving[SpliceLocked](_.fundingTxId == spliceTx.txid) + alice2bob.forward(bob) + + // Alice sends an HTLC to Bob, but Bob doesn't receive the commit_sig messages. + addHtlc(25_000_000 msat, alice, bob, alice2bob, bob2alice) + alice ! CMD_SIGN() + assert(alice2bob.expectMsgType[CommitSigBatch].batchSize == 2) + + // At the same time, the splice confirms on Bob's side, who now expects a single commit_sig message. + bob ! WatchFundingConfirmedTriggered(BlockHeight(400000), 42, spliceTx) + bob2blockchain.expectMsgTypeHaving[WatchFundingSpent](_.txId == spliceTx.txid) + bob2alice.expectMsgTypeHaving[SpliceLocked](_.fundingTxId == spliceTx.txid) + bob2alice.forward(alice) + + disconnect(f) + reconnect(f) + + // On reconnection, Alice will only re-send commit_sig for the (locked) splice transaction. + assert(alice.commitments.active.size == 1) + assert(bob.commitments.active.size == 1) + alice2bob.expectMsgType[UpdateAddHtlc] + alice2bob.forward(bob) + assert(alice2bob.expectMsgType[CommitSig].tlvStream.get[CommitSigTlv.BatchTlv].isEmpty) + alice2bob.forward(bob) + bob2alice.expectMsgType[RevokeAndAck] + bob2alice.forward(alice) + bob2alice.expectMsgType[CommitSig] + bob2alice.forward(alice) + alice2bob.expectMsgType[RevokeAndAck] + alice2bob.forward(bob) + } + + test("disconnect after exchanging tx_signatures and one side sends commit_sig for channel update") { f => + import f._ + + // alice bob + // | ... | + // | | + // |<----- tx_complete ----| + // |------ tx_complete --->| + // |------ commit_sig ---->| + // |<------ commit_sig ----| + // |<---- tx_signatures ---| + // |----- tx_signatures -->| + // |--- update_add_htlc -->| + // |------ start_batch ---X| batch_size = 2 + // |------ commit_sig ----X| funding_txid = FundingTx + // |------ commit_sig ----X| funding_txid = SpliceFundingTx + // | | + // | | + // | | + // |--- update_add_htlc -->| + // |------ start_batch --->| batch_size = 2 + // |------ commit_sig ---->| funding_txid = FundingTx + // |------ commit_sig ---->| funding_txid = SpliceFundingTx + // |<--- revoke_and_ack ---| + // |<----- start_batch ----| batch_size = 2 + // |<----- commit_sig -----| + // |<----- commit_sig -----| + // |---- revoke_and_ack -->| + + val fundingTx = initiateSplice(f, spliceIn_opt = Some(SpliceIn(500_000 sat, pushAmount = 0 msat))) + checkWatchConfirmed(f, fundingTx) + + awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].spliceStatus == SpliceStatus.NoSplice) + addHtlc(25_000_000 msat, alice, bob, alice2bob, bob2alice) + alice ! CMD_SIGN(None) + inside(alice2bob.expectMsgType[CommitSigBatch]) { batch => // Bob doesn't receive Alice's commit_sig + assert(batch.batchSize == 2) + } + alice2bob.expectNoMessage(100 millis) + bob2alice.expectNoMessage(100 millis) + + // Bob will not receive Alice's commit_sigs before disconnecting. + disconnect(f) + val (channelReestablishAlice, channelReestablishBob) = reconnect(f) + assert(channelReestablishAlice.nextFundingTxId_opt.isEmpty) + assert(channelReestablishAlice.nextLocalCommitmentNumber == 1) + assert(channelReestablishBob.nextFundingTxId_opt.isEmpty) + assert(channelReestablishBob.nextLocalCommitmentNumber == 1) + + // Alice must retransmit update_add_htlc and commit_sigs first. + alice2bob.expectMsgType[UpdateAddHtlc] + alice2bob.forward(bob) + inside(alice2bob.expectMsgType[CommitSigBatch]) { batch => + assert(batch.batchSize == 2) + alice2bob.forward(bob) + } + bob2alice.expectMsgType[RevokeAndAck] + bob2alice.forward(alice) + inside(bob2alice.expectMsgType[CommitSigBatch]) { batch => + assert(batch.batchSize == 2) + bob2alice.forward(alice) + } + alice2bob.expectMsgType[RevokeAndAck] + alice2bob.forward(bob) + alice2bob.expectNoMessage(100 millis) + bob2alice.expectNoMessage(100 millis) + } + + test("disconnect after exchanging tx_signatures and both sides send commit_sig for channel update; revoke_and_ack not received") { f => + import f._ + // alice bob + // | ... | + // | | + // |<----- tx_complete ----| + // |------ tx_complete --->| + // |------ commit_sig ---->| + // |<------ commit_sig ----| + // |<---- tx_signatures ---| + // |----- tx_signatures -->| + // |--- update_add_htlc -->| + // |------ start_batch --->| batch_size = 2 + // |------ commit_sig ---->| funding_txid = FundingTx + // |------ commit_sig ---->| funding_txid = SpliceFundingTx + // |X--- revoke_and_ack ---| + // |X----- start_batch ----| batch_size = 2 + // |X----- commit_sig -----| funding_txid = FundingTx + // |X----- commit_sig -----| funding_txid = SpliceFundingTx + // | | + // | | + // | | next_revocation_number = 0 (for both alice and bob) + // |<--- revoke_and_ack ---| + // |<----- start_batch ----| batch_size = 2 + // |<----- commit_sig -----| funding_txid = FundingTx + // |<----- commit_sig -----| funding_txid = SpliceFundingTx + // |---- revoke_and_ack -->| + + val fundingTx = initiateSplice(f, spliceIn_opt = Some(SpliceIn(500_000 sat, pushAmount = 0 msat))) + checkWatchConfirmed(f, fundingTx) + + awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].spliceStatus == SpliceStatus.NoSplice) + addHtlc(25_000_000 msat, alice, bob, alice2bob, bob2alice) + alice ! CMD_SIGN(None) + alice2bob.expectMsgType[CommitSigBatch] + alice2bob.forward(bob) + bob2alice.expectMsgType[RevokeAndAck] // Alice doesn't receive Bob's revoke_and_ack + inside(bob2alice.expectMsgType[CommitSigBatch]) { batch => // Alice doesn't receive Bob's commit_sig + assert(batch.batchSize == 2) + } + alice2bob.expectNoMessage(100 millis) + bob2alice.expectNoMessage(100 millis) + + // Alice will not receive Bob's commit_sigs before disconnecting. + disconnect(f) + val (channelReestablishAlice, channelReestablishBob) = reconnect(f) + assert(channelReestablishAlice.nextFundingTxId_opt.isEmpty) + assert(channelReestablishAlice.nextRemoteRevocationNumber == 0) + assert(channelReestablishBob.nextFundingTxId_opt.isEmpty) + assert(channelReestablishBob.nextRemoteRevocationNumber == 0) + + // Bob must retransmit his commit_sigs first. + alice2bob.expectNoMessage(100 millis) + bob2alice.expectMsgType[RevokeAndAck] + bob2alice.forward(alice) + inside(bob2alice.expectMsgType[CommitSigBatch]) { batch => + assert(batch.batchSize == 2) + bob2alice.forward(alice) + } + alice2bob.expectMsgType[RevokeAndAck] + alice2bob.forward(bob) + alice2bob.expectNoMessage(100 millis) + bob2alice.expectNoMessage(100 millis) + } + + test("disconnect after exchanging tx_signatures and both sides send commit_sig for channel update") { f => + import f._ + // alice bob + // | ... | + // | | + // |<----- tx_complete ----| + // |------ tx_complete --->| + // |------ commit_sig ---->| + // |<------ commit_sig ----| + // |<---- tx_signatures ---| + // |----- tx_signatures -->| + // |--- update_add_htlc -->| + // |------ start_batch --->| batch_size = 2 + // |------ commit_sig ---->| funding_txid = FundingTx + // |------ commit_sig ---->| funding_txid = SpliceFundingTx + // |<--- revoke_and_ack ---| + // |X----- start_batch ----| batch_size = 2 + // |X----- commit_sig -----| funding_txid = FundingTx + // |X----- commit_sig -----| funding_txid = SpliceFundingTx + // | | + // | | next_revocation_number = 1 (alice) and 0 (bob) + // | | + // |<----- start_batch ----| batch_size = 2 + // |<----- commit_sig -----| funding_txid = FundingTx + // |<----- commit_sig -----| funding_txid = SpliceFundingTx + // |---- revoke_and_ack -->| + + val fundingTx = initiateSplice(f, spliceIn_opt = Some(SpliceIn(500_000 sat, pushAmount = 0 msat))) + checkWatchConfirmed(f, fundingTx) + + awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].spliceStatus == SpliceStatus.NoSplice) + addHtlc(25_000_000 msat, alice, bob, alice2bob, bob2alice) + alice ! CMD_SIGN(None) + inside(alice2bob.expectMsgType[CommitSigBatch]) { batch => + assert(batch.batchSize == 2) + alice2bob.forward(bob) + } + bob2alice.expectMsgType[RevokeAndAck] + bob2alice.forward(alice) + inside(bob2alice.expectMsgType[CommitSigBatch]) { batch => // Alice doesn't receive Bob's commit_sig + assert(batch.batchSize == 2) + } + alice2bob.expectNoMessage(100 millis) + bob2alice.expectNoMessage(100 millis) + + // Alice will not receive Bob's commit_sigs before disconnecting. + disconnect(f) + val (channelReestablishAlice, channelReestablishBob) = reconnect(f) + assert(channelReestablishAlice.nextFundingTxId_opt.isEmpty) + assert(channelReestablishAlice.nextRemoteRevocationNumber == 1) + assert(channelReestablishBob.nextFundingTxId_opt.isEmpty) + assert(channelReestablishBob.nextRemoteRevocationNumber == 0) + + // Bob must retransmit his commit_sigs first. + alice2bob.expectNoMessage(100 millis) + inside(bob2alice.expectMsgType[CommitSigBatch]) { batch => + assert(batch.batchSize == 2) + bob2alice.forward(alice) + } + alice2bob.expectMsgType[RevokeAndAck] + alice2bob.forward(bob) + alice2bob.expectNoMessage(100 millis) + bob2alice.expectNoMessage(100 millis) + } + test("disconnect before receiving announcement_signatures from one peer", Tag(ChannelStateTestsTags.ChannelsPublic)) { f => import f._ @@ -2670,16 +3285,6 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik } } - /** Check type of published transactions */ - def assertPublished(probe: TestProbe, desc: String): Transaction = { - val p = probe.expectMsgType[PublishTx] - assert(desc == p.desc) - p match { - case p: PublishFinalTx => p.tx - case p: PublishReplaceableTx => p.txInfo.tx - } - } - test("force-close with multiple splices (simple)") { f => import f._ @@ -2700,17 +3305,24 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik // From Alice's point of view, we now have two unconfirmed splices. alice ! CMD_FORCECLOSE(ActorRef.noSender) alice2bob.expectMsgType[Error] - val commitTx2 = assertPublished(alice2blockchain, "commit-tx") + val commitTx2 = alice2blockchain.expectFinalTxPublished("commit-tx").tx Transaction.correctlySpends(commitTx2, Seq(fundingTx2), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - val claimMainDelayed2 = assertPublished(alice2blockchain, "local-main-delayed") + val anchorAlice = alice2blockchain.expectReplaceableTxPublished[ClaimLocalAnchorTx] + val claimMainAlice = alice2blockchain.expectFinalTxPublished("local-main-delayed") // Alice publishes her htlc timeout transactions. - val htlcsTxsOut = htlcs.aliceToBob.map(_ => assertPublished(alice2blockchain, "htlc-timeout")) - htlcsTxsOut.foreach(tx => Transaction.correctlySpends(tx, Seq(commitTx2), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) + val aliceHtlcTimeout = htlcs.aliceToBob.map(_ => alice2blockchain.expectReplaceableTxPublished[HtlcTimeoutTx].sign()) + aliceHtlcTimeout.foreach(htlcTx => Transaction.correctlySpends(htlcTx, Seq(commitTx2), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) - val watchConfirmedCommit2 = alice2blockchain.expectWatchTxConfirmed(commitTx2.txid) - val watchConfirmedClaimMainDelayed2 = alice2blockchain.expectWatchTxConfirmed(claimMainDelayed2.txid) - val watchHtlcsOut = htlcs.aliceToBob.map(_ => alice2blockchain.expectMsgType[WatchOutputSpent]) - htlcs.bobToAlice.map(_ => alice2blockchain.expectMsgType[WatchOutputSpent]) + // Bob detects Alice's commit tx. + bob ! WatchFundingSpentTriggered(commitTx2) + val anchorBob = bob2blockchain.expectReplaceableTxPublished[ClaimRemoteAnchorTx] + val claimMainBob = bob2blockchain.expectFinalTxPublished("remote-main-delayed") + val bobHtlcTimeout = htlcs.bobToAlice.map(_ => bob2blockchain.expectReplaceableTxPublished[ClaimHtlcTimeoutTx]) + bobHtlcTimeout.foreach(htlcTx => Transaction.correctlySpends(htlcTx.sign(), Seq(commitTx2), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) + bob2blockchain.expectWatchTxConfirmed(commitTx2.txid) + bob2blockchain.expectWatchOutputsSpent(Seq(claimMainBob.input, anchorBob.input.outPoint) ++ aliceHtlcTimeout.map(_.txIn.head.outPoint) ++ bobHtlcTimeout.map(_.input.outPoint)) + alice2blockchain.expectWatchTxConfirmed(commitTx2.txid) + alice2blockchain.expectWatchOutputsSpent(Seq(claimMainAlice.input, anchorAlice.input.outPoint) ++ aliceHtlcTimeout.map(_.txIn.head.outPoint) ++ bobHtlcTimeout.map(_.input.outPoint)) // The first splice transaction confirms. alice ! WatchFundingConfirmedTriggered(BlockHeight(400000), 42, fundingTx1) @@ -2720,43 +3332,55 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik alice ! WatchFundingConfirmedTriggered(BlockHeight(400000), 42, fundingTx2) alice2blockchain.expectMsgType[WatchFundingSpent] - // The commit confirms, along with Alice's 2nd-stage transactions. - watchConfirmedCommit2.replyTo ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, commitTx2) - watchConfirmedClaimMainDelayed2.replyTo ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, claimMainDelayed2) - watchHtlcsOut.zip(htlcsTxsOut).foreach { case (watch, tx) => watch.replyTo ! WatchOutputSpentTriggered(watch.amount, tx) } - htlcsTxsOut.foreach { tx => - alice2blockchain.expectWatchTxConfirmed(tx.txid) - alice ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, tx) - } - - // Alice publishes 3rd-stage transactions. - htlcs.aliceToBob.foreach { _ => - val tx = assertPublished(alice2blockchain, "htlc-delayed") - alice2blockchain.expectWatchTxConfirmed(tx.txid) - alice ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, tx) - } - - // Bob's htlc-timeout txs confirm. - bob ! WatchFundingSpentTriggered(commitTx2) - val bobHtlcsTxsOut = htlcs.bobToAlice.map(_ => assertPublished(bob2blockchain, "claim-htlc-timeout")) - val remoteOutpoints = alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.map(rcp => rcp.htlcTxs.filter(_._2.isEmpty).keys).toSeq.flatten - assert(remoteOutpoints.size == htlcs.bobToAlice.size) - assert(remoteOutpoints.toSet == bobHtlcsTxsOut.flatMap(_.txIn.map(_.outPoint)).toSet) - bobHtlcsTxsOut.foreach { tx => alice ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, tx) } + // Alice detects that the commit confirms, along with 2nd-stage and 3rd-stage transactions. + alice ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, commitTx2) + alice ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, claimMainAlice.tx) + aliceHtlcTimeout.foreach(htlcTx => { + alice ! WatchOutputSpentTriggered(0 sat, htlcTx) + alice2blockchain.expectWatchTxConfirmed(htlcTx.txid) + alice ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, htlcTx) + val htlcDelayed = alice2blockchain.expectFinalTxPublished("htlc-delayed") + alice2blockchain.expectWatchOutputSpent(htlcDelayed.input) + alice ! WatchOutputSpentTriggered(0 sat, htlcDelayed.tx) + alice2blockchain.expectWatchTxConfirmed(htlcDelayed.tx.txid) + alice ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, htlcDelayed.tx) + }) + bobHtlcTimeout.foreach(htlcTx => { + alice ! WatchOutputSpentTriggered(htlcTx.amountIn, htlcTx.tx) + alice2blockchain.expectWatchTxConfirmed(htlcTx.tx.txid) + alice ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, htlcTx.tx) + }) alice2blockchain.expectNoMessage(100 millis) - - checkPostSpliceState(f, spliceOutFee(f, capacity = 1_900_000.sat)) awaitCond(alice.stateName == CLOSED) - assert(Helpers.Closing.isClosed(alice.stateData.asInstanceOf[DATA_CLOSING], None).exists(_.isInstanceOf[LocalClose])) + assert(alice.stateData.asInstanceOf[DATA_CLOSED].closingTxId == commitTx2.txid) + assert(alice.stateData.asInstanceOf[DATA_CLOSED].fundingTxId == fundingTx2.txid) + + // Bob also detects that the commit confirms, along with 2nd-stage transactions. + bob ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, commitTx2) + bobHtlcTimeout.foreach(htlcTx => { + bob ! WatchOutputSpentTriggered(htlcTx.amountIn, htlcTx.tx) + bob2blockchain.expectWatchTxConfirmed(htlcTx.tx.txid) + bob ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, htlcTx.tx) + }) + aliceHtlcTimeout.foreach(htlcTx => { + bob ! WatchOutputSpentTriggered(0 sat, htlcTx) + bob2blockchain.expectWatchTxConfirmed(htlcTx.txid) + bob ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, htlcTx) + }) + bob2blockchain.expectNoMessage(100 millis) + bob ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, claimMainBob.tx) + awaitCond(bob.stateName == CLOSED) + assert(bob.stateData.asInstanceOf[DATA_CLOSED].closingTxId == commitTx2.txid) + assert(bob.stateData.asInstanceOf[DATA_CLOSED].fundingTxId == fundingTx2.txid) } - test("force-close with multiple splices (previous active remote)", Tag(ChannelStateTestsTags.StaticRemoteKey), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + test("force-close with multiple splices (previous active remote)", Tag(ChannelStateTestsTags.OptionSimpleTaproot), Tag(ChannelStateTestsTags.ZeroConf)) { f => import f._ val htlcs = setupHtlcs(f) val fundingTx1 = initiateSplice(f, spliceIn_opt = Some(SpliceIn(500_000 sat)), spliceOut_opt = Some(SpliceOut(100_000 sat, defaultSpliceOutScriptPubKey))) - checkWatchConfirmed(f, fundingTx1) + checkWatchPublished(f, fundingTx1) // The first splice confirms on Bob's side. bob ! WatchFundingConfirmedTriggered(BlockHeight(400000), 42, fundingTx1) @@ -2765,21 +3389,19 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik bob2alice.forward(alice) val fundingTx2 = initiateSplice(f, spliceIn_opt = Some(SpliceIn(500_000 sat))) - checkWatchConfirmed(f, fundingTx2) + checkWatchPublished(f, fundingTx2) alice2bob.expectNoMessage(100 millis) bob2alice.expectNoMessage(100 millis) // From Alice's point of view, we now have two unconfirmed splices. alice ! CMD_FORCECLOSE(ActorRef.noSender) alice2bob.expectMsgType[Error] - val aliceCommitTx2 = assertPublished(alice2blockchain, "commit-tx") - assertPublished(alice2blockchain, "local-anchor") - assertPublished(alice2blockchain, "local-main-delayed") - val htlcsTxsOut = htlcs.aliceToBob.map(_ => assertPublished(alice2blockchain, "htlc-timeout")) - htlcsTxsOut.foreach(tx => Transaction.correctlySpends(tx, Seq(aliceCommitTx2), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) - alice2blockchain.expectMsgType[WatchTxConfirmed] - alice2blockchain.expectMsgType[WatchTxConfirmed] - alice2blockchain.expectMsgType[WatchOutputSpent] + val aliceCommitTx2 = alice2blockchain.expectFinalTxPublished("commit-tx").tx + val localAnchor = alice2blockchain.expectReplaceableTxPublished[ClaimLocalAnchorTx] + val localMain = alice2blockchain.expectFinalTxPublished("local-main-delayed") + htlcs.aliceToBob.map(_ => alice2blockchain.expectReplaceableTxPublished[HtlcTimeoutTx]) + alice2blockchain.expectWatchTxConfirmed(aliceCommitTx2.txid) + alice2blockchain.expectWatchOutputsSpent(Seq(localMain.input, localAnchor.input.outPoint)) htlcs.aliceToBob.map(_ => alice2blockchain.expectMsgType[WatchOutputSpent]) htlcs.bobToAlice.map(_ => alice2blockchain.expectMsgType[WatchOutputSpent]) @@ -2788,45 +3410,50 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik alice2blockchain.expectWatchFundingSpent(fundingTx1.txid) // Bob publishes his commit tx for the first splice transaction (which double-spends the second splice transaction). - val bobCommitment1 = bob.stateData.asInstanceOf[ChannelDataWithCommitments].commitments.active.find(_.fundingTxIndex == 1).get - val bobCommitTx1 = bobCommitment1.fullySignedLocalCommitTx(bob.stateData.asInstanceOf[ChannelDataWithCommitments].commitments.params, bob.underlyingActor.keyManager).tx + val bobCommitments = bob.stateData.asInstanceOf[ChannelDataWithCommitments].commitments + val previousCommitment = bobCommitments.active.find(_.fundingTxIndex == 1).get + val bobCommitTx1 = previousCommitment.fullySignedLocalCommitTx(bobCommitments.channelParams, bob.underlyingActor.channelKeys) + val bobHtlcTxs = previousCommitment.htlcTxs(bobCommitments.channelParams, bob.underlyingActor.channelKeys).map(_._1) Transaction.correctlySpends(bobCommitTx1, Seq(fundingTx1), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) alice ! WatchFundingSpentTriggered(bobCommitTx1) - val watchAlternativeConfirmed = alice2blockchain.expectMsgType[WatchAlternativeCommitTxConfirmed] + assert(alice2blockchain.expectMsgType[WatchAlternativeCommitTxConfirmed].txId == bobCommitTx1.txid) alice2blockchain.expectNoMessage(100 millis) // Bob's commit tx confirms. - watchAlternativeConfirmed.replyTo ! WatchAlternativeCommitTxConfirmedTriggered(BlockHeight(400000), 42, bobCommitTx1) + alice ! WatchAlternativeCommitTxConfirmedTriggered(BlockHeight(400000), 42, bobCommitTx1) // We're back to the normal handling of remote commit. - assertPublished(alice2blockchain, "local-anchor") - val claimMain = assertPublished(alice2blockchain, "remote-main-delayed") - val htlcsTxsOut1 = htlcs.aliceToBob.map(_ => assertPublished(alice2blockchain, "claim-htlc-timeout")) - htlcsTxsOut1.foreach(tx => Transaction.correctlySpends(tx, Seq(bobCommitTx1), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) - val watchConfirmedRemoteCommit = alice2blockchain.expectWatchTxConfirmed(bobCommitTx1.txid) + val remoteAnchor = alice2blockchain.expectReplaceableTxPublished[ClaimRemoteAnchorTx] + val remoteMain = alice2blockchain.expectFinalTxPublished("remote-main-delayed") + val htlcTimeout = htlcs.aliceToBob.map(_ => alice2blockchain.expectReplaceableTxPublished[ClaimHtlcTimeoutTx]) + htlcTimeout.foreach(htlcTx => Transaction.correctlySpends(htlcTx.sign(), Seq(bobCommitTx1), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) // NB: this one fires immediately, tx is already confirmed. - watchConfirmedRemoteCommit.replyTo ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, bobCommitTx1) + alice2blockchain.expectWatchTxConfirmed(bobCommitTx1.txid) + alice ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, bobCommitTx1) + alice2blockchain.expectWatchOutputsSpent(Seq(remoteMain.input, remoteAnchor.input.outPoint)) + htlcs.aliceToBob.map(_ => alice2blockchain.expectMsgType[WatchOutputSpent]) + htlcs.bobToAlice.map(_ => alice2blockchain.expectMsgType[WatchOutputSpent]) // Alice's 2nd-stage transactions confirm. - val watchConfirmedClaimMain = alice2blockchain.expectWatchTxConfirmed(claimMain.txid) - watchConfirmedClaimMain.replyTo ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, claimMain) - val watchHtlcsOut1 = htlcs.aliceToBob.map(_ => alice2blockchain.expectMsgType[WatchOutputSpent]) - htlcs.bobToAlice.map(_ => alice2blockchain.expectMsgType[WatchOutputSpent]) - watchHtlcsOut1.zip(htlcsTxsOut1).foreach { case (watch, tx) => watch.replyTo ! WatchOutputSpentTriggered(watch.amount, tx) } - htlcsTxsOut1.foreach { tx => - alice2blockchain.expectWatchTxConfirmed(tx.txid) - alice ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, tx) - } + alice ! WatchOutputSpentTriggered(0 sat, remoteMain.tx) + alice2blockchain.expectWatchTxConfirmed(remoteMain.tx.txid) + alice ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, remoteMain.tx) + htlcTimeout.foreach(htlcTx => { + alice ! WatchOutputSpentTriggered(htlcTx.amountIn, htlcTx.tx) + alice2blockchain.expectWatchTxConfirmed(htlcTx.tx.txid) + alice ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, htlcTx.tx) + }) + assert(alice.stateName == CLOSING) // Bob's 2nd-stage transactions confirm. - bobCommitment1.localCommit.htlcTxsAndRemoteSigs.foreach(txAndSig => alice ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, txAndSig.htlcTx.tx)) + bobHtlcTxs.foreach(htlcTx => alice ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, htlcTx.tx)) alice2blockchain.expectNoMessage(100 millis) - - checkPostSpliceState(f, spliceOutFee = 0.sat) awaitCond(alice.stateName == CLOSED) - assert(Helpers.Closing.isClosed(alice.stateData.asInstanceOf[DATA_CLOSING], None).exists(_.isInstanceOf[RemoteClose])) + assert(alice.stateData.asInstanceOf[DATA_CLOSED].closingType == "remote-close") + assert(alice.stateData.asInstanceOf[DATA_CLOSED].closingTxId == bobCommitTx1.txid) + assert(alice.stateData.asInstanceOf[DATA_CLOSED].fundingTxId == fundingTx1.txid) } - test("force-close with multiple splices (previous active revoked)", Tag(ChannelStateTestsTags.StaticRemoteKey), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + test("force-close with multiple splices (previous active revoked)") { f => import f._ val htlcs = setupHtlcs(f) @@ -2835,7 +3462,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik val fundingTx1 = initiateSplice(f, spliceIn_opt = Some(SpliceIn(500_000 sat, pushAmount = 10_000_000 msat)), spliceOut_opt = Some(SpliceOut(100_000 sat, defaultSpliceOutScriptPubKey))) checkWatchConfirmed(f, fundingTx1) // remember bob's commitment for later - val bobCommit1 = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.active.head + val bobRevokedCommitTx = bob.signCommitTx() // The first splice confirms on Bob's side. bob ! WatchFundingConfirmedTriggered(BlockHeight(400000), 42, fundingTx1) @@ -2858,37 +3485,33 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik // Alice adds another HTLC that isn't signed by Bob. val (_, htlcOut2) = addHtlc(15_000_000 msat, alice, bob, alice2bob, bob2alice) alice ! CMD_SIGN() - alice2bob.expectMsgType[CommitSig] // Bob ignores Alice's message + alice2bob.expectMsgType[CommitSigBatch] // Bob ignores Alice's message // The first splice transaction confirms. alice ! WatchFundingConfirmedTriggered(BlockHeight(400000), 42, fundingTx1) alice2blockchain.expectWatchFundingSpent(fundingTx1.txid) // Bob publishes a revoked commitment for fundingTx1! - val bobRevokedCommitTx = bobCommit1.localCommit.commitTxAndRemoteSig.commitTx.tx alice ! WatchFundingSpentTriggered(bobRevokedCommitTx) // Alice watches bob's revoked commit tx, and force-closes with her latest commitment. assert(alice2blockchain.expectMsgType[WatchAlternativeCommitTxConfirmed].txId == bobRevokedCommitTx.txid) - val aliceCommitTx2 = assertPublished(alice2blockchain, "commit-tx") - assertPublished(alice2blockchain, "local-anchor") - val claimMainDelayed2 = assertPublished(alice2blockchain, "local-main-delayed") - (htlcs.aliceToBob.map(_._2) ++ Seq(htlcOut1)).map(_ => assertPublished(alice2blockchain, "htlc-timeout")) + val aliceCommitTx2 = alice2blockchain.expectFinalTxPublished("commit-tx").tx + val localAnchor = alice2blockchain.expectReplaceableTxPublished[ClaimLocalAnchorTx] + val localMain = alice2blockchain.expectFinalTxPublished("local-main-delayed") + (htlcs.aliceToBob.map(_._2) ++ Seq(htlcOut1)).map(_ => alice2blockchain.expectReplaceableTxPublished[HtlcTimeoutTx]) alice2blockchain.expectWatchTxConfirmed(aliceCommitTx2.txid) - alice2blockchain.expectWatchTxConfirmed(claimMainDelayed2.txid) - alice2blockchain.expectMsgType[WatchOutputSpent] + alice2blockchain.expectWatchOutputsSpent(Seq(localMain.input, localAnchor.input.outPoint)) (htlcs.aliceToBob.map(_._2) ++ Seq(htlcOut1)).map(_ => alice2blockchain.expectMsgType[WatchOutputSpent]) (htlcs.bobToAlice.map(_._2) ++ Seq(htlcIn)).map(_ => alice2blockchain.expectMsgType[WatchOutputSpent]) // Bob's revoked commit tx wins. alice ! WatchAlternativeCommitTxConfirmedTriggered(BlockHeight(400000), 42, bobRevokedCommitTx) // Alice reacts by punishing bob. - val aliceClaimMain = assertPublished(alice2blockchain, "remote-main-delayed") - val aliceMainPenalty = assertPublished(alice2blockchain, "main-penalty") - val aliceHtlcsPenalty = htlcs.aliceToBob.map(_ => assertPublished(alice2blockchain, "htlc-penalty")) ++ htlcs.bobToAlice.map(_ => assertPublished(alice2blockchain, "htlc-penalty")) - aliceHtlcsPenalty.foreach(tx => Transaction.correctlySpends(tx, Seq(bobRevokedCommitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) + val remoteMain = alice2blockchain.expectFinalTxPublished("remote-main-delayed") + val mainPenalty = alice2blockchain.expectFinalTxPublished("main-penalty") + val htlcPenalty = (htlcs.aliceToBob ++ htlcs.bobToAlice).map(_ => alice2blockchain.expectFinalTxPublished("htlc-penalty")) + htlcPenalty.foreach(penalty => Transaction.correctlySpends(penalty.tx, Seq(bobRevokedCommitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) alice2blockchain.expectWatchTxConfirmed(bobRevokedCommitTx.txid) - alice2blockchain.expectWatchTxConfirmed(aliceClaimMain.txid) - assert(alice2blockchain.expectMsgType[WatchOutputSpent].txId == bobRevokedCommitTx.txid) - aliceHtlcsPenalty.map(_ => alice2blockchain.expectMsgType[WatchOutputSpent]) + alice2blockchain.expectWatchOutputsSpent(Seq(remoteMain.input, mainPenalty.input) ++ htlcPenalty.map(_.input)) alice2blockchain.expectNoMessage(100 millis) // Alice sends a failure upstream for every outgoing HTLC, including the ones that don't appear in the revoked commitment. @@ -2899,15 +3522,16 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik // Alice's penalty txs confirm. alice ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, bobRevokedCommitTx) - alice ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, aliceClaimMain) - alice ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, aliceMainPenalty) - aliceHtlcsPenalty.foreach { tx => alice ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, tx) } - + alice ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, remoteMain.tx) + alice ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, mainPenalty.tx) + htlcPenalty.foreach { penalty => alice ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, penalty.tx) } awaitCond(alice.stateName == CLOSED) - assert(Helpers.Closing.isClosed(alice.stateData.asInstanceOf[DATA_CLOSING], None).exists(_.isInstanceOf[RevokedClose])) + assert(alice.stateData.asInstanceOf[DATA_CLOSED].closingType == "revoked-close") + assert(alice.stateData.asInstanceOf[DATA_CLOSED].closingTxId == bobRevokedCommitTx.txid) + assert(alice.stateData.asInstanceOf[DATA_CLOSED].closingAmount == (Seq(remoteMain.tx, mainPenalty.tx) ++ htlcPenalty.map(_.tx)).map(_.txOut.head.amount).sum) } - test("force-close with multiple splices (inactive remote)", Tag(ChannelStateTestsTags.ZeroConf), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + test("force-close with multiple splices (inactive remote)", Tag(ChannelStateTestsTags.ZeroConf)) { f => import f._ val htlcs = setupHtlcs(f) @@ -2935,7 +3559,8 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik crossSign(alice, bob, alice2bob, bob2alice) // remember bob's commitment for later - val bobCommit1 = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.active.head + val bobCommitTx1 = bob.signCommitTx() + val bobHtlcTxs = bob.htlcTxs() initiateSplice(f, spliceIn_opt = Some(SpliceIn(500_000 sat))) val fundingTx2 = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localFundingStatus.signedTx_opt.get @@ -2966,58 +3591,45 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.active.size == 1) awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.inactive.size == 1) // bob publishes his latest (inactive) commitment for fundingTx1 - val bobCommitTx1 = bobCommit1.localCommit.commitTxAndRemoteSig.commitTx.tx alice ! WatchFundingSpentTriggered(bobCommitTx1) // alice watches bob's commit tx, and force-closes with her latest commitment assert(alice2blockchain.expectMsgType[WatchAlternativeCommitTxConfirmed].txId == bobCommitTx1.txid) - val aliceCommitTx2 = assertPublished(alice2blockchain, "commit-tx") - assertPublished(alice2blockchain, "local-anchor") - val claimMainDelayed2 = assertPublished(alice2blockchain, "local-main-delayed") - val htlcsTxsOut = htlcs.aliceToBob.map(_ => assertPublished(alice2blockchain, "htlc-timeout")) - htlcsTxsOut.foreach(tx => Transaction.correctlySpends(tx, Seq(aliceCommitTx2), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) - + val aliceCommitTx2 = alice2blockchain.expectFinalTxPublished("commit-tx").tx + val localAnchor = alice2blockchain.expectReplaceableTxPublished[ClaimLocalAnchorTx] + val localMain = alice2blockchain.expectFinalTxPublished("local-main-delayed") + htlcs.aliceToBob.map(_ => alice2blockchain.expectReplaceableTxPublished[HtlcTimeoutTx]) alice2blockchain.expectWatchTxConfirmed(aliceCommitTx2.txid) - alice2blockchain.expectWatchTxConfirmed(claimMainDelayed2.txid) - alice2blockchain.expectMsgType[WatchOutputSpent] // local-anchor - htlcs.aliceToBob.map(_ => assert(alice2blockchain.expectMsgType[WatchOutputSpent].txId == aliceCommitTx2.txid)) - htlcs.bobToAlice.map(_ => assert(alice2blockchain.expectMsgType[WatchOutputSpent].txId == aliceCommitTx2.txid)) + alice2blockchain.expectWatchOutputsSpent(Seq(localMain.input, localAnchor.input.outPoint)) + htlcs.aliceToBob.map(_ => alice2blockchain.expectMsgType[WatchOutputSpent]) + htlcs.bobToAlice.map(_ => alice2blockchain.expectMsgType[WatchOutputSpent]) alice2blockchain.expectNoMessage(100 millis) // bob's remote tx wins alice ! WatchAlternativeCommitTxConfirmedTriggered(BlockHeight(400000), 42, bobCommitTx1) // we're back to the normal handling of remote commit - inside(alice2blockchain.expectMsgType[PublishReplaceableTx]) { tx => - assert(tx.txInfo.isInstanceOf[Transactions.ClaimLocalAnchorOutputTx]) - assert(tx.commitTx == bobCommitTx1) - } - val claimMain = alice2blockchain.expectMsgType[PublishFinalTx].tx - val claimHtlcsTxsOut = htlcs.aliceToBob.map(_ => assertPublished(alice2blockchain, "claim-htlc-timeout")) - claimHtlcsTxsOut.foreach(tx => Transaction.correctlySpends(tx, Seq(bobCommitTx1), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) - awaitCond(wallet.asInstanceOf[SingleKeyOnChainWallet].abandoned.contains(fundingTx2.txid)) - - val watchConfirmedRemoteCommit = alice2blockchain.expectWatchTxConfirmed(bobCommitTx1.txid) + val remoteAnchor = alice2blockchain.expectReplaceableTxPublished[ClaimRemoteAnchorTx] + val remoteMain = alice2blockchain.expectFinalTxPublished("remote-main-delayed") + val claimHtlcTimeout = htlcs.aliceToBob.map(_ => alice2blockchain.expectReplaceableTxPublished[ClaimHtlcTimeoutTx]) + claimHtlcTimeout.foreach(htlcTx => Transaction.correctlySpends(htlcTx.sign(), Seq(bobCommitTx1), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) + awaitCond(aliceWallet.asInstanceOf[SingleKeyOnChainWallet].abandoned.contains(fundingTx2.txid)) // this one fires immediately, tx is already confirmed - watchConfirmedRemoteCommit.replyTo ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, bobCommitTx1) - val watchConfirmedClaimMain = alice2blockchain.expectWatchTxConfirmed(claimMain.txid) - watchConfirmedClaimMain.replyTo ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, claimMain) + alice2blockchain.expectWatchTxConfirmed(bobCommitTx1.txid) + alice ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, bobCommitTx1) + alice2blockchain.expectWatchOutputsSpent(Seq(remoteMain.input, remoteAnchor.input.outPoint)) // watch alice and bob's htlcs and publish alice's htlcs-timeout txs - htlcs.aliceToBob.map(_ => assert(alice2blockchain.expectMsgType[WatchOutputSpent].txId == bobCommitTx1.txid)) - htlcs.bobToAlice.map(_ => assert(alice2blockchain.expectMsgType[WatchOutputSpent].txId == bobCommitTx1.txid)) - claimHtlcsTxsOut.foreach { tx => alice ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, tx) } - - // publish bob's htlc-timeout txs - bobCommit1.localCommit.htlcTxsAndRemoteSigs.foreach(txAndSigs => alice ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, txAndSigs.htlcTx.tx)) + htlcs.aliceToBob.map(_ => alice2blockchain.expectMsgType[WatchOutputSpent]) + htlcs.bobToAlice.map(_ => alice2blockchain.expectMsgType[WatchOutputSpent]) + alice ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, remoteMain.tx) + claimHtlcTimeout.foreach(htlcTx => alice ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, htlcTx.tx)) + assert(alice.stateName == CLOSING) + // Bob's htlc-timeout txs confirm. + bobHtlcTxs.foreach(htlcTx => alice ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, htlcTx.tx)) alice2blockchain.expectNoMessage(100 millis) - - // alice's final commitment includes the initial htlcs, but not bob's payment - checkPostSpliceState(f, spliceOutFee = 0.sat) - - // done awaitCond(alice.stateName == CLOSED) - assert(Helpers.Closing.isClosed(alice.stateData.asInstanceOf[DATA_CLOSING], None).exists(_.isInstanceOf[RemoteClose])) + assert(alice.stateData.asInstanceOf[DATA_CLOSED].closingTxId == bobCommitTx1.txid) } - test("force-close with multiple splices (inactive revoked)", Tag(ChannelStateTestsTags.ZeroConf), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + test("force-close with multiple splices (inactive revoked)", Tag(ChannelStateTestsTags.ZeroConf), Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => import f._ val htlcs = setupHtlcs(f) @@ -3027,7 +3639,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik alice2blockchain.expectWatchPublished(fundingTx1.txid) bob2blockchain.expectWatchPublished(fundingTx1.txid) // remember bob's commitment for later - val bobCommit1 = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.active.head + val bobRevokedCommitTx = bob.signCommitTx() // splice 1 gets published alice ! WatchPublishedTriggered(fundingTx1) bob ! WatchPublishedTriggered(fundingTx1) @@ -3078,33 +3690,29 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.active.size == 1) awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.inactive.size == 1) // bob publishes his latest commitment for fundingTx1, which is now revoked - val bobRevokedCommitTx = bobCommit1.localCommit.commitTxAndRemoteSig.commitTx.tx alice ! WatchFundingSpentTriggered(bobRevokedCommitTx) // alice watches bob's revoked commit tx, and force-closes with her latest commitment assert(alice2blockchain.expectMsgType[WatchAlternativeCommitTxConfirmed].txId == bobRevokedCommitTx.txid) - val aliceCommitTx2 = assertPublished(alice2blockchain, "commit-tx") - assertPublished(alice2blockchain, "local-anchor") - val claimMainDelayed2 = assertPublished(alice2blockchain, "local-main-delayed") - (htlcs.aliceToBob.map(_._2) ++ Seq(htlcOut1)).map(_ => assertPublished(alice2blockchain, "htlc-timeout")) + val aliceCommitTx2 = alice2blockchain.expectFinalTxPublished("commit-tx").tx + val localAnchor = alice2blockchain.expectReplaceableTxPublished[ClaimLocalAnchorTx] + val localMain = alice2blockchain.expectFinalTxPublished("local-main-delayed") + (htlcs.aliceToBob.map(_._2) ++ Seq(htlcOut1)).map(_ => alice2blockchain.expectReplaceableTxPublished[HtlcTimeoutTx]) alice2blockchain.expectWatchTxConfirmed(aliceCommitTx2.txid) - alice2blockchain.expectWatchTxConfirmed(claimMainDelayed2.txid) - alice2blockchain.expectMsgType[WatchOutputSpent] // local-anchor - (htlcs.aliceToBob.map(_._2) ++ Seq(htlcOut1)).foreach(_ => assert(alice2blockchain.expectMsgType[WatchOutputSpent].txId == aliceCommitTx2.txid)) - (htlcs.bobToAlice.map(_._2) ++ Seq(htlcIn)).foreach(_ => assert(alice2blockchain.expectMsgType[WatchOutputSpent].txId == aliceCommitTx2.txid)) + alice2blockchain.expectWatchOutputsSpent(Seq(localMain.input, localAnchor.input.outPoint)) + (htlcs.aliceToBob.map(_._2) ++ Seq(htlcOut1)).foreach(_ => alice2blockchain.expectMsgType[WatchOutputSpent]) + (htlcs.bobToAlice.map(_._2) ++ Seq(htlcIn)).foreach(_ => alice2blockchain.expectMsgType[WatchOutputSpent]) alice2blockchain.expectNoMessage(100 millis) // bob's revoked tx wins alice ! WatchAlternativeCommitTxConfirmedTriggered(BlockHeight(400000), 42, bobRevokedCommitTx) // alice reacts by punishing bob - val aliceClaimMain = assertPublished(alice2blockchain, "remote-main-delayed") - val aliceMainPenalty = assertPublished(alice2blockchain, "main-penalty") - val aliceHtlcsPenalty = htlcs.aliceToBob.map(_ => assertPublished(alice2blockchain, "htlc-penalty")) ++ htlcs.bobToAlice.map(_ => assertPublished(alice2blockchain, "htlc-penalty")) + val remoteMain = alice2blockchain.expectFinalTxPublished("remote-main-delayed") + val mainPenalty = alice2blockchain.expectFinalTxPublished("main-penalty") + val htlcPenalty = (htlcs.aliceToBob ++ htlcs.bobToAlice).map(_ => alice2blockchain.expectFinalTxPublished("htlc-penalty")) alice2blockchain.expectWatchTxConfirmed(bobRevokedCommitTx.txid) - alice2blockchain.expectWatchTxConfirmed(aliceClaimMain.txid) - assert(alice2blockchain.expectMsgType[WatchOutputSpent].txId == bobRevokedCommitTx.txid) // main-penalty - aliceHtlcsPenalty.map(_ => assert(alice2blockchain.expectMsgType[WatchOutputSpent].txId == bobRevokedCommitTx.txid)) - awaitCond(wallet.asInstanceOf[SingleKeyOnChainWallet].abandoned.contains(fundingTx2.txid)) + alice2blockchain.expectWatchOutputsSpent(Seq(remoteMain.input, mainPenalty.input) ++ htlcPenalty.map(_.input)) alice2blockchain.expectNoMessage(100 millis) + awaitCond(aliceWallet.asInstanceOf[SingleKeyOnChainWallet].abandoned.contains(fundingTx2.txid)) // Alice sends a failure upstream for every outgoing HTLC, including the ones that don't appear in the revoked commitment. val outgoingHtlcs = (htlcs.aliceToBob.map(_._2) ++ Set(htlcOut1, htlcOut2)).map(htlc => (htlc, alice.stateData.asInstanceOf[DATA_CLOSING].commitments.originChannels(htlc.id))) @@ -3114,20 +3722,315 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik // all penalty txs confirm alice ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, bobRevokedCommitTx) - alice ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, aliceClaimMain) - alice ! WatchOutputSpentTriggered(aliceMainPenalty.txOut(0).amount, aliceMainPenalty) - alice2blockchain.expectWatchTxConfirmed(aliceMainPenalty.txid) - alice ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, aliceMainPenalty) - aliceHtlcsPenalty.foreach { tx => alice ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, tx) } + alice ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, remoteMain.tx) + alice ! WatchOutputSpentTriggered(0 sat, mainPenalty.tx) + alice2blockchain.expectWatchTxConfirmed(mainPenalty.tx.txid) + alice ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, mainPenalty.tx) + htlcPenalty.foreach { penalty => alice ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, penalty.tx) } + awaitCond(alice.stateName == CLOSED) + assert(alice.stateData.asInstanceOf[DATA_CLOSED].closingTxId == bobRevokedCommitTx.txid) + } + + test("force-close after channel type upgrade (latest active)", Tag(ChannelStateTestsTags.AnchorOutputsPhoenix)) { f => + import f._ + + val htlcs = setupHtlcs(f) + + // Our first splice upgrades the channel to taproot. + val fundingTx1 = initiateSplice(f, spliceIn_opt = Some(SpliceIn(500_000 sat)), channelType_opt = Some(ChannelTypes.SimpleTaprootChannelsPhoenix)) + checkWatchConfirmed(f, fundingTx1) + + // The first splice confirms on Bob's side. + bob ! WatchFundingConfirmedTriggered(BlockHeight(400000), 42, fundingTx1) + bob2blockchain.expectMsgTypeHaving[WatchFundingSpent](_.txId == fundingTx1.txid) + bob2alice.expectMsgTypeHaving[SpliceLocked](_.fundingTxId == fundingTx1.txid) + bob2alice.forward(alice) + // The second splice preserves the taproot commitment format. + val fundingTx2 = initiateSplice(f, spliceOut_opt = Some(SpliceOut(100_000 sat, defaultSpliceOutScriptPubKey))) + checkWatchConfirmed(f, fundingTx2) + assert(alice.commitments.active.map(_.commitmentFormat).count(_ == UnsafeLegacyAnchorOutputsCommitmentFormat) == 1) + assert(alice.commitments.active.map(_.commitmentFormat).count(_ == PhoenixSimpleTaprootChannelCommitmentFormat) == 2) + + // From Alice's point of view, we now have two unconfirmed splices. + alice ! CMD_FORCECLOSE(ActorRef.noSender) + alice2bob.expectMsgType[Error] + val commitTx2 = alice2blockchain.expectFinalTxPublished("commit-tx").tx + Transaction.correctlySpends(commitTx2, Seq(fundingTx2), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + val aliceAnchorTx = alice2blockchain.expectReplaceableTxPublished[ClaimLocalAnchorTx] + val claimMainAlice = alice2blockchain.expectFinalTxPublished("local-main-delayed") + Transaction.correctlySpends(claimMainAlice.tx, Seq(commitTx2), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + // Alice publishes her htlc timeout transactions. + val aliceHtlcTimeout = htlcs.aliceToBob.map(_ => alice2blockchain.expectReplaceableTxPublished[HtlcTimeoutTx]) + aliceHtlcTimeout.foreach(htlcTx => Transaction.correctlySpends(htlcTx.sign(), Seq(commitTx2), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) + + // Bob detects Alice's commit tx. + bob ! WatchFundingSpentTriggered(commitTx2) + val bobAnchorTx = bob2blockchain.expectReplaceableTxPublished[ClaimRemoteAnchorTx] + val claimMainBob = bob2blockchain.expectFinalTxPublished("remote-main-delayed") + Transaction.correctlySpends(claimMainBob.tx, Seq(commitTx2), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + val bobHtlcTimeout = htlcs.bobToAlice.map(_ => bob2blockchain.expectReplaceableTxPublished[ClaimHtlcTimeoutTx]) + bobHtlcTimeout.foreach(htlcTx => Transaction.correctlySpends(htlcTx.sign(), Seq(commitTx2), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) + bob2blockchain.expectWatchTxConfirmed(commitTx2.txid) + bob2blockchain.expectWatchOutputsSpent(Seq(claimMainBob.input, bobAnchorTx.input.outPoint) ++ aliceHtlcTimeout.map(_.input.outPoint) ++ bobHtlcTimeout.map(_.input.outPoint)) + alice2blockchain.expectWatchTxConfirmed(commitTx2.txid) + alice2blockchain.expectWatchOutputsSpent(Seq(claimMainAlice.input, aliceAnchorTx.input.outPoint) ++ aliceHtlcTimeout.map(_.input.outPoint) ++ bobHtlcTimeout.map(_.input.outPoint)) + + // The first splice transaction confirms. + alice ! WatchFundingConfirmedTriggered(BlockHeight(400000), 42, fundingTx1) + alice2blockchain.expectMsgType[WatchFundingSpent] + + // The second splice transaction confirms. + alice ! WatchFundingConfirmedTriggered(BlockHeight(400000), 42, fundingTx2) + alice2blockchain.expectMsgType[WatchFundingSpent] + + // Alice detects that the commit confirms, along with 2nd-stage and 3rd-stage transactions. + alice ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, commitTx2) + alice ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, claimMainAlice.tx) + aliceHtlcTimeout.foreach(htlcTx => { + alice ! WatchOutputSpentTriggered(0 sat, htlcTx.tx) + alice2blockchain.expectWatchTxConfirmed(htlcTx.tx.txid) + alice ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, htlcTx.tx) + val htlcDelayed = alice2blockchain.expectFinalTxPublished("htlc-delayed") + alice2blockchain.expectWatchOutputSpent(htlcDelayed.input) + alice ! WatchOutputSpentTriggered(0 sat, htlcDelayed.tx) + alice2blockchain.expectWatchTxConfirmed(htlcDelayed.tx.txid) + alice ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, htlcDelayed.tx) + }) + bobHtlcTimeout.foreach(htlcTx => { + alice ! WatchOutputSpentTriggered(htlcTx.amountIn, htlcTx.tx) + alice2blockchain.expectWatchTxConfirmed(htlcTx.tx.txid) + alice ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, htlcTx.tx) + }) + alice2blockchain.expectNoMessage(100 millis) awaitCond(alice.stateName == CLOSED) - assert(Helpers.Closing.isClosed(alice.stateData.asInstanceOf[DATA_CLOSING], None).exists(_.isInstanceOf[RevokedClose])) + assert(alice.stateData.asInstanceOf[DATA_CLOSED].closingType == "local-close") + assert(alice.stateData.asInstanceOf[DATA_CLOSED].closingTxId == commitTx2.txid) + assert(alice.stateData.asInstanceOf[DATA_CLOSED].fundingTxId == fundingTx2.txid) + + // Bob also detects that the commit confirms, along with 2nd-stage transactions. + bob ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, commitTx2) + bob ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, claimMainBob.tx) + bobHtlcTimeout.foreach(htlcTx => { + bob ! WatchOutputSpentTriggered(htlcTx.amountIn, htlcTx.tx) + bob2blockchain.expectWatchTxConfirmed(htlcTx.tx.txid) + bob ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, htlcTx.tx) + }) + aliceHtlcTimeout.foreach(htlcTx => { + bob ! WatchOutputSpentTriggered(0 sat, htlcTx.tx) + bob2blockchain.expectWatchTxConfirmed(htlcTx.tx.txid) + bob ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, htlcTx.tx) + }) + bob2blockchain.expectNoMessage(100 millis) + awaitCond(bob.stateName == CLOSED) + assert(bob.stateData.asInstanceOf[DATA_CLOSED].closingType == "remote-close") + assert(bob.stateData.asInstanceOf[DATA_CLOSED].closingTxId == commitTx2.txid) + assert(bob.stateData.asInstanceOf[DATA_CLOSED].fundingTxId == fundingTx2.txid) + } + + test("force-close after channel type upgrade (previous active)", Tag(ChannelStateTestsTags.AnchorOutputsPhoenix)) { f => + import f._ + + val htlcs = setupHtlcs(f) + + // Our splice upgrades the channel to taproot. + val spliceTx = initiateSplice(f, spliceIn_opt = Some(SpliceIn(500_000 sat)), channelType_opt = Some(ChannelTypes.SimpleTaprootChannelsPhoenix)) + assert(alice.commitments.active.head.commitmentFormat == PhoenixSimpleTaprootChannelCommitmentFormat) + assert(alice.commitments.active.last.commitmentFormat == UnsafeLegacyAnchorOutputsCommitmentFormat) + checkWatchConfirmed(f, spliceTx) + + // Alice force-closes using the non-taproot commitment. + val aliceCommitTx = alice.commitments.active.last.fullySignedLocalCommitTx(alice.commitments.channelParams, alice.underlyingActor.channelKeys) + bob ! WatchFundingSpentTriggered(aliceCommitTx) + assert(bob2blockchain.expectMsgType[WatchAlternativeCommitTxConfirmed].txId == aliceCommitTx.txid) + // Bob reacts by publishing the taproot commitment. + val bobCommitTx = bob2blockchain.expectFinalTxPublished("commit-tx").tx + Transaction.correctlySpends(bobCommitTx, Seq(spliceTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + val localAnchor = bob2blockchain.expectReplaceableTxPublished[ClaimLocalAnchorTx] + val localMain = bob2blockchain.expectFinalTxPublished("local-main-delayed") + htlcs.bobToAlice.map(_ => bob2blockchain.expectReplaceableTxPublished[HtlcTimeoutTx]) + bob2blockchain.expectWatchTxConfirmed(bobCommitTx.txid) + bob2blockchain.expectWatchOutputSpent(localMain.input) + bob2blockchain.expectWatchOutputSpent(localAnchor.input.outPoint) + (htlcs.aliceToBob.map(_._2) ++ htlcs.bobToAlice.map(_._2)).foreach(_ => bob2blockchain.expectMsgType[WatchOutputSpent]) + bob2blockchain.expectNoMessage(100 millis) + + // Alice's commit tx confirms. + bob ! WatchAlternativeCommitTxConfirmedTriggered(BlockHeight(450_000), 5, aliceCommitTx) + val anchorTx = bob2blockchain.expectReplaceableTxPublished[ClaimRemoteAnchorTx] + val mainTx = bob2blockchain.expectFinalTxPublished("remote-main-delayed") + Transaction.correctlySpends(mainTx.tx, Seq(aliceCommitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + val bobHtlcTimeout = htlcs.bobToAlice.map(_ => bob2blockchain.expectReplaceableTxPublished[ClaimHtlcTimeoutTx]) + bobHtlcTimeout.foreach(htlcTx => Transaction.correctlySpends(htlcTx.sign(), Seq(aliceCommitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) + bob2blockchain.expectWatchTxConfirmed(aliceCommitTx.txid) + bob2blockchain.expectWatchOutputSpent(mainTx.input) + bob2blockchain.expectWatchOutputSpent(anchorTx.input.outPoint) + (htlcs.aliceToBob.map(_._2) ++ htlcs.bobToAlice.map(_._2)).foreach(_ => bob2blockchain.expectMsgType[WatchOutputSpent]) + bob2blockchain.expectNoMessage(100 millis) + } + + test("force-close after channel type upgrade (revoked previous active)", Tag(ChannelStateTestsTags.AnchorOutputsPhoenix)) { f => + import f._ + + val htlcs = setupHtlcs(f) + + // Our splice upgrades the channel to taproot. + val spliceTx = initiateSplice(f, spliceIn_opt = Some(SpliceIn(500_000 sat)), channelType_opt = Some(ChannelTypes.SimpleTaprootChannelsPhoenix)) + assert(alice.commitments.active.head.commitmentFormat == PhoenixSimpleTaprootChannelCommitmentFormat) + assert(alice.commitments.active.last.commitmentFormat == UnsafeLegacyAnchorOutputsCommitmentFormat) + checkWatchConfirmed(f, spliceTx) + + // Alice will force-close using a non-taproot revoked commitment. + val aliceCommitTx = alice.commitments.active.last.fullySignedLocalCommitTx(alice.commitments.channelParams, alice.underlyingActor.channelKeys) + addHtlc(20_000_000 msat, alice, bob, alice2bob, bob2alice) + crossSign(alice, bob, alice2bob, bob2alice) + bob ! WatchFundingSpentTriggered(aliceCommitTx) + assert(bob2blockchain.expectMsgType[WatchAlternativeCommitTxConfirmed].txId == aliceCommitTx.txid) + // Bob reacts by publishing the taproot commitment. + val bobCommitTx = bob2blockchain.expectFinalTxPublished("commit-tx").tx + Transaction.correctlySpends(bobCommitTx, Seq(spliceTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + val localAnchor = bob2blockchain.expectReplaceableTxPublished[ClaimLocalAnchorTx] + val localMain = bob2blockchain.expectFinalTxPublished("local-main-delayed") + htlcs.bobToAlice.map(_ => bob2blockchain.expectReplaceableTxPublished[HtlcTimeoutTx]) + bob2blockchain.expectWatchTxConfirmed(bobCommitTx.txid) + bob2blockchain.expectWatchOutputSpent(localMain.input) + bob2blockchain.expectWatchOutputSpent(localAnchor.input.outPoint) + (htlcs.aliceToBob.map(_._2) ++ htlcs.bobToAlice.map(_._2)).foreach(_ => bob2blockchain.expectMsgType[WatchOutputSpent]) + bob2blockchain.expectMsgType[WatchOutputSpent] // newly added HTLC + bob2blockchain.expectNoMessage(100 millis) + + // Alice's commit tx confirms. + bob ! WatchAlternativeCommitTxConfirmedTriggered(BlockHeight(450_000), 5, aliceCommitTx) + val mainTx = bob2blockchain.expectFinalTxPublished("remote-main-delayed") + Transaction.correctlySpends(mainTx.tx, Seq(aliceCommitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + val penaltyTx = bob2blockchain.expectFinalTxPublished("main-penalty") + Transaction.correctlySpends(penaltyTx.tx, Seq(aliceCommitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + val htlcPenalty = (htlcs.aliceToBob ++ htlcs.bobToAlice).map(_ => bob2blockchain.expectFinalTxPublished("htlc-penalty")) + htlcPenalty.foreach(penalty => Transaction.correctlySpends(penalty.tx, Seq(aliceCommitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) + bob2blockchain.expectWatchTxConfirmed(aliceCommitTx.txid) + bob2blockchain.expectWatchOutputsSpent(Seq(mainTx.input, penaltyTx.input) ++ htlcPenalty.map(_.input)) + bob2blockchain.expectNoMessage(100 millis) + + // Bob's penalty txs confirm. + bob ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, aliceCommitTx) + bob ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, mainTx.tx) + bob ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, penaltyTx.tx) + htlcPenalty.foreach { penalty => bob ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, penalty.tx) } + awaitCond(bob.stateName == CLOSED) + assert(bob.stateData.asInstanceOf[DATA_CLOSED].closingType == "revoked-close") + assert(bob.stateData.asInstanceOf[DATA_CLOSED].closingTxId == aliceCommitTx.txid) + } + + test("force-close after channel type upgrade (revoked latest active)", Tag(ChannelStateTestsTags.AnchorOutputsPhoenix)) { f => + import f._ + + val htlcs = setupHtlcs(f) + + // Our splice upgrades the channel to taproot. + val spliceTx = initiateSplice(f, spliceIn_opt = Some(SpliceIn(500_000 sat)), channelType_opt = Some(ChannelTypes.SimpleTaprootChannelsPhoenix)) + assert(alice.commitments.active.head.commitmentFormat == PhoenixSimpleTaprootChannelCommitmentFormat) + assert(alice.commitments.active.last.commitmentFormat == UnsafeLegacyAnchorOutputsCommitmentFormat) + checkWatchConfirmed(f, spliceTx) + + // Alice will force-close using a taproot revoked commitment. + val aliceCommitTx = alice.commitments.active.head.fullySignedLocalCommitTx(alice.commitments.channelParams, alice.underlyingActor.channelKeys) + addHtlc(20_000_000 msat, alice, bob, alice2bob, bob2alice) + crossSign(alice, bob, alice2bob, bob2alice) + bob ! WatchFundingSpentTriggered(aliceCommitTx) + // Bob reacts by publishing penalty transactions. + val mainTx = bob2blockchain.expectFinalTxPublished("remote-main-delayed") + Transaction.correctlySpends(mainTx.tx, Seq(aliceCommitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + val penaltyTx = bob2blockchain.expectFinalTxPublished("main-penalty") + Transaction.correctlySpends(penaltyTx.tx, Seq(aliceCommitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + val htlcPenalty = (htlcs.aliceToBob ++ htlcs.bobToAlice).map(_ => bob2blockchain.expectFinalTxPublished("htlc-penalty")) + htlcPenalty.foreach(penalty => Transaction.correctlySpends(penalty.tx, Seq(aliceCommitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) + bob2blockchain.expectWatchTxConfirmed(aliceCommitTx.txid) + bob2blockchain.expectWatchOutputsSpent(Seq(mainTx.input, penaltyTx.input) ++ htlcPenalty.map(_.input)) + bob2blockchain.expectNoMessage(100 millis) + + // Bob's penalty txs confirm. + bob ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, aliceCommitTx) + bob ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, mainTx.tx) + bob ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, penaltyTx.tx) + htlcPenalty.foreach { penalty => bob ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, penalty.tx) } + awaitCond(bob.stateName == CLOSED) + assert(bob.stateData.asInstanceOf[DATA_CLOSED].closingType == "revoked-close") + assert(bob.stateData.asInstanceOf[DATA_CLOSED].closingTxId == aliceCommitTx.txid) + assert(bob.stateData.asInstanceOf[DATA_CLOSED].fundingTxId == spliceTx.txid) + } + + test("force-close after channel type upgrade (revoked previous inactive)", Tag(ChannelStateTestsTags.AnchorOutputsPhoenix), Tag(ChannelStateTestsTags.ZeroConf)) { f => + import f._ + + val htlcs = setupHtlcs(f) + + // Our splice upgrades the channel to taproot. + val spliceTx = initiateSplice(f, spliceIn_opt = Some(SpliceIn(500_000 sat)), channelType_opt = Some(ChannelTypes.SimpleTaprootChannelsPhoenix)) + assert(alice.commitments.active.head.commitmentFormat == PhoenixSimpleTaprootChannelCommitmentFormat) + assert(alice.commitments.active.last.commitmentFormat == UnsafeLegacyAnchorOutputsCommitmentFormat) + assert(alice2blockchain.expectMsgType[WatchPublished].txId == spliceTx.txid) + assert(bob2blockchain.expectMsgType[WatchPublished].txId == spliceTx.txid) + + // Alice will force-close using a non-taproot revoked inactive commitment. + val aliceCommitTx = alice.commitments.active.last.fullySignedLocalCommitTx(alice.commitments.channelParams, alice.underlyingActor.channelKeys) + // Alice and Bob send splice_locked: Alice's commitment is now inactive. + alice ! WatchPublishedTriggered(spliceTx) + alice2blockchain.expectWatchFundingConfirmed(spliceTx.txid) + alice2bob.expectMsgType[SpliceLocked] + alice2bob.forward(bob) + bob ! WatchPublishedTriggered(spliceTx) + bob2blockchain.expectWatchFundingConfirmed(spliceTx.txid) + bob2alice.expectMsgType[SpliceLocked] + bob2alice.forward(alice) + awaitCond(bob.commitments.active.size == 1) + awaitCond(bob.commitments.inactive.size == 1) + + // Alice and Bob update the channel: Alice's commitment is now inactive and revoked. + addHtlc(20_000_000 msat, alice, bob, alice2bob, bob2alice) + crossSign(alice, bob, alice2bob, bob2alice) + + // Alice publishes her revoked commitment: Bob reacts by publishing the latest commitment. + bob ! WatchFundingSpentTriggered(aliceCommitTx) + assert(bob2blockchain.expectMsgType[WatchAlternativeCommitTxConfirmed].txId == aliceCommitTx.txid) + // Bob reacts by publishing the taproot commitment. + val bobCommitTx = bob2blockchain.expectFinalTxPublished("commit-tx").tx + Transaction.correctlySpends(bobCommitTx, Seq(spliceTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + val localAnchor = bob2blockchain.expectReplaceableTxPublished[ClaimLocalAnchorTx] + val localMain = bob2blockchain.expectFinalTxPublished("local-main-delayed") + htlcs.bobToAlice.map(_ => bob2blockchain.expectReplaceableTxPublished[HtlcTimeoutTx]) + bob2blockchain.expectWatchTxConfirmed(bobCommitTx.txid) + bob2blockchain.expectWatchOutputSpent(localMain.input) + bob2blockchain.expectWatchOutputSpent(localAnchor.input.outPoint) + (htlcs.aliceToBob.map(_._2) ++ htlcs.bobToAlice.map(_._2)).foreach(_ => bob2blockchain.expectMsgType[WatchOutputSpent]) + bob2blockchain.expectMsgType[WatchOutputSpent] // newly added HTLC + bob2blockchain.expectNoMessage(100 millis) + + // Alice's revoked commit tx confirms. + bob ! WatchAlternativeCommitTxConfirmedTriggered(BlockHeight(450_000), 5, aliceCommitTx) + val mainTx = bob2blockchain.expectFinalTxPublished("remote-main-delayed") + Transaction.correctlySpends(mainTx.tx, Seq(aliceCommitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + val penaltyTx = bob2blockchain.expectFinalTxPublished("main-penalty") + Transaction.correctlySpends(penaltyTx.tx, Seq(aliceCommitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + val htlcPenalty = (htlcs.aliceToBob ++ htlcs.bobToAlice).map(_ => bob2blockchain.expectFinalTxPublished("htlc-penalty")) + htlcPenalty.foreach(penalty => Transaction.correctlySpends(penalty.tx, Seq(aliceCommitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) + bob2blockchain.expectWatchTxConfirmed(aliceCommitTx.txid) + bob2blockchain.expectWatchOutputsSpent(Seq(mainTx.input, penaltyTx.input) ++ htlcPenalty.map(_.input)) + bob2blockchain.expectNoMessage(100 millis) + + // Bob's penalty txs confirm. + bob ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, aliceCommitTx) + bob ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, mainTx.tx) + bob ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, penaltyTx.tx) + htlcPenalty.foreach { penalty => bob ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, penalty.tx) } + awaitCond(bob.stateName == CLOSED) + assert(bob.stateData.asInstanceOf[DATA_CLOSED].closingType == "revoked-close") + assert(bob.stateData.asInstanceOf[DATA_CLOSED].closingTxId == aliceCommitTx.txid) } test("put back watches after restart") { f => import f._ - val fundingTx0 = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localFundingStatus.signedTx_opt.get + val fundingTxId0 = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.fundingTxId val fundingTx1 = initiateSplice(f, spliceIn_opt = Some(SpliceIn(500_000 sat, pushAmount = 10_000_000 msat)), spliceOut_opt = Some(SpliceOut(100_000 sat, defaultSpliceOutScriptPubKey))) checkWatchConfirmed(f, fundingTx1) @@ -3145,7 +4048,9 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik val (alicePeer, bobPeer) = (alice.getParent, bob.getParent) val aliceData = alice.stateData.asInstanceOf[PersistentChannelData] + val aliceKeys = alice.underlyingActor.channelKeys val bobData = bob.stateData.asInstanceOf[PersistentChannelData] + val bobKeys = bob.underlyingActor.channelKeys alice.stop() bob.stop() @@ -3153,24 +4058,24 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik alice2blockchain.expectNoMessage(100 millis) bob2blockchain.expectNoMessage(100 millis) - val alice2 = TestFSMRef(new Channel(aliceNodeParams, wallet, bobNodeParams.nodeId, alice2blockchain.ref, TestProbe().ref, FakeTxPublisherFactory(alice2blockchain)), alicePeer) + val alice2 = TestFSMRef(new Channel(aliceNodeParams, aliceKeys, aliceWallet, bobNodeParams.nodeId, alice2blockchain.ref, TestProbe().ref, FakeTxPublisherFactory(alice2blockchain)), alicePeer) alice2 ! INPUT_RESTORED(aliceData) alice2blockchain.expectMsgType[SetChannelId] alice2blockchain.expectWatchFundingConfirmed(fundingTx2.txid) alice2blockchain.expectWatchFundingConfirmed(fundingTx1.txid) - alice2blockchain.expectWatchFundingSpent(fundingTx0.txid) + alice2blockchain.expectWatchFundingSpent(fundingTxId0) alice2blockchain.expectNoMessage(100 millis) - val bob2 = TestFSMRef(new Channel(bobNodeParams, wallet, aliceNodeParams.nodeId, bob2blockchain.ref, TestProbe().ref, FakeTxPublisherFactory(bob2blockchain)), bobPeer) + val bob2 = TestFSMRef(new Channel(bobNodeParams, bobKeys, bobWallet, aliceNodeParams.nodeId, bob2blockchain.ref, TestProbe().ref, FakeTxPublisherFactory(bob2blockchain)), bobPeer) bob2 ! INPUT_RESTORED(bobData) bob2blockchain.expectMsgType[SetChannelId] bob2blockchain.expectWatchFundingConfirmed(fundingTx2.txid) bob2blockchain.expectWatchFundingSpent(fundingTx1.txid) - bob2blockchain.expectWatchFundingSpent(fundingTx0.txid) + bob2blockchain.expectWatchFundingSpent(fundingTxId0) bob2blockchain.expectNoMessage(100 millis) } - test("put back watches after restart (inactive)", Tag(ChannelStateTestsTags.ZeroConf), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + test("put back watches after restart (inactive)", Tag(ChannelStateTestsTags.ZeroConf)) { f => import f._ val fundingTx0 = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localFundingStatus.signedTx_opt.get @@ -3204,7 +4109,9 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik val (alicePeer, bobPeer) = (alice.getParent, bob.getParent) val aliceData = alice.stateData.asInstanceOf[PersistentChannelData] + val aliceKeys = alice.underlyingActor.channelKeys val bobData = bob.stateData.asInstanceOf[PersistentChannelData] + val bobKeys = bob.underlyingActor.channelKeys alice.stop() bob.stop() @@ -3212,7 +4119,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik alice2blockchain.expectNoMessage(100 millis) bob2blockchain.expectNoMessage(100 millis) - val alice2 = TestFSMRef(new Channel(aliceNodeParams, wallet, bobNodeParams.nodeId, alice2blockchain.ref, TestProbe().ref, FakeTxPublisherFactory(alice2blockchain)), alicePeer) + val alice2 = TestFSMRef(new Channel(aliceNodeParams, aliceKeys, aliceWallet, bobNodeParams.nodeId, alice2blockchain.ref, TestProbe().ref, FakeTxPublisherFactory(alice2blockchain)), alicePeer) alice2 ! INPUT_RESTORED(aliceData) alice2blockchain.expectMsgType[SetChannelId] alice2blockchain.expectWatchPublished(fundingTx2.txid) @@ -3220,7 +4127,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik alice2blockchain.expectWatchFundingSpent(fundingTx0.txid) alice2blockchain.expectNoMessage(100 millis) - val bob2 = TestFSMRef(new Channel(bobNodeParams, wallet, aliceNodeParams.nodeId, bob2blockchain.ref, TestProbe().ref, FakeTxPublisherFactory(bob2blockchain)), bobPeer) + val bob2 = TestFSMRef(new Channel(bobNodeParams, bobKeys, bobWallet, aliceNodeParams.nodeId, bob2blockchain.ref, TestProbe().ref, FakeTxPublisherFactory(bob2blockchain)), bobPeer) bob2 ! INPUT_RESTORED(bobData) bob2blockchain.expectMsgType[SetChannelId] bob2blockchain.expectWatchPublished(fundingTx2.txid) @@ -3229,7 +4136,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik bob2blockchain.expectNoMessage(100 millis) } - test("recv CMD_SPLICE (splice-in + splice-out) with pre and post splice htlcs") { f => + def spliceWithPreAndPostHtlcs(f: FixtureParam, commitmentFormat: CommitmentFormat): Unit = { import f._ val htlcs = setupHtlcs(f) @@ -3240,13 +4147,15 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik crossSign(bob, alice, bob2alice, alice2bob) val aliceCommitments1 = alice.stateData.asInstanceOf[DATA_NORMAL].commitments aliceCommitments1.active.foreach { c => - val commitTx = c.fullySignedLocalCommitTx(aliceCommitments1.params, alice.underlyingActor.keyManager).tx - Transaction.correctlySpends(commitTx, Map(c.commitInput.outPoint -> c.commitInput.txOut), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + assert(c.commitmentFormat == commitmentFormat) + val commitTx = c.fullySignedLocalCommitTx(aliceCommitments1.channelParams, alice.underlyingActor.channelKeys) + Transaction.correctlySpends(commitTx, Map(c.fundingInput -> c.commitInput(alice.underlyingActor.channelKeys).txOut), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) } val bobCommitments1 = bob.stateData.asInstanceOf[DATA_NORMAL].commitments bobCommitments1.active.foreach { c => - val commitTx = c.fullySignedLocalCommitTx(bobCommitments1.params, bob.underlyingActor.keyManager).tx - Transaction.correctlySpends(commitTx, Map(c.commitInput.outPoint -> c.commitInput.txOut), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + assert(c.commitmentFormat == commitmentFormat) + val commitTx = c.fullySignedLocalCommitTx(bobCommitments1.channelParams, bob.underlyingActor.channelKeys) + Transaction.correctlySpends(commitTx, Map(c.fundingInput -> c.commitInput(bob.underlyingActor.channelKeys).txOut), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) } // alice fulfills that HTLC in both commitments @@ -3254,19 +4163,27 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik crossSign(alice, bob, alice2bob, bob2alice) val aliceCommitments2 = alice.stateData.asInstanceOf[DATA_NORMAL].commitments aliceCommitments2.active.foreach { c => - val commitTx = c.fullySignedLocalCommitTx(aliceCommitments2.params, alice.underlyingActor.keyManager).tx - Transaction.correctlySpends(commitTx, Map(c.commitInput.outPoint -> c.commitInput.txOut), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + val commitTx = c.fullySignedLocalCommitTx(aliceCommitments2.channelParams, alice.underlyingActor.channelKeys) + Transaction.correctlySpends(commitTx, Map(c.fundingInput -> c.commitInput(alice.underlyingActor.channelKeys).txOut), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) } val bobCommitments2 = bob.stateData.asInstanceOf[DATA_NORMAL].commitments bobCommitments2.active.foreach { c => - val commitTx = c.fullySignedLocalCommitTx(bobCommitments2.params, bob.underlyingActor.keyManager).tx - Transaction.correctlySpends(commitTx, Map(c.commitInput.outPoint -> c.commitInput.txOut), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + val commitTx = c.fullySignedLocalCommitTx(bobCommitments2.channelParams, bob.underlyingActor.channelKeys) + Transaction.correctlySpends(commitTx, Map(c.fundingInput -> c.commitInput(bob.underlyingActor.channelKeys).txOut), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) } resolveHtlcs(f, htlcs) } - test("recv CMD_SPLICE (splice-in + splice-out) with pending htlcs, resolved after splice locked", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + test("recv CMD_SPLICE (splice-in + splice-out) with pre and post splice htlcs") { f => + spliceWithPreAndPostHtlcs(f, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) + } + + test("recv CMD_SPLICE (splice-in + splice-out) with pre and post splice htlcs (taproot)", Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => + spliceWithPreAndPostHtlcs(f, ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat) + } + + test("recv CMD_SPLICE (splice-in + splice-out) with pending htlcs, resolved after splice locked") { f => import f._ val htlcs = setupHtlcs(f) @@ -3337,15 +4254,15 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik // Bob is waiting to sign its outgoing HTLC before sending stfu. bob2alice.expectNoMessage(100 millis) bob ! CMD_SIGN() - (0 until 3).foreach { _ => - bob2alice.expectMsgType[CommitSig] - bob2alice.forward(alice) + inside(bob2alice.expectMsgType[CommitSigBatch]) { batch => + assert(batch.batchSize == 3) + bob2alice.forward(alice, batch) } alice2bob.expectMsgType[RevokeAndAck] alice2bob.forward(bob) - (0 until 3).foreach { _ => - alice2bob.expectMsgType[CommitSig] - alice2bob.forward(bob) + inside(alice2bob.expectMsgType[CommitSigBatch]) { batch => + assert(batch.batchSize == 3) + alice2bob.forward(bob, batch) } bob2alice.expectMsgType[RevokeAndAck] bob2alice.forward(alice) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala index 4533175d59..afe94bf6c5 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala @@ -21,27 +21,29 @@ import akka.testkit.TestProbe import com.softwaremill.quicklens.ModifyPimp import fr.acinq.bitcoin.ScriptFlags import fr.acinq.bitcoin.scalacompat.Crypto.PrivateKey -import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, Crypto, OutPoint, SatoshiLong, Script, Transaction} -import fr.acinq.eclair.Features.StaticRemoteKey +import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, Crypto, SatoshiLong, Script, Transaction, TxOut} import fr.acinq.eclair.TestConstants.{Alice, Bob} import fr.acinq.eclair._ import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ import fr.acinq.eclair.blockchain.fee._ import fr.acinq.eclair.blockchain.{CurrentBlockHeight, CurrentFeerates} +import fr.acinq.eclair.channel.ChannelSpendSignature.IndividualSignature import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.fsm.Channel._ -import fr.acinq.eclair.channel.publish.TxPublisher.{PublishFinalTx, PublishReplaceableTx, PublishTx} +import fr.acinq.eclair.channel.publish.TxPublisher.{PublishFinalTx, PublishReplaceableTx} import fr.acinq.eclair.channel.states.ChannelStateTestsBase.PimpTestFSM import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags} -import fr.acinq.eclair.crypto.Sphinx +import fr.acinq.eclair.crypto.{NonceGenerator, Sphinx} import fr.acinq.eclair.io.Peer import fr.acinq.eclair.payment.OutgoingPaymentPacket import fr.acinq.eclair.payment.relay.Relayer._ +import fr.acinq.eclair.reputation.Reputation import fr.acinq.eclair.router.Announcements +import fr.acinq.eclair.testutils.PimpTestProbe.convert import fr.acinq.eclair.transactions.DirectedHtlc.{incoming, outgoing} import fr.acinq.eclair.transactions.Transactions import fr.acinq.eclair.transactions.Transactions._ -import fr.acinq.eclair.wire.protocol.{AnnouncementSignatures, ChannelUpdate, ClosingSigned, CommitSig, Error, FailureMessageCodecs, FailureReason, PermanentChannelFailure, RevokeAndAck, Shutdown, TemporaryNodeFailure, TlvStream, UpdateAddHtlc, UpdateFailHtlc, UpdateFailMalformedHtlc, UpdateFee, UpdateFulfillHtlc, Warning} +import fr.acinq.eclair.wire.protocol.{AnnouncementSignatures, ChannelReestablish, ChannelReestablishTlv, ChannelUpdate, ClosingSigned, CommitSig, CommitSigTlv, Error, FailureMessageCodecs, FailureReason, Init, PermanentChannelFailure, RevokeAndAck, RevokeAndAckTlv, Shutdown, TemporaryNodeFailure, TlvStream, UpdateAddHtlc, UpdateFailHtlc, UpdateFailMalformedHtlc, UpdateFee, UpdateFulfillHtlc, Warning} import org.scalatest.Inside.inside import org.scalatest.funsuite.FixtureAnyFunSuiteLike import org.scalatest.{Outcome, Tag} @@ -77,7 +79,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val listener = TestProbe() alice.underlying.system.eventStream.subscribe(listener.ref, classOf[AvailableBalanceChanged]) val h = randomBytes32() - val add = CMD_ADD_HTLC(sender.ref, 50000000 msat, h, CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, 1.0, None, localOrigin(sender.ref)) + val add = CMD_ADD_HTLC(sender.ref, 50000000 msat, h, CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, Reputation.Score.max, None, localOrigin(sender.ref)) alice ! add sender.expectMsgType[RES_SUCCESS[CMD_ADD_HTLC]] val e = listener.expectMsgType[AvailableBalanceChanged] @@ -103,7 +105,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val sender = TestProbe() val h = randomBytes32() for (i <- 0 until 10) { - alice ! CMD_ADD_HTLC(sender.ref, 500000 msat, h, CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, 1.0, None, localOrigin(sender.ref)) + alice ! CMD_ADD_HTLC(sender.ref, 500000 msat, h, CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, Reputation.Score.max, None, localOrigin(sender.ref)) sender.expectMsgType[RES_SUCCESS[CMD_ADD_HTLC]] val htlc = alice2bob.expectMsgType[UpdateAddHtlc] assert(htlc.id == i && htlc.paymentHash == h) @@ -115,9 +117,9 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] val sender = TestProbe() val h = randomBytes32() - val originHtlc = UpdateAddHtlc(channelId = randomBytes32(), id = 5656, amountMsat = 50000000 msat, cltvExpiry = CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), paymentHash = h, onionRoutingPacket = TestConstants.emptyOnionPacket, pathKey_opt = None, confidence = 1.0, fundingFee_opt = None) - val origin = Origin.Hot(sender.ref, Upstream.Hot.Channel(originHtlc, TimestampMilli.now(), randomKey().publicKey)) - val cmd = CMD_ADD_HTLC(sender.ref, originHtlc.amountMsat - 10_000.msat, h, originHtlc.cltvExpiry - CltvExpiryDelta(7), TestConstants.emptyOnionPacket, None, 1.0, None, origin) + val originHtlc = UpdateAddHtlc(channelId = randomBytes32(), id = 5656, amountMsat = 50000000 msat, cltvExpiry = CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), paymentHash = h, onionRoutingPacket = TestConstants.emptyOnionPacket, pathKey_opt = None, endorsement = Reputation.maxEndorsement, fundingFee_opt = None) + val origin = Origin.Hot(sender.ref, Upstream.Hot.Channel(originHtlc, TimestampMilli.now(), randomKey().publicKey, 0.1)) + val cmd = CMD_ADD_HTLC(sender.ref, originHtlc.amountMsat - 10_000.msat, h, originHtlc.cltvExpiry - CltvExpiryDelta(7), TestConstants.emptyOnionPacket, None, Reputation.Score.max, None, origin) alice ! cmd sender.expectMsgType[RES_SUCCESS[CMD_ADD_HTLC]] val htlc = alice2bob.expectMsgType[UpdateAddHtlc] @@ -133,10 +135,10 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] val sender = TestProbe() val h = randomBytes32() - val originHtlc1 = UpdateAddHtlc(randomBytes32(), 47, 30000000 msat, h, CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, 1.0, None) - val originHtlc2 = UpdateAddHtlc(randomBytes32(), 32, 20000000 msat, h, CltvExpiryDelta(160).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, 1.0, None) - val origin = Origin.Hot(sender.ref, Upstream.Hot.Trampoline(List(originHtlc1, originHtlc2).map(htlc => Upstream.Hot.Channel(htlc, TimestampMilli.now(), randomKey().publicKey)))) - val cmd = CMD_ADD_HTLC(sender.ref, originHtlc1.amountMsat + originHtlc2.amountMsat - 10000.msat, h, originHtlc2.cltvExpiry - CltvExpiryDelta(7), TestConstants.emptyOnionPacket, None, 1.0, None, origin) + val originHtlc1 = UpdateAddHtlc(randomBytes32(), 47, 30000000 msat, h, CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, Reputation.maxEndorsement, None) + val originHtlc2 = UpdateAddHtlc(randomBytes32(), 32, 20000000 msat, h, CltvExpiryDelta(160).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, Reputation.maxEndorsement, None) + val origin = Origin.Hot(sender.ref, Upstream.Hot.Trampoline(List(originHtlc1, originHtlc2).map(htlc => Upstream.Hot.Channel(htlc, TimestampMilli.now(), randomKey().publicKey, 0.1)))) + val cmd = CMD_ADD_HTLC(sender.ref, originHtlc1.amountMsat + originHtlc2.amountMsat - 10000.msat, h, originHtlc2.cltvExpiry - CltvExpiryDelta(7), TestConstants.emptyOnionPacket, None, Reputation.Score.max, None, origin) alice ! cmd sender.expectMsgType[RES_SUCCESS[CMD_ADD_HTLC]] val htlc = alice2bob.expectMsgType[UpdateAddHtlc] @@ -152,11 +154,11 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val sender = TestProbe() val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] val expiryTooSmall = CltvExpiry(currentBlockHeight) - val add = CMD_ADD_HTLC(sender.ref, 500000000 msat, randomBytes32(), expiryTooSmall, TestConstants.emptyOnionPacket, None, 1.0, None, localOrigin(sender.ref)) + val add = CMD_ADD_HTLC(sender.ref, 500000000 msat, randomBytes32(), expiryTooSmall, TestConstants.emptyOnionPacket, None, Reputation.Score.max, None, localOrigin(sender.ref)) alice ! add val error = ExpiryTooSmall(channelId(alice), CltvExpiry(currentBlockHeight + 3), expiryTooSmall, currentBlockHeight) sender.expectMsg(RES_ADD_FAILED(add, error, Some(initialState.channelUpdate))) - alice2bob.expectNoMessage(200 millis) + alice2bob.expectNoMessage(100 millis) } test("recv CMD_ADD_HTLC (expiry too big)") { f => @@ -165,35 +167,35 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] val maxAllowedExpiryDelta = alice.underlyingActor.nodeParams.channelConf.maxExpiryDelta val expiryTooBig = (maxAllowedExpiryDelta + 1).toCltvExpiry(currentBlockHeight) - val add = CMD_ADD_HTLC(sender.ref, 500000000 msat, randomBytes32(), expiryTooBig, TestConstants.emptyOnionPacket, None, 1.0, None, localOrigin(sender.ref)) + val add = CMD_ADD_HTLC(sender.ref, 500000000 msat, randomBytes32(), expiryTooBig, TestConstants.emptyOnionPacket, None, Reputation.Score.max, None, localOrigin(sender.ref)) alice ! add val error = ExpiryTooBig(channelId(alice), maximum = maxAllowedExpiryDelta.toCltvExpiry(currentBlockHeight), actual = expiryTooBig, blockHeight = currentBlockHeight) sender.expectMsg(RES_ADD_FAILED(add, error, Some(initialState.channelUpdate))) - alice2bob.expectNoMessage(200 millis) + alice2bob.expectNoMessage(100 millis) } test("recv CMD_ADD_HTLC (value too small)") { f => import f._ val sender = TestProbe() val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] - val add = CMD_ADD_HTLC(sender.ref, 50 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, 1.0, None, localOrigin(sender.ref)) + val add = CMD_ADD_HTLC(sender.ref, 50 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, Reputation.Score.max, None, localOrigin(sender.ref)) alice ! add val error = HtlcValueTooSmall(channelId(alice), 1000 msat, 50 msat) sender.expectMsg(RES_ADD_FAILED(add, error, Some(initialState.channelUpdate))) - alice2bob.expectNoMessage(200 millis) + alice2bob.expectNoMessage(100 millis) } test("recv CMD_ADD_HTLC (0 msat)") { f => import f._ val sender = TestProbe() // Alice has a minimum set to 0 msat (which should be invalid, but may mislead Bob into relaying 0-value HTLCs which is forbidden by the spec). - assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.params.localParams.htlcMinimum == 0.msat) + assert(alice.commitments.latest.localCommitParams.htlcMinimum == 0.msat) val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] - val add = CMD_ADD_HTLC(sender.ref, 0 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, 1.0, None, localOrigin(sender.ref)) + val add = CMD_ADD_HTLC(sender.ref, 0 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, Reputation.Score.max, None, localOrigin(sender.ref)) bob ! add val error = HtlcValueTooSmall(channelId(bob), 1 msat, 0 msat) sender.expectMsg(RES_ADD_FAILED(add, error, Some(initialState.channelUpdate))) - bob2alice.expectNoMessage(200 millis) + bob2alice.expectNoMessage(100 millis) } test("recv CMD_ADD_HTLC (increasing balance but still below reserve)", Tag(ChannelStateTestsTags.NoPushAmount)) { f => @@ -201,7 +203,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val sender = TestProbe() // channel starts with all funds on alice's side, alice sends some funds to bob, but not enough to make it go above reserve val h = randomBytes32() - val add = CMD_ADD_HTLC(sender.ref, 50000000 msat, h, CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, 1.0, None, localOrigin(sender.ref)) + val add = CMD_ADD_HTLC(sender.ref, 50000000 msat, h, CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, Reputation.Score.max, None, localOrigin(sender.ref)) alice ! add sender.expectMsgType[RES_SUCCESS[CMD_ADD_HTLC]] } @@ -210,50 +212,35 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with import f._ val sender = TestProbe() val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] - val add = CMD_ADD_HTLC(sender.ref, MilliSatoshi(Int.MaxValue), randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, 1.0, None, localOrigin(sender.ref)) - alice ! add - val error = InsufficientFunds(channelId(alice), amount = MilliSatoshi(Int.MaxValue), missing = 1388843 sat, reserve = 20000 sat, fees = 8960 sat) - sender.expectMsg(RES_ADD_FAILED(add, error, Some(initialState.channelUpdate))) - alice2bob.expectNoMessage(200 millis) - } - - test("recv CMD_ADD_HTLC (insufficient funds) (anchor outputs)", Tag(ChannelStateTestsTags.AnchorOutputs)) { f => - import f._ - val sender = TestProbe() - val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] - // The anchor outputs commitment format costs more fees for the funder (bigger commit tx + cost of anchor outputs) - assert(initialState.commitments.availableBalanceForSend < initialState.commitments.modify(_.params.channelFeatures).setTo(ChannelFeatures()).availableBalanceForSend) - val add = CMD_ADD_HTLC(sender.ref, initialState.commitments.availableBalanceForSend + 1.msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, 1.0, None, localOrigin(sender.ref)) + val add = CMD_ADD_HTLC(sender.ref, initialState.commitments.availableBalanceForSend + 1.msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, Reputation.Score.max, None, localOrigin(sender.ref)) alice ! add - val error = InsufficientFunds(channelId(alice), amount = add.amount, missing = 0 sat, reserve = 20000 sat, fees = 3900 sat) sender.expectMsg(RES_ADD_FAILED(add, error, Some(initialState.channelUpdate))) - alice2bob.expectNoMessage(200 millis) + alice2bob.expectNoMessage(100 millis) } test("recv CMD_ADD_HTLC (insufficient funds, missing 1 msat)") { f => import f._ val sender = TestProbe() val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] - val add = CMD_ADD_HTLC(sender.ref, initialState.commitments.availableBalanceForSend + 1.msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, 1.0, None, localOrigin(sender.ref)) + val add = CMD_ADD_HTLC(sender.ref, initialState.commitments.availableBalanceForSend + 1.msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, Reputation.Score.max, None, localOrigin(sender.ref)) bob ! add - val error = InsufficientFunds(channelId(alice), amount = add.amount, missing = 0 sat, reserve = 10000 sat, fees = 0 sat) sender.expectMsg(RES_ADD_FAILED(add, error, Some(initialState.channelUpdate))) - bob2alice.expectNoMessage(200 millis) + bob2alice.expectNoMessage(100 millis) } test("recv CMD_ADD_HTLC (HTLC dips into remote funder fee reserve)", Tag(ChannelStateTestsTags.NoMaxHtlcValueInFlight)) { f => import f._ val sender = TestProbe() - addHtlc(758_640_000 msat, alice, bob, alice2bob, bob2alice) + addHtlc(772_000_000 msat, alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.availableBalanceForSend == 0.msat) // At this point alice has the minimal amount to sustain a channel. // Alice maintains an extra reserve to accommodate for a few more HTLCs, so the first few HTLCs should be allowed. - val htlcs = (1 to 7).map { _ => - bob ! CMD_ADD_HTLC(sender.ref, 12_000_000 msat, randomBytes32(), CltvExpiry(400144), TestConstants.emptyOnionPacket, None, 1.0, None, localOrigin(sender.ref)) + val htlcs = (1 to 9).map { _ => + bob ! CMD_ADD_HTLC(sender.ref, 12_000_000 msat, randomBytes32(), CltvExpiry(400144), TestConstants.emptyOnionPacket, None, Reputation.Score.max, None, localOrigin(sender.ref)) sender.expectMsgType[RES_SUCCESS[CMD_ADD_HTLC]] val add = bob2alice.expectMsgType[UpdateAddHtlc] bob2alice.forward(alice, add) @@ -261,9 +248,9 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with } // But this one will dip alice below her reserve: we must wait for the previous HTLCs to settle before sending any more. - val failedAdd = CMD_ADD_HTLC(sender.ref, 11_000_000 msat, randomBytes32(), CltvExpiry(400144), TestConstants.emptyOnionPacket, None, 1.0, None, localOrigin(sender.ref)) + val failedAdd = CMD_ADD_HTLC(sender.ref, 11_000_000 msat, randomBytes32(), CltvExpiry(400144), TestConstants.emptyOnionPacket, None, Reputation.Score.max, None, localOrigin(sender.ref)) bob ! failedAdd - val error = RemoteCannotAffordFeesForNewHtlc(channelId(bob), failedAdd.amount, missing = 1360 sat, 20_000 sat, 22_720 sat) + val error = RemoteCannotAffordFeesForNewHtlc(channelId(bob), failedAdd.amount, missing = 200 sat, 20_000 sat, 8_200 sat) sender.expectMsg(RES_ADD_FAILED(failedAdd, error, Some(bob.stateData.asInstanceOf[DATA_NORMAL].channelUpdate))) // If Bob had sent this HTLC, Alice would have accepted dipping into her reserve. @@ -276,29 +263,34 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with test("recv CMD_ADD_HTLC (HTLC dips into remote funder channel reserve)", Tag(ChannelStateTestsTags.NoMaxHtlcValueInFlight)) { f => import f._ val sender = TestProbe() - addHtlc(758_640_000 msat, alice, bob, alice2bob, bob2alice) + addHtlc(772_000_000 msat, alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.availableBalanceForSend == 0.msat) // We increase the feerate to get Alice's balance closer to her channel reserve. - bob.underlyingActor.nodeParams.setBitcoinCoreFeerates(FeeratesPerKw.single(FeeratePerKw(17_500 sat))) - updateFee(FeeratePerKw(17_500 sat), alice, bob, alice2bob, bob2alice) + alice.underlyingActor.nodeParams.setBitcoinCoreFeerates(FeeratesPerKw.single(FeeratePerKw(3_500 sat))) + bob.underlyingActor.nodeParams.setBitcoinCoreFeerates(FeeratesPerKw.single(FeeratePerKw(3_500 sat))) + updateFee(FeeratePerKw(3_500 sat), alice, bob, alice2bob, bob2alice) // At this point alice has the minimal amount to sustain a channel. - // Alice maintains an extra reserve to accommodate for a one more HTLCs, so the first few HTLCs should be allowed. - bob ! CMD_ADD_HTLC(sender.ref, 25_000_000 msat, randomBytes32(), CltvExpiry(400144), TestConstants.emptyOnionPacket, None, 1.0, None, localOrigin(sender.ref)) - sender.expectMsgType[RES_SUCCESS[CMD_ADD_HTLC]] - val add = bob2alice.expectMsgType[UpdateAddHtlc] - bob2alice.forward(alice, add) + // Alice maintains an extra reserve to accommodate for a few more HTLCs, so the first few HTLCs should be allowed. + val htlcs = (1 to 4).map { _ => + bob ! CMD_ADD_HTLC(sender.ref, 25_000_000 msat, randomBytes32(), CltvExpiry(400144), TestConstants.emptyOnionPacket, None, Reputation.Score.max, None, localOrigin(sender.ref)) + sender.expectMsgType[RES_SUCCESS[CMD_ADD_HTLC]] + val add = bob2alice.expectMsgType[UpdateAddHtlc] + bob2alice.forward(alice, add) + add + } // But this one will dip alice below her reserve: we must wait for the previous HTLCs to settle before sending any more. - val failedAdd = CMD_ADD_HTLC(sender.ref, 25_000_000 msat, randomBytes32(), CltvExpiry(400144), TestConstants.emptyOnionPacket, None, 1.0, None, localOrigin(sender.ref)) + val failedAdd = CMD_ADD_HTLC(sender.ref, 25_000_000 msat, randomBytes32(), CltvExpiry(400144), TestConstants.emptyOnionPacket, None, Reputation.Score.max, None, localOrigin(sender.ref)) bob ! failedAdd - val error = RemoteCannotAffordFeesForNewHtlc(channelId(bob), failedAdd.amount, missing = 340 sat, 20_000 sat, 21_700 sat) + val error = RemoteCannotAffordFeesForNewHtlc(channelId(bob), failedAdd.amount, missing = 206 sat, 20_000 sat, 8_206 sat) sender.expectMsg(RES_ADD_FAILED(failedAdd, error, Some(bob.stateData.asInstanceOf[DATA_NORMAL].channelUpdate))) // If Bob had sent this HTLC, Alice would have accepted dipping into her reserve. + val add = htlcs.last.copy(id = htlcs.last.id + 1) val proposedChanges = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.changes.remoteChanges.proposed.size - alice ! add.copy(id = add.id + 1) + alice ! add awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.changes.remoteChanges.proposed.size == proposedChanges + 1) } @@ -306,116 +298,116 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with import f._ val sender = TestProbe() val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] - alice ! CMD_ADD_HTLC(sender.ref, 500000000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, 1.0, None, localOrigin(sender.ref)) + alice ! CMD_ADD_HTLC(sender.ref, 500_000_000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, Reputation.Score.max, None, localOrigin(sender.ref)) sender.expectMsgType[RES_SUCCESS[CMD_ADD_HTLC]] alice2bob.expectMsgType[UpdateAddHtlc] - alice ! CMD_ADD_HTLC(sender.ref, 200000000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, 1.0, None, localOrigin(sender.ref)) + alice ! CMD_ADD_HTLC(sender.ref, 200_000_000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, Reputation.Score.max, None, localOrigin(sender.ref)) sender.expectMsgType[RES_SUCCESS[CMD_ADD_HTLC]] alice2bob.expectMsgType[UpdateAddHtlc] - alice ! CMD_ADD_HTLC(sender.ref, 51760000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, 1.0, None, localOrigin(sender.ref)) + alice ! CMD_ADD_HTLC(sender.ref, 70_000_000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, Reputation.Score.max, None, localOrigin(sender.ref)) sender.expectMsgType[RES_SUCCESS[CMD_ADD_HTLC]] alice2bob.expectMsgType[UpdateAddHtlc] - val add = CMD_ADD_HTLC(sender.ref, 1000000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, 1.0, None, localOrigin(sender.ref)) + val add = CMD_ADD_HTLC(sender.ref, 1_000_000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, Reputation.Score.max, None, localOrigin(sender.ref)) alice ! add - val error = InsufficientFunds(channelId(alice), amount = 1000000 msat, missing = 1000 sat, reserve = 20000 sat, fees = 12400 sat) + val error = InsufficientFunds(channelId(alice), amount = 1_000_000 msat, missing = 1580 sat, reserve = 20_000 sat, fees = 5_190 sat) sender.expectMsg(RES_ADD_FAILED(add, error, Some(initialState.channelUpdate))) - alice2bob.expectNoMessage(200 millis) + alice2bob.expectNoMessage(100 millis) } test("recv CMD_ADD_HTLC (insufficient funds w/ pending htlcs 2/2)", Tag(ChannelStateTestsTags.NoMaxHtlcValueInFlight)) { f => import f._ val sender = TestProbe() val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] - alice ! CMD_ADD_HTLC(sender.ref, 300000000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, 1.0, None, localOrigin(sender.ref)) + alice ! CMD_ADD_HTLC(sender.ref, 300_000_000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, Reputation.Score.max, None, localOrigin(sender.ref)) sender.expectMsgType[RES_SUCCESS[CMD_ADD_HTLC]] alice2bob.expectMsgType[UpdateAddHtlc] - alice ! CMD_ADD_HTLC(sender.ref, 300000000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, 1.0, None, localOrigin(sender.ref)) + alice ! CMD_ADD_HTLC(sender.ref, 300_000_000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, Reputation.Score.max, None, localOrigin(sender.ref)) sender.expectMsgType[RES_SUCCESS[CMD_ADD_HTLC]] alice2bob.expectMsgType[UpdateAddHtlc] - val add = CMD_ADD_HTLC(sender.ref, 500000000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, 1.0, None, localOrigin(sender.ref)) + val add = CMD_ADD_HTLC(sender.ref, 500_000_000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, Reputation.Score.max, None, localOrigin(sender.ref)) alice ! add - val error = InsufficientFunds(channelId(alice), amount = 500000000 msat, missing = 348240 sat, reserve = 20000 sat, fees = 12400 sat) + val error = InsufficientFunds(channelId(alice), amount = 500_000_000 msat, missing = 329_720 sat, reserve = 20_000 sat, fees = 4_760 sat) sender.expectMsg(RES_ADD_FAILED(add, error, Some(initialState.channelUpdate))) - alice2bob.expectNoMessage(200 millis) + alice2bob.expectNoMessage(100 millis) } test("recv CMD_ADD_HTLC (over remote max inflight htlc value)", Tag(ChannelStateTestsTags.AliceLowMaxHtlcValueInFlight)) { f => import f._ val sender = TestProbe() val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] - assert(initialState.commitments.params.localParams.maxHtlcValueInFlightMsat == initialState.commitments.latest.capacity.toMilliSatoshi) - assert(initialState.commitments.params.remoteParams.maxHtlcValueInFlightMsat == UInt64(150000000)) - val add = CMD_ADD_HTLC(sender.ref, 151000000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, 1.0, None, localOrigin(sender.ref)) + assert(initialState.commitments.latest.localCommitParams.maxHtlcValueInFlight == UInt64(initialState.commitments.latest.capacity.toMilliSatoshi.toLong)) + assert(initialState.commitments.latest.remoteCommitParams.maxHtlcValueInFlight == UInt64(150_000_000)) + val add = CMD_ADD_HTLC(sender.ref, 151_000_000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, Reputation.Score.max, None, localOrigin(sender.ref)) bob ! add - val error = HtlcValueTooHighInFlight(channelId(bob), maximum = 150000000 msat, actual = 151000000 msat) + val error = HtlcValueTooHighInFlight(channelId(bob), maximum = UInt64(150_000_000), actual = 151_000_000 msat) sender.expectMsg(RES_ADD_FAILED(add, error, Some(initialState.channelUpdate))) - bob2alice.expectNoMessage(200 millis) + bob2alice.expectNoMessage(100 millis) } test("recv CMD_ADD_HTLC (over remote max inflight htlc value with duplicate amounts)", Tag(ChannelStateTestsTags.AliceLowMaxHtlcValueInFlight)) { f => import f._ val sender = TestProbe() val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] - assert(initialState.commitments.params.localParams.maxHtlcValueInFlightMsat == initialState.commitments.latest.capacity.toMilliSatoshi) - assert(initialState.commitments.params.remoteParams.maxHtlcValueInFlightMsat == UInt64(150000000)) - val add = CMD_ADD_HTLC(sender.ref, 75500000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, 1.0, None, localOrigin(sender.ref)) + assert(initialState.commitments.latest.localCommitParams.maxHtlcValueInFlight == UInt64(initialState.commitments.latest.capacity.toMilliSatoshi.toLong)) + assert(initialState.commitments.latest.remoteCommitParams.maxHtlcValueInFlight == UInt64(150_000_000)) + val add = CMD_ADD_HTLC(sender.ref, 75_500_000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, Reputation.Score.max, None, localOrigin(sender.ref)) bob ! add sender.expectMsgType[RES_SUCCESS[CMD_ADD_HTLC]] bob2alice.expectMsgType[UpdateAddHtlc] - val add1 = CMD_ADD_HTLC(sender.ref, 75500000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, 1.0, None, localOrigin(sender.ref)) + val add1 = CMD_ADD_HTLC(sender.ref, 75_500_000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, Reputation.Score.max, None, localOrigin(sender.ref)) bob ! add1 - val error = HtlcValueTooHighInFlight(channelId(bob), maximum = 150000000 msat, actual = 151000000 msat) + val error = HtlcValueTooHighInFlight(channelId(bob), maximum = UInt64(150_000_000), actual = 151_000_000 msat) sender.expectMsg(RES_ADD_FAILED(add1, error, Some(initialState.channelUpdate))) - bob2alice.expectNoMessage(200 millis) + bob2alice.expectNoMessage(100 millis) } test("recv CMD_ADD_HTLC (over local max inflight htlc value)", Tag(ChannelStateTestsTags.AliceLowMaxHtlcValueInFlight)) { f => import f._ val sender = TestProbe() val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] - assert(initialState.commitments.params.localParams.maxHtlcValueInFlightMsat == 150000000.msat) - assert(initialState.commitments.params.remoteParams.maxHtlcValueInFlightMsat == UInt64(initialState.commitments.latest.capacity.toMilliSatoshi.toLong)) - val add = CMD_ADD_HTLC(sender.ref, 151000000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, 1.0, None, localOrigin(sender.ref)) + assert(initialState.commitments.latest.localCommitParams.maxHtlcValueInFlight == UInt64(150_000_000)) + assert(initialState.commitments.latest.remoteCommitParams.maxHtlcValueInFlight == UInt64(initialState.commitments.latest.capacity.toMilliSatoshi.toLong)) + val add = CMD_ADD_HTLC(sender.ref, 151_000_000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, Reputation.Score.max, None, localOrigin(sender.ref)) alice ! add - val error = HtlcValueTooHighInFlight(channelId(alice), maximum = 150000000 msat, actual = 151000000 msat) + val error = HtlcValueTooHighInFlight(channelId(alice), maximum = UInt64(150_000_000), actual = 151_000_000 msat) sender.expectMsg(RES_ADD_FAILED(add, error, Some(initialState.channelUpdate))) - alice2bob.expectNoMessage(200 millis) + alice2bob.expectNoMessage(100 millis) } test("recv CMD_ADD_HTLC (over remote max accepted htlcs)") { f => import f._ val sender = TestProbe() val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] - assert(initialState.commitments.params.localParams.maxAcceptedHtlcs == 100) - assert(initialState.commitments.params.remoteParams.maxAcceptedHtlcs == 30) // Bob accepts a maximum of 30 htlcs + assert(initialState.commitments.latest.localCommitParams.maxAcceptedHtlcs == 100) + assert(initialState.commitments.latest.remoteCommitParams.maxAcceptedHtlcs == 30) // Bob accepts a maximum of 30 htlcs for (_ <- 0 until 30) { - alice ! CMD_ADD_HTLC(sender.ref, 10000000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, 1.0, None, localOrigin(sender.ref)) + alice ! CMD_ADD_HTLC(sender.ref, 10_000_000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, Reputation.Score.max, None, localOrigin(sender.ref)) sender.expectMsgType[RES_SUCCESS[CMD_ADD_HTLC]] alice2bob.expectMsgType[UpdateAddHtlc] } - val add = CMD_ADD_HTLC(sender.ref, 10000000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, 1.0, None, localOrigin(sender.ref)) + val add = CMD_ADD_HTLC(sender.ref, 10_000_000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, Reputation.Score.max, None, localOrigin(sender.ref)) alice ! add val error = TooManyAcceptedHtlcs(channelId(alice), maximum = 30) sender.expectMsg(RES_ADD_FAILED(add, error, Some(initialState.channelUpdate))) - alice2bob.expectNoMessage(200 millis) + alice2bob.expectNoMessage(100 millis) } test("recv CMD_ADD_HTLC (over local max accepted htlcs)") { f => import f._ val sender = TestProbe() val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] - assert(initialState.commitments.params.localParams.maxAcceptedHtlcs == 30) // Bob accepts a maximum of 30 htlcs - assert(initialState.commitments.params.remoteParams.maxAcceptedHtlcs == 100) // Alice accepts more, but Bob will stop at 30 HTLCs + assert(initialState.commitments.latest.localCommitParams.maxAcceptedHtlcs == 30) // Bob accepts a maximum of 30 htlcs + assert(initialState.commitments.latest.remoteCommitParams.maxAcceptedHtlcs == 100) // Alice accepts more, but Bob will stop at 30 HTLCs for (_ <- 0 until 30) { - bob ! CMD_ADD_HTLC(sender.ref, 500000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, 1.0, None, localOrigin(sender.ref)) + bob ! CMD_ADD_HTLC(sender.ref, 500_000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, Reputation.Score.max, None, localOrigin(sender.ref)) sender.expectMsgType[RES_SUCCESS[CMD_ADD_HTLC]] bob2alice.expectMsgType[UpdateAddHtlc] } - val add = CMD_ADD_HTLC(sender.ref, 500000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, 1.0, None, localOrigin(sender.ref)) + val add = CMD_ADD_HTLC(sender.ref, 500_000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, Reputation.Score.max, None, localOrigin(sender.ref)) bob ! add val error = TooManyAcceptedHtlcs(channelId(bob), maximum = 30) sender.expectMsg(RES_ADD_FAILED(add, error, Some(initialState.channelUpdate))) - bob2alice.expectNoMessage(200 millis) + bob2alice.expectNoMessage(100 millis) } test("recv CMD_ADD_HTLC (over max dust htlc exposure)") { f => @@ -424,38 +416,28 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] val aliceCommitments = initialState.commitments assert(alice.underlyingActor.nodeParams.onChainFeeConf.feerateToleranceFor(bob.underlyingActor.nodeParams.nodeId).dustTolerance.maxExposure == 25_000.sat) - assert(Transactions.offeredHtlcTrimThreshold(aliceCommitments.params.localParams.dustLimit, aliceCommitments.latest.localCommit.spec, aliceCommitments.params.commitmentFormat) == 7730.sat) - assert(Transactions.receivedHtlcTrimThreshold(aliceCommitments.params.localParams.dustLimit, aliceCommitments.latest.localCommit.spec, aliceCommitments.params.commitmentFormat) == 8130.sat) - assert(Transactions.offeredHtlcTrimThreshold(aliceCommitments.params.remoteParams.dustLimit, aliceCommitments.latest.localCommit.spec, aliceCommitments.params.commitmentFormat) == 7630.sat) - assert(Transactions.receivedHtlcTrimThreshold(aliceCommitments.params.remoteParams.dustLimit, aliceCommitments.latest.localCommit.spec, aliceCommitments.params.commitmentFormat) == 8030.sat) + assert(Transactions.offeredHtlcTrimThreshold(aliceCommitments.latest.localCommitParams.dustLimit, aliceCommitments.latest.localCommit.spec, aliceCommitments.latest.commitmentFormat) == 1100.sat) + assert(Transactions.receivedHtlcTrimThreshold(aliceCommitments.latest.localCommitParams.dustLimit, aliceCommitments.latest.localCommit.spec, aliceCommitments.latest.commitmentFormat) == 1100.sat) + assert(Transactions.offeredHtlcTrimThreshold(aliceCommitments.latest.remoteCommitParams.dustLimit, aliceCommitments.latest.localCommit.spec, aliceCommitments.latest.commitmentFormat) == 1000.sat) + assert(Transactions.receivedHtlcTrimThreshold(aliceCommitments.latest.remoteCommitParams.dustLimit, aliceCommitments.latest.localCommit.spec, aliceCommitments.latest.commitmentFormat) == 1000.sat) // Alice sends HTLCs to Bob that add 10 000 sat to the dust exposure: - addHtlc(500.sat.toMilliSatoshi, alice, bob, alice2bob, bob2alice) // dust htlc - addHtlc(1250.sat.toMilliSatoshi, alice, bob, alice2bob, bob2alice) // trimmed htlc - addHtlc(8250.sat.toMilliSatoshi, alice, bob, alice2bob, bob2alice) // slightly above the trimmed threshold -> included in the dust exposure - addHtlc(15000.sat.toMilliSatoshi, alice, bob, alice2bob, bob2alice) // way above the trimmed threshold -> not included in the dust exposure + (1 to 20).foreach { _ => addHtlc(500.sat.toMilliSatoshi, alice, bob, alice2bob, bob2alice) } + addHtlc(15_000.sat.toMilliSatoshi, alice, bob, alice2bob, bob2alice) // way above the trimmed threshold -> not included in the dust exposure crossSign(alice, bob, alice2bob, bob2alice) // Bob sends HTLCs to Alice that add 14 500 sat to the dust exposure: - addHtlc(300.sat.toMilliSatoshi, bob, alice, bob2alice, alice2bob) // dust htlc - addHtlc(6000.sat.toMilliSatoshi, bob, alice, bob2alice, alice2bob) // trimmed htlc - addHtlc(8200.sat.toMilliSatoshi, bob, alice, bob2alice, alice2bob) // slightly above the trimmed threshold -> included in the dust exposure - addHtlc(18000.sat.toMilliSatoshi, bob, alice, bob2alice, alice2bob) // way above the trimmed threshold -> not included in the dust exposure + (1 to 29).foreach { _ => addHtlc(500.sat.toMilliSatoshi, bob, alice, bob2alice, alice2bob) } + addHtlc(18_000.sat.toMilliSatoshi, bob, alice, bob2alice, alice2bob) // way above the trimmed threshold -> not included in the dust exposure crossSign(bob, alice, bob2alice, alice2bob) // HTLCs that take Alice's dust exposure above her threshold are rejected. - val dustAdd = CMD_ADD_HTLC(sender.ref, 501.sat.toMilliSatoshi, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, 1.0, None, localOrigin(sender.ref)) + val dustAdd = CMD_ADD_HTLC(sender.ref, 501.sat.toMilliSatoshi, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, Reputation.Score.max, None, localOrigin(sender.ref)) alice ! dustAdd - sender.expectMsg(RES_ADD_FAILED(dustAdd, LocalDustHtlcExposureTooHigh(channelId(alice), 25000.sat, 25001.sat.toMilliSatoshi), Some(initialState.channelUpdate))) - val trimmedAdd = CMD_ADD_HTLC(sender.ref, 5000.sat.toMilliSatoshi, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, 1.0, None, localOrigin(sender.ref)) - alice ! trimmedAdd - sender.expectMsg(RES_ADD_FAILED(trimmedAdd, LocalDustHtlcExposureTooHigh(channelId(alice), 25000.sat, 29500.sat.toMilliSatoshi), Some(initialState.channelUpdate))) - val justAboveTrimmedAdd = CMD_ADD_HTLC(sender.ref, 8500.sat.toMilliSatoshi, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, 1.0, None, localOrigin(sender.ref)) - alice ! justAboveTrimmedAdd - sender.expectMsg(RES_ADD_FAILED(justAboveTrimmedAdd, LocalDustHtlcExposureTooHigh(channelId(alice), 25000.sat, 33000.sat.toMilliSatoshi), Some(initialState.channelUpdate))) + sender.expectMsg(RES_ADD_FAILED(dustAdd, LocalDustHtlcExposureTooHigh(channelId(alice), 25_000.sat, 25_001.sat.toMilliSatoshi), Some(initialState.channelUpdate))) // HTLCs that don't contribute to dust exposure are accepted. - alice ! CMD_ADD_HTLC(sender.ref, 25000.sat.toMilliSatoshi, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, 1.0, None, localOrigin(sender.ref)) + alice ! CMD_ADD_HTLC(sender.ref, 25_000.sat.toMilliSatoshi, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, Reputation.Score.max, None, localOrigin(sender.ref)) sender.expectMsgType[RES_SUCCESS[CMD_ADD_HTLC]] alice2bob.expectMsgType[UpdateAddHtlc] } @@ -466,27 +448,24 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] assert(alice.underlyingActor.nodeParams.onChainFeeConf.feerateToleranceFor(bob.underlyingActor.nodeParams.nodeId).dustTolerance.maxExposure == 25_000.sat) - // Alice sends HTLCs to Bob that add 20 000 sat to the dust exposure. + // Alice sends HTLCs to Bob that add 22 500 sat to the dust exposure. // She signs them but Bob doesn't answer yet. - addHtlc(4000.sat.toMilliSatoshi, alice, bob, alice2bob, bob2alice) - addHtlc(3000.sat.toMilliSatoshi, alice, bob, alice2bob, bob2alice) - addHtlc(7000.sat.toMilliSatoshi, alice, bob, alice2bob, bob2alice) - addHtlc(6000.sat.toMilliSatoshi, alice, bob, alice2bob, bob2alice) + (1 to 25).foreach { _ => addHtlc(900.sat.toMilliSatoshi, alice, bob, alice2bob, bob2alice) } alice ! CMD_SIGN(Some(sender.ref)) sender.expectMsgType[RES_SUCCESS[CMD_SIGN]] alice2bob.expectMsgType[CommitSig] - // Alice sends HTLCs to Bob that add 4 000 sat to the dust exposure. - addHtlc(2500.sat.toMilliSatoshi, alice, bob, alice2bob, bob2alice) - addHtlc(1500.sat.toMilliSatoshi, alice, bob, alice2bob, bob2alice) + // Alice sends HTLCs to Bob that add 1 800 sat to the dust exposure. + addHtlc(900.sat.toMilliSatoshi, alice, bob, alice2bob, bob2alice) + addHtlc(900.sat.toMilliSatoshi, alice, bob, alice2bob, bob2alice) // HTLCs that take Alice's dust exposure above her threshold are rejected. - val add = CMD_ADD_HTLC(sender.ref, 1001.sat.toMilliSatoshi, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, 1.0, None, localOrigin(sender.ref)) + val add = CMD_ADD_HTLC(sender.ref, 701.sat.toMilliSatoshi, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, Reputation.Score.max, None, localOrigin(sender.ref)) alice ! add sender.expectMsg(RES_ADD_FAILED(add, LocalDustHtlcExposureTooHigh(channelId(alice), 25000.sat, 25001.sat.toMilliSatoshi), Some(initialState.channelUpdate))) } - test("recv CMD_ADD_HTLC (over max dust htlc exposure in local commit only with pending local changes)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + test("recv CMD_ADD_HTLC (over max dust htlc exposure in local commit only with pending local changes)") { f => import f._ val sender = TestProbe() val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] @@ -505,12 +484,12 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with (1 to 3).foreach(_ => addHtlc(1050.sat.toMilliSatoshi, alice, bob, alice2bob, bob2alice)) // HTLCs that take Alice's dust exposure above her threshold are rejected. - val add = CMD_ADD_HTLC(sender.ref, 1050.sat.toMilliSatoshi, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, 1.0, None, localOrigin(sender.ref)) + val add = CMD_ADD_HTLC(sender.ref, 1050.sat.toMilliSatoshi, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, Reputation.Score.max, None, localOrigin(sender.ref)) alice ! add sender.expectMsg(RES_ADD_FAILED(add, LocalDustHtlcExposureTooHigh(channelId(alice), 25000.sat, 25200.sat.toMilliSatoshi), Some(initialState.channelUpdate))) } - test("recv CMD_ADD_HTLC (over max dust htlc exposure in remote commit only with pending local changes)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + test("recv CMD_ADD_HTLC (over max dust htlc exposure in remote commit only with pending local changes)") { f => import f._ val sender = TestProbe() val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] @@ -529,7 +508,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with (1 to 8).foreach(_ => addHtlc(1050.sat.toMilliSatoshi, bob, alice, bob2alice, alice2bob)) // HTLCs that take Bob's dust exposure above his threshold are rejected. - val add = CMD_ADD_HTLC(sender.ref, 1050.sat.toMilliSatoshi, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, 1.0, None, localOrigin(sender.ref)) + val add = CMD_ADD_HTLC(sender.ref, 1050.sat.toMilliSatoshi, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, Reputation.Score.max, None, localOrigin(sender.ref)) bob ! add sender.expectMsg(RES_ADD_FAILED(add, RemoteDustHtlcExposureTooHigh(channelId(bob), 30000.sat, 30450.sat.toMilliSatoshi), Some(initialState.channelUpdate))) } @@ -538,43 +517,18 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with import f._ val sender = TestProbe() val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] - val add1 = CMD_ADD_HTLC(sender.ref, TestConstants.fundingSatoshis.toMilliSatoshi * 2 / 3, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, 1.0, None, localOrigin(sender.ref)) + val add1 = CMD_ADD_HTLC(sender.ref, TestConstants.fundingSatoshis.toMilliSatoshi * 2 / 3, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, Reputation.Score.max, None, localOrigin(sender.ref)) alice ! add1 sender.expectMsgType[RES_SUCCESS[CMD_ADD_HTLC]] alice2bob.expectMsgType[UpdateAddHtlc] alice ! CMD_SIGN() alice2bob.expectMsgType[CommitSig] // this is over channel-capacity - val add2 = CMD_ADD_HTLC(sender.ref, TestConstants.fundingSatoshis.toMilliSatoshi * 2 / 3, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, 1.0, None, localOrigin(sender.ref)) + val add2 = CMD_ADD_HTLC(sender.ref, TestConstants.fundingSatoshis.toMilliSatoshi * 2 / 3, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, Reputation.Score.max, None, localOrigin(sender.ref)) alice ! add2 - val error = InsufficientFunds(channelId(alice), add2.amount, 578133 sat, 20000 sat, 10680 sat) + val error = InsufficientFunds(channelId(alice), add2.amount, 562193 sat, 20000 sat, 4330 sat) sender.expectMsg(RES_ADD_FAILED(add2, error, Some(initialState.channelUpdate))) - alice2bob.expectNoMessage(200 millis) - } - - test("recv CMD_ADD_HTLC (channel feerate mismatch)") { f => - import f._ - - val sender = TestProbe() - bob.setBitcoinCoreFeerate(FeeratePerKw(20000 sat)) - bob ! CurrentFeerates.BitcoinCore(FeeratesPerKw.single(FeeratePerKw(20000 sat))) - bob2alice.expectNoMessage(100 millis) // we don't close because the commitment doesn't contain any HTLC - - val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] - val upstream = localOrigin(sender.ref) - val add = CMD_ADD_HTLC(sender.ref, 500000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, 1.0, None, upstream) - bob ! add - val error = FeerateTooDifferent(channelId(bob), FeeratePerKw(20000 sat), FeeratePerKw(10000 sat)) - sender.expectMsg(RES_ADD_FAILED(add, error, Some(initialState.channelUpdate))) - bob2alice.expectNoMessage(100 millis) // we don't close the channel, we can simply avoid using it while we disagree on feerate - - // we now agree on feerate so we can send HTLCs - bob.setBitcoinCoreFeerate(FeeratePerKw(11000 sat)) - bob ! CurrentFeerates.BitcoinCore(FeeratesPerKw.single(FeeratePerKw(11000 sat))) - bob2alice.expectNoMessage(100 millis) - bob ! add - sender.expectMsgType[RES_SUCCESS[CMD_ADD_HTLC]] - bob2alice.expectMsgType[UpdateAddHtlc] + alice2bob.expectNoMessage(100 millis) } test("recv CMD_ADD_HTLC (after having sent Shutdown)") { f => @@ -587,11 +541,11 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].localShutdown.isDefined && alice.stateData.asInstanceOf[DATA_NORMAL].remoteShutdown.isEmpty) // actual test starts here - val add = CMD_ADD_HTLC(sender.ref, 500000000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, 1.0, None, localOrigin(sender.ref)) + val add = CMD_ADD_HTLC(sender.ref, 500000000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, Reputation.Score.max, None, localOrigin(sender.ref)) alice ! add val error = NoMoreHtlcsClosingInProgress(channelId(alice)) sender.expectMsg(RES_ADD_FAILED(add, error, Some(initialState.channelUpdate))) - alice2bob.expectNoMessage(200 millis) + alice2bob.expectNoMessage(100 millis) } test("recv CMD_ADD_HTLC (after having received Shutdown)") { f => @@ -599,14 +553,14 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val sender = TestProbe() val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] // let's make alice send an htlc - val add1 = CMD_ADD_HTLC(sender.ref, 50000000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, 1.0, None, localOrigin(sender.ref)) + val add1 = CMD_ADD_HTLC(sender.ref, 50000000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, Reputation.Score.max, None, localOrigin(sender.ref)) alice ! add1 sender.expectMsgType[RES_SUCCESS[CMD_ADD_HTLC]] // at the same time bob initiates a closing bob ! CMD_CLOSE(sender.ref, None, None) sender.expectMsgType[RES_SUCCESS[CMD_CLOSE]] // this command will be received by alice right after having received the shutdown - val add2 = CMD_ADD_HTLC(sender.ref, 10000000 msat, randomBytes32(), CltvExpiry(300000), TestConstants.emptyOnionPacket, None, 1.0, None, localOrigin(sender.ref)) + val add2 = CMD_ADD_HTLC(sender.ref, 10000000 msat, randomBytes32(), CltvExpiry(300000), TestConstants.emptyOnionPacket, None, Reputation.Score.max, None, localOrigin(sender.ref)) // messages cross alice2bob.expectMsgType[UpdateAddHtlc] alice2bob.forward(bob) @@ -620,7 +574,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with test("recv UpdateAddHtlc") { f => import f._ val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] - val htlc = UpdateAddHtlc(ByteVector32.Zeroes, 0, 150000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, 1.0, None) + val htlc = UpdateAddHtlc(ByteVector32.Zeroes, 0, 150000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, Reputation.maxEndorsement, None) bob ! htlc awaitCond(bob.stateData == initialState .modify(_.commitments.changes.remoteChanges.proposed).using(_ :+ htlc) @@ -631,8 +585,8 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with test("recv UpdateAddHtlc (unexpected id)") { f => import f._ - val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx - val htlc = UpdateAddHtlc(ByteVector32.Zeroes, 42, 150000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, 1.0, None) + val tx = bob.signCommitTx() + val htlc = UpdateAddHtlc(ByteVector32.Zeroes, 42, 150000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, Reputation.maxEndorsement, None) bob ! htlc.copy(id = 0) bob ! htlc.copy(id = 1) bob ! htlc.copy(id = 2) @@ -641,118 +595,125 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val error = bob2alice.expectMsgType[Error] assert(new String(error.data.toArray) == UnexpectedHtlcId(channelId(bob), expected = 4, actual = 42).getMessage) awaitCond(bob.stateName == CLOSING) - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) - bob2blockchain.expectMsgType[PublishTx] - bob2blockchain.expectMsgType[WatchTxConfirmed] + bob2blockchain.expectFinalTxPublished(tx.txid) + bob2blockchain.expectReplaceableTxPublished[ClaimLocalAnchorTx] + bob2blockchain.expectFinalTxPublished("local-main-delayed") + bob2blockchain.expectWatchTxConfirmed(tx.txid) } test("recv UpdateAddHtlc (value too small)") { f => import f._ - val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx - val htlc = UpdateAddHtlc(ByteVector32.Zeroes, 0, 150 msat, randomBytes32(), cltvExpiry = CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, 1.0, None) + val tx = bob.signCommitTx() + val htlc = UpdateAddHtlc(ByteVector32.Zeroes, 0, 150 msat, randomBytes32(), cltvExpiry = CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, Reputation.maxEndorsement, None) alice2bob.forward(bob, htlc) val error = bob2alice.expectMsgType[Error] assert(new String(error.data.toArray) == HtlcValueTooSmall(channelId(bob), minimum = 1000 msat, actual = 150 msat).getMessage) awaitCond(bob.stateName == CLOSING) // channel should be advertised as down assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId == bob.stateData.asInstanceOf[DATA_CLOSING].channelId) - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) - bob2blockchain.expectMsgType[PublishTx] - bob2blockchain.expectMsgType[WatchTxConfirmed] + bob2blockchain.expectFinalTxPublished(tx.txid) + bob2blockchain.expectReplaceableTxPublished[ClaimLocalAnchorTx] + bob2blockchain.expectFinalTxPublished("local-main-delayed") + bob2blockchain.expectWatchTxConfirmed(tx.txid) } test("recv UpdateAddHtlc (insufficient funds)") { f => import f._ - val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx - val htlc = UpdateAddHtlc(ByteVector32.Zeroes, 0, MilliSatoshi(Long.MaxValue), randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, 1.0, None) + val tx = bob.signCommitTx() + val htlc = UpdateAddHtlc(ByteVector32.Zeroes, 0, MilliSatoshi(Long.MaxValue), randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, Reputation.maxEndorsement, None) alice2bob.forward(bob, htlc) val error = bob2alice.expectMsgType[Error] - assert(new String(error.data.toArray) == InsufficientFunds(channelId(bob), amount = MilliSatoshi(Long.MaxValue), missing = 9223372036083735L sat, reserve = 20000 sat, fees = 8960 sat).getMessage) + assert(new String(error.data.toArray) == InsufficientFunds(channelId(bob), amount = MilliSatoshi(Long.MaxValue), missing = 9223372036078675L sat, reserve = 20000 sat, fees = 3900 sat).getMessage) awaitCond(bob.stateName == CLOSING) // channel should be advertised as down assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId == bob.stateData.asInstanceOf[DATA_CLOSING].channelId) - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) - bob2blockchain.expectMsgType[PublishTx] - bob2blockchain.expectMsgType[WatchTxConfirmed] + bob2blockchain.expectFinalTxPublished(tx.txid) + bob2blockchain.expectReplaceableTxPublished[ClaimLocalAnchorTx] + bob2blockchain.expectFinalTxPublished("local-main-delayed") + bob2blockchain.expectWatchTxConfirmed(tx.txid) } - test("recv UpdateAddHtlc (insufficient funds w/ pending htlcs) (anchor outputs)", Tag(ChannelStateTestsTags.AnchorOutputs)) { f => + test("recv UpdateAddHtlc (insufficient funds w/ pending htlcs)") { f => import f._ - val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx - alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, 0, 400000000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, 1.0, None)) - alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, 1, 300000000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, 1.0, None)) - alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, 2, 100000000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, 1.0, None)) + val tx = bob.signCommitTx() + alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, 0, 400000000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, Reputation.maxEndorsement, None)) + alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, 1, 300000000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, Reputation.maxEndorsement, None)) + alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, 2, 100000000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, Reputation.maxEndorsement, None)) val error = bob2alice.expectMsgType[Error] assert(new String(error.data.toArray) == InsufficientFunds(channelId(bob), amount = 100000000 msat, missing = 24760 sat, reserve = 20000 sat, fees = 4760 sat).getMessage) awaitCond(bob.stateName == CLOSING) // channel should be advertised as down assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId == bob.stateData.asInstanceOf[DATA_CLOSING].channelId) - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) + bob2blockchain.expectFinalTxPublished(tx.txid) } test("recv UpdateAddHtlc (insufficient funds w/ pending htlcs 1/2)") { f => import f._ - val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx - alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, 0, 400000000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, 1.0, None)) - alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, 1, 200000000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, 1.0, None)) - alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, 2, 167600000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, 1.0, None)) - alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, 3, 10000000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, 1.0, None)) + val tx = bob.signCommitTx() + alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, 0, 400000000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, Reputation.maxEndorsement, None)) + alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, 1, 200000000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, Reputation.maxEndorsement, None)) + alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, 2, 167600000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, Reputation.maxEndorsement, None)) + alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, 3, 10000000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, Reputation.maxEndorsement, None)) val error = bob2alice.expectMsgType[Error] - assert(new String(error.data.toArray) == InsufficientFunds(channelId(bob), amount = 10000000 msat, missing = 11720 sat, reserve = 20000 sat, fees = 14120 sat).getMessage) + assert(new String(error.data.toArray) == InsufficientFunds(channelId(bob), amount = 10000000 msat, missing = 2790 sat, reserve = 20000 sat, fees = 5190 sat).getMessage) awaitCond(bob.stateName == CLOSING) // channel should be advertised as down assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId == bob.stateData.asInstanceOf[DATA_CLOSING].channelId) - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) - bob2blockchain.expectMsgType[PublishTx] - bob2blockchain.expectMsgType[WatchTxConfirmed] + bob2blockchain.expectFinalTxPublished(tx.txid) + bob2blockchain.expectReplaceableTxPublished[ClaimLocalAnchorTx] + bob2blockchain.expectFinalTxPublished("local-main-delayed") + bob2blockchain.expectWatchTxConfirmed(tx.txid) } test("recv UpdateAddHtlc (insufficient funds w/ pending htlcs 2/2)") { f => import f._ - val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx - alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, 0, 300000000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, 1.0, None)) - alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, 1, 300000000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, 1.0, None)) - alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, 2, 500000000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, 1.0, None)) + val tx = bob.signCommitTx() + alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, 0, 300000000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, Reputation.maxEndorsement, None)) + alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, 1, 300000000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, Reputation.maxEndorsement, None)) + alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, 2, 500000000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, Reputation.maxEndorsement, None)) val error = bob2alice.expectMsgType[Error] - assert(new String(error.data.toArray) == InsufficientFunds(channelId(bob), amount = 500000000 msat, missing = 332400 sat, reserve = 20000 sat, fees = 12400 sat).getMessage) + assert(new String(error.data.toArray) == InsufficientFunds(channelId(bob), amount = 500000000 msat, missing = 324760 sat, reserve = 20000 sat, fees = 4760 sat).getMessage) awaitCond(bob.stateName == CLOSING) // channel should be advertised as down assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId == bob.stateData.asInstanceOf[DATA_CLOSING].channelId) - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) - bob2blockchain.expectMsgType[PublishTx] - bob2blockchain.expectMsgType[WatchTxConfirmed] + bob2blockchain.expectFinalTxPublished(tx.txid) + bob2blockchain.expectReplaceableTxPublished[ClaimLocalAnchorTx] + bob2blockchain.expectFinalTxPublished("local-main-delayed") + bob2blockchain.expectWatchTxConfirmed(tx.txid) } test("recv UpdateAddHtlc (over max inflight htlc value)", Tag(ChannelStateTestsTags.AliceLowMaxHtlcValueInFlight)) { f => import f._ - val tx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx - alice2bob.forward(alice, UpdateAddHtlc(ByteVector32.Zeroes, 0, 151000000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, 1.0, None)) + val tx = alice.signCommitTx() + alice2bob.forward(alice, UpdateAddHtlc(ByteVector32.Zeroes, 0, 151_000_000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, Reputation.maxEndorsement, None)) val error = alice2bob.expectMsgType[Error] - assert(new String(error.data.toArray) == HtlcValueTooHighInFlight(channelId(alice), maximum = 150000000 msat, actual = 151000000 msat).getMessage) + assert(new String(error.data.toArray) == HtlcValueTooHighInFlight(channelId(alice), maximum = UInt64(150_000_000), actual = 151_000_000 msat).getMessage) awaitCond(alice.stateName == CLOSING) // channel should be advertised as down assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId == alice.stateData.asInstanceOf[DATA_CLOSING].channelId) - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) - alice2blockchain.expectMsgType[PublishTx] - alice2blockchain.expectMsgType[WatchTxConfirmed] + alice2blockchain.expectFinalTxPublished(tx.txid) + alice2blockchain.expectReplaceableTxPublished[ClaimLocalAnchorTx] + alice2blockchain.expectFinalTxPublished("local-main-delayed") + alice2blockchain.expectWatchTxConfirmed(tx.txid) } test("recv UpdateAddHtlc (over max accepted htlcs)") { f => import f._ - val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val tx = bob.signCommitTx() // Bob accepts a maximum of 30 htlcs for (i <- 0 until 30) { - alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, i, 1000000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, 1.0, None)) + alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, i, 1000000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, Reputation.maxEndorsement, None)) } - alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, 30, 1000000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, 1.0, None)) + alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, 30, 1000000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, Reputation.maxEndorsement, None)) val error = bob2alice.expectMsgType[Error] assert(new String(error.data.toArray) == TooManyAcceptedHtlcs(channelId(bob), maximum = 30).getMessage) awaitCond(bob.stateName == CLOSING) // channel should be advertised as down assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId == bob.stateData.asInstanceOf[DATA_CLOSING].channelId) - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) - bob2blockchain.expectMsgType[PublishTx] - bob2blockchain.expectMsgType[WatchTxConfirmed] + bob2blockchain.expectFinalTxPublished(tx.txid) + bob2blockchain.expectReplaceableTxPublished[ClaimLocalAnchorTx] + bob2blockchain.expectFinalTxPublished("local-main-delayed") + bob2blockchain.expectWatchTxConfirmed(tx.txid) } test("recv CMD_SIGN") { f => @@ -767,7 +728,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with test("recv CMD_SIGN (two identical htlcs in each direction)") { f => import f._ val sender = TestProbe() - val add = CMD_ADD_HTLC(sender.ref, 10000000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, 1.0, None, localOrigin(sender.ref)) + val add = CMD_ADD_HTLC(sender.ref, 10000000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, Reputation.Score.max, None, localOrigin(sender.ref)) alice ! add sender.expectMsgType[RES_SUCCESS[CMD_ADD_HTLC]] alice2bob.expectMsgType[UpdateAddHtlc] @@ -798,7 +759,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with import f._ val sender = TestProbe() // for the test to be really useful we have constraint on parameters - assert(Alice.nodeParams.channelConf.dustLimit > Bob.nodeParams.channelConf.dustLimit) + assert(Alice.nodeParams.channelConf.dustLimit > Bob.nodeParams.channelConf.dustLimit + 10.sat) // and a low feerate to avoid messing with dust exposure limits val currentFeerate = FeeratePerKw(2500 sat) alice.setBitcoinCoreFeerate(currentFeerate) @@ -806,31 +767,23 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with updateFee(currentFeerate, alice, bob, alice2bob, bob2alice) // we're gonna exchange two htlcs in each direction, the goal is to have bob's commitment have 4 htlcs, and alice's // commitment only have 3. We will then check that alice indeed persisted 4 htlcs, and bob only 3. - val aliceMinReceive = Alice.nodeParams.channelConf.dustLimit + weight2fee(currentFeerate, DefaultCommitmentFormat.htlcSuccessWeight) - val aliceMinOffer = Alice.nodeParams.channelConf.dustLimit + weight2fee(currentFeerate, DefaultCommitmentFormat.htlcTimeoutWeight) - val bobMinReceive = Bob.nodeParams.channelConf.dustLimit + weight2fee(currentFeerate, DefaultCommitmentFormat.htlcSuccessWeight) - val bobMinOffer = Bob.nodeParams.channelConf.dustLimit + weight2fee(currentFeerate, DefaultCommitmentFormat.htlcTimeoutWeight) - val a2b_1 = bobMinReceive + 10.sat // will be in alice and bob tx - val a2b_2 = bobMinReceive + 20.sat // will be in alice and bob tx - val b2a_1 = aliceMinReceive + 10.sat // will be in alice and bob tx - val b2a_2 = bobMinOffer + 10.sat // will be only be in bob tx - assert(a2b_1 > aliceMinOffer && a2b_1 > bobMinReceive) - assert(a2b_2 > aliceMinOffer && a2b_2 > bobMinReceive) - assert(b2a_1 > aliceMinReceive && b2a_1 > bobMinOffer) - assert(b2a_2 < aliceMinReceive && b2a_2 > bobMinOffer) - alice ! CMD_ADD_HTLC(sender.ref, a2b_1.toMilliSatoshi, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, 1.0, None, localOrigin(sender.ref)) + val a2b_1 = Alice.nodeParams.channelConf.dustLimit + 10.sat // will be in alice and bob tx + val a2b_2 = Alice.nodeParams.channelConf.dustLimit + 20.sat // will be in alice and bob tx + val b2a_1 = Alice.nodeParams.channelConf.dustLimit + 10.sat // will be in alice and bob tx + val b2a_2 = Bob.nodeParams.channelConf.dustLimit + 10.sat // will be only be in bob tx + alice ! CMD_ADD_HTLC(sender.ref, a2b_1.toMilliSatoshi, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, Reputation.Score.max, None, localOrigin(sender.ref)) sender.expectMsgType[RES_SUCCESS[CMD_ADD_HTLC]] alice2bob.expectMsgType[UpdateAddHtlc] alice2bob.forward(bob) - alice ! CMD_ADD_HTLC(sender.ref, a2b_2.toMilliSatoshi, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, 1.0, None, localOrigin(sender.ref)) + alice ! CMD_ADD_HTLC(sender.ref, a2b_2.toMilliSatoshi, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, Reputation.Score.max, None, localOrigin(sender.ref)) sender.expectMsgType[RES_SUCCESS[CMD_ADD_HTLC]] alice2bob.expectMsgType[UpdateAddHtlc] alice2bob.forward(bob) - bob ! CMD_ADD_HTLC(sender.ref, b2a_1.toMilliSatoshi, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, 1.0, None, localOrigin(sender.ref)) + bob ! CMD_ADD_HTLC(sender.ref, b2a_1.toMilliSatoshi, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, Reputation.Score.max, None, localOrigin(sender.ref)) sender.expectMsgType[RES_SUCCESS[CMD_ADD_HTLC]] bob2alice.expectMsgType[UpdateAddHtlc] bob2alice.forward(alice) - bob ! CMD_ADD_HTLC(sender.ref, b2a_2.toMilliSatoshi, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, 1.0, None, localOrigin(sender.ref)) + bob ! CMD_ADD_HTLC(sender.ref, b2a_2.toMilliSatoshi, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, Reputation.Score.max, None, localOrigin(sender.ref)) sender.expectMsgType[RES_SUCCESS[CMD_ADD_HTLC]] bob2alice.expectMsgType[UpdateAddHtlc] bob2alice.forward(alice) @@ -850,7 +803,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with test("recv CMD_SIGN (htlcs with same pubkeyScript but different amounts)") { f => import f._ val sender = TestProbe() - val add = CMD_ADD_HTLC(sender.ref, 10000000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, 1.0, None, localOrigin(sender.ref)) + val add = CMD_ADD_HTLC(sender.ref, 10_000_000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, Reputation.Score.max, None, localOrigin(sender.ref)) val epsilons = List(3, 1, 5, 7, 6) // unordered on purpose val htlcCount = epsilons.size for (i <- epsilons) { @@ -864,9 +817,9 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val commitSig = alice2bob.expectMsgType[CommitSig] assert(commitSig.htlcSignatures.toSet.size == htlcCount) alice2bob.forward(bob) - awaitCond(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.htlcTxsAndRemoteSigs.size == htlcCount) - val htlcTxs = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.htlcTxsAndRemoteSigs - val amounts = htlcTxs.map(_.htlcTx.tx.txOut.head.amount.toLong) + awaitCond(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.htlcRemoteSigs.size == htlcCount) + val htlcTxs = bob.htlcTxs() + val amounts = htlcTxs.map(_.tx.txOut.head.amount.toLong) assert(amounts == amounts.sorted) } @@ -874,7 +827,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with import f._ val sender = TestProbe() alice ! CMD_SIGN() - sender.expectNoMessage(1 second) // just ignored + sender.expectNoMessage(100 millis) // just ignored //sender.expectMsg("cannot sign when there are no changes") } @@ -914,6 +867,10 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with test("recv CMD_SIGN (going above balance threshold)", Tag(ChannelStateTestsTags.NoPushAmount), Tag(ChannelStateTestsTags.ChannelsPublic), Tag(ChannelStateTestsTags.DoNotInterceptGossip), Tag(ChannelStateTestsTags.AdaptMaxHtlcAmount)) { f => import f._ + val listener = TestProbe() + systemA.eventStream.subscribe(listener.ref, classOf[OutgoingHtlcAdded]) + systemA.eventStream.subscribe(listener.ref, classOf[OutgoingHtlcFulfilled]) + val aliceListener = TestProbe() alice.underlying.system.eventStream.subscribe(aliceListener.ref, classOf[LocalChannelUpdate]) val bobListener = TestProbe() @@ -935,7 +892,9 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with } assert(alice.stateData.asInstanceOf[DATA_NORMAL].channelUpdate.channelFlags.isEnabled) inside(bobListener.expectMsgType[LocalChannelUpdate]) { lcu => - assert(lcu.channelUpdate.htlcMaximumMsat == 0.msat) + assert(lcu.commitments.latest.localCommitParams.htlcMinimum == 1000.msat) + assert(lcu.commitments.latest.remoteCommitParams.htlcMinimum == 0.msat) + assert(lcu.channelUpdate.htlcMaximumMsat == 1000.msat) assert(lcu.channelUpdate.shortChannelId.isInstanceOf[RealShortChannelId]) assert(lcu.channelUpdate.channelFlags.isEnabled) } @@ -950,15 +909,17 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.spec.toLocal == 1_000_000_000.msat) assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.spec.toLocal == 0.msat) val (p1, htlc1) = addHtlc(10_000_000 msat, alice, bob, alice2bob, bob2alice) + listener.expectMsgType[OutgoingHtlcAdded] crossSign(alice, bob, alice2bob, bob2alice) fulfillHtlc(htlc1.id, p1, bob, alice, bob2alice, alice2bob) + listener.expectMsgType[OutgoingHtlcFulfilled] crossSign(bob, alice, bob2alice, alice2bob) assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.spec.toLocal == 990_000_000.msat) assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.spec.toLocal == 10_000_000.msat) aliceListener.expectNoMessage(100 millis) bobListener.expectNoMessage(100 millis) assert(alice.stateData.asInstanceOf[DATA_NORMAL].channelUpdate.htlcMaximumMsat == 500_000_000.msat) - assert(bob.stateData.asInstanceOf[DATA_NORMAL].channelUpdate.htlcMaximumMsat == 0.msat) + assert(bob.stateData.asInstanceOf[DATA_NORMAL].channelUpdate.htlcMaximumMsat == 1000.msat) // Alice sends more funds, reaching Bob's third balance bucket and causing him to update his htlc_maximum_msat. val (p2, htlc2) = addHtlc(2_000_000 msat, alice, bob, alice2bob, bob2alice) @@ -979,8 +940,8 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with crossSign(alice, bob, alice2bob, bob2alice) assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.spec.toLocal == 989_500_000.msat) assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.spec.toLocal == 10_500_000.msat) - assert(bobListener.expectMsgType[LocalChannelUpdate].channelUpdate.htlcMaximumMsat == 0.msat) - awaitCond(bob.stateData.asInstanceOf[DATA_NORMAL].channelUpdate.htlcMaximumMsat == 0.msat) + assert(bobListener.expectMsgType[LocalChannelUpdate].channelUpdate.htlcMaximumMsat == 1000.msat) + awaitCond(bob.stateData.asInstanceOf[DATA_NORMAL].channelUpdate.htlcMaximumMsat == 1000.msat) aliceListener.expectNoMessage(100 millis) assert(alice.stateData.asInstanceOf[DATA_NORMAL].channelUpdate.htlcMaximumMsat == 500_000_000.msat) @@ -997,14 +958,14 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(alice.stateData.asInstanceOf[DATA_NORMAL].channelUpdate.htlcMaximumMsat == 500_000_000.msat) // Alice sends another large amount and goes below her balance threshold. - val (p5, htlc5) = addHtlc(439_500_000 msat, alice, bob, alice2bob, bob2alice) + val (p5, htlc5) = addHtlc(452_000_000 msat, alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) fulfillHtlc(htlc5.id, p5, bob, alice, bob2alice, alice2bob) crossSign(bob, alice, bob2alice, alice2bob) - assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.spec.toLocal == 50_000_000.msat) + assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.spec.toLocal == 37_500_000.msat) assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.availableBalanceForSend > 5_000_000.msat) assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.availableBalanceForSend < 10_000_000.msat) - assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.spec.toLocal == 950_000_000.msat) + assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.spec.toLocal == 962_500_000.msat) bobListener.expectNoMessage(100 millis) assert(bob.stateData.asInstanceOf[DATA_NORMAL].channelUpdate.htlcMaximumMsat == 500_000_000.msat) assert(aliceListener.expectMsgType[LocalChannelUpdate].channelUpdate.htlcMaximumMsat == 5_000_000.msat) @@ -1038,13 +999,13 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with bob2alice.expectMsgType[CommitSig] awaitCond(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.spec.htlcs.collect(incoming).exists(_.id == htlc.id)) - assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.htlcTxsAndRemoteSigs.size == 1) + assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.htlcRemoteSigs.size == 1) assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.spec.toLocal == initialState.commitments.latest.localCommit.spec.toLocal) assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.changes.remoteChanges.acked.size == 0) assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.changes.remoteChanges.signed.size == 1) } - test("recv CommitSig (one htlc sent)") { f => + test("recv CommitSig (one htlc sent)", Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => import f._ val (_, htlc) = addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) @@ -1061,38 +1022,11 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with bob2alice.forward(alice) awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.spec.htlcs.collect(outgoing).exists(_.id == htlc.id)) - assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.htlcTxsAndRemoteSigs.size == 1) + assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.htlcRemoteSigs.size == 1) assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.spec.toLocal == initialState.commitments.latest.localCommit.spec.toLocal) } - test("recv CommitSig (multiple htlcs in both directions)") { f => - import f._ - - addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) // a->b (regular) - addHtlc(80000000 msat, alice, bob, alice2bob, bob2alice) // a->b (regular) - addHtlc(1200000 msat, bob, alice, bob2alice, alice2bob) // b->a (trimmed to dust) - addHtlc(10000000 msat, alice, bob, alice2bob, bob2alice) // a->b (regular) - addHtlc(50000000 msat, bob, alice, bob2alice, alice2bob) // b->a (regular) - addHtlc(1200000 msat, alice, bob, alice2bob, bob2alice) // a->b (trimmed to dust) - addHtlc(40000000 msat, bob, alice, bob2alice, alice2bob) // b->a (regular) - - alice ! CMD_SIGN() - val aliceCommitSig = alice2bob.expectMsgType[CommitSig] - assert(aliceCommitSig.htlcSignatures.length == 3) - alice2bob.forward(bob, aliceCommitSig) - bob2alice.expectMsgType[RevokeAndAck] - bob2alice.forward(alice) - - // actual test begins - val bobCommitSig = bob2alice.expectMsgType[CommitSig] - assert(bobCommitSig.htlcSignatures.length == 5) - bob2alice.forward(alice, bobCommitSig) - - awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.index == 1) - assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.htlcTxsAndRemoteSigs.size == 5) - } - - test("recv CommitSig (multiple htlcs in both directions) (anchor outputs)", Tag(ChannelStateTestsTags.AnchorOutputs)) { f => + def testRecvCommitSigMultipleHtlcs(f: FixtureParam): Unit = { import f._ addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) // a->b (regular) @@ -1116,10 +1050,18 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with bob2alice.forward(alice, bobCommitSig) awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.index == 1) - assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.htlcTxsAndRemoteSigs.size == 3) + assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.htlcRemoteSigs.size == 3) } - test("recv CommitSig (multiple htlcs in both directions) (anchor outputs zero fee htlc txs)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + test("recv CommitSig (multiple htlcs in both directions, anchor outputs phoenix)", Tag(ChannelStateTestsTags.AnchorOutputsPhoenix)) { f => + testRecvCommitSigMultipleHtlcs(f) + } + + test("recv CommitSig (multiple htlcs in both directions, phoenix taproot)", Tag(ChannelStateTestsTags.OptionSimpleTaprootPhoenix)) { f => + testRecvCommitSigMultipleHtlcs(f) + } + + def testRecvCommitSigMultipleHtlcZeroFees(f: FixtureParam): Unit = { import f._ addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) // a->b (regular) @@ -1143,7 +1085,15 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with bob2alice.forward(alice, bobCommitSig) awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.index == 1) - assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.htlcTxsAndRemoteSigs.size == 5) + assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.htlcRemoteSigs.size == 5) + } + + test("recv CommitSig (multiple htlcs in both directions, anchor outputs zero fee htlc txs)") { f => + testRecvCommitSigMultipleHtlcZeroFees(f) + } + + test("recv CommitSig (multiple htlcs in both directions, taproot zero fee htlc txs)", Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => + testRecvCommitSigMultipleHtlcZeroFees(f) } test("recv CommitSig (multiple htlcs in both directions) (without fundingTxId tlv)") { f => @@ -1169,12 +1119,12 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with test("recv CommitSig (only fee update)") { f => import f._ - alice ! CMD_UPDATE_FEE(TestConstants.feeratePerKw + FeeratePerKw(1000 sat), commit = false) + alice ! CMD_UPDATE_FEE(TestConstants.anchorOutputsFeeratePerKw + FeeratePerKw(1000 sat), commit = false) alice ! CMD_SIGN() // actual test begins (note that channel sends a CMD_SIGN to itself when it receives RevokeAndAck and there are changes) val updateFee = alice2bob.expectMsgType[UpdateFee] - assert(updateFee.feeratePerKw == TestConstants.feeratePerKw + FeeratePerKw(1000 sat)) + assert(updateFee.feeratePerKw == TestConstants.anchorOutputsFeeratePerKw + FeeratePerKw(1000 sat)) alice2bob.forward(bob) alice2bob.expectMsgType[CommitSig] alice2bob.forward(bob) @@ -1188,12 +1138,12 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val r = randomBytes32() val h = Crypto.sha256(r) - alice ! CMD_ADD_HTLC(sender.ref, 50000000 msat, h, CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, 1.0, None, localOrigin(sender.ref)) + alice ! CMD_ADD_HTLC(sender.ref, 50000000 msat, h, CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, Reputation.Score.max, None, localOrigin(sender.ref)) sender.expectMsgType[RES_SUCCESS[CMD_ADD_HTLC]] val htlc1 = alice2bob.expectMsgType[UpdateAddHtlc] alice2bob.forward(bob) - alice ! CMD_ADD_HTLC(sender.ref, 50000000 msat, h, CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, 1.0, None, localOrigin(sender.ref)) + alice ! CMD_ADD_HTLC(sender.ref, 50000000 msat, h, CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, Reputation.Score.max, None, localOrigin(sender.ref)) sender.expectMsgType[RES_SUCCESS[CMD_ADD_HTLC]] val htlc2 = alice2bob.expectMsgType[UpdateAddHtlc] alice2bob.forward(bob) @@ -1203,46 +1153,72 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with crossSign(alice, bob, alice2bob, bob2alice) awaitCond(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.spec.htlcs.collect(incoming).exists(_.id == htlc1.id)) - assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.htlcTxsAndRemoteSigs.size == 2) + assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.htlcRemoteSigs.size == 2) assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.spec.toLocal == initialState.commitments.latest.localCommit.spec.toLocal) - assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx.txOut.count(_.amount == 50000.sat) == 2) + assert(bob.signCommitTx().txOut.count(_.amount == 50000.sat) == 2) } - ignore("recv CommitSig (no changes)") { f => + test("recv CommitSig (invalid signature)") { f => import f._ - val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx - // signature is invalid but it doesn't matter - bob ! CommitSig(ByteVector32.Zeroes, ByteVector64.Zeroes, Nil) + addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) + val tx = bob.signCommitTx() + + // actual test begins + bob ! CommitSig(ByteVector32.Zeroes, IndividualSignature(ByteVector64.Zeroes), Nil) val error = bob2alice.expectMsgType[Error] - assert(new String(error.data.toArray).startsWith("cannot sign when there are no changes")) + assert(new String(error.data.toArray).startsWith("invalid commitment signature")) awaitCond(bob.stateName == CLOSING) - // channel should be advertised as down - assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId == bob.stateData.asInstanceOf[DATA_CLOSING].channelId) - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) - bob2blockchain.expectMsgType[PublishTx] - bob2blockchain.expectMsgType[WatchTxConfirmed] + bob2blockchain.expectFinalTxPublished(tx.txid) + bob2blockchain.expectReplaceableTxPublished[ClaimLocalAnchorTx] + bob2blockchain.expectFinalTxPublished("local-main-delayed") + bob2blockchain.expectWatchTxConfirmed(tx.txid) } - test("recv CommitSig (invalid signature)") { f => + test("recv CommitSig (simple taproot channels, missing partial signature)", Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => import f._ addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) - val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val tx = bob.signCommitTx() // actual test begins - bob ! CommitSig(ByteVector32.Zeroes, ByteVector64.Zeroes, Nil) + alice ! CMD_SIGN() + val commitSig = alice2bob.expectMsgType[CommitSig] + val commitSigMissingPartialSig = commitSig.copy(tlvStream = commitSig.tlvStream.copy(records = commitSig.tlvStream.records.filterNot(_.isInstanceOf[CommitSigTlv.PartialSignatureWithNonceTlv]))) + bob ! commitSigMissingPartialSig val error = bob2alice.expectMsgType[Error] assert(new String(error.data.toArray).startsWith("invalid commitment signature")) awaitCond(bob.stateName == CLOSING) - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) - bob2blockchain.expectMsgType[PublishTx] - bob2blockchain.expectMsgType[WatchTxConfirmed] + bob2blockchain.expectFinalTxPublished(tx.txid) + bob2blockchain.expectReplaceableTxPublished[ClaimLocalAnchorTx] + bob2blockchain.expectFinalTxPublished("local-main-delayed") + bob2blockchain.expectWatchTxConfirmed(tx.txid) + } + + test("recv CommitSig (simple taproot channels, invalid partial signature)", Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => + import f._ + addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) + val tx = bob.signCommitTx() + + // actual test begins + alice ! CMD_SIGN() + val commitSig = alice2bob.expectMsgType[CommitSig] + val Some(psig) = commitSig.partialSignature_opt + val invalidPsig = CommitSigTlv.PartialSignatureWithNonceTlv(psig.copy(partialSig = psig.partialSig.reverse)) + val commitSigWithInvalidPsig = commitSig.copy(tlvStream = commitSig.tlvStream.copy(records = commitSig.tlvStream.records.filterNot(_.isInstanceOf[CommitSigTlv.PartialSignatureWithNonceTlv]) + invalidPsig)) + bob ! commitSigWithInvalidPsig + val error = bob2alice.expectMsgType[Error] + assert(new String(error.data.toArray).startsWith("invalid commitment signature")) + awaitCond(bob.stateName == CLOSING) + bob2blockchain.expectFinalTxPublished(tx.txid) + bob2blockchain.expectReplaceableTxPublished[ClaimLocalAnchorTx] + bob2blockchain.expectFinalTxPublished("local-main-delayed") + bob2blockchain.expectWatchTxConfirmed(tx.txid) } test("recv CommitSig (bad htlc sig count)") { f => import f._ addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) - val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val tx = bob.signCommitTx() alice ! CMD_SIGN() val commitSig = alice2bob.expectMsgType[CommitSig] @@ -1252,44 +1228,30 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with bob ! badCommitSig val error = bob2alice.expectMsgType[Error] assert(new String(error.data.toArray) == HtlcSigCountMismatch(channelId(bob), expected = 1, actual = 2).getMessage) - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) - bob2blockchain.expectMsgType[PublishTx] - bob2blockchain.expectMsgType[WatchTxConfirmed] + bob2blockchain.expectFinalTxPublished(tx.txid) + bob2blockchain.expectReplaceableTxPublished[ClaimLocalAnchorTx] + bob2blockchain.expectFinalTxPublished("local-main-delayed") + bob2blockchain.expectWatchTxConfirmed(tx.txid) } test("recv CommitSig (invalid htlc sig)") { f => import f._ addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) - val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val tx = bob.signCommitTx() alice ! CMD_SIGN() val commitSig = alice2bob.expectMsgType[CommitSig] // actual test begins - val badCommitSig = commitSig.copy(htlcSignatures = commitSig.signature :: Nil) + val badCommitSig = commitSig.copy(htlcSignatures = commitSig.signature.sig :: Nil) bob ! badCommitSig val error = bob2alice.expectMsgType[Error] assert(new String(error.data.toArray).startsWith("invalid htlc signature")) - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) - bob2blockchain.expectMsgType[PublishTx] - bob2blockchain.expectMsgType[WatchTxConfirmed] - } - - test("recv RevokeAndAck (one htlc sent)") { f => - import f._ - addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) - - alice ! CMD_SIGN() - alice2bob.expectMsgType[CommitSig] - alice2bob.forward(bob) - - // actual test begins - awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteNextCommitInfo.isLeft) - bob2alice.expectMsgType[RevokeAndAck] - bob2alice.forward(alice) - awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteNextCommitInfo.isRight) - awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.changes.localChanges.acked.size == 1) + bob2blockchain.expectFinalTxPublished(tx.txid) + bob2blockchain.expectReplaceableTxPublished[ClaimLocalAnchorTx] + bob2blockchain.expectFinalTxPublished("local-main-delayed") + bob2blockchain.expectWatchTxConfirmed(tx.txid) } test("recv RevokeAndAck (one htlc received)") { f => @@ -1366,9 +1328,9 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice2bob.expectMsgType[CommitSig] } - test("recv RevokeAndAck (invalid preimage)") { f => + test("recv RevokeAndAck (invalid revocation)") { f => import f._ - val tx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val tx = alice.signCommitTx() addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) alice ! CMD_SIGN() @@ -1382,47 +1344,92 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with awaitCond(alice.stateName == CLOSING) // channel should be advertised as down assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId == alice.stateData.asInstanceOf[DATA_CLOSING].channelId) - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) - alice2blockchain.expectMsgType[PublishTx] - alice2blockchain.expectMsgType[WatchTxConfirmed] + alice2blockchain.expectFinalTxPublished(tx.txid) + alice2blockchain.expectReplaceableTxPublished[ClaimLocalAnchorTx] + alice2blockchain.expectFinalTxPublished("local-main-delayed") + alice2blockchain.expectWatchTxConfirmed(tx.txid) + } + + test("recv RevokeAndAck (simple taproot channels, missing nonce)", Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => + import f._ + val tx = alice.signCommitTx() + addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) + + alice ! CMD_SIGN() + alice2bob.expectMsgType[CommitSig] + alice2bob.forward(bob) + + // actual test begins + val revokeAndAck = bob2alice.expectMsgType[RevokeAndAck] + val revokeAndAckWithMissingNonce = revokeAndAck.copy(tlvStream = revokeAndAck.tlvStream.copy(records = revokeAndAck.tlvStream.records.filterNot(tlv => tlv.isInstanceOf[RevokeAndAckTlv.NextLocalNoncesTlv]))) + alice ! revokeAndAckWithMissingNonce + alice2bob.expectMsgType[Error] + awaitCond(alice.stateName == CLOSING) + // channel should be advertised as down + assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId == alice.stateData.asInstanceOf[DATA_CLOSING].channelId) + alice2blockchain.expectFinalTxPublished(tx.txid) + alice2blockchain.expectReplaceableTxPublished[ClaimLocalAnchorTx] + alice2blockchain.expectFinalTxPublished("local-main-delayed") + alice2blockchain.expectWatchTxConfirmed(tx.txid) + } + + test("recv RevokeAndAck (simple taproot channels, invalid nonce)", Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => + import f._ + // Alice sends an HTLC to Bob. + val (r, add) = addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) + alice ! CMD_SIGN() + alice2bob.expectMsgType[CommitSig] + alice2bob.forward(bob) + // Bob responds with an invalid nonce for its *next* commitment. + val revokeAndAck = bob2alice.expectMsgType[RevokeAndAck] + val bobInvalidNonces = RevokeAndAckTlv.NextLocalNoncesTlv(revokeAndAck.nextCommitNonces.map { case (txId, _) => txId -> NonceGenerator.signingNonce(randomKey().publicKey, randomKey().publicKey, txId).publicNonce }.toSeq) + val revokeAndAckWithInvalidNonce = revokeAndAck.copy(tlvStream = revokeAndAck.tlvStream.copy(records = revokeAndAck.tlvStream.records.filterNot(tlv => tlv.isInstanceOf[RevokeAndAckTlv.NextLocalNoncesTlv]) + bobInvalidNonces)) + bob2alice.forward(alice, revokeAndAckWithInvalidNonce) + // This applies to the *next* commitment, there is no issue when finalizing the *current* commitment. + bob2alice.expectMsgType[CommitSig] + bob2alice.forward(alice) + alice2bob.expectMsgType[RevokeAndAck] + alice2bob.forward(bob) + // Bob will force-close when receiving Alice's next commit_sig. + val commitTx = bob.signCommitTx() + fulfillHtlc(add.id, r, bob, alice, bob2alice, alice2bob) + bob ! CMD_SIGN() + bob2alice.expectMsgType[CommitSig] + bob2alice.forward(alice) + alice2bob.expectMsgType[RevokeAndAck] + alice2bob.forward(bob) + alice2bob.expectMsgType[CommitSig] + alice2bob.forward(bob) + bob2alice.expectMsgType[Error] + awaitCond(bob.stateName == CLOSING) + bob2blockchain.expectFinalTxPublished(commitTx.txid) } test("recv RevokeAndAck (over max dust htlc exposure)") { f => import f._ val aliceCommitments = alice.stateData.asInstanceOf[DATA_NORMAL].commitments assert(alice.underlyingActor.nodeParams.onChainFeeConf.feerateToleranceFor(bob.underlyingActor.nodeParams.nodeId).dustTolerance.maxExposure == 25_000.sat) - assert(Transactions.offeredHtlcTrimThreshold(aliceCommitments.params.localParams.dustLimit, aliceCommitments.latest.localCommit.spec, aliceCommitments.params.commitmentFormat) == 7730.sat) - assert(Transactions.receivedHtlcTrimThreshold(aliceCommitments.params.remoteParams.dustLimit, aliceCommitments.latest.localCommit.spec, aliceCommitments.params.commitmentFormat) == 8030.sat) + assert(Transactions.offeredHtlcTrimThreshold(aliceCommitments.latest.localCommitParams.dustLimit, aliceCommitments.latest.localCommit.spec, aliceCommitments.latest.commitmentFormat) == 1100.sat) + assert(Transactions.receivedHtlcTrimThreshold(aliceCommitments.latest.remoteCommitParams.dustLimit, aliceCommitments.latest.localCommit.spec, aliceCommitments.latest.commitmentFormat) == 1000.sat) - // Alice sends HTLCs to Bob that add 10 000 sat to the dust exposure: - addHtlc(500.sat.toMilliSatoshi, alice, bob, alice2bob, bob2alice) // dust htlc - addHtlc(1250.sat.toMilliSatoshi, alice, bob, alice2bob, bob2alice) // trimmed htlc - addHtlc(8250.sat.toMilliSatoshi, alice, bob, alice2bob, bob2alice) // slightly above the trimmed threshold -> included in the dust exposure + // Alice sends HTLCs to Bob that add 24 000 sat to the dust exposure: + (1 to 24).foreach(_ => addHtlc(1000.sat.toMilliSatoshi, alice, bob, alice2bob, bob2alice)) crossSign(alice, bob, alice2bob, bob2alice) // Bob sends HTLCs to Alice that overflow the dust exposure: - val (_, dust1) = addHtlc(500.sat.toMilliSatoshi, bob, alice, bob2alice, alice2bob) // dust htlc - val (_, dust2) = addHtlc(500.sat.toMilliSatoshi, bob, alice, bob2alice, alice2bob) // dust htlc - val (_, trimmed1) = addHtlc(4000.sat.toMilliSatoshi, bob, alice, bob2alice, alice2bob) // trimmed htlc - val (_, trimmed2) = addHtlc(6400.sat.toMilliSatoshi, bob, alice, bob2alice, alice2bob) // trimmed htlc - val (_, almostTrimmed) = addHtlc(8500.sat.toMilliSatoshi, bob, alice, bob2alice, alice2bob) // slightly above the trimmed threshold -> included in the dust exposure - val (_, nonDust) = addHtlc(20000.sat.toMilliSatoshi, bob, alice, bob2alice, alice2bob) // way above the trimmed threshold -> not included in the dust exposure + val (_, dust1) = addHtlc(750.sat.toMilliSatoshi, bob, alice, bob2alice, alice2bob) + val (_, dust2) = addHtlc(500.sat.toMilliSatoshi, bob, alice, bob2alice, alice2bob) + val (_, nonDust) = addHtlc(20_000.sat.toMilliSatoshi, bob, alice, bob2alice, alice2bob) // way above the trimmed threshold -> not included in the dust exposure crossSign(bob, alice, bob2alice, alice2bob) // Alice forwards HTLCs that fit in the dust exposure. alice2relayer.expectMsgAllOf( - RelayForward(nonDust, TestConstants.Bob.nodeParams.nodeId), - RelayForward(almostTrimmed, TestConstants.Bob.nodeParams.nodeId), - RelayForward(trimmed2, TestConstants.Bob.nodeParams.nodeId), + RelayForward(dust1, TestConstants.Bob.nodeParams.nodeId, 3.0 / 30), + RelayForward(nonDust, TestConstants.Bob.nodeParams.nodeId, 3.0 / 30), ) alice2relayer.expectNoMessage(100 millis) // And instantly fails the others. - val failedHtlcs = Seq( - alice2bob.expectMsgType[UpdateFailHtlc], - alice2bob.expectMsgType[UpdateFailHtlc], - alice2bob.expectMsgType[UpdateFailHtlc] - ) - assert(failedHtlcs.map(_.id).toSet == Set(dust1.id, dust2.id, trimmed1.id)) + assert(alice2bob.expectMsgType[UpdateFailHtlc].id == dust2.id) alice2bob.expectMsgType[CommitSig] alice2bob.expectNoMessage(100 millis) } @@ -1433,19 +1440,15 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(alice.underlyingActor.nodeParams.onChainFeeConf.feerateToleranceFor(bob.underlyingActor.nodeParams.nodeId).dustTolerance.maxExposure == 25_000.sat) // Bob sends HTLCs to Alice that add 10 000 sat to the dust exposure. - addHtlc(4000.sat.toMilliSatoshi, bob, alice, bob2alice, alice2bob) - addHtlc(6000.sat.toMilliSatoshi, bob, alice, bob2alice, alice2bob) + (1 to 10).foreach(_ => addHtlc(1000.sat.toMilliSatoshi, bob, alice, bob2alice, alice2bob)) crossSign(bob, alice, bob2alice, alice2bob) - alice2relayer.expectMsgType[RelayForward] - alice2relayer.expectMsgType[RelayForward] + (1 to 10).foreach(_ => alice2relayer.expectMsgType[RelayForward]) // Alice sends HTLCs to Bob that add 10 000 sat to the dust exposure but doesn't sign them yet. - addHtlc(6500.sat.toMilliSatoshi, alice, bob, alice2bob, bob2alice) - addHtlc(3500.sat.toMilliSatoshi, alice, bob, alice2bob, bob2alice) + (1 to 10).foreach(_ => addHtlc(1000.sat.toMilliSatoshi, alice, bob, alice2bob, bob2alice)) // Bob sends HTLCs to Alice that add 10 000 sat to the dust exposure. - val (_, rejectedHtlc) = addHtlc(7000.sat.toMilliSatoshi, bob, alice, bob2alice, alice2bob) - val (_, acceptedHtlc) = addHtlc(3000.sat.toMilliSatoshi, bob, alice, bob2alice, alice2bob) + (1 to 10).foreach(_ => addHtlc(1000.sat.toMilliSatoshi, bob, alice, bob2alice, alice2bob)) bob ! CMD_SIGN(Some(sender.ref)) sender.expectMsgType[RES_SUCCESS[CMD_SIGN]] bob2alice.expectMsgType[CommitSig] @@ -1458,9 +1461,9 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with bob2alice.forward(alice) // Alice forwards HTLCs that fit in the dust exposure and instantly fails the others. - alice2relayer.expectMsg(RelayForward(acceptedHtlc, TestConstants.Bob.nodeParams.nodeId)) + (1 to 5).foreach(_ => alice2relayer.expectMsgType[RelayForward]) alice2relayer.expectNoMessage(100 millis) - assert(alice2bob.expectMsgType[UpdateFailHtlc].id == rejectedHtlc.id) + (1 to 5).foreach(_ => alice2bob.expectMsgType[UpdateFailHtlc]) alice2bob.expectMsgType[CommitSig] alice2bob.expectNoMessage(100 millis) } @@ -1499,14 +1502,14 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice2bob.expectNoMessage(100 millis) } - test("recv RevokeAndAck (over max dust htlc exposure in local commit only with pending local changes)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), Tag(ChannelStateTestsTags.HighDustLimitDifferenceAliceBob)) { f => + test("recv RevokeAndAck (over max dust htlc exposure in local commit only with pending local changes)", Tag(ChannelStateTestsTags.HighDustLimitDifferenceAliceBob)) { f => import f._ assert(alice.underlyingActor.nodeParams.channelConf.dustLimit == 5000.sat) assert(bob.underlyingActor.nodeParams.channelConf.dustLimit == 1000.sat) testRevokeAndAckDustOverflowSingleCommit(f) } - test("recv RevokeAndAck (over max dust htlc exposure in remote commit only with pending local changes)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), Tag(ChannelStateTestsTags.HighDustLimitDifferenceBobAlice)) { f => + test("recv RevokeAndAck (over max dust htlc exposure in remote commit only with pending local changes)", Tag(ChannelStateTestsTags.HighDustLimitDifferenceBobAlice)) { f => import f._ assert(alice.underlyingActor.nodeParams.channelConf.dustLimit == 1000.sat) assert(bob.underlyingActor.nodeParams.channelConf.dustLimit == 5000.sat) @@ -1515,25 +1518,32 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with test("recv RevokeAndAck (unexpectedly)") { f => import f._ - val tx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val tx = alice.signCommitTx() awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteNextCommitInfo.isRight) alice ! RevokeAndAck(ByteVector32.Zeroes, PrivateKey(randomBytes32()), PrivateKey(randomBytes32()).publicKey) alice2bob.expectMsgType[Error] awaitCond(alice.stateName == CLOSING) // channel should be advertised as down assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId == alice.stateData.asInstanceOf[DATA_CLOSING].channelId) - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) - alice2blockchain.expectMsgType[PublishTx] - alice2blockchain.expectMsgType[WatchTxConfirmed] + alice2blockchain.expectFinalTxPublished(tx.txid) + alice2blockchain.expectReplaceableTxPublished[ClaimLocalAnchorTx] + alice2blockchain.expectFinalTxPublished("local-main-delayed") + alice2blockchain.expectWatchTxConfirmed(tx.txid) } test("recv RevokeAndAck (forward UpdateFailHtlc)") { f => import f._ + val listener = TestProbe() + systemA.eventStream.subscribe(listener.ref, classOf[OutgoingHtlcAdded]) + systemA.eventStream.subscribe(listener.ref, classOf[OutgoingHtlcFailed]) + val (_, htlc) = addHtlc(150000000 msat, alice, bob, alice2bob, bob2alice) + listener.expectMsgType[OutgoingHtlcAdded] crossSign(alice, bob, alice2bob, bob2alice) - bob ! CMD_FAIL_HTLC(htlc.id, FailureReason.LocalFailure(PermanentChannelFailure())) + bob ! CMD_FAIL_HTLC(htlc.id, FailureReason.LocalFailure(PermanentChannelFailure()), None) val fail = bob2alice.expectMsgType[UpdateFailHtlc] bob2alice.forward(alice) + listener.expectMsgType[OutgoingHtlcFailed] bob ! CMD_SIGN() bob2alice.expectMsgType[CommitSig] bob2alice.forward(alice) @@ -1579,51 +1589,51 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(forward.htlc == htlc) } - def testRevokeAndAckHtlcStaticRemoteKey(f: FixtureParam): Unit = { + def testRevokeAndAckHtlc(f: FixtureParam, commitmentFormat: CommitmentFormat): Unit = { import f._ - assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.params.localParams.initFeatures.hasFeature(StaticRemoteKey)) - assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.params.localParams.initFeatures.hasFeature(StaticRemoteKey)) + assert(alice.commitments.latest.commitmentFormat == commitmentFormat) + assert(bob.commitments.latest.commitmentFormat == commitmentFormat) def aliceToRemoteScript(): ByteVector = { - val toRemoteAmount = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.spec.toRemote - val Some(toRemoteOut) = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx.txOut.find(_.amount == toRemoteAmount.truncateToSatoshi) + val toRemoteAmount = alice.commitments.latest.localCommit.spec.toRemote + val Some(toRemoteOut) = alice.signCommitTx().txOut.find(_.amount == toRemoteAmount.truncateToSatoshi) toRemoteOut.publicKeyScript } val initialToRemoteScript = aliceToRemoteScript() - addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) + addHtlc(50_000_000 msat, alice, bob, alice2bob, bob2alice) alice ! CMD_SIGN() alice2bob.expectMsgType[CommitSig] alice2bob.forward(bob) bob2alice.expectMsgType[RevokeAndAck] bob2alice.forward(alice) - awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteNextCommitInfo.isRight) + awaitCond(alice.commitments.remoteNextCommitInfo.isRight) bob2alice.expectMsgType[CommitSig] bob2alice.forward(alice) alice2bob.expectMsgType[RevokeAndAck] alice2bob.forward(bob) - awaitCond(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteNextCommitInfo.isRight) + awaitCond(bob.commitments.remoteNextCommitInfo.isRight) awaitCond(alice.stateName == NORMAL) // using option_static_remotekey alice's view of bob toRemote script stays the same across commitments assert(initialToRemoteScript == aliceToRemoteScript()) } - test("recv RevokeAndAck (one htlc sent, static_remotekey)", Tag(ChannelStateTestsTags.StaticRemoteKey)) { - testRevokeAndAckHtlcStaticRemoteKey _ + test("recv RevokeAndAck (one htlc sent)") { f => + testRevokeAndAckHtlc(f, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) } - test("recv RevokeAndAck (one htlc sent, anchor_outputs)", Tag(ChannelStateTestsTags.AnchorOutputs)) { - testRevokeAndAckHtlcStaticRemoteKey _ + test("recv RevokeAndAck (one htlc sent, anchor_outputs_phoenix)", Tag(ChannelStateTestsTags.AnchorOutputsPhoenix)) { f => + testRevokeAndAckHtlc(f, UnsafeLegacyAnchorOutputsCommitmentFormat) } - test("recv RevokeAndAck (one htlc sent, anchors_zero_fee_htlc_tx)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { - testRevokeAndAckHtlcStaticRemoteKey _ + test("recv RevokeAndAck (one htlc sent, option_simple_taproot)", Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => + testRevokeAndAckHtlc(f, ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat) } test("recv RevocationTimeout") { f => @@ -1635,39 +1645,38 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice2bob.forward(bob) // actual test begins - awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteNextCommitInfo.isLeft) + awaitCond(alice.commitments.remoteNextCommitInfo.isLeft) val peer = TestProbe() - alice ! RevocationTimeout(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteCommitIndex, peer.ref) - peer.expectMsg(Peer.Disconnect(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.params.remoteParams.nodeId)) + alice ! RevocationTimeout(alice.commitments.remoteCommitIndex, peer.ref) + peer.expectMsg(Peer.Disconnect(alice.commitments.remoteNodeId)) } - private def testReceiveCmdFulfillHtlc(f: FixtureParam): Unit = { + private def testReceiveCmdFulfillHtlc(f: FixtureParam, commitmentFormat: CommitmentFormat): Unit = { import f._ + assert(alice.commitments.latest.commitmentFormat == commitmentFormat) + assert(bob.commitments.latest.commitmentFormat == commitmentFormat) + val (r, htlc) = addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) // actual test begins val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] - bob ! CMD_FULFILL_HTLC(htlc.id, r) + bob ! CMD_FULFILL_HTLC(htlc.id, r, None) val fulfill = bob2alice.expectMsgType[UpdateFulfillHtlc] awaitCond(bob.stateData == initialState.modify(_.commitments.changes.localChanges.proposed).using(_ :+ fulfill)) } - test("recv CMD_FULFILL_HTLC") { - testReceiveCmdFulfillHtlc _ + test("recv CMD_FULFILL_HTLC") { f => + testReceiveCmdFulfillHtlc(f, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) } - test("recv CMD_FULFILL_HTLC (static_remotekey)", Tag(ChannelStateTestsTags.StaticRemoteKey)) { - testReceiveCmdFulfillHtlc _ + test("recv CMD_FULFILL_HTLC (anchor_outputs_phoenix)", Tag(ChannelStateTestsTags.AnchorOutputsPhoenix)) { f => + testReceiveCmdFulfillHtlc(f, UnsafeLegacyAnchorOutputsCommitmentFormat) } - test("recv CMD_FULFILL_HTLC (anchor_outputs)", Tag(ChannelStateTestsTags.AnchorOutputs)) { - testReceiveCmdFulfillHtlc _ - } - - test("recv CMD_FULFILL_HTLC (anchors_zero_fee_htlc_tx)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { - testReceiveCmdFulfillHtlc _ + test("recv CMD_FULFILL_HTLC (option_simple_taproot)", Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => + testReceiveCmdFulfillHtlc(f, ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat) } test("recv CMD_FULFILL_HTLC (unknown htlc id)") { f => @@ -1676,7 +1685,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val r = randomBytes32() val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] - val c = CMD_FULFILL_HTLC(42, r, replyTo_opt = Some(sender.ref)) + val c = CMD_FULFILL_HTLC(42, r, None, replyTo_opt = Some(sender.ref)) bob ! c sender.expectMsg(RES_FAILURE(c, UnknownHtlcId(channelId(bob), 42))) assert(initialState == bob.stateData) @@ -1690,7 +1699,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // actual test begins val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] - val c = CMD_FULFILL_HTLC(htlc.id, ByteVector32.Zeroes, replyTo_opt = Some(sender.ref)) + val c = CMD_FULFILL_HTLC(htlc.id, ByteVector32.Zeroes, None, replyTo_opt = Some(sender.ref)) bob ! c sender.expectMsg(RES_FAILURE(c, InvalidHtlcPreimage(channelId(bob), 0))) assert(initialState == bob.stateData) @@ -1704,7 +1713,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // actual test begins val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] - val c = CMD_FULFILL_HTLC(htlc.id, r, replyTo_opt = Some(sender.ref)) + val c = CMD_FULFILL_HTLC(htlc.id, r, None, replyTo_opt = Some(sender.ref)) // this would be done automatically when the relayer calls safeSend bob.underlyingActor.nodeParams.db.pendingCommands.addSettlementCommand(initialState.channelId, c) bob ! c @@ -1719,17 +1728,21 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val sender = TestProbe() val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] - val c = CMD_FULFILL_HTLC(42, randomBytes32(), replyTo_opt = Some(sender.ref)) + val c = CMD_FULFILL_HTLC(42, randomBytes32(), None, replyTo_opt = Some(sender.ref)) sender.send(bob, c) // this will fail sender.expectMsg(RES_FAILURE(c, UnknownHtlcId(channelId(bob), 42))) awaitCond(bob.underlyingActor.nodeParams.db.pendingCommands.listSettlementCommands(initialState.channelId).isEmpty) } - private def testUpdateFulfillHtlc(f: FixtureParam): Unit = { + private def testUpdateFulfillHtlc(f: FixtureParam, commitmentFormat: CommitmentFormat): Unit = { import f._ + + assert(alice.commitments.latest.commitmentFormat == commitmentFormat) + assert(bob.commitments.latest.commitmentFormat == commitmentFormat) + val (r, htlc) = addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) - bob ! CMD_FULFILL_HTLC(htlc.id, r) + bob ! CMD_FULFILL_HTLC(htlc.id, r, None) val fulfill = bob2alice.expectMsgType[UpdateFulfillHtlc] // actual test begins @@ -1742,20 +1755,16 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(forward.htlc == htlc) } - test("recv UpdateFulfillHtlc") { - testUpdateFulfillHtlc _ - } - - test("recv UpdateFulfillHtlc (static_remotekey)", Tag(ChannelStateTestsTags.StaticRemoteKey)) { - testUpdateFulfillHtlc _ + test("recv UpdateFulfillHtlc") { f => + testUpdateFulfillHtlc(f, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) } - test("recv UpdateFulfillHtlc (anchor_outputs)", Tag(ChannelStateTestsTags.AnchorOutputs)) { - testUpdateFulfillHtlc _ + test("recv UpdateFulfillHtlc (anchor_outputs_phoenix)", Tag(ChannelStateTestsTags.AnchorOutputsPhoenix)) { f => + testUpdateFulfillHtlc(f, UnsafeLegacyAnchorOutputsCommitmentFormat) } - test("recv UpdateFulfillHtlc (anchors_zero_fee_htlc_tx)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { - testUpdateFulfillHtlc _ + test("recv UpdateFulfillHtlc (option_simple_taproot)", Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => + testUpdateFulfillHtlc(f, ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat) } test("recv UpdateFulfillHtlc (sender has not signed htlc)") { f => @@ -1765,28 +1774,30 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice2bob.expectMsgType[CommitSig] // actual test begins - val tx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val tx = alice.signCommitTx() alice ! UpdateFulfillHtlc(ByteVector32.Zeroes, htlc.id, r) alice2bob.expectMsgType[Error] awaitCond(alice.stateName == CLOSING) // channel should be advertised as down assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId == alice.stateData.asInstanceOf[DATA_CLOSING].channelId) - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) - alice2blockchain.expectMsgType[PublishTx] - alice2blockchain.expectMsgType[WatchTxConfirmed] + alice2blockchain.expectFinalTxPublished(tx.txid) + alice2blockchain.expectReplaceableTxPublished[ClaimLocalAnchorTx] + alice2blockchain.expectFinalTxPublished("local-main-delayed") + alice2blockchain.expectWatchTxConfirmed(tx.txid) } test("recv UpdateFulfillHtlc (unknown htlc id)") { f => import f._ - val tx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val tx = alice.signCommitTx() alice ! UpdateFulfillHtlc(ByteVector32.Zeroes, 42, ByteVector32.Zeroes) alice2bob.expectMsgType[Error] awaitCond(alice.stateName == CLOSING) // channel should be advertised as down assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId == alice.stateData.asInstanceOf[DATA_CLOSING].channelId) - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) - alice2blockchain.expectMsgType[PublishTx] - alice2blockchain.expectMsgType[WatchTxConfirmed] + alice2blockchain.expectFinalTxPublished(tx.txid) + alice2blockchain.expectReplaceableTxPublished[ClaimLocalAnchorTx] + alice2blockchain.expectFinalTxPublished("local-main-delayed") + alice2blockchain.expectWatchTxConfirmed(tx.txid) } test("recv UpdateFulfillHtlc (invalid preimage)") { f => @@ -1794,7 +1805,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val (_, htlc) = addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) bob2relayer.expectMsgType[RelayForward] - val tx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val tx = alice.signCommitTx() // actual test begins alice ! UpdateFulfillHtlc(ByteVector32.Zeroes, htlc.id, ByteVector32.Zeroes) @@ -1802,41 +1813,42 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with awaitCond(alice.stateName == CLOSING) // channel should be advertised as down assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId == alice.stateData.asInstanceOf[DATA_CLOSING].channelId) - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) - alice2blockchain.expectMsgType[PublishTx] // main delayed - alice2blockchain.expectMsgType[PublishTx] // htlc timeout - alice2blockchain.expectMsgType[WatchTxConfirmed] + alice2blockchain.expectFinalTxPublished(tx.txid) + alice2blockchain.expectReplaceableTxPublished[ClaimLocalAnchorTx] + alice2blockchain.expectFinalTxPublished("local-main-delayed") + alice2blockchain.expectReplaceableTxPublished[HtlcTimeoutTx] + alice2blockchain.expectWatchTxConfirmed(tx.txid) } - private def testCmdFailHtlc(f: FixtureParam): Unit = { + private def testCmdFailHtlc(f: FixtureParam, commitmentFormat: CommitmentFormat): Unit = { import f._ + + assert(alice.commitments.latest.commitmentFormat == commitmentFormat) + assert(bob.commitments.latest.commitmentFormat == commitmentFormat) + val (_, htlc) = addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) // actual test begins val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] - val cmd = CMD_FAIL_HTLC(htlc.id, FailureReason.LocalFailure(PermanentChannelFailure())) - val Right(fail) = OutgoingPaymentPacket.buildHtlcFailure(Bob.nodeParams.privateKey, cmd, htlc) + val cmd = CMD_FAIL_HTLC(htlc.id, FailureReason.LocalFailure(PermanentChannelFailure()), None) + val Right(fail) = OutgoingPaymentPacket.buildHtlcFailure(Bob.nodeParams.privateKey, useAttributableFailures = false, cmd, htlc) assert(fail.id == htlc.id) bob ! cmd bob2alice.expectMsg(fail) awaitCond(bob.stateData == initialState.modify(_.commitments.changes.localChanges.proposed).using(_ :+ fail)) } - test("recv CMD_FAIL_HTLC") { - testCmdFailHtlc _ - } - - test("recv CMD_FAIL_HTLC (static_remotekey)", Tag(ChannelStateTestsTags.StaticRemoteKey)) { - testCmdFailHtlc _ + test("recv CMD_FAIL_HTLC") { f => + testCmdFailHtlc(f, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) } - test("recv CMD_FAIL_HTLC (anchor_outputs)", Tag(ChannelStateTestsTags.AnchorOutputs)) { - testCmdFailHtlc _ + test("recv CMD_FAIL_HTLC (anchor_outputs_phoenix)", Tag(ChannelStateTestsTags.AnchorOutputsPhoenix)) { f => + testCmdFailHtlc(f, UnsafeLegacyAnchorOutputsCommitmentFormat) } - test("recv CMD_FAIL_HTLC (anchors_zero_fee_htlc_tx)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { - testCmdFailHtlc _ + test("recv CMD_FAIL_HTLC (option_simple_taproot)", Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => + testCmdFailHtlc(f, ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat) } test("recv CMD_FAIL_HTLC (with delay)") { f => @@ -1845,8 +1857,8 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with crossSign(alice, bob, alice2bob, bob2alice) val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] - val cmd = CMD_FAIL_HTLC(htlc.id, FailureReason.LocalFailure(PermanentChannelFailure()), delay_opt = Some(50 millis)) - val Right(fail) = OutgoingPaymentPacket.buildHtlcFailure(Bob.nodeParams.privateKey, cmd, htlc) + val cmd = CMD_FAIL_HTLC(htlc.id, FailureReason.LocalFailure(PermanentChannelFailure()), None, delay_opt = Some(50 millis)) + val Right(fail) = OutgoingPaymentPacket.buildHtlcFailure(Bob.nodeParams.privateKey, useAttributableFailures = false, cmd, htlc) assert(fail.id == htlc.id) bob ! cmd bob2alice.expectMsg(fail) @@ -1858,7 +1870,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val sender = TestProbe() val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] - val c = CMD_FAIL_HTLC(42, FailureReason.LocalFailure(PermanentChannelFailure()), replyTo_opt = Some(sender.ref)) + val c = CMD_FAIL_HTLC(42, FailureReason.LocalFailure(PermanentChannelFailure()), None, replyTo_opt = Some(sender.ref)) bob ! c sender.expectMsg(RES_FAILURE(c, UnknownHtlcId(channelId(bob), 42))) assert(initialState == bob.stateData) @@ -1872,13 +1884,13 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with crossSign(alice, bob, alice2bob, bob2alice) // HTLC is fulfilled but alice doesn't send its revocation. - bob ! CMD_FULFILL_HTLC(htlc.id, r) + bob ! CMD_FULFILL_HTLC(htlc.id, r, None) bob ! CMD_SIGN() bob2alice.expectMsgType[UpdateFulfillHtlc] bob2alice.expectMsgType[CommitSig] // We cannot fail the HTLC, we must wait for the fulfill to be acked. - val c = CMD_FAIL_HTLC(htlc.id, FailureReason.LocalFailure(TemporaryNodeFailure()), replyTo_opt = Some(sender.ref)) + val c = CMD_FAIL_HTLC(htlc.id, FailureReason.LocalFailure(TemporaryNodeFailure()), None, replyTo_opt = Some(sender.ref)) bob ! c sender.expectMsg(RES_FAILURE(c, UnknownHtlcId(channelId(bob), htlc.id))) } @@ -1888,7 +1900,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val sender = TestProbe() val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] - val c = CMD_FAIL_HTLC(42, FailureReason.LocalFailure(PermanentChannelFailure()), replyTo_opt = Some(sender.ref)) + val c = CMD_FAIL_HTLC(42, FailureReason.LocalFailure(PermanentChannelFailure()), None, replyTo_opt = Some(sender.ref)) sender.send(bob, c) // this will fail sender.expectMsg(RES_FAILURE(c, UnknownHtlcId(channelId(bob), 42))) awaitCond(bob.underlyingActor.nodeParams.db.pendingCommands.listSettlementCommands(initialState.channelId).isEmpty) @@ -1938,11 +1950,15 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with awaitCond(bob.underlyingActor.nodeParams.db.pendingCommands.listSettlementCommands(initialState.channelId).isEmpty) } - private def testUpdateFailHtlc(f: FixtureParam): Unit = { + private def testUpdateFailHtlc(f: FixtureParam, commitmentFormat: CommitmentFormat): Unit = { import f._ + + assert(alice.commitments.latest.commitmentFormat == commitmentFormat) + assert(bob.commitments.latest.commitmentFormat == commitmentFormat) + val (_, htlc) = addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) - bob ! CMD_FAIL_HTLC(htlc.id, FailureReason.LocalFailure(PermanentChannelFailure())) + bob ! CMD_FAIL_HTLC(htlc.id, FailureReason.LocalFailure(PermanentChannelFailure()), None) val fail = bob2alice.expectMsgType[UpdateFailHtlc] // actual test begins @@ -1953,20 +1969,16 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice2relayer.expectNoMessage() } - test("recv UpdateFailHtlc") { - testUpdateFailHtlc _ - } - - test("recv UpdateFailHtlc (static_remotekey)", Tag(ChannelStateTestsTags.StaticRemoteKey)) { - testUpdateFailHtlc _ + test("recv UpdateFailHtlc") { f => + testUpdateFailHtlc(f, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) } - test("recv UpdateFailHtlc (anchor_outputs)", Tag(ChannelStateTestsTags.AnchorOutputs)) { - testUpdateFailHtlc _ + test("recv UpdateFailHtlc (anchor_outputs_phoenix)", Tag(ChannelStateTestsTags.AnchorOutputsPhoenix)) { f => + testUpdateFailHtlc(f, UnsafeLegacyAnchorOutputsCommitmentFormat) } - test("recv UpdateFailHtlc (anchors_zero_fee_htlc_tx)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { - testUpdateFailHtlc _ + test("recv UpdateFailHtlc (option_simple_taproot)", Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => + testUpdateFailHtlc(f, ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat) } test("recv UpdateFailMalformedHtlc") { f => @@ -2001,16 +2013,17 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with crossSign(alice, bob, alice2bob, bob2alice) // actual test begins - val tx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val tx = alice.signCommitTx() val fail = UpdateFailMalformedHtlc(ByteVector32.Zeroes, htlc.id, Sphinx.hash(htlc.onionRoutingPacket), 42) alice ! fail val error = alice2bob.expectMsgType[Error] assert(new String(error.data.toArray) == InvalidFailureCode(ByteVector32.Zeroes).getMessage) awaitCond(alice.stateName == CLOSING) - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) // commit tx - alice2blockchain.expectMsgType[PublishTx] // main delayed - alice2blockchain.expectMsgType[PublishTx] // htlc timeout - alice2blockchain.expectMsgType[WatchTxConfirmed] + alice2blockchain.expectFinalTxPublished(tx.txid) + alice2blockchain.expectReplaceableTxPublished[ClaimLocalAnchorTx] + alice2blockchain.expectFinalTxPublished("local-main-delayed") + alice2blockchain.expectReplaceableTxPublished[HtlcTimeoutTx] + alice2blockchain.expectWatchTxConfirmed(tx.txid) } test("recv UpdateFailHtlc (sender has not signed htlc)") { f => @@ -2020,28 +2033,30 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice2bob.expectMsgType[CommitSig] // actual test begins - val tx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val tx = alice.signCommitTx() alice ! UpdateFailHtlc(ByteVector32.Zeroes, htlc.id, ByteVector.fill(152)(0)) alice2bob.expectMsgType[Error] awaitCond(alice.stateName == CLOSING) // channel should be advertised as down assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId == alice.stateData.asInstanceOf[DATA_CLOSING].channelId) - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) - alice2blockchain.expectMsgType[PublishTx] - alice2blockchain.expectMsgType[WatchTxConfirmed] + alice2blockchain.expectFinalTxPublished(tx.txid) + alice2blockchain.expectReplaceableTxPublished[ClaimLocalAnchorTx] + alice2blockchain.expectFinalTxPublished("local-main-delayed") + alice2blockchain.expectWatchTxConfirmed(tx.txid) } test("recv UpdateFailHtlc (unknown htlc id)") { f => import f._ - val tx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val tx = alice.signCommitTx() alice ! UpdateFailHtlc(ByteVector32.Zeroes, 42, ByteVector.fill(152)(0)) alice2bob.expectMsgType[Error] awaitCond(alice.stateName == CLOSING) // channel should be advertised as down assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId == alice.stateData.asInstanceOf[DATA_CLOSING].channelId) - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) - alice2blockchain.expectMsgType[PublishTx] - alice2blockchain.expectMsgType[WatchTxConfirmed] + alice2blockchain.expectFinalTxPublished(tx.txid) + alice2blockchain.expectReplaceableTxPublished[ClaimLocalAnchorTx] + alice2blockchain.expectFinalTxPublished("local-main-delayed") + alice2blockchain.expectWatchTxConfirmed(tx.txid) } test("recv UpdateFailHtlc (onion error bigger than recommended value)") { f => @@ -2049,124 +2064,35 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val (_, htlc) = addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) // Bob receives a failure with a completely invalid onion error (missing mac) - bob ! CMD_FAIL_HTLC(htlc.id, FailureReason.EncryptedDownstreamFailure(ByteVector.fill(561)(42))) + bob ! CMD_FAIL_HTLC(htlc.id, FailureReason.EncryptedDownstreamFailure(ByteVector.fill(561)(42), None), None) val fail = bob2alice.expectMsgType[UpdateFailHtlc] assert(fail.id == htlc.id) // We propagate failure upstream (hopefully the sender knows how to unwrap them). assert(fail.reason.length == 561) } - private def testCmdUpdateFee(f: FixtureParam): Unit = { + private def testCmdUpdateFee(f: FixtureParam, commitmentFormat: CommitmentFormat): Unit = { import f._ + + assert(alice.commitments.latest.commitmentFormat == commitmentFormat) + assert(bob.commitments.latest.commitmentFormat == commitmentFormat) + val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] alice ! CMD_UPDATE_FEE(FeeratePerKw(20000 sat)) val fee = alice2bob.expectMsgType[UpdateFee] awaitCond(alice.stateData == initialState.modify(_.commitments.changes.localChanges.proposed).using(_ :+ fee)) } - test("recv CMD_UPDATE_FEE") { - testCmdUpdateFee _ + test("recv CMD_UPDATE_FEE") { f => + testCmdUpdateFee(f, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) } - test("recv CMD_UPDATE_FEE (anchor outputs)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { - testCmdUpdateFee _ + test("recv CMD_UPDATE_FEE (anchor_output_phoenix)", Tag(ChannelStateTestsTags.AnchorOutputsPhoenix)) { f => + testCmdUpdateFee(f, UnsafeLegacyAnchorOutputsCommitmentFormat) } - test("recv CMD_UPDATE_FEE (over max dust htlc exposure)") { f => - import f._ - - // Alice sends HTLCs to Bob that are not included in the dust exposure at the current feerate: - addHtlc(13000.sat.toMilliSatoshi, alice, bob, alice2bob, bob2alice) - addHtlc(14000.sat.toMilliSatoshi, alice, bob, alice2bob, bob2alice) - crossSign(alice, bob, alice2bob, bob2alice) - val aliceCommitments = alice.stateData.asInstanceOf[DATA_NORMAL].commitments - assert(DustExposure.computeExposure(aliceCommitments.latest.localCommit.spec, aliceCommitments.params.localParams.dustLimit, aliceCommitments.params.commitmentFormat) == 0.msat) - assert(DustExposure.computeExposure(aliceCommitments.latest.remoteCommit.spec, aliceCommitments.params.remoteParams.dustLimit, aliceCommitments.params.commitmentFormat) == 0.msat) - - // A large feerate increase would make these HTLCs overflow alice's dust exposure, so she rejects it: - val sender = TestProbe() - val cmd = CMD_UPDATE_FEE(FeeratePerKw(20000 sat), replyTo_opt = Some(sender.ref)) - alice ! cmd - sender.expectMsg(RES_FAILURE(cmd, LocalDustHtlcExposureTooHigh(channelId(alice), 25000 sat, 27000000 msat))) - } - - test("recv CMD_UPDATE_FEE (over max dust htlc exposure with pending local changes)") { f => - import f._ - val sender = TestProbe() - assert(alice.underlyingActor.nodeParams.onChainFeeConf.feerateToleranceFor(bob.underlyingActor.nodeParams.nodeId).dustTolerance.maxExposure == 25_000.sat) - - // Alice sends an HTLC to Bob that is not included in the dust exposure at the current feerate. - // She signs them but Bob doesn't answer yet. - addHtlc(13000.sat.toMilliSatoshi, alice, bob, alice2bob, bob2alice) - alice ! CMD_SIGN(Some(sender.ref)) - sender.expectMsgType[RES_SUCCESS[CMD_SIGN]] - alice2bob.expectMsgType[CommitSig] - - // Alice sends another HTLC to Bob that is not included in the dust exposure at the current feerate. - addHtlc(14000.sat.toMilliSatoshi, alice, bob, alice2bob, bob2alice) - val aliceCommitments = alice.stateData.asInstanceOf[DATA_NORMAL].commitments - assert(DustExposure.computeExposure(aliceCommitments.latest.localCommit.spec, aliceCommitments.params.localParams.dustLimit, aliceCommitments.params.commitmentFormat) == 0.msat) - assert(DustExposure.computeExposure(aliceCommitments.latest.remoteCommit.spec, aliceCommitments.params.remoteParams.dustLimit, aliceCommitments.params.commitmentFormat) == 0.msat) - - // A large feerate increase would make these HTLCs overflow alice's dust exposure, so she rejects it: - val cmd = CMD_UPDATE_FEE(FeeratePerKw(20000 sat), replyTo_opt = Some(sender.ref)) - alice ! cmd - sender.expectMsg(RES_FAILURE(cmd, LocalDustHtlcExposureTooHigh(channelId(alice), 25000 sat, 27000000 msat))) - } - - def testCmdUpdateFeeDustOverflowSingleCommit(f: FixtureParam): Unit = { - import f._ - val sender = TestProbe() - // We start with a low feerate. - val initialFeerate = FeeratePerKw(500 sat) - alice.setBitcoinCoreFeerate(initialFeerate) - bob.setBitcoinCoreFeerate(initialFeerate) - updateFee(initialFeerate, alice, bob, alice2bob, bob2alice) - val aliceCommitments = alice.stateData.asInstanceOf[DATA_NORMAL].commitments - assert(alice.underlyingActor.nodeParams.onChainFeeConf.feerateToleranceFor(bob.underlyingActor.nodeParams.nodeId).dustTolerance.maxExposure == 25_000.sat) - val higherDustLimit = Seq(aliceCommitments.params.localParams.dustLimit, aliceCommitments.params.remoteParams.dustLimit).max - val lowerDustLimit = Seq(aliceCommitments.params.localParams.dustLimit, aliceCommitments.params.remoteParams.dustLimit).min - // We have the following dust thresholds at the current feerate - assert(Transactions.offeredHtlcTrimThreshold(higherDustLimit, aliceCommitments.latest.localCommit.spec.copy(commitTxFeerate = DustExposure.feerateForDustExposure(initialFeerate)), aliceCommitments.params.commitmentFormat) == 6989.sat) - assert(Transactions.receivedHtlcTrimThreshold(higherDustLimit, aliceCommitments.latest.localCommit.spec.copy(commitTxFeerate = DustExposure.feerateForDustExposure(initialFeerate)), aliceCommitments.params.commitmentFormat) == 7109.sat) - assert(Transactions.offeredHtlcTrimThreshold(lowerDustLimit, aliceCommitments.latest.localCommit.spec.copy(commitTxFeerate = DustExposure.feerateForDustExposure(initialFeerate)), aliceCommitments.params.commitmentFormat) == 2989.sat) - assert(Transactions.receivedHtlcTrimThreshold(lowerDustLimit, aliceCommitments.latest.localCommit.spec.copy(commitTxFeerate = DustExposure.feerateForDustExposure(initialFeerate)), aliceCommitments.params.commitmentFormat) == 3109.sat) - // And the following thresholds after the feerate update - // NB: we apply the real feerate when sending update_fee, not the one adjusted for dust - val updatedFeerate = FeeratePerKw(4000 sat) - assert(Transactions.offeredHtlcTrimThreshold(higherDustLimit, aliceCommitments.latest.localCommit.spec.copy(commitTxFeerate = updatedFeerate), aliceCommitments.params.commitmentFormat) == 7652.sat) - assert(Transactions.receivedHtlcTrimThreshold(higherDustLimit, aliceCommitments.latest.localCommit.spec.copy(commitTxFeerate = updatedFeerate), aliceCommitments.params.commitmentFormat) == 7812.sat) - assert(Transactions.offeredHtlcTrimThreshold(lowerDustLimit, aliceCommitments.latest.localCommit.spec.copy(commitTxFeerate = updatedFeerate), aliceCommitments.params.commitmentFormat) == 3652.sat) - assert(Transactions.receivedHtlcTrimThreshold(lowerDustLimit, aliceCommitments.latest.localCommit.spec.copy(commitTxFeerate = updatedFeerate), aliceCommitments.params.commitmentFormat) == 3812.sat) - - // Alice send HTLCs to Bob that are not included in the dust exposure at the current feerate. - // She signs them but Bob doesn't answer yet. - (1 to 2).foreach(_ => addHtlc(7400.sat.toMilliSatoshi, alice, bob, alice2bob, bob2alice)) - alice ! CMD_SIGN(Some(sender.ref)) - sender.expectMsgType[RES_SUCCESS[CMD_SIGN]] - alice2bob.expectMsgType[CommitSig] - - // Alice sends other HTLCs to Bob that are not included in the dust exposure at the current feerate, without signing them. - (1 to 2).foreach(_ => addHtlc(7400.sat.toMilliSatoshi, alice, bob, alice2bob, bob2alice)) - - // A feerate increase makes these HTLCs become dust in one of the commitments but not the other. - val cmd = CMD_UPDATE_FEE(updatedFeerate, replyTo_opt = Some(sender.ref)) - alice.setBitcoinCoreFeerate(updatedFeerate) - bob.setBitcoinCoreFeerate(updatedFeerate) - alice ! cmd - if (higherDustLimit == aliceCommitments.params.localParams.dustLimit) { - sender.expectMsg(RES_FAILURE(cmd, LocalDustHtlcExposureTooHigh(channelId(alice), 25000 sat, 29600000 msat))) - } else { - sender.expectMsg(RES_FAILURE(cmd, RemoteDustHtlcExposureTooHigh(channelId(alice), 25000 sat, 29600000 msat))) - } - } - - test("recv CMD_UPDATE_FEE (over max dust htlc exposure in local commit only with pending local changes)", Tag(ChannelStateTestsTags.HighDustLimitDifferenceAliceBob)) { f => - testCmdUpdateFeeDustOverflowSingleCommit(f) - } - - test("recv CMD_UPDATE_FEE (over max dust htlc exposure in remote commit only with pending local changes)", Tag(ChannelStateTestsTags.HighDustLimitDifferenceBobAlice)) { f => - testCmdUpdateFeeDustOverflowSingleCommit(f) + test("recv CMD_UPDATE_FEE (simple taproot channel)", Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => + testCmdUpdateFee(f, ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat) } test("recv CMD_UPDATE_FEE (two in a row)") { f => @@ -2190,16 +2116,6 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with } test("recv UpdateFee") { f => - import f._ - val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] - val fee = UpdateFee(ByteVector32.Zeroes, FeeratePerKw(12000 sat)) - bob ! fee - awaitCond(bob.stateData == initialState - .modify(_.commitments.changes.remoteChanges.proposed).using(_ :+ fee) - .modify(_.commitments.changes.remoteNextHtlcId).setTo(0)) - } - - test("recv UpdateFee (anchor outputs)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => import f._ val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] assert(initialState.commitments.latest.localCommit.spec.commitTxFeerate == TestConstants.anchorOutputsFeeratePerKw) @@ -2213,9 +2129,9 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with test("recv UpdateFee (two in a row)") { f => import f._ val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] - val fee1 = UpdateFee(ByteVector32.Zeroes, FeeratePerKw(12000 sat)) + val fee1 = UpdateFee(ByteVector32.Zeroes, TestConstants.anchorOutputsFeeratePerKw * 0.8) bob ! fee1 - val fee2 = UpdateFee(ByteVector32.Zeroes, FeeratePerKw(14000 sat)) + val fee2 = UpdateFee(ByteVector32.Zeroes, TestConstants.anchorOutputsFeeratePerKw * 1.2) bob ! fee2 awaitCond(bob.stateData == initialState .modify(_.commitments.changes.remoteChanges.proposed).using(_ :+ fee2) @@ -2224,37 +2140,21 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with test("recv UpdateFee (when sender is not funder)") { f => import f._ - val tx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx - alice ! UpdateFee(ByteVector32.Zeroes, FeeratePerKw(12000 sat)) + val tx = alice.signCommitTx() + alice ! UpdateFee(ByteVector32.Zeroes, TestConstants.anchorOutputsFeeratePerKw * 1.2) alice2bob.expectMsgType[Error] awaitCond(alice.stateName == CLOSING) // channel should be advertised as down assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId == alice.stateData.asInstanceOf[DATA_CLOSING].channelId) - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) - alice2blockchain.expectMsgType[PublishTx] - alice2blockchain.expectMsgType[WatchTxConfirmed] - } - - test("recv UpdateFee (sender can't afford it)") { f => - import f._ - val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx - val fee = UpdateFee(ByteVector32.Zeroes, FeeratePerKw(100000000 sat)) - // we first update the feerates so that we don't trigger a 'fee too different' error - bob.setBitcoinCoreFeerate(fee.feeratePerKw) - bob ! fee - val error = bob2alice.expectMsgType[Error] - assert(new String(error.data.toArray) == CannotAffordFees(channelId(bob), missing = 71620000L sat, reserve = 20000L sat, fees = 72400000L sat).getMessage) - awaitCond(bob.stateName == CLOSING) - // channel should be advertised as down - assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId == bob.stateData.asInstanceOf[DATA_CLOSING].channelId) - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) // commit tx - //bob2blockchain.expectMsgType[PublishTx] // main delayed (removed because of the high fees) - bob2blockchain.expectMsgType[WatchTxConfirmed] + alice2blockchain.expectFinalTxPublished(tx.txid) + alice2blockchain.expectReplaceableTxPublished[ClaimLocalAnchorTx] + alice2blockchain.expectFinalTxPublished("local-main-delayed") + alice2blockchain.expectWatchTxConfirmed(tx.txid) } - test("recv UpdateFee (sender can't afford it, anchor outputs)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), Tag(ChannelStateTestsTags.HighFeerateMismatchTolerance)) { f => + test("recv UpdateFee (sender can't afford it)", Tag(ChannelStateTestsTags.HighFeerateMismatchTolerance)) { f => import f._ - val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val tx = bob.signCommitTx() // This feerate is just above the threshold: (800000 (alice balance) - 20000 (reserve) - 660 (anchors)) / 1124 (commit tx weight) = 693363 bob ! UpdateFee(ByteVector32.Zeroes, FeeratePerKw(693364 sat)) val error = bob2alice.expectMsgType[Error] @@ -2265,28 +2165,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) // commit tx } - test("recv UpdateFee (local/remote feerates are too different)") { f => - import f._ - - val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] - val commitTx = initialState.commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx - assert(initialState.commitments.latest.localCommit.spec.commitTxFeerate == TestConstants.feeratePerKw) - alice2bob.send(bob, UpdateFee(ByteVector32.Zeroes, TestConstants.feeratePerKw / 2)) - bob2alice.expectNoMessage(250 millis) // we don't close because the commitment doesn't contain any HTLC - - // when we try to add an HTLC, we still disagree on the feerate so we close - alice2bob.send(bob, UpdateAddHtlc(ByteVector32.Zeroes, 0, 2500000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, 1.0, None)) - val error = bob2alice.expectMsgType[Error] - assert(new String(error.data.toArray).contains("local/remote feerates are too different")) - awaitCond(bob.stateName == CLOSING) - // channel should be advertised as down - assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId == bob.stateData.asInstanceOf[DATA_CLOSING].channelId) - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == commitTx.txid) - bob2blockchain.expectMsgType[PublishTx] - bob2blockchain.expectMsgType[WatchTxConfirmed] - } - - test("recv UpdateFee (remote feerate is too high, anchor outputs)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + test("recv UpdateFee (remote feerate is too high)") { f => import f._ val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] @@ -2298,14 +2177,14 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with awaitCond(bob.stateName == CLOSING) } - test("recv UpdateFee (remote feerate is too small, anchor outputs)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + test("recv UpdateFee (remote feerate is too small)") { f => import f._ val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] assert(initialState.commitments.latest.localCommit.spec.commitTxFeerate == TestConstants.anchorOutputsFeeratePerKw) - val add = UpdateAddHtlc(ByteVector32.Zeroes, 0, 2500000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, 1.0, None) + val add = UpdateAddHtlc(ByteVector32.Zeroes, 0, 2500000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, Reputation.maxEndorsement, None) alice2bob.send(bob, add) - val fee = UpdateFee(initialState.channelId, FeeratePerKw(FeeratePerByte(2 sat))) + val fee = UpdateFee(initialState.channelId, FeeratePerByte(2 sat).perKw) alice2bob.send(bob, fee) awaitCond(bob.stateData == initialState .modify(_.commitments.changes.remoteChanges.proposed).using(_ :+ add :+ fee) @@ -2313,129 +2192,6 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with bob2alice.expectNoMessage(250 millis) // we don't close because we're using anchor outputs } - test("recv UpdateFee (remote feerate is too small)") { f => - import f._ - val bobCommitments = bob.stateData.asInstanceOf[DATA_NORMAL].commitments - val tx = bobCommitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx - val expectedFeeratePerKw = bob.underlyingActor.nodeParams.onChainFeeConf.getCommitmentFeerate(bob.underlyingActor.nodeParams.currentBitcoinCoreFeerates, bob.underlyingActor.remoteNodeId, bobCommitments.params.commitmentFormat, bobCommitments.latest.capacity) - assert(bobCommitments.latest.localCommit.spec.commitTxFeerate == expectedFeeratePerKw) - bob ! UpdateFee(ByteVector32.Zeroes, FeeratePerKw(252 sat)) - val error = bob2alice.expectMsgType[Error] - assert(new String(error.data.toArray) == "remote fee rate is too small: remoteFeeratePerKw=252") - awaitCond(bob.stateName == CLOSING) - // channel should be advertised as down - assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId == bob.stateData.asInstanceOf[DATA_CLOSING].channelId) - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) - bob2blockchain.expectMsgType[PublishTx] - bob2blockchain.expectMsgType[WatchTxConfirmed] - } - - test("recv UpdateFee (over max dust htlc exposure)") { f => - import f._ - - // Alice sends HTLCs to Bob that are not included in the dust exposure at the current feerate: - addHtlc(13000.sat.toMilliSatoshi, alice, bob, alice2bob, bob2alice) - addHtlc(13500.sat.toMilliSatoshi, alice, bob, alice2bob, bob2alice) - addHtlc(14000.sat.toMilliSatoshi, alice, bob, alice2bob, bob2alice) - crossSign(alice, bob, alice2bob, bob2alice) - val bobCommitments = bob.stateData.asInstanceOf[DATA_NORMAL].commitments - assert(DustExposure.computeExposure(bobCommitments.latest.localCommit.spec, bobCommitments.params.localParams.dustLimit, bobCommitments.params.commitmentFormat) == 0.msat) - assert(DustExposure.computeExposure(bobCommitments.latest.remoteCommit.spec, bobCommitments.params.remoteParams.dustLimit, bobCommitments.params.commitmentFormat) == 0.msat) - val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx - - // A large feerate increase would make these HTLCs overflow Bob's dust exposure, so he force-closes: - bob.setBitcoinCoreFeerate(FeeratePerKw(20000 sat)) - bob ! UpdateFee(channelId(bob), FeeratePerKw(20000 sat)) - val error = bob2alice.expectMsgType[Error] - assert(new String(error.data.toArray) == LocalDustHtlcExposureTooHigh(channelId(bob), 30000 sat, 40500000 msat).getMessage) - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) - awaitCond(bob.stateName == CLOSING) - } - - test("recv UpdateFee (over max dust htlc exposure with pending local changes)") { f => - import f._ - val sender = TestProbe() - assert(bob.underlyingActor.nodeParams.onChainFeeConf.feerateToleranceFor(alice.underlyingActor.nodeParams.nodeId).dustTolerance.maxExposure == 30_000.sat) - - // Bob sends HTLCs to Alice that are not included in the dust exposure at the current feerate. - // He signs them but Alice doesn't answer yet. - addHtlc(13000.sat.toMilliSatoshi, bob, alice, bob2alice, alice2bob) - addHtlc(13500.sat.toMilliSatoshi, bob, alice, bob2alice, alice2bob) - bob ! CMD_SIGN(Some(sender.ref)) - sender.expectMsgType[RES_SUCCESS[CMD_SIGN]] - bob2alice.expectMsgType[CommitSig] - - // Bob sends another HTLC to Alice that is not included in the dust exposure at the current feerate. - addHtlc(14000.sat.toMilliSatoshi, bob, alice, bob2alice, alice2bob) - val bobCommitments = bob.stateData.asInstanceOf[DATA_NORMAL].commitments - assert(DustExposure.computeExposure(bobCommitments.latest.localCommit.spec, bobCommitments.params.localParams.dustLimit, bobCommitments.params.commitmentFormat) == 0.msat) - assert(DustExposure.computeExposure(bobCommitments.latest.remoteCommit.spec, bobCommitments.params.remoteParams.dustLimit, bobCommitments.params.commitmentFormat) == 0.msat) - - // A large feerate increase would make these HTLCs overflow Bob's dust exposure, so he force-close: - val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx - bob.setBitcoinCoreFeerate(FeeratePerKw(20000 sat)) - bob ! UpdateFee(channelId(bob), FeeratePerKw(20000 sat)) - val error = bob2alice.expectMsgType[Error] - assert(new String(error.data.toArray) == LocalDustHtlcExposureTooHigh(channelId(bob), 30000 sat, 40500000 msat).getMessage) - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) - awaitCond(bob.stateName == CLOSING) - } - - def testUpdateFeeDustOverflowSingleCommit(f: FixtureParam): Unit = { - import f._ - val sender = TestProbe() - // We start with a low feerate. - val initialFeerate = FeeratePerKw(500 sat) - alice.setBitcoinCoreFeerate(initialFeerate) - bob.setBitcoinCoreFeerate(initialFeerate) - updateFee(initialFeerate, alice, bob, alice2bob, bob2alice) - val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] - val aliceCommitments = initialState.commitments - assert(alice.underlyingActor.nodeParams.onChainFeeConf.feerateToleranceFor(bob.underlyingActor.nodeParams.nodeId).dustTolerance.maxExposure == 25_000.sat) - val higherDustLimit = Seq(aliceCommitments.params.localParams.dustLimit, aliceCommitments.params.remoteParams.dustLimit).max - val lowerDustLimit = Seq(aliceCommitments.params.localParams.dustLimit, aliceCommitments.params.remoteParams.dustLimit).min - // We have the following dust thresholds at the current feerate - assert(Transactions.offeredHtlcTrimThreshold(higherDustLimit, aliceCommitments.latest.localCommit.spec.copy(commitTxFeerate = DustExposure.feerateForDustExposure(initialFeerate)), aliceCommitments.params.commitmentFormat) == 6989.sat) - assert(Transactions.receivedHtlcTrimThreshold(higherDustLimit, aliceCommitments.latest.localCommit.spec.copy(commitTxFeerate = DustExposure.feerateForDustExposure(initialFeerate)), aliceCommitments.params.commitmentFormat) == 7109.sat) - assert(Transactions.offeredHtlcTrimThreshold(lowerDustLimit, aliceCommitments.latest.localCommit.spec.copy(commitTxFeerate = DustExposure.feerateForDustExposure(initialFeerate)), aliceCommitments.params.commitmentFormat) == 2989.sat) - assert(Transactions.receivedHtlcTrimThreshold(lowerDustLimit, aliceCommitments.latest.localCommit.spec.copy(commitTxFeerate = DustExposure.feerateForDustExposure(initialFeerate)), aliceCommitments.params.commitmentFormat) == 3109.sat) - // And the following thresholds after the feerate update - // NB: we apply the real feerate when sending update_fee, not the one adjusted for dust - val updatedFeerate = FeeratePerKw(4000 sat) - assert(Transactions.offeredHtlcTrimThreshold(higherDustLimit, aliceCommitments.latest.localCommit.spec.copy(commitTxFeerate = updatedFeerate), aliceCommitments.params.commitmentFormat) == 7652.sat) - assert(Transactions.receivedHtlcTrimThreshold(higherDustLimit, aliceCommitments.latest.localCommit.spec.copy(commitTxFeerate = updatedFeerate), aliceCommitments.params.commitmentFormat) == 7812.sat) - assert(Transactions.offeredHtlcTrimThreshold(lowerDustLimit, aliceCommitments.latest.localCommit.spec.copy(commitTxFeerate = updatedFeerate), aliceCommitments.params.commitmentFormat) == 3652.sat) - assert(Transactions.receivedHtlcTrimThreshold(lowerDustLimit, aliceCommitments.latest.localCommit.spec.copy(commitTxFeerate = updatedFeerate), aliceCommitments.params.commitmentFormat) == 3812.sat) - - // Bob send HTLCs to Alice that are not included in the dust exposure at the current feerate. - // He signs them but Alice doesn't answer yet. - (1 to 3).foreach(_ => addHtlc(7400.sat.toMilliSatoshi, bob, alice, bob2alice, alice2bob)) - bob ! CMD_SIGN(Some(sender.ref)) - sender.expectMsgType[RES_SUCCESS[CMD_SIGN]] - bob2alice.expectMsgType[CommitSig] - - // Bob sends other HTLCs to Alice that are not included in the dust exposure at the current feerate, without signing them. - (1 to 2).foreach(_ => addHtlc(7400.sat.toMilliSatoshi, bob, alice, bob2alice, alice2bob)) - - // A feerate increase makes these HTLCs become dust in one of the commitments but not the other. - val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx - bob.setBitcoinCoreFeerate(updatedFeerate) - bob ! UpdateFee(channelId(bob), updatedFeerate) - val error = bob2alice.expectMsgType[Error] - // NB: we don't need to distinguish local and remote, the error message is exactly the same. - assert(new String(error.data.toArray) == LocalDustHtlcExposureTooHigh(channelId(bob), 30000 sat, 37000000 msat).getMessage) - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) - awaitCond(bob.stateName == CLOSING) - } - - test("recv UpdateFee (over max dust htlc exposure in local commit only with pending local changes)", Tag(ChannelStateTestsTags.HighDustLimitDifferenceBobAlice)) { f => - testUpdateFeeDustOverflowSingleCommit(f) - } - - test("recv UpdateFee (over max dust htlc exposure in remote commit only with pending local changes)", Tag(ChannelStateTestsTags.HighDustLimitDifferenceAliceBob)) { f => - testUpdateFeeDustOverflowSingleCommit(f) - } - test("recv CMD_UPDATE_RELAY_FEE ") { f => import f._ val sender = TestProbe() @@ -2447,7 +2203,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val localUpdate = channelUpdateListener.expectMsgType[LocalChannelUpdate] assert(localUpdate.channelUpdate.feeBaseMsat == newFeeBaseMsat) assert(localUpdate.channelUpdate.feeProportionalMillionths == newFeeProportionalMillionth) - alice2relayer.expectNoMessage(1 seconds) + alice2relayer.expectNoMessage(100 millis) } def testCmdClose(f: FixtureParam, script_opt: Option[ByteVector]): Unit = { @@ -2466,11 +2222,11 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with testCmdClose(f, None) } - test("recv CMD_CLOSE (no pending htlcs) (anchor outputs)", Tag(ChannelStateTestsTags.AnchorOutputs)) { f => + test("recv CMD_CLOSE (no pending htlcs) (anchor outputs phoenix)", Tag(ChannelStateTestsTags.AnchorOutputsPhoenix)) { f => testCmdClose(f, None) } - test("recv CMD_CLOSE (no pending htlcs) (anchor outputs zero fee htlc txs)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + test("recv CMD_CLOSE (no pending htlcs) (simple taproot channel)", Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => testCmdClose(f, None) } @@ -2512,14 +2268,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with sender.expectMsgType[RES_FAILURE[CMD_CLOSE, InvalidFinalScript]] } - test("recv CMD_CLOSE (with unsupported native segwit script)") { f => - import f._ - val sender = TestProbe() - alice ! CMD_CLOSE(sender.ref, Some(hex"51050102030405"), None) - sender.expectMsgType[RES_FAILURE[CMD_CLOSE, InvalidFinalScript]] - } - - test("recv CMD_CLOSE (with native segwit script)", Tag(ChannelStateTestsTags.ShutdownAnySegwit)) { f => + test("recv CMD_CLOSE (with native segwit script)") { f => testCmdClose(f, Some(hex"51050102030405")) } @@ -2589,11 +2338,11 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with test("recv CMD_CLOSE (with a script that does match our upfront shutdown script)", Tag(ChannelStateTestsTags.UpfrontShutdownScript)) { f => import f._ val sender = TestProbe() - val shutdownScript = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.params.localParams.upfrontShutdownScript_opt.get + val shutdownScript = alice.commitments.localChannelParams.upfrontShutdownScript_opt.get alice ! CMD_CLOSE(sender.ref, Some(shutdownScript), None) sender.expectMsgType[RES_SUCCESS[CMD_CLOSE]] val shutdown = alice2bob.expectMsgType[Shutdown] - assert(shutdown.scriptPubKey == alice.stateData.asInstanceOf[DATA_NORMAL].commitments.params.localParams.upfrontShutdownScript_opt.get) + assert(shutdown.scriptPubKey == alice.commitments.localChannelParams.upfrontShutdownScript_opt.get) awaitCond(alice.stateName == NORMAL) awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].localShutdown.isDefined) } @@ -2604,7 +2353,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice ! CMD_CLOSE(sender.ref, None, None) sender.expectMsgType[RES_SUCCESS[CMD_CLOSE]] val shutdown = alice2bob.expectMsgType[Shutdown] - assert(shutdown.scriptPubKey == alice.stateData.asInstanceOf[DATA_NORMAL].commitments.params.localParams.upfrontShutdownScript_opt.get) + assert(shutdown.scriptPubKey == alice.commitments.localChannelParams.upfrontShutdownScript_opt.get) awaitCond(alice.stateName == NORMAL) awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].localShutdown.isDefined) } @@ -2639,11 +2388,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with testShutdown(f, None) } - test("recv Shutdown (no pending htlcs) (anchor outputs)", Tag(ChannelStateTestsTags.AnchorOutputs)) { f => - testShutdown(f, None) - } - - test("recv Shutdown (no pending htlcs) (anchor outputs zero fee htlc txs)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + test("recv Shutdown (no pending htlcs) (anchor outputs phoenix)", Tag(ChannelStateTestsTags.AnchorOutputsPhoenix)) { f => testShutdown(f, None) } @@ -2675,16 +2420,17 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val aliceData = alice.stateData.asInstanceOf[DATA_NORMAL] bob ! Shutdown(ByteVector32.Zeroes, alice.underlyingActor.getOrGenerateFinalScriptPubKey(aliceData)) bob2alice.expectMsgType[Error] - bob2blockchain.expectMsgType[PublishTx] - bob2blockchain.expectMsgType[PublishTx] - bob2blockchain.expectMsgType[WatchTxConfirmed] + val commitTx = bob2blockchain.expectFinalTxPublished("commit-tx") + bob2blockchain.expectReplaceableTxPublished[ClaimLocalAnchorTx] + bob2blockchain.expectFinalTxPublished("local-main-delayed") + bob2blockchain.expectWatchTxConfirmed(commitTx.tx.txid) awaitCond(bob.stateName == CLOSING) } test("recv Shutdown (with unsigned fee update)") { f => import f._ val sender = TestProbe() - alice ! CMD_UPDATE_FEE(FeeratePerKw(10_000 sat), commit = true) + alice ! CMD_UPDATE_FEE(FeeratePerKw(3_000 sat), commit = true) alice2bob.expectMsgType[UpdateFee] alice2bob.forward(bob) val sig = alice2bob.expectMsgType[CommitSig] @@ -2715,23 +2461,12 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with bob2alice.expectMsgType[Warning] // we should fail the connection as per the BOLTs bobPeer.fishForMessage(3 seconds) { - case Peer.Disconnect(nodeId, _) if nodeId == bob.stateData.asInstanceOf[DATA_NORMAL].commitments.params.remoteParams.nodeId => true + case Peer.Disconnect(nodeId, _) if nodeId == bob.commitments.remoteNodeId => true case _ => false } } - test("recv Shutdown (with unsupported native segwit script)") { f => - import f._ - bob ! Shutdown(ByteVector32.Zeroes, hex"51050102030405") - bob2alice.expectMsgType[Warning] - // we should fail the connection as per the BOLTs - bobPeer.fishForMessage(3 seconds) { - case Peer.Disconnect(nodeId, _) if nodeId == bob.stateData.asInstanceOf[DATA_NORMAL].commitments.params.remoteParams.nodeId => true - case _ => false - } - } - - test("recv Shutdown (with native segwit script)", Tag(ChannelStateTestsTags.ShutdownAnySegwit)) { f => + test("recv Shutdown (with native segwit script)") { f => testShutdown(f, Some(hex"51050102030405")) } @@ -2746,7 +2481,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with bob ! Shutdown(ByteVector32.Zeroes, hex"00112233445566778899") // we should fail the connection as per the BOLTs bobPeer.fishForMessage(3 seconds) { - case Peer.Disconnect(nodeId, _) if nodeId == bob.stateData.asInstanceOf[DATA_NORMAL].commitments.params.remoteParams.nodeId => true + case Peer.Disconnect(nodeId, _) if nodeId == bob.commitments.remoteNodeId => true case _ => false } } @@ -2757,7 +2492,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // we should fail the connection as per the BOLTs bobPeer.fishForMessage(3 seconds) { - case Peer.Disconnect(nodeId, _) if nodeId == bob.stateData.asInstanceOf[DATA_NORMAL].commitments.params.remoteParams.nodeId => true + case Peer.Disconnect(nodeId, _) if nodeId == bob.commitments.remoteNodeId => true case _ => false } } @@ -2774,16 +2509,12 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with awaitCond(bob.stateName == SHUTDOWN) } - test("recv Shutdown (with signed htlcs)") { - testShutdownWithHtlcs _ + test("recv Shutdown (with signed htlcs)") { f => + testShutdownWithHtlcs(f) } - test("recv Shutdown (with signed htlcs) (anchor outputs)", Tag(ChannelStateTestsTags.AnchorOutputs)) { - testShutdownWithHtlcs _ - } - - test("recv Shutdown (with signed htlcs) (anchor outputs zero fee htlc txs)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { - testShutdownWithHtlcs _ + test("recv Shutdown (with signed htlcs) (anchor outputs phoenix)", Tag(ChannelStateTestsTags.AnchorOutputsPhoenix)) { f => + testShutdownWithHtlcs(f) } test("recv Shutdown (while waiting for a RevokeAndAck)") { f => @@ -2858,13 +2589,13 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with crossSign(alice, bob, alice2bob, bob2alice) // actual test begins - val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] - val aliceCommitTx = initialState.commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val aliceCommitTx = alice.signCommitTx() alice ! CurrentBlockHeight(BlockHeight(400145)) - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == aliceCommitTx.txid) - alice2blockchain.expectMsgType[PublishTx] // main delayed - alice2blockchain.expectMsgType[PublishTx] // htlc timeout - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == aliceCommitTx.txid) + alice2blockchain.expectFinalTxPublished(aliceCommitTx.txid) + alice2blockchain.expectReplaceableTxPublished[ClaimLocalAnchorTx] + alice2blockchain.expectFinalTxPublished("local-main-delayed") + alice2blockchain.expectReplaceableTxPublished[HtlcTimeoutTx] + alice2blockchain.expectWatchTxConfirmed(aliceCommitTx.txid) channelUpdateListener.expectMsgType[LocalChannelDown] } @@ -2881,11 +2612,11 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // * Alice does not react to the fulfill (drops the message for some reason) // * When the HTLC timeout on Alice side is near, Bob needs to close the channel to avoid an on-chain race // condition between his HTLC-success and Alice's HTLC-timeout - val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] - val initialCommitTx = initialState.commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx - val HtlcSuccessTx(_, htlcSuccessTx, _, _, _) = initialState.commitments.latest.localCommit.htlcTxsAndRemoteSigs.head.htlcTx + val commitTx = bob.signCommitTx() + val htlcSuccessTx = bob.htlcTxs().head + assert(htlcSuccessTx.isInstanceOf[UnsignedHtlcSuccessTx]) - bob ! CMD_FULFILL_HTLC(htlc.id, r, commit = true) + bob ! CMD_FULFILL_HTLC(htlc.id, r, None, commit = true) bob2alice.expectMsgType[UpdateFulfillHtlc] bob2alice.expectMsgType[CommitSig] bob ! CurrentBlockHeight(htlc.cltvExpiry.blockHeight - Bob.nodeParams.channelConf.fulfillSafetyBeforeTimeout.toInt) @@ -2894,12 +2625,13 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(isFatal) assert(err.isInstanceOf[HtlcsWillTimeoutUpstream]) - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == initialCommitTx.txid) - bob2blockchain.expectMsgType[PublishTx] // main delayed - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txOut == htlcSuccessTx.txOut) - assert(bob2blockchain.expectMsgType[WatchTxConfirmed].txId == initialCommitTx.txid) + bob2blockchain.expectFinalTxPublished(commitTx.txid) + bob2blockchain.expectReplaceableTxPublished[ClaimLocalAnchorTx] + bob2blockchain.expectFinalTxPublished("local-main-delayed") + assert(bob2blockchain.expectReplaceableTxPublished[HtlcSuccessTx].input.outPoint == htlcSuccessTx.input.outPoint) + bob2blockchain.expectWatchTxConfirmed(commitTx.txid) channelUpdateListener.expectMsgType[LocalChannelDown] - alice2blockchain.expectNoMessage(500 millis) + alice2blockchain.expectNoMessage(100 millis) } test("recv CurrentBlockCount (fulfilled proposed htlc ignored by upstream peer)") { f => @@ -2915,25 +2647,26 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // * Alice does not react to the fulfill (drops the message for some reason) // * When the HTLC timeout on Alice side is near, Bob needs to close the channel to avoid an on-chain race // condition between his HTLC-success and Alice's HTLC-timeout - val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] - val initialCommitTx = initialState.commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx - val HtlcSuccessTx(_, htlcSuccessTx, _, _, _) = initialState.commitments.latest.localCommit.htlcTxsAndRemoteSigs.head.htlcTx + val commitTx = bob.signCommitTx() + val htlcSuccessTx = bob.htlcTxs().head + assert(htlcSuccessTx.isInstanceOf[UnsignedHtlcSuccessTx]) - bob ! CMD_FULFILL_HTLC(htlc.id, r, commit = false) + bob ! CMD_FULFILL_HTLC(htlc.id, r, None, commit = false) bob2alice.expectMsgType[UpdateFulfillHtlc] - bob2alice.expectNoMessage(500 millis) + bob2alice.expectNoMessage(100 millis) bob ! CurrentBlockHeight(htlc.cltvExpiry.blockHeight - Bob.nodeParams.channelConf.fulfillSafetyBeforeTimeout.toInt) val ChannelErrorOccurred(_, _, _, LocalError(err), isFatal) = listener.expectMsgType[ChannelErrorOccurred] assert(isFatal) assert(err.isInstanceOf[HtlcsWillTimeoutUpstream]) - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == initialCommitTx.txid) - bob2blockchain.expectMsgType[PublishTx] // main delayed - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txOut == htlcSuccessTx.txOut) - assert(bob2blockchain.expectMsgType[WatchTxConfirmed].txId == initialCommitTx.txid) + bob2blockchain.expectFinalTxPublished(commitTx.txid) + bob2blockchain.expectReplaceableTxPublished[ClaimLocalAnchorTx] + bob2blockchain.expectFinalTxPublished("local-main-delayed") + assert(bob2blockchain.expectReplaceableTxPublished[HtlcSuccessTx].input.outPoint == htlcSuccessTx.input.outPoint) + bob2blockchain.expectWatchTxConfirmed(commitTx.txid) channelUpdateListener.expectMsgType[LocalChannelDown] - alice2blockchain.expectNoMessage(500 millis) + alice2blockchain.expectNoMessage(100 millis) } test("recv CurrentBlockCount (fulfilled proposed htlc acked but not committed by upstream peer)") { f => @@ -2949,11 +2682,11 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // * Alice acks but doesn't commit // * When the HTLC timeout on Alice side is near, Bob needs to close the channel to avoid an on-chain race // condition between his HTLC-success and Alice's HTLC-timeout - val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] - val initialCommitTx = initialState.commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx - val HtlcSuccessTx(_, htlcSuccessTx, _, _, _) = initialState.commitments.latest.localCommit.htlcTxsAndRemoteSigs.head.htlcTx + val commitTx = bob.signCommitTx() + val htlcSuccessTx = bob.htlcTxs().head + assert(htlcSuccessTx.isInstanceOf[UnsignedHtlcSuccessTx]) - bob ! CMD_FULFILL_HTLC(htlc.id, r, commit = true) + bob ! CMD_FULFILL_HTLC(htlc.id, r, None, commit = true) bob2alice.expectMsgType[UpdateFulfillHtlc] bob2alice.forward(alice) bob2alice.expectMsgType[CommitSig] @@ -2966,24 +2699,16 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(isFatal) assert(err.isInstanceOf[HtlcsWillTimeoutUpstream]) - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == initialCommitTx.txid) - bob2blockchain.expectMsgType[PublishTx] // main delayed - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txOut == htlcSuccessTx.txOut) - assert(bob2blockchain.expectMsgType[WatchTxConfirmed].txId == initialCommitTx.txid) + bob2blockchain.expectFinalTxPublished(commitTx.txid) + bob2blockchain.expectReplaceableTxPublished[ClaimLocalAnchorTx] + bob2blockchain.expectFinalTxPublished("local-main-delayed") + assert(bob2blockchain.expectReplaceableTxPublished[HtlcSuccessTx].input.outPoint == htlcSuccessTx.input.outPoint) + bob2blockchain.expectWatchTxConfirmed(commitTx.txid) channelUpdateListener.expectMsgType[LocalChannelDown] - alice2blockchain.expectNoMessage(500 millis) + alice2blockchain.expectNoMessage(100 millis) } test("recv CurrentFeerate (when funder, triggers an UpdateFee)") { f => - import f._ - val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] - val event = CurrentFeerates.BitcoinCore(FeeratesPerKw(minimum = FeeratePerKw(250 sat), fastest = FeeratePerKw(10_000 sat), fast = FeeratePerKw(5_000 sat), medium = FeeratePerKw(1000 sat), slow = FeeratePerKw(500 sat))) - alice.setBitcoinCoreFeerates(event.feeratesPerKw) - alice ! event - alice2bob.expectMsg(UpdateFee(initialState.commitments.channelId, alice.underlyingActor.nodeParams.onChainFeeConf.getCommitmentFeerate(alice.underlyingActor.nodeParams.currentBitcoinCoreFeerates, alice.underlyingActor.remoteNodeId, initialState.commitments.params.commitmentFormat, initialState.commitments.latest.capacity))) - } - - test("recv CurrentFeerate (when funder, triggers an UpdateFee, anchor outputs)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => import f._ val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] assert(initialState.commitments.latest.localCommit.spec.commitTxFeerate == TestConstants.anchorOutputsFeeratePerKw) @@ -3000,21 +2725,13 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with } test("recv CurrentFeerate (when funder, doesn't trigger an UpdateFee)") { f => - import f._ - val event = CurrentFeerates.BitcoinCore(FeeratesPerKw.single(FeeratePerKw(10010 sat))) - alice.setBitcoinCoreFeerates(event.feeratesPerKw) - alice ! event - alice2bob.expectNoMessage(500 millis) - } - - test("recv CurrentFeerate (when funder, doesn't trigger an UpdateFee, anchor outputs)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => import f._ val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] assert(initialState.commitments.latest.localCommit.spec.commitTxFeerate == TestConstants.anchorOutputsFeeratePerKw) val event = CurrentFeerates.BitcoinCore(FeeratesPerKw.single(TestConstants.anchorOutputsFeeratePerKw * 2).copy(minimum = FeeratePerKw(250 sat))) alice.setBitcoinCoreFeerates(event.feeratesPerKw) alice ! event - alice2bob.expectNoMessage(500 millis) + alice2bob.expectNoMessage(100 millis) } test("recv CurrentFeerate (when fundee, commit-fee/network-fee are close)") { f => @@ -3022,28 +2739,12 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val event = CurrentFeerates.BitcoinCore(FeeratesPerKw.single(FeeratePerKw(11000 sat))) bob.setBitcoinCoreFeerates(event.feeratesPerKw) bob ! event - bob2alice.expectNoMessage(500 millis) + bob2alice.expectNoMessage(100 millis) } test("recv CurrentFeerate (when fundee, commit-fee/network-fee are very different, with HTLCs)") { f => import f._ - addHtlc(10000000 msat, alice, bob, alice2bob, bob2alice) - crossSign(alice, bob, alice2bob, bob2alice) - - val event = CurrentFeerates.BitcoinCore(FeeratesPerKw.single(FeeratePerKw(14000 sat))) - bob.setBitcoinCoreFeerates(event.feeratesPerKw) - bob ! event - bob2alice.expectMsgType[Error] - bob2blockchain.expectMsgType[PublishTx] // commit tx - bob2blockchain.expectMsgType[PublishTx] // main delayed - bob2blockchain.expectMsgType[WatchTxConfirmed] - awaitCond(bob.stateName == CLOSING) - } - - test("recv CurrentFeerate (when fundee, commit-fee/network-fee are very different, with HTLCs, anchor outputs)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => - import f._ - // We start with a feerate lower than the 10 sat/byte threshold. alice.setBitcoinCoreFeerate(TestConstants.anchorOutputsFeeratePerKw / 2) bob.setBitcoinCoreFeerate(TestConstants.anchorOutputsFeeratePerKw / 2) @@ -3062,31 +2763,14 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(bob.stateName == NORMAL) } - test("recv CurrentFeerate (when fundee, commit-fee/network-fee are very different, without HTLCs)") { f => - import f._ - - val event = CurrentFeerates.BitcoinCore(FeeratesPerKw.single(FeeratePerKw(15_000 sat))) - bob.setBitcoinCoreFeerates(event.feeratesPerKw) - bob ! event - bob2alice.expectNoMessage(250 millis) // we don't close because the commitment doesn't contain any HTLC - - // when we try to add an HTLC, we still disagree on the feerate so we close - alice2bob.send(bob, UpdateAddHtlc(ByteVector32.Zeroes, 0, 2500000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, 1.0, None)) - bob2alice.expectMsgType[Error] - bob2blockchain.expectMsgType[PublishTx] // commit tx - bob2blockchain.expectMsgType[PublishTx] // main delayed - bob2blockchain.expectMsgType[WatchTxConfirmed] - awaitCond(bob.stateName == CLOSING) - } - - test("recv WatchFundingSpentTriggered (their commit w/ htlc)", Tag(ChannelStateTestsTags.StaticRemoteKey), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + test("recv WatchFundingSpentTriggered (their commit w/ htlc)") { f => import f._ - val (ra1, htlca1) = addHtlc(250000000 msat, CltvExpiryDelta(50), alice, bob, alice2bob, bob2alice) - val (ra2, htlca2) = addHtlc(100000000 msat, CltvExpiryDelta(60), alice, bob, alice2bob, bob2alice) - val (ra3, htlca3) = addHtlc(10000 msat, alice, bob, alice2bob, bob2alice) - val (rb1, htlcb1) = addHtlc(50000000 msat, CltvExpiryDelta(55), bob, alice, bob2alice, alice2bob) - val (rb2, htlcb2) = addHtlc(55000000 msat, CltvExpiryDelta(65), bob, alice, bob2alice, alice2bob) + val (_, htlca1) = addHtlc(250_000_000 msat, CltvExpiryDelta(50), alice, bob, alice2bob, bob2alice) + val (ra2, htlca2) = addHtlc(100_000_000 msat, CltvExpiryDelta(60), alice, bob, alice2bob, bob2alice) + addHtlc(10_000 msat, alice, bob, alice2bob, bob2alice) + val (rb1, htlcb1) = addHtlc(50_000_000 msat, CltvExpiryDelta(55), bob, alice, bob2alice, alice2bob) + addHtlc(55_000_000 msat, CltvExpiryDelta(65), bob, alice, bob2alice, alice2bob) crossSign(alice, bob, alice2bob, bob2alice) fulfillHtlc(htlca2.id, ra2, bob, alice, bob2alice, alice2bob) fulfillHtlc(htlcb1.id, rb1, alice, bob, alice2bob, bob2alice) @@ -3103,47 +2787,42 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // bob -> alice : 55 000 000 (alice does not have the preimage) => nothing to do, bob will get his money back after the timeout // bob publishes his current commit tx - val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val bobCommitTx = bob.signCommitTx() assert(bobCommitTx.txOut.size == 8) // two anchor outputs, two main outputs and 4 pending htlcs alice ! WatchFundingSpentTriggered(bobCommitTx) + awaitCond(alice.stateName == CLOSING) + assert(alice.stateData.asInstanceOf[DATA_CLOSING].remoteCommitPublished.isDefined) + val rcp = alice.stateData.asInstanceOf[DATA_CLOSING].remoteCommitPublished.get + assert(rcp.htlcOutputs.size == 4) // in response to that, alice publishes her claim txs - alice2blockchain.expectMsgType[PublishReplaceableTx] - val claimMain = alice2blockchain.expectMsgType[PublishFinalTx].tx + val claimAnchor = alice2blockchain.expectReplaceableTxPublished[ClaimRemoteAnchorTx] + val claimMain = alice2blockchain.expectFinalTxPublished("remote-main-delayed").tx // in addition to her main output, alice can only claim 3 out of 4 htlcs, she can't do anything regarding the htlc sent by bob for which she does not have the preimage val claimHtlcTxs = (1 to 3).map(_ => alice2blockchain.expectMsgType[PublishReplaceableTx]) - val htlcAmountClaimed = (for (claimHtlcTx <- claimHtlcTxs) yield { + alice2blockchain.expectWatchTxConfirmed(bobCommitTx.txid) + alice2blockchain.expectWatchOutputsSpent(Seq(claimAnchor.input.outPoint, claimMain.txIn.head.outPoint) ++ rcp.htlcOutputs.toSeq) + alice2blockchain.expectNoMessage(100 millis) + + val htlcAmountClaimed = claimHtlcTxs.map(claimHtlcTx => { assert(claimHtlcTx.txInfo.tx.txIn.size == 1) assert(claimHtlcTx.txInfo.tx.txOut.size == 1) - Transaction.correctlySpends(claimHtlcTx.txInfo.tx, bobCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + Transaction.correctlySpends(claimHtlcTx.txInfo.sign(), bobCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) claimHtlcTx.txInfo.tx.txOut.head.amount }).sum // at best we have a little less than 450 000 + 250 000 + 100 000 + 50 000 = 850 000 (because fees) val amountClaimed = claimMain.txOut.head.amount + htlcAmountClaimed - assert(amountClaimed == 823680.sat) + assert(amountClaimed == 839_959.sat) // alice sets the confirmation targets to the HTLC expiry - assert(claimHtlcTxs.map(_.commitTx.txid).toSet == Set(bobCommitTx.txid)) - assert(claimHtlcTxs.collect { case PublishReplaceableTx(tx: ClaimHtlcSuccessTx, _, _) => (tx.htlcId, tx.confirmationTarget.confirmBefore) }.toMap == Map(htlcb1.id -> htlcb1.cltvExpiry.blockHeight)) - assert(claimHtlcTxs.collect { case PublishReplaceableTx(tx: ClaimHtlcTimeoutTx, _, _) => (tx.htlcId, tx.confirmationTarget.confirmBefore) }.toMap == Map(htlca1.id -> htlca1.cltvExpiry.blockHeight, htlca2.id -> htlca2.cltvExpiry.blockHeight)) - - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == bobCommitTx.txid) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == claimMain.txid) - alice2blockchain.expectMsgType[WatchOutputSpent] // htlc 1 - alice2blockchain.expectMsgType[WatchOutputSpent] // htlc 2 - alice2blockchain.expectMsgType[WatchOutputSpent] // htlc 3 - alice2blockchain.expectMsgType[WatchOutputSpent] // htlc 4 - alice2blockchain.expectNoMessage(1 second) - - awaitCond(alice.stateName == CLOSING) - assert(alice.stateData.asInstanceOf[DATA_CLOSING].remoteCommitPublished.isDefined) - val rcp = alice.stateData.asInstanceOf[DATA_CLOSING].remoteCommitPublished.get - assert(rcp.claimHtlcTxs.size == 4) - assert(getClaimHtlcSuccessTxs(rcp).length == 1) - assert(getClaimHtlcTimeoutTxs(rcp).length == 2) + claimHtlcTxs.foreach(p => assert(p.commitTx.txid == bobCommitTx.txid)) + val htlcSuccessConfirmationTargets = claimHtlcTxs.collect { case PublishReplaceableTx(tx: ClaimHtlcSuccessTx, _, _, confirmationTarget) => (tx.htlcId, confirmationTarget) }.toMap + assert(htlcSuccessConfirmationTargets == Map(htlcb1.id -> ConfirmationTarget.Absolute(htlcb1.cltvExpiry.blockHeight))) + val htlcTimeoutConfirmationTargets = claimHtlcTxs.collect { case PublishReplaceableTx(tx: ClaimHtlcTimeoutTx, _, _, confirmationTarget) => (tx.htlcId, confirmationTarget) }.toMap + assert(htlcTimeoutConfirmationTargets == Map(htlca1.id -> ConfirmationTarget.Absolute(htlca1.cltvExpiry.blockHeight), htlca2.id -> ConfirmationTarget.Absolute(htlca2.cltvExpiry.blockHeight))) // assert the feerate of the claim main is what we expect - val expectedFeeRate = alice.underlyingActor.nodeParams.onChainFeeConf.getClosingFeerate(alice.underlyingActor.nodeParams.currentBitcoinCoreFeerates) + val expectedFeeRate = alice.underlyingActor.nodeParams.onChainFeeConf.getClosingFeerate(alice.underlyingActor.nodeParams.currentBitcoinCoreFeerates, None) val claimFee = claimMain.txIn.map(in => bobCommitTx.txOut(in.outPoint.index.toInt).amount).sum - claimMain.txOut.map(_.amount).sum val claimFeeRate = Transactions.fee2rate(claimFee, claimMain.weight()) assert(claimFeeRate >= expectedFeeRate * 0.9 && claimFeeRate <= expectedFeeRate * 1.2) @@ -3159,20 +2838,20 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // actual test starts here // bob publishes his current commit tx - val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val bobCommitTx = bob.signCommitTx() alice ! WatchFundingSpentTriggered(bobCommitTx) val addSettled = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.ChannelFailureBeforeSigned.type]] assert(addSettled.htlc == htlc1) } - test("recv WatchFundingSpentTriggered (their *next* commit w/ htlc)", Tag(ChannelStateTestsTags.StaticRemoteKey), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + test("recv WatchFundingSpentTriggered (their *next* commit w/ htlc)") { f => import f._ - val (ra1, htlca1) = addHtlc(250000000 msat, CltvExpiryDelta(24), alice, bob, alice2bob, bob2alice) - val (ra2, htlca2) = addHtlc(100000000 msat, CltvExpiryDelta(30), alice, bob, alice2bob, bob2alice) - val (ra3, htlca3) = addHtlc(10000 msat, alice, bob, alice2bob, bob2alice) - val (rb1, htlcb1) = addHtlc(50000000 msat, bob, alice, bob2alice, alice2bob) - val (rb2, htlcb2) = addHtlc(55000000 msat, bob, alice, bob2alice, alice2bob) + val (_, htlca1) = addHtlc(250_000_000 msat, CltvExpiryDelta(24), alice, bob, alice2bob, bob2alice) + val (ra2, htlca2) = addHtlc(100_000_000 msat, CltvExpiryDelta(30), alice, bob, alice2bob, bob2alice) + addHtlc(10_000 msat, alice, bob, alice2bob, bob2alice) + val (rb1, htlcb1) = addHtlc(50_000_000 msat, bob, alice, bob2alice, alice2bob) + addHtlc(55_000_000 msat, bob, alice, bob2alice, alice2bob) crossSign(alice, bob, alice2bob, bob2alice) fulfillHtlc(htlca2.id, ra2, bob, alice, bob2alice, alice2bob) fulfillHtlc(htlcb1.id, rb1, alice, bob, alice2bob, bob2alice) @@ -3196,41 +2875,36 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // bob -> alice : 55 000 000 (alice does not have the preimage) => nothing to do, bob will get his money back after the timeout // bob publishes his current commit tx - val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val bobCommitTx = bob.signCommitTx() assert(bobCommitTx.txOut.size == 7) // two anchor outputs, two main outputs and 3 pending htlcs alice ! WatchFundingSpentTriggered(bobCommitTx) + awaitCond(alice.stateName == CLOSING) + assert(alice.stateData.asInstanceOf[DATA_CLOSING].nextRemoteCommitPublished.isDefined) + val rcp = alice.stateData.asInstanceOf[DATA_CLOSING].nextRemoteCommitPublished.get // in response to that, alice publishes her claim txs - val claimAnchor = alice2blockchain.expectMsgType[PublishReplaceableTx].txInfo.tx - val claimMain = alice2blockchain.expectMsgType[PublishFinalTx].tx + val claimAnchor = alice2blockchain.expectReplaceableTxPublished[ClaimRemoteAnchorTx] + val claimMain = alice2blockchain.expectFinalTxPublished("remote-main-delayed").tx // in addition to her main output, alice can only claim 2 out of 3 htlcs, she can't do anything regarding the htlc sent by bob for which she does not have the preimage val claimHtlcTxs = (1 to 2).map(_ => alice2blockchain.expectMsgType[PublishReplaceableTx]) - val htlcAmountClaimed = (for (claimHtlcTx <- claimHtlcTxs) yield { + alice2blockchain.expectWatchTxConfirmed(bobCommitTx.txid) + alice2blockchain.expectWatchOutputsSpent(Seq(claimAnchor.input.outPoint, claimMain.txIn.head.outPoint) ++ rcp.htlcOutputs.toSeq) + alice2blockchain.expectNoMessage(100 millis) + + val htlcAmountClaimed = claimHtlcTxs.map(claimHtlcTx => { assert(claimHtlcTx.txInfo.tx.txIn.size == 1) assert(claimHtlcTx.txInfo.tx.txOut.size == 1) - Transaction.correctlySpends(claimHtlcTx.txInfo.tx, bobCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + Transaction.correctlySpends(claimHtlcTx.txInfo.sign(), bobCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) claimHtlcTx.txInfo.tx.txOut.head.amount }).sum // at best we have a little less than 500 000 + 250 000 + 100 000 = 850 000 (because fees) val amountClaimed = claimMain.txOut.head.amount + htlcAmountClaimed - assert(amountClaimed == 829850.sat) + assert(amountClaimed == 840_534.sat) // alice sets the confirmation targets to the HTLC expiry - assert(claimHtlcTxs.map(_.commitTx.txid).toSet == Set(bobCommitTx.txid)) - assert(claimHtlcTxs.collect { case PublishReplaceableTx(tx: ClaimHtlcTimeoutTx, _, _) => (tx.htlcId, tx.confirmationTarget.confirmBefore) }.toMap == Map(htlca1.id -> htlca1.cltvExpiry.blockHeight, htlca2.id -> htlca2.cltvExpiry.blockHeight)) - - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == bobCommitTx.txid) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == claimMain.txid) // claim-main - alice2blockchain.expectMsgType[WatchOutputSpent] // htlc 1 - alice2blockchain.expectMsgType[WatchOutputSpent] // htlc 2 - alice2blockchain.expectMsgType[WatchOutputSpent] // htlc 3 - alice2blockchain.expectNoMessage(1 second) - - awaitCond(alice.stateName == CLOSING) - assert(alice.stateData.asInstanceOf[DATA_CLOSING].nextRemoteCommitPublished.isDefined) - val rcp = alice.stateData.asInstanceOf[DATA_CLOSING].nextRemoteCommitPublished.get - assert(getClaimHtlcSuccessTxs(rcp).length == 0) - assert(getClaimHtlcTimeoutTxs(rcp).length == 2) + claimHtlcTxs.foreach(p => assert(p.commitTx.txid == bobCommitTx.txid)) + val htlcConfirmationTargets = claimHtlcTxs.collect { case PublishReplaceableTx(tx: ClaimHtlcTimeoutTx, _, _, confirmationTarget) => (tx.htlcId, confirmationTarget) }.toMap + assert(htlcConfirmationTargets == Map(htlca1.id -> ConfirmationTarget.Absolute(htlca1.cltvExpiry.blockHeight), htlca2.id -> ConfirmationTarget.Absolute(htlca2.cltvExpiry.blockHeight))) } test("recv WatchFundingSpentTriggered (their *next* commit w/ pending unsigned htlcs)") { f => @@ -3250,26 +2924,25 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // actual test starts here // bob publishes his current commit tx - val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val bobCommitTx = bob.signCommitTx() alice ! WatchFundingSpentTriggered(bobCommitTx) val addSettled = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.ChannelFailureBeforeSigned.type]] assert(addSettled.htlc == htlc2) } - test("recv WatchFundingSpentTriggered (revoked commit)", Tag(ChannelStateTestsTags.StaticRemoteKey), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + test("recv WatchFundingSpentTriggered (revoked commit)") { f => import f._ // initially we have : - // alice = 800 000 + // alice = 800 000 // bob = 200 000 def send(): Transaction = { - // alice sends 8 000 sat - addHtlc(10000000 msat, alice, bob, alice2bob, bob2alice) + // alice sends 10 000 sat + addHtlc(10_000_000 msat, alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) - - bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + bob.signCommitTx() } - val txs = for (_ <- 0 until 10) yield send() + val txs = (0 until 10).map(_ => send()) // bob now has 10 spendable tx, 9 of them being revoked // let's say that bob published this tx @@ -3285,43 +2958,35 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(revokedTx.txOut.size == 8) alice ! WatchFundingSpentTriggered(revokedTx) alice2bob.expectMsgType[Error] + awaitCond(alice.stateName == CLOSING) + assert(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.size == 1) + val rvk = alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.head + assert(rvk.htlcOutputs.size == 4) - val mainTx = alice2blockchain.expectMsgType[PublishFinalTx].tx - val mainPenaltyTx = alice2blockchain.expectMsgType[PublishFinalTx].tx - val htlcPenaltyTxs = for (_ <- 0 until 4) yield alice2blockchain.expectMsgType[PublishFinalTx].tx - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == revokedTx.txid) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == mainTx.txid) - alice2blockchain.expectMsgType[WatchOutputSpent] // main-penalty + val mainTx = alice2blockchain.expectFinalTxPublished("remote-main-delayed").tx + val mainPenaltyTx = alice2blockchain.expectFinalTxPublished("main-penalty").tx + val htlcPenaltyTxs = (0 until 4).map(_ => alice2blockchain.expectFinalTxPublished("htlc-penalty").tx) // let's make sure that htlc-penalty txs each spend a different output - assert(htlcPenaltyTxs.map(_.txIn.head.outPoint.index).toSet.size == htlcPenaltyTxs.size) - htlcPenaltyTxs.foreach(_ => alice2blockchain.expectMsgType[WatchOutputSpent]) - alice2blockchain.expectNoMessage(1 second) - - Transaction.correctlySpends(mainTx, Seq(revokedTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - Transaction.correctlySpends(mainPenaltyTx, Seq(revokedTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - htlcPenaltyTxs.foreach(htlcPenaltyTx => Transaction.correctlySpends(htlcPenaltyTx, Seq(revokedTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) + assert(htlcPenaltyTxs.map(_.txIn.head.outPoint).toSet.size == 4) + (mainTx +: mainPenaltyTx +: htlcPenaltyTxs).foreach(tx => Transaction.correctlySpends(tx, Seq(revokedTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) + alice2blockchain.expectWatchTxConfirmed(revokedTx.txid) + alice2blockchain.expectWatchOutputsSpent((mainTx +: mainPenaltyTx +: htlcPenaltyTxs).flatMap(_.txIn.map(_.outPoint))) + alice2blockchain.expectNoMessage(100 millis) // two main outputs are 760 000 and 200 000 - assert(mainTx.txOut.head.amount == 750390.sat) - assert(mainPenaltyTx.txOut.head.amount == 195160.sat) - assert(htlcPenaltyTxs(0).txOut.head.amount == 4510.sat) - assert(htlcPenaltyTxs(1).txOut.head.amount == 4510.sat) - assert(htlcPenaltyTxs(2).txOut.head.amount == 4510.sat) - assert(htlcPenaltyTxs(3).txOut.head.amount == 4510.sat) - - awaitCond(alice.stateName == CLOSING) - assert(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.size == 1) + assert(mainTx.txOut.head.amount == 750_390.sat) + assert(mainPenaltyTx.txOut.head.amount == 195_170.sat) + htlcPenaltyTxs.foreach(tx => assert(tx.txOut.head.amount == 4_200.sat)) } - test("recv WatchFundingSpentTriggered (revoked commit with identical htlcs)", Tag(ChannelStateTestsTags.StaticRemoteKey), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + test("recv WatchFundingSpentTriggered (revoked commit with identical htlcs)") { f => import f._ val sender = TestProbe() // initially we have : - // alice = 800 000 + // alice = 800 000 // bob = 200 000 - - val add = CMD_ADD_HTLC(sender.ref, 10000000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, 1.0, None, localOrigin(sender.ref)) + val add = CMD_ADD_HTLC(sender.ref, 10000000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, Reputation.Score.max, None, localOrigin(sender.ref)) alice ! add sender.expectMsgType[RES_SUCCESS[CMD_ADD_HTLC]] alice2bob.expectMsgType[UpdateAddHtlc] @@ -3333,7 +2998,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with crossSign(alice, bob, alice2bob, bob2alice) // bob will publish this tx after it is revoked - val revokedTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val revokedTx = bob.signCommitTx() alice ! add sender.expectMsgType[RES_SUCCESS[CMD_ADD_HTLC]] @@ -3351,34 +3016,31 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(revokedTx.txOut.size == 6) alice ! WatchFundingSpentTriggered(revokedTx) alice2bob.expectMsgType[Error] + awaitCond(alice.stateName == CLOSING) + assert(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.size == 1) - val mainTx = alice2blockchain.expectMsgType[PublishFinalTx].tx - val mainPenaltyTx = alice2blockchain.expectMsgType[PublishFinalTx].tx - val htlcPenaltyTxs = for (_ <- 0 until 2) yield alice2blockchain.expectMsgType[PublishFinalTx].tx + val mainTx = alice2blockchain.expectFinalTxPublished("remote-main-delayed").tx + val mainPenaltyTx = alice2blockchain.expectFinalTxPublished("main-penalty").tx + val htlcPenaltyTxs = (0 until 2).map(_ => alice2blockchain.expectFinalTxPublished("htlc-penalty").tx) // let's make sure that htlc-penalty txs each spend a different output - assert(htlcPenaltyTxs.map(_.txIn.head.outPoint.index).toSet.size == htlcPenaltyTxs.size) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == revokedTx.txid) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == mainTx.txid) - alice2blockchain.expectMsgType[WatchOutputSpent] // main-penalty - htlcPenaltyTxs.foreach(_ => alice2blockchain.expectMsgType[WatchOutputSpent]) - alice2blockchain.expectNoMessage(1 second) - - Transaction.correctlySpends(mainTx, Seq(revokedTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - Transaction.correctlySpends(mainPenaltyTx, Seq(revokedTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - htlcPenaltyTxs.foreach(htlcPenaltyTx => Transaction.correctlySpends(htlcPenaltyTx, Seq(revokedTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) + assert(htlcPenaltyTxs.map(_.txIn.head.outPoint).toSet.size == htlcPenaltyTxs.size) + (mainTx +: mainPenaltyTx +: htlcPenaltyTxs).foreach(tx => Transaction.correctlySpends(tx, Seq(revokedTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) + alice2blockchain.expectWatchTxConfirmed(revokedTx.txid) + alice2blockchain.expectWatchOutputsSpent((mainTx +: mainPenaltyTx +: htlcPenaltyTxs).flatMap(_.txIn.map(_.outPoint))) + alice2blockchain.expectNoMessage(100 millis) } test("recv WatchFundingSpentTriggered (revoked commit w/ pending unsigned htlcs)") { f => import f._ val sender = TestProbe() - addHtlc(10000 msat, alice, bob, alice2bob, bob2alice, sender.ref) + addHtlc(10_000 msat, alice, bob, alice2bob, bob2alice, sender.ref) sender.expectMsgType[RES_SUCCESS[CMD_ADD_HTLC]] crossSign(alice, bob, alice2bob, bob2alice) - val bobRevokedCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx - addHtlc(10000 msat, alice, bob, alice2bob, bob2alice, sender.ref) + val bobRevokedCommitTx = bob.signCommitTx() + addHtlc(10_000 msat, alice, bob, alice2bob, bob2alice, sender.ref) sender.expectMsgType[RES_SUCCESS[CMD_ADD_HTLC]] crossSign(alice, bob, alice2bob, bob2alice) - val (_, htlc3) = addHtlc(10000 msat, alice, bob, alice2bob, bob2alice, sender.ref) + val (_, htlc3) = addHtlc(10_000 msat, alice, bob, alice2bob, bob2alice, sender.ref) sender.expectMsgType[RES_SUCCESS[CMD_ADD_HTLC]] val aliceData = alice.stateData.asInstanceOf[DATA_NORMAL] assert(aliceData.commitments.changes.localChanges.proposed.size == 1) @@ -3393,21 +3055,21 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with test("recv WatchFundingSpentTriggered (unrecognized commit)") { f => import f._ - alice ! WatchFundingSpentTriggered(Transaction(0, Nil, Nil, 0)) + alice ! WatchFundingSpentTriggered(Transaction(2, Nil, TxOut(100_000 sat, Script.pay2wpkh(randomKey().publicKey)) :: Nil, 0)) alice2blockchain.expectNoMessage(100 millis) assert(alice.stateName == NORMAL) } test("recv Error") { f => import f._ - val (ra1, htlca1) = addHtlc(250000000 msat, alice, bob, alice2bob, bob2alice) - val (ra2, htlca2) = addHtlc(100000000 msat, alice, bob, alice2bob, bob2alice) - val (ra3, htlca3) = addHtlc(10000 msat, alice, bob, alice2bob, bob2alice) - val (rb1, htlcb1) = addHtlc(50000000 msat, bob, alice, bob2alice, alice2bob) - val (rb2, htlcb2) = addHtlc(55000000 msat, bob, alice, bob2alice, alice2bob) + addHtlc(250_000_000 msat, alice, bob, alice2bob, bob2alice) + val (ra, htlca) = addHtlc(100_000_000 msat, alice, bob, alice2bob, bob2alice) + addHtlc(10_000 msat, alice, bob, alice2bob, bob2alice) + val (rb, htlcb) = addHtlc(50_000_000 msat, bob, alice, bob2alice, alice2bob) + addHtlc(55_000_000 msat, bob, alice, bob2alice, alice2bob) crossSign(alice, bob, alice2bob, bob2alice) - fulfillHtlc(htlca2.id, ra2, bob, alice, bob2alice, alice2bob) - fulfillHtlc(htlcb1.id, rb1, alice, bob, alice2bob, bob2alice) + fulfillHtlc(htlca.id, ra, bob, alice, bob2alice, alice2bob) + fulfillHtlc(htlcb.id, rb, alice, bob, alice2bob, bob2alice) // at this point here is the situation from alice pov and what she should do when she publishes his commit tx: // balances : @@ -3421,50 +3083,44 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // bob -> alice : 55 000 000 (alice does not have the preimage) => nothing to do, bob will get his money back after the timeout // an error occurs and alice publishes her commit tx - val aliceCommitTx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val aliceCommitTx = alice.signCommitTx() alice ! Error(ByteVector32.Zeroes, "oops") - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == aliceCommitTx.txid) - assert(aliceCommitTx.txOut.size == 6) // two main outputs and 4 pending htlcs + alice2blockchain.expectFinalTxPublished(aliceCommitTx.txid) + assert(aliceCommitTx.txOut.size == 8) // two main outputs, two anchor outputs and 4 pending htlcs awaitCond(alice.stateName == CLOSING) assert(alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.isDefined) val localCommitPublished = alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.get assert(localCommitPublished.commitTx.txid == aliceCommitTx.txid) - assert(localCommitPublished.htlcTxs.size == 4) - assert(getHtlcSuccessTxs(localCommitPublished).length == 1) - assert(getHtlcTimeoutTxs(localCommitPublished).length == 2) - assert(localCommitPublished.claimHtlcDelayedTxs.isEmpty) + assert(localCommitPublished.htlcOutputs.size == 4) + assert(localCommitPublished.htlcDelayedOutputs.isEmpty) // alice can only claim 3 out of 4 htlcs, she can't do anything regarding the htlc sent by bob for which she does not have the preimage // so we expect 4 transactions: // - 1 tx to claim the main delayed output // - 3 txs for each htlc // NB: 3rd-stage txs will only be published once the htlc txs confirm - val claimMain = alice2blockchain.expectMsgType[PublishFinalTx] - val htlcTx1 = alice2blockchain.expectMsgType[PublishFinalTx] - val htlcTx2 = alice2blockchain.expectMsgType[PublishFinalTx] - val htlcTx3 = alice2blockchain.expectMsgType[PublishFinalTx] + alice2blockchain.expectReplaceableTxPublished[ClaimLocalAnchorTx] + val claimMain = alice2blockchain.expectFinalTxPublished("local-main-delayed").tx + val htlcTx1 = alice2blockchain.expectReplaceableTxPublished[HtlcSuccessTx].sign() + val htlcTx2 = alice2blockchain.expectReplaceableTxPublished[HtlcTimeoutTx].sign() + val htlcTx3 = alice2blockchain.expectReplaceableTxPublished[HtlcTimeoutTx].sign() // the main delayed output and htlc txs spend the commitment transaction - Seq(claimMain, htlcTx1, htlcTx2, htlcTx3).foreach(tx => Transaction.correctlySpends(tx.tx, aliceCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) - - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == aliceCommitTx.txid) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == claimMain.tx.txid) // main-delayed - alice2blockchain.expectMsgType[WatchOutputSpent] // htlc 1 - alice2blockchain.expectMsgType[WatchOutputSpent] // htlc 2 - alice2blockchain.expectMsgType[WatchOutputSpent] // htlc 3 - alice2blockchain.expectMsgType[WatchOutputSpent] // htlc 4 - alice2blockchain.expectNoMessage(1 second) + Seq(claimMain, htlcTx1, htlcTx2, htlcTx3).foreach(tx => Transaction.correctlySpends(tx, aliceCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) + alice2blockchain.expectWatchTxConfirmed(aliceCommitTx.txid) + alice2blockchain.expectWatchOutputsSpent(Seq(localCommitPublished.localOutput_opt, localCommitPublished.anchorOutput_opt).flatten ++ localCommitPublished.htlcOutputs.toSeq) + alice2blockchain.expectNoMessage(100 millis) // 3rd-stage txs are published when htlc txs confirm - Seq(htlcTx1, htlcTx2, htlcTx3).foreach { htlcTimeoutTx => - alice ! WatchOutputSpentTriggered(htlcTimeoutTx.amount, htlcTimeoutTx.tx) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == htlcTimeoutTx.tx.txid) - alice ! WatchTxConfirmedTriggered(BlockHeight(2701), 3, htlcTimeoutTx.tx) - val claimHtlcDelayedTx = alice2blockchain.expectMsgType[PublishFinalTx].tx - Transaction.correctlySpends(claimHtlcDelayedTx, htlcTimeoutTx.tx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == claimHtlcDelayedTx.txid) + Seq(htlcTx1, htlcTx2, htlcTx3).foreach { htlcTx => + alice ! WatchOutputSpentTriggered(0 sat, htlcTx) + alice2blockchain.expectWatchTxConfirmed(htlcTx.txid) + alice ! WatchTxConfirmedTriggered(BlockHeight(2701), 3, htlcTx) + val htlcDelayedTx = alice2blockchain.expectFinalTxPublished("htlc-delayed") + Transaction.correctlySpends(htlcDelayedTx.tx, htlcTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + alice2blockchain.expectWatchOutputSpent(htlcDelayedTx.input) } - awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.get.claimHtlcDelayedTxs.length == 3) - alice2blockchain.expectNoMessage(1 second) + awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.get.htlcDelayedOutputs.size == 3) + alice2blockchain.expectNoMessage(100 millis) } test("recv Error (ignored internal error from lnd)") { f => @@ -3478,51 +3134,52 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with def testErrorAnchorOutputsWithHtlcs(f: FixtureParam): Unit = { import f._ - val (ra1, htlca1) = addHtlc(250000000 msat, CltvExpiryDelta(20), alice, bob, alice2bob, bob2alice) - val (ra2, htlca2) = addHtlc(100000000 msat, CltvExpiryDelta(25), alice, bob, alice2bob, bob2alice) - val (ra3, htlca3) = addHtlc(10000 msat, alice, bob, alice2bob, bob2alice) - val (rb1, htlcb1) = addHtlc(50000000 msat, CltvExpiryDelta(30), bob, alice, bob2alice, alice2bob) - val (rb2, htlcb2) = addHtlc(55000000 msat, CltvExpiryDelta(35), bob, alice, bob2alice, alice2bob) + val (_, htlca1) = addHtlc(250_000_000 msat, CltvExpiryDelta(20), alice, bob, alice2bob, bob2alice) + val (_, htlca2) = addHtlc(100_000_000 msat, CltvExpiryDelta(25), alice, bob, alice2bob, bob2alice) + addHtlc(10_000 msat, alice, bob, alice2bob, bob2alice) + val (rb1, htlcb1) = addHtlc(50_000_000 msat, CltvExpiryDelta(30), bob, alice, bob2alice, alice2bob) + addHtlc(55_000_000 msat, CltvExpiryDelta(35), bob, alice, bob2alice, alice2bob) crossSign(alice, bob, alice2bob, bob2alice) fulfillHtlc(htlcb1.id, rb1, alice, bob, alice2bob, bob2alice) // an error occurs and alice publishes her commit tx - val aliceCommitTx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val aliceCommitTx = alice.signCommitTx() alice ! Error(ByteVector32.Zeroes, "oops") - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == aliceCommitTx.txid) + alice2blockchain.expectFinalTxPublished(aliceCommitTx.txid) assert(aliceCommitTx.txOut.size == 8) // two main outputs, two anchors and 4 pending htlcs awaitCond(alice.stateName == CLOSING) + val localCommitPublished = alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.get val localAnchor = alice2blockchain.expectMsgType[PublishReplaceableTx] - assert(localAnchor.txInfo.confirmationTarget == ConfirmationTarget.Absolute(htlca1.cltvExpiry.blockHeight)) // the target is set to match the first htlc that expires - val claimMain = alice2blockchain.expectMsgType[PublishFinalTx] + assert(localAnchor.txInfo.isInstanceOf[ClaimLocalAnchorTx]) + assert(localAnchor.confirmationTarget == ConfirmationTarget.Absolute(htlca1.cltvExpiry.blockHeight)) // the target is set to match the first htlc that expires + val claimMain = alice2blockchain.expectFinalTxPublished("local-main-delayed") // alice can only claim 3 out of 4 htlcs, she can't do anything regarding the htlc sent by bob for which she does not have the preimage - val htlcConfirmationTargets = Seq( - alice2blockchain.expectMsgType[PublishReplaceableTx], // htlc 1 - alice2blockchain.expectMsgType[PublishReplaceableTx], // htlc 2 - alice2blockchain.expectMsgType[PublishReplaceableTx], // htlc 3 - ).map(p => p.txInfo.asInstanceOf[HtlcTx].htlcId -> p.txInfo.asInstanceOf[HtlcTx].confirmationTarget.confirmBefore).toMap - assert(htlcConfirmationTargets == Map(htlcb1.id -> htlcb1.cltvExpiry.blockHeight, htlca1.id -> htlca1.cltvExpiry.blockHeight, htlca2.id -> htlca2.cltvExpiry.blockHeight)) - - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == aliceCommitTx.txid) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == claimMain.tx.txid) - val watchedOutputs = Seq( - alice2blockchain.expectMsgType[WatchOutputSpent], // htlc 1 - alice2blockchain.expectMsgType[WatchOutputSpent], // htlc 2 - alice2blockchain.expectMsgType[WatchOutputSpent], // htlc 3 - alice2blockchain.expectMsgType[WatchOutputSpent], // htlc 4 - alice2blockchain.expectMsgType[WatchOutputSpent], // local anchor - ).map(w => OutPoint(w.txId, w.outputIndex)).toSet - val localCommitPublished = alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.get - assert(watchedOutputs == localCommitPublished.htlcTxs.keySet + localAnchor.txInfo.input.outPoint) - alice2blockchain.expectNoMessage(1 second) + val htlcTxs = (0 until 3).map(_ => alice2blockchain.expectMsgType[PublishReplaceableTx]) + alice2blockchain.expectWatchTxConfirmed(aliceCommitTx.txid) + alice2blockchain.expectWatchOutputsSpent(claimMain.input +: localAnchor.input +: localCommitPublished.htlcOutputs.toSeq) + alice2blockchain.expectNoMessage(100 millis) + + // alice sets the confirmation target of each htlc transaction to the htlc expiry + assert(htlcTxs.map(_.txInfo).collect { case tx: HtlcSuccessTx => tx }.size == 1) + assert(htlcTxs.map(_.txInfo).collect { case tx: HtlcTimeoutTx => tx }.size == 2) + val htlcConfirmationTargets = htlcTxs.map(p => p.txInfo.asInstanceOf[SignedHtlcTx].htlcId -> p.confirmationTarget).toMap + assert(htlcConfirmationTargets == Map( + htlcb1.id -> ConfirmationTarget.Absolute(htlcb1.cltvExpiry.blockHeight), + htlca1.id -> ConfirmationTarget.Absolute(htlca1.cltvExpiry.blockHeight), + htlca2.id -> ConfirmationTarget.Absolute(htlca2.cltvExpiry.blockHeight) + )) + } + + test("recv Error (anchor outputs zero fee htlc txs)") { f => + testErrorAnchorOutputsWithHtlcs(f) } - test("recv Error (anchor outputs zero fee htlc txs)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + test("recv Error (simple taproot channel)", Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => testErrorAnchorOutputsWithHtlcs(f) } - test("recv Error (anchor outputs zero fee htlc txs, fee-bumping for commit txs without htlcs disabled)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), Tag(ChannelStateTestsTags.DontSpendAnchorWithoutHtlcs)) { f => + test("recv Error (anchor outputs zero fee htlc txs, fee-bumping for commit txs without htlcs disabled)", Tag(ChannelStateTestsTags.DontSpendAnchorWithoutHtlcs)) { f => // We should ignore the disable flag since there are htlcs in the commitment (funds at risk). testErrorAnchorOutputsWithHtlcs(f) } @@ -3531,35 +3188,33 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with import f._ // an error occurs and alice publishes her commit tx - val aliceCommitTx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val aliceCommitTx = alice.signCommitTx() alice ! Error(ByteVector32.Zeroes, "oops") - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == aliceCommitTx.txid) + alice2blockchain.expectFinalTxPublished(aliceCommitTx.txid) assert(aliceCommitTx.txOut.size == 4) // two main outputs and two anchors awaitCond(alice.stateName == CLOSING) + val lcp = alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.get - val currentBlockHeight = alice.underlyingActor.nodeParams.currentBlockHeight - if (commitFeeBumpDisabled) { - val claimMain = alice2blockchain.expectMsgType[PublishFinalTx] - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === aliceCommitTx.txid) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === claimMain.tx.txid) - alice2blockchain.expectNoMessage(1 second) - } else { - val localAnchor = alice2blockchain.expectMsgType[PublishReplaceableTx] - // When there are no pending HTLCs, there is no absolute deadline to get the commit tx confirmed, we use priority - assert(localAnchor.txInfo.confirmationTarget == ConfirmationTarget.Priority(ConfirmationPriority.Medium)) - val claimMain = alice2blockchain.expectMsgType[PublishFinalTx] - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === aliceCommitTx.txid) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === claimMain.tx.txid) - assert(alice2blockchain.expectMsgType[WatchOutputSpent].outputIndex === localAnchor.input.index) - alice2blockchain.expectNoMessage(1 second) + if (!commitFeeBumpDisabled) { + // When there are no pending HTLCs, there is no absolute deadline to get the commit tx confirmed: we use a medium priority. + alice2blockchain.expectReplaceableTxPublished[ClaimLocalAnchorTx](ConfirmationTarget.Priority(ConfirmationPriority.Medium)) } + + alice2blockchain.expectFinalTxPublished("local-main-delayed") + alice2blockchain.expectWatchTxConfirmed(aliceCommitTx.txid) + alice2blockchain.expectWatchOutputsSpent(lcp.anchorOutput_opt.toSeq ++ lcp.localOutput_opt.toSeq) + alice2blockchain.expectNoMessage(100 millis) + } + + test("recv Error (anchor outputs zero fee htlc txs without htlcs)") { f => + testErrorAnchorOutputsWithoutHtlcs(f, commitFeeBumpDisabled = false) } - test("recv Error (anchor outputs zero fee htlc txs without htlcs)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + test("recv Error (simple taproot channel without htlcs)", Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => testErrorAnchorOutputsWithoutHtlcs(f, commitFeeBumpDisabled = false) } - test("recv Error (anchor outputs zero fee htlc txs without htlcs, fee-bumping for commit txs without htlcs disabled)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), Tag(ChannelStateTestsTags.DontSpendAnchorWithoutHtlcs)) { f => + test("recv Error (anchor outputs zero fee htlc txs without htlcs, fee-bumping for commit txs without htlcs disabled)", Tag(ChannelStateTestsTags.DontSpendAnchorWithoutHtlcs)) { f => testErrorAnchorOutputsWithoutHtlcs(f, commitFeeBumpDisabled = true) } @@ -3570,11 +3225,11 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // have lost its data and need assistance // an error occurs and alice publishes her commit tx - val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val bobCommitTx = bob.signCommitTx() bob ! Error(ByteVector32.Zeroes, "oops") - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == bobCommitTx.txid) - assert(bobCommitTx.txOut.size == 1) // only one main output - alice2blockchain.expectNoMessage(1 second) + bob2blockchain.expectFinalTxPublished(bobCommitTx.txid) + assert(bobCommitTx.txOut.size == 2) // alice's main output and anchor output + alice2blockchain.expectNoMessage(100 millis) awaitCond(bob.stateName == CLOSING) assert(bob.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.isDefined) @@ -3596,7 +3251,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(addSettled.htlc == htlc1) } - test("recv WatchFundingConfirmedTriggered (public channel, zero-conf)", Tag(ChannelStateTestsTags.ChannelsPublic), Tag(ChannelStateTestsTags.DoNotInterceptGossip), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), Tag(ChannelStateTestsTags.ZeroConf)) { f => + test("recv WatchFundingConfirmedTriggered (public channel, zero-conf)", Tag(ChannelStateTestsTags.ChannelsPublic), Tag(ChannelStateTestsTags.DoNotInterceptGossip), Tag(ChannelStateTestsTags.ZeroConf)) { f => import f._ // For zero-conf channels we don't have a real short_channel_id when going to the NORMAL state. val aliceState = alice.stateData.asInstanceOf[DATA_NORMAL] @@ -3606,7 +3261,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val fundingTx = aliceState.commitments.latest.localFundingStatus.signedTx_opt.get val (blockHeight, txIndex) = (BlockHeight(400_000), 42) alice ! WatchFundingConfirmedTriggered(blockHeight, txIndex, fundingTx) - val realShortChannelId = RealShortChannelId(blockHeight, txIndex, alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.commitInput.outPoint.index.toInt) + val realShortChannelId = RealShortChannelId(blockHeight, txIndex, alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.fundingInput.index.toInt) val annSigsA = alice2bob.expectMsgType[AnnouncementSignatures] assert(annSigsA.shortChannelId == realShortChannelId) // Alice updates her internal state wih the real scid. @@ -3627,7 +3282,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(alice.stateData.asInstanceOf[DATA_NORMAL].channelUpdate.shortChannelId == realShortChannelId) } - test("recv WatchFundingConfirmedTriggered (private channel, zero-conf)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), Tag(ChannelStateTestsTags.ZeroConf)) { f => + test("recv WatchFundingConfirmedTriggered (private channel, zero-conf)", Tag(ChannelStateTestsTags.ZeroConf)) { f => import f._ // we create a new listener that registers after alice has published the funding tx val listener = TestProbe() @@ -3657,7 +3312,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val annSigsB = bob2alice.expectMsgType[AnnouncementSignatures] assert(annSigsB.shortChannelId == realShortChannelId) bob2alice.expectMsgType[ChannelUpdate] - val aliceFundingKey = Alice.channelKeyManager.fundingPublicKey(initialState.commitments.params.localParams.fundingKeyPath, fundingTxIndex = 0).publicKey + val aliceFundingKey = alice.underlyingActor.channelKeys.fundingKey(fundingTxIndex = 0).publicKey val bobFundingKey = initialState.commitments.latest.remoteFundingPubKey val channelAnn = Announcements.makeChannelAnnouncement(Alice.nodeParams.chainHash, annSigsA.shortChannelId, Alice.nodeParams.nodeId, Bob.nodeParams.nodeId, aliceFundingKey, bobFundingKey, annSigsA.nodeSignature, annSigsB.nodeSignature, annSigsA.bitcoinSignature, annSigsB.bitcoinSignature) // actual test starts here @@ -3694,7 +3349,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with bob2alice.forward(alice, annSigsB_invalid) alice2bob.expectMsg(Error(channelId, InvalidAnnouncementSignatures(channelId, annSigsB_invalid).getMessage)) alice2bob.forward(bob) - alice2bob.expectNoMessage(200 millis) + alice2bob.expectNoMessage(100 millis) awaitCond(alice.stateName == CLOSING) } @@ -3722,7 +3377,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // actual test starts here Thread.sleep(1100) alice ! BroadcastChannelUpdate(Reconnected) - channelUpdateListener.expectNoMessage(1 second) + channelUpdateListener.expectNoMessage(100 millis) } test("recv INPUT_DISCONNECTED") { f => @@ -3732,8 +3387,8 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // actual test starts here alice ! INPUT_DISCONNECTED awaitCond(alice.stateName == OFFLINE) - alice2bob.expectNoMessage(1 second) - channelUpdateListener.expectNoMessage(1 second) + alice2bob.expectNoMessage(100 millis) + channelUpdateListener.expectNoMessage(100 millis) } test("recv INPUT_DISCONNECTED (with pending unsigned htlcs)") { f => @@ -3760,6 +3415,80 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with awaitCond(alice.stateName == OFFLINE) } + test("recv INPUT_DISCONNECTED (with pending htlcs, taproot)", Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => + import f._ + + alice2bob.ignoreMsg { case _: ChannelUpdate => true } + bob2alice.ignoreMsg { case _: ChannelUpdate => true } + + // Alice sends an HTLC to Bob. + val (ra1, htlcA1) = addHtlc(15_000_000 msat, alice, bob, alice2bob, bob2alice) + // Bob sends an HTLC to Alice. + val (rb, htlcB) = addHtlc(25_000_000 msat, bob, alice, bob2alice, alice2bob) + bob ! CMD_SIGN() + bob2alice.expectMsgType[CommitSig] + bob2alice.forward(alice) + val revA1 = alice2bob.expectMsgType[RevokeAndAck] // not received by Bob + alice2bob.expectMsgType[CommitSig] // not received by Bob + val (_, htlcA2) = addHtlc(10_000_000 msat, alice, bob, alice2bob, bob2alice) // not signed by either Alice or Bob + + alice ! INPUT_DISCONNECTED + val addSettledA = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult]] + assert(addSettledA.htlc == htlcA2) + assert(addSettledA.result.isInstanceOf[HtlcResult.DisconnectedBeforeSigned]) + alice2relayer.expectNoMessage(100 millis) + awaitCond(alice.stateName == OFFLINE) + bob ! INPUT_DISCONNECTED + bob2relayer.expectNoMessage(100 millis) + awaitCond(bob.stateName == OFFLINE) + + // Alice and Bob finish signing the HTLCs on reconnection. + val aliceInit = Init(alice.underlyingActor.nodeParams.features.initFeatures()) + val bobInit = Init(bob.underlyingActor.nodeParams.features.initFeatures()) + alice ! INPUT_RECONNECTED(bob, aliceInit, bobInit) + bob ! INPUT_RECONNECTED(alice, bobInit, aliceInit) + assert(alice2bob.expectMsgType[ChannelReestablish].nextCommitNonces == revA1.nextCommitNonces) + alice2bob.forward(bob) + bob2alice.expectMsgType[ChannelReestablish] + bob2alice.forward(alice) + assert(alice2bob.expectMsgType[RevokeAndAck].nextCommitNonces == revA1.nextCommitNonces) + alice2bob.forward(bob) + assert(alice2bob.expectMsgType[UpdateAddHtlc].paymentHash == htlcA1.paymentHash) + alice2bob.forward(bob) + alice2bob.expectMsgType[CommitSig] + alice2bob.forward(bob) + bob2alice.expectMsgType[RevokeAndAck] + bob2alice.forward(alice) + bob2alice.expectMsgType[CommitSig] + bob2alice.forward(alice) + alice2bob.expectMsgType[RevokeAndAck] + alice2bob.forward(bob) + + // Alice and Bob fulfill the pending HTLCs. + fulfillHtlc(htlcA1.id, ra1, bob, alice, bob2alice, alice2bob) + fulfillHtlc(htlcB.id, rb, alice, bob, alice2bob, bob2alice) + crossSign(alice, bob, alice2bob, bob2alice) + } + + test("recv INPUT_DISCONNECTED (missing nonces, taproot)", Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => + import f._ + + alice ! INPUT_DISCONNECTED + awaitCond(alice.stateName == OFFLINE) + bob ! INPUT_DISCONNECTED + awaitCond(bob.stateName == OFFLINE) + + val aliceInit = Init(alice.underlyingActor.nodeParams.features.initFeatures()) + val bobInit = Init(bob.underlyingActor.nodeParams.features.initFeatures()) + alice ! INPUT_RECONNECTED(bob, aliceInit, bobInit) + bob ! INPUT_RECONNECTED(alice, bobInit, aliceInit) + val channelReestablish = alice2bob.expectMsgType[ChannelReestablish] + assert(channelReestablish.nextCommitNonces.size == 1) + bob2alice.expectMsgType[ChannelReestablish] + alice2bob.forward(bob, channelReestablish.copy(tlvStream = TlvStream(channelReestablish.tlvStream.records.filterNot(_.isInstanceOf[ChannelReestablishTlv.NextLocalNoncesTlv])))) + bob2alice.expectMsgType[Error] + } + test("recv INPUT_DISCONNECTED (public channel)", Tag(ChannelStateTestsTags.ChannelsPublic), Tag(ChannelStateTestsTags.DoNotInterceptGossip)) { f => import f._ bob2alice.expectMsgType[AnnouncementSignatures] @@ -3770,7 +3499,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // actual test starts here alice ! INPUT_DISCONNECTED awaitCond(alice.stateName == OFFLINE) - channelUpdateListener.expectNoMessage(1 second) + channelUpdateListener.expectNoMessage(100 millis) } test("recv INPUT_DISCONNECTED (public channel, with pending unsigned htlcs)", Tag(ChannelStateTestsTags.ChannelsPublic), Tag(ChannelStateTestsTags.DoNotInterceptGossip)) { f => diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/OfflineStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/OfflineStateSpec.scala index 7766558f80..08f7d96e18 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/OfflineStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/OfflineStateSpec.scala @@ -28,10 +28,12 @@ import fr.acinq.eclair.blockchain.fee.FeeratesPerKw import fr.acinq.eclair.blockchain.{CurrentBlockHeight, CurrentFeerates} import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.fsm.Channel -import fr.acinq.eclair.channel.publish.TxPublisher.{PublishFinalTx, PublishReplaceableTx, PublishTx} +import fr.acinq.eclair.channel.publish.TxPublisher.PublishFinalTx import fr.acinq.eclair.channel.states.ChannelStateTestsBase.PimpTestFSM import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags} -import fr.acinq.eclair.transactions.Transactions.{ClaimHtlcTimeoutTx, HtlcSuccessTx} +import fr.acinq.eclair.reputation.Reputation +import fr.acinq.eclair.testutils.PimpTestProbe.convert +import fr.acinq.eclair.transactions.Transactions._ import fr.acinq.eclair.wire.protocol._ import fr.acinq.eclair.{BlockHeight, CltvExpiry, CltvExpiryDelta, MilliSatoshiLong, TestConstants, TestKitBaseClass, TestUtils, randomBytes32} import org.scalatest.funsuite.FixtureAnyFunSuiteLike @@ -108,7 +110,7 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // |--- sig --X | // | | val sender = TestProbe() - alice ! CMD_ADD_HTLC(sender.ref, 1000000 msat, ByteVector32.Zeroes, CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, 1.0, None, localOrigin(sender.ref)) + alice ! CMD_ADD_HTLC(sender.ref, 1000000 msat, ByteVector32.Zeroes, CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, Reputation.Score.max, None, localOrigin(sender.ref)) val htlc = alice2bob.expectMsgType[UpdateAddHtlc] // bob receives the htlc alice2bob.forward(bob) @@ -140,8 +142,8 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice2bob.expectMsgType[RevokeAndAck] alice2bob.forward(bob) - alice2bob.expectNoMessage(500 millis) - bob2alice.expectNoMessage(500 millis) + alice2bob.expectNoMessage(100 millis) + bob2alice.expectNoMessage(100 millis) awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.changes.localNextHtlcId == 1) awaitCond(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.changes.remoteNextHtlcId == 1) @@ -158,7 +160,7 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // | X-- rev ---| // | X-- sig ---| val sender = TestProbe() - alice ! CMD_ADD_HTLC(ActorRef.noSender, 1000000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, 1.0, None, localOrigin(sender.ref)) + alice ! CMD_ADD_HTLC(ActorRef.noSender, 1000000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, Reputation.Score.max, None, localOrigin(sender.ref)) val htlc = alice2bob.expectMsgType[UpdateAddHtlc] // bob receives the htlc and the signature alice2bob.forward(bob, htlc) @@ -169,7 +171,7 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // bob received the signature, but alice won't receive the revocation val revB = bob2alice.expectMsgType[RevokeAndAck] val sigB = bob2alice.expectMsgType[CommitSig] - bob2alice.expectNoMessage(500 millis) + bob2alice.expectNoMessage(100 millis) disconnect(alice, bob) val (aliceCurrentPerCommitmentPoint, bobCurrentPerCommitmentPoint) = reconnect(alice, bob, alice2bob, bob2alice) @@ -184,8 +186,8 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // and a signature bob2alice.expectMsg(sigB) - alice2bob.expectNoMessage(500 millis) - bob2alice.expectNoMessage(500 millis) + alice2bob.expectNoMessage(100 millis) + bob2alice.expectNoMessage(100 millis) awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.changes.localNextHtlcId == 1) awaitCond(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.changes.remoteNextHtlcId == 1) @@ -202,7 +204,7 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // |<--- rev ---| // | X-- sig ---| val sender = TestProbe() - alice ! CMD_ADD_HTLC(ActorRef.noSender, 1000000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, 1.0, None, localOrigin(sender.ref)) + alice ! CMD_ADD_HTLC(ActorRef.noSender, 1000000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, Reputation.Score.max, None, localOrigin(sender.ref)) val htlc = alice2bob.expectMsgType[UpdateAddHtlc] // bob receives the htlc and the signature alice2bob.forward(bob, htlc) @@ -213,7 +215,7 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // bob sends a revocation and a signature val revB = bob2alice.expectMsgType[RevokeAndAck] val sigB = bob2alice.expectMsgType[CommitSig] - bob2alice.expectNoMessage(500 millis) + bob2alice.expectNoMessage(100 millis) // alice receives the revocation but not the signature bob2alice.forward(alice, revB) @@ -230,8 +232,8 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // bob re-sends the lost signature (but not the revocation) bob2alice.expectMsg(sigB) - alice2bob.expectNoMessage(500 millis) - bob2alice.expectNoMessage(500 millis) + alice2bob.expectNoMessage(100 millis) + bob2alice.expectNoMessage(100 millis) awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.changes.localNextHtlcId == 1) awaitCond(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.changes.remoteNextHtlcId == 1) @@ -246,27 +248,27 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // |<--- sig ---| // |--- rev --X | bob2alice.forward(alice, sigB) - bob2alice.expectNoMessage(500 millis) + bob2alice.expectNoMessage(100 millis) val revA = alice2bob.expectMsgType[RevokeAndAck] disconnect(alice, bob) { val (aliceCurrentPerCommitmentPoint, bobCurrentPerCommitmentPoint) = reconnect(alice, bob, alice2bob, bob2alice) - val reestablishA = alice2bob.expectMsg(ChannelReestablish(htlc.channelId, 2, 1, revB.perCommitmentSecret, aliceCurrentPerCommitmentPoint,TlvStream(lastFundingLockedTlvs(alice.stateData.asInstanceOf[DATA_NORMAL].commitments)))) - val reestablishB = bob2alice.expectMsg(ChannelReestablish(htlc.channelId, 2, 0, PrivateKey(ByteVector32.Zeroes), bobCurrentPerCommitmentPoint,TlvStream(lastFundingLockedTlvs(bob.stateData.asInstanceOf[DATA_NORMAL].commitments)))) + val reestablishA = alice2bob.expectMsg(ChannelReestablish(htlc.channelId, 2, 1, revB.perCommitmentSecret, aliceCurrentPerCommitmentPoint, TlvStream(lastFundingLockedTlvs(alice.stateData.asInstanceOf[DATA_NORMAL].commitments)))) + val reestablishB = bob2alice.expectMsg(ChannelReestablish(htlc.channelId, 2, 0, PrivateKey(ByteVector32.Zeroes), bobCurrentPerCommitmentPoint, TlvStream(lastFundingLockedTlvs(bob.stateData.asInstanceOf[DATA_NORMAL].commitments)))) alice2bob.forward(bob, reestablishA) bob2alice.forward(alice, reestablishB) } // alice re-sends the lost revocation alice2bob.expectMsg(revA) - alice2bob.expectNoMessage(500 millis) + alice2bob.expectNoMessage(100 millis) awaitCond(alice.stateName == NORMAL) awaitCond(bob.stateName == NORMAL) } - test("resume htlc settlement", Tag(IgnoreChannelUpdates)) { f => + test("resume htlc settlement", Tag(IgnoreChannelUpdates), Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => import f._ // Successfully send a first payment. @@ -285,7 +287,7 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice2bob.expectMsgType[CommitSig] alice2bob.forward(bob) val revB = bob2alice.expectMsgType[RevokeAndAck] - bob2alice.expectMsgType[CommitSig] + val bobCommitSig1 = bob2alice.expectMsgType[CommitSig] disconnect(alice, bob) reconnect(alice, bob, alice2bob, bob2alice) @@ -297,14 +299,16 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(reestablishB.nextRemoteRevocationNumber == 3) bob2alice.forward(alice, reestablishB) - // alice does not re-send messages bob already received + // Alice does not re-send messages that Bob already received. alice2bob.expectNoMessage(100 millis) alice2bob.forward(bob, reestablishA) - // bob re-sends its revocation and signature, alice then completes the update + // Bob re-sends its revocation and signature, Alice then completes the update. + // Note that Bob signs with a fresh nonce instead of retransmitting its previous signature, in case Alice changed her nonce. bob2alice.expectMsg(revB) bob2alice.forward(alice) - bob2alice.expectMsgType[CommitSig] + val bobCommitSig2 = bob2alice.expectMsgType[CommitSig] + assert(bobCommitSig2.sigOrPartialSig != bobCommitSig1.sigOrPartialSig) bob2alice.forward(alice) alice2bob.expectMsgType[RevokeAndAck] alice2bob.forward(bob) @@ -313,7 +317,7 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommitIndex == 4) } - test("reconnect with an outdated commitment", Tag(IgnoreChannelUpdates), Tag(ChannelStateTestsTags.StaticRemoteKey), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + test("reconnect with an outdated commitment", Tag(IgnoreChannelUpdates)) { f => import f._ val (ra1, htlca1) = addHtlc(250000000 msat, alice, bob, alice2bob, bob2alice) @@ -363,7 +367,7 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Transaction.correctlySpends(claimMainOutput, bobCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) } - test("reconnect with an outdated commitment (but counterparty can't tell)", Tag(IgnoreChannelUpdates), Tag(ChannelStateTestsTags.StaticRemoteKey), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + test("reconnect with an outdated commitment (but counterparty can't tell)", Tag(IgnoreChannelUpdates)) { f => import f._ // we start by storing the current state @@ -378,7 +382,7 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with bob2alice.expectMsgType[CommitSig] // we keep track of bob commitment tx for later - val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val bobCommitTx = bob.signCommitTx() // we simulate a disconnection disconnect(alice, bob) @@ -417,13 +421,13 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Transaction.correctlySpends(claimMainOutput, bobCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) } - test("counterparty lies about having a more recent commitment and publishes current commitment", Tag(IgnoreChannelUpdates), Tag(ChannelStateTestsTags.StaticRemoteKey), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + test("counterparty lies about having a more recent commitment and publishes current commitment", Tag(IgnoreChannelUpdates)) { f => import f._ // the current state contains a pending htlc addHtlc(250_000_000 msat, alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) - val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val bobCommitTx = bob.signCommitTx() // we simulate a disconnection followed by a reconnection disconnect(alice, bob) @@ -444,19 +448,18 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice ! WatchFundingSpentTriggered(bobCommitTx) // alice is able to claim her main output and the htlc (once it times out) - alice2blockchain.expectMsgType[PublishReplaceableTx].txInfo.tx + alice2blockchain.expectReplaceableTxPublished[ClaimRemoteAnchorTx] val claimMainOutput = alice2blockchain.expectMsgType[PublishFinalTx].tx Transaction.correctlySpends(claimMainOutput, bobCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - val claimHtlc = alice2blockchain.expectMsgType[PublishReplaceableTx] - assert(claimHtlc.txInfo.isInstanceOf[ClaimHtlcTimeoutTx]) - Transaction.correctlySpends(claimHtlc.txInfo.tx, bobCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + val claimHtlc = alice2blockchain.expectReplaceableTxPublished[ClaimHtlcTimeoutTx] + Transaction.correctlySpends(claimHtlc.sign(), bobCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) } - test("counterparty lies about having a more recent commitment and publishes revoked commitment", Tag(IgnoreChannelUpdates), Tag(ChannelStateTestsTags.StaticRemoteKey), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + test("counterparty lies about having a more recent commitment and publishes revoked commitment", Tag(IgnoreChannelUpdates)) { f => import f._ // we sign a new commitment to make sure the first one is revoked - val bobRevokedCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val bobRevokedCommitTx = bob.signCommitTx() addHtlc(250_000_000 msat, alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) @@ -534,7 +537,7 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with channelUpdateListener.expectNoMessage(300 millis) // we attempt to send a payment - alice ! CMD_ADD_HTLC(sender.ref, 4200 msat, randomBytes32(), CltvExpiry(123456), TestConstants.emptyOnionPacket, None, 1.0, None, localOrigin(sender.ref)) + alice ! CMD_ADD_HTLC(sender.ref, 4200 msat, randomBytes32(), CltvExpiry(123456), TestConstants.emptyOnionPacket, None, Reputation.Score.max, None, localOrigin(sender.ref)) sender.expectMsgType[RES_ADD_FAILED[ChannelUnavailable]] // alice will broadcast a new disabled channel_update @@ -550,7 +553,7 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with disconnect(alice, bob) // We simulate a pending fulfill - bob.underlyingActor.nodeParams.db.pendingCommands.addSettlementCommand(initialState.channelId, CMD_FULFILL_HTLC(htlc.id, r, commit = true)) + bob.underlyingActor.nodeParams.db.pendingCommands.addSettlementCommand(initialState.channelId, CMD_FULFILL_HTLC(htlc.id, r, None, commit = true)) // then we reconnect them reconnect(alice, bob, alice2bob, bob2alice) @@ -581,7 +584,7 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with disconnect(alice, bob) // We simulate a pending fulfill - bob.underlyingActor.nodeParams.db.pendingCommands.addSettlementCommand(initialState.channelId, CMD_FULFILL_HTLC(htlc.id, r, commit = true)) + bob.underlyingActor.nodeParams.db.pendingCommands.addSettlementCommand(initialState.channelId, CMD_FULFILL_HTLC(htlc.id, r, None, commit = true)) // then we reconnect them reconnect(alice, bob, alice2bob, bob2alice) @@ -604,95 +607,73 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with test("pending non-relayed fulfill htlcs will timeout upstream") { f => import f._ - val (r, htlc) = addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) + val (r, htlc) = addHtlc(50_000_000 msat, alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) val listener = TestProbe() bob.underlying.system.eventStream.subscribe(listener.ref, classOf[ChannelErrorOccurred]) val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] - val initialCommitTx = initialState.commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx - val HtlcSuccessTx(_, htlcSuccessTx, _, _, _) = initialState.commitments.latest.localCommit.htlcTxsAndRemoteSigs.head.htlcTx + val initialCommitTx = bob.signCommitTx() + val htlcSuccessTx = bob.htlcTxs().head + assert(htlcSuccessTx.isInstanceOf[UnsignedHtlcSuccessTx]) disconnect(alice, bob) // We simulate a pending fulfill on that HTLC but not relayed. // When it is close to expiring upstream, we should close the channel. - bob.underlyingActor.nodeParams.db.pendingCommands.addSettlementCommand(initialState.channelId, CMD_FULFILL_HTLC(htlc.id, r, commit = true)) + bob.underlyingActor.nodeParams.db.pendingCommands.addSettlementCommand(initialState.channelId, CMD_FULFILL_HTLC(htlc.id, r, None, commit = true)) bob ! CurrentBlockHeight(htlc.cltvExpiry.blockHeight - bob.underlyingActor.nodeParams.channelConf.fulfillSafetyBeforeTimeout.toInt) val ChannelErrorOccurred(_, _, _, LocalError(err), isFatal) = listener.expectMsgType[ChannelErrorOccurred] assert(isFatal) assert(err.isInstanceOf[HtlcsWillTimeoutUpstream]) - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == initialCommitTx.txid) - bob2blockchain.expectMsgType[PublishTx] // main delayed - assert(bob2blockchain.expectMsgType[WatchTxConfirmed].txId == initialCommitTx.txid) - bob2blockchain.expectMsgType[WatchTxConfirmed] // main delayed - bob2blockchain.expectMsgType[WatchOutputSpent] // htlc - - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == initialCommitTx.txid) - bob2blockchain.expectMsgType[PublishTx] // main delayed - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txOut == htlcSuccessTx.txOut) - assert(bob2blockchain.expectMsgType[WatchTxConfirmed].txId == initialCommitTx.txid) - bob2blockchain.expectMsgType[WatchTxConfirmed] // main delayed - bob2blockchain.expectMsgType[WatchOutputSpent] // htlc - bob2blockchain.expectNoMessage(500 millis) + bob2blockchain.expectFinalTxPublished(initialCommitTx.txid) + val anchorTx = bob2blockchain.expectReplaceableTxPublished[ClaimLocalAnchorTx] + val mainDelayedTx = bob2blockchain.expectFinalTxPublished("local-main-delayed") + bob2blockchain.expectWatchTxConfirmed(initialCommitTx.txid) + bob2blockchain.expectWatchOutputsSpent(Seq(mainDelayedTx.input, anchorTx.input.outPoint, htlcSuccessTx.input.outPoint)) + assert(bob2blockchain.expectReplaceableTxPublished[HtlcSuccessTx].input == htlcSuccessTx.input) + bob2blockchain.expectNoMessage(100 millis) } test("pending non-relayed fail htlcs will timeout upstream") { f => import f._ - val (_, htlc) = addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) + val (_, htlc) = addHtlc(50_000_000 msat, alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) disconnect(alice, bob) // We simulate a pending failure on that HTLC. // Even if we get close to expiring upstream we shouldn't close the channel, because we have nothing to lose. - bob ! CMD_FAIL_HTLC(htlc.id, FailureReason.LocalFailure(IncorrectOrUnknownPaymentDetails(0 msat, BlockHeight(0)))) + bob ! CMD_FAIL_HTLC(htlc.id, FailureReason.LocalFailure(IncorrectOrUnknownPaymentDetails(0 msat, BlockHeight(0))), None) bob ! CurrentBlockHeight(htlc.cltvExpiry.blockHeight - bob.underlyingActor.nodeParams.channelConf.fulfillSafetyBeforeTimeout.toInt) - - bob2blockchain.expectNoMessage(250 millis) - alice2blockchain.expectNoMessage(250 millis) - } - - test("handle feerate changes while offline (funder scenario)") { f => - import f._ - - // we only close channels on feerate mismatch if there are HTLCs at risk in the commitment - addHtlc(125000000 msat, alice, bob, alice2bob, bob2alice) - crossSign(alice, bob, alice2bob, bob2alice) - - testHandleFeerateFunder(f, shouldClose = true) + bob2blockchain.expectNoMessage(100 millis) + alice2blockchain.expectNoMessage(100 millis) } test("handle feerate changes while offline without HTLCs (funder scenario)") { f => - testHandleFeerateFunder(f, shouldClose = false) + testHandleFeerateFunder(f) } - def testHandleFeerateFunder(f: FixtureParam, shouldClose: Boolean): Unit = { + def testHandleFeerateFunder(f: FixtureParam): Unit = { import f._ // we simulate a disconnection disconnect(alice, bob) - val aliceStateData = alice.stateData.asInstanceOf[DATA_NORMAL] - val aliceCommitTx = aliceStateData.commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx - - val currentFeeratePerKw = aliceStateData.commitments.latest.localCommit.spec.commitTxFeerate + val aliceCommitTx = alice.signCommitTx() + val currentFeerate = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.spec.commitTxFeerate // we receive a feerate update that makes our current feerate too low compared to the network's (we multiply by 1.1 // to ensure the network's feerate is 10% above our threshold). - val networkFeeratePerKw = currentFeeratePerKw * (1.1 / alice.underlyingActor.nodeParams.onChainFeeConf.feerateToleranceFor(Bob.nodeParams.nodeId).ratioLow) - val networkFeerates = FeeratesPerKw.single(networkFeeratePerKw) + val networkFeerate = currentFeerate * (1.1 / alice.underlyingActor.nodeParams.onChainFeeConf.feerateToleranceFor(Bob.nodeParams.nodeId).ratioLow) + val networkFeerates = FeeratesPerKw.single(networkFeerate) // alice is funder alice.setBitcoinCoreFeerates(networkFeerates) alice ! CurrentFeerates.BitcoinCore(networkFeerates) - if (shouldClose) { - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == aliceCommitTx.txid) - } else { - alice2blockchain.expectNoMessage(100 millis) - } + alice2blockchain.expectNoMessage(100 millis) } test("handle feerate changes while offline (don't close on mismatch)", Tag(DisableOfflineMismatch)) { f => @@ -719,92 +700,6 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice2bob.expectNoMessage(100 millis) } - def testUpdateFeeOnReconnect(f: FixtureParam, shouldUpdateFee: Boolean): Unit = { - import f._ - - // we simulate a disconnection - disconnect(alice, bob) - - val localFeeratePerKw = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.spec.commitTxFeerate - val networkFeeratePerKw = localFeeratePerKw * 2 - val networkFeerates = FeeratesPerKw.single(networkFeeratePerKw) - - // Alice ignores feerate changes while offline - alice.setBitcoinCoreFeerates(networkFeerates) - alice ! CurrentFeerates.BitcoinCore(networkFeerates) - alice2blockchain.expectNoMessage(100 millis) - alice2bob.expectNoMessage(100 millis) - - // then we reconnect them; Alice should send the feerate changes to Bob - reconnect(alice, bob, alice2bob, bob2alice) - - // peers exchange channel_reestablish messages - alice2bob.expectMsgType[ChannelReestablish] - bob2alice.expectMsgType[ChannelReestablish] - bob2alice.forward(alice) - - if (shouldUpdateFee) { - alice2bob.expectMsg(UpdateFee(channelId(alice), networkFeeratePerKw)) - } else { - alice2bob.expectMsgType[Shutdown] - alice2bob.expectNoMessage(100 millis) - } - } - - test("handle feerate changes while offline (update at reconnection)", Tag(IgnoreChannelUpdates)) { f => - testUpdateFeeOnReconnect(f, shouldUpdateFee = true) - } - - test("handle feerate changes while offline (shutdown sent, don't update at reconnection)", Tag(IgnoreChannelUpdates)) { f => - import f._ - - // alice initiates a shutdown - val sender = TestProbe() - alice ! CMD_CLOSE(sender.ref, None, None) - alice2bob.expectMsgType[Shutdown] - - testUpdateFeeOnReconnect(f, shouldUpdateFee = false) - } - - test("handle feerate changes while offline (fundee scenario)") { f => - import f._ - - // we only close channels on feerate mismatch if there are HTLCs at risk in the commitment - addHtlc(125000000 msat, alice, bob, alice2bob, bob2alice) - crossSign(alice, bob, alice2bob, bob2alice) - - testHandleFeerateFundee(f, shouldClose = true) - } - - test("handle feerate changes while offline without HTLCs (fundee scenario)") { f => - testHandleFeerateFundee(f, shouldClose = false) - } - - def testHandleFeerateFundee(f: FixtureParam, shouldClose: Boolean): Unit = { - import f._ - - // we simulate a disconnection - disconnect(alice, bob) - - val bobStateData = bob.stateData.asInstanceOf[DATA_NORMAL] - val bobCommitTx = bobStateData.commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx - - val currentFeeratePerKw = bobStateData.commitments.latest.localCommit.spec.commitTxFeerate - // we receive a feerate update that makes our current feerate too low compared to the network's (we multiply by 1.1 - // to ensure the network's feerate is 10% above our threshold). - val networkFeeratePerKw = currentFeeratePerKw * (1.1 / bob.underlyingActor.nodeParams.onChainFeeConf.feerateToleranceFor(Alice.nodeParams.nodeId).ratioLow) - val networkFeerates = FeeratesPerKw.single(networkFeeratePerKw) - - // bob is fundee - bob.setBitcoinCoreFeerates(networkFeerates) - bob ! CurrentFeerates.BitcoinCore(networkFeerates) - if (shouldClose) { - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == bobCommitTx.txid) - } else { - bob2blockchain.expectNoMessage(100 millis) - } - } - test("re-send announcement_signatures on reconnection", Tag(ChannelStateTestsTags.ChannelsPublic), Tag(ChannelStateTestsTags.DoNotInterceptGossip)) { f => import f._ @@ -920,14 +815,10 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with bob ! INPUT_RECONNECTED(bob2alice.ref, bobInit, aliceInit) val aliceCommitments = alice.stateData.asInstanceOf[ChannelDataWithCommitments].commitments - val aliceCurrentPerCommitmentPoint = TestConstants.Alice.channelKeyManager.commitmentPoint( - TestConstants.Alice.channelKeyManager.keyPath(aliceCommitments.params.localParams, aliceCommitments.params.channelConfig), - aliceCommitments.localCommitIndex) + val aliceCurrentPerCommitmentPoint = alice.underlyingActor.channelKeys.commitmentPoint(aliceCommitments.localCommitIndex) val bobCommitments = bob.stateData.asInstanceOf[ChannelDataWithCommitments].commitments - val bobCurrentPerCommitmentPoint = TestConstants.Bob.channelKeyManager.commitmentPoint( - TestConstants.Bob.channelKeyManager.keyPath(bobCommitments.params.localParams, bobCommitments.params.channelConfig), - bobCommitments.localCommitIndex) + val bobCurrentPerCommitmentPoint = bob.underlyingActor.channelKeys.commitmentPoint(bobCommitments.localCommitIndex) (aliceCurrentPerCommitmentPoint, bobCurrentPerCommitmentPoint) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/f/ShutdownStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/f/ShutdownStateSpec.scala index 4ac576cab6..6628813716 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/f/ShutdownStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/f/ShutdownStateSpec.scala @@ -24,15 +24,18 @@ import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, Crypto, Satoshi import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ import fr.acinq.eclair.blockchain.fee.{FeeratePerKw, FeeratesPerKw} import fr.acinq.eclair.blockchain.{CurrentBlockHeight, CurrentFeerates} +import fr.acinq.eclair.channel.ChannelSpendSignature.IndividualSignature import fr.acinq.eclair.channel._ -import fr.acinq.eclair.channel.publish.TxPublisher.{PublishFinalTx, PublishReplaceableTx, PublishTx} +import fr.acinq.eclair.channel.publish.TxPublisher.{PublishFinalTx, SetChannelId} import fr.acinq.eclair.channel.states.ChannelStateTestsBase.PimpTestFSM import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags} import fr.acinq.eclair.payment._ import fr.acinq.eclair.payment.relay.Relayer._ import fr.acinq.eclair.payment.send.SpontaneousRecipient -import fr.acinq.eclair.transactions.Transactions.ClaimLocalAnchorOutputTx -import fr.acinq.eclair.wire.protocol.{AnnouncementSignatures, ChannelUpdate, ClosingSigned, CommitSig, Error, FailureMessageCodecs, FailureReason, PermanentChannelFailure, RevokeAndAck, Shutdown, UpdateAddHtlc, UpdateFailHtlc, UpdateFailMalformedHtlc, UpdateFee, UpdateFulfillHtlc} +import fr.acinq.eclair.reputation.Reputation +import fr.acinq.eclair.testutils.PimpTestProbe.convert +import fr.acinq.eclair.transactions.Transactions._ +import fr.acinq.eclair.wire.protocol.{AnnouncementSignatures, ChannelReestablish, ChannelUpdate, ClosingComplete, ClosingSig, ClosingSigned, CommitSig, Error, FailureMessageCodecs, FailureReason, Init, PermanentChannelFailure, RevokeAndAck, Shutdown, UpdateAddHtlc, UpdateFailHtlc, UpdateFailMalformedHtlc, UpdateFee, UpdateFulfillHtlc} import fr.acinq.eclair.{BlockHeight, CltvExpiry, CltvExpiryDelta, MilliSatoshiLong, TestConstants, TestKitBaseClass, randomBytes32, randomKey} import org.scalatest.funsuite.FixtureAnyFunSuiteLike import org.scalatest.{Outcome, Tag} @@ -48,8 +51,8 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit type FixtureParam = SetupFixture - val r1 = randomBytes32() - val r2 = randomBytes32() + val r1: ByteVector32 = randomBytes32() + val r2: ByteVector32 = randomBytes32() override def withFixture(test: OneArgTest): Outcome = { val setup = init(tags = test.tags) @@ -60,7 +63,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit // alice sends an HTLC to bob val h1 = Crypto.sha256(r1) val recipient1 = SpontaneousRecipient(TestConstants.Bob.nodeParams.nodeId, 300_000_000 msat, CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), r1) - val Right(cmd1) = OutgoingPaymentPacket.buildOutgoingPayment(localOrigin(sender.ref), h1, makeSingleHopRoute(recipient1.totalAmount, recipient1.nodeId), recipient1, 1.0).map(_.cmd.copy(commit = false)) + val Right(cmd1) = OutgoingPaymentPacket.buildOutgoingPayment(localOrigin(sender.ref), h1, makeSingleHopRoute(recipient1.totalAmount, recipient1.nodeId), recipient1, Reputation.Score.max).map(_.cmd.copy(commit = false)) alice ! cmd1 sender.expectMsgType[RES_SUCCESS[CMD_ADD_HTLC]] val htlc1 = alice2bob.expectMsgType[UpdateAddHtlc] @@ -69,7 +72,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit // alice sends another HTLC to bob val h2 = Crypto.sha256(r2) val recipient2 = SpontaneousRecipient(TestConstants.Bob.nodeParams.nodeId, 200_000_000 msat, CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), r2) - val Right(cmd2) = OutgoingPaymentPacket.buildOutgoingPayment(localOrigin(sender.ref), h2, makeSingleHopRoute(recipient2.totalAmount, recipient2.nodeId), recipient2, 1.0).map(_.cmd.copy(commit = false)) + val Right(cmd2) = OutgoingPaymentPacket.buildOutgoingPayment(localOrigin(sender.ref), h2, makeSingleHopRoute(recipient2.totalAmount, recipient2.nodeId), recipient2, Reputation.Score.max).map(_.cmd.copy(commit = false)) alice ! cmd2 sender.expectMsgType[RES_SUCCESS[CMD_ADD_HTLC]] val htlc2 = alice2bob.expectMsgType[UpdateAddHtlc] @@ -112,7 +115,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit systemA.eventStream.subscribe(aliceListener.ref, classOf[LocalChannelUpdate]) val bobListener = TestProbe() systemB.eventStream.subscribe(bobListener.ref, classOf[LocalChannelUpdate]) - + alice2bob.expectMsgType[AnnouncementSignatures] alice2bob.forward(bob) alice2bob.expectMsgType[ChannelUpdate] @@ -141,7 +144,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit test("recv CMD_ADD_HTLC") { f => import f._ val sender = TestProbe() - val add = CMD_ADD_HTLC(sender.ref, 500000000 msat, r1, cltvExpiry = CltvExpiry(300000), TestConstants.emptyOnionPacket, None, 1.0, None, localOrigin(sender.ref)) + val add = CMD_ADD_HTLC(sender.ref, 500000000 msat, r1, cltvExpiry = CltvExpiry(300000), TestConstants.emptyOnionPacket, None, Reputation.Score.max, None, localOrigin(sender.ref)) alice ! add val error = ChannelUnavailable(channelId(alice)) sender.expectMsg(RES_ADD_FAILED(add, error, None)) @@ -151,18 +154,25 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit test("recv CMD_FULFILL_HTLC") { f => import f._ val initialState = bob.stateData.asInstanceOf[DATA_SHUTDOWN] - bob ! CMD_FULFILL_HTLC(0, r1) + bob ! CMD_FULFILL_HTLC(0, r1, None) val fulfill = bob2alice.expectMsgType[UpdateFulfillHtlc] - awaitCond(bob.stateData == initialState - .modify(_.commitments.changes.localChanges.proposed).using(_ :+ fulfill) + awaitCond(bob.stateData == initialState.modify(_.commitments.changes.localChanges.proposed).using(_ :+ fulfill) ) } + test("recv CMD_FULFILL_HTLC (taproot)", Tag(ChannelStateTestsTags.SimpleClose), Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => + import f._ + val initialState = bob.stateData.asInstanceOf[DATA_SHUTDOWN] + bob ! CMD_FULFILL_HTLC(0, r1, None) + val fulfill = bob2alice.expectMsgType[UpdateFulfillHtlc] + awaitCond(bob.stateData == initialState.modify(_.commitments.changes.localChanges.proposed).using(_ :+ fulfill)) + } + test("recv CMD_FULFILL_HTLC (unknown htlc id)") { f => import f._ val sender = TestProbe() val initialState = bob.stateData.asInstanceOf[DATA_SHUTDOWN] - bob ! CMD_FULFILL_HTLC(42, randomBytes32(), replyTo_opt = Some(sender.ref)) + bob ! CMD_FULFILL_HTLC(42, randomBytes32(), None, replyTo_opt = Some(sender.ref)) sender.expectMsgType[RES_FAILURE[CMD_FULFILL_HTLC, UnknownHtlcId]] assert(initialState == bob.stateData) } @@ -171,7 +181,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit import f._ val sender = TestProbe() val initialState = bob.stateData.asInstanceOf[DATA_SHUTDOWN] - val c = CMD_FULFILL_HTLC(1, ByteVector32.Zeroes, replyTo_opt = Some(sender.ref)) + val c = CMD_FULFILL_HTLC(1, ByteVector32.Zeroes, None, replyTo_opt = Some(sender.ref)) bob ! c sender.expectMsg(RES_FAILURE(c, InvalidHtlcPreimage(channelId(bob), 1))) @@ -184,7 +194,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit // actual test begins val initialState = bob.stateData.asInstanceOf[DATA_SHUTDOWN] - val c = CMD_FULFILL_HTLC(0, r1, replyTo_opt = Some(sender.ref)) + val c = CMD_FULFILL_HTLC(0, r1, None, replyTo_opt = Some(sender.ref)) // this would be done automatically when the relayer calls safeSend bob.underlyingActor.nodeParams.db.pendingCommands.addSettlementCommand(initialState.channelId, c) bob ! c @@ -199,7 +209,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit val sender = TestProbe() val initialState = bob.stateData.asInstanceOf[DATA_SHUTDOWN] - val c = CMD_FULFILL_HTLC(42, randomBytes32(), replyTo_opt = Some(sender.ref)) + val c = CMD_FULFILL_HTLC(42, randomBytes32(), None, replyTo_opt = Some(sender.ref)) sender.send(bob, c) // this will fail sender.expectMsg(RES_FAILURE(c, UnknownHtlcId(channelId(bob), 42))) awaitCond(bob.underlyingActor.nodeParams.db.pendingCommands.listSettlementCommands(initialState.channelId).isEmpty) @@ -215,35 +225,27 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit test("recv UpdateFulfillHtlc (unknown htlc id)") { f => import f._ - val tx = alice.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val tx = alice.signCommitTx() val fulfill = UpdateFulfillHtlc(ByteVector32.Zeroes, 42, ByteVector32.Zeroes) alice ! fulfill alice2bob.expectMsgType[Error] awaitCond(alice.stateName == CLOSING) - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) // commit tx - alice2blockchain.expectMsgType[PublishTx] // main delayed - alice2blockchain.expectMsgType[PublishTx] // htlc timeout 1 - alice2blockchain.expectMsgType[PublishTx] // htlc timeout 2 - alice2blockchain.expectMsgType[WatchTxConfirmed] + alice2blockchain.expectFinalTxPublished(tx.txid) } test("recv UpdateFulfillHtlc (invalid preimage)") { f => import f._ - val tx = alice.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val tx = alice.signCommitTx() alice ! UpdateFulfillHtlc(ByteVector32.Zeroes, 42, ByteVector32.Zeroes) alice2bob.expectMsgType[Error] awaitCond(alice.stateName == CLOSING) - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) // commit tx - alice2blockchain.expectMsgType[PublishTx] // main delayed - alice2blockchain.expectMsgType[PublishTx] // htlc timeout 1 - alice2blockchain.expectMsgType[PublishTx] // htlc timeout 2 - alice2blockchain.expectMsgType[WatchTxConfirmed] + alice2blockchain.expectFinalTxPublished(tx.txid) } test("recv CMD_FAIL_HTLC") { f => import f._ val initialState = bob.stateData.asInstanceOf[DATA_SHUTDOWN] - bob ! CMD_FAIL_HTLC(1, FailureReason.LocalFailure(PermanentChannelFailure())) + bob ! CMD_FAIL_HTLC(1, FailureReason.LocalFailure(PermanentChannelFailure()), None) val fail = bob2alice.expectMsgType[UpdateFailHtlc] awaitCond(bob.stateData == initialState .modify(_.commitments.changes.localChanges.proposed).using(_ :+ fail) @@ -254,7 +256,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit import f._ val sender = TestProbe() val initialState = bob.stateData.asInstanceOf[DATA_SHUTDOWN] - val c = CMD_FAIL_HTLC(42, FailureReason.LocalFailure(PermanentChannelFailure()), replyTo_opt = Some(sender.ref)) + val c = CMD_FAIL_HTLC(42, FailureReason.LocalFailure(PermanentChannelFailure()), None, replyTo_opt = Some(sender.ref)) bob ! c sender.expectMsg(RES_FAILURE(c, UnknownHtlcId(channelId(bob), 42))) assert(initialState == bob.stateData) @@ -264,7 +266,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit import f._ val sender = TestProbe() val initialState = bob.stateData.asInstanceOf[DATA_SHUTDOWN] - val c = CMD_FAIL_HTLC(42, FailureReason.LocalFailure(PermanentChannelFailure()), replyTo_opt = Some(sender.ref)) + val c = CMD_FAIL_HTLC(42, FailureReason.LocalFailure(PermanentChannelFailure()), None, replyTo_opt = Some(sender.ref)) sender.send(bob, c) // this will fail sender.expectMsg(RES_FAILURE(c, UnknownHtlcId(channelId(bob), 42))) awaitCond(bob.underlyingActor.nodeParams.db.pendingCommands.listSettlementCommands(initialState.channelId).isEmpty) @@ -320,14 +322,15 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit test("recv UpdateFailHtlc (unknown htlc id)") { f => import f._ - val tx = alice.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val tx = alice.signCommitTx() alice ! UpdateFailHtlc(ByteVector32.Zeroes, 42, ByteVector.fill(152)(0)) alice2bob.expectMsgType[Error] awaitCond(alice.stateName == CLOSING) - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) // commit tx - alice2blockchain.expectMsgType[PublishTx] // main delayed - alice2blockchain.expectMsgType[PublishTx] // htlc timeout 1 - alice2blockchain.expectMsgType[PublishTx] // htlc timeout 2 + alice2blockchain.expectFinalTxPublished(tx.txid) + alice2blockchain.expectReplaceableTxPublished[ClaimLocalAnchorTx] + alice2blockchain.expectFinalTxPublished("local-main-delayed") + alice2blockchain.expectReplaceableTxPublished[HtlcTimeoutTx] + alice2blockchain.expectReplaceableTxPublished[HtlcTimeoutTx] alice2blockchain.expectMsgType[WatchTxConfirmed] } @@ -341,24 +344,20 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit test("recv UpdateFailMalformedHtlc (invalid failure_code)") { f => import f._ - val tx = alice.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val tx = alice.signCommitTx() val fail = UpdateFailMalformedHtlc(ByteVector32.Zeroes, 1, Crypto.sha256(ByteVector.empty), 42) alice ! fail val error = alice2bob.expectMsgType[Error] assert(new String(error.data.toArray) == InvalidFailureCode(ByteVector32.Zeroes).getMessage) awaitCond(alice.stateName == CLOSING) - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) // commit tx - alice2blockchain.expectMsgType[PublishTx] // main delayed - alice2blockchain.expectMsgType[PublishTx] // htlc timeout 1 - alice2blockchain.expectMsgType[PublishTx] // htlc timeout 2 - alice2blockchain.expectMsgType[WatchTxConfirmed] + alice2blockchain.expectFinalTxPublished(tx.txid) } test("recv CMD_SIGN") { f => import f._ val sender = TestProbe() // we need to have something to sign so we first send a fulfill and acknowledge (=sign) it - bob ! CMD_FULFILL_HTLC(0, r1) + bob ! CMD_FULFILL_HTLC(0, r1, None) bob2alice.expectMsgType[UpdateFulfillHtlc] bob2alice.forward(alice) bob ! CMD_SIGN(replyTo_opt = Some(sender.ref)) @@ -371,18 +370,34 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit awaitCond(alice.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.remoteNextCommitInfo.isLeft) } + test("recv CMD_SIGN (taproot)", Tag(ChannelStateTestsTags.SimpleClose), Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => + import f._ + val sender = TestProbe() + bob ! CMD_FULFILL_HTLC(0, r1, None) + bob2alice.expectMsgType[UpdateFulfillHtlc] + bob2alice.forward(alice) + bob ! CMD_SIGN(replyTo_opt = Some(sender.ref)) + sender.expectMsgType[RES_SUCCESS[CMD_SIGN]] + assert(bob2alice.expectMsgType[CommitSig].partialSignature_opt.nonEmpty) + bob2alice.forward(alice) + assert(alice2bob.expectMsgType[RevokeAndAck].nextCommitNonces.contains(bob.commitments.latest.fundingTxId)) + alice2bob.forward(bob) + assert(alice2bob.expectMsgType[CommitSig].partialSignature_opt.nonEmpty) + awaitCond(alice.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.remoteNextCommitInfo.isLeft) + } + test("recv CMD_SIGN (no changes)") { f => import f._ val sender = TestProbe() alice ! CMD_SIGN(replyTo_opt = Some(sender.ref)) - sender.expectNoMessage(1 second) // just ignored + sender.expectNoMessage(100 millis) // just ignored //sender.expectMsg("cannot sign when there are no changes") } test("recv CMD_SIGN (while waiting for RevokeAndAck)") { f => import f._ val sender = TestProbe() - bob ! CMD_FULFILL_HTLC(0, r1) + bob ! CMD_FULFILL_HTLC(0, r1, None) bob2alice.expectMsgType[UpdateFulfillHtlc] bob ! CMD_SIGN(replyTo_opt = Some(sender.ref)) sender.expectMsgType[RES_SUCCESS[CMD_SIGN]] @@ -398,7 +413,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit test("recv CommitSig") { f => import f._ - bob ! CMD_FULFILL_HTLC(0, r1) + bob ! CMD_FULFILL_HTLC(0, r1, None) bob2alice.expectMsgType[UpdateFulfillHtlc] bob2alice.forward(alice) bob ! CMD_SIGN() @@ -407,27 +422,13 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit alice2bob.expectMsgType[RevokeAndAck] } - test("recv CommitSig (no changes)") { f => - import f._ - val tx = bob.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx - // signature is invalid but it doesn't matter - bob ! CommitSig(ByteVector32.Zeroes, ByteVector64.Zeroes, Nil) - bob2alice.expectMsgType[Error] - awaitCond(bob.stateName == CLOSING) - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) // commit tx - bob2blockchain.expectMsgType[PublishTx] // main delayed - bob2blockchain.expectMsgType[WatchTxConfirmed] - } - test("recv CommitSig (invalid signature)") { f => import f._ - val tx = bob.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx - bob ! CommitSig(ByteVector32.Zeroes, ByteVector64.Zeroes, Nil) + val tx = bob.signCommitTx() + bob ! CommitSig(ByteVector32.Zeroes, IndividualSignature(ByteVector64.Zeroes), Nil) bob2alice.expectMsgType[Error] awaitCond(bob.stateName == CLOSING) - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) // commit tx - bob2blockchain.expectMsgType[PublishTx] // main delayed - bob2blockchain.expectMsgType[WatchTxConfirmed] + bob2blockchain.expectFinalTxPublished(tx.txid) } test("recv RevokeAndAck (with remaining htlcs on both sides)") { f => @@ -467,10 +468,23 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit awaitCond(alice.stateName == NEGOTIATING) } + test("recv RevokeAndAck (no more htlcs on either side, taproot)", Tag(ChannelStateTestsTags.SimpleClose), Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => + import f._ + // Bob fulfills the first HTLC. + fulfillHtlc(0, r1, bob, alice, bob2alice, alice2bob) + crossSign(bob, alice, bob2alice, alice2bob) + assert(alice.stateName == SHUTDOWN) + // Bob fulfills the second HTLC. + fulfillHtlc(1, r2, bob, alice, bob2alice, alice2bob) + crossSign(bob, alice, bob2alice, alice2bob) + awaitCond(alice.stateName == NEGOTIATING_SIMPLE) + awaitCond(bob.stateName == NEGOTIATING_SIMPLE) + } + test("recv RevokeAndAck (invalid preimage)") { f => import f._ - val tx = bob.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx - bob ! CMD_FULFILL_HTLC(0, r1) + val tx = bob.signCommitTx() + bob ! CMD_FULFILL_HTLC(0, r1, None) bob2alice.expectMsgType[UpdateFulfillHtlc] bob2alice.forward(alice) bob ! CMD_SIGN() @@ -481,29 +495,22 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit bob ! RevokeAndAck(ByteVector32.Zeroes, PrivateKey(randomBytes32()), PrivateKey(randomBytes32()).publicKey) bob2alice.expectMsgType[Error] awaitCond(bob.stateName == CLOSING) - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) // commit tx - bob2blockchain.expectMsgType[PublishTx] // main delayed - bob2blockchain.expectMsgType[PublishTx] // htlc success - bob2blockchain.expectMsgType[WatchTxConfirmed] + bob2blockchain.expectFinalTxPublished(tx.txid) } test("recv RevokeAndAck (unexpectedly)") { f => import f._ - val tx = alice.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val tx = alice.signCommitTx() awaitCond(alice.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.remoteNextCommitInfo.isRight) alice ! RevokeAndAck(ByteVector32.Zeroes, PrivateKey(randomBytes32()), PrivateKey(randomBytes32()).publicKey) alice2bob.expectMsgType[Error] awaitCond(alice.stateName == CLOSING) - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) // commit tx - alice2blockchain.expectMsgType[PublishTx] // main delayed - alice2blockchain.expectMsgType[PublishTx] // htlc timeout 1 - alice2blockchain.expectMsgType[PublishTx] // htlc timeout 2 - alice2blockchain.expectMsgType[WatchTxConfirmed] + alice2blockchain.expectFinalTxPublished(tx.txid) } test("recv RevokeAndAck (forward UpdateFailHtlc)") { f => import f._ - bob ! CMD_FAIL_HTLC(1, FailureReason.LocalFailure(PermanentChannelFailure())) + bob ! CMD_FAIL_HTLC(1, FailureReason.LocalFailure(PermanentChannelFailure()), None) val fail = bob2alice.expectMsgType[UpdateFailHtlc] bob2alice.forward(alice) bob ! CMD_SIGN() @@ -571,7 +578,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit test("recv UpdateFee") { f => import f._ val initialData = bob.stateData.asInstanceOf[DATA_SHUTDOWN] - val fee = UpdateFee(ByteVector32.Zeroes, FeeratePerKw(12000 sat)) + val fee = UpdateFee(ByteVector32.Zeroes, TestConstants.anchorOutputsFeeratePerKw * 1.2) bob ! fee awaitCond(bob.stateData == initialData .modify(_.commitments.changes.remoteChanges.proposed).using(_ :+ fee) @@ -580,54 +587,31 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit test("recv UpdateFee (when sender is not funder)") { f => import f._ - val tx = alice.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx - alice ! UpdateFee(ByteVector32.Zeroes, FeeratePerKw(12000 sat)) + val tx = alice.signCommitTx() + alice ! UpdateFee(ByteVector32.Zeroes, TestConstants.anchorOutputsFeeratePerKw * 1.2) alice2bob.expectMsgType[Error] awaitCond(alice.stateName == CLOSING) - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) // commit tx - alice2blockchain.expectMsgType[PublishTx] // main delayed - alice2blockchain.expectMsgType[PublishTx] // htlc timeout 1 - alice2blockchain.expectMsgType[PublishTx] // htlc timeout 2 - alice2blockchain.expectMsgType[WatchTxConfirmed] - } - - test("recv UpdateFee (sender can't afford it)") { f => - import f._ - val tx = bob.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx - val fee = UpdateFee(ByteVector32.Zeroes, FeeratePerKw(100000000 sat)) - // we first update the feerates so that we don't trigger a 'fee too different' error - bob.setBitcoinCoreFeerate(fee.feeratePerKw) - bob ! fee - val error = bob2alice.expectMsgType[Error] - assert(new String(error.data.toArray) == CannotAffordFees(channelId(bob), missing = 72120000L sat, reserve = 20000L sat, fees = 72400000L sat).getMessage) - awaitCond(bob.stateName == CLOSING) - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) // commit tx - //bob2blockchain.expectMsgType[PublishTx] // main delayed (removed because of the high fees) - bob2blockchain.expectMsgType[WatchTxConfirmed] + alice2blockchain.expectFinalTxPublished(tx.txid) } test("recv UpdateFee (local/remote feerates are too different)") { f => import f._ - val tx = bob.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val tx = bob.signCommitTx() bob ! UpdateFee(ByteVector32.Zeroes, FeeratePerKw(65000 sat)) val error = bob2alice.expectMsgType[Error] - assert(new String(error.data.toArray) == "local/remote feerates are too different: remoteFeeratePerKw=65000 localFeeratePerKw=10000") + assert(new String(error.data.toArray) == "local/remote feerates are too different: remoteFeeratePerKw=65000 localFeeratePerKw=2500") awaitCond(bob.stateName == CLOSING) - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) // commit tx - bob2blockchain.expectMsgType[PublishTx] // main delayed - bob2blockchain.expectMsgType[WatchTxConfirmed] + bob2blockchain.expectFinalTxPublished(tx.txid) } test("recv UpdateFee (remote feerate is too small)") { f => import f._ - val tx = bob.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val tx = bob.signCommitTx() bob ! UpdateFee(ByteVector32.Zeroes, FeeratePerKw(252 sat)) val error = bob2alice.expectMsgType[Error] assert(new String(error.data.toArray) == "remote fee rate is too small: remoteFeeratePerKw=252") awaitCond(bob.stateName == CLOSING) - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) // commit tx - bob2blockchain.expectMsgType[PublishTx] // main delayed - bob2blockchain.expectMsgType[WatchTxConfirmed] + bob2blockchain.expectFinalTxPublished(tx.txid) } test("recv CMD_UPDATE_RELAY_FEE ") { f => @@ -637,7 +621,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit val newFeeProportionalMillionth = TestConstants.Alice.nodeParams.relayParams.publicChannelFees.feeProportionalMillionths * 2 alice ! CMD_UPDATE_RELAY_FEE(sender.ref, newFeeBaseMsat, newFeeProportionalMillionth) sender.expectMsgType[RES_SUCCESS[CMD_UPDATE_RELAY_FEE]] - alice2relayer.expectNoMessage(1 seconds) + alice2relayer.expectNoMessage(100 millis) } test("recv CurrentBlockCount (no htlc timed out)") { f => @@ -649,26 +633,17 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit test("recv CurrentBlockCount (an htlc timed out)") { f => import f._ - val initialState = alice.stateData.asInstanceOf[DATA_SHUTDOWN] - val aliceCommitTx = initialState.commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val aliceCommitTx = alice.signCommitTx() alice ! CurrentBlockHeight(BlockHeight(400145)) - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == aliceCommitTx.txid) // commit tx - alice2blockchain.expectMsgType[PublishTx] // main delayed - alice2blockchain.expectMsgType[PublishTx] // htlc timeout 1 - alice2blockchain.expectMsgType[PublishTx] // htlc timeout 2 + alice2blockchain.expectFinalTxPublished(aliceCommitTx.txid) + alice2blockchain.expectReplaceableTxPublished[ClaimLocalAnchorTx] + alice2blockchain.expectFinalTxPublished("local-main-delayed") + alice2blockchain.expectReplaceableTxPublished[HtlcTimeoutTx] + alice2blockchain.expectReplaceableTxPublished[HtlcTimeoutTx] assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == aliceCommitTx.txid) } test("recv CurrentFeerate (when funder, triggers an UpdateFee)") { f => - import f._ - val initialState = alice.stateData.asInstanceOf[DATA_SHUTDOWN] - val event = CurrentFeerates.BitcoinCore(FeeratesPerKw(minimum = FeeratePerKw(250 sat), fastest = FeeratePerKw(10_000 sat), fast = FeeratePerKw(5_000 sat), medium = FeeratePerKw(1000 sat), slow = FeeratePerKw(500 sat))) - alice.setBitcoinCoreFeerates(event.feeratesPerKw) - alice ! event - alice2bob.expectMsg(UpdateFee(initialState.commitments.channelId, alice.underlyingActor.nodeParams.onChainFeeConf.getCommitmentFeerate(alice.underlyingActor.nodeParams.currentBitcoinCoreFeerates, alice.underlyingActor.remoteNodeId, initialState.commitments.params.commitmentFormat, initialState.commitments.latest.capacity))) - } - - test("recv CurrentFeerate (when funder, triggers an UpdateFee, anchor outputs)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => import f._ val initialState = alice.stateData.asInstanceOf[DATA_SHUTDOWN] assert(initialState.commitments.latest.localCommit.spec.commitTxFeerate == TestConstants.anchorOutputsFeeratePerKw) @@ -679,14 +654,6 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit } test("recv CurrentFeerate (when funder, doesn't trigger an UpdateFee)") { f => - import f._ - val event = CurrentFeerates.BitcoinCore(FeeratesPerKw.single(FeeratePerKw(10010 sat))) - alice.setBitcoinCoreFeerates(event.feeratesPerKw) - alice ! event - alice2bob.expectNoMessage(500 millis) - } - - test("recv CurrentFeerate (when funder, doesn't trigger an UpdateFee, anchor outputs)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => import f._ val initialState = alice.stateData.asInstanceOf[DATA_SHUTDOWN] assert(initialState.commitments.latest.localCommit.spec.commitTxFeerate == TestConstants.anchorOutputsFeeratePerKw) @@ -704,56 +671,38 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit bob2alice.expectNoMessage(500 millis) } - test("recv CurrentFeerate (when fundee, commit-fee/network-fee are very different)") { f => - import f._ - val event = CurrentFeerates.BitcoinCore(FeeratesPerKw.single(FeeratePerKw(25000 sat))) - bob.setBitcoinCoreFeerates(event.feeratesPerKw) - bob ! event - bob2alice.expectMsgType[Error] - bob2blockchain.expectMsgType[PublishTx] // commit tx - bob2blockchain.expectMsgType[PublishTx] // main delayed - bob2blockchain.expectMsgType[WatchTxConfirmed] - awaitCond(bob.stateName == CLOSING) - } - - test("recv WatchFundingSpentTriggered (their commit)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), Tag(ChannelStateTestsTags.StaticRemoteKey)) { f => + test("recv WatchFundingSpentTriggered (their commit)") { f => import f._ // bob publishes his current commit tx, which contains two pending htlcs alice->bob - val bobCommitTx = bob.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val bobCommitTx = bob.signCommitTx() assert(bobCommitTx.txOut.size == 6) // two main outputs and 2 pending htlcs alice ! WatchFundingSpentTriggered(bobCommitTx) + awaitCond(alice.stateName == CLOSING) + assert(alice.stateData.asInstanceOf[DATA_CLOSING].remoteCommitPublished.isDefined) + val rcp = alice.stateData.asInstanceOf[DATA_CLOSING].remoteCommitPublished.get + assert(rcp.htlcOutputs.size == 2) // in response to that, alice publishes her claim txs - val anchorTx = alice2blockchain.expectMsgType[PublishReplaceableTx] - assert(anchorTx.txInfo.isInstanceOf[ClaimLocalAnchorOutputTx]) - val claimMain = alice2blockchain.expectMsgType[PublishFinalTx].tx + val anchorTx = alice2blockchain.expectReplaceableTxPublished[ClaimRemoteAnchorTx] + val claimMain = alice2blockchain.expectFinalTxPublished("remote-main-delayed") // in addition to her main output, alice can only claim 2 out of 3 htlcs, she can't do anything regarding the htlc sent by bob for which she does not have the preimage - val claimHtlcTxs = (1 to 2).map(_ => alice2blockchain.expectMsgType[PublishReplaceableTx].txInfo.tx) - val htlcAmountClaimed = (for (claimHtlcTx <- claimHtlcTxs) yield { - assert(claimHtlcTx.txIn.size == 1) - assert(claimHtlcTx.txOut.size == 1) - Transaction.correctlySpends(claimHtlcTx, bobCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - claimHtlcTx.txOut.head.amount + val claimHtlcTxs = (1 to 2).map(_ => alice2blockchain.expectReplaceableTxPublished[ClaimHtlcTimeoutTx]) + val htlcAmountClaimed = claimHtlcTxs.map(claimHtlcTx => { + assert(claimHtlcTx.tx.txIn.size == 1) + assert(claimHtlcTx.tx.txOut.size == 1) + Transaction.correctlySpends(claimHtlcTx.sign(), bobCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + claimHtlcTx.tx.txOut.head.amount }).sum // htlc will timeout and be eventually refunded so we have a little less than fundingSatoshis - pushMsat = 1000000 - 200000 = 800000 (because fees) - val amountClaimed = htlcAmountClaimed + claimMain.txOut.head.amount - assert(amountClaimed == 780290.sat) - - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == bobCommitTx.txid) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == claimMain.txid) - alice2blockchain.expectMsgType[WatchOutputSpent] - alice2blockchain.expectMsgType[WatchOutputSpent] - alice2blockchain.expectNoMessage(1 second) + val amountClaimed = htlcAmountClaimed + claimMain.tx.txOut.head.amount + assert(amountClaimed == 790_974.sat) - awaitCond(alice.stateName == CLOSING) - assert(alice.stateData.asInstanceOf[DATA_CLOSING].remoteCommitPublished.isDefined) - val rcp = alice.stateData.asInstanceOf[DATA_CLOSING].remoteCommitPublished.get - assert(rcp.claimHtlcTxs.size == 2) - assert(getClaimHtlcSuccessTxs(rcp).length == 0) - assert(getClaimHtlcTimeoutTxs(rcp).length == 2) + alice2blockchain.expectWatchTxConfirmed(bobCommitTx.txid) + alice2blockchain.expectWatchOutputsSpent(Seq(anchorTx.input.outPoint, claimMain.input) ++ claimHtlcTxs.map(_.input.outPoint)) + alice2blockchain.expectNoMessage(100 millis) } - test("recv WatchFundingSpentTriggered (their next commit)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), Tag(ChannelStateTestsTags.StaticRemoteKey)) { f => + test("recv WatchFundingSpentTriggered (their next commit)") { f => import f._ // bob fulfills the first htlc fulfillHtlc(0, r1, bob, alice, bob2alice, alice2bob) @@ -770,42 +719,38 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit // as far as alice knows, bob currently has two valid unrevoked commitment transactions // bob publishes his current commit tx, which contains one pending htlc alice->bob - val bobCommitTx = bob.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val bobCommitTx = bob.signCommitTx() assert(bobCommitTx.txOut.size == 5) // two anchor outputs, two main outputs and 1 pending htlc alice ! WatchFundingSpentTriggered(bobCommitTx) + awaitCond(alice.stateName == CLOSING) + assert(alice.stateData.asInstanceOf[DATA_CLOSING].nextRemoteCommitPublished.isDefined) + val rcp = alice.stateData.asInstanceOf[DATA_CLOSING].nextRemoteCommitPublished.get + assert(rcp.htlcOutputs.size == 1) // in response to that, alice publishes her claim txs - alice2blockchain.expectMsgType[PublishReplaceableTx].txInfo.tx // claim local anchor output + val anchorTx = alice2blockchain.expectReplaceableTxPublished[ClaimRemoteAnchorTx] val claimTxs = Seq( - alice2blockchain.expectMsgType[PublishFinalTx].tx, + alice2blockchain.expectFinalTxPublished("remote-main-delayed").tx, // there is only one htlc to claim in the commitment bob published - alice2blockchain.expectMsgType[PublishReplaceableTx].txInfo.tx + alice2blockchain.expectReplaceableTxPublished[ClaimHtlcTimeoutTx].sign() ) - val amountClaimed = (for (claimTx <- claimTxs) yield { + val amountClaimed = claimTxs.map(claimTx => { assert(claimTx.txIn.size == 1) assert(claimTx.txOut.size == 1) Transaction.correctlySpends(claimTx, bobCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) claimTx.txOut.head.amount }).sum // htlc will timeout and be eventually refunded so we have a little less than fundingSatoshis - pushMsat - htlc1 = 1000000 - 200000 - 300 000 = 500000 (because fees) - assert(amountClaimed == 486200.sat) + assert(amountClaimed == 491_542.sat) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == bobCommitTx.txid) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == claimTxs(0).txid) - alice2blockchain.expectMsgType[WatchOutputSpent] - alice2blockchain.expectNoMessage(1 second) - - awaitCond(alice.stateName == CLOSING) - assert(alice.stateData.asInstanceOf[DATA_CLOSING].nextRemoteCommitPublished.isDefined) - val rcp = alice.stateData.asInstanceOf[DATA_CLOSING].nextRemoteCommitPublished.get - assert(rcp.claimHtlcTxs.size == 1) - assert(getClaimHtlcSuccessTxs(rcp).length == 0) - assert(getClaimHtlcTimeoutTxs(rcp).length == 1) + alice2blockchain.expectWatchTxConfirmed(bobCommitTx.txid) + alice2blockchain.expectWatchOutputsSpent(anchorTx.input.outPoint +: claimTxs.flatMap(_.txIn.map(_.outPoint))) + alice2blockchain.expectNoMessage(100 millis) } - test("recv WatchFundingSpentTriggered (revoked tx)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), Tag(ChannelStateTestsTags.StaticRemoteKey)) { f => + test("recv WatchFundingSpentTriggered (revoked tx)") { f => import f._ - val revokedTx = bob.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val revokedTx = bob.signCommitTx() // two main outputs + 2 htlc assert(revokedTx.txOut.size == 6) @@ -818,42 +763,34 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit // bob published the revoked tx alice ! WatchFundingSpentTriggered(revokedTx) alice2bob.expectMsgType[Error] + awaitCond(alice.stateName == CLOSING) + assert(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.size == 1) - val mainTx = alice2blockchain.expectMsgType[PublishFinalTx].tx - val mainPenaltyTx = alice2blockchain.expectMsgType[PublishFinalTx].tx - val htlc1PenaltyTx = alice2blockchain.expectMsgType[PublishFinalTx].tx - val htlc2PenaltyTx = alice2blockchain.expectMsgType[PublishFinalTx].tx - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == revokedTx.txid) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == mainTx.txid) - alice2blockchain.expectMsgType[WatchOutputSpent] // main-penalty - alice2blockchain.expectMsgType[WatchOutputSpent] // htlc1-penalty - alice2blockchain.expectMsgType[WatchOutputSpent] // htlc2-penalty - alice2blockchain.expectNoMessage(1 second) - - Transaction.correctlySpends(mainTx, Seq(revokedTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - Transaction.correctlySpends(mainPenaltyTx, Seq(revokedTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - Transaction.correctlySpends(htlc1PenaltyTx, Seq(revokedTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - Transaction.correctlySpends(htlc2PenaltyTx, Seq(revokedTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - + val mainTx = alice2blockchain.expectFinalTxPublished("remote-main-delayed").tx + val mainPenaltyTx = alice2blockchain.expectFinalTxPublished("main-penalty").tx + val htlc1PenaltyTx = alice2blockchain.expectFinalTxPublished("htlc-penalty").tx + val htlc2PenaltyTx = alice2blockchain.expectFinalTxPublished("htlc-penalty").tx + Seq(mainTx, mainPenaltyTx, htlc1PenaltyTx, htlc2PenaltyTx).foreach(tx => Transaction.correctlySpends(tx, Seq(revokedTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) // two main outputs are 300 000 and 200 000, htlcs are 300 000 and 200 000 - assert(mainTx.txOut.head.amount == 291250.sat) - assert(mainPenaltyTx.txOut.head.amount == 195160.sat) - assert(htlc1PenaltyTx.txOut.head.amount == 194510.sat) - assert(htlc2PenaltyTx.txOut.head.amount == 294510.sat) + assert(mainTx.txOut.head.amount == 291_250.sat) + assert(mainPenaltyTx.txOut.head.amount == 195_170.sat) + assert(htlc1PenaltyTx.txOut.head.amount == 194_200.sat) + assert(htlc2PenaltyTx.txOut.head.amount == 294_200.sat) - awaitCond(alice.stateName == CLOSING) - assert(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.size == 1) + alice2blockchain.expectWatchTxConfirmed(revokedTx.txid) + alice2blockchain.expectWatchOutputsSpent(Seq(mainTx, mainPenaltyTx, htlc1PenaltyTx, htlc2PenaltyTx).flatMap(_.txIn.map(_.outPoint))) + alice2blockchain.expectNoMessage(100 millis) } - test("recv WatchFundingSpentTriggered (revoked tx with updated commitment)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), Tag(ChannelStateTestsTags.StaticRemoteKey)) { f => + test("recv WatchFundingSpentTriggered (revoked tx with updated commitment)") { f => import f._ - val initialCommitTx = bob.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val initialCommitTx = bob.signCommitTx() assert(initialCommitTx.txOut.size == 6) // two main outputs + 2 htlc // bob fulfills one of the pending htlc (commitment update while in shutdown state) fulfillHtlc(0, r1, bob, alice, bob2alice, alice2bob) crossSign(bob, alice, bob2alice, alice2bob) - val revokedTx = bob.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val revokedTx = bob.signCommitTx() assert(revokedTx.txOut.size == 5) // two anchor outputs, two main outputs + 1 htlc // bob fulfills the second pending htlc (and revokes the previous commitment) @@ -864,27 +801,21 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit // bob published the revoked tx alice ! WatchFundingSpentTriggered(revokedTx) alice2bob.expectMsgType[Error] + awaitCond(alice.stateName == CLOSING) + assert(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.size == 1) - val mainTx = alice2blockchain.expectMsgType[PublishFinalTx].tx - val mainPenaltyTx = alice2blockchain.expectMsgType[PublishFinalTx].tx - val htlcPenaltyTx = alice2blockchain.expectMsgType[PublishFinalTx].tx - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == revokedTx.txid) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == mainTx.txid) - alice2blockchain.expectMsgType[WatchOutputSpent] // main-penalty - alice2blockchain.expectMsgType[WatchOutputSpent] // htlc-penalty - alice2blockchain.expectNoMessage(1 second) - - Transaction.correctlySpends(mainTx, Seq(revokedTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - Transaction.correctlySpends(mainPenaltyTx, Seq(revokedTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - Transaction.correctlySpends(htlcPenaltyTx, Seq(revokedTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - + val mainTx = alice2blockchain.expectFinalTxPublished("remote-main-delayed").tx + val mainPenaltyTx = alice2blockchain.expectFinalTxPublished("main-penalty").tx + val htlcPenaltyTx = alice2blockchain.expectFinalTxPublished("htlc-penalty").tx + Seq(mainTx, mainPenaltyTx, htlcPenaltyTx).foreach(tx => Transaction.correctlySpends(tx, Seq(revokedTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) // two main outputs are 300 000 and 200 000, htlcs are 300 000 and 200 000 - assert(mainTx.txOut(0).amount == 291680.sat) - assert(mainPenaltyTx.txOut(0).amount == 495160.sat) - assert(htlcPenaltyTx.txOut(0).amount == 194510.sat) + assert(mainTx.txOut(0).amount == 291_680.sat) + assert(mainPenaltyTx.txOut(0).amount == 495_170.sat) + assert(htlcPenaltyTx.txOut(0).amount == 194_200.sat) - awaitCond(alice.stateName == CLOSING) - assert(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.size == 1) + alice2blockchain.expectWatchTxConfirmed(revokedTx.txid) + alice2blockchain.expectWatchOutputsSpent(Seq(mainTx, mainPenaltyTx, htlcPenaltyTx).flatMap(_.txIn.map(_.outPoint))) + alice2blockchain.expectNoMessage(100 millis) } test("recv CMD_CLOSE") { f => @@ -933,40 +864,38 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit test("recv CMD_FORCECLOSE") { f => import f._ - val aliceCommitTx = alice.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx - assert(aliceCommitTx.txOut.size == 4) // two main outputs and two htlcs + val aliceCommitTx = alice.signCommitTx() + assert(aliceCommitTx.txOut.size == 6) // two main outputs, two anchor outputs and two htlcs val sender = TestProbe() alice ! CMD_FORCECLOSE(sender.ref) sender.expectMsgType[RES_SUCCESS[CMD_FORCECLOSE]] - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == aliceCommitTx.txid) + alice2blockchain.expectFinalTxPublished(aliceCommitTx.txid) awaitCond(alice.stateName == CLOSING) assert(alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.isDefined) val lcp = alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.get - assert(lcp.htlcTxs.size == 2) - assert(lcp.claimHtlcDelayedTxs.isEmpty) // 3rd-stage txs will be published once htlc txs confirm - - val claimMain = alice2blockchain.expectMsgType[PublishFinalTx] - val htlc1 = alice2blockchain.expectMsgType[PublishFinalTx] - val htlc2 = alice2blockchain.expectMsgType[PublishFinalTx] - Seq(claimMain, htlc1, htlc2).foreach(tx => Transaction.correctlySpends(tx.tx, aliceCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == aliceCommitTx.txid) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == claimMain.tx.txid) - alice2blockchain.expectMsgType[WatchOutputSpent] - alice2blockchain.expectMsgType[WatchOutputSpent] - alice2blockchain.expectNoMessage(1 second) + assert(lcp.htlcOutputs.size == 2) + + val anchorTx = alice2blockchain.expectReplaceableTxPublished[ClaimLocalAnchorTx].tx + val claimMain = alice2blockchain.expectFinalTxPublished("local-main-delayed").tx + val htlc1 = alice2blockchain.expectReplaceableTxPublished[HtlcTimeoutTx].sign() + val htlc2 = alice2blockchain.expectReplaceableTxPublished[HtlcTimeoutTx].sign() + Seq(claimMain, htlc1, htlc2).foreach(tx => Transaction.correctlySpends(tx, aliceCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) + alice2blockchain.expectWatchTxConfirmed(aliceCommitTx.txid) + alice2blockchain.expectWatchOutputsSpent(Seq(claimMain, anchorTx, htlc1, htlc2).map(_.txIn.head.outPoint)) + alice2blockchain.expectNoMessage(100 millis) // 3rd-stage txs are published when htlc txs confirm Seq(htlc1, htlc2).foreach(htlcTimeoutTx => { - alice ! WatchOutputSpentTriggered(htlcTimeoutTx.amount, htlcTimeoutTx.tx) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == htlcTimeoutTx.tx.txid) - alice ! WatchTxConfirmedTriggered(BlockHeight(2701), 3, htlcTimeoutTx.tx) - val claimHtlcDelayedTx = alice2blockchain.expectMsgType[PublishFinalTx].tx - Transaction.correctlySpends(claimHtlcDelayedTx, htlcTimeoutTx.tx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == claimHtlcDelayedTx.txid) + alice ! WatchOutputSpentTriggered(0 sat, htlcTimeoutTx) + alice2blockchain.expectWatchTxConfirmed(htlcTimeoutTx.txid) + alice ! WatchTxConfirmedTriggered(BlockHeight(2701), 3, htlcTimeoutTx) + val htlcDelayedTx = alice2blockchain.expectFinalTxPublished("htlc-delayed") + Transaction.correctlySpends(htlcDelayedTx.tx, htlcTimeoutTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + alice2blockchain.expectWatchOutputSpent(htlcDelayedTx.input) }) - awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.get.claimHtlcDelayedTxs.length == 2) - alice2blockchain.expectNoMessage(1 second) + awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.get.htlcDelayedOutputs.size == 2) + alice2blockchain.expectNoMessage(100 millis) } test("recv WatchFundingSpentTriggered (unrecognized commit)") { f => @@ -976,12 +905,79 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit assert(alice.stateName == SHUTDOWN) } + def testInputRestored(f: FixtureParam, commitmentFormat: CommitmentFormat): Unit = { + import f._ + // Alice and Bob restart. + val aliceData = alice.underlyingActor.nodeParams.db.channels.getChannel(channelId(alice)).get + alice.setState(WAIT_FOR_INIT_INTERNAL, Nothing) + alice ! INPUT_RESTORED(aliceData) + alice2blockchain.expectMsgType[SetChannelId] + val fundingTxId = alice2blockchain.expectMsgType[WatchFundingSpent].txId + awaitCond(alice.stateName == OFFLINE) + val bobData = bob.underlyingActor.nodeParams.db.channels.getChannel(channelId(bob)).get + bob.setState(WAIT_FOR_INIT_INTERNAL, Nothing) + bob ! INPUT_RESTORED(bobData) + bob2blockchain.expectMsgType[SetChannelId] + bob2blockchain.expectMsgType[WatchFundingSpent] + awaitCond(bob.stateName == OFFLINE) + // They reconnect and provide nonces to resume HTLC settlement. + val aliceInit = Init(alice.underlyingActor.nodeParams.features.initFeatures()) + val bobInit = Init(bob.underlyingActor.nodeParams.features.initFeatures()) + alice ! INPUT_RECONNECTED(bob, aliceInit, bobInit) + bob ! INPUT_RECONNECTED(alice, bobInit, aliceInit) + val channelReestablishAlice = alice2bob.expectMsgType[ChannelReestablish] + val channelReestablishBob = bob2alice.expectMsgType[ChannelReestablish] + Seq(channelReestablishAlice, channelReestablishBob).foreach(channelReestablish => commitmentFormat match { + case _: SegwitV0CommitmentFormat => + assert(channelReestablish.currentCommitNonce_opt.isEmpty) + assert(channelReestablish.nextCommitNonces.isEmpty) + case _: TaprootCommitmentFormat => + assert(channelReestablish.currentCommitNonce_opt.isEmpty) + assert(channelReestablish.nextCommitNonces.contains(fundingTxId)) + }) + alice2bob.forward(bob, channelReestablishAlice) + bob2alice.forward(alice, channelReestablishBob) + // They retransmit shutdown. + val shutdownAlice = alice2bob.expectMsgType[Shutdown] + alice2bob.forward(bob, shutdownAlice) + val shutdownBob = bob2alice.expectMsgType[Shutdown] + bob2alice.forward(alice, shutdownBob) + Seq(shutdownAlice, shutdownBob).foreach(shutdown => commitmentFormat match { + case _: SegwitV0CommitmentFormat => assert(shutdown.closeeNonce_opt.isEmpty) + case _: TaprootCommitmentFormat => assert(shutdown.closeeNonce_opt.nonEmpty) + }) + // They resume HTLC settlement. + fulfillHtlc(0, r1, bob, alice, bob2alice, alice2bob) + crossSign(bob, alice, bob2alice, alice2bob) + assert(alice.stateName == SHUTDOWN) + fulfillHtlc(1, r2, bob, alice, bob2alice, alice2bob) + crossSign(bob, alice, bob2alice, alice2bob) + awaitCond(alice.stateName == NEGOTIATING_SIMPLE) + awaitCond(bob.stateName == NEGOTIATING_SIMPLE) + // They can now sign the closing transaction. + val closingCompleteAlice = alice2bob.expectMsgType[ClosingComplete] + alice2bob.forward(bob, closingCompleteAlice) + bob2alice.expectMsgType[ClosingComplete] // ignored + val closingTx = bob2blockchain.expectMsgType[PublishFinalTx] + val closingSigBob = bob2alice.expectMsgType[ClosingSig] + bob2alice.forward(alice, closingSigBob) + assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == closingTx.tx.txid) + } + + test("recv INPUT_RESTORED", Tag(ChannelStateTestsTags.SimpleClose)) { f => + testInputRestored(f, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) + } + + test("recv INPUT_RESTORED (taproot)", Tag(ChannelStateTestsTags.SimpleClose), Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => + testInputRestored(f, ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat) + } + test("recv Error") { f => import f._ - val aliceCommitTx = alice.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val aliceCommitTx = alice.signCommitTx() alice ! Error(ByteVector32.Zeroes, "oops") - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == aliceCommitTx.txid) - assert(aliceCommitTx.txOut.size == 4) // two main outputs and two htlcs + alice2blockchain.expectFinalTxPublished(aliceCommitTx.txid) + assert(aliceCommitTx.txOut.size == 6) // two main outputs, two anchor outputs and two htlcs awaitCond(alice.stateName == CLOSING) assert(alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.isDefined) @@ -989,14 +985,15 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit // - 1 tx to claim the main delayed output // - 2 txs for each htlc // NB: 3rd-stage txs will only be published once the htlc txs confirm - val claimTxs = for (_ <- 0 until 3) yield alice2blockchain.expectMsgType[PublishFinalTx].tx + val anchorTx = alice2blockchain.expectReplaceableTxPublished[ClaimLocalAnchorTx] + val claimMain = alice2blockchain.expectFinalTxPublished("local-main-delayed") + val htlcTxs = (0 until 2).map(_ => alice2blockchain.expectReplaceableTxPublished[HtlcTimeoutTx]) // the main delayed output and htlc txs spend the commitment transaction - claimTxs.foreach(tx => Transaction.correctlySpends(tx, aliceCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == aliceCommitTx.txid) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == claimTxs(0).txid) // main-delayed - alice2blockchain.expectMsgType[WatchOutputSpent] - alice2blockchain.expectMsgType[WatchOutputSpent] - alice2blockchain.expectNoMessage(1 second) + Transaction.correctlySpends(claimMain.tx, aliceCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + htlcTxs.foreach(tx => Transaction.correctlySpends(tx.sign(), aliceCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) + alice2blockchain.expectWatchTxConfirmed(aliceCommitTx.txid) + alice2blockchain.expectWatchOutputsSpent((Seq(anchorTx.tx, claimMain.tx) ++ htlcTxs.map(_.tx)).flatMap(_.txIn.map(_.outPoint))) + alice2blockchain.expectNoMessage(100 millis) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/g/NegotiatingStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/g/NegotiatingStateSpec.scala index 5c587a0109..cda42664a1 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/g/NegotiatingStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/g/NegotiatingStateSpec.scala @@ -26,15 +26,17 @@ import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.channel.publish.TxPublisher.{PublishFinalTx, PublishTx, SetChannelId} import fr.acinq.eclair.channel.states.ChannelStateTestsBase.PimpTestFSM import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags} +import fr.acinq.eclair.reputation.Reputation import fr.acinq.eclair.testutils.PimpTestProbe._ import fr.acinq.eclair.transactions.Transactions -import fr.acinq.eclair.transactions.Transactions.ZeroFeeHtlcTxAnchorOutputsCommitmentFormat +import fr.acinq.eclair.transactions.Transactions._ import fr.acinq.eclair.wire.protocol.ClosingSignedTlv.FeeRange -import fr.acinq.eclair.wire.protocol.{AnnouncementSignatures, ChannelUpdate, ClosingComplete, ClosingSig, ClosingSigned, ClosingTlv, Error, Shutdown, TlvStream, Warning} +import fr.acinq.eclair.wire.protocol.{AnnouncementSignatures, ChannelUpdate, ClosingComplete, ClosingCompleteTlv, ClosingSig, ClosingSigTlv, ClosingSigned, ClosingTlv, Error, Shutdown, TlvStream, Warning} import fr.acinq.eclair.{BlockHeight, CltvExpiry, Features, MilliSatoshiLong, TestConstants, TestKitBaseClass, randomBytes32, randomKey} import org.scalatest.Inside.inside import org.scalatest.funsuite.FixtureAnyFunSuiteLike import org.scalatest.{Outcome, Tag} +import scodec.bits.ByteVector import scala.concurrent.duration._ @@ -56,43 +58,47 @@ class NegotiatingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike implicit val log: akka.event.LoggingAdapter = akka.event.NoLogging - def aliceClose(f: FixtureParam, feerates: Option[ClosingFeerates] = None): Unit = { + def aliceClose(f: FixtureParam, feerates: Option[ClosingFeerates] = None, script_opt: Option[ByteVector] = None): Unit = { import f._ val sender = TestProbe() - alice ! CMD_CLOSE(sender.ref, None, feerates) + alice ! CMD_CLOSE(sender.ref, script_opt, feerates) sender.expectMsgType[RES_SUCCESS[CMD_CLOSE]] val aliceShutdown = alice2bob.expectMsgType[Shutdown] + if (alice.commitments.latest.commitmentFormat.isInstanceOf[TaprootCommitmentFormat]) assert(aliceShutdown.closeeNonce_opt.nonEmpty) alice2bob.forward(bob, aliceShutdown) val bobShutdown = bob2alice.expectMsgType[Shutdown] + if (bob.commitments.latest.commitmentFormat.isInstanceOf[TaprootCommitmentFormat]) assert(bobShutdown.closeeNonce_opt.nonEmpty) bob2alice.forward(alice, bobShutdown) - if (alice.stateData.asInstanceOf[ChannelDataWithCommitments].commitments.params.localParams.initFeatures.hasFeature(Features.SimpleClose)) { + if (alice.stateData.asInstanceOf[ChannelDataWithCommitments].commitments.localChannelParams.initFeatures.hasFeature(Features.SimpleClose)) { awaitCond(alice.stateName == NEGOTIATING_SIMPLE) awaitCond(bob.stateName == NEGOTIATING_SIMPLE) } else { awaitCond(alice.stateName == NEGOTIATING) - assert(alice.stateData.asInstanceOf[DATA_NEGOTIATING].commitments.params.localParams.upfrontShutdownScript_opt.forall(_ == aliceShutdown.scriptPubKey)) + assert(alice.stateData.asInstanceOf[DATA_NEGOTIATING].commitments.localChannelParams.upfrontShutdownScript_opt.forall(_ == aliceShutdown.scriptPubKey)) awaitCond(bob.stateName == NEGOTIATING) - assert(bob.stateData.asInstanceOf[DATA_NEGOTIATING].commitments.params.localParams.upfrontShutdownScript_opt.forall(_ == bobShutdown.scriptPubKey)) + assert(bob.stateData.asInstanceOf[DATA_NEGOTIATING].commitments.localChannelParams.upfrontShutdownScript_opt.forall(_ == bobShutdown.scriptPubKey)) } } - def bobClose(f: FixtureParam, feerates: Option[ClosingFeerates] = None): Unit = { + def bobClose(f: FixtureParam, feerates: Option[ClosingFeerates] = None, script_opt: Option[ByteVector] = None): Unit = { import f._ val sender = TestProbe() - bob ! CMD_CLOSE(sender.ref, None, feerates) + bob ! CMD_CLOSE(sender.ref, script_opt, feerates) sender.expectMsgType[RES_SUCCESS[CMD_CLOSE]] val bobShutdown = bob2alice.expectMsgType[Shutdown] + if (bob.commitments.latest.commitmentFormat.isInstanceOf[TaprootCommitmentFormat]) assert(bobShutdown.closeeNonce_opt.nonEmpty) bob2alice.forward(alice, bobShutdown) val aliceShutdown = alice2bob.expectMsgType[Shutdown] + if (alice.commitments.latest.commitmentFormat.isInstanceOf[TaprootCommitmentFormat]) assert(aliceShutdown.closeeNonce_opt.nonEmpty) alice2bob.forward(bob, aliceShutdown) - if (bob.stateData.asInstanceOf[ChannelDataWithCommitments].commitments.params.localParams.initFeatures.hasFeature(Features.SimpleClose)) { + if (bob.stateData.asInstanceOf[ChannelDataWithCommitments].commitments.localChannelParams.initFeatures.hasFeature(Features.SimpleClose)) { awaitCond(alice.stateName == NEGOTIATING_SIMPLE) awaitCond(bob.stateName == NEGOTIATING_SIMPLE) } else { awaitCond(alice.stateName == NEGOTIATING) - assert(alice.stateData.asInstanceOf[DATA_NEGOTIATING].commitments.params.localParams.upfrontShutdownScript_opt.forall(_ == aliceShutdown.scriptPubKey)) + assert(alice.stateData.asInstanceOf[DATA_NEGOTIATING].commitments.localChannelParams.upfrontShutdownScript_opt.forall(_ == aliceShutdown.scriptPubKey)) awaitCond(bob.stateName == NEGOTIATING) - assert(bob.stateData.asInstanceOf[DATA_NEGOTIATING].commitments.params.localParams.upfrontShutdownScript_opt.forall(_ == bobShutdown.scriptPubKey)) + assert(bob.stateData.asInstanceOf[DATA_NEGOTIATING].commitments.localChannelParams.upfrontShutdownScript_opt.forall(_ == bobShutdown.scriptPubKey)) } } @@ -106,7 +112,7 @@ class NegotiatingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike systemA.eventStream.subscribe(aliceListener.ref, classOf[LocalChannelUpdate]) val bobListener = TestProbe() systemB.eventStream.subscribe(bobListener.ref, classOf[LocalChannelUpdate]) - + alice2bob.expectMsgType[AnnouncementSignatures] alice2bob.forward(bob) alice2bob.expectMsgType[ChannelUpdate] @@ -133,7 +139,7 @@ class NegotiatingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike aliceClose(f) alice2bob.expectMsgType[ClosingSigned] val sender = TestProbe() - val add = CMD_ADD_HTLC(sender.ref, 5000000000L msat, randomBytes32(), CltvExpiry(300000), TestConstants.emptyOnionPacket, None, 1.0, None, localOrigin(sender.ref)) + val add = CMD_ADD_HTLC(sender.ref, 5000000000L msat, randomBytes32(), CltvExpiry(300000), TestConstants.emptyOnionPacket, None, Reputation.Score.max, None, localOrigin(sender.ref)) alice ! add val error = ChannelUnavailable(channelId(alice)) sender.expectMsg(RES_ADD_FAILED(add, error, None)) @@ -155,18 +161,18 @@ class NegotiatingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike // alice is funder so she initiates the negotiation val aliceCloseSig1 = alice2bob.expectMsgType[ClosingSigned] - assert(aliceCloseSig1.feeSatoshis == 3370.sat) // matches a feerate of 5000 sat/kw + assert(aliceCloseSig1.feeSatoshis == 3850.sat) // matches a feerate of 5000 sat/kw assert(aliceCloseSig1.feeRange_opt.nonEmpty) assert(aliceCloseSig1.feeRange_opt.get.min < aliceCloseSig1.feeSatoshis) assert(aliceCloseSig1.feeSatoshis < aliceCloseSig1.feeRange_opt.get.max) assert(alice.stateData.asInstanceOf[DATA_NEGOTIATING].closingTxProposed.length == 1) assert(alice.stateData.asInstanceOf[DATA_NEGOTIATING].closingTxProposed.last.length == 1) assert(alice.stateData.asInstanceOf[DATA_NEGOTIATING].bestUnpublishedClosingTx_opt.isEmpty) - if (alice.stateData.asInstanceOf[DATA_NEGOTIATING].commitments.params.channelFeatures.hasFeature(Features.UpfrontShutdownScript)) { + if (alice.stateData.asInstanceOf[DATA_NEGOTIATING].commitments.channelParams.channelFeatures.hasFeature(Features.UpfrontShutdownScript)) { // check that the closing tx uses Alice and Bob's default closing scripts val closingTx = alice.stateData.asInstanceOf[DATA_NEGOTIATING].closingTxProposed.last.head.unsignedTx.tx - val expectedLocalScript = alice.stateData.asInstanceOf[DATA_NEGOTIATING].commitments.params.localParams.upfrontShutdownScript_opt.get - val expectedRemoteScript = bob.stateData.asInstanceOf[DATA_NEGOTIATING].commitments.params.localParams.upfrontShutdownScript_opt.get + val expectedLocalScript = alice.stateData.asInstanceOf[DATA_NEGOTIATING].commitments.localChannelParams.upfrontShutdownScript_opt.get + val expectedRemoteScript = bob.stateData.asInstanceOf[DATA_NEGOTIATING].commitments.localChannelParams.upfrontShutdownScript_opt.get assert(closingTx.txOut.map(_.publicKeyScript).toSet == Set(expectedLocalScript, expectedRemoteScript)) } alice2bob.forward(bob) @@ -203,11 +209,7 @@ class NegotiatingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike testClosingSignedDifferentFees(f, bobInitiates = true) } - test("recv ClosingSigned (theirCloseFee != ourCloseFee, anchor outputs)", Tag(ChannelStateTestsTags.AnchorOutputs)) { f => - testClosingSignedDifferentFees(f) - } - - test("recv ClosingSigned (theirCloseFee != ourCloseFee, anchor outputs, upfront shutdown scripts)", Tag(ChannelStateTestsTags.AnchorOutputs), Tag(ChannelStateTestsTags.UpfrontShutdownScript)) { f => + test("recv ClosingSigned (theirCloseFee != ourCloseFee, upfront shutdown scripts)", Tag(ChannelStateTestsTags.UpfrontShutdownScript)) { f => testClosingSignedDifferentFees(f) } @@ -248,7 +250,7 @@ class NegotiatingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike // alice is funder so she initiates the negotiation val aliceCloseSig1 = alice2bob.expectMsgType[ClosingSigned] - assert(aliceCloseSig1.feeSatoshis == 3370.sat) // matches a feerate of 5 000 sat/kw + assert(aliceCloseSig1.feeSatoshis == 3850.sat) // matches a feerate of 5 000 sat/kw assert(aliceCloseSig1.feeRange_opt.nonEmpty) alice2bob.forward(bob) // bob agrees with that proposal @@ -270,10 +272,6 @@ class NegotiatingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike testClosingSignedSameFees(f, bobInitiates = true) } - test("recv ClosingSigned (theirCloseFee == ourCloseFee, anchor outputs)", Tag(ChannelStateTestsTags.AnchorOutputs)) { f => - testClosingSignedSameFees(f) - } - test("recv ClosingSigned (theirCloseFee == ourCloseFee, upfront shutdown script)", Tag(ChannelStateTestsTags.UpfrontShutdownScript)) { f => testClosingSignedSameFees(f) } @@ -285,15 +283,15 @@ class NegotiatingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike aliceClose(f, Some(ClosingFeerates(FeeratePerKw(2500 sat), FeeratePerKw(2000 sat), FeeratePerKw(3000 sat)))) // alice initiates the negotiation with a very low feerate val aliceCloseSig = alice2bob.expectMsgType[ClosingSigned] - assert(aliceCloseSig.feeSatoshis == 1685.sat) - assert(aliceCloseSig.feeRange_opt.contains(FeeRange(1348 sat, 2022 sat))) + assert(aliceCloseSig.feeSatoshis == 1925.sat) + assert(aliceCloseSig.feeRange_opt.contains(FeeRange(1540 sat, 2310 sat))) alice2bob.forward(bob) // bob chooses alice's highest fee val bobCloseSig = bob2alice.expectMsgType[ClosingSigned] - assert(bobCloseSig.feeSatoshis == 2022.sat) + assert(bobCloseSig.feeSatoshis == 2310.sat) bob2alice.forward(alice) // alice accepts this proposition - assert(alice2bob.expectMsgType[ClosingSigned].feeSatoshis == 2022.sat) + assert(alice2bob.expectMsgType[ClosingSigned].feeSatoshis == 2310.sat) alice2bob.forward(bob) val mutualCloseTx = alice2blockchain.expectMsgType[PublishFinalTx].tx assert(bob2blockchain.expectMsgType[PublishFinalTx].tx == mutualCloseTx) @@ -308,7 +306,7 @@ class NegotiatingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike bobClose(f, Some(ClosingFeerates(FeeratePerKw(2500 sat), FeeratePerKw(2000 sat), FeeratePerKw(3000 sat)))) // alice is funder, so bob's override will simply be ignored val aliceCloseSig = alice2bob.expectMsgType[ClosingSigned] - assert(aliceCloseSig.feeSatoshis == 6740.sat) // matches a feerate of 10000 sat/kw + assert(aliceCloseSig.feeSatoshis == 7700.sat) // matches a feerate of 10000 sat/kw alice2bob.forward(bob) // bob directly agrees because their fee estimator matches val bobCloseSig = bob2alice.expectMsgType[ClosingSigned] @@ -336,13 +334,13 @@ class NegotiatingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike private def makeLegacyClosingSigned(f: FixtureParam, closingFee: Satoshi): (ClosingSigned, ClosingSigned) = { import f._ val aliceState = alice.stateData.asInstanceOf[DATA_NEGOTIATING] - val aliceKeyManager = alice.underlyingActor.nodeParams.channelKeyManager + val aliceKeys = alice.underlyingActor.channelKeys val aliceScript = aliceState.localShutdown.scriptPubKey val bobState = bob.stateData.asInstanceOf[DATA_NEGOTIATING] - val bobKeyManager = bob.underlyingActor.nodeParams.channelKeyManager + val bobKeys = bob.underlyingActor.channelKeys val bobScript = bobState.localShutdown.scriptPubKey - val (_, aliceClosingSigned) = Closing.MutualClose.makeClosingTx(aliceKeyManager, aliceState.commitments.latest, aliceScript, bobScript, ClosingFees(closingFee, closingFee, closingFee)) - val (_, bobClosingSigned) = Closing.MutualClose.makeClosingTx(bobKeyManager, bobState.commitments.latest, bobScript, aliceScript, ClosingFees(closingFee, closingFee, closingFee)) + val (_, aliceClosingSigned) = Closing.MutualClose.makeClosingTx(aliceKeys, aliceState.commitments.latest, aliceScript, bobScript, ClosingFees(closingFee, closingFee, closingFee)) + val (_, bobClosingSigned) = Closing.MutualClose.makeClosingTx(bobKeys, bobState.commitments.latest, bobScript, aliceScript, ClosingFees(closingFee, closingFee, closingFee)) (aliceClosingSigned.copy(tlvStream = TlvStream.empty), bobClosingSigned.copy(tlvStream = TlvStream.empty)) } @@ -352,8 +350,8 @@ class NegotiatingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike aliceClose(f) val aliceClosing1 = alice2bob.expectMsgType[ClosingSigned] val Some(FeeRange(_, maxFee)) = aliceClosing1.feeRange_opt - assert(aliceClosing1.feeSatoshis == 674.sat) - assert(maxFee == 1348.sat) + assert(aliceClosing1.feeSatoshis == 770.sat) + assert(maxFee == 1540.sat) assert(alice.stateData.asInstanceOf[DATA_NEGOTIATING].closingTxProposed.last.length == 1) assert(alice.stateData.asInstanceOf[DATA_NEGOTIATING].bestUnpublishedClosingTx_opt.isEmpty) // bob makes a proposal outside our fee range @@ -361,21 +359,21 @@ class NegotiatingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike bob2alice.send(alice, bobClosing1) val aliceClosing2 = alice2bob.expectMsgType[ClosingSigned] assert(aliceClosing1.feeSatoshis < aliceClosing2.feeSatoshis) - assert(aliceClosing2.feeSatoshis < 1600.sat) + assert(aliceClosing2.feeSatoshis < 1700.sat) assert(alice.stateData.asInstanceOf[DATA_NEGOTIATING].closingTxProposed.last.length == 2) assert(alice.stateData.asInstanceOf[DATA_NEGOTIATING].bestUnpublishedClosingTx_opt.nonEmpty) val (_, bobClosing2) = makeLegacyClosingSigned(f, 2000 sat) bob2alice.send(alice, bobClosing2) val aliceClosing3 = alice2bob.expectMsgType[ClosingSigned] assert(aliceClosing2.feeSatoshis < aliceClosing3.feeSatoshis) - assert(aliceClosing3.feeSatoshis < 1800.sat) + assert(aliceClosing3.feeSatoshis < 1900.sat) assert(alice.stateData.asInstanceOf[DATA_NEGOTIATING].closingTxProposed.last.length == 3) assert(alice.stateData.asInstanceOf[DATA_NEGOTIATING].bestUnpublishedClosingTx_opt.nonEmpty) - val (_, bobClosing3) = makeLegacyClosingSigned(f, 1800 sat) + val (_, bobClosing3) = makeLegacyClosingSigned(f, 1900 sat) bob2alice.send(alice, bobClosing3) val aliceClosing4 = alice2bob.expectMsgType[ClosingSigned] assert(aliceClosing3.feeSatoshis < aliceClosing4.feeSatoshis) - assert(aliceClosing4.feeSatoshis < 1800.sat) + assert(aliceClosing4.feeSatoshis < 1900.sat) assert(alice.stateData.asInstanceOf[DATA_NEGOTIATING].closingTxProposed.last.length == 4) assert(alice.stateData.asInstanceOf[DATA_NEGOTIATING].bestUnpublishedClosingTx_opt.nonEmpty) val (_, bobClosing4) = makeLegacyClosingSigned(f, aliceClosing4.feeSatoshis) @@ -453,10 +451,10 @@ class NegotiatingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike bob2alice.expectNoMessage(100 millis) } - test("recv ClosingSigned (fee higher than commit tx fee)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + test("recv ClosingSigned (fee higher than commit tx fee)") { f => import f._ val commitment = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest - val commitFee = Transactions.commitTxFeeMsat(commitment.localParams.dustLimit, commitment.localCommit.spec, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) + val commitFee = Transactions.commitTxFeeMsat(commitment.localCommitParams.dustLimit, commitment.localCommit.spec, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) aliceClose(f) val aliceCloseSig = alice2bob.expectMsgType[ClosingSigned] assert(aliceCloseSig.feeSatoshis > commitFee.truncateToSatoshi) @@ -474,36 +472,54 @@ class NegotiatingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike import f._ aliceClose(f) val aliceCloseSig = alice2bob.expectMsgType[ClosingSigned] - val tx = bob.stateData.asInstanceOf[DATA_NEGOTIATING].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val tx = bob.signCommitTx() bob ! aliceCloseSig.copy(signature = ByteVector64.Zeroes) val error = bob2alice.expectMsgType[Error] assert(new String(error.data.toArray).startsWith("invalid close signature")) - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) - bob2blockchain.expectMsgType[PublishTx] - bob2blockchain.expectMsgType[WatchTxConfirmed] + bob2blockchain.expectFinalTxPublished(tx.txid) + bob2blockchain.expectReplaceableTxPublished[ClaimLocalAnchorTx] + bob2blockchain.expectFinalTxPublished("local-main-delayed") + bob2blockchain.expectWatchTxConfirmed(tx.txid) } - test("recv ClosingComplete (both outputs)", Tag(ChannelStateTestsTags.SimpleClose)) { f => + def testReceiveClosingCompleteBothOutputs(f: FixtureParam, commitmentFormat: CommitmentFormat): Unit = { import f._ + aliceClose(f) val aliceClosingComplete = alice2bob.expectMsgType[ClosingComplete] assert(aliceClosingComplete.fees > 0.sat) - assert(aliceClosingComplete.closerAndCloseeOutputsSig_opt.nonEmpty) - assert(aliceClosingComplete.closerOutputOnlySig_opt.nonEmpty) - assert(aliceClosingComplete.closeeOutputOnlySig_opt.isEmpty) + commitmentFormat match { + case _: SegwitV0CommitmentFormat => + assert(aliceClosingComplete.closerAndCloseeOutputsSig_opt.nonEmpty) + assert(aliceClosingComplete.closerOutputOnlySig_opt.nonEmpty) + case _: TaprootCommitmentFormat => + assert(aliceClosingComplete.closerAndCloseeOutputsPartialSig_opt.nonEmpty) + assert(aliceClosingComplete.closerOutputOnlyPartialSig_opt.nonEmpty) + } + assert(aliceClosingComplete.closeeOutputOnlySig_opt.orElse(aliceClosingComplete.closeeOutputOnlyPartialSig_opt).isEmpty) val bobClosingComplete = bob2alice.expectMsgType[ClosingComplete] assert(bobClosingComplete.fees > 0.sat) - assert(bobClosingComplete.closerAndCloseeOutputsSig_opt.nonEmpty) - assert(bobClosingComplete.closerOutputOnlySig_opt.nonEmpty) - assert(bobClosingComplete.closeeOutputOnlySig_opt.isEmpty) + commitmentFormat match { + case _: SegwitV0CommitmentFormat => + assert(bobClosingComplete.closerAndCloseeOutputsSig_opt.nonEmpty) + assert(bobClosingComplete.closerOutputOnlySig_opt.nonEmpty) + case _: TaprootCommitmentFormat => + assert(bobClosingComplete.closerAndCloseeOutputsPartialSig_opt.nonEmpty) + assert(bobClosingComplete.closerOutputOnlyPartialSig_opt.nonEmpty) + } + assert(bobClosingComplete.closeeOutputOnlySig_opt.orElse(bobClosingComplete.closeeOutputOnlyPartialSig_opt).isEmpty) alice2bob.forward(bob, aliceClosingComplete) val bobClosingSig = bob2alice.expectMsgType[ClosingSig] assert(bobClosingSig.fees == aliceClosingComplete.fees) assert(bobClosingSig.lockTime == aliceClosingComplete.lockTime) + commitmentFormat match { + case _: SegwitV0CommitmentFormat => assert(bobClosingSig.closerAndCloseeOutputsSig_opt.nonEmpty) + case _: TaprootCommitmentFormat => assert(bobClosingSig.closerAndCloseeOutputsPartialSig_opt.nonEmpty) + } bob2alice.forward(alice, bobClosingSig) val aliceTx = alice2blockchain.expectMsgType[PublishFinalTx] - assert(aliceTx.desc == "closing") + assert(aliceTx.desc == "closing-tx") assert(aliceTx.fee > 0.sat) alice2blockchain.expectWatchTxConfirmed(aliceTx.tx.txid) inside(bob2blockchain.expectMsgType[PublishFinalTx]) { p => @@ -517,9 +533,13 @@ class NegotiatingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val aliceClosingSig = alice2bob.expectMsgType[ClosingSig] assert(aliceClosingSig.fees == bobClosingComplete.fees) assert(aliceClosingSig.lockTime == bobClosingComplete.lockTime) + commitmentFormat match { + case _: SegwitV0CommitmentFormat => assert(aliceClosingSig.closerAndCloseeOutputsSig_opt.nonEmpty) + case _: TaprootCommitmentFormat => assert(aliceClosingSig.closerAndCloseeOutputsPartialSig_opt.nonEmpty) + } alice2bob.forward(bob, aliceClosingSig) val bobTx = bob2blockchain.expectMsgType[PublishFinalTx] - assert(bobTx.desc == "closing") + assert(bobTx.desc == "closing-tx") assert(bobTx.fee > 0.sat) bob2blockchain.expectWatchTxConfirmed(bobTx.tx.txid) inside(alice2blockchain.expectMsgType[PublishFinalTx]) { p => @@ -531,19 +551,36 @@ class NegotiatingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike assert(bob.stateName == NEGOTIATING_SIMPLE) } - test("recv ClosingComplete (single output)", Tag(ChannelStateTestsTags.SimpleClose), Tag(ChannelStateTestsTags.NoPushAmount)) { f => + test("recv ClosingComplete (both outputs)", Tag(ChannelStateTestsTags.SimpleClose)) { f => + testReceiveClosingCompleteBothOutputs(f, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) + } + + test("recv ClosingComplete (both outputs, simple taproot channels)", Tag(ChannelStateTestsTags.SimpleClose), Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => + testReceiveClosingCompleteBothOutputs(f, ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat) + } + + def testReceiveClosingCompleteSingleOutput(f: FixtureParam, commitmentFormat: CommitmentFormat): Unit = { import f._ aliceClose(f) val closingComplete = alice2bob.expectMsgType[ClosingComplete] + commitmentFormat match { + case _: SegwitV0CommitmentFormat => assert(closingComplete.closerOutputOnlySig_opt.nonEmpty) + case _: TaprootCommitmentFormat => assert(closingComplete.closerOutputOnlyPartialSig_opt.nonEmpty) + } assert(closingComplete.closerAndCloseeOutputsSig_opt.isEmpty) - assert(closingComplete.closerOutputOnlySig_opt.nonEmpty) + assert(closingComplete.closerAndCloseeOutputsPartialSig_opt.isEmpty) assert(closingComplete.closeeOutputOnlySig_opt.isEmpty) + assert(closingComplete.closeeOutputOnlyPartialSig_opt.isEmpty) // Bob has nothing at stake. bob2alice.expectNoMessage(100 millis) alice2bob.forward(bob, closingComplete) - bob2alice.expectMsgType[ClosingSig] - bob2alice.forward(alice) + val closingSig = bob2alice.expectMsgType[ClosingSig] + commitmentFormat match { + case _: SegwitV0CommitmentFormat => assert(closingSig.closerOutputOnlySig_opt.nonEmpty) + case _: TaprootCommitmentFormat => assert(closingSig.closerOutputOnlyPartialSig_opt.nonEmpty) + } + bob2alice.forward(alice, closingSig) val closingTx = alice2blockchain.expectMsgType[PublishFinalTx] assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == closingTx.tx.txid) alice2blockchain.expectWatchTxConfirmed(closingTx.tx.txid) @@ -552,6 +589,14 @@ class NegotiatingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike assert(bob.stateName == NEGOTIATING_SIMPLE) } + test("recv ClosingComplete (single output)", Tag(ChannelStateTestsTags.SimpleClose), Tag(ChannelStateTestsTags.NoPushAmount)) { f => + testReceiveClosingCompleteSingleOutput(f, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) + } + + test("recv ClosingComplete (single output, taproot)", Tag(ChannelStateTestsTags.SimpleClose), Tag(ChannelStateTestsTags.OptionSimpleTaproot), Tag(ChannelStateTestsTags.NoPushAmount)) { f => + testReceiveClosingCompleteSingleOutput(f, ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat) + } + test("recv ClosingComplete (single output, trimmed)", Tag(ChannelStateTestsTags.SimpleClose), Tag(ChannelStateTestsTags.NoPushAmount)) { f => import f._ val (r, htlc) = addHtlc(250_000 msat, alice, bob, alice2bob, bob2alice) @@ -580,24 +625,40 @@ class NegotiatingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike assert(bob.stateName == NEGOTIATING_SIMPLE) } - test("recv ClosingComplete (missing closee output)", Tag(ChannelStateTestsTags.SimpleClose)) { f => + def testReceiveClosingCompleteMissingCloseeOutput(f: FixtureParam, commitmentFormat: CommitmentFormat): Unit = { import f._ aliceClose(f) val aliceClosingComplete = alice2bob.expectMsgType[ClosingComplete] val bobClosingComplete = bob2alice.expectMsgType[ClosingComplete] - alice2bob.forward(bob, aliceClosingComplete.copy(tlvStream = TlvStream(ClosingTlv.CloserOutputOnly(aliceClosingComplete.closerOutputOnlySig_opt.get)))) + val aliceClosingComplete1 = commitmentFormat match { + case _: SegwitV0CommitmentFormat => aliceClosingComplete.copy(tlvStream = TlvStream(ClosingTlv.CloserOutputOnly(aliceClosingComplete.closerOutputOnlySig_opt.get))) + case _: TaprootCommitmentFormat => aliceClosingComplete.copy(tlvStream = TlvStream(ClosingCompleteTlv.CloserOutputOnlyPartialSignature(aliceClosingComplete.closerOutputOnlyPartialSig_opt.get))) + } + alice2bob.forward(bob, aliceClosingComplete1) // Bob expects to receive a signature for a closing transaction containing his output, so he ignores Alice's // closing_complete instead of sending back his closing_sig. bob2alice.expectMsgType[Warning] bob2alice.expectNoMessage(100 millis) bob2alice.forward(alice, bobClosingComplete) val aliceClosingSig = alice2bob.expectMsgType[ClosingSig] - alice2bob.forward(bob, aliceClosingSig.copy(tlvStream = TlvStream(ClosingTlv.CloseeOutputOnly(aliceClosingSig.closerAndCloseeOutputsSig_opt.get)))) + val aliceClosingSig1 = commitmentFormat match { + case _: SegwitV0CommitmentFormat => aliceClosingSig.copy(tlvStream = TlvStream(ClosingTlv.CloseeOutputOnly(aliceClosingSig.closerAndCloseeOutputsSig_opt.get))) + case _: TaprootCommitmentFormat => aliceClosingSig.copy(tlvStream = TlvStream(ClosingSigTlv.CloseeOutputOnlyPartialSignature(aliceClosingSig.closerAndCloseeOutputsPartialSig_opt.get))) + } + alice2bob.forward(bob, aliceClosingSig1) bob2alice.expectMsgType[Warning] bob2alice.expectNoMessage(100 millis) bob2blockchain.expectNoMessage(100 millis) } + test("recv ClosingComplete (missing closee output)", Tag(ChannelStateTestsTags.SimpleClose)) { f => + testReceiveClosingCompleteMissingCloseeOutput(f, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) + } + + test("recv ClosingComplete (missing closee output, taproot)", Tag(ChannelStateTestsTags.SimpleClose), Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => + testReceiveClosingCompleteMissingCloseeOutput(f, ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat) + } + test("recv ClosingComplete (with concurrent script update)", Tag(ChannelStateTestsTags.SimpleClose)) { f => import f._ aliceClose(f) @@ -880,6 +941,38 @@ class NegotiatingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike awaitCond(bob.stateName == CLOSING) } + test("recv CMD_CLOSE with RBF feerates (taproot)", Tag(ChannelStateTestsTags.SimpleClose), Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => + import f._ + // Alice creates a first closing transaction. + aliceClose(f) + alice2bob.expectMsgType[ClosingComplete] + alice2bob.forward(bob) + bob2alice.expectMsgType[ClosingComplete] // ignored + val aliceTx1 = bob2blockchain.expectMsgType[PublishFinalTx] + bob2blockchain.expectWatchTxConfirmed(aliceTx1.tx.txid) + val closingSig1 = bob2alice.expectMsgType[ClosingSig] + assert(closingSig1.nextCloseeNonce_opt.nonEmpty) + bob2alice.forward(alice, closingSig1) + alice2blockchain.expectFinalTxPublished(aliceTx1.tx.txid) + alice2blockchain.expectWatchTxConfirmed(aliceTx1.tx.txid) + + // Alice sends another closing_complete, updating her fees. + val probe = TestProbe() + val aliceFeerate2 = alice.stateData.asInstanceOf[DATA_NEGOTIATING_SIMPLE].lastClosingFeerate * 1.25 + alice ! CMD_CLOSE(probe.ref, None, Some(ClosingFeerates(aliceFeerate2, aliceFeerate2, aliceFeerate2))) + probe.expectMsgType[RES_SUCCESS[CMD_CLOSE]] + assert(alice2bob.expectMsgType[ClosingComplete].fees > aliceTx1.fee) + alice2bob.forward(bob) + val aliceTx2 = bob2blockchain.expectMsgType[PublishFinalTx] + bob2blockchain.expectWatchTxConfirmed(aliceTx2.tx.txid) + val closingSig2 = bob2alice.expectMsgType[ClosingSig] + assert(closingSig2.nextCloseeNonce_opt.nonEmpty) + assert(closingSig2.nextCloseeNonce_opt != closingSig1.nextCloseeNonce_opt) + bob2alice.forward(alice, closingSig2) + alice2blockchain.expectFinalTxPublished(aliceTx2.tx.txid) + alice2blockchain.expectWatchTxConfirmed(aliceTx2.tx.txid) + } + test("recv CMD_CLOSE with RBF feerate too low", Tag(ChannelStateTestsTags.SimpleClose)) { f => import f._ @@ -900,7 +993,7 @@ class NegotiatingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike test("receive INPUT_RESTORED", Tag(ChannelStateTestsTags.SimpleClose)) { f => import f._ - aliceClose(f) + aliceClose(f, script_opt = Some(Script.write(Script.pay2wpkh(randomKey().publicKey)))) alice2bob.expectMsgType[ClosingComplete] alice2bob.forward(bob) val aliceTx = bob2blockchain.expectMsgType[PublishFinalTx].tx @@ -927,13 +1020,21 @@ class NegotiatingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike // Alice's transaction (published by Bob) confirms. alice ! WatchFundingSpentTriggered(aliceTx) - inside(alice2blockchain.expectMsgType[PublishFinalTx]) { p => + val fee = inside(alice2blockchain.expectMsgType[PublishFinalTx]) { p => assert(p.tx.txid == aliceTx.txid) assert(p.fee > 0.sat) + p.fee } assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == aliceTx.txid) alice ! WatchTxConfirmedTriggered(BlockHeight(100), 3, aliceTx) awaitCond(alice.stateName == CLOSED) + assert(alice.stateData.isInstanceOf[DATA_CLOSED]) + assert(alice.stateData.asInstanceOf[DATA_CLOSED].isChannelOpener) + assert(alice.stateData.asInstanceOf[DATA_CLOSED].closingType == "mutual-close") + assert(alice.stateData.asInstanceOf[DATA_CLOSED].closingTxId == aliceTx.txid) + assert(alice.stateData.asInstanceOf[DATA_CLOSED].remoteNodeId == alice.underlyingActor.remoteNodeId) + assert(alice.stateData.asInstanceOf[DATA_CLOSED].capacity == 1_000_000.sat) + assert(alice.stateData.asInstanceOf[DATA_CLOSED].closingAmount == 800_000.sat - fee) // Bob restarts and detects that Alice's closing transaction is confirmed. bob.setState(WAIT_FOR_INIT_INTERNAL, Nothing) @@ -945,18 +1046,26 @@ class NegotiatingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike assert(bob2blockchain.expectMsgType[WatchTxConfirmed].txId == aliceTx.txid) bob ! WatchTxConfirmedTriggered(BlockHeight(100), 3, aliceTx) awaitCond(bob.stateName == CLOSED) + assert(bob.stateData.isInstanceOf[DATA_CLOSED]) + assert(!bob.stateData.asInstanceOf[DATA_CLOSED].isChannelOpener) + assert(bob.stateData.asInstanceOf[DATA_CLOSED].closingType == "mutual-close") + assert(bob.stateData.asInstanceOf[DATA_CLOSED].closingTxId == aliceTx.txid) + assert(bob.stateData.asInstanceOf[DATA_CLOSED].remoteNodeId == bob.underlyingActor.remoteNodeId) + assert(bob.stateData.asInstanceOf[DATA_CLOSED].capacity == TestConstants.fundingSatoshis) + assert(bob.stateData.asInstanceOf[DATA_CLOSED].closingAmount == 200_000.sat) } test("recv Error") { f => import f._ bobClose(f) alice2bob.expectMsgType[ClosingSigned] - val tx = alice.stateData.asInstanceOf[DATA_NEGOTIATING].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val tx = alice.signCommitTx() alice ! Error(ByteVector32.Zeroes, "oops") awaitCond(alice.stateName == CLOSING) - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) - alice2blockchain.expectMsgType[PublishTx] - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == tx.txid) + alice2blockchain.expectFinalTxPublished(tx.txid) + alice2blockchain.expectReplaceableTxPublished[ClaimLocalAnchorTx] + alice2blockchain.expectFinalTxPublished("local-main-delayed") + alice2blockchain.expectWatchTxConfirmed(tx.txid) } test("recv Error (option_simple_close)", Tag(ChannelStateTestsTags.SimpleClose)) { f => diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala index 8cb3b22709..3216b151d3 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala @@ -16,27 +16,28 @@ package fr.acinq.eclair.channel.states.h -import akka.actor.typed.scaladsl.adapter.{ClassicActorRefOps, actorRefAdapter} +import akka.actor.typed.scaladsl.adapter.actorRefAdapter import akka.testkit.{TestFSMRef, TestProbe} import fr.acinq.bitcoin.ScriptFlags import fr.acinq.bitcoin.scalacompat.Crypto.PrivateKey -import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, Crypto, OutPoint, SatoshiLong, Script, Transaction, TxIn, TxOut} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, Crypto, OutPoint, SatoshiLong, Script, Transaction, TxIn, TxOut} import fr.acinq.eclair.TestUtils.randomTxId -import fr.acinq.eclair.blockchain.DummyOnChainWallet import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ import fr.acinq.eclair.blockchain.fee.{ConfirmationPriority, ConfirmationTarget, FeeratePerKw, FeeratesPerKw} import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.channel.fsm.Channel.{BITCOIN_FUNDING_PUBLISH_FAILED, BITCOIN_FUNDING_TIMEOUT} -import fr.acinq.eclair.channel.publish.TxPublisher.{PublishFinalTx, PublishReplaceableTx, PublishTx, SetChannelId} +import fr.acinq.eclair.channel.publish.TxPublisher.{PublishFinalTx, PublishReplaceableTx, SetChannelId} import fr.acinq.eclair.channel.states.ChannelStateTestsBase.PimpTestFSM import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags} import fr.acinq.eclair.payment._ import fr.acinq.eclair.payment.relay.Relayer._ +import fr.acinq.eclair.reputation.Reputation +import fr.acinq.eclair.testutils.PimpTestProbe.convert import fr.acinq.eclair.transactions.Transactions import fr.acinq.eclair.transactions.Transactions._ import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{BlockHeight, CltvExpiry, CltvExpiryDelta, Features, MilliSatoshiLong, TestConstants, TestKitBaseClass, TimestampSecond, randomBytes32, randomKey} +import fr.acinq.eclair.{BlockHeight, CltvExpiry, CltvExpiryDelta, MilliSatoshiLong, TestConstants, TestKitBaseClass, TimestampSecond, randomBytes32, randomKey} import org.scalatest.Inside.inside import org.scalatest.funsuite.FixtureAnyFunSuiteLike import org.scalatest.{Outcome, Tag} @@ -50,7 +51,7 @@ import scala.concurrent.duration._ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with ChannelStateTestsBase { - case class FixtureParam(alice: TestFSMRef[ChannelState, ChannelData, Channel], bob: TestFSMRef[ChannelState, ChannelData, Channel], alice2bob: TestProbe, bob2alice: TestProbe, alice2blockchain: TestProbe, bob2blockchain: TestProbe, alice2relayer: TestProbe, bob2relayer: TestProbe, channelUpdateListener: TestProbe, txListener: TestProbe, eventListener: TestProbe, bobCommitTxs: List[CommitTxAndRemoteSig]) + case class FixtureParam(alice: TestFSMRef[ChannelState, ChannelData, Channel], bob: TestFSMRef[ChannelState, ChannelData, Channel], alice2bob: TestProbe, bob2alice: TestProbe, alice2blockchain: TestProbe, bob2blockchain: TestProbe, alice2relayer: TestProbe, bob2relayer: TestProbe, channelUpdateListener: TestProbe, txListener: TestProbe, eventListener: TestProbe, bobCommitTxs: List[Transaction]) override def withFixture(test: OneArgTest): Outcome = { val setup = init() @@ -71,14 +72,10 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with if (unconfirmedFundingTx) { within(30 seconds) { - val channelConfig = ChannelConfig.standard - val channelFlags = ChannelFlags(announceChannel = false) - val (aliceParams, bobParams, channelType) = computeFeatures(setup, test.tags, channelFlags) - val aliceInit = Init(aliceParams.initFeatures) - val bobInit = Init(bobParams.initFeatures) - alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, TestConstants.fundingSatoshis, dualFunded = false, TestConstants.feeratePerKw, TestConstants.feeratePerKw, fundingTxFeeBudget_opt = None, Some(TestConstants.initiatorPushAmount), requireConfirmedInputs = false, requestFunding_opt = None, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType, replyTo = aliceOpenReplyTo.ref.toTyped) + val channelParams = computeChannelParams(setup, test.tags) + alice ! channelParams.initChannelAlice(TestConstants.fundingSatoshis, pushAmount_opt = Some(TestConstants.initiatorPushAmount)) alice2blockchain.expectMsgType[SetChannelId] - bob ! INPUT_INIT_CHANNEL_NON_INITIATOR(ByteVector32.Zeroes, None, dualFunded = false, None, requireConfirmedInputs = false, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) + bob ! channelParams.initChannelBob() bob2blockchain.expectMsgType[SetChannelId] alice2bob.expectMsgType[OpenChannel] alice2bob.forward(bob) @@ -117,20 +114,20 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with systemA.eventStream.subscribe(txListener.ref, classOf[TransactionConfirmed]) systemB.eventStream.subscribe(txListener.ref, classOf[TransactionPublished]) systemB.eventStream.subscribe(txListener.ref, classOf[TransactionConfirmed]) - val bobCommitTxs: List[CommitTxAndRemoteSig] = (for (amt <- List(100000000 msat, 200000000 msat, 300000000 msat)) yield { + val bobCommitTxs = List(100_000_000 msat, 200_000_000 msat, 300_000_000 msat).flatMap(amt => { val (r, htlc) = addHtlc(amt, alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) bob2relayer.expectMsgType[RelayForward] - val bobCommitTx1 = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig + val bobCommitTx1 = bob.signCommitTx() fulfillHtlc(htlc.id, r, bob, alice, bob2alice, alice2bob) // alice forwards the fulfill upstream alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.Fulfill]] crossSign(bob, alice, bob2alice, alice2bob) // bob confirms that it has forwarded the fulfill to alice awaitCond(bob.nodeParams.db.pendingCommands.listSettlementCommands(htlc.channelId).isEmpty) - val bobCommitTx2 = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig + val bobCommitTx2 = bob.signCommitTx() bobCommitTx1 :: bobCommitTx2 :: Nil - }).flatten + }) awaitCond(alice.stateName == NORMAL) awaitCond(bob.stateName == NORMAL) @@ -144,8 +141,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val sender = TestProbe() alice ! CMD_FORCECLOSE(sender.ref) awaitCond(alice.stateName == CLOSING) - alice2blockchain.expectMsgType[PublishTx] - alice2blockchain.expectMsgType[PublishTx] // claim-main-delayed + alice2blockchain.expectFinalTxPublished("commit-tx") eventListener.expectMsgType[ChannelAborted] // test starts here @@ -159,8 +155,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val sender = TestProbe() alice ! CMD_FORCECLOSE(sender.ref) awaitCond(alice.stateName == CLOSING) - alice2blockchain.expectMsgType[PublishTx] - alice2blockchain.expectMsgType[PublishTx] // claim-main-delayed + alice2blockchain.expectFinalTxPublished("commit-tx") eventListener.expectMsgType[ChannelAborted] // test starts here @@ -176,10 +171,11 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice ! CMD_FORCECLOSE(sender.ref) awaitCond(alice.stateName == CLOSING) alice2bob.expectMsgType[Error] - alice2blockchain.expectMsgType[PublishTx] - alice2blockchain.expectMsgType[PublishTx] // claim-main-delayed - alice2blockchain.expectMsgType[WatchTxConfirmed] // commitment - alice2blockchain.expectMsgType[WatchTxConfirmed] // claim-main-delayed + val commitTx = alice2blockchain.expectFinalTxPublished("commit-tx").tx + val anchorTx = alice2blockchain.expectReplaceableTxPublished[ClaimLocalAnchorTx] + val claimMain = alice2blockchain.expectFinalTxPublished("local-main-delayed").tx + alice2blockchain.expectWatchTxConfirmed(commitTx.txid) + alice2blockchain.expectWatchOutputsSpent(Seq(claimMain.txIn.head.outPoint, anchorTx.input.outPoint)) eventListener.expectMsgType[ChannelAborted] // test starts here @@ -196,16 +192,18 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice ! CMD_FORCECLOSE(sender.ref) awaitCond(alice.stateName == CLOSING) alice2bob.expectMsgType[Error] - alice2blockchain.expectMsgType[PublishTx] - alice2blockchain.expectMsgType[PublishTx] // claim-main-delayed - alice2blockchain.expectMsgType[WatchTxConfirmed] // commitment - alice2blockchain.expectMsgType[WatchTxConfirmed] // claim-main-delayed + val commitTx = alice2blockchain.expectFinalTxPublished("commit-tx").tx + val anchorTx = alice2blockchain.expectReplaceableTxPublished[ClaimLocalAnchorTx] + val claimMain = alice2blockchain.expectFinalTxPublished("local-main-delayed").tx + alice2blockchain.expectWatchTxConfirmed(commitTx.txid) + alice2blockchain.expectWatchOutputsSpent(Seq(claimMain.txIn.head.outPoint, anchorTx.input.outPoint)) eventListener.expectMsgType[ChannelAborted] // test starts here alice ! GetTxWithMetaResponse(fundingTx.txid, None, TimestampSecond.now()) alice2bob.expectNoMessage(100 millis) assert(alice2blockchain.expectMsgType[PublishFinalTx].tx == fundingTx) // we republish the funding tx + alice2blockchain.expectNoMessage(100 millis) assert(alice.stateName == CLOSING) // the above expectNoMsg will make us wait, so this checks that we are still in CLOSING } @@ -216,10 +214,11 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with bob ! CMD_FORCECLOSE(sender.ref) awaitCond(bob.stateName == CLOSING) bob2alice.expectMsgType[Error] - bob2blockchain.expectMsgType[PublishTx] - bob2blockchain.expectMsgType[PublishTx] // claim-main-delayed - bob2blockchain.expectMsgType[WatchTxConfirmed] // commitment - bob2blockchain.expectMsgType[WatchTxConfirmed] // claim-main-delayed + val commitTx = bob2blockchain.expectFinalTxPublished("commit-tx").tx + val anchorTx = bob2blockchain.expectReplaceableTxPublished[ClaimLocalAnchorTx] + val claimMain = bob2blockchain.expectFinalTxPublished("local-main-delayed").tx + bob2blockchain.expectWatchTxConfirmed(commitTx.txid) + bob2blockchain.expectWatchOutputsSpent(Seq(claimMain.txIn.head.outPoint, anchorTx.input.outPoint)) eventListener.expectMsgType[ChannelAborted] // test starts here @@ -236,10 +235,11 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with bob ! CMD_FORCECLOSE(sender.ref) awaitCond(bob.stateName == CLOSING) bob2alice.expectMsgType[Error] - bob2blockchain.expectMsgType[PublishTx] - bob2blockchain.expectMsgType[PublishTx] // claim-main-delayed - bob2blockchain.expectMsgType[WatchTxConfirmed] // commitment - bob2blockchain.expectMsgType[WatchTxConfirmed] // claim-main-delayed + val commitTx = bob2blockchain.expectFinalTxPublished("commit-tx").tx + val anchorTx = bob2blockchain.expectReplaceableTxPublished[ClaimLocalAnchorTx] + val claimMain = bob2blockchain.expectFinalTxPublished("local-main-delayed").tx + bob2blockchain.expectWatchTxConfirmed(commitTx.txid) + bob2blockchain.expectWatchOutputsSpent(Seq(claimMain.txIn.head.outPoint, anchorTx.input.outPoint)) eventListener.expectMsgType[ChannelAborted] // test starts here @@ -256,10 +256,11 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with bob ! CMD_FORCECLOSE(sender.ref) awaitCond(bob.stateName == CLOSING) bob2alice.expectMsgType[Error] - bob2blockchain.expectMsgType[PublishTx] - bob2blockchain.expectMsgType[PublishTx] // claim-main-delayed - bob2blockchain.expectMsgType[WatchTxConfirmed] // commitment - bob2blockchain.expectMsgType[WatchTxConfirmed] // claim-main-delayed + val commitTx = bob2blockchain.expectFinalTxPublished("commit-tx").tx + val anchorTx = bob2blockchain.expectReplaceableTxPublished[ClaimLocalAnchorTx] + val claimMain = bob2blockchain.expectFinalTxPublished("local-main-delayed").tx + bob2blockchain.expectWatchTxConfirmed(commitTx.txid) + bob2blockchain.expectWatchOutputsSpent(Seq(claimMain.txIn.head.outPoint, anchorTx.input.outPoint)) eventListener.expectMsgType[ChannelAborted] // test starts here @@ -276,7 +277,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // actual test starts here val sender = TestProbe() - val add = CMD_ADD_HTLC(sender.ref, 500000000 msat, ByteVector32(ByteVector.fill(32)(1)), cltvExpiry = CltvExpiry(300000), onion = TestConstants.emptyOnionPacket, None, 1.0, None, localOrigin(sender.ref)) + val add = CMD_ADD_HTLC(sender.ref, 500000000 msat, ByteVector32(ByteVector.fill(32)(1)), cltvExpiry = CltvExpiry(300000), onion = TestConstants.emptyOnionPacket, None, Reputation.Score.max, None, localOrigin(sender.ref)) alice ! add val error = ChannelUnavailable(channelId(alice)) sender.expectMsg(RES_ADD_FAILED(add, error, None)) @@ -289,17 +290,15 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // actual test starts here val sender = TestProbe() - val c = CMD_FULFILL_HTLC(42, randomBytes32(), replyTo_opt = Some(sender.ref)) + val c = CMD_FULFILL_HTLC(42, randomBytes32(), None, replyTo_opt = Some(sender.ref)) alice ! c sender.expectMsg(RES_FAILURE(c, UnknownHtlcId(channelId(alice), 42))) - - // NB: nominal case is tested in IntegrationSpec } - def testMutualCloseBeforeConverge(f: FixtureParam, channelFeatures: ChannelFeatures): Unit = { + def testMutualCloseBeforeConverge(f: FixtureParam, commitmentFormat: CommitmentFormat): Unit = { import f._ val sender = TestProbe() - assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.params.channelFeatures == channelFeatures) + assert(alice.commitments.latest.commitmentFormat == commitmentFormat) bob.setBitcoinCoreFeerates(FeeratesPerKw.single(FeeratePerKw(2500 sat)).copy(minimum = FeeratePerKw(250 sat), slow = FeeratePerKw(250 sat))) // alice initiates a closing with a low fee alice ! CMD_CLOSE(sender.ref, None, Some(ClosingFeerates(FeeratePerKw(500 sat), FeeratePerKw(250 sat), FeeratePerKw(1000 sat)))) @@ -327,11 +326,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with } test("recv WatchFundingSpentTriggered (mutual close before converging)") { f => - testMutualCloseBeforeConverge(f, ChannelFeatures(Features.StaticRemoteKey)) - } - - test("recv WatchFundingSpentTriggered (mutual close before converging, anchor outputs)", Tag(ChannelStateTestsTags.AnchorOutputs)) { f => - testMutualCloseBeforeConverge(f, ChannelFeatures(Features.StaticRemoteKey, Features.AnchorOutputs)) + testMutualCloseBeforeConverge(f, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) } test("recv WatchTxConfirmedTriggered (mutual close)") { f => @@ -339,7 +334,6 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with mutualClose(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain) val mutualCloseTx = alice.stateData.asInstanceOf[DATA_CLOSING].mutualClosePublished.last - // actual test starts here alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, mutualCloseTx.tx) awaitCond(alice.stateName == CLOSED) } @@ -359,12 +353,11 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with test("recv WatchFundingSpentTriggered (local commit)") { f => import f._ // an error occurs and alice publishes her commit tx - val aliceCommitTx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val aliceCommitTx = alice.signCommitTx() localClose(alice, alice2blockchain) val initialState = alice.stateData.asInstanceOf[DATA_CLOSING] assert(initialState.localCommitPublished.isDefined) - // actual test starts here // we are notified afterwards from our watcher about the tx that we just published alice ! WatchFundingSpentTriggered(aliceCommitTx) assert(alice.stateData == initialState) // this was a no-op @@ -392,26 +385,24 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with crossSign(alice, bob, alice2bob, bob2alice) // Bob has the preimage for those HTLCs, but Alice force-closes before receiving it. - bob ! CMD_FULFILL_HTLC(htlc1.id, preimage) + bob ! CMD_FULFILL_HTLC(htlc1.id, preimage, None) bob2alice.expectMsgType[UpdateFulfillHtlc] // ignored - val lcp = localClose(alice, alice2blockchain) + val (lcp, closingTxs) = localClose(alice, alice2blockchain, htlcTimeoutCount = 2) + assert(lcp.htlcOutputs.size == 2) + assert(closingTxs.htlcTimeoutTxs.size == 2) val initialState = alice.stateData.asInstanceOf[DATA_CLOSING] assert(initialState.localCommitPublished.contains(lcp)) // Bob claims the htlc output from Alice's commit tx using its preimage. bob ! WatchFundingSpentTriggered(lcp.commitTx) - if (initialState.commitments.params.channelFeatures.hasFeature(Features.AnchorOutputsZeroFeeHtlcTx)) { - assert(bob2blockchain.expectMsgType[PublishReplaceableTx].txInfo.isInstanceOf[ClaimLocalAnchorOutputTx]) - bob2blockchain.expectMsgType[PublishFinalTx] // main-delayed - } - val claimHtlcSuccessTx1 = bob2blockchain.expectMsgType[PublishReplaceableTx] - assert(claimHtlcSuccessTx1.txInfo.isInstanceOf[ClaimHtlcSuccessTx]) - val claimHtlcSuccessTx2 = bob2blockchain.expectMsgType[PublishReplaceableTx] - assert(claimHtlcSuccessTx2.txInfo.isInstanceOf[ClaimHtlcSuccessTx]) - assert(claimHtlcSuccessTx1.input != claimHtlcSuccessTx2.input) + bob2blockchain.expectReplaceableTxPublished[ClaimRemoteAnchorTx] + bob2blockchain.expectFinalTxPublished("remote-main-delayed") + val claimHtlcSuccessTx1 = bob2blockchain.expectReplaceableTxPublished[ClaimHtlcSuccessTx].sign() + val claimHtlcSuccessTx2 = bob2blockchain.expectReplaceableTxPublished[ClaimHtlcSuccessTx].sign() + assert(Seq(claimHtlcSuccessTx1, claimHtlcSuccessTx2).flatMap(_.txIn.map(_.outPoint)).toSet == lcp.htlcOutputs) // Alice extracts the preimage and forwards it upstream. - alice ! WatchOutputSpentTriggered(htlc1.amountMsat.truncateToSatoshi, claimHtlcSuccessTx1.txInfo.tx) + alice ! WatchOutputSpentTriggered(htlc1.amountMsat.truncateToSatoshi, claimHtlcSuccessTx1) Seq(htlc1, htlc2).foreach(htlc => inside(alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFulfill]]) { fulfill => assert(fulfill.htlc == htlc) assert(fulfill.result.paymentPreimage == preimage) @@ -420,8 +411,8 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(alice.stateData == initialState) // this was a no-op // The Claim-HTLC-success transaction confirms: nothing to do, preimage has already been relayed. - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == claimHtlcSuccessTx1.txInfo.tx.txid) - alice ! WatchTxConfirmedTriggered(alice.nodeParams.currentBlockHeight, 6, claimHtlcSuccessTx1.txInfo.tx) + alice2blockchain.expectWatchTxConfirmed(claimHtlcSuccessTx1.txid) + alice ! WatchTxConfirmedTriggered(alice.nodeParams.currentBlockHeight, 6, claimHtlcSuccessTx1) alice2blockchain.expectNoMessage(100 millis) alice2relayer.expectNoMessage(100 millis) } @@ -430,7 +421,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with extractPreimageFromClaimHtlcSuccess(f) } - test("recv WatchOutputSpentTriggered (extract preimage from Claim-HTLC-success tx, anchor outputs)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + test("recv WatchOutputSpentTriggered (extract preimage from Claim-HTLC-success tx, taproot)", Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => extractPreimageFromClaimHtlcSuccess(f) } @@ -444,19 +435,17 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with crossSign(alice, bob, alice2bob, bob2alice) // Bob has the preimage for those HTLCs, but he force-closes before Alice receives it. - bob ! CMD_FULFILL_HTLC(htlc1.id, preimage) + bob ! CMD_FULFILL_HTLC(htlc1.id, preimage, None) bob2alice.expectMsgType[UpdateFulfillHtlc] // ignored - val rcp = localClose(bob, bob2blockchain) + val (rcp, closingTxs) = localClose(bob, bob2blockchain, htlcSuccessCount = 2) // Bob claims the htlc outputs from his own commit tx using its preimage. - assert(rcp.htlcTxs.size == 2) - rcp.htlcTxs.values.foreach(tx_opt => assert(tx_opt.nonEmpty)) - val htlcSuccessTxs = rcp.htlcTxs.values.flatten - htlcSuccessTxs.foreach(tx => assert(tx.isInstanceOf[HtlcSuccessTx])) + assert(rcp.htlcOutputs.size == 2) + assert(closingTxs.htlcSuccessTxs.size == 2) // Alice extracts the preimage and forwards it upstream. alice ! WatchFundingSpentTriggered(rcp.commitTx) - alice ! WatchOutputSpentTriggered(htlc1.amountMsat.truncateToSatoshi, htlcSuccessTxs.head.tx) + alice ! WatchOutputSpentTriggered(htlc1.amountMsat.truncateToSatoshi, closingTxs.htlcSuccessTxs.head) Seq(htlc1, htlc2).foreach(htlc => inside(alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFulfill]]) { fulfill => assert(fulfill.htlc == htlc) assert(fulfill.result.paymentPreimage == preimage) @@ -464,7 +453,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with }) // The HTLC-success transaction confirms: nothing to do, preimage has already been relayed. - alice ! WatchTxConfirmedTriggered(alice.nodeParams.currentBlockHeight, 6, htlcSuccessTxs.head.tx) + alice ! WatchTxConfirmedTriggered(alice.nodeParams.currentBlockHeight, 6, closingTxs.htlcSuccessTxs.head) alice2relayer.expectNoMessage(100 millis) } @@ -472,7 +461,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with extractPreimageFromHtlcSuccess(f) } - test("recv WatchOutputSpentTriggered (extract preimage from HTLC-success tx, anchor outputs)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + test("recv WatchOutputSpentTriggered (extract preimage from HTLC-success tx, taproot)", Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => extractPreimageFromHtlcSuccess(f) } @@ -501,34 +490,24 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // At that point, the HTLCs are not in Alice's commitment anymore. // But Bob has not revoked his commitment yet that contains them. bob.setState(NORMAL, bobStateWithHtlc) - bob ! CMD_FULFILL_HTLC(htlc1.id, preimage) + bob ! CMD_FULFILL_HTLC(htlc1.id, preimage, None) bob2alice.expectMsgType[UpdateFulfillHtlc] // ignored // Bob claims the htlc outputs from his previous commit tx using its preimage. - val rcp = localClose(bob, bob2blockchain) - assert(rcp.htlcTxs.size == 3) - val htlcSuccessTxs = rcp.htlcTxs.values.flatten - assert(htlcSuccessTxs.size == 2) // Bob doesn't have the preimage for the last HTLC. - htlcSuccessTxs.foreach(tx => assert(tx.isInstanceOf[HtlcSuccessTx])) + val (rcp, closingTxs) = localClose(bob, bob2blockchain, htlcSuccessCount = 2) + assert(rcp.htlcOutputs.size == 3) + assert(closingTxs.htlcSuccessTxs.size == 2) // Bob doesn't have the preimage for the last HTLC. // Alice prepares Claim-HTLC-timeout transactions for each HTLC. alice ! WatchFundingSpentTriggered(rcp.commitTx) - if (alice.stateData.asInstanceOf[DATA_CLOSING].commitments.params.channelFeatures.hasFeature(Features.AnchorOutputsZeroFeeHtlcTx)) { - assert(alice2blockchain.expectMsgType[PublishReplaceableTx].txInfo.isInstanceOf[ClaimLocalAnchorOutputTx]) - assert(alice2blockchain.expectMsgType[PublishFinalTx].desc == "remote-main-delayed") - } - Seq(htlc1, htlc2, htlc3).foreach(_ => assert(alice2blockchain.expectMsgType[PublishReplaceableTx].txInfo.isInstanceOf[ClaimHtlcTimeoutTx])) - val claimHtlcTimeoutTxs = getClaimHtlcTimeoutTxs(alice.stateData.asInstanceOf[DATA_CLOSING].remoteCommitPublished.get) - assert(claimHtlcTimeoutTxs.size == 3) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == rcp.commitTx.txid) - if (alice.stateData.asInstanceOf[DATA_CLOSING].commitments.params.channelFeatures.hasFeature(Features.AnchorOutputsZeroFeeHtlcTx)) { - alice2blockchain.expectMsgType[WatchTxConfirmed] // remote-main-delayed - } - assert(Set( - alice2blockchain.expectMsgType[WatchOutputSpent].outputIndex, - alice2blockchain.expectMsgType[WatchOutputSpent].outputIndex, - alice2blockchain.expectMsgType[WatchOutputSpent].outputIndex, - ) == claimHtlcTimeoutTxs.map(_.input.outPoint.index).toSet) + val anchorTx = alice2blockchain.expectReplaceableTxPublished[ClaimRemoteAnchorTx] + val mainTx = alice2blockchain.expectFinalTxPublished("remote-main-delayed") + val claimHtlcTimeoutTxs = Seq(htlc1, htlc2, htlc3).map(_ => alice2blockchain.expectReplaceableTxPublished[ClaimHtlcTimeoutTx]) + assert(claimHtlcTimeoutTxs.map(_.htlcId).toSet == Set(htlc1, htlc2, htlc3).map(_.id)) + alice2blockchain.expectWatchTxConfirmed(rcp.commitTx.txid) + alice2blockchain.expectWatchOutputSpent(mainTx.input) + alice2blockchain.expectWatchOutputSpent(anchorTx.input.outPoint) + alice2blockchain.expectWatchOutputsSpent(claimHtlcTimeoutTxs.map(_.input.outPoint)) alice2blockchain.expectNoMessage(100 millis) // Bob's commitment confirms. @@ -537,7 +516,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice2relayer.expectNoMessage(100 millis) // Alice extracts the preimage from Bob's HTLC-success and forwards it upstream. - alice ! WatchOutputSpentTriggered(htlc1.amountMsat.truncateToSatoshi, htlcSuccessTxs.head.tx) + alice ! WatchOutputSpentTriggered(htlc1.amountMsat.truncateToSatoshi, closingTxs.htlcSuccessTxs.head) Seq(htlc1, htlc2).foreach(htlc => inside(alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFulfill]]) { fulfill => assert(fulfill.htlc == htlc) assert(fulfill.result.paymentPreimage == preimage) @@ -546,15 +525,16 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice2relayer.expectNoMessage(100 millis) // The HTLC-success transaction confirms: nothing to do, preimage has already been relayed. - alice ! WatchTxConfirmedTriggered(alice.nodeParams.currentBlockHeight, 6, htlcSuccessTxs.head.tx) + alice ! WatchTxConfirmedTriggered(alice.nodeParams.currentBlockHeight, 6, closingTxs.htlcSuccessTxs.head) alice2relayer.expectNoMessage(100 millis) // Alice's Claim-HTLC-timeout transaction confirms: we relay the failure upstream. val claimHtlcTimeout = claimHtlcTimeoutTxs.find(_.htlcId == htlc3.id).get + val origin = alice.stateData.asInstanceOf[DATA_CLOSING].commitments.originChannels(htlc3.id) alice ! WatchTxConfirmedTriggered(alice.nodeParams.currentBlockHeight, 13, claimHtlcTimeout.tx) inside(alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]]) { fail => assert(fail.htlc == htlc3) - assert(fail.origin == alice.stateData.asInstanceOf[DATA_CLOSING].commitments.originChannels(htlc3.id)) + assert(fail.origin == origin) } } @@ -562,7 +542,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with extractPreimageFromRemovedHtlc(f) } - test("recv WatchOutputSpentTriggered (extract preimage for removed HTLC, anchor outputs)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + test("recv WatchOutputSpentTriggered (extract preimage for removed HTLC, taproot)", Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => extractPreimageFromRemovedHtlc(f) } @@ -590,35 +570,30 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice.setState(NORMAL, aliceStateWithoutHtlcs) // At that point, the HTLCs are not in Alice's commitment yet. - val rcp = localClose(bob, bob2blockchain) - assert(rcp.htlcTxs.size == 3) + val (rcp, closingTxs) = localClose(bob, bob2blockchain) + assert(rcp.htlcOutputs.size == 3) // Bob doesn't have the preimage yet for any of those HTLCs. - rcp.htlcTxs.values.foreach(tx_opt => assert(tx_opt.isEmpty)) + assert(closingTxs.htlcTxs.isEmpty) // Bob receives the preimage for the first two HTLCs. - bob ! CMD_FULFILL_HTLC(htlc1.id, preimage) - awaitCond(bob.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.get.htlcTxs.values.exists(_.nonEmpty)) - val htlcSuccessTxs = bob.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.get.htlcTxs.values.flatten.filter(_.isInstanceOf[HtlcSuccessTx]).toSeq - assert(htlcSuccessTxs.map(_.htlcId).toSet == Set(htlc1.id, htlc2.id)) - val batchHtlcSuccessTx = Transaction(2, htlcSuccessTxs.flatMap(_.tx.txIn), htlcSuccessTxs.flatMap(_.tx.txOut), 0) + bob ! CMD_FULFILL_HTLC(htlc1.id, preimage, None) + val htlcSuccessTxs = { + val htlcSuccess = (0 until 2).map(_ => bob2blockchain.expectReplaceableTxPublished[HtlcSuccessTx]) + assert(htlcSuccess.map(_.htlcId).toSet == Set(htlc1.id, htlc2.id)) + htlcSuccess.map(_.sign()) + } + bob2blockchain.expectNoMessage(100 millis) + val batchHtlcSuccessTx = Transaction(2, htlcSuccessTxs.flatMap(_.txIn), htlcSuccessTxs.flatMap(_.txOut), 0) // Alice prepares Claim-HTLC-timeout transactions for each HTLC. alice ! WatchFundingSpentTriggered(rcp.commitTx) - if (alice.stateData.asInstanceOf[DATA_CLOSING].commitments.params.channelFeatures.hasFeature(Features.AnchorOutputsZeroFeeHtlcTx)) { - assert(alice2blockchain.expectMsgType[PublishReplaceableTx].txInfo.isInstanceOf[ClaimLocalAnchorOutputTx]) - assert(alice2blockchain.expectMsgType[PublishFinalTx].desc == "remote-main-delayed") - } - Seq(htlc1, htlc2, htlc3).foreach(_ => assert(alice2blockchain.expectMsgType[PublishReplaceableTx].txInfo.isInstanceOf[ClaimHtlcTimeoutTx])) - val claimHtlcTimeoutTxs = getClaimHtlcTimeoutTxs(alice.stateData.asInstanceOf[DATA_CLOSING].nextRemoteCommitPublished.get) - assert(claimHtlcTimeoutTxs.size == 3) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == rcp.commitTx.txid) - if (alice.stateData.asInstanceOf[DATA_CLOSING].commitments.params.channelFeatures.hasFeature(Features.AnchorOutputsZeroFeeHtlcTx)) { - alice2blockchain.expectMsgType[WatchTxConfirmed] // remote-main-delayed - } - assert(Set( - alice2blockchain.expectMsgType[WatchOutputSpent].outputIndex, - alice2blockchain.expectMsgType[WatchOutputSpent].outputIndex, - alice2blockchain.expectMsgType[WatchOutputSpent].outputIndex, - ) == claimHtlcTimeoutTxs.map(_.input.outPoint.index).toSet) + val anchorTx = alice2blockchain.expectReplaceableTxPublished[ClaimRemoteAnchorTx] + val mainTx = alice2blockchain.expectFinalTxPublished("remote-main-delayed") + val claimHtlcTimeoutTxs = Seq(htlc1, htlc2, htlc3).map(_ => alice2blockchain.expectReplaceableTxPublished[ClaimHtlcTimeoutTx]) + assert(claimHtlcTimeoutTxs.map(_.htlcId).toSet == Set(htlc1, htlc2, htlc3).map(_.id)) + alice2blockchain.expectWatchTxConfirmed(rcp.commitTx.txid) + alice2blockchain.expectWatchOutputSpent(mainTx.input) + alice2blockchain.expectWatchOutputSpent(anchorTx.input.outPoint) + alice2blockchain.expectWatchOutputsSpent(claimHtlcTimeoutTxs.map(_.input.outPoint)) alice2blockchain.expectNoMessage(100 millis) // Bob's commitment confirms. @@ -628,12 +603,12 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // Alice extracts the preimage from Bob's batched HTLC-success and forwards it upstream. alice ! WatchOutputSpentTriggered(htlc1.amountMsat.truncateToSatoshi, batchHtlcSuccessTx) + alice2blockchain.expectWatchTxConfirmed(batchHtlcSuccessTx.txid) Seq(htlc1, htlc2).foreach(htlc => inside(alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFulfill]]) { fulfill => assert(fulfill.htlc == htlc) assert(fulfill.result.paymentPreimage == preimage) assert(fulfill.origin == alice.stateData.asInstanceOf[DATA_CLOSING].commitments.originChannels(htlc.id)) }) - alice2relayer.expectNoMessage(100 millis) // The HTLC-success transaction confirms: nothing to do, preimage has already been relayed. alice ! WatchTxConfirmedTriggered(alice.nodeParams.currentBlockHeight, 6, batchHtlcSuccessTx) @@ -641,60 +616,44 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // Alice's Claim-HTLC-timeout transaction confirms: we relay the failure upstream. val claimHtlcTimeout = claimHtlcTimeoutTxs.find(_.htlcId == htlc3.id).get + val origin = alice.stateData.asInstanceOf[DATA_CLOSING].commitments.originChannels(htlc3.id) alice ! WatchTxConfirmedTriggered(alice.nodeParams.currentBlockHeight, 13, claimHtlcTimeout.tx) inside(alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]]) { fail => assert(fail.htlc == htlc3) - assert(fail.origin == alice.stateData.asInstanceOf[DATA_CLOSING].commitments.originChannels(htlc3.id)) + assert(fail.origin == origin) } + alice2relayer.expectNoMessage(100 millis) } test("recv WatchOutputSpentTriggered (extract preimage for next batch of HTLCs)") { f => extractPreimageFromNextHtlcs(f) } - test("recv WatchOutputSpentTriggered (extract preimage for next batch of HTLCs, anchor outputs)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + test("recv WatchOutputSpentTriggered (extract preimage for next batch of HTLCs, taproot)", Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => extractPreimageFromNextHtlcs(f) } - test("recv CMD_BUMP_FORCE_CLOSE_FEE (local commit)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + test("recv CMD_BUMP_FORCE_CLOSE_FEE (local commit)") { f => import f._ - localClose(alice, alice2blockchain) + val (localCommitPublished, _) = localClose(alice, alice2blockchain) val initialState = alice.stateData.asInstanceOf[DATA_CLOSING] assert(initialState.localCommitPublished.nonEmpty) - val localCommitPublished1 = initialState.localCommitPublished.get - assert(localCommitPublished1.claimAnchorTxs.nonEmpty) - val Some(localAnchor1) = localCommitPublished1.claimAnchorTxs.collectFirst { case tx: ClaimLocalAnchorOutputTx => tx } - assert(localAnchor1.confirmationTarget == ConfirmationTarget.Priority(ConfirmationPriority.Medium)) val replyTo = TestProbe() alice ! CMD_BUMP_FORCE_CLOSE_FEE(replyTo.ref, ConfirmationTarget.Priority(ConfirmationPriority.Fast)) replyTo.expectMsgType[RES_SUCCESS[CMD_BUMP_FORCE_CLOSE_FEE]] - val localAnchor2 = inside(alice2blockchain.expectMsgType[PublishReplaceableTx]) { tx => - assert(tx.txInfo.isInstanceOf[ClaimLocalAnchorOutputTx]) - assert(tx.commitTx == localCommitPublished1.commitTx) - tx.txInfo.asInstanceOf[ClaimLocalAnchorOutputTx] + inside(alice2blockchain.expectMsgType[PublishReplaceableTx]) { publish => + assert(publish.txInfo.isInstanceOf[ClaimLocalAnchorTx]) + assert(publish.commitTx == localCommitPublished.commitTx) + assert(publish.confirmationTarget == ConfirmationTarget.Priority(ConfirmationPriority.Fast)) } - assert(localAnchor2.confirmationTarget == ConfirmationTarget.Priority(ConfirmationPriority.Fast)) - val localCommitPublished2 = alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.get - assert(localCommitPublished2.claimAnchorTxs.contains(localAnchor2)) - - // If we try bumping again, but with a lower priority, this won't override the previous priority. - alice ! CMD_BUMP_FORCE_CLOSE_FEE(replyTo.ref, ConfirmationTarget.Priority(ConfirmationPriority.Medium)) - replyTo.expectMsgType[RES_SUCCESS[CMD_BUMP_FORCE_CLOSE_FEE]] - val localAnchor3 = inside(alice2blockchain.expectMsgType[PublishReplaceableTx]) { tx => - assert(tx.txInfo.isInstanceOf[ClaimLocalAnchorOutputTx]) - assert(tx.commitTx == localCommitPublished1.commitTx) - tx.txInfo.asInstanceOf[ClaimLocalAnchorOutputTx] - } - assert(localAnchor3.confirmationTarget == ConfirmationTarget.Priority(ConfirmationPriority.Fast)) - assert(alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.contains(localCommitPublished2)) } - def testLocalCommitTxConfirmed(f: FixtureParam, channelFeatures: ChannelFeatures): Unit = { + def testLocalCommitTxConfirmed(f: FixtureParam, commitmentFormat: CommitmentFormat): Unit = { import f._ - assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.params.channelFeatures == channelFeatures) + assert(alice.commitments.latest.commitmentFormat == commitmentFormat) val listener = TestProbe() systemA.eventStream.subscribe(listener.ref, classOf[LocalCommitConfirmed]) @@ -703,21 +662,21 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // alice sends an htlc to bob val (_, htlca1) = addHtlc(50_000_000 msat, alice, bob, alice2bob, bob2alice) // alice sends an htlc below dust to bob - val amountBelowDust = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.params.localParams.dustLimit - 100.msat + val amountBelowDust = alice.commitments.latest.localCommitParams.dustLimit - 100.msat val (_, htlca2) = addHtlc(amountBelowDust, alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) - val closingState = localClose(alice, alice2blockchain) + val (closingState, closingTxs) = localClose(alice, alice2blockchain, htlcTimeoutCount = 1) // actual test starts here - assert(closingState.claimMainDelayedOutputTx.isDefined) - assert(closingState.htlcTxs.size == 1) - assert(getHtlcSuccessTxs(closingState).isEmpty) - assert(getHtlcTimeoutTxs(closingState).length == 1) - val htlcTimeoutTx = getHtlcTimeoutTxs(closingState).head.tx - assert(closingState.claimHtlcDelayedTxs.isEmpty) + assert(closingState.localOutput_opt.isDefined) + assert(closingTxs.mainTx_opt.isDefined) + assert(closingState.htlcOutputs.size == 1) + assert(closingTxs.htlcTimeoutTxs.size == 1) + val htlcTimeoutTx = closingTxs.htlcTimeoutTxs.head + assert(closingState.htlcDelayedOutputs.isEmpty) alice ! WatchTxConfirmedTriggered(BlockHeight(42), 0, closingState.commitTx) assert(txListener.expectMsgType[TransactionConfirmed].tx == closingState.commitTx) - assert(listener.expectMsgType[LocalCommitConfirmed].refundAtBlock == BlockHeight(42) + bob.stateData.asInstanceOf[DATA_NORMAL].commitments.params.localParams.toSelfDelay.toInt) + assert(listener.expectMsgType[LocalCommitConfirmed].refundAtBlock == BlockHeight(42) + alice.commitments.latest.localCommitParams.toSelfDelay.toInt) assert(listener.expectMsgType[PaymentSettlingOnChain].paymentHash == htlca1.paymentHash) // htlcs below dust will never reach the chain, once the commit tx is confirmed we can consider them failed inside(alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]]) { settled => @@ -725,9 +684,9 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(settled.origin == alice.stateData.asInstanceOf[DATA_CLOSING].commitments.originChannels(htlca2.id)) } alice2relayer.expectNoMessage(100 millis) - alice ! WatchTxConfirmedTriggered(BlockHeight(200), 0, closingState.claimMainDelayedOutputTx.get.tx) + alice ! WatchTxConfirmedTriggered(BlockHeight(200), 0, closingTxs.mainTx_opt.get) alice ! WatchTxConfirmedTriggered(BlockHeight(201), 0, htlcTimeoutTx) - assert(alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.get.irrevocablySpent.values.toSet == Set(closingState.commitTx, closingState.claimMainDelayedOutputTx.get.tx, htlcTimeoutTx)) + assert(alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.get.irrevocablySpent.values.toSet == Set(closingState.commitTx, closingTxs.mainTx_opt.get, htlcTimeoutTx)) inside(alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]]) { settled => assert(settled.htlc == htlca1) assert(settled.origin == alice.stateData.asInstanceOf[DATA_CLOSING].commitments.originChannels(htlca1.id)) @@ -735,20 +694,24 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice2relayer.expectNoMessage(100 millis) // We claim the htlc-delayed output now that the HTLC tx has been confirmed. - val claimHtlcDelayedTx = alice2blockchain.expectMsgType[PublishFinalTx] - Transaction.correctlySpends(claimHtlcDelayedTx.tx, Seq(htlcTimeoutTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.get.claimHtlcDelayedTxs.length == 1) - alice ! WatchTxConfirmedTriggered(BlockHeight(202), 0, claimHtlcDelayedTx.tx) - + val htlcDelayedTx = alice2blockchain.expectFinalTxPublished("htlc-delayed") + assert(htlcDelayedTx.input == OutPoint(htlcTimeoutTx, 0)) + Transaction.correctlySpends(htlcDelayedTx.tx, Seq(htlcTimeoutTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + alice2blockchain.expectWatchOutputSpent(htlcDelayedTx.input) + alice ! WatchOutputSpentTriggered(0 sat, htlcDelayedTx.tx) + alice2blockchain.expectWatchTxConfirmed(htlcDelayedTx.tx.txid) + alice2blockchain.expectNoMessage(100 millis) + assert(alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.get.htlcDelayedOutputs.size == 1) + alice ! WatchTxConfirmedTriggered(BlockHeight(202), 0, htlcDelayedTx.tx) awaitCond(alice.stateName == CLOSED) } - test("recv WatchTxConfirmedTriggered (local commit)") { f => - testLocalCommitTxConfirmed(f, ChannelFeatures(Features.StaticRemoteKey)) + test("recv WatchTxConfirmedTriggered (local commit)", Tag(ChannelStateTestsTags.AnchorOutputsPhoenix)) { f => + testLocalCommitTxConfirmed(f, UnsafeLegacyAnchorOutputsCommitmentFormat) } - test("recv WatchTxConfirmedTriggered (local commit, anchor outputs)", Tag(ChannelStateTestsTags.AnchorOutputs)) { f => - testLocalCommitTxConfirmed(f, ChannelFeatures(Features.StaticRemoteKey, Features.AnchorOutputs)) + test("recv WatchTxConfirmedTriggered (local commit, taproot)", Tag(ChannelStateTestsTags.OptionSimpleTaprootPhoenix)) { f => + testLocalCommitTxConfirmed(f, PhoenixSimpleTaprootChannelCommitmentFormat) } test("recv WatchTxConfirmedTriggered (local commit with multiple htlcs for the same payment)") { f => @@ -761,47 +724,52 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val htlca2 = addHtlc(cmd2, alice, bob, alice2bob, bob2alice) val (_, cmd3) = makeCmdAdd(30_000_000 msat, bob.nodeParams.nodeId, alice.nodeParams.currentBlockHeight, ra1) val htlca3 = addHtlc(cmd3, alice, bob, alice2bob, bob2alice) - val amountBelowDust = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.params.localParams.dustLimit - 100.msat + val amountBelowDust = alice.commitments.latest.localCommitParams.dustLimit - 100.msat val (_, dustCmd) = makeCmdAdd(amountBelowDust, bob.nodeParams.nodeId, alice.nodeParams.currentBlockHeight, ra1) val dust = addHtlc(dustCmd, alice, bob, alice2bob, bob2alice) val (_, cmd4) = makeCmdAdd(20_000_000 msat, bob.nodeParams.nodeId, alice.nodeParams.currentBlockHeight + 1, ra1) val htlca4 = addHtlc(cmd4, alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) - val closingState = localClose(alice, alice2blockchain) + val (closingState, closingTxs) = localClose(alice, alice2blockchain, htlcTimeoutCount = 4) // actual test starts here - assert(closingState.claimMainDelayedOutputTx.isDefined) - assert(closingState.htlcTxs.size == 4) - assert(getHtlcSuccessTxs(closingState).isEmpty) - val htlcTimeoutTxs = getHtlcTimeoutTxs(closingState).map(_.tx) - assert(htlcTimeoutTxs.length == 4) - assert(closingState.claimHtlcDelayedTxs.isEmpty) + assert(closingState.localOutput_opt.isDefined) + assert(closingTxs.mainTx_opt.isDefined) + assert(closingState.htlcOutputs.size == 4) + assert(closingTxs.htlcTimeoutTxs.size == 4) + assert(closingState.htlcDelayedOutputs.isEmpty) // if commit tx and htlc-timeout txs end up in the same block, we may receive the htlc-timeout confirmation before the commit tx confirmation - alice ! WatchTxConfirmedTriggered(BlockHeight(42), 0, htlcTimeoutTxs(0)) + alice ! WatchTxConfirmedTriggered(BlockHeight(42), 0, closingTxs.htlcTimeoutTxs(0)) val forwardedFail1 = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc alice2relayer.expectNoMessage(100 millis) alice ! WatchTxConfirmedTriggered(BlockHeight(42), 1, closingState.commitTx) assert(alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc == dust) alice2relayer.expectNoMessage(100 millis) - alice ! WatchTxConfirmedTriggered(BlockHeight(200), 0, closingState.claimMainDelayedOutputTx.get.tx) - alice ! WatchTxConfirmedTriggered(BlockHeight(202), 0, htlcTimeoutTxs(1)) + alice ! WatchTxConfirmedTriggered(BlockHeight(200), 0, closingTxs.mainTx_opt.get) + alice ! WatchTxConfirmedTriggered(BlockHeight(202), 0, closingTxs.htlcTimeoutTxs(1)) val forwardedFail2 = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc alice2relayer.expectNoMessage(100 millis) - alice ! WatchTxConfirmedTriggered(BlockHeight(202), 1, htlcTimeoutTxs(2)) + alice ! WatchTxConfirmedTriggered(BlockHeight(202), 1, closingTxs.htlcTimeoutTxs(2)) val forwardedFail3 = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc alice2relayer.expectNoMessage(100 millis) - alice ! WatchTxConfirmedTriggered(BlockHeight(203), 0, htlcTimeoutTxs(3)) + alice ! WatchTxConfirmedTriggered(BlockHeight(203), 0, closingTxs.htlcTimeoutTxs(3)) val forwardedFail4 = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc assert(Set(forwardedFail1, forwardedFail2, forwardedFail3, forwardedFail4) == Set(htlca1, htlca2, htlca3, htlca4)) alice2relayer.expectNoMessage(100 millis) - awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.get.claimHtlcDelayedTxs.length == 4) - val claimHtlcDelayedTxs = alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.get.claimHtlcDelayedTxs - alice ! WatchTxConfirmedTriggered(BlockHeight(203), 0, claimHtlcDelayedTxs(0).tx) - alice ! WatchTxConfirmedTriggered(BlockHeight(203), 1, claimHtlcDelayedTxs(1).tx) - alice ! WatchTxConfirmedTriggered(BlockHeight(203), 2, claimHtlcDelayedTxs(2).tx) - alice ! WatchTxConfirmedTriggered(BlockHeight(203), 3, claimHtlcDelayedTxs(3).tx) + val htlcDelayedTxs = closingTxs.htlcTimeoutTxs.map(htlcTx => { + val htlcDelayedTx = alice2blockchain.expectFinalTxPublished("htlc-delayed") + assert(htlcDelayedTx.input == OutPoint(htlcTx, 0)) + alice2blockchain.expectWatchOutputSpent(htlcDelayedTx.input) + htlcDelayedTx + }) + awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.get.htlcDelayedOutputs.size == 4) + htlcDelayedTxs.foreach(tx => { + alice ! WatchOutputSpentTriggered(0 sat, tx.tx) + alice2blockchain.expectWatchTxConfirmed(tx.tx.txid) + }) + htlcDelayedTxs.foreach(tx => alice ! WatchTxConfirmedTriggered(BlockHeight(203), 0, tx.tx)) awaitCond(alice.stateName == CLOSED) } @@ -809,7 +777,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with import f._ val listener = TestProbe() systemA.eventStream.subscribe(listener.ref, classOf[PaymentSettlingOnChain]) - val aliceCommitTx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val aliceCommitTx = alice.signCommitTx() // alice sends an htlc val (_, htlc) = addHtlc(50_000_000 msat, alice, bob, alice2bob, bob2alice) // and signs it (but bob doesn't sign it) @@ -817,15 +785,16 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice2bob.expectMsgType[CommitSig] // note that bob doesn't receive the new sig! // then we make alice unilaterally close the channel - val closingState = localClose(alice, alice2blockchain) + val (closingState, closingTxs) = localClose(alice, alice2blockchain) // actual test starts here channelUpdateListener.expectMsgType[LocalChannelDown] - assert(closingState.htlcTxs.isEmpty && closingState.claimHtlcDelayedTxs.isEmpty) + assert(closingState.htlcOutputs.isEmpty && closingState.htlcDelayedOutputs.isEmpty) + assert(closingTxs.htlcTxs.isEmpty) // when the commit tx is confirmed, alice knows that the htlc she sent right before the unilateral close will never reach the chain + val origin = alice.stateData.asInstanceOf[DATA_CLOSING].commitments.originChannels(htlc.id) alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, aliceCommitTx) // so she fails it - val origin = alice.stateData.asInstanceOf[DATA_CLOSING].commitments.originChannels(htlc.id) alice2relayer.expectMsg(RES_ADD_SETTLED(origin, htlc, HtlcResult.OnChainFail(HtlcOverriddenByLocalCommit(channelId(alice), htlc)))) // the htlc will not settle on chain listener.expectNoMessage(100 millis) @@ -841,18 +810,19 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with bob2alice.forward(alice) alice2bob.expectMsgType[RevokeAndAck] alice2relayer.expectNoMessage(100 millis) // the HTLC is not relayed downstream - assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.htlcTxsAndRemoteSigs.size == 1) - val aliceCommitTx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.htlcRemoteSigs.size == 1) + val aliceCommitTx = alice.signCommitTx() // Note that alice has not signed the htlc yet! // We make her unilaterally close the channel. - val closingState = localClose(alice, alice2blockchain) + val (closingState, closingTxs) = localClose(alice, alice2blockchain) channelUpdateListener.expectMsgType[LocalChannelDown] - assert(closingState.htlcTxs.isEmpty && closingState.claimHtlcDelayedTxs.isEmpty) + assert(closingState.htlcOutputs.isEmpty && closingState.htlcDelayedOutputs.isEmpty) + assert(closingTxs.htlcTxs.isEmpty) // Alice should ignore the htlc (she hasn't relayed it yet): it is Bob's responsibility to claim it. // Once the commit tx and her main output are confirmed, she can consider the channel closed. alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, aliceCommitTx) - closingState.claimMainDelayedOutputTx.foreach(claimMain => alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, claimMain.tx)) + closingTxs.mainTx_opt.foreach(tx => alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, tx)) alice2relayer.expectNoMessage(100 millis) awaitCond(alice.stateName == CLOSED) } @@ -870,17 +840,18 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice2bob.forward(bob) bob2alice.expectMsgType[RevokeAndAck] // not received by Alice alice2relayer.expectNoMessage(100 millis) // the HTLC is not relayed downstream - assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.htlcTxsAndRemoteSigs.size == 1) - val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.htlcRemoteSigs.size == 1) + val bobCommitTx = bob.signCommitTx() // We make Bob unilaterally close the channel. - val rcp = remoteClose(bobCommitTx, alice, alice2blockchain) + val (rcp, closingTxs) = remoteClose(bobCommitTx, alice, alice2blockchain) channelUpdateListener.expectMsgType[LocalChannelDown] - assert(rcp.claimHtlcTxs.isEmpty) + assert(rcp.htlcOutputs.isEmpty) + assert(closingTxs.htlcTxs.isEmpty) // Alice should ignore the htlc (she hasn't relayed it yet): it is Bob's responsibility to claim it. // Once the commit tx and her main output are confirmed, she can consider the channel closed. alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, bobCommitTx) - rcp.claimMainOutputTx.foreach(claimMain => alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, claimMain.tx)) + closingTxs.mainTx_opt.foreach(tx => alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, tx)) alice2relayer.expectNoMessage(100 millis) awaitCond(alice.stateName == CLOSED) } @@ -891,11 +862,11 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val (r, htlc) = addHtlc(110_000_000 msat, bob, alice, bob2alice, alice2bob) crossSign(bob, alice, bob2alice, alice2bob) assert(alice2relayer.expectMsgType[RelayForward].add == htlc) - val aliceCommitTx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx - assert(aliceCommitTx.txOut.size == 3) // 2 main outputs + 1 htlc + val aliceCommitTx = alice.signCommitTx() + assert(aliceCommitTx.txOut.size == 5) // 2 main outputs + 2 anchor outputs + 1 htlc // alice fulfills the HTLC but bob doesn't receive the signature - alice ! CMD_FULFILL_HTLC(htlc.id, r, commit = true) + alice ! CMD_FULFILL_HTLC(htlc.id, r, None, commit = true) alice2bob.expectMsgType[UpdateFulfillHtlc] alice2bob.forward(bob) inside(bob2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.Fulfill]]) { settled => @@ -906,10 +877,8 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice2bob.expectMsgType[CommitSig] // note that bob doesn't receive the new sig! // then we make alice unilaterally close the channel - val closingState = localClose(alice, alice2blockchain) + val (closingState, _) = localClose(alice, alice2blockchain, htlcSuccessCount = 1) assert(closingState.commitTx.txid == aliceCommitTx.txid) - assert(getHtlcTimeoutTxs(closingState).isEmpty) - assert(getHtlcSuccessTxs(closingState).length == 1) } test("recv WatchTxConfirmedTriggered (local commit with fail not acked by remote)") { f => @@ -929,35 +898,105 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with bob2alice.expectMsgType[RevokeAndAck] // note that alice doesn't receive the last revocation // then we make alice unilaterally close the channel - val closingState = localClose(alice, alice2blockchain) - assert(closingState.commitTx.txOut.length == 2) // htlc has been removed + val (closingState, closingTxs) = localClose(alice, alice2blockchain) + assert(closingState.commitTx.txOut.length == 4) // htlc has been removed (only main outputs and anchor outputs) // actual test starts here channelUpdateListener.expectMsgType[LocalChannelDown] - assert(closingState.htlcTxs.isEmpty && closingState.claimHtlcDelayedTxs.isEmpty) + assert(closingState.htlcOutputs.isEmpty && closingState.htlcDelayedOutputs.isEmpty) + assert(closingTxs.htlcTxs.isEmpty) // when the commit tx is confirmed, alice knows that the htlc will never reach the chain + val origin = alice.stateData.asInstanceOf[DATA_CLOSING].commitments.originChannels(htlc.id) alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, closingState.commitTx) // so she fails it - val origin = alice.stateData.asInstanceOf[DATA_CLOSING].commitments.originChannels(htlc.id) alice2relayer.expectMsg(RES_ADD_SETTLED(origin, htlc, HtlcResult.OnChainFail(HtlcOverriddenByLocalCommit(channelId(alice), htlc)))) // the htlc will not settle on chain listener.expectNoMessage(100 millis) alice2relayer.expectNoMessage(100 millis) } + test("recv WatchTxConfirmedTriggered (local commit followed by htlc settlement)") { f => + import f._ + // Bob sends 2 HTLCs to Alice that will be settled during the force-close: one will be fulfilled, the other will be failed. + val (r1, htlc1) = addHtlc(75_000_000 msat, CltvExpiryDelta(48), bob, alice, bob2alice, alice2bob) + val (_, htlc2) = addHtlc(65_000_000 msat, CltvExpiryDelta(36), bob, alice, bob2alice, alice2bob) + crossSign(bob, alice, bob2alice, alice2bob) + assert(alice2relayer.expectMsgType[RelayForward].add == htlc1) + assert(alice2relayer.expectMsgType[RelayForward].add == htlc2) + + // Alice force-closes. + val (closingState, closingTxs) = localClose(alice, alice2blockchain) + assert(closingState.commitTx.txOut.length == 6) // 2 main outputs + 2 anchor outputs + 2 htlcs + assert(closingState.localOutput_opt.nonEmpty) + assert(closingState.htlcOutputs.size == 2) + assert(closingTxs.htlcTxs.isEmpty) // we don't have the preimage to claim the htlc-success yet + + // Alice's commitment and main transaction confirm: she waits for the HTLC outputs to be spent. + alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, closingState.commitTx) + closingTxs.mainTx_opt.foreach(tx => alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, tx)) + assert(alice.stateName == CLOSING) + + // Alice receives the preimage for the first HTLC from downstream; she can now claim the corresponding HTLC output. + alice ! CMD_FULFILL_HTLC(htlc1.id, r1, None, commit = true) + val htlcSuccess = alice2blockchain.expectReplaceableTxPublished[HtlcSuccessTx](ConfirmationTarget.Absolute(htlc1.cltvExpiry.blockHeight)) + assert(htlcSuccess.preimage == r1) + Transaction.correctlySpends(htlcSuccess.sign(), closingState.commitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + alice2blockchain.expectNoMessage(100 millis) + + // Alice receives a failure for the second HTLC from downstream; she can stop watching the corresponding HTLC output. + alice ! CMD_FAIL_HTLC(htlc2.id, FailureReason.EncryptedDownstreamFailure(ByteVector.empty, None), None) + alice2blockchain.expectNoMessage(100 millis) + + // Alice restarts before the HTLC transaction confirmed. + val beforeRestart1 = alice.stateData.asInstanceOf[DATA_CLOSING] + alice.setState(WAIT_FOR_INIT_INTERNAL, Nothing) + alice ! INPUT_RESTORED(beforeRestart1) + alice2blockchain.expectMsgType[SetChannelId] + awaitCond(alice.stateName == CLOSING) + // Alice republishes the HTLC-success transaction, which then confirms. + assert(alice2blockchain.expectReplaceableTxPublished[HtlcSuccessTx].input == htlcSuccess.input) + alice2blockchain.expectWatchOutputSpent(htlcSuccess.input.outPoint) + alice ! WatchOutputSpentTriggered(htlcSuccess.amountIn, htlcSuccess.tx) + alice2blockchain.expectWatchTxConfirmed(htlcSuccess.tx.txid) + alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, htlcSuccess.tx) + // Alice publishes a 3rd-stage HTLC transaction. + val htlcDelayedTx = alice2blockchain.expectFinalTxPublished("htlc-delayed") + assert(htlcDelayedTx.input == OutPoint(htlcSuccess.tx, 0)) + alice2blockchain.expectWatchOutputSpent(htlcDelayedTx.input) + alice2blockchain.expectNoMessage(100 millis) + + // Alice restarts again before the 3rd-stage HTLC transaction confirmed. + val beforeRestart2 = alice.stateData.asInstanceOf[DATA_CLOSING] + alice.setState(WAIT_FOR_INIT_INTERNAL, Nothing) + alice ! INPUT_RESTORED(beforeRestart2) + alice2blockchain.expectMsgType[SetChannelId] + awaitCond(alice.stateName == CLOSING) + // Alice republishes the 3rd-stage HTLC transaction, which then confirms. + alice2blockchain.expectFinalTxPublished(htlcDelayedTx.tx.txid) + alice2blockchain.expectWatchOutputSpent(htlcDelayedTx.input) + alice ! WatchOutputSpentTriggered(0 sat, htlcDelayedTx.tx) + alice2blockchain.expectWatchTxConfirmed(htlcDelayedTx.tx.txid) + alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, htlcDelayedTx.tx) + alice2blockchain.expectNoMessage(100 millis) + alice2relayer.expectNoMessage(100 millis) + awaitCond(alice.stateName == CLOSED) + } + test("recv INPUT_RESTORED (local commit)") { f => import f._ // alice sends an htlc to bob addHtlc(50_000_000 msat, alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) - val closingState = localClose(alice, alice2blockchain) - val htlcTimeoutTx = getHtlcTimeoutTxs(closingState).head + val (closingState, closingTxs) = localClose(alice, alice2blockchain, htlcTimeoutCount = 1) + assert(closingTxs.mainTx_opt.nonEmpty) + val htlcTimeoutTx = closingTxs.htlcTimeoutTxs.head - // simulate a node restart after a feerate increase + // simulate a node restart after a feerate increase that exceeds our max-closing-feerate val beforeRestart = alice.stateData.asInstanceOf[DATA_CLOSING] alice.setState(WAIT_FOR_INIT_INTERNAL, Nothing) - alice.nodeParams.setBitcoinCoreFeerates(FeeratesPerKw.single(FeeratePerKw(15_000 sat))) + assert(alice.nodeParams.onChainFeeConf.maxClosingFeerate == FeeratePerKw(15_000 sat)) + alice.nodeParams.setBitcoinCoreFeerates(FeeratesPerKw.single(FeeratePerKw(20_000 sat))) alice ! INPUT_RESTORED(beforeRestart) alice2blockchain.expectMsgType[SetChannelId] awaitCond(alice.stateName == CLOSING) @@ -965,21 +1004,26 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // the commit tx hasn't been confirmed yet, so we watch the funding output first alice2blockchain.expectMsgType[WatchFundingSpent] // then we should re-publish unconfirmed transactions - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx == closingState.commitTx) - closingState.claimMainDelayedOutputTx.foreach(claimMain => assert(alice2blockchain.expectMsgType[PublishFinalTx].tx == claimMain.tx)) - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx == htlcTimeoutTx.tx) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == closingState.commitTx.txid) - closingState.claimMainDelayedOutputTx.foreach(claimMain => assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == claimMain.tx.txid)) - assert(alice2blockchain.expectMsgType[WatchOutputSpent].outputIndex == htlcTimeoutTx.input.outPoint.index) + alice2blockchain.expectFinalTxPublished(closingState.commitTx.txid) + // we increase the feerate of our main transaction, but cap it to our max-closing-feerate + alice2blockchain.expectReplaceableTxPublished[ClaimLocalAnchorTx] + val mainTx2 = closingTxs.mainTx_opt.map(_ => alice2blockchain.expectFinalTxPublished("local-main-delayed")).get + assert(mainTx2.tx.txOut.head.amount < closingTxs.mainTx_opt.get.txOut.head.amount) + val mainFeerate = Transactions.fee2rate(mainTx2.fee, mainTx2.tx.weight()) + assert(mainFeerate <= FeeratePerKw(15_000 sat)) + assert(alice2blockchain.expectReplaceableTxPublished[HtlcTimeoutTx].input.outPoint == htlcTimeoutTx.txIn.head.outPoint) + alice2blockchain.expectWatchTxConfirmed(closingState.commitTx.txid) + closingTxs.mainTx_opt.foreach(tx => alice2blockchain.expectWatchOutputSpent(tx.txIn.head.outPoint)) + alice2blockchain.expectWatchOutputSpent(closingTxs.anchorTx.txIn.head.outPoint) + alice2blockchain.expectWatchOutputSpent(htlcTimeoutTx.txIn.head.outPoint) // the htlc transaction confirms, so we publish a 3rd-stage transaction alice ! WatchTxConfirmedTriggered(BlockHeight(2701), 1, closingState.commitTx) - alice ! WatchTxConfirmedTriggered(BlockHeight(2702), 0, htlcTimeoutTx.tx) - awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.get.claimHtlcDelayedTxs.nonEmpty) + alice ! WatchTxConfirmedTriggered(BlockHeight(2702), 0, htlcTimeoutTx) + val htlcDelayed = alice2blockchain.expectFinalTxPublished("htlc-delayed") + alice2blockchain.expectWatchOutputSpent(htlcDelayed.input) + awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.get.htlcDelayedOutputs.nonEmpty) val beforeSecondRestart = alice.stateData.asInstanceOf[DATA_CLOSING] - val claimHtlcTimeoutTx = beforeSecondRestart.localCommitPublished.get.claimHtlcDelayedTxs.head - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx == claimHtlcTimeoutTx.tx) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == claimHtlcTimeoutTx.tx.txid) // simulate another node restart alice.setState(WAIT_FOR_INIT_INTERNAL, Nothing) @@ -988,13 +1032,21 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with awaitCond(alice.stateName == CLOSING) // we should re-publish unconfirmed transactions - closingState.claimMainDelayedOutputTx.foreach(claimMain => assert(alice2blockchain.expectMsgType[PublishFinalTx].tx == claimMain.tx)) - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx == claimHtlcTimeoutTx.tx) - closingState.claimMainDelayedOutputTx.foreach(claimMain => assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == claimMain.tx.txid)) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == claimHtlcTimeoutTx.tx.txid) + closingTxs.mainTx_opt.foreach(mainTx => { + alice2blockchain.expectFinalTxPublished("local-main-delayed") + alice2blockchain.expectWatchOutputSpent(mainTx.txIn.head.outPoint) + }) + assert(alice2blockchain.expectFinalTxPublished("htlc-delayed").input == htlcDelayed.input) + alice2blockchain.expectWatchOutputSpent(htlcDelayed.input) + // the main transaction confirms + closingTxs.mainTx_opt.foreach(mainTx => alice ! WatchTxConfirmedTriggered(BlockHeight(2801), 5, mainTx)) + assert(alice.stateName == CLOSING) + // the htlc delayed transaction confirms + alice ! WatchTxConfirmedTriggered(BlockHeight(2802), 5, htlcDelayed.tx) + awaitCond(alice.stateName == CLOSED) } - test("recv INPUT_RESTORED (local commit with htlc-delayed transactions)", Tag(ChannelStateTestsTags.AnchorOutputs)) { f => + test("recv INPUT_RESTORED (local commit with htlc-delayed transactions)", Tag(ChannelStateTestsTags.AnchorOutputsPhoenix)) { f => import f._ // Alice has one incoming and one outgoing HTLC. @@ -1003,45 +1055,32 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with crossSign(alice, bob, alice2bob, bob2alice) // Alice force-closes. - val closingState1 = localClose(alice, alice2blockchain) - assert(closingState1.claimMainDelayedOutputTx.nonEmpty) - val claimMainTx = closingState1.claimMainDelayedOutputTx.get.tx - assert(getHtlcSuccessTxs(closingState1).isEmpty) - assert(getHtlcTimeoutTxs(closingState1).length == 1) - val htlcTimeoutTx = getHtlcTimeoutTxs(closingState1).head.tx + val (closingState1, closingTxs) = localClose(alice, alice2blockchain, htlcTimeoutCount = 1) + assert(closingTxs.mainTx_opt.nonEmpty) + val htlcTimeoutTx = closingTxs.htlcTimeoutTxs.head // The commit tx confirms. alice ! WatchTxConfirmedTriggered(BlockHeight(42), 0, closingState1.commitTx) + alice ! WatchTxConfirmedTriggered(BlockHeight(42), 1, closingTxs.anchorTx) alice2blockchain.expectNoMessage(100 millis) // Alice receives the preimage for the incoming HTLC. - alice ! CMD_FULFILL_HTLC(incomingHtlc.id, preimage, commit = true) - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == claimMainTx.txid) - assert(alice2blockchain.expectMsgType[PublishReplaceableTx].txInfo.isInstanceOf[HtlcTimeoutTx]) - assert(alice2blockchain.expectMsgType[PublishReplaceableTx].txInfo.isInstanceOf[HtlcSuccessTx]) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == claimMainTx.txid) - alice2blockchain.expectMsgType[WatchOutputSpent] - alice2blockchain.expectMsgType[WatchOutputSpent] + alice ! CMD_FULFILL_HTLC(incomingHtlc.id, preimage, None, commit = true) + val htlcSuccess = alice2blockchain.expectReplaceableTxPublished[HtlcSuccessTx] + assert(htlcSuccess.preimage == preimage) alice2blockchain.expectNoMessage(100 millis) - val closingState2 = alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.get - assert(getHtlcSuccessTxs(closingState2).length == 1) - val htlcSuccessTx = getHtlcSuccessTxs(closingState2).head.tx // The HTLC txs confirms, so we publish 3rd-stage txs. alice ! WatchTxConfirmedTriggered(BlockHeight(201), 0, htlcTimeoutTx) - val claimHtlcTimeoutDelayedTx = alice2blockchain.expectMsgType[PublishFinalTx].tx - inside(alice2blockchain.expectMsgType[WatchTxConfirmed]) { w => - assert(w.txId == claimHtlcTimeoutDelayedTx.txid) - assert(w.delay_opt.map(_.parentTxId).contains(htlcTimeoutTx.txid)) - } - Transaction.correctlySpends(claimHtlcTimeoutDelayedTx, Seq(htlcTimeoutTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - alice ! WatchTxConfirmedTriggered(BlockHeight(201), 0, htlcSuccessTx) - val claimHtlcSuccessDelayedTx = alice2blockchain.expectMsgType[PublishFinalTx].tx - inside(alice2blockchain.expectMsgType[WatchTxConfirmed]) { w => - assert(w.txId == claimHtlcSuccessDelayedTx.txid) - assert(w.delay_opt.map(_.parentTxId).contains(htlcSuccessTx.txid)) - } - Transaction.correctlySpends(claimHtlcSuccessDelayedTx, Seq(htlcSuccessTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + val htlcTimeoutDelayedTx = alice2blockchain.expectFinalTxPublished("htlc-delayed") + assert(htlcTimeoutDelayedTx.input == OutPoint(htlcTimeoutTx, 0)) + alice2blockchain.expectWatchOutputSpent(htlcTimeoutDelayedTx.input) + Transaction.correctlySpends(htlcTimeoutDelayedTx.tx, Seq(htlcTimeoutTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + alice ! WatchTxConfirmedTriggered(BlockHeight(201), 0, htlcSuccess.tx) + val htlcSuccessDelayedTx = alice2blockchain.expectFinalTxPublished("htlc-delayed") + assert(htlcSuccessDelayedTx.input == OutPoint(htlcSuccess.tx, 0)) + alice2blockchain.expectWatchOutputSpent(htlcSuccessDelayedTx.input) + Transaction.correctlySpends(htlcSuccessDelayedTx.tx, Seq(htlcSuccess.tx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) // We simulate a node restart after a feerate increase. val beforeRestart = alice.stateData.asInstanceOf[DATA_CLOSING] @@ -1051,45 +1090,185 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice2blockchain.expectMsgType[SetChannelId] awaitCond(alice.stateName == CLOSING) - // We re-publish closing transactions. - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == claimMainTx.txid) - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == claimHtlcTimeoutDelayedTx.txid) - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == claimHtlcSuccessDelayedTx.txid) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == claimMainTx.txid) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == claimHtlcTimeoutDelayedTx.txid) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == claimHtlcSuccessDelayedTx.txid) + // We re-publish closing transactions with a higher feerate. + val mainTx = alice2blockchain.expectFinalTxPublished("local-main-delayed") + assert(mainTx.tx.txOut.head.amount < closingTxs.mainTx_opt.get.txOut.head.amount) + alice2blockchain.expectWatchOutputSpent(mainTx.input) + val htlcDelayedTxs = Seq( + alice2blockchain.expectFinalTxPublished("htlc-delayed"), + alice2blockchain.expectFinalTxPublished("htlc-delayed"), + ) + assert(htlcDelayedTxs.map(_.input).toSet == Seq(htlcTimeoutDelayedTx, htlcSuccessDelayedTx).map(_.input).toSet) + assert(htlcDelayedTxs.flatMap(_.tx.txOut.map(_.amount)).sum < Seq(htlcTimeoutDelayedTx, htlcSuccessDelayedTx).flatMap(_.tx.txOut.map(_.amount)).sum) + alice2blockchain.expectWatchOutputsSpent(htlcDelayedTxs.map(_.input)) // We replay the HTLC fulfillment: nothing happens since we already published a 3rd-stage transaction. - alice ! CMD_FULFILL_HTLC(incomingHtlc.id, preimage, commit = true) + alice ! CMD_FULFILL_HTLC(incomingHtlc.id, preimage, None, commit = true) alice2blockchain.expectNoMessage(100 millis) // The remaining transactions confirm. - alice ! WatchTxConfirmedTriggered(BlockHeight(43), 0, claimMainTx) - alice ! WatchTxConfirmedTriggered(BlockHeight(43), 1, claimHtlcTimeoutDelayedTx) - alice ! WatchTxConfirmedTriggered(BlockHeight(43), 2, claimHtlcSuccessDelayedTx) + alice ! WatchTxConfirmedTriggered(BlockHeight(43), 0, mainTx.tx) + alice ! WatchTxConfirmedTriggered(BlockHeight(43), 1, htlcTimeoutDelayedTx.tx) + alice ! WatchTxConfirmedTriggered(BlockHeight(43), 2, htlcSuccessDelayedTx.tx) awaitCond(alice.stateName == CLOSED) } + test("recv INPUT_RESTORED (htlcs claimed by both local and remote)") { f => + import f._ + + // Alice and Bob each sends 3 HTLCs: + // - one of them will be fulfilled and claimed with the preimage on-chain + // - one of them will be fulfilled but will lose the race with the htlc-timeout on-chain + // - the other will be timed out on-chain + val (r1a, htlc1a) = addHtlc(50_000_000 msat, alice, bob, alice2bob, bob2alice) + val (r2a, htlc2a) = addHtlc(55_000_000 msat, alice, bob, alice2bob, bob2alice) + addHtlc(60_000_000 msat, alice, bob, alice2bob, bob2alice) + val (r1b, htlc1b) = addHtlc(75_000_000 msat, bob, alice, bob2alice, alice2bob) + val (r2b, htlc2b) = addHtlc(55_000_000 msat, bob, alice, bob2alice, alice2bob) + addHtlc(40_000_000 msat, bob, alice, bob2alice, alice2bob) + crossSign(alice, bob, alice2bob, bob2alice) + // Bob has the preimage for 2 of the 3 HTLCs he received. + bob ! CMD_FULFILL_HTLC(htlc1a.id, r1a, None) + bob2alice.expectMsgType[UpdateFulfillHtlc] + bob ! CMD_FULFILL_HTLC(htlc2a.id, r2a, None) + bob2alice.expectMsgType[UpdateFulfillHtlc] + // Alice has the preimage for 2 of the 3 HTLCs she received. + alice ! CMD_FULFILL_HTLC(htlc1b.id, r1b, None) + alice2bob.expectMsgType[UpdateFulfillHtlc] + alice ! CMD_FULFILL_HTLC(htlc2b.id, r2b, None) + alice2bob.expectMsgType[UpdateFulfillHtlc] + + // Alice force-closes. + val (closingStateAlice, closingTxsAlice) = localClose(alice, alice2blockchain, htlcSuccessCount = 2, htlcTimeoutCount = 3) + assert(closingStateAlice.htlcOutputs.size == 6) + assert(closingTxsAlice.htlcSuccessTxs.size == 2) + assert(closingTxsAlice.htlcTimeoutTxs.size == 3) + + // Bob detects Alice's force-close. + val (closingStateBob, closingTxsBob) = remoteClose(closingStateAlice.commitTx, bob, bob2blockchain, htlcSuccessCount = 2, htlcTimeoutCount = 3) + assert(closingStateBob.htlcOutputs.size == 6) + assert(closingTxsBob.htlcSuccessTxs.size == 2) + assert(closingTxsBob.htlcTimeoutTxs.size == 3) + + // The commit transaction and main transactions confirm. + alice ! WatchTxConfirmedTriggered(BlockHeight(750_000), 3, closingStateAlice.commitTx) + alice ! WatchTxConfirmedTriggered(BlockHeight(750_000), 5, closingTxsAlice.anchorTx) + alice ! WatchTxConfirmedTriggered(BlockHeight(750_001), 1, closingTxsAlice.mainTx_opt.get) + alice ! WatchTxConfirmedTriggered(BlockHeight(750_001), 2, closingTxsBob.mainTx_opt.get) + alice2blockchain.expectNoMessage(100 millis) + bob ! WatchTxConfirmedTriggered(BlockHeight(750_000), 3, closingStateAlice.commitTx) + bob ! WatchTxConfirmedTriggered(BlockHeight(750_001), 1, closingTxsAlice.mainTx_opt.get) + bob ! WatchTxConfirmedTriggered(BlockHeight(750_001), 2, closingTxsBob.mainTx_opt.get) + bob2blockchain.expectNoMessage(100 millis) + + // One of Alice's HTLC-success transactions confirms. + val htlcSuccessAlice = closingTxsAlice.htlcSuccessTxs.head + alice ! WatchTxConfirmedTriggered(BlockHeight(750_005), 0, htlcSuccessAlice) + val htlcDelayed1 = alice2blockchain.expectFinalTxPublished("htlc-delayed") + Transaction.correctlySpends(htlcDelayed1.tx, Seq(htlcSuccessAlice), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + alice2blockchain.expectWatchOutputSpent(htlcDelayed1.input) + alice2blockchain.expectNoMessage(100 millis) + bob ! WatchTxConfirmedTriggered(BlockHeight(750_005), 0, htlcSuccessAlice) + bob2blockchain.expectNoMessage(100 millis) + + // One of Bob's HTLC-success transactions confirms. + val htlcSuccessBob = closingTxsBob.htlcSuccessTxs.last + alice ! WatchTxConfirmedTriggered(BlockHeight(750_008), 13, htlcSuccessBob) + alice2blockchain.expectNoMessage(100 millis) + bob ! WatchTxConfirmedTriggered(BlockHeight(750_008), 13, htlcSuccessBob) + bob2blockchain.expectNoMessage(100 millis) + + // Alice and Bob have two remaining HTLC-timeout transactions, one of which conflicts with an HTLC-success transaction. + val htlcTimeoutTxsAlice = closingTxsAlice.htlcTimeoutTxs.filter(_.txIn.head.outPoint != htlcSuccessBob.txIn.head.outPoint) + assert(htlcTimeoutTxsAlice.size == 2) + val htlcTimeoutTxsBob = closingTxsBob.htlcTimeoutTxs.filter(_.txIn.head.outPoint != htlcSuccessAlice.txIn.head.outPoint) + assert(htlcTimeoutTxsBob.size == 2) + val htlcTimeoutTxBob1 = htlcTimeoutTxsBob.find(_.txIn.head.outPoint == closingTxsAlice.htlcSuccessTxs.last.txIn.head.outPoint).get + val htlcTimeoutTxBob2 = htlcTimeoutTxsBob.find(_.txIn.head.outPoint != closingTxsAlice.htlcSuccessTxs.last.txIn.head.outPoint).get + + // Bob's HTLC-timeout transaction which conflicts with Alice's HTLC-success transaction confirms. + alice ! WatchTxConfirmedTriggered(BlockHeight(750_008), 13, htlcTimeoutTxBob1) + alice2blockchain.expectNoMessage(100 millis) + bob ! WatchTxConfirmedTriggered(BlockHeight(750_008), 13, htlcTimeoutTxBob1) + bob2blockchain.expectNoMessage(100 millis) + val remainingHtlcOutputs = htlcTimeoutTxBob2.txIn.head.outPoint +: htlcTimeoutTxsAlice.map(_.txIn.head.outPoint) + + // We simulate a node restart after a feerate decrease. + Seq(alice, bob).foreach { peer => + val beforeRestart = peer.stateData.asInstanceOf[DATA_CLOSING] + peer.nodeParams.setBitcoinCoreFeerates(FeeratesPerKw.single(FeeratePerKw(2500 sat))) + peer.setState(WAIT_FOR_INIT_INTERNAL, Nothing) + peer ! INPUT_RESTORED(beforeRestart) + awaitCond(peer.stateName == CLOSING) + } + Seq(alice2blockchain, bob2blockchain).foreach(_.expectMsgType[SetChannelId]) + + // Alice re-publishes closing transactions: her remaining HTLC-success transaction has been double-spent, so she + // only has HTLC-timeout transactions left. + val republishedHtlcTxsAlice = (1 to 2).map(_ => alice2blockchain.expectReplaceableTxPublished[HtlcTimeoutTx]) + alice2blockchain.expectWatchOutputsSpent(remainingHtlcOutputs) + assert(republishedHtlcTxsAlice.map(_.input.outPoint).toSet == htlcTimeoutTxsAlice.map(_.txIn.head.outPoint).toSet) + assert(alice2blockchain.expectFinalTxPublished("htlc-delayed").input == htlcDelayed1.input) + alice2blockchain.expectWatchOutputSpent(htlcDelayed1.input) + alice2blockchain.expectNoMessage(100 millis) + + // Bob re-publishes closing transactions: he has 1 HTLC-success and 1 HTLC-timeout transactions left. + val republishedHtlcTxsBob = (1 to 2).map(_ => bob2blockchain.expectMsgType[PublishReplaceableTx]) + bob2blockchain.expectWatchOutputsSpent(remainingHtlcOutputs) + assert(republishedHtlcTxsBob.map(_.input).toSet == Set(htlcTimeoutTxBob2.txIn.head.outPoint, closingTxsBob.htlcSuccessTxs.head.txIn.head.outPoint)) + bob2blockchain.expectNoMessage(100 millis) + + // Bob's previous HTLC-timeout transaction confirms. + alice ! WatchTxConfirmedTriggered(BlockHeight(750_009), 21, htlcTimeoutTxBob2) + bob ! WatchTxConfirmedTriggered(BlockHeight(750_009), 21, htlcTimeoutTxBob2) + + // Alice's re-published HTLC-timeout transactions confirm. + bob ! WatchTxConfirmedTriggered(BlockHeight(750_009), 25, republishedHtlcTxsAlice.head.tx) + alice ! WatchTxConfirmedTriggered(BlockHeight(750_009), 25, republishedHtlcTxsAlice.head.tx) + val htlcDelayed2 = alice2blockchain.expectFinalTxPublished("htlc-delayed") + alice2blockchain.expectWatchOutputSpent(htlcDelayed2.input) + assert(alice.stateName == CLOSING) + assert(bob.stateName == CLOSING) + bob ! WatchTxConfirmedTriggered(BlockHeight(750_009), 26, republishedHtlcTxsAlice.last.tx) + alice ! WatchTxConfirmedTriggered(BlockHeight(750_009), 26, republishedHtlcTxsAlice.last.tx) + val htlcDelayed3 = alice2blockchain.expectFinalTxPublished("htlc-delayed") + alice2blockchain.expectWatchOutputSpent(htlcDelayed3.input) + assert(alice.stateName == CLOSING) + awaitCond(bob.stateName == CLOSED) + val closedBob = bob.stateData.asInstanceOf[DATA_CLOSED] + assert(closedBob.closingType == "remote-close") + assert(closedBob.closingTxId == closingStateAlice.commitTx.txid) + assert(closedBob.closingAmount == closingTxsBob.mainTx_opt.get.txOut.head.amount + Seq(htlcTimeoutTxBob1, htlcTimeoutTxBob2, htlcSuccessBob).map(_.txOut.head.amount).sum) + + // Alice's 3rd-stage transactions confirm. + Seq(htlcDelayed1, htlcDelayed2, htlcDelayed3).foreach(p => alice ! WatchTxConfirmedTriggered(BlockHeight(750_100), 0, p.tx)) + alice2blockchain.expectNoMessage(100 millis) + awaitCond(alice.stateName == CLOSED) + val closedAlice = alice.stateData.asInstanceOf[DATA_CLOSED] + assert(closedAlice.closingType == "local-close") + assert(closedAlice.closingTxId == closingStateAlice.commitTx.txid) + assert(closedAlice.closingAmount == closingTxsAlice.mainTx_opt.get.txOut.head.amount + Seq(htlcDelayed1, htlcDelayed2, htlcDelayed3).map(_.tx.txOut.head.amount).sum) + } + test("recv WatchTxConfirmedTriggered (remote commit with htlcs only signed by local in next remote commit)") { f => import f._ val listener = TestProbe() systemA.eventStream.subscribe(listener.ref, classOf[PaymentSettlingOnChain]) - val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val bobCommitTx = bob.signCommitTx() // alice sends an htlc val (_, htlc) = addHtlc(50_000_000 msat, alice, bob, alice2bob, bob2alice) // and signs it (but bob doesn't sign it) alice ! CMD_SIGN() alice2bob.expectMsgType[CommitSig] - val closingState = remoteClose(bobCommitTx, alice, alice2blockchain) + val (closingState, _) = remoteClose(bobCommitTx, alice, alice2blockchain) // actual test starts here channelUpdateListener.expectMsgType[LocalChannelDown] - assert(closingState.claimMainOutputTx.isEmpty) - assert(closingState.claimHtlcTxs.isEmpty) + assert(closingState.htlcOutputs.isEmpty) // when the commit tx is signed, alice knows that the htlc she sent right before the unilateral close will never reach the chain + val origin = alice.stateData.asInstanceOf[DATA_CLOSING].commitments.originChannels(htlc.id) alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, bobCommitTx) // so she fails it - val origin = alice.stateData.asInstanceOf[DATA_CLOSING].commitments.originChannels(htlc.id) alice2relayer.expectMsg(RES_ADD_SETTLED(origin, htlc, HtlcResult.OnChainFail(HtlcOverriddenByLocalCommit(channelId(alice), htlc)))) // the htlc will not settle on chain listener.expectNoMessage(100 millis) @@ -1123,9 +1302,8 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with bob2alice.expectMsgType[RevokeAndAck] // not sent to alice // bob closes the channel using his latest commitment, which doesn't contain any htlc. - val bobCommit = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit - assert(bobCommit.htlcTxsAndRemoteSigs.isEmpty) - val commitTx = bobCommit.commitTxAndRemoteSig.commitTx.tx + assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.htlcRemoteSigs.isEmpty) + val commitTx = bob.signCommitTx() alice ! WatchFundingSpentTriggered(commitTx) alice ! WatchTxConfirmedTriggered(BlockHeight(42), 0, commitTx) // the two HTLCs have been overridden by the on-chain commit @@ -1146,12 +1324,10 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with mutualClose(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain) val initialState = alice.stateData.asInstanceOf[DATA_CLOSING] // bob publishes his last current commit tx, the one it had when entering NEGOTIATING state - val bobCommitTx = bobCommitTxs.last.commitTx.tx - assert(bobCommitTx.txOut.size == 2) // two main outputs - val closingState = remoteClose(bobCommitTx, alice, alice2blockchain) - assert(bobCommitTx.txOut.exists(_.publicKeyScript == Script.write(Script.pay2wpkh(DummyOnChainWallet.dummyReceivePubkey)))) // bob's commit tx sends directly to our wallet - assert(closingState.claimMainOutputTx.isEmpty) - assert(closingState.claimHtlcTxs.isEmpty) + val bobCommitTx = bobCommitTxs.last + assert(bobCommitTx.txOut.size == 4) // two main outputs and two anchor outputs + val (closingState, _) = remoteClose(bobCommitTx, alice, alice2blockchain) + assert(closingState.htlcOutputs.isEmpty) assert(alice.stateData.asInstanceOf[DATA_CLOSING].copy(remoteCommitPublished = None) == initialState) val txPublished = txListener.expectMsgType[TransactionPublished] assert(txPublished.tx == bobCommitTx) @@ -1165,7 +1341,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with systemA.eventStream.subscribe(listener.ref, classOf[LocalChannelUpdate]) // bob publishes his commit tx - val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val bobCommitTx = bob.signCommitTx() remoteClose(bobCommitTx, alice, alice2blockchain) // alice notifies the network that the channel shouldn't be used anymore inside(listener.expectMsgType[LocalChannelUpdate]) { u => assert(!u.channelUpdate.channelFlags.isEnabled) } @@ -1175,107 +1351,76 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with import f._ mutualClose(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain) // Bob publishes his last current commit tx, the one it had when entering NEGOTIATING state. - val bobCommitTx = bobCommitTxs.last.commitTx.tx - val closingState = remoteClose(bobCommitTx, alice, alice2blockchain) - assert(closingState.claimHtlcTxs.isEmpty) + val bobCommitTx = bobCommitTxs.last + val (closingState, _) = remoteClose(bobCommitTx, alice, alice2blockchain) + assert(closingState.htlcOutputs.isEmpty) val txPublished = txListener.expectMsgType[TransactionPublished] assert(txPublished.tx == bobCommitTx) assert(txPublished.miningFee > 0.sat) // alice is funder, she pays the fee for the remote commit } - test("recv CMD_BUMP_FORCE_CLOSE_FEE (remote commit)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + test("recv CMD_BUMP_FORCE_CLOSE_FEE (remote commit)") { f => import f._ - val bobCommitTx = bobCommitTxs.last.commitTx.tx - val closingState1 = remoteClose(bobCommitTx, alice, alice2blockchain) - assert(closingState1.claimAnchorTxs.nonEmpty) - val Some(localAnchor1) = closingState1.claimAnchorTxs.collectFirst { case tx: ClaimLocalAnchorOutputTx => tx } - assert(localAnchor1.confirmationTarget == ConfirmationTarget.Priority(ConfirmationPriority.Medium)) + val bobCommitTx = bobCommitTxs.last + val (closingState, _) = remoteClose(bobCommitTx, alice, alice2blockchain) + assert(closingState.anchorOutput_opt.nonEmpty) val replyTo = TestProbe() alice ! CMD_BUMP_FORCE_CLOSE_FEE(replyTo.ref, ConfirmationTarget.Priority(ConfirmationPriority.Fast)) replyTo.expectMsgType[RES_SUCCESS[CMD_BUMP_FORCE_CLOSE_FEE]] - val localAnchor2 = inside(alice2blockchain.expectMsgType[PublishReplaceableTx]) { tx => - assert(tx.txInfo.isInstanceOf[ClaimLocalAnchorOutputTx]) - assert(tx.commitTx == bobCommitTx) - tx.txInfo.asInstanceOf[ClaimLocalAnchorOutputTx] - } - assert(localAnchor2.confirmationTarget == ConfirmationTarget.Priority(ConfirmationPriority.Fast)) - val closingState2 = alice.stateData.asInstanceOf[DATA_CLOSING].remoteCommitPublished.get - assert(closingState2.claimAnchorTxs.contains(localAnchor2)) - - // If we try bumping again, but with a lower priority, this won't override the previous priority. - alice ! CMD_BUMP_FORCE_CLOSE_FEE(replyTo.ref, ConfirmationTarget.Priority(ConfirmationPriority.Medium)) - replyTo.expectMsgType[RES_SUCCESS[CMD_BUMP_FORCE_CLOSE_FEE]] - val localAnchor3 = inside(alice2blockchain.expectMsgType[PublishReplaceableTx]) { tx => - assert(tx.txInfo.isInstanceOf[ClaimLocalAnchorOutputTx]) - assert(tx.commitTx == bobCommitTx) - tx.txInfo.asInstanceOf[ClaimLocalAnchorOutputTx] + inside(alice2blockchain.expectMsgType[PublishReplaceableTx]) { publish => + assert(publish.txInfo.isInstanceOf[ClaimRemoteAnchorTx]) + assert(publish.commitTx == bobCommitTx) + assert(publish.confirmationTarget == ConfirmationTarget.Priority(ConfirmationPriority.Fast)) } - assert(localAnchor3.confirmationTarget == ConfirmationTarget.Priority(ConfirmationPriority.Fast)) - assert(alice.stateData.asInstanceOf[DATA_CLOSING].remoteCommitPublished.contains(closingState2)) } - test("recv WatchTxConfirmedTriggered (remote commit)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + test("recv WatchTxConfirmedTriggered (remote commit)") { f => import f._ mutualClose(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain) val initialState = alice.stateData.asInstanceOf[DATA_CLOSING] // bob publishes his last current commit tx, the one it had when entering NEGOTIATING state - val bobCommitTx = bobCommitTxs.last.commitTx.tx + val bobCommitTx = bobCommitTxs.last assert(bobCommitTx.txOut.size == 4) // two main outputs + two anchors - val closingState = remoteClose(bobCommitTx, alice, alice2blockchain) + val (closingState, closingTxs) = remoteClose(bobCommitTx, alice, alice2blockchain) // actual test starts here - assert(closingState.claimMainOutputTx.nonEmpty) - assert(closingState.claimHtlcTxs.isEmpty) + assert(closingState.localOutput_opt.nonEmpty) + assert(closingTxs.mainTx_opt.nonEmpty) + assert(closingState.htlcOutputs.isEmpty) assert(alice.stateData.asInstanceOf[DATA_CLOSING].copy(remoteCommitPublished = None) == initialState) txListener.expectMsgType[TransactionPublished] alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, bobCommitTx) - alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, closingState.claimMainOutputTx.get.tx) + alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, closingTxs.mainTx_opt.get) assert(txListener.expectMsgType[TransactionConfirmed].tx == bobCommitTx) awaitCond(alice.stateName == CLOSED) } - test("recv WatchTxConfirmedTriggered (remote commit, option_static_remotekey)", Tag(ChannelStateTestsTags.StaticRemoteKey), Tag(ChannelStateTestsTags.SimpleClose)) { f => - import f._ - mutualClose(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain) - assert(alice.stateData.asInstanceOf[DATA_NEGOTIATING_SIMPLE].commitments.params.channelFeatures == ChannelFeatures(Features.StaticRemoteKey)) - // bob publishes his last current commit tx, the one it had when entering NEGOTIATING state - val bobCommitTx = bobCommitTxs.last.commitTx.tx - assert(bobCommitTx.txOut.size == 2) // two main outputs - alice ! WatchFundingSpentTriggered(bobCommitTx) - - // alice won't create a claimMainOutputTx because her main output is already spendable by the wallet - awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].remoteCommitPublished.get.claimMainOutputTx.isEmpty) - assert(alice.stateName == CLOSING) - // once the remote commit is confirmed the channel is definitively closed - alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, bobCommitTx) - awaitCond(alice.stateName == CLOSED) - } - - test("recv WatchTxConfirmedTriggered (remote commit, anchor outputs zero fee htlc txs)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + test("recv WatchTxConfirmedTriggered (remote commit, anchor outputs zero fee htlc txs)") { f => import f._ mutualClose(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain) val initialState = alice.stateData.asInstanceOf[DATA_CLOSING] - assert(initialState.commitments.params.channelFeatures == ChannelFeatures(Features.StaticRemoteKey, Features.AnchorOutputsZeroFeeHtlcTx)) + assert(initialState.commitments.latest.commitmentFormat == ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) // bob publishes his last current commit tx, the one it had when entering NEGOTIATING state - val bobCommitTx = bobCommitTxs.last.commitTx.tx + val bobCommitTx = bobCommitTxs.last assert(bobCommitTx.txOut.size == 4) // two main outputs + two anchors - val closingState = remoteClose(bobCommitTx, alice, alice2blockchain) + val (closingState, closingTxs) = remoteClose(bobCommitTx, alice, alice2blockchain) // actual test starts here - assert(closingState.claimMainOutputTx.nonEmpty) - assert(closingState.claimHtlcTxs.isEmpty) + assert(closingState.localOutput_opt.nonEmpty) + assert(closingTxs.mainTx_opt.nonEmpty) + assert(closingState.htlcOutputs.isEmpty) assert(alice.stateData.asInstanceOf[DATA_CLOSING].copy(remoteCommitPublished = None) == initialState) alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, bobCommitTx) - alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, closingState.claimMainOutputTx.get.tx) + alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, closingTxs.mainTx_opt.get) awaitCond(alice.stateName == CLOSED) } - def testRemoteCommitTxWithHtlcsConfirmed(f: FixtureParam, channelFeatures: ChannelFeatures): Unit = { + def testRemoteCommitTxWithHtlcsConfirmed(f: FixtureParam, commitmentFormat: CommitmentFormat): Unit = { import f._ - assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.params.channelFeatures == channelFeatures) + assert(alice.commitments.latest.commitmentFormat == commitmentFormat) // alice sends a first htlc to bob val (ra1, htlca1) = addHtlc(15_000_000 msat, alice, bob, alice2bob, bob2alice) @@ -1287,123 +1432,134 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with crossSign(alice, bob, alice2bob, bob2alice) // Bob publishes the latest commit tx. - val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx - channelFeatures.commitmentFormat match { - case _: AnchorOutputsCommitmentFormat => assert(bobCommitTx.txOut.length == 7) // two main outputs + two anchors + 3 HTLCs - case DefaultCommitmentFormat => assert(bobCommitTx.txOut.length == 5) // two main outputs + 3 HTLCs - } - val closingState = remoteClose(bobCommitTx, alice, alice2blockchain) - assert(closingState.claimHtlcTxs.size == 3) - val claimHtlcTimeoutTxs = getClaimHtlcTimeoutTxs(closingState).map(_.tx) - assert(claimHtlcTimeoutTxs.length == 3) + val bobCommitTx = bob.signCommitTx() + assert(bobCommitTx.txOut.length == 7) // two main outputs + two anchors + 3 HTLCs + val (closingState, closingTxs) = remoteClose(bobCommitTx, alice, alice2blockchain, htlcTimeoutCount = 3) + assert(closingState.htlcOutputs.size == 3) + assert(closingTxs.htlcTimeoutTxs.length == 3) alice ! WatchTxConfirmedTriggered(BlockHeight(42), 0, bobCommitTx) // for static_remote_key channels there is no claimMainOutputTx (bob's commit tx directly sends to our wallet) - closingState.claimMainOutputTx.foreach(claimMainOutputTx => alice ! WatchTxConfirmedTriggered(BlockHeight(45), 0, claimMainOutputTx.tx)) + closingTxs.mainTx_opt.foreach(tx => alice ! WatchTxConfirmedTriggered(BlockHeight(45), 0, tx)) alice2relayer.expectNoMessage(100 millis) - alice ! WatchTxConfirmedTriggered(BlockHeight(201), 0, claimHtlcTimeoutTxs(0)) + alice ! WatchTxConfirmedTriggered(BlockHeight(201), 0, closingTxs.htlcTimeoutTxs(0)) val forwardedFail1 = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc alice2relayer.expectNoMessage(100 millis) - alice ! WatchTxConfirmedTriggered(BlockHeight(202), 0, claimHtlcTimeoutTxs(1)) + alice ! WatchTxConfirmedTriggered(BlockHeight(202), 0, closingTxs.htlcTimeoutTxs(1)) val forwardedFail2 = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc alice2relayer.expectNoMessage(100 millis) - alice ! WatchTxConfirmedTriggered(BlockHeight(203), 1, claimHtlcTimeoutTxs(2)) + alice ! WatchTxConfirmedTriggered(BlockHeight(203), 1, closingTxs.htlcTimeoutTxs(2)) val forwardedFail3 = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc assert(Set(forwardedFail1, forwardedFail2, forwardedFail3) == Set(htlca1, htlca2, htlca3)) alice2relayer.expectNoMessage(100 millis) awaitCond(alice.stateName == CLOSED) } - test("recv WatchTxConfirmedTriggered (remote commit with multiple htlcs for the same payment)") { f => - testRemoteCommitTxWithHtlcsConfirmed(f, ChannelFeatures(Features.StaticRemoteKey)) + test("recv WatchTxConfirmedTriggered (remote commit with multiple htlcs for the same payment, anchor outputs zero fee htlc txs)") { f => + testRemoteCommitTxWithHtlcsConfirmed(f, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) } - test("recv WatchTxConfirmedTriggered (remote commit with multiple htlcs for the same payment, anchor outputs)", Tag(ChannelStateTestsTags.AnchorOutputs)) { f => - testRemoteCommitTxWithHtlcsConfirmed(f, ChannelFeatures(Features.StaticRemoteKey, Features.AnchorOutputs)) + test("recv WatchTxConfirmedTriggered (remote commit with multiple htlcs for the same payment, anchor outputs phoenix)", Tag(ChannelStateTestsTags.AnchorOutputsPhoenix)) { f => + testRemoteCommitTxWithHtlcsConfirmed(f, UnsafeLegacyAnchorOutputsCommitmentFormat) } - test("recv WatchTxConfirmedTriggered (remote commit with multiple htlcs for the same payment, anchor outputs zero fee htlc txs)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => - testRemoteCommitTxWithHtlcsConfirmed(f, ChannelFeatures(Features.StaticRemoteKey, Features.AnchorOutputsZeroFeeHtlcTx)) + test("recv WatchTxConfirmedTriggered (remote commit with multiple htlcs for the same payment, taproot)", Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => + testRemoteCommitTxWithHtlcsConfirmed(f, ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat) } - test("recv WatchTxConfirmedTriggered (remote commit) followed by CMD_FULFILL_HTLC") { f => + test("recv WatchTxConfirmedTriggered (remote commit) followed by htlc settlement") { f => import f._ - // An HTLC Bob -> Alice is cross-signed that will be fulfilled later. + // Bob sends 2 HTLCs to Alice that will be settled during the force-close: one will be fulfilled, the other will be failed. val (r1, htlc1) = addHtlc(110_000_000 msat, CltvExpiryDelta(48), bob, alice, bob2alice, alice2bob) + val (_, htlc2) = addHtlc(60_000_000 msat, CltvExpiryDelta(36), bob, alice, bob2alice, alice2bob) crossSign(bob, alice, bob2alice, alice2bob) assert(alice2relayer.expectMsgType[RelayForward].add == htlc1) + assert(alice2relayer.expectMsgType[RelayForward].add == htlc2) - // An HTLC Alice -> Bob is only signed by Alice: Bob has two spendable commit tx. - val (_, htlc2) = addHtlc(95_000_000 msat, CltvExpiryDelta(144), alice, bob, alice2bob, bob2alice) + // Alice sends an HTLC to Bob: Bob has two spendable commit txs. + val (_, htlc3) = addHtlc(95_000_000 msat, CltvExpiryDelta(144), alice, bob, alice2bob, bob2alice) alice ! CMD_SIGN() alice2bob.expectMsgType[CommitSig] // We stop here: Alice sent her CommitSig, but doesn't hear back from Bob. // Now Bob publishes the first commit tx (force-close). - val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx - assert(bobCommitTx.txOut.length == 3) // two main outputs + 1 HTLC - val closingState = remoteClose(bobCommitTx, alice, alice2blockchain) - assert(closingState.claimMainOutputTx.isEmpty) - assert(bobCommitTx.txOut.exists(_.publicKeyScript == Script.write(Script.pay2wpkh(DummyOnChainWallet.dummyReceivePubkey)))) - assert(closingState.claimHtlcTxs.size == 1) - assert(getClaimHtlcSuccessTxs(closingState).isEmpty) // we don't have the preimage to claim the htlc-success yet - assert(getClaimHtlcTimeoutTxs(closingState).isEmpty) + val bobCommitTx = bob.signCommitTx() + assert(bobCommitTx.txOut.length == 6) // 2 main outputs + 2 anchor outputs + 2 HTLCs + val (closingState, closingTxs) = remoteClose(bobCommitTx, alice, alice2blockchain) + assert(closingState.localOutput_opt.nonEmpty) + assert(closingState.htlcOutputs.size == 2) + assert(closingTxs.htlcTxs.isEmpty) // we don't have the preimage to claim the htlc-success yet // Alice receives the preimage for the first HTLC from downstream; she can now claim the corresponding HTLC output. - alice ! CMD_FULFILL_HTLC(htlc1.id, r1, commit = true) - val claimHtlcSuccessTx = getClaimHtlcSuccessTxs(alice.stateData.asInstanceOf[DATA_CLOSING].remoteCommitPublished.get).head.tx - Transaction.correctlySpends(claimHtlcSuccessTx, bobCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - val publishHtlcSuccessTx = alice2blockchain.expectMsgType[PublishReplaceableTx].txInfo.asInstanceOf[ClaimHtlcSuccessTx] - assert(publishHtlcSuccessTx.tx == claimHtlcSuccessTx) - assert(publishHtlcSuccessTx.confirmationTarget == ConfirmationTarget.Absolute(htlc1.cltvExpiry.blockHeight)) - - // Alice resets watches on all relevant transactions. - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == bobCommitTx.txid) - val watchHtlcSuccess = alice2blockchain.expectMsgType[WatchOutputSpent] - assert(watchHtlcSuccess.txId == bobCommitTx.txid) - assert(watchHtlcSuccess.outputIndex == claimHtlcSuccessTx.txIn.head.outPoint.index) + alice ! CMD_FULFILL_HTLC(htlc1.id, r1, None, commit = true) + val htlcSuccess = alice2blockchain.expectReplaceableTxPublished[ClaimHtlcSuccessTx](ConfirmationTarget.Absolute(htlc1.cltvExpiry.blockHeight)) + assert(htlcSuccess.preimage == r1) + Transaction.correctlySpends(htlcSuccess.sign(), bobCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) alice2blockchain.expectNoMessage(100 millis) + // Bob's commitment confirms: the third htlc was not included in the commit tx published on-chain, so we can consider it failed. alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, bobCommitTx) - // The second htlc was not included in the commit tx published on-chain, so we can consider it failed - assert(alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc == htlc2) - alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, claimHtlcSuccessTx) - assert(alice.stateData.asInstanceOf[DATA_CLOSING].remoteCommitPublished.get.irrevocablySpent.values.toSet == Set(bobCommitTx, claimHtlcSuccessTx)) - awaitCond(alice.stateName == CLOSED) + alice ! WatchTxConfirmedTriggered(BlockHeight(0), 1, closingTxs.anchorTx) + assert(alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc == htlc3) + // Alice's main transaction confirms. + closingTxs.mainTx_opt.foreach(tx => alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, tx)) + + // Alice receives a failure for the second HTLC from downstream; she can stop watching the corresponding HTLC output. + alice ! CMD_FAIL_HTLC(htlc2.id, FailureReason.EncryptedDownstreamFailure(ByteVector.empty, None), None) + alice2blockchain.expectNoMessage(100 millis) + + // Alice restarts, and pending transactions confirm. + val beforeRestart = alice.stateData.asInstanceOf[DATA_CLOSING] + alice.setState(WAIT_FOR_INIT_INTERNAL, Nothing) + alice ! INPUT_RESTORED(beforeRestart) + alice2blockchain.expectMsgType[SetChannelId] + awaitCond(alice.stateName == CLOSING) + // Alice republishes the HTLC-success transaction, which then confirms. + assert(alice2blockchain.expectReplaceableTxPublished[ClaimHtlcSuccessTx].input == htlcSuccess.input) + alice2blockchain.expectWatchOutputSpent(htlcSuccess.input.outPoint) + alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, htlcSuccess.tx) alice2blockchain.expectNoMessage(100 millis) alice2relayer.expectNoMessage(100 millis) + awaitCond(alice.stateName == CLOSED) } test("recv INPUT_RESTORED (remote commit)") { f => import f._ - // alice sends an htlc to bob - val (_, htlca) = addHtlc(50000000 msat, CltvExpiryDelta(24), alice, bob, alice2bob, bob2alice) + // Alice sends an htlc to Bob: Bob then force-closes. + val (_, htlc) = addHtlc(50_000_000 msat, CltvExpiryDelta(24), alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) - val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx - val closingState = remoteClose(bobCommitTx, alice, alice2blockchain) - val htlcTimeoutTx = getClaimHtlcTimeoutTxs(closingState).head + val bobCommitTx = bob.signCommitTx() + val (_, closingTxs) = remoteClose(bobCommitTx, alice, alice2blockchain, htlcTimeoutCount = 1) + assert(closingTxs.htlcTimeoutTxs.size == 1) + val htlcTimeoutTx = closingTxs.htlcTxs.head alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, bobCommitTx) - // simulate a node restart + // We simulate a node restart with a lower feerate. val beforeRestart = alice.stateData.asInstanceOf[DATA_CLOSING] + alice.nodeParams.setBitcoinCoreFeerates(FeeratesPerKw.single(FeeratePerKw(2_500 sat))) alice.setState(WAIT_FOR_INIT_INTERNAL, Nothing) alice ! INPUT_RESTORED(beforeRestart) alice2blockchain.expectMsgType[SetChannelId] awaitCond(alice.stateName == CLOSING) - // we should re-publish unconfirmed transactions - closingState.claimMainOutputTx.foreach(claimMain => assert(alice2blockchain.expectMsgType[PublishFinalTx].tx == claimMain.tx)) - val publishClaimHtlcTimeoutTx = alice2blockchain.expectMsgType[PublishReplaceableTx].txInfo.asInstanceOf[ClaimHtlcTimeoutTx] - assert(publishClaimHtlcTimeoutTx.tx == htlcTimeoutTx.tx) - assert(publishClaimHtlcTimeoutTx.confirmationTarget == ConfirmationTarget.Absolute(htlca.cltvExpiry.blockHeight)) - closingState.claimMainOutputTx.foreach(claimMain => assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == claimMain.tx.txid)) - assert(alice2blockchain.expectMsgType[WatchOutputSpent].outputIndex == htlcTimeoutTx.input.outPoint.index) + // We should re-publish unconfirmed transactions. + // Our main transaction should have a lower feerate. + // HTLC transactions are unchanged: the feerate will be based on their expiry. + closingTxs.mainTx_opt.foreach(tx => { + val tx2 = alice2blockchain.expectFinalTxPublished("remote-main-delayed") + assert(tx2.tx.txIn.head.outPoint == tx.txIn.head.outPoint) + assert(tx2.tx.txOut.head.amount > tx.txOut.head.amount) + }) + val htlcTimeout = alice2blockchain.expectReplaceableTxPublished[ClaimHtlcTimeoutTx](ConfirmationTarget.Absolute(htlc.cltvExpiry.blockHeight)) + assert(htlcTimeout.input.outPoint == htlcTimeoutTx.txIn.head.outPoint) + assert(htlcTimeout.tx.txid == htlcTimeoutTx.txid) + alice2blockchain.expectWatchOutputsSpent(Seq(closingTxs.mainTx_opt.get.txIn.head.outPoint, htlcTimeout.input.outPoint)) } - private def testNextRemoteCommitTxConfirmed(f: FixtureParam, channelFeatures: ChannelFeatures): (Transaction, RemoteCommitPublished, Set[UpdateAddHtlc]) = { + private def testNextRemoteCommitTxConfirmed(f: FixtureParam, commitmentFormat: CommitmentFormat): (Transaction, PublishedForceCloseTxs, Set[UpdateAddHtlc]) = { import f._ - assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.params.channelFeatures == channelFeatures) + assert(alice.commitments.latest.commitmentFormat == commitmentFormat) // alice sends a first htlc to bob val (ra1, htlca1) = addHtlc(15_000_000 msat, alice, bob, alice2bob, bob2alice) @@ -1421,89 +1577,67 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with bob2alice.expectMsgType[CommitSig] // not forwarded to Alice (malicious Bob) // Bob publishes the next commit tx. - val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx - channelFeatures.commitmentFormat match { - case _: AnchorOutputsCommitmentFormat => assert(bobCommitTx.txOut.length == 7) // two main outputs + two anchors + 3 HTLCs - case DefaultCommitmentFormat => assert(bobCommitTx.txOut.length == 5) // two main outputs + 3 HTLCs - } - val closingState = remoteClose(bobCommitTx, alice, alice2blockchain) - assert(getClaimHtlcTimeoutTxs(closingState).length == 3) - (bobCommitTx, closingState, Set(htlca1, htlca2, htlca3)) + val bobCommitTx = bob.signCommitTx() + assert(bobCommitTx.txOut.length == 7) // two main outputs + two anchors + 3 HTLCs + val (closingState, closingTxs) = remoteClose(bobCommitTx, alice, alice2blockchain, htlcTimeoutCount = 3) + assert(closingState.htlcOutputs.size == 3) + assert(closingTxs.htlcTimeoutTxs.size == 3) + (bobCommitTx, closingTxs, Set(htlca1, htlca2, htlca3)) } - test("recv WatchTxConfirmedTriggered (next remote commit)") { f => + test("recv WatchTxConfirmedTriggered (next remote commit, anchor outputs zero fee htlc txs)") { f => import f._ - val (bobCommitTx, closingState, htlcs) = testNextRemoteCommitTxConfirmed(f, ChannelFeatures(Features.StaticRemoteKey)) - val txPublished = txListener.expectMsgType[TransactionPublished] - assert(txPublished.tx == bobCommitTx) - assert(txPublished.miningFee > 0.sat) // alice is funder, she pays the fee for the remote commit - val claimHtlcTimeoutTxs = getClaimHtlcTimeoutTxs(closingState).map(_.tx) + val (bobCommitTx, closingTxs, htlcs) = testNextRemoteCommitTxConfirmed(f, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) alice ! WatchTxConfirmedTriggered(BlockHeight(42), 0, bobCommitTx) - assert(txListener.expectMsgType[TransactionConfirmed].tx == bobCommitTx) - closingState.claimMainOutputTx.foreach(claimMainOutputTx => alice ! WatchTxConfirmedTriggered(BlockHeight(45), 0, claimMainOutputTx.tx)) + closingTxs.mainTx_opt.foreach(tx => alice ! WatchTxConfirmedTriggered(BlockHeight(45), 0, tx)) alice2relayer.expectNoMessage(100 millis) - alice ! WatchTxConfirmedTriggered(BlockHeight(201), 0, claimHtlcTimeoutTxs(0)) + alice ! WatchTxConfirmedTriggered(BlockHeight(201), 0, closingTxs.htlcTimeoutTxs(0)) val forwardedFail1 = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc alice2relayer.expectNoMessage(100 millis) - alice ! WatchTxConfirmedTriggered(BlockHeight(202), 0, claimHtlcTimeoutTxs(1)) + alice ! WatchTxConfirmedTriggered(BlockHeight(202), 0, closingTxs.htlcTimeoutTxs(1)) val forwardedFail2 = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc alice2relayer.expectNoMessage(100 millis) - alice ! WatchTxConfirmedTriggered(BlockHeight(203), 1, claimHtlcTimeoutTxs(2)) + alice ! WatchTxConfirmedTriggered(BlockHeight(203), 1, closingTxs.htlcTimeoutTxs(2)) val forwardedFail3 = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc assert(Set(forwardedFail1, forwardedFail2, forwardedFail3) == htlcs) alice2relayer.expectNoMessage(100 millis) awaitCond(alice.stateName == CLOSED) + val closedAlice = alice.stateData.asInstanceOf[DATA_CLOSED] + assert(closedAlice.closingType == "next-remote-close") + assert(closedAlice.closingTxId == bobCommitTx.txid) + assert(closedAlice.closingAmount == closingTxs.mainTx_opt.map(_.txOut.head.amount).getOrElse(0 sat) + closingTxs.htlcTimeoutTxs.map(_.txOut.head.amount).sum) } - test("recv WatchTxConfirmedTriggered (next remote commit, static_remotekey)", Tag(ChannelStateTestsTags.StaticRemoteKey)) { f => + test("recv WatchTxConfirmedTriggered (next remote commit, taproot)", Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => import f._ - val (bobCommitTx, closingState, htlcs) = testNextRemoteCommitTxConfirmed(f, ChannelFeatures(Features.StaticRemoteKey)) - val claimHtlcTimeoutTxs = getClaimHtlcTimeoutTxs(closingState).map(_.tx) + val (bobCommitTx, closingTxs, htlcs) = testNextRemoteCommitTxConfirmed(f, ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat) alice ! WatchTxConfirmedTriggered(BlockHeight(42), 0, bobCommitTx) - assert(closingState.claimMainOutputTx.isEmpty) // with static_remotekey we don't claim out main output + closingTxs.mainTx_opt.foreach(tx => alice ! WatchTxConfirmedTriggered(BlockHeight(45), 0, tx)) alice2relayer.expectNoMessage(100 millis) - alice ! WatchTxConfirmedTriggered(BlockHeight(201), 0, claimHtlcTimeoutTxs(0)) + alice ! WatchTxConfirmedTriggered(BlockHeight(201), 0, closingTxs.htlcTimeoutTxs(0)) val forwardedFail1 = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc alice2relayer.expectNoMessage(100 millis) - alice ! WatchTxConfirmedTriggered(BlockHeight(202), 0, claimHtlcTimeoutTxs(1)) + alice ! WatchTxConfirmedTriggered(BlockHeight(202), 0, closingTxs.htlcTimeoutTxs(1)) val forwardedFail2 = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc alice2relayer.expectNoMessage(100 millis) - alice ! WatchTxConfirmedTriggered(BlockHeight(203), 1, claimHtlcTimeoutTxs(2)) + alice ! WatchTxConfirmedTriggered(BlockHeight(203), 1, closingTxs.htlcTimeoutTxs(2)) val forwardedFail3 = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc assert(Set(forwardedFail1, forwardedFail2, forwardedFail3) == htlcs) alice2relayer.expectNoMessage(100 millis) awaitCond(alice.stateName == CLOSED) } - test("recv WatchTxConfirmedTriggered (next remote commit, anchor outputs zero fee htlc txs)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + test("recv WatchTxConfirmedTriggered (next remote commit) followed by htlc settlement") { f => import f._ - val (bobCommitTx, closingState, htlcs) = testNextRemoteCommitTxConfirmed(f, ChannelFeatures(Features.StaticRemoteKey, Features.AnchorOutputsZeroFeeHtlcTx)) - val claimHtlcTimeoutTxs = getClaimHtlcTimeoutTxs(closingState).map(_.tx) - alice ! WatchTxConfirmedTriggered(BlockHeight(42), 0, bobCommitTx) - closingState.claimMainOutputTx.foreach(claimMainOutputTx => alice ! WatchTxConfirmedTriggered(BlockHeight(45), 0, claimMainOutputTx.tx)) - alice2relayer.expectNoMessage(100 millis) - alice ! WatchTxConfirmedTriggered(BlockHeight(201), 0, claimHtlcTimeoutTxs(0)) - val forwardedFail1 = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc - alice2relayer.expectNoMessage(100 millis) - alice ! WatchTxConfirmedTriggered(BlockHeight(202), 0, claimHtlcTimeoutTxs(1)) - val forwardedFail2 = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc - alice2relayer.expectNoMessage(100 millis) - alice ! WatchTxConfirmedTriggered(BlockHeight(203), 1, claimHtlcTimeoutTxs(2)) - val forwardedFail3 = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc - assert(Set(forwardedFail1, forwardedFail2, forwardedFail3) == htlcs) - alice2relayer.expectNoMessage(100 millis) - awaitCond(alice.stateName == CLOSED) - } - - test("recv WatchTxConfirmedTriggered (next remote commit) followed by CMD_FULFILL_HTLC") { f => - import f._ - // An HTLC Bob -> Alice is cross-signed that will be fulfilled later. + // Bob sends 2 HTLCs to Alice that will be settled during the force-close: one will be fulfilled, the other will be failed. val (r1, htlc1) = addHtlc(110_000_000 msat, CltvExpiryDelta(64), bob, alice, bob2alice, alice2bob) + val (_, htlc2) = addHtlc(70_000_000 msat, CltvExpiryDelta(96), bob, alice, bob2alice, alice2bob) crossSign(bob, alice, bob2alice, alice2bob) assert(alice2relayer.expectMsgType[RelayForward].add == htlc1) + assert(alice2relayer.expectMsgType[RelayForward].add == htlc2) - // An HTLC Alice -> Bob is only signed by Alice: Bob has two spendable commit tx. - val (_, htlc2) = addHtlc(95_000_000 msat, CltvExpiryDelta(32), alice, bob, alice2bob, bob2alice) + // Alice sends an HTLC to Bob: Bob has two spendable commit txs. + val (_, htlc3) = addHtlc(95_000_000 msat, CltvExpiryDelta(32), alice, bob, alice2bob, bob2alice) alice ! CMD_SIGN() alice2bob.expectMsgType[CommitSig] alice2bob.forward(bob) @@ -1511,53 +1645,55 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with bob2alice.expectMsgType[CommitSig] // not forwarded to Alice (malicious Bob) // Now Bob publishes the next commit tx (force-close). - val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx - assert(bobCommitTx.txOut.length == 4) // two main outputs + 2 HTLCs - val closingState = remoteClose(bobCommitTx, alice, alice2blockchain) - if (!bob.stateData.asInstanceOf[DATA_NORMAL].commitments.params.channelFeatures.paysDirectlyToWallet) { - assert(closingState.claimMainOutputTx.nonEmpty) - } else { - assert(closingState.claimMainOutputTx.isEmpty) - } - assert(closingState.claimHtlcTxs.size == 2) - assert(getClaimHtlcSuccessTxs(closingState).isEmpty) // we don't have the preimage to claim the htlc-success yet - assert(getClaimHtlcTimeoutTxs(closingState).length == 1) - val claimHtlcTimeoutTx = getClaimHtlcTimeoutTxs(closingState).head.tx + val bobCommitTx = bob.signCommitTx() + assert(bobCommitTx.txOut.length == 7) // 2 main outputs + 2 anchor outputs + 3 HTLCs + val (closingState, closingTxs) = remoteClose(bobCommitTx, alice, alice2blockchain, htlcTimeoutCount = 1) + assert(closingState.localOutput_opt.nonEmpty) + assert(closingState.htlcOutputs.size == 3) + assert(closingTxs.htlcTxs.size == 1) // we don't have the preimage to claim the htlc-success yet + val htlcTimeoutTx = closingTxs.htlcTimeoutTxs.head // Alice receives the preimage for the first HTLC from downstream; she can now claim the corresponding HTLC output. - alice ! CMD_FULFILL_HTLC(htlc1.id, r1, commit = true) - closingState.claimMainOutputTx.foreach(claimMainOutputTx => assert(alice2blockchain.expectMsgType[PublishFinalTx].tx == claimMainOutputTx.tx)) - val claimHtlcSuccessTx = getClaimHtlcSuccessTxs(alice.stateData.asInstanceOf[DATA_CLOSING].nextRemoteCommitPublished.get).head.tx - Transaction.correctlySpends(claimHtlcSuccessTx, bobCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - val publishHtlcSuccessTx = alice2blockchain.expectMsgType[PublishReplaceableTx].txInfo.asInstanceOf[ClaimHtlcSuccessTx] - assert(publishHtlcSuccessTx.tx == claimHtlcSuccessTx) - assert(publishHtlcSuccessTx.confirmationTarget == ConfirmationTarget.Absolute(htlc1.cltvExpiry.blockHeight)) - val publishHtlcTimeoutTx = alice2blockchain.expectMsgType[PublishReplaceableTx].txInfo.asInstanceOf[ClaimHtlcTimeoutTx] - assert(publishHtlcTimeoutTx.tx == claimHtlcTimeoutTx) - assert(publishHtlcTimeoutTx.confirmationTarget == ConfirmationTarget.Absolute(htlc2.cltvExpiry.blockHeight)) - - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == bobCommitTx.txid) - closingState.claimMainOutputTx.foreach(claimMainOutputTx => assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == claimMainOutputTx.tx.txid)) - val watchHtlcs = alice2blockchain.expectMsgType[WatchOutputSpent] :: alice2blockchain.expectMsgType[WatchOutputSpent] :: Nil - watchHtlcs.foreach(ws => assert(ws.txId == bobCommitTx.txid)) - assert(watchHtlcs.map(_.outputIndex).toSet == Set(claimHtlcSuccessTx, claimHtlcTimeoutTx).map(_.txIn.head.outPoint.index)) + alice ! CMD_FULFILL_HTLC(htlc1.id, r1, None, commit = true) + val htlcSuccess = alice2blockchain.expectReplaceableTxPublished[ClaimHtlcSuccessTx](ConfirmationTarget.Absolute(htlc1.cltvExpiry.blockHeight)) + assert(htlcSuccess.preimage == r1) + Transaction.correctlySpends(htlcSuccess.sign(), bobCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) alice2blockchain.expectNoMessage(100 millis) + // Bob's commitment and Alice's main transaction confirm. alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, bobCommitTx) - closingState.claimMainOutputTx.foreach(claimMainOutputTx => alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, claimMainOutputTx.tx)) - alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, claimHtlcSuccessTx) - alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, claimHtlcTimeoutTx) - assert(alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc == htlc2) - awaitCond(alice.stateName == CLOSED) + alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, closingTxs.anchorTx) + closingTxs.mainTx_opt.foreach(tx => alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, tx)) + + // Alice receives a failure for the second HTLC from downstream; she can stop watching the corresponding HTLC output. + alice ! CMD_FAIL_HTLC(htlc2.id, FailureReason.EncryptedDownstreamFailure(ByteVector.empty, None), None) + alice2blockchain.expectNoMessage(100 millis) + + // Alice restarts, and pending HTLC transactions confirm. + val beforeRestart = alice.stateData.asInstanceOf[DATA_CLOSING] + alice.setState(WAIT_FOR_INIT_INTERNAL, Nothing) + alice ! INPUT_RESTORED(beforeRestart) + alice2blockchain.expectMsgType[SetChannelId] + awaitCond(alice.stateName == CLOSING) + // Alice republishes the HTLC transactions, which then confirm. + val htlcTx1 = alice2blockchain.expectMsgType[PublishReplaceableTx] + val htlcTx2 = alice2blockchain.expectMsgType[PublishReplaceableTx] + assert(Set(htlcTx1.input, htlcTx2.input) == Set(htlcTimeoutTx.txIn.head.outPoint, htlcSuccess.input.outPoint)) + alice2blockchain.expectWatchOutputsSpent(Seq(htlcTx1.input, htlcTx2.input)) + alice2blockchain.expectNoMessage(100 millis) + alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, htlcSuccess.tx) + assert(alice.stateName == CLOSING) + alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, htlcTimeoutTx) + assert(alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc == htlc3) alice2blockchain.expectNoMessage(100 millis) alice2relayer.expectNoMessage(100 millis) + awaitCond(alice.stateName == CLOSED) } - test("recv INPUT_RESTORED (next remote commit, anchor outputs zero fee htlc txs)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + test("recv INPUT_RESTORED (next remote commit)") { f => import f._ - val (bobCommitTx, closingState, _) = testNextRemoteCommitTxConfirmed(f, ChannelFeatures(Features.StaticRemoteKey, Features.AnchorOutputsZeroFeeHtlcTx)) - val claimHtlcTimeoutTxs = getClaimHtlcTimeoutTxs(closingState) + val (bobCommitTx, closingTxs, _) = testNextRemoteCommitTxConfirmed(f, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) // simulate a node restart val beforeRestart = alice.stateData.asInstanceOf[DATA_CLOSING] @@ -1569,21 +1705,20 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // the commit tx hasn't been confirmed yet, so we watch the funding output first alice2blockchain.expectMsgType[WatchFundingSpent] // then we should re-publish unconfirmed transactions - inside(alice2blockchain.expectMsgType[PublishReplaceableTx]) { tx => - assert(tx.txInfo.isInstanceOf[ClaimLocalAnchorOutputTx]) - assert(tx.commitTx == bobCommitTx) - } - closingState.claimMainOutputTx.foreach(claimMain => assert(alice2blockchain.expectMsgType[PublishFinalTx].tx == claimMain.tx)) - claimHtlcTimeoutTxs.foreach(claimHtlcTimeout => assert(alice2blockchain.expectMsgType[PublishReplaceableTx].txInfo.tx == claimHtlcTimeout.tx)) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == bobCommitTx.txid) - closingState.claimMainOutputTx.foreach(claimMain => assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == claimMain.tx.txid)) - claimHtlcTimeoutTxs.foreach(claimHtlcTimeout => assert(alice2blockchain.expectMsgType[WatchOutputSpent].outputIndex == claimHtlcTimeout.input.outPoint.index)) + val anchorTx = alice2blockchain.expectReplaceableTxPublished[ClaimRemoteAnchorTx] + closingTxs.mainTx_opt.foreach(_ => alice2blockchain.expectFinalTxPublished("remote-main-delayed")) + val htlcTimeoutTxs = closingTxs.htlcTxs.map(_ => alice2blockchain.expectReplaceableTxPublished[ClaimHtlcTimeoutTx]) + assert(htlcTimeoutTxs.map(_.input.outPoint).toSet == closingTxs.htlcTxs.map(_.txIn.head.outPoint).toSet) + alice2blockchain.expectWatchTxConfirmed(bobCommitTx.txid) + closingTxs.mainTx_opt.foreach(tx => alice2blockchain.expectWatchOutputSpent(tx.txIn.head.outPoint)) + alice2blockchain.expectWatchOutputSpent(anchorTx.input.outPoint) + alice2blockchain.expectWatchOutputsSpent(htlcTimeoutTxs.map(_.input.outPoint)) } - private def testFutureRemoteCommitTxConfirmed(f: FixtureParam, channelFeatures: ChannelFeatures): Transaction = { + private def testFutureRemoteCommitTxConfirmed(f: FixtureParam, commitmentFormat: CommitmentFormat): Transaction = { import f._ val oldStateData = alice.stateData - assert(oldStateData.asInstanceOf[DATA_NORMAL].commitments.params.channelFeatures == channelFeatures) + assert(oldStateData.asInstanceOf[DATA_NORMAL].commitments.latest.commitmentFormat == commitmentFormat) // This HTLC will be fulfilled. val (ra1, htlca1) = addHtlc(25_000_000 msat, alice, bob, alice2bob, bob2alice) // These 2 HTLCs should timeout on-chain, but since alice lost data, she won't be able to claim them. @@ -1616,63 +1751,49 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // alice now waits for bob to publish its commitment awaitCond(alice.stateName == WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT) // bob is nice and publishes its commitment - val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx - channelFeatures.commitmentFormat match { - case _: AnchorOutputsCommitmentFormat => assert(bobCommitTx.txOut.length == 6) // two main outputs + two anchors + 2 HTLCs - case DefaultCommitmentFormat => assert(bobCommitTx.txOut.length == 4) // two main outputs + 2 HTLCs - } + val bobCommitTx = bob.signCommitTx() + assert(bobCommitTx.txOut.length == 6) // two main outputs + two anchors + 2 HTLCs alice ! WatchFundingSpentTriggered(bobCommitTx) bobCommitTx } - test("recv WatchTxConfirmedTriggered (future remote commit)") { f => + test("recv WatchTxConfirmedTriggered (future remote commit, anchor outputs zero fee htlc txs)") { f => import f._ - val bobCommitTx = testFutureRemoteCommitTxConfirmed(f, ChannelFeatures(Features.StaticRemoteKey)) - val txPublished = txListener.expectMsgType[TransactionPublished] - assert(txPublished.tx == bobCommitTx) - assert(txPublished.miningFee > 0.sat) // alice is funder, she pays the fee for the remote commit - // bob's commit tx sends directly to alice's wallet - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == bobCommitTx.txid) - awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].futureRemoteCommitPublished.isDefined) + val bobCommitTx = testFutureRemoteCommitTxConfirmed(f, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) + // alice is able to claim its main output + val mainTx = alice2blockchain.expectFinalTxPublished("remote-main-delayed") + Transaction.correctlySpends(mainTx.tx, bobCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + alice2blockchain.expectWatchTxConfirmed(bobCommitTx.txid) + alice2blockchain.expectWatchOutputSpent(mainTx.input) alice2blockchain.expectNoMessage(100 millis) // alice ignores the htlc-timeout + awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].futureRemoteCommitPublished.isDefined) // actual test starts here alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, bobCommitTx) - assert(txListener.expectMsgType[TransactionConfirmed].tx == bobCommitTx) + alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, mainTx.tx) awaitCond(alice.stateName == CLOSED) } - test("recv WatchTxConfirmedTriggered (future remote commit, option_static_remotekey)", Tag(ChannelStateTestsTags.StaticRemoteKey)) { f => - import f._ - val bobCommitTx = testFutureRemoteCommitTxConfirmed(f, ChannelFeatures(Features.StaticRemoteKey)) - // using option_static_remotekey alice doesn't need to sweep her output - awaitCond(alice.stateName == CLOSING, 10 seconds) - alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, bobCommitTx) - // after the commit tx is confirmed the channel is closed, no claim transactions needed - awaitCond(alice.stateName == CLOSED, 10 seconds) - } - - test("recv WatchTxConfirmedTriggered (future remote commit, anchor outputs zero fee htlc txs)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + test("recv WatchTxConfirmedTriggered (future remote commit, taproot)", Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => import f._ - val bobCommitTx = testFutureRemoteCommitTxConfirmed(f, ChannelFeatures(Features.StaticRemoteKey, Features.AnchorOutputsZeroFeeHtlcTx)) + val bobCommitTx = testFutureRemoteCommitTxConfirmed(f, ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat) // alice is able to claim its main output - val claimMainTx = alice2blockchain.expectMsgType[PublishFinalTx].tx - Transaction.correctlySpends(claimMainTx, bobCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == bobCommitTx.txid) - awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].futureRemoteCommitPublished.isDefined) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == claimMainTx.txid) + val mainTx = alice2blockchain.expectFinalTxPublished("remote-main-delayed") + Transaction.correctlySpends(mainTx.tx, bobCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + alice2blockchain.expectWatchTxConfirmed(bobCommitTx.txid) + alice2blockchain.expectWatchOutputSpent(mainTx.input) alice2blockchain.expectNoMessage(100 millis) // alice ignores the htlc-timeout + awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].futureRemoteCommitPublished.isDefined) // actual test starts here alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, bobCommitTx) - alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, claimMainTx) + alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, mainTx.tx) awaitCond(alice.stateName == CLOSED) } test("recv INPUT_RESTORED (future remote commit)") { f => import f._ - - val bobCommitTx = testFutureRemoteCommitTxConfirmed(f, ChannelFeatures(Features.StaticRemoteKey)) + val bobCommitTx = testFutureRemoteCommitTxConfirmed(f, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) // simulate a node restart val beforeRestart = alice.stateData.asInstanceOf[DATA_CLOSING] @@ -1680,133 +1801,110 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice ! INPUT_RESTORED(beforeRestart) awaitCond(alice.stateName == CLOSING) - // bob's commit tx sends funds directly to our wallet - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == bobCommitTx.txid) + // bob republishes his main transaction + val mainTx = alice2blockchain.expectFinalTxPublished("remote-main-delayed") + alice2blockchain.expectWatchTxConfirmed(bobCommitTx.txid) + alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, bobCommitTx) + alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, mainTx.tx) + awaitCond(alice.stateName == CLOSED) } - case class RevokedCloseFixture(bobRevokedTxs: Seq[LocalCommit], htlcsAlice: Seq[(UpdateAddHtlc, ByteVector32)], htlcsBob: Seq[(UpdateAddHtlc, ByteVector32)]) + case class RevokedCommit(commitTx: Transaction, htlcTxs: Seq[UnsignedHtlcTx]) - private def prepareRevokedClose(f: FixtureParam, channelFeatures: ChannelFeatures): RevokedCloseFixture = { + case class RevokedCloseFixture(bobRevokedTxs: Seq[RevokedCommit], htlcsAlice: Seq[(UpdateAddHtlc, ByteVector32)], htlcsBob: Seq[(UpdateAddHtlc, ByteVector32)]) + + private def prepareRevokedClose(f: FixtureParam): RevokedCloseFixture = { import f._ // Bob's first commit tx doesn't contain any htlc - val localCommit1 = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit - channelFeatures.commitmentFormat match { - case _: AnchorOutputsCommitmentFormat => assert(localCommit1.commitTxAndRemoteSig.commitTx.tx.txOut.size == 4) // 2 main outputs + 2 anchors - case DefaultCommitmentFormat => assert(localCommit1.commitTxAndRemoteSig.commitTx.tx.txOut.size == 2) // 2 main outputs - } + val bobCommit1 = RevokedCommit(bob.signCommitTx(), Nil) + assert(bobCommit1.commitTx.txOut.size == 4) // 2 main outputs + 2 anchors // Bob's second commit tx contains 1 incoming htlc and 1 outgoing htlc - val (localCommit2, htlcAlice1, htlcBob1) = { + val (bobCommit2, htlcAlice1, htlcBob1) = { val (ra, htlcAlice) = addHtlc(35_000_000 msat, alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) val (rb, htlcBob) = addHtlc(20_000_000 msat, bob, alice, bob2alice, alice2bob) crossSign(bob, alice, bob2alice, alice2bob) - val localCommit = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit - (localCommit, (htlcAlice, ra), (htlcBob, rb)) + val bobCommit2 = RevokedCommit(bob.signCommitTx(), bob.htlcTxs()) + (bobCommit2, (htlcAlice, ra), (htlcBob, rb)) } - assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx.txOut.size == localCommit2.commitTxAndRemoteSig.commitTx.tx.txOut.size) - channelFeatures.commitmentFormat match { - case _: AnchorOutputsCommitmentFormat => assert(localCommit2.commitTxAndRemoteSig.commitTx.tx.txOut.size == 6) - case DefaultCommitmentFormat => assert(localCommit2.commitTxAndRemoteSig.commitTx.tx.txOut.size == 4) - } + assert(alice.signCommitTx().txOut.size == bobCommit2.commitTx.txOut.size) + assert(bobCommit2.commitTx.txOut.size == 6) // Bob's third commit tx contains 2 incoming htlcs and 2 outgoing htlcs - val (localCommit3, htlcAlice2, htlcBob2) = { + val (bobCommit3, htlcAlice2, htlcBob2) = { val (ra, htlcAlice) = addHtlc(25_000_000 msat, alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) val (rb, htlcBob) = addHtlc(18_000_000 msat, bob, alice, bob2alice, alice2bob) crossSign(bob, alice, bob2alice, alice2bob) - val localCommit = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit - (localCommit, (htlcAlice, ra), (htlcBob, rb)) + val bobCommit3 = RevokedCommit(bob.signCommitTx(), bob.htlcTxs()) + (bobCommit3, (htlcAlice, ra), (htlcBob, rb)) } - assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx.txOut.size == localCommit3.commitTxAndRemoteSig.commitTx.tx.txOut.size) - channelFeatures.commitmentFormat match { - case _: AnchorOutputsCommitmentFormat => assert(localCommit3.commitTxAndRemoteSig.commitTx.tx.txOut.size == 8) - case DefaultCommitmentFormat => assert(localCommit3.commitTxAndRemoteSig.commitTx.tx.txOut.size == 6) - } + assert(alice.signCommitTx().txOut.size == bobCommit3.commitTx.txOut.size) + assert(bobCommit3.commitTx.txOut.size == 8) // Bob's fourth commit tx doesn't contain any htlc - val localCommit4 = { + val bobCommit4 = { Seq(htlcAlice1, htlcAlice2).foreach { case (htlcAlice, _) => failHtlc(htlcAlice.id, bob, alice, bob2alice, alice2bob) } Seq(htlcBob1, htlcBob2).foreach { case (htlcBob, _) => failHtlc(htlcBob.id, alice, bob, alice2bob, bob2alice) } crossSign(alice, bob, alice2bob, bob2alice) - bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit + RevokedCommit(bob.signCommitTx(), Nil) } - assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx.txOut.size == localCommit4.commitTxAndRemoteSig.commitTx.tx.txOut.size) - channelFeatures.commitmentFormat match { - case _: AnchorOutputsCommitmentFormat => assert(localCommit4.commitTxAndRemoteSig.commitTx.tx.txOut.size == 4) - case DefaultCommitmentFormat => assert(localCommit4.commitTxAndRemoteSig.commitTx.tx.txOut.size == 2) - } + assert(alice.signCommitTx().txOut.size == bobCommit4.commitTx.txOut.size) + assert(bobCommit4.commitTx.txOut.size == 4) - RevokedCloseFixture(Seq(localCommit1, localCommit2, localCommit3, localCommit4), Seq(htlcAlice1, htlcAlice2), Seq(htlcBob1, htlcBob2)) + RevokedCloseFixture(Seq(bobCommit1, bobCommit2, bobCommit3, bobCommit4), Seq(htlcAlice1, htlcAlice2), Seq(htlcBob1, htlcBob2)) } - private def setupFundingSpentRevokedTx(f: FixtureParam, channelFeatures: ChannelFeatures): (Transaction, RevokedCommitPublished) = { + case class RevokedCloseTxs(mainTx: Transaction, mainPenaltyTx: Transaction, htlcPenaltyTxs: Seq[Transaction]) + + private def setupFundingSpentRevokedTx(f: FixtureParam, commitmentFormat: CommitmentFormat): (Transaction, RevokedCloseTxs) = { import f._ - val revokedCloseFixture = prepareRevokedClose(f, channelFeatures) - assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.params.channelFeatures == channelFeatures) + val revokedCloseFixture = prepareRevokedClose(f) + assert(alice.commitments.latest.commitmentFormat == commitmentFormat) // bob publishes one of his revoked txs - val bobRevokedTx = revokedCloseFixture.bobRevokedTxs(1).commitTxAndRemoteSig.commitTx.tx + val bobRevokedTx = revokedCloseFixture.bobRevokedTxs(1).commitTx alice ! WatchFundingSpentTriggered(bobRevokedTx) awaitCond(alice.stateData.isInstanceOf[DATA_CLOSING]) awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.size == 1) val rvk = alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.head assert(rvk.commitTx == bobRevokedTx) - if (!channelFeatures.paysDirectlyToWallet) { - assert(rvk.claimMainOutputTx.nonEmpty) - } - assert(rvk.mainPenaltyTx.nonEmpty) - assert(rvk.htlcPenaltyTxs.size == 2) - assert(rvk.claimHtlcDelayedPenaltyTxs.isEmpty) - val penaltyTxs = rvk.claimMainOutputTx.toList ++ rvk.mainPenaltyTx.toList ++ rvk.htlcPenaltyTxs + assert(rvk.localOutput_opt.nonEmpty) + assert(rvk.remoteOutput_opt.nonEmpty) + assert(rvk.htlcOutputs.size == 2) + assert(rvk.htlcDelayedOutputs.isEmpty) // alice publishes the penalty txs - if (!channelFeatures.paysDirectlyToWallet) { - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx == rvk.claimMainOutputTx.get.tx) - } - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx == rvk.mainPenaltyTx.get.tx) - assert(Set(alice2blockchain.expectMsgType[PublishFinalTx].tx, alice2blockchain.expectMsgType[PublishFinalTx].tx) == rvk.htlcPenaltyTxs.map(_.tx).toSet) - for (penaltyTx <- penaltyTxs) { - Transaction.correctlySpends(penaltyTx.tx, bobRevokedTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - } + val mainTx = alice2blockchain.expectFinalTxPublished("remote-main-delayed") + val mainPenaltyTx = alice2blockchain.expectFinalTxPublished("main-penalty") + Transaction.correctlySpends(mainPenaltyTx.tx, bobRevokedTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + val htlcPenaltyTxs = (0 until 2).map(_ => alice2blockchain.expectFinalTxPublished("htlc-penalty")) + assert(htlcPenaltyTxs.map(_.input).toSet == rvk.htlcOutputs) + htlcPenaltyTxs.foreach(penaltyTx => Transaction.correctlySpends(penaltyTx.tx, bobRevokedTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) // alice spends all outpoints of the revoked tx, except her main output when it goes directly to our wallet - val spentOutpoints = penaltyTxs.flatMap(_.tx.txIn.map(_.outPoint)).toSet - assert(spentOutpoints.forall(_.txid == bobRevokedTx.txid)) - if (channelFeatures.commitmentFormat.isInstanceOf[AnchorOutputsCommitmentFormat]) { - assert(spentOutpoints.size == bobRevokedTx.txOut.size - 2) // we don't claim the anchors - } - else if (channelFeatures.paysDirectlyToWallet) { - assert(spentOutpoints.size == bobRevokedTx.txOut.size - 1) // we don't claim our main output, it directly goes to our wallet - } else { - assert(spentOutpoints.size == bobRevokedTx.txOut.size) - } - - // alice watches confirmation for the outputs only her can claim - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == bobRevokedTx.txid) - if (!channelFeatures.paysDirectlyToWallet) { - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == rvk.claimMainOutputTx.get.tx.txid) - } + val spentOutpoints = Seq(mainTx.input, mainPenaltyTx.input) ++ htlcPenaltyTxs.map(_.input) + assert(spentOutpoints.size == bobRevokedTx.txOut.size - 2) // we don't claim the anchors - // alice watches outputs that can be spent by both parties - val watchedOutpoints = Seq(alice2blockchain.expectMsgType[WatchOutputSpent], alice2blockchain.expectMsgType[WatchOutputSpent], alice2blockchain.expectMsgType[WatchOutputSpent]).map(_.outputIndex).toSet - assert(watchedOutpoints == (rvk.mainPenaltyTx.get :: rvk.htlcPenaltyTxs).map(_.input.outPoint.index).toSet) + // alice watches on-chain transactions + alice2blockchain.expectWatchTxConfirmed(bobRevokedTx.txid) + alice2blockchain.expectWatchOutputsSpent(spentOutpoints) alice2blockchain.expectNoMessage(100 millis) - (bobRevokedTx, rvk) + (bobRevokedTx, RevokedCloseTxs(mainTx.tx, mainPenaltyTx.tx, htlcPenaltyTxs.map(_.tx))) } - private def testFundingSpentRevokedTx(f: FixtureParam, channelFeatures: ChannelFeatures): Unit = { + private def testFundingSpentRevokedTx(f: FixtureParam, commitmentFormat: CommitmentFormat): Unit = { import f._ - val (bobRevokedTx, rvk) = setupFundingSpentRevokedTx(f, channelFeatures) + val (bobRevokedTx, closingTxs) = setupFundingSpentRevokedTx(f, commitmentFormat) val txPublished = txListener.expectMsgType[TransactionPublished] assert(txPublished.tx == bobRevokedTx) assert(txPublished.miningFee > 0.sat) // alice is funder, she pays the fee for the revoked commit @@ -1814,80 +1912,75 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // once all txs are confirmed, alice can move to the closed state alice ! WatchTxConfirmedTriggered(BlockHeight(100), 3, bobRevokedTx) assert(txListener.expectMsgType[TransactionConfirmed].tx == bobRevokedTx) - alice ! WatchTxConfirmedTriggered(BlockHeight(110), 1, rvk.mainPenaltyTx.get.tx) - if (!channelFeatures.paysDirectlyToWallet) { - alice ! WatchTxConfirmedTriggered(BlockHeight(110), 2, rvk.claimMainOutputTx.get.tx) - } - alice ! WatchTxConfirmedTriggered(BlockHeight(115), 0, rvk.htlcPenaltyTxs(0).tx) + alice ! WatchTxConfirmedTriggered(BlockHeight(110), 1, closingTxs.mainPenaltyTx) + alice ! WatchTxConfirmedTriggered(BlockHeight(110), 2, closingTxs.mainTx) + closingTxs.htlcPenaltyTxs.dropRight(1).foreach(tx => alice ! WatchTxConfirmedTriggered(BlockHeight(115), 0, tx)) assert(alice.stateName == CLOSING) - alice ! WatchTxConfirmedTriggered(BlockHeight(115), 2, rvk.htlcPenaltyTxs(1).tx) + alice ! WatchTxConfirmedTriggered(BlockHeight(115), 2, closingTxs.htlcPenaltyTxs.last) awaitCond(alice.stateName == CLOSED) + val closedAlice = alice.stateData.asInstanceOf[DATA_CLOSED] + assert(closedAlice.closingType == "revoked-close") + assert(closedAlice.closingTxId == bobRevokedTx.txid) + assert(closedAlice.closingAmount == closingTxs.mainTx.txOut.head.amount + closingTxs.mainPenaltyTx.txOut.head.amount + closingTxs.htlcPenaltyTxs.map(_.txOut.head.amount).sum) } - test("recv WatchFundingSpentTriggered (one revoked tx, option_static_remotekey)", Tag(ChannelStateTestsTags.StaticRemoteKey)) { f => - testFundingSpentRevokedTx(f, ChannelFeatures(Features.StaticRemoteKey)) + test("recv WatchFundingSpentTriggered (one revoked tx)") { f => + testFundingSpentRevokedTx(f, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) } - test("recv WatchFundingSpentTriggered (one revoked tx, anchor outputs)", Tag(ChannelStateTestsTags.AnchorOutputs)) { f => - testFundingSpentRevokedTx(f, ChannelFeatures(Features.StaticRemoteKey, Features.AnchorOutputs)) + test("recv WatchFundingSpentTriggered (one revoked tx, anchor outputs phoenix)", Tag(ChannelStateTestsTags.AnchorOutputsPhoenix)) { f => + testFundingSpentRevokedTx(f, UnsafeLegacyAnchorOutputsCommitmentFormat) } - test("recv WatchFundingSpentTriggered (one revoked tx, anchor outputs zero fee htlc txs)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => - testFundingSpentRevokedTx(f, ChannelFeatures(Features.StaticRemoteKey, Features.AnchorOutputsZeroFeeHtlcTx)) + test("recv WatchFundingSpentTriggered (one revoked tx, taproot)", Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => + testFundingSpentRevokedTx(f, ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat) } - test("recv WatchFundingSpentTriggered (multiple revoked tx)") { f => + test("recv WatchFundingSpentTriggered (multiple revoked tx)", Tag(ChannelStateTestsTags.OptionSimpleTaprootPhoenix)) { f => import f._ - val revokedCloseFixture = prepareRevokedClose(f, ChannelFeatures(Features.StaticRemoteKey)) - assert(revokedCloseFixture.bobRevokedTxs.map(_.commitTxAndRemoteSig.commitTx.tx.txid).toSet.size == revokedCloseFixture.bobRevokedTxs.size) // all commit txs are distinct + val revokedCloseFixture = prepareRevokedClose(f) + assert(revokedCloseFixture.bobRevokedTxs.map(_.commitTx.txid).toSet.size == revokedCloseFixture.bobRevokedTxs.size) // all commit txs are distinct - def broadcastBobRevokedTx(revokedTx: Transaction, htlcCount: Int, revokedCount: Int): RevokedCommitPublished = { + def broadcastBobRevokedTx(revokedTx: Transaction, htlcCount: Int, revokedCount: Int): RevokedCloseTxs = { alice ! WatchFundingSpentTriggered(revokedTx) awaitCond(alice.stateData.isInstanceOf[DATA_CLOSING]) awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.size == revokedCount) assert(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.last.commitTx == revokedTx) // alice publishes penalty txs - val mainPenalty = alice2blockchain.expectMsgType[PublishFinalTx].tx - val claimMain_opt = if (!alice.stateData.asInstanceOf[DATA_CLOSING].commitments.params.channelFeatures.paysDirectlyToWallet) Some(alice2blockchain.expectMsgType[PublishFinalTx].tx) else None - val htlcPenaltyTxs = (1 to htlcCount).map(_ => alice2blockchain.expectMsgType[PublishFinalTx].tx) - (mainPenalty +: (claimMain_opt.toList ++ htlcPenaltyTxs)).foreach(tx => Transaction.correctlySpends(tx, revokedTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) - - // alice watches confirmation for the outputs only her can claim - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == revokedTx.txid) - claimMain_opt.foreach(claimMain => assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == claimMain.txid)) - - // alice watches outputs that can be spent by both parties - assert(alice2blockchain.expectMsgType[WatchOutputSpent].outputIndex == mainPenalty.txIn.head.outPoint.index) - val htlcOutpoints = (1 to htlcCount).map(_ => alice2blockchain.expectMsgType[WatchOutputSpent].outputIndex).toSet - assert(htlcOutpoints == htlcPenaltyTxs.flatMap(_.txIn.map(_.outPoint.index)).toSet) + val mainTx = alice2blockchain.expectFinalTxPublished("remote-main-delayed") + val mainPenalty = alice2blockchain.expectFinalTxPublished("main-penalty") + val htlcPenaltyTxs = (1 to htlcCount).map(_ => alice2blockchain.expectFinalTxPublished("htlc-penalty")) + (mainTx.tx +: mainPenalty.tx +: htlcPenaltyTxs.map(_.tx)).foreach(tx => Transaction.correctlySpends(tx, revokedTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) + alice2blockchain.expectWatchTxConfirmed(revokedTx.txid) + alice2blockchain.expectWatchOutputsSpent(mainTx.input +: mainPenalty.input +: htlcPenaltyTxs.map(_.input)) alice2blockchain.expectNoMessage(100 millis) - alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.last + RevokedCloseTxs(mainTx.tx, mainPenalty.tx, htlcPenaltyTxs.map(_.tx)) } // bob publishes a first revoked tx (no htlc in that commitment) - broadcastBobRevokedTx(revokedCloseFixture.bobRevokedTxs.head.commitTxAndRemoteSig.commitTx.tx, 0, 1) + broadcastBobRevokedTx(revokedCloseFixture.bobRevokedTxs.head.commitTx, 0, 1) // bob publishes a second revoked tx - val rvk2 = broadcastBobRevokedTx(revokedCloseFixture.bobRevokedTxs(1).commitTxAndRemoteSig.commitTx.tx, 2, 2) + val closingTxs = broadcastBobRevokedTx(revokedCloseFixture.bobRevokedTxs(1).commitTx, 2, 2) // bob publishes a third revoked tx - broadcastBobRevokedTx(revokedCloseFixture.bobRevokedTxs(2).commitTxAndRemoteSig.commitTx.tx, 4, 3) + broadcastBobRevokedTx(revokedCloseFixture.bobRevokedTxs(2).commitTx, 4, 3) // bob's second revoked tx confirms: once all penalty txs are confirmed, alice can move to the closed state // NB: if multiple txs confirm in the same block, we may receive the events in any order - alice ! WatchTxConfirmedTriggered(BlockHeight(100), 1, rvk2.mainPenaltyTx.get.tx) - rvk2.claimMainOutputTx.foreach(claimMainOutputTx => alice ! WatchTxConfirmedTriggered(BlockHeight(100), 2, claimMainOutputTx.tx)) - alice ! WatchTxConfirmedTriggered(BlockHeight(100), 3, rvk2.commitTx) - alice ! WatchTxConfirmedTriggered(BlockHeight(115), 0, rvk2.htlcPenaltyTxs(0).tx) + alice ! WatchTxConfirmedTriggered(BlockHeight(100), 1, closingTxs.mainPenaltyTx) + alice ! WatchTxConfirmedTriggered(BlockHeight(100), 2, closingTxs.mainTx) + alice ! WatchTxConfirmedTriggered(BlockHeight(100), 3, revokedCloseFixture.bobRevokedTxs(1).commitTx) + alice ! WatchTxConfirmedTriggered(BlockHeight(115), 0, closingTxs.htlcPenaltyTxs(0)) assert(alice.stateName == CLOSING) - alice ! WatchTxConfirmedTriggered(BlockHeight(115), 2, rvk2.htlcPenaltyTxs(1).tx) + alice ! WatchTxConfirmedTriggered(BlockHeight(115), 2, closingTxs.htlcPenaltyTxs(1)) awaitCond(alice.stateName == CLOSED) } - def testInputRestoredRevokedTx(f: FixtureParam, channelFeatures: ChannelFeatures): Unit = { + def testInputRestoredRevokedTx(f: FixtureParam, commitmentFormat: CommitmentFormat): Unit = { import f._ - val (bobRevokedTx, rvk) = setupFundingSpentRevokedTx(f, channelFeatures) + val (bobRevokedTx, closingTxs) = setupFundingSpentRevokedTx(f, commitmentFormat) // simulate a node restart val beforeRestart = alice.stateData.asInstanceOf[DATA_CLOSING] @@ -1899,176 +1992,151 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // the commit tx hasn't been confirmed yet, so we watch the funding output first alice2blockchain.expectMsgType[WatchFundingSpent] // then we should re-publish unconfirmed transactions - rvk.claimMainOutputTx.foreach(claimMain => assert(alice2blockchain.expectMsgType[PublishFinalTx].tx == claimMain.tx)) - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx == rvk.mainPenaltyTx.get.tx) - rvk.htlcPenaltyTxs.foreach(htlcPenalty => assert(alice2blockchain.expectMsgType[PublishFinalTx].tx == htlcPenalty.tx)) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == bobRevokedTx.txid) - rvk.claimMainOutputTx.foreach(claimMain => assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == claimMain.tx.txid)) - assert(alice2blockchain.expectMsgType[WatchOutputSpent].outputIndex == rvk.mainPenaltyTx.get.input.outPoint.index) - rvk.htlcPenaltyTxs.foreach(htlcPenalty => assert(alice2blockchain.expectMsgType[WatchOutputSpent].outputIndex == htlcPenalty.input.outPoint.index)) + alice2blockchain.expectFinalTxPublished("remote-main-delayed") + assert(alice2blockchain.expectFinalTxPublished("main-penalty").input == closingTxs.mainPenaltyTx.txIn.head.outPoint) + val htlcPenaltyTxs = closingTxs.htlcPenaltyTxs.map(_ => alice2blockchain.expectFinalTxPublished("htlc-penalty")) + assert(htlcPenaltyTxs.map(_.input).toSet == closingTxs.htlcPenaltyTxs.map(_.txIn.head.outPoint).toSet) + alice2blockchain.expectWatchTxConfirmed(bobRevokedTx.txid) + alice2blockchain.expectWatchOutputSpent(closingTxs.mainTx.txIn.head.outPoint) + alice2blockchain.expectWatchOutputSpent(closingTxs.mainPenaltyTx.txIn.head.outPoint) + alice2blockchain.expectWatchOutputsSpent(htlcPenaltyTxs.map(_.input)) } test("recv INPUT_RESTORED (one revoked tx)") { f => - testInputRestoredRevokedTx(f, ChannelFeatures(Features.StaticRemoteKey)) + testInputRestoredRevokedTx(f, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) } - test("recv INPUT_RESTORED (one revoked tx, anchor outputs)", Tag(ChannelStateTestsTags.AnchorOutputs)) { f => - testInputRestoredRevokedTx(f, ChannelFeatures(Features.StaticRemoteKey, Features.AnchorOutputs)) + test("recv INPUT_RESTORED (one revoked tx, anchor outputs phoenix)", Tag(ChannelStateTestsTags.AnchorOutputsPhoenix)) { f => + testInputRestoredRevokedTx(f, UnsafeLegacyAnchorOutputsCommitmentFormat) } - test("recv INPUT_RESTORED (one revoked tx, anchor outputs zero fee htlc txs)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => - testInputRestoredRevokedTx(f, ChannelFeatures(Features.StaticRemoteKey, Features.AnchorOutputsZeroFeeHtlcTx)) + test("recv INPUT_RESTORED (one revoked tx, taproot)", Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => + testInputRestoredRevokedTx(f, ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat) } - def testOutputSpentRevokedTx(f: FixtureParam, channelFeatures: ChannelFeatures): Unit = { + def testRevokedHtlcTxConfirmed(f: FixtureParam, commitmentFormat: CommitmentFormat): Unit = { import f._ - val revokedCloseFixture = prepareRevokedClose(f, channelFeatures) - assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.params.channelFeatures == channelFeatures) - val commitmentFormat = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.params.commitmentFormat + val revokedCloseFixture = prepareRevokedClose(f) + assert(alice.commitments.latest.commitmentFormat == commitmentFormat) // bob publishes one of his revoked txs val bobRevokedCommit = revokedCloseFixture.bobRevokedTxs(2) - alice ! WatchFundingSpentTriggered(bobRevokedCommit.commitTxAndRemoteSig.commitTx.tx) + alice ! WatchFundingSpentTriggered(bobRevokedCommit.commitTx) awaitCond(alice.stateData.isInstanceOf[DATA_CLOSING]) awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.size == 1) val rvk = alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.head - assert(rvk.commitTx == bobRevokedCommit.commitTxAndRemoteSig.commitTx.tx) - if (channelFeatures.paysDirectlyToWallet) { - assert(rvk.claimMainOutputTx.isEmpty) - } else { - assert(rvk.claimMainOutputTx.nonEmpty) - } - assert(rvk.mainPenaltyTx.nonEmpty) - assert(rvk.htlcPenaltyTxs.size == 4) - assert(rvk.claimHtlcDelayedPenaltyTxs.isEmpty) + assert(rvk.commitTx == bobRevokedCommit.commitTx) + assert(rvk.localOutput_opt.nonEmpty) + assert(rvk.remoteOutput_opt.nonEmpty) + assert(rvk.htlcOutputs.size == 4) + assert(rvk.htlcDelayedOutputs.isEmpty) // alice publishes the penalty txs and watches outputs - val claimTxsCount = if (channelFeatures.paysDirectlyToWallet) 5 else 6 // 2 main outputs and 4 htlcs - (1 to claimTxsCount).foreach(_ => alice2blockchain.expectMsgType[PublishTx]) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == rvk.commitTx.txid) - if (!channelFeatures.paysDirectlyToWallet) { - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == rvk.claimMainOutputTx.get.tx.txid) - } - (1 to 5).foreach(_ => alice2blockchain.expectMsgType[WatchOutputSpent]) // main output penalty and 4 htlc penalties + val mainTx = alice2blockchain.expectFinalTxPublished("remote-main-delayed") + val mainPenalty = alice2blockchain.expectFinalTxPublished("main-penalty") + val htlcPenalty = (1 to 4).map(_ => alice2blockchain.expectFinalTxPublished("htlc-penalty")) + alice2blockchain.expectWatchTxConfirmed(rvk.commitTx.txid) + alice2blockchain.expectWatchOutputsSpent(Seq(mainTx.input, mainPenalty.input) ++ htlcPenalty.map(_.input)) alice2blockchain.expectNoMessage(100 millis) - // bob manages to claim 2 htlc outputs before alice can penalize him: 1 htlc-success and 1 htlc-timeout. - val (fulfilledHtlc, preimage) = revokedCloseFixture.htlcsAlice.head - val (failedHtlc, _) = revokedCloseFixture.htlcsBob.last - val bobHtlcSuccessTx1 = bobRevokedCommit.htlcTxsAndRemoteSigs.collectFirst { - case HtlcTxAndRemoteSig(txInfo: HtlcSuccessTx, _) if txInfo.htlcId == fulfilledHtlc.id => - assert(fulfilledHtlc.paymentHash == txInfo.paymentHash) - Transactions.addSigs(txInfo, ByteVector64.Zeroes, ByteVector64.Zeroes, preimage, commitmentFormat) - }.get - val bobHtlcTimeoutTx = bobRevokedCommit.htlcTxsAndRemoteSigs.collectFirst { - case HtlcTxAndRemoteSig(txInfo: HtlcTimeoutTx, _) if txInfo.htlcId == failedHtlc.id => - Transactions.addSigs(txInfo, ByteVector64.Zeroes, ByteVector64.Zeroes, commitmentFormat) - }.get - val bobOutpoints = Seq(bobHtlcSuccessTx1, bobHtlcTimeoutTx).map(_.input.outPoint).toSet - assert(bobOutpoints.size == 2) - - // alice reacts by publishing penalty txs that spend bob's htlc transactions + // the revoked commit and main penalty transactions confirm + alice ! WatchTxConfirmedTriggered(BlockHeight(100), 3, rvk.commitTx) + alice ! WatchTxConfirmedTriggered(BlockHeight(110), 0, mainPenalty.tx) + alice ! WatchTxConfirmedTriggered(BlockHeight(110), 1, mainTx.tx) + + // bob publishes one of his HTLC-success transactions + val (fulfilledHtlc, _) = revokedCloseFixture.htlcsAlice.head + val bobHtlcSuccessTx1 = bobRevokedCommit.htlcTxs.collectFirst { case txInfo: UnsignedHtlcSuccessTx if txInfo.htlcId == fulfilledHtlc.id => txInfo }.get + assert(bobHtlcSuccessTx1.paymentHash == fulfilledHtlc.paymentHash) alice ! WatchOutputSpentTriggered(bobHtlcSuccessTx1.amountIn, bobHtlcSuccessTx1.tx) - awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.head.claimHtlcDelayedPenaltyTxs.size == 1) - val claimHtlcSuccessPenalty1 = alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.head.claimHtlcDelayedPenaltyTxs.last - Transaction.correctlySpends(claimHtlcSuccessPenalty1.tx, bobHtlcSuccessTx1.tx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == bobHtlcSuccessTx1.tx.txid) - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx == claimHtlcSuccessPenalty1.tx) - val watchSpent1 = alice2blockchain.expectMsgType[WatchOutputSpent] - assert(watchSpent1.txId == bobHtlcSuccessTx1.tx.txid) - assert(watchSpent1.outputIndex == claimHtlcSuccessPenalty1.input.outPoint.index) - alice2blockchain.expectNoMessage(100 millis) + alice2blockchain.expectWatchTxConfirmed(bobHtlcSuccessTx1.tx.txid) + // bob publishes one of his HTLC-timeout transactions + val (failedHtlc, _) = revokedCloseFixture.htlcsBob.last + val bobHtlcTimeoutTx = bobRevokedCommit.htlcTxs.collectFirst { case txInfo: UnsignedHtlcTimeoutTx if txInfo.htlcId == failedHtlc.id => txInfo }.get + assert(bobHtlcTimeoutTx.paymentHash == failedHtlc.paymentHash) alice ! WatchOutputSpentTriggered(bobHtlcTimeoutTx.amountIn, bobHtlcTimeoutTx.tx) - awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.head.claimHtlcDelayedPenaltyTxs.size == 2) - val claimHtlcTimeoutPenalty = alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.head.claimHtlcDelayedPenaltyTxs.last - Transaction.correctlySpends(claimHtlcTimeoutPenalty.tx, bobHtlcTimeoutTx.tx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == bobHtlcTimeoutTx.tx.txid) - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx == claimHtlcTimeoutPenalty.tx) - val watchSpent2 = alice2blockchain.expectMsgType[WatchOutputSpent] - assert(watchSpent2.txId == bobHtlcTimeoutTx.tx.txid) - assert(watchSpent2.outputIndex == claimHtlcTimeoutPenalty.input.outPoint.index) - alice2blockchain.expectNoMessage(100 millis) + alice2blockchain.expectWatchTxConfirmed(bobHtlcTimeoutTx.tx.txid) // bob RBFs his htlc-success with a different transaction val bobHtlcSuccessTx2 = bobHtlcSuccessTx1.tx.copy(txIn = TxIn(OutPoint(randomTxId(), 0), Nil, 0) +: bobHtlcSuccessTx1.tx.txIn) assert(bobHtlcSuccessTx2.txid !== bobHtlcSuccessTx1.tx.txid) alice ! WatchOutputSpentTriggered(bobHtlcSuccessTx1.amountIn, bobHtlcSuccessTx2) - awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.head.claimHtlcDelayedPenaltyTxs.size == 3) - val claimHtlcSuccessPenalty2 = alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.head.claimHtlcDelayedPenaltyTxs.last - assert(claimHtlcSuccessPenalty1.tx.txid != claimHtlcSuccessPenalty2.tx.txid) - Transaction.correctlySpends(claimHtlcSuccessPenalty2.tx, bobHtlcSuccessTx2 :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == bobHtlcSuccessTx2.txid) - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx == claimHtlcSuccessPenalty2.tx) - val watchSpent3 = alice2blockchain.expectMsgType[WatchOutputSpent] - assert(watchSpent3.txId == bobHtlcSuccessTx2.txid) - assert(watchSpent3.outputIndex == claimHtlcSuccessPenalty2.input.outPoint.index) + alice2blockchain.expectWatchTxConfirmed(bobHtlcSuccessTx2.txid) + + // bob's HTLC-timeout confirms: alice reacts by publishing a penalty tx + alice ! WatchTxConfirmedTriggered(BlockHeight(115), 0, bobHtlcTimeoutTx.tx) + awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.head.htlcDelayedOutputs.size == 1) + val htlcTimeoutDelayedPenalty = alice2blockchain.expectFinalTxPublished("htlc-delayed-penalty") + Transaction.correctlySpends(htlcTimeoutDelayedPenalty.tx, bobHtlcTimeoutTx.tx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + alice2blockchain.expectWatchOutputSpent(htlcTimeoutDelayedPenalty.input) + alice2blockchain.expectNoMessage(100 millis) + + // bob's htlc-success RBF confirms: alice reacts by publishing a penalty tx + alice ! WatchTxConfirmedTriggered(BlockHeight(115), 1, bobHtlcSuccessTx2) + awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.head.htlcDelayedOutputs.size == 2) + val htlcSuccessDelayedPenalty = alice2blockchain.expectFinalTxPublished("htlc-delayed-penalty") + Transaction.correctlySpends(htlcSuccessDelayedPenalty.tx, bobHtlcSuccessTx2 :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + alice2blockchain.expectWatchOutputSpent(htlcSuccessDelayedPenalty.input) alice2blockchain.expectNoMessage(100 millis) // transactions confirm: alice can move to the closed state - val remainingHtlcPenaltyTxs = rvk.htlcPenaltyTxs.filterNot(htlcPenalty => bobOutpoints.contains(htlcPenalty.input.outPoint)) + val bobHtlcOutpoints = Set(bobHtlcTimeoutTx.input.outPoint, bobHtlcSuccessTx1.input.outPoint) + val remainingHtlcPenaltyTxs = htlcPenalty.filterNot(tx => bobHtlcOutpoints.contains(tx.input)) assert(remainingHtlcPenaltyTxs.size == 2) - alice ! WatchTxConfirmedTriggered(BlockHeight(100), 3, rvk.commitTx) - alice ! WatchTxConfirmedTriggered(BlockHeight(110), 0, rvk.mainPenaltyTx.get.tx) - if (!channelFeatures.paysDirectlyToWallet) { - alice ! WatchTxConfirmedTriggered(BlockHeight(110), 1, rvk.claimMainOutputTx.get.tx) - } alice ! WatchTxConfirmedTriggered(BlockHeight(110), 2, remainingHtlcPenaltyTxs.head.tx) alice ! WatchTxConfirmedTriggered(BlockHeight(115), 2, remainingHtlcPenaltyTxs.last.tx) - alice ! WatchTxConfirmedTriggered(BlockHeight(115), 0, bobHtlcTimeoutTx.tx) - alice ! WatchTxConfirmedTriggered(BlockHeight(115), 1, bobHtlcSuccessTx2) + alice ! WatchTxConfirmedTriggered(BlockHeight(120), 0, htlcTimeoutDelayedPenalty.tx) assert(alice.stateName == CLOSING) - - alice ! WatchTxConfirmedTriggered(BlockHeight(120), 0, claimHtlcTimeoutPenalty.tx) - alice ! WatchTxConfirmedTriggered(BlockHeight(121), 0, claimHtlcSuccessPenalty2.tx) + alice ! WatchTxConfirmedTriggered(BlockHeight(121), 0, htlcSuccessDelayedPenalty.tx) awaitCond(alice.stateName == CLOSED) } - test("recv WatchOutputSpentTriggered (one revoked tx, counterparty published htlc-success tx, option_static_remotekey)", Tag(ChannelStateTestsTags.StaticRemoteKey)) { f => - testOutputSpentRevokedTx(f, ChannelFeatures(Features.StaticRemoteKey)) + test("recv WatchTxConfirmedTriggered (revoked htlc-success tx)") { f => + testRevokedHtlcTxConfirmed(f, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) } - test("recv WatchOutputSpentTriggered (one revoked tx, counterparty published htlc-success tx, anchor outputs)", Tag(ChannelStateTestsTags.AnchorOutputs)) { f => - testOutputSpentRevokedTx(f, ChannelFeatures(Features.StaticRemoteKey, Features.AnchorOutputs)) + test("recv WatchTxConfirmedTriggered (revoked htlc-success tx, anchor outputs phoenix)", Tag(ChannelStateTestsTags.AnchorOutputsPhoenix)) { f => + testRevokedHtlcTxConfirmed(f, UnsafeLegacyAnchorOutputsCommitmentFormat) } - test("recv WatchOutputSpentTriggered (one revoked tx, counterparty published htlc-success tx, anchor outputs zero fee htlc txs)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => - testOutputSpentRevokedTx(f, ChannelFeatures(Features.StaticRemoteKey, Features.AnchorOutputsZeroFeeHtlcTx)) + test("recv WatchTxConfirmedTriggered (revoked htlc-success tx, taproot)", Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => + testRevokedHtlcTxConfirmed(f, ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat) } - test("recv WatchOutputSpentTriggered (one revoked tx, counterparty published aggregated htlc tx)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + def testRevokedAggregatedHtlcTxConfirmed(f: FixtureParam, commitmentFormat: CommitmentFormat): Unit = { import f._ // bob publishes one of his revoked txs - val revokedCloseFixture = prepareRevokedClose(f, ChannelFeatures(Features.StaticRemoteKey, Features.AnchorOutputsZeroFeeHtlcTx)) + val revokedCloseFixture = prepareRevokedClose(f) val bobRevokedCommit = revokedCloseFixture.bobRevokedTxs(2) - val commitmentFormat = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.params.commitmentFormat - alice ! WatchFundingSpentTriggered(bobRevokedCommit.commitTxAndRemoteSig.commitTx.tx) + alice ! WatchFundingSpentTriggered(bobRevokedCommit.commitTx) awaitCond(alice.stateData.isInstanceOf[DATA_CLOSING]) - assert(alice.stateData.asInstanceOf[DATA_CLOSING].commitments.params.commitmentFormat == ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) + assert(alice.commitments.latest.commitmentFormat == commitmentFormat) awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.size == 1) val rvk = alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.head - assert(rvk.commitTx == bobRevokedCommit.commitTxAndRemoteSig.commitTx.tx) - assert(rvk.htlcPenaltyTxs.size == 4) - assert(rvk.claimHtlcDelayedPenaltyTxs.isEmpty) + assert(rvk.commitTx == bobRevokedCommit.commitTx) + assert(rvk.htlcOutputs.size == 4) + assert(rvk.htlcDelayedOutputs.isEmpty) // alice publishes the penalty txs and watches outputs - (1 to 6).foreach(_ => alice2blockchain.expectMsgType[PublishTx]) // 2 main outputs and 4 htlcs - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == rvk.commitTx.txid) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == rvk.claimMainOutputTx.get.tx.txid) - (1 to 5).foreach(_ => alice2blockchain.expectMsgType[WatchOutputSpent]) // main output penalty and 4 htlc penalties + val mainTx = alice2blockchain.expectFinalTxPublished("remote-main-delayed") + val mainPenalty = alice2blockchain.expectFinalTxPublished("main-penalty") + val htlcPenalty = (1 to 4).map(_ => alice2blockchain.expectFinalTxPublished("htlc-penalty")) + alice2blockchain.expectWatchTxConfirmed(rvk.commitTx.txid) + alice2blockchain.expectWatchOutputsSpent(Seq(mainTx.input, mainPenalty.input) ++ htlcPenalty.map(_.input)) alice2blockchain.expectNoMessage(100 millis) // bob claims multiple htlc outputs in a single transaction (this is possible with anchor outputs because signatures // use sighash_single | sighash_anyonecanpay) - val bobHtlcTxs = bobRevokedCommit.htlcTxsAndRemoteSigs.collect { - case HtlcTxAndRemoteSig(txInfo: HtlcSuccessTx, _) => + val bobHtlcTxs = bobRevokedCommit.htlcTxs.collect { + case txInfo: UnsignedHtlcSuccessTx => val preimage = revokedCloseFixture.htlcsAlice.collectFirst { case (add, preimage) if add.id == txInfo.htlcId => preimage }.get assert(Crypto.sha256(preimage) == txInfo.paymentHash) - Transactions.addSigs(txInfo, ByteVector64.Zeroes, ByteVector64.Zeroes, preimage, commitmentFormat) - case HtlcTxAndRemoteSig(txInfo: HtlcTimeoutTx, _) => - Transactions.addSigs(txInfo, ByteVector64.Zeroes, ByteVector64.Zeroes, commitmentFormat) + txInfo + case txInfo: UnsignedHtlcTimeoutTx => + txInfo } assert(bobHtlcTxs.map(_.input.outPoint).size == 4) val bobHtlcTx = Transaction( @@ -2092,45 +2160,152 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // alice reacts by publishing penalty txs that spend bob's htlc transaction alice ! WatchOutputSpentTriggered(bobHtlcTxs(0).amountIn, bobHtlcTx) - awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.head.claimHtlcDelayedPenaltyTxs.size == 4) - val claimHtlcDelayedPenaltyTxs = alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.head.claimHtlcDelayedPenaltyTxs - val spentOutpoints = Set(OutPoint(bobHtlcTx, 1), OutPoint(bobHtlcTx, 2), OutPoint(bobHtlcTx, 3), OutPoint(bobHtlcTx, 4)) - assert(claimHtlcDelayedPenaltyTxs.map(_.input.outPoint).toSet == spentOutpoints) - claimHtlcDelayedPenaltyTxs.foreach(claimHtlcPenalty => Transaction.correctlySpends(claimHtlcPenalty.tx, bobHtlcTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == bobHtlcTx.txid) - val publishedPenaltyTxs = Set( - alice2blockchain.expectMsgType[PublishFinalTx], - alice2blockchain.expectMsgType[PublishFinalTx], - alice2blockchain.expectMsgType[PublishFinalTx], - alice2blockchain.expectMsgType[PublishFinalTx] - ) - assert(publishedPenaltyTxs.map(_.tx) == claimHtlcDelayedPenaltyTxs.map(_.tx).toSet) - val watchedOutpoints = Seq( - alice2blockchain.expectMsgType[WatchOutputSpent], - alice2blockchain.expectMsgType[WatchOutputSpent], - alice2blockchain.expectMsgType[WatchOutputSpent], - alice2blockchain.expectMsgType[WatchOutputSpent] - ).map(w => OutPoint(w.txId, w.outputIndex)).toSet - assert(watchedOutpoints == spentOutpoints) + alice2blockchain.expectWatchTxConfirmed(bobHtlcTx.txid) + alice ! WatchTxConfirmedTriggered(BlockHeight(129), 7, bobHtlcTx) + awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.head.htlcDelayedOutputs.size == 4) + val htlcDelayedPenalty = (1 to 4).map(_ => alice2blockchain.expectFinalTxPublished("htlc-delayed-penalty")) + val spentOutpoints = Seq(OutPoint(bobHtlcTx, 1), OutPoint(bobHtlcTx, 2), OutPoint(bobHtlcTx, 3), OutPoint(bobHtlcTx, 4)) + assert(htlcDelayedPenalty.map(_.input).toSet == spentOutpoints.toSet) + assert(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.head.htlcDelayedOutputs == spentOutpoints.toSet) + htlcDelayedPenalty.foreach(penalty => Transaction.correctlySpends(penalty.tx, bobHtlcTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) + alice2blockchain.expectWatchOutputsSpent(spentOutpoints) alice2blockchain.expectNoMessage(100 millis) } - private def testRevokedTxConfirmed(f: FixtureParam, channelFeatures: ChannelFeatures): Unit = { + test("recv WatchTxConfirmedTriggered (revoked aggregated htlc tx)") { f => + testRevokedAggregatedHtlcTxConfirmed(f, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) + } + + test("recv WatchTxConfirmedTriggered (revoked aggregated htlc tx, taproot)", Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => + testRevokedAggregatedHtlcTxConfirmed(f, ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat) + } + + def testInputRestoredRevokedHtlcTxConfirmed(f: FixtureParam, commitmentFormat: CommitmentFormat): Unit = { import f._ - assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.params.channelFeatures == channelFeatures) - val initOutputCount = channelFeatures.commitmentFormat match { - case _: AnchorOutputsCommitmentFormat => 4 - case DefaultCommitmentFormat => 2 + + // Bob publishes one of his revoked txs. + alice.nodeParams.setBitcoinCoreFeerates(FeeratesPerKw.single(FeeratePerKw(2_500 sat))) + val revokedCloseFixture = prepareRevokedClose(f) + val bobRevokedCommit = revokedCloseFixture.bobRevokedTxs(2) + val commitTx = bobRevokedCommit.commitTx + alice ! WatchFundingSpentTriggered(commitTx) + awaitCond(alice.stateData.isInstanceOf[DATA_CLOSING]) + assert(alice.commitments.latest.commitmentFormat == commitmentFormat) + + // Alice publishes the penalty txs and watches outputs. + val mainTx = alice2blockchain.expectFinalTxPublished("remote-main-delayed") + val mainPenalty = alice2blockchain.expectFinalTxPublished("main-penalty") + val htlcPenalty = (1 to 4).map(_ => alice2blockchain.expectFinalTxPublished("htlc-penalty")) + alice2blockchain.expectWatchTxConfirmed(commitTx.txid) + alice2blockchain.expectWatchOutputsSpent(Seq(mainTx.input, mainPenalty.input) ++ htlcPenalty.map(_.input)) + alice ! WatchTxConfirmedTriggered(BlockHeight(700_000), 2, commitTx) + alice2blockchain.expectNoMessage(100 millis) + + // Bob claims HTLC outputs using aggregated transactions. + val bobHtlcTxs = bobRevokedCommit.htlcTxs + assert(bobHtlcTxs.map(_.input.outPoint).size == 4) + val bobHtlcTx1 = Transaction( + 2, + Seq( + TxIn(OutPoint(randomTxId(), 4), Nil, 1), // utxo used for fee bumping + bobHtlcTxs(0).tx.txIn.head, + TxIn(OutPoint(randomTxId(), 4), Nil, 1), // unrelated utxo + bobHtlcTxs(1).tx.txIn.head, + ), + Seq( + TxOut(10_000 sat, Script.pay2wpkh(randomKey().publicKey)), // change output + bobHtlcTxs(0).tx.txOut.head, + TxOut(15_000 sat, Script.pay2wpkh(randomKey().publicKey)), // unrelated output + bobHtlcTxs(1).tx.txOut.head, + ), + 0 + ) + val bobHtlcTx2 = Transaction( + 2, + Seq( + bobHtlcTxs(2).tx.txIn.head, + bobHtlcTxs(3).tx.txIn.head, + TxIn(OutPoint(randomTxId(), 0), Nil, 1), // utxo used for fee bumping + ), + Seq( + bobHtlcTxs(2).tx.txOut.head, + bobHtlcTxs(3).tx.txOut.head, + TxOut(20_000 sat, Script.pay2wpkh(randomKey().publicKey)), // change output + ), + 0 + ) + + // Alice reacts by publishing penalty txs that spend bob's htlc transactions. + val htlcDelayedPenalty = Seq(bobHtlcTx1, bobHtlcTx2).flatMap(bobHtlcTx => { + alice ! WatchOutputSpentTriggered(bobHtlcTxs(0).amountIn, bobHtlcTx) + alice2blockchain.expectWatchTxConfirmed(bobHtlcTx.txid) + alice ! WatchTxConfirmedTriggered(BlockHeight(700_004), 7, bobHtlcTx) + val htlcDelayedPenalty = (1 to 2).map(_ => alice2blockchain.expectFinalTxPublished("htlc-delayed-penalty")) + alice2blockchain.expectWatchOutputsSpent(htlcDelayedPenalty.map(_.input)) + htlcDelayedPenalty.foreach(penalty => Transaction.correctlySpends(penalty.tx, bobHtlcTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) + htlcDelayedPenalty + }) + assert(htlcDelayedPenalty.map(_.input).toSet == Set(OutPoint(bobHtlcTx1, 1), OutPoint(bobHtlcTx1, 3), OutPoint(bobHtlcTx2, 0), OutPoint(bobHtlcTx2, 1))) + awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.head.htlcDelayedOutputs.size == 4) + + // We simulate a node restart after a feerate increase. + val beforeRestart = alice.stateData.asInstanceOf[DATA_CLOSING] + alice.nodeParams.setBitcoinCoreFeerates(FeeratesPerKw.single(FeeratePerKw(5_000 sat))) + alice.setState(WAIT_FOR_INIT_INTERNAL, Nothing) + alice ! INPUT_RESTORED(beforeRestart) + alice2blockchain.expectMsgType[SetChannelId] + awaitCond(alice.stateName == CLOSING) + + // We re-publish closing transactions with a higher feerate. + val mainTx2 = alice2blockchain.expectFinalTxPublished("remote-main-delayed") + assert(mainTx2.input == mainTx.input) + assert(mainTx2.tx.txOut.head.amount < mainTx.tx.txOut.head.amount) + Transaction.correctlySpends(mainTx2.tx, Seq(commitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + val mainPenalty2 = alice2blockchain.expectFinalTxPublished("main-penalty") + assert(mainPenalty2.input == mainPenalty.input) + assert(mainPenalty2.tx.txOut.head.amount < mainPenalty.tx.txOut.head.amount) + Transaction.correctlySpends(mainPenalty2.tx, Seq(commitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + alice2blockchain.expectWatchOutputsSpent(Seq(mainTx2.input, mainPenalty2.input)) + val htlcDelayedPenalty2 = (1 to 4).map(_ => alice2blockchain.expectFinalTxPublished("htlc-delayed-penalty")) + alice2blockchain.expectWatchOutputsSpent(htlcDelayedPenalty2.map(_.input)) + assert(htlcDelayedPenalty2.map(_.input).toSet == htlcDelayedPenalty.map(_.input).toSet) + assert(htlcDelayedPenalty2.map(_.tx.txOut.head.amount).sum < htlcDelayedPenalty.map(_.tx.txOut.head.amount).sum) + htlcDelayedPenalty2.foreach { + case txInfo if txInfo.input.txid == bobHtlcTx1.txid => Transaction.correctlySpends(txInfo.tx, Seq(bobHtlcTx1), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + case txInfo => Transaction.correctlySpends(txInfo.tx, Seq(bobHtlcTx2), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) } - assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx.txOut.size == initOutputCount) + + // The remaining transactions confirm. + alice ! WatchTxConfirmedTriggered(BlockHeight(700_009), 18, mainTx.tx) + alice ! WatchTxConfirmedTriggered(BlockHeight(700_011), 11, mainPenalty2.tx) + // Some of the updated HTLC-delayed penalty transactions confirm. + htlcDelayedPenalty2.take(3).foreach(p => alice ! WatchTxConfirmedTriggered(BlockHeight(700_015), 0, p.tx)) + assert(alice.stateName == CLOSING) + // The last HTLC-delayed penalty to confirm is the previous version with a lower feerate. + htlcDelayedPenalty.filter(p => !htlcDelayedPenalty2.take(3).map(_.input).contains(p.input)).foreach(p => alice ! WatchTxConfirmedTriggered(BlockHeight(700_016), 0, p.tx)) + awaitCond(alice.stateName == CLOSED) + } + + test("recv INPUT_RESTORED (revoked htlc transactions confirmed)") { f => + testInputRestoredRevokedHtlcTxConfirmed(f, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) + } + + test("recv INPUT_RESTORED (revoked htlc transactions confirmed, taproot)", Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => + testInputRestoredRevokedHtlcTxConfirmed(f, ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat) + } + + private def testRevokedTxConfirmed(f: FixtureParam, commitmentFormat: CommitmentFormat): Unit = { + import f._ + assert(alice.commitments.latest.commitmentFormat == commitmentFormat) + assert(bob.signCommitTx().txOut.size == 4) // bob's second commit tx contains 2 incoming htlcs val (bobRevokedTx, htlcs1) = { val (_, htlc1) = addHtlc(35_000_000 msat, alice, bob, alice2bob, bob2alice) val (_, htlc2) = addHtlc(20_000_000 msat, alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) - val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx - assert(bobCommitTx.txOut.size == initOutputCount + 2) + val bobCommitTx = bob.signCommitTx() + assert(bobCommitTx.txOut.size == 6) (bobCommitTx, Seq(htlc1, htlc2)) } @@ -2140,7 +2315,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val (_, htlc4) = addHtlc(18_000_000 msat, alice, bob, alice2bob, bob2alice) failHtlc(htlcs1.head.id, bob, alice, bob2alice, alice2bob) crossSign(bob, alice, bob2alice, alice2bob) - assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx.txOut.size == initOutputCount + 3) + assert(bob.signCommitTx().txOut.size == 7) Seq(htlc3, htlc4) } @@ -2164,16 +2339,12 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice2relayer.expectNoMessage(100 millis) } - test("recv WatchTxConfirmedTriggered (one revoked tx, pending htlcs)") { f => - testRevokedTxConfirmed(f, ChannelFeatures(Features.StaticRemoteKey)) - } - - test("recv WatchTxConfirmedTriggered (one revoked tx, pending htlcs, anchor outputs)", Tag(ChannelStateTestsTags.AnchorOutputs)) { f => - testRevokedTxConfirmed(f, ChannelFeatures(Features.StaticRemoteKey, Features.AnchorOutputs)) + test("recv WatchTxConfirmedTriggered (revoked commit tx, pending htlcs)") { f => + testRevokedTxConfirmed(f, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) } - test("recv WatchTxConfirmedTriggered (one revoked tx, pending htlcs, anchor outputs zero fee htlc txs)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => - testRevokedTxConfirmed(f, ChannelFeatures(Features.StaticRemoteKey, Features.AnchorOutputsZeroFeeHtlcTx)) + test("recv WatchTxConfirmedTriggered (revoked commit tx, pending htlcs, taproot)", Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => + testRevokedTxConfirmed(f, ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat) } test("recv ChannelReestablish") { f => @@ -2181,12 +2352,8 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with mutualClose(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain) val initialState = alice.stateData.asInstanceOf[DATA_CLOSING] val bobCommitments = bob.stateData.asInstanceOf[DATA_CLOSING].commitments - val bobCurrentPerCommitmentPoint = TestConstants.Bob.channelKeyManager.commitmentPoint( - TestConstants.Bob.channelKeyManager.keyPath(bobCommitments.params.localParams, bobCommitments.params.channelConfig), - bobCommitments.localCommitIndex) - + val bobCurrentPerCommitmentPoint = bob.underlyingActor.channelKeys.commitmentPoint(bobCommitments.localCommitIndex) alice ! ChannelReestablish(channelId(bob), 42, 42, PrivateKey(ByteVector32.Zeroes), bobCurrentPerCommitmentPoint) - val error = alice2bob.expectMsgType[Error] assert(new String(error.data.toArray) == FundingTxSpent(channelId(alice), initialState.spendingTxs.head.txid).getMessage) } @@ -2208,4 +2375,34 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with sender.expectMsg(RES_FAILURE(c, ClosingAlreadyInProgress(channelId(alice)))) } + test("recv CMD_FORCECLOSE (max_closing_feerate override)") { f => + import f._ + + val sender = TestProbe() + alice ! CMD_FORCECLOSE(sender.ref) + awaitCond(alice.stateName == CLOSING) + assert(alice.stateData.asInstanceOf[DATA_CLOSING].maxClosingFeerate_opt.isEmpty) + val commitTx = alice2blockchain.expectFinalTxPublished("commit-tx").tx + val anchorTx = alice2blockchain.expectReplaceableTxPublished[ClaimLocalAnchorTx] + val claimMain1 = alice2blockchain.expectFinalTxPublished("local-main-delayed") + alice2blockchain.expectWatchTxConfirmed(commitTx.txid) + alice2blockchain.expectWatchOutputsSpent(Seq(claimMain1.input, anchorTx.input.outPoint)) + + alice ! CMD_FORCECLOSE(sender.ref, maxClosingFeerate_opt = Some(FeeratePerKw(5_000 sat))) + awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].maxClosingFeerate_opt.contains(FeeratePerKw(5_000 sat))) + alice2blockchain.expectFinalTxPublished(commitTx.txid) + alice2blockchain.expectReplaceableTxPublished[ClaimLocalAnchorTx] + val claimMain2 = alice2blockchain.expectFinalTxPublished("local-main-delayed") + alice2blockchain.expectWatchTxConfirmed(commitTx.txid) + alice2blockchain.expectWatchOutputsSpent(Seq(claimMain2.input, anchorTx.input.outPoint)) + assert(claimMain2.fee != claimMain1.fee) + + alice ! CMD_FORCECLOSE(sender.ref, maxClosingFeerate_opt = Some(FeeratePerKw(10_000 sat))) + awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].maxClosingFeerate_opt.contains(FeeratePerKw(10_000 sat))) + alice2blockchain.expectFinalTxPublished(commitTx.txid) + alice2blockchain.expectReplaceableTxPublished[ClaimLocalAnchorTx] + val claimMain3 = alice2blockchain.expectFinalTxPublished("local-main-delayed") + assert(claimMain2.fee * 1.9 <= claimMain3.fee && claimMain3.fee <= claimMain2.fee * 2.1) + } + } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/GeneratorsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/GeneratorsSpec.scala deleted file mode 100644 index 760b9ff414..0000000000 --- a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/GeneratorsSpec.scala +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright 2019 ACINQ SAS - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package fr.acinq.eclair.crypto - -import fr.acinq.bitcoin.scalacompat.ByteVector32 -import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} -import org.scalatest.funsuite.AnyFunSuite -import scodec.bits._ - - -class GeneratorsSpec extends AnyFunSuite { - val base_secret: PrivateKey = PrivateKey(hex"000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f") - val per_commitment_secret: PrivateKey = PrivateKey(hex"1f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100") - val base_point = PublicKey(hex"036d6caac248af96f6afa7f904f550253a0f3ef3f5aa2fe6838a95b216691468e2") - val per_commitment_point = PublicKey(hex"025f7117a78150fe2ef97db7cfc83bd57b2e2c0d0dd25eaf467a4a1c2a45ce1486") - - test("derivation of key from basepoint and per-commitment-point") { - val localKey = Generators.derivePubKey(base_point, per_commitment_point) - assert(localKey.value == hex"0235f2dbfaa89b57ec7b055afe29849ef7ddfeb1cefdb9ebdc43f5494984db29e5") - } - - test("derivation of secret key from basepoint secret and per-commitment-secret") { - val localprivkey = Generators.derivePrivKey(base_secret, per_commitment_point) - assert(localprivkey.value == ByteVector32(hex"cbced912d3b21bf196a766651e436aff192362621ce317704ea2f75d87e7be0f")) - } - - test("derivation of revocation key from basepoint and per-commitment-point") { - val revocationkey = Generators.revocationPubKey(base_point, per_commitment_point) - assert(revocationkey.value == hex"02916e326636d19c33f13e8c0c3a03dd157f332f3e99c317c141dd865eb01f8ff0") - } - - test("derivation of revocation secret from basepoint-secret and per-commitment-secret") { - val revocationprivkey = Generators.revocationPrivKey(base_secret, per_commitment_secret) - assert(revocationprivkey.value == ByteVector32(hex"d09ffff62ddb2297ab000cc85bcb4283fdeb6aa052affbc9dddcf33b61078110")) - } -} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/NonceGeneratorSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/NonceGeneratorSpec.scala new file mode 100644 index 0000000000..917d8aa998 --- /dev/null +++ b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/NonceGeneratorSpec.scala @@ -0,0 +1,59 @@ +/* + * Copyright 2025 ACINQ SAS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package fr.acinq.eclair.crypto + +import fr.acinq.eclair.TestUtils.randomTxId +import fr.acinq.eclair.randomKey +import org.scalatest.funsuite.AnyFunSuite + +class NonceGeneratorSpec extends AnyFunSuite { + + test("generate deterministic commitment verification nonces") { + val fundingTxId1 = randomTxId() + val fundingKey1 = randomKey() + val remoteFundingKey1 = randomKey().publicKey + val fundingTxId2 = randomTxId() + val fundingKey2 = randomKey() + val remoteFundingKey2 = randomKey().publicKey + // The verification nonce changes for each commitment. + val nonces1 = (0 until 15).map(commitIndex => NonceGenerator.verificationNonce(fundingTxId1, fundingKey1, remoteFundingKey1, commitIndex)) + assert(nonces1.toSet.size == 15) + // We can re-compute verification nonces deterministically. + (0 until 15).foreach(i => assert(nonces1(i) == NonceGenerator.verificationNonce(fundingTxId1, fundingKey1, remoteFundingKey1, i))) + // Nonces for different splices are different. + val nonces2 = (0 until 15).map(commitIndex => NonceGenerator.verificationNonce(fundingTxId2, fundingKey2, remoteFundingKey2, commitIndex)) + assert((nonces1 ++ nonces2).toSet.size == 30) + // Changing any of the parameters changes the nonce value. + assert(!nonces1.contains(NonceGenerator.verificationNonce(fundingTxId2, fundingKey1, remoteFundingKey1, 3))) + assert(!nonces1.contains(NonceGenerator.verificationNonce(fundingTxId1, fundingKey2, remoteFundingKey1, 11))) + assert(!nonces1.contains(NonceGenerator.verificationNonce(fundingTxId1, fundingKey1, remoteFundingKey2, 7))) + } + + test("generate random signing nonces") { + val fundingTxId = randomTxId() + val localFundingKey = randomKey().publicKey + val remoteFundingKey = randomKey().publicKey + // Signing nonces are random and different every time, even if the parameters are the same. + val nonce1 = NonceGenerator.signingNonce(localFundingKey, remoteFundingKey, fundingTxId) + val nonce2 = NonceGenerator.signingNonce(localFundingKey, remoteFundingKey, fundingTxId) + assert(nonce1 != nonce2) + val nonce3 = NonceGenerator.signingNonce(localFundingKey, remoteFundingKey, randomTxId()) + assert(nonce3 != nonce1) + assert(nonce3 != nonce2) + } + +} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala index 23dc635138..c4af4341ee 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala @@ -18,13 +18,15 @@ package fr.acinq.eclair.crypto import fr.acinq.bitcoin.scalacompat.ByteVector32 import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} +import fr.acinq.eclair.crypto.Sphinx.FailurePacket import fr.acinq.eclair.crypto.Sphinx.RouteBlinding.{BlindedRoute, BlindedRouteDetails} import fr.acinq.eclair.wire.protocol import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{BlockHeight, CltvExpiry, CltvExpiryDelta, EncodedNodeId, MilliSatoshiLong, ShortChannelId, UInt64, randomKey} +import fr.acinq.eclair.{BlockHeight, CltvExpiry, CltvExpiryDelta, EncodedNodeId, MilliSatoshiLong, ShortChannelId, UInt64, randomBytes, randomKey} import org.scalatest.funsuite.AnyFunSuite import scodec.bits._ +import scala.concurrent.duration.DurationInt import scala.util.Success /** @@ -226,22 +228,22 @@ class SphinxSpec extends AnyFunSuite { val expected = DecryptedFailurePacket(publicKeys.head, InvalidOnionKey(ByteVector32.One)) - val packet1 = FailurePacket.create(sharedSecrets.head, expected.failureMessage) + val packet1 = createAndWrap(sharedSecrets.head, expected.failureMessage) assert(packet1.length == 292) - val Right(decrypted1) = FailurePacket.decrypt(packet1, Seq(0).map(i => SharedSecret(sharedSecrets(i), publicKeys(i)))) + val Right(decrypted1) = FailurePacket.decrypt(packet1, None, Seq(0).map(i => SharedSecret(sharedSecrets(i), publicKeys(i)))).failure assert(decrypted1 == expected) val packet2 = FailurePacket.wrap(packet1, sharedSecrets(1)) assert(packet2.length == 292) - val Right(decrypted2) = FailurePacket.decrypt(packet2, Seq(1, 0).map(i => SharedSecret(sharedSecrets(i), publicKeys(i)))) + val Right(decrypted2) = FailurePacket.decrypt(packet2, None, Seq(1, 0).map(i => SharedSecret(sharedSecrets(i), publicKeys(i)))).failure assert(decrypted2 == expected) val packet3 = FailurePacket.wrap(packet2, sharedSecrets(2)) assert(packet3.length == 292) - val Right(decrypted3) = FailurePacket.decrypt(packet3, Seq(2, 1, 0).map(i => SharedSecret(sharedSecrets(i), publicKeys(i)))) + val Right(decrypted3) = FailurePacket.decrypt(packet3, None, Seq(2, 1, 0).map(i => SharedSecret(sharedSecrets(i), publicKeys(i)))).failure assert(decrypted3 == expected) } @@ -254,11 +256,11 @@ class SphinxSpec extends AnyFunSuite { val packet = FailurePacket.wrap( FailurePacket.wrap( - FailurePacket.create(sharedSecrets.head, InvalidOnionPayload(UInt64(0), 0)), + createAndWrap(sharedSecrets.head, InvalidOnionPayload(UInt64(0), 0)), sharedSecrets(1)), sharedSecrets(2)) - assert(FailurePacket.decrypt(packet, Seq(0, 2, 1).map(i => SharedSecret(sharedSecrets(i), publicKeys(i)))).isLeft) + assert(FailurePacket.decrypt(packet, None, Seq(0, 2, 1).map(i => SharedSecret(sharedSecrets(i), publicKeys(i)))).failure.isLeft) } test("last node replies with a short failure message (old reference test vector)") { @@ -274,7 +276,7 @@ class SphinxSpec extends AnyFunSuite { assert(lastPacket.isLastPacket) // node #4 want to reply with an error message - val error = FailurePacket.create(sharedSecret4, TemporaryNodeFailure()) + val error = createAndWrap(sharedSecret4, TemporaryNodeFailure()) assert(error == hex"a5e6bd0c74cb347f10cce367f949098f2457d14c046fd8a22cb96efb30b0fdcda8cb9168b50f2fd45edd73c1b0c8b33002df376801ff58aaa94000bf8a86f92620f343baef38a580102395ae3abf9128d1047a0736ff9b83d456740ebbb4aeb3aa9737f18fb4afb4aa074fb26c4d702f42968888550a3bded8c05247e045b866baef0499f079fdaeef6538f31d44deafffdfd3afa2fb4ca9082b8f1c465371a9894dd8c243fb4847e004f5256b3e90e2edde4c9fb3082ddfe4d1e734cacd96ef0706bf63c9984e22dc98851bcccd1c3494351feb458c9c6af41c0044bea3c47552b1d992ae542b17a2d0bba1a096c78d169034ecb55b6e3a7263c26017f033031228833c1daefc0dedb8cf7c3e37c9c37ebfe42f3225c326e8bcfd338804c145b16e34e4") val error1 = FailurePacket.wrap(error, sharedSecret3) assert(error1 == hex"c49a1ce81680f78f5f2000cda36268de34a3f0a0662f55b4e837c83a8773c22aa081bab1616a0011585323930fa5b9fae0c85770a2279ff59ec427ad1bbff9001c0cd1497004bd2a0f68b50704cf6d6a4bf3c8b6a0833399a24b3456961ba00736785112594f65b6b2d44d9f5ea4e49b5e1ec2af978cbe31c67114440ac51a62081df0ed46d4a3df295da0b0fe25c0115019f03f15ec86fabb4c852f83449e812f141a9395b3f70b766ebbd4ec2fae2b6955bd8f32684c15abfe8fd3a6261e52650e8807a92158d9f1463261a925e4bfba44bd20b166d532f0017185c3a6ac7957adefe45559e3072c8dc35abeba835a8cb01a71a15c736911126f27d46a36168ca5ef7dccd4e2886212602b181463e0dd30185c96348f9743a02aca8ec27c0b90dca270") @@ -286,7 +288,7 @@ class SphinxSpec extends AnyFunSuite { assert(error4 == hex"9c5add3963fc7f6ed7f148623c84134b5647e1306419dbe2174e523fa9e2fbed3a06a19f899145610741c83ad40b7712aefaddec8c6baf7325d92ea4ca4d1df8bce517f7e54554608bf2bd8071a4f52a7a2f7ffbb1413edad81eeea5785aa9d990f2865dc23b4bc3c301a94eec4eabebca66be5cf638f693ec256aec514620cc28ee4a94bd9565bc4d4962b9d3641d4278fb319ed2b84de5b665f307a2db0f7fbb757366067d88c50f7e829138fde4f78d39b5b5802f1b92a8a820865af5cc79f9f30bc3f461c66af95d13e5e1f0381c184572a91dee1c849048a647a1158cf884064deddbf1b0b88dfe2f791428d0ba0f6fb2f04e14081f69165ae66d9297c118f0907705c9c4954a199bae0bb96fad763d690e7daa6cfda59ba7f2c8d11448b604d12d") // origin parses error packet and can see that it comes from node #4 - val Right(DecryptedFailurePacket(pubkey, failure)) = FailurePacket.decrypt(error4, sharedSecrets) + val Right(DecryptedFailurePacket(pubkey, failure)) = FailurePacket.decrypt(error4, None, sharedSecrets).failure assert(pubkey == publicKeys(4)) assert(failure == TemporaryNodeFailure()) } @@ -306,24 +308,128 @@ class SphinxSpec extends AnyFunSuite { // node #4 want to reply with an error message val failure = IncorrectOrUnknownPaymentDetails(100 msat, BlockHeight(800000), TlvStream(Set.empty[FailureMessageTlv], Set(GenericTlv(UInt64(34001), ByteVector.fill(300)(128))))) - val error = createCustomLengthFailurePacket(failure, sharedSecret4, 1024) + val failurePacket = createCustomLengthFailurePacket(failure, sharedSecret4, 1024) + val error = Sphinx.FailurePacket.wrap(failurePacket, sharedSecret4) assert(error == hex"146e94a9086dbbed6a0ab6932d00c118a7195dbf69b7d7a12b0e6956fc54b5e0a989f165b5f12fd45edd73a5b0c48630ff5be69500d3d82a29c0803f0a0679a6a073c33a6fb8250090a3152eba3f11a85184fa87b67f1b0354d6f48e3b342e332a17b7710f342f342a87cf32eccdf0afc2160808d58abb5e5840d2c760c538e63a6f841970f97d2e6fe5b8739dc45e2f7f5f532f227bcc2988ab0f9cc6d3f12909cd5842c37bc8c7608475a5ebbe10626d5ecc1f3388ad5f645167b44a4d166f87863fe34918cea25c18059b4c4d9cb414b59f6bc50c1cea749c80c43e2344f5d23159122ed4ab9722503b212016470d9610b46c35dbeebaf2e342e09770b38392a803bc9d2e7c8d6d384ffcbeb74943fe3f64afb2a543a6683c7db3088441c531eeb4647518cb41992f8954f1269fb969630944928c2d2b45593731b5da0c4e70d04a0a57afe4af42e99912fbb4f8883a5ecb9cb29b883cb6bfa0f4db2279ff8c6d2b56a232f55ba28fe7dfa70a9ab0433a085388f25cce8d53de6a2fbd7546377d6ede9027ad173ba1f95767461a3689ef405ab608a21086165c64b02c1782b04a6dba2361a7784603069124e12f2f6dcb1ec7612a4fbf94c0e14631a2bef6190c3d5f35e0c4b32aa85201f449d830fd8f782ec758b0910428e3ec3ca1dba3b6c7d89f69e1ee1b9df3dfbbf6d361e1463886b38d52e8f43b73a3bd48c6f36f5897f514b93364a31d49d1d506340b1315883d425cb36f4ea553430d538fd6f3596d4afc518db2f317dd051abc0d4bfb0a7870c3db70f19fe78d6604bbf088fcb4613f54e67b038277fedcd9680eb97bdffc3be1ab2cbcbafd625b8a7ac34d8c190f98d3064ecd3b95b8895157c6a37f31ef4de094b2cb9dbf8ff1f419ba0ecacb1bb13df0253b826bec2ccca1e745dd3b3e7cc6277ce284d649e7b8285727735ff4ef6cca6c18e2714f4e2a1ac67b25213d3bb49763b3b94e7ebf72507b71fb2fe0329666477ee7cb7ebd6b88ad5add8b217188b1ca0fa13de1ec09cc674346875105be6e0e0d6c8928eb0df23c39a639e04e4aedf535c4e093f08b2c905a14f25c0c0fe47a5a1535ab9eae0d9d67bdd79de13a08d59ee05385c7ea4af1ad3248e61dd22f8990e9e99897d653dd7b1b1433a6d464ea9f74e377f2d8ce99ba7dbc753297644234d25ecb5bd528e2e2082824681299ac30c05354baaa9c3967d86d7c07736f87fc0f63e5036d47235d7ae12178ced3ae36ee5919c093a02579e4fc9edad2c446c656c790704bfc8e2c491a42500aa1d75c8d4921ce29b753f883e17c79b09ea324f1f32ddf1f3284cd70e847b09d90f6718c42e5c94484cc9cbb0df659d255630a3f5a27e7d5dd14fa6b974d1719aa98f01a20fb4b7b1c77b42d57fab3c724339d459ee4a1c6b5d3bd4e08624c786a257872acc9ad3ff62222f2265a658d9f2a007229a5293b67ec91c84c4b4407c228434bad8a815ca9b256c776bd2c9f") + val attribution = Attribution.create(None, Some(failurePacket), 100 millisecond, sharedSecret4) + assert(attribution == hex"d77d0711b5f71d1d1be56bd88b3bb7ebc1792bb739ea7ebc1bc3b031b8bc2df3a50e25aeb99f47d7f7ab39e24187d3f4df9c4333463b053832ee9ac07274a5261b8b2a01fc09ce9ea7cd04d7b585dfb8cf5958e3f3f2a4365d1ec0df1d83c6a6221b5b7d1ff30156a2289a1d3ee559e7c7256bda444bb8e046f860e00b3a59a85e1e1a43de215fd5e6bf646a5deab97b1912c934e31b1cfd344764d6ca7e14ea7b3f2a951aba907c964c0f5d19a44e6d1d7279637321fa598adde927b3087d238f8b426ecde500d318617cdb7a56e6ce3520fc95be41a549973764e4dc483853ecc313947709f1b5199cb077d46e701fa633e11d3e13b03e9212c115ca6fa004b2f3dd912814693b705a561a06da54cdf603677a3abecdc22c7358c2de3cef771b366a568150aeecc86ad1990bb0f4e2865933b03ea0df87901bff467908273dc6cea31cbab0e2b8d398d10b001058c259ed221b7b55762f4c7e49c8c11a45a107b7a2c605c26dc5b0b10d719b1c844670102b2b6a36c43fe4753a78a483fc39166ae28420f112d50c10ee64ca69569a2f690712905236b7c2cb7ac8954f02922d2d918c56d42649261593c47b14b324a65038c3c5be8d3c403ce0c8f19299b1664bf077d7cf1636c4fb9685a8e58b7029fd0939fa07925a60bed339b23f973293598f595e75c8f9d455d7cebe4b5e23357c8bd47d66d6628b39427e37e0aecbabf46c11be6771f7136e108a143ae9bafba0fc47a51b6c7deef4cba54bae906398ee3162a41f2191ca386b628bde7e1dd63d1611aa01a95c456df337c763cb8c3a81a6013aa633739d8cd554c688102211725e6adad165adc1bcd429d020c51b4b25d2117e8bb27eb0cc7020f9070d4ad19ac31a76ebdf5f9246646aeadbfb9a3f1d75bd8237961e786302516a1a781780e8b73f58dc06f307e58bd0eb1d8f5c9111f01312974c1dc777a6a2d3834d8a2a40014e9818d0685cb3919f6b3b788ddc640b0ff9b1854d7098c7dd6f35196e902b26709640bc87935a3914869a807e8339281e9cedaaca99474c3e7bdd35050bb998ab4546f9900904e0e39135e861ff7862049269701081ebce32e4cca992c6967ff0fd239e38233eaf614af31e186635e9439ec5884d798f9174da6ff569d68ed5c092b78bd3f880f5e88a7a8ab36789e1b57b035fb6c32a6358f51f83e4e5f46220bcad072943df8bd9541a61b7dae8f30fa3dd5fb39b1fd9a0b8e802552b78d4ec306ecee15bfe6da14b29ba6d19ce5be4dd478bca74a52429cd5309d404655c3dec85c252") val error1 = FailurePacket.wrap(error, sharedSecret3) assert(error1 == hex"7512354d6a26781d25e65539772ba049b7ed7c530bf75ab7ef80cf974b978a07a1c3dabc61940011585323f70fa98cfa1d4c868da30b1f751e44a72d9b3f79809c8c51c9f0843daa8fe83587844fedeacb7348362003b31922cbb4d6169b2087b6f8d192d9cfe5363254cd1fde24641bde9e422f170c3eb146f194c48a459ae2889d706dc654235fa9dd20307ea54091d09970bf956c067a3bcc05af03c41e01af949a131533778bf6ee3b546caf2eabe9d53d0fb2e8cc952b7e0f5326a69ed2e58e088729a1d85971c6b2e129a5643f3ac43da031e655b27081f10543262cf9d72d6f64d5d96387ac0d43da3e3a03da0c309af121dcf3e99192efa754eab6960c256ffd4c546208e292e0ab9894e3605db098dc16b40f17c320aa4a0e42fc8b105c22f08c9bc6537182c24e32062c6cd6d7ec7062a0c2c2ecdae1588c82185cdc61d874ee916a7873ac54cddf929354f307e870011704a0e9fbc5c7802d6140134028aca0e78a7e2f3d9e5c7e49e20c3a56b624bfea51196ec9e88e4e56be38ff56031369f45f1e03be826d44a182f270c153ee0d9f8cf9f1f4132f33974e37c7887d5b857365c873cb218cbf20d4be3abdb2a2011b14add0a5672e01e5845421cf6dd6faca1f2f443757aae575c53ab797c2227ecdab03882bbbf4599318cefafa72fa0c9a0f5a51d13c9d0e5d25bfcfb0154ed25895260a9df8743ac188714a3f16960e6e2ff663c08bffda41743d50960ea2f28cda0bc3bd4a180e297b5b41c700b674cb31d99c7f2a1445e121e772984abff2bbe3f42d757ceeda3d03fb1ffe710aecabda21d738b1f4620e757e57b123dbc3c4aa5d9617dfa72f4a12d788ca596af14bea583f502f16fdc13a5e739afb0715424af2767049f6b9aa107f69c5da0e85f6d8c5e46507e14616d5d0b797c3dea8b74a1b12d4e47ba7f57f09d515f6c7314543f78b5e85329d50c5f96ee2f55bbe0df742b4003b24ccbd4598a64413ee4807dc7f2a9c0b92424e4ae1b418a3cdf02ea4da5c3b12139348aa7022cc8272a3a1714ee3e4ae111cffd1bdfd62c503c80bdf27b2feaea0d5ab8fe00f9cec66e570b00fd24b4a2ed9a5f6384f148a4d6325110a41ca5659ebc5b98721d298a52819b6fb150f273383f1c5754d320be428941922da790e17f482989c365c078f7f3ae100965e1b38c052041165295157e1a7c5b7a57671b842d4d85a7d971323ad1f45e17a16c4656d889fc75c12fc3d8033f598306196e29571e414281c5da19c12605f48347ad5b4648e371757cbe1c40adb93052af1d6110cfbf611af5c8fc682b7e2ade3bfca8b5c7717d19fc9f97964ba6025aebbc91a6671e259949dcf40984342118de1f6b514a7786bd4f6598ffbe1604cef476b2a4cb1343db608aca09d1d38fc23e98ee9c65e7f6023a8d1e61fd4f34f753454bd8e858c8ad6be6403edc599c220e03ca917db765980ac781e758179cd93983e9c1e769e4241d47c") + val attribution1 = Attribution.create(Some(attribution), Some(error), 200 milliseconds, sharedSecret3) + assert(attribution1 == hex"1571e10db7f8aa9f8e7e99caaf9c892e106c817df1d8e3b7b0e39d1c48f631e473e17e205489dd7b3c634cac3be0825cbf01418cd46e83c24b8d9c207742db9a0f0e5bcd888086498159f08080ba7bf36dee297079eb841391ccd3096da76461e314863b6412efe0ffe228d51c6097db10d3edb2e50ea679820613bfe9db11ba02920ab4c1f2a79890d997f1fc022f3ab78f0029cc6de0c90be74d55f4a99bf77a50e20f8d076fe61776190a61d2f41c408871c0279309cba3b60fcdc7efc4a0e90b47cb4a418fc78f362ecc7f15ebbce9f854c09c7be300ebc1a40a69d4c7cb7a19779b6905e82bec221a709c1dab8cbdcde7b527aca3f54bde651aa9f3f2178829cee3f1c0b9292758a40cc63bd998fcd0d3ed4bdcaf1023267b8f8e44130a63ad15f76145936552381eabb6d684c0a3af6ba8efcf207cebaea5b7acdbb63f8e7221102409d10c23f0514dc9f4d0efb2264161a193a999a23e992632710580a0d320f676d367b9190721194514457761af05207cdab2b6328b1b3767eacb36a7ef4f7bd2e16762d13df188e0898b7410f62459458712a44bf594ae662fd89eb300abb6952ff8ad40164f2bcd7f86db5c7650b654b79046de55d51aa8061ce35f867a3e8f5bf98ad920be827101c64fb871d86e53a4b3c0455bfac5784168218aa72cbee86d9c750a9fa63c363a8b43d7bf4b2762516706a306f0aa3be1ec788b5e13f8b24837e53ac414f211e11c7a093cd9653dfa5fba4e377c79adfa5e841e2ddb6afc054fc715c05ddc6c8fc3e1ee3406e1ffceb2df77dc2f02652614d1bfcfaddebaa53ba919c7051034e2c7b7cfaabdf89f26e7f8e3f956d205dfab747ad0cb505b85b54a68439621b25832cbc2898919d0cd7c0a64cfd235388982dd4dd68240cb668f57e1d2619a656ed326f8c92357ee0d9acead3c20008bc5f04ca8059b55d77861c6d04dfc57cfba57315075acbe1451c96cf28e1e328e142890248d18f53b5d3513ce574dea7156cf596fdb3d909095ec287651f9cf1bcdc791c5938a5dd9b47e84c004d24ab3ae74492c7e8dcc1da15f65324be2672947ec82074cac8ce2b925bc555facbbf1b55d63ea6fbea6a785c97d4caf2e1dad9551b7f66c31caae5ebc7c0047e892f201308fcf452c588be0e63d89152113d87bf0dbd01603b4cdc7f0b724b0714a9851887a01f709408882e18230fe810b9fafa58a666654576d8eba3005f07221f55a6193815a672e5db56204053bc4286fa3db38250396309fd28011b5708a26a2d76c4a333b69b6bfd272fb") val error2 = FailurePacket.wrap(error1, sharedSecret2) assert(error2 == hex"145bc1c63058f7204abbd2320d422e69fb1b3801a14312f81e5e29e6b5f4774cfed8a25241d3dfb7466e749c1b3261559e49090853612e07bd669dfb5f4c54162fa504138dabd6ebcf0db8017840c35f12a2cfb84f89cc7c8959a6d51815b1d2c5136cedec2e4106bb5f2af9a21bd0a02c40b44ded6e6a90a145850614fb1b0eef2a03389f3f2693bc8a755630fc81fff1d87a147052863a71ad5aebe8770537f333e07d841761ec448257f948540d8f26b1d5b66f86e073746106dfdbb86ac9475acf59d95ece037fba360670d924dce53aaa74262711e62a8fc9eb70cd8618fbedae22853d3053c7f10b1a6f75369d7f73c419baa7dbf9f1fc5895362dcc8b6bd60cca4943ef7143956c91992119bccbe1666a20b7de8a2ff30a46112b53a6bb79b763903ecbd1f1f74952fb1d8eb0950c504df31fe702679c23b463f82a921a2c931500ab08e686cffb2d87258d254fb17843959cccd265a57ba26c740f0f231bb76df932b50c12c10be90174b37d454a3f8b284c849e86578a6182c4a7b2e47dd57d44730a1be9fec4ad07287a397e28dce4fda57e9cdfdb2eb5afdf0d38ef19d982341d18d07a556bb16c1416f480a396f278373b8fd9897023a4ac506e65cf4c306377730f9c8ca63cf47565240b59c4861e52f1dab84d938e96fb31820064d534aca05fd3d2600834fe4caea98f2a748eb8f200af77bd9fbf46141952b9ddda66ef0ebea17ea1e7bb5bce65b6e71554c56dd0d4e14f4cf74c77a150776bf31e7419756c71e7421dc22efe9cf01de9e19fc8808d5b525431b944400db121a77994518d6025711cb25a18774068bba7faaa16d8f65c91bec8768848333156dcb4a08dfbbd9fef392da3e4de13d4d74e83a7d6e46cfe530ee7a6f711e2caf8ad5461ba8177b2ef0a518baf9058ff9156e6aa7b08d938bd8d1485a787809d7b4c8aed97be880708470cd2b2cdf8e2f13428cc4b04ef1f2acbc9562f3693b948d0aa94b0e6113cafa684f8e4a67dc431dfb835726874bef1de36f273f52ee694ec46b0700f77f8538067642a552968e866a72a3f2031ad116663ac17b172b446c5bc705b84777363a9a3fdc6443c07b2f4ef58858122168d4ebbaee920cefc312e1cea870ed6e15eec046ab2073bbf08b0a3366f55cfc6ad4681a12ab0946534e7b6f90ea8992d530ec3daa6b523b3cf03101c60cadd914f30dec932c1ef4341b5a8efac3c921e203574cfe0f1f83433fddb8ccfd273f7c3cab7bc27efe3bb61fdccd5146f1185364b9b621e7fb2b74b51f5ee6be72ab6ff46a6359dc2c855e61469724c1dbeb273df9d2e1c1fb74891239c0019dc12d5c7535f7238f963b761d7102b585372cf021b64c4fc85bfb3161e59d2e298bba44cfd34d6859d9dba9dc6271e5047d525468c814f2ae438474b0a977273036da1a2292f88fcfb89574a6bdca1185b40f8aa54026d5926725f99ef028da1be892e3586361efe15f4a148ff1bc9") + val attribution2 = Attribution.create(Some(attribution1), Some(error1), 300 milliseconds, sharedSecret2) + assert(attribution2 == hex"34e34397b8621ec2f2b54dbe6c14073e267324cd60b152bce76aec8729a6ddefb61bc263be4b57bd592aae604a32bea69afe6ef4a6b573c26b17d69381ec1fc9b5aa769d148f2f1f8b5377a73840bb6dc641f68e356323d766fff0aaca5039fe7fc27038195844951a97d5a5b26698a4ca1e9cd4bca1fcca0aac5fee91b18977d2ad0e399ba159733fc98f6e96898ebc39bf0028c9c81619233bab6fad0328aa183a635fac20437fa6e00e899b2527c3697a8ab7342e42d55a679b176ab76671fcd480a9894cb897fa6af0a45b917a162bed6c491972403185df7235502f7ada65769d1bfb12d29f10e25b0d3cc08bbf6de8481ac5c04df32b4533b4f764c2aefb7333202645a629fb16e4a208e9045dc36830759c852b31dd613d8b2b10bbead1ed4eb60c85e8a4517deba5ab53e39867c83c26802beee2ee545bdd713208751added5fc0eb2bc89a5aa2decb18ee37dac39f22a33b60cc1a369d24de9f3d2d8b63c039e248806de4e36a47c7a0aed30edd30c3d62debdf1ad82bf7aedd7edec413850d91c261e12beec7ad1586a9ad25b2db62c58ca17119d61dcc4f3e5c4520c42a8e384a45d8659b338b3a08f9e123a1d3781f5fc97564ccff2c1d97f06fa0150cfa1e20eacabefb0c339ec109336d207cc63d9170752fc58314c43e6d4a528fd0975afa85f3aa186ff1b6b8cb12c97ed4ace295b0ef5f075f0217665b8bb180246b87982d10f43c9866b22878106f5214e99188781180478b07764a5e12876ddcb709e0a0a8dd42cf004c695c6fc1669a6fd0e4a1ca54b024d0d80eac492a9e5036501f36fb25b72a054189294955830e43c18e55668337c8c6733abb09fc2d4ade18d5a853a2b82f7b4d77151a64985004f1d9218f2945b63c56fdebd1e96a2a7e49fa70acb4c39873947b83c191c10e9a8f40f60f3ad5a2be47145c22ea59ed3f5f4e61cb069e875fb67142d281d784bf925cc286eacc2c43e94d08da4924b83e58dbf2e43fa625bdd620eba6d9ce960ff17d14ed1f2dbee7d08eceb540fdc75ff06dabc767267658fad8ce99e2a3236e46d2deedcb51c3c6f81589357edebac9772a70b3d910d83cd1b9ce6534a011e9fa557b891a23b5d88afcc0d9856c6dabeab25eea55e9a248182229e4927f268fe5431672fcce52f434ca3d27d1a2136bae5770bb36920df12fbc01d0e8165610efa04794f414c1417f1d4059435c5385bfe2de83ce0e238d6fd2dbd3c0487c69843298577bfa480fe2a16ab2a0e4bc712cd8b5a14871cda61c993b6835303d9043d7689a") val error3 = FailurePacket.wrap(error2, sharedSecret1) assert(error3 == hex"1b4b09a935ce7af95b336baae307f2b400e3a7e808d9b4cf421cc4b3955620acb69dcdb656128dae8857adbd4e6b37fbb1be9c1f2f02e61e9e59a630c4c77cf383cb37b07413aa4de2f2fbf5b40ae40a91a8f4c6d74aeacef1bb1be4ecbc26ec2c824d2bc45db4b9098e732a769788f1cff3f5b41b0d25c132d40dc5ad045ef0043b15332ca3c5a09de2cdb17455a0f82a8f20da08346282823dab062cdbd2111e238528141d69de13de6d83994fbc711e3e269df63a12d3a4177c5c149150eb4dc2f589cd8acabcddba14dec3b0dada12d663b36176cd3c257c5460bab93981ad99f58660efa9b31d7e63b39915329695b3fa60e0a3bdb93e7e29a54ca6a8f360d3848866198f9c3da3ba958e7730847fe1e6478ce8597848d3412b4ae48b06e05ba9a104e648f6eaf183226b5f63ed2e68f77f7e38711b393766a6fab7921b03eba82b5d7cb78e34dc961948d6161eadd7cf5d95d9c56df2ff5faa6ccf85eacdc9ff2fc3abafe41c365a5bd14fd486d6b5e2f24199319e7813e02e798877ffe31a70ae2398d9e31b9e3727e6c1a3c0d995c67d37bb6e72e9660aaaa9232670f382add2edd468927e3303b6142672546997fe105583e7c5a3c4c2b599731308b5416e6c9a3f3ba55b181ad0439d3535356108b059f2cb8742eed7a58d4eba9fe79eaa77c34b12aff1abdaea93197aabd0e74cb271269ca464b3b06aef1d6573df5e1224179616036b368677f26479376681b772d3760e871d99efd34cca5cd6beca95190d967da820b21e5bec60082ea46d776b0517488c84f26d12873912d1f68fafd67bcf4c298e43cfa754959780682a2db0f75f95f0598c0d04fd014c50e4beb86a9e37d95f2bba7e5065ae052dc306555bca203d104c44a538b438c9762de299e1c4ad30d5b4a6460a76484661fc907682af202cd69b9a4473813b2fdc1142f1403a49b7e69a650b7cde9ff133997dcc6d43f049ecac5fce097a21e2bce49c810346426585e3a5a18569b4cddd5ff6bdec66d0b69fcbc5ab3b137b34cc8aefb8b850a764df0e685c81c326611d901c392a519866e132bbb73234f6a358ba284fbafb21aa3605cacbaf9d0c901390a98b7a7dac9d4f0b405f7291c88b2ff45874241c90ac6c5fc895a440453c344d3a365cb929f9c91b9e39cb98b142444aae03a6ae8284c77eb04b0a163813d4c21883df3c0f398f47bf127b5525f222107a2d8fe55289f0cfd3f4bbad6c5387b0594ef8a966afc9e804ccaf75fe39f35c6446f7ee076d433f2f8a44dba1515acc78e589fa8c71b0a006fe14feebd51d0e0aa4e51110d16759eee86192eee90b34432130f387e0ccd2ee71023f1f641cddb571c690107e08f592039fe36d81336a421e89378f351e633932a2f5f697d25b620ffb8e84bb6478e9bd229bf3b164b48d754ae97bd23f319e3c56b3bcdaaeb3bd7fc02ec02066b324cb72a09b6b43dec1097f49d69d3c138ce6f1a6402898baf7568c") + val attribution3 = Attribution.create(Some(attribution2), Some(error2), 400 milliseconds, sharedSecret1) + assert(attribution3 == hex"74a4ea61339463642a2182758871b2ea724f31f531aa98d80f1c3043febca41d5ee52e8b1e127e61719a0d078db8909748d57839e58424b91f063c4fbc8a221bef261140e66a9b596ca6d420a973ad54fef30646ae53ccf0855b61f291a81e0ec6dc0f6bf69f0ca0e5889b7e23f577ba67d2a7d6a2aa91264ab9b20630ed52f8ed56cc10a869807cd1a4c2cd802d8433fee5685d6a04edb0bff248a480b93b01904bed3bb31705d1ecb7332004290cc0cd9cc2f7907cf9db28eec02985301668f53fbc28c3e095c8f3a6cd8cab28e5e442fd9ba608b8b12e098731bbfda755393bd403c62289093b40390b2bae337fc87d2606ca028311d73a9ffbdffef56020c735ada30f54e577c6a9ec515ae2739290609503404b118d7494499ecf0457d75015bb60a16288a4959d74cf5ac5d8d6c113de39f748a418d2a7083b90c9c0a09a49149fd1f2d2cde4412e5aa2421eca6fd4f6fe6b2c362ff37d1a0608c931c7ca3b8fefcfd4c44ef9c38357a0767b14f83cb49bd1989fb3f8e2ab202ac98bd8439790764a40bf309ea2205c1632610956495720030a25dc7118e0c868fdfa78c3e9ecce58215579a0581b3bafdb7dbbe53be9e904567fdc0ce1236aab5d22f1ebc18997e3ea83d362d891e04c5785fd5238326f767bce499209f8db211a50e1402160486e98e7235cf397dbb9ae19fd9b79ef589c821c6f99f28be33452405a003b33f4540fe0a41dfcc286f4d7cc10b70552ba7850869abadcd4bb7f256823face853633d6e2a999ac9fcd259c71d08e266db5d744e1909a62c0db673745ad9585949d108ab96640d2bc27fb4acac7fa8b170a30055a5ede90e004df9a44bdc29aeb4a6bec1e85dde1de6aaf01c6a5d12405d0bec22f49026cb23264f8c04b8401d3c2ab6f2e109948b6193b3bec27adfe19fb8afb8a92364d6fc5b219e8737d583e7ff3a4bcb75d53edda3bf3f52896ac36d8a877ad9f296ea6c045603fc62ac4ae41272bde85ef7c3b3fd3538aacfd5b025fefbe277c2906821ecb20e6f75ea479fa3280f9100fb0089203455c56b6bc775e5c2f0f58c63edd63fa3eec0b40da4b276d0d41da2ec0ead865a98d12bc694e23d8eaadd2b4d0ee88e9570c88fb878930f492e036d27998d593e47763927ff7eb80b188864a3846dd2238f7f95f4090ed399ae95deaeb37abca1cf37c397cc12189affb42dca46b4ff6988eb8c060691d155302d448f50ff70a794d97c0408f8cee9385d6a71fa412e36edcb22dbf433db9db4779f27b682ee17fc05e70c8e794b9f7f6d1") val error4 = FailurePacket.wrap(error3, sharedSecret0) assert(error4 == hex"2dd2f49c1f5af0fcad371d96e8cddbdcd5096dc309c1d4e110f955926506b3c03b44c192896f45610741c85ed4074212537e0c118d472ff3a559ae244acd9d783c65977765c5d4e00b723d00f12475aafaafff7b31c1be5a589e6e25f8da2959107206dd42bbcb43438129ce6cce2b6b4ae63edc76b876136ca5ea6cd1c6a04ca86eca143d15e53ccdc9e23953e49dc2f87bb11e5238cd6536e57387225b8fff3bf5f3e686fd08458ffe0211b87d64770db9353500af9b122828a006da754cf979738b4374e146ea79dd93656170b89c98c5f2299d6e9c0410c826c721950c780486cd6d5b7130380d7eaff994a8503a8fef3270ce94889fe996da66ed121741987010f785494415ca991b2e8b39ef2df6bde98efd2aec7d251b2772485194c8368451ad49c2354f9d30d95367bde316fec6cbdddc7dc0d25e99d3075e13d3de0822669861dafcd29de74eac48b64411987285491f98d78584d0c2a163b7221ea796f9e8671b2bb91e38ef5e18aaf32c6c02f2fb690358872a1ed28166172631a82c2568d23238017188ebbd48944a147f6cdb3690d5f88e51371cb70adf1fa02afe4ed8b581afc8bcc5104922843a55d52acde09bc9d2b71a663e178788280f3c3eae127d21b0b95777976b3eb17be40a702c244d0e5f833ff49dae6403ff44b131e66df8b88e33ab0a58e379f2c34bf5113c66b9ea8241fc7aa2b1fa53cf4ed3cdd91d407730c66fb039ef3a36d4050dde37d34e80bcfe02a48a6b14ae28227b1627b5ad07608a7763a531f2ffc96dff850e8c583461831b19feffc783bc1beab6301f647e9617d14c92c4b1d63f5147ccda56a35df8ca4806b8884c4aa3c3cc6a174fdc2232404822569c01aba686c1df5eecc059ba97e9688c8b16b70f0d24eacfdba15db1c71f72af1b2af85bd168f0b0800483f115eeccd9b02adf03bdd4a88eab03e43ce342877af2b61f9d3d85497cd1c6b96674f3d4f07f635bb26add1e36835e321d70263b1c04234e222124dad30ffb9f2a138e3ef453442df1af7e566890aedee568093aa922dd62db188aa8361c55503f8e2c2e6ba93de744b55c15260f15ec8e69bb01048ca1fa7bbbd26975bde80930a5b95054688a0ea73af0353cc84b997626a987cc06a517e18f91e02908829d4f4efc011b9867bd9bfe04c5f94e4b9261d30cc39982eb7b250f12aee2a4cce0484ff34eebba89bc6e35bd48d3968e4ca2d77527212017e202141900152f2fd8af0ac3aa456aae13276a13b9b9492a9a636e18244654b3245f07b20eb76b8e1cea8c55e5427f08a63a16b0a633af67c8e48ef8e53519041c9138176eb14b8782c6c2ee76146b8490b97978ee73cd0104e12f483be5a4af414404618e9f6633c55dda6f22252cb793d3d16fae4f0e1431434e7acc8fa2c009d4f6e345ade172313d558a4e61b4377e31b8ed4e28f7cd13a7fe3f72a409bc3bdabfe0ba47a6d861e21f64d2fac706dab18b3e546df4") + val attribution4 = Attribution.create(Some(attribution3), Some(error3), 500 milliseconds, sharedSecret0) + assert(attribution4 == hex"84986c936d26bfd3bb2d34d3ec62cfdb63e0032fdb3d9d75f3e5d456f73dffa7e35aab1db4f1bd3b98ff585caf004f656c51037a3f4e810d275f3f6aea0c8e3a125ebee5f374b6440bcb9bb2955ebf706f42be9999a62ed49c7a81fc73c0b4a16419fd6d334532f40bf179dd19afec21bd8519d5e6ebc3802501ef373bc378eee1f14a6fc5fab5b697c91ce31d5922199d1b0ad5ee12176aacafc7c81d54bc5b8fb7e63f3bfd40a3b6e21f985340cbd1c124c7f85f0369d1aa86ebc66def417107a7861131c8bcd73e8946f4fb54bfac87a2dc15bd7af642f32ae583646141e8875ef81ec9083d7e32d5f135131eab7a43803360434100ff67087762bbe3d6afe2034f5746b8c50e0c3c20dd62a4c174c38b1df7365dccebc7f24f19406649fbf48981448abe5c858bbd4bef6eb983ae7a23e9309fb33b5e7c0522554e88ca04b1d65fc190947dead8c0ccd32932976537d869b5ca53ed4945bccafab2a014ea4cbdc6b0250b25be66ba0afff2ff19c0058c68344fd1b9c472567147525b13b1bc27563e61310110935cf89fda0e34d0575e2389d57bdf2869398ca2965f64a6f04e1d1c2edf2082b97054264a47824dd1a9691c27902b39d57ae4a94dd6481954a9bd1b5cff4ab29ca221fa2bf9b28a362c9661206f896fc7cec563fb80aa5eaccb26c09fa4ef7a981e63028a9c4dac12f82ccb5bea090d56bbb1a4c431e315d9a169299224a8dbd099fb67ea61dfc604edf8a18ee742550b636836bb552dabb28820221bf8546331f32b0c143c1c89310c4fa2e1e0e895ce1a1eb0f43278fdb528131a3e32bfffe0c6de9006418f5309cba773ca38b6ad8507cc59445ccc0257506ebc16a4c01d4cd97e03fcf7a2049fea0db28447858f73b8e9fe98b391b136c9dc510288630a1f0af93b26a8891b857bfe4b818af99a1e011e6dbaa53982d29cf74ae7dffef45545279f19931708ed3eede5e82280eab908e8eb80abff3f1f023ab66869297b40da8496861dc455ac3abe1efa8a6f9e2c4eda48025d43a486a3f26f269743eaa30d6f0e1f48db6287751358a41f5b07aee0f098862e3493731fe2697acce734f004907c6f11eef189424fee52cd30ad708707eaf2e441f52bcf3d0c5440c1742458653c0c8a27b5ade784d9e09c8b47f1671901a29360e7e5e94946b9c75752a1a8d599d2a3e14ac81b84d42115cd688c8383a64fc6e7e1dc5568bb4837358ebe63207a4067af66b2027ad2ce8fb7ae3a452d40723a51fdf9f9c9913e8029a222cf81d12ad41e58860d75deb6de30ad") // origin parses error packet and can see that it comes from node #4 - val Right(DecryptedFailurePacket(pubkey, parsedFailure)) = FailurePacket.decrypt(error4, sharedSecrets) + val HtlcFailure(holdTimes, Right(DecryptedFailurePacket(pubkey, parsedFailure))) = FailurePacket.decrypt(error4, Some(attribution4), sharedSecrets) + assert(holdTimes == Seq(HoldTime(500 millisecond, publicKeys(0)), HoldTime(400 milliseconds, publicKeys(1)), HoldTime(300 milliseconds, publicKeys(2)), HoldTime(200 milliseconds, publicKeys(3)), HoldTime(100 milliseconds, publicKeys(4)))) assert(pubkey == publicKeys(4)) assert(parsedFailure == failure) } } + test("fulfilled HTLC with attribution data (reference test vector)") { + for ((payloads, packetPayloadLength) <- Seq((referencePaymentPayloads, 1300), (paymentPayloadsFull, 1300))) { + // origin build the onion packet + val Success(PacketAndSecrets(packet, sharedSecrets)) = create(sessionKey, packetPayloadLength, publicKeys, payloads, associatedData) + // each node parses and forwards the packet + val Right(DecryptedPacket(_, packet1, sharedSecret0)) = peel(privKeys(0), associatedData, packet) + val Right(DecryptedPacket(_, packet2, sharedSecret1)) = peel(privKeys(1), associatedData, packet1) + val Right(DecryptedPacket(_, packet3, sharedSecret2)) = peel(privKeys(2), associatedData, packet2) + val Right(DecryptedPacket(_, packet4, sharedSecret3)) = peel(privKeys(3), associatedData, packet3) + val Right(lastPacket@DecryptedPacket(_, _, sharedSecret4)) = peel(privKeys(4), associatedData, packet4) + assert(lastPacket.isLastPacket) + + val attribution = Attribution.create(None, None, 100 millisecond, sharedSecret4) + assert(attribution == hex"d77d0711b5f71d1d1be56bd88b3bb7ebc1792bb739ea7ebc1bc3b031b8bc2df3a50e25aeb99f47d7f7ab39e24187d3f4df9c4333463b053832ee9ac07274a5261b8b2a01fc09ce9ea7cd04d7b585dfb83299fb6570d71f793c1fcac0ef498766952c8c6840efa02a567d558a3cf6822b12476324b9b9efa03e5f8f26f81fa93daac46cbf00c98e69b6747cf69caaa2a71b025bd18830c4c54cd08f598cfde6197b3f2a951aba907c964c0f5d19a44e6d1d7279637321fa598adde927b3087d238f8b426ecde500d318617cdb7a56e6ce3520fc95be41a549973764e4dc483853ecc313947709f1b5199cb077d46e701fa633e11d3e13b03e9212c115ca6fa004b2f3dd912814693b705a561a06da54cdf603677a3abecdc22c7358c2de3cef771b366a568150aeecc86ad1990bb0f4e2865933b03ea0df87901bff467908273dc6cea31cbab0e2b8d398d10b001058c259ed221b7b55762f4c7e49c8c11a45a107b7a2c605c26dc5b0b10d719b1c844670102b2b6a36c43fe4753a78a483fc39166ae28420f112d50c10ee64ca69569a2f690712905236b7c2cb7ac8954f02922d2d918c56d42649261593c47b14b324a65038c3c5be8d3c403ce0c8f19299b1664bf077d7cf1636c4fb9685a8e58b7029fd0939fa07925a60bed339b23f973293598f595e75c8f9d455d7cebe4b5e23357c8bd47d66d6628b39427e37e0aecbabf46c11be6771f7136e108a143ae9bafba0fc47a51b6c7deef4cba54bae906398ee3162a41f2191ca386b628bde7e1dd63d1611aa01a95c456df337c763cb8c3a81a6013aa633739d8cd554c688102211725e6adad165adc1bcd429d020c51b4b25d2117e8bb27eb0cc7020f9070d4ad19ac31a76ebdf5f9246646aeadbfb9a3f1d75bd8237961e786302516a1a781780e8b73f58dc06f307e58bd0eb1d8f5c9111f01312974c1dc777a6a2d3834d8a2a40014e9818d0685cb3919f6b3b788ddc640b0ff9b1854d7098c7dd6f35196e902b26709640bc87935a3914869a807e8339281e9cedaaca99474c3e7bdd35050bb998ab4546f9900904e0e39135e861ff7862049269701081ebce32e4cca992c6967ff0fd239e38233eaf614af31e186635e9439ec5884d798f9174da6ff569d68ed5c092b78bd3f880f5e88a7a8ab36789e1b57b035fb6c32a6358f51f83e4e5f46220bcad072943df8bd9541a61b7dae8f30fa3dd5fb39b1fd9a0b8e802552b78d4ec306ecee15bfe6da14b29ba6d19ce5be4dd478bca74a52429cd5309d404655c3dec85c252") + val attribution1 = Attribution.create(Some(attribution), None, 200 milliseconds, sharedSecret3) + assert(attribution1 == hex"1571e10db7f8aa9f8e7e99caaf9c892e106c817df1d8e3b7b0e39d1c48f631e473e17e205489dd7b3c634cac3be0825cbf01418cd46e83c24b8d9c207742db9a0f0e5bcd888086498159f08080ba7bf3ea029c0b493227c4e75a90f70340d9e21f00979fc7e4fb2078477c1a457ba242ed54b313e590b13a2a13bfeed753dab133c78059f460075b2594b4c31c50f31076f8f1a0f7ad0530d0fadaf2d86e505ff9755940ec0665f9e5bc58cad6e523091f94d0bcd3c6c65ca1a5d401128dcc5e14f9108b32e660017c13de598bcf9d403710857cccb0fb9c2a81bfd66bc4552e1132afa3119203a4aaa1e8839c1dab8cbdcde7b527aca3f54bde651aa9f3f2178829cee3f1c0b9292758a40cc63bd998fcd0d3ed4bdcaf1023267b8f8e44130a63ad15f76145936552381eabb6d684c0a3af6ba8efcf207cebaea5b7acdbb63f8e7221102409d10c23f0514dc9f4d0efb2264161a193a999a23e992632710580a0d320f676d367b9190721194514457761af05207cdab2b6328b1b3767eacb36a7ef4f7bd2e16762d13df188e0898b7410f62459458712a44bf594ae662fd89eb300abb6952ff8ad40164f2bcd7f86db5c7650b654b79046de55d51aa8061ce35f867a3e8f5bf98ad920be827101c64fb871d86e53a4b3c0455bfac5784168218aa72cbee86d9c750a9fa63c363a8b43d7bf4b2762516706a306f0aa3be1ec788b5e13f8b24837e53ac414f211e11c7a093cd9653dfa5fba4e377c79adfa5e841e2ddb6afc054fc715c05ddc6c8fc3e1ee3406e1ffceb2df77dc2f02652614d1bfcfaddebaa53ba919c7051034e2c7b7cfaabdf89f26e7f8e3f956d205dfab747ad0cb505b85b54a68439621b25832cbc2898919d0cd7c0a64cfd235388982dd4dd68240cb668f57e1d2619a656ed326f8c92357ee0d9acead3c20008bc5f04ca8059b55d77861c6d04dfc57cfba57315075acbe1451c96cf28e1e328e142890248d18f53b5d3513ce574dea7156cf596fdb3d909095ec287651f9cf1bcdc791c5938a5dd9b47e84c004d24ab3ae74492c7e8dcc1da15f65324be2672947ec82074cac8ce2b925bc555facbbf1b55d63ea6fbea6a785c97d4caf2e1dad9551b7f66c31caae5ebc7c0047e892f201308fcf452c588be0e63d89152113d87bf0dbd01603b4cdc7f0b724b0714a9851887a01f709408882e18230fe810b9fafa58a666654576d8eba3005f07221f55a6193815a672e5db56204053bc4286fa3db38250396309fd28011b5708a26a2d76c4a333b69b6bfd272fb") + val attribution2 = Attribution.create(Some(attribution1), None, 300 milliseconds, sharedSecret2) + assert(attribution2 == hex"34e34397b8621ec2f2b54dbe6c14073e267324cd60b152bce76aec8729a6ddefb61bc263be4b57bd592aae604a32bea69afe6ef4a6b573c26b17d69381ec1fc9b5aa769d148f2f1f8b5377a73840bb6dffc324ded0d1c00dc0c99e3dbc13273b2f89510af6410b525dd8836208abbbaae12753ae2276fa0ca49950374f94e187bf65cefcdd9dd9142074edc4bd0052d0eb027cb1ab6182497f9a10f9fe800b3228e3c088dab60081c807b30a67313667ca8c9e77b38b161a037cae8e973038d0fc4a97ea215914c6c4e23baf6ac4f0fb1e7fcc8aac3f6303658dae1f91588b535eb678e2200f45383c2590a55dc181a09f2209da72f79ae6745992c803310d39f960e8ecf327aed706e4b3e2704eeb9b304dc0e0685f5dcd0389ec377bdba37610ad556a0e957a413a56339dd3c40817214bced5802beee2ee545bdd713208751added5fc0eb2bc89a5aa2decb18ee37dac39f22a33b60cc1a369d24de9f3d2d8b63c039e248806de4e36a47c7a0aed30edd30c3d62debdf1ad82bf7aedd7edec413850d91c261e12beec7ad1586a9ad25b2db62c58ca17119d61dcc4f3e5c4520c42a8e384a45d8659b338b3a08f9e123a1d3781f5fc97564ccff2c1d97f06fa0150cfa1e20eacabefb0c339ec109336d207cc63d9170752fc58314c43e6d4a528fd0975afa85f3aa186ff1b6b8cb12c97ed4ace295b0ef5f075f0217665b8bb180246b87982d10f43c9866b22878106f5214e99188781180478b07764a5e12876ddcb709e0a0a8dd42cf004c695c6fc1669a6fd0e4a1ca54b024d0d80eac492a9e5036501f36fb25b72a054189294955830e43c18e55668337c8c6733abb09fc2d4ade18d5a853a2b82f7b4d77151a64985004f1d9218f2945b63c56fdebd1e96a2a7e49fa70acb4c39873947b83c191c10e9a8f40f60f3ad5a2be47145c22ea59ed3f5f4e61cb069e875fb67142d281d784bf925cc286eacc2c43e94d08da4924b83e58dbf2e43fa625bdd620eba6d9ce960ff17d14ed1f2dbee7d08eceb540fdc75ff06dabc767267658fad8ce99e2a3236e46d2deedcb51c3c6f81589357edebac9772a70b3d910d83cd1b9ce6534a011e9fa557b891a23b5d88afcc0d9856c6dabeab25eea55e9a248182229e4927f268fe5431672fcce52f434ca3d27d1a2136bae5770bb36920df12fbc01d0e8165610efa04794f414c1417f1d4059435c5385bfe2de83ce0e238d6fd2dbd3c0487c69843298577bfa480fe2a16ab2a0e4bc712cd8b5a14871cda61c993b6835303d9043d7689a") + val attribution3 = Attribution.create(Some(attribution2), None, 400 milliseconds, sharedSecret1) + assert(attribution3 == hex"74a4ea61339463642a2182758871b2ea724f31f531aa98d80f1c3043febca41d5ee52e8b1e127e61719a0d078db8909748d57839e58424b91f063c4fbc8a221bef261140e66a9b596ca6d420a973ad5431adfa8280a7355462fe50d4cac15cdfbd7a535c4b72a0b6d7d8a64cff3f719ff9b8be28036826342dc3bf3781efc70063d1e6fc79dff86334ae0564a5ab87bd61f8446465ef6713f8c4ef9d0200ebb375f90ee115216b469af42de554622df222858d30d733af1c9223e327ae09d9126be8baee6dd59a112d83a57cc6e0252104c11bc11705d384220eedd72f1a29a0597d97967e28b2ad13ba28b3d8a53c3613c1bb49fe9700739969ef1f795034ef9e2e983af2d3bbd6c637fb12f2f7dfc3aee85e08711e9b604106e95d7a4974e5b047674a6015792dae5d913681d84f71edd415910582e5d86590df2ecfd561dc6e1cdb08d3e10901312326a45fb0498a177319389809c6ba07a76cfad621e07b9af097730e94df92fbd311b2cb5da32c80ab5f14971b6d40f8e2ab202ac98bd8439790764a40bf309ea2205c1632610956495720030a25dc7118e0c868fdfa78c3e9ecce58215579a0581b3bafdb7dbbe53be9e904567fdc0ce1236aab5d22f1ebc18997e3ea83d362d891e04c5785fd5238326f767bce499209f8db211a50e1402160486e98e7235cf397dbb9ae19fd9b79ef589c821c6f99f28be33452405a003b33f4540fe0a41dfcc286f4d7cc10b70552ba7850869abadcd4bb7f256823face853633d6e2a999ac9fcd259c71d08e266db5d744e1909a62c0db673745ad9585949d108ab96640d2bc27fb4acac7fa8b170a30055a5ede90e004df9a44bdc29aeb4a6bec1e85dde1de6aaf01c6a5d12405d0bec22f49026cb23264f8c04b8401d3c2ab6f2e109948b6193b3bec27adfe19fb8afb8a92364d6fc5b219e8737d583e7ff3a4bcb75d53edda3bf3f52896ac36d8a877ad9f296ea6c045603fc62ac4ae41272bde85ef7c3b3fd3538aacfd5b025fefbe277c2906821ecb20e6f75ea479fa3280f9100fb0089203455c56b6bc775e5c2f0f58c63edd63fa3eec0b40da4b276d0d41da2ec0ead865a98d12bc694e23d8eaadd2b4d0ee88e9570c88fb878930f492e036d27998d593e47763927ff7eb80b188864a3846dd2238f7f95f4090ed399ae95deaeb37abca1cf37c397cc12189affb42dca46b4ff6988eb8c060691d155302d448f50ff70a794d97c0408f8cee9385d6a71fa412e36edcb22dbf433db9db4779f27b682ee17fc05e70c8e794b9f7f6d1") + val attribution4 = Attribution.create(Some(attribution3), None, 500 milliseconds, sharedSecret0) + assert(attribution4 == hex"84986c936d26bfd3bb2d34d3ec62cfdb63e0032fdb3d9d75f3e5d456f73dffa7e35aab1db4f1bd3b98ff585caf004f656c51037a3f4e810d275f3f6aea0c8e3a125ebee5f374b6440bcb9bb2955ebf70c06d64090f9f6cf098200305f7f4305ba9e1350a0c3f7dab4ccf35b8399b9650d8e363bf83d3a0a09706433f0adae6562eb338b21ea6f21329b3775905e59187c325c9cbf589f5da5e915d9e5ad1d21aa1431f9bdc587185ed8b5d4928e697e67cc96bee6d5354e3764cede3f385588fa665310356b2b1e68f8bd30c75d395405614a40a587031ebd6ace60dfb7c6dd188b572bd8e3e9a47b06c2187b528c5ed35c32da5130a21cd881138a5fcac806858ce6c596d810a7492eb261bcc91cead1dae75075b950c2e81cecf7e5fdb2b51df005d285803201ce914dfbf3218383829a0caa8f15486dd801133f1ed7edec436730b0ec98f48732547927229ac80269fcdc5e4f4db264274e940178732b429f9f0e582c559f994a7cdfb76c93ffc39de91ff936316726cc561a6520d47b2cd487299a96322dadc463ef06127fc63902ff9cc4f265e2fbd9de3fa5e48b7b51aa0850580ef9f3b5ebb60c6c3216c5a75a93e82936113d9cad57ae4a94dd6481954a9bd1b5cff4ab29ca221fa2bf9b28a362c9661206f896fc7cec563fb80aa5eaccb26c09fa4ef7a981e63028a9c4dac12f82ccb5bea090d56bbb1a4c431e315d9a169299224a8dbd099fb67ea61dfc604edf8a18ee742550b636836bb552dabb28820221bf8546331f32b0c143c1c89310c4fa2e1e0e895ce1a1eb0f43278fdb528131a3e32bfffe0c6de9006418f5309cba773ca38b6ad8507cc59445ccc0257506ebc16a4c01d4cd97e03fcf7a2049fea0db28447858f73b8e9fe98b391b136c9dc510288630a1f0af93b26a8891b857bfe4b818af99a1e011e6dbaa53982d29cf74ae7dffef45545279f19931708ed3eede5e82280eab908e8eb80abff3f1f023ab66869297b40da8496861dc455ac3abe1efa8a6f9e2c4eda48025d43a486a3f26f269743eaa30d6f0e1f48db6287751358a41f5b07aee0f098862e3493731fe2697acce734f004907c6f11eef189424fee52cd30ad708707eaf2e441f52bcf3d0c5440c1742458653c0c8a27b5ade784d9e09c8b47f1671901a29360e7e5e94946b9c75752a1a8d599d2a3e14ac81b84d42115cd688c8383a64fc6e7e1dc5568bb4837358ebe63207a4067af66b2027ad2ce8fb7ae3a452d40723a51fdf9f9c9913e8029a222cf81d12ad41e58860d75deb6de30ad") + + val Attribution.UnwrappedAttribution(holdTimes, Some(_)) = Attribution.unwrap(attribution4, sharedSecrets) + assert(holdTimes == Seq(HoldTime(500 millisecond, publicKeys(0)), HoldTime(400 milliseconds, publicKeys(1)), HoldTime(300 milliseconds, publicKeys(2)), HoldTime(200 milliseconds, publicKeys(3)), HoldTime(100 milliseconds, publicKeys(4)))) + } + } + + test("only some nodes in the route support attributable failures") { + for ((payloads, packetPayloadLength) <- Seq((referencePaymentPayloads, 1300), (paymentPayloadsFull, 1300))) { + // origin build the onion packet + val Success(PacketAndSecrets(packet, sharedSecrets)) = create(sessionKey, packetPayloadLength, publicKeys, payloads, associatedData) + // each node parses and forwards the packet + val Right(DecryptedPacket(_, packet1, sharedSecret0)) = peel(privKeys(0), associatedData, packet) + val Right(DecryptedPacket(_, packet2, sharedSecret1)) = peel(privKeys(1), associatedData, packet1) + val Right(DecryptedPacket(_, packet3, sharedSecret2)) = peel(privKeys(2), associatedData, packet2) + val Right(DecryptedPacket(_, packet4, sharedSecret3)) = peel(privKeys(3), associatedData, packet3) + val Right(lastPacket@DecryptedPacket(_, _, sharedSecret4)) = peel(privKeys(4), associatedData, packet4) + assert(lastPacket.isLastPacket) + + // node #4 want to reply with an error message + val failure = IncorrectOrUnknownPaymentDetails(100 msat, BlockHeight(800000), TlvStream(Set.empty[FailureMessageTlv])) + val failurePacket = createCustomLengthFailurePacket(failure, sharedSecret4, 1024) + val error = Sphinx.FailurePacket.wrap(failurePacket, sharedSecret4) + val error1 = FailurePacket.wrap(error, sharedSecret3) + // node #4 does not support attributable failures, nodes #0 to #3 support attributable failures + val attribution1 = Attribution.create(None, Some(error), 200 milliseconds, sharedSecret3) + val error2 = FailurePacket.wrap(error1, sharedSecret2) + val attribution2 = Attribution.create(Some(attribution1), Some(error1), 300 milliseconds, sharedSecret2) + val error3 = FailurePacket.wrap(error2, sharedSecret1) + val attribution3 = Attribution.create(Some(attribution2), Some(error2), 400 milliseconds, sharedSecret1) + val error4 = FailurePacket.wrap(error3, sharedSecret0) + val attribution4 = Attribution.create(Some(attribution3), Some(error3), 500 milliseconds, sharedSecret0) + + // origin parses error packet and can see that it comes from node #4 + val HtlcFailure(holdTimes, Right(DecryptedFailurePacket(pubkey, parsedFailure))) = FailurePacket.decrypt(error4, Some(attribution4), sharedSecrets) + // We're missing attribution data from node #4 but we get hold times until node #3 + assert(holdTimes == Seq(HoldTime(500 millisecond, publicKeys(0)), HoldTime(400 milliseconds, publicKeys(1)), HoldTime(300 milliseconds, publicKeys(2)), HoldTime(200 milliseconds, publicKeys(3)))) + assert(pubkey == publicKeys(4)) + assert(parsedFailure == failure) + } + } + + test("failing node tries to hide its identity") { + for ((payloads, packetPayloadLength) <- Seq((referencePaymentPayloads, 1300), (paymentPayloadsFull, 1300))) { + // origin build the onion packet + val Success(PacketAndSecrets(packet, sharedSecrets)) = create(sessionKey, packetPayloadLength, publicKeys, payloads, associatedData) + // each node parses and forwards the packet + val Right(DecryptedPacket(_, packet1, sharedSecret0)) = peel(privKeys(0), associatedData, packet) + val Right(DecryptedPacket(_, packet2, sharedSecret1)) = peel(privKeys(1), associatedData, packet1) + val Right(DecryptedPacket(_, packet3, sharedSecret2)) = peel(privKeys(2), associatedData, packet2) + val Right(DecryptedPacket(_, packet4, sharedSecret3)) = peel(privKeys(3), associatedData, packet3) + val Right(lastPacket@DecryptedPacket(_, _, sharedSecret4)) = peel(privKeys(4), associatedData, packet4) + assert(lastPacket.isLastPacket) + + // node #4 fails but instead of returning a failure message it returns random data + val error = Sphinx.FailurePacket.wrap(randomBytes(1024), sharedSecret4) + val error1 = FailurePacket.wrap(error, sharedSecret3) + val attribution1 = Attribution.create(None, Some(error), 200 milliseconds, sharedSecret3) + val error2 = FailurePacket.wrap(error1, sharedSecret2) + val attribution2 = Attribution.create(Some(attribution1), Some(error1), 300 milliseconds, sharedSecret2) + val error3 = FailurePacket.wrap(error2, sharedSecret1) + val attribution3 = Attribution.create(Some(attribution2), Some(error2), 400 milliseconds, sharedSecret1) + val error4 = FailurePacket.wrap(error3, sharedSecret0) + val attribution4 = Attribution.create(Some(attribution3), Some(error3), 500 milliseconds, sharedSecret0) + + // origin can't parse the failure packet but the hold times tell us that nodes #0 to #2 are honest + val HtlcFailure(holdTimes, Left(CannotDecryptFailurePacket(_, _))) = FailurePacket.decrypt(error4, Some(attribution4), sharedSecrets) + assert(holdTimes == Seq(HoldTime(500 millisecond, publicKeys(0)), HoldTime(400 milliseconds, publicKeys(1)), HoldTime(300 milliseconds, publicKeys(2)), HoldTime(200 milliseconds, publicKeys(3)))) + } + } + test("last node replies with a failure message (arbitrary length)") { val Success(PacketAndSecrets(packet, sharedSecrets)) = create(sessionKey, 1300, publicKeys, referencePaymentPayloads, associatedData) val Right(DecryptedPacket(_, packet1, sharedSecret0)) = peel(privKeys(0), associatedData, packet) @@ -334,7 +440,7 @@ class SphinxSpec extends AnyFunSuite { assert(lastPacket.isLastPacket) // node #4 want to reply with an error message using a custom length - val error = createCustomLengthFailurePacket(TemporaryNodeFailure(), sharedSecret4, 1024) + val error = Sphinx.FailurePacket.wrap(createCustomLengthFailurePacket(TemporaryNodeFailure(), sharedSecret4, 1024), sharedSecret4) assert(error == hex"4ca0784803691f89f7558ff4560ba55aa6b94486e5c5cf1d0922750ad01e185ba8cb9168b60f2fd45edd73c1b0c8b33002df376801ff58aaa94000bf8a86f92620f343baef38a580102395ae3abf9128d1047a0736ff9b83d456740ebbb4aeb3aa9737f18fb4afb4aa074fb26c4d702f42968888550a3bded8c05247e045b866baef0499f079fdaeef6538f31d44deafffdfd3afa2fb4ca9082b8f1c465371a9894dd8c243fb4847e004f5256b3e90e2edde4c9fb3082ddfe4d1e734cacd96ef0706bf63c9984e22dc98851bcccd1c3494351feb458c9c6af41c0044bea3c47552b1d992ae542b17a2d0bba1a096c78d169034ecb55b6e3a7263c26017f033031228833c1daefc0dedb8cf7c3e37c9c37ebfe42f3225c326e8bcfd338804c145b16e34e4f5984bc119af09d471a61f39e9e389c4120cadabc5d9b7b1355a8ccef050ca8ad72f642fc26919927b347808bade4b1c321b08bc363f20745ba2f97f0ced2996a232f55ba28fe7dfa70a9ab0433a085388f25cce8d53de6a2fbd7546377d6ede9027ad173ba1f95767461a3689ef405ab608a21086165c64b02c1782b04a6dba2361a7784603069124e12f2f6dcb1ec7612a4fbf94c0e14631a2bef6190c3d5f35e0c4b32aa85201f449d830fd8f782ec758b0910428e3ec3ca1dba3b6c7d89f69e1ee1b9df3dfbbf6d361e1463886b38d52e8f43b73a3bd48c6f36f5897f514b93364a31d49d1d506340b1315883d425cb36f4ea553430d538fd6f3596d4afc518db2f317dd051abc0d4bfb0a7870c3db70f19fe78d6604bbf088fcb4613f54e67b038277fedcd9680eb97bdffc3be1ab2cbcbafd625b8a7ac34d8c190f98d3064ecd3b95b8895157c6a37f31ef4de094b2cb9dbf8ff1f419ba0ecacb1bb13df0253b826bec2ccca1e745dd3b3e7cc6277ce284d649e7b8285727735ff4ef6cca6c18e2714f4e2a1ac67b25213d3bb49763b3b94e7ebf72507b71fb2fe0329666477ee7cb7ebd6b88ad5add8b217188b1ca0fa13de1ec09cc674346875105be6e0e0d6c8928eb0df23c39a639e04e4aedf535c4e093f08b2c905a14f25c0c0fe47a5a1535ab9eae0d9d67bdd79de13a08d59ee05385c7ea4af1ad3248e61dd22f8990e9e99897d653dd7b1b1433a6d464ea9f74e377f2d8ce99ba7dbc753297644234d25ecb5bd528e2e2082824681299ac30c05354baaa9c3967d86d7c07736f87fc0f63e5036d47235d7ae12178ced3ae36ee5919c093a02579e4fc9edad2c446c656c790704bfc8e2c491a42500aa1d75c8d4921ce29b753f883e17c79b09ea324f1f32ddf1f3284cd70e847b09d90f6718c42e5c94484cc9cbb0df659d255630a3f5a27e7d5dd14fa6b974d1719aa98f01a20fb4b7b1c77b42d57fab3c724339d459ee4a1c6b5d3bd4e08624c786a257872acc9ad3ff62222f2265a658d9f2a007229a5293b67ec91c84c4b4407c228434bad8a815ca9b256c776bd2c9f") val error1 = FailurePacket.wrap(error, sharedSecret3) assert(error1 == hex"2ddcd9ac6122dc79b8b96c5e0c20c40bb64d656a8785420bcdacd3cb67dd27bca081bab1626a0011585323930fa5b9fae0c85770a2279ff59ec427ad1bbff9001c0cd1497004bd2a0f68b50704cf6d6a4bf3c8b6a0833399a24b3456961ba00736785112594f65b6b2d44d9f5ea4e49b5e1ec2af978cbe31c67114440ac51a62081df0ed46d4a3df295da0b0fe25c0115019f03f15ec86fabb4c852f83449e812f141a9395b3f70b766ebbd4ec2fae2b6955bd8f32684c15abfe8fd3a6261e52650e8807a92158d9f1463261a925e4bfba44bd20b166d532f0017185c3a6ac7957adefe45559e3072c8dc35abeba835a8cb01a71a15c736911126f27d46a36168ca5ef7dccd4e2886212602b181463e0dd30185c96348f9743a02aca8ec27c0b90dca2700c1b46d3f10242ceb286acec56576cf0e22042426c5a61d80c0298dc5ce158f46e11eaf8f32cd44d5f1213d4738768f081978420697b454700ade1c093c02a6ca0e78a7e2f3d9e5c7e49e20c3a56b624bfea51196ec9e88e4e56be38ff56031369f45f1e03be826d44a182f270c153ee0d9f8cf9f1f4132f33974e37c7887d5b857365c873cb218cbf20d4be3abdb2a2011b14add0a5672e01e5845421cf6dd6faca1f2f443757aae575c53ab797c2227ecdab03882bbbf4599318cefafa72fa0c9a0f5a51d13c9d0e5d25bfcfb0154ed25895260a9df8743ac188714a3f16960e6e2ff663c08bffda41743d50960ea2f28cda0bc3bd4a180e297b5b41c700b674cb31d99c7f2a1445e121e772984abff2bbe3f42d757ceeda3d03fb1ffe710aecabda21d738b1f4620e757e57b123dbc3c4aa5d9617dfa72f4a12d788ca596af14bea583f502f16fdc13a5e739afb0715424af2767049f6b9aa107f69c5da0e85f6d8c5e46507e14616d5d0b797c3dea8b74a1b12d4e47ba7f57f09d515f6c7314543f78b5e85329d50c5f96ee2f55bbe0df742b4003b24ccbd4598a64413ee4807dc7f2a9c0b92424e4ae1b418a3cdf02ea4da5c3b12139348aa7022cc8272a3a1714ee3e4ae111cffd1bdfd62c503c80bdf27b2feaea0d5ab8fe00f9cec66e570b00fd24b4a2ed9a5f6384f148a4d6325110a41ca5659ebc5b98721d298a52819b6fb150f273383f1c5754d320be428941922da790e17f482989c365c078f7f3ae100965e1b38c052041165295157e1a7c5b7a57671b842d4d85a7d971323ad1f45e17a16c4656d889fc75c12fc3d8033f598306196e29571e414281c5da19c12605f48347ad5b4648e371757cbe1c40adb93052af1d6110cfbf611af5c8fc682b7e2ade3bfca8b5c7717d19fc9f97964ba6025aebbc91a6671e259949dcf40984342118de1f6b514a7786bd4f6598ffbe1604cef476b2a4cb1343db608aca09d1d38fc23e98ee9c65e7f6023a8d1e61fd4f34f753454bd8e858c8ad6be6403edc599c220e03ca917db765980ac781e758179cd93983e9c1e769e4241d47c") @@ -346,7 +452,7 @@ class SphinxSpec extends AnyFunSuite { assert(error4 == hex"751c187d145e5498306824f193c6bf9ed4a974fa85b3cc5d32d549ce494c1e7b3a06a19f8a9145610741c83ad40b7712aefaddec8c6baf7325d92ea4ca4d1df8bce517f7e54554608bf2bd8071a4f52a7a2f7ffbb1413edad81eeea5785aa9d990f2865dc23b4bc3c301a94eec4eabebca66be5cf638f693ec256aec514620cc28ee4a94bd9565bc4d4962b9d3641d4278fb319ed2b84de5b665f307a2db0f7fbb757366067d88c50f7e829138fde4f78d39b5b5802f1b92a8a820865af5cc79f9f30bc3f461c66af95d13e5e1f0381c184572a91dee1c849048a647a1158cf884064deddbf1b0b88dfe2f791428d0ba0f6fb2f04e14081f69165ae66d9297c118f0907705c9c4954a199bae0bb96fad763d690e7daa6cfda59ba7f2c8d11448b604d12dc942b5cf1db059d3e73d63967e464b5d5cfd4052de195387de93535e88a2e618e15a7c521d67ce2cc836c49118f205c99f18570504504221e337a29e2716fb28671b2bb91e38ef5e18aaf32c6c02f2fb690358872a1ed28166172631a82c2568d23238017188ebbd48944a147f6cdb3690d5f88e51371cb70adf1fa02afe4ed8b581afc8bcc5104922843a55d52acde09bc9d2b71a663e178788280f3c3eae127d21b0b95777976b3eb17be40a702c244d0e5f833ff49dae6403ff44b131e66df8b88e33ab0a58e379f2c34bf5113c66b9ea8241fc7aa2b1fa53cf4ed3cdd91d407730c66fb039ef3a36d4050dde37d34e80bcfe02a48a6b14ae28227b1627b5ad07608a7763a531f2ffc96dff850e8c583461831b19feffc783bc1beab6301f647e9617d14c92c4b1d63f5147ccda56a35df8ca4806b8884c4aa3c3cc6a174fdc2232404822569c01aba686c1df5eecc059ba97e9688c8b16b70f0d24eacfdba15db1c71f72af1b2af85bd168f0b0800483f115eeccd9b02adf03bdd4a88eab03e43ce342877af2b61f9d3d85497cd1c6b96674f3d4f07f635bb26add1e36835e321d70263b1c04234e222124dad30ffb9f2a138e3ef453442df1af7e566890aedee568093aa922dd62db188aa8361c55503f8e2c2e6ba93de744b55c15260f15ec8e69bb01048ca1fa7bbbd26975bde80930a5b95054688a0ea73af0353cc84b997626a987cc06a517e18f91e02908829d4f4efc011b9867bd9bfe04c5f94e4b9261d30cc39982eb7b250f12aee2a4cce0484ff34eebba89bc6e35bd48d3968e4ca2d77527212017e202141900152f2fd8af0ac3aa456aae13276a13b9b9492a9a636e18244654b3245f07b20eb76b8e1cea8c55e5427f08a63a16b0a633af67c8e48ef8e53519041c9138176eb14b8782c6c2ee76146b8490b97978ee73cd0104e12f483be5a4af414404618e9f6633c55dda6f22252cb793d3d16fae4f0e1431434e7acc8fa2c009d4f6e345ade172313d558a4e61b4377e31b8ed4e28f7cd13a7fe3f72a409bc3bdabfe0ba47a6d861e21f64d2fac706dab18b3e546df4") // origin parses error packet and can see that it comes from node #4 - val Right(DecryptedFailurePacket(pubkey, failure)) = FailurePacket.decrypt(error4, sharedSecrets) + val Right(DecryptedFailurePacket(pubkey, failure)) = FailurePacket.decrypt(error4, None, sharedSecrets).failure assert(pubkey == publicKeys(4)) assert(failure == TemporaryNodeFailure()) } @@ -361,12 +467,12 @@ class SphinxSpec extends AnyFunSuite { val Right(DecryptedPacket(_, _, sharedSecret2)) = peel(privKeys(2), associatedData, packet2) // node #2 want to reply with an error message - val error = FailurePacket.create(sharedSecret2, InvalidRealm()) + val error = createAndWrap(sharedSecret2, InvalidRealm()) val error1 = FailurePacket.wrap(error, sharedSecret1) val error2 = FailurePacket.wrap(error1, sharedSecret0) // origin parses error packet and can see that it comes from node #2 - val Right(DecryptedFailurePacket(pubkey, failure)) = FailurePacket.decrypt(error2, sharedSecrets) + val Right(DecryptedFailurePacket(pubkey, failure)) = FailurePacket.decrypt(error2, None, sharedSecrets).failure assert(pubkey == publicKeys(2)) assert(failure == InvalidRealm()) } @@ -379,7 +485,7 @@ class SphinxSpec extends AnyFunSuite { val Right(DecryptedPacket(_, _, sharedSecret2)) = peel(privKeys(2), associatedData, packet2) // node #2 want to reply with an error message - val error = createCustomLengthFailurePacket(InvalidRealm(), sharedSecret2, 1024) + val error = Sphinx.FailurePacket.wrap(createCustomLengthFailurePacket(InvalidRealm(), sharedSecret2, 1024), sharedSecret2) assert(error == hex"f1ca7d3b281a71af53d4a0f83f22b618aae9f9c11b1f3302b13615c66d9aefcc5f1938ef23b9dfa61e3d576b149bedaf83058f85f06a3172a3223ad6c4732d96b32955da7d2feb4140e58d86fc0f2eb5d9d1878e6f8a7f65ab9212030e8e915573ebbd7f35e1a430890be7e67c3fb4bbf2def662fa625421e7b411c29ebe81ec67b77355596b05cc155755664e59c16e21410aabe53e80404a615f44ebb31b365ca77a6e91241667b26c6cad24fb2324cf64e8b9dd6e2ce65f1f098cfd1ef41ba2d4c7def0ff165a0e7c84e7597c40e3dffe97d417c144545a0e38ee33ebaae12cc0c14650e453d46bfc48c0514f354773435ee89b7b2810606eb73262c77a1d67f3633705178d79a1078c3a01b5fadc9651feb63603d19decd3a00c1f69af2dab2595931ca50d8280758b1cc91ba2dc43dbbc3d91bf25c08b46c2ecef7a32cec64d4b61ee3a629ef563afe058b71e71bcb69033948bc8728c5ebe65ec596e4f305b9fc159d53f723dfc95b57f3d51717f1c89af97a6d587e89e62efcc92198a1b2bd66e2d875505ea4046c04389f8cb0ee98f0af03af2652e2f3d9a9c48430f2891a4d9b16e7d18099e4a3dd334c24aba1e2450792c2f22092c170da549d43a440021e699bd6c20d8bbf1961100a01ebcce06a4609f5ad93066287acf68294cfa9ea7cea03a508983b134a9f0118b16409a61c06aaa95897d2067cb7cd59123f3e2ccf0e16091571d616c44818f118bb7835a679f5c0eea8cf1bd5479882b2c2a341ec26dbe5da87b3d37d66b1fbd176f71ab203a3b6eaf7f214d579e7d0e4a3e59089ebd26ba04a62403ae7a793516ec16d971d51c5c0107a917d1a70221e6de16edca7cb057c7d06902b5191f298aa4d478a0c3a6260c257eae504ebbf2b591688e6f3f77af770b6f566ae9868d2f26c12574d3bf9323af59f0fe0072ff94ae597c2aa6fbcbf0831989e02f9d3d1b9fd6dd97f509185d9ecbf272e38bd621ee94b97af8e1cd43853a8f6aa6e8372585c71bf88246d064ade524e1e0bd8496b620c4c2d3ae06b6b064c97536aaf8d515046229f72bee8aa398cd0cc21afd5449595016bef4c77cb1e2e9d31fe1ca3ffde06515e6a4331ccc84edf702e5777b10fc844faf17601a4be3235931f6feca4582a8d247c1d6e4773f8fb6de320cf902bbb1767192782dc550d8e266e727a2aa2a414b816d1826ea46af71701537193c22bbcc0123d7ff5a23b0aa8d7967f36fef27b14fe1866ff3ab215eb29e07af49e19174887d71da7e7fe1b7aa1b3c805c063e0fafedf125fa6c57e38cce33a3f7bb35fd8a9f0950de3c22e49743c05f40bc55f960b8a8b5e2fde4bb229f125538438de418cb318d13968532499118cb7dcaaf8b6d635ac4001273bdafd12c8ea0702fb2f0dac81dbaaf68c1c32266382b293fa3951cb952ed5c1bdc41750cdbc0bd62c51bb685616874e251f031a929c06faef5bfcb0857f815ae20620b823f0abecfb5") val error1 = FailurePacket.wrap(error, sharedSecret1) assert(error1 == hex"fedab5542d8cfc76425c1960d1676ac551116628b2859535ed74f8934d38b82c175c570b34788dbfd0048e4a41c2bb01acf21a928c09f96b801d011d5ff805731f476679849797e76d1ace72304509e05adbbcf0f74959d7d370af32fa27066b9a7a9cb91d92518f3bdabe35a8b3ecea116db79b0c011b70742599012741c4128ca6655eeaf7e6ff343fed810af0e069fa1650659d5864f8b9f1aea92f1fcc10b1b71f3b012e1e55e53056d7f5e092daf7eb1b9244d2de468f69730f3237ce39a84cfd0ee42b12e5ac7ca63fea15bee528125e135090988e55fda565f99f15787ab49ae2b536ca34b1732069a72f314c99836091c17f4e50afecc602184c1e656cf6eb752a4ded94df315a3e16e3d3e422517e9b9a5c566f8bf3eb6144a6778df0078b51887d8ea59b73416c59594f81f8bf1b0f1c98b3d9d5ed87fe76358a47df8a705fb3edddf64770c2d49744854a5ed0272d94cec1cd1b049a6dece2e4aade89d783634c259a330bc407af06368aece354d6fe73608716da08a037dec9c71c4c73bd4a6c86fd1820b54aa2602132a95495933a24e28b189219859ab46847340ad08968a70d5a0df8223aab06a6ea532a4cb25498f3687361a59b9896975c948e03ba60f5248a1f2f4d7aa6e8f00f82f6ca92273f6084cac56c51d4dda2511d64d88dcfd11df5a07ae6779d445f141f5759fca37e09826e2e481ed5dced02956104b219f839f508f60d8828250d0a3617b9d021fad48cde24a5cb42e3278dff0d95af795d4c71bccd344fa98129c9d6f53dd4f7acab78a98711fc5d04112ae971dedc97649608597b7e53369be2fe3f9b0e6b349b3fadcf9bd2a3d24b5e876c74e1006f7c330714ea5146986f3f73b09cae5cdf6277e23a34ecce0d92d909442743ef415be81050c341eb305e93b14b07b55c079766cd894ea00826ce50d5c45707870b0cf411113b8e4e43cf34caf79f3936fdfdbeae185ff52db69ca72442d892ed0e45b9fac939aa172bfa873cebee1e2196fe124597feb92880339ebca8233acaf3061591ed8cf290dfd9b0a06d7efc299993b9c680451992e15d2cb8b5b4a3dc1e511a39d781818144a9662bdfbd01371e898c454a8a092b7a0d32a8d58aec8134891a974ac7b297c3b4f94100083db891bde0ebc1e737dc6c33dad87cf20429d1b865c7e8ee5032d66a17c5a731d288dda8fc38e2c963c317f12a786ded3eac484dcc11b5c530dec0e4cc40ec4bb2c529555a51d8655a4de08fdde774781b5672150d1c771bf0916fc5df6ddb2f2e683e86aa23a52c0fc2efe72eeb1fa5f86ad7926685f40d57ab19b29e1ce5dce8c98ae35aacf740cacb257915fe8421ec09d0883d4ae41fe2695679264b0196e8d0b874d47e2fd675c9dfba26e666d407572e19a65c84ca54ac7235ef1bd4aedd9b0f6406cc7eeb08020e325f22396bc1a42d2de5ff71042b4e098cb0358741a50757a31c45de1f7ecf3a5e5e06f8b682f0") @@ -387,7 +493,7 @@ class SphinxSpec extends AnyFunSuite { assert(error2 == hex"c843486107187673b4586f5cdaad43ad84fbac03b39df51bbf9169b2bd682b409a855b2feb0545705f12eba9dbaecee84e328a9c2e4c3086bb1d0909d1f2e4f8a0e9c6be9541e94a849a0887756b984031dcb74d11c20d437a55daf3ee4109dea68ad74f9b742e7571d5e4d1b2ea4f7094787cf361b448a22a547ea85b833aae20f3ba79fb41c6636414c2092d41dd5328e2c1a1c754cb1f0d297628219f91fe946169f593ce7fce79103945d4d24adce46c083ab24757870356af55fcd3d22b9cfd83c45d409eb3081b218448d5dca3a201cf89ac88c9b66049d7c262b32081d3aba2098ea853bfa173ec23aa9253e083dfa881ef487b76780435c1b9f8a1d794557f0ac91d261d280bfb8513ad0c4dab0d7152eb9ee36ae63b8d384613684326d8735dc559f31cecb21b1d55bbcf7a281127adbedd0210b243325fd291cb82d443beec8f4b96aaee4b1a619724d7456b756d391e8fd3256d2b0766e39a435eb4d6d144c7fca1c73105710266e31120565444dfd6e9099e44d73a0f28419809577a267bbbc6671f723669d00c35c8e60fad88d89d4a7477a0c30f9839485197ed76338330f2ca00cf0e31c59da4eeebef977f429ad2c61acac35939866dac5b1df1c3c487ebaf961340c0c1dbc4bedebde7ee0633c3f480b7df265a3d90e78a4bcb9497f4228169fadb647e77afe6f43aa129286bb21767f6e75ac5c092473f99f2cf8b4e191f300c70b210e077a0385d483971bc0c66f5c119c0731a8753793ad12703d9cc5153eb1c8f25b71ee88a8d1d4433aa8f8277366c82111dbebfe0f548411588d54c3606742330d3d84a2f107df98d60995297de11672f6300b11444a04e252d69d8187772798afc6a9cd8b245a5ebd51bf0659f18c57daf1d1f724d2f15d524ab6902fb17a8fa6cee8e01df67735eac34bb0efc183dcb8d2a7cb401bd786c32a17f14c9d9ffc02b4f58c4ebab898a78b4913647d4cb5bafe6f7f27b5a256d1635c10f0ca71796610068c090c270c20bb18ec9d205e640d7655bdf5c9aeae20d7f9426eade0733c19d0aa577caf31f9d5be0a99ed0c509e84ccb555389ca69f09c3e66694a4ea2785f8d839d7dfff08b2c21aff89a023161cb1ebdd1e7a46d6380c0ddbc88eb3526e624fadcd222ecaa09566c2678158f933f03623299fec134a880d39a9d82ba2b29211e7787b3f32d478df856389a02cb68b66fc0dfc0b52353e7360f31e5457a6a9dd34512e912afeb5a92f3cbd3883b62c37e3ba5e4e8b688033150103c810740d130a5597c8a4a16311f50cfb3a919aac1e0a1096f20a14a536c55068ad38f40e62fc6f178b2fee67ca2cbd8afa29ef6c89b217aee02419ca26d59b604521a55e37c0a5a693fbc3ebcba23cd62479ddf62e5521847a2b4ac5e7686ef662c29cf8a8983660530942ee9a6c53b55e08af0b43467989693cefe6267fd524435152c01c9b93aebdec6146366a94162f99ac4c7157c15b988") // origin parses error packet and can see that it comes from node #2 - val Right(DecryptedFailurePacket(pubkey, failure)) = FailurePacket.decrypt(error2, sharedSecrets) + val Right(DecryptedFailurePacket(pubkey, failure)) = FailurePacket.decrypt(error2, None, sharedSecrets).failure assert(pubkey == publicKeys(2)) assert(failure == InvalidRealm()) } @@ -600,7 +706,11 @@ object SphinxSpec { def createCustomLengthFailurePacket(failure: FailureMessage, sharedSecret: ByteVector32, length: Int): ByteVector = { val um = Sphinx.generateKey("um", sharedSecret) val packet = FailureMessageCodecs.failureOnionCodec(Hmac256(um), length).encode(failure).require.toByteVector - Sphinx.FailurePacket.wrap(packet, sharedSecret) + packet + } + + def createAndWrap(sharedSecret: ByteVector32, failure: FailureMessage): ByteVector = { + FailurePacket.wrap(FailurePacket.create(sharedSecret, failure), sharedSecret) } val privKeys = Seq( diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/TransportHandlerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/TransportHandlerSpec.scala index 0f0b2a89e2..e0d8b72b53 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/TransportHandlerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/TransportHandlerSpec.scala @@ -19,17 +19,17 @@ package fr.acinq.eclair.crypto import akka.actor.{Actor, ActorLogging, ActorRef, OneForOneStrategy, Props, Stash, SupervisorStrategy, Terminated} import akka.io.Tcp import akka.testkit.{TestActorRef, TestFSMRef, TestProbe} -import fr.acinq.eclair.TestKitBaseClass +import fr.acinq.eclair.{TestKitBaseClass, randomBytes32} import fr.acinq.eclair.crypto.Noise.{Chacha20Poly1305CipherFunctions, CipherState} import fr.acinq.eclair.crypto.TransportHandler.{Encryptor, ExtendedCipherState, Listener} -import fr.acinq.eclair.wire.protocol.CommonCodecs +import fr.acinq.eclair.wire.protocol.LightningMessageCodecs.{lightningMessageCodec, pingCodec, warningCodec} +import fr.acinq.eclair.wire.protocol.{LightningMessage, Ping, Pong, Warning} import org.scalatest.BeforeAndAfterAll import org.scalatest.funsuite.AnyFunSuiteLike import scodec.Codec import scodec.bits._ import scodec.codecs._ -import java.nio.charset.Charset import scala.annotation.tailrec import scala.concurrent.duration._ @@ -38,19 +38,19 @@ class TransportHandlerSpec extends TestKitBaseClass with AnyFunSuiteLike with Be import TransportHandlerSpec._ object Initiator { - val s = Noise.Secp256k1DHFunctions.generateKeyPair(hex"1111111111111111111111111111111111111111111111111111111111111111") + val s: Noise.KeyPair = Noise.Secp256k1DHFunctions.generateKeyPair(hex"1111111111111111111111111111111111111111111111111111111111111111") } object Responder { - val s = Noise.Secp256k1DHFunctions.generateKeyPair(hex"2121212121212121212121212121212121212121212121212121212121212121") + val s: Noise.KeyPair = Noise.Secp256k1DHFunctions.generateKeyPair(hex"2121212121212121212121212121212121212121212121212121212121212121") } test("successful handshake") { val pipe = system.actorOf(Props[MyPipe]()) val probe1 = TestProbe() val probe2 = TestProbe() - val initiator = TestFSMRef(new TransportHandler(Initiator.s, Some(Responder.s.pub), pipe, CommonCodecs.varsizebinarydata)) - val responder = TestFSMRef(new TransportHandler(Responder.s, None, pipe, CommonCodecs.varsizebinarydata)) + val initiator = TestFSMRef(new TransportHandler(Initiator.s, Some(Responder.s.pub), pipe, lightningMessageCodec)) + val responder = TestFSMRef(new TransportHandler(Responder.s, None, pipe, lightningMessageCodec)) pipe ! (initiator, responder) awaitCond(initiator.stateName == TransportHandler.WaitingForListener) @@ -62,43 +62,11 @@ class TransportHandlerSpec extends TestKitBaseClass with AnyFunSuiteLike with Be awaitCond(initiator.stateName == TransportHandler.Normal) awaitCond(responder.stateName == TransportHandler.Normal) - initiator.tell(ByteVector("hello".getBytes), probe1.ref) - probe2.expectMsg(ByteVector("hello".getBytes)) + initiator.tell(Ping(1105, ByteVector("hello".getBytes)), probe1.ref) + probe2.expectMsg(Ping(1105, ByteVector("hello".getBytes))) - responder.tell(ByteVector("bonjour".getBytes), probe2.ref) - probe1.expectMsg(ByteVector("bonjour".getBytes)) - - probe1.watch(pipe) - initiator.stop() - responder.stop() - system.stop(pipe) - probe1.expectTerminated(pipe) - } - - test("successful handshake with custom serializer") { - case class MyMessage(payload: String) - val mycodec: Codec[MyMessage] = ("payload" | scodec.codecs.string32L(Charset.defaultCharset())).as[MyMessage] - val pipe = system.actorOf(Props[MyPipe]()) - val probe1 = TestProbe() - val probe2 = TestProbe() - val initiator = TestFSMRef(new TransportHandler(Initiator.s, Some(Responder.s.pub), pipe, mycodec)) - val responder = TestFSMRef(new TransportHandler(Responder.s, None, pipe, mycodec)) - pipe ! (initiator, responder) - - awaitCond(initiator.stateName == TransportHandler.WaitingForListener) - awaitCond(responder.stateName == TransportHandler.WaitingForListener) - - initiator ! Listener(probe1.ref) - responder ! Listener(probe2.ref) - - awaitCond(initiator.stateName == TransportHandler.Normal) - awaitCond(responder.stateName == TransportHandler.Normal) - - initiator.tell(MyMessage("hello"), probe1.ref) - probe2.expectMsg(MyMessage("hello")) - - responder.tell(MyMessage("bonjour"), probe2.ref) - probe1.expectMsg(MyMessage("bonjour")) + responder.tell(Pong(ByteVector("bonjour".getBytes)), probe2.ref) + probe1.expectMsg(Pong(ByteVector("bonjour".getBytes))) probe1.watch(pipe) initiator.stop() @@ -108,22 +76,15 @@ class TransportHandlerSpec extends TestKitBaseClass with AnyFunSuiteLike with Be } test("handle unknown messages") { - sealed trait Message - case object Msg1 extends Message - case object Msg2 extends Message - - val codec1: Codec[Message] = discriminated[Message].by(uint8) - .typecase(1, provide(Msg1)) - - val codec12: Codec[Message] = discriminated[Message].by(uint8) - .typecase(1, provide(Msg1)) - .typecase(2, provide(Msg2)) + val incompleteCodec: Codec[LightningMessage] = discriminated[LightningMessage].by(uint16) + .typecase(1, warningCodec) + .typecase(18, pingCodec) val pipe = system.actorOf(Props[MyPipePull]()) val probe1 = TestProbe() val probe2 = TestProbe() - val initiator = TestFSMRef(new TransportHandler(Initiator.s, Some(Responder.s.pub), pipe, codec1)) - val responder = TestFSMRef(new TransportHandler(Responder.s, None, pipe, codec12)) + val initiator = TestFSMRef(new TransportHandler(Initiator.s, Some(Responder.s.pub), pipe, incompleteCodec)) + val responder = TestFSMRef(new TransportHandler(Responder.s, None, pipe, lightningMessageCodec)) pipe ! (initiator, responder) awaitCond(initiator.stateName == TransportHandler.WaitingForListener) @@ -135,16 +96,18 @@ class TransportHandlerSpec extends TestKitBaseClass with AnyFunSuiteLike with Be awaitCond(initiator.stateName == TransportHandler.Normal) awaitCond(responder.stateName == TransportHandler.Normal) - responder ! Msg1 - probe1.expectMsg(Msg1) - probe1.reply(TransportHandler.ReadAck(Msg1)) + val msg1 = Ping(130, hex"deadbeef") + responder ! msg1 + probe1.expectMsg(msg1) + probe1.reply(TransportHandler.ReadAck(msg1)) - responder ! Msg2 + responder ! Pong(hex"deadbeef") probe1.expectNoMessage(2 seconds) // unknown message - responder ! Msg1 - probe1.expectMsg(Msg1) - probe1.reply(TransportHandler.ReadAck(Msg1)) + val msg2 = Warning(randomBytes32(), hex"beefdead") + responder ! msg2 + probe1.expectMsg(msg2) + probe1.reply(TransportHandler.ReadAck(msg2)) probe1.watch(pipe) initiator.stop() @@ -157,8 +120,8 @@ class TransportHandlerSpec extends TestKitBaseClass with AnyFunSuiteLike with Be val pipe = system.actorOf(Props[MyPipeSplitter]()) val probe1 = TestProbe() val probe2 = TestProbe() - val initiator = TestFSMRef(new TransportHandler(Initiator.s, Some(Responder.s.pub), pipe, CommonCodecs.varsizebinarydata)) - val responder = TestFSMRef(new TransportHandler(Responder.s, None, pipe, CommonCodecs.varsizebinarydata)) + val initiator = TestFSMRef(new TransportHandler(Initiator.s, Some(Responder.s.pub), pipe, lightningMessageCodec)) + val responder = TestFSMRef(new TransportHandler(Responder.s, None, pipe, lightningMessageCodec)) pipe ! (initiator, responder) awaitCond(initiator.stateName == TransportHandler.WaitingForListener) @@ -170,11 +133,11 @@ class TransportHandlerSpec extends TestKitBaseClass with AnyFunSuiteLike with Be awaitCond(initiator.stateName == TransportHandler.Normal) awaitCond(responder.stateName == TransportHandler.Normal) - initiator.tell(ByteVector("hello".getBytes), probe1.ref) - probe2.expectMsg(ByteVector("hello".getBytes)) + initiator.tell(Ping(187, ByteVector("hello".getBytes)), probe1.ref) + probe2.expectMsg(Ping(187, ByteVector("hello".getBytes))) - responder.tell(ByteVector("bonjour".getBytes), probe2.ref) - probe1.expectMsg(ByteVector("bonjour".getBytes)) + responder.tell(Pong(ByteVector("bonjour".getBytes)), probe2.ref) + probe1.expectMsg(Pong(ByteVector("bonjour".getBytes))) probe1.watch(pipe) initiator.stop() @@ -187,11 +150,11 @@ class TransportHandlerSpec extends TestKitBaseClass with AnyFunSuiteLike with Be val pipe = system.actorOf(Props[MyPipe]()) val probe1 = TestProbe() val supervisor = TestActorRef(Props(new MySupervisor())) - val initiator = TestFSMRef(new TransportHandler(Initiator.s, Some(Initiator.s.pub), pipe, CommonCodecs.varsizebinarydata), supervisor, "ini") - val responder = TestFSMRef(new TransportHandler(Responder.s, None, pipe, CommonCodecs.varsizebinarydata), supervisor, "res") + val initiator = TestFSMRef(new TransportHandler(Initiator.s, Some(Initiator.s.pub), pipe, lightningMessageCodec), supervisor, "ini") + val responder = TestFSMRef(new TransportHandler(Responder.s, None, pipe, lightningMessageCodec), supervisor, "res") probe1.watch(responder) pipe ! (initiator, responder) - + // We automatically disconnect after a while if the handshake doesn't succeed. probe1.expectTerminated(responder, 3 seconds) } @@ -219,9 +182,7 @@ class TransportHandlerSpec extends TestKitBaseClass with AnyFunSuiteLike with Be */ val ck = hex"919219dbb2920afa8db80f9a51787a840bcf111ed8d588caf9ab4be716e42b01" val sk = hex"969ab31b4d288cedf6218839b27a3e2140827047f2c0f01bf5c04435d43511a9" - val rk = hex"bb9020b8965f4df047e07f955f3c4b88418984aadc5cdb35096b9ea8fa5c3442" val enc = ExtendedCipherState(CipherState(sk, Chacha20Poly1305CipherFunctions), ck) - val dec = ExtendedCipherState(CipherState(rk, Chacha20Poly1305CipherFunctions), ck) @tailrec def loop(cs: Encryptor, count: Int, acc: Vector[ByteVector] = Vector.empty[ByteVector]): Vector[ByteVector] = { @@ -239,20 +200,51 @@ class TransportHandlerSpec extends TestKitBaseClass with AnyFunSuiteLike with Be assert(ciphertexts(1000) == hex"4a2f3cc3b5e78ddb83dcb426d9863d9d9a723b0337c89dd0b005d89f8d3c05c52b76b29b740f09") assert(ciphertexts(1001) == hex"2ecd8c8a5629d0d02ab457a0fdd0f7b90a192cd46be5ecb6ca570bfc5e268338b1a16cf4ef2d36") } + + test("reject ping flooding") { + val pipe = system.actorOf(Props[MyPipe]()) + val probe1 = TestProbe() + val probe2 = TestProbe() + val initiator = TestFSMRef(new TransportHandler(Initiator.s, Some(Responder.s.pub), pipe, lightningMessageCodec)) + val responder = TestFSMRef(new TransportHandler(Responder.s, None, pipe, lightningMessageCodec)) + pipe ! (initiator, responder) + + awaitCond(initiator.stateName == TransportHandler.WaitingForListener) + awaitCond(responder.stateName == TransportHandler.WaitingForListener) + + initiator ! Listener(probe1.ref) + responder ! Listener(probe2.ref) + + awaitCond(initiator.stateName == TransportHandler.Normal) + awaitCond(responder.stateName == TransportHandler.Normal) + + initiator.tell(Ping(1105, ByteVector("hello 1".getBytes)), probe1.ref) + probe2.expectMsg(Ping(1105, ByteVector("hello 1".getBytes))) + + initiator.tell(Ping(1105, ByteVector("hello 2".getBytes)), probe1.ref) + probe2.expectNoMessage() + + probe1.watch(initiator) + probe1.expectTerminated(initiator) + + probe1.watch(responder) + probe1.expectTerminated(responder) + + probe1.watch(pipe) + probe1.expectTerminated(pipe) + } } object TransportHandlerSpec { class MyPipe extends Actor with Stash with ActorLogging { - - def receive = { + def receive: Receive = { case (a: ActorRef, b: ActorRef) => unstashAll() context watch a context watch b context become ready(a, b) - - case msg => stash() + case _ => stash() } def ready(a: ActorRef, b: ActorRef): Receive = { @@ -267,15 +259,13 @@ object TransportHandlerSpec { } class MyPipeSplitter extends Actor with Stash { - - def receive = { + def receive: Receive = { case (a: ActorRef, b: ActorRef) => unstashAll() context watch a context watch b context become ready(a, b) - - case msg => stash() + case _ => stash() } def ready(a: ActorRef, b: ActorRef): Receive = { @@ -296,15 +286,13 @@ object TransportHandlerSpec { } class MyPipePull extends Actor with Stash { - - def receive = { + def receive: Receive = { case (a: ActorRef, b: ActorRef) => unstashAll() context watch a context watch b context become ready(a, b, aResume = true, bResume = true) - - case msg => stash() + case _ => stash() } def ready(a: ActorRef, b: ActorRef, aResume: Boolean, bResume: Boolean): Receive = { @@ -336,7 +324,7 @@ object TransportHandlerSpec { case _ => SupervisorStrategy.stop } - def receive = { + def receive: Receive = { case _ => () } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/keymanager/LocalChannelKeyManagerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/keymanager/LocalChannelKeyManagerSpec.scala index c0375b7990..818093d201 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/keymanager/LocalChannelKeyManagerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/keymanager/LocalChannelKeyManagerSpec.scala @@ -22,7 +22,7 @@ import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, DeterministicWallet} import fr.acinq.eclair.Setup.Seeds import fr.acinq.eclair.channel.ChannelConfig import fr.acinq.eclair.crypto.ShaChain -import fr.acinq.eclair.{NodeParams, TestConstants, TestUtils} +import fr.acinq.eclair.{NodeParams, TestUtils, randomBytes32} import org.scalatest.funsuite.AnyFunSuite import scodec.bits._ @@ -31,32 +31,51 @@ import java.nio.file.Files class LocalChannelKeyManagerSpec extends AnyFunSuite { + test("generate the same secrets from the same seed") { // data was generated with eclair 0.3 val seed = hex"17b086b228025fa8f4416324b6ba2ec36e68570ae2fc3d392520969f2a9d0c1501" - val nodeKeyManager = new LocalNodeKeyManager(seed, Block.Testnet3GenesisBlock.hash) - val channelKeyManager = new LocalChannelKeyManager(seed, Block.Testnet3GenesisBlock.hash) + val nodeKeyManager = LocalNodeKeyManager(seed, Block.Testnet3GenesisBlock.hash) + val channelKeyManager = LocalChannelKeyManager(seed, Block.Testnet3GenesisBlock.hash) assert(nodeKeyManager.nodeId == PublicKey(hex"02a051267759c3a149e3e72372f4e0c4054ba597ebfd0eda78a2273023667205ee")) - val keyPath = KeyPath("m/1'/2'/3'/4'") - assert(channelKeyManager.commitmentSecret(keyPath, 0L).value == ByteVector32.fromValidHex("fa7a8c2fc62642f7a9a19ea0bfad14d39a430f3c9899c185dcecc61c8077891e")) - assert(channelKeyManager.commitmentSecret(keyPath, 1L).value == ByteVector32.fromValidHex("3e82338d3e487c760ee10448127613d196b040e86ce90d2d437db6425bb7301c")) - assert(channelKeyManager.commitmentSecret(keyPath, 2L).value == ByteVector32.fromValidHex("102357f7a9b2d0b9147f645c98aa156d3278ddb4745caf0631773dd663e76e6f")) - assert(channelKeyManager.commitmentPoint(keyPath, 0L).value == hex"0x0237dd5a0ea26ed84ed1249d46cc715679b542939d6943b42232e043825cde3944") - assert(DeterministicWallet.encode(channelKeyManager.delayedPaymentPoint(keyPath), DeterministicWallet.tpub) == "tpubDMBn7xW1g1Gsok5eThkJAKJnB3ZFqZQnvsdWv8VvM3RjZkqVPZZpjPDAAmbyDHnZPdAZY8EnFBh1ibTBtiuDqb8t9wRcAZiFihma3yYRG1f") - assert(DeterministicWallet.encode(channelKeyManager.htlcPoint(keyPath), DeterministicWallet.tpub) == "tpubDMBn7xW1g1GsqpsqaVNB1ehpjktQUX44Dycy7fJ6thp774XGzNeWFmQf5L6dVChHREgkoc8BYc2caHqwc2mZzTYCwoxsvrpchBSujsPCvGH") - assert(DeterministicWallet.encode(channelKeyManager.paymentPoint(keyPath), DeterministicWallet.tpub) == "tpubDMBn7xW1g1Gsme9jTAEJwTvizDJtJEgE3jc9vkDqQ9azuh9Es2aM6GsioFiouwdvWPJoNw2zavCkVTMta6UJN6BWR5cMZQsSHvsFyQNfGzv") - assert(DeterministicWallet.encode(channelKeyManager.revocationPoint(keyPath), DeterministicWallet.tpub) == "tpubDMBn7xW1g1GsizhaZ7M4co6sBtUDhRUKgUUPWRv3WfLTpTGYrSjATJy6ZVSoYFCKRnaBop5dFig3Ham1P145NQAKuUgPUbujLAooL7F2vy6") + val fundingKeyPath = KeyPath("m/1'/2'/3'/4'") + val channelKeys = channelKeyManager.channelKeys(ChannelConfig(), fundingKeyPath) + assert(channelKeys.commitmentSecret(0).value == ByteVector32.fromValidHex("fa7a8c2fc62642f7a9a19ea0bfad14d39a430f3c9899c185dcecc61c8077891e")) + assert(channelKeys.commitmentSecret(1).value == ByteVector32.fromValidHex("3e82338d3e487c760ee10448127613d196b040e86ce90d2d437db6425bb7301c")) + assert(channelKeys.commitmentSecret(2).value == ByteVector32.fromValidHex("102357f7a9b2d0b9147f645c98aa156d3278ddb4745caf0631773dd663e76e6f")) + assert(channelKeys.commitmentPoint(0).value == hex"0x0237dd5a0ea26ed84ed1249d46cc715679b542939d6943b42232e043825cde3944") + assert(channelKeys.delayedPaymentBaseSecret == PrivateKey(hex"195f8f7de612978117baaa750c0098362eb17ed287161cc84dc03f869e321317")) + assert(channelKeys.htlcBaseSecret == PrivateKey(hex"9430df6ca38bf1a00a8ea8f1123ec870ad04c3cbce9641e38b6bf94cb910f7f3")) + assert(channelKeys.paymentBaseSecret == PrivateKey(hex"6a2577dbac51e4ddc6fc325ff63f3eba6f37b08c3e5ac173168810d20c5632cd")) + assert(channelKeys.revocationBaseSecret == PrivateKey(hex"0871e813ddc7b29bca128c9c9b048f6e60fbe6a53fda1558f33545951e04e1ab")) + assert(DeterministicWallet.ExtendedPublicKey.decode("tpubDMBn7xW1g1Gsok5eThkJAKJnB3ZFqZQnvsdWv8VvM3RjZkqVPZZpjPDAAmbyDHnZPdAZY8EnFBh1ibTBtiuDqb8t9wRcAZiFihma3yYRG1f")._2.publicKey == channelKeys.delayedPaymentBasePoint) + assert(DeterministicWallet.ExtendedPublicKey.decode("tpubDMBn7xW1g1GsqpsqaVNB1ehpjktQUX44Dycy7fJ6thp774XGzNeWFmQf5L6dVChHREgkoc8BYc2caHqwc2mZzTYCwoxsvrpchBSujsPCvGH")._2.publicKey == channelKeys.htlcBasePoint) + assert(DeterministicWallet.ExtendedPublicKey.decode("tpubDMBn7xW1g1Gsme9jTAEJwTvizDJtJEgE3jc9vkDqQ9azuh9Es2aM6GsioFiouwdvWPJoNw2zavCkVTMta6UJN6BWR5cMZQsSHvsFyQNfGzv")._2.publicKey == channelKeys.paymentBasePoint) + assert(DeterministicWallet.ExtendedPublicKey.decode("tpubDMBn7xW1g1GsizhaZ7M4co6sBtUDhRUKgUUPWRv3WfLTpTGYrSjATJy6ZVSoYFCKRnaBop5dFig3Ham1P145NQAKuUgPUbujLAooL7F2vy6")._2.publicKey == channelKeys.revocationBasePoint) } test("compute channel key path from funding keys") { // if this test fails it means that we don't generate the same channel key path from the same funding pubkey, which // will break existing channels ! val pub = PrivateKey(ByteVector32.fromValidHex("01" * 32)).publicKey - val keyPath = ChannelKeyManager.keyPath(pub) + val keyPath = ChannelKeyManager.keyPathFromPublicKey(pub) assert(keyPath.toString() == "m/1909530642'/1080788911/847211985'/1791010671/1303008749'/34154019'/723973395/767609665") } - def makefundingKeyPath(entropy: ByteVector, isInitiator: Boolean): KeyPath = { + test("deterministically derive channel keys based on channel config") { + val channelKeyManager = LocalChannelKeyManager(randomBytes32(), Block.Testnet3GenesisBlock.hash) + val fundingKeyPath1 = channelKeyManager.newFundingKeyPath(isChannelOpener = true) + val fundingKeyPath2 = channelKeyManager.newFundingKeyPath(isChannelOpener = true) + assert(fundingKeyPath1 != fundingKeyPath2) + + val channelKeys1 = channelKeyManager.channelKeys(ChannelConfig.standard, fundingKeyPath1) + val channelKeys2 = channelKeyManager.channelKeys(ChannelConfig.standard, fundingKeyPath2) + assert(channelKeys1 == channelKeyManager.channelKeys(ChannelConfig.standard, fundingKeyPath1)) + assert(channelKeys1 != channelKeyManager.channelKeys(ChannelConfig(), fundingKeyPath1)) + assert(channelKeys1 != channelKeys2) + } + + def makeFundingKeyPath(entropy: ByteVector, isInitiator: Boolean): KeyPath = { val items = for (i <- 0 to 7) yield entropy.drop(i * 4).take(4).toInt(signed = false) & 0xFFFFFFFFL val last = DeterministicWallet.hardened(if (isInitiator) 1L else 0L) KeyPath(items :+ last) @@ -64,70 +83,90 @@ class LocalChannelKeyManagerSpec extends AnyFunSuite { test("test vectors (testnet, funder)") { val seed = ByteVector.fromValidHex("17b086b228025fa8f4416324b6ba2ec36e68570ae2fc3d392520969f2a9d0c1501") - val channelKeyManager = new LocalChannelKeyManager(seed, Block.Testnet3GenesisBlock.hash) - val fundingKeyPath = makefundingKeyPath(hex"be4fa97c62b9f88437a3be577b31eb48f2165c7bc252194a15ff92d995778cfb", isInitiator = true) - val fundingPub = channelKeyManager.fundingPublicKey(fundingKeyPath, fundingTxIndex = 0) - - val localParams = TestConstants.Alice.channelParams.copy(fundingKeyPath = fundingKeyPath) - val channelKeyPath = channelKeyManager.keyPath(localParams, ChannelConfig.standard) - - assert(fundingPub.publicKey == PrivateKey(hex"216414970b4216b197a1040367419ad6922f80e8b73ced083e9afe5e6ddd8e4c").publicKey) - assert(channelKeyManager.revocationPoint(channelKeyPath).publicKey == PrivateKey(hex"a4e7ab3c54752a3487b3c474467843843f28d3bb9113e65e92056ad45d1e318e").publicKey) - assert(channelKeyManager.paymentPoint(channelKeyPath).publicKey == PrivateKey(hex"de24c43d24b8d6bc66b020ac81164206bb577c7924511d4e99431c0d60505012").publicKey) - assert(channelKeyManager.delayedPaymentPoint(channelKeyPath).publicKey == PrivateKey(hex"8aa7b8b14a7035540c331c030be0dd73e8806fb0c97a2519d63775c2f579a950").publicKey) - assert(channelKeyManager.htlcPoint(channelKeyPath).publicKey == PrivateKey(hex"94eca6eade204d6e753344c347b46bb09067c92b2fe371cf4f8362c1594c8c59").publicKey) - assert(channelKeyManager.commitmentSecret(channelKeyPath, 0).value == ShaChain.shaChainFromSeed(ByteVector32.fromValidHex("64e9d1e9840add3bb02c1525995edd28feea67f1df7a9ee075179e8541adc7a2"), 0xFFFFFFFFFFFFL)) + val channelKeyManager = LocalChannelKeyManager(seed, Block.Testnet3GenesisBlock.hash) + val fundingKeyPath = makeFundingKeyPath(hex"be4fa97c62b9f88437a3be577b31eb48f2165c7bc252194a15ff92d995778cfb", isInitiator = true) + val channelKeys = channelKeyManager.channelKeys(ChannelConfig.standard, fundingKeyPath) + val fundingKey = channelKeys.fundingKey(fundingTxIndex = 0) + + assert(fundingKey.publicKey == PrivateKey(hex"216414970b4216b197a1040367419ad6922f80e8b73ced083e9afe5e6ddd8e4c").publicKey) + assert(channelKeys.revocationBasePoint == PrivateKey(hex"a4e7ab3c54752a3487b3c474467843843f28d3bb9113e65e92056ad45d1e318e").publicKey) + assert(channelKeys.paymentBasePoint == PrivateKey(hex"de24c43d24b8d6bc66b020ac81164206bb577c7924511d4e99431c0d60505012").publicKey) + assert(channelKeys.delayedPaymentBasePoint == PrivateKey(hex"8aa7b8b14a7035540c331c030be0dd73e8806fb0c97a2519d63775c2f579a950").publicKey) + assert(channelKeys.htlcBasePoint == PrivateKey(hex"94eca6eade204d6e753344c347b46bb09067c92b2fe371cf4f8362c1594c8c59").publicKey) + assert(channelKeys.commitmentSecret(0).value == ShaChain.shaChainFromSeed(ByteVector32.fromValidHex("64e9d1e9840add3bb02c1525995edd28feea67f1df7a9ee075179e8541adc7a2"), 0xFFFFFFFFFFFFL)) } test("test vectors (testnet, fundee)") { val seed = ByteVector.fromValidHex("aeb3e9b5642cd4523e9e09164047f60adb413633549c3c6189192921311894d501") - val channelKeyManager = new LocalChannelKeyManager(seed, Block.Testnet3GenesisBlock.hash) - val fundingKeyPath = makefundingKeyPath(hex"06535806c1aa73971ec4877a5e2e684fa636136c073810f190b63eefc58ca488", isInitiator = false) - val fundingPub = channelKeyManager.fundingPublicKey(fundingKeyPath, fundingTxIndex = 0) - - val localParams = TestConstants.Alice.channelParams.copy(fundingKeyPath = fundingKeyPath) - val channelKeyPath = channelKeyManager.keyPath(localParams, ChannelConfig.standard) - - assert(fundingPub.publicKey == PrivateKey(hex"7bb8019c99fcba1c6bd0cc7f3c635c14c658d26751232d6a6350d8b6127d53c3").publicKey) - assert(channelKeyManager.revocationPoint(channelKeyPath).publicKey == PrivateKey(hex"26510db99546c9b08418fe9df2da710a92afa6cc4e5681141610dfb8019052e6").publicKey) - assert(channelKeyManager.paymentPoint(channelKeyPath).publicKey == PrivateKey(hex"0766c93fd06f69287fcc7b343916e678b83942345d4080e83f4c8a061b1a9f4b").publicKey) - assert(channelKeyManager.delayedPaymentPoint(channelKeyPath).publicKey == PrivateKey(hex"094aa052a9647228fd80e42461cae26c04f6cdd1665b816d4660df686915319a").publicKey) - assert(channelKeyManager.htlcPoint(channelKeyPath).publicKey == PrivateKey(hex"8ec62bd03b241a2e522477ae1a9861a668429ab3e443abd2aa0f2f10e2dc2206").publicKey) - assert(channelKeyManager.commitmentSecret(channelKeyPath, 0).value == ShaChain.shaChainFromSeed(ByteVector32.fromValidHex("c49e98202b0fee19f28fd3af60691aaacdd2c09e20896f5fa3ad1b9b70e4879f"), 0xFFFFFFFFFFFFL)) + val channelKeyManager = LocalChannelKeyManager(seed, Block.Testnet3GenesisBlock.hash) + val fundingKeyPath = makeFundingKeyPath(hex"06535806c1aa73971ec4877a5e2e684fa636136c073810f190b63eefc58ca488", isInitiator = false) + val channelKeys = channelKeyManager.channelKeys(ChannelConfig.standard, fundingKeyPath) + val fundingKey = channelKeys.fundingKey(fundingTxIndex = 0) + + assert(fundingKey.publicKey == PrivateKey(hex"7bb8019c99fcba1c6bd0cc7f3c635c14c658d26751232d6a6350d8b6127d53c3").publicKey) + assert(channelKeys.revocationBasePoint == PrivateKey(hex"26510db99546c9b08418fe9df2da710a92afa6cc4e5681141610dfb8019052e6").publicKey) + assert(channelKeys.paymentBasePoint == PrivateKey(hex"0766c93fd06f69287fcc7b343916e678b83942345d4080e83f4c8a061b1a9f4b").publicKey) + assert(channelKeys.delayedPaymentBasePoint == PrivateKey(hex"094aa052a9647228fd80e42461cae26c04f6cdd1665b816d4660df686915319a").publicKey) + assert(channelKeys.htlcBasePoint == PrivateKey(hex"8ec62bd03b241a2e522477ae1a9861a668429ab3e443abd2aa0f2f10e2dc2206").publicKey) + assert(channelKeys.commitmentSecret(0).value == ShaChain.shaChainFromSeed(ByteVector32.fromValidHex("c49e98202b0fee19f28fd3af60691aaacdd2c09e20896f5fa3ad1b9b70e4879f"), 0xFFFFFFFFFFFFL)) } test("test vectors (mainnet, funder)") { val seed = ByteVector.fromValidHex("d8d5431487c2b19ee6486aad6c3bdfb99d10b727bade7fa848e2ab7901c15bff01") - val channelKeyManager = new LocalChannelKeyManager(seed, Block.LivenetGenesisBlock.hash) - val fundingKeyPath = makefundingKeyPath(hex"ec1c41cd6be2b6e4ef46c1107f6c51fbb2066d7e1f7720bde4715af233ae1322", isInitiator = true) - val fundingPub = channelKeyManager.fundingPublicKey(fundingKeyPath, fundingTxIndex = 0) - - val localParams = TestConstants.Alice.channelParams.copy(fundingKeyPath = fundingKeyPath) - val channelKeyPath = channelKeyManager.keyPath(localParams, ChannelConfig.standard) - - assert(fundingPub.publicKey == PrivateKey(hex"b97c04796850e9d74a06c9d7230d85e2ecca3598b162ddf902895ece820c8f09").publicKey) - assert(channelKeyManager.revocationPoint(channelKeyPath).publicKey == PrivateKey(hex"ee13db7f2d7e672f21395111ee169af8462c6e8d1a6a78d808f7447b27155ffb").publicKey) - assert(channelKeyManager.paymentPoint(channelKeyPath).publicKey == PrivateKey(hex"7fc18e4c925bf3c5a83411eac7f234f0c5eaef9a8022b22ec6e3272ae329e17e").publicKey) - assert(channelKeyManager.delayedPaymentPoint(channelKeyPath).publicKey == PrivateKey(hex"c0d9a3e3601d79b11b948db9d672fcddafcb9a3c0873c6a738bb09087ea2bfc6").publicKey) - assert(channelKeyManager.htlcPoint(channelKeyPath).publicKey == PrivateKey(hex"bd3ba7068d131a9ab47f33202d532c5824cc5fc35a9adada3644ac2994372228").publicKey) - assert(channelKeyManager.commitmentSecret(channelKeyPath, 0).value == ShaChain.shaChainFromSeed(ByteVector32.fromValidHex("7799de34239f97837a12191f5b60e766e32e9704bb84b0f12b539e9bf6a0dc2a"), 0xFFFFFFFFFFFFL)) + val channelKeyManager = LocalChannelKeyManager(seed, Block.LivenetGenesisBlock.hash) + val fundingKeyPath = makeFundingKeyPath(hex"ec1c41cd6be2b6e4ef46c1107f6c51fbb2066d7e1f7720bde4715af233ae1322", isInitiator = true) + val channelKeys = channelKeyManager.channelKeys(ChannelConfig.standard, fundingKeyPath) + val fundingKey = channelKeys.fundingKey(fundingTxIndex = 0) + + assert(fundingKey.publicKey == PrivateKey(hex"b97c04796850e9d74a06c9d7230d85e2ecca3598b162ddf902895ece820c8f09").publicKey) + assert(channelKeys.revocationBasePoint == PrivateKey(hex"ee13db7f2d7e672f21395111ee169af8462c6e8d1a6a78d808f7447b27155ffb").publicKey) + assert(channelKeys.paymentBasePoint == PrivateKey(hex"7fc18e4c925bf3c5a83411eac7f234f0c5eaef9a8022b22ec6e3272ae329e17e").publicKey) + assert(channelKeys.delayedPaymentBasePoint == PrivateKey(hex"c0d9a3e3601d79b11b948db9d672fcddafcb9a3c0873c6a738bb09087ea2bfc6").publicKey) + assert(channelKeys.htlcBasePoint == PrivateKey(hex"bd3ba7068d131a9ab47f33202d532c5824cc5fc35a9adada3644ac2994372228").publicKey) + assert(channelKeys.commitmentSecret(0).value == ShaChain.shaChainFromSeed(ByteVector32.fromValidHex("7799de34239f97837a12191f5b60e766e32e9704bb84b0f12b539e9bf6a0dc2a"), 0xFFFFFFFFFFFFL)) } test("test vectors (mainnet, fundee)") { val seed = ByteVector.fromValidHex("4b809dd593b36131c454d60c2f7bdfd49d12ec455e5b657c47a9ca0f5dfc5eef01") - val channelKeyManager = new LocalChannelKeyManager(seed, Block.LivenetGenesisBlock.hash) - val fundingKeyPath = makefundingKeyPath(hex"2b4f045be5303d53f9d3a84a1e70c12251168dc29f300cf9cece0ec85cd8182b", isInitiator = false) - val fundingPub = channelKeyManager.fundingPublicKey(fundingKeyPath, fundingTxIndex = 0) - - val localParams = TestConstants.Alice.channelParams.copy(fundingKeyPath = fundingKeyPath) - val channelKeyPath = channelKeyManager.keyPath(localParams, ChannelConfig.standard) - - assert(fundingPub.publicKey == PrivateKey(hex"46a4e818615a48a99ce9f6bd73eea07d5822dcfcdff18081ea781d4e5e6c036c").publicKey) - assert(channelKeyManager.revocationPoint(channelKeyPath).publicKey == PrivateKey(hex"c2cd9e2f9f8203f16b1751bd252285bb2e7fc4688857d620467b99645ebdfbe6").publicKey) - assert(channelKeyManager.paymentPoint(channelKeyPath).publicKey == PrivateKey(hex"1e4d3527788b39dc8ebc0ae6368a67e92eff55a43bea8e93054338ca850fa340").publicKey) - assert(channelKeyManager.delayedPaymentPoint(channelKeyPath).publicKey == PrivateKey(hex"6bc30b0852fbc653451662a1ff6ad530f311d58b5e5661b541eb57dba8206937").publicKey) - assert(channelKeyManager.htlcPoint(channelKeyPath).publicKey == PrivateKey(hex"b1be27b5232e3bc5d6a261949b4ee68d96fa61f481998d36342e2ad99444cf8a").publicKey) - assert(channelKeyManager.commitmentSecret(channelKeyPath, 0).value == ShaChain.shaChainFromSeed(ByteVector32.fromValidHex("eeb3bad6808e8bb5f1774581ccf64aa265fef38eca80a1463d6310bb801b3ba7"), 0xFFFFFFFFFFFFL)) + val channelKeyManager = LocalChannelKeyManager(seed, Block.LivenetGenesisBlock.hash) + val fundingKeyPath = makeFundingKeyPath(hex"2b4f045be5303d53f9d3a84a1e70c12251168dc29f300cf9cece0ec85cd8182b", isInitiator = false) + val channelKeys = channelKeyManager.channelKeys(ChannelConfig.standard, fundingKeyPath) + val fundingKey = channelKeys.fundingKey(fundingTxIndex = 0) + + assert(fundingKey.publicKey == PrivateKey(hex"46a4e818615a48a99ce9f6bd73eea07d5822dcfcdff18081ea781d4e5e6c036c").publicKey) + assert(channelKeys.revocationBasePoint == PrivateKey(hex"c2cd9e2f9f8203f16b1751bd252285bb2e7fc4688857d620467b99645ebdfbe6").publicKey) + assert(channelKeys.paymentBasePoint == PrivateKey(hex"1e4d3527788b39dc8ebc0ae6368a67e92eff55a43bea8e93054338ca850fa340").publicKey) + assert(channelKeys.delayedPaymentBasePoint == PrivateKey(hex"6bc30b0852fbc653451662a1ff6ad530f311d58b5e5661b541eb57dba8206937").publicKey) + assert(channelKeys.htlcBasePoint == PrivateKey(hex"b1be27b5232e3bc5d6a261949b4ee68d96fa61f481998d36342e2ad99444cf8a").publicKey) + assert(channelKeys.commitmentSecret(0).value == ShaChain.shaChainFromSeed(ByteVector32.fromValidHex("eeb3bad6808e8bb5f1774581ccf64aa265fef38eca80a1463d6310bb801b3ba7"), 0xFFFFFFFFFFFFL)) + } + + test("derivation of local key from base key and per-commitment-point") { + val baseKey: PrivateKey = PrivateKey(hex"000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f") + val commitmentPoint = PublicKey(hex"025f7117a78150fe2ef97db7cfc83bd57b2e2c0d0dd25eaf467a4a1c2a45ce1486") + val localprivkey = ChannelKeys.derivePerCommitmentKey(baseKey, commitmentPoint) + assert(localprivkey.value == ByteVector32(hex"cbced912d3b21bf196a766651e436aff192362621ce317704ea2f75d87e7be0f")) + } + + test("derivation remote public key from base point and per-commitment-point") { + val basePoint = PublicKey(hex"036d6caac248af96f6afa7f904f550253a0f3ef3f5aa2fe6838a95b216691468e2") + val commitmentPoint = PublicKey(hex"025f7117a78150fe2ef97db7cfc83bd57b2e2c0d0dd25eaf467a4a1c2a45ce1486") + val publicKey = ChannelKeys.remotePerCommitmentPublicKey(basePoint, commitmentPoint) + assert(publicKey.value == hex"0235f2dbfaa89b57ec7b055afe29849ef7ddfeb1cefdb9ebdc43f5494984db29e5") + } + + test("derivation of revocation key from base key and per-commitment-secret") { + val baseKey: PrivateKey = PrivateKey(hex"000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f") + val commitmentSecret: PrivateKey = PrivateKey(hex"1f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100") + val revocationKey = ChannelKeys.revocationKey(baseKey, commitmentSecret) + assert(revocationKey.value == ByteVector32(hex"d09ffff62ddb2297ab000cc85bcb4283fdeb6aa052affbc9dddcf33b61078110")) + } + + test("derivation of revocation public key from base point and per-commitment-point") { + val basePoint = PublicKey(hex"036d6caac248af96f6afa7f904f550253a0f3ef3f5aa2fe6838a95b216691468e2") + val commitmentPoint = PublicKey(hex"025f7117a78150fe2ef97db7cfc83bd57b2e2c0d0dd25eaf467a4a1c2a45ce1486") + val revocationKey = ChannelKeys.revocationPublicKey(basePoint, commitmentPoint) + assert(revocationKey.value == hex"02916e326636d19c33f13e8c0c3a03dd157f332f3e99c317c141dd865eb01f8ff0") } test("keep the same channel seed after a migration from the old seed.dat file") { @@ -142,4 +181,5 @@ class LocalChannelKeyManagerSpec extends AnyFunSuite { val channelSeedContent = ByteVector(Files.readAllBytes(channelSeedDatFile.toPath)) assert(seed == channelSeedContent) } + } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/keymanager/LocalNodeKeyManagerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/keymanager/LocalNodeKeyManagerSpec.scala index c2aa787ac7..de4c3256d3 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/keymanager/LocalNodeKeyManagerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/keymanager/LocalNodeKeyManagerSpec.scala @@ -17,9 +17,9 @@ package fr.acinq.eclair.crypto.keymanager import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey -import fr.acinq.bitcoin.scalacompat.DeterministicWallet.KeyPath import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, Crypto} import fr.acinq.eclair.Setup.Seeds +import fr.acinq.eclair.channel.ChannelConfig import fr.acinq.eclair.{NodeParams, TestUtils} import org.scalatest.funsuite.AnyFunSuite import scodec.bits._ @@ -39,15 +39,15 @@ class LocalNodeKeyManagerSpec extends AnyFunSuite { test("generate different node ids from the same seed on different chains") { val seed = hex"17b086b228025fa8f4416324b6ba2ec36e68570ae2fc3d392520969f2a9d0c1501" - val nodeKeyManager1 = new LocalNodeKeyManager(seed, Block.Testnet3GenesisBlock.hash) - val nodeKeyManager2 = new LocalNodeKeyManager(seed, Block.LivenetGenesisBlock.hash) - val channelKeyManager1 = new LocalChannelKeyManager(seed, Block.Testnet3GenesisBlock.hash) - val channelKeyManager2 = new LocalChannelKeyManager(seed, Block.LivenetGenesisBlock.hash) + val nodeKeyManager1 = LocalNodeKeyManager(seed, Block.Testnet3GenesisBlock.hash) + val nodeKeyManager2 = LocalNodeKeyManager(seed, Block.LivenetGenesisBlock.hash) assert(nodeKeyManager1.nodeId != nodeKeyManager2.nodeId) - val keyPath = KeyPath(1L :: Nil) - assert(channelKeyManager1.fundingPublicKey(keyPath, fundingTxIndex = 0) != channelKeyManager2.fundingPublicKey(keyPath, fundingTxIndex = 0)) - assert(channelKeyManager1.fundingPublicKey(keyPath, fundingTxIndex = 42) != channelKeyManager2.fundingPublicKey(keyPath, fundingTxIndex = 42)) - assert(channelKeyManager1.commitmentPoint(keyPath, 1) != channelKeyManager2.commitmentPoint(keyPath, 1)) + val channelKeyManager = LocalChannelKeyManager(seed, Block.Testnet3GenesisBlock.hash) + val channelKeys1 = channelKeyManager.channelKeys(ChannelConfig.standard, channelKeyManager.newFundingKeyPath(isChannelOpener = true)) + val channelKeys2 = channelKeyManager.channelKeys(ChannelConfig.standard, channelKeyManager.newFundingKeyPath(isChannelOpener = true)) + assert(channelKeys1.fundingKey(fundingTxIndex = 0) != channelKeys2.fundingKey(fundingTxIndex = 0)) + assert(channelKeys1.fundingKey(fundingTxIndex = 42) != channelKeys2.fundingKey(fundingTxIndex = 42)) + assert(channelKeys1.commitmentPoint(1) != channelKeys2.commitmentPoint(1)) } test("keep the same node seed after a migration from the old seed.dat file") { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/AuditDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/AuditDbSpec.scala index d79665a0a0..aff010d21d 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/AuditDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/AuditDbSpec.scala @@ -31,7 +31,6 @@ import fr.acinq.eclair.db.sqlite.SqliteAuditDb import fr.acinq.eclair.payment.Bolt11Invoice.ExtraHop import fr.acinq.eclair.payment._ import fr.acinq.eclair.router.Announcements -import fr.acinq.eclair.transactions.Transactions.PlaceHolderPubKey import fr.acinq.eclair.wire.protocol.Error import org.scalatest.Tag import org.scalatest.funsuite.AnyFunSuite @@ -65,7 +64,7 @@ class AuditDbSpec extends AnyFunSuite { val db = dbs.audit val now = TimestampMilli.now() - val e1 = PaymentSent(ZERO_UUID, randomBytes32(), randomBytes32(), 40000 msat, randomKey().publicKey, PaymentSent.PartialPayment(ZERO_UUID, 42000 msat, 1000 msat, randomBytes32(), None) :: Nil) + val e1 = PaymentSent(ZERO_UUID, randomBytes32(), randomBytes32(), 40000 msat, randomKey().publicKey, PaymentSent.PartialPayment(ZERO_UUID, 42000 msat, 1000 msat, randomBytes32(), None) :: Nil, None) val pp2a = PaymentReceived.PartialPayment(42000 msat, randomBytes32()) val pp2b = PaymentReceived.PartialPayment(42100 msat, randomBytes32()) val e2 = PaymentReceived(randomBytes32(), pp2a :: pp2b :: Nil) @@ -75,9 +74,9 @@ class AuditDbSpec extends AnyFunSuite { val e4c = TransactionConfirmed(randomBytes32(), randomKey().publicKey, Transaction(2, Nil, TxOut(500 sat, hex"1234") :: Nil, 0)) val pp5a = PaymentSent.PartialPayment(UUID.randomUUID(), 42000 msat, 1000 msat, randomBytes32(), None, timestamp = 0 unixms) val pp5b = PaymentSent.PartialPayment(UUID.randomUUID(), 42100 msat, 900 msat, randomBytes32(), None, timestamp = 1 unixms) - val e5 = PaymentSent(UUID.randomUUID(), randomBytes32(), randomBytes32(), 84100 msat, randomKey().publicKey, pp5a :: pp5b :: Nil) + val e5 = PaymentSent(UUID.randomUUID(), randomBytes32(), randomBytes32(), 84100 msat, randomKey().publicKey, pp5a :: pp5b :: Nil, None) val pp6 = PaymentSent.PartialPayment(UUID.randomUUID(), 42000 msat, 1000 msat, randomBytes32(), None, timestamp = now + 10.minutes) - val e6 = PaymentSent(UUID.randomUUID(), randomBytes32(), randomBytes32(), 42000 msat, randomKey().publicKey, pp6 :: Nil) + val e6 = PaymentSent(UUID.randomUUID(), randomBytes32(), randomBytes32(), 42000 msat, randomKey().publicKey, pp6 :: Nil, None) val e7 = ChannelEvent(randomBytes32(), randomKey().publicKey, 456123000 sat, isChannelOpener = true, isPrivate = false, ChannelEvent.EventType.Closed(MutualClose(null))) val e8 = ChannelErrorOccurred(null, randomBytes32(), randomKey().publicKey, LocalError(new RuntimeException("oops")), isFatal = true) val e9 = ChannelErrorOccurred(null, randomBytes32(), randomKey().publicKey, RemoteError(Error(randomBytes32(), "remote oops")), isFatal = true) @@ -235,10 +234,10 @@ class AuditDbSpec extends AnyFunSuite { val dbs = TestSqliteDatabases() - val ps = PaymentSent(UUID.randomUUID(), randomBytes32(), randomBytes32(), 42000 msat, PrivateKey(ByteVector32.One).publicKey, PaymentSent.PartialPayment(UUID.randomUUID(), 42000 msat, 1000 msat, randomBytes32(), None) :: Nil) + val ps = PaymentSent(UUID.randomUUID(), randomBytes32(), randomBytes32(), 42000 msat, PrivateKey(ByteVector32.One).publicKey, PaymentSent.PartialPayment(UUID.randomUUID(), 42000 msat, 1000 msat, randomBytes32(), None) :: Nil, None) val pp1 = PaymentSent.PartialPayment(UUID.randomUUID(), 42001 msat, 1001 msat, randomBytes32(), None) val pp2 = PaymentSent.PartialPayment(UUID.randomUUID(), 42002 msat, 1002 msat, randomBytes32(), None) - val ps1 = PaymentSent(UUID.randomUUID(), randomBytes32(), randomBytes32(), 84003 msat, PrivateKey(ByteVector32.One).publicKey, pp1 :: pp2 :: Nil) + val ps1 = PaymentSent(UUID.randomUUID(), randomBytes32(), randomBytes32(), 84003 msat, PrivateKey(ByteVector32.One).publicKey, pp1 :: pp2 :: Nil, None) val e1 = ChannelErrorOccurred(null, randomBytes32(), randomKey().publicKey, LocalError(new RuntimeException("oops")), isFatal = true) val e2 = ChannelErrorOccurred(null, randomBytes32(), randomKey().publicKey, RemoteError(Error(randomBytes32(), "remote oops")), isFatal = true) @@ -350,7 +349,7 @@ class AuditDbSpec extends AnyFunSuite { val pp1 = PaymentSent.PartialPayment(UUID.randomUUID(), 500 msat, 10 msat, randomBytes32(), None, 100 unixms) val pp2 = PaymentSent.PartialPayment(UUID.randomUUID(), 600 msat, 5 msat, randomBytes32(), None, 110 unixms) - val ps1 = PaymentSent(UUID.randomUUID(), randomBytes32(), randomBytes32(), 1100 msat, PrivateKey(ByteVector32.One).publicKey, pp1 :: pp2 :: Nil) + val ps1 = PaymentSent(UUID.randomUUID(), randomBytes32(), randomBytes32(), 1100 msat, PrivateKey(ByteVector32.One).publicKey, pp1 :: pp2 :: Nil, None) val relayed1 = ChannelPaymentRelayed(600 msat, 500 msat, randomBytes32(), randomBytes32(), randomBytes32(), 105 unixms, 105 unixms) val relayed2 = ChannelPaymentRelayed(650 msat, 500 msat, randomBytes32(), randomBytes32(), randomBytes32(), 115 unixms, 115 unixms) @@ -424,7 +423,7 @@ class AuditDbSpec extends AnyFunSuite { val ps2 = PaymentSent(UUID.randomUUID(), randomBytes32(), randomBytes32(), 1100 msat, randomKey().publicKey, Seq( PaymentSent.PartialPayment(UUID.randomUUID(), 500 msat, 10 msat, randomBytes32(), None, 160 unixms), PaymentSent.PartialPayment(UUID.randomUUID(), 600 msat, 5 msat, randomBytes32(), None, 165 unixms) - )) + ), None) val relayed3 = TrampolinePaymentRelayed(randomBytes32(), Seq(PaymentRelayed.IncomingPart(450 msat, randomBytes32(), 150 unixms), PaymentRelayed.IncomingPart(500 msat, randomBytes32(), 150 unixms)), Seq(PaymentRelayed.OutgoingPart(800 msat, randomBytes32(), 150 unixms)), randomKey().publicKey, 700 msat) postMigrationDb.add(ps2) assert(postMigrationDb.listSent(155 unixms, 200 unixms) == Seq(ps2)) @@ -435,9 +434,9 @@ class AuditDbSpec extends AnyFunSuite { } test("migrate audit database v4 -> current") { - val relayed1 = ChannelPaymentRelayed(600 msat, 500 msat, randomBytes32(), randomBytes32(), randomBytes32(), 105 unixms, 105 unixms) - val relayed2 = TrampolinePaymentRelayed(randomBytes32(), Seq(PaymentRelayed.IncomingPart(300 msat, randomBytes32(), 110 unixms), PaymentRelayed.IncomingPart(350 msat, randomBytes32(), 110 unixms)), Seq(PaymentRelayed.OutgoingPart(600 msat, randomBytes32(), 110 unixms)), PlaceHolderPubKey, 0 msat) + // We weren't properly storing the outgoing trampoline node, which makes this useless, so we'll skip it when migrating. + val relayed2 = TrampolinePaymentRelayed(randomBytes32(), Seq(PaymentRelayed.IncomingPart(300 msat, randomBytes32(), 110 unixms), PaymentRelayed.IncomingPart(350 msat, randomBytes32(), 110 unixms)), Seq(PaymentRelayed.OutgoingPart(600 msat, randomBytes32(), 110 unixms)), randomKey().publicKey, 0 msat) forAllDbs { case dbs: TestPgDatabases => @@ -510,7 +509,7 @@ class AuditDbSpec extends AnyFunSuite { postCheck = connection => { val migratedDb = dbs.audit - assert(migratedDb.listRelayed(100 unixms, 120 unixms) == Seq(relayed1, relayed2)) + assert(migratedDb.listRelayed(100 unixms, 120 unixms) == Seq(relayed1)) val postMigrationDb = new PgAuditDb()(dbs.datasource) using(connection.createStatement()) { statement => @@ -518,7 +517,7 @@ class AuditDbSpec extends AnyFunSuite { } val relayed3 = TrampolinePaymentRelayed(randomBytes32(), Seq(PaymentRelayed.IncomingPart(450 msat, randomBytes32(), 150 unixms), PaymentRelayed.IncomingPart(500 msat, randomBytes32(), 150 unixms)), Seq(PaymentRelayed.OutgoingPart(800 msat, randomBytes32(), 150 unixms)), randomKey().publicKey, 700 msat) postMigrationDb.add(relayed3) - assert(postMigrationDb.listRelayed(100 unixms, 160 unixms) == Seq(relayed1, relayed2, relayed3)) + assert(postMigrationDb.listRelayed(100 unixms, 160 unixms) == Seq(relayed1, relayed3)) } ) case dbs: TestSqliteDatabases => @@ -593,7 +592,7 @@ class AuditDbSpec extends AnyFunSuite { using(connection.createStatement()) { statement => assert(getVersion(statement, "audit").contains(SqliteAuditDb.CURRENT_VERSION)) } - assert(migratedDb.listRelayed(100 unixms, 120 unixms) == Seq(relayed1, relayed2)) + assert(migratedDb.listRelayed(100 unixms, 120 unixms) == Seq(relayed1)) val postMigrationDb = new SqliteAuditDb(connection) using(connection.createStatement()) { statement => @@ -601,7 +600,7 @@ class AuditDbSpec extends AnyFunSuite { } val relayed3 = TrampolinePaymentRelayed(randomBytes32(), Seq(PaymentRelayed.IncomingPart(450 msat, randomBytes32(), 150 unixms), PaymentRelayed.IncomingPart(500 msat, randomBytes32(), 150 unixms)), Seq(PaymentRelayed.OutgoingPart(800 msat, randomBytes32(), 150 unixms)), randomKey().publicKey, 700 msat) postMigrationDb.add(relayed3) - assert(postMigrationDb.listRelayed(100 unixms, 160 unixms) == Seq(relayed1, relayed2, relayed3)) + assert(postMigrationDb.listRelayed(100 unixms, 160 unixms) == Seq(relayed1, relayed3)) } ) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/ChannelsDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/ChannelsDbSpec.scala index b64bc10768..86a328da73 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/ChannelsDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/ChannelsDbSpec.scala @@ -17,27 +17,26 @@ package fr.acinq.eclair.db import com.softwaremill.quicklens._ -import fr.acinq.bitcoin.scalacompat.ByteVector32 -import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} -import fr.acinq.eclair.TestDatabases.{TestPgDatabases, TestSqliteDatabases, migrationCheck} -import fr.acinq.eclair.db.ChannelsDbSpec.{getPgTimestamp, getTimestamp, testCases} +import fr.acinq.bitcoin.scalacompat.Crypto.PrivateKey +import fr.acinq.bitcoin.scalacompat.{ByteVector32, SatoshiLong} +import fr.acinq.eclair.TestDatabases.{TestPgDatabases, TestSqliteDatabases} +import fr.acinq.eclair.TestUtils.randomTxId +import fr.acinq.eclair.channel._ +import fr.acinq.eclair.db.ChannelsDbSpec.getTimestamp import fr.acinq.eclair.db.DbEventHandler.ChannelEvent import fr.acinq.eclair.db.jdbc.JdbcUtils.using -import fr.acinq.eclair.db.pg.PgUtils.{getVersion, setVersion} -import fr.acinq.eclair.db.pg.{PgChannelsDb, PgUtils} +import fr.acinq.eclair.db.pg.PgChannelsDb import fr.acinq.eclair.db.sqlite.SqliteChannelsDb import fr.acinq.eclair.db.sqlite.SqliteUtils.ExtendedResultSet._ -import fr.acinq.eclair.wire.internal.channel.ChannelCodecs.channelDataCodec import fr.acinq.eclair.wire.internal.channel.ChannelCodecsSpec -import fr.acinq.eclair.{Alias, CltvExpiry, TestDatabases, randomBytes32, randomKey, randomLong} +import fr.acinq.eclair.{Alias, CltvExpiry, MilliSatoshiLong, TestDatabases, randomBytes32, randomLong} import org.scalatest.funsuite.AnyFunSuite -import scodec.bits.ByteVector +import scodec.bits.{ByteVector, HexStringSyntax} -import java.sql.{Connection, SQLException} +import java.sql.Connection import java.util.concurrent.Executors import scala.concurrent.duration._ import scala.concurrent.{Await, ExecutionContext, ExecutionContextExecutor, Future} -import scala.util.Random class ChannelsDbSpec extends AnyFunSuite { @@ -59,7 +58,7 @@ class ChannelsDbSpec extends AnyFunSuite { val db = dbs.channels val channel1 = ChannelCodecsSpec.normal - val channel2a = ChannelCodecsSpec.normal.modify(_.commitments.params.channelId).setTo(randomBytes32()) + val channel2a = ChannelCodecsSpec.normal.modify(_.commitments.channelParams.channelId).setTo(randomBytes32()) val channel2b = channel2a.modify(_.aliases.remoteAlias_opt).setTo(Some(Alias(randomLong()))) val commitNumber = 42 @@ -68,8 +67,6 @@ class ChannelsDbSpec extends AnyFunSuite { val paymentHash2 = ByteVector32(ByteVector.fill(32)(1)) val cltvExpiry2 = CltvExpiry(656) - intercept[SQLException](db.addHtlcInfo(channel1.channelId, commitNumber, paymentHash1, cltvExpiry1)) // no related channel - assert(db.listLocalChannels().isEmpty) db.addOrUpdateChannel(channel1) db.addOrUpdateChannel(channel1) @@ -89,16 +86,19 @@ class ChannelsDbSpec extends AnyFunSuite { assert(db.listHtlcInfos(channel1.channelId, commitNumber + 1).isEmpty) assert(db.listClosedChannels(None, None).isEmpty) - db.removeChannel(channel1.channelId) + val closed1 = DATA_CLOSED(channel1.channelId, channel1.remoteNodeId, randomTxId(), 3, 2, channel1.channelParams.localParams.fundingKeyPath.toString(), channel1.channelParams.channelFeatures.toString, isChannelOpener = true, "anchor_outputs", announced = true, 100_000 sat, randomTxId(), "local-close", hex"deadbeef", 61_000_500 msat, 40_000_000 msat, 60_000 sat) + db.removeChannel(channel1.channelId, Some(closed1)) assert(db.getChannel(channel1.channelId).isEmpty) assert(db.listLocalChannels() == List(channel2b)) - assert(db.listClosedChannels(None, None) == List(channel1)) - assert(db.listClosedChannels(Some(channel1.remoteNodeId), None) == List(channel1)) + assert(db.listClosedChannels(None, None) == List(closed1)) + assert(db.listClosedChannels(Some(channel1.remoteNodeId), None) == List(closed1)) assert(db.listClosedChannels(Some(PrivateKey(randomBytes32()).publicKey), None).isEmpty) - db.removeChannel(channel2b.channelId) + // If no closing data is provided, the channel won't be backed-up in the closed_channels table. + db.removeChannel(channel2b.channelId, None) assert(db.getChannel(channel2b.channelId).isEmpty) assert(db.listLocalChannels().isEmpty) + assert(db.listClosedChannels(None, None) == Seq(closed1)) } } @@ -107,7 +107,7 @@ class ChannelsDbSpec extends AnyFunSuite { val db = dbs.channels val channel1 = ChannelCodecsSpec.normal - val channel2 = ChannelCodecsSpec.normal.modify(_.commitments.params.channelId).setTo(randomBytes32()) + val channel2 = ChannelCodecsSpec.normal.modify(_.commitments.channelParams.channelId).setTo(randomBytes32()) db.addOrUpdateChannel(channel1) db.addOrUpdateChannel(channel2) @@ -121,7 +121,7 @@ class ChannelsDbSpec extends AnyFunSuite { db.markHtlcInfosForRemoval(channel1.channelId, commitNumberSplice1) db.addHtlcInfo(channel1.channelId, 51, randomBytes32(), CltvExpiry(561)) db.addHtlcInfo(channel1.channelId, 52, randomBytes32(), CltvExpiry(561)) - db.removeChannel(channel1.channelId) + db.removeChannel(channel1.channelId, None) // The second channel has two splice transactions. db.addHtlcInfo(channel2.channelId, 48, randomBytes32(), CltvExpiry(561)) @@ -169,7 +169,7 @@ class ChannelsDbSpec extends AnyFunSuite { val channelIds = (0 until 10).map(_ => randomBytes32()).toList val futures = for (i <- 0 until 10000) yield { val channelId = channelIds(i % channelIds.size) - Future(db.addOrUpdateChannel(channel.modify(_.commitments.params.channelId).setTo(channelId))) + Future(db.addOrUpdateChannel(channel.modify(_.commitments.channelParams.channelId).setTo(channelId))) Future(db.updateChannelMeta(channelId, ChannelEvent.EventType.PaymentSent)) } val res = Future.sequence(futures) @@ -182,7 +182,7 @@ class ChannelsDbSpec extends AnyFunSuite { val db = dbs.channels val channel1 = ChannelCodecsSpec.normal - val channel2 = channel1.modify(_.commitments.params.channelId).setTo(randomBytes32()) + val channel2 = channel1.modify(_.commitments.channelParams.channelId).setTo(randomBytes32()) // first we add channels db.addOrUpdateChannel(channel1) @@ -192,7 +192,6 @@ class ChannelsDbSpec extends AnyFunSuite { assert(getTimestamp(dbs, channel1.channelId, "last_payment_sent_timestamp").isEmpty) assert(getTimestamp(dbs, channel1.channelId, "last_payment_received_timestamp").isEmpty) assert(getTimestamp(dbs, channel1.channelId, "last_connected_timestamp").nonEmpty) - assert(getTimestamp(dbs, channel1.channelId, "closed_timestamp").isEmpty) db.updateChannelMeta(channel1.channelId, ChannelEvent.EventType.Created) assert(getTimestamp(dbs, channel1.channelId, "created_timestamp").nonEmpty) @@ -206,199 +205,15 @@ class ChannelsDbSpec extends AnyFunSuite { db.updateChannelMeta(channel1.channelId, ChannelEvent.EventType.Connected) assert(getTimestamp(dbs, channel1.channelId, "last_connected_timestamp").nonEmpty) - db.removeChannel(channel1.channelId) - assert(getTimestamp(dbs, channel1.channelId, "closed_timestamp").nonEmpty) + db.removeChannel(channel1.channelId, None) assert(getTimestamp(dbs, channel2.channelId, "created_timestamp").nonEmpty) assert(getTimestamp(dbs, channel2.channelId, "last_payment_sent_timestamp").isEmpty) assert(getTimestamp(dbs, channel2.channelId, "last_payment_received_timestamp").isEmpty) assert(getTimestamp(dbs, channel2.channelId, "last_connected_timestamp").nonEmpty) - assert(getTimestamp(dbs, channel2.channelId, "closed_timestamp").isEmpty) - } - } - - test("migrate sqlite channel database v1 -> current") { - forAllDbs { - case _: TestPgDatabases => // no migration - case dbs: TestSqliteDatabases => - val sqlite = dbs.connection - - // create a v1 channels database - using(sqlite.createStatement()) { statement => - statement.execute("PRAGMA foreign_keys = ON") - statement.executeUpdate("CREATE TABLE IF NOT EXISTS local_channels (channel_id BLOB NOT NULL PRIMARY KEY, data BLOB NOT NULL)") - statement.executeUpdate("CREATE TABLE IF NOT EXISTS htlc_infos (channel_id BLOB NOT NULL, commitment_number BLOB NOT NULL, payment_hash BLOB NOT NULL, cltv_expiry INTEGER NOT NULL, FOREIGN KEY(channel_id) REFERENCES local_channels(channel_id))") - statement.executeUpdate("CREATE INDEX IF NOT EXISTS htlc_infos_idx ON htlc_infos(channel_id, commitment_number)") - setVersion(statement, "channels", 1) - } - - // insert data - for (testCase <- testCases) { - using(sqlite.prepareStatement("INSERT INTO local_channels VALUES (?, ?)")) { statement => - statement.setBytes(1, testCase.channelId.toArray) - statement.setBytes(2, testCase.data.toArray) - statement.executeUpdate() - } - for (commitmentNumber <- testCase.commitmentNumbers) { - using(sqlite.prepareStatement("INSERT INTO htlc_infos (channel_id, commitment_number, payment_hash, cltv_expiry) VALUES (?, ?, ?, ?)")) { statement => - statement.setBytes(1, testCase.channelId.toArray) - statement.setLong(2, commitmentNumber) - statement.setBytes(3, randomBytes32().toArray) - statement.setLong(4, 500000 + Random.nextInt(500000)) - statement.executeUpdate() - } - } - } - - // check that db migration works - val targetVersion = SqliteChannelsDb.CURRENT_VERSION - val db = new SqliteChannelsDb(sqlite) - using(sqlite.createStatement()) { statement => - assert(getVersion(statement, "channels").contains(targetVersion)) - } - assert(db.listLocalChannels().size == testCases.size) - for (testCase <- testCases) { - db.updateChannelMeta(testCase.channelId, ChannelEvent.EventType.Created) // this call must not fail - for (commitmentNumber <- testCase.commitmentNumbers) { - assert(db.listHtlcInfos(testCase.channelId, commitmentNumber).size == testCase.commitmentNumbers.count(_ == commitmentNumber)) - } - } - } - } - - test("migrate channel database v2 -> current") { - def postCheck(channelsDb: ChannelsDb): Unit = { - assert(channelsDb.listLocalChannels().size == testCases.filterNot(_.isClosed).size) - for (testCase <- testCases.filterNot(_.isClosed)) { - channelsDb.updateChannelMeta(testCase.channelId, ChannelEvent.EventType.Created) // this call must not fail - for (commitmentNumber <- testCase.commitmentNumbers) { - assert(channelsDb.listHtlcInfos(testCase.channelId, commitmentNumber).size == testCase.commitmentNumbers.count(_ == commitmentNumber)) - } - } - } - - forAllDbs { - case dbs: TestPgDatabases => - migrationCheck( - dbs = dbs, - initializeTables = connection => { - // initialize a v2 database - using(connection.createStatement()) { statement => - statement.executeUpdate("CREATE TABLE IF NOT EXISTS local_channels (channel_id TEXT NOT NULL PRIMARY KEY, data BYTEA NOT NULL, is_closed BOOLEAN NOT NULL DEFAULT FALSE)") - statement.executeUpdate("CREATE TABLE IF NOT EXISTS htlc_infos (channel_id TEXT NOT NULL, commitment_number TEXT NOT NULL, payment_hash TEXT NOT NULL, cltv_expiry BIGINT NOT NULL, FOREIGN KEY(channel_id) REFERENCES local_channels(channel_id))") - statement.executeUpdate("CREATE INDEX IF NOT EXISTS htlc_infos_idx ON htlc_infos(channel_id, commitment_number)") - setVersion(statement, "channels", 2) - } - // insert data - testCases.foreach { testCase => - using(connection.prepareStatement("INSERT INTO local_channels (channel_id, data, is_closed) VALUES (?, ?, ?)")) { statement => - statement.setString(1, testCase.channelId.toHex) - statement.setBytes(2, testCase.data.toArray) - statement.setBoolean(3, testCase.isClosed) - statement.executeUpdate() - for (commitmentNumber <- testCase.commitmentNumbers) { - using(connection.prepareStatement("INSERT INTO htlc_infos (channel_id, commitment_number, payment_hash, cltv_expiry) VALUES (?, ?, ?, ?)")) { statement => - statement.setString(1, testCase.channelId.toHex) - statement.setLong(2, commitmentNumber) - statement.setString(3, randomBytes32().toHex) - statement.setLong(4, 500000 + Random.nextInt(500000)) - statement.executeUpdate() - } - } - } - } - }, - dbName = PgChannelsDb.DB_NAME, - targetVersion = PgChannelsDb.CURRENT_VERSION, - postCheck = _ => postCheck(dbs.channels) - ) - case dbs: TestSqliteDatabases => - migrationCheck( - dbs = dbs, - initializeTables = connection => { - // create a v2 channels database - using(connection.createStatement()) { statement => - statement.execute("PRAGMA foreign_keys = ON") - statement.executeUpdate("CREATE TABLE IF NOT EXISTS local_channels (channel_id BLOB NOT NULL PRIMARY KEY, data BLOB NOT NULL, is_closed BOOLEAN NOT NULL DEFAULT 0)") - statement.executeUpdate("CREATE TABLE IF NOT EXISTS htlc_infos (channel_id BLOB NOT NULL, commitment_number BLOB NOT NULL, payment_hash BLOB NOT NULL, cltv_expiry INTEGER NOT NULL, FOREIGN KEY(channel_id) REFERENCES local_channels(channel_id))") - statement.executeUpdate("CREATE INDEX IF NOT EXISTS htlc_infos_idx ON htlc_infos(channel_id, commitment_number)") - setVersion(statement, "channels", 2) - } - // insert data - testCases.foreach { testCase => - using(connection.prepareStatement("INSERT INTO local_channels (channel_id, data, is_closed) VALUES (?, ?, ?)")) { statement => - statement.setBytes(1, testCase.channelId.toArray) - statement.setBytes(2, testCase.data.toArray) - statement.setBoolean(3, testCase.isClosed) - statement.executeUpdate() - for (commitmentNumber <- testCase.commitmentNumbers) { - using(connection.prepareStatement("INSERT INTO htlc_infos (channel_id, commitment_number, payment_hash, cltv_expiry) VALUES (?, ?, ?, ?)")) { statement => - statement.setBytes(1, testCase.channelId.toArray) - statement.setLong(2, commitmentNumber) - statement.setBytes(3, randomBytes32().toArray) - statement.setLong(4, 500000 + Random.nextInt(500000)) - statement.executeUpdate() - } - } - } - } - }, - dbName = SqliteChannelsDb.DB_NAME, - targetVersion = SqliteChannelsDb.CURRENT_VERSION, - postCheck = _ => postCheck(dbs.channels) - ) } } - test("migrate pg channel database v3 -> current") { - val dbs = TestPgDatabases() - - migrationCheck( - dbs = dbs, - initializeTables = connection => { - using(connection.createStatement()) { statement => - // initialize a v3 database - statement.executeUpdate("CREATE TABLE local_channels (channel_id TEXT NOT NULL PRIMARY KEY, data BYTEA NOT NULL, is_closed BOOLEAN NOT NULL DEFAULT FALSE, created_timestamp BIGINT, last_payment_sent_timestamp BIGINT, last_payment_received_timestamp BIGINT, last_connected_timestamp BIGINT, closed_timestamp BIGINT)") - statement.executeUpdate("CREATE TABLE htlc_infos (channel_id TEXT NOT NULL, commitment_number TEXT NOT NULL, payment_hash TEXT NOT NULL, cltv_expiry BIGINT NOT NULL, FOREIGN KEY(channel_id) REFERENCES local_channels(channel_id))") - statement.executeUpdate("CREATE INDEX htlc_infos_idx ON htlc_infos(channel_id, commitment_number)") - PgUtils.setVersion(statement, "channels", 3) - } - // insert data - testCases.foreach { testCase => - using(connection.prepareStatement("INSERT INTO local_channels (channel_id, data, is_closed, created_timestamp, last_payment_sent_timestamp, last_payment_received_timestamp, last_connected_timestamp, closed_timestamp) VALUES (?, ?, ?, ?, ?, ?, ?, ?)")) { statement => - statement.setString(1, testCase.channelId.toHex) - statement.setBytes(2, testCase.data.toArray) - statement.setBoolean(3, testCase.isClosed) - statement.setObject(4, testCase.createdTimestamp.orNull) - statement.setObject(5, testCase.lastPaymentSentTimestamp.orNull) - statement.setObject(6, testCase.lastPaymentReceivedTimestamp.orNull) - statement.setObject(7, testCase.lastConnectedTimestamp.orNull) - statement.setObject(8, testCase.closedTimestamp.orNull) - statement.executeUpdate() - } - } - }, - dbName = PgChannelsDb.DB_NAME, - targetVersion = PgChannelsDb.CURRENT_VERSION, - postCheck = connection => { - assert(dbs.channels.listLocalChannels().size == testCases.filterNot(_.isClosed).size) - testCases.foreach { testCase => - assert(getPgTimestamp(connection, testCase.channelId, "created_timestamp") == testCase.createdTimestamp) - assert(getPgTimestamp(connection, testCase.channelId, "last_payment_sent_timestamp") == testCase.lastPaymentSentTimestamp) - assert(getPgTimestamp(connection, testCase.channelId, "last_payment_received_timestamp") == testCase.lastPaymentReceivedTimestamp) - assert(getPgTimestamp(connection, testCase.channelId, "last_connected_timestamp") == testCase.lastConnectedTimestamp) - assert(getPgTimestamp(connection, testCase.channelId, "closed_timestamp") == testCase.closedTimestamp) - using(connection.prepareStatement(s"SELECT remote_node_id FROM local.channels WHERE channel_id=?")) { statement => - statement.setString(1, testCase.channelId.toHex) - val rs = statement.executeQuery() - rs.next() - assert(rs.getString("remote_node_id") == testCase.remoteNodeId.toHex) - } - } - } - ) - } - test("json column reset (postgres)") { val dbs = TestPgDatabases() val db = dbs.channels @@ -416,38 +231,6 @@ class ChannelsDbSpec extends AnyFunSuite { object ChannelsDbSpec { - case class TestCase(channelId: ByteVector32, - remoteNodeId: PublicKey, - data: ByteVector, - isClosed: Boolean, - createdTimestamp: Option[Long], - lastPaymentSentTimestamp: Option[Long], - lastPaymentReceivedTimestamp: Option[Long], - lastConnectedTimestamp: Option[Long], - closedTimestamp: Option[Long], - commitmentNumbers: Seq[Int]) - - val testCases: Seq[TestCase] = for (_ <- 0 until 10) yield { - val channelId = randomBytes32() - val remoteNodeId = randomKey().publicKey - val channel = ChannelCodecsSpec.normal - .modify(_.commitments.params.channelId).setTo(channelId) - .modify(_.commitments.params.remoteParams.nodeId).setTo(remoteNodeId) - val data = channelDataCodec.encode(channel).require.bytes - TestCase( - channelId = channelId, - remoteNodeId = remoteNodeId, - data = data, - isClosed = Random.nextBoolean(), - createdTimestamp = if (Random.nextBoolean()) Some(Random.nextInt(Int.MaxValue)) else None, - lastPaymentSentTimestamp = if (Random.nextBoolean()) Some(Random.nextInt(Int.MaxValue)) else None, - lastPaymentReceivedTimestamp = if (Random.nextBoolean()) Some(Random.nextInt(Int.MaxValue)) else None, - lastConnectedTimestamp = if (Random.nextBoolean()) Some(Random.nextInt(Int.MaxValue)) else None, - closedTimestamp = if (Random.nextBoolean()) Some(Random.nextInt(Int.MaxValue)) else None, - commitmentNumbers = for (_ <- 0 until Random.nextInt(10)) yield Random.nextInt(5) // there will be repetitions, on purpose - ) - } - def getTimestamp(dbs: TestDatabases, channelId: ByteVector32, columnName: String): Option[Long] = { dbs match { case _: TestPgDatabases => getPgTimestamp(dbs.connection, channelId, columnName) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/DbMigrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/DbMigrationSpec.scala deleted file mode 100644 index a88168418b..0000000000 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/DbMigrationSpec.scala +++ /dev/null @@ -1,117 +0,0 @@ -package fr.acinq.eclair.db - -import akka.actor.ActorSystem -import com.zaxxer.hikari.HikariConfig -import fr.acinq.eclair.db.Databases.{PostgresDatabases, SqliteDatabases} -import fr.acinq.eclair.db.migration._ -import fr.acinq.eclair.db.pg.PgUtils.PgLock -import fr.acinq.eclair.db.pg._ -import io.zonky.test.db.postgres.embedded.EmbeddedPostgres -import org.scalatest.Ignore -import org.scalatest.funsuite.AnyFunSuite -import org.sqlite.SQLiteConfig - -import java.io.File -import java.sql.{Connection, DriverManager} -import java.util.UUID -import javax.sql.DataSource - -/** - * To run this test, create a `migration` directory in your project's `user.dir` - * and copy your sqlite files to it (eclair.sqlite, network.sqlite, audit.sqlite). - * Then remove the `Ignore` annotation and run the test. - */ -@Ignore -class DbMigrationSpec extends AnyFunSuite { - - import DbMigrationSpec._ - - test("eclair migration test") { - val sqlite = loadSqlite("migration\\eclair.sqlite") - val postgresDatasource = EmbeddedPostgres.start().getPostgresDatabase - - new PgChannelsDb()(postgresDatasource, PgLock.NoLock) - new PgPendingCommandsDb()(postgresDatasource, PgLock.NoLock) - new PgPeersDb()(postgresDatasource, PgLock.NoLock) - new PgPaymentsDb()(postgresDatasource, PgLock.NoLock) - - PgUtils.inTransaction { postgres => - MigrateChannelsDb.migrateAllTables(sqlite, postgres) - MigratePendingCommandsDb.migrateAllTables(sqlite, postgres) - MigratePeersDb.migrateAllTables(sqlite, postgres) - MigratePaymentsDb.migrateAllTables(sqlite, postgres) - assert(CompareChannelsDb.compareAllTables(sqlite, postgres)) - assert(ComparePendingCommandsDb.compareAllTables(sqlite, postgres)) - assert(ComparePeersDb.compareAllTables(sqlite, postgres)) - assert(ComparePaymentsDb.compareAllTables(sqlite, postgres)) - }(postgresDatasource) - - sqlite.close() - } - - test("network migration test") { - val sqlite = loadSqlite("migration\\network.sqlite") - val postgresDatasource = EmbeddedPostgres.start().getPostgresDatabase - - new PgNetworkDb()(postgresDatasource) - - PgUtils.inTransaction { postgres => - MigrateNetworkDb.migrateAllTables(sqlite, postgres) - assert(CompareNetworkDb.compareAllTables(sqlite, postgres)) - }(postgresDatasource) - - sqlite.close() - } - - test("audit migration test") { - val sqlite = loadSqlite("migration\\audit.sqlite") - val postgresDatasource = EmbeddedPostgres.start().getPostgresDatabase - - new PgAuditDb()(postgresDatasource) - - PgUtils.inTransaction { postgres => - MigrateAuditDb.migrateAllTables(sqlite, postgres) - assert(CompareAuditDb.compareAllTables(sqlite, postgres)) - }(postgresDatasource) - - sqlite.close() - } - - test("full migration") { - // we need to open in read/write because of the getVersion call - val sqlite = SqliteDatabases( - auditJdbc = loadSqlite("migration\\audit.sqlite", readOnly = false), - eclairJdbc = loadSqlite("migration\\eclair.sqlite", readOnly = false), - networkJdbc = loadSqlite("migration\\network.sqlite", readOnly = false), - jdbcUrlFile_opt = None - ) - val postgres = { - val pg = EmbeddedPostgres.start() - val datasource: DataSource = pg.getPostgresDatabase - val hikariConfig = new HikariConfig - hikariConfig.setDataSource(datasource) - PostgresDatabases( - hikariConfig = hikariConfig, - instanceId = UUID.randomUUID(), - lock = PgLock.NoLock, - jdbcUrlFile_opt = None, - readOnlyUser_opt = None, - resetJsonColumns = false, - safetyChecks_opt = None - )(ActorSystem()) - } - val dualDb = DualDatabases(sqlite, postgres) - MigrateDb.migrateAll(dualDb) - CompareDb.compareAll(dualDb) - } - -} - -object DbMigrationSpec { - def loadSqlite(path: String, readOnly: Boolean = true): Connection = { - val sqliteConfig = new SQLiteConfig() - sqliteConfig.setReadOnly(readOnly) - val dbFile = new File(path) - DriverManager.getConnection(s"jdbc:sqlite:$dbFile", sqliteConfig.toProperties) - } -} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/DualDatabasesSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/DualDatabasesSpec.scala deleted file mode 100644 index e65bfe48b8..0000000000 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/DualDatabasesSpec.scala +++ /dev/null @@ -1,86 +0,0 @@ -package fr.acinq.eclair.db - -import com.typesafe.config.{Config, ConfigFactory} -import fr.acinq.eclair.wire.internal.channel.ChannelCodecsSpec -import fr.acinq.eclair.{TestKitBaseClass, TestUtils} -import io.zonky.test.db.postgres.embedded.EmbeddedPostgres -import org.scalatest.funsuite.AnyFunSuiteLike - -import java.io.File -import java.util.UUID - -class DualDatabasesSpec extends TestKitBaseClass with AnyFunSuiteLike { - - def fixture(driver: String): DualDatabases = { - val pg = EmbeddedPostgres.start() - val config = DualDatabasesSpec.testConfig(pg.getPort, driver) - val datadir = new File(TestUtils.BUILD_DIRECTORY, s"pg_test_${UUID.randomUUID()}") - datadir.mkdirs() - val instanceId = UUID.randomUUID() - Databases.init(config, instanceId, datadir).asInstanceOf[DualDatabases] - } - - test("sqlite primary") { - val db = fixture("dual-sqlite-primary") - - db.channels.addOrUpdateChannel(ChannelCodecsSpec.normal) - assert(db.primary.channels.listLocalChannels().nonEmpty) - awaitCond(db.primary.channels.listLocalChannels() == db.secondary.channels.listLocalChannels()) - } - - test("postgres primary") { - val db = fixture("dual-postgres-primary") - - db.channels.addOrUpdateChannel(ChannelCodecsSpec.normal) - assert(db.primary.channels.listLocalChannels().nonEmpty) - awaitCond(db.primary.channels.listLocalChannels() == db.secondary.channels.listLocalChannels()) - } -} - -object DualDatabasesSpec { - def testConfig(port: Int, driver: String): Config = - ConfigFactory.parseString( - s""" - |driver = $driver - |postgres { - | database = "" - | host = "localhost" - | port = $port - | username = "postgres" - | password = "" - | readonly-user = "" - | reset-json-columns = false - | pool { - | max-size = 10 // recommended value = number_of_cpu_cores * 2 - | connection-timeout = 30 seconds - | idle-timeout = 10 minutes - | max-life-time = 30 minutes - | } - | lock-type = "lease" // lease or none (do not use none in production) - | lease { - | interval = 5 seconds // lease-interval must be greater than lease-renew-interval - | renew-interval = 2 seconds - | lock-timeout = 5 seconds // timeout for the lock statement on the lease table - | auto-release-at-shutdown = false // automatically release the lock when eclair is stopping - | } - | safety-checks { - | // a set of basic checks on data to make sure we use the correct database - | enabled = false - | max-age { - | local-channels = 3 minutes - | network-nodes = 30 minutes - | audit-relayed = 10 minutes - | } - | min-count { - | local-channels = 10 - | network-nodes = 3000 - | network-channels = 20000 - | } - | } - |} - |dual { - | migrate-on-restart = false // migrate sqlite -> postgres on restart (only applies if sqlite is primary) - | compare-on-restart = false // compare sqlite and postgres dbs on restart - |} - |""".stripMargin) -} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/LiquidityDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/LiquidityDbSpec.scala index 1f4ee0cbc7..dea38770a4 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/LiquidityDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/LiquidityDbSpec.scala @@ -21,6 +21,7 @@ import fr.acinq.eclair.TestDatabases.forAllDbs import fr.acinq.eclair.channel.{ChannelLiquidityPurchased, LiquidityPurchase, Upstream} import fr.acinq.eclair.payment.relay.OnTheFlyFunding import fr.acinq.eclair.payment.relay.OnTheFlyFundingSpec.{createWillAdd, randomOnion} +import fr.acinq.eclair.reputation.Reputation import fr.acinq.eclair.wire.protocol.{LiquidityAds, UpdateAddHtlc} import fr.acinq.eclair.{CltvExpiry, MilliSatoshiLong, TimestampMilli, randomBytes32, randomKey} import org.scalatest.funsuite.AnyFunSuite @@ -70,10 +71,10 @@ class LiquidityDbSpec extends AnyFunSuite { val paymentHash1 = randomBytes32() val paymentHash2 = randomBytes32() val upstream = Seq( - Upstream.Hot.Channel(UpdateAddHtlc(randomBytes32(), 7, 25_000_000 msat, paymentHash1, CltvExpiry(750_000), randomOnion(), None, 1.0, None), TimestampMilli(0), randomKey().publicKey), - Upstream.Hot.Channel(UpdateAddHtlc(randomBytes32(), 0, 1 msat, paymentHash1, CltvExpiry(750_000), randomOnion(), Some(randomKey().publicKey), 1.0, None), TimestampMilli.now(), randomKey().publicKey), - Upstream.Hot.Channel(UpdateAddHtlc(randomBytes32(), 561, 100_000_000 msat, paymentHash2, CltvExpiry(799_999), randomOnion(), None, 1.0, None), TimestampMilli.now(), randomKey().publicKey), - Upstream.Hot.Channel(UpdateAddHtlc(randomBytes32(), 1105, 100_000_000 msat, paymentHash2, CltvExpiry(799_999), randomOnion(), None, 1.0, None), TimestampMilli.now(), randomKey().publicKey), + Upstream.Hot.Channel(UpdateAddHtlc(randomBytes32(), 7, 25_000_000 msat, paymentHash1, CltvExpiry(750_000), randomOnion(), None, Reputation.maxEndorsement, None), TimestampMilli(0), randomKey().publicKey, 0.1), + Upstream.Hot.Channel(UpdateAddHtlc(randomBytes32(), 0, 1 msat, paymentHash1, CltvExpiry(750_000), randomOnion(), Some(randomKey().publicKey), Reputation.maxEndorsement, None), TimestampMilli.now(), randomKey().publicKey, 0.1), + Upstream.Hot.Channel(UpdateAddHtlc(randomBytes32(), 561, 100_000_000 msat, paymentHash2, CltvExpiry(799_999), randomOnion(), None, Reputation.maxEndorsement, None), TimestampMilli.now(), randomKey().publicKey, 0.1), + Upstream.Hot.Channel(UpdateAddHtlc(randomBytes32(), 1105, 100_000_000 msat, paymentHash2, CltvExpiry(799_999), randomOnion(), None, Reputation.maxEndorsement, None), TimestampMilli.now(), randomKey().publicKey, 0.1), ) val pendingAlice = Seq( OnTheFlyFunding.Pending( diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/PaymentsDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/PaymentsDbSpec.scala index 829b521612..5d00c1d905 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/PaymentsDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/PaymentsDbSpec.scala @@ -31,6 +31,7 @@ import fr.acinq.eclair.wire.protocol.OfferTypes._ import fr.acinq.eclair.wire.protocol.{ChannelUpdate, UnknownNextPeer} import fr.acinq.eclair.{CltvExpiryDelta, Features, MilliSatoshi, MilliSatoshiLong, Paginated, ShortChannelId, TimestampMilli, TimestampMilliLong, TimestampSecond, TimestampSecondLong, randomBytes, randomBytes32, randomBytes64, randomKey} import org.scalatest.funsuite.AnyFunSuite +import scodec.bits.ByteVector import java.time.Instant import java.util.UUID @@ -199,7 +200,7 @@ class PaymentsDbSpec extends AnyFunSuite { val ps6 = OutgoingPayment(UUID.randomUUID(), UUID.randomUUID(), Some("3"), randomBytes32(), PaymentType.Standard, 789 msat, 789 msat, bob, 1250 unixms, None, None, OutgoingPaymentStatus.Failed(Nil, 1300 unixms)) db.addOutgoingPayment(ps4) db.addOutgoingPayment(ps5.copy(status = OutgoingPaymentStatus.Pending)) - db.updateOutgoingPayment(PaymentSent(ps5.parentId, ps5.paymentHash, preimage1, ps5.amount, ps5.recipientNodeId, Seq(PaymentSent.PartialPayment(ps5.id, ps5.amount, 42 msat, randomBytes32(), None, 1180 unixms)))) + db.updateOutgoingPayment(PaymentSent(ps5.parentId, ps5.paymentHash, preimage1, ps5.amount, ps5.recipientNodeId, Seq(PaymentSent.PartialPayment(ps5.id, ps5.amount, 42 msat, randomBytes32(), None, 1180 unixms)), None)) db.addOutgoingPayment(ps6.copy(status = OutgoingPaymentStatus.Pending)) db.updateOutgoingPayment(PaymentFailed(ps6.id, ps6.paymentHash, Nil, 1300 unixms)) @@ -771,12 +772,12 @@ class PaymentsDbSpec extends AnyFunSuite { assert(db.getOutgoingPayment(s4.id).contains(ss4)) // can't update again once it's in a final state - assertThrows[IllegalArgumentException](db.updateOutgoingPayment(PaymentSent(parentId, s3.paymentHash, preimage1, s3.recipientAmount, s3.recipientNodeId, Seq(PaymentSent.PartialPayment(s3.id, s3.amount, 42 msat, randomBytes32(), None))))) + assertThrows[IllegalArgumentException](db.updateOutgoingPayment(PaymentSent(parentId, s3.paymentHash, preimage1, s3.recipientAmount, s3.recipientNodeId, Seq(PaymentSent.PartialPayment(s3.id, s3.amount, 42 msat, randomBytes32(), None)), None))) val paymentSent = PaymentSent(parentId, paymentHash1, preimage1, 600 msat, carol, Seq( PaymentSent.PartialPayment(s1.id, s1.amount, 15 msat, randomBytes32(), None, 400 unixms), PaymentSent.PartialPayment(s2.id, s2.amount, 20 msat, randomBytes32(), Some(Seq(hop_ab, hop_bc)), 410 unixms) - )) + ), None) val ss1 = s1.copy(status = OutgoingPaymentStatus.Succeeded(preimage1, 15 msat, Nil, 400 unixms)) val ss2 = s2.copy(status = OutgoingPaymentStatus.Succeeded(preimage1, 20 msat, Seq(HopSummary(alice, bob, Some(ShortChannelId(42))), HopSummary(bob, carol, None)), 410 unixms)) db.updateOutgoingPayment(paymentSent) @@ -802,7 +803,7 @@ object PaymentsDbSpec { def createBolt12Invoice(amount: MilliSatoshi, payerKey: PrivateKey, recipientKey: PrivateKey, preimage: ByteVector32): Bolt12Invoice = { val offer = Offer(Some(amount), Some("some offer"), recipientKey.publicKey, Features.empty, Block.Testnet3GenesisBlock.hash) val invoiceRequest = InvoiceRequest(offer, 789 msat, 1, Features.empty, payerKey, Block.Testnet3GenesisBlock.hash) - val dummyRoute = PaymentBlindedRoute(RouteBlinding.create(randomKey(), Seq(randomKey().publicKey), Seq(randomBytes(100))).route, PaymentInfo(0 msat, 0, CltvExpiryDelta(0), 0 msat, 0 msat, Features.empty)) + val dummyRoute = PaymentBlindedRoute(RouteBlinding.create(randomKey(), Seq(randomKey().publicKey), Seq(randomBytes(100))).route, PaymentInfo(0 msat, 0, CltvExpiryDelta(0), 0 msat, 0 msat, ByteVector.empty)) Bolt12Invoice(invoiceRequest, preimage, recipientKey, 1 hour, Features.empty, Seq(dummyRoute)) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/PendingCommandsDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/PendingCommandsDbSpec.scala index f5f395be19..496608644c 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/PendingCommandsDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/PendingCommandsDbSpec.scala @@ -18,13 +18,15 @@ package fr.acinq.eclair.db import fr.acinq.bitcoin.scalacompat.ByteVector32 import fr.acinq.eclair.TestDatabases.{TestPgDatabases, TestSqliteDatabases} -import fr.acinq.eclair.channel.{CMD_FAIL_HTLC, CMD_FAIL_MALFORMED_HTLC, CMD_FULFILL_HTLC, HtlcSettlementCommand} +import fr.acinq.eclair.channel._ +import fr.acinq.eclair.crypto.Sphinx +import fr.acinq.eclair.crypto.Sphinx.Attribution import fr.acinq.eclair.db.pg.PgPendingCommandsDb import fr.acinq.eclair.db.sqlite.SqlitePendingCommandsDb import fr.acinq.eclair.db.sqlite.SqliteUtils.{setVersion, using} -import fr.acinq.eclair.randomBytes32 import fr.acinq.eclair.wire.internal.CommandCodecs.cmdCodec import fr.acinq.eclair.wire.protocol.{FailureMessageCodecs, FailureReason, UnknownNextPeer} +import fr.acinq.eclair.{TimestampMilli, randomBytes, randomBytes32} import org.scalatest.funsuite.AnyFunSuite import scala.util.Random @@ -51,20 +53,20 @@ class PendingCommandsDbSpec extends AnyFunSuite { val channelId1 = randomBytes32() val channelId2 = randomBytes32() - val msg0 = CMD_FULFILL_HTLC(0, randomBytes32()) - val msg1 = CMD_FULFILL_HTLC(1, randomBytes32()) - val msg2 = CMD_FAIL_HTLC(2, FailureReason.EncryptedDownstreamFailure(randomBytes32())) - val msg3 = CMD_FAIL_HTLC(3, FailureReason.EncryptedDownstreamFailure(randomBytes32())) + val msg0 = CMD_FULFILL_HTLC(0, randomBytes32(), None) + val msg1 = CMD_FULFILL_HTLC(1, randomBytes32(), Some(FulfillAttributionData(TimestampMilli(600), None, Some(randomBytes(Attribution.totalLength))))) + val msg2 = CMD_FAIL_HTLC(2, FailureReason.EncryptedDownstreamFailure(randomBytes32(), None), None) + val msg3 = CMD_FAIL_HTLC(3, FailureReason.EncryptedDownstreamFailure(randomBytes32(), Some(randomBytes(Sphinx.Attribution.totalLength))), Some(FailureAttributionData(TimestampMilli(500), Some(TimestampMilli(550))))) val msg4 = CMD_FAIL_MALFORMED_HTLC(4, randomBytes32(), FailureMessageCodecs.BADONION) assert(db.listSettlementCommands(channelId1).toSet == Set.empty) db.addSettlementCommand(channelId1, msg0) db.addSettlementCommand(channelId1, msg0) // duplicate - db.addSettlementCommand(channelId1, CMD_FAIL_HTLC(msg0.id, FailureReason.EncryptedDownstreamFailure(randomBytes32()))) // conflicting command + db.addSettlementCommand(channelId1, CMD_FAIL_HTLC(msg0.id, FailureReason.EncryptedDownstreamFailure(randomBytes32(), None), None)) // conflicting command db.addSettlementCommand(channelId1, msg1) db.addSettlementCommand(channelId1, msg2) db.addSettlementCommand(channelId1, msg3) - db.addSettlementCommand(channelId1, CMD_FULFILL_HTLC(msg3.id, randomBytes32())) // conflicting command + db.addSettlementCommand(channelId1, CMD_FULFILL_HTLC(msg3.id, randomBytes32(), None)) // conflicting command db.addSettlementCommand(channelId1, msg4) db.addSettlementCommand(channelId2, msg0) // same messages but for different channel db.addSettlementCommand(channelId2, msg1) @@ -138,8 +140,8 @@ object PendingCommandsDbSpec { val channelId = randomBytes32() val cmds = (0 until Random.nextInt(5)).map { _ => Random.nextInt(2) match { - case 0 => CMD_FULFILL_HTLC(Random.nextLong(100_000), randomBytes32()) - case 1 => CMD_FAIL_HTLC(Random.nextLong(100_000), FailureReason.LocalFailure(UnknownNextPeer())) + case 0 => CMD_FULFILL_HTLC(Random.nextLong(100_000), randomBytes32(), None) + case 1 => CMD_FAIL_HTLC(Random.nextLong(100_000), FailureReason.LocalFailure(UnknownNextPeer()), None) } } cmds.map(cmd => TestCase(channelId, cmd)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/RevokedHtlcInfoCleanerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/RevokedHtlcInfoCleanerSpec.scala index 2200e97534..a120fea36d 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/RevokedHtlcInfoCleanerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/RevokedHtlcInfoCleanerSpec.scala @@ -34,11 +34,11 @@ class RevokedHtlcInfoCleanerSpec extends ScalaTestWithActorTestKit(ConfigFactory val channelsDb = TestSqliteDatabases().channels val channelId = randomBytes32() - channelsDb.addOrUpdateChannel(ChannelCodecsSpec.normal.modify(_.commitments.params.channelId).setTo(channelId)) + channelsDb.addOrUpdateChannel(ChannelCodecsSpec.normal.modify(_.commitments.channelParams.channelId).setTo(channelId)) channelsDb.addHtlcInfo(channelId, 17, randomBytes32(), CltvExpiry(561)) channelsDb.addHtlcInfo(channelId, 19, randomBytes32(), CltvExpiry(1105)) channelsDb.addHtlcInfo(channelId, 23, randomBytes32(), CltvExpiry(1729)) - channelsDb.removeChannel(channelId) + channelsDb.removeChannel(channelId, None) assert(channelsDb.listHtlcInfos(channelId, 17).nonEmpty) assert(channelsDb.listHtlcInfos(channelId, 19).nonEmpty) assert(channelsDb.listHtlcInfos(channelId, 23).nonEmpty) @@ -57,7 +57,7 @@ class RevokedHtlcInfoCleanerSpec extends ScalaTestWithActorTestKit(ConfigFactory val channelsDb = TestSqliteDatabases().channels val channelId = randomBytes32() - channelsDb.addOrUpdateChannel(ChannelCodecsSpec.normal.modify(_.commitments.params.channelId).setTo(channelId)) + channelsDb.addOrUpdateChannel(ChannelCodecsSpec.normal.modify(_.commitments.channelParams.channelId).setTo(channelId)) channelsDb.addHtlcInfo(channelId, 1, randomBytes32(), CltvExpiry(561)) channelsDb.addHtlcInfo(channelId, 2, randomBytes32(), CltvExpiry(1105)) channelsDb.addHtlcInfo(channelId, 2, randomBytes32(), CltvExpiry(1105)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/ChannelIntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/ChannelIntegrationSpec.scala index 71c80329d2..66c476229d 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/ChannelIntegrationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/ChannelIntegrationSpec.scala @@ -17,7 +17,6 @@ package fr.acinq.eclair.integration import akka.actor.ActorRef -import akka.actor.Status.Failure import akka.actor.typed.scaladsl.adapter.ClassicActorRefOps import akka.pattern.pipe import akka.testkit.TestProbe @@ -26,7 +25,7 @@ import fr.acinq.bitcoin.ScriptFlags import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, Crypto, OutPoint, SatoshiLong, Script, Transaction, TxId, addressFromPublicKeyScript} import fr.acinq.eclair.blockchain.bitcoind.BitcoindService.BitcoinReq -import fr.acinq.eclair.blockchain.bitcoind.rpc.{BitcoinCoreClient, JsonRPCError} +import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinCoreClient import fr.acinq.eclair.channel._ import fr.acinq.eclair.crypto.Sphinx.DecryptedFailurePacket import fr.acinq.eclair.io.{Peer, Switchboard} @@ -35,7 +34,6 @@ import fr.acinq.eclair.payment.receive.MultiPartHandler.ReceiveStandardPayment import fr.acinq.eclair.payment.receive.{ForwardHandler, PaymentHandler} import fr.acinq.eclair.payment.send.PaymentInitiator.SendPaymentToNode import fr.acinq.eclair.router.Router -import fr.acinq.eclair.transactions.Transactions.{AnchorOutputsCommitmentFormat, CommitmentFormat, DefaultCommitmentFormat, TxOwner} import fr.acinq.eclair.transactions.{OutgoingHtlc, Scripts, Transactions} import fr.acinq.eclair.wire.protocol._ import fr.acinq.eclair.{MilliSatoshi, MilliSatoshiLong, randomBytes32} @@ -52,6 +50,8 @@ import scala.jdk.CollectionConverters._ abstract class ChannelIntegrationSpec extends IntegrationSpec { + def channelType: SupportedChannelType + def awaitAnnouncements(channels: Int): Unit = { val sender = TestProbe() awaitCond({ @@ -111,7 +111,7 @@ abstract class ChannelIntegrationSpec extends IntegrationSpec { case class ForceCloseFixture(sender: TestProbe, paymentSender: TestProbe, stateListenerC: TestProbe, stateListenerF: TestProbe, paymentId: UUID, htlc: UpdateAddHtlc, preimage: ByteVector32, minerAddress: String, finalAddressC: String, finalAddressF: String) /** Prepare a C <-> F channel for a force-close test by adding an HTLC that will be hodl-ed at F. */ - def prepareForceCloseCF(commitmentFormat: Transactions.CommitmentFormat): ForceCloseFixture = { + def prepareForceCloseCF(): ForceCloseFixture = { val sender = TestProbe() sender.send(bitcoincli, BitcoinReq("getnewaddress")) val JString(minerAddress) = sender.expectMsgType[JValue] @@ -121,7 +121,7 @@ abstract class ChannelIntegrationSpec extends IntegrationSpec { nodes("C").system.eventStream.subscribe(stateListenerC.ref, classOf[ChannelStateChanged]) nodes("F").system.eventStream.subscribe(stateListenerF.ref, classOf[ChannelStateChanged]) // we create and announce a channel between C and F; we use push_msat to ensure both nodes have an output in the commit tx - connect(nodes("C"), nodes("F"), 5000000 sat, 500000000 msat) + connect(nodes("C"), nodes("F"), 5000000 sat, 500000000 msat, channelType) awaitCond(stateListenerC.expectMsgType[ChannelStateChanged](max = 60 seconds).currentState == WAIT_FOR_FUNDING_CONFIRMED, max = 30 seconds) awaitCond(stateListenerF.expectMsgType[ChannelStateChanged](max = 60 seconds).currentState == WAIT_FOR_FUNDING_CONFIRMED, max = 30 seconds) // we exchange channel_ready and move to the NORMAL state after 8 blocks @@ -147,17 +147,17 @@ abstract class ChannelIntegrationSpec extends IntegrationSpec { // now that we have the channel id, we retrieve channels default final addresses sender.send(nodes("C").register, Register.Forward(sender.ref.toTyped[Any], htlc.channelId, CMD_GET_CHANNEL_DATA(ActorRef.noSender))) val dataC = sender.expectMsgType[RES_GET_CHANNEL_DATA[DATA_NORMAL]].data - assert(dataC.commitments.params.commitmentFormat == commitmentFormat) + assert(dataC.commitments.latest.commitmentFormat == channelType.commitmentFormat) val Right(finalAddressC) = addressFromPublicKeyScript(Block.RegtestGenesisBlock.hash, nodes("C").wallet.getReceivePublicKeyScript(renew = false)) sender.send(nodes("F").register, Register.Forward(sender.ref.toTyped[Any], htlc.channelId, CMD_GET_CHANNEL_DATA(ActorRef.noSender))) val dataF = sender.expectMsgType[RES_GET_CHANNEL_DATA[DATA_NORMAL]].data - assert(dataF.commitments.params.commitmentFormat == commitmentFormat) + assert(dataF.commitments.latest.commitmentFormat == channelType.commitmentFormat) val Right(finalAddressF) = addressFromPublicKeyScript(Block.RegtestGenesisBlock.hash, nodes("F").wallet.getReceivePublicKeyScript(renew = false)) ForceCloseFixture(sender, paymentSender, stateListenerC, stateListenerF, paymentId, htlc, preimage, minerAddress, finalAddressC, finalAddressF) } - def testDownstreamFulfillLocalCommit(commitmentFormat: Transactions.CommitmentFormat): Unit = { - val forceCloseFixture = prepareForceCloseCF(commitmentFormat) + def testDownstreamFulfillLocalCommit(): Unit = { + val forceCloseFixture = prepareForceCloseCF() import forceCloseFixture._ // we retrieve transactions already received so that we don't take them into account when evaluating the outcome of this test @@ -173,17 +173,14 @@ abstract class ChannelIntegrationSpec extends IntegrationSpec { // we generate a few blocks to get the commit tx confirmed generateBlocks(3, Some(minerAddress)) // we then fulfill the htlc, which will make F redeem it on-chain - sender.send(nodes("F").register, Register.Forward(sender.ref.toTyped[Any], htlc.channelId, CMD_FULFILL_HTLC(htlc.id, preimage))) + sender.send(nodes("F").register, Register.Forward(sender.ref.toTyped[Any], htlc.channelId, CMD_FULFILL_HTLC(htlc.id, preimage, None))) // we don't need to generate blocks to confirm the htlc-success; C should extract the preimage as soon as it enters // the mempool and fulfill the payment upstream. paymentSender.expectMsgType[PaymentSent](max = 60 seconds) // we then generate enough blocks so that nodes get their main delayed output generateBlocks(25, Some(minerAddress)) val expectedTxCountC = 1 // C should have 1 recv transaction: its main output - val expectedTxCountF = commitmentFormat match { - case _: AnchorOutputsCommitmentFormat => 2 // F should have 2 recv transactions: the redeemed htlc and its main output - case Transactions.DefaultCommitmentFormat => 1 // F's main output uses static_remotekey - } + val expectedTxCountF = 2 // F should have 2 recv transactions: the redeemed htlc and its main output awaitCond({ val receivedByC = listReceivedByAddress(finalAddressC, sender) val receivedByF = listReceivedByAddress(finalAddressF) @@ -197,8 +194,8 @@ abstract class ChannelIntegrationSpec extends IntegrationSpec { awaitAnnouncements(1) } - def testDownstreamFulfillRemoteCommit(commitmentFormat: Transactions.CommitmentFormat): Unit = { - val forceCloseFixture = prepareForceCloseCF(commitmentFormat) + def testDownstreamFulfillRemoteCommit(): Unit = { + val forceCloseFixture = prepareForceCloseCF() import forceCloseFixture._ // we retrieve transactions already received so that we don't take them into account when evaluating the outcome of this test @@ -214,16 +211,13 @@ abstract class ChannelIntegrationSpec extends IntegrationSpec { // we generate a few blocks to get the commit tx confirmed generateBlocks(3, Some(minerAddress)) // we then fulfill the htlc (it won't be sent to C, and will be used to pull funds on-chain) - sender.send(nodes("F").register, Register.Forward(sender.ref.toTyped[Any], htlc.channelId, CMD_FULFILL_HTLC(htlc.id, preimage))) + sender.send(nodes("F").register, Register.Forward(sender.ref.toTyped[Any], htlc.channelId, CMD_FULFILL_HTLC(htlc.id, preimage, None))) // we don't need to generate blocks to confirm the htlc-success; C should extract the preimage as soon as it enters // the mempool and fulfill the payment upstream. paymentSender.expectMsgType[PaymentSent](max = 60 seconds) // we then generate enough blocks so that F gets its htlc-success delayed output generateBlocks(25, Some(minerAddress)) - val expectedTxCountC = commitmentFormat match { - case _: AnchorOutputsCommitmentFormat => 1 // C should have 1 recv transaction: its main output - case Transactions.DefaultCommitmentFormat => 0 // C's main output uses static_remotekey - } + val expectedTxCountC = 1 // C should have 1 recv transaction: its main output val expectedTxCountF = 2 // F should have 2 recv transactions: the redeemed htlc and its main output awaitCond({ val receivedByC = listReceivedByAddress(finalAddressC, sender) @@ -238,8 +232,8 @@ abstract class ChannelIntegrationSpec extends IntegrationSpec { awaitAnnouncements(1) } - def testDownstreamTimeoutLocalCommit(commitmentFormat: Transactions.CommitmentFormat): Unit = { - val forceCloseFixture = prepareForceCloseCF(commitmentFormat) + def testDownstreamTimeoutLocalCommit(): Unit = { + val forceCloseFixture = prepareForceCloseCF() import forceCloseFixture._ // we retrieve transactions already received so that we don't take them into account when evaluating the outcome of this test @@ -261,8 +255,8 @@ abstract class ChannelIntegrationSpec extends IntegrationSpec { // we generate a few blocks to get the commit tx confirmed generateBlocks(8, Some(minerAddress)) // we wait until the htlc-timeout has been broadcast - assert(localCommit.htlcTxs.size == 1) - waitForOutputSpent(localCommit.htlcTxs.keys.head, bitcoinClient, sender) + assert(localCommit.htlcOutputs.size == 1) + waitForOutputSpent(localCommit.htlcOutputs.head, bitcoinClient, sender) // we generate more blocks for the htlc-timeout to reach enough confirmations generateBlocks(8, Some(minerAddress)) // this will fail the htlc @@ -274,10 +268,7 @@ abstract class ChannelIntegrationSpec extends IntegrationSpec { // we then generate enough blocks to confirm all delayed transactions generateBlocks(25, Some(minerAddress)) val expectedTxCountC = 2 // C should have 2 recv transactions: its main output and the htlc timeout - val expectedTxCountF = commitmentFormat match { - case _: AnchorOutputsCommitmentFormat => 1 // F should have 1 recv transaction: its main output - case Transactions.DefaultCommitmentFormat => 0 // F's main output uses static_remotekey - } + val expectedTxCountF = 1 // F should have 1 recv transaction: its main output awaitCond({ val receivedByC = listReceivedByAddress(finalAddressC, sender) val receivedByF = listReceivedByAddress(finalAddressF, sender) @@ -291,8 +282,8 @@ abstract class ChannelIntegrationSpec extends IntegrationSpec { awaitAnnouncements(1) } - def testDownstreamTimeoutRemoteCommit(commitmentFormat: Transactions.CommitmentFormat): Unit = { - val forceCloseFixture = prepareForceCloseCF(commitmentFormat) + def testDownstreamTimeoutRemoteCommit(): Unit = { + val forceCloseFixture = prepareForceCloseCF() import forceCloseFixture._ // we retrieve transactions already received so that we don't take them into account when evaluating the outcome of this test @@ -317,8 +308,8 @@ abstract class ChannelIntegrationSpec extends IntegrationSpec { generateBlocks((htlc.cltvExpiry.blockHeight - getBlockHeight()).toInt, Some(minerAddress)) // we wait until the claim-htlc-timeout has been broadcast val bitcoinClient = new BitcoinCoreClient(bitcoinrpcclient) - assert(remoteCommit.claimHtlcTxs.size == 1) - waitForOutputSpent(remoteCommit.claimHtlcTxs.keys.head, bitcoinClient, sender) + assert(remoteCommit.htlcOutputs.size == 1) + waitForOutputSpent(remoteCommit.htlcOutputs.head, bitcoinClient, sender) // and we generate blocks for the claim-htlc-timeout to reach enough confirmations generateBlocks(8, Some(minerAddress)) // this will fail the htlc @@ -329,10 +320,7 @@ abstract class ChannelIntegrationSpec extends IntegrationSpec { assert(failed.failures.head.asInstanceOf[RemoteFailure].e == DecryptedFailurePacket(nodes("C").nodeParams.nodeId, PermanentChannelFailure())) // we then generate enough blocks to confirm all delayed transactions generateBlocks(25, Some(minerAddress)) - val expectedTxCountC = commitmentFormat match { - case _: AnchorOutputsCommitmentFormat => 2 // C should have 2 recv transactions: its main output and the htlc timeout - case Transactions.DefaultCommitmentFormat => 1 // C's main output uses static_remotekey - } + val expectedTxCountC = 2 // C should have 2 recv transactions: its main output and the htlc timeout val expectedTxCountF = 1 // F should have 1 recv transaction: its main output awaitCond({ val receivedByC = listReceivedByAddress(finalAddressC, sender) @@ -349,10 +337,10 @@ abstract class ChannelIntegrationSpec extends IntegrationSpec { case class RevokedCommitFixture(sender: TestProbe, stateListenerC: TestProbe, revokedCommitTx: Transaction, htlcSuccess: Seq[Transaction], htlcTimeout: Seq[Transaction], finalAddressC: String) - def testRevokedCommit(commitmentFormat: Transactions.CommitmentFormat): RevokedCommitFixture = { + def testRevokedCommit(): RevokedCommitFixture = { val sender = TestProbe() // we create and announce a channel between C and F; we use push_msat to ensure F has a balance - connect(nodes("C"), nodes("F"), 5000000 sat, 300000000 msat) + connect(nodes("C"), nodes("F"), 5000000 sat, 300000000 msat, channelType) generateBlocks(8) awaitAnnouncements(2) // we subscribe to C's channel state transitions @@ -400,16 +388,17 @@ abstract class ChannelIntegrationSpec extends IntegrationSpec { forwardHandlerC.forward(buffer.ref) val commitmentsF = sigListener.expectMsgType[ChannelSignatureReceived].commitments sigListener.expectNoMessage(1 second) - assert(commitmentsF.params.commitmentFormat == commitmentFormat) + assert(commitmentsF.latest.commitmentFormat == channelType.commitmentFormat) + // we prepare the revoked transactions F will publish + val channelKeysF = nodes("F").nodeParams.channelKeyManager.channelKeys(commitmentsF.channelParams.channelConfig, commitmentsF.localChannelParams.fundingKeyPath) + val commitmentKeysF = commitmentsF.latest.localKeys(channelKeysF) + val revokedCommitTx = commitmentsF.latest.fullySignedLocalCommitTx(channelKeysF) // in this commitment, both parties should have a main output, there are four pending htlcs and anchor outputs if applicable - val localCommitF = commitmentsF.latest.localCommit - commitmentFormat match { - case Transactions.DefaultCommitmentFormat => assert(localCommitF.commitTxAndRemoteSig.commitTx.tx.txOut.size == 6) - case _: Transactions.AnchorOutputsCommitmentFormat => assert(localCommitF.commitTxAndRemoteSig.commitTx.tx.txOut.size == 8) - } - val outgoingHtlcExpiry = localCommitF.spec.htlcs.collect { case OutgoingHtlc(add) => add.cltvExpiry }.max - val htlcTimeoutTxs = localCommitF.htlcTxsAndRemoteSigs.collect { case h@HtlcTxAndRemoteSig(_: Transactions.HtlcTimeoutTx, _) => h } - val htlcSuccessTxs = localCommitF.htlcTxsAndRemoteSigs.collect { case h@HtlcTxAndRemoteSig(_: Transactions.HtlcSuccessTx, _) => h } + assert(revokedCommitTx.txOut.size == 8) + val outgoingHtlcExpiry = commitmentsF.latest.localCommit.spec.htlcs.collect { case OutgoingHtlc(add) => add.cltvExpiry }.max + val htlcTxsF = commitmentsF.latest.htlcTxs(channelKeysF) + val htlcTimeoutTxs = htlcTxsF.collect { case (tx: Transactions.UnsignedHtlcTimeoutTx, remoteSig) => (tx, remoteSig) } + val htlcSuccessTxs = htlcTxsF.collect { case (tx: Transactions.UnsignedHtlcSuccessTx, remoteSig) => (tx, remoteSig) } assert(htlcTimeoutTxs.size == 2) assert(htlcSuccessTxs.size == 2) // we fulfill htlcs to get the preimages @@ -435,24 +424,11 @@ abstract class ChannelIntegrationSpec extends IntegrationSpec { sender.send(nodes("C").register, Register.Forward(sender.ref.toTyped[Any], commitmentsF.channelId, CMD_GET_CHANNEL_DATA(ActorRef.noSender))) sender.expectMsgType[RES_GET_CHANNEL_DATA[DATA_NORMAL]] val Right(finalAddressC) = addressFromPublicKeyScript(Block.RegtestGenesisBlock.hash, nodes("C").wallet.getReceivePublicKeyScript(renew = false)) - // we prepare the revoked transactions F will publish - val keyManagerF = nodes("F").nodeParams.channelKeyManager - val channelKeyPathF = keyManagerF.keyPath(commitmentsF.params.localParams, commitmentsF.params.channelConfig) - val localPerCommitmentPointF = keyManagerF.commitmentPoint(channelKeyPathF, commitmentsF.localCommitIndex) - val revokedCommitTx = { - val commitTx = localCommitF.commitTxAndRemoteSig.commitTx - val localSig = keyManagerF.sign(commitTx, keyManagerF.fundingPublicKey(commitmentsF.params.localParams.fundingKeyPath, commitmentsF.latest.fundingTxIndex), TxOwner.Local, commitmentFormat, Map.empty) - val RemoteSignature.FullSignature(remoteSig) = localCommitF.commitTxAndRemoteSig.remoteSig - Transactions.addSigs(commitTx, keyManagerF.fundingPublicKey(commitmentsF.params.localParams.fundingKeyPath, commitmentsF.latest.fundingTxIndex).publicKey, commitmentsF.latest.remoteFundingPubKey, localSig, remoteSig).tx - } val htlcSuccess = htlcSuccessTxs.zip(Seq(preimage1, preimage2)).map { - case (htlcTxAndSigs, preimage) => - val localSig = keyManagerF.sign(htlcTxAndSigs.htlcTx, keyManagerF.htlcPoint(channelKeyPathF), localPerCommitmentPointF, TxOwner.Local, commitmentFormat, Map.empty) - Transactions.addSigs(htlcTxAndSigs.htlcTx.asInstanceOf[Transactions.HtlcSuccessTx], localSig, htlcTxAndSigs.remoteSig, preimage, commitmentsF.params.commitmentFormat).tx + case ((htlcTx, remoteSig), preimage) => htlcTx.addRemoteSig(commitmentKeysF, remoteSig, preimage).sign() } - val htlcTimeout = htlcTimeoutTxs.map { htlcTxAndSigs => - val localSig = keyManagerF.sign(htlcTxAndSigs.htlcTx, keyManagerF.htlcPoint(channelKeyPathF), localPerCommitmentPointF, TxOwner.Local, commitmentFormat, Map.empty) - Transactions.addSigs(htlcTxAndSigs.htlcTx.asInstanceOf[Transactions.HtlcTimeoutTx], localSig, htlcTxAndSigs.remoteSig, commitmentsF.params.commitmentFormat).tx + val htlcTimeout = htlcTimeoutTxs.map { + case (htlcTx, remoteSig) => htlcTx.addRemoteSig(commitmentKeysF, remoteSig).sign() } htlcSuccess.foreach(tx => Transaction.correctlySpends(tx, Seq(revokedCommitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) htlcTimeout.foreach(tx => Transaction.correctlySpends(tx, Seq(revokedCommitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) @@ -461,100 +437,15 @@ abstract class ChannelIntegrationSpec extends IntegrationSpec { } -class StandardChannelIntegrationSpec extends ChannelIntegrationSpec { - - test("start eclair nodes") { - instantiateEclairNode("A", ConfigFactory.parseMap(Map("eclair.node-alias" -> "A", "eclair.channel.expiry-delta-blocks" -> 40, "eclair.channel.fulfill-safety-before-timeout-blocks" -> 12, "eclair.server.port" -> (if (useEclairSigner) 29840 else 29740), "eclair.api.port" -> (if (useEclairSigner) 28190 else 28090)).asJava).withFallback(withStaticRemoteKey).withFallback(commonConfig)) - instantiateEclairNode("C", ConfigFactory.parseMap(Map("eclair.node-alias" -> "C", "eclair.channel.expiry-delta-blocks" -> 40, "eclair.channel.fulfill-safety-before-timeout-blocks" -> 12, "eclair.server.port" -> (if (useEclairSigner) 29841 else 29741), "eclair.api.port" -> (if (useEclairSigner) 28191 else 28091)).asJava).withFallback(withAnchorOutputs).withFallback(commonConfig)) - instantiateEclairNode("F", ConfigFactory.parseMap(Map("eclair.node-alias" -> "F", "eclair.channel.expiry-delta-blocks" -> 40, "eclair.channel.fulfill-safety-before-timeout-blocks" -> 12, "eclair.server.port" -> (if (useEclairSigner) 29842 else 29742), "eclair.api.port" -> (if (useEclairSigner) 28192 else 28092)).asJava).withFallback(withStaticRemoteKey).withFallback(commonConfig)) - } - - test("connect nodes") { - // A --- C --- F - val eventListener = TestProbe() - nodes("A").system.eventStream.subscribe(eventListener.ref, classOf[ChannelStateChanged]) - nodes("C").system.eventStream.subscribe(eventListener.ref, classOf[ChannelStateChanged]) - - connect(nodes("A"), nodes("C"), 11000000 sat, 0 msat) - // confirm the funding tx - generateBlocks(8) - within(60 seconds) { - var count = 0 - while (count < 2) { - if (eventListener.expectMsgType[ChannelStateChanged](max = 60 seconds).currentState == NORMAL) count = count + 1 - } - } - awaitAnnouncements(1) - } - - test("propagate a fulfill upstream when a downstream htlc is redeemed on-chain (local commit)") { - testDownstreamFulfillLocalCommit(Transactions.DefaultCommitmentFormat) - } - - test("propagate a fulfill upstream when a downstream htlc is redeemed on-chain (remote commit)") { - testDownstreamFulfillRemoteCommit(Transactions.DefaultCommitmentFormat) - } - - test("propagate a failure upstream when a downstream htlc times out (local commit)") { - testDownstreamTimeoutLocalCommit(Transactions.DefaultCommitmentFormat) - } - - test("propagate a failure upstream when a downstream htlc times out (remote commit)") { - testDownstreamTimeoutRemoteCommit(Transactions.DefaultCommitmentFormat) - } - - test("punish a node that has published a revoked commit tx") { - val revokedCommitFixture = testRevokedCommit(Transactions.DefaultCommitmentFormat) - import revokedCommitFixture._ - - val bitcoinClient = new BitcoinCoreClient(bitcoinrpcclient) - // we retrieve transactions already received so that we don't take them into account when evaluating the outcome of this test - val previouslyReceivedByC = listReceivedByAddress(finalAddressC, sender) - // F publishes the revoked commitment, one HTLC-success, one HTLC-timeout and leaves the other HTLC outputs unclaimed - bitcoinClient.publishTransaction(revokedCommitTx).pipeTo(sender.ref) - sender.expectMsg(revokedCommitTx.txid) - bitcoinClient.publishTransaction(htlcSuccess.head).pipeTo(sender.ref) - sender.expectMsgType[Any] match { - case txid: TxId => assert(txid == htlcSuccess.head.txid) - // 3rd stage txs (txs spending htlc txs) are not tested if C publishes the htlc-penalty transaction before F publishes its htlc-success - case Failure(e: JsonRPCError) => assert(e.error.message == "txn-mempool-conflict") - } - bitcoinClient.publishTransaction(htlcTimeout.head).pipeTo(sender.ref) - sender.expectMsgType[Any] match { - case txid: TxId => assert(txid == htlcTimeout.head.txid) - // 3rd stage txs (txs spending htlc txs) are not tested if C publishes the htlc-penalty transaction before F publishes its htlc-timeout - case Failure(e: JsonRPCError) => assert(e.error.message == "txn-mempool-conflict") - } - // at this point C should have 5 recv transactions: F's main output and all htlc outputs (taken as punishment) - // C's main output uses static_remotekey, so C doesn't need to claim it - awaitCond({ - val receivedByC = listReceivedByAddress(finalAddressC, sender) - (receivedByC diff previouslyReceivedByC).size == 5 - }, max = 30 seconds, interval = 1 second) - // we generate enough blocks for the channel to be deeply confirmed - generateBlocks(12) - // and we wait for C's channel to close - awaitCond(stateListenerC.expectMsgType[ChannelStateChanged](max = 60 seconds).currentState == CLOSED, max = 60 seconds) - awaitAnnouncements(1) - } - -} - -class StandardChannelIntegrationWithEclairSignerSpec extends StandardChannelIntegrationSpec { - override def useEclairSigner: Boolean = true -} - abstract class AnchorChannelIntegrationSpec extends ChannelIntegrationSpec { - val commitmentFormat: AnchorOutputsCommitmentFormat - - def connectNodes(expectedCommitmentFormat: CommitmentFormat): Unit = { + def connectNodes(): Unit = { // A --- C --- F val eventListener = TestProbe() nodes("A").system.eventStream.subscribe(eventListener.ref, classOf[ChannelStateChanged]) nodes("C").system.eventStream.subscribe(eventListener.ref, classOf[ChannelStateChanged]) - connect(nodes("A"), nodes("C"), 11000000 sat, 0 msat) + connect(nodes("A"), nodes("C"), 11000000 sat, 0 msat, channelType) // confirm the funding tx generateBlocks(8) within(60 seconds) { @@ -563,7 +454,7 @@ abstract class AnchorChannelIntegrationSpec extends ChannelIntegrationSpec { val stateEvent = eventListener.expectMsgType[ChannelStateChanged](max = 60 seconds) if (stateEvent.currentState == NORMAL) { assert(stateEvent.commitments_opt.nonEmpty) - assert(stateEvent.commitments_opt.get.params.commitmentFormat == expectedCommitmentFormat) + assert(stateEvent.commitments_opt.get.latest.commitmentFormat == channelType.commitmentFormat) count = count + 1 } } @@ -571,8 +462,8 @@ abstract class AnchorChannelIntegrationSpec extends ChannelIntegrationSpec { awaitAnnouncements(1) } - def testOpenPayClose(expectedCommitmentFormat: CommitmentFormat): Unit = { - connect(nodes("C"), nodes("F"), 5000000 sat, 0 msat) + def testOpenPayClose(): Unit = { + connect(nodes("C"), nodes("F"), 5000000 sat, 0 msat, channelType) generateBlocks(8) awaitAnnouncements(2) @@ -584,14 +475,14 @@ abstract class AnchorChannelIntegrationSpec extends ChannelIntegrationSpec { sender.send(nodes("F").register, Register.Forward(sender.ref.toTyped[Any], channelId, CMD_GET_CHANNEL_DATA(ActorRef.noSender))) val initialStateDataF = sender.expectMsgType[RES_GET_CHANNEL_DATA[DATA_NORMAL]].data - assert(initialStateDataF.commitments.params.commitmentFormat == expectedCommitmentFormat) + assert(initialStateDataF.commitments.latest.commitmentFormat == channelType.commitmentFormat) val initialCommitmentIndex = initialStateDataF.commitments.localCommitIndex - // the 'to remote' address is a simple script spending to the remote payment basepoint with a 1-block CSV delay - val toRemoteAddress = Script.pay2wsh(Scripts.toRemoteDelayed(initialStateDataF.commitments.params.remoteParams.paymentBasepoint)) - - // toRemote output of C as seen by F - val Some(toRemoteOutC) = initialStateDataF.commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx.txOut.find(_.publicKeyScript == Script.write(toRemoteAddress)) + val toRemoteAddress = { + val channelKeys = nodes("F").nodeParams.channelKeyManager.channelKeys(initialStateDataF.channelParams.channelConfig, initialStateDataF.channelParams.localParams.fundingKeyPath) + val toRemote = Scripts.toRemoteDelayed(initialStateDataF.commitments.latest.localKeys(channelKeys).publicKeys) + Script.write(Script.pay2wsh(toRemote)) + } // let's make a payment to advance the commit index val amountMsat = 4200000.msat @@ -615,16 +506,10 @@ abstract class AnchorChannelIntegrationSpec extends ChannelIntegrationSpec { sender.send(nodes("F").register, Register.Forward(sender.ref.toTyped[Any], channelId, CMD_GET_CHANNEL_DATA(ActorRef.noSender))) val stateDataF = sender.expectMsgType[RES_GET_CHANNEL_DATA[DATA_NORMAL]].data val commitmentIndex = stateDataF.commitments.localCommitIndex - val commitTx = stateDataF.commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx - val Some(toRemoteOutCNew) = commitTx.txOut.find(_.publicKeyScript == Script.write(toRemoteAddress)) - + val commitTxId = stateDataF.commitments.latest.localCommit.txId // there is a new commitment index in the channel state assert(commitmentIndex > initialCommitmentIndex) - // script pubkeys of toRemote output remained the same across commitments - assert(toRemoteOutCNew.publicKeyScript == toRemoteOutC.publicKeyScript) - assert(toRemoteOutCNew.amount < toRemoteOutC.amount) - val stateListener = TestProbe() nodes("C").system.eventStream.subscribe(stateListener.ref, classOf[ChannelStateChanged]) @@ -637,16 +522,17 @@ abstract class AnchorChannelIntegrationSpec extends ChannelIntegrationSpec { awaitCond(stateListener.expectMsgType[ChannelStateChanged](max = 60 seconds).currentState == CLOSING, max = 60 seconds) val bitcoinClient = new BitcoinCoreClient(bitcoinrpcclient) - awaitCond({ - bitcoinClient.getTransaction(commitTx.txid).map(tx => Some(tx)).recover(_ => None).pipeTo(sender.ref) + val commitTx = awaitAssert({ + bitcoinClient.getTransaction(commitTxId).map(tx => Some(tx)).recover(_ => None).pipeTo(sender.ref) val tx = sender.expectMsgType[Option[Transaction]] // the unilateral close contains the static toRemote output - tx.exists(_.txOut.exists(_.publicKeyScript == toRemoteOutC.publicKeyScript)) + assert(tx.exists(_.txOut.exists(_.publicKeyScript == toRemoteAddress))) + tx.get }, max = 20 seconds, interval = 1 second) // bury the unilateral close in a block, C should claim its main output generateBlocks(2) - val mainOutputC = OutPoint(commitTx, commitTx.txOut.indexWhere(_.publicKeyScript == toRemoteOutC.publicKeyScript)) + val mainOutputC = OutPoint(commitTx, commitTx.txOut.indexWhere(_.publicKeyScript == toRemoteAddress)) awaitCond({ bitcoinClient.getMempool().pipeTo(sender.ref) sender.expectMsgType[Seq[Transaction]].exists(_.txIn.head.outPoint == mainOutputC) @@ -659,7 +545,7 @@ abstract class AnchorChannelIntegrationSpec extends ChannelIntegrationSpec { } def testPunishRevokedCommit(): Unit = { - val revokedCommitFixture = testRevokedCommit(commitmentFormat) + val revokedCommitFixture = testRevokedCommit() import revokedCommitFixture._ val bitcoinClient = new BitcoinCoreClient(bitcoinrpcclient) @@ -689,78 +575,38 @@ abstract class AnchorChannelIntegrationSpec extends ChannelIntegrationSpec { } -class AnchorOutputChannelIntegrationSpec extends AnchorChannelIntegrationSpec { - - override val commitmentFormat = Transactions.UnsafeLegacyAnchorOutputsCommitmentFormat - - test("start eclair nodes") { - instantiateEclairNode("A", ConfigFactory.parseMap(Map("eclair.node-alias" -> "A", "eclair.channel.expiry-delta-blocks" -> 40, "eclair.channel.fulfill-safety-before-timeout-blocks" -> 12, "eclair.server.port" -> 29750, "eclair.api.port" -> 28093).asJava).withFallback(withStaticRemoteKey).withFallback(commonConfig)) - instantiateEclairNode("C", ConfigFactory.parseMap(Map("eclair.node-alias" -> "C", "eclair.channel.expiry-delta-blocks" -> 40, "eclair.channel.fulfill-safety-before-timeout-blocks" -> 12, "eclair.server.port" -> 29751, "eclair.api.port" -> 28094).asJava).withFallback(withAnchorOutputs).withFallback(commonConfig)) - instantiateEclairNode("F", ConfigFactory.parseMap(Map("eclair.node-alias" -> "F", "eclair.channel.expiry-delta-blocks" -> 40, "eclair.channel.fulfill-safety-before-timeout-blocks" -> 12, "eclair.server.port" -> 29753, "eclair.api.port" -> 28095).asJava).withFallback(withAnchorOutputs).withFallback(commonConfig)) - } - - test("connect nodes") { - connectNodes(DefaultCommitmentFormat) - } - - test("open channel C <-> F, send payments and close (anchor outputs)") { - testOpenPayClose(commitmentFormat) - } - - test("propagate a fulfill upstream when a downstream htlc is redeemed on-chain (local commit, anchor outputs)") { - testDownstreamFulfillLocalCommit(commitmentFormat) - } - - test("propagate a fulfill upstream when a downstream htlc is redeemed on-chain (remote commit, anchor outputs)") { - testDownstreamFulfillRemoteCommit(commitmentFormat) - } - - test("propagate a failure upstream when a downstream htlc times out (local commit, anchor outputs)") { - testDownstreamTimeoutLocalCommit(commitmentFormat) - } - - test("propagate a failure upstream when a downstream htlc times out (remote commit, anchor outputs)") { - testDownstreamTimeoutRemoteCommit(commitmentFormat) - } - - test("punish a node that has published a revoked commit tx (anchor outputs)") { - testPunishRevokedCommit() - } - -} - class AnchorOutputZeroFeeHtlcTxsChannelIntegrationSpec extends AnchorChannelIntegrationSpec { - override val commitmentFormat = Transactions.ZeroFeeHtlcTxAnchorOutputsCommitmentFormat + override val channelType: SupportedChannelType = ChannelTypes.AnchorOutputsZeroFeeHtlcTx() test("start eclair nodes") { - instantiateEclairNode("A", ConfigFactory.parseMap(Map("eclair.node-alias" -> "A", "eclair.channel.expiry-delta-blocks" -> 40, "eclair.channel.fulfill-safety-before-timeout-blocks" -> 12, "eclair.server.port" -> 29760, "eclair.api.port" -> 28096).asJava).withFallback(withStaticRemoteKey).withFallback(commonConfig)) - instantiateEclairNode("C", ConfigFactory.parseMap(Map("eclair.node-alias" -> "C", "eclair.channel.expiry-delta-blocks" -> 40, "eclair.channel.fulfill-safety-before-timeout-blocks" -> 12, "eclair.server.port" -> 29761, "eclair.api.port" -> 28097).asJava).withFallback(withAnchorOutputsZeroFeeHtlcTxs).withFallback(commonConfig)) - instantiateEclairNode("F", ConfigFactory.parseMap(Map("eclair.node-alias" -> "F", "eclair.channel.expiry-delta-blocks" -> 40, "eclair.channel.fulfill-safety-before-timeout-blocks" -> 12, "eclair.server.port" -> 29763, "eclair.api.port" -> 28098).asJava).withFallback(withAnchorOutputsZeroFeeHtlcTxs).withFallback(commonConfig)) + instantiateEclairNode("A", ConfigFactory.parseMap(Map("eclair.node-alias" -> "A", "eclair.channel.expiry-delta-blocks" -> 40, "eclair.channel.fulfill-safety-before-timeout-blocks" -> 12, "eclair.server.port" -> (if (useEclairSigner) 29840 else 29740), "eclair.api.port" -> (if (useEclairSigner) 28190 else 28090)).asJava).withFallback(withAnchorOutputsZeroFeeHtlcTxs).withFallback(commonConfig)) + instantiateEclairNode("C", ConfigFactory.parseMap(Map("eclair.node-alias" -> "C", "eclair.channel.expiry-delta-blocks" -> 40, "eclair.channel.fulfill-safety-before-timeout-blocks" -> 12, "eclair.server.port" -> (if (useEclairSigner) 29841 else 29741), "eclair.api.port" -> (if (useEclairSigner) 28191 else 28091)).asJava).withFallback(withAnchorOutputsZeroFeeHtlcTxs).withFallback(commonConfig)) + instantiateEclairNode("F", ConfigFactory.parseMap(Map("eclair.node-alias" -> "F", "eclair.channel.expiry-delta-blocks" -> 40, "eclair.channel.fulfill-safety-before-timeout-blocks" -> 12, "eclair.server.port" -> (if (useEclairSigner) 29842 else 29742), "eclair.api.port" -> (if (useEclairSigner) 28192 else 28092)).asJava).withFallback(withAnchorOutputsZeroFeeHtlcTxs).withFallback(commonConfig)) } test("connect nodes") { - connectNodes(DefaultCommitmentFormat) + connectNodes() } test("open channel C <-> F, send payments and close (anchor outputs zero fee htlc txs)") { - testOpenPayClose(commitmentFormat) + testOpenPayClose() } test("propagate a fulfill upstream when a downstream htlc is redeemed on-chain (local commit, anchor outputs zero fee htlc txs)") { - testDownstreamFulfillLocalCommit(commitmentFormat) + testDownstreamFulfillLocalCommit() } test("propagate a fulfill upstream when a downstream htlc is redeemed on-chain (remote commit, anchor outputs zero fee htlc txs)") { - testDownstreamFulfillRemoteCommit(commitmentFormat) + testDownstreamFulfillRemoteCommit() } test("propagate a failure upstream when a downstream htlc times out (local commit, anchor outputs zero fee htlc txs)") { - testDownstreamTimeoutLocalCommit(commitmentFormat) + testDownstreamTimeoutLocalCommit() } test("propagate a failure upstream when a downstream htlc times out (remote commit, anchor outputs zero fee htlc txs)") { - testDownstreamTimeoutRemoteCommit(commitmentFormat) + testDownstreamTimeoutRemoteCommit() } test("punish a node that has published a revoked commit tx (anchor outputs)") { @@ -768,3 +614,7 @@ class AnchorOutputZeroFeeHtlcTxsChannelIntegrationSpec extends AnchorChannelInte } } + +class AnchorOutputZeroFeeHtlcTxsChannelWithEclairSignerIntegrationSpec extends AnchorOutputZeroFeeHtlcTxsChannelIntegrationSpec { + override def useEclairSigner: Boolean = true +} \ No newline at end of file diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/IntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/IntegrationSpec.scala index 04cbfa7097..d9bdd08397 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/IntegrationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/IntegrationSpec.scala @@ -22,10 +22,11 @@ import com.typesafe.config.{Config, ConfigFactory} import fr.acinq.bitcoin.scalacompat.Satoshi import fr.acinq.eclair.Features._ import fr.acinq.eclair.blockchain.bitcoind.BitcoindService +import fr.acinq.eclair.channel.SupportedChannelType import fr.acinq.eclair.io.Peer.OpenChannelResponse import fr.acinq.eclair.io.{Peer, PeerConnection} import fr.acinq.eclair.payment.relay.Relayer.RelayFees -import fr.acinq.eclair.router.Graph.PaymentWeightRatios +import fr.acinq.eclair.router.Graph.HeuristicsConstants import fr.acinq.eclair.router.RouteCalculation.ROUTE_MAX_LENGTH import fr.acinq.eclair.router.Router.{MultiPartParams, PathFindingConf, SearchBoundaries, NORMAL => _, State => _} import fr.acinq.eclair.{BlockHeight, CltvExpiryDelta, Kit, MilliSatoshi, MilliSatoshiLong, Setup, TestKitBaseClass} @@ -57,14 +58,14 @@ abstract class IntegrationSpec extends TestKitBaseClass with BitcoindService wit maxFeeProportional = 0.03, maxCltv = CltvExpiryDelta(Int.MaxValue), maxRouteLength = ROUTE_MAX_LENGTH), - heuristics = PaymentWeightRatios( - baseFactor = 0, - cltvDeltaFactor = 1, - ageFactor = 0, - capacityFactor = 0, + heuristics = HeuristicsConstants( + lockedFundsRisk = 0, + failureFees = RelayFees(0 msat, 0), hopFees = RelayFees(0 msat, 0), + useLogProbability = false, + usePastRelaysData = false ), - mpp = MultiPartParams(15000000 msat, 6), + mpp = MultiPartParams(15000000 msat, 6, MultiPartParams.FullCapacity), experimentName = "my-test-experiment", experimentPercentage = 100 ).getDefaultRouteParams @@ -85,12 +86,12 @@ abstract class IntegrationSpec extends TestKitBaseClass with BitcoindService wit "eclair.channel.max-htlc-value-in-flight-percent" -> 100, "eclair.channel.max-block-processing-delay" -> "2 seconds", "eclair.channel.to-remote-delay-blocks" -> 24, + "eclair.router.channel-spent-splice-delay" -> 12, "eclair.router.broadcast-interval" -> "2 seconds", "eclair.auto-reconnect" -> false, "eclair.multi-part-payment-expiry" -> "20 seconds", "eclair.channel.channel-update.balance-thresholds" -> Nil.asJava, - "eclair.channel.channel-update.min-time-between-updates" -> java.time.Duration.ZERO, - "eclair.channel.accept-incoming-static-remote-key-channels" -> true).asJava).withFallback(ConfigFactory.load()) + "eclair.channel.channel-update.min-time-between-updates" -> java.time.Duration.ZERO).asJava).withFallback(ConfigFactory.load()) private val commonFeatures = ConfigFactory.parseMap(Map( s"eclair.features.${DataLossProtect.rfcName}" -> "optional", @@ -103,27 +104,18 @@ abstract class IntegrationSpec extends TestKitBaseClass with BitcoindService wit s"eclair.features.${ShutdownAnySegwit.rfcName}" -> "optional", s"eclair.features.${ChannelType.rfcName}" -> "optional", s"eclair.features.${RouteBlinding.rfcName}" -> "optional", + s"eclair.features.${StaticRemoteKey.rfcName}" -> "mandatory", // We keep dual-funding disabled in tests, unless explicitly requested, as most of the network doesn't support it yet. s"eclair.features.${DualFunding.rfcName}" -> "disabled", ).asJava) - val withStaticRemoteKey = commonFeatures.withFallback(ConfigFactory.parseMap(Map( - s"eclair.features.${StaticRemoteKey.rfcName}" -> "mandatory", - s"eclair.features.${AnchorOutputs.rfcName}" -> "disabled", - s"eclair.features.${AnchorOutputsZeroFeeHtlcTx.rfcName}" -> "disabled", - ).asJava)) - - val withAnchorOutputs = ConfigFactory.parseMap(Map( - s"eclair.features.${AnchorOutputs.rfcName}" -> "optional" - ).asJava).withFallback(withStaticRemoteKey) - val withAnchorOutputsZeroFeeHtlcTxs = ConfigFactory.parseMap(Map( s"eclair.features.${AnchorOutputsZeroFeeHtlcTx.rfcName}" -> "optional" - ).asJava).withFallback(withStaticRemoteKey) + ).asJava).withFallback(commonFeatures) val withDualFunding = ConfigFactory.parseMap(Map( s"eclair.features.${DualFunding.rfcName}" -> "optional" - ).asJava).withFallback(withAnchorOutputsZeroFeeHtlcTxs) + ).asJava).withFallback(commonFeatures) implicit val formats: Formats = DefaultFormats @@ -173,13 +165,13 @@ abstract class IntegrationSpec extends TestKitBaseClass with BitcoindService wit sender.expectMsgType[PeerConnection.ConnectionResult.HasConnection](10 seconds) } - def connect(node1: Kit, node2: Kit, fundingAmount: Satoshi, pushMsat: MilliSatoshi): OpenChannelResponse.Created = { + def connect(node1: Kit, node2: Kit, fundingAmount: Satoshi, pushMsat: MilliSatoshi, channelType: SupportedChannelType): OpenChannelResponse.Created = { val sender = TestProbe() connect(node1, node2) sender.send(node1.switchboard, Peer.OpenChannel( remoteNodeId = node2.nodeParams.nodeId, fundingAmount = fundingAmount, - channelType_opt = None, + channelType_opt = Some(channelType), pushAmount_opt = Some(pushMsat), fundingTxFeerate_opt = None, fundingTxFeeBudget_opt = None, diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/MessageIntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/MessageIntegrationSpec.scala index 53fb435af5..21dfc1d61c 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/MessageIntegrationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/MessageIntegrationSpec.scala @@ -23,12 +23,12 @@ import akka.testkit.TestProbe import akka.util.Timeout import com.typesafe.config.ConfigFactory import fr.acinq.bitcoin.Transaction -import fr.acinq.bitcoin.scalacompat.{ByteVector32, Satoshi} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, SatoshiLong} import fr.acinq.eclair.TestUtils.waitEventStreamSynced import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.{Watch, WatchFundingConfirmed} import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinCoreClient -import fr.acinq.eclair.channel.{CMD_CLOSE, RES_SUCCESS, Register} +import fr.acinq.eclair.channel.{CMD_CLOSE, ChannelTypes, RES_SUCCESS, Register} import fr.acinq.eclair.io.Switchboard import fr.acinq.eclair.message.OnionMessages import fr.acinq.eclair.message.OnionMessages.{IntermediateNode, Recipient, buildRoute} @@ -36,7 +36,7 @@ import fr.acinq.eclair.router.Router import fr.acinq.eclair.wire.protocol.OnionMessagePayloadTlv.ReplyPath import fr.acinq.eclair.wire.protocol.TlvCodecs.genericTlv import fr.acinq.eclair.wire.protocol.{GenericTlv, NodeAnnouncement} -import fr.acinq.eclair.{EclairImpl, EncodedNodeId, Features, MilliSatoshi, SendOnionMessageResponse, UInt64, randomBytes, randomKey} +import fr.acinq.eclair.{EclairImpl, EncodedNodeId, Features, MilliSatoshiLong, SendOnionMessageResponse, UInt64, randomBytes, randomKey} import scodec.bits.{ByteVector, HexStringSyntax} import scala.concurrent.ExecutionContext.Implicits.global @@ -231,21 +231,20 @@ class MessageIntegrationSpec extends IntegrationSpec { eventListener.expectNoMessage() } - // TODO: fails... test("open channels") { val probe = TestProbe() // We connect A -> B -> C - connect(nodes("B"), nodes("A"), Satoshi(100_000), MilliSatoshi(0)) - connect(nodes("B"), nodes("C"), Satoshi(100_000), MilliSatoshi(0)) + connect(nodes("B"), nodes("A"), 100_000 sat, 0 msat, ChannelTypes.AnchorOutputsZeroFeeHtlcTx()) + connect(nodes("B"), nodes("C"), 100_000 sat, 0 msat, ChannelTypes.AnchorOutputsZeroFeeHtlcTx()) // We connect A -> E -> C - connect(nodes("E"), nodes("A"), Satoshi(100_000), MilliSatoshi(0)) - connect(nodes("E"), nodes("C"), Satoshi(100_000), MilliSatoshi(0)) + connect(nodes("E"), nodes("A"), 100_000 sat, 0 msat, ChannelTypes.AnchorOutputsZeroFeeHtlcTx()) + connect(nodes("E"), nodes("C"), 100_000 sat, 0 msat, ChannelTypes.AnchorOutputsZeroFeeHtlcTx()) // We connect A -> F -> C - connect(nodes("F"), nodes("A"), Satoshi(100_000), MilliSatoshi(0)) - connect(nodes("F"), nodes("C"), Satoshi(100_000), MilliSatoshi(0)) + connect(nodes("F"), nodes("A"), 100_000 sat, 0 msat, ChannelTypes.AnchorOutputsZeroFeeHtlcTx()) + connect(nodes("F"), nodes("C"), 100_000 sat, 0 msat, ChannelTypes.AnchorOutputsZeroFeeHtlcTx()) // we make sure all channels have set up their WatchConfirmed for the funding tx awaitCond({ @@ -265,9 +264,9 @@ class MessageIntegrationSpec extends IntegrationSpec { }, max = 20 seconds, interval = 500 millis) // We also connect A -> D, B -> D, C -> D - connect(nodes("D"), nodes("A"), Satoshi(100_000), MilliSatoshi(0)) - connect(nodes("D"), nodes("B"), Satoshi(100_000), MilliSatoshi(0)) - connect(nodes("D"), nodes("C"), Satoshi(100_000), MilliSatoshi(0)) + connect(nodes("D"), nodes("A"), 100_000 sat, 0 msat, ChannelTypes.AnchorOutputsZeroFeeHtlcTx()) + connect(nodes("D"), nodes("B"), 100_000 sat, 0 msat, ChannelTypes.AnchorOutputsZeroFeeHtlcTx()) + connect(nodes("D"), nodes("C"), 100_000 sat, 0 msat, ChannelTypes.AnchorOutputsZeroFeeHtlcTx()) // we make sure all channels have set up their WatchConfirmed for the funding tx awaitCond({ diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/PaymentIntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/PaymentIntegrationSpec.scala index c4c59b6eaa..5dd30389b1 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/PaymentIntegrationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/PaymentIntegrationSpec.scala @@ -43,7 +43,7 @@ import fr.acinq.eclair.payment.receive.MultiPartHandler.ReceiveStandardPayment import fr.acinq.eclair.payment.relay.Relayer import fr.acinq.eclair.payment.relay.Relayer.RelayFees import fr.acinq.eclair.payment.send.PaymentInitiator.{SendPaymentToNode, SendTrampolinePayment} -import fr.acinq.eclair.router.Graph.PaymentWeightRatios +import fr.acinq.eclair.router.Graph.HeuristicsConstants import fr.acinq.eclair.router.Router.{ChannelHop, GossipDecision, PublicChannel} import fr.acinq.eclair.router.{Announcements, AnnouncementsBatchValidationSpec, Router} import fr.acinq.eclair.wire.protocol.OfferTypes.{Offer, OfferPaths} @@ -64,10 +64,10 @@ import scala.jdk.CollectionConverters._ class PaymentIntegrationSpec extends IntegrationSpec { test("start eclair nodes") { - instantiateEclairNode("A", ConfigFactory.parseMap(Map("eclair.node-alias" -> "A", "eclair.channel.expiry-delta-blocks" -> 130, "eclair.server.port" -> 29730, "eclair.api.port" -> 28080, "eclair.channel.channel-flags.announce-channel" -> false).asJava).withFallback(withStaticRemoteKey).withFallback(commonConfig)) // A's channels are private - instantiateEclairNode("B", ConfigFactory.parseMap(Map("eclair.node-alias" -> "B", "eclair.channel.expiry-delta-blocks" -> 131, "eclair.server.port" -> 29731, "eclair.api.port" -> 28081, "eclair.trampoline-payments-enable" -> true, "eclair.onion-messages.relay-policy" -> "relay-all").asJava).withFallback(withStaticRemoteKey).withFallback(commonConfig)) + instantiateEclairNode("A", ConfigFactory.parseMap(Map("eclair.node-alias" -> "A", "eclair.channel.expiry-delta-blocks" -> 130, "eclair.server.port" -> 29730, "eclair.api.port" -> 28080, "eclair.channel.channel-flags.announce-channel" -> false).asJava).withFallback(withAnchorOutputsZeroFeeHtlcTxs).withFallback(commonConfig)) // A's channels are private + instantiateEclairNode("B", ConfigFactory.parseMap(Map("eclair.node-alias" -> "B", "eclair.channel.expiry-delta-blocks" -> 131, "eclair.server.port" -> 29731, "eclair.api.port" -> 28081, "eclair.trampoline-payments-enable" -> true, "eclair.onion-messages.relay-policy" -> "relay-all").asJava).withFallback(withAnchorOutputsZeroFeeHtlcTxs).withFallback(commonConfig)) instantiateEclairNode("C", ConfigFactory.parseMap(Map("eclair.node-alias" -> "C", "eclair.channel.expiry-delta-blocks" -> 132, "eclair.server.port" -> 29732, "eclair.api.port" -> 28082, "eclair.trampoline-payments-enable" -> true).asJava).withFallback(withDualFunding).withFallback(commonConfig)) - instantiateEclairNode("D", ConfigFactory.parseMap(Map("eclair.node-alias" -> "D", "eclair.channel.expiry-delta-blocks" -> 133, "eclair.server.port" -> 29733, "eclair.api.port" -> 28083, "eclair.trampoline-payments-enable" -> true).asJava).withFallback(withStaticRemoteKey).withFallback(commonConfig)) + instantiateEclairNode("D", ConfigFactory.parseMap(Map("eclair.node-alias" -> "D", "eclair.channel.expiry-delta-blocks" -> 133, "eclair.server.port" -> 29733, "eclair.api.port" -> 28083, "eclair.trampoline-payments-enable" -> true).asJava).withFallback(withAnchorOutputsZeroFeeHtlcTxs).withFallback(commonConfig)) instantiateEclairNode("E", ConfigFactory.parseMap(Map("eclair.node-alias" -> "E", "eclair.channel.expiry-delta-blocks" -> 134, "eclair.server.port" -> 29734, "eclair.api.port" -> 28084).asJava).withFallback(withDualFunding).withFallback(commonConfig)) instantiateEclairNode("F", ConfigFactory.parseMap(Map("eclair.node-alias" -> "F", "eclair.channel.expiry-delta-blocks" -> 135, "eclair.server.port" -> 29735, "eclair.api.port" -> 28085, "eclair.trampoline-payments-enable" -> true).asJava).withFallback(commonConfig)) instantiateEclairNode("G", ConfigFactory.parseMap(Map("eclair.node-alias" -> "G", "eclair.channel.expiry-delta-blocks" -> 136, "eclair.server.port" -> 29736, "eclair.api.port" -> 28086, "eclair.relay.fees.public-channels.fee-base-msat" -> 1010, "eclair.relay.fees.public-channels.fee-proportional-millionths" -> 102, "eclair.trampoline-payments-enable" -> true).asJava).withFallback(commonConfig)) @@ -86,15 +86,15 @@ class PaymentIntegrationSpec extends IntegrationSpec { val eventListener = TestProbe() nodes.values.foreach(_.system.eventStream.subscribe(eventListener.ref, classOf[ChannelStateChanged])) - connect(nodes("A"), nodes("B"), 11000000 sat, 0 msat) - connect(nodes("B"), nodes("C"), 2000000 sat, 0 msat) - connect(nodes("C"), nodes("D"), 5000000 sat, 0 msat) - connect(nodes("C"), nodes("D"), 5000000 sat, 0 msat) - connect(nodes("C"), nodes("F"), 16000000 sat, 0 msat) - connect(nodes("B"), nodes("E"), 10000000 sat, 0 msat) - connect(nodes("E"), nodes("C"), 10000000 sat, 0 msat) - connect(nodes("B"), nodes("G"), 16000000 sat, 0 msat) - connect(nodes("G"), nodes("C"), 16000000 sat, 0 msat) + connect(nodes("A"), nodes("B"), 11000000 sat, 0 msat, ChannelTypes.AnchorOutputsZeroFeeHtlcTx()) + connect(nodes("B"), nodes("C"), 2000000 sat, 0 msat, ChannelTypes.AnchorOutputsZeroFeeHtlcTx()) + connect(nodes("C"), nodes("D"), 5000000 sat, 0 msat, ChannelTypes.AnchorOutputsZeroFeeHtlcTx()) + connect(nodes("C"), nodes("D"), 5000000 sat, 0 msat, ChannelTypes.AnchorOutputsZeroFeeHtlcTx()) + connect(nodes("C"), nodes("F"), 16000000 sat, 0 msat, ChannelTypes.AnchorOutputsZeroFeeHtlcTx()) + connect(nodes("B"), nodes("E"), 10000000 sat, 0 msat, ChannelTypes.AnchorOutputsZeroFeeHtlcTx()) + connect(nodes("E"), nodes("C"), 10000000 sat, 0 msat, ChannelTypes.AnchorOutputsZeroFeeHtlcTx()) + connect(nodes("B"), nodes("G"), 16000000 sat, 0 msat, ChannelTypes.AnchorOutputsZeroFeeHtlcTx()) + connect(nodes("G"), nodes("C"), 16000000 sat, 0 msat, ChannelTypes.AnchorOutputsZeroFeeHtlcTx()) val numberOfChannels = 9 val channelEndpointsCount = 2 * numberOfChannels @@ -158,8 +158,9 @@ class PaymentIntegrationSpec extends IntegrationSpec { } test("send an HTLC A->D") { - val (sender, eventListener) = (TestProbe(), TestProbe()) + val (sender, eventListener, holdTimesRecorder) = (TestProbe(), TestProbe(), TestProbe()) nodes("D").system.eventStream.subscribe(eventListener.ref, classOf[PaymentMetadataReceived]) + nodes("A").system.eventStream.subscribe(holdTimesRecorder.ref, classOf[Router.ReportedHoldTimes]) // first we retrieve a payment hash from D val amountMsat = 4200000.msat @@ -174,6 +175,8 @@ class PaymentIntegrationSpec extends IntegrationSpec { assert(ps.id == paymentId) assert(Crypto.sha256(ps.paymentPreimage) == invoice.paymentHash) eventListener.expectMsg(PaymentMetadataReceived(invoice.paymentHash, invoice.paymentMetadata.get)) + + assert(holdTimesRecorder.expectMsgType[Router.ReportedHoldTimes].holdTimes.map(_.remoteNodeId) == Seq("B", "C", "D").map(nodes(_).nodeParams.nodeId)) } test("send an HTLC A->D with an invalid expiry delta for B") { @@ -243,7 +246,9 @@ class PaymentIntegrationSpec extends IntegrationSpec { } test("send an HTLC A->D with an unknown payment hash") { - val sender = TestProbe() + val (sender, holdTimesRecorder) = (TestProbe(), TestProbe()) + nodes("A").system.eventStream.subscribe(holdTimesRecorder.ref, classOf[Router.ReportedHoldTimes]) + val amount = 100000000 msat val unknownInvoice = Bolt11Invoice(Block.RegtestGenesisBlock.hash, Some(amount), randomBytes32(), nodes("D").nodeParams.privateKey, Left("test"), finalCltvExpiryDelta) val invoice = SendPaymentToNode(sender.ref, amount, unknownInvoice, Nil, routeParams = integrationTestRouteParams, maxAttempts = 5) @@ -256,6 +261,8 @@ class PaymentIntegrationSpec extends IntegrationSpec { assert(failed.paymentHash == invoice.paymentHash) assert(failed.failures.size == 1) assert(failed.failures.head.asInstanceOf[RemoteFailure].e == DecryptedFailurePacket(nodes("D").nodeParams.nodeId, IncorrectOrUnknownPaymentDetails(amount, getBlockHeight()))) + + assert(holdTimesRecorder.expectMsgType[Router.ReportedHoldTimes].holdTimes.map(_.remoteNodeId) == Seq("B", "C", "D").map(nodes(_).nodeParams.nodeId)) } test("send an HTLC A->D with a lower amount than requested") { @@ -327,17 +334,20 @@ class PaymentIntegrationSpec extends IntegrationSpec { } test("send an HTLC A->B->G->C using heuristics to select the route") { - val sender = TestProbe() + val (sender, holdTimesRecorder) = (TestProbe(), TestProbe()) + nodes("A").system.eventStream.subscribe(holdTimesRecorder.ref, classOf[Router.ReportedHoldTimes]) // first we retrieve a payment hash from C - val amountMsat = 2000.msat + val amountMsat = 200000.msat sender.send(nodes("C").paymentHandler, ReceiveStandardPayment(sender.ref, Some(amountMsat), Left("Change from coffee"))) val invoice = sender.expectMsgType[Bolt11Invoice] // the payment is requesting to use a capacity-optimized route which will select node G even though it's a bit more expensive - sender.send(nodes("A").paymentInitiator, SendPaymentToNode(sender.ref, amountMsat, invoice, Nil, maxAttempts = 1, routeParams = integrationTestRouteParams.copy(heuristics = PaymentWeightRatios(0, 0, 0, 1, RelayFees(0 msat, 0))))) + sender.send(nodes("A").paymentInitiator, SendPaymentToNode(sender.ref, amountMsat, invoice, Nil, maxAttempts = 1, routeParams = integrationTestRouteParams.copy(heuristics = HeuristicsConstants(0, RelayFees(10000000000L msat, 10000000000L), RelayFees(0 msat, 0), useLogProbability = false, usePastRelaysData = false)))) sender.expectMsgType[UUID] val ps = sender.expectMsgType[PaymentSent] - ps.parts.foreach(part => assert(part.route.getOrElse(Nil).exists(_.nodeId == nodes("G").nodeParams.nodeId))) + ps.parts.foreach(part => assert(part.route.get.exists(_.nodeId == nodes("G").nodeParams.nodeId))) + + assert(holdTimesRecorder.expectMsgType[Router.ReportedHoldTimes].holdTimes.map(_.remoteNodeId) == Seq("B", "G", "C").map(nodes(_).nodeParams.nodeId)) } test("send a multi-part payment B->D") { @@ -370,7 +380,7 @@ class PaymentIntegrationSpec extends IntegrationSpec { awaitCond(nodes("B").nodeParams.db.audit.listSent(start, TimestampMilli.now()).nonEmpty) val sent = nodes("B").nodeParams.db.audit.listSent(start, TimestampMilli.now()) assert(sent.length == 1, sent) - assert(sent.head.copy(parts = sent.head.parts.sortBy(_.timestamp)) == paymentSent.copy(parts = paymentSent.parts.map(_.copy(route = None)).sortBy(_.timestamp)), sent) + assert(sent.head.copy(parts = sent.head.parts.sortBy(_.timestamp)) == paymentSent.copy(parts = paymentSent.parts.map(_.copy(route = None)).sortBy(_.timestamp), remainingAttribution_opt = None), sent) awaitCond(nodes("D").nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).exists(_.status.isInstanceOf[IncomingPaymentStatus.Received])) val Some(IncomingStandardPayment(_, _, _, _, IncomingPaymentStatus.Received(receivedAmount, _))) = nodes("D").nodeParams.db.payments.getIncomingPayment(invoice.paymentHash) @@ -463,7 +473,9 @@ class PaymentIntegrationSpec extends IntegrationSpec { test("send a trampoline payment B->F1 (via trampoline G)") { val start = TimestampMilli.now() - val sender = TestProbe() + val (sender, holdTimesRecorderB, holdTimesRecorderG) = (TestProbe(), TestProbe(), TestProbe()) + nodes("B").system.eventStream.subscribe(holdTimesRecorderB.ref, classOf[Router.ReportedHoldTimes]) + nodes("G").system.eventStream.subscribe(holdTimesRecorderG.ref, classOf[Router.ReportedHoldTimes]) val amount = 4_000_000_000L.msat sender.send(nodes("F").paymentHandler, ReceiveStandardPayment(sender.ref, Some(amount), Left("like trampoline much?"))) val invoice = sender.expectMsgType[Bolt11Invoice] @@ -492,6 +504,9 @@ class PaymentIntegrationSpec extends IntegrationSpec { val relayed = nodes("G").nodeParams.db.audit.listRelayed(start, TimestampMilli.now()).filter(_.paymentHash == invoice.paymentHash).head assert(relayed.amountIn - relayed.amountOut > 0.msat, relayed) assert(relayed.amountIn - relayed.amountOut < paymentSent.feesPaid, relayed) + + assert(holdTimesRecorderG.expectMsgType[Router.ReportedHoldTimes].holdTimes.map(_.remoteNodeId) == Seq("C", "F").map(nodes(_).nodeParams.nodeId)) + assert(holdTimesRecorderB.expectMsgType[Router.ReportedHoldTimes].holdTimes.map(_.remoteNodeId) == Seq("G").map(nodes(_).nodeParams.nodeId)) } test("send a trampoline payment D->B (via trampoline C)") { @@ -567,7 +582,7 @@ class PaymentIntegrationSpec extends IntegrationSpec { } test("send a trampoline payment B->D (temporary local failure at trampoline)") { - val sender = TestProbe() + val (sender, holdTimesRecorder) = (TestProbe(), TestProbe()) // We put most of the capacity C <-> D on D's side. sender.send(nodes("D").paymentHandler, ReceiveStandardPayment(sender.ref, Some(8_000_000_000L msat), Left("plz send everything"))) @@ -583,16 +598,22 @@ class PaymentIntegrationSpec extends IntegrationSpec { assert(invoice.features.hasFeature(Features.BasicMultiPartPayment)) assert(invoice.features.hasFeature(Features.TrampolinePaymentPrototype)) + nodes("B").system.eventStream.subscribe(holdTimesRecorder.ref, classOf[Router.ReportedHoldTimes]) val payment = SendTrampolinePayment(sender.ref, invoice, nodes("C").nodeParams.nodeId, routeParams = integrationTestRouteParams) sender.send(nodes("B").paymentInitiator, payment) val paymentId = sender.expectMsgType[UUID] val paymentFailed = sender.expectMsgType[PaymentFailed](max = 30 seconds) assert(paymentFailed.id == paymentId, paymentFailed) assert(paymentFailed.paymentHash == invoice.paymentHash, paymentFailed) + + assert(holdTimesRecorder.expectMsgType[Router.ReportedHoldTimes].holdTimes.map(_.remoteNodeId) == Seq("C").map(nodes(_).nodeParams.nodeId)) } test("send a trampoline payment A->D (temporary remote failure at trampoline)") { - val sender = TestProbe() + val (sender, holdTimesRecorderA, holdTimesRecorderB) = (TestProbe(), TestProbe(), TestProbe()) + nodes("A").system.eventStream.subscribe(holdTimesRecorderA.ref, classOf[Router.ReportedHoldTimes]) + nodes("B").system.eventStream.subscribe(holdTimesRecorderB.ref, classOf[Router.ReportedHoldTimes]) + val amount = 1_800_000_000L.msat // B can forward to C, but C doesn't have that much outgoing capacity to D sender.send(nodes("D").paymentHandler, ReceiveStandardPayment(sender.ref, Some(amount), Left("I iz not Satoshi"))) val invoice = sender.expectMsgType[Bolt11Invoice] @@ -605,6 +626,9 @@ class PaymentIntegrationSpec extends IntegrationSpec { val paymentFailed = sender.expectMsgType[PaymentFailed](max = 30 seconds) assert(paymentFailed.id == paymentId, paymentFailed) assert(paymentFailed.paymentHash == invoice.paymentHash, paymentFailed) + + assert(holdTimesRecorderB.expectMsgType[Router.ReportedHoldTimes].holdTimes.map(_.remoteNodeId) == Seq("C").map(nodes(_).nodeParams.nodeId)) + assert(holdTimesRecorderA.expectMsgType[Router.ReportedHoldTimes].holdTimes.map(_.remoteNodeId) == Seq("B").map(nodes(_).nodeParams.nodeId)) } test("send a blinded payment B->D with many blinded routes") { @@ -765,7 +789,9 @@ class PaymentIntegrationSpec extends IntegrationSpec { val offerHandler = TypedProbe[HandlerCommand]()(nodes("D").system.toTyped) nodes("D").offerManager ! RegisterOffer(offer, Some(nodes("D").nodeParams.privateKey), None, offerHandler.ref) - val sender = TestProbe() + val (sender, holdTimesRecorderA, holdTimesRecorderB) = (TestProbe(), TestProbe(), TestProbe()) + nodes("A").system.eventStream.subscribe(holdTimesRecorderA.ref, classOf[Router.ReportedHoldTimes]) + nodes("B").system.eventStream.subscribe(holdTimesRecorderB.ref, classOf[Router.ReportedHoldTimes]) val alice = new EclairImpl(nodes("A")) alice.payOfferTrampoline(offer, amount, 1, nodes("B").nodeParams.nodeId, maxAttempts_opt = Some(1))(30 seconds).pipeTo(sender.ref) @@ -788,6 +814,9 @@ class PaymentIntegrationSpec extends IntegrationSpec { awaitCond(nodes("D").nodeParams.db.payments.getIncomingPayment(paymentSent.paymentHash).exists(_.status.isInstanceOf[IncomingPaymentStatus.Received])) val Some(IncomingBlindedPayment(_, _, _, _, IncomingPaymentStatus.Received(receivedAmount, _))) = nodes("D").nodeParams.db.payments.getIncomingPayment(paymentSent.paymentHash) assert(receivedAmount >= amount) + + assert(holdTimesRecorderB.expectMsgType[Router.ReportedHoldTimes].holdTimes.map(_.remoteNodeId) == Seq("C").map(nodes(_).nodeParams.nodeId)) + assert(holdTimesRecorderA.expectMsgType[Router.ReportedHoldTimes].holdTimes.map(_.remoteNodeId) == Seq("B").map(nodes(_).nodeParams.nodeId)) } test("send to compact route") { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/PerformanceIntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/PerformanceIntegrationSpec.scala index 4329f251ae..50958aefa5 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/PerformanceIntegrationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/PerformanceIntegrationSpec.scala @@ -35,7 +35,6 @@ import java.util.concurrent.Executors import scala.concurrent.duration._ import scala.concurrent.{Await, ExecutionContext, Future} import scala.jdk.CollectionConverters._ -import scala.util.{Success, Try} /** * Created by PM on 12/07/2021. @@ -60,7 +59,7 @@ class PerformanceIntegrationSpec extends IntegrationSpec { val eventListener = TestProbe() nodes.values.foreach(_.system.eventStream.subscribe(eventListener.ref, classOf[ChannelStateChanged])) - connect(nodes("A"), nodes("B"), 100_000_000 sat, 0 msat) + connect(nodes("A"), nodes("B"), 100_000_000 sat, 0 msat, ChannelTypes.AnchorOutputsZeroFeeHtlcTx()) // confirming the funding tx generateBlocks(6) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/StartupIntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/StartupIntegrationSpec.scala index b33bfdca2f..68bcb8d186 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/StartupIntegrationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/StartupIntegrationSpec.scala @@ -33,7 +33,7 @@ import scala.jdk.CollectionConverters._ class StartupIntegrationSpec extends IntegrationSpec { private def createConfig(wallet_opt: Option[String], waitForBitcoind: Boolean = false): Config = { - val defaultConfig = ConfigFactory.parseMap(Map("eclair.bitcoind.wait-for-bitcoind-up" -> waitForBitcoind, "eclair.server.port" -> TestUtils.availablePort).asJava).withFallback(withStaticRemoteKey).withFallback(commonConfig) + val defaultConfig = ConfigFactory.parseMap(Map("eclair.bitcoind.wait-for-bitcoind-up" -> waitForBitcoind, "eclair.server.port" -> TestUtils.availablePort).asJava).withFallback(withAnchorOutputsZeroFeeHtlcTxs).withFallback(commonConfig) wallet_opt match { case Some(wallet) => ConfigFactory.parseMap(Map("eclair.bitcoind.wallet" -> wallet).asJava).withFallback(defaultConfig) case None => defaultConfig.withoutPath("eclair.bitcoind.wallet") diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/channel/GossipIntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/channel/GossipIntegrationSpec.scala index 7498d871aa..24bcd32ebe 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/channel/GossipIntegrationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/channel/GossipIntegrationSpec.scala @@ -107,8 +107,15 @@ class GossipIntegrationSpec extends FixtureSpec with IntegrationPatience { // Bob also creates a channel_announcement for the splice transaction and updates the graph. inside(getRouterData(bob)) { routerData => - assert(routerData.channels.keys == Set(splice_scid_ab, scid_bc)) - assert(routerData.spentChannels.isEmpty) + routerData.spentChannels match { + case spentChannels if spentChannels.isEmpty => + assert(routerData.channels.keys == Set(splice_scid_ab, scid_bc)) + case spentChannels => + // Handle the special case where Bob receives ExternalChannelSpent after validating the local channel update + // for the splice and treating it as a new channel; the original splice will be removed when the splice tx confirms. + assert(spentChannels.contains(spliceTxId) && spentChannels(spliceTxId) == Set(scid_ab)) + assert(routerData.channels.keys == Set(splice_scid_ab, scid_bc, scid_ab)) + } assert(routerData.channels.get(splice_scid_ab).map(_.ann).contains(spliceAnn)) routerData.channels.get(splice_scid_ab).foreach(c => { assert(c.capacity == 200_000.sat) @@ -124,8 +131,15 @@ class GossipIntegrationSpec extends FixtureSpec with IntegrationPatience { // The channel_announcement for the splice propagates to Carol. inside(getRouterData(carol)) { routerData => - assert(routerData.channels.keys == Set(splice_scid_ab, scid_bc)) - assert(routerData.spentChannels.isEmpty) + routerData.spentChannels match { + case spentChannels if spentChannels.isEmpty => + assert(routerData.channels.keys == Set(splice_scid_ab, scid_bc)) + case spentChannels => + // Handle the special case where Carol receives ExternalChannelSpent after validating the local channel update + // for the splice and treating it as a new channel; the original splice will be removed when the splice tx confirms. + assert(spentChannels.contains(spliceTxId) && spentChannels(spliceTxId) == Set(scid_ab)) + assert(routerData.channels.keys == Set(splice_scid_ab, scid_bc, scid_ab)) + } assert(routerData.channels.get(splice_scid_ab).map(_.ann).contains(spliceAnn)) routerData.channels.get(splice_scid_ab).foreach(c => { assert(c.capacity == 200_000.sat) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/fixtures/MinimalNodeFixture.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/fixtures/MinimalNodeFixture.scala index eaff03f4a6..e49f8cc91a 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/fixtures/MinimalNodeFixture.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/fixtures/MinimalNodeFixture.scala @@ -28,9 +28,10 @@ import fr.acinq.eclair.payment.offer.{DefaultOfferHandler, OfferManager} import fr.acinq.eclair.payment.receive.{MultiPartHandler, PaymentHandler} import fr.acinq.eclair.payment.relay.{ChannelRelayer, PostRestartHtlcCleaner, Relayer} import fr.acinq.eclair.payment.send.PaymentInitiator +import fr.acinq.eclair.reputation.ReputationRecorder import fr.acinq.eclair.router.Router import fr.acinq.eclair.wire.protocol.IPAddress -import fr.acinq.eclair.{BlockHeight, EclairImpl, Kit, MilliSatoshi, MilliSatoshiLong, NodeParams, RealShortChannelId, SubscriptionsComplete, TestBitcoinCoreClient, TestDatabases, nodeFee} +import fr.acinq.eclair.{BlockHeight, MilliSatoshi, MilliSatoshiLong, NodeParams, RealShortChannelId, SubscriptionsComplete, TestBitcoinCoreClient, TestDatabases} import org.scalatest.concurrent.{Eventually, IntegrationPatience} import org.scalatest.{Assertions, EitherValues} @@ -69,8 +70,8 @@ object MinimalNodeFixture extends Assertions with Eventually with IntegrationPat NodeParams.makeNodeParams( config = ConfigFactory.load().getConfig("eclair"), instanceId = UUID.randomUUID(), - nodeKeyManager = new LocalNodeKeyManager(seed, Block.RegtestGenesisBlock.hash), - channelKeyManager = new LocalChannelKeyManager(seed, Block.RegtestGenesisBlock.hash), + nodeKeyManager = LocalNodeKeyManager(seed, Block.RegtestGenesisBlock.hash), + channelKeyManager = LocalChannelKeyManager(seed, Block.RegtestGenesisBlock.hash), onChainKeyManager_opt = None, torAddress_opt = None, database = TestDatabases.inMemoryDb(), @@ -97,7 +98,7 @@ object MinimalNodeFixture extends Assertions with Eventually with IntegrationPat val offerManager = system.spawn(OfferManager(nodeParams, 1 minute), "offer-manager") val defaultOfferHandler = system.spawn(DefaultOfferHandler(nodeParams, router), "default-offer-handler") val paymentHandler = system.actorOf(PaymentHandler.props(nodeParams, register, offerManager), "payment-handler") - val relayer = system.actorOf(Relayer.props(nodeParams, router, register, paymentHandler), "relayer") + val relayer = system.actorOf(Relayer.props(nodeParams, router, register, paymentHandler, None), "relayer") val txPublisherFactory = Channel.SimpleTxPublisherFactory(nodeParams, bitcoinClient) val channelFactory = Peer.SimpleChannelFactory(nodeParams, watcherTyped, relayer, wallet, txPublisherFactory) val pendingChannelsRateLimiter = system.spawnAnonymous(Behaviors.supervise(PendingChannelsRateLimiter(nodeParams, router.toTyped, Seq())).onFailure(typed.SupervisorStrategy.resume)) @@ -182,7 +183,7 @@ object MinimalNodeFixture extends Assertions with Eventually with IntegrationPat sender.expectMsgType[ConnectionResult.Connected] } - def openChannel(node1: MinimalNodeFixture, node2: MinimalNodeFixture, funding: Satoshi, channelType_opt: Option[SupportedChannelType] = None)(implicit system: ActorSystem): OpenChannelResponse.Created = { + def openChannel(node1: MinimalNodeFixture, node2: MinimalNodeFixture, funding: Satoshi, channelType_opt: Option[SupportedChannelType] = Some(ChannelTypes.AnchorOutputsZeroFeeHtlcTx()))(implicit system: ActorSystem): OpenChannelResponse.Created = { val sender = TestProbe("sender") sender.send(node1.switchboard, Peer.OpenChannel(node2.nodeParams.nodeId, funding, channelType_opt, None, None, None, None, None, None)) sender.expectMsgType[OpenChannelResponse.Created] @@ -191,7 +192,7 @@ object MinimalNodeFixture extends Assertions with Eventually with IntegrationPat def spliceIn(node1: MinimalNodeFixture, channelId: ByteVector32, amountIn: Satoshi, pushAmount_opt: Option[MilliSatoshi])(implicit system: ActorSystem): CommandResponse[CMD_SPLICE] = { val sender = TestProbe("sender") val spliceIn = SpliceIn(additionalLocalFunding = amountIn, pushAmount = pushAmount_opt.getOrElse(0.msat)) - val cmd = CMD_SPLICE(sender.ref.toTyped, spliceIn_opt = Some(spliceIn), spliceOut_opt = None, requestFunding_opt = None) + val cmd = CMD_SPLICE(sender.ref.toTyped, spliceIn_opt = Some(spliceIn), spliceOut_opt = None, requestFunding_opt = None, channelType_opt = None) sender.send(node1.register, Register.Forward(sender.ref.toTyped, channelId, cmd)) sender.expectMsgType[CommandResponse[CMD_SPLICE]] } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/payment/OfferPaymentSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/payment/OfferPaymentSpec.scala index 859ef6aa1a..74ed201526 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/payment/OfferPaymentSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/payment/OfferPaymentSpec.scala @@ -43,7 +43,8 @@ import fr.acinq.eclair.testutils.FixtureSpec import fr.acinq.eclair.wire.protocol.OfferTypes.{BlindedPath, Offer, OfferPaths} import fr.acinq.eclair.wire.protocol.{IncorrectOrUnknownPaymentDetails, InvalidOnionBlinding} import fr.acinq.eclair.{CltvExpiryDelta, EncodedNodeId, Features, MilliSatoshi, MilliSatoshiLong, ShortChannelId, randomBytes32, randomKey} -import org.scalatest.concurrent.IntegrationPatience +import org.scalatest.concurrent.{IntegrationPatience, PatienceConfiguration} +import org.scalatest.time.{Seconds, Span} import org.scalatest.{Tag, TestData} import scodec.bits.HexStringSyntax @@ -82,9 +83,9 @@ class OfferPaymentSpec extends FixtureSpec with IntegrationPatience { val f = ThreeNodesFixture(aliceParams, bobParams, carolParams, testData.name) import f._ - alice.watcher.setAutoPilot(watcherAutopilot(knownFundingTxs(alice, bob))) + alice.watcher.setAutoPilot(watcherAutopilot(knownFundingTxs(alice, bob, carol))) bob.watcher.setAutoPilot(watcherAutopilot(knownFundingTxs(alice, bob, carol))) - carol.watcher.setAutoPilot(watcherAutopilot(knownFundingTxs(bob, carol))) + carol.watcher.setAutoPilot(watcherAutopilot(knownFundingTxs(alice, bob, carol))) connect(alice, bob) connect(bob, carol) @@ -113,6 +114,8 @@ class OfferPaymentSpec extends FixtureSpec with IntegrationPatience { eventually { assert(getRouterData(alice).channels.size == 3 || testData.tags.contains(PrivateChannels)) + assert(getRouterData(carol).graphWithBalances.graph.getEdgesBetween(alice.nodeId, bob.nodeId).nonEmpty || testData.tags.contains(PrivateChannels)) + assert(getRouterData(carol).graphWithBalances.graph.getEdgesBetween(bob.nodeId, carol.nodeId).size == 2) } } @@ -139,7 +142,7 @@ class OfferPaymentSpec extends FixtureSpec with IntegrationPatience { private def waitForAllChannelUpdates(f: FixtureParam, channelsCount: Int): Unit = { import f._ - eventually { + eventually(timeout = PatienceConfiguration.Timeout(Span(30, Seconds))) { // We wait for Alice and Carol to receive channel updates for the path Alice -> Bob -> Carol. Seq(getRouterData(alice), getRouterData(carol)).foreach(routerData => { assert(routerData.channels.size == channelsCount) @@ -271,7 +274,7 @@ class OfferPaymentSpec extends FixtureSpec with IntegrationPatience { InvoiceRequestActor.Route(route.hops, maxFinalExpiryDelta), InvoiceRequestActor.Route(route.hops, maxFinalExpiryDelta), ) - val (offer, result) = sendOfferPayment(alice, carol, amount, routes, maxAttempts = 3) + val (offer, result) = sendOfferPayment(alice, carol, amount, routes, maxAttempts = 4) val payment = verifyPaymentSuccess(offer, amount, result) assert(payment.parts.length == 2) assert(payment.parts.forall(_.feesPaid > 0.msat)) @@ -290,7 +293,7 @@ class OfferPaymentSpec extends FixtureSpec with IntegrationPatience { InvoiceRequestActor.Route(route.hops, maxFinalExpiryDelta, feeOverride_opt = Some(RelayFees.zero)), InvoiceRequestActor.Route(route.hops, maxFinalExpiryDelta, feeOverride_opt = Some(RelayFees.zero)), ) - val (offer, result) = sendOfferPayment(alice, carol, amount, routes, maxAttempts = 3) + val (offer, result) = sendOfferPayment(alice, carol, amount, routes, maxAttempts = 4) val payment = verifyPaymentSuccess(offer, amount, result) assert(payment.parts.length == 2) assert(payment.parts.forall(_.feesPaid == 0.msat)) @@ -314,7 +317,7 @@ class OfferPaymentSpec extends FixtureSpec with IntegrationPatience { // None of the channels between Bob and Carol have enough balance for the payment: Alice needs to split it. val amount = 50_000_000 msat - val (offer, result) = sendPrivateOfferPayment(alice, carol, amount, routes, maxAttempts = 3) + val (offer, result) = sendPrivateOfferPayment(alice, carol, amount, routes, maxAttempts = 4) val payment = verifyPaymentSuccess(offer, amount, result) assert(payment.parts.length > 1) } @@ -338,7 +341,7 @@ class OfferPaymentSpec extends FixtureSpec with IntegrationPatience { val routes = Seq(InvoiceRequestActor.Route(route.hops, maxFinalExpiryDelta)) val amount1 = 150_000_000 msat - val (offer, result) = sendPrivateOfferPayment(alice, carol, amount1, routes, maxAttempts = 3) + val (offer, result) = sendPrivateOfferPayment(alice, carol, amount1, routes, maxAttempts = 4) val payment = verifyPaymentSuccess(offer, amount1, result) assert(payment.parts.length > 1) } @@ -462,6 +465,9 @@ class OfferPaymentSpec extends FixtureSpec with IntegrationPatience { // We create a first channel between Bob and Carol. val channelId_bc_1 = openChannel(bob, carol, 200_000 sat).channelId waitForChannelCreatedBC(f, channelId_bc_1) + eventually { + assert(getRouterData(carol).graphWithBalances.graph.getEdgesBetween(bob.nodeId, carol.nodeId).nonEmpty) + } val sender = TestProbe() carol.router ! Router.FinalizeRoute(sender.ref.toTyped, Router.PredefinedNodeRoute(50_000_000 msat, Seq(bob.nodeId, carol.nodeId))) @@ -479,7 +485,7 @@ class OfferPaymentSpec extends FixtureSpec with IntegrationPatience { // None of the channels have enough balance for the payment: it must be split. val amount = 150_000_000 msat - val (offer, result) = sendOfferPayment(bob, carol, amount, routes, maxAttempts = 3) + val (offer, result) = sendOfferPayment(bob, carol, amount, routes, maxAttempts = 4) val payment = verifyPaymentSuccess(offer, amount, result) assert(payment.parts.length > 1) } @@ -537,7 +543,7 @@ class OfferPaymentSpec extends FixtureSpec with IntegrationPatience { { val routes = Seq(InvoiceRequestActor.Route(route.hops, maxFinalExpiryDelta)) val amount2 = 200_000_000 msat - val (offer, result) = sendOfferPayment(alice, carol, amount2, routes, maxAttempts = 3) + val (offer, result) = sendOfferPayment(alice, carol, amount2, routes, maxAttempts = 4) val payment = verifyPaymentSuccess(offer, amount2, result) assert(payment.parts.length > 1) } @@ -566,7 +572,7 @@ class OfferPaymentSpec extends FixtureSpec with IntegrationPatience { // Carol receives a payment that requires using MPP. val routes = Seq(InvoiceRequestActor.Route(route.hops, maxFinalExpiryDelta)) val amount = 300_000_000 msat - val (offer, result) = sendOfferPayment(alice, carol, amount, routes, maxAttempts = 3) + val (offer, result) = sendOfferPayment(alice, carol, amount, routes, maxAttempts = 4) val payment = verifyPaymentSuccess(offer, amount, result) assert(payment.parts.length > 1) } @@ -594,7 +600,7 @@ class OfferPaymentSpec extends FixtureSpec with IntegrationPatience { // Carol receives a payment that requires using MPP. val routes = Seq(InvoiceRequestActor.Route(route.hops, maxFinalExpiryDelta)) val amount = 200_000_000 msat - val (offer, result) = sendOfferPayment(alice, carol, amount, routes, maxAttempts = 3) + val (offer, result) = sendOfferPayment(alice, carol, amount, routes, maxAttempts = 4) val payment = verifyPaymentSuccess(offer, amount, result) assert(payment.parts.length > 1) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/zeroconf/ZeroConfActivationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/zeroconf/ZeroConfActivationSpec.scala index 00041387f8..8523fd5ee6 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/zeroconf/ZeroConfActivationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/zeroconf/ZeroConfActivationSpec.scala @@ -43,7 +43,7 @@ class ZeroConfActivationSpec extends FixtureSpec with IntegrationPatience { fixture.cleanup() } - private def createChannel(f: FixtureParam, channelType_opt: Option[SupportedChannelType] = None): ByteVector32 = { + private def createChannel(f: FixtureParam, channelType_opt: Option[SupportedChannelType] = Some(ChannelTypes.AnchorOutputsZeroFeeHtlcTx())): ByteVector32 = { import f._ alice.watcher.setAutoPilot(watcherAutopilot(knownFundingTxs(alice, bob), confirm = false)) @@ -61,8 +61,8 @@ class ZeroConfActivationSpec extends FixtureSpec with IntegrationPatience { val channelId = createChannel(f) eventually { - assert(!getChannelData(alice, channelId).asInstanceOf[ChannelDataWithCommitments].commitments.params.channelFeatures.hasFeature(ZeroConf)) - assert(!getChannelData(bob, channelId).asInstanceOf[ChannelDataWithCommitments].commitments.params.channelFeatures.hasFeature(ZeroConf)) + assert(!getChannelData(alice, channelId).asInstanceOf[ChannelDataWithCommitments].commitments.channelParams.channelFeatures.hasFeature(ZeroConf)) + assert(!getChannelData(bob, channelId).asInstanceOf[ChannelDataWithCommitments].commitments.channelParams.channelFeatures.hasFeature(ZeroConf)) } } @@ -90,8 +90,8 @@ class ZeroConfActivationSpec extends FixtureSpec with IntegrationPatience { val channelId = createChannel(f, channelType_opt = Some(channelType)) eventually { - assert(getChannelData(alice, channelId).asInstanceOf[DATA_NORMAL].commitments.params.channelFeatures.hasFeature(ZeroConf)) - assert(getChannelData(bob, channelId).asInstanceOf[DATA_NORMAL].commitments.params.channelFeatures.hasFeature(ZeroConf)) + assert(getChannelData(alice, channelId).asInstanceOf[DATA_NORMAL].commitments.channelParams.channelFeatures.hasFeature(ZeroConf)) + assert(getChannelData(bob, channelId).asInstanceOf[DATA_NORMAL].commitments.channelParams.channelFeatures.hasFeature(ZeroConf)) } } @@ -123,8 +123,8 @@ class ZeroConfActivationSpec extends FixtureSpec with IntegrationPatience { val channelId = createChannel(f, channelType_opt = Some(channelType)) eventually { - assert(getChannelData(alice, channelId).asInstanceOf[DATA_NORMAL].commitments.params.channelFeatures.hasFeature(ZeroConf)) - assert(getChannelData(bob, channelId).asInstanceOf[DATA_NORMAL].commitments.params.channelFeatures.hasFeature(ZeroConf)) + assert(getChannelData(alice, channelId).asInstanceOf[DATA_NORMAL].commitments.channelParams.channelFeatures.hasFeature(ZeroConf)) + assert(getChannelData(bob, channelId).asInstanceOf[DATA_NORMAL].commitments.channelParams.channelFeatures.hasFeature(ZeroConf)) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/zeroconf/ZeroConfAliasIntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/zeroconf/ZeroConfAliasIntegrationSpec.scala index 7fd3bdc948..fd1b540cbf 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/zeroconf/ZeroConfAliasIntegrationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/zeroconf/ZeroConfAliasIntegrationSpec.scala @@ -122,9 +122,9 @@ class ZeroConfAliasIntegrationSpec extends FixtureSpec with IntegrationPatience val (_, channelId_bc) = createChannels(f, confirm) eventually { - assert(getChannelData(bob, channelId_bc).asInstanceOf[DATA_NORMAL].commitments.params.channelFeatures.features.contains(ZeroConf) == bcZeroConf) - assert(getChannelData(bob, channelId_bc).asInstanceOf[DATA_NORMAL].commitments.params.channelFeatures.features.contains(ScidAlias) == bcScidAlias) - assert(getChannelData(bob, channelId_bc).asInstanceOf[DATA_NORMAL].commitments.params.channelFlags.announceChannel == bcPublic) + assert(getChannelData(bob, channelId_bc).asInstanceOf[DATA_NORMAL].commitments.channelParams.channelFeatures.features.contains(ZeroConf) == bcZeroConf) + assert(getChannelData(bob, channelId_bc).asInstanceOf[DATA_NORMAL].commitments.channelParams.channelFeatures.features.contains(ScidAlias) == bcScidAlias) + assert(getChannelData(bob, channelId_bc).asInstanceOf[DATA_NORMAL].commitments.channelParams.channelFlags.announceChannel == bcPublic) if (confirm) { assert(getChannelData(bob, channelId_bc).asInstanceOf[DATA_NORMAL].commitments.latest.localFundingStatus.isInstanceOf[LocalFundingStatus.ConfirmedFundingTx]) } else { @@ -261,8 +261,8 @@ class ZeroConfAliasIntegrationSpec extends FixtureSpec with IntegrationPatience val (_, channelId_bc) = createChannels(f, confirm = false) eventually { - assert(getChannelData(bob, channelId_bc).asInstanceOf[DATA_NORMAL].commitments.params.channelFeatures.features.contains(ZeroConf)) - assert(getChannelData(bob, channelId_bc).asInstanceOf[DATA_NORMAL].commitments.params.channelFeatures.features.contains(ScidAlias)) + assert(getChannelData(bob, channelId_bc).asInstanceOf[DATA_NORMAL].commitments.channelParams.channelFeatures.features.contains(ZeroConf)) + assert(getChannelData(bob, channelId_bc).asInstanceOf[DATA_NORMAL].commitments.channelParams.channelFeatures.features.contains(ScidAlias)) assert(getChannelData(bob, channelId_bc).asInstanceOf[DATA_NORMAL].commitments.latest.shortChannelId_opt.isEmpty) assert(getRouterData(bob).privateChannels.values.exists(_.nodeId2 == carol.nodeParams.nodeId)) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/interop/rustytests/RustyTestsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/interop/rustytests/RustyTestsSpec.scala deleted file mode 100644 index 5b936aeba9..0000000000 --- a/eclair-core/src/test/scala/fr/acinq/eclair/interop/rustytests/RustyTestsSpec.scala +++ /dev/null @@ -1,116 +0,0 @@ -/* - * Copyright 2019 ACINQ SAS - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package fr.acinq.eclair.interop.rustytests - -import akka.actor.typed.scaladsl.adapter.actorRefAdapter -import akka.actor.{ActorRef, Props} -import akka.testkit.{TestFSMRef, TestKit, TestProbe} -import fr.acinq.bitcoin.scalacompat.{ByteVector32, SatoshiLong} -import fr.acinq.eclair.TestConstants.{Alice, Bob, feeratePerKw} -import fr.acinq.eclair.blockchain.DummyOnChainWallet -import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ -import fr.acinq.eclair.channel._ -import fr.acinq.eclair.channel.fsm.Channel -import fr.acinq.eclair.channel.publish.TxPublisher -import fr.acinq.eclair.channel.states.ChannelStateTestsBase.FakeTxPublisherFactory -import fr.acinq.eclair.payment.receive.{ForwardHandler, PaymentHandler} -import fr.acinq.eclair.wire.protocol.Init -import fr.acinq.eclair.{BlockHeight, MilliSatoshiLong, TestKitBaseClass, TestUtils} -import org.scalatest.funsuite.FixtureAnyFunSuiteLike -import org.scalatest.matchers.should.Matchers -import org.scalatest.{BeforeAndAfterAll, Outcome} - -import java.io.File -import java.util.concurrent.atomic.AtomicLong -import java.util.concurrent.{CountDownLatch, TimeUnit} -import scala.concurrent.duration._ -import scala.io.Source - -/** - * Created by PM on 30/05/2016. - */ - -class RustyTestsSpec extends TestKitBaseClass with Matchers with FixtureAnyFunSuiteLike with BeforeAndAfterAll { - - case class FixtureParam(ref: List[String], res: List[String]) - - override def withFixture(test: OneArgTest): Outcome = { - val blockHeight = new AtomicLong(0) - val latch = new CountDownLatch(1) - val pipe: ActorRef = system.actorOf(Props(new SynchronizationPipe(latch))) - val alicePeer = TestProbe() - val bobPeer = TestProbe() - TestUtils.forwardOutgoingToPipe(alicePeer, pipe) - TestUtils.forwardOutgoingToPipe(bobPeer, pipe) - val alice2blockchain = TestProbe() - val bob2blockchain = TestProbe() - val paymentHandler = system.actorOf(Props(new PaymentHandler(Bob.nodeParams, TestProbe().ref, TestProbe().ref))) - paymentHandler ! new ForwardHandler(TestProbe().ref) - // we just bypass the relayer for this test - val relayer = paymentHandler - val wallet = new DummyOnChainWallet() - val aliceNodeParams = Alice.nodeParams.copy(blockHeight = blockHeight) - val bobNodeParams = Bob.nodeParams.copy(blockHeight = blockHeight) - val channelConfig = ChannelConfig.standard - val channelType = ChannelTypes.Standard() - val alice: TestFSMRef[ChannelState, ChannelData, Channel] = TestFSMRef(new Channel(aliceNodeParams, wallet, Bob.nodeParams.nodeId, alice2blockchain.ref, relayer, FakeTxPublisherFactory(alice2blockchain)), alicePeer.ref) - val bob: TestFSMRef[ChannelState, ChannelData, Channel] = TestFSMRef(new Channel(bobNodeParams, wallet, Alice.nodeParams.nodeId, bob2blockchain.ref, relayer, FakeTxPublisherFactory(bob2blockchain)), bobPeer.ref) - val aliceInit = Init(Alice.channelParams.initFeatures) - val bobInit = Init(Bob.channelParams.initFeatures) - // alice and bob will both have 1 000 000 sat - alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, 2000000 sat, dualFunded = false, commitTxFeerate = feeratePerKw, fundingTxFeerate = feeratePerKw, fundingTxFeeBudget_opt = None, Some(1000000000 msat), requireConfirmedInputs = false, requestFunding_opt = None, Alice.channelParams, pipe, bobInit, ChannelFlags(announceChannel = false), channelConfig, channelType, replyTo = system.deadLetters) - alice2blockchain.expectMsgType[TxPublisher.SetChannelId] - bob ! INPUT_INIT_CHANNEL_NON_INITIATOR(ByteVector32.Zeroes, None, dualFunded = false, None, requireConfirmedInputs = false, Bob.channelParams, pipe, aliceInit, channelConfig, channelType) - bob2blockchain.expectMsgType[TxPublisher.SetChannelId] - pipe ! (alice, bob) - within(30 seconds) { - alice2blockchain.expectMsgType[TxPublisher.SetChannelId] - alice2blockchain.expectMsgType[WatchFundingConfirmed] - bob2blockchain.expectMsgType[TxPublisher.SetChannelId] - bob2blockchain.expectMsgType[WatchFundingConfirmed] - awaitCond(alice.stateName == WAIT_FOR_FUNDING_CONFIRMED) - val fundingTx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].fundingTx_opt.get - alice ! WatchFundingConfirmedTriggered(BlockHeight(400000), 42, fundingTx) - bob ! WatchFundingConfirmedTriggered(BlockHeight(400000), 42, fundingTx) - alice2blockchain.expectMsgType[WatchFundingSpent] - bob2blockchain.expectMsgType[WatchFundingSpent] - awaitCond(alice.stateName == NORMAL) - awaitCond(bob.stateName == NORMAL) - pipe ! new File(getClass.getResource(s"/scenarii/${test.name}.script").getFile) - latch.await(30, TimeUnit.SECONDS) - val ref = Source.fromFile(getClass.getResource(s"/scenarii/${test.name}.script.expected").getFile).getLines().filterNot(_.startsWith("#")).toList - val res = Source.fromFile(new File(TestUtils.BUILD_DIRECTORY, "result.tmp")).getLines().filterNot(_.startsWith("#")).toList - withFixture(test.toNoArgTest(FixtureParam(ref, res))) - } - } - - override def afterAll(): Unit = { - TestKit.shutdownActorSystem(system) - } - - test("01-offer1") { f => assert(f.ref == f.res) } - test("02-offer2") { f => assert(f.ref == f.res) } - test("03-fulfill1") { f => assert(f.ref == f.res) } - // test("04-two-commits-onedir") { f => assert(f.ref == f.res) } DOES NOT PASS : we now automatically sign back when we receive a revocation and have acked changes - // test("05-two-commits-in-flight") { f => assert(f.ref == f.res)} DOES NOT PASS : cannot send two commit in a row (without having first revocation) - test("10-offers-crossover") { f => assert(f.ref == f.res) } - test("11-commits-crossover") { f => assert(f.ref == f.res) } - /*test("13-fee") { f => assert(f.ref == f.res)} - test("14-fee-twice") { f => assert(f.ref == f.res)} - test("15-fee-twice-back-to-back") { f => assert(f.ref == f.res)}*/ - -} \ No newline at end of file diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/interop/rustytests/SynchronizationPipe.scala b/eclair-core/src/test/scala/fr/acinq/eclair/interop/rustytests/SynchronizationPipe.scala deleted file mode 100644 index 0575af9fe1..0000000000 --- a/eclair-core/src/test/scala/fr/acinq/eclair/interop/rustytests/SynchronizationPipe.scala +++ /dev/null @@ -1,163 +0,0 @@ -/* - * Copyright 2019 ACINQ SAS - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package fr.acinq.eclair.interop.rustytests - -import java.io.{BufferedWriter, File, FileWriter} -import java.util.UUID -import java.util.concurrent.CountDownLatch - -import akka.actor.{Actor, ActorLogging, ActorRef, Stash} -import fr.acinq.bitcoin.scalacompat.ByteVector32 -import fr.acinq.eclair.channel._ -import fr.acinq.eclair.transactions.{IncomingHtlc, OutgoingHtlc} -import fr.acinq.eclair.{CltvExpiry, MilliSatoshi, TestConstants, TestUtils} - -/** - * Created by PM on 30/05/2016. - */ - - -/* - Handles a bi-directional path between 2 actors - used to avoid the chicken-and-egg problem of: - a = new Channel(b) - b = new Channel(a) - This pipe executes scripted tests and allows for - fine grained control on the order of messages - */ -class SynchronizationPipe(latch: CountDownLatch) extends Actor with ActorLogging with Stash { - - val offer = "(.):offer ([0-9]+),([0-9a-f]+)".r - val fulfill = "(.):fulfill ([0-9]+),([0-9a-f]+)".r - val commit = "(.):commit".r - val feechange = "(.):feechange".r - val recv = "(.):recv.*".r - val nocommitwait = "(.):nocommitwait.*".r - val echo = "echo (.*)".r - val dump = "(.):dump".r - - val fout = new BufferedWriter(new FileWriter(new File(TestUtils.BUILD_DIRECTORY, "result.tmp"))) - - def exec(script: List[String], a: ActorRef, b: ActorRef): Unit = { - def resolve(x: String) = if (x == "A") a else b - - (script: @unchecked) match { - case offer(x, amount, rhash) :: rest => - resolve(x) ! CMD_ADD_HTLC(self, MilliSatoshi(amount.toInt), ByteVector32.fromValidHex(rhash), CltvExpiry(144), TestConstants.emptyOnionPacket, None, 1.0, None, Origin.Hot(self, Upstream.Local(UUID.randomUUID()))) - exec(rest, a, b) - case fulfill(x, id, r) :: rest => - resolve(x) ! CMD_FULFILL_HTLC(id.toInt, ByteVector32.fromValidHex(r)) - exec(rest, a, b) - case commit(x) :: rest => - resolve(x) ! CMD_SIGN() - exec(rest, a, b) - /*case feechange(x) :: rest => - resolve(x) ! CmdFeeChange() - exec(rest, a, b)*/ - case recv(x) :: rest => - context.become(wait(a, b, script)) - case nocommitwait(x) :: rest => - log.warning("ignoring nocommitwait") - exec(rest, a, b) - case "checksync" :: rest => - log.warning("ignoring checksync") - exec(rest, a, b) - case echo(s) :: rest => - fout.write(s) - fout.newLine() - exec(rest, a, b) - case dump(x) :: rest => - resolve(x) ! CMD_GET_CHANNEL_DATA(ActorRef.noSender) - context.become(wait(a, b, script)) - case "" :: rest => - exec(rest, a, b) - case List() | Nil => - log.info(s"done") - fout.close() - latch.countDown() - } - } - - def receive = { - case (a: ActorRef, b: ActorRef) => - unstashAll() - context become passthrough(a, b) - case msg => stash() - } - - def passthrough(a: ActorRef, b: ActorRef): Receive = { - case file: File => - import scala.io.Source - val script = Source.fromFile(file).getLines().filterNot(_.startsWith("#")).toList - exec(script, a, b) - case _: RES_SUCCESS[_] => {} - case msg if sender() == a => - log.info(s"a -> b $msg") - b forward msg - case msg if sender() == b => - log.info(s"b -> a $msg") - a forward msg - case msg => log.error("" + msg) - } - - def wait(a: ActorRef, b: ActorRef, script: List[String]): Receive = { - case _: RES_SUCCESS[_] => {} - case msg if sender() == a && script.head.startsWith("B:recv") => - log.info(s"a -> b $msg") - b forward msg - unstashAll() - exec(script.drop(1), a, b) - case msg if sender() == b && script.head.startsWith("A:recv") => - log.info(s"b -> a $msg") - a forward msg - unstashAll() - exec(script.drop(1), a, b) - case RES_GET_CHANNEL_DATA(d: DATA_NORMAL) if script.head.endsWith(":dump") => - def rtrim(s: String) = s.replaceAll("\\s+$", "") - import d.commitments.latest._ - val l = List( - "LOCAL COMMITS:", - s" Commit ${localCommit.index}:", - s" Offered htlcs: ${localCommit.spec.htlcs.collect { case OutgoingHtlc(add) => (add.id, add.amountMsat) }.mkString(" ")}", - s" Received htlcs: ${localCommit.spec.htlcs.collect { case IncomingHtlc(add) => (add.id, add.amountMsat) }.mkString(" ")}", - s" Balance us: ${localCommit.spec.toLocal}", - s" Balance them: ${localCommit.spec.toRemote}", - s" Fee rate: ${localCommit.spec.commitTxFeerate.toLong}", - "REMOTE COMMITS:", - s" Commit ${remoteCommit.index}:", - s" Offered htlcs: ${remoteCommit.spec.htlcs.collect { case OutgoingHtlc(add) => (add.id, add.amountMsat) }.mkString(" ")}", - s" Received htlcs: ${remoteCommit.spec.htlcs.collect { case IncomingHtlc(add) => (add.id, add.amountMsat) }.mkString(" ")}", - s" Balance us: ${remoteCommit.spec.toLocal}", - s" Balance them: ${remoteCommit.spec.toRemote}", - s" Fee rate: ${remoteCommit.spec.commitTxFeerate.toLong}") - .foreach(s => { - fout.write(rtrim(s)) - fout.newLine() - }) - unstashAll() - exec(script.drop(1), a, b) - case other => - stash() - } - - - override def preRestart(reason: Throwable, message: Option[Any]): Unit = { - reason.printStackTrace() - super.preRestart(reason, message) - } - -} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/io/OpenChannelInterceptorSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/io/OpenChannelInterceptorSpec.scala index a8abd39b02..6b582ec107 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/io/OpenChannelInterceptorSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/io/OpenChannelInterceptorSpec.scala @@ -30,14 +30,14 @@ import fr.acinq.eclair.channel.ChannelTypes.UnsupportedChannelType import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.channel.states.ChannelStateTestsTags -import fr.acinq.eclair.io.OpenChannelInterceptor.{DefaultParams, OpenChannelInitiator, OpenChannelNonInitiator} +import fr.acinq.eclair.io.OpenChannelInterceptor.{OpenChannelInitiator, OpenChannelNonInitiator} import fr.acinq.eclair.io.Peer.{OpenChannelResponse, OutgoingMessage, SpawnChannelInitiator, SpawnChannelNonInitiator} import fr.acinq.eclair.io.PeerSpec.{createOpenChannelMessage, createOpenDualFundedChannelMessage} import fr.acinq.eclair.io.PendingChannelsRateLimiter.AddOrRejectChannel import fr.acinq.eclair.transactions.Transactions.{ClosingTx, InputInfo} import fr.acinq.eclair.wire.internal.channel.ChannelCodecsSpec -import fr.acinq.eclair.wire.protocol.{ChannelReestablish, ChannelTlv, Error, IPAddress, LiquidityAds, NodeAddress, OpenChannel, OpenChannelTlv, Shutdown, TlvStream} -import fr.acinq.eclair.{AcceptOpenChannel, BlockHeight, CltvExpiryDelta, FeatureSupport, Features, InitFeature, InterceptOpenChannelCommand, InterceptOpenChannelPlugin, InterceptOpenChannelReceived, MilliSatoshiLong, RejectOpenChannel, TestConstants, UnknownFeature, randomBytes32, randomKey} +import fr.acinq.eclair.wire.protocol.{ChannelReestablish, Error, IPAddress, LiquidityAds, NodeAddress, OpenChannel, Shutdown, TlvStream} +import fr.acinq.eclair.{AcceptOpenChannel, BlockHeight, FeatureSupport, Features, InitFeature, InterceptOpenChannelCommand, InterceptOpenChannelPlugin, InterceptOpenChannelReceived, MilliSatoshiLong, RejectOpenChannel, TestConstants, UnknownFeature, randomBytes32, randomKey} import org.scalatest.funsuite.FixtureAnyFunSuiteLike import org.scalatest.{Outcome, Tag} import scodec.bits.ByteVector @@ -47,13 +47,10 @@ import scala.concurrent.duration.DurationInt class OpenChannelInterceptorSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("application")) with FixtureAnyFunSuiteLike { val remoteNodeId: Crypto.PublicKey = randomKey().publicKey - val defaultParams: DefaultParams = DefaultParams(100 sat, 100000 msat, 100 msat, CltvExpiryDelta(288), 10) - val openChannel: OpenChannel = createOpenChannelMessage() + val openChannel: OpenChannel = createOpenChannelMessage(ChannelTypes.AnchorOutputsZeroFeeHtlcTx()) val remoteAddress: NodeAddress = IPAddress(InetAddress.getLoopbackAddress, 19735) - val defaultFeatures: Features[InitFeature] = Features(Map[InitFeature, FeatureSupport](StaticRemoteKey -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional)) - val staticRemoteKeyFeatures: Features[InitFeature] = Features(Map[InitFeature, FeatureSupport](StaticRemoteKey -> Optional)) + val defaultFeatures: Features[InitFeature] = Features(Map[InitFeature, FeatureSupport](StaticRemoteKey -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional, ChannelType -> Optional)) - val acceptStaticRemoteKeyChannelsTag = "accept static_remote_key channels" val noPlugin = "no plugin" override def withFixture(test: OneArgTest): Outcome = { @@ -70,7 +67,6 @@ class OpenChannelInterceptorSpec extends ScalaTestWithActorTestKit(ConfigFactory } val nodeParams = TestConstants.Alice.nodeParams .modify(_.pluginParams).usingIf(!test.tags.contains(noPlugin))(_ :+ plugin) - .modify(_.channelConf).usingIf(test.tags.contains(acceptStaticRemoteKeyChannelsTag))(_.copy(acceptIncomingStaticRemoteKeyChannels = true)) val eventListener = TestProbe[ChannelAborted]() system.eventStream ! EventStream.Subscribe(eventListener.ref) @@ -82,8 +78,9 @@ class OpenChannelInterceptorSpec extends ScalaTestWithActorTestKit(ConfigFactory case class FixtureParam(openChannelInterceptor: ActorRef[OpenChannelInterceptor.Command], peer: TestProbe[Any], pluginInterceptor: TestProbe[InterceptOpenChannelCommand], pendingChannelsRateLimiter: TestProbe[PendingChannelsRateLimiter.Command], peerConnection: TestProbe[Any], eventListener: TestProbe[ChannelAborted], wallet: DummyOnChainWallet) private def commitments(isOpener: Boolean = false): Commitments = { - val commitments = CommitmentsSpec.makeCommitments(500_000 msat, 400_000 msat, TestConstants.Alice.nodeParams.nodeId, remoteNodeId, announcement_opt = None) - commitments.copy(params = commitments.params.copy(localParams = commitments.params.localParams.copy(isChannelOpener = isOpener, paysCommitTxFees = isOpener))) + CommitmentsSpec.makeCommitments(500_000 msat, 400_000 msat, TestConstants.Alice.nodeParams.nodeId, remoteNodeId, announcement_opt = None) + .modify(_.channelParams.localParams.isChannelOpener).setTo(isOpener) + .modify(_.channelParams.localParams.paysCommitTxFees).setTo(isOpener) } test("reject channel open if timeout waiting for plugin to respond") { f => @@ -103,13 +100,8 @@ class OpenChannelInterceptorSpec extends ScalaTestWithActorTestKit(ConfigFactory val openChannelNonInitiator = OpenChannelNonInitiator(remoteNodeId, Left(openChannel), defaultFeatures, defaultFeatures, peerConnection.ref, remoteAddress) openChannelInterceptor ! openChannelNonInitiator pendingChannelsRateLimiter.expectMessageType[AddOrRejectChannel].replyTo ! PendingChannelsRateLimiter.AcceptOpenChannel - pluginInterceptor.expectMessageType[InterceptOpenChannelReceived].replyTo ! AcceptOpenChannel(randomBytes32(), defaultParams, addFunding_opt = None) - val updatedLocalParams = peer.expectMessageType[SpawnChannelNonInitiator].localParams - assert(updatedLocalParams.dustLimit == defaultParams.dustLimit) - assert(updatedLocalParams.htlcMinimum == defaultParams.htlcMinimum) - assert(updatedLocalParams.maxAcceptedHtlcs == defaultParams.maxAcceptedHtlcs) - assert(updatedLocalParams.maxHtlcValueInFlightMsat == defaultParams.maxHtlcValueInFlightMsat) - assert(updatedLocalParams.toSelfDelay == defaultParams.toSelfDelay) + pluginInterceptor.expectMessageType[InterceptOpenChannelReceived].replyTo ! AcceptOpenChannel(randomBytes32(), addFunding_opt = None) + assert(peer.expectMessageType[SpawnChannelNonInitiator].addFunding_opt.isEmpty) } test("add liquidity if interceptor plugin requests it") { f => @@ -119,7 +111,7 @@ class OpenChannelInterceptorSpec extends ScalaTestWithActorTestKit(ConfigFactory openChannelInterceptor ! openChannelNonInitiator pendingChannelsRateLimiter.expectMessageType[AddOrRejectChannel].replyTo ! PendingChannelsRateLimiter.AcceptOpenChannel val addFunding = LiquidityAds.AddFunding(100_000 sat, None) - pluginInterceptor.expectMessageType[InterceptOpenChannelReceived].replyTo ! AcceptOpenChannel(randomBytes32(), defaultParams, Some(addFunding)) + pluginInterceptor.expectMessageType[InterceptOpenChannelReceived].replyTo ! AcceptOpenChannel(randomBytes32(), Some(addFunding)) assert(peer.expectMessageType[SpawnChannelNonInitiator].addFunding_opt.contains(addFunding)) } @@ -128,9 +120,8 @@ class OpenChannelInterceptorSpec extends ScalaTestWithActorTestKit(ConfigFactory val features = defaultFeatures.add(Features.SplicePrototype, FeatureSupport.Optional).add(Features.OnTheFlyFunding, FeatureSupport.Optional) val requestFunding = LiquidityAds.RequestFunding(250_000 sat, TestConstants.defaultLiquidityRates.fundingRates.head, LiquidityAds.PaymentDetails.FromChannelBalanceForFutureHtlc(randomBytes32() :: Nil)) - val open = createOpenDualFundedChannelMessage().copy( + val open = createOpenDualFundedChannelMessage(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(), Some(requestFunding)).copy( channelFlags = ChannelFlags(nonInitiatorPaysCommitFees = true, announceChannel = false), - tlvStream = TlvStream(ChannelTlv.RequestFundingTlv(requestFunding)) ) val openChannelNonInitiator = OpenChannelNonInitiator(remoteNodeId, Right(open), features, features, peerConnection.ref, remoteAddress) openChannelInterceptor ! openChannelNonInitiator @@ -139,14 +130,13 @@ class OpenChannelInterceptorSpec extends ScalaTestWithActorTestKit(ConfigFactory val currentChannels = Seq( Peer.ChannelInfo(TestProbe().ref, SHUTDOWN, DATA_SHUTDOWN(commitments(isOpener = true), Shutdown(randomBytes32(), ByteVector.empty), Shutdown(randomBytes32(), ByteVector.empty), CloseStatus.Initiator(None))), Peer.ChannelInfo(TestProbe().ref, NEGOTIATING, DATA_NEGOTIATING(commitments(), Shutdown(randomBytes32(), ByteVector.empty), Shutdown(randomBytes32(), ByteVector.empty), List(Nil), None)), - Peer.ChannelInfo(TestProbe().ref, CLOSING, DATA_CLOSING(commitments(), BlockHeight(0), ByteVector.empty, Nil, ClosingTx(InputInfo(OutPoint(TxId(randomBytes32()), 5), TxOut(100_000 sat, Nil), Nil), Transaction(2, Nil, Nil, 0), None) :: Nil)), + Peer.ChannelInfo(TestProbe().ref, CLOSING, DATA_CLOSING(commitments(), BlockHeight(0), ByteVector.empty, Nil, ClosingTx(InputInfo(OutPoint(TxId(randomBytes32()), 5), TxOut(100_000 sat, Nil)), Transaction(2, Nil, Nil, 0), None) :: Nil)), Peer.ChannelInfo(TestProbe().ref, WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT, DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT(commitments(), ChannelReestablish(randomBytes32(), 0, 0, randomKey(), randomKey().publicKey))), ) peer.expectMessageType[Peer.GetPeerChannels].replyTo ! Peer.PeerChannels(remoteNodeId, currentChannels) val result = peer.expectMessageType[SpawnChannelNonInitiator] assert(!result.localParams.isChannelOpener) assert(result.localParams.paysCommitTxFees) - assert(result.localParams.maxHtlcValueInFlightMsat == 500_000_000.msat) assert(result.addFunding_opt.map(_.fundingAmount).contains(250_000 sat)) assert(result.addFunding_opt.flatMap(_.rates_opt).contains(TestConstants.defaultLiquidityRates)) } @@ -156,11 +146,10 @@ class OpenChannelInterceptorSpec extends ScalaTestWithActorTestKit(ConfigFactory val probe = TestProbe[Any]() val requestFunding = LiquidityAds.RequestFunding(150_000 sat, LiquidityAds.FundingRate(0 sat, 200_000 sat, 400, 100, 0 sat, 0 sat), LiquidityAds.PaymentDetails.FromChannelBalance) - val openChannelInitiator = OpenChannelInitiator(probe.ref, remoteNodeId, Peer.OpenChannel(remoteNodeId, 300_000 sat, None, None, None, None, Some(requestFunding), None, None), defaultFeatures, defaultFeatures) + val openChannelInitiator = OpenChannelInitiator(probe.ref, remoteNodeId, Peer.OpenChannel(remoteNodeId, 300_000 sat, Some(ChannelTypes.AnchorOutputsZeroFeeHtlcTx()), None, None, None, Some(requestFunding), None, None), defaultFeatures, defaultFeatures) openChannelInterceptor ! openChannelInitiator val result = peer.expectMessageType[SpawnChannelInitiator] assert(result.cmd == openChannelInitiator.open) - assert(result.localParams.maxHtlcValueInFlightMsat == 450_000_000.msat) } test("continue channel open if no interceptor plugin registered and pending channels rate limiter accepts it") { f => @@ -176,15 +165,6 @@ class OpenChannelInterceptorSpec extends ScalaTestWithActorTestKit(ConfigFactory assert(peer.expectMessageType[SpawnChannelNonInitiator].addFunding_opt.isEmpty) } - test("reject open channel request if channel type is obsolete") { f => - import f._ - - val openChannelNonInitiator = OpenChannelNonInitiator(remoteNodeId, Left(openChannel), Features.empty, Features.empty, peerConnection.ref, remoteAddress) - openChannelInterceptor ! openChannelNonInitiator - assert(peer.expectMessageType[OutgoingMessage].msg.asInstanceOf[Error].toAscii.contains("rejecting incoming channel: anchor outputs must be used for new channels")) - eventListener.expectMessageType[ChannelAborted] - } - test("reject open channel request if rejected by the plugin") { f => import f._ @@ -222,38 +202,18 @@ class OpenChannelInterceptorSpec extends ScalaTestWithActorTestKit(ConfigFactory assert(peer.expectMessageType[OutgoingMessage].msg.asInstanceOf[Error].channelId == ByteVector32.One) // original request accepted after plugin accepts it - pluginInterceptor.expectMessageType[InterceptOpenChannelReceived].replyTo ! AcceptOpenChannel(randomBytes32(), defaultParams, None) + pluginInterceptor.expectMessageType[InterceptOpenChannelReceived].replyTo ! AcceptOpenChannel(randomBytes32(), None) assert(peer.expectMessageType[SpawnChannelNonInitiator].open == Left(openChannel)) eventListener.expectMessageType[ChannelAborted] } - test("reject static_remote_key open channel request") { f => - import f._ - - val openChannelNonInitiator = OpenChannelNonInitiator(remoteNodeId, Left(openChannel), staticRemoteKeyFeatures, staticRemoteKeyFeatures, peerConnection.ref, remoteAddress) - openChannelInterceptor ! openChannelNonInitiator - assert(peer.expectMessageType[OutgoingMessage].msg.asInstanceOf[Error].toAscii.contains("rejecting incoming static_remote_key channel: anchor outputs must be used for new channels")) - eventListener.expectMessageType[ChannelAborted] - } - - test("accept static_remote_key open channel request if node is configured to accept them", Tag(acceptStaticRemoteKeyChannelsTag)) { f => - import f._ - - val openChannelNonInitiator = OpenChannelNonInitiator(remoteNodeId, Left(openChannel), staticRemoteKeyFeatures, staticRemoteKeyFeatures, peerConnection.ref, remoteAddress) - openChannelInterceptor ! openChannelNonInitiator - pendingChannelsRateLimiter.expectMessageType[AddOrRejectChannel].replyTo ! PendingChannelsRateLimiter.AcceptOpenChannel - pluginInterceptor.expectMessageType[InterceptOpenChannelReceived].replyTo ! AcceptOpenChannel(randomBytes32(), defaultParams, Some(LiquidityAds.AddFunding(50_000 sat, None))) - peer.expectMessageType[SpawnChannelNonInitiator] - } - test("reject on-the-fly channel if another channel exists", Tag(noPlugin)) { f => import f._ val features = defaultFeatures.add(Features.SplicePrototype, FeatureSupport.Optional).add(Features.OnTheFlyFunding, FeatureSupport.Optional) val requestFunding = LiquidityAds.RequestFunding(250_000 sat, TestConstants.defaultLiquidityRates.fundingRates.head, LiquidityAds.PaymentDetails.FromChannelBalanceForFutureHtlc(randomBytes32() :: Nil)) - val open = createOpenDualFundedChannelMessage().copy( + val open = createOpenDualFundedChannelMessage(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(), Some(requestFunding)).copy( channelFlags = ChannelFlags(nonInitiatorPaysCommitFees = true, announceChannel = false), - tlvStream = TlvStream(ChannelTlv.RequestFundingTlv(requestFunding)) ) val currentChannel = Seq( Peer.ChannelInfo(TestProbe().ref, NORMAL, ChannelCodecsSpec.normal), @@ -290,41 +250,34 @@ class OpenChannelInterceptorSpec extends ScalaTestWithActorTestKit(ConfigFactory test("don't spawn a channel if we don't support their channel type") { f => import f._ - // They only support anchor outputs and we don't. - { - val open = createOpenChannelMessage(TlvStream[OpenChannelTlv](ChannelTlv.ChannelTypeTlv(ChannelTypes.AnchorOutputs()))) - openChannelInterceptor ! OpenChannelNonInitiator(remoteNodeId, Left(open), Features.empty, Features.empty, peerConnection.ref, remoteAddress) - peer.expectMessage(OutgoingMessage(Error(open.temporaryChannelId, "invalid channel_type=anchor_outputs, expected channel_type=standard"), peerConnection.ref.toClassic)) - eventListener.expectMessageType[ChannelAborted] - } - // They only support anchor outputs with zero fee htlc txs and we don't. + // We don't support non-anchor static_remotekey channels. { - val open = createOpenChannelMessage(TlvStream[OpenChannelTlv](ChannelTlv.ChannelTypeTlv(ChannelTypes.AnchorOutputsZeroFeeHtlcTx()))) - openChannelInterceptor ! OpenChannelNonInitiator(remoteNodeId, Left(open), Features.empty, Features.empty, peerConnection.ref, remoteAddress) - peer.expectMessage(OutgoingMessage(Error(open.temporaryChannelId, "invalid channel_type=anchor_outputs_zero_fee_htlc_tx, expected channel_type=standard"), peerConnection.ref.toClassic)) + val open = createOpenChannelMessage(ChannelTypes.UnsupportedChannelType(Features(StaticRemoteKey -> Mandatory))) + openChannelInterceptor ! OpenChannelNonInitiator(remoteNodeId, Left(open), defaultFeatures, defaultFeatures, peerConnection.ref, remoteAddress) + peer.expectMessage(OutgoingMessage(Error(open.temporaryChannelId, "invalid channel_type=0x1000"), peerConnection.ref.toClassic)) eventListener.expectMessageType[ChannelAborted] } - // They want to use a channel type that doesn't exist in the spec. + // They only support unsafe anchor outputs and we don't. { - val open = createOpenChannelMessage(TlvStream[OpenChannelTlv](ChannelTlv.ChannelTypeTlv(UnsupportedChannelType(Features(AnchorOutputs -> Optional))))) - openChannelInterceptor ! OpenChannelNonInitiator(remoteNodeId, Left(open), Features.empty, Features.empty, peerConnection.ref, remoteAddress) - peer.expectMessage(OutgoingMessage(Error(open.temporaryChannelId, "invalid channel_type=0x200000, expected channel_type=standard"), peerConnection.ref.toClassic)) + val open = createOpenChannelMessage(ChannelTypes.AnchorOutputs()) + openChannelInterceptor ! OpenChannelNonInitiator(remoteNodeId, Left(open), defaultFeatures, defaultFeatures.add(AnchorOutputs, Optional), peerConnection.ref, remoteAddress) + peer.expectMessage(OutgoingMessage(Error(open.temporaryChannelId, "invalid channel_type=anchor_outputs"), peerConnection.ref.toClassic)) eventListener.expectMessageType[ChannelAborted] } // They want to use a channel type we don't support yet. { - val open = createOpenChannelMessage(TlvStream[OpenChannelTlv](ChannelTlv.ChannelTypeTlv(UnsupportedChannelType(Features(Map(StaticRemoteKey -> Mandatory), unknown = Set(UnknownFeature(22))))))) - openChannelInterceptor ! OpenChannelNonInitiator(remoteNodeId, Left(open), Features.empty, Features.empty, peerConnection.ref, remoteAddress) - peer.expectMessage(OutgoingMessage(Error(open.temporaryChannelId, "invalid channel_type=0x401000, expected channel_type=standard"), peerConnection.ref.toClassic)) + val open = createOpenChannelMessage(UnsupportedChannelType(Features(activated = Map.empty, unknown = Set(UnknownFeature(120))))) + openChannelInterceptor ! OpenChannelNonInitiator(remoteNodeId, Left(open), defaultFeatures, defaultFeatures, peerConnection.ref, remoteAddress) + peer.expectMessage(OutgoingMessage(Error(open.temporaryChannelId, "invalid channel_type=0x01000000000000000000000000000000"), peerConnection.ref.toClassic)) eventListener.expectMessageType[ChannelAborted] } } - test("don't spawn a channel if channel type is missing with the feature bit set") { f => + test("don't spawn a channel if channel type is missing") { f => import f._ - val open = createOpenChannelMessage() - openChannelInterceptor ! OpenChannelNonInitiator(remoteNodeId, Left(open), defaultFeatures.add(ChannelType, Optional), defaultFeatures.add(ChannelType, Optional), peerConnection.ref, remoteAddress) + val open = createOpenChannelMessage(ChannelTypes.AnchorOutputsZeroFeeHtlcTx()).copy(tlvStream = TlvStream.empty) + openChannelInterceptor ! OpenChannelNonInitiator(remoteNodeId, Left(open), defaultFeatures, defaultFeatures, peerConnection.ref, remoteAddress) peer.expectMessage(OutgoingMessage(Error(open.temporaryChannelId, "option_channel_type was negotiated but channel_type is missing"), peerConnection.ref.toClassic)) eventListener.expectMessageType[ChannelAborted] } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerConnectionSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerConnectionSpec.scala index 4b9b1a152a..33801cdb8a 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerConnectionSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerConnectionSpec.scala @@ -21,8 +21,10 @@ import akka.testkit.{TestFSMRef, TestProbe} import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32} import fr.acinq.eclair.FeatureSupport.{Mandatory, Optional} -import fr.acinq.eclair.Features.{BasicMultiPartPayment, ChannelRangeQueries, ChannelType, PaymentSecret, StaticRemoteKey, VariableLengthOnion} +import fr.acinq.eclair.Features._ import fr.acinq.eclair.TestConstants._ +import fr.acinq.eclair.TestUtils.randomTxId +import fr.acinq.eclair.channel.ChannelSpendSignature.IndividualSignature import fr.acinq.eclair.crypto.TransportHandler import fr.acinq.eclair.io.Peer.ConnectionDown import fr.acinq.eclair.message.OnionMessages.{Recipient, buildMessage} @@ -333,6 +335,100 @@ class PeerConnectionSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wi transport.expectNoMessage(10 / transport.testKitSettings.TestTimeFactor seconds) // we don't want dilated time here } + test("send batch of commit_sig messages") { f => + import f._ + val probe = TestProbe() + connect(nodeParams, remoteNodeId, switchboard, router, connection, transport, peerConnection, peer) + val channelId = randomBytes32() + val commitSigs = Seq( + CommitSig(channelId, IndividualSignature(randomBytes64()), Nil), + CommitSig(channelId, IndividualSignature(randomBytes64()), Nil), + CommitSig(channelId, IndividualSignature(randomBytes64()), Nil), + ) + probe.send(peerConnection, CommitSigBatch(commitSigs)) + commitSigs.foreach(commitSig => transport.expectMsg(commitSig)) + transport.expectNoMessage(100 millis) + } + + test("receive legacy batch of commit_sig messages") { f => + import f._ + connect(nodeParams, remoteNodeId, switchboard, router, connection, transport, peerConnection, peer) + + // We receive a batch of commit_sig messages from a first channel. + val channelId1 = randomBytes32() + val commitSigs1 = Seq( + CommitSig(channelId1, IndividualSignature(randomBytes64()), Nil, batchSize = 2), + CommitSig(channelId1, IndividualSignature(randomBytes64()), Nil, batchSize = 2), + ) + transport.send(peerConnection, commitSigs1.head) + transport.expectMsg(TransportHandler.ReadAck(commitSigs1.head)) + peer.expectNoMessage(100 millis) + transport.send(peerConnection, commitSigs1.last) + transport.expectMsg(TransportHandler.ReadAck(commitSigs1.last)) + peer.expectMsg(CommitSigBatch(commitSigs1)) + + // We receive a batch of commit_sig messages from a second channel. + val channelId2 = randomBytes32() + val commitSigs2 = Seq( + CommitSig(channelId2, IndividualSignature(randomBytes64()), Nil, batchSize = 3), + CommitSig(channelId2, IndividualSignature(randomBytes64()), Nil, batchSize = 3), + CommitSig(channelId2, IndividualSignature(randomBytes64()), Nil, batchSize = 3), + ) + commitSigs2.dropRight(1).foreach(commitSig => { + transport.send(peerConnection, commitSig) + transport.expectMsg(TransportHandler.ReadAck(commitSig)) + }) + peer.expectNoMessage(100 millis) + transport.send(peerConnection, commitSigs2.last) + transport.expectMsg(TransportHandler.ReadAck(commitSigs2.last)) + peer.expectMsg(CommitSigBatch(commitSigs2)) + + // We receive another batch of commit_sig messages from the first channel, with unrelated messages in the batch. + val commitSigs3 = Seq( + CommitSig(channelId1, IndividualSignature(randomBytes64()), Nil, batchSize = 2), + CommitSig(channelId1, IndividualSignature(randomBytes64()), Nil, batchSize = 2), + ) + transport.send(peerConnection, commitSigs3.head) + transport.expectMsg(TransportHandler.ReadAck(commitSigs3.head)) + val spliceLocked1 = SpliceLocked(channelId1, randomTxId()) + transport.send(peerConnection, spliceLocked1) + transport.expectMsg(TransportHandler.ReadAck(spliceLocked1)) + peer.expectMsg(spliceLocked1) + val spliceLocked2 = SpliceLocked(channelId2, randomTxId()) + transport.send(peerConnection, spliceLocked2) + transport.expectMsg(TransportHandler.ReadAck(spliceLocked2)) + peer.expectMsg(spliceLocked2) + peer.expectNoMessage(100 millis) + transport.send(peerConnection, commitSigs3.last) + transport.expectMsg(TransportHandler.ReadAck(commitSigs3.last)) + peer.expectMsg(CommitSigBatch(commitSigs3)) + + // We start receiving a batch of commit_sig messages from the first channel, interleaved with a batch from the second + // channel, which is not supported. + val commitSigs4 = Seq( + CommitSig(channelId1, IndividualSignature(randomBytes64()), Nil, batchSize = 2), + CommitSig(channelId2, IndividualSignature(randomBytes64()), Nil, batchSize = 2), + CommitSig(channelId2, IndividualSignature(randomBytes64()), Nil, batchSize = 2), + ) + transport.send(peerConnection, commitSigs4.head) + transport.expectMsg(TransportHandler.ReadAck(commitSigs4.head)) + peer.expectNoMessage(100 millis) + transport.send(peerConnection, commitSigs4(1)) + transport.expectMsg(TransportHandler.ReadAck(commitSigs4(1))) + peer.expectMsg(CommitSigBatch(commitSigs4.take(1))) + transport.send(peerConnection, commitSigs4.last) + transport.expectMsg(TransportHandler.ReadAck(commitSigs4.last)) + peer.expectMsg(CommitSigBatch(commitSigs4.tail)) + + // We receive a batch that exceeds our threshold: we process them individually. + val invalidCommitSigs = (0 until 30).map(_ => CommitSig(channelId2, IndividualSignature(randomBytes64()), Nil, batchSize = 30)) + invalidCommitSigs.foreach(commitSig => { + transport.send(peerConnection, commitSig) + transport.expectMsg(TransportHandler.ReadAck(commitSig)) + peer.expectMsg(commitSig) + }) + } + test("react to peer's bad behavior") { f => import f._ val probe = TestProbe() diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala index 45cca03df0..b1b794ba60 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala @@ -30,14 +30,13 @@ import fr.acinq.eclair.blockchain.{CurrentFeerates, DummyOnChainWallet} import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.channel.states.ChannelStateTestsTags +import fr.acinq.eclair.crypto.keymanager.ChannelKeys import fr.acinq.eclair.io.Peer._ import fr.acinq.eclair.message.OnionMessages.{Recipient, buildMessage} import fr.acinq.eclair.testutils.FixtureSpec import fr.acinq.eclair.wire.internal.channel.ChannelCodecsSpec -import fr.acinq.eclair.wire.internal.channel.ChannelCodecsSpec.localParams import fr.acinq.eclair.wire.protocol import fr.acinq.eclair.wire.protocol._ -import org.scalatest.Inside.inside import org.scalatest.{Tag, TestData} import scodec.bits.{ByteVector, HexStringSyntax} @@ -70,9 +69,7 @@ class PeerSpec extends FixtureSpec { import com.softwaremill.quicklens._ val aliceParams = TestConstants.Alice.nodeParams - .modify(_.features).setToIf(testData.tags.contains(ChannelStateTestsTags.StaticRemoteKey))(Features(StaticRemoteKey -> Optional)) - .modify(_.features).setToIf(testData.tags.contains(ChannelStateTestsTags.AnchorOutputs))(Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional)) - .modify(_.features).setToIf(testData.tags.contains(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs))(Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional)) + .modify(_.features).setToIf(testData.tags.contains(ChannelStateTestsTags.AnchorOutputsPhoenix))(Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional)) .modify(_.features).setToIf(testData.tags.contains(ChannelStateTestsTags.DualFunding))(Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional, DualFunding -> Optional)) .modify(_.channelConf.maxHtlcValueInFlightMsat).setToIf(testData.tags.contains("max-htlc-value-in-flight-percent"))(100_000_000 msat) .modify(_.channelConf.maxHtlcValueInFlightPercent).setToIf(testData.tags.contains("max-htlc-value-in-flight-percent"))(25) @@ -85,7 +82,7 @@ class PeerSpec extends FixtureSpec { } case class FakeChannelFactory(channel: TestProbe) extends ChannelFactory { - override def spawn(context: ActorContext, remoteNodeId: PublicKey): ActorRef = { + override def spawn(context: ActorContext, remoteNodeId: PublicKey, channelKeys: ChannelKeys): ActorRef = { assert(remoteNodeId == Bob.nodeParams.nodeId) channel.ref } @@ -339,29 +336,26 @@ class PeerSpec extends FixtureSpec { connect(remoteNodeId, peer, peerConnection, switchboard, channels = Set(ChannelCodecsSpec.normal)) // We regularly update our internal feerates. - val bitcoinCoreFeerates = FeeratesPerKw(FeeratePerKw(253 sat), FeeratePerKw(1000 sat), FeeratePerKw(2500 sat), FeeratePerKw(5000 sat), FeeratePerKw(10_000 sat)) + val bitcoinCoreFeerates = FeeratesPerKw(FeeratePerKw(253 sat), FeeratePerKw(400 sat), FeeratePerKw(500 sat), FeeratePerKw(1000 sat), FeeratePerKw(1500 sat)) nodeParams.setBitcoinCoreFeerates(bitcoinCoreFeerates) peer ! CurrentFeerates.BitcoinCore(bitcoinCoreFeerates) peerConnection.expectMsg(RecommendedFeerates( chainHash = Block.RegtestGenesisBlock.hash, - fundingFeerate = FeeratePerKw(2_500 sat), - commitmentFeerate = FeeratePerKw(5000 sat), + fundingFeerate = FeeratePerKw(500 sat), + commitmentFeerate = FeeratePerKw(1000 sat), tlvStream = TlvStream[RecommendedFeeratesTlv]( - RecommendedFeeratesTlv.FundingFeerateRange(FeeratePerKw(1250 sat), FeeratePerKw(20_000 sat)), - RecommendedFeeratesTlv.CommitmentFeerateRange(FeeratePerKw(2500 sat), FeeratePerKw(40_000 sat)) + RecommendedFeeratesTlv.FundingFeerateRange(FeeratePerKw(253 sat), FeeratePerKw(4_000 sat)), + RecommendedFeeratesTlv.CommitmentFeerateRange(FeeratePerKw(500 sat), FeeratePerKw(8_000 sat)) ) )) } - test("reject funding requests if funding feerate is too low for on-the-fly funding", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + test("reject funding requests if funding feerate is too low for on-the-fly funding", Tag(ChannelStateTestsTags.DualFunding)) { f => import f._ connect(remoteNodeId, peer, peerConnection, switchboard, remoteInit = protocol.Init(Features(StaticRemoteKey -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional, DualFunding -> Optional))) val requestFunds = LiquidityAds.RequestFunding(50_000 sat, LiquidityAds.FundingRate(10_000 sat, 100_000 sat, 0, 0, 0 sat, 0 sat), LiquidityAds.PaymentDetails.FromFutureHtlc(randomBytes32() :: Nil)) - val open = { - val open = createOpenDualFundedChannelMessage() - open.copy(fundingFeerate = FeeratePerKw(5000 sat), tlvStream = TlvStream(ChannelTlv.RequestFundingTlv(requestFunds))) - } + val open = createOpenDualFundedChannelMessage(ChannelTypes.AnchorOutputs(), Some(requestFunds)).copy(fundingFeerate = FeeratePerKw(5000 sat)) // Our current and previous feerates are higher than what will be proposed. Seq(FeeratePerKw(7500 sat), FeeratePerKw(6000 sat)).foreach(feerate => { @@ -406,7 +400,7 @@ class PeerSpec extends FixtureSpec { channel.expectMsg(splice) } - test("don't spawn a channel with duplicate temporary channel id", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + test("don't spawn a channel with duplicate temporary channel id") { f => import f._ val probe = TestProbe() @@ -414,7 +408,7 @@ class PeerSpec extends FixtureSpec { connect(remoteNodeId, peer, peerConnection, switchboard) assert(peer.stateData.channels.isEmpty) - val open = createOpenChannelMessage() + val open = createOpenChannelMessage(ChannelTypes.AnchorOutputsZeroFeeHtlcTx()) peerConnection.send(peer, open) eventually { assert(peer.stateData.channels.nonEmpty) @@ -445,7 +439,7 @@ class PeerSpec extends FixtureSpec { assert(peer.stateData.channels.isEmpty) val requestFunds = LiquidityAds.RequestFunding(50_000 sat, LiquidityAds.FundingRate(10_000 sat, 100_000 sat, 0, 0, 0 sat, 0 sat), LiquidityAds.PaymentDetails.FromChannelBalance) - val open = Peer.OpenChannel(remoteNodeId, 10000 sat, None, None, None, None, Some(requestFunds), None, None) + val open = Peer.OpenChannel(remoteNodeId, 10000 sat, Some(ChannelTypes.AnchorOutputs()), None, None, None, Some(requestFunds), None, None) peerConnection.send(peer, open) assert(channel.expectMsgType[INPUT_INIT_CHANNEL_INITIATOR].requestFunding_opt.contains(requestFunds)) } @@ -454,7 +448,7 @@ class PeerSpec extends FixtureSpec { import f._ connect(remoteNodeId, peer, peerConnection, switchboard) - val open = createOpenDualFundedChannelMessage() + val open = createOpenDualFundedChannelMessage(ChannelTypes.AnchorOutputsZeroFeeHtlcTx()) peerConnection.send(peer, open) peerConnection.expectMsg(Error(open.temporaryChannelId, "dual funding is not supported")) } @@ -466,7 +460,7 @@ class PeerSpec extends FixtureSpec { // Both peers support option_dual_fund, so it is automatically used. connect(remoteNodeId, peer, peerConnection, switchboard, remoteInit = protocol.Init(Features(StaticRemoteKey -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional, DualFunding -> Optional))) assert(peer.stateData.channels.isEmpty) - probe.send(peer, Peer.OpenChannel(remoteNodeId, 25000 sat, None, None, None, None, None, None, None)) + probe.send(peer, Peer.OpenChannel(remoteNodeId, 25000 sat, Some(ChannelTypes.AnchorOutputsZeroFeeHtlcTx()), None, None, None, None, None, None)) assert(channel.expectMsgType[INPUT_INIT_CHANNEL_INITIATOR].dualFunded) } @@ -476,7 +470,7 @@ class PeerSpec extends FixtureSpec { // Both peers support option_dual_fund, so it is automatically used. connect(remoteNodeId, peer, peerConnection, switchboard, remoteInit = protocol.Init(Features(StaticRemoteKey -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional, DualFunding -> Optional))) assert(peer.stateData.channels.isEmpty) - val open = createOpenDualFundedChannelMessage() + val open = createOpenDualFundedChannelMessage(ChannelTypes.AnchorOutputsZeroFeeHtlcTx()) peerConnection.send(peer, open) eventually { assert(peer.stateData.channels.nonEmpty) @@ -485,13 +479,13 @@ class PeerSpec extends FixtureSpec { channel.expectMsg(open) } - test("use their channel type when spawning a channel", Tag(ChannelStateTestsTags.AnchorOutputs)) { f => + test("use their channel type when spawning a channel", Tag(ChannelStateTestsTags.AnchorOutputsPhoenix)) { f => import f._ - // We both support option_anchors_zero_fee_htlc_tx they want to open an anchor_outputs channel. - connect(remoteNodeId, peer, peerConnection, switchboard, remoteInit = protocol.Init(Features(AnchorOutputsZeroFeeHtlcTx -> Optional))) + // We both support option_anchors_zero_fee_htlc_tx, but they want to open an anchor_outputs channel. + connect(remoteNodeId, peer, peerConnection, switchboard, remoteInit = protocol.Init(Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional))) assert(peer.stateData.channels.isEmpty) - val open = createOpenChannelMessage(TlvStream[OpenChannelTlv](ChannelTlv.ChannelTypeTlv(ChannelTypes.AnchorOutputs()))) + val open = createOpenChannelMessage(ChannelTypes.AnchorOutputs()) peerConnection.send(peer, open) eventually { assert(peer.stateData.channels.nonEmpty) @@ -502,48 +496,44 @@ class PeerSpec extends FixtureSpec { channel.expectMsg(open) } - test("use requested channel type when spawning a channel", Tag(ChannelStateTestsTags.StaticRemoteKey)) { f => + test("use requested channel type when spawning a channel", Tag(ChannelStateTestsTags.AnchorOutputsPhoenix)) { f => import f._ val probe = TestProbe() - connect(remoteNodeId, peer, peerConnection, switchboard, remoteInit = protocol.Init(Features(StaticRemoteKey -> Mandatory))) + connect(remoteNodeId, peer, peerConnection, switchboard, remoteInit = protocol.Init(Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional))) assert(peer.stateData.channels.isEmpty) - probe.send(peer, Peer.OpenChannel(remoteNodeId, 15000 sat, None, None, None, None, None, None, None)) - assert(channel.expectMsgType[INPUT_INIT_CHANNEL_INITIATOR].channelType == ChannelTypes.StaticRemoteKey()) - - // We can create channels that don't use the features we have enabled. - probe.send(peer, Peer.OpenChannel(remoteNodeId, 15000 sat, Some(ChannelTypes.Standard()), None, None, None, None, None, None)) - assert(channel.expectMsgType[INPUT_INIT_CHANNEL_INITIATOR].channelType == ChannelTypes.Standard()) + probe.send(peer, Peer.OpenChannel(remoteNodeId, 15000 sat, Some(ChannelTypes.AnchorOutputsZeroFeeHtlcTx()), None, None, None, None, None, None)) + assert(channel.expectMsgType[INPUT_INIT_CHANNEL_INITIATOR].channelType == ChannelTypes.AnchorOutputsZeroFeeHtlcTx()) - // We can create channels that use features that we haven't enabled. + // We can create channels that use a different channel type. probe.send(peer, Peer.OpenChannel(remoteNodeId, 15000 sat, Some(ChannelTypes.AnchorOutputs()), None, None, None, None, None, None)) assert(channel.expectMsgType[INPUT_INIT_CHANNEL_INITIATOR].channelType == ChannelTypes.AnchorOutputs()) } - test("handle OpenChannelInterceptor accepting an open channel message", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + test("handle OpenChannelInterceptor accepting an open channel message") { f => import f._ connect(remoteNodeId, peer, peerConnection, switchboard) - val open = createOpenChannelMessage() + val open = createOpenChannelMessage(ChannelTypes.AnchorOutputsZeroFeeHtlcTx()) peerConnection.send(peer, open) assert(channel.expectMsgType[INPUT_INIT_CHANNEL_NON_INITIATOR].temporaryChannelId == open.temporaryChannelId) channel.expectMsg(open) } - test("handle OpenChannelInterceptor rejecting an open channel message", Tag("rate_limited"), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + test("handle OpenChannelInterceptor rejecting an open channel message", Tag("rate_limited")) { f => import f._ connect(remoteNodeId, peer, peerConnection, switchboard) - val open = createOpenChannelMessage() + val open = createOpenChannelMessage(ChannelTypes.AnchorOutputsZeroFeeHtlcTx()) peerConnection.send(peer, open) peerConnection.expectMsg(Error(open.temporaryChannelId, "rate limit reached")) assert(peer.stateData.channels.isEmpty) } - test("use correct on-chain fee rates when spawning a channel (anchor outputs)", Tag(ChannelStateTestsTags.AnchorOutputs)) { f => + test("use correct on-chain fee rates when spawning a channel (anchor outputs phoenix)", Tag(ChannelStateTestsTags.AnchorOutputsPhoenix)) { f => import f._ val probe = TestProbe() @@ -552,16 +542,16 @@ class PeerSpec extends FixtureSpec { // We ensure the current network feerate is higher than the default anchor output feerate. nodeParams.setBitcoinCoreFeerates(FeeratesPerKw.single(TestConstants.anchorOutputsFeeratePerKw * 2).copy(minimum = FeeratePerKw(250 sat))) - probe.send(peer, Peer.OpenChannel(remoteNodeId, 15000 sat, None, None, None, None, None, None, None)) + probe.send(peer, Peer.OpenChannel(remoteNodeId, 15000 sat, Some(ChannelTypes.AnchorOutputs()), None, None, None, None, None, None)) val init = channel.expectMsgType[INPUT_INIT_CHANNEL_INITIATOR] assert(init.channelType == ChannelTypes.AnchorOutputs()) assert(!init.dualFunded) assert(init.fundingAmount == 15000.sat) - assert(init.commitTxFeerate == TestConstants.anchorOutputsFeeratePerKw) + assert(init.commitTxFeerate == TestConstants.phoenixCommitFeeratePerKw) assert(init.fundingTxFeerate == nodeParams.onChainFeeConf.getFundingFeerate(nodeParams.currentFeeratesForFundingClosing)) } - test("use correct on-chain fee rates when spawning a channel (anchor outputs zero fee htlc)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + test("use correct on-chain fee rates when spawning a channel (anchor outputs zero fee htlc)") { f => import f._ val probe = TestProbe() @@ -570,7 +560,7 @@ class PeerSpec extends FixtureSpec { // We ensure the current network feerate is higher than the default anchor output feerate. nodeParams.setBitcoinCoreFeerates(FeeratesPerKw.single(TestConstants.anchorOutputsFeeratePerKw * 2).copy(minimum = FeeratePerKw(250 sat))) - probe.send(peer, Peer.OpenChannel(remoteNodeId, 15000 sat, None, None, None, None, None, None, None)) + probe.send(peer, Peer.OpenChannel(remoteNodeId, 15000 sat, Some(ChannelTypes.AnchorOutputsZeroFeeHtlcTx()), None, None, None, None, None, None)) val init = channel.expectMsgType[INPUT_INIT_CHANNEL_INITIATOR] assert(init.channelType == ChannelTypes.AnchorOutputsZeroFeeHtlcTx()) assert(!init.dualFunded) @@ -579,20 +569,7 @@ class PeerSpec extends FixtureSpec { assert(init.fundingTxFeerate == nodeParams.onChainFeeConf.getFundingFeerate(nodeParams.currentFeeratesForFundingClosing)) } - test("use correct final script if option_static_remotekey is negotiated", Tag(ChannelStateTestsTags.StaticRemoteKey)) { f => - import f._ - - val probe = TestProbe() - connect(remoteNodeId, peer, peerConnection, switchboard, remoteInit = protocol.Init(Features(StaticRemoteKey -> Mandatory))) - probe.send(peer, Peer.OpenChannel(remoteNodeId, 24000 sat, None, None, None, None, None, None, None)) - val init = channel.expectMsgType[INPUT_INIT_CHANNEL_INITIATOR] - assert(init.channelType == ChannelTypes.StaticRemoteKey()) - assert(!init.dualFunded) - assert(init.localParams.walletStaticPaymentBasepoint.isDefined) - assert(init.localParams.upfrontShutdownScript_opt.isEmpty) - } - - test("compute max-htlc-value-in-flight based on funding amount", Tag("max-htlc-value-in-flight-percent"), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + test("compute max-htlc-value-in-flight based on funding amount", Tag("max-htlc-value-in-flight-percent")) { f => import f._ val probe = TestProbe() @@ -601,27 +578,27 @@ class PeerSpec extends FixtureSpec { assert(peer.underlyingActor.nodeParams.channelConf.maxHtlcValueInFlightMsat == 100_000_000.msat) { - probe.send(peer, Peer.OpenChannel(remoteNodeId, 200_000 sat, None, None, None, None, None, None, None)) + probe.send(peer, Peer.OpenChannel(remoteNodeId, 200_000 sat, Some(ChannelTypes.AnchorOutputsZeroFeeHtlcTx()), None, None, None, None, None, None)) val init = channel.expectMsgType[INPUT_INIT_CHANNEL_INITIATOR] - assert(init.localParams.maxHtlcValueInFlightMsat == 50_000_000.msat) // max-htlc-value-in-flight-percent + assert(init.proposedCommitParams.localMaxHtlcValueInFlight == UInt64(50_000_000)) // max-htlc-value-in-flight-percent } { - probe.send(peer, Peer.OpenChannel(remoteNodeId, 500_000 sat, None, None, None, None, None, None, None)) + probe.send(peer, Peer.OpenChannel(remoteNodeId, 500_000 sat, Some(ChannelTypes.AnchorOutputsZeroFeeHtlcTx()), None, None, None, None, None, None)) val init = channel.expectMsgType[INPUT_INIT_CHANNEL_INITIATOR] - assert(init.localParams.maxHtlcValueInFlightMsat == 100_000_000.msat) // max-htlc-value-in-flight-msat + assert(init.proposedCommitParams.localMaxHtlcValueInFlight == UInt64(100_000_000)) // max-htlc-value-in-flight-msat } { - val open = createOpenChannelMessage().copy(fundingSatoshis = 200_000 sat) + val open = createOpenChannelMessage(ChannelTypes.AnchorOutputsZeroFeeHtlcTx()).copy(fundingSatoshis = 200_000 sat) peerConnection.send(peer, open) val init = channel.expectMsgType[INPUT_INIT_CHANNEL_NON_INITIATOR] - assert(init.localParams.maxHtlcValueInFlightMsat == 50_000_000.msat) // max-htlc-value-in-flight-percent + assert(init.proposedCommitParams.localMaxHtlcValueInFlight == UInt64(50_000_000)) // max-htlc-value-in-flight-percent channel.expectMsg(open) } { - val open = createOpenChannelMessage().copy(fundingSatoshis = 500_000 sat) + val open = createOpenChannelMessage(ChannelTypes.AnchorOutputsZeroFeeHtlcTx()).copy(fundingSatoshis = 500_000 sat) peerConnection.send(peer, open) val init = channel.expectMsgType[INPUT_INIT_CHANNEL_NON_INITIATOR] - assert(init.localParams.maxHtlcValueInFlightMsat == 100_000_000.msat) // max-htlc-value-in-flight-msat + assert(init.proposedCommitParams.localMaxHtlcValueInFlight == UInt64(100_000_000)) // max-htlc-value-in-flight-msat channel.expectMsg(open) } } @@ -639,7 +616,7 @@ class PeerSpec extends FixtureSpec { val probe = TestProbe() connect(remoteNodeId, peer, peerConnection, switchboard) - probe.send(peer, Peer.OpenChannel(remoteNodeId, 15000 sat, None, Some(100 msat), None, None, None, None, None)) + probe.send(peer, Peer.OpenChannel(remoteNodeId, 15000 sat, Some(ChannelTypes.AnchorOutputsZeroFeeHtlcTx()), Some(100 msat), None, None, None, None, None)) val init = channel.expectMsgType[INPUT_INIT_CHANNEL_INITIATOR] assert(init.replyTo == probe.ref.toTyped[OpenChannelResponse]) } @@ -710,10 +687,10 @@ class PeerSpec extends FixtureSpec { test("abort channel open request if peer reconnects before channel is accepted") { f => import f._ val probe = TestProbe() - val open = createOpenChannelMessage() + val open = createOpenChannelMessage(ChannelTypes.AnchorOutputsZeroFeeHtlcTx()) system.eventStream.subscribe(probe.ref, classOf[ChannelAborted]) connect(remoteNodeId, peer, peerConnection, switchboard) - peer ! SpawnChannelNonInitiator(Left(open), ChannelConfig.standard, ChannelTypes.Standard(), None, localParams, ActorRef.noSender) + peer ! SpawnChannelNonInitiator(Left(open), ChannelConfig.standard, ChannelTypes.AnchorOutputsZeroFeeHtlcTx(), None, ChannelCodecsSpec.localChannelParams, ActorRef.noSender) val channelAborted = probe.expectMsgType[ChannelAborted] assert(channelAborted.remoteNodeId == remoteNodeId) assert(channelAborted.channelId == open.temporaryChannelId) @@ -723,7 +700,7 @@ class PeerSpec extends FixtureSpec { import f._ connect(remoteNodeId, peer, peerConnection, switchboard, remoteInit = protocol.Init(Features(StaticRemoteKey -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional, DualFunding -> Optional))) - val open = createOpenDualFundedChannelMessage() + val open = createOpenDualFundedChannelMessage(ChannelTypes.AnchorOutputsZeroFeeHtlcTx()) peerConnection.send(peer, open) assert(channel.expectMsgType[INPUT_INIT_CHANNEL_NON_INITIATOR].dualFunded) channel.expectMsg(open) @@ -755,7 +732,7 @@ class PeerSpec extends FixtureSpec { val paymentHash = randomBytes32() connect(remoteNodeId, peer, peerConnection, switchboard, remoteInit = protocol.Init(Features(StaticRemoteKey -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional, DualFunding -> Optional))) val requestFunds = LiquidityAds.RequestFunding(50_000 sat, LiquidityAds.FundingRate(10_000 sat, 100_000 sat, 0, 0, 0 sat, 0 sat), LiquidityAds.PaymentDetails.FromFutureHtlc(paymentHash :: Nil)) - val open = inside(createOpenDualFundedChannelMessage()) { msg => msg.copy(tlvStream = TlvStream(ChannelTlv.RequestFundingTlv(requestFunds))) } + val open = createOpenDualFundedChannelMessage(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(), Some(requestFunds)) peerConnection.send(peer, open) peerConnection.expectMsg(CancelOnTheFlyFunding(open.temporaryChannelId, paymentHash :: Nil, "payments paid with future HTLCs are currently disabled")) channel.expectNoMessage(100 millis) @@ -842,7 +819,7 @@ class PeerSpec extends FixtureSpec { assert(nodeParams.db.peers.getPeer(remoteNodeId).isEmpty) // Our peer wants to open a channel to us, but we disconnect before we have a confirmed channel. - peer ! SpawnChannelNonInitiator(Left(createOpenChannelMessage()), ChannelConfig.standard, ChannelTypes.Standard(), None, localParams, peerConnection.ref) + peer ! SpawnChannelNonInitiator(Left(createOpenChannelMessage(ChannelTypes.AnchorOutputsZeroFeeHtlcTx())), ChannelConfig.standard, ChannelTypes.AnchorOutputsZeroFeeHtlcTx(), None, ChannelCodecsSpec.localChannelParams, peerConnection.ref) peer ! Peer.ConnectionDown(peerConnection.ref) probe.send(peer, Peer.GetPeerInfo(Some(probe.ref.toTyped))) assert(probe.expectMsgType[Peer.PeerInfo].state == Peer.DISCONNECTED) @@ -889,12 +866,16 @@ object PeerSpec { (mockServer, mockServer.getLocalAddress.asInstanceOf[InetSocketAddress]) } - def createOpenChannelMessage(openTlv: TlvStream[OpenChannelTlv] = TlvStream.empty): protocol.OpenChannel = { - protocol.OpenChannel(Block.RegtestGenesisBlock.hash, randomBytes32(), 250_000 sat, 0 msat, 483 sat, UInt64(100), 1000 sat, 1 msat, TestConstants.feeratePerKw, CltvExpiryDelta(144), 10, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, ChannelFlags(announceChannel = false), openTlv) + def createOpenChannelMessage(channelType: ChannelType): protocol.OpenChannel = { + protocol.OpenChannel(Block.RegtestGenesisBlock.hash, randomBytes32(), 250_000 sat, 0 msat, 483 sat, UInt64(100), 1000 sat, 1 msat, TestConstants.feeratePerKw, CltvExpiryDelta(144), 10, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, ChannelFlags(announceChannel = false), TlvStream(ChannelTlv.ChannelTypeTlv(channelType))) } - def createOpenDualFundedChannelMessage(): protocol.OpenDualFundedChannel = { - protocol.OpenDualFundedChannel(Block.RegtestGenesisBlock.hash, randomBytes32(), TestConstants.feeratePerKw, TestConstants.anchorOutputsFeeratePerKw, 250_000 sat, 483 sat, UInt64(100), 1 msat, CltvExpiryDelta(144), 10, 0, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, ChannelFlags(announceChannel = false)) + def createOpenDualFundedChannelMessage(channelType: ChannelType, requestFunding_opt: Option[LiquidityAds.RequestFunding] = None): protocol.OpenDualFundedChannel = { + val tlvs = TlvStream(Set( + Some(ChannelTlv.ChannelTypeTlv(channelType)), + requestFunding_opt.map(ChannelTlv.RequestFundingTlv(_)), + ).flatten[OpenDualFundedChannelTlv]) + protocol.OpenDualFundedChannel(Block.RegtestGenesisBlock.hash, randomBytes32(), TestConstants.feeratePerKw, TestConstants.anchorOutputsFeeratePerKw, 250_000 sat, 483 sat, UInt64(100), 1 msat, CltvExpiryDelta(144), 10, 0, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, ChannelFlags(announceChannel = false), tlvs) } } \ No newline at end of file diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/io/PendingChannelsRateLimiterSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/io/PendingChannelsRateLimiterSpec.scala index 6dd156453f..713b399db9 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/io/PendingChannelsRateLimiterSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/io/PendingChannelsRateLimiterSpec.scala @@ -18,6 +18,7 @@ package fr.acinq.eclair.io import akka.actor.testkit.typed.scaladsl.{ScalaTestWithActorTestKit, TestProbe} import akka.actor.typed.eventstream.EventStream.Publish +import com.softwaremill.quicklens.ModifyPimp import com.typesafe.config.ConfigFactory import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, SatoshiLong, Transaction, TxId, TxOut} @@ -50,16 +51,16 @@ class PendingChannelsRateLimiterSpec extends ScalaTestWithActorTestKit(ConfigFac val channelIdAtLimit2: ByteVector32 = ByteVector32(hex"0999999990000000000000000000000000000000000000000000000000000000") // This peer is whitelisted and starts tests with pending channels at the rate-limit (which should be ignored because it is whitelisted). - val peerOnWhitelistAtLimit = randomKey().publicKey + val peerOnWhitelistAtLimit: PublicKey = randomKey().publicKey // The following two peers start tests already at their rate-limit. val peerAtLimit1: PublicKey = randomKey().publicKey val peerAtLimit2: PublicKey = randomKey().publicKey - val peersAtLimit = Seq(peerAtLimit1, peerAtLimit2) + val peersAtLimit: Seq[PublicKey] = Seq(peerAtLimit1, peerAtLimit2) // The following two peers start tests with one available slot before reaching the rate-limit. val peerBelowLimit1: PublicKey = randomKey().publicKey val peerBelowLimit2: PublicKey = randomKey().publicKey - val peersBelowLimit = Seq(peerBelowLimit1, peerBelowLimit2) - val publicPeers = Seq(peerOnWhitelistAtLimit, peerAtLimit1, peerAtLimit2, peerBelowLimit1, peerBelowLimit2) + val peersBelowLimit: Seq[PublicKey] = Seq(peerBelowLimit1, peerBelowLimit2) + val publicPeers: Seq[PublicKey] = Seq(peerOnWhitelistAtLimit, peerAtLimit1, peerAtLimit2, peerBelowLimit1, peerBelowLimit2) // This peer has one pending private channel. val privatePeer1: PublicKey = randomKey().publicKey // This peer has one private channel that isn't pending. @@ -72,7 +73,7 @@ class PendingChannelsRateLimiterSpec extends ScalaTestWithActorTestKit(ConfigFac val probe = TestProbe[PendingChannelsRateLimiter.Response]() val nodeParams = TestConstants.Alice.nodeParams.copy(channelConf = TestConstants.Alice.nodeParams.channelConf.copy(maxPendingChannelsPerPeer = maxPendingChannelsPerPeer, maxTotalPendingChannelsPrivateNodes = maxTotalPendingChannelsPrivateNodes, channelOpenerWhitelist = Set(peerOnWhitelistAtLimit))) val tx = Transaction.read("010000000110f01d4a4228ef959681feb1465c2010d0135be88fd598135b2e09d5413bf6f1000000006a473044022074658623424cebdac8290488b76f893cfb17765b7a3805e773e6770b7b17200102202892cfa9dda662d5eac394ba36fcfd1ea6c0b8bb3230ab96220731967bbdb90101210372d437866d9e4ead3d362b01b615d24cc0d5152c740d51e3c55fb53f6d335d82ffffffff01408b0700000000001976a914678db9a7caa2aca887af1177eda6f3d0f702df0d88ac00000000") - val closingTx = ClosingTx(InputInfo(tx.txIn.head.outPoint, TxOut(10_000 sat, Nil), Nil), tx, None) + val closingTx = ClosingTx(InputInfo(tx.txIn.head.outPoint, TxOut(10_000 sat, Nil)), tx, None) val channelsOnWhitelistAtLimit: Seq[PersistentChannelData] = Seq( DATA_WAIT_FOR_FUNDING_CONFIRMED(commitments(peerOnWhitelistAtLimit, randomBytes32()), BlockHeight(0), None, Left(FundingCreated(randomBytes32(), TxId(ByteVector32.Zeroes), 3, randomBytes64()))), DATA_WAIT_FOR_CHANNEL_READY(commitments(peerOnWhitelistAtLimit, randomBytes32()), ShortIdAliases(ShortChannelId.generateLocalAlias(), None)), @@ -90,13 +91,13 @@ class PendingChannelsRateLimiterSpec extends ScalaTestWithActorTestKit(ConfigFac ) val channelsBelowLimit2 = Seq( DATA_WAIT_FOR_DUAL_FUNDING_READY(commitments(peerBelowLimit2, channelIdBelowLimit2), ShortIdAliases(ShortChannelId.generateLocalAlias(), None)), - DATA_NORMAL(commitments(peerBelowLimit2, randomBytes32()), ShortIdAliases(ShortChannelId.generateLocalAlias(), None), None, null, None, None, None, SpliceStatus.NoSplice), + DATA_NORMAL(commitments(peerBelowLimit2, randomBytes32()), ShortIdAliases(ShortChannelId.generateLocalAlias(), None), None, null, SpliceStatus.NoSplice, None, None, None), DATA_SHUTDOWN(commitments(peerBelowLimit2, randomBytes32()), Shutdown(randomBytes32(), ByteVector.empty), Shutdown(randomBytes32(), ByteVector.empty), CloseStatus.Initiator(None)), DATA_CLOSING(commitments(peerBelowLimit2, randomBytes32()), BlockHeight(0), ByteVector.empty, List(), List(closingTx)) ) val privateChannels = Seq( DATA_WAIT_FOR_DUAL_FUNDING_READY(commitments(privatePeer1, channelIdPrivate1), ShortIdAliases(ShortChannelId.generateLocalAlias(), None)), - DATA_NORMAL(commitments(privatePeer2, randomBytes32()), ShortIdAliases(ShortChannelId.generateLocalAlias(), None), None, null, None, None, None, SpliceStatus.NoSplice), + DATA_NORMAL(commitments(privatePeer2, randomBytes32()), ShortIdAliases(ShortChannelId.generateLocalAlias(), None), None, null, SpliceStatus.NoSplice, None, None, None), ) val initiatorChannels = Seq( DATA_WAIT_FOR_FUNDING_CONFIRMED(commitments(peerBelowLimit1, randomBytes32(), isOpener = true), BlockHeight(0), None, Left(FundingCreated(channelIdAtLimit1, TxId(ByteVector32.Zeroes), 3, randomBytes64()))), @@ -114,8 +115,9 @@ class PendingChannelsRateLimiterSpec extends ScalaTestWithActorTestKit(ConfigFac def commitments(remoteNodeId: PublicKey, channelId: ByteVector32, isOpener: Boolean = false): Commitments = { val ann = Announcements.makeChannelAnnouncement(Block.RegtestGenesisBlock.hash, RealShortChannelId(42), TestConstants.Alice.nodeParams.nodeId, remoteNodeId, randomKey().publicKey, randomKey().publicKey, randomBytes64(), randomBytes64(), randomBytes64(), randomBytes64()) - val commitments = CommitmentsSpec.makeCommitments(500_000 msat, 400_000 msat, TestConstants.Alice.nodeParams.nodeId, remoteNodeId, announcement_opt = Some(ann)) - commitments.copy(params = commitments.params.copy(channelId = channelId, localParams = commitments.params.localParams.copy(isChannelOpener = isOpener))) + CommitmentsSpec.makeCommitments(500_000 msat, 400_000 msat, TestConstants.Alice.nodeParams.nodeId, remoteNodeId, announcement_opt = Some(ann)) + .modify(_.channelParams.channelId).setTo(channelId) + .modify(_.channelParams.localParams.isChannelOpener).setTo(isOpener) } def processRestoredChannels(f: FixtureParam, restoredChannels: Seq[PersistentChannelData]): Unit = { @@ -318,7 +320,7 @@ class PendingChannelsRateLimiterSpec extends ScalaTestWithActorTestKit(ConfigFac val channels = Seq( DATA_WAIT_FOR_CHANNEL_READY(commitments(randomKey().publicKey, randomBytes32()), ShortIdAliases(ShortChannelId.generateLocalAlias(), None)), DATA_WAIT_FOR_DUAL_FUNDING_READY(commitments(randomKey().publicKey, randomBytes32()), ShortIdAliases(ShortChannelId.generateLocalAlias(), None)), - DATA_NORMAL(commitments(randomKey().publicKey, randomBytes32()), ShortIdAliases(ShortChannelId.generateLocalAlias(), None), None, null, None, None, None, SpliceStatus.NoSplice), + DATA_NORMAL(commitments(randomKey().publicKey, randomBytes32()), ShortIdAliases(ShortChannelId.generateLocalAlias(), None), None, null, SpliceStatus.NoSplice, None, None, None), DATA_SHUTDOWN(commitments(randomKey().publicKey, randomBytes32()), Shutdown(randomBytes32(), ByteVector.empty), Shutdown(randomBytes32(), ByteVector.empty), CloseStatus.Initiator(None)), DATA_WAIT_FOR_FUNDING_CONFIRMED(commitments(randomKey().publicKey, randomBytes32()), BlockHeight(0), None, Left(FundingCreated(randomBytes32(), TxId(ByteVector32.Zeroes), 3, randomBytes64()))), ) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/io/ReconnectionTaskSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/io/ReconnectionTaskSpec.scala index be1f99970c..9f2611fe45 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/io/ReconnectionTaskSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/io/ReconnectionTaskSpec.scala @@ -250,7 +250,7 @@ class ReconnectionTaskSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike assert(ReconnectionTask.selectNodeAddress(nodeParams, List(clearnet, tor)).contains(clearnet)) } { - // tor supported and enabled for clearnet addresses: return tor addresses when available + // tor supported and enabled for clearnet addresses: return both tor and clearnet addresses when available val socksParams = mock[Socks5ProxyParams] socksParams.useForTor returns true socksParams.useForIPv4 returns true @@ -258,7 +258,109 @@ class ReconnectionTaskSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike nodeParams.socksProxy_opt returns Some(socksParams) assert(ReconnectionTask.selectNodeAddress(nodeParams, List(clearnet)).contains(clearnet)) assert(ReconnectionTask.selectNodeAddress(nodeParams, List(tor)).contains(tor)) - assert(ReconnectionTask.selectNodeAddress(nodeParams, List(clearnet, tor)).contains(tor)) + assert(ReconnectionTask.selectNodeAddress(nodeParams, List(clearnet, tor)).exists(Set(clearnet, tor)(_))) + } + { + // tor supported and enabled for clearnet addresses, but disabled for tor: return clearnet addresses when available + val socksParams = mock[Socks5ProxyParams] + socksParams.useForTor returns false + socksParams.useForIPv4 returns true + socksParams.useForIPv6 returns true + nodeParams.socksProxy_opt returns Some(socksParams) + assert(ReconnectionTask.selectNodeAddress(nodeParams, List(clearnet)).contains(clearnet)) + assert(ReconnectionTask.selectNodeAddress(nodeParams, List(tor)).isEmpty) + assert(ReconnectionTask.selectNodeAddress(nodeParams, List(clearnet, tor)).contains(clearnet)) + } + } + + test("select node addresses for reconnection") { () => + val nodeParams = mock[NodeParams] + val clearnetIPv4 = NodeAddress.fromParts("1.2.3.4", 9735).get + val clearnetIPv6 = NodeAddress.fromParts("2001:db8::1", 9735).get + val tor = NodeAddress.fromParts("iq7zhmhck54vcax2vlrdcavq2m32wao7ekh6jyeglmnuuvv3js57r4id.onion", 9735).get + val dnsHostname = NodeAddress.fromParts("example.com", 9735).get + + { + // no proxy configured: only return clearnet addresses + nodeParams.socksProxy_opt returns None + assert(ReconnectionTask.selectNodeAddresses(nodeParams, List(clearnetIPv4)) == List(clearnetIPv4)) + assert(ReconnectionTask.selectNodeAddresses(nodeParams, List(tor)) == List.empty) + assert(ReconnectionTask.selectNodeAddresses(nodeParams, List(clearnetIPv4, tor)) == List(clearnetIPv4)) + assert(ReconnectionTask.selectNodeAddresses(nodeParams, List(clearnetIPv4, clearnetIPv6, tor)) == List(clearnetIPv4, clearnetIPv6)) + assert(ReconnectionTask.selectNodeAddresses(nodeParams, List(dnsHostname, tor)) == List(dnsHostname)) + } + { + // proxy configured but not for tor: only return clearnet addresses + val socksParams = mock[Socks5ProxyParams] + socksParams.useForTor returns false + socksParams.useForIPv4 returns true + socksParams.useForIPv6 returns true + nodeParams.socksProxy_opt returns Some(socksParams) + assert(ReconnectionTask.selectNodeAddresses(nodeParams, List(clearnetIPv4)) == List(clearnetIPv4)) + assert(ReconnectionTask.selectNodeAddresses(nodeParams, List(tor)) == List.empty) + assert(ReconnectionTask.selectNodeAddresses(nodeParams, List(clearnetIPv4, tor)) == List(clearnetIPv4)) + assert(ReconnectionTask.selectNodeAddresses(nodeParams, List(clearnetIPv4, clearnetIPv6, tor)) == List(clearnetIPv4, clearnetIPv6)) + assert(ReconnectionTask.selectNodeAddresses(nodeParams, List(dnsHostname, tor)) == List(dnsHostname)) + } + { + // proxy configured for tor but not for IPv4: return tor addresses only if there ar no clearnet addresses, otherwise return clearnet addresses + val socksParams = mock[Socks5ProxyParams] + socksParams.useForTor returns true + socksParams.useForIPv4 returns false + socksParams.useForIPv6 returns true + nodeParams.socksProxy_opt returns Some(socksParams) + assert(ReconnectionTask.selectNodeAddresses(nodeParams, List(clearnetIPv4)) == List(clearnetIPv4)) + assert(ReconnectionTask.selectNodeAddresses(nodeParams, List(clearnetIPv6)) == List(clearnetIPv6)) + assert(ReconnectionTask.selectNodeAddresses(nodeParams, List(tor)) == List(tor)) + assert(ReconnectionTask.selectNodeAddresses(nodeParams, List(clearnetIPv4, tor)) == List(clearnetIPv4)) + assert(ReconnectionTask.selectNodeAddresses(nodeParams, List(clearnetIPv6, tor)) == List(clearnetIPv6)) + assert(ReconnectionTask.selectNodeAddresses(nodeParams, List(clearnetIPv4, clearnetIPv6, tor)) == List(clearnetIPv4, clearnetIPv6)) + assert(ReconnectionTask.selectNodeAddresses(nodeParams, List(dnsHostname, tor)) == List(dnsHostname)) + } + { + // proxy configured for tor but not for IPv6: return tor addresses only if there ar no clearnet addresses, otherwise return clearnet addresses + val socksParams = mock[Socks5ProxyParams] + socksParams.useForTor returns true + socksParams.useForIPv4 returns true + socksParams.useForIPv6 returns false + nodeParams.socksProxy_opt returns Some(socksParams) + assert(ReconnectionTask.selectNodeAddresses(nodeParams, List(clearnetIPv4)) == List(clearnetIPv4)) + assert(ReconnectionTask.selectNodeAddresses(nodeParams, List(clearnetIPv6)) == List(clearnetIPv6)) + assert(ReconnectionTask.selectNodeAddresses(nodeParams, List(tor)) == List(tor)) + assert(ReconnectionTask.selectNodeAddresses(nodeParams, List(clearnetIPv4, tor)) == List(clearnetIPv4)) + assert(ReconnectionTask.selectNodeAddresses(nodeParams, List(clearnetIPv6, tor)) == List(clearnetIPv6)) + assert(ReconnectionTask.selectNodeAddresses(nodeParams, List(clearnetIPv4, clearnetIPv6, tor)) == List(clearnetIPv4, clearnetIPv6)) + assert(ReconnectionTask.selectNodeAddresses(nodeParams, List(dnsHostname, tor)) == List(dnsHostname)) + } + { + // proxy configured for tor but not for IPv4 and IPv6: return tor addresses only if there ar no clearnet addresses, otherwise return clearnet addresses + val socksParams = mock[Socks5ProxyParams] + socksParams.useForTor returns true + socksParams.useForIPv4 returns false + socksParams.useForIPv6 returns false + nodeParams.socksProxy_opt returns Some(socksParams) + assert(ReconnectionTask.selectNodeAddresses(nodeParams, List(clearnetIPv4)) == List(clearnetIPv4)) + assert(ReconnectionTask.selectNodeAddresses(nodeParams, List(clearnetIPv6)) == List(clearnetIPv6)) + assert(ReconnectionTask.selectNodeAddresses(nodeParams, List(tor)) == List(tor)) + assert(ReconnectionTask.selectNodeAddresses(nodeParams, List(clearnetIPv4, tor)) == List(clearnetIPv4)) + assert(ReconnectionTask.selectNodeAddresses(nodeParams, List(clearnetIPv6, tor)) == List(clearnetIPv6)) + assert(ReconnectionTask.selectNodeAddresses(nodeParams, List(clearnetIPv4, clearnetIPv6, tor)) == List(clearnetIPv4, clearnetIPv6)) + assert(ReconnectionTask.selectNodeAddresses(nodeParams, List(dnsHostname, tor)) == List(dnsHostname)) + } + { + // proxy configured for tor and both IPv4/IPv6: return all addresses + val socksParams = mock[Socks5ProxyParams] + socksParams.useForTor returns true + socksParams.useForIPv4 returns true + socksParams.useForIPv6 returns true + nodeParams.socksProxy_opt returns Some(socksParams) + assert(ReconnectionTask.selectNodeAddresses(nodeParams, List(clearnetIPv4)) == List(clearnetIPv4)) + assert(ReconnectionTask.selectNodeAddresses(nodeParams, List(clearnetIPv6)) == List(clearnetIPv6)) + assert(ReconnectionTask.selectNodeAddresses(nodeParams, List(tor)) == List(tor)) + assert(ReconnectionTask.selectNodeAddresses(nodeParams, List(clearnetIPv4, tor)) == List(clearnetIPv4, tor)) + assert(ReconnectionTask.selectNodeAddresses(nodeParams, List(clearnetIPv6, tor)) == List(clearnetIPv6, tor)) + assert(ReconnectionTask.selectNodeAddresses(nodeParams, List(clearnetIPv4, clearnetIPv6, tor)) == List(clearnetIPv4, clearnetIPv6, tor)) + assert(ReconnectionTask.selectNodeAddresses(nodeParams, List(dnsHostname, tor)) == List(dnsHostname, tor)) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/io/SwitchboardSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/io/SwitchboardSpec.scala index d3f0e8c7db..ab4bdca2ab 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/io/SwitchboardSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/io/SwitchboardSpec.scala @@ -4,18 +4,16 @@ import akka.actor.typed.scaladsl.adapter.ClassicActorRefOps import akka.actor.{Actor, ActorContext, ActorRef, Props} import akka.testkit.{TestActorRef, TestProbe} import com.softwaremill.quicklens.ModifyPimp -import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, OutPoint, Satoshi, TxHash, TxOut} import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.{ByteVector64, Satoshi} import fr.acinq.eclair.TestConstants._ -import fr.acinq.eclair.channel.SpliceStatus.NoSplice -import fr.acinq.eclair.channel.{ChannelFeatures, ChannelFlags, ChannelIdAssigned, ChannelParams, CommitTxAndRemoteSig, Commitment, Commitments, DATA_NORMAL, LocalCommit, LocalParams, PersistentChannelData, RemoteCommit, RemoteParams, Upstream} +import fr.acinq.eclair.channel.{ChannelIdAssigned, DATA_NORMAL, PersistentChannelData, Upstream} import fr.acinq.eclair.io.Peer.PeerNotFound import fr.acinq.eclair.io.Switchboard._ import fr.acinq.eclair.payment.relay.{OnTheFlyFunding, OnTheFlyFundingSpec} -import fr.acinq.eclair.transactions.Transactions.{CommitTx, InputInfo} import fr.acinq.eclair.wire.internal.channel.ChannelCodecsSpec import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{CltvExpiry, Features, InitFeature, MilliSatoshiLong, NodeParams, TestKitBaseClass, TimestampSecondLong, UInt64, randomBytes32, randomKey} +import fr.acinq.eclair.{CltvExpiry, Features, InitFeature, MilliSatoshiLong, NodeParams, TestKitBaseClass, TimestampSecondLong, randomBytes32, randomKey} import org.scalatest.funsuite.AnyFunSuiteLike import scodec.bits._ @@ -166,8 +164,9 @@ class SwitchboardSpec extends TestKitBaseClass with AnyFunSuiteLike { } def dummyDataNormal(remoteNodeId: PublicKey, capacity: Satoshi): DATA_NORMAL = { - val data = ChannelCodecsSpec.normal.modify(_.commitments.params.remoteParams.nodeId).setTo(remoteNodeId) - .modify(_.commitments.active).apply(_.map(_.modify(_.localCommit.commitTxAndRemoteSig.commitTx.input.txOut.amount).setTo(capacity))) + val data = ChannelCodecsSpec.normal + .modify(_.commitments.channelParams.remoteParams.nodeId).setTo(remoteNodeId) + .modify(_.commitments.active).apply(_.map(_.modify(_.fundingAmount).setTo(capacity))) assert(data.remoteNodeId == remoteNodeId) assert(data.commitments.capacity == capacity) data diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/json/JsonSerializersSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/json/JsonSerializersSpec.scala index ac539bc313..0582de64ce 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/json/JsonSerializersSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/json/JsonSerializersSpec.scala @@ -22,26 +22,26 @@ import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin.scalacompat.{Block, Btc, ByteVector32, ByteVector64, DeterministicWallet, OutPoint, Satoshi, SatoshiLong, Transaction, TxHash, TxId, TxOut} import fr.acinq.eclair._ import fr.acinq.eclair.balance.CheckBalance -import fr.acinq.eclair.balance.CheckBalance.{ClosingBalance, GlobalBalance, MainAndHtlcBalance, PossiblyPublishedMainAndHtlcBalance, PossiblyPublishedMainBalance} -import fr.acinq.eclair.blockchain.fee.{ConfirmationTarget, FeeratePerKw} -import fr.acinq.eclair.channel.Helpers.Funding +import fr.acinq.eclair.balance.CheckBalance.{GlobalBalance, MainAndHtlcBalance} +import fr.acinq.eclair.blockchain.fee.FeeratePerKw +import fr.acinq.eclair.channel.ChannelSpendSignature.IndividualSignature import fr.acinq.eclair.channel._ -import fr.acinq.eclair.crypto.ShaChain +import fr.acinq.eclair.crypto.{ShaChain, Sphinx} import fr.acinq.eclair.db.OfferData import fr.acinq.eclair.io.Peer import fr.acinq.eclair.io.Peer.PeerInfo import fr.acinq.eclair.payment.{Invoice, PaymentSettlingOnChain} import fr.acinq.eclair.transactions.Transactions._ -import fr.acinq.eclair.transactions.{CommitmentSpec, IncomingHtlc, OutgoingHtlc} +import fr.acinq.eclair.transactions.{CommitmentSpec, IncomingHtlc, OutgoingHtlc, Transactions} import fr.acinq.eclair.wire.internal.channel.ChannelCodecs -import fr.acinq.eclair.wire.protocol.OfferTypes.Offer +import fr.acinq.eclair.wire.protocol.OfferTypes.{Offer, OfferTlv} import fr.acinq.eclair.wire.protocol._ import org.scalatest.funsuite.AnyFunSuiteLike import org.scalatest.matchers.should.Matchers import scodec.bits._ import java.net.InetAddress -import java.util.UUID +import java.util.{Currency, UUID} class JsonSerializersSpec extends TestKitBaseClass with AnyFunSuiteLike with Matchers { @@ -121,10 +121,12 @@ class JsonSerializersSpec extends TestKitBaseClass with AnyFunSuiteLike with Mat val probe = TestProbe()(system) val dummyPublicKey = PrivateKey(hex"0101010101010101010101010101010101010101010101010101010101010101").publicKey val dummyBytes32 = ByteVector32(hex"0202020202020202020202020202020202020202020202020202020202020202") - val localParams = LocalParams(dummyPublicKey, DeterministicWallet.KeyPath(Seq(42L)), 546 sat, Long.MaxValue.msat, Some(1000 sat), 1 msat, CltvExpiryDelta(144), 50, isChannelOpener = true, paysCommitTxFees = true, None, None, Features.empty) - val remoteParams = RemoteParams(dummyPublicKey, 546 sat, UInt64.MaxValue, Some(1000 sat), 1 msat, CltvExpiryDelta(144), 50, dummyPublicKey, dummyPublicKey, dummyPublicKey, dummyPublicKey, Features.empty, None) - val commitmentInput = Funding.makeFundingInputInfo(TxId(dummyBytes32), 0, 150_000 sat, dummyPublicKey, dummyPublicKey) - val localCommit = LocalCommit(0, CommitmentSpec(Set.empty, FeeratePerKw(2500 sat), 100_000_000 msat, 50_000_000 msat), CommitTxAndRemoteSig(CommitTx(commitmentInput, Transaction(2, Nil, Nil, 0)), ByteVector64.Zeroes), Nil) + val localChannelParams = LocalChannelParams(dummyPublicKey, DeterministicWallet.KeyPath(Seq(42L)), Some(1000 sat), isChannelOpener = true, paysCommitTxFees = true, None, Features.empty) + val localCommitParams = CommitParams(546 sat, 1 msat, UInt64(Long.MaxValue), 50, CltvExpiryDelta(144)) + val remoteChannelParams = RemoteChannelParams(dummyPublicKey, Some(1000 sat), dummyPublicKey, dummyPublicKey, dummyPublicKey, dummyPublicKey, Features.empty, None) + val remoteCommitParams = CommitParams(546 sat, 1 msat, UInt64.MaxValue, 50, CltvExpiryDelta(144)) + val commitmentInput = Transactions.makeFundingInputInfo(TxId(dummyBytes32), 0, 150_000 sat, dummyPublicKey, dummyPublicKey, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) + val localCommit = LocalCommit(0, CommitmentSpec(Set.empty, FeeratePerKw(2500 sat), 100_000_000 msat, 50_000_000 msat), TxId(dummyBytes32), IndividualSignature(ByteVector64.Zeroes), Nil) val remoteCommit = RemoteCommit(0, CommitmentSpec(Set.empty, FeeratePerKw(2500 sat), 50_000_000 msat, 100_000_000 msat), TxId(dummyBytes32), dummyPublicKey) val channelInfo = RES_GET_CHANNEL_INFO( PublicKey(hex"0270685ca81a8e4d4d01beec5781f4cc924684072ae52c507f8ebe9daf0caaab7b"), @@ -133,9 +135,9 @@ class JsonSerializersSpec extends TestKitBaseClass with AnyFunSuiteLike with Mat NORMAL, DATA_NORMAL( Commitments( - ChannelParams(dummyBytes32, ChannelConfig.standard, ChannelFeatures(), localParams, remoteParams, ChannelFlags(announceChannel = true)), + ChannelParams(dummyBytes32, ChannelConfig.standard, ChannelFeatures(), localChannelParams, remoteChannelParams, ChannelFlags(announceChannel = true)), CommitmentChanges(LocalChanges(Nil, Nil, Nil), RemoteChanges(Nil, Nil, Nil), localNextHtlcId = 1, remoteNextHtlcId = 1), - List(Commitment(0, 0, dummyPublicKey, LocalFundingStatus.SingleFundedUnconfirmedFundingTx(None), RemoteFundingStatus.Locked, localCommit, remoteCommit, None)), + List(Commitment(0, 0, commitmentInput.outPoint, 150_000 sat, dummyPublicKey, LocalFundingStatus.SingleFundedUnconfirmedFundingTx(None), RemoteFundingStatus.Locked, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat, localCommitParams, localCommit, remoteCommitParams, remoteCommit, None)), inactive = Nil, Right(dummyPublicKey), ShaChain.init, @@ -144,7 +146,7 @@ class JsonSerializersSpec extends TestKitBaseClass with AnyFunSuiteLike with Mat ShortIdAliases(Alias(42), None), None, ChannelUpdate(ByteVector64(hex"345b2b05ec046ffe0c14d3b61838c79980713ad1cf8ae7a45c172ce90c9c0b9f345b2b05ec046ffe0c14d3b61838c79980713ad1cf8ae7a45c172ce90c9c0b9f"), Block.RegtestGenesisBlock.hash, ShortChannelId(0), 0 unixsec, ChannelUpdate.MessageFlags(dontForward = false), ChannelUpdate.ChannelFlags.DUMMY, CltvExpiryDelta(12), 1 msat, 100 msat, 0, 2_000_000 msat), - None, None, None, SpliceStatus.NoSplice + SpliceStatus.NoSplice, None, None, None ) ) val expected = @@ -155,31 +157,21 @@ class JsonSerializersSpec extends TestKitBaseClass with AnyFunSuiteLike with Mat | "data": { | "type": "DATA_NORMAL", | "commitments": { - | "params": { + | "channelParams": { | "channelId": "0202020202020202020202020202020202020202020202020202020202020202", | "channelConfig": ["funding_pubkey_based_channel_keypath"], | "channelFeatures": [], | "localParams": { | "nodeId": "031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f", | "fundingKeyPath": [42], - | "dustLimit": 546, - | "maxHtlcValueInFlightMsat": 9223372036854775807, | "initialRequestedChannelReserve_opt": 1000, - | "htlcMinimum": 1, - | "toSelfDelay": 144, - | "maxAcceptedHtlcs": 50, | "isChannelOpener": true, | "paysCommitTxFees" : true, | "initFeatures": { "activated": {}, "unknown": [] } | }, | "remoteParams": { | "nodeId": "031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f", - | "dustLimit": 546, - | "maxHtlcValueInFlightMsat": 18446744073709551615, | "initialRequestedChannelReserve_opt": 1000, - | "htlcMinimum": 1, - | "toSelfDelay": 144, - | "maxAcceptedHtlcs": 50, | "revocationBasepoint": "031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f", | "paymentBasepoint": "031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f", | "delayedPaymentBasepoint": "031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f", @@ -200,19 +192,38 @@ class JsonSerializersSpec extends TestKitBaseClass with AnyFunSuiteLike with Mat | "active": [ | { | "fundingTxIndex": 0, - | "fundingTx": { "outPoint": "0202020202020202020202020202020202020202020202020202020202020202:0", "amountSatoshis": 150000 }, + | "fundingInput": "0202020202020202020202020202020202020202020202020202020202020202:0", + | "fundingAmount": 150000, | "localFunding": { "status":"unconfirmed" }, | "remoteFunding": { "status":"locked" }, + | "commitmentFormat": "anchor_outputs", + | "localCommitParams": { + | "dustLimit": 546, + | "htlcMinimum": 1, + | "maxHtlcValueInFlight": 9223372036854775807, + | "maxAcceptedHtlcs": 50, + | "toSelfDelay": 144 + | }, | "localCommit": { | "index": 0, | "spec": { "htlcs": [], "commitTxFeerate": 2500, "toLocal": 100000000, "toRemote": 50000000 }, - | "commitTxAndRemoteSig": { "commitTx": { "txid": "4ebd325a4b394cff8c57e8317ccf5a8d0e2bdf1b8526f8aad6c8e43d8240621a", "tx": "02000000000000000000" },"remoteSig": "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" }, - | "htlcTxsAndRemoteSigs": [] + | "txId": "0202020202020202020202020202020202020202020202020202020202020202", + | "remoteSig": { + | "sig": "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + | }, + | "htlcRemoteSigs": [] + | }, + | "remoteCommitParams": { + | "dustLimit": 546, + | "htlcMinimum": 1, + | "maxHtlcValueInFlight": 18446744073709551615, + | "maxAcceptedHtlcs": 50, + | "toSelfDelay": 144 | }, | "remoteCommit": { | "index": 0, | "spec": { "htlcs": [], "commitTxFeerate": 2500, "toLocal": 50000000, "toRemote": 100000000 }, - | "txid": "0202020202020202020202020202020202020202020202020202020202020202", + | "txId": "0202020202020202020202020202020202020202020202020202020202020202", | "remotePerCommitmentPoint": "031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f" | } | } @@ -257,7 +268,7 @@ class JsonSerializersSpec extends TestKitBaseClass with AnyFunSuiteLike with Mat hmac = ByteVector32(hex"9442626f72c475963dbddf8a57ab2cef3013eb3d6a5e8afbea9e631dac4481f5") ), pathKey_opt = None, - confidence = 0.7, + endorsement = 6, fundingFee_opt = None, ) @@ -288,7 +299,6 @@ class JsonSerializersSpec extends TestKitBaseClass with AnyFunSuiteLike with Mat val inputInfo = InputInfo( outPoint = OutPoint(TxHash.fromValidHex("345b2b05ec046ffe0c14d3b61838c79980713ad1cf8ae7a45c172ce90c9c0b9f"), 42), txOut = TxOut(456651 sat, hex"3c7a66997c681a3de1bae56438abeee4fc50a16554725a430ade1dc8db6bdd76704d45c6151c4051d710cf487e63"), - redeemScript = hex"00dc6c50f445ed53d2fb41067fdcb25686fe79492d90e6e5db43235726ace247210220773" ) JsonSerializers.serialization.write(inputInfo)(JsonSerializers.formats) shouldBe """{"outPoint":"9f0b9c0ce92c175ca4e78acfd13a718099c73818b6d3140cfe6f04ec052b5b34:42","amountSatoshis":456651}""" } @@ -333,6 +343,30 @@ class JsonSerializersSpec extends TestKitBaseClass with AnyFunSuiteLike with Mat JsonSerializers.serialization.write(pr)(JsonSerializers.formats) shouldBe """{"amount":456001234,"nodeId":"03c48ac97e09f3cbbaeb35b02aaa6d072b57726841a34d25952157caca60a1caf5","paymentHash":"2cb0e7b052366787450c33daf6d2f2c3cb6132221326e1c1b49ac97fdd7eb720","description":"minimal offer","features":{"activated":{},"unknown":[]},"blindedPaths":[{"introductionNodeId":"03c48ac97e09f3cbbaeb35b02aaa6d072b57726841a34d25952157caca60a1caf5","blindedNodeIds":["031fca650042031dcb777156ef66806c73b01a7f52c4e73c89a0d15823a1ac6237"]}],"createdAt":1665412681,"expiresAt":1665412981,"serialized":"lni1qqsf4h8fsnpjkj057gjg9c3eqhv889440xh0z6f5kng9vsaad8pgq7sgqsdjuqsqpgxk66twd9kkzmpqdanxvetjzcss83y2e9lqnu7tht4ntvp24fksw26hwf5yrg6dyk2jz472efs2rjh42qsxlc5vp2m0rvmjcxn2y34wv0m5lyc7sdj7zksgn35dvxgqqqqqqqzjqsdjupkjtqssx05572ha26x39rczan5yft22pgwa72jw8gytavkm5ydn7yf5kpgh5zsq83y2e9lqnu7tht4ntvp24fksw26hwf5yrg6dyk2jz472efs2rjh4q2rd3ny0elv9m7mh38xxwe6ypfheeqeqlwgft05r6dhc50gtw0nv2qgrrl9x2qzzqvwukam32mhkdqrvwwcp5l6jcnnnezdq69vz8gdvvgmsqwk3efqf3f6gmf0ul63940awz429rdhhsts86s0r30e5nffwhrqw90xgxf7f60sm7tcclvyqwz7cer5q9223madstdy2p5q6y8qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqf2qheqqqqq2gprrgshynfszqyk2sgpvkrnmq53kv7r52rpnmtmd9ukredsnygsnymsurdy6e9la6l4hyz4qgxewqmftqggrcj9vjlsf709m46e4kq425mg89dthy6zp5dxjt9fp2l9v5c9pet6lqsx4k5r7rsld3hhe87psyy5cnhhzt4dz838f75734mted7pdsrflpvys23tkafmhctf3musnsaa42h6qjdggyqlhtevutzzpzlnwd8alq"}""" } + test("Bolt 12 offer") { + val minimalOffer = Offer(TlvStream[OfferTlv](OfferTypes.OfferNodeId(PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f")))) + JsonSerializers.serialization.write(minimalOffer)(JsonSerializers.formats) shouldBe """{"nodeId":"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f"}""" + + val ref = "lno1pqzqzltcgq9q6urvv4shxefqv3hkuct5v58qxrp4qqfquctvd93k2srpvd5kuufwvdh3vggzg2hd49ueds8phzcahvh4p2m3pnen649dza2h3k6gxpaequr8fhtq" + val offer = Offer.decode(ref).get + JsonSerializers.serialization.write(offer)(JsonSerializers.formats) shouldBe """{"amount":"25000.000","currency":"satoshi","description":"please donate","expiry":{"iso":"1970-01-10T06:13:20Z","unix":800000},"issuer":"alice@acinq.co","nodeId":"0242aeda97996c0e1b8b1dbb2f50ab710cf33d54ad175578db48307b9070674dd6"}""" + + val bigOffer = Offer(TlvStream(Set[OfferTlv]( + OfferTypes.OfferChains(Seq(Block.Testnet4GenesisBlock.hash)), + OfferTypes.OfferMetadata(hex"d5f4a6"), + OfferTypes.OfferCurrency(Currency.getInstance("EUR")), + OfferTypes.OfferAmount(86205), + OfferTypes.OfferDescription("offer with a lot of fields in it"), + OfferTypes.OfferFeatures(Features(Features.ProvideStorage -> FeatureSupport.Mandatory).toByteVector), + OfferTypes.OfferAbsoluteExpiry(TimestampSecond(3600)), + OfferTypes.OfferPaths(Seq(Sphinx.RouteBlinding.BlindedRoute(EncodedNodeId.WithPublicKey.Plain(PublicKey(hex"022812e3a3760ac989b8749ee9fc70fd12e4d7f3cad5e3e2bf572e9e4eaaa7b7d9")), PublicKey(hex"028a2b20b2debdfd97de08f6e2374f2946116492f358b78acf9eac05f6fdac632d"), Seq(Sphinx.RouteBlinding.BlindedHop(PublicKey(hex"031b27d9e97dbb0ef87c48bb0231c96c6bca1ee54b0e0cfe869ad2388ce247719f"), hex"def5"))))), + OfferTypes.OfferIssuer("bob@bobcorp.com"), + OfferTypes.OfferQuantityMax(5), + OfferTypes.OfferNodeId(PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f")), + ), Set(GenericTlv(UInt64(71), hex"bd4e85ce")))) + JsonSerializers.serialization.write(bigOffer)(JsonSerializers.formats) shouldBe """{"chains":["43f08bdab050e35b567c864b91f47f50ae725ae2de53bcfbbaf284da00000000"],"amount":"862.05","currency":"EUR","description":"offer with a lot of fields in it","expiry":{"iso":"1970-01-01T01:00:00Z","unix":3600},"issuer":"bob@bobcorp.com","nodeId":"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f","paths":[{"firstNodeId":{"publicKey":"022812e3a3760ac989b8749ee9fc70fd12e4d7f3cad5e3e2bf572e9e4eaaa7b7d9"},"length":1}],"quantityMax":5,"features":{"activated":{"option_provide_storage":"mandatory"},"unknown":[]},"metadata":"d5f4a6","unknownTlvs":{"71":"bd4e85ce"}}""" + } + test("Bolt 12 offer data") { val ref = "lno1pqzqzltcgq9q6urvv4shxefqv3hkuct5v58qxrp4qqfquctvd93k2srpvd5kuufwvdh3vggzg2hd49ueds8phzcahvh4p2m3pnen649dza2h3k6gxpaequr8fhtq" val offer = OfferData(Offer.decode(ref).get, None, createdAt = TimestampMilli(100), disabledAt_opt = None) @@ -348,27 +382,25 @@ class JsonSerializersSpec extends TestKitBaseClass with AnyFunSuiteLike with Mat test("GlobalBalance serializer") { val gb = GlobalBalance( onChain = CheckBalance.DetailedOnChainBalance( - confirmed = Map(OutPoint(TxId.fromValidHex("9fcd45bbaa09c60c991ac0425704163c3f3d2d683c789fa409455b9c97792692"), 3) -> Btc(1.4)), + deeplyConfirmed = Map(OutPoint(TxId.fromValidHex("9fcd45bbaa09c60c991ac0425704163c3f3d2d683c789fa409455b9c97792692"), 3) -> Btc(1.4)), + recentlyConfirmed = Map(OutPoint(TxId.fromValidHex("4d176ad844c363bed59edf81962b008faa6194c3b3757ffcd26ba60f95716db2"), 5) -> Btc(0.7)), unconfirmed = Map(OutPoint(TxId.fromValidHex("345b2b05ec046ffe0c14d3b61838c79980713ad1cf8ae7a45c172ce90c9c0b9f"), 1) -> Btc(0.05)), - utxos = Seq.empty + utxos = Seq.empty, + recentlySpentInputs = Set.empty, ), - offChain = CheckBalance.OffChainBalance(normal = MainAndHtlcBalance( - toLocal = Btc(0.2), - htlcs = Btc(0.05) - ), closing = ClosingBalance( - localCloseBalance = PossiblyPublishedMainAndHtlcBalance( - toLocal = Map(OutPoint(TxId.fromValidHex("4d176ad844c363bed59edf81962b008faa6194c3b3757ffcd26ba60f95716db2"), 0) -> Btc(0.1)), - htlcs = Map(OutPoint(TxId.fromValidHex("94b70cec5a98d67d17c6e3de5c7697f8a6cab4f698df91e633ce35efa3574d71"), 1) -> Btc(0.03), OutPoint(TxId.fromValidHex("a844edd41ce8503864f3c95d89d850b177a09d7d35e950a7d27c14abb63adb13"), 3) -> Btc(0.06)), - htlcsUnpublished = Btc(0.01)), - mutualCloseBalance = PossiblyPublishedMainBalance( - toLocal = Map(OutPoint(TxId.fromValidHex("7e3b012534afe0bb8d1f2f09c724b1a10a813ce704e5b9c217ccfdffffff0247"), 4) -> Btc(0.1))) - ) + offChain = CheckBalance.OffChainBalance( + normal = MainAndHtlcBalance( + toLocal = Btc(0.2), + htlcs = Btc(0.05) + ), + closing = MainAndHtlcBalance( + toLocal = Btc(0.3), + htlcs = Btc(0.07) + ) ), channels = Map.empty, // not used by json serializer - knownPreimages = Set.empty, // not used by json serializer - ) - JsonSerializers.serialization.write(gb)(JsonSerializers.formats) shouldBe """{"total":2.0,"onChain":{"total":1.45,"confirmed":{"9fcd45bbaa09c60c991ac0425704163c3f3d2d683c789fa409455b9c97792692:3":1.4},"unconfirmed":{"345b2b05ec046ffe0c14d3b61838c79980713ad1cf8ae7a45c172ce90c9c0b9f:1":0.05}},"offChain":{"waitForFundingConfirmed":0.0,"waitForChannelReady":0.0,"normal":{"toLocal":0.2,"htlcs":0.05},"shutdown":{"toLocal":0.0,"htlcs":0.0},"negotiating":0.0,"closing":{"localCloseBalance":{"toLocal":{"4d176ad844c363bed59edf81962b008faa6194c3b3757ffcd26ba60f95716db2:0":0.1},"htlcs":{"94b70cec5a98d67d17c6e3de5c7697f8a6cab4f698df91e633ce35efa3574d71:1":0.03,"a844edd41ce8503864f3c95d89d850b177a09d7d35e950a7d27c14abb63adb13:3":0.06},"htlcsUnpublished":0.01},"remoteCloseBalance":{"toLocal":{},"htlcs":{},"htlcsUnpublished":0.0},"mutualCloseBalance":{"toLocal":{"7e3b012534afe0bb8d1f2f09c724b1a10a813ce704e5b9c217ccfdffffff0247:4":0.1}},"unknownCloseBalance":{"toLocal":0.0,"htlcs":0.0}},"waitForPublishFutureCommitment":0.0}}""" + JsonSerializers.serialization.write(gb)(JsonSerializers.formats) shouldBe """{"total":2.77,"onChain":{"total":2.15,"deeplyConfirmed":{"9fcd45bbaa09c60c991ac0425704163c3f3d2d683c789fa409455b9c97792692:3":1.4},"recentlyConfirmed":{"4d176ad844c363bed59edf81962b008faa6194c3b3757ffcd26ba60f95716db2:5":0.7},"unconfirmed":{"345b2b05ec046ffe0c14d3b61838c79980713ad1cf8ae7a45c172ce90c9c0b9f:1":0.05}},"offChain":{"waitForFundingConfirmed":0.0,"waitForChannelReady":0.0,"normal":{"toLocal":0.2,"htlcs":0.05},"shutdown":{"toLocal":0.0,"htlcs":0.0},"negotiating":{"toLocal":0.0,"htlcs":0.0},"closing":{"toLocal":0.3,"htlcs":0.07},"waitForPublishFutureCommitment":0.0}}""" } test("type hints") { @@ -383,43 +415,43 @@ class JsonSerializersSpec extends TestKitBaseClass with AnyFunSuiteLike with Mat test("TransactionWithInputInfo serializer") { // the input info is ignored when serializing to JSON - val dummyInputInfo = InputInfo(OutPoint(TxId(ByteVector32.Zeroes), 0), TxOut(Satoshi(0), Nil), Nil) + val dummyInputInfo = InputInfo(OutPoint(TxId(ByteVector32.Zeroes), 0), TxOut(Satoshi(0), Nil)) val htlcSuccessTx = Transaction.read("0200000001c8a8934fb38a44b969528252bc37be66ee166c7897c57384d1e561449e110c93010000006b483045022100dc6c50f445ed53d2fb41067fdcb25686fe79492d90e6e5db43235726ace247210220773d35228af0800c257970bee9cf75175d75217de09a8ecd83521befd040c4ca012102082b751372fe7e3b012534afe0bb8d1f2f09c724b1a10a813ce704e5b9c217ccfdffffff0247ba2300000000001976a914f97a7641228e6b17d4b0b08252ae75bd62a95fe788ace3de24000000000017a914a9fefd4b9a9282a1d7a17d2f14ac7d1eb88141d287f7d50800") - val htlcSuccessTxInfo = HtlcSuccessTx(dummyInputInfo, htlcSuccessTx, ByteVector32.One, 3, ConfirmationTarget.Absolute(BlockHeight(1105))) + val htlcSuccessTxInfo = UnsignedHtlcSuccessTx(dummyInputInfo, htlcSuccessTx, ByteVector32.One, 3, CltvExpiry(1105), ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) val htlcSuccessJson = s"""{ | "txid": "${htlcSuccessTx.txid.value.toHex}", | "tx": "0200000001c8a8934fb38a44b969528252bc37be66ee166c7897c57384d1e561449e110c93010000006b483045022100dc6c50f445ed53d2fb41067fdcb25686fe79492d90e6e5db43235726ace247210220773d35228af0800c257970bee9cf75175d75217de09a8ecd83521befd040c4ca012102082b751372fe7e3b012534afe0bb8d1f2f09c724b1a10a813ce704e5b9c217ccfdffffff0247ba2300000000001976a914f97a7641228e6b17d4b0b08252ae75bd62a95fe788ace3de24000000000017a914a9fefd4b9a9282a1d7a17d2f14ac7d1eb88141d287f7d50800", | "paymentHash": "0100000000000000000000000000000000000000000000000000000000000000", | "htlcId": 3, - | "confirmBeforeBlock": 1105 + | "htlcExpiry": 1105 |} """.stripMargin assertJsonEquals(JsonSerializers.serialization.write(htlcSuccessTxInfo)(JsonSerializers.formats), htlcSuccessJson) val claimHtlcTimeoutTx = Transaction.read("010000000110f01d4a4228ef959681feb1465c2010d0135be88fd598135b2e09d5413bf6f1000000006a473044022074658623424cebdac8290488b76f893cfb17765b7a3805e773e6770b7b17200102202892cfa9dda662d5eac394ba36fcfd1ea6c0b8bb3230ab96220731967bbdb90101210372d437866d9e4ead3d362b01b615d24cc0d5152c740d51e3c55fb53f6d335d82ffffffff01408b0700000000001976a914678db9a7caa2aca887af1177eda6f3d0f702df0d88ac00000000") - val claimHtlcTimeoutTxInfo = ClaimHtlcTimeoutTx(dummyInputInfo, claimHtlcTimeoutTx, 2, ConfirmationTarget.Absolute(BlockHeight(144))) + val claimHtlcTimeoutTxInfo = ClaimHtlcTimeoutTx(null, dummyInputInfo, claimHtlcTimeoutTx, ByteVector32.One, 2, CltvExpiry(144), ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) val claimHtlcTimeoutJson = s"""{ | "txid": "${claimHtlcTimeoutTx.txid.value.toHex}", | "tx": "010000000110f01d4a4228ef959681feb1465c2010d0135be88fd598135b2e09d5413bf6f1000000006a473044022074658623424cebdac8290488b76f893cfb17765b7a3805e773e6770b7b17200102202892cfa9dda662d5eac394ba36fcfd1ea6c0b8bb3230ab96220731967bbdb90101210372d437866d9e4ead3d362b01b615d24cc0d5152c740d51e3c55fb53f6d335d82ffffffff01408b0700000000001976a914678db9a7caa2aca887af1177eda6f3d0f702df0d88ac00000000", + | "paymentHash": "0100000000000000000000000000000000000000000000000000000000000000", | "htlcId": 2, - | "confirmBeforeBlock": 144 + | "htlcExpiry": 144 |} """.stripMargin assertJsonEquals(JsonSerializers.serialization.write(claimHtlcTimeoutTxInfo)(JsonSerializers.formats), claimHtlcTimeoutJson) val closingTx = Transaction.read("0100000001be43e9788523ed4de0b24a007a90009bc25e667ddac0e9ee83049be03e220138000000006b483045022100f74dd6ad3e6a00201d266a0ed860a6379c6e68b473970423f3fc8a15caa1ea0f022065b4852c9da230d9e036df743cb743601ca5229e1cb610efdd99769513f2a2260121020636de7755830fb4a3f136e97ecc6c58941611957ba0364f01beae164b945b2fffffffff0150f80c000000000017a9146809053148799a10480eada3d56d15edf4a648c88700000000") - val closingTxWithLocalOutput = ClosingTx(dummyInputInfo, closingTx, Some(OutputInfo(1, Satoshi(15000), hex"deadbeef"))) + val closingTxWithLocalOutput = ClosingTx(dummyInputInfo, closingTx, Some(0)) val closingTxWithLocalOutputJson = s"""{ | "txid": "${closingTx.txid.value.toHex}", | "tx": "0100000001be43e9788523ed4de0b24a007a90009bc25e667ddac0e9ee83049be03e220138000000006b483045022100f74dd6ad3e6a00201d266a0ed860a6379c6e68b473970423f3fc8a15caa1ea0f022065b4852c9da230d9e036df743cb743601ca5229e1cb610efdd99769513f2a2260121020636de7755830fb4a3f136e97ecc6c58941611957ba0364f01beae164b945b2fffffffff0150f80c000000000017a9146809053148799a10480eada3d56d15edf4a648c88700000000", | "toLocalOutput": { - | "index": 1, - | "amount": 15000, - | "publicKeyScript": "deadbeef" + | "amount": 850000, + | "publicKeyScript": "a9146809053148799a10480eada3d56d15edf4a648c887" | } |} """.stripMargin @@ -436,10 +468,10 @@ class JsonSerializersSpec extends TestKitBaseClass with AnyFunSuiteLike with Mat } test("type hints on channel data") { - val dataNormal = hex"0100220000000103af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d000094a9267a1f2b86e492dacf939afd1561a0e42ed248d1a09f711204c176fef1b0f80000001000000000000044c0000000008f0d1800000000000002710000000000000000000900064ff16001429acc00c77e6894e41e417ae712e557d7bba1c460000186b0200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000028a82039dc0e0b1d25905e44fdf6f8e89755a5e219685840d0bc1d28d3308f9628a358500000000000003e8ffffffffffffffff0000000000004e2000000000000003e80090001e0252a01d9d8b03db1a99b1a82223f7dd60ecffa69dbef47c06228fdea2fe0f021503c2a3cb942c336afbe484a00452f5b027378e2124c7ea452f05c26ab89591c18f03019eec506c69765552b397d706dd276f0718c82e0a49224fbaccfad75f81b2f502d3d560591f03da622f338b0988bbf0f612fafa4f1b22ed51398cf7a07c5f4192025d67e71808e128eb21c82881cef04a510067a50d0f61f2f455bcfb7e168ae7ad00000003028a820000000000000000010007fffd05aa5d10939a524d7b2ab225cdb41decfc129208c372d51ca1db705bb1b9831e53ee000000000000000000000000000493e0f81fd474bea478df0202b311c69e85dc6215f629491dd15ad0929faae2535abb00061b10000370b2714d2734e6b8cde085794dd7b41c8a9b6c03c1edd8e3db168ee7fce39493596e882b18b5b1b79c16400c6762b9856075821be6be9fab8f469f56820d8f341554400a8da7f1f1a8501902581d43b9fa6e5c68015716f718a2190b87fce41bf1b509aa61806394a42489d63c457fe4c79e7480eced1315edd731887e57704fc9102f50cb7f0d242d755cfd5a2172dbaf7f01b124861cc6d1dc804796bbb84165c805f0c0f3fb9ec97b74c2a694de56a9cf8d79d1a679260ee1169d78214b34c8a654ba22e59ddcd32beff4713de33549f035b342660405b0159a7a508e5691ef4805689140e72b8a0ef2e61be74dea5d0b8f5589e0e373cac2e2e1cc39b2121c05cf4122ad0f8b9af6fbf1de2ea26376c2650ccd306c13a7b64acbf2a3feed128754abe44658009e642768ae3d84f5e0fa5f7f360c2a1c76d26985817ae77b71fb59014a5483ebba9271cafa5e5d8031c569adeceb8bae6444e98d2522b28f6682109fc7d31cdb83ebd45e5d81e7f046df42345b49f470dbef9ed87709301d2c6131215d33a30b8d18e63e54a2aff85dd57672f8198bca6a67ee147c7d0ae649e5661ab6bf78a662fef9a164f1e332b9f16e6fb3d5769ddcbc1d1c07338d3394b9245d17618c2474e86c064fca4df00ad3a93dc051fd8c3328cde2a987798b0f22a21c90426700abeb1e6f38dffb485b5477ec44c690fa80e317b32a982fd3082253bba8595783290dbffee4fc9296ffdf16a8bf3154971bb720e78674969e9db2e0fbab9e9e13f24bc8b3af5e2f00f262f0da56de443f70398ab68f747d35370fcd8e1c0e130f7269e08f862b5a67f2c129be254df2358762ce3a947eb27d66450af51540e7721b47c8a5a86098ea64dad381f14e07aabbbc470949a99c07612add3ab4c575fe2e520bbe511a1a674aea37a44535c13ee3380f8f39bd230fc1481cd31912af36c6751e23c6f383cd37a8b13fa7df9f0c7e460739f2c6226638ee14f14d36366211cbc6a1e16b4856bf302a540aa9d9e833b1d59c510473096384c8b450f2f3f1dab9e614af822949d5cc93d76bc4d1a52891bc85f1981ef83161195ab7d8181ee4fb163bc6c685a10e87c7f4b15ed7d05833c230a4a5b63841fc65b959f0ff010e697f47c583f9b7fa9b389c0eff6614e47d85b83c483136f182be4c151d272f5d938b912a95e47d333e5de6a409ad271679a778a7eb3f169c71525302fac5d4575e2645c09763c2ef165736a7a726ca605038e2781404328790ffaacef2b9c2bf90122042cd571287bc4e3973da65fbd4e3da9e40e4347ca6eb4ef1ffef4e5a34be80425cae3e81533f7f2953f95fca53a22057a39125f5c76350fba7fc6c036838fb951d0aa8702e7f44c6f8a9cbce3b64fa8ddc2bb8c8b35d1e29a21beda6fdd332b31a749321455277231fd9d70ea4aded95053b395f88fa6916d126e1626fc0f1be6cd2a9538d17c498b40927f12b3bb40fa3e272e82cd2242b670afefa387470f4e6e0a1236028954c9e90311f486617187956a23b90b356d71e219e6dd055c2120771003a6c12769aa3ceacb9642bc01022731ca7a413b68ee7d1d5444f75dfa51a68b74a01ac85f6ceaf5e56987b9d67d6de896f5aafd25c78c413a6d4b5b03d571167524cd231ba13bd9f80fd7413faf21e8170cef0d08b242c5c38a2b0158da56e358ba0692f670d4611c7a3624b234adc30c5b7198e0afc941f5d13eae3a94ddffa652c784c34c582e04e948da91a5ac3038a9df38fd4f1733779f4f122ca2d7ff9d03bac9def35d9ee3a183161f8f2808d472b2e64581209359cea58ca7757164c666029982223877e2b14d2d537afb012f1ffc12cd083c16dfc64213c56f3d4d22b603d3dfab1d21e239d6fc1f9f153ed61f1ac91c29c85c16f4aa2985f84052f5a08d32bdd00fd05aa5d10939a524d7b2ab225cdb41decfc129208c372d51ca1db705bb1b9831e53ee00000000000000000000000002faf08011b29e60a38883a8d4434f17ca3d92161109f7ddd8799e64e86d6b8509babd1100061b100003b8c1771721551fa88af8fdde92909add6e5b8fa90a4b0484ee065ed3ed7c56e733a7fcd775d4ab92956c1328ffee9c195004c5dee5dd5b9d0f9034589e6769e72579d3d5837ad70785ec420b4a24c04d36668728c0d2534ff1feea9aac2410423fd79c7db9231ee7efd3d585646e378fe53d731d18f38d6356a970f5c3026edc849d49ff34e58dfb7548512461110088ac3aa800e10785029ba3b0e9ab7bde0f056939e4921792dba2f5c005135daf57e32cae06a9ccb1b4d321f3ba015e5b92def1ff1c200e56b3990d82570586bfae26e9398e17dc6c069f92d80e6dfedf6b2f24b1dc3cc9d63e684d861f40fdbf508d4ac34b7f10c57be2a9b0c5921f86869c29ada5394b8780d2488a4fca3cd98ddb0ff8ea4415a07caea436682835744e94d5cff6d3024a9525dbd697e499b7ef23062b18b225bfaa4c5bb07166f34ff7866ec8f0fbbc12f695c609692798364fa20bf7977e321deda3fe5510833494532fba94fc1f0dd14ec74f3e9fe8ee659634621b63d16d46a8958132c24bd82c516bdf9ae9515cebae42778e4de6be7047c31cf86c0df0306f7b6562e1f35be51e5e64cc6d9d4c010849e6ac7ddacaa4b7b6fb1d35aac815964090940e73a1193eece11c1c1d37e373ef58c5e2d690b6ed6338360af9906146da9db8329bd2786bbf92df10445ee093f0b1b2a640cc2daf003fa7141435ba1dd54f9cdbf5417fa7f539b255452852a85d2ce97ce5abed4980e7b409e283f97ccc9c01e104b55155f96ace6789f61c4661962d34fc5d7e6f5f5233180933b2fa7f7a5b074714645489f5221966160946b7bfbf0fe6733e6beb8af4457b9d36cde1200811009ec483a9d730ca980aa28f636942af5e89794a8edbc1b75d555ba134974374d0fe23d31c26566064eb9998d649bb2bf066bf710da50672f4e3ab4df843a0c8942bad0a071c237d4c1759eca37380919e36aec73284db202a32d3d1619f3e5b757b2df8b04bde567783dc8e465d996799782f1a1b8de9331681a35aa04edb427de87264c8ae9c397f29d3e8730db91256425a10b960a9de1a48d0d4186d617d2b69c87e2540f6570faff4ee1f6303d7d281434947abeaad83c86a4d25bef4de2bb3c6104aa0ceed7c8df039f4be6a42851a118adb1b8f98e02f6727b75d98541bab2ff24fb2f20342e86150c678941825409b62a844f44ca1ccdf0d9f7c2cf9b222fbed00bc92be0802fbfbbeefa71c8976cba8fc4aeb031480f434027b1cd593d08cbc14c2a360b736b06b5afb8da35f0be3818fff4275b8c830f5248a8b8edea1327454e1360bd90d4fa08e965f459b0b027e1180290cf762f813a31e8109f472d9657b03af737d1f7bd2e59441541a84ba818f1413c5cd1f8b9882e9188e0def9e44e2f4a7c710c893c7188ba86423f8ae86068d84e1832af548289e87c34d68b186df7e24ca5b051f8f5e4a44e2e7383ba2a09615b4147b34e86486731290ea67f3be24c13a9c5cc37f06555989b3f10c580a9cd2b416d0ee4210855c6833a25996761dfabb036f3893cff7db7e310baa8faa79f46e0ee43bf4dfd732eae7f44bad2e7c032b9c6d14947af6b0e37e5ec98372a622f716ffba0cde04b9d4508392dd154ddc34829412bfa604d4f00e4b10a553587343ef5c0944165e7ee1e34387b09c147ecba943cf36dbc4269efe50ec3a5a3075c43be9651d6db6acb9f657476952b78c990557f05935247a71077373ec436ec586def177448f8859ba096b7a838e5b4ce7a463f9082f705c26d99936eb1be584ea9b58a44b9b4faa07fd8247fa66cf4529d1b8cdb92ed7bd96bf0968db4376489c7d46f0f27d58ac884c29736502953723ef1ab41e19c7041d3e0e9091d7de2e3904d032de02292edb1225a672ab438d3c65f7921c06a9f181f8ffda4ac524d0e000fd05aa5d10939a524d7b2ab225cdb41decfc129208c372d51ca1db705bb1b9831e53ee000000000000000200000000000f4240657c0bd97e795218aa623b27cf9a71764379c4762fcee8993aa0b1ab1e32194a00061b100002c0ec5d388a8c78491bbd870faa2c46e4282e11796123b69c2792e5806748ef068397f0212a33e3f79162ba4ea247ccec410db039dd323af48ac27bd0ba77ecb870c587477d4543d9a29c53fc02bc98d7cd0144c7abf80b999c22b42a28ae8d625478ca304001f9a49782ec970031e673c76e4e27357a321729d6df38d1d88dcbf764c69eda3baac9739bed637010f44638cfe1deecc56b76f6e02d1d0c3f104462b9ffa5f20de4cf092d86a5bb35d5f62b0fb1c983a2c06df17c9ffc809c83e4b4335f5903fab536fbd9719847bb063541ecbe05c12ef8d058b3547faca054e3d662250f1cc1f925dd71297abc25fe37ab33a086759fac76208a64552f84d2e4d84daccdc3aa2bbd2c2f922bf262596742dfe034529d1ead2975dd3d197ab0e2e1c75c8b8f160ca6077638022d4afbd107979949cf342cb399347f3990029f0db6d9ac0c569d61d42539371f9a7ff59e9c83ff97d15bf0eeb254ae58fb7b1f9d8710c546ac8a227930c66ac841bc4f475229e5cadd14ba5a01e6b2da99c55861a08e2100e62c4499d30003fe30ddaa347d7a27c2158d3787d58fe51ae57d797bbef7f900508d1580df3e5233f0887567fba1faa918c246d2ec5c3b7aa022cb8a652d00b4d719e312482f57655eee80a90cdc73151fd7ab9c5367793d60c6088fab98f0547d7f547e10db202a25e027a5cd0abc41bb0e3ef563c0a6d469a702b2a26f0e8b4fddb845a16a5f06b9dee33c3adb31430c94942c5023179d3e4441948a332069a1c3b69dca65f05a43452e42fd28a2f7e6344f98ab9a7e4eece3c1709be1f7bb620f8b6c45989a8bccad39a4bf40e8215183d1449196f1f9fc17de778b616856152e6e6145a1b7a3d7f226becaea5ebe34aa4bd06e60f0fed207bfd21f5663bfadd37dd722437bcd46a26fb4e19d062574a81bcd817eecbc1914a5878809128961acdd73113ae9c51070ff4494e16d81ccec777eeb513da82bf43d4884812b26546b4370dc315793271b069f60f4285f648cf122ed8b22b0c7a27e94ccd59a273eb774c109e19980e146850de95f82cdd8aa0e82022672024c917b281422d284df0ee0bdeb3d4ac56b4ca675ebdb835c17b6a822d79ae7310f4aa41d80ac61c5e45c1c0e1d64542622a31091a9f87c335e86d964dd85a951d7c9bf41c9f2b1a9bb8424d7d1b26413da8034182fa42d2b1cd1f8745482c49d8348d19c72cf5a02bd28e4cba82128af8bf5d9c1215c4f543ef4d185f100f8d803dfa29c300c072e44ad9542b82fb1380d55c15c9a4b4398876e2450b90b49990746f339abca8cc8a462b62329a128758ce0e46b5f998af1bb485a3044bd125424eb5c623afd2a11befe4ec544eafe275ed1ad82b940dad5e9a9710d48562e51b296ba81f2d70593685ba0e3f3b25089a187e61d5675dd481aa99620276cb0a841a3c4df201a929287b1127270c5d25d06fb286dae1a9a5a5cdb60003f0c30d2021074bf252e550685f7b51a087a77b0871e883104e55f898aa5bc4cf8538c293253a737556d7f220e15b90cf0eda7d5f2172372e3c50c12cc588f312da37191b5038e944825044b130bff281ecd47a4252a1411ab7a9305c2b37e9facd435e9c434de37641498f8e4bfa7b42966da29c84200aca87ea1c3b00db54906b340e524a7dc4a15403bb82bc24517cb91026096bdf18f5f7ae5640ed6de1f0c5d184813d6b9d244b32b58e9ff524741a39383eec3a530d60db13deb26e3523a725f0599b671b625c07002704fb600b77318417d2527537359d122e22a1f7581eaebdc19e65ba50bdda18ae08e9a8694fcb0ff1a2cd98d910dbcd52064c15a4282d67b278c72a0fdbf228abf6b519dd28ac21c57d1da4bb7ad5b5ab10da6b83132df1da79ccfc77fb45598bbd91ef5ab96d8a2ee148639a562debafffd05aa5d10939a524d7b2ab225cdb41decfc129208c372d51ca1db705bb1b9831e53ee00000000000000010000000002faf080ab009f4d0b317a141e5b3f865599676f895834400b66dd09d33ad27163ca176f00061b1000028fe56ab181f454372c939ca0516f5782b26cec2010885c70e55c41a1e43de3af6635d2ed0bed90837cefa9f1805f4808e8092a4d44efe5fb616ff7678487a460b367134fcc728ba273fb1d22718bc6a95e47a120a6d952c7c2cb8f38e59c2a4efa63977cced7e4b8f46e4d47d29098a73beee807c3337c4acddcbb32b78eddbd124b2f33ec6cd8bdd364aa4ad2c10eb69dab808fe5f5f0aec19750e51ac65a8746f345c05d4b8823ffbeadd6200ad01449c39a008fbe117a8ee904445488811336d0c439419fa4f285f9f62a34f10b076c99c0092968e3cc9fa656016b6da049bd56b910d7a9356e76d24e746b280f0275ec9e9bace82d852bf0a137ea02d4cbd3b68450bfb593564d8c20953bb758890a55a8c381a4b3303b61ec26a56111361bf7262b3f6f2503aad06758130d86ea607cdbba53415aaf253430d92fdd81c685ab39233e94654e6508eec1347747e2df2862169382aef6f99dd78b50629c5d98b1fcc73e865679d862b42f8e9d54ef6288ed2c3f2713f0fa4db538cd3e70ec1a30cd65dbf873f581b30892acedacd39b5f0aa774d1f3f77d8fd11ed628bcd02ac33f89123595aa455ec54a07e93e26f94338fedd8bb84094a0add52f912ed5f9019e3a28d90d251cc6ed7ffd35254dcadd9f1e9b28eb0e06fd4fe961d60cb690a7757f475c08aef07c2e54668121540a42a9c779623709a2124629e8c4bd4021763979647f625b360a4559dfd3f57798dfe5d36e9d902904af3ed67d8f4b0894538c7718f5160d211cec27375a7e6a2ec42f2c8fcd1c953b7b8379d42439a2c6b921a66d5102ceb6bd6bc20b17098e69a0a4f708b42520e4792474c3d115a12c83ef60ac6e69d8842c5981e9a6d178efa352e73e4a34bed4fb590dbeecb259617668e6ffb9f955297f26e3a6a3b95d9617529a61f08666ca1069d2ee1876337d3e786244c5bb45a8236577184584cf3018118d7e4e78973ee510b6773bd922797e580cd240dea3ca31892d23c1e6e4fa92f1a01da8ea40044f5613a9429ebe7906f79b32636204d025115810b376d4c6436da136b96c7c10649e3290caecd6ca14d995a817e3725fee7e621c5366f80c752e50aeffee1af3361924f31cbb1cb44731d19963ff30127ca2363ce15e50948be14c43400737ee8910ed06027599da74b06e77eb82ac523cc031c57c02dd82dbc0d53629d072615c92034cf829e7a5d4437b1f58e2bd4b16993e1e1b05c26ed8d695351db11d21df36a7f5811ef5fe001ab1e1c6ce9d2b69b6ac3af8087e6666317f75b645e3b1caefac0eb65327fcb9fa62be341c99f191cc869e48dbc8fee3e42d4393cbc6505c880dd6739a69be4f7ef3de306480a7a51f413d310926f252ea96a0c772d8b8e94e7d6cedbfbdb21fae2ffc379eb17c2680fa2bc56a8726c93e7bf2d446221ce95e49da93d29bec8e53ddcfd262c33d556c2b8921c3de93236408b462d28612d3343fbb9cc538b1e6b33c341c3b91dd41f936931e61f146fd00aee1c5c0de97b47cf7efce889012e1c22dd8faf0fe155f4e9930c27941d8b0907502a835bfffff801f6835de69ad33e95232f773219eec0e2374c421230f323257dbc91629c4ca8a61584f737b827fe8e8f5b69b88a7b64b362f8142b043f08ea82c4a0ab7c4e0b9805533e806f90597095242ff64f314801fb7ad838e98e1859b2c05c9b027ae5a4baf780d15977bc1492dee9b14b1cb0fb3243eb2304919486fcde89a3cf35a64e31b1698e35fbb8528a73526a19189d406272b8becec94379f69372afa99d06bb4f34df72e1c3b49557855ae8ac265160bdf48ab34cde30d2665891cfeda24adcc657d851431f38953f917f1a111f023d2c71845ccf25a562d2450bf8b4986b64ae4fd09e5a8ab9610d11fd68e9d570b3a467780236de974c7fffd05aa5d10939a524d7b2ab225cdb41decfc129208c372d51ca1db705bb1b9831e53ee000000000000000200000000003d0900559bb949364ed92a64f449cd6ee3eaba2e607595d3cecb36b06b145baaa69bc000061b100002860fd058656f185036e81a64fe19034d29223d3620bdf4bf2ff3c1def9c6bdd70c2cda7f660e1b202672741bc3258b04fb755fbbb1350261e7992bce930e256bd5d8d4fde61365b7b24904b788ef2040fdd6eb87f97c2d2f29eab4291d9a28b5da306f28b98d01d93517b203a028199d423a3545aa17522c63247f73af7b63335b0b48e4b875c69b42f4cb1573bb3e5fd68837f90c50b0161f067a9eafd9a0790e53ed5053ce60ffff410e4b16a4b7bc5a52c57e78ef266100c9f79753f81878c08e5dbd4d80c6e46a339578d8ac8c572df77ef614800cedc460c06878c0da97908067729ed35e3afe919071724f89ab736f5791a9c9b5d422136332213434c836e2ceb9fce0e2e96a9a6d7befe8c132867d5fafea1a7809ddd6b3a89c8ef6ea83028d3e2cca00f1bc6e12ea8b67e91a98acaa2edebaf6dd3a18c655b6b1fbbff5c641f8002780758d05f1f39c9470a124a5add314abd2262142733120747cda2f1d9eb90d68ecb9c7fbab23d73a35f2a20a2a365de6cd678d53bd5bd9bd518333d04e8e678b5d08f028982dad08c80be7d8fbb0638dd814232224c687f8321baf96ed8b39a1e9ab52dfd69d8eed79ac3f5a2c480a585bff038c92b367743317b937d969cdd533ae1d797a789ff7994f86a0d6cae470b64ebddbc478573af347a110dd1feaaeb4779441ec439cfdbafaba870105efd86b9d85a4df7ddb9b09f5b6b4144cd1fad5932df37ebf19a62648659fc1969142310a5cc9b4d0c48ba6bb0f863ed53a0b75fe1ee6515a46993f95be2e34166408b54a43e55c4802b37ac902fb4c8367ce38990d07ed3104d0728d327d3b9de6452b520f9af534505885788109ec78c1176ca0864d28422e826cc83f821b7eaf028d6a7e350b3037d0fe58d1d4e18113c8f61913932e71c0f334402534d8663f15445f900fb9dc6b3a93223868167be26fcbd70c0459eee37f81fd539c319eb0b04bd478b94b5f4cd23b4d496c2bdd6e8a154fd76c4ecbdf7647fe9e7be88c6a3a8e7696e2e596dfc25ba798db6ca331d135e9ce7c0aab9721d3f70ca53354f96ecd028236259b9b0d9e0bbf73c8e841b1d4276214f7be8feb525c91d39910b0e091997a2b89e945806e93cd325cb51463b0729f1a519334038cba09653799ef533a49e812e86b81af7e5099a02ca11c2b17dfc8b9e51a57a20546f2c92826676ebcb4f64fe7cc77424388dfec7199179cb125bb4613c8bf05edc4173987d7d5ae0fcbfa08a1e5ea2d6b01406d740b49c5b1a68da585549590c3ec13479efa3136c5ade68057fe173ace55593ceca8440372b03f332969866d1bfbce3fe9dd907d27593b8b2ad25eb4b12afd0f3abe7931ba7789c84a3ed65a03df06d998a9956043a4de786c359bdd58f0b9e5cfc32bc709b626ce8e63f3997a0e9f784f6b94e342b4710553e805cc8399191254189058ec75a15556467b2456d9b38a7e4d15cf59727ad2c32f0daaef1748be04311ff484479eb31f7eff9e32b816fa40f40430e801c15e931294b39ec4d6c93e8130fb4a1b53ca6471886a33b10b46d6973d02bbe3b39345e121473ea4ba41f3cfced07c3a613393e38b8ca01c353ccab96128093bb5fd000978b11ae6f7c1fedf48c3682119d1b44629d02ea3800d2e247ef1e78e527e5d574b4dd144e1fe1b0c6551442d419baba300258657792947443747b29b6fb9417d3c57536de6aa379c02f3addbd2066554189ca817c7f57331c972c30e3ed06fc7e521b8e2bb57c023dff9816f260a153f1ffd55df2a2fae568f3dc734d62ab47a77c54772b5e4cf2758c912cb473ec372173d9eda0c58b103ab12d11dcb976a40816ecfa8b9c9384f6415017555933652e77ae20e14ea9ac25b5811fc03364c4883bb78c82e29f8a8892415ff652b782642bf6036b3e2200e00fd05aa5d10939a524d7b2ab225cdb41decfc129208c372d51ca1db705bb1b9831e53ee000000000000000100000000007a1200ffb841fb291ecf09ae7b5dcec1feeb45ba196ca02d6b9e50ae2b3dcd9ca5d72500061b100003acd0a3acb9b4f9540678dc9324480bb3d4f54e5e007739c9c1d600bff75dfbbd0191e75c0a2d810a6ec5b03d02cffbf1a66123c87790e66eee8d416caf82e7ea7090a62fe14276fa88af32ab9793f7a100de5525eedf71967c13c8d361246d98b78cc74383e714f88899f34407644c14874046b3b722186015c07b8db042db955c91cb14abbf3cfa646aea81ad15bf67108a763539c64c5a8e8115d46e063e956671e8ea8d8fd638a6414d71e9b475ebe070da7faf75a898f29048ab5a2b6c7e3a72a178b8e470e8375f539ebf6284d15486c5a8774d46ea164ba2b62181f47623fd987ff5958550c962a193638679b79fcd477fdf2a09c0fa879bb22cb493fbaed27518f5cb265741535b4ab14246077ca18f11ece7aaa0e01ec5bf02c3c3b541ea08bf254df123079c1538e266dcd3161bf1b9ed41d873f1491906e1459ba51ac9dd95e783598d3c356e0cc5b98c2a96b148f55f102a9810181eedd46cd00b445d861baeeac46eba469435aa4ddc877bd68b53f4d005aa2566d356cc344aafcdc86abd774ea28cc838d2fdc541c4b6da494a96e128b8c2abab4b21b3ae2646cdcc3528ef6fd8587b3a0636ead67a62309fb003afdc14177d329b062622313e9dee912847763c68678df663a39b89c69efdb6d916d5754534bdca9030955cbcbae6fb7ff1df6282175cd37a30a904418b976af05809f0e0e7e4b4e2ec018f1e9c6bcb" ++ hex"e7a7822c8699669946f5e684671d63e68cc7c9cca2963945dc21c52232e6f83b1875b2bed7c80c37371a480a2e5255d49d390c3b2adfc695036ed91371cda7d79bdfbae464581f0b32942f03826aca17ab9da6ade4a778d310ec3da17fc3af426d21b347aee7c2db7b5e188e35714dc514e3a1c100e8595c9e0e4399ad796021976f077e5733ea535cc6daec2e371853dcb715fc366ea7d6b9a5b3509dccf5c2e1225e3a51de9f5bb9b6586b282a0b27a9ae7ae8f2be14ec677670241e384b462eecfde68957839b1327c9e5c622c0f67cdaf3845ddbe6f754401d720d6b6d5c061dc906bfa70fb76e1168c6ac1a25cabe8873c3c1e540ae44ae631a2638accd7951f368442dba7b38d0662ccb0140d1e4ca23f51de731a6f5adcf816c3235359afd607e58948da29a5f06c96b4312aee7d35ed4c2c811a58c5a196ac2f377d653d51cfccb5213c928955ac880b5fb1b91e88a52d5c217cbf78e071275fe626c230fded548b0f1667af1309149ff74c5542119d4e269fdc1b241d9f53e02e38e015b7c5c2d2ee623bcb4167e37edafddc7fadd642c20f81b454db1a3b578d527f124dbd1f3d99fdd1590256ae4e47c2e8b3bfe8708a0d5506d6ce8b130ce6b70028161454a5065e9925d75c0095dc24ba789489fa1e9236e25330ca1a45e61224ee027664f6589028a240961aff09187fb719ff3477b56427189b7b3c790b4031f6539c5e3a8a5d7fd99c2534ed1646920a43e7315bba98d59c51b337ba7a1b038006bb574df46830f96a5685e07ee8a0a41e712810faacfb231c67e69d0fc24b98d782c70e15524d5d2dcf4e64b4e26772dfa7067f6dc7ffc8b06e6ef3ecb13f927d466e0cd3a7ea09aaed90b7810bbfdcd8a1274bbe78a453ffff11ccee62059ec25955b34a1a1cdf8a3e506d99dcbadf16032646117556ad71cd93ceeec42be0350a6c9f194fea783558c42a56d034dcaf6fb1b28037362c7c6e2446bcda71d0a88adda3144589447ef13cb85a4d2cd16ef444097bc03e32c3b1a055e952f7ac87078b04deb900375a16dbd382ea4375ddc0a9645deca620590337e803ea8b41337f3f4e4030119a2337424dbea3d21214063ce853843dd4e6df94e3dc3bbb36d89d9eb8e15e52d0699bc6ad1de9d12afc95c8785d63756576d357126e13b25502d542774f6e5d2fdb559d52698d08600fd05aa5d10939a524d7b2ab225cdb41decfc129208c372d51ca1db705bb1b9831e53ee0000000000000003000000000007a120bf5899359b258acdec0ea500e437267aa6ba18e8509e15df74518f6ba7aa348f00061b1000036d768d1d63f1c9f09c252b5b48cf4d2db3f4935150c551391b37727723168982d7e58ce2a769313e461a0f3791a8c0208a769f5bdf4d57fbd0a9de104a0b1bbc1ab25ebbf87d1bc86dbbd42fd0dc0ff18a7ebfdd692c7dc3b95d095052540ce7801f3e25cbbcfd9dab857bc39624f59facbfacef5ab1e1e1f889b85f3b1f2a580cd660b73aa662fb15487722bd3c3f93d272725289136f2ee0fae4efe1afea25e6d4fc8334a47ad62d22be06605a15538dbd2a81311c4489b91d1cb143743e4570a6345c4f035c060aaf287ef66e7ebbe7b9037c10b66e087827478fdf76a02d25fe90e0f8228c1edfcb12eee3dd4e505a6c5a7bf2f5954ebb5560cd8c7f8b8f3f4ddca41a48a0d6c0e1092dcadc853752f459486bed349213cc15044585255842717ab70a3de3f0eedeecca10112c85a8bc248dc66c883a62288b49588d9fcc048c51081e94d65bd4731e7d71fba13693b82d2831d3bea370918aa5ddf1cc4f0d5015bb8dfb951ab13fbb9d26d5cb83dc980fc36f1712d616ce24d2c530253320f4f322f093a605ea426c577544f2983efc80be56791f443652c2233039a68f966c2f0b6351068616755dd2036b6226244d394a2b5b59160217603149901e8abe19a2bf404f384c2ed7fb0c5e470ee5ee8561f58f66bda729c2c8816853ad2357a009e537efcb4a28e845ca616be917b15aa6b8eb280bfcb321ea62fff21168b8ed54d58ccfdeee0e7752bfc0f02549d76615c85dd1e152a85ee931b34f436439e2233740328ba504c49f9764e1dca645ebf6a1377310ab53b68b4d0a6e6c952068249b86c29061725035db8d294ab9c56901485814735aa2a8d6987b1a19ced65a332f97751c4cd8a27093851f7775e5314078c04d254754d976bed2dbd2e6ecda62e9a0c7fd95299b4b13a54c9498d384210fb42d3b6bc5d8f0d42e42879f86c21eb7c5c6d1bffdd598b8f3cfcb75df159f1125a65f960637c62c7c5632d73b7b4b0544082008ede22d87e79e20eb08be0817650fefcd111de48ba2be02a7b080275c991a0ee4445dab89312644c7cf4101895e2dbcaad7d87e8e3b13e62751861b204a7e6f5a476eab0817c294d59aa0247903077d4cbe4a98e7984d2b04623d2b2ef4c650b43db15541ede229c12c045529b5c77993eb6acbdc28d812a486b5957fb996731980555bdd59ad824a882ebe1a77cbe6b9035f1c69dd01b2a27a47be5febfa65c721354e70071b07db4ebc2f01d143587c1b32a5337dd010d2a76a7773f4a7c665b4cfe4a61b103b1c319d85e007eb99b52400cd8776697e1d6118197655bf7bd0a5e7f4594bf36a2706128d5f5c3ee166b586c4d515611f4597a4c1088c1853a5959f73830cb973ea922d6211ee7d9b1d67b1025486f8f3c72a517d0d48d9a57d64c0f48e513c3b09e14ce91b515a87f3035ab55d241ccb12108dd299a362af26af96ada920202dfe26d456065717a85e6bbec540637059d82480f6c917a11434a9be5fee5ba33a8552b7b0e59f123991525e1dc14bfdfd109625b2df477bce565045375dacf6ffe99081914fc9f64df7ef8eb26801ce01be083555fd2f8a338a33c07f484b01a310ee420f5c932cfc5a8f6d32a20c3915a188833ac5775500c5d2b73e4ea595512e869f91f83de8e048c804ec8cd6de45b7ad264bcfb3c235325d3c09673a2a94e60736ceada065cefb1e355bd35bd4b56ae513343e85a3d840c2211da2d1941112318f97f825e1714f74fc2d33430196cca2423f43641b0d75b4b1a1a1ef0f07af31fda06220a2628aaab9f303cb4fe6d73fa9b39e8f6083af3bbca1becdd646afbd888c560bbf7cf4fcf5c02f712726eb23b03fe5c290df63a85ed1026cee07ab0d2bb868aa4dd6594252cb75ad5d11d9c53c5238047ea3d311c1443d478dd283a260270400002710000000002c2322200000000008af34a0245d10939a524d7b2ab225cdb41decfc129208c372d51ca1db705bb1b9831e53ee000000002b40420f0000000000220020c57347ca54a9e6f279f6a1f1e50f19b48289c3baccdd356fb03033ae7ccb6e444752210252a01d9d8b03db1a99b1a82223f7dd60ecffa69dbef47c06228fdea2fe0f0215210322a747c1d7f77fc7577a689618bbeadf28b941412404ac5e216d684a32d57a8e52aefd01d9020000000001015d10939a524d7b2ab225cdb41decfc129208c372d51ca1db705bb1b9831e53ee0000000000e136df8005401f00000000000022002019cccc41d4120d54831ad41b091b1e52b8902711365f1cbd9a44a7b5a3dc22b450c3000000000000220020653fa8ccacaea967b6bbfe9411c3f812cb44c4a18d4f6806fd81512009f706e750c3000000000000220020c9c8b5e68eea1a6f32394124873d68c66b1cab8ffe657c852696e1a1597783d924390200000000001600141fa232bbb376f103dd015631dc8d6f4f9d306122241c0b00000000002200207121fb3dfd089e1a7b7cf0934e4cc42e99678c7db90927a358985a6471d0cffc040046304302204b7524262f7aa8fbf4e9fadf476ba42b4202617b16e4d1e1ac7c9db1adb8a126021f59622119b96d12a66c6f18f12b5c04764c576f9ccb4216049ddd6e719cef9201473044022003f76c8bbfc91e5323ddc1d7a95e8a3c7707de898e6a50fd5e6562b8412e32fb022010b2bb7edcf519a9451b3883832a39a0f9008903b52fb2db90e6e371bf20307b014752210252a01d9d8b03db1a99b1a82223f7dd60ecffa69dbef47c06228fdea2fe0f0215210322a747c1d7f77fc7577a689618bbeadf28b941412404ac5e216d684a32d57a8e52aec34a86200003000324489133c313ebc91202d43a19b2034b1b2df6ccc257712bb0c38cd1d3141e42ac000000002b401f00000000000022002019cccc41d4120d54831ad41b091b1e52b8902711365f1cbd9a44a7b5a3dc22b48576a91423ca5b93a48d00f645081be0479fa799c70731a28763ac672102897afb6799af1c1af49fe4c96800ee1cea77553c0b5ae9b8cca5347238cf5be77c820120876475527c21035a26304d46e27993c218a0c644a613ca87630c8f0e1db5f112a01c3af26cdb8f52ae67a914d3260c3a0710948e34d708a99ff708f9c257ac5288ac68685e0200000001489133c313ebc91202d43a19b2034b1b2df6ccc257712bb0c38cd1d3141e42ac000000000000000000015a050000000000002200207121fb3dfd089e1a7b7cf0934e4cc42e99678c7db90927a358985a6471d0cffc101b060040ac4bb46e1b736a6fc76a4d86b24df1e9f9f91013025209ba64daf333863a2bde59a51a7af931bf0b07d6154daa30ed3a9c8c33d173c75765b46674a4e249bb734061aa4d418884ef272ea2bd65d745c42b5bd5f6781b7f876402b10eb505c60d086ddf3e108528e65f4712503114c24a20c8fab7867761677401defa780c4574f5000324489133c313ebc91202d43a19b2034b1b2df6ccc257712bb0c38cd1d3141e42ac010000002b50c3000000000000220020653fa8ccacaea967b6bbfe9411c3f812cb44c4a18d4f6806fd81512009f706e78576a91423ca5b93a48d00f645081be0479fa799c70731a28763ac672102897afb6799af1c1af49fe4c96800ee1cea77553c0b5ae9b8cca5347238cf5be77c820120876475527c21035a26304d46e27993c218a0c644a613ca87630c8f0e1db5f112a01c3af26cdb8f52ae67a914caf9c0d315f1ca821720d6911e090095585cee2788ac68685e0200000001489133c313ebc91202d43a19b2034b1b2df6ccc257712bb0c38cd1d3141e42ac010000000000000000016aa90000000000002200207121fb3dfd089e1a7b7cf0934e4cc42e99678c7db90927a358985a6471d0cffc101b060040dc5ef2218b256e0db9981ab591cf830819b62ae2068b6c4d19a85ba03153ff39316c14c1570a8fdbc2765b38a16c74e76ec7190f47aa3754e08c770d8c7e918640678fd252d77d34b3ecefff1fa369e92dad0b4a2632efe13974e538e13e14780c4c6c21838a7ead4878c9836405e45333651de6cbc012af4d6c1bda4a0039d5aa000224489133c313ebc91202d43a19b2034b1b2df6ccc257712bb0c38cd1d3141e42ac020000002b50c3000000000000220020c9c8b5e68eea1a6f32394124873d68c66b1cab8ffe657c852696e1a1597783d98b76a91423ca5b93a48d00f645081be0479fa799c70731a28763ac672102897afb6799af1c1af49fe4c96800ee1cea77553c0b5ae9b8cca5347238cf5be77c8201208763a9149bd34d5a8ea2d91b8ac4d1fb4ce79bcfd481c1f088527c21035a26304d46e27993c218a0c644a613ca87630c8f0e1db5f112a01c3af26cdb8f52ae677503101b06b175ac68685e0200000001489133c313ebc91202d43a19b2034b1b2df6ccc257712bb0c38cd1d3141e42ac02000000000000000001daa70000000000002200207121fb3dfd089e1a7b7cf0934e4cc42e99678c7db90927a358985a6471d0cffc00000000ab009f4d0b317a141e5b3f865599676f895834400b66dd09d33ad27163ca176f401e7b146982a595ed1a95e1225312dce24f45237477efca22c759c9cb16c4a6bc1c9e8834a18eb0c6c389a14ea03fb7062f788003c5c58e738d78e28e004014a64013f1f51471ecdb32897a5f604a254334f7f5fa26de5fc63b96c942db2196ae9e43b34f00054e0c58a061d5ae5f4938b5d449b8f9371edd76efdfcce758a102c500000000000000010004fffd05aa5d10939a524d7b2ab225cdb41decfc129208c372d51ca1db705bb1b9831e53ee00000000000000000000000002faf08011b29e60a38883a8d4434f17ca3d92161109f7ddd8799e64e86d6b8509babd1100061b100003b8c1771721551fa88af8fdde92909add6e5b8fa90a4b0484ee065ed3ed7c56e733a7fcd775d4ab92956c1328ffee9c195004c5dee5dd5b9d0f9034589e6769e72579d3d5837ad70785ec420b4a24c04d36668728c0d2534ff1feea9aac2410423fd79c7db9231ee7efd3d585646e378fe53d731d18f38d6356a970f5c3026edc849d49ff34e58dfb7548512461110088ac3aa800e10785029ba3b0e9ab7bde0f056939e4921792dba2f5c005135daf57e32cae06a9ccb1b4d321f3ba015e5b92def1ff1c200e56b3990d82570586bfae26e9398e17dc6c069f92d80e6dfedf6b2f24b1dc3cc9d63e684d861f40fdbf508d4ac34b7f10c57be2a9b0c5921f86869c29ada5394b8780d2488a4fca3cd98ddb0ff8ea4415a07caea436682835744e94d5cff6d3024a9525dbd697e499b7ef23062b18b225bfaa4c5bb07166f34ff7866ec8f0fbbc12f695c609692798364fa20bf7977e321deda3fe5510833494532fba94fc1f0dd14ec74f3e9fe8ee659634621b63d16d46a8958132c24bd82c516bdf9ae9515cebae42778e4de6be7047c31cf86c0df0306f7b6562e1f35be51e5e64cc6d9d4c010849e6ac7ddacaa4b7b6fb1d35aac815964090940e73a1193eece11c1c1d37e373ef58c5e2d690b6ed6338360af9906146da9db8329bd2786bbf92df10445ee093f0b1b2a640cc2daf003fa7141435ba1dd54f9cdbf5417fa7f539b255452852a85d2ce97ce5abed4980e7b409e283f97ccc9c01e104b55155f96ace6789f61c4661962d34fc5d7e6f5f5233180933b2fa7f7a5b074714645489f5221966160946b7bfbf0fe6733e6beb8af4457b9d36cde1200811009ec483a9d730ca980aa28f636942af5e89794a8edbc1b75d555ba134974374d0fe23d31c26566064eb9998d649bb2bf066bf710da50672f4e3ab4df843a0c8942bad0a071c237d4c1759eca37380919e36aec73284db202a32d3d1619f3e5b757b2df8b04bde567783dc8e465d996799782f1a1b8de9331681a35aa04edb427de87264c8ae9c397f29d3e8730db91256425a10b960a9de1a48d0d4186d617d2b69c87e2540f6570faff4ee1f6303d7d281434947abeaad83c86a4d25bef4de2bb3c6104aa0ceed7c8df039f4be6a42851a118adb1b8f98e02f6727b75d98541bab2ff24fb2f20342e86150c678941825409b62a844f44ca1ccdf0d9f7c2cf9b222fbed00bc92be0802fbfbbeefa71c8976cba8fc4aeb031480f434027b1cd593d08cbc14c2a360b736b06b5afb8da35f0be3818fff4275b8c830f5248a8b8edea1327454e1360bd90d4fa08e965f459b0b027e1180290cf762f813a31e8109f472d9657b03af737d1f7bd2e59441541a84ba818f1413c5cd1f8b9882e9188e0def9e44e2f4a7c710c893c7188ba86423f8ae86068d84e1832af548289e87c34d68b186df7e24ca5b051f8f5e4a44e2e7383ba2a09615b4147b34e86486731290ea67f3be24c13a9c5cc37f06555989b3f10c580a9cd2b416d0ee4210855c6833a25996761dfabb036f3893cff7db7e310baa8faa79f46e0ee43bf4dfd732eae7f44bad2e7c032b9c6d14947af6b0e37e5ec98372a622f716ffba0cde04b9d4508392dd154ddc34829412bfa604d4f00e4b10a553587343ef5c0944165e7ee1e34387b09c147ecba943cf36dbc4269efe50ec3a5a3075c43be9651d6db6acb9f657476952b78c990557f05935247a71077373ec436ec586def177448f8859ba096b7a838e5b4ce7a463f9082f705c26d99936eb1be584ea9b58a44b9b4faa07fd8247fa66cf4529d1b8cdb92ed7bd96bf0968db4376489c7d46f0f27d58ac884c29736502953723ef1ab41e19c7041d3e0e9091d7de2e3904d032de02292edb1225a672ab438d3c65f7921c06a9f181f8ffda4ac524d0e0fffd05aa5d10939a524d7b2ab225cdb41decfc129208c372d51ca1db705bb1b9831e53ee000000000000000100000000007a1200ffb841fb291ecf09ae7b5dcec1feeb45ba196ca02d6b9e50ae2b3dcd9ca5d72500061b100003acd0a3acb9b4f9540678dc9324480bb3d4f54e5e007739c9c1d600bff75dfbbd0191e75c0a2d810a6ec5b03d02cffbf1a66123c87790e66eee8d416caf82e7ea7090a62fe14276fa88af32ab9793f7a100de5525eedf71967c13c8d361246d98b78cc74383e714f88899f34407644c14874046b3b722186015c07b8db042db955c91cb14abbf3cfa646aea81ad15bf67108a763539c64c5a8e8115d46e063e956671e8ea8d8fd638a6414d71e9b475ebe070da7faf75a898f29048ab5a2b6c7e3a72a178b8e470e8375f539ebf6284d15486c5a8774d46ea164ba2b62181f47623fd987ff5958550c962a193638679b79fcd477fdf2a09c0fa879bb22cb493fbaed27518f5cb265741535b4ab14246077ca18f11ece7aaa0e01ec5bf02c3c3b541ea08bf254df123079c1538e266dcd3161bf1b9ed41d873f1491906e1459ba51ac9dd95e783598d3c356e0cc5b98c2a96b148f55f102a9810181eedd46cd00b445d861baeeac46eba469435aa4ddc877bd68b53f4d005aa2566d356cc344aafcdc86abd774ea28cc838d2fdc541c4b6da494a96e128b8c2abab4b21b3ae2646cdcc3528ef6fd8587b3a0636ead67a62309fb003afdc14177d329b062622313e9dee912847763c68678df663a39b89c69efdb6d916d5754534bdca9030955cbcbae6fb7ff1df6282175cd37a30a904418b976af05809f0e0e7e4b4e2ec018f1e9c6bcbe7a7822c8699669946f5e684671d63e68cc7c9cca2963945dc21c52232e6f83b1875b2bed7c80c37371a480a2e5255d49d390c3b2adfc695036ed91371cda7d79bdfbae464581f0b32942f03826aca17ab9da6ade4a778d310ec3da17fc3af426d21b347aee7c2db7b5e188e35714dc514e3a1c100e8595c9e0e4399ad796021976f077e5733ea535cc6daec2e371853dcb715fc366ea7d6b9a5b3509dccf5c2e1225e3a51de9f5bb9b6586b282a0b27a9ae7ae8f2be14ec677670241e384b462eecfde68957839b1327c9e5c622c0f67cdaf3845ddbe6f754401d720d6b6d5c061dc906bfa70fb76e1168c6ac1a25cabe8873c3c1e540ae44ae631a2638accd7951f368442dba7b38d0662ccb0140d1e4ca23f51de731a6f5adcf816c3235359afd607e58948da29a5f06c96b4312aee7d35ed4c2c811a58c5a196ac2f377d653d51cfccb5213c928955ac880b5fb1b91e88a52d5c217cbf78e071275fe626c230fded548b0f1667af1309149ff74c5542119d4e269fdc1b241d9f53e02e38e015b7c5c2d2ee623bcb4167e37edafddc7fadd642c20f81b454db1a3b578d527f124dbd1f3d99fdd1590256ae4e47c2e8b3bfe8708a0d5506d6ce8b130ce6b70028161454a5065e9925d75c0095dc24ba789489fa1e9236e25330ca1a45e61224ee027664f6589028a240961aff09187fb719ff3477b56427189b7b3c790b4031f6539c5e3a8a5d7fd99c2534ed1646920a43e7315bba98d59c51b337ba7a1b038006bb574df46830f96a5685e07ee8a0a41e712810faacfb231c67e69d0fc24b98d782c70e15524d5d2dcf4e64b4e26772dfa7067f6dc7ffc8b06e6ef3ecb13f927d466e0cd3a7ea09aaed90b7810bbfdcd8a1274bbe78a453ffff11ccee62059ec25955b34a1a1cdf8a3e506d99dcbadf16032646117556ad71cd93ceeec42be0350a6c9f194fea783558c42a56d034dcaf6fb1b28037362c7c6e2446bcda71d0a88adda3144589447ef13cb85a4d2cd16ef444097bc03e32c3b1a055e952f7ac87078b04deb900375a16dbd382ea4375ddc0a9645deca620590337e803ea8b41337f3f4e4030119a2337424dbea3d21214063ce853843dd4e6df94e3dc3bbb36d89d9eb8e15e52d0699bc6ad1de9d12afc95c8785d63756576d357126e13b25502d542774f6e5d2fdb559d52698d086fffd05aa5d10939a524d7b2ab225cdb41decfc129208c372d51ca1db705bb1b9831e53ee000000000000000200000000000f4240657c0bd97e795218aa623b27cf9a71764379c4762fcee8993aa0b1ab1e32194a00061b100002c0ec5d388a8c78491bbd870faa2c46e4282e11796123b69c2792e5806748ef068397f0212a33e3f79162ba4ea247ccec410db039dd323af48ac27bd0ba77ecb870c587477d4543d9a29c53fc02bc98d7cd0144c7abf80b999c22b42a28ae8d625478ca304001f9a49782ec970031e673c76e4e27357a321729d6df38d1d88dcbf764c69eda3baac9739bed637010f44638cfe1deecc56b76f6e02d1d0c3f104462b9ffa5f20de4cf092d86a5bb35d5f62b0fb1c983a2c06df17c9ffc809c83e4b4335f5903fab536fbd9719847bb063541ecbe05c12ef8d058b3547faca054e3d662250f1cc1f925dd71297abc25fe37ab33a086759fac76208a64552f84d2e4d84daccdc3aa2bbd2c2f922bf262596742dfe034529d1ead2975dd3d197ab0e2e1c75c8b8f160ca6077638022d4afbd107979949cf342cb399347f3990029f0db6d9ac0c569d61d42539371f9a7ff59e9c83ff97d15bf0eeb254ae58fb7b1f9d8710c546ac8a227930c66ac841bc4f475229e5cadd14ba5a01e6b2da99c55861a08e2100e62c4499d30003fe30ddaa347d7a27c2158d3787d58fe51ae57d797bbef7f900508d1580df3e5233f0887567fba1faa918c246d2ec5c3b7aa022cb8a652d00b4d719e312482f57655eee80a90cdc73151fd7ab9c5367793d60c6088fab98f0547d7f547e10db202a25e027a5cd0abc41bb0e3ef563c0a6d469a702b2a26f0e8b4fddb845a16a5f06b9dee33c3adb31430c94942c5023179d3e4441948a332069a1c3b69dca65f05a43452e42fd28a2f7e6344f98ab9a7e4eece3c1709be1f7bb620f8b6c45989a8bccad39a4bf40e8215183d1449196f1f9fc17de778b616856152e6e6145a1b7a3d7f226becaea5ebe34aa4bd06e60f0fed207bfd21f5663bfadd37dd722437bcd46a26fb4e19d062574a81bcd817eecbc1914a5878809128961acdd73113ae9c51070ff4494e16d81ccec777eeb513da82bf43d4884812b26546b4370dc315793271b069f60f4285f648cf122ed8b22b0c7a27e94ccd59a273eb774c109e19980e146850de95f82cdd8aa0e82022672024c917b281422d284df0ee0bdeb3d4ac56b4ca675ebdb835c17b6a822d79ae7310f4aa41d80ac61c5e45c1c0e1d64542622a31091a9f87c335e86d964dd85a951d7c9bf41c9f2b1a9bb8424d7d1b26413da8034182fa42d2b1cd1f8745482c49d8348d19c72cf5a02bd28e4cba82128af8bf5d9c1215c4f543ef4d185f100f8d803dfa29c300c072e44ad9542b82fb1380d55c15c9a4b4398876e2450b90b49990746f339abca8cc8a462b62329a128758ce0e46b5f998af1bb485a3044bd125424eb5c623afd2a11befe4ec544eafe275ed1ad82b940dad5e9a9710d48562e51b296ba81f2d70593685ba0e3f3b25089a187e61d5675dd481aa99620276cb0a841a3c4df201a929287b1127270c5d25d06fb286dae1a9a5a5cdb60003f0c30d2021074bf252e550685f7b51a087a77b0871e883104e55f898aa5bc4cf8538c293253a737556d7f220e15b90cf0eda7d5f2172372e3c50c12cc588f312da37191b5038e944825044b130bff281ecd47a4252a1411ab7a9305c2b37e9facd435e9c434de37641498f8e4bfa7b42966da29c84200aca87ea1c3b00db54906b340e524a7dc4a15403bb82bc24517cb91026096bdf18f5f7ae5640ed6de1f0c5d184813d6b9d244b32b58e9ff524741a39383eec3a530d60db13deb26e3523a725f0599b671b625c07002704fb600b77318417d2527537359d122e22a1f7581eaebdc19e65ba50bdda18ae08e9a8694fcb0ff1a2cd98d910dbcd52064c15a4282d67b278c72a0fdbf228abf6b519dd28ac21c57d1da4bb7ad5b5ab10da6b83132df1da79ccfc77fb45598bbd91ef5ab96d8a2ee148639a562debafffd05aa5d10939a524d7b2ab225cdb41decfc129208c372d51ca1db705bb1b9831e53ee0000000000000003000000000007a120bf5899359b258acdec0ea500e437267aa6ba18e8509e15df74518f6ba7aa348f00061b1000036d768d1d63f1c9f09c252b5b48cf4d2db3f4935150c551391b37727723168982d7e58ce2a769313e461a0f3791a8c0208a769f5bdf4d57fbd0a9de104a0b1bbc1ab25ebbf87d1bc86dbbd42fd0dc0ff18a7ebfdd692c7dc3b95d095052540ce7801f3e25cbbcfd9dab857bc39624f59facbfacef5ab1e1e1f889b85f3b1f2a580cd660b73aa662fb15487722bd3c3f93d272725289136f2ee0fae4efe1afea25e6d4fc8334a47ad62d22be06605a15538dbd2a81311c4489b91d1cb143743e4570a6345c4f035c060aaf287ef66e7ebbe7b9037c10b66e087827478fdf76a02d25fe90e0f8228c1edfcb12eee3dd4e505a6c5a7bf2f5954ebb5560cd8c7f8b8f3f4ddca41a48a0d6c0e1092dcadc853752f459486bed349213cc15044585255842717ab70a3de3f0eedeecca10112c85a8bc248dc66c883a62288b49588d9fcc048c51081e94d65bd4731e7d71fba13693b82d2831d3bea370918aa5ddf1cc4f0d5015bb8dfb951ab13fbb9d26d5cb83dc980fc36f1712d616ce24d2c530253320f4f322f093a605ea426c577544f2983efc80be56791f443652c2233039a68f966c2f0b6351068616755dd2036b6226244d394a2b5b59160217603149901e8abe19a2bf404f384c2ed7fb0c5e470ee5ee8561f58f66bda729c2c8816853ad2357a009e537efcb4a28e845ca616be917b15aa6b8eb280bfcb321ea62fff21168b8ed54d58ccfdeee0e7752bfc0f02549d76615c85dd1e152a85ee931b34f436439e2233740328ba504c49f9764e1dca645ebf6a1377310ab53b68b4d0a6e6c952068249b86c29061725035db8d294ab9c56901485814735aa2a8d6987b1a19ced65a332f97751c4cd8a27093851f7775e5314078c04d254754d976bed2dbd2e6ecda62e9a0c7fd95299b4b13a54c9498d384210fb42d3b6bc5d8f0d42e42879f86c21eb7c5c6d1bffdd598b8f3cfcb75df159f1125a65f960637c62c7c5632d73b7b4b0544082008ede22d87e79e20eb08be0817650fefcd111de48ba2be02a7b080275c991a0ee4445dab89312644c7cf4101895e2dbcaad7d87e8e3b13e62751861b204a7e6f5a476eab0817c294d59aa0247903077d4cbe4a98e7984d2b04623d2b2ef4c650b43db15541ede229c12c045529b5c77993eb6acbdc28d812a486b5957fb996731980555bdd59ad824a882ebe1a77cbe6b9035f1c69dd01b2a27a47be5febfa65c721354e70071b07db4ebc2f01d143587c1b32a5337dd010d2a76a7773f4a7c665b4cfe4a61b103b1c319d85e007eb99b52400cd8776697e1d6118197655bf7bd0a5e7f4594bf36a2706128d5f5c3ee166b586c4d515611f4597a4c1088c1853a5959f73830cb973ea922d6211ee7d9b1d67b1025486f8f3c72a517d0d48d9a57d64c0f48e513c3b09e14ce91b515a87f3035ab55d241ccb12108dd299a362af26af96ada920202dfe26d456065717a85e6bbec540637059d82480f6c917a11434a9be5fee5ba33a8552b7b0e59f123991525e1dc14bfdfd109625b2df477bce565045375dacf6ffe99081914fc9f64df7ef8eb26801ce01be083555fd2f8a338a33c07f484b01a310ee420f5c932cfc5a8f6d32a20c3915a188833ac5775500c5d2b73e4ea595512e869f91f83de8e048c804ec8cd6de45b7ad264bcfb3c235325d3c09673a2a94e60736ceada065cefb1e355bd35bd4b56ae513343e85a3d840c2211da2d1941112318f97f825e1714f74fc2d33430196cca2423f43641b0d75b4b1a1a1ef0f07af31fda06220a2628aaab9f303cb4fe6d73fa9b39e8f6083af3bbca1becdd646afbd888c560bbf7cf4fcf5c02f712726eb23b03fe5c290df63a85ed1026cee07ab0d2bb868aa4dd6594252cb75ad5d11d9c53c5238047ea3d311c1443d478dd283a260270400002710000000000bebc200000000002c232220b8e78d93ef82c5351ede6c6aceaaafb806ef6ff23deb81fd78e7a9bebfcdeb5302f6bed70479b4d98f5c93f32b382524233983ebebd14b2b50fe35142766300efe000000000000000000000003fd05ac00805d10939a524d7b2ab225cdb41decfc129208c372d51ca1db705bb1b9831e53ee000000000000000000000000000493e0f81fd474bea478df0202b311c69e85dc6215f629491dd15ad0929faae2535abb00061b10000370b2714d2734e6b8cde085794dd7b41c8a9b6c03c1edd8e3db168ee7fce39493596e882b18b5b1b79c16400c6762b9856075821be6be9fab8f469f56820d8f341554400a8da7f1f1a8501902581d43b9fa6e5c68015716f718a2190b87fce41bf1b509aa61806394a42489d63c457fe4c79e7480eced1315edd731887e57704fc9102f50cb7f0d242d755cfd5a2172dbaf7f01b124861cc6d1dc804796bbb84165c805f0c0f3fb9ec97b74c2a694de56a9cf8d79d1a679260ee1169d78214b34c8a654ba22e59ddcd32beff4713de33549f035b342660405b0159a7a508e5691ef4805689140e72b8a0ef2e61be74dea5d0b8f5589e0e373cac2e2e1cc39b2121c05cf4122ad0f8b9af6fbf1de2ea26376c2650ccd306c13a7b64acbf2a3feed128754abe44658009e642768ae3d84f5e0fa5f7f360c2a1c76d26985817ae77b71fb59014a5483ebba9271cafa5e5d8031c569adeceb8bae6444e98d2522b28f6682109fc7d31cdb83ebd45e5d81e7f046df42345b49f470dbef9ed87709301d2c6131215d33a30b8d18e63e54a2aff85dd57672f8198bca6a67ee147c7d0ae649e5661ab6bf78a662fef9a164f1e332b9f16e6fb3d5769ddcbc1d1c07338d3394b9245d17618c2474e86c064fca4df00ad3a93dc051fd8c3328cde2a987798b0f22a21c90426700abeb1e6f38dffb485b5477ec44c690fa80e317b32a982fd3082253bba8595783290dbffee4fc9296ffdf16a8bf3154971bb720e78674969e9db2e0fbab9e9e13f24bc8b3af5e2f00f262f0da56de443f70398ab68f747d35370fcd8e1c0e130f7269e08f862b5a67f2c129be254df2358762ce3a947eb27d66450af51540e7721b47c8a5a86098ea64dad381f14e07aabbbc470949a99c07612add3ab4c575fe2e520bbe511a1a674aea37a44535c13ee3380f8f39bd230fc1481cd31912af36c6751e23c6f383cd37a8b13fa7df9f0c7e460739f2c6226638ee14f14d36366211cbc6a1e16b4856bf302a540aa9d9e833b1d59c510473096384c8b450f2f3f1dab9e614af822949d5cc93d76bc4d1a52891bc85f1981ef83161195ab7d8181ee4fb163bc6c685a10e87c7f4b15ed7d05833c230a4a5b63841fc65b959f0ff010e697f47c583f9b7fa9b389c0eff6614e47d85b83c483136f182be4c151d272f5d938b912a95e47d333e5de6a409ad271679a778a7eb3f169c71525302fac5d4575e2645c09763c2ef165736a7a726ca605038e2781404328790ffaacef2b9c2bf90122042cd571287bc4e3973da65fbd4e3da9e40e4347ca6eb4ef1ffef4e5a34be80425cae3e81533f7f2953f95fca53a22057a39125f5c76350fba7fc6c036838fb951d0aa8702e7f44c6f8a9cbce3b64fa8ddc2bb8c8b35d1e29a21beda6fdd332b31a749321455277231fd9d70ea4aded95053b395f88fa6916d126e1626fc0f1be6cd2a9538d17c498b40927f12b3bb40fa3e272e82cd2242b670afefa387470f4e6e0a1236028954c9e90311f486617187956a23b90b356d71e219e6dd055c2120771003a6c12769aa3ceacb9642bc01022731ca7a413b68ee7d1d5444f75dfa51a68b74a01ac85f6ceaf5e56987b9d67d6de896f5aafd25c78c413a6d4b5b03d571167524cd231ba13bd9f80fd7413faf21e8170cef0d08b242c5c38a2b0158da56e358ba0692f670d4611c7a3624b234adc30c5b7198e0afc941f5d13eae3a94ddffa652c784c34c582e04e948da91a5ac3038a9df38fd4f1733779f4f122ca2d7ff9d03bac9def35d9ee3a183161f8f2808d472b2e64581209359cea58ca7757164c666029982223877e2b14d2d537afb012f1ffc12cd083c16dfc64213c56f3d4d22b603d3dfab1d21e239d6fc1f9f153ed61f1ac91c29c85c16f4aa2985f84052f5a08d32bddfd05ac00805d10939a524d7b2ab225cdb41decfc129208c372d51ca1db705bb1b9831e53ee00000000000000010000000002faf080ab009f4d0b317a141e5b3f865599676f895834400b66dd09d33ad27163ca176f00061b1000028fe56ab181f454372c939ca0516f5782b26cec2010885c70e55c41a1e43de3af6635d2ed0bed90837cefa9f1805f4808e8092a4d44efe5fb616ff7678487a460b367134fcc728ba273fb1d22718bc6a95e47a120a6d952c7c2cb8f38e59c2a4efa63977cced7e4b8f46e4d47d29098a73beee807c3337c4acddcbb32b78eddbd124b2f33ec6cd8bdd364aa4ad2c10eb69dab808fe5f5f0aec19750e51ac65a8746f345c05d4b8823ffbeadd6200ad01449c39a008fbe117a8ee904445488811336d0c439419fa4f285f9f62a34f10b076c99c0092968e3cc9fa656016b6da049bd56b910d7a9356e76d24e746b280f0275ec9e9bace82d852bf0a137ea02d4cbd3b68450bfb593564d8c20953bb758890a55a8c381a4b3303b61ec26a56111361bf7262b3f6f2503aad06758130d86ea607cdbba53415aaf253430d92fdd81c685ab39233e94654e6508eec1347747e2df2862169382aef6f99dd78b50629c5d98b1fcc73e865679d862b42f8e9d54ef6288ed2c3f2713f0fa4db538cd3e70ec1a30cd65dbf873f581b30892acedacd39b5f0aa774d1f3f77d8fd11ed628bcd02ac33f89123595aa455ec54a07e93e26f94338fedd8bb84094a0add52f912ed5f9019e3a28d90d251cc6ed7ffd35254dcadd9f1e9b28eb0e06fd4fe961d60cb690a7757f475c08aef07c2e54668121540a42a9c779623709a2124629e8c4bd4021763979647f625b360a4559dfd3f57798dfe5d36e9d902904af3ed67d8f4b0894538c7718f5160d211cec27375a7e6a2ec42f2c8fcd1c953b7b8379d42439a2c6b921a66d5102ceb6bd6bc20b17098e69a0a4f708b42520e4792474c3d115a12c83ef60ac6e69d8842c5981e9a6d178efa352e73e4a34bed4fb590dbeecb259617668e6ffb9f955297f26e3a6a3b95d9617529a61f08666ca1069d2ee1876337d3e786244c5bb45a8236577184584cf3018118d7e4e78973ee510b6773bd922797e580cd240dea3ca31892d23c1e6e4fa92f1a01da8ea40044f5613a9429ebe7906f79b32636204d025115810b376d4c6436da136b96c7c10649e3290caecd6ca14d995a817e3725fee7e621c5366f80c752e50aeffee1af3361924f31cbb1cb44731d19963ff30127ca2363ce15e50948be14c43400737ee8910ed06027599da74b06e77eb82ac523cc031c57c02dd82dbc0d53629d072615c92034cf829e7a5d4437b1f58e2bd4b16993e1e1b05c26ed8d695351db11d21df36a7f5811ef5fe001ab1e1c6ce9d2b69b6ac3af8087e6666317f75b645e3b1caefac0eb65327fcb9fa62be341c99f191cc869e48dbc8fee3e42d4393cbc6505c880dd6739a69be4f7ef3de306480a7a51f413d310926f252ea96a0c772d8b8e94e7d6cedbfbdb21fae2ffc379eb17c2680fa2bc56a8726c93e7bf2d446221ce95e49da93d29bec8e53ddcfd262c33d556c2b8921c3de93236408b462d28612d3343fbb9cc538b1e6b33c341c3b91dd41f936931e61f146fd00aee1c5c0de97b47cf7efce889012e1c22dd8faf0fe155f4e9930c27941d8b0907502a835bfffff801f6835de69ad33e95232f773219eec0e2374c421230f323257dbc91629c4ca8a61584f737b827fe8e8f5b69b88a7b64b362f8142b043f08ea82c4a0ab7c4e0b9805533e806f90597095242ff64f314801fb7ad838e98e1859b2c05c9b027ae5a4baf780d15977bc1492dee9b14b1cb0fb3243eb2304919486fcde89a3cf35a64e31b1698e35fbb8528a73526a19189d406272b8becec94379f69372afa99d06bb4f34df72e1c3b49557855ae8ac265160bdf48ab34cde30d2665891cfeda24adcc657d851431f38953f917f1a111f023d2c71845ccf25a562d2450bf8b4986b64ae4fd09e5a8ab9610d11fd68e9d570b3a467780236de974c7fd05ac00805d10939a524d7b2ab225cdb41decfc129208c372d51ca1db705bb1b9831e53ee000000000000000200000000003d0900559bb949364ed92a64f449cd6ee3eaba2e607595d3cecb36b06b145baaa69bc000061b100002860fd058656f185036e81a64fe19034d29223d3620bdf4bf2ff3c1def9c6bdd70c2cda7f660e1b202672741bc3258b04fb755fbbb1350261e7992bce930e256bd5d8d4fde61365b7b24904b788ef2040fdd6eb87f97c2d2f29eab4291d9a28b5da306f28b98d01d93517b203a028199d423a3545aa17522c63247f73af7b63335b0b48e4b875c69b42f4cb1573bb3e5fd68837f90c50b0161f067a9eafd9a0790e53ed5053ce60ffff410e4b16a4b7bc5a52c57e78ef266100c9f79753f81878c08e5dbd4d80c6e46a339578d8ac8c572df77ef614800cedc460c06878c0da97908067729ed35e3afe919071724f89ab736f5791a9c9b5d422136332213434c836e2ceb9fce0e2e96a9a6d7befe8c132867d5fafea1a7809ddd6b3a89c8ef6ea83028d3e2cca00f1bc6e12ea8b67e91a98acaa2edebaf6dd3a18c655b6b1fbbff5c641f8002780758d05f1f39c9470a124a5add314abd2262142733120747cda2f1d9eb90d68ecb9c7fbab23d73a35f2a20a2a365de6cd678d53bd5bd9bd518333d04e8e678b5d08f028982dad08c80be7d8fbb0638dd814232224c687f8321baf96ed8b39a1e9ab52dfd69d8eed79ac3f5a2c480a585bff038c92b367743317b937d969cdd533ae1d797a789ff7994f86a0d6cae470b64ebddbc478573af347a110dd1feaaeb4779441ec439cfdbafaba870105efd86b9d85a4df7ddb9b09f5b6b4144cd1fad5932df37ebf19a62648659fc1969142310a5cc9b4d0c48ba6bb0f863ed53a0b75fe1ee6515a46993f95be2e34166408b54a43e55c4802b37ac902fb4c8367ce38990d07ed3104d0728d327d3b9de6452b520f9af534505885788109ec78c1176ca0864d28422e826cc83f821b7eaf028d6a7e350b3037d0fe58d1d4e18113c8f61913932e71c0f334402534d8663f15445f900fb9dc6b3a93223868167be26fcbd70c0459eee37f81fd539c319eb0b04bd478b94b5f4cd23b4d496c2bdd6e8a154fd76c4ecbdf7647fe9e7be88c6a3a8e7696e2e596dfc25ba798db6ca331d135e9ce7c0aab9721d3f70ca53354f96ecd028236259b9b0d9e0bbf73c8e841b1d4276214f7be8feb525c91d39910b0e091997a2b89e945806e93cd325cb51463b0729f1a519334038cba09653799ef533a49e812e86b81af7e5099a02ca11c2b17dfc8b9e51a57a20546f2c92826676ebcb4f64fe7cc77424388dfec7199179cb125bb4613c8bf05edc4173987d7d5ae0fcbfa08a1e5ea2d6b01406d740b49c5b1a68da585549590c3ec13479efa3136c5ade68057fe173ace55593ceca8440372b03f332969866d1bfbce3fe9dd907d27593b8b2ad25eb4b12afd0f3abe7931ba7789c84a3ed65a03df06d998a9956043a4de786c359bdd58f0b9e5cfc32bc709b626ce8e63f3997a0e9f784f6b94e342b4710553e805cc8399191254189058ec75a15556467b2456d9b38a7e4d15cf59727ad2c32f0daaef1748be04311ff484479eb31f7eff9e32b816fa40f40430e801c15e931294b39ec4d6c93e8130fb4a1b53ca6471886a33b10b46d6973d02bbe3b39345e121473ea4ba41f3cfced07c3a613393e38b8ca01c353ccab96128093bb5fd000978b11ae6f7c1fedf48c3682119d1b44629d02ea3800d2e247ef1e78e527e5d574b4dd144e1fe1b0c6551442d419baba300258657792947443747b29b6fb9417d3c57536de6aa379c02f3addbd2066554189ca817c7f57331c972c30e3ed06fc7e521b8e2bb57c023dff9816f260a153f1ffd55df2a2fae568f3dc734d62ab47a77c54772b5e4cf2758c912cb473ec372173d9eda0c58b103ab12d11dcb976a40816ecfa8b9c9384f6415017555933652e77ae20e14ea9ac25b5811fc03364c4883bb78c82e29f8a8892415ff652b782642bf6036b3e2200e0000000000000004000000000000000300040000000000000000000319c0569a7b4f4021827771a963002b8b00000000000000010003d56d81344fe44d1f8f1a95850431563c00000000000000020003d33fd4c8d4ad4ce3880bf79a4856ca2100000000000000030003efcb10168b144c8bb4f694a3b98a129f000000000000000002000700fd05aa5d10939a524d7b2ab225cdb41decfc129208c372d51ca1db705bb1b9831e53ee000000000000000200000000003d0900559bb949364ed92a64f449cd6ee3eaba2e607595d3cecb36b06b145baaa69bc000061b100002860fd058656f185036e81a64fe19034d29223d3620bdf4bf2ff3c1def9c6bdd70c2cda7f660e1b202672741bc3258b04fb755fbbb1350261e7992bce930e256bd5d8d4fde61365b7b24904b788ef2040fdd6eb87f97c2d2f29eab4291d9a28b5da306f28b98d01d93517b203a028199d423a3545aa17522c63247f73af7b63335b0b48e4b875c69b42f4cb1573bb3e5fd68837f90c50b0161f067a9eafd9a0790e53ed5053ce60ffff410e4b16a4b7bc5a52c57e78ef266100c9f79753f81878c08e5dbd4d80c6e46a339578d8ac8c572df77ef614800cedc460c06878c0da97908067729ed35e3afe919071724f89ab736f5791a9c9b5d422136332213434c836e2ceb9fce0e2e96a9a6d7befe8c132867d5fafea1a7809ddd6b3a89c8ef6ea83028d3e2cca00f1bc6e12ea8b67e91a98acaa2edebaf6dd3a18c655b6b1fbbff5c641f8002780758d05f1f39c9470a124a5add314abd2262142733120747cda2f1d9eb90d68ecb9c7fbab23d73a35f2a20a2a365de6cd678d53bd5bd9bd518333d04e8e678b5d08f028982dad08c80be7d8fbb0638dd814232224c687f8321baf96ed8b39a1e9ab52dfd69d8eed79ac3f5a2c480a585bff038c92b367743317b937d969cdd533ae1d797a789ff7994f86a0d6cae470b64ebddbc478573af347a110dd1feaaeb4779441ec439cfdbafaba870105efd86b9d85a4df7ddb9b09f5b6b4144cd1fad5932df37ebf19a62648659fc1969142310a5cc9b4d0c48ba6bb0f863ed53a0b75fe1ee6515a46993f95be2e34166408b54a43e55c4802b37ac902fb4c8367ce38990d07ed3104d0728d327d3b9de6452b520f9af534505885788109ec78c1176ca0864d28422e826cc83f821b7eaf028d6a7e350b3037d0fe58d1d4e18113c8f61913932e71c0f334402534d8663f15445f900fb9dc6b3a93223868167be26fcbd70c0459eee37f81fd539c319eb0b04bd478b94b5f4cd23b4d496c2bdd6e8a154fd76c4ecbdf7647fe9e7be88c6a3a8e7696e2e596dfc25ba798db6ca331d135e9ce7c0aab9721d3f70ca53354f96ecd028236259b9b0d9e0bbf73c8e841b1d4276214f7be8feb525c91d39910b0e091997a2b89e945806e93cd325cb51463b0729f1a519334038cba09653799ef533a49e812e86b81af7e5099a02ca11c2b17dfc8b9e51a57a20546f2c92826676ebcb4f64fe7cc77424388dfec7199179cb125bb4613c8bf05edc4173987d7d5ae0fcbfa08a1e5ea2d6b01406d740b49c5b1a68da585549590c3ec13479efa3136c5ade68057fe173ace55593ceca8440372b03f332969866d1bfbce3fe9dd907d27593b8b2ad25eb4b12afd0f3abe7931ba7789c84a3ed65a03df06d998a9956043a4de786c359bdd58f0b9e5cfc32bc709b626ce8e63f3997a0e9f784f6b94e342b4710553e805cc8399191254189058ec75a15556467b2456d9b38a7e4d15cf59727ad2c32f0daaef1748be04311ff484479eb31f7eff9e32b816fa40f40430e801c15e931294b39ec4d6c93e8130fb4a1b53ca6471886a33b10b46d6973d02bbe3b39345e121473ea4ba41f3cfced07c3a613393e38b8ca01c353ccab96128093bb5fd000978b11ae6f7c1fedf48c3682119d1b44629d02ea3800d2e247ef1e78e527e5d574b4dd144e1fe1b0c6551442d419baba300258657792947443747b29b6fb9417d3c57536de6aa379c02f3addbd2066554189ca817c7f57331c972c30e3ed06fc7e521b8e2bb57c023dff9816f260a153f1ffd55df2a2fae568f3dc734d62ab47a77c54772b5e4cf2758c912cb473ec372173d9eda0c58b103ab12d11dcb976a40816ecfa8b9c9384f6415017555933652e77ae20e14ea9ac25b5811fc03364c4883bb78c82e29f8a8892415ff652b782642bf6036b3e2200efffd05aa5d10939a524d7b2ab225cdb41decfc129208c372d51ca1db705bb1b9831e53ee0000000000000003000000000007a120bf5899359b258acdec0ea500e437267aa6ba18e8509e15df74518f6ba7aa348f00061b1000036d768d1d63f1c9f09c252b5b48cf4d2db3f4935150c551391b37727723168982d7e58ce2a769313e461a0f3791a8c0208a769f5bdf4d57fbd0a9de104a0b1bbc1ab25ebbf87d1bc86dbbd42fd0dc0ff18a7ebfdd692c7dc3b95d095052540ce7801f3e25cbbcfd9dab857bc39624f59facbfacef5ab1e1e1f889b85f3b1f2a580cd660b73aa662fb15487722bd3c3f93d272725289136f2ee0fae4efe1afea25e6d4fc8334a47ad62d22be06605a15538dbd2a81311c4489b91d1cb143743e4570a6345c4f035c060aaf287ef66e7ebbe7b9037c10b66e087827478fdf76a02d25fe90e0f8228c1edfcb12eee3dd4e505a6c5a7bf2f5954ebb5560cd8c7f8b8f3f4ddca41a48a0d6c0e1092dcadc853752f459486bed349213cc15044585255842717ab70a3de3f0eedeecca10112c85a8bc248dc66c883a62288b49588d9fcc048c51081e94d65bd4731e7d71fba13693b82d2831d3bea370918aa5ddf1cc4f0d5015bb8dfb951ab13fbb9d26d5cb83dc980fc36f1712d616ce24d2c530253320f4f322f093a605ea426c577544f2983efc80be56791f443652c2233039a68f966c2f0b6351068616755dd2036b6226244d394a2b5b59160217603149901e8abe19a2bf404f384c2ed7fb0c5e470ee5ee8561f58f66bda729c2c8816853ad2357a009e537efcb4a28e845ca616be917b15aa6b8eb280bfcb321ea62fff21168b8ed54d58ccfdeee0e7752bfc0f02549d76615c85dd1e152a85ee931b34f436439e2233740328ba504c49f9764e1dca645ebf6a1377310ab53b68b4d0a6e6c952068249b86c29061725035db8d294ab9c56901485814735aa2a8d6987b1a19ced65a332f97751c4cd8a27093851f7775e5314078c04d254754d976bed2dbd2e6ecda62e9a0c7fd95299b4b13a54c9498d384210fb42d3b6bc5d8f0d42e42879f86c21eb7c5c6d1bffdd598b8f3cfcb75df159f1125a65f960637c62c7c5632d73b7b4b0544082008ede22d87e79e20eb08be0817650fefcd111de48ba2be02a7b080275c991a0ee4445dab89312644c7cf4101895e2dbcaad7d87e8e3b13e62751861b204a7e6f5a476eab0817c294d59aa0247903077d4cbe4a98e7984d2b04623d2b2ef4c650b43db15541ede229c12c045529b5c77993eb6acbdc28d812a486b5957fb996731980555bdd59ad824a882ebe1a77cbe6b9035f1c69dd01b2a27a47be5febfa65c721354e70071b07db4ebc2f01d143587c1b32a5337dd010d2a76a7773f4a7c665b4cfe4a61b103b1c319d85e007eb99b52400cd8776697e1d6118197655bf7bd0a5e7f4594bf36a2706128d5f5c3ee166b586c4d515611f4597a4c1088c1853a5959f73830cb973ea922d6211ee7d9b1d67b1025486f8f3c72a517d0d48d9a57d64c0f48e513c3b09e14ce91b515a87f3035ab55d241ccb12108dd299a362af26af96ada920202dfe26d456065717a85e6bbec540637059d82480f6c917a11434a9be5fee5ba33a8552b7b0e59f123991525e1dc14bfdfd109625b2df477bce565045375dacf6ffe99081914fc9f64df7ef8eb26801ce01be083555fd2f8a338a33c07f484b01a310ee420f5c932cfc5a8f6d32a20c3915a188833ac5775500c5d2b73e4ea595512e869f91f83de8e048c804ec8cd6de45b7ad264bcfb3c235325d3c09673a2a94e60736ceada065cefb1e355bd35bd4b56ae513343e85a3d840c2211da2d1941112318f97f825e1714f74fc2d33430196cca2423f43641b0d75b4b1a1a1ef0f07af31fda06220a2628aaab9f303cb4fe6d73fa9b39e8f6083af3bbca1becdd646afbd888c560bbf7cf4fcf5c02f712726eb23b03fe5c290df63a85ed1026cee07ab0d2bb868aa4dd6594252cb75ad5d11d9c53c5238047ea3d311c1443d478dd283a2602704fffd05aa5d10939a524d7b2ab225cdb41decfc129208c372d51ca1db705bb1b9831e53ee000000000000000100000000007a1200ffb841fb291ecf09ae7b5dcec1feeb45ba196ca02d6b9e50ae2b3dcd9ca5d72500061b100003acd0a3acb9b4f9540678dc9324480bb3d4f54e5e007739c9c1d600bff75dfbbd0191e75c0a2d810a6ec5b03d02cffbf1a66123c87790e66eee8d416caf82e7ea7090a62fe14276fa88af32ab9793f7a100de5525eedf71967c13c8d361246d98b78cc74383e714f88899f34407644c14874046b3b722186015c07b8db042db955c91cb14abbf3cfa646aea81ad15bf67108a763539c64c5a8e8115d46e063e956671e8ea8d8fd638a6414d71e9b475ebe070da7faf75a898f29048ab5a2b6c7e3a72a178b8e470e8375f539ebf6284d15486c5a8774d46ea164ba2b62181f47623fd987ff5958550c962a193638679b79fcd477fdf2a09c0fa879bb22cb493fbaed27518f5cb265741535b4ab14246077ca18f11ece7aaa0e01ec5bf02c3c3b541ea08bf254df123079c1538e266dcd3161bf1b9ed41d873f1491906e1459ba51ac9dd95e783598d3c356e0cc5b98c2a96b148f55f102a9810181eedd46cd00b445d861baeeac46eba469435aa4ddc877bd68b53f4d005aa2566d356cc344aafcdc86abd774ea28cc838d2fdc541c4b6da494a96e128b8c2abab4b21b3ae2646cdcc3528ef6fd8587b3a0636ead67a62309fb003afdc14177d329b062622313e9dee912847763c68678df663a39b89c69efdb6d916d5754534bdca9030955cbcbae6fb7ff1df6282175cd37a30a904418b976af05809f0e0e7e4b4e2ec018f1e9c6bcbe7a7822c8699669946f5e684671d63e68cc7c9cca2963945dc21c52232e6f83b1875b2bed7c80c37371a480a2e5255d49d390c3b2adfc695036ed91371cda7d79bdfbae464581f0b32942f03826aca17ab9da6ade4a778d310ec3da17fc3af426d21b347aee7c2db7b5e188e35714dc514e3a1c100e8595c9e0e4399ad796021976f077e5733ea535cc6daec2e371853dcb715fc366ea7d6b9a5b3509dccf5c2e1225e3a51de9f5bb9b6586b282a0b27a9ae7ae8f2be14ec677670241e384b462eecfde68957839b1327c9e5c622c0f67cdaf3845ddbe6f754401d720d6b6d5c061dc906bfa70fb76e1168c6ac1a25cabe8873c3c1e540ae44ae631a2638accd7951f368442dba7b38d0662ccb0140d1e4ca23f51de731a6f5adcf816c3235359afd607e58948da29a5f06c96b4312aee7d35ed4c2c811a58c5a196ac2f377d653d51cfccb5213c928955ac880b5fb1b91e88a52d5c217cbf78e071275fe626c230fded548b0f1667af1309149ff74c5542119d4e269fdc1b241d9f53e02e38e015b7c5c2d2ee623bcb4167e37edafddc7fadd642c20f81b454db1a3b578d527f124dbd1f3d99fdd1590256ae4e47c2e8b3bfe8708a0d5506d6ce8b130ce6b70028161454a5065e9925d75c0095dc24ba789489fa1e9236e25330ca1a45e61224ee027664f6589028a240961aff09187fb719ff3477b56427189b7b3c790b4031f6539c5e3a8a5d7fd99c2534ed1646920a43e7315bba98d59c51b337ba7a1b038006bb574df46830f96a5685e07ee8a0a41e712810faacfb231c67e69d0fc24b98d782c70e15524d5d2dcf4e64b4e26772dfa7067f6dc7ffc8b06e6ef3ecb13f927d466e0cd3a7ea09aaed90b7810bbfdcd8a1274bbe78a453ffff11ccee62059ec25955b34a1a1cdf8a3e506d99dcbadf16032646117556ad71cd93ceeec42be0350a6c9f194fea783558c42a56d034dcaf6fb1b28037362c7c6e2446bcda71d0a88adda3144589447ef13cb85a4d2cd16ef444097bc03e32c3b1a055e952f7ac87078b04deb900375a16dbd382ea4375ddc0a9645deca620590337e803ea8b41337f3f4e4030119a2337424dbea3d21214063ce853843dd4e6df94e3dc3bbb36d89d9eb8e15e52d0699bc6ad1de9d12afc95c8785d63756576d357126e13b25502d542774f6e5d2fdb559d52698d08600fd05aa5d10939a524d7b2ab225cdb41decfc129208c372d51ca1db705bb1b9831e53ee00000000000000010000000002faf080ab009f4d0b317a141e5b3f865599676f895834400b66dd09d33ad27163ca176f00061b1000028fe56ab181f454372c939ca0516f5782b26cec2010885c70e55c41a1e43de3af6635d2ed0bed90837cefa9f1805f4808e8092a4d44efe5fb616ff7678487a460b367134fcc728ba273fb1d22718bc6a95e47a120a6d952c7c2cb8f38e59c2a4efa63977cced7e4b8f46e4d47d29098a73beee807c3337c4acddcbb32b78eddbd124b2f33ec6cd8bdd364aa4ad2c10eb69dab808fe5f5f0aec19750e51ac65a8746f345c05d4b8823ffbeadd6200ad01449c39a008fbe117a8ee904445488811336d0c439419fa4f285f9f62a34f10b076c99c0092968e3cc9fa656016b6da049bd56b910d7a9356e76d24e746b280f0275ec9e9bace82d852bf0a137ea02d4cbd3b68450bfb593564d8c20953bb758890a55a8c381a4b3303b61ec26a56111361bf7262b3f6f2503aad06758130d86ea607cdbba53415aaf253430d92fdd81c685ab39233e94654e6508eec1347747e2df2862169382aef6f99dd78b50629c5d98b1fcc73e865679d862b42f8e9d54ef6288ed2c3f2713f0fa4db538cd3e70ec1a30cd65dbf873f581b30892acedacd39b5f0aa774d1f3f77d8fd11ed628bcd02ac33f89123595aa455ec54a07e93e26f94338fedd8bb84094a0add52f912ed5f9019e3a28d90d251cc6ed7ffd35254dcadd9f1e9b28eb0e06fd4fe961d60cb690a7757f475c08aef07c2e54668121540a42a9c779623709a2124629e8c4bd4021763979647f625b360a4559dfd3f57798dfe5d36e9d902904af3ed67d8f4b0894538c7718f5160d211cec27375a7e6a2ec42f2c8fcd1c953b7b8379d42439a2c6b921a66d5102ceb6bd6bc20b17098e69a0a4f708b42520e4792474c3d115a12c83ef60ac6e69d8842c5981e9a6d178efa352e73e4a34bed4fb590dbeecb259617668e6ffb9f955297f26e3a6a3b95d9617529a61f08666ca1069d2ee1876337d3e786244c5bb45a8236577184584cf3018118d7e4e78973ee510b6773bd922797e580cd240dea3ca31892d23c1e6e4fa92f1a01da8ea40044f5613a9429ebe7906f79b32636204d025115810b376d4c6436da136b96c7c10649e3290caecd6ca14d995a817e3725fee7e621c5366f80c752e50aeffee1af3361924f31cbb1cb44731d19963ff30127ca2363ce15e50948be14c43400737ee8910ed06027599da74b06e77eb82ac523cc031c57c02dd82dbc0d53629d072615c92034cf829e7a5d4437b1f58e2bd4b16993e1e1b05c26ed8d695351db11d21df36a7f5811ef5fe001ab1e1c6ce9d2b69b6ac3af8087e6666317f75b645e3b1caefac0eb65327fcb9fa62be341c99f191cc869e48dbc8fee3e42d4393cbc6505c880dd6739a69be4f7ef3de306480a7a51f413d310926f252ea96a0c772d8b8e94e7d6cedbfbdb21fae2ffc379eb17c2680fa2bc56a8726c93e7bf2d446221ce95e49da93d29bec8e53ddcfd262c33d556c2b8921c3de93236408b462d28612d3343fbb9cc538b1e6b33c341c3b91dd41f936931e61f146fd00aee1c5c0de97b47cf7efce889012e1c22dd8faf0fe155f4e9930c27941d8b0907502a835bfffff801f6835de69ad33e95232f773219eec0e2374c421230f323257dbc91629c4ca8a61584f737b827fe8e8f5b69b88a7b64b362f8142b043f08ea82c4a0ab7c4e0b9805533e806f90597095242ff64f314801fb7ad838e98e1859b2c05c9b027ae5a4baf780d15977bc1492dee9b14b1cb0fb3243eb2304919486fcde89a3cf35a64e31b1698e35fbb8528a73526a19189d406272b8becec94379f69372afa99d06bb4f34df72e1c3b49557855ae8ac265160bdf48ab34cde30d2665891cfeda24adcc657d851431f38953f917f1a111f023d2c71845ccf25a562d2450bf8b4986b64ae4fd09e5a8ab9610d11fd68e9d570b3a467780236de974c7fffd05aa5d10939a524d7b2ab225cdb41decfc129208c372d51ca1db705bb1b9831e53ee000000000000000200000000000f4240657c0bd97e795218aa623b27cf9a71764379c4762fcee8993aa0b1ab1e32194a00061b100002c0ec5d388a8c78491bbd870faa2c46e4282e11796123b69c2792e5806748ef068397f0212a33e3f79162ba4ea247ccec410db039dd323af48ac27bd0ba77ecb870c587477d4543d9a29c53fc02bc98d7cd0144c7abf80b999c22b42a28ae8d625478ca304001f9a49782ec970031e673c76e4e27357a321729d6df38d1d88dcbf764c69eda3baac9739bed637010f44638cfe1deecc56b76f6e02d1d0c3f104462b9ffa5f20de4cf092d86a5bb35d5f62b0fb1c983a2c06df17c9ffc809c83e4b4335f5903fab536fbd9719847bb063541ecbe05c12ef8d058b3547faca054e3d662250f1cc1f925dd71297abc25fe37ab33a086759fac76208a64552f84d2e4d84daccdc3aa2bbd2c2f922bf262596742dfe034529d1ead2975dd3d197ab0e2e1c75c8b8f160ca6077638022d4afbd107979949cf342cb399347f3990029f0db6d9ac0c569d61d42539371f9a7ff59e9c83ff97d15bf0eeb254ae58fb7b1f9d8710c546ac8a227930c66ac841bc4f475229e5cadd14ba5a01e6b2da99c55861a08e2100e62c4499d30003fe30ddaa347d7a27c2158d3787d58fe51ae57d797bbef7f900508d1580df3e5233f0887567fba1faa918c246d2ec5c3b7aa022cb8a652d00b4d719e312482f57655eee80a90cdc73151fd7ab9c5367793d60c6088fab98f0547d7f547e10db202a25e027a5cd0abc41bb0e3ef563c0a6d469a702b2a26f0e8b4fddb845a16a5f06b9dee33c3adb31430c94942c5023179d3e4441948a332069a1c3b69dca65f05a43452e42fd28a2f7e6344f98ab9a7e4eece3c1709be1f7bb620f8b6c45989a8bccad39a4bf40e8215183d1449196f1f9fc17de778b616856152e6e6145a1b7a3d7f226becaea5ebe34aa4bd06e60f0fed207bfd21f5663bfadd37dd722437bcd46a26fb4e19d062574a81bcd817eecbc1914a5878809128961acdd73113ae9c51070ff4494e16d81ccec777eeb513da82bf43d4884812b26546b4370dc315793271b069f60f4285f648cf122ed8b22b0c7a27e94ccd59a273eb774c109e19980e146850de95f82cdd8aa0e82022672024c917b281422d284df0ee0bdeb3d4ac56b4ca675ebdb835c17b6a822d79ae7310f4aa41d80ac61c5e45c1c0e1d64542622a31091a9f87c335e86d964dd85a951d7c9bf41c9f2b1a9bb8424d7d1b26413da8034182fa42d2b1cd1f8745482c49d8348d19c72cf5a02bd28e4cba82128af8bf5d9c1215c4f543ef4d185f100f8d803dfa29c300c072e44ad9542b82fb1380d55c15c9a4b4398876e2450b90b49990746f339abca8cc8a462b62329a128758ce0e46b5f998af1bb485a3044bd125424eb5c623afd2a11befe4ec544eafe275ed1ad82b940dad5e9a9710d48562e51b296ba81f2d70593685ba0e3f3b25089a187e61d5675dd481aa99620276cb0a841a3c4df201a929287b1127270c5d25d06fb286dae1a9a5a5cdb60003f0c30d2021074bf252e550685f7b51a087a77b0871e883104e55f898aa5bc4cf8538c293253a737556d7f220e15b90cf0eda7d5f2172372e3c50c12cc588f312da37191b5038e944825044b130bff281ecd47a4252a1411ab7a9305c2b37e9facd435e9c434de37641498f8e4bfa7b42966da29c84200aca87ea1c3b00db54906b340e524a7dc4a15403bb82bc24517cb91026096bdf18f5f7ae5640ed6de1f0c5d184813d6b9d244b32b58e9ff524741a39383eec3a530d60db13deb26e3523a725f0599b671b625c07002704fb600b77318417d2527537359d122e22a1f7581eaebdc19e65ba50bdda18ae08e9a8694fcb0ff1a2cd98d910dbcd52064c15a4282d67b278c72a0fdbf228abf6b519dd28ac21c57d1da4bb7ad5b5ab10da6b83132df1da79ccfc77fb45598bbd91ef5ab96d8a2ee148639a562debafffd05aa5d10939a524d7b2ab225cdb41decfc129208c372d51ca1db705bb1b9831e53ee00000000000000000000000002faf08011b29e60a38883a8d4434f17ca3d92161109f7ddd8799e64e86d6b8509babd1100061b100003b8c1771721551fa88af8fdde92909add6e5b8fa90a4b0484ee065ed3ed7c56e733a7fcd775d4ab92956c1328ffee9c195004c5dee5dd5b9d0f9034589e6769e72579d3d5837ad70785ec420b4a24c04d36668728c0d2534ff1feea9aac2410423fd79c7db9231ee7efd3d585646e378fe53d731d18f38d6356a970f5c3026edc849d49ff34e58dfb7548512461110088ac3aa800e10785029ba3b0e9ab7bde0f056939e4921792dba2f5c005135daf57e32cae06a9ccb1b4d321f3ba015e5b92def1ff1c200e56b3990d82570586bfae26e9398e17dc6c069f92d80e6dfedf6b2f24b1dc3cc9d63e684d861f40fdbf508d4ac34b7f10c57be2a9b0c5921f86869c29ada5394b8780d2488a4fca3cd98ddb0ff8ea4415a07caea436682835744e94d5cff6d3024a9525dbd697e499b7ef23062b18b225bfaa4c5bb07166f34ff7866ec8f0fbbc12f695c609692798364fa20bf7977e321deda3fe5510833494532fba94fc1f0dd14ec74f3e9fe8ee659634621b63d16d46a8958132c24bd82c516bdf9ae9515cebae42778e4de6be7047c31cf86c0df0306f7b6562e1f35be51e5e64cc6d9d4c010849e6ac7ddacaa4b7b6fb1d35aac815964090940e73a1193eece11c1c1d37e373ef58c5e2d690b6ed6338360af9906146da9db8329bd2786bbf92df10445ee093f0b1b2a640cc2daf003fa7141435ba1dd54f9cdbf5417fa7f539b255452852a85d2ce97ce5abed4980e7b409e283f97ccc9c01e104b55155f96ace6789f61c4661962d34fc5d7e6f5f5233180933b2fa7f7a5b074714645489f5221966160946b7bfbf0fe6733e6beb8af4457b9d36cde1200811009ec483a9d730ca980aa28f636942af5e89794a8edbc1b75d555ba134974374d0fe23d31c26566064eb9998d649bb2bf066bf710da50672f4e3ab4df843a0c8942bad0a071c237d4c1759eca37380919e36aec73284db202a32d3d1619f3e5b757b2df8b04bde567783dc8e465d996799782f1a1b8de9331681a35aa04edb427de87264c8ae9c397f29d3e8730db91256425a10b960a9de1a48d0d4186d617d2b69c87e2540f6570faff4ee1f6303d7d281434947abeaad83c86a4d25bef4de2bb3c6104aa0ceed7c8df039f4be6a42851a118adb1b8f98e02f6727b75d98541bab2ff24fb2f20342e86150c678941825409b62a844f44ca1ccdf0d9f7c2cf9b222fbed00bc92be0802fbfbbeefa71c8976cba8fc4aeb031480f434027b1cd593d08cbc14c2a360b736b06b5afb8da35f0be3818fff4275b8c830f5248a8b8edea1327454e1360bd90d4fa08e965f459b0b027e1180290cf762f813a31e8109f472d9657b03af737d1f7bd2e59441541a84ba818f1413c5cd1f8b9882e9188e0def9e44e2f4a7c710c893c7188ba86423f8ae86068d84e1832af548289e87c34d68b186df7e24ca5b051f8f5e4a44e2e7383ba2a09615b4147b34e86486731290ea67f3be24c13a9c5cc37f06555989b3f10c580a9cd2b416d0ee4210855c6833a25996761dfabb036f3893cff7db7e310baa8faa79f46e0ee43bf4dfd732eae7f44bad2e7c032b9c6d14947af6b0e37e5ec98372a622f716ffba0cde04b9d4508392dd154ddc34829412bfa604d4f00e4b10a553587343ef5c0944165e7ee1e34387b09c147ecba943cf36dbc4269efe50ec3a5a3075c43be9651d6db6acb9f657476952b78c990557f05935247a71077373ec436ec586def177448f8859ba096b7a838e5b4ce7a463f9082f705c26d99936eb1be584ea9b58a44b9b4faa07fd8247fa66cf4529d1b8cdb92ed7bd96bf0968db4376489c7d46f0f27d58ac884c29736502953723ef1ab41e19c7041d3e0e9091d7de2e3904d032de02292edb1225a672ab438d3c65f7921c06a9f181f8ffda4ac524d0e000fd05aa5d10939a524d7b2ab225cdb41decfc129208c372d51ca1db705bb1b9831e53ee000000000000000000000000000493e0f81fd474bea478df0202b311c69e85dc6215f629491dd15ad0929faae2535abb00061b10000370b2714d2734e6b8cde085794dd7b41c8a9b6c03c1edd8e3db168ee7fce39493596e882b18b5b1b79c16400c6762b9856075821be6be9fab8f469f56820d8f341554400a8da7f1f1a8501902581d43b9fa6e5c68015716f718a2190b87fce41bf1b509aa61806394a42489d63c457fe4c79e7480eced1315edd731887e57704fc9102f50cb7f0d242d755cfd5a2172dbaf7f01b124861cc6d1dc804796bbb84165c805f0c0f3fb9ec97b74c2a694de56a9cf8d79d1a679260ee1169d78214b34c8a654ba22e59ddcd32beff4713de33549f035b342660405b0159a7a508e5691ef4805689140e72b8a0ef2e61be74dea5d0b8f5589e0e373cac2e2e1cc39b2121c05cf4122ad0f8b9af6fbf1de2ea26376c2650ccd306c13a7b64acbf2a3feed128754abe44658009e642768ae3d84f5e0fa5f7f360c2a1c76d26985817ae77b71fb59014a5483ebba9271cafa5e5d8031c569adeceb8bae6444e98d2522b28f6682109fc7d31cdb83ebd45e5d81e7f046df42345b49f470dbef9ed87709301d2c6131215d33a30b8d18e63e54a2aff85dd57672f8198bca6a67ee147c7d0ae649e5661ab6bf78a662fef9a164f1e332b9f16e6fb3d5769ddcbc1d1c07338d3394b9245d17618c2474e86c064fca4df00ad3a93dc051fd8c3328cde2a987798b0f22a21c90426700abeb1e6f38dffb485b5477ec44c690fa80e317b32a982fd3082253bba8595783290dbffee4fc9296ffdf16a8bf3154971bb720e78674969e9db2e0fbab9e9e13f24bc8b3af5e2f00f262f0da56de443f70398ab68f747d35370fcd8e1c0e130f7269e08f862b5a67f2c129be254df2358762ce3a947eb27d66450af51540e7721b47c8a5a86098ea64dad381f14e07aabbbc470949a99c07612add3ab4c575fe2e520bbe511a1a674aea37a44535c13ee3380f8f39bd230fc1481cd31912af36c6751e23c6f383cd37a8b13fa7df9f0c7e460739f2c6226638ee14f14d36366211cbc6a1e16b4856bf302a540aa9d9e833b1d59c510473096384c8b450f2f3f1dab9e614af822949d5cc93d76bc4d1a52891bc85f1981ef83161195ab7d8181ee4fb163bc6c685a10e87c7f4b15ed7d05833c230a4a5b63841fc65b959f0ff010e697f47c583f9b7fa9b389c0eff6614e47d85b83c483136f182be4c151d272f5d938b912a95e47d333e5de6a409ad271679a778a7eb3f169c71525302fac5d4575e2645c09763c2ef165736a7a726ca605038e2781404328790ffaacef2b9c2bf90122042cd571287bc4e3973da65fbd4e3da9e40e4347ca6eb4ef1ffef4e5a34be80425cae3e81533f7f2953f95fca53a22057a39125f5c76350fba7fc6c036838fb951d0aa8702e7f44c6f8a9cbce3b64fa8ddc2bb8c8b35d1e29a21beda6fdd332b31a749321455277231fd9d70ea4aded95053b395f88fa6916d126e1626fc0f1be6cd2a9538d17c498b40927f12b3bb40fa3e272e82cd2242b670afefa387470f4e6e0a1236028954c9e90311f486617187956a23b90b356d71e219e6dd055c2120771003a6c12769aa3ceacb9642bc01022731ca7a413b68ee7d1d5444f75dfa51a68b74a01ac85f6ceaf5e56987b9d67d6de896f5aafd25c78c413a6d4b5b03d571167524cd231ba13bd9f80fd7413faf21e8170cef0d08b242c5c38a2b0158da56e358ba0692f670d4611c7a3624b234adc30c5b7198e0afc941f5d13eae3a94ddffa652c784c34c582e04e948da91a5ac3038a9df38fd4f1733779f4f122ca2d7ff9d03bac9def35d9ee3a183161f8f2808d472b2e64581209359cea58ca7757164c666029982223877e2b14d2d537afb012f1ffc12cd083c16dfc64213c56f3d4d22b603d3dfab1d21e239d6fc1f9f153ed61f1ac91c29c85c16f4aa2985f84052f5a08d32bdd000027100000000008af34a0000000002c2322200183cab01341b3b937dc48c2d5d70e119b4fd5b4dc6d85e0bb49e98f1fe4ed87027bb80c7d8cec36237511da378ad5c121861660506bdf411240139e49a93e13aee25d10939a524d7b2ab225cdb41decfc129208c372d51ca1db705bb1b9831e53ee270248e28af5950cd59a64545f84957e4cd869de45b4fb50b85313894a1567c134666207e5bec5803b71d6fd6a9cfea3e6ea9a8dfe645a7f4dc7c9cd6c4ce15a0002547bca65416a28af342589b5b771ce54464b17b2153b49a1d34a2ed0b0788ec37083e54750e6f368b4a40d18a8730522e8f23901ac856b697dbd91992d75e097e3f175514453a79521d93ca8b3ffed47cbe300afdd75d1dd23cbe0c14cf67b3856fbe2128b9b7543625f1da313516f7e441255f85e341371fb7f33c6833abf02000000000000000100245d10939a524d7b2ab225cdb41decfc129208c372d51ca1db705bb1b9831e53ee000000002b40420f0000000000220020c57347ca54a9e6f279f6a1f1e50f19b48289c3baccdd356fb03033ae7ccb6e444752210252a01d9d8b03db1a99b1a82223f7dd60ecffa69dbef47c06228fdea2fe0f0215210322a747c1d7f77fc7577a689618bbeadf28b941412404ac5e216d684a32d57a8e52ae000100400000ffffffffffff0020cd6d4a4bf51a8c36a25cde5bc08188c5ae037fd9ad9e92b400bb3d10e473b55480007fffffffffff805d10939a524d7b2ab225cdb41decfc129208c372d51ca1db705bb1b9831e53ee061a8000002a00000000883a86353a00c855b5caa13998033c04330f88b88e084b3c00f228299e5554f0b66e9d5c630e194cd572acaee6e5124b612583b9722ccf24581716292785c4925c06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f061a8000002a000060535ae20101009000000000000003e8000854d00000000a000000003b9aca000000" - val dataShutdown = hex"0100230000000103af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d000090cc78408b6ea14d14bab8c89a4e1e0cbdc4cb4645f9c3a6457b0bed5788c389a80000001000000000000044c0000000008f0d1800000000000002710000000000000000000900064ff1600146663df19ca3fcc1c04447b18d2cd795c485f19410000186b0200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000028a82039dc0e0b1d25905e44fdf6f8e89755a5e219685840d0bc1d28d3308f9628a358500000000000003e8ffffffffffffffff0000000000004e2000000000000003e80090001e0386998d8c3ecc235c80eb4605ec24c5192d27ddef57d204227d3ac0c54547312c02c02cf88a39307d6e9aa4ba00bda029ab3cd426316eb63c186875c3a5d80969500381d707ac8494a96e1e8b8bf4a3a703a78358c69856f4741dda173d9f9448219303d6ba71bae191ee8d282e57d3a7793919d621b9e2df77dcafe45a5cdb913784f80203212eea182f9f5d5666a54b3e96a7525a48d5b9ee0f9a160d13926480cb9fbe00000003028a82000000000000000001000200fd05aa9eb27b82a5de32ca44c8d8e38e705c62f5f4d3b945dfb5f03973dae05fa4388500000000000000000000000011e1a3000a00cedece5db1cf150c39b679dd048140f491a72e93196f9e205841a23791c500061b100003f2b3c9277a3dd9ee0fd08afce183d75f570299155a8dc5c1c5acc79a572629c37ee817bee5023531eb3929c8e95025deb4600cb6ccb900176a23ed7cc35f99f7266ec8b0e59f8e0c0150916c2533133bfd105e883ed7b33915f7a532e36d46db78a2842d7e99f1f8abc863a5f6f8866b130025610795266d8a9603a8ae11ee23ca765f9a82ef00686612da9f70c2b2c2ba242bcaa9e3d95e542cbe59461a75e5a8f01553da2552d363593b8a85bc546f17f8e8bdd1449602fe3f46ed566e42a4155cf0b6c26df2cc5a8312ebebaa6bf11df9a55aaad2a96cdf41d7e70e815b7457c9332cc58ae9de3b9f52da71e7f328afa8de7777c76ea82e17e4a2cb46c3669ccc5f644414764019bcb2d4069c3e7a29814f0462abe479804d4dcceb498f1da52d5796f69f7d1d235e2da967e8683f77e7f206034ab12c02aba8554ece3444f8e0803df9ba5a9b1b918f13d94fd89235e1e2ec32df3154b204bb7642d33112ce70ef5384b11cf07bcace83a97339e1f9fb33e4b2cf1ccd62cb5311ab2124a8ed33a6c95c49ec90d9a477f3556d098a39235e8f1b904ed7aec0052deb3173f5d85126bf90271c567f3e9a10c12bc50f37f41ae40b49c71cf74cf18cb6e5eac206cc596349503f913178dcb3ee4ec5410b30474a4a2831b18ac07cb78fc143e056aee802aaa8bfd5d97e403e718069b221e3effcd57accfd13bb61d4eb78e98f269e79062c414a9e0b39075d8baf371c0fe09aaed436445d62664de1a96e8c974a3fe0cc045dab3064c7601dda9213d89b8fe30437f0b5cffa296bce9c141e3c012502431ff0cd5cde95eb52f9dc78ce3fca95b6a75de178ed13f9e3e7610cdbd1a1a893dd635e07459aae22a8f8c5bfc0a33a1f88fdcc1b99173080b602f91f1c9549b235e21bfc4ca071e0070f0057d65d98343cd82035970f6d75fc9a4b191270d83cb0dc6aa84238567e230bb68d16b0bba3563a28d151b308a74766eac50d4b4b287e1f46230ed1c4e8259cefdbba86c367392097909a740f692b745bb8c4809115ddd42de543dac65245faa6870f25aa2df16118d6bee2be181789ab64e5ba727048eeda81e6a90995c681924b2f807554401b6aef9436a345253be21747c313e8d9e2bf048d890350c055016cc61a06dc082a2b2c1b038405248ce18bd5037afa0e04b704198d6129da78ef9b97a8cee66a1104acd89c2bb2ab57d519f6693fd94fbabbca63fb9bd29decd9dacb34000f0f40af6969577f37a9aa38b4f2cb3d5d974a3d53440e78aa7ba013ac9a242be930e819ec36a96850cd7c630ea102fab614f81a4c9b9a01e60aaf9956a4f109cf5496ac8a43d03b0802303a6fe515199f9dfd4e66c2eac124d4653e999702832073f75ebc177fffc3efd404141927b3635980a40ee6ae50ff14010ad8e21493a38e4930a82b9d636d7115a55511bf4296d80ea8105a07f65730db9315a084600be9a3dd2b19943fa7cc85d2293de47f7799459288caa8d40c55605b5abfdfbf1325d0ea90aba749b0a625847f4b006ff34b6c9bd14a1c107c8a0175983ad10ae47a285b7d503e870625711fe3e1557377433ee845e2254c538304a97850e5acd2d4ba14241630b026230ed508b84f9442d6fb9a45369e2cbbd0d494cfa2573d4a0e553c0013ecb33d44f2395e4e4a3f752b8f86955061eecf4a9f9fed27648446369a8dcae2e6ae0b51b9a4fe315ad5c7521764738cb591734b32a8e35c1d6c80d9202aacdbf65cc243ba0640f8f8c23d637d7331c5b9f4c44fa5f2858cf2d74937beb4b345d22c2614bb6346f38933f84aea2bd5456505709fd8b5335fbe54e99c351e151f1f209dcac9e37742d3978c9a95eed0b29fc0b4150229a0990f465bd3bbcf97f73237961042359aaa0f11b1bc4c572de165a6372f3f438e7071a19700fd05aa9eb27b82a5de32ca44c8d8e38e705c62f5f4d3b945dfb5f03973dae05fa438850000000000000001000000000bebc200a043ffa6f358e330873add2e0d4f05a72150cc831bb2f1062e5c378b9bced8e800061b1000025eacbc7939a2307c66fa4841480bc6c87f041d1bf722da78bf13983d485df3fece09e54b1fb1534dee2931eb75f0bb95f21e924e565e8e5b06dd16a9fd4868fe60295bcfee2f6c53481a13c9e49d468a8c6992aca5464619ee54f052f5cb2a6a4ed6abeb6951d22ccd0662816882497d6c8fcfe64152e6a9b6ab15bdcb43545fc3f2fe9caf2679f5705fb68b90f8dd1579b9346901dc16923d222125652c8b15b5e003f660fc1af452ee0edb61b67272a3369ffeb3671f2bfacaf3b9452db611760d95c734151639733adcf7dcd2ab7a298d7dd0d31c61004be368b9858d19907eb76b421e3524d83d4a2dff76c73abc0de61114f6c7d61622074e719b729c52e9b11f65e8260e90f66e71de5686c89da73e9b1001541d621663c3613eb4ce2faf7f9ae94f0f17047e3b283baf98145936ff491c6c031a2059e2c52a7725f0790943bdd21ecd3e237000fa8bf27e81343902489101dd96f2a9bd6cc1cb9c4f4a9bf33ec39b0743901f8bec541fe9090d984d9c071eca64ee9fb4996564cad02923c0a662db9b42d76bacb09314c6bae42077ddab05905a02f3cbab41fb6a9e8c1ed2b27b5052ced38f4192cd97e0b9747f219f2484b46add32e937fd7f8fd8098aa2147e3e1400226703860766549868d719a8c372fad4ea7c8256908ba2865c68683a2cc28436b1effcd6e5ad990b83824181fc80a576f8906a45f56ce7e3748ad4a4385143768876d1e73ad962eeb6a2466f42573da8cf4a082997ffb87efc7f2711b92af0b36a72f1157c0df404aef2f958b32100991574ef4593f134d680e1a87729a577d7432b3e2ccfde6fde5334cedb8b601a07932f2f63835705e480e57f92b51e4a08ec44e07157bf6f80095fe908637d569708779eaf5b9be3c33e0dd9b840c3e1ef3cba0a0bbb868b4f07b5abb934fcf5d47d3511941a32e54ab5d4c547b81708dd5a876d3667f8447e6819625f49ac1e8f8bfd9df2b190aa318927d212c964a5b8b36c2a94d7543c8d94c4e2a1a43824cf652873ddbbdd321b473640f0982cbcea32288c6ecd2272e57cdcc20a9bd60a760de289bd86cdcf590cc7118e407ecd369412e544cc1d256e177fadc3fb15d8238199782aeadea3f33558dd68ff042d9cb440f556132dae1fbcef3e693691b089939883ca0e8c94a630d45c6adc33201cb274ee3f3225cd2a835b4b83e0a319eba92cc2bf728f6e5bcb786ccf8ca7c42e6fd2a9d7941580a8f7983a6d9d88ffa6ada598ef4571d6d5ff67917f43bff4df2330081244bd3fbaefb21b6a8d14ab81846824379415c186726d20afcdd298b921bb850eab4bdd188d72dd42d6f1a211b580e0ca831c2443f7f50eb911ac3b3100513be666b3a48be024471f92e1689909392670c0f3d7f937ec318ff9a1f1fc147597e49e735978abe5ebc1c1fb6474acbb610af539c57a248fbaa8909587418a15e536a53801736ea27b120e43a070a8e2f402ce744d71c2f81caa3a23a669462c1e0012009c0201b9cfc5b01a78b26c2acd939c1ebaa5e127b57c58078a79dec8bc206111e4cba54b645d0dda5e7cdeaf1f9e6374037f8ecc7d72d2802b0a5981f48c75b1f15ade54472450e0f16736902702e915117ba02344ed0d58bcc9f2e03fdcf6907812a3655dfc07602144a16e5e1cbcf3629a8bb274f8c1652db6cbe12bab258d574181c94a8972392428a0bafd1c021bfe42df0a942f87c112c8bc340bd518b3cce475e481b7006e266065ca620d70923dadcc7518df79198990ac8e7d99f066eba8bcfb2992515f5cbf2da8ef55b19fac4418309cf7bd237cb69b1d7c6a3c6b456f1ee228b3e9fbaf1b3cadddfaa307039dff33ad59bcfe44851c33fd09346e5791adf0b0a846326266756fa5a035a840e79ff0fc1735b04a4f611da1c29f9ca64a0a9d1d07bd000027100000000011e1a300000000000bebc200249eb27b82a5de32ca44c8d8e38e705c62f5f4d3b945dfb5f03973dae05fa43885000000002b40420f00000000002200207f89b60332c0a813268782be20d14c67626b76aa4832e1b9726c57a01750bc83475221032edd70e88cbfcda8c91eb9b20ff6a2c569a52de908933b048bf6b8414d7667b7210386998d8c3ecc235c80eb4605ec24c5192d27ddef57d204227d3ac0c54547312c52aefd01af020000000001019eb27b82a5de32ca44c8d8e38e705c62f5f4d3b945dfb5f03973dae05fa438850000000000d2a9188004400d030000000000160014a203180611650c5e77e05d64ff0d794aa4ad35ae400d030000000000220020fea2002eacc21b3490a177df4ee69f30c231d919b99e0b32bf518767169aa764286a040000000000220020f757d7d804dfbc35427f8bffb749454a114be5055a683d08ea66581d1e51dcfce093040000000000220020e518e9c7e70e16eb89facff2af3f233e1af45d8a6d04ae7f36f4fffeadcf3e1604004730440220203be2c76c9d051b47db6da5a99ce689eff53ce5bf016f1d9ac403ab7c9fc9a40220290da75c0701cc212ac502d3d894801e92a701a20a4e85c3f640f579f96762e001473044022006c47ea4c15c389369ff75d25b83770396920a1f06a2c468ce56a32f9cb7cded0220759b6227e0da03ab542187f482b9f6123c93e3187070de646b07547e7493e8ce01475221032edd70e88cbfcda8c91eb9b20ff6a2c569a52de908933b048bf6b8414d7667b7210386998d8c3ecc235c80eb4605ec24c5192d27ddef57d204227d3ac0c54547312c52ae08758b200002000324cd3bf6d98e9f7f88be0e61167f9797164b578df264206f79a96e104cf4abd0e1010000002b400d030000000000220020fea2002eacc21b3490a177df4ee69f30c231d919b99e0b32bf518767169aa7648576a91490767756314724028f101050cd76819b6a1756368763ac672102551ddb3bbc7ea3a02cadba123a00b171d7b75ee46e7aaba9caf112012c77ab587c820120876475527c2103b89e41f7c1cefc7864881d9a78f262a9077a99cb05194684e48591bbffc0a5ce52ae67a9148acca5ea2fc12ce9d71ece802b1f500a7e84378d88ac68685e0200000001cd3bf6d98e9f7f88be0e61167f9797164b578df264206f79a96e104cf4abd0e1010000000000000000015af3020000000000220020f757d7d804dfbc35427f8bffb749454a114be5055a683d08ea66581d1e51dcfc101b0600402470f4a38f40a7d71f39f9552254c007ba5557adbec6ffa7176669fb309195d75a33b4556e6f35536757085c24fb83c09ecc1cdd976cb8d2b0bc679d461789d040db8c19bf05d1d55cbe50de7ce29c9388bb541f6397dc43f9452a92a931c76c42722ce6ad02d0343dde9b692e001349d9b1e075527f1a8d3d1aa90fc72164271a000324cd3bf6d98e9f7f88be0e61167f9797164b578df264206f79a96e104cf4abd0e1030000002be093040000000000220020e518e9c7e70e16eb89facff2af3f233e1af45d8a6d04ae7f36f4fffeadcf3e168576a91490767756314724028f101050cd76819b6a1756368763ac672102551ddb3bbc7ea3a02cadba123a00b171d7b75ee46e7aaba9caf112012c77ab587c820120876475527c2103b89e41f7c1cefc7864881d9a78f262a9077a99cb05194684e48591bbffc0a5ce52ae67a9144976a85ce7e37d8557d74cdd86254926d991f77b88ac68685e0200000001cd3bf6d98e9f7f88be0e61167f9797164b578df264206f79a96e104cf4abd0e103000000000000000001fa79040000000000220020f757d7d804dfbc35427f8bffb749454a114be5055a683d08ea66581d1e51dcfc101b06004062a64d249f7301aa354b7951f0c7b13882b7196e7985727395c624bdb84ab9234a5e0edbff3b5a5b12bd385a695722be5aef598ab36ec1eb605e944964a1adbd4061013fe37fb5d1ed7f8d8137501a755c195bd2bdcf345e30cac31c2962f4e0aa0614d7f96b9c49d8120089f2d24919ddf40bda3af04312f7d5255248075d28e200000000000000010002fffd05aa9eb27b82a5de32ca44c8d8e38e705c62f5f4d3b945dfb5f03973dae05fa4388500000000000000000000000011e1a3000a00cedece5db1cf150c39b679dd048140f491a72e93196f9e205841a23791c500061b100003f2b3c9277a3dd9ee0fd08afce183d75f570299155a8dc5c1c5acc79a572629c37ee817bee5023531eb3929c8e95025deb4600cb6ccb900176a23ed7cc35f99f7266ec8b0e59f8e0c0150916c2533133bfd105e883ed7b33915f7a532e36d46db78a2842d7e99f1f8abc863a5f6f8866b130025610795266d8a9603a8ae11ee23ca765f9a82ef00686612da9f70c2b2c2ba242bcaa9e3d95e542cbe59461a75e5a8f01553da2552d363593b8a85bc546f17f8e8bdd1449602fe3f46ed566e42a4155cf0b6c26df2cc5a8312ebebaa6bf11df9a55aaad2a96cdf41d7e70e815b7457c9332cc58ae9de3b9f52da71e7f328afa8de7777c76ea82e17e4a2cb46c3669ccc5f644414764019bcb2d4069c3e7a29814f0462abe479804d4dcceb498f1da52d5796f69f7d1d235e2da967e8683f77e7f206034ab12c02aba8554ece3444f8e0803df9ba5a9b1b918f13d94fd89235e1e2ec32df3154b204bb7642d33112ce70ef5384b11cf07bcace83a97339e1f9fb33e4b2cf1ccd62cb5311ab2124a8ed33a6c95c49ec90d9a477f3556d098a39235e8f1b904ed7aec0052deb3173f5d85126bf90271c567f3e9a10c12bc50f37f41ae40b49c71cf74cf18cb6e5eac206cc596349503f913178dcb3ee4ec5410b30474a4a2831b18ac07cb78fc143e056aee802aaa8bfd5d97e403e718069b221e3effcd57accfd13bb61d4eb78e98f269e79062c414a9e0b39075d8baf371c0fe09aaed436445d62664de1a96e8c974a3fe0cc045dab3064c7601dda9213d89b8fe30437f0b5cffa296bce9c141e3c012502431ff0cd5cde95eb52f9dc78ce3fca95b6a75de178ed13f9e3e7610cdbd1a1a893dd635e07459aae22a8f8c5bfc0a33a1f88fdcc1b99173080b602f91f1c9549b235e21bfc4ca071e0070f0057d65d98343cd82035970f6d75fc9a4b191270d83cb0dc6aa84238567e230bb68d16b0bba3563a28d151b308a74766eac50d4b4b287e1f46230ed1c4e8259cefdbba86c367392097909a740f692b745bb8c4809115ddd42de543dac65245faa6870f25aa2df16118d6bee2be181789ab64e5ba727048eeda81e6a90995c681924b2f807554401b6aef9436a345253be21747c313e8d9e2bf048d890350c055016cc61a06dc082a2b2c1b038405248ce18bd5037afa0e04b704198d6129da78ef9b97a8cee66a1104acd89c2bb2ab57d519f6693fd94fbabbca63fb9bd29decd9dacb34000f0f40af6969577f37a9aa38b4f2cb3d5d974a3d53440e78aa7ba013ac9a242be930e819ec36a96850cd7c630ea102fab614f81a4c9b9a01e60aaf9956a4f109cf5496ac8a43d03b0802303a6fe515199f9dfd4e66c2eac124d4653e999702832073f75ebc177fffc3efd404141927b3635980a40ee6ae50ff14010ad8e21493a38e4930a82b9d636d7115a55511bf4296d80ea8105a07f65730db9315a084600be9a3dd2b19943fa7cc85d2293de47f7799459288caa8d40c55605b5abfdfbf1325d0ea90aba749b0a625847f4b006ff34b6c9bd14a1c107c8a0175983ad10ae47a285b7d503e870625711fe3e1557377433ee845e2254c538304a97850e5acd2d4ba14241630b026230ed508b84f9442d6fb9a45369e2cbbd0d494cfa2573d4a0e553c0013ecb33d44f2395e4e4a3f752b8f86955061eecf4a9f9fed27648446369a8dcae2e6ae0b51b9a4fe315ad5c7521764738cb591734b32a8e35c1d6c80d9202aacdbf65cc243ba0640f8f8c23d637d7331c5b9f4c44fa5f2858cf2d74937beb4b345d22c2614bb6346f38933f84aea2bd5456505709fd8b5335fbe54e99c351e151f1f209dcac9e37742d3978c9a95eed0b29fc0b4150229a0990f465bd3bbcf97f73237961042359aaa0f11b1bc4c572de165a6372f3f438e7071a197fffd05aa9eb27b82a5de32ca44c8d8e38e705c62f5f4d3b945dfb5f03973dae05fa438850000000000000001000000000bebc200a043ffa6f358e330873add2e0d4f05a72150cc831bb2f1062e5c378b9bced8e800061b1000025eacbc7939a2307c66fa4841480bc6c87f041d1bf722da78bf13983d485df3fece09e54b1fb1534dee2931eb75f0bb95f21e924e565e8e5b06dd16a9fd4868fe60295bcfee2f6c53481a13c9e49d468a8c6992aca5464619ee54f052f5cb2a6a4ed6abeb6951d22ccd0662816882497d6c8fcfe64152e6a9b6ab15bdcb43545fc3f2fe9caf2679f5705fb68b90f8dd1579b9346901dc16923d222125652c8b15b5e003f660fc1af452ee0edb61b67272a3369ffeb3671f2bfacaf3b9452db611760d95c734151639733adcf7dcd2ab7a298d7dd0d31c61004be368b9858d19907eb76b421e3524d83d4a2dff76c73abc0de61114f6c7d61622074e719b729c52e9b11f65e8260e90f66e71de5686c89da73e9b1001541d621663c3613eb4ce2faf7f9ae94f0f17047e3b283baf98145936ff491c6c031a2059e2c52a7725f0790943bdd21ecd3e237000fa8bf27e81343902489101dd96f2a9bd6cc1cb9c4f4a9bf33ec39b0743901f8bec541fe9090d984d9c071eca64ee9fb4996564cad02923c0a662db9b42d76bacb09314c6bae42077ddab05905a02f3cbab41fb6a9e8c1ed2b27b5052ced38f4192cd97e0b9747f219f2484b46add32e937fd7f8fd8098aa2147e3e1400226703860766549868d719a8c372fad4ea7c8256908ba2865c68683a2cc28436b1effcd6e5ad990b83824181fc80a576f8906a45f56ce7e3748ad4a4385143768876d1e73ad962eeb6a2466f42573da8cf4a082997ffb87efc7f2711b92af0b36a72f1157c0df404aef2f958b32100991574ef4593f134d680e1a87729a577d7432b3e2ccfde6fde5334cedb8b601a07932f2f63835705e480e57f92b51e4a08ec44e07157bf6f80095fe908637d569708779eaf5b9be3c33e0dd9b840c3e1ef3cba0a0bbb868b4f07b5abb934fcf5d47d3511941a32e54ab5d4c547b81708dd5a876d3667f8447e6819625f49ac1e8f8bfd9df2b190aa318927d212c964a5b8b36c2a94d7543c8d94c4e2a1a43824cf652873ddbbdd321b473640f0982cbcea32288c6ecd2272e57cdcc20a9bd60a760de289bd86cdcf590cc7118e407ecd369412e544cc1d256e177fadc3fb15d8238199782aeadea3f33558dd68ff042d9cb440f556132dae1fbcef3e693691b089939883ca0e8c94a630d45c6adc33201cb274ee3f3225cd2a835b4b83e0a319eba92cc2bf728f6e5bcb786ccf8ca7c42e6fd2a9d7941580a8f7983a6d9d88ffa6ada598ef4571d6d5ff67917f43bff4df2330081244bd3fbaefb21b6a8d14ab81846824379415c186726d20afcdd298b921bb850eab4bdd188d72dd42d6f1a211b580e0ca831c2443f7f50eb911ac3b3100513be666b3a48be024471f92e1689909392670c0f3d7f937ec318ff9a1f1fc147597e49e735978abe5ebc1c1fb6474acbb610af539c57a248fbaa8909587418a15e536a53801736ea27b120e43a070a8e2f402ce744d71c2f81caa3a23a669462c1e0012009c0201b9cfc5b01a78b26c2acd939c1ebaa5e127b57c58078a79dec8bc206111e4cba54b645d0dda5e7cdeaf1f9e6374037f8ecc7d72d2802b0a5981f48c75b1f15ade54472450e0f16736902702e915117ba02344ed0d58bcc9f2e03fdcf6907812a3655dfc07602144a16e5e1cbcf3629a8bb274f8c1652db6cbe12bab258d574181c94a8972392428a0bafd1c021bfe42df0a942f87c112c8bc340bd518b3cce475e481b7006e266065ca620d70923dadcc7518df79198990ac8e7d99f066eba8bcfb2992515f5cbf2da8ef55b19fac4418309cf7bd237cb69b1d7c6a3c6b456f1ee228b3e9fbaf1b3cadddfaa307039dff33ad59bcfe44851c33fd09346e5791adf0b0a846326266756fa5a035a840e79ff0fc1735b04a4f611da1c29f9ca64a0a9d1d07bd00002710000000000bebc2000000000011e1a300f36f3bd94a6751ab579d9481cbe484dd6b1d2abba32bcc757aea07e70d0ea28c0226f5d9131a28e7d24ecc1b7fa2b63b34a379879f645d8e3346aea28f9ab603eb00000000000000000000000000000000000000020000000000000000000200000000000000000003ea1865f4cd884edcb9921736b2ae36b50000000000000001000384fe405b653143169925b03dd2cf38bdff034a0fd7fb350f7c089c1129dfb82cc772d5525d0b922cc3d989fcaec7c16b03c3249eb27b82a5de32ca44c8d8e38e705c62f5f4d3b945dfb5f03973dae05fa43885000000002b40420f00000000002200207f89b60332c0a813268782be20d14c67626b76aa4832e1b9726c57a01750bc83475221032edd70e88cbfcda8c91eb9b20ff6a2c569a52de908933b048bf6b8414d7667b7210386998d8c3ecc235c80eb4605ec24c5192d27ddef57d204227d3ac0c54547312c52ae000100400000ffffffffffff0020d22458cead349cb596ccc159d17f2809dcc22620268d1a300a6e152547f7235280007fffffffffff809eb27b82a5de32ca44c8d8e38e705c62f5f4d3b945dfb5f03973dae05fa43885389eb27b82a5de32ca44c8d8e38e705c62f5f4d3b945dfb5f03973dae05fa43885001600146663df19ca3fcc1c04447b18d2cd795c485f1941389eb27b82a5de32ca44c8d8e38e705c62f5f4d3b945dfb5f03973dae05fa4388500160014d5f0a47e22dd767bca0be8f6d109a81a8815ff33" - val dataNegotiating = hex"0100240000000703af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0000928544434bbbda1da0790cf138ef4b3881f5cec34b933ab77ffd57a7e12992bf780000001000000000000044c0000000008f0d1800000000000002710000000000000000000900064ff1600140cbd801be794f9854b38981ee859e3a000ad104c0000186b0200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000229a82039dc0e0b1d25905e44fdf6f8e89755a5e219685840d0bc1d28d3308f9628a358500000000000003e8ffffffffffffffff0000000000004e2000000000000003e80090001e0234ea57c6a0d7308e0479811f0315df88650da7a8af626fb6ceb9a6b3e1bf068e032f5bf3637d4efa39b50a7bad5e3f6d32663a1cc207a166155f00f8b9a9f621cc026b8899cafac94bcfee408aecfec60e86a30082b0ea587d8a7ab6b2b309fc66110210996c2725e4129dad191f6c6d9ba8c35ab58df4d340051d7025d366423aa15703bf59f021a7431277a31a53b2101bd3ff5730adefeae596020e58c9099728b7f000000003229a820000000000000000000000000009c4000000002faf0800000000000bebc2002424d4fd09fbcbde0363fda5d43c8bc68cea396aa1edd3d48d118ec27bb30fd1d5000000002b40420f0000000000220020552fd9e112e447f57c95b896d384461ee713c8108894ced2ea1272e0d354aab74752210234ea57c6a0d7308e0479811f0315df88650da7a8af626fb6ceb9a6b3e1bf068e2103d45fea036aa6817a71387c3976790beeb4705739d4cf0271007043854dde877552aefd01bc0200000000010124d4fd09fbcbde0363fda5d43c8bc68cea396aa1edd3d48d118ec27bb30fd1d50000000000c089a180044a010000000000002200207ba66732d8b8e8863cbdcad193c4602b54df24f9af202a977e3a6315e85072704a01000000000000220020f25aee697436b14ebdc0bdab0b3a0c339c98e49cd6a8b84fbfb84c7c5557aad1400d030000000000220020a659aeaecad3be965ed5a498adac881967d2d3ed2a773019da1b26337c7d0d1372270c000000000022002095c2df1035ebb714a728a166d88332e1569b3da5c035037df1bc07e227b7e1ec040047304402205837856dc4bc8f4a679015460580c38a29899986f6cca09afe3525097dcef36702201b2abd6fdbfa890e247cbe6a096bb01f3d47901a552f7f5e5c6ecb6c80a07d1701483045022100abfa35999608ea0c993418f7d432c7f9710b993f4eb45e17fbb95e10c6899e190220542593d102382a92a11eac65bef87a75742ee4447abebb38fc47c4d27d74f9a2014752210234ea57c6a0d7308e0479811f0315df88650da7a8af626fb6ceb9a6b3e1bf068e2103d45fea036aa6817a71387c3976790beeb4705739d4cf0271007043854dde877552ae68075c20000000000000000000000000000009c4000000000bebc200000000002faf080054b02aad4844030f2e1c3c61f95b34df8d14c0fdbddbe5d56090667016ca8979033d0000a1e29b94b3517a04106dc7bb74f4f091a2ee419d889ecf8434bcfdf127000000000000000000000000000000000000000000000000000000000000ff03228da9a93e02211af77afa576b94f57d747099099f5352298d8b3c4dbc7215c62424d4fd09fbcbde0363fda5d43c8bc68cea396aa1edd3d48d118ec27bb30fd1d5000000002b40420f0000000000220020552fd9e112e447f57c95b896d384461ee713c8108894ced2ea1272e0d354aab74752210234ea57c6a0d7308e0479811f0315df88650da7a8af626fb6ceb9a6b3e1bf068e2103d45fea036aa6817a71387c3976790beeb4705739d4cf0271007043854dde877552ae00000024d4fd09fbcbde0363fda5d43c8bc68cea396aa1edd3d48d118ec27bb30fd1d53824d4fd09fbcbde0363fda5d43c8bc68cea396aa1edd3d48d118ec27bb30fd1d5001600140cbd801be794f9854b38981ee859e3a000ad104c3824d4fd09fbcbde0363fda5d43c8bc68cea396aa1edd3d48d118ec27bb30fd1d50016001458043e6e40996eb9d3c77752a81398115ec43d5900010002db71020000000124d4fd09fbcbde0363fda5d43c8bc68cea396aa1edd3d48d118ec27bb30fd1d50000000000ffffffff02400d03000000000016001458043e6e40996eb9d3c77752a81398115ec43d59ac1a0c00000000001600140cbd801be794f9854b38981ee859e3a000ad104c000000006824d4fd09fbcbde0363fda5d43c8bc68cea396aa1edd3d48d118ec27bb30fd1d50000000000001a54fa574848f29f143c52f2717510e2a8831afaf7752217ce3452c7e88d24eb14905191e04229fc6433aef69bc33f9f2f6eb3c841fee628f7c583b9efe5a149aa47db71020000000124d4fd09fbcbde0363fda5d43c8bc68cea396aa1edd3d48d118ec27bb30fd1d50000000000ffffffff02400d03000000000016001458043e6e40996eb9d3c77752a81398115ec43d59f81d0c00000000001600140cbd801be794f9854b38981ee859e3a000ad104c000000006824d4fd09fbcbde0363fda5d43c8bc68cea396aa1edd3d48d118ec27bb30fd1d50000000000001708fc4039310f093081478d723e70b6bb1f17aa925472ccef2f79057aafce602726039f88ed9a0eb0841c61c39a5c224516288a81a53256693588f5af97e9e76c88fffd014e0200000000010124d4fd09fbcbde0363fda5d43c8bc68cea396aa1edd3d48d118ec27bb30fd1d50000000000ffffffff02400d03000000000016001458043e6e40996eb9d3c77752a81398115ec43d5942210c00000000001600140cbd801be794f9854b38981ee859e3a000ad104c0400483045022100ced76c39d6c63a52374b17103eba46ac6d21b5f5e427d48ffcd5e505a26477b90220588f30b94c0938632bcee8cfe9bc185325b2edde88c85fb4d226eda215abf26b01473044022042741816de3767e7b54fc1cdf94b6f0072203daf76c01ba5abb120801965413b02205237d5f3978b2332aa14ce65501fa784f51ea4e97030d44536f52f188f768074014752210234ea57c6a0d7308e0479811f0315df88650da7a8af626fb6ceb9a6b3e1bf068e2103d45fea036aa6817a71387c3976790beeb4705739d4cf0271007043854dde877552ae00000000" - val dataClosingLocal = hex"0100250000000103af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0000948d8efaf118cae9f142433a624b41a9ef7e09327fa45083ebd7694e1a61c429980000001000000000000044c0000000008f0d1800000000000002710000000000000000000900064ff16001401396601573e7aef81f91a89fc4b4b56feb7a35e0000186b0200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000028a82039dc0e0b1d25905e44fdf6f8e89755a5e219685840d0bc1d28d3308f9628a358500000000000003e8ffffffffffffffff0000000000004e2000000000000003e80090001e03174f3de90105ae02d88ccc1a7ed4bf0d116f27ef6010f1ec308273c1010ae3dc02fc17b2289973a29fd374ee24671d5d9643465a11bd725bac1d640b56493bd8f702b5460ae3379bc8a92d2aa2488f3e55a73d8909695e677beddd61a94d3655490f0357a1a37c3ae33b2a94bca0b6ae541036c22feeca86860c7c5501153ccfffd585028fb43c64d53c5d891bb0af3b7ed41acf3ebb0de59599b3e7836a8401645cb6e000000003028a82000000000000000007000500fd05aa0a007eacf337b3a80941f2defd530cf824f13cd263ba2ca194e7170072ce28e8000000000000000400000000017d784077c3e8caa53f1a775a5e89b1a54488aff835593d5e2b7355179136bbdd10a86c00061b1000020b7604aeb7cb080b12de62f75a292bed3134b4a3de764bd032fb049e2c2a1d3942cc11b9b12619d42cda3c7eb63da952644a3f355bdf772d77f1fc2a0b36bf116cdacfcda3b77974536cc3f3d0f8209dba69408073cbe0f039eba220fcf3b0b96d5875bb71acdc999fd9feeab68a0b63629efe0dc91e92dcf93b3c49285c981746a68ead7a96a9c1ce21284a2796b0051e6b7cf36a264d545063c054454b87cc3f9ddad5350382585de18cdc4e65e02484695f541b8fc082c87ea4a56aabc84fd1b7c1cd40e3517ae811f9ff921fba672e0191c88a2681ef8ea1300eeb6c1d8fccdb5ab05bf80b2a5c3ed5c018752cc841305edae02149389bc5e58ea5a59a1c59755c7ab5f35e3740c9f6218d227370363a7e386b214f5b2e771ea8b94a2a5e640d8a715d57756665f4a659f4ef6488f9ceab5e22d523ee52da368701d799fcb3459a31e6fb03de91ffd9b608656b172945015da3e5000f7f6c3dc0b4e102ba5f101bab5c2ceab8021d071bf89cf9d65d2439fe9a9dea4291eccb43a431904313655391ace4b82bf5da6270eb5bc12a60a69e7cf96b75a699db5a374713558eff7101bb2d8471e0c4ad0e202fd10aa46098bd5565c1d410e695fdd799c866090f1a7aa002305388acb68c8d82d588c1a66df5fb12653686bc4cf487a3484c0c23578a428de3df57539ed11227f2648bc36261c7f3f4f13e18d238dc856b8dd8f07de9fb73663d3a0026a03d9ee9f807e2a03c9546d2537a1f30a1f06767622d8bacdf4ab3b0f245a7b4f482baa60080fce3e7e15d2e086c670d6ec11d907e3da593977c8c25d620ed40cf82dd503c25f1a5f3e8a5ddb65ae7117600794844252e1448d17849028bd5df960a5fddf56e48cf082cfbc5c29666bca6af335c06f8240912a311e6c07946eeea9ef57e46b3e48b95c82cbee3830bf2b55d5e0223b9480fe072729a8af4c6c8c2f86038cb8a2e02405ce5f834c87b241fcb9963f008c273c64bc1f7289cc3a0c5eea7ea1a6dc7bc6228a97978a2ddd7dc49303c5beb15b36c8044f656716cad0a62d1e8c381db46f007be7f8096f72d3a8f08d338d9f2bbc43c0244215df229824a9ad34b655d840b38c5061462d54358be1c184b51d3a805beeea78047f544b15cc333d591f3d8cb60cbdd78e58ed47d21739762957ed6365fea14d7ba8368371fe4333be4f5ae289ff444c9f377a072a3a4173bd2680c6d6074743908d9c4f09aa30d064c7866a7a34303fce53deba6a597a24c211d209815bbff97736ba9fc79e390b36b2343d2590c8fd6db3e54206580eb7a53b9de09a42cfaa2cffb00ea90dbd07f1d357e69ca96fbba4efefc4e07b1a1a174a9a8a5568c0ec9a9487add3890a20391eebd69a56c4dd1a7c7112766b05d29fcfdd0f60d8c3f7287ec070201b4d0200e3ebbf47a97c230a7d2f05b25bec59cfd125f6cc15529f13fc624ebfd387327119434b66e61a8446e9fd9e14f4e0a42cee83f4600b195a06f4f0e55d67239c55256db69c3eb5218f9d214e0d9ca0992dee76630b55a96136a251a84711096475028e326782112f2afd64c3795f438ea623754cb0ea5e6a0d4e1ded7ee633f1eb8bf71b791646c8953e54a07242a178c02baf8fc68e7c1d3e6982b265802f50353b7f1eb0d8f33c9aed1b18432ecd04d10f951473a0e7d7e3e88a3cfca46ad1b11c5abca59bab0c8e880519753b9f4a9ab4f7a756d58a389dc3bdb25940a71a03899125b08efd7ec85b3cf0c1577014a81bf63b2144617cf259383a41af2d50a7ebb7d45a8968e3e3edfb1f4a45368ba380d172f82e641eb16949e192a3c2a37117df8e22fb7a4e7ed425f9951b392bc59184db81899ed2270844446a29aaed5cb3839c392d51636a24b737a3100417a849104c08606b4644a785080b67a92dfe888cd987100fd05aa0a007eacf337b3a80941f2defd530cf824f13cd263ba2ca194e7170072ce28e800000000000000070000000001312d0077c3e8caa53f1a775a5e89b1a54488aff835593d5e2b7355179136bbdd10a86c00061b1100030159105c3e04b59668b1516a793628be01d018e0de5972a7e6094a8b29761bd07a1654dc17ab21c8f940806cd4ccfd904db9594a8170f310b1e2d95e0e8b0baab7f8484c2c66a91b7f4274ec9e5cc3651ef2ee3a0177281548905a3c7d03c4ddf2bf8b736a940d69f2a5e6beaea33d92ce189a96803654a52b0d3da0cbdbbe0725e64dc4a6633f33318de0f6ca50b9f2a79ac68fdfe3a6f6c01a5215dc3bdfb56a780dc17968c5afd3270c50d4f7e1f540364e184d460e8b8479719788023ed029ad1439d0a0c85570b7a3f50ff70a82bd9ff9c04d6d8154046a30a3606ce25c2fbda28c863109cfaa95b7fe4f96622c715422ed1a8e99ce5bdecadb7c39798822115215e3914409d5de1e2c95a6c375c530b27fd0263d02167eb28628b945280a668f88c607bd74e3aac0ac704dfda9e796a76e6a856aee502b41c6f3932b6a89425484d580b6abc232cb12530b38b5c20cbbdcb0357210c3ad09a3ad7a56353dbbd6087f7cbccbae1f656ad06d370e4ec5a7906c9841a1f36f24ba6eab5c70b5d66bc8aecb60d9feb4576ef33b23456332483f01fcd0e01477789448ddc20fa207f496db56203fdaf53f6d64d97153d8e8c8c89c821f526f4b376e929d7598cf368b826313eb0ac5e435097b3f4acef26cfb30e63ab70622ff1ed04f9a1c02074bdb2d302d932cffbba4f833ab95912272ec87d8c05a8c5f1b2b101fb78a75cf9d0ab6e751163f27018f08c2fae76c7a6a417cf5cae2cdbd867115d9c1f162e83c3fc4f52202d3471212c3ae6eb39f882aa322609958dfbc8ca3f3a2eb051ef1b43d452b54c2352e8c9d184cbbd46ea936161067893164a72a34f6e2fd63c94353e524c9db699d15cef86f970bcf6a92dc367152f9740e84b2c292dbb338e4f21e6a47171b5a5cec0f4026906b528ad2c115866fb9e807faa200b600637abd774148f7637789946a82ec5c8e35692470c0c0f12a6a39a0c5dddf73635f2dbbb82d9ea56d8313a25cfd879ee024ee5144817b1f96945f34632cf0a5a3dd3ebcb8b42bb6c4b1c3d2a7fc6f817c97ec6282a3981a8d9e3d7070c178f7df847fcec8013b0a0d07a4f8c13c829209172022e0d724caf5a01219575f1d5a26f50f53d72ef8b596ae2263d2c2eeb822166178d56eba9ee9a9c60adb4fe3a7be89044d34288c7bd59cbf6e88e01f971b687200fccdc68d5a21f8430a8e5654a17f05773b2254eb301a5624c4aadbd3db8641cfe866ff72424a891f5e0487a48c204870c932ed9895427ce9a3d0a183fc40b7bbea3617b321f35cbba2c2180908e9bea2e07e1e5d8d5de9846994b78a313b8828eca935f4bdf9ca861326cca20776c6a4df35eeee257899467973941e6e8c567054480a7d8077e6e61ab480562ad53ad8127874a7fa4f747ccb2601cfa6e8c43e89f678dd38e4df80adef7bf07149edd32bf0e9775f3478870da6af26709a5bebbac77018050a07f1a8ffe4539c2df9de1dfe18c9bf0aa640178b0912bd9631acebdc2f2f993039b6cfa414d5cfcd0c363a178443e954db312efcd4463ee7cfe0d8a1eabe74d47692653f996cdd9b93c9fad41c5b16aa91a7bd5b267578a537e45d2af048ee0ce09ea41c72020d61509e5e91575022e7d5956b8e680f229909642366d5a16a17f021f41ca3d13df75ac3d3c8c772c2ba042680ba81466f8d05c07e19b1cde5a4d27ba6b820a63fffc5a42234e18109f128cb1e3524d621e38855b5c11cfbe7edf25dc3cd470308971b655b3381f946531d89ed23ad24f6a3cacd022fb9e5eb1d5a188a5d0936d0dfe3c7c424dba5f8892b2adda87a171d73bb5c5b8f809812689794ee3a5a7dc7d80f9c84bdad2fb7f89b2a45dc25836a42c8757eaddabcab7f82e1e51502cdcd3c4745009c55fd70b1c8f1a748e71e2b96b312f71f21cb00fd05aa0a007eacf337b3a80941f2defd530cf824f13cd263ba2ca194e7170072ce28e80000000000000006000000000010c87c77c3e8caa53f1a775a5e89b1a54488aff835593d5e2b7355179136bbdd10a86c00061b100002a9a1e8cbe48ae54ff2f32eb81ac70966e623abd77532a8cef6c3dab174da1e2879020c2238504e525d2d3fae9adb39044e8e6d0c394ae9e6ff1f01ae21e354d0bd1a5970e77015dbb6a635157b6465c32aee891c26ff8942ccfd24b612e4df241c55fc34dc1a5a613d431b813e14d0ad78023314f93c38f2bba6a6ea6e642d59a547f97b306ae0d072944c62cdec15933ffd85dafd9cabf278d9b8121db56915075baa049ac0d47df8a32b6c54a9935fbc1284fe8af288d3dd72870c8faf77e34a20970bf44b47bda06f2b8bfc931252456760ba1fa58669a3ec443737b429095db43f5bdf8e5d7f2d2f9c7318cfe012213ad35d54751539605337143de28756a146779a41308dbf94be4c9290422cb79ba768c340bc91012488afc3b388fbc253bdb9a35b1f4e39307c89f9ec3bb58e5da660013a741c1ab6c0cb22a3c94db0bf6d207599a527f18831530179880ef00950886378abcb99d2d1502b874669fb8f0a596ce3909e314057d7d1f668ca80e23c46f86127a71a729021623038d23cba281ed4967ea654470d4fdb31b297def0d22f5897f58d28af0cd86287fa51722af2682fe259987f56a9fe74af94138bb520ed12bd86453cc44f31f00e337647997b56c8bf40e3e9233e42b4c4ffe2ccc438417c754e04c95a2e7f357eb238020a0b235b8757a40996b6dab0e09e526f891f1773153151d99f3f65e62f0db6d4d94fb18293ea2f13e8c900d575cc0800969ef1e6903e6958c02dac84a8e863bcb8226bd3c6bab1038b628661333449d801b8b739bb08e09daf59c969691306e9e09c68508c5a558708a9b138fb841906a1967ea6290198be18ea3941a72a95b1b55e38403bda57d391cfdb86db844a0d9b4762a550cb1d7010de80fd6507d1e2dc7830ccd49784f8e80a3c87d9301339572b0347db9336bb7b1ede0fd1e187ca94aba942fe2b5c230f7dd1d3d0376aa882136b112883999227e2be17e9d681fb813acb70807b47e99bac127ca67c2d9619049783f8a9089d3fea4275585533683308ab94f3f02f577fb840ffae279813439fcb30eb5874329c48160768c2ca6b5d6368775989b96a056f22f7dddfbcd1b22a7fb74bb2d1b36f3e94914b82709a9351e1be88364a17a36b2a92ef61bb0520fefb802393b9e9effd6d54741ab9c6dbf28911bc45b334d9c2a6d76d94ecc2a19a3115d0f2b5e5ddb1f29fc9db0b3a21651afc2f93d321189621d8b59ab07e8d4e348f4bdbdb25b91d19b313850afddf92300c7fcebd9b266792745a24a505c56a3f98428ec70b65b0396a46836cabe49024d382eb1c8227e2df4da21385960f3d3801fb63d73ae69a4fcb6aca7018e874e8bf44f73c5e845ce81b2c643862c0344e3f4e50f4e6cb51b53a6c90ea545bc897dd73e2d2b3250711ed2a666413ae85b22e848735e8a6306e3b80f3677c8f40f85bf2f457f7764343288530a9c58b160de657df482af5eb0f291a50071d207cf46969643c113a6b15354b0b590885fbf4a7136939463ef66e013cb794ac6ab0fa887352817827ffc420bd8d1ee0b6a04d6819eadc1d96517c2edd8fe028101cd9842e407a44a8eb6bbda3fc5c54997fa43aaf340ed8ab6f75e3b588dd906ebd9deba889711b3895bd7df494f042e68cd78d7e75c237bd22b90e49e5571e169487508dfea22bfe2c83f21ec1e91f7d9331e5ac4ba6c52c1183afb1e083f3d0c42af6cf41e82e9151b5717b934ba6e4f52757d27a7c8139c523f977cb9c7f29e4a19cf41a1288c60a9ee10f58189351a51925422276e2da9ce3a4ca6944b767601e686745e88b7902e0e110dd66189dec74e3abbc76a4045d5051a6b76c91b2b20fc2ab1ca7d5d63f25d2c83f5d8ab31034e3ca42c4af1ed6017f5b10c2f44a52d78bf858aa7c148b6f07156ee365cd463d3c00fd05aa0a007eacf337b3a80941f2defd530cf824f13cd263ba2ca194e7170072ce28e800000000000000030000000001c9c38077c3e8caa53f1a775a5e89b1a54488aff835593d5e2b7355179136bbdd10a86c00061b1000031a12d4e5b7253515c5822879f6f1738224e451910cacebbb545aa637e2a0f631a151e02f206b6397055534bd6a0b7c7990d269d153d3886fc7e51fd4e09c5357a96694911e9912135e3f9ccec1058be709a743ee44f07feebb0fc6a3078bec155a1b9f67afaac6914debcc890fd86150d790f8de1b7168d4501064dacfd16c19f61a8c13c7658df92d6e5ad79b1b251a96db66bfc498e8e4114f98b9b36bc41663c9d2d15a280fdae479c14e52e148b0f86abbe4f9ef78570e193585b5aa30e53d54352eba5ad54d238f0d756b54aca9889fb21c57b6c639a6713a4f655d5dde92391185a9d8c929bca42c3bc9ba5f65b059dcbab02e89cf2e95fe1b1fb291aa1c2aeb8b57ab24f92414fc296a37b683a1d47155acb5210ffb6a170642e0f1cf7a4704861099b23c0294f367f28bf6fb6f839ba3e958ba6954ef630f90d014b687e63decc26c395601a885583ee0f1a86de5d60069664e2b93a7e936579e934a084b3e34850a1c26dc5fe7a8479278e6000f5025552b0d43ec186cd01a4d6ae748ba9289d39046c3568982aac9593a3496d3eec561b538a9b0d9ed91a4560f504174bb70725fc1c9bc354afe11d1e8aab3389774d619fdff96ea096c8283c2555e9d3169c8dd543bc74eb9f3dd4f17105563922aa18243f7135429b7a71368b44975e07443fb6f5eae089b88dc2d729d87e303104b8a7172902cdb8500ce9cd2086f7a7f6ce683942caae21ed05596c46d8f87f98eadb034c472a3f6b9d7a83d41fa70b44beb4288973cee486adc191a9aad1d1b7cebac6225aec67eed51c347fa5c145e183454b2a514a5044808eea8a9b28b24e39b115e2faa75f04d370285684857129e14e6726bb23aa0ff9323e9687578446868705f3137289c49357ff4acb134f6f3f7dc7efe6b6a332cd2fbbef0579493c4368b70815ff13abc1e3cb08c3931d8b7c99bb8b5e89fccda77a6d51fee07754332500517b75bd26bc66c81cf9f0e8a34e01c700b86590fa1cc7682a6d20c10d325d735dabc43db87ae2700e4966d8be90e408aca19ba1327ab209d4a574095e37285880c31cd631aabafa6b6c31b6db80b989ec57141ab35a5d7e32445f153da3832b223831bcaa8d1cedf6cf9d8e2dc121fd1b8a744481690be8668d6185113853ad1e86fbfe5a1b216208d3300532b0c80bc751d805260f1e6b014fe8f0b904a54560baa1b9f9d81479024b9d722fa577d9708eeb5b84c5005766f2e2c622689e1d750dc16ddd74450fedec590cb49014d4cba6dff6d2aa2c15b71067c8c02128e587f6a09556ee2e8e6b1b6bb8380ea6e79f43fb6e427fa66f0b616b84261b18f07d3607eaf4b932f38fd7267c742c503862458a1d1f01b239e94f39bcce1864564ce2f021fd43673964e77d3616bf8405b4c770996fa48d312458b29be5c05a6941ce5b3b2ff4ff32ad784ea47a664fbc2bec22fb6faecf928539773cd5c0972d9e6a10762a66190c3b5cc9b77d3546ec02d355aeec68ca9af34b2f8c44071b6d9b608f17e24ec3ac185cd9fab9b9bfdfb7873e748796af1b87820707752798534334b0e79bf108d60ef61a2c3cbdffc663b54f2ef407ae43749d31681e3da13ab3beb1f4ac2053cc7286bd7a3192ca412d1692ddcb1fc33131c19bc312d72b1616b91110fc15a8f33b2429eb0fd07bb034405bb879e54cf2f4331c2fbafc69cca8c2ff5a762c7f139edbe123735e63490a17f309ec895077731e5f6d6a53e513f1a3c15eaf72db4605d65eaf5cdc830155f5b9bf4e96a3644dd0ae9ab1f09778fdcd763668507d1c49663dc4a64def5f24577d2b1b34113f6a2a92999bcaf649536e431b8c2c30491cb54fe84b7e087c7fdd1e9b92dfaa31651138e7602602a45d331528ebd79b450ea237b195e80a04a2261a00fdd918cd0b0116f9d00fd05aa0a007eacf337b3a80941f2defd530cf824f13cd263ba2ca194e7170072ce28e800000000000000050000000001c9c38077c3e8caa53f1a775a5e89b1a54488aff835593d5e2b7355179136bbdd10a86c00061b100003d4b11a6510316810f0248ea0530220e3255171fd5fce2eee5d0bff33d694904b77c108bd2c322ebc2c3231e1f78618ab9ba83f81fcbbc7278f4d0cf17edb4342ccf40ba133606f6597f474b0dde594a6f8af078aadcfc940d5fcccd32793a1266190f06e55aec9b95b9f2e552ee8d5e4a662a7cdddb20599b14e3105699f2618395ca9d80ca232f5a96e48e27a09b24b3c28b38b42bf63464dbfc0d233c635d1379784581f5ff2f2fa2c6dff4cf8c12454745c695ea394d0fc477f4907e82e636e347306ee70730f61aa75ddbb10cf023b28a5e1f7dfed48470bf4b3180e343ea7f578a0d8f33505aa36b3875226b668376db40b3171f5a2eef326b0a416fd27cd7c7936e71cc9822421d9e317db5fa31de68f4be412666c3ef4c699acbadf94d2bfd1f32ad1f962da04965d20d783047a415f5f6a59117422dc26c5f05a5ef0180081e9b7771277ae8520647e833c2dc80149baea35700c35aadce8c4c81ef6e3fc3ec55e3fe3b9f846710fd648668ca3ba05f6b7cf5eddc9b5a7e73f2ac4d1f53864765cf70f55128ec53692bf26656da9468ea93b7030063a2021ad57cf3ab1df019c21118f6f9cec64def026910242fc4809d2224210460d63d02f2d6475b371b97907cc3c1e72f56ce8285cc08fd4e37f439f39b63667c045f4b5dc5f982cdc8b32fa0392ce38dccfa6a79f5bcaccd2bdaae666404947fd8b6d8ad7e16ae1307d7b24cc15d712814d1311bdf2155ee28da446ddd2bd0e782c225a8a379073314ec486aa2a9dc54c4edc2e2dea278f39ee23b0d7c2f5df7a20ffe5d77541d1c42a94a17e977763f15dd889c939104b691553ec8c99d5c87a9513553a7384e3a45fac0193b77fd8b7a2c5bf291a6954c767addeca4c91a04946688491c56818bb9de51d2e2dfc2c0b2e8f3e929ba04d9b3f457935f8c797dd770cd6558b6238ba0706a500bd9aaa17565a8a8b8abb41e4717f312d9b449de7dcd5317e1f6ba71b428f5e96857fc9acaef6298d4e994746a45534715af1326c79836f0f3a7ae3aaa41c90f172ec8544d13086237420207fddbae7d349a2871a62f465a6c57bb54e623c36067f41f33282a8548e5047d3d2da4c83483811ee3559bd7479216631281aeee1152f1fb876beb57b51c9cbf002ce9e6c81129a119120e24789958bb1de20448f01692ea4b955099b808af3c4dc7ad6652a9eff075f91fd3bbf54ec7d450c8000005894542b810974b36f6a66125e047dc491aa376b70aef9fd65379c0668937a86e0c12772520a151df018c031b931b2e0c0a25419f10080fa9010ddee60b4861692ed00bef79ab5cae1a4d5baf62552a111845c3969538bed014baac2169b2c6e847b3123a7aff9761736ba9e2f5f4d0f94852c921599615f91bbbdeb230ddf834305c9c70490307c300952b8e61e5255b31cd09f2ca7cdbb4c4e76798a78c0a8709d86c7dfd5dc008af596308a36e0d28c4ba60d68151858e740ccbcc068b0acb3fb81bea92e4e979ac39057ccb19c8603658efc62f6222621cf956b377b7ff0fea770908f04600c2720b03371a62ef941159fcc015248ba326f31d1bc82e4d94c186d4f6c39e1f55600221a2cdf85b560b78dc5fb3e74069021e9062e2e7c6173716ca70dc843cf02070253d31ddb9c4b569df4fe243b78a8300788104b575d641108510bafea96a25bbea6b8f6474fa1886de9d525a4fbdb480afc4ed96482ff2aa8e64142c285b11c87ffef811b5026b3a2587a91aff83c6825c4d0634ccc7aab85e795c11491f3e0883568c23e67efcd4c26a5db3ca0b5c3bbded3c6e355738319c1a087d8da87db4da52d4c5214d272cd1631b2843cbd6b015e52eb51a8bb759d529ae6ab7b397aa8963f2074c9278fa8499de47dc9152bb8726de26f5bbf24e9ca7e329353ca2da6363b4e00002710000000000598cd44000000002faf0800240a007eacf337b3a80941f2defd530cf824f13cd263ba2ca194e7170072ce28e8000000002b40420f0000000000220020e2eb1e4df160f9e11d4d06a6075034b47e9d3e1c3d21d0feab64905d2e6ab17b47522103174f3de90105ae02d88ccc1a7ed4bf0d116f27ef6010f1ec308273c1010ae3dc2103548b338f50bdae124711cadef7a82ce832bff61826f13b031677793abb8c674c52aefd0205020000000001010a007eacf337b3a80941f2defd530cf824f13cd263ba2ca194e7170072ce28e80000000000a1c4a78006204e0000000000002200204c81bcdcc2c4f01d3c045348a155c9249ddba4d48c16ccf33c1d8b6e34614180a8610000000000002200204c81bcdcc2c4f01d3c045348a155c9249ddba4d48c16ccf33c1d8b6e3461418030750000000000002200204c81bcdcc2c4f01d3c045348a155c9249ddba4d48c16ccf33c1d8b6e3461418030750000000000002200204c81bcdcc2c4f01d3c045348a155c9249ddba4d48c16ccf33c1d8b6e34614180a437010000000000220020680a82e78074810847ca530cd74fabdbd2e34beaf0fdd2bc832ddfcd2669876c00350c0000000000160014e1fdef3169db3728018a906129e3913dd841ab0f04004730440220639ddfeb4cfd418c28418208226f5f6be762b65bb242968695ac1574d1fae8e702207bd3b3f60f12f171a37e50b7126c709a4cb87e417efdfe520707962d393bd28c01473044022058407ee97260aa8020eea7036f348ec4959fbd8a820e8628244705138803e00e02207d53e7fec4e01bbc04a0495e8c31210c0c6c2542e38d6c9ae677b78bd568cedb0147522103174f3de90105ae02d88ccc1a7ed4bf0d116f27ef6010f1ec308273c1010ae3dc2103548b338f50bdae124711cadef7a82ce832bff61826f13b031677793abb8c674c52ae340169200004000324efa7874aa335cd976b8209145d670a6dba17471f6a4f47dacaa432dd972d801c000000002b204e0000000000002200204c81bcdcc2c4f01d3c045348a155c9249ddba4d48c16ccf33c1d8b6e346141808576a9144a2fe35d15fdacd3b3cba332b5879b6acfb2e79f8763ac67210375099d4e25580f8d2c59718d7319e9d26c7119f9476ca3a623fb638843d3c8617c820120876475527c210375f5c7f3e5f1da49febb149cb9dc97098402628f84b79b586d5d516c17e6327852ae67a914950b69c651378982bec084b296e6dd23a33c41b588ac68685e0200000001efa7874aa335cd976b8209145d670a6dba17471f6a4f47dacaa432dd972d801c000000000000000000013a34000000000000220020680a82e78074810847ca530cd74fabdbd2e34beaf0fdd2bc832ddfcd2669876c111b06004093d252d2f22cecee6c2b604a1190120db574f0ba33b49ac826229b95071230bd7a4baa7a1704813804ba9b7d0e2aef3932a7d7d096862c1636736ad3296a46f5401f096356d49cb9c0d1dad9a01530aa892aa75cdcb626f42ef09b734f39d0a2650d6274377598b29f3f3eae1281a8b61d5cb9853b478cc973b8035af5d60c26a0000324efa7874aa335cd976b8209145d670a6dba17471f6a4f47dacaa432dd972d801c010000002ba8610000000000002200204c81bcdcc2c4f01d3c045348a155c9249ddba4d48c16ccf33c1d8b6e346141808576a9144a2fe35d15fdacd3b3cba332b5879b6acfb2e79f8763ac67210375099d4e25580f8d2c59718d7319e9d26c7119f9476ca3a623fb638843d3c8617c820120876475527c210375f5c7f3e5f1da49febb149cb9dc97098402628f84b79b586d5d516c17e6327852ae67a914950b69c651378982bec084b296e6dd23a33c41b588ac68685e0200000001efa7874aa335cd976b8209145d670a6dba17471f6a4f47dacaa432dd972d801c01000000000000000001c247000000000000220020680a82e78074810847ca530cd74fabdbd2e34beaf0fdd2bc832ddfcd2669876c101b060040774ca3fbbee6dc29db89dbb2bdb977afdf31727548aa6060e9e23a79090bf658474a5aa2de53dae36371ec535e13e4291c9fbd1942e7cc812ff4ca4c9d84483c4088605547607e6c8b1b24a671ed340700ad396a8bf613fa9e4646c0b11c2aeff708e4448ba70fe48903e3034ace605d8651091fcd6f80fddb57d2e2b09da6765d000324efa7874aa335cd976b8209145d670a6dba17471f6a4f47dacaa432dd972d801c020000002b30750000000000002200204c81bcdcc2c4f01d3c045348a155c9249ddba4d48c16ccf33c1d8b6e346141808576a9144a2fe35d15fdacd3b3cba332b5879b6acfb2e79f8763ac67210375099d4e25580f8d2c59718d7319e9d26c7119f9476ca3a623fb638843d3c8617c820120876475527c210375f5c7f3e5f1da49febb149cb9dc97098402628f84b79b586d5d516c17e6327852ae67a914950b69c651378982bec084b296e6dd23a33c41b588ac68685e0200000001efa7874aa335cd976b8209145d670a6dba17471f6a4f47dacaa432dd972d801c020000000000000000014a5b000000000000220020680a82e78074810847ca530cd74fabdbd2e34beaf0fdd2bc832ddfcd2669876c101b0600404ec5ef779e7b0db022d9da0986bee62eeb0372337f3974e0bfa8ecbddec7a3ed47930ce5ca6796d3cde21dbf9e0678f4d0af5d84889c24e2b960786d141b900a402c736579e487f2ceedb15546a438818e9a13d3b9f9232d4ff418855f9112795e2bce08fb52f8737268b616158eeb06e36abdc83942396fb407d3e603ed1edd7b000324efa7874aa335cd976b8209145d670a6dba17471f6a4f47dacaa432dd972d801c030000002b30750000000000002200204c81bcdcc2c4f01d3c045348a155c9249ddba4d48c16ccf33c1d8b6e346141808576a9144a2fe35d15fdacd3b3cba332b5879b6acfb2e79f8763ac67210375099d4e25580f8d2c59718d7319e9d26c7119f9476ca3a623fb638843d3c8617c820120876475527c210375f5c7f3e5f1da49febb149cb9dc97098402628f84b79b586d5d516c17e6327852ae67a914950b69c651378982bec084b296e6dd23a33c41b588ac68685e0200000001efa7874aa335cd976b8209145d670a6dba17471f6a4f47dacaa432dd972d801c030000000000000000014a5b000000000000220020680a82e78074810847ca530cd74fabdbd2e34beaf0fdd2bc832ddfcd2669876c101b060040ff0a3853c07d426ac93a8b772017a41f324558851ec69bcd65ed1cd81490e0f11c3c01574fe196df6311cf21fbb78abd7d40659479ea080c7ccb3c5fd53027df404f63e06d948b7890c8a3fb1ae568e88de8cd4ef3364c092bcdeb006c40558b082a5fc572f9f02f5d2a0870101ff7d900da184fb19b5cd410ba0779bc55533c4a00000000000000070005fffd05aa0a007eacf337b3a80941f2defd530cf824f13cd263ba2ca194e7170072ce28e800000000000000030000000001c9c38077c3e8caa53f1a775a5e89b1a54488aff835593d5e2b7355179136bbdd10a86c00061b1000031a12d4e5b7253515c5822879f6f1738224e451910cacebbb545aa637e2a0f631a151e02f206b6397055534bd6a0b7c7990d269d153d3886fc7e51fd4e09c5357a96694911e9912135e3f9ccec1058be709a743ee44f07feebb0fc6a3078bec155a1b9f67afaac6914debcc890fd86150d790f8de1b7168d4501064dacfd16c19f61a8c13c7658df92d6e5ad79b1b251a96db66bfc498e8e4114f98b9b36bc41663c9d2d15a280fdae479c14e52e148b0f86abbe4f9ef78570e193585b5aa30e53d54352eba5ad54d238f0d756b54aca9889fb21c57b6c639a6713a4f655d5dde92391185a9d8c929bca42c3bc9ba5f65b059dcbab02e89cf2e95fe1b1fb291aa1c2aeb8b57ab24f92414fc296a37b683a1d47155acb5210ffb6a170642e0f1cf7a4704861099b23c0294f367f28bf6fb6f839ba3e958ba6954ef630f90d014b687e63decc26c395601a885583ee0f1a86de5d60069664e2b93a7e936579e934a084b3e34850a1c26dc5fe7a8479278e6000f5025552b0d43ec186cd01a4d6ae748ba9289d39046c3568982aac9593a3496d3eec561b538a9b0d9ed91a4560f504174bb70725fc1c9bc354afe11d1e8aab3389774d619fdff96ea096c8283c2555e9d3169c8dd543bc74eb9f3dd4f17105563922aa18243f7135429b7a71368b44975e07443fb6f5eae089b88dc2d729d87e303104b8a7172902cdb8500ce9cd2086f7a7f6ce683942caae21ed05596c46d8f87f98eadb034c472a3f6b9d7a83d41fa70b44beb4288973cee486adc191a9aad1d1b7cebac6225aec67eed51c347fa5c145e183454b2a514a5044808eea8a9b28b24e39b115e2faa75f04d370285684857129e14e6726bb23aa0ff9323e9687578446868705f3137289c49357ff4acb134f6f3f7dc7efe6b6a332cd2fbbef0579493c4368b70815ff13abc1e3cb08c3931d8b7c99bb8b5e89fccda77a6d51fee07754332500517b75bd26bc66c81cf9f0e8a34e01c700b86590fa1cc7682a6d20c10d325d735dabc43db87ae2700e4966d8be90e408aca19ba1327ab209d4a574095e37285880c31cd631aabafa6b6c31b6db80b989ec57141ab35a5d7e32445f153da3832b223831bcaa8d1cedf6cf9d8e2dc121fd1b8a744481690be8668d6185113853ad1e86fbfe5a1b216208d3300532b0c80bc751d805260f1e6b014fe8f0b904a54560baa1b9f9d81479024b9d722fa577d9708eeb5b84c5005766f2e2c622689e1d750dc16ddd74450fedec590cb49014d4cba6dff6d2aa2c15b71067c8c02128e587f6a09556ee2e8e6b1b6bb8380ea6e79f43fb6e427fa66f0b616b84261b18f07d3607eaf4b932f38fd7267c742c503862458a1d1f01b239e94f39bcce1864564ce2f021fd43673964e77d3616bf8405b4c770996fa48d312458b29be5c05a6941ce5b3b2ff4ff32ad784ea47a664fbc2bec22fb6faecf928539773cd5c0972d9e6a10762a66190c3b5cc9b77d3546ec02d355aeec68ca9af34b2f8c44071b6d9b608f17e24ec3ac185cd9fab9b9bfdfb7873e748796af1b87820707752798534334b0e79bf108d60ef61a2c3cbdffc663b54f2ef407ae43749d31681e3da13ab3beb1f4ac2053cc7286bd7a3192ca412d1692ddcb1fc33131c19bc312d72b1616b91110fc15a8f33b2429eb0fd07bb034405bb879e54cf2f4331c2fbafc69cca8c2ff5a762c7f139edbe123735e63490a17f309ec895077731e5f6d6a53e513f1a3c15eaf72db4605d65eaf5cdc830155f5b9bf4e96a3644dd0ae9ab1f09778fdcd763668507d1c49663dc4a64def5f24577d2b1b34113f6a2a92999bcaf649536e431b8c2c30491cb54fe84b7e087c7fdd1e9b92dfaa31651138e7602602a45d331528ebd79b450ea237b195e80a04a2261a00fdd918cd0b0116f9dfffd05aa0a007eacf337b3a80941f2defd530cf824f13cd263ba2ca194e7170072ce28e800000000000000050000000001c9c38077c3e8caa53f1a775a5e89b1a54488aff835593d5e2b7355179136bbdd10a86c00061b100003d4b11a6510316810f0248ea0530220e3255171fd5fce2eee5d0bff33d694904b77c108bd2c322ebc2c3231e1f78618ab9ba83f81fcbbc7278f4d0cf17edb4342ccf40ba133606f6597f474b0dde594a6f8af078aadcfc940d5fcccd32793a1266190f06e55aec9b95b9f2e552ee8d5e4a662a7cdddb20599b14e3105699f2618395ca9d80ca232f5a96e48e27a09b24b3c28b38b42bf63464dbfc0d233c635d1379784581f5ff2f2fa2c6dff4cf8c12454745c695ea394d0fc477f4907e82e636e347306ee70730f61aa75ddbb10cf023b28a5e1f7dfed48470bf4b3180e343ea7f578a0d8f33505aa36b3875226b668376db40b3171f5a2eef326b0a416fd27cd7c7936e71cc9822421d9e317db5fa31de68f4be412666c3ef4c699acbadf94d2bfd1f32ad1f962da04965d20d783047a415f5f6a59117422dc26c5f05a5ef0180081e9b7771277ae8520647e833c2dc80149baea35700c35aadce8c4c81ef6e3fc3ec55e3fe3b9f846710fd648668ca3ba05f6b7cf5eddc9b5a7e73f2ac4d1f53864765cf70f55128ec53692bf26656da9468ea93b7030063a2021ad57cf3ab1df019c21118f6f9cec64def026910242fc4809d2224210460d63d02f2d6475b371b97907cc3c1e72f56ce8285cc08fd4e37f439f39b63667c045f4b5dc5f982cdc8b32fa0392ce38dccfa6a79f5bcaccd2bdaae666404947fd8b6d8ad7e16ae1307d7b24cc15d712814d1311bdf2155ee28da446ddd2bd0e782c225a8a379073314ec486aa2a9dc54c4edc2e2dea278f39ee23b0d7c2f5df7a20ffe5d77541d1c42a94a17e977763f15dd889c939104b691553ec8c99d5c87a9513553a7384e3a45fac0193b77fd8b7a2c5bf291a6954c767addeca4c91a04946688491c56818bb9de51d2e2dfc2c0b2e8f3e929ba04d9b3f457935f8c797dd770cd6558b6238ba0706a500bd9aaa17565a8a8b8abb41e4717f312d9b449de7dcd5317e1f6ba71b428f5e96857fc9acaef6298d4e994746a45534715af1326c79836f0f3a7ae3aaa41c90f172ec8544d13086237420207fddbae7d349a2871a62f465a6c57bb54e623c36067f41f33282a8548e5047d3d2da4c83483811ee3559bd7479216631281aeee1152f1fb876beb57b51c9cbf002ce9e6c81129a119120e24789958bb1de20448f01692ea4b955099b808af3c4dc7ad6652a9eff075f91fd3bbf54ec7d450c8000005894542b810974b36f6a66125e047dc491aa376b70aef9fd65379c0668937a86e0c12772520a151df018c031b931b2e0c0a25419f10080fa9010ddee60b4861692ed00bef79ab5cae1a4d5baf62552a111845c3969538bed014baac2169b2c6e847b3123a7aff9761736ba9e2f5f4d0f94852c921599615f91bbbdeb230ddf834305c9c70490307c300952b8e61e5255b31cd09f2ca7cdbb4c4e76798a78c0a8709d86c7dfd5dc008af596308a36e0d28c4ba60d68151858e740ccbcc068b0acb3fb81bea92e4e979ac39057ccb19c8603658efc62f6222621cf956b377b7ff0fea770908f04600c2720b03371a62ef941159fcc015248ba326f31d1bc82e4d94c186d4f6c39e1f55600221a2cdf85b560b78dc5fb3e74069021e9062e2e7c6173716ca70dc843cf02070253d31ddb9c4b569df4fe243b78a8300788104b575d641108510bafea96a25bbea6b8f6474fa1886de9d525a4fbdb480afc4ed96482ff2aa8e64142c285b11c87ffef811b5026b3a2587a91aff83c6825c4d0634ccc7aab85e795c11491f3e0883568c23e67efcd4c26a5db3ca0b5c3bbded3c6e355738319c1a087d8da87db4da52d4c5214d272cd1631b2843cbd6b015e52eb51a8bb759d529ae6ab7b397aa8963f2074c9278fa8499de47dc9152bb8726de26f5bbf24e9ca7e329353ca2da6363b4efffd05aa0a007eacf337b3a80941f2defd530cf824f13cd263ba2ca194e7170072ce28e8000000000000000400000000017d784077c3e8caa53f1a775a5e89b1a54488aff835593d5e2b7355179136bbdd10a86c00061b1000020b7604aeb7cb080b12de62f75a292bed3134b4a3de764bd032fb049e2c2a1d3942cc11b9b12619d42cda3c7eb63da952644a3f355bdf772d77f1fc2a0b36bf116cdacfcda3b77974536cc3f3d0f8209dba69408073cbe0f039eba220fcf3b0b96d5875bb71acdc999fd9feeab68a0b63629efe0dc91e92dcf93b3c49285c981746a68ead7a96a9c1ce21284a2796b0051e6b7cf36a264d545063c054454b87cc3f9ddad5350382585de18cdc4e65e02484695f541b8fc082c87ea4a56aabc84fd1b7c1cd40e3517ae811f9ff921fba672e0191c88a2681ef8ea1300eeb6c1d8fccdb5ab05bf80b2a5c3ed5c018752cc841305edae02149389bc5e58ea5a59a1c59755c7ab5f35e3740c9f6218d227370363a7e386b214f5b2e771ea8b94a2a5e640d8a715d57756665f4a659f4ef6488f9ceab5e22d523ee52da368701d799fcb3459a31e6fb03de91ffd9b608656b172945015da3e5000f7f6c3dc0b4e102ba5f101bab5c2ceab8021d071bf89cf9d65d2439fe9a9dea4291eccb43a431904313655391ace4b82bf5da6270eb5bc12a60a69e7cf96b75a699db5a374713558eff7101bb2d8471e0c4ad0e202fd10aa46098bd5565c1d410e695fdd799c866090f1a7aa002305388acb68c8d82d588c1a66df5fb12653686bc4cf487a3484c0c23578a428de3df57539ed11227f2648bc36261c7f3f4f13e18d238dc856b8dd8f07de9fb73663d3a0026a03d9ee9f807e2a03c9546d2537a1f30a1f06767622d8bacdf4ab3b0f245a7b4f482baa60080fce3e7e15d2e086c670d6ec11d907e3da593977c8c25d620ed40cf82dd503c25f1a5f3e8a5ddb65ae7117600794844252e1448d17849028bd5df960a5fddf56e48cf082cfbc5c29666bca6af335c06f8240912a311e6c07946eeea9ef57e46b3e48b95c82cbee3830bf2b55d5e0223b9480fe072729a8af4c6c8c2f86038cb8a2e02405ce5f834c87b241fcb9963f008c273c64bc1f7289cc3a0c5eea7ea1a6dc7bc6228a97978a2ddd7dc49303c5beb15b36c8044f656716cad0a62d1e8c381db46f007be7f8096f72d3a8f08d338d9f2bbc43c0244215df229824a9ad34b655d840b38c5061462d54358be1c184b51d3a805beeea78047f544b15cc333d591f3d8cb60cbdd78e58ed47d21739762957ed6365fea14d7ba8368371fe4333be4f5ae289ff444c9f377a072a3a4173bd2680c6d6074743908d9c4f09aa30d064c7866a7a34303fce53deba6a597a24c211d209815bbff97736ba9fc79e390b36b2343d2590c8fd6db3e54206580eb7a53b9de09a42cfaa2cffb00ea90dbd07f1d357e69ca96fbba4efefc4e07b1a1a174a9a8a5568c0ec9a9487add3890a20391eebd69a56c4dd1a7c7112766b05d29fcfdd0f60d8c3f7287ec070201b4d0200e3ebbf47a97c230a7d2f05b25bec59cfd125f6cc15529f13fc624ebfd387327119434b66e61a8446e9fd9e14f4e0a42cee83f4600b195a06f4f0e55d67239c55256db69c3eb5218f9d214e0d9ca0992dee76630b55a96136a251a84711096475028e326782112f2afd64c3795f438ea623754cb0ea5e6a0d4e1ded7ee633f1eb8bf71b791646c8953e54a07242a178c02baf8fc68e7c1d3e6982b265802f50353b7f1eb0d8f33c9aed1b18432ecd04d10f951473a0e7d7e3e88a3cfca46ad1b11c5abca59bab0c8e880519753b9f4a9ab4f7a756d58a389dc3bdb25940a71a03899125b08efd7ec85b3cf0c1577014a81bf63b2144617cf259383a41af2d50a7ebb7d45a8968e3e3edfb1f4a45368ba380d172f82e641eb16949e192a3c2a37117df8e22fb7a4e7ed425f9951b392bc59184db81899ed2270844446a29aaed5cb3839c392d51636a24b737a3100417a849104c08606b4644a785080b67a92dfe888cd9871fffd05aa0a007eacf337b3a80941f2defd530cf824f13cd263ba2ca194e7170072ce28e80000000000000006000000000010c87c77c3e8caa53f1a775a5e89b1a54488aff835593d5e2b7355179136bbdd10a86c00061b100002a9a1e8cbe48ae54ff2f32eb81ac70966e623abd77532a8cef6c3dab174da1e2879020c2238504e525d2d3fae9adb39044e8e6d0c394ae9e6ff1f01ae21e354d0bd1a5970e77015dbb6a635157b6465c32aee891c26ff8942ccfd24b612e4df241c55fc34dc1a5a613d431b813e14d0ad78023314f93c38f2bba6a6ea6e642d59a547f97b306ae0d072944c62cdec15933ffd85dafd9cabf278d9b8121db56915075baa049ac0d47df8a32b6c54a9935fbc1284fe8af288d3dd72870c8faf77e34a20970bf44b47bda06f2b8bfc931252456760ba1fa58669a3ec443737b429095db43f5bdf8e5d7f2d2f9c7318cfe012213ad35d54751539605337143de28756a146779a41308dbf94be4c9290422cb79ba768c340bc91012488afc3b388fbc253bdb9a35b1f4e39307c89f9ec3bb58e5da660013a741c1ab6c0cb22a3c94db0bf6d207599a527f18831530179880ef00950886378abcb99d2d1502b874669fb8f0a596ce3909e314057d7d1f668ca80e23c46f86127a71a729021623038d23cba281ed4967ea654470d4fdb31b297def0d22f5897f58d28af0cd86287fa51722af2682fe259987f56a9fe74af94138bb520ed12bd86453cc44f31f00e337647997b56c8bf40e3e9233e42b4c4ffe2ccc438417c754e04c95a2e7f357eb238020a0b235b8757a40996b6dab0e09e526f891f1773153151d99f3f65e62f0db6d4d94fb18293ea2f13e8c900d575cc0800969ef1e6903e6958c02dac84a8e863bcb8226bd3c6bab1038b628661333449d801b8b739bb08e09daf59c969691306e9e09c68508c5a558708a9b138fb841906a1967ea6290198be18ea3941a72a95b1b55e38403bda57d391cfdb86db844a0d9b4762a550cb1d7010de80fd6507d1e2dc7830ccd49784f8e80a3c87d9301339572b0347db9336bb7b1ede0fd1e187ca94aba942fe2b5c230f7dd1d3d0376aa882136b112883999227e2be17e9d681fb813acb70807b47e99bac127ca67c2d9619049783f8a9089d3fea4275585533683308ab94f3f02f577fb840ffae279813439fcb30eb5874329c48160768c2ca6b5d6368775989b96a056f22f7dddfbcd1b22a7fb74bb2d1b36f3e94914b82709a9351e1be88364a17a36b2a92ef61bb0520fefb802393b9e9effd6d54741ab9c6dbf28911bc45b334d9c2a6d76d94ecc2a19a3115d0f2b5e5ddb1f29fc9db0b3a21651afc2f93d321189621d8b59ab07e8d4e348f4bdbdb25b91d19b313850afddf92300c7fcebd9b266792745a24a505c56a3f98428ec70b65b0396a46836cabe49024d382eb1c8227e2df4da21385960f3d3801fb63d73ae69a4fcb6aca7018e874e8bf44f73c5e845ce81b2c643862c0344e3f4e50f4e6cb51b53a6c90ea545bc897dd73e2d2b3250711ed2a666413ae85b22e848735e8a6306e3b80f3677c8f40f85bf2f457f7764343288530a9c58b160de657df482af5eb0f291a50071d207cf46969643c113a6b15354b0b590885fbf4a7136939463ef66e013cb794ac6ab0fa887352817827ffc420bd8d1ee0b6a04d6819eadc1d96517c2edd8fe028101cd9842e407a44a8eb6bbda3fc5c54997fa43aaf340ed8ab6f75e3b588dd906ebd9deba889711b3895bd7df494f042e68cd78d7e75c237bd22b90e49e5571e169487508dfea22bfe2c83f21ec1e91f7d9331e5ac4ba6c52c1183afb1e083f3d0c42af6cf41e82e9151b5717b934ba6e4f52757d27a7c8139c523f977cb9c7f29e4a19cf41a1288c60a9ee10f58189351a51925422276e2da9ce3a4ca6944b767601e686745e88b7902e0e110dd66189dec74e3abbc76a4045d5051a6b76c91b2b20fc2ab1ca7d5d63f25d2c83f5d8ab31034e3ca42c4af1ed6017f5b10c2f44a52d78bf858aa7c148b6f07156ee365cd463d3cfffd05aa0a007eacf337b3a80941f2defd530cf824f13cd263ba2ca194e7170072ce28e800000000000000070000000001312d0077c3e8caa53f1a775a5e89b1a54488aff835593d5e2b7355179136bbdd10a86c00061b1100030159105c3e04b59668b1516a793628be01d018e0de5972a7e6094a8b29761bd07a1654dc17ab21c8f940806cd4ccfd904db9594a8170f310b1e2d95e0e8b0baab7f8484c2c66a91b7f4274ec9e5cc3651ef2ee3a0177281548905a3c7d03c4ddf2bf8b736a940d69f2a5e6beaea33d92ce189a96803654a52b0d3da0cbdbbe0725e64dc4a6633f33318de0f6ca50b9f2a79ac68fdfe3a6f6c01a5215dc3bdfb56a780dc17968c5afd3270c50d4f7e1f540364e184d460e8b8479719788023ed029ad1439d0a0c85570b7a3f50ff70a82bd9ff9c04d6d8154046a30a3606ce25c2fbda28c863109cfaa95b7fe4f96622c715422ed1a8e99ce5bdecadb7c39798822115215e3914409d5de1e2c95a6c375c530b27fd0263d02167eb28628b945280a668f88c607bd74e3aac0ac704dfda9e796a76e6a856aee502b41c6f3932b6a89425484d580b6abc232cb12530b38b5c20cbbdcb0357210c3ad09a3ad7a56353dbbd6087f7cbccbae1f656ad06d370e4ec5a7906c9841a1f36f24ba6eab5c70b5d66bc8aecb60d9feb4576ef33b23456332483f01fcd0e01477789448ddc20fa207f496db56203fdaf53f6d64d97153d8e8c8c89c821f526f4b376e929d7598cf368b826313eb0ac5e435097b3f4acef26cfb30e63ab70622ff1ed04f9a1c02074bdb2d302d932cffbba4f833ab95912272ec87d8c05a8c5f1b2b101fb78a75cf9d0ab6e751163f27018f08c2fae76c7a6a417cf5cae2cdbd867115d9c1f162e83c3fc4f52202d3471212c3ae6eb39f882aa322609958dfbc8ca3f3a2eb051ef1b43d452b54c2352e8c9d184cbbd46ea936161067893164a72a34f6e2fd63c94353e524c9db699d15cef86f970bcf6a92dc367152f9740e84b2c292dbb338e4f21e6a47171b5a5cec0f4026906b528ad2c115866fb9e807faa200b600637abd774148f7637789946a82ec5c8e35692470c0c0f12a6a39a0c5dddf73635f2dbbb82d9ea56d8313a25cfd879ee024ee5144817b1f96945f34632cf0a5a3dd3ebcb8b42bb6c4b1c3d2a7fc6f817c97ec6282a3981a8d9e3d7070c178f7df847fcec8013b0a0d07a4f8c13c829209172022e0d724caf5a01219575f1d5a26f50f53d72ef8b596ae2263d2c2eeb822166178d56eba9ee9a9c60adb4fe3a7be89044d34288c7bd59cbf6e88e01f971b687200fccdc68d5a21f8430a8e5654a17f05773b2254eb301a5624c4aadbd3db8641cfe866ff72424a891f5e0487a48c204870c932ed9895427ce9a3d0a183fc40b7bbea3617b321f35cbba2c2180908e9bea2e07e1e5d8d5de9846994b78a313b8828eca935f4bdf9ca861326cca20776c6a4df35eeee257899467973941e6e8c567054480a7d8077e6e61ab480562ad53ad8127874a7fa4f747ccb2601cfa6e8c43e89f678dd38e4df80adef7bf07149edd32bf0e9775f3478870da6af26709a5bebbac77018050a07f1a8ffe4539c2df9de1dfe18c9bf0aa640178b0912bd9631acebdc2f2f993039b6cfa414d5cfcd0c363a178443e954db312efcd4463ee7cfe0d8a1eabe74d47692653f996cdd9b93c9fad41c5b16aa91a7bd5b267578a537e45d2af048ee0ce09ea41c72020d61509e5e91575022e7d5956b8e680f229909642366d5a16a17f021f41ca3d13df75ac3d3c8c772c2ba042680ba81466f8d05c07e19b1cde5a4d27ba6b820a63fffc5a42234e18109f128cb1e3524d621e38855b5c11cfbe7edf25dc3cd470308971b655b3381f946531d89ed23ad24f6a3cacd022fb9e5eb1d5a188a5d0936d0dfe3c7c424dba5f8892b2adda87a171d73bb5c5b8f809812689794ee3a5a7dc7d80f9c84bdad2fb7f89b2a45dc25836a42c8757eaddabcab7f82e1e51502cdcd3c4745009c55fd70b1c8f1a748e71e2b96b312f71f21cb00002710000000002faf0800000000000598cd446da1a38c05425e396afcb5fbb37ea2a9db15c56121e2fae11ec9f06da63917610371ece811f4d34713b42d65c38a775487c71c193a52df6ac46d6a013164681dd200000000000000000000000000000000000000080000000000000000000500000000000000050003d6ec06b2431d4e1f9c36a610af90d6c40000000000000006000317def351ac5745148ba3275aacb2b68f000000000000000700035d913d0e286346e1a50fe900f618b18600000000000000030003c35487d4d1e24e38bb4f923885d19b44000000000000000400030c7e6ecdbccf41baab2f7d95ca9e5e9bff03d32570a2783f1792d20fbe141f337d5dded9509c875e13288fe140400f36560e240a007eacf337b3a80941f2defd530cf824f13cd263ba2ca194e7170072ce28e8000000002b40420f0000000000220020e2eb1e4df160f9e11d4d06a6075034b47e9d3e1c3d21d0feab64905d2e6ab17b47522103174f3de90105ae02d88ccc1a7ed4bf0d116f27ef6010f1ec308273c1010ae3dc2103548b338f50bdae124711cadef7a82ce832bff61826f13b031677793abb8c674c52ae0003003e0000fffffffffffc008030b0e83d8813791baf507a3c85c55d81cf80a50fd9f08504260743bb8033212800fc0003ffffffffffe80101460f774080d0e1bad18203f9af025451bfb4b8de611116890dd1fa45dd0f8e9802000007ffffffffffc80104c31735de0c70dc61744d1123646e49d871b662ae090c568a3ba7666fe370ce0c0003ffffffffffe40a007eacf337b3a80941f2defd530cf824f13cd263ba2ca194e7170072ce28e8000000000000061a8000000000fffd0205020000000001010a007eacf337b3a80941f2defd530cf824f13cd263ba2ca194e7170072ce28e80000000000a1c4a78006204e0000000000002200204c81bcdcc2c4f01d3c045348a155c9249ddba4d48c16ccf33c1d8b6e34614180a8610000000000002200204c81bcdcc2c4f01d3c045348a155c9249ddba4d48c16ccf33c1d8b6e3461418030750000000000002200204c81bcdcc2c4f01d3c045348a155c9249ddba4d48c16ccf33c1d8b6e3461418030750000000000002200204c81bcdcc2c4f01d3c045348a155c9249ddba4d48c16ccf33c1d8b6e34614180a437010000000000220020680a82e78074810847ca530cd74fabdbd2e34beaf0fdd2bc832ddfcd2669876c00350c0000000000160014e1fdef3169db3728018a906129e3913dd841ab0f04004730440220639ddfeb4cfd418c28418208226f5f6be762b65bb242968695ac1574d1fae8e702207bd3b3f60f12f171a37e50b7126c709a4cb87e417efdfe520707962d393bd28c01473044022058407ee97260aa8020eea7036f348ec4959fbd8a820e8628244705138803e00e02207d53e7fec4e01bbc04a0495e8c31210c0c6c2542e38d6c9ae677b78bd568cedb0147522103174f3de90105ae02d88ccc1a7ed4bf0d116f27ef6010f1ec308273c1010ae3dc2103548b338f50bdae124711cadef7a82ce832bff61826f13b031677793abb8c674c52ae34016920ffed02000000000101efa7874aa335cd976b8209145d670a6dba17471f6a4f47dacaa432dd972d801c04000000009000000001c62401000000000016001401396601573e7aef81f91a89fc4b4b56feb7a35e03483045022100c787b74c895285a11e04a680a50cef980d74a1ea53998bc2abaa2f7e13b6131b02204e0a8216f2efe89880feac989f9b0cfbf6aea1a61444d453d5547060aa65cc5901004d632103d6a391d54b9683da5c4c1ab1720739757422861b7ca98da226118061b40c412e67029000b2752102f2b77c73154588dfa6301b6c69062c0a713467996542a836f7a2ce6a239ba68868ac0000000000000004fd017a02000000000101efa7874aa335cd976b8209145d670a6dba17471f6a4f47dacaa432dd972d801c000000000000000000013a34000000000000220020680a82e78074810847ca530cd74fabdbd2e34beaf0fdd2bc832ddfcd2669876c050047304402201f096356d49cb9c0d1dad9a01530aa892aa75cdcb626f42ef09b734f39d0a26502200d6274377598b29f3f3eae1281a8b61d5cb9853b478cc973b8035af5d60c26a00148304502210093d252d2f22cecee6c2b604a1190120db574f0ba33b49ac826229b95071230bd02207a4baa7a1704813804ba9b7d0e2aef3932a7d7d096862c1636736ad3296a46f501008576a9144a2fe35d15fdacd3b3cba332b5879b6acfb2e79f8763ac67210375099d4e25580f8d2c59718d7319e9d26c7119f9476ca3a623fb638843d3c8617c820120876475527c210375f5c7f3e5f1da49febb149cb9dc97098402628f84b79b586d5d516c17e6327852ae67a914950b69c651378982bec084b296e6dd23a33c41b588ac6868111b0600fd017a02000000000101efa7874aa335cd976b8209145d670a6dba17471f6a4f47dacaa432dd972d801c01000000000000000001c247000000000000220020680a82e78074810847ca530cd74fabdbd2e34beaf0fdd2bc832ddfcd2669876c050048304502210088605547607e6c8b1b24a671ed340700ad396a8bf613fa9e4646c0b11c2aeff7022008e4448ba70fe48903e3034ace605d8651091fcd6f80fddb57d2e2b09da6765d014730440220774ca3fbbee6dc29db89dbb2bdb977afdf31727548aa6060e9e23a79090bf6580220474a5aa2de53dae36371ec535e13e4291c9fbd1942e7cc812ff4ca4c9d84483c01008576a9144a2fe35d15fdacd3b3cba332b5879b6acfb2e79f8763ac67210375099d4e25580f8d2c59718d7319e9d26c7119f9476ca3a623fb638843d3c8617c820120876475527c210375f5c7f3e5f1da49febb149cb9dc97098402628f84b79b586d5d516c17e6327852ae67a914950b69c651378982bec084b296e6dd23a33c41b588ac6868101b0600fd017902000000000101efa7874aa335cd976b8209145d670a6dba17471f6a4f47dacaa432dd972d801c020000000000000000014a5b000000000000220020680a82e78074810847ca530cd74fabdbd2e34beaf0fdd2bc832ddfcd2669876c050047304402202c736579e487f2ceedb15546a438818e9a13d3b9f9232d4ff418855f9112795e02202bce08fb52f8737268b616158eeb06e36abdc83942396fb407d3e603ed1edd7b0147304402204ec5ef779e7b0db022d9da0986bee62eeb0372337f3974e0bfa8ecbddec7a3ed022047930ce5ca6796d3cde21dbf9e0678f4d0af5d84889c24e2b960786d141b900a01008576a9144a2fe35d15fdacd3b3cba332b5879b6acfb2e79f8763ac67210375099d4e25580f8d2c59718d7319e9d26c7119f9476ca3a623fb638843d3c8617c820120876475527c210375f5c7f3e5f1da49febb149cb9dc97098402628f84b79b586d5d516c17e6327852ae67a914950b69c651378982bec084b296e6dd23a33c41b588ac6868101b0600fd017a02000000000101efa7874aa335cd976b8209145d670a6dba17471f6a4f47dacaa432dd972d801c030000000000000000014a5b000000000000220020680a82e78074810847ca530cd74fabdbd2e34beaf0fdd2bc832ddfcd2669876c050047304402204f63e06d948b7890c8a3fb1ae568e88de8cd4ef3364c092bcdeb006c40558b0802202a5fc572f9f02f5d2a0870101ff7d900da184fb19b5cd410ba0779bc55533c4a01483045022100ff0a3853c07d426ac93a8b772017a41f324558851ec69bcd65ed1cd81490e0f102201c3c01574fe196df6311cf21fbb78abd7d40659479ea080c7ccb3c5fd53027df01008576a9144a2fe35d15fdacd3b3cba332b5879b6acfb2e79f8763ac67210375099d4e25580f8d2c59718d7319e9d26c7119f9476ca3a623fb638843d3c8617c820120876475527c210375f5c7f3e5f1da49febb149cb9dc97098402628f84b79b586d5d516c17e6327852ae67a914950b69c651378982bec084b296e6dd23a33c41b588ac6868101b06000004ec02000000000101ea542e3a509a29b8d2afac84f21bc8f4e100df901b35c9fc19a331c588683519000000000090000000015c2100000000000016001401396601573e7aef81f91a89fc4b4b56feb7a35e034730440220359f29d38879b19665015a2b85bd60e4e3949292f75d2294d5a545aa392dd1fd022036aa2b42bdedd9deca319eece3e8a5670fe89e78231e1025b9fc1079a931398d01004d632103d6a391d54b9683da5c4c1ab1720739757422861b7ca98da226118061b40c412e67029000b2752102f2b77c73154588dfa6301b6c69062c0a713467996542a836f7a2ce6a239ba68868ac00000000ed02000000000101374c80d9d5213e7bf0ea94c3a8085866a4d533590d89f68e829162d03d431a2600000000009000000001e43400000000000016001401396601573e7aef81f91a89fc4b4b56feb7a35e03483045022100d486e826ecfddcc474b719cb69e641efe97b4795b8ea4bb9729237788a3d35ed022051b097591bfc53dc855303672e17ad8488de9f2bcca670995060f331a7dfe55e01004d632103d6a391d54b9683da5c4c1ab1720739757422861b7ca98da226118061b40c412e67029000b2752102f2b77c73154588dfa6301b6c69062c0a713467996542a836f7a2ce6a239ba68868ac00000000ed02000000000101a436af3ff69624fd11b543c0b8d451190bcb7891a7259832cb64445571dc3003000000000090000000016c4800000000000016001401396601573e7aef81f91a89fc4b4b56feb7a35e03483045022100c203f2093d4b182ba5b78a471f806acf13c4d44f7191068abaf2b7a6742f427e02200fabe2a24e947f467a48fca7bba3ed68387ce46c32533366d99a52cefb0e3ad801004d632103d6a391d54b9683da5c4c1ab1720739757422861b7ca98da226118061b40c412e67029000b2752102f2b77c73154588dfa6301b6c69062c0a713467996542a836f7a2ce6a239ba68868ac00000000ec020000000001013798507505fc7de865bccaad655dc03b3899bba86e139dfbc543a2a2f9b4b842000000000090000000016c4800000000000016001401396601573e7aef81f91a89fc4b4b56feb7a35e034730440220724b0e5a340e80de26f3908ae510a34c93b5632dbd539c1a155856cad5375ac402203de9139fbaadd86a11168c612b75f974a6143e68c564de77c8b350f2978ff2f901004d632103d6a391d54b9683da5c4c1ab1720739757422861b7ca98da226118061b40c412e67029000b2752102f2b77c73154588dfa6301b6c69062c0a713467996542a836f7a2ce6a239ba68868ac0000000000000000000000" + val dataNormal = hex"05000601635f355f559285ba7df0603df45ecf2a4d2e19dc5025cbfced52ae7d3518402101010002aaaa00ce2f18a967dc4f25f414e671ba6585f8ef0b8c5fb812c21064f55a2eaa0009f1be4fa2ef24fbc4722591a2c93ebf23eeaacf0e917275156fbcecfd3f4d2e7980000001ff0000000000002710c000000000186b0200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000180802aa698202bbbb671d15145722fb8c28d732cddb249bcc6652ed2b297ff1f77a18371b1e63ff0000000000004e20034a9fcdb3b656feca23e62dec62f971f3da8841cfb596991db1e94b4fa31bd75a03bcb4056f62387ef42a2eb855b79a94054c114aa815e3d3f8cc7f3f61e5027d2803cd767fc0acd4f4e974ba30599c55d2ece63b974aad10d6e30424769eeb1f265103e21f886691747790cde23b2c65a95aaf8b5a3d488187a114343e0c0ad3a72912000000140800000000000000000000000000100802aa69820001000000000000000000000000000000000000000000000000000000000000000100000000000000000000000024635f355f559285ba7df0603df45ecf2a4d2e19dc5025cbfced52ae7d351840210000000000000000000f424002c344d288048d49ce0ac2fb71a40ff0a128c9a301061e69f4949da90fbb25ab6b0400012401010101010101010101010101010101010101010101010101010101010101012a0000002b40420f0000000000220020dcda7a0f7ed8d5f0794b7337b1f32d0849906aa11b8326aa74735a82a555957f061a8000002a000000000202000000000000044c0000000000000000000000001dcd6500006402d000000000000000000000000009c4000000002faf0800000000000bebc2004c1ff676a17a9ddd84b1f1955d0ca6d2d9593a0c05ec4fbf97d7eb979752e9e40194a03db75a8577dd24c7e4e15a5c90b078d8e09e90590f9b3a29e1dec217aee94cadc472f5b379ac31bef8dd4b46b3d4c4838142176f65ea37327317de6053da000000000000000003e800000000000003e8000000003b9aca00001e009000000000000000000000000009c4000000000bebc200000000002faf0800fb278db08cea85146fd04fbf3533cb1dc3c995c0a2356dc97b48da6a0831286e02976ca9b96fb46faeedbcc9be78577da3d6a55aa84248abfa89ad0815eff9dcd3000000ff024e5ac7d7df6e211ff7a7a0a0c7c5bc338b5ca5fdafa693fc6fb212fbe778aade000000000000000103a57caafcb7e674ff01014796cf9418e3fffd01ae74496b7ab70410b6315053f0b9bab6169d3f2f518102acf70eb108e4f9720f5305eceb19c643667f20351e7f5108bdb594d71d4de15c1f3a838edbe32e0e79be60b30057c47fbe6b37896e350bcd4c039f3f6e5a83ce1ad4cc991914fd7df244387593ab9762d1309116cfcd4c93359d240c7014769e56dd6e949782042e3fd5c5c3a3c582ae450844574694311cc8fda45445246b1c28be736bb306db70afc02172b3562b1b827f37290e8c113cd5617c6bddfa32328d3607845da71ba2905d9786d785aa174393b9aecc32605d89b81ae4f7f7c727f2ee455f774176c0893a2eb330de6ed8b03882459d2734b03653b84636a0b8bcf57d8c1a09c010de4309000006226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f061a8000002a000002aaaa00ce2f18a967dc4f25f414e671ba6585f8ef0b8c5fb812c21064f55a2eaa02bbbb671d15145722fb8c28d732cddb249bcc6652ed2b297ff1f77a18371b1e6302c8739e013b1b3de3a3ec74f3e3c28f137b7cac16f85e89c6a0f6ab70276c79cd02c344d288048d49ce0ac2fb71a40ff0a128c9a301061e69f4949da90fbb25ab6b8866ef30667c0e95b4f5c3b959e2fa4d7c3f5dc72a0099c53a82e54c35dcd60db723c3b1582fea00d611ab7ca34a3c56dbf0d8f4362af752e192355238f2746c2006226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f061a8000002a000068ba93080100009000000000000003e8000854d00000000a000000001dcd650001000000" ++ hex"e7a7822c8699669946f5e684671d63e68cc7c9cca2963945dc21c52232e6f83b1875b2bed7c80c37371a480a2e5255d49d390c3b2adfc695036ed91371cda7d79bdfbae464581f0b32942f03826aca17ab9da6ade4a778d310ec3da17fc3af426d21b347aee7c2db7b5e188e35714dc514e3a1c100e8595c9e0e4399ad796021976f077e5733ea535cc6daec2e371853dcb715fc366ea7d6b9a5b3509dccf5c2e1225e3a51de9f5bb9b6586b282a0b27a9ae7ae8f2be14ec677670241e384b462eecfde68957839b1327c9e5c622c0f67cdaf3845ddbe6f754401d720d6b6d5c061dc906bfa70fb76e1168c6ac1a25cabe8873c3c1e540ae44ae631a2638accd7951f368442dba7b38d0662ccb0140d1e4ca23f51de731a6f5adcf816c3235359afd607e58948da29a5f06c96b4312aee7d35ed4c2c811a58c5a196ac2f377d653d51cfccb5213c928955ac880b5fb1b91e88a52d5c217cbf78e071275fe626c230fded548b0f1667af1309149ff74c5542119d4e269fdc1b241d9f53e02e38e015b7c5c2d2ee623bcb4167e37edafddc7fadd642c20f81b454db1a3b578d527f124dbd1f3d99fdd1590256ae4e47c2e8b3bfe8708a0d5506d6ce8b130ce6b70028161454a5065e9925d75c0095dc24ba789489fa1e9236e25330ca1a45e61224ee027664f6589028a240961aff09187fb719ff3477b56427189b7b3c790b4031f6539c5e3a8a5d7fd99c2534ed1646920a43e7315bba98d59c51b337ba7a1b038006bb574df46830f96a5685e07ee8a0a41e712810faacfb231c67e69d0fc24b98d782c70e15524d5d2dcf4e64b4e26772dfa7067f6dc7ffc8b06e6ef3ecb13f927d466e0cd3a7ea09aaed90b7810bbfdcd8a1274bbe78a453ffff11ccee62059ec25955b34a1a1cdf8a3e506d99dcbadf16032646117556ad71cd93ceeec42be0350a6c9f194fea783558c42a56d034dcaf6fb1b28037362c7c6e2446bcda71d0a88adda3144589447ef13cb85a4d2cd16ef444097bc03e32c3b1a055e952f7ac87078b04deb900375a16dbd382ea4375ddc0a9645deca620590337e803ea8b41337f3f4e4030119a2337424dbea3d21214063ce853843dd4e6df94e3dc3bbb36d89d9eb8e15e52d0699bc6ad1de9d12afc95c8785d63756576d357126e13b25502d542774f6e5d2fdb559d52698d08600fd05aa5d10939a524d7b2ab225cdb41decfc129208c372d51ca1db705bb1b9831e53ee0000000000000003000000000007a120bf5899359b258acdec0ea500e437267aa6ba18e8509e15df74518f6ba7aa348f00061b1000036d768d1d63f1c9f09c252b5b48cf4d2db3f4935150c551391b37727723168982d7e58ce2a769313e461a0f3791a8c0208a769f5bdf4d57fbd0a9de104a0b1bbc1ab25ebbf87d1bc86dbbd42fd0dc0ff18a7ebfdd692c7dc3b95d095052540ce7801f3e25cbbcfd9dab857bc39624f59facbfacef5ab1e1e1f889b85f3b1f2a580cd660b73aa662fb15487722bd3c3f93d272725289136f2ee0fae4efe1afea25e6d4fc8334a47ad62d22be06605a15538dbd2a81311c4489b91d1cb143743e4570a6345c4f035c060aaf287ef66e7ebbe7b9037c10b66e087827478fdf76a02d25fe90e0f8228c1edfcb12eee3dd4e505a6c5a7bf2f5954ebb5560cd8c7f8b8f3f4ddca41a48a0d6c0e1092dcadc853752f459486bed349213cc15044585255842717ab70a3de3f0eedeecca10112c85a8bc248dc66c883a62288b49588d9fcc048c51081e94d65bd4731e7d71fba13693b82d2831d3bea370918aa5ddf1cc4f0d5015bb8dfb951ab13fbb9d26d5cb83dc980fc36f1712d616ce24d2c530253320f4f322f093a605ea426c577544f2983efc80be56791f443652c2233039a68f966c2f0b6351068616755dd2036b6226244d394a2b5b59160217603149901e8abe19a2bf404f384c2ed7fb0c5e470ee5ee8561f58f66bda729c2c8816853ad2357a009e537efcb4a28e845ca616be917b15aa6b8eb280bfcb321ea62fff21168b8ed54d58ccfdeee0e7752bfc0f02549d76615c85dd1e152a85ee931b34f436439e2233740328ba504c49f9764e1dca645ebf6a1377310ab53b68b4d0a6e6c952068249b86c29061725035db8d294ab9c56901485814735aa2a8d6987b1a19ced65a332f97751c4cd8a27093851f7775e5314078c04d254754d976bed2dbd2e6ecda62e9a0c7fd95299b4b13a54c9498d384210fb42d3b6bc5d8f0d42e42879f86c21eb7c5c6d1bffdd598b8f3cfcb75df159f1125a65f960637c62c7c5632d73b7b4b0544082008ede22d87e79e20eb08be0817650fefcd111de48ba2be02a7b080275c991a0ee4445dab89312644c7cf4101895e2dbcaad7d87e8e3b13e62751861b204a7e6f5a476eab0817c294d59aa0247903077d4cbe4a98e7984d2b04623d2b2ef4c650b43db15541ede229c12c045529b5c77993eb6acbdc28d812a486b5957fb996731980555bdd59ad824a882ebe1a77cbe6b9035f1c69dd01b2a27a47be5febfa65c721354e70071b07db4ebc2f01d143587c1b32a5337dd010d2a76a7773f4a7c665b4cfe4a61b103b1c319d85e007eb99b52400cd8776697e1d6118197655bf7bd0a5e7f4594bf36a2706128d5f5c3ee166b586c4d515611f4597a4c1088c1853a5959f73830cb973ea922d6211ee7d9b1d67b1025486f8f3c72a517d0d48d9a57d64c0f48e513c3b09e14ce91b515a87f3035ab55d241ccb12108dd299a362af26af96ada920202dfe26d456065717a85e6bbec540637059d82480f6c917a11434a9be5fee5ba33a8552b7b0e59f123991525e1dc14bfdfd109625b2df477bce565045375dacf6ffe99081914fc9f64df7ef8eb26801ce01be083555fd2f8a338a33c07f484b01a310ee420f5c932cfc5a8f6d32a20c3915a188833ac5775500c5d2b73e4ea595512e869f91f83de8e048c804ec8cd6de45b7ad264bcfb3c235325d3c09673a2a94e60736ceada065cefb1e355bd35bd4b56ae513343e85a3d840c2211da2d1941112318f97f825e1714f74fc2d33430196cca2423f43641b0d75b4b1a1a1ef0f07af31fda06220a2628aaab9f303cb4fe6d73fa9b39e8f6083af3bbca1becdd646afbd888c560bbf7cf4fcf5c02f712726eb23b03fe5c290df63a85ed1026cee07ab0d2bb868aa4dd6594252cb75ad5d11d9c53c5238047ea3d311c1443d478dd283a260270400002710000000002c2322200000000008af34a0245d10939a524d7b2ab225cdb41decfc129208c372d51ca1db705bb1b9831e53ee000000002b40420f0000000000220020c57347ca54a9e6f279f6a1f1e50f19b48289c3baccdd356fb03033ae7ccb6e444752210252a01d9d8b03db1a99b1a82223f7dd60ecffa69dbef47c06228fdea2fe0f0215210322a747c1d7f77fc7577a689618bbeadf28b941412404ac5e216d684a32d57a8e52aefd01d9020000000001015d10939a524d7b2ab225cdb41decfc129208c372d51ca1db705bb1b9831e53ee0000000000e136df8005401f00000000000022002019cccc41d4120d54831ad41b091b1e52b8902711365f1cbd9a44a7b5a3dc22b450c3000000000000220020653fa8ccacaea967b6bbfe9411c3f812cb44c4a18d4f6806fd81512009f706e750c3000000000000220020c9c8b5e68eea1a6f32394124873d68c66b1cab8ffe657c852696e1a1597783d924390200000000001600141fa232bbb376f103dd015631dc8d6f4f9d306122241c0b00000000002200207121fb3dfd089e1a7b7cf0934e4cc42e99678c7db90927a358985a6471d0cffc040046304302204b7524262f7aa8fbf4e9fadf476ba42b4202617b16e4d1e1ac7c9db1adb8a126021f59622119b96d12a66c6f18f12b5c04764c576f9ccb4216049ddd6e719cef9201473044022003f76c8bbfc91e5323ddc1d7a95e8a3c7707de898e6a50fd5e6562b8412e32fb022010b2bb7edcf519a9451b3883832a39a0f9008903b52fb2db90e6e371bf20307b014752210252a01d9d8b03db1a99b1a82223f7dd60ecffa69dbef47c06228fdea2fe0f0215210322a747c1d7f77fc7577a689618bbeadf28b941412404ac5e216d684a32d57a8e52aec34a86200003000324489133c313ebc91202d43a19b2034b1b2df6ccc257712bb0c38cd1d3141e42ac000000002b401f00000000000022002019cccc41d4120d54831ad41b091b1e52b8902711365f1cbd9a44a7b5a3dc22b48576a91423ca5b93a48d00f645081be0479fa799c70731a28763ac672102897afb6799af1c1af49fe4c96800ee1cea77553c0b5ae9b8cca5347238cf5be77c820120876475527c21035a26304d46e27993c218a0c644a613ca87630c8f0e1db5f112a01c3af26cdb8f52ae67a914d3260c3a0710948e34d708a99ff708f9c257ac5288ac68685e0200000001489133c313ebc91202d43a19b2034b1b2df6ccc257712bb0c38cd1d3141e42ac000000000000000000015a050000000000002200207121fb3dfd089e1a7b7cf0934e4cc42e99678c7db90927a358985a6471d0cffc101b060040ac4bb46e1b736a6fc76a4d86b24df1e9f9f91013025209ba64daf333863a2bde59a51a7af931bf0b07d6154daa30ed3a9c8c33d173c75765b46674a4e249bb734061aa4d418884ef272ea2bd65d745c42b5bd5f6781b7f876402b10eb505c60d086ddf3e108528e65f4712503114c24a20c8fab7867761677401defa780c4574f5000324489133c313ebc91202d43a19b2034b1b2df6ccc257712bb0c38cd1d3141e42ac010000002b50c3000000000000220020653fa8ccacaea967b6bbfe9411c3f812cb44c4a18d4f6806fd81512009f706e78576a91423ca5b93a48d00f645081be0479fa799c70731a28763ac672102897afb6799af1c1af49fe4c96800ee1cea77553c0b5ae9b8cca5347238cf5be77c820120876475527c21035a26304d46e27993c218a0c644a613ca87630c8f0e1db5f112a01c3af26cdb8f52ae67a914caf9c0d315f1ca821720d6911e090095585cee2788ac68685e0200000001489133c313ebc91202d43a19b2034b1b2df6ccc257712bb0c38cd1d3141e42ac010000000000000000016aa90000000000002200207121fb3dfd089e1a7b7cf0934e4cc42e99678c7db90927a358985a6471d0cffc101b060040dc5ef2218b256e0db9981ab591cf830819b62ae2068b6c4d19a85ba03153ff39316c14c1570a8fdbc2765b38a16c74e76ec7190f47aa3754e08c770d8c7e918640678fd252d77d34b3ecefff1fa369e92dad0b4a2632efe13974e538e13e14780c4c6c21838a7ead4878c9836405e45333651de6cbc012af4d6c1bda4a0039d5aa000224489133c313ebc91202d43a19b2034b1b2df6ccc257712bb0c38cd1d3141e42ac020000002b50c3000000000000220020c9c8b5e68eea1a6f32394124873d68c66b1cab8ffe657c852696e1a1597783d98b76a91423ca5b93a48d00f645081be0479fa799c70731a28763ac672102897afb6799af1c1af49fe4c96800ee1cea77553c0b5ae9b8cca5347238cf5be77c8201208763a9149bd34d5a8ea2d91b8ac4d1fb4ce79bcfd481c1f088527c21035a26304d46e27993c218a0c644a613ca87630c8f0e1db5f112a01c3af26cdb8f52ae677503101b06b175ac68685e0200000001489133c313ebc91202d43a19b2034b1b2df6ccc257712bb0c38cd1d3141e42ac02000000000000000001daa70000000000002200207121fb3dfd089e1a7b7cf0934e4cc42e99678c7db90927a358985a6471d0cffc00000000ab009f4d0b317a141e5b3f865599676f895834400b66dd09d33ad27163ca176f401e7b146982a595ed1a95e1225312dce24f45237477efca22c759c9cb16c4a6bc1c9e8834a18eb0c6c389a14ea03fb7062f788003c5c58e738d78e28e004014a64013f1f51471ecdb32897a5f604a254334f7f5fa26de5fc63b96c942db2196ae9e43b34f00054e0c58a061d5ae5f4938b5d449b8f9371edd76efdfcce758a102c500000000000000010004fffd05aa5d10939a524d7b2ab225cdb41decfc129208c372d51ca1db705bb1b9831e53ee00000000000000000000000002faf08011b29e60a38883a8d4434f17ca3d92161109f7ddd8799e64e86d6b8509babd1100061b100003b8c1771721551fa88af8fdde92909add6e5b8fa90a4b0484ee065ed3ed7c56e733a7fcd775d4ab92956c1328ffee9c195004c5dee5dd5b9d0f9034589e6769e72579d3d5837ad70785ec420b4a24c04d36668728c0d2534ff1feea9aac2410423fd79c7db9231ee7efd3d585646e378fe53d731d18f38d6356a970f5c3026edc849d49ff34e58dfb7548512461110088ac3aa800e10785029ba3b0e9ab7bde0f056939e4921792dba2f5c005135daf57e32cae06a9ccb1b4d321f3ba015e5b92def1ff1c200e56b3990d82570586bfae26e9398e17dc6c069f92d80e6dfedf6b2f24b1dc3cc9d63e684d861f40fdbf508d4ac34b7f10c57be2a9b0c5921f86869c29ada5394b8780d2488a4fca3cd98ddb0ff8ea4415a07caea436682835744e94d5cff6d3024a9525dbd697e499b7ef23062b18b225bfaa4c5bb07166f34ff7866ec8f0fbbc12f695c609692798364fa20bf7977e321deda3fe5510833494532fba94fc1f0dd14ec74f3e9fe8ee659634621b63d16d46a8958132c24bd82c516bdf9ae9515cebae42778e4de6be7047c31cf86c0df0306f7b6562e1f35be51e5e64cc6d9d4c010849e6ac7ddacaa4b7b6fb1d35aac815964090940e73a1193eece11c1c1d37e373ef58c5e2d690b6ed6338360af9906146da9db8329bd2786bbf92df10445ee093f0b1b2a640cc2daf003fa7141435ba1dd54f9cdbf5417fa7f539b255452852a85d2ce97ce5abed4980e7b409e283f97ccc9c01e104b55155f96ace6789f61c4661962d34fc5d7e6f5f5233180933b2fa7f7a5b074714645489f5221966160946b7bfbf0fe6733e6beb8af4457b9d36cde1200811009ec483a9d730ca980aa28f636942af5e89794a8edbc1b75d555ba134974374d0fe23d31c26566064eb9998d649bb2bf066bf710da50672f4e3ab4df843a0c8942bad0a071c237d4c1759eca37380919e36aec73284db202a32d3d1619f3e5b757b2df8b04bde567783dc8e465d996799782f1a1b8de9331681a35aa04edb427de87264c8ae9c397f29d3e8730db91256425a10b960a9de1a48d0d4186d617d2b69c87e2540f6570faff4ee1f6303d7d281434947abeaad83c86a4d25bef4de2bb3c6104aa0ceed7c8df039f4be6a42851a118adb1b8f98e02f6727b75d98541bab2ff24fb2f20342e86150c678941825409b62a844f44ca1ccdf0d9f7c2cf9b222fbed00bc92be0802fbfbbeefa71c8976cba8fc4aeb031480f434027b1cd593d08cbc14c2a360b736b06b5afb8da35f0be3818fff4275b8c830f5248a8b8edea1327454e1360bd90d4fa08e965f459b0b027e1180290cf762f813a31e8109f472d9657b03af737d1f7bd2e59441541a84ba818f1413c5cd1f8b9882e9188e0def9e44e2f4a7c710c893c7188ba86423f8ae86068d84e1832af548289e87c34d68b186df7e24ca5b051f8f5e4a44e2e7383ba2a09615b4147b34e86486731290ea67f3be24c13a9c5cc37f06555989b3f10c580a9cd2b416d0ee4210855c6833a25996761dfabb036f3893cff7db7e310baa8faa79f46e0ee43bf4dfd732eae7f44bad2e7c032b9c6d14947af6b0e37e5ec98372a622f716ffba0cde04b9d4508392dd154ddc34829412bfa604d4f00e4b10a553587343ef5c0944165e7ee1e34387b09c147ecba943cf36dbc4269efe50ec3a5a3075c43be9651d6db6acb9f657476952b78c990557f05935247a71077373ec436ec586def177448f8859ba096b7a838e5b4ce7a463f9082f705c26d99936eb1be584ea9b58a44b9b4faa07fd8247fa66cf4529d1b8cdb92ed7bd96bf0968db4376489c7d46f0f27d58ac884c29736502953723ef1ab41e19c7041d3e0e9091d7de2e3904d032de02292edb1225a672ab438d3c65f7921c06a9f181f8ffda4ac524d0e0fffd05aa5d10939a524d7b2ab225cdb41decfc129208c372d51ca1db705bb1b9831e53ee000000000000000100000000007a1200ffb841fb291ecf09ae7b5dcec1feeb45ba196ca02d6b9e50ae2b3dcd9ca5d72500061b100003acd0a3acb9b4f9540678dc9324480bb3d4f54e5e007739c9c1d600bff75dfbbd0191e75c0a2d810a6ec5b03d02cffbf1a66123c87790e66eee8d416caf82e7ea7090a62fe14276fa88af32ab9793f7a100de5525eedf71967c13c8d361246d98b78cc74383e714f88899f34407644c14874046b3b722186015c07b8db042db955c91cb14abbf3cfa646aea81ad15bf67108a763539c64c5a8e8115d46e063e956671e8ea8d8fd638a6414d71e9b475ebe070da7faf75a898f29048ab5a2b6c7e3a72a178b8e470e8375f539ebf6284d15486c5a8774d46ea164ba2b62181f47623fd987ff5958550c962a193638679b79fcd477fdf2a09c0fa879bb22cb493fbaed27518f5cb265741535b4ab14246077ca18f11ece7aaa0e01ec5bf02c3c3b541ea08bf254df123079c1538e266dcd3161bf1b9ed41d873f1491906e1459ba51ac9dd95e783598d3c356e0cc5b98c2a96b148f55f102a9810181eedd46cd00b445d861baeeac46eba469435aa4ddc877bd68b53f4d005aa2566d356cc344aafcdc86abd774ea28cc838d2fdc541c4b6da494a96e128b8c2abab4b21b3ae2646cdcc3528ef6fd8587b3a0636ead67a62309fb003afdc14177d329b062622313e9dee912847763c68678df663a39b89c69efdb6d916d5754534bdca9030955cbcbae6fb7ff1df6282175cd37a30a904418b976af05809f0e0e7e4b4e2ec018f1e9c6bcbe7a7822c8699669946f5e684671d63e68cc7c9cca2963945dc21c52232e6f83b1875b2bed7c80c37371a480a2e5255d49d390c3b2adfc695036ed91371cda7d79bdfbae464581f0b32942f03826aca17ab9da6ade4a778d310ec3da17fc3af426d21b347aee7c2db7b5e188e35714dc514e3a1c100e8595c9e0e4399ad796021976f077e5733ea535cc6daec2e371853dcb715fc366ea7d6b9a5b3509dccf5c2e1225e3a51de9f5bb9b6586b282a0b27a9ae7ae8f2be14ec677670241e384b462eecfde68957839b1327c9e5c622c0f67cdaf3845ddbe6f754401d720d6b6d5c061dc906bfa70fb76e1168c6ac1a25cabe8873c3c1e540ae44ae631a2638accd7951f368442dba7b38d0662ccb0140d1e4ca23f51de731a6f5adcf816c3235359afd607e58948da29a5f06c96b4312aee7d35ed4c2c811a58c5a196ac2f377d653d51cfccb5213c928955ac880b5fb1b91e88a52d5c217cbf78e071275fe626c230fded548b0f1667af1309149ff74c5542119d4e269fdc1b241d9f53e02e38e015b7c5c2d2ee623bcb4167e37edafddc7fadd642c20f81b454db1a3b578d527f124dbd1f3d99fdd1590256ae4e47c2e8b3bfe8708a0d5506d6ce8b130ce6b70028161454a5065e9925d75c0095dc24ba789489fa1e9236e25330ca1a45e61224ee027664f6589028a240961aff09187fb719ff3477b56427189b7b3c790b4031f6539c5e3a8a5d7fd99c2534ed1646920a43e7315bba98d59c51b337ba7a1b038006bb574df46830f96a5685e07ee8a0a41e712810faacfb231c67e69d0fc24b98d782c70e15524d5d2dcf4e64b4e26772dfa7067f6dc7ffc8b06e6ef3ecb13f927d466e0cd3a7ea09aaed90b7810bbfdcd8a1274bbe78a453ffff11ccee62059ec25955b34a1a1cdf8a3e506d99dcbadf16032646117556ad71cd93ceeec42be0350a6c9f194fea783558c42a56d034dcaf6fb1b28037362c7c6e2446bcda71d0a88adda3144589447ef13cb85a4d2cd16ef444097bc03e32c3b1a055e952f7ac87078b04deb900375a16dbd382ea4375ddc0a9645deca620590337e803ea8b41337f3f4e4030119a2337424dbea3d21214063ce853843dd4e6df94e3dc3bbb36d89d9eb8e15e52d0699bc6ad1de9d12afc95c8785d63756576d357126e13b25502d542774f6e5d2fdb559d52698d086fffd05aa5d10939a524d7b2ab225cdb41decfc129208c372d51ca1db705bb1b9831e53ee000000000000000200000000000f4240657c0bd97e795218aa623b27cf9a71764379c4762fcee8993aa0b1ab1e32194a00061b100002c0ec5d388a8c78491bbd870faa2c46e4282e11796123b69c2792e5806748ef068397f0212a33e3f79162ba4ea247ccec410db039dd323af48ac27bd0ba77ecb870c587477d4543d9a29c53fc02bc98d7cd0144c7abf80b999c22b42a28ae8d625478ca304001f9a49782ec970031e673c76e4e27357a321729d6df38d1d88dcbf764c69eda3baac9739bed637010f44638cfe1deecc56b76f6e02d1d0c3f104462b9ffa5f20de4cf092d86a5bb35d5f62b0fb1c983a2c06df17c9ffc809c83e4b4335f5903fab536fbd9719847bb063541ecbe05c12ef8d058b3547faca054e3d662250f1cc1f925dd71297abc25fe37ab33a086759fac76208a64552f84d2e4d84daccdc3aa2bbd2c2f922bf262596742dfe034529d1ead2975dd3d197ab0e2e1c75c8b8f160ca6077638022d4afbd107979949cf342cb399347f3990029f0db6d9ac0c569d61d42539371f9a7ff59e9c83ff97d15bf0eeb254ae58fb7b1f9d8710c546ac8a227930c66ac841bc4f475229e5cadd14ba5a01e6b2da99c55861a08e2100e62c4499d30003fe30ddaa347d7a27c2158d3787d58fe51ae57d797bbef7f900508d1580df3e5233f0887567fba1faa918c246d2ec5c3b7aa022cb8a652d00b4d719e312482f57655eee80a90cdc73151fd7ab9c5367793d60c6088fab98f0547d7f547e10db202a25e027a5cd0abc41bb0e3ef563c0a6d469a702b2a26f0e8b4fddb845a16a5f06b9dee33c3adb31430c94942c5023179d3e4441948a332069a1c3b69dca65f05a43452e42fd28a2f7e6344f98ab9a7e4eece3c1709be1f7bb620f8b6c45989a8bccad39a4bf40e8215183d1449196f1f9fc17de778b616856152e6e6145a1b7a3d7f226becaea5ebe34aa4bd06e60f0fed207bfd21f5663bfadd37dd722437bcd46a26fb4e19d062574a81bcd817eecbc1914a5878809128961acdd73113ae9c51070ff4494e16d81ccec777eeb513da82bf43d4884812b26546b4370dc315793271b069f60f4285f648cf122ed8b22b0c7a27e94ccd59a273eb774c109e19980e146850de95f82cdd8aa0e82022672024c917b281422d284df0ee0bdeb3d4ac56b4ca675ebdb835c17b6a822d79ae7310f4aa41d80ac61c5e45c1c0e1d64542622a31091a9f87c335e86d964dd85a951d7c9bf41c9f2b1a9bb8424d7d1b26413da8034182fa42d2b1cd1f8745482c49d8348d19c72cf5a02bd28e4cba82128af8bf5d9c1215c4f543ef4d185f100f8d803dfa29c300c072e44ad9542b82fb1380d55c15c9a4b4398876e2450b90b49990746f339abca8cc8a462b62329a128758ce0e46b5f998af1bb485a3044bd125424eb5c623afd2a11befe4ec544eafe275ed1ad82b940dad5e9a9710d48562e51b296ba81f2d70593685ba0e3f3b25089a187e61d5675dd481aa99620276cb0a841a3c4df201a929287b1127270c5d25d06fb286dae1a9a5a5cdb60003f0c30d2021074bf252e550685f7b51a087a77b0871e883104e55f898aa5bc4cf8538c293253a737556d7f220e15b90cf0eda7d5f2172372e3c50c12cc588f312da37191b5038e944825044b130bff281ecd47a4252a1411ab7a9305c2b37e9facd435e9c434de37641498f8e4bfa7b42966da29c84200aca87ea1c3b00db54906b340e524a7dc4a15403bb82bc24517cb91026096bdf18f5f7ae5640ed6de1f0c5d184813d6b9d244b32b58e9ff524741a39383eec3a530d60db13deb26e3523a725f0599b671b625c07002704fb600b77318417d2527537359d122e22a1f7581eaebdc19e65ba50bdda18ae08e9a8694fcb0ff1a2cd98d910dbcd52064c15a4282d67b278c72a0fdbf228abf6b519dd28ac21c57d1da4bb7ad5b5ab10da6b83132df1da79ccfc77fb45598bbd91ef5ab96d8a2ee148639a562debafffd05aa5d10939a524d7b2ab225cdb41decfc129208c372d51ca1db705bb1b9831e53ee0000000000000003000000000007a120bf5899359b258acdec0ea500e437267aa6ba18e8509e15df74518f6ba7aa348f00061b1000036d768d1d63f1c9f09c252b5b48cf4d2db3f4935150c551391b37727723168982d7e58ce2a769313e461a0f3791a8c0208a769f5bdf4d57fbd0a9de104a0b1bbc1ab25ebbf87d1bc86dbbd42fd0dc0ff18a7ebfdd692c7dc3b95d095052540ce7801f3e25cbbcfd9dab857bc39624f59facbfacef5ab1e1e1f889b85f3b1f2a580cd660b73aa662fb15487722bd3c3f93d272725289136f2ee0fae4efe1afea25e6d4fc8334a47ad62d22be06605a15538dbd2a81311c4489b91d1cb143743e4570a6345c4f035c060aaf287ef66e7ebbe7b9037c10b66e087827478fdf76a02d25fe90e0f8228c1edfcb12eee3dd4e505a6c5a7bf2f5954ebb5560cd8c7f8b8f3f4ddca41a48a0d6c0e1092dcadc853752f459486bed349213cc15044585255842717ab70a3de3f0eedeecca10112c85a8bc248dc66c883a62288b49588d9fcc048c51081e94d65bd4731e7d71fba13693b82d2831d3bea370918aa5ddf1cc4f0d5015bb8dfb951ab13fbb9d26d5cb83dc980fc36f1712d616ce24d2c530253320f4f322f093a605ea426c577544f2983efc80be56791f443652c2233039a68f966c2f0b6351068616755dd2036b6226244d394a2b5b59160217603149901e8abe19a2bf404f384c2ed7fb0c5e470ee5ee8561f58f66bda729c2c8816853ad2357a009e537efcb4a28e845ca616be917b15aa6b8eb280bfcb321ea62fff21168b8ed54d58ccfdeee0e7752bfc0f02549d76615c85dd1e152a85ee931b34f436439e2233740328ba504c49f9764e1dca645ebf6a1377310ab53b68b4d0a6e6c952068249b86c29061725035db8d294ab9c56901485814735aa2a8d6987b1a19ced65a332f97751c4cd8a27093851f7775e5314078c04d254754d976bed2dbd2e6ecda62e9a0c7fd95299b4b13a54c9498d384210fb42d3b6bc5d8f0d42e42879f86c21eb7c5c6d1bffdd598b8f3cfcb75df159f1125a65f960637c62c7c5632d73b7b4b0544082008ede22d87e79e20eb08be0817650fefcd111de48ba2be02a7b080275c991a0ee4445dab89312644c7cf4101895e2dbcaad7d87e8e3b13e62751861b204a7e6f5a476eab0817c294d59aa0247903077d4cbe4a98e7984d2b04623d2b2ef4c650b43db15541ede229c12c045529b5c77993eb6acbdc28d812a486b5957fb996731980555bdd59ad824a882ebe1a77cbe6b9035f1c69dd01b2a27a47be5febfa65c721354e70071b07db4ebc2f01d143587c1b32a5337dd010d2a76a7773f4a7c665b4cfe4a61b103b1c319d85e007eb99b52400cd8776697e1d6118197655bf7bd0a5e7f4594bf36a2706128d5f5c3ee166b586c4d515611f4597a4c1088c1853a5959f73830cb973ea922d6211ee7d9b1d67b1025486f8f3c72a517d0d48d9a57d64c0f48e513c3b09e14ce91b515a87f3035ab55d241ccb12108dd299a362af26af96ada920202dfe26d456065717a85e6bbec540637059d82480f6c917a11434a9be5fee5ba33a8552b7b0e59f123991525e1dc14bfdfd109625b2df477bce565045375dacf6ffe99081914fc9f64df7ef8eb26801ce01be083555fd2f8a338a33c07f484b01a310ee420f5c932cfc5a8f6d32a20c3915a188833ac5775500c5d2b73e4ea595512e869f91f83de8e048c804ec8cd6de45b7ad264bcfb3c235325d3c09673a2a94e60736ceada065cefb1e355bd35bd4b56ae513343e85a3d840c2211da2d1941112318f97f825e1714f74fc2d33430196cca2423f43641b0d75b4b1a1a1ef0f07af31fda06220a2628aaab9f303cb4fe6d73fa9b39e8f6083af3bbca1becdd646afbd888c560bbf7cf4fcf5c02f712726eb23b03fe5c290df63a85ed1026cee07ab0d2bb868aa4dd6594252cb75ad5d11d9c53c5238047ea3d311c1443d478dd283a260270400002710000000000bebc200000000002c232220b8e78d93ef82c5351ede6c6aceaaafb806ef6ff23deb81fd78e7a9bebfcdeb5302f6bed70479b4d98f5c93f32b382524233983ebebd14b2b50fe35142766300efe000000000000000000000003fd05ac00805d10939a524d7b2ab225cdb41decfc129208c372d51ca1db705bb1b9831e53ee000000000000000000000000000493e0f81fd474bea478df0202b311c69e85dc6215f629491dd15ad0929faae2535abb00061b10000370b2714d2734e6b8cde085794dd7b41c8a9b6c03c1edd8e3db168ee7fce39493596e882b18b5b1b79c16400c6762b9856075821be6be9fab8f469f56820d8f341554400a8da7f1f1a8501902581d43b9fa6e5c68015716f718a2190b87fce41bf1b509aa61806394a42489d63c457fe4c79e7480eced1315edd731887e57704fc9102f50cb7f0d242d755cfd5a2172dbaf7f01b124861cc6d1dc804796bbb84165c805f0c0f3fb9ec97b74c2a694de56a9cf8d79d1a679260ee1169d78214b34c8a654ba22e59ddcd32beff4713de33549f035b342660405b0159a7a508e5691ef4805689140e72b8a0ef2e61be74dea5d0b8f5589e0e373cac2e2e1cc39b2121c05cf4122ad0f8b9af6fbf1de2ea26376c2650ccd306c13a7b64acbf2a3feed128754abe44658009e642768ae3d84f5e0fa5f7f360c2a1c76d26985817ae77b71fb59014a5483ebba9271cafa5e5d8031c569adeceb8bae6444e98d2522b28f6682109fc7d31cdb83ebd45e5d81e7f046df42345b49f470dbef9ed87709301d2c6131215d33a30b8d18e63e54a2aff85dd57672f8198bca6a67ee147c7d0ae649e5661ab6bf78a662fef9a164f1e332b9f16e6fb3d5769ddcbc1d1c07338d3394b9245d17618c2474e86c064fca4df00ad3a93dc051fd8c3328cde2a987798b0f22a21c90426700abeb1e6f38dffb485b5477ec44c690fa80e317b32a982fd3082253bba8595783290dbffee4fc9296ffdf16a8bf3154971bb720e78674969e9db2e0fbab9e9e13f24bc8b3af5e2f00f262f0da56de443f70398ab68f747d35370fcd8e1c0e130f7269e08f862b5a67f2c129be254df2358762ce3a947eb27d66450af51540e7721b47c8a5a86098ea64dad381f14e07aabbbc470949a99c07612add3ab4c575fe2e520bbe511a1a674aea37a44535c13ee3380f8f39bd230fc1481cd31912af36c6751e23c6f383cd37a8b13fa7df9f0c7e460739f2c6226638ee14f14d36366211cbc6a1e16b4856bf302a540aa9d9e833b1d59c510473096384c8b450f2f3f1dab9e614af822949d5cc93d76bc4d1a52891bc85f1981ef83161195ab7d8181ee4fb163bc6c685a10e87c7f4b15ed7d05833c230a4a5b63841fc65b959f0ff010e697f47c583f9b7fa9b389c0eff6614e47d85b83c483136f182be4c151d272f5d938b912a95e47d333e5de6a409ad271679a778a7eb3f169c71525302fac5d4575e2645c09763c2ef165736a7a726ca605038e2781404328790ffaacef2b9c2bf90122042cd571287bc4e3973da65fbd4e3da9e40e4347ca6eb4ef1ffef4e5a34be80425cae3e81533f7f2953f95fca53a22057a39125f5c76350fba7fc6c036838fb951d0aa8702e7f44c6f8a9cbce3b64fa8ddc2bb8c8b35d1e29a21beda6fdd332b31a749321455277231fd9d70ea4aded95053b395f88fa6916d126e1626fc0f1be6cd2a9538d17c498b40927f12b3bb40fa3e272e82cd2242b670afefa387470f4e6e0a1236028954c9e90311f486617187956a23b90b356d71e219e6dd055c2120771003a6c12769aa3ceacb9642bc01022731ca7a413b68ee7d1d5444f75dfa51a68b74a01ac85f6ceaf5e56987b9d67d6de896f5aafd25c78c413a6d4b5b03d571167524cd231ba13bd9f80fd7413faf21e8170cef0d08b242c5c38a2b0158da56e358ba0692f670d4611c7a3624b234adc30c5b7198e0afc941f5d13eae3a94ddffa652c784c34c582e04e948da91a5ac3038a9df38fd4f1733779f4f122ca2d7ff9d03bac9def35d9ee3a183161f8f2808d472b2e64581209359cea58ca7757164c666029982223877e2b14d2d537afb012f1ffc12cd083c16dfc64213c56f3d4d22b603d3dfab1d21e239d6fc1f9f153ed61f1ac91c29c85c16f4aa2985f84052f5a08d32bddfd05ac00805d10939a524d7b2ab225cdb41decfc129208c372d51ca1db705bb1b9831e53ee00000000000000010000000002faf080ab009f4d0b317a141e5b3f865599676f895834400b66dd09d33ad27163ca176f00061b1000028fe56ab181f454372c939ca0516f5782b26cec2010885c70e55c41a1e43de3af6635d2ed0bed90837cefa9f1805f4808e8092a4d44efe5fb616ff7678487a460b367134fcc728ba273fb1d22718bc6a95e47a120a6d952c7c2cb8f38e59c2a4efa63977cced7e4b8f46e4d47d29098a73beee807c3337c4acddcbb32b78eddbd124b2f33ec6cd8bdd364aa4ad2c10eb69dab808fe5f5f0aec19750e51ac65a8746f345c05d4b8823ffbeadd6200ad01449c39a008fbe117a8ee904445488811336d0c439419fa4f285f9f62a34f10b076c99c0092968e3cc9fa656016b6da049bd56b910d7a9356e76d24e746b280f0275ec9e9bace82d852bf0a137ea02d4cbd3b68450bfb593564d8c20953bb758890a55a8c381a4b3303b61ec26a56111361bf7262b3f6f2503aad06758130d86ea607cdbba53415aaf253430d92fdd81c685ab39233e94654e6508eec1347747e2df2862169382aef6f99dd78b50629c5d98b1fcc73e865679d862b42f8e9d54ef6288ed2c3f2713f0fa4db538cd3e70ec1a30cd65dbf873f581b30892acedacd39b5f0aa774d1f3f77d8fd11ed628bcd02ac33f89123595aa455ec54a07e93e26f94338fedd8bb84094a0add52f912ed5f9019e3a28d90d251cc6ed7ffd35254dcadd9f1e9b28eb0e06fd4fe961d60cb690a7757f475c08aef07c2e54668121540a42a9c779623709a2124629e8c4bd4021763979647f625b360a4559dfd3f57798dfe5d36e9d902904af3ed67d8f4b0894538c7718f5160d211cec27375a7e6a2ec42f2c8fcd1c953b7b8379d42439a2c6b921a66d5102ceb6bd6bc20b17098e69a0a4f708b42520e4792474c3d115a12c83ef60ac6e69d8842c5981e9a6d178efa352e73e4a34bed4fb590dbeecb259617668e6ffb9f955297f26e3a6a3b95d9617529a61f08666ca1069d2ee1876337d3e786244c5bb45a8236577184584cf3018118d7e4e78973ee510b6773bd922797e580cd240dea3ca31892d23c1e6e4fa92f1a01da8ea40044f5613a9429ebe7906f79b32636204d025115810b376d4c6436da136b96c7c10649e3290caecd6ca14d995a817e3725fee7e621c5366f80c752e50aeffee1af3361924f31cbb1cb44731d19963ff30127ca2363ce15e50948be14c43400737ee8910ed06027599da74b06e77eb82ac523cc031c57c02dd82dbc0d53629d072615c92034cf829e7a5d4437b1f58e2bd4b16993e1e1b05c26ed8d695351db11d21df36a7f5811ef5fe001ab1e1c6ce9d2b69b6ac3af8087e6666317f75b645e3b1caefac0eb65327fcb9fa62be341c99f191cc869e48dbc8fee3e42d4393cbc6505c880dd6739a69be4f7ef3de306480a7a51f413d310926f252ea96a0c772d8b8e94e7d6cedbfbdb21fae2ffc379eb17c2680fa2bc56a8726c93e7bf2d446221ce95e49da93d29bec8e53ddcfd262c33d556c2b8921c3de93236408b462d28612d3343fbb9cc538b1e6b33c341c3b91dd41f936931e61f146fd00aee1c5c0de97b47cf7efce889012e1c22dd8faf0fe155f4e9930c27941d8b0907502a835bfffff801f6835de69ad33e95232f773219eec0e2374c421230f323257dbc91629c4ca8a61584f737b827fe8e8f5b69b88a7b64b362f8142b043f08ea82c4a0ab7c4e0b9805533e806f90597095242ff64f314801fb7ad838e98e1859b2c05c9b027ae5a4baf780d15977bc1492dee9b14b1cb0fb3243eb2304919486fcde89a3cf35a64e31b1698e35fbb8528a73526a19189d406272b8becec94379f69372afa99d06bb4f34df72e1c3b49557855ae8ac265160bdf48ab34cde30d2665891cfeda24adcc657d851431f38953f917f1a111f023d2c71845ccf25a562d2450bf8b4986b64ae4fd09e5a8ab9610d11fd68e9d570b3a467780236de974c7fd05ac00805d10939a524d7b2ab225cdb41decfc129208c372d51ca1db705bb1b9831e53ee000000000000000200000000003d0900559bb949364ed92a64f449cd6ee3eaba2e607595d3cecb36b06b145baaa69bc000061b100002860fd058656f185036e81a64fe19034d29223d3620bdf4bf2ff3c1def9c6bdd70c2cda7f660e1b202672741bc3258b04fb755fbbb1350261e7992bce930e256bd5d8d4fde61365b7b24904b788ef2040fdd6eb87f97c2d2f29eab4291d9a28b5da306f28b98d01d93517b203a028199d423a3545aa17522c63247f73af7b63335b0b48e4b875c69b42f4cb1573bb3e5fd68837f90c50b0161f067a9eafd9a0790e53ed5053ce60ffff410e4b16a4b7bc5a52c57e78ef266100c9f79753f81878c08e5dbd4d80c6e46a339578d8ac8c572df77ef614800cedc460c06878c0da97908067729ed35e3afe919071724f89ab736f5791a9c9b5d422136332213434c836e2ceb9fce0e2e96a9a6d7befe8c132867d5fafea1a7809ddd6b3a89c8ef6ea83028d3e2cca00f1bc6e12ea8b67e91a98acaa2edebaf6dd3a18c655b6b1fbbff5c641f8002780758d05f1f39c9470a124a5add314abd2262142733120747cda2f1d9eb90d68ecb9c7fbab23d73a35f2a20a2a365de6cd678d53bd5bd9bd518333d04e8e678b5d08f028982dad08c80be7d8fbb0638dd814232224c687f8321baf96ed8b39a1e9ab52dfd69d8eed79ac3f5a2c480a585bff038c92b367743317b937d969cdd533ae1d797a789ff7994f86a0d6cae470b64ebddbc478573af347a110dd1feaaeb4779441ec439cfdbafaba870105efd86b9d85a4df7ddb9b09f5b6b4144cd1fad5932df37ebf19a62648659fc1969142310a5cc9b4d0c48ba6bb0f863ed53a0b75fe1ee6515a46993f95be2e34166408b54a43e55c4802b37ac902fb4c8367ce38990d07ed3104d0728d327d3b9de6452b520f9af534505885788109ec78c1176ca0864d28422e826cc83f821b7eaf028d6a7e350b3037d0fe58d1d4e18113c8f61913932e71c0f334402534d8663f15445f900fb9dc6b3a93223868167be26fcbd70c0459eee37f81fd539c319eb0b04bd478b94b5f4cd23b4d496c2bdd6e8a154fd76c4ecbdf7647fe9e7be88c6a3a8e7696e2e596dfc25ba798db6ca331d135e9ce7c0aab9721d3f70ca53354f96ecd028236259b9b0d9e0bbf73c8e841b1d4276214f7be8feb525c91d39910b0e091997a2b89e945806e93cd325cb51463b0729f1a519334038cba09653799ef533a49e812e86b81af7e5099a02ca11c2b17dfc8b9e51a57a20546f2c92826676ebcb4f64fe7cc77424388dfec7199179cb125bb4613c8bf05edc4173987d7d5ae0fcbfa08a1e5ea2d6b01406d740b49c5b1a68da585549590c3ec13479efa3136c5ade68057fe173ace55593ceca8440372b03f332969866d1bfbce3fe9dd907d27593b8b2ad25eb4b12afd0f3abe7931ba7789c84a3ed65a03df06d998a9956043a4de786c359bdd58f0b9e5cfc32bc709b626ce8e63f3997a0e9f784f6b94e342b4710553e805cc8399191254189058ec75a15556467b2456d9b38a7e4d15cf59727ad2c32f0daaef1748be04311ff484479eb31f7eff9e32b816fa40f40430e801c15e931294b39ec4d6c93e8130fb4a1b53ca6471886a33b10b46d6973d02bbe3b39345e121473ea4ba41f3cfced07c3a613393e38b8ca01c353ccab96128093bb5fd000978b11ae6f7c1fedf48c3682119d1b44629d02ea3800d2e247ef1e78e527e5d574b4dd144e1fe1b0c6551442d419baba300258657792947443747b29b6fb9417d3c57536de6aa379c02f3addbd2066554189ca817c7f57331c972c30e3ed06fc7e521b8e2bb57c023dff9816f260a153f1ffd55df2a2fae568f3dc734d62ab47a77c54772b5e4cf2758c912cb473ec372173d9eda0c58b103ab12d11dcb976a40816ecfa8b9c9384f6415017555933652e77ae20e14ea9ac25b5811fc03364c4883bb78c82e29f8a8892415ff652b782642bf6036b3e2200e0000000000000004000000000000000300040000000000000000000319c0569a7b4f4021827771a963002b8b00000000000000010003d56d81344fe44d1f8f1a95850431563c00000000000000020003d33fd4c8d4ad4ce3880bf79a4856ca2100000000000000030003efcb10168b144c8bb4f694a3b98a129f000000000000000002000700fd05aa5d10939a524d7b2ab225cdb41decfc129208c372d51ca1db705bb1b9831e53ee000000000000000200000000003d0900559bb949364ed92a64f449cd6ee3eaba2e607595d3cecb36b06b145baaa69bc000061b100002860fd058656f185036e81a64fe19034d29223d3620bdf4bf2ff3c1def9c6bdd70c2cda7f660e1b202672741bc3258b04fb755fbbb1350261e7992bce930e256bd5d8d4fde61365b7b24904b788ef2040fdd6eb87f97c2d2f29eab4291d9a28b5da306f28b98d01d93517b203a028199d423a3545aa17522c63247f73af7b63335b0b48e4b875c69b42f4cb1573bb3e5fd68837f90c50b0161f067a9eafd9a0790e53ed5053ce60ffff410e4b16a4b7bc5a52c57e78ef266100c9f79753f81878c08e5dbd4d80c6e46a339578d8ac8c572df77ef614800cedc460c06878c0da97908067729ed35e3afe919071724f89ab736f5791a9c9b5d422136332213434c836e2ceb9fce0e2e96a9a6d7befe8c132867d5fafea1a7809ddd6b3a89c8ef6ea83028d3e2cca00f1bc6e12ea8b67e91a98acaa2edebaf6dd3a18c655b6b1fbbff5c641f8002780758d05f1f39c9470a124a5add314abd2262142733120747cda2f1d9eb90d68ecb9c7fbab23d73a35f2a20a2a365de6cd678d53bd5bd9bd518333d04e8e678b5d08f028982dad08c80be7d8fbb0638dd814232224c687f8321baf96ed8b39a1e9ab52dfd69d8eed79ac3f5a2c480a585bff038c92b367743317b937d969cdd533ae1d797a789ff7994f86a0d6cae470b64ebddbc478573af347a110dd1feaaeb4779441ec439cfdbafaba870105efd86b9d85a4df7ddb9b09f5b6b4144cd1fad5932df37ebf19a62648659fc1969142310a5cc9b4d0c48ba6bb0f863ed53a0b75fe1ee6515a46993f95be2e34166408b54a43e55c4802b37ac902fb4c8367ce38990d07ed3104d0728d327d3b9de6452b520f9af534505885788109ec78c1176ca0864d28422e826cc83f821b7eaf028d6a7e350b3037d0fe58d1d4e18113c8f61913932e71c0f334402534d8663f15445f900fb9dc6b3a93223868167be26fcbd70c0459eee37f81fd539c319eb0b04bd478b94b5f4cd23b4d496c2bdd6e8a154fd76c4ecbdf7647fe9e7be88c6a3a8e7696e2e596dfc25ba798db6ca331d135e9ce7c0aab9721d3f70ca53354f96ecd028236259b9b0d9e0bbf73c8e841b1d4276214f7be8feb525c91d39910b0e091997a2b89e945806e93cd325cb51463b0729f1a519334038cba09653799ef533a49e812e86b81af7e5099a02ca11c2b17dfc8b9e51a57a20546f2c92826676ebcb4f64fe7cc77424388dfec7199179cb125bb4613c8bf05edc4173987d7d5ae0fcbfa08a1e5ea2d6b01406d740b49c5b1a68da585549590c3ec13479efa3136c5ade68057fe173ace55593ceca8440372b03f332969866d1bfbce3fe9dd907d27593b8b2ad25eb4b12afd0f3abe7931ba7789c84a3ed65a03df06d998a9956043a4de786c359bdd58f0b9e5cfc32bc709b626ce8e63f3997a0e9f784f6b94e342b4710553e805cc8399191254189058ec75a15556467b2456d9b38a7e4d15cf59727ad2c32f0daaef1748be04311ff484479eb31f7eff9e32b816fa40f40430e801c15e931294b39ec4d6c93e8130fb4a1b53ca6471886a33b10b46d6973d02bbe3b39345e121473ea4ba41f3cfced07c3a613393e38b8ca01c353ccab96128093bb5fd000978b11ae6f7c1fedf48c3682119d1b44629d02ea3800d2e247ef1e78e527e5d574b4dd144e1fe1b0c6551442d419baba300258657792947443747b29b6fb9417d3c57536de6aa379c02f3addbd2066554189ca817c7f57331c972c30e3ed06fc7e521b8e2bb57c023dff9816f260a153f1ffd55df2a2fae568f3dc734d62ab47a77c54772b5e4cf2758c912cb473ec372173d9eda0c58b103ab12d11dcb976a40816ecfa8b9c9384f6415017555933652e77ae20e14ea9ac25b5811fc03364c4883bb78c82e29f8a8892415ff652b782642bf6036b3e2200efffd05aa5d10939a524d7b2ab225cdb41decfc129208c372d51ca1db705bb1b9831e53ee0000000000000003000000000007a120bf5899359b258acdec0ea500e437267aa6ba18e8509e15df74518f6ba7aa348f00061b1000036d768d1d63f1c9f09c252b5b48cf4d2db3f4935150c551391b37727723168982d7e58ce2a769313e461a0f3791a8c0208a769f5bdf4d57fbd0a9de104a0b1bbc1ab25ebbf87d1bc86dbbd42fd0dc0ff18a7ebfdd692c7dc3b95d095052540ce7801f3e25cbbcfd9dab857bc39624f59facbfacef5ab1e1e1f889b85f3b1f2a580cd660b73aa662fb15487722bd3c3f93d272725289136f2ee0fae4efe1afea25e6d4fc8334a47ad62d22be06605a15538dbd2a81311c4489b91d1cb143743e4570a6345c4f035c060aaf287ef66e7ebbe7b9037c10b66e087827478fdf76a02d25fe90e0f8228c1edfcb12eee3dd4e505a6c5a7bf2f5954ebb5560cd8c7f8b8f3f4ddca41a48a0d6c0e1092dcadc853752f459486bed349213cc15044585255842717ab70a3de3f0eedeecca10112c85a8bc248dc66c883a62288b49588d9fcc048c51081e94d65bd4731e7d71fba13693b82d2831d3bea370918aa5ddf1cc4f0d5015bb8dfb951ab13fbb9d26d5cb83dc980fc36f1712d616ce24d2c530253320f4f322f093a605ea426c577544f2983efc80be56791f443652c2233039a68f966c2f0b6351068616755dd2036b6226244d394a2b5b59160217603149901e8abe19a2bf404f384c2ed7fb0c5e470ee5ee8561f58f66bda729c2c8816853ad2357a009e537efcb4a28e845ca616be917b15aa6b8eb280bfcb321ea62fff21168b8ed54d58ccfdeee0e7752bfc0f02549d76615c85dd1e152a85ee931b34f436439e2233740328ba504c49f9764e1dca645ebf6a1377310ab53b68b4d0a6e6c952068249b86c29061725035db8d294ab9c56901485814735aa2a8d6987b1a19ced65a332f97751c4cd8a27093851f7775e5314078c04d254754d976bed2dbd2e6ecda62e9a0c7fd95299b4b13a54c9498d384210fb42d3b6bc5d8f0d42e42879f86c21eb7c5c6d1bffdd598b8f3cfcb75df159f1125a65f960637c62c7c5632d73b7b4b0544082008ede22d87e79e20eb08be0817650fefcd111de48ba2be02a7b080275c991a0ee4445dab89312644c7cf4101895e2dbcaad7d87e8e3b13e62751861b204a7e6f5a476eab0817c294d59aa0247903077d4cbe4a98e7984d2b04623d2b2ef4c650b43db15541ede229c12c045529b5c77993eb6acbdc28d812a486b5957fb996731980555bdd59ad824a882ebe1a77cbe6b9035f1c69dd01b2a27a47be5febfa65c721354e70071b07db4ebc2f01d143587c1b32a5337dd010d2a76a7773f4a7c665b4cfe4a61b103b1c319d85e007eb99b52400cd8776697e1d6118197655bf7bd0a5e7f4594bf36a2706128d5f5c3ee166b586c4d515611f4597a4c1088c1853a5959f73830cb973ea922d6211ee7d9b1d67b1025486f8f3c72a517d0d48d9a57d64c0f48e513c3b09e14ce91b515a87f3035ab55d241ccb12108dd299a362af26af96ada920202dfe26d456065717a85e6bbec540637059d82480f6c917a11434a9be5fee5ba33a8552b7b0e59f123991525e1dc14bfdfd109625b2df477bce565045375dacf6ffe99081914fc9f64df7ef8eb26801ce01be083555fd2f8a338a33c07f484b01a310ee420f5c932cfc5a8f6d32a20c3915a188833ac5775500c5d2b73e4ea595512e869f91f83de8e048c804ec8cd6de45b7ad264bcfb3c235325d3c09673a2a94e60736ceada065cefb1e355bd35bd4b56ae513343e85a3d840c2211da2d1941112318f97f825e1714f74fc2d33430196cca2423f43641b0d75b4b1a1a1ef0f07af31fda06220a2628aaab9f303cb4fe6d73fa9b39e8f6083af3bbca1becdd646afbd888c560bbf7cf4fcf5c02f712726eb23b03fe5c290df63a85ed1026cee07ab0d2bb868aa4dd6594252cb75ad5d11d9c53c5238047ea3d311c1443d478dd283a2602704fffd05aa5d10939a524d7b2ab225cdb41decfc129208c372d51ca1db705bb1b9831e53ee000000000000000100000000007a1200ffb841fb291ecf09ae7b5dcec1feeb45ba196ca02d6b9e50ae2b3dcd9ca5d72500061b100003acd0a3acb9b4f9540678dc9324480bb3d4f54e5e007739c9c1d600bff75dfbbd0191e75c0a2d810a6ec5b03d02cffbf1a66123c87790e66eee8d416caf82e7ea7090a62fe14276fa88af32ab9793f7a100de5525eedf71967c13c8d361246d98b78cc74383e714f88899f34407644c14874046b3b722186015c07b8db042db955c91cb14abbf3cfa646aea81ad15bf67108a763539c64c5a8e8115d46e063e956671e8ea8d8fd638a6414d71e9b475ebe070da7faf75a898f29048ab5a2b6c7e3a72a178b8e470e8375f539ebf6284d15486c5a8774d46ea164ba2b62181f47623fd987ff5958550c962a193638679b79fcd477fdf2a09c0fa879bb22cb493fbaed27518f5cb265741535b4ab14246077ca18f11ece7aaa0e01ec5bf02c3c3b541ea08bf254df123079c1538e266dcd3161bf1b9ed41d873f1491906e1459ba51ac9dd95e783598d3c356e0cc5b98c2a96b148f55f102a9810181eedd46cd00b445d861baeeac46eba469435aa4ddc877bd68b53f4d005aa2566d356cc344aafcdc86abd774ea28cc838d2fdc541c4b6da494a96e128b8c2abab4b21b3ae2646cdcc3528ef6fd8587b3a0636ead67a62309fb003afdc14177d329b062622313e9dee912847763c68678df663a39b89c69efdb6d916d5754534bdca9030955cbcbae6fb7ff1df6282175cd37a30a904418b976af05809f0e0e7e4b4e2ec018f1e9c6bcbe7a7822c8699669946f5e684671d63e68cc7c9cca2963945dc21c52232e6f83b1875b2bed7c80c37371a480a2e5255d49d390c3b2adfc695036ed91371cda7d79bdfbae464581f0b32942f03826aca17ab9da6ade4a778d310ec3da17fc3af426d21b347aee7c2db7b5e188e35714dc514e3a1c100e8595c9e0e4399ad796021976f077e5733ea535cc6daec2e371853dcb715fc366ea7d6b9a5b3509dccf5c2e1225e3a51de9f5bb9b6586b282a0b27a9ae7ae8f2be14ec677670241e384b462eecfde68957839b1327c9e5c622c0f67cdaf3845ddbe6f754401d720d6b6d5c061dc906bfa70fb76e1168c6ac1a25cabe8873c3c1e540ae44ae631a2638accd7951f368442dba7b38d0662ccb0140d1e4ca23f51de731a6f5adcf816c3235359afd607e58948da29a5f06c96b4312aee7d35ed4c2c811a58c5a196ac2f377d653d51cfccb5213c928955ac880b5fb1b91e88a52d5c217cbf78e071275fe626c230fded548b0f1667af1309149ff74c5542119d4e269fdc1b241d9f53e02e38e015b7c5c2d2ee623bcb4167e37edafddc7fadd642c20f81b454db1a3b578d527f124dbd1f3d99fdd1590256ae4e47c2e8b3bfe8708a0d5506d6ce8b130ce6b70028161454a5065e9925d75c0095dc24ba789489fa1e9236e25330ca1a45e61224ee027664f6589028a240961aff09187fb719ff3477b56427189b7b3c790b4031f6539c5e3a8a5d7fd99c2534ed1646920a43e7315bba98d59c51b337ba7a1b038006bb574df46830f96a5685e07ee8a0a41e712810faacfb231c67e69d0fc24b98d782c70e15524d5d2dcf4e64b4e26772dfa7067f6dc7ffc8b06e6ef3ecb13f927d466e0cd3a7ea09aaed90b7810bbfdcd8a1274bbe78a453ffff11ccee62059ec25955b34a1a1cdf8a3e506d99dcbadf16032646117556ad71cd93ceeec42be0350a6c9f194fea783558c42a56d034dcaf6fb1b28037362c7c6e2446bcda71d0a88adda3144589447ef13cb85a4d2cd16ef444097bc03e32c3b1a055e952f7ac87078b04deb900375a16dbd382ea4375ddc0a9645deca620590337e803ea8b41337f3f4e4030119a2337424dbea3d21214063ce853843dd4e6df94e3dc3bbb36d89d9eb8e15e52d0699bc6ad1de9d12afc95c8785d63756576d357126e13b25502d542774f6e5d2fdb559d52698d08600fd05aa5d10939a524d7b2ab225cdb41decfc129208c372d51ca1db705bb1b9831e53ee00000000000000010000000002faf080ab009f4d0b317a141e5b3f865599676f895834400b66dd09d33ad27163ca176f00061b1000028fe56ab181f454372c939ca0516f5782b26cec2010885c70e55c41a1e43de3af6635d2ed0bed90837cefa9f1805f4808e8092a4d44efe5fb616ff7678487a460b367134fcc728ba273fb1d22718bc6a95e47a120a6d952c7c2cb8f38e59c2a4efa63977cced7e4b8f46e4d47d29098a73beee807c3337c4acddcbb32b78eddbd124b2f33ec6cd8bdd364aa4ad2c10eb69dab808fe5f5f0aec19750e51ac65a8746f345c05d4b8823ffbeadd6200ad01449c39a008fbe117a8ee904445488811336d0c439419fa4f285f9f62a34f10b076c99c0092968e3cc9fa656016b6da049bd56b910d7a9356e76d24e746b280f0275ec9e9bace82d852bf0a137ea02d4cbd3b68450bfb593564d8c20953bb758890a55a8c381a4b3303b61ec26a56111361bf7262b3f6f2503aad06758130d86ea607cdbba53415aaf253430d92fdd81c685ab39233e94654e6508eec1347747e2df2862169382aef6f99dd78b50629c5d98b1fcc73e865679d862b42f8e9d54ef6288ed2c3f2713f0fa4db538cd3e70ec1a30cd65dbf873f581b30892acedacd39b5f0aa774d1f3f77d8fd11ed628bcd02ac33f89123595aa455ec54a07e93e26f94338fedd8bb84094a0add52f912ed5f9019e3a28d90d251cc6ed7ffd35254dcadd9f1e9b28eb0e06fd4fe961d60cb690a7757f475c08aef07c2e54668121540a42a9c779623709a2124629e8c4bd4021763979647f625b360a4559dfd3f57798dfe5d36e9d902904af3ed67d8f4b0894538c7718f5160d211cec27375a7e6a2ec42f2c8fcd1c953b7b8379d42439a2c6b921a66d5102ceb6bd6bc20b17098e69a0a4f708b42520e4792474c3d115a12c83ef60ac6e69d8842c5981e9a6d178efa352e73e4a34bed4fb590dbeecb259617668e6ffb9f955297f26e3a6a3b95d9617529a61f08666ca1069d2ee1876337d3e786244c5bb45a8236577184584cf3018118d7e4e78973ee510b6773bd922797e580cd240dea3ca31892d23c1e6e4fa92f1a01da8ea40044f5613a9429ebe7906f79b32636204d025115810b376d4c6436da136b96c7c10649e3290caecd6ca14d995a817e3725fee7e621c5366f80c752e50aeffee1af3361924f31cbb1cb44731d19963ff30127ca2363ce15e50948be14c43400737ee8910ed06027599da74b06e77eb82ac523cc031c57c02dd82dbc0d53629d072615c92034cf829e7a5d4437b1f58e2bd4b16993e1e1b05c26ed8d695351db11d21df36a7f5811ef5fe001ab1e1c6ce9d2b69b6ac3af8087e6666317f75b645e3b1caefac0eb65327fcb9fa62be341c99f191cc869e48dbc8fee3e42d4393cbc6505c880dd6739a69be4f7ef3de306480a7a51f413d310926f252ea96a0c772d8b8e94e7d6cedbfbdb21fae2ffc379eb17c2680fa2bc56a8726c93e7bf2d446221ce95e49da93d29bec8e53ddcfd262c33d556c2b8921c3de93236408b462d28612d3343fbb9cc538b1e6b33c341c3b91dd41f936931e61f146fd00aee1c5c0de97b47cf7efce889012e1c22dd8faf0fe155f4e9930c27941d8b0907502a835bfffff801f6835de69ad33e95232f773219eec0e2374c421230f323257dbc91629c4ca8a61584f737b827fe8e8f5b69b88a7b64b362f8142b043f08ea82c4a0ab7c4e0b9805533e806f90597095242ff64f314801fb7ad838e98e1859b2c05c9b027ae5a4baf780d15977bc1492dee9b14b1cb0fb3243eb2304919486fcde89a3cf35a64e31b1698e35fbb8528a73526a19189d406272b8becec94379f69372afa99d06bb4f34df72e1c3b49557855ae8ac265160bdf48ab34cde30d2665891cfeda24adcc657d851431f38953f917f1a111f023d2c71845ccf25a562d2450bf8b4986b64ae4fd09e5a8ab9610d11fd68e9d570b3a467780236de974c7fffd05aa5d10939a524d7b2ab225cdb41decfc129208c372d51ca1db705bb1b9831e53ee000000000000000200000000000f4240657c0bd97e795218aa623b27cf9a71764379c4762fcee8993aa0b1ab1e32194a00061b100002c0ec5d388a8c78491bbd870faa2c46e4282e11796123b69c2792e5806748ef068397f0212a33e3f79162ba4ea247ccec410db039dd323af48ac27bd0ba77ecb870c587477d4543d9a29c53fc02bc98d7cd0144c7abf80b999c22b42a28ae8d625478ca304001f9a49782ec970031e673c76e4e27357a321729d6df38d1d88dcbf764c69eda3baac9739bed637010f44638cfe1deecc56b76f6e02d1d0c3f104462b9ffa5f20de4cf092d86a5bb35d5f62b0fb1c983a2c06df17c9ffc809c83e4b4335f5903fab536fbd9719847bb063541ecbe05c12ef8d058b3547faca054e3d662250f1cc1f925dd71297abc25fe37ab33a086759fac76208a64552f84d2e4d84daccdc3aa2bbd2c2f922bf262596742dfe034529d1ead2975dd3d197ab0e2e1c75c8b8f160ca6077638022d4afbd107979949cf342cb399347f3990029f0db6d9ac0c569d61d42539371f9a7ff59e9c83ff97d15bf0eeb254ae58fb7b1f9d8710c546ac8a227930c66ac841bc4f475229e5cadd14ba5a01e6b2da99c55861a08e2100e62c4499d30003fe30ddaa347d7a27c2158d3787d58fe51ae57d797bbef7f900508d1580df3e5233f0887567fba1faa918c246d2ec5c3b7aa022cb8a652d00b4d719e312482f57655eee80a90cdc73151fd7ab9c5367793d60c6088fab98f0547d7f547e10db202a25e027a5cd0abc41bb0e3ef563c0a6d469a702b2a26f0e8b4fddb845a16a5f06b9dee33c3adb31430c94942c5023179d3e4441948a332069a1c3b69dca65f05a43452e42fd28a2f7e6344f98ab9a7e4eece3c1709be1f7bb620f8b6c45989a8bccad39a4bf40e8215183d1449196f1f9fc17de778b616856152e6e6145a1b7a3d7f226becaea5ebe34aa4bd06e60f0fed207bfd21f5663bfadd37dd722437bcd46a26fb4e19d062574a81bcd817eecbc1914a5878809128961acdd73113ae9c51070ff4494e16d81ccec777eeb513da82bf43d4884812b26546b4370dc315793271b069f60f4285f648cf122ed8b22b0c7a27e94ccd59a273eb774c109e19980e146850de95f82cdd8aa0e82022672024c917b281422d284df0ee0bdeb3d4ac56b4ca675ebdb835c17b6a822d79ae7310f4aa41d80ac61c5e45c1c0e1d64542622a31091a9f87c335e86d964dd85a951d7c9bf41c9f2b1a9bb8424d7d1b26413da8034182fa42d2b1cd1f8745482c49d8348d19c72cf5a02bd28e4cba82128af8bf5d9c1215c4f543ef4d185f100f8d803dfa29c300c072e44ad9542b82fb1380d55c15c9a4b4398876e2450b90b49990746f339abca8cc8a462b62329a128758ce0e46b5f998af1bb485a3044bd125424eb5c623afd2a11befe4ec544eafe275ed1ad82b940dad5e9a9710d48562e51b296ba81f2d70593685ba0e3f3b25089a187e61d5675dd481aa99620276cb0a841a3c4df201a929287b1127270c5d25d06fb286dae1a9a5a5cdb60003f0c30d2021074bf252e550685f7b51a087a77b0871e883104e55f898aa5bc4cf8538c293253a737556d7f220e15b90cf0eda7d5f2172372e3c50c12cc588f312da37191b5038e944825044b130bff281ecd47a4252a1411ab7a9305c2b37e9facd435e9c434de37641498f8e4bfa7b42966da29c84200aca87ea1c3b00db54906b340e524a7dc4a15403bb82bc24517cb91026096bdf18f5f7ae5640ed6de1f0c5d184813d6b9d244b32b58e9ff524741a39383eec3a530d60db13deb26e3523a725f0599b671b625c07002704fb600b77318417d2527537359d122e22a1f7581eaebdc19e65ba50bdda18ae08e9a8694fcb0ff1a2cd98d910dbcd52064c15a4282d67b278c72a0fdbf228abf6b519dd28ac21c57d1da4bb7ad5b5ab10da6b83132df1da79ccfc77fb45598bbd91ef5ab96d8a2ee148639a562debafffd05aa5d10939a524d7b2ab225cdb41decfc129208c372d51ca1db705bb1b9831e53ee00000000000000000000000002faf08011b29e60a38883a8d4434f17ca3d92161109f7ddd8799e64e86d6b8509babd1100061b100003b8c1771721551fa88af8fdde92909add6e5b8fa90a4b0484ee065ed3ed7c56e733a7fcd775d4ab92956c1328ffee9c195004c5dee5dd5b9d0f9034589e6769e72579d3d5837ad70785ec420b4a24c04d36668728c0d2534ff1feea9aac2410423fd79c7db9231ee7efd3d585646e378fe53d731d18f38d6356a970f5c3026edc849d49ff34e58dfb7548512461110088ac3aa800e10785029ba3b0e9ab7bde0f056939e4921792dba2f5c005135daf57e32cae06a9ccb1b4d321f3ba015e5b92def1ff1c200e56b3990d82570586bfae26e9398e17dc6c069f92d80e6dfedf6b2f24b1dc3cc9d63e684d861f40fdbf508d4ac34b7f10c57be2a9b0c5921f86869c29ada5394b8780d2488a4fca3cd98ddb0ff8ea4415a07caea436682835744e94d5cff6d3024a9525dbd697e499b7ef23062b18b225bfaa4c5bb07166f34ff7866ec8f0fbbc12f695c609692798364fa20bf7977e321deda3fe5510833494532fba94fc1f0dd14ec74f3e9fe8ee659634621b63d16d46a8958132c24bd82c516bdf9ae9515cebae42778e4de6be7047c31cf86c0df0306f7b6562e1f35be51e5e64cc6d9d4c010849e6ac7ddacaa4b7b6fb1d35aac815964090940e73a1193eece11c1c1d37e373ef58c5e2d690b6ed6338360af9906146da9db8329bd2786bbf92df10445ee093f0b1b2a640cc2daf003fa7141435ba1dd54f9cdbf5417fa7f539b255452852a85d2ce97ce5abed4980e7b409e283f97ccc9c01e104b55155f96ace6789f61c4661962d34fc5d7e6f5f5233180933b2fa7f7a5b074714645489f5221966160946b7bfbf0fe6733e6beb8af4457b9d36cde1200811009ec483a9d730ca980aa28f636942af5e89794a8edbc1b75d555ba134974374d0fe23d31c26566064eb9998d649bb2bf066bf710da50672f4e3ab4df843a0c8942bad0a071c237d4c1759eca37380919e36aec73284db202a32d3d1619f3e5b757b2df8b04bde567783dc8e465d996799782f1a1b8de9331681a35aa04edb427de87264c8ae9c397f29d3e8730db91256425a10b960a9de1a48d0d4186d617d2b69c87e2540f6570faff4ee1f6303d7d281434947abeaad83c86a4d25bef4de2bb3c6104aa0ceed7c8df039f4be6a42851a118adb1b8f98e02f6727b75d98541bab2ff24fb2f20342e86150c678941825409b62a844f44ca1ccdf0d9f7c2cf9b222fbed00bc92be0802fbfbbeefa71c8976cba8fc4aeb031480f434027b1cd593d08cbc14c2a360b736b06b5afb8da35f0be3818fff4275b8c830f5248a8b8edea1327454e1360bd90d4fa08e965f459b0b027e1180290cf762f813a31e8109f472d9657b03af737d1f7bd2e59441541a84ba818f1413c5cd1f8b9882e9188e0def9e44e2f4a7c710c893c7188ba86423f8ae86068d84e1832af548289e87c34d68b186df7e24ca5b051f8f5e4a44e2e7383ba2a09615b4147b34e86486731290ea67f3be24c13a9c5cc37f06555989b3f10c580a9cd2b416d0ee4210855c6833a25996761dfabb036f3893cff7db7e310baa8faa79f46e0ee43bf4dfd732eae7f44bad2e7c032b9c6d14947af6b0e37e5ec98372a622f716ffba0cde04b9d4508392dd154ddc34829412bfa604d4f00e4b10a553587343ef5c0944165e7ee1e34387b09c147ecba943cf36dbc4269efe50ec3a5a3075c43be9651d6db6acb9f657476952b78c990557f05935247a71077373ec436ec586def177448f8859ba096b7a838e5b4ce7a463f9082f705c26d99936eb1be584ea9b58a44b9b4faa07fd8247fa66cf4529d1b8cdb92ed7bd96bf0968db4376489c7d46f0f27d58ac884c29736502953723ef1ab41e19c7041d3e0e9091d7de2e3904d032de02292edb1225a672ab438d3c65f7921c06a9f181f8ffda4ac524d0e000fd05aa5d10939a524d7b2ab225cdb41decfc129208c372d51ca1db705bb1b9831e53ee000000000000000000000000000493e0f81fd474bea478df0202b311c69e85dc6215f629491dd15ad0929faae2535abb00061b10000370b2714d2734e6b8cde085794dd7b41c8a9b6c03c1edd8e3db168ee7fce39493596e882b18b5b1b79c16400c6762b9856075821be6be9fab8f469f56820d8f341554400a8da7f1f1a8501902581d43b9fa6e5c68015716f718a2190b87fce41bf1b509aa61806394a42489d63c457fe4c79e7480eced1315edd731887e57704fc9102f50cb7f0d242d755cfd5a2172dbaf7f01b124861cc6d1dc804796bbb84165c805f0c0f3fb9ec97b74c2a694de56a9cf8d79d1a679260ee1169d78214b34c8a654ba22e59ddcd32beff4713de33549f035b342660405b0159a7a508e5691ef4805689140e72b8a0ef2e61be74dea5d0b8f5589e0e373cac2e2e1cc39b2121c05cf4122ad0f8b9af6fbf1de2ea26376c2650ccd306c13a7b64acbf2a3feed128754abe44658009e642768ae3d84f5e0fa5f7f360c2a1c76d26985817ae77b71fb59014a5483ebba9271cafa5e5d8031c569adeceb8bae6444e98d2522b28f6682109fc7d31cdb83ebd45e5d81e7f046df42345b49f470dbef9ed87709301d2c6131215d33a30b8d18e63e54a2aff85dd57672f8198bca6a67ee147c7d0ae649e5661ab6bf78a662fef9a164f1e332b9f16e6fb3d5769ddcbc1d1c07338d3394b9245d17618c2474e86c064fca4df00ad3a93dc051fd8c3328cde2a987798b0f22a21c90426700abeb1e6f38dffb485b5477ec44c690fa80e317b32a982fd3082253bba8595783290dbffee4fc9296ffdf16a8bf3154971bb720e78674969e9db2e0fbab9e9e13f24bc8b3af5e2f00f262f0da56de443f70398ab68f747d35370fcd8e1c0e130f7269e08f862b5a67f2c129be254df2358762ce3a947eb27d66450af51540e7721b47c8a5a86098ea64dad381f14e07aabbbc470949a99c07612add3ab4c575fe2e520bbe511a1a674aea37a44535c13ee3380f8f39bd230fc1481cd31912af36c6751e23c6f383cd37a8b13fa7df9f0c7e460739f2c6226638ee14f14d36366211cbc6a1e16b4856bf302a540aa9d9e833b1d59c510473096384c8b450f2f3f1dab9e614af822949d5cc93d76bc4d1a52891bc85f1981ef83161195ab7d8181ee4fb163bc6c685a10e87c7f4b15ed7d05833c230a4a5b63841fc65b959f0ff010e697f47c583f9b7fa9b389c0eff6614e47d85b83c483136f182be4c151d272f5d938b912a95e47d333e5de6a409ad271679a778a7eb3f169c71525302fac5d4575e2645c09763c2ef165736a7a726ca605038e2781404328790ffaacef2b9c2bf90122042cd571287bc4e3973da65fbd4e3da9e40e4347ca6eb4ef1ffef4e5a34be80425cae3e81533f7f2953f95fca53a22057a39125f5c76350fba7fc6c036838fb951d0aa8702e7f44c6f8a9cbce3b64fa8ddc2bb8c8b35d1e29a21beda6fdd332b31a749321455277231fd9d70ea4aded95053b395f88fa6916d126e1626fc0f1be6cd2a9538d17c498b40927f12b3bb40fa3e272e82cd2242b670afefa387470f4e6e0a1236028954c9e90311f486617187956a23b90b356d71e219e6dd055c2120771003a6c12769aa3ceacb9642bc01022731ca7a413b68ee7d1d5444f75dfa51a68b74a01ac85f6ceaf5e56987b9d67d6de896f5aafd25c78c413a6d4b5b03d571167524cd231ba13bd9f80fd7413faf21e8170cef0d08b242c5c38a2b0158da56e358ba0692f670d4611c7a3624b234adc30c5b7198e0afc941f5d13eae3a94ddffa652c784c34c582e04e948da91a5ac3038a9df38fd4f1733779f4f122ca2d7ff9d03bac9def35d9ee3a183161f8f2808d472b2e64581209359cea58ca7757164c666029982223877e2b14d2d537afb012f1ffc12cd083c16dfc64213c56f3d4d22b603d3dfab1d21e239d6fc1f9f153ed61f1ac91c29c85c16f4aa2985f84052f5a08d32bdd000027100000000008af34a0000000002c2322200183cab01341b3b937dc48c2d5d70e119b4fd5b4dc6d85e0bb49e98f1fe4ed87027bb80c7d8cec36237511da378ad5c121861660506bdf411240139e49a93e13aee25d10939a524d7b2ab225cdb41decfc129208c372d51ca1db705bb1b9831e53ee270248e28af5950cd59a64545f84957e4cd869de45b4fb50b85313894a1567c134666207e5bec5803b71d6fd6a9cfea3e6ea9a8dfe645a7f4dc7c9cd6c4ce15a0002547bca65416a28af342589b5b771ce54464b17b2153b49a1d34a2ed0b0788ec37083e54750e6f368b4a40d18a8730522e8f23901ac856b697dbd91992d75e097e3f175514453a79521d93ca8b3ffed47cbe300afdd75d1dd23cbe0c14cf67b3856fbe2128b9b7543625f1da313516f7e441255f85e341371fb7f33c6833abf02000000000000000100245d10939a524d7b2ab225cdb41decfc129208c372d51ca1db705bb1b9831e53ee000000002b40420f0000000000220020c57347ca54a9e6f279f6a1f1e50f19b48289c3baccdd356fb03033ae7ccb6e444752210252a01d9d8b03db1a99b1a82223f7dd60ecffa69dbef47c06228fdea2fe0f0215210322a747c1d7f77fc7577a689618bbeadf28b941412404ac5e216d684a32d57a8e52ae000100400000ffffffffffff0020cd6d4a4bf51a8c36a25cde5bc08188c5ae037fd9ad9e92b400bb3d10e473b55480007fffffffffff805d10939a524d7b2ab225cdb41decfc129208c372d51ca1db705bb1b9831e53ee061a8000002a00000000883a86353a00c855b5caa13998033c04330f88b88e084b3c00f228299e5554f0b66e9d5c630e194cd572acaee6e5124b612583b9722ccf24581716292785c4925c06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f061a8000002a000060535ae20101009000000000000003e8000854d00000000a000000003b9aca000000" + val dataShutdown = hex"050007012ef2c0b2095e3e9131dc501a0c98e5bdd2cf2ac43561fea5a268475e5f37db0001010002aaaa00ce2f18a967dc4f25f414e671ba6585f8ef0b8c5fb812c21064f55a2eaa0009b24bcf77abf6881a270dfb46ae2bc02b3e37d09a687bf42a4665090edc3491d880000001ff0000000000002710c000000000186b0200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000180802aa698202bbbb671d15145722fb8c28d732cddb249bcc6652ed2b297ff1f77a18371b1e63ff0000000000004e2003f0285c07046e02a67017e9094e04c5958c4894c59de7fb525e8848b06c33b71f02b12174865e2776d03ea17440cc0793b6e18401b87f9d675d3fc06a4a1c57a6b202ecd1e11cd7fc82a559567b19448e8023e5ae6c8960fdd7cecbb1ea5bc7d39fc503995345f0570ae8563976e70810bece1297a3c46db73675416d646482af784bbd000000140800000000000000000000000000100802aa6982000000000000000000000000000000000000000000020000000000000000000102fd05b12ef2c0b2095e3e9131dc501a0c98e5bdd2cf2ac43561fea5a268475e5f37db0000000000000000000000000011e1a300c829f6cc2a9b97272924872878a38c89436ac184bd890cdb3c03cb1b1030bb1900061b100003b7a341904a7325229f40e7e730700cad5dec73fdbf9ac439f5fccab34f0cf92e358a4203d0671599024681ca68360773643762f685dfdf5acbd1d386ef767219f51353e472bb9e6de2f0cf9ae73d323c13b489cdcf3d3dfacf43990bed4a6dc9e4260ab126270d556ff9dc93d347311a92632e8e3cc36139a13a2698ca802b4f1dc9cbda8bcfd76ed51ab556540b1afb86f75b963837d3e021ed2f4d83caee2bf160b89df97a0494f7c5b4b90ff5d0d69ea2bb152876a187e6a790ad0a4a03510b2dd8ba0a17aa8200661c9001c4f7fd3d1b959f367184ea37634725b343d6f474ea0f326abde8f1bab94da9fca54e2af3769197a9866862507ec79bc1dc0ec7a368a02fe2d1cf514692267854cc5d17aa8ad07a9ecd2d0a9e9f6d50646f1940ad444332105a170e1324b077010b3bf38a8ca7ae461cbf766e7af6c6f53a884eed01cc6fcad3c36962049eaa29f6c7a7324b084aabe64e32b812ffccbf7fdf3d9d6c4b6db1ee68e22efb94b6b29b1987b3a13e294c05cc437469d5898e45e8281fe3797212bf7e4c432abec13f9d20d801dd4d6aa02b24fcac38bf2448f4b4f3ff53a050b9562bfdb9a8d4d674328187bb94ffa383c4c36dab29f59ef26d1f4f1d7769bfddc0a54aef286b6432c7a91956571b440307b3db986e1b8c8f57acfc227a149122c63b4cfb15a1cabc88e089261e77808ba6e57f04789c5b76ecfd3bf48bf8175c08fe504c0ead6d85b077629127a31293e5caed20ba3db44750f5ba459efa2c71af0fa536b21de0ef5aae1bbc869687193c9796b4a86dc47d06c04411e71a10bac27f53889f0b287673fc85f5b1f7a842a8e7d82e77926a1b979dc12143a9729f5c9cddda49e56cc9767328e212326f4218485f633e27531e26dcbeb49459935de3a95d5f90cc8342d0774ae0003a43f2263b326a38d2bda2a467a4c58c2059078e501f766460732ead1296668ab8504f94f856150ec620241d270f3a1f73ceb2d565f88a6b1b1bc7c0439258a1d80dc39fd27333134d4590330af99679c8dc2f19a662b6daa035cff8da4aa318847551ef6b3b33149b150ac35d7fd151f44c970bd99b1bdef4c4266f5b1ea4dcf486757306ca256afb23808959619771261f1e934b69bff9a13ee2796ca7628a4b864cb765401b3b6d3355516208cff9f401cd1f9974055d5278e07411e1c4a0feebbef7e87be9f2c28cbd2d9e4d019c0cf34bdfb0cb71bb19255ee3b57153b747a5953d5a83613b843b550f9e8e4f6f9689c80c41e1ba5313373b4893897cbf37d1435061e9e6855c2aaa579e19b4088083ef78119a03a74e0857a0c8279d6ea86ec1ebb7fdb41a1afa436c9fb91226322902965fc6a79c54b21c1493f0bb9f692511dd90d101449f637b3e605dd425d08167dfa8aecd35d6945e74857e30d64fdcc3767586d2792faef4b1b15dfd6f683c490ad640fd32b955edf862b6f1d1c09fb531b142b08b72f355f3d2971e9667c3322d21e791225453c797c66e4b0710461a314bb20e098b6b64076bb508ebb70c12116f1db5168c946090a83735a4367ab7d0917a1647122c1ac47c8f18368a7ede37ade9531cd69df43504a327281bae1826cef8e960e2d3651d32d21f594b66ac022a1dcdd28027da271256bb1e43a630f1f0315b4728b449c4468035c063b9923fecc2bff863f080ffe3d9c49dd73a2b10498509beed2f38d6cbaeaf7974627e6a28d383d79acd380b0d9ed6fd12d6709294586067ee68fd67a071665d3c9e5be13478c67cad78683ded8d3ec8fe7db736e8c50bc4a3762839782886f007db05afa46aa6c480a81f8c54d6444d053849af8199e3a502bf77649d72e03662d29bac6b448ea4a91deb627d8936df7dd06f204063b67ac03d04fb6dcc8add741fe16675e2ce187f267e911c6b2f42f3f8fe0001a14701070001000000000000000000000000242ef2c0b2095e3e9131dc501a0c98e5bdd2cf2ac43561fea5a268475e5f37db000000000000000000000f424002ed9f70ce496242e1c79388e2a176adc7f98f8a1e0377a5f38c7cea688e414c8e0400012401010101010101010101010101010101010101010101010101010101010101012a0000002b40420f00000000002200203f1e1ebc1c015ee48121698baa1c9ff86a5dae6754c1778d8c940e4858e07fb3061a8000002a000000000202000000000000044c0000000000000000000000001dcd6500006402d000000000000000020001020000000000000000000009c40000000011e1a3000000000017d7840067d9164bf0ed90c5e76ed1786334530f9a89b57dd8645d61a9d88bf328e9ac8b0151db1af176a59269ea456d589fed9ea79c0cabddad5902f877dbf4f82c30562a60748f3b2f5fd86db13a8c3f61d92c6960ebecb610211209978c5496676729850001392ca4a5200406a05b9e90815d84fab4082e273620be56a2af5447b8687df1d976aae1ad300d88c5017ccc9ec7fa135a4396762f94879641471568d8eda054a700000000000003e800000000000003e8000000003b9aca00001e009000000000000000020001010000000000000000000009c40000000017d784000000000011e1a3009737dfe5d0c76ba8da73730db86b30b7c749cec84c20c8c82b1f983a6875bfdc02d6c263c2da69cd9c05468823f3016634104e05cb02380fbc959a3d44e52a7a55000000ff02828d2753c7955e13a3b1109c215e8920e34ac565bf4aa61c974e1c480ed450560001003f0000fffffffffffe0041a4772678ca4b2a3539b2c521e3ac1dbe572cee66236d20d420b7dd17e96c11b10000fffffffffffe0001000000000000000000013f0e212ced22456cbbfbd4c61e30a81800382ef2c0b2095e3e9131dc501a0c98e5bdd2cf2ac43561fea5a268475e5f37db0000160014761879f7b274ce995f87150a02e75cc0c037e8e3382ef2c0b2095e3e9131dc501a0c98e5bdd2cf2ac43561fea5a268475e5f37db0000160014761879f7b274ce995f87150a02e75cc0c037e8e30100" + val dataNegotiating = hex"05000801dca86c28f42d8793ae0e65a6ec747983e252fc9c93ea042f510dc9938bf6b43e01010002bbbb671d15145722fb8c28d732cddb249bcc6652ed2b297ff1f77a18371b1e630009681017da40f48f9b5fcd904ea23a0e16291341ab605e77bbbf3905af11eb17d180000000ff0000000000004e20000000000000140800000000000000000000000000100802aa698202aaaa00ce2f18a967dc4f25f414e671ba6585f8ef0b8c5fb812c21064f55a2eaaff0000000000002710021ad1d4b30a4f9139aff5627f426b8ff04f4d1cf4cfd58781e7174dbf8a76acb003e3711dd21be2f65fad17ec131d75d8eeda649d496d0ebc2f91b26520dcf26982027dd7a5313e51856a13efb6383bd67faa923fe20ad96223c18ea555a8f5d3ba3002f4aaf2516efe365c0a3cf14f898ca593a278ad855745d640751180104fd73feb0000186b0200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000180802aa69820000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000024dca86c28f42d8793ae0e65a6ec747983e252fc9c93ea042f510dc9938bf6b43e0000000000000000000f424003f3841b40e4a5046ed410f8eb1fb79c59bc6ddc968f73eb92f87286c9b9d204470400012401010101010101010101010101010101010101010101010101010101010101012a0000002b40420f0000000000220020c4e7d23983470367c25e869bd7b18432ef51e87e017363d7bc59d22b0280c2cf061a8000002a00000000020200000000000003e800000000000003e8000000003b9aca00001e009000000000000000000000000009c4000000000bebc200000000002faf0800f2789e59bd755935d9b2edb1c8922f6643be2ce8d8639cbb6729d0b2f4716527017b988f15ee147358fc4f9292e356f630610bd0f357870ed25bb3cb1259db834154aa6539cb6e8b2835d9cc0a7553b9e8015c1977c187a5142e958660d20c13420000000000000000044c0000000000000000000000001dcd6500006402d000000000000000000000000009c4000000002faf0800000000000bebc200095b196f66547fd545611cae4c4820b8e6d4508b1d920b0abb0ff7471c3a7a42021f8171d635e4f51bd1928f8858fddbc3b478af680a9face1412ee2fd36452346000000ff028f1bb0bb97608dca87a17a922b1db19029efd8d91ae756ba2107abe3230953eb00000000000038dca86c28f42d8793ae0e65a6ec747983e252fc9c93ea042f510dc9938bf6b43e00160014761879f7b274ce995f87150a02e75cc0c037e8e338dca86c28f42d8793ae0e65a6ec747983e252fc9c93ea042f510dc9938bf6b43e00160014761879f7b274ce995f87150a02e75cc0c037e8e300010003fd014324dca86c28f42d8793ae0e65a6ec747983e252fc9c93ea042f510dc9938bf6b43e000000002b40420f0000000000220020c4e7d23983470367c25e869bd7b18432ef51e87e017363d7bc59d22b0280c2cf710200000001dca86c28f42d8793ae0e65a6ec747983e252fc9c93ea042f510dc9938bf6b43e0000000000ffffffff02400d030000000000160014761879f7b274ce995f87150a02e75cc0c037e8e3dc260c0000000000160014761879f7b274ce995f87150a02e75cc0c037e8e300000000ff000000007adca86c28f42d8793ae0e65a6ec747983e252fc9c93ea042f510dc9938bf6b43e0000000000000e2463e56c7056bd78cb27a28bb76e22fa67a1737012b2175c0dc5992ffe75247c9e167490658a73c4f2be18e99dbf83c6194c691f009eb9ab753f73596e246dc5c301100000000000001a5400000000000034a8fd014324dca86c28f42d8793ae0e65a6ec747983e252fc9c93ea042f510dc9938bf6b43e000000002b40420f0000000000220020c4e7d23983470367c25e869bd7b18432ef51e87e017363d7bc59d22b0280c2cf710200000001dca86c28f42d8793ae0e65a6ec747983e252fc9c93ea042f510dc9938bf6b43e0000000000ffffffff02400d030000000000160014761879f7b274ce995f87150a02e75cc0c037e8e3782c0c0000000000160014761879f7b274ce995f87150a02e75cc0c037e8e300000000ff000000007adca86c28f42d8793ae0e65a6ec747983e252fc9c93ea042f510dc9938bf6b43e00000000000008885cee0b9b066cc86016de90d618da7b7f16349d7994b7232b80fc1cd3bc326b5a007d61da1c6bf8071b6ae1a5a8c29ea1c9b9ee777c59b0dd8268d6b57cb237eb01100000000000001a5400000000000034a8fd014324dca86c28f42d8793ae0e65a6ec747983e252fc9c93ea042f510dc9938bf6b43e000000002b40420f0000000000220020c4e7d23983470367c25e869bd7b18432ef51e87e017363d7bc59d22b0280c2cf710200000001dca86c28f42d8793ae0e65a6ec747983e252fc9c93ea042f510dc9938bf6b43e0000000000ffffffff02400d030000000000160014761879f7b274ce995f87150a02e75cc0c037e8e3c82e0c0000000000160014761879f7b274ce995f87150a02e75cc0c037e8e300000000ff000000007adca86c28f42d8793ae0e65a6ec747983e252fc9c93ea042f510dc9938bf6b43e0000000000000638e1c5909ac828b7fcad839afdcbb08085f529b96ce803db98cfe1242eda97f3b4778731d114e9490cf07e81cb687dc90d9a6ccd3433128af54d87b81586000ad201100000000000001a5400000000000034a8ff24dca86c28f42d8793ae0e65a6ec747983e252fc9c93ea042f510dc9938bf6b43e000000002b40420f0000000000220020c4e7d23983470367c25e869bd7b18432ef51e87e017363d7bc59d22b0280c2cffd014d02000000000101dca86c28f42d8793ae0e65a6ec747983e252fc9c93ea042f510dc9938bf6b43e0000000000ffffffff02400d030000000000160014761879f7b274ce995f87150a02e75cc0c037e8e318310c0000000000160014761879f7b274ce995f87150a02e75cc0c037e8e3040047304402204c28eab56bf1f1477d8d9ac7b02a4b1e582608c2dc1da72f3060b65f95d2510b02204e55ed9444d39a542cdb0e270d3758e935bd122a6fddff297bda2209c5aa38f901473044022046dfba01365cd0e8989980ff558fa0a1ef649262f7d37522ebd143792115fcd10220342d658ec27efa39627c3fb55abce8b850179843f00da872be4a47a359b33f8f014752210376510328f91bf430fc444048aa4f204b7d04b208826b07e76e99c6290879991e2103f3841b40e4a5046ed410f8eb1fb79c59bc6ddc968f73eb92f87286c9b9d2044752ae00000000ff00000000" + val dataClosingLocal = hex"05000a012f2eb1b005627f7bfa0a75f9a58264e76b3e5556a0c0390c3391d7bbcb01dbb501010002aaaa00ce2f18a967dc4f25f414e671ba6585f8ef0b8c5fb812c21064f55a2eaa0009fb06f68c2377f76291612ec4a90a34399180c41c3eeb5130d4593c2648aaebf380000001ff0000000000002710c000000000186b0200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000180802aa698202bbbb671d15145722fb8c28d732cddb249bcc6652ed2b297ff1f77a18371b1e63ff0000000000004e200319b253943833df9299411fd93035b7c7907ca7af1c522ada034aa860c7aaf117034cda59ef0c36106fef2ef99769b553ba116916c64c8d7e17619e0cedf4d327d30353418e63b7891f578f6caa7fe041815c19ba3afcd70b01955fbafd0fe08f06dd02b244d38d0ced96d8282c4006d44e9f3eb34e80acb0d612cd3a7970d626a74186000000140800000000000000000000000000100802aa69820000000000014a00822f2eb1b005627f7bfa0a75f9a58264e76b3e5556a0c0390c3391d7bbcb01dbb500000000000000006e582d0c2714b4495f83b884257b0560d2141bd6266867cb073306dd9e14f43e000000000000000000000000000000030000000000000001000101fd05b12f2eb1b005627f7bfa0a75f9a58264e76b3e5556a0c0390c3391d7bbcb01dbb5000000000000000000000000068e7780f13140ed218f8882803ee844374172569e40df747c52cb1b36aad3b645f99b9000061b100002ce8864be51a66ca5c6eb0a230e56f4dcfcfa3b293df870745e2290896d1458b1c7963d6b246f5f4e025759adcf2066fe86cc2faf5667bee06ee6224074222234af87155870dc44b51c553acf2dfcb15bd914991732e1a7379f85fb120e17993fa335ef07c3032b96393d37fd993d8f9a6318d538c912a30768fee364457bde1f252e783dfb3a5a0f19f4753cefe3ed343b1e346bcdeb16efa3e0b54fd87265708dbca839a36b5e04ff9bf4118c7829c79aaac2d6507cb964751d65bada9389a64cf2c18758cc30aede626c4edef10da1b33a7372838087fa4a02ba3a1db8b24f7748528e71bde418b35876f16bac6c4230b6f03cb3b0349eda81ed1a427ce423fc47db0bffd67ae4065d9c93f494bb9c4932d5a4ddfa7c83c4bffe381873b2946293d0cd44500760a947f27ec91161a1ccf48c0b5f83e9123fecd9b1ca1294a7bf20b35b42c7fd1d66973679d819d4025104c845e972d826165de4df315197b0e53caf66276437c0b4c37ab4c8b93f2abd522a8f28f60bf01cfe3d0d0e97dc281ace63888dabaea00fbd7af4718d0f752210bf84b6a947f8314d6966115b7da9cfe219fa8e9fbdafd7129a7ffd183ba436173548e707db5233e6616bd94ae24fc841e6dfd2b9eba1202a4ed06fa09c0010d82dc2d225f117ec122d1c9458fc92333af73ac40206fb201366f113485d0cebb12163cfde5479793335b9d5e742a1e48d0645c9184d6a2509d9ee41bf2f1192ea19e0b36ea913ab8d00d4dfd532a42e240db95e709b8fd6df30eeaf7788abc8856ef4414d6262c534727116d622b88376cf405c69d44d5332fc9174a914f4c3324b5f3107f81a4389caf2d2bbded2fdfe5ddad8c82dafbf52aedfb14226a306521aeab9bdd4036fa8c8a8ea92c843c1b9542562ffb5afd5c49de0f70d77794f11737621eaab43803761592bb56b711248756816a92edea6e445aba84381bfb3c42de2893aacddeaec90f47159927538ebb12aab84b5060109b25316ba59f87a25058645a7e3a4175d5a915c46cc5962efce2504d40efdab3d7cec04019cb06b162136a093d8ee62e3142839f4c465c4236c331573d3f44bc7057498baff80a480ba1aef97bbd4ee5324ced9ffe9cec175c334b41246177efacd5c6d3ad4ffe8dc346509e562ee59049285b863f81c8f4e6f031c571e6244cbe4bb83ac9b183aaea3f8d1bc048dcff42b10ef2df17c3d8798f9e14ad7ef94f0c95fff06841d9aae4a196ba9a089fa4031d26f463377558670cf8c1e1169b34846e407d0e92722d2a94d0a9a600db4bc44eff4194e1133c312de59818b5eca63a0d838c67053d10e5c8245b7855f1773867bfb05ea9c7b3941f7900f9e31ee959b4de8b694ff00306b64bac1e7939356e4bb354bcb78b94bb85da84427347daea704df10c78ffa639c32f747f575cd8273d23b9141275e38d073988463590fd3e9af73499e7fce8e873f23dc579df19f205472534db58e97781b78783d9be95dde62aaeb70a07627a58137cc908e26ff1b9191f005c5a4e470db1fb8d288fe09a6938dff43cac3694f390be5a40839e3e535f73c82711b1005d7e30da9d04efd5af1839e37d75d29c799928cc3cac114b701b160d990382d3d036f943b15fb54f92f12373f96a97ff194f57d5ecf2ec544190613f059f88658e642e6cbc2d8e7fe9bb65a720714d79b07ea3477efba4babb11c5be1550577884af16304561ee156cea61b58780d88a1620bce4d25dd345aece78e8d46ec674cfbc2a7e670399a2fee1a20679994c98c3e59c97a1d7e983c0b53cb5e7020a0bcbc9032c059bab48b4a0d37bb95573f083c6feab01502d3ddffaed478b4349acecb69c6f9a9267d51a807d8d9ec2dfce6eb734028208b28a3f4942c402314f34a692d47a6341ac6de75da8b535ecf16fff5fe0001a14701070001000000000000000000000000242f2eb1b005627f7bfa0a75f9a58264e76b3e5556a0c0390c3391d7bbcb01dbb50000000000000000000f4240038e5944509edcc47eabb32e68747f92c61bbea857b1e122649df2acb4112bec600400012401010101010101010101010101010101010101010101010101010101010101012a0000002b40420f0000000000220020c45bdb7d812241b0d1697b60708b02eab5c0f0f9ac4e5764bb9885995643e37a061a8000002a000000000202000000000000044c0000000000000000000000001dcd6500006402d000000000000000070001010000000000000000000009c4000000000bebc2000000000029209080b6d9cb30fefe3c2ed5f9dd0a8c3ed9ccf54d620b701c56410e4ed78fcefca9490174dae76c12d1f3faf5efb7f292840af187d0ea0d008cdb8f46e0efc6ecfdb8b542431cca69c265250c3a3658d3a8b02cedd971cfbb11cd28f65a9ffcc6d5a64400013837689fab474750fc8e9666bb5471d22d1d52ace1214e99e21edbbf6941e9ad7760e73a34b1c1d81a444af0fd52ea77ffb17a6d548684c93b70512a8b18ccc000000000000003e800000000000003e8000000003b9aca00001e009000000000000000070001020000000000000000000009c40000000029209080000000000bebc2002d88d75ca245aedaee7709732088573676471c227727a98b06400a2061846799022bdcbdf3c0ff1e21548b7d455e884599b19f0d57318bf14d56fde08084ec550cff622f2eb1b005627f7bfa0a75f9a58264e76b3e5556a0c0390c3391d7bbcb01dbb5a44461f6b1d094ed5897fd8df1b4783e64bbea90fca7b57c462324918f9d980e24790d7ed23e5bde59de2c46f985473228d923ed2b82996afe4dec7f98890628000000000000000000080000000009c4000000002920908000000000127a3980bf359514f86a18ff97933d96c98066162b1242becb32a7052d3f03ba6f819b67035e30811486ba863d85ac010fa0933784b127b5131a62b2350627e01e93dd052c00000000000000000000070003003e0000fffffffffffc008099021981b18487b451d600bb56646a67d4907a3488c272d433d89a3cf653926400fc0003ffffffffffe80101e785c8322c3b362fc88469cca7a3b9106603b25da1a49e200bd6f55680402c5802000007ffffffffffc80105973e8039fc5967d85026e102f6e5eb92a0f9cc2de954df456b3f280cbab1d1040003ffffffffffe400000000061a80160014761879f7b274ce995f87150a02e75cc0c037e8e300000000fffd01e7020000000001012f2eb1b005627f7bfa0a75f9a58264e76b3e5556a0c0390c3391d7bbcb01dbb50000000000a33aee80054a01000000000000220020387286e7d751204df392696a71984ea6477974810a5e4b2995f4643e429c624b4a01000000000000220020fd9d83d03e8d5299369d43466aa0d0f63942791c2fb9081d06f6170dc58b8dd7b0ad010000000000220020c678813094320ce2737c4a64d5a969022f41c530b4f10aa8e67c5d03e1efdd4804fe0200000000002200202f6616b0cd17eb00350ba0ec9c533de947891504d1540dfc6b76760c2d35252b50870a0000000000220020aa5617da3371dfa43aa83633223bb74554051a5419158bde45ff7df812ddbf690400483045022100c3df79bff08d2d1d253621523eb1d0d8318ec5113ad313ed38a117fae15f6f830220292be1fc199f6ab0c36deb3dab91f45f4d70b7933458187cc005a2db36911c3b01473044022074dae76c12d1f3faf5efb7f292840af187d0ea0d008cdb8f46e0efc6ecfdb8b5022042431cca69c265250c3a3658d3a8b02cedd971cfbb11cd28f65a9ffcc6d5a6440147522102efc3254975667057c8d8ee43cff68662eea27bb06b21cfd6aaf7fe14e33277be21038e5944509edcc47eabb32e68747f92c61bbea857b1e122649df2acb4112bec6052ae9e52f120ff2449a9fcce8fd74e0e41561c700b624df5ccd93e8c0addf9d52e3cfefe30cbd9b603000000ff2449a9fcce8fd74e0e41561c700b624df5ccd93e8c0addf9d52e3cfefe30cbd9b60000000000012449a9fcce8fd74e0e41561c700b624df5ccd93e8c0addf9d52e3cfefe30cbd9b6020000000000000000000000000000000000000000000000" assert(JsonSerializers.serialization.write(ChannelCodecs.channelDataCodec.decode(dataNormal.bits).require.value)(JsonSerializers.formats).contains(""""type":"DATA_NORMAL"""")) assert(JsonSerializers.serialization.write(ChannelCodecs.channelDataCodec.decode(dataShutdown.bits).require.value)(JsonSerializers.formats).contains(""""type":"DATA_SHUTDOWN"""")) @@ -484,7 +516,7 @@ class JsonSerializersSpec extends TestKitBaseClass with AnyFunSuiteLike with Mat } /** utility method that strips line breaks in the expected json */ - def assertJsonEquals(actual: String, expected: String) = { + def assertJsonEquals(actual: String, expected: String): Unit = { val cleanedExpected = expected .replace("\n", "") .replace("\r", "") diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/message/PostmanSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/message/PostmanSpec.scala index 72f345d28f..ff30764253 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/message/PostmanSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/message/PostmanSpec.scala @@ -20,6 +20,7 @@ import akka.actor.testkit.typed.scaladsl.{ScalaTestWithActorTestKit, TestProbe} import akka.actor.typed.ActorRef import akka.actor.typed.eventstream.EventStream import akka.actor.typed.scaladsl.adapter.TypedActorRefOps +import com.softwaremill.quicklens.ModifyPimp import com.typesafe.config.ConfigFactory import fr.acinq.bitcoin.scalacompat.Block import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} @@ -37,8 +38,8 @@ import fr.acinq.eclair.wire.protocol.OnionMessagePayloadTlv.{InvoiceRequest, Rep import fr.acinq.eclair.wire.protocol.RouteBlindingEncryptedDataTlv.PathId import fr.acinq.eclair.wire.protocol.{GenericTlv, MessageOnion, OfferTypes, OnionMessage, OnionMessagePayloadTlv, TlvStream} import fr.acinq.eclair.{EncodedNodeId, Features, MilliSatoshiLong, NodeParams, RealShortChannelId, ShortChannelId, TestConstants, UInt64, randomKey} -import org.scalatest.Outcome import org.scalatest.funsuite.FixtureAnyFunSuiteLike +import org.scalatest.{Outcome, Tag} import scodec.bits.HexStringSyntax import scala.annotation.tailrec @@ -46,10 +47,11 @@ import scala.concurrent.duration.DurationInt class PostmanSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("application")) with FixtureAnyFunSuiteLike { + private val ShortTimeout = "short timeout" case class FixtureParam(postman: ActorRef[Command], nodeParams: NodeParams, messageSender: TestProbe[OnionMessageResponse], switchboard: TestProbe[Any], offerManager: TestProbe[RequestInvoice], router: TestProbe[Router.PostmanRequest]) override def withFixture(test: OneArgTest): Outcome = { - val nodeParams = TestConstants.Alice.nodeParams + val nodeParams = if (test.tags.contains(ShortTimeout)) TestConstants.Alice.nodeParams.modify(_.onionMessageConfig.timeout).setTo(1 millis) else TestConstants.Alice.nodeParams val messageSender = TestProbe[OnionMessageResponse]("messageSender") val switchboard = TestProbe[Any]("switchboard") val offerManager = TestProbe[RequestInvoice]("offerManager") @@ -131,7 +133,7 @@ class PostmanSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("applicat messageSender.expectNoMessage(10 millis) } - test("timeout") { f => + test("timeout", Tag(ShortTimeout)) { f => import f._ val recipientKey = randomKey() 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 f8d25b39c2..dc185b4dca 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 @@ -37,9 +37,9 @@ import scala.util.Success class Bolt11InvoiceSpec extends AnyFunSuite { - val priv = PrivateKey(hex"e126f68f7eafcc8b74f54d269fe206be715000f94dac067d1c04a8ca3b2db734") - val pub = priv.publicKey - val nodeId = pub + val priv: PrivateKey = PrivateKey(hex"e126f68f7eafcc8b74f54d269fe206be715000f94dac067d1c04a8ca3b2db734") + val pub: PublicKey = priv.publicKey + val nodeId: PublicKey = pub assert(nodeId == PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) // Copy of Bolt11Invoice.apply that doesn't strip unknown features @@ -386,6 +386,24 @@ class Bolt11InvoiceSpec extends AnyFunSuite { assert(invoice.sign(priv).toString == ref) } + test("On mainnet, with fallback (P2TR) address bc1pptdvg0d2nj99568qn6ssdy4cygnwuxgw2ukmnwgwz7jpqjz2kszse2s3lm") { + val ref = "lnbc20m1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsfp4pptdvg0d2nj99568qn6ssdy4cygnwuxgw2ukmnwgwz7jpqjz2kszs9qrsgqy606dznq28exnydt2r4c29y56xjtn3sk4mhgjtl4pg2y4ar3249rq4ajlmj9jy8zvlzw7cr8mggqzm842xfr0v72rswzq9xvr4hknfsqwmn6xd" + val Success(invoice) = Bolt11Invoice.fromString(ref) + assert(invoice.prefix == "lnbc") + assert(invoice.amount_opt.contains(2000000000 msat)) + assert(invoice.nodeId == PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) + assert(invoice.fallbackAddress().contains("bc1pptdvg0d2nj99568qn6ssdy4cygnwuxgw2ukmnwgwz7jpqjz2kszse2s3lm")) + assert(invoice.sign(priv).toString == ref) + } + + test("On mainnet, public-key recovery with high-S signature") { + val ref = "lnbc1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpl2pkx2ctnv5sxxmmwwd5kgetjypeh2ursdae8g6twvus8g6rfwvs8qun0dfjkxaq9qrsgq357wnc5r2ueh7ck6q93dj32dlqnls087fxdwk8qakdyafkq3yap2r09nt4ndd0unm3z9u5t48y6ucv4r5sg7lk98c77ctvjczkspk5qprc90gx" + val Success(invoice) = Bolt11Invoice.fromString(ref) + assert(invoice.prefix == "lnbc") + assert(invoice.amount_opt.isEmpty) + assert(invoice.nodeId == PublicKey(hex"02d0139ce7427d6dfffd26a326c18be754ef1e64672b42694ba5b23ef6e6e7803d")) + } + test("reject invalid invoices") { val refs = Seq( // Bech32 checksum is invalid. @@ -404,8 +422,14 @@ class Bolt11InvoiceSpec extends AnyFunSuite { "lnbc2500000001p1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpusp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9qrsgq0lzc236j96a95uv0m3umg28gclm5lqxtqqwk32uuk4k6673k6n5kfvx3d2h8s295fad45fdhmusm8sjudfhlf6dcsxmfvkeywmjdkxcp99202x", // Missing payment secret. "lnbc1qqygh9qpp5s7zxqqqqqqqqqqqqpjqqqqqqqqqqqqqqqqqqcqpjqqqsqqqqqqqqdqqqqqqqqqqqqqqqqqqqqqqqqqqqqquqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzxqqqqqqqqqqqqqqqy6f523d", + // Missing payment secret. + "lnbc20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqs9qrsgq7ea976txfraylvgzuxs8kgcw23ezlrszfnh8r6qtfpr6cxga50aj6txm9rxrydzd06dfeawfk6swupvz4erwnyutnjq7x39ymw6j38gp49qdkj", // Invalid signature public key recovery id. - "lnbc1qqqqpqqnp4qqqlftcw9qqqqqqqqqqqqygh9qpp5qpp5s7zxqqqqcqpjpqqygh9qpp5s7zxqqqqcqpjpqqlqqqqqqqqqqqqcqqpqqqqqqqqqqqsqqqqqqqqdqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqlqqqcqpjptfqptfqptfqpqqqqqqqqqqqqqqqqqqq8ddm0a" + "lnbc1qqqqpqqnp4qqqlftcw9qqqqqqqqqqqqygh9qpp5qpp5s7zxqqqqcqpjpqqygh9qpp5s7zxqqqqcqpjpqqlqqqqqqqqqqqqcqqpqqqqqqqqqqqsqqqqqqqqdqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqlqqqcqpjptfqptfqptfqpqqqqqqqqqqqqqqqqqqq8ddm0a", + // Empty routing hint field. + "lnbc1p5q54jjpp5fe0dhqdt4m97psq0fv3wjlk95cclnatvuvq49xtnc8rzrp0dysusdqqcqzzsxqrrs0fppqy6uew5229e67r9xzzm9mjyfwseclstdgsp5rnanj9x5rnanj9xnq28hhgd6c7yxlmh6lta047h6lqqqqqqqqqqqrqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq6qqqqqqqqqqqqqqqqqqq9kvnknh7ug5mttnqqqqqqqqq8849gwfhvnp9rqpe0cy97", + // Invalid min_final_expiry_delta_blocks. + "lnbc1p5q54jjpp5fe0dhqdt4m97psq0fv3wjlk95cclnatvuvq49xtnc8rzrp0d5susdqqcqg3vrywwwjsppqy6uew5229e67rzxzzm9mjyfwseclstdgsp5rnanj9xnq28hhgd6c7yxlmh6lta047h6lqqqqqqqqqqqrqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq9kvnknh7ug5mttn2yu5ha6m98cpda2rtwu08849gwfhvnp9rqpqqqqqqqqqg58lts", ) for (ref <- refs) { assert(Bolt11Invoice.fromString(ref).isFailure) @@ -528,6 +552,7 @@ class Bolt11InvoiceSpec extends AnyFunSuite { } test("feature bits to minimally-encoded feature bytes") { + // Invoice features are encoded as 5-bits chunks, which we decode to bytes. val testCases = Seq( (bin" 0010000100000101", hex" 2105"), (bin" 1010000100000101", hex" a105"), @@ -539,8 +564,24 @@ class Bolt11InvoiceSpec extends AnyFunSuite { (bin"1000110000000000110", hex"046006") ) - for ((bitmask, featureBytes) <- testCases) { - assert(Features(bitmask).toByteVector == featureBytes) + for ((invoiceFeatureBits, featureBytes) <- testCases) { + assert(Features(invoiceFeatureBits).toByteVector == featureBytes) + } + } + + test("feature bytes to minimally-encoded feature bits") { + // When encoding features into 5-bits chunks, we want to get rid of leading zeroes. + val testCases = Seq( + (bin"00010000", bin"10000"), + (bin"00100000", bin"0000100000"), + (bin"10010000", bin"0010010000"), + (bin"0000000100000000", bin"0100000000"), + (bin"0000001000000000", bin"1000000000"), + (bin"0000010000000000", bin"000010000000000"), + ) + + for ((featureBytes, invoiceFeatureBits) <- testCases) { + assert(features2bits(Features(featureBytes)) == invoiceFeatureBits) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/Bolt12InvoiceSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/Bolt12InvoiceSpec.scala index 15282f14f2..a1389b70c5 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/Bolt12InvoiceSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/Bolt12InvoiceSpec.scala @@ -54,7 +54,7 @@ class Bolt12InvoiceSpec extends AnyFunSuite { def createPaymentBlindedRoute(nodeId: PublicKey, sessionKey: PrivateKey = randomKey(), pathId: ByteVector = randomBytes32()): PaymentBlindedRoute = { val selfPayload = blindedRouteDataCodec.encode(TlvStream(PathId(pathId), PaymentConstraints(CltvExpiry(1234567), 0 msat), AllowedFeatures(Features.empty))).require.bytes - PaymentBlindedRoute(Sphinx.RouteBlinding.create(sessionKey, Seq(nodeId), Seq(selfPayload)).route, PaymentInfo(1 msat, 2, CltvExpiryDelta(3), 4 msat, 5 msat, Features.empty)) + PaymentBlindedRoute(Sphinx.RouteBlinding.create(sessionKey, Seq(nodeId), Seq(selfPayload)).route, PaymentInfo(1 msat, 2, CltvExpiryDelta(3), 4 msat, 5 msat, ByteVector.empty)) } test("check invoice signature") { @@ -70,7 +70,7 @@ class Bolt12InvoiceSpec extends AnyFunSuite { // changing fields makes the signature invalid val withModifiedUnknownTlv = Bolt12Invoice(invoice.records.copy(unknown = Set(GenericTlv(UInt64(7), hex"ade4")))) assert(!withModifiedUnknownTlv.checkSignature()) - val withModifiedAmount = Bolt12Invoice(TlvStream(invoice.records.records.map { case OfferAmount(amount) => OfferAmount(amount + 100.msat) case x => x }, invoice.records.unknown)) + val withModifiedAmount = Bolt12Invoice(TlvStream(invoice.records.records.map { case OfferAmount(amount) => OfferAmount(amount + 100) case x => x }, invoice.records.unknown)) assert(!withModifiedAmount.checkSignature()) } @@ -92,7 +92,7 @@ class Bolt12InvoiceSpec extends AnyFunSuite { val invoice = Bolt12Invoice(request, randomBytes32(), nodeKey, 300 seconds, Features.empty, Seq(createPaymentBlindedRoute(nodeKey.publicKey))) assert(invoice.validateFor(request, nodeKey.publicKey).isRight) // amount must match the request - val withOtherAmount = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.map { case OfferAmount(_) => OfferAmount(9000 msat) case x => x })), nodeKey) + val withOtherAmount = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.map { case OfferAmount(_) => OfferAmount(9000) case x => x })), nodeKey) assert(withOtherAmount.validateFor(request, nodeKey.publicKey).isLeft) // description must match the offer val withOtherDescription = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.map { case OfferDescription(_) => OfferDescription("other description") case x => x })), nodeKey) @@ -127,7 +127,7 @@ class Bolt12InvoiceSpec extends AnyFunSuite { assert(request.quantity_opt.isEmpty) // when paying for a single item, the quantity field must not be present val invoice = Bolt12Invoice(request, randomBytes32(), nodeKey, 300 seconds, Features(BasicMultiPartPayment -> Optional), Seq(createPaymentBlindedRoute(nodeKey.publicKey))) assert(invoice.validateFor(request, nodeKey.publicKey).isRight) - val withInvalidFeatures = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.map { case InvoiceFeatures(_) => InvoiceFeatures(Features(BasicMultiPartPayment -> Mandatory)) case x => x })), nodeKey) + val withInvalidFeatures = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.map { case InvoiceFeatures(_) => InvoiceFeatures(Features(BasicMultiPartPayment -> Mandatory).toByteVector) case x => x })), nodeKey) assert(withInvalidFeatures.validateFor(request, nodeKey.publicKey).isLeft) val withAmountTooBig = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.map { case InvoiceRequestAmount(_) => InvoiceRequestAmount(20000 msat) case x => x })), nodeKey) assert(withAmountTooBig.validateFor(request, nodeKey.publicKey).isLeft) @@ -148,7 +148,7 @@ class Bolt12InvoiceSpec extends AnyFunSuite { InvoiceRequestAmount(15000 msat), InvoiceRequestPayerId(payerKey.publicKey), InvoiceRequestPayerNote("I am Batman"), - OfferFeatures(Features(VariableLengthOnion -> Mandatory)) + OfferFeatures(Features(VariableLengthOnion -> Mandatory).toByteVector) ) val signature = signSchnorr(InvoiceRequest.signatureTag, rootHash(TlvStream(tlvs), invoiceRequestTlvCodec), payerKey) InvoiceRequest(TlvStream(tlvs + Signature(signature))) @@ -188,7 +188,7 @@ class Bolt12InvoiceSpec extends AnyFunSuite { InvoiceRequestAmount(1684 msat), InvoiceRequestPayerId(randomKey().publicKey), InvoicePaths(Seq(createPaymentBlindedRoute(randomKey().publicKey).route)), - InvoiceBlindedPay(Seq(PaymentInfo(0 msat, 0, CltvExpiryDelta(0), 0 msat, 765432 msat, Features.empty))), + InvoiceBlindedPay(Seq(PaymentInfo(0 msat, 0, CltvExpiryDelta(0), 0 msat, 765432 msat, ByteVector.empty))), InvoiceCreatedAt(TimestampSecond(123456789L)), InvoicePaymentHash(randomBytes32()), InvoiceAmount(1684 msat), @@ -217,7 +217,7 @@ class Bolt12InvoiceSpec extends AnyFunSuite { val features = Features.empty val issuer = "alice" val nodeKey = PrivateKey(hex"998cf8ecab46f949bb960813b79d3317cabf4193452a211795cd8af1b9a25d90") - val path = createPaymentBlindedRoute(nodeKey.publicKey, PrivateKey(hex"f0442c17bdd2cefe4a4ede210f163b068bb3fea6113ffacea4f322de7aa9737b"), hex"76030536ba732cdc4e7bb0a883750bab2e88cb3dddd042b1952c44b4849c86bb").copy(paymentInfo = PaymentInfo(2345 msat, 765, CltvExpiryDelta(324), 1000 msat, amount, Features.empty)) + val path = createPaymentBlindedRoute(nodeKey.publicKey, PrivateKey(hex"f0442c17bdd2cefe4a4ede210f163b068bb3fea6113ffacea4f322de7aa9737b"), hex"76030536ba732cdc4e7bb0a883750bab2e88cb3dddd042b1952c44b4849c86bb").copy(paymentInfo = PaymentInfo(2345 msat, 765, CltvExpiryDelta(324), 1000 msat, amount, ByteVector.empty)) val quantity = 57 val payerKey = PublicKey(hex"024a8d96f4d13c4219f211b8a8e7b4ab7a898fd1b2e90274ca5a8737a9eda377f8") val payerNote = "I'm Bob" @@ -229,9 +229,9 @@ class Bolt12InvoiceSpec extends AnyFunSuite { val tlvs = TlvStream[InvoiceTlv](Set[InvoiceTlv]( InvoiceRequestMetadata(payerInfo), OfferChains(Seq(chain)), - OfferAmount(amount), + OfferAmount(amount.toLong), OfferDescription(description), - OfferFeatures(Features.empty), + OfferFeatures(ByteVector.empty), OfferIssuer(issuer), OfferNodeId(nodeKey.publicKey), InvoiceRequestChain(chain), @@ -246,7 +246,7 @@ class Bolt12InvoiceSpec extends AnyFunSuite { InvoicePaymentHash(paymentHash), InvoiceAmount(amount), InvoiceFallbacks(fallbacks), - InvoiceFeatures(Features.empty), + InvoiceFeatures(ByteVector.empty), InvoiceNodeId(nodeKey.publicKey), ), Set(GenericTlv(UInt64(121), hex"010203"), GenericTlv(UInt64(313), hex"baba"))) val signature = signSchnorr(Bolt12Invoice.signatureTag, rootHash(tlvs, invoiceTlvCodec), nodeKey) @@ -339,7 +339,7 @@ class Bolt12InvoiceSpec extends AnyFunSuite { val preimage = ByteVector32(hex"99221825b86576e94391b179902be8b22c7cfa7c3d14aec6ae86657dfd9bd2a8") val offer = Offer(TlvStream[OfferTlv]( OfferChains(Seq(Block.Testnet3GenesisBlock.hash)), - OfferAmount(100000 msat), + OfferAmount(100000), OfferDescription("offer with quantity"), OfferIssuer("alice@bigshop.com"), OfferQuantityMax(1000), @@ -376,4 +376,16 @@ class Bolt12InvoiceSpec extends AnyFunSuite { assert(invoice.paymentHash == ByteVector32(hex"14805a7006b96286e7b0a3f618c1cd7f1059f76da766044c5bfc3fa31d5e9442")) assert(invoice.description.contains("yolo")) } + + test("invoice with non-minimally encoded feature bits"){ + val encodedInvoice = "lni1qqsyzre2s0lc77w5h33ck6540xxsyjehjl66f9tfp83w85zcxqyhltczyqrzymjxzydqkkw24ufxqslttwlj3s608f0rx2slc7etw0833zgs7zqyqh67zqq2qqgwsqktzd0na8g54f2r8secsaemc7ww2d6spl397celwcv20egnau2z8gp83d0dg7gvtkkvklnqlvp0erhq9nh9928rexerg578wnyew6dj6xczq2nqtavvd94k7jq2slng76uk560g6qeu38ru2gjjtdd4w9jxfqcc5qpnvvduearw4k75xdsgrc9ntzs274hwumtk5zwlrcr8yzwn8q0ry40f6lcmarq2nqkz9j2anajrlpchwwfguypms9x0uptvcsspwzjp3vg8srqx27crkqe8v9nzqaktzwwy5szk0rsq9sq7vhqncvv63mseqsx9lzmjraxhfnhc6f9tgnm05v7x0s4dhzwac9gruy44n9yht645cd4jzcssyjvcf2ptqztsenmzyw0e6kpx209mmmpal9ptutxpeygerepwh5rc2qsqvgnwgcg35z6ee2h3yczraddm72xrfua9uve2rlrm9deu7xyfzr6jqsae4jsq2spsyqqqtqss9g4l2s06jx69u2vtvezfmh07puh8pzhp76yddr7yvjpt2q38puqx5r7sgacrnpvghfhfzdzm9rertx4egjnarr2plwp26yfzcnv4ef536h9nu8lq9xyejhphnyv97axrqwr982vvedhfzj3cn5uhdymxwejfh55p2putqvpeskyt5m53x3dj3u34n2u5ff7334qlhq4dzy3vfk2u56gatje7rlsqgllx5cs3433fgn37scpz5ysn7df4tcfvgw5hgn998qut5l63vvmlv85xj4gj9rs6ja6gj45ddfjvwrcq9qthepk3xtpy4x8tsmmaqhas3v8k6chxp4ds8367lgw3q4mtpm5zmlr84tx4xpshtaxa0es0kcjuah80xt23pm08qprase5e2euq8ndvymuzcdznh78qyg28lw65wve2fpphd5zpwy4v3gfpa245dgtmqkp34gg8s4tfxytnx5vxhclwzmpzdy80jlfyznklk9t0karg42yvxqey68py3t0yg5rew5jke2sr6l5akw3r4x4cyp5f9ty27yjqtsn5ucqywkk84sxudl89xdxw34kvvtq67pk64r3kmyzz5dum0c66sjh7a5ylr6u38ycdmdq5rm7pp5m87rmsg7ntkqr4dcateeafrchaw085my236hxg47745nsrdtmjvnhy4a9ppd95g5m3u40wa0pcnmlhcm99xd0flh0484vht6ysx5cg5nmjxzaqsqv33sgptsrgmfuqgwjuvw5v58k379638h6hda8tqvpk4aexfmj27jsskj6y2dc72hhwhsufalmudjjnxh5lmh6n6kt4azgqg7en2fg446vmtj2zgncc9wv4sa8zhyxm60zadqlf664d8mhdx6g5g6cls2glkqdmayuvypt7fuljtswlmz4w5e8nkkpzr8m6txz7gzvfcexj9dmdhuhsx35lnwnmzm52vq2wgr49g25dwk4jlh0n2yq6yufpewngg7llkgxwqpr5nlruajj55sel09axp2tmkhaf2hkh2lsjyth098l2r2kfg7u9440ymwswpwd20j9zdp562ejm0yy0x68q4knmd6a6g4nz0a2nm3842yw4pdx8udqggqkxa03jwmrzzuzwp2mn6az3exhunlqcpmphsks3cur22l3hvzn74vqy0kf70r6hd5cy2va94czl9g594856j287cefqej8qlre5ewyc5l02wtsx0hcjr4jhup6z4rj46lmrylsr034r5w2csnsgcy83yz848lafh5wue9aue8grnpvghfhfzdzm9rertx4egjnarr2plwp26yfzcnv4ef536h9nu8lq86u0a3w8zcxwy9hj9gvdwv8fhahpdauyzmuegpkefl3xc798mft7qvpeskyt5m53x3dj3u34n2u5ff7334qlhq4dzy3vfk2u56gatje7rlsqg0xlmw039msmmqtt4jqkgqts08ervu9dsx05qwzr67dazwklna9yjzdker5mhmeghxde2jlu5gvl4wrshvrg6x6a0j7hqsgpcc3ngm0ucvftuq6k8q0tpgxknk3d3t8nc9p9frafrfndz788hkaut704urzsj06t45qy8qk5hewf9p3sej3m2xrwyk6ny5hg8t24aq50a7re8evssrd0nmtrpjttuj04nlhs8ygteqepyc6sg5lsdajrc63xjp26j7surx83vx5u4326qfk6vw0sqhme6cw9247ef75ymtz4mp3esduvl07ykrnzzre3aq5jgqzrzcj59yjdcvp38nq7uvdqwmnhvy0h7t9062znl8ly02k9d02tyxev6mf6we8ztfjrdu73wc6gctxg5lmgj4a8v8z9lzqdfvlsmcwzyznagl929pqqqqyfjqqqqqpszqcqqqqqqqqqq3xqqqqqqz0490jqqqgqqqqqqqqqqqqqqqqqqqqqqqqqqqqqpzvsqqqqqvqsxqqqqqqqqqqyfsqqqqqqnaftusqqzqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqgnyqqqqqrqypsqqqqqqqqqpzvqqqqqqp4rw2vqqqsqqqqqqqqqqqqqqqqqqqqqqqqqzjqg6zm7ju2sgrmk0u67xstmskz34gfjfnjfxwvjltp3jsrd8rn40s7pgk8tzxwt64qgwu6egqtqggzfxvy4q4sp9cvea3z88uatqn98jaaas7ljs479nqujyv3usht6pu0qs8wdac52sykqfjnxg0xhva4fcv00hr4tqzjwkjnkayykkm9dnr97ladr5jjjx4xyjtun7ucye660akfv4nl9tupwnyemp0sasfxapvcw" + val invoice = Bolt12Invoice.fromString(encodedInvoice).get + assert(invoice.checkSignature()) + assert(invoice.amount == 1000000000.msat) + } + + test("invoice paths is set but and empty") { + val invoiceWithEmptyPaths = "lni1qqx2n6mw2fh2ckwdnwylkgqzypp5jl7hlqnf2ugg7j3slkwwcwht57vhyzzwjr4dq84rxzgqqqqqqzqrq83yqzscd9h8vmmfvdjjqamfw35zqmtpdeujqenfv4kxgucvqqfq2ctvd93k293pq0zxw03kpc8tc2vv3kfdne0kntqhq8p70wtdncwq2zngaqp529mmc5pqgdyhl4lcy62hzz855v8annkr46a8n9eqsn5satgpagesjqqqqqq9yqcpufq9vqfetqssyj5djm6dz0zzr8eprw9gu762k75f3lgm96gzwn994peh48k6xalctyr5jfmdyppx7cneqvqsyqaqqz3qpfqyv2sqd04xqg8pp2pq2x236nzneyzqxhct9y7unhcupeukwgf5xzhq0f0nuy6v6vej2dq65qcpufq2cysyqqzpy02klqrqqz8t8twx39z77cq6uq9syypugee7xc8qa0pf3jxe9k0976dvzuqu8eaedk0pcpg2dr5qx3gh008sgrn58w7cg2qhcunaapk9j6patmtda7nhqhzvwv6hflxygyrrglpqka8l6zfhfhprxazkufcn88rl07yxfp5mvjl70etp2pzdkhud3ekul5qnjq46hg" + assert(Bolt12Invoice.fromString(invoiceWithEmptyPaths).isFailure) + } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartHandlerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartHandlerSpec.scala index 00b8f6eea3..14fe0755d8 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartHandlerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartHandlerSpec.scala @@ -22,7 +22,7 @@ import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, Crypto} import fr.acinq.eclair.FeatureSupport.{Mandatory, Optional} import fr.acinq.eclair.Features.{KeySend, _} import fr.acinq.eclair.TestConstants.Alice -import fr.acinq.eclair.channel.{CMD_FAIL_HTLC, CMD_FULFILL_HTLC, Register} +import fr.acinq.eclair.channel.{CMD_FAIL_HTLC, CMD_FULFILL_HTLC, FailureAttributionData, FulfillAttributionData, Register} import fr.acinq.eclair.db.{IncomingBlindedPayment, IncomingPaymentStatus, IncomingStandardPayment, PaymentType} import fr.acinq.eclair.payment.Bolt11Invoice.ExtraHop import fr.acinq.eclair.payment.PaymentReceived.PartialPayment @@ -31,6 +31,7 @@ import fr.acinq.eclair.payment.receive.MultiPartHandler._ import fr.acinq.eclair.payment.receive.MultiPartPaymentFSM.HtlcPart import fr.acinq.eclair.payment.receive.{MultiPartPaymentFSM, PaymentHandler} import fr.acinq.eclair.payment.relay.Relayer.RelayFees +import fr.acinq.eclair.reputation.Reputation import fr.acinq.eclair.router.BlindedRouteCreation.aggregatePaymentInfo import fr.acinq.eclair.router.Router import fr.acinq.eclair.router.Router.ChannelHop @@ -83,7 +84,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike lazy val handlerWithKeySend = TestActorRef[PaymentHandler](PaymentHandler.props(nodeParams.copy(features = featuresWithKeySend), register.ref, offerManager.ref)) lazy val handlerWithRouteBlinding = TestActorRef[PaymentHandler](PaymentHandler.props(nodeParams.copy(features = featuresWithRouteBlinding), register.ref, offerManager.ref)) - def createEmptyReceivingRoute(pathId: ByteVector): Seq[ReceivingRoute] = Seq(ReceivingRoute(Nil, pathId, CltvExpiryDelta(144), PaymentInfo(0 msat, 0, CltvExpiryDelta(0), 0 msat, 1_000_000_000 msat, Features.empty))) + def createEmptyReceivingRoute(pathId: ByteVector): Seq[ReceivingRoute] = Seq(ReceivingRoute(Nil, pathId, CltvExpiryDelta(144), PaymentInfo(0 msat, 0, CltvExpiryDelta(0), 0 msat, 1_000_000_000 msat, ByteVector.empty))) } override def withFixture(test: OneArgTest): Outcome = { @@ -97,9 +98,9 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike } def createBlindedPacket(amount: MilliSatoshi, paymentHash: ByteVector32, expiry: CltvExpiry, finalExpiry: CltvExpiry, pathId: ByteVector): IncomingPaymentPacket.FinalPacket = { - val add = UpdateAddHtlc(ByteVector32.One, 0, amount, paymentHash, expiry, TestConstants.emptyOnionPacket, Some(randomKey().publicKey), 1.0, None) + val add = UpdateAddHtlc(ByteVector32.One, 0, amount, paymentHash, expiry, TestConstants.emptyOnionPacket, Some(randomKey().publicKey), Reputation.maxEndorsement, None) val payload = FinalPayload.Blinded(TlvStream(AmountToForward(amount), TotalAmount(amount), OutgoingCltv(finalExpiry), EncryptedRecipientData(hex"deadbeef")), TlvStream(PathId(pathId), PaymentConstraints(CltvExpiry(500_000), 1 msat))) - IncomingPaymentPacket.FinalPacket(add, payload) + IncomingPaymentPacket.FinalPacket(add, payload, TimestampMilli.now()) } test("PaymentHandler should reply with a fulfill/fail, emit a PaymentReceived and add payment in DB") { f => @@ -116,8 +117,8 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike assert(!incoming.get.invoice.isExpired()) assert(Crypto.sha256(incoming.get.paymentPreimage) == invoice.paymentHash) - val add = UpdateAddHtlc(ByteVector32.One, 1, amountMsat, invoice.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket, None, 1.0, None) - sender.send(handlerWithoutMpp, IncomingPaymentPacket.FinalPacket(add, FinalPayload.Standard.createPayload(add.amountMsat, add.amountMsat, add.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata))) + val add = UpdateAddHtlc(ByteVector32.One, 1, amountMsat, invoice.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket, None, Reputation.maxEndorsement, None) + sender.send(handlerWithoutMpp, IncomingPaymentPacket.FinalPacket(add, FinalPayload.Standard.createPayload(add.amountMsat, add.amountMsat, add.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata), TimestampMilli.now())) assert(register.expectMsgType[Register.Forward[CMD_FULFILL_HTLC]].message.id == add.id) val paymentReceived = eventListener.expectMsgType[PaymentReceived] @@ -132,8 +133,8 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike sender.send(handlerWithoutMpp, ReceiveStandardPayment(sender.ref, Some(50_000 msat), Left("1 coffee with extra fees and expiry"))) val invoice = sender.expectMsgType[Bolt11Invoice] - val add = UpdateAddHtlc(ByteVector32.One, 1, 75_000 msat, invoice.paymentHash, defaultExpiry + CltvExpiryDelta(12), TestConstants.emptyOnionPacket, None, 1.0, None) - sender.send(handlerWithoutMpp, IncomingPaymentPacket.FinalPacket(add, FinalPayload.Standard.createPayload(70_000 msat, 70_000 msat, defaultExpiry, invoice.paymentSecret, invoice.paymentMetadata))) + val add = UpdateAddHtlc(ByteVector32.One, 1, 75_000 msat, invoice.paymentHash, defaultExpiry + CltvExpiryDelta(12), TestConstants.emptyOnionPacket, None, Reputation.maxEndorsement, None) + sender.send(handlerWithoutMpp, IncomingPaymentPacket.FinalPacket(add, FinalPayload.Standard.createPayload(70_000 msat, 70_000 msat, defaultExpiry, invoice.paymentSecret, invoice.paymentMetadata), TimestampMilli.now())) assert(register.expectMsgType[Register.Forward[CMD_FULFILL_HTLC]].message.id == add.id) val paymentReceived = eventListener.expectMsgType[PaymentReceived] @@ -150,8 +151,8 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike assert(invoice.features.hasFeature(BasicMultiPartPayment)) assert(nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).get.status == IncomingPaymentStatus.Pending) - val add = UpdateAddHtlc(ByteVector32.One, 2, amountMsat, invoice.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket, None, 1.0, None) - sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, FinalPayload.Standard.createPayload(add.amountMsat, add.amountMsat, add.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata))) + val add = UpdateAddHtlc(ByteVector32.One, 2, amountMsat, invoice.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket, None, Reputation.maxEndorsement, None) + sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, FinalPayload.Standard.createPayload(add.amountMsat, add.amountMsat, add.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata), TimestampMilli.now())) assert(register.expectMsgType[Register.Forward[CMD_FULFILL_HTLC]].message.id == add.id) val paymentReceived = eventListener.expectMsgType[PaymentReceived] @@ -197,8 +198,8 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val invoice = sender.expectMsgType[Bolt11Invoice] assert(nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).get.status == IncomingPaymentStatus.Pending) - val add = UpdateAddHtlc(ByteVector32.One, 0, amountMsat, invoice.paymentHash, CltvExpiryDelta(3).toCltvExpiry(nodeParams.currentBlockHeight), TestConstants.emptyOnionPacket, None, 1.0, None) - sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, FinalPayload.Standard.createPayload(add.amountMsat, add.amountMsat, add.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata))) + val add = UpdateAddHtlc(ByteVector32.One, 0, amountMsat, invoice.paymentHash, CltvExpiryDelta(3).toCltvExpiry(nodeParams.currentBlockHeight), TestConstants.emptyOnionPacket, None, Reputation.maxEndorsement, None) + sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, FinalPayload.Standard.createPayload(add.amountMsat, add.amountMsat, add.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata), TimestampMilli.now())) val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message assert(cmd.reason == FailureReason.LocalFailure(IncorrectOrUnknownPaymentDetails(amountMsat, nodeParams.currentBlockHeight))) assert(nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).get.status == IncomingPaymentStatus.Pending) @@ -279,7 +280,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val receivingRoutes = Seq( ReceivingRoute(hops1, randomBytes32(), CltvExpiryDelta(100), aggregatePaymentInfo(amount, hops1, nodeParams.channelConf.minFinalExpiryDelta)), ReceivingRoute(hops2, randomBytes32(), CltvExpiryDelta(50), aggregatePaymentInfo(amount, hops2, nodeParams.channelConf.minFinalExpiryDelta)), - ReceivingRoute(Nil, randomBytes32(), CltvExpiryDelta(250), PaymentInfo(0 msat, 0, nodeParams.channelConf.minFinalExpiryDelta, 0 msat, amount, Features.empty)), + ReceivingRoute(Nil, randomBytes32(), CltvExpiryDelta(250), PaymentInfo(0 msat, 0, nodeParams.channelConf.minFinalExpiryDelta, 0 msat, amount, ByteVector.empty)), ) sender.send(handlerWithRouteBlinding, ReceiveOfferPayment(sender.ref, privKey, invoiceReq, receivingRoutes, randomBytes32())) val invoice = sender.expectMsgType[Bolt12Invoice] @@ -291,13 +292,13 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike assert(invoice.blindedPaths.length == 3) assert(invoice.blindedPaths(0).route.blindedNodeIds.length == 4) assert(invoice.blindedPaths(0).route.firstNodeId == EncodedNodeId(a)) - assert(invoice.blindedPaths(0).paymentInfo == PaymentInfo(1950 msat, 0, CltvExpiryDelta(193), 1 msat, 25_000 msat, Features.empty)) + assert(invoice.blindedPaths(0).paymentInfo == PaymentInfo(1950 msat, 0, CltvExpiryDelta(193), 1 msat, 25_000 msat, ByteVector.empty)) assert(invoice.blindedPaths(1).route.blindedNodeIds.length == 4) assert(invoice.blindedPaths(1).route.firstNodeId == EncodedNodeId(c)) - assert(invoice.blindedPaths(1).paymentInfo == PaymentInfo(400 msat, 0, CltvExpiryDelta(183), 1 msat, 25_000 msat, Features.empty)) + assert(invoice.blindedPaths(1).paymentInfo == PaymentInfo(400 msat, 0, CltvExpiryDelta(183), 1 msat, 25_000 msat, ByteVector.empty)) assert(invoice.blindedPaths(2).route.blindedNodeIds.length == 1) assert(invoice.blindedPaths(2).route.firstNodeId == EncodedNodeId(d)) - assert(invoice.blindedPaths(2).paymentInfo == PaymentInfo(0 msat, 0, CltvExpiryDelta(18), 0 msat, 25_000 msat, Features.empty)) + assert(invoice.blindedPaths(2).paymentInfo == PaymentInfo(0 msat, 0, CltvExpiryDelta(18), 0 msat, 25_000 msat, ByteVector.empty)) // Offer invoices shouldn't be stored in the DB until we receive a payment for it. assert(nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).isEmpty) // Check that all non-final encrypted payloads for blinded routes have the same length. @@ -330,8 +331,8 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike assert(!invoice.features.hasFeature(BasicMultiPartPayment)) assert(invoice.isExpired()) - val add = UpdateAddHtlc(ByteVector32.One, 0, 1000 msat, invoice.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket, None, 1.0, None) - sender.send(handlerWithoutMpp, IncomingPaymentPacket.FinalPacket(add, FinalPayload.Standard.createPayload(add.amountMsat, add.amountMsat, add.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata))) + val add = UpdateAddHtlc(ByteVector32.One, 0, 1000 msat, invoice.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket, None, Reputation.maxEndorsement, None) + sender.send(handlerWithoutMpp, IncomingPaymentPacket.FinalPacket(add, FinalPayload.Standard.createPayload(add.amountMsat, add.amountMsat, add.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata), TimestampMilli.now())) register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]] val Some(incoming) = nodeParams.db.payments.getIncomingPayment(invoice.paymentHash) assert(incoming.invoice.isExpired() && incoming.status == IncomingPaymentStatus.Expired) @@ -345,8 +346,8 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike assert(invoice.features.hasFeature(BasicMultiPartPayment)) assert(invoice.isExpired()) - val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, invoice.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket, None, 1.0, None) - sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, FinalPayload.Standard.createPayload(add.amountMsat, 1000 msat, add.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata))) + val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, invoice.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket, None, Reputation.maxEndorsement, None) + sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, FinalPayload.Standard.createPayload(add.amountMsat, 1000 msat, add.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata), TimestampMilli.now())) val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message assert(cmd.reason == FailureReason.LocalFailure(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight))) val Some(incoming) = nodeParams.db.payments.getIncomingPayment(invoice.paymentHash) @@ -360,8 +361,8 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val invoice = sender.expectMsgType[Bolt11Invoice] assert(!invoice.features.hasFeature(BasicMultiPartPayment)) - val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, invoice.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket, None, 1.0, None) - sender.send(handlerWithoutMpp, IncomingPaymentPacket.FinalPacket(add, FinalPayload.Standard.createPayload(add.amountMsat, 1000 msat, add.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata))) + val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, invoice.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket, None, Reputation.maxEndorsement, None) + sender.send(handlerWithoutMpp, IncomingPaymentPacket.FinalPacket(add, FinalPayload.Standard.createPayload(add.amountMsat, 1000 msat, add.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata), TimestampMilli.now())) val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message assert(cmd.reason == FailureReason.LocalFailure(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight))) assert(nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).get.status == IncomingPaymentStatus.Pending) @@ -375,8 +376,8 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike assert(invoice.features.hasFeature(BasicMultiPartPayment)) val lowCltvExpiry = nodeParams.channelConf.fulfillSafetyBeforeTimeout.toCltvExpiry(nodeParams.currentBlockHeight) - val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, invoice.paymentHash, lowCltvExpiry, TestConstants.emptyOnionPacket, None, 1.0, None) - sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, FinalPayload.Standard.createPayload(add.amountMsat, 1000 msat, add.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata))) + val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, invoice.paymentHash, lowCltvExpiry, TestConstants.emptyOnionPacket, None, Reputation.maxEndorsement, None) + sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, FinalPayload.Standard.createPayload(add.amountMsat, 1000 msat, add.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata), TimestampMilli.now())) val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message assert(cmd.reason == FailureReason.LocalFailure(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight))) assert(nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).get.status == IncomingPaymentStatus.Pending) @@ -389,8 +390,8 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val invoice = sender.expectMsgType[Bolt11Invoice] assert(invoice.features.hasFeature(BasicMultiPartPayment)) - val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, invoice.paymentHash.reverse, defaultExpiry, TestConstants.emptyOnionPacket, None, 1.0, None) - sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, FinalPayload.Standard.createPayload(add.amountMsat, 1000 msat, add.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata))) + val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, invoice.paymentHash.reverse, defaultExpiry, TestConstants.emptyOnionPacket, None, Reputation.maxEndorsement, None) + sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, FinalPayload.Standard.createPayload(add.amountMsat, 1000 msat, add.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata), TimestampMilli.now())) val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message assert(cmd.reason == FailureReason.LocalFailure(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight))) assert(nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).get.status == IncomingPaymentStatus.Pending) @@ -403,8 +404,8 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val invoice = sender.expectMsgType[Bolt11Invoice] assert(invoice.features.hasFeature(BasicMultiPartPayment)) - val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, invoice.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket, None, 1.0, None) - sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, FinalPayload.Standard.createPayload(add.amountMsat, 999 msat, add.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata))) + val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, invoice.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket, None, Reputation.maxEndorsement, None) + sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, FinalPayload.Standard.createPayload(add.amountMsat, 999 msat, add.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata), TimestampMilli.now())) val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message assert(cmd.reason == FailureReason.LocalFailure(IncorrectOrUnknownPaymentDetails(999 msat, nodeParams.currentBlockHeight))) assert(nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).get.status == IncomingPaymentStatus.Pending) @@ -417,8 +418,8 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val invoice = sender.expectMsgType[Bolt11Invoice] assert(invoice.features.hasFeature(BasicMultiPartPayment)) - val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, invoice.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket, None, 1.0, None) - sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, FinalPayload.Standard.createPayload(add.amountMsat, 2001 msat, add.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata))) + val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, invoice.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket, None, Reputation.maxEndorsement, None) + sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, FinalPayload.Standard.createPayload(add.amountMsat, 2001 msat, add.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata), TimestampMilli.now())) val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message assert(cmd.reason == FailureReason.LocalFailure(IncorrectOrUnknownPaymentDetails(2001 msat, nodeParams.currentBlockHeight))) assert(nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).get.status == IncomingPaymentStatus.Pending) @@ -432,8 +433,8 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike assert(invoice.features.hasFeature(BasicMultiPartPayment)) // Invalid payment secret. - val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, invoice.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket, None, 1.0, None) - sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, FinalPayload.Standard.createPayload(add.amountMsat, 1000 msat, add.cltvExpiry, invoice.paymentSecret.reverse, invoice.paymentMetadata))) + val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, invoice.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket, None, Reputation.maxEndorsement, None) + sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, FinalPayload.Standard.createPayload(add.amountMsat, 1000 msat, add.cltvExpiry, invoice.paymentSecret.reverse, invoice.paymentMetadata), TimestampMilli.now())) val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message assert(cmd.reason == FailureReason.LocalFailure(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight))) assert(nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).get.status == IncomingPaymentStatus.Pending) @@ -464,8 +465,8 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike sender.send(handlerWithRouteBlinding, ReceiveOfferPayment(sender.ref, nodeKey, invoiceReq, createEmptyReceivingRoute(randomBytes32()), randomBytes32())) val invoice = sender.expectMsgType[Bolt12Invoice] - val add = UpdateAddHtlc(ByteVector32.One, 0, 5000 msat, invoice.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket, None, 1.0, None) - sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, FinalPayload.Standard.createPayload(add.amountMsat, add.amountMsat, add.cltvExpiry, randomBytes32(), None))) + val add = UpdateAddHtlc(ByteVector32.One, 0, 5000 msat, invoice.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket, None, Reputation.maxEndorsement, None) + sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, FinalPayload.Standard.createPayload(add.amountMsat, add.amountMsat, add.cltvExpiry, randomBytes32(), None), TimestampMilli.now())) val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message assert(cmd.reason == FailureReason.LocalFailure(IncorrectOrUnknownPaymentDetails(5000 msat, nodeParams.currentBlockHeight))) assert(nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).isEmpty) @@ -545,14 +546,16 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike // Partial payment missing additional parts. f.sender.send(handler, ReceiveStandardPayment(f.sender.ref, Some(1000 msat), Left("1 slow coffee"))) val pr1 = f.sender.expectMsgType[Bolt11Invoice] - val add1 = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, pr1.paymentHash, f.defaultExpiry, TestConstants.emptyOnionPacket, None, 1.0, None) - f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add1, FinalPayload.Standard.createPayload(add1.amountMsat, 1000 msat, add1.cltvExpiry, pr1.paymentSecret, pr1.paymentMetadata))) + val receivedAt1 = TimestampMilli.now() + val add1 = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, pr1.paymentHash, f.defaultExpiry, TestConstants.emptyOnionPacket, None, Reputation.maxEndorsement, None) + f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add1, FinalPayload.Standard.createPayload(add1.amountMsat, 1000 msat, add1.cltvExpiry, pr1.paymentSecret, pr1.paymentMetadata), receivedAt1)) // Partial payment exceeding the invoice amount, but incomplete because it promises to overpay. f.sender.send(handler, ReceiveStandardPayment(f.sender.ref, Some(1500 msat), Left("1 slow latte"))) val pr2 = f.sender.expectMsgType[Bolt11Invoice] - val add2 = UpdateAddHtlc(ByteVector32.One, 1, 1600 msat, pr2.paymentHash, f.defaultExpiry, TestConstants.emptyOnionPacket, None, 1.0, None) - f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add2, FinalPayload.Standard.createPayload(add2.amountMsat, 2000 msat, add2.cltvExpiry, pr2.paymentSecret, pr2.paymentMetadata))) + val receivedAt2 = receivedAt1 + 1.millis + val add2 = UpdateAddHtlc(ByteVector32.One, 1, 1600 msat, pr2.paymentHash, f.defaultExpiry, TestConstants.emptyOnionPacket, None, Reputation.maxEndorsement, None) + f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add2, FinalPayload.Standard.createPayload(add2.amountMsat, 2000 msat, add2.cltvExpiry, pr2.paymentSecret, pr2.paymentMetadata), receivedAt2)) awaitCond { f.sender.send(handler, GetPendingPayments) @@ -561,8 +564,8 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val commands = f.register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]] :: f.register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]] :: Nil assert(commands.toSet == Set( - Register.Forward(null, ByteVector32.One, CMD_FAIL_HTLC(0, FailureReason.LocalFailure(PaymentTimeout()), commit = true)), - Register.Forward(null, ByteVector32.One, CMD_FAIL_HTLC(1, FailureReason.LocalFailure(PaymentTimeout()), commit = true)) + Register.Forward(null, ByteVector32.One, CMD_FAIL_HTLC(0, FailureReason.LocalFailure(PaymentTimeout()), Some(FailureAttributionData(receivedAt1, None)), commit = true)), + Register.Forward(null, ByteVector32.One, CMD_FAIL_HTLC(1, FailureReason.LocalFailure(PaymentTimeout()), Some(FailureAttributionData(receivedAt2, None)), commit = true)) )) awaitCond({ f.sender.send(handler, GetPendingPayments) @@ -570,8 +573,9 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike }) // Extraneous HTLCs should be failed. - f.sender.send(handler, MultiPartPaymentFSM.ExtraPaymentReceived(pr1.paymentHash, HtlcPart(1000 msat, UpdateAddHtlc(ByteVector32.One, 42, 200 msat, pr1.paymentHash, add1.cltvExpiry, add1.onionRoutingPacket, None, 1.0, None)), Some(PaymentTimeout()))) - f.register.expectMsg(Register.Forward(null, ByteVector32.One, CMD_FAIL_HTLC(42, FailureReason.LocalFailure(PaymentTimeout()), commit = true))) + val receivedAt3 = receivedAt1 + 2.millis + f.sender.send(handler, MultiPartPaymentFSM.ExtraPaymentReceived(pr1.paymentHash, HtlcPart(1000 msat, UpdateAddHtlc(ByteVector32.One, 42, 200 msat, pr1.paymentHash, add1.cltvExpiry, add1.onionRoutingPacket, None, Reputation.maxEndorsement, None), receivedAt3), Some(PaymentTimeout()))) + f.register.expectMsg(Register.Forward(null, ByteVector32.One, CMD_FAIL_HTLC(42, FailureReason.LocalFailure(PaymentTimeout()), Some(FailureAttributionData(receivedAt3, None)), commit = true))) // The payment should still be pending in DB. val Some(incomingPayment) = nodeParams.db.payments.getIncomingPayment(pr1.paymentHash) @@ -586,18 +590,21 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike f.sender.send(handler, ReceiveStandardPayment(f.sender.ref, Some(1000 msat), Left("1 fast coffee"), paymentPreimage_opt = Some(preimage))) val invoice = f.sender.expectMsgType[Bolt11Invoice] - val add1 = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, invoice.paymentHash, f.defaultExpiry, TestConstants.emptyOnionPacket, None, 1.0, None) - f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add1, FinalPayload.Standard.createPayload(add1.amountMsat, 1000 msat, add1.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata))) + val receivedAt1 = TimestampMilli.now() + val add1 = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, invoice.paymentHash, f.defaultExpiry, TestConstants.emptyOnionPacket, None, Reputation.maxEndorsement, None) + f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add1, FinalPayload.Standard.createPayload(add1.amountMsat, 1000 msat, add1.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata), receivedAt1)) // Invalid payment secret -> should be rejected. - val add2 = UpdateAddHtlc(ByteVector32.Zeroes, 42, 200 msat, invoice.paymentHash, f.defaultExpiry, TestConstants.emptyOnionPacket, None, 1.0, None) - f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add2, FinalPayload.Standard.createPayload(add2.amountMsat, 1000 msat, add2.cltvExpiry, invoice.paymentSecret.reverse, invoice.paymentMetadata))) + val receivedAt2 = receivedAt1 + 1.millis + val add2 = UpdateAddHtlc(ByteVector32.Zeroes, 42, 200 msat, invoice.paymentHash, f.defaultExpiry, TestConstants.emptyOnionPacket, None, Reputation.maxEndorsement, None) + f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add2, FinalPayload.Standard.createPayload(add2.amountMsat, 1000 msat, add2.cltvExpiry, invoice.paymentSecret.reverse, invoice.paymentMetadata), receivedAt2)) + val receivedAt3 = receivedAt1 + 2.millis val add3 = add2.copy(id = 43) - f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add3, FinalPayload.Standard.createPayload(add3.amountMsat, 1000 msat, add3.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata))) + f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add3, FinalPayload.Standard.createPayload(add3.amountMsat, 1000 msat, add3.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata), receivedAt3)) f.register.expectMsgAllOf( - Register.Forward(null, add2.channelId, CMD_FAIL_HTLC(add2.id, FailureReason.LocalFailure(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight)), commit = true)), - Register.Forward(null, add1.channelId, CMD_FULFILL_HTLC(add1.id, preimage, commit = true)), - Register.Forward(null, add3.channelId, CMD_FULFILL_HTLC(add3.id, preimage, commit = true)) + Register.Forward(null, add2.channelId, CMD_FAIL_HTLC(add2.id, FailureReason.LocalFailure(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight)), Some(FailureAttributionData(receivedAt2, None)), commit = true)), + Register.Forward(null, add1.channelId, CMD_FULFILL_HTLC(add1.id, preimage, Some(FulfillAttributionData(receivedAt1, None, None)), commit = true)), + Register.Forward(null, add3.channelId, CMD_FULFILL_HTLC(add3.id, preimage, Some(FulfillAttributionData(receivedAt3, None, None)), commit = true)) ) val paymentReceived = f.eventListener.expectMsgType[PaymentReceived] @@ -611,8 +618,9 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike }) // Extraneous HTLCs should be fulfilled. - f.sender.send(handler, MultiPartPaymentFSM.ExtraPaymentReceived(invoice.paymentHash, HtlcPart(1000 msat, UpdateAddHtlc(ByteVector32.One, 44, 200 msat, invoice.paymentHash, add1.cltvExpiry, add1.onionRoutingPacket, None, 1.0, None)), None)) - f.register.expectMsg(Register.Forward(null, ByteVector32.One, CMD_FULFILL_HTLC(44, preimage, commit = true))) + val receivedAt4 = receivedAt1 + 3.millis + f.sender.send(handler, MultiPartPaymentFSM.ExtraPaymentReceived(invoice.paymentHash, HtlcPart(1000 msat, UpdateAddHtlc(ByteVector32.One, 44, 200 msat, invoice.paymentHash, add1.cltvExpiry, add1.onionRoutingPacket, None, Reputation.maxEndorsement, None), receivedAt4), None)) + f.register.expectMsg(Register.Forward(null, ByteVector32.One, CMD_FULFILL_HTLC(44, preimage, Some(FulfillAttributionData(receivedAt4, None, None)), commit = true))) assert(f.eventListener.expectMsgType[PaymentReceived].amount == 200.msat) val received2 = nodeParams.db.payments.getIncomingPayment(invoice.paymentHash) assert(received2.get.status.asInstanceOf[IncomingPaymentStatus.Received].amount == 1200.msat) @@ -629,14 +637,16 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike f.sender.send(handler, ReceiveStandardPayment(f.sender.ref, Some(1000 msat), Left("1 coffee with tip please"), paymentPreimage_opt = Some(preimage))) val invoice = f.sender.expectMsgType[Bolt11Invoice] - val add1 = UpdateAddHtlc(randomBytes32(), 0, 1100 msat, invoice.paymentHash, f.defaultExpiry, TestConstants.emptyOnionPacket, None, 1.0, None) - f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add1, FinalPayload.Standard.createPayload(add1.amountMsat, 1500 msat, add1.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata))) - val add2 = UpdateAddHtlc(randomBytes32(), 1, 500 msat, invoice.paymentHash, f.defaultExpiry, TestConstants.emptyOnionPacket, None, 1.0, None) - f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add2, FinalPayload.Standard.createPayload(add2.amountMsat, 1500 msat, add2.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata))) + val add1 = UpdateAddHtlc(randomBytes32(), 0, 1100 msat, invoice.paymentHash, f.defaultExpiry, TestConstants.emptyOnionPacket, None, Reputation.maxEndorsement, None) + val receivedAt1 = TimestampMilli.now() + f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add1, FinalPayload.Standard.createPayload(add1.amountMsat, 1500 msat, add1.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata), receivedAt1)) + val add2 = UpdateAddHtlc(randomBytes32(), 1, 500 msat, invoice.paymentHash, f.defaultExpiry, TestConstants.emptyOnionPacket, None, Reputation.maxEndorsement, None) + val receivedAt2 = TimestampMilli.now() + 5.millis + f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add2, FinalPayload.Standard.createPayload(add2.amountMsat, 1500 msat, add2.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata), receivedAt2)) f.register.expectMsgAllOf( - Register.Forward(null, add1.channelId, CMD_FULFILL_HTLC(add1.id, preimage, commit = true)), - Register.Forward(null, add2.channelId, CMD_FULFILL_HTLC(add2.id, preimage, commit = true)) + Register.Forward(null, add1.channelId, CMD_FULFILL_HTLC(add1.id, preimage, Some(FulfillAttributionData(receivedAt1, None, None)), commit = true)), + Register.Forward(null, add2.channelId, CMD_FULFILL_HTLC(add2.id, preimage, Some(FulfillAttributionData(receivedAt2, None, None)), commit = true)) ) val paymentReceived = f.eventListener.expectMsgType[PaymentReceived] @@ -656,23 +666,26 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike assert(invoice.features.hasFeature(BasicMultiPartPayment)) assert(invoice.paymentHash == Crypto.sha256(preimage)) - val add1 = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, invoice.paymentHash, f.defaultExpiry, TestConstants.emptyOnionPacket, None, 1.0, None) - f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add1, FinalPayload.Standard.createPayload(add1.amountMsat, 1000 msat, add1.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata))) - f.register.expectMsg(Register.Forward(null, ByteVector32.One, CMD_FAIL_HTLC(0, FailureReason.LocalFailure(PaymentTimeout()), commit = true))) + val add1 = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, invoice.paymentHash, f.defaultExpiry, TestConstants.emptyOnionPacket, None, Reputation.maxEndorsement, None) + val receivedAt1 = TimestampMilli.now() + f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add1, FinalPayload.Standard.createPayload(add1.amountMsat, 1000 msat, add1.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata), receivedAt1)) + f.register.expectMsg(Register.Forward(null, ByteVector32.One, CMD_FAIL_HTLC(0, FailureReason.LocalFailure(PaymentTimeout()), Some(FailureAttributionData(receivedAt1, None)), commit = true))) awaitCond({ f.sender.send(handler, GetPendingPayments) f.sender.expectMsgType[PendingPayments].paymentHashes.isEmpty }) - val add2 = UpdateAddHtlc(ByteVector32.One, 2, 300 msat, invoice.paymentHash, f.defaultExpiry, TestConstants.emptyOnionPacket, None, 1.0, None) - f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add2, FinalPayload.Standard.createPayload(add2.amountMsat, 1000 msat, add2.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata))) - val add3 = UpdateAddHtlc(ByteVector32.Zeroes, 5, 700 msat, invoice.paymentHash, f.defaultExpiry, TestConstants.emptyOnionPacket, None, 1.0, None) - f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add3, FinalPayload.Standard.createPayload(add3.amountMsat, 1000 msat, add3.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata))) + val add2 = UpdateAddHtlc(ByteVector32.One, 2, 300 msat, invoice.paymentHash, f.defaultExpiry, TestConstants.emptyOnionPacket, None, Reputation.maxEndorsement, None) + val receivedAt2 = TimestampMilli.now() + 10.millis + f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add2, FinalPayload.Standard.createPayload(add2.amountMsat, 1000 msat, add2.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata), receivedAt2)) + val add3 = UpdateAddHtlc(ByteVector32.Zeroes, 5, 700 msat, invoice.paymentHash, f.defaultExpiry, TestConstants.emptyOnionPacket, None, Reputation.maxEndorsement, None) + val receivedAt3 = TimestampMilli.now() + 50.millis + f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add3, FinalPayload.Standard.createPayload(add3.amountMsat, 1000 msat, add3.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata), receivedAt3)) // the fulfill are not necessarily in the same order as the commands f.register.expectMsgAllOf( - Register.Forward(null, add2.channelId, CMD_FULFILL_HTLC(2, preimage, commit = true)), - Register.Forward(null, add3.channelId, CMD_FULFILL_HTLC(5, preimage, commit = true)) + Register.Forward(null, add2.channelId, CMD_FULFILL_HTLC(2, preimage, Some(FulfillAttributionData(receivedAt2, None, None)), commit = true)), + Register.Forward(null, add3.channelId, CMD_FULFILL_HTLC(5, preimage, Some(FulfillAttributionData(receivedAt3, None, None)), commit = true)) ) val paymentReceived = f.eventListener.expectMsgType[PaymentReceived] @@ -698,8 +711,8 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike assert(nodeParams.db.payments.getIncomingPayment(paymentHash).isEmpty) - val add = UpdateAddHtlc(ByteVector32.One, 0, amountMsat, paymentHash, defaultExpiry, TestConstants.emptyOnionPacket, None, 1.0, None) - sender.send(handlerWithKeySend, IncomingPaymentPacket.FinalPacket(add, payload)) + val add = UpdateAddHtlc(ByteVector32.One, 0, amountMsat, paymentHash, defaultExpiry, TestConstants.emptyOnionPacket, None, Reputation.maxEndorsement, None) + sender.send(handlerWithKeySend, IncomingPaymentPacket.FinalPacket(add, payload, TimestampMilli.now())) register.expectMsgType[Register.Forward[CMD_FULFILL_HTLC]] val paymentReceived = eventListener.expectMsgType[PaymentReceived] @@ -719,8 +732,8 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike assert(nodeParams.db.payments.getIncomingPayment(paymentHash).isEmpty) - val add = UpdateAddHtlc(ByteVector32.One, 0, amountMsat, paymentHash, defaultExpiry, TestConstants.emptyOnionPacket, None, 1.0, None) - sender.send(handlerWithKeySend, IncomingPaymentPacket.FinalPacket(add, payload)) + val add = UpdateAddHtlc(ByteVector32.One, 0, amountMsat, paymentHash, defaultExpiry, TestConstants.emptyOnionPacket, None, Reputation.maxEndorsement, None) + sender.send(handlerWithKeySend, IncomingPaymentPacket.FinalPacket(add, payload, TimestampMilli.now())) register.expectMsgType[Register.Forward[CMD_FULFILL_HTLC]] val paymentReceived = eventListener.expectMsgType[PaymentReceived] @@ -741,10 +754,11 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike assert(nodeParams.db.payments.getIncomingPayment(paymentHash).isEmpty) - val add = UpdateAddHtlc(ByteVector32.One, 0, amountMsat, paymentHash, defaultExpiry, TestConstants.emptyOnionPacket, None, 1.0, None) - sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, payload)) + val add = UpdateAddHtlc(ByteVector32.One, 0, amountMsat, paymentHash, defaultExpiry, TestConstants.emptyOnionPacket, None, Reputation.maxEndorsement, None) + val receivedAt = TimestampMilli.now() + sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, payload, receivedAt)) - f.register.expectMsg(Register.Forward(null, add.channelId, CMD_FAIL_HTLC(add.id, FailureReason.LocalFailure(IncorrectOrUnknownPaymentDetails(42000 msat, nodeParams.currentBlockHeight)), commit = true))) + f.register.expectMsg(Register.Forward(null, add.channelId, CMD_FAIL_HTLC(add.id, FailureReason.LocalFailure(IncorrectOrUnknownPaymentDetails(42000 msat, nodeParams.currentBlockHeight)), Some(FailureAttributionData(receivedAt, None)), commit = true))) assert(nodeParams.db.payments.getIncomingPayment(paymentHash).isEmpty) } @@ -755,8 +769,8 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val paymentSecret = randomBytes32() assert(nodeParams.db.payments.getIncomingPayment(paymentHash).isEmpty) - val add = UpdateAddHtlc(ByteVector32.One, 0, 1000 msat, paymentHash, defaultExpiry, TestConstants.emptyOnionPacket, None, 1.0, None) - sender.send(handlerWithoutMpp, IncomingPaymentPacket.FinalPacket(add, FinalPayload.Standard.createPayload(add.amountMsat, add.amountMsat, add.cltvExpiry, paymentSecret, None))) + val add = UpdateAddHtlc(ByteVector32.One, 0, 1000 msat, paymentHash, defaultExpiry, TestConstants.emptyOnionPacket, None, Reputation.maxEndorsement, None) + sender.send(handlerWithoutMpp, IncomingPaymentPacket.FinalPacket(add, FinalPayload.Standard.createPayload(add.amountMsat, add.amountMsat, add.cltvExpiry, paymentSecret, None), TimestampMilli.now())) val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message assert(cmd.id == add.id) assert(cmd.reason == FailureReason.LocalFailure(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight))) @@ -769,8 +783,8 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val paymentSecret = randomBytes32() assert(nodeParams.db.payments.getIncomingPayment(paymentHash).isEmpty) - val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, paymentHash, defaultExpiry, TestConstants.emptyOnionPacket, None, 1.0, None) - sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, FinalPayload.Standard.createPayload(add.amountMsat, 1000 msat, add.cltvExpiry, paymentSecret, Some(hex"012345")))) + val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, paymentHash, defaultExpiry, TestConstants.emptyOnionPacket, None, Reputation.maxEndorsement, None) + sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, FinalPayload.Standard.createPayload(add.amountMsat, 1000 msat, add.cltvExpiry, paymentSecret, Some(hex"012345")), TimestampMilli.now())) val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message assert(cmd.id == add.id) assert(cmd.reason == FailureReason.LocalFailure(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight))) @@ -783,10 +797,10 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val paymentHash = Crypto.sha256(paymentPreimage) assert(nodeParams.db.payments.getIncomingPayment(paymentHash).isEmpty) - val add = UpdateAddHtlc(ByteVector32.One, 0, 1000 msat, paymentHash, defaultExpiry, TestConstants.emptyOnionPacket, None, 1.0, None) + val add = UpdateAddHtlc(ByteVector32.One, 0, 1000 msat, paymentHash, defaultExpiry, TestConstants.emptyOnionPacket, None, Reputation.maxEndorsement, None) val invoice = Bolt11Invoice(Block.Testnet3GenesisBlock.hash, None, paymentHash, randomKey(), Left("dummy"), CltvExpiryDelta(12)) val incomingPayment = IncomingStandardPayment(invoice, paymentPreimage, PaymentType.Standard, invoice.createdAt.toTimestampMilli, IncomingPaymentStatus.Pending) - val fulfill = DoFulfill(incomingPayment, MultiPartPaymentFSM.MultiPartPaymentSucceeded(paymentHash, Queue(HtlcPart(1000 msat, add)))) + val fulfill = DoFulfill(incomingPayment, MultiPartPaymentFSM.MultiPartPaymentSucceeded(paymentHash, Queue(HtlcPart(1000 msat, add, TimestampMilli.now())))) sender.send(handlerWithoutMpp, fulfill) val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message assert(cmd.id == add.id) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentFSMSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentFSMSpec.scala index dcdd2ef225..8fa6def395 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentFSMSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentFSMSpec.scala @@ -21,9 +21,10 @@ import akka.testkit.{TestActorRef, TestProbe} import fr.acinq.bitcoin.scalacompat.ByteVector32 import fr.acinq.eclair.payment.receive.MultiPartPaymentFSM import fr.acinq.eclair.payment.receive.MultiPartPaymentFSM._ +import fr.acinq.eclair.reputation.Reputation import fr.acinq.eclair.wire.protocol import fr.acinq.eclair.wire.protocol.{IncorrectOrUnknownPaymentDetails, UpdateAddHtlc} -import fr.acinq.eclair.{BlockHeight, CltvExpiry, MilliSatoshi, MilliSatoshiLong, NodeParams, TestConstants, TestKitBaseClass, randomBytes32} +import fr.acinq.eclair.{BlockHeight, CltvExpiry, MilliSatoshi, MilliSatoshiLong, NodeParams, TestConstants, TestKitBaseClass, TimestampMilli, randomBytes32} import org.scalatest.funsuite.AnyFunSuiteLike import scodec.bits.ByteVector @@ -232,8 +233,8 @@ object MultiPartPaymentFSMSpec { def htlcIdToChannelId(htlcId: Long) = ByteVector32(ByteVector.fromLong(htlcId).padLeft(32)) def createMultiPartHtlc(totalAmount: MilliSatoshi, htlcAmount: MilliSatoshi, htlcId: Long): HtlcPart = { - val htlc = UpdateAddHtlc(htlcIdToChannelId(htlcId), htlcId, htlcAmount, paymentHash, CltvExpiry(42), TestConstants.emptyOnionPacket, None, 1.0, None) - HtlcPart(totalAmount, htlc) + val htlc = UpdateAddHtlc(htlcIdToChannelId(htlcId), htlcId, htlcAmount, paymentHash, CltvExpiry(42), TestConstants.emptyOnionPacket, None, Reputation.maxEndorsement, None) + HtlcPart(totalAmount, htlc, TimestampMilli.now()) } } \ No newline at end of file diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentLifecycleSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentLifecycleSpec.scala index 4d791ac85b..ececd0b3af 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentLifecycleSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentLifecycleSpec.scala @@ -32,7 +32,7 @@ import fr.acinq.eclair.payment.send.PaymentInitiator.SendPaymentConfig import fr.acinq.eclair.payment.send.PaymentLifecycle.SendPaymentToRoute import fr.acinq.eclair.payment.send._ import fr.acinq.eclair.router.BaseRouterSpec.{blindedRouteFromHops, channelHopFromUpdate} -import fr.acinq.eclair.router.Graph.PaymentWeightRatios +import fr.acinq.eclair.router.Graph.HeuristicsConstants import fr.acinq.eclair.router.Router._ import fr.acinq.eclair.router.{Announcements, RouteNotFound} import fr.acinq.eclair.wire.protocol._ @@ -66,7 +66,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS override def withFixture(test: OneArgTest): Outcome = { val id = UUID.randomUUID() - val cfg = SendPaymentConfig(id, id, Some("42"), paymentHash, randomKey().publicKey, Upstream.Local(id), None, None, storeInDb = true, publishEvent = true, recordPathFindingMetrics = true, confidence = 1.0) + val cfg = SendPaymentConfig(id, id, Some("42"), paymentHash, randomKey().publicKey, Upstream.Local(id), None, None, storeInDb = true, publishEvent = true, recordPathFindingMetrics = true) val nodeParams = TestConstants.Alice.nodeParams val (childPayFsm, router, sender, eventListener, metricsListener) = (TestProbe(), TestProbe(), TestProbe(), TestProbe(), TestProbe()) val paymentHandler = TestFSMRef(new MultiPartPaymentLifecycle(nodeParams, cfg, publishPreimage = true, router.ref, FakePaymentFactory(childPayFsm))) @@ -408,7 +408,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS val failures = Seq( LocalFailure(finalAmount, Nil, ChannelUnavailable(randomBytes32())), RemoteFailure(finalAmount, Nil, Sphinx.DecryptedFailurePacket(b, FeeInsufficient(100 msat, Some(makeChannelUpdate(ShortChannelId(2), 15 msat, 150, CltvExpiryDelta(48)))))), - UnreadableRemoteFailure(finalAmount, Nil, randomBytes(292)) + UnreadableRemoteFailure(finalAmount, Nil, Sphinx.CannotDecryptFailurePacket(randomBytes(292), None), Nil) ) val extraEdges1 = Seq( ExtraEdge(a, b, ShortChannelId(1), 10 msat, 0, CltvExpiryDelta(12), 1 msat, None), @@ -444,14 +444,14 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS childPayFsm.expectMsgType[SendPaymentToRoute] val (failedId1, failedRoute1) = payFsm.stateData.asInstanceOf[PaymentProgress].pending.head - childPayFsm.send(payFsm, PaymentFailed(failedId1, paymentHash, Seq(UnreadableRemoteFailure(failedRoute1.amount, failedRoute1.hops, randomBytes(292))))) + childPayFsm.send(payFsm, PaymentFailed(failedId1, paymentHash, Seq(UnreadableRemoteFailure(failedRoute1.amount, failedRoute1.hops, Sphinx.CannotDecryptFailurePacket(randomBytes(292), None), Nil)))) router.expectMsgType[RouteRequest] router.send(payFsm, RouteResponse(Seq(Route(500_000 msat, hop_ad :: hop_de :: Nil, None)))) childPayFsm.expectMsgType[SendPaymentToRoute] assert(!payFsm.stateData.asInstanceOf[PaymentProgress].pending.contains(failedId1)) val (failedId2, failedRoute2) = payFsm.stateData.asInstanceOf[PaymentProgress].pending.head - val result = abortAfterFailure(f, PaymentFailed(failedId2, paymentHash, Seq(UnreadableRemoteFailure(failedRoute2.amount, failedRoute2.hops, randomBytes(292))))) + val result = abortAfterFailure(f, PaymentFailed(failedId2, paymentHash, Seq(UnreadableRemoteFailure(failedRoute2.amount, failedRoute2.hops, Sphinx.CannotDecryptFailurePacket(randomBytes(292), None), Nil)))) assert(result.failures.length >= 3) assert(result.failures.contains(LocalFailure(finalAmount, Nil, RetryExhausted))) @@ -539,7 +539,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS childPayFsm.expectMsgType[SendPaymentToRoute] val (failedId1, failedRoute1) :: (failedId2, failedRoute2) :: Nil = payFsm.stateData.asInstanceOf[PaymentProgress].pending.toSeq - childPayFsm.send(payFsm, PaymentFailed(failedId1, paymentHash, Seq(UnreadableRemoteFailure(failedRoute1.amount, failedRoute1.hops, randomBytes(292))))) + childPayFsm.send(payFsm, PaymentFailed(failedId1, paymentHash, Seq(UnreadableRemoteFailure(failedRoute1.amount, failedRoute1.hops, Sphinx.CannotDecryptFailurePacket(randomBytes(292), None), Nil)))) router.expectMsgType[RouteRequest] val result = abortAfterFailure(f, PaymentFailed(failedId2, paymentHash, Seq(RemoteFailure(failedRoute2.amount, failedRoute2.hops, Sphinx.DecryptedFailurePacket(e, PaymentTimeout()))))) @@ -557,7 +557,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS childPayFsm.expectMsgType[SendPaymentToRoute] val (failedId, failedRoute) :: (successId, successRoute) :: Nil = payFsm.stateData.asInstanceOf[PaymentProgress].pending.toSeq - childPayFsm.send(payFsm, PaymentFailed(failedId, paymentHash, Seq(UnreadableRemoteFailure(failedRoute.amount, failedRoute.fullRoute, randomBytes(292))))) + childPayFsm.send(payFsm, PaymentFailed(failedId, paymentHash, Seq(UnreadableRemoteFailure(failedRoute.amount, failedRoute.fullRoute, Sphinx.CannotDecryptFailurePacket(randomBytes(292), None), Nil)))) router.expectMsgType[RouteRequest] val result = fulfillPendingPayments(f, 1, e, finalAmount) @@ -580,8 +580,8 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS awaitCond(payFsm.stateName == PAYMENT_ABORTED) sender.watch(payFsm) - childPayFsm.send(payFsm, PaymentSent(cfg.id, paymentHash, paymentPreimage, finalAmount, e, Seq(PaymentSent.PartialPayment(successId, successRoute.amount, successRoute.channelFee(false), randomBytes32(), Some(successRoute.fullRoute))))) - sender.expectMsg(PreimageReceived(paymentHash, paymentPreimage)) + childPayFsm.send(payFsm, PaymentSent(cfg.id, paymentHash, paymentPreimage, finalAmount, e, Seq(PaymentSent.PartialPayment(successId, successRoute.amount, successRoute.channelFee(false), randomBytes32(), Some(successRoute.fullRoute))), None)) + sender.expectMsg(PreimageReceived(paymentHash, paymentPreimage, None)) val result = sender.expectMsgType[PaymentSent] assert(result.id == cfg.id) assert(result.paymentHash == paymentHash) @@ -608,8 +608,8 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS childPayFsm.expectMsgType[SendPaymentToRoute] val (childId, route) :: (failedId, failedRoute) :: Nil = payFsm.stateData.asInstanceOf[PaymentProgress].pending.toSeq - childPayFsm.send(payFsm, PaymentSent(cfg.id, paymentHash, paymentPreimage, finalAmount, e, Seq(PaymentSent.PartialPayment(childId, route.amount, route.channelFee(false), randomBytes32(), Some(route.fullRoute))))) - sender.expectMsg(PreimageReceived(paymentHash, paymentPreimage)) + childPayFsm.send(payFsm, PaymentSent(cfg.id, paymentHash, paymentPreimage, finalAmount, e, Seq(PaymentSent.PartialPayment(childId, route.amount, route.channelFee(false), randomBytes32(), Some(route.fullRoute))), None)) + sender.expectMsg(PreimageReceived(paymentHash, paymentPreimage, None)) awaitCond(payFsm.stateName == PAYMENT_SUCCEEDED) sender.watch(payFsm) @@ -634,8 +634,8 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS val partialPayments = pending.map { case (childId, route) => PaymentSent.PartialPayment(childId, route.amount, route.channelFee(false) + route.blindedFee, randomBytes32(), Some(route.fullRoute)) } - partialPayments.foreach(pp => childPayFsm.send(payFsm, PaymentSent(cfg.id, paymentHash, paymentPreimage, finalAmount, e, Seq(pp)))) - sender.expectMsg(PreimageReceived(paymentHash, paymentPreimage)) + partialPayments.foreach(pp => childPayFsm.send(payFsm, PaymentSent(cfg.id, paymentHash, paymentPreimage, finalAmount, e, Seq(pp), None))) + sender.expectMsg(PreimageReceived(paymentHash, paymentPreimage, None)) val result = sender.expectMsgType[PaymentSent] assert(result.id == cfg.id) assert(result.paymentHash == paymentHash) @@ -696,8 +696,8 @@ object MultiPartPaymentLifecycleSpec { 0.00, 6, CltvExpiryDelta(1008)), - PaymentWeightRatios(1, 0, 0, 0, RelayFees(0 msat, 0)), - MultiPartParams(1000 msat, 5), + HeuristicsConstants(0, RelayFees(0 msat, 0), RelayFees(0 msat, 0), useLogProbability = false, usePastRelaysData = false), + MultiPartParams(1000 msat, 5, MultiPartParams.FullCapacity), experimentName = "my-test-experiment", experimentPercentage = 100 ).getDefaultRouteParams diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentInitiatorSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentInitiatorSpec.scala index 79011ef984..d81b19a2da 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentInitiatorSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentInitiatorSpec.scala @@ -175,7 +175,7 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val request = SendPaymentToRoute(finalAmount, invoice, Nil, route, None, None) sender.send(initiator, request) val payment = sender.expectMsgType[SendPaymentToRouteResponse] - payFsm.expectMsg(SendPaymentConfig(payment.paymentId, payment.parentId, None, paymentHash, c, Upstream.Local(payment.paymentId), Some(invoice), None, storeInDb = true, publishEvent = true, recordPathFindingMetrics = false, confidence = 1.0)) + payFsm.expectMsg(SendPaymentConfig(payment.paymentId, payment.parentId, None, paymentHash, c, Upstream.Local(payment.paymentId), Some(invoice), None, storeInDb = true, publishEvent = true, recordPathFindingMetrics = false)) payFsm.expectMsg(PaymentLifecycle.SendPaymentToRoute(initiator, Left(route), ClearRecipient(invoice, finalAmount, finalExpiryDelta.toCltvExpiry(nodeParams.currentBlockHeight + 1), Set.empty))) sender.send(initiator, GetPayment(PaymentIdentifier.PaymentUUID(payment.paymentId))) @@ -200,7 +200,7 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike assert(req.finalExpiry(nodeParams) == (finalExpiryDelta + 1).toCltvExpiry(nodeParams.currentBlockHeight)) sender.send(initiator, req) val id = sender.expectMsgType[UUID] - payFsm.expectMsg(SendPaymentConfig(id, id, None, paymentHash, c, Upstream.Local(id), Some(invoice), None, storeInDb = true, publishEvent = true, recordPathFindingMetrics = true, confidence = 1.0)) + payFsm.expectMsg(SendPaymentConfig(id, id, None, paymentHash, c, Upstream.Local(id), Some(invoice), None, storeInDb = true, publishEvent = true, recordPathFindingMetrics = true)) payFsm.expectMsg(PaymentLifecycle.SendPaymentToNode(initiator, ClearRecipient(invoice, finalAmount, req.finalExpiry(nodeParams), Set.empty), 1, nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams)) sender.send(initiator, GetPayment(PaymentIdentifier.PaymentUUID(id))) @@ -223,7 +223,7 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val req = SendPaymentToNode(sender.ref, finalAmount + 100.msat, invoice, Nil, 1, routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams) sender.send(initiator, req) val id = sender.expectMsgType[UUID] - multiPartPayFsm.expectMsg(SendPaymentConfig(id, id, None, paymentHash, c, Upstream.Local(id), Some(invoice), None, storeInDb = true, publishEvent = true, recordPathFindingMetrics = true, confidence = 1.0)) + multiPartPayFsm.expectMsg(SendPaymentConfig(id, id, None, paymentHash, c, Upstream.Local(id), Some(invoice), None, storeInDb = true, publishEvent = true, recordPathFindingMetrics = true)) multiPartPayFsm.expectMsg(SendMultiPartPayment(initiator, ClearRecipient(invoice, finalAmount + 100.msat, req.finalExpiry(nodeParams), Set.empty), 1, nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams)) sender.send(initiator, GetPayment(PaymentIdentifier.PaymentUUID(id))) @@ -231,7 +231,7 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike sender.send(initiator, GetPayment(PaymentIdentifier.PaymentHash(invoice.paymentHash))) sender.expectMsg(PaymentIsPending(id, invoice.paymentHash, PendingPaymentToNode(sender.ref, req))) - val ps = PaymentSent(id, invoice.paymentHash, randomBytes32(), finalAmount, priv_c.publicKey, Seq(PartialPayment(UUID.randomUUID(), finalAmount, 0 msat, randomBytes32(), None))) + val ps = PaymentSent(id, invoice.paymentHash, randomBytes32(), finalAmount, priv_c.publicKey, Seq(PartialPayment(UUID.randomUUID(), finalAmount, 0 msat, randomBytes32(), None)), None) payFsm.send(initiator, ps) sender.expectMsg(ps) eventListener.expectNoMessage(100 millis) @@ -247,7 +247,7 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val req = SendPaymentToNode(sender.ref, finalAmount, invoice, Nil, 1, routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams) sender.send(initiator, req) val id = sender.expectMsgType[UUID] - multiPartPayFsm.expectMsg(SendPaymentConfig(id, id, None, paymentHash, c, Upstream.Local(id), Some(invoice), None, storeInDb = true, publishEvent = true, recordPathFindingMetrics = true, confidence = 1.0)) + multiPartPayFsm.expectMsg(SendPaymentConfig(id, id, None, paymentHash, c, Upstream.Local(id), Some(invoice), None, storeInDb = true, publishEvent = true, recordPathFindingMetrics = true)) val payment = multiPartPayFsm.expectMsgType[SendMultiPartPayment] val expiry = payment.recipient.asInstanceOf[ClearRecipient].expiry assert(nodeParams.currentBlockHeight + invoiceFinalExpiryDelta.toInt + 50 <= expiry.blockHeight) @@ -261,7 +261,7 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val req = SendPaymentToRoute(finalAmount, invoice, Nil, route, None, None) sender.send(initiator, req) val payment = sender.expectMsgType[SendPaymentToRouteResponse] - payFsm.expectMsg(SendPaymentConfig(payment.paymentId, payment.parentId, None, paymentHash, c, Upstream.Local(payment.paymentId), Some(invoice), None, storeInDb = true, publishEvent = true, recordPathFindingMetrics = false, confidence = 1.0)) + payFsm.expectMsg(SendPaymentConfig(payment.paymentId, payment.parentId, None, paymentHash, c, Upstream.Local(payment.paymentId), Some(invoice), None, storeInDb = true, publishEvent = true, recordPathFindingMetrics = false)) val msg = payFsm.expectMsgType[PaymentLifecycle.SendPaymentToRoute] assert(msg.replyTo == initiator) assert(msg.route == Left(route)) @@ -288,7 +288,7 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val offer = Offer(None, Some("Bolt12 r0cks"), e, features, Block.RegtestGenesisBlock.hash) val invoiceRequest = InvoiceRequest(offer, finalAmount, 1, features, payerKey, Block.RegtestGenesisBlock.hash) val blindedRoute = BlindedRouteCreation.createBlindedRouteFromHops(Nil, e, hex"2a2a2a2a", 1 msat, CltvExpiry(500_000)).route - val paymentInfo = OfferTypes.PaymentInfo(1_000 msat, 0, CltvExpiryDelta(24), 0 msat, finalAmount, Features.empty) + val paymentInfo = OfferTypes.PaymentInfo(1_000 msat, 0, CltvExpiryDelta(24), 0 msat, finalAmount, ByteVector.empty) Bolt12Invoice(invoiceRequest, paymentPreimage, priv_e.privateKey, 300 seconds, features, Seq(PaymentBlindedRoute(blindedRoute, paymentInfo))) } @@ -303,7 +303,7 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val req = SendPaymentToNode(sender.ref, finalAmount, invoice, resolvedPaths, 1, routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams, payerKey_opt = Some(payerKey)) sender.send(initiator, req) val id = sender.expectMsgType[UUID] - payFsm.expectMsg(SendPaymentConfig(id, id, None, paymentHash, invoice.nodeId, Upstream.Local(id), Some(invoice), Some(payerKey), storeInDb = true, publishEvent = true, recordPathFindingMetrics = true, confidence = 1.0)) + payFsm.expectMsg(SendPaymentConfig(id, id, None, paymentHash, invoice.nodeId, Upstream.Local(id), Some(invoice), Some(payerKey), storeInDb = true, publishEvent = true, recordPathFindingMetrics = true)) val payment = payFsm.expectMsgType[PaymentLifecycle.SendPaymentToNode] assert(payment.amount == finalAmount) assert(payment.recipient.nodeId == invoice.nodeId) @@ -337,7 +337,7 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val req = SendPaymentToNode(sender.ref, finalAmount, invoice, resolvedPaths, 1, routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams, payerKey_opt = Some(payerKey)) sender.send(initiator, req) val id = sender.expectMsgType[UUID] - multiPartPayFsm.expectMsg(SendPaymentConfig(id, id, None, paymentHash, invoice.nodeId, Upstream.Local(id), Some(invoice), Some(payerKey), storeInDb = true, publishEvent = true, recordPathFindingMetrics = true, confidence = 1.0)) + multiPartPayFsm.expectMsg(SendPaymentConfig(id, id, None, paymentHash, invoice.nodeId, Upstream.Local(id), Some(invoice), Some(payerKey), storeInDb = true, publishEvent = true, recordPathFindingMetrics = true)) val payment = multiPartPayFsm.expectMsgType[SendMultiPartPayment] assert(payment.recipient.nodeId == invoice.nodeId) assert(payment.recipient.totalAmount == finalAmount) @@ -350,7 +350,7 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike sender.send(initiator, GetPayment(PaymentIdentifier.PaymentHash(invoice.paymentHash))) sender.expectMsg(PaymentIsPending(id, invoice.paymentHash, PendingPaymentToNode(sender.ref, req))) - val ps = PaymentSent(id, invoice.paymentHash, paymentPreimage, finalAmount, invoice.nodeId, Seq(PartialPayment(UUID.randomUUID(), finalAmount, 0 msat, randomBytes32(), None))) + val ps = PaymentSent(id, invoice.paymentHash, paymentPreimage, finalAmount, invoice.nodeId, Seq(PartialPayment(UUID.randomUUID(), finalAmount, 0 msat, randomBytes32(), None)), None) payFsm.send(initiator, ps) sender.expectMsg(ps) eventListener.expectNoMessage(100 millis) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala index b7d1c4ad20..101c8bc12f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala @@ -18,6 +18,7 @@ package fr.acinq.eclair.payment import akka.actor.ActorRef import akka.actor.FSM.{CurrentState, SubscribeTransitionCallBack, Transition} +import akka.actor.typed.scaladsl.Behaviors import akka.actor.typed.scaladsl.adapter._ import akka.testkit.{TestFSMRef, TestProbe} import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey @@ -28,7 +29,8 @@ import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.{UtxoStatus, ValidateReque import fr.acinq.eclair.channel.Register.{ForwardShortId, ForwardShortIdFailure} import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.fsm.Channel -import fr.acinq.eclair.crypto.Sphinx +import fr.acinq.eclair.crypto.Sphinx.HoldTime +import fr.acinq.eclair.crypto.{Sphinx, SphinxSpec} import fr.acinq.eclair.db.{OutgoingPayment, OutgoingPaymentStatus, PaymentType} import fr.acinq.eclair.io.Peer.PeerRoutingMessage import fr.acinq.eclair.payment.Invoice.ExtraEdge @@ -37,9 +39,10 @@ import fr.acinq.eclair.payment.relay.Relayer.RelayFees import fr.acinq.eclair.payment.send.PaymentInitiator.SendPaymentConfig import fr.acinq.eclair.payment.send.PaymentLifecycle._ import fr.acinq.eclair.payment.send.{ClearRecipient, PaymentLifecycle, Recipient} +import fr.acinq.eclair.reputation.{Reputation, ReputationRecorder} import fr.acinq.eclair.router.Announcements.makeChannelUpdate import fr.acinq.eclair.router.BaseRouterSpec.{blindedRouteFromHops, channelAnnouncement, channelHopFromUpdate} -import fr.acinq.eclair.router.Graph.PaymentWeightRatios +import fr.acinq.eclair.router.Graph.HeuristicsConstants import fr.acinq.eclair.router.Router._ import fr.acinq.eclair.router._ import fr.acinq.eclair.transactions.Scripts @@ -89,9 +92,14 @@ class PaymentLifecycleSpec extends BaseRouterSpec { def createPaymentLifecycle(invoice: Invoice, storeInDb: Boolean = true, publishEvent: Boolean = true, recordMetrics: Boolean = true): PaymentFixture = { val (id, parentId) = (UUID.randomUUID(), UUID.randomUUID()) val nodeParams = TestConstants.Alice.nodeParams.copy(nodeKeyManager = testNodeKeyManager, channelKeyManager = testChannelKeyManager) - val cfg = SendPaymentConfig(id, parentId, Some(defaultExternalId), defaultPaymentHash, invoice.nodeId, Upstream.Local(id), Some(invoice), None, storeInDb, publishEvent, recordMetrics, confidence = 1.0) + val cfg = SendPaymentConfig(id, parentId, Some(defaultExternalId), defaultPaymentHash, invoice.nodeId, Upstream.Local(id), Some(invoice), None, storeInDb, publishEvent, recordMetrics) val (routerForwarder, register, sender, monitor, eventListener, metricsListener) = (TestProbe(), TestProbe(), TestProbe(), TestProbe(), TestProbe(), TestProbe()) - val paymentFSM = TestFSMRef(new PaymentLifecycle(nodeParams, cfg, routerForwarder.ref, register.ref)) + val reputationRecorder = system.spawnAnonymous(Behaviors.receiveMessage[ReputationRecorder.GetConfidence](getConfidence => { + assert(getConfidence.upstream.isInstanceOf[Upstream.Local]) + getConfidence.replyTo ! Reputation.Score.max + Behaviors.same + })) + val paymentFSM = TestFSMRef(new PaymentLifecycle(nodeParams, cfg, routerForwarder.ref, register.ref, Some(reputationRecorder))) paymentFSM ! SubscribeTransitionCallBack(monitor.ref) val CurrentState(_, WAITING_FOR_REQUEST) = monitor.expectMsgClass(classOf[CurrentState[_]]) system.eventStream.subscribe(eventListener.ref, classOf[PaymentEvent]) @@ -102,7 +110,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { def addCompleted(result: HtlcResult) = { RES_ADD_SETTLED( origin = defaultOrigin, - htlc = UpdateAddHtlc(ByteVector32.Zeroes, 0, defaultAmountMsat, defaultPaymentHash, defaultExpiry, TestConstants.emptyOnionPacket, None, 1.0, None), + htlc = UpdateAddHtlc(ByteVector32.Zeroes, 0, defaultAmountMsat, defaultPaymentHash, defaultExpiry, TestConstants.emptyOnionPacket, None, Reputation.maxEndorsement, None), result) } @@ -118,7 +126,8 @@ class PaymentLifecycleSpec extends BaseRouterSpec { routerForwarder.expectNoMessage(100 millis) // we don't need the router, we have the pre-computed route val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]]) - val Transition(_, WAITING_FOR_ROUTE, WAITING_FOR_PAYMENT_COMPLETE) = monitor.expectMsgClass(classOf[Transition[_]]) + val Transition(_, WAITING_FOR_ROUTE, WAITING_FOR_CONFIDENCE) = monitor.expectMsgClass(classOf[Transition[_]]) + val Transition(_, WAITING_FOR_CONFIDENCE, WAITING_FOR_PAYMENT_COMPLETE) = monitor.expectMsgClass(classOf[Transition[_]]) awaitCond(nodeParams.db.payments.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.Pending)) val Some(outgoing) = nodeParams.db.payments.getOutgoingPayment(id) assert(outgoing.copy(createdAt = 0 unixms) == OutgoingPayment(id, parentId, Some(defaultExternalId), defaultPaymentHash, PaymentType.Standard, defaultAmountMsat, defaultAmountMsat, d, 0 unixms, Some(defaultInvoice), None, OutgoingPaymentStatus.Pending)) @@ -150,7 +159,8 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]]) routerForwarder.forward(routerFixture.router) - val Transition(_, WAITING_FOR_ROUTE, WAITING_FOR_PAYMENT_COMPLETE) = monitor.expectMsgClass(classOf[Transition[_]]) + val Transition(_, WAITING_FOR_ROUTE, WAITING_FOR_CONFIDENCE) = monitor.expectMsgClass(classOf[Transition[_]]) + val Transition(_, WAITING_FOR_CONFIDENCE, WAITING_FOR_PAYMENT_COMPLETE) = monitor.expectMsgClass(classOf[Transition[_]]) awaitCond(nodeParams.db.payments.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.Pending)) val Some(outgoing) = nodeParams.db.payments.getOutgoingPayment(id) assert(outgoing.copy(createdAt = 0 unixms) == OutgoingPayment(id, parentId, Some(defaultExternalId), defaultPaymentHash, PaymentType.Standard, defaultAmountMsat, defaultAmountMsat, d, 0 unixms, Some(defaultInvoice), None, OutgoingPaymentStatus.Pending)) @@ -218,7 +228,8 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]]) routerForwarder.forward(routerFixture.router) - val Transition(_, WAITING_FOR_ROUTE, WAITING_FOR_PAYMENT_COMPLETE) = monitor.expectMsgClass(classOf[Transition[_]]) + val Transition(_, WAITING_FOR_ROUTE, WAITING_FOR_CONFIDENCE) = monitor.expectMsgClass(classOf[Transition[_]]) + val Transition(_, WAITING_FOR_CONFIDENCE, WAITING_FOR_PAYMENT_COMPLETE) = monitor.expectMsgClass(classOf[Transition[_]]) // Payment accepted by the recipient. sender.send(paymentFSM, addCompleted(HtlcResult.OnChainFulfill(defaultPaymentPreimage))) @@ -290,8 +301,8 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val routeParams = PathFindingConf( randomize = false, boundaries = SearchBoundaries(100 msat, 0.0, 20, CltvExpiryDelta(2016)), - PaymentWeightRatios(1, 0, 0, 0, RelayFees(0 msat, 0)), - MultiPartParams(10_000 msat, 5), + HeuristicsConstants(0, RelayFees(0 msat, 0), RelayFees(0 msat, 0), useLogProbability = false, usePastRelaysData = false), + MultiPartParams(10_000 msat, 5, MultiPartParams.FullCapacity), "my-test-experiment", experimentPercentage = 100 ).getDefaultRouteParams @@ -512,7 +523,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { register.expectMsg(ForwardShortId(paymentFSM.toTyped, scid_ab, cmd1)) val failure = TemporaryChannelFailure(Some(update_bc)) - sender.send(paymentFSM, addCompleted(HtlcResult.RemoteFail(UpdateFailHtlc(ByteVector32.Zeroes, 0, Sphinx.FailurePacket.create(sharedSecrets1.head.secret, failure))))) + sender.send(paymentFSM, addCompleted(HtlcResult.RemoteFail(UpdateFailHtlc(ByteVector32.Zeroes, 0, SphinxSpec.createAndWrap(sharedSecrets1.head.secret, failure))))) // payment lifecycle will ask the router to temporarily exclude this channel from its route calculations assert(routerForwarder.expectMsgType[ChannelCouldNotRelay].hop.shortChannelId == update_bc.shortChannelId) @@ -545,7 +556,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val channelUpdate_bc_modified = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_b, c, scid_bc, CltvExpiryDelta(42), htlcMinimumMsat = update_bc.htlcMinimumMsat, feeBaseMsat = update_bc.feeBaseMsat, feeProportionalMillionths = update_bc.feeProportionalMillionths, htlcMaximumMsat = update_bc.htlcMaximumMsat) val failure = IncorrectCltvExpiry(CltvExpiry(5), Some(channelUpdate_bc_modified)) // and node replies with a failure containing a new channel update - sender.send(paymentFSM, addCompleted(HtlcResult.RemoteFail(UpdateFailHtlc(ByteVector32.Zeroes, 0, Sphinx.FailurePacket.create(sharedSecrets1.head.secret, failure))))) + sender.send(paymentFSM, addCompleted(HtlcResult.RemoteFail(UpdateFailHtlc(ByteVector32.Zeroes, 0, SphinxSpec.createAndWrap(sharedSecrets1.head.secret, failure))))) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.Pending)) // 1 failure but not final, the payment is still PENDING expectRouteRequest(routerForwarder, a, cfg) @@ -560,7 +571,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val channelUpdate_bc_modified_2 = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_b, c, scid_bc, CltvExpiryDelta(43), htlcMinimumMsat = update_bc.htlcMinimumMsat, feeBaseMsat = update_bc.feeBaseMsat, feeProportionalMillionths = update_bc.feeProportionalMillionths, htlcMaximumMsat = update_bc.htlcMaximumMsat) val failure2 = IncorrectCltvExpiry(CltvExpiry(5), Some(channelUpdate_bc_modified_2)) // and node replies with a failure containing a new channel update - sender.send(paymentFSM, addCompleted(HtlcResult.RemoteFail(UpdateFailHtlc(ByteVector32.Zeroes, 0, Sphinx.FailurePacket.create(sharedSecrets2.head.secret, failure2))))) + sender.send(paymentFSM, addCompleted(HtlcResult.RemoteFail(UpdateFailHtlc(ByteVector32.Zeroes, 0, SphinxSpec.createAndWrap(sharedSecrets2.head.secret, failure2))))) // this time the payment lifecycle will ask the router to temporarily exclude this channel from its route calculations routerForwarder.expectMsg(ExcludeChannel(ChannelDesc(update_bc.shortChannelId, b, c), Some(nodeParams.routerConf.channelExcludeDuration))) @@ -590,7 +601,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { // the node replies with a temporary failure containing the same update as the one we already have (likely a balance issue) val failure = TemporaryChannelFailure(Some(update_bc)) - sender.send(paymentFSM, addCompleted(HtlcResult.RemoteFail(UpdateFailHtlc(ByteVector32.Zeroes, 0, Sphinx.FailurePacket.create(sharedSecrets1.head.secret, failure))))) + sender.send(paymentFSM, addCompleted(HtlcResult.RemoteFail(UpdateFailHtlc(ByteVector32.Zeroes, 0, SphinxSpec.createAndWrap(sharedSecrets1.head.secret, failure))))) // we should temporarily exclude that channel assert(routerForwarder.expectMsgType[ChannelCouldNotRelay].hop.shortChannelId == update_bc.shortChannelId) routerForwarder.expectMsg(ExcludeChannel(ChannelDesc(update_bc.shortChannelId, b, c), Some(nodeParams.routerConf.channelExcludeDuration))) @@ -624,7 +635,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val channelUpdate_bc_modified = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_b, c, scid_bc, CltvExpiryDelta(42), htlcMinimumMsat = update_bc.htlcMinimumMsat, feeBaseMsat = update_bc.feeBaseMsat, feeProportionalMillionths = update_bc.feeProportionalMillionths, htlcMaximumMsat = update_bc.htlcMaximumMsat) val failure = IncorrectCltvExpiry(CltvExpiry(5), Some(channelUpdate_bc_modified)) // and node replies with a failure containing a new channel update - sender.send(paymentFSM, addCompleted(HtlcResult.RemoteFail(UpdateFailHtlc(ByteVector32.Zeroes, 0, Sphinx.FailurePacket.create(sharedSecrets1.head.secret, failure))))) + sender.send(paymentFSM, addCompleted(HtlcResult.RemoteFail(UpdateFailHtlc(ByteVector32.Zeroes, 0, SphinxSpec.createAndWrap(sharedSecrets1.head.secret, failure))))) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.Pending)) // 1 failure but not final, the payment is still PENDING val extraEdges1 = Seq( @@ -663,7 +674,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { // we disable the channel val channelUpdate_cd_disabled = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_c, d, scid_cd, CltvExpiryDelta(42), update_cd.htlcMinimumMsat, update_cd.feeBaseMsat, update_cd.feeProportionalMillionths, update_cd.htlcMaximumMsat, enable = false) val failure = ChannelDisabled(channelUpdate_cd_disabled.messageFlags, channelUpdate_cd_disabled.channelFlags, Some(channelUpdate_cd_disabled)) - val failureOnion = Sphinx.FailurePacket.wrap(Sphinx.FailurePacket.create(sharedSecrets1(1).secret, failure), sharedSecrets1.head.secret) + val failureOnion = Sphinx.FailurePacket.wrap(SphinxSpec.createAndWrap(sharedSecrets1(1).secret, failure), sharedSecrets1.head.secret) sender.send(paymentFSM, addCompleted(HtlcResult.RemoteFail(UpdateFailHtlc(ByteVector32.Zeroes, 0, failureOnion)))) assert(routerForwarder.expectMsgType[RouteCouldRelay].route.hops.map(_.shortChannelId) == Seq(update_ab, update_bc).map(_.shortChannelId)) @@ -686,7 +697,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val WaitingForComplete(_, cmd1, Nil, sharedSecrets1, _, route1) = paymentFSM.stateData register.expectMsg(ForwardShortId(paymentFSM.toTyped, scid_ab, cmd1)) - sender.send(paymentFSM, addCompleted(HtlcResult.RemoteFail(UpdateFailHtlc(ByteVector32.Zeroes, 0, Sphinx.FailurePacket.create(sharedSecrets1.head.secret, failure))))) + sender.send(paymentFSM, addCompleted(HtlcResult.RemoteFail(UpdateFailHtlc(ByteVector32.Zeroes, 0, SphinxSpec.createAndWrap(sharedSecrets1.head.secret, failure))))) // payment lifecycle forwards the embedded channelUpdate to the router awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE) @@ -725,7 +736,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { // The payment fails inside the blinded route: the introduction node sends back an error. val failure = InvalidOnionBlinding(randomBytes32()) - val failureOnion = Sphinx.FailurePacket.create(sharedSecrets.head.secret, failure) + val failureOnion = SphinxSpec.createAndWrap(sharedSecrets.head.secret, failure) sender.send(paymentFSM, addCompleted(HtlcResult.RemoteFail(UpdateFailHtlc(ByteVector32.Zeroes, 0, failureOnion)))) // We retry but we exclude the failed blinded route. @@ -750,7 +761,8 @@ class PaymentLifecycleSpec extends BaseRouterSpec { routerForwarder.expectMsgType[RouteRequest] val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]]) routerForwarder.forward(routerFixture.router) - val Transition(_, WAITING_FOR_ROUTE, WAITING_FOR_PAYMENT_COMPLETE) = monitor.expectMsgClass(classOf[Transition[_]]) + val Transition(_, WAITING_FOR_ROUTE, WAITING_FOR_CONFIDENCE) = monitor.expectMsgClass(classOf[Transition[_]]) + val Transition(_, WAITING_FOR_CONFIDENCE, WAITING_FOR_PAYMENT_COMPLETE) = monitor.expectMsgClass(classOf[Transition[_]]) awaitCond(nodeParams.db.payments.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.Pending)) val Some(outgoing) = nodeParams.db.payments.getOutgoingPayment(id) assert(outgoing.copy(createdAt = 0 unixms) == OutgoingPayment(id, parentId, Some(defaultExternalId), defaultPaymentHash, PaymentType.Standard, defaultAmountMsat, defaultAmountMsat, d, 0 unixms, Some(defaultInvoice), None, OutgoingPaymentStatus.Pending)) @@ -808,11 +820,12 @@ class PaymentLifecycleSpec extends BaseRouterSpec { // the route will be A -> B -> H where B -> H has a channel_update with fees=0 val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]]) routerForwarder.forward(router) - val Transition(_, WAITING_FOR_ROUTE, WAITING_FOR_PAYMENT_COMPLETE) = monitor.expectMsgClass(classOf[Transition[_]]) + val Transition(_, WAITING_FOR_ROUTE, WAITING_FOR_CONFIDENCE) = monitor.expectMsgClass(classOf[Transition[_]]) + val Transition(_, WAITING_FOR_CONFIDENCE, WAITING_FOR_PAYMENT_COMPLETE) = monitor.expectMsgClass(classOf[Transition[_]]) sender.send(paymentFSM, addCompleted(HtlcResult.OnChainFulfill(defaultPaymentPreimage))) val paymentOK = sender.expectMsgType[PaymentSent] - val PaymentSent(_, _, paymentOK.paymentPreimage, finalAmount, _, PartialPayment(_, partAmount, fee, ByteVector32.Zeroes, _, _) :: Nil) = eventListener.expectMsgType[PaymentSent] + val PaymentSent(_, _, paymentOK.paymentPreimage, finalAmount, _, PartialPayment(_, partAmount, fee, ByteVector32.Zeroes, _, _) :: Nil, _) = eventListener.expectMsgType[PaymentSent] assert(partAmount == request.amount) assert(finalAmount == defaultAmountMsat) @@ -900,12 +913,13 @@ class PaymentLifecycleSpec extends BaseRouterSpec { (RemoteFailure(defaultAmountMsat, route_abcd, Sphinx.DecryptedFailurePacket(b, FeeInsufficient(100 msat, Some(update_bc)))), Set.empty, Set.empty), (RemoteFailure(defaultAmountMsat, blindedRoute_abc, Sphinx.DecryptedFailurePacket(b, InvalidOnionBlinding(randomBytes32()))), Set.empty, Set(ChannelDesc(blindedHop_bc.dummyId, blindedHop_bc.nodeId, blindedHop_bc.nextNodeId))), (RemoteFailure(defaultAmountMsat, blindedRoute_abc, Sphinx.DecryptedFailurePacket(blindedHop_bc.resolved.route.blindedNodeIds(1), InvalidOnionBlinding(randomBytes32()))), Set.empty, Set(ChannelDesc(blindedHop_bc.dummyId, blindedHop_bc.nodeId, blindedHop_bc.nextNodeId))), - // unreadable remote failures -> blacklist all nodes except our direct peer, the final recipient or the last hop - (UnreadableRemoteFailure(defaultAmountMsat, channelHopFromUpdate(a, b, update_ab) :: Nil, ByteVector.empty), Set.empty, Set.empty), - (UnreadableRemoteFailure(defaultAmountMsat, channelHopFromUpdate(a, b, update_ab) :: channelHopFromUpdate(b, c, update_bc) :: channelHopFromUpdate(c, d, update_cd) :: Nil, ByteVector.empty), Set(c), Set.empty), - (UnreadableRemoteFailure(defaultAmountMsat, channelHopFromUpdate(a, b, update_ab) :: channelHopFromUpdate(b, c, update_bc) :: channelHopFromUpdate(c, d, update_cd) :: channelHopFromUpdate(d, e, update_de) :: Nil, ByteVector.empty), Set(c, d), Set.empty), - (UnreadableRemoteFailure(defaultAmountMsat, channelHopFromUpdate(a, b, update_ab) :: channelHopFromUpdate(b, c, update_bc) :: channelHopFromUpdate(c, d, update_cd) :: NodeHop(d, e, CltvExpiryDelta(24), 0 msat) :: Nil, ByteVector.empty), Set(c), Set.empty), - (UnreadableRemoteFailure(defaultAmountMsat, channelHopFromUpdate(a, b, update_ab) :: channelHopFromUpdate(b, c, update_bc) :: channelHopFromUpdate(c, d, update_cd) :: blindedHop_de :: Nil, ByteVector.empty), Set(c), Set.empty), + // unreadable remote failures -> blacklist all nodes except our direct peer, the final recipient, the last hop or nodes relaying attribution data + (UnreadableRemoteFailure(defaultAmountMsat, channelHopFromUpdate(a, b, update_ab) :: Nil, Sphinx.CannotDecryptFailurePacket(ByteVector.empty, None), Nil), Set.empty, Set.empty), + (UnreadableRemoteFailure(defaultAmountMsat, channelHopFromUpdate(a, b, update_ab) :: channelHopFromUpdate(b, c, update_bc) :: channelHopFromUpdate(c, d, update_cd) :: Nil, Sphinx.CannotDecryptFailurePacket(ByteVector.empty, None), Nil), Set(c), Set.empty), + (UnreadableRemoteFailure(defaultAmountMsat, channelHopFromUpdate(a, b, update_ab) :: channelHopFromUpdate(b, c, update_bc) :: channelHopFromUpdate(c, d, update_cd) :: channelHopFromUpdate(d, e, update_de) :: Nil, Sphinx.CannotDecryptFailurePacket(ByteVector.empty, None), Nil), Set(c, d), Set.empty), + (UnreadableRemoteFailure(defaultAmountMsat, channelHopFromUpdate(a, b, update_ab) :: channelHopFromUpdate(b, c, update_bc) :: channelHopFromUpdate(c, d, update_cd) :: channelHopFromUpdate(d, e, update_de) :: Nil, Sphinx.CannotDecryptFailurePacket(ByteVector.empty, None), Seq(HoldTime(100 millis, b), HoldTime(90 millis, c), HoldTime(80 millis, d))), Set(d), Set.empty), + (UnreadableRemoteFailure(defaultAmountMsat, channelHopFromUpdate(a, b, update_ab) :: channelHopFromUpdate(b, c, update_bc) :: channelHopFromUpdate(c, d, update_cd) :: NodeHop(d, e, CltvExpiryDelta(24), 0 msat) :: Nil, Sphinx.CannotDecryptFailurePacket(ByteVector.empty, None), Nil), Set(c), Set.empty), + (UnreadableRemoteFailure(defaultAmountMsat, channelHopFromUpdate(a, b, update_ab) :: channelHopFromUpdate(b, c, update_bc) :: channelHopFromUpdate(c, d, update_cd) :: blindedHop_de :: Nil, Sphinx.CannotDecryptFailurePacket(ByteVector.empty, None), Nil), Set(c), Set.empty), ) for ((failure, expectedNodes, expectedChannels) <- testCases) { @@ -934,7 +948,8 @@ class PaymentLifecycleSpec extends BaseRouterSpec { routerForwarder.expectMsgType[RouteRequest] val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]]) routerForwarder.forward(routerFixture.router) - val Transition(_, WAITING_FOR_ROUTE, WAITING_FOR_PAYMENT_COMPLETE) = monitor.expectMsgClass(classOf[Transition[_]]) + val Transition(_, WAITING_FOR_ROUTE, WAITING_FOR_CONFIDENCE) = monitor.expectMsgClass(classOf[Transition[_]]) + val Transition(_, WAITING_FOR_CONFIDENCE, WAITING_FOR_PAYMENT_COMPLETE) = monitor.expectMsgClass(classOf[Transition[_]]) assert(nodeParams.db.payments.getOutgoingPayment(id).isEmpty) sender.send(paymentFSM, addCompleted(HtlcResult.RemoteFulfill(UpdateFulfillHtlc(ByteVector32.Zeroes, 0, defaultPaymentPreimage)))) @@ -957,7 +972,8 @@ class PaymentLifecycleSpec extends BaseRouterSpec { routerForwarder.expectNoMessage(100 millis) // we don't need the router, we have the pre-computed route val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]]) - val Transition(_, WAITING_FOR_ROUTE, WAITING_FOR_PAYMENT_COMPLETE) = monitor.expectMsgClass(classOf[Transition[_]]) + val Transition(_, WAITING_FOR_ROUTE, WAITING_FOR_CONFIDENCE) = monitor.expectMsgClass(classOf[Transition[_]]) + val Transition(_, WAITING_FOR_CONFIDENCE, WAITING_FOR_PAYMENT_COMPLETE) = monitor.expectMsgClass(classOf[Transition[_]]) val WaitingForComplete(_, _, Nil, sharedSecrets1, _, _) = paymentFSM.stateData awaitCond(nodeParams.db.payments.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.Pending)) val Some(outgoing) = nodeParams.db.payments.getOutgoingPayment(id) @@ -967,7 +983,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val channelUpdate_bc_modified = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_b, c, scid_bc, CltvExpiryDelta(42), htlcMinimumMsat = update_bc.htlcMinimumMsat, feeBaseMsat = update_bc.feeBaseMsat, feeProportionalMillionths = update_bc.feeProportionalMillionths, htlcMaximumMsat = update_bc.htlcMaximumMsat) val failure = IncorrectCltvExpiry(CltvExpiry(5), Some(channelUpdate_bc_modified)) // and node replies with a failure containing a new channel update - sender.send(paymentFSM, addCompleted(HtlcResult.RemoteFail(UpdateFailHtlc(ByteVector32.Zeroes, 0, Sphinx.FailurePacket.create(sharedSecrets1.head.secret, failure))))) + sender.send(paymentFSM, addCompleted(HtlcResult.RemoteFail(UpdateFailHtlc(ByteVector32.Zeroes, 0, SphinxSpec.createAndWrap(sharedSecrets1.head.secret, failure))))) // The payment fails without retrying sender.expectMsgType[PaymentFailed] diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentPacketSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentPacketSpec.scala index dc3cb76fee..cfac6c68d5 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentPacketSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentPacketSpec.scala @@ -18,21 +18,24 @@ package fr.acinq.eclair.payment import akka.actor.ActorRef import fr.acinq.bitcoin.scalacompat.DeterministicWallet.ExtendedPrivateKey -import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, Crypto, DeterministicWallet, OutPoint, Satoshi, SatoshiLong, Transaction, TxOut} +import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, ByteVector64, Crypto, DeterministicWallet, OutPoint, Satoshi, SatoshiLong, Transaction, TxOut} import fr.acinq.eclair.FeatureSupport.{Mandatory, Optional} import fr.acinq.eclair.Features._ +import fr.acinq.eclair.TestUtils.randomTxId +import fr.acinq.eclair.channel.ChannelSpendSignature.IndividualSignature import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.fsm.Channel +import fr.acinq.eclair.crypto.Sphinx.HoldTime import fr.acinq.eclair.crypto.{ShaChain, Sphinx} import fr.acinq.eclair.payment.IncomingPaymentPacket._ import fr.acinq.eclair.payment.OutgoingPaymentPacket._ import fr.acinq.eclair.payment.send.BlindedPathsResolver.{FullBlindedRoute, ResolvedPath} import fr.acinq.eclair.payment.send.{BlindedRecipient, ClearRecipient, TrampolinePayment} +import fr.acinq.eclair.reputation.Reputation import fr.acinq.eclair.router.BaseRouterSpec.{blindedRouteFromHops, channelHopFromUpdate} import fr.acinq.eclair.router.BlindedRouteCreation import fr.acinq.eclair.router.Router.{NodeHop, Route} -import fr.acinq.eclair.transactions.Transactions -import fr.acinq.eclair.transactions.Transactions.InputInfo +import fr.acinq.eclair.transactions.Transactions.ZeroFeeHtlcTxAnchorOutputsCommitmentFormat import fr.acinq.eclair.wire.protocol.OfferTypes.{InvoiceRequest, Offer, PaymentInfo} import fr.acinq.eclair.wire.protocol.PaymentOnion.{FinalPayload, IntermediatePayload, OutgoingBlindedPerHopPayload} import fr.acinq.eclair.wire.protocol._ @@ -65,7 +68,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { def testBuildOutgoingPayment(): Unit = { val recipient = ClearRecipient(e, Features.empty, finalAmount, finalExpiry, paymentSecret) - val Right(payment) = buildOutgoingPayment(TestConstants.emptyOrigin, paymentHash, Route(finalAmount, hops, None), recipient, 1.0) + val Right(payment) = buildOutgoingPayment(TestConstants.emptyOrigin, paymentHash, Route(finalAmount, hops, None), recipient, Reputation.Score.max) assert(payment.outgoingChannel == channelUpdate_ab.shortChannelId) assert(payment.cmd.amount == amount_ab) assert(payment.cmd.cltvExpiry == expiry_ab) @@ -76,8 +79,8 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { } def testPeelOnion(packet_b: OnionRoutingPacket): Unit = { - val add_b = UpdateAddHtlc(randomBytes32(), 0, amount_ab, paymentHash, expiry_ab, packet_b, None, 1.0, None) - val Right(relay_b@ChannelRelayPacket(add_b2, payload_b, packet_c)) = decrypt(add_b, priv_b.privateKey, Features.empty) + val add_b = UpdateAddHtlc(randomBytes32(), 0, amount_ab, paymentHash, expiry_ab, packet_b, None, Reputation.maxEndorsement, None) + val Right(relay_b@ChannelRelayPacket(add_b2, payload_b, packet_c, _)) = decrypt(add_b, priv_b.privateKey, Features.empty) assert(add_b2 == add_b) assert(packet_c.payload.length == PaymentOnionCodecs.paymentOnionPayloadLength) assert(relay_b.amountToForward == amount_bc) @@ -86,8 +89,8 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { assert(relay_b.relayFeeMsat == fee_b) assert(relay_b.expiryDelta == channelUpdate_bc.cltvExpiryDelta) - val add_c = UpdateAddHtlc(randomBytes32(), 1, amount_bc, paymentHash, expiry_bc, packet_c, None, 1.0, None) - val Right(relay_c@ChannelRelayPacket(add_c2, payload_c, packet_d)) = decrypt(add_c, priv_c.privateKey, Features.empty) + val add_c = UpdateAddHtlc(randomBytes32(), 1, amount_bc, paymentHash, expiry_bc, packet_c, None, Reputation.maxEndorsement, None) + val Right(relay_c@ChannelRelayPacket(add_c2, payload_c, packet_d, _)) = decrypt(add_c, priv_c.privateKey, Features.empty) assert(add_c2 == add_c) assert(packet_d.payload.length == PaymentOnionCodecs.paymentOnionPayloadLength) assert(relay_c.amountToForward == amount_cd) @@ -96,8 +99,8 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { assert(relay_c.relayFeeMsat == fee_c) assert(relay_c.expiryDelta == channelUpdate_cd.cltvExpiryDelta) - val add_d = UpdateAddHtlc(randomBytes32(), 2, amount_cd, paymentHash, expiry_cd, packet_d, None, 1.0, None) - val Right(relay_d@ChannelRelayPacket(add_d2, payload_d, packet_e)) = decrypt(add_d, priv_d.privateKey, Features.empty) + val add_d = UpdateAddHtlc(randomBytes32(), 2, amount_cd, paymentHash, expiry_cd, packet_d, None, Reputation.maxEndorsement, None) + val Right(relay_d@ChannelRelayPacket(add_d2, payload_d, packet_e, _)) = decrypt(add_d, priv_d.privateKey, Features.empty) assert(add_d2 == add_d) assert(packet_e.payload.length == PaymentOnionCodecs.paymentOnionPayloadLength) assert(relay_d.amountToForward == amount_de) @@ -106,8 +109,8 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { assert(relay_d.relayFeeMsat == fee_d) assert(relay_d.expiryDelta == channelUpdate_de.cltvExpiryDelta) - val add_e = UpdateAddHtlc(randomBytes32(), 2, amount_de, paymentHash, expiry_de, packet_e, None, 1.0, None) - val Right(FinalPacket(add_e2, payload_e)) = decrypt(add_e, priv_e.privateKey, Features.empty) + val add_e = UpdateAddHtlc(randomBytes32(), 2, amount_de, paymentHash, expiry_de, packet_e, None, Reputation.maxEndorsement, None) + val Right(FinalPacket(add_e2, payload_e, _)) = decrypt(add_e, priv_e.privateKey, Features.empty) assert(add_e2 == add_e) assert(payload_e.isInstanceOf[FinalPayload.Standard]) assert(payload_e.amount == finalAmount) @@ -123,15 +126,15 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { test("build outgoing payment for direct peer") { val recipient = ClearRecipient(b, Features.empty, finalAmount, finalExpiry, paymentSecret, paymentMetadata_opt = Some(paymentMetadata)) val route = Route(finalAmount, hops.take(1), None) - val Right(payment) = buildOutgoingPayment(TestConstants.emptyOrigin, paymentHash, route, recipient, 1.0) + val Right(payment) = buildOutgoingPayment(TestConstants.emptyOrigin, paymentHash, route, recipient, Reputation.Score.max) assert(payment.cmd.amount == finalAmount) assert(payment.cmd.cltvExpiry == finalExpiry) assert(payment.cmd.paymentHash == paymentHash) assert(payment.cmd.onion.payload.length == PaymentOnionCodecs.paymentOnionPayloadLength) // let's peel the onion - val add_b = UpdateAddHtlc(randomBytes32(), 0, finalAmount, paymentHash, finalExpiry, payment.cmd.onion, None, 1.0, None) - val Right(FinalPacket(add_b2, payload_b)) = decrypt(add_b, priv_b.privateKey, Features.empty) + val add_b = UpdateAddHtlc(randomBytes32(), 0, finalAmount, paymentHash, finalExpiry, payment.cmd.onion, None, Reputation.maxEndorsement, None) + val Right(FinalPacket(add_b2, payload_b, _)) = decrypt(add_b, priv_b.privateKey, Features.empty) assert(add_b2 == add_b) assert(payload_b.isInstanceOf[FinalPayload.Standard]) assert(payload_b.amount == finalAmount) @@ -144,11 +147,11 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { test("build outgoing payment with greater amount and expiry") { val recipient = ClearRecipient(b, Features.empty, finalAmount, finalExpiry, paymentSecret, paymentMetadata_opt = Some(paymentMetadata)) val route = Route(finalAmount, hops.take(1), None) - val Right(payment) = buildOutgoingPayment(TestConstants.emptyOrigin, paymentHash, route, recipient, 1.0) + val Right(payment) = buildOutgoingPayment(TestConstants.emptyOrigin, paymentHash, route, recipient, Reputation.Score.max) // let's peel the onion - val add_b = UpdateAddHtlc(randomBytes32(), 0, finalAmount + 100.msat, paymentHash, finalExpiry + CltvExpiryDelta(6), payment.cmd.onion, None, 1.0, None) - val Right(FinalPacket(_, payload_b)) = decrypt(add_b, priv_b.privateKey, Features.empty) + val add_b = UpdateAddHtlc(randomBytes32(), 0, finalAmount + 100.msat, paymentHash, finalExpiry + CltvExpiryDelta(6), payment.cmd.onion, None, Reputation.maxEndorsement, None) + val Right(FinalPacket(_, payload_b, _)) = decrypt(add_b, priv_b.privateKey, Features.empty) assert(payload_b.isInstanceOf[FinalPayload.Standard]) assert(payload_b.amount == finalAmount) assert(payload_b.totalAmount == finalAmount) @@ -161,14 +164,14 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { assert(recipient.extraEdges.length == 1) assert(recipient.extraEdges.head.sourceNodeId == c) assert(recipient.extraEdges.head.targetNodeId == invoice.nodeId) - val Right(payment) = buildOutgoingPayment(TestConstants.emptyOrigin, paymentHash, route, recipient, 1.0) + val Right(payment) = buildOutgoingPayment(TestConstants.emptyOrigin, paymentHash, route, recipient, Reputation.Score.max) assert(payment.outgoingChannel == channelUpdate_ab.shortChannelId) assert(payment.cmd.amount >= amount_ab) assert(payment.cmd.cltvExpiry == expiry_ab) assert(payment.cmd.nextPathKey_opt.isEmpty) - val add_b = UpdateAddHtlc(randomBytes32(), 0, payment.cmd.amount, payment.cmd.paymentHash, payment.cmd.cltvExpiry, payment.cmd.onion, payment.cmd.nextPathKey_opt, 1.0, payment.cmd.fundingFee_opt) - val Right(relay_b@ChannelRelayPacket(_, payload_b, packet_c)) = decrypt(add_b, priv_b.privateKey, Features.empty) + val add_b = UpdateAddHtlc(randomBytes32(), 0, payment.cmd.amount, payment.cmd.paymentHash, payment.cmd.cltvExpiry, payment.cmd.onion, payment.cmd.nextPathKey_opt, Reputation.maxEndorsement, payment.cmd.fundingFee_opt) + val Right(relay_b@ChannelRelayPacket(_, payload_b, packet_c, _)) = decrypt(add_b, priv_b.privateKey, Features.empty) assert(packet_c.payload.length == PaymentOnionCodecs.paymentOnionPayloadLength) assert(relay_b.amountToForward >= amount_bc) assert(relay_b.outgoingCltv == expiry_bc) @@ -177,8 +180,8 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { assert(relay_b.expiryDelta == channelUpdate_bc.cltvExpiryDelta) assert(payload_b.isInstanceOf[IntermediatePayload.ChannelRelay.Standard]) - val add_c = UpdateAddHtlc(randomBytes32(), 1, relay_b.amountToForward, relay_b.add.paymentHash, relay_b.outgoingCltv, packet_c, None, 1.0, None) - val Right(relay_c@ChannelRelayPacket(_, payload_c, packet_d)) = decrypt(add_c, priv_c.privateKey, Features(RouteBlinding -> Optional)) + val add_c = UpdateAddHtlc(randomBytes32(), 1, relay_b.amountToForward, relay_b.add.paymentHash, relay_b.outgoingCltv, packet_c, None, Reputation.maxEndorsement, None) + val Right(relay_c@ChannelRelayPacket(_, payload_c, packet_d, _)) = decrypt(add_c, priv_c.privateKey, Features(RouteBlinding -> Optional)) assert(packet_d.payload.length == PaymentOnionCodecs.paymentOnionPayloadLength) assert(relay_c.amountToForward >= amount_cd) assert(relay_c.outgoingCltv == expiry_cd) @@ -188,8 +191,8 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { assert(payload_c.isInstanceOf[IntermediatePayload.ChannelRelay.Blinded]) val pathKey_d = payload_c.asInstanceOf[IntermediatePayload.ChannelRelay.Blinded].nextPathKey - val add_d = UpdateAddHtlc(randomBytes32(), 2, relay_c.amountToForward, relay_c.add.paymentHash, relay_c.outgoingCltv, packet_d, Some(pathKey_d), 1.0, None) - val Right(relay_d@ChannelRelayPacket(_, payload_d, packet_e)) = decrypt(add_d, priv_d.privateKey, Features(RouteBlinding -> Optional)) + val add_d = UpdateAddHtlc(randomBytes32(), 2, relay_c.amountToForward, relay_c.add.paymentHash, relay_c.outgoingCltv, packet_d, Some(pathKey_d), Reputation.maxEndorsement, None) + val Right(relay_d@ChannelRelayPacket(_, payload_d, packet_e, _)) = decrypt(add_d, priv_d.privateKey, Features(RouteBlinding -> Optional)) assert(packet_e.payload.length == PaymentOnionCodecs.paymentOnionPayloadLength) assert(relay_d.amountToForward >= amount_de) assert(relay_d.outgoingCltv == expiry_de) @@ -199,8 +202,8 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { assert(payload_d.isInstanceOf[IntermediatePayload.ChannelRelay.Blinded]) val pathKey_e = payload_d.asInstanceOf[IntermediatePayload.ChannelRelay.Blinded].nextPathKey - val add_e = UpdateAddHtlc(randomBytes32(), 2, relay_d.amountToForward, relay_d.add.paymentHash, relay_d.outgoingCltv, packet_e, Some(pathKey_e), 1.0, None) - val Right(FinalPacket(_, payload_e)) = decrypt(add_e, priv_e.privateKey, Features(RouteBlinding -> Optional)) + val add_e = UpdateAddHtlc(randomBytes32(), 2, relay_d.amountToForward, relay_d.add.paymentHash, relay_d.outgoingCltv, packet_e, Some(pathKey_e), Reputation.maxEndorsement, None) + val Right(FinalPacket(_, payload_e, _)) = decrypt(add_e, priv_e.privateKey, Features(RouteBlinding -> Optional)) assert(payload_e.amount == finalAmount) assert(payload_e.totalAmount == finalAmount) assert(add_e.cltvExpiry == finalExpiry) @@ -216,7 +219,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { val offer = Offer(None, Some("Bolt12 r0cks"), recipientKey.publicKey, features, Block.RegtestGenesisBlock.hash) val invoiceRequest = InvoiceRequest(offer, amount_bc, 1, features, randomKey(), Block.RegtestGenesisBlock.hash) val blindedRoute = BlindedRouteCreation.createBlindedRouteFromHops(Nil, c, hex"deadbeef", 1 msat, CltvExpiry(500_000)).route - val paymentInfo = PaymentInfo(0 msat, 0, CltvExpiryDelta(0), 1 msat, amount_bc, Features.empty) + val paymentInfo = PaymentInfo(0 msat, 0, CltvExpiryDelta(0), 1 msat, amount_bc, ByteVector.empty) val invoice = Bolt12Invoice(invoiceRequest, paymentPreimage, recipientKey, 300 seconds, features, Seq(PaymentBlindedRoute(blindedRoute, paymentInfo))) val resolvedPaths = invoice.blindedPaths.map(path => { val introductionNodeId = path.route.firstNodeId.asInstanceOf[EncodedNodeId.WithPublicKey].publicKey @@ -224,14 +227,14 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { }) val recipient = BlindedRecipient(invoice, resolvedPaths, amount_bc, expiry_bc, Set.empty) val hops = Seq(channelHopFromUpdate(a, b, channelUpdate_ab), channelHopFromUpdate(b, c, channelUpdate_bc)) - val Right(payment) = buildOutgoingPayment(TestConstants.emptyOrigin, paymentHash, Route(amount_bc, hops, Some(recipient.blindedHops.head)), recipient, 1.0) + val Right(payment) = buildOutgoingPayment(TestConstants.emptyOrigin, paymentHash, Route(amount_bc, hops, Some(recipient.blindedHops.head)), recipient, Reputation.Score.max) assert(payment.outgoingChannel == channelUpdate_ab.shortChannelId) assert(payment.cmd.amount == amount_ab) assert(payment.cmd.cltvExpiry == expiry_ab) assert(payment.cmd.nextPathKey_opt.isEmpty) - val add_b = UpdateAddHtlc(randomBytes32(), 0, payment.cmd.amount, payment.cmd.paymentHash, payment.cmd.cltvExpiry, payment.cmd.onion, payment.cmd.nextPathKey_opt, 1.0, payment.cmd.fundingFee_opt) - val Right(relay_b@ChannelRelayPacket(_, payload_b, packet_c)) = decrypt(add_b, priv_b.privateKey, Features.empty) + val add_b = UpdateAddHtlc(randomBytes32(), 0, payment.cmd.amount, payment.cmd.paymentHash, payment.cmd.cltvExpiry, payment.cmd.onion, payment.cmd.nextPathKey_opt, Reputation.maxEndorsement, payment.cmd.fundingFee_opt) + val Right(relay_b@ChannelRelayPacket(_, payload_b, packet_c, _)) = decrypt(add_b, priv_b.privateKey, Features.empty) assert(packet_c.payload.length == PaymentOnionCodecs.paymentOnionPayloadLength) assert(relay_b.amountToForward >= amount_bc) assert(relay_b.outgoingCltv == expiry_bc) @@ -240,8 +243,8 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { assert(relay_b.expiryDelta == channelUpdate_bc.cltvExpiryDelta) assert(payload_b.isInstanceOf[IntermediatePayload.ChannelRelay.Standard]) - val add_c = UpdateAddHtlc(randomBytes32(), 1, amount_bc, paymentHash, expiry_bc, packet_c, None, 1.0, None) - val Right(FinalPacket(_, payload_c)) = decrypt(add_c, priv_c.privateKey, Features(RouteBlinding -> Optional)) + val add_c = UpdateAddHtlc(randomBytes32(), 1, amount_bc, paymentHash, expiry_bc, packet_c, None, Reputation.maxEndorsement, None) + val Right(FinalPacket(_, payload_c, _)) = decrypt(add_c, priv_c.privateKey, Features(RouteBlinding -> Optional)) assert(payload_c.amount == amount_bc) assert(payload_c.totalAmount == amount_bc) assert(payload_c.expiry == expiry_bc) @@ -256,8 +259,8 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { assert(payment.cmd.cltvExpiry == finalExpiry) assert(payment.cmd.nextPathKey_opt.nonEmpty) - val add_b = UpdateAddHtlc(randomBytes32(), 0, payment.cmd.amount, payment.cmd.paymentHash, payment.cmd.cltvExpiry, payment.cmd.onion, payment.cmd.nextPathKey_opt, 1.0, payment.cmd.fundingFee_opt) - val Right(FinalPacket(_, payload_b)) = decrypt(add_b, priv_b.privateKey, Features(RouteBlinding -> Optional)) + val add_b = UpdateAddHtlc(randomBytes32(), 0, payment.cmd.amount, payment.cmd.paymentHash, payment.cmd.cltvExpiry, payment.cmd.onion, payment.cmd.nextPathKey_opt, Reputation.maxEndorsement, payment.cmd.fundingFee_opt) + val Right(FinalPacket(_, payload_b, _)) = decrypt(add_b, priv_b.privateKey, Features(RouteBlinding -> Optional)) assert(payload_b.amount == finalAmount) assert(payload_b.totalAmount == finalAmount) assert(add_b.cltvExpiry == finalExpiry) @@ -269,15 +272,15 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { val Right(payment) = buildOutgoingBlindedPaymentAB(paymentHash) assert(payment.outgoingChannel == channelUpdate_ab.shortChannelId) - val add_b = UpdateAddHtlc(randomBytes32(), 0, payment.cmd.amount + 100.msat, payment.cmd.paymentHash, payment.cmd.cltvExpiry + CltvExpiryDelta(6), payment.cmd.onion, payment.cmd.nextPathKey_opt, 1.0, payment.cmd.fundingFee_opt) - val Right(FinalPacket(_, payload_b)) = decrypt(add_b, priv_b.privateKey, Features(RouteBlinding -> Optional)) + val add_b = UpdateAddHtlc(randomBytes32(), 0, payment.cmd.amount + 100.msat, payment.cmd.paymentHash, payment.cmd.cltvExpiry + CltvExpiryDelta(6), payment.cmd.onion, payment.cmd.nextPathKey_opt, Reputation.maxEndorsement, payment.cmd.fundingFee_opt) + val Right(FinalPacket(_, payload_b, _)) = decrypt(add_b, priv_b.privateKey, Features(RouteBlinding -> Optional)) assert(payload_b.amount == finalAmount) assert(payload_b.totalAmount == finalAmount) } private def testRelayTrampolinePayment(invoice: Bolt11Invoice, payment: TrampolinePayment.OutgoingPayment): Unit = { - val add_c = UpdateAddHtlc(randomBytes32(), 2, payment.trampolineAmount, paymentHash, payment.trampolineExpiry, payment.onion.packet, None, 1.0, None) - val Right(RelayToTrampolinePacket(add_c2, outer_c, inner_c, trampolinePacket_e)) = decrypt(add_c, priv_c.privateKey, Features.empty) + val add_c = UpdateAddHtlc(randomBytes32(), 2, payment.trampolineAmount, paymentHash, payment.trampolineExpiry, payment.onion.packet, None, Reputation.maxEndorsement, None) + val Right(RelayToTrampolinePacket(add_c2, outer_c, inner_c, trampolinePacket_e, _)) = decrypt(add_c, priv_c.privateKey, Features.empty) assert(add_c2 == add_c) assert(outer_c.amount == payment.trampolineAmount) assert(outer_c.totalAmount == payment.trampolineAmount) @@ -289,17 +292,17 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { // c forwards the trampoline payment to e through d. val recipient_e = ClearRecipient(e, Features.empty, inner_c.amountToForward, inner_c.outgoingCltv, randomBytes32(), nextTrampolineOnion_opt = Some(trampolinePacket_e)) - val Right(payment_e) = buildOutgoingPayment(Origin.Hot(ActorRef.noSender, Upstream.Hot.Trampoline(List(Upstream.Hot.Channel(add_c, TimestampMilli(1687345927000L), b)))), paymentHash, Route(inner_c.amountToForward, afterTrampolineChannelHops, None), recipient_e, 1.0) + val Right(payment_e) = buildOutgoingPayment(Origin.Hot(ActorRef.noSender, Upstream.Hot.Trampoline(List(Upstream.Hot.Channel(add_c, TimestampMilli(1687345927000L), b, 0.1)))), paymentHash, Route(inner_c.amountToForward, afterTrampolineChannelHops, None), recipient_e, Reputation.Score.max) assert(payment_e.outgoingChannel == channelUpdate_cd.shortChannelId) assert(payment_e.cmd.amount == amount_cd) assert(payment_e.cmd.cltvExpiry == expiry_cd) - val add_d = UpdateAddHtlc(randomBytes32(), 3, payment_e.cmd.amount, paymentHash, payment_e.cmd.cltvExpiry, payment_e.cmd.onion, None, 1.0, None) - val Right(ChannelRelayPacket(add_d2, payload_d, packet_e)) = decrypt(add_d, priv_d.privateKey, Features.empty) + val add_d = UpdateAddHtlc(randomBytes32(), 3, payment_e.cmd.amount, paymentHash, payment_e.cmd.cltvExpiry, payment_e.cmd.onion, None, Reputation.maxEndorsement, None) + val Right(ChannelRelayPacket(add_d2, payload_d, packet_e, _)) = decrypt(add_d, priv_d.privateKey, Features.empty) assert(add_d2 == add_d) assert(payload_d == IntermediatePayload.ChannelRelay.Standard(channelUpdate_de.shortChannelId, amount_de, expiry_de)) - val add_e = UpdateAddHtlc(randomBytes32(), 4, amount_de, paymentHash, expiry_de, packet_e, None, 1.0, None) - val Right(FinalPacket(add_e2, payload_e)) = decrypt(add_e, priv_e.privateKey, Features.empty) + val add_e = UpdateAddHtlc(randomBytes32(), 4, amount_de, paymentHash, expiry_de, packet_e, None, Reputation.maxEndorsement, None) + val Right(FinalPacket(add_e2, payload_e, _)) = decrypt(add_e, priv_e.privateKey, Features.empty) assert(add_e2 == add_e) assert(payload_e.isInstanceOf[FinalPayload.Standard]) assert(payload_e.amount == finalAmount) @@ -342,8 +345,8 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { val invoice = Bolt11Invoice(Block.RegtestGenesisBlock.hash, Some(finalAmount), paymentHash, priv_e.privateKey, Left("#reckless"), CltvExpiryDelta(18), extraHops = routingHints, features = invoiceFeatures, paymentMetadata = Some(hex"010203")) val payment = TrampolinePayment.buildOutgoingPayment(c, invoice, finalExpiry) - val add_c = UpdateAddHtlc(randomBytes32(), 2, payment.trampolineAmount, paymentHash, payment.trampolineExpiry, payment.onion.packet, None, 1.0, None) - val Right(RelayToNonTrampolinePacket(_, outer_c, inner_c)) = decrypt(add_c, priv_c.privateKey, Features.empty) + val add_c = UpdateAddHtlc(randomBytes32(), 2, payment.trampolineAmount, paymentHash, payment.trampolineExpiry, payment.onion.packet, None, Reputation.maxEndorsement, None) + val Right(RelayToNonTrampolinePacket(_, outer_c, inner_c, _)) = decrypt(add_c, priv_c.privateKey, Features.empty) assert(outer_c.amount == payment.trampolineAmount) assert(outer_c.totalAmount == payment.trampolineAmount) assert(outer_c.expiry == payment.trampolineExpiry) @@ -360,17 +363,17 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { // c forwards the trampoline payment to e through d. val recipient_e = ClearRecipient(e, Features.empty, inner_c.amountToForward, inner_c.outgoingCltv, inner_c.paymentSecret, invoice.extraEdges, inner_c.paymentMetadata) - val Right(payment_e) = buildOutgoingPayment(Origin.Hot(ActorRef.noSender, Upstream.Hot.Trampoline(List(Upstream.Hot.Channel(add_c, TimestampMilli(1687345927000L), b)))), paymentHash, Route(inner_c.amountToForward, afterTrampolineChannelHops, None), recipient_e, 1.0) + val Right(payment_e) = buildOutgoingPayment(Origin.Hot(ActorRef.noSender, Upstream.Hot.Trampoline(List(Upstream.Hot.Channel(add_c, TimestampMilli(1687345927000L), b, 0.1)))), paymentHash, Route(inner_c.amountToForward, afterTrampolineChannelHops, None), recipient_e, Reputation.Score.max) assert(payment_e.outgoingChannel == channelUpdate_cd.shortChannelId) assert(payment_e.cmd.amount == amount_cd) assert(payment_e.cmd.cltvExpiry == expiry_cd) - val add_d = UpdateAddHtlc(randomBytes32(), 3, payment_e.cmd.amount, paymentHash, payment_e.cmd.cltvExpiry, payment_e.cmd.onion, None, 1.0, None) - val Right(ChannelRelayPacket(add_d2, payload_d, packet_e)) = decrypt(add_d, priv_d.privateKey, Features.empty) + val add_d = UpdateAddHtlc(randomBytes32(), 3, payment_e.cmd.amount, paymentHash, payment_e.cmd.cltvExpiry, payment_e.cmd.onion, None, Reputation.maxEndorsement, None) + val Right(ChannelRelayPacket(add_d2, payload_d, packet_e, _)) = decrypt(add_d, priv_d.privateKey, Features.empty) assert(add_d2 == add_d) assert(payload_d == IntermediatePayload.ChannelRelay.Standard(channelUpdate_de.shortChannelId, amount_de, expiry_de)) - val add_e = UpdateAddHtlc(randomBytes32(), 4, amount_de, paymentHash, expiry_de, packet_e, None, 1.0, None) - val Right(FinalPacket(add_e2, payload_e)) = decrypt(add_e, priv_e.privateKey, Features.empty) + val add_e = UpdateAddHtlc(randomBytes32(), 4, amount_de, paymentHash, expiry_de, packet_e, None, Reputation.maxEndorsement, None) + val Right(FinalPacket(add_e2, payload_e, _)) = decrypt(add_e, priv_e.privateKey, Features.empty) assert(add_e2 == add_e) assert(payload_e.isInstanceOf[FinalPayload.Standard]) assert(payload_e.amount == finalAmount) @@ -404,8 +407,8 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { TrampolinePayment.OutgoingPayment(trampolineAmount, trampolineExpiry, paymentOnion, trampolineOnion) } - val add_c = UpdateAddHtlc(randomBytes32(), 2, payment.trampolineAmount, paymentHash, payment.trampolineExpiry, payment.onion.packet, None, 1.0, None) - val Right(RelayToNonTrampolinePacket(_, outer_c, inner_c)) = decrypt(add_c, priv_c.privateKey, Features.empty) + val add_c = UpdateAddHtlc(randomBytes32(), 2, payment.trampolineAmount, paymentHash, payment.trampolineExpiry, payment.onion.packet, None, Reputation.maxEndorsement, None) + val Right(RelayToNonTrampolinePacket(_, outer_c, inner_c, _)) = decrypt(add_c, priv_c.privateKey, Features.empty) assert(outer_c.amount == payment.trampolineAmount) assert(outer_c.totalAmount == payment.trampolineAmount) assert(outer_c.expiry == payment.trampolineExpiry) @@ -422,17 +425,17 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { // c forwards the trampoline payment to e through d. val recipient_e = ClearRecipient(e, Features.empty, inner_c.amountToForward, inner_c.outgoingCltv, inner_c.paymentSecret, invoice.extraEdges, inner_c.paymentMetadata) - val Right(payment_e) = buildOutgoingPayment(Origin.Hot(ActorRef.noSender, Upstream.Hot.Trampoline(List(Upstream.Hot.Channel(add_c, TimestampMilli(1687345927000L), b)))), paymentHash, Route(inner_c.amountToForward, afterTrampolineChannelHops, None), recipient_e, 1.0) + val Right(payment_e) = buildOutgoingPayment(Origin.Hot(ActorRef.noSender, Upstream.Hot.Trampoline(List(Upstream.Hot.Channel(add_c, TimestampMilli(1687345927000L), b, 0.1)))), paymentHash, Route(inner_c.amountToForward, afterTrampolineChannelHops, None), recipient_e, Reputation.Score.max) assert(payment_e.outgoingChannel == channelUpdate_cd.shortChannelId) assert(payment_e.cmd.amount == amount_cd) assert(payment_e.cmd.cltvExpiry == expiry_cd) - val add_d = UpdateAddHtlc(randomBytes32(), 3, payment_e.cmd.amount, paymentHash, payment_e.cmd.cltvExpiry, payment_e.cmd.onion, None, 1.0, None) - val Right(ChannelRelayPacket(add_d2, payload_d, packet_e)) = decrypt(add_d, priv_d.privateKey, Features.empty) + val add_d = UpdateAddHtlc(randomBytes32(), 3, payment_e.cmd.amount, paymentHash, payment_e.cmd.cltvExpiry, payment_e.cmd.onion, None, Reputation.maxEndorsement, None) + val Right(ChannelRelayPacket(add_d2, payload_d, packet_e, _)) = decrypt(add_d, priv_d.privateKey, Features.empty) assert(add_d2 == add_d) assert(payload_d == IntermediatePayload.ChannelRelay.Standard(channelUpdate_de.shortChannelId, amount_de, expiry_de)) - val add_e = UpdateAddHtlc(randomBytes32(), 4, amount_de, paymentHash, expiry_de, packet_e, None, 1.0, None) - val Right(FinalPacket(add_e2, payload_e)) = decrypt(add_e, priv_e.privateKey, Features.empty) + val add_e = UpdateAddHtlc(randomBytes32(), 4, amount_de, paymentHash, expiry_de, packet_e, None, Reputation.maxEndorsement, None) + val Right(FinalPacket(add_e2, payload_e, _)) = decrypt(add_e, priv_e.privateKey, Features.empty) assert(add_e2 == add_e) assert(payload_e.isInstanceOf[FinalPayload.Standard]) assert(payload_e.amount == finalAmount) @@ -445,22 +448,22 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { test("fail to build outgoing payment with invalid route") { val recipient = ClearRecipient(e, Features.empty, finalAmount, finalExpiry, paymentSecret) val route = Route(finalAmount, hops.dropRight(1), None) // route doesn't reach e - val Left(failure) = buildOutgoingPayment(TestConstants.emptyOrigin, paymentHash, route, recipient, 1.0) + val Left(failure) = buildOutgoingPayment(TestConstants.emptyOrigin, paymentHash, route, recipient, Reputation.Score.max) assert(failure == InvalidRouteRecipient(e, d)) } test("fail to build outgoing blinded payment with invalid route") { val (_, route, recipient) = longBlindedHops(hex"deadbeef") - assert(buildOutgoingPayment(TestConstants.emptyOrigin, paymentHash, route, recipient, 1.0).isRight) + assert(buildOutgoingPayment(TestConstants.emptyOrigin, paymentHash, route, recipient, Reputation.Score.max).isRight) val routeMissingBlindedHop = route.copy(finalHop_opt = None) - val Left(failure) = buildOutgoingPayment(TestConstants.emptyOrigin, paymentHash, routeMissingBlindedHop, recipient, 1.0) + val Left(failure) = buildOutgoingPayment(TestConstants.emptyOrigin, paymentHash, routeMissingBlindedHop, recipient, Reputation.Score.max) assert(failure == MissingBlindedHop(Set(c))) } test("fail to decrypt when the onion is invalid") { val recipient = ClearRecipient(e, Features.empty, finalAmount, finalExpiry, paymentSecret) - val Right(payment) = buildOutgoingPayment(TestConstants.emptyOrigin, paymentHash, Route(finalAmount, hops, None), recipient, 1.0) - val add = UpdateAddHtlc(randomBytes32(), 1, payment.cmd.amount, paymentHash, payment.cmd.cltvExpiry, payment.cmd.onion.copy(payload = payment.cmd.onion.payload.reverse), None, 1.0, None) + val Right(payment) = buildOutgoingPayment(TestConstants.emptyOrigin, paymentHash, Route(finalAmount, hops, None), recipient, Reputation.Score.max) + val add = UpdateAddHtlc(randomBytes32(), 1, payment.cmd.amount, paymentHash, payment.cmd.cltvExpiry, payment.cmd.onion.copy(payload = payment.cmd.onion.payload.reverse), None, Reputation.maxEndorsement, None) val Left(failure) = decrypt(add, priv_b.privateKey, Features.empty) assert(failure.isInstanceOf[InvalidOnionHmac]) } @@ -470,25 +473,25 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { val invoice = Bolt11Invoice(Block.RegtestGenesisBlock.hash, Some(finalAmount), paymentHash, priv_e.privateKey, Left("invoice"), CltvExpiryDelta(6), paymentSecret = paymentSecret, features = invoiceFeatures, paymentMetadata = Some(hex"010203")) val payment = TrampolinePayment.buildOutgoingPayment(c, invoice, finalExpiry) - val add_c = UpdateAddHtlc(randomBytes32(), 2, payment.trampolineAmount, paymentHash, payment.trampolineExpiry, payment.onion.packet, None, 1.0, None) - val Right(RelayToTrampolinePacket(_, _, inner_c, trampolinePacket_e)) = decrypt(add_c, priv_c.privateKey, Features.empty) + val add_c = UpdateAddHtlc(randomBytes32(), 2, payment.trampolineAmount, paymentHash, payment.trampolineExpiry, payment.onion.packet, None, Reputation.maxEndorsement, None) + val Right(RelayToTrampolinePacket(_, _, inner_c, trampolinePacket_e, _)) = decrypt(add_c, priv_c.privateKey, Features.empty) // c forwards an invalid trampoline onion to e through d. val recipient_e = ClearRecipient(e, Features.empty, inner_c.amountToForward, inner_c.outgoingCltv, randomBytes32(), nextTrampolineOnion_opt = Some(trampolinePacket_e.copy(payload = trampolinePacket_e.payload.reverse))) - val Right(payment_e) = buildOutgoingPayment(Origin.Hot(ActorRef.noSender, Upstream.Hot.Trampoline(List(Upstream.Hot.Channel(add_c, TimestampMilli(1687345927000L), b)))), paymentHash, Route(inner_c.amountToForward, afterTrampolineChannelHops, None), recipient_e, 1.0) + val Right(payment_e) = buildOutgoingPayment(Origin.Hot(ActorRef.noSender, Upstream.Hot.Trampoline(List(Upstream.Hot.Channel(add_c, TimestampMilli(1687345927000L), b, 0.1)))), paymentHash, Route(inner_c.amountToForward, afterTrampolineChannelHops, None), recipient_e, Reputation.Score.max) assert(payment_e.outgoingChannel == channelUpdate_cd.shortChannelId) - val add_d = UpdateAddHtlc(randomBytes32(), 3, payment_e.cmd.amount, paymentHash, payment_e.cmd.cltvExpiry, payment_e.cmd.onion, None, 1.0, None) - val Right(ChannelRelayPacket(_, _, packet_e)) = decrypt(add_d, priv_d.privateKey, Features.empty) + val add_d = UpdateAddHtlc(randomBytes32(), 3, payment_e.cmd.amount, paymentHash, payment_e.cmd.cltvExpiry, payment_e.cmd.onion, None, Reputation.maxEndorsement, None) + val Right(ChannelRelayPacket(_, _, packet_e, _)) = decrypt(add_d, priv_d.privateKey, Features.empty) - val add_e = UpdateAddHtlc(randomBytes32(), 4, amount_de, paymentHash, expiry_de, packet_e, None, 1.0, None) + val add_e = UpdateAddHtlc(randomBytes32(), 4, amount_de, paymentHash, expiry_de, packet_e, None, Reputation.maxEndorsement, None) val Left(failure) = decrypt(add_e, priv_e.privateKey, Features.empty) assert(failure.isInstanceOf[InvalidOnionHmac]) } test("fail to decrypt when payment hash doesn't match associated data") { val recipient = ClearRecipient(e, Features.empty, finalAmount, finalExpiry, paymentSecret) - val Right(payment) = buildOutgoingPayment(TestConstants.emptyOrigin, paymentHash.reverse, Route(finalAmount, hops, None), recipient, 1.0) - val add = UpdateAddHtlc(randomBytes32(), 1, payment.cmd.amount, paymentHash, payment.cmd.cltvExpiry, payment.cmd.onion, None, 1.0, None) + val Right(payment) = buildOutgoingPayment(TestConstants.emptyOrigin, paymentHash.reverse, Route(finalAmount, hops, None), recipient, Reputation.Score.max) + val add = UpdateAddHtlc(randomBytes32(), 1, payment.cmd.amount, paymentHash, payment.cmd.cltvExpiry, payment.cmd.onion, None, Reputation.maxEndorsement, None) val Left(failure) = decrypt(add, priv_b.privateKey, Features.empty) assert(failure.isInstanceOf[InvalidOnionHmac]) } @@ -501,7 +504,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { // We send the wrong blinded payload to the introduction node. val tmpBlindedRoute = BlindedRouteCreation.createBlindedRouteFromHops(Seq(channelHopFromUpdate(b, c, channelUpdate_bc)), c, hex"deadbeef", 1 msat, CltvExpiry(500_000)).route val blindedRoute = tmpBlindedRoute.copy(blindedHops = tmpBlindedRoute.blindedHops.reverse) - val paymentInfo = OfferTypes.PaymentInfo(fee_b, 0, channelUpdate_bc.cltvExpiryDelta, 0 msat, amount_bc, Features.empty) + val paymentInfo = OfferTypes.PaymentInfo(fee_b, 0, channelUpdate_bc.cltvExpiryDelta, 0 msat, amount_bc, ByteVector.empty) val invoice = Bolt12Invoice(invoiceRequest, paymentPreimage, priv_c.privateKey, 300 seconds, features, Seq(PaymentBlindedRoute(blindedRoute, paymentInfo))) val resolvedPaths = invoice.blindedPaths.map(path => { val introductionNodeId = path.route.firstNodeId.asInstanceOf[EncodedNodeId.WithPublicKey].publicKey @@ -511,19 +514,19 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { val route = Route(amount_bc, Seq(channelHopFromUpdate(a, b, channelUpdate_ab)), Some(recipient.blindedHops.head)) (route, recipient) } - val Right(payment) = buildOutgoingPayment(TestConstants.emptyOrigin, paymentHash, route, recipient, 1.0) + val Right(payment) = buildOutgoingPayment(TestConstants.emptyOrigin, paymentHash, route, recipient, Reputation.Score.max) assert(payment.outgoingChannel == channelUpdate_ab.shortChannelId) assert(payment.cmd.amount == amount_bc + fee_b) - val add_b = UpdateAddHtlc(randomBytes32(), 0, payment.cmd.amount, payment.cmd.paymentHash, payment.cmd.cltvExpiry, payment.cmd.onion, payment.cmd.nextPathKey_opt, 1.0, payment.cmd.fundingFee_opt) + val add_b = UpdateAddHtlc(randomBytes32(), 0, payment.cmd.amount, payment.cmd.paymentHash, payment.cmd.cltvExpiry, payment.cmd.onion, payment.cmd.nextPathKey_opt, Reputation.maxEndorsement, payment.cmd.fundingFee_opt) val Left(failure) = decrypt(add_b, priv_b.privateKey, Features(RouteBlinding -> Optional)) assert(failure.isInstanceOf[InvalidOnionBlinding]) } test("fail to decrypt blinded payment when route blinding is disabled") { val (route, recipient) = shortBlindedHops() - val Right(payment) = buildOutgoingPayment(TestConstants.emptyOrigin, paymentHash, route, recipient, 1.0) - val add_d = UpdateAddHtlc(randomBytes32(), 0, payment.cmd.amount, payment.cmd.paymentHash, payment.cmd.cltvExpiry, payment.cmd.onion, payment.cmd.nextPathKey_opt, 1.0, payment.cmd.fundingFee_opt) + val Right(payment) = buildOutgoingPayment(TestConstants.emptyOrigin, paymentHash, route, recipient, Reputation.Score.max) + val add_d = UpdateAddHtlc(randomBytes32(), 0, payment.cmd.amount, payment.cmd.paymentHash, payment.cmd.cltvExpiry, payment.cmd.onion, payment.cmd.nextPathKey_opt, Reputation.maxEndorsement, payment.cmd.fundingFee_opt) val Left(failure) = decrypt(add_d, priv_d.privateKey, Features.empty) // d doesn't support route blinding assert(failure == InvalidOnionPayload(UInt64(10), 0)) } @@ -531,8 +534,8 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { test("fail to decrypt at the final node when amount has been modified by next-to-last node") { val recipient = ClearRecipient(b, Features.empty, finalAmount, finalExpiry, paymentSecret) val route = Route(finalAmount, hops.take(1), None) - val Right(payment) = buildOutgoingPayment(TestConstants.emptyOrigin, paymentHash, route, recipient, 1.0) - val add = UpdateAddHtlc(randomBytes32(), 1, payment.cmd.amount - 100.msat, paymentHash, payment.cmd.cltvExpiry, payment.cmd.onion, None, 1.0, None) + val Right(payment) = buildOutgoingPayment(TestConstants.emptyOrigin, paymentHash, route, recipient, Reputation.Score.max) + val add = UpdateAddHtlc(randomBytes32(), 1, payment.cmd.amount - 100.msat, paymentHash, payment.cmd.cltvExpiry, payment.cmd.onion, None, Reputation.maxEndorsement, None) val Left(failure) = decrypt(add, priv_b.privateKey, Features.empty) assert(failure == FinalIncorrectHtlcAmount(payment.cmd.amount - 100.msat)) } @@ -540,30 +543,30 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { test("fail to decrypt at the final node when expiry has been modified by next-to-last node") { val recipient = ClearRecipient(b, Features.empty, finalAmount, finalExpiry, paymentSecret) val route = Route(finalAmount, hops.take(1), None) - val Right(payment) = buildOutgoingPayment(TestConstants.emptyOrigin, paymentHash, route, recipient, 1.0) - val add = UpdateAddHtlc(randomBytes32(), 1, payment.cmd.amount, paymentHash, payment.cmd.cltvExpiry - CltvExpiryDelta(12), payment.cmd.onion, None, 1.0, None) + val Right(payment) = buildOutgoingPayment(TestConstants.emptyOrigin, paymentHash, route, recipient, Reputation.Score.max) + val add = UpdateAddHtlc(randomBytes32(), 1, payment.cmd.amount, paymentHash, payment.cmd.cltvExpiry - CltvExpiryDelta(12), payment.cmd.onion, None, Reputation.maxEndorsement, None) val Left(failure) = decrypt(add, priv_b.privateKey, Features.empty) assert(failure == FinalIncorrectCltvExpiry(payment.cmd.cltvExpiry - CltvExpiryDelta(12))) } test("fail to decrypt blinded payment at the final node when expiry is too low") { val (route, recipient) = shortBlindedHops() - val Right(payment) = buildOutgoingPayment(TestConstants.emptyOrigin, paymentHash, route, recipient, 1.0) + val Right(payment) = buildOutgoingPayment(TestConstants.emptyOrigin, paymentHash, route, recipient, Reputation.Score.max) assert(payment.outgoingChannel == channelUpdate_cd.shortChannelId) assert(payment.cmd.cltvExpiry == expiry_cd) // A smaller expiry is sent to d, who doesn't know that it's invalid. // Intermediate nodes can reduce the expiry by at most min_final_expiry_delta. val invalidExpiry = payment.cmd.cltvExpiry - Channel.MIN_CLTV_EXPIRY_DELTA - CltvExpiryDelta(1) - val add_d = UpdateAddHtlc(randomBytes32(), 0, payment.cmd.amount, paymentHash, invalidExpiry, payment.cmd.onion, payment.cmd.nextPathKey_opt, 1.0, payment.cmd.fundingFee_opt) - val Right(relay_d@ChannelRelayPacket(_, payload_d, packet_e)) = decrypt(add_d, priv_d.privateKey, Features(RouteBlinding -> Optional)) + val add_d = UpdateAddHtlc(randomBytes32(), 0, payment.cmd.amount, paymentHash, invalidExpiry, payment.cmd.onion, payment.cmd.nextPathKey_opt, Reputation.maxEndorsement, payment.cmd.fundingFee_opt) + val Right(relay_d@ChannelRelayPacket(_, payload_d, packet_e, _)) = decrypt(add_d, priv_d.privateKey, Features(RouteBlinding -> Optional)) assert(payload_d.outgoing.contains(channelUpdate_de.shortChannelId)) assert(relay_d.outgoingCltv < CltvExpiry(currentBlockCount)) assert(payload_d.isInstanceOf[IntermediatePayload.ChannelRelay.Blinded]) val pathKey_e = payload_d.asInstanceOf[IntermediatePayload.ChannelRelay.Blinded].nextPathKey // When e receives a smaller expiry than expected, it rejects the payment. - val add_e = UpdateAddHtlc(randomBytes32(), 0, relay_d.amountToForward, paymentHash, relay_d.outgoingCltv, packet_e, Some(pathKey_e), 1.0, None) + val add_e = UpdateAddHtlc(randomBytes32(), 0, relay_d.amountToForward, paymentHash, relay_d.outgoingCltv, packet_e, Some(pathKey_e), Reputation.maxEndorsement, None) val Left(failure) = decrypt(add_e, priv_e.privateKey, Features(RouteBlinding -> Optional)) assert(failure.isInstanceOf[InvalidOnionBlinding]) } @@ -571,11 +574,11 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { test("fail to decrypt blinded payment at intermediate node when expiry is too high") { val routeExpiry = expiry_de - channelUpdate_de.cltvExpiryDelta val (route, recipient) = shortBlindedHops(routeExpiry) - val Right(payment) = buildOutgoingPayment(TestConstants.emptyOrigin, paymentHash, route, recipient, 1.0) + val Right(payment) = buildOutgoingPayment(TestConstants.emptyOrigin, paymentHash, route, recipient, Reputation.Score.max) assert(payment.outgoingChannel == channelUpdate_cd.shortChannelId) assert(payment.cmd.cltvExpiry > expiry_de) - val add_d = UpdateAddHtlc(randomBytes32(), 0, payment.cmd.amount, paymentHash, payment.cmd.cltvExpiry, payment.cmd.onion, payment.cmd.nextPathKey_opt, 1.0, payment.cmd.fundingFee_opt) + val add_d = UpdateAddHtlc(randomBytes32(), 0, payment.cmd.amount, paymentHash, payment.cmd.cltvExpiry, payment.cmd.onion, payment.cmd.nextPathKey_opt, Reputation.maxEndorsement, payment.cmd.fundingFee_opt) val Left(failure) = decrypt(add_d, priv_d.privateKey, Features(RouteBlinding -> Optional)) assert(failure.isInstanceOf[InvalidOnionBlinding]) } @@ -590,37 +593,37 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { val invoiceFeatures = Features[Bolt11Feature](VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory, BasicMultiPartPayment -> Optional, TrampolinePaymentPrototype -> Optional) val invoice = Bolt11Invoice(Block.RegtestGenesisBlock.hash, Some(finalAmount), paymentHash, priv_e.privateKey, Left("invoice"), CltvExpiryDelta(6), paymentSecret = paymentSecret, features = invoiceFeatures) val payment = TrampolinePayment.buildOutgoingPayment(c, invoice, finalExpiry) - UpdateAddHtlc(randomBytes32(), 2, payment.trampolineAmount, paymentHash, payment.trampolineExpiry, payment.onion.packet, None, 1.0, None) + UpdateAddHtlc(randomBytes32(), 2, payment.trampolineAmount, paymentHash, payment.trampolineExpiry, payment.onion.packet, None, Reputation.maxEndorsement, None) } test("fail to decrypt at the final trampoline node when amount has been decreased by next-to-last trampoline") { val add_c = createIntermediateTrampolinePayment() - val Right(RelayToTrampolinePacket(_, _, inner_c, trampolinePacket_e)) = decrypt(add_c, priv_c.privateKey, Features.empty) + val Right(RelayToTrampolinePacket(_, _, inner_c, trampolinePacket_e, _)) = decrypt(add_c, priv_c.privateKey, Features.empty) // c forwards an invalid amount to e through (the outer total amount doesn't match the inner amount). val invalidTotalAmount = inner_c.amountToForward - 1.msat val recipient_e = ClearRecipient(e, Features.empty, invalidTotalAmount, inner_c.outgoingCltv, randomBytes32(), nextTrampolineOnion_opt = Some(trampolinePacket_e)) - val Right(payment_e) = buildOutgoingPayment(Origin.Hot(ActorRef.noSender, Upstream.Hot.Trampoline(List(Upstream.Hot.Channel(add_c, TimestampMilli(1687345927000L), b)))), paymentHash, Route(invalidTotalAmount, afterTrampolineChannelHops, None), recipient_e, 1.0) - val add_d = UpdateAddHtlc(randomBytes32(), 3, payment_e.cmd.amount, paymentHash, payment_e.cmd.cltvExpiry, payment_e.cmd.onion, None, 1.0, None) - val Right(ChannelRelayPacket(_, payload_d, packet_e)) = decrypt(add_d, priv_d.privateKey, Features.empty) + val Right(payment_e) = buildOutgoingPayment(Origin.Hot(ActorRef.noSender, Upstream.Hot.Trampoline(List(Upstream.Hot.Channel(add_c, TimestampMilli(1687345927000L), b, 0.1)))), paymentHash, Route(invalidTotalAmount, afterTrampolineChannelHops, None), recipient_e, Reputation.Score.max) + val add_d = UpdateAddHtlc(randomBytes32(), 3, payment_e.cmd.amount, paymentHash, payment_e.cmd.cltvExpiry, payment_e.cmd.onion, None, Reputation.maxEndorsement, None) + val Right(ChannelRelayPacket(_, payload_d, packet_e, _)) = decrypt(add_d, priv_d.privateKey, Features.empty) - val add_e = UpdateAddHtlc(randomBytes32(), 4, payload_d.amountToForward(add_d.amountMsat), paymentHash, payload_d.outgoingCltv(add_d.cltvExpiry), packet_e, None, 1.0, None) + val add_e = UpdateAddHtlc(randomBytes32(), 4, payload_d.amountToForward(add_d.amountMsat), paymentHash, payload_d.outgoingCltv(add_d.cltvExpiry), packet_e, None, Reputation.maxEndorsement, None) val Left(failure) = decrypt(add_e, priv_e.privateKey, Features.empty) assert(failure == FinalIncorrectHtlcAmount(invalidTotalAmount)) } test("fail to decrypt at the final trampoline node when expiry has been modified by next-to-last trampoline") { val add_c = createIntermediateTrampolinePayment() - val Right(RelayToTrampolinePacket(_, _, inner_c, trampolinePacket_e)) = decrypt(add_c, priv_c.privateKey, Features.empty) + val Right(RelayToTrampolinePacket(_, _, inner_c, trampolinePacket_e, _)) = decrypt(add_c, priv_c.privateKey, Features.empty) // c forwards an invalid amount to e through (the outer expiry doesn't match the inner expiry). val invalidExpiry = inner_c.outgoingCltv - CltvExpiryDelta(12) val recipient_e = ClearRecipient(e, Features.empty, inner_c.amountToForward, invalidExpiry, randomBytes32(), nextTrampolineOnion_opt = Some(trampolinePacket_e)) - val Right(payment_e) = buildOutgoingPayment(Origin.Hot(ActorRef.noSender, Upstream.Hot.Trampoline(List(Upstream.Hot.Channel(add_c, TimestampMilli(1687345927000L), b)))), paymentHash, Route(inner_c.amountToForward, afterTrampolineChannelHops, None), recipient_e, 1.0) - val add_d = UpdateAddHtlc(randomBytes32(), 3, payment_e.cmd.amount, paymentHash, payment_e.cmd.cltvExpiry, payment_e.cmd.onion, None, 1.0, None) - val Right(ChannelRelayPacket(_, payload_d, packet_e)) = decrypt(add_d, priv_d.privateKey, Features.empty) + val Right(payment_e) = buildOutgoingPayment(Origin.Hot(ActorRef.noSender, Upstream.Hot.Trampoline(List(Upstream.Hot.Channel(add_c, TimestampMilli(1687345927000L), b, 0.1)))), paymentHash, Route(inner_c.amountToForward, afterTrampolineChannelHops, None), recipient_e, Reputation.Score.max) + val add_d = UpdateAddHtlc(randomBytes32(), 3, payment_e.cmd.amount, paymentHash, payment_e.cmd.cltvExpiry, payment_e.cmd.onion, None, Reputation.maxEndorsement, None) + val Right(ChannelRelayPacket(_, payload_d, packet_e, _)) = decrypt(add_d, priv_d.privateKey, Features.empty) - val add_e = UpdateAddHtlc(randomBytes32(), 4, payload_d.amountToForward(add_d.amountMsat), paymentHash, payload_d.outgoingCltv(add_d.cltvExpiry), packet_e, None, 1.0, None) + val add_e = UpdateAddHtlc(randomBytes32(), 4, payload_d.amountToForward(add_d.amountMsat), paymentHash, payload_d.outgoingCltv(add_d.cltvExpiry), packet_e, None, Reputation.maxEndorsement, None) val Left(failure) = decrypt(add_e, priv_e.privateKey, Features.empty) assert(failure == FinalIncorrectCltvExpiry(invalidExpiry)) } @@ -643,28 +646,59 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { test("build htlc failure onion") { // a -> b -> c -> d -> e val recipient = ClearRecipient(e, Features.empty, finalAmount, finalExpiry, paymentSecret) - val Right(payment) = buildOutgoingPayment(TestConstants.emptyOrigin, paymentHash, Route(finalAmount, hops, None), recipient, 1.0) - val add_b = UpdateAddHtlc(randomBytes32(), 0, amount_ab, paymentHash, expiry_ab, payment.cmd.onion, None, 1.0, None) - val Right(ChannelRelayPacket(_, _, packet_c)) = decrypt(add_b, priv_b.privateKey, Features.empty) - val add_c = UpdateAddHtlc(randomBytes32(), 1, amount_bc, paymentHash, expiry_bc, packet_c, None, 1.0, None) - val Right(ChannelRelayPacket(_, _, packet_d)) = decrypt(add_c, priv_c.privateKey, Features.empty) - val add_d = UpdateAddHtlc(randomBytes32(), 2, amount_cd, paymentHash, expiry_cd, packet_d, None, 1.0, None) - val Right(ChannelRelayPacket(_, _, packet_e)) = decrypt(add_d, priv_d.privateKey, Features.empty) - val add_e = UpdateAddHtlc(randomBytes32(), 3, amount_de, paymentHash, expiry_de, packet_e, None, 1.0, None) - val Right(FinalPacket(_, payload_e)) = decrypt(add_e, priv_e.privateKey, Features.empty) + val Right(payment) = buildOutgoingPayment(TestConstants.emptyOrigin, paymentHash, Route(finalAmount, hops, None), recipient, Reputation.Score.max) + val add_b = UpdateAddHtlc(randomBytes32(), 0, amount_ab, paymentHash, expiry_ab, payment.cmd.onion, None, Reputation.maxEndorsement, None) + val Right(ChannelRelayPacket(_, _, packet_c, _)) = decrypt(add_b, priv_b.privateKey, Features.empty) + val add_c = UpdateAddHtlc(randomBytes32(), 1, amount_bc, paymentHash, expiry_bc, packet_c, None, Reputation.maxEndorsement, None) + val Right(ChannelRelayPacket(_, _, packet_d, _)) = decrypt(add_c, priv_c.privateKey, Features.empty) + val add_d = UpdateAddHtlc(randomBytes32(), 2, amount_cd, paymentHash, expiry_cd, packet_d, None, Reputation.maxEndorsement, None) + val Right(ChannelRelayPacket(_, _, packet_e, _)) = decrypt(add_d, priv_d.privateKey, Features.empty) + val add_e = UpdateAddHtlc(randomBytes32(), 3, amount_de, paymentHash, expiry_de, packet_e, None, Reputation.maxEndorsement, None) + val Right(FinalPacket(_, payload_e, _)) = decrypt(add_e, priv_e.privateKey, Features.empty) assert(payload_e.isInstanceOf[FinalPayload.Standard]) // e returns a failure val failure = IncorrectOrUnknownPaymentDetails(finalAmount, BlockHeight(currentBlockCount)) - val Right(fail_e: UpdateFailHtlc) = buildHtlcFailure(priv_e.privateKey, CMD_FAIL_HTLC(add_e.id, FailureReason.LocalFailure(failure)), add_e) + val Right(fail_e: UpdateFailHtlc) = buildHtlcFailure(priv_e.privateKey, useAttributableFailures = false, CMD_FAIL_HTLC(add_e.id, FailureReason.LocalFailure(failure), None), add_e) assert(fail_e.id == add_e.id) - val Right(fail_d: UpdateFailHtlc) = buildHtlcFailure(priv_d.privateKey, CMD_FAIL_HTLC(add_d.id, FailureReason.EncryptedDownstreamFailure(fail_e.reason)), add_d) + val Right(fail_d: UpdateFailHtlc) = buildHtlcFailure(priv_d.privateKey, useAttributableFailures = false, CMD_FAIL_HTLC(add_d.id, FailureReason.EncryptedDownstreamFailure(fail_e.reason, None), None), add_d) assert(fail_d.id == add_d.id) - val Right(fail_c: UpdateFailHtlc) = buildHtlcFailure(priv_c.privateKey, CMD_FAIL_HTLC(add_c.id, FailureReason.EncryptedDownstreamFailure(fail_d.reason)), add_c) + val Right(fail_c: UpdateFailHtlc) = buildHtlcFailure(priv_c.privateKey, useAttributableFailures = false, CMD_FAIL_HTLC(add_c.id, FailureReason.EncryptedDownstreamFailure(fail_d.reason, None), None), add_c) assert(fail_c.id == add_c.id) - val Right(fail_b: UpdateFailHtlc) = buildHtlcFailure(priv_b.privateKey, CMD_FAIL_HTLC(add_b.id, FailureReason.EncryptedDownstreamFailure(fail_c.reason)), add_b) + val Right(fail_b: UpdateFailHtlc) = buildHtlcFailure(priv_b.privateKey, useAttributableFailures = false, CMD_FAIL_HTLC(add_b.id, FailureReason.EncryptedDownstreamFailure(fail_c.reason, None), None), add_b) assert(fail_b.id == add_b.id) - val Right(Sphinx.DecryptedFailurePacket(failingNode, decryptedFailure)) = Sphinx.FailurePacket.decrypt(fail_b.reason, payment.sharedSecrets) + val Right(Sphinx.DecryptedFailurePacket(failingNode, decryptedFailure)) = Sphinx.FailurePacket.decrypt(fail_b.reason, fail_b.attribution_opt, payment.sharedSecrets).failure + assert(failingNode == e) + assert(decryptedFailure == failure) + } + + test("build htlc failure onion with attribution data") { + // a -> b -> c -> d -> e + val recipient = ClearRecipient(e, Features.empty, finalAmount, finalExpiry, paymentSecret) + val Right(payment) = buildOutgoingPayment(TestConstants.emptyOrigin, paymentHash, Route(finalAmount, hops, None), recipient, Reputation.Score.max) + val add_b = UpdateAddHtlc(randomBytes32(), 0, amount_ab, paymentHash, expiry_ab, payment.cmd.onion, None, Reputation.maxEndorsement, None) + val Right(ChannelRelayPacket(_, _, packet_c, _)) = decrypt(add_b, priv_b.privateKey, Features.empty) + val add_c = UpdateAddHtlc(randomBytes32(), 1, amount_bc, paymentHash, expiry_bc, packet_c, None, Reputation.maxEndorsement, None) + val Right(ChannelRelayPacket(_, _, packet_d, _)) = decrypt(add_c, priv_c.privateKey, Features.empty) + val add_d = UpdateAddHtlc(randomBytes32(), 2, amount_cd, paymentHash, expiry_cd, packet_d, None, Reputation.maxEndorsement, None) + val Right(ChannelRelayPacket(_, _, packet_e, _)) = decrypt(add_d, priv_d.privateKey, Features.empty) + val add_e = UpdateAddHtlc(randomBytes32(), 3, amount_de, paymentHash, expiry_de, packet_e, None, Reputation.maxEndorsement, None) + val Right(FinalPacket(_, payload_e, _)) = decrypt(add_e, priv_e.privateKey, Features.empty) + assert(payload_e.isInstanceOf[FinalPayload.Standard]) + + // e returns a failure + val failure = IncorrectOrUnknownPaymentDetails(finalAmount, BlockHeight(currentBlockCount)) + val Right(fail_e: UpdateFailHtlc) = buildHtlcFailure(priv_e.privateKey, useAttributableFailures = true, CMD_FAIL_HTLC(add_e.id, FailureReason.LocalFailure(failure), Some(FailureAttributionData(TimestampMilli(672), None))), add_e, now = TimestampMilli(735)) + assert(fail_e.id == add_e.id) + val Right(fail_d: UpdateFailHtlc) = buildHtlcFailure(priv_d.privateKey, useAttributableFailures = true, CMD_FAIL_HTLC(add_d.id, FailureReason.EncryptedDownstreamFailure(fail_e.reason, fail_e.attribution_opt), Some(FailureAttributionData(TimestampMilli(349), None))), add_d, now = TimestampMilli(844)) + assert(fail_d.id == add_d.id) + val Right(fail_c: UpdateFailHtlc) = buildHtlcFailure(priv_c.privateKey, useAttributableFailures = true, CMD_FAIL_HTLC(add_c.id, FailureReason.EncryptedDownstreamFailure(fail_d.reason, fail_d.attribution_opt), Some(FailureAttributionData(TimestampMilli(295), None))), add_c, now = TimestampMilli(912)) + assert(fail_c.id == add_c.id) + val Right(fail_b: UpdateFailHtlc) = buildHtlcFailure(priv_b.privateKey, useAttributableFailures = true, CMD_FAIL_HTLC(add_b.id, FailureReason.EncryptedDownstreamFailure(fail_c.reason, fail_c.attribution_opt), Some(FailureAttributionData(TimestampMilli(0), None))), add_b, now = TimestampMilli(1265)) + assert(fail_b.id == add_b.id) + val htlcFailure = Sphinx.FailurePacket.decrypt(fail_b.reason, fail_b.attribution_opt, payment.sharedSecrets) + assert(htlcFailure.holdTimes == Seq(HoldTime(1200 milliseconds, b), HoldTime(600 milliseconds, c), HoldTime(400 milliseconds, d), HoldTime(0 milliseconds, e))) + val Right(Sphinx.DecryptedFailurePacket(failingNode, decryptedFailure)) = htlcFailure.failure assert(failingNode == e) assert(decryptedFailure == failure) } @@ -672,35 +706,35 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { test("build htlc failure onion (blinded payment)") { // a -> b -> c -> d -> e, blinded after c val (_, route, recipient) = longBlindedHops(hex"0451") - val Right(payment) = buildOutgoingPayment(TestConstants.emptyOrigin, paymentHash, route, recipient, 1.0) - val add_b = UpdateAddHtlc(randomBytes32(), 0, payment.cmd.amount, payment.cmd.paymentHash, payment.cmd.cltvExpiry, payment.cmd.onion, payment.cmd.nextPathKey_opt, 1.0, payment.cmd.fundingFee_opt) - val Right(ChannelRelayPacket(_, _, packet_c)) = decrypt(add_b, priv_b.privateKey, Features.empty) - val add_c = UpdateAddHtlc(randomBytes32(), 1, amount_bc, paymentHash, expiry_bc, packet_c, None, 1.0, None) - val Right(ChannelRelayPacket(_, payload_c, packet_d)) = decrypt(add_c, priv_c.privateKey, Features(RouteBlinding -> Optional)) + val Right(payment) = buildOutgoingPayment(TestConstants.emptyOrigin, paymentHash, route, recipient, Reputation.Score.max) + val add_b = UpdateAddHtlc(randomBytes32(), 0, payment.cmd.amount, payment.cmd.paymentHash, payment.cmd.cltvExpiry, payment.cmd.onion, payment.cmd.nextPathKey_opt, Reputation.maxEndorsement, payment.cmd.fundingFee_opt) + val Right(ChannelRelayPacket(_, _, packet_c, _)) = decrypt(add_b, priv_b.privateKey, Features.empty) + val add_c = UpdateAddHtlc(randomBytes32(), 1, amount_bc, paymentHash, expiry_bc, packet_c, None, Reputation.maxEndorsement, None) + val Right(ChannelRelayPacket(_, payload_c, packet_d, _)) = decrypt(add_c, priv_c.privateKey, Features(RouteBlinding -> Optional)) val pathKey_d = payload_c.asInstanceOf[IntermediatePayload.ChannelRelay.Blinded].nextPathKey - val add_d = UpdateAddHtlc(randomBytes32(), 2, amount_cd, paymentHash, expiry_cd, packet_d, Some(pathKey_d), 1.0, None) - val Right(ChannelRelayPacket(_, payload_d, packet_e)) = decrypt(add_d, priv_d.privateKey, Features(RouteBlinding -> Optional)) + val add_d = UpdateAddHtlc(randomBytes32(), 2, amount_cd, paymentHash, expiry_cd, packet_d, Some(pathKey_d), Reputation.maxEndorsement, None) + val Right(ChannelRelayPacket(_, payload_d, packet_e, _)) = decrypt(add_d, priv_d.privateKey, Features(RouteBlinding -> Optional)) val pathKey_e = payload_d.asInstanceOf[IntermediatePayload.ChannelRelay.Blinded].nextPathKey - val add_e = UpdateAddHtlc(randomBytes32(), 3, amount_de, paymentHash, expiry_de, packet_e, Some(pathKey_e), 1.0, None) - val Right(FinalPacket(_, payload_e)) = decrypt(add_e, priv_e.privateKey, Features(RouteBlinding -> Optional)) + val add_e = UpdateAddHtlc(randomBytes32(), 3, amount_de, paymentHash, expiry_de, packet_e, Some(pathKey_e), Reputation.maxEndorsement, None) + val Right(FinalPacket(_, payload_e, _)) = decrypt(add_e, priv_e.privateKey, Features(RouteBlinding -> Optional)) assert(payload_e.isInstanceOf[FinalPayload.Blinded]) // nodes after the introduction node cannot send `update_fail_htlc` messages - val Right(fail_e: UpdateFailMalformedHtlc) = buildHtlcFailure(priv_e.privateKey, CMD_FAIL_HTLC(add_e.id, FailureReason.LocalFailure(TemporaryNodeFailure())), add_e) + val Right(fail_e: UpdateFailMalformedHtlc) = buildHtlcFailure(priv_e.privateKey, useAttributableFailures = false, CMD_FAIL_HTLC(add_e.id, FailureReason.LocalFailure(TemporaryNodeFailure()), None), add_e) assert(fail_e.id == add_e.id) assert(fail_e.onionHash == Sphinx.hash(add_e.onionRoutingPacket)) assert(fail_e.failureCode == InvalidOnionBlinding(fail_e.onionHash).code) - val Right(fail_d: UpdateFailMalformedHtlc) = buildHtlcFailure(priv_d.privateKey, CMD_FAIL_HTLC(add_d.id, FailureReason.LocalFailure(UnknownNextPeer())), add_d) + val Right(fail_d: UpdateFailMalformedHtlc) = buildHtlcFailure(priv_d.privateKey, useAttributableFailures = false, CMD_FAIL_HTLC(add_d.id, FailureReason.LocalFailure(UnknownNextPeer()), None), add_d) assert(fail_d.id == add_d.id) assert(fail_d.onionHash == Sphinx.hash(add_d.onionRoutingPacket)) assert(fail_d.failureCode == InvalidOnionBlinding(fail_d.onionHash).code) // only the introduction node is allowed to send an `update_fail_htlc` message val failure = InvalidOnionBlinding(Sphinx.hash(add_c.onionRoutingPacket)) - val Right(fail_c: UpdateFailHtlc) = buildHtlcFailure(priv_c.privateKey, CMD_FAIL_HTLC(add_c.id, FailureReason.LocalFailure(failure)), add_c) + val Right(fail_c: UpdateFailHtlc) = buildHtlcFailure(priv_c.privateKey, useAttributableFailures = false, CMD_FAIL_HTLC(add_c.id, FailureReason.LocalFailure(failure), None), add_c) assert(fail_c.id == add_c.id) - val Right(fail_b: UpdateFailHtlc) = buildHtlcFailure(priv_b.privateKey, CMD_FAIL_HTLC(add_b.id, FailureReason.EncryptedDownstreamFailure(fail_c.reason)), add_b) + val Right(fail_b: UpdateFailHtlc) = buildHtlcFailure(priv_b.privateKey, useAttributableFailures = false, CMD_FAIL_HTLC(add_b.id, FailureReason.EncryptedDownstreamFailure(fail_c.reason, None), None), add_b) assert(fail_b.id == add_b.id) - val Right(Sphinx.DecryptedFailurePacket(failingNode, decryptedFailure)) = Sphinx.FailurePacket.decrypt(fail_b.reason, payment.sharedSecrets) + val Right(Sphinx.DecryptedFailurePacket(failingNode, decryptedFailure)) = Sphinx.FailurePacket.decrypt(fail_b.reason, fail_b.attribution_opt, payment.sharedSecrets).failure assert(failingNode == c) assert(decryptedFailure == failure) } @@ -711,23 +745,24 @@ object PaymentPacketSpec { def makeCommitments(channelId: ByteVector32, testAvailableBalanceForSend: MilliSatoshi = 50000000 msat, testAvailableBalanceForReceive: MilliSatoshi = 50000000 msat, testCapacity: Satoshi = 100000 sat, channelFeatures: ChannelFeatures = ChannelFeatures(), announcement_opt: Option[ChannelAnnouncement] = None): Commitments = { val channelReserve = testCapacity * 0.01 - val localParams = LocalParams(null, null, null, Long.MaxValue.msat, Some(channelReserve), null, null, 0, isChannelOpener = true, paysCommitTxFees = true, None, None, Features.empty) - val remoteParams = RemoteParams(randomKey().publicKey, null, UInt64.MaxValue, Some(channelReserve), null, null, maxAcceptedHtlcs = 0, null, null, null, null, null, None) + val localChannelParams = LocalChannelParams(null, null, Some(channelReserve), isChannelOpener = true, paysCommitTxFees = true, None, Features.empty) + val remoteChannelParams = RemoteChannelParams(randomKey().publicKey, Some(channelReserve), null, null, null, null, null, None) + val commitParams = CommitParams(546 sat, 1 msat, UInt64.MaxValue, 30, CltvExpiryDelta(720)) val fundingTx = Transaction(2, Nil, Seq(TxOut(testCapacity, Nil)), 0) - val commitInput = InputInfo(OutPoint(fundingTx, 0), fundingTx.txOut.head, Nil) - val localCommit = LocalCommit(0, null, CommitTxAndRemoteSig(Transactions.CommitTx(commitInput, null), RemoteSignature.FullSignature(null)), Nil) - val remoteCommit = RemoteCommit(0, null, null, randomKey().publicKey) + val localCommit = LocalCommit(0, null, randomTxId(), IndividualSignature(ByteVector64.Zeroes), Nil) + val remoteCommit = RemoteCommit(0, null, randomTxId(), randomKey().publicKey) val localChanges = LocalChanges(Nil, Nil, Nil) val remoteChanges = RemoteChanges(Nil, Nil, Nil) val localFundingStatus = announcement_opt match { - case Some(ann) => LocalFundingStatus.ConfirmedFundingTx(fundingTx, ann.shortChannelId, None, None) + case Some(ann) => LocalFundingStatus.ConfirmedFundingTx(Nil, fundingTx.txOut.head, ann.shortChannelId, None, None) case None => LocalFundingStatus.SingleFundedUnconfirmedFundingTx(None) } val channelFlags = ChannelFlags(announceChannel = announcement_opt.nonEmpty) + val commitmentFormat = ZeroFeeHtlcTxAnchorOutputsCommitmentFormat new Commitments( - ChannelParams(channelId, ChannelConfig.standard, channelFeatures, localParams, remoteParams, channelFlags), + ChannelParams(channelId, ChannelConfig.standard, channelFeatures, localChannelParams, remoteChannelParams, channelFlags), CommitmentChanges(localChanges, remoteChanges, 0, 0), - List(Commitment(0, 0, null, localFundingStatus, RemoteFundingStatus.Locked, localCommit, remoteCommit, None)), + List(Commitment(0, 0, OutPoint(fundingTx, 0), testCapacity, randomKey().publicKey, localFundingStatus, RemoteFundingStatus.Locked, commitmentFormat, commitParams, localCommit, commitParams, remoteCommit, None)), inactive = Nil, Right(randomKey().publicKey), ShaChain.init, @@ -784,7 +819,7 @@ object PaymentPacketSpec { val blindedRoute = BlindedRouteCreation.createBlindedRouteFromHops(Nil, b, hex"deadbeef", 1.msat, routeExpiry).route val finalPayload = NodePayload(blindedRoute.firstNode.blindedPublicKey, OutgoingBlindedPerHopPayload.createFinalPayload(finalAmount, finalAmount, finalExpiry, blindedRoute.firstNode.encryptedPayload)) val onion = buildOnion(Seq(finalPayload), paymentHash, Some(PaymentOnionCodecs.paymentOnionPayloadLength)).toOption.get // BOLT 2 requires that associatedData == paymentHash - val cmd = CMD_ADD_HTLC(ActorRef.noSender, finalAmount, paymentHash, finalExpiry, onion.packet, Some(blindedRoute.firstPathKey), 1.0, None, TestConstants.emptyOrigin, commit = true) + val cmd = CMD_ADD_HTLC(ActorRef.noSender, finalAmount, paymentHash, finalExpiry, onion.packet, Some(blindedRoute.firstPathKey), Reputation.Score.max, None, TestConstants.emptyOrigin, commit = true) Right(OutgoingPaymentPacket(cmd, channelUpdate_ab.shortChannelId, onion.sharedSecrets)) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PostRestartHtlcCleanerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PostRestartHtlcCleanerSpec.scala index 8133fafc34..fe15c42619 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PostRestartHtlcCleanerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PostRestartHtlcCleanerSpec.scala @@ -32,9 +32,9 @@ import fr.acinq.eclair.payment.PaymentPacketSpec._ import fr.acinq.eclair.payment.relay.OnTheFlyFundingSpec._ import fr.acinq.eclair.payment.relay.{OnTheFlyFunding, PostRestartHtlcCleaner, Relayer} import fr.acinq.eclair.payment.send.SpontaneousRecipient +import fr.acinq.eclair.reputation.Reputation import fr.acinq.eclair.router.BaseRouterSpec.channelHopFromUpdate import fr.acinq.eclair.router.Router.Route -import fr.acinq.eclair.transactions.Transactions.{ClaimRemoteDelayedOutputTx, InputInfo} import fr.acinq.eclair.transactions.{DirectedHtlc, IncomingHtlc, OutgoingHtlc} import fr.acinq.eclair.wire.internal.channel.{ChannelCodecs, ChannelCodecsSpec} import fr.acinq.eclair.wire.protocol._ @@ -57,7 +57,7 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit case class FixtureParam(nodeParams: NodeParams, register: TestProbe, sender: TestProbe, eventListener: TestProbe) { def createRelayer(nodeParams1: NodeParams): (ActorRef, ActorRef) = { - val relayer = system.actorOf(Relayer.props(nodeParams1, TestProbe().ref, register.ref, TestProbe().ref)) + val relayer = system.actorOf(Relayer.props(nodeParams1, TestProbe().ref, register.ref, TestProbe().ref, None)) // we need ensure the post-htlc-restart child actor is initialized sender.send(relayer, Relayer.GetChildActors(sender.ref)) (relayer, sender.expectMsgType[Relayer.ChildActors].postRestartCleaner) @@ -122,7 +122,7 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit // channel 1 goes to NORMAL state: system.eventStream.publish(ChannelStateChanged(channel.ref, channels.head.commitments.channelId, system.deadLetters, a, OFFLINE, NORMAL, Some(channels.head.commitments))) channel.expectMsgAllOf( - CMD_FAIL_HTLC(1, FailureReason.LocalFailure(TemporaryNodeFailure()), commit = true), + CMD_FAIL_HTLC(1, FailureReason.LocalFailure(TemporaryNodeFailure()), None, commit = true), CMD_FAIL_MALFORMED_HTLC(4, ByteVector32.Zeroes, FailureMessageCodecs.BADONION | FailureMessageCodecs.PERM | 24, commit = true) ) channel.expectNoMessage(100 millis) @@ -130,15 +130,15 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit // channel 2 goes to NORMAL state: system.eventStream.publish(ChannelStateChanged(channel.ref, channels(1).commitments.channelId, system.deadLetters, a, OFFLINE, NORMAL, Some(channels(1).commitments))) channel.expectMsgAllOf( - CMD_FAIL_HTLC(0, FailureReason.LocalFailure(TemporaryNodeFailure()), commit = true), - CMD_FAIL_HTLC(4, FailureReason.LocalFailure(TemporaryNodeFailure()), commit = true) + CMD_FAIL_HTLC(0, FailureReason.LocalFailure(TemporaryNodeFailure()), None, commit = true), + CMD_FAIL_HTLC(4, FailureReason.LocalFailure(TemporaryNodeFailure()), None, commit = true) ) channel.expectNoMessage(100 millis) // let's assume that channel 1 was disconnected before having signed the fails, and gets connected again: system.eventStream.publish(ChannelStateChanged(channel.ref, channels.head.channelId, system.deadLetters, a, OFFLINE, NORMAL, Some(channels.head.commitments))) channel.expectMsgAllOf( - CMD_FAIL_HTLC(1, FailureReason.LocalFailure(TemporaryNodeFailure()), commit = true), + CMD_FAIL_HTLC(1, FailureReason.LocalFailure(TemporaryNodeFailure()), None, commit = true), CMD_FAIL_MALFORMED_HTLC(4, ByteVector32.Zeroes, FailureMessageCodecs.BADONION | FailureMessageCodecs.PERM | 24, commit = true) ) channel.expectNoMessage(100 millis) @@ -173,9 +173,9 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit ) // The HTLCs were not relayed yet, but they match pending on-the-fly funding proposals. - val upstreamChannel = Upstream.Hot.Channel(htlc_ab_1.head.add, TimestampMilli.now(), a) + val upstreamChannel = Upstream.Hot.Channel(htlc_ab_1.head.add, TimestampMilli.now(), a, 0.1) val downstreamChannel = OnTheFlyFunding.Pending(Seq(OnTheFlyFunding.Proposal(createWillAdd(100_000 msat, channelPaymentHash, CltvExpiry(500)), upstreamChannel, Nil)), createStatus()) - val upstreamTrampoline = Upstream.Hot.Trampoline(List(htlc_ab_1.last, htlc_ab_2.head).map(htlc => Upstream.Hot.Channel(htlc.add, TimestampMilli.now(), a))) + val upstreamTrampoline = Upstream.Hot.Trampoline(List(htlc_ab_1.last, htlc_ab_2.head).map(htlc => Upstream.Hot.Channel(htlc.add, TimestampMilli.now(), a, 0.1))) val downstreamTrampoline = OnTheFlyFunding.Pending(Seq(OnTheFlyFunding.Proposal(createWillAdd(100_000 msat, trampolinePaymentHash, CltvExpiry(500)), upstreamTrampoline, Nil)), createStatus()) nodeParams.db.liquidity.addPendingOnTheFlyFunding(randomKey().publicKey, downstreamChannel) nodeParams.db.liquidity.addPendingOnTheFlyFunding(randomKey().publicKey, downstreamTrampoline) @@ -225,10 +225,10 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit // channel 1 goes to NORMAL state: system.eventStream.publish(ChannelStateChanged(channel.ref, channels.head.channelId, system.deadLetters, a, OFFLINE, NORMAL, Some(channels.head.commitments))) val expected1 = Set( - CMD_FAIL_HTLC(0, FailureReason.LocalFailure(TemporaryNodeFailure()), commit = true), - CMD_FULFILL_HTLC(3, preimage, commit = true), - CMD_FULFILL_HTLC(5, preimage, commit = true), - CMD_FAIL_HTLC(7, FailureReason.LocalFailure(TemporaryNodeFailure()), commit = true) + CMD_FAIL_HTLC(0, FailureReason.LocalFailure(TemporaryNodeFailure()), None, commit = true), + CMD_FULFILL_HTLC(3, preimage, None, commit = true), + CMD_FULFILL_HTLC(5, preimage, None, commit = true), + CMD_FAIL_HTLC(7, FailureReason.LocalFailure(TemporaryNodeFailure()), None, commit = true) ) val received1 = expected1.map(_ => channel.expectMsgType[Command]) assert(received1 == expected1) @@ -237,10 +237,10 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit // channel 2 goes to NORMAL state: system.eventStream.publish(ChannelStateChanged(channel.ref, channels(1).channelId, system.deadLetters, a, OFFLINE, NORMAL, Some(channels(1).commitments))) val expected2 = Set( - CMD_FAIL_HTLC(1, FailureReason.LocalFailure(TemporaryNodeFailure()), commit = true), - CMD_FAIL_HTLC(3, FailureReason.LocalFailure(TemporaryNodeFailure()), commit = true), - CMD_FULFILL_HTLC(4, preimage, commit = true), - CMD_FAIL_HTLC(9, FailureReason.LocalFailure(TemporaryNodeFailure()), commit = true) + CMD_FAIL_HTLC(1, FailureReason.LocalFailure(TemporaryNodeFailure()), None, commit = true), + CMD_FAIL_HTLC(3, FailureReason.LocalFailure(TemporaryNodeFailure()), None, commit = true), + CMD_FULFILL_HTLC(4, preimage, None, commit = true), + CMD_FAIL_HTLC(9, FailureReason.LocalFailure(TemporaryNodeFailure()), None, commit = true) ) val received2 = expected2.map(_ => channel.expectMsgType[Command]) assert(received2 == expected2) @@ -374,9 +374,9 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit val htlc_upstream_1 = Seq(buildHtlcIn(0, channelId_ab_1, paymentHash1), buildHtlcIn(5, channelId_ab_1, paymentHash2)) val htlc_upstream_2 = Seq(buildHtlcIn(7, channelId_ab_2, paymentHash1), buildHtlcIn(9, channelId_ab_2, paymentHash2)) val htlc_upstream_3 = Seq(buildHtlcIn(11, randomBytes32(), paymentHash3)) - val upstream_1 = Upstream.Hot.Trampoline(Upstream.Hot.Channel(htlc_upstream_1.head.add, TimestampMilli(1687345927000L), a) :: Upstream.Hot.Channel(htlc_upstream_2.head.add, TimestampMilli(1687345967000L), a) :: Nil) - val upstream_2 = Upstream.Hot.Trampoline(Upstream.Hot.Channel(htlc_upstream_1(1).add, TimestampMilli(1687345902000L), a) :: Upstream.Hot.Channel(htlc_upstream_2(1).add, TimestampMilli(1687345999000L), a) :: Nil) - val upstream_3 = Upstream.Hot.Trampoline(Upstream.Hot.Channel(htlc_upstream_3.head.add, TimestampMilli(1687345980000L), a) :: Nil) + val upstream_1 = Upstream.Hot.Trampoline(Upstream.Hot.Channel(htlc_upstream_1.head.add, TimestampMilli(1687345927000L), a, 0.1) :: Upstream.Hot.Channel(htlc_upstream_2.head.add, TimestampMilli(1687345967000L), a, 0.2) :: Nil) + val upstream_2 = Upstream.Hot.Trampoline(Upstream.Hot.Channel(htlc_upstream_1(1).add, TimestampMilli(1687345902000L), a, 0.3) :: Upstream.Hot.Channel(htlc_upstream_2(1).add, TimestampMilli(1687345999000L), a, 0.4) :: Nil) + val upstream_3 = Upstream.Hot.Trampoline(Upstream.Hot.Channel(htlc_upstream_3.head.add, TimestampMilli(1687345980000L), a, 0.5) :: Nil) val data_upstream_1 = ChannelCodecsSpec.makeChannelDataNormal(htlc_upstream_1, Map.empty) val data_upstream_2 = ChannelCodecsSpec.makeChannelDataNormal(htlc_upstream_2, Map.empty) val data_upstream_3 = ChannelCodecsSpec.makeChannelDataNormal(htlc_upstream_3, Map.empty) @@ -423,14 +423,14 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit alice2bob.expectMsgType[CommitSig] } - val closingState = localClose(alice, alice2blockchain) + val (closingState, closingTxs) = localClose(alice, alice2blockchain, htlcTimeoutCount = 4) alice ! WatchTxConfirmedTriggered(BlockHeight(42), 0, closingState.commitTx) // All committed htlcs timed out except the last two; one will be fulfilled later and the other will timeout later. - assert(closingState.htlcTxs.size == 4) - assert(getHtlcTimeoutTxs(closingState).length == 4) - val htlcTxs = getHtlcTimeoutTxs(closingState).sortBy(_.tx.txOut.map(_.amount).sum) + assert(closingState.htlcOutputs.size == 4) + assert(closingTxs.htlcTxs.size == 4) + val htlcTxs = closingTxs.htlcTxs.sortBy(_.txOut.map(_.amount).sum) htlcTxs.reverse.drop(2).zipWithIndex.foreach { - case (htlcTx, i) => alice ! WatchTxConfirmedTriggered(BlockHeight(201), i, htlcTx.tx) + case (htlcTx, i) => alice ! WatchTxConfirmedTriggered(BlockHeight(201), i, htlcTx) } (alice.stateData.asInstanceOf[DATA_CLOSING], htlc_2_2) } @@ -447,8 +447,8 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit system.eventStream.publish(ChannelStateChanged(channel_upstream_3.ref, data_upstream_3.channelId, system.deadLetters, a, OFFLINE, NORMAL, Some(data_upstream_3.commitments))) // Payment 1 should fail instantly. - channel_upstream_1.expectMsg(CMD_FAIL_HTLC(0, FailureReason.LocalFailure(TemporaryNodeFailure()), commit = true)) - channel_upstream_2.expectMsg(CMD_FAIL_HTLC(7, FailureReason.LocalFailure(TemporaryNodeFailure()), commit = true)) + channel_upstream_1.expectMsg(CMD_FAIL_HTLC(0, FailureReason.LocalFailure(TemporaryNodeFailure()), None, commit = true)) + channel_upstream_2.expectMsg(CMD_FAIL_HTLC(7, FailureReason.LocalFailure(TemporaryNodeFailure()), None, commit = true)) channel_upstream_1.expectNoMessage(100 millis) channel_upstream_2.expectNoMessage(100 millis) @@ -456,8 +456,8 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit val origin_2 = Origin.Cold(Upstream.Cold(upstream_2)) sender.send(relayer, RES_ADD_SETTLED(origin_2, htlc_2_2, HtlcResult.OnChainFulfill(preimage2))) register.expectMsgAllOf( - Register.Forward(replyTo = null, channelId_ab_1, CMD_FULFILL_HTLC(5, preimage2, commit = true)), - Register.Forward(replyTo = null, channelId_ab_2, CMD_FULFILL_HTLC(9, preimage2, commit = true)) + Register.Forward(replyTo = null, channelId_ab_1, CMD_FULFILL_HTLC(5, preimage2, None, commit = true)), + Register.Forward(replyTo = null, channelId_ab_2, CMD_FULFILL_HTLC(9, preimage2, None, commit = true)) ) // Payment 3 should not be failed: we are still waiting for on-chain confirmation. @@ -474,8 +474,8 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit buildHtlcIn(4, channelId_ab_1, randomBytes32()), ) val channelData = ChannelCodecsSpec.makeChannelDataNormal(htlc_ab, Map.empty) - nodeParams.db.pendingCommands.addSettlementCommand(channelId_ab_1, CMD_FULFILL_HTLC(1, randomBytes32())) - nodeParams.db.pendingCommands.addSettlementCommand(channelId_ab_1, CMD_FAIL_HTLC(4, FailureReason.LocalFailure(PermanentChannelFailure()))) + nodeParams.db.pendingCommands.addSettlementCommand(channelId_ab_1, CMD_FULFILL_HTLC(1, randomBytes32(), None)) + nodeParams.db.pendingCommands.addSettlementCommand(channelId_ab_1, CMD_FAIL_HTLC(4, FailureReason.LocalFailure(PermanentChannelFailure()), None)) val (_, postRestart) = f.createRelayer(nodeParams) postRestart ! PostRestartHtlcCleaner.Init(List(channelData)) @@ -511,10 +511,8 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit val normal = ChannelCodecsSpec.makeChannelDataNormal(htlc_bc, origins) // NB: this isn't actually a revoked commit tx, but we don't check that here, if the channel says it's a revoked // commit we accept it as such, so it simplifies the test. - val revokedCommitTx = normal.commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx.copy(txOut = Seq(TxOut(4500 sat, Script.pay2wpkh(randomKey().publicKey)))) - val dummyClaimMainTx = Transaction(2, Seq(TxIn(OutPoint(revokedCommitTx, 0), Nil, 0)), Seq(revokedCommitTx.txOut.head.copy(amount = 4000 sat)), 0) - val dummyClaimMain = ClaimRemoteDelayedOutputTx(InputInfo(OutPoint(revokedCommitTx, 0), revokedCommitTx.txOut.head, Nil), dummyClaimMainTx) - val rcp = RevokedCommitPublished(revokedCommitTx, Some(dummyClaimMain), None, Nil, Nil, Map(revokedCommitTx.txIn.head.outPoint -> revokedCommitTx)) + val revokedCommitTx = Transaction(2, Seq(TxIn(normal.commitments.latest.fundingInput, Nil, 0)), Seq(TxOut(4500 sat, Script.pay2wpkh(randomKey().publicKey))), 0) + val rcp = RevokedCommitPublished(revokedCommitTx, Some(OutPoint(revokedCommitTx, 0)), None, Set.empty, Set.empty, Map(revokedCommitTx.txIn.head.outPoint -> revokedCommitTx)) DATA_CLOSING(normal.commitments, BlockHeight(0), Script.write(Script.pay2wpkh(randomKey().publicKey)), mutualCloseProposed = Nil, revokedCommitPublished = List(rcp)) } @@ -573,7 +571,7 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit register.expectNoMessage(100 millis) sender.send(relayer, buildForwardFulfill(testCase.downstream, testCase.upstream, preimage1)) - register.expectMsg(Register.Forward(null, testCase.upstream.originChannelId, CMD_FULFILL_HTLC(testCase.upstream.originHtlcId, preimage1, commit = true))) + register.expectMsg(Register.Forward(null, testCase.upstream.originChannelId, CMD_FULFILL_HTLC(testCase.upstream.originHtlcId, preimage1, None, commit = true))) eventListener.expectMsgType[ChannelPaymentRelayed] sender.send(relayer, buildForwardFulfill(testCase.downstream, testCase.upstream, preimage1)) @@ -593,7 +591,7 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit sender.send(relayer, buildForwardFail(testCase.downstream_1_1, testCase.upstream_1)) val fails = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]] :: register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]] :: Nil assert(fails.toSet == testCase.upstream_1.originHtlcs.map { - case Upstream.Cold.Channel(channelId, htlcId, _) => Register.Forward(null, channelId, CMD_FAIL_HTLC(htlcId, FailureReason.LocalFailure(TemporaryNodeFailure()), commit = true)) + case Upstream.Cold.Channel(channelId, htlcId, _) => Register.Forward(null, channelId, CMD_FAIL_HTLC(htlcId, FailureReason.LocalFailure(TemporaryNodeFailure()), None, commit = true)) }.toSet) sender.send(relayer, buildForwardFail(testCase.downstream_1_1, testCase.upstream_1)) @@ -605,7 +603,7 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit sender.send(relayer, buildForwardFail(testCase.downstream_2_3, testCase.upstream_2)) register.expectMsg(testCase.upstream_2.originHtlcs.map { - case Upstream.Cold.Channel(channelId, htlcId, _) => Register.Forward(null, channelId, CMD_FAIL_HTLC(htlcId, FailureReason.LocalFailure(TemporaryNodeFailure()), commit = true)) + case Upstream.Cold.Channel(channelId, htlcId, _) => Register.Forward(null, channelId, CMD_FAIL_HTLC(htlcId, FailureReason.LocalFailure(TemporaryNodeFailure()), None, commit = true)) }.head) register.expectNoMessage(100 millis) @@ -624,7 +622,7 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit sender.send(relayer, buildForwardFulfill(testCase.downstream_1_1, testCase.upstream_1, preimage1)) val fulfills = register.expectMsgType[Register.Forward[CMD_FULFILL_HTLC]] :: register.expectMsgType[Register.Forward[CMD_FULFILL_HTLC]] :: Nil assert(fulfills.toSet == testCase.upstream_1.originHtlcs.map { - case Upstream.Cold.Channel(channelId, htlcId, _) => Register.Forward(null, channelId, CMD_FULFILL_HTLC(htlcId, preimage1, commit = true)) + case Upstream.Cold.Channel(channelId, htlcId, _) => Register.Forward(null, channelId, CMD_FULFILL_HTLC(htlcId, preimage1, None, commit = true)) }.toSet) sender.send(relayer, buildForwardFulfill(testCase.downstream_1_1, testCase.upstream_1, preimage1)) @@ -633,7 +631,7 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit // This payment has 3 downstream HTLCs, but we should fulfill upstream as soon as we receive the preimage. sender.send(relayer, buildForwardFulfill(testCase.downstream_2_1, testCase.upstream_2, preimage2)) register.expectMsg(testCase.upstream_2.originHtlcs.map { - case Upstream.Cold.Channel(channelId, htlcId, _) => Register.Forward(null, channelId, CMD_FULFILL_HTLC(htlcId, preimage2, commit = true)) + case Upstream.Cold.Channel(channelId, htlcId, _) => Register.Forward(null, channelId, CMD_FULFILL_HTLC(htlcId, preimage2, None, commit = true)) }.head) sender.send(relayer, buildForwardFulfill(testCase.downstream_2_2, testCase.upstream_2, preimage2)) @@ -653,7 +651,7 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit sender.send(relayer, buildForwardFail(testCase.downstream_2_1, testCase.upstream_2)) sender.send(relayer, buildForwardFulfill(testCase.downstream_2_2, testCase.upstream_2, preimage2)) register.expectMsg(testCase.upstream_2.originHtlcs.map { - case Upstream.Cold.Channel(channelId, htlcId, _) => Register.Forward(null, channelId, CMD_FULFILL_HTLC(htlcId, preimage2, commit = true)) + case Upstream.Cold.Channel(channelId, htlcId, _) => Register.Forward(null, channelId, CMD_FULFILL_HTLC(htlcId, preimage2, None, commit = true)) }.head) sender.send(relayer, buildForwardFail(testCase.downstream_2_3, testCase.upstream_2)) @@ -671,7 +669,7 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit buildHtlcIn(2, channelId_ab_1, paymentHash2), // trampoline relayed ) // The first upstream HTLC was not relayed but has a pending on-the-fly funding proposal. - val downstreamChannel = OnTheFlyFunding.Pending(Seq(OnTheFlyFunding.Proposal(createWillAdd(100_000 msat, paymentHash1, CltvExpiry(500)), Upstream.Hot.Channel(htlc_ab(0).add, TimestampMilli.now(), a), Nil)), createStatus()) + val downstreamChannel = OnTheFlyFunding.Pending(Seq(OnTheFlyFunding.Proposal(createWillAdd(100_000 msat, paymentHash1, CltvExpiry(500)), Upstream.Hot.Channel(htlc_ab(0).add, TimestampMilli.now(), a, 0.1), Nil)), createStatus()) nodeParams.db.liquidity.addPendingOnTheFlyFunding(randomKey().publicKey, downstreamChannel) // The other two HTLCs were relayed after completing on-the-fly funding. val htlc_bc = Seq( @@ -736,7 +734,7 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit // Standard channel goes to NORMAL state: system.eventStream.publish(ChannelStateChanged(channel.ref, c.commitments.channelId, system.deadLetters, a, OFFLINE, NORMAL, Some(c.commitments))) - channel.expectMsg(CMD_FAIL_HTLC(1L, FailureReason.LocalFailure(TemporaryNodeFailure()), commit = true)) + channel.expectMsg(CMD_FAIL_HTLC(1L, FailureReason.LocalFailure(TemporaryNodeFailure()), None, commit = true)) channel.expectNoMessage(100 millis) } @@ -754,8 +752,8 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit } // @formatter:on - val cmd1 = CMD_FAIL_HTLC(id = 0L, reason = FailureReason.EncryptedDownstreamFailure(ByteVector.empty), replyTo_opt = None) - val cmd2 = CMD_FAIL_HTLC(id = 1L, reason = FailureReason.EncryptedDownstreamFailure(ByteVector.empty), replyTo_opt = None) + val cmd1 = CMD_FAIL_HTLC(id = 0L, reason = FailureReason.EncryptedDownstreamFailure(ByteVector.empty, None), None, replyTo_opt = None) + val cmd2 = CMD_FAIL_HTLC(id = 1L, reason = FailureReason.EncryptedDownstreamFailure(ByteVector.empty, None), None, replyTo_opt = None) val nodeParams1 = nodeParams.copy(pluginParams = List(pluginParams)) nodeParams1.db.pendingCommands.addSettlementCommand(channelId_ab_1, cmd1) nodeParams1.db.pendingCommands.addSettlementCommand(channelId_ab_1, cmd2) @@ -785,9 +783,9 @@ object PostRestartHtlcCleanerSpec { buildOutgoingBlindedPaymentAB(paymentHash) } else { val (route, recipient) = (Route(finalAmount, hops, None), SpontaneousRecipient(e, finalAmount, finalExpiry, randomBytes32())) - buildOutgoingPayment(TestConstants.emptyOrigin, paymentHash, route, recipient, 1.0) + buildOutgoingPayment(TestConstants.emptyOrigin, paymentHash, route, recipient, Reputation.Score.max) } - UpdateAddHtlc(channelId, htlcId, payment.cmd.amount, paymentHash, payment.cmd.cltvExpiry, payment.cmd.onion, payment.cmd.nextPathKey_opt, 1.0, payment.cmd.fundingFee_opt) + UpdateAddHtlc(channelId, htlcId, payment.cmd.amount, paymentHash, payment.cmd.cltvExpiry, payment.cmd.onion, payment.cmd.nextPathKey_opt, Reputation.maxEndorsement, payment.cmd.fundingFee_opt) } def buildHtlcIn(htlcId: Long, channelId: ByteVector32, paymentHash: ByteVector32, blinded: Boolean = false): DirectedHtlc = IncomingHtlc(buildHtlc(htlcId, channelId, paymentHash, blinded)) @@ -795,8 +793,8 @@ object PostRestartHtlcCleanerSpec { def buildHtlcOut(htlcId: Long, channelId: ByteVector32, paymentHash: ByteVector32, blinded: Boolean = false): DirectedHtlc = OutgoingHtlc(buildHtlc(htlcId, channelId, paymentHash, blinded)) def buildFinalHtlc(htlcId: Long, channelId: ByteVector32, paymentHash: ByteVector32): DirectedHtlc = { - val Right(payment) = buildOutgoingPayment(TestConstants.emptyOrigin, paymentHash, Route(finalAmount, Seq(channelHopFromUpdate(a, b, channelUpdate_ab)), None), SpontaneousRecipient(b, finalAmount, finalExpiry, randomBytes32()), 1.0) - IncomingHtlc(UpdateAddHtlc(channelId, htlcId, payment.cmd.amount, paymentHash, payment.cmd.cltvExpiry, payment.cmd.onion, None, 1.0, None)) + val Right(payment) = buildOutgoingPayment(TestConstants.emptyOrigin, paymentHash, Route(finalAmount, Seq(channelHopFromUpdate(a, b, channelUpdate_ab)), None), SpontaneousRecipient(b, finalAmount, finalExpiry, randomBytes32()), Reputation.Score.max) + IncomingHtlc(UpdateAddHtlc(channelId, htlcId, payment.cmd.amount, paymentHash, payment.cmd.cltvExpiry, payment.cmd.onion, None, Reputation.maxEndorsement, None)) } def buildForwardFail(add: UpdateAddHtlc, upstream: Upstream.Cold): RES_ADD_SETTLED[Origin.Cold, HtlcResult.Fail] = @@ -819,11 +817,11 @@ object PostRestartHtlcCleanerSpec { val parentId = UUID.randomUUID() val (id1, id2, id3) = (UUID.randomUUID(), UUID.randomUUID(), UUID.randomUUID()) - val add1 = UpdateAddHtlc(channelId_bc_1, 72, 561 msat, paymentHash1, CltvExpiry(4200), onionRoutingPacket = TestConstants.emptyOnionPacket, pathKey_opt = None, confidence = 1.0, fundingFee_opt = None) + val add1 = UpdateAddHtlc(channelId_bc_1, 72, 561 msat, paymentHash1, CltvExpiry(4200), onionRoutingPacket = TestConstants.emptyOnionPacket, pathKey_opt = None, endorsement = Reputation.maxEndorsement, fundingFee_opt = None) val origin1 = Origin.Cold(Upstream.Local(id1)) - val add2 = UpdateAddHtlc(channelId_bc_1, 75, 1105 msat, paymentHash2, CltvExpiry(4250), onionRoutingPacket = TestConstants.emptyOnionPacket, pathKey_opt = None, confidence = 1.0, fundingFee_opt = None) + val add2 = UpdateAddHtlc(channelId_bc_1, 75, 1105 msat, paymentHash2, CltvExpiry(4250), onionRoutingPacket = TestConstants.emptyOnionPacket, pathKey_opt = None, endorsement = Reputation.maxEndorsement, fundingFee_opt = None) val origin2 = Origin.Cold(Upstream.Local(id2)) - val add3 = UpdateAddHtlc(channelId_bc_1, 78, 1729 msat, paymentHash2, CltvExpiry(4300), onionRoutingPacket = TestConstants.emptyOnionPacket, pathKey_opt = None, confidence = 1.0, fundingFee_opt = None) + val add3 = UpdateAddHtlc(channelId_bc_1, 78, 1729 msat, paymentHash2, CltvExpiry(4300), onionRoutingPacket = TestConstants.emptyOnionPacket, pathKey_opt = None, endorsement = Reputation.maxEndorsement, fundingFee_opt = None) val origin3 = Origin.Cold(Upstream.Local(id3)) // Prepare channels and payment state before restart. @@ -932,10 +930,10 @@ object PostRestartHtlcCleanerSpec { val notRelayed = Set((1L, channelId_bc_1), (0L, channelId_bc_2), (3L, channelId_bc_3), (5L, channelId_bc_3), (7L, channelId_bc_4)) - val downstream_1_1 = UpdateAddHtlc(channelId_bc_1, 6L, finalAmount, paymentHash1, finalExpiry, TestConstants.emptyOnionPacket, None, 1.0, None) - val downstream_2_1 = UpdateAddHtlc(channelId_bc_1, 8L, finalAmount, paymentHash2, finalExpiry, TestConstants.emptyOnionPacket, None, 1.0, None) - val downstream_2_2 = UpdateAddHtlc(channelId_bc_2, 1L, finalAmount, paymentHash2, finalExpiry, TestConstants.emptyOnionPacket, None, 1.0, None) - val downstream_2_3 = UpdateAddHtlc(channelId_bc_3, 4L, finalAmount, paymentHash2, finalExpiry, TestConstants.emptyOnionPacket, None, 1.0, None) + val downstream_1_1 = UpdateAddHtlc(channelId_bc_1, 6L, finalAmount, paymentHash1, finalExpiry, TestConstants.emptyOnionPacket, None, Reputation.maxEndorsement, None) + val downstream_2_1 = UpdateAddHtlc(channelId_bc_1, 8L, finalAmount, paymentHash2, finalExpiry, TestConstants.emptyOnionPacket, None, Reputation.maxEndorsement, None) + val downstream_2_2 = UpdateAddHtlc(channelId_bc_2, 1L, finalAmount, paymentHash2, finalExpiry, TestConstants.emptyOnionPacket, None, Reputation.maxEndorsement, None) + val downstream_2_3 = UpdateAddHtlc(channelId_bc_3, 4L, finalAmount, paymentHash2, finalExpiry, TestConstants.emptyOnionPacket, None, Reputation.maxEndorsement, None) val data_ab_1 = ChannelCodecsSpec.makeChannelDataNormal(htlc_ab_1, Map.empty) val data_ab_2 = ChannelCodecsSpec.makeChannelDataNormal(htlc_ab_2, Map.empty) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/ChannelRelayerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/ChannelRelayerSpec.scala index 8828fe117e..525eaff283 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/ChannelRelayerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/ChannelRelayerSpec.scala @@ -35,6 +35,7 @@ import fr.acinq.eclair.io.{Peer, PeerReadyManager, Switchboard} import fr.acinq.eclair.payment.IncomingPaymentPacket.ChannelRelayPacket import fr.acinq.eclair.payment.relay.ChannelRelayer._ import fr.acinq.eclair.payment.{ChannelPaymentRelayed, IncomingPaymentPacket, PaymentPacketSpec} +import fr.acinq.eclair.reputation.{Reputation, ReputationRecorder} import fr.acinq.eclair.router.Announcements import fr.acinq.eclair.wire.protocol.BlindedRouteData.PaymentRelayData import fr.acinq.eclair.wire.protocol.PaymentOnion.IntermediatePayload @@ -55,8 +56,9 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a val wakeUpEnabled = "wake_up_enabled" val wakeUpTimeout = "wake_up_timeout" val onTheFlyFunding = "on_the_fly_funding" + val ignoreReputation = "ignore_reputation" - case class FixtureParam(nodeParams: NodeParams, channelRelayer: typed.ActorRef[ChannelRelayer.Command], register: TestProbe[Any]) { + case class FixtureParam(nodeParams: NodeParams, channelRelayer: typed.ActorRef[ChannelRelayer.Command], register: TestProbe[Any], reputationRecorder: TestProbe[ReputationRecorder.Command]) { def createWakeUpActors(): (TestProbe[PeerReadyManager.Register], TestProbe[Switchboard.GetPeerInfo]) = { val peerReadyManager = TestProbe[PeerReadyManager.Register]() system.receptionist ! Receptionist.Register(PeerReadyManager.PeerReadyManagerServiceKey, peerReadyManager.ref) @@ -69,6 +71,12 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a system.receptionist ! Receptionist.Deregister(PeerReadyManager.PeerReadyManagerServiceKey, peerReadyManager.ref) system.receptionist ! Receptionist.Deregister(Switchboard.SwitchboardServiceKey, switchboard.ref) } + + def receiveConfidence(score: Reputation.Score): Unit = { + val getConfidence = reputationRecorder.expectMessageType[ReputationRecorder.GetConfidence] + assert(getConfidence.upstream.asInstanceOf[Upstream.Hot.Channel].receivedFrom == TestConstants.Alice.nodeParams.nodeId) + getConfidence.replyTo ! score + } } override def withFixture(test: OneArgTest): Outcome = { @@ -78,17 +86,18 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a .modify(_.peerWakeUpConfig.timeout).setToIf(test.tags.contains(wakeUpTimeout))(100 millis) .modify(_.features.activated).usingIf(test.tags.contains(onTheFlyFunding))(_ + (Features.OnTheFlyFunding -> FeatureSupport.Optional)) val register = TestProbe[Any]("register") - val channelRelayer = testKit.spawn(ChannelRelayer.apply(nodeParams, register.ref.toClassic)) + val reputationRecorder = TestProbe[ReputationRecorder.Command]("reputation-recorder") + val channelRelayer = testKit.spawn(ChannelRelayer.apply(nodeParams, register.ref.toClassic, if (test.tags.contains(ignoreReputation)) None else Some(reputationRecorder.ref))) try { - withFixture(test.toNoArgTest(FixtureParam(nodeParams, channelRelayer, register))) + withFixture(test.toNoArgTest(FixtureParam(nodeParams, channelRelayer, register, reputationRecorder))) } finally { testKit.stop(channelRelayer) } } - def expectFwdFail(register: TestProbe[Any], channelId: ByteVector32, cmd: channel.Command): Register.Forward[channel.Command] = { - val fwd = register.expectMessageType[Register.Forward[channel.Command]] - assert(fwd.message == cmd) + def expectFwdFail(register: TestProbe[Any], channelId: ByteVector32, cmd: CMD_FAIL_HTLC): Register.Forward[CMD_FAIL_HTLC] = { + val fwd = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]] + assert(fwd.message.copy(attribution_opt = None) == cmd.copy(attribution_opt = None)) assert(fwd.channelId == channelId) fwd } @@ -98,7 +107,7 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a inside(fwd.message) { case add: CMD_ADD_HTLC => assert(add.amount == outAmount) assert(add.cltvExpiry == outExpiry) - assert((add.confidence * 7.999).toInt == outEndorsement) + assert(add.reputationScore.endorsement == outEndorsement) } assert(fwd.channelId == channelId) fwd @@ -111,44 +120,44 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a val r = createValidIncomingPacket(payload) channelRelayer ! WrappedLocalChannelUpdate(lcu) - channelRelayer ! Relay(r, TestConstants.Alice.nodeParams.nodeId) + channelRelayer ! Relay(r, TestConstants.Alice.nodeParams.nodeId, 0.1) if (success) { expectFwdAdd(register, lcu.channelId, outgoingAmount, outgoingExpiry, 7) } else { - expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, FailureReason.LocalFailure(UnknownNextPeer()), commit = true)) + expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, FailureReason.LocalFailure(UnknownNextPeer()), None, commit = true)) } } - test("relay with real scid (channel update uses real scid)") { f => + test("relay with real scid (channel update uses real scid)", Tag(ignoreReputation)) { f => basicRelayTest(f)(relayPayloadScid = realScid1, lcu = createLocalUpdate(channelId1), success = true) } - test("relay with real scid (channel update uses local alias)") { f => + test("relay with real scid (channel update uses local alias)", Tag(ignoreReputation)) { f => basicRelayTest(f)(relayPayloadScid = realScid1, lcu = createLocalUpdate(channelId1, channelUpdateScid_opt = Some(localAlias1)), success = true) } - test("relay with local alias (channel update uses real scid)") { f => + test("relay with local alias (channel update uses real scid)", Tag(ignoreReputation)) { f => basicRelayTest(f)(relayPayloadScid = localAlias1, lcu = createLocalUpdate(channelId1), success = true) } - test("relay with local alias (channel update uses local alias)") { f => + test("relay with local alias (channel update uses local alias)", Tag(ignoreReputation)) { f => basicRelayTest(f)(relayPayloadScid = localAlias1, lcu = createLocalUpdate(channelId1, channelUpdateScid_opt = Some(localAlias1)), success = true) } - test("fail to relay with real scid when option_scid_alias is enabled (channel update uses real scid)") { f => + test("fail to relay with real scid when option_scid_alias is enabled (channel update uses real scid)", Tag(ignoreReputation)) { f => basicRelayTest(f)(relayPayloadScid = realScid1, lcu = createLocalUpdate(channelId1, optionScidAlias = true), success = false) } - test("fail to relay with real scid when option_scid_alias is enabled (channel update uses local alias)") { f => + test("fail to relay with real scid when option_scid_alias is enabled (channel update uses local alias)", Tag(ignoreReputation)) { f => basicRelayTest(f)(relayPayloadScid = realScid1, lcu = createLocalUpdate(channelId1, optionScidAlias = true, channelUpdateScid_opt = Some(localAlias1)), success = false) } - test("relay with local alias when option_scid_alias is enabled (channel update uses real scid)") { f => + test("relay with local alias when option_scid_alias is enabled (channel update uses real scid)", Tag(ignoreReputation)) { f => basicRelayTest(f)(relayPayloadScid = localAlias1, lcu = createLocalUpdate(channelId1, optionScidAlias = true), success = true) } - test("relay with local alias when option_scid_alias is enabled (channel update uses local alias)") { f => + test("relay with local alias when option_scid_alias is enabled (channel update uses local alias)", Tag(ignoreReputation)) { f => basicRelayTest(f)(relayPayloadScid = localAlias1, lcu = createLocalUpdate(channelId1, optionScidAlias = true, channelUpdateScid_opt = Some(localAlias1)), success = true) } @@ -160,7 +169,8 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a val r = createValidIncomingPacket(payload, outgoingAmount + u.channelUpdate.feeBaseMsat, outgoingExpiry + u.channelUpdate.cltvExpiryDelta) channelRelayer ! WrappedLocalChannelUpdate(u) - channelRelayer ! Relay(r, TestConstants.Alice.nodeParams.nodeId) + channelRelayer ! Relay(r, TestConstants.Alice.nodeParams.nodeId, 0.1) + receiveConfidence(Reputation.Score.fromEndorsement(7)) expectFwdAdd(register, channelIds(realScid1), outgoingAmount, outgoingExpiry, 7) } @@ -175,14 +185,15 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a val r = createValidIncomingPacket(payload, outgoingAmount + u.channelUpdate.feeBaseMsat, outgoingExpiry + u.channelUpdate.cltvExpiryDelta) channelRelayer ! WrappedLocalChannelUpdate(u) - channelRelayer ! Relay(r, TestConstants.Alice.nodeParams.nodeId) + channelRelayer ! Relay(r, TestConstants.Alice.nodeParams.nodeId, 0.1) + receiveConfidence(Reputation.Score.fromEndorsement(6)) // We try to wake-up the next node. peerReadyManager.expectMessageType[PeerReadyManager.Register].replyTo ! PeerReadyManager.Registered(outgoingNodeId, otherAttempts = 0) val wakeUp = switchboard.expectMessageType[Switchboard.GetPeerInfo] assert(wakeUp.remoteNodeId == outgoingNodeId) wakeUp.replyTo ! Peer.PeerInfo(TestProbe[Any]().ref.toClassic, outgoingNodeId, Peer.CONNECTED, Some(nodeParams.features.initFeatures()), None, Set.empty) - expectFwdAdd(register, channelIds(realScid1), outgoingAmount, outgoingExpiry, 7) + expectFwdAdd(register, channelIds(realScid1), outgoingAmount, outgoingExpiry, 6) }) cleanUpWakeUpActors(peerReadyManager, switchboard) @@ -198,7 +209,8 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a val r = createValidIncomingPacket(payload, outgoingAmount + u.channelUpdate.feeBaseMsat, outgoingExpiry + u.channelUpdate.cltvExpiryDelta) channelRelayer ! WrappedLocalChannelUpdate(u) - channelRelayer ! Relay(r, TestConstants.Alice.nodeParams.nodeId) + channelRelayer ! Relay(r, TestConstants.Alice.nodeParams.nodeId, 0.1) + receiveConfidence(Reputation.Score.fromEndorsement(5)) // We try to wake-up the next node. val wakeUp = peerReadyManager.expectMessageType[PeerReadyManager.Register] @@ -210,7 +222,7 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a cleanUpWakeUpActors(peerReadyManager, switchboard) // We try to use existing channels, but they don't have enough liquidity. - val fwd = expectFwdAdd(register, channelIds(realScid1), outgoingAmount, outgoingExpiry, 7) + val fwd = expectFwdAdd(register, channelIds(realScid1), outgoingAmount, outgoingExpiry, 5) fwd.message.replyTo ! RES_ADD_FAILED(fwd.message, InsufficientFunds(channelIds(realScid1), outgoingAmount, 100 sat, 0 sat, 0 sat), Some(u.channelUpdate)) val fwdNodeId = register.expectMessageType[ForwardNodeId[Peer.ProposeOnTheFlyFunding]] @@ -229,7 +241,8 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a val payload = createBlindedPayload(Left(outgoingNodeId), u.channelUpdate, isIntroduction = false) val r = createValidIncomingPacket(payload, outgoingAmount + u.channelUpdate.feeBaseMsat, outgoingExpiry + u.channelUpdate.cltvExpiryDelta) - channelRelayer ! Relay(r, TestConstants.Alice.nodeParams.nodeId) + channelRelayer ! Relay(r, TestConstants.Alice.nodeParams.nodeId, 0.1) + receiveConfidence(Reputation.Score.max) // We try to wake-up the next node. peerReadyManager.expectMessageType[PeerReadyManager.Register].replyTo ! PeerReadyManager.Registered(outgoingNodeId, otherAttempts = 1) @@ -240,7 +253,7 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a cleanUpWakeUpActors(peerReadyManager, switchboard) // We fail without attempting on-the-fly funding. - expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, FailureReason.LocalFailure(UnknownNextPeer()), commit = true)) + expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, FailureReason.LocalFailure(UnknownNextPeer()), None, commit = true)) } test("relay blinded payment (on-the-fly funding failed)", Tag(wakeUpEnabled), Tag(onTheFlyFunding)) { f => @@ -252,7 +265,8 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a val payload = createBlindedPayload(Left(outgoingNodeId), u.channelUpdate, isIntroduction = false) val r = createValidIncomingPacket(payload, outgoingAmount + u.channelUpdate.feeBaseMsat, outgoingExpiry + u.channelUpdate.cltvExpiryDelta) - channelRelayer ! Relay(r, TestConstants.Alice.nodeParams.nodeId) + channelRelayer ! Relay(r, TestConstants.Alice.nodeParams.nodeId, 0.1) + receiveConfidence(Reputation.Score.max) // We try to wake-up the next node. peerReadyManager.expectMessageType[PeerReadyManager.Register].replyTo ! PeerReadyManager.Registered(outgoingNodeId, otherAttempts = 1) @@ -265,7 +279,7 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a val fwdNodeId = register.expectMessageType[ForwardNodeId[Peer.ProposeOnTheFlyFunding]] assert(fwdNodeId.nodeId == outgoingNodeId) fwdNodeId.replyTo ! Register.ForwardNodeIdFailure(fwdNodeId) - expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, FailureReason.LocalFailure(UnknownNextPeer()), commit = true)) + expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, FailureReason.LocalFailure(UnknownNextPeer()), None, commit = true)) } test("relay blinded payment (on-the-fly funding not attempted)", Tag(wakeUpEnabled), Tag(onTheFlyFunding)) { f => @@ -278,7 +292,8 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a val r = createValidIncomingPacket(payload, outgoingAmount + u.channelUpdate.feeBaseMsat, outgoingExpiry + u.channelUpdate.cltvExpiryDelta) channelRelayer ! WrappedLocalChannelUpdate(u) - channelRelayer ! Relay(r, TestConstants.Alice.nodeParams.nodeId) + channelRelayer ! Relay(r, TestConstants.Alice.nodeParams.nodeId, 0.1) + receiveConfidence(Reputation.Score.fromEndorsement(7)) // We try to wake-up the next node. peerReadyManager.expectMessageType[PeerReadyManager.Register].replyTo ! PeerReadyManager.Registered(outgoingNodeId, otherAttempts = 0) @@ -292,7 +307,7 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a fwd.message.replyTo ! RES_ADD_FAILED(fwd.message, TooManyAcceptedHtlcs(channelIds(realScid1), 10), Some(u.channelUpdate)) // We fail without attempting on-the-fly funding. - expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, FailureReason.LocalFailure(TemporaryNodeFailure()), commit = true)) + expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, FailureReason.LocalFailure(TemporaryNodeFailure()), None, commit = true)) } test("relay with retries") { f => @@ -309,20 +324,21 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a val u2 = createLocalUpdate(channelId2, balance = 80_000_000 msat) channelRelayer ! WrappedLocalChannelUpdate(u2) - channelRelayer ! Relay(r, TestConstants.Alice.nodeParams.nodeId) + channelRelayer ! Relay(r, TestConstants.Alice.nodeParams.nodeId, 0.1) + receiveConfidence(Reputation.Score.max) // first try val fwd1 = expectFwdAdd(register, channelIds(realScid2), outgoingAmount, outgoingExpiry, 7) // channel returns an error - fwd1.message.replyTo ! RES_ADD_FAILED(fwd1.message, HtlcValueTooHighInFlight(channelIds(realScid2), 1000000000 msat, 1516977616 msat), Some(u2.channelUpdate)) + fwd1.message.replyTo ! RES_ADD_FAILED(fwd1.message, HtlcValueTooHighInFlight(channelIds(realScid2), UInt64(1_000_000_000), 1_516_977_616 msat), Some(u2.channelUpdate)) // second try val fwd2 = expectFwdAdd(register, channelIds(realScid1), outgoingAmount, outgoingExpiry, 7) // failure again - fwd1.message.replyTo ! RES_ADD_FAILED(fwd2.message, HtlcValueTooHighInFlight(channelIds(realScid1), 1000000000 msat, 1516977616 msat), Some(u1.channelUpdate)) + fwd1.message.replyTo ! RES_ADD_FAILED(fwd2.message, HtlcValueTooHighInFlight(channelIds(realScid1), UInt64(1_000_000_000), 1_516_977_616 msat), Some(u1.channelUpdate)) // the relayer should give up - expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, FailureReason.LocalFailure(TemporaryChannelFailure(Some(u1.channelUpdate))), commit = true)) + expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, FailureReason.LocalFailure(TemporaryChannelFailure(Some(u1.channelUpdate))), None, commit = true)) } test("fail to relay when we have no channel_update for the next channel") { f => @@ -331,9 +347,10 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a val payload = ChannelRelay.Standard(realScid1, outgoingAmount, outgoingExpiry) val r = createValidIncomingPacket(payload) - channelRelayer ! Relay(r, TestConstants.Alice.nodeParams.nodeId) + channelRelayer ! Relay(r, TestConstants.Alice.nodeParams.nodeId, 0.1) + receiveConfidence(Reputation.Score.max) - expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, FailureReason.LocalFailure(UnknownNextPeer()), commit = true)) + expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, FailureReason.LocalFailure(UnknownNextPeer()), None, commit = true)) } test("fail to relay when register returns an error") { f => @@ -344,12 +361,13 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a val u = createLocalUpdate(channelId1) channelRelayer ! WrappedLocalChannelUpdate(u) - channelRelayer ! Relay(r, TestConstants.Alice.nodeParams.nodeId) + channelRelayer ! Relay(r, TestConstants.Alice.nodeParams.nodeId, 0.1) + receiveConfidence(Reputation.Score.fromEndorsement(6)) - val fwd = expectFwdAdd(register, channelIds(realScid1), outgoingAmount, outgoingExpiry, 7) + val fwd = expectFwdAdd(register, channelIds(realScid1), outgoingAmount, outgoingExpiry, 6) fwd.replyTo ! Register.ForwardFailure(fwd) - expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, FailureReason.LocalFailure(UnknownNextPeer()), commit = true)) + expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, FailureReason.LocalFailure(UnknownNextPeer()), None, commit = true)) } test("fail to relay when the channel is advertised as unusable (down)") { f => @@ -362,9 +380,10 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a channelRelayer ! WrappedLocalChannelUpdate(u) channelRelayer ! WrappedLocalChannelDown(d) - channelRelayer ! Relay(r, TestConstants.Alice.nodeParams.nodeId) + channelRelayer ! Relay(r, TestConstants.Alice.nodeParams.nodeId, 0.1) + receiveConfidence(Reputation.Score.max) - expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, FailureReason.LocalFailure(UnknownNextPeer()), commit = true)) + expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, FailureReason.LocalFailure(UnknownNextPeer()), None, commit = true)) } test("fail to relay when channel is disabled") { f => @@ -375,9 +394,10 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a val u = createLocalUpdate(channelId1, enabled = false) channelRelayer ! WrappedLocalChannelUpdate(u) - channelRelayer ! Relay(r, TestConstants.Alice.nodeParams.nodeId) + channelRelayer ! Relay(r, TestConstants.Alice.nodeParams.nodeId, 0.1) + receiveConfidence(Reputation.Score.max) - expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, FailureReason.LocalFailure(ChannelDisabled(u.channelUpdate.messageFlags, u.channelUpdate.channelFlags, Some(u.channelUpdate))), commit = true)) + expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, FailureReason.LocalFailure(ChannelDisabled(u.channelUpdate.messageFlags, u.channelUpdate.channelFlags, Some(u.channelUpdate))), None, commit = true)) } test("fail to relay when amount is below minimum") { f => @@ -388,9 +408,10 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a val u = createLocalUpdate(channelId1, htlcMinimum = outgoingAmount + 1.msat) channelRelayer ! WrappedLocalChannelUpdate(u) - channelRelayer ! Relay(r, TestConstants.Alice.nodeParams.nodeId) + channelRelayer ! Relay(r, TestConstants.Alice.nodeParams.nodeId, 0.1) + receiveConfidence(Reputation.Score.max) - expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, FailureReason.LocalFailure(AmountBelowMinimum(outgoingAmount, Some(u.channelUpdate))), commit = true)) + expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, FailureReason.LocalFailure(AmountBelowMinimum(outgoingAmount, Some(u.channelUpdate))), None, commit = true)) } test("fail to relay blinded payment") { f => @@ -402,7 +423,8 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a val r = createValidIncomingPacket(createBlindedPayload(Right(u.channelUpdate.shortChannelId), u.channelUpdate, isIntroduction), outgoingAmount + u.channelUpdate.feeBaseMsat, outgoingExpiry + u.channelUpdate.cltvExpiryDelta) channelRelayer ! WrappedLocalChannelUpdate(u) - channelRelayer ! Relay(r, TestConstants.Alice.nodeParams.nodeId) + channelRelayer ! Relay(r, TestConstants.Alice.nodeParams.nodeId, 0.1) + receiveConfidence(Reputation.Score.max) val cmd = register.expectMessageType[Register.Forward[channel.Command]] assert(cmd.channelId == r.add.channelId) @@ -432,7 +454,8 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a val r = createValidIncomingPacket(payload, outgoingAmount + u.channelUpdate.feeBaseMsat, outgoingExpiry + u.channelUpdate.cltvExpiryDelta) channelRelayer ! WrappedLocalChannelUpdate(u) - channelRelayer ! Relay(r, TestConstants.Alice.nodeParams.nodeId) + channelRelayer ! Relay(r, TestConstants.Alice.nodeParams.nodeId, 0.1) + receiveConfidence(Reputation.Score.max) // We try to wake-up the next node, but we timeout before they connect. peerReadyManager.expectMessageType[PeerReadyManager.Register].replyTo ! PeerReadyManager.Registered(outgoingNodeId, otherAttempts = 0) @@ -451,7 +474,8 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a val r = createValidIncomingPacket(payload, expiryIn = outgoingExpiry + u.channelUpdate.cltvExpiryDelta + CltvExpiryDelta(1)) channelRelayer ! WrappedLocalChannelUpdate(u) - channelRelayer ! Relay(r, TestConstants.Alice.nodeParams.nodeId) + channelRelayer ! Relay(r, TestConstants.Alice.nodeParams.nodeId, 0.1) + receiveConfidence(Reputation.Score.max) expectFwdAdd(register, channelIds(realScid1), r.amountToForward, r.outgoingCltv, 7).message } @@ -464,9 +488,10 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a val r = createValidIncomingPacket(payload, expiryIn = outgoingExpiry + u.channelUpdate.cltvExpiryDelta - CltvExpiryDelta(1)) channelRelayer ! WrappedLocalChannelUpdate(u) - channelRelayer ! Relay(r, TestConstants.Alice.nodeParams.nodeId) + channelRelayer ! Relay(r, TestConstants.Alice.nodeParams.nodeId, 0.1) + receiveConfidence(Reputation.Score.max) - expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, FailureReason.LocalFailure(IncorrectCltvExpiry(r.outgoingCltv, Some(u.channelUpdate))), commit = true)) + expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, FailureReason.LocalFailure(IncorrectCltvExpiry(r.outgoingCltv, Some(u.channelUpdate))), None, commit = true)) } test("fail to relay when fee is insufficient") { f => @@ -477,9 +502,10 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a val u = createLocalUpdate(channelId1) channelRelayer ! WrappedLocalChannelUpdate(u) - channelRelayer ! Relay(r, TestConstants.Alice.nodeParams.nodeId) + channelRelayer ! Relay(r, TestConstants.Alice.nodeParams.nodeId, 0.1) + receiveConfidence(Reputation.Score.max) - expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, FailureReason.LocalFailure(FeeInsufficient(r.add.amountMsat, Some(u.channelUpdate))), commit = true)) + expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, FailureReason.LocalFailure(FeeInsufficient(r.add.amountMsat, Some(u.channelUpdate))), None, commit = true)) } test("relay that would fail (fee insufficient) with a recent channel update but succeed with the previous update") { f => @@ -490,27 +516,30 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a val u1 = createLocalUpdate(channelId1, timestamp = TimestampSecond.now(), feeBaseMsat = 1 msat, feeProportionalMillionths = 0) channelRelayer ! WrappedLocalChannelUpdate(u1) - channelRelayer ! Relay(r, TestConstants.Alice.nodeParams.nodeId) + channelRelayer ! Relay(r, TestConstants.Alice.nodeParams.nodeId, 0.1) + receiveConfidence(Reputation.Score.fromEndorsement(1)) // relay succeeds with current channel update (u1) with lower fees - expectFwdAdd(register, channelIds(realScid1), outgoingAmount, outgoingExpiry, 7) + expectFwdAdd(register, channelIds(realScid1), outgoingAmount, outgoingExpiry, 1) val u2 = createLocalUpdate(channelId1, timestamp = TimestampSecond.now() - 530) channelRelayer ! WrappedLocalChannelUpdate(u2) - channelRelayer ! Relay(r, TestConstants.Alice.nodeParams.nodeId) + channelRelayer ! Relay(r, TestConstants.Alice.nodeParams.nodeId, 0.1) + receiveConfidence(Reputation.Score.fromEndorsement(2)) // relay succeeds because the current update (u2) with higher fees occurred less than 10 minutes ago - expectFwdAdd(register, channelIds(realScid1), outgoingAmount, outgoingExpiry, 7) + expectFwdAdd(register, channelIds(realScid1), outgoingAmount, outgoingExpiry, 2) val u3 = createLocalUpdate(channelId1, timestamp = TimestampSecond.now() - 601) channelRelayer ! WrappedLocalChannelUpdate(u1) channelRelayer ! WrappedLocalChannelUpdate(u3) - channelRelayer ! Relay(r, TestConstants.Alice.nodeParams.nodeId) + channelRelayer ! Relay(r, TestConstants.Alice.nodeParams.nodeId, 0.1) + receiveConfidence(Reputation.Score.max) // relay fails because the current update (u3) with higher fees occurred more than 10 minutes ago - expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, FailureReason.LocalFailure(FeeInsufficient(r.add.amountMsat, Some(u3.channelUpdate))), commit = true)) + expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, FailureReason.LocalFailure(FeeInsufficient(r.add.amountMsat, Some(u3.channelUpdate))), None, commit = true)) } test("fail to relay when there is a local error") { f => @@ -528,7 +557,7 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a TestCase(ExpiryTooSmall(channelId1, CltvExpiry(100), CltvExpiry(0), BlockHeight(0)), u.channelUpdate, ExpiryTooSoon(Some(u.channelUpdate))), TestCase(ExpiryTooBig(channelId1, CltvExpiry(100), CltvExpiry(200), BlockHeight(0)), u.channelUpdate, ExpiryTooFar()), TestCase(TooManyAcceptedHtlcs(channelId1, 10), u.channelUpdate, TemporaryChannelFailure(Some(u.channelUpdate))), - TestCase(HtlcValueTooHighInFlight(channelId1, 250_000_000 msat, 300_000_000 msat), u.channelUpdate, TemporaryChannelFailure(Some(u.channelUpdate))), + TestCase(HtlcValueTooHighInFlight(channelId1, UInt64(250_000_000), 300_000_000 msat), u.channelUpdate, TemporaryChannelFailure(Some(u.channelUpdate))), TestCase(InsufficientFunds(channelId1, r.amountToForward, 100 sat, 0 sat, 0 sat), u.channelUpdate, TemporaryChannelFailure(Some(u.channelUpdate))), TestCase(FeerateTooDifferent(channelId1, FeeratePerKw(1000 sat), FeeratePerKw(300 sat)), u.channelUpdate, TemporaryChannelFailure(Some(u.channelUpdate))), TestCase(ChannelUnavailable(channelId1), u_disabled.channelUpdate, ChannelDisabled(u_disabled.channelUpdate.messageFlags, u_disabled.channelUpdate.channelFlags, Some(u_disabled.channelUpdate))) @@ -536,10 +565,11 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a testCases.foreach { testCase => channelRelayer ! WrappedLocalChannelUpdate(u) - channelRelayer ! Relay(r, TestConstants.Alice.nodeParams.nodeId) + channelRelayer ! Relay(r, TestConstants.Alice.nodeParams.nodeId, 0.1) + receiveConfidence(Reputation.Score.max) val fwd = expectFwdAdd(register, channelIds(realScid1), outgoingAmount, outgoingExpiry, 7) fwd.message.replyTo ! RES_ADD_FAILED(fwd.message, testCase.exc, Some(testCase.update)) - expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, FailureReason.LocalFailure(testCase.failure), commit = true)) + expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, FailureReason.LocalFailure(testCase.failure), None, commit = true)) } } @@ -572,7 +602,8 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a { val payload = ChannelRelay.Standard(ShortChannelId(12345), 998900 msat, CltvExpiry(60)) val r = createValidIncomingPacket(payload, 1000000 msat, CltvExpiry(70), endorsementIn = 5) - channelRelayer ! Relay(r, TestConstants.Alice.nodeParams.nodeId) + channelRelayer ! Relay(r, TestConstants.Alice.nodeParams.nodeId, 0.1) + receiveConfidence(Reputation.Score.fromEndorsement(5)) // select the channel to the same node, with the lowest capacity and balance but still high enough to handle the payment val cmd1 = expectFwdAdd(register, channelUpdates(ShortChannelId(22223)).channelId, r.amountToForward, r.outgoingCltv, 5).message cmd1.replyTo ! RES_ADD_FAILED(cmd1, ChannelUnavailable(randomBytes32()), None) @@ -584,44 +615,49 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a cmd3.replyTo ! RES_ADD_FAILED(cmd3, TooManyAcceptedHtlcs(randomBytes32(), 42), Some(channelUpdates(ShortChannelId(12345)).channelUpdate)) // select 4th-to-best channel: same capacity but higher balance val cmd4 = expectFwdAdd(register, channelUpdates(ShortChannelId(11111)).channelId, r.amountToForward, r.outgoingCltv, 5).message - cmd4.replyTo ! RES_ADD_FAILED(cmd4, HtlcValueTooHighInFlight(randomBytes32(), 100000000 msat, 100000000 msat), Some(channelUpdates(ShortChannelId(11111)).channelUpdate)) + cmd4.replyTo ! RES_ADD_FAILED(cmd4, HtlcValueTooHighInFlight(randomBytes32(), UInt64(100_000_000), 100_000_000 msat), Some(channelUpdates(ShortChannelId(11111)).channelUpdate)) // all the suitable channels have been tried - expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, FailureReason.LocalFailure(TemporaryChannelFailure(Some(channelUpdates(ShortChannelId(12345)).channelUpdate))), commit = true)) + expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, FailureReason.LocalFailure(TemporaryChannelFailure(Some(channelUpdates(ShortChannelId(12345)).channelUpdate))), None, commit = true)) } { // higher amount payment (have to increased incoming htlc amount for fees to be sufficient) val payload = ChannelRelay.Standard(ShortChannelId(12345), 50000000 msat, CltvExpiry(60)) val r = createValidIncomingPacket(payload, 60000000 msat, CltvExpiry(70), endorsementIn = 0) - channelRelayer ! Relay(r, TestConstants.Alice.nodeParams.nodeId) + channelRelayer ! Relay(r, TestConstants.Alice.nodeParams.nodeId, 0.1) + receiveConfidence(Reputation.Score.fromEndorsement(0)) expectFwdAdd(register, channelUpdates(ShortChannelId(11111)).channelId, r.amountToForward, r.outgoingCltv, 0).message } { // lower amount payment val payload = ChannelRelay.Standard(ShortChannelId(12345), 1000 msat, CltvExpiry(60)) val r = createValidIncomingPacket(payload, 60000000 msat, CltvExpiry(70), endorsementIn = 6) - channelRelayer ! Relay(r, TestConstants.Alice.nodeParams.nodeId) + channelRelayer ! Relay(r, TestConstants.Alice.nodeParams.nodeId, 0.1) + receiveConfidence(Reputation.Score.fromEndorsement(6)) expectFwdAdd(register, channelUpdates(ShortChannelId(33333)).channelId, r.amountToForward, r.outgoingCltv, 6).message } { // payment too high, no suitable channel found, we keep the requested one val payload = ChannelRelay.Standard(ShortChannelId(12345), 1000000000 msat, CltvExpiry(60)) val r = createValidIncomingPacket(payload, 1010000000 msat, CltvExpiry(70)) - channelRelayer ! Relay(r, TestConstants.Alice.nodeParams.nodeId) + channelRelayer ! Relay(r, TestConstants.Alice.nodeParams.nodeId, 0.1) + receiveConfidence(Reputation.Score.fromEndorsement(7)) expectFwdAdd(register, channelUpdates(ShortChannelId(12345)).channelId, r.amountToForward, r.outgoingCltv, 7).message } { // cltv expiry larger than our requirements val payload = ChannelRelay.Standard(ShortChannelId(12345), 998900 msat, CltvExpiry(50)) val r = createValidIncomingPacket(payload, 1000000 msat, CltvExpiry(70)) - channelRelayer ! Relay(r, TestConstants.Alice.nodeParams.nodeId) + channelRelayer ! Relay(r, TestConstants.Alice.nodeParams.nodeId, 0.1) + receiveConfidence(Reputation.Score.fromEndorsement(7)) expectFwdAdd(register, channelUpdates(ShortChannelId(22223)).channelId, r.amountToForward, r.outgoingCltv, 7).message } { // cltv expiry too small, no suitable channel found val payload = ChannelRelay.Standard(ShortChannelId(12345), 998900 msat, CltvExpiry(61)) val r = createValidIncomingPacket(payload, 1000000 msat, CltvExpiry(70)) - channelRelayer ! Relay(r, TestConstants.Alice.nodeParams.nodeId) - expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, FailureReason.LocalFailure(IncorrectCltvExpiry(CltvExpiry(61), Some(channelUpdates(ShortChannelId(12345)).channelUpdate))), commit = true)) + channelRelayer ! Relay(r, TestConstants.Alice.nodeParams.nodeId, 0.1) + receiveConfidence(Reputation.Score.fromEndorsement(4)) + expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, FailureReason.LocalFailure(IncorrectCltvExpiry(CltvExpiry(61), Some(channelUpdates(ShortChannelId(12345)).channelUpdate))), None, commit = true)) } } @@ -632,21 +668,22 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a val payload = ChannelRelay.Standard(realScid1, outgoingAmount, outgoingExpiry) val r = createValidIncomingPacket(payload, outgoingAmount + u.channelUpdate.feeBaseMsat, outgoingExpiry + u.channelUpdate.cltvExpiryDelta) val u_disabled = createLocalUpdate(channelId1, enabled = false) - val downstream_htlc = UpdateAddHtlc(channelId1, 7, outgoingAmount, paymentHash, outgoingExpiry, emptyOnionPacket, None, 1.0, None) + val downstream_htlc = UpdateAddHtlc(channelId1, 7, outgoingAmount, paymentHash, outgoingExpiry, emptyOnionPacket, None, 7, None) - case class TestCase(result: HtlcResult, cmd: channel.HtlcSettlementCommand) + case class TestCase(result: HtlcResult, cmd: CMD_FAIL_HTLC) val testCases = Seq( - TestCase(HtlcResult.RemoteFail(UpdateFailHtlc(channelId1, downstream_htlc.id, hex"deadbeef")), CMD_FAIL_HTLC(r.add.id, FailureReason.EncryptedDownstreamFailure(hex"deadbeef"), commit = true)), - TestCase(HtlcResult.RemoteFailMalformed(UpdateFailMalformedHtlc(channelId1, downstream_htlc.id, ByteVector32.One, FailureMessageCodecs.BADONION | FailureMessageCodecs.PERM | 5)), CMD_FAIL_HTLC(r.add.id, FailureReason.LocalFailure(InvalidOnionHmac(ByteVector32.One)), commit = true)), - TestCase(HtlcResult.OnChainFail(HtlcOverriddenByLocalCommit(channelId1, downstream_htlc)), CMD_FAIL_HTLC(r.add.id, FailureReason.LocalFailure(PermanentChannelFailure()), commit = true)), - TestCase(HtlcResult.DisconnectedBeforeSigned(u_disabled.channelUpdate), CMD_FAIL_HTLC(r.add.id, FailureReason.LocalFailure(TemporaryChannelFailure(Some(u_disabled.channelUpdate))), commit = true)), - TestCase(HtlcResult.ChannelFailureBeforeSigned, CMD_FAIL_HTLC(r.add.id, FailureReason.LocalFailure(PermanentChannelFailure()), commit = true)) + TestCase(HtlcResult.RemoteFail(UpdateFailHtlc(channelId1, downstream_htlc.id, hex"deadbeef")), CMD_FAIL_HTLC(r.add.id, FailureReason.EncryptedDownstreamFailure(hex"deadbeef", None), None, commit = true)), + TestCase(HtlcResult.RemoteFailMalformed(UpdateFailMalformedHtlc(channelId1, downstream_htlc.id, ByteVector32.One, FailureMessageCodecs.BADONION | FailureMessageCodecs.PERM | 5)), CMD_FAIL_HTLC(r.add.id, FailureReason.LocalFailure(InvalidOnionHmac(ByteVector32.One)), None, commit = true)), + TestCase(HtlcResult.OnChainFail(HtlcOverriddenByLocalCommit(channelId1, downstream_htlc)), CMD_FAIL_HTLC(r.add.id, FailureReason.LocalFailure(PermanentChannelFailure()), None, commit = true)), + TestCase(HtlcResult.DisconnectedBeforeSigned(u_disabled.channelUpdate), CMD_FAIL_HTLC(r.add.id, FailureReason.LocalFailure(TemporaryChannelFailure(Some(u_disabled.channelUpdate))), None, commit = true)), + TestCase(HtlcResult.ChannelFailureBeforeSigned, CMD_FAIL_HTLC(r.add.id, FailureReason.LocalFailure(PermanentChannelFailure()), None, commit = true)) ) testCases.foreach { testCase => channelRelayer ! WrappedLocalChannelUpdate(u) - channelRelayer ! Relay(r, TestConstants.Alice.nodeParams.nodeId) + channelRelayer ! Relay(r, TestConstants.Alice.nodeParams.nodeId, 0.1) + receiveConfidence(Reputation.Score.max) val fwd = expectFwdAdd(register, channelIds(realScid1), outgoingAmount, outgoingExpiry, 7) fwd.message.replyTo ! RES_SUCCESS(fwd.message, channelId1) fwd.message.origin.replyTo ! RES_ADD_SETTLED(fwd.message.origin, downstream_htlc, testCase.result) @@ -658,7 +695,7 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a import f._ val u = createLocalUpdate(channelId1, feeBaseMsat = 5000 msat, feeProportionalMillionths = 0) - val downstream = UpdateAddHtlc(channelId1, 7, outgoingAmount, paymentHash, outgoingExpiry, emptyOnionPacket, None, 0.0625, None) + val downstream = UpdateAddHtlc(channelId1, 7, outgoingAmount, paymentHash, outgoingExpiry, emptyOnionPacket, None, 0, None) val testCases = Seq( HtlcResult.RemoteFail(UpdateFailHtlc(channelId1, downstream.id, hex"deadbeef")), @@ -672,7 +709,8 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a testCases.foreach { htlcResult => val r = createValidIncomingPacket(createBlindedPayload(Right(u.channelUpdate.shortChannelId), u.channelUpdate, isIntroduction), outgoingAmount + u.channelUpdate.feeBaseMsat, outgoingExpiry + u.channelUpdate.cltvExpiryDelta, endorsementIn = 0) channelRelayer ! WrappedLocalChannelUpdate(u) - channelRelayer ! Relay(r, TestConstants.Alice.nodeParams.nodeId) + channelRelayer ! Relay(r, TestConstants.Alice.nodeParams.nodeId, 0.1) + receiveConfidence(Reputation.Score.fromEndorsement(0)) val fwd = expectFwdAdd(register, channelId1, outgoingAmount, outgoingExpiry, 0) fwd.message.replyTo ! RES_SUCCESS(fwd.message, channelId1) fwd.message.origin.replyTo ! RES_ADD_SETTLED(fwd.message.origin, downstream, htlcResult) @@ -704,7 +742,7 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a val payload = ChannelRelay.Standard(realScid1, outgoingAmount, outgoingExpiry) val r = createValidIncomingPacket(payload, endorsementIn = 3) val u = createLocalUpdate(channelId1) - val downstream_htlc = UpdateAddHtlc(channelId1, 7, outgoingAmount, paymentHash, outgoingExpiry, emptyOnionPacket, None, 0.4375, None) + val downstream_htlc = UpdateAddHtlc(channelId1, 7, outgoingAmount, paymentHash, outgoingExpiry, emptyOnionPacket, None, 3, None) case class TestCase(result: HtlcResult) @@ -715,7 +753,8 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a testCases.foreach { testCase => channelRelayer ! WrappedLocalChannelUpdate(u) - channelRelayer ! Relay(r, TestConstants.Alice.nodeParams.nodeId) + channelRelayer ! Relay(r, TestConstants.Alice.nodeParams.nodeId, 0.1) + receiveConfidence(Reputation.Score.fromEndorsement(3)) val fwd1 = expectFwdAdd(register, channelIds(realScid1), outgoingAmount, outgoingExpiry, 3) fwd1.message.replyTo ! RES_SUCCESS(fwd1.message, channelId1) @@ -727,7 +766,7 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a assert(fwd2.message.r == paymentPreimage) val paymentRelayed = eventListener.expectMessageType[ChannelPaymentRelayed] - assert(paymentRelayed.copy(startedAt = 0 unixms, settledAt = 0 unixms) == ChannelPaymentRelayed(r.add.amountMsat, r.amountToForward, r.add.paymentHash, r.add.channelId, channelId1, startedAt = 0 unixms, settledAt = 0 unixms)) + assert(paymentRelayed.copy(receivedAt = 0 unixms, settledAt = 0 unixms) == ChannelPaymentRelayed(r.add.amountMsat, r.amountToForward, r.add.paymentHash, r.add.channelId, channelId1, receivedAt = 0 unixms, settledAt = 0 unixms)) } } @@ -740,12 +779,13 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a val payload = ChannelRelay.Standard(realScid1, outgoingAmount, outgoingExpiry) val r = createValidIncomingPacket(payload, endorsementIn = 3) - channelRelayer ! Relay(r, TestConstants.Alice.nodeParams.nodeId) + channelRelayer ! Relay(r, TestConstants.Alice.nodeParams.nodeId, 0.1) + receiveConfidence(Reputation.Score.fromEndorsement(3)) val fwd1 = expectFwdAdd(register, channelIds(realScid1), outgoingAmount, outgoingExpiry, 3) fwd1.message.replyTo ! RES_SUCCESS(fwd1.message, channelId1) // The downstream HTLC is fulfilled. - val downstream = UpdateAddHtlc(randomBytes32(), 7, outgoingAmount, paymentHash, outgoingExpiry, emptyOnionPacket, None, 0.4375, None) + val downstream = UpdateAddHtlc(randomBytes32(), 7, outgoingAmount, paymentHash, outgoingExpiry, emptyOnionPacket, None, 3, None) fwd1.message.origin.replyTo ! RES_ADD_SETTLED(fwd1.message.origin, downstream, HtlcResult.RemoteFulfill(UpdateFulfillHtlc(downstream.channelId, downstream.id, paymentPreimage))) val fulfill = inside(register.expectMessageType[Register.Forward[CMD_FULFILL_HTLC]]) { fwd => assert(fwd.channelId == r.add.channelId) @@ -859,7 +899,7 @@ object ChannelRelayerSpec { } val tlvs = TlvStream(Set[Option[UpdateAddHtlcTlv]](nextPathKey_opt, Some(UpdateAddHtlcTlv.Endorsement(endorsementIn))).flatten) val add_ab = UpdateAddHtlc(channelId = randomBytes32(), id = 123456, amountIn, paymentHash, expiryIn, emptyOnionPacket, tlvs) - ChannelRelayPacket(add_ab, payload, emptyOnionPacket) + ChannelRelayPacket(add_ab, payload, emptyOnionPacket, TimestampMilli.now()) } def createAliases(channelId: ByteVector32): ShortIdAliases = { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/NodeRelayerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/NodeRelayerSpec.scala index 520e044e57..b571a8dc24 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/NodeRelayerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/NodeRelayerSpec.scala @@ -42,6 +42,7 @@ import fr.acinq.eclair.payment.send.MultiPartPaymentLifecycle.{PreimageReceived, import fr.acinq.eclair.payment.send.PaymentInitiator.SendPaymentConfig import fr.acinq.eclair.payment.send.PaymentLifecycle.SendPaymentToNode import fr.acinq.eclair.payment.send.{BlindedRecipient, ClearRecipient} +import fr.acinq.eclair.reputation.Reputation import fr.acinq.eclair.router.Router.{ChannelHop, HopRelayParams, PaymentRouteNotFound, RouteRequest} import fr.acinq.eclair.router.{BalanceTooLow, BlindedRouteCreation, RouteNotFound, Router} import fr.acinq.eclair.wire.protocol.OfferTypes._ @@ -101,7 +102,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl case class RealOutgoingPaymentFactory(f: FixtureParam) extends NodeRelay.OutgoingPaymentFactory { override def spawnOutgoingPayFSM(context: ActorContext[NodeRelay.Command], cfg: SendPaymentConfig, multiPart: Boolean): akka.actor.ActorRef = { - val outgoingPayFSM = NodeRelay.SimpleOutgoingPaymentFactory(f.nodeParams, f.router.ref.toClassic, f.register.ref.toClassic).spawnOutgoingPayFSM(context, cfg, multiPart) + val outgoingPayFSM = NodeRelay.SimpleOutgoingPaymentFactory(f.nodeParams, f.router.ref.toClassic, f.register.ref.toClassic, None).spawnOutgoingPayFSM(context, cfg, multiPart) f.mockPayFSM.ref ! outgoingPayFSM outgoingPayFSM } @@ -132,26 +133,26 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl val (paymentHash1, paymentSecret1) = (randomBytes32(), randomBytes32()) val payment1 = createPartialIncomingPacket(paymentHash1, paymentSecret1) - parentRelayer ! NodeRelayer.Relay(payment1, randomKey().publicKey) + parentRelayer ! NodeRelayer.Relay(payment1, randomKey().publicKey, 0.01) parentRelayer ! NodeRelayer.GetPendingPayments(probe.ref.toClassic) val pending1 = probe.expectMessageType[Map[PaymentKey, ActorRef[NodeRelay.Command]]] assert(pending1.keySet == Set(PaymentKey(paymentHash1, paymentSecret1))) val (paymentHash2, paymentSecret2) = (randomBytes32(), randomBytes32()) val payment2 = createPartialIncomingPacket(paymentHash2, paymentSecret2) - parentRelayer ! NodeRelayer.Relay(payment2, randomKey().publicKey) + parentRelayer ! NodeRelayer.Relay(payment2, randomKey().publicKey, 0.01) parentRelayer ! NodeRelayer.GetPendingPayments(probe.ref.toClassic) val pending2 = probe.expectMessageType[Map[PaymentKey, ActorRef[NodeRelay.Command]]] assert(pending2.keySet == Set(PaymentKey(paymentHash1, paymentSecret1), PaymentKey(paymentHash2, paymentSecret2))) val payment3a = createPartialIncomingPacket(paymentHash1, paymentSecret2) - parentRelayer ! NodeRelayer.Relay(payment3a, randomKey().publicKey) + parentRelayer ! NodeRelayer.Relay(payment3a, randomKey().publicKey, 0.01) parentRelayer ! NodeRelayer.GetPendingPayments(probe.ref.toClassic) val pending3 = probe.expectMessageType[Map[PaymentKey, ActorRef[NodeRelay.Command]]] assert(pending3.keySet == Set(PaymentKey(paymentHash1, paymentSecret1), PaymentKey(paymentHash2, paymentSecret2), PaymentKey(paymentHash1, paymentSecret2))) val payment3b = createPartialIncomingPacket(paymentHash1, paymentSecret2) - parentRelayer ! NodeRelayer.Relay(payment3b, randomKey().publicKey) + parentRelayer ! NodeRelayer.Relay(payment3b, randomKey().publicKey, 0.01) parentRelayer ! NodeRelayer.GetPendingPayments(probe.ref.toClassic) val pending4 = probe.expectMessageType[Map[PaymentKey, ActorRef[NodeRelay.Command]]] assert(pending4.keySet == Set(PaymentKey(paymentHash1, paymentSecret1), PaymentKey(paymentHash2, paymentSecret2), PaymentKey(paymentHash1, paymentSecret2))) @@ -200,7 +201,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl } { val parentRelayer = testKit.spawn(NodeRelayer(nodeParams, register.ref.toClassic, outgoingPaymentFactory, router.ref.toClassic)) - parentRelayer ! NodeRelayer.Relay(incomingMultiPart.head, randomKey().publicKey) + parentRelayer ! NodeRelayer.Relay(incomingMultiPart.head, randomKey().publicKey, 0.01) parentRelayer ! NodeRelayer.GetPendingPayments(probe.ref.toClassic) val pending1 = probe.expectMessageType[Map[PaymentKey, ActorRef[NodeRelay.Command]]] assert(pending1.size == 1) @@ -210,7 +211,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl parentRelayer ! NodeRelayer.GetPendingPayments(probe.ref.toClassic) probe.expectMessage(Map.empty) - parentRelayer ! NodeRelayer.Relay(incomingMultiPart.head, randomKey().publicKey) + parentRelayer ! NodeRelayer.Relay(incomingMultiPart.head, randomKey().publicKey, 0.01) parentRelayer ! NodeRelayer.GetPendingPayments(probe.ref.toClassic) val pending2 = probe.expectMessageType[Map[PaymentKey, ActorRef[NodeRelay.Command]]] assert(pending2.size == 1) @@ -224,13 +225,13 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl val (nodeRelayer, parent) = f.createNodeRelay(incomingMultiPart.head) // Receive a partial upstream multi-part payment. - incomingMultiPart.dropRight(1).foreach(incoming => nodeRelayer ! NodeRelay.Relay(incoming, randomKey().publicKey)) + incomingMultiPart.dropRight(1).foreach(incoming => nodeRelayer ! NodeRelay.Relay(incoming, randomKey().publicKey, 0.01)) // after a while the payment times out incomingMultiPart.dropRight(1).foreach { p => val fwd = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]](30 seconds) assert(fwd.channelId == p.add.channelId) val failure = FailureReason.LocalFailure(PaymentTimeout()) - assert(fwd.message == CMD_FAIL_HTLC(p.add.id, failure, commit = true)) + assert(fwd.message == CMD_FAIL_HTLC(p.add.id, failure, Some(FailureAttributionData(p.receivedAt, None)), commit = true)) } parent.expectMessageType[NodeRelayer.RelayComplete] @@ -242,14 +243,16 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl val (nodeRelayer, _) = f.createNodeRelay(incomingMultiPart.head) // We send all the parts of a mpp - incomingMultiPart.foreach(incoming => nodeRelayer ! NodeRelay.Relay(incoming, randomKey().publicKey)) + incomingMultiPart.foreach(incoming => nodeRelayer ! NodeRelay.Relay(incoming, randomKey().publicKey, 0.01)) // and then one extra + val extraReceivedAt = TimestampMilli.now() val extra = IncomingPaymentPacket.RelayToTrampolinePacket( - UpdateAddHtlc(randomBytes32(), Random.nextInt(100), 1000 msat, paymentHash, CltvExpiry(499990), TestConstants.emptyOnionPacket, None, 1.0, None), + UpdateAddHtlc(randomBytes32(), Random.nextInt(100), 1000 msat, paymentHash, CltvExpiry(499990), TestConstants.emptyOnionPacket, None, Reputation.maxEndorsement, None), FinalPayload.Standard.createPayload(1000 msat, incomingAmount, CltvExpiry(499990), incomingSecret, None), IntermediatePayload.NodeRelay.Standard(outgoingAmount, outgoingExpiry, outgoingNodeId), - createTrampolinePacket(outgoingAmount, outgoingExpiry)) - nodeRelayer ! NodeRelay.Relay(extra, randomKey().publicKey) + createTrampolinePacket(outgoingAmount, outgoingExpiry), + extraReceivedAt) + nodeRelayer ! NodeRelay.Relay(extra, randomKey().publicKey, 0.01) val getPeerInfo = register.expectMessageType[Register.ForwardNodeId[Peer.GetPeerInfo]](100 millis) getPeerInfo.message.replyTo.foreach(_ ! Peer.PeerNotFound(getPeerInfo.nodeId)) @@ -258,7 +261,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl val fwd = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]] assert(fwd.channelId == extra.add.channelId) val failure = FailureReason.LocalFailure(IncorrectOrUnknownPaymentDetails(extra.add.amountMsat, nodeParams.currentBlockHeight)) - assert(fwd.message == CMD_FAIL_HTLC(extra.add.id, failure, commit = true)) + assert(fwd.message == CMD_FAIL_HTLC(extra.add.id, failure, Some(FailureAttributionData(extraReceivedAt, None)), commit = true)) register.expectNoMessage(100 millis) } @@ -268,41 +271,45 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl val (nodeRelayer, _) = f.createNodeRelay(incomingMultiPart.head) // Receive a complete upstream multi-part payment, which we relay out. - incomingMultiPart.foreach(incoming => nodeRelayer ! NodeRelay.Relay(incoming, randomKey().publicKey)) + incomingMultiPart.foreach(incoming => nodeRelayer ! NodeRelay.Relay(incoming, randomKey().publicKey, 0.01)) val getPeerInfo = register.expectMessageType[Register.ForwardNodeId[Peer.GetPeerInfo]](100 millis) getPeerInfo.message.replyTo.foreach(_ ! Peer.PeerNotFound(getPeerInfo.nodeId)) val outgoingCfg = mockPayFSM.expectMessageType[SendPaymentConfig] - validateOutgoingCfg(outgoingCfg, Upstream.Hot.Trampoline(incomingMultiPart.map(p => Upstream.Hot.Channel(p.add, TimestampMilli.now(), randomKey().publicKey)).toList), 5) + validateOutgoingCfg(outgoingCfg, Upstream.Hot.Trampoline(incomingMultiPart.map(p => Upstream.Hot.Channel(p.add, p.receivedAt, randomKey().publicKey, 0.01)).toList)) val outgoingPayment = mockPayFSM.expectMessageType[SendMultiPartPayment] validateOutgoingPayment(outgoingPayment) // Receive new extraneous multi-part HTLC. + val receivedAt1 = TimestampMilli.now() val i1 = IncomingPaymentPacket.RelayToTrampolinePacket( - UpdateAddHtlc(randomBytes32(), Random.nextInt(100), 1000 msat, paymentHash, CltvExpiry(499990), TestConstants.emptyOnionPacket, None, 1.0, None), + UpdateAddHtlc(randomBytes32(), Random.nextInt(100), 1000 msat, paymentHash, CltvExpiry(499990), TestConstants.emptyOnionPacket, None, 6, None), FinalPayload.Standard.createPayload(1000 msat, incomingAmount, CltvExpiry(499990), incomingSecret, None), IntermediatePayload.NodeRelay.Standard(outgoingAmount, outgoingExpiry, outgoingNodeId), - createTrampolinePacket(outgoingAmount, outgoingExpiry)) - nodeRelayer ! NodeRelay.Relay(i1, randomKey().publicKey) + createTrampolinePacket(outgoingAmount, outgoingExpiry), + receivedAt1) + nodeRelayer ! NodeRelay.Relay(i1, randomKey().publicKey, 0.01) val fwd1 = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]] assert(fwd1.channelId == i1.add.channelId) val failure1 = FailureReason.LocalFailure(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight)) - assert(fwd1.message == CMD_FAIL_HTLC(i1.add.id, failure1, commit = true)) + assert(fwd1.message == CMD_FAIL_HTLC(i1.add.id, failure1, Some(FailureAttributionData(receivedAt1, None)), commit = true)) // Receive new HTLC with different details, but for the same payment hash. + val receivedAt2 = TimestampMilli.now() + 1.millis val i2 = IncomingPaymentPacket.RelayToTrampolinePacket( - UpdateAddHtlc(randomBytes32(), Random.nextInt(100), 1500 msat, paymentHash, CltvExpiry(499990), TestConstants.emptyOnionPacket, None, 1.0, None), + UpdateAddHtlc(randomBytes32(), Random.nextInt(100), 1500 msat, paymentHash, CltvExpiry(499990), TestConstants.emptyOnionPacket, None, 4, None), PaymentOnion.FinalPayload.Standard.createPayload(1500 msat, 1500 msat, CltvExpiry(499990), incomingSecret, None), IntermediatePayload.NodeRelay.Standard(1250 msat, outgoingExpiry, outgoingNodeId), - createTrampolinePacket(outgoingAmount, outgoingExpiry)) - nodeRelayer ! NodeRelay.Relay(i2, randomKey().publicKey) + createTrampolinePacket(outgoingAmount, outgoingExpiry), + receivedAt2) + nodeRelayer ! NodeRelay.Relay(i2, randomKey().publicKey, 0.01) val fwd2 = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]] assert(fwd1.channelId == i1.add.channelId) val failure2 = FailureReason.LocalFailure(IncorrectOrUnknownPaymentDetails(1500 msat, nodeParams.currentBlockHeight)) - assert(fwd2.message == CMD_FAIL_HTLC(i2.add.id, failure2, commit = true)) + assert(fwd2.message == CMD_FAIL_HTLC(i2.add.id, failure2, Some(FailureAttributionData(receivedAt2, None)), commit = true)) register.expectNoMessage(100 millis) } @@ -312,13 +319,13 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl val expiryIn = CltvExpiry(500000) // not ok (delta = 100) val expiryOut = CltvExpiry(499900) - val p = createValidIncomingPacket(2000000 msat, 2000000 msat, expiryIn, 1000000 msat, expiryOut) + val p = createValidIncomingPacket(2000000 msat, 2000000 msat, expiryIn, 1000000 msat, expiryOut, TimestampMilli.now()) val (nodeRelayer, _) = f.createNodeRelay(p) - nodeRelayer ! NodeRelay.Relay(p, randomKey().publicKey) + nodeRelayer ! NodeRelay.Relay(p, randomKey().publicKey, 0.01) val fwd = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]] assert(fwd.channelId == p.add.channelId) - assert(fwd.message == CMD_FAIL_HTLC(p.add.id, FailureReason.LocalFailure(TrampolineExpiryTooSoon()), commit = true)) + assert(fwd.message == CMD_FAIL_HTLC(p.add.id, FailureReason.LocalFailure(TrampolineExpiryTooSoon()), Some(FailureAttributionData(p.receivedAt, Some(p.receivedAt))), commit = true)) register.expectNoMessage(100 millis) } @@ -328,13 +335,13 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl val expiryIn = CltvExpiry(500000) val expiryOut = CltvExpiry(300000) // not ok (chain height = 400000) - val p = createValidIncomingPacket(2000000 msat, 2000000 msat, expiryIn, 1000000 msat, expiryOut) + val p = createValidIncomingPacket(2000000 msat, 2000000 msat, expiryIn, 1000000 msat, expiryOut, TimestampMilli.now()) val (nodeRelayer, _) = f.createNodeRelay(p) - nodeRelayer ! NodeRelay.Relay(p, randomKey().publicKey) + nodeRelayer ! NodeRelay.Relay(p, randomKey().publicKey, 0.01) val fwd = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]] assert(fwd.channelId == p.add.channelId) - assert(fwd.message == CMD_FAIL_HTLC(p.add.id, FailureReason.LocalFailure(TrampolineExpiryTooSoon()), commit = true)) + assert(fwd.message == CMD_FAIL_HTLC(p.add.id, FailureReason.LocalFailure(TrampolineExpiryTooSoon()), Some(FailureAttributionData(p.receivedAt, Some(p.receivedAt))), commit = true)) register.expectNoMessage(100 millis) } @@ -346,16 +353,16 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl val expiryIn2 = CltvExpiry(500000) // not ok (delta = 100) val expiryOut = CltvExpiry(499900) val p = Seq( - createValidIncomingPacket(2000000 msat, 3000000 msat, expiryIn1, 2100000 msat, expiryOut), - createValidIncomingPacket(1000000 msat, 3000000 msat, expiryIn2, 2100000 msat, expiryOut) + createValidIncomingPacket(2000000 msat, 3000000 msat, expiryIn1, 2100000 msat, expiryOut, TimestampMilli(10)), + createValidIncomingPacket(1000000 msat, 3000000 msat, expiryIn2, 2100000 msat, expiryOut, TimestampMilli(20)) ) val (nodeRelayer, _) = f.createNodeRelay(p.head) - p.foreach(p => nodeRelayer ! NodeRelay.Relay(p, randomKey().publicKey)) + p.foreach(p => nodeRelayer ! NodeRelay.Relay(p, randomKey().publicKey, 0.01)) p.foreach { p => val fwd = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]] assert(fwd.channelId == p.add.channelId) - assert(fwd.message == CMD_FAIL_HTLC(p.add.id, FailureReason.LocalFailure(TrampolineExpiryTooSoon()), commit = true)) + assert(fwd.message == CMD_FAIL_HTLC(p.add.id, FailureReason.LocalFailure(TrampolineExpiryTooSoon()), Some(FailureAttributionData(p.receivedAt, Some(TimestampMilli(20)))), commit = true)) } register.expectNoMessage(100 millis) @@ -367,25 +374,25 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl assert(!nodeParams.features.hasFeature(AsyncPaymentPrototype)) val (nodeRelayer, parent) = createNodeRelay(incomingAsyncPayment.head) - incomingAsyncPayment.foreach(p => nodeRelayer ! NodeRelay.Relay(p, randomKey().publicKey)) + incomingAsyncPayment.foreach(p => nodeRelayer ! NodeRelay.Relay(p, randomKey().publicKey, 0.01)) val getPeerInfo = register.expectMessageType[Register.ForwardNodeId[Peer.GetPeerInfo]](100 millis) getPeerInfo.message.replyTo.foreach(_ ! Peer.PeerNotFound(getPeerInfo.nodeId)) // upstream payment relayed val outgoingCfg = mockPayFSM.expectMessageType[SendPaymentConfig] - validateOutgoingCfg(outgoingCfg, Upstream.Hot.Trampoline(incomingAsyncPayment.map(p => Upstream.Hot.Channel(p.add, TimestampMilli.now(), randomKey().publicKey)).toList), 5) + validateOutgoingCfg(outgoingCfg, Upstream.Hot.Trampoline(incomingAsyncPayment.map(p => Upstream.Hot.Channel(p.add, p.receivedAt, randomKey().publicKey, 0.01)).toList)) val outgoingPayment = mockPayFSM.expectMessageType[SendMultiPartPayment] validateOutgoingPayment(outgoingPayment) // those are adapters for pay-fsm messages val nodeRelayerAdapters = outgoingPayment.replyTo // A first downstream HTLC is fulfilled: we should immediately forward the fulfill upstream. - nodeRelayerAdapters ! PreimageReceived(paymentHash, paymentPreimage) + nodeRelayerAdapters ! PreimageReceived(paymentHash, paymentPreimage, None) incomingAsyncPayment.foreach { p => val fwd = register.expectMessageType[Register.Forward[CMD_FULFILL_HTLC]] assert(fwd.channelId == p.add.channelId) - assert(fwd.message == CMD_FULFILL_HTLC(p.add.id, paymentPreimage, commit = true)) + assert(fwd.message == CMD_FULFILL_HTLC(p.add.id, paymentPreimage, Some(FulfillAttributionData(p.receivedAt, Some(incomingAsyncPayment.last.receivedAt), None)), commit = true)) } // Once all the downstream payments have settled, we should emit the relayed event. @@ -401,13 +408,13 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl test("fail to relay when fees are insufficient (single-part)") { f => import f._ - val p = createValidIncomingPacket(2000000 msat, 2000000 msat, CltvExpiry(500000), 1999000 msat, CltvExpiry(490000)) + val p = createValidIncomingPacket(2000000 msat, 2000000 msat, CltvExpiry(500000), 1999000 msat, CltvExpiry(490000), TimestampMilli.now()) val (nodeRelayer, _) = f.createNodeRelay(p) - nodeRelayer ! NodeRelay.Relay(p, randomKey().publicKey) + nodeRelayer ! NodeRelay.Relay(p, randomKey().publicKey, 0.01) val fwd = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]] assert(fwd.channelId == p.add.channelId) - assert(fwd.message == CMD_FAIL_HTLC(p.add.id, FailureReason.LocalFailure(TrampolineFeeInsufficient()), commit = true)) + assert(fwd.message == CMD_FAIL_HTLC(p.add.id, FailureReason.LocalFailure(TrampolineFeeInsufficient()), Some(FailureAttributionData(p.receivedAt, Some(p.receivedAt))), commit = true)) register.expectNoMessage(100 millis) } @@ -416,16 +423,16 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl import f._ val p = Seq( - createValidIncomingPacket(2000000 msat, 3000000 msat, CltvExpiry(500000), 2999000 msat, CltvExpiry(400000)), - createValidIncomingPacket(1000000 msat, 3000000 msat, CltvExpiry(500000), 2999000 msat, CltvExpiry(400000)) + createValidIncomingPacket(2000000 msat, 3000000 msat, CltvExpiry(500000), 2999000 msat, CltvExpiry(400000), TimestampMilli(153)), + createValidIncomingPacket(1000000 msat, 3000000 msat, CltvExpiry(500000), 2999000 msat, CltvExpiry(400000), TimestampMilli(486)) ) val (nodeRelayer, _) = f.createNodeRelay(p.head) - p.foreach(p => nodeRelayer ! NodeRelay.Relay(p, randomKey().publicKey)) + p.foreach(p => nodeRelayer ! NodeRelay.Relay(p, randomKey().publicKey, 0.01)) p.foreach { p => val fwd = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]] assert(fwd.channelId == p.add.channelId) - assert(fwd.message == CMD_FAIL_HTLC(p.add.id, FailureReason.LocalFailure(TrampolineFeeInsufficient()), commit = true)) + assert(fwd.message == CMD_FAIL_HTLC(p.add.id, FailureReason.LocalFailure(TrampolineFeeInsufficient()), Some(FailureAttributionData(p.receivedAt, Some(TimestampMilli(486)))), commit = true)) } register.expectNoMessage(100 millis) @@ -434,13 +441,13 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl test("fail to relay when amount is 0 (single-part)") { f => import f._ - val p = createValidIncomingPacket(5000000 msat, 5000000 msat, CltvExpiry(500000), 0 msat, CltvExpiry(490000)) + val p = createValidIncomingPacket(5000000 msat, 5000000 msat, CltvExpiry(500000), 0 msat, CltvExpiry(490000), TimestampMilli.now()) val (nodeRelayer, _) = f.createNodeRelay(p) - nodeRelayer ! NodeRelay.Relay(p, randomKey().publicKey) + nodeRelayer ! NodeRelay.Relay(p, randomKey().publicKey, 0.01) val fwd = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]] assert(fwd.channelId == p.add.channelId) - assert(fwd.message == CMD_FAIL_HTLC(p.add.id, FailureReason.LocalFailure(InvalidOnionPayload(UInt64(2), 0)), commit = true)) + assert(fwd.message == CMD_FAIL_HTLC(p.add.id, FailureReason.LocalFailure(InvalidOnionPayload(UInt64(2), 0)), Some(FailureAttributionData(p.receivedAt, Some(p.receivedAt))), commit = true)) register.expectNoMessage(100 millis) } @@ -449,16 +456,16 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl import f._ val p = Seq( - createValidIncomingPacket(4000000 msat, 5000000 msat, CltvExpiry(500000), 0 msat, CltvExpiry(490000)), - createValidIncomingPacket(1000000 msat, 5000000 msat, CltvExpiry(500000), 0 msat, CltvExpiry(490000)) + createValidIncomingPacket(4000000 msat, 5000000 msat, CltvExpiry(500000), 0 msat, CltvExpiry(490000), TimestampMilli(7)), + createValidIncomingPacket(1000000 msat, 5000000 msat, CltvExpiry(500000), 0 msat, CltvExpiry(490000), TimestampMilli(9)) ) val (nodeRelayer, _) = f.createNodeRelay(p.head) - p.foreach(p => nodeRelayer ! NodeRelay.Relay(p, randomKey().publicKey)) + p.foreach(p => nodeRelayer ! NodeRelay.Relay(p, randomKey().publicKey, 0.01)) p.foreach { p => val fwd = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]] assert(fwd.channelId == p.add.channelId) - assert(fwd.message == CMD_FAIL_HTLC(p.add.id, FailureReason.LocalFailure(InvalidOnionPayload(UInt64(2), 0)), commit = true)) + assert(fwd.message == CMD_FAIL_HTLC(p.add.id, FailureReason.LocalFailure(InvalidOnionPayload(UInt64(2), 0)), Some(FailureAttributionData(p.receivedAt, Some(TimestampMilli(9)))), commit = true)) } register.expectNoMessage(100 millis) @@ -469,7 +476,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl // Receive an upstream multi-part payment. val (nodeRelayer, _) = f.createNodeRelay(incomingMultiPart.head) - incomingMultiPart.foreach(p => nodeRelayer ! NodeRelay.Relay(p, randomKey().publicKey)) + incomingMultiPart.foreach(p => nodeRelayer ! NodeRelay.Relay(p, randomKey().publicKey, 0.01)) val getPeerInfo = register.expectMessageType[Register.ForwardNodeId[Peer.GetPeerInfo]](100 millis) getPeerInfo.message.replyTo.foreach(_ ! Peer.PeerNotFound(getPeerInfo.nodeId)) @@ -483,7 +490,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl incomingMultiPart.foreach { p => val fwd = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]] assert(fwd.channelId == p.add.channelId) - assert(fwd.message == CMD_FAIL_HTLC(p.add.id, FailureReason.LocalFailure(TrampolineFeeInsufficient()), commit = true)) + assert(fwd.message == CMD_FAIL_HTLC(p.add.id, FailureReason.LocalFailure(TrampolineFeeInsufficient()), Some(FailureAttributionData(p.receivedAt, Some(incomingMultiPart.last.receivedAt))), commit = true)) } register.expectNoMessage(100 millis) @@ -495,11 +502,11 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl // Receive an upstream multi-part payment. val incoming = Seq( - createValidIncomingPacket(outgoingAmount, outgoingAmount * 2, CltvExpiry(500000), outgoingAmount, outgoingExpiry), - createValidIncomingPacket(outgoingAmount, outgoingAmount * 2, CltvExpiry(500000), outgoingAmount, outgoingExpiry), + createValidIncomingPacket(outgoingAmount, outgoingAmount * 2, CltvExpiry(500000), outgoingAmount, outgoingExpiry, TimestampMilli(1)), + createValidIncomingPacket(outgoingAmount, outgoingAmount * 2, CltvExpiry(500000), outgoingAmount, outgoingExpiry, TimestampMilli(2)), ) val (nodeRelayer, _) = f.createNodeRelay(incoming.head, useRealPaymentFactory = true) - incoming.foreach(p => nodeRelayer ! NodeRelay.Relay(p, randomKey().publicKey)) + incoming.foreach(p => nodeRelayer ! NodeRelay.Relay(p, randomKey().publicKey, 0.01)) val getPeerInfo = register.expectMessageType[Register.ForwardNodeId[Peer.GetPeerInfo]](100 millis) getPeerInfo.message.replyTo.foreach(_ ! Peer.PeerNotFound(getPeerInfo.nodeId)) @@ -511,7 +518,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl incoming.foreach { p => val fwd = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]] assert(fwd.channelId == p.add.channelId) - assert(fwd.message == CMD_FAIL_HTLC(p.add.id, FailureReason.LocalFailure(TemporaryNodeFailure()), commit = true)) + assert(fwd.message == CMD_FAIL_HTLC(p.add.id, FailureReason.LocalFailure(TemporaryNodeFailure()), Some(FailureAttributionData(p.receivedAt, Some(incoming.last.receivedAt))), commit = true)) } register.expectNoMessage(100 millis) @@ -522,7 +529,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl // Receive an upstream multi-part payment. val (nodeRelayer, _) = f.createNodeRelay(incomingMultiPart.head, useRealPaymentFactory = true) - incomingMultiPart.foreach(p => nodeRelayer ! NodeRelay.Relay(p, randomKey().publicKey)) + incomingMultiPart.foreach(p => nodeRelayer ! NodeRelay.Relay(p, randomKey().publicKey, 0.01)) val getPeerInfo = register.expectMessageType[Register.ForwardNodeId[Peer.GetPeerInfo]](100 millis) getPeerInfo.message.replyTo.foreach(_ ! Peer.PeerNotFound(getPeerInfo.nodeId)) @@ -537,7 +544,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl incomingMultiPart.foreach { p => val fwd = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]] assert(fwd.channelId == p.add.channelId) - assert(fwd.message == CMD_FAIL_HTLC(p.add.id, FailureReason.LocalFailure(TrampolineFeeInsufficient()), commit = true)) + assert(fwd.message == CMD_FAIL_HTLC(p.add.id, FailureReason.LocalFailure(TrampolineFeeInsufficient()), Some(FailureAttributionData(p.receivedAt, Some(incomingMultiPart.last.receivedAt))), commit = true)) } register.expectNoMessage(100 millis) @@ -548,7 +555,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl // Receive an upstream multi-part payment. val (nodeRelayer, _) = f.createNodeRelay(incomingMultiPart.head, useRealPaymentFactory = true) - incomingMultiPart.foreach(p => nodeRelayer ! NodeRelay.Relay(p, randomKey().publicKey)) + incomingMultiPart.foreach(p => nodeRelayer ! NodeRelay.Relay(p, randomKey().publicKey, 0.01)) val getPeerInfo = register.expectMessageType[Register.ForwardNodeId[Peer.GetPeerInfo]](100 millis) getPeerInfo.message.replyTo.foreach(_ ! Peer.PeerNotFound(getPeerInfo.nodeId)) @@ -556,13 +563,13 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl val payFSM = mockPayFSM.expectMessageType[akka.actor.ActorRef] router.expectMessageType[RouteRequest] - val failures = RemoteFailure(outgoingAmount, Nil, Sphinx.DecryptedFailurePacket(outgoingNodeId, FinalIncorrectHtlcAmount(42 msat))) :: UnreadableRemoteFailure(outgoingAmount, Nil, ByteVector.empty) :: Nil + val failures = RemoteFailure(outgoingAmount, Nil, Sphinx.DecryptedFailurePacket(outgoingNodeId, FinalIncorrectHtlcAmount(42 msat))) :: UnreadableRemoteFailure(outgoingAmount, Nil, Sphinx.CannotDecryptFailurePacket(ByteVector.empty, None), Nil) :: Nil payFSM ! PaymentFailed(relayId, incomingMultiPart.head.add.paymentHash, failures) incomingMultiPart.foreach { p => val fwd = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]] assert(fwd.channelId == p.add.channelId) - assert(fwd.message == CMD_FAIL_HTLC(p.add.id, FailureReason.LocalFailure(FinalIncorrectHtlcAmount(42 msat)), commit = true)) + assert(fwd.message == CMD_FAIL_HTLC(p.add.id, FailureReason.LocalFailure(FinalIncorrectHtlcAmount(42 msat)), Some(FailureAttributionData(p.receivedAt, Some(incomingMultiPart.last.receivedAt))), commit = true)) } register.expectNoMessage(100 millis) @@ -573,7 +580,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl // Receive an upstream payment. val (nodeRelayer, _) = f.createNodeRelay(incomingSinglePart, useRealPaymentFactory = true) - nodeRelayer ! NodeRelay.Relay(incomingSinglePart, randomKey().publicKey) + nodeRelayer ! NodeRelay.Relay(incomingSinglePart, randomKey().publicKey, 0.01) val getPeerInfo = register.expectMessageType[Register.ForwardNodeId[Peer.GetPeerInfo]](100 millis) getPeerInfo.message.replyTo.foreach(_ ! Peer.PeerNotFound(getPeerInfo.nodeId)) @@ -591,31 +598,31 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl // Receive an upstream multi-part payment. val (nodeRelayer, parent) = f.createNodeRelay(incomingMultiPart.head) - incomingMultiPart.dropRight(1).foreach(p => nodeRelayer ! NodeRelay.Relay(p, randomKey().publicKey)) + incomingMultiPart.dropRight(1).foreach(p => nodeRelayer ! NodeRelay.Relay(p, randomKey().publicKey, 0.01)) mockPayFSM.expectNoMessage(100 millis) // we should NOT trigger a downstream payment before we received a complete upstream payment - nodeRelayer ! NodeRelay.Relay(incomingMultiPart.last, randomKey().publicKey) + nodeRelayer ! NodeRelay.Relay(incomingMultiPart.last, randomKey().publicKey, 0.01) val getPeerInfo = register.expectMessageType[Register.ForwardNodeId[Peer.GetPeerInfo]](100 millis) getPeerInfo.message.replyTo.foreach(_ ! Peer.PeerNotFound(getPeerInfo.nodeId)) val outgoingCfg = mockPayFSM.expectMessageType[SendPaymentConfig] - validateOutgoingCfg(outgoingCfg, Upstream.Hot.Trampoline(incomingMultiPart.map(p => Upstream.Hot.Channel(p.add, TimestampMilli.now(), randomKey().publicKey)).toList), 5) + validateOutgoingCfg(outgoingCfg, Upstream.Hot.Trampoline(incomingMultiPart.map(p => Upstream.Hot.Channel(p.add, p.receivedAt, randomKey().publicKey, 0.01)).toList)) val outgoingPayment = mockPayFSM.expectMessageType[SendMultiPartPayment] validateOutgoingPayment(outgoingPayment) // those are adapters for pay-fsm messages val nodeRelayerAdapters = outgoingPayment.replyTo // A first downstream HTLC is fulfilled: we should immediately forward the fulfill upstream. - nodeRelayerAdapters ! PreimageReceived(paymentHash, paymentPreimage) + nodeRelayerAdapters ! PreimageReceived(paymentHash, paymentPreimage, None) incomingMultiPart.foreach { p => val fwd = register.expectMessageType[Register.Forward[CMD_FULFILL_HTLC]] assert(fwd.channelId == p.add.channelId) - assert(fwd.message == CMD_FULFILL_HTLC(p.add.id, paymentPreimage, commit = true)) + assert(fwd.message == CMD_FULFILL_HTLC(p.add.id, paymentPreimage, Some(FulfillAttributionData(p.receivedAt, Some(incomingMultiPart.last.receivedAt), None)), commit = true)) } // If the payment FSM sends us duplicate preimage events, we should not fulfill a second time upstream. - nodeRelayerAdapters ! PreimageReceived(paymentHash, paymentPreimage) + nodeRelayerAdapters ! PreimageReceived(paymentHash, paymentPreimage, None) register.expectNoMessage(100 millis) // Once all the downstream payments have settled, we should emit the relayed event. @@ -633,7 +640,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl // Receive an upstream multi-part payment. val (nodeRelayer, parent) = f.createNodeRelay(incomingMultiPart.head) - incomingMultiPart.foreach(p => nodeRelayer ! NodeRelay.Relay(p, randomKey().publicKey)) + incomingMultiPart.foreach(p => nodeRelayer ! NodeRelay.Relay(p, randomKey().publicKey, 0.01)) val getPeerInfo = register.expectMessageType[Register.ForwardNodeId[Peer.GetPeerInfo]](100 millis) getPeerInfo.message.replyTo.foreach(_ ! Peer.PeerNotFound(getPeerInfo.nodeId)) @@ -645,11 +652,11 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl val nodeRelayerAdapters = outgoingPayment.replyTo // A first downstream HTLC is fulfilled: we immediately forward the fulfill upstream. - nodeRelayerAdapters ! PreimageReceived(paymentHash, paymentPreimage) + nodeRelayerAdapters ! PreimageReceived(paymentHash, paymentPreimage, None) val fulfills = incomingMultiPart.map { p => val fwd = register.expectMessageType[Register.Forward[CMD_FULFILL_HTLC]] assert(fwd.channelId == p.add.channelId) - assert(fwd.message == CMD_FULFILL_HTLC(p.add.id, paymentPreimage, commit = true)) + assert(fwd.message == CMD_FULFILL_HTLC(p.add.id, paymentPreimage, Some(FulfillAttributionData(p.receivedAt, Some(incomingMultiPart.last.receivedAt), None)), commit = true)) fwd } // We store the commands in our DB in case we restart before relaying them upstream. @@ -660,7 +667,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl // The remaining downstream HTLCs are failed (e.g. because a revoked commitment confirmed that doesn't include them). // The corresponding commands conflict with the previous fulfill and are ignored. - val downstreamHtlc = UpdateAddHtlc(randomBytes32(), 7, outgoingAmount, paymentHash, outgoingExpiry, TestConstants.emptyOnionPacket, None, 0.4375, None) + val downstreamHtlc = UpdateAddHtlc(randomBytes32(), 7, outgoingAmount, paymentHash, outgoingExpiry, TestConstants.emptyOnionPacket, None, 3, None) val failure = LocalFailure(outgoingAmount, Nil, HtlcOverriddenByLocalCommit(randomBytes32(), downstreamHtlc)) nodeRelayerAdapters ! PaymentFailed(relayId, incomingMultiPart.head.add.paymentHash, Seq(failure)) eventListener.expectNoMessage(100 millis) // the payment didn't succeed, but didn't fail either, so we just ignore it @@ -676,23 +683,23 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl // Receive an upstream single-part payment. val (nodeRelayer, parent) = f.createNodeRelay(incomingSinglePart) - nodeRelayer ! NodeRelay.Relay(incomingSinglePart, randomKey().publicKey) + nodeRelayer ! NodeRelay.Relay(incomingSinglePart, randomKey().publicKey, 0.01) val getPeerInfo = register.expectMessageType[Register.ForwardNodeId[Peer.GetPeerInfo]](100 millis) getPeerInfo.message.replyTo.foreach(_ ! Peer.PeerNotFound(getPeerInfo.nodeId)) val outgoingCfg = mockPayFSM.expectMessageType[SendPaymentConfig] - validateOutgoingCfg(outgoingCfg, Upstream.Hot.Trampoline(Upstream.Hot.Channel(incomingSinglePart.add, TimestampMilli.now(), randomKey().publicKey) :: Nil), 7) + validateOutgoingCfg(outgoingCfg, Upstream.Hot.Trampoline(Upstream.Hot.Channel(incomingSinglePart.add, incomingSinglePart.receivedAt, randomKey().publicKey, 0.01) :: Nil)) val outgoingPayment = mockPayFSM.expectMessageType[SendMultiPartPayment] validateOutgoingPayment(outgoingPayment) // those are adapters for pay-fsm messages val nodeRelayerAdapters = outgoingPayment.replyTo - nodeRelayerAdapters ! PreimageReceived(paymentHash, paymentPreimage) + nodeRelayerAdapters ! PreimageReceived(paymentHash, paymentPreimage, None) val incomingAdd = incomingSinglePart.add val fwd = register.expectMessageType[Register.Forward[CMD_FULFILL_HTLC]] assert(fwd.channelId == incomingAdd.channelId) - assert(fwd.message == CMD_FULFILL_HTLC(incomingAdd.id, paymentPreimage, commit = true)) + assert(fwd.message == CMD_FULFILL_HTLC(incomingAdd.id, paymentPreimage, Some(FulfillAttributionData(incomingSinglePart.receivedAt, Some(incomingSinglePart.receivedAt), None)), commit = true)) nodeRelayerAdapters ! createSuccessEvent() val relayEvent = eventListener.expectMessageType[TrampolinePaymentRelayed] @@ -710,7 +717,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl // Receive an upstream multi-part payment. val (nodeRelayer, parent) = f.createNodeRelay(incomingMultiPart.head) - incomingMultiPart.foreach(p => nodeRelayer ! NodeRelay.Relay(p, randomKey().publicKey)) + incomingMultiPart.foreach(p => nodeRelayer ! NodeRelay.Relay(p, randomKey().publicKey, 0.01)) // We first check if the outgoing node is our peer and supports wake-up notifications. val peerFeaturesRequest = register.expectMessageType[Register.ForwardNodeId[Peer.GetPeerInfo]] @@ -727,7 +734,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl cleanUpWakeUpActors(peerReadyManager, switchboard) val outgoingCfg = mockPayFSM.expectMessageType[SendPaymentConfig] - validateOutgoingCfg(outgoingCfg, Upstream.Hot.Trampoline(incomingMultiPart.map(p => Upstream.Hot.Channel(p.add, TimestampMilli.now(), randomKey().publicKey)).toList), 5) + validateOutgoingCfg(outgoingCfg, Upstream.Hot.Trampoline(incomingMultiPart.map(p => Upstream.Hot.Channel(p.add, p.receivedAt, randomKey().publicKey, 0.01)).toList)) val outgoingPayment = mockPayFSM.expectMessageType[SendMultiPartPayment] validateOutgoingPayment(outgoingPayment) @@ -738,7 +745,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl assert(fwd.message.nextPathKey_opt.isEmpty) assert(fwd.message.onion.payload.size == PaymentOnionCodecs.paymentOnionPayloadLength) // We verify that the next node is able to decrypt the onion that we will send in will_add_htlc. - val dummyAdd = UpdateAddHtlc(randomBytes32(), 0, fwd.message.amount, fwd.message.paymentHash, fwd.message.expiry, fwd.message.onion, None, 1.0, None) + val dummyAdd = UpdateAddHtlc(randomBytes32(), 0, fwd.message.amount, fwd.message.paymentHash, fwd.message.expiry, fwd.message.onion, None, 7, None) val Right(incoming) = IncomingPaymentPacket.decrypt(dummyAdd, outgoingNodeKey, nodeParams.features) assert(incoming.isInstanceOf[IncomingPaymentPacket.FinalPacket]) val finalPayload = incoming.asInstanceOf[IncomingPaymentPacket.FinalPacket].payload.asInstanceOf[FinalPayload.Standard] @@ -758,7 +765,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl // Receive an upstream multi-part payment. val (nodeRelayer, parent) = f.createNodeRelay(incomingMultiPart.head) - incomingMultiPart.foreach(p => nodeRelayer ! NodeRelay.Relay(p, randomKey().publicKey)) + incomingMultiPart.foreach(p => nodeRelayer ! NodeRelay.Relay(p, randomKey().publicKey, 0.01)) // We first check if the outgoing node is our peer and supports wake-up notifications. val peerFeaturesRequest = register.expectMessageType[Register.ForwardNodeId[Peer.GetPeerInfo]] @@ -773,7 +780,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl cleanUpWakeUpActors(peerReadyManager, switchboard) val outgoingCfg = mockPayFSM.expectMessageType[SendPaymentConfig] - validateOutgoingCfg(outgoingCfg, Upstream.Hot.Trampoline(incomingMultiPart.map(p => Upstream.Hot.Channel(p.add, TimestampMilli.now(), randomKey().publicKey)).toList), 5) + validateOutgoingCfg(outgoingCfg, Upstream.Hot.Trampoline(incomingMultiPart.map(p => Upstream.Hot.Channel(p.add, p.receivedAt, randomKey().publicKey, 0.01)).toList)) val outgoingPayment = mockPayFSM.expectMessageType[SendMultiPartPayment] validateOutgoingPayment(outgoingPayment) @@ -782,7 +789,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl incomingMultiPart.foreach { p => val fwd = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]] assert(fwd.channelId == p.add.channelId) - assert(fwd.message == CMD_FAIL_HTLC(p.add.id, FailureReason.LocalFailure(TemporaryNodeFailure()), commit = true)) + assert(fwd.message == CMD_FAIL_HTLC(p.add.id, FailureReason.LocalFailure(TemporaryNodeFailure()), Some(FailureAttributionData(p.receivedAt, Some(incomingMultiPart.last.receivedAt))), commit = true)) } parent.expectMessageType[NodeRelayer.RelayComplete] } @@ -796,16 +803,16 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl val invoice = Bolt11Invoice(Block.LivenetGenesisBlock.hash, Some(outgoingAmount * 3), paymentHash, outgoingNodeKey, Left("Some invoice"), CltvExpiryDelta(18), extraHops = List(hints), paymentMetadata = Some(hex"123456"), features = features) val incomingPayments = incomingMultiPart.map(incoming => { val innerPayload = IntermediatePayload.NodeRelay.ToNonTrampoline(incoming.innerPayload.amountToForward, outgoingAmount * 3, outgoingExpiry, outgoingNodeId, invoice) - RelayToNonTrampolinePacket(incoming.add, incoming.outerPayload, innerPayload) + RelayToNonTrampolinePacket(incoming.add, incoming.outerPayload, innerPayload, incoming.receivedAt) }) val (nodeRelayer, parent) = f.createNodeRelay(incomingPayments.head) - incomingPayments.foreach(incoming => nodeRelayer ! NodeRelay.Relay(incoming, randomKey().publicKey)) + incomingPayments.foreach(incoming => nodeRelayer ! NodeRelay.Relay(incoming, randomKey().publicKey, 0.01)) val getPeerInfo = register.expectMessageType[Register.ForwardNodeId[Peer.GetPeerInfo]](100 millis) getPeerInfo.message.replyTo.foreach(_ ! Peer.PeerNotFound(getPeerInfo.nodeId)) val outgoingCfg = mockPayFSM.expectMessageType[SendPaymentConfig] - validateOutgoingCfg(outgoingCfg, Upstream.Hot.Trampoline(incomingMultiPart.map(p => Upstream.Hot.Channel(p.add, TimestampMilli.now(), randomKey().publicKey)).toList), 5) + validateOutgoingCfg(outgoingCfg, Upstream.Hot.Trampoline(incomingMultiPart.map(p => Upstream.Hot.Channel(p.add, p.receivedAt, randomKey().publicKey, 0.01)).toList)) val outgoingPayment = mockPayFSM.expectMessageType[SendMultiPartPayment] assert(outgoingPayment.recipient.nodeId == outgoingNodeId) assert(outgoingPayment.recipient.totalAmount == outgoingAmount) @@ -819,11 +826,11 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl // those are adapters for pay-fsm messages val nodeRelayerAdapters = outgoingPayment.replyTo - nodeRelayerAdapters ! PreimageReceived(paymentHash, paymentPreimage) + nodeRelayerAdapters ! PreimageReceived(paymentHash, paymentPreimage, None) incomingMultiPart.foreach { p => val fwd = register.expectMessageType[Register.Forward[CMD_FULFILL_HTLC]] assert(fwd.channelId == p.add.channelId) - assert(fwd.message == CMD_FULFILL_HTLC(p.add.id, paymentPreimage, commit = true)) + assert(fwd.message == CMD_FULFILL_HTLC(p.add.id, paymentPreimage, Some(FulfillAttributionData(p.receivedAt, Some(incomingMultiPart.last.receivedAt), None)), commit = true)) } nodeRelayerAdapters ! createSuccessEvent() @@ -844,16 +851,16 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl assert(!invoice.features.hasFeature(BasicMultiPartPayment)) val incomingPayments = incomingMultiPart.map(incoming => { val innerPayload = IntermediatePayload.NodeRelay.ToNonTrampoline(incoming.innerPayload.amountToForward, incoming.innerPayload.amountToForward, outgoingExpiry, outgoingNodeId, invoice) - RelayToNonTrampolinePacket(incoming.add, incoming.outerPayload, innerPayload) + RelayToNonTrampolinePacket(incoming.add, incoming.outerPayload, innerPayload, incoming.receivedAt) }) val (nodeRelayer, parent) = f.createNodeRelay(incomingPayments.head) - incomingPayments.foreach(incoming => nodeRelayer ! NodeRelay.Relay(incoming, randomKey().publicKey)) + incomingPayments.foreach(incoming => nodeRelayer ! NodeRelay.Relay(incoming, randomKey().publicKey, 0.01)) val getPeerInfo = register.expectMessageType[Register.ForwardNodeId[Peer.GetPeerInfo]](100 millis) getPeerInfo.message.replyTo.foreach(_ ! Peer.PeerNotFound(getPeerInfo.nodeId)) val outgoingCfg = mockPayFSM.expectMessageType[SendPaymentConfig] - validateOutgoingCfg(outgoingCfg, Upstream.Hot.Trampoline(incomingMultiPart.map(p => Upstream.Hot.Channel(p.add, TimestampMilli.now(), randomKey().publicKey)).toList), 5) + validateOutgoingCfg(outgoingCfg, Upstream.Hot.Trampoline(incomingMultiPart.map(p => Upstream.Hot.Channel(p.add, p.receivedAt, randomKey().publicKey, 0.01)).toList)) val outgoingPayment = mockPayFSM.expectMessageType[SendPaymentToNode] assert(outgoingPayment.recipient.nodeId == outgoingNodeId) assert(outgoingPayment.amount == outgoingAmount) @@ -867,11 +874,11 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl // those are adapters for pay-fsm messages val nodeRelayerAdapters = outgoingPayment.replyTo - nodeRelayerAdapters ! PreimageReceived(paymentHash, paymentPreimage) + nodeRelayerAdapters ! PreimageReceived(paymentHash, paymentPreimage, None) incomingMultiPart.foreach { p => val fwd = register.expectMessageType[Register.Forward[CMD_FULFILL_HTLC]] assert(fwd.channelId == p.add.channelId) - assert(fwd.message == CMD_FULFILL_HTLC(p.add.id, paymentPreimage, commit = true)) + assert(fwd.message == CMD_FULFILL_HTLC(p.add.id, paymentPreimage, Some(FulfillAttributionData(p.receivedAt, Some(incomingMultiPart.last.receivedAt), None)), commit = true)) } nodeRelayerAdapters ! createSuccessEvent() @@ -888,10 +895,10 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl val incomingPayments = createIncomingPaymentsToRemoteBlindedPath(Features.empty, None) val (nodeRelayer, parent) = f.createNodeRelay(incomingPayments.head) - incomingPayments.foreach(incoming => nodeRelayer ! NodeRelay.Relay(incoming, randomKey().publicKey)) + incomingPayments.foreach(incoming => nodeRelayer ! NodeRelay.Relay(incoming, randomKey().publicKey, 0.01)) val outgoingCfg = mockPayFSM.expectMessageType[SendPaymentConfig] - validateOutgoingCfg(outgoingCfg, Upstream.Hot.Trampoline(incomingPayments.map(p => Upstream.Hot.Channel(p.add, TimestampMilli.now(), randomKey().publicKey)).toList), 5, ignoreNodeId = true) + validateOutgoingCfg(outgoingCfg, Upstream.Hot.Trampoline(incomingPayments.map(p => Upstream.Hot.Channel(p.add, p.receivedAt, randomKey().publicKey, 0.01)).toList), ignoreNodeId = true) val outgoingPayment = mockPayFSM.expectMessageType[SendPaymentToNode] assert(outgoingPayment.amount == outgoingAmount) assert(outgoingPayment.recipient.expiry == outgoingExpiry) @@ -900,11 +907,11 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl // those are adapters for pay-fsm messages val nodeRelayerAdapters = outgoingPayment.replyTo - nodeRelayerAdapters ! PreimageReceived(paymentHash, paymentPreimage) + nodeRelayerAdapters ! PreimageReceived(paymentHash, paymentPreimage, None) incomingPayments.foreach { p => val fwd = register.expectMessageType[Register.Forward[CMD_FULFILL_HTLC]] assert(fwd.channelId == p.add.channelId) - assert(fwd.message == CMD_FULFILL_HTLC(p.add.id, paymentPreimage, commit = true)) + assert(fwd.message == CMD_FULFILL_HTLC(p.add.id, paymentPreimage, Some(FulfillAttributionData(p.receivedAt, Some(incomingMultiPart.last.receivedAt), None)), commit = true)) } nodeRelayerAdapters ! createSuccessEvent() @@ -921,10 +928,10 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl val incomingPayments = createIncomingPaymentsToRemoteBlindedPath(Features(Features.BasicMultiPartPayment -> FeatureSupport.Optional), None) val (nodeRelayer, parent) = f.createNodeRelay(incomingPayments.head) - incomingPayments.foreach(incoming => nodeRelayer ! NodeRelay.Relay(incoming, randomKey().publicKey)) + incomingPayments.foreach(incoming => nodeRelayer ! NodeRelay.Relay(incoming, randomKey().publicKey, 0.01)) val outgoingCfg = mockPayFSM.expectMessageType[SendPaymentConfig] - validateOutgoingCfg(outgoingCfg, Upstream.Hot.Trampoline(incomingPayments.map(p => Upstream.Hot.Channel(p.add, TimestampMilli.now(), randomKey().publicKey)).toList), 5, ignoreNodeId = true) + validateOutgoingCfg(outgoingCfg, Upstream.Hot.Trampoline(incomingPayments.map(p => Upstream.Hot.Channel(p.add, p.receivedAt, randomKey().publicKey, 0.01)).toList), ignoreNodeId = true) val outgoingPayment = mockPayFSM.expectMessageType[SendMultiPartPayment] assert(outgoingPayment.recipient.totalAmount == outgoingAmount) assert(outgoingPayment.recipient.expiry == outgoingExpiry) @@ -933,11 +940,11 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl // those are adapters for pay-fsm messages val nodeRelayerAdapters = outgoingPayment.replyTo - nodeRelayerAdapters ! PreimageReceived(paymentHash, paymentPreimage) + nodeRelayerAdapters ! PreimageReceived(paymentHash, paymentPreimage, None) incomingPayments.foreach { p => val fwd = register.expectMessageType[Register.Forward[CMD_FULFILL_HTLC]] assert(fwd.channelId == p.add.channelId) - assert(fwd.message == CMD_FULFILL_HTLC(p.add.id, paymentPreimage, commit = true)) + assert(fwd.message == CMD_FULFILL_HTLC(p.add.id, paymentPreimage, Some(FulfillAttributionData(p.receivedAt, Some(incomingMultiPart.last.receivedAt), None)), commit = true)) } nodeRelayerAdapters ! createSuccessEvent() @@ -956,7 +963,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl val incomingPayments = createIncomingPaymentsToWalletBlindedPath(nodeParams) val (nodeRelayer, parent) = f.createNodeRelay(incomingPayments.head) - incomingPayments.foreach(incoming => nodeRelayer ! NodeRelay.Relay(incoming, randomKey().publicKey)) + incomingPayments.foreach(incoming => nodeRelayer ! NodeRelay.Relay(incoming, randomKey().publicKey, 0.01)) // The remote node is a wallet node: we try to wake them up before relaying the payment. peerReadyManager.expectMessageType[PeerReadyManager.Register].replyTo ! PeerReadyManager.Registered(outgoingNodeId, otherAttempts = 0) @@ -966,7 +973,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl cleanUpWakeUpActors(peerReadyManager, switchboard) val outgoingCfg = mockPayFSM.expectMessageType[SendPaymentConfig] - validateOutgoingCfg(outgoingCfg, Upstream.Hot.Trampoline(incomingPayments.map(p => Upstream.Hot.Channel(p.add, TimestampMilli.now(), randomKey().publicKey)).toList), 5, ignoreNodeId = true) + validateOutgoingCfg(outgoingCfg, Upstream.Hot.Trampoline(incomingPayments.map(p => Upstream.Hot.Channel(p.add, p.receivedAt, randomKey().publicKey, 0.01)).toList), ignoreNodeId = true) val outgoingPayment = mockPayFSM.expectMessageType[SendMultiPartPayment] assert(outgoingPayment.recipient.totalAmount == outgoingAmount) assert(outgoingPayment.recipient.expiry == outgoingExpiry) @@ -975,11 +982,11 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl // those are adapters for pay-fsm messages val nodeRelayerAdapters = outgoingPayment.replyTo - nodeRelayerAdapters ! PreimageReceived(paymentHash, paymentPreimage) + nodeRelayerAdapters ! PreimageReceived(paymentHash, paymentPreimage, None) incomingPayments.foreach { p => val fwd = register.expectMessageType[Register.Forward[CMD_FULFILL_HTLC]] assert(fwd.channelId == p.add.channelId) - assert(fwd.message == CMD_FULFILL_HTLC(p.add.id, paymentPreimage, commit = true)) + assert(fwd.message == CMD_FULFILL_HTLC(p.add.id, paymentPreimage, Some(FulfillAttributionData(p.receivedAt, Some(incomingMultiPart.last.receivedAt), None)), commit = true)) } nodeRelayerAdapters ! createSuccessEvent() @@ -998,7 +1005,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl val incomingPayments = createIncomingPaymentsToWalletBlindedPath(nodeParams) val (nodeRelayer, _) = f.createNodeRelay(incomingPayments.head) - incomingPayments.foreach(incoming => nodeRelayer ! NodeRelay.Relay(incoming, randomKey().publicKey)) + incomingPayments.foreach(incoming => nodeRelayer ! NodeRelay.Relay(incoming, randomKey().publicKey, 0.01)) // The remote node is a wallet node: we try to wake them up before relaying the payment, but it times out. peerReadyManager.expectMessageType[PeerReadyManager.Register].replyTo ! PeerReadyManager.Registered(outgoingNodeId, otherAttempts = 3) @@ -1009,7 +1016,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl incomingPayments.foreach { p => val fwd = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]] assert(fwd.channelId == p.add.channelId) - assert(fwd.message == CMD_FAIL_HTLC(p.add.id, FailureReason.LocalFailure(UnknownNextPeer()), commit = true)) + assert(fwd.message == CMD_FAIL_HTLC(p.add.id, FailureReason.LocalFailure(UnknownNextPeer()), Some(FailureAttributionData(p.receivedAt, Some(incomingMultiPart.last.receivedAt))), commit = true)) } } @@ -1020,7 +1027,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl val incomingPayments = createIncomingPaymentsToWalletBlindedPath(nodeParams) val (nodeRelayer, parent) = f.createNodeRelay(incomingPayments.head) - incomingPayments.foreach(incoming => nodeRelayer ! NodeRelay.Relay(incoming, randomKey().publicKey)) + incomingPayments.foreach(incoming => nodeRelayer ! NodeRelay.Relay(incoming, randomKey().publicKey, 0.01)) // The remote node is a wallet node: we wake them up before relaying the payment. peerReadyManager.expectMessageType[PeerReadyManager.Register].replyTo ! PeerReadyManager.Registered(outgoingNodeId, otherAttempts = 1) @@ -1029,7 +1036,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl cleanUpWakeUpActors(peerReadyManager, switchboard) val outgoingCfg = mockPayFSM.expectMessageType[SendPaymentConfig] - validateOutgoingCfg(outgoingCfg, Upstream.Hot.Trampoline(incomingPayments.map(p => Upstream.Hot.Channel(p.add, TimestampMilli.now(), randomKey().publicKey)).toList), 5, ignoreNodeId = true) + validateOutgoingCfg(outgoingCfg, Upstream.Hot.Trampoline(incomingPayments.map(p => Upstream.Hot.Channel(p.add, p.receivedAt, randomKey().publicKey, 0.01)).toList), ignoreNodeId = true) val outgoingPayment = mockPayFSM.expectMessageType[SendMultiPartPayment] // The outgoing payment fails because we don't have enough balance: we trigger on-the-fly funding. @@ -1039,7 +1046,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl assert(fwd.message.nextPathKey_opt.nonEmpty) assert(fwd.message.onion.payload.size == PaymentOnionCodecs.paymentOnionPayloadLength) // We verify that the next node is able to decrypt the onion that we will send in will_add_htlc. - val dummyAdd = UpdateAddHtlc(randomBytes32(), 0, fwd.message.amount, fwd.message.paymentHash, fwd.message.expiry, fwd.message.onion, fwd.message.nextPathKey_opt, 1.0, None) + val dummyAdd = UpdateAddHtlc(randomBytes32(), 0, fwd.message.amount, fwd.message.paymentHash, fwd.message.expiry, fwd.message.onion, fwd.message.nextPathKey_opt, 7, None) val Right(incoming) = IncomingPaymentPacket.decrypt(dummyAdd, outgoingNodeKey, nodeParams.features) assert(incoming.isInstanceOf[IncomingPaymentPacket.FinalPacket]) val finalPayload = incoming.asInstanceOf[IncomingPaymentPacket.FinalPacket].payload.asInstanceOf[FinalPayload.Blinded] @@ -1059,7 +1066,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl val incomingPayments = createIncomingPaymentsToWalletBlindedPath(nodeParams) val (nodeRelayer, _) = f.createNodeRelay(incomingPayments.head) - incomingPayments.foreach(incoming => nodeRelayer ! NodeRelay.Relay(incoming, randomKey().publicKey)) + incomingPayments.foreach(incoming => nodeRelayer ! NodeRelay.Relay(incoming, randomKey().publicKey, 0.01)) // The remote node is a wallet node: we wake them up before relaying the payment. peerReadyManager.expectMessageType[PeerReadyManager.Register].replyTo ! PeerReadyManager.Registered(outgoingNodeId, otherAttempts = 0) @@ -1068,7 +1075,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl cleanUpWakeUpActors(peerReadyManager, switchboard) val outgoingCfg = mockPayFSM.expectMessageType[SendPaymentConfig] - validateOutgoingCfg(outgoingCfg, Upstream.Hot.Trampoline(incomingPayments.map(p => Upstream.Hot.Channel(p.add, TimestampMilli.now(), randomKey().publicKey)).toList), 5, ignoreNodeId = true) + validateOutgoingCfg(outgoingCfg, Upstream.Hot.Trampoline(incomingPayments.map(p => Upstream.Hot.Channel(p.add, p.receivedAt, randomKey().publicKey, 0.01)).toList), ignoreNodeId = true) val outgoingPayment = mockPayFSM.expectMessageType[SendMultiPartPayment] // The outgoing payment fails because we don't have enough balance: we trigger on-the-fly funding, but can't reach our peer. @@ -1079,7 +1086,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl incomingPayments.foreach { p => val fwd = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]] assert(fwd.channelId == p.add.channelId) - assert(fwd.message == CMD_FAIL_HTLC(p.add.id, FailureReason.LocalFailure(UnknownNextPeer()), commit = true)) + assert(fwd.message == CMD_FAIL_HTLC(p.add.id, FailureReason.LocalFailure(UnknownNextPeer()), Some(FailureAttributionData(p.receivedAt, Some(incomingMultiPart.last.receivedAt))), commit = true)) } } @@ -1089,7 +1096,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl val scidDir = ShortChannelIdDir(isNode1 = true, RealShortChannelId(123456L)) val incomingPayments = createIncomingPaymentsToRemoteBlindedPath(Features.empty, Some(scidDir)) val (nodeRelayer, parent) = f.createNodeRelay(incomingPayments.head) - incomingPayments.foreach(incoming => nodeRelayer ! NodeRelay.Relay(incoming, randomKey().publicKey)) + incomingPayments.foreach(incoming => nodeRelayer ! NodeRelay.Relay(incoming, randomKey().publicKey, 0.01)) val getNodeId = router.expectMessageType[Router.GetNodeId] assert(getNodeId.isNode1 == scidDir.isNode1) @@ -1097,7 +1104,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl getNodeId.replyTo ! Some(outgoingNodeId) val outgoingCfg = mockPayFSM.expectMessageType[SendPaymentConfig] - validateOutgoingCfg(outgoingCfg, Upstream.Hot.Trampoline(incomingPayments.map(p => Upstream.Hot.Channel(p.add, TimestampMilli.now(), randomKey().publicKey)).toList), 5, ignoreNodeId = true) + validateOutgoingCfg(outgoingCfg, Upstream.Hot.Trampoline(incomingPayments.map(p => Upstream.Hot.Channel(p.add, p.receivedAt, randomKey().publicKey, 0.01)).toList), ignoreNodeId = true) val outgoingPayment = mockPayFSM.expectMessageType[SendPaymentToNode] assert(outgoingPayment.amount == outgoingAmount) assert(outgoingPayment.recipient.expiry == outgoingExpiry) @@ -1106,11 +1113,11 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl // those are adapters for pay-fsm messages val nodeRelayerAdapters = outgoingPayment.replyTo - nodeRelayerAdapters ! PreimageReceived(paymentHash, paymentPreimage) + nodeRelayerAdapters ! PreimageReceived(paymentHash, paymentPreimage, None) incomingPayments.foreach { p => val fwd = register.expectMessageType[Register.Forward[CMD_FULFILL_HTLC]] assert(fwd.channelId == p.add.channelId) - assert(fwd.message == CMD_FULFILL_HTLC(p.add.id, paymentPreimage, commit = true)) + assert(fwd.message == CMD_FULFILL_HTLC(p.add.id, paymentPreimage, Some(FulfillAttributionData(p.receivedAt, Some(incomingMultiPart.last.receivedAt), None)), commit = true)) } nodeRelayerAdapters ! createSuccessEvent() @@ -1128,7 +1135,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl val scidDir = ShortChannelIdDir(isNode1 = true, RealShortChannelId(123456L)) val incomingPayments = createIncomingPaymentsToRemoteBlindedPath(Features.empty, Some(scidDir)) val (nodeRelayer, _) = f.createNodeRelay(incomingPayments.head) - incomingPayments.foreach(incoming => nodeRelayer ! NodeRelay.Relay(incoming, randomKey().publicKey)) + incomingPayments.foreach(incoming => nodeRelayer ! NodeRelay.Relay(incoming, randomKey().publicKey, 0.01)) val getNodeId = router.expectMessageType[Router.GetNodeId] assert(getNodeId.isNode1 == scidDir.isNode1) @@ -1140,11 +1147,11 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl incomingPayments.foreach { p => val fwd = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]] assert(fwd.channelId == p.add.channelId) - assert(fwd.message == CMD_FAIL_HTLC(p.add.id, FailureReason.LocalFailure(UnknownNextPeer()), commit = true)) + assert(fwd.message == CMD_FAIL_HTLC(p.add.id, FailureReason.LocalFailure(UnknownNextPeer()), Some(FailureAttributionData(p.receivedAt, Some(incomingMultiPart.last.receivedAt))), commit = true)) } } - def validateOutgoingCfg(outgoingCfg: SendPaymentConfig, upstream: Upstream, endorsement: Int, ignoreNodeId: Boolean = false): Unit = { + def validateOutgoingCfg(outgoingCfg: SendPaymentConfig, upstream: Upstream, ignoreNodeId: Boolean = false): Unit = { assert(!outgoingCfg.publishEvent) assert(!outgoingCfg.storeInDb) assert(outgoingCfg.paymentHash == paymentHash) @@ -1154,7 +1161,6 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl case (Upstream.Hot.Trampoline(adds1), Upstream.Hot.Trampoline(adds2)) => assert(adds1.map(_.add) == adds2.map(_.add)) case _ => assert(outgoingCfg.upstream == upstream) } - assert((outgoingCfg.confidence * 7.999).toInt == endorsement) } def validateOutgoingPayment(outgoingPayment: SendMultiPartPayment): Unit = { @@ -1194,11 +1200,11 @@ object NodeRelayerSpec { val incomingAmount = 41_000_000 msat val incomingSecret = randomBytes32() val incomingMultiPart = Seq( - createValidIncomingPacket(15_000_000 msat, incomingAmount, CltvExpiry(500000), outgoingAmount, outgoingExpiry, endorsementIn = 6), - createValidIncomingPacket(15_000_000 msat, incomingAmount, CltvExpiry(499999), outgoingAmount, outgoingExpiry, endorsementIn = 5), - createValidIncomingPacket(11_000_000 msat, incomingAmount, CltvExpiry(499999), outgoingAmount, outgoingExpiry, endorsementIn = 7) + createValidIncomingPacket(15_000_000 msat, incomingAmount, CltvExpiry(500000), outgoingAmount, outgoingExpiry, TimestampMilli(1000), endorsementIn = 6), + createValidIncomingPacket(15_000_000 msat, incomingAmount, CltvExpiry(499999), outgoingAmount, outgoingExpiry, TimestampMilli(2000), endorsementIn = 5), + createValidIncomingPacket(11_000_000 msat, incomingAmount, CltvExpiry(499999), outgoingAmount, outgoingExpiry, TimestampMilli(3000), endorsementIn = 7) ) - val incomingSinglePart = createValidIncomingPacket(incomingAmount, incomingAmount, CltvExpiry(500000), outgoingAmount, outgoingExpiry) + val incomingSinglePart = createValidIncomingPacket(incomingAmount, incomingAmount, CltvExpiry(500000), outgoingAmount, outgoingExpiry, TimestampMilli(5000)) val incomingAsyncPayment: Seq[RelayToTrampolinePacket] = incomingMultiPart.map(p => p.copy(innerPayload = IntermediatePayload.NodeRelay.Standard.createNodeRelayForAsyncPayment(p.innerPayload.amountToForward, p.innerPayload.outgoingCltv, outgoingNodeId))) def asyncTimeoutHeight(nodeParams: NodeParams): BlockHeight = @@ -1208,7 +1214,7 @@ object NodeRelayerSpec { (paymentPackets.map(_.outerPayload.expiry).min - nodeParams.relayParams.asyncPaymentsParams.cancelSafetyBeforeTimeout).blockHeight def createSuccessEvent(): PaymentSent = - PaymentSent(relayId, paymentHash, paymentPreimage, outgoingAmount, outgoingNodeId, Seq(PaymentSent.PartialPayment(UUID.randomUUID(), outgoingAmount, 10 msat, randomBytes32(), None))) + PaymentSent(relayId, paymentHash, paymentPreimage, outgoingAmount, outgoingNodeId, Seq(PaymentSent.PartialPayment(UUID.randomUUID(), outgoingAmount, 10 msat, randomBytes32(), None)), None) def createTrampolinePacket(amount: MilliSatoshi, expiry: CltvExpiry): OnionRoutingPacket = { val payload = NodePayload(outgoingNodeId, FinalPayload.Standard.createPayload(amount, amount, expiry, paymentSecret)) @@ -1216,29 +1222,31 @@ object NodeRelayerSpec { onion.packet } - def createValidIncomingPacket(amountIn: MilliSatoshi, totalAmountIn: MilliSatoshi, expiryIn: CltvExpiry, amountOut: MilliSatoshi, expiryOut: CltvExpiry, endorsementIn: Int = 7): RelayToTrampolinePacket = { + def createValidIncomingPacket(amountIn: MilliSatoshi, totalAmountIn: MilliSatoshi, expiryIn: CltvExpiry, amountOut: MilliSatoshi, expiryOut: CltvExpiry, receivedAt: TimestampMilli, endorsementIn: Int = 7): RelayToTrampolinePacket = { val outerPayload = FinalPayload.Standard.createPayload(amountIn, totalAmountIn, expiryIn, incomingSecret, None) val tlvs = TlvStream[UpdateAddHtlcTlv](UpdateAddHtlcTlv.Endorsement(endorsementIn)) RelayToTrampolinePacket( UpdateAddHtlc(randomBytes32(), Random.nextInt(100), amountIn, paymentHash, expiryIn, TestConstants.emptyOnionPacket, tlvs), outerPayload, IntermediatePayload.NodeRelay.Standard(amountOut, expiryOut, outgoingNodeId), - createTrampolinePacket(amountOut, expiryOut)) + createTrampolinePacket(amountOut, expiryOut), + receivedAt) } def createPartialIncomingPacket(paymentHash: ByteVector32, paymentSecret: ByteVector32): RelayToTrampolinePacket = { val (expiryIn, expiryOut) = (CltvExpiry(500000), CltvExpiry(490000)) val amountIn = incomingAmount / 2 RelayToTrampolinePacket( - UpdateAddHtlc(randomBytes32(), Random.nextInt(100), amountIn, paymentHash, expiryIn, TestConstants.emptyOnionPacket, None, 1.0, None), + UpdateAddHtlc(randomBytes32(), Random.nextInt(100), amountIn, paymentHash, expiryIn, TestConstants.emptyOnionPacket, None, 7, None), FinalPayload.Standard.createPayload(amountIn, incomingAmount, expiryIn, paymentSecret, None), IntermediatePayload.NodeRelay.Standard(outgoingAmount, expiryOut, outgoingNodeId), - createTrampolinePacket(outgoingAmount, expiryOut)) + createTrampolinePacket(outgoingAmount, expiryOut), + TimestampMilli.now()) } def createPaymentBlindedRoute(nodeId: PublicKey, sessionKey: PrivateKey = randomKey(), pathId: ByteVector = randomBytes32()): PaymentBlindedRoute = { val selfPayload = blindedRouteDataCodec.encode(TlvStream(PathId(pathId), PaymentConstraints(CltvExpiry(1234567), 0 msat), AllowedFeatures(Features.empty))).require.bytes - PaymentBlindedRoute(Sphinx.RouteBlinding.create(sessionKey, Seq(nodeId), Seq(selfPayload)).route, PaymentInfo(1 msat, 2, CltvExpiryDelta(3), 4 msat, 5 msat, Features.empty)) + PaymentBlindedRoute(Sphinx.RouteBlinding.create(sessionKey, Seq(nodeId), Seq(selfPayload)).route, PaymentInfo(1 msat, 2, CltvExpiryDelta(3), 4 msat, 5 msat, ByteVector.empty)) } /** Create payments to a blinded path that starts at a remote node. */ @@ -1255,7 +1263,7 @@ object NodeRelayerSpec { val invoice = Bolt12Invoice(request, randomBytes32(), outgoingNodeKey, 300 seconds, features, Seq(paymentBlindedRoute)) incomingMultiPart.map(incoming => { val innerPayload = IntermediatePayload.NodeRelay.ToBlindedPaths(incoming.innerPayload.amountToForward, outgoingExpiry, invoice) - RelayToBlindedPathsPacket(incoming.add, incoming.outerPayload, innerPayload) + RelayToBlindedPathsPacket(incoming.add, incoming.outerPayload, innerPayload, incoming.receivedAt) }) } @@ -1271,7 +1279,7 @@ object NodeRelayerSpec { val invoice = Bolt12Invoice(request, randomBytes32(), outgoingNodeKey, 300 seconds, features, Seq(PaymentBlindedRoute(route, paymentInfo))) incomingMultiPart.map(incoming => { val innerPayload = IntermediatePayload.NodeRelay.ToBlindedPaths(incoming.innerPayload.amountToForward, outgoingExpiry, invoice) - RelayToBlindedPathsPacket(incoming.add, incoming.outerPayload, innerPayload) + RelayToBlindedPathsPacket(incoming.add, incoming.outerPayload, innerPayload, incoming.receivedAt) }) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/OnTheFlyFundingSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/OnTheFlyFundingSpec.scala index 793eeda47d..da6f822b49 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/OnTheFlyFundingSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/OnTheFlyFundingSpec.scala @@ -26,9 +26,11 @@ import fr.acinq.eclair.blockchain.{CurrentBlockHeight, DummyOnChainWallet} import fr.acinq.eclair.channel.Upstream.Hot import fr.acinq.eclair.channel._ import fr.acinq.eclair.crypto.Sphinx +import fr.acinq.eclair.crypto.keymanager.ChannelKeys import fr.acinq.eclair.io.Peer._ import fr.acinq.eclair.io.PendingChannelsRateLimiter.AddOrRejectChannel import fr.acinq.eclair.io.{Peer, PeerConnection, PendingChannelsRateLimiter} +import fr.acinq.eclair.reputation.Reputation import fr.acinq.eclair.wire.protocol import fr.acinq.eclair.wire.protocol._ import fr.acinq.eclair.{Alias, BlockHeight, CltvExpiry, CltvExpiryDelta, FeatureSupport, Features, MilliSatoshi, MilliSatoshiLong, NodeParams, TestConstants, TestKitBaseClass, TimestampMilli, ToMilliSatoshiConversion, UInt64, randomBytes, randomBytes32, randomKey, randomLong} @@ -76,8 +78,8 @@ class OnTheFlyFundingSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike { assert(peerInfo.state == Peer.CONNECTED) } - def openChannel(fundingAmount: Satoshi): ByteVector32 = { - peer ! Peer.OpenChannel(remoteNodeId, fundingAmount, None, None, None, None, None, None, None) + def openChannel(fundingAmount: Satoshi, channelType: SupportedChannelType = ChannelTypes.AnchorOutputsZeroFeeHtlcTx()): ByteVector32 = { + peer ! Peer.OpenChannel(remoteNodeId, fundingAmount, Some(channelType), None, None, None, None, None, None) val temporaryChannelId = channel.expectMsgType[INPUT_INIT_CHANNEL_INITIATOR].temporaryChannelId val channelId = randomBytes32() peer ! ChannelIdAssigned(channel.ref, remoteNodeId, temporaryChannelId, channelId) @@ -164,14 +166,14 @@ class OnTheFlyFundingSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike { def makeChannelData(htlcMinimum: MilliSatoshi = 1 msat, localChanges: LocalChanges = LocalChanges(Nil, Nil, Nil)): DATA_NORMAL = { val commitments = CommitmentsSpec.makeCommitments(500_000_000 msat, 500_000_000 msat, nodeParams.nodeId, remoteNodeId, announcement_opt = None) - .modify(_.params.remoteParams.htlcMinimum).setTo(htlcMinimum) + .modify(_.active).apply(_.map(_.modify(_.remoteCommitParams.htlcMinimum).setTo(htlcMinimum))) .modify(_.changes.localChanges).setTo(localChanges) - DATA_NORMAL(commitments, ShortIdAliases(Alias(42), None), None, null, None, None, None, SpliceStatus.NoSplice) + DATA_NORMAL(commitments, ShortIdAliases(Alias(42), None), None, null, SpliceStatus.NoSplice, None, None, None) } } case class FakeChannelFactory(remoteNodeId: PublicKey, channel: TestProbe) extends ChannelFactory { - override def spawn(context: ActorContext, remoteNodeId: PublicKey): ActorRef = { + override def spawn(context: ActorContext, remoteNodeId: PublicKey, channelKeys: ChannelKeys): ActorRef = { assert(remoteNodeId == remoteNodeId) channel.ref } @@ -238,7 +240,7 @@ class OnTheFlyFundingSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike { val fwd1 = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]] assert(fwd1.channelId == upstream1.add.channelId) assert(fwd1.message.id == upstream1.add.id) - assert(fwd1.message.reason == FailureReason.EncryptedDownstreamFailure(fail1.reason)) + assert(fwd1.message.reason == FailureReason.EncryptedDownstreamFailure(fail1.reason, None)) register.expectNoMessage(100 millis) val fail2 = WillFailHtlc(willAdd2.id, paymentHash, randomBytes(50)) @@ -246,7 +248,7 @@ class OnTheFlyFundingSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike { val fwd2 = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]] assert(fwd2.channelId == upstream2.add.channelId) assert(fwd2.message.id == upstream2.add.id) - assert(fwd2.message.reason == FailureReason.EncryptedDownstreamFailure(fail2.reason)) + assert(fwd2.message.reason == FailureReason.EncryptedDownstreamFailure(fail2.reason, None)) val fail3 = WillFailMalformedHtlc(willAdd3.id, paymentHash, randomBytes32(), InvalidOnionHmac(randomBytes32()).code) peerConnection.send(peer, fail3) @@ -562,8 +564,8 @@ class OnTheFlyFundingSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike { peerConnection.send(peer, open) rateLimiter.expectMsgType[AddOrRejectChannel].replyTo ! PendingChannelsRateLimiter.AcceptOpenChannel val init = channel.expectMsgType[INPUT_INIT_CHANNEL_NON_INITIATOR] - assert(!init.localParams.isChannelOpener) - assert(init.localParams.paysCommitTxFees) + assert(!init.localChannelParams.isChannelOpener) + assert(init.localChannelParams.paysCommitTxFees) assert(init.fundingContribution_opt.contains(LiquidityAds.AddFunding(requestFunding.requestedAmount, nodeParams.liquidityAdsConfig.rates_opt))) // The preimage was provided, so we fulfill upstream HTLCs. @@ -615,8 +617,8 @@ class OnTheFlyFundingSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike { peerConnection.send(peer, open3) rateLimiter.expectMsgType[AddOrRejectChannel].replyTo ! PendingChannelsRateLimiter.AcceptOpenChannel val init = channel.expectMsgType[INPUT_INIT_CHANNEL_NON_INITIATOR] - assert(!init.localParams.isChannelOpener) - assert(init.localParams.paysCommitTxFees) + assert(!init.localChannelParams.isChannelOpener) + assert(init.localChannelParams.paysCommitTxFees) assert(init.fundingContribution_opt.contains(LiquidityAds.AddFunding(requestFunding.requestedAmount, nodeParams.liquidityAdsConfig.rates_opt))) assert(channel.expectMsgType[OpenDualFundedChannel].useFeeCredit_opt.contains(3_000_000 msat)) @@ -829,7 +831,7 @@ class OnTheFlyFundingSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike { // The payments are fulfilled. val (add1, add2) = if (cmd1.paymentHash == paymentHash1) (cmd1, cmd2) else (cmd2, cmd1) - val outgoing = Seq(add1, add2).map(add => UpdateAddHtlc(purchase.channelId, randomHtlcId(), add.amount, add.paymentHash, add.cltvExpiry, add.onion, add.nextPathKey_opt, add.confidence, add.fundingFee_opt)) + val outgoing = Seq(add1, add2).map(add => UpdateAddHtlc(purchase.channelId, randomHtlcId(), add.amount, add.paymentHash, add.cltvExpiry, add.onion, add.nextPathKey_opt, add.reputationScore.endorsement, add.fundingFee_opt)) add1.replyTo ! RES_ADD_SETTLED(add1.origin, outgoing.head, HtlcResult.RemoteFulfill(UpdateFulfillHtlc(purchase.channelId, outgoing.head.id, preimage1))) verifyFulfilledUpstream(upstream1, preimage1) add2.replyTo ! RES_ADD_SETTLED(add2.origin, outgoing.last, HtlcResult.OnChainFulfill(preimage2)) @@ -881,7 +883,7 @@ class OnTheFlyFundingSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike { // The recipient fails the payments: we don't relay the failure upstream and will retry. adds1.take(2).foreach(add => { - val htlc = UpdateAddHtlc(channelId, randomHtlcId(), add.amount, paymentHash, add.cltvExpiry, add.onion, add.nextPathKey_opt, add.confidence, add.fundingFee_opt) + val htlc = UpdateAddHtlc(channelId, randomHtlcId(), add.amount, paymentHash, add.cltvExpiry, add.onion, add.nextPathKey_opt, add.reputationScore.endorsement, add.fundingFee_opt) val fail = UpdateFailHtlc(channelId, htlc.id, randomBytes(50)) add.replyTo ! RES_SUCCESS(add, purchase.channelId) add.replyTo ! RES_ADD_SETTLED(add.origin, htlc, HtlcResult.RemoteFail(fail)) @@ -901,7 +903,7 @@ class OnTheFlyFundingSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike { // The payment succeeds. adds2.foreach(add => { - val htlc = UpdateAddHtlc(channelId, randomHtlcId(), add.amount, paymentHash, add.cltvExpiry, add.onion, add.nextPathKey_opt, add.confidence, add.fundingFee_opt) + val htlc = UpdateAddHtlc(channelId, randomHtlcId(), add.amount, paymentHash, add.cltvExpiry, add.onion, add.nextPathKey_opt, add.reputationScore.endorsement, add.fundingFee_opt) add.replyTo ! RES_ADD_SETTLED(add.origin, htlc, HtlcResult.OnChainFulfill(preimage)) }) val fwds = Seq( @@ -941,7 +943,7 @@ class OnTheFlyFundingSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike { // We don't collect additional fees if they were paid from our peer's channel balance already. val cmd1 = channel.expectMsgType[CMD_ADD_HTLC] cmd1.replyTo ! RES_SUCCESS(cmd1, purchase.channelId) - val htlc = UpdateAddHtlc(channelId, 0, cmd1.amount, paymentHash, cmd1.cltvExpiry, cmd1.onion, cmd1.nextPathKey_opt, cmd1.confidence, cmd1.fundingFee_opt) + val htlc = UpdateAddHtlc(channelId, 0, cmd1.amount, paymentHash, cmd1.cltvExpiry, cmd1.onion, cmd1.nextPathKey_opt, cmd1.reputationScore.endorsement, cmd1.fundingFee_opt) assert(cmd1.fundingFee_opt.contains(LiquidityAds.FundingFee(0 msat, purchase.txId))) channel.expectNoMessage(100 millis) @@ -1013,7 +1015,7 @@ class OnTheFlyFundingSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike { cmd.replyTo ! RES_SUCCESS(cmd, purchase.channelId) channel.expectNoMessage(100 millis) - val add = UpdateAddHtlc(purchase.channelId, randomHtlcId(), cmd.amount, cmd.paymentHash, cmd.cltvExpiry, cmd.onion, cmd.nextPathKey_opt, cmd.confidence, cmd.fundingFee_opt) + val add = UpdateAddHtlc(purchase.channelId, randomHtlcId(), cmd.amount, cmd.paymentHash, cmd.cltvExpiry, cmd.onion, cmd.nextPathKey_opt, cmd.reputationScore.endorsement, cmd.fundingFee_opt) cmd.replyTo ! RES_ADD_SETTLED(cmd.origin, add, HtlcResult.RemoteFulfill(UpdateFulfillHtlc(purchase.channelId, add.id, preimage2))) verifyFulfilledUpstream(upstream2, preimage2) register.expectNoMessage(100 millis) @@ -1234,8 +1236,8 @@ object OnTheFlyFundingSpec { def upstreamChannel(amountIn: MilliSatoshi, expiryIn: CltvExpiry, paymentHash: ByteVector32 = randomBytes32(), blinded: Boolean = false): Upstream.Hot.Channel = { val pathKey = if (blinded) Some(randomKey().publicKey) else None - val add = UpdateAddHtlc(randomBytes32(), randomHtlcId(), amountIn, paymentHash, expiryIn, TestConstants.emptyOnionPacket, pathKey, 1.0, None) - Upstream.Hot.Channel(add, TimestampMilli.now(), randomKey().publicKey) + val add = UpdateAddHtlc(randomBytes32(), randomHtlcId(), amountIn, paymentHash, expiryIn, TestConstants.emptyOnionPacket, pathKey, Reputation.maxEndorsement, None) + Upstream.Hot.Channel(add, TimestampMilli.now(), randomKey().publicKey, 0.01) } def createWillAdd(amount: MilliSatoshi, paymentHash: ByteVector32, expiry: CltvExpiry, pathKey_opt: Option[PublicKey] = None): WillAddHtlc = { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/RelayerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/RelayerSpec.scala index 232996934d..af8c75a240 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/RelayerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/RelayerSpec.scala @@ -34,6 +34,7 @@ import fr.acinq.eclair.payment.OutgoingPaymentPacket.{NodePayload, buildOnion, b import fr.acinq.eclair.payment.PaymentPacketSpec._ import fr.acinq.eclair.payment.relay.Relayer._ import fr.acinq.eclair.payment.send.{ClearRecipient, TrampolinePayment} +import fr.acinq.eclair.reputation.{Reputation, ReputationRecorder} import fr.acinq.eclair.router.BaseRouterSpec.{blindedRouteFromHops, channelHopFromUpdate} import fr.acinq.eclair.router.Router.Route import fr.acinq.eclair.wire.protocol.PaymentOnion.FinalPayload @@ -47,7 +48,13 @@ import scala.concurrent.duration.DurationInt class RelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("application")) with FixtureAnyFunSuiteLike { - case class FixtureParam(nodeParams: NodeParams, relayer: akka.actor.ActorRef, router: TestProbe[Any], register: TestProbe[Any], childActors: ChildActors, paymentHandler: TestProbe[Any]) + case class FixtureParam(nodeParams: NodeParams, relayer: akka.actor.ActorRef, router: TestProbe[Any], register: TestProbe[Any], childActors: ChildActors, paymentHandler: TestProbe[Any], reputationRecorder: TestProbe[ReputationRecorder.Command]) { + def receiveConfidence(score: Reputation.Score): Unit = { + val getConfidence = reputationRecorder.expectMessageType[ReputationRecorder.GetConfidence] + assert(getConfidence.upstream.asInstanceOf[Upstream.Hot.Channel].receivedFrom == TestConstants.Alice.nodeParams.nodeId) + getConfidence.replyTo ! score + } + } override def withFixture(test: OneArgTest): Outcome = { // we are node B in the route A -> B -> C -> .... @@ -56,17 +63,18 @@ class RelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("applicat val router = TestProbe[Any]("router") val register = TestProbe[Any]("register") val paymentHandler = TestProbe[Any]("payment-handler") + val reputationRecorder = TestProbe[ReputationRecorder.Command]("reputation-recorder") val probe = TestProbe[Any]() // we can't spawn top-level actors with akka typed testKit.spawn(Behaviors.setup[Any] { context => - val relayer = context.toClassic.actorOf(Relayer.props(nodeParams, router.ref.toClassic, register.ref.toClassic, paymentHandler.ref.toClassic)) + val relayer = context.toClassic.actorOf(Relayer.props(nodeParams, router.ref.toClassic, register.ref.toClassic, paymentHandler.ref.toClassic, Some(reputationRecorder.ref))) probe.ref ! relayer Behaviors.empty[Any] }) val relayer = probe.expectMessageType[akka.actor.ActorRef] relayer ! GetChildActors(probe.ref.toClassic) val childActors = probe.expectMessageType[ChildActors] - withFixture(test.toNoArgTest(FixtureParam(nodeParams, relayer, router, register, childActors, paymentHandler))) + withFixture(test.toNoArgTest(FixtureParam(nodeParams, relayer, router, register, childActors, paymentHandler, reputationRecorder))) } val channelId_ab = randomBytes32() @@ -90,19 +98,20 @@ class RelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("applicat } // we use this to build a valid onion - val Right(payment) = buildOutgoingPayment(TestConstants.emptyOrigin, paymentHash, Route(finalAmount, hops, None), ClearRecipient(e, Features.empty, finalAmount, finalExpiry, paymentSecret), 1.0) + val Right(payment) = buildOutgoingPayment(TestConstants.emptyOrigin, paymentHash, Route(finalAmount, hops, None), ClearRecipient(e, Features.empty, finalAmount, finalExpiry, paymentSecret), Reputation.Score.max) // and then manually build an htlc - val add_ab = UpdateAddHtlc(randomBytes32(), 123456, payment.cmd.amount, payment.cmd.paymentHash, payment.cmd.cltvExpiry, payment.cmd.onion, None, 1.0, None) - relayer ! RelayForward(add_ab, priv_a.publicKey) + val add_ab = UpdateAddHtlc(randomBytes32(), 123456, payment.cmd.amount, payment.cmd.paymentHash, payment.cmd.cltvExpiry, payment.cmd.onion, None, Reputation.maxEndorsement, None) + relayer ! RelayForward(add_ab, priv_a.publicKey, 0.05) + receiveConfidence(Reputation.Score(0.3, 0.6)) register.expectMessageType[Register.Forward[CMD_ADD_HTLC]] } test("relay an htlc-add at the final node to the payment handler") { f => import f._ - val Right(payment) = buildOutgoingPayment(TestConstants.emptyOrigin, paymentHash, Route(finalAmount, hops.take(1), None), ClearRecipient(b, Features.empty, finalAmount, finalExpiry, paymentSecret), 1.0) - val add_ab = UpdateAddHtlc(channelId_ab, 123456, payment.cmd.amount, payment.cmd.paymentHash, payment.cmd.cltvExpiry, payment.cmd.onion, None, 1.0, None) - relayer ! RelayForward(add_ab, priv_a.publicKey) + val Right(payment) = buildOutgoingPayment(TestConstants.emptyOrigin, paymentHash, Route(finalAmount, hops.take(1), None), ClearRecipient(b, Features.empty, finalAmount, finalExpiry, paymentSecret), Reputation.Score.max) + val add_ab = UpdateAddHtlc(channelId_ab, 123456, payment.cmd.amount, payment.cmd.paymentHash, payment.cmd.cltvExpiry, payment.cmd.onion, None, Reputation.maxEndorsement, None) + relayer ! RelayForward(add_ab, priv_a.publicKey, 0.05) val fp = paymentHandler.expectMessageType[FinalPacket] assert(fp.add == add_ab) @@ -119,11 +128,11 @@ class RelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("applicat val finalTrampolinePayload = NodePayload(b, FinalPayload.Standard.createPayload(finalAmount, totalAmount, finalExpiry, paymentSecret)) val Right(trampolineOnion) = buildOnion(Seq(finalTrampolinePayload), paymentHash, None) val recipient = ClearRecipient(b, nodeParams.features.invoiceFeatures(), finalAmount, finalExpiry, randomBytes32(), nextTrampolineOnion_opt = Some(trampolineOnion.packet)) - val Right(payment) = buildOutgoingPayment(TestConstants.emptyOrigin, paymentHash, Route(finalAmount, Seq(channelHopFromUpdate(priv_a.publicKey, b, channelUpdate_ab)), None), recipient, 1.0) + val Right(payment) = buildOutgoingPayment(TestConstants.emptyOrigin, paymentHash, Route(finalAmount, Seq(channelHopFromUpdate(priv_a.publicKey, b, channelUpdate_ab)), None), recipient, Reputation.Score.max) assert(payment.cmd.amount == finalAmount) assert(payment.cmd.cltvExpiry == finalExpiry) - val add_ab = UpdateAddHtlc(channelId_ab, 123456, payment.cmd.amount, payment.cmd.paymentHash, payment.cmd.cltvExpiry, payment.cmd.onion, None, 1.0, None) - relayer ! RelayForward(add_ab, priv_a.publicKey) + val add_ab = UpdateAddHtlc(channelId_ab, 123456, payment.cmd.amount, payment.cmd.paymentHash, payment.cmd.cltvExpiry, payment.cmd.onion, None, Reputation.maxEndorsement, None) + relayer ! RelayForward(add_ab, priv_a.publicKey, 0.05) val fp = paymentHandler.expectMessageType[FinalPacket] assert(fp.add == add_ab) @@ -140,10 +149,10 @@ class RelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("applicat import f._ // we use this to build a valid onion - val Right(payment) = buildOutgoingPayment(TestConstants.emptyOrigin, paymentHash, Route(finalAmount, hops, None), ClearRecipient(e, Features.empty, finalAmount, finalExpiry, paymentSecret), 1.0) + val Right(payment) = buildOutgoingPayment(TestConstants.emptyOrigin, paymentHash, Route(finalAmount, hops, None), ClearRecipient(e, Features.empty, finalAmount, finalExpiry, paymentSecret), Reputation.Score.max) // and then manually build an htlc with an invalid onion (hmac) - val add_ab = UpdateAddHtlc(channelId_ab, 123456, payment.cmd.amount, payment.cmd.paymentHash, payment.cmd.cltvExpiry, payment.cmd.onion.copy(hmac = payment.cmd.onion.hmac.reverse), None, 1.0, None) - relayer ! RelayForward(add_ab, priv_a.publicKey) + val add_ab = UpdateAddHtlc(channelId_ab, 123456, payment.cmd.amount, payment.cmd.paymentHash, payment.cmd.cltvExpiry, payment.cmd.onion.copy(hmac = payment.cmd.onion.hmac.reverse), None, Reputation.maxEndorsement, None) + relayer ! RelayForward(add_ab, priv_a.publicKey, 0.05) val fail = register.expectMessageType[Register.Forward[CMD_FAIL_MALFORMED_HTLC]].message assert(fail.id == add_ab.id) @@ -160,9 +169,9 @@ class RelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("applicat val routeExpiry = CltvExpiry(nodeParams.currentBlockHeight - 10) val (_, blindedHop, recipient) = blindedRouteFromHops(finalAmount, finalExpiry, Seq(channelHopFromUpdate(b, c, channelUpdate_bc)), routeExpiry, paymentPreimage, hex"deadbeef") val route = Route(finalAmount, Seq(channelHopFromUpdate(priv_a.publicKey, b, channelUpdate_ab)), Some(blindedHop)) - val Right(payment) = buildOutgoingPayment(TestConstants.emptyOrigin, paymentHash, route, recipient, 1.0) - val add_ab = UpdateAddHtlc(channelId_ab, 0, payment.cmd.amount, payment.cmd.paymentHash, payment.cmd.cltvExpiry, payment.cmd.onion, payment.cmd.nextPathKey_opt, 1.0, payment.cmd.fundingFee_opt) - relayer ! RelayForward(add_ab, priv_a.publicKey) + val Right(payment) = buildOutgoingPayment(TestConstants.emptyOrigin, paymentHash, route, recipient, Reputation.Score.max) + val add_ab = UpdateAddHtlc(channelId_ab, 0, payment.cmd.amount, payment.cmd.paymentHash, payment.cmd.cltvExpiry, payment.cmd.onion, payment.cmd.nextPathKey_opt, Reputation.maxEndorsement, payment.cmd.fundingFee_opt) + relayer ! RelayForward(add_ab, priv_a.publicKey, 0.05) val fail = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]].message assert(fail.id == add_ab.id) @@ -177,8 +186,8 @@ class RelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("applicat // we use an expired blinded route. val Right(payment) = buildOutgoingBlindedPaymentAB(paymentHash, routeExpiry = CltvExpiry(nodeParams.currentBlockHeight - 1)) - val add_ab = UpdateAddHtlc(channelId_ab, 0, payment.cmd.amount, payment.cmd.paymentHash, payment.cmd.cltvExpiry, payment.cmd.onion, payment.cmd.nextPathKey_opt, 1.0, payment.cmd.fundingFee_opt) - relayer ! RelayForward(add_ab, priv_a.publicKey) + val add_ab = UpdateAddHtlc(channelId_ab, 0, payment.cmd.amount, payment.cmd.paymentHash, payment.cmd.cltvExpiry, payment.cmd.onion, payment.cmd.nextPathKey_opt, Reputation.maxEndorsement, payment.cmd.fundingFee_opt) + relayer ! RelayForward(add_ab, priv_a.publicKey, 0.05) val fail = register.expectMessageType[Register.Forward[CMD_FAIL_MALFORMED_HTLC]].message assert(fail.id == add_ab.id) @@ -197,8 +206,8 @@ class RelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("applicat val payment = TrampolinePayment.buildOutgoingPayment(b, invoice, finalExpiry) // and then manually build an htlc - val add_ab = UpdateAddHtlc(channelId_ab, 123456, payment.trampolineAmount, invoice.paymentHash, payment.trampolineExpiry, payment.onion.packet, None, 1.0, None) - relayer ! RelayForward(add_ab, priv_a.publicKey) + val add_ab = UpdateAddHtlc(channelId_ab, 123456, payment.trampolineAmount, invoice.paymentHash, payment.trampolineExpiry, payment.onion.packet, None, Reputation.maxEndorsement, None) + relayer ! RelayForward(add_ab, priv_a.publicKey, 0.05) val fail = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]].message assert(fail.id == add_ab.id) @@ -211,10 +220,10 @@ class RelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("applicat import f._ val replyTo = TestProbe[Any]() - val add_ab = UpdateAddHtlc(channelId_ab, 42, 11000000 msat, ByteVector32.Zeroes, CltvExpiry(4200), TestConstants.emptyOnionPacket, None, 1.0, None) - val add_bc = UpdateAddHtlc(channelId_bc, 72, 1000 msat, paymentHash, CltvExpiry(1), TestConstants.emptyOnionPacket, None, 1.0, None) - val channelOrigin = Origin.Hot(replyTo.ref.toClassic, Upstream.Hot.Channel(add_ab, TimestampMilli.now(), priv_a.publicKey)) - val trampolineOrigin = Origin.Hot(replyTo.ref.toClassic, Upstream.Hot.Trampoline(List(Upstream.Hot.Channel(add_ab, TimestampMilli.now(), priv_a.publicKey)))) + val add_ab = UpdateAddHtlc(channelId_ab, 42, 11000000 msat, ByteVector32.Zeroes, CltvExpiry(4200), TestConstants.emptyOnionPacket, None, Reputation.maxEndorsement, None) + val add_bc = UpdateAddHtlc(channelId_bc, 72, 1000 msat, paymentHash, CltvExpiry(1), TestConstants.emptyOnionPacket, None, Reputation.maxEndorsement, None) + val channelOrigin = Origin.Hot(replyTo.ref.toClassic, Upstream.Hot.Channel(add_ab, TimestampMilli.now(), priv_a.publicKey, 0.1)) + val trampolineOrigin = Origin.Hot(replyTo.ref.toClassic, Upstream.Hot.Trampoline(List(Upstream.Hot.Channel(add_ab, TimestampMilli.now(), priv_a.publicKey, 0.1)))) val addSettled = Seq( RES_ADD_SETTLED(channelOrigin, add_bc, HtlcResult.OnChainFulfill(randomBytes32())), @@ -237,9 +246,9 @@ class RelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("applicat import f._ val replyTo = TestProbe[Any]() - val add_ab = UpdateAddHtlc(channelId_ab, 17, 50_000 msat, paymentHash, CltvExpiry(800_000), TestConstants.emptyOnionPacket, None, 1.0, None) - val add_bc = UpdateAddHtlc(channelId_bc, 21, 45_000 msat, paymentHash, CltvExpiry(799_000), TestConstants.emptyOnionPacket, None, 1.0, Some(LiquidityAds.FundingFee(1000 msat, TxId(randomBytes32())))) - val originHot = Origin.Hot(replyTo.ref.toClassic, Upstream.Hot.Channel(add_ab, TimestampMilli.now(), randomKey().publicKey)) + val add_ab = UpdateAddHtlc(channelId_ab, 17, 50_000 msat, paymentHash, CltvExpiry(800_000), TestConstants.emptyOnionPacket, None, Reputation.maxEndorsement, None) + val add_bc = UpdateAddHtlc(channelId_bc, 21, 45_000 msat, paymentHash, CltvExpiry(799_000), TestConstants.emptyOnionPacket, None, Reputation.maxEndorsement, Some(LiquidityAds.FundingFee(1000 msat, TxId(randomBytes32())))) + val originHot = Origin.Hot(replyTo.ref.toClassic, Upstream.Hot.Channel(add_ab, TimestampMilli.now(), randomKey().publicKey, 0.1)) val originCold = Origin.Cold(originHot) val addFulfilled = Seq( diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/send/BlindedPathsResolverSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/send/BlindedPathsResolverSpec.scala index ab2a0ac59d..ea98870159 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/send/BlindedPathsResolverSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/send/BlindedPathsResolverSpec.scala @@ -34,7 +34,7 @@ import fr.acinq.eclair.wire.protocol.OfferTypes.PaymentInfo import fr.acinq.eclair.{Alias, BlockHeight, CltvExpiry, CltvExpiryDelta, EncodedNodeId, Features, MilliSatoshiLong, NodeParams, RealShortChannelId, TestConstants, randomBytes32, randomKey} import org.scalatest.Outcome import org.scalatest.funsuite.FixtureAnyFunSuiteLike -import scodec.bits.HexStringSyntax +import scodec.bits.{ByteVector, HexStringSyntax} import scala.concurrent.duration.DurationInt @@ -61,7 +61,7 @@ class BlindedPathsResolverSpec extends ScalaTestWithActorTestKit(ConfigFactory.l val probe = TestProbe() val Seq(a, b, c) = Seq(randomKey(), randomKey(), randomKey()).map(_.publicKey) - val paymentInfo = PaymentInfo(100 msat, 250, CltvExpiryDelta(36), 1 msat, 50_000_000 msat, Features.empty) + val paymentInfo = PaymentInfo(100 msat, 250, CltvExpiryDelta(36), 1 msat, 50_000_000 msat, ByteVector.empty) val blindedPaths = Seq( RouteBlinding.create(randomKey(), Seq(a), Seq(hex"deadbeef")), RouteBlinding.create(randomKey(), Seq(b, randomKey().publicKey), Seq(hex"deadbeef", hex"deadbeef")), @@ -87,7 +87,7 @@ class BlindedPathsResolverSpec extends ScalaTestWithActorTestKit(ConfigFactory.l val introductionNodeId = randomKey().publicKey val scidDir = EncodedNodeId.ShortChannelIdDir(isNode1 = false, RealShortChannelId(BlockHeight(750_000), 3, 7)) val route = RouteBlinding.create(randomKey(), Seq(introductionNodeId), Seq(hex"deadbeef")).route.copy(firstNodeId = scidDir) - val paymentInfo = PaymentInfo(100 msat, 250, CltvExpiryDelta(36), 1 msat, 50_000_000 msat, Features.empty) + val paymentInfo = PaymentInfo(100 msat, 250, CltvExpiryDelta(36), 1 msat, 50_000_000 msat, ByteVector.empty) resolver ! Resolve(probe.ref, Seq(PaymentBlindedRoute(route, paymentInfo))) // We must resolve the scid_dir to a node_id. val routerReq = router.expectMsgType[Router.GetNodeId] @@ -220,7 +220,7 @@ class BlindedPathsResolverSpec extends ScalaTestWithActorTestKit(ConfigFactory.l BlindedRouteCreation.createBlindedRouteToWallet(ChannelHop(scid, nodeParams.nodeId, edgeLowFees.targetNodeId, HopRelayParams.FromHint(edgeLowFees)), hex"deadbeef", 1 msat, CltvExpiry(800_000)).route, // We reject blinded routes that cannot be decrypted. BlindedRouteCreation.createBlindedRouteFromHops(Seq(ChannelHop(scid, nodeParams.nodeId, edgeLowFees.targetNodeId, HopRelayParams.FromHint(edgeLowFees))), edgeLowFees.targetNodeId, hex"deadbeef", 1 msat, CltvExpiry(800_000)).route.copy(firstPathKey = randomKey().publicKey) - ).map(r => PaymentBlindedRoute(r, PaymentInfo(1_000_000 msat, 2500, CltvExpiryDelta(300), 1 msat, 500_000_000 msat, Features.empty))) + ).map(r => PaymentBlindedRoute(r, PaymentInfo(1_000_000 msat, 2500, CltvExpiryDelta(300), 1 msat, 500_000_000 msat, ByteVector.empty))) resolver ! Resolve(probe.ref, toResolve) // The routes with low fees or expiry require resolving the next node. register.expectMsgType[Register.GetNextNodeId].replyTo ! Some(edgeLowFees.targetNodeId) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/send/OfferPaymentSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/send/OfferPaymentSpec.scala index af5e1f2f79..37ac2c3838 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/send/OfferPaymentSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/send/OfferPaymentSpec.scala @@ -34,7 +34,7 @@ import fr.acinq.eclair.wire.protocol.{OfferTypes, OnionMessagePayloadTlv, TlvStr import fr.acinq.eclair.{CltvExpiryDelta, Features, MilliSatoshiLong, NodeParams, TestConstants, randomBytes, randomBytes32, randomKey} import org.scalatest.Outcome import org.scalatest.funsuite.FixtureAnyFunSuiteLike -import scodec.bits.HexStringSyntax +import scodec.bits.{ByteVector, HexStringSyntax} import scala.concurrent.duration.DurationInt @@ -74,7 +74,7 @@ class OfferPaymentSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("app val Right(invoiceRequest) = InvoiceRequest.validate(message.get[OnionMessagePayloadTlv.InvoiceRequest].get.tlvs) val preimage = randomBytes32() - val paymentRoute = PaymentBlindedRoute(RouteBlinding.create(randomKey(), Seq(merchantKey.publicKey), Seq(hex"7777")).route, PaymentInfo(0 msat, 0, CltvExpiryDelta(0), 0 msat, 1_000_000_000 msat, Features.empty)) + val paymentRoute = PaymentBlindedRoute(RouteBlinding.create(randomKey(), Seq(merchantKey.publicKey), Seq(hex"7777")).route, PaymentInfo(0 msat, 0, CltvExpiryDelta(0), 0 msat, 1_000_000_000 msat, ByteVector.empty)) val invoice = Bolt12Invoice(invoiceRequest, preimage, merchantKey, 1 minute, Features.empty, Seq(paymentRoute)) replyTo ! Postman.Response(InvoicePayload(TlvStream(OnionMessagePayloadTlv.Invoice(invoice.records)), TlvStream.empty)) val send = paymentInitiator.expectMsgType[SendPaymentToNode] @@ -121,7 +121,7 @@ class OfferPaymentSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("app val Right(invoiceRequest) = InvoiceRequest.validate(message.get[OnionMessagePayloadTlv.InvoiceRequest].get.tlvs) val preimage = randomBytes32() - val paymentRoute = PaymentBlindedRoute(RouteBlinding.create(randomKey(), Seq(merchantKey.publicKey), Seq(hex"7777")).route, PaymentInfo(0 msat, 0, CltvExpiryDelta(0), 0 msat, 1_000_000_000 msat, Features.empty)) + val paymentRoute = PaymentBlindedRoute(RouteBlinding.create(randomKey(), Seq(merchantKey.publicKey), Seq(hex"7777")).route, PaymentInfo(0 msat, 0, CltvExpiryDelta(0), 0 msat, 1_000_000_000 msat, ByteVector.empty)) val invoice = Bolt12Invoice(invoiceRequest, preimage, randomKey(), 1 minute, Features.empty, Seq(paymentRoute)) replyTo ! Postman.Response(InvoicePayload(TlvStream(OnionMessagePayloadTlv.Invoice(invoice.records)), TlvStream.empty)) @@ -148,7 +148,7 @@ class OfferPaymentSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("app val Right(invoiceRequest) = InvoiceRequest.validate(message.get[OnionMessagePayloadTlv.InvoiceRequest].get.tlvs) val preimage = randomBytes32() - val paymentRoute = PaymentBlindedRoute(RouteBlinding.create(randomKey(), Seq(merchantKey.publicKey), Seq(hex"7777")).route, PaymentInfo(0 msat, 0, CltvExpiryDelta(0), 0 msat, 1_000_000_000 msat, Features.empty)) + val paymentRoute = PaymentBlindedRoute(RouteBlinding.create(randomKey(), Seq(merchantKey.publicKey), Seq(hex"7777")).route, PaymentInfo(0 msat, 0, CltvExpiryDelta(0), 0 msat, 1_000_000_000 msat, ByteVector.empty)) val blindedMerchantKey = RouteBlinding.derivePrivateKey(merchantKey, route.lastPathKey) val invoice = Bolt12Invoice(invoiceRequest, preimage, blindedMerchantKey, 1 minute, Features.empty, Seq(paymentRoute)) replyTo ! Postman.Response(InvoicePayload(TlvStream(OnionMessagePayloadTlv.Invoice(invoice.records)), TlvStream.empty)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/reputation/ReputationRecorderSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/reputation/ReputationRecorderSpec.scala new file mode 100644 index 0000000000..1cbbe2b3c5 --- /dev/null +++ b/eclair-core/src/test/scala/fr/acinq/eclair/reputation/ReputationRecorderSpec.scala @@ -0,0 +1,251 @@ +/* + * Copyright 2025 ACINQ SAS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package fr.acinq.eclair.reputation + +import akka.actor.testkit.typed.scaladsl.{ScalaTestWithActorTestKit, TestProbe} +import akka.actor.typed.ActorRef +import akka.actor.typed.eventstream.EventStream +import akka.testkit.TestKit.awaitCond +import com.typesafe.config.ConfigFactory +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey +import fr.acinq.eclair.channel.{OutgoingHtlcAdded, OutgoingHtlcFailed, OutgoingHtlcFulfilled, Upstream} +import fr.acinq.eclair.reputation.ReputationRecorder._ +import fr.acinq.eclair.wire.protocol.{TlvStream, UpdateAddHtlc, UpdateAddHtlcTlv, UpdateFailHtlc, UpdateFulfillHtlc} +import fr.acinq.eclair.{BlockHeight, CltvExpiry, MilliSatoshi, MilliSatoshiLong, TimestampMilli, randomBytes, randomBytes32, randomKey, randomLong} +import org.scalatest.Outcome +import org.scalatest.funsuite.FixtureAnyFunSuiteLike + +import scala.concurrent.duration.DurationInt + +class ReputationRecorderSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("application")) with FixtureAnyFunSuiteLike { + val originNode: PublicKey = randomKey().publicKey + + case class FixtureParam(config: Reputation.Config, reputationRecorder: ActorRef[Command], replyTo: TestProbe[Reputation.Score]) + + override def withFixture(test: OneArgTest): Outcome = { + val config = Reputation.Config(enabled = true, 1 day, 10 minutes) + val replyTo = TestProbe[Reputation.Score]("confidence") + val reputationRecorder = testKit.spawn(ReputationRecorder(config)) + withFixture(test.toNoArgTest(FixtureParam(config, reputationRecorder.ref, replyTo))) + } + + def makeChannelUpstream(nodeId: PublicKey, endorsement: Int, amount: MilliSatoshi = 1000000 msat): Upstream.Hot.Channel = + Upstream.Hot.Channel(UpdateAddHtlc(randomBytes32(), randomLong(), amount, randomBytes32(), CltvExpiry(1234), null, TlvStream(UpdateAddHtlcTlv.Endorsement(endorsement))), TimestampMilli.now(), nodeId, 0.25) + + def makeOutgoingHtlcAdded(upstream: Upstream.Hot, downstream: PublicKey, fee: MilliSatoshi, expiry: CltvExpiry): OutgoingHtlcAdded = + OutgoingHtlcAdded(UpdateAddHtlc(randomBytes32(), randomLong(), 100000 msat, randomBytes32(), expiry, null, TlvStream.empty), downstream, upstream, fee) + + def makeOutgoingHtlcFulfilled(add: UpdateAddHtlc): OutgoingHtlcFulfilled = + OutgoingHtlcFulfilled(UpdateFulfillHtlc(add.channelId, add.id, randomBytes32(), TlvStream.empty)) + + def makeOutgoingHtlcFailed(add: UpdateAddHtlc): OutgoingHtlcFailed = + OutgoingHtlcFailed(UpdateFailHtlc(add.channelId, add.id, randomBytes(100), TlvStream.empty)) + + test("channel relay") { f => + import f._ + + val (nextA, nextB) = (randomKey().publicKey, randomKey().publicKey) + + val upstream1 = makeChannelUpstream(originNode, 7) + reputationRecorder ! GetConfidence(replyTo.ref, upstream1, Some(nextA), 2000 msat, BlockHeight(0), CltvExpiry(2)) + replyTo.expectMessage(Reputation.Score(0.0, 0.0)) + val added1 = makeOutgoingHtlcAdded(upstream1, nextA, 2000 msat, CltvExpiry(2)) + reputationRecorder ! WrappedOutgoingHtlcAdded(added1) + reputationRecorder ! WrappedOutgoingHtlcSettled(makeOutgoingHtlcFulfilled(added1.add)) + val upstream2 = makeChannelUpstream(originNode, 7) + awaitCond({ + reputationRecorder ! GetConfidence(replyTo.ref, upstream2, Some(nextB), 1000 msat, BlockHeight(0), CltvExpiry(2)) + val score = replyTo.expectMessageType[Reputation.Score] + score.incomingConfidence === (2.0 / 4) +- 0.001 && score.outgoingConfidence == 0.0 + }, max = 60 seconds) + val added2 = makeOutgoingHtlcAdded(upstream2, nextB, 1000 msat, CltvExpiry(2)) + reputationRecorder ! WrappedOutgoingHtlcAdded(added2) + awaitCond({ + reputationRecorder ! GetConfidence(replyTo.ref, makeChannelUpstream(originNode, 7), Some(nextA), 3000 msat, BlockHeight(0), CltvExpiry(2)) + val score = replyTo.expectMessageType[Reputation.Score] + score.incomingConfidence === (2.0 / 10) +- 0.001 && score.outgoingConfidence === (2.0 / 8) +- 0.001 + }, max = 60 seconds) + val upstream3 = makeChannelUpstream(originNode, 7) + reputationRecorder ! GetConfidence(replyTo.ref, upstream3, Some(nextB), 1000 msat, BlockHeight(0), CltvExpiry(2)) + assert({ + val score = replyTo.expectMessageType[Reputation.Score] + score.incomingConfidence === (2.0 / 6) +- 0.001 && score.outgoingConfidence == 0.0 + }) + val added3 = makeOutgoingHtlcAdded(upstream3, nextB, 1000 msat, CltvExpiry(2)) + reputationRecorder ! WrappedOutgoingHtlcAdded(added3) + reputationRecorder ! WrappedOutgoingHtlcSettled(makeOutgoingHtlcFulfilled(added3.add)) + reputationRecorder ! WrappedOutgoingHtlcSettled(makeOutgoingHtlcFailed(added2.add)) + // Not endorsed + awaitCond({ + reputationRecorder ! GetConfidence(replyTo.ref, makeChannelUpstream(originNode, 0), Some(nextA), 1000 msat, BlockHeight(0), CltvExpiry(2)) + val score = replyTo.expectMessageType[Reputation.Score] + score.incomingConfidence == 0.0 && score.outgoingConfidence === (2.0 / 4) +- 0.001 + }, max = 60 seconds) + // Different origin node + reputationRecorder ! GetConfidence(replyTo.ref, makeChannelUpstream(randomKey().publicKey, 7), Some(randomKey().publicKey), 1000 msat, BlockHeight(0), CltvExpiry(2)) + assert({ + val score = replyTo.expectMessageType[Reputation.Score] + score.incomingConfidence == 0.0 && score.outgoingConfidence == 0.0 + }) + // Very large HTLC + reputationRecorder ! GetConfidence(replyTo.ref, makeChannelUpstream(originNode, 7), Some(nextA), 100000000 msat, BlockHeight(0), CltvExpiry(2)) + assert({ + val score = replyTo.expectMessageType[Reputation.Score] + score.incomingConfidence === 0.0 +- 0.001 && score.outgoingConfidence === 0.0 +- 0.001 + }) + } + + test("trampoline relay") { f => + import f._ + + val (a, b, c) = (randomKey().publicKey, randomKey().publicKey, randomKey().publicKey) + val (d, e) = (randomKey().publicKey, randomKey().publicKey) + + val upstream1 = Upstream.Hot.Trampoline(makeChannelUpstream(a, 7, 20000 msat) :: makeChannelUpstream(b, 7, 40000 msat) :: makeChannelUpstream(c, 0, 10000 msat) :: makeChannelUpstream(c, 2, 20000 msat) :: makeChannelUpstream(c, 2, 30000 msat) :: Nil) + reputationRecorder ! GetConfidence(replyTo.ref, upstream1, Some(d), 12000 msat, BlockHeight(0), CltvExpiry(2)) + assert({ + val score = replyTo.expectMessageType[Reputation.Score] + score.incomingConfidence == 0.0 && score.outgoingConfidence == 0.0 + }) + val added1 = makeOutgoingHtlcAdded(upstream1, d, 6000 msat, CltvExpiry(2)) + reputationRecorder ! WrappedOutgoingHtlcAdded(added1) + reputationRecorder ! WrappedOutgoingHtlcSettled(makeOutgoingHtlcFulfilled(added1.add)) + val upstream2 = Upstream.Hot.Trampoline(makeChannelUpstream(a, 7, 10000 msat) :: makeChannelUpstream(c, 0, 10000 msat) :: Nil) + awaitCond({ + reputationRecorder ! GetConfidence(replyTo.ref, upstream2, Some(d), 2000 msat, BlockHeight(0), CltvExpiry(2)) + val score = replyTo.expectMessageType[Reputation.Score] + score.incomingConfidence === (1.0 / 3) +- 0.001 && score.outgoingConfidence === (6.0 / 10) +- 0.001 + }, max = 60 seconds) + val added2 = makeOutgoingHtlcAdded(upstream2, d, 2000 msat, CltvExpiry(2)) + reputationRecorder ! WrappedOutgoingHtlcAdded(added2) + val upstream3 = Upstream.Hot.Trampoline(makeChannelUpstream(a, 0, 10000 msat) :: makeChannelUpstream(b, 7, 15000 msat) :: makeChannelUpstream(b, 7, 5000 msat) :: Nil) + awaitCond({ + reputationRecorder ! GetConfidence(replyTo.ref, upstream3, Some(e), 3000 msat, BlockHeight(0), CltvExpiry(2)) + val score = replyTo.expectMessageType[Reputation.Score] + score.incomingConfidence == 0.0 && score.outgoingConfidence == 0.0 + }, max = 60 seconds) + val added3 = makeOutgoingHtlcAdded(upstream3, e, 3000 msat, CltvExpiry(2)) + reputationRecorder ! WrappedOutgoingHtlcAdded(added3) + reputationRecorder ! WrappedOutgoingHtlcSettled(makeOutgoingHtlcFailed(added2.add)) + reputationRecorder ! WrappedOutgoingHtlcSettled(makeOutgoingHtlcFulfilled(added3.add)) + + awaitCond({ + reputationRecorder ! GetConfidence(replyTo.ref, makeChannelUpstream(a, 7), Some(d), 1000 msat, BlockHeight(0), CltvExpiry(2)) + val score = replyTo.expectMessageType[Reputation.Score] + score.incomingConfidence === (2.0 / 4) +- 0.001 && score.outgoingConfidence === (6.0 / 8) +- 0.001 + }, max = 60 seconds) + reputationRecorder ! GetConfidence(replyTo.ref, makeChannelUpstream(a, 0), Some(d), 1000 msat, BlockHeight(0), CltvExpiry(2)) + assert({ + val score = replyTo.expectMessageType[Reputation.Score] + score.incomingConfidence === (1.0 / 3) +- 0.001 && score.outgoingConfidence === (6.0 / 8) +- 0.001 + }) + reputationRecorder ! GetConfidence(replyTo.ref, makeChannelUpstream(b, 7), Some(d), 1000 msat, BlockHeight(0), CltvExpiry(2)) + assert({ + val score = replyTo.expectMessageType[Reputation.Score] + score.incomingConfidence === (4.0 / 6) +- 0.001 && score.outgoingConfidence === (6.0 / 8) +- 0.001 + }) + reputationRecorder ! GetConfidence(replyTo.ref, makeChannelUpstream(b, 0), Some(e), 1000 msat, BlockHeight(0), CltvExpiry(2)) + assert({ + val score = replyTo.expectMessageType[Reputation.Score] + score.incomingConfidence == 0.0 && score.outgoingConfidence === (3.0 / 5) +- 0.001 + }) + reputationRecorder ! GetConfidence(replyTo.ref, makeChannelUpstream(c, 0), Some(e), 1000 msat, BlockHeight(0), CltvExpiry(2)) + assert({ + val score = replyTo.expectMessageType[Reputation.Score] + score.incomingConfidence === (3.0 / 5) +- 0.001 && score.outgoingConfidence === (3.0 / 5) +- 0.001 + }) + } + + test("basic attack") { f => + import f._ + + val nextNode = randomKey().publicKey + + // Our peer builds a good reputation by sending successful endorsed payments + for (_ <- 1 to 200) { + val upstream = makeChannelUpstream(originNode, 7) + val added = makeOutgoingHtlcAdded(upstream, nextNode, 10000 msat, CltvExpiry(2)) + reputationRecorder ! WrappedOutgoingHtlcAdded(added) + reputationRecorder ! WrappedOutgoingHtlcSettled(makeOutgoingHtlcFulfilled(added.add)) + } + awaitCond({ + reputationRecorder ! GetConfidence(replyTo.ref, makeChannelUpstream(originNode, 7), Some(nextNode), 10000 msat, BlockHeight(0), CltvExpiry(2)) + val score = replyTo.expectMessageType[Reputation.Score] + (score.incomingConfidence === 0.99 +- 0.01) && (score.outgoingConfidence === 0.99 +- 0.01) + }, max = 60 seconds) + + // HTLCs with lower endorsement don't benefit from this high reputation. + for (endorsement <- 0 to 6) { + reputationRecorder ! GetConfidence(replyTo.ref, makeChannelUpstream(originNode, endorsement), Some(nextNode), 10000 msat, BlockHeight(0), CltvExpiry(2)) + assert(replyTo.expectMessageType[Reputation.Score].incomingConfidence == 0.0) + } + + // The attack starts, HTLCs stay pending. + for (_ <- 1 to 100) { + val upstream = makeChannelUpstream(originNode, 7) + val added = makeOutgoingHtlcAdded(upstream, nextNode, 10000 msat, CltvExpiry(2)) + reputationRecorder ! WrappedOutgoingHtlcAdded(added) + } + awaitCond({ + reputationRecorder ! GetConfidence(replyTo.ref, makeChannelUpstream(originNode, 7), Some(nextNode), 10000 msat, BlockHeight(0), CltvExpiry(2)) + replyTo.expectMessageType[Reputation.Score].incomingConfidence < 1.0 / 2 + }, max = 60 seconds) + } + + test("sink attack") {f => + import f._ + + val (a, b, c) = (randomKey().publicKey, randomKey().publicKey, randomKey().publicKey) + val attacker = randomKey().publicKey + + // A, B and C are good nodes with a good reputation. + for (node <- Seq(a, b, c)) { + val upstream = makeChannelUpstream(node, 7) + val added = makeOutgoingHtlcAdded(upstream, randomKey().publicKey, 10000000 msat, CltvExpiry(2)) + reputationRecorder ! WrappedOutgoingHtlcAdded(added) + reputationRecorder ! WrappedOutgoingHtlcSettled(makeOutgoingHtlcFulfilled(added.add)) + awaitCond({ + reputationRecorder ! GetConfidence(replyTo.ref, makeChannelUpstream(node, 7), Some(attacker), 10000 msat, BlockHeight(0), CltvExpiry(2)) + val score = replyTo.expectMessageType[Reputation.Score] + score.incomingConfidence > 0.9 && score.outgoingConfidence == 0.0 + }, max = 60 seconds) + } + + // The attacker attracts payments by setting low fees and builds its outgoing reputation. + for (node <- Seq(a, b, c)) { + for (_ <- 1 to 100) { + val upstream = makeChannelUpstream(node, 7) + val added = makeOutgoingHtlcAdded(upstream, attacker, 10000 msat, CltvExpiry(2)) + reputationRecorder ! WrappedOutgoingHtlcAdded(added) + reputationRecorder ! WrappedOutgoingHtlcSettled(makeOutgoingHtlcFulfilled(added.add)) + } + } + + // When the attack starts, the outgoing confidence goes down quickly. + for (node <- Seq(a, b, c)) { + for (_ <- 1 to 50) { + val upstream = makeChannelUpstream(node, 7) + val added = makeOutgoingHtlcAdded(upstream, attacker, 10000 msat, CltvExpiry(2)) + reputationRecorder ! WrappedOutgoingHtlcAdded(added) + } + } + awaitCond({ + reputationRecorder ! GetConfidence(replyTo.ref, makeChannelUpstream(a, 7), Some(attacker), 10000 msat, BlockHeight(0), CltvExpiry(2)) + replyTo.expectMessageType[Reputation.Score].outgoingConfidence < 1.0 / 2 + }, max = 60 seconds) + } +} \ No newline at end of file diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/reputation/ReputationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/reputation/ReputationSpec.scala new file mode 100644 index 0000000000..efcc7ff7b2 --- /dev/null +++ b/eclair-core/src/test/scala/fr/acinq/eclair/reputation/ReputationSpec.scala @@ -0,0 +1,112 @@ +/* + * Copyright 2025 ACINQ SAS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package fr.acinq.eclair.reputation + +import fr.acinq.eclair.reputation.Reputation._ +import fr.acinq.eclair.wire.protocol.{TlvStream, UpdateAddHtlc} +import fr.acinq.eclair.{BlockHeight, CltvExpiry, MilliSatoshiLong, TimestampMilli, randomBytes32, randomLong} +import org.scalactic.Tolerance.convertNumericToPlusOrMinusWrapper +import org.scalatest.funsuite.AnyFunSuite + +import scala.concurrent.duration.DurationInt + +class ReputationSpec extends AnyFunSuite { + def makeAdd(expiry: CltvExpiry): UpdateAddHtlc = UpdateAddHtlc(randomBytes32(), randomLong(), 100000 msat, randomBytes32(), expiry, null, TlvStream.empty) + + test("basic, single endorsement level") { + var r = Reputation.init(Config(enabled = true, 1 day, 10 minutes)) + assert(r.getConfidence(10000 msat, 0, BlockHeight(0), CltvExpiry(5)) == 0) + val add1 = makeAdd(CltvExpiry(5)) + r = r.addPendingHtlc(add1, 10000 msat, 0) + r = r.settlePendingHtlc(HtlcId(add1), isSuccess = true) + assert(r.getConfidence(10000 msat, 0, BlockHeight(0), CltvExpiry(2)) === (1.0 / 3) +- 0.001) + val add2 = makeAdd(CltvExpiry(2)) + r = r.addPendingHtlc(add2, 10000 msat, 0) + assert(r.getConfidence(10000 msat, 0, BlockHeight(0), CltvExpiry(3)) === (1.0 / 6) +- 0.001) + val add3 = makeAdd(CltvExpiry(3)) + r = r.addPendingHtlc(add3, 10000 msat, 0) + r = r.settlePendingHtlc(HtlcId(add2), isSuccess = true) + r = r.settlePendingHtlc(HtlcId(add3), isSuccess = true) + assert(r.getConfidence(1 msat, 0, BlockHeight(0), CltvExpiry(4)) === 1.0 +- 0.001) + val add4 = makeAdd(CltvExpiry(4)) + r = r.addPendingHtlc(add4, 1 msat, 0) + assert(r.getConfidence(40000 msat, 0, BlockHeight(0), CltvExpiry(2)) === (3.0 / 11) +- 0.001) + val add5 = makeAdd(CltvExpiry(2)) + r = r.addPendingHtlc(add5, 40000 msat, 0) + assert(r.getConfidence(10000 msat, 0, BlockHeight(0), CltvExpiry(3)) === (3.0 / 14) +- 0.001) + val add6 = makeAdd(CltvExpiry(3)) + r = r.addPendingHtlc(add6, 10000 msat, 0) + r = r.settlePendingHtlc(HtlcId(add6), isSuccess = false) + assert(r.getConfidence(10000 msat, 0, BlockHeight(0), CltvExpiry(2)) === (3.0 / 13) +- 0.001) + } + + test("long HTLC, single endorsement level") { + var r = Reputation.init(Config(enabled = true, 1000 day, 1 minute)) + assert(r.getConfidence(100000 msat, 1, BlockHeight(0), CltvExpiry(6), TimestampMilli(0)) == 0) + val add1 = makeAdd(CltvExpiry(6)) + r = r.addPendingHtlc(add1, 100000 msat, 1, TimestampMilli(0)) + r = r.settlePendingHtlc(HtlcId(add1), isSuccess = true, now = TimestampMilli(0)) + assert(r.getConfidence(1000 msat, 1, BlockHeight(0), CltvExpiry(1), TimestampMilli(0)) === (10.0 / 11) +- 0.001) + val add2 = makeAdd(CltvExpiry(1)) + r = r.addPendingHtlc(add2, 1000 msat, 1, TimestampMilli(0)) + r = r.settlePendingHtlc(HtlcId(add2), isSuccess = false, now = TimestampMilli(0) + 100.minutes) + assert(r.getConfidence(0 msat, 1, BlockHeight(0), CltvExpiry(1), now = TimestampMilli(0) + 100.minutes) === 0.5 +- 0.001) + } + + test("exponential decay, single endorsement level") { + var r = Reputation.init(Config(enabled = true, 100 seconds, 10 minutes)) + val add1 = makeAdd(CltvExpiry(2)) + r = r.addPendingHtlc(add1, 1000 msat, 2, TimestampMilli(0)) + r = r.settlePendingHtlc(HtlcId(add1), isSuccess = true, now = TimestampMilli(0)) + assert(r.getConfidence(1000 msat, 2, BlockHeight(0), CltvExpiry(1), TimestampMilli(0)) == 1.0 / 2) + val add2 = makeAdd(CltvExpiry(2)) + r = r.addPendingHtlc(add2, 1000 msat, 2, TimestampMilli(0)) + r = r.settlePendingHtlc(HtlcId(add2), isSuccess = true, now = TimestampMilli(0)) + assert(r.getConfidence(1000 msat, 2, BlockHeight(0), CltvExpiry(1), TimestampMilli(0)) == 2.0 / 3) + val add3 = makeAdd(CltvExpiry(2)) + r = r.addPendingHtlc(add3, 1000 msat, 2, TimestampMilli(0)) + r = r.settlePendingHtlc(HtlcId(add3), isSuccess = true, now = TimestampMilli(0)) + assert(r.getConfidence(1000 msat, 2, BlockHeight(0), CltvExpiry(1), TimestampMilli(0) + 100.seconds) == 1.5 / 2.5) + assert(r.getConfidence(1000 msat, 2, BlockHeight(0), CltvExpiry(1), TimestampMilli(0) + 1.hour) < 0.000001) + } + + test("multiple endorsement levels") { + var r = Reputation.init(Config(enabled = true, 1 day, 1 minute)) + assert(r.getConfidence(1 msat, 7, BlockHeight(0), CltvExpiry(1), TimestampMilli(0)) == 0) + val add1 = makeAdd(CltvExpiry(3)) + r = r.addPendingHtlc(add1, 100000 msat, 0, TimestampMilli(0)) + r = r.settlePendingHtlc(HtlcId(add1), isSuccess = true, TimestampMilli(0)) + val add2 = makeAdd(CltvExpiry(4)) + r = r.addPendingHtlc(add2, 900000 msat, 0, TimestampMilli(0)) + r = r.settlePendingHtlc(HtlcId(add2), isSuccess = false, TimestampMilli(0) + 1.minute) + val add3 = makeAdd(CltvExpiry(5)) + r = r.addPendingHtlc(add3, 50000 msat, 4, TimestampMilli(0) + 1.minute) + r = r.settlePendingHtlc(HtlcId(add3), isSuccess = true, TimestampMilli(0) + 1.minute) + val add4 = makeAdd(CltvExpiry(6)) + r = r.addPendingHtlc(add4, 50000 msat, 4, TimestampMilli(0) + 1.minute) + r = r.settlePendingHtlc(HtlcId(add4), isSuccess = false, TimestampMilli(0) + 2.minutes) + assert(r.getConfidence(1 msat, 0, BlockHeight(0), CltvExpiry(1), TimestampMilli(0) + 2.minutes) === 0.1 +- 0.01) + assert(r.getConfidence(1 msat, 4, BlockHeight(0), CltvExpiry(1), TimestampMilli(0) + 2.minutes) === 0.5 +- 0.01) + assert(r.getConfidence(1 msat, 7, BlockHeight(0), CltvExpiry(1), TimestampMilli(0) + 2.minutes) === 0.5 +- 0.01) + assert(r.getConfidence(1000 msat, 0, BlockHeight(0), CltvExpiry(1), TimestampMilli(0) + 2.minutes) === 0.1 +- 0.01) + assert(r.getConfidence(1000 msat, 4, BlockHeight(0), CltvExpiry(1), TimestampMilli(0) + 2.minutes) === 5.0 / 11 +- 0.01) + assert(r.getConfidence(1000 msat, 7, BlockHeight(0), CltvExpiry(1), TimestampMilli(0) + 2.minutes) === 5.0 / 11 +- 0.01) + assert(r.getConfidence(100000 msat, 0, BlockHeight(0), CltvExpiry(1), TimestampMilli(0) + 2.minutes) === 0.05 +- 0.01) + assert(r.getConfidence(100000 msat, 4, BlockHeight(0), CltvExpiry(1), TimestampMilli(0) + 2.minutes) === 1.0 / 14 +- 0.01) + assert(r.getConfidence(100000 msat, 7, BlockHeight(0), CltvExpiry(1), TimestampMilli(0) + 2.minutes) === 1.0 / 14 +- 0.01) + } +} \ No newline at end of file diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/BalanceEstimateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/BalanceEstimateSpec.scala index 15cc094900..2ded699d82 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/BalanceEstimateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/BalanceEstimateSpec.scala @@ -338,20 +338,21 @@ class BalanceEstimateSpec extends AnyFunSuite { )) val graphWithBalances = GraphWithBalanceEstimates(g, 1 day) + val now = TimestampSecond.now() // NB: it doesn't matter which edge is selected, the balance estimation takes all existing edges into account. val edge_ab = makeEdge(a, b, 1, 10 sat) val edge_ba = makeEdge(b, a, 1, 10 sat) val edge_bc = makeEdge(b, c, 6, 10 sat) - assert(graphWithBalances.canSend(27500 msat, edge_ab) === 0.75 +- 0.01) - assert(graphWithBalances.canSend(55000 msat, edge_ab) === 0.5 +- 0.01) - assert(graphWithBalances.canSend(30000 msat, edge_ba) === 0.75 +- 0.01) - assert(graphWithBalances.canSend(60000 msat, edge_ba) === 0.5 +- 0.01) - assert(graphWithBalances.canSend(75000 msat, edge_bc) === 0.5 +- 0.01) - assert(graphWithBalances.canSend(100000 msat, edge_bc) === 0.33 +- 0.01) + assert(graphWithBalances.balances.get(edge_ab).canSend(27500 msat, now) === 0.75 +- 0.01) + assert(graphWithBalances.balances.get(edge_ab).canSend(55000 msat, now) === 0.5 +- 0.01) + assert(graphWithBalances.balances.get(edge_ba).canSend(30000 msat, now) === 0.75 +- 0.01) + assert(graphWithBalances.balances.get(edge_ba).canSend(60000 msat, now) === 0.5 +- 0.01) + assert(graphWithBalances.balances.get(edge_bc).canSend(75000 msat, now) === 0.5 +- 0.01) + assert(graphWithBalances.balances.get(edge_bc).canSend(100000 msat, now) === 0.33 +- 0.01) val unknownEdge = makeEdge(42, 40 sat) - assert(graphWithBalances.canSend(10000 msat, unknownEdge) === 0.75 +- 0.01) - assert(graphWithBalances.canSend(20000 msat, unknownEdge) === 0.5 +- 0.01) - assert(graphWithBalances.canSend(30000 msat, unknownEdge) === 0.25 +- 0.01) + assert(graphWithBalances.balances.get(unknownEdge).canSend(10000 msat, now) === 0.75 +- 0.01) + assert(graphWithBalances.balances.get(unknownEdge).canSend(20000 msat, now) === 0.5 +- 0.01) + assert(graphWithBalances.balances.get(unknownEdge).canSend(30000 msat, now) === 0.25 +- 0.01) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/BaseRouterSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/BaseRouterSpec.scala index f216c580be..7ee6c32a61 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/BaseRouterSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/BaseRouterSpec.scala @@ -28,7 +28,7 @@ import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.{UtxoStatus, ValidateReque import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.crypto.TransportHandler -import fr.acinq.eclair.crypto.keymanager.{LocalChannelKeyManager, LocalNodeKeyManager} +import fr.acinq.eclair.crypto.keymanager.{ChannelKeys, LocalChannelKeyManager, LocalNodeKeyManager} import fr.acinq.eclair.io.Peer.PeerRoutingMessage import fr.acinq.eclair.payment.send.BlindedPathsResolver.{FullBlindedRoute, ResolvedPath} import fr.acinq.eclair.payment.send.BlindedRecipient @@ -60,8 +60,8 @@ abstract class BaseRouterSpec extends TestKitBaseClass with FixtureAnyFunSuiteLi val htlcMaximum = 500000000 msat val seed = ByteVector32(ByteVector.fill(32)(2)) - val testNodeKeyManager = new LocalNodeKeyManager(seed, Block.RegtestGenesisBlock.hash) - val testChannelKeyManager = new LocalChannelKeyManager(seed, Block.RegtestGenesisBlock.hash) + val testNodeKeyManager = LocalNodeKeyManager(seed, Block.RegtestGenesisBlock.hash) + val testChannelKeyManager = LocalChannelKeyManager(seed, Block.RegtestGenesisBlock.hash) val (priv_a, priv_b, priv_c, priv_d, priv_e, priv_f, priv_g, priv_h) = (testNodeKeyManager.nodeKey.privateKey, randomKey(), randomKey(), randomKey(), randomKey(), randomKey(), randomKey(), randomKey()) val (a, b, c, d, e, f, g, h) = (priv_a.publicKey, priv_b.publicKey, priv_c.publicKey, priv_d.publicKey, priv_e.publicKey, priv_f.publicKey, priv_g.publicKey, priv_h.publicKey) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRouterIntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRouterIntegrationSpec.scala index 0f4776454c..51dee1ef93 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRouterIntegrationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRouterIntegrationSpec.scala @@ -203,7 +203,7 @@ class ChannelRouterIntegrationSpec extends TestKitBaseClass with FixtureAnyFunSu internalTest(f) } - test("private local channel (zeroconf)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), Tag(ChannelStateTestsTags.ZeroConf)) { f => + test("private local channel (zeroconf)", Tag(ChannelStateTestsTags.ZeroConf)) { f => internalTest(f) } @@ -211,7 +211,7 @@ class ChannelRouterIntegrationSpec extends TestKitBaseClass with FixtureAnyFunSu internalTest(f) } - test("public local channel (zeroconf)", Tag(ChannelStateTestsTags.ChannelsPublic), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), Tag(ChannelStateTestsTags.ZeroConf)) { f => + test("public local channel (zeroconf)", Tag(ChannelStateTestsTags.ChannelsPublic), Tag(ChannelStateTestsTags.ZeroConf)) { f => internalTest(f) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/GraphSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/GraphSpec.scala index c84799c77f..5e15a56b39 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/GraphSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/GraphSpec.scala @@ -21,7 +21,7 @@ import fr.acinq.bitcoin.scalacompat.SatoshiLong import fr.acinq.eclair.payment.relay.Relayer.RelayFees import fr.acinq.eclair.router.Announcements.makeNodeAnnouncement import fr.acinq.eclair.router.Graph.GraphStructure.{DirectedGraph, GraphEdge} -import fr.acinq.eclair.router.Graph.{HeuristicsConstants, MessagePathWeight, MessageWeightRatios, PaymentWeightRatios, dijkstraMessagePath, routeBlindingPaths, yenKshortestPaths} +import fr.acinq.eclair.router.Graph.{HeuristicsConstants, MessagePathWeight, MessageWeightRatios, dijkstraMessagePath, routeBlindingPaths, yenKshortestPaths} import fr.acinq.eclair.router.RouteCalculationSpec._ import fr.acinq.eclair.router.Router.ChannelDesc import fr.acinq.eclair.wire.protocol.Color @@ -29,6 +29,8 @@ import fr.acinq.eclair.{BlockHeight, FeatureSupport, Features, MilliSatoshiLong, import org.scalactic.Tolerance.convertNumericToPlusOrMinusWrapper import org.scalatest.funsuite.AnyFunSuite +import scala.concurrent.duration.DurationInt + class GraphSpec extends AnyFunSuite { val (priv_a, priv_b, priv_c, priv_d, priv_e, priv_f, priv_g, priv_h) = (randomKey(), randomKey(), randomKey(), randomKey(), randomKey(), randomKey(), randomKey(), randomKey()) @@ -260,9 +262,9 @@ class GraphSpec extends AnyFunSuite { val edgeDE = makeEdge(6L, d, e, 9 msat, 0, capacity = 200000 sat) val graph = DirectedGraph(Seq(edgeAB, edgeBC, edgeCD, edgeDC, edgeCE, edgeDE)) - val path :: Nil = yenKshortestPaths(graph, a, e, 100000000 msat, + val path :: Nil = yenKshortestPaths(GraphWithBalanceEstimates(graph, 1 day), a, e, 100000000 msat, Set.empty, Set.empty, Set.empty, 1, - HeuristicsConstants(1.0E-8, RelayFees(2000 msat, 500), RelayFees(50 msat, 20), useLogProbability = true), + HeuristicsConstants(1.0E-8, RelayFees(2000 msat, 500), RelayFees(50 msat, 20), useLogProbability = true, usePastRelaysData = false), BlockHeight(714930), _ => true, includeLocalChannelCost = true) assert(path.path == Seq(edgeAB, edgeBC, edgeCE)) } @@ -284,9 +286,9 @@ class GraphSpec extends AnyFunSuite { val edgeDE = makeEdge(6L, d, e, 1 msat, 0, capacity = 200000 sat) val graph = DirectedGraph(Seq(edgeAB, edgeBC, edgeCD, edgeDC, edgeCE, edgeDE)) - val paths = yenKshortestPaths(graph, a, e, 90000000 msat, + val paths = yenKshortestPaths(GraphWithBalanceEstimates(graph, 1 day), a, e, 90000000 msat, Set.empty, Set.empty, Set.empty, 2, - PaymentWeightRatios(1, 0, 0, 0, RelayFees(0 msat, 0)), + HeuristicsConstants(0, RelayFees(0 msat, 0), RelayFees(0 msat, 0), useLogProbability = false, usePastRelaysData = false), BlockHeight(714930), _ => true, includeLocalChannelCost = true) assert(paths.length == 2) @@ -310,9 +312,9 @@ class GraphSpec extends AnyFunSuite { val edgeDE = makeEdge(6L, d, e, 1 msat, 0, capacity = 200000 sat) val graph = DirectedGraph(Seq(edgeAB, edgeBC, edgeCD, edgeDC, edgeCE, edgeDE)) - val paths = yenKshortestPaths(graph, a, e, 90000000 msat, + val paths = yenKshortestPaths(GraphWithBalanceEstimates(graph, 1 day), a, e, 90000000 msat, Set.empty, Set.empty, Set.empty, 2, - PaymentWeightRatios(1, 0, 0, 0, RelayFees(0 msat, 0)), + HeuristicsConstants(0, RelayFees(0 msat, 0), RelayFees(0 msat, 0), useLogProbability = false, usePastRelaysData = false), BlockHeight(714930), _ => true, includeLocalChannelCost = true) // Even though paths to find is 2, we only find 1 because that is all the valid paths that there are. @@ -343,9 +345,9 @@ class GraphSpec extends AnyFunSuite { val edgeGH = makeEdge(9L, g, h, 2 msat, 0, capacity = 100000 sat, minHtlc = 1000 msat) val graph = DirectedGraph(Seq(edgeCD, edgeDF, edgeCE, edgeED, edgeEF, edgeFG, edgeFH, edgeEG, edgeGH)) - val paths = yenKshortestPaths(graph, c, h, 10000000 msat, + val paths = yenKshortestPaths(GraphWithBalanceEstimates(graph, 1 day), c, h, 10000000 msat, Set.empty, Set.empty, Set.empty, 3, - PaymentWeightRatios(1, 0, 0, 0, RelayFees(0 msat, 0)), + HeuristicsConstants(0, RelayFees(0 msat, 0), RelayFees(0 msat, 0), useLogProbability = false, usePastRelaysData = false), BlockHeight(714930), _ => true, includeLocalChannelCost = true) assert(paths.length == 3) assert(paths(0).path == Seq(edgeCE, edgeEF, edgeFH)) @@ -384,9 +386,9 @@ class GraphSpec extends AnyFunSuite { val edgeCB = makeEdge(3L, c, b, 2 msat, 4, capacity = 100000 sat, minHtlc = 1000 msat) val graph = DirectedGraph(Seq(edgeAB, edgeAC, edgeCB)) - val paths = yenKshortestPaths(graph, a, b, 10000000 msat, + val paths = yenKshortestPaths(GraphWithBalanceEstimates(graph, 1 day), a, b, 10000000 msat, Set.empty, Set.empty, Set.empty, 1, - PaymentWeightRatios(1, 0, 0, 0, RelayFees(0 msat, 0)), + HeuristicsConstants(0, RelayFees(0 msat, 0), RelayFees(0 msat, 0), useLogProbability = false, usePastRelaysData = false), BlockHeight(714930), _ => true, includeLocalChannelCost = true) assert(paths.head.path == Seq(edgeAB)) } @@ -417,7 +419,7 @@ class GraphSpec extends AnyFunSuite { // All nodes can relay messages, same weight for each channel. val boundaries = (w: MessagePathWeight) => w.length <= 8 val wr = MessageWeightRatios(1.0, 0.0, 0.0) - val Some(path) = dijkstraMessagePath(graph, a, d, Set.empty, boundaries, BlockHeight(793397), wr) + val Some(path) = dijkstraMessagePath(GraphWithBalanceEstimates(graph, 1 day), a, d, Set.empty, boundaries, BlockHeight(793397), wr) assert(path.map(_.desc.shortChannelId.toLong) == Seq(4, 5)) } { @@ -426,7 +428,7 @@ class GraphSpec extends AnyFunSuite { val wr = MessageWeightRatios(1.0, 0.0, 0.0) val g = graph.addOrUpdateVertex(makeNodeAnnouncement(priv_a, "A", Color(0, 0, 0), Nil, Features.empty)) .addOrUpdateVertex(makeNodeAnnouncement(priv_d, "D", Color(0, 0, 0), Nil, Features.empty)) - val Some(path) = dijkstraMessagePath(g, a, d, Set.empty, boundaries, BlockHeight(793397), wr) + val Some(path) = dijkstraMessagePath(GraphWithBalanceEstimates(g, 1 day), a, d, Set.empty, boundaries, BlockHeight(793397), wr) assert(path.map(_.desc.shortChannelId.toLong) == Seq(4, 5)) } { @@ -434,28 +436,28 @@ class GraphSpec extends AnyFunSuite { val boundaries = (w: MessagePathWeight) => w.length <= 8 val wr = MessageWeightRatios(1.0, 0.0, 0.0) val g = graph.addOrUpdateVertex(makeNodeAnnouncement(priv_e, "E", Color(0, 0, 0), Nil, Features.empty)) - val Some(path) = dijkstraMessagePath(g, a, d, Set.empty, boundaries, BlockHeight(793397), wr) + val Some(path) = dijkstraMessagePath(GraphWithBalanceEstimates(g, 1 day), a, d, Set.empty, boundaries, BlockHeight(793397), wr) assert(path.map(_.desc.shortChannelId.toLong) == Seq(1, 2, 3)) } { // Prefer high-capacity channels. val boundaries = (w: MessagePathWeight) => w.length <= 8 val wr = MessageWeightRatios(0.0, 0.0, 1.0) - val Some(path) = dijkstraMessagePath(graph, a, d, Set.empty, boundaries, BlockHeight(793397), wr) + val Some(path) = dijkstraMessagePath(GraphWithBalanceEstimates(graph, 1 day), a, d, Set.empty, boundaries, BlockHeight(793397), wr) assert(path.map(_.desc.shortChannelId.toLong) == Seq(1, 2, 3)) } { // We ignore E. val boundaries = (w: MessagePathWeight) => w.length <= 8 val wr = MessageWeightRatios(1.0, 0.0, 0.0) - val Some(path) = dijkstraMessagePath(graph, a, d, Set(e), boundaries, BlockHeight(793397), wr) + val Some(path) = dijkstraMessagePath(GraphWithBalanceEstimates(graph, 1 day), a, d, Set(e), boundaries, BlockHeight(793397), wr) assert(path.map(_.desc.shortChannelId.toLong) == Seq(1, 2, 3)) } { // Target not in graph. val boundaries = (w: MessagePathWeight) => w.length <= 8 val wr = MessageWeightRatios(1.0, 0.0, 0.0) - assert(dijkstraMessagePath(graph, a, f, Set.empty, boundaries, BlockHeight(793397), wr).isEmpty) + assert(dijkstraMessagePath(GraphWithBalanceEstimates(graph, 1 day), a, f, Set.empty, boundaries, BlockHeight(793397), wr).isEmpty) } } @@ -522,13 +524,13 @@ class GraphSpec extends AnyFunSuite { .addOrUpdateVertex(makeNodeAnnouncement(priv_h, "H", Color(0, 0, 0), Nil, Features(Features.RouteBlinding -> FeatureSupport.Optional))) { - val paths = routeBlindingPaths(graph, a, h, 20_000_000 msat, Set.empty, Set.empty, pathsToFind = 3, PaymentWeightRatios(1, 0, 0, 0, RelayFees(0 msat, 0)), BlockHeight(793397), _ => true) + val paths = routeBlindingPaths(GraphWithBalanceEstimates(graph, 1 day), a, h, 20_000_000 msat, Set.empty, Set.empty, pathsToFind = 3, HeuristicsConstants(0, RelayFees(0 msat, 0), RelayFees(0 msat, 0), useLogProbability = false, usePastRelaysData = false), BlockHeight(793397), _ => true) assert(paths.length == 2) assert(paths(0).path.map(_.desc.a) == Seq(a, b)) assert(paths(1).path.map(_.desc.a) == Seq(a, e, f)) } { - val paths = routeBlindingPaths(graph, c, h, 20_000_000 msat, Set.empty, Set.empty, pathsToFind = 3, PaymentWeightRatios(1, 0, 0, 0, RelayFees(0 msat, 0)), BlockHeight(793397), _ => true) + val paths = routeBlindingPaths(GraphWithBalanceEstimates(graph, 1 day), c, h, 20_000_000 msat, Set.empty, Set.empty, pathsToFind = 3, HeuristicsConstants(0, RelayFees(0 msat, 0), RelayFees(0 msat, 0), useLogProbability = false, usePastRelaysData = false), BlockHeight(793397), _ => true) assert(paths.length == 1) assert(paths(0).path.map(_.desc.a) == Seq(c, a, b)) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/RouteCalculationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/RouteCalculationSpec.scala index d7ad81bf67..7e716dbf51 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/RouteCalculationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/RouteCalculationSpec.scala @@ -18,14 +18,15 @@ package fr.acinq.eclair.router import com.softwaremill.quicklens.ModifyPimp import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey -import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, ByteVector64, Satoshi, SatoshiLong, TxId} +import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, Satoshi, SatoshiLong, TxId} import fr.acinq.eclair.payment.relay.Relayer.RelayFees import fr.acinq.eclair.router.Announcements.makeNodeAnnouncement import fr.acinq.eclair.router.BaseRouterSpec.channelHopFromUpdate import fr.acinq.eclair.router.Graph.GraphStructure.DirectedGraph.graphEdgeToHop import fr.acinq.eclair.router.Graph.GraphStructure.{DirectedGraph, GraphEdge} -import fr.acinq.eclair.router.Graph.{HeuristicsConstants, PaymentPathWeight, PaymentWeightRatios} +import fr.acinq.eclair.router.Graph.{HeuristicsConstants, PaymentPathWeight} import fr.acinq.eclair.router.RouteCalculation._ +import fr.acinq.eclair.router.Router.MultiPartParams.{FullCapacity, MaxExpectedAmount, Randomize} import fr.acinq.eclair.router.Router._ import fr.acinq.eclair.transactions.Transactions import fr.acinq.eclair.wire.protocol._ @@ -35,8 +36,8 @@ import org.scalatest.funsuite.AnyFunSuite import org.scalatest.{ParallelTestExecution, Tag} import scodec.bits._ -import scala.collection.immutable.SortedMap import scala.collection.mutable +import scala.concurrent.duration.DurationInt import scala.util.{Failure, Random, Success} /** @@ -47,15 +48,17 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { import RouteCalculationSpec._ + implicit val log: akka.event.LoggingAdapter = akka.event.NoLogging + val (a, b, c, d, e, f) = (randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey) test("calculate simple route") { - val g = DirectedGraph(List( + val g = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(1L, a, b, 1 msat, 10, cltvDelta = CltvExpiryDelta(1), balance_opt = Some(DEFAULT_AMOUNT_MSAT * 2)), makeEdge(2L, b, c, 1 msat, 10, cltvDelta = CltvExpiryDelta(1)), makeEdge(3L, c, d, 1 msat, 10, cltvDelta = CltvExpiryDelta(1)), makeEdge(4L, d, e, 1 msat, 10, cltvDelta = CltvExpiryDelta(1)) - )) + )), 1 day) val Success(route :: Nil) = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(route2Ids(route) == 1 :: 2 :: 3 :: 4 :: Nil) @@ -71,12 +74,12 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { val routeParams = DEFAULT_ROUTE_PARAMS.modify(_.boundaries.maxFeeFlat).setTo(1 msat) val maxFee = routeParams.getMaxFee(DEFAULT_AMOUNT_MSAT) - val g = DirectedGraph(List( + val g = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(1L, a, b, 10 msat, 10, cltvDelta = CltvExpiryDelta(1)), makeEdge(2L, b, c, 10 msat, 10, cltvDelta = CltvExpiryDelta(1)), makeEdge(3L, c, d, 10 msat, 10, cltvDelta = CltvExpiryDelta(1)), makeEdge(4L, d, e, 10 msat, 10, cltvDelta = CltvExpiryDelta(1)) - )) + )), 1 day) val Success(route :: Nil) = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, maxFee, numRoutes = 1, routeParams = routeParams, currentBlockHeight = BlockHeight(400000)) assert(route2Ids(route) == 1 :: 2 :: 3 :: 4 :: Nil) @@ -112,17 +115,17 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { val amount = 10000 msat val expectedCost = 10007 msat - val graph = DirectedGraph(List( + val graph = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(1L, a, b, feeBase = 1 msat, feeProportionalMillionth = 200, minHtlc = 0 msat), makeEdge(4L, a, e, feeBase = 1 msat, feeProportionalMillionth = 200, minHtlc = 0 msat), makeEdge(2L, b, c, feeBase = 1 msat, feeProportionalMillionth = 300, minHtlc = 0 msat), makeEdge(3L, c, d, feeBase = 1 msat, feeProportionalMillionth = 400, minHtlc = 0 msat), makeEdge(5L, e, f, feeBase = 1 msat, feeProportionalMillionth = 400, minHtlc = 0 msat), makeEdge(6L, f, d, feeBase = 1 msat, feeProportionalMillionth = 100, minHtlc = 0 msat) - )) + )), 1 day) val Success(route :: Nil) = findRoute(graph, a, d, amount, maxFee = 7 msat, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) - val weightedPath = Graph.pathWeight(a, route2Edges(route), amount, BlockHeight(0), NO_WEIGHT_RATIOS, includeLocalChannelCost = false) + val weightedPath = Graph.pathWeight(graph.balances, a, route2Edges(route), amount, BlockHeight(0), NO_WEIGHT_RATIOS, includeLocalChannelCost = false) assert(route2Ids(route) == 4 :: 5 :: 6 :: Nil) assert(weightedPath.length == 3) assert(weightedPath.amount == expectedCost) @@ -138,25 +141,25 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { } test("calculate route considering the direct channel pays no fees") { - val g = DirectedGraph(List( + val g = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(1L, a, b, 5 msat, 0), // a -> b makeEdge(2L, a, d, 15 msat, 0), // a -> d this goes a bit closer to the target and asks for higher fees but is a direct channel makeEdge(3L, b, c, 5 msat, 0), // b -> c makeEdge(4L, c, d, 5 msat, 0), // c -> d makeEdge(5L, d, e, 5 msat, 0) // d -> e - )) + )), 1 day) val Success(route :: Nil) = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(route2Ids(route) == 2 :: 5 :: Nil) } test("calculate simple route (add and remove edges") { - val g = DirectedGraph(List( + val g = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(1L, a, b, 0 msat, 0), makeEdge(2L, b, c, 0 msat, 0), makeEdge(3L, c, d, 0 msat, 0), makeEdge(4L, d, e, 0 msat, 0) - )) + )), 1 day) val Success(route1 :: Nil) = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(route2Ids(route1) == 1 :: 2 :: 3 :: 4 :: Nil) @@ -174,12 +177,12 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { PublicKey(hex"029e059b6780f155f38e83601969919aae631ddf6faed58fe860c72225eb327d7c") // target ) - val graph = DirectedGraph(List( + val graph = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(1L, f, g, 1 msat, 0), makeEdge(2L, g, h, 1 msat, 0), makeEdge(3L, h, i, 1 msat, 0), makeEdge(4L, f, h, 50 msat, 0) // more expensive but fee will be ignored since f is the payer - )) + )), 1 day) val Success(route :: Nil) = findRoute(graph, f, i, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(route2Ids(route) == 4 :: 3 :: Nil) @@ -193,12 +196,12 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { PublicKey(hex"029e059b6780f155f38e83601969919aae631ddf6faed58fe860c72225eb327d7c") // target ) - val graph = DirectedGraph(List( + val graph = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(1L, f, g, 0 msat, 0), makeEdge(4L, f, i, 50 msat, 0), // our starting node F has a direct channel with I makeEdge(2L, g, h, 0 msat, 0), makeEdge(3L, h, i, 0 msat, 0) - )) + )), 1 day) val Success(route1 :: route2 :: Nil) = findRoute(graph, f, i, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 2, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(route2Ids(route1) == 4 :: Nil) @@ -213,12 +216,12 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { PublicKey(hex"029e059b6780f155f38e83601969919aae631ddf6faed58fe860c72225eb327d7c") // I target ) - val graph = DirectedGraph(List( + val graph = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(1L, f, g, 1 msat, 0, balance_opt = Some(DEFAULT_AMOUNT_MSAT + 50.msat)), // the maximum htlc allowed by this channel is only 50 msat greater than what we're sending makeEdge(2L, g, h, 1 msat, 0, maxHtlc = Some(DEFAULT_AMOUNT_MSAT + 50.msat)), makeEdge(3L, h, i, 1 msat, 0) - )) + )), 1 day) val Success(route :: Nil) = findRoute(graph, f, i, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(route2Ids(route) == 1 :: 2 :: 3 :: Nil) @@ -232,12 +235,12 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { PublicKey(hex"029e059b6780f155f38e83601969919aae631ddf6faed58fe860c72225eb327d7c") // I target ) - val graph = DirectedGraph(List( + val graph = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(1L, f, g, 1 msat, 0, balance_opt = Some(DEFAULT_AMOUNT_MSAT + 50.msat)), // this channel requires a minimum amount that is larger than what we are sending makeEdge(2L, g, h, 1 msat, 0, minHtlc = DEFAULT_AMOUNT_MSAT + 50.msat), makeEdge(3L, h, i, 1 msat, 0) - )) + )), 1 day) val route = findRoute(graph, f, i, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(route == Failure(RouteNotFound)) @@ -251,12 +254,12 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { PublicKey(hex"029e059b6780f155f38e83601969919aae631ddf6faed58fe860c72225eb327d7c") // I target ) - val graph = DirectedGraph(List( + val graph = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(1L, f, g, 0 msat, 0), makeEdge(2L, g, h, 5 msat, 5), // expensive g -> h channel makeEdge(6L, g, h, 0 msat, 0), // cheap g -> h channel makeEdge(3L, h, i, 0 msat, 0) - )) + )), 1 day) val Success(route :: Nil) = findRoute(graph, f, i, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(route2Ids(route) == 1 :: 6 :: 3 :: Nil) @@ -270,46 +273,46 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { PublicKey(hex"029e059b6780f155f38e83601969919aae631ddf6faed58fe860c72225eb327d7c") // I target ) - val graph = DirectedGraph(List( + val graph = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(1L, f, g, 0 msat, 0), makeEdge(2L, g, h, 5 msat, 5, balance_opt = Some(DEFAULT_AMOUNT_MSAT + 1.msat)), // expensive g -> h channel with enough balance makeEdge(6L, g, h, 0 msat, 0, balance_opt = Some(DEFAULT_AMOUNT_MSAT - 10.msat)), // cheap g -> h channel without enough balance makeEdge(3L, h, i, 0 msat, 0) - )) + )), 1 day) val Success(route :: Nil) = findRoute(graph, f, i, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(route2Ids(route) == 1 :: 2 :: 3 :: Nil) } test("calculate longer but cheaper route") { - val g = DirectedGraph(List( + val g = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(1L, a, b, 0 msat, 0), makeEdge(2L, b, c, 0 msat, 0), makeEdge(3L, c, d, 0 msat, 0), makeEdge(4L, d, e, 0 msat, 0), makeEdge(5L, b, e, 10 msat, 10) - )) + )), 1 day) val Success(route :: Nil) = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(route2Ids(route) == 1 :: 2 :: 3 :: 4 :: Nil) } test("no local channels") { - val g = DirectedGraph(List( + val g = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(2L, b, c, 0 msat, 0), makeEdge(4L, d, e, 0 msat, 0) - )) + )), 1 day) val route = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(route == Failure(RouteNotFound)) } test("route not found") { - val g = DirectedGraph(List( + val g = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(1L, a, b, 0 msat, 0), makeEdge(2L, b, c, 0 msat, 0), makeEdge(4L, d, e, 0 msat, 0) - )) + )), 1 day) val route = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(route == Failure(RouteNotFound)) @@ -323,10 +326,10 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { val e = priv_e.publicKey val annE = makeNodeAnnouncement(priv_e, "E", Color(0, 0, 0), Nil, Features.empty) - val g = DirectedGraph(List( + val g = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(2L, b, c, 0 msat, 0), makeEdge(4L, c, d, 0 msat, 0) - )).addOrUpdateVertex(annA).addOrUpdateVertex(annE) + )).addOrUpdateVertex(annA).addOrUpdateVertex(annE), 1 day) assert(findRoute(g, a, d, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) == Failure(RouteNotFound)) assert(findRoute(g, b, e, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) == Failure(RouteNotFound)) @@ -348,60 +351,60 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { makeEdge(3L, c, d, 0 msat, 0) ) - val g = DirectedGraph(edgesHi) - val g1 = DirectedGraph(edgesLo) + val g = GraphWithBalanceEstimates(DirectedGraph(edgesHi), 1 day) + val g1 = GraphWithBalanceEstimates(DirectedGraph(edgesLo), 1 day) assert(findRoute(g, a, d, highAmount, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) == Failure(RouteNotFound)) assert(findRoute(g1, a, d, lowAmount, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) == Failure(RouteNotFound)) } test("route not found (balance too low)") { - val g = DirectedGraph(List( + val g = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(1L, a, b, 1 msat, 2, minHtlc = 10000 msat), makeEdge(2L, b, c, 1 msat, 2, minHtlc = 10000 msat), makeEdge(3L, c, d, 1 msat, 2, minHtlc = 10000 msat) - )) + )), 1 day) assert(findRoute(g, a, d, 15000 msat, 100 msat, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)).isSuccess) // not enough balance on the last edge - val g1 = DirectedGraph(List( + val g1 = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(1L, a, b, 1 msat, 2, minHtlc = 10000 msat), makeEdge(2L, b, c, 1 msat, 2, minHtlc = 10000 msat), makeEdge(3L, c, d, 1 msat, 2, minHtlc = 10000 msat, balance_opt = Some(10000 msat)) - )) + )), 1 day) // not enough balance on intermediate edge (taking fee into account) - val g2 = DirectedGraph(List( + val g2 = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(1L, a, b, 1 msat, 2, minHtlc = 10000 msat), makeEdge(2L, b, c, 1 msat, 2, minHtlc = 10000 msat, balance_opt = Some(15000 msat)), makeEdge(3L, c, d, 1 msat, 2, minHtlc = 10000 msat) - )) + )), 1 day) // no enough balance on first edge (taking fee into account) - val g3 = DirectedGraph(List( + val g3 = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(1L, a, b, 1 msat, 2, minHtlc = 10000 msat, balance_opt = Some(15000 msat)), makeEdge(2L, b, c, 1 msat, 2, minHtlc = 10000 msat), makeEdge(3L, c, d, 1 msat, 2, minHtlc = 10000 msat) - )) + )), 1 day) Seq(g1, g2, g3).foreach(g => assert(findRoute(g, a, d, 15000 msat, 100 msat, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) == Failure(RouteNotFound))) } test("route to self") { - val g = DirectedGraph(List( + val g = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(1L, a, b, 0 msat, 0), makeEdge(2L, b, c, 0 msat, 0), makeEdge(3L, c, d, 0 msat, 0) - )) + )), 1 day) val route = findRoute(g, a, a, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(route == Failure(CannotRouteToSelf)) } test("route to immediate neighbor") { - val g = DirectedGraph(List( + val g = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(1L, a, b, 0 msat, 0, balance_opt = Some(DEFAULT_AMOUNT_MSAT)), makeEdge(2L, b, c, 0 msat, 0), makeEdge(3L, c, d, 0 msat, 0), makeEdge(4L, d, e, 0 msat, 0) - )) + )), 1 day) val Success(route :: Nil) = findRoute(g, a, b, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(route2Ids(route) == 1 :: Nil) @@ -409,12 +412,12 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { test("directed graph") { // a->e works, e->a fails - val g = DirectedGraph(List( + val g = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(1L, a, b, 0 msat, 0), makeEdge(2L, b, c, 0 msat, 0), makeEdge(3L, c, d, 0 msat, 0), makeEdge(4L, d, e, 0 msat, 0) - )) + )), 1 day) val Success(route1 :: Nil) = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(route2Ids(route1) == 1 :: 2 :: 3 :: 4 :: Nil) @@ -446,26 +449,26 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { GraphEdge(ChannelDesc(ShortChannelId(4L), e, d), HopRelayParams.FromAnnouncement(ued), DEFAULT_CAPACITY, None) ) - val g = DirectedGraph(edges) + val g = GraphWithBalanceEstimates(DirectedGraph(edges), 1 day) val Success(route :: Nil) = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(route.hops == channelHopFromUpdate(a, b, uab) :: channelHopFromUpdate(b, c, ubc) :: channelHopFromUpdate(c, d, ucd) :: channelHopFromUpdate(d, e, ude) :: Nil) } test("blacklist routes") { - val g = DirectedGraph(List( + val g = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(1L, a, b, 0 msat, 0), makeEdge(2L, b, c, 0 msat, 0), makeEdge(3L, c, d, 0 msat, 0), makeEdge(4L, d, e, 0 msat, 0) - )) + )), 1 day) val route1 = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, ignoredEdges = Set(ChannelDesc(ShortChannelId(3L), c, d)), routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(route1 == Failure(RouteNotFound)) // verify that we left the graph untouched - assert(g.containsEdge(ChannelDesc(ShortChannelId(3), c, d))) - assert(g.containsVertex(c)) - assert(g.containsVertex(d)) + assert(g.graph.containsEdge(ChannelDesc(ShortChannelId(3), c, d))) + assert(g.graph.containsVertex(c)) + assert(g.graph.containsVertex(d)) // make sure we can find a route if without the blacklist val Success(route2 :: Nil) = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) @@ -473,11 +476,11 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { } test("route to a destination that is not in the graph (with assisted routes)") { - val g = DirectedGraph(List( + val g = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(1L, a, b, 10 msat, 10), makeEdge(2L, b, c, 10 msat, 10), makeEdge(3L, c, d, 10 msat, 10) - )) + )), 1 day) val route = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(route == Failure(RouteNotFound)) @@ -489,10 +492,10 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { } test("route from a source that is not in the graph (with assisted routes)") { - val g = DirectedGraph(List( + val g = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(2L, b, c, 10 msat, 10), makeEdge(3L, c, d, 10 msat, 10) - )) + )), 1 day) val route = findRoute(g, a, d, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(route == Failure(RouteNotFound)) @@ -504,12 +507,12 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { } test("verify that extra hops takes precedence over known channels") { - val g = DirectedGraph(List( + val g = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(1L, a, b, 10 msat, 10), makeEdge(2L, b, c, 10 msat, 10), makeEdge(3L, c, d, 10 msat, 10), makeEdge(4L, d, e, 10 msat, 10) - )) + )), 1 day) val Success(route1 :: Nil) = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(route2Ids(route1) == 1 :: 2 :: 3 :: 4 :: Nil) @@ -572,7 +575,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { .zipWithIndex // ((0, 1), 0) :: ((1, 2), 1) :: ... .map { case ((na, nb), index) => makeEdge(index, na, nb, 5 msat, 0) } - val g = DirectedGraph(edges) + val g = GraphWithBalanceEstimates(DirectedGraph(edges), 1 day) assert(findRoute(g, nodes(0), nodes(18), DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)).map(r => route2Ids(r.head)) == Success(0 until 18)) assert(findRoute(g, nodes(0), nodes(19), DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)).map(r => route2Ids(r.head)) == Success(0 until 19)) @@ -590,7 +593,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { val expensiveShortEdge = makeEdge(99, nodes(2), nodes(48), 1000 msat, 0) // expensive shorter route - val g = DirectedGraph(expensiveShortEdge :: edges) + val g = GraphWithBalanceEstimates(DirectedGraph(expensiveShortEdge :: edges), 1 day) val Success(route :: Nil) = findRoute(g, nodes(0), nodes(49), DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(route2Ids(route) == 0 :: 1 :: 99 :: 48 :: Nil) @@ -598,14 +601,14 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { test("ignore cheaper route when it has more than the requested CLTV") { val f = randomKey().publicKey - val g = DirectedGraph(List( + val g = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(1, a, b, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, CltvExpiryDelta(50)), makeEdge(2, b, c, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, CltvExpiryDelta(50)), makeEdge(3, c, d, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, CltvExpiryDelta(50)), makeEdge(4, a, e, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, CltvExpiryDelta(9)), makeEdge(5, e, f, feeBase = 5 msat, 0, minHtlc = 0 msat, maxHtlc = None, CltvExpiryDelta(9)), makeEdge(6, f, d, feeBase = 5 msat, 0, minHtlc = 0 msat, maxHtlc = None, CltvExpiryDelta(9)) - )) + )), 1 day) val Success(route :: Nil) = findRoute(g, a, d, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.modify(_.boundaries.maxCltv).setTo(CltvExpiryDelta(28)), currentBlockHeight = BlockHeight(400000)) assert(route2Ids(route) == 4 :: 5 :: 6 :: Nil) @@ -613,27 +616,27 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { test("ignore cheaper route when it grows longer than the requested size") { val f = randomKey().publicKey - val g = DirectedGraph(List( + val g = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(1, a, b, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, CltvExpiryDelta(9)), makeEdge(2, b, c, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, CltvExpiryDelta(9)), makeEdge(3, c, d, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, CltvExpiryDelta(9)), makeEdge(4, d, e, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, CltvExpiryDelta(9)), makeEdge(5, e, f, feeBase = 5 msat, 0, minHtlc = 0 msat, maxHtlc = None, CltvExpiryDelta(9)), makeEdge(6, b, f, feeBase = 5 msat, 0, minHtlc = 0 msat, maxHtlc = None, CltvExpiryDelta(9)) - )) + )), 1 day) val Success(route :: Nil) = findRoute(g, a, f, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.modify(_.boundaries.maxRouteLength).setTo(3), currentBlockHeight = BlockHeight(400000)) assert(route2Ids(route) == 1 :: 6 :: Nil) } test("ignore loops") { - val g = DirectedGraph(List( + val g = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(1L, a, b, 10 msat, 10), makeEdge(2L, b, c, 10 msat, 10), makeEdge(3L, c, a, 10 msat, 10), makeEdge(4L, c, d, 10 msat, 10), makeEdge(5L, d, e, 10 msat, 10) - )) + )), 1 day) val Success(route :: Nil) = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(route2Ids(route) == 1 :: 2 :: 4 :: 5 :: Nil) @@ -641,7 +644,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { test("ensure the route calculation terminates correctly when selecting 0-fees edges") { // the graph contains a possible 0-cost path that goes back on its steps ( e -> f, f -> e ) - val g = DirectedGraph(List( + val g = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(1L, a, b, 10 msat, 10), // a -> b makeEdge(2L, b, c, 10 msat, 10), makeEdge(4L, c, d, 10 msat, 10), @@ -649,7 +652,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { makeEdge(6L, e, f, 0 msat, 0), // e -> f makeEdge(6L, f, e, 0 msat, 0), // e <- f makeEdge(5L, e, d, 0 msat, 0) // e -> d - )) + )), 1 day) val Success(route :: Nil) = findRoute(g, a, d, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(route2Ids(route) == 1 :: 3 :: 5 :: Nil) @@ -674,7 +677,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { PublicKey(hex"03fc5b91ce2d857f146fd9b986363374ffe04dc143d8bcd6d7664c8873c463cdfc") ) - val g1 = DirectedGraph(Seq( + val g1 = GraphWithBalanceEstimates(DirectedGraph(Seq( makeEdge(1L, d, a, 1 msat, 0, balance_opt = Some(DEFAULT_AMOUNT_MSAT + 4.msat)), makeEdge(2L, d, e, 1 msat, 0, balance_opt = Some(DEFAULT_AMOUNT_MSAT + 3.msat)), makeEdge(3L, a, e, 1 msat, 0, balance_opt = Some(DEFAULT_AMOUNT_MSAT + 3.msat)), @@ -682,7 +685,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { makeEdge(5L, e, f, 1 msat, 0, balance_opt = Some(DEFAULT_AMOUNT_MSAT)), makeEdge(6L, b, c, 1 msat, 0, balance_opt = Some(DEFAULT_AMOUNT_MSAT + 1.msat)), makeEdge(7L, c, f, 1 msat, 0, balance_opt = Some(DEFAULT_AMOUNT_MSAT)) - )) + )), 1 day) val fourShortestPaths = Graph.yenKshortestPaths(g1, d, f, DEFAULT_AMOUNT_MSAT, Set.empty, Set.empty, Set.empty, pathsToFind = 4, NO_WEIGHT_RATIOS, BlockHeight(0), noopBoundaries, includeLocalChannelCost = false) assert(fourShortestPaths.size == 4) @@ -710,7 +713,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { PublicKey(hex"03fc5b91ce2d857f146fd9b986363374ffe04dc143d8bcd6d7664c8873c463cdfc") ) - val graph = DirectedGraph(Seq( + val graph = GraphWithBalanceEstimates(DirectedGraph(Seq( makeEdge(10L, c, e, 2 msat, 0), makeEdge(20L, c, d, 3 msat, 0), makeEdge(30L, d, f, 4 msat, 5), // D- > F has a higher cost to distinguish it from the 2nd cheapest route @@ -720,7 +723,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { makeEdge(70L, f, g, 2 msat, 0), makeEdge(80L, f, h, 1 msat, 0), makeEdge(90L, g, h, 2 msat, 0) - )) + )), 1 day) val twoShortestPaths = Graph.yenKshortestPaths(graph, c, h, DEFAULT_AMOUNT_MSAT, Set.empty, Set.empty, Set.empty, pathsToFind = 2, NO_WEIGHT_RATIOS, BlockHeight(0), noopBoundaries, includeLocalChannelCost = false) @@ -736,7 +739,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { val f = randomKey().publicKey // simple graph with only 2 possible paths from A to F - val graph = DirectedGraph(Seq( + val graph = GraphWithBalanceEstimates(DirectedGraph(Seq( makeEdge(1L, a, b, 1 msat, 0), makeEdge(1L, b, a, 1 msat, 0), makeEdge(2L, b, c, 1 msat, 0), @@ -750,7 +753,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { makeEdge(5L, e, d, 1 msat, 0), makeEdge(6L, e, f, 1 msat, 0), makeEdge(6L, f, e, 1 msat, 0) - )) + )), 1 day) // we ask for 3 shortest paths but only 2 can be found val foundPaths = Graph.yenKshortestPaths(graph, a, f, DEFAULT_AMOUNT_MSAT, Set.empty, Set.empty, Set.empty, pathsToFind = 3, NO_WEIGHT_RATIOS, BlockHeight(0), noopBoundaries, includeLocalChannelCost = false) @@ -764,13 +767,14 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { .modify(_.boundaries.maxFeeFlat).setTo(7 msat) .modify(_.boundaries.maxFeeProportional).setTo(0) .modify(_.randomize).setTo(true) + .modify(_.mpp.splittingStrategy).setTo(Randomize) val strictFee = strictFeeParams.getMaxFee(DEFAULT_AMOUNT_MSAT) assert(strictFee == 7.msat) // A -> B -> C -> D has total cost of 10000005 // A -> E -> C -> D has total cost of 10000103 !! // A -> E -> F -> D has total cost of 10000006 - val g = DirectedGraph(List( + val g = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(1L, a, b, feeBase = 1 msat, 0), makeEdge(2L, b, c, feeBase = 2 msat, 0), makeEdge(3L, c, d, feeBase = 3 msat, 0), @@ -778,12 +782,12 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { makeEdge(5L, e, f, feeBase = 3 msat, 0), makeEdge(6L, f, d, feeBase = 3 msat, 0), makeEdge(7L, e, c, feeBase = 100 msat, 0) - )) + )), 1 day) for (_ <- 0 to 10) { val Success(routes) = findRoute(g, a, d, DEFAULT_AMOUNT_MSAT, strictFee, numRoutes = 3, routeParams = strictFeeParams, currentBlockHeight = BlockHeight(400000)) assert(routes.length == 2, routes) - val weightedPath = Graph.pathWeight(a, route2Edges(routes.head), DEFAULT_AMOUNT_MSAT, BlockHeight(400000), NO_WEIGHT_RATIOS, includeLocalChannelCost = false) + val weightedPath = Graph.pathWeight(g.balances, a, route2Edges(routes.head), DEFAULT_AMOUNT_MSAT, BlockHeight(400000), NO_WEIGHT_RATIOS, includeLocalChannelCost = false) val totalFees = weightedPath.amount - DEFAULT_AMOUNT_MSAT // over the three routes we could only get the 2 cheapest because the third is too expensive (over 7 msat of fees) assert(totalFees == 5.msat || totalFees == 6.msat) @@ -798,7 +802,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { // A -> B -> C -> D is 'fee optimized', lower fees route (totFees = 2, totCltv = 4000) // A -> E -> F -> D is 'timeout optimized', lower CLTV route (totFees = 3, totCltv = 18) // A -> E -> C -> D is 'capacity optimized', more recent channel/larger capacity route - val g = DirectedGraph(List( + val g = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(1L, a, b, feeBase = 0 msat, 1000, minHtlc = 0 msat, capacity = defaultCapacity, cltvDelta = CltvExpiryDelta(13)), makeEdge(4L, a, e, feeBase = 0 msat, 1000, minHtlc = 0 msat, capacity = defaultCapacity, cltvDelta = CltvExpiryDelta(12)), makeEdge(2L, b, c, feeBase = 1 msat, 1000, minHtlc = 0 msat, capacity = defaultCapacity, cltvDelta = CltvExpiryDelta(500)), @@ -806,69 +810,46 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { makeEdge(5L, e, f, feeBase = 2 msat, 1000, minHtlc = 0 msat, capacity = defaultCapacity, cltvDelta = CltvExpiryDelta(9)), makeEdge(6L, f, d, feeBase = 2 msat, 1000, minHtlc = 0 msat, capacity = defaultCapacity, cltvDelta = CltvExpiryDelta(9)), makeEdge(7L, e, c, feeBase = 2 msat, 1000, minHtlc = 0 msat, capacity = largeCapacity, cltvDelta = CltvExpiryDelta(12)) - )) + )), 1 day) val Success(routeFeeOptimized :: Nil) = findRoute(g, a, d, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(route2Nodes(routeFeeOptimized) == (a, b) :: (b, c) :: (c, d) :: Nil) - val Success(routeCltvOptimized :: Nil) = findRoute(g, a, d, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(heuristics = PaymentWeightRatios( - baseFactor = 0, - cltvDeltaFactor = 1, - ageFactor = 0, - capacityFactor = 0, + val Success(routeCltvOptimized :: Nil) = findRoute(g, a, d, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(heuristics = HeuristicsConstants( + lockedFundsRisk = 1, + failureFees = RelayFees(0 msat, 0), hopFees = RelayFees(0 msat, 0), + useLogProbability = false, + usePastRelaysData = false, )), currentBlockHeight = BlockHeight(400000)) assert(route2Nodes(routeCltvOptimized) == (a, e) :: (e, f) :: (f, d) :: Nil) - val Success(routeCapacityOptimized :: Nil) = findRoute(g, a, d, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(heuristics = PaymentWeightRatios( - baseFactor = 0, - cltvDeltaFactor = 0, - ageFactor = 0, - capacityFactor = 1, + val Success(routeCapacityOptimized :: Nil) = findRoute(g, a, d, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(heuristics = HeuristicsConstants( + lockedFundsRisk = 0, + failureFees = RelayFees(1000 msat, 1000), hopFees = RelayFees(0 msat, 0), + useLogProbability = false, + usePastRelaysData = false, )), currentBlockHeight = BlockHeight(400000)) assert(route2Nodes(routeCapacityOptimized) == (a, e) :: (e, c) :: (c, d) :: Nil) } - test("prefer going through an older channel if fees and CLTV are the same") { - val currentBlockHeight = BlockHeight(554000) - - val g = DirectedGraph(List( - makeEdge(ShortChannelId.fromCoordinates(s"${currentBlockHeight.toLong}x0x1").success.value.toLong, a, b, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, cltvDelta = CltvExpiryDelta(144)), - makeEdge(ShortChannelId.fromCoordinates(s"${currentBlockHeight.toLong}x0x4").success.value.toLong, a, e, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, cltvDelta = CltvExpiryDelta(144)), - makeEdge(ShortChannelId.fromCoordinates(s"${currentBlockHeight.toLong - 3000}x0x2").success.value.toLong, b, c, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, cltvDelta = CltvExpiryDelta(144)), // younger channel - makeEdge(ShortChannelId.fromCoordinates(s"${currentBlockHeight.toLong - 3000}x0x3").success.value.toLong, c, d, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, cltvDelta = CltvExpiryDelta(144)), - makeEdge(ShortChannelId.fromCoordinates(s"${currentBlockHeight.toLong}x0x5").success.value.toLong, e, f, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, cltvDelta = CltvExpiryDelta(144)), - makeEdge(ShortChannelId.fromCoordinates(s"${currentBlockHeight.toLong}x0x6").success.value.toLong, f, d, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, cltvDelta = CltvExpiryDelta(144)) - )) - - val Success(routeScoreOptimized :: Nil) = findRoute(g, a, d, DEFAULT_AMOUNT_MSAT / 2, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(heuristics = PaymentWeightRatios( - baseFactor = 0.01, - ageFactor = 0.33, - cltvDeltaFactor = 0.33, - capacityFactor = 0.33, - hopFees = RelayFees(0 msat, 0), - )), currentBlockHeight = currentBlockHeight) - - assert(route2Nodes(routeScoreOptimized) == (a, b) :: (b, c) :: (c, d) :: Nil) - } - test("prefer a route with a smaller total CLTV if fees and score are the same") { - val g = DirectedGraph(List( + val g = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(1, a, b, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, cltvDelta = CltvExpiryDelta(12)), makeEdge(4, a, e, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, cltvDelta = CltvExpiryDelta(12)), makeEdge(2, b, c, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, cltvDelta = CltvExpiryDelta(10)), // smaller CLTV makeEdge(3, c, d, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, cltvDelta = CltvExpiryDelta(12)), makeEdge(5, e, f, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, cltvDelta = CltvExpiryDelta(12)), makeEdge(6, f, d, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, cltvDelta = CltvExpiryDelta(12)) - )) + )), 1 day) - val Success(routeScoreOptimized :: Nil) = findRoute(g, a, d, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(heuristics = PaymentWeightRatios( - baseFactor = 0.01, - ageFactor = 0.33, - cltvDeltaFactor = 0.33, - capacityFactor = 0.33, + val Success(routeScoreOptimized :: Nil) = findRoute(g, a, d, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(heuristics = HeuristicsConstants( + lockedFundsRisk = 1e-7, + failureFees = RelayFees(100 msat, 100), hopFees = RelayFees(0 msat, 0), + useLogProbability = false, + usePastRelaysData = false, )), currentBlockHeight = BlockHeight(400000)) assert(route2Nodes(routeScoreOptimized) == (a, b) :: (b, c) :: (c, d) :: Nil) @@ -877,69 +858,26 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { test("avoid a route that breaks off the max CLTV") { // A -> B -> C -> D is cheaper but has a total CLTV > 2016! // A -> E -> F -> D is more expensive but has a total CLTV < 2016 - val g = DirectedGraph(List( + val g = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(1, a, b, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, cltvDelta = CltvExpiryDelta(144)), - makeEdge(4, a, e, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, cltvDelta = CltvExpiryDelta(144)), + makeEdge(4, a, e, feeBase = 100 msat, 0, minHtlc = 0 msat, maxHtlc = None, cltvDelta = CltvExpiryDelta(144)), makeEdge(2, b, c, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, cltvDelta = CltvExpiryDelta(1000)), makeEdge(3, c, d, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, cltvDelta = CltvExpiryDelta(900)), - makeEdge(5, e, f, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, cltvDelta = CltvExpiryDelta(144)), - makeEdge(6, f, d, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, cltvDelta = CltvExpiryDelta(144)) - )) - - val Success(routeScoreOptimized :: Nil) = findRoute(g, a, d, DEFAULT_AMOUNT_MSAT / 2, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(heuristics = PaymentWeightRatios( - baseFactor = 0.01, - ageFactor = 0.33, - cltvDeltaFactor = 0.33, - capacityFactor = 0.33, - hopFees = RelayFees(0 msat, 0), + makeEdge(5, e, f, feeBase = 100 msat, 0, minHtlc = 0 msat, maxHtlc = None, cltvDelta = CltvExpiryDelta(144)), + makeEdge(6, f, d, feeBase = 100 msat, 0, minHtlc = 0 msat, maxHtlc = None, cltvDelta = CltvExpiryDelta(144)) + )), 1 day) + + val Success(routeScoreOptimized :: Nil) = findRoute(g, a, d, DEFAULT_AMOUNT_MSAT / 2, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(heuristics = HeuristicsConstants( + lockedFundsRisk = 0, + failureFees = RelayFees(100 msat, 100), + hopFees = RelayFees(500 msat, 200), + useLogProbability = false, + usePastRelaysData = false, )), currentBlockHeight = BlockHeight(400000)) assert(route2Nodes(routeScoreOptimized) == (a, e) :: (e, f) :: (f, d) :: Nil) } - test("cost function is monotonic") { - // This test have a channel (542280x2156x0) that according to heuristics is very convenient but actually useless to reach the target, - // then if the cost function is not monotonic the path-finding breaks because the result path contains a loop. - val updates = SortedMap( - RealShortChannelId(BlockHeight(565643), 1216, 0) -> PublicChannel( - ann = makeChannel(ShortChannelId.fromCoordinates("565643x1216x0").success.value.toLong, PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f"), PublicKey(hex"024655b768ef40951b20053a5c4b951606d4d86085d51238f2c67c7dec29c792ca")), - fundingTxId = TxId(ByteVector32.Zeroes), - capacity = DEFAULT_CAPACITY, - update_1_opt = Some(ChannelUpdate(ByteVector64.Zeroes, Block.RegtestGenesisBlock.hash, ShortChannelId.fromCoordinates("565643x1216x0").success.value, 0 unixsec, ChannelUpdate.MessageFlags(dontForward = false), ChannelUpdate.ChannelFlags.DUMMY, CltvExpiryDelta(14), htlcMinimumMsat = 1 msat, feeBaseMsat = 1000 msat, 10, 4_294_967_295L msat)), - update_2_opt = Some(ChannelUpdate(ByteVector64.Zeroes, Block.RegtestGenesisBlock.hash, ShortChannelId.fromCoordinates("565643x1216x0").success.value, 0 unixsec, ChannelUpdate.MessageFlags(dontForward = false), ChannelUpdate.ChannelFlags(isEnabled = true, isNode1 = false), CltvExpiryDelta(144), htlcMinimumMsat = 0 msat, feeBaseMsat = 1000 msat, 100, 15_000_000_000L msat)), - meta_opt = None - ), - RealShortChannelId(BlockHeight(542280), 2156, 0) -> PublicChannel( - ann = makeChannel(ShortChannelId.fromCoordinates("542280x2156x0").success.value.toLong, PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f"), PublicKey(hex"03cb7983dc247f9f81a0fa2dfa3ce1c255365f7279c8dd143e086ca333df10e278")), - fundingTxId = TxId(ByteVector32.Zeroes), - capacity = DEFAULT_CAPACITY, - update_1_opt = Some(ChannelUpdate(ByteVector64.Zeroes, Block.RegtestGenesisBlock.hash, ShortChannelId.fromCoordinates("542280x2156x0").success.value, 0 unixsec, ChannelUpdate.MessageFlags(dontForward = false), ChannelUpdate.ChannelFlags.DUMMY, CltvExpiryDelta(144), htlcMinimumMsat = 1000 msat, feeBaseMsat = 1000 msat, 100, 16_777_000_000L msat)), - update_2_opt = Some(ChannelUpdate(ByteVector64.Zeroes, Block.RegtestGenesisBlock.hash, ShortChannelId.fromCoordinates("542280x2156x0").success.value, 0 unixsec, ChannelUpdate.MessageFlags(dontForward = false), ChannelUpdate.ChannelFlags(isEnabled = true, isNode1 = false), CltvExpiryDelta(144), htlcMinimumMsat = 1 msat, feeBaseMsat = 667 msat, 1, 16_777_000_000L msat)), - meta_opt = None - ), - RealShortChannelId(BlockHeight(565779), 2711, 0) -> PublicChannel( - ann = makeChannel(ShortChannelId.fromCoordinates("565779x2711x0").success.value.toLong, PublicKey(hex"036d65409c41ab7380a43448f257809e7496b52bf92057c09c4f300cbd61c50d96"), PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f")), - fundingTxId = TxId(ByteVector32.Zeroes), - capacity = DEFAULT_CAPACITY, - update_1_opt = Some(ChannelUpdate(ByteVector64.Zeroes, Block.RegtestGenesisBlock.hash, ShortChannelId.fromCoordinates("565779x2711x0").success.value, 0 unixsec, ChannelUpdate.MessageFlags(dontForward = false), ChannelUpdate.ChannelFlags.DUMMY, CltvExpiryDelta(144), htlcMinimumMsat = 1 msat, feeBaseMsat = 1000 msat, 100, 230_000_000L msat)), - update_2_opt = Some(ChannelUpdate(ByteVector64.Zeroes, Block.RegtestGenesisBlock.hash, ShortChannelId.fromCoordinates("565779x2711x0").success.value, 0 unixsec, ChannelUpdate.MessageFlags(dontForward = false), ChannelUpdate.ChannelFlags(isEnabled = false, isNode1 = false), CltvExpiryDelta(144), htlcMinimumMsat = 1 msat, feeBaseMsat = 1000 msat, 100, 230_000_000L msat)), - meta_opt = None - ) - ) - - val g = DirectedGraph.makeGraph(updates, Seq.empty) - val params = DEFAULT_ROUTE_PARAMS - .modify(_.boundaries.maxCltv).setTo(CltvExpiryDelta(1008)) - .modify(_.heuristics).setTo(PaymentWeightRatios(baseFactor = 0, cltvDeltaFactor = 0.15, ageFactor = 0.35, capacityFactor = 0.5, hopFees = RelayFees(0 msat, 0))) - val thisNode = PublicKey(hex"036d65409c41ab7380a43448f257809e7496b52bf92057c09c4f300cbd61c50d96") - val targetNode = PublicKey(hex"024655b768ef40951b20053a5c4b951606d4d86085d51238f2c67c7dec29c792ca") - val amount = 351000 msat - - val Success(route :: Nil) = findRoute(g, thisNode, targetNode, amount, DEFAULT_MAX_FEE, 1, Set.empty, Set.empty, Set.empty, params, currentBlockHeight = BlockHeight(567634)) // simulate mainnet block for heuristic - assert(route.hops.length == 2) - assert(route.hops.last.nextNodeId == targetNode) - } - test("validate path fees") { val ab = makeEdge(1L, a, b, feeBase = 100 msat, 10000, minHtlc = 150 msat, maxHtlc = Some(300 msat), capacity = 1 sat, balance_opt = Some(260 msat)) val bc = makeEdge(10L, b, c, feeBase = 5 msat, 10000, minHtlc = 100 msat, maxHtlc = Some(400 msat), capacity = 1 sat) @@ -962,14 +900,14 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { test("calculate multipart route to neighbor (many channels, known balance)") { val amount = 60000 msat - val g = DirectedGraph(List( + val g = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(1L, a, b, 50 msat, 100, minHtlc = 1 msat, balance_opt = Some(15000 msat)), makeEdge(2L, a, b, 15 msat, 10, minHtlc = 1 msat, balance_opt = Some(21000 msat)), makeEdge(3L, a, b, 1 msat, 50, minHtlc = 1 msat, balance_opt = Some(17000 msat)), makeEdge(4L, a, b, 100 msat, 20, minHtlc = 1 msat, balance_opt = Some(16000 msat)), - )) + )), 1 day) // We set max-parts to 3, but it should be ignored when sending to a direct neighbor. - val routeParams = DEFAULT_ROUTE_PARAMS.copy(mpp = MultiPartParams(2500 msat, 3)) + val routeParams = DEFAULT_ROUTE_PARAMS.copy(mpp = MultiPartParams(2500 msat, 3, DEFAULT_ROUTE_PARAMS.mpp.splittingStrategy)) { val Success(routes) = findMultiPartRoute(g, a, b, amount, 1 msat, routeParams = routeParams, currentBlockHeight = BlockHeight(400000)) @@ -978,25 +916,31 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { checkRouteAmounts(routes, amount, 0 msat) } { - val Success(routes) = findMultiPartRoute(g, a, b, amount, 1 msat, routeParams = routeParams.copy(randomize = true), currentBlockHeight = BlockHeight(400000)) + val Success(routes) = findMultiPartRoute(g, a, b, amount, 1 msat, routeParams = routeParams.copy(randomize = true).modify(_.mpp.splittingStrategy).setTo(Randomize), currentBlockHeight = BlockHeight(400000)) + assert(routes.length >= 4, routes) + assert(routes.forall(_.hops.length == 1), routes) + checkRouteAmounts(routes, amount, 0 msat) + } + { + val Success(routes) = findMultiPartRoute(g, a, b, amount, 1 msat, routeParams = routeParams.modify(_.mpp.splittingStrategy).setTo(MaxExpectedAmount), currentBlockHeight = BlockHeight(400000)) assert(routes.length >= 4, routes) assert(routes.forall(_.hops.length == 1), routes) checkRouteAmounts(routes, amount, 0 msat) } { // We set min-part-amount to a value that excludes channels 1 and 4. - val failure = findMultiPartRoute(g, a, b, amount, 1 msat, routeParams = routeParams.copy(mpp = MultiPartParams(16500 msat, 3)), currentBlockHeight = BlockHeight(400000)) + val failure = findMultiPartRoute(g, a, b, amount, 1 msat, routeParams = routeParams.copy(mpp = MultiPartParams(16500 msat, 3, routeParams.mpp.splittingStrategy)), currentBlockHeight = BlockHeight(400000)) assert(failure == Failure(RouteNotFound)) } } test("calculate multipart route to neighbor (single channel, known balance)") { - val g = DirectedGraph(List( + val g = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(1L, a, b, 50 msat, 100, minHtlc = 1 msat, balance_opt = Some(25000 msat)), makeEdge(2L, a, c, 1 msat, 0, minHtlc = 1 msat, balance_opt = Some(50000 msat)), makeEdge(3L, c, b, 1 msat, 0, minHtlc = 1 msat), makeEdge(4L, a, d, 1 msat, 0, minHtlc = 1 msat, balance_opt = Some(45000 msat)), - )) + )), 1 day) val amount = 25000 msat val Success(routes) = findMultiPartRoute(g, a, b, amount, 1 msat, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) @@ -1006,13 +950,13 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { } test("calculate multipart route to neighbor (many channels, some balance unknown)") { - val g = DirectedGraph(List( + val g = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(1L, a, b, 50 msat, 100, minHtlc = 1 msat, balance_opt = Some(15000 msat)), makeEdge(2L, a, b, 15 msat, 10, minHtlc = 1 msat, balance_opt = Some(25000 msat)), makeEdge(3L, a, b, 1 msat, 50, minHtlc = 1 msat, balance_opt = None, capacity = 20 sat), makeEdge(4L, a, b, 100 msat, 20, minHtlc = 1 msat, balance_opt = Some(10000 msat)), makeEdge(5L, a, d, 1 msat, 0, minHtlc = 1 msat, balance_opt = Some(45000 msat)), - )) + )), 1 day) val amount = 65000 msat val Success(routes) = findMultiPartRoute(g, a, b, amount, 1 msat, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) @@ -1023,7 +967,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { test("calculate multipart route to neighbor (many channels, some empty)") { val amount = 35000 msat - val g = DirectedGraph(List( + val g = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(1L, a, b, 50 msat, 100, minHtlc = 1 msat, balance_opt = Some(15000 msat)), makeEdge(2L, a, b, 15 msat, 10, minHtlc = 1 msat, balance_opt = Some(0 msat)), makeEdge(3L, a, b, 1 msat, 50, minHtlc = 1 msat, balance_opt = None, capacity = 15 sat), @@ -1031,7 +975,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { makeEdge(5L, a, b, 100 msat, 20, minHtlc = 1 msat, balance_opt = Some(10000 msat)), makeEdge(6L, a, d, 1 msat, 0, minHtlc = 1 msat, balance_opt = Some(45000 msat)), makeEdge(7L, a, d, 0 msat, 0, minHtlc = 0 msat, balance_opt = Some(0 msat)), - )) + )), 1 day) { val Success(routes) = findMultiPartRoute(g, a, b, amount, 1 msat, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) @@ -1041,7 +985,14 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { checkRouteAmounts(routes, amount, 0 msat) } { - val Success(routes) = findMultiPartRoute(g, a, b, amount, 1 msat, routeParams = DEFAULT_ROUTE_PARAMS.copy(randomize = true), currentBlockHeight = BlockHeight(400000)) + val Success(routes) = findMultiPartRoute(g, a, b, amount, 1 msat, routeParams = DEFAULT_ROUTE_PARAMS.copy(randomize = true).modify(_.mpp.splittingStrategy).setTo(Randomize), currentBlockHeight = BlockHeight(400000)) + assert(routes.length >= 3, routes) + assert(routes.forall(_.hops.length == 1), routes) + checkIgnoredChannels(routes, 2L) + checkRouteAmounts(routes, amount, 0 msat) + } + { + val Success(routes) = findMultiPartRoute(g, a, b, amount, 1 msat, routeParams = DEFAULT_ROUTE_PARAMS.modify(_.mpp.splittingStrategy).setTo(MaxExpectedAmount), currentBlockHeight = BlockHeight(400000)) assert(routes.length >= 3, routes) assert(routes.forall(_.hops.length == 1), routes) checkIgnoredChannels(routes, 2L) @@ -1050,14 +1001,14 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { } test("calculate multipart route to neighbor (ignored channels)") { - val g = DirectedGraph(List( + val g = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(1L, a, b, 50 msat, 100, minHtlc = 1 msat, balance_opt = Some(15000 msat)), makeEdge(2L, a, b, 15 msat, 10, minHtlc = 1 msat, balance_opt = Some(25000 msat)), makeEdge(3L, a, b, 1 msat, 50, minHtlc = 1 msat, balance_opt = None, capacity = 50 sat), makeEdge(4L, a, b, 100 msat, 20, minHtlc = 1 msat, balance_opt = Some(10000 msat)), makeEdge(5L, a, b, 1 msat, 10, minHtlc = 1 msat, balance_opt = None, capacity = 10 sat), makeEdge(6L, a, d, 1 msat, 0, minHtlc = 1 msat, balance_opt = Some(45000 msat)), - )) + )), 1 day) val amount = 20000 msat val ignoredEdges = Set(ChannelDesc(ShortChannelId(2L), a, b), ChannelDesc(ShortChannelId(3L), a, b)) @@ -1071,12 +1022,12 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { val edge_ab_1 = makeEdge(1L, a, b, 50 msat, 100, minHtlc = 1 msat, balance_opt = Some(15000 msat)) val edge_ab_2 = makeEdge(2L, a, b, 15 msat, 10, minHtlc = 1 msat, balance_opt = Some(25000 msat)) val edge_ab_3 = makeEdge(3L, a, b, 1 msat, 50, minHtlc = 1 msat, balance_opt = None, capacity = 15 sat) - val g = DirectedGraph(List( + val g = GraphWithBalanceEstimates(DirectedGraph(List( edge_ab_1, edge_ab_2, edge_ab_3, makeEdge(4L, a, d, 1 msat, 0, minHtlc = 1 msat, balance_opt = Some(45000 msat)), - )) + )), 1 day) val amount = 50000 msat // These pending HTLCs will have already been taken into account in the edge's `balance_opt` field: findMultiPartRoute @@ -1088,12 +1039,12 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { } test("calculate multipart route to neighbor (restricted htlc_maximum_msat)") { - val g = DirectedGraph(List( + val g = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(1L, a, b, 25 msat, 15, minHtlc = 1 msat, maxHtlc = Some(5000 msat), balance_opt = Some(18000 msat)), makeEdge(2L, a, b, 15 msat, 10, minHtlc = 1 msat, maxHtlc = Some(5000 msat), balance_opt = Some(23000 msat)), makeEdge(3L, a, b, 1 msat, 50, minHtlc = 1 msat, maxHtlc = Some(5000 msat), balance_opt = Some(21000 msat)), makeEdge(4L, a, d, 1 msat, 0, minHtlc = 1 msat, balance_opt = Some(45000 msat)), - )) + )), 1 day) val amount = 50000 msat val Success(routes) = findMultiPartRoute(g, a, b, amount, 1 msat, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) @@ -1104,15 +1055,15 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { } test("calculate multipart route to neighbor (restricted htlc_minimum_msat)") { - val g = DirectedGraph(List( + val g = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(1L, a, b, 25 msat, 15, minHtlc = 2500 msat, balance_opt = Some(18000 msat)), makeEdge(2L, a, b, 15 msat, 10, minHtlc = 2500 msat, balance_opt = Some(7000 msat)), makeEdge(3L, a, b, 1 msat, 50, minHtlc = 2500 msat, balance_opt = Some(10000 msat)), makeEdge(4L, a, d, 1 msat, 0, minHtlc = 1 msat, balance_opt = Some(45000 msat)), - )) + )), 1 day) val amount = 30000 msat - val routeParams = DEFAULT_ROUTE_PARAMS.copy(mpp = MultiPartParams(2500 msat, 5)) + val routeParams = DEFAULT_ROUTE_PARAMS.copy(mpp = MultiPartParams(2500 msat, 5, DEFAULT_ROUTE_PARAMS.mpp.splittingStrategy)) val Success(routes) = findMultiPartRoute(g, a, b, amount, 1 msat, routeParams = routeParams, currentBlockHeight = BlockHeight(400000)) assert(routes.forall(_.hops.length == 1), routes) assert(routes.length == 3, routes) @@ -1120,13 +1071,13 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { } test("calculate multipart route to neighbor (through remote channels)") { - val g = DirectedGraph(List( + val g = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(1L, a, b, 25 msat, 15, minHtlc = 1000 msat, balance_opt = Some(18000 msat)), makeEdge(2L, a, b, 15 msat, 10, minHtlc = 1000 msat, balance_opt = Some(7000 msat)), makeEdge(3L, a, c, 1000 msat, 10000, minHtlc = 1000 msat, balance_opt = Some(10000 msat)), makeEdge(4L, c, b, 10 msat, 1000, minHtlc = 1000 msat), makeEdge(5L, a, d, 1 msat, 0, minHtlc = 1 msat, balance_opt = Some(25000 msat)), - )) + )), 1 day) val amount = 30000 msat val maxFeeTooLow = findMultiPartRoute(g, a, b, amount, 1 msat, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) @@ -1139,48 +1090,56 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { } test("cannot find multipart route to neighbor (not enough balance)") { - val g = DirectedGraph(List( + val g = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(1L, a, b, 0 msat, 0, minHtlc = 1 msat, balance_opt = Some(15000 msat)), makeEdge(2L, a, b, 0 msat, 0, minHtlc = 1 msat, balance_opt = Some(5000 msat)), makeEdge(3L, a, b, 0 msat, 0, minHtlc = 1 msat, balance_opt = Some(10000 msat)), makeEdge(4L, a, d, 0 msat, 0, minHtlc = 1 msat, balance_opt = Some(45000 msat)), - )) + )), 1 day) { val result = findMultiPartRoute(g, a, b, 40000 msat, 1 msat, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(result == Failure(RouteNotFound)) } { - val result = findMultiPartRoute(g, a, b, 40000 msat, 1 msat, routeParams = DEFAULT_ROUTE_PARAMS.copy(randomize = true), currentBlockHeight = BlockHeight(400000)) + val result = findMultiPartRoute(g, a, b, 40000 msat, 1 msat, routeParams = DEFAULT_ROUTE_PARAMS.copy(randomize = true).modify(_.mpp.splittingStrategy).setTo(Randomize), currentBlockHeight = BlockHeight(400000)) + assert(result == Failure(RouteNotFound)) + } + { + val result = findMultiPartRoute(g, a, b, 40000 msat, 1 msat, routeParams = DEFAULT_ROUTE_PARAMS.modify(_.mpp.splittingStrategy).setTo(MaxExpectedAmount), currentBlockHeight = BlockHeight(400000)) assert(result == Failure(RouteNotFound)) } } test("cannot find multipart route to neighbor (not enough capacity)") { - val g = DirectedGraph(List( + val g = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(1L, a, b, 0 msat, 0, minHtlc = 1 msat, capacity = 1500 sat), makeEdge(2L, a, b, 0 msat, 0, minHtlc = 1 msat, capacity = 2000 sat), makeEdge(3L, a, b, 0 msat, 0, minHtlc = 1 msat, capacity = 1200 sat), makeEdge(4L, a, d, 0 msat, 0, minHtlc = 1 msat, capacity = 4500 sat), - )) + )), 1 day) val result = findMultiPartRoute(g, a, b, 5000000 msat, 1 msat, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(result == Failure(RouteNotFound)) } test("cannot find multipart route to neighbor (restricted htlc_minimum_msat)") { - val g = DirectedGraph(List( + val g = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(1L, a, b, 25 msat, 15, minHtlc = 5000 msat, balance_opt = Some(6000 msat)), makeEdge(2L, a, b, 15 msat, 10, minHtlc = 5000 msat, balance_opt = Some(7000 msat)), makeEdge(3L, a, d, 0 msat, 0, minHtlc = 5000 msat, balance_opt = Some(9000 msat)), - )) + )), 1 day) { val result = findMultiPartRoute(g, a, b, 10000 msat, 1 msat, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(result == Failure(RouteNotFound)) } { - val result = findMultiPartRoute(g, a, b, 10000 msat, 1 msat, routeParams = DEFAULT_ROUTE_PARAMS.copy(randomize = true), currentBlockHeight = BlockHeight(400000)) + val result = findMultiPartRoute(g, a, b, 10000 msat, 1 msat, routeParams = DEFAULT_ROUTE_PARAMS.copy(randomize = true).modify(_.mpp.splittingStrategy).setTo(Randomize), currentBlockHeight = BlockHeight(400000)) + assert(result == Failure(RouteNotFound)) + } + { + val result = findMultiPartRoute(g, a, b, 10000 msat, 1 msat, routeParams = DEFAULT_ROUTE_PARAMS.modify(_.mpp.splittingStrategy).setTo(MaxExpectedAmount), currentBlockHeight = BlockHeight(400000)) assert(result == Failure(RouteNotFound)) } } @@ -1193,14 +1152,14 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { // +--- B --- D ---+ val (amount, maxFee) = (30000 msat, 150 msat) val edge_ab = makeEdge(1L, a, b, 50 msat, 100, minHtlc = 1 msat, balance_opt = Some(15000 msat)) - val g = DirectedGraph(List( + val g = GraphWithBalanceEstimates(DirectedGraph(List( edge_ab, makeEdge(2L, b, d, 15 msat, 0, minHtlc = 1 msat, capacity = 25 sat), makeEdge(3L, d, e, 15 msat, 0, minHtlc = 0 msat, capacity = 20 sat), makeEdge(4L, a, c, 1 msat, 50, minHtlc = 1 msat, balance_opt = Some(10000 msat)), makeEdge(5L, a, c, 1 msat, 50, minHtlc = 1 msat, balance_opt = Some(8000 msat)), makeEdge(6L, c, e, 50 msat, 30, minHtlc = 1 msat, capacity = 20 sat), - )) + )), 1 day) { val Success(routes) = findMultiPartRoute(g, a, e, amount, maxFee, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) @@ -1209,14 +1168,18 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { } { // Update A - B with unknown balance, capacity should be used instead. - val g1 = g.addEdge(edge_ab.copy(capacity = 15 sat, balance_opt = None)) + val g1 = g.addEdge(makeEdge(1L, a, b, 50 msat, 100, minHtlc = 1 msat, capacity = 15 sat, balance_opt = None)) val Success(routes) = findMultiPartRoute(g1, a, e, amount, maxFee, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) checkRouteAmounts(routes, amount, maxFee) assert(routes2Ids(routes) == Set(Seq(1L, 2L, 3L), Seq(4L, 6L), Seq(5L, 6L))) } { // Randomize routes. - val Success(routes) = findMultiPartRoute(g, a, e, amount, maxFee, routeParams = DEFAULT_ROUTE_PARAMS.copy(randomize = true), currentBlockHeight = BlockHeight(400000)) + val Success(routes) = findMultiPartRoute(g, a, e, amount, maxFee, routeParams = DEFAULT_ROUTE_PARAMS.copy(randomize = true).modify(_.mpp.splittingStrategy).setTo(Randomize), currentBlockHeight = BlockHeight(400000)) + checkRouteAmounts(routes, amount, maxFee) + } + { + val Success(routes) = findMultiPartRoute(g, a, e, amount, maxFee, routeParams = DEFAULT_ROUTE_PARAMS.modify(_.mpp.splittingStrategy).setTo(MaxExpectedAmount), currentBlockHeight = BlockHeight(400000)) checkRouteAmounts(routes, amount, maxFee) } { @@ -1227,7 +1190,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { } { // Update capacity A - B to be too low. - val g1 = g.addEdge(edge_ab.copy(capacity = 5 sat, balance_opt = None)) + val g1 = g.addEdge(makeEdge(1L, a, b, 50 msat, 100, minHtlc = 1 msat, capacity = 5 sat, balance_opt = None)) val failure = findMultiPartRoute(g1, a, e, amount, maxFee, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(failure == Failure(RouteNotFound)) } @@ -1244,14 +1207,14 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { // | | // +--- B --- D ---+ // Our balance and the amount we want to send are below the minimum part amount. - val routeParams = DEFAULT_ROUTE_PARAMS.copy(mpp = MultiPartParams(5000 msat, 5)) - val g = DirectedGraph(List( + val routeParams = DEFAULT_ROUTE_PARAMS.copy(mpp = MultiPartParams(5000 msat, 5, DEFAULT_ROUTE_PARAMS.mpp.splittingStrategy)) + val g = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(1L, a, b, 50 msat, 100, minHtlc = 1 msat, balance_opt = Some(1500 msat)), makeEdge(2L, b, d, 15 msat, 0, minHtlc = 1 msat, capacity = 25 sat), makeEdge(3L, d, e, 15 msat, 0, minHtlc = 1 msat, capacity = 20 sat), makeEdge(4L, a, c, 1 msat, 50, minHtlc = 1 msat, balance_opt = Some(1000 msat)), makeEdge(5L, c, e, 50 msat, 30, minHtlc = 1 msat, capacity = 20 sat), - )) + )), 1 day) { // We can send single-part tiny payments. @@ -1269,11 +1232,11 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { test("calculate multipart route to remote node (single path)") { val (amount, maxFee) = (100000 msat, 500 msat) - val g = DirectedGraph(List( + val g = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(1L, a, b, 50 msat, 100, minHtlc = 1 msat, balance_opt = Some(500000 msat)), makeEdge(2L, b, c, 10 msat, 30, minHtlc = 1 msat, capacity = 150 sat), makeEdge(3L, c, d, 15 msat, 50, minHtlc = 1 msat, capacity = 150 sat), - )) + )), 1 day) val Success(routes) = findMultiPartRoute(g, a, d, amount, maxFee, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) checkRouteAmounts(routes, amount, maxFee) @@ -1289,7 +1252,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { // +----- E -------+ val (amount, maxFee) = (400000 msat, 250 msat) val edge_ab = makeEdge(1L, a, b, 50 msat, 100, minHtlc = 1 msat, balance_opt = Some(500000 msat)) - val g = DirectedGraph(List( + val g = GraphWithBalanceEstimates(DirectedGraph(List( edge_ab, makeEdge(2L, b, c, 10 msat, 30, minHtlc = 1 msat, capacity = 150 sat), makeEdge(3L, c, d, 15 msat, 50, minHtlc = 1 msat, capacity = 150 sat), @@ -1297,7 +1260,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { makeEdge(5L, d, f, 5 msat, 50, minHtlc = 1 msat, capacity = 300 sat), makeEdge(6L, b, e, 15 msat, 80, minHtlc = 1 msat, capacity = 210 sat), makeEdge(7L, e, f, 15 msat, 100, minHtlc = 1 msat, capacity = 200 sat), - )) + )), 1 day) { val Success(routes) = findMultiPartRoute(g, a, f, amount, maxFee, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) @@ -1306,12 +1269,16 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { } { // Randomize routes. - val Success(routes) = findMultiPartRoute(g, a, f, amount, maxFee, routeParams = DEFAULT_ROUTE_PARAMS.copy(randomize = true), currentBlockHeight = BlockHeight(400000)) + val Success(routes) = findMultiPartRoute(g, a, f, amount, maxFee, routeParams = DEFAULT_ROUTE_PARAMS.copy(randomize = true).modify(_.mpp.splittingStrategy).setTo(Randomize), currentBlockHeight = BlockHeight(400000)) + checkRouteAmounts(routes, amount, maxFee) + } + { + val Success(routes) = findMultiPartRoute(g, a, f, amount, maxFee, routeParams = DEFAULT_ROUTE_PARAMS.modify(_.mpp.splittingStrategy).setTo(MaxExpectedAmount), currentBlockHeight = BlockHeight(400000)) checkRouteAmounts(routes, amount, maxFee) } { // Update A - B with unknown balance, capacity should be used instead. - val g1 = g.addEdge(edge_ab.copy(capacity = 500 sat, balance_opt = None)) + val g1 = g.addEdge(makeEdge(1L, a, b, 50 msat, 100, minHtlc = 1 msat, capacity = 500 sat, balance_opt = None)) val Success(routes) = findMultiPartRoute(g1, a, f, amount, maxFee, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) checkRouteAmounts(routes, amount, maxFee) assert(routes2Ids(routes) == Set(Seq(1L, 2L, 3L, 5L), Seq(1L, 4L, 5L), Seq(1L, 6L, 7L))) @@ -1324,7 +1291,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { } { // Update capacity A - B to be too low to cover fees. - val g1 = g.addEdge(edge_ab.copy(capacity = 400 sat, balance_opt = None)) + val g1 = g.addEdge(makeEdge(1L, a, b, 50 msat, 100, minHtlc = 1 msat, capacity = 400 sat, balance_opt = None)) val failure = findMultiPartRoute(g1, a, f, amount, maxFee, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(failure == Failure(RouteNotFound)) } @@ -1359,12 +1326,12 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { makeEdge(100, a, c, 5 msat, 1000, minHtlc = 1 msat, capacity = 25000 sat, balance_opt = Some(20_000_000 msat)), makeEdge(101, c, d, 5 msat, 1000, minHtlc = 1 msat, capacity = 25000 sat), ) - val g = DirectedGraph(preferredEdges ++ cheapEdges) + val g = GraphWithBalanceEstimates(DirectedGraph(preferredEdges ++ cheapEdges), 1 day) { val amount = 15_000_000 msat val maxFee = 50_000 msat // this fee is enough to go through the preferred route - val routeParams = DEFAULT_ROUTE_PARAMS.copy(randomize = false, mpp = MultiPartParams(50_000 msat, 5)) + val routeParams = DEFAULT_ROUTE_PARAMS.copy(randomize = false, mpp = MultiPartParams(50_000 msat, 5, FullCapacity)) val Success(routes) = findMultiPartRoute(g, a, d, amount, maxFee, routeParams = routeParams, currentBlockHeight = BlockHeight(400000)) checkRouteAmounts(routes, amount, maxFee) assert(routes2Ids(routes) == Set(Seq(100L, 101L))) @@ -1372,14 +1339,14 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { { val amount = 15_000_000 msat val maxFee = 10_000 msat // this fee is too low to go through the preferred route - val routeParams = DEFAULT_ROUTE_PARAMS.copy(randomize = false, mpp = MultiPartParams(50_000 msat, 5)) + val routeParams = DEFAULT_ROUTE_PARAMS.copy(randomize = false, mpp = MultiPartParams(50_000 msat, 5, FullCapacity)) val failure = findMultiPartRoute(g, a, d, amount, maxFee, routeParams = routeParams, currentBlockHeight = BlockHeight(400000)) assert(failure == Failure(RouteNotFound)) } { val amount = 5_000_000 msat val maxFee = 10_000 msat // this fee is enough to go through the preferred route, but the cheaper ones can handle it - val routeParams = DEFAULT_ROUTE_PARAMS.copy(randomize = false, mpp = MultiPartParams(50_000 msat, 5)) + val routeParams = DEFAULT_ROUTE_PARAMS.copy(randomize = false, mpp = MultiPartParams(50_000 msat, 5, FullCapacity)) val Success(routes) = findMultiPartRoute(g, a, d, amount, maxFee, routeParams = routeParams, currentBlockHeight = BlockHeight(400000)) assert(routes.length == 5) routes.foreach(route => { @@ -1401,7 +1368,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { // | +-------------------+ | // +---------- E ----------+ val (amount, maxFee) = (25000 msat, 5 msat) - val g = DirectedGraph(List( + val g = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(1L, a, b, 1 msat, 0, minHtlc = 1 msat, balance_opt = Some(75000 msat)), makeEdge(2L, b, c, 1 msat, 0, minHtlc = 1 msat, capacity = 150 sat), makeEdge(3L, c, f, 1 msat, 0, minHtlc = 1 msat, capacity = 150 sat), @@ -1412,7 +1379,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { makeEdge(8L, a, f, 1 msat, 0, minHtlc = 1 msat, balance_opt = Some(10000 msat)), makeEdge(9L, a, e, 1 msat, 0, minHtlc = 1 msat, balance_opt = Some(18000 msat)), makeEdge(10L, e, f, 1 msat, 0, minHtlc = 1 msat, capacity = 15 sat), - )) + )), 1 day) val ignoredNodes = Set(d) val ignoredChannels = Set(ChannelDesc(ShortChannelId(2L), b, c)) @@ -1421,35 +1388,6 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { assert(routes2Ids(routes) == Set(Seq(8L), Seq(9L, 10L))) } - test("calculate multipart route to remote node (restricted htlc_minimum_msat and htlc_maximum_msat)") { - // +----- B -----+ - // | | - // A----- C ---- E - // | | - // +----- D -----+ - val (amount, maxFee) = (15000 msat, 5 msat) - val g = DirectedGraph(List( - // The A -> B -> E path is impossible because the A -> B balance is lower than the B -> E htlc_minimum_msat. - makeEdge(1L, a, b, 1 msat, 0, minHtlc = 500 msat, balance_opt = Some(7000 msat)), - makeEdge(2L, b, e, 1 msat, 0, minHtlc = 10000 msat, capacity = 50 sat), - makeEdge(3L, a, c, 1 msat, 0, minHtlc = 500 msat, balance_opt = Some(10000 msat)), - makeEdge(4L, c, e, 1 msat, 0, minHtlc = 500 msat, maxHtlc = Some(4000 msat), capacity = 50 sat), - makeEdge(5L, a, d, 1 msat, 0, minHtlc = 500 msat, balance_opt = Some(10000 msat)), - makeEdge(6L, d, e, 1 msat, 0, minHtlc = 500 msat, maxHtlc = Some(4000 msat), capacity = 50 sat), - )) - - val Success(routes) = findMultiPartRoute(g, a, e, amount, maxFee, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) - checkRouteAmounts(routes, amount, maxFee) - assert(routes.length >= 4, routes) - assert(routes.forall(_.amount <= 4000.msat), routes) - assert(routes.forall(_.amount >= 500.msat), routes) - checkIgnoredChannels(routes, 1L, 2L) - - val maxFeeTooLow = 3 msat - val failure = findMultiPartRoute(g, a, e, amount, maxFeeTooLow, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) - assert(failure == Failure(RouteNotFound)) - } - test("calculate multipart route to remote node (complex graph)") { // +---+ +---+ +---+ // | A |-----+ +--->| B |--->| C | @@ -1460,7 +1398,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { // +---+ | | +---+ | // | D |-----+ +--->| F |<-----+ // +---+ +---+ - val g = DirectedGraph(Seq( + val g = GraphWithBalanceEstimates(DirectedGraph(Seq( makeEdge(1L, d, a, 100 msat, 1000, minHtlc = 1000 msat, balance_opt = Some(80000 msat)), makeEdge(2L, d, e, 100 msat, 1000, minHtlc = 1500 msat, balance_opt = Some(20000 msat)), makeEdge(3L, a, e, 5 msat, 50, minHtlc = 1200 msat, capacity = 100 sat), @@ -1468,8 +1406,8 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { makeEdge(5L, e, b, 10 msat, 100, minHtlc = 1100 msat, capacity = 75 sat), makeEdge(6L, b, c, 5 msat, 50, minHtlc = 1000 msat, capacity = 20 sat), makeEdge(7L, c, f, 5 msat, 10, minHtlc = 1500 msat, capacity = 50 sat) - )) - val routeParams = DEFAULT_ROUTE_PARAMS.copy(mpp = MultiPartParams(1500 msat, 10)) + )), 1 day) + val routeParams = DEFAULT_ROUTE_PARAMS.copy(mpp = MultiPartParams(1500 msat, 10, DEFAULT_ROUTE_PARAMS.mpp.splittingStrategy)) { val (amount, maxFee) = (15000 msat, 50 msat) @@ -1493,7 +1431,12 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { } { val (amount, maxFee) = (40000 msat, 100 msat) - val Success(routes) = findMultiPartRoute(g, d, f, amount, maxFee, routeParams = routeParams.copy(randomize = true), currentBlockHeight = BlockHeight(400000)) + val Success(routes) = findMultiPartRoute(g, d, f, amount, maxFee, routeParams = routeParams.copy(randomize = true).modify(_.mpp.splittingStrategy).setTo(Randomize), currentBlockHeight = BlockHeight(400000)) + checkRouteAmounts(routes, amount, maxFee) + } + { + val (amount, maxFee) = (40000 msat, 100 msat) + val Success(routes) = findMultiPartRoute(g, d, f, amount, maxFee, routeParams = routeParams.modify(_.mpp.splittingStrategy).setTo(MaxExpectedAmount), currentBlockHeight = BlockHeight(400000)) checkRouteAmounts(routes, amount, maxFee) } { @@ -1508,12 +1451,12 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { // A D (---) E (---) F // +--- C ---+ val (amount, maxFeeE, maxFeeF) = (10000 msat, 50 msat, 100 msat) - val g = DirectedGraph(List( + val g = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(1L, a, b, 1 msat, 0, minHtlc = 1 msat, maxHtlc = Some(4000 msat), balance_opt = Some(7000 msat)), makeEdge(2L, b, d, 1 msat, 0, minHtlc = 1 msat, capacity = 50 sat), makeEdge(3L, a, c, 1 msat, 0, minHtlc = 1 msat, maxHtlc = Some(4000 msat), balance_opt = Some(6000 msat)), makeEdge(4L, c, d, 1 msat, 0, minHtlc = 1 msat, capacity = 40 sat), - )) + )), 1 day) val extraEdges = Set( makeEdge(10L, d, e, 10 msat, 100, minHtlc = 500 msat, capacity = 15 sat), makeEdge(11L, e, f, 5 msat, 100, minHtlc = 500 msat, capacity = 10 sat), @@ -1543,7 +1486,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { val (amount, maxFee) = (15000 msat, 100 msat) val edge_ab = makeEdge(1L, a, b, 1 msat, 0, minHtlc = 100 msat, balance_opt = Some(5000 msat)) val edge_be = makeEdge(2L, b, e, 1 msat, 0, minHtlc = 100 msat, capacity = 5 sat) - val g = DirectedGraph(List( + val g = GraphWithBalanceEstimates(DirectedGraph(List( // The A -> B -> E route is the most economic one, but we already have a pending HTLC in it. edge_ab, edge_be, @@ -1551,7 +1494,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { makeEdge(4L, c, e, 50 msat, 0, minHtlc = 100 msat, capacity = 25 sat), makeEdge(5L, a, d, 50 msat, 0, minHtlc = 100 msat, balance_opt = Some(10000 msat)), makeEdge(6L, d, e, 50 msat, 0, minHtlc = 100 msat, capacity = 25 sat), - )) + )), 1 day) val pendingHtlcs = Seq(Route(5000 msat, graphEdgeToHop(edge_ab) :: graphEdgeToHop(edge_be) :: Nil, None)) val Success(routes) = findMultiPartRoute(g, a, e, amount, maxFee, pendingHtlcs = pendingHtlcs, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) @@ -1580,7 +1523,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { for (_ <- 1 to 100) { val amount = (100 + Random.nextLong(200000)).msat val maxFee = 50.msat.max(amount * 0.03) - val g = DirectedGraph(List( + val g = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(1L, d, f, Random.nextLong(250).msat, Random.nextInt(10000), minHtlc = Random.nextLong(100).msat, maxHtlc = Some((20000 + Random.nextLong(80000)).msat), CltvExpiryDelta(Random.nextInt(288)), capacity = (10 + Random.nextLong(100)).sat, balance_opt = Some(Random.nextLong(2 * amount.toLong).msat)), makeEdge(2L, d, a, Random.nextLong(250).msat, Random.nextInt(10000), minHtlc = Random.nextLong(100).msat, maxHtlc = Some((20000 + Random.nextLong(80000)).msat), CltvExpiryDelta(Random.nextInt(288)), capacity = (10 + Random.nextLong(100)).sat, balance_opt = Some(Random.nextLong(2 * amount.toLong).msat)), makeEdge(3L, d, e, Random.nextLong(250).msat, Random.nextInt(10000), minHtlc = Random.nextLong(100).msat, maxHtlc = Some((20000 + Random.nextLong(80000)).msat), CltvExpiryDelta(Random.nextInt(288)), capacity = (10 + Random.nextLong(100)).sat, balance_opt = Some(Random.nextLong(2 * amount.toLong).msat)), @@ -1590,15 +1533,65 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { makeEdge(7L, e, b, Random.nextLong(250).msat, Random.nextInt(10000), minHtlc = Random.nextLong(100).msat, maxHtlc = Some((20000 + Random.nextLong(80000)).msat), CltvExpiryDelta(Random.nextInt(288)), capacity = (10 + Random.nextLong(100)).sat), makeEdge(8L, b, c, Random.nextLong(250).msat, Random.nextInt(10000), minHtlc = Random.nextLong(100).msat, maxHtlc = Some((20000 + Random.nextLong(80000)).msat), CltvExpiryDelta(Random.nextInt(288)), capacity = (10 + Random.nextLong(100)).sat), makeEdge(9L, c, f, Random.nextLong(250).msat, Random.nextInt(10000), minHtlc = Random.nextLong(100).msat, maxHtlc = Some((20000 + Random.nextLong(80000)).msat), CltvExpiryDelta(Random.nextInt(288)), capacity = (10 + Random.nextLong(100)).sat) - )) + )), 1 day) - findMultiPartRoute(g, d, f, amount, maxFee, routeParams = DEFAULT_ROUTE_PARAMS.copy(randomize = true), currentBlockHeight = BlockHeight(400000)) match { + findMultiPartRoute(g, d, f, amount, maxFee, routeParams = DEFAULT_ROUTE_PARAMS.copy(randomize = true).modify(_.mpp.splittingStrategy).setTo(Randomize), currentBlockHeight = BlockHeight(400000)) match { case Success(routes) => checkRouteAmounts(routes, amount, maxFee) case Failure(ex) => assert(ex == RouteNotFound) } } } + test("calculate multipart route to remote node using max expected amount splitting strategy") { + // A-------------E + // | | + // +----- B -----+ + // | | + // +----- C ---- + + // | | + // +----- D -----+ + val (amount, maxFee) = (60000 msat, 1000 msat) + val g = GraphWithBalanceEstimates(DirectedGraph(List( + // The A -> B -> E route is the most economic one, but we already have a pending HTLC in it. + makeEdge(0L, a, e, 50 msat, 0, minHtlc = 100 msat, balance_opt = Some(10000 msat)), + makeEdge(1L, a, b, 50 msat, 0, minHtlc = 100 msat, balance_opt = Some(100000 msat)), + makeEdge(2L, b, e, 50 msat, 0, minHtlc = 100 msat, capacity = 50 sat), + makeEdge(3L, a, c, 50 msat, 0, minHtlc = 100 msat, balance_opt = Some(100000 msat)), + makeEdge(4L, c, e, 50 msat, 0, minHtlc = 100 msat, capacity = 25 sat), + makeEdge(5L, a, d, 50 msat, 0, minHtlc = 100 msat, balance_opt = Some(100000 msat)), + makeEdge(6L, d, e, 50 msat, 0, minHtlc = 100 msat, capacity = 25 sat), + )), 1 day) + + val routeParams = DEFAULT_ROUTE_PARAMS.copy(mpp = DEFAULT_ROUTE_PARAMS.mpp.copy(splittingStrategy = MultiPartParams.MaxExpectedAmount)) + val Success(routes) = findMultiPartRoute(g, a, e, amount, maxFee, routeParams = routeParams, currentBlockHeight = BlockHeight(400000)) + checkRouteAmounts(routes, amount, maxFee) + assert(routes.map(route => (route.amount, route.hops.head.shortChannelId.toLong)).toSet == Set((10000 msat, 0L), (25000 msat, 1L), (12500 msat, 3L), (12500 msat, 5L))) + } + + test("calculate multipart route to remote node using max expected amount splitting strategy, respect minPartAmount") { + // +----- B -----+ + // | | + // A----- C ---- E + // | | + // +----- D -----+ + val (amount, maxFee) = (55000 msat, 1000 msat) + val g = GraphWithBalanceEstimates(DirectedGraph(List( + // The A -> B -> E route is the most economic one, but we already have a pending HTLC in it. + makeEdge(1L, a, b, 50 msat, 0, minHtlc = 100 msat, balance_opt = Some(100000 msat)), + makeEdge(2L, b, e, 50 msat, 0, minHtlc = 100 msat, capacity = 50 sat), + makeEdge(3L, a, c, 50 msat, 0, minHtlc = 100 msat, balance_opt = Some(100000 msat)), + makeEdge(4L, c, e, 50 msat, 0, minHtlc = 100 msat, capacity = 25 sat), + makeEdge(5L, a, d, 50 msat, 0, minHtlc = 100 msat, balance_opt = Some(100000 msat)), + makeEdge(6L, d, e, 50 msat, 0, minHtlc = 100 msat, capacity = 25 sat), + )), 1 day) + + val routeParams = DEFAULT_ROUTE_PARAMS.copy(mpp = DEFAULT_ROUTE_PARAMS.mpp.copy(minPartAmount = 15000 msat, splittingStrategy = MultiPartParams.MaxExpectedAmount)) + val Success(routes) = findMultiPartRoute(g, a, e, amount, maxFee, routeParams = routeParams, currentBlockHeight = BlockHeight(400000)) + assert(routes.forall(_.hops.length == 2), routes) + checkRouteAmounts(routes, amount, maxFee) + assert(routes.map(route => (route.amount, route.hops.head.shortChannelId.toLong)).toSet == Set((25000 msat, 1L), (15000 msat, 3L), (15000 msat, 5L))) + } + test("loop trap") { // +-----------------+ // | | @@ -1607,7 +1600,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { // ^ | // | | // F <---+ - val g = DirectedGraph(List( + val g = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(1L, a, b, 1000 msat, 1000), makeEdge(2L, b, c, 1000 msat, 1000), makeEdge(3L, c, d, 1000 msat, 1000), @@ -1615,7 +1608,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { makeEdge(5L, b, e, 1000 msat, 1000), makeEdge(6L, c, f, 1000 msat, 1000), makeEdge(7L, f, b, 1000 msat, 1000), - )) + )), 1 day) val Success(routes) = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 3, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(routes.length == 2) @@ -1632,7 +1625,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { // | ^ // | | // F ----+ - val g = DirectedGraph(List( + val g = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(1L, b, a, 1000 msat, 1000), makeEdge(2L, c, b, 1000 msat, 1000), makeEdge(3L, d, c, 1000 msat, 1000), @@ -1640,7 +1633,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { makeEdge(5L, e, b, 1000 msat, 1000), makeEdge(6L, f, c, 1000 msat, 1000), makeEdge(7L, b, f, 1000 msat, 1000), - )) + )), 1 day) val Success(routes) = findRoute(g, e, a, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 3, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(routes.length == 2) @@ -1676,7 +1669,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { q.toSeq } - val g = DirectedGraph(makeEdges(10)) + val g = GraphWithBalanceEstimates(DirectedGraph(makeEdges(10)), 1 day) val Success(routes) = findRoute(g, a, b, DEFAULT_AMOUNT_MSAT, 100000000 msat, numRoutes = 10, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(routes.distinct.length == 10) @@ -1708,7 +1701,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { q.toSeq } - val g = DirectedGraph(makeEdges(10)) + val g = GraphWithBalanceEstimates(DirectedGraph(makeEdges(10)), 1 day) val Success(routes) = findRoute(g, a, b, DEFAULT_AMOUNT_MSAT, 100000000 msat, numRoutes = 10, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(routes.distinct.length == 10) @@ -1717,9 +1710,9 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { } test("can't relay if fee is not sufficient") { - val g = DirectedGraph(List( + val g = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(1L, a, b, 1000 msat, 7000), - )) + )), 1 day) assert(findRoute(g, a, b, 10000000 msat, 10000 msat, numRoutes = 3, routeParams = DEFAULT_ROUTE_PARAMS.copy(includeLocalChannelCost = true), currentBlockHeight = BlockHeight(400000)) == Failure(RouteNotFound)) assert(findRoute(g, a, b, 10000000 msat, 100000 msat, numRoutes = 3, routeParams = DEFAULT_ROUTE_PARAMS.copy(includeLocalChannelCost = true), currentBlockHeight = BlockHeight(400000)).isSuccess) @@ -1734,7 +1727,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { // v | // E ---> F val start = randomKey().publicKey - val g = DirectedGraph(List( + val g = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(0L, start, a, 0 msat, 0), makeEdge(1L, a, b, 1000 msat, 1000), makeEdge(2L, a, c, 0 msat, 0), @@ -1743,7 +1736,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { makeEdge(5L, c, e, 0 msat, 0), makeEdge(6L, e, f, 600 msat, 1000), makeEdge(7L, f, d, 0 msat, 0), - )) + )), 1 day) { // No hop cost val Success(routes) = findRoute(g, start, b, DEFAULT_AMOUNT_MSAT, 100000000 msat, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) @@ -1752,13 +1745,13 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { assert(route2Ids(route) == 0 :: 2 :: 5 :: 6 :: 7 :: 4 :: Nil) } { // small base hop cost - val Success(routes) = findRoute(g, start, b, DEFAULT_AMOUNT_MSAT, 100000000 msat, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(heuristics = PaymentWeightRatios(1, 0, 0, 0, RelayFees(100 msat, 0))), currentBlockHeight = BlockHeight(400000)) + val Success(routes) = findRoute(g, start, b, DEFAULT_AMOUNT_MSAT, 100000000 msat, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(heuristics = HeuristicsConstants(0, RelayFees(0 msat, 0), RelayFees(100 msat, 0), useLogProbability = false, usePastRelaysData = false)), currentBlockHeight = BlockHeight(400000)) assert(routes.distinct.length == 1) val route :: Nil = routes assert(route2Ids(route) == 0 :: 2 :: 3 :: 4 :: Nil) } { // large proportional hop cost - val Success(routes) = findRoute(g, start, b, DEFAULT_AMOUNT_MSAT, 100000000 msat, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(heuristics = PaymentWeightRatios(1, 0, 0, 0, RelayFees(0 msat, 200))), currentBlockHeight = BlockHeight(400000)) + val Success(routes) = findRoute(g, start, b, DEFAULT_AMOUNT_MSAT, 100000000 msat, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(heuristics = HeuristicsConstants(0, RelayFees(0 msat, 0), RelayFees(0 msat, 200), useLogProbability = false, usePastRelaysData = false)), currentBlockHeight = BlockHeight(400000)) assert(routes.distinct.length == 1) val route :: Nil = routes assert(route2Ids(route) == 0 :: 1 :: Nil) @@ -1771,13 +1764,13 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { // v | // C ---> D val start = randomKey().publicKey - val g = DirectedGraph(List( + val g = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(0L, start, a, 0 msat, 0), makeEdge(1L, a, b, 1000 msat, 1000, capacity = (DEFAULT_AMOUNT_MSAT * 1.2).truncateToSatoshi), makeEdge(2L, a, c, 400 msat, 500, capacity = (DEFAULT_AMOUNT_MSAT * 3).truncateToSatoshi), makeEdge(3L, c, d, 400 msat, 500, capacity = (DEFAULT_AMOUNT_MSAT * 3).truncateToSatoshi), makeEdge(4L, d, b, 400 msat, 500, capacity = (DEFAULT_AMOUNT_MSAT * 3).truncateToSatoshi), - )) + )), 1 day) { val hc = HeuristicsConstants( @@ -1785,6 +1778,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { failureFees = RelayFees(1000 msat, 500), hopFees = RelayFees(0 msat, 0), useLogProbability = false, + usePastRelaysData = true, ) val Success(routes) = findRoute(g, start, b, DEFAULT_AMOUNT_MSAT, 100000000 msat, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(heuristics = hc), currentBlockHeight = BlockHeight(400000)) assert(routes.distinct.length == 1) @@ -1799,6 +1793,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { failureFees = RelayFees(10000 msat, 1000), hopFees = RelayFees(0 msat, 0), useLogProbability = true, + usePastRelaysData = true, ) val Success(routes) = findRoute(g, start, b, DEFAULT_AMOUNT_MSAT, 100000000 msat, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(heuristics = hc), currentBlockHeight = BlockHeight(400000)) assert(routes.distinct.length == 1) @@ -1813,19 +1808,20 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { // v | // C ---> D val start = randomKey().publicKey - val g = DirectedGraph(List( + val g = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(0L, start, a, 0 msat, 0), makeEdge(1L, a, b, 1000 msat, 1000, cltvDelta = CltvExpiryDelta(1000)), makeEdge(2L, a, c, 350 msat, 350, cltvDelta = CltvExpiryDelta(10)), makeEdge(3L, c, d, 350 msat, 350, cltvDelta = CltvExpiryDelta(10)), makeEdge(4L, d, b, 350 msat, 350, cltvDelta = CltvExpiryDelta(10)), - )) + )), 1 day) val hc = HeuristicsConstants( lockedFundsRisk = 1e-7, failureFees = RelayFees(0 msat, 0), hopFees = RelayFees(0 msat, 0), useLogProbability = true, + usePastRelaysData = true, ) val Success(routes) = findRoute(g, start, b, DEFAULT_AMOUNT_MSAT, 100000000 msat, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(heuristics = hc), currentBlockHeight = BlockHeight(400000)) assert(routes.distinct.length == 1) @@ -1835,17 +1831,18 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { test("edge too small to relay payment is ignored") { // A ===> B ===> C <--- D - val g = DirectedGraph(List( + val g = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(1L, a, b, 100 msat, 100), makeEdge(2L, b, c, 100 msat, 100), makeEdge(3L, d, c, 100 msat, 100, capacity = 1000 sat), - )) + )), 1 day) val hc = HeuristicsConstants( lockedFundsRisk = 1e-7, failureFees = RelayFees(0 msat, 0), hopFees = RelayFees(0 msat, 0), useLogProbability = true, + usePastRelaysData = true, ) val Success(routes) = findRoute(g, a, c, DEFAULT_AMOUNT_MSAT, 100000000 msat, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(heuristics = hc), currentBlockHeight = BlockHeight(400000)) assert(routes.distinct.length == 1) @@ -1857,18 +1854,18 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { // A ===> B ===> C // \___________/ val recentChannelId = ShortChannelId.fromCoordinates("399990x1x2").success.value.toLong - val g = DirectedGraph(List( + val g = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(1L, a, b, 1 msat, 1, capacity = 100_000_000 sat), makeEdge(2L, b, c, 1 msat, 1, capacity = 100_000_000 sat), makeEdge(recentChannelId, a, c, 1000 msat, 100), - )) + )), 1 day) - val wr = PaymentWeightRatios( - baseFactor = 0, - cltvDeltaFactor = 0, - ageFactor = 0.5, - capacityFactor = 0.5, + val wr = HeuristicsConstants( + lockedFundsRisk = 0, + failureFees = RelayFees(100 msat, 100), hopFees = RelayFees(500 msat, 200), + useLogProbability = false, + usePastRelaysData = false, ) val Success(routes) = findRoute(g, a, c, DEFAULT_AMOUNT_MSAT, 100_000_000 msat, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(heuristics = wr), currentBlockHeight = BlockHeight(400000)) assert(routes.distinct.length == 1) @@ -1878,7 +1875,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { test("trampoline relay with direct channel to target") { val amount = 100_000_000 msat - val g = DirectedGraph(List(makeEdge(1L, a, b, 1000 msat, 1000, capacity = 100_000_000 sat))) + val g = GraphWithBalanceEstimates(DirectedGraph(List(makeEdge(1L, a, b, 1000 msat, 1000, capacity = 100_000_000 sat))), 1 day) { val routeParams = DEFAULT_ROUTE_PARAMS.copy(includeLocalChannelCost = true, boundaries = SearchBoundaries(100_999 msat, 0.0, 6, CltvExpiryDelta(576))) @@ -1893,24 +1890,56 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { test("small local edge with liquidity is better than big remote edge") { // A == B == C -- D // \_______/ - val g = DirectedGraph(List( + val g = GraphWithBalanceEstimates(DirectedGraph(List( makeEdge(1L, a, b, 100 msat, 100, minHtlc = 1000 msat, capacity = 100000000 sat, balance_opt = Some(10000000 msat)), makeEdge(2L, b, c, 100 msat, 100, minHtlc = 1000 msat, capacity = 100000000 sat), makeEdge(3L, a, c, 100 msat, 100, minHtlc = 1000 msat, capacity = 100 sat, balance_opt = Some(100000 msat)), makeEdge(4L, c, d, 100 msat, 100, minHtlc = 1000 msat, capacity = 100000000 sat), - )) + )), 1 day) - val wr = PaymentWeightRatios( - baseFactor = 0, - cltvDeltaFactor = 0, - ageFactor = 0, - capacityFactor = 1, - hopFees = RelayFees(500 msat, 200), + val wr = HeuristicsConstants( + lockedFundsRisk = 0, + failureFees = RelayFees(1000 msat, 1000), + hopFees = RelayFees(0 msat, 0), + useLogProbability = false, + usePastRelaysData = false, ) val Success(routes) = findRoute(g, a, d, 50000 msat, 100000000 msat, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(heuristics = wr, includeLocalChannelCost = true), currentBlockHeight = BlockHeight(400000)) val route :: Nil = routes assert(route2Ids(route) == 3 :: 4 :: Nil) } + + test("take past attempts into account") { + // C + // / \ + // A -- B E + // \ / + // D + val g = GraphWithBalanceEstimates(DirectedGraph(List( + makeEdge(1L, a, b, 100 msat, 100, minHtlc = 1000 msat, capacity = 100000000 sat), + makeEdge(2L, b, c, 100 msat, 100, minHtlc = 1000 msat, capacity = 100000000 sat), + makeEdge(3L, c, e, 100 msat, 100, minHtlc = 1000 msat, capacity = 100000000 sat), + makeEdge(4L, b, d, 1000 msat, 1000, minHtlc = 1000 msat, capacity = 100000 sat), + makeEdge(5L, d, e, 1000 msat, 1000, minHtlc = 1000 msat, capacity = 100000 sat), + )), 1 day) + + val amount = 50000 msat + + val hc = HeuristicsConstants( + lockedFundsRisk = 0, + failureFees = RelayFees(1000 msat, 1000), + hopFees = RelayFees(500 msat, 200), + useLogProbability = true, + usePastRelaysData = true + ) + val Success(route1 :: Nil) = findRoute(g, a, e, amount, 100000000 msat, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(heuristics = hc, includeLocalChannelCost = true), currentBlockHeight = BlockHeight(400000)) + assert(route2Ids(route1) == 1 :: 2 :: 3 :: Nil) + + val h = g.routeCouldRelay(route1.stopAt(c)).channelCouldNotSend(route1.hops.last, amount) + + val Success(route2 :: Nil) = findRoute(h, a, e, amount, 100000000 msat, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(heuristics = hc, includeLocalChannelCost = true), currentBlockHeight = BlockHeight(400000)) + assert(route2Ids(route2) == 1 :: 4 :: 5 :: Nil) + } } object RouteCalculationSpec { @@ -1922,12 +1951,12 @@ object RouteCalculationSpec { val DEFAULT_EXPIRY = CltvExpiry(TestConstants.defaultBlockHeight) val DEFAULT_CAPACITY = 100_000 sat - val NO_WEIGHT_RATIOS: PaymentWeightRatios = PaymentWeightRatios(1, 0, 0, 0, RelayFees(0 msat, 0)) + val NO_WEIGHT_RATIOS: HeuristicsConstants = HeuristicsConstants(0, RelayFees(0 msat, 0), RelayFees(0 msat, 0), useLogProbability = false, usePastRelaysData = false) val DEFAULT_ROUTE_PARAMS = PathFindingConf( randomize = false, boundaries = SearchBoundaries(21000 msat, 0.03, 6, CltvExpiryDelta(2016)), NO_WEIGHT_RATIOS, - MultiPartParams(1000 msat, 10), + MultiPartParams(1000 msat, 10, FullCapacity), experimentName = "my-test-experiment", experimentPercentage = 100).getDefaultRouteParams @@ -1948,11 +1977,11 @@ object RouteCalculationSpec { cltvDelta: CltvExpiryDelta = CltvExpiryDelta(0), capacity: Satoshi = DEFAULT_CAPACITY, balance_opt: Option[MilliSatoshi] = None): GraphEdge = { - val update = makeUpdateShort(ShortChannelId(shortChannelId), nodeId1, nodeId2, feeBase, feeProportionalMillionth, minHtlc, maxHtlc, cltvDelta) + val update = makeUpdateShort(ShortChannelId(shortChannelId), nodeId1, nodeId2, feeBase, feeProportionalMillionth, minHtlc, maxHtlc.getOrElse(capacity.toMilliSatoshi), cltvDelta) GraphEdge(ChannelDesc(RealShortChannelId(shortChannelId), nodeId1, nodeId2), HopRelayParams.FromAnnouncement(update), capacity, balance_opt) } - def makeUpdateShort(shortChannelId: ShortChannelId, nodeId1: PublicKey, nodeId2: PublicKey, feeBase: MilliSatoshi, feeProportionalMillionth: Int, minHtlc: MilliSatoshi = DEFAULT_AMOUNT_MSAT, maxHtlc: Option[MilliSatoshi] = None, cltvDelta: CltvExpiryDelta = CltvExpiryDelta(0), timestamp: TimestampSecond = 0 unixsec): ChannelUpdate = + def makeUpdateShort(shortChannelId: ShortChannelId, nodeId1: PublicKey, nodeId2: PublicKey, feeBase: MilliSatoshi, feeProportionalMillionth: Int, minHtlc: MilliSatoshi = DEFAULT_AMOUNT_MSAT, maxHtlc: MilliSatoshi = 500_000_000 msat, cltvDelta: CltvExpiryDelta = CltvExpiryDelta(0), timestamp: TimestampSecond = 0 unixsec): ChannelUpdate = ChannelUpdate( signature = DUMMY_SIG, chainHash = Block.RegtestGenesisBlock.hash, @@ -1964,7 +1993,7 @@ object RouteCalculationSpec { htlcMinimumMsat = minHtlc, feeBaseMsat = feeBase, feeProportionalMillionths = feeProportionalMillionth, - htlcMaximumMsat = maxHtlc.getOrElse(500_000_000 msat) + htlcMaximumMsat = maxHtlc ) def hops2Ids(hops: Seq[ChannelHop]): Seq[Long] = hops.map(hop => hop.shortChannelId.toLong) @@ -1973,7 +2002,7 @@ object RouteCalculationSpec { def routes2Ids(routes: Seq[Route]): Set[Seq[Long]] = routes.map(route2Ids).toSet - def route2Edges(route: Route): Seq[GraphEdge] = route.hops.map(hop => GraphEdge(ChannelDesc(hop.shortChannelId, hop.nodeId, hop.nextNodeId), hop.params, 0 sat, None)) + def route2Edges(route: Route): Seq[GraphEdge] = route.hops.map(hop => GraphEdge(ChannelDesc(hop.shortChannelId, hop.nodeId, hop.nextNodeId), hop.params, 1000000 sat, None)) def route2Nodes(route: Route): Seq[(PublicKey, PublicKey)] = route.hops.map(hop => (hop.nodeId, hop.nextNodeId)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/RouterSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/RouterSpec.scala index 2856f0f48c..8ed41eead4 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/RouterSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/RouterSpec.scala @@ -558,7 +558,7 @@ class RouterSpec extends BaseRouterSpec { router ! RouteRequest(sender.ref, a, recipient, DEFAULT_ROUTE_PARAMS, allowMultiPart = true) val routes = sender.expectMessageType[RouteResponse].routes assert(routes.length == 2) - assert(routes.flatMap(_.finalHop_opt) == recipient.blindedHops) + assert(routes.flatMap(_.finalHop_opt).toSet == recipient.blindedHops.toSet) assert(routes.map(route => route2NodeIds(route)).toSet == Set(Seq(a, b), Seq(a, b, c))) assert(routes.map(route => route.blindedFee + route.channelFee(false)).toSet == Set(510 msat, 800 msat)) } @@ -586,9 +586,9 @@ class RouterSpec extends BaseRouterSpec { router ! RouteRequest(sender.ref, a, recipient, DEFAULT_ROUTE_PARAMS, allowMultiPart = true) val routes1 = sender.expectMessageType[RouteResponse].routes assert(routes1.length == 2) - router ! RouteRequest(sender.ref, a, recipient, DEFAULT_ROUTE_PARAMS, allowMultiPart = true, pendingPayments = Seq(routes1.head)) + router ! RouteRequest(sender.ref, a, recipient, DEFAULT_ROUTE_PARAMS, allowMultiPart = true, pendingPayments = routes1.drop(1)) val routes2 = sender.expectMessageType[RouteResponse].routes - assert(routes2 == routes1.tail) + assert(routes2 == routes1.take(1)) } { // One blinded route is pending, we send two htlcs to the other one: @@ -596,10 +596,11 @@ class RouterSpec extends BaseRouterSpec { router ! RouteRequest(sender.ref, a, recipient, DEFAULT_ROUTE_PARAMS, allowMultiPart = true) val routes1 = sender.expectMessageType[RouteResponse].routes assert(routes1.length == 2) - router ! RouteRequest(sender.ref, a, recipient, DEFAULT_ROUTE_PARAMS, allowMultiPart = true, pendingPayments = Seq(routes1.head)) + router ! RouteRequest(sender.ref, a, recipient, DEFAULT_ROUTE_PARAMS, allowMultiPart = true, pendingPayments = routes1.drop(1)) val routes2 = sender.expectMessageType[RouteResponse].routes - assert(routes2 == routes1.tail) - router ! RouteRequest(sender.ref, a, recipient, DEFAULT_ROUTE_PARAMS, allowMultiPart = true, pendingPayments = Seq(routes1.head, routes2.head.copy(amount = routes2.head.amount - 25_000.msat))) + assert(routes2.length == 1) + assert(routes2 == routes1.take(1)) + router ! RouteRequest(sender.ref, a, recipient, DEFAULT_ROUTE_PARAMS, allowMultiPart = true, pendingPayments = routes2.head.copy(amount = routes2.head.amount - 25_000.msat) +: routes1.drop(1)) val routes3 = sender.expectMessageType[RouteResponse].routes assert(routes3.length == 1) assert(routes3.head.amount == 25_000.msat) @@ -611,7 +612,7 @@ class RouterSpec extends BaseRouterSpec { router ! RouteRequest(sender.ref, a, recipient, routeParams1, allowMultiPart = true) val routes1 = sender.expectMessageType[RouteResponse].routes assert(routes1.length == 2) - assert(routes1.head.blindedFee + routes1.head.channelFee(false) == 800.msat) + assert(routes1.map(r => r.blindedFee + r.channelFee(false)) == Seq(800 msat, 510 msat)) val routeParams2 = DEFAULT_ROUTE_PARAMS.copy(boundaries = SearchBoundaries(1000 msat, 0.0, 6, CltvExpiryDelta(1008))) router ! RouteRequest(sender.ref, a, recipient, routeParams2, allowMultiPart = true, pendingPayments = Seq(routes1.head)) sender.expectMessage(PaymentRouteNotFound(RouteNotFound)) @@ -1097,12 +1098,12 @@ class RouterSpec extends BaseRouterSpec { assert(!data.channels.contains(scid2)) assert(data.scid2PrivateChannels.get(aliases2.localAlias.toLong).contains(commitments2.channelId)) val chan2 = data.privateChannels(commitments2.channelId) - assert(chan2.capacity == 99_000.sat) // for private channels, we use the balance to compute the channel's capacity + assert(chan2.capacity == 98_340.sat) // for private channels, we use the balance to compute the channel's capacity assert((chan2.update_1_opt.toSet ++ chan2.update_2_opt.toSet) == Set(update2)) } // The second channel is announced and moves from the private channels to the public channels. - val fundingConfirmed = LocalFundingStatus.ConfirmedFundingTx(Transaction(2, Nil, TxOut(100_000 sat, Nil) :: Nil, 0), scid2, None, None) + val fundingConfirmed = LocalFundingStatus.ConfirmedFundingTx(Nil, TxOut(100_000 sat, Nil), scid2, None, None) val commitments3 = commitments2.updateLocalFundingStatus(commitments2.latest.fundingTxId, fundingConfirmed, None)(akka.event.NoLogging).toOption.get._1 assert(commitments3.channelId == commitments2.channelId) sender.send(router, LocalChannelUpdate(sender.ref, commitments3.channelId, aliases2, x.publicKey, Some(AnnouncedCommitment(commitments3.latest.commitment, announcement2)), update2, commitments3)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/testutils/PimpTestProbe.scala b/eclair-core/src/test/scala/fr/acinq/eclair/testutils/PimpTestProbe.scala index be46dc636a..47a84e835e 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/testutils/PimpTestProbe.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/testutils/PimpTestProbe.scala @@ -1,10 +1,13 @@ package fr.acinq.eclair.testutils import akka.testkit.TestProbe -import fr.acinq.bitcoin.scalacompat.{Satoshi, TxId} +import fr.acinq.bitcoin.scalacompat.{OutPoint, Satoshi, TxId} import fr.acinq.eclair.MilliSatoshi -import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.{WatchFundingConfirmed, WatchFundingSpent, WatchPublished, WatchTxConfirmed} +import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ +import fr.acinq.eclair.blockchain.fee.ConfirmationTarget import fr.acinq.eclair.channel.AvailableBalanceChanged +import fr.acinq.eclair.channel.publish.TxPublisher.{PublishFinalTx, PublishReplaceableTx} +import fr.acinq.eclair.transactions.Transactions.ForceCloseTransaction import org.scalatest.Assertions import scala.reflect.ClassTag @@ -17,11 +20,34 @@ case class PimpTestProbe(probe: TestProbe) extends Assertions { * @param asserts should contains asserts on the message */ def expectMsgTypeHaving[T](asserts: T => Unit)(implicit t: ClassTag[T]): T = { - val msg = probe.expectMsgType[T] + val msg = probe.expectMsgType[T](t) asserts(msg) msg } + def expectFinalTxPublished(desc: String): PublishFinalTx = + expectMsgTypeHaving[PublishFinalTx](p => assert(p.desc == desc)) + + def expectFinalTxPublished(txId: TxId): PublishFinalTx = + expectMsgTypeHaving[PublishFinalTx](p => assert(p.tx.txid == txId)) + + private def expectForceCloseTx[T <: ForceCloseTransaction](tx: ForceCloseTransaction)(implicit t: ClassTag[T]): T = { + val c = t.runtimeClass.asInstanceOf[Class[T]] + assert(c.isInstance(tx), s"expected force-close tx of type ${c.getSimpleName} but got ${tx.getClass.getSimpleName}") + tx.asInstanceOf[T] + } + + def expectReplaceableTxPublished[T <: ForceCloseTransaction](implicit t: ClassTag[T]): T = { + val p = probe.expectMsgType[PublishReplaceableTx] + expectForceCloseTx(p.txInfo)(t) + } + + def expectReplaceableTxPublished[T <: ForceCloseTransaction](confirmationTarget: ConfirmationTarget)(implicit t: ClassTag[T]): T = { + val p = probe.expectMsgType[PublishReplaceableTx] + assert(p.confirmationTarget == confirmationTarget) + expectForceCloseTx(p.txInfo)(t) + } + def expectWatchFundingSpent(txid: TxId, hints_opt: Option[Set[TxId]] = None): WatchFundingSpent = expectMsgTypeHaving[WatchFundingSpent](w => { assert(w.txId == txid, "txid") @@ -31,6 +57,16 @@ case class PimpTestProbe(probe: TestProbe) extends Assertions { def expectWatchFundingConfirmed(txid: TxId): WatchFundingConfirmed = expectMsgTypeHaving[WatchFundingConfirmed](w => assert(w.txId == txid, "txid")) + def expectWatchOutputSpent(outpoint: OutPoint): WatchOutputSpent = + expectMsgTypeHaving[WatchOutputSpent](w => assert(OutPoint(w.txId, w.outputIndex.toLong) == outpoint, "outpoint")) + + def expectWatchOutputsSpent(outpoints: Seq[OutPoint]): Seq[WatchOutputSpent] = { + val watches = outpoints.map(_ => probe.expectMsgType[WatchOutputSpent]) + val watched = watches.map(w => OutPoint(w.txId, w.outputIndex.toLong)) + assert(watched.toSet == outpoints.toSet) + watches + } + def expectWatchTxConfirmed(txid: TxId): WatchTxConfirmed = expectMsgTypeHaving[WatchTxConfirmed](w => assert(w.txId == txid, "txid")) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/transactions/CommitmentSpecSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/transactions/CommitmentSpecSpec.scala index 97a0737be0..b74813d416 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/transactions/CommitmentSpecSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/transactions/CommitmentSpecSpec.scala @@ -18,6 +18,7 @@ package fr.acinq.eclair.transactions import fr.acinq.bitcoin.scalacompat.{ByteVector32, Crypto, SatoshiLong} import fr.acinq.eclair.blockchain.fee.FeeratePerKw +import fr.acinq.eclair.reputation.Reputation import fr.acinq.eclair.wire.protocol.{UpdateAddHtlc, UpdateFailHtlc, UpdateFulfillHtlc} import fr.acinq.eclair.{CltvExpiry, MilliSatoshi, MilliSatoshiLong, TestConstants, randomBytes32} import org.scalatest.funsuite.AnyFunSuite @@ -29,11 +30,11 @@ class CommitmentSpecSpec extends AnyFunSuite { val R = randomBytes32() val H = Crypto.sha256(R) - val add1 = UpdateAddHtlc(ByteVector32.Zeroes, 1, (2000 * 1000) msat, H, CltvExpiry(400), TestConstants.emptyOnionPacket, None, 1.0, None) + val add1 = UpdateAddHtlc(ByteVector32.Zeroes, 1, (2000 * 1000) msat, H, CltvExpiry(400), TestConstants.emptyOnionPacket, None, Reputation.maxEndorsement, None) val spec1 = CommitmentSpec.reduce(spec, add1 :: Nil, Nil) assert(spec1 == spec.copy(htlcs = Set(OutgoingHtlc(add1)), toLocal = 3000000 msat)) - val add2 = UpdateAddHtlc(ByteVector32.Zeroes, 2, (1000 * 1000) msat, H, CltvExpiry(400), TestConstants.emptyOnionPacket, None, 1.0, None) + val add2 = UpdateAddHtlc(ByteVector32.Zeroes, 2, (1000 * 1000) msat, H, CltvExpiry(400), TestConstants.emptyOnionPacket, None, Reputation.maxEndorsement, None) val spec2 = CommitmentSpec.reduce(spec1, add2 :: Nil, Nil) assert(spec2 == spec1.copy(htlcs = Set(OutgoingHtlc(add1), OutgoingHtlc(add2)), toLocal = 2000000 msat)) @@ -51,11 +52,11 @@ class CommitmentSpecSpec extends AnyFunSuite { val R = randomBytes32() val H = Crypto.sha256(R) - val add1 = UpdateAddHtlc(ByteVector32.Zeroes, 1, (2000 * 1000) msat, H, CltvExpiry(400), TestConstants.emptyOnionPacket, None, 1.0, None) + val add1 = UpdateAddHtlc(ByteVector32.Zeroes, 1, (2000 * 1000) msat, H, CltvExpiry(400), TestConstants.emptyOnionPacket, None, Reputation.maxEndorsement, None) val spec1 = CommitmentSpec.reduce(spec, Nil, add1 :: Nil) assert(spec1 == spec.copy(htlcs = Set(IncomingHtlc(add1)), toRemote = 3000 * 1000 msat)) - val add2 = UpdateAddHtlc(ByteVector32.Zeroes, 2, (1000 * 1000) msat, H, CltvExpiry(400), TestConstants.emptyOnionPacket, None, 1.0, None) + val add2 = UpdateAddHtlc(ByteVector32.Zeroes, 2, (1000 * 1000) msat, H, CltvExpiry(400), TestConstants.emptyOnionPacket, None, Reputation.maxEndorsement, None) val spec2 = CommitmentSpec.reduce(spec1, Nil, add2 :: Nil) assert(spec2 == spec1.copy(htlcs = Set(IncomingHtlc(add1), IncomingHtlc(add2)), toRemote = (2000 * 1000) msat)) @@ -70,13 +71,12 @@ class CommitmentSpecSpec extends AnyFunSuite { test("compute htlc tx feerate based on commitment format") { val spec = CommitmentSpec(htlcs = Set(), commitTxFeerate = FeeratePerKw(2500 sat), toLocal = (5000 * 1000) msat, toRemote = (2500 * 1000) msat) - assert(spec.htlcTxFeerate(Transactions.DefaultCommitmentFormat) == FeeratePerKw(2500 sat)) assert(spec.htlcTxFeerate(Transactions.UnsafeLegacyAnchorOutputsCommitmentFormat) == FeeratePerKw(2500 sat)) assert(spec.htlcTxFeerate(Transactions.ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) == FeeratePerKw(0 sat)) } def createHtlc(amount: MilliSatoshi): UpdateAddHtlc = { - UpdateAddHtlc(ByteVector32.Zeroes, 0, amount, randomBytes32(), CltvExpiry(500), TestConstants.emptyOnionPacket, None, 1.0, None) + UpdateAddHtlc(ByteVector32.Zeroes, 0, amount, randomBytes32(), CltvExpiry(500), TestConstants.emptyOnionPacket, None, Reputation.maxEndorsement, None) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TestVectorsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TestVectorsSpec.scala index 81a7111cf4..9ce0d162b3 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TestVectorsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TestVectorsSpec.scala @@ -20,12 +20,11 @@ import fr.acinq.bitcoin.ScriptFlags import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin.scalacompat.{ByteVector32, Crypto, Satoshi, SatoshiLong, Script, Transaction} import fr.acinq.eclair.blockchain.fee.FeeratePerKw -import fr.acinq.eclair.channel.ChannelFeatures -import fr.acinq.eclair.channel.Helpers.Funding -import fr.acinq.eclair.crypto.Generators +import fr.acinq.eclair.crypto.keymanager.{ChannelKeys, LocalCommitmentKeys, RemoteCommitmentKeys} +import fr.acinq.eclair.reputation.Reputation import fr.acinq.eclair.transactions.Transactions._ import fr.acinq.eclair.wire.protocol.UpdateAddHtlc -import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, Features, MilliSatoshi, MilliSatoshiLong, TestConstants} +import fr.acinq.eclair.{ChannelTypeFeature, CltvExpiry, CltvExpiryDelta, Features, MilliSatoshi, MilliSatoshiLong, TestConstants} import grizzled.slf4j.Logging import org.scalatest.funsuite.AnyFunSuite import scodec.bits._ @@ -36,10 +35,15 @@ trait TestVectorsSpec extends AnyFunSuite with Logging { // @formatter:off def filename: String - def channelFeatures: ChannelFeatures - val commitmentFormat = channelFeatures.commitmentFormat + def channelFeatures: Set[ChannelTypeFeature] // @formatter:on + val commitmentFormat: CommitmentFormat = if (channelFeatures.contains(Features.AnchorOutputsZeroFeeHtlcTx)) { + ZeroFeeHtlcTxAnchorOutputsCommitmentFormat + } else { + UnsafeLegacyAnchorOutputsCommitmentFormat + } + val tests = { val tests = collection.mutable.HashMap.empty[String, Map[String, String]] val current = collection.mutable.HashMap.empty[String, String] @@ -97,9 +101,9 @@ trait TestVectorsSpec extends AnyFunSuite with Logging { val funding_privkey = PrivateKey(hex"30ff4956bbdd3222d44cc5e8a1261dab1e07957bdac5ae88fe3261ef321f374901") val funding_pubkey = funding_privkey.publicKey val per_commitment_point = PublicKey(hex"025f7117a78150fe2ef97db7cfc83bd57b2e2c0d0dd25eaf467a4a1c2a45ce1486") - val htlc_privkey = Generators.derivePrivKey(payment_basepoint_secret, per_commitment_point) - val payment_privkey = if (channelFeatures.hasFeature(Features.StaticRemoteKey)) payment_basepoint_secret else htlc_privkey - val delayed_payment_privkey = Generators.derivePrivKey(delayed_payment_basepoint_secret, per_commitment_point) + val htlc_privkey = ChannelKeys.derivePerCommitmentKey(payment_basepoint_secret, per_commitment_point) + val payment_privkey = if (channelFeatures.contains(Features.StaticRemoteKey)) payment_basepoint_secret else htlc_privkey + val delayed_payment_privkey = ChannelKeys.derivePerCommitmentKey(delayed_payment_basepoint_secret, per_commitment_point) val revocation_pubkey = PublicKey(hex"0212a140cd0c6539d07cd08dfe09984dec3251ea808b892efeac3ede9402bf2b19") val feerate_per_kw = 15000 } @@ -114,17 +118,37 @@ trait TestVectorsSpec extends AnyFunSuite with Logging { val revocation_basepoint = revocation_basepoint_secret.publicKey val funding_privkey = PrivateKey(hex"1552dfba4f6cf29a62a0af13c8d6981d36d0ef8d61ba10fb0fe90da7634d7e1301") val funding_pubkey = funding_privkey.publicKey - val htlc_privkey = Generators.derivePrivKey(payment_basepoint_secret, Local.per_commitment_point) - val payment_privkey = if (channelFeatures.hasFeature(Features.StaticRemoteKey)) payment_basepoint_secret else htlc_privkey + val htlc_privkey = ChannelKeys.derivePerCommitmentKey(payment_basepoint_secret, Local.per_commitment_point) + val payment_privkey = if (channelFeatures.contains(Features.StaticRemoteKey)) payment_basepoint_secret else htlc_privkey } + // Keys used by the local node to spend outputs of its local commitment. + val localCommitmentKeys = LocalCommitmentKeys( + ourDelayedPaymentKey = Local.delayed_payment_privkey, + theirPaymentPublicKey = Remote.payment_privkey.publicKey, + ourPaymentBasePoint = Local.payment_basepoint, + ourHtlcKey = Local.htlc_privkey, + theirHtlcPublicKey = Remote.htlc_privkey.publicKey, + revocationPublicKey = Local.revocation_pubkey + ) + // Keys used by the remote node to spend outputs of our local commitment. + val remoteCommitmentKeys = RemoteCommitmentKeys( + ourPaymentKey = Remote.payment_privkey, + theirDelayedPaymentPublicKey = Local.delayed_payment_privkey.publicKey, + ourPaymentBasePoint = Remote.payment_basepoint, + ourHtlcKey = Remote.htlc_privkey, + theirHtlcPublicKey = Local.htlc_privkey.publicKey, + revocationPublicKey = Local.revocation_pubkey + ) + val coinbaseTx = Transaction.read("01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff03510101ffffffff0100f2052a010000001976a9143ca33c2e4446f4a305f23c80df8ad1afdcf652f988ac00000000") val fundingTx = Transaction.read("0200000001adbb20ea41a8423ea937e76e8151636bf6093b70eaff942930d20576600521fd000000006b48304502210090587b6201e166ad6af0227d3036a9454223d49a1f11839c1a362184340ef0240220577f7cd5cca78719405cbf1de7414ac027f0239ef6e214c90fcaab0454d84b3b012103535b32d5eb0a6ed0982a0479bbadc9868d9836f6ba94dd5a63be16d875069184ffffffff028096980000000000220020c015c4a6be010e21657068fc2e6a9d02b27ebe4d490a25846f7237f104d1a3cd20256d29010000001600143ca33c2e4446f4a305f23c80df8ad1afdcf652f900000000") val fundingAmount = fundingTx.txOut(0).amount logger.info(s"# funding-tx: $fundingTx}") - val commitmentInput = Funding.makeFundingInputInfo(fundingTx.txid, 0, fundingAmount, Local.funding_pubkey, Remote.funding_pubkey) + val fundingScript = makeFundingScript(Local.funding_pubkey, Remote.funding_pubkey, commitmentFormat).asInstanceOf[RedeemInfo.SegwitV0] + val commitmentInput = makeFundingInputInfo(fundingTx.txid, 0, fundingAmount, Local.funding_pubkey, Remote.funding_pubkey, commitmentFormat) val obscured_tx_number = Transactions.obscuredCommitTxNumber(42, localIsChannelOpener = true, Local.payment_basepoint, Remote.payment_basepoint) assert(obscured_tx_number == (0x2bb038521914L ^ 42L)) @@ -140,8 +164,8 @@ trait TestVectorsSpec extends AnyFunSuite with Logging { logger.info(s"remotekey: ${Remote.payment_privkey.publicKey}") logger.info(s"local_delayedkey: ${Local.delayed_payment_privkey.publicKey}") logger.info(s"local_revocation_key: ${Local.revocation_pubkey}") - logger.info(s"# funding wscript = ${commitmentInput.redeemScript}") - assert(commitmentInput.redeemScript == hex"5221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae") + logger.info(s"# funding wscript = ${fundingScript.redeemScript}") + assert(fundingScript.redeemScript == hex"5221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae") val paymentPreimages = Seq( ByteVector32(hex"0000000000000000000000000000000000000000000000000000000000000000"), @@ -153,17 +177,17 @@ trait TestVectorsSpec extends AnyFunSuite with Logging { ) val htlcs = Seq[DirectedHtlc]( - IncomingHtlc(UpdateAddHtlc(ByteVector32.Zeroes, 0, 1000000 msat, Crypto.sha256(paymentPreimages(0)), CltvExpiry(500), TestConstants.emptyOnionPacket, None, 1.0, None)), - IncomingHtlc(UpdateAddHtlc(ByteVector32.Zeroes, 1, 2000000 msat, Crypto.sha256(paymentPreimages(1)), CltvExpiry(501), TestConstants.emptyOnionPacket, None, 1.0, None)), - OutgoingHtlc(UpdateAddHtlc(ByteVector32.Zeroes, 0, 2000000 msat, Crypto.sha256(paymentPreimages(2)), CltvExpiry(502), TestConstants.emptyOnionPacket, None, 1.0, None)), - OutgoingHtlc(UpdateAddHtlc(ByteVector32.Zeroes, 1, 3000000 msat, Crypto.sha256(paymentPreimages(3)), CltvExpiry(503), TestConstants.emptyOnionPacket, None, 1.0, None)), - IncomingHtlc(UpdateAddHtlc(ByteVector32.Zeroes, 2, 4000000 msat, Crypto.sha256(paymentPreimages(4)), CltvExpiry(504), TestConstants.emptyOnionPacket, None, 1.0, None)), - OutgoingHtlc(UpdateAddHtlc(ByteVector32.Zeroes, 2, 5000001.msat, Crypto.sha256(paymentPreimages(5)), CltvExpiry(505), TestConstants.emptyOnionPacket, None, 1.0, None)), - OutgoingHtlc(UpdateAddHtlc(ByteVector32.Zeroes, 3, 5000000.msat, Crypto.sha256(paymentPreimages(5)), CltvExpiry(506), TestConstants.emptyOnionPacket, None, 1.0, None)) + IncomingHtlc(UpdateAddHtlc(ByteVector32.Zeroes, 0, 1000000 msat, Crypto.sha256(paymentPreimages(0)), CltvExpiry(500), TestConstants.emptyOnionPacket, None, Reputation.maxEndorsement, None)), + IncomingHtlc(UpdateAddHtlc(ByteVector32.Zeroes, 1, 2000000 msat, Crypto.sha256(paymentPreimages(1)), CltvExpiry(501), TestConstants.emptyOnionPacket, None, Reputation.maxEndorsement, None)), + OutgoingHtlc(UpdateAddHtlc(ByteVector32.Zeroes, 0, 2000000 msat, Crypto.sha256(paymentPreimages(2)), CltvExpiry(502), TestConstants.emptyOnionPacket, None, Reputation.maxEndorsement, None)), + OutgoingHtlc(UpdateAddHtlc(ByteVector32.Zeroes, 1, 3000000 msat, Crypto.sha256(paymentPreimages(3)), CltvExpiry(503), TestConstants.emptyOnionPacket, None, Reputation.maxEndorsement, None)), + IncomingHtlc(UpdateAddHtlc(ByteVector32.Zeroes, 2, 4000000 msat, Crypto.sha256(paymentPreimages(4)), CltvExpiry(504), TestConstants.emptyOnionPacket, None, Reputation.maxEndorsement, None)), + OutgoingHtlc(UpdateAddHtlc(ByteVector32.Zeroes, 2, 5000001.msat, Crypto.sha256(paymentPreimages(5)), CltvExpiry(505), TestConstants.emptyOnionPacket, None, Reputation.maxEndorsement, None)), + OutgoingHtlc(UpdateAddHtlc(ByteVector32.Zeroes, 3, 5000000.msat, Crypto.sha256(paymentPreimages(5)), CltvExpiry(506), TestConstants.emptyOnionPacket, None, Reputation.maxEndorsement, None)) ) val htlcScripts = htlcs.map { - case OutgoingHtlc(add) => Scripts.htlcOffered(Local.htlc_privkey.publicKey, Remote.htlc_privkey.publicKey, Local.revocation_pubkey, Crypto.ripemd160(add.paymentHash), commitmentFormat) - case IncomingHtlc(add) => Scripts.htlcReceived(Local.htlc_privkey.publicKey, Remote.htlc_privkey.publicKey, Local.revocation_pubkey, Crypto.ripemd160(add.paymentHash), add.cltvExpiry, commitmentFormat) + case OutgoingHtlc(add) => Scripts.htlcOffered(localCommitmentKeys.publicKeys, add.paymentHash, commitmentFormat) + case IncomingHtlc(add) => Scripts.htlcReceived(localCommitmentKeys.publicKeys, add.paymentHash, add.cltvExpiry, commitmentFormat) } val defaultHtlcs = htlcs.take(5) // most test cases only use the first 5 htlcs @@ -179,7 +203,7 @@ trait TestVectorsSpec extends AnyFunSuite with Logging { logger.info(s"htlc $i payment_preimage: ${if (i < paymentPreimages.size) paymentPreimages(i) else paymentPreimages.last}") } - def run(name: String, specHtlcs: Set[DirectedHtlc]): (CommitTx, Seq[TransactionWithInputInfo]) = { + def run(name: String, specHtlcs: Set[DirectedHtlc]): (Transaction, Seq[Transaction]) = { logger.info(s"name: $name") val spec = CommitmentSpec(specHtlcs, getFeerate(name), getToLocal(name), getToRemote(name)) val dustLimit = getDustLimit(name).getOrElse(Local.dustLimit) @@ -188,19 +212,15 @@ trait TestVectorsSpec extends AnyFunSuite with Logging { logger.info(s"local_feerate_per_kw: ${spec.commitTxFeerate}") val outputs = Transactions.makeCommitTxOutputs( - localPaysCommitTxFees = true, - localDustLimit = dustLimit, - localRevocationPubkey = Local.revocation_pubkey, - toLocalDelay = Local.toSelfDelay, - localDelayedPaymentPubkey = Local.delayed_payment_privkey.publicKey, - remotePaymentPubkey = Remote.payment_privkey.publicKey, - localHtlcPubkey = Local.htlc_privkey.publicKey, - remoteHtlcPubkey = Remote.htlc_privkey.publicKey, - localFundingPubkey = Local.funding_pubkey, - remoteFundingPubkey = Remote.funding_pubkey, + Local.funding_pubkey, + Remote.funding_pubkey, + localCommitmentKeys.publicKeys, + payCommitTxFees = true, + dustLimit, + Local.toSelfDelay, spec, - commitmentFormat) - + commitmentFormat + ) val commitTx = { val tx = Transactions.makeCommitTx( commitTxInput = commitmentInput, @@ -209,111 +229,111 @@ trait TestVectorsSpec extends AnyFunSuite with Logging { remotePaymentBasePoint = Remote.payment_basepoint, localIsChannelOpener = true, outputs = outputs) - val local_sig = tx.sign(Local.funding_privkey, TxOwner.Local, commitmentFormat, Map.empty) - logger.info(s"# local_signature = ${Scripts.der(local_sig).dropRight(1).toHex}") - val remote_sig = tx.sign(Remote.funding_privkey, TxOwner.Remote, commitmentFormat, Map.empty) - logger.info(s"remote_signature: ${Scripts.der(remote_sig).dropRight(1).toHex}") - Transactions.addSigs(tx, Local.funding_pubkey, Remote.funding_pubkey, local_sig, remote_sig) + val local_sig = tx.sign(Local.funding_privkey, Remote.funding_pubkey) + logger.info(s"# local_signature = ${Scripts.der(local_sig.sig).dropRight(1).toHex}") + val remote_sig = tx.sign(Remote.funding_privkey, Local.funding_pubkey) + logger.info(s"remote_signature: ${Scripts.der(remote_sig.sig).dropRight(1).toHex}") + tx.aggregateSigs(Local.funding_pubkey, Remote.funding_pubkey, local_sig, remote_sig) } val baseFee = Transactions.commitTxFeeMsat(dustLimit, spec, commitmentFormat) logger.info(s"# base commitment transaction fee = ${baseFee.toLong}") - val actualFee = fundingAmount - commitTx.tx.txOut.map(_.amount).sum + val actualFee = fundingAmount - commitTx.txOut.map(_.amount).sum logger.info(s"# actual commitment transaction fee = ${actualFee.toLong}") - commitTx.tx.txOut.foreach(txOut => { + commitTx.txOut.foreach(txOut => { txOut.publicKeyScript.length match { case 22 => logger.info(s"# to-remote amount ${txOut.amount.toLong} P2WPKH(${Remote.payment_privkey.publicKey})") case 34 => val index = htlcScripts.indexWhere(s => Script.write(Script.pay2wsh(s)) == txOut.publicKeyScript) - if (index == -1) logger.info(s"# to-local amount ${txOut.amount.toLong} wscript ${Script.write(Scripts.toLocalDelayed(Local.revocation_pubkey, Local.toSelfDelay, Local.delayed_payment_privkey.publicKey))}") + if (index == -1) logger.info(s"# to-local amount ${txOut.amount.toLong} wscript ${Script.write(Scripts.toLocalDelayed(localCommitmentKeys.publicKeys, Local.toSelfDelay))}") else logger.info(s"# HTLC #${if (htlcs(index).isInstanceOf[OutgoingHtlc]) "offered" else "received"} amount ${txOut.amount.toLong} wscript ${Script.write(htlcScripts(index))}") } }) - assert(Transactions.getCommitTxNumber(commitTx.tx, localIsChannelOpener = true, Local.payment_basepoint, Remote.payment_basepoint) == Local.commitTxNumber) - Transaction.correctlySpends(commitTx.tx, Seq(fundingTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - logger.info(s"output commit_tx: ${commitTx.tx}") + assert(Transactions.getCommitTxNumber(commitTx, localIsChannelOpener = true, Local.payment_basepoint, Remote.payment_basepoint) == Local.commitTxNumber) + Transaction.correctlySpends(commitTx, Seq(fundingTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + logger.info(s"output commit_tx: $commitTx") val unsignedHtlcTxs = Transactions.makeHtlcTxs( - commitTx.tx, - dustLimit, - Local.revocation_pubkey, - Local.toSelfDelay, Local.delayed_payment_privkey.publicKey, - spec.htlcTxFeerate(commitmentFormat), + commitTx, outputs, - commitmentFormat) - + commitmentFormat + ) val htlcTxs: Seq[TransactionWithInputInfo] = unsignedHtlcTxs.sortBy(_.input.outPoint.index) logger.info(s"num_htlcs: ${htlcTxs.length}") val signedTxs = htlcTxs.collect { - case tx: HtlcSuccessTx => - val localSig = tx.sign(Local.htlc_privkey, TxOwner.Local, commitmentFormat, Map.empty) - val remoteSig = tx.sign(Remote.htlc_privkey, TxOwner.Remote, commitmentFormat, Map.empty) - val htlcIndex = htlcScripts.indexOf(Script.parse(tx.input.asInstanceOf[InputInfo.SegwitInput].redeemScript)) + case tx: UnsignedHtlcSuccessTx => + val remoteSig = tx.localSig(remoteCommitmentKeys) + val htlcIndex = tx.redeemInfo(localCommitmentKeys.publicKeys) match { + case redeemInfo: RedeemInfo.SegwitV0 => htlcScripts.indexOf(Script.parse(redeemInfo.redeemScript)) + case _: RedeemInfo.Taproot => ??? + } val preimage = paymentPreimages.find(p => Crypto.sha256(p) == tx.paymentHash).get - val tx1 = Transactions.addSigs(tx, localSig, remoteSig, preimage, commitmentFormat) - Transaction.correctlySpends(tx1.tx, Seq(commitTx.tx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + val tx1 = tx.addRemoteSig(localCommitmentKeys, remoteSig, preimage).sign() + Transaction.correctlySpends(tx1, Seq(commitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) logger.info(s"# signature for output #${tx.input.outPoint.index} (htlc-success for htlc #$htlcIndex)") logger.info(s"remote_htlc_signature = ${Scripts.der(remoteSig).dropRight(1).toHex}") - logger.info(s"# local_htlc_signature = ${Scripts.der(localSig).dropRight(1).toHex}") - logger.info(s"htlc_success_tx (htlc #$htlcIndex): ${tx1.tx}") + logger.info(s"# local_htlc_signature = ${Scripts.der(tx.addRemoteSig(localCommitmentKeys, remoteSig, preimage).localSig(WalletInputs(Nil, None))).dropRight(1).toHex}") + logger.info(s"htlc_success_tx (htlc #$htlcIndex): $tx1") tx1 - case tx: HtlcTimeoutTx => - val localSig = tx.sign(Local.htlc_privkey, TxOwner.Local, commitmentFormat, Map.empty) - val remoteSig = tx.sign(Remote.htlc_privkey, TxOwner.Remote, commitmentFormat, Map.empty) - val htlcIndex = htlcScripts.indexOf(Script.parse(tx.input.asInstanceOf[InputInfo.SegwitInput].redeemScript)) - val tx1 = Transactions.addSigs(tx, localSig, remoteSig, commitmentFormat) - Transaction.correctlySpends(tx1.tx, Seq(commitTx.tx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + case tx: UnsignedHtlcTimeoutTx => + val remoteSig = tx.localSig(remoteCommitmentKeys) + val htlcIndex = tx.redeemInfo(localCommitmentKeys.publicKeys) match { + case redeemInfo: RedeemInfo.SegwitV0 => htlcScripts.indexOf(Script.parse(redeemInfo.redeemScript)) + case _: RedeemInfo.Taproot => ??? + } + val tx1 = tx.addRemoteSig(localCommitmentKeys, remoteSig).sign() + Transaction.correctlySpends(tx1, Seq(commitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) logger.info(s"# signature for output #${tx.input.outPoint.index} (htlc-timeout for htlc #$htlcIndex)") logger.info(s"remote_htlc_signature = ${Scripts.der(remoteSig).dropRight(1).toHex}") - logger.info(s"# local_htlc_signature = ${Scripts.der(localSig).dropRight(1).toHex}") - logger.info(s"htlc_timeout_tx (htlc #$htlcIndex): ${tx1.tx}") + logger.info(s"# local_htlc_signature = ${Scripts.der(tx.addRemoteSig(localCommitmentKeys, remoteSig).localSig(WalletInputs(Nil, None))).dropRight(1).toHex}") + logger.info(s"htlc_timeout_tx (htlc #$htlcIndex): $tx1") tx1 } (commitTx, signedTxs) } - def verifyHtlcTxs(name: String, htlcTxs: Seq[TransactionWithInputInfo]): Unit = { + def verifyHtlcTxs(name: String, htlcTxs: Seq[Transaction]): Unit = { val check = (0 to 4).flatMap(i => tests(name).get(s"htlc_success_tx (htlc #$i)").toSeq ++ tests(name).get(s"htlc_timeout_tx (htlc #$i)").toSeq ).toSet.map((tx: String) => Transaction.read(tx)) - assert(htlcTxs.map(_.tx).toSet == check) + assert(htlcTxs.toSet == check) } test("simple commitment tx with no HTLCs") { val name = "simple commitment tx with no HTLCs" val (commitTx, _) = run(name, Set.empty[DirectedHtlc]) - assert(commitTx.tx == Transaction.read(tests(name)("output commit_tx"))) + assert(commitTx == Transaction.read(tests(name)("output commit_tx"))) } test("simple commitment tx with no HTLCs and single anchor") { val name = "simple commitment tx with no HTLCs and single anchor" if (commitmentFormat.isInstanceOf[AnchorOutputsCommitmentFormat]) { val (commitTx, _) = run(name, Set.empty[DirectedHtlc]) - assert(commitTx.tx == Transaction.read(tests(name)("output commit_tx"))) + assert(commitTx == Transaction.read(tests(name)("output commit_tx"))) } } test("commitment tx with all five HTLCs untrimmed (minimum feerate)") { val name = "commitment tx with all five HTLCs untrimmed (minimum feerate)" val (commitTx, htlcTxs) = run(name, defaultHtlcs.toSet) - assert(commitTx.tx == Transaction.read(tests(name)("output commit_tx"))) + assert(commitTx == Transaction.read(tests(name)("output commit_tx"))) verifyHtlcTxs(name, htlcTxs) } test("commitment tx with seven outputs untrimmed (maximum feerate)") { val name = "commitment tx with seven outputs untrimmed (maximum feerate)" val (commitTx, htlcTxs) = run(name, defaultHtlcs.toSet) - assert(commitTx.tx == Transaction.read(tests(name)("output commit_tx"))) + assert(commitTx == Transaction.read(tests(name)("output commit_tx"))) verifyHtlcTxs(name, htlcTxs) } test("commitment tx with six outputs untrimmed (minimum feerate)") { val name = "commitment tx with six outputs untrimmed (minimum feerate)" val (commitTx, htlcTxs) = run(name, defaultHtlcs.toSet) - assert(commitTx.tx == Transaction.read(tests(name)("output commit_tx"))) + assert(commitTx == Transaction.read(tests(name)("output commit_tx"))) verifyHtlcTxs(name, htlcTxs) } @@ -321,7 +341,7 @@ trait TestVectorsSpec extends AnyFunSuite with Logging { if (commitmentFormat != ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) { val name = "commitment tx with six outputs untrimmed (maximum feerate)" val (commitTx, htlcTxs) = run(name, defaultHtlcs.toSet) - assert(commitTx.tx == Transaction.read(tests(name)("output commit_tx"))) + assert(commitTx == Transaction.read(tests(name)("output commit_tx"))) verifyHtlcTxs(name, htlcTxs) } } @@ -330,7 +350,7 @@ trait TestVectorsSpec extends AnyFunSuite with Logging { if (commitmentFormat != ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) { val name = "commitment tx with five outputs untrimmed (minimum feerate)" val (commitTx, htlcTxs) = run(name, defaultHtlcs.toSet) - assert(commitTx.tx == Transaction.read(tests(name)("output commit_tx"))) + assert(commitTx == Transaction.read(tests(name)("output commit_tx"))) verifyHtlcTxs(name, htlcTxs) } } @@ -339,7 +359,7 @@ trait TestVectorsSpec extends AnyFunSuite with Logging { if (commitmentFormat != ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) { val name = "commitment tx with five outputs untrimmed (maximum feerate)" val (commitTx, htlcTxs) = run(name, defaultHtlcs.toSet) - assert(commitTx.tx == Transaction.read(tests(name)("output commit_tx"))) + assert(commitTx == Transaction.read(tests(name)("output commit_tx"))) verifyHtlcTxs(name, htlcTxs) } } @@ -347,7 +367,7 @@ trait TestVectorsSpec extends AnyFunSuite with Logging { test("commitment tx with four outputs untrimmed (minimum feerate)") { val name = "commitment tx with four outputs untrimmed (minimum feerate)" val (commitTx, htlcTxs) = run(name, defaultHtlcs.toSet) - assert(commitTx.tx == Transaction.read(tests(name)("output commit_tx"))) + assert(commitTx == Transaction.read(tests(name)("output commit_tx"))) verifyHtlcTxs(name, htlcTxs) } @@ -355,7 +375,7 @@ trait TestVectorsSpec extends AnyFunSuite with Logging { if (commitmentFormat != ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) { val name = "commitment tx with four outputs untrimmed (maximum feerate)" val (commitTx, htlcTxs) = run(name, defaultHtlcs.toSet) - assert(commitTx.tx == Transaction.read(tests(name)("output commit_tx"))) + assert(commitTx == Transaction.read(tests(name)("output commit_tx"))) verifyHtlcTxs(name, htlcTxs) } } @@ -363,7 +383,7 @@ trait TestVectorsSpec extends AnyFunSuite with Logging { test("commitment tx with three outputs untrimmed (minimum feerate)") { val name = "commitment tx with three outputs untrimmed (minimum feerate)" val (commitTx, htlcTxs) = run(name, defaultHtlcs.toSet) - assert(commitTx.tx == Transaction.read(tests(name)("output commit_tx"))) + assert(commitTx == Transaction.read(tests(name)("output commit_tx"))) verifyHtlcTxs(name, htlcTxs) } @@ -371,7 +391,7 @@ trait TestVectorsSpec extends AnyFunSuite with Logging { if (commitmentFormat != ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) { val name = "commitment tx with three outputs untrimmed (maximum feerate)" val (commitTx, htlcTxs) = run(name, defaultHtlcs.toSet) - assert(commitTx.tx == Transaction.read(tests(name)("output commit_tx"))) + assert(commitTx == Transaction.read(tests(name)("output commit_tx"))) verifyHtlcTxs(name, htlcTxs) } } @@ -379,7 +399,7 @@ trait TestVectorsSpec extends AnyFunSuite with Logging { test("commitment tx with two outputs untrimmed (minimum feerate)") { val name = "commitment tx with two outputs untrimmed (minimum feerate)" val (commitTx, htlcTxs) = run(name, defaultHtlcs.toSet) - assert(commitTx.tx == Transaction.read(tests(name)("output commit_tx"))) + assert(commitTx == Transaction.read(tests(name)("output commit_tx"))) verifyHtlcTxs(name, htlcTxs) } @@ -387,7 +407,7 @@ trait TestVectorsSpec extends AnyFunSuite with Logging { if (commitmentFormat != ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) { val name = "commitment tx with two outputs untrimmed (maximum feerate)" val (commitTx, htlcTxs) = run(name, defaultHtlcs.toSet) - assert(commitTx.tx == Transaction.read(tests(name)("output commit_tx"))) + assert(commitTx == Transaction.read(tests(name)("output commit_tx"))) verifyHtlcTxs(name, htlcTxs) } } @@ -395,7 +415,7 @@ trait TestVectorsSpec extends AnyFunSuite with Logging { test("commitment tx with one output untrimmed (minimum feerate)") { val name = "commitment tx with one output untrimmed (minimum feerate)" val (commitTx, htlcTxs) = run(name, defaultHtlcs.toSet) - assert(commitTx.tx == Transaction.read(tests(name)("output commit_tx"))) + assert(commitTx == Transaction.read(tests(name)("output commit_tx"))) verifyHtlcTxs(name, htlcTxs) } @@ -403,7 +423,7 @@ trait TestVectorsSpec extends AnyFunSuite with Logging { if (commitmentFormat != ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) { val name = "commitment tx with fee greater than funder amount" val (commitTx, htlcTxs) = run(name, defaultHtlcs.toSet) - assert(commitTx.tx == Transaction.read(tests(name)("output commit_tx"))) + assert(commitTx == Transaction.read(tests(name)("output commit_tx"))) verifyHtlcTxs(name, htlcTxs) } } @@ -412,40 +432,26 @@ trait TestVectorsSpec extends AnyFunSuite with Logging { val name = "commitment tx with 3 htlc outputs, 2 offered having the same amount and preimage" val someHtlcs = Seq(htlcs(1), htlcs(6), htlcs(5)) val (commitTx, htlcTxs) = run(name, someHtlcs.toSet[DirectedHtlc]) - assert(commitTx.tx == Transaction.read(tests(name)("output commit_tx"))) + assert(commitTx == Transaction.read(tests(name)("output commit_tx"))) assert(htlcTxs.size == 3) // one htlc-success-tx + two htlc-timeout-tx - assert(htlcTxs(0).tx == Transaction.read(tests(name)("htlc_success_tx (htlc #1)"))) - assert(htlcTxs(1).tx == Transaction.read(tests(name)("htlc_timeout_tx (htlc #6)"))) - assert(htlcTxs(2).tx == Transaction.read(tests(name)("htlc_timeout_tx (htlc #5)"))) + assert(htlcTxs(0) == Transaction.read(tests(name)("htlc_success_tx (htlc #1)"))) + assert(htlcTxs(1) == Transaction.read(tests(name)("htlc_timeout_tx (htlc #6)"))) + assert(htlcTxs(2) == Transaction.read(tests(name)("htlc_timeout_tx (htlc #5)"))) } } -class DefaultCommitmentTestVectorSpec extends TestVectorsSpec { - // @formatter:off - override def filename: String = "/bolt3-tx-test-vectors-default-commitment-format.txt" - override def channelFeatures: ChannelFeatures = ChannelFeatures() - // @formatter:on -} - -class StaticRemoteKeyTestVectorSpec extends TestVectorsSpec { - // @formatter:off - override def filename: String = "/bolt3-tx-test-vectors-static-remotekey-format.txt" - override def channelFeatures: ChannelFeatures = ChannelFeatures(Features.StaticRemoteKey) - // @formatter:on -} - class AnchorOutputsTestVectorSpec extends TestVectorsSpec { // @formatter:off override def filename: String = "/bolt3-tx-test-vectors-anchor-outputs-format.txt" - override def channelFeatures: ChannelFeatures = ChannelFeatures(Features.StaticRemoteKey, Features.AnchorOutputs) + override def channelFeatures: Set[ChannelTypeFeature] = Set(Features.StaticRemoteKey, Features.AnchorOutputs) // @formatter:on } class AnchorOutputsZeroFeeHtlcTxTestVectorSpec extends TestVectorsSpec { // @formatter:off override def filename: String = "/bolt3-tx-test-vectors-anchor-outputs-zero-fee-htlc-tx-format.txt" - override def channelFeatures: ChannelFeatures = ChannelFeatures(Features.StaticRemoteKey, Features.AnchorOutputsZeroFeeHtlcTx) + override def channelFeatures: Set[ChannelTypeFeature] = Set(Features.StaticRemoteKey, Features.AnchorOutputsZeroFeeHtlcTx) // @formatter:on } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TransactionsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TransactionsSpec.scala index b6ce30a205..513c5485be 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TransactionsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TransactionsSpec.scala @@ -18,14 +18,14 @@ package fr.acinq.eclair.transactions import fr.acinq.bitcoin.SigHash._ import fr.acinq.bitcoin.scalacompat.Crypto._ -import fr.acinq.bitcoin.scalacompat.Script.{pay2wpkh, pay2wsh, write} -import fr.acinq.bitcoin.scalacompat.{Btc, ByteVector32, Crypto, MilliBtc, MilliBtcDouble, Musig2, OP_2, OP_CHECKMULTISIG, OP_PUSHDATA, OP_RETURN, OutPoint, Protocol, Satoshi, SatoshiLong, Script, ScriptWitness, Transaction, TxId, TxIn, TxOut, millibtc2satoshi} -import fr.acinq.bitcoin.{ScriptFlags, ScriptTree, SigHash, SigVersion} +import fr.acinq.bitcoin.scalacompat.{Btc, ByteVector32, ByteVector64, Crypto, MilliBtc, MilliBtcDouble, Musig2, OP_2, OP_CHECKMULTISIG, OP_PUSHDATA, OP_RETURN, OutPoint, Satoshi, SatoshiLong, Script, Transaction, TxIn, TxOut} +import fr.acinq.bitcoin.{ScriptFlags, SigVersion} import fr.acinq.eclair.TestUtils.randomTxId import fr.acinq.eclair._ -import fr.acinq.eclair.blockchain.fee.{ConfirmationTarget, FeeratePerKw} -import fr.acinq.eclair.channel.Helpers.Funding -import fr.acinq.eclair.transactions.CommitmentOutput.{InHtlc, OutHtlc} +import fr.acinq.eclair.blockchain.fee.FeeratePerKw +import fr.acinq.eclair.channel.ChannelSpendSignature +import fr.acinq.eclair.crypto.keymanager.{LocalCommitmentKeys, RemoteCommitmentKeys} +import fr.acinq.eclair.reputation.Reputation import fr.acinq.eclair.transactions.Scripts._ import fr.acinq.eclair.transactions.Transactions.AnchorOutputsCommitmentFormat.anchorAmount import fr.acinq.eclair.transactions.Transactions._ @@ -34,28 +34,43 @@ import grizzled.slf4j.Logging import org.scalatest.funsuite.AnyFunSuite import scodec.bits._ -import java.nio.ByteOrder -import scala.io.Source -import scala.util.{Random, Try} +import scala.util.Random /** * Created by PM on 16/12/2016. */ class TransactionsSpec extends AnyFunSuite with Logging { - val localFundingPriv = PrivateKey(randomBytes32()) - val remoteFundingPriv = PrivateKey(randomBytes32()) - val localRevocationPriv = PrivateKey(randomBytes32()) - val localPaymentPriv = PrivateKey(randomBytes32()) - val localDelayedPaymentPriv = PrivateKey(randomBytes32()) - val remotePaymentPriv = PrivateKey(randomBytes32()) - val localHtlcPriv = PrivateKey(randomBytes32()) - val remoteHtlcPriv = PrivateKey(randomBytes32()) - val finalPubKeyScript = Script.write(Script.pay2wpkh(PrivateKey(randomBytes32()).publicKey)) - val commitInput = Funding.makeFundingInputInfo(randomTxId(), 0, Btc(1), localFundingPriv.publicKey, remoteFundingPriv.publicKey) - val toLocalDelay = CltvExpiryDelta(144) - val localDustLimit = Satoshi(546) - val feeratePerKw = FeeratePerKw(22000 sat) + private val localFundingPriv = randomKey() + private val remoteFundingPriv = randomKey() + private val localRevocationPriv = randomKey() + private val localPaymentPriv = randomKey() + private val localPaymentBasePoint = randomKey().publicKey + private val localDelayedPaymentPriv = randomKey() + private val remotePaymentPriv = randomKey() + private val localHtlcPriv = randomKey() + private val remoteHtlcPriv = randomKey() + // Keys used by the local node to spend outputs of its local commitment. + private val localKeys = LocalCommitmentKeys( + ourDelayedPaymentKey = localDelayedPaymentPriv, + theirPaymentPublicKey = remotePaymentPriv.publicKey, + ourPaymentBasePoint = localPaymentBasePoint, + ourHtlcKey = localHtlcPriv, + theirHtlcPublicKey = remoteHtlcPriv.publicKey, + revocationPublicKey = localRevocationPriv.publicKey, + ) + // Keys used by the remote node to spend outputs of our local commitment. + private val remoteKeys = RemoteCommitmentKeys( + ourPaymentKey = remotePaymentPriv, + theirDelayedPaymentPublicKey = localDelayedPaymentPriv.publicKey, + ourPaymentBasePoint = remotePaymentPriv.publicKey, + ourHtlcKey = remoteHtlcPriv, + theirHtlcPublicKey = localHtlcPriv.publicKey, + revocationPublicKey = localRevocationPriv.publicKey, + ) + private val toLocalDelay = CltvExpiryDelta(144) + private val localDustLimit = Satoshi(546) + private val feeratePerKw = FeeratePerKw(22000 sat) test("extract csv and cltv timeouts") { val parentTxId1 = randomTxId() @@ -99,342 +114,46 @@ class TransactionsSpec extends AnyFunSuite with Logging { } test("compute fees") { - // see BOLT #3 specs val htlcs = Set[DirectedHtlc]( - OutgoingHtlc(UpdateAddHtlc(ByteVector32.Zeroes, 0, 5000000 msat, ByteVector32.Zeroes, CltvExpiry(552), TestConstants.emptyOnionPacket, None, 1.0, None)), - OutgoingHtlc(UpdateAddHtlc(ByteVector32.Zeroes, 0, 1000000 msat, ByteVector32.Zeroes, CltvExpiry(553), TestConstants.emptyOnionPacket, None, 1.0, None)), - IncomingHtlc(UpdateAddHtlc(ByteVector32.Zeroes, 0, 7000000 msat, ByteVector32.Zeroes, CltvExpiry(550), TestConstants.emptyOnionPacket, None, 1.0, None)), - IncomingHtlc(UpdateAddHtlc(ByteVector32.Zeroes, 0, 800000 msat, ByteVector32.Zeroes, CltvExpiry(551), TestConstants.emptyOnionPacket, None, 1.0, None)) + OutgoingHtlc(UpdateAddHtlc(ByteVector32.Zeroes, 0, 5000000 msat, ByteVector32.Zeroes, CltvExpiry(552), TestConstants.emptyOnionPacket, None, Reputation.maxEndorsement, None)), + OutgoingHtlc(UpdateAddHtlc(ByteVector32.Zeroes, 0, 1000000 msat, ByteVector32.Zeroes, CltvExpiry(553), TestConstants.emptyOnionPacket, None, Reputation.maxEndorsement, None)), + IncomingHtlc(UpdateAddHtlc(ByteVector32.Zeroes, 0, 7000000 msat, ByteVector32.Zeroes, CltvExpiry(550), TestConstants.emptyOnionPacket, None, Reputation.maxEndorsement, None)), + IncomingHtlc(UpdateAddHtlc(ByteVector32.Zeroes, 0, 800000 msat, ByteVector32.Zeroes, CltvExpiry(551), TestConstants.emptyOnionPacket, None, Reputation.maxEndorsement, None)) ) val spec = CommitmentSpec(htlcs, FeeratePerKw(5000 sat), toLocal = 0 msat, toRemote = 0 msat) - val fee = commitTxFeeMsat(546 sat, spec, DefaultCommitmentFormat) - assert(fee == 5340000.msat) + val fee = commitTxFeeMsat(546 sat, spec, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) + assert(fee == 9_060_000.msat) } - test("check pre-computed transaction weights") { - val finalPubKeyScript = Script.write(Script.pay2wpkh(PrivateKey(randomBytes32()).publicKey)) - val localDustLimit = 546 sat - val toLocalDelay = CltvExpiryDelta(144) - val feeratePerKw = FeeratePerKw.MinimumFeeratePerKw - val blockHeight = BlockHeight(400000) - - { - // ClaimP2WPKHOutputTx - // first we create a fake commitTx tx, containing only the output that will be spent by the ClaimP2WPKHOutputTx - val pubKeyScript = write(pay2wpkh(localPaymentPriv.publicKey)) - val commitTx = Transaction(version = 2, txIn = Nil, txOut = TxOut(20000 sat, pubKeyScript) :: Nil, lockTime = 0) - val Right(claimP2WPKHOutputTx) = makeClaimP2WPKHOutputTx(commitTx, localDustLimit, localPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) - // we use dummy signatures to compute the weight - val weight = Transaction.weight(addSigs(claimP2WPKHOutputTx, localPaymentPriv.publicKey, PlaceHolderSig).tx) - assert(claimP2WPKHOutputWeight == weight) - assert(claimP2WPKHOutputTx.fee >= claimP2WPKHOutputTx.minRelayFee) - } - { - // HtlcDelayedTx - // first we create a fake htlcSuccessOrTimeoutTx tx, containing only the output that will be spent by the 3rd-stage tx - val pubKeyScript = write(pay2wsh(toLocalDelayed(localRevocationPriv.publicKey, toLocalDelay, localPaymentPriv.publicKey))) - val htlcSuccessOrTimeoutTx = Transaction(version = 2, txIn = Nil, txOut = TxOut(20000 sat, pubKeyScript) :: Nil, lockTime = 0) - val Right(htlcDelayedTx) = makeHtlcDelayedTx(htlcSuccessOrTimeoutTx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) - // we use dummy signatures to compute the weight - val weight = Transaction.weight(addSigs(htlcDelayedTx, PlaceHolderSig).tx) - assert(htlcDelayedWeight == weight) - assert(htlcDelayedTx.fee >= htlcDelayedTx.minRelayFee) - } - { - // MainPenaltyTx - // first we create a fake commitTx tx, containing only the output that will be spent by the MainPenaltyTx - val pubKeyScript = write(pay2wsh(toLocalDelayed(localRevocationPriv.publicKey, toLocalDelay, localPaymentPriv.publicKey))) - val commitTx = Transaction(version = 2, txIn = Nil, txOut = TxOut(20000 sat, pubKeyScript) :: Nil, lockTime = 0) - val Right(mainPenaltyTx) = makeMainPenaltyTx(commitTx, localDustLimit, localRevocationPriv.publicKey, finalPubKeyScript, toLocalDelay, localPaymentPriv.publicKey, feeratePerKw) - // we use dummy signatures to compute the weight - val weight = Transaction.weight(addSigs(mainPenaltyTx, PlaceHolderSig).tx) - assert(mainPenaltyWeight == weight) - assert(mainPenaltyTx.fee >= mainPenaltyTx.minRelayFee) - } - { - // HtlcPenaltyTx - // first we create a fake commitTx tx, containing only the output that will be spent by the ClaimHtlcSuccessTx - val paymentPreimage = randomBytes32() - val htlc = UpdateAddHtlc(ByteVector32.Zeroes, 0, (20000 * 1000) msat, sha256(paymentPreimage), CltvExpiryDelta(144).toCltvExpiry(blockHeight), TestConstants.emptyOnionPacket, None, 1.0, None) - val redeemScript = htlcReceived(localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localRevocationPriv.publicKey, ripemd160(htlc.paymentHash), htlc.cltvExpiry, DefaultCommitmentFormat) - val pubKeyScript = write(pay2wsh(redeemScript)) - val commitTx = Transaction(version = 2, txIn = Nil, txOut = TxOut(htlc.amountMsat.truncateToSatoshi, pubKeyScript) :: Nil, lockTime = 0) - val Right(htlcPenaltyTx) = makeHtlcPenaltyTx(commitTx, 0, Script.write(redeemScript), localDustLimit, finalPubKeyScript, feeratePerKw) - // we use dummy signatures to compute the weight - val weight = Transaction.weight(addSigs(htlcPenaltyTx, PlaceHolderSig, localRevocationPriv.publicKey).tx) - assert(htlcPenaltyWeight == weight) - assert(htlcPenaltyTx.fee >= htlcPenaltyTx.minRelayFee) - } - { - // ClaimHtlcSuccessTx - // first we create a fake commitTx tx, containing only the output that will be spent by the ClaimHtlcSuccessTx - val paymentPreimage = randomBytes32() - val htlc = UpdateAddHtlc(ByteVector32.Zeroes, 0, (20000 * 1000) msat, sha256(paymentPreimage), CltvExpiryDelta(144).toCltvExpiry(blockHeight), TestConstants.emptyOnionPacket, None, 1.0, None) - val spec = CommitmentSpec(Set(OutgoingHtlc(htlc)), feeratePerKw, toLocal = 0 msat, toRemote = 0 msat) - val outputs = makeCommitTxOutputs(localPaysCommitTxFees = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, spec, DefaultCommitmentFormat) - val pubKeyScript = write(pay2wsh(htlcOffered(localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localRevocationPriv.publicKey, ripemd160(htlc.paymentHash), DefaultCommitmentFormat))) - val commitTx = Transaction(version = 2, txIn = Nil, txOut = TxOut(htlc.amountMsat.truncateToSatoshi, pubKeyScript) :: Nil, lockTime = 0) - val Right(claimHtlcSuccessTx) = makeClaimHtlcSuccessTx(commitTx, outputs, localDustLimit, remoteHtlcPriv.publicKey, localHtlcPriv.publicKey, localRevocationPriv.publicKey, finalPubKeyScript, htlc, feeratePerKw, DefaultCommitmentFormat) - // we use dummy signatures to compute the weight - val weight = Transaction.weight(addSigs(claimHtlcSuccessTx, PlaceHolderSig, paymentPreimage).tx) - assert(claimHtlcSuccessWeight == weight) - assert(claimHtlcSuccessTx.fee >= claimHtlcSuccessTx.minRelayFee) - } - { - // ClaimHtlcTimeoutTx - // first we create a fake commitTx tx, containing only the output that will be spent by the ClaimHtlcTimeoutTx - val paymentPreimage = randomBytes32() - val htlc = UpdateAddHtlc(ByteVector32.Zeroes, 0, (20000 * 1000) msat, sha256(paymentPreimage), toLocalDelay.toCltvExpiry(blockHeight), TestConstants.emptyOnionPacket, None, 1.0, None) - val spec = CommitmentSpec(Set(IncomingHtlc(htlc)), feeratePerKw, toLocal = 0 msat, toRemote = 0 msat) - val outputs = makeCommitTxOutputs(localPaysCommitTxFees = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, spec, DefaultCommitmentFormat) - val pubKeyScript = write(pay2wsh(htlcReceived(localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localRevocationPriv.publicKey, ripemd160(htlc.paymentHash), htlc.cltvExpiry, DefaultCommitmentFormat))) - val commitTx = Transaction(version = 2, txIn = Nil, txOut = TxOut(htlc.amountMsat.truncateToSatoshi, pubKeyScript) :: Nil, lockTime = 0) - val Right(claimClaimHtlcTimeoutTx) = makeClaimHtlcTimeoutTx(commitTx, outputs, localDustLimit, remoteHtlcPriv.publicKey, localHtlcPriv.publicKey, localRevocationPriv.publicKey, finalPubKeyScript, htlc, feeratePerKw, DefaultCommitmentFormat) - // we use dummy signatures to compute the weight - val weight = Transaction.weight(addSigs(claimClaimHtlcTimeoutTx, PlaceHolderSig).tx) - assert(claimHtlcTimeoutWeight == weight) - assert(claimClaimHtlcTimeoutTx.fee >= claimClaimHtlcTimeoutTx.minRelayFee) - } - { - // ClaimAnchorOutputTx - // first we create a fake commitTx tx, containing only the output that will be spent by the ClaimAnchorOutputTx - val pubKeyScript = write(pay2wsh(anchor(localFundingPriv.publicKey))) - val commitTx = Transaction(version = 2, txIn = Nil, txOut = TxOut(anchorAmount, pubKeyScript) :: Nil, lockTime = 0) - val Right(claimAnchorOutputTx) = makeClaimLocalAnchorOutputTx(commitTx, localFundingPriv.publicKey, ConfirmationTarget.Absolute(BlockHeight(1105))) - assert(claimAnchorOutputTx.tx.txOut.isEmpty) - assert(claimAnchorOutputTx.confirmationTarget == ConfirmationTarget.Absolute(BlockHeight(1105))) - // we will always add at least one input and one output to be able to set our desired feerate - // we use dummy signatures to compute the weight - val p2wpkhWitness = ScriptWitness(Seq(Scripts.der(PlaceHolderSig), PlaceHolderPubKey.value)) - val claimAnchorOutputTxWithFees = claimAnchorOutputTx.copy(tx = claimAnchorOutputTx.tx.copy( - txIn = claimAnchorOutputTx.tx.txIn :+ TxIn(OutPoint(randomTxId(), 3), ByteVector.empty, 0, p2wpkhWitness), - txOut = Seq(TxOut(1500 sat, Script.pay2wpkh(randomKey().publicKey))) - )) - val signedTx = addSigs(claimAnchorOutputTxWithFees, PlaceHolderSig).tx - val weight = Transaction.weight(signedTx) - assert(weight == 717) - assert(weight >= claimAnchorOutputMinWeight) - val inputWeight = Transaction.weight(signedTx) - Transaction.weight(signedTx.copy(txIn = signedTx.txIn.tail)) - assert(inputWeight == anchorInputWeight) - } - } - - test("generate valid commitment with some outputs that don't materialize (default commitment format)") { - val spec = CommitmentSpec(htlcs = Set.empty, commitTxFeerate = feeratePerKw, toLocal = 400.millibtc.toMilliSatoshi, toRemote = 300.millibtc.toMilliSatoshi) - val commitFee = commitTxTotalCost(localDustLimit, spec, DefaultCommitmentFormat) - val belowDust = (localDustLimit * 0.9).toMilliSatoshi - val belowDustWithFee = (localDustLimit + commitFee * 0.9).toMilliSatoshi - - { - val toRemoteFundeeBelowDust = spec.copy(toRemote = belowDust) - val outputs = makeCommitTxOutputs(localPaysCommitTxFees = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, toRemoteFundeeBelowDust, DefaultCommitmentFormat) - assert(outputs.map(_.commitmentOutput) == Seq(CommitmentOutput.ToLocal)) - assert(outputs.head.output.amount.toMilliSatoshi == toRemoteFundeeBelowDust.toLocal - commitFee) - } - { - val toLocalFunderBelowDust = spec.copy(toLocal = belowDustWithFee) - val outputs = makeCommitTxOutputs(localPaysCommitTxFees = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, toLocalFunderBelowDust, DefaultCommitmentFormat) - assert(outputs.map(_.commitmentOutput) == Seq(CommitmentOutput.ToRemote)) - assert(outputs.head.output.amount.toMilliSatoshi == toLocalFunderBelowDust.toRemote) - } - { - val toRemoteFunderBelowDust = spec.copy(toRemote = belowDustWithFee) - val outputs = makeCommitTxOutputs(localPaysCommitTxFees = false, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, toRemoteFunderBelowDust, DefaultCommitmentFormat) - assert(outputs.map(_.commitmentOutput) == Seq(CommitmentOutput.ToLocal)) - assert(outputs.head.output.amount.toMilliSatoshi == toRemoteFunderBelowDust.toLocal) - } - { - val toLocalFundeeBelowDust = spec.copy(toLocal = belowDust) - val outputs = makeCommitTxOutputs(localPaysCommitTxFees = false, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, toLocalFundeeBelowDust, DefaultCommitmentFormat) - assert(outputs.map(_.commitmentOutput) == Seq(CommitmentOutput.ToRemote)) - assert(outputs.head.output.amount.toMilliSatoshi == toLocalFundeeBelowDust.toRemote - commitFee) - } - { - val allBelowDust = spec.copy(toLocal = belowDust, toRemote = belowDust) - val outputs = makeCommitTxOutputs(localPaysCommitTxFees = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, allBelowDust, DefaultCommitmentFormat) - assert(outputs.isEmpty) - } - } - - test("generate valid commitment and htlc transactions (default commitment format)") { - val finalPubKeyScript = Script.write(Script.pay2wpkh(PrivateKey(randomBytes32()).publicKey)) - val commitInput = Funding.makeFundingInputInfo(randomTxId(), 0, Btc(1), localFundingPriv.publicKey, remoteFundingPriv.publicKey) - - // htlc1 and htlc2 are regular IN/OUT htlcs - val paymentPreimage1 = randomBytes32() - val htlc1 = UpdateAddHtlc(ByteVector32.Zeroes, 0, MilliBtc(100).toMilliSatoshi, sha256(paymentPreimage1), CltvExpiry(300), TestConstants.emptyOnionPacket, None, 1.0, None) - val paymentPreimage2 = randomBytes32() - val htlc2 = UpdateAddHtlc(ByteVector32.Zeroes, 1, MilliBtc(200).toMilliSatoshi, sha256(paymentPreimage2), CltvExpiry(310), TestConstants.emptyOnionPacket, None, 1.0, None) - // htlc3 and htlc4 are dust IN/OUT htlcs, with an amount large enough to be included in the commit tx, but too small to be claimed at 2nd stage - val paymentPreimage3 = randomBytes32() - val htlc3 = UpdateAddHtlc(ByteVector32.Zeroes, 2, (localDustLimit + weight2fee(feeratePerKw, DefaultCommitmentFormat.htlcTimeoutWeight)).toMilliSatoshi, sha256(paymentPreimage3), CltvExpiry(295), TestConstants.emptyOnionPacket, None, 1.0, None) - val paymentPreimage4 = randomBytes32() - val htlc4 = UpdateAddHtlc(ByteVector32.Zeroes, 3, (localDustLimit + weight2fee(feeratePerKw, DefaultCommitmentFormat.htlcSuccessWeight)).toMilliSatoshi, sha256(paymentPreimage4), CltvExpiry(300), TestConstants.emptyOnionPacket, None, 1.0, None) - // htlc5 and htlc6 are dust IN/OUT htlcs - val htlc5 = UpdateAddHtlc(ByteVector32.Zeroes, 4, (localDustLimit * 0.9).toMilliSatoshi, sha256(randomBytes32()), CltvExpiry(295), TestConstants.emptyOnionPacket, None, 1.0, None) - val htlc6 = UpdateAddHtlc(ByteVector32.Zeroes, 5, (localDustLimit * 0.9).toMilliSatoshi, sha256(randomBytes32()), CltvExpiry(305), TestConstants.emptyOnionPacket, None, 1.0, None) - val spec = CommitmentSpec( - htlcs = Set( - OutgoingHtlc(htlc1), - IncomingHtlc(htlc2), - OutgoingHtlc(htlc3), - IncomingHtlc(htlc4), - OutgoingHtlc(htlc5), - IncomingHtlc(htlc6) + test("pre-compute wallet input and output weight") { + // ECDSA signatures are usually between at 71 and 73 bytes. + val dummyEcdsaSig = ByteVector.fill(73)(0) + val dummySchnorrSig = ByteVector64.Zeroes + val dummyTx = Transaction( + version = 2, + txIn = Seq( + TxIn(OutPoint(randomTxId(), 5), ByteVector.empty, 0, Script.witnessPay2wpkh(randomKey().publicKey, dummyEcdsaSig)), + TxIn(OutPoint(randomTxId(), 3), ByteVector.empty, 0, Script.witnessKeyPathPay2tr(dummySchnorrSig)) ), - commitTxFeerate = feeratePerKw, - toLocal = 400.millibtc.toMilliSatoshi, - toRemote = 300.millibtc.toMilliSatoshi) - - val outputs = makeCommitTxOutputs(localPaysCommitTxFees = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, spec, DefaultCommitmentFormat) - - val commitTxNumber = 0x404142434445L - val commitTx = { - val txInfo = makeCommitTx(commitInput, commitTxNumber, localPaymentPriv.publicKey, remotePaymentPriv.publicKey, localIsChannelOpener = true, outputs) - val localSig = txInfo.sign(localPaymentPriv, TxOwner.Local, DefaultCommitmentFormat, Map.empty) - val remoteSig = txInfo.sign(remotePaymentPriv, TxOwner.Remote, DefaultCommitmentFormat, Map.empty) - Transactions.addSigs(txInfo, localFundingPriv.publicKey, remoteFundingPriv.publicKey, localSig, remoteSig) - } - - { - assert(getCommitTxNumber(commitTx.tx, localIsChannelOpener = true, localPaymentPriv.publicKey, remotePaymentPriv.publicKey) == commitTxNumber) - val hash = Crypto.sha256(localPaymentPriv.publicKey.value ++ remotePaymentPriv.publicKey.value) - val num = Protocol.uint64(hash.takeRight(8).toArray, ByteOrder.BIG_ENDIAN) & 0xffffffffffffL - val check = ((commitTx.tx.txIn.head.sequence & 0xffffff) << 24) | (commitTx.tx.lockTime & 0xffffff) - assert((check ^ num) == commitTxNumber) - } - - val htlcTxs = makeHtlcTxs(commitTx.tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, spec.htlcTxFeerate(DefaultCommitmentFormat), outputs, DefaultCommitmentFormat) - assert(htlcTxs.length == 4) - val confirmationTargets = htlcTxs.map(tx => tx.htlcId -> tx.confirmationTarget.confirmBefore.toLong).toMap - assert(confirmationTargets == Map(0 -> 300, 1 -> 310, 2 -> 295, 3 -> 300)) - val htlcSuccessTxs = htlcTxs.collect { case tx: HtlcSuccessTx => tx } - val htlcTimeoutTxs = htlcTxs.collect { case tx: HtlcTimeoutTx => tx } - assert(htlcTimeoutTxs.size == 2) // htlc1 and htlc3 - assert(htlcTimeoutTxs.map(_.htlcId).toSet == Set(0, 2)) - assert(htlcSuccessTxs.size == 2) // htlc2 and htlc4 - assert(htlcSuccessTxs.map(_.htlcId).toSet == Set(1, 3)) + txOut = Seq( + TxOut(250_000 sat, Script.pay2wpkh(randomKey().publicKey)), + TxOut(250_000 sat, Script.pay2tr(randomKey().publicKey.xOnly, scripts_opt = None)), + ), + lockTime = 0 + ) + assert(dummyTx.weight() - dummyTx.copy(txIn = dummyTx.txIn.tail).weight() == p2wpkhInputWeight) + assert(dummyTx.weight() - dummyTx.copy(txIn = dummyTx.txIn.take(1)).weight() == p2trInputWeight) + assert(dummyTx.weight() - dummyTx.copy(txOut = dummyTx.txOut.tail).weight() == p2wpkhOutputWeight) + assert(dummyTx.weight() - dummyTx.copy(txOut = dummyTx.txOut.take(1)).weight() == p2trOutputWeight) + } - { - // either party spends local->remote htlc output with htlc timeout tx - for (htlcTimeoutTx <- htlcTimeoutTxs) { - val localSig = htlcTimeoutTx.sign(localHtlcPriv, TxOwner.Local, DefaultCommitmentFormat, Map.empty) - val remoteSig = htlcTimeoutTx.sign(remoteHtlcPriv, TxOwner.Remote, DefaultCommitmentFormat, Map.empty) - val signed = addSigs(htlcTimeoutTx, localSig, remoteSig, DefaultCommitmentFormat) - assert(checkSpendable(signed).isSuccess) - } - } - { - // local spends delayed output of htlc1 timeout tx - val Right(htlcDelayed) = makeHtlcDelayedTx(htlcTimeoutTxs(1).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) - val localSig = htlcDelayed.sign(localDelayedPaymentPriv, TxOwner.Local, DefaultCommitmentFormat, Map.empty) - val signedTx = addSigs(htlcDelayed, localSig) - assert(checkSpendable(signedTx).isSuccess) - // local can't claim delayed output of htlc3 timeout tx because it is below the dust limit - val htlcDelayed1 = makeHtlcDelayedTx(htlcTimeoutTxs(0).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) - assert(htlcDelayed1 == Left(OutputNotFound)) - } - { - // remote spends local->remote htlc1/htlc3 output directly in case of success - for ((htlc, paymentPreimage) <- (htlc1, paymentPreimage1) :: (htlc3, paymentPreimage3) :: Nil) { - val Right(claimHtlcSuccessTx) = makeClaimHtlcSuccessTx(commitTx.tx, outputs, localDustLimit, remoteHtlcPriv.publicKey, localHtlcPriv.publicKey, localRevocationPriv.publicKey, finalPubKeyScript, htlc, feeratePerKw, DefaultCommitmentFormat) - val localSig = claimHtlcSuccessTx.sign(remoteHtlcPriv, TxOwner.Local, DefaultCommitmentFormat, Map.empty) - val signed = addSigs(claimHtlcSuccessTx, localSig, paymentPreimage) - assert(checkSpendable(signed).isSuccess) - } - } - { - // local spends remote->local htlc2/htlc4 output with htlc success tx using payment preimage - for ((htlcSuccessTx, paymentPreimage) <- (htlcSuccessTxs(1), paymentPreimage2) :: (htlcSuccessTxs(0), paymentPreimage4) :: Nil) { - val localSig = htlcSuccessTx.sign(localHtlcPriv, TxOwner.Local, DefaultCommitmentFormat, Map.empty) - val remoteSig = htlcSuccessTx.sign(remoteHtlcPriv, TxOwner.Remote, DefaultCommitmentFormat, Map.empty) - val signedTx = addSigs(htlcSuccessTx, localSig, remoteSig, paymentPreimage, DefaultCommitmentFormat) - assert(checkSpendable(signedTx).isSuccess) - // check remote sig - assert(htlcSuccessTx.checkSig(remoteSig, remoteHtlcPriv.publicKey, TxOwner.Remote, DefaultCommitmentFormat)) - } - } - { - // local spends delayed output of htlc2 success tx - val Right(htlcDelayed) = makeHtlcDelayedTx(htlcSuccessTxs(1).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) - val localSig = htlcDelayed.sign(localDelayedPaymentPriv, TxOwner.Local, DefaultCommitmentFormat, Map.empty) - val signedTx = addSigs(htlcDelayed, localSig) - assert(checkSpendable(signedTx).isSuccess) - // local can't claim delayed output of htlc4 success tx because it is below the dust limit - val htlcDelayed1 = makeHtlcDelayedTx(htlcSuccessTxs(0).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) - assert(htlcDelayed1 == Left(AmountBelowDustLimit)) - } - { - // local spends main delayed output - val Right(claimMainOutputTx) = makeClaimLocalDelayedOutputTx(commitTx.tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) - val localSig = claimMainOutputTx.sign(localDelayedPaymentPriv, TxOwner.Local, DefaultCommitmentFormat, Map.empty) - val signedTx = addSigs(claimMainOutputTx, localSig) - assert(checkSpendable(signedTx).isSuccess) - } - { - // remote spends main output - val Right(claimP2WPKHOutputTx) = makeClaimP2WPKHOutputTx(commitTx.tx, localDustLimit, remotePaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) - val localSig = claimP2WPKHOutputTx.sign(remotePaymentPriv, TxOwner.Local, DefaultCommitmentFormat, Map.empty) - val signedTx = addSigs(claimP2WPKHOutputTx, remotePaymentPriv.publicKey, localSig) - assert(checkSpendable(signedTx).isSuccess) - } - { - // remote spends remote->local htlc output directly in case of timeout - val Right(claimHtlcTimeoutTx) = makeClaimHtlcTimeoutTx(commitTx.tx, outputs, localDustLimit, remoteHtlcPriv.publicKey, localHtlcPriv.publicKey, localRevocationPriv.publicKey, finalPubKeyScript, htlc2, feeratePerKw, DefaultCommitmentFormat) - val localSig = claimHtlcTimeoutTx.sign(remoteHtlcPriv, TxOwner.Local, DefaultCommitmentFormat, Map.empty) - val signed = addSigs(claimHtlcTimeoutTx, localSig) - assert(checkSpendable(signed).isSuccess) - } - { - // remote spends local main delayed output with revocation key - val Right(mainPenaltyTx) = makeMainPenaltyTx(commitTx.tx, localDustLimit, localRevocationPriv.publicKey, finalPubKeyScript, toLocalDelay, localDelayedPaymentPriv.publicKey, feeratePerKw) - val sig = mainPenaltyTx.sign(localRevocationPriv, TxOwner.Local, DefaultCommitmentFormat, Map.empty) - val signed = addSigs(mainPenaltyTx, sig) - assert(checkSpendable(signed).isSuccess) - } - { - // remote spends htlc1's htlc-timeout tx with revocation key - val Seq(Right(claimHtlcDelayedPenaltyTx)) = makeClaimHtlcDelayedOutputPenaltyTxs(htlcTimeoutTxs(1).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) - val sig = claimHtlcDelayedPenaltyTx.sign(localRevocationPriv, TxOwner.Local, DefaultCommitmentFormat, Map.empty) - val signed = addSigs(claimHtlcDelayedPenaltyTx, sig) - assert(checkSpendable(signed).isSuccess) - // remote can't claim revoked output of htlc3's htlc-timeout tx because it is below the dust limit - val claimHtlcDelayedPenaltyTx1 = makeClaimHtlcDelayedOutputPenaltyTxs(htlcTimeoutTxs(0).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) - assert(claimHtlcDelayedPenaltyTx1 == Seq(Left(AmountBelowDustLimit))) - } - { - // remote spends offered HTLC output with revocation key - val script = Script.write(Scripts.htlcOffered(localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localRevocationPriv.publicKey, Crypto.ripemd160(htlc1.paymentHash), DefaultCommitmentFormat)) - val Some(htlcOutputIndex) = outputs.zipWithIndex.find { - case (CommitmentOutputLink(_, _, OutHtlc(OutgoingHtlc(someHtlc))), _) => someHtlc.id == htlc1.id - case _ => false - }.map(_._2) - val Right(htlcPenaltyTx) = makeHtlcPenaltyTx(commitTx.tx, htlcOutputIndex, script, localDustLimit, finalPubKeyScript, feeratePerKw) - val sig = htlcPenaltyTx.sign(localRevocationPriv, TxOwner.Local, DefaultCommitmentFormat, Map.empty) - val signed = addSigs(htlcPenaltyTx, sig, localRevocationPriv.publicKey) - assert(checkSpendable(signed).isSuccess) - } - { - // remote spends htlc2's htlc-success tx with revocation key - val Seq(Right(claimHtlcDelayedPenaltyTx)) = makeClaimHtlcDelayedOutputPenaltyTxs(htlcSuccessTxs(1).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) - val sig = claimHtlcDelayedPenaltyTx.sign(localRevocationPriv, TxOwner.Local, DefaultCommitmentFormat, Map.empty) - val signed = addSigs(claimHtlcDelayedPenaltyTx, sig) - assert(checkSpendable(signed).isSuccess) - // remote can't claim revoked output of htlc4's htlc-success tx because it is below the dust limit - val claimHtlcDelayedPenaltyTx1 = makeClaimHtlcDelayedOutputPenaltyTxs(htlcSuccessTxs(0).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) - assert(claimHtlcDelayedPenaltyTx1 == Seq(Left(AmountBelowDustLimit))) - } - { - // remote spends received HTLC output with revocation key - val script = Script.write(Scripts.htlcReceived(localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localRevocationPriv.publicKey, Crypto.ripemd160(htlc2.paymentHash), htlc2.cltvExpiry, DefaultCommitmentFormat)) - val Some(htlcOutputIndex) = outputs.zipWithIndex.find { - case (CommitmentOutputLink(_, _, InHtlc(IncomingHtlc(someHtlc))), _) => someHtlc.id == htlc2.id - case _ => false - }.map(_._2) - val Right(htlcPenaltyTx) = makeHtlcPenaltyTx(commitTx.tx, htlcOutputIndex, script, localDustLimit, finalPubKeyScript, feeratePerKw) - val sig = htlcPenaltyTx.sign(localRevocationPriv, TxOwner.Local, DefaultCommitmentFormat, Map.empty) - val signed = addSigs(htlcPenaltyTx, sig, localRevocationPriv.publicKey) - assert(checkSpendable(signed).isSuccess) + private def checkExpectedWeight(actual: Int, expected: Int, commitmentFormat: CommitmentFormat): Unit = { + commitmentFormat match { + case _: SimpleTaprootChannelCommitmentFormat => assert(actual == expected) + case _: AnchorOutputsCommitmentFormat => + // ECDSA signatures are der-encoded, which creates some variability in signature size compared to the baseline. + assert(actual <= expected + 2) + assert(actual >= expected - 2) } } @@ -445,71 +164,73 @@ class TransactionsSpec extends AnyFunSuite with Logging { val belowDustWithFeeAndAnchors = (localDustLimit + commitFeeAndAnchorCost * 0.9).toMilliSatoshi { - val outputs = makeCommitTxOutputs(localPaysCommitTxFees = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, spec, UnsafeLegacyAnchorOutputsCommitmentFormat) - assert(outputs.map(_.commitmentOutput).toSet == Set(CommitmentOutput.ToLocal, CommitmentOutput.ToRemote, CommitmentOutput.ToLocalAnchor, CommitmentOutput.ToRemoteAnchor)) - assert(outputs.find(_.commitmentOutput == CommitmentOutput.ToLocalAnchor).get.output.amount == anchorAmount) - assert(outputs.find(_.commitmentOutput == CommitmentOutput.ToRemoteAnchor).get.output.amount == anchorAmount) - assert(outputs.find(_.commitmentOutput == CommitmentOutput.ToLocal).get.output.amount.toMilliSatoshi == spec.toLocal - commitFeeAndAnchorCost) - assert(outputs.find(_.commitmentOutput == CommitmentOutput.ToRemote).get.output.amount.toMilliSatoshi == spec.toRemote) + val outputs = makeCommitTxOutputs(localFundingPriv.publicKey, remoteFundingPriv.publicKey, localKeys.publicKeys, payCommitTxFees = true, localDustLimit, toLocalDelay, spec, UnsafeLegacyAnchorOutputsCommitmentFormat) + assert(outputs.size == 4) + assert(outputs.find(_.isInstanceOf[CommitmentOutput.ToLocalAnchor]).get.txOut.amount == anchorAmount) + assert(outputs.find(_.isInstanceOf[CommitmentOutput.ToRemoteAnchor]).get.txOut.amount == anchorAmount) + assert(outputs.find(_.isInstanceOf[CommitmentOutput.ToLocal]).get.txOut.amount.toMilliSatoshi == spec.toLocal - commitFeeAndAnchorCost) + assert(outputs.find(_.isInstanceOf[CommitmentOutput.ToRemote]).get.txOut.amount.toMilliSatoshi == spec.toRemote) } { val toRemoteFundeeBelowDust = spec.copy(toRemote = belowDust) - val outputs = makeCommitTxOutputs(localPaysCommitTxFees = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, toRemoteFundeeBelowDust, UnsafeLegacyAnchorOutputsCommitmentFormat) - assert(outputs.map(_.commitmentOutput).toSet == Set(CommitmentOutput.ToLocal, CommitmentOutput.ToLocalAnchor)) - assert(outputs.find(_.commitmentOutput == CommitmentOutput.ToLocalAnchor).get.output.amount == anchorAmount) - assert(outputs.find(_.commitmentOutput == CommitmentOutput.ToLocal).get.output.amount.toMilliSatoshi == spec.toLocal - commitFeeAndAnchorCost) + val outputs = makeCommitTxOutputs(localFundingPriv.publicKey, remoteFundingPriv.publicKey, localKeys.publicKeys, payCommitTxFees = true, localDustLimit, toLocalDelay, toRemoteFundeeBelowDust, UnsafeLegacyAnchorOutputsCommitmentFormat) + assert(outputs.size == 2) + assert(outputs.find(_.isInstanceOf[CommitmentOutput.ToLocalAnchor]).get.txOut.amount == anchorAmount) + assert(outputs.find(_.isInstanceOf[CommitmentOutput.ToLocal]).get.txOut.amount.toMilliSatoshi == spec.toLocal - commitFeeAndAnchorCost) } { val toLocalFunderBelowDust = spec.copy(toLocal = belowDustWithFeeAndAnchors) - val outputs = makeCommitTxOutputs(localPaysCommitTxFees = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, toLocalFunderBelowDust, UnsafeLegacyAnchorOutputsCommitmentFormat) - assert(outputs.map(_.commitmentOutput).toSet == Set(CommitmentOutput.ToRemote, CommitmentOutput.ToRemoteAnchor)) - assert(outputs.find(_.commitmentOutput == CommitmentOutput.ToRemoteAnchor).get.output.amount == anchorAmount) - assert(outputs.find(_.commitmentOutput == CommitmentOutput.ToRemote).get.output.amount.toMilliSatoshi == spec.toRemote) + val outputs = makeCommitTxOutputs(localFundingPriv.publicKey, remoteFundingPriv.publicKey, localKeys.publicKeys, payCommitTxFees = true, localDustLimit, toLocalDelay, toLocalFunderBelowDust, UnsafeLegacyAnchorOutputsCommitmentFormat) + assert(outputs.size == 2) + assert(outputs.find(_.isInstanceOf[CommitmentOutput.ToRemoteAnchor]).get.txOut.amount == anchorAmount) + assert(outputs.find(_.isInstanceOf[CommitmentOutput.ToRemote]).get.txOut.amount.toMilliSatoshi == spec.toRemote) } { val toRemoteFunderBelowDust = spec.copy(toRemote = belowDustWithFeeAndAnchors) - val outputs = makeCommitTxOutputs(localPaysCommitTxFees = false, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, toRemoteFunderBelowDust, UnsafeLegacyAnchorOutputsCommitmentFormat) - assert(outputs.map(_.commitmentOutput).toSet == Set(CommitmentOutput.ToLocal, CommitmentOutput.ToLocalAnchor)) - assert(outputs.find(_.commitmentOutput == CommitmentOutput.ToLocalAnchor).get.output.amount == anchorAmount) - assert(outputs.find(_.commitmentOutput == CommitmentOutput.ToLocal).get.output.amount.toMilliSatoshi == spec.toLocal) + val outputs = makeCommitTxOutputs(localFundingPriv.publicKey, remoteFundingPriv.publicKey, localKeys.publicKeys, payCommitTxFees = false, localDustLimit, toLocalDelay, toRemoteFunderBelowDust, UnsafeLegacyAnchorOutputsCommitmentFormat) + assert(outputs.size == 2) + assert(outputs.find(_.isInstanceOf[CommitmentOutput.ToLocalAnchor]).get.txOut.amount == anchorAmount) + assert(outputs.find(_.isInstanceOf[CommitmentOutput.ToLocal]).get.txOut.amount.toMilliSatoshi == spec.toLocal) } { val toLocalFundeeBelowDust = spec.copy(toLocal = belowDust) - val outputs = makeCommitTxOutputs(localPaysCommitTxFees = false, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, toLocalFundeeBelowDust, UnsafeLegacyAnchorOutputsCommitmentFormat) - assert(outputs.map(_.commitmentOutput).toSet == Set(CommitmentOutput.ToRemote, CommitmentOutput.ToRemoteAnchor)) - assert(outputs.find(_.commitmentOutput == CommitmentOutput.ToRemoteAnchor).get.output.amount == anchorAmount) - assert(outputs.find(_.commitmentOutput == CommitmentOutput.ToRemote).get.output.amount.toMilliSatoshi == spec.toRemote - commitFeeAndAnchorCost) + val outputs = makeCommitTxOutputs(localFundingPriv.publicKey, remoteFundingPriv.publicKey, localKeys.publicKeys, payCommitTxFees = false, localDustLimit, toLocalDelay, toLocalFundeeBelowDust, UnsafeLegacyAnchorOutputsCommitmentFormat) + assert(outputs.size == 2) + assert(outputs.find(_.isInstanceOf[CommitmentOutput.ToRemoteAnchor]).get.txOut.amount == anchorAmount) + assert(outputs.find(_.isInstanceOf[CommitmentOutput.ToRemote]).get.txOut.amount.toMilliSatoshi == spec.toRemote - commitFeeAndAnchorCost) } { val allBelowDust = spec.copy(toLocal = belowDust, toRemote = belowDust) - val outputs = makeCommitTxOutputs(localPaysCommitTxFees = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, allBelowDust, UnsafeLegacyAnchorOutputsCommitmentFormat) + val outputs = makeCommitTxOutputs(localFundingPriv.publicKey, remoteFundingPriv.publicKey, localKeys.publicKeys, payCommitTxFees = true, localDustLimit, toLocalDelay, allBelowDust, UnsafeLegacyAnchorOutputsCommitmentFormat) assert(outputs.isEmpty) } } - test("generate valid commitment and htlc transactions (anchor outputs)") { + private def testCommitAndHtlcTxs(commitmentFormat: CommitmentFormat): Unit = { val walletPriv = randomKey() val walletPub = walletPriv.publicKey val finalPubKeyScript = Script.write(Script.pay2wpkh(walletPub)) - val commitInput = Funding.makeFundingInputInfo(randomTxId(), 0, Btc(1), localFundingPriv.publicKey, remoteFundingPriv.publicKey) + val fundingInfo = makeFundingScript(localFundingPriv.publicKey, remoteFundingPriv.publicKey, commitmentFormat) + val fundingTx = Transaction(version = 2, txIn = Nil, txOut = TxOut(Btc(1), fundingInfo.pubkeyScript) :: Nil, lockTime = 0) + val fundingTxOutpoint = OutPoint(fundingTx.txid, 0) + val commitInput = makeFundingInputInfo(fundingTxOutpoint.txid, fundingTxOutpoint.index.toInt, Btc(1), localFundingPriv.publicKey, remoteFundingPriv.publicKey, commitmentFormat) + + val paymentPreimages = Seq(randomBytes32(), randomBytes32(), randomBytes32(), randomBytes32(), randomBytes32(), randomBytes32(), randomBytes32(), randomBytes32()) + val paymentPreimageMap = paymentPreimages.map(p => sha256(p) -> p).toMap // htlc1, htlc2a and htlc2b are regular IN/OUT htlcs - val paymentPreimage1 = randomBytes32() - val htlc1 = UpdateAddHtlc(ByteVector32.Zeroes, 0, MilliBtc(100).toMilliSatoshi, sha256(paymentPreimage1), CltvExpiry(300), TestConstants.emptyOnionPacket, None, 1.0, None) - val paymentPreimage2 = randomBytes32() - val htlc2a = UpdateAddHtlc(ByteVector32.Zeroes, 1, MilliBtc(50).toMilliSatoshi, sha256(paymentPreimage2), CltvExpiry(310), TestConstants.emptyOnionPacket, None, 1.0, None) - val htlc2b = UpdateAddHtlc(ByteVector32.Zeroes, 2, MilliBtc(150).toMilliSatoshi, sha256(paymentPreimage2), CltvExpiry(310), TestConstants.emptyOnionPacket, None, 1.0, None) + val htlc1 = UpdateAddHtlc(ByteVector32.Zeroes, 0, MilliBtc(100).toMilliSatoshi, sha256(paymentPreimages(0)), CltvExpiry(300), TestConstants.emptyOnionPacket, None, Reputation.maxEndorsement, None) + val htlc2a = UpdateAddHtlc(ByteVector32.Zeroes, 1, MilliBtc(50).toMilliSatoshi, sha256(paymentPreimages(1)), CltvExpiry(310), TestConstants.emptyOnionPacket, None, Reputation.maxEndorsement, None) + val htlc2b = UpdateAddHtlc(ByteVector32.Zeroes, 2, MilliBtc(150).toMilliSatoshi, sha256(paymentPreimages(1)), CltvExpiry(310), TestConstants.emptyOnionPacket, None, Reputation.maxEndorsement, None) // htlc3 and htlc4 are dust IN/OUT htlcs, with an amount large enough to be included in the commit tx, but too small to be claimed at 2nd stage - val paymentPreimage3 = randomBytes32() - val htlc3 = UpdateAddHtlc(ByteVector32.Zeroes, 3, (localDustLimit + weight2fee(feeratePerKw, UnsafeLegacyAnchorOutputsCommitmentFormat.htlcTimeoutWeight)).toMilliSatoshi, sha256(paymentPreimage3), CltvExpiry(295), TestConstants.emptyOnionPacket, None, 1.0, None) - val paymentPreimage4 = randomBytes32() - val htlc4 = UpdateAddHtlc(ByteVector32.Zeroes, 4, (localDustLimit + weight2fee(feeratePerKw, UnsafeLegacyAnchorOutputsCommitmentFormat.htlcSuccessWeight)).toMilliSatoshi, sha256(paymentPreimage4), CltvExpiry(300), TestConstants.emptyOnionPacket, None, 1.0, None) + val htlc3 = UpdateAddHtlc(ByteVector32.Zeroes, 3, (localDustLimit + weight2fee(feeratePerKw, commitmentFormat.htlcTimeoutWeight)).toMilliSatoshi, sha256(paymentPreimages(2)), CltvExpiry(295), TestConstants.emptyOnionPacket, None, Reputation.maxEndorsement, None) + val htlc4 = UpdateAddHtlc(ByteVector32.Zeroes, 4, (localDustLimit + weight2fee(feeratePerKw, commitmentFormat.htlcSuccessWeight)).toMilliSatoshi, sha256(paymentPreimages(3)), CltvExpiry(300), TestConstants.emptyOnionPacket, None, Reputation.maxEndorsement, None) // htlc5 and htlc6 are dust IN/OUT htlcs - val htlc5 = UpdateAddHtlc(ByteVector32.Zeroes, 5, (localDustLimit * 0.9).toMilliSatoshi, sha256(randomBytes32()), CltvExpiry(295), TestConstants.emptyOnionPacket, None, 1.0, None) - val htlc6 = UpdateAddHtlc(ByteVector32.Zeroes, 6, (localDustLimit * 0.9).toMilliSatoshi, sha256(randomBytes32()), CltvExpiry(305), TestConstants.emptyOnionPacket, None, 1.0, None) + val htlc5 = UpdateAddHtlc(ByteVector32.Zeroes, 5, (localDustLimit * 0.9).toMilliSatoshi, sha256(paymentPreimages(4)), CltvExpiry(295), TestConstants.emptyOnionPacket, None, Reputation.maxEndorsement, None) + val htlc6 = UpdateAddHtlc(ByteVector32.Zeroes, 6, (localDustLimit * 0.9).toMilliSatoshi, sha256(paymentPreimages(5)), CltvExpiry(305), TestConstants.emptyOnionPacket, None, Reputation.maxEndorsement, None) // htlc7 and htlc8 are at the dust limit when we ignore 2nd-stage tx fees - val htlc7 = UpdateAddHtlc(ByteVector32.Zeroes, 7, localDustLimit.toMilliSatoshi, sha256(randomBytes32()), CltvExpiry(300), TestConstants.emptyOnionPacket, None, 1.0, None) - val htlc8 = UpdateAddHtlc(ByteVector32.Zeroes, 8, localDustLimit.toMilliSatoshi, sha256(randomBytes32()), CltvExpiry(302), TestConstants.emptyOnionPacket, None, 1.0, None) + val htlc7 = UpdateAddHtlc(ByteVector32.Zeroes, 7, localDustLimit.toMilliSatoshi, sha256(paymentPreimages(6)), CltvExpiry(300), TestConstants.emptyOnionPacket, None, Reputation.maxEndorsement, None) + val htlc8 = UpdateAddHtlc(ByteVector32.Zeroes, 8, localDustLimit.toMilliSatoshi, sha256(paymentPreimages(7)), CltvExpiry(302), TestConstants.emptyOnionPacket, None, Reputation.maxEndorsement, None) val spec = CommitmentSpec( htlcs = Set( OutgoingHtlc(htlc1), @@ -525,201 +246,196 @@ class TransactionsSpec extends AnyFunSuite with Logging { commitTxFeerate = feeratePerKw, toLocal = 400.millibtc.toMilliSatoshi, toRemote = 300.millibtc.toMilliSatoshi) + val localNonce = Musig2.generateNonce(randomBytes32(), Left(localFundingPriv), Seq(localFundingPriv.publicKey), None, None) + val remoteNonce = Musig2.generateNonce(randomBytes32(), Left(remoteFundingPriv), Seq(remoteFundingPriv.publicKey), None, None) + val publicNonces = Seq(localNonce, remoteNonce).map(_.publicNonce) val (commitTx, commitTxOutputs, htlcTimeoutTxs, htlcSuccessTxs) = { val commitTxNumber = 0x404142434445L - val outputs = makeCommitTxOutputs(localPaysCommitTxFees = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, spec, UnsafeLegacyAnchorOutputsCommitmentFormat) + val outputs = makeCommitTxOutputs(localFundingPriv.publicKey, remoteFundingPriv.publicKey, localKeys.publicKeys, payCommitTxFees = true, localDustLimit, toLocalDelay, spec, commitmentFormat) val txInfo = makeCommitTx(commitInput, commitTxNumber, localPaymentPriv.publicKey, remotePaymentPriv.publicKey, localIsChannelOpener = true, outputs) - val localSig = txInfo.sign(localPaymentPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat, Map.empty) - val remoteSig = txInfo.sign(remotePaymentPriv, TxOwner.Remote, UnsafeLegacyAnchorOutputsCommitmentFormat, Map.empty) - val commitTx = Transactions.addSigs(txInfo, localFundingPriv.publicKey, remoteFundingPriv.publicKey, localSig, remoteSig) - - val htlcTxs = makeHtlcTxs(commitTx.tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, spec.htlcTxFeerate(UnsafeLegacyAnchorOutputsCommitmentFormat), outputs, UnsafeLegacyAnchorOutputsCommitmentFormat) - assert(htlcTxs.length == 5) - val confirmationTargets = htlcTxs.map(tx => tx.htlcId -> tx.confirmationTarget.confirmBefore.toLong).toMap - assert(confirmationTargets == Map(0 -> 300, 1 -> 310, 2 -> 310, 3 -> 295, 4 -> 300)) - val htlcSuccessTxs = htlcTxs.collect { case tx: HtlcSuccessTx => tx } - val htlcTimeoutTxs = htlcTxs.collect { case tx: HtlcTimeoutTx => tx } - assert(htlcTimeoutTxs.size == 2) // htlc1 and htlc3 - assert(htlcTimeoutTxs.map(_.htlcId).toSet == Set(0, 3)) - assert(htlcSuccessTxs.size == 3) // htlc2a, htlc2b and htlc4 - assert(htlcSuccessTxs.map(_.htlcId).toSet == Set(1, 2, 4)) - - val zeroFeeOutputs = makeCommitTxOutputs(localPaysCommitTxFees = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, spec, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) - val zeroFeeCommitTx = makeCommitTx(commitInput, commitTxNumber, localPaymentPriv.publicKey, remotePaymentPriv.publicKey, localIsChannelOpener = true, zeroFeeOutputs) - val zeroFeeHtlcTxs = makeHtlcTxs(zeroFeeCommitTx.tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, spec.htlcTxFeerate(ZeroFeeHtlcTxAnchorOutputsCommitmentFormat), zeroFeeOutputs, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) - assert(zeroFeeHtlcTxs.length == 7) - val zeroFeeConfirmationTargets = zeroFeeHtlcTxs.map(tx => tx.htlcId -> tx.confirmationTarget.confirmBefore.toLong).toMap - assert(zeroFeeConfirmationTargets == Map(0 -> 300, 1 -> 310, 2 -> 310, 3 -> 295, 4 -> 300, 7 -> 300, 8 -> 302)) - val zeroFeeHtlcSuccessTxs = zeroFeeHtlcTxs.collect { case tx: HtlcSuccessTx => tx } - val zeroFeeHtlcTimeoutTxs = zeroFeeHtlcTxs.collect { case tx: HtlcTimeoutTx => tx } - zeroFeeHtlcSuccessTxs.foreach(tx => assert(tx.fee == 0.sat)) - zeroFeeHtlcTimeoutTxs.foreach(tx => assert(tx.fee == 0.sat)) - assert(zeroFeeHtlcTimeoutTxs.size == 3) // htlc1, htlc3 and htlc7 - assert(zeroFeeHtlcTimeoutTxs.map(_.htlcId).toSet == Set(0, 3, 7)) - assert(zeroFeeHtlcSuccessTxs.size == 4) // htlc2a, htlc2b, htlc4 and htlc8 - assert(zeroFeeHtlcSuccessTxs.map(_.htlcId).toSet == Set(1, 2, 4, 8)) - + val commitTx = commitmentFormat match { + case _: SimpleTaprootChannelCommitmentFormat => + val Right(commitTx) = for { + localPartialSig <- txInfo.partialSign(localFundingPriv, remoteFundingPriv.publicKey, localNonce, publicNonces) + remotePartialSig <- txInfo.partialSign(remoteFundingPriv, localFundingPriv.publicKey, remoteNonce, publicNonces) + _ = assert(txInfo.checkRemotePartialSignature(localFundingPriv.publicKey, remoteFundingPriv.publicKey, remotePartialSig, localNonce.publicNonce)) + invalidRemotePartialSig = ChannelSpendSignature.PartialSignatureWithNonce(randomBytes32(), remotePartialSig.nonce) + _ = assert(!txInfo.checkRemotePartialSignature(localFundingPriv.publicKey, remoteFundingPriv.publicKey, invalidRemotePartialSig, localNonce.publicNonce)) + tx <- txInfo.aggregateSigs(localFundingPriv.publicKey, remoteFundingPriv.publicKey, localPartialSig, remotePartialSig) + } yield tx + commitTx + case _: AnchorOutputsCommitmentFormat => + val localSig = txInfo.sign(localFundingPriv, remoteFundingPriv.publicKey) + val remoteSig = txInfo.sign(remoteFundingPriv, localFundingPriv.publicKey) + assert(txInfo.checkRemoteSig(localFundingPubkey = localFundingPriv.publicKey, remoteFundingPriv.publicKey, remoteSig)) + val invalidRemoteSig = ChannelSpendSignature.IndividualSignature(randomBytes64()) + assert(!txInfo.checkRemoteSig(localFundingPubkey = localFundingPriv.publicKey, remoteFundingPriv.publicKey, invalidRemoteSig)) + val commitTx = txInfo.aggregateSigs(localFundingPriv.publicKey, remoteFundingPriv.publicKey, localSig, remoteSig) + commitTx + } + commitTx.correctlySpends(Seq(fundingTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + // We check the expected weight of the commit input: + val commitInputWeight = commitTx.copy(txIn = Seq(commitTx.txIn.head, commitTx.txIn.head)).weight() - commitTx.weight() + checkExpectedWeight(commitInputWeight, commitmentFormat.fundingInputWeight, commitmentFormat) + val htlcTxs = makeHtlcTxs(commitTx, outputs, commitmentFormat) + val expiries = htlcTxs.map(tx => tx.htlcId -> tx.htlcExpiry.toLong).toMap + val htlcSuccessTxs = htlcTxs.collect { case tx: UnsignedHtlcSuccessTx => tx } + val htlcTimeoutTxs = htlcTxs.collect { case tx: UnsignedHtlcTimeoutTx => tx } + commitmentFormat match { + case ZeroFeeHtlcTxAnchorOutputsCommitmentFormat | ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat => + assert(htlcTxs.length == 7) + assert(expiries == Map(0 -> 300, 1 -> 310, 2 -> 310, 3 -> 295, 4 -> 300, 7 -> 300, 8 -> 302)) + assert(htlcTimeoutTxs.size == 3) // htlc1 and htlc3 and htlc7 + assert(htlcTimeoutTxs.map(_.htlcId).toSet == Set(0, 3, 7)) + assert(htlcSuccessTxs.size == 4) // htlc2a, htlc2b, htlc4 and htlc8 + assert(htlcSuccessTxs.map(_.htlcId).toSet == Set(1, 2, 4, 8)) + case _ => + assert(htlcTxs.length == 5) + assert(expiries == Map(0 -> 300, 1 -> 310, 2 -> 310, 3 -> 295, 4 -> 300)) + assert(htlcTimeoutTxs.size == 2) // htlc1 and htlc3 + assert(htlcTimeoutTxs.map(_.htlcId).toSet == Set(0, 3)) + assert(htlcSuccessTxs.size == 3) // htlc2a, htlc2b and htlc4 + assert(htlcSuccessTxs.map(_.htlcId).toSet == Set(1, 2, 4)) + } (commitTx, outputs, htlcTimeoutTxs, htlcSuccessTxs) } { // local spends main delayed output - val Right(claimMainOutputTx) = makeClaimLocalDelayedOutputTx(commitTx.tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) - val localSig = claimMainOutputTx.sign(localDelayedPaymentPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat, Map.empty) - val signedTx = addSigs(claimMainOutputTx, localSig) - assert(checkSpendable(signedTx).isSuccess) - } - { - // remote cannot spend main output with default commitment format - val Left(failure) = makeClaimP2WPKHOutputTx(commitTx.tx, localDustLimit, remotePaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) - assert(failure == OutputNotFound) + val Right(claimMainOutputTx) = ClaimLocalDelayedOutputTx.createUnsignedTx(localKeys, commitTx, localDustLimit, toLocalDelay, finalPubKeyScript, feeratePerKw, commitmentFormat).map(_.sign()) + checkExpectedWeight(claimMainOutputTx.weight(), commitmentFormat.toLocalDelayedWeight, commitmentFormat) + Transaction.correctlySpends(claimMainOutputTx, Seq(commitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) } { // remote spends main delayed output - val Right(claimRemoteDelayedOutputTx) = makeClaimRemoteDelayedOutputTx(commitTx.tx, localDustLimit, remotePaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) - val localSig = claimRemoteDelayedOutputTx.sign(remotePaymentPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat, Map.empty) - val signedTx = addSigs(claimRemoteDelayedOutputTx, localSig) - assert(checkSpendable(signedTx).isSuccess) + val Right(claimRemoteDelayedOutputTx) = ClaimRemoteDelayedOutputTx.createUnsignedTx(remoteKeys, commitTx, localDustLimit, finalPubKeyScript, feeratePerKw, commitmentFormat).map(_.sign()) + checkExpectedWeight(claimRemoteDelayedOutputTx.weight(), commitmentFormat.toRemoteWeight, commitmentFormat) + Transaction.correctlySpends(claimRemoteDelayedOutputTx, Seq(commitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) } { // local spends local anchor with additional wallet inputs val walletAmount = 50_000 sat - val walletInputs = Map( - OutPoint(randomTxId(), 3) -> TxOut(walletAmount, Script.pay2wpkh(walletPub)), - OutPoint(randomTxId(), 0) -> TxOut(walletAmount, Script.pay2wpkh(walletPub)), - ) - val Right(claimAnchorOutputTx) = makeClaimLocalAnchorOutputTx(commitTx.tx, localFundingPriv.publicKey, ConfirmationTarget.Absolute(BlockHeight(0))).map(anchorTx => { - val walletTxIn = walletInputs.map { case (outpoint, _) => TxIn(outpoint, ByteVector.empty, 0) } - val unsignedTx = anchorTx.tx.copy(txIn = anchorTx.tx.txIn ++ walletTxIn) - val sig1 = unsignedTx.signInput(1, Script.pay2pkh(walletPub), SIGHASH_ALL, walletAmount, SigVersion.SIGVERSION_WITNESS_V0, walletPriv) - val sig2 = unsignedTx.signInput(2, Script.pay2pkh(walletPub), SIGHASH_ALL, walletAmount, SigVersion.SIGVERSION_WITNESS_V0, walletPriv) - val walletSignedTx = unsignedTx + val walletInputs = WalletInputs(Seq( + WalletInput(TxIn(OutPoint(randomTxId(), 3), Nil, 0), TxOut(walletAmount, Script.pay2wpkh(walletPub))), + WalletInput(TxIn(OutPoint(randomTxId(), 0), Nil, 0), TxOut(walletAmount, Script.pay2wpkh(walletPub))), + ), changeOutput_opt = Some(TxOut(25_000 sat, Script.pay2wpkh(walletPub)))) + val Right(claimAnchorTx) = ClaimLocalAnchorTx.createUnsignedTx(localFundingPriv, localKeys, commitTx, commitmentFormat).map(anchorTx => { + val locallySignedTx = anchorTx.sign(walletInputs) + val sig1 = locallySignedTx.signInput(1, Script.pay2pkh(walletPub), SIGHASH_ALL, walletAmount, SigVersion.SIGVERSION_WITNESS_V0, walletPriv) + val sig2 = locallySignedTx.signInput(2, Script.pay2pkh(walletPub), SIGHASH_ALL, walletAmount, SigVersion.SIGVERSION_WITNESS_V0, walletPriv) + val signedTx = locallySignedTx .updateWitness(1, Script.witnessPay2wpkh(walletPub, sig1)) .updateWitness(2, Script.witnessPay2wpkh(walletPub, sig2)) - anchorTx.copy(tx = walletSignedTx) + anchorTx.copy(tx = signedTx) }) - val allInputs = walletInputs + (claimAnchorOutputTx.input.outPoint -> claimAnchorOutputTx.input.txOut) - assert(Try(Transaction.correctlySpends(claimAnchorOutputTx.tx, allInputs, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)).isFailure) - // All wallet inputs must be provided when signing. - assert(Try(claimAnchorOutputTx.sign(localFundingPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat, Map.empty)).isFailure) - assert(Try(claimAnchorOutputTx.sign(localFundingPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat, walletInputs.take(1))).isFailure) - val localSig = claimAnchorOutputTx.sign(localFundingPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat, walletInputs) - val signedTx = addSigs(claimAnchorOutputTx, localSig) - Transaction.correctlySpends(signedTx.tx, allInputs, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + val spentOutputs = walletInputs.spentUtxos + (claimAnchorTx.input.outPoint -> claimAnchorTx.input.txOut) + Transaction.correctlySpends(claimAnchorTx.tx, spentOutputs, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + val anchorInputWeight = claimAnchorTx.tx.weight() - claimAnchorTx.tx.copy(txIn = claimAnchorTx.tx.txIn.tail).weight() + checkExpectedWeight(anchorInputWeight, commitmentFormat.anchorInputWeight, commitmentFormat) + val anchorTxWeight = claimAnchorTx.tx.copy(txIn = claimAnchorTx.tx.txIn.take(1), txOut = Nil).weight() + checkExpectedWeight(anchorTxWeight, claimAnchorTx.expectedWeight, commitmentFormat) } { // remote spends remote anchor - val Right(claimAnchorOutputTx) = makeClaimLocalAnchorOutputTx(commitTx.tx, remoteFundingPriv.publicKey, ConfirmationTarget.Absolute(BlockHeight(0))) - assert(checkSpendable(claimAnchorOutputTx).isFailure) - val localSig = claimAnchorOutputTx.sign(remoteFundingPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat, Map.empty) - val signedTx = addSigs(claimAnchorOutputTx, localSig) - assert(checkSpendable(signedTx).isSuccess) + val Right(claimAnchorOutputTx) = ClaimRemoteAnchorTx.createUnsignedTx(remoteFundingPriv, remoteKeys, commitTx, commitmentFormat) + assert(!claimAnchorOutputTx.validate(Map.empty)) + val signedTx = claimAnchorOutputTx.sign() + Transaction.correctlySpends(signedTx, Seq(commitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) } { // remote spends local main delayed output with revocation key - val Right(mainPenaltyTx) = makeMainPenaltyTx(commitTx.tx, localDustLimit, localRevocationPriv.publicKey, finalPubKeyScript, toLocalDelay, localDelayedPaymentPriv.publicKey, feeratePerKw) - val sig = mainPenaltyTx.sign(localRevocationPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat, Map.empty) - val signed = addSigs(mainPenaltyTx, sig) - assert(checkSpendable(signed).isSuccess) + val Right(mainPenaltyTx) = MainPenaltyTx.createUnsignedTx(remoteKeys, localRevocationPriv, commitTx, localDustLimit, finalPubKeyScript, toLocalDelay, feeratePerKw, commitmentFormat).map(_.sign()) + checkExpectedWeight(mainPenaltyTx.weight(), commitmentFormat.mainPenaltyWeight, commitmentFormat) + Transaction.correctlySpends(mainPenaltyTx, Seq(commitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) } { // local spends received htlc with HTLC-timeout tx for (htlcTimeoutTx <- htlcTimeoutTxs) { - val localSig = htlcTimeoutTx.sign(localHtlcPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat, Map.empty) - val remoteSig = htlcTimeoutTx.sign(remoteHtlcPriv, TxOwner.Remote, UnsafeLegacyAnchorOutputsCommitmentFormat, Map.empty) - val signedTx = addSigs(htlcTimeoutTx, localSig, remoteSig, UnsafeLegacyAnchorOutputsCommitmentFormat) - assert(checkSpendable(signedTx).isSuccess) + val remoteSig = htlcTimeoutTx.localSig(remoteKeys) + assert(htlcTimeoutTx.checkRemoteSig(localKeys, remoteSig)) + val signedTx = htlcTimeoutTx.addRemoteSig(localKeys, remoteSig).sign() + Transaction.correctlySpends(signedTx, Seq(commitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) // local detects when remote doesn't use the right sighash flags val invalidSighash = Seq(SIGHASH_ALL, SIGHASH_ALL | SIGHASH_ANYONECANPAY, SIGHASH_SINGLE, SIGHASH_NONE) for (sighash <- invalidSighash) { - val invalidRemoteSig = htlcTimeoutTx.sign(remoteHtlcPriv, sighash, Map.empty) - val invalidTx = addSigs(htlcTimeoutTx, localSig, invalidRemoteSig, UnsafeLegacyAnchorOutputsCommitmentFormat) - assert(checkSpendable(invalidTx).isFailure) + val invalidRemoteSig = htlcTimeoutTx.localSigWithInvalidSighash(remoteKeys, sighash) + assert(!htlcTimeoutTx.checkRemoteSig(localKeys, invalidRemoteSig)) } } } { // local spends delayed output of htlc1 timeout tx - val Right(htlcDelayed) = makeHtlcDelayedTx(htlcTimeoutTxs(1).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) - val localSig = htlcDelayed.sign(localDelayedPaymentPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat, Map.empty) - val signedTx = addSigs(htlcDelayed, localSig) - assert(checkSpendable(signedTx).isSuccess) + val Right(htlcDelayed) = HtlcDelayedTx.createUnsignedTx(localKeys, htlcTimeoutTxs(1).tx, localDustLimit, toLocalDelay, finalPubKeyScript, feeratePerKw, commitmentFormat).map(_.sign()) + checkExpectedWeight(htlcDelayed.weight(), commitmentFormat.htlcDelayedWeight, commitmentFormat) + Transaction.correctlySpends(htlcDelayed, Seq(htlcTimeoutTxs(1).tx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) // local can't claim delayed output of htlc3 timeout tx because it is below the dust limit - val htlcDelayed1 = makeHtlcDelayedTx(htlcTimeoutTxs(0).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) - assert(htlcDelayed1 == Left(OutputNotFound)) + val htlcDelayed1 = HtlcDelayedTx.createUnsignedTx(localKeys, htlcTimeoutTxs(0).tx, localDustLimit, toLocalDelay, finalPubKeyScript, feeratePerKw, commitmentFormat) + assert(htlcDelayed1 == Left(AmountBelowDustLimit)) } { // local spends offered htlc with HTLC-success tx - for ((htlcSuccessTx, paymentPreimage) <- (htlcSuccessTxs(0), paymentPreimage4) :: (htlcSuccessTxs(1), paymentPreimage2) :: (htlcSuccessTxs(2), paymentPreimage2) :: Nil) { - val localSig = htlcSuccessTx.sign(localHtlcPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat, Map.empty) - val remoteSig = htlcSuccessTx.sign(remoteHtlcPriv, TxOwner.Remote, UnsafeLegacyAnchorOutputsCommitmentFormat, Map.empty) - val signedTx = addSigs(htlcSuccessTx, localSig, remoteSig, paymentPreimage, UnsafeLegacyAnchorOutputsCommitmentFormat) - assert(checkSpendable(signedTx).isSuccess) - // check remote sig - assert(htlcSuccessTx.checkSig(remoteSig, remoteHtlcPriv.publicKey, TxOwner.Remote, UnsafeLegacyAnchorOutputsCommitmentFormat)) + for (htlcSuccessTx <- htlcSuccessTxs(0) :: htlcSuccessTxs(1) :: htlcSuccessTxs(2) :: Nil) { + val preimage = paymentPreimageMap(htlcSuccessTx.paymentHash) + val remoteSig = htlcSuccessTx.localSig(remoteKeys) + val signedTx = htlcSuccessTx.addRemoteSig(localKeys, remoteSig, preimage).sign() + Transaction.correctlySpends(signedTx, Seq(commitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + assert(htlcSuccessTx.checkRemoteSig(localKeys, remoteSig)) // local detects when remote doesn't use the right sighash flags val invalidSighash = Seq(SIGHASH_ALL, SIGHASH_ALL | SIGHASH_ANYONECANPAY, SIGHASH_SINGLE, SIGHASH_NONE) for (sighash <- invalidSighash) { - val invalidRemoteSig = htlcSuccessTx.sign(remoteHtlcPriv, sighash, Map.empty) - val invalidTx = addSigs(htlcSuccessTx, localSig, invalidRemoteSig, paymentPreimage, UnsafeLegacyAnchorOutputsCommitmentFormat) - assert(checkSpendable(invalidTx).isFailure) - assert(!invalidTx.checkSig(invalidRemoteSig, remoteHtlcPriv.publicKey, TxOwner.Remote, UnsafeLegacyAnchorOutputsCommitmentFormat)) + val invalidRemoteSig = htlcSuccessTx.localSigWithInvalidSighash(remoteKeys, sighash) + assert(!htlcSuccessTx.checkRemoteSig(localKeys, invalidRemoteSig)) } } } { // local spends delayed output of htlc2a and htlc2b success txs - val Right(htlcDelayedA) = makeHtlcDelayedTx(htlcSuccessTxs(1).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) - val Right(htlcDelayedB) = makeHtlcDelayedTx(htlcSuccessTxs(2).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) - for (htlcDelayed <- Seq(htlcDelayedA, htlcDelayedB)) { - val localSig = htlcDelayed.sign(localDelayedPaymentPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat, Map.empty) - val signedTx = addSigs(htlcDelayed, localSig) - assert(checkSpendable(signedTx).isSuccess) - } + val Right(htlcDelayedA) = HtlcDelayedTx.createUnsignedTx(localKeys, htlcSuccessTxs(1).tx, localDustLimit, toLocalDelay, finalPubKeyScript, feeratePerKw, commitmentFormat).map(_.sign()) + val Right(htlcDelayedB) = HtlcDelayedTx.createUnsignedTx(localKeys, htlcSuccessTxs(2).tx, localDustLimit, toLocalDelay, finalPubKeyScript, feeratePerKw, commitmentFormat).map(_.sign()) + Seq(htlcDelayedA, htlcDelayedB).foreach(htlcDelayed => checkExpectedWeight(htlcDelayed.weight(), commitmentFormat.htlcDelayedWeight, commitmentFormat)) + Transaction.correctlySpends(htlcDelayedA, Seq(htlcSuccessTxs(1).tx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + Transaction.correctlySpends(htlcDelayedB, Seq(htlcSuccessTxs(2).tx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) // local can't claim delayed output of htlc4 success tx because it is below the dust limit - val claimHtlcDelayed1 = makeClaimLocalDelayedOutputTx(htlcSuccessTxs(0).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) - assert(claimHtlcDelayed1 == Left(AmountBelowDustLimit)) + val htlcDelayedC = HtlcDelayedTx.createUnsignedTx(localKeys, htlcSuccessTxs(0).tx, localDustLimit, toLocalDelay, finalPubKeyScript, feeratePerKw, commitmentFormat) + assert(htlcDelayedC == Left(AmountBelowDustLimit)) } { // remote spends local->remote htlc outputs directly in case of success - for ((htlc, paymentPreimage) <- (htlc1, paymentPreimage1) :: (htlc3, paymentPreimage3) :: Nil) { - val Right(claimHtlcSuccessTx) = makeClaimHtlcSuccessTx(commitTx.tx, commitTxOutputs, localDustLimit, remoteHtlcPriv.publicKey, localHtlcPriv.publicKey, localRevocationPriv.publicKey, finalPubKeyScript, htlc, feeratePerKw, UnsafeLegacyAnchorOutputsCommitmentFormat) - val localSig = claimHtlcSuccessTx.sign(remoteHtlcPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat, Map.empty) - val signed = addSigs(claimHtlcSuccessTx, localSig, paymentPreimage) - assert(checkSpendable(signed).isSuccess) + for (htlc <- htlc1 :: htlc3 :: Nil) { + val paymentPreimage = paymentPreimageMap(htlc.paymentHash) + val Right(claimHtlcSuccessTx) = ClaimHtlcSuccessTx.createUnsignedTx(remoteKeys, commitTx, localDustLimit, commitTxOutputs, finalPubKeyScript, htlc, paymentPreimage, feeratePerKw, commitmentFormat).map(_.sign()) + checkExpectedWeight(claimHtlcSuccessTx.weight(), commitmentFormat.claimHtlcSuccessWeight, commitmentFormat) + Transaction.correctlySpends(claimHtlcSuccessTx, Seq(commitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) } } { // remote spends htlc1's htlc-timeout tx with revocation key - val Seq(Right(claimHtlcDelayedPenaltyTx)) = makeClaimHtlcDelayedOutputPenaltyTxs(htlcTimeoutTxs(1).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) - val sig = claimHtlcDelayedPenaltyTx.sign(localRevocationPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat, Map.empty) - val signed = addSigs(claimHtlcDelayedPenaltyTx, sig) - assert(checkSpendable(signed).isSuccess) + val Seq(Right(claimHtlcDelayedPenaltyTx)) = ClaimHtlcDelayedOutputPenaltyTx.createUnsignedTxs(remoteKeys, localRevocationPriv, htlcTimeoutTxs(1).tx, localDustLimit, toLocalDelay, finalPubKeyScript, feeratePerKw, commitmentFormat) + val signedTx = claimHtlcDelayedPenaltyTx.sign() + checkExpectedWeight(signedTx.weight(), commitmentFormat.claimHtlcPenaltyWeight, commitmentFormat) + Transaction.correctlySpends(signedTx, Seq(htlcTimeoutTxs(1).tx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) // remote can't claim revoked output of htlc3's htlc-timeout tx because it is below the dust limit - val claimHtlcDelayedPenaltyTx1 = makeClaimHtlcDelayedOutputPenaltyTxs(htlcTimeoutTxs(0).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) + val claimHtlcDelayedPenaltyTx1 = ClaimHtlcDelayedOutputPenaltyTx.createUnsignedTxs(remoteKeys, localRevocationPriv, htlcTimeoutTxs(0).tx, localDustLimit, toLocalDelay, finalPubKeyScript, feeratePerKw, commitmentFormat) assert(claimHtlcDelayedPenaltyTx1 == Seq(Left(AmountBelowDustLimit))) } { // remote spends remote->local htlc output directly in case of timeout for (htlc <- Seq(htlc2a, htlc2b)) { - val Right(claimHtlcTimeoutTx) = makeClaimHtlcTimeoutTx(commitTx.tx, commitTxOutputs, localDustLimit, remoteHtlcPriv.publicKey, localHtlcPriv.publicKey, localRevocationPriv.publicKey, finalPubKeyScript, htlc, feeratePerKw, UnsafeLegacyAnchorOutputsCommitmentFormat) - val localSig = claimHtlcTimeoutTx.sign(remoteHtlcPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat, Map.empty) - val signed = addSigs(claimHtlcTimeoutTx, localSig) - assert(checkSpendable(signed).isSuccess) + val Right(claimHtlcTimeoutTx) = ClaimHtlcTimeoutTx.createUnsignedTx(remoteKeys, commitTx, localDustLimit, commitTxOutputs, finalPubKeyScript, htlc, feeratePerKw, commitmentFormat).map(_.sign()) + checkExpectedWeight(claimHtlcTimeoutTx.weight(), commitmentFormat.claimHtlcTimeoutWeight, commitmentFormat) + Transaction.correctlySpends(claimHtlcTimeoutTx, Seq(commitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) } } { // remote spends htlc2a/htlc2b's htlc-success tx with revocation key - val Seq(Right(claimHtlcDelayedPenaltyTxA)) = makeClaimHtlcDelayedOutputPenaltyTxs(htlcSuccessTxs(1).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) - val Seq(Right(claimHtlcDelayedPenaltyTxB)) = makeClaimHtlcDelayedOutputPenaltyTxs(htlcSuccessTxs(2).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) - for (claimHtlcSuccessPenaltyTx <- Seq(claimHtlcDelayedPenaltyTxA, claimHtlcDelayedPenaltyTxB)) { - val sig = claimHtlcSuccessPenaltyTx.sign(localRevocationPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat, Map.empty) - val signed = addSigs(claimHtlcSuccessPenaltyTx, sig) - assert(checkSpendable(signed).isSuccess) - } + val Seq(Right(claimHtlcDelayedPenaltyTxA)) = ClaimHtlcDelayedOutputPenaltyTx.createUnsignedTxs(remoteKeys, localRevocationPriv, htlcSuccessTxs(1).tx, localDustLimit, toLocalDelay, finalPubKeyScript, feeratePerKw, commitmentFormat) + val Seq(Right(claimHtlcDelayedPenaltyTxB)) = ClaimHtlcDelayedOutputPenaltyTx.createUnsignedTxs(remoteKeys, localRevocationPriv, htlcSuccessTxs(2).tx, localDustLimit, toLocalDelay, finalPubKeyScript, feeratePerKw, commitmentFormat) + Seq(claimHtlcDelayedPenaltyTxA, claimHtlcDelayedPenaltyTxB).foreach(claimHtlcSuccessPenaltyTx => checkExpectedWeight(claimHtlcSuccessPenaltyTx.sign().weight(), commitmentFormat.claimHtlcPenaltyWeight, commitmentFormat)) + Transaction.correctlySpends(claimHtlcDelayedPenaltyTxA.sign(), Seq(htlcSuccessTxs(1).tx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + Transaction.correctlySpends(claimHtlcDelayedPenaltyTxB.sign(), Seq(htlcSuccessTxs(2).tx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) // remote can't claim revoked output of htlc4's htlc-success tx because it is below the dust limit - val claimHtlcDelayedPenaltyTx1 = makeClaimHtlcDelayedOutputPenaltyTxs(htlcSuccessTxs(0).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) + val claimHtlcDelayedPenaltyTx1 = ClaimHtlcDelayedOutputPenaltyTx.createUnsignedTxs(remoteKeys, localRevocationPriv, htlcSuccessTxs(0).tx, localDustLimit, toLocalDelay, finalPubKeyScript, feeratePerKw, commitmentFormat) assert(claimHtlcDelayedPenaltyTx1 == Seq(Left(AmountBelowDustLimit))) } { @@ -727,283 +443,61 @@ class TransactionsSpec extends AnyFunSuite with Logging { val txIn = htlcTimeoutTxs.flatMap(_.tx.txIn) ++ htlcSuccessTxs.flatMap(_.tx.txIn) val txOut = htlcTimeoutTxs.flatMap(_.tx.txOut) ++ htlcSuccessTxs.flatMap(_.tx.txOut) val aggregatedHtlcTx = Transaction(2, txIn, txOut, 0) - val claimHtlcDelayedPenaltyTxs = makeClaimHtlcDelayedOutputPenaltyTxs(aggregatedHtlcTx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) - assert(claimHtlcDelayedPenaltyTxs.size == 5) + val claimHtlcDelayedPenaltyTxs = ClaimHtlcDelayedOutputPenaltyTx.createUnsignedTxs(remoteKeys, localRevocationPriv, aggregatedHtlcTx, localDustLimit, toLocalDelay, finalPubKeyScript, feeratePerKw, commitmentFormat) val skipped = claimHtlcDelayedPenaltyTxs.collect { case Left(reason) => reason } - assert(skipped.size == 2) - assert(skipped.toSet == Set(AmountBelowDustLimit)) val claimed = claimHtlcDelayedPenaltyTxs.collect { case Right(tx) => tx } - assert(claimed.size == 3) - assert(claimed.map(_.input.outPoint).toSet.size == 3) - } - { - // remote spends offered htlc output with revocation key - val script = Script.write(Scripts.htlcOffered(localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localRevocationPriv.publicKey, Crypto.ripemd160(htlc1.paymentHash), UnsafeLegacyAnchorOutputsCommitmentFormat)) - val Some(htlcOutputIndex) = commitTxOutputs.zipWithIndex.find { - case (CommitmentOutputLink(_, _, OutHtlc(OutgoingHtlc(someHtlc))), _) => someHtlc.id == htlc1.id - case _ => false - }.map(_._2) - val Right(htlcPenaltyTx) = makeHtlcPenaltyTx(commitTx.tx, htlcOutputIndex, script, localDustLimit, finalPubKeyScript, feeratePerKw) - val sig = htlcPenaltyTx.sign(localRevocationPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat, Map.empty) - val signed = addSigs(htlcPenaltyTx, sig, localRevocationPriv.publicKey) - assert(checkSpendable(signed).isSuccess) + commitmentFormat match { + case ZeroFeeHtlcTxAnchorOutputsCommitmentFormat | ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat => + assert(claimHtlcDelayedPenaltyTxs.size == 7) + assert(skipped.size == 2) + assert(skipped.toSet == Set(AmountBelowDustLimit)) + assert(claimed.size == 5) + assert(claimed.map(_.input.outPoint).toSet.size == 5) + case _ => + assert(claimHtlcDelayedPenaltyTxs.size == 5) + assert(skipped.size == 2) + assert(skipped.toSet == Set(AmountBelowDustLimit)) + assert(claimed.size == 3) + assert(claimed.map(_.input.outPoint).toSet.size == 3) + } + claimed.foreach { htlcPenaltyTx => + val signedTx = htlcPenaltyTx.sign() + checkExpectedWeight(signedTx.weight(), commitmentFormat.claimHtlcPenaltyWeight, commitmentFormat) + Transaction.correctlySpends(signedTx, Seq(aggregatedHtlcTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + } } { - // remote spends received htlc output with revocation key - for (htlc <- Seq(htlc2a, htlc2b)) { - val script = Script.write(Scripts.htlcReceived(localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localRevocationPriv.publicKey, Crypto.ripemd160(htlc.paymentHash), htlc.cltvExpiry, UnsafeLegacyAnchorOutputsCommitmentFormat)) - val Some(htlcOutputIndex) = commitTxOutputs.zipWithIndex.find { - case (CommitmentOutputLink(_, _, InHtlc(IncomingHtlc(someHtlc))), _) => someHtlc.id == htlc.id - case _ => false - }.map(_._2) - val Right(htlcPenaltyTx) = makeHtlcPenaltyTx(commitTx.tx, htlcOutputIndex, script, localDustLimit, finalPubKeyScript, feeratePerKw) - val sig = htlcPenaltyTx.sign(localRevocationPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat, Map.empty) - val signed = addSigs(htlcPenaltyTx, sig, localRevocationPriv.publicKey) - assert(checkSpendable(signed).isSuccess) + // remote spends htlc outputs with revocation key + val htlcs = spec.htlcs.map(_.add).map(add => (add.paymentHash, add.cltvExpiry)).toSeq + val htlcPenaltyTxs = HtlcPenaltyTx.createUnsignedTxs(remoteKeys, localRevocationPriv, commitTx, htlcs, localDustLimit, finalPubKeyScript, feeratePerKw, commitmentFormat) + assert(htlcPenaltyTxs.collect { case Right(htlcPenaltyTx) => htlcPenaltyTx.paymentHash }.toSet == Set(htlc1, htlc2a, htlc2b, htlc3, htlc4).map(_.paymentHash)) // the first 5 htlcs are above the dust limit + htlcPenaltyTxs.collect { case Right(htlcPenaltyTx) => htlcPenaltyTx }.foreach { htlcPenaltyTx => + val signedTx = htlcPenaltyTx.sign() + val expectedWeight = if (htlcTimeoutTxs.map(_.input.outPoint).toSet.contains(htlcPenaltyTx.input.outPoint)) { + commitmentFormat.htlcOfferedPenaltyWeight + } else { + commitmentFormat.htlcReceivedPenaltyWeight + } + checkExpectedWeight(signedTx.weight(), expectedWeight, commitmentFormat) + Transaction.correctlySpends(signedTx, Seq(commitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) } } } - test("generate valid commitment and htlc transactions (taproot)") { - import fr.acinq.bitcoin.scalacompat.KotlinUtils._ - import fr.acinq.eclair.transactions.Scripts.Taproot - - // funding tx sends to musig2 aggregate of local and remote funding keys - val fundingTxOutpoint = OutPoint(randomTxId(), 0) - val fundingOutput = TxOut(Btc(1), Script.pay2tr(Taproot.musig2Aggregate(localFundingPriv.publicKey, remoteFundingPriv.publicKey), None)) - - // offered HTLC - val preimage = ByteVector32.fromValidHex("01" * 32) - val paymentHash = Crypto.sha256(preimage) - - val txNumber = 0x404142434445L - val (sequence, lockTime) = encodeTxNumber(txNumber) - val commitTx = { - val tx = Transaction( - version = 2, - txIn = TxIn(fundingTxOutpoint, Nil, sequence) :: Nil, - txOut = Seq( - TxOut(300.millibtc, Taproot.toLocal(localDelayedPaymentPriv.publicKey, toLocalDelay, localRevocationPriv.publicKey)), - TxOut(400.millibtc, Taproot.toRemote(remotePaymentPriv.publicKey)), - TxOut(330.sat, Taproot.anchor(localDelayedPaymentPriv.publicKey)), - TxOut(330.sat, Taproot.anchor(remotePaymentPriv.publicKey)), - TxOut(25_000.sat, Taproot.offeredHtlc(localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, paymentHash, localRevocationPriv.publicKey)), - TxOut(15_000.sat, Taproot.receivedHtlc(localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, paymentHash, CltvExpiry(300), localRevocationPriv.publicKey)) - ), - lockTime - ) - - val (secretLocalNonce, publicLocalNonce) = Musig2.generateNonce(randomBytes32(), localFundingPriv, Seq(localFundingPriv.publicKey)) - val (secretRemoteNonce, publicRemoteNonce) = Musig2.generateNonce(randomBytes32(), remoteFundingPriv, Seq(remoteFundingPriv.publicKey)) - val publicKeys = Scripts.sort(Seq(localFundingPriv.publicKey, remoteFundingPriv.publicKey)) - val publicNonces = Seq(publicLocalNonce, publicRemoteNonce) - val Right(sig) = for { - localPartialSig <- Musig2.signTaprootInput(localFundingPriv, tx, 0, Seq(fundingOutput), publicKeys, secretLocalNonce, publicNonces, None) - remotePartialSig <- Musig2.signTaprootInput(remoteFundingPriv, tx, 0, Seq(fundingOutput), publicKeys, secretRemoteNonce, publicNonces, None) - sig <- Musig2.aggregateTaprootSignatures(Seq(localPartialSig, remotePartialSig), tx, 0, Seq(fundingOutput), publicKeys, publicNonces, None) - } yield sig - - tx.updateWitness(0, Script.witnessKeyPathPay2tr(sig)) - } - Transaction.correctlySpends(commitTx, Map(fundingTxOutpoint -> fundingOutput), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - - val finalPubKeyScript = Script.write(Script.pay2wpkh(PrivateKey(randomBytes32()).publicKey)) - - val spendToLocalOutputTx = { - val tx = Transaction( - version = 2, - txIn = TxIn(OutPoint(commitTx, 0), Seq(), sequence = toLocalDelay.toInt) :: Nil, - txOut = TxOut(300.millibtc, finalPubKeyScript) :: Nil, - lockTime = 0) - val scriptTree = Taproot.toLocalScriptTree(localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey) - val sig = Transaction.signInputTaprootScriptPath(localDelayedPaymentPriv, tx, 0, Seq(commitTx.txOut(0)), SigHash.SIGHASH_DEFAULT, scriptTree.getLeft.hash()) - val witness = Script.witnessScriptPathPay2tr(Taproot.NUMS_POINT.xOnly, scriptTree.getLeft.asInstanceOf[ScriptTree.Leaf], ScriptWitness(Seq(sig)), scriptTree) - tx.updateWitness(0, witness) - } - Transaction.correctlySpends(spendToLocalOutputTx, Seq(commitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - - val mainPenaltyTx = { - val tx = Transaction( - version = 2, - txIn = TxIn(OutPoint(commitTx, 0), Nil, sequence = TxIn.SEQUENCE_FINAL) :: Nil, - txOut = TxOut(300.millibtc, finalPubKeyScript) :: Nil, - lockTime = 0) - val scriptTree = Taproot.toLocalScriptTree(localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey) - val sig = Transaction.signInputTaprootScriptPath(localRevocationPriv, tx, 0, Seq(commitTx.txOut(0)), SigHash.SIGHASH_DEFAULT, scriptTree.getRight.hash()) - val witness = Script.witnessScriptPathPay2tr(XonlyPublicKey(Taproot.NUMS_POINT), scriptTree.getRight.asInstanceOf[ScriptTree.Leaf], ScriptWitness(Seq(sig)), scriptTree) - tx.updateWitness(0, witness) - } - Transaction.correctlySpends(mainPenaltyTx, Seq(commitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - - val spendToRemoteOutputTx = { - val tx = Transaction( - version = 2, - txIn = TxIn(OutPoint(commitTx, 1), Nil, sequence = 1) :: Nil, - txOut = TxOut(400.millibtc, finalPubKeyScript) :: Nil, - lockTime = 0) - val scriptTree = Taproot.toRemoteScriptTree(remotePaymentPriv.publicKey) - val sig = Transaction.signInputTaprootScriptPath(remotePaymentPriv, tx, 0, Seq(commitTx.txOut(1)), SigHash.SIGHASH_DEFAULT, scriptTree.hash()) - val witness = Script.witnessScriptPathPay2tr(Taproot.NUMS_POINT.xOnly, scriptTree, ScriptWitness(Seq(sig)), scriptTree) - tx.updateWitness(0, witness) - } - Transaction.correctlySpends(spendToRemoteOutputTx, Seq(commitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - - val spendLocalAnchorTx = { - val tx = Transaction( - version = 2, - txIn = TxIn(OutPoint(commitTx, 2), Nil, sequence = TxIn.SEQUENCE_FINAL) :: Nil, - txOut = TxOut(330.sat, finalPubKeyScript) :: Nil, - lockTime = 0) - val sig = Transaction.signInputTaprootKeyPath(localDelayedPaymentPriv, tx, 0, Seq(commitTx.txOut(2)), SigHash.SIGHASH_DEFAULT, Some(Scripts.Taproot.anchorScriptTree)) - val witness = Script.witnessKeyPathPay2tr(sig) - tx.updateWitness(0, witness) - } - Transaction.correctlySpends(spendLocalAnchorTx, Seq(commitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - - val spendLocalAnchorAfterDelayTx = { - val tx = Transaction( - version = 2, - txIn = TxIn(OutPoint(commitTx, 2), Nil, sequence = 16) :: Nil, - txOut = TxOut(330.sat, finalPubKeyScript) :: Nil, - lockTime = 0) - // after 16 blocks, anchor outputs can be spent without a signature BUT spenders still need to know the local/remote payment public key - val witness = Script.witnessScriptPathPay2tr(localDelayedPaymentPriv.xOnlyPublicKey(), Scripts.Taproot.anchorScriptTree, ScriptWitness.empty, Scripts.Taproot.anchorScriptTree) - tx.updateWitness(0, witness) - } - Transaction.correctlySpends(spendLocalAnchorAfterDelayTx, Seq(commitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - - val spendRemoteAnchorTx = { - val tx = Transaction( - version = 2, - txIn = TxIn(OutPoint(commitTx, 3), Nil, sequence = TxIn.SEQUENCE_FINAL) :: Nil, - txOut = TxOut(330.sat, finalPubKeyScript) :: Nil, - lockTime = 0) - val sig = Transaction.signInputTaprootKeyPath(remotePaymentPriv, tx, 0, Seq(commitTx.txOut(3)), SigHash.SIGHASH_DEFAULT, Some(Scripts.Taproot.anchorScriptTree)) - val witness = Script.witnessKeyPathPay2tr(sig) - tx.updateWitness(0, witness) - } - Transaction.correctlySpends(spendRemoteAnchorTx, Seq(commitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - - val spendRemoteAnchorAfterDelayTx = { - val tx = Transaction( - version = 2, - txIn = TxIn(OutPoint(commitTx, 3), Nil, sequence = 16) :: Nil, - txOut = TxOut(330.sat, finalPubKeyScript) :: Nil, - lockTime = 0) - val witness = Script.witnessScriptPathPay2tr(remotePaymentPriv.xOnlyPublicKey(), Scripts.Taproot.anchorScriptTree, ScriptWitness.empty, Scripts.Taproot.anchorScriptTree) - tx.updateWitness(0, witness) - } - Transaction.correctlySpends(spendRemoteAnchorAfterDelayTx, Seq(commitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - - // Spend offered HTLC with HTLC-Timeout tx. - val htlcTimeoutTx = { - val tx = Transaction( - version = 2, - txIn = TxIn(OutPoint(commitTx, 4), Nil, sequence = TxIn.SEQUENCE_FINAL) :: Nil, - txOut = TxOut(25_000.sat, Taproot.htlcDelayed(localDelayedPaymentPriv.publicKey, toLocalDelay, localRevocationPriv.publicKey)) :: Nil, - lockTime = 300) - val scriptTree = Taproot.offeredHtlcScriptTree(localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, paymentHash) - val sigHash = SigHash.SIGHASH_SINGLE | SigHash.SIGHASH_ANYONECANPAY - val localSig = Taproot.encodeSig(Transaction.signInputTaprootScriptPath(localHtlcPriv, tx, 0, Seq(commitTx.txOut(4)), sigHash, scriptTree.getLeft.hash()), sigHash) - val remoteSig = Taproot.encodeSig(Transaction.signInputTaprootScriptPath(remoteHtlcPriv, tx, 0, Seq(commitTx.txOut(4)), sigHash, scriptTree.getLeft.hash()), sigHash) - val witness = Script.witnessScriptPathPay2tr(localRevocationPriv.xOnlyPublicKey(), scriptTree.getLeft.asInstanceOf[ScriptTree.Leaf], ScriptWitness(Seq(remoteSig, localSig)), scriptTree) - tx.updateWitness(0, witness) - } - Transaction.correctlySpends(htlcTimeoutTx, Seq(commitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - - val offeredHtlcPenaltyTx = { - val tx = Transaction( - version = 2, - txIn = TxIn(OutPoint(commitTx, 4), Nil, sequence = TxIn.SEQUENCE_FINAL) :: Nil, - txOut = TxOut(25_000.sat, finalPubKeyScript) :: Nil, - lockTime = 0) - val scriptTree = Taproot.offeredHtlcScriptTree(localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, paymentHash) - val sig = Transaction.signInputTaprootKeyPath(localRevocationPriv, tx, 0, Seq(commitTx.txOut(4)), SigHash.SIGHASH_DEFAULT, Some(scriptTree)) - val witness = Script.witnessKeyPathPay2tr(sig) - tx.updateWitness(0, witness) - } - Transaction.correctlySpends(offeredHtlcPenaltyTx, Seq(commitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - - val spendHtlcTimeoutTx = { - val tx = Transaction( - version = 2, - txIn = TxIn(OutPoint(htlcTimeoutTx, 0), Nil, sequence = toLocalDelay.toInt) :: Nil, - txOut = TxOut(25_000.sat, finalPubKeyScript) :: Nil, - lockTime = 0) - val scriptTree = Taproot.htlcDelayedScriptTree(localDelayedPaymentPriv.publicKey, toLocalDelay) - val localSig = Transaction.signInputTaprootScriptPath(localDelayedPaymentPriv, tx, 0, Seq(htlcTimeoutTx.txOut(0)), SigHash.SIGHASH_DEFAULT, scriptTree.hash()) - val witness = Script.witnessScriptPathPay2tr(localRevocationPriv.xOnlyPublicKey(), scriptTree, ScriptWitness(Seq(localSig)), scriptTree) - tx.updateWitness(0, witness) - } - Transaction.correctlySpends(spendHtlcTimeoutTx, Seq(htlcTimeoutTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - - val htlcTimeoutPenaltyTx = { - val tx = Transaction( - version = 2, - txIn = TxIn(OutPoint(htlcTimeoutTx, 0), Nil, sequence = TxIn.SEQUENCE_FINAL) :: Nil, - txOut = TxOut(25_000.sat, finalPubKeyScript) :: Nil, - lockTime = 0) - val scriptTree = Taproot.htlcDelayedScriptTree(localDelayedPaymentPriv.publicKey, toLocalDelay) - val sig = Transaction.signInputTaprootKeyPath(localRevocationPriv, tx, 0, Seq(htlcTimeoutTx.txOut(0)), SigHash.SIGHASH_DEFAULT, Some(scriptTree)) - val witness = Script.witnessKeyPathPay2tr(sig) - tx.updateWitness(0, witness) - } - Transaction.correctlySpends(htlcTimeoutPenaltyTx, Seq(htlcTimeoutTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - - // Spend received HTLC with HTLC-Success tx. - val htlcSuccessTx = { - val tx = Transaction( - version = 2, - txIn = TxIn(OutPoint(commitTx, 5), Nil, sequence = 1) :: Nil, - txOut = TxOut(15_000.sat, Taproot.htlcDelayed(localDelayedPaymentPriv.publicKey, toLocalDelay, localRevocationPriv.publicKey)) :: Nil, - lockTime = 0) - val scriptTree = Taproot.receivedHtlcScriptTree(localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, paymentHash, CltvExpiry(300)) - val sigHash = SigHash.SIGHASH_SINGLE | SigHash.SIGHASH_ANYONECANPAY - val localSig = Taproot.encodeSig(Transaction.signInputTaprootScriptPath(localHtlcPriv, tx, 0, Seq(commitTx.txOut(5)), sigHash, scriptTree.getRight.hash()), sigHash) - val remoteSig = Taproot.encodeSig(Transaction.signInputTaprootScriptPath(remoteHtlcPriv, tx, 0, Seq(commitTx.txOut(5)), sigHash, scriptTree.getRight.hash()), sigHash) - val witness = Script.witnessScriptPathPay2tr(localRevocationPriv.xOnlyPublicKey(), scriptTree.getRight.asInstanceOf[ScriptTree.Leaf], ScriptWitness(Seq(remoteSig, localSig, preimage)), scriptTree) - tx.updateWitness(0, witness) - } - Transaction.correctlySpends(htlcSuccessTx, Seq(commitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + test("generate valid commitment and htlc transactions (legacy anchor outputs)") { + testCommitAndHtlcTxs(UnsafeLegacyAnchorOutputsCommitmentFormat) + } - val receivedHtlcPenaltyTx = { - val tx = Transaction( - version = 2, - txIn = TxIn(OutPoint(commitTx, 5), Nil, sequence = 1) :: Nil, - txOut = TxOut(15_000.sat, finalPubKeyScript) :: Nil, - lockTime = 0) - val scriptTree = Taproot.receivedHtlcScriptTree(localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, paymentHash, CltvExpiry(300)) - val sig = Transaction.signInputTaprootKeyPath(localRevocationPriv, tx, 0, Seq(commitTx.txOut(5)), SigHash.SIGHASH_DEFAULT, Some(scriptTree)) - val witness = Script.witnessKeyPathPay2tr(sig) - tx.updateWitness(0, witness) - } - Transaction.correctlySpends(receivedHtlcPenaltyTx, Seq(commitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + test("generate valid commitment and htlc transactions (zero fee anchor outputs)") { + testCommitAndHtlcTxs(ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) + } - val spendHtlcSuccessTx = { - val tx = Transaction( - version = 2, - txIn = TxIn(OutPoint(htlcSuccessTx, 0), Nil, sequence = toLocalDelay.toInt) :: Nil, - txOut = TxOut(15_000.sat, finalPubKeyScript) :: Nil, - lockTime = 0) - val scriptTree = Taproot.htlcDelayedScriptTree(localDelayedPaymentPriv.publicKey, toLocalDelay) - val localSig = Transaction.signInputTaprootScriptPath(localDelayedPaymentPriv, tx, 0, Seq(htlcSuccessTx.txOut(0)), SigHash.SIGHASH_DEFAULT, scriptTree.hash()) - val witness = Script.witnessScriptPathPay2tr(localRevocationPriv.xOnlyPublicKey(), scriptTree, ScriptWitness(Seq(localSig)), scriptTree) - tx.updateWitness(0, witness) - } - Transaction.correctlySpends(spendHtlcSuccessTx, Seq(htlcSuccessTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + test("generate valid commitment and htlc transactions (simple taproot channels)") { + testCommitAndHtlcTxs(ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat) + } - val htlcSuccessPenaltyTx = { - val tx = Transaction( - version = 2, - txIn = TxIn(OutPoint(htlcSuccessTx, 0), Nil, sequence = TxIn.SEQUENCE_FINAL) :: Nil, - txOut = TxOut(15_000.sat, finalPubKeyScript) :: Nil, - lockTime = 0) - val scriptTree = Taproot.htlcDelayedScriptTree(localDelayedPaymentPriv.publicKey, toLocalDelay) - val sig = Transaction.signInputTaprootKeyPath(localRevocationPriv, tx, 0, Seq(htlcSuccessTx.txOut(0)), SigHash.SIGHASH_DEFAULT, Some(scriptTree)) - val witness = Script.witnessKeyPathPay2tr(sig) - tx.updateWitness(0, witness) - } - Transaction.correctlySpends(htlcSuccessPenaltyTx, Seq(htlcSuccessTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + test("generate valid commitment and htlc transactions (phoenix simple taproot channels)") { + testCommitAndHtlcTxs(PhoenixSimpleTaprootChannelCommitmentFormat) } test("generate taproot NUMS point") { @@ -1023,89 +517,21 @@ class TransactionsSpec extends AnyFunSuite with Logging { assert(Taproot.musig2Aggregate(pubkey2, pubkey1) == Musig2.aggregateKeys(Seq(pubkey1, pubkey2))) } - test("sort the htlc outputs using BIP69 and cltv expiry") { - val localFundingPriv = PrivateKey(hex"a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1") - val remoteFundingPriv = PrivateKey(hex"a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2") - val localRevocationPriv = PrivateKey(hex"a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3") - val localPaymentPriv = PrivateKey(hex"a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4") - val localDelayedPaymentPriv = PrivateKey(hex"a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5") - val remotePaymentPriv = PrivateKey(hex"a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6") - val localHtlcPriv = PrivateKey(hex"a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7") - val remoteHtlcPriv = PrivateKey(hex"a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8") - val commitInput = Funding.makeFundingInputInfo(TxId.fromValidHex("a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0"), 0, Btc(1), localFundingPriv.publicKey, remoteFundingPriv.publicKey) - - // htlc1 and htlc2 are two regular incoming HTLCs with different amounts. - // htlc2 and htlc3 have the same amounts and should be sorted according to their scriptPubKey - // htlc4 is identical to htlc3 and htlc5 has same payment_hash/amount but different CLTV - val paymentPreimage1 = ByteVector32(hex"1111111111111111111111111111111111111111111111111111111111111111") - val paymentPreimage2 = ByteVector32(hex"2222222222222222222222222222222222222222222222222222222222222222") - val paymentPreimage3 = ByteVector32(hex"3333333333333333333333333333333333333333333333333333333333333333") - val htlc1 = UpdateAddHtlc(randomBytes32(), 1, millibtc2satoshi(MilliBtc(100)).toMilliSatoshi, sha256(paymentPreimage1), CltvExpiry(300), TestConstants.emptyOnionPacket, None, 1.0, None) - val htlc2 = UpdateAddHtlc(randomBytes32(), 2, millibtc2satoshi(MilliBtc(200)).toMilliSatoshi, sha256(paymentPreimage2), CltvExpiry(300), TestConstants.emptyOnionPacket, None, 1.0, None) - val htlc3 = UpdateAddHtlc(randomBytes32(), 3, millibtc2satoshi(MilliBtc(200)).toMilliSatoshi, sha256(paymentPreimage3), CltvExpiry(300), TestConstants.emptyOnionPacket, None, 1.0, None) - val htlc4 = UpdateAddHtlc(randomBytes32(), 4, millibtc2satoshi(MilliBtc(200)).toMilliSatoshi, sha256(paymentPreimage3), CltvExpiry(300), TestConstants.emptyOnionPacket, None, 1.0, None) - val htlc5 = UpdateAddHtlc(randomBytes32(), 5, millibtc2satoshi(MilliBtc(200)).toMilliSatoshi, sha256(paymentPreimage3), CltvExpiry(301), TestConstants.emptyOnionPacket, None, 1.0, None) - - val spec = CommitmentSpec( - htlcs = Set( - OutgoingHtlc(htlc1), - OutgoingHtlc(htlc2), - OutgoingHtlc(htlc3), - OutgoingHtlc(htlc4), - OutgoingHtlc(htlc5) - ), - commitTxFeerate = feeratePerKw, - toLocal = millibtc2satoshi(MilliBtc(400)).toMilliSatoshi, - toRemote = millibtc2satoshi(MilliBtc(300)).toMilliSatoshi) - - val commitTxNumber = 0x404142434446L - val (commitTx, outputs, htlcTxs) = { - val outputs = makeCommitTxOutputs(localPaysCommitTxFees = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, spec, DefaultCommitmentFormat) - val txInfo = makeCommitTx(commitInput, commitTxNumber, localPaymentPriv.publicKey, remotePaymentPriv.publicKey, localIsChannelOpener = true, outputs) - val localSig = txInfo.sign(localPaymentPriv, TxOwner.Local, DefaultCommitmentFormat, Map.empty) - val remoteSig = txInfo.sign(remotePaymentPriv, TxOwner.Remote, DefaultCommitmentFormat, Map.empty) - val commitTx = Transactions.addSigs(txInfo, localFundingPriv.publicKey, remoteFundingPriv.publicKey, localSig, remoteSig) - val htlcTxs = makeHtlcTxs(commitTx.tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, feeratePerKw, outputs, DefaultCommitmentFormat) - (commitTx, outputs, htlcTxs) - } - - // htlc1 comes before htlc2 because of the smaller amount (BIP69) - // htlc2 and htlc3 have the same amount but htlc2 comes first because its pubKeyScript is lexicographically smaller than htlc3's - // htlc5 comes after htlc3 and htlc4 because of the higher CLTV - val htlcOut1 :: htlcOut2 :: htlcOut3 :: htlcOut4 :: htlcOut5 :: _ = commitTx.tx.txOut.toList - assert(htlcOut1.amount == 10000000.sat) - for (htlcOut <- Seq(htlcOut2, htlcOut3, htlcOut4, htlcOut5)) { - assert(htlcOut.amount == 20000000.sat) - } - - // htlc3 and htlc4 are completely identical, their relative order can't be enforced. - assert(htlcTxs.length == 5) - htlcTxs.foreach(tx => assert(tx.isInstanceOf[HtlcTimeoutTx])) - val htlcIds = htlcTxs.sortBy(_.input.outPoint.index).map(_.htlcId) - assert(htlcIds == Seq(1, 2, 3, 4, 5) || htlcIds == Seq(1, 2, 4, 3, 5)) - - assert(htlcOut2.publicKeyScript.toHex < htlcOut3.publicKeyScript.toHex) - assert(outputs.find(_.commitmentOutput == OutHtlc(OutgoingHtlc(htlc2))).map(_.output.publicKeyScript).contains(htlcOut2.publicKeyScript)) - assert(outputs.find(_.commitmentOutput == OutHtlc(OutgoingHtlc(htlc3))).map(_.output.publicKeyScript).contains(htlcOut3.publicKeyScript)) - assert(outputs.find(_.commitmentOutput == OutHtlc(OutgoingHtlc(htlc4))).map(_.output.publicKeyScript).contains(htlcOut4.publicKeyScript)) - assert(outputs.find(_.commitmentOutput == OutHtlc(OutgoingHtlc(htlc5))).map(_.output.publicKeyScript).contains(htlcOut5.publicKeyScript)) - } - test("find our output in closing tx") { - val commitInput = Funding.makeFundingInputInfo(randomTxId(), 0, Btc(1), localFundingPriv.publicKey, remoteFundingPriv.publicKey) + val commitInput = makeFundingInputInfo(randomTxId(), 0, Btc(1), localFundingPriv.publicKey, remoteFundingPriv.publicKey, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) val localPubKeyScript = Script.write(Script.pay2wpkh(PrivateKey(randomBytes32()).publicKey)) val remotePubKeyScript = Script.write(Script.pay2wpkh(PrivateKey(randomBytes32()).publicKey)) { // Different amounts, both outputs untrimmed, local is funder: val spec = CommitmentSpec(Set.empty, feeratePerKw, 150_000_000 msat, 250_000_000 msat) - val closingTx = makeClosingTx(commitInput, localPubKeyScript, remotePubKeyScript, localPaysClosingFees = true, localDustLimit, 1000 sat, spec) + val closingTx = ClosingTx.createUnsignedTx(commitInput, localPubKeyScript, remotePubKeyScript, localPaysClosingFees = true, localDustLimit, 1000 sat, spec) assert(closingTx.tx.txOut.length == 2) - assert(closingTx.toLocalOutput !== None) - val toLocal = closingTx.toLocalOutput.get + assert(closingTx.toLocalOutput_opt.nonEmpty) + val toLocal = closingTx.toLocalOutput_opt.get assert(toLocal.publicKeyScript == localPubKeyScript) assert(toLocal.amount == 149_000.sat) // funder pays the fee - val toRemoteIndex = (toLocal.index + 1) % 2 + val toRemoteIndex = (closingTx.toLocalOutputIndex_opt.get + 1) % 2 assert(closingTx.tx.txOut(toRemoteIndex.toInt).amount == 250_000.sat) } { @@ -1115,10 +541,10 @@ class TransactionsSpec extends AnyFunSuite with Logging { assert(closingTxs.localAndRemote_opt.nonEmpty) assert(closingTxs.localOnly_opt.nonEmpty) assert(closingTxs.remoteOnly_opt.isEmpty) - val localAndRemote = closingTxs.localAndRemote_opt.flatMap(_.toLocalOutput).get + val localAndRemote = closingTxs.localAndRemote_opt.flatMap(_.toLocalOutput_opt).get assert(localAndRemote.publicKeyScript == localPubKeyScript) assert(localAndRemote.amount == 145_000.sat) - val localOnly = closingTxs.localOnly_opt.flatMap(_.toLocalOutput).get + val localOnly = closingTxs.localOnly_opt.flatMap(_.toLocalOutput_opt).get assert(localOnly.publicKeyScript == localPubKeyScript) assert(localOnly.amount == 145_000.sat) } @@ -1130,13 +556,14 @@ class TransactionsSpec extends AnyFunSuite with Logging { assert(closingTxs.localAndRemote_opt.nonEmpty) assert(closingTxs.localOnly_opt.nonEmpty) assert(closingTxs.remoteOnly_opt.isEmpty) - val localAndRemote = closingTxs.localAndRemote_opt.flatMap(_.toLocalOutput).get + val localAndRemoteIndex = closingTxs.localAndRemote_opt.flatMap(_.toLocalOutputIndex_opt).get + val localAndRemote = closingTxs.localAndRemote_opt.flatMap(_.toLocalOutput_opt).get assert(localAndRemote.publicKeyScript == localPubKeyScript) assert(localAndRemote.amount == 145_000.sat) - val remoteOutput = closingTxs.localAndRemote_opt.get.tx.txOut((localAndRemote.index.toInt + 1) % 2) + val remoteOutput = closingTxs.localAndRemote_opt.get.tx.txOut((localAndRemoteIndex.toInt + 1) % 2) assert(remoteOutput.amount == 0.sat) assert(remoteOutput.publicKeyScript == remotePubKeyScript) - val localOnly = closingTxs.localOnly_opt.flatMap(_.toLocalOutput).get + val localOnly = closingTxs.localOnly_opt.flatMap(_.toLocalOutput_opt).get assert(localOnly.publicKeyScript == localPubKeyScript) assert(localOnly.amount == 145_000.sat) } @@ -1148,38 +575,39 @@ class TransactionsSpec extends AnyFunSuite with Logging { assert(closingTxs.localAndRemote_opt.nonEmpty) assert(closingTxs.localOnly_opt.nonEmpty) assert(closingTxs.remoteOnly_opt.isEmpty) - val localAndRemote = closingTxs.localAndRemote_opt.flatMap(_.toLocalOutput).get + val localAndRemoteIndex = closingTxs.localAndRemote_opt.flatMap(_.toLocalOutputIndex_opt).get + val localAndRemote = closingTxs.localAndRemote_opt.flatMap(_.toLocalOutput_opt).get assert(localAndRemote.publicKeyScript == localPubKeyScript) assert(localAndRemote.amount == 150_000.sat) - val remoteOutput = closingTxs.localAndRemote_opt.get.tx.txOut((localAndRemote.index.toInt + 1) % 2) + val remoteOutput = closingTxs.localAndRemote_opt.get.tx.txOut((localAndRemoteIndex.toInt + 1) % 2) assert(remoteOutput.amount == 0.sat) assert(remoteOutput.publicKeyScript == remotePubKeyScript) - val localOnly = closingTxs.localOnly_opt.flatMap(_.toLocalOutput).get + val localOnly = closingTxs.localOnly_opt.flatMap(_.toLocalOutput_opt).get assert(localOnly.publicKeyScript == localPubKeyScript) assert(localOnly.amount == 150_000.sat) } { // Same amounts, both outputs untrimmed, local is fundee: val spec = CommitmentSpec(Set.empty, feeratePerKw, 150_000_000 msat, 150_000_000 msat) - val closingTx = makeClosingTx(commitInput, localPubKeyScript, remotePubKeyScript, localPaysClosingFees = false, localDustLimit, 1000 sat, spec) + val closingTx = ClosingTx.createUnsignedTx(commitInput, localPubKeyScript, remotePubKeyScript, localPaysClosingFees = false, localDustLimit, 1000 sat, spec) assert(closingTx.tx.txOut.length == 2) - assert(closingTx.toLocalOutput !== None) - val toLocal = closingTx.toLocalOutput.get + assert(closingTx.toLocalOutput_opt.nonEmpty) + val toLocal = closingTx.toLocalOutput_opt.get assert(toLocal.publicKeyScript == localPubKeyScript) assert(toLocal.amount == 150_000.sat) - val toRemoteIndex = (toLocal.index + 1) % 2 + val toRemoteIndex = (closingTx.toLocalOutputIndex_opt.get + 1) % 2 assert(closingTx.tx.txOut(toRemoteIndex.toInt).amount < 150_000.sat) } { // Their output is trimmed: val spec = CommitmentSpec(Set.empty, feeratePerKw, 150_000_000 msat, 1_000 msat) - val closingTx = makeClosingTx(commitInput, localPubKeyScript, remotePubKeyScript, localPaysClosingFees = false, localDustLimit, 1000 sat, spec) + val closingTx = ClosingTx.createUnsignedTx(commitInput, localPubKeyScript, remotePubKeyScript, localPaysClosingFees = false, localDustLimit, 1000 sat, spec) assert(closingTx.tx.txOut.length == 1) - assert(closingTx.toLocalOutput !== None) - val toLocal = closingTx.toLocalOutput.get + assert(closingTx.toLocalOutputIndex_opt.contains(0)) + assert(closingTx.toLocalOutput_opt.nonEmpty) + val toLocal = closingTx.toLocalOutput_opt.get assert(toLocal.publicKeyScript == localPubKeyScript) assert(toLocal.amount == 150_000.sat) - assert(toLocal.index == 0) } { // Their output is trimmed (option_simple_close): @@ -1187,10 +615,10 @@ class TransactionsSpec extends AnyFunSuite with Logging { val closingTxs = makeSimpleClosingTxs(commitInput, spec, SimpleClosingTxFee.PaidByThem(800 sat), 0, localPubKeyScript, remotePubKeyScript) assert(closingTxs.all.size == 1) assert(closingTxs.localOnly_opt.nonEmpty) - val toLocal = closingTxs.localOnly_opt.flatMap(_.toLocalOutput).get + val toLocal = closingTxs.localOnly_opt.flatMap(_.toLocalOutput_opt).get assert(toLocal.publicKeyScript == localPubKeyScript) assert(toLocal.amount == 150_000.sat) - assert(toLocal.index == 0) + assert(closingTxs.localOnly_opt.flatMap(_.toLocalOutputIndex_opt).contains(0)) } { // Their OP_RETURN output is trimmed (option_simple_close): @@ -1199,17 +627,17 @@ class TransactionsSpec extends AnyFunSuite with Logging { val closingTxs = makeSimpleClosingTxs(commitInput, spec, SimpleClosingTxFee.PaidByThem(1_001 sat), 0, localPubKeyScript, remotePubKeyScript) assert(closingTxs.all.size == 1) assert(closingTxs.localOnly_opt.nonEmpty) - val toLocal = closingTxs.localOnly_opt.flatMap(_.toLocalOutput).get + val toLocal = closingTxs.localOnly_opt.flatMap(_.toLocalOutput_opt).get assert(toLocal.publicKeyScript == localPubKeyScript) assert(toLocal.amount == 150_000.sat) - assert(toLocal.index == 0) + assert(closingTxs.localOnly_opt.flatMap(_.toLocalOutputIndex_opt).contains(0)) } { // Our output is trimmed: val spec = CommitmentSpec(Set.empty, feeratePerKw, 50_000 msat, 150_000_000 msat) - val closingTx = makeClosingTx(commitInput, localPubKeyScript, remotePubKeyScript, localPaysClosingFees = true, localDustLimit, 1000 sat, spec) + val closingTx = ClosingTx.createUnsignedTx(commitInput, localPubKeyScript, remotePubKeyScript, localPaysClosingFees = true, localDustLimit, 1000 sat, spec) assert(closingTx.tx.txOut.length == 1) - assert(closingTx.toLocalOutput.isEmpty) + assert(closingTx.toLocalOutput_opt.isEmpty) } { // Our output is trimmed (option_simple_close): @@ -1217,60 +645,15 @@ class TransactionsSpec extends AnyFunSuite with Logging { val closingTxs = makeSimpleClosingTxs(commitInput, spec, SimpleClosingTxFee.PaidByUs(800 sat), 0, localPubKeyScript, remotePubKeyScript) assert(closingTxs.all.size == 1) assert(closingTxs.remoteOnly_opt.nonEmpty) - assert(closingTxs.remoteOnly_opt.flatMap(_.toLocalOutput).isEmpty) + assert(closingTxs.remoteOnly_opt.flatMap(_.toLocalOutput_opt).isEmpty) } { // Both outputs are trimmed: val spec = CommitmentSpec(Set.empty, feeratePerKw, 50_000 msat, 10_000 msat) - val closingTx = makeClosingTx(commitInput, localPubKeyScript, remotePubKeyScript, localPaysClosingFees = true, localDustLimit, 1000 sat, spec) + val closingTx = ClosingTx.createUnsignedTx(commitInput, localPubKeyScript, remotePubKeyScript, localPaysClosingFees = true, localDustLimit, 1000 sat, spec) assert(closingTx.tx.txOut.isEmpty) - assert(closingTx.toLocalOutput.isEmpty) - } - } - - test("BOLT 3 fee tests") { - val dustLimit = 546 sat - val bolt3 = { - val fetch = Source.fromURL("https://raw.githubusercontent.com/lightning/bolts/master/03-transactions.md") - // We'll use character '$' to separate tests: - val formatted = fetch.mkString.replace(" name:", "$ name:") - fetch.close() - formatted + assert(closingTx.toLocalOutput_opt.isEmpty) } - - def htlcIn(amount: Satoshi): DirectedHtlc = IncomingHtlc(UpdateAddHtlc(ByteVector32.Zeroes, 0, amount.toMilliSatoshi, ByteVector32.Zeroes, CltvExpiry(144), TestConstants.emptyOnionPacket, None, 1.0, None)) - - def htlcOut(amount: Satoshi): DirectedHtlc = OutgoingHtlc(UpdateAddHtlc(ByteVector32.Zeroes, 0, amount.toMilliSatoshi, ByteVector32.Zeroes, CltvExpiry(144), TestConstants.emptyOnionPacket, None, 1.0, None)) - - case class TestVector(name: String, spec: CommitmentSpec, expectedFee: Satoshi) - - // this regex extract params from a given test - val testRegex = ("""name: (.*)\n""" + - """.*to_local_msat: ([0-9]+)\n""" + - """.*to_remote_msat: ([0-9]+)\n""" + - """.*feerate_per_kw: ([0-9]+)\n""" + - """.*base commitment transaction fee = ([0-9]+)\n""" + - """[^$]+""").r - // this regex extracts htlc direction and amounts - val htlcRegex = """.*HTLC #[0-9] ([a-z]+) amount ([0-9]+).*""".r - val tests = testRegex.findAllIn(bolt3).map(s => { - val testRegex(name, to_local_msat, to_remote_msat, feerate_per_kw, fee) = s - val htlcs = htlcRegex.findAllIn(s).map(l => { - val htlcRegex(direction, amount) = l - direction match { - case "offered" => htlcOut(Satoshi(amount.toLong)) - case "received" => htlcIn(Satoshi(amount.toLong)) - } - }).toSet - TestVector(name, CommitmentSpec(htlcs, FeeratePerKw(feerate_per_kw.toLong.sat), MilliSatoshi(to_local_msat.toLong), MilliSatoshi(to_remote_msat.toLong)), Satoshi(fee.toLong)) - }).toSeq - - assert(tests.size == 15, "there were 15 tests at e042c615efb5139a0bfdca0c6391c3c13df70418") // simple non-reg to make sure we are not missing tests - tests.foreach(test => { - logger.info(s"running BOLT 3 test: '${test.name}'") - val fee = commitTxTotalCost(dustLimit, test.spec, DefaultCommitmentFormat) - assert(fee == test.expectedFee) - }) } } \ No newline at end of file diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/CommandCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/CommandCodecsSpec.scala index e6956f1d12..586e188da7 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/CommandCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/CommandCodecsSpec.scala @@ -17,9 +17,9 @@ package fr.acinq.eclair.wire.internal import fr.acinq.bitcoin.scalacompat.ByteVector32 -import fr.acinq.eclair.UInt64 import fr.acinq.eclair.channel._ import fr.acinq.eclair.wire.protocol._ +import fr.acinq.eclair.{TimestampMilli, UInt64} import org.scalatest.funsuite.AnyFunSuite import scodec.bits.{ByteVector, HexStringSyntax} @@ -31,10 +31,14 @@ class CommandCodecsSpec extends AnyFunSuite { test("encode/decode all settlement commands") { val testCases: Map[HtlcSettlementCommand, ByteVector] = Map( - CMD_FULFILL_HTLC(1573, ByteVector32(hex"e64e7c07667366e517886af99a25a5dd547014c95ba392ea4623fbf47fe00927")) -> hex"0000 0000000000000625 e64e7c07667366e517886af99a25a5dd547014c95ba392ea4623fbf47fe00927", - CMD_FAIL_HTLC(42456, FailureReason.EncryptedDownstreamFailure(hex"d21a88a158067efecbee41da24e1d7407747f135e585e7417843729d3bff5c160817d14ce569761d93749a23d227edc0ade99c1a8d59541e45e1f623af2602d568a9a3c3bca71f1b4860ae0b599ba016c58224eab7721ed930eb2bdfd83ff940cc9e8106b0bd6b2027821f8d102b8c680664d90ce9e69d8bb96453a7b495710b83c13e4b3085bb0156b7091ed927305c44")) -> hex"0004 000000000000a5d8 00 0091 d21a88a158067efecbee41da24e1d7407747f135e585e7417843729d3bff5c160817d14ce569761d93749a23d227edc0ade99c1a8d59541e45e1f623af2602d568a9a3c3bca71f1b4860ae0b599ba016c58224eab7721ed930eb2bdfd83ff940cc9e8106b0bd6b2027821f8d102b8c680664d90ce9e69d8bb96453a7b495710b83c13e4b3085bb0156b7091ed927305c44", - CMD_FAIL_HTLC(253, FailureReason.LocalFailure(TemporaryNodeFailure())) -> hex"0004 00000000000000fd 01 0002 2002", - CMD_FAIL_HTLC(253, FailureReason.LocalFailure(TemporaryNodeFailure(TlvStream(Set.empty[FailureMessageTlv], Set(GenericTlv(UInt64(17), hex"deadbeef")))))) -> hex"0004 00000000000000fd 01 0008 2002 1104deadbeef", + CMD_FULFILL_HTLC(1573, ByteVector32(hex"e64e7c07667366e517886af99a25a5dd547014c95ba392ea4623fbf47fe00927"), Some(FulfillAttributionData(TimestampMilli(123), None, None))) -> hex"0008 0000000000000625 e64e7c07667366e517886af99a25a5dd547014c95ba392ea4623fbf47fe00927 ff 000000000000007b 00 00", + CMD_FULFILL_HTLC(1573, ByteVector32(hex"e64e7c07667366e517886af99a25a5dd547014c95ba392ea4623fbf47fe00927"), Some(FulfillAttributionData(TimestampMilli(123), Some(TimestampMilli(125)), None))) -> hex"0008 0000000000000625 e64e7c07667366e517886af99a25a5dd547014c95ba392ea4623fbf47fe00927 ff 000000000000007b ff 000000000000007d 00", + CMD_FULFILL_HTLC(1573, ByteVector32(hex"e64e7c07667366e517886af99a25a5dd547014c95ba392ea4623fbf47fe00927"), Some(FulfillAttributionData(TimestampMilli(123), None, Some(hex"636ac35e3d982332a8347578a2753f443fa53b7ca3ab9ce5486eeb857ebbf4c49eed261430e666aa5a724484e9ea4a194c3c21d7d1fbce42c13376e9b1c420b6d8c90cb3883af13a73c4d541ebd83e0fc6366263110922cd71133babfa375dc1e0aa8d898dfd152805a48cfc16e7fac88a3a298caf7511d23ab5ecd4eafeb0275a6f736f25d07f6d8182a75f7e692705a3ac481400bf7944f27f611bfb4cdffdfb726f7d877d3c031ff16992f67b7f6e8951aa8b974d245520487cb4a8d9149d0bce892e7af8da9997d679556d515f849a5acf028fbaa81290c681124b14957b1beba8b2716c5ace289fe495ddf3222dcf00764f73d3a4f979586327d460ae61ddbb3b249efa73b3ca8dfc1105a88bdd0b9470fe17b052f954ba41b75c6d28e5da27bd05f63a20cdde22211a37dd460137bb90bf684ac47f1a5e3a82b1ed7550414282d02bca3920f2d0db1064ede422de2151872586462623b280a99284a2faa571aab1b92da43ad331e82480cb18bfc62a9a20b2614e1daf28ed5a3ef294296bbca459edf90becf4a883e1486c0bf9db8a0a842b1868482447aed4fdb28f8de201d9aa6009a2e0a85b560f8333665400fad8d3f75fc924e72f1ef259db26dbf0a59813146fff405876bdde9b877d5828a933d6cccab461a81d3791712f65353863b64b7045a0d1bc27157dd71ad9c18913421a077e4d073d0c50480b0fa0f6b160ed72fd106e3c9aea00683f4532549dbc9d40a81f66b20ec4c9ee1a873bbbb6ca4b167ee163a001880e455da43e7d9c00bf747fdf3587b3407afe2de86c5f5a76a3052866260514bb06b433f64bf6ca2f69c335ac5afada81c801c708be9872c8a242ed93e5e9ebb70ec9b615a083700db350463d3e9368f6d778635663cfaaa3b725a03298e736e43a9201d222dff7d54e045da37ea84b0d607c7244ad46a90aa77d7905dae4a2160caadd1282918f80f8659a559f29f2eefc5709625d7169fd8abb9c532772fbdce93fd9b6495bad4f184a3cc39a302555cf1361d68c0b1528d38bb10f20638e407f3252f053f15d0b21ae26046c29b3a83ac2e8d69e354ce4178df615bc46cd66cc65e9206dfbb3b0d6613071c1d92d6f1116513f38777ee686771dcacc8ad5d8b615ca48ca6da267f48ea401bd2abd0e251dd5e1953c4f5d5619eb31e4d91176db345f9f269225d7598384ecc0a7eb0e24bccd1dc9a6822e41fb7c2fff7baddee630ff4d7ed08f15c241b15568ced090e2d8f94b51c8cb14d5b4f450ceda4008907f2e31ae636579a383499aaa85")))) -> hex"0008 0000000000000625 e64e7c07667366e517886af99a25a5dd547014c95ba392ea4623fbf47fe00927 ff 000000000000007b 00 ff 636ac35e3d982332a8347578a2753f443fa53b7ca3ab9ce5486eeb857ebbf4c49eed261430e666aa5a724484e9ea4a194c3c21d7d1fbce42c13376e9b1c420b6d8c90cb3883af13a73c4d541ebd83e0fc6366263110922cd71133babfa375dc1e0aa8d898dfd152805a48cfc16e7fac88a3a298caf7511d23ab5ecd4eafeb0275a6f736f25d07f6d8182a75f7e692705a3ac481400bf7944f27f611bfb4cdffdfb726f7d877d3c031ff16992f67b7f6e8951aa8b974d245520487cb4a8d9149d0bce892e7af8da9997d679556d515f849a5acf028fbaa81290c681124b14957b1beba8b2716c5ace289fe495ddf3222dcf00764f73d3a4f979586327d460ae61ddbb3b249efa73b3ca8dfc1105a88bdd0b9470fe17b052f954ba41b75c6d28e5da27bd05f63a20cdde22211a37dd460137bb90bf684ac47f1a5e3a82b1ed7550414282d02bca3920f2d0db1064ede422de2151872586462623b280a99284a2faa571aab1b92da43ad331e82480cb18bfc62a9a20b2614e1daf28ed5a3ef294296bbca459edf90becf4a883e1486c0bf9db8a0a842b1868482447aed4fdb28f8de201d9aa6009a2e0a85b560f8333665400fad8d3f75fc924e72f1ef259db26dbf0a59813146fff405876bdde9b877d5828a933d6cccab461a81d3791712f65353863b64b7045a0d1bc27157dd71ad9c18913421a077e4d073d0c50480b0fa0f6b160ed72fd106e3c9aea00683f4532549dbc9d40a81f66b20ec4c9ee1a873bbbb6ca4b167ee163a001880e455da43e7d9c00bf747fdf3587b3407afe2de86c5f5a76a3052866260514bb06b433f64bf6ca2f69c335ac5afada81c801c708be9872c8a242ed93e5e9ebb70ec9b615a083700db350463d3e9368f6d778635663cfaaa3b725a03298e736e43a9201d222dff7d54e045da37ea84b0d607c7244ad46a90aa77d7905dae4a2160caadd1282918f80f8659a559f29f2eefc5709625d7169fd8abb9c532772fbdce93fd9b6495bad4f184a3cc39a302555cf1361d68c0b1528d38bb10f20638e407f3252f053f15d0b21ae26046c29b3a83ac2e8d69e354ce4178df615bc46cd66cc65e9206dfbb3b0d6613071c1d92d6f1116513f38777ee686771dcacc8ad5d8b615ca48ca6da267f48ea401bd2abd0e251dd5e1953c4f5d5619eb31e4d91176db345f9f269225d7598384ecc0a7eb0e24bccd1dc9a6822e41fb7c2fff7baddee630ff4d7ed08f15c241b15568ced090e2d8f94b51c8cb14d5b4f450ceda4008907f2e31ae636579a383499aaa85", + CMD_FAIL_HTLC(42456, FailureReason.EncryptedDownstreamFailure(hex"d21a88a158067efecbee41da24e1d7407747f135e585e7417843729d3bff5c160817d14ce569761d93749a23d227edc0ade99c1a8d59541e45e1f623af2602d568a9a3c3bca71f1b4860ae0b599ba016c58224eab7721ed930eb2bdfd83ff940cc9e8106b0bd6b2027821f8d102b8c680664d90ce9e69d8bb96453a7b495710b83c13e4b3085bb0156b7091ed927305c44", None), Some(FailureAttributionData(TimestampMilli(123456), None))) -> hex"0007 000000000000a5d8 02 0091 d21a88a158067efecbee41da24e1d7407747f135e585e7417843729d3bff5c160817d14ce569761d93749a23d227edc0ade99c1a8d59541e45e1f623af2602d568a9a3c3bca71f1b4860ae0b599ba016c58224eab7721ed930eb2bdfd83ff940cc9e8106b0bd6b2027821f8d102b8c680664d90ce9e69d8bb96453a7b495710b83c13e4b3085bb0156b7091ed927305c44 00 ff 000000000001e240 00", + CMD_FAIL_HTLC(42456, FailureReason.EncryptedDownstreamFailure(hex"d21a88a158067efecbee41da24e1d7407747f135e585e7417843729d3bff5c160817d14ce569761d93749a23d227edc0ade99c1a8d59541e45e1f623af2602d568a9a3c3bca71f1b4860ae0b599ba016c58224eab7721ed930eb2bdfd83ff940cc9e8106b0bd6b2027821f8d102b8c680664d90ce9e69d8bb96453a7b495710b83c13e4b3085bb0156b7091ed927305c44", Some(hex"c8e7a432d1db82765a5e4cecf360b439ff9cabe795f470249065a1b8d893c9ac5142092e9260b073caadc7097d3d0aa6b5f8181c02b8eb9ecb9c17a9867e8e533766e79c00844321a7a15afcd562cefbde4cc7a66865b57192f770f68260cc09e133876181921018ade17ac1bdca027af5073b5e354cbe888651a0592a228de9019b1136b702e56424c84e01ec60cee5df2ad7179196809bcbf58e17f91fd851f91fd2a5be1ea5efb43ea18d1a5b8ad2d1fe69a9f29fbea84565c34ca3088e848e5c4297f7bacea917f332a311beb365f3f131d21871120fd3fdeefb0566cb56e8dac56cdb1eee4ebb2bfcdc954326566cfb42481e7ef5fb3031ac9190e8e02f35a9430ce7cf5167f75e5c016056e2dba3022acabfe20a6891f57cfcf0abf09102b7af1b91223badca2eb865e8a523b141dcf91955631e4efd7e9664205e89aaa2282826ac65e9651620dc3392231f8f28821271da0ce9c5eb3f145837aefde0e5b33b5cb8f847de6caa51b3488baedbb706012c9b7034919f23b7c043d3e484f4be9ab72b9b37985c34c21b5ffcbb40e9b11fe83661cef912c97f5a6b4cce76518a0245d45d0fb2844b2853457a982a9418fa83934fead109f8a38ac23c3a03bbb32d573d2349bb2228c8a53efc65e9165526b53034e53d4f8540960129657b88e28e75c9f8d26f48c4d2cc86006456f03e0bff14262e97f94d64e58369798e43bb89f6f1ae731301eded7bce4ec21613670be6939ad17b8b6d4a9ca059cc5ed33dcc7c7608dda6e2810c7d76b20fa23f5e8adb91fa895ef2ff030086168e16bcc4a4376d36f2c169e5821bb40316df88e456a8b99057f130b8ad1f097e7c9f81d64c816531a325e6b517de4022bea321a41a92386ce50ca271ff1d2ebaf0694e57545e624ef3f76eae1454314136276bf1bab91e3fcdf541eb60052fc65932505be888e3ec1de782e27a3689727128cb1018fbd4ca7b51916ba05280f1004cf5bfdde6159453e2936b76842c1d978e34d0a5f65ba2a27dd235538c2875a1ca9433b7a799aa30e28facb6603e8644da1ae9a8cf169df35f905a366aada5e0c4fc1a7f9cb36f75a12983fd9e43d3339d506d37aebd9886a52f98cc330c812605508525d8c8ad5b93a9c08c5d41123dbd7e15644f61a9ab5758aa2615cc1781d997f4d2177d4b56c4be0276e67debb4cd01e398e8a6d9d3ceac030a01235b965b1a733f2710251bb1638cdb77894667aecedb1bd56b8e9979f938a4f9a66a9da44a6d6727fdfa01641b021d0d89061370a0cb2fa68d1f242a")), Some(FailureAttributionData(TimestampMilli(123456), None))) -> hex"0007 000000000000a5d8 02 0091 d21a88a158067efecbee41da24e1d7407747f135e585e7417843729d3bff5c160817d14ce569761d93749a23d227edc0ade99c1a8d59541e45e1f623af2602d568a9a3c3bca71f1b4860ae0b599ba016c58224eab7721ed930eb2bdfd83ff940cc9e8106b0bd6b2027821f8d102b8c680664d90ce9e69d8bb96453a7b495710b83c13e4b3085bb0156b7091ed927305c44 ff c8e7a432d1db82765a5e4cecf360b439ff9cabe795f470249065a1b8d893c9ac5142092e9260b073caadc7097d3d0aa6b5f8181c02b8eb9ecb9c17a9867e8e533766e79c00844321a7a15afcd562cefbde4cc7a66865b57192f770f68260cc09e133876181921018ade17ac1bdca027af5073b5e354cbe888651a0592a228de9019b1136b702e56424c84e01ec60cee5df2ad7179196809bcbf58e17f91fd851f91fd2a5be1ea5efb43ea18d1a5b8ad2d1fe69a9f29fbea84565c34ca3088e848e5c4297f7bacea917f332a311beb365f3f131d21871120fd3fdeefb0566cb56e8dac56cdb1eee4ebb2bfcdc954326566cfb42481e7ef5fb3031ac9190e8e02f35a9430ce7cf5167f75e5c016056e2dba3022acabfe20a6891f57cfcf0abf09102b7af1b91223badca2eb865e8a523b141dcf91955631e4efd7e9664205e89aaa2282826ac65e9651620dc3392231f8f28821271da0ce9c5eb3f145837aefde0e5b33b5cb8f847de6caa51b3488baedbb706012c9b7034919f23b7c043d3e484f4be9ab72b9b37985c34c21b5ffcbb40e9b11fe83661cef912c97f5a6b4cce76518a0245d45d0fb2844b2853457a982a9418fa83934fead109f8a38ac23c3a03bbb32d573d2349bb2228c8a53efc65e9165526b53034e53d4f8540960129657b88e28e75c9f8d26f48c4d2cc86006456f03e0bff14262e97f94d64e58369798e43bb89f6f1ae731301eded7bce4ec21613670be6939ad17b8b6d4a9ca059cc5ed33dcc7c7608dda6e2810c7d76b20fa23f5e8adb91fa895ef2ff030086168e16bcc4a4376d36f2c169e5821bb40316df88e456a8b99057f130b8ad1f097e7c9f81d64c816531a325e6b517de4022bea321a41a92386ce50ca271ff1d2ebaf0694e57545e624ef3f76eae1454314136276bf1bab91e3fcdf541eb60052fc65932505be888e3ec1de782e27a3689727128cb1018fbd4ca7b51916ba05280f1004cf5bfdde6159453e2936b76842c1d978e34d0a5f65ba2a27dd235538c2875a1ca9433b7a799aa30e28facb6603e8644da1ae9a8cf169df35f905a366aada5e0c4fc1a7f9cb36f75a12983fd9e43d3339d506d37aebd9886a52f98cc330c812605508525d8c8ad5b93a9c08c5d41123dbd7e15644f61a9ab5758aa2615cc1781d997f4d2177d4b56c4be0276e67debb4cd01e398e8a6d9d3ceac030a01235b965b1a733f2710251bb1638cdb77894667aecedb1bd56b8e9979f938a4f9a66a9da44a6d6727fdfa01641b021d0d89061370a0cb2fa68d1f242a ff 000000000001e240 00", + CMD_FAIL_HTLC(253, FailureReason.LocalFailure(TemporaryNodeFailure()), Some(FailureAttributionData(TimestampMilli(123), None))) -> hex"0007 00000000000000fd 01 0002 2002 ff 000000000000007b 00", + CMD_FAIL_HTLC(253, FailureReason.LocalFailure(TemporaryNodeFailure()), Some(FailureAttributionData(TimestampMilli(123), Some(TimestampMilli(125))))) -> hex"0007 00000000000000fd 01 0002 2002 ff 000000000000007b ff 000000000000007d", + CMD_FAIL_HTLC(253, FailureReason.LocalFailure(TemporaryNodeFailure(TlvStream(Set.empty[FailureMessageTlv], Set(GenericTlv(UInt64(17), hex"deadbeef"))))), Some(FailureAttributionData(TimestampMilli(456), None))) -> hex"0007 00000000000000fd 01 0008 2002 1104deadbeef ff 00000000000001c8 00", CMD_FAIL_MALFORMED_HTLC(7984, ByteVector32(hex"17cc093e177c7a7fcaa9e96ab407146c8886546a5690f945c98ac20c4ab3b4f3"), FailureMessageCodecs.BADONION) -> hex"0002 0000000000001f30 17cc093e177c7a7fcaa9e96ab407146c8886546a5690f945c98ac20c4ab3b4f38000", ) @@ -50,13 +54,17 @@ class CommandCodecsSpec extends AnyFunSuite { val data32 = ByteVector32(hex"e4927c04913251b44d0a3a8e57ded746fee80ff3b424e70dad2a1428eeba86cb") val data123 = hex"fea75bb8cf45349eb544d8da832af5af30eefa671ec27cf2e4867bacada2dbe00a6ce5141164aa153ac8b4b25c75c3af15c4b5cb6a293607751a079bc546da17f654b76a74bc57b6b21ed73d2d3909f3682f01b85418a0f0ecddb759e9481d4563a572ac1ddcb77c64ae167d8dfbd889703cb5c33b4b9636bad472" val testCases = Map( - hex"0000 000000000000002ae4927c04913251b44d0a3a8e57ded746fee80ff3b424e70dad2a1428eeba86cb" -> CMD_FULFILL_HTLC(42, data32, commit = false, None), - hex"0001 000000000000002a003dff53addc67a29a4f5aa26c6d41957ad798777d338f613e7972433dd656d16df00536728a08b2550a9d645a592e3ae1d78ae25ae5b5149b03ba8d03cde2a36d0bfb2a5bb53a5e2bdb590f6b9e969c84f9b41780dc2a0c5078766edbacf4a40ea2b1d2b9560eee5bbe32570b3ec6fdec44b81e5ae19da5cb1b5d6a3900" -> CMD_FAIL_HTLC(42, FailureReason.EncryptedDownstreamFailure(data123), None, commit = false, None), - hex"0001 000000000000002a900100" -> CMD_FAIL_HTLC(42, FailureReason.LocalFailure(TemporaryNodeFailure())), - hex"0003 000000000000a5d8 00 0091 d21a88a158067efecbee41da24e1d7407747f135e585e7417843729d3bff5c160817d14ce569761d93749a23d227edc0ade99c1a8d59541e45e1f623af2602d568a9a3c3bca71f1b4860ae0b599ba016c58224eab7721ed930eb2bdfd83ff940cc9e8106b0bd6b2027821f8d102b8c680664d90ce9e69d8bb96453a7b495710b83c13e4b3085bb0156b7091ed927305c44" -> CMD_FAIL_HTLC(42456, FailureReason.EncryptedDownstreamFailure(hex"d21a88a158067efecbee41da24e1d7407747f135e585e7417843729d3bff5c160817d14ce569761d93749a23d227edc0ade99c1a8d59541e45e1f623af2602d568a9a3c3bca71f1b4860ae0b599ba016c58224eab7721ed930eb2bdfd83ff940cc9e8106b0bd6b2027821f8d102b8c680664d90ce9e69d8bb96453a7b495710b83c13e4b3085bb0156b7091ed927305c44")), - hex"0003 00000000000000fd ff 0002 2002" -> CMD_FAIL_HTLC(253, FailureReason.LocalFailure(TemporaryNodeFailure())), - hex"0003 00000000000000fd ff 0008 2002 1104deadbeef" -> CMD_FAIL_HTLC(253, FailureReason.LocalFailure(TemporaryNodeFailure(TlvStream(Set.empty[FailureMessageTlv], Set(GenericTlv(UInt64(17), hex"deadbeef")))))), + hex"0000 000000000000002ae4927c04913251b44d0a3a8e57ded746fee80ff3b424e70dad2a1428eeba86cb" -> CMD_FULFILL_HTLC(42, data32, None, commit = false, None), + hex"0000 0000000000000625 e64e7c07667366e517886af99a25a5dd547014c95ba392ea4623fbf47fe00927" -> CMD_FULFILL_HTLC(1573, ByteVector32(hex"e64e7c07667366e517886af99a25a5dd547014c95ba392ea4623fbf47fe00927"), None), + hex"0001 000000000000002a003dff53addc67a29a4f5aa26c6d41957ad798777d338f613e7972433dd656d16df00536728a08b2550a9d645a592e3ae1d78ae25ae5b5149b03ba8d03cde2a36d0bfb2a5bb53a5e2bdb590f6b9e969c84f9b41780dc2a0c5078766edbacf4a40ea2b1d2b9560eee5bbe32570b3ec6fdec44b81e5ae19da5cb1b5d6a3900" -> CMD_FAIL_HTLC(42, FailureReason.EncryptedDownstreamFailure(data123, None), None, None, commit = false, None), + hex"0001 000000000000002a900100" -> CMD_FAIL_HTLC(42, FailureReason.LocalFailure(TemporaryNodeFailure()), None), + hex"0003 000000000000a5d8 00 0091 d21a88a158067efecbee41da24e1d7407747f135e585e7417843729d3bff5c160817d14ce569761d93749a23d227edc0ade99c1a8d59541e45e1f623af2602d568a9a3c3bca71f1b4860ae0b599ba016c58224eab7721ed930eb2bdfd83ff940cc9e8106b0bd6b2027821f8d102b8c680664d90ce9e69d8bb96453a7b495710b83c13e4b3085bb0156b7091ed927305c44" -> CMD_FAIL_HTLC(42456, FailureReason.EncryptedDownstreamFailure(hex"d21a88a158067efecbee41da24e1d7407747f135e585e7417843729d3bff5c160817d14ce569761d93749a23d227edc0ade99c1a8d59541e45e1f623af2602d568a9a3c3bca71f1b4860ae0b599ba016c58224eab7721ed930eb2bdfd83ff940cc9e8106b0bd6b2027821f8d102b8c680664d90ce9e69d8bb96453a7b495710b83c13e4b3085bb0156b7091ed927305c44", None), None), + hex"0003 00000000000000fd ff 0002 2002" -> CMD_FAIL_HTLC(253, FailureReason.LocalFailure(TemporaryNodeFailure()), None), + hex"0003 00000000000000fd ff 0008 2002 1104deadbeef" -> CMD_FAIL_HTLC(253, FailureReason.LocalFailure(TemporaryNodeFailure(TlvStream(Set.empty[FailureMessageTlv], Set(GenericTlv(UInt64(17), hex"deadbeef"))))), None), hex"0002 000000000000002ae4927c04913251b44d0a3a8e57ded746fee80ff3b424e70dad2a1428eeba86cb01c8" -> CMD_FAIL_MALFORMED_HTLC(42, data32, 456, commit = false, None), + hex"0004 000000000000a5d8 00 0091 d21a88a158067efecbee41da24e1d7407747f135e585e7417843729d3bff5c160817d14ce569761d93749a23d227edc0ade99c1a8d59541e45e1f623af2602d568a9a3c3bca71f1b4860ae0b599ba016c58224eab7721ed930eb2bdfd83ff940cc9e8106b0bd6b2027821f8d102b8c680664d90ce9e69d8bb96453a7b495710b83c13e4b3085bb0156b7091ed927305c44" -> CMD_FAIL_HTLC(42456, FailureReason.EncryptedDownstreamFailure(hex"d21a88a158067efecbee41da24e1d7407747f135e585e7417843729d3bff5c160817d14ce569761d93749a23d227edc0ade99c1a8d59541e45e1f623af2602d568a9a3c3bca71f1b4860ae0b599ba016c58224eab7721ed930eb2bdfd83ff940cc9e8106b0bd6b2027821f8d102b8c680664d90ce9e69d8bb96453a7b495710b83c13e4b3085bb0156b7091ed927305c44", None), None), + hex"0004 00000000000000fd 01 0002 2002" -> CMD_FAIL_HTLC(253, FailureReason.LocalFailure(TemporaryNodeFailure()), None), + hex"0004 00000000000000fd 01 0008 2002 1104deadbeef" -> CMD_FAIL_HTLC(253, FailureReason.LocalFailure(TemporaryNodeFailure(TlvStream(Set.empty[FailureMessageTlv], Set(GenericTlv(UInt64(17), hex"deadbeef"))))), None), ) testCases.foreach { case (bin, command) => diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/ChannelCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/ChannelCodecsSpec.scala index 1cfebe6ec3..8630a2f4e1 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/ChannelCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/ChannelCodecsSpec.scala @@ -17,17 +17,18 @@ package fr.acinq.eclair.wire.internal.channel import fr.acinq.bitcoin.scalacompat.Crypto.PrivateKey -import fr.acinq.bitcoin.scalacompat.{Block, BlockHash, ByteVector32, ByteVector64, Crypto, DeterministicWallet, Satoshi, SatoshiLong, Transaction, TxId, TxIn} +import fr.acinq.bitcoin.scalacompat.{Block, BlockHash, ByteVector32, ByteVector64, Crypto, DeterministicWallet, OutPoint, SatoshiLong, Transaction, TxId} import fr.acinq.eclair._ import fr.acinq.eclair.blockchain.fee.FeeratePerKw -import fr.acinq.eclair.channel.Helpers.Funding +import fr.acinq.eclair.channel.ChannelSpendSignature.IndividualSignature import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.crypto.ShaChain -import fr.acinq.eclair.crypto.keymanager.{LocalChannelKeyManager, LocalNodeKeyManager} +import fr.acinq.eclair.crypto.keymanager.{ChannelKeyManager, LocalChannelKeyManager, LocalNodeKeyManager, NodeKeyManager} import fr.acinq.eclair.json.JsonSerializers +import fr.acinq.eclair.reputation.Reputation import fr.acinq.eclair.router.Announcements -import fr.acinq.eclair.transactions.Transactions.{AnchorOutputsCommitmentFormat, CommitTx, TxOwner} +import fr.acinq.eclair.transactions.Transactions.ZeroFeeHtlcTxAnchorOutputsCommitmentFormat import fr.acinq.eclair.transactions._ import fr.acinq.eclair.wire.internal.channel.ChannelCodecs._ import fr.acinq.eclair.wire.protocol.{CommonCodecs, UpdateAddHtlc} @@ -47,59 +48,12 @@ import scala.io.Source class ChannelCodecsSpec extends AnyFunSuite { - test("nonreg for der/bin64 signatures") { - val bin = ByteVector.fromValidHex(Source.fromInputStream(getClass.getResourceAsStream("/normal_data_htlcs.bin")).mkString) - val c = ChannelCodecs.channelDataCodec.decode(bin.toBitVector).require.value.asInstanceOf[ChannelDataWithCommitments] - - val ref = Seq( - hex"304502210097fcda40b22916b5d61badedf6126658c2b5927d5002cc2c3e5f88a78ba5f45b02204a74bcf8827d894cab153fc051f39d8e2aeb660162a6a05797f7140587a6133301", - hex"3045022100aa403fa23e82379a16ba16b446dbdee5a4f879ba690ad3f4f10dc445df2832ba022000a51fdbdb69dcbd5518274303fcece60d719cb6d593e882fdb0190253bbaaab01", - hex"3045022100a9ad65dada5e5500897173bca610135a13008895ce445fbc90d440d5406bd6150220644d75e5ca774ef6b559ffaf1083a9a5da250c3c87666d96daf35b480ef0c65701", - hex"3044022063a6839c031fd5534d7807a8fff8ca0f9a72d8aa9d78ee104d6ece2b417ac5ce0220193d9b521a44011d31d2bb85be5043119c42a7aee3d9ef68b7388c3c9c3a780501", - hex"304402205dda44c9d8aaf37a6f5f6c99713d2a001682f2593a960ccaf5c23059cd20016b02200991b09bccdfc87918852650a4bfa7b4ac9028101362631b5ec376427084138e01", - hex"3045022100eddaa4f767bc70fd672bee983b1644dbff9479def0efc7cca79f0daa1bad370d02204c810238968ae9e86b99d348464e9ac7a06e40225022ae4203ae36fad928c22401", - hex"3044022020e6d43dee03f54574d8245edf2e312d0a492dd2350b7f8df68390b8876de5640220555d46cd545ff0ecc280e6bc82e976ff494bab5f2b128807626753ffb9e5796e01", - hex"3045022100812a360a6ddc44179f80e5b4252bca74bb5dbe1da25230c9e8afcd388a2fd64702202e45a658123f0263ca1157ef9a9995ede1625d1ecba532957185f1d8044aa1d301", - hex"3044022076a338d225b8954412198ce5936aaa6433da1f51dd9bcbe69d95a1e0960c169802207db267517fc73e358e09f4c89313ae17ed4d5f6d8432faec9ec1e784a2a7da7c01", - hex"304402201d099a464a7696b22a8e58b65c52e9a519a06a5c49e944155d4e5fbd14d3f5b902203c091c0ec5b840a80be739d29b5fc2c75cb94928e5ea83133f84d226f28cd4b701", - hex"304402203af7b7ea16cc018fdb414f52cd38ed548dc257cbb06c812c9dc1d60500b21485022072cd74b7e49bfd813e09bae778da903b44b7b0ae22b87af4c34cf8bb77dfdef201", - hex"3044022043573edb37be815d1b97b90803f601dfc91c25279ccda606ad6515fee721fe57022030ac2883408a2075a47337443eb539062a8ac6b5453befb2b9863d697e35dd8201", - hex"304502210093fd7dfa3ef6cdf5b94cfadf83022be98062d53cd7097a73947453b210a481eb0220622e63a21b787ea7bb55f01ab6fe503fcb8ef4cb65adce7a264ae014403646fe01" - ) - - val sigs = c.commitments.latest - .localCommit - .htlcTxsAndRemoteSigs - .map(data => Scripts.der(data.remoteSig)) - - assert(ref == sigs) - } - test("nonreg for channel flags codec") { // make sure that we correctly decode data encoded with the previous standard 'byte' codec assert(CommonCodecs.channelflags.decode(byte.encode(0).require).require == DecodeResult(ChannelFlags(announceChannel = false), BitVector.empty)) assert(CommonCodecs.channelflags.decode(byte.encode(1).require).require == DecodeResult(ChannelFlags(announceChannel = true), BitVector.empty)) } - test("backward compatibility DATA_WAIT_FOR_FUNDING_CONFIRMED_COMPAT_01_Codec") { - // this is a DATA_WAIT_FOR_FUNDING_CONFIRMED encoded with the previous version of the codec (at commit 997aceea82942769649bf03e95c5ea549a7beb0c) - val bin_old = hex"000001033785fe882e8682340d11df42213f182755531bd587ae305e0062f563a52d841800049b86343ecd1baa49e0304a6e32dddeb50000000000000222000000012a05f2000000000000004e2000000000000000010090001e800bd48a66494d6275d73cc4b8e167bf840c9261a471271ec380000000c501c99c425578eb58841cbf2f7f2e435e796c654697b8076d4cedc90a7e1389589a0000000000000111000000009502f900000000000000271000000000000000008048000f01419f088c6cd366cadb656148952b730fdf73ccbcf030758c008e6a900f756bfb011fa7aae5886b9c34c5f264d996a7a1def7566424c0f90db8b688794b9ca43db8017331002fd85b09961fa68a1a6c2bc995e717ecf99f10c428a79457a46ce5d47b01325bcce8670aa8e0373c279531553ef280791c283189b1acdee55c21d239cdd90196e7a6fc79329b936e87a07b2d429e7b61ff89db92a4c66ec70e2562a14664570000000140410080000000000000000000000002ee000000003b9aca000000000000000000001278970eefbc9e9c7f44d0ab40555711618404aebd87d861ceca9867ce4f7bfe5d000000000015c0420f0000000000110010326963ce09728e7b80fd51c6d6b7f64d6e124cf55c0d2e887bc862499e325da00023a91081419f088c6cd366cadb656148952b730fdf73ccbcf030758c008e6a900f756bfb1081484d9633cfc4a9ab9f4220a7b3c679e648f18e53a7dadb7154242943b587415aa957009d81000000000080f8970eefbc9e9c7f44d0ab40555711618404aebd87d861ceca9867ce4f7bfe5d00000000007b7ef94000a1400f000000000011001013dc34f3aebaa16836581958168fcc55a5719f1a0c312ea9033c110afb4c677c82002418228110806fb9c53b82a46021cf2b48a18dc49434e9d207f3bff2f09c84e5df5f9526057481100c5bab065bd059dafb7ccef1ce14a850a4bb206b132b53cb9df70faa5be8812e00a39822011021c7d70a781a0ceb7605bed811e9aacdbff8c71f2904d54a7f57a46bd735c9f901101b6fde320e8cc9388538ab65bb2f55ac7cec1218163ec0d590e13ac3fbd7d60e80a3a91081419f088c6cd366cadb656148952b730fdf73ccbcf030758c008e6a900f756bfb1081484d9633cfc4a9ab9f4220a7b3c679e648f18e53a7dadb7154242943b587415aa95770612810000000000000000000000000000002ee0000000000000000000000003b9aca0030e78713eebb77ebfb57a70dee19a85a4e54a11fdf74c9fda7fc108ceaa942db8133978aafdb8e7232a61dca8495134873c1c9ceebb926c85256f432b73b111a9f00000000000000000000000000000000000000000000000000000000000040f5a7aef68b8bc802d20427a43145bfbeded7d322c363f661569a38c58064da6700093c4b8777de4f4e3fa26855a02aab88b0c202575ec3ec30e7654c33e727bdff2e80000000000ae0210780000000000880081934b1e704b9473dc07ea8e36b5bfb26b709267aae0697443de43124cf192ed00011d48840a0cf84463669b3656db2b0a44a95b987efb9e65e78183ac60047354807bab5fd8840a426cb19e7e254d5cfa11053d9e33cf32478c729d3ed6db8aa1214a1dac3a0ad54ab80001e25c3bbef27a71fd1342ad01555c45861012baf61f61873b2a619f393deff9740237cd8e4ea0093bb773bab03a938d5e35e9ebbb5b321ffd7ee031feff96eb82f8970eefbc9e9c7f44d0ab40555711618404aebd87d861ceca9867ce4f7bfe5d000006b0aade318a54c3339808654a781d421412758912d2df1b039399ffed52d3b784f698e9230da056f09212e8ac5cebe7ab1283c08aa3cd548ed5e1a4f81ebfe10" - // currently version=0 and discriminator type=1 - assert(bin_old.startsWith(hex"000001")) - // let's decode the old data (this will use the old codec that provides default values for new fields) - val data_new = channelDataCodec.decode(bin_old.toBitVector).require.value - assert(data_new.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].fundingTx_opt.isEmpty) - assert(TimestampSecond.now().toLong - data_new.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].waitingSince.toLong < 3600) // we just set this to current time - // and re-encode it with the new codec - val bin_new = ByteVector(channelDataCodec.encode(data_new).require.toByteVector.toArray) - // data should now be encoded under the new format - assert(bin_new.startsWith(hex"04000a")) - // now let's decode it again - val data_new2 = channelDataCodec.decode(bin_new.toBitVector).require.value - // data should match perfectly - assert(data_new == data_new2) - } - test("backward compatibility older codecs (integrity)") { // It's not enough to just verify that codecs migrate without errors, we also need to make sure that the decoded // data is correct. To do that, we compare json-serialized representations of the data. @@ -159,123 +113,23 @@ class ChannelCodecsSpec extends AnyFunSuite { } } - test("backward compatibility with eclair <= v0.5.1") { - // the following were encoded with eclair v0.5.1 - val dataNormal = hex"0100220000000103af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d000094a9267a1f2b86e492dacf939afd1561a0e42ed248d1a09f711204c176fef1b0f80000001000000000000044c0000000008f0d1800000000000002710000000000000000000900064ff16001429acc00c77e6894e41e417ae712e557d7bba1c460000186b0200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000028a82039dc0e0b1d25905e44fdf6f8e89755a5e219685840d0bc1d28d3308f9628a358500000000000003e8ffffffffffffffff0000000000004e2000000000000003e80090001e0252a01d9d8b03db1a99b1a82223f7dd60ecffa69dbef47c06228fdea2fe0f021503c2a3cb942c336afbe484a00452f5b027378e2124c7ea452f05c26ab89591c18f03019eec506c69765552b397d706dd276f0718c82e0a49224fbaccfad75f81b2f502d3d560591f03da622f338b0988bbf0f612fafa4f1b22ed51398cf7a07c5f4192025d67e71808e128eb21c82881cef04a510067a50d0f61f2f455bcfb7e168ae7ad00000003028a820000000000000000010007fffd05aa5d10939a524d7b2ab225cdb41decfc129208c372d51ca1db705bb1b9831e53ee000000000000000000000000000493e0f81fd474bea478df0202b311c69e85dc6215f629491dd15ad0929faae2535abb00061b10000370b2714d2734e6b8cde085794dd7b41c8a9b6c03c1edd8e3db168ee7fce39493596e882b18b5b1b79c16400c6762b9856075821be6be9fab8f469f56820d8f341554400a8da7f1f1a8501902581d43b9fa6e5c68015716f718a2190b87fce41bf1b509aa61806394a42489d63c457fe4c79e7480eced1315edd731887e57704fc9102f50cb7f0d242d755cfd5a2172dbaf7f01b124861cc6d1dc804796bbb84165c805f0c0f3fb9ec97b74c2a694de56a9cf8d79d1a679260ee1169d78214b34c8a654ba22e59ddcd32beff4713de33549f035b342660405b0159a7a508e5691ef4805689140e72b8a0ef2e61be74dea5d0b8f5589e0e373cac2e2e1cc39b2121c05cf4122ad0f8b9af6fbf1de2ea26376c2650ccd306c13a7b64acbf2a3feed128754abe44658009e642768ae3d84f5e0fa5f7f360c2a1c76d26985817ae77b71fb59014a5483ebba9271cafa5e5d8031c569adeceb8bae6444e98d2522b28f6682109fc7d31cdb83ebd45e5d81e7f046df42345b49f470dbef9ed87709301d2c6131215d33a30b8d18e63e54a2aff85dd57672f8198bca6a67ee147c7d0ae649e5661ab6bf78a662fef9a164f1e332b9f16e6fb3d5769ddcbc1d1c07338d3394b9245d17618c2474e86c064fca4df00ad3a93dc051fd8c3328cde2a987798b0f22a21c90426700abeb1e6f38dffb485b5477ec44c690fa80e317b32a982fd3082253bba8595783290dbffee4fc9296ffdf16a8bf3154971bb720e78674969e9db2e0fbab9e9e13f24bc8b3af5e2f00f262f0da56de443f70398ab68f747d35370fcd8e1c0e130f7269e08f862b5a67f2c129be254df2358762ce3a947eb27d66450af51540e7721b47c8a5a86098ea64dad381f14e07aabbbc470949a99c07612add3ab4c575fe2e520bbe511a1a674aea37a44535c13ee3380f8f39bd230fc1481cd31912af36c6751e23c6f383cd37a8b13fa7df9f0c7e460739f2c6226638ee14f14d36366211cbc6a1e16b4856bf302a540aa9d9e833b1d59c510473096384c8b450f2f3f1dab9e614af822949d5cc93d76bc4d1a52891bc85f1981ef83161195ab7d8181ee4fb163bc6c685a10e87c7f4b15ed7d05833c230a4a5b63841fc65b959f0ff010e697f47c583f9b7fa9b389c0eff6614e47d85b83c483136f182be4c151d272f5d938b912a95e47d333e5de6a409ad271679a778a7eb3f169c71525302fac5d4575e2645c09763c2ef165736a7a726ca605038e2781404328790ffaacef2b9c2bf90122042cd571287bc4e3973da65fbd4e3da9e40e4347ca6eb4ef1ffef4e5a34be80425cae3e81533f7f2953f95fca53a22057a39125f5c76350fba7fc6c036838fb951d0aa8702e7f44c6f8a9cbce3b64fa8ddc2bb8c8b35d1e29a21beda6fdd332b31a749321455277231fd9d70ea4aded95053b395f88fa6916d126e1626fc0f1be6cd2a9538d17c498b40927f12b3bb40fa3e272e82cd2242b670afefa387470f4e6e0a1236028954c9e90311f486617187956a23b90b356d71e219e6dd055c2120771003a6c12769aa3ceacb9642bc01022731ca7a413b68ee7d1d5444f75dfa51a68b74a01ac85f6ceaf5e56987b9d67d6de896f5aafd25c78c413a6d4b5b03d571167524cd231ba13bd9f80fd7413faf21e8170cef0d08b242c5c38a2b0158da56e358ba0692f670d4611c7a3624b234adc30c5b7198e0afc941f5d13eae3a94ddffa652c784c34c582e04e948da91a5ac3038a9df38fd4f1733779f4f122ca2d7ff9d03bac9def35d9ee3a183161f8f2808d472b2e64581209359cea58ca7757164c666029982223877e2b14d2d537afb012f1ffc12cd083c16dfc64213c56f3d4d22b603d3dfab1d21e239d6fc1f9f153ed61f1ac91c29c85c16f4aa2985f84052f5a08d32bdd00fd05aa5d10939a524d7b2ab225cdb41decfc129208c372d51ca1db705bb1b9831e53ee00000000000000000000000002faf08011b29e60a38883a8d4434f17ca3d92161109f7ddd8799e64e86d6b8509babd1100061b100003b8c1771721551fa88af8fdde92909add6e5b8fa90a4b0484ee065ed3ed7c56e733a7fcd775d4ab92956c1328ffee9c195004c5dee5dd5b9d0f9034589e6769e72579d3d5837ad70785ec420b4a24c04d36668728c0d2534ff1feea9aac2410423fd79c7db9231ee7efd3d585646e378fe53d731d18f38d6356a970f5c3026edc849d49ff34e58dfb7548512461110088ac3aa800e10785029ba3b0e9ab7bde0f056939e4921792dba2f5c005135daf57e32cae06a9ccb1b4d321f3ba015e5b92def1ff1c200e56b3990d82570586bfae26e9398e17dc6c069f92d80e6dfedf6b2f24b1dc3cc9d63e684d861f40fdbf508d4ac34b7f10c57be2a9b0c5921f86869c29ada5394b8780d2488a4fca3cd98ddb0ff8ea4415a07caea436682835744e94d5cff6d3024a9525dbd697e499b7ef23062b18b225bfaa4c5bb07166f34ff7866ec8f0fbbc12f695c609692798364fa20bf7977e321deda3fe5510833494532fba94fc1f0dd14ec74f3e9fe8ee659634621b63d16d46a8958132c24bd82c516bdf9ae9515cebae42778e4de6be7047c31cf86c0df0306f7b6562e1f35be51e5e64cc6d9d4c010849e6ac7ddacaa4b7b6fb1d35aac815964090940e73a1193eece11c1c1d37e373ef58c5e2d690b6ed6338360af9906146da9db8329bd2786bbf92df10445ee093f0b1b2a640cc2daf003fa7141435ba1dd54f9cdbf5417fa7f539b255452852a85d2ce97ce5abed4980e7b409e283f97ccc9c01e104b55155f96ace6789f61c4661962d34fc5d7e6f5f5233180933b2fa7f7a5b074714645489f5221966160946b7bfbf0fe6733e6beb8af4457b9d36cde1200811009ec483a9d730ca980aa28f636942af5e89794a8edbc1b75d555ba134974374d0fe23d31c26566064eb9998d649bb2bf066bf710da50672f4e3ab4df843a0c8942bad0a071c237d4c1759eca37380919e36aec73284db202a32d3d1619f3e5b757b2df8b04bde567783dc8e465d996799782f1a1b8de9331681a35aa04edb427de87264c8ae9c397f29d3e8730db91256425a10b960a9de1a48d0d4186d617d2b69c87e2540f6570faff4ee1f6303d7d281434947abeaad83c86a4d25bef4de2bb3c6104aa0ceed7c8df039f4be6a42851a118adb1b8f98e02f6727b75d98541bab2ff24fb2f20342e86150c678941825409b62a844f44ca1ccdf0d9f7c2cf9b222fbed00bc92be0802fbfbbeefa71c8976cba8fc4aeb031480f434027b1cd593d08cbc14c2a360b736b06b5afb8da35f0be3818fff4275b8c830f5248a8b8edea1327454e1360bd90d4fa08e965f459b0b027e1180290cf762f813a31e8109f472d9657b03af737d1f7bd2e59441541a84ba818f1413c5cd1f8b9882e9188e0def9e44e2f4a7c710c893c7188ba86423f8ae86068d84e1832af548289e87c34d68b186df7e24ca5b051f8f5e4a44e2e7383ba2a09615b4147b34e86486731290ea67f3be24c13a9c5cc37f06555989b3f10c580a9cd2b416d0ee4210855c6833a25996761dfabb036f3893cff7db7e310baa8faa79f46e0ee43bf4dfd732eae7f44bad2e7c032b9c6d14947af6b0e37e5ec98372a622f716ffba0cde04b9d4508392dd154ddc34829412bfa604d4f00e4b10a553587343ef5c0944165e7ee1e34387b09c147ecba943cf36dbc4269efe50ec3a5a3075c43be9651d6db6acb9f657476952b78c990557f05935247a71077373ec436ec586def177448f8859ba096b7a838e5b4ce7a463f9082f705c26d99936eb1be584ea9b58a44b9b4faa07fd8247fa66cf4529d1b8cdb92ed7bd96bf0968db4376489c7d46f0f27d58ac884c29736502953723ef1ab41e19c7041d3e0e9091d7de2e3904d032de02292edb1225a672ab438d3c65f7921c06a9f181f8ffda4ac524d0e000fd05aa5d10939a524d7b2ab225cdb41decfc129208c372d51ca1db705bb1b9831e53ee000000000000000200000000000f4240657c0bd97e795218aa623b27cf9a71764379c4762fcee8993aa0b1ab1e32194a00061b100002c0ec5d388a8c78491bbd870faa2c46e4282e11796123b69c2792e5806748ef068397f0212a33e3f79162ba4ea247ccec410db039dd323af48ac27bd0ba77ecb870c587477d4543d9a29c53fc02bc98d7cd0144c7abf80b999c22b42a28ae8d625478ca304001f9a49782ec970031e673c76e4e27357a321729d6df38d1d88dcbf764c69eda3baac9739bed637010f44638cfe1deecc56b76f6e02d1d0c3f104462b9ffa5f20de4cf092d86a5bb35d5f62b0fb1c983a2c06df17c9ffc809c83e4b4335f5903fab536fbd9719847bb063541ecbe05c12ef8d058b3547faca054e3d662250f1cc1f925dd71297abc25fe37ab33a086759fac76208a64552f84d2e4d84daccdc3aa2bbd2c2f922bf262596742dfe034529d1ead2975dd3d197ab0e2e1c75c8b8f160ca6077638022d4afbd107979949cf342cb399347f3990029f0db6d9ac0c569d61d42539371f9a7ff59e9c83ff97d15bf0eeb254ae58fb7b1f9d8710c546ac8a227930c66ac841bc4f475229e5cadd14ba5a01e6b2da99c55861a08e2100e62c4499d30003fe30ddaa347d7a27c2158d3787d58fe51ae57d797bbef7f900508d1580df3e5233f0887567fba1faa918c246d2ec5c3b7aa022cb8a652d00b4d719e312482f57655eee80a90cdc73151fd7ab9c5367793d60c6088fab98f0547d7f547e10db202a25e027a5cd0abc41bb0e3ef563c0a6d469a702b2a26f0e8b4fddb845a16a5f06b9dee33c3adb31430c94942c5023179d3e4441948a332069a1c3b69dca65f05a43452e42fd28a2f7e6344f98ab9a7e4eece3c1709be1f7bb620f8b6c45989a8bccad39a4bf40e8215183d1449196f1f9fc17de778b616856152e6e6145a1b7a3d7f226becaea5ebe34aa4bd06e60f0fed207bfd21f5663bfadd37dd722437bcd46a26fb4e19d062574a81bcd817eecbc1914a5878809128961acdd73113ae9c51070ff4494e16d81ccec777eeb513da82bf43d4884812b26546b4370dc315793271b069f60f4285f648cf122ed8b22b0c7a27e94ccd59a273eb774c109e19980e146850de95f82cdd8aa0e82022672024c917b281422d284df0ee0bdeb3d4ac56b4ca675ebdb835c17b6a822d79ae7310f4aa41d80ac61c5e45c1c0e1d64542622a31091a9f87c335e86d964dd85a951d7c9bf41c9f2b1a9bb8424d7d1b26413da8034182fa42d2b1cd1f8745482c49d8348d19c72cf5a02bd28e4cba82128af8bf5d9c1215c4f543ef4d185f100f8d803dfa29c300c072e44ad9542b82fb1380d55c15c9a4b4398876e2450b90b49990746f339abca8cc8a462b62329a128758ce0e46b5f998af1bb485a3044bd125424eb5c623afd2a11befe4ec544eafe275ed1ad82b940dad5e9a9710d48562e51b296ba81f2d70593685ba0e3f3b25089a187e61d5675dd481aa99620276cb0a841a3c4df201a929287b1127270c5d25d06fb286dae1a9a5a5cdb60003f0c30d2021074bf252e550685f7b51a087a77b0871e883104e55f898aa5bc4cf8538c293253a737556d7f220e15b90cf0eda7d5f2172372e3c50c12cc588f312da37191b5038e944825044b130bff281ecd47a4252a1411ab7a9305c2b37e9facd435e9c434de37641498f8e4bfa7b42966da29c84200aca87ea1c3b00db54906b340e524a7dc4a15403bb82bc24517cb91026096bdf18f5f7ae5640ed6de1f0c5d184813d6b9d244b32b58e9ff524741a39383eec3a530d60db13deb26e3523a725f0599b671b625c07002704fb600b77318417d2527537359d122e22a1f7581eaebdc19e65ba50bdda18ae08e9a8694fcb0ff1a2cd98d910dbcd52064c15a4282d67b278c72a0fdbf228abf6b519dd28ac21c57d1da4bb7ad5b5ab10da6b83132df1da79ccfc77fb45598bbd91ef5ab96d8a2ee148639a562debafffd05aa5d10939a524d7b2ab225cdb41decfc129208c372d51ca1db705bb1b9831e53ee00000000000000010000000002faf080ab009f4d0b317a141e5b3f865599676f895834400b66dd09d33ad27163ca176f00061b1000028fe56ab181f454372c939ca0516f5782b26cec2010885c70e55c41a1e43de3af6635d2ed0bed90837cefa9f1805f4808e8092a4d44efe5fb616ff7678487a460b367134fcc728ba273fb1d22718bc6a95e47a120a6d952c7c2cb8f38e59c2a4efa63977cced7e4b8f46e4d47d29098a73beee807c3337c4acddcbb32b78eddbd124b2f33ec6cd8bdd364aa4ad2c10eb69dab808fe5f5f0aec19750e51ac65a8746f345c05d4b8823ffbeadd6200ad01449c39a008fbe117a8ee904445488811336d0c439419fa4f285f9f62a34f10b076c99c0092968e3cc9fa656016b6da049bd56b910d7a9356e76d24e746b280f0275ec9e9bace82d852bf0a137ea02d4cbd3b68450bfb593564d8c20953bb758890a55a8c381a4b3303b61ec26a56111361bf7262b3f6f2503aad06758130d86ea607cdbba53415aaf253430d92fdd81c685ab39233e94654e6508eec1347747e2df2862169382aef6f99dd78b50629c5d98b1fcc73e865679d862b42f8e9d54ef6288ed2c3f2713f0fa4db538cd3e70ec1a30cd65dbf873f581b30892acedacd39b5f0aa774d1f3f77d8fd11ed628bcd02ac33f89123595aa455ec54a07e93e26f94338fedd8bb84094a0add52f912ed5f9019e3a28d90d251cc6ed7ffd35254dcadd9f1e9b28eb0e06fd4fe961d60cb690a7757f475c08aef07c2e54668121540a42a9c779623709a2124629e8c4bd4021763979647f625b360a4559dfd3f57798dfe5d36e9d902904af3ed67d8f4b0894538c7718f5160d211cec27375a7e6a2ec42f2c8fcd1c953b7b8379d42439a2c6b921a66d5102ceb6bd6bc20b17098e69a0a4f708b42520e4792474c3d115a12c83ef60ac6e69d8842c5981e9a6d178efa352e73e4a34bed4fb590dbeecb259617668e6ffb9f955297f26e3a6a3b95d9617529a61f08666ca1069d2ee1876337d3e786244c5bb45a8236577184584cf3018118d7e4e78973ee510b6773bd922797e580cd240dea3ca31892d23c1e6e4fa92f1a01da8ea40044f5613a9429ebe7906f79b32636204d025115810b376d4c6436da136b96c7c10649e3290caecd6ca14d995a817e3725fee7e621c5366f80c752e50aeffee1af3361924f31cbb1cb44731d19963ff30127ca2363ce15e50948be14c43400737ee8910ed06027599da74b06e77eb82ac523cc031c57c02dd82dbc0d53629d072615c92034cf829e7a5d4437b1f58e2bd4b16993e1e1b05c26ed8d695351db11d21df36a7f5811ef5fe001ab1e1c6ce9d2b69b6ac3af8087e6666317f75b645e3b1caefac0eb65327fcb9fa62be341c99f191cc869e48dbc8fee3e42d4393cbc6505c880dd6739a69be4f7ef3de306480a7a51f413d310926f252ea96a0c772d8b8e94e7d6cedbfbdb21fae2ffc379eb17c2680fa2bc56a8726c93e7bf2d446221ce95e49da93d29bec8e53ddcfd262c33d556c2b8921c3de93236408b462d28612d3343fbb9cc538b1e6b33c341c3b91dd41f936931e61f146fd00aee1c5c0de97b47cf7efce889012e1c22dd8faf0fe155f4e9930c27941d8b0907502a835bfffff801f6835de69ad33e95232f773219eec0e2374c421230f323257dbc91629c4ca8a61584f737b827fe8e8f5b69b88a7b64b362f8142b043f08ea82c4a0ab7c4e0b9805533e806f90597095242ff64f314801fb7ad838e98e1859b2c05c9b027ae5a4baf780d15977bc1492dee9b14b1cb0fb3243eb2304919486fcde89a3cf35a64e31b1698e35fbb8528a73526a19189d406272b8becec94379f69372afa99d06bb4f34df72e1c3b49557855ae8ac265160bdf48ab34cde30d2665891cfeda24adcc657d851431f38953f917f1a111f023d2c71845ccf25a562d2450bf8b4986b64ae4fd09e5a8ab9610d11fd68e9d570b3a467780236de974c7fffd05aa5d10939a524d7b2ab225cdb41decfc129208c372d51ca1db705bb1b9831e53ee000000000000000200000000003d0900559bb949364ed92a64f449cd6ee3eaba2e607595d3cecb36b06b145baaa69bc000061b100002860fd058656f185036e81a64fe19034d29223d3620bdf4bf2ff3c1def9c6bdd70c2cda7f660e1b202672741bc3258b04fb755fbbb1350261e7992bce930e256bd5d8d4fde61365b7b24904b788ef2040fdd6eb87f97c2d2f29eab4291d9a28b5da306f28b98d01d93517b203a028199d423a3545aa17522c63247f73af7b63335b0b48e4b875c69b42f4cb1573bb3e5fd68837f90c50b0161f067a9eafd9a0790e53ed5053ce60ffff410e4b16a4b7bc5a52c57e78ef266100c9f79753f81878c08e5dbd4d80c6e46a339578d8ac8c572df77ef614800cedc460c06878c0da97908067729ed35e3afe919071724f89ab736f5791a9c9b5d422136332213434c836e2ceb9fce0e2e96a9a6d7befe8c132867d5fafea1a7809ddd6b3a89c8ef6ea83028d3e2cca00f1bc6e12ea8b67e91a98acaa2edebaf6dd3a18c655b6b1fbbff5c641f8002780758d05f1f39c9470a124a5add314abd2262142733120747cda2f1d9eb90d68ecb9c7fbab23d73a35f2a20a2a365de6cd678d53bd5bd9bd518333d04e8e678b5d08f028982dad08c80be7d8fbb0638dd814232224c687f8321baf96ed8b39a1e9ab52dfd69d8eed79ac3f5a2c480a585bff038c92b367743317b937d969cdd533ae1d797a789ff7994f86a0d6cae470b64ebddbc478573af347a110dd1feaaeb4779441ec439cfdbafaba870105efd86b9d85a4df7ddb9b09f5b6b4144cd1fad5932df37ebf19a62648659fc1969142310a5cc9b4d0c48ba6bb0f863ed53a0b75fe1ee6515a46993f95be2e34166408b54a43e55c4802b37ac902fb4c8367ce38990d07ed3104d0728d327d3b9de6452b520f9af534505885788109ec78c1176ca0864d28422e826cc83f821b7eaf028d6a7e350b3037d0fe58d1d4e18113c8f61913932e71c0f334402534d8663f15445f900fb9dc6b3a93223868167be26fcbd70c0459eee37f81fd539c319eb0b04bd478b94b5f4cd23b4d496c2bdd6e8a154fd76c4ecbdf7647fe9e7be88c6a3a8e7696e2e596dfc25ba798db6ca331d135e9ce7c0aab9721d3f70ca53354f96ecd028236259b9b0d9e0bbf73c8e841b1d4276214f7be8feb525c91d39910b0e091997a2b89e945806e93cd325cb51463b0729f1a519334038cba09653799ef533a49e812e86b81af7e5099a02ca11c2b17dfc8b9e51a57a20546f2c92826676ebcb4f64fe7cc77424388dfec7199179cb125bb4613c8bf05edc4173987d7d5ae0fcbfa08a1e5ea2d6b01406d740b49c5b1a68da585549590c3ec13479efa3136c5ade68057fe173ace55593ceca8440372b03f332969866d1bfbce3fe9dd907d27593b8b2ad25eb4b12afd0f3abe7931ba7789c84a3ed65a03df06d998a9956043a4de786c359bdd58f0b9e5cfc32bc709b626ce8e63f3997a0e9f784f6b94e342b4710553e805cc8399191254189058ec75a15556467b2456d9b38a7e4d15cf59727ad2c32f0daaef1748be04311ff484479eb31f7eff9e32b816fa40f40430e801c15e931294b39ec4d6c93e8130fb4a1b53ca6471886a33b10b46d6973d02bbe3b39345e121473ea4ba41f3cfced07c3a613393e38b8ca01c353ccab96128093bb5fd000978b11ae6f7c1fedf48c3682119d1b44629d02ea3800d2e247ef1e78e527e5d574b4dd144e1fe1b0c6551442d419baba300258657792947443747b29b6fb9417d3c57536de6aa379c02f3addbd2066554189ca817c7f57331c972c30e3ed06fc7e521b8e2bb57c023dff9816f260a153f1ffd55df2a2fae568f3dc734d62ab47a77c54772b5e4cf2758c912cb473ec372173d9eda0c58b103ab12d11dcb976a40816ecfa8b9c9384f6415017555933652e77ae20e14ea9ac25b5811fc03364c4883bb78c82e29f8a8892415ff652b782642bf6036b3e2200e00fd05aa5d10939a524d7b2ab225cdb41decfc129208c372d51ca1db705bb1b9831e53ee000000000000000100000000007a1200ffb841fb291ecf09ae7b5dcec1feeb45ba196ca02d6b9e50ae2b3dcd9ca5d72500061b100003acd0a3acb9b4f9540678dc9324480bb3d4f54e5e007739c9c1d600bff75dfbbd0191e75c0a2d810a6ec5b03d02cffbf1a66123c87790e66eee8d416caf82e7ea7090a62fe14276fa88af32ab9793f7a100de5525eedf71967c13c8d361246d98b78cc74383e714f88899f34407644c14874046b3b722186015c07b8db042db955c91cb14abbf3cfa646aea81ad15bf67108a763539c64c5a8e8115d46e063e956671e8ea8d8fd638a6414d71e9b475ebe070da7faf75a898f29048ab5a2b6c7e3a72a178b8e470e8375f539ebf6284d15486c5a8774d46ea164ba2b62181f47623fd987ff5958550c962a193638679b79fcd477fdf2a09c0fa879bb22cb493fbaed27518f5cb265741535b4ab14246077ca18f11ece7aaa0e01ec5bf02c3c3b541ea08bf254df123079c1538e266dcd3161bf1b9ed41d873f1491906e1459ba51ac9dd95e783598d3c356e0cc5b98c2a96b148f55f102a9810181eedd46cd00b445d861baeeac46eba469435aa4ddc877bd68b53f4d005aa2566d356cc344aafcdc86abd774ea28cc838d2fdc541c4b6da494a96e128b8c2abab4b21b3ae2646cdcc3528ef6fd8587b3a0636ead67a62309fb003afdc14177d329b062622313e9dee912847763c68678df663a39b89c69efdb6d916d5754534bdca9030955cbcbae6fb7ff1df6282175cd37a30a904418b976af05809f0e0e7e4b4e2ec018f1e9c6bcb" ++ hex"e7a7822c8699669946f5e684671d63e68cc7c9cca2963945dc21c52232e6f83b1875b2bed7c80c37371a480a2e5255d49d390c3b2adfc695036ed91371cda7d79bdfbae464581f0b32942f03826aca17ab9da6ade4a778d310ec3da17fc3af426d21b347aee7c2db7b5e188e35714dc514e3a1c100e8595c9e0e4399ad796021976f077e5733ea535cc6daec2e371853dcb715fc366ea7d6b9a5b3509dccf5c2e1225e3a51de9f5bb9b6586b282a0b27a9ae7ae8f2be14ec677670241e384b462eecfde68957839b1327c9e5c622c0f67cdaf3845ddbe6f754401d720d6b6d5c061dc906bfa70fb76e1168c6ac1a25cabe8873c3c1e540ae44ae631a2638accd7951f368442dba7b38d0662ccb0140d1e4ca23f51de731a6f5adcf816c3235359afd607e58948da29a5f06c96b4312aee7d35ed4c2c811a58c5a196ac2f377d653d51cfccb5213c928955ac880b5fb1b91e88a52d5c217cbf78e071275fe626c230fded548b0f1667af1309149ff74c5542119d4e269fdc1b241d9f53e02e38e015b7c5c2d2ee623bcb4167e37edafddc7fadd642c20f81b454db1a3b578d527f124dbd1f3d99fdd1590256ae4e47c2e8b3bfe8708a0d5506d6ce8b130ce6b70028161454a5065e9925d75c0095dc24ba789489fa1e9236e25330ca1a45e61224ee027664f6589028a240961aff09187fb719ff3477b56427189b7b3c790b4031f6539c5e3a8a5d7fd99c2534ed1646920a43e7315bba98d59c51b337ba7a1b038006bb574df46830f96a5685e07ee8a0a41e712810faacfb231c67e69d0fc24b98d782c70e15524d5d2dcf4e64b4e26772dfa7067f6dc7ffc8b06e6ef3ecb13f927d466e0cd3a7ea09aaed90b7810bbfdcd8a1274bbe78a453ffff11ccee62059ec25955b34a1a1cdf8a3e506d99dcbadf16032646117556ad71cd93ceeec42be0350a6c9f194fea783558c42a56d034dcaf6fb1b28037362c7c6e2446bcda71d0a88adda3144589447ef13cb85a4d2cd16ef444097bc03e32c3b1a055e952f7ac87078b04deb900375a16dbd382ea4375ddc0a9645deca620590337e803ea8b41337f3f4e4030119a2337424dbea3d21214063ce853843dd4e6df94e3dc3bbb36d89d9eb8e15e52d0699bc6ad1de9d12afc95c8785d63756576d357126e13b25502d542774f6e5d2fdb559d52698d08600fd05aa5d10939a524d7b2ab225cdb41decfc129208c372d51ca1db705bb1b9831e53ee0000000000000003000000000007a120bf5899359b258acdec0ea500e437267aa6ba18e8509e15df74518f6ba7aa348f00061b1000036d768d1d63f1c9f09c252b5b48cf4d2db3f4935150c551391b37727723168982d7e58ce2a769313e461a0f3791a8c0208a769f5bdf4d57fbd0a9de104a0b1bbc1ab25ebbf87d1bc86dbbd42fd0dc0ff18a7ebfdd692c7dc3b95d095052540ce7801f3e25cbbcfd9dab857bc39624f59facbfacef5ab1e1e1f889b85f3b1f2a580cd660b73aa662fb15487722bd3c3f93d272725289136f2ee0fae4efe1afea25e6d4fc8334a47ad62d22be06605a15538dbd2a81311c4489b91d1cb143743e4570a6345c4f035c060aaf287ef66e7ebbe7b9037c10b66e087827478fdf76a02d25fe90e0f8228c1edfcb12eee3dd4e505a6c5a7bf2f5954ebb5560cd8c7f8b8f3f4ddca41a48a0d6c0e1092dcadc853752f459486bed349213cc15044585255842717ab70a3de3f0eedeecca10112c85a8bc248dc66c883a62288b49588d9fcc048c51081e94d65bd4731e7d71fba13693b82d2831d3bea370918aa5ddf1cc4f0d5015bb8dfb951ab13fbb9d26d5cb83dc980fc36f1712d616ce24d2c530253320f4f322f093a605ea426c577544f2983efc80be56791f443652c2233039a68f966c2f0b6351068616755dd2036b6226244d394a2b5b59160217603149901e8abe19a2bf404f384c2ed7fb0c5e470ee5ee8561f58f66bda729c2c8816853ad2357a009e537efcb4a28e845ca616be917b15aa6b8eb280bfcb321ea62fff21168b8ed54d58ccfdeee0e7752bfc0f02549d76615c85dd1e152a85ee931b34f436439e2233740328ba504c49f9764e1dca645ebf6a1377310ab53b68b4d0a6e6c952068249b86c29061725035db8d294ab9c56901485814735aa2a8d6987b1a19ced65a332f97751c4cd8a27093851f7775e5314078c04d254754d976bed2dbd2e6ecda62e9a0c7fd95299b4b13a54c9498d384210fb42d3b6bc5d8f0d42e42879f86c21eb7c5c6d1bffdd598b8f3cfcb75df159f1125a65f960637c62c7c5632d73b7b4b0544082008ede22d87e79e20eb08be0817650fefcd111de48ba2be02a7b080275c991a0ee4445dab89312644c7cf4101895e2dbcaad7d87e8e3b13e62751861b204a7e6f5a476eab0817c294d59aa0247903077d4cbe4a98e7984d2b04623d2b2ef4c650b43db15541ede229c12c045529b5c77993eb6acbdc28d812a486b5957fb996731980555bdd59ad824a882ebe1a77cbe6b9035f1c69dd01b2a27a47be5febfa65c721354e70071b07db4ebc2f01d143587c1b32a5337dd010d2a76a7773f4a7c665b4cfe4a61b103b1c319d85e007eb99b52400cd8776697e1d6118197655bf7bd0a5e7f4594bf36a2706128d5f5c3ee166b586c4d515611f4597a4c1088c1853a5959f73830cb973ea922d6211ee7d9b1d67b1025486f8f3c72a517d0d48d9a57d64c0f48e513c3b09e14ce91b515a87f3035ab55d241ccb12108dd299a362af26af96ada920202dfe26d456065717a85e6bbec540637059d82480f6c917a11434a9be5fee5ba33a8552b7b0e59f123991525e1dc14bfdfd109625b2df477bce565045375dacf6ffe99081914fc9f64df7ef8eb26801ce01be083555fd2f8a338a33c07f484b01a310ee420f5c932cfc5a8f6d32a20c3915a188833ac5775500c5d2b73e4ea595512e869f91f83de8e048c804ec8cd6de45b7ad264bcfb3c235325d3c09673a2a94e60736ceada065cefb1e355bd35bd4b56ae513343e85a3d840c2211da2d1941112318f97f825e1714f74fc2d33430196cca2423f43641b0d75b4b1a1a1ef0f07af31fda06220a2628aaab9f303cb4fe6d73fa9b39e8f6083af3bbca1becdd646afbd888c560bbf7cf4fcf5c02f712726eb23b03fe5c290df63a85ed1026cee07ab0d2bb868aa4dd6594252cb75ad5d11d9c53c5238047ea3d311c1443d478dd283a260270400002710000000002c2322200000000008af34a0245d10939a524d7b2ab225cdb41decfc129208c372d51ca1db705bb1b9831e53ee000000002b40420f0000000000220020c57347ca54a9e6f279f6a1f1e50f19b48289c3baccdd356fb03033ae7ccb6e444752210252a01d9d8b03db1a99b1a82223f7dd60ecffa69dbef47c06228fdea2fe0f0215210322a747c1d7f77fc7577a689618bbeadf28b941412404ac5e216d684a32d57a8e52aefd01d9020000000001015d10939a524d7b2ab225cdb41decfc129208c372d51ca1db705bb1b9831e53ee0000000000e136df8005401f00000000000022002019cccc41d4120d54831ad41b091b1e52b8902711365f1cbd9a44a7b5a3dc22b450c3000000000000220020653fa8ccacaea967b6bbfe9411c3f812cb44c4a18d4f6806fd81512009f706e750c3000000000000220020c9c8b5e68eea1a6f32394124873d68c66b1cab8ffe657c852696e1a1597783d924390200000000001600141fa232bbb376f103dd015631dc8d6f4f9d306122241c0b00000000002200207121fb3dfd089e1a7b7cf0934e4cc42e99678c7db90927a358985a6471d0cffc040046304302204b7524262f7aa8fbf4e9fadf476ba42b4202617b16e4d1e1ac7c9db1adb8a126021f59622119b96d12a66c6f18f12b5c04764c576f9ccb4216049ddd6e719cef9201473044022003f76c8bbfc91e5323ddc1d7a95e8a3c7707de898e6a50fd5e6562b8412e32fb022010b2bb7edcf519a9451b3883832a39a0f9008903b52fb2db90e6e371bf20307b014752210252a01d9d8b03db1a99b1a82223f7dd60ecffa69dbef47c06228fdea2fe0f0215210322a747c1d7f77fc7577a689618bbeadf28b941412404ac5e216d684a32d57a8e52aec34a86200003000324489133c313ebc91202d43a19b2034b1b2df6ccc257712bb0c38cd1d3141e42ac000000002b401f00000000000022002019cccc41d4120d54831ad41b091b1e52b8902711365f1cbd9a44a7b5a3dc22b48576a91423ca5b93a48d00f645081be0479fa799c70731a28763ac672102897afb6799af1c1af49fe4c96800ee1cea77553c0b5ae9b8cca5347238cf5be77c820120876475527c21035a26304d46e27993c218a0c644a613ca87630c8f0e1db5f112a01c3af26cdb8f52ae67a914d3260c3a0710948e34d708a99ff708f9c257ac5288ac68685e0200000001489133c313ebc91202d43a19b2034b1b2df6ccc257712bb0c38cd1d3141e42ac000000000000000000015a050000000000002200207121fb3dfd089e1a7b7cf0934e4cc42e99678c7db90927a358985a6471d0cffc101b060040ac4bb46e1b736a6fc76a4d86b24df1e9f9f91013025209ba64daf333863a2bde59a51a7af931bf0b07d6154daa30ed3a9c8c33d173c75765b46674a4e249bb734061aa4d418884ef272ea2bd65d745c42b5bd5f6781b7f876402b10eb505c60d086ddf3e108528e65f4712503114c24a20c8fab7867761677401defa780c4574f5000324489133c313ebc91202d43a19b2034b1b2df6ccc257712bb0c38cd1d3141e42ac010000002b50c3000000000000220020653fa8ccacaea967b6bbfe9411c3f812cb44c4a18d4f6806fd81512009f706e78576a91423ca5b93a48d00f645081be0479fa799c70731a28763ac672102897afb6799af1c1af49fe4c96800ee1cea77553c0b5ae9b8cca5347238cf5be77c820120876475527c21035a26304d46e27993c218a0c644a613ca87630c8f0e1db5f112a01c3af26cdb8f52ae67a914caf9c0d315f1ca821720d6911e090095585cee2788ac68685e0200000001489133c313ebc91202d43a19b2034b1b2df6ccc257712bb0c38cd1d3141e42ac010000000000000000016aa90000000000002200207121fb3dfd089e1a7b7cf0934e4cc42e99678c7db90927a358985a6471d0cffc101b060040dc5ef2218b256e0db9981ab591cf830819b62ae2068b6c4d19a85ba03153ff39316c14c1570a8fdbc2765b38a16c74e76ec7190f47aa3754e08c770d8c7e918640678fd252d77d34b3ecefff1fa369e92dad0b4a2632efe13974e538e13e14780c4c6c21838a7ead4878c9836405e45333651de6cbc012af4d6c1bda4a0039d5aa000224489133c313ebc91202d43a19b2034b1b2df6ccc257712bb0c38cd1d3141e42ac020000002b50c3000000000000220020c9c8b5e68eea1a6f32394124873d68c66b1cab8ffe657c852696e1a1597783d98b76a91423ca5b93a48d00f645081be0479fa799c70731a28763ac672102897afb6799af1c1af49fe4c96800ee1cea77553c0b5ae9b8cca5347238cf5be77c8201208763a9149bd34d5a8ea2d91b8ac4d1fb4ce79bcfd481c1f088527c21035a26304d46e27993c218a0c644a613ca87630c8f0e1db5f112a01c3af26cdb8f52ae677503101b06b175ac68685e0200000001489133c313ebc91202d43a19b2034b1b2df6ccc257712bb0c38cd1d3141e42ac02000000000000000001daa70000000000002200207121fb3dfd089e1a7b7cf0934e4cc42e99678c7db90927a358985a6471d0cffc00000000ab009f4d0b317a141e5b3f865599676f895834400b66dd09d33ad27163ca176f401e7b146982a595ed1a95e1225312dce24f45237477efca22c759c9cb16c4a6bc1c9e8834a18eb0c6c389a14ea03fb7062f788003c5c58e738d78e28e004014a64013f1f51471ecdb32897a5f604a254334f7f5fa26de5fc63b96c942db2196ae9e43b34f00054e0c58a061d5ae5f4938b5d449b8f9371edd76efdfcce758a102c500000000000000010004fffd05aa5d10939a524d7b2ab225cdb41decfc129208c372d51ca1db705bb1b9831e53ee00000000000000000000000002faf08011b29e60a38883a8d4434f17ca3d92161109f7ddd8799e64e86d6b8509babd1100061b100003b8c1771721551fa88af8fdde92909add6e5b8fa90a4b0484ee065ed3ed7c56e733a7fcd775d4ab92956c1328ffee9c195004c5dee5dd5b9d0f9034589e6769e72579d3d5837ad70785ec420b4a24c04d36668728c0d2534ff1feea9aac2410423fd79c7db9231ee7efd3d585646e378fe53d731d18f38d6356a970f5c3026edc849d49ff34e58dfb7548512461110088ac3aa800e10785029ba3b0e9ab7bde0f056939e4921792dba2f5c005135daf57e32cae06a9ccb1b4d321f3ba015e5b92def1ff1c200e56b3990d82570586bfae26e9398e17dc6c069f92d80e6dfedf6b2f24b1dc3cc9d63e684d861f40fdbf508d4ac34b7f10c57be2a9b0c5921f86869c29ada5394b8780d2488a4fca3cd98ddb0ff8ea4415a07caea436682835744e94d5cff6d3024a9525dbd697e499b7ef23062b18b225bfaa4c5bb07166f34ff7866ec8f0fbbc12f695c609692798364fa20bf7977e321deda3fe5510833494532fba94fc1f0dd14ec74f3e9fe8ee659634621b63d16d46a8958132c24bd82c516bdf9ae9515cebae42778e4de6be7047c31cf86c0df0306f7b6562e1f35be51e5e64cc6d9d4c010849e6ac7ddacaa4b7b6fb1d35aac815964090940e73a1193eece11c1c1d37e373ef58c5e2d690b6ed6338360af9906146da9db8329bd2786bbf92df10445ee093f0b1b2a640cc2daf003fa7141435ba1dd54f9cdbf5417fa7f539b255452852a85d2ce97ce5abed4980e7b409e283f97ccc9c01e104b55155f96ace6789f61c4661962d34fc5d7e6f5f5233180933b2fa7f7a5b074714645489f5221966160946b7bfbf0fe6733e6beb8af4457b9d36cde1200811009ec483a9d730ca980aa28f636942af5e89794a8edbc1b75d555ba134974374d0fe23d31c26566064eb9998d649bb2bf066bf710da50672f4e3ab4df843a0c8942bad0a071c237d4c1759eca37380919e36aec73284db202a32d3d1619f3e5b757b2df8b04bde567783dc8e465d996799782f1a1b8de9331681a35aa04edb427de87264c8ae9c397f29d3e8730db91256425a10b960a9de1a48d0d4186d617d2b69c87e2540f6570faff4ee1f6303d7d281434947abeaad83c86a4d25bef4de2bb3c6104aa0ceed7c8df039f4be6a42851a118adb1b8f98e02f6727b75d98541bab2ff24fb2f20342e86150c678941825409b62a844f44ca1ccdf0d9f7c2cf9b222fbed00bc92be0802fbfbbeefa71c8976cba8fc4aeb031480f434027b1cd593d08cbc14c2a360b736b06b5afb8da35f0be3818fff4275b8c830f5248a8b8edea1327454e1360bd90d4fa08e965f459b0b027e1180290cf762f813a31e8109f472d9657b03af737d1f7bd2e59441541a84ba818f1413c5cd1f8b9882e9188e0def9e44e2f4a7c710c893c7188ba86423f8ae86068d84e1832af548289e87c34d68b186df7e24ca5b051f8f5e4a44e2e7383ba2a09615b4147b34e86486731290ea67f3be24c13a9c5cc37f06555989b3f10c580a9cd2b416d0ee4210855c6833a25996761dfabb036f3893cff7db7e310baa8faa79f46e0ee43bf4dfd732eae7f44bad2e7c032b9c6d14947af6b0e37e5ec98372a622f716ffba0cde04b9d4508392dd154ddc34829412bfa604d4f00e4b10a553587343ef5c0944165e7ee1e34387b09c147ecba943cf36dbc4269efe50ec3a5a3075c43be9651d6db6acb9f657476952b78c990557f05935247a71077373ec436ec586def177448f8859ba096b7a838e5b4ce7a463f9082f705c26d99936eb1be584ea9b58a44b9b4faa07fd8247fa66cf4529d1b8cdb92ed7bd96bf0968db4376489c7d46f0f27d58ac884c29736502953723ef1ab41e19c7041d3e0e9091d7de2e3904d032de02292edb1225a672ab438d3c65f7921c06a9f181f8ffda4ac524d0e0fffd05aa5d10939a524d7b2ab225cdb41decfc129208c372d51ca1db705bb1b9831e53ee000000000000000100000000007a1200ffb841fb291ecf09ae7b5dcec1feeb45ba196ca02d6b9e50ae2b3dcd9ca5d72500061b100003acd0a3acb9b4f9540678dc9324480bb3d4f54e5e007739c9c1d600bff75dfbbd0191e75c0a2d810a6ec5b03d02cffbf1a66123c87790e66eee8d416caf82e7ea7090a62fe14276fa88af32ab9793f7a100de5525eedf71967c13c8d361246d98b78cc74383e714f88899f34407644c14874046b3b722186015c07b8db042db955c91cb14abbf3cfa646aea81ad15bf67108a763539c64c5a8e8115d46e063e956671e8ea8d8fd638a6414d71e9b475ebe070da7faf75a898f29048ab5a2b6c7e3a72a178b8e470e8375f539ebf6284d15486c5a8774d46ea164ba2b62181f47623fd987ff5958550c962a193638679b79fcd477fdf2a09c0fa879bb22cb493fbaed27518f5cb265741535b4ab14246077ca18f11ece7aaa0e01ec5bf02c3c3b541ea08bf254df123079c1538e266dcd3161bf1b9ed41d873f1491906e1459ba51ac9dd95e783598d3c356e0cc5b98c2a96b148f55f102a9810181eedd46cd00b445d861baeeac46eba469435aa4ddc877bd68b53f4d005aa2566d356cc344aafcdc86abd774ea28cc838d2fdc541c4b6da494a96e128b8c2abab4b21b3ae2646cdcc3528ef6fd8587b3a0636ead67a62309fb003afdc14177d329b062622313e9dee912847763c68678df663a39b89c69efdb6d916d5754534bdca9030955cbcbae6fb7ff1df6282175cd37a30a904418b976af05809f0e0e7e4b4e2ec018f1e9c6bcbe7a7822c8699669946f5e684671d63e68cc7c9cca2963945dc21c52232e6f83b1875b2bed7c80c37371a480a2e5255d49d390c3b2adfc695036ed91371cda7d79bdfbae464581f0b32942f03826aca17ab9da6ade4a778d310ec3da17fc3af426d21b347aee7c2db7b5e188e35714dc514e3a1c100e8595c9e0e4399ad796021976f077e5733ea535cc6daec2e371853dcb715fc366ea7d6b9a5b3509dccf5c2e1225e3a51de9f5bb9b6586b282a0b27a9ae7ae8f2be14ec677670241e384b462eecfde68957839b1327c9e5c622c0f67cdaf3845ddbe6f754401d720d6b6d5c061dc906bfa70fb76e1168c6ac1a25cabe8873c3c1e540ae44ae631a2638accd7951f368442dba7b38d0662ccb0140d1e4ca23f51de731a6f5adcf816c3235359afd607e58948da29a5f06c96b4312aee7d35ed4c2c811a58c5a196ac2f377d653d51cfccb5213c928955ac880b5fb1b91e88a52d5c217cbf78e071275fe626c230fded548b0f1667af1309149ff74c5542119d4e269fdc1b241d9f53e02e38e015b7c5c2d2ee623bcb4167e37edafddc7fadd642c20f81b454db1a3b578d527f124dbd1f3d99fdd1590256ae4e47c2e8b3bfe8708a0d5506d6ce8b130ce6b70028161454a5065e9925d75c0095dc24ba789489fa1e9236e25330ca1a45e61224ee027664f6589028a240961aff09187fb719ff3477b56427189b7b3c790b4031f6539c5e3a8a5d7fd99c2534ed1646920a43e7315bba98d59c51b337ba7a1b038006bb574df46830f96a5685e07ee8a0a41e712810faacfb231c67e69d0fc24b98d782c70e15524d5d2dcf4e64b4e26772dfa7067f6dc7ffc8b06e6ef3ecb13f927d466e0cd3a7ea09aaed90b7810bbfdcd8a1274bbe78a453ffff11ccee62059ec25955b34a1a1cdf8a3e506d99dcbadf16032646117556ad71cd93ceeec42be0350a6c9f194fea783558c42a56d034dcaf6fb1b28037362c7c6e2446bcda71d0a88adda3144589447ef13cb85a4d2cd16ef444097bc03e32c3b1a055e952f7ac87078b04deb900375a16dbd382ea4375ddc0a9645deca620590337e803ea8b41337f3f4e4030119a2337424dbea3d21214063ce853843dd4e6df94e3dc3bbb36d89d9eb8e15e52d0699bc6ad1de9d12afc95c8785d63756576d357126e13b25502d542774f6e5d2fdb559d52698d086fffd05aa5d10939a524d7b2ab225cdb41decfc129208c372d51ca1db705bb1b9831e53ee000000000000000200000000000f4240657c0bd97e795218aa623b27cf9a71764379c4762fcee8993aa0b1ab1e32194a00061b100002c0ec5d388a8c78491bbd870faa2c46e4282e11796123b69c2792e5806748ef068397f0212a33e3f79162ba4ea247ccec410db039dd323af48ac27bd0ba77ecb870c587477d4543d9a29c53fc02bc98d7cd0144c7abf80b999c22b42a28ae8d625478ca304001f9a49782ec970031e673c76e4e27357a321729d6df38d1d88dcbf764c69eda3baac9739bed637010f44638cfe1deecc56b76f6e02d1d0c3f104462b9ffa5f20de4cf092d86a5bb35d5f62b0fb1c983a2c06df17c9ffc809c83e4b4335f5903fab536fbd9719847bb063541ecbe05c12ef8d058b3547faca054e3d662250f1cc1f925dd71297abc25fe37ab33a086759fac76208a64552f84d2e4d84daccdc3aa2bbd2c2f922bf262596742dfe034529d1ead2975dd3d197ab0e2e1c75c8b8f160ca6077638022d4afbd107979949cf342cb399347f3990029f0db6d9ac0c569d61d42539371f9a7ff59e9c83ff97d15bf0eeb254ae58fb7b1f9d8710c546ac8a227930c66ac841bc4f475229e5cadd14ba5a01e6b2da99c55861a08e2100e62c4499d30003fe30ddaa347d7a27c2158d3787d58fe51ae57d797bbef7f900508d1580df3e5233f0887567fba1faa918c246d2ec5c3b7aa022cb8a652d00b4d719e312482f57655eee80a90cdc73151fd7ab9c5367793d60c6088fab98f0547d7f547e10db202a25e027a5cd0abc41bb0e3ef563c0a6d469a702b2a26f0e8b4fddb845a16a5f06b9dee33c3adb31430c94942c5023179d3e4441948a332069a1c3b69dca65f05a43452e42fd28a2f7e6344f98ab9a7e4eece3c1709be1f7bb620f8b6c45989a8bccad39a4bf40e8215183d1449196f1f9fc17de778b616856152e6e6145a1b7a3d7f226becaea5ebe34aa4bd06e60f0fed207bfd21f5663bfadd37dd722437bcd46a26fb4e19d062574a81bcd817eecbc1914a5878809128961acdd73113ae9c51070ff4494e16d81ccec777eeb513da82bf43d4884812b26546b4370dc315793271b069f60f4285f648cf122ed8b22b0c7a27e94ccd59a273eb774c109e19980e146850de95f82cdd8aa0e82022672024c917b281422d284df0ee0bdeb3d4ac56b4ca675ebdb835c17b6a822d79ae7310f4aa41d80ac61c5e45c1c0e1d64542622a31091a9f87c335e86d964dd85a951d7c9bf41c9f2b1a9bb8424d7d1b26413da8034182fa42d2b1cd1f8745482c49d8348d19c72cf5a02bd28e4cba82128af8bf5d9c1215c4f543ef4d185f100f8d803dfa29c300c072e44ad9542b82fb1380d55c15c9a4b4398876e2450b90b49990746f339abca8cc8a462b62329a128758ce0e46b5f998af1bb485a3044bd125424eb5c623afd2a11befe4ec544eafe275ed1ad82b940dad5e9a9710d48562e51b296ba81f2d70593685ba0e3f3b25089a187e61d5675dd481aa99620276cb0a841a3c4df201a929287b1127270c5d25d06fb286dae1a9a5a5cdb60003f0c30d2021074bf252e550685f7b51a087a77b0871e883104e55f898aa5bc4cf8538c293253a737556d7f220e15b90cf0eda7d5f2172372e3c50c12cc588f312da37191b5038e944825044b130bff281ecd47a4252a1411ab7a9305c2b37e9facd435e9c434de37641498f8e4bfa7b42966da29c84200aca87ea1c3b00db54906b340e524a7dc4a15403bb82bc24517cb91026096bdf18f5f7ae5640ed6de1f0c5d184813d6b9d244b32b58e9ff524741a39383eec3a530d60db13deb26e3523a725f0599b671b625c07002704fb600b77318417d2527537359d122e22a1f7581eaebdc19e65ba50bdda18ae08e9a8694fcb0ff1a2cd98d910dbcd52064c15a4282d67b278c72a0fdbf228abf6b519dd28ac21c57d1da4bb7ad5b5ab10da6b83132df1da79ccfc77fb45598bbd91ef5ab96d8a2ee148639a562debafffd05aa5d10939a524d7b2ab225cdb41decfc129208c372d51ca1db705bb1b9831e53ee0000000000000003000000000007a120bf5899359b258acdec0ea500e437267aa6ba18e8509e15df74518f6ba7aa348f00061b1000036d768d1d63f1c9f09c252b5b48cf4d2db3f4935150c551391b37727723168982d7e58ce2a769313e461a0f3791a8c0208a769f5bdf4d57fbd0a9de104a0b1bbc1ab25ebbf87d1bc86dbbd42fd0dc0ff18a7ebfdd692c7dc3b95d095052540ce7801f3e25cbbcfd9dab857bc39624f59facbfacef5ab1e1e1f889b85f3b1f2a580cd660b73aa662fb15487722bd3c3f93d272725289136f2ee0fae4efe1afea25e6d4fc8334a47ad62d22be06605a15538dbd2a81311c4489b91d1cb143743e4570a6345c4f035c060aaf287ef66e7ebbe7b9037c10b66e087827478fdf76a02d25fe90e0f8228c1edfcb12eee3dd4e505a6c5a7bf2f5954ebb5560cd8c7f8b8f3f4ddca41a48a0d6c0e1092dcadc853752f459486bed349213cc15044585255842717ab70a3de3f0eedeecca10112c85a8bc248dc66c883a62288b49588d9fcc048c51081e94d65bd4731e7d71fba13693b82d2831d3bea370918aa5ddf1cc4f0d5015bb8dfb951ab13fbb9d26d5cb83dc980fc36f1712d616ce24d2c530253320f4f322f093a605ea426c577544f2983efc80be56791f443652c2233039a68f966c2f0b6351068616755dd2036b6226244d394a2b5b59160217603149901e8abe19a2bf404f384c2ed7fb0c5e470ee5ee8561f58f66bda729c2c8816853ad2357a009e537efcb4a28e845ca616be917b15aa6b8eb280bfcb321ea62fff21168b8ed54d58ccfdeee0e7752bfc0f02549d76615c85dd1e152a85ee931b34f436439e2233740328ba504c49f9764e1dca645ebf6a1377310ab53b68b4d0a6e6c952068249b86c29061725035db8d294ab9c56901485814735aa2a8d6987b1a19ced65a332f97751c4cd8a27093851f7775e5314078c04d254754d976bed2dbd2e6ecda62e9a0c7fd95299b4b13a54c9498d384210fb42d3b6bc5d8f0d42e42879f86c21eb7c5c6d1bffdd598b8f3cfcb75df159f1125a65f960637c62c7c5632d73b7b4b0544082008ede22d87e79e20eb08be0817650fefcd111de48ba2be02a7b080275c991a0ee4445dab89312644c7cf4101895e2dbcaad7d87e8e3b13e62751861b204a7e6f5a476eab0817c294d59aa0247903077d4cbe4a98e7984d2b04623d2b2ef4c650b43db15541ede229c12c045529b5c77993eb6acbdc28d812a486b5957fb996731980555bdd59ad824a882ebe1a77cbe6b9035f1c69dd01b2a27a47be5febfa65c721354e70071b07db4ebc2f01d143587c1b32a5337dd010d2a76a7773f4a7c665b4cfe4a61b103b1c319d85e007eb99b52400cd8776697e1d6118197655bf7bd0a5e7f4594bf36a2706128d5f5c3ee166b586c4d515611f4597a4c1088c1853a5959f73830cb973ea922d6211ee7d9b1d67b1025486f8f3c72a517d0d48d9a57d64c0f48e513c3b09e14ce91b515a87f3035ab55d241ccb12108dd299a362af26af96ada920202dfe26d456065717a85e6bbec540637059d82480f6c917a11434a9be5fee5ba33a8552b7b0e59f123991525e1dc14bfdfd109625b2df477bce565045375dacf6ffe99081914fc9f64df7ef8eb26801ce01be083555fd2f8a338a33c07f484b01a310ee420f5c932cfc5a8f6d32a20c3915a188833ac5775500c5d2b73e4ea595512e869f91f83de8e048c804ec8cd6de45b7ad264bcfb3c235325d3c09673a2a94e60736ceada065cefb1e355bd35bd4b56ae513343e85a3d840c2211da2d1941112318f97f825e1714f74fc2d33430196cca2423f43641b0d75b4b1a1a1ef0f07af31fda06220a2628aaab9f303cb4fe6d73fa9b39e8f6083af3bbca1becdd646afbd888c560bbf7cf4fcf5c02f712726eb23b03fe5c290df63a85ed1026cee07ab0d2bb868aa4dd6594252cb75ad5d11d9c53c5238047ea3d311c1443d478dd283a260270400002710000000000bebc200000000002c232220b8e78d93ef82c5351ede6c6aceaaafb806ef6ff23deb81fd78e7a9bebfcdeb5302f6bed70479b4d98f5c93f32b382524233983ebebd14b2b50fe35142766300efe000000000000000000000003fd05ac00805d10939a524d7b2ab225cdb41decfc129208c372d51ca1db705bb1b9831e53ee000000000000000000000000000493e0f81fd474bea478df0202b311c69e85dc6215f629491dd15ad0929faae2535abb00061b10000370b2714d2734e6b8cde085794dd7b41c8a9b6c03c1edd8e3db168ee7fce39493596e882b18b5b1b79c16400c6762b9856075821be6be9fab8f469f56820d8f341554400a8da7f1f1a8501902581d43b9fa6e5c68015716f718a2190b87fce41bf1b509aa61806394a42489d63c457fe4c79e7480eced1315edd731887e57704fc9102f50cb7f0d242d755cfd5a2172dbaf7f01b124861cc6d1dc804796bbb84165c805f0c0f3fb9ec97b74c2a694de56a9cf8d79d1a679260ee1169d78214b34c8a654ba22e59ddcd32beff4713de33549f035b342660405b0159a7a508e5691ef4805689140e72b8a0ef2e61be74dea5d0b8f5589e0e373cac2e2e1cc39b2121c05cf4122ad0f8b9af6fbf1de2ea26376c2650ccd306c13a7b64acbf2a3feed128754abe44658009e642768ae3d84f5e0fa5f7f360c2a1c76d26985817ae77b71fb59014a5483ebba9271cafa5e5d8031c569adeceb8bae6444e98d2522b28f6682109fc7d31cdb83ebd45e5d81e7f046df42345b49f470dbef9ed87709301d2c6131215d33a30b8d18e63e54a2aff85dd57672f8198bca6a67ee147c7d0ae649e5661ab6bf78a662fef9a164f1e332b9f16e6fb3d5769ddcbc1d1c07338d3394b9245d17618c2474e86c064fca4df00ad3a93dc051fd8c3328cde2a987798b0f22a21c90426700abeb1e6f38dffb485b5477ec44c690fa80e317b32a982fd3082253bba8595783290dbffee4fc9296ffdf16a8bf3154971bb720e78674969e9db2e0fbab9e9e13f24bc8b3af5e2f00f262f0da56de443f70398ab68f747d35370fcd8e1c0e130f7269e08f862b5a67f2c129be254df2358762ce3a947eb27d66450af51540e7721b47c8a5a86098ea64dad381f14e07aabbbc470949a99c07612add3ab4c575fe2e520bbe511a1a674aea37a44535c13ee3380f8f39bd230fc1481cd31912af36c6751e23c6f383cd37a8b13fa7df9f0c7e460739f2c6226638ee14f14d36366211cbc6a1e16b4856bf302a540aa9d9e833b1d59c510473096384c8b450f2f3f1dab9e614af822949d5cc93d76bc4d1a52891bc85f1981ef83161195ab7d8181ee4fb163bc6c685a10e87c7f4b15ed7d05833c230a4a5b63841fc65b959f0ff010e697f47c583f9b7fa9b389c0eff6614e47d85b83c483136f182be4c151d272f5d938b912a95e47d333e5de6a409ad271679a778a7eb3f169c71525302fac5d4575e2645c09763c2ef165736a7a726ca605038e2781404328790ffaacef2b9c2bf90122042cd571287bc4e3973da65fbd4e3da9e40e4347ca6eb4ef1ffef4e5a34be80425cae3e81533f7f2953f95fca53a22057a39125f5c76350fba7fc6c036838fb951d0aa8702e7f44c6f8a9cbce3b64fa8ddc2bb8c8b35d1e29a21beda6fdd332b31a749321455277231fd9d70ea4aded95053b395f88fa6916d126e1626fc0f1be6cd2a9538d17c498b40927f12b3bb40fa3e272e82cd2242b670afefa387470f4e6e0a1236028954c9e90311f486617187956a23b90b356d71e219e6dd055c2120771003a6c12769aa3ceacb9642bc01022731ca7a413b68ee7d1d5444f75dfa51a68b74a01ac85f6ceaf5e56987b9d67d6de896f5aafd25c78c413a6d4b5b03d571167524cd231ba13bd9f80fd7413faf21e8170cef0d08b242c5c38a2b0158da56e358ba0692f670d4611c7a3624b234adc30c5b7198e0afc941f5d13eae3a94ddffa652c784c34c582e04e948da91a5ac3038a9df38fd4f1733779f4f122ca2d7ff9d03bac9def35d9ee3a183161f8f2808d472b2e64581209359cea58ca7757164c666029982223877e2b14d2d537afb012f1ffc12cd083c16dfc64213c56f3d4d22b603d3dfab1d21e239d6fc1f9f153ed61f1ac91c29c85c16f4aa2985f84052f5a08d32bddfd05ac00805d10939a524d7b2ab225cdb41decfc129208c372d51ca1db705bb1b9831e53ee00000000000000010000000002faf080ab009f4d0b317a141e5b3f865599676f895834400b66dd09d33ad27163ca176f00061b1000028fe56ab181f454372c939ca0516f5782b26cec2010885c70e55c41a1e43de3af6635d2ed0bed90837cefa9f1805f4808e8092a4d44efe5fb616ff7678487a460b367134fcc728ba273fb1d22718bc6a95e47a120a6d952c7c2cb8f38e59c2a4efa63977cced7e4b8f46e4d47d29098a73beee807c3337c4acddcbb32b78eddbd124b2f33ec6cd8bdd364aa4ad2c10eb69dab808fe5f5f0aec19750e51ac65a8746f345c05d4b8823ffbeadd6200ad01449c39a008fbe117a8ee904445488811336d0c439419fa4f285f9f62a34f10b076c99c0092968e3cc9fa656016b6da049bd56b910d7a9356e76d24e746b280f0275ec9e9bace82d852bf0a137ea02d4cbd3b68450bfb593564d8c20953bb758890a55a8c381a4b3303b61ec26a56111361bf7262b3f6f2503aad06758130d86ea607cdbba53415aaf253430d92fdd81c685ab39233e94654e6508eec1347747e2df2862169382aef6f99dd78b50629c5d98b1fcc73e865679d862b42f8e9d54ef6288ed2c3f2713f0fa4db538cd3e70ec1a30cd65dbf873f581b30892acedacd39b5f0aa774d1f3f77d8fd11ed628bcd02ac33f89123595aa455ec54a07e93e26f94338fedd8bb84094a0add52f912ed5f9019e3a28d90d251cc6ed7ffd35254dcadd9f1e9b28eb0e06fd4fe961d60cb690a7757f475c08aef07c2e54668121540a42a9c779623709a2124629e8c4bd4021763979647f625b360a4559dfd3f57798dfe5d36e9d902904af3ed67d8f4b0894538c7718f5160d211cec27375a7e6a2ec42f2c8fcd1c953b7b8379d42439a2c6b921a66d5102ceb6bd6bc20b17098e69a0a4f708b42520e4792474c3d115a12c83ef60ac6e69d8842c5981e9a6d178efa352e73e4a34bed4fb590dbeecb259617668e6ffb9f955297f26e3a6a3b95d9617529a61f08666ca1069d2ee1876337d3e786244c5bb45a8236577184584cf3018118d7e4e78973ee510b6773bd922797e580cd240dea3ca31892d23c1e6e4fa92f1a01da8ea40044f5613a9429ebe7906f79b32636204d025115810b376d4c6436da136b96c7c10649e3290caecd6ca14d995a817e3725fee7e621c5366f80c752e50aeffee1af3361924f31cbb1cb44731d19963ff30127ca2363ce15e50948be14c43400737ee8910ed06027599da74b06e77eb82ac523cc031c57c02dd82dbc0d53629d072615c92034cf829e7a5d4437b1f58e2bd4b16993e1e1b05c26ed8d695351db11d21df36a7f5811ef5fe001ab1e1c6ce9d2b69b6ac3af8087e6666317f75b645e3b1caefac0eb65327fcb9fa62be341c99f191cc869e48dbc8fee3e42d4393cbc6505c880dd6739a69be4f7ef3de306480a7a51f413d310926f252ea96a0c772d8b8e94e7d6cedbfbdb21fae2ffc379eb17c2680fa2bc56a8726c93e7bf2d446221ce95e49da93d29bec8e53ddcfd262c33d556c2b8921c3de93236408b462d28612d3343fbb9cc538b1e6b33c341c3b91dd41f936931e61f146fd00aee1c5c0de97b47cf7efce889012e1c22dd8faf0fe155f4e9930c27941d8b0907502a835bfffff801f6835de69ad33e95232f773219eec0e2374c421230f323257dbc91629c4ca8a61584f737b827fe8e8f5b69b88a7b64b362f8142b043f08ea82c4a0ab7c4e0b9805533e806f90597095242ff64f314801fb7ad838e98e1859b2c05c9b027ae5a4baf780d15977bc1492dee9b14b1cb0fb3243eb2304919486fcde89a3cf35a64e31b1698e35fbb8528a73526a19189d406272b8becec94379f69372afa99d06bb4f34df72e1c3b49557855ae8ac265160bdf48ab34cde30d2665891cfeda24adcc657d851431f38953f917f1a111f023d2c71845ccf25a562d2450bf8b4986b64ae4fd09e5a8ab9610d11fd68e9d570b3a467780236de974c7fd05ac00805d10939a524d7b2ab225cdb41decfc129208c372d51ca1db705bb1b9831e53ee000000000000000200000000003d0900559bb949364ed92a64f449cd6ee3eaba2e607595d3cecb36b06b145baaa69bc000061b100002860fd058656f185036e81a64fe19034d29223d3620bdf4bf2ff3c1def9c6bdd70c2cda7f660e1b202672741bc3258b04fb755fbbb1350261e7992bce930e256bd5d8d4fde61365b7b24904b788ef2040fdd6eb87f97c2d2f29eab4291d9a28b5da306f28b98d01d93517b203a028199d423a3545aa17522c63247f73af7b63335b0b48e4b875c69b42f4cb1573bb3e5fd68837f90c50b0161f067a9eafd9a0790e53ed5053ce60ffff410e4b16a4b7bc5a52c57e78ef266100c9f79753f81878c08e5dbd4d80c6e46a339578d8ac8c572df77ef614800cedc460c06878c0da97908067729ed35e3afe919071724f89ab736f5791a9c9b5d422136332213434c836e2ceb9fce0e2e96a9a6d7befe8c132867d5fafea1a7809ddd6b3a89c8ef6ea83028d3e2cca00f1bc6e12ea8b67e91a98acaa2edebaf6dd3a18c655b6b1fbbff5c641f8002780758d05f1f39c9470a124a5add314abd2262142733120747cda2f1d9eb90d68ecb9c7fbab23d73a35f2a20a2a365de6cd678d53bd5bd9bd518333d04e8e678b5d08f028982dad08c80be7d8fbb0638dd814232224c687f8321baf96ed8b39a1e9ab52dfd69d8eed79ac3f5a2c480a585bff038c92b367743317b937d969cdd533ae1d797a789ff7994f86a0d6cae470b64ebddbc478573af347a110dd1feaaeb4779441ec439cfdbafaba870105efd86b9d85a4df7ddb9b09f5b6b4144cd1fad5932df37ebf19a62648659fc1969142310a5cc9b4d0c48ba6bb0f863ed53a0b75fe1ee6515a46993f95be2e34166408b54a43e55c4802b37ac902fb4c8367ce38990d07ed3104d0728d327d3b9de6452b520f9af534505885788109ec78c1176ca0864d28422e826cc83f821b7eaf028d6a7e350b3037d0fe58d1d4e18113c8f61913932e71c0f334402534d8663f15445f900fb9dc6b3a93223868167be26fcbd70c0459eee37f81fd539c319eb0b04bd478b94b5f4cd23b4d496c2bdd6e8a154fd76c4ecbdf7647fe9e7be88c6a3a8e7696e2e596dfc25ba798db6ca331d135e9ce7c0aab9721d3f70ca53354f96ecd028236259b9b0d9e0bbf73c8e841b1d4276214f7be8feb525c91d39910b0e091997a2b89e945806e93cd325cb51463b0729f1a519334038cba09653799ef533a49e812e86b81af7e5099a02ca11c2b17dfc8b9e51a57a20546f2c92826676ebcb4f64fe7cc77424388dfec7199179cb125bb4613c8bf05edc4173987d7d5ae0fcbfa08a1e5ea2d6b01406d740b49c5b1a68da585549590c3ec13479efa3136c5ade68057fe173ace55593ceca8440372b03f332969866d1bfbce3fe9dd907d27593b8b2ad25eb4b12afd0f3abe7931ba7789c84a3ed65a03df06d998a9956043a4de786c359bdd58f0b9e5cfc32bc709b626ce8e63f3997a0e9f784f6b94e342b4710553e805cc8399191254189058ec75a15556467b2456d9b38a7e4d15cf59727ad2c32f0daaef1748be04311ff484479eb31f7eff9e32b816fa40f40430e801c15e931294b39ec4d6c93e8130fb4a1b53ca6471886a33b10b46d6973d02bbe3b39345e121473ea4ba41f3cfced07c3a613393e38b8ca01c353ccab96128093bb5fd000978b11ae6f7c1fedf48c3682119d1b44629d02ea3800d2e247ef1e78e527e5d574b4dd144e1fe1b0c6551442d419baba300258657792947443747b29b6fb9417d3c57536de6aa379c02f3addbd2066554189ca817c7f57331c972c30e3ed06fc7e521b8e2bb57c023dff9816f260a153f1ffd55df2a2fae568f3dc734d62ab47a77c54772b5e4cf2758c912cb473ec372173d9eda0c58b103ab12d11dcb976a40816ecfa8b9c9384f6415017555933652e77ae20e14ea9ac25b5811fc03364c4883bb78c82e29f8a8892415ff652b782642bf6036b3e2200e0000000000000004000000000000000300040000000000000000000319c0569a7b4f4021827771a963002b8b00000000000000010003d56d81344fe44d1f8f1a95850431563c00000000000000020003d33fd4c8d4ad4ce3880bf79a4856ca2100000000000000030003efcb10168b144c8bb4f694a3b98a129f000000000000000002000700fd05aa5d10939a524d7b2ab225cdb41decfc129208c372d51ca1db705bb1b9831e53ee000000000000000200000000003d0900559bb949364ed92a64f449cd6ee3eaba2e607595d3cecb36b06b145baaa69bc000061b100002860fd058656f185036e81a64fe19034d29223d3620bdf4bf2ff3c1def9c6bdd70c2cda7f660e1b202672741bc3258b04fb755fbbb1350261e7992bce930e256bd5d8d4fde61365b7b24904b788ef2040fdd6eb87f97c2d2f29eab4291d9a28b5da306f28b98d01d93517b203a028199d423a3545aa17522c63247f73af7b63335b0b48e4b875c69b42f4cb1573bb3e5fd68837f90c50b0161f067a9eafd9a0790e53ed5053ce60ffff410e4b16a4b7bc5a52c57e78ef266100c9f79753f81878c08e5dbd4d80c6e46a339578d8ac8c572df77ef614800cedc460c06878c0da97908067729ed35e3afe919071724f89ab736f5791a9c9b5d422136332213434c836e2ceb9fce0e2e96a9a6d7befe8c132867d5fafea1a7809ddd6b3a89c8ef6ea83028d3e2cca00f1bc6e12ea8b67e91a98acaa2edebaf6dd3a18c655b6b1fbbff5c641f8002780758d05f1f39c9470a124a5add314abd2262142733120747cda2f1d9eb90d68ecb9c7fbab23d73a35f2a20a2a365de6cd678d53bd5bd9bd518333d04e8e678b5d08f028982dad08c80be7d8fbb0638dd814232224c687f8321baf96ed8b39a1e9ab52dfd69d8eed79ac3f5a2c480a585bff038c92b367743317b937d969cdd533ae1d797a789ff7994f86a0d6cae470b64ebddbc478573af347a110dd1feaaeb4779441ec439cfdbafaba870105efd86b9d85a4df7ddb9b09f5b6b4144cd1fad5932df37ebf19a62648659fc1969142310a5cc9b4d0c48ba6bb0f863ed53a0b75fe1ee6515a46993f95be2e34166408b54a43e55c4802b37ac902fb4c8367ce38990d07ed3104d0728d327d3b9de6452b520f9af534505885788109ec78c1176ca0864d28422e826cc83f821b7eaf028d6a7e350b3037d0fe58d1d4e18113c8f61913932e71c0f334402534d8663f15445f900fb9dc6b3a93223868167be26fcbd70c0459eee37f81fd539c319eb0b04bd478b94b5f4cd23b4d496c2bdd6e8a154fd76c4ecbdf7647fe9e7be88c6a3a8e7696e2e596dfc25ba798db6ca331d135e9ce7c0aab9721d3f70ca53354f96ecd028236259b9b0d9e0bbf73c8e841b1d4276214f7be8feb525c91d39910b0e091997a2b89e945806e93cd325cb51463b0729f1a519334038cba09653799ef533a49e812e86b81af7e5099a02ca11c2b17dfc8b9e51a57a20546f2c92826676ebcb4f64fe7cc77424388dfec7199179cb125bb4613c8bf05edc4173987d7d5ae0fcbfa08a1e5ea2d6b01406d740b49c5b1a68da585549590c3ec13479efa3136c5ade68057fe173ace55593ceca8440372b03f332969866d1bfbce3fe9dd907d27593b8b2ad25eb4b12afd0f3abe7931ba7789c84a3ed65a03df06d998a9956043a4de786c359bdd58f0b9e5cfc32bc709b626ce8e63f3997a0e9f784f6b94e342b4710553e805cc8399191254189058ec75a15556467b2456d9b38a7e4d15cf59727ad2c32f0daaef1748be04311ff484479eb31f7eff9e32b816fa40f40430e801c15e931294b39ec4d6c93e8130fb4a1b53ca6471886a33b10b46d6973d02bbe3b39345e121473ea4ba41f3cfced07c3a613393e38b8ca01c353ccab96128093bb5fd000978b11ae6f7c1fedf48c3682119d1b44629d02ea3800d2e247ef1e78e527e5d574b4dd144e1fe1b0c6551442d419baba300258657792947443747b29b6fb9417d3c57536de6aa379c02f3addbd2066554189ca817c7f57331c972c30e3ed06fc7e521b8e2bb57c023dff9816f260a153f1ffd55df2a2fae568f3dc734d62ab47a77c54772b5e4cf2758c912cb473ec372173d9eda0c58b103ab12d11dcb976a40816ecfa8b9c9384f6415017555933652e77ae20e14ea9ac25b5811fc03364c4883bb78c82e29f8a8892415ff652b782642bf6036b3e2200efffd05aa5d10939a524d7b2ab225cdb41decfc129208c372d51ca1db705bb1b9831e53ee0000000000000003000000000007a120bf5899359b258acdec0ea500e437267aa6ba18e8509e15df74518f6ba7aa348f00061b1000036d768d1d63f1c9f09c252b5b48cf4d2db3f4935150c551391b37727723168982d7e58ce2a769313e461a0f3791a8c0208a769f5bdf4d57fbd0a9de104a0b1bbc1ab25ebbf87d1bc86dbbd42fd0dc0ff18a7ebfdd692c7dc3b95d095052540ce7801f3e25cbbcfd9dab857bc39624f59facbfacef5ab1e1e1f889b85f3b1f2a580cd660b73aa662fb15487722bd3c3f93d272725289136f2ee0fae4efe1afea25e6d4fc8334a47ad62d22be06605a15538dbd2a81311c4489b91d1cb143743e4570a6345c4f035c060aaf287ef66e7ebbe7b9037c10b66e087827478fdf76a02d25fe90e0f8228c1edfcb12eee3dd4e505a6c5a7bf2f5954ebb5560cd8c7f8b8f3f4ddca41a48a0d6c0e1092dcadc853752f459486bed349213cc15044585255842717ab70a3de3f0eedeecca10112c85a8bc248dc66c883a62288b49588d9fcc048c51081e94d65bd4731e7d71fba13693b82d2831d3bea370918aa5ddf1cc4f0d5015bb8dfb951ab13fbb9d26d5cb83dc980fc36f1712d616ce24d2c530253320f4f322f093a605ea426c577544f2983efc80be56791f443652c2233039a68f966c2f0b6351068616755dd2036b6226244d394a2b5b59160217603149901e8abe19a2bf404f384c2ed7fb0c5e470ee5ee8561f58f66bda729c2c8816853ad2357a009e537efcb4a28e845ca616be917b15aa6b8eb280bfcb321ea62fff21168b8ed54d58ccfdeee0e7752bfc0f02549d76615c85dd1e152a85ee931b34f436439e2233740328ba504c49f9764e1dca645ebf6a1377310ab53b68b4d0a6e6c952068249b86c29061725035db8d294ab9c56901485814735aa2a8d6987b1a19ced65a332f97751c4cd8a27093851f7775e5314078c04d254754d976bed2dbd2e6ecda62e9a0c7fd95299b4b13a54c9498d384210fb42d3b6bc5d8f0d42e42879f86c21eb7c5c6d1bffdd598b8f3cfcb75df159f1125a65f960637c62c7c5632d73b7b4b0544082008ede22d87e79e20eb08be0817650fefcd111de48ba2be02a7b080275c991a0ee4445dab89312644c7cf4101895e2dbcaad7d87e8e3b13e62751861b204a7e6f5a476eab0817c294d59aa0247903077d4cbe4a98e7984d2b04623d2b2ef4c650b43db15541ede229c12c045529b5c77993eb6acbdc28d812a486b5957fb996731980555bdd59ad824a882ebe1a77cbe6b9035f1c69dd01b2a27a47be5febfa65c721354e70071b07db4ebc2f01d143587c1b32a5337dd010d2a76a7773f4a7c665b4cfe4a61b103b1c319d85e007eb99b52400cd8776697e1d6118197655bf7bd0a5e7f4594bf36a2706128d5f5c3ee166b586c4d515611f4597a4c1088c1853a5959f73830cb973ea922d6211ee7d9b1d67b1025486f8f3c72a517d0d48d9a57d64c0f48e513c3b09e14ce91b515a87f3035ab55d241ccb12108dd299a362af26af96ada920202dfe26d456065717a85e6bbec540637059d82480f6c917a11434a9be5fee5ba33a8552b7b0e59f123991525e1dc14bfdfd109625b2df477bce565045375dacf6ffe99081914fc9f64df7ef8eb26801ce01be083555fd2f8a338a33c07f484b01a310ee420f5c932cfc5a8f6d32a20c3915a188833ac5775500c5d2b73e4ea595512e869f91f83de8e048c804ec8cd6de45b7ad264bcfb3c235325d3c09673a2a94e60736ceada065cefb1e355bd35bd4b56ae513343e85a3d840c2211da2d1941112318f97f825e1714f74fc2d33430196cca2423f43641b0d75b4b1a1a1ef0f07af31fda06220a2628aaab9f303cb4fe6d73fa9b39e8f6083af3bbca1becdd646afbd888c560bbf7cf4fcf5c02f712726eb23b03fe5c290df63a85ed1026cee07ab0d2bb868aa4dd6594252cb75ad5d11d9c53c5238047ea3d311c1443d478dd283a2602704fffd05aa5d10939a524d7b2ab225cdb41decfc129208c372d51ca1db705bb1b9831e53ee000000000000000100000000007a1200ffb841fb291ecf09ae7b5dcec1feeb45ba196ca02d6b9e50ae2b3dcd9ca5d72500061b100003acd0a3acb9b4f9540678dc9324480bb3d4f54e5e007739c9c1d600bff75dfbbd0191e75c0a2d810a6ec5b03d02cffbf1a66123c87790e66eee8d416caf82e7ea7090a62fe14276fa88af32ab9793f7a100de5525eedf71967c13c8d361246d98b78cc74383e714f88899f34407644c14874046b3b722186015c07b8db042db955c91cb14abbf3cfa646aea81ad15bf67108a763539c64c5a8e8115d46e063e956671e8ea8d8fd638a6414d71e9b475ebe070da7faf75a898f29048ab5a2b6c7e3a72a178b8e470e8375f539ebf6284d15486c5a8774d46ea164ba2b62181f47623fd987ff5958550c962a193638679b79fcd477fdf2a09c0fa879bb22cb493fbaed27518f5cb265741535b4ab14246077ca18f11ece7aaa0e01ec5bf02c3c3b541ea08bf254df123079c1538e266dcd3161bf1b9ed41d873f1491906e1459ba51ac9dd95e783598d3c356e0cc5b98c2a96b148f55f102a9810181eedd46cd00b445d861baeeac46eba469435aa4ddc877bd68b53f4d005aa2566d356cc344aafcdc86abd774ea28cc838d2fdc541c4b6da494a96e128b8c2abab4b21b3ae2646cdcc3528ef6fd8587b3a0636ead67a62309fb003afdc14177d329b062622313e9dee912847763c68678df663a39b89c69efdb6d916d5754534bdca9030955cbcbae6fb7ff1df6282175cd37a30a904418b976af05809f0e0e7e4b4e2ec018f1e9c6bcbe7a7822c8699669946f5e684671d63e68cc7c9cca2963945dc21c52232e6f83b1875b2bed7c80c37371a480a2e5255d49d390c3b2adfc695036ed91371cda7d79bdfbae464581f0b32942f03826aca17ab9da6ade4a778d310ec3da17fc3af426d21b347aee7c2db7b5e188e35714dc514e3a1c100e8595c9e0e4399ad796021976f077e5733ea535cc6daec2e371853dcb715fc366ea7d6b9a5b3509dccf5c2e1225e3a51de9f5bb9b6586b282a0b27a9ae7ae8f2be14ec677670241e384b462eecfde68957839b1327c9e5c622c0f67cdaf3845ddbe6f754401d720d6b6d5c061dc906bfa70fb76e1168c6ac1a25cabe8873c3c1e540ae44ae631a2638accd7951f368442dba7b38d0662ccb0140d1e4ca23f51de731a6f5adcf816c3235359afd607e58948da29a5f06c96b4312aee7d35ed4c2c811a58c5a196ac2f377d653d51cfccb5213c928955ac880b5fb1b91e88a52d5c217cbf78e071275fe626c230fded548b0f1667af1309149ff74c5542119d4e269fdc1b241d9f53e02e38e015b7c5c2d2ee623bcb4167e37edafddc7fadd642c20f81b454db1a3b578d527f124dbd1f3d99fdd1590256ae4e47c2e8b3bfe8708a0d5506d6ce8b130ce6b70028161454a5065e9925d75c0095dc24ba789489fa1e9236e25330ca1a45e61224ee027664f6589028a240961aff09187fb719ff3477b56427189b7b3c790b4031f6539c5e3a8a5d7fd99c2534ed1646920a43e7315bba98d59c51b337ba7a1b038006bb574df46830f96a5685e07ee8a0a41e712810faacfb231c67e69d0fc24b98d782c70e15524d5d2dcf4e64b4e26772dfa7067f6dc7ffc8b06e6ef3ecb13f927d466e0cd3a7ea09aaed90b7810bbfdcd8a1274bbe78a453ffff11ccee62059ec25955b34a1a1cdf8a3e506d99dcbadf16032646117556ad71cd93ceeec42be0350a6c9f194fea783558c42a56d034dcaf6fb1b28037362c7c6e2446bcda71d0a88adda3144589447ef13cb85a4d2cd16ef444097bc03e32c3b1a055e952f7ac87078b04deb900375a16dbd382ea4375ddc0a9645deca620590337e803ea8b41337f3f4e4030119a2337424dbea3d21214063ce853843dd4e6df94e3dc3bbb36d89d9eb8e15e52d0699bc6ad1de9d12afc95c8785d63756576d357126e13b25502d542774f6e5d2fdb559d52698d08600fd05aa5d10939a524d7b2ab225cdb41decfc129208c372d51ca1db705bb1b9831e53ee00000000000000010000000002faf080ab009f4d0b317a141e5b3f865599676f895834400b66dd09d33ad27163ca176f00061b1000028fe56ab181f454372c939ca0516f5782b26cec2010885c70e55c41a1e43de3af6635d2ed0bed90837cefa9f1805f4808e8092a4d44efe5fb616ff7678487a460b367134fcc728ba273fb1d22718bc6a95e47a120a6d952c7c2cb8f38e59c2a4efa63977cced7e4b8f46e4d47d29098a73beee807c3337c4acddcbb32b78eddbd124b2f33ec6cd8bdd364aa4ad2c10eb69dab808fe5f5f0aec19750e51ac65a8746f345c05d4b8823ffbeadd6200ad01449c39a008fbe117a8ee904445488811336d0c439419fa4f285f9f62a34f10b076c99c0092968e3cc9fa656016b6da049bd56b910d7a9356e76d24e746b280f0275ec9e9bace82d852bf0a137ea02d4cbd3b68450bfb593564d8c20953bb758890a55a8c381a4b3303b61ec26a56111361bf7262b3f6f2503aad06758130d86ea607cdbba53415aaf253430d92fdd81c685ab39233e94654e6508eec1347747e2df2862169382aef6f99dd78b50629c5d98b1fcc73e865679d862b42f8e9d54ef6288ed2c3f2713f0fa4db538cd3e70ec1a30cd65dbf873f581b30892acedacd39b5f0aa774d1f3f77d8fd11ed628bcd02ac33f89123595aa455ec54a07e93e26f94338fedd8bb84094a0add52f912ed5f9019e3a28d90d251cc6ed7ffd35254dcadd9f1e9b28eb0e06fd4fe961d60cb690a7757f475c08aef07c2e54668121540a42a9c779623709a2124629e8c4bd4021763979647f625b360a4559dfd3f57798dfe5d36e9d902904af3ed67d8f4b0894538c7718f5160d211cec27375a7e6a2ec42f2c8fcd1c953b7b8379d42439a2c6b921a66d5102ceb6bd6bc20b17098e69a0a4f708b42520e4792474c3d115a12c83ef60ac6e69d8842c5981e9a6d178efa352e73e4a34bed4fb590dbeecb259617668e6ffb9f955297f26e3a6a3b95d9617529a61f08666ca1069d2ee1876337d3e786244c5bb45a8236577184584cf3018118d7e4e78973ee510b6773bd922797e580cd240dea3ca31892d23c1e6e4fa92f1a01da8ea40044f5613a9429ebe7906f79b32636204d025115810b376d4c6436da136b96c7c10649e3290caecd6ca14d995a817e3725fee7e621c5366f80c752e50aeffee1af3361924f31cbb1cb44731d19963ff30127ca2363ce15e50948be14c43400737ee8910ed06027599da74b06e77eb82ac523cc031c57c02dd82dbc0d53629d072615c92034cf829e7a5d4437b1f58e2bd4b16993e1e1b05c26ed8d695351db11d21df36a7f5811ef5fe001ab1e1c6ce9d2b69b6ac3af8087e6666317f75b645e3b1caefac0eb65327fcb9fa62be341c99f191cc869e48dbc8fee3e42d4393cbc6505c880dd6739a69be4f7ef3de306480a7a51f413d310926f252ea96a0c772d8b8e94e7d6cedbfbdb21fae2ffc379eb17c2680fa2bc56a8726c93e7bf2d446221ce95e49da93d29bec8e53ddcfd262c33d556c2b8921c3de93236408b462d28612d3343fbb9cc538b1e6b33c341c3b91dd41f936931e61f146fd00aee1c5c0de97b47cf7efce889012e1c22dd8faf0fe155f4e9930c27941d8b0907502a835bfffff801f6835de69ad33e95232f773219eec0e2374c421230f323257dbc91629c4ca8a61584f737b827fe8e8f5b69b88a7b64b362f8142b043f08ea82c4a0ab7c4e0b9805533e806f90597095242ff64f314801fb7ad838e98e1859b2c05c9b027ae5a4baf780d15977bc1492dee9b14b1cb0fb3243eb2304919486fcde89a3cf35a64e31b1698e35fbb8528a73526a19189d406272b8becec94379f69372afa99d06bb4f34df72e1c3b49557855ae8ac265160bdf48ab34cde30d2665891cfeda24adcc657d851431f38953f917f1a111f023d2c71845ccf25a562d2450bf8b4986b64ae4fd09e5a8ab9610d11fd68e9d570b3a467780236de974c7fffd05aa5d10939a524d7b2ab225cdb41decfc129208c372d51ca1db705bb1b9831e53ee000000000000000200000000000f4240657c0bd97e795218aa623b27cf9a71764379c4762fcee8993aa0b1ab1e32194a00061b100002c0ec5d388a8c78491bbd870faa2c46e4282e11796123b69c2792e5806748ef068397f0212a33e3f79162ba4ea247ccec410db039dd323af48ac27bd0ba77ecb870c587477d4543d9a29c53fc02bc98d7cd0144c7abf80b999c22b42a28ae8d625478ca304001f9a49782ec970031e673c76e4e27357a321729d6df38d1d88dcbf764c69eda3baac9739bed637010f44638cfe1deecc56b76f6e02d1d0c3f104462b9ffa5f20de4cf092d86a5bb35d5f62b0fb1c983a2c06df17c9ffc809c83e4b4335f5903fab536fbd9719847bb063541ecbe05c12ef8d058b3547faca054e3d662250f1cc1f925dd71297abc25fe37ab33a086759fac76208a64552f84d2e4d84daccdc3aa2bbd2c2f922bf262596742dfe034529d1ead2975dd3d197ab0e2e1c75c8b8f160ca6077638022d4afbd107979949cf342cb399347f3990029f0db6d9ac0c569d61d42539371f9a7ff59e9c83ff97d15bf0eeb254ae58fb7b1f9d8710c546ac8a227930c66ac841bc4f475229e5cadd14ba5a01e6b2da99c55861a08e2100e62c4499d30003fe30ddaa347d7a27c2158d3787d58fe51ae57d797bbef7f900508d1580df3e5233f0887567fba1faa918c246d2ec5c3b7aa022cb8a652d00b4d719e312482f57655eee80a90cdc73151fd7ab9c5367793d60c6088fab98f0547d7f547e10db202a25e027a5cd0abc41bb0e3ef563c0a6d469a702b2a26f0e8b4fddb845a16a5f06b9dee33c3adb31430c94942c5023179d3e4441948a332069a1c3b69dca65f05a43452e42fd28a2f7e6344f98ab9a7e4eece3c1709be1f7bb620f8b6c45989a8bccad39a4bf40e8215183d1449196f1f9fc17de778b616856152e6e6145a1b7a3d7f226becaea5ebe34aa4bd06e60f0fed207bfd21f5663bfadd37dd722437bcd46a26fb4e19d062574a81bcd817eecbc1914a5878809128961acdd73113ae9c51070ff4494e16d81ccec777eeb513da82bf43d4884812b26546b4370dc315793271b069f60f4285f648cf122ed8b22b0c7a27e94ccd59a273eb774c109e19980e146850de95f82cdd8aa0e82022672024c917b281422d284df0ee0bdeb3d4ac56b4ca675ebdb835c17b6a822d79ae7310f4aa41d80ac61c5e45c1c0e1d64542622a31091a9f87c335e86d964dd85a951d7c9bf41c9f2b1a9bb8424d7d1b26413da8034182fa42d2b1cd1f8745482c49d8348d19c72cf5a02bd28e4cba82128af8bf5d9c1215c4f543ef4d185f100f8d803dfa29c300c072e44ad9542b82fb1380d55c15c9a4b4398876e2450b90b49990746f339abca8cc8a462b62329a128758ce0e46b5f998af1bb485a3044bd125424eb5c623afd2a11befe4ec544eafe275ed1ad82b940dad5e9a9710d48562e51b296ba81f2d70593685ba0e3f3b25089a187e61d5675dd481aa99620276cb0a841a3c4df201a929287b1127270c5d25d06fb286dae1a9a5a5cdb60003f0c30d2021074bf252e550685f7b51a087a77b0871e883104e55f898aa5bc4cf8538c293253a737556d7f220e15b90cf0eda7d5f2172372e3c50c12cc588f312da37191b5038e944825044b130bff281ecd47a4252a1411ab7a9305c2b37e9facd435e9c434de37641498f8e4bfa7b42966da29c84200aca87ea1c3b00db54906b340e524a7dc4a15403bb82bc24517cb91026096bdf18f5f7ae5640ed6de1f0c5d184813d6b9d244b32b58e9ff524741a39383eec3a530d60db13deb26e3523a725f0599b671b625c07002704fb600b77318417d2527537359d122e22a1f7581eaebdc19e65ba50bdda18ae08e9a8694fcb0ff1a2cd98d910dbcd52064c15a4282d67b278c72a0fdbf228abf6b519dd28ac21c57d1da4bb7ad5b5ab10da6b83132df1da79ccfc77fb45598bbd91ef5ab96d8a2ee148639a562debafffd05aa5d10939a524d7b2ab225cdb41decfc129208c372d51ca1db705bb1b9831e53ee00000000000000000000000002faf08011b29e60a38883a8d4434f17ca3d92161109f7ddd8799e64e86d6b8509babd1100061b100003b8c1771721551fa88af8fdde92909add6e5b8fa90a4b0484ee065ed3ed7c56e733a7fcd775d4ab92956c1328ffee9c195004c5dee5dd5b9d0f9034589e6769e72579d3d5837ad70785ec420b4a24c04d36668728c0d2534ff1feea9aac2410423fd79c7db9231ee7efd3d585646e378fe53d731d18f38d6356a970f5c3026edc849d49ff34e58dfb7548512461110088ac3aa800e10785029ba3b0e9ab7bde0f056939e4921792dba2f5c005135daf57e32cae06a9ccb1b4d321f3ba015e5b92def1ff1c200e56b3990d82570586bfae26e9398e17dc6c069f92d80e6dfedf6b2f24b1dc3cc9d63e684d861f40fdbf508d4ac34b7f10c57be2a9b0c5921f86869c29ada5394b8780d2488a4fca3cd98ddb0ff8ea4415a07caea436682835744e94d5cff6d3024a9525dbd697e499b7ef23062b18b225bfaa4c5bb07166f34ff7866ec8f0fbbc12f695c609692798364fa20bf7977e321deda3fe5510833494532fba94fc1f0dd14ec74f3e9fe8ee659634621b63d16d46a8958132c24bd82c516bdf9ae9515cebae42778e4de6be7047c31cf86c0df0306f7b6562e1f35be51e5e64cc6d9d4c010849e6ac7ddacaa4b7b6fb1d35aac815964090940e73a1193eece11c1c1d37e373ef58c5e2d690b6ed6338360af9906146da9db8329bd2786bbf92df10445ee093f0b1b2a640cc2daf003fa7141435ba1dd54f9cdbf5417fa7f539b255452852a85d2ce97ce5abed4980e7b409e283f97ccc9c01e104b55155f96ace6789f61c4661962d34fc5d7e6f5f5233180933b2fa7f7a5b074714645489f5221966160946b7bfbf0fe6733e6beb8af4457b9d36cde1200811009ec483a9d730ca980aa28f636942af5e89794a8edbc1b75d555ba134974374d0fe23d31c26566064eb9998d649bb2bf066bf710da50672f4e3ab4df843a0c8942bad0a071c237d4c1759eca37380919e36aec73284db202a32d3d1619f3e5b757b2df8b04bde567783dc8e465d996799782f1a1b8de9331681a35aa04edb427de87264c8ae9c397f29d3e8730db91256425a10b960a9de1a48d0d4186d617d2b69c87e2540f6570faff4ee1f6303d7d281434947abeaad83c86a4d25bef4de2bb3c6104aa0ceed7c8df039f4be6a42851a118adb1b8f98e02f6727b75d98541bab2ff24fb2f20342e86150c678941825409b62a844f44ca1ccdf0d9f7c2cf9b222fbed00bc92be0802fbfbbeefa71c8976cba8fc4aeb031480f434027b1cd593d08cbc14c2a360b736b06b5afb8da35f0be3818fff4275b8c830f5248a8b8edea1327454e1360bd90d4fa08e965f459b0b027e1180290cf762f813a31e8109f472d9657b03af737d1f7bd2e59441541a84ba818f1413c5cd1f8b9882e9188e0def9e44e2f4a7c710c893c7188ba86423f8ae86068d84e1832af548289e87c34d68b186df7e24ca5b051f8f5e4a44e2e7383ba2a09615b4147b34e86486731290ea67f3be24c13a9c5cc37f06555989b3f10c580a9cd2b416d0ee4210855c6833a25996761dfabb036f3893cff7db7e310baa8faa79f46e0ee43bf4dfd732eae7f44bad2e7c032b9c6d14947af6b0e37e5ec98372a622f716ffba0cde04b9d4508392dd154ddc34829412bfa604d4f00e4b10a553587343ef5c0944165e7ee1e34387b09c147ecba943cf36dbc4269efe50ec3a5a3075c43be9651d6db6acb9f657476952b78c990557f05935247a71077373ec436ec586def177448f8859ba096b7a838e5b4ce7a463f9082f705c26d99936eb1be584ea9b58a44b9b4faa07fd8247fa66cf4529d1b8cdb92ed7bd96bf0968db4376489c7d46f0f27d58ac884c29736502953723ef1ab41e19c7041d3e0e9091d7de2e3904d032de02292edb1225a672ab438d3c65f7921c06a9f181f8ffda4ac524d0e000fd05aa5d10939a524d7b2ab225cdb41decfc129208c372d51ca1db705bb1b9831e53ee000000000000000000000000000493e0f81fd474bea478df0202b311c69e85dc6215f629491dd15ad0929faae2535abb00061b10000370b2714d2734e6b8cde085794dd7b41c8a9b6c03c1edd8e3db168ee7fce39493596e882b18b5b1b79c16400c6762b9856075821be6be9fab8f469f56820d8f341554400a8da7f1f1a8501902581d43b9fa6e5c68015716f718a2190b87fce41bf1b509aa61806394a42489d63c457fe4c79e7480eced1315edd731887e57704fc9102f50cb7f0d242d755cfd5a2172dbaf7f01b124861cc6d1dc804796bbb84165c805f0c0f3fb9ec97b74c2a694de56a9cf8d79d1a679260ee1169d78214b34c8a654ba22e59ddcd32beff4713de33549f035b342660405b0159a7a508e5691ef4805689140e72b8a0ef2e61be74dea5d0b8f5589e0e373cac2e2e1cc39b2121c05cf4122ad0f8b9af6fbf1de2ea26376c2650ccd306c13a7b64acbf2a3feed128754abe44658009e642768ae3d84f5e0fa5f7f360c2a1c76d26985817ae77b71fb59014a5483ebba9271cafa5e5d8031c569adeceb8bae6444e98d2522b28f6682109fc7d31cdb83ebd45e5d81e7f046df42345b49f470dbef9ed87709301d2c6131215d33a30b8d18e63e54a2aff85dd57672f8198bca6a67ee147c7d0ae649e5661ab6bf78a662fef9a164f1e332b9f16e6fb3d5769ddcbc1d1c07338d3394b9245d17618c2474e86c064fca4df00ad3a93dc051fd8c3328cde2a987798b0f22a21c90426700abeb1e6f38dffb485b5477ec44c690fa80e317b32a982fd3082253bba8595783290dbffee4fc9296ffdf16a8bf3154971bb720e78674969e9db2e0fbab9e9e13f24bc8b3af5e2f00f262f0da56de443f70398ab68f747d35370fcd8e1c0e130f7269e08f862b5a67f2c129be254df2358762ce3a947eb27d66450af51540e7721b47c8a5a86098ea64dad381f14e07aabbbc470949a99c07612add3ab4c575fe2e520bbe511a1a674aea37a44535c13ee3380f8f39bd230fc1481cd31912af36c6751e23c6f383cd37a8b13fa7df9f0c7e460739f2c6226638ee14f14d36366211cbc6a1e16b4856bf302a540aa9d9e833b1d59c510473096384c8b450f2f3f1dab9e614af822949d5cc93d76bc4d1a52891bc85f1981ef83161195ab7d8181ee4fb163bc6c685a10e87c7f4b15ed7d05833c230a4a5b63841fc65b959f0ff010e697f47c583f9b7fa9b389c0eff6614e47d85b83c483136f182be4c151d272f5d938b912a95e47d333e5de6a409ad271679a778a7eb3f169c71525302fac5d4575e2645c09763c2ef165736a7a726ca605038e2781404328790ffaacef2b9c2bf90122042cd571287bc4e3973da65fbd4e3da9e40e4347ca6eb4ef1ffef4e5a34be80425cae3e81533f7f2953f95fca53a22057a39125f5c76350fba7fc6c036838fb951d0aa8702e7f44c6f8a9cbce3b64fa8ddc2bb8c8b35d1e29a21beda6fdd332b31a749321455277231fd9d70ea4aded95053b395f88fa6916d126e1626fc0f1be6cd2a9538d17c498b40927f12b3bb40fa3e272e82cd2242b670afefa387470f4e6e0a1236028954c9e90311f486617187956a23b90b356d71e219e6dd055c2120771003a6c12769aa3ceacb9642bc01022731ca7a413b68ee7d1d5444f75dfa51a68b74a01ac85f6ceaf5e56987b9d67d6de896f5aafd25c78c413a6d4b5b03d571167524cd231ba13bd9f80fd7413faf21e8170cef0d08b242c5c38a2b0158da56e358ba0692f670d4611c7a3624b234adc30c5b7198e0afc941f5d13eae3a94ddffa652c784c34c582e04e948da91a5ac3038a9df38fd4f1733779f4f122ca2d7ff9d03bac9def35d9ee3a183161f8f2808d472b2e64581209359cea58ca7757164c666029982223877e2b14d2d537afb012f1ffc12cd083c16dfc64213c56f3d4d22b603d3dfab1d21e239d6fc1f9f153ed61f1ac91c29c85c16f4aa2985f84052f5a08d32bdd000027100000000008af34a0000000002c2322200183cab01341b3b937dc48c2d5d70e119b4fd5b4dc6d85e0bb49e98f1fe4ed87027bb80c7d8cec36237511da378ad5c121861660506bdf411240139e49a93e13aee25d10939a524d7b2ab225cdb41decfc129208c372d51ca1db705bb1b9831e53ee270248e28af5950cd59a64545f84957e4cd869de45b4fb50b85313894a1567c134666207e5bec5803b71d6fd6a9cfea3e6ea9a8dfe645a7f4dc7c9cd6c4ce15a0002547bca65416a28af342589b5b771ce54464b17b2153b49a1d34a2ed0b0788ec37083e54750e6f368b4a40d18a8730522e8f23901ac856b697dbd91992d75e097e3f175514453a79521d93ca8b3ffed47cbe300afdd75d1dd23cbe0c14cf67b3856fbe2128b9b7543625f1da313516f7e441255f85e341371fb7f33c6833abf02000000000000000100245d10939a524d7b2ab225cdb41decfc129208c372d51ca1db705bb1b9831e53ee000000002b40420f0000000000220020c57347ca54a9e6f279f6a1f1e50f19b48289c3baccdd356fb03033ae7ccb6e444752210252a01d9d8b03db1a99b1a82223f7dd60ecffa69dbef47c06228fdea2fe0f0215210322a747c1d7f77fc7577a689618bbeadf28b941412404ac5e216d684a32d57a8e52ae000100400000ffffffffffff0020cd6d4a4bf51a8c36a25cde5bc08188c5ae037fd9ad9e92b400bb3d10e473b55480007fffffffffff805d10939a524d7b2ab225cdb41decfc129208c372d51ca1db705bb1b9831e53ee061a8000002a00000000883a86353a00c855b5caa13998033c04330f88b88e084b3c00f228299e5554f0b66e9d5c630e194cd572acaee6e5124b612583b9722ccf24581716292785c4925c06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f061a8000002a000060535ae20101009000000000000003e8000854d00000000a000000003b9aca000000" - val dataWaitForFundingConfirmed = hex"0100200000000103af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d000094083ad48a454dc5e607c1e5d2591338c4e34ec5af5c31a2e33d3a22e7ed94ad080000001000000000000044c0000000008f0d1800000000000002710000000000000000000900064ff160014208e15ba2108797d7d18b1711ade2bd5aaed0f470000186b0200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000028a82039dc0e0b1d25905e44fdf6f8e89755a5e219685840d0bc1d28d3308f9628a358500000000000003e8ffffffffffffffff0000000000004e2000000000000003e80090001e027bea4d02aefb98936d4d01a257fd3a98876d072bc46f0e4a168914de0a31d50d030dc932af8995a73c2a4b9266e2a81d562abac61a9d164f611261a087c9cf9cae02f21e4fed9c5e492e3a11cbf1921b816bc459e4b22bea94d215aa7530041b50e002aa0a7dbb9f730266b3f11d02f9c18553f0cfa245b7c8833701a9c4393abaf12603d52a694312d3b8aa68c6cfe607fd80685dde32d753c4cc25f0c4e3c4244b988800000003028a82000000000000000000000000002710000000002faf0800000000000bebc2002462cce806e70ebc4135574d196ef37c95ddd5d361833b4a950f233ec9bb7dd8dc000000002b40420f00000000002200207a8dd41cecf77356d7af608c10789f04f613b3edb079781111224b9a18138245475221027bea4d02aefb98936d4d01a257fd3a98876d072bc46f0e4a168914de0a31d50d21030b22c9cb71700ccc4bfbaf489c1fb7d3b276ed961cbe3afbb6058251ee89f56752aefd01590200000000010162cce806e70ebc4135574d196ef37c95ddd5d361833b4a950f233ec9bb7dd8dc0000000000bd40fe8002400d030000000000160014166c63052cb6a6756dd8274cf00b353ede684a74b8180c0000000000220020cc58790fd3c71f7d06df4b2d495737c1b066c6f995646309d23a04fca1d4440d040047304402203e06e51907a0f064a7e93706a8b1800a912bac7d5e526807352a78ca6ee123ee02205f9246645b638de1d3e8072e3bd73c5c7a06783120437bc6950d8b13ee0929b20147304402202934216692b1ece972969823d0900844f0dad3753a6b9c2023b5c92ae7561ae30220519e1669e44cd25a6a61a7332cb354fe400c9b32c838875717827431abb9bffe01475221027bea4d02aefb98936d4d01a257fd3a98876d072bc46f0e4a168914de0a31d50d21030b22c9cb71700ccc4bfbaf489c1fb7d3b276ed961cbe3afbb6058251ee89f56752ae364f712000000000000000000000000000002710000000000bebc200000000002faf0800cb2d59de649710ee85768a56bc16fa35f6e92d1046a5dc15a2538891cc13e79102f629dfcce594404ee14bed2642ee2b32c56e9caeda34604c8f3026bf6cde6fef000000000000000000000000000000000000000000000000000000000000ff028004eb06e51aa0c25332815422138e289366488590c30c2d36a8d475fe178f752462cce806e70ebc4135574d196ef37c95ddd5d361833b4a950f233ec9bb7dd8dc000000002b40420f00000000002200207a8dd41cecf77356d7af608c10789f04f613b3edb079781111224b9a18138245475221027bea4d02aefb98936d4d01a257fd3a98876d072bc46f0e4a168914de0a31d50d21030b22c9cb71700ccc4bfbaf489c1fb7d3b276ed961cbe3afbb6058251ee89f56752ae00000062cce806e70ebc4135574d196ef37c95ddd5d361833b4a950f233ec9bb7dd8dcff5e020000000101010101010101010101010101010101010101010101010101010101010101012a00000000ffffffff0140420f00000000002200207a8dd41cecf77356d7af608c10789f04f613b3edb079781111224b9a181382450000000000000000629db8f3000082000000000000000000000000000000000000000000000000000000000000000062cce806e70ebc4135574d196ef37c95ddd5d361833b4a950f233ec9bb7dd8dc000034577c7e4b037b38f635ffdc91509a0b378d964a01cd64f7b3471fea1fcf220f5ed28c1cfc118f32c017c6bf03cd8e15c2df20c7c7789603524827019a227fb6" - val dataShutdown = hex"0100230000000103af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d000090cc78408b6ea14d14bab8c89a4e1e0cbdc4cb4645f9c3a6457b0bed5788c389a80000001000000000000044c0000000008f0d1800000000000002710000000000000000000900064ff1600146663df19ca3fcc1c04447b18d2cd795c485f19410000186b0200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000028a82039dc0e0b1d25905e44fdf6f8e89755a5e219685840d0bc1d28d3308f9628a358500000000000003e8ffffffffffffffff0000000000004e2000000000000003e80090001e0386998d8c3ecc235c80eb4605ec24c5192d27ddef57d204227d3ac0c54547312c02c02cf88a39307d6e9aa4ba00bda029ab3cd426316eb63c186875c3a5d80969500381d707ac8494a96e1e8b8bf4a3a703a78358c69856f4741dda173d9f9448219303d6ba71bae191ee8d282e57d3a7793919d621b9e2df77dcafe45a5cdb913784f80203212eea182f9f5d5666a54b3e96a7525a48d5b9ee0f9a160d13926480cb9fbe00000003028a82000000000000000001000200fd05aa9eb27b82a5de32ca44c8d8e38e705c62f5f4d3b945dfb5f03973dae05fa4388500000000000000000000000011e1a3000a00cedece5db1cf150c39b679dd048140f491a72e93196f9e205841a23791c500061b100003f2b3c9277a3dd9ee0fd08afce183d75f570299155a8dc5c1c5acc79a572629c37ee817bee5023531eb3929c8e95025deb4600cb6ccb900176a23ed7cc35f99f7266ec8b0e59f8e0c0150916c2533133bfd105e883ed7b33915f7a532e36d46db78a2842d7e99f1f8abc863a5f6f8866b130025610795266d8a9603a8ae11ee23ca765f9a82ef00686612da9f70c2b2c2ba242bcaa9e3d95e542cbe59461a75e5a8f01553da2552d363593b8a85bc546f17f8e8bdd1449602fe3f46ed566e42a4155cf0b6c26df2cc5a8312ebebaa6bf11df9a55aaad2a96cdf41d7e70e815b7457c9332cc58ae9de3b9f52da71e7f328afa8de7777c76ea82e17e4a2cb46c3669ccc5f644414764019bcb2d4069c3e7a29814f0462abe479804d4dcceb498f1da52d5796f69f7d1d235e2da967e8683f77e7f206034ab12c02aba8554ece3444f8e0803df9ba5a9b1b918f13d94fd89235e1e2ec32df3154b204bb7642d33112ce70ef5384b11cf07bcace83a97339e1f9fb33e4b2cf1ccd62cb5311ab2124a8ed33a6c95c49ec90d9a477f3556d098a39235e8f1b904ed7aec0052deb3173f5d85126bf90271c567f3e9a10c12bc50f37f41ae40b49c71cf74cf18cb6e5eac206cc596349503f913178dcb3ee4ec5410b30474a4a2831b18ac07cb78fc143e056aee802aaa8bfd5d97e403e718069b221e3effcd57accfd13bb61d4eb78e98f269e79062c414a9e0b39075d8baf371c0fe09aaed436445d62664de1a96e8c974a3fe0cc045dab3064c7601dda9213d89b8fe30437f0b5cffa296bce9c141e3c012502431ff0cd5cde95eb52f9dc78ce3fca95b6a75de178ed13f9e3e7610cdbd1a1a893dd635e07459aae22a8f8c5bfc0a33a1f88fdcc1b99173080b602f91f1c9549b235e21bfc4ca071e0070f0057d65d98343cd82035970f6d75fc9a4b191270d83cb0dc6aa84238567e230bb68d16b0bba3563a28d151b308a74766eac50d4b4b287e1f46230ed1c4e8259cefdbba86c367392097909a740f692b745bb8c4809115ddd42de543dac65245faa6870f25aa2df16118d6bee2be181789ab64e5ba727048eeda81e6a90995c681924b2f807554401b6aef9436a345253be21747c313e8d9e2bf048d890350c055016cc61a06dc082a2b2c1b038405248ce18bd5037afa0e04b704198d6129da78ef9b97a8cee66a1104acd89c2bb2ab57d519f6693fd94fbabbca63fb9bd29decd9dacb34000f0f40af6969577f37a9aa38b4f2cb3d5d974a3d53440e78aa7ba013ac9a242be930e819ec36a96850cd7c630ea102fab614f81a4c9b9a01e60aaf9956a4f109cf5496ac8a43d03b0802303a6fe515199f9dfd4e66c2eac124d4653e999702832073f75ebc177fffc3efd404141927b3635980a40ee6ae50ff14010ad8e21493a38e4930a82b9d636d7115a55511bf4296d80ea8105a07f65730db9315a084600be9a3dd2b19943fa7cc85d2293de47f7799459288caa8d40c55605b5abfdfbf1325d0ea90aba749b0a625847f4b006ff34b6c9bd14a1c107c8a0175983ad10ae47a285b7d503e870625711fe3e1557377433ee845e2254c538304a97850e5acd2d4ba14241630b026230ed508b84f9442d6fb9a45369e2cbbd0d494cfa2573d4a0e553c0013ecb33d44f2395e4e4a3f752b8f86955061eecf4a9f9fed27648446369a8dcae2e6ae0b51b9a4fe315ad5c7521764738cb591734b32a8e35c1d6c80d9202aacdbf65cc243ba0640f8f8c23d637d7331c5b9f4c44fa5f2858cf2d74937beb4b345d22c2614bb6346f38933f84aea2bd5456505709fd8b5335fbe54e99c351e151f1f209dcac9e37742d3978c9a95eed0b29fc0b4150229a0990f465bd3bbcf97f73237961042359aaa0f11b1bc4c572de165a6372f3f438e7071a19700fd05aa9eb27b82a5de32ca44c8d8e38e705c62f5f4d3b945dfb5f03973dae05fa438850000000000000001000000000bebc200a043ffa6f358e330873add2e0d4f05a72150cc831bb2f1062e5c378b9bced8e800061b1000025eacbc7939a2307c66fa4841480bc6c87f041d1bf722da78bf13983d485df3fece09e54b1fb1534dee2931eb75f0bb95f21e924e565e8e5b06dd16a9fd4868fe60295bcfee2f6c53481a13c9e49d468a8c6992aca5464619ee54f052f5cb2a6a4ed6abeb6951d22ccd0662816882497d6c8fcfe64152e6a9b6ab15bdcb43545fc3f2fe9caf2679f5705fb68b90f8dd1579b9346901dc16923d222125652c8b15b5e003f660fc1af452ee0edb61b67272a3369ffeb3671f2bfacaf3b9452db611760d95c734151639733adcf7dcd2ab7a298d7dd0d31c61004be368b9858d19907eb76b421e3524d83d4a2dff76c73abc0de61114f6c7d61622074e719b729c52e9b11f65e8260e90f66e71de5686c89da73e9b1001541d621663c3613eb4ce2faf7f9ae94f0f17047e3b283baf98145936ff491c6c031a2059e2c52a7725f0790943bdd21ecd3e237000fa8bf27e81343902489101dd96f2a9bd6cc1cb9c4f4a9bf33ec39b0743901f8bec541fe9090d984d9c071eca64ee9fb4996564cad02923c0a662db9b42d76bacb09314c6bae42077ddab05905a02f3cbab41fb6a9e8c1ed2b27b5052ced38f4192cd97e0b9747f219f2484b46add32e937fd7f8fd8098aa2147e3e1400226703860766549868d719a8c372fad4ea7c8256908ba2865c68683a2cc28436b1effcd6e5ad990b83824181fc80a576f8906a45f56ce7e3748ad4a4385143768876d1e73ad962eeb6a2466f42573da8cf4a082997ffb87efc7f2711b92af0b36a72f1157c0df404aef2f958b32100991574ef4593f134d680e1a87729a577d7432b3e2ccfde6fde5334cedb8b601a07932f2f63835705e480e57f92b51e4a08ec44e07157bf6f80095fe908637d569708779eaf5b9be3c33e0dd9b840c3e1ef3cba0a0bbb868b4f07b5abb934fcf5d47d3511941a32e54ab5d4c547b81708dd5a876d3667f8447e6819625f49ac1e8f8bfd9df2b190aa318927d212c964a5b8b36c2a94d7543c8d94c4e2a1a43824cf652873ddbbdd321b473640f0982cbcea32288c6ecd2272e57cdcc20a9bd60a760de289bd86cdcf590cc7118e407ecd369412e544cc1d256e177fadc3fb15d8238199782aeadea3f33558dd68ff042d9cb440f556132dae1fbcef3e693691b089939883ca0e8c94a630d45c6adc33201cb274ee3f3225cd2a835b4b83e0a319eba92cc2bf728f6e5bcb786ccf8ca7c42e6fd2a9d7941580a8f7983a6d9d88ffa6ada598ef4571d6d5ff67917f43bff4df2330081244bd3fbaefb21b6a8d14ab81846824379415c186726d20afcdd298b921bb850eab4bdd188d72dd42d6f1a211b580e0ca831c2443f7f50eb911ac3b3100513be666b3a48be024471f92e1689909392670c0f3d7f937ec318ff9a1f1fc147597e49e735978abe5ebc1c1fb6474acbb610af539c57a248fbaa8909587418a15e536a53801736ea27b120e43a070a8e2f402ce744d71c2f81caa3a23a669462c1e0012009c0201b9cfc5b01a78b26c2acd939c1ebaa5e127b57c58078a79dec8bc206111e4cba54b645d0dda5e7cdeaf1f9e6374037f8ecc7d72d2802b0a5981f48c75b1f15ade54472450e0f16736902702e915117ba02344ed0d58bcc9f2e03fdcf6907812a3655dfc07602144a16e5e1cbcf3629a8bb274f8c1652db6cbe12bab258d574181c94a8972392428a0bafd1c021bfe42df0a942f87c112c8bc340bd518b3cce475e481b7006e266065ca620d70923dadcc7518df79198990ac8e7d99f066eba8bcfb2992515f5cbf2da8ef55b19fac4418309cf7bd237cb69b1d7c6a3c6b456f1ee228b3e9fbaf1b3cadddfaa307039dff33ad59bcfe44851c33fd09346e5791adf0b0a846326266756fa5a035a840e79ff0fc1735b04a4f611da1c29f9ca64a0a9d1d07bd000027100000000011e1a300000000000bebc200249eb27b82a5de32ca44c8d8e38e705c62f5f4d3b945dfb5f03973dae05fa43885000000002b40420f00000000002200207f89b60332c0a813268782be20d14c67626b76aa4832e1b9726c57a01750bc83475221032edd70e88cbfcda8c91eb9b20ff6a2c569a52de908933b048bf6b8414d7667b7210386998d8c3ecc235c80eb4605ec24c5192d27ddef57d204227d3ac0c54547312c52aefd01af020000000001019eb27b82a5de32ca44c8d8e38e705c62f5f4d3b945dfb5f03973dae05fa438850000000000d2a9188004400d030000000000160014a203180611650c5e77e05d64ff0d794aa4ad35ae400d030000000000220020fea2002eacc21b3490a177df4ee69f30c231d919b99e0b32bf518767169aa764286a040000000000220020f757d7d804dfbc35427f8bffb749454a114be5055a683d08ea66581d1e51dcfce093040000000000220020e518e9c7e70e16eb89facff2af3f233e1af45d8a6d04ae7f36f4fffeadcf3e1604004730440220203be2c76c9d051b47db6da5a99ce689eff53ce5bf016f1d9ac403ab7c9fc9a40220290da75c0701cc212ac502d3d894801e92a701a20a4e85c3f640f579f96762e001473044022006c47ea4c15c389369ff75d25b83770396920a1f06a2c468ce56a32f9cb7cded0220759b6227e0da03ab542187f482b9f6123c93e3187070de646b07547e7493e8ce01475221032edd70e88cbfcda8c91eb9b20ff6a2c569a52de908933b048bf6b8414d7667b7210386998d8c3ecc235c80eb4605ec24c5192d27ddef57d204227d3ac0c54547312c52ae08758b200002000324cd3bf6d98e9f7f88be0e61167f9797164b578df264206f79a96e104cf4abd0e1010000002b400d030000000000220020fea2002eacc21b3490a177df4ee69f30c231d919b99e0b32bf518767169aa7648576a91490767756314724028f101050cd76819b6a1756368763ac672102551ddb3bbc7ea3a02cadba123a00b171d7b75ee46e7aaba9caf112012c77ab587c820120876475527c2103b89e41f7c1cefc7864881d9a78f262a9077a99cb05194684e48591bbffc0a5ce52ae67a9148acca5ea2fc12ce9d71ece802b1f500a7e84378d88ac68685e0200000001cd3bf6d98e9f7f88be0e61167f9797164b578df264206f79a96e104cf4abd0e1010000000000000000015af3020000000000220020f757d7d804dfbc35427f8bffb749454a114be5055a683d08ea66581d1e51dcfc101b0600402470f4a38f40a7d71f39f9552254c007ba5557adbec6ffa7176669fb309195d75a33b4556e6f35536757085c24fb83c09ecc1cdd976cb8d2b0bc679d461789d040db8c19bf05d1d55cbe50de7ce29c9388bb541f6397dc43f9452a92a931c76c42722ce6ad02d0343dde9b692e001349d9b1e075527f1a8d3d1aa90fc72164271a000324cd3bf6d98e9f7f88be0e61167f9797164b578df264206f79a96e104cf4abd0e1030000002be093040000000000220020e518e9c7e70e16eb89facff2af3f233e1af45d8a6d04ae7f36f4fffeadcf3e168576a91490767756314724028f101050cd76819b6a1756368763ac672102551ddb3bbc7ea3a02cadba123a00b171d7b75ee46e7aaba9caf112012c77ab587c820120876475527c2103b89e41f7c1cefc7864881d9a78f262a9077a99cb05194684e48591bbffc0a5ce52ae67a9144976a85ce7e37d8557d74cdd86254926d991f77b88ac68685e0200000001cd3bf6d98e9f7f88be0e61167f9797164b578df264206f79a96e104cf4abd0e103000000000000000001fa79040000000000220020f757d7d804dfbc35427f8bffb749454a114be5055a683d08ea66581d1e51dcfc101b06004062a64d249f7301aa354b7951f0c7b13882b7196e7985727395c624bdb84ab9234a5e0edbff3b5a5b12bd385a695722be5aef598ab36ec1eb605e944964a1adbd4061013fe37fb5d1ed7f8d8137501a755c195bd2bdcf345e30cac31c2962f4e0aa0614d7f96b9c49d8120089f2d24919ddf40bda3af04312f7d5255248075d28e200000000000000010002fffd05aa9eb27b82a5de32ca44c8d8e38e705c62f5f4d3b945dfb5f03973dae05fa4388500000000000000000000000011e1a3000a00cedece5db1cf150c39b679dd048140f491a72e93196f9e205841a23791c500061b100003f2b3c9277a3dd9ee0fd08afce183d75f570299155a8dc5c1c5acc79a572629c37ee817bee5023531eb3929c8e95025deb4600cb6ccb900176a23ed7cc35f99f7266ec8b0e59f8e0c0150916c2533133bfd105e883ed7b33915f7a532e36d46db78a2842d7e99f1f8abc863a5f6f8866b130025610795266d8a9603a8ae11ee23ca765f9a82ef00686612da9f70c2b2c2ba242bcaa9e3d95e542cbe59461a75e5a8f01553da2552d363593b8a85bc546f17f8e8bdd1449602fe3f46ed566e42a4155cf0b6c26df2cc5a8312ebebaa6bf11df9a55aaad2a96cdf41d7e70e815b7457c9332cc58ae9de3b9f52da71e7f328afa8de7777c76ea82e17e4a2cb46c3669ccc5f644414764019bcb2d4069c3e7a29814f0462abe479804d4dcceb498f1da52d5796f69f7d1d235e2da967e8683f77e7f206034ab12c02aba8554ece3444f8e0803df9ba5a9b1b918f13d94fd89235e1e2ec32df3154b204bb7642d33112ce70ef5384b11cf07bcace83a97339e1f9fb33e4b2cf1ccd62cb5311ab2124a8ed33a6c95c49ec90d9a477f3556d098a39235e8f1b904ed7aec0052deb3173f5d85126bf90271c567f3e9a10c12bc50f37f41ae40b49c71cf74cf18cb6e5eac206cc596349503f913178dcb3ee4ec5410b30474a4a2831b18ac07cb78fc143e056aee802aaa8bfd5d97e403e718069b221e3effcd57accfd13bb61d4eb78e98f269e79062c414a9e0b39075d8baf371c0fe09aaed436445d62664de1a96e8c974a3fe0cc045dab3064c7601dda9213d89b8fe30437f0b5cffa296bce9c141e3c012502431ff0cd5cde95eb52f9dc78ce3fca95b6a75de178ed13f9e3e7610cdbd1a1a893dd635e07459aae22a8f8c5bfc0a33a1f88fdcc1b99173080b602f91f1c9549b235e21bfc4ca071e0070f0057d65d98343cd82035970f6d75fc9a4b191270d83cb0dc6aa84238567e230bb68d16b0bba3563a28d151b308a74766eac50d4b4b287e1f46230ed1c4e8259cefdbba86c367392097909a740f692b745bb8c4809115ddd42de543dac65245faa6870f25aa2df16118d6bee2be181789ab64e5ba727048eeda81e6a90995c681924b2f807554401b6aef9436a345253be21747c313e8d9e2bf048d890350c055016cc61a06dc082a2b2c1b038405248ce18bd5037afa0e04b704198d6129da78ef9b97a8cee66a1104acd89c2bb2ab57d519f6693fd94fbabbca63fb9bd29decd9dacb34000f0f40af6969577f37a9aa38b4f2cb3d5d974a3d53440e78aa7ba013ac9a242be930e819ec36a96850cd7c630ea102fab614f81a4c9b9a01e60aaf9956a4f109cf5496ac8a43d03b0802303a6fe515199f9dfd4e66c2eac124d4653e999702832073f75ebc177fffc3efd404141927b3635980a40ee6ae50ff14010ad8e21493a38e4930a82b9d636d7115a55511bf4296d80ea8105a07f65730db9315a084600be9a3dd2b19943fa7cc85d2293de47f7799459288caa8d40c55605b5abfdfbf1325d0ea90aba749b0a625847f4b006ff34b6c9bd14a1c107c8a0175983ad10ae47a285b7d503e870625711fe3e1557377433ee845e2254c538304a97850e5acd2d4ba14241630b026230ed508b84f9442d6fb9a45369e2cbbd0d494cfa2573d4a0e553c0013ecb33d44f2395e4e4a3f752b8f86955061eecf4a9f9fed27648446369a8dcae2e6ae0b51b9a4fe315ad5c7521764738cb591734b32a8e35c1d6c80d9202aacdbf65cc243ba0640f8f8c23d637d7331c5b9f4c44fa5f2858cf2d74937beb4b345d22c2614bb6346f38933f84aea2bd5456505709fd8b5335fbe54e99c351e151f1f209dcac9e37742d3978c9a95eed0b29fc0b4150229a0990f465bd3bbcf97f73237961042359aaa0f11b1bc4c572de165a6372f3f438e7071a197fffd05aa9eb27b82a5de32ca44c8d8e38e705c62f5f4d3b945dfb5f03973dae05fa438850000000000000001000000000bebc200a043ffa6f358e330873add2e0d4f05a72150cc831bb2f1062e5c378b9bced8e800061b1000025eacbc7939a2307c66fa4841480bc6c87f041d1bf722da78bf13983d485df3fece09e54b1fb1534dee2931eb75f0bb95f21e924e565e8e5b06dd16a9fd4868fe60295bcfee2f6c53481a13c9e49d468a8c6992aca5464619ee54f052f5cb2a6a4ed6abeb6951d22ccd0662816882497d6c8fcfe64152e6a9b6ab15bdcb43545fc3f2fe9caf2679f5705fb68b90f8dd1579b9346901dc16923d222125652c8b15b5e003f660fc1af452ee0edb61b67272a3369ffeb3671f2bfacaf3b9452db611760d95c734151639733adcf7dcd2ab7a298d7dd0d31c61004be368b9858d19907eb76b421e3524d83d4a2dff76c73abc0de61114f6c7d61622074e719b729c52e9b11f65e8260e90f66e71de5686c89da73e9b1001541d621663c3613eb4ce2faf7f9ae94f0f17047e3b283baf98145936ff491c6c031a2059e2c52a7725f0790943bdd21ecd3e237000fa8bf27e81343902489101dd96f2a9bd6cc1cb9c4f4a9bf33ec39b0743901f8bec541fe9090d984d9c071eca64ee9fb4996564cad02923c0a662db9b42d76bacb09314c6bae42077ddab05905a02f3cbab41fb6a9e8c1ed2b27b5052ced38f4192cd97e0b9747f219f2484b46add32e937fd7f8fd8098aa2147e3e1400226703860766549868d719a8c372fad4ea7c8256908ba2865c68683a2cc28436b1effcd6e5ad990b83824181fc80a576f8906a45f56ce7e3748ad4a4385143768876d1e73ad962eeb6a2466f42573da8cf4a082997ffb87efc7f2711b92af0b36a72f1157c0df404aef2f958b32100991574ef4593f134d680e1a87729a577d7432b3e2ccfde6fde5334cedb8b601a07932f2f63835705e480e57f92b51e4a08ec44e07157bf6f80095fe908637d569708779eaf5b9be3c33e0dd9b840c3e1ef3cba0a0bbb868b4f07b5abb934fcf5d47d3511941a32e54ab5d4c547b81708dd5a876d3667f8447e6819625f49ac1e8f8bfd9df2b190aa318927d212c964a5b8b36c2a94d7543c8d94c4e2a1a43824cf652873ddbbdd321b473640f0982cbcea32288c6ecd2272e57cdcc20a9bd60a760de289bd86cdcf590cc7118e407ecd369412e544cc1d256e177fadc3fb15d8238199782aeadea3f33558dd68ff042d9cb440f556132dae1fbcef3e693691b089939883ca0e8c94a630d45c6adc33201cb274ee3f3225cd2a835b4b83e0a319eba92cc2bf728f6e5bcb786ccf8ca7c42e6fd2a9d7941580a8f7983a6d9d88ffa6ada598ef4571d6d5ff67917f43bff4df2330081244bd3fbaefb21b6a8d14ab81846824379415c186726d20afcdd298b921bb850eab4bdd188d72dd42d6f1a211b580e0ca831c2443f7f50eb911ac3b3100513be666b3a48be024471f92e1689909392670c0f3d7f937ec318ff9a1f1fc147597e49e735978abe5ebc1c1fb6474acbb610af539c57a248fbaa8909587418a15e536a53801736ea27b120e43a070a8e2f402ce744d71c2f81caa3a23a669462c1e0012009c0201b9cfc5b01a78b26c2acd939c1ebaa5e127b57c58078a79dec8bc206111e4cba54b645d0dda5e7cdeaf1f9e6374037f8ecc7d72d2802b0a5981f48c75b1f15ade54472450e0f16736902702e915117ba02344ed0d58bcc9f2e03fdcf6907812a3655dfc07602144a16e5e1cbcf3629a8bb274f8c1652db6cbe12bab258d574181c94a8972392428a0bafd1c021bfe42df0a942f87c112c8bc340bd518b3cce475e481b7006e266065ca620d70923dadcc7518df79198990ac8e7d99f066eba8bcfb2992515f5cbf2da8ef55b19fac4418309cf7bd237cb69b1d7c6a3c6b456f1ee228b3e9fbaf1b3cadddfaa307039dff33ad59bcfe44851c33fd09346e5791adf0b0a846326266756fa5a035a840e79ff0fc1735b04a4f611da1c29f9ca64a0a9d1d07bd00002710000000000bebc2000000000011e1a300f36f3bd94a6751ab579d9481cbe484dd6b1d2abba32bcc757aea07e70d0ea28c0226f5d9131a28e7d24ecc1b7fa2b63b34a379879f645d8e3346aea28f9ab603eb00000000000000000000000000000000000000020000000000000000000200000000000000000003ea1865f4cd884edcb9921736b2ae36b50000000000000001000384fe405b653143169925b03dd2cf38bdff034a0fd7fb350f7c089c1129dfb82cc772d5525d0b922cc3d989fcaec7c16b03c3249eb27b82a5de32ca44c8d8e38e705c62f5f4d3b945dfb5f03973dae05fa43885000000002b40420f00000000002200207f89b60332c0a813268782be20d14c67626b76aa4832e1b9726c57a01750bc83475221032edd70e88cbfcda8c91eb9b20ff6a2c569a52de908933b048bf6b8414d7667b7210386998d8c3ecc235c80eb4605ec24c5192d27ddef57d204227d3ac0c54547312c52ae000100400000ffffffffffff0020d22458cead349cb596ccc159d17f2809dcc22620268d1a300a6e152547f7235280007fffffffffff809eb27b82a5de32ca44c8d8e38e705c62f5f4d3b945dfb5f03973dae05fa43885389eb27b82a5de32ca44c8d8e38e705c62f5f4d3b945dfb5f03973dae05fa43885001600146663df19ca3fcc1c04447b18d2cd795c485f1941389eb27b82a5de32ca44c8d8e38e705c62f5f4d3b945dfb5f03973dae05fa4388500160014d5f0a47e22dd767bca0be8f6d109a81a8815ff33" - val dataNegotiating = hex"0100240000000703af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0000928544434bbbda1da0790cf138ef4b3881f5cec34b933ab77ffd57a7e12992bf780000001000000000000044c0000000008f0d1800000000000002710000000000000000000900064ff1600140cbd801be794f9854b38981ee859e3a000ad104c0000186b0200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000229a82039dc0e0b1d25905e44fdf6f8e89755a5e219685840d0bc1d28d3308f9628a358500000000000003e8ffffffffffffffff0000000000004e2000000000000003e80090001e0234ea57c6a0d7308e0479811f0315df88650da7a8af626fb6ceb9a6b3e1bf068e032f5bf3637d4efa39b50a7bad5e3f6d32663a1cc207a166155f00f8b9a9f621cc026b8899cafac94bcfee408aecfec60e86a30082b0ea587d8a7ab6b2b309fc66110210996c2725e4129dad191f6c6d9ba8c35ab58df4d340051d7025d366423aa15703bf59f021a7431277a31a53b2101bd3ff5730adefeae596020e58c9099728b7f000000003229a820000000000000000000000000009c4000000002faf0800000000000bebc2002424d4fd09fbcbde0363fda5d43c8bc68cea396aa1edd3d48d118ec27bb30fd1d5000000002b40420f0000000000220020552fd9e112e447f57c95b896d384461ee713c8108894ced2ea1272e0d354aab74752210234ea57c6a0d7308e0479811f0315df88650da7a8af626fb6ceb9a6b3e1bf068e2103d45fea036aa6817a71387c3976790beeb4705739d4cf0271007043854dde877552aefd01bc0200000000010124d4fd09fbcbde0363fda5d43c8bc68cea396aa1edd3d48d118ec27bb30fd1d50000000000c089a180044a010000000000002200207ba66732d8b8e8863cbdcad193c4602b54df24f9af202a977e3a6315e85072704a01000000000000220020f25aee697436b14ebdc0bdab0b3a0c339c98e49cd6a8b84fbfb84c7c5557aad1400d030000000000220020a659aeaecad3be965ed5a498adac881967d2d3ed2a773019da1b26337c7d0d1372270c000000000022002095c2df1035ebb714a728a166d88332e1569b3da5c035037df1bc07e227b7e1ec040047304402205837856dc4bc8f4a679015460580c38a29899986f6cca09afe3525097dcef36702201b2abd6fdbfa890e247cbe6a096bb01f3d47901a552f7f5e5c6ecb6c80a07d1701483045022100abfa35999608ea0c993418f7d432c7f9710b993f4eb45e17fbb95e10c6899e190220542593d102382a92a11eac65bef87a75742ee4447abebb38fc47c4d27d74f9a2014752210234ea57c6a0d7308e0479811f0315df88650da7a8af626fb6ceb9a6b3e1bf068e2103d45fea036aa6817a71387c3976790beeb4705739d4cf0271007043854dde877552ae68075c20000000000000000000000000000009c4000000000bebc200000000002faf080054b02aad4844030f2e1c3c61f95b34df8d14c0fdbddbe5d56090667016ca8979033d0000a1e29b94b3517a04106dc7bb74f4f091a2ee419d889ecf8434bcfdf127000000000000000000000000000000000000000000000000000000000000ff03228da9a93e02211af77afa576b94f57d747099099f5352298d8b3c4dbc7215c62424d4fd09fbcbde0363fda5d43c8bc68cea396aa1edd3d48d118ec27bb30fd1d5000000002b40420f0000000000220020552fd9e112e447f57c95b896d384461ee713c8108894ced2ea1272e0d354aab74752210234ea57c6a0d7308e0479811f0315df88650da7a8af626fb6ceb9a6b3e1bf068e2103d45fea036aa6817a71387c3976790beeb4705739d4cf0271007043854dde877552ae00000024d4fd09fbcbde0363fda5d43c8bc68cea396aa1edd3d48d118ec27bb30fd1d53824d4fd09fbcbde0363fda5d43c8bc68cea396aa1edd3d48d118ec27bb30fd1d5001600140cbd801be794f9854b38981ee859e3a000ad104c3824d4fd09fbcbde0363fda5d43c8bc68cea396aa1edd3d48d118ec27bb30fd1d50016001458043e6e40996eb9d3c77752a81398115ec43d5900010002db71020000000124d4fd09fbcbde0363fda5d43c8bc68cea396aa1edd3d48d118ec27bb30fd1d50000000000ffffffff02400d03000000000016001458043e6e40996eb9d3c77752a81398115ec43d59ac1a0c00000000001600140cbd801be794f9854b38981ee859e3a000ad104c000000006824d4fd09fbcbde0363fda5d43c8bc68cea396aa1edd3d48d118ec27bb30fd1d50000000000001a54fa574848f29f143c52f2717510e2a8831afaf7752217ce3452c7e88d24eb14905191e04229fc6433aef69bc33f9f2f6eb3c841fee628f7c583b9efe5a149aa47db71020000000124d4fd09fbcbde0363fda5d43c8bc68cea396aa1edd3d48d118ec27bb30fd1d50000000000ffffffff02400d03000000000016001458043e6e40996eb9d3c77752a81398115ec43d59f81d0c00000000001600140cbd801be794f9854b38981ee859e3a000ad104c000000006824d4fd09fbcbde0363fda5d43c8bc68cea396aa1edd3d48d118ec27bb30fd1d50000000000001708fc4039310f093081478d723e70b6bb1f17aa925472ccef2f79057aafce602726039f88ed9a0eb0841c61c39a5c224516288a81a53256693588f5af97e9e76c88fffd014e0200000000010124d4fd09fbcbde0363fda5d43c8bc68cea396aa1edd3d48d118ec27bb30fd1d50000000000ffffffff02400d03000000000016001458043e6e40996eb9d3c77752a81398115ec43d5942210c00000000001600140cbd801be794f9854b38981ee859e3a000ad104c0400483045022100ced76c39d6c63a52374b17103eba46ac6d21b5f5e427d48ffcd5e505a26477b90220588f30b94c0938632bcee8cfe9bc185325b2edde88c85fb4d226eda215abf26b01473044022042741816de3767e7b54fc1cdf94b6f0072203daf76c01ba5abb120801965413b02205237d5f3978b2332aa14ce65501fa784f51ea4e97030d44536f52f188f768074014752210234ea57c6a0d7308e0479811f0315df88650da7a8af626fb6ceb9a6b3e1bf068e2103d45fea036aa6817a71387c3976790beeb4705739d4cf0271007043854dde877552ae00000000" - val dataClosingLocal = hex"0100250000000103af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0000948d8efaf118cae9f142433a624b41a9ef7e09327fa45083ebd7694e1a61c429980000001000000000000044c0000000008f0d1800000000000002710000000000000000000900064ff16001401396601573e7aef81f91a89fc4b4b56feb7a35e0000186b0200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000028a82039dc0e0b1d25905e44fdf6f8e89755a5e219685840d0bc1d28d3308f9628a358500000000000003e8ffffffffffffffff0000000000004e2000000000000003e80090001e03174f3de90105ae02d88ccc1a7ed4bf0d116f27ef6010f1ec308273c1010ae3dc02fc17b2289973a29fd374ee24671d5d9643465a11bd725bac1d640b56493bd8f702b5460ae3379bc8a92d2aa2488f3e55a73d8909695e677beddd61a94d3655490f0357a1a37c3ae33b2a94bca0b6ae541036c22feeca86860c7c5501153ccfffd585028fb43c64d53c5d891bb0af3b7ed41acf3ebb0de59599b3e7836a8401645cb6e000000003028a82000000000000000007000500fd05aa0a007eacf337b3a80941f2defd530cf824f13cd263ba2ca194e7170072ce28e8000000000000000400000000017d784077c3e8caa53f1a775a5e89b1a54488aff835593d5e2b7355179136bbdd10a86c00061b1000020b7604aeb7cb080b12de62f75a292bed3134b4a3de764bd032fb049e2c2a1d3942cc11b9b12619d42cda3c7eb63da952644a3f355bdf772d77f1fc2a0b36bf116cdacfcda3b77974536cc3f3d0f8209dba69408073cbe0f039eba220fcf3b0b96d5875bb71acdc999fd9feeab68a0b63629efe0dc91e92dcf93b3c49285c981746a68ead7a96a9c1ce21284a2796b0051e6b7cf36a264d545063c054454b87cc3f9ddad5350382585de18cdc4e65e02484695f541b8fc082c87ea4a56aabc84fd1b7c1cd40e3517ae811f9ff921fba672e0191c88a2681ef8ea1300eeb6c1d8fccdb5ab05bf80b2a5c3ed5c018752cc841305edae02149389bc5e58ea5a59a1c59755c7ab5f35e3740c9f6218d227370363a7e386b214f5b2e771ea8b94a2a5e640d8a715d57756665f4a659f4ef6488f9ceab5e22d523ee52da368701d799fcb3459a31e6fb03de91ffd9b608656b172945015da3e5000f7f6c3dc0b4e102ba5f101bab5c2ceab8021d071bf89cf9d65d2439fe9a9dea4291eccb43a431904313655391ace4b82bf5da6270eb5bc12a60a69e7cf96b75a699db5a374713558eff7101bb2d8471e0c4ad0e202fd10aa46098bd5565c1d410e695fdd799c866090f1a7aa002305388acb68c8d82d588c1a66df5fb12653686bc4cf487a3484c0c23578a428de3df57539ed11227f2648bc36261c7f3f4f13e18d238dc856b8dd8f07de9fb73663d3a0026a03d9ee9f807e2a03c9546d2537a1f30a1f06767622d8bacdf4ab3b0f245a7b4f482baa60080fce3e7e15d2e086c670d6ec11d907e3da593977c8c25d620ed40cf82dd503c25f1a5f3e8a5ddb65ae7117600794844252e1448d17849028bd5df960a5fddf56e48cf082cfbc5c29666bca6af335c06f8240912a311e6c07946eeea9ef57e46b3e48b95c82cbee3830bf2b55d5e0223b9480fe072729a8af4c6c8c2f86038cb8a2e02405ce5f834c87b241fcb9963f008c273c64bc1f7289cc3a0c5eea7ea1a6dc7bc6228a97978a2ddd7dc49303c5beb15b36c8044f656716cad0a62d1e8c381db46f007be7f8096f72d3a8f08d338d9f2bbc43c0244215df229824a9ad34b655d840b38c5061462d54358be1c184b51d3a805beeea78047f544b15cc333d591f3d8cb60cbdd78e58ed47d21739762957ed6365fea14d7ba8368371fe4333be4f5ae289ff444c9f377a072a3a4173bd2680c6d6074743908d9c4f09aa30d064c7866a7a34303fce53deba6a597a24c211d209815bbff97736ba9fc79e390b36b2343d2590c8fd6db3e54206580eb7a53b9de09a42cfaa2cffb00ea90dbd07f1d357e69ca96fbba4efefc4e07b1a1a174a9a8a5568c0ec9a9487add3890a20391eebd69a56c4dd1a7c7112766b05d29fcfdd0f60d8c3f7287ec070201b4d0200e3ebbf47a97c230a7d2f05b25bec59cfd125f6cc15529f13fc624ebfd387327119434b66e61a8446e9fd9e14f4e0a42cee83f4600b195a06f4f0e55d67239c55256db69c3eb5218f9d214e0d9ca0992dee76630b55a96136a251a84711096475028e326782112f2afd64c3795f438ea623754cb0ea5e6a0d4e1ded7ee633f1eb8bf71b791646c8953e54a07242a178c02baf8fc68e7c1d3e6982b265802f50353b7f1eb0d8f33c9aed1b18432ecd04d10f951473a0e7d7e3e88a3cfca46ad1b11c5abca59bab0c8e880519753b9f4a9ab4f7a756d58a389dc3bdb25940a71a03899125b08efd7ec85b3cf0c1577014a81bf63b2144617cf259383a41af2d50a7ebb7d45a8968e3e3edfb1f4a45368ba380d172f82e641eb16949e192a3c2a37117df8e22fb7a4e7ed425f9951b392bc59184db81899ed2270844446a29aaed5cb3839c392d51636a24b737a3100417a849104c08606b4644a785080b67a92dfe888cd987100fd05aa0a007eacf337b3a80941f2defd530cf824f13cd263ba2ca194e7170072ce28e800000000000000070000000001312d0077c3e8caa53f1a775a5e89b1a54488aff835593d5e2b7355179136bbdd10a86c00061b1100030159105c3e04b59668b1516a793628be01d018e0de5972a7e6094a8b29761bd07a1654dc17ab21c8f940806cd4ccfd904db9594a8170f310b1e2d95e0e8b0baab7f8484c2c66a91b7f4274ec9e5cc3651ef2ee3a0177281548905a3c7d03c4ddf2bf8b736a940d69f2a5e6beaea33d92ce189a96803654a52b0d3da0cbdbbe0725e64dc4a6633f33318de0f6ca50b9f2a79ac68fdfe3a6f6c01a5215dc3bdfb56a780dc17968c5afd3270c50d4f7e1f540364e184d460e8b8479719788023ed029ad1439d0a0c85570b7a3f50ff70a82bd9ff9c04d6d8154046a30a3606ce25c2fbda28c863109cfaa95b7fe4f96622c715422ed1a8e99ce5bdecadb7c39798822115215e3914409d5de1e2c95a6c375c530b27fd0263d02167eb28628b945280a668f88c607bd74e3aac0ac704dfda9e796a76e6a856aee502b41c6f3932b6a89425484d580b6abc232cb12530b38b5c20cbbdcb0357210c3ad09a3ad7a56353dbbd6087f7cbccbae1f656ad06d370e4ec5a7906c9841a1f36f24ba6eab5c70b5d66bc8aecb60d9feb4576ef33b23456332483f01fcd0e01477789448ddc20fa207f496db56203fdaf53f6d64d97153d8e8c8c89c821f526f4b376e929d7598cf368b826313eb0ac5e435097b3f4acef26cfb30e63ab70622ff1ed04f9a1c02074bdb2d302d932cffbba4f833ab95912272ec87d8c05a8c5f1b2b101fb78a75cf9d0ab6e751163f27018f08c2fae76c7a6a417cf5cae2cdbd867115d9c1f162e83c3fc4f52202d3471212c3ae6eb39f882aa322609958dfbc8ca3f3a2eb051ef1b43d452b54c2352e8c9d184cbbd46ea936161067893164a72a34f6e2fd63c94353e524c9db699d15cef86f970bcf6a92dc367152f9740e84b2c292dbb338e4f21e6a47171b5a5cec0f4026906b528ad2c115866fb9e807faa200b600637abd774148f7637789946a82ec5c8e35692470c0c0f12a6a39a0c5dddf73635f2dbbb82d9ea56d8313a25cfd879ee024ee5144817b1f96945f34632cf0a5a3dd3ebcb8b42bb6c4b1c3d2a7fc6f817c97ec6282a3981a8d9e3d7070c178f7df847fcec8013b0a0d07a4f8c13c829209172022e0d724caf5a01219575f1d5a26f50f53d72ef8b596ae2263d2c2eeb822166178d56eba9ee9a9c60adb4fe3a7be89044d34288c7bd59cbf6e88e01f971b687200fccdc68d5a21f8430a8e5654a17f05773b2254eb301a5624c4aadbd3db8641cfe866ff72424a891f5e0487a48c204870c932ed9895427ce9a3d0a183fc40b7bbea3617b321f35cbba2c2180908e9bea2e07e1e5d8d5de9846994b78a313b8828eca935f4bdf9ca861326cca20776c6a4df35eeee257899467973941e6e8c567054480a7d8077e6e61ab480562ad53ad8127874a7fa4f747ccb2601cfa6e8c43e89f678dd38e4df80adef7bf07149edd32bf0e9775f3478870da6af26709a5bebbac77018050a07f1a8ffe4539c2df9de1dfe18c9bf0aa640178b0912bd9631acebdc2f2f993039b6cfa414d5cfcd0c363a178443e954db312efcd4463ee7cfe0d8a1eabe74d47692653f996cdd9b93c9fad41c5b16aa91a7bd5b267578a537e45d2af048ee0ce09ea41c72020d61509e5e91575022e7d5956b8e680f229909642366d5a16a17f021f41ca3d13df75ac3d3c8c772c2ba042680ba81466f8d05c07e19b1cde5a4d27ba6b820a63fffc5a42234e18109f128cb1e3524d621e38855b5c11cfbe7edf25dc3cd470308971b655b3381f946531d89ed23ad24f6a3cacd022fb9e5eb1d5a188a5d0936d0dfe3c7c424dba5f8892b2adda87a171d73bb5c5b8f809812689794ee3a5a7dc7d80f9c84bdad2fb7f89b2a45dc25836a42c8757eaddabcab7f82e1e51502cdcd3c4745009c55fd70b1c8f1a748e71e2b96b312f71f21cb00fd05aa0a007eacf337b3a80941f2defd530cf824f13cd263ba2ca194e7170072ce28e80000000000000006000000000010c87c77c3e8caa53f1a775a5e89b1a54488aff835593d5e2b7355179136bbdd10a86c00061b100002a9a1e8cbe48ae54ff2f32eb81ac70966e623abd77532a8cef6c3dab174da1e2879020c2238504e525d2d3fae9adb39044e8e6d0c394ae9e6ff1f01ae21e354d0bd1a5970e77015dbb6a635157b6465c32aee891c26ff8942ccfd24b612e4df241c55fc34dc1a5a613d431b813e14d0ad78023314f93c38f2bba6a6ea6e642d59a547f97b306ae0d072944c62cdec15933ffd85dafd9cabf278d9b8121db56915075baa049ac0d47df8a32b6c54a9935fbc1284fe8af288d3dd72870c8faf77e34a20970bf44b47bda06f2b8bfc931252456760ba1fa58669a3ec443737b429095db43f5bdf8e5d7f2d2f9c7318cfe012213ad35d54751539605337143de28756a146779a41308dbf94be4c9290422cb79ba768c340bc91012488afc3b388fbc253bdb9a35b1f4e39307c89f9ec3bb58e5da660013a741c1ab6c0cb22a3c94db0bf6d207599a527f18831530179880ef00950886378abcb99d2d1502b874669fb8f0a596ce3909e314057d7d1f668ca80e23c46f86127a71a729021623038d23cba281ed4967ea654470d4fdb31b297def0d22f5897f58d28af0cd86287fa51722af2682fe259987f56a9fe74af94138bb520ed12bd86453cc44f31f00e337647997b56c8bf40e3e9233e42b4c4ffe2ccc438417c754e04c95a2e7f357eb238020a0b235b8757a40996b6dab0e09e526f891f1773153151d99f3f65e62f0db6d4d94fb18293ea2f13e8c900d575cc0800969ef1e6903e6958c02dac84a8e863bcb8226bd3c6bab1038b628661333449d801b8b739bb08e09daf59c969691306e9e09c68508c5a558708a9b138fb841906a1967ea6290198be18ea3941a72a95b1b55e38403bda57d391cfdb86db844a0d9b4762a550cb1d7010de80fd6507d1e2dc7830ccd49784f8e80a3c87d9301339572b0347db9336bb7b1ede0fd1e187ca94aba942fe2b5c230f7dd1d3d0376aa882136b112883999227e2be17e9d681fb813acb70807b47e99bac127ca67c2d9619049783f8a9089d3fea4275585533683308ab94f3f02f577fb840ffae279813439fcb30eb5874329c48160768c2ca6b5d6368775989b96a056f22f7dddfbcd1b22a7fb74bb2d1b36f3e94914b82709a9351e1be88364a17a36b2a92ef61bb0520fefb802393b9e9effd6d54741ab9c6dbf28911bc45b334d9c2a6d76d94ecc2a19a3115d0f2b5e5ddb1f29fc9db0b3a21651afc2f93d321189621d8b59ab07e8d4e348f4bdbdb25b91d19b313850afddf92300c7fcebd9b266792745a24a505c56a3f98428ec70b65b0396a46836cabe49024d382eb1c8227e2df4da21385960f3d3801fb63d73ae69a4fcb6aca7018e874e8bf44f73c5e845ce81b2c643862c0344e3f4e50f4e6cb51b53a6c90ea545bc897dd73e2d2b3250711ed2a666413ae85b22e848735e8a6306e3b80f3677c8f40f85bf2f457f7764343288530a9c58b160de657df482af5eb0f291a50071d207cf46969643c113a6b15354b0b590885fbf4a7136939463ef66e013cb794ac6ab0fa887352817827ffc420bd8d1ee0b6a04d6819eadc1d96517c2edd8fe028101cd9842e407a44a8eb6bbda3fc5c54997fa43aaf340ed8ab6f75e3b588dd906ebd9deba889711b3895bd7df494f042e68cd78d7e75c237bd22b90e49e5571e169487508dfea22bfe2c83f21ec1e91f7d9331e5ac4ba6c52c1183afb1e083f3d0c42af6cf41e82e9151b5717b934ba6e4f52757d27a7c8139c523f977cb9c7f29e4a19cf41a1288c60a9ee10f58189351a51925422276e2da9ce3a4ca6944b767601e686745e88b7902e0e110dd66189dec74e3abbc76a4045d5051a6b76c91b2b20fc2ab1ca7d5d63f25d2c83f5d8ab31034e3ca42c4af1ed6017f5b10c2f44a52d78bf858aa7c148b6f07156ee365cd463d3c00fd05aa0a007eacf337b3a80941f2defd530cf824f13cd263ba2ca194e7170072ce28e800000000000000030000000001c9c38077c3e8caa53f1a775a5e89b1a54488aff835593d5e2b7355179136bbdd10a86c00061b1000031a12d4e5b7253515c5822879f6f1738224e451910cacebbb545aa637e2a0f631a151e02f206b6397055534bd6a0b7c7990d269d153d3886fc7e51fd4e09c5357a96694911e9912135e3f9ccec1058be709a743ee44f07feebb0fc6a3078bec155a1b9f67afaac6914debcc890fd86150d790f8de1b7168d4501064dacfd16c19f61a8c13c7658df92d6e5ad79b1b251a96db66bfc498e8e4114f98b9b36bc41663c9d2d15a280fdae479c14e52e148b0f86abbe4f9ef78570e193585b5aa30e53d54352eba5ad54d238f0d756b54aca9889fb21c57b6c639a6713a4f655d5dde92391185a9d8c929bca42c3bc9ba5f65b059dcbab02e89cf2e95fe1b1fb291aa1c2aeb8b57ab24f92414fc296a37b683a1d47155acb5210ffb6a170642e0f1cf7a4704861099b23c0294f367f28bf6fb6f839ba3e958ba6954ef630f90d014b687e63decc26c395601a885583ee0f1a86de5d60069664e2b93a7e936579e934a084b3e34850a1c26dc5fe7a8479278e6000f5025552b0d43ec186cd01a4d6ae748ba9289d39046c3568982aac9593a3496d3eec561b538a9b0d9ed91a4560f504174bb70725fc1c9bc354afe11d1e8aab3389774d619fdff96ea096c8283c2555e9d3169c8dd543bc74eb9f3dd4f17105563922aa18243f7135429b7a71368b44975e07443fb6f5eae089b88dc2d729d87e303104b8a7172902cdb8500ce9cd2086f7a7f6ce683942caae21ed05596c46d8f87f98eadb034c472a3f6b9d7a83d41fa70b44beb4288973cee486adc191a9aad1d1b7cebac6225aec67eed51c347fa5c145e183454b2a514a5044808eea8a9b28b24e39b115e2faa75f04d370285684857129e14e6726bb23aa0ff9323e9687578446868705f3137289c49357ff4acb134f6f3f7dc7efe6b6a332cd2fbbef0579493c4368b70815ff13abc1e3cb08c3931d8b7c99bb8b5e89fccda77a6d51fee07754332500517b75bd26bc66c81cf9f0e8a34e01c700b86590fa1cc7682a6d20c10d325d735dabc43db87ae2700e4966d8be90e408aca19ba1327ab209d4a574095e37285880c31cd631aabafa6b6c31b6db80b989ec57141ab35a5d7e32445f153da3832b223831bcaa8d1cedf6cf9d8e2dc121fd1b8a744481690be8668d6185113853ad1e86fbfe5a1b216208d3300532b0c80bc751d805260f1e6b014fe8f0b904a54560baa1b9f9d81479024b9d722fa577d9708eeb5b84c5005766f2e2c622689e1d750dc16ddd74450fedec590cb49014d4cba6dff6d2aa2c15b71067c8c02128e587f6a09556ee2e8e6b1b6bb8380ea6e79f43fb6e427fa66f0b616b84261b18f07d3607eaf4b932f38fd7267c742c503862458a1d1f01b239e94f39bcce1864564ce2f021fd43673964e77d3616bf8405b4c770996fa48d312458b29be5c05a6941ce5b3b2ff4ff32ad784ea47a664fbc2bec22fb6faecf928539773cd5c0972d9e6a10762a66190c3b5cc9b77d3546ec02d355aeec68ca9af34b2f8c44071b6d9b608f17e24ec3ac185cd9fab9b9bfdfb7873e748796af1b87820707752798534334b0e79bf108d60ef61a2c3cbdffc663b54f2ef407ae43749d31681e3da13ab3beb1f4ac2053cc7286bd7a3192ca412d1692ddcb1fc33131c19bc312d72b1616b91110fc15a8f33b2429eb0fd07bb034405bb879e54cf2f4331c2fbafc69cca8c2ff5a762c7f139edbe123735e63490a17f309ec895077731e5f6d6a53e513f1a3c15eaf72db4605d65eaf5cdc830155f5b9bf4e96a3644dd0ae9ab1f09778fdcd763668507d1c49663dc4a64def5f24577d2b1b34113f6a2a92999bcaf649536e431b8c2c30491cb54fe84b7e087c7fdd1e9b92dfaa31651138e7602602a45d331528ebd79b450ea237b195e80a04a2261a00fdd918cd0b0116f9d00fd05aa0a007eacf337b3a80941f2defd530cf824f13cd263ba2ca194e7170072ce28e800000000000000050000000001c9c38077c3e8caa53f1a775a5e89b1a54488aff835593d5e2b7355179136bbdd10a86c00061b100003d4b11a6510316810f0248ea0530220e3255171fd5fce2eee5d0bff33d694904b77c108bd2c322ebc2c3231e1f78618ab9ba83f81fcbbc7278f4d0cf17edb4342ccf40ba133606f6597f474b0dde594a6f8af078aadcfc940d5fcccd32793a1266190f06e55aec9b95b9f2e552ee8d5e4a662a7cdddb20599b14e3105699f2618395ca9d80ca232f5a96e48e27a09b24b3c28b38b42bf63464dbfc0d233c635d1379784581f5ff2f2fa2c6dff4cf8c12454745c695ea394d0fc477f4907e82e636e347306ee70730f61aa75ddbb10cf023b28a5e1f7dfed48470bf4b3180e343ea7f578a0d8f33505aa36b3875226b668376db40b3171f5a2eef326b0a416fd27cd7c7936e71cc9822421d9e317db5fa31de68f4be412666c3ef4c699acbadf94d2bfd1f32ad1f962da04965d20d783047a415f5f6a59117422dc26c5f05a5ef0180081e9b7771277ae8520647e833c2dc80149baea35700c35aadce8c4c81ef6e3fc3ec55e3fe3b9f846710fd648668ca3ba05f6b7cf5eddc9b5a7e73f2ac4d1f53864765cf70f55128ec53692bf26656da9468ea93b7030063a2021ad57cf3ab1df019c21118f6f9cec64def026910242fc4809d2224210460d63d02f2d6475b371b97907cc3c1e72f56ce8285cc08fd4e37f439f39b63667c045f4b5dc5f982cdc8b32fa0392ce38dccfa6a79f5bcaccd2bdaae666404947fd8b6d8ad7e16ae1307d7b24cc15d712814d1311bdf2155ee28da446ddd2bd0e782c225a8a379073314ec486aa2a9dc54c4edc2e2dea278f39ee23b0d7c2f5df7a20ffe5d77541d1c42a94a17e977763f15dd889c939104b691553ec8c99d5c87a9513553a7384e3a45fac0193b77fd8b7a2c5bf291a6954c767addeca4c91a04946688491c56818bb9de51d2e2dfc2c0b2e8f3e929ba04d9b3f457935f8c797dd770cd6558b6238ba0706a500bd9aaa17565a8a8b8abb41e4717f312d9b449de7dcd5317e1f6ba71b428f5e96857fc9acaef6298d4e994746a45534715af1326c79836f0f3a7ae3aaa41c90f172ec8544d13086237420207fddbae7d349a2871a62f465a6c57bb54e623c36067f41f33282a8548e5047d3d2da4c83483811ee3559bd7479216631281aeee1152f1fb876beb57b51c9cbf002ce9e6c81129a119120e24789958bb1de20448f01692ea4b955099b808af3c4dc7ad6652a9eff075f91fd3bbf54ec7d450c8000005894542b810974b36f6a66125e047dc491aa376b70aef9fd65379c0668937a86e0c12772520a151df018c031b931b2e0c0a25419f10080fa9010ddee60b4861692ed00bef79ab5cae1a4d5baf62552a111845c3969538bed014baac2169b2c6e847b3123a7aff9761736ba9e2f5f4d0f94852c921599615f91bbbdeb230ddf834305c9c70490307c300952b8e61e5255b31cd09f2ca7cdbb4c4e76798a78c0a8709d86c7dfd5dc008af596308a36e0d28c4ba60d68151858e740ccbcc068b0acb3fb81bea92e4e979ac39057ccb19c8603658efc62f6222621cf956b377b7ff0fea770908f04600c2720b03371a62ef941159fcc015248ba326f31d1bc82e4d94c186d4f6c39e1f55600221a2cdf85b560b78dc5fb3e74069021e9062e2e7c6173716ca70dc843cf02070253d31ddb9c4b569df4fe243b78a8300788104b575d641108510bafea96a25bbea6b8f6474fa1886de9d525a4fbdb480afc4ed96482ff2aa8e64142c285b11c87ffef811b5026b3a2587a91aff83c6825c4d0634ccc7aab85e795c11491f3e0883568c23e67efcd4c26a5db3ca0b5c3bbded3c6e355738319c1a087d8da87db4da52d4c5214d272cd1631b2843cbd6b015e52eb51a8bb759d529ae6ab7b397aa8963f2074c9278fa8499de47dc9152bb8726de26f5bbf24e9ca7e329353ca2da6363b4e00002710000000000598cd44000000002faf0800240a007eacf337b3a80941f2defd530cf824f13cd263ba2ca194e7170072ce28e8000000002b40420f0000000000220020e2eb1e4df160f9e11d4d06a6075034b47e9d3e1c3d21d0feab64905d2e6ab17b47522103174f3de90105ae02d88ccc1a7ed4bf0d116f27ef6010f1ec308273c1010ae3dc2103548b338f50bdae124711cadef7a82ce832bff61826f13b031677793abb8c674c52aefd0205020000000001010a007eacf337b3a80941f2defd530cf824f13cd263ba2ca194e7170072ce28e80000000000a1c4a78006204e0000000000002200204c81bcdcc2c4f01d3c045348a155c9249ddba4d48c16ccf33c1d8b6e34614180a8610000000000002200204c81bcdcc2c4f01d3c045348a155c9249ddba4d48c16ccf33c1d8b6e3461418030750000000000002200204c81bcdcc2c4f01d3c045348a155c9249ddba4d48c16ccf33c1d8b6e3461418030750000000000002200204c81bcdcc2c4f01d3c045348a155c9249ddba4d48c16ccf33c1d8b6e34614180a437010000000000220020680a82e78074810847ca530cd74fabdbd2e34beaf0fdd2bc832ddfcd2669876c00350c0000000000160014e1fdef3169db3728018a906129e3913dd841ab0f04004730440220639ddfeb4cfd418c28418208226f5f6be762b65bb242968695ac1574d1fae8e702207bd3b3f60f12f171a37e50b7126c709a4cb87e417efdfe520707962d393bd28c01473044022058407ee97260aa8020eea7036f348ec4959fbd8a820e8628244705138803e00e02207d53e7fec4e01bbc04a0495e8c31210c0c6c2542e38d6c9ae677b78bd568cedb0147522103174f3de90105ae02d88ccc1a7ed4bf0d116f27ef6010f1ec308273c1010ae3dc2103548b338f50bdae124711cadef7a82ce832bff61826f13b031677793abb8c674c52ae340169200004000324efa7874aa335cd976b8209145d670a6dba17471f6a4f47dacaa432dd972d801c000000002b204e0000000000002200204c81bcdcc2c4f01d3c045348a155c9249ddba4d48c16ccf33c1d8b6e346141808576a9144a2fe35d15fdacd3b3cba332b5879b6acfb2e79f8763ac67210375099d4e25580f8d2c59718d7319e9d26c7119f9476ca3a623fb638843d3c8617c820120876475527c210375f5c7f3e5f1da49febb149cb9dc97098402628f84b79b586d5d516c17e6327852ae67a914950b69c651378982bec084b296e6dd23a33c41b588ac68685e0200000001efa7874aa335cd976b8209145d670a6dba17471f6a4f47dacaa432dd972d801c000000000000000000013a34000000000000220020680a82e78074810847ca530cd74fabdbd2e34beaf0fdd2bc832ddfcd2669876c111b06004093d252d2f22cecee6c2b604a1190120db574f0ba33b49ac826229b95071230bd7a4baa7a1704813804ba9b7d0e2aef3932a7d7d096862c1636736ad3296a46f5401f096356d49cb9c0d1dad9a01530aa892aa75cdcb626f42ef09b734f39d0a2650d6274377598b29f3f3eae1281a8b61d5cb9853b478cc973b8035af5d60c26a0000324efa7874aa335cd976b8209145d670a6dba17471f6a4f47dacaa432dd972d801c010000002ba8610000000000002200204c81bcdcc2c4f01d3c045348a155c9249ddba4d48c16ccf33c1d8b6e346141808576a9144a2fe35d15fdacd3b3cba332b5879b6acfb2e79f8763ac67210375099d4e25580f8d2c59718d7319e9d26c7119f9476ca3a623fb638843d3c8617c820120876475527c210375f5c7f3e5f1da49febb149cb9dc97098402628f84b79b586d5d516c17e6327852ae67a914950b69c651378982bec084b296e6dd23a33c41b588ac68685e0200000001efa7874aa335cd976b8209145d670a6dba17471f6a4f47dacaa432dd972d801c01000000000000000001c247000000000000220020680a82e78074810847ca530cd74fabdbd2e34beaf0fdd2bc832ddfcd2669876c101b060040774ca3fbbee6dc29db89dbb2bdb977afdf31727548aa6060e9e23a79090bf658474a5aa2de53dae36371ec535e13e4291c9fbd1942e7cc812ff4ca4c9d84483c4088605547607e6c8b1b24a671ed340700ad396a8bf613fa9e4646c0b11c2aeff708e4448ba70fe48903e3034ace605d8651091fcd6f80fddb57d2e2b09da6765d000324efa7874aa335cd976b8209145d670a6dba17471f6a4f47dacaa432dd972d801c020000002b30750000000000002200204c81bcdcc2c4f01d3c045348a155c9249ddba4d48c16ccf33c1d8b6e346141808576a9144a2fe35d15fdacd3b3cba332b5879b6acfb2e79f8763ac67210375099d4e25580f8d2c59718d7319e9d26c7119f9476ca3a623fb638843d3c8617c820120876475527c210375f5c7f3e5f1da49febb149cb9dc97098402628f84b79b586d5d516c17e6327852ae67a914950b69c651378982bec084b296e6dd23a33c41b588ac68685e0200000001efa7874aa335cd976b8209145d670a6dba17471f6a4f47dacaa432dd972d801c020000000000000000014a5b000000000000220020680a82e78074810847ca530cd74fabdbd2e34beaf0fdd2bc832ddfcd2669876c101b0600404ec5ef779e7b0db022d9da0986bee62eeb0372337f3974e0bfa8ecbddec7a3ed47930ce5ca6796d3cde21dbf9e0678f4d0af5d84889c24e2b960786d141b900a402c736579e487f2ceedb15546a438818e9a13d3b9f9232d4ff418855f9112795e2bce08fb52f8737268b616158eeb06e36abdc83942396fb407d3e603ed1edd7b000324efa7874aa335cd976b8209145d670a6dba17471f6a4f47dacaa432dd972d801c030000002b30750000000000002200204c81bcdcc2c4f01d3c045348a155c9249ddba4d48c16ccf33c1d8b6e346141808576a9144a2fe35d15fdacd3b3cba332b5879b6acfb2e79f8763ac67210375099d4e25580f8d2c59718d7319e9d26c7119f9476ca3a623fb638843d3c8617c820120876475527c210375f5c7f3e5f1da49febb149cb9dc97098402628f84b79b586d5d516c17e6327852ae67a914950b69c651378982bec084b296e6dd23a33c41b588ac68685e0200000001efa7874aa335cd976b8209145d670a6dba17471f6a4f47dacaa432dd972d801c030000000000000000014a5b000000000000220020680a82e78074810847ca530cd74fabdbd2e34beaf0fdd2bc832ddfcd2669876c101b060040ff0a3853c07d426ac93a8b772017a41f324558851ec69bcd65ed1cd81490e0f11c3c01574fe196df6311cf21fbb78abd7d40659479ea080c7ccb3c5fd53027df404f63e06d948b7890c8a3fb1ae568e88de8cd4ef3364c092bcdeb006c40558b082a5fc572f9f02f5d2a0870101ff7d900da184fb19b5cd410ba0779bc55533c4a00000000000000070005fffd05aa0a007eacf337b3a80941f2defd530cf824f13cd263ba2ca194e7170072ce28e800000000000000030000000001c9c38077c3e8caa53f1a775a5e89b1a54488aff835593d5e2b7355179136bbdd10a86c00061b1000031a12d4e5b7253515c5822879f6f1738224e451910cacebbb545aa637e2a0f631a151e02f206b6397055534bd6a0b7c7990d269d153d3886fc7e51fd4e09c5357a96694911e9912135e3f9ccec1058be709a743ee44f07feebb0fc6a3078bec155a1b9f67afaac6914debcc890fd86150d790f8de1b7168d4501064dacfd16c19f61a8c13c7658df92d6e5ad79b1b251a96db66bfc498e8e4114f98b9b36bc41663c9d2d15a280fdae479c14e52e148b0f86abbe4f9ef78570e193585b5aa30e53d54352eba5ad54d238f0d756b54aca9889fb21c57b6c639a6713a4f655d5dde92391185a9d8c929bca42c3bc9ba5f65b059dcbab02e89cf2e95fe1b1fb291aa1c2aeb8b57ab24f92414fc296a37b683a1d47155acb5210ffb6a170642e0f1cf7a4704861099b23c0294f367f28bf6fb6f839ba3e958ba6954ef630f90d014b687e63decc26c395601a885583ee0f1a86de5d60069664e2b93a7e936579e934a084b3e34850a1c26dc5fe7a8479278e6000f5025552b0d43ec186cd01a4d6ae748ba9289d39046c3568982aac9593a3496d3eec561b538a9b0d9ed91a4560f504174bb70725fc1c9bc354afe11d1e8aab3389774d619fdff96ea096c8283c2555e9d3169c8dd543bc74eb9f3dd4f17105563922aa18243f7135429b7a71368b44975e07443fb6f5eae089b88dc2d729d87e303104b8a7172902cdb8500ce9cd2086f7a7f6ce683942caae21ed05596c46d8f87f98eadb034c472a3f6b9d7a83d41fa70b44beb4288973cee486adc191a9aad1d1b7cebac6225aec67eed51c347fa5c145e183454b2a514a5044808eea8a9b28b24e39b115e2faa75f04d370285684857129e14e6726bb23aa0ff9323e9687578446868705f3137289c49357ff4acb134f6f3f7dc7efe6b6a332cd2fbbef0579493c4368b70815ff13abc1e3cb08c3931d8b7c99bb8b5e89fccda77a6d51fee07754332500517b75bd26bc66c81cf9f0e8a34e01c700b86590fa1cc7682a6d20c10d325d735dabc43db87ae2700e4966d8be90e408aca19ba1327ab209d4a574095e37285880c31cd631aabafa6b6c31b6db80b989ec57141ab35a5d7e32445f153da3832b223831bcaa8d1cedf6cf9d8e2dc121fd1b8a744481690be8668d6185113853ad1e86fbfe5a1b216208d3300532b0c80bc751d805260f1e6b014fe8f0b904a54560baa1b9f9d81479024b9d722fa577d9708eeb5b84c5005766f2e2c622689e1d750dc16ddd74450fedec590cb49014d4cba6dff6d2aa2c15b71067c8c02128e587f6a09556ee2e8e6b1b6bb8380ea6e79f43fb6e427fa66f0b616b84261b18f07d3607eaf4b932f38fd7267c742c503862458a1d1f01b239e94f39bcce1864564ce2f021fd43673964e77d3616bf8405b4c770996fa48d312458b29be5c05a6941ce5b3b2ff4ff32ad784ea47a664fbc2bec22fb6faecf928539773cd5c0972d9e6a10762a66190c3b5cc9b77d3546ec02d355aeec68ca9af34b2f8c44071b6d9b608f17e24ec3ac185cd9fab9b9bfdfb7873e748796af1b87820707752798534334b0e79bf108d60ef61a2c3cbdffc663b54f2ef407ae43749d31681e3da13ab3beb1f4ac2053cc7286bd7a3192ca412d1692ddcb1fc33131c19bc312d72b1616b91110fc15a8f33b2429eb0fd07bb034405bb879e54cf2f4331c2fbafc69cca8c2ff5a762c7f139edbe123735e63490a17f309ec895077731e5f6d6a53e513f1a3c15eaf72db4605d65eaf5cdc830155f5b9bf4e96a3644dd0ae9ab1f09778fdcd763668507d1c49663dc4a64def5f24577d2b1b34113f6a2a92999bcaf649536e431b8c2c30491cb54fe84b7e087c7fdd1e9b92dfaa31651138e7602602a45d331528ebd79b450ea237b195e80a04a2261a00fdd918cd0b0116f9dfffd05aa0a007eacf337b3a80941f2defd530cf824f13cd263ba2ca194e7170072ce28e800000000000000050000000001c9c38077c3e8caa53f1a775a5e89b1a54488aff835593d5e2b7355179136bbdd10a86c00061b100003d4b11a6510316810f0248ea0530220e3255171fd5fce2eee5d0bff33d694904b77c108bd2c322ebc2c3231e1f78618ab9ba83f81fcbbc7278f4d0cf17edb4342ccf40ba133606f6597f474b0dde594a6f8af078aadcfc940d5fcccd32793a1266190f06e55aec9b95b9f2e552ee8d5e4a662a7cdddb20599b14e3105699f2618395ca9d80ca232f5a96e48e27a09b24b3c28b38b42bf63464dbfc0d233c635d1379784581f5ff2f2fa2c6dff4cf8c12454745c695ea394d0fc477f4907e82e636e347306ee70730f61aa75ddbb10cf023b28a5e1f7dfed48470bf4b3180e343ea7f578a0d8f33505aa36b3875226b668376db40b3171f5a2eef326b0a416fd27cd7c7936e71cc9822421d9e317db5fa31de68f4be412666c3ef4c699acbadf94d2bfd1f32ad1f962da04965d20d783047a415f5f6a59117422dc26c5f05a5ef0180081e9b7771277ae8520647e833c2dc80149baea35700c35aadce8c4c81ef6e3fc3ec55e3fe3b9f846710fd648668ca3ba05f6b7cf5eddc9b5a7e73f2ac4d1f53864765cf70f55128ec53692bf26656da9468ea93b7030063a2021ad57cf3ab1df019c21118f6f9cec64def026910242fc4809d2224210460d63d02f2d6475b371b97907cc3c1e72f56ce8285cc08fd4e37f439f39b63667c045f4b5dc5f982cdc8b32fa0392ce38dccfa6a79f5bcaccd2bdaae666404947fd8b6d8ad7e16ae1307d7b24cc15d712814d1311bdf2155ee28da446ddd2bd0e782c225a8a379073314ec486aa2a9dc54c4edc2e2dea278f39ee23b0d7c2f5df7a20ffe5d77541d1c42a94a17e977763f15dd889c939104b691553ec8c99d5c87a9513553a7384e3a45fac0193b77fd8b7a2c5bf291a6954c767addeca4c91a04946688491c56818bb9de51d2e2dfc2c0b2e8f3e929ba04d9b3f457935f8c797dd770cd6558b6238ba0706a500bd9aaa17565a8a8b8abb41e4717f312d9b449de7dcd5317e1f6ba71b428f5e96857fc9acaef6298d4e994746a45534715af1326c79836f0f3a7ae3aaa41c90f172ec8544d13086237420207fddbae7d349a2871a62f465a6c57bb54e623c36067f41f33282a8548e5047d3d2da4c83483811ee3559bd7479216631281aeee1152f1fb876beb57b51c9cbf002ce9e6c81129a119120e24789958bb1de20448f01692ea4b955099b808af3c4dc7ad6652a9eff075f91fd3bbf54ec7d450c8000005894542b810974b36f6a66125e047dc491aa376b70aef9fd65379c0668937a86e0c12772520a151df018c031b931b2e0c0a25419f10080fa9010ddee60b4861692ed00bef79ab5cae1a4d5baf62552a111845c3969538bed014baac2169b2c6e847b3123a7aff9761736ba9e2f5f4d0f94852c921599615f91bbbdeb230ddf834305c9c70490307c300952b8e61e5255b31cd09f2ca7cdbb4c4e76798a78c0a8709d86c7dfd5dc008af596308a36e0d28c4ba60d68151858e740ccbcc068b0acb3fb81bea92e4e979ac39057ccb19c8603658efc62f6222621cf956b377b7ff0fea770908f04600c2720b03371a62ef941159fcc015248ba326f31d1bc82e4d94c186d4f6c39e1f55600221a2cdf85b560b78dc5fb3e74069021e9062e2e7c6173716ca70dc843cf02070253d31ddb9c4b569df4fe243b78a8300788104b575d641108510bafea96a25bbea6b8f6474fa1886de9d525a4fbdb480afc4ed96482ff2aa8e64142c285b11c87ffef811b5026b3a2587a91aff83c6825c4d0634ccc7aab85e795c11491f3e0883568c23e67efcd4c26a5db3ca0b5c3bbded3c6e355738319c1a087d8da87db4da52d4c5214d272cd1631b2843cbd6b015e52eb51a8bb759d529ae6ab7b397aa8963f2074c9278fa8499de47dc9152bb8726de26f5bbf24e9ca7e329353ca2da6363b4efffd05aa0a007eacf337b3a80941f2defd530cf824f13cd263ba2ca194e7170072ce28e8000000000000000400000000017d784077c3e8caa53f1a775a5e89b1a54488aff835593d5e2b7355179136bbdd10a86c00061b1000020b7604aeb7cb080b12de62f75a292bed3134b4a3de764bd032fb049e2c2a1d3942cc11b9b12619d42cda3c7eb63da952644a3f355bdf772d77f1fc2a0b36bf116cdacfcda3b77974536cc3f3d0f8209dba69408073cbe0f039eba220fcf3b0b96d5875bb71acdc999fd9feeab68a0b63629efe0dc91e92dcf93b3c49285c981746a68ead7a96a9c1ce21284a2796b0051e6b7cf36a264d545063c054454b87cc3f9ddad5350382585de18cdc4e65e02484695f541b8fc082c87ea4a56aabc84fd1b7c1cd40e3517ae811f9ff921fba672e0191c88a2681ef8ea1300eeb6c1d8fccdb5ab05bf80b2a5c3ed5c018752cc841305edae02149389bc5e58ea5a59a1c59755c7ab5f35e3740c9f6218d227370363a7e386b214f5b2e771ea8b94a2a5e640d8a715d57756665f4a659f4ef6488f9ceab5e22d523ee52da368701d799fcb3459a31e6fb03de91ffd9b608656b172945015da3e5000f7f6c3dc0b4e102ba5f101bab5c2ceab8021d071bf89cf9d65d2439fe9a9dea4291eccb43a431904313655391ace4b82bf5da6270eb5bc12a60a69e7cf96b75a699db5a374713558eff7101bb2d8471e0c4ad0e202fd10aa46098bd5565c1d410e695fdd799c866090f1a7aa002305388acb68c8d82d588c1a66df5fb12653686bc4cf487a3484c0c23578a428de3df57539ed11227f2648bc36261c7f3f4f13e18d238dc856b8dd8f07de9fb73663d3a0026a03d9ee9f807e2a03c9546d2537a1f30a1f06767622d8bacdf4ab3b0f245a7b4f482baa60080fce3e7e15d2e086c670d6ec11d907e3da593977c8c25d620ed40cf82dd503c25f1a5f3e8a5ddb65ae7117600794844252e1448d17849028bd5df960a5fddf56e48cf082cfbc5c29666bca6af335c06f8240912a311e6c07946eeea9ef57e46b3e48b95c82cbee3830bf2b55d5e0223b9480fe072729a8af4c6c8c2f86038cb8a2e02405ce5f834c87b241fcb9963f008c273c64bc1f7289cc3a0c5eea7ea1a6dc7bc6228a97978a2ddd7dc49303c5beb15b36c8044f656716cad0a62d1e8c381db46f007be7f8096f72d3a8f08d338d9f2bbc43c0244215df229824a9ad34b655d840b38c5061462d54358be1c184b51d3a805beeea78047f544b15cc333d591f3d8cb60cbdd78e58ed47d21739762957ed6365fea14d7ba8368371fe4333be4f5ae289ff444c9f377a072a3a4173bd2680c6d6074743908d9c4f09aa30d064c7866a7a34303fce53deba6a597a24c211d209815bbff97736ba9fc79e390b36b2343d2590c8fd6db3e54206580eb7a53b9de09a42cfaa2cffb00ea90dbd07f1d357e69ca96fbba4efefc4e07b1a1a174a9a8a5568c0ec9a9487add3890a20391eebd69a56c4dd1a7c7112766b05d29fcfdd0f60d8c3f7287ec070201b4d0200e3ebbf47a97c230a7d2f05b25bec59cfd125f6cc15529f13fc624ebfd387327119434b66e61a8446e9fd9e14f4e0a42cee83f4600b195a06f4f0e55d67239c55256db69c3eb5218f9d214e0d9ca0992dee76630b55a96136a251a84711096475028e326782112f2afd64c3795f438ea623754cb0ea5e6a0d4e1ded7ee633f1eb8bf71b791646c8953e54a07242a178c02baf8fc68e7c1d3e6982b265802f50353b7f1eb0d8f33c9aed1b18432ecd04d10f951473a0e7d7e3e88a3cfca46ad1b11c5abca59bab0c8e880519753b9f4a9ab4f7a756d58a389dc3bdb25940a71a03899125b08efd7ec85b3cf0c1577014a81bf63b2144617cf259383a41af2d50a7ebb7d45a8968e3e3edfb1f4a45368ba380d172f82e641eb16949e192a3c2a37117df8e22fb7a4e7ed425f9951b392bc59184db81899ed2270844446a29aaed5cb3839c392d51636a24b737a3100417a849104c08606b4644a785080b67a92dfe888cd9871fffd05aa0a007eacf337b3a80941f2defd530cf824f13cd263ba2ca194e7170072ce28e80000000000000006000000000010c87c77c3e8caa53f1a775a5e89b1a54488aff835593d5e2b7355179136bbdd10a86c00061b100002a9a1e8cbe48ae54ff2f32eb81ac70966e623abd77532a8cef6c3dab174da1e2879020c2238504e525d2d3fae9adb39044e8e6d0c394ae9e6ff1f01ae21e354d0bd1a5970e77015dbb6a635157b6465c32aee891c26ff8942ccfd24b612e4df241c55fc34dc1a5a613d431b813e14d0ad78023314f93c38f2bba6a6ea6e642d59a547f97b306ae0d072944c62cdec15933ffd85dafd9cabf278d9b8121db56915075baa049ac0d47df8a32b6c54a9935fbc1284fe8af288d3dd72870c8faf77e34a20970bf44b47bda06f2b8bfc931252456760ba1fa58669a3ec443737b429095db43f5bdf8e5d7f2d2f9c7318cfe012213ad35d54751539605337143de28756a146779a41308dbf94be4c9290422cb79ba768c340bc91012488afc3b388fbc253bdb9a35b1f4e39307c89f9ec3bb58e5da660013a741c1ab6c0cb22a3c94db0bf6d207599a527f18831530179880ef00950886378abcb99d2d1502b874669fb8f0a596ce3909e314057d7d1f668ca80e23c46f86127a71a729021623038d23cba281ed4967ea654470d4fdb31b297def0d22f5897f58d28af0cd86287fa51722af2682fe259987f56a9fe74af94138bb520ed12bd86453cc44f31f00e337647997b56c8bf40e3e9233e42b4c4ffe2ccc438417c754e04c95a2e7f357eb238020a0b235b8757a40996b6dab0e09e526f891f1773153151d99f3f65e62f0db6d4d94fb18293ea2f13e8c900d575cc0800969ef1e6903e6958c02dac84a8e863bcb8226bd3c6bab1038b628661333449d801b8b739bb08e09daf59c969691306e9e09c68508c5a558708a9b138fb841906a1967ea6290198be18ea3941a72a95b1b55e38403bda57d391cfdb86db844a0d9b4762a550cb1d7010de80fd6507d1e2dc7830ccd49784f8e80a3c87d9301339572b0347db9336bb7b1ede0fd1e187ca94aba942fe2b5c230f7dd1d3d0376aa882136b112883999227e2be17e9d681fb813acb70807b47e99bac127ca67c2d9619049783f8a9089d3fea4275585533683308ab94f3f02f577fb840ffae279813439fcb30eb5874329c48160768c2ca6b5d6368775989b96a056f22f7dddfbcd1b22a7fb74bb2d1b36f3e94914b82709a9351e1be88364a17a36b2a92ef61bb0520fefb802393b9e9effd6d54741ab9c6dbf28911bc45b334d9c2a6d76d94ecc2a19a3115d0f2b5e5ddb1f29fc9db0b3a21651afc2f93d321189621d8b59ab07e8d4e348f4bdbdb25b91d19b313850afddf92300c7fcebd9b266792745a24a505c56a3f98428ec70b65b0396a46836cabe49024d382eb1c8227e2df4da21385960f3d3801fb63d73ae69a4fcb6aca7018e874e8bf44f73c5e845ce81b2c643862c0344e3f4e50f4e6cb51b53a6c90ea545bc897dd73e2d2b3250711ed2a666413ae85b22e848735e8a6306e3b80f3677c8f40f85bf2f457f7764343288530a9c58b160de657df482af5eb0f291a50071d207cf46969643c113a6b15354b0b590885fbf4a7136939463ef66e013cb794ac6ab0fa887352817827ffc420bd8d1ee0b6a04d6819eadc1d96517c2edd8fe028101cd9842e407a44a8eb6bbda3fc5c54997fa43aaf340ed8ab6f75e3b588dd906ebd9deba889711b3895bd7df494f042e68cd78d7e75c237bd22b90e49e5571e169487508dfea22bfe2c83f21ec1e91f7d9331e5ac4ba6c52c1183afb1e083f3d0c42af6cf41e82e9151b5717b934ba6e4f52757d27a7c8139c523f977cb9c7f29e4a19cf41a1288c60a9ee10f58189351a51925422276e2da9ce3a4ca6944b767601e686745e88b7902e0e110dd66189dec74e3abbc76a4045d5051a6b76c91b2b20fc2ab1ca7d5d63f25d2c83f5d8ab31034e3ca42c4af1ed6017f5b10c2f44a52d78bf858aa7c148b6f07156ee365cd463d3cfffd05aa0a007eacf337b3a80941f2defd530cf824f13cd263ba2ca194e7170072ce28e800000000000000070000000001312d0077c3e8caa53f1a775a5e89b1a54488aff835593d5e2b7355179136bbdd10a86c00061b1100030159105c3e04b59668b1516a793628be01d018e0de5972a7e6094a8b29761bd07a1654dc17ab21c8f940806cd4ccfd904db9594a8170f310b1e2d95e0e8b0baab7f8484c2c66a91b7f4274ec9e5cc3651ef2ee3a0177281548905a3c7d03c4ddf2bf8b736a940d69f2a5e6beaea33d92ce189a96803654a52b0d3da0cbdbbe0725e64dc4a6633f33318de0f6ca50b9f2a79ac68fdfe3a6f6c01a5215dc3bdfb56a780dc17968c5afd3270c50d4f7e1f540364e184d460e8b8479719788023ed029ad1439d0a0c85570b7a3f50ff70a82bd9ff9c04d6d8154046a30a3606ce25c2fbda28c863109cfaa95b7fe4f96622c715422ed1a8e99ce5bdecadb7c39798822115215e3914409d5de1e2c95a6c375c530b27fd0263d02167eb28628b945280a668f88c607bd74e3aac0ac704dfda9e796a76e6a856aee502b41c6f3932b6a89425484d580b6abc232cb12530b38b5c20cbbdcb0357210c3ad09a3ad7a56353dbbd6087f7cbccbae1f656ad06d370e4ec5a7906c9841a1f36f24ba6eab5c70b5d66bc8aecb60d9feb4576ef33b23456332483f01fcd0e01477789448ddc20fa207f496db56203fdaf53f6d64d97153d8e8c8c89c821f526f4b376e929d7598cf368b826313eb0ac5e435097b3f4acef26cfb30e63ab70622ff1ed04f9a1c02074bdb2d302d932cffbba4f833ab95912272ec87d8c05a8c5f1b2b101fb78a75cf9d0ab6e751163f27018f08c2fae76c7a6a417cf5cae2cdbd867115d9c1f162e83c3fc4f52202d3471212c3ae6eb39f882aa322609958dfbc8ca3f3a2eb051ef1b43d452b54c2352e8c9d184cbbd46ea936161067893164a72a34f6e2fd63c94353e524c9db699d15cef86f970bcf6a92dc367152f9740e84b2c292dbb338e4f21e6a47171b5a5cec0f4026906b528ad2c115866fb9e807faa200b600637abd774148f7637789946a82ec5c8e35692470c0c0f12a6a39a0c5dddf73635f2dbbb82d9ea56d8313a25cfd879ee024ee5144817b1f96945f34632cf0a5a3dd3ebcb8b42bb6c4b1c3d2a7fc6f817c97ec6282a3981a8d9e3d7070c178f7df847fcec8013b0a0d07a4f8c13c829209172022e0d724caf5a01219575f1d5a26f50f53d72ef8b596ae2263d2c2eeb822166178d56eba9ee9a9c60adb4fe3a7be89044d34288c7bd59cbf6e88e01f971b687200fccdc68d5a21f8430a8e5654a17f05773b2254eb301a5624c4aadbd3db8641cfe866ff72424a891f5e0487a48c204870c932ed9895427ce9a3d0a183fc40b7bbea3617b321f35cbba2c2180908e9bea2e07e1e5d8d5de9846994b78a313b8828eca935f4bdf9ca861326cca20776c6a4df35eeee257899467973941e6e8c567054480a7d8077e6e61ab480562ad53ad8127874a7fa4f747ccb2601cfa6e8c43e89f678dd38e4df80adef7bf07149edd32bf0e9775f3478870da6af26709a5bebbac77018050a07f1a8ffe4539c2df9de1dfe18c9bf0aa640178b0912bd9631acebdc2f2f993039b6cfa414d5cfcd0c363a178443e954db312efcd4463ee7cfe0d8a1eabe74d47692653f996cdd9b93c9fad41c5b16aa91a7bd5b267578a537e45d2af048ee0ce09ea41c72020d61509e5e91575022e7d5956b8e680f229909642366d5a16a17f021f41ca3d13df75ac3d3c8c772c2ba042680ba81466f8d05c07e19b1cde5a4d27ba6b820a63fffc5a42234e18109f128cb1e3524d621e38855b5c11cfbe7edf25dc3cd470308971b655b3381f946531d89ed23ad24f6a3cacd022fb9e5eb1d5a188a5d0936d0dfe3c7c424dba5f8892b2adda87a171d73bb5c5b8f809812689794ee3a5a7dc7d80f9c84bdad2fb7f89b2a45dc25836a42c8757eaddabcab7f82e1e51502cdcd3c4745009c55fd70b1c8f1a748e71e2b96b312f71f21cb00002710000000002faf0800000000000598cd446da1a38c05425e396afcb5fbb37ea2a9db15c56121e2fae11ec9f06da63917610371ece811f4d34713b42d65c38a775487c71c193a52df6ac46d6a013164681dd200000000000000000000000000000000000000080000000000000000000500000000000000050003d6ec06b2431d4e1f9c36a610af90d6c40000000000000006000317def351ac5745148ba3275aacb2b68f000000000000000700035d913d0e286346e1a50fe900f618b18600000000000000030003c35487d4d1e24e38bb4f923885d19b44000000000000000400030c7e6ecdbccf41baab2f7d95ca9e5e9bff03d32570a2783f1792d20fbe141f337d5dded9509c875e13288fe140400f36560e240a007eacf337b3a80941f2defd530cf824f13cd263ba2ca194e7170072ce28e8000000002b40420f0000000000220020e2eb1e4df160f9e11d4d06a6075034b47e9d3e1c3d21d0feab64905d2e6ab17b47522103174f3de90105ae02d88ccc1a7ed4bf0d116f27ef6010f1ec308273c1010ae3dc2103548b338f50bdae124711cadef7a82ce832bff61826f13b031677793abb8c674c52ae0003003e0000fffffffffffc008030b0e83d8813791baf507a3c85c55d81cf80a50fd9f08504260743bb8033212800fc0003ffffffffffe80101460f774080d0e1bad18203f9af025451bfb4b8de611116890dd1fa45dd0f8e9802000007ffffffffffc80104c31735de0c70dc61744d1123646e49d871b662ae090c568a3ba7666fe370ce0c0003ffffffffffe40a007eacf337b3a80941f2defd530cf824f13cd263ba2ca194e7170072ce28e8000000000000061a8000000000fffd0205020000000001010a007eacf337b3a80941f2defd530cf824f13cd263ba2ca194e7170072ce28e80000000000a1c4a78006204e0000000000002200204c81bcdcc2c4f01d3c045348a155c9249ddba4d48c16ccf33c1d8b6e34614180a8610000000000002200204c81bcdcc2c4f01d3c045348a155c9249ddba4d48c16ccf33c1d8b6e3461418030750000000000002200204c81bcdcc2c4f01d3c045348a155c9249ddba4d48c16ccf33c1d8b6e3461418030750000000000002200204c81bcdcc2c4f01d3c045348a155c9249ddba4d48c16ccf33c1d8b6e34614180a437010000000000220020680a82e78074810847ca530cd74fabdbd2e34beaf0fdd2bc832ddfcd2669876c00350c0000000000160014e1fdef3169db3728018a906129e3913dd841ab0f04004730440220639ddfeb4cfd418c28418208226f5f6be762b65bb242968695ac1574d1fae8e702207bd3b3f60f12f171a37e50b7126c709a4cb87e417efdfe520707962d393bd28c01473044022058407ee97260aa8020eea7036f348ec4959fbd8a820e8628244705138803e00e02207d53e7fec4e01bbc04a0495e8c31210c0c6c2542e38d6c9ae677b78bd568cedb0147522103174f3de90105ae02d88ccc1a7ed4bf0d116f27ef6010f1ec308273c1010ae3dc2103548b338f50bdae124711cadef7a82ce832bff61826f13b031677793abb8c674c52ae34016920ffed02000000000101efa7874aa335cd976b8209145d670a6dba17471f6a4f47dacaa432dd972d801c04000000009000000001c62401000000000016001401396601573e7aef81f91a89fc4b4b56feb7a35e03483045022100c787b74c895285a11e04a680a50cef980d74a1ea53998bc2abaa2f7e13b6131b02204e0a8216f2efe89880feac989f9b0cfbf6aea1a61444d453d5547060aa65cc5901004d632103d6a391d54b9683da5c4c1ab1720739757422861b7ca98da226118061b40c412e67029000b2752102f2b77c73154588dfa6301b6c69062c0a713467996542a836f7a2ce6a239ba68868ac0000000000000004fd017a02000000000101efa7874aa335cd976b8209145d670a6dba17471f6a4f47dacaa432dd972d801c000000000000000000013a34000000000000220020680a82e78074810847ca530cd74fabdbd2e34beaf0fdd2bc832ddfcd2669876c050047304402201f096356d49cb9c0d1dad9a01530aa892aa75cdcb626f42ef09b734f39d0a26502200d6274377598b29f3f3eae1281a8b61d5cb9853b478cc973b8035af5d60c26a00148304502210093d252d2f22cecee6c2b604a1190120db574f0ba33b49ac826229b95071230bd02207a4baa7a1704813804ba9b7d0e2aef3932a7d7d096862c1636736ad3296a46f501008576a9144a2fe35d15fdacd3b3cba332b5879b6acfb2e79f8763ac67210375099d4e25580f8d2c59718d7319e9d26c7119f9476ca3a623fb638843d3c8617c820120876475527c210375f5c7f3e5f1da49febb149cb9dc97098402628f84b79b586d5d516c17e6327852ae67a914950b69c651378982bec084b296e6dd23a33c41b588ac6868111b0600fd017a02000000000101efa7874aa335cd976b8209145d670a6dba17471f6a4f47dacaa432dd972d801c01000000000000000001c247000000000000220020680a82e78074810847ca530cd74fabdbd2e34beaf0fdd2bc832ddfcd2669876c050048304502210088605547607e6c8b1b24a671ed340700ad396a8bf613fa9e4646c0b11c2aeff7022008e4448ba70fe48903e3034ace605d8651091fcd6f80fddb57d2e2b09da6765d014730440220774ca3fbbee6dc29db89dbb2bdb977afdf31727548aa6060e9e23a79090bf6580220474a5aa2de53dae36371ec535e13e4291c9fbd1942e7cc812ff4ca4c9d84483c01008576a9144a2fe35d15fdacd3b3cba332b5879b6acfb2e79f8763ac67210375099d4e25580f8d2c59718d7319e9d26c7119f9476ca3a623fb638843d3c8617c820120876475527c210375f5c7f3e5f1da49febb149cb9dc97098402628f84b79b586d5d516c17e6327852ae67a914950b69c651378982bec084b296e6dd23a33c41b588ac6868101b0600fd017902000000000101efa7874aa335cd976b8209145d670a6dba17471f6a4f47dacaa432dd972d801c020000000000000000014a5b000000000000220020680a82e78074810847ca530cd74fabdbd2e34beaf0fdd2bc832ddfcd2669876c050047304402202c736579e487f2ceedb15546a438818e9a13d3b9f9232d4ff418855f9112795e02202bce08fb52f8737268b616158eeb06e36abdc83942396fb407d3e603ed1edd7b0147304402204ec5ef779e7b0db022d9da0986bee62eeb0372337f3974e0bfa8ecbddec7a3ed022047930ce5ca6796d3cde21dbf9e0678f4d0af5d84889c24e2b960786d141b900a01008576a9144a2fe35d15fdacd3b3cba332b5879b6acfb2e79f8763ac67210375099d4e25580f8d2c59718d7319e9d26c7119f9476ca3a623fb638843d3c8617c820120876475527c210375f5c7f3e5f1da49febb149cb9dc97098402628f84b79b586d5d516c17e6327852ae67a914950b69c651378982bec084b296e6dd23a33c41b588ac6868101b0600fd017a02000000000101efa7874aa335cd976b8209145d670a6dba17471f6a4f47dacaa432dd972d801c030000000000000000014a5b000000000000220020680a82e78074810847ca530cd74fabdbd2e34beaf0fdd2bc832ddfcd2669876c050047304402204f63e06d948b7890c8a3fb1ae568e88de8cd4ef3364c092bcdeb006c40558b0802202a5fc572f9f02f5d2a0870101ff7d900da184fb19b5cd410ba0779bc55533c4a01483045022100ff0a3853c07d426ac93a8b772017a41f324558851ec69bcd65ed1cd81490e0f102201c3c01574fe196df6311cf21fbb78abd7d40659479ea080c7ccb3c5fd53027df01008576a9144a2fe35d15fdacd3b3cba332b5879b6acfb2e79f8763ac67210375099d4e25580f8d2c59718d7319e9d26c7119f9476ca3a623fb638843d3c8617c820120876475527c210375f5c7f3e5f1da49febb149cb9dc97098402628f84b79b586d5d516c17e6327852ae67a914950b69c651378982bec084b296e6dd23a33c41b588ac6868101b06000004ec02000000000101ea542e3a509a29b8d2afac84f21bc8f4e100df901b35c9fc19a331c588683519000000000090000000015c2100000000000016001401396601573e7aef81f91a89fc4b4b56feb7a35e034730440220359f29d38879b19665015a2b85bd60e4e3949292f75d2294d5a545aa392dd1fd022036aa2b42bdedd9deca319eece3e8a5670fe89e78231e1025b9fc1079a931398d01004d632103d6a391d54b9683da5c4c1ab1720739757422861b7ca98da226118061b40c412e67029000b2752102f2b77c73154588dfa6301b6c69062c0a713467996542a836f7a2ce6a239ba68868ac00000000ed02000000000101374c80d9d5213e7bf0ea94c3a8085866a4d533590d89f68e829162d03d431a2600000000009000000001e43400000000000016001401396601573e7aef81f91a89fc4b4b56feb7a35e03483045022100d486e826ecfddcc474b719cb69e641efe97b4795b8ea4bb9729237788a3d35ed022051b097591bfc53dc855303672e17ad8488de9f2bcca670995060f331a7dfe55e01004d632103d6a391d54b9683da5c4c1ab1720739757422861b7ca98da226118061b40c412e67029000b2752102f2b77c73154588dfa6301b6c69062c0a713467996542a836f7a2ce6a239ba68868ac00000000ed02000000000101a436af3ff69624fd11b543c0b8d451190bcb7891a7259832cb64445571dc3003000000000090000000016c4800000000000016001401396601573e7aef81f91a89fc4b4b56feb7a35e03483045022100c203f2093d4b182ba5b78a471f806acf13c4d44f7191068abaf2b7a6742f427e02200fabe2a24e947f467a48fca7bba3ed68387ce46c32533366d99a52cefb0e3ad801004d632103d6a391d54b9683da5c4c1ab1720739757422861b7ca98da226118061b40c412e67029000b2752102f2b77c73154588dfa6301b6c69062c0a713467996542a836f7a2ce6a239ba68868ac00000000ec020000000001013798507505fc7de865bccaad655dc03b3899bba86e139dfbc543a2a2f9b4b842000000000090000000016c4800000000000016001401396601573e7aef81f91a89fc4b4b56feb7a35e034730440220724b0e5a340e80de26f3908ae510a34c93b5632dbd539c1a155856cad5375ac402203de9139fbaadd86a11168c612b75f974a6143e68c564de77c8b350f2978ff2f901004d632103d6a391d54b9683da5c4c1ab1720739757422861b7ca98da226118061b40c412e67029000b2752102f2b77c73154588dfa6301b6c69062c0a713467996542a836f7a2ce6a239ba68868ac0000000000000000000000" - val dataClosingRemote = hex"0100250000000703af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d00009157899e63ee4dc5f8ce060a1bab76e8ae829aa9b3cafe6ebe19f2a608a453ff680000001000000000000044c0000000008f0d1800000000000002710000000000000000000900064ff160014c1c7289591b6e67b38e3f0a63a433da5493348410000186b0200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000229a82039dc0e0b1d25905e44fdf6f8e89755a5e219685840d0bc1d28d3308f9628a358500000000000003e8ffffffffffffffff0000000000004e2000000000000003e80090001e031015fdff76ce8a1d7c1281c28e981300256525ad3b533f5464ff453a1ba2048602d93a6ec6fd9e9147cac7fa53c602ce68dcc35013e2edb7475a3f0b35557307e602c1d9c74cd082a8e25197d98ddbf62039113f3909122ed479b2005aab8b85836a03304a5e9c4fc78c68dbba22e21b76c3cd24ae7c87414f63ed6d429c67dc721d9103bd4c12aa179f4fe1ee2cf08c770f794caf8a83e7a9b091e7b44512ed3a845bb200000003229a82000000000000000007000300fd05aabd8de2389047c2515292ce12da17b431dcface72910e502c97f8779eda57a28300000000000000030000000000e4e1c0ea851289c7e6144af83059520a132678410f370c0349d53ace3fb2d4b11761d000061b100002494b30e6e77ee6cabb23c8a7c081a7179d5f712d843914abd26867855cc0271b4c4af2b65b2e568c9aaf41fa200afee1821aa546e177cae8e0f753ef0c15a8f47d06f1ac76e0bdd4d0e38926e908d92d7252bf6246018bab1bb2477c1265f1bd7dcabbba79d5b8df198640ae5c845bdd16df15d1b40380dabefd6c7b3ca5afb188f38c3ebb77c06c8eb783ff1fb029a8c194caa09b3469f779180e2a09eb0b40d78868fd9226c9f6d25349495a90351a4be2edf01105fc94310b764f8a3b18557934a9f9b39aa47a7ef69d3cf6d9201fcdb4537e12c9e6c589d5f4e800a49c4e090005d526d5515000e6855d0546acea7322cc195df08662a1b9a74644819285eae65b93818a3faf5de7769ec8bed30fdf57af35acfb6f88adc6b95920ae1edc795eb06a0039b16da091f0eab98c83b176ca208ab24c41978bc782457aa1018f49edb22ece7fa520d0053ee995f2850b76b095b298b55b6403b82a16608d369aafdf5a19364b1180b2066cf00d5419bd4e283cdc0c9535e4a0ad0d0615647a3601b95ff5f206538e4482731273e4e33235835cd227d97cab80e9a6c21853bbcb47ffa529a9ac7b0ed70af6d0574f9f1e979d99a94dacad5a0682623b9d25a4885f98bfb98222637c64ccefbbe9568ecf3cc103660c44301fb289104c5083b07ba9ce535c702dac687cefd4cd1d1d6cf9eb6dab0c9b643b6ec4fa3cf2126a65d6a7a344652523849b538d06e1d0089d10cc9edb58ddcc0e8d4f451712f36c38198325b71c9aedecb90845996714dc3671455462875f4cca75f3c2b0060b0fc0ec7e927e8900c1036ae93155ada9047186e06364ae4e63a6871fe6747d75b2f8ff30a925835504ec7f23bc2ba405df732e404542f2eae7d6646db3080d35e4c1dfa385370318945b530f2777e2503fd85818051b3482984accdbfebbdcc80e76bd8ab0cd3da3e0f5ab9d9f344016c11ce513ff3b6abc9928947da5f2785cfae693126ce5f6ce6049fbce41654aa5e0d86e32bd1dedb16114e8c66419fefd87d83a063e02f7f8a37db88cc8f5634caf033efc6e6867b81fedf436b6295633dff3bac0b325e9dd34cf3fa924d12b4daf2edf0ff58a2d78ff709f8711a536a34b31fa2daab34b5e877b88cd8ced38ea6d22230390c3567586d111aa21a0ab8bff630b86ae8aba818f62fc68e6169bbc88d3586a1e58652b9e90dc2f74809f83e2ffb47b6257bac95c487d8f32c72e8cdb24bff3d96dedf6d5fba9602ea8984abb3f2a36a7491d602767b14a712256d6a0b2f8bc6181aaa64b6ce0ce6bc64aba2432cc1b7762079abecc59f9e9c19d408ddbf0ea2294c2ad7c12717acc1ab0f4246bcd000ce886049a935b712dba203907d1defaa07d2a37c093eeec8ec5fbcb42341361611d9416f009ee22d21443eb58c7835bfe2082fdec855fbdc26c11f120eb5eb74574bb0f12429e401420350eef27ddc833dee32d96a909f49ef8e1d962ea9d0996f1f910dcde1609b0cb6338e4e29fed5327156fa5dbb82f9b5f7bd184c1eb24380fe3ad40727fa9a60bb9a5a14dfa5fa34e59243198bdbaa26e4cf7db298712f9ea41583dfd63e2f87262107e48241c24341cacab2f0ae64168b0f539b7a570991a44381c26b669a74f973907cab17cb8e8171e6d078379f29906ad926d4c2e85e0fb31cabf4019a797c065e0b1d3ba6c927d187365c596d009c79777eaa6b9d798d60ee76dd6c88dffc1f7a8619bb21b8a3f48ff774bff36b11e7c0f17efe58b38fe41c5099c0eef256ed66cab7483303b84d83188588a05aa775d2a5c28f912da358b4fc9f9e348f6312878d3c919567679931d547a3d05c401b324785e7e2c4387692792cf762925110db2ef8d85bec34f59bb98de736d479c3580729672af7b0eeb9b2eadf706b52c3dd16efca574616c00fd05aabd8de2389047c2515292ce12da17b431dcface72910e502c97f8779eda57a28300000000000000040000000000e4e1c0ea851289c7e6144af83059520a132678410f370c0349d53ace3fb2d4b11761d000061b1000038c08be032cff6f282a21a7a69a3bf3da8fa32f751f4978232ae774aea67295cd154028a32d0881dc27fb5a46beb9e5931926d35866103d8bef028dd6031cad768283018e7e8d49e81b033e42e24105d9681f6638f634bb482b38182d08b9863011e3c19949a86fe744f90212a3076e149125742a5e8ae8f72f0af19370fe1a995ddcc48d6cff9f7e3ff96d18fbdb4bc034a0dc23ee3d31489ae2d4438b46bb9a23d53de03d0686710db3a5a72e504a65fa370f21e5d2c023c0d1bd6e0821853bea11dad9c9ea88a4b12f2c6a18b47bfb35aeb45ff012ea413ff83de1ab0c2e5a9166efa17ebff5b83ca9c1d7b680e57e9d5abff81980d42403cac0b6e12070346545ca9d5b794f3b89066bd37391d4c32ec7b610efabe18d4d315109012430c346d145926681c5fbd82d63500b4c7cb4e061c096ef8f75f0695e0b17321dd32cfe1497d71b59d7132a3580d81632b7a9bfe9d621e707d4d5654721dabc154b522013a48974545a822045cdf649270c2b0ce422a8f87df4e3603dc3c1c6701e62c9a7908e9ab7dd1fb19d8b4fb08a59e7877fd4dc733c059dcff11c59b70912e07e5afa2e609e2d8af9a5351304b2fb79b9137cb8d3a82046ff3568af861c3376f64fd5fc6e056139a0f2fe180686db998bf9e956cb3548bfd6ecbaafbe31a35d1f496696adcd140b28430a2bdcd0ffd75f78e3272e3b3ef3c441d9884a748cfb08a4f00dbf5c754583acfe3f90e3caaed206e64a018fc58d26234740e079bc5d01b2633fe5bd81e256aeccbd74b8904b3bbc86c1527af0669c7c73f58160e02ddc6618a5a1169e8be3a5a7809689faecf535d5532c878533fd26a886b44c8e673c63324b22b2e28e3fd3674d75256ab6bdbf418fed262d30cfb55bf5a6ef02d595d947b2aca6ed178086ffbbbb1dd7863164e0c75d56587be40fd3c5543bbe87d89b653e062a67b3295a69e724ca9e9c80ff5289d53d346861d97a1f38d194d257f64516129cafa0cb7ec861fcb004c919a22016f96b0ab646c600882aaeb39d20d374162a048b8548879e6e5042e5b913a606abbb847422256c1883578524448dc3c6a11608b20206d9d908ec98e81163ceb586faa501889284c970ec4d6a8a0e142b497447db2d1c8cf7fa9fa51eb2dbf8f99e014be9128e2009bdd6fb1558e85fb528136d822ac60a00f1af625ea74938b05aeec2fec143e9ef1abd326166a424464932e2fdff2ce553e5404113383ca261a48fb8dacec0a72005b5a5d5c4459d795372bc6d9a7d7788a35cdb1769b728380baceed0a667f63a06c39eb4250c5b90da1d80c6f6dd2a8300ef8f028ef11c3ae7cb4a95a01f6777dad6b6a84e5f420e9510698d24963d2c81e85bb1fea6e66fe53fb108f417065dced6547eef1f2c1baa4cb2b477ca7cba58746ba7ad13d73030ec351631d162161be16270410bfa756e27d196467ac64d3fc4ae70b82cc7ee9651029259ed39b81b98e55793dbcd1e2a94b3c8a4c1c22827c176ce103d1789413eddc9d23f74046f9984d0753bc29611b34fdfeaf6bfc7f18f591cc0a689ebc1f7a4d53b21fc89bdef6cab1ca85f942508cf163f2c1b6b6aa5391b3a3ebbf30fe0eb67f5864052c86c5056e507513f422efd721e6c981c736ed886c55e914c08cd7bf5ec7a8db36e7e7fcc2defd601479b672ab608895cd43bf8554733b328757630ebd3f230aa0a30809e7b76dcf59a7d9649c236ff4ffb1271ce40a0ebe9cabfce5fb297cd8b291a5556917438924c5f76dbc6bb8ab50835a05eea952939b065f0c23669f115b9181cede8f9d583d5bd3a651114f7277291481555d8e66d923111a71c49d12bee354107737349acfb08ea7ab046728777a9753f631353d0880d6e3fe6eb92c8c12f923a50942a4bef3b492ede9bc0cfb387fb8b39e2581e8200fd05aabd8de2389047c2515292ce12da17b431dcface72910e502c97f8779eda57a28300000000000000050000000001312d00ea851289c7e6144af83059520a132678410f370c0349d53ace3fb2d4b11761d000061b0f0003c45571d40572e15114d1731236a3768c74cf13eda20d58aafaba564570a043fd6e6543c50cff6e1f8cc5342cc95f2e30b9003fa74ec84e2f7c95bfde69f464d5886d8ae0a99294ff5b18bf6c57f3ff0c8577030d4d7a9469302228d9510b5b85e962b9a48bb1bfb7710d43e3df7fb78d734f258c05886d532e108e55f2a2159537cbe48edd13245a612f41889f5014dd5d1734c0416315384ede850b25ee716f24d40b77cc17e341f5d6e61c4589d245f9923a501d2548716c5568eac4691476b60cfcdb2d94ba948cbb7ecb710871b23ce8a9179d5fdcea8fc94e4bc9b56000e425734dab1247a441a73598e879934bf161815a037bfa259b3038490ea6cfb577ca825ecbd4b39c4f1397902bfc9587c52680755097e3938113373aaddbe62ca55db19e45140f125d84ac69cfae7103d11079c2adc3d3c51f13797e82443a8bf69c2acb34e7646095e18d160338c9aafedebf6e30e72302ea579585d74ef2e0af3005f93e9c76902dc272871e91dd5124c99a19597adcaca362d2cf9afaa674958e19a9c6d37b76b6b2be9e3779297e5f65c733c19dd456d62a66a2452b0eb091d97a82b965f1b5536d11b7f6ee8643d5505224e37839798ca99d8623bdd1e2f50b6b60b450a8e0572f44dee06a8658014e650ba5cb614f727035a8da06a2f7a60564e22971c2488fdd1fffee6b8b9eb5e9596b623a447bc4b06294952a18a6a1cd988c9d2223fec1714724ba2168639db964e1a9e2cdef4efc1c1ac7c2c5c1ea81daa34ddc8278ba8e9e54cbc3234725fa874a9a3c0c58f629d3872fa0b765b6cff95593492782398dcf097bee861476a08f31d82eeefac4c8f868cb3d1acddea2ba4f5dfc3671c42dca6a584e5f3a0e7aebe1083a997a03279f27d7319c6928e9d190392f718bd7278b806846f5c2c2dd4cbc06db84261fe6a557e86439ee7fef09809930d4a78d0e43f3b11d163539461d971bf0a83b3a52d3f8fa87c600a369a06e36cfe2349798d9597362a8385a97d82a7b445a4d7f7c1541c8e51531728d432422f3478e3733b5582e3ddb1137af5e732bf85e5c9abec1c44dd1d472774d913aad656a7e3a45659e99ba048a5e85ffbdc4af146e79241ea4a45e0b0139bd3dae63c6f89b0515d6c31566d067198e51cd01f82a41cf7d04538c4393b4892332d94f8c584531426053f2b4e6e945b3d28b492f9a4364671bdc4ee878cdf9fec1ab90ca2dc131c086e0df17bfa59b0269ea1d5a1bdde7c826c2af89da485b046e17cb8ef7c55fbbbb42a409661b6a1cefd62f05c906384d475764e51c4bae7f73a904a77622bc3515913a9fcf393ff14851c779679b4b0df58d9aa8216a390b847206f0f83ec4d982462ad88c569f878368f94fd2940975bca95669471c00894420bf2fab9bb09a3372774b716a9d1bc7d991cae8ffd759fca9c3b5ecfc1323db3f4c777308af23ef7530cacb4e4bfdaee107f059b90a2b495058bc7400ffb31e12660cf8e4fe22d3a01511595bd864cdccf6123545d024a611ba380096da71b89412c13f8be3287ec4db4c86cd4f023d53d6d4a0f7102956c3d1b0769516b6dc83c66ea73c8a5131ce74a2444c3ed9accfa085ed2ae9862ac1094c1a84db8a99c82f96f370c450c53c4372d931daffe0663d4e7b3b651fe995ee3cf09fde460b8d3360b567dda5df1483407c5286f0df33567e9407f8539eaf5e117900bef904f4f6c1081c0cc57afafce7d618cf865da5d1729e1028bf2c3a58af700690650a13340dd9217f4d3e564dc7aa3c7823caafce7ae4d3220c263b3bddf8e6624ed57c5c177abec462f39b24e8f44020490bc3098525d341a51b6b91ccff06df8dc771b911659414faa0708d30902b4461dbd6621904e3a506dd1888660e22b4b46c814135f8d033dc509a37ea7af5cbeb018f000009c40000000008f0d180000000002faf080024bd8de2389047c2515292ce12da17b431dcface72910e502c97f8779eda57a283000000002b40420f000000000022002031589753b0c53bac3611dc7043ed3fd39f3e028d084f4ccced10610770c61eed47522103074b1de9739810ce2a885ac85c0f919a17018e8826aefbe6ab92935267e25cf721031015fdff76ce8a1d7c1281c28e981300256525ad3b533f5464ff453a1ba2048652aefd023d02000000000101bd8de2389047c2515292ce12da17b431dcface72910e502c97f8779eda57a2830000000000f8719480074a0100000000000022002040c49f845d675d6dd824933252c4f85caaa4dc7b023d3c6accc9e57323dcbcff4a0100000000000022002044e4c2c048661a29bc9e194687e5e973b14379028f3d86bea22b68c4a4a25b96983a0000000000002200208930be41bd0d33eb7b5714a4d9bf3179170daef6fd827e79e39401157f5f8427983a0000000000002200208930be41bd0d33eb7b5714a4d9bf3179170daef6fd827e79e39401157f5f8427204e0000000000002200208930be41bd0d33eb7b5714a4d9bf3179170daef6fd827e79e39401157f5f84275837020000000000220020433f81252b8fbd947c2ade785fb393184fea5b3d6df574528efa6b01495f812700350c00000000002200207f0ec90c88cba1da4cfcd677a9962f617097c470a07b5a157d42e22f6f8c12fd040047304402204b4bdba0b08185935323d31c398004c6476b0d3781045bcdfb1f8917eaa0486f02203db71b78186cf7a500bc7b4e3216f2c8b9bd37a2911e34777adbaabccd50dbfb01483045022100a08231029e1d326be1eaf23f947766ec11b22d2a774f818fe5a6700b07ea379002202f365ea341df3d7678be1bf136d05efc60794209b4b68a7fd1e7760d2b8656fa0147522103074b1de9739810ce2a885ac85c0f919a17018e8826aefbe6ab92935267e25cf721031015fdff76ce8a1d7c1281c28e981300256525ad3b533f5464ff453a1ba2048652ae1e7512200003000324baa7cb41926907ca87f2875f857786966641cc7cae8ce9b3c2a50d4661bf368c020000002b983a0000000000002200208930be41bd0d33eb7b5714a4d9bf3179170daef6fd827e79e39401157f5f84278876a914b639a79d1aa3426d75a36b98d3ef3cb79db674358763ac6721039d5c3528a43f5f532bf649ddbb05b350e34453610003e2a4a159c81dfbdb45ad7c820120876475527c21025f47871e3cf61241abadb85d3badad8c789676754a5f77d0ae5009458d2c395152ae67a914120850f1eeb36e375b36d78a9a3ecc632c9d442888ac6851b275685e0200000001baa7cb41926907ca87f2875f857786966641cc7cae8ce9b3c2a50d4661bf368c020000000001000000011734000000000000220020433f81252b8fbd947c2ade785fb393184fea5b3d6df574528efa6b01495f8127101b0600404693180da32f1d5e3a5cf74a174a22a363150e638208f838efb7ad8a0b43427424154595262a91ee14af9432f6c857f72cea7868a0e49c0e54b344593ef3d78d406b4ac7294e4519eb1d0b9e451955a4961f87693ae723013e24073a27928e580652aa6c394b7c98dd4f97e6245768124997b14f3d049ca0bdd4666ac97b02d65c000324baa7cb41926907ca87f2875f857786966641cc7cae8ce9b3c2a50d4661bf368c030000002b983a0000000000002200208930be41bd0d33eb7b5714a4d9bf3179170daef6fd827e79e39401157f5f84278876a914b639a79d1aa3426d75a36b98d3ef3cb79db674358763ac6721039d5c3528a43f5f532bf649ddbb05b350e34453610003e2a4a159c81dfbdb45ad7c820120876475527c21025f47871e3cf61241abadb85d3badad8c789676754a5f77d0ae5009458d2c395152ae67a914120850f1eeb36e375b36d78a9a3ecc632c9d442888ac6851b275685e0200000001baa7cb41926907ca87f2875f857786966641cc7cae8ce9b3c2a50d4661bf368c030000000001000000011734000000000000220020433f81252b8fbd947c2ade785fb393184fea5b3d6df574528efa6b01495f8127101b06004045b8a6b6a6611e6d5aba881c9d6cf58c0314e2596da2e4a202afa424d551ac44494b1b9bdfe85e3079f90db9ec0f0281e6796d2e2b0825ba8771b23bc41d894d409b9ae17cf065f8bf57c6aa38297af2ccf76ff61a8e289fc9dc8edcde898a9a6c4db11ecccb2d97ea9135f9360844aa1c6b595e42a353f80cb8347cb2d8413978000324baa7cb41926907ca87f2875f857786966641cc7cae8ce9b3c2a50d4661bf368c040000002b204e0000000000002200208930be41bd0d33eb7b5714a4d9bf3179170daef6fd827e79e39401157f5f84278876a914b639a79d1aa3426d75a36b98d3ef3cb79db674358763ac6721039d5c3528a43f5f532bf649ddbb05b350e34453610003e2a4a159c81dfbdb45ad7c820120876475527c21025f47871e3cf61241abadb85d3badad8c789676754a5f77d0ae5009458d2c395152ae67a914120850f1eeb36e375b36d78a9a3ecc632c9d442888ac6851b275685e0200000001baa7cb41926907ca87f2875f857786966641cc7cae8ce9b3c2a50d4661bf368c040000000001000000019f47000000000000220020433f81252b8fbd947c2ade785fb393184fea5b3d6df574528efa6b01495f81270f1b060040c395381f0200842628e2e4a42c6bfea2a2134509f622e4838743e21576e8dec0504690bc58416694470a9b3f8f8d3d3e81c112a19acd607532f07526a04fcf7e401e169a806c424664e39e2ae979256894e50ce777ffd3330a68444e9f57ea653831fa96cb65eb5e5b8bdb88f87c6fa0eb7c1aa54ab026159fa4e31ebb58073aef00000000000000070003fffd05aabd8de2389047c2515292ce12da17b431dcface72910e502c97f8779eda57a28300000000000000030000000000e4e1c0ea851289c7e6144af83059520a132678410f370c0349d53ace3fb2d4b11761d000061b100002494b30e6e77ee6cabb23c8a7c081a7179d5f712d843914abd26867855cc0271b4c4af2b65b2e568c9aaf41fa200afee1821aa546e177cae8e0f753ef0c15a8f47d06f1ac76e0bdd4d0e38926e908d92d7252bf6246018bab1bb2477c1265f1bd7dcabbba79d5b8df198640ae5c845bdd16df15d1b40380dabefd6c7b3ca5afb188f38c3ebb77c06c8eb783ff1fb029a8c194caa09b3469f779180e2a09eb0b40d78868fd9226c9f6d25349495a90351a4be2edf01105fc94310b764f8a3b18557934a9f9b39aa47a7ef69d3cf6d9201fcdb4537e12c9e6c589d5f4e800a49c4e090005d526d5515000e6855d0546acea7322cc195df08662a1b9a74644819285eae65b93818a3faf5de7769ec8bed30fdf57af35acfb6f88adc6b95920ae1edc795eb06a0039b16da091f0eab98c83b176ca208ab24c41978bc782457aa1018f49edb22ece7fa520d0053ee995f2850b76b095b298b55b6403b82a16608d369aafdf5a19364b1180b2066cf00d5419bd4e283cdc0c9535e4a0ad0d0615647a3601b95ff5f206538e4482731273e4e33235835cd227d97cab80e9a6c21853bbcb47ffa529a9ac7b0ed70af6d0574f9f1e979d99a94dacad5a0682623b9d25a4885f98bfb98222637c64ccefbbe9568ecf3cc103660c44301fb289104c5083b07ba9ce535c702dac687cefd4cd1d1d6cf9eb6dab0c9b643b6ec4fa3cf2126a65d6a7a344652523849b538d06e1d0089d10cc9edb58ddcc0e8d4f451712f36c38198325b71c9aedecb90845996714dc3671455462875f4cca75f3c2b0060b0fc0ec7e927e8900c1036ae93155ada9047186e06364ae4e63a6871fe6747d75b2f8ff30a925835504ec7f23bc2ba405df732e404542f2eae7d6646db3080d35e4c1dfa385370318945b530f2777e2503fd85818051b3482984accdbfebbdcc80e76bd8ab0cd3da3e0f5ab9d9f344016c11ce513ff3b6abc9928947da5f2785cfae693126ce5f6ce6049fbce41654aa5e0d86e32bd1dedb16114e8c66419fefd87d83a063e02f7f8a37db88cc8f5634caf033efc6e6867b81fedf436b6295633dff3bac0b325e9dd34cf3fa924d12b4daf2edf0ff58a2d78ff709f8711a536a34b31fa2daab34b5e877b88cd8ced38ea6d22230390c3567586d111aa21a0ab8bff630b86ae8aba818f62fc68e6169bbc88d3586a1e58652b9e90dc2f74809f83e2ffb47b6257bac95c487d8f32c72e8cdb24bff3d96dedf6d5fba9602ea8984abb3f2a36a7491d602767b14a712256d6a0b2f8bc6181aaa64b6ce0ce6bc64aba2432cc1b7762079abecc59f9e9c19d408ddbf0ea2294c2ad7c12717acc1ab0f4246bcd000ce886049a935b712dba203907d1defaa07d2a37c093eeec8ec5fbcb42341361611d9416f009ee22d21443eb58c7835bfe2082fdec855fbdc26c11f120eb5eb74574bb0f12429e401420350eef27ddc833dee32d96a909f49ef8e1d962ea9d0996f1f910dcde1609b0cb6338e4e29fed5327156fa5dbb82f9b5f7bd184c1eb24380fe3ad40727fa9a60bb9a5a14dfa5fa34e59243198bdbaa26e4cf7db298712f9ea41583dfd63e2f87262107e48241c24341cacab2f0ae64168b0f539b7a570991a44381c26b669a74f973907cab17cb8e8171e6d078379f29906ad926d4c2e85e0fb31cabf4019a797c065e0b1d3ba6c927d187365c596d009c79777eaa6b9d798d60ee76dd6c88dffc1f7a8619bb21b8a3f48ff774bff36b11e7c0f17efe58b38fe41c5099c0eef256ed66cab7483303b84d83188588a05aa775d2a5c28f912da358b4fc9f9e348f6312878d3c919567679931d547a3d05c401b324785e7e2c4387692792cf762925110db2ef8d85bec34f59bb98de736d479c3580729672af7b0eeb9b2eadf706b52c3dd16efca574616cfffd05aabd8de2389047c2515292ce12da17b431dcface72910e502c97f8779eda57a28300000000000000040000000000e4e1c0ea851289c7e6144af83059520a132678410f370c0349d53ace3fb2d4b11761d000061b1000038c08be032cff6f282a21a7a69a3bf3da8fa32f751f4978232ae774aea67295cd154028a32d0881dc27fb5a46beb9e5931926d35866103d8bef028dd6031cad768283018e7e8d49e81b033e42e24105d9681f6638f634bb482b38182d08b9863011e3c19949a86fe744f90212a3076e149125742a5e8ae8f72f0af19370fe1a995ddcc48d6cff9f7e3ff96d18fbdb4bc034a0dc23ee3d31489ae2d4438b46bb9a23d53de03d0686710db3a5a72e504a65fa370f21e5d2c023c0d1bd6e0821853bea11dad9c9ea88a4b12f2c6a18b47bfb35aeb45ff012ea413ff83de1ab0c2e5a9166efa17ebff5b83ca9c1d7b680e57e9d5abff81980d42403cac0b6e12070346545ca9d5b794f3b89066bd37391d4c32ec7b610efabe18d4d315109012430c346d145926681c5fbd82d63500b4c7cb4e061c096ef8f75f0695e0b17321dd32cfe1497d71b59d7132a3580d81632b7a9bfe9d621e707d4d5654721dabc154b522013a48974545a822045cdf649270c2b0ce422a8f87df4e3603dc3c1c6701e62c9a7908e9ab7dd1fb19d8b4fb08a59e7877fd4dc733c059dcff11c59b70912e07e5afa2e609e2d8af9a5351304b2fb79b9137cb8d3a82046ff3568af861c3376f64fd5fc6e056139a0f2fe180686db998bf9e956cb3548bfd6ecbaafbe31a35d1f496696adcd140b28430a2bdcd0ffd75f78e3272e3b3ef3c441d9884a748cfb08a4f00dbf5c754583acfe3f90e3caaed206e64a018fc58d26234740e079bc5d01b2633fe5bd81e256aeccbd74b8904b3bbc86c1527af0669c7c73f58160e02ddc6618a5a1169e8be3a5a7809689faecf535d5532c878533fd26a886b44c8e673c63324b22b2e28e3fd3674d75256ab6bdbf418fed262d30cfb55bf5a6ef02d595d947b2aca6ed178086ffbbbb1dd7863164e0c75d56587be40fd3c5543bbe87d89b653e062a67b3295a69e724ca9e9c80ff5289d53d346861d97a1f38d194d257f64516129cafa0cb7ec861fcb004c919a22016f96b0ab646c600882aaeb39d20d374162a048b8548879e6e5042e5b913a606abbb847422256c1883578524448dc3c6a11608b20206d9d908ec98e81163ceb586faa501889284c970ec4d6a8a0e142b497447db2d1c8cf7fa9fa51eb2dbf8f99e014be9128e2009bdd6fb1558e85fb528136d822ac60a00f1af625ea74938b05aeec2fec143e9ef1abd326166a424464932e2fdff2ce553e5404113383ca261a48fb8dacec0a72005b5a5d5c4459d795372bc6d9a7d7788a35cdb1769b728380baceed0a667f63a06c39eb4250c5b90da1d80c6f6dd2a8300ef8f028ef11c3ae7cb4a95a01f6777dad6b6a84e5f420e9510698d24963d2c81e85bb1fea6e66fe53fb108f417065dced6547eef1f2c1baa4cb2b477ca7cba58746ba7ad13d73030ec351631d162161be16270410bfa756e27d196467ac64d3fc4ae70b82cc7ee9651029259ed39b81b98e55793dbcd1e2a94b3c8a4c1c22827c176ce103d1789413eddc9d23f74046f9984d0753bc29611b34fdfeaf6bfc7f18f591cc0a689ebc1f7a4d53b21fc89bdef6cab1ca85f942508cf163f2c1b6b6aa5391b3a3ebbf30fe0eb67f5864052c86c5056e507513f422efd721e6c981c736ed886c55e914c08cd7bf5ec7a8db36e7e7fcc2defd601479b672ab608895cd43bf8554733b328757630ebd3f230aa0a30809e7b76dcf59a7d9649c236ff4ffb1271ce40a0ebe9cabfce5fb297cd8b291a5556917438924c5f76dbc6bb8ab50835a05eea952939b065f0c23669f115b9181cede8f9d583d5bd3a651114f7277291481555d8e66d923111a71c49d12bee354107737349acfb08ea7ab046728777a9753f631353d0880d6e3fe6eb92c8c12f923a50942a4bef3b492ede9bc0cfb387fb8b39e2581e82fffd05aabd8de2389047c2515292ce12da17b431dcface72910e502c97f8779eda57a28300000000000000050000000001312d00ea851289c7e6144af83059520a132678410f370c0349d53ace3fb2d4b11761d000061b0f0003c45571d40572e15114d1731236a3768c74cf13eda20d58aafaba564570a043fd6e6543c50cff6e1f8cc5342cc95f2e30b9003fa74ec84e2f7c95bfde69f464d5886d8ae0a99294ff5b18bf6c57f3ff0c8577030d4d7a9469302228d9510b5b85e962b9a48bb1bfb7710d43e3df7fb78d734f258c05886d532e108e55f2a2159537cbe48edd13245a612f41889f5014dd5d1734c0416315384ede850b25ee716f24d40b77cc17e341f5d6e61c4589d245f9923a501d2548716c5568eac4691476b60cfcdb2d94ba948cbb7ecb710871b23ce8a9179d5fdcea8fc94e4bc9b56000e425734dab1247a441a73598e879934bf161815a037bfa259b3038490ea6cfb577ca825ecbd4b39c4f1397902bfc9587c52680755097e3938113373aaddbe62ca55db19e45140f125d84ac69cfae7103d11079c2adc3d3c51f13797e82443a8bf69c2acb34e7646095e18d160338c9aafedebf6e30e72302ea579585d74ef2e0af3005f93e9c76902dc272871e91dd5124c99a19597adcaca362d2cf9afaa674958e19a9c6d37b76b6b2be9e3779297e5f65c733c19dd456d62a66a2452b0eb091d97a82b965f1b5536d11b7f6ee8643d5505224e37839798ca99d8623bdd1e2f50b6b60b450a8e0572f44dee06a8658014e650ba5cb614f727035a8da06a2f7a60564e22971c2488fdd1fffee6b8b9eb5e9596b623a447bc4b06294952a18a6a1cd988c9d2223fec1714724ba2168639db964e1a9e2cdef4efc1c1ac7c2c5c1ea81daa34ddc8278ba8e9e54cbc3234725fa874a9a3c0c58f629d3872fa0b765b6cff95593492782398dcf097bee861476a08f31d82eeefac4c8f868cb3d1acddea2ba4f5dfc3671c42dca6a584e5f3a0e7aebe1083a997a03279f27d7319c6928e9d190392f718bd7278b806846f5c2c2dd4cbc06db84261fe6a557e86439ee7fef09809930d4a78d0e43f3b11d163539461d971bf0a83b3a52d3f8fa87c600a369a06e36cfe2349798d9597362a8385a97d82a7b445a4d7f7c1541c8e51531728d432422f3478e3733b5582e3ddb1137af5e732bf85e5c9abec1c44dd1d472774d913aad656a7e3a45659e99ba048a5e85ffbdc4af146e79241ea4a45e0b0139bd3dae63c6f89b0515d6c31566d067198e51cd01f82a41cf7d04538c4393b4892332d94f8c584531426053f2b4e6e945b3d28b492f9a4364671bdc4ee878cdf9fec1ab90ca2dc131c086e0df17bfa59b0269ea1d5a1bdde7c826c2af89da485b046e17cb8ef7c55fbbbb42a409661b6a1cefd62f05c906384d475764e51c4bae7f73a904a77622bc3515913a9fcf393ff14851c779679b4b0df58d9aa8216a390b847206f0f83ec4d982462ad88c569f878368f94fd2940975bca95669471c00894420bf2fab9bb09a3372774b716a9d1bc7d991cae8ffd759fca9c3b5ecfc1323db3f4c777308af23ef7530cacb4e4bfdaee107f059b90a2b495058bc7400ffb31e12660cf8e4fe22d3a01511595bd864cdccf6123545d024a611ba380096da71b89412c13f8be3287ec4db4c86cd4f023d53d6d4a0f7102956c3d1b0769516b6dc83c66ea73c8a5131ce74a2444c3ed9accfa085ed2ae9862ac1094c1a84db8a99c82f96f370c450c53c4372d931daffe0663d4e7b3b651fe995ee3cf09fde460b8d3360b567dda5df1483407c5286f0df33567e9407f8539eaf5e117900bef904f4f6c1081c0cc57afafce7d618cf865da5d1729e1028bf2c3a58af700690650a13340dd9217f4d3e564dc7aa3c7823caafce7ae4d3220c263b3bddf8e6624ed57c5c177abec462f39b24e8f44020490bc3098525d341a51b6b91ccff06df8dc771b911659414faa0708d30902b4461dbd6621904e3a506dd1888660e22b4b46c814135f8d033dc509a37ea7af5cbeb018f000009c4000000002faf08000000000008f0d18037073c01e5e7fdc85bee624bcc26b237744faa71acc095a5d456c306e0b84c570254366cdc771b7bf8b81d90c93c86d9fa27d602d49d72e0b8bb30bb7dab6cc7a100000000000000000000000000000000000000060000000000000000000300000000000000030003da107ef61715474784465d8d7a4c9b460000000000000004000380d777b851b445a4882760ed591ac96000000000000000050003ab7783e399d84ce28eb9d6c64e1cc1ebff02d009cfedda76c975c77dc72543b16b6288e702ac1d989c9d76e8c7d6be3d8b5e24bd8de2389047c2515292ce12da17b431dcface72910e502c97f8779eda57a283000000002b40420f000000000022002031589753b0c53bac3611dc7043ed3fd39f3e028d084f4ccced10610770c61eed47522103074b1de9739810ce2a885ac85c0f919a17018e8826aefbe6ab92935267e25cf721031015fdff76ce8a1d7c1281c28e981300256525ad3b533f5464ff453a1ba2048652ae0003003e0000fffffffffffc0081f719ad9ced864fec5efa6b78e12e255bc98a916a4bc5f9b5cb8f32e697106a8c00fc0003ffffffffffe80105a101c31ea6ddba8de1c0bb5450b4a974eb55537e794338926645ec773aafe38802000007ffffffffffc80105b32c7c8b2bbfa59f8cf725a987e9f8a61a2fec4ac26cdee7bb64601081749db40003ffffffffffe4bd8de2389047c2515292ce12da17b431dcface72910e502c97f8779eda57a283000000000000061a800000000000fffd023e02000000000101bd8de2389047c2515292ce12da17b431dcface72910e502c97f8779eda57a2830000000000f8719480074a0100000000000022002040c49f845d675d6dd824933252c4f85caaa4dc7b023d3c6accc9e57323dcbcff4a0100000000000022002044e4c2c048661a29bc9e194687e5e973b14379028f3d86bea22b68c4a4a25b96983a000000000000220020e2928b9a0be275a3d3ff2f0a4a694d353b54903908b5aa665641cc3c3ceefea3983a000000000000220020e2928b9a0be275a3d3ff2f0a4a694d353b54903908b5aa665641cc3c3ceefea3204e0000000000002200206e5e8c4f2aea44a87052c41297157ac18f217623a59fa1353abef22a75d998e2583702000000000022002028d8bbb17d6825838406905b42f8086e03f0654cd8d61d0ee2fee623d8190e0300350c00000000002200200117fc94b3c5d70b727cd6e9aad460adab3cec2a453ec40e9dfd75569d4ae54d04004830450221008b3f1d003dd2c41d72c928c08a765f7a769c8b1081600138de4d5f4bb9fd5580022018ed8947befd01b8d06dbd1daecb906b310a558d4ca297cae2f5f066a477f66f014830450221009ad38cdf4595dbbef080ce00011b4bbb676d6b300e3b252c093a285f3a69303c022025dd87f45aba9ffe2c520cb3ed7bb55b5b386070ed48ce7fd4e118f4a771c1980147522103074b1de9739810ce2a885ac85c0f919a17018e8826aefbe6ab92935267e25cf721031015fdff76ce8a1d7c1281c28e981300256525ad3b533f5464ff453a1ba2048652ae1e751220ffc402000000000101574cb8e006c356d4a595c0ac71aa4f7437b226cc4b62ee5bc8fde7e5013c0737050000000001000000011426020000000000160014c1c7289591b6e67b38e3f0a63a433da54933484102483045022100de26022c9a370b05d8f6e200d32efbe3e0f50ed462581bcf69392a7edea08b4102207f738cecdf403341487fefa58c85655b28eab8fa5bf500f25c04e91c85e9049a01252102dbee0f1efff1842122d3523429101c7bd5691e191f99a5e341df467d9db88492ad51b20000000000000003fd012e02000000000101574cb8e006c356d4a595c0ac71aa4f7437b226cc4b62ee5bc8fde7e5013c0737020000000001000000013025000000000000160014c1c7289591b6e67b38e3f0a63a433da54933484103483045022100ef73a23215a1b32d15c9ba5fa1f18e861864a5c6f58dad40d8da2aab70e916c702203e24080b6ff3d0be32836ce3ea1cefc5ab02aee69a38532277aa24b6f273686201008e76a9140ab41ae30b3e5520b9285bc70a1706dd18f584538763ac672102243182d8f24376ce7d563bc6ce9f0c8be8ceef940cdbc6c90d7cfd8910efd6197c8201208763a914120850f1eeb36e375b36d78a9a3ecc632c9d442888527c21027aeba4eeccb5cf053a8bf33109abf45c4e718df202948fa4b2d99d4ddd9b6b9052ae677503101b06b175ac6851b27568101b0600fd012d02000000000101574cb8e006c356d4a595c0ac71aa4f7437b226cc4b62ee5bc8fde7e5013c0737030000000001000000013025000000000000160014c1c7289591b6e67b38e3f0a63a433da54933484103473044022063af75e673d37952609ff9ef56bcb29b3db945dd26514e34bb3519aa7f4725a702204b30378d31c16ce8713b2bbe6db99db2d3589b10e797575e95c451f70bb418a101008e76a9140ab41ae30b3e5520b9285bc70a1706dd18f584538763ac672102243182d8f24376ce7d563bc6ce9f0c8be8ceef940cdbc6c90d7cfd8910efd6197c8201208763a914120850f1eeb36e375b36d78a9a3ecc632c9d442888527c21027aeba4eeccb5cf053a8bf33109abf45c4e718df202948fa4b2d99d4ddd9b6b9052ae677503101b06b175ac6851b27568101b0600fd012d02000000000101574cb8e006c356d4a595c0ac71aa4f7437b226cc4b62ee5bc8fde7e5013c073704000000000100000001b838000000000000160014c1c7289591b6e67b38e3f0a63a433da5493348410347304402204f9fcba505bf2cabcbb503c32bdc4d42b2d057b0159252a747f20756a55fc7f002207f448fbc3b38d93171a18dee85b4580ad4445a120dc568329042b1485ab3058601008e76a9140ab41ae30b3e5520b9285bc70a1706dd18f584538763ac672102243182d8f24376ce7d563bc6ce9f0c8be8ceef940cdbc6c90d7cfd8910efd6197c8201208763a914120850f1eeb36e375b36d78a9a3ecc632c9d442888527c21027aeba4eeccb5cf053a8bf33109abf45c4e718df202948fa4b2d99d4ddd9b6b9052ae6775030f1b06b175ac6851b275680f1b0600000000000000" - val dataClosingRevoked = hex"0100250000000103af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d000094906719a3060b6b7a8b2cd5b0b7ffb12daa3a7407d0cedef637bbcd52601b3a080000001000000000000044c0000000008f0d1800000000000002710000000000000000000900064ff1600144aa46655da279b5eedda63abf309f1a805ed087c0000186b0200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000028a82039dc0e0b1d25905e44fdf6f8e89755a5e219685840d0bc1d28d3308f9628a358500000000000003e8ffffffffffffffff0000000000004e2000000000000003e80090001e03b99798090319eaea87ab11d7d007e88d438ac147ec6e2165eb87171e08a1c2b1031c918d3f8fb25f30cbe149e9061d0249eb0a98e53c2c7589593ecc8c36e14dc803f0b100abb8898ef76144b486b2daa59fedc616bc366e380f66ed2e1da5663c4303e4dee6072779d8ee287ea4599573583513270eaaf20082aa0cd2b63c910a8ee803208519c8ace6160bbc39b6c56037772d1991175541903c889debc59bb7b2286a00000003028a8200000000000000000b000000002710000000000bebc200000000002faf08002481c26703ee350c2defc5f06186e513b95038f0070263155a11a55b110019d666000000002b40420f00000000002200209e56159a6e582da6e14db7461a5936a58a55baa243c83231611cd367ad4f9649475221030efc1f0cb52dfc48335810d38630a98e6884ea75e1e5fbd7c05a4b888beeebec2103b99798090319eaea87ab11d7d007e88d438ac147ec6e2165eb87171e08a1c2b152aefd01590200000000010181c26703ee350c2defc5f06186e513b95038f0070263155a11a55b110019d66600000000008786e08002f8f0020000000000220020ff7e851fb97475ad8d8c30429ac0c7cbb02b47bb985f82554a0f436a3dd79e6500350c00000000001600142044ca0a60adefac91f19ae938bd98cc6cea69b0040047304402203b05b39a4fadc7ab82fe2afd8088e69db334e0b5e481ea39ebd9ef53a529724e02204103fa2839221efdad56038a254977fcf10222821295f619fec5841fa3438b560147304402200949fffc854052ecaccb8dd071ea0c4b7085ae575ac60103251ada0847fda81f0220234c0752cb1c3937175a8002e0cbd8f71b7ee63c286e1548b335c6cdb98205ea01475221030efc1f0cb52dfc48335810d38630a98e6884ea75e1e5fbd7c05a4b888beeebec2103b99798090319eaea87ab11d7d007e88d438ac147ec6e2165eb87171e08a1c2b152aeacf333200000000000000000000c000000002710000000002faf0800000000000bebc200fc852480fce78897590d18931f3d0697af871b67f1428e8c59d21c4aa1ddfb1702186519b93a8c9bec55b3496c9483242a0587525bff5603d9484742100b53afe9000000000000000000000000000000000000000500000000000000020000ff020f63b47126beb7dc930d1639f9e0971a0d147a557d00fa6b713f92f07e8a73bb2481c26703ee350c2defc5f06186e513b95038f0070263155a11a55b110019d666000000002b40420f00000000002200209e56159a6e582da6e14db7461a5936a58a55baa243c83231611cd367ad4f9649475221030efc1f0cb52dfc48335810d38630a98e6884ea75e1e5fbd7c05a4b888beeebec2103b99798090319eaea87ab11d7d007e88d438ac147ec6e2165eb87171e08a1c2b152ae0002003d0000fffffffffff8010650c983a9aa663bb634b47c144792150fd9cd0e2621dd26d40b7604352765071801f00007ffffffffffa0040d41ea1fb5689120f08b5f97c7929cbc72fece2f9cb179c2ef08043575dcc6f230000fffffffffff4081c26703ee350c2defc5f06186e513b95038f0070263155a11a55b110019d666000000000000061a8000000000000000000001fd02050200000000010181c26703ee350c2defc5f06186e513b95038f0070263155a11a55b110019d66600000000008786e0800650460000000000002200208d17f8ce0fb5199bf3e11b7a955ea7daaa11571057fd9455d735b05aafd6d7f8204e000000000000220020907e62299a1b92c8836bf03a7d1a9c887dc5c3e0ffe66feff9609bd81cf7390da8610000000000002200202f5d49e4c427129d546154f7ba80cc67d93fb720965bc64bf65b2a5b73889d90b88800000000000022002026bc9c36e6ed935e183c0eb92de335a9c8da6038e48c8dcf8361101de37c956bb8eb010000000000160014fc8bf8fcffad6adab3501a470afac2426df90c8190a00b0000000000220020b80866b204a5b57648b7c3ebd06eddf8936ded233ec5550d6b82081c4d7685930400473044022063a11e78bcf2d0241bc74005b6ea50604b030085b69bb135b972c7718f2f95f40220231d562020bceecc452e78616813d8c0790967ed1441ffc8d5546f51380866a10147304402200cb958272d3a7d9bc66015fc9cd661512c5db8150810c3cc157d83f9932cbdf602206dd941faab7aa65757d44d35418641c73e0f6435624286cb6e1d0d342fc0f70c01475221030efc1f0cb52dfc48335810d38630a98e6884ea75e1e5fbd7c05a4b888beeebec2103b99798090319eaea87ab11d7d007e88d438ac147ec6e2165eb87171e08a1c2b152aeadf33320ffc00200000000010144a2a76d9c8ca0533c23f0a4bb4fb2152bc6cead6981ac750a88e54951e16bcb040000000000000000019cda0100000000001600144aa46655da279b5eedda63abf309f1a805ed087c02483045022100e3b47f5e6962f8cd79e93ce4fe7632fa7f6d249dd17b86433d4a2f79c957c2ba022039e951552a72f5c884f7fc765abe54331eed07beaf361b0dbd522e67d9b80d7b012103241283b366441780a43655750be5a57971f8b2130c30188a0d7f2184519a076500000000ffee0200000000010144a2a76d9c8ca0533c23f0a4bb4fb2152bc6cead6981ac750a88e54951e16bcb0500000000ffffffff01a88d0b00000000001600144aa46655da279b5eedda63abf309f1a805ed087c03483045022100d63cd44d813ce92f0fd65140b954f84fd381f5d35b387e4e2d681e8ef1a5adf7022066706a758c18d1c193b0090f747b0eb85c2add3c11ace0d16c94997740e12e410101014d6321020373586870387b0483cd65641fba5b3af25dc15c72e44f641299697411b64e8b67029000b275210232d85a8549f7c760b20b062899c097a9999a6bf7c46e960def69c0cc2aa9c9b868ac000000000004fd01450200000000010144a2a76d9c8ca0533c23f0a4bb4fb2152bc6cead6981ac750a88e54951e16bcb0000000000ffffffff0138310000000000001600144aa46655da279b5eedda63abf309f1a805ed087c0347304402205c0ed89c351d43af0c7abf6a24ce30636dfe55261105aaaa03f017b61b97fcdb022013872d525dcf2367d110a939a8d1850dc3f2124b11dc66f1bfb4e0e487f1a93e0121020373586870387b0483cd65641fba5b3af25dc15c72e44f641299697411b64e8b8576a914ba607324990f54b8449be0e46f783c254675991c8763ac67210224cdb71653a2a77768106e6710fc335045c23399172c1e8b5444ee17580090fd7c820120876475527c210386000084d80729cbeb94e9816fc91ac124011b79ca68a0629212a46675b662f352ae67a914d9d01c45f608fa6d73753a83863469b973b64d3288ac686800000000fd01460200000000010144a2a76d9c8ca0533c23f0a4bb4fb2152bc6cead6981ac750a88e54951e16bcb0100000000ffffffff0108390000000000001600144aa46655da279b5eedda63abf309f1a805ed087c03483045022100de4f0a5d5f225303ac1e5185620e33e32b970e67561a1a94d3b28b08bc39e85c02202a43ef5ae02ad0ca79eabac2e844aa5e0c02aca2b46c200b613fa5de51ac278c0121020373586870387b0483cd65641fba5b3af25dc15c72e44f641299697411b64e8b8576a914ba607324990f54b8449be0e46f783c254675991c8763ac67210224cdb71653a2a77768106e6710fc335045c23399172c1e8b5444ee17580090fd7c820120876475527c210386000084d80729cbeb94e9816fc91ac124011b79ca68a0629212a46675b662f352ae67a914f43516dcc02db1dbfc68b53a1129f3578116dd4388ac686800000000fd014b0200000000010144a2a76d9c8ca0533c23f0a4bb4fb2152bc6cead6981ac750a88e54951e16bcb0200000000ffffffff01544c0000000000001600144aa46655da279b5eedda63abf309f1a805ed087c0347304402204238cf6593e137b4f8087a7661335566713badd6ab823b58cfb01e8aa04d984502205da1af983b813831e2f0f4dc0510f334cc9f20c19a9f66d01f9ce107008747310121020373586870387b0483cd65641fba5b3af25dc15c72e44f641299697411b64e8b8b76a914ba607324990f54b8449be0e46f783c254675991c8763ac67210224cdb71653a2a77768106e6710fc335045c23399172c1e8b5444ee17580090fd7c8201208763a914bffcd1ff89d9a58a7fbce039452ada42b9ad5dd088527c210386000084d80729cbeb94e9816fc91ac124011b79ca68a0629212a46675b662f352ae677503101b06b175ac686800000000fd014b0200000000010144a2a76d9c8ca0533c23f0a4bb4fb2152bc6cead6981ac750a88e54951e16bcb0300000000ffffffff0164730000000000001600144aa46655da279b5eedda63abf309f1a805ed087c03473044022049b9e9d5780960c6e93b6ee1398179020059c8688e31ba906b19547f73690f5d02203db4bc7331d6b8f61ea2b8deb491d0e998ce9a189f287f967f3f0865c1f1a3700121020373586870387b0483cd65641fba5b3af25dc15c72e44f641299697411b64e8b8b76a914ba607324990f54b8449be0e46f783c254675991c8763ac67210224cdb71653a2a77768106e6710fc335045c23399172c1e8b5444ee17580090fd7c8201208763a914965a3bfb147aa783bd0c850cca40f558a4ebdbce88527c210386000084d80729cbeb94e9816fc91ac124011b79ca68a0629212a46675b662f352ae677503101b06b175ac6868000000000002ed02000000000101a4cb5d4a3a460408b582cb720ff78e22e29019deb21d7399fbb7a299cf01d0400000000000ffffffff015a5a0000000000001600144aa46655da279b5eedda63abf309f1a805ed087c034730440220683a50c89b9770f0cb072e73fad0c97a3728532f8e007ffcdea0a526d791aad80220658115603831d9062a5d4e19e0fad5c79b6df34f565791ccb7bf373932a970e30101014d6321020373586870387b0483cd65641fba5b3af25dc15c72e44f641299697411b64e8b67029000b275210232d85a8549f7c760b20b062899c097a9999a6bf7c46e960def69c0cc2aa9c9b868ac00000000ee020000000001019586cb37e5cd9d1fac6cc4f51d7b452ae3b30ec1abac2f1b971d51c69da3ee960000000000ffffffff0182190000000000001600144aa46655da279b5eedda63abf309f1a805ed087c03483045022100dc35cab8904ed96334111419bbbd7b18327cd17515462af18052b99448e7e69102204120ead6f0d3de63911d5d56fc6f70507716c34b7dc59fac54d1e811cf65b21f0101014d6321020373586870387b0483cd65641fba5b3af25dc15c72e44f641299697411b64e8b67029000b275210232d85a8549f7c760b20b062899c097a9999a6bf7c46e960def69c0cc2aa9c9b868ac000000000000" - - Seq(dataNormal, dataWaitForFundingConfirmed, dataShutdown, dataNegotiating, dataClosingLocal, dataClosingRemote, dataClosingRevoked).foreach(oldBin => { - // check that this data has been encoded with the old codec - assert(oldBin.startsWith(hex"0100")) - // we decode with the new codec - val decoded1 = channelDataCodec.decode(oldBin.bits).require.value - // and we encode with the new codec - val newBin = channelDataCodec.encode(decoded1).require.bytes - // make sure that encoding used the new codec - assert(newBin.startsWith(hex"0400")) - // make sure that round-trip yields the same data - val decoded2 = channelDataCodec.decode(newBin.bits).require.value - assert(decoded1 == decoded2) - }) - - val negotiating = channelDataCodec.decode(dataNegotiating.bits).require.value.asInstanceOf[DATA_NEGOTIATING] - assert(negotiating.bestUnpublishedClosingTx_opt.nonEmpty) - negotiating.bestUnpublishedClosingTx_opt.foreach(tx => assert(tx.toLocalOutput.isEmpty)) - assert(negotiating.closingTxProposed.flatten.nonEmpty) - negotiating.closingTxProposed.flatten.foreach(tx => assert(tx.unsignedTx.toLocalOutput.isEmpty)) - - val normal = channelDataCodec.decode(dataNormal.bits).require.value.asInstanceOf[DATA_NORMAL] - assert(normal.commitments.latest.localCommit.htlcTxsAndRemoteSigs.nonEmpty) - normal.commitments.latest.localCommit.htlcTxsAndRemoteSigs.foreach(tx => assert(tx.htlcTx.htlcId == 0)) - - val closingLocal = channelDataCodec.decode(dataClosingLocal.bits).require.value.asInstanceOf[DATA_CLOSING] - assert(closingLocal.localCommitPublished.nonEmpty) - assert(closingLocal.localCommitPublished.get.commitTx.txOut.size == 6) - assert(closingLocal.localCommitPublished.get.htlcTxs.size == 4) - assert(closingLocal.localCommitPublished.get.claimHtlcDelayedTxs.size == 4) - assert(closingLocal.localCommitPublished.get.irrevocablySpent.isEmpty) - - val closingRemote = channelDataCodec.decode(dataClosingRemote.bits).require.value.asInstanceOf[DATA_CLOSING] - assert(closingRemote.remoteCommitPublished.nonEmpty) - assert(closingRemote.remoteCommitPublished.get.commitTx.txOut.size == 7) - assert(closingRemote.remoteCommitPublished.get.commitTx.txOut.count(_.amount == AnchorOutputsCommitmentFormat.anchorAmount) == 2) - assert(closingRemote.remoteCommitPublished.get.claimHtlcTxs.size == 3) - assert(closingRemote.remoteCommitPublished.get.irrevocablySpent.isEmpty) - - val closingRevoked = channelDataCodec.decode(dataClosingRevoked.bits).require.value.asInstanceOf[DATA_CLOSING] - assert(closingRevoked.revokedCommitPublished.size == 1) - assert(closingRevoked.revokedCommitPublished.head.commitTx.txOut.size == 6) - assert(closingRevoked.revokedCommitPublished.head.htlcPenaltyTxs.size == 4) - assert(closingRevoked.revokedCommitPublished.head.claimHtlcDelayedPenaltyTxs.size == 2) - assert(closingRevoked.revokedCommitPublished.head.irrevocablySpent.isEmpty) - } - - test("verify that we don't store local sigs for local commitment") { - val oldbins = List( - hex"00000103933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134000400633f73979e2e7cc9a76f2dbcccf4d9000000000000022200000002540be40000000000000003e800000000000000010090001e000bd48a6ca0842ad7ec52d29f4be4c7015ea447e435e5404380000000c5019a59a1964d2e91fa2ae22c629bd57e7458b98373a3503a6b3675778cc09c3298000000000000011e8000000002f34f6000000000000001f400000000000001f4004800f18145c7b7b7586534692bb0520ce003ebf3a611147729a240217370358f5712a72f01df72613647456031c11cf28c90e06230848223b1064616b89bb2e8d2557c631f011d20840603112fa5faaa4053f09f9f50bcdf94f392941569ce09310340dc50020117810d980eb608cae903683abfc8438fef5421397e688d8c4aa3a47e0fc3418601b035c2fabbe71271d3a07be023eae63e4ac5272bd09a6b990a5409160f3ab28a000000008400800000000000000000000000186a00000000000000000000000002faf08000124e7f7ef1634dc24da6981ce7b05c6afdaae490a8229d2d947a6e7e5861d76463800000000015d04300800000000011001070d0d1c4bd6ad8c9b7bcb36f6308b955dc28e4970b43991232fe217bf4a56deb0023a9108106cda8faa263d0e030d33025d6ebce83453f083c77035009f0bd1ce09e598179108145c7b7b7586534692bb0520ce003ebf3a611147729a240217370358f5712a72f2957009801000000000080ce7f7ef1634dc24da6981ce7b05c6afdaae490a8229d2d947a6e7e5861d7646380000000004e14ca4000a3318080000000000b000a0b0517d7dabd6f2da1a5bfef97681bd4958a94018200241822811080579dbcb14b3d3f2c7200693391d43977881357806ebd47b2633d0c1b5ac1a56481103c3a8f6a5e15cab0144f99ec5f875fabbdf32ef6748326f4a5b3a1b29b8c7f9e80a418228110806b4c99af1b3c8f74bef604336f1a1200a754f1680923a511f7af92391bac39ab811019085fba36cec85b5537807d07f2931858413a56c47b2cf7753fbcf4cc7206bc80a3a9108106cda8faa263d0e030d33025d6ebce83453f083c77035009f0bd1ce09e598179108145c7b7b7586534692bb0520ce003ebf3a611147729a240217370358f5712a72f2957435cc1100000000000000000000000000000186a0000000002faf08000000000000000005fbb84fad0781822e1268df19eb42468a5041d0b44dc8c7a21aa7aae19409bab81e514d9069fc6064c17f35d1d0f227fbe1b74ca89fec6c67b9668fe96341d43e200000000000000000000000000000000000000000000000000000000000040ee389313797b7f5177522d46afba5c43e37edebee6598c72e5a94c81fcc50a840009273fbf78b1a6e126d34c0e73d82e357ed5724854114e96ca3d373f2c30ebb231c0000000000ae821804000000000088008386868e25eb56c64dbde59b7b1845caaee14724b85a1cc89197f10bdfa52b6f58011d488408366d47d5131e87018699812eb75e741a29f841e3b81a804f85e8e704f2cc0bc8840a2e3dbdbac329a3495d829067001f5f9d3088a3b94d12010b9b81ac7ab89539794ab8000139fdfbc58d3709369a60739ec171abf6ab9242a08a74b651e9b9f961875d918ece7f7ef1634dc24da6981ce7b05c6afdaae490a8229d2d947a6e7e5861d76463cc80398ba555ae3310a427fe5a79299e970d0b199fdade631246d123ea6324e58f40736fc2307f587e516761e35b11c3d960991f1d905d523ff0cbdd88d527790", - hex"000002010000000103933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b1340009c5b105f42a2aea346cf8759f7c78135158b93878faac0659b26f723cc549fded80000000000000000000022200000004a817c8000000000000028f5c000000000000000102d0001e000b000a490f9af5166835d37b0f2155fa3a7a2b4fae66fd8000000105450189fed9a69c67c55d9743b3dde05b031088442ad94e9e3437e15f479f5cd66a7e000000000000011e80000001eefffe5c00000000000147ae00000000000001f403f000f18162eadafb948e88e02967005ef0da28be984f95b9fcce3cc7e1f1994d90af198c8179a7ba86216d011a9c1c49684578a6e0ca111279e6641404dee194eabcbbd21c81aaa1f3d35df2711a08374da8278fe259f6085189cc539a144c1aa0744b23495601331777d0354951fa354a8a2ee70e2bf40c9bb3f4fe340c9dab02bba5b6bb1aa00158bc5a03c0c79b947e5ac70f76f600c7fab8561e244f1425935fc51ffbe330380000000181515080800000000000000000000000007e800000000000000000000001f3fffe0c00121c2ba37173c23b01a1781e65cdf373e97395526f9e855164dac95c37502e9af3800000000015ffffff80000000001100101621d570811311537fddef81ee6f7023e2d17c11d32a53f472887dd035a78a518023a9108162eadafb948e88e02967005ef0da28be984f95b9fcce3cc7e1f1994d90af198c908181c230fcd426eee78d0fc56fa125155292858d5856a9a82ebfd6729d0db8664ea9570097010000000000809c2ba37173c23b01a1781e65cdf373e97395526f9e855164dac95c37502e9af380000000005657b04000a47fff80000000000b000a66e6e46b946f3259039de2840809a4b340aa510e8200239822011030949de6ac0462e1b5a42ded6c7763779d9f6aab069cb3366a3ca330bd1fe52b81101564903650deeb2a1f719865742e99ea0986774184727607a69bfff9f552a45a00a398220110228710b1083bde27eeb793ad155142cb69655ad8424eeae2044808edc631840d8110225098637d7f91c20e00d4a82a56520ad3cead728dd5b82b0bdcfe40d4b6acfb00a3a9108162eadafb948e88e02967005ef0da28be984f95b9fcce3cc7e1f1994d90af198c908181c230fcd426eee78d0fc56fa125155292858d5856a9a82ebfd6729d0db8664ea95773ad9b100000000000000000000000000000007e80000001f3fffe0c000000000000000063aa3a9e7e5e45c265ae8bf12e96a239ddbae4224c6f4560e149cce8a2675b208178d03c15e7303b3415e3549606ecab96ff5250ce38b2052b0a9dd2ed0af5ee75800000000000000000000000000000000000000000000000000000000000409cb1d9e3463cd48a65e9a2fa95f0755e924dba3664026d3682892bbf9527396bc0090e15d1b8b9e11d80d0bc0f32e6f9b9f4b9caa937cf42a8b26d64ae1ba8174d79c0000000000affffffc0000000000880080b10eab8408988a9bfeef7c0f737b811f168be08e99529fa39443ee81ad3c528c011d48840b1756d7dca47447014b3802f786d145f4c27cadcfe671e63f0f8cca6c8578cc64840c0e1187e6a137773c687e2b7d0928aa94942c6ac2b54d4175feb394e86dc332754ab8000070ae8dc5cf08ec0685e0799737cdcfa5ce5549be7a1545936b2570dd40ba6bce32de50000008000070ae8dc5cf08ec0685e0799737cdcfa5ce5549be7a1545936b2570dd40ba6bce0656b85b7c0e2dd09a620e28ddf865c6197074354c922bb9b76fb27ffe7fbf19cc0", - hex"00000203933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b13400041507665ff588dba1b9d4949e739ad05b0000000000000222000000003b9aca00000000000002718000000000000000010090001e000bd48a65a1ef54e50a013d73e46815abed7672835c83f34380000000c101d9ed460a901187b39b2c00d859f885104116abf778bf31c369342e4147db56e4800000000000011e80000001d87278e000000000000138c000000000000001f403c200f1811e1179456c73d778f073e17544813b4499e5053f9c8ac8f9fb1f0eb37f45ca4701e06722e502349a2857e99bf9299fe781a343aebce7557c0c3738e20f14b9e96f81985a99219fd30dcdf40438cc4d01d6027087f65d6dea4e0cd9864016febcbcbf8176c51f0ee44572f83a8a02f50053ed3cfcedbf5ce6c70f70970f5784c0bb0dfe0195a18fc266e7e7f1dabcc5234f493620de9f8e8d5932de3e8a44626974e0dd1080000000c100000000000000000000000005b8d8000000000000000000000001dd3826e0001208e981c8344120b2dce21aadce8a1ad9eb0123a574d898931d3d1449a33966f10080000000158c2b7a00000000001100101395ec5380c9641495ac8c4bb01605cf6ac58ee89a33da93d5eb09ab4be0e0ab8023a910811e1179456c73d778f073e17544813b4499e5053f9c8ac8f9fb1f0eb37f45ca471081ab89c14b9f60439db334231eff8706a1374282cf18444622420b7556a424830da95700980100000000008088e981c8344120b2dce21aadce8a1ad9eb0123a574d898931d3d1449a33966f100800000003eb8c1c0008006f600000000000b000a0eb37e79f65de8a4ca4df80979505338bcea6e76020024182281108070acce2513bf0df155ec51404333ac0397b6015bff2d87120d51d8dbf0b0f698811013a616842936434a093f8e9f85cbccb846ec2e0c49c9b541d46563cfb181886b00a418228110807ab650f61af7ce294df5a26e8cc1abb3658f0201c0cb9a2484e43fb9e61622d081102767e9f7608de2330774556121ddbd48c7a45d9598e38c821a3d9846c37e75f580a3a910811e1179456c73d778f073e17544813b4499e5053f9c8ac8f9fb1f0eb37f45ca471081ab89c14b9f60439db334231eff8706a1374282cf18444622420b7556a424830da957360f66900000000000000000000000000005b8d800000001dd3826e000000000000000005ef1d8da098d1f363033e64f2272a12b638bd3fe501c829d8350e8f3fc1c6b1481f12f3b5425786f43870f00174ddd36710d1864bbcf24aa54694cb183b43ebcbf000000000000000000000000000000000000000000000000000000000000408200a48c21439ebc4d7c70b310000bef69e454f808dee73be19aa960d54cac8100090474c0e41a2090596e710d56e7450d6cf58091d2ba6c4c498e9e8a24d19cb37880400000000ac615bd000000000008800809caf629c064b20a4ad64625d80b02e7b562c7744d19ed49eaf584d5a5f07055c011d488408f08bca2b639ebbc7839f0baa2409da24cf2829fce45647cfd8f8759bfa2e5238840d5c4e0a5cfb021ced99a118f7fc383509ba141678c2223112105baab52124186d4ab8000023a60720d10482cb73886ab73a286b67ac048e95d362624c74f451268ce59bc62b21ec0008d8000223a60720d10482cb73886ab73a286b67ac048e95d362624c74f451268ce59bc6049c6fe56892029844393a0afbf8c2c19088f565e80764b75a7e0591b97c1108ac0", - hex"000005010000000003933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b13400045d37887e8d909085b00af1d8b09c514b000000000000022200000004a817c8000000000000000b18000000000000000102d0001e000bd48a3d3ca19286d69a373eb6dda039da0cccf0bb6330c380000000c501df7e8244b99cc46d750bcef5830755886bad20416438b5973b3c8174f9d85b93000000000000011e80000000086131ec000000000000058c00000000000001f4004800f1817ce5a3cfc79898018c8d4489a393b1a46f6890a79a56737f867bfa56c511aa388107f85642fedecfcb7ef3cea6b38e26aead2759263d123692fd0c7d458fbc188881580b52cf6cdb449abc060bce9db3f35eebec2c87b316fd7a2c3193296d4a6e2e81fd2e032dd33ba9fa974246fccfc3ab5ccc1eb9ede9c900fc4e5d455c42150b3a8113237ef34839fc8055cbb63c0b9892defe6554c9dc70813681c2f3f35dedc13300000000c08080000000000000000000000010de8000000000000000000000000876dccc00125f6d0ab2880e92007f31c38dc0a8460c95a4c15291a24c92af1bca9264d8a915808000000015b7aa82000000000011001004a9c0705b42fa733f200639ed816601f0f413b9f29616f9c23f02f0dfc1df7e0023a910810115b6a5e11518dc1598fbc803018f3d87edac78de6e5544ff868bacb803485c10817ce5a3cfc79898018c8d4489a393b1a46f6890a79a56737f867bfa56c511aa38a957009781000000000080df6d0ab2880e92007f31c38dc0a8460c95a4c15291a24c92af1bca9264d8a91580800000003df2714000811e8200000000000b000a11e33689533b2499781f9b2f9c1893add8c57c4e02002418228110806567eeed21d6e87a045f3debf5ca10a05339c25f021400d96f7b492a957a94d301101df62a1c365062cce3b9a3ef530a4cd37eae4346491ed37e050dfd54213ef54f00a3982201103484562bcbb22ae3d892808bb04ff25f309a4732474a57bf6dd7691e17fc3df6811006854f362bcec61184d4721dfd30f777485be3580cd116564d139668bb6ee3a280a3a910810115b6a5e11518dc1598fbc803018f3d87edac78de6e5544ff868bacb803485c10817ce5a3cfc79898018c8d4489a393b1a46f6890a79a56737f867bfa56c511aa38a9573b51e910000000000000000000000000000010de800000000876dccc000000000000000025f43cb5a86018845618974486d0cb225355649b88690a914ee6cfec1a75286481d9daff2fcb7edb8d99f8fab9f77ff1033d5fc33a42ea7f8b70d29614b6d3195d000000000000000000000000000000000000000000000000000000000000409799acc6bbef72e7bc3dbed7831ac4e4ecfdad169d19628429edaa495d3c79ef00092fb68559440749003f98e1c6e05423064ad260a948d12649578de549326c548ac0400000000adbd54100000000000880080254e0382da17d399f90031cf6c0b300f87a09dcf94b0b7ce11f81786fe0efbf0011d48840808adb52f08a8c6e0acc7de40180c79ec3f6d63c6f372aa27fc345d65c01a42e0840be72d1e7e3cc4c00c646a244d1c9d8d237b44853cd2b39bfc33dfd2b6288d51c54ab800017db42aca203a4801fcc70e3702a118325693054a4689324abc6f2a499362a4557db42aca203a4801fcc70e3702a118325693054a4689324abc6f2a499362a454002f5228f4f2864a1b5a68dcfadb7680e7683333c2ed8cc30f7db42aca203a4801fcc70e3702a118325693054a4689324abc6f2a499362a454002c0028c95c468f1c569d43c9af6d335b8820bdda4888fe000200000", - hex"00000503933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134000454699830eddf332655e0ff86e6c536f30000000000000222000000003b9aca0000000000000186a000000000000000010090001e000bd48a482e9d4f3df4621c059bdf7df20f5ec9c4df26364380000000c101bf0694efa44b50d2817c3ac271bd38ad5110d72162deb6328cc919097ed7727f8000000000000111000000001dcd6500000000000000c35000000000000000008048000f01f7f51dd8abfc590feb6569d6f656b2a6205f339ec297e7c90239bf40aa432e9d01125974b8fc60d0122160b22db09edf15265c9f8d42c407a4b6e286445ce2897e01c8dfdd96184b8a8a0faa4d5d92acea243bb257e2bf911767efbf3e0fee216d3b81f4207be48f4ebbc397fb0bf3d223696ce431919a583ad69a7f3825323977b38a811b7e73e5b685f78f56e8d250e68d99452deed356a826fb5f207d314d0b61b97780000000c50000000000000000000000000001770000000000000000000000012a05f20000121a8b778cb9a0d244c835acbedbfc43713175d98e2d1a48316af9b751f4c75686800000000015c04b4c000000000011001029e7f0fe79b14835816eb3cb89338bf442f040ee84ea6fdfb401ac2aac96919a0023a91081eabde40c6773291d64bb71962995d7fd3401470b2e53a320331df956212b0c171081f7f51dd8abfc590feb6569d6f656b2a6205f339ec297e7c90239bf40aa432e9d29570097810000000000809a8b778cb9a0d244c835acbedbfc43713175d98e2d1a48316af9b751f4c7568680000000003b78dfc000b0ca4c00000000000b000a62d6449887f18b11f7c38a7d73a58620a23bda4682002418228110805b5b0d8c18572b99a6496d58eefe9bc20bcf359d91cc480c022591ba7c86d2280110096244c4df48199bd71b654c29ac49aa2a1dfcb6f2554a89b7d38bd28de98f6200a39822011013d445ecdc92ced0ebe8920580d3e5e7a1cdfd19fd39a5e57495a80e8a7cc0630110163a3f38ba3f23acc687ac397fb98d8dedb9a07a4085dec28d5a792e9ea2aece00a3a91081eabde40c6773291d64bb71962995d7fd3401470b2e53a320331df956212b0c171081f7f51dd8abfc590feb6569d6f656b2a6205f339ec297e7c90239bf40aa432e9d29570cf71b1000000000000000000000000000000177000000012a05f200000000000000000054e507323b7ab08f00c834e2ba694233f9d50293312291fc893691e512805f45814c861ee65261b9b7bdaaaebd59d316e5bf00417c2c9afeb5db143096feff164480000000000000000000000000000000000000000000000000000000000040b625f1f1e5239ce80103faf7851d4adbfbed7cf5bace51b959d5d6050f88e65d40090d45bbc65cd06922641ad65f6dfe21b898baecc7168d2418b57cdba8fa63ab4340000000000ae025a6000000000008800814f3f87f3cd8a41ac0b759e5c499c5fa21782077427537efda00d615564b48cd0011d48840f55ef20633b9948eb25db8cb14caebfe9a00a3859729d190198efcab1095860b8840fbfa8eec55fe2c87f5b2b4eb7b2b5953102f99cf614bf3e4811cdfa05521974e94ab800006a2dde32e683491320d6b2fb6ff10dc4c5d76638b46920c5abe6dd47d31d5a1a6a2dde32e683491320d6b2fb6ff10dc4c5d76638b46920c5abe6dd47d31d5a1a002f522920ba753cf7d18870166f7df7c83d7b27137c98d90e6a2dde32e683491320d6b2fb6ff10dc4c5d76638b46920c5abe6dd47d31d5a1a002f522994760b5d8d708a41fe54bdd87888baf41f10e7bd0e000200000", - hex"00000603933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b1340004004c3bfd866d20986aa90ec265fdfcbe0000000000000222000000012a05f20000000000000186a000000000000000010090001e000bd48a3b48ca7482f0442970651ba40bf45091fddef3a84380000000c501a0528fcd6c721246634deb024236eb2f4c124e13b587093a6652e7bfc12db3250000000000000111000000009502f900000000000000c350000000000000000083f0000f01210cce90e63d17d62e20b45b61b1add86631f6a5c90328c19fc1caf8085e600e0110d8d9aa7be8b4553b0af05b921c72c48356c4a0af61ffed7f1eadb7c5071c64816bf131172fe61d3145d39511755ee8095927534b76c4bb2464b706eaaee1486e01dd6caa8417804e26ce56f83a2525aa05fba25d3bab6db2cae5a203ce126da1ec01c8dfca1943df0dd0d59ffda66e207316c4af1cd3a4b6443fc5298a7a3864658f00000001404500000000000000001400000000017700000000015b95aa0000000128aa5c560012338c4e025330542050c2387b69ebb64e9c884113bb1045e996ae73e2b1b00a45800000000015c04b4c00000000001100104c97a415e599a3817965ff9a64e87d2af9bbcd77cae148ea934f9488216ffee10023a91081210cce90e63d17d62e20b45b61b1add86631f6a5c90328c19fc1caf8085e600e10814be9ac4ddf6ef583cde6ca4aea965a3f51028afcb1280f86f0ad47a533149d08a95700ad01000000000080b38c4e025330542050c2387b69ebb64e9c884113bb1045e996ae73e2b1b00a4580000000003eef56c0017b588000000000001100100783bea5072cc7b3be1475a5a8d7c45f16978bbc8b914937c89631a4e7f0d7e3b5714b80000000000b000a0c933f5731198ebb4e0fda5b7ba130c1c4d2fc870200239822011032e32cd21310cf1864c862bb32b9c78b1b8a626d1094d47d0e1938166388243f81101bbf4ba992687498419d57da8e6d700809fdf1f882425296292ff55b67ec82bd80a4182281108054815d830690f798ccd75722576882f62a088d37e81cde50c40bc5b080615c3e011030965e18229cbf3a03024a97988d75cb1b1569eaa1ce381d0689958161c4799900a3a91081210cce90e63d17d62e20b45b61b1add86631f6a5c90328c19fc1caf8085e600e10814be9ac4ddf6ef583cde6ca4aea965a3f51028afcb1280f86f0ad47a533149d08a9571b2c3e90000000000000000000140000000001770000000128aa5c5600000000015b95aa392494f078bec5b681d4997f2497d52112ad54eaaa81dec63f6480336139048b81fc2e019907aefb5efc5922187f575e10635611bde69ae0bd46b0b6ea44aea7748000000000000000000000000000000000000000000000000000000a000040f7dd9de11426234d0121ec1f5d2d19c3b4e78679b2e07a5c4f91bc624458352bc00919c6270129982a1028611c3db4f5db274e442089dd8822f4cb5739f158d80522c0000000000ae025a60000000000088008264bd20af2ccd1c0bcb2ffcd32743e957cdde6bbe570a47549a7ca4410b7ff708011d4884090866748731e8beb17105a2db0d8d6ec3318fb52e4819460cfe0e57c042f30070840a5f4d626efb77ac1e6f36525754b2d1fa881457e589407c37856a3d2998a4e8454ab8000800ec0003ffffffffff80106b5a98105912cee7f3cd2a4ed131283ee6fa4f5aef2e74aec08ebc2d6770ee38001e80007fffffffffec0082e868d9d8ebcc94bff771ca9d328bccfdfafdbc17e2b078b728caa0bb7179c6420001ffffffffffb0ce3138094cc150814308e1eda7aed93a7221044eec4117a65ab9cf8ac6c02916000200e60400000002ce3138094cc150814308e1eda7aed93a7221044eec4117a65ab9cf8ac6c029160000000001fffffffe05ed620000000000002f5228ed2329d20bc110a5c1946e902fd14247f77bcea10f17c52e00000000002f52295b2c2f65d18b177c1a91981b0335c8d209f93ea10e000000000002029e04000000000202ce3138094cc150814308e1eda7aed93a7221044eec4117a65ab9cf8ac6c029160000000001fffffffe05ed620000000000002f5228ed2329d20bc110a5c1946e902fd14247f77bcea10f17c52e00000000002f52295b2c2f65d18b177c1a91981b0335c8d209f93ea10e08008e608804401c9f21ea6ff52325709bc8fec460de1d9f584cbf6aced6abeda5842595fbb968044076a3a4b918db66a59ee47c4d3a2f8bb0f17665e6d904df5cab74ee06cbafbb3e028e60880440b6af6af4787b1229039a7351f7156d0cc30755a1902c6d483ea85915a29b40f6044023ea5be6088489f6dda062498c485465c7451d7bf49eb46dae206bf952ddc860028ea4420484333a4398f45f58b882d16d86c6b76198c7da97240ca3067f072be02179803842052fa6b1377dbbd60f379b292baa5968fd440a2bf2c4a03e1bc2b51e94cc527422a55c0000000000000", - hex"000008010000000003933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b1340004515822333393159ec64e3694540844cd000000000000022200000004a817c80000000000000003e8000000000000000102d0001e000bd48a04a8223d31a871ecced64af1c4690532d4b5de984380000000c5012e53ad31acde84a88f55e981f2c33e8d6b451e15463d3da9335dbe9e30a474c60000000000000111000000009502f90000000000000001f400000000000000008168000f01e35c520b80e355ee4ca8a1964a439e7d64fd2e1ddd6c10af140096471093a46c0126a65afe85868caba0a468ae6adf48b02e3dbd527a7d6e86c8b6426af06a5cce81ece00b5452805039314df65a98f5eeb21eab3d917853cf49567c28392e094806012f5f8f9177f320504b51ee8112a62fb5c505b8d9167737731545d99141d3485501004c1a5ece2ff034dcc63ec008df0ec8185934a2fcaf3075a75a5a999989d96580000000c50080000000000000000000000057e400000000000000000000000002faf08000125d32c79d17913addb93e6c7b952be8c2f93718867c1e39098daa4b85fc56a26c800000000015d04300800000000011001006333ad899044f5d624983aa15a497eb65df4372319091299696efb7f3d7b4cd8023a91081a2ad9c86cfde386a9c79f1c55d78d10bf335a2e43ff812f9df3c3c34bd423cb51081e35c520b80e355ee4ca8a1964a439e7d64fd2e1ddd6c10af140096471093a46c2957009781000000000080dd32c79d17913addb93e6c7b952be8c2f93718867c1e39098daa4b85fc56a26c80000000003b1f4bc000ae038080000000000b000a12c0490f3a2d18421362bc7a0b9aa7302410903302002418228110807b9455b3bbed8d0994cf1fd0d8fcd2e63a03710f470943da74ef0f4513fc6f43811009275b2d4020f8bfc179ae4dd5cb3935c30767f48ee723cca0d708184b01f2c400a3982201102f192e0d772fc5b08cba1e7b1c39dbed4f64439569c493af18a9539818a6f36c0110272c9b5f9058e69485ca53388311c00ff69b5631334ed4e002cef0958389e42880a3a91081a2ad9c86cfde386a9c79f1c55d78d10bf335a2e43ff812f9df3c3c34bd423cb51081e35c520b80e355ee4ca8a1964a439e7d64fd2e1ddd6c10af140096471093a46c295717042f10000000000000000000000000000057e40000000002faf080000000000000000046ec68ce2b1bca12747a13122270527bdad325cd34478d112adcc4bd0fc25f1e814f2919e34f1842677f99901e2aaf7f0abfafd39e6350238b52b8c2bf3b00ff7f00000000000000000000000000000000000000000000000000000000000040f5ca8901722651214939b99ca3c79307f3380af44ae1aecd4d317784fda7a15d80092e9963ce8bc89d6edc9f363dca95f4617c9b8c433e0f1c84c6d525c2fe2b513640000000000ae82180400000000008800803199d6c4c8227aeb124c1d50ad24bf5b2efa1b918c84894cb4b77dbf9ebda66c011d48840d156ce4367ef1c354e3cf8e2aebc6885f99ad1721ffc097cef9e1e1a5ea11e5a8840f1ae2905c071aaf7265450cb2521cf3eb27e970eeeb608578a004b238849d23614ab8000174cb1e745e44eb76e4f9b1ee54afa30be4dc6219f078e42636a92e17f15a89b2000000005d353b426e9963ce8bc89d6edc9f363dca95f4617c9b8c433e0f1c84c6d525c2fe2b513660192a0c75d44f2a6f4e1bc66fddf10b2949b9896cbe8548edeba49c0dd8300d82f42ef7330fb4601c7f2e1af3be67e3c3e86bb4c93a3bd9085c53732111c42dc", - hex"000009010000000003933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134000411bde5f686930a7085566a22fd7fac49000000000000022200000004a817c80000000000000005dc000000000000000102d0001e000bd48a142af4dfb86cd991ad356e17ef245bd58413568ec3800101000000c5017ebac21a9e7d42c23db38b88b689f1efb9ad52d0e728e4553493dcde20dff8f70000000000000111000000009502f90000000000000002ee000000000000000083f0000f015256f7e31cd7f9de64390c3a94e3adf51d0ccc2bd001322f4ccc8d4a1323fb7e81290a232a7ee5a18660742d9368628ddb929322bf8e55695aa3af19002b1fea2e810acfc782fa2344e096e930b4850396e2e004d875727f0839975bd3d79e3419eb81cc09de2631507630dd3788182d4e3e2c16c246fa0277f68efb26f195d11d6ce801a2b78a195c345f107a9dbd8d4927ca9c95b68c9c7431bd64132f1d5012d9c8dd8000000140450000000000000000030000000001770000000002549e60000000000223ca6000122b8f7f0d614f1b4439e38dc516105f660639328ea162648daa422a11369ff4c3000000000015f824810000000000110010682606ad0a329b07977b91626c3018d8c82cf084a7d4ef1f800efa5cd36985e90023a910815256f7e31cd7f9de64390c3a94e3adf51d0ccc2bd001322f4ccc8d4a1323fb7e9081ea642b2e5a831c6e4d1b68a3f138d52582957a4c56da11f68a0149160f4f248d295700ad01000000000080ab8f7f0d614f1b4439e38dc516105f660639328ea162648daa422a11369ff4c30000000000625ece40012c8b0080000000000b000a23484912e6810c08eeb52dfc67cee7a1d9321e1abc1880800000000011001028f4b0e4b17b183bbc7432902506a3accedb2459daf3146d8e0b1ac8d2a4b27782002418228110806873db49b74aa163df0331441bcc8cae3935262ce6accc9ce7e70dec462535c4811036156baddb0c8598cacfd150a6f7f3893d53b784ea0e188bd2400f0d68abf5ff00a398220110054bfe99fb9bd42af0827b4092a86f8c113ffecd69a7e7ac15d057da965d20ea01100c244a344b23872542da30b81f0dadb9790f4370e224de0b75ad606d79f76daf00a3a910815256f7e31cd7f9de64390c3a94e3adf51d0ccc2bd001322f4ccc8d4a1323fb7e9081ea642b2e5a831c6e4d1b68a3f138d52582957a4c56da11f68a0149160f4f248d2957663d311000000000000000000003000000000177000000000223ca600000000002549e602184aefb8063b9f14a8ef4be8ba84c09107359d62c994df78cc3b07e4c2abd7d018c4e716e3df369d6d8b17aed7197e453ac4ebb7a99b55e101cd2347794edc471000000000000000000000000000000000000000000000000000000010000409d47ae68afd388ad7bf011aac01308facdcab44edb8dc6f39409b69c4837f76d400915c7bf86b0a78da21cf1c6e28b082fb3031c994750b13246d52115089b4ffa6180000000000afc124080000000000880083413035685194d83cbbdc8b136180c6c6416784253ea778fc0077d2e69b4c2f48011d48840a92b7bf18e6bfcef321c861d4a71d6fa8e866615e8009917a66646a50991fdbf4840f53215972d418e37268db451f89c6a92c14abd262b6d08fb4500a48b07a7924694ab8000800f80003fffffffffff0020f2d63ebf3cd801d8cd1d3e6be1841a0e1027cff13d633b8efc7362951ad8c040003f0000fffffffffffa00407685484eaf45d16eae74cfb9a045e3e475f69d6362cda0f327659264a2db94d30000fffffffffffa571efe1ac29e368873c71b8a2c20becc0c72651d42c4c91b548454226d3fe986000000002eb1182c8000000040568080000000004055c7bf86b0a78da21cf1c6e28b082fb3031c994750b13246d52115089b4ffa618000000000312f672000964580400000000005800511a4248973408604775a96fe33e773d0ec990f0d5e0c404000000000088008147a587258bd8c1dde3a1948128351d6676d922ced798a36c7058d646952593bc100120c114088403439eda4dba550b1ef8198a20de646571c9a93167356664e73f386f623129ae240881b0ab5d6ed8642cc6567e8a8537bf9c49ea9dbc275070c45e9200786b455faff8051cc11008802a5ff4cfdcdea1578413da0495437c6089fff66b4d3f3d60ae82bed4b2e907500880612251a2591c392a16d185c0f86d6dcbc87a1b871126f05bad6b036bcfbb6d78051d48840a92b7bf18e6bfcef321c861d4a71d6fa8e866615e8009917a66646a50991fdbf4840f53215972d418e37268db451f89c6a92c14abd262b6d08fb4500a48b07a7924694abb31e9888201dc04000000000202a58948e8ec017cafede7ca2686457010875559b273a63b417b6fe6c22eeb21a80200000001c00e00000216600200000000002f522850abd37ee1b36646b4d5b85fbc916f56104d5a3b0e0690608a04420138a23062b387099c97d9458403dd33953e565d054b209e98684f4bd602cca268044076e7132bc91195ff251682cdaf978e6e7998f065ef6d4384c0ae7176888774ec02009ac64206e1126632e2d08e50f47925f11f8b04aba20cad7a65a25d0a32e2931d17945424ce05c00f64ea4206a88d1fa87c84e0c5228a2a5b045dccb605a01117740b58a7384f5d66528d7062d1580000000000000000000000040048ae3dfc35853c6d10e78e371458417d9818e4ca3a85899236a908a844da7fd30c00000001a920ea2ec3e76f7b403ba673b35954861170448627cbe7ecaf7c00ece94988a40048a58948e8ec017cafede7ca2686457010875559b273a63b417b6fe6c22eeb21a8020000009d78c152837378b3cbbc9b373ffbf698792e9046ee8f942855f69cd7808891e000000", - hex"0100220000000103933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b13400092dd581059d2bb431f96f6c199e59a9cdf4ddaea58dca8229725660804592523a80000000000000000000022200000004a817c80000000000000004b0000000000000000102d0001e0016001411e6e9810bf2d21b329be20c89fa19556bdd933c00000003080a8a037245a5d1fc8a4d0cf1bf882c121732b9255a327efba1c1c63e10f6ff481199f30000000000000222000000012a05f20000000000000004b0000000000000000107e0001e028f1b5095a886ec9efca0b014aa794c434f072fc8a435d2e98fe3db571fb26572021021b7b90467b6d39f86b9160d4801dc1e92eedbf5303ffc419fc013bd24a91d02766d36e2bc109b08ec5b0850c53a59372cd306cc1f95cefc4ceb24a6d7886e1b03a9ca87c8642c6770627ee356f92ceb075fa23aed68a6b952b50455ba939c9d85038ffc2170ef33aca1f4741347f1de06cda9f60cd2d5121e6bc8892953a38bb00e000000028a8a0000000000000000080000000002ee0000000000b2872000000000067486e0240098a98f1d736e4650bea1303e122e5e8c80005950b33590856332a2b1bea51f000000002bc0d40100000000002200205be011cd1ed87926bd56a773c1b1907fe457ba0bcc876dc61f32c1ca85470e49475221028f1b5095a886ec9efca0b014aa794c434f072fc8a435d2e98fe3db571fb265722103d5ddbf3cf1c396ad4be4c715759d8389a1f0b9bf1be1344c1258ec9bd79d2c4652aefd015a020000000001010098a98f1d736e4650bea1303e122e5e8c80005950b33590856332a2b1bea51f00000000005141888002b42d000000000000220020143e4a3411ea01f087e0904a2ae0f4a312355368091aa08acf2279f9b6b72711eda40100000000001600149be0e57e4e3561b30fb11014bc3ed2036855381c04004730440220068466106db388aabc63e77d137b815649614d0ac2530589ab4c5e05d78afe3f022064c9ee1be2b7b794b6645c7b5ec8f0e69e727980dc7219fe1715e9803178a98201483045022100e8e5ad22d38e27f56ad53e43e34656026a74ae3527ceb06752f9114483b057d702205978489fb86927818e0b71deeed44c8c5827237f40f92483b36688cc48171bb101475221028f1b5095a886ec9efca0b014aa794c434f072fc8a435d2e98fe3db571fb265722103d5ddbf3cf1c396ad4be4c715759d8389a1f0b9bf1be1344c1258ec9bd79d2c4652aee93b9820000000000000000000080000000002ee00000000067486e00000000000b28720ed5846e908bbaaa6b2f994f3c03f8f96350d2825cd6849104207a8ee263849b302bcc40fecd594b6fa15d22cd5e9a8df070ce97e8a1e90b4fdd8a1ef0ddb4c0d97000000000000000000000000000000000000000000000000000000010000ff036fd9558ffbcee019936ae3251b1696cf0ec92a58a73dcc6e0c33ea64738242e4240098a98f1d736e4650bea1303e122e5e8c80005950b33590856332a2b1bea51f000000002bc0d40100000000002200205be011cd1ed87926bd56a773c1b1907fe457ba0bcc876dc61f32c1ca85470e49475221028f1b5095a886ec9efca0b014aa794c434f072fc8a435d2e98fe3db571fb265722103d5ddbf3cf1c396ad4be4c715759d8389a1f0b9bf1be1344c1258ec9bd79d2c4652ae0001003d0000fffffffffff80100a03da9ba67d38e18a51216c7d501d200aa3127445c959dd972193d3dd5951e7c0003ffffffffffe00098a98f1d736e4650bea1303e122e5e8c80005950b33590856332a2b1bea51f1bffef0000420000ff0088d2195367160d41c7c2bf78ce8996e62b632adfb829c8382955b1e8a5c27008dd7e9d91440a520b59c49af0c1a5a5e1768e5eab47556a81c6248278acc7f5757643497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea3309000000001bffef00004200005f644c69010100900000000000000001000003e8000000640000000007270e000000", - hex"0100250000000003933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134000425ea3f2629ba65b6849f78829099a1d10000000000000222000000003b9aca000000000000027efb00000000000000010090001e0017a914d73900cac21afc97ce994c27af395e7f4d681bcb870000000182030540ba863ec0edc426981ffa8eace0be4e9a03295b3a0ff6ca545d21983e3b6b0000000000000222000000003b9aca000000000000027efb00000000000000010090001e02b8f1b6e1830cfc069650ea1d7571f7fffc591dd80ba0e2280a6643677de52c2303ac60bfe54dbbd771b9d4c92dd33b584d91cda6f4da296aa10f009f043743aa90034980f41ef05b9248f6793f77666534685df92b54dcb4ee1030f5dbef2e88a4e102177e1b10f681a1a46221c0f87aaa8e2a49d8257924d229faf8525e3f447374e5023c6191ef9b284cc6aa0c88b3913d91bf1bde6c5ec6f9d6f8dafa6bb65c2dcbbd000000010a01000000000000011f00000000251c000000000000000000000003cf01bee02412f1713bd4417815932d254d0e2e1ddc9292f81c41c90de45e6ae6301934c73c000000002b0c9af90000000000220020b2ad00d661059d790dea8056dfd8a962a53023a3603fda636c20eb8b4e434ecb475221029eb94e19945896a4cbe830d925272d3edb4426096b53cd88012edd1cc0518ff22102b8f1b6e1830cfc069650ea1d7571f7fffc591dd80ba0e2280a6643677de52c2352aefd01300200000000010112f1713bd4417815932d254d0e2e1ddc9292f81c41c90de45e6ae6301934c73c0000000000e3400480012e7ff90000000000160014c6a763425c341e0ba7d0c12ed9126a661e38aac3040048304502210097df6e1d55051150897b7196be2944239dc1d820ae08ba72cffdd2822d57da320220403951e2355f52e385a34665156402cb2e85e6133b3d2606e37947809ccf4888014830450221008700f11d141bad2fc7eb66163a8f621685420b75bfb9f2c49c7227bf8fdb2ef702202f4a8485c71dad72263bcae61d6e17f21fbbe24331c3119deda2eaa1420cd74e01475221029eb94e19945896a4cbe830d925272d3edb4426096b53cd88012edd1cc0518ff22102b8f1b6e1830cfc069650ea1d7571f7fffc591dd80ba0e2280a6643677de52c2352aea375b2200000000000000000011f00000000251c00000003cf01bee00000000000000000a6b6d346b134eeb64ed57d0b125934490af31cbb650813ae6454ab83d12042550331d262498b6ff23a23ac5a3e834afddf752bf1b5e81259233316a9ec72f5562f000000000000000000000000000000000000000000000000000000000000ff02db56e0e64c827f44aade7b016a37e6a7c0780db3666f7d8e8b422b6b02474b362412f1713bd4417815932d254d0e2e1ddc9292f81c41c90de45e6ae6301934c73c000000002b0c9af90000000000220020b2ad00d661059d790dea8056dfd8a962a53023a3603fda636c20eb8b4e434ecb475221029eb94e19945896a4cbe830d925272d3edb4426096b53cd88012edd1cc0518ff22102b8f1b6e1830cfc069650ea1d7571f7fffc591dd80ba0e2280a6643677de52c2352ae0006003f0000fffffffffee20041f7ae2006d76bc11dc18f8457cb4f76df375a85312315edbcfe508aca1596bcf4007a0001fffffffffdd002022e11a1cea6e7a8d662f74967d072ac654a72b59ada363c0e363ec4d26f8ef6800380000ffffffffff002065215a990d4af376ceb7647998ceea281d44fba94ec18c3a505f595fae0efe79003c0000fffffffffef00209a10496ebab959fe95ddfbe43f26cc9566bdc3a9cebc509e098367a2916b5e86003e0000fffffffffee40083551db0a0be081e7788bf7b13023ab3f306657022a9623a2179ca37c92f2e0d3801000003fffffffffb84008210ec1faedb71958c276e8e1502e7e1e588d4406cc9986157958e9d84f02a667e0001fffffffffdc212f1713bd4417815932d254d0e2e1ddc9292f81c41c90de45e6ae6301934c73c00000000005f47acf200000000fffd01300200000000010112f1713bd4417815932d254d0e2e1ddc9292f81c41c90de45e6ae6301934c73c0000000000e3400480012e7ff90000000000160014c6a763425c341e0ba7d0c12ed9126a661e38aac3040048304502210097df6e1d55051150897b7196be2944239dc1d820ae08ba72cffdd2822d57da320220403951e2355f52e385a34665156402cb2e85e6133b3d2606e37947809ccf4888014830450221008700f11d141bad2fc7eb66163a8f621685420b75bfb9f2c49c7227bf8fdb2ef702202f4a8485c71dad72263bcae61d6e17f21fbbe24331c3119deda2eaa1420cd74e01475221029eb94e19945896a4cbe830d925272d3edb4426096b53cd88012edd1cc0518ff22102b8f1b6e1830cfc069650ea1d7571f7fffc591dd80ba0e2280a6643677de52c2352aea375b2200000000000000000012412f1713bd4417815932d254d0e2e1ddc9292f81c41c90de45e6ae6301934c73c0000000083b5c6292f61ad3cba1e702abb6d4ec1aec503c5b1b393e09c942ad4448a1b930000000000", - hex"0200050000000103933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b1340009e9907c0bc4f4e5b55a3b7012180e63b0ebf76498c543b991c056f0930a6ac09d80000000000000000000022200000004a817c800000000000000c350000000000000000102d0001e0016001495cde5680fa7aaa16935b74cc11b4ee695aa6b1500000004080aa98202cc1bef9c46bae2f3a965d7bf4bfca249098c5847b71d700ca0eba00db523986a0000000000000222000000012a05f200000000000000c350000000000000000107e0001e026345c7976588f6d96e7438f8df34e3225d527e9da5e201dbf83b43a1f4c2731603d49ddadc0517b1bb1f9c2773621e62b7720b2a622e4a5548becc37164bcd70f10368718d6b2887b302f75e5af0bac2f3b883663bde0fd61344ce16ed9a31c55eda03530674aa8610a599d0cd8be226923b5d8fe2a422cfbdc4fb790e39c7b05e0b7e0246ce4c46c2ee4110cce17bae3c7e6de01a8f0f8c1970cf58b2122b8fc77ffdeb000000028a8a0000000000000001aa0000000002ee0000000019545f970000000110b19269240e985f9ec14d882a73f50843ad0fbcf37c644100b13c7eb3d91da703246a669d000000002b404b4c0000000000220020c670f08ef02bd0e9226f315eb2e2d193d2946d4aa3d03e612a0bdda231cfe9c4475221026345c7976588f6d96e7438f8df34e3225d527e9da5e201dbf83b43a1f4c273162103f4b51b541e6a9d935e83828bedbfcfb0ce404c809712376e672b624bb250beba52aefd015a020000000001010e985f9ec14d882a73f50843ad0fbcf37c644100b13c7eb3d91da703246a669d00000000008dc3288002ff7b0600000000002200206444bace1cb90460914f5c964acfdd7846104e93a205d0bd5d00eb73f03acced21cd4500000000001600148636658ef4d15c8d9824bfea9b70ddc592b24bb404004730440220137ad66cce66df0041e172036c0c6de1e1cc8981130883bf693960778a2160fa022039d9505c98f78f54aa30172f4deccae26f9c4a7b55e2b72d4bd8875809143d5501483045022100ced9a63de710f1339c5d9086b1cbe0097f942872fbf1d0ca3a643409933cc86802201d5e311f655f64fb64eae62c9a3e321329158d055ecce9c537657979c17fe62501475221026345c7976588f6d96e7438f8df34e3225d527e9da5e201dbf83b43a1f4c273162103f4b51b541e6a9d935e83828bedbfcfb0ce404c809712376e672b624bb250beba52ae38497c20000000000000000001aa0000000002ee0000000110b192690000000019545f97359430d3b112dd6d24edb449958073132a7721c737f2a72edf12ff74606d6c6f022030518cd036759a25ae66be2bbd46855ba6cdd214742f42aecd72b9093a444a000000000000000000000000000000000000000000000000000000d10000ff0200c2341c9b6d1ba8955188aa913cbf5b303c65ad19491df7d15fa0a811c44d38240e985f9ec14d882a73f50843ad0fbcf37c644100b13c7eb3d91da703246a669d000000002b404b4c0000000000220020c670f08ef02bd0e9226f315eb2e2d193d2946d4aa3d03e612a0bdda231cfe9c4475221026345c7976588f6d96e7438f8df34e3225d527e9da5e201dbf83b43a1f4c273162103f4b51b541e6a9d935e83828bedbfcfb0ce404c809712376e672b624bb250beba52ae0005003d0000fffffffffe580103151b3b767e35873dd3babc0f5bc9ea4a99069c269456b9e1157dda87db454b7801f80007fffffffff2b0020b4a390fde1b8bf038232ea79e48b825b5d1e930a97f2532aacbe0ddfaf24e9be00380000ffffffffff002038da5bbdce565d1eea32b7b9f4c4932e63892a154197054f5eb0116aab0f41de003b0000fffffffffe60041298f04989916fa9ac7b83da8a340a8bc6fcad73925c190576d728b28dfcf8de400720001fffffffffd00204225a2a872ea98ff1b22991635a2d6cb598839bd8a3988eeeb53adf784149fa880007fffffffff2b000e985f9ec14d882a73f50843ad0fbcf37c644100b13c7eb3d91da703246a669d0000000000001e2d5a0004240e985f9ec14d882a73f50843ad0fbcf37c644100b13c7eb3d91da703246a669d000000002b404b4c0000000000220020c670f08ef02bd0e9226f315eb2e2d193d2946d4aa3d03e612a0bdda231cfe9c4475221026345c7976588f6d96e7438f8df34e3225d527e9da5e201dbf83b43a1f4c273162103f4b51b541e6a9d935e83828bedbfcfb0ce404c809712376e672b624bb250beba52ae7202000000010e985f9ec14d882a73f50843ad0fbcf37c644100b13c7eb3d91da703246a669d0000000000ffffffff02ff7b06000000000016001495cde5680fa7aaa16935b74cc11b4ee695aa6b15eecd45000000000017a9142a90566bc7c0515d50552fb8bdf9e7f5c541637c8700000000ff000000000000000000067bff16001495cde5680fa7aaa16935b74cc11b4ee695aa6b15240e985f9ec14d882a73f50843ad0fbcf37c644100b13c7eb3d91da703246a669d000000002b404b4c0000000000220020c670f08ef02bd0e9226f315eb2e2d193d2946d4aa3d03e612a0bdda231cfe9c4475221026345c7976588f6d96e7438f8df34e3225d527e9da5e201dbf83b43a1f4c273162103f4b51b541e6a9d935e83828bedbfcfb0ce404c809712376e672b624bb250beba52ae7202000000010e985f9ec14d882a73f50843ad0fbcf37c644100b13c7eb3d91da703246a669d0000000000ffffffff02ff7b06000000000016001495cde5680fa7aaa16935b74cc11b4ee695aa6b15c4cd45000000000017a9142a90566bc7c0515d50552fb8bdf9e7f5c541637c8700000000ff000000000000000000067bff16001495cde5680fa7aaa16935b74cc11b4ee695aa6b15240e985f9ec14d882a73f50843ad0fbcf37c644100b13c7eb3d91da703246a669d000000002b404b4c0000000000220020c670f08ef02bd0e9226f315eb2e2d193d2946d4aa3d03e612a0bdda231cfe9c4475221026345c7976588f6d96e7438f8df34e3225d527e9da5e201dbf83b43a1f4c273162103f4b51b541e6a9d935e83828bedbfcfb0ce404c809712376e672b624bb250beba52ae7202000000010e985f9ec14d882a73f50843ad0fbcf37c644100b13c7eb3d91da703246a669d0000000000ffffffff02ff7b06000000000016001495cde5680fa7aaa16935b74cc11b4ee695aa6b15bacd45000000000017a9142a90566bc7c0515d50552fb8bdf9e7f5c541637c8700000000ff000000000000000000067bff16001495cde5680fa7aaa16935b74cc11b4ee695aa6b15240e985f9ec14d882a73f50843ad0fbcf37c644100b13c7eb3d91da703246a669d000000002b404b4c0000000000220020c670f08ef02bd0e9226f315eb2e2d193d2946d4aa3d03e612a0bdda231cfe9c4475221026345c7976588f6d96e7438f8df34e3225d527e9da5e201dbf83b43a1f4c273162103f4b51b541e6a9d935e83828bedbfcfb0ce404c809712376e672b624bb250beba52ae7202000000010e985f9ec14d882a73f50843ad0fbcf37c644100b13c7eb3d91da703246a669d0000000000ffffffff02ff7b06000000000016001495cde5680fa7aaa16935b74cc11b4ee695aa6b15b8cd45000000000017a9142a90566bc7c0515d50552fb8bdf9e7f5c541637c8700000000ff000000000000000000067bff16001495cde5680fa7aaa16935b74cc11b4ee695aa6b150001240e985f9ec14d882a73f50843ad0fbcf37c644100b13c7eb3d91da703246a669d000000002b404b4c0000000000220020c670f08ef02bd0e9226f315eb2e2d193d2946d4aa3d03e612a0bdda231cfe9c4475221026345c7976588f6d96e7438f8df34e3225d527e9da5e201dbf83b43a1f4c273162103f4b51b541e6a9d935e83828bedbfcfb0ce404c809712376e672b624bb250beba52aefd014f020000000001010e985f9ec14d882a73f50843ad0fbcf37c644100b13c7eb3d91da703246a669d0000000000ffffffff02ff7b06000000000016001495cde5680fa7aaa16935b74cc11b4ee695aa6b15b8cd45000000000017a9142a90566bc7c0515d50552fb8bdf9e7f5c541637c87040047304402203167bba6344d2d82aa11db437dcfd17903de22079f58597f8927813cb96c32c3022036ead9938307bcc82561f85cebd8123b273ca26e9aeafc6e0502e97a38b508da01483045022100e0cffda9bd2bd045b99024d99597698a25803d42cd22c8b58d7a9e29363e79e802202a0e27d553bce59bb7b4e2cd89b847ade01d39bcb1027e0a7c0b6e0d4fff096601475221026345c7976588f6d96e7438f8df34e3225d527e9da5e201dbf83b43a1f4c273162103f4b51b541e6a9d935e83828bedbfcfb0ce404c809712376e672b624bb250beba52ae00000000ff000000000000000000067bff16001495cde5680fa7aaa16935b74cc11b4ee695aa6b15000000000000", - ) - - oldbins.foreach { oldbin => - // we decode with compat codec - val oldnormal = channelDataCodec.decode(oldbin.bits).require.value.asInstanceOf[ChannelDataWithCommitments] - // and we encode with new codec - val newbin = channelDataCodec.encode(oldnormal).require.bytes - // make sure that round-trip yields the same data - val newnormal = channelDataCodec.decode(newbin.bits).require.value.asInstanceOf[ChannelDataWithCommitments] - assert(newnormal == oldnormal) - // make sure that we have stripped sigs from the transactions - assert(newnormal.commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx.txIn.forall(_.witness.stack.isEmpty)) - assert(newnormal.commitments.latest.localCommit.htlcTxsAndRemoteSigs.forall(_.htlcTx.tx.txIn.forall(_.witness.stack.isEmpty))) - // make sure that we have extracted the remote sig of the local tx - val RemoteSignature.FullSignature(remoteSig) = newnormal.commitments.latest.localCommit.commitTxAndRemoteSig.remoteSig - newnormal.commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.checkSig(remoteSig, newnormal.commitments.remoteNodeId, TxOwner.Remote, newnormal.commitments.params.commitmentFormat) - } - } - } object ChannelCodecsSpec { val seed: ByteVector32 = ByteVector32(ByteVector.fill(32)(1)) - val nodeKeyManager = new LocalNodeKeyManager(seed, Block.RegtestGenesisBlock.hash) - val channelKeyManager = new LocalChannelKeyManager(seed, Block.RegtestGenesisBlock.hash) - val localParams: LocalParams = LocalParams( + val nodeKeyManager: NodeKeyManager = LocalNodeKeyManager(seed, Block.RegtestGenesisBlock.hash) + val channelKeyManager: ChannelKeyManager = LocalChannelKeyManager(seed, Block.RegtestGenesisBlock.hash) + val localChannelParams: LocalChannelParams = LocalChannelParams( nodeKeyManager.nodeId, fundingKeyPath = DeterministicWallet.KeyPath(Seq(42L)), - dustLimit = Satoshi(546), - maxHtlcValueInFlightMsat = 50_000_000 msat, initialRequestedChannelReserve_opt = Some(10000 sat), - htlcMinimum = 10000 msat, - toSelfDelay = CltvExpiryDelta(144), - maxAcceptedHtlcs = 50, upfrontShutdownScript_opt = None, - walletStaticPaymentBasepoint = None, isChannelOpener = true, paysCommitTxFees = true, initFeatures = Features.empty) - - val remoteParams: RemoteParams = RemoteParams( + val remoteChannelParams: RemoteChannelParams = RemoteChannelParams( nodeId = randomKey().publicKey, - dustLimit = 546 sat, - maxHtlcValueInFlightMsat = UInt64(5000000), initialRequestedChannelReserve_opt = Some(10000 sat), - htlcMinimum = 5000 msat, - toSelfDelay = CltvExpiryDelta(144), - maxAcceptedHtlcs = 50, revocationBasepoint = PrivateKey(ByteVector.fill(32)(2)).publicKey, paymentBasepoint = PrivateKey(ByteVector.fill(32)(3)).publicKey, delayedPaymentBasepoint = PrivateKey(ByteVector.fill(32)(4)).publicKey, @@ -283,7 +137,7 @@ object ChannelCodecsSpec { initFeatures = Features.empty, upfrontShutdownScript_opt = None) - val paymentPreimages = Seq( + val paymentPreimages: Seq[ByteVector32] = Seq( ByteVector32(hex"0000000000000000000000000000000000000000000000000000000000000000"), ByteVector32(hex"0101010101010101010101010101010101010101010101010101010101010101"), ByteVector32(hex"0202020202020202020202020202020202020202020202020202020202020202"), @@ -292,11 +146,11 @@ object ChannelCodecsSpec { ) val htlcs: Seq[DirectedHtlc] = Seq[DirectedHtlc]( - IncomingHtlc(UpdateAddHtlc(ByteVector32.Zeroes, 0, 1000000 msat, Crypto.sha256(paymentPreimages(0)), CltvExpiry(500), TestConstants.emptyOnionPacket, None, 1.0, None)), - IncomingHtlc(UpdateAddHtlc(ByteVector32.Zeroes, 1, 2000000 msat, Crypto.sha256(paymentPreimages(1)), CltvExpiry(501), TestConstants.emptyOnionPacket, None, 1.0, None)), - OutgoingHtlc(UpdateAddHtlc(ByteVector32.Zeroes, 30, 2000000 msat, Crypto.sha256(paymentPreimages(2)), CltvExpiry(502), TestConstants.emptyOnionPacket, None, 1.0, None)), - OutgoingHtlc(UpdateAddHtlc(ByteVector32.Zeroes, 31, 3000000 msat, Crypto.sha256(paymentPreimages(3)), CltvExpiry(503), TestConstants.emptyOnionPacket, None, 1.0, None)), - IncomingHtlc(UpdateAddHtlc(ByteVector32.Zeroes, 2, 4000000 msat, Crypto.sha256(paymentPreimages(4)), CltvExpiry(504), TestConstants.emptyOnionPacket, None, 1.0, None)) + IncomingHtlc(UpdateAddHtlc(ByteVector32.Zeroes, 0, 1000000 msat, Crypto.sha256(paymentPreimages(0)), CltvExpiry(500), TestConstants.emptyOnionPacket, None, Reputation.maxEndorsement, None)), + IncomingHtlc(UpdateAddHtlc(ByteVector32.Zeroes, 1, 2000000 msat, Crypto.sha256(paymentPreimages(1)), CltvExpiry(501), TestConstants.emptyOnionPacket, None, Reputation.maxEndorsement, None)), + OutgoingHtlc(UpdateAddHtlc(ByteVector32.Zeroes, 30, 2000000 msat, Crypto.sha256(paymentPreimages(2)), CltvExpiry(502), TestConstants.emptyOnionPacket, None, Reputation.maxEndorsement, None)), + OutgoingHtlc(UpdateAddHtlc(ByteVector32.Zeroes, 31, 3000000 msat, Crypto.sha256(paymentPreimages(3)), CltvExpiry(503), TestConstants.emptyOnionPacket, None, Reputation.maxEndorsement, None)), + IncomingHtlc(UpdateAddHtlc(ByteVector32.Zeroes, 2, 4000000 msat, Crypto.sha256(paymentPreimages(4)), CltvExpiry(504), TestConstants.emptyOnionPacket, None, Reputation.maxEndorsement, None)) ) val normal: DATA_NORMAL = { @@ -313,30 +167,21 @@ object ChannelCodecsSpec { val fundingAmount = fundingTx.txOut.head.amount val fundingTxIndex = 0 val remoteFundingPubKey = PrivateKey(ByteVector32(ByteVector.fill(32)(1)) :+ 1.toByte).publicKey - val commitmentInput = Funding.makeFundingInputInfo(fundingTx.txid, 0, fundingAmount, channelKeyManager.fundingPublicKey(localParams.fundingKeyPath, fundingTxIndex).publicKey, remoteFundingPubKey) val remoteSig = ByteVector64(hex"2148d2d4aac8c793eb82d31bcf22d4db707b9fd7eee1b89b4b1444c9e19ab7172bab8c3d997d29163fa0cb255c75afb8ade13617ad1350c1515e9be4a222a04d") - val commitTx = Transaction( - version = 2, - txIn = TxIn( - outPoint = commitmentInput.outPoint, - signatureScript = ByteVector.empty, - sequence = 0, - witness = Scripts.witness2of2(randomBytes64(), remoteSig, channelKeyManager.fundingPublicKey(localParams.fundingKeyPath, fundingTxIndex).publicKey, remoteFundingPubKey)) :: Nil, - txOut = Nil, - lockTime = 0 - ) - val localCommit = LocalCommit(0, CommitmentSpec(htlcs.toSet, FeeratePerKw(1500 sat), 50000000 msat, 70000000 msat), CommitTxAndRemoteSig(CommitTx(commitmentInput, commitTx), remoteSig), Nil) + val localCommitParams = CommitParams(546 sat, 10_000 msat, UInt64(50_000_000), 50, CltvExpiryDelta(144)) + val localCommit = LocalCommit(0, CommitmentSpec(htlcs.toSet, FeeratePerKw(1500 sat), 50000000 msat, 70000000 msat), TxId.fromValidHex("2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a"), IndividualSignature(remoteSig), Nil) + val remoteCommitParams = CommitParams(546 sat, 5_000 msat, UInt64(5_000_000), 50, CltvExpiryDelta(144)) val remoteCommit = RemoteCommit(0, CommitmentSpec(htlcs.map(_.opposite).toSet, FeeratePerKw(1500 sat), 50000 msat, 700000 msat), TxId.fromValidHex("0303030303030303030303030303030303030303030303030303030303030303"), PrivateKey(ByteVector.fill(32)(4)).publicKey) val channelId = htlcs.headOption.map(_.add.channelId).getOrElse(ByteVector32.Zeroes) val channelFlags = ChannelFlags(announceChannel = true) val commitments = Commitments( - ChannelParams(channelId, ChannelConfig.standard, ChannelFeatures(), localParams, remoteParams, channelFlags), + ChannelParams(channelId, ChannelConfig.standard, ChannelFeatures(), localChannelParams, remoteChannelParams, channelFlags), CommitmentChanges(LocalChanges(Nil, Nil, Nil), RemoteChanges(Nil, Nil, Nil), localNextHtlcId = 32, remoteNextHtlcId = 4), - Seq(Commitment(fundingTxIndex, 0, remoteFundingPubKey, LocalFundingStatus.SingleFundedUnconfirmedFundingTx(None), RemoteFundingStatus.NotLocked, localCommit, remoteCommit, None)), + Seq(Commitment(fundingTxIndex, 0, OutPoint(fundingTx.txid, 0), fundingAmount, remoteFundingPubKey, LocalFundingStatus.SingleFundedUnconfirmedFundingTx(None), RemoteFundingStatus.NotLocked, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat, localCommitParams, localCommit, remoteCommitParams, remoteCommit, None)), remoteNextCommitInfo = Right(randomKey().publicKey), remotePerCommitmentSecrets = ShaChain.init, originChannels = origins) - DATA_NORMAL(commitments, ShortIdAliases(ShortChannelId.generateLocalAlias(), None), None, channelUpdate, None, None, None, SpliceStatus.NoSplice) + DATA_NORMAL(commitments, ShortIdAliases(ShortChannelId.generateLocalAlias(), None), None, channelUpdate, SpliceStatus.NoSplice, None, None, None) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelCodecs0Spec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelCodecs0Spec.scala deleted file mode 100644 index 647b973d76..0000000000 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelCodecs0Spec.scala +++ /dev/null @@ -1,88 +0,0 @@ -package fr.acinq.eclair.wire.internal.channel.version0 - -import fr.acinq.bitcoin.scalacompat.ByteVector32 -import fr.acinq.eclair.transactions.{IncomingHtlc, OutgoingHtlc} -import fr.acinq.eclair.wire.internal.channel.version0.ChannelCodecs0.Codecs._ -import fr.acinq.eclair.wire.internal.channel.version0.ChannelTypes0.ChannelVersion -import fr.acinq.eclair.wire.protocol.{OnionRoutingPacket, TlvStream, UpdateAddHtlc} -import fr.acinq.eclair.{CltvExpiry, MilliSatoshiLong} -import org.scalatest.funsuite.AnyFunSuite -import scodec.bits._ -import scodec.{Attempt, DecodeResult} - -class ChannelCodecs0Spec extends AnyFunSuite { - - test("encode/decode channel version in a backward compatible way (legacy)") { - val codec = channelVersionCodec - - // before we had commitment version, public keys were stored first (they started with 0x02 and 0x03) - val legacy02 = hex"02a06ea3081f0f7a8ce31eb4f0822d10d2da120d5a1b1451f0727f51c7372f0f9b" - val legacy03 = hex"03d5c030835d6a6248b2d1d4cac60813838011b995a66b6f78dcc9fb8b5c40c3f3" - val current02 = hex"010000000102a06ea3081f0f7a8ce31eb4f0822d10d2da120d5a1b1451f0727f51c7372f0f9b" - val current03 = hex"010000000103d5c030835d6a6248b2d1d4cac60813838011b995a66b6f78dcc9fb8b5c40c3f3" - val current04 = hex"010000000303d5c030835d6a6248b2d1d4cac60813838011b995a66b6f78dcc9fb8b5c40c3f3" - - assert(codec.decode(legacy02.bits) == Attempt.successful(DecodeResult(ChannelVersion.ZEROES, legacy02.bits))) - assert(codec.decode(legacy03.bits) == Attempt.successful(DecodeResult(ChannelVersion.ZEROES, legacy03.bits))) - assert(codec.decode(current02.bits) == Attempt.successful(DecodeResult(ChannelVersion.STANDARD, current02.drop(5).bits))) - assert(codec.decode(current03.bits) == Attempt.successful(DecodeResult(ChannelVersion.STANDARD, current03.drop(5).bits))) - assert(codec.decode(current04.bits) == Attempt.successful(DecodeResult(ChannelVersion.STATIC_REMOTEKEY, current04.drop(5).bits))) - - assert(codec.encode(ChannelVersion.STANDARD) == Attempt.successful(hex"0100000001".bits)) - assert(codec.encode(ChannelVersion.STATIC_REMOTEKEY) == Attempt.successful(hex"0100000003".bits)) - } - - test("backward compatibility local params with global features") { - // Backwards-compatibility: decode localparams with global features. - val withGlobalFeatures = hex"033b1d42aa7c6a1a3502cbcfe4d2787e9f96237465cd1ba675f50cadf0be17092500010000002a0000000026cb536b00000000568a2768000000004f182e8d0000000040dd1d3d10e3040d00422f82d368b09056d1dcb2d67c4e8cae516abbbc8932f2b7d8f93b3be8e8cc6b64bb164563d567189bad0e07e24e821795aaef2dcbb9e5c1ad579961680202b38de5dd5426c524c7523b1fcdcf8c600d47f4b96a6dd48516b8e0006e81c83464b2800db0f3f63ceeb23a81511d159bae9ad07d10c0d144ba2da6f0cff30e7154eb48c908e9000101000001044500" - val withGlobalFeaturesDecoded = localParamsCodec(ChannelVersion.STANDARD).decode(withGlobalFeatures.bits).require.value - assert(withGlobalFeaturesDecoded.initFeatures.toByteVector == hex"0a8a") - } - - test("backward compatibility of htlc codec") { - - val codec = htlcCodec - - // these encoded HTLC were produced by a previous version of the codec (at commit 8932785e001ddfe32839b3f83468ea19cf00b289) - val encodedHtlc1 = hex"89d5618930919bd77c07ea3931e7791010a9637c3d06e091b2dad38f21bbcffa00000000384ffa48000000003dbf35101f8724fbd36096ea5f462be20d20bc2f93ebc2c8a3f00b7a78c5158b5a0e9f6b1666923e800f1d073e2adcfba5904f1b8234af1c43a6e84a862a044d15f33addf8d41b3cfb7f96d815d2248322aeadd0ce7bacbcc44e611f66c35c439423c099e678c077203d5fc415ec37b798c6c74c9eed0806e6cb20f2b613855772c086cee60642b3b9c3919627c877a62a57dcf6ce003bedc53a8b7a0d7aa91b2304ef8b512fe1a9a043e410d7cd3009edffcb5d4c05cfb545aced4afea8bbe26c5ff492602edf9d4eb731541e60e48fd1ae5e33b04a614346fb16e09ccd9bcb8907fe9fc287757ea9280a03462299e950a274c1dc53fbae8c421e67d7de35709eda0f11bcd417c0f215667e8b8ccae1035d0281214af25bf690102b180e5d4b57323d02ab5cee5d3669b4300539d02eff553143f085cd70e5428b7af3262418aa7664d56c3fd29c00a2f88a6a5ee9685f45b6182c45d492b2170b092e4b5891247bcffe82623b86637bec291cca1dc729f5747d842ecdf2fc24eaf95c522cbebe9a841e7cff837e715b689f0b366b92a2850875636962ba42863ab6df12ee938ada6e6ae795f8b4fbe81adea478caa9899fed0d6ccdf7a2173b69b2d3ff1b93c82c08c4da63b426d2f94912109997e8ee5830c5ffe3b60c97438ae1521a2956e73a9a60f16dc13a5e6565904e04bf66ceda3db693fc7a0c6ad4f7dc8cb7f1ef54527c11589b7c35ce5b20e7f23a0ab107a406fa747435ff08096a7533a8ab7f5d3630d5c20c9161101f922c76079497e00e3ca62bce033b2bb065ea1733c50b5a06492d2b46715812003f29a8754b5dc1649082893e7be76550c58d98e81556e4ddf20a244f363bc23e756c95224335d0eeccd3da06a9161c4c72ae3d93afe902a806eadd2167d15c04cf3028fc61d0843bd270fd702a2c5af889ab5bc79a294847914f8dd409a9b990a96397d9046c385ca9810fb7c7b2c61491c67264257a601be7fe8c47a859b56af41caf06be7ea1cdb540719fc3bc2603675b79fd36a6f2911043b78da9f186d2a01f1209d0d91508e8ebecce09fd72823d0c166542f6d059fa8725d9d719a2532289c88f7a291a6bbe01f5b1f83cc2232d716f7dfc6a103fb8637d759aab939aaa278cffe04a64f4142564113080276bee7d3ec62e3f887838e3821f0dd713337972df994160edc29ccb9b9630c41a9ec7c994cbef2501a610e1c3684e697df230fd6f6f10526c9446e8307a1fb7e4988cdf7fc8aa32c8a09206113d8247aaae42e3942c0ffd291d67837d2c88231c85882667582eca1d2566134c4ee1301de8e1637f09467b473ba3e353992488048bd34b26dcc6f6f474751b7ac5bbad468c64eda2aeabfe6a92150a4faab142229d7934c4a24427441850d0deae5db802b02940435f39ceaa85e2d3d2269510881ab26926c3167487aa138d38b9cf650f59f0aa0b84297479271c2009cde61e5c58c26bf8a15aba86869af83941ec14972d93b6ae4a6ecf6584238150a61487d6bd394db40a10d710fd2d065850e52ea6536a74d88947448221c1ce493fecbf2070998e04d5263935488c2935f2d3afed4d0fc7472c03e652f928e6a18f78029043f219f652d992e104529149a978e5c660c0081fe6a179dbe62dcb597f3b4e497c6049b0255f8f306e4b18c97c339c98270abf86a4eb1af93b14d880eeda203bb3ba5b6e3113d0e003f8e55f3d446bd4dcda686b357ca0adf1fe25390767a40ff086a9258d04c19b0474488aaafac321f087d2bd0dc0e056ad9f5b5afa5f3d82bc3f18b33de9044529637fed05879f6bd440f331c06008dd38c2fb822c22fc4201e97f9ef9fc351807c045dece147d19fd01a68604c3cb6b5e0db1b4d1ebe387670021067d94206fbdc9ed33ac1f49d87f961cb5d44f48805e55f8637ca3de4ec9dd969944ed61de45970b7ef96d9f313a41de1cae380e0fe4b56729f275e2a0a87403c90e80" - val encodedHtlc2 = hex"09d5618930919bd77c07ea3931e7791010a9637c3d06e091b2dad38f21bbcffa00000000384ffa48000000003dbf35101f8724fbd36096ea5f462be20d20bc2f93ebc2c8a3f00b7a78c5158b5a0e9f6b1666923e800f1d073e2adcfba5904f1b8234af1c43a6e84a862a044d15f33addf8d41b3cfb7f96d815d2248322aeadd0ce7bacbcc44e611f66c35c439423c099e678c077203d5fc415ec37b798c6c74c9eed0806e6cb20f2b613855772c086cee60642b3b9c3919627c877a62a57dcf6ce003bedc53a8b7a0d7aa91b2304ef8b512fe1a9a043e410d7cd3009edffcb5d4c05cfb545aced4afea8bbe26c5ff492602edf9d4eb731541e60e48fd1ae5e33b04a614346fb16e09ccd9bcb8907fe9fc287757ea9280a03462299e950a274c1dc53fbae8c421e67d7de35709eda0f11bcd417c0f215667e8b8ccae1035d0281214af25bf690102b180e5d4b57323d02ab5cee5d3669b4300539d02eff553143f085cd70e5428b7af3262418aa7664d56c3fd29c00a2f88a6a5ee9685f45b6182c45d492b2170b092e4b5891247bcffe82623b86637bec291cca1dc729f5747d842ecdf2fc24eaf95c522cbebe9a841e7cff837e715b689f0b366b92a2850875636962ba42863ab6df12ee938ada6e6ae795f8b4fbe81adea478caa9899fed0d6ccdf7a2173b69b2d3ff1b93c82c08c4da63b426d2f94912109997e8ee5830c5ffe3b60c97438ae1521a2956e73a9a60f16dc13a5e6565904e04bf66ceda3db693fc7a0c6ad4f7dc8cb7f1ef54527c11589b7c35ce5b20e7f23a0ab107a406fa747435ff08096a7533a8ab7f5d3630d5c20c9161101f922c76079497e00e3ca62bce033b2bb065ea1733c50b5a06492d2b46715812003f29a8754b5dc1649082893e7be76550c58d98e81556e4ddf20a244f363bc23e756c95224335d0eeccd3da06a9161c4c72ae3d93afe902a806eadd2167d15c04cf3028fc61d0843bd270fd702a2c5af889ab5bc79a294847914f8dd409a9b990a96397d9046c385ca9810fb7c7b2c61491c67264257a601be7fe8c47a859b56af41caf06be7ea1cdb540719fc3bc2603675b79fd36a6f2911043b78da9f186d2a01f1209d0d91508e8ebecce09fd72823d0c166542f6d059fa8725d9d719a2532289c88f7a291a6bbe01f5b1f83cc2232d716f7dfc6a103fb8637d759aab939aaa278cffe04a64f4142564113080276bee7d3ec62e3f887838e3821f0dd713337972df994160edc29ccb9b9630c41a9ec7c994cbef2501a610e1c3684e697df230fd6f6f10526c9446e8307a1fb7e4988cdf7fc8aa32c8a09206113d8247aaae42e3942c0ffd291d67837d2c88231c85882667582eca1d2566134c4ee1301de8e1637f09467b473ba3e353992488048bd34b26dcc6f6f474751b7ac5bbad468c64eda2aeabfe6a92150a4faab142229d7934c4a24427441850d0deae5db802b02940435f39ceaa85e2d3d2269510881ab26926c3167487aa138d38b9cf650f59f0aa0b84297479271c2009cde61e5c58c26bf8a15aba86869af83941ec14972d93b6ae4a6ecf6584238150a61487d6bd394db40a10d710fd2d065850e52ea6536a74d88947448221c1ce493fecbf2070998e04d5263935488c2935f2d3afed4d0fc7472c03e652f928e6a18f78029043f219f652d992e104529149a978e5c660c0081fe6a179dbe62dcb597f3b4e497c6049b0255f8f306e4b18c97c339c98270abf86a4eb1af93b14d880eeda203bb3ba5b6e3113d0e003f8e55f3d446bd4dcda686b357ca0adf1fe25390767a40ff086a9258d04c19b0474488aaafac321f087d2bd0dc0e056ad9f5b5afa5f3d82bc3f18b33de9044529637fed05879f6bd440f331c06008dd38c2fb822c22fc4201e97f9ef9fc351807c045dece147d19fd01a68604c3cb6b5e0db1b4d1ebe387670021067d94206fbdc9ed33ac1f49d87f961cb5d44f48805e55f8637ca3de4ec9dd969944ed61de45970b7ef96d9f313a41de1cae380e0fe4b56729f275e2a0a87403c90e80" - - val ref = UpdateAddHtlc( - ByteVector32(hex"13aac312612337aef80fd47263cef2202152c6f87a0dc12365b5a71e43779ff4"), - 1889531024, - 2071882272 msat, - ByteVector32(hex"3f0e49f7a6c12dd4be8c57c41a41785f27d7859147e016f4f18a2b16b41d3ed6"), - CltvExpiry(751641725), - OnionRoutingPacket( - 0, - hex"1e3a0e7c55b9f74b209e3704695e38874dd0950c54089a2be675bbf1a83679f6ff", - hex"2db02ba44906455d5ba19cf75979889cc23ecd86b88728478133ccf180ee407abf882bd86f6f318d8e993dda100dcd9641e56c270aaee5810d9dcc0c85677387232c4f90ef4c54afb9ed9c0077db8a7516f41af552364609df16a25fc3534087c821af9a6013dbff96ba980b9f6a8b59da95fd5177c4d8bfe924c05dbf3a9d6e62a83cc1c91fa35cbc676094c2868df62dc1399b3797120ffd3f850eeafd525014068c4533d2a144e983b8a7f75d18843ccfafbc6ae13db41e2379a82f81e42accfd171995c206ba05024295e4b7ed202056301cba96ae647a0556b9dcba6cd368600a73a05dfeaa6287e10b9ae1ca8516f5e64c483154ecc9aad87fa5380145f114d4bdd2d0be8b6c30588ba925642e16125c96b12248f79ffd04c4770cc6f7d85239943b8e53eae8fb085d9be5f849d5f2b8a4597d7d35083cf9ff06fce2b6d13e166cd725450a10eac6d2c574850c756dbe25dd2715b4dcd5cf2bf169f7d035bd48f19553133fda1ad99bef442e76d365a7fe372790581189b4c7684da5f2922421332fd1dcb0618bffc76c192e8715c2a43452adce7534c1e2db8274bccacb209c097ecd9db47b6d27f8f418d5a9efb9196fe3dea8a4f822b136f86b9cb641cfe47415620f480df4e8e86bfe1012d4ea675156feba6c61ab841922c2203f2458ec0f292fc01c794c579c06765760cbd42e678a16b40c925a568ce2b024007e5350ea96bb82c92105127cf7cecaa18b1b31d02aadc9bbe414489e6c77847cead92a44866ba1dd99a7b40d522c3898e55c7b275fd205500dd5ba42cfa2b8099e6051f8c3a10877a4e1fae05458b5f11356b78f3452908f229f1ba81353732152c72fb208d870b953021f6f8f658c29238ce4c84af4c037cffd188f50b36ad5e8395e0d7cfd439b6a80e33f87784c06ceb6f3fa6d4de52220876f1b53e30da5403e2413a1b22a11d1d7d99c13fae5047a182cca85eda0b3f50e4bb3ae3344a64513911ef45234d77c03eb63f07984465ae2defbf8d4207f70c6faeb35572735544f19ffc094c9e8284ac82261004ed7dcfa7d8c5c7f10f071c7043e1bae2666f2e5bf3282c1db853997372c6188353d8f932997de4a034c21c386d09cd2fbe461fadede20a4d9288dd060f43f6fc93119beff9154659141240c227b048f555c85c728581ffa523acf06fa591046390b104cceb05d943a4acc26989dc2603bd1c2c6fe128cf68e7747c6a73249100917a6964db98dede8e8ea36f58b775a8d18c9db455d57fcd5242a149f556284453af2698944884e8830a1a1bd5cbb700560528086be739d550bc5a7a44d2a21103564d24d862ce90f54271a71739eca1eb3e154170852e8f24e3840139bcc3cb8b184d7f142b5750d0d35f07283d8292e5b276d5c94dd9ecb084702a14c290fad7a729b681421ae21fa5a0cb0a1ca5d4ca6d4e9b1128e890443839c927fd97e40e1331c09aa4c726a9118526be5a75fda9a1f8e8e5807cca5f251cd431ef0052087e433eca5b325c208a5229352f1cb8cc180103fcd42f3b7cc5b96b2fe769c92f8c093604abf1e60dc963192f86739304e157f0d49d635f27629b101ddb440776774b6dc6227a1c007f1cabe7a88d7a9b9b4d0d66af9415be3fc4a720ecf481fe10d524b1a09833608e8911555f58643e10fa57a1b81c0ad5b3eb6b5f4be7b05787e31667bd2088a52c6ffda0b0f3ed7a881e66380c011ba7185f7045845f88403d2ff3df3f86a300f808bbd9c28fa33fa034d0c098796d6bc1b6369a3d7c70ece00420cfb2840df7b93da67583e93b0ff2c396ba89e9100bcabf0c6f947bc9d93bb2d3289", - ByteVector32(hex"dac3bc8b2e16fdf2db3e627483bc395c701c1fc96ace53e4ebc54150e807921d")), - TlvStream.empty - ) - val remaining = bin"0000000" // 7 bits remainder because the direction is encoded with 1 bit and we are dealing with bytes - - val DecodeResult(h1, r1) = codec.decode(encodedHtlc1.toBitVector).require - val DecodeResult(h2, r2) = codec.decode(encodedHtlc2.toBitVector).require - - assert(h1 == IncomingHtlc(ref)) - assert(h2 == OutgoingHtlc(ref)) - assert(r1 == remaining) - assert(r2 == remaining) - - assert(codec.encode(h1).require.bytes == encodedHtlc1) - assert(codec.encode(h2).require.bytes == encodedHtlc2) - } - - test("tell channel_update length") { - // with option_channel_htlc_max (136 bytes) - val u1 = hex"81D6DE1B150A0EAD83B214C879BD55FB3EE82E54F3F38DF601B692B481DFFBBF66582C018B271FBEBF17B0F72FAD1E1B78EF19232621AA2D8055B51D94C5A39543497FD7F826957108F4A30FD9CEC3AEBA79972084E90EAD01EA33090000000013AB9500006E00005D117A190100009000000000000003E8000003E80000000100000003E8000000".bits - // without option_channel_htlc_max (128 bytes) - val u2 = hex"A94A853FCDE515F89259E03D10368B1A600B3BF78F6BD5C968469C0816F45EFF7878714DF26B580D5A304334E46816D5AC37B098EBC46C1CE47E37504D052DD643497FD7F826957108F4A30FD9CEC3AEBA79972084E90EAD01EA33090000000013AB9500006E00005D1149290001009000000000000003E8000003E800000001".bits - - // check that we decode correct length, and that we just take a peek without actually consuming data - assert(noUnknownFieldsChannelUpdateSizeCodec.decode(u1) == Attempt.successful(DecodeResult(136, u1))) - assert(noUnknownFieldsChannelUpdateSizeCodec.decode(u2) == Attempt.successful(DecodeResult(128, u2))) - } - -} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1Spec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1Spec.scala deleted file mode 100644 index 2fb4def03c..0000000000 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1Spec.scala +++ /dev/null @@ -1,174 +0,0 @@ -package fr.acinq.eclair.wire.internal.channel.version1 - -import akka.actor.ActorSystem -import akka.testkit.TestProbe -import fr.acinq.bitcoin.scalacompat.DeterministicWallet.KeyPath -import fr.acinq.bitcoin.scalacompat.{OutPoint, SatoshiLong} -import fr.acinq.eclair.TestUtils.randomTxId -import fr.acinq.eclair.blockchain.fee.FeeratePerKw -import fr.acinq.eclair.channel.{Origin, Upstream} -import fr.acinq.eclair.transactions.{CommitmentSpec, DirectedHtlc, IncomingHtlc, OutgoingHtlc} -import fr.acinq.eclair.wire.internal.channel.version0.ChannelTypes0.ChannelVersion -import fr.acinq.eclair.wire.internal.channel.version1.ChannelCodecs1.Codecs._ -import fr.acinq.eclair.wire.protocol.UpdateAddHtlc -import fr.acinq.eclair.{CltvExpiry, MilliSatoshi, MilliSatoshiLong, TestConstants, TimestampMilli, randomBytes32, randomKey} -import org.scalatest.funsuite.AnyFunSuite -import scodec.bits._ -import scodec.{Attempt, DecodeResult} - -import java.util.UUID -import scala.util.Random - -class ChannelCodecs1Spec extends AnyFunSuite { - - test("encode/decode key paths (all 0s)") { - val keyPath = KeyPath(Seq(0L, 0L, 0L, 0L)) - val encoded = keyPathCodec.encode(keyPath).require - val decoded = keyPathCodec.decode(encoded).require - assert(keyPath == decoded.value) - } - - test("encode/decode key paths (all 1s)") { - val keyPath = KeyPath(Seq(0xffffffffL, 0xffffffffL, 0xffffffffL, 0xffffffffL)) - val encoded = keyPathCodec.encode(keyPath).require - val decoded = keyPathCodec.decode(encoded).require - assert(keyPath == decoded.value) - } - - test("encode/decode channel version") { - val current02 = hex"0000000102a06ea3081f0f7a8ce31eb4f0822d10d2da120d5a1b1451f0727f51c7372f0f9b" - val current03 = hex"0000000103d5c030835d6a6248b2d1d4cac60813838011b995a66b6f78dcc9fb8b5c40c3f3" - val current04 = hex"0000000303d5c030835d6a6248b2d1d4cac60813838011b995a66b6f78dcc9fb8b5c40c3f3" - val current05 = hex"0000000703d5c030835d6a6248b2d1d4cac60813838011b995a66b6f78dcc9fb8b5c40c3f3" - - assert(channelVersionCodec.decode(current02.bits) == Attempt.successful(DecodeResult(ChannelVersion.STANDARD, current02.drop(4).bits))) - assert(channelVersionCodec.decode(current03.bits) == Attempt.successful(DecodeResult(ChannelVersion.STANDARD, current03.drop(4).bits))) - assert(channelVersionCodec.decode(current04.bits) == Attempt.successful(DecodeResult(ChannelVersion.STATIC_REMOTEKEY, current04.drop(4).bits))) - assert(channelVersionCodec.decode(current05.bits) == Attempt.successful(DecodeResult(ChannelVersion.ANCHOR_OUTPUTS, current05.drop(4).bits))) - - assert(channelVersionCodec.encode(ChannelVersion.STANDARD) == Attempt.successful(hex"00000001".bits)) - assert(channelVersionCodec.encode(ChannelVersion.STATIC_REMOTEKEY) == Attempt.successful(hex"00000003".bits)) - assert(channelVersionCodec.encode(ChannelVersion.ANCHOR_OUTPUTS) == Attempt.successful(hex"00000007".bits)) - } - - test("decode local params") { - // we use data encoded with v1 codecs (before upfrontShutdownScript_opt was made optional) and check it can still be decoded and that upfrontShutdownScript_opt is always defined - val std = hex"0312f3b6afc20f21b77d8404dc9a4159d60b181b44354945b654a08f86868434bf00010000002a000000004916f98200000000795517c4000000001df2678e0000000052ccc3c658d63bd20016001498a16518484aa1f90e924b6d2443393f477bad9000000100e99636b8c1b912ea3ead7d98c8329a7bfa5fd1d39f8c49ae5899fcce188b94f1a51284e1cec6c359e81ba93a368764af5c1633e959a77bec2549669c6a9140b3bd1948d0ff13d297199f6d72a9972476cf92686f1fb2e24e49f9716a5f07dcf698c36824f8b01ba5bc62e9651ff836b742e4582ad44d129baafc3d9db053e202d40828be32a59f177da042e9e6a9b23aa737df386c6028b5aeb41444a1fe719e6f2e71eedac180fb3fdcadc28834f286adba403baa3e9c241acd2451cf82d84bd3c0da8a178de9150b6d94eae100e949d2e83de961841b453838ecd1f7e69382779be1c0369c0cbbe34a73190903bc2e2fb1d6fbc144e6b299109a8e26481896" - val staticRemoteKey = hex"0312f3b6afc20f21b77d8404dc9a4159d60b181b44354945b654a08f86868434bf00010000002a000000004916f98200000000795517c4000000001df2678e0000000052ccc3c658d63bd20016001498a16518484aa1f90e924b6d2443393f477bad90021b8b033c7bbb4473ca4e5554fa3c549f258061eb5d0612a2c9cfbc253b11a98c00000100e99636b8c1b912ea3ead7d98c8329a7bfa5fd1d39f8c49ae5899fcce188b94f1a51284e1cec6c359e81ba93a368764af5c1633e959a77bec2549669c6a9140b3bd1948d0ff13d297199f6d72a9972476cf92686f1fb2e24e49f9716a5f07dcf698c36824f8b01ba5bc62e9651ff836b742e4582ad44d129baafc3d9db053e202d40828be32a59f177da042e9e6a9b23aa737df386c6028b5aeb41444a1fe719e6f2e71eedac180fb3fdcadc28834f286adba403baa3e9c241acd2451cf82d84bd3c0da8a178de9150b6d94eae100e949d2e83de961841b453838ecd1f7e69382779be1c0369c0cbbe34a73190903bc2e2fb1d6fbc144e6b299109a8e26481896" - - require(localParamsCodec(ChannelVersion.ZEROES).decode(std.toBitVector).require.value.upfrontShutdownScript_opt.isDefined) - require(localParamsCodec(ChannelVersion.ANCHOR_OUTPUTS).decode(std.toBitVector).require.value.upfrontShutdownScript_opt.isDefined) - require(localParamsCodec(ChannelVersion.STATIC_REMOTEKEY).decode(staticRemoteKey.toBitVector).require.value.upfrontShutdownScript_opt.isDefined) - } - - test("decode remote params with global features (backward compat)") { - val withGlobalFeatures = hex"03c70c3b813815a8b79f41622b6f2c343fa24d94fb35fa7110bbb3d4d59cd9612e0000000059844cbc000000001b1524ea000000001503cbac000000006b75d3272e38777e029fa4e94066163024177311de7ba1befec2e48b473c387bbcee1484bf276a54460215e3dfb8e6f262222c5f343f5e38c5c9a43d2594c7f06dd7ac1a4326c665dd050347aba4d56d7007a7dcf03594423dccba9ed700d11e665d261594e1154203df31020d457ee336ba6eeb328d00f1b8bd8bfefb8a4dcd5af6db4c438b7ec5106c7edc0380df17e1beb0f238e51a39122ac4c6fb57f3c4f5b7bc9432f991b1ef4a8af3570002020000018a" - val withGlobalFeaturesDecoded = remoteParamsCodec.decode(withGlobalFeatures.bits).require.value - assert(withGlobalFeaturesDecoded.initFeatures.toByteVector == hex"028a") - } - - test("encode/decode htlc") { - val add = UpdateAddHtlc( - channelId = randomBytes32(), - id = Random.nextInt(Int.MaxValue), - amountMsat = MilliSatoshi(Random.nextInt(Int.MaxValue)), - cltvExpiry = CltvExpiry(Random.nextInt(Int.MaxValue)), - paymentHash = randomBytes32(), - onionRoutingPacket = TestConstants.emptyOnionPacket, - pathKey_opt = None, - confidence = 1.0, - fundingFee_opt = None) - val htlc1 = IncomingHtlc(add) - val htlc2 = OutgoingHtlc(add) - assert(htlcCodec.decodeValue(htlcCodec.encode(htlc1).require).require == htlc1) - assert(htlcCodec.decodeValue(htlcCodec.encode(htlc2).require).require == htlc2) - } - - test("encode/decode commitment spec") { - val add1 = UpdateAddHtlc( - channelId = randomBytes32(), - id = Random.nextInt(Int.MaxValue), - amountMsat = MilliSatoshi(Random.nextInt(Int.MaxValue)), - cltvExpiry = CltvExpiry(Random.nextInt(Int.MaxValue)), - paymentHash = randomBytes32(), - onionRoutingPacket = TestConstants.emptyOnionPacket, - pathKey_opt = None, - confidence = 1.0, - fundingFee_opt = None) - val add2 = UpdateAddHtlc( - channelId = randomBytes32(), - id = Random.nextInt(Int.MaxValue), - amountMsat = MilliSatoshi(Random.nextInt(Int.MaxValue)), - cltvExpiry = CltvExpiry(Random.nextInt(Int.MaxValue)), - paymentHash = randomBytes32(), - onionRoutingPacket = TestConstants.emptyOnionPacket, - pathKey_opt = None, - confidence = 1.0, - fundingFee_opt = None) - val htlc1 = IncomingHtlc(add1) - val htlc2 = OutgoingHtlc(add2) - val htlcs = Set[DirectedHtlc](htlc1, htlc2) - assert(setCodec(htlcCodec).decodeValue(setCodec(htlcCodec).encode(htlcs).require).require == htlcs) - val o = CommitmentSpec( - htlcs = Set(htlc1, htlc2), - commitTxFeerate = FeeratePerKw(Random.nextInt(Int.MaxValue).sat), - toLocal = MilliSatoshi(Random.nextInt(Int.MaxValue)), - toRemote = MilliSatoshi(Random.nextInt(Int.MaxValue)) - ) - val encoded = commitmentSpecCodec.encode(o).require - val decoded = commitmentSpecCodec.decode(encoded).require - assert(o == decoded.value) - } - - test("encode/decode origin") { - val replyTo = TestProbe("replyTo")(ActorSystem("system")).ref - - val localHot = Origin.Hot(replyTo, Upstream.Local(UUID.randomUUID())) - val localCold = Origin.Cold(localHot) - assert(originCodec.decodeValue(originCodec.encode(localHot).require).require == localCold) - assert(originCodec.decodeValue(originCodec.encode(localCold).require).require == localCold) - - val add = UpdateAddHtlc(randomBytes32(), 4324, 11000000 msat, randomBytes32(), CltvExpiry(400000), TestConstants.emptyOnionPacket, None, 1.0, None) - val relayedHot = Origin.Hot(replyTo, Upstream.Hot.Channel(add, TimestampMilli(0), randomKey().publicKey)) - val relayedCold = Origin.Cold(Upstream.Cold.Channel(add.channelId, add.id, add.amountMsat)) - assert(originCodec.decodeValue(originCodec.encode(relayedHot).require).require == relayedCold) - assert(originCodec.decodeValue(originCodec.encode(relayedCold).require).require == relayedCold) - - val adds = Seq( - UpdateAddHtlc(randomBytes32(), 1L, 1000 msat, randomBytes32(), CltvExpiry(400000), TestConstants.emptyOnionPacket, None, 1.0, None), - UpdateAddHtlc(randomBytes32(), 1L, 2000 msat, randomBytes32(), CltvExpiry(400000), TestConstants.emptyOnionPacket, None, 1.0, None), - UpdateAddHtlc(randomBytes32(), 2L, 3000 msat, randomBytes32(), CltvExpiry(400000), TestConstants.emptyOnionPacket, None, 1.0, None), - ) - val trampolineRelayedHot = Origin.Hot(replyTo, Upstream.Hot.Trampoline(adds.map(add => Upstream.Hot.Channel(add, TimestampMilli(0), randomKey().publicKey)).toList)) - // We didn't encode the incoming HTLC amount. - val trampolineRelayed = Origin.Cold(Upstream.Cold.Trampoline(adds.map(add => Upstream.Cold.Channel(add.channelId, add.id, 0 msat)).toList)) - assert(originCodec.decodeValue(originCodec.encode(trampolineRelayedHot).require).require == trampolineRelayed) - assert(originCodec.decodeValue(originCodec.encode(trampolineRelayed).require).require == trampolineRelayed) - } - - test("encode/decode map of origins") { - val map = Map( - 1L -> Origin.Cold(Upstream.Local(UUID.randomUUID())), - 42L -> Origin.Cold(Upstream.Cold.Channel(randomBytes32(), 4324, 12_000_000 msat)), - 43L -> Origin.Cold(Upstream.Cold.Trampoline(Upstream.Cold.Channel(randomBytes32(), 17, 0 msat) :: Upstream.Cold.Channel(randomBytes32(), 21, 0 msat) :: Upstream.Cold.Channel(randomBytes32(), 21, 0 msat) :: Nil)), - 130L -> Origin.Cold(Upstream.Cold.Channel(randomBytes32(), -45, 13_000_000 msat)), - 140L -> Origin.Cold(Upstream.Cold.Trampoline(Upstream.Cold.Channel(randomBytes32(), 0, 0 msat) :: Nil)), - 1000L -> Origin.Cold(Upstream.Cold.Channel(randomBytes32(), 10, 14_000_000 msat)), - -32L -> Origin.Cold(Upstream.Cold.Channel(randomBytes32(), 54, 15_000_000 msat)), - -54L -> Origin.Cold(Upstream.Cold.Trampoline(Upstream.Cold.Channel(randomBytes32(), 1, 0 msat) :: Upstream.Cold.Channel(randomBytes32(), 2, 0 msat) :: Nil)), - -4L -> Origin.Cold(Upstream.Local(UUID.randomUUID())) - ) - assert(originsMapCodec.decodeValue(originsMapCodec.encode(map).require).require == map) - } - - test("encode/decode map of spending txes") { - val map = Map( - OutPoint(randomTxId(), 42) -> randomTxId(), - OutPoint(randomTxId(), 14502) -> randomTxId(), - OutPoint(randomTxId(), 0) -> randomTxId(), - OutPoint(randomTxId(), 454513) -> randomTxId() - ) - assert(spentMapCodec.decodeValue(spentMapCodec.encode(map).require).require == map) - } - -} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version2/ChannelCodecs2Spec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version2/ChannelCodecs2Spec.scala deleted file mode 100644 index 3aa76a4714..0000000000 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version2/ChannelCodecs2Spec.scala +++ /dev/null @@ -1,70 +0,0 @@ -package fr.acinq.eclair.wire.internal.channel.version2 - -import fr.acinq.bitcoin.scalacompat.{ByteVector64, OutPoint, Transaction} -import fr.acinq.eclair.Features -import fr.acinq.eclair.TestUtils.randomTxId -import fr.acinq.eclair.channel.{ChannelConfig, ChannelDataWithCommitments, ChannelFeatures, HtlcTxAndRemoteSig} -import fr.acinq.eclair.wire.internal.channel.version2.ChannelCodecs2.Codecs._ -import fr.acinq.eclair.wire.internal.channel.version2.ChannelCodecs2.channelDataCodec -import org.scalatest.funsuite.AnyFunSuite -import scodec.bits.HexStringSyntax - -class ChannelCodecs2Spec extends AnyFunSuite { - - // Data from a channel in NORMAL state using codecs version 2. - val dataNormal = hex"00020000000103af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d000091b1206959fd8fcd024797f81605e598b4a08e2b4bf103197ec17b79f1c4c2c7680000001000000000000044c000000001dcd65000000000000002710000000000000000000900064ff1600140097be5a335f9c489d4ef63d4af8710b9ee8ec760000186b0200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000024982039dc0e0b1d25905e44fdf6f8e89755a5e219685840d0bc1d28d3308f9628a358500000000000003e8ffffffffffffffff0000000000004e2000000000000003e80090001e02790de7416ff75ad5028f42e81819043dd007565aa21267fba62a35615b7f2a300336c13e3b3294c1c80e41c1ef3f7d976ed8c85cb262807bb8697db3ff05240e4d0342f01378b499ef16b888617838635230d9f60a37efcd1fe5f7daa80b10d455100348e3aa6f37ab809ae57d0739ca31bbb001ac1ffac363f75d7ed33a2d142a28b302281a45ca766c3b34937985d3d539d8cfaae58a87ce94dc230708aea0c260ca9800000003024982000000000000000001000200fd05aa7997c9c34f9ff781e6eeb6a27867a89d63ac7da76c5dbb175c092f74803ebd3900000000000000000000000011e1a300b5982a1ddd30d66654d27234f7f31921ee7011ed6290dd92043a50d6e1ab08b200061b1000038ec7647194925944599ae67df7bcf07dbf3453f249a07b3aaf4c61bb038be497080cf1e3f2854f1fbfcf5ca6be6284e402c89ba5bfc717e2d0c6fa1223be4277809ca9a2b2e625e83e06c561d7004a9725d3dc8c7e24f371592ab0954c020d1813984f96e70f39f057b404300f09487fb21cfa815dd8b5ceaf171155af7ea9a14e0711109f26fc854e7b2750c394f55c47193fd72f810c063a8506bc494403b52fdd14fa18005009b8f79c520f530c9fc2c9ac5f1f4f4926e3e671dd336d233c59878c2d04f309a118b9a71424d681a4d781df60cf89f3ca16aee95116a0a9445730b4ed1708ff296023c5c0f7a6e17d6b56b3916a1a32937f550680c6121e74c3a67717c98a5f97c3fcac8054a642476460678ed09b75da6d0a3825b8844b4babd3d29e8fdd09764f0415e95811b564a708e8b44084f336af96ad690f0207060234fe649a3b4f7c835fa391261ff13551079261dc5ad9ab29e3ded1561f1f8b5ea07c02b084a0c80ee49a4b137ebf201bef8ae39b74d20e39eb414be67dbad1b56d5b774a4897cb8a9703e76b69a8f64d154148e0e161f9b32f038cb1bff774dd1cbe5bc20a119cef8a1140960ced2681c9c03a4f45c08be8d553b986ec9a2bc4fa1e5ced2d40806ce87958145cf54fce170dcdc71902eede20cc7ac29235696a49c762b7ecaa2fd92e1e40cb2b951b67e6d076f7de966220571d9068c14153e9fc20ea078a81443114a2817bffe4d8b52b20476e09ece02185ebf1a35dad53680bda5c6e72cc78f35b0ac0758f0929800f1a4f7d2d8c3dee7eb43fd013dd08c2400be7a5b8431433423f85557f6dfbe81906fc1e5d365cf98ffa21af3bb8ce793a4a8531fe46978f6442a802e4c86967923e1466a40574e3f85e27c34267f31edeffca7e3309e810ba887afb09d17d2e6d6e2a806466767c16b5237bb49a84f9a2bd6cecfdfffca5059a78259323ab1b368639dc796e30bea42f942b68b6baf055398cdba211cc3af4e29a0648829cfd8cdfeb35cefef1e141da79a1bff107dbb6dc717fd9dce1b3c925cedcbf8f2cbf5f25d8fb8c301275ba70706395698ccd158eb6cff35343c60934bb6ee48b9edd2d3ff7625334954b61435f545d4ffddbf6264bc798857b244c4b8928d4967e0da4cca8077af0766e6e99975afe993d37f83363996df9d75cb59440a2c9411aa1e044f3b1739c7b59cae26984bdb49c661684705f38c363d0ee39e4ed2a297a1025a9cc1e972ec7963c1be54bb5e847feda1b14bbe118d617a1a41f1251c3ec8b2f8ddc7cad3a4d8a1d45af14e1c0c2a1bbcabae9df671a399894890bc49080ea75c9c4068bb89fb0eec586389efc0fff11704b69fd10bca02c06f764844334e58abba3b554f64c4e4e26529042a2a1ae9965846816d1e8a411fee4d27ce3fcb1776d40526aa1f83dfb583fa5e2e06c46197c5032c2c48c3f0e0136e601ca5f8ce508441ff2ddc7e46f7c58811071669c00bed4008e8c0dbd7ed2fe00e9e0a656aa22acd22485fba08efb3065a099fd6769a520a45a8f778f5dfdfef959a26d8e6b8d45df8e3641de245a83b0ed1a312159b728333ab457b72ffc2cec97fd6ce29275e754b7ac8aa122be7db85d7d0c3d7a67dc82a406301fd79936f4ff3e2ee491de4756144868304d1afed8df91580eeecb1af2446c035b2debb6ddf3f6a19fccfcbdf5d73df3420df8153d8a8307e37969a52978e673a0fc003d6dad6ba8269349ef1b8dfd61da4c8568f52033c352b0422b3d8298ddccc684708aa5797b32bce5b97236b7002c158b8c878b1bfa0c34b54636c295f71e2f281f19d5c6cc796a6c06cc56d9490a41911f279be3cf73de1dd214e1d344c26525456c4493dd28096c518d4a2aac87570145f577059c7f371e53df6d2cd90e7b75720a5da71bd29d264d00fd05aa7997c9c34f9ff781e6eeb6a27867a89d63ac7da76c5dbb175c092f74803ebd390000000000000001000000000bebc200382fbabf52c177971225c0ea45319a772f6be42a8b099c818b1ce88bba017f8900061b1000027c752ef61af1ec61f41ba11ffb40f4fb6253469db722f57a38ca88f2acd98a7d486eb77ae94a3b80a5e37240879dd708a56d6bae9293af899fc288ab04207db8318ef957fb928c1308fe0be1f3b4660a4f06e61735dc598270d6e6500adff32599467596b83332e7a10ce38b42c1f0d04a4d1e12ad0ffaaef319de97c6d2ddad04f0e47f952c5bb924e6635aa696878ba4153afe362e9ac5f707b6d944033a9bafef4e4340af88c9da8ce0b922a9b2094092320c912afd20f57e456e17c52ec3acae731bddb200b669f69b557fe50e2842d9f68add54879b4f7f132c8882d2829389966ff98e9ad69c9472c4af68b765f63a5104fb04f0cecae0c8c1915015926eb47a7afe36854f8406a03b6f7be35528a2b6e01c546a95342767e092f0ee0956cd2268322428486364831d8a3ae8d1fc5cfe4125e91886a16b5fc6a563df8a8dea688f677c8b18fe030938fced39cdf6576d1a4017aa01aa13bde00b2cfda58f388a5b8f135c18c53e527e70fe9c2900db541cbaeea6b48437a05828865d34c9a9a05c4660e3d1e9f6c3852d646dcf270a945cfebc82d83cdbc80c59ae5d69913118007b71183eb3e13fd9161e19ed2f50b5f91d60968b2497fd67849bce826a5fb2cd9de5fb5e04df52d2a3f8cd7e4472533bf4669435efe0015d1d6d7492dea0f76330859ca32d71c38c388ac861f02f1e77b874f7db43fa444408b51787f1f92ed6a4d96b35ab75fd237c19fc4dd1df39d6264372d6b1efaf684776aaf98c7c832010e4473069e3049a8122d2701b61f0968dfece018a2efad783d8842ff94c828e8a7b3444f92a00d27e8731dee02b9f9b5edaa919a1265bd48518e709e34282cb94b6a635c6b482956eb46516361096e79255592cd0203a2141e29366e22d9259c60c9273e7a2c9e95d4b69507c57b91b465be0c1304021f8a2cee0d25c587ba82707b4fadff569d74bbdff5251d3d8872f2769e27de692c0de4993fc5d6fdffb6dba82603d5042edbc44c784a2287b48a359c992486350ca6a3ead2a4dd9e82f226e266ca114d7a0b0188061672850e16c5c886b3e5d34d991ae880a2193d26408ab68dae073a17e18cfee9f0e08fca3fb9f48d949e4e4cafd221736c38db13091dbcd16c9c9bb89d731b65f2b970323be4c0fb34a44fb3c622e6ab39a7f4c8aaf3512729aee55c9ba5a5de3f1ea6fb06afd96d3c99378390fae7a553ad832fa44d9470a6f4569bb86dcfc2f10bcbc6459ad9f97c174c559a416906a258197d20d82c550a5a0e79beedf230539daa5cafa782c9890cd040ccaaf68d7ec7a44f319e2375486766a63265cab324c4d56eef1172d0116d9802a0aad40eafd31ea924b11b5c9aeb43b38e73de8c5accbb3eacd320eca9483809d73b898f25c798047522344bbb210d6c7eadff0327b4c09c605b7f9f80b6e3cf0f8a675233ddbfc7de0332b6e8174a271f9a3cb8f0acb23b30e345cba48f8b59131aa4b57de6b7d12e8c275745ad6376c6f871689aaad7930c8f1d7c2024d0d73b308842fe826c6d6cdf1d3a05b3776e03a2963d8da9ef5f6bc15d5b9c9f67d3695f3b1a5748c96cc60ec8b2dabac816a400dfc917dddf2846c3f0855367bf6487408bf442a2652ba17011a71708356ad05a55600adccd07467287b6820546caf6e01274ac98617107a2752d5da76ce2ebe50e0b4345186faed59708d4524a8e8d07a8ead3d2d502cef7c32024b06e62defd69053a67aa9482453b22b445f3bae0e6550ff758c0da8c404a5a8cd860cd8cae21fa7731d96fb53c6aba0526c65d370fb0aec1e917ab4e1020a8d22c2a87e121305de81c3a754e415b8843eb7574a0b825807260f192c1af37bcc099269cba32aa72a9bbd5d33764b3a3470573987afea776b409d15a7dc9ee64c464e2038eebd75584fafa9bb000027100000000011e1a300000000000bebc200247997c9c34f9ff781e6eeb6a27867a89d63ac7da76c5dbb175c092f74803ebd39000000002b40420f0000000000220020aa081f2d59ff746a00257804e21092927e75f7530ad6c2cf00008e3359e18a6547522102790de7416ff75ad5028f42e81819043dd007565aa21267fba62a35615b7f2a3021029dc09099a99c869d47de24d4d9dc7aab0311496b0941421aa02baaf67c18a52b52aefd01af020000000001017997c9c34f9ff781e6eeb6a27867a89d63ac7da76c5dbb175c092f74803ebd3900000000000bcafd8004400d0300000000001600149a9b19387a6ad09813cbb83ef4f454291e70be49400d03000000000022002080f0c5ff9a0048c10092f2a37e9bca6940c5b75bf002d184ce79acc84e80c338286a04000000000022002098c0086284e401aca234212eddcc5f0438129b1db646935ef88be6436074c723e093040000000000220020bf6f1c0351aeb2a95ec7013f5adfcad06eecf6ec490bb9413b82c7cd032976600400473044022026b8117ceb8c1bfafd5f48f693244a486a8b8353757273840aba9360b1e63895022015823314a8315b2a5b56028c27dc5e6576e53616c303f49e5e1a7896b988b5d9014730440220312f27ed593dab7e9a8cc3dadbd54ae149f0b08d6b073f7a115a0f445b163b530220394be760728211085d5a97ddf7d4782c124592427abb6496b6f90ec9ff89b92d0147522102790de7416ff75ad5028f42e81819043dd007565aa21267fba62a35615b7f2a3021029dc09099a99c869d47de24d4d9dc7aab0311496b0941421aa02baaf67c18a52b52aed0b5f5200002022418f81a9801c2ef879bc892123c0d54874a35440132fe0094b1c7c264352f875a010000002b400d03000000000022002080f0c5ff9a0048c10092f2a37e9bca6940c5b75bf002d184ce79acc84e80c3388576a914861c563a4226e265ed1ca742307c7ace9444cc4e8763ac672103d9170264d4e477451c4acad19a32b905b7a6cddc24b40d10de12c7b2ceb59dec7c820120876475527c2102cbd109cb3882917fda2a8bb8c85db26efec6c3d3f078e86ca322b6067b0ffa9152ae67a914665582422423b9d14aca94b0420fba776a5d9a0888ac68685e020000000118f81a9801c2ef879bc892123c0d54874a35440132fe0094b1c7c264352f875a010000000000000000015af302000000000022002098c0086284e401aca234212eddcc5f0438129b1db646935ef88be6436074c723101b0600000000000000000140d70567f99e57bdc912717bd2f3574eb97007982dfd00f23e2ad65c0b936bc04277f388f63fa76b6e76088ed4ff9078cb19912aa7d4b6eaf193c2e55e051077124031b18ab87d66ccc4f03303805a4852a8dce66fee01f9e8f7863a8a0d84d676ce5b020800e520197afc71836b36fdbd3e67747d93229908fdfd8cd28f120d0fa4022418f81a9801c2ef879bc892123c0d54874a35440132fe0094b1c7c264352f875a030000002be093040000000000220020bf6f1c0351aeb2a95ec7013f5adfcad06eecf6ec490bb9413b82c7cd032976608576a914861c563a4226e265ed1ca742307c7ace9444cc4e8763ac672103d9170264d4e477451c4acad19a32b905b7a6cddc24b40d10de12c7b2ceb59dec7c820120876475527c2102cbd109cb3882917fda2a8bb8c85db26efec6c3d3f078e86ca322b6067b0ffa9152ae67a914f39c581de7435ed6fd334295d4bd4892c86127a988ac68685e020000000118f81a9801c2ef879bc892123c0d54874a35440132fe0094b1c7c264352f875a03000000000000000001fa7904000000000022002098c0086284e401aca234212eddcc5f0438129b1db646935ef88be6436074c723101b060000000000000000004089a4e5cecdfc3c25f4e4210f1a37d58a6b977c71afb91bac5e727f632a6d4a2330c71587d91c98fdad3a9b662409e7c75cf4c0d8ad180c342d4d3fb761fef31e403e925f9d57997647d41647a1f3d28024a902d175425c5d76e00925d13da452115255a02ca2164a58d2863583cff636006f1e5fe1c8273def053cbaaa5a8059d100000000000000010002fffd05aa7997c9c34f9ff781e6eeb6a27867a89d63ac7da76c5dbb175c092f74803ebd3900000000000000000000000011e1a300b5982a1ddd30d66654d27234f7f31921ee7011ed6290dd92043a50d6e1ab08b200061b1000038ec7647194925944599ae67df7bcf07dbf3453f249a07b3aaf4c61bb038be497080cf1e3f2854f1fbfcf5ca6be6284e402c89ba5bfc717e2d0c6fa1223be4277809ca9a2b2e625e83e06c561d7004a9725d3dc8c7e24f371592ab0954c020d1813984f96e70f39f057b404300f09487fb21cfa815dd8b5ceaf171155af7ea9a14e0711109f26fc854e7b2750c394f55c47193fd72f810c063a8506bc494403b52fdd14fa18005009b8f79c520f530c9fc2c9ac5f1f4f4926e3e671dd336d233c59878c2d04f309a118b9a71424d681a4d781df60cf89f3ca16aee95116a0a9445730b4ed1708ff296023c5c0f7a6e17d6b56b3916a1a32937f550680c6121e74c3a67717c98a5f97c3fcac8054a642476460678ed09b75da6d0a3825b8844b4babd3d29e8fdd09764f0415e95811b564a708e8b44084f336af96ad690f0207060234fe649a3b4f7c835fa391261ff13551079261dc5ad9ab29e3ded1561f1f8b5ea07c02b084a0c80ee49a4b137ebf201bef8ae39b74d20e39eb414be67dbad1b56d5b774a4897cb8a9703e76b69a8f64d154148e0e161f9b32f038cb1bff774dd1cbe5bc20a119cef8a1140960ced2681c9c03a4f45c08be8d553b986ec9a2bc4fa1e5ced2d40806ce87958145cf54fce170dcdc71902eede20cc7ac29235696a49c762b7ecaa2fd92e1e40cb2b951b67e6d076f7de966220571d9068c14153e9fc20ea078a81443114a2817bffe4d8b52b20476e09ece02185ebf1a35dad53680bda5c6e72cc78f35b0ac0758f0929800f1a4f7d2d8c3dee7eb43fd013dd08c2400be7a5b8431433423f85557f6dfbe81906fc1e5d365cf98ffa21af3bb8ce793a4a8531fe46978f6442a802e4c86967923e1466a40574e3f85e27c34267f31edeffca7e3309e810ba887afb09d17d2e6d6e2a806466767c16b5237bb49a84f9a2bd6cecfdfffca5059a78259323ab1b368639dc796e30bea42f942b68b6baf055398cdba211cc3af4e29a0648829cfd8cdfeb35cefef1e141da79a1bff107dbb6dc717fd9dce1b3c925cedcbf8f2cbf5f25d8fb8c301275ba70706395698ccd158eb6cff35343c60934bb6ee48b9edd2d3ff7625334954b61435f545d4ffddbf6264bc798857b244c4b8928d4967e0da4cca8077af0766e6e99975afe993d37f83363996df9d75cb59440a2c9411aa1e044f3b1739c7b59cae26984bdb49c661684705f38c363d0ee39e4ed2a297a1025a9cc1e972ec7963c1be54bb5e847feda1b14bbe118d617a1a41f1251c3ec8b2f8ddc7cad3a4d8a1d45af14e1c0c2a1bbcabae9df671a399894890bc49080ea75c9c4068bb89fb0eec586389efc0fff11704b69fd10bca02c06f764844334e58abba3b554f64c4e4e26529042a2a1ae9965846816d1e8a411fee4d27ce3fcb1776d40526aa1f83dfb583fa5e2e06c46197c5032c2c48c3f0e0136e601ca5f8ce508441ff2ddc7e46f7c58811071669c00bed4008e8c0dbd7ed2fe00e9e0a656aa22acd22485fba08efb3065a099fd6769a520a45a8f778f5dfdfef959a26d8e6b8d45df8e3641de245a83b0ed1a312159b728333ab457b72ffc2cec97fd6ce29275e754b7ac8aa122be7db85d7d0c3d7a67dc82a406301fd79936f4ff3e2ee491de4756144868304d1afed8df91580eeecb1af2446c035b2debb6ddf3f6a19fccfcbdf5d73df3420df8153d8a8307e37969a52978e673a0fc003d6dad6ba8269349ef1b8dfd61da4c8568f52033c352b0422b3d8298ddccc684708aa5797b32bce5b97236b7002c158b8c878b1bfa0c34b54636c295f71e2f281f19d5c6cc796a6c06cc56d9490a41911f279be3cf73de1dd214e1d344c26525456c4493dd28096c518d4a2aac87570145f577059c7f371e53df6d2cd90e7b75720a5da71bd29d264dfffd05aa7997c9c34f9ff781e6eeb6a27867a89d63ac7da76c5dbb175c092f74803ebd390000000000000001000000000bebc200382fbabf52c177971225c0ea45319a772f6be42a8b099c818b1ce88bba017f8900061b1000027c752ef61af1ec61f41ba11ffb40f4fb6253469db722f57a38ca88f2acd98a7d486eb77ae94a3b80a5e37240879dd708a56d6bae9293af899fc288ab04207db8318ef957fb928c1308fe0be1f3b4660a4f06e61735dc598270d6e6500adff32599467596b83332e7a10ce38b42c1f0d04a4d1e12ad0ffaaef319de97c6d2ddad04f0e47f952c5bb924e6635aa696878ba4153afe362e9ac5f707b6d944033a9bafef4e4340af88c9da8ce0b922a9b2094092320c912afd20f57e456e17c52ec3acae731bddb200b669f69b557fe50e2842d9f68add54879b4f7f132c8882d2829389966ff98e9ad69c9472c4af68b765f63a5104fb04f0cecae0c8c1915015926eb47a7afe36854f8406a03b6f7be35528a2b6e01c546a95342767e092f0ee0956cd2268322428486364831d8a3ae8d1fc5cfe4125e91886a16b5fc6a563df8a8dea688f677c8b18fe030938fced39cdf6576d1a4017aa01aa13bde00b2cfda58f388a5b8f135c18c53e527e70fe9c2900db541cbaeea6b48437a05828865d34c9a9a05c4660e3d1e9f6c3852d646dcf270a945cfebc82d83cdbc80c59ae5d69913118007b71183eb3e13fd9161e19ed2f50b5f91d60968b2497fd67849bce826a5fb2cd9de5fb5e04df52d2a3f8cd7e4472533bf4669435efe0015d1d6d7492dea0f76330859ca32d71c38c388ac861f02f1e77b874f7db43fa444408b51787f1f92ed6a4d96b35ab75fd237c19fc4dd1df39d6264372d6b1efaf684776aaf98c7c832010e4473069e3049a8122d2701b61f0968dfece018a2efad783d8842ff94c828e8a7b3444f92a00d27e8731dee02b9f9b5edaa919a1265bd48518e709e34282cb94b6a635c6b482956eb46516361096e79255592cd0203a2141e29366e22d9259c60c9273e7a2c9e95d4b69507c57b91b465be0c1304021f8a2cee0d25c587ba82707b4fadff569d74bbdff5251d3d8872f2769e27de692c0de4993fc5d6fdffb6dba82603d5042edbc44c784a2287b48a359c992486350ca6a3ead2a4dd9e82f226e266ca114d7a0b0188061672850e16c5c886b3e5d34d991ae880a2193d26408ab68dae073a17e18cfee9f0e08fca3fb9f48d949e4e4cafd221736c38db13091dbcd16c9c9bb89d731b65f2b970323be4c0fb34a44fb3c622e6ab39a7f4c8aaf3512729aee55c9ba5a5de3f1ea6fb06afd96d3c99378390fae7a553ad832fa44d9470a6f4569bb86dcfc2f10bcbc6459ad9f97c174c559a416906a258197d20d82c550a5a0e79beedf230539daa5cafa782c9890cd040ccaaf68d7ec7a44f319e2375486766a63265cab324c4d56eef1172d0116d9802a0aad40eafd31ea924b11b5c9aeb43b38e73de8c5accbb3eacd320eca9483809d73b898f25c798047522344bbb210d6c7eadff0327b4c09c605b7f9f80b6e3cf0f8a675233ddbfc7de0332b6e8174a271f9a3cb8f0acb23b30e345cba48f8b59131aa4b57de6b7d12e8c275745ad6376c6f871689aaad7930c8f1d7c2024d0d73b308842fe826c6d6cdf1d3a05b3776e03a2963d8da9ef5f6bc15d5b9c9f67d3695f3b1a5748c96cc60ec8b2dabac816a400dfc917dddf2846c3f0855367bf6487408bf442a2652ba17011a71708356ad05a55600adccd07467287b6820546caf6e01274ac98617107a2752d5da76ce2ebe50e0b4345186faed59708d4524a8e8d07a8ead3d2d502cef7c32024b06e62defd69053a67aa9482453b22b445f3bae0e6550ff758c0da8c404a5a8cd860cd8cae21fa7731d96fb53c6aba0526c65d370fb0aec1e917ab4e1020a8d22c2a87e121305de81c3a754e415b8843eb7574a0b825807260f192c1af37bcc099269cba32aa72a9bbd5d33764b3a3470573987afea776b409d15a7dc9ee64c464e2038eebd75584fafa9bb00002710000000000bebc2000000000011e1a300e88c922c64297e36d03de41bfe52a7d1d8898ef4ee5152fd79e112d217ff20ca0378f690d4be831f1af3ad558fbb2ebf72976262657a31f34b6c5a654ee07de5b800000000000000000000000000000000000000020000000000000000000200000000000000000003c1a3d97b25374e3ca3aba318297d0e7800000000000000010003519b0c1acfcd44e98d240aae9f6d575eff03c16438e110e8f196905ccc0feee52086773b5b63815a4b8524db2c4bc59005d3247997c9c34f9ff781e6eeb6a27867a89d63ac7da76c5dbb175c092f74803ebd39000000002b40420f0000000000220020aa081f2d59ff746a00257804e21092927e75f7530ad6c2cf00008e3359e18a6547522102790de7416ff75ad5028f42e81819043dd007565aa21267fba62a35615b7f2a3021029dc09099a99c869d47de24d4d9dc7aab0311496b0941421aa02baaf67c18a52b52ae000100400000ffffffffffff0020ab16d22ee6ffe5e30b4f77d4cb61282e8e2489fe2fbf13ca6c4d3a5263b91ae780007fffffffffff807997c9c34f9ff781e6eeb6a27867a89d63ac7da76c5dbb175c092f74803ebd39061a8000002a00000000884bf9f9a48c2cb42b70045b55dfab8c1e14a1538a7b504810ee4973f274aa673e049baa1586f2acf25a831494d401d05af8cdf57c570a75ed38946568d1bdcda306226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f061a8000002a000060db51bf0101009000000000000003e8000854d00000000a000000003b9aca000000" - - test("encode/decode map of spending txs") { - val map = Map( - OutPoint(randomTxId(), 42) -> Transaction.read("020000000001018154ecccf11a5fb56c39654c4deb4d2296f83c69268280b94d021370c94e219701000000000000000001d0070000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100d5275b3619953cb0c3b5aa577f04bc512380e60fa551762ce3d7a1bb7401cff9022037237ab0dac3fe100cde094e82e2bed9ba0ed1bb40154b48e56aa70f259e608b01483045022100c89172099507ff50f4c925e6c5150e871fb6e83dd73ff9fbb72f6ce829a9633f02203a63821d9162e99f9be712a68f9e589483994feae2661e4546cd5b6cec007be501008576a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a914b43e1b38138a41b37f7cd9a1d274bc63e3a9b5d188ac6868f6010000"), - OutPoint(randomTxId(), 14502) -> Transaction.read("02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b80094a010000000000002200202b1b5854183c12d3316565972c4668929d314d81c5dcdbb21cb45fe8a9a8114f4a01000000000000220020e9e86e4823faa62e222ebc858a226636856158f07e69898da3b0d1af0ddb3994e80300000000000022002010f88bf09e56f14fb4543fd26e47b0db50ea5de9cf3fc46434792471082621aed0070000000000002200203e68115ae0b15b8de75b6c6bc9af5ac9f01391544e0870dae443a1e8fe7837ead007000000000000220020fe0598d74fee2205cc3672e6e6647706b4f3099713b4661b62482c3addd04a5eb80b000000000000220020f96d0334feb64a4f40eb272031d07afcb038db56aa57446d60308c9f8ccadef9a00f000000000000220020ce6e751274836ff59622a0d1e07f8831d80bd6730bd48581398bfadd2bb8da9ac0c62d0000000000220020f3394e1e619b0eca1f91be2fb5ab4dfc59ba5b84ebe014ad1d43a564d012994a4c9e6a00000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0400483045022100cf8f902751006923e4062f5dbf55f8475bef08f4b5ac060d219bbff6c1a4431b02206006c515754ffc1f4f263004f61082e1fe4241449629da9096b0679e7e30972201473044022076a51aed1bd085487a7023f2ca8a87544a60a5b7277805b614b6ff7d36f1a44c02207ffac246b6572f3b4c9a7867ffa97c203500eebbf14659df78cfa0fadea22a6401475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220"), - OutPoint(randomTxId(), 0) -> Transaction.read("02000000000101b8cefef62ea66f5178b9361b2371be0759cbc8c689bcfa7a8e6746d497ec221a040000000001000000010a060000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e050047304402203c3a699fb80a38112aafd73d6e3a9b7d40bc2c3ed8b7fbc182a20f43b215172202204e71821b984d1af52c4b8e2cd4c572578c12a965866130c2345f61e4c2d3fef48347304402205bcfa92f83c69289a412b0b6dd4f2a0fe0b0fc2d45bd74706e963257a09ea24902203783e47883e60b86240e877fcbf33d50b1742f65bc93b3162d1be26583b367ee012001010101010101010101010101010101010101010101010101010101010101018d76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a9144b6b2e5444c2639cc0fb7bcea5afba3f3cdce23988527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f501b175ac6851b2756800000000"), - OutPoint(randomTxId(), 454513) -> Transaction.read("02000000000101ab84ff284f162cfbfef241f853b47d4368d171f9e2a1445160cd591c4c7d882b00000000000000000001e8030000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100d9e29616b8f3959f1d3d7f7ce893ffedcdc407717d0de8e37d808c91d3a7c50d022078c3033f6d00095c8720a4bc943c1b45727818c082e4e3ddbc6d3116435b624b014730440220636de5682ef0c5b61f124ec74e8aa2461a69777521d6998295dcea36bc3338110220165285594b23c50b28b82df200234566628a27bcd17f7f14404bd865354eb3ce012000000000000000000000000000000000000000000000000000000000000000008a76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a914b8bcb07f6344b42ab04250c86a6e8b75d3fdbbc688527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f401b175ac686800000000") - ) - assert(spentMapCodec.decodeValue(spentMapCodec.encode(map).require).require == map) - } - - test("remove signatures from commitment txs") { - val commitments = channelDataCodec.decode(dataNormal.bits).require.value.asInstanceOf[ChannelDataWithCommitments].commitments.latest - commitments.localCommit.commitTxAndRemoteSig.commitTx.tx.txIn.foreach(txIn => assert(txIn.witness.isNull)) - assert(commitments.localCommit.htlcTxsAndRemoteSigs.nonEmpty) - commitments.localCommit.htlcTxsAndRemoteSigs.foreach { - case HtlcTxAndRemoteSig(htlcTx, remoteSig) => - assert(remoteSig !== ByteVector64.Zeroes) - htlcTx.tx.txIn.foreach(txIn => assert(txIn.witness.isNull)) - } - } - - test("split channel version into channel config and channel features") { - { - // Standard channel - val commitments = channelDataCodec.decode(dataNormal.bits).require.value.asInstanceOf[ChannelDataWithCommitments].commitments - assert(commitments.params.channelConfig == ChannelConfig.standard) - assert(commitments.params.channelFeatures == ChannelFeatures()) - } - { - val staticRemoteKeyChannel = hex"00020000000303af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d000090ef5e61dc12e5215dfcf8a1263f66b998a162fd80ea9aabf4fd1483d63fcd68280000001000000000000044c0000000008f0d1800000000000002710000000000000000000900064ff160014170e6de64a6d55c73f3ff657ef441d43739837ab028feba10d0eafd0fad8fe20e6d9206e6bd30242826de05c63f459a00aced24b120000186b0200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000026982039dc0e0b1d25905e44fdf6f8e89755a5e219685840d0bc1d28d3308f9628a358500000000000003e8ffffffffffffffff0000000000004e2000000000000003e80090001e023da0b6ec447fbda75506f743247a7fb6f8e29c1e18cfff53f2f9136801c617a7022dc9905a27397dedf60ef915579f2464c8fa930fae14adb6563e5559905067e5028feba10d0eafd0fad8fe20e6d9206e6bd30242826de05c63f459a00aced24b12032ed22e583b835accc8b4b5bfc94331ce06b07c31952551fd9520e126b1ed43c703da7493b310a19286c452e3d1f8ee66afaf6f1656736f6464d00deedd0b58585b00000003026982000000000000000000000000002710000000002faf0800000000000bebc20024f452ff0b3c6f363530f2a203e8fd6fcd88d177a2dac14b4b045abead090e4f4f000000002b40420f000000000022002065adbae4db91fa9069e2c8dce1f716c65e6dbc302229772d62305bd2e0e6494e475221023da0b6ec447fbda75506f743247a7fb6f8e29c1e18cfff53f2f9136801c617a72103a62130179b4cb0451ddbcd4480c326d5343a691bc01e07bb6eee35fc91a7601752aefd015b02000000000101f452ff0b3c6f363530f2a203e8fd6fcd88d177a2dac14b4b045abead090e4f4f000000000036a2ab8002400d030000000000160014761879f7b274ce995f87150a02e75cc0c037e8e3b8180c00000000002200204b354f6e432cae820a572c8294423f4bc6a5b9a2509d3de22b93c6316b59e14b0400483045022100fe9a0106b51293216f200a5a0b17a783f7ace4edbd71b644dee3e25c2688834802203a1087649b2cba80f5a634c1c67fbce4355f6c5605dd873f12d0399a542e363d01483045022100e71334e385755ab65b7ac6be2709a0be39f98fde6a6c10a895fd6f90ecd3fd7d022036550ecd40ed942f0a4ae3d0e29b6142a3cc4492eb73c3a62ced6d934e48584d01475221023da0b6ec447fbda75506f743247a7fb6f8e29c1e18cfff53f2f9136801c617a72103a62130179b4cb0451ddbcd4480c326d5343a691bc01e07bb6eee35fc91a7601752aeac67892000000000000000000000000000002710000000000bebc200000000002faf080018fa9f3051c52bba85fc20763e8835aed58c763f52a1647c90cc850aeb62ac3d02eada0cd5618069ed848f3975a631f02c87beb9968df8bd99511fa08f75d64529000000000000000000000000000000000000000000000000000000000000ff03b960d87d264cc2c99f71ed6ba6dd1bdb06c03666f76d90a1670a73dc468428e924f452ff0b3c6f363530f2a203e8fd6fcd88d177a2dac14b4b045abead090e4f4f000000002b40420f000000000022002065adbae4db91fa9069e2c8dce1f716c65e6dbc302229772d62305bd2e0e6494e475221023da0b6ec447fbda75506f743247a7fb6f8e29c1e18cfff53f2f9136801c617a72103a62130179b4cb0451ddbcd4480c326d5343a691bc01e07bb6eee35fc91a7601752ae000000f452ff0b3c6f363530f2a203e8fd6fcd88d177a2dac14b4b045abead090e4f4f061a8000002a00000000884a5e841be07f8612321ece5e40b39d715ffe94978c06cf3bb3af1a0cadcefc4b4aaade3deb191662f00b304edeae18ae8c80ed3dc505e8c78876b6fb61682fcb06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f061a8000002a0000629dbe540101009000000000000003e8000854d00000000a000000003b9aca000000" - val commitments = channelDataCodec.decode(staticRemoteKeyChannel.bits).require.value.asInstanceOf[ChannelDataWithCommitments].commitments - assert(commitments.params.channelConfig == ChannelConfig.standard) - assert(commitments.params.channelFeatures == ChannelFeatures(Features.StaticRemoteKey)) - } - { - val anchorOutputsChannel = hex"00020000000703af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d000094b82816a3f15ab3e324ddf6f0f5a116cef8c3200a353240db684337d2dcaa81e80000001000000000000044c000000001dcd65000000000000002710000000000000000000900064ff160014f09f6f93de60c6af417ed31d52c2c883b6113f260000186b0200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000225982039dc0e0b1d25905e44fdf6f8e89755a5e219685840d0bc1d28d3308f9628a358500000000000003e8ffffffffffffffff0000000000004e2000000000000003e80090001e02e9cd12509fbc345c10e2b2e10427ae43eabbf0788bda8118b34344e5576d5d5002eb112dd8d61a02bf3434d5417e1020ed2d951e75bd1e14615d473dd5f1991b4402ca8b6891d6a53aea035259fa0d57a1e672d3cc59a2c696a96247d3557ee726cd030f2aa0ca0ae0e00d6da847d897cf7288ae286dd12037f06250937dc7b37fe0b5028d59e68c5799980a98b120acb891dabfd091e80c6596b0c2a7de57a298d48512000000032259820000000000000000000000000009c4000000002faf0800000000000bebc2002432e16e6e875d1f02a9512943ba2eb9309f4715390b52e4965dd6e39a2989ae63000000002b40420f00000000002200204de81ee2e9850dabdb9c364a12cc2dd75dd20c21d4ca0a809a2e6af3caeeaf9647522102e9cd12509fbc345c10e2b2e10427ae43eabbf0788bda8118b34344e5576d5d502103683425bf640d5e4338553ca6f6afea948ff2bf378eed1b3e9497a3f18f477d5352aefd01bc0200000000010132e16e6e875d1f02a9512943ba2eb9309f4715390b52e4965dd6e39a2989ae63000000000089273f80044a010000000000002200202102815a0b14d50a2a8c028cd47413e743f02ad5460151bd4ae5930639cbbd484a010000000000002200204e81bd6470c45c349068c2b0ad544934b718ef49c1615f2a0b0cd8766400b562400d0300000000002200208eadab92c95168438acfb73f2d3d92f56d6fc30d3f79fc4527f4e77df88b8add72270c0000000000220020895c0fef7efb7aa0e4805381434363b30386d31f6cb621abb0ca4a52b269fbc304004830450221009f09a584b1af9ba86f1618aad1ca9817324f97c3d3789b2490c3b9435c7bf1dd02205e4dcca3d7937f6b80a0ed39084e4ea9e9c531b80698ebd0c116fd9319a0db1201473044022001e4104ec25ba9bc43bf3f623d6e98f371434442b134255667ddeedd9cd9cd1b02201f057d2c15ee4e5d62a73f1db262846ca7ad35deaed2496abdb5b16cd91c5cb80147522102e9cd12509fbc345c10e2b2e10427ae43eabbf0788bda8118b34344e5576d5d502103683425bf640d5e4338553ca6f6afea948ff2bf378eed1b3e9497a3f18f477d5352ae25899520000000000000000000000000000009c4000000000bebc200000000002faf0800acdb0bf90ed7350d429c2d70fce66300ac9b27d8dbb3bceb5bd2cc8bd443938f03f54d00601f1b95e82836540d760ab6b38bc6fc1701df5d5e3b410f1936212d54000000000000000000000000000000000000000000000000000000000000ff03658b1e02efd8464688f87a052518691ff4d818a7c36addf4c6520ae07b7e20912432e16e6e875d1f02a9512943ba2eb9309f4715390b52e4965dd6e39a2989ae63000000002b40420f00000000002200204de81ee2e9850dabdb9c364a12cc2dd75dd20c21d4ca0a809a2e6af3caeeaf9647522102e9cd12509fbc345c10e2b2e10427ae43eabbf0788bda8118b34344e5576d5d502103683425bf640d5e4338553ca6f6afea948ff2bf378eed1b3e9497a3f18f477d5352ae00000032e16e6e875d1f02a9512943ba2eb9309f4715390b52e4965dd6e39a2989ae63061a8000002a000000008833a9275befbedbf928e743a71c9476f46528baf6e832ad25478c2607e4108dea67657c51db7344032627c855518179dc7a94b59f306faf9a3b4eef7684d5d73106226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f061a8000002a000060e32a590101009000000000000003e8000854d00000000a000000003b9aca000000" - val commitments = channelDataCodec.decode(anchorOutputsChannel.bits).require.value.asInstanceOf[ChannelDataWithCommitments].commitments - assert(commitments.params.channelConfig == ChannelConfig.standard) - assert(commitments.params.channelFeatures == ChannelFeatures(Features.StaticRemoteKey, Features.AnchorOutputs)) - } - { - val wumboChannel = hex"00000000000103af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d00009b3b6da18b7e5e43405415a43e61abd77edf4d300c131af17a955950837d7db4980000001000000000000044c000000001dcd65000000000000002710000000000000000000900064ff1600142946bdbb3141be9a40ed8133ef159ee0022176e90000186b02000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a4982039dc0e0b1d25905e44fdf6f8e89755a5e219685840d0bc1d28d3308f9628a358500000000000003e8ffffffffffffffff0000000000004e2000000000000003e80090001e03a6be2613a9ff761a769b077abb43d76c5f06944fc32010891156179bf8b81607025c24f0c3572631ea09dade913ccbb1cd8a55585fc152cc2165bf4f43af702c8b0306c241c667eefee031639052c4a35c4ae076ab56b49f76f7e12f63dfb7bb32a503fdbff1ca92c99c09a9c0eac3cfe1ca35deb658d5fdb815fe7a240df25da5e20f02c47d9e4e1fb501c81933cfc4d3ba7f0e8203003cda2683569f1f67aac2b6e20d000000030a4982000000000000000000000000002710000000745e66c600000000000bebc2002464fc06b43e94d9ce0a5a0e1ef13f63ba78e56d920e7d606d0c6e7a631c48c32c000000002b0065cd1d00000000220020d1b88cc50b09abce945ce6703f30dae3d71ec626be2201925db9379a3e8b981047522103044d393b0da6bcd92030f3131d0780b0d6fa017a0ff45b7fed708903c2950ced2103a6be2613a9ff761a769b077abb43d76c5f06944fc32010891156179bf8b8160752aefd01590200000000010164fc06b43e94d9ce0a5a0e1ef13f63ba78e56d920e7d606d0c6e7a631c48c32c000000000018e2678002400d030000000000160014cf3c9d08ec27d3ffefbf7e2640d3773e11a60ceb783bca1d00000000220020153e514ade07f347db9ed32cf6b2096d066d48e0cf8c504590cd1155de68be2204004730440220056ac83d5b2eb9eb9714e85294d080a9b64f73916d8206b15533613b4303ed3d022044da40ba433e27ddb8743c75acc0f6b050c346bc9d241ab3a1b65051820ce06a0147304402207683c10f33682e2389e5f02dd34090bbe4d786633d49e1777d5ec2ab71a7924902206d99dceb74ff809237ae8e06c118b38047e2d4a49f9956c538bedca47cb4b1da0147522103044d393b0da6bcd92030f3131d0780b0d6fa017a0ff45b7fed708903c2950ced2103a6be2613a9ff761a769b077abb43d76c5f06944fc32010891156179bf8b8160752aef5c4602000000000000000000000000000002710000000000bebc200000000745e66c6000bdebd6cf39db4c2ce1a675281b0a714e525525149032ba048d75c89e5a9b355021a87b53e12e4b36287635cdf887991a732c2fd56b12f5377d755b8b890795db6000000000000000000000000000000000000000000000000000000000000ff032c5f941f78a00105b995fba7f3c340afe53e287a60361135386ae1448d6f3d632464fc06b43e94d9ce0a5a0e1ef13f63ba78e56d920e7d606d0c6e7a631c48c32c000000002b0065cd1d00000000220020d1b88cc50b09abce945ce6703f30dae3d71ec626be2201925db9379a3e8b981047522103044d393b0da6bcd92030f3131d0780b0d6fa017a0ff45b7fed708903c2950ced2103a6be2613a9ff761a769b077abb43d76c5f06944fc32010891156179bf8b8160752ae00000064fc06b43e94d9ce0a5a0e1ef13f63ba78e56d920e7d606d0c6e7a631c48c32cff5e020000000101010101010101010101010101010101010101010101010101010101010101012a00000000ffffffff010065cd1d00000000220020d1b88cc50b09abce945ce6703f30dae3d71ec626be2201925db9379a3e8b9810000000000000000060e32bf9000082000000000000000000000000000000000000000000000000000000000000000064fc06b43e94d9ce0a5a0e1ef13f63ba78e56d920e7d606d0c6e7a631c48c32c0000f4957823b0a6b76697d7c545bcca66abec8522a0f6350a722e45f3f2830f7f824c84efed8daffa04bb0822ce1a08fb2de77dd20c23a91cced7a3966808956644" - val commitments = channelDataCodec.decode(wumboChannel.bits).require.value.asInstanceOf[ChannelDataWithCommitments].commitments - assert(commitments.params.channelConfig == ChannelConfig.standard) - assert(commitments.params.channelFeatures == ChannelFeatures()) - } - } - - test("ensure remote shutdown script is not set") { - val commitments = channelDataCodec.decode(dataNormal.bits).require.value.asInstanceOf[ChannelDataWithCommitments].commitments - assert(commitments.params.remoteParams.upfrontShutdownScript_opt.isEmpty) - } - -} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3Spec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3Spec.scala deleted file mode 100644 index f85299f4e2..0000000000 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3Spec.scala +++ /dev/null @@ -1,133 +0,0 @@ -/* - * Copyright 2019 ACINQ SAS - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package fr.acinq.eclair.wire.internal.channel.version3 - -import fr.acinq.eclair.blockchain.fee.ConfirmationTarget -import fr.acinq.eclair.channel._ -import fr.acinq.eclair.transactions.Transactions._ -import fr.acinq.eclair.wire.internal.channel.version3.ChannelCodecs3.Codecs._ -import fr.acinq.eclair.wire.internal.channel.version3.ChannelCodecs3.channelDataCodec -import fr.acinq.eclair.{BlockHeight, RealShortChannelId, ShortChannelId} -import org.scalatest.funsuite.AnyFunSuite -import scodec.bits.HexStringSyntax - -class ChannelCodecs3Spec extends AnyFunSuite { - - test("decode all known channel configuration options") { - import scala.reflect.ClassTag - import scala.reflect.runtime.universe._ - import scala.reflect.runtime.{universe => runtime} - val mirror = runtime.runtimeMirror(ClassLoader.getSystemClassLoader) - - def extract[T: TypeTag](container: T)(implicit c: ClassTag[T]): Set[ChannelConfigOption] = { - typeOf[T].decls.filter(_.isPublic).flatMap(symbol => { - if (symbol.isTerm && symbol.isModule) { - mirror.reflectModule(symbol.asModule).instance match { - case f: ChannelConfigOption => Some(f) - case _ => None - } - } else { - None - } - }).toSet - } - - val declaredOptions = extract(ChannelConfig) - assert(declaredOptions.nonEmpty) - val encoded = channelConfigCodec.encode(ChannelConfig(declaredOptions)).require - val decoded = channelConfigCodec.decode(encoded).require.value - assert(decoded.options == declaredOptions) - } - - test("backward compatibility DATA_NORMAL_COMPAT_02_Codec") { - val oldBin = hex"00022aed498450b3eb2f6aafedd40640b54efc60ae681da9e6a35299b8b6a6125a7301010003af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d00009d2b17f27b3938b2b50ec713df1b1ae5fd3d23010c9e2e22385f13a168c6acf2c80000001000000000000044c000000001dcd65000000000000002710000000000000000000900064ff1600149d706d0fa71a0b6aa0f3fa400bee18102b45c8170000186b0200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000024982039dc0e0b1d25905e44fdf6f8e89755a5e219685840d0bc1d28d3308f9628a358500000000000003e8ffffffffffffffff0000000000004e2000000000000003e80090001e03fb08866066d5f6f539314220fc31a8e7fdf6ccd12ccde150acc59bac5bb971e003f399d17a9e2fb13a52373d1631807c3161250b2774642fffa629858ad0831f68020b4ecc52820a93f6da40a60d7859307edc737347054d82592959ed1cd0b02e9c03db348203bfd939779bfdd825bc807e904225c3348fa5e3bb57d38ac4b9f40f850284c0df55fbfc2212cbd18cf8ab0eb5283b4b883350ff07e81988e01ae2bb71e20000000302498200000000000000000000000000002710000000002faf0800000000000bebc200242aed498450b3eb2f6aafedd40640b54efc60ae681da9e6a35299b8b6a6125a73000000002b40420f00000000002200203305e10ec90f004675285e9b0371a663ead7abe6caba8c6d5739ace6c9eab4534752210390d6a42ff78a21b41560f75359f0f8a9edaaf0ddcf1a6609130f0d5f234463662103fb08866066d5f6f539314220fc31a8e7fdf6ccd12ccde150acc59bac5bb971e052ae7d02000000012aed498450b3eb2f6aafedd40640b54efc60ae681da9e6a35299b8b6a6125a7300000000000a1e418002400d0300000000001600146862a069e7038573a81f5627ae7d0c6ee5cd0acfb8180c0000000000220020a5f137a8049afcac9f0451b0f31677a81b5443f1c04c1910b37b0d2b8aa4ca0084a4eb2096e400507ac49b57910c914cff8338b8bc57884983541ee1b6919ece7431f4030b09a5fcd87b35682dea0d8faa394e47cc31af897cdf3e6ff502b086cfac37c700000000000000000000000000002710000000000bebc200000000002faf080028131acadf245e7d95d3d7a7f1ac0c0411ead7957ab00d310696b0b5d7d14ac8020597a38e090850030f255fb2781a53713faf7ba81c44de931a63a78efd9908ef000000000000000000000000000000000000000000000000000000000000ff035878c87f6ed100476648193e10a1462bfb55cea3ec5a8f4fbd0fe7304979094b242aed498450b3eb2f6aafedd40640b54efc60ae681da9e6a35299b8b6a6125a73000000002b40420f00000000002200203305e10ec90f004675285e9b0371a663ead7abe6caba8c6d5739ace6c9eab4534752210390d6a42ff78a21b41560f75359f0f8a9edaaf0ddcf1a6609130f0d5f234463662103fb08866066d5f6f539314220fc31a8e7fdf6ccd12ccde150acc59bac5bb971e052ae000000061a8000002a0000000088ec7ae2af533c270809b48f9a0b5a9650df9961a2177e04d7e9929ab319fcd0d150e3ded703b8b4a3f5faff0f2fedf22a6729760deefdea4feed868e4e3cdcf1b06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f061a8000002a000061163fb60101009000000000000003e8000858b800000014000000003b9aca000000" - val decoded1 = channelDataCodec.decode(oldBin.bits).require.value - assert(decoded1.asInstanceOf[DATA_NORMAL].closeStatus_opt.isEmpty) - } - - test("backward compatibility DATA_SHUTDOWN_COMPAT_03_Codec") { - val oldBin = hex"0003342b8fd35a1f3384c6db37723ecc766b672f61b4b0f2e7d5d81cf1e451b584d301010003af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d00009f2408e50889ac3f6c19a007e25752bf143ab0d920cedb9f1c7b7b6b2270c1fe080000001000000000000044c000000001dcd65000000000000002710000000000000000000900064ff160014d0cd80743458194e5f86d9a74592765442fd15bb0000186b0200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000024982039dc0e0b1d25905e44fdf6f8e89755a5e219685840d0bc1d28d3308f9628a358500000000000003e8ffffffffffffffff0000000000004e2000000000000003e80090001e03d1a9a2a574585f60abaaa33826488f9eb8d53ca76693bfa69b42ee2bc8ebe9c50223dc803cceb3edff5deacc9965002cc366b719331e776abf76ee680e4afbf46a0315880bf27110fa57f5b75607bc32a098c209fe8a5a2a30120b2a246fd3fcd62e025dc435be4cb790e5ab12705fb3c0b717ddd9f4d0234fdb557358ecefc6a4e97a0238e3723a9d60dac60c2673ec836e2a909ba9fc888fcbd1cd2ad57ff0b10c5c360000000302498200000000000000000001000200fd05aa342b8fd35a1f3384c6db37723ecc766b672f61b4b0f2e7d5d81cf1e451b584d300000000000000000000000011e1a300d91ec390338376481ff3aea604bf2106cf380d78e946e6a4fc0ad72dc0dec51b00061b100002909a15094ba406ca280bce032eab4d25eae4a9e8b51ff65574b6de2033a292cb1bc6ef576207f4b585273048502efad58af480f08dabcbff1a9642eb3b08811eb4417ac14bf6e5e3883d55ba3b7fdaf27e5ebac16ce63d183b2521867f6cedcf55692f1490da9bc655e38281a26150c44d56e02013faf1cdb2457bec4830e67c10ff0d731798d9e383cc3ce9ba21bddd8971e1d206c54c22317d75411b73abb08c9af348cca2aa2f17232b008aaef4bedb98c426041d3698924d089b59c639396ba38d2ba6d59ea2b0ee8ec9f548deced719e9aae73854d8b6c10c78890999ae09b912a8ae98d3513119bfa0ffdd3037cf5690122020f2c0d01b282ad0cbe345bd31cf7e55d8e78c61abfa62f97d2a095e2256c6569d3e6b5652b365d935d16451f2cf7df04e99e675d9eb649b583fc5d810dae3860001e667b9b78733b3ed6ca5058e6806ca4eb493c5c9a53b0ad78b9ea2091d6e94130afb1d79ce8e274a93d748af9d4ee7ae5b1c06cf1f65507bc79db22c30ae6df93a16c279e4beaf50a0e4e87449e9c830fe9b3877b7332b0632743d4e6267ffa404a35d74d79c5d724bdc9aaaa0423a157fb269b916b0022fd52bb0b2702e6abcc7d9d39d7f69a09a9814b3110cc72574fed031bbc0fd5a03fcee1181076369e76c183b2833385a38dd10773fe3228cb18aa731674a574d310a76052cf89c80077aa89380fcfdeda00508f79261c1e1886240de3bbc343eb84ea7879de24f7dbd0b1527201131bcaf5621e49c69ceec36280f9db6d27161c1676c9b11597d4bd8f10032a964fc1d1afcea594237f3408b184b16eba952778252bc6debc30e6e595c7ebbc2af0c00456ca771d024a56d9f074ca5a7f78f1ef107aa42c2a6426f8dbb2ea8962e2128cfc15161512d2e923e2879635c8ba80bc17f92d09abfae4e08a7f34decc1acd4aa4cc08181017bf248321558a8c4d1a45b92b96274dd9f336eb45d8af6d13147be3a250c17106ec6729e0cffc1af5255352296477dea870426aa82e7af3cc07156b65f8d98b482985618b69b99ee3871240db751923dbeddea6e36f5c3f0599300973fe064ccf4f15cadb7372aaab5b300674f5bf28ce905ab411f5605fba7075ed4a6b08dca3ed5867dc32e491c64e7f09ff4095081e0731cbfc53fa42c33e79bd1a26d8e9f8c5b546b07bd1b300f1f357919e955bded1f987401f4da1a453282b6978ff3bfd13b280f6253dd968cb4e7741a15faf7b4c70dbd9eab6461ca3d3a576410213bc4e144a63685488afeeb7857af4ef83e73c88b17798616a9ccbd603d61ec8f4dbbdd5fea5ca748cf40c38fec50d4eebf1e5b57fd32500baa892da24349598c3125162b2ea0ea049ea9aacdfa848f41fdf7f9eb983776bac58ddf762d131196ab18774349a5eca2fc5b51b60d7aceae1321c1decd462f9c7dbbd820399f71a3c3ccbb30a29f76cd8e94243e400c4dc6e6d32b6f94155a21eb89b6b4c0fd1660bd98509d11b3a45fb257ae1e3c5ebae7bae038200c0f7f2d4ee744c3f2d9007d6fb282b0254ccd71eae5b4c4996b52edb2f2f4120356096c81e42168ee43d98bd630b0af01c270db3fb680cd931d388e1c1260126707548ed1f27006a64db7885ac7500ce020f6349a1abf59fb3ca4bc5bdcbd19b7a0cfe82aeea62f0373d5c16002cd9adcb3ef5e0c862e2fda972530b399b0f6365f4edbc36a32bd1766763104aca34450f278418211d62ff3fff2d0de7f516028011e5681ee892288835a3ec6d0d353f8bc7168086a0eed30d5e81d8a51f7debbb2290dc1a1745e0fc2c08778d11ccb78dc7c98a8b4a4b28fd8decda3a2063ad73c23c112d609b6903613c5ad8e178c90fed2ebd10b176b17c9c2dfc630ef744ac289b437ad229b77a393c6b05c10f36ba2f80482ed6e26b3d9c34d2200fd05aa342b8fd35a1f3384c6db37723ecc766b672f61b4b0f2e7d5d81cf1e451b584d30000000000000001000000000bebc20092ebfcedc573c52ba840ff42bb322419ed390dc845c2fe80636b6b82cf41210000061b100002f211b0d97e6c7f7fb800dbb8d1c7536ad7ef284c40a85fc7316b771bb525c91f7e1d4bba90df0cf0d12c19906e4ae0a1b83954632b59f5f499dedaeba19d75c3bc244ccb57089abeaf29c7d73d67ae822aea2ed4d85d27dc65042589e8172aa3b66d928d7e35f027d96bf23e656701bdf13651e25bed745fef014fdec3faaba4795638299975d3cdc8d2c5b19e2e411e6e4f7b3cede7e269c8902306d25b1027e99736489fde667f4eaf5726349851986ff15a9e967385e080079a14c0a9c3001bbce5da720dcf739e67801cfe2a565b24f5515f8d8889a6d2dc0b51fae4facb72904ba8c1949550332cd8b9095655da72a9815537a80efd204d497af47fd18ef0f6d0639424eb81dab0e34f228aed9406d23773874b4c41d9a21dcaf632419e459baaca5f338f0f9718c89fb72108788d32aeb07e2eb343c45891958779e75fe0039eddaf7e5c4aa8ac2aee9928c68dc529d415780d8e14adcecc91b142b238e7657ef1dc6b6e9843bb5fbe8d04564307e515d88b34105b90b3fa9ed91025297a373507cba86160ba5b7b569948c95fd483b28b8970bebd5e17fcad76c6ed64615cc2d7297994483d94c010a210ad3602b7f359d7d1391b97968f9eaa70de83ea1168fdb05190a6e9fc7c984213da3e2a6672d09704d8e29e2aa9349c9ae44eca3dc0522c769887cbaa0926c5bad64eaf7c22399f7abcc01505d921cce15f4613406956715fe3030ab5c74506d1acc30649b596931316a8e210a368d758d7c1f406f5888499f8317a9ba629dfe85453b4413107796bde15636e6f628078863c0009db7d97cfadc2bfdcdfed237debac33dc525f25865663bc7a812099102fe9c65c8a37b179337141c8d76f365824b0db54b0b18d6c88b21406a1e35061fb4f5ef8a280f9c2ac2ada9eea8cb365d346c5e9eaa56728584aa71c4296a1c11d76f03351a91a030ae6df1da008a5d0f92207b8ca22f335b5a347707ef2500119597d5b92c559f477d8405f1d0dfd790c99ca5a7b4aeac232224ea8461ed07636931e65fee7b063d0ef49f471c04e2fd45b2fff3fb6923314e26045259640335c417ed14a0799553dda3eff5065ec5606303c42309221881c7dea17227da4003a6e396e308ca77da3b4b743a1c2771252c2fa8caad98c9985f9157dbcdd355d3a70b2edeca9682e4bba3e7da24fdd472017b89103e61ab9a4bcc9ab75bd28f7fad29327bf710b3b4759bb76983b003a0907794ad73194d14a70769ce6731b0ae21d565b53780275b6ac42c650d47f570331c7ac57fc75390d3b232eece8277a7c8453b5f28206ff1026879981f2ac83a0d4760554401e671a110de6c03cc104927c55de7254311680af4fb5d36b2199e1348f6fa50877d6cb163dd5f70381f2418f20d145d8ed5df2714cdcda59a4362b0c544ee1af01ac41bc15729951b1e53f3a135fe9011d3e938b08c50d53da259e2ab19b97a47b96d6b7fd559704ac9d1d8a223cbb88c9f77fa563c0c66524c260f0ff54471acae87e71c0507b14edf02bd42993966eea6b2f5d36db12b7fec7a63b80a13651822867db1f046081ad2cf3676b88fa2ef700b8c865a7dedcdee739795853584b74c3f59868daf79e81bf70afed8ea1ce689d67090a32dfe351fd0bdaf60b3903d7ff7c149134a95b2ddf1d63e0f89dfe0b710accf2ca3c37647973532338c59d32e562d7c55573277c1fab09d57a452f4214131178683533fbfe7726276903aacd579d36c6679187b1d22dda32fc5302b91244840a54c258e4d416deb0d20f96b214c335608db6b725175d8517f35f9d6b466c751a1253b20051863a02f2e0c073830cb7de27c96c4ccb196e079edc817ebdf70972b917b6a07c6a431567b99c6072b102e5103a22908683f60bb4fb72538ae3a3768de85f9fc682e67c6aea0e000027100000000011e1a300000000000bebc20024342b8fd35a1f3384c6db37723ecc766b672f61b4b0f2e7d5d81cf1e451b584d3000000002b40420f000000000022002091a342be9a9171a40f83766fbf34a0be03b79675dd20c14e13d91651f7f8273e47522103a1bd05e59ffa63f010cee2ff6848730c6f718da57aef6a5df381683d409a815e2103d1a9a2a574585f60abaaa33826488f9eb8d53ca76693bfa69b42ee2bc8ebe9c552aed30200000001342b8fd35a1f3384c6db37723ecc766b672f61b4b0f2e7d5d81cf1e451b584d300000000007d30f88004400d030000000000160014dab249c7d3b8d826d45244f609bb90f72e691daa400d030000000000220020d24765dcd888e08581c2add26ddfaa6f3ef06b56578373c59915bcc25aa0c6ee286a04000000000022002054d7877901aedaf54d84b20a4233107c2fdab9ff637106d44097e69a813d394ee093040000000000220020039e4d3b414381fb8fda640b122e604b20aa6f64e87607dd9354cdc636cb483d1b17b4207914784cfd82df8f93f15defff862cede27811ce4c59df98e86fd57e5b5105921e842a02a47ee6ddb313b37bc0b6706fe3011e2dd2d82066c85ab11a8d7b4e47000202241ad33e0af0717b31e19b50fd6b0469a42807eec1a080ad29cd56d04a4de54313010000002b400d030000000000220020d24765dcd888e08581c2add26ddfaa6f3ef06b56578373c59915bcc25aa0c6ee8576a91474e49ec772064db47a555b032479b20992beeefe8763ac672102cd66c9b36fdd627754f492f15bc2387f44fd70bc4c9ccdb08df37e25250a5b197c820120876475527c2103d80a13db0dffe34a9b1ff383306a52aed0a40a99ce1c34b113ca7467327f608452ae67a9142a784c1850f79298eab62c5c7709a5d468f5522788ac68685e02000000011ad33e0af0717b31e19b50fd6b0469a42807eec1a080ad29cd56d04a4de54313010000000000000000015af302000000000022002054d7877901aedaf54d84b20a4233107c2fdab9ff637106d44097e69a813d394e101b06000000000000000001f8c46e9fa497a4f9f4fdf422840d264f67178defb43320b83c1d9825a6c152995775652ba3689025f4841f165acbd16732ad88629429b3811d71c7b03adf254402241ad33e0af0717b31e19b50fd6b0469a42807eec1a080ad29cd56d04a4de54313030000002be093040000000000220020039e4d3b414381fb8fda640b122e604b20aa6f64e87607dd9354cdc636cb483d8576a91474e49ec772064db47a555b032479b20992beeefe8763ac672102cd66c9b36fdd627754f492f15bc2387f44fd70bc4c9ccdb08df37e25250a5b197c820120876475527c2103d80a13db0dffe34a9b1ff383306a52aed0a40a99ce1c34b113ca7467327f608452ae67a91448d436723875e511f3a4fc540ca63c91637e926388ac68685e02000000011ad33e0af0717b31e19b50fd6b0469a42807eec1a080ad29cd56d04a4de5431303000000000000000001fa7904000000000022002054d7877901aedaf54d84b20a4233107c2fdab9ff637106d44097e69a813d394e101b06000000000000000000ce718c1999b28ddd1207bcdd6789541ec2f405f775efb096bd896bb237b7f5c17543684f99cb53efeade77bc386997e7b5361418c510ed442700cee38c36f18900000000000000010002fffd05aa342b8fd35a1f3384c6db37723ecc766b672f61b4b0f2e7d5d81cf1e451b584d300000000000000000000000011e1a300d91ec390338376481ff3aea604bf2106cf380d78e946e6a4fc0ad72dc0dec51b00061b100002909a15094ba406ca280bce032eab4d25eae4a9e8b51ff65574b6de2033a292cb1bc6ef576207f4b585273048502efad58af480f08dabcbff1a9642eb3b08811eb4417ac14bf6e5e3883d55ba3b7fdaf27e5ebac16ce63d183b2521867f6cedcf55692f1490da9bc655e38281a26150c44d56e02013faf1cdb2457bec4830e67c10ff0d731798d9e383cc3ce9ba21bddd8971e1d206c54c22317d75411b73abb08c9af348cca2aa2f17232b008aaef4bedb98c426041d3698924d089b59c639396ba38d2ba6d59ea2b0ee8ec9f548deced719e9aae73854d8b6c10c78890999ae09b912a8ae98d3513119bfa0ffdd3037cf5690122020f2c0d01b282ad0cbe345bd31cf7e55d8e78c61abfa62f97d2a095e2256c6569d3e6b5652b365d935d16451f2cf7df04e99e675d9eb649b583fc5d810dae3860001e667b9b78733b3ed6ca5058e6806ca4eb493c5c9a53b0ad78b9ea2091d6e94130afb1d79ce8e274a93d748af9d4ee7ae5b1c06cf1f65507bc79db22c30ae6df93a16c279e4beaf50a0e4e87449e9c830fe9b3877b7332b0632743d4e6267ffa404a35d74d79c5d724bdc9aaaa0423a157fb269b916b0022fd52bb0b2702e6abcc7d9d39d7f69a09a9814b3110cc72574fed031bbc0fd5a03fcee1181076369e76c183b2833385a38dd10773fe3228cb18aa731674a574d310a76052cf89c80077aa89380fcfdeda00508f79261c1e1886240de3bbc343eb84ea7879de24f7dbd0b1527201131bcaf5621e49c69ceec36280f9db6d27161c1676c9b11597d4bd8f10032a964fc1d1afcea594237f3408b184b16eba952778252bc6debc30e6e595c7ebbc2af0c00456ca771d024a56d9f074ca5a7f78f1ef107aa42c2a6426f8dbb2ea8962e2128cfc15161512d2e923e2879635c8ba80bc17f92d09abfae4e08a7f34decc1acd4aa4cc08181017bf248321558a8c4d1a45b92b96274dd9f336eb45d8af6d13147be3a250c17106ec6729e0cffc1af5255352296477dea870426aa82e7af3cc07156b65f8d98b482985618b69b99ee3871240db751923dbeddea6e36f5c3f0599300973fe064ccf4f15cadb7372aaab5b300674f5bf28ce905ab411f5605fba7075ed4a6b08dca3ed5867dc32e491c64e7f09ff4095081e0731cbfc53fa42c33e79bd1a26d8e9f8c5b546b07bd1b300f1f357919e955bded1f987401f4da1a453282b6978ff3bfd13b280f6253dd968cb4e7741a15faf7b4c70dbd9eab6461ca3d3a576410213bc4e144a63685488afeeb7857af4ef83e73c88b17798616a9ccbd603d61ec8f4dbbdd5fea5ca748cf40c38fec50d4eebf1e5b57fd32500baa892da24349598c3125162b2ea0ea049ea9aacdfa848f41fdf7f9eb983776bac58ddf762d131196ab18774349a5eca2fc5b51b60d7aceae1321c1decd462f9c7dbbd820399f71a3c3ccbb30a29f76cd8e94243e400c4dc6e6d32b6f94155a21eb89b6b4c0fd1660bd98509d11b3a45fb257ae1e3c5ebae7bae038200c0f7f2d4ee744c3f2d9007d6fb282b0254ccd71eae5b4c4996b52edb2f2f4120356096c81e42168ee43d98bd630b0af01c270db3fb680cd931d388e1c1260126707548ed1f27006a64db7885ac7500ce020f6349a1abf59fb3ca4bc5bdcbd19b7a0cfe82aeea62f0373d5c16002cd9adcb3ef5e0c862e2fda972530b399b0f6365f4edbc36a32bd1766763104aca34450f278418211d62ff3fff2d0de7f516028011e5681ee892288835a3ec6d0d353f8bc7168086a0eed30d5e81d8a51f7debbb2290dc1a1745e0fc2c08778d11ccb78dc7c98a8b4a4b28fd8decda3a2063ad73c23c112d609b6903613c5ad8e178c90fed2ebd10b176b17c9c2dfc630ef744ac289b437ad229b77a393c6b05c10f36ba2f80482ed6e26b3d9c34d22fffd05aa342b8fd35a1f3384c6db37723ecc766b672f61b4b0f2e7d5d81cf1e451b584d30000000000000001000000000bebc20092ebfcedc573c52ba840ff42bb322419ed390dc845c2fe80636b6b82cf41210000061b100002f211b0d97e6c7f7fb800dbb8d1c7536ad7ef284c40a85fc7316b771bb525c91f7e1d4bba90df0cf0d12c19906e4ae0a1b83954632b59f5f499dedaeba19d75c3bc244ccb57089abeaf29c7d73d67ae822aea2ed4d85d27dc65042589e8172aa3b66d928d7e35f027d96bf23e656701bdf13651e25bed745fef014fdec3faaba4795638299975d3cdc8d2c5b19e2e411e6e4f7b3cede7e269c8902306d25b1027e99736489fde667f4eaf5726349851986ff15a9e967385e080079a14c0a9c3001bbce5da720dcf739e67801cfe2a565b24f5515f8d8889a6d2dc0b51fae4facb72904ba8c1949550332cd8b9095655da72a9815537a80efd204d497af47fd18ef0f6d0639424eb81dab0e34f228aed9406d23773874b4c41d9a21dcaf632419e459baaca5f338f0f9718c89fb72108788d32aeb07e2eb343c45891958779e75fe0039eddaf7e5c4aa8ac2aee9928c68dc529d415780d8e14adcecc91b142b238e7657ef1dc6b6e9843bb5fbe8d04564307e515d88b34105b90b3fa9ed91025297a373507cba86160ba5b7b569948c95fd483b28b8970bebd5e17fcad76c6ed64615cc2d7297994483d94c010a210ad3602b7f359d7d1391b97968f9eaa70de83ea1168fdb05190a6e9fc7c984213da3e2a6672d09704d8e29e2aa9349c9ae44eca3dc0522c769887cbaa0926c5bad64eaf7c22399f7abcc01505d921cce15f4613406956715fe3030ab5c74506d1acc30649b596931316a8e210a368d758d7c1f406f5888499f8317a9ba629dfe85453b4413107796bde15636e6f628078863c0009db7d97cfadc2bfdcdfed237debac33dc525f25865663bc7a812099102fe9c65c8a37b179337141c8d76f365824b0db54b0b18d6c88b21406a1e35061fb4f5ef8a280f9c2ac2ada9eea8cb365d346c5e9eaa56728584aa71c4296a1c11d76f03351a91a030ae6df1da008a5d0f92207b8ca22f335b5a347707ef2500119597d5b92c559f477d8405f1d0dfd790c99ca5a7b4aeac232224ea8461ed07636931e65fee7b063d0ef49f471c04e2fd45b2fff3fb6923314e26045259640335c417ed14a0799553dda3eff5065ec5606303c42309221881c7dea17227da4003a6e396e308ca77da3b4b743a1c2771252c2fa8caad98c9985f9157dbcdd355d3a70b2edeca9682e4bba3e7da24fdd472017b89103e61ab9a4bcc9ab75bd28f7fad29327bf710b3b4759bb76983b003a0907794ad73194d14a70769ce6731b0ae21d565b53780275b6ac42c650d47f570331c7ac57fc75390d3b232eece8277a7c8453b5f28206ff1026879981f2ac83a0d4760554401e671a110de6c03cc104927c55de7254311680af4fb5d36b2199e1348f6fa50877d6cb163dd5f70381f2418f20d145d8ed5df2714cdcda59a4362b0c544ee1af01ac41bc15729951b1e53f3a135fe9011d3e938b08c50d53da259e2ab19b97a47b96d6b7fd559704ac9d1d8a223cbb88c9f77fa563c0c66524c260f0ff54471acae87e71c0507b14edf02bd42993966eea6b2f5d36db12b7fec7a63b80a13651822867db1f046081ad2cf3676b88fa2ef700b8c865a7dedcdee739795853584b74c3f59868daf79e81bf70afed8ea1ce689d67090a32dfe351fd0bdaf60b3903d7ff7c149134a95b2ddf1d63e0f89dfe0b710accf2ca3c37647973532338c59d32e562d7c55573277c1fab09d57a452f4214131178683533fbfe7726276903aacd579d36c6679187b1d22dda32fc5302b91244840a54c258e4d416deb0d20f96b214c335608db6b725175d8517f35f9d6b466c751a1253b20051863a02f2e0c073830cb7de27c96c4ccb196e079edc817ebdf70972b917b6a07c6a431567b99c6072b102e5103a22908683f60bb4fb72538ae3a3768de85f9fc682e67c6aea0e00002710000000000bebc2000000000011e1a300306acd0b21100f755a6d193a819042fde1b4c3839598e730905dfa7e6ef699220240a9cccdb58d887b42ccadeee5704030acd52bd9c7759ba01f47f0995531ca360000000000000000000000000000000000000002000000000000000000020000000000000000000334c7b350311d40b3b422e07f3b7759b400000000000000010003e3dd409b342046e7b410cc380c9a09c2ff03cdeeaf422678a9eac407da8f811a402718083016f88d5404f5508054fe25acb824342b8fd35a1f3384c6db37723ecc766b672f61b4b0f2e7d5d81cf1e451b584d3000000002b40420f000000000022002091a342be9a9171a40f83766fbf34a0be03b79675dd20c14e13d91651f7f8273e47522103a1bd05e59ffa63f010cee2ff6848730c6f718da57aef6a5df381683d409a815e2103d1a9a2a574585f60abaaa33826488f9eb8d53ca76693bfa69b42ee2bc8ebe9c552ae000100400000ffffffffffff00204cec06f1f1496c01cd4840cddff19b47c24b7daa478bcaea862c6d11fcc9fff180007fffffffffff8038342b8fd35a1f3384c6db37723ecc766b672f61b4b0f2e7d5d81cf1e451b584d300160014d0cd80743458194e5f86d9a74592765442fd15bb38342b8fd35a1f3384c6db37723ecc766b672f61b4b0f2e7d5d81cf1e451b584d30016001483366b494906052362a88e967304296937926cd1" - val decoded1 = channelDataCodec.decode(oldBin.bits).require.value - assert(decoded1.asInstanceOf[DATA_SHUTDOWN].closeStatus.feerates_opt.isEmpty) - } - - test("backwards compatibility with legacy claim-htlc-success transactions") { - // We can decode old data that didn't contain a payment hash. - val oldTxWithInputInfoCodecBin = hex"0004 24020bc3ab58d19af4aac858786c2cbec2e8f9c43a3bd1ce4a51f6b0e1e1b17ffd000000002bb0ad010000000000220020e63b4729b67c90212244953d1c9eb15da73dffb035eaef21a5fd883c5968145b8576a914a0fc54aee923e51ceb5d37283b6f263a571cae428763ac672103ea61bef3c6ef05f1e65aaea2020c786e6fc923102c8fe0e53a6fe0315da2e62e7c820120876475527c21023b468606d008f702a9ad940c5fb8539f5b66cf2a3d0a7baa4960377dda9a61d152ae67a91452d220bcd80633ae134ec71b1bd8e79dc1490b9388ac6868fd014302000000000101020bc3ab58d19af4aac858786c2cbec2e8f9c43a3bd1ce4a51f6b0e1e1b17ffd0000000000ffffffff016297010000000000160014b95a632df5806773f328bb583f973fe5cb05e7a303463043022045676e573cc8314baad6038f2655f106c5cff1d968cd2373ea1a746cb764ffd5021f4c373a06f917f4ed606c9abb3cb9e770c215bc77480c95c6d36e90edea31680120f992c96b4fe64f43cd031c52be0bc39d6c9bf6fc2712a5a9c310dbd58872a9fc8576a914a0fc54aee923e51ceb5d37283b6f263a571cae428763ac672103ea61bef3c6ef05f1e65aaea2020c786e6fc923102c8fe0e53a6fe0315da2e62e7c820120876475527c21023b468606d008f702a9ad940c5fb8539f5b66cf2a3d0a7baa4960377dda9a61d152ae67a91452d220bcd80633ae134ec71b1bd8e79dc1490b9388ac6868000000000000000000000000" - val oldClaimHtlcTxBin = hex"01 24020bc3ab58d19af4aac858786c2cbec2e8f9c43a3bd1ce4a51f6b0e1e1b17ffd000000002bb0ad010000000000220020e63b4729b67c90212244953d1c9eb15da73dffb035eaef21a5fd883c5968145b8576a914a0fc54aee923e51ceb5d37283b6f263a571cae428763ac672103ea61bef3c6ef05f1e65aaea2020c786e6fc923102c8fe0e53a6fe0315da2e62e7c820120876475527c21023b468606d008f702a9ad940c5fb8539f5b66cf2a3d0a7baa4960377dda9a61d152ae67a91452d220bcd80633ae134ec71b1bd8e79dc1490b9388ac6868fd014302000000000101020bc3ab58d19af4aac858786c2cbec2e8f9c43a3bd1ce4a51f6b0e1e1b17ffd0000000000ffffffff016297010000000000160014b95a632df5806773f328bb583f973fe5cb05e7a303463043022045676e573cc8314baad6038f2655f106c5cff1d968cd2373ea1a746cb764ffd5021f4c373a06f917f4ed606c9abb3cb9e770c215bc77480c95c6d36e90edea31680120f992c96b4fe64f43cd031c52be0bc39d6c9bf6fc2712a5a9c310dbd58872a9fc8576a914a0fc54aee923e51ceb5d37283b6f263a571cae428763ac672103ea61bef3c6ef05f1e65aaea2020c786e6fc923102c8fe0e53a6fe0315da2e62e7c820120876475527c21023b468606d008f702a9ad940c5fb8539f5b66cf2a3d0a7baa4960377dda9a61d152ae67a91452d220bcd80633ae134ec71b1bd8e79dc1490b9388ac6868000000000000000000000000" - val legacyClaimHtlcSuccess = { - val claimHtlcSuccessTx1 = txWithInputInfoCodec.decode(oldTxWithInputInfoCodecBin.bits).require.value - val claimHtlcSuccessTx2 = claimHtlcTxCodec.decode(oldClaimHtlcTxBin.bits).require.value - assert(claimHtlcSuccessTx1 == claimHtlcSuccessTx2) - assert(claimHtlcSuccessTx1.isInstanceOf[LegacyClaimHtlcSuccessTx]) - claimHtlcSuccessTx1.asInstanceOf[LegacyClaimHtlcSuccessTx] - } - } - - test("backwards compatibility with transactions missing a confirmation target") { - { - val oldAnchorTxBin = hex"0011 24bd0be30e31c748c7afdde7d2c527d711fadf88500971a4a1136bca375dba07b8000000002b4a0100000000000022002036c067df8952dbcd5db347e7c152ca3fa4514f2072d27867837b1c2d319a7e01282103cc89f1459b5201cda08e08c6fb7b1968c54e8172c555896da27c6fdc10522ceeac736460b268330200000001bd0be30e31c748c7afdde7d2c527d711fadf88500971a4a1136bca375dba07b80000000000000000000000000000" - val oldAnchorTx = txWithInputInfoCodec.decode(oldAnchorTxBin.bits).require.value - assert(oldAnchorTx.isInstanceOf[ClaimLocalAnchorOutputTx]) - assert(oldAnchorTx.asInstanceOf[ClaimLocalAnchorOutputTx].confirmationTarget == ConfirmationTarget.Absolute(BlockHeight(0))) - } - { - val oldHtlcSuccessTxBin = hex"0002 24f5580de0577271dce09d2de26e19ec58bf2373b0171473291a8f8be9b04fb289000000002bb0ad010000000000220020462cf8912ffc5f27764c109bed188950500011a2837ff8b9c8f9a39cffa395a58b76a91406b0950d9feded82239b3e6c9082308900f389de8763ac672102d65aa07658a7214ff129f91a1a22ade2ea4d1b07cc14b2f85a2842c34240836f7c8201208763a914c461c897e2165c7e44e14850dfcfd68f99127aed88527c21033c8d41cfbe1511a909b63fed68e75a29c3ce30418c39bbb8294c8b36c6a6c16a52ae677503101b06b175ac6868fd01a002000000000101f5580de0577271dce09d2de26e19ec58bf2373b0171473291a8f8be9b04fb289000000000000000000013a920100000000002200208742b16c9fd4e74854dcd84322dd1de06f7993fe627fd2ca0be4b996a936d56b050047304402201b4527c8f420852550af00bbd9149db9b31adcb7e1f127766e75e1e01746df0302202a57bb1e274ed7d3e8dbe5f205de721a23092c1e2ce2135f4750f18f6c0b51b001483045022100b6df309c8e5746a077b1f7c2f299528e164946bd514a5049475af7f5665805da0220392ae877112a3c52f74d190b354b4f5c020da9c1a71a7a08ced0a5363e795a27012017ea8f5afde8f708258d5669e1bbd454e82ddca8c6c480ec5302b4b1e8051d3d8b76a91406b0950d9feded82239b3e6c9082308900f389de8763ac672102d65aa07658a7214ff129f91a1a22ade2ea4d1b07cc14b2f85a2842c34240836f7c8201208763a914c461c897e2165c7e44e14850dfcfd68f99127aed88527c21033c8d41cfbe1511a909b63fed68e75a29c3ce30418c39bbb8294c8b36c6a6c16a52ae677503101b06b175ac686800000000dc7002a387673f17ebaf08545ccec712a9b6914813cdb83b4270932294f20f660000000000000000" - val oldHtlcSuccessTx = txWithInputInfoCodec.decode(oldHtlcSuccessTxBin.bits).require.value - assert(oldHtlcSuccessTx.isInstanceOf[HtlcSuccessTx]) - assert(oldHtlcSuccessTx.asInstanceOf[HtlcSuccessTx].confirmationTarget == ConfirmationTarget.Absolute(BlockHeight(0))) - } - { - val oldHtlcTimeoutTxBin = hex"0003 248f0619a4b2a351977b3e5b0ddd700482e1d697d40deea2dd7356df99345d51d0000000002b50c300000000000022002005ff644937d7f5f32ec194424f551371e8d4bcf2cda3e1096cdd2fe88687fc408576a914c98707b6420ef3454f3bd10d663adcc04452baea8763ac672102fb20469287c8ade948011bd001440d74633d5e1a98574e4783dd38b76509d8f67c820120876475527c210279d3a1c2086a0968404a0160c8f8c6f88c0ce7184022bb7406f98fdb503ea51452ae67a9140550b2b1621e788d795fe3ae308e7dec06a6a1e088ac6868fd0179020000000001018f0619a4b2a351977b3e5b0ddd700482e1d697d40deea2dd7356df99345d51d0000000000000000000016aa9000000000000220020c980a1573ce6dc6a1bb8a1d60ecffdf2f0a7aa983c49786a2ab1ba8f0cd74a76050047304402200d2b631fb0e5a7f406f3de2b36fe3c0ab3337fe98f114dadf38de8f5548e87eb02203ad7c385c7cf62ac17ec15329dee071ee2c479aceb34a14d700dfeba80008574014730440220653b4ff490b03de06a9053aa7ab0b30e85c484c76900c586db65f7308f38ad86022019ac12ffb127e4a17a01dc6db730d3eccea837d2dc85a0275bdea8b393a2f11d01008576a914c98707b6420ef3454f3bd10d663adcc04452baea8763ac672102fb20469287c8ade948011bd001440d74633d5e1a98574e4783dd38b76509d8f67c820120876475527c210279d3a1c2086a0968404a0160c8f8c6f88c0ce7184022bb7406f98fdb503ea51452ae67a9140550b2b1621e788d795fe3ae308e7dec06a6a1e088ac6868101b06000000000000000003" - val oldHtlcTimeoutTx = txWithInputInfoCodec.decode(oldHtlcTimeoutTxBin.bits).require.value - assert(oldHtlcTimeoutTx.isInstanceOf[HtlcTimeoutTx]) - assert(oldHtlcTimeoutTx.asInstanceOf[HtlcTimeoutTx].confirmationTarget == ConfirmationTarget.Absolute(BlockHeight(0))) - } - { - val oldClaimHtlcSuccessTxBin = hex"0016 24e75b5236d1cdd482a6f540d5f08b9aa27b74a9ecae6e2622a67110b3ee1b3d89000000002bb0ad01000000000022002063e22369052a2bad9eb124737742690b8d1aba7693869d041da16443e2973e638576a91494957f4639ebc6f8a30e126552aff8429174dfb18763ac672102e1aa04ff55771238012edb958e6e0525af0415a01d52dd8d5f69fb391e586adc7c820120876475527c2102384e785d34b3b1fe35d3c093a750b234fbc79d8316c149e7845929f628a5baa052ae67a9145e3be49c9ace2d9eab11dc5fc29e40cd4148262e88ac6868fd014502000000000101e75b5236d1cdd482a6f540d5f08b9aa27b74a9ecae6e2622a67110b3ee1b3d890000000000000000000162970100000000001600140262586eef1a2c8f47ebc139a2733123d09e315603483045022100898f6b51361f044c8c54468dcb1b9decd75edcc1b69b62e584059b512eb075900220675f8b23aa402bb7d5bfdefbac4074088f82bd34d09b34c651c0dff48e6967930120efb38d645311af59c028cd8a9bf8ee21ff9e7c1a1cff1a0398a0315280247ac38576a91494957f4639ebc6f8a30e126552aff8429174dfb18763ac672102e1aa04ff55771238012edb958e6e0525af0415a01d52dd8d5f69fb391e586adc7c820120876475527c2102384e785d34b3b1fe35d3c093a750b234fbc79d8316c149e7845929f628a5baa052ae67a9145e3be49c9ace2d9eab11dc5fc29e40cd4148262e88ac686800000000ad02394dd18774f6f403f783afb518b6c69a89d531f718b29868ddca3e7905020000000000000000" - val oldClaimHtlcSuccessTx = txWithInputInfoCodec.decode(oldClaimHtlcSuccessTxBin.bits).require.value - assert(oldClaimHtlcSuccessTx.isInstanceOf[ClaimHtlcSuccessTx]) - assert(oldClaimHtlcSuccessTx.asInstanceOf[ClaimHtlcSuccessTx].confirmationTarget == ConfirmationTarget.Absolute(BlockHeight(0))) - } - { - val oldClaimHtlcTimeoutTxBin = hex"0005 24df6aa4cd4e8877e4b3a363d6180951036d6f18ef214dabd50a6c05a60077d4d8000000002b50c30000000000002200205db892d76fb358ed508ec09c77b5a184cd9de3aa3e74a025a5c5d3f7adc221f78b76a914e0c241e0656088953f84475cbe5c70ded12e05b58763ac6721028876f3e23b21e07f889f10fc2aa0875b96021359a06d0b7f52a79fc284f6b2837c8201208763a9144ae5c96e8b7495fd35a5ca5681aa7c8f4ab6bc9b88527c2102b4d398ee7a42e87012de5a76832d9ebfa8532ef267490a7f3fe75b2c2e18cc9152ae677503981a06b175ac6868fd012b02000000000101df6aa4cd4e8877e4b3a363d6180951036d6f18ef214dabd50a6c05a60077d4d80000000000000000000106ae0000000000001600142b8e221121004b248f6551a0c6fb8ce219f4997e03483045022100ecdb8179f92c097594844756ac2bd7948f1c44ae74b54dfda86971800e59bf980220125edbe563f24cde15fa1fa25f4eac7cb938a3ace6f9329eb487938005c7621501008b76a914e0c241e0656088953f84475cbe5c70ded12e05b58763ac6721028876f3e23b21e07f889f10fc2aa0875b96021359a06d0b7f52a79fc284f6b2837c8201208763a9144ae5c96e8b7495fd35a5ca5681aa7c8f4ab6bc9b88527c2102b4d398ee7a42e87012de5a76832d9ebfa8532ef267490a7f3fe75b2c2e18cc9152ae677503981a06b175ac6868981a06000000000000000003" - val oldClaimHtlcTimeoutTx = txWithInputInfoCodec.decode(oldClaimHtlcTimeoutTxBin.bits).require.value - assert(oldClaimHtlcTimeoutTx.isInstanceOf[ClaimHtlcTimeoutTx]) - assert(oldClaimHtlcTimeoutTx.asInstanceOf[ClaimHtlcTimeoutTx].confirmationTarget == ConfirmationTarget.Absolute(BlockHeight(0))) - } - } - - test("backwards compatibility with codecs pre-alias") { - { - val bin = hex"0001000000000000000000000000000000000000000000000000000000000000000001010003af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d000010000002a00000000000002220000000002faf0800000000000002710000000000000271000900032ff0000000000022cd2a00cddf20323d320bd14ce0e59b00d62def4d853b88e8bf7dc44c556fc07000000000000022200000000004c4b400000000000002710000000000000138800900032031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f024d4b6cd1361032ca9bd2aeb9d900aa4d45d9ead80ac9423374c451a7254d076602531fe6068134503d2723133227c867ac8fa6c83c537e9a44c3c5bdbdcb1fe33703462779ad4aad39514614751a71085f2f10e1c7a593e4e030efb5b8721ce55b0b03f006a18d5653c4edf5391ff23a61f03ff83d237e880ee61187fa9f379a028e0a00000000000100000000000000000005fffd05aa0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f424066687aadf862bd776c8fc18b8e9f8e20089714856ee233b3902a591d0d5f2925000001f40000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fd05aa0000000000000000000000000000000000000000000000000000000000000000000000000000001e00000000001e848075877bb41d393b5fb8455ce60ecd8dda001d06316496b14dfa7f895656eeca4a000001f600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fffd05aa0000000000000000000000000000000000000000000000000000000000000000000000000000000100000000001e848072cd6e8422c407fb6d098690f1130b7ded7ec2f7f5e1d30bd9d521f015363793000001f50000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fd05aa0000000000000000000000000000000000000000000000000000000000000000000000000000001f00000000002dc6c0648aa5c579fb30f38af744d97d6ec840c7a91277a499a0d780f3e7314eca090b000001f700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fffd05aa0000000000000000000000000000000000000000000000000000000000000000000000000000000200000000003d09009f4fb68f3e1dac82202f9aa581ce0bbf1f765df0e9ac3c8c57e20f685abab8ed000001f800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005dc0000000002faf08000000000042c1d8024bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000002b80969800000000002200205e9ed9d4087f82a14496be26b842e968f9ae2e65e331fd93fb97e1f5c6577934475221031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f2103afd229e5da2cc156d1fb929c22bf6878791adad2574614e1c1e5decd65a71a3752aefd010f02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a48848900000000000000000000040047304402202148d2d4aac8c793eb82d31bcf22d4db707b9fd7eee1b89b4b1444c9e19ab71702202bab8c3d997d29163fa0cb255c75afb8ade13617ad1350c1515e9be4a222a04d0147304402206cb12624b253adeb0a41210d63ac6280154923c502202ea16a581bc1839e1e610220178e31542e4a7735d9e243927a5aac00bae1b2889cb9eb785c4d182f3b22a87d01475221031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f2103afd229e5da2cc156d1fb929c22bf6878791adad2574614e1c1e5decd65a71a3752ae000000002148d2d4aac8c793eb82d31bcf22d4db707b9fd7eee1b89b4b1444c9e19ab7172bab8c3d997d29163fa0cb255c75afb8ade13617ad1350c1515e9be4a222a04d00000000000000000000000500fd05aa0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f424066687aadf862bd776c8fc18b8e9f8e20089714856ee233b3902a591d0d5f2925000001f400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fffd05aa0000000000000000000000000000000000000000000000000000000000000000000000000000001f00000000002dc6c0648aa5c579fb30f38af744d97d6ec840c7a91277a499a0d780f3e7314eca090b000001f700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fffd05aa0000000000000000000000000000000000000000000000000000000000000000000000000000001e00000000001e848075877bb41d393b5fb8455ce60ecd8dda001d06316496b14dfa7f895656eeca4a000001f60000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fd05aa0000000000000000000000000000000000000000000000000000000000000000000000000000000200000000003d09009f4fb68f3e1dac82202f9aa581ce0bbf1f765df0e9ac3c8c57e20f685abab8ed000001f80000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fd05aa0000000000000000000000000000000000000000000000000000000000000000000000000000000100000000001e848072cd6e8422c407fb6d098690f1130b7ded7ec2f7f5e1d30bd9d521f015363793000001f500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005dc000000000000c35000000000000aae60030303030303030303030303030303030303030303030303030303030303030303462779ad4aad39514614751a71085f2f10e1c7a593e4e030efb5b8721ce55b0b000000000000000000000000000000000000002000000000000000040002000000000000002a00036e9078b299874e92af44d59fcbc9d2190000000000003a9800022a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a000000000000002b0000000000a7d8c00000000000989680ff023730c8e0fc52aff6ba2f618f29e5ebd551c0129e13ce20312df76e4403c5abbc24bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000002b80969800000000002200205e9ed9d4087f82a14496be26b842e968f9ae2e65e331fd93fb97e1f5c6577934475221031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f2103afd229e5da2cc156d1fb929c22bf6878791adad2574614e1c1e5decd65a71a3752ae00000000000000075bcd1541dd93fe76288e183498a7d57065cf472a4413c643730f9c117ca806be6b57f75303a153d48f51439f168ca85ef2fd6f3e0642fc7132f7a530cd50d880ec05cb4c17" - val data = channelDataCodec.decode(bin.bits).require.value.asInstanceOf[DATA_WAIT_FOR_CHANNEL_READY] - assert(data.aliases.localAlias == ShortChannelId(123456789L)) - // By resetting the funding status, we will re-fill it correctly when the channel is restored. - assert(data.commitments.latest.shortChannelId_opt.isEmpty) - assert(data.commitments.latest.localFundingStatus.isInstanceOf[LocalFundingStatus.UnconfirmedFundingTx]) - } - - { - val bin = hex"0007000000000000000000000000000000000000000000000000000000000000000001010003af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d000010000002a00000000000002220000000002faf0800000000000002710000000000000271000900032ff0000000000022cd2a00cddf20323d320bd14ce0e59b00d62def4d853b88e8bf7dc44c556fc07000000000000022200000000004c4b400000000000002710000000000000138800900032031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f024d4b6cd1361032ca9bd2aeb9d900aa4d45d9ead80ac9423374c451a7254d076602531fe6068134503d2723133227c867ac8fa6c83c537e9a44c3c5bdbdcb1fe33703462779ad4aad39514614751a71085f2f10e1c7a593e4e030efb5b8721ce55b0b03f006a18d5653c4edf5391ff23a61f03ff83d237e880ee61187fa9f379a028e0a00000000000100000000000000000005fffd05aa0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f424066687aadf862bd776c8fc18b8e9f8e20089714856ee233b3902a591d0d5f2925000001f40000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fd05aa0000000000000000000000000000000000000000000000000000000000000000000000000000001e00000000001e848075877bb41d393b5fb8455ce60ecd8dda001d06316496b14dfa7f895656eeca4a000001f600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fffd05aa0000000000000000000000000000000000000000000000000000000000000000000000000000000100000000001e848072cd6e8422c407fb6d098690f1130b7ded7ec2f7f5e1d30bd9d521f015363793000001f50000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fd05aa0000000000000000000000000000000000000000000000000000000000000000000000000000001f00000000002dc6c0648aa5c579fb30f38af744d97d6ec840c7a91277a499a0d780f3e7314eca090b000001f700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fffd05aa0000000000000000000000000000000000000000000000000000000000000000000000000000000200000000003d09009f4fb68f3e1dac82202f9aa581ce0bbf1f765df0e9ac3c8c57e20f685abab8ed000001f800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005dc0000000002faf08000000000042c1d8024bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000002b80969800000000002200205e9ed9d4087f82a14496be26b842e968f9ae2e65e331fd93fb97e1f5c6577934475221031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f2103afd229e5da2cc156d1fb929c22bf6878791adad2574614e1c1e5decd65a71a3752aefd010f02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a48848900000000000000000000040047304402202148d2d4aac8c793eb82d31bcf22d4db707b9fd7eee1b89b4b1444c9e19ab71702202bab8c3d997d29163fa0cb255c75afb8ade13617ad1350c1515e9be4a222a04d0147304402206cb12624b253adeb0a41210d63ac6280154923c502202ea16a581bc1839e1e610220178e31542e4a7735d9e243927a5aac00bae1b2889cb9eb785c4d182f3b22a87d01475221031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f2103afd229e5da2cc156d1fb929c22bf6878791adad2574614e1c1e5decd65a71a3752ae000000002148d2d4aac8c793eb82d31bcf22d4db707b9fd7eee1b89b4b1444c9e19ab7172bab8c3d997d29163fa0cb255c75afb8ade13617ad1350c1515e9be4a222a04d00000000000000000000000500fd05aa0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f424066687aadf862bd776c8fc18b8e9f8e20089714856ee233b3902a591d0d5f2925000001f400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fffd05aa0000000000000000000000000000000000000000000000000000000000000000000000000000001f00000000002dc6c0648aa5c579fb30f38af744d97d6ec840c7a91277a499a0d780f3e7314eca090b000001f700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fffd05aa0000000000000000000000000000000000000000000000000000000000000000000000000000001e00000000001e848075877bb41d393b5fb8455ce60ecd8dda001d06316496b14dfa7f895656eeca4a000001f60000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fd05aa0000000000000000000000000000000000000000000000000000000000000000000000000000000200000000003d09009f4fb68f3e1dac82202f9aa581ce0bbf1f765df0e9ac3c8c57e20f685abab8ed000001f80000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fd05aa0000000000000000000000000000000000000000000000000000000000000000000000000000000100000000001e848072cd6e8422c407fb6d098690f1130b7ded7ec2f7f5e1d30bd9d521f015363793000001f500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005dc000000000000c35000000000000aae60030303030303030303030303030303030303030303030303030303030303030303462779ad4aad39514614751a71085f2f10e1c7a593e4e030efb5b8721ce55b0b000000000000000000000000000000000000002000000000000000040002000000000000002a00036e9078b299874e92af44d59fcbc9d2190000000000003a9800022a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a000000000000002b0000000000a7d8c00000000000989680ff023730c8e0fc52aff6ba2f618f29e5ebd551c0129e13ce20312df76e4403c5abbc24bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000002b80969800000000002200205e9ed9d4087f82a14496be26b842e968f9ae2e65e331fd93fb97e1f5c6577934475221031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f2103afd229e5da2cc156d1fb929c22bf6878791adad2574614e1c1e5decd65a71a3752ae000000000000000000002aff008821c93fbf280b2391826bc70ae858cf815d4afc1816f85445364f188e635d4ae64dfe1c58bedb017dd6f267452444d991b66fcfc638396f72fa6926f69d6125ff01010101010101010101010101010101010101010101010101010101010101010000000000022cd962975eec0101002a000000000000000f0000023f0000003500000003e8000000000000" - val data = channelDataCodec.decode(bin.bits).require.value.asInstanceOf[DATA_NORMAL] - assert(data.aliases.localAlias == ShortChannelId(42)) - // By resetting the funding status, we will re-fill it correctly when the channel is restored. - assert(data.commitments.latest.shortChannelId_opt.isEmpty) - assert(data.commitments.latest.localFundingStatus.isInstanceOf[LocalFundingStatus.UnconfirmedFundingTx]) - } - } - -} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version4/ChannelCodecs4Spec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version5/ChannelCodecs5Spec.scala similarity index 62% rename from eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version4/ChannelCodecs4Spec.scala rename to eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version5/ChannelCodecs5Spec.scala index ca67359422..b149dc06f8 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version4/ChannelCodecs4Spec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version5/ChannelCodecs5Spec.scala @@ -1,130 +1,27 @@ -package fr.acinq.eclair.wire.internal.channel.version4 +package fr.acinq.eclair.wire.internal.channel.version5 -import com.softwaremill.quicklens.ModifyPimp -import fr.acinq.bitcoin.scalacompat.Crypto.PrivateKey -import fr.acinq.bitcoin.scalacompat.{ByteVector32, DeterministicWallet, OutPoint, Satoshi, SatoshiLong, Script, Transaction, TxId, TxIn, TxOut} +import fr.acinq.bitcoin.scalacompat.SatoshiLong import fr.acinq.eclair.FeatureSupport.{Mandatory, Optional} import fr.acinq.eclair.Features.{ChannelRangeQueries, PaymentSecret, VariableLengthOnion} import fr.acinq.eclair.TestUtils.randomTxId -import fr.acinq.eclair.blockchain.fee.{FeeratePerByte, FeeratePerKw} -import fr.acinq.eclair.channel.LocalFundingStatus.DualFundedUnconfirmedFundingTx +import fr.acinq.eclair.blockchain.fee.FeeratePerKw import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.fund.InteractiveTxBuilder.{InteractiveTxParams, PartiallySignedSharedTransaction, RequireConfirmedInputs, SharedTransaction} import fr.acinq.eclair.channel.fund.InteractiveTxSigningSession.UnsignedLocalCommit import fr.acinq.eclair.channel.fund.{InteractiveTxBuilder, InteractiveTxSigningSession} -import fr.acinq.eclair.transactions.Transactions.{CommitTx, InputInfo} -import fr.acinq.eclair.transactions.{CommitmentSpec, Scripts} -import fr.acinq.eclair.wire.internal.channel.ChannelCodecsSpec.normal -import fr.acinq.eclair.wire.internal.channel.version4.ChannelCodecs4.Codecs._ -import fr.acinq.eclair.wire.internal.channel.version4.ChannelCodecs4.channelDataCodec +import fr.acinq.eclair.transactions.CommitmentSpec +import fr.acinq.eclair.transactions.Transactions.ZeroFeeHtlcTxAnchorOutputsCommitmentFormat +import fr.acinq.eclair.wire.internal.channel.version5.ChannelCodecs5.Codecs.{dualFundingStatusCodec, remoteChannelParamsCodec} +import fr.acinq.eclair.wire.internal.channel.version5.ChannelCodecs5.channelDataCodec import fr.acinq.eclair.wire.protocol.{LiquidityAds, TxSignatures} -import fr.acinq.eclair.{BlockHeight, CltvExpiryDelta, Features, MilliSatoshi, MilliSatoshiLong, UInt64, randomBytes32, randomKey} +import fr.acinq.eclair.{CltvExpiryDelta, Features, MilliSatoshiLong, UInt64, randomBytes32, randomKey} import org.scalatest.funsuite.AnyFunSuite -import scodec.bits._ +import scodec.bits.{ByteVector, HexStringSyntax} -import scala.util.Random - -class ChannelCodecs4Spec extends AnyFunSuite { - - test("basic serialization test (NORMAL)") { - val data = normal - val bin = channelDataCodec.encode(data).require - val check = channelDataCodec.decodeValue(bin).require.asInstanceOf[ChannelDataWithCommitments] - assert(data.commitments.latest.localCommit.spec == check.commitments.latest.localCommit.spec) - assert(data == check) - } - - test("encode/decode channel configuration options") { - assert(channelConfigCodec.encode(ChannelConfig(Set.empty[ChannelConfigOption])).require.bytes == hex"00") - assert(channelConfigCodec.decode(hex"00".bits).require.value == ChannelConfig(Set.empty[ChannelConfigOption])) - assert(channelConfigCodec.decode(hex"01f0".bits).require.value == ChannelConfig(Set.empty[ChannelConfigOption])) - assert(channelConfigCodec.decode(hex"020000".bits).require.value == ChannelConfig(Set.empty[ChannelConfigOption])) - - assert(channelConfigCodec.encode(ChannelConfig.standard).require.bytes == hex"0101") - assert(channelConfigCodec.encode(ChannelConfig(ChannelConfig.FundingPubKeyBasedChannelKeyPath)).require.bytes == hex"0101") - assert(channelConfigCodec.decode(hex"0101".bits).require.value == ChannelConfig(ChannelConfig.FundingPubKeyBasedChannelKeyPath)) - assert(channelConfigCodec.decode(hex"01ff".bits).require.value == ChannelConfig(ChannelConfig.FundingPubKeyBasedChannelKeyPath)) - assert(channelConfigCodec.decode(hex"020001".bits).require.value == ChannelConfig(ChannelConfig.FundingPubKeyBasedChannelKeyPath)) - } - - test("encode/decode optional channel reserve") { - val localParams = LocalParams( - randomKey().publicKey, - DeterministicWallet.KeyPath(Seq(42L)), - Satoshi(660), - MilliSatoshi(500000), - Some(Satoshi(15000)), - MilliSatoshi(1000), - CltvExpiryDelta(36), - 50, - Random.nextBoolean(), - Random.nextBoolean(), - Some(hex"deadbeef"), - None, - Features().initFeatures()) - val remoteParams = RemoteParams( - randomKey().publicKey, - Satoshi(500), - UInt64(100000), - Some(Satoshi(30000)), - MilliSatoshi(1500), - CltvExpiryDelta(144), - 10, - randomKey().publicKey, - randomKey().publicKey, - randomKey().publicKey, - randomKey().publicKey, - Features(), - None) - - { - val localCodec = localParamsCodec(ChannelFeatures()) - val remoteCodec = remoteParamsCodec(ChannelFeatures()) - val decodedLocalParams = localCodec.decode(localCodec.encode(localParams).require).require.value - val decodedRemoteParams = remoteCodec.decode(remoteCodec.encode(remoteParams).require).require.value - assert(decodedLocalParams == localParams) - assert(decodedRemoteParams == remoteParams) - } - { - val localCodec = localParamsCodec(ChannelFeatures(Features.DualFunding)) - val remoteCodec = remoteParamsCodec(ChannelFeatures(Features.DualFunding)) - val decodedLocalParams = localCodec.decode(localCodec.encode(localParams).require).require.value - val decodedRemoteParams = remoteCodec.decode(remoteCodec.encode(remoteParams).require).require.value - assert(decodedLocalParams == localParams.copy(initialRequestedChannelReserve_opt = None)) - assert(decodedRemoteParams == remoteParams.copy(initialRequestedChannelReserve_opt = None)) - } - } - - test("encode/decode optional shutdown script") { - val codec = remoteParamsCodec(ChannelFeatures()) - val remoteParams = RemoteParams( - randomKey().publicKey, - Satoshi(600), - UInt64(123456L), - Some(Satoshi(300)), - MilliSatoshi(1000), - CltvExpiryDelta(42), - 42, - randomKey().publicKey, - randomKey().publicKey, - randomKey().publicKey, - randomKey().publicKey, - Features(ChannelRangeQueries -> Optional, VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory), - None) - assert(codec.decodeValue(codec.encode(remoteParams).require).require == remoteParams) - val remoteParams1 = remoteParams.copy(upfrontShutdownScript_opt = Some(ByteVector.fromValidHex("deadbeef"))) - assert(codec.decodeValue(codec.encode(remoteParams1).require).require == remoteParams1) - - val dataWithoutRemoteShutdownScript = normal.modify(_.commitments.params.remoteParams).setTo(remoteParams) - assert(channelDataCodec.decode(channelDataCodec.encode(dataWithoutRemoteShutdownScript).require).require.value == dataWithoutRemoteShutdownScript) - - val dataWithRemoteShutdownScript = normal.modify(_.commitments.params.remoteParams).setTo(remoteParams1) - assert(channelDataCodec.decode(channelDataCodec.encode(dataWithRemoteShutdownScript).require).require.value == dataWithRemoteShutdownScript) - } +class ChannelCodecs5Spec extends AnyFunSuite { test("encode/decode rbf status") { val channelId = randomBytes32() - val fundingInput = InputInfo(OutPoint(randomTxId(), 3), TxOut(175_000 sat, Script.pay2wpkh(randomKey().publicKey)), Nil) val fundingTx = SharedTransaction( sharedInput_opt = None, sharedOutput = InteractiveTxBuilder.Output.Shared(UInt64(8), ByteVector.empty, 100_000_600 msat, 74_000_400 msat, 0 msat), @@ -132,15 +29,13 @@ class ChannelCodecs4Spec extends AnyFunSuite { localOutputs = Nil, remoteOutputs = Nil, lockTime = 0 ) - val commitTx = CommitTx( - fundingInput, - Transaction(2, Seq(TxIn(fundingInput.outPoint, Nil, 0)), Seq(TxOut(150_000 sat, Script.pay2wpkh(randomKey().publicKey))), 0), - ) val waitingForSigs = InteractiveTxSigningSession.WaitingForSigs( - InteractiveTxParams(channelId, isInitiator = true, 100_000 sat, 75_000 sat, None, randomKey().publicKey, Nil, 0, 330 sat, FeeratePerKw(500 sat), RequireConfirmedInputs(forLocal = false, forRemote = false)), + InteractiveTxParams(channelId, isInitiator = true, 100_000 sat, 75_000 sat, None, randomKey().publicKey, Nil, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat, 0, 330 sat, FeeratePerKw(500 sat), RequireConfirmedInputs(forLocal = false, forRemote = false)), fundingTxIndex = 0, PartiallySignedSharedTransaction(fundingTx, TxSignatures(channelId, randomTxId(), Nil)), - Left(UnsignedLocalCommit(0, CommitmentSpec(Set.empty, FeeratePerKw(1000 sat), 100_000_000 msat, 75_000_000 msat), commitTx, Nil)), + CommitParams(330 sat, 1 msat, UInt64.MaxValue, 30, CltvExpiryDelta(720)), + Left(UnsignedLocalCommit(0, CommitmentSpec(Set.empty, FeeratePerKw(1000 sat), 100_000_000 msat, 75_000_000 msat), randomTxId())), + CommitParams(500 sat, 1000 msat, UInt64.MaxValue, 483, CltvExpiryDelta(144)), RemoteCommit(0, CommitmentSpec(Set.empty, FeeratePerKw(1000 sat), 75_000_000 msat, 100_000_000 msat), randomTxId(), randomKey().publicKey), Some(LiquidityAds.PurchaseBasicInfo(isBuyer = true, 100_000 sat, LiquidityAds.Fees(1000 sat, 500 sat))), ) @@ -159,75 +54,30 @@ class ChannelCodecs4Spec extends AnyFunSuite { } } - test("decode unconfirmed dual funded") { - // data encoded with the previous version of eclair, when Shared.Input did not include a pubkey script - val raw = ByteVector.fromValidHex("0x020001ff02000000000000002a2400000000000000000000000000000000000000000000000000000000000000000000000000003039000000000000006400000000000000c8000000000000012c02000000000000002b04deadbeef000000000000006400000000000000c8000000000000012c00000000000000000000000042000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003e80000000000000000000000000000000000000000000000000000000000000000ff000000000000006400000000000000c8ff0001240000000000000000000000000000000000000000000000000000000000000000000000002be803000000000000220020eb72e573a9513d982a01f0e6a6b53e92764db81a0c26d2be94c5fc5b69a0db7d475221024d4b6cd1361032ca9bd2aeb9d900aa4d45d9ead80ac9423374c451a7254d076621031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f52ae00000000024d4b6cd1361032ca9bd2aeb9d900aa4d45d9ead80ac9423374c451a7254d0766031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f000000000000000000000000014a000002ee0000") - val decoded = fundingTxStatusCodec.decode(raw.bits).require.value.asInstanceOf[LocalFundingStatus.DualFundedUnconfirmedFundingTx] - - // check that our codec will set the pubkeyscript using the one from the funding params - val channelId = ByteVector32.Zeroes - val script = Scripts.multiSig2of2(PrivateKey(ByteVector.fromValidHex("01" * 32)).publicKey, PrivateKey(ByteVector.fromValidHex("02" * 32)).publicKey) - val dualFundedUnconfirmedFundingTx = DualFundedUnconfirmedFundingTx( - PartiallySignedSharedTransaction( - SharedTransaction( - // we include the correct pubkey script here - Some(InteractiveTxBuilder.Input.Shared(UInt64(42), OutPoint(TxId(ByteVector32.Zeroes), 0), Script.write(Script.pay2wsh(script)), 12345L, MilliSatoshi(100), MilliSatoshi(200), MilliSatoshi(300))), - sharedOutput = InteractiveTxBuilder.Output.Shared(UInt64(43), ByteVector.fromValidHex("deadbeef"), MilliSatoshi(100), MilliSatoshi(200), MilliSatoshi(300)), - localInputs = Nil, remoteInputs = Nil, localOutputs = Nil, remoteOutputs = Nil, lockTime = 0 - ), - localSigs = TxSignatures(channelId, TxId(ByteVector32.Zeroes), Nil) - ), - createdAt = BlockHeight(1000), - fundingParams = InteractiveTxParams(channelId = channelId, isInitiator = true, localContribution = 100.sat, remoteContribution = 200.sat, - sharedInput_opt = Some(InteractiveTxBuilder.Multisig2of2Input( - InputInfo(OutPoint(TxId(ByteVector32.Zeroes), 0), TxOut(1000.sat, Script.pay2wsh(script)), script), - 0, - PrivateKey(ByteVector.fromValidHex("02" * 32)).publicKey - )), - remoteFundingPubKey = PrivateKey(ByteVector.fromValidHex("01" * 32)).publicKey, - localOutputs = Nil, lockTime = 0, dustLimit = 330.sat, targetFeerate = FeeratePerKw(FeeratePerByte(3.sat)), requireConfirmedInputs = RequireConfirmedInputs(forLocal = false, forRemote = false)), - liquidityPurchase_opt = None - ) - assert(decoded == dualFundedUnconfirmedFundingTx) - - val dualFundedUnconfirmedFundingTx1 = dualFundedUnconfirmedFundingTx.copy( - liquidityPurchase_opt = Some(LiquidityAds.PurchaseBasicInfo(isBuyer = true, 250_000 sat, LiquidityAds.Fees(1500 sat, 700 sat))) - ) - assert(fundingTxStatusCodec.decode(fundingTxStatusCodec.encode(dualFundedUnconfirmedFundingTx1).require).require.value == dualFundedUnconfirmedFundingTx1) - } - - test("decode local params pay commit tx fees field") { - // The data in this test was encoded using eclair v0.10.0, where a single is_initiator boolean was encoded instead - // of two separate booleans (is_channel_opener and pay_commit_tx_fees). - val initiator = hex"000e0158e6936235824897192ae96caea018191fd812bc94f8421b65bd476faf9e30d901010002aaaa00ce2f18a967dc4f25f414e671ba6585f8ef0b8c5fb812c21064f55a2eaa0009828b4e829a55e78d7711e93ba9a9502dd7a6d0ef2910258323efe35533ddff0f80000001000000000000044c000000001dcd65000000000000002710000000000000000000900064ff000000186b02000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020a498202bbbb671d15145722fb8c28d732cddb249bcc6652ed2b297ff1f77a18371b1e6300000000000003e8000000003b9aca000000000000004e2000000000000003e80090001e02e89c4b89177eb7cd291a21c610e9ac4445a558430e9943767481e29d1d1d790902a10d318e979d1b74de10cb66f716126ca0accd1922a0e3cdae90bdbfe637772802b2b6c791a935a20ce98a0b349e1ffb7a88121478f5dc39d3715b2416878d1d35039c795469e7814b454aee2d36f3c33ef56c34192ab49736c74d02a6697ece8f7b00000004020a498200000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000025b9fd5132058bc95861f228b98d7a6b6ff341ae7492e3f91978ed745470d27a6065e020000000101010101010101010101010101010101010101010101010101010101010101012a00000000ffffffff0140420f00000000002200209997cca7a6aed2533b0014929deeb88341516dcb5d01fbd1071681fa566650960000000000010000000000000000000000002710000000002faf0800000000000bebc2002458e6936235824897192ae96caea018191fd812bc94f8421b65bd476faf9e30d9000000002b40420f00000000002200209997cca7a6aed2533b0014929deeb88341516dcb5d01fbd1071681fa56665096475221025b9fd5132058bc95861f228b98d7a6b6ff341ae7492e3f91978ed745470d27a62102b90d8c2b072181a56fd6fbd5be9e1f723dee869d79ef4278450b01f09c3fa37f52ae7d020000000158e6936235824897192ae96caea018191fd812bc94f8421b65bd476faf9e30d90000000000deefa28002400d030000000000160014b2e9d9708cd135eec2ffc2dce8c4860b4b41e8b4b8180c0000000000220020de29e3b845619436603ee692c2d8a56a116599bf6ec1332e7ec51d09e26f7bc5123a44209c41e27950a3cc928116474ff4c92279124c00919e37174cb6e5011c208ca69807d936ecb290f93c797592d709ae72672e151aad4c15929c5c649e761f5fa05000000000000000000000000000002710000000000bebc200000000002faf0800ce0de659e08f34dda3a8710267d96bb0af73fd78f559efa3cc8f846f2f6dba1f03d64b4a3d14f322bdad603bfb9359cd08d2b9b0efce54aa0fe787b2e3406c26a2000000ff0338f1d042dcb5dcfb7b0b29c58fd3ecb9d9b0957fa03703bf515433854800b1f100000000000001061a8000002a000000010289b8c594531eebff023976e44871e45500885e48c0ebe39357451ff3914d0a6648d576d4d62b8274d38ab1b41691c78d68562764c8771495b2517fe7fff8d75fd7622d54c466ac93baee1d6fac4a53ddef0d06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f0289b8c594531eeb6605b1300300009000000000000003e8000858b800000014000000001dcd650000000001" - val initiatorDecoded = channelDataCodec.decode(initiator.bits).require.value.asInstanceOf[DATA_NORMAL] - assert(initiatorDecoded.commitments.params.localParams.isChannelOpener) - assert(initiatorDecoded.commitments.params.localParams.paysCommitTxFees) - val nonInitiator = hex"000e0158e6936235824897192ae96caea018191fd812bc94f8421b65bd476faf9e30d901010002bbbb671d15145722fb8c28d732cddb249bcc6652ed2b297ff1f77a18371b1e63000927012067c2958dc0317a439185b5619e15c528726ebb2b75b4e6ae80c89468a28000000000000000000003e8000000003b9aca000000000000004e2000000000000003e80090001e000000000004020a498202aaaa00ce2f18a967dc4f25f414e671ba6585f8ef0b8c5fb812c21064f55a2eaa000000000000044c000000001dcd6500000000000000271000000000000000000090006402a69186cd9e36bcb3dd3e92be5e1224984239175c4e3afdf7d6a14023042dc5bd03c191e410e8545c6fc8aeee9aeb5a10cec30c03e950b9920b2474eb09e1cab70e037ca17e6afb2fab6e03fa4aac50be6d9beffea390761ee2ff6ef336e2783101c0036e0b2571509ce82bd31dfb1e810c1c2c294430a02d58fe997f4a7a2d06508ccc0000186b02000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020a49820000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000002b90d8c2b072181a56fd6fbd5be9e1f723dee869d79ef4278450b01f09c3fa37f065e020000000101010101010101010101010101010101010101010101010101010101010101012a00000000ffffffff0140420f00000000002200209997cca7a6aed2533b0014929deeb88341516dcb5d01fbd1071681fa566650960000000000010000000000000000000000002710000000000bebc200000000002faf08002458e6936235824897192ae96caea018191fd812bc94f8421b65bd476faf9e30d9000000002b40420f00000000002200209997cca7a6aed2533b0014929deeb88341516dcb5d01fbd1071681fa56665096475221025b9fd5132058bc95861f228b98d7a6b6ff341ae7492e3f91978ed745470d27a62102b90d8c2b072181a56fd6fbd5be9e1f723dee869d79ef4278450b01f09c3fa37f52ae7d020000000158e6936235824897192ae96caea018191fd812bc94f8421b65bd476faf9e30d90000000000deefa28002400d0300000000002200201f9ecd10ef79baef28367302176824ca2a62bd566e18cb705bf5286d2e667eefb8180c000000000016001490c2a2723c9873ec2931c49515e64a610967039e123a44200cebdab89f4b171f63558d1f30e9416b21907adbb0208e7288dfa61bb6b702852ba3acd0569e913ec5d7ef5b6e350d82bac30cfecb08ffffb0742f1948736bbe00000000000000000000000000002710000000002faf0800000000000bebc200a45281427a62b46937d6209986950c76616f874f3d266cb796ec202d441e314302f05e6396357b0a0dc6467f5324760e66eab5dbc0c0875e183742672ddc47ec0a000000ff032af65d7da1b3c4c61145c759eb36296e1b5608c909e0bcd761d1908ac3c571ce00000000000001061a8000002a00000001023976e44871e455ff0289b8c594531eeb0088cdd6527cef83026f1a2ff2e626bdfd94aa2eddd11450b2ab6dca617af53db0c91c81dafe1bbe47757c62eb4709d5f9fb27c456409dff7d1d610aed6ffad1be9a06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f023976e44871e4556605b12f030100900000000000000000000858b800000014000000001dcd650000000001" - val nonInitiatorDecoded = channelDataCodec.decode(nonInitiator.bits).require.value.asInstanceOf[DATA_NORMAL] - assert(!nonInitiatorDecoded.commitments.params.localParams.isChannelOpener) - assert(!nonInitiatorDecoded.commitments.params.localParams.paysCommitTxFees) - } - - test("encode/decode cold origins") { - val origins = Map( - 13L -> Origin.Cold(Upstream.Cold.Channel(randomBytes32(), 2765, 1863 msat)), - 27L -> Origin.Cold(Upstream.Cold.Trampoline(Upstream.Cold.Channel(randomBytes32(), 541, 6500 msat) :: Nil)), - 28L -> Origin.Cold(Upstream.Cold.Trampoline(Upstream.Cold.Channel(randomBytes32(), 0, 0 msat) :: Upstream.Cold.Channel(randomBytes32(), 6778, 250_000_001 msat) :: Nil)), - ) - val encoded = originsMapCodec.encode(origins).require - val decoded = originsMapCodec.decode(encoded).require.value - assert(decoded == origins) + test("encode/decode optional shutdown script") { + val remoteParams = RemoteChannelParams( + randomKey().publicKey, + Some(300_000 sat), + randomKey().publicKey, + randomKey().publicKey, + randomKey().publicKey, + randomKey().publicKey, + Features(ChannelRangeQueries -> Optional, VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory), + None) + assert(remoteChannelParamsCodec.decodeValue(remoteChannelParamsCodec.encode(remoteParams).require).require == remoteParams) + val remoteParams1 = remoteParams.copy(upfrontShutdownScript_opt = Some(ByteVector.fromValidHex("deadbeef"))) + assert(remoteChannelParamsCodec.decodeValue(remoteChannelParamsCodec.encode(remoteParams1).require).require == remoteParams1) } - test("decode origin backwards compatibility") { - // The following values were encoded with eclair v0.10.0. - val channelRelayed = Origin.Cold(Upstream.Cold.Channel(ByteVector32(hex"fe99ac49738d44ff8b73d8e5da01e868584a2071326320e8f51fe8bdbbe84c64"), 1561, 1721 msat)) - val channelRelayedBin = hex"0002fe99ac49738d44ff8b73d8e5da01e868584a2071326320e8f51fe8bdbbe84c64000000000000061900000000000006b900000000000005dc" - assert(originCodec.decode(channelRelayedBin.bits).require.value == channelRelayed) - val trampolineRelayed = Origin.Cold(Upstream.Cold.Trampoline(Upstream.Cold.Channel(ByteVector32(hex"19a3e2f5b1d747e6e59cec57d927c068c976eab0b914a1bf66aaacaa0917d49d"), 17, 0 msat) :: Upstream.Cold.Channel(ByteVector32(hex"4fbf1090bf27e4ef3af8b784f8e0e62dd2fc836775131d6e58400a68ec8fcf2c"), 21, 0 msat) :: Nil)) - val trampolineRelayedBin = hex"0004000219a3e2f5b1d747e6e59cec57d927c068c976eab0b914a1bf66aaacaa0917d49d00000000000000114fbf1090bf27e4ef3af8b784f8e0e62dd2fc836775131d6e58400a68ec8fcf2c0000000000000015" - assert(originCodec.decode(trampolineRelayedBin.bits).require.value == trampolineRelayed) + test("decode next remote commit with local sig") { + // We previously included our commit_sig in the next remote commit, which was then removed. + val data = hex"000601f43830d0f526556303b25738bde42d88c28efdc91d3919f829beaff30b258c7901010002bbbb671d15145722fb8c28d732cddb249bcc6652ed2b297ff1f77a18371b1e630009e4339e114c4b79062494bccd4c68e42513ee8cc63b1d50101e87ece4caf7442f80000000ff0000000000004e20000000000000140800000000000000000000000000100802aa698202aaaa00ce2f18a967dc4f25f414e671ba6585f8ef0b8c5fb812c21064f55a2eaaff000000000000271003848a1c528b43bbf9d5b41726af470bec7a70e723e019cc66d60f5fd604da46ee038b6665469260317a7916d408aa8183fc7b3cbf3cb221296976e9ba251759f7f802662a75915a93fd734c3dd0fd45aeb0955086a76d2ca9fe663574677abfc9fe1a02d74e2de9a063e435ebfdca09bdb45e38bd3598d2c5f546064d164fcf2355acad0000186b0200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000180802aa6982000000000002fd05b30080f43830d0f526556303b25738bde42d88c28efdc91d3919f829beaff30b258c7900000000000000000000000000989680e39773295a0d4ca41f8d3296f747c62bcb4ab6fb450cc132ecc62bfa4b5e19f200061b1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fe0001a1470107fd05b30080f43830d0f526556303b25738bde42d88c28efdc91d3919f829beaff30b258c7900000000000000010000000000989680e39773295a0d4ca41f8d3296f747c62bcb4ab6fb450cc132ecc62bfa4b5e19f200061b1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fe0001a1470107000000000000000000000000000000020000000000000002000401fd05b1f43830d0f526556303b25738bde42d88c28efdc91d3919f829beaff30b258c7900000000000000000000000000989680e39773295a0d4ca41f8d3296f747c62bcb4ab6fb450cc132ecc62bfa4b5e19f200061b1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fe0001a147010701fd05b1f43830d0f526556303b25738bde42d88c28efdc91d3919f829beaff30b258c7900000000000000010000000000989680e39773295a0d4ca41f8d3296f747c62bcb4ab6fb450cc132ecc62bfa4b5e19f200061b1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fe0001a147010702fd05b1f43830d0f526556303b25738bde42d88c28efdc91d3919f829beaff30b258c7900000000000000000000000000989680e39773295a0d4ca41f8d3296f747c62bcb4ab6fb450cc132ecc62bfa4b5e19f200061b1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fe0001a147010702fd05b1f43830d0f526556303b25738bde42d88c28efdc91d3919f829beaff30b258c7900000000000000010000000000989680e39773295a0d4ca41f8d3296f747c62bcb4ab6fb450cc132ecc62bfa4b5e19f200061b1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fe0001a1470107000100000000000000000000000024f43830d0f526556303b25738bde42d88c28efdc91d3919f829beaff30b258c790000000000000000000f424003aa939f1ba99432524ba89981618690cb137b1fd20b7c00fe594bf8204404ad910400012401010101010101010101010101010101010101010101010101010101010101012a0000002b40420f0000000000220020b75be8b30696f7138c5744ea32dd1eaecad1ba9fb21fbd73ee05fa4ef7795af1061a8000002a00000000020200000000000003e800000000000003e8000000003b9aca00001e009000000000000000010002010000000000000000010000000000000001000009c4000000000bebc200000000002e7ddb00f7a749de90158ec0334c50145e83d443039b1f4cc61c65f5c089ffcae175b44601be0cadd4d166f142a11de8b4e035c2c5a7ceb4a3160196a6e7500551b96b58b22d07281e988fa4dc032197294185e512373e9f27870bd535d4fe2e1610cec80800026ecda215289729225586be698c3386d2abf72854e30c92f06ca4bb525851e9563f996e488934cf2895ac5873b2e295f28299e0795b8d158ca714c43b6488ec6ed68f2055cc2bf023c842ff052ce588023618afcecec7c53858a9e6bd5c9e9b2c4b5cd2559bd7c3df2a2178852c85c1d3cfddc42c1ef9b2ea9d36310d5dae7619000000000000044c0000000000000000000000001dcd6500006402d000000000000000010002020000000000000000020000000000000001000009c4000000002e7ddb00000000000bebc2009ea3b732ba139ec68ea953e89c7f8c27bfb21627fb59fed8729267d24aae6cac03b04d85469db4306e69392111b1f5c7a39aad2303626755c44677712994a4a45afffd0162f43830d0f526556303b25738bde42d88c28efdc91d3919f829beaff30b258c79b1f4f39e8e7f8135319254c2e8c730cdeb4c0319b96e201b2196bcb90a05ab941af4409cd0c5b5146c6407cae9a46729bb8316a6feeb676bbab67e06bd7c76530004b1dd884af8abb418a8dca68e57ef7335c456c778d19e74d99bda5f5c762bbb1a28430d6461a6e5a4bcdaee10f1c1c31ef9ab2b8c7ac56f0520fc527adf58893a58a3e6eda1952b22766bf95c87ba4da9035a7a9f6b3e9d3a2b8ac2500c8c4e1f3355aeea738a9e15b655e60cf8c3ce026e5b291ed9306734a5ffc157bd0221b26fff6c51847f1be721883e4cc06106c6c865b15f187858e9c436b1f8f73d30c4640649a82be3e331f576d821c8ce47c3a4db0ab4e5e686ce4e4696944b8dc560626d6190bc1be9edf4f15a45cde70d6ff2bb95c8cab38d621302e5083dd477b50dd7f19b11893d174219a959ada6dadb720877daf3e811d6b8b0aa9ab99ccc1800000000000000020004020000000000000000020000000000000001010000000000000000010000000000000001000009c4000000002e7ddb00000000000aba950096697cdae6d868d426cb707b6a5317f9d88dd0a36c52733812f30bf9f85ec42f02ce20153b619ef455bc2c17ef261592d07ac330736c411bdf9af30bbb636d7b4e0000000000000000000001000100400000ffffffffffff00207d73dcf750c035b8829d403a3c3920527cf6f3d1b9b14ba683ffe6967786c0ee80007fffffffffff80000200000000000000000001008f963fab2e443aacdd1a4e2759006800000000000000010001008f963fab2e443aacdd1a4e275900680000010067eaf14e46c2abff012e5f30f125b4680088850e1466aca19c286a653a27d75ced3648dc39a8f2b558f3a22f5224896b3c6c177c29f969d35fb33b8260d4b68e5c36990e8f2d00cc7438ccb7b6b0fd92992206226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f0067eaf14e46c2ab68bec30f030100900000000000000000000858b800000014000000001dcd650001000000" + val decoded = channelDataCodec.decode(data.bits).require.value.asInstanceOf[DATA_NORMAL] + assert(decoded.commitments.remoteNextCommitInfo == Left(WaitForRev(1))) + val reEncoded = channelDataCodec.encode(decoded).require.bytes + assert(reEncoded.size < data.size) // we don't encode the commit_sig anymore + val reDecoded = channelDataCodec.decode(reEncoded.bits).require.value.asInstanceOf[DATA_NORMAL] + assert(decoded == reDecoded) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/CommonCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/CommonCodecsSpec.scala index 7ac2c988f9..ec7cae41b5 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/CommonCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/CommonCodecsSpec.scala @@ -23,7 +23,7 @@ import fr.acinq.eclair.blockchain.fee.FeeratePerKw import fr.acinq.eclair.channel.ChannelFlags import fr.acinq.eclair.crypto.Hmac256 import fr.acinq.eclair.wire.protocol.CommonCodecs._ -import fr.acinq.eclair.{UInt64, randomBytes32} +import fr.acinq.eclair.{MilliSatoshi, MilliSatoshiLong, UInt64, randomBytes32} import org.scalatest.funsuite.AnyFunSuite import scodec.DecodeResult import scodec.bits.{BinStringSyntax, BitVector, HexStringSyntax} @@ -139,6 +139,22 @@ class CommonCodecsSpec extends AnyFunSuite { } } + test("encode/decode millisatoshi amounts") { + val testCases = Seq( + 0 msat, + 1 msat, + 100_000 msat, + 250_000_000 msat, + MilliSatoshi.MaxMoney - 1.msat, + MilliSatoshi.MaxMoney, + Long.MaxValue.msat - 1.msat, + Long.MaxValue.msat, + ) + testCases.foreach { amount => + assert(millisatoshi.decode(millisatoshi.encode(amount).require).require.value == amount) + } + } + test("encode/decode channel flags") { val testCases = Map( bin"00000000" -> ChannelFlags(nonInitiatorPaysCommitFees = false, announceChannel = false), diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecsSpec.scala index ec318af73c..589ca88ece 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecsSpec.scala @@ -18,14 +18,18 @@ package fr.acinq.eclair.wire.protocol import com.google.common.base.Charsets import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} +import fr.acinq.bitcoin.scalacompat.Musig2.IndividualNonce import fr.acinq.bitcoin.scalacompat.{Block, BlockHash, ByteVector32, ByteVector64, OutPoint, SatoshiLong, Script, ScriptWitness, Transaction, TxHash, TxId} import fr.acinq.eclair.FeatureSupport.Optional import fr.acinq.eclair.Features.DataLossProtect import fr.acinq.eclair.TestUtils.randomTxId import fr.acinq.eclair._ import fr.acinq.eclair.blockchain.fee.FeeratePerKw +import fr.acinq.eclair.channel.ChannelSpendSignature.{IndividualSignature, PartialSignatureWithNonce} +import fr.acinq.eclair.channel.ChannelTypes.SimpleTaprootChannelsPhoenix import fr.acinq.eclair.channel.{ChannelFlags, ChannelTypes} import fr.acinq.eclair.json.JsonSerializers +import fr.acinq.eclair.reputation.Reputation import fr.acinq.eclair.router.Announcements import fr.acinq.eclair.transactions.Scripts import fr.acinq.eclair.wire.protocol.ChannelTlv._ @@ -139,10 +143,14 @@ class LightningMessageCodecsSpec extends AnyFunSuite { test("nonreg generic tlv") { val channelId = randomBytes32() + val partialSig = randomBytes32() val signature = randomBytes64() val key = randomKey() val point = randomKey().publicKey val txId = randomTxId() + val nextTxId = randomTxId() + val nonce = IndividualNonce(randomBytes(66)) + val nextNonce = IndividualNonce(randomBytes(66)) val randomData = randomBytes(42) val tlvTag = UInt64(hex"47010000") @@ -155,18 +163,24 @@ class LightningMessageCodecsSpec extends AnyFunSuite { hex"0088" ++ channelId ++ hex"0001020304050607 0809aabbccddeeff" ++ key.value ++ point.value ++ hex"00 20" ++ txId.value.reverse -> ChannelReestablish(channelId, 0x01020304050607L, 0x0809aabbccddeeffL, key, point, TlvStream(ChannelReestablishTlv.NextFundingTlv(txId))), hex"0088" ++ channelId ++ hex"0001020304050607 0809aabbccddeeff" ++ key.value ++ point.value ++ hex"01 20" ++ txId.value.reverse -> ChannelReestablish(channelId, 0x01020304050607L, 0x0809aabbccddeeffL, key, point, TlvStream(ChannelReestablishTlv.YourLastFundingLockedTlv(txId))), hex"0088" ++ channelId ++ hex"0001020304050607 0809aabbccddeeff" ++ key.value ++ point.value ++ hex"03 20" ++ txId.value.reverse -> ChannelReestablish(channelId, 0x01020304050607L, 0x0809aabbccddeeffL, key, point, TlvStream(ChannelReestablishTlv.MyCurrentFundingLockedTlv(txId))), + hex"0088" ++ channelId ++ hex"0001020304050607 0809aabbccddeeff" ++ key.value ++ point.value ++ hex"18 42" ++ nonce.data -> ChannelReestablish(channelId, 0x01020304050607L, 0x0809aabbccddeeffL, key, point, TlvStream(ChannelReestablishTlv.CurrentCommitNonceTlv(nonce))), + hex"0088" ++ channelId ++ hex"0001020304050607 0809aabbccddeeff" ++ key.value ++ point.value ++ hex"16 c4" ++ txId.value.reverse ++ nonce.data ++ nextTxId.value.reverse ++ nextNonce.data -> ChannelReestablish(channelId, 0x01020304050607L, 0x0809aabbccddeeffL, key, point, TlvStream(ChannelReestablishTlv.NextLocalNoncesTlv(Seq(txId -> nonce, nextTxId -> nextNonce)))), hex"0088" ++ channelId ++ hex"0001020304050607 0809aabbccddeeff" ++ key.value ++ point.value ++ hex"fe47010000 00" -> ChannelReestablish(channelId, 0x01020304050607L, 0x0809aabbccddeeffL, key, point, TlvStream[ChannelReestablishTlv](Set.empty[ChannelReestablishTlv], Set(GenericTlv(tlvTag, ByteVector.empty)))), hex"0088" ++ channelId ++ hex"0001020304050607 0809aabbccddeeff" ++ key.value ++ point.value ++ hex"fe47010000 07 bbbbbbbbbbbbbb" -> ChannelReestablish(channelId, 0x01020304050607L, 0x0809aabbccddeeffL, key, point, TlvStream[ChannelReestablishTlv](Set.empty[ChannelReestablishTlv], Set(GenericTlv(tlvTag, hex"bbbbbbbbbbbbbb")))), - hex"0084" ++ channelId ++ signature ++ hex"0000" -> CommitSig(channelId, signature, Nil), - hex"0084" ++ channelId ++ signature ++ hex"0000 fe47010000 00" -> CommitSig(channelId, signature, Nil, TlvStream[CommitSigTlv](Set.empty[CommitSigTlv], Set(GenericTlv(tlvTag, ByteVector.empty)))), - hex"0084" ++ channelId ++ signature ++ hex"0000 fe47010000 07 cccccccccccccc" -> CommitSig(channelId, signature, Nil, TlvStream[CommitSigTlv](Set.empty[CommitSigTlv], Set(GenericTlv(tlvTag, hex"cccccccccccccc")))), + hex"0084" ++ channelId ++ signature ++ hex"0000" -> CommitSig(channelId, IndividualSignature(signature), Nil), + hex"0084" ++ channelId ++ ByteVector64.Zeroes ++ hex"0000" ++ hex"02 62" ++ partialSig ++ nonce.data -> CommitSig(channelId, PartialSignatureWithNonce(partialSig, nonce), Nil, batchSize = 1), + hex"0084" ++ channelId ++ signature ++ hex"0000 fe47010000 00" -> CommitSig(channelId, IndividualSignature(signature), Nil, TlvStream[CommitSigTlv](Set.empty[CommitSigTlv], Set(GenericTlv(tlvTag, ByteVector.empty)))), + hex"0084" ++ channelId ++ signature ++ hex"0000 fe47010000 07 cccccccccccccc" -> CommitSig(channelId, IndividualSignature(signature), Nil, TlvStream[CommitSigTlv](Set.empty[CommitSigTlv], Set(GenericTlv(tlvTag, hex"cccccccccccccc")))), hex"0085" ++ channelId ++ key.value ++ point.value -> RevokeAndAck(channelId, key, point), + hex"0085" ++ channelId ++ key.value ++ point.value ++ hex"16 62" ++ txId.value.reverse ++ nonce.data -> RevokeAndAck(channelId, key, point, Seq(txId -> nonce)), + hex"0085" ++ channelId ++ key.value ++ point.value ++ hex"16 c4" ++ txId.value.reverse ++ nonce.data ++ nextTxId.value.reverse ++ nextNonce.data -> RevokeAndAck(channelId, key, point, Seq(txId -> nonce, nextTxId -> nextNonce)), hex"0085" ++ channelId ++ key.value ++ point.value ++ hex" fe47010000 00" -> RevokeAndAck(channelId, key, point, TlvStream[RevokeAndAckTlv](Set.empty[RevokeAndAckTlv], Set(GenericTlv(tlvTag, ByteVector.empty)))), hex"0085" ++ channelId ++ key.value ++ point.value ++ hex" fe47010000 07 cccccccccccccc" -> RevokeAndAck(channelId, key, point, TlvStream[RevokeAndAckTlv](Set.empty[RevokeAndAckTlv], Set(GenericTlv(tlvTag, hex"cccccccccccccc")))), hex"0026" ++ channelId ++ hex"002a" ++ randomData -> Shutdown(channelId, randomData), + hex"0026" ++ channelId ++ hex"002a" ++ randomData ++ hex"08 42" ++ nonce.data -> Shutdown(channelId, randomData, nonce), hex"0026" ++ channelId ++ hex"002a" ++ randomData ++ hex"fe47010000 00" -> Shutdown(channelId, randomData, TlvStream[ShutdownTlv](Set.empty[ShutdownTlv], Set(GenericTlv(tlvTag, ByteVector.empty)))), hex"0026" ++ channelId ++ hex"002a" ++ randomData ++ hex"fe47010000 07 cccccccccccccc" -> Shutdown(channelId, randomData, TlvStream[ShutdownTlv](Set.empty[ShutdownTlv], Set(GenericTlv(tlvTag, hex"cccccccccccccc")))), @@ -194,27 +208,35 @@ class LightningMessageCodecsSpec extends AnyFunSuite { val channelId1 = ByteVector32(hex"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") val channelId2 = ByteVector32(hex"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb") val signature = ByteVector64(hex"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb") + val partialSig = ByteVector32(hex"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb") // This is a random mainnet transaction. val txBin1 = hex"020000000001014ade359c5deb7c1cde2e94f401854658f97d7fa31c17ce9a831db253120a0a410100000017160014eb9a5bd79194a23d19d6ec473c768fb74f9ed32cffffffff021ca408000000000017a914946118f24bb7b37d5e9e39579e4a411e70f5b6a08763e703000000000017a9143638b2602d11f934c04abc6adb1494f69d1f14af8702473044022059ddd943b399211e4266a349f26b3289979e29f9b067792c6cfa8cc5ae25f44602204d627a5a5b603d0562e7969011fb3d64908af90a3ec7c876eaa9baf61e1958af012102f5188df1da92ed818581c29778047800ed6635788aa09d9469f7d17628f7323300000000" val tx1 = Transaction.read(txBin1.toArray) // This is random, longer mainnet transaction. val txBin2 = hex"0200000000010142180a8812fc79a3da7fb2471eff3e22d7faee990604c2ba7f2fc8dfb15b550a0200000000feffffff030f241800000000001976a9146774040642a78ca3b8b395e70f8391b21ec026fc88ac4a155801000000001600148d2e0b57adcb8869e603fd35b5179caf053361253b1d010000000000160014e032f4f4b9f8611df0d30a20648c190c263bbc33024730440220506005aa347f5b698542cafcb4f1a10250aeb52a609d6fd67ef68f9c1a5d954302206b9bb844343f4012bccd9d08a0f5430afb9549555a3252e499be7df97aae477a012103976d6b3eea3de4b056cd88cdfd50a22daf121e0fb5c6e45ba0f40e1effbd275a00000000" val tx2 = Transaction.read(txBin2.toArray) + val nonce = IndividualNonce(hex"2062534ccb3be5a8997843f3b6bc530a94cbc60eceb538674ceedd62d8be07f2dfa5df6acf3ded7444268d56925bb2c33afe71a55f4fa88f3985451a681415930f6b") + val nextNonce = IndividualNonce(hex"b218b34786408f0a1aee2b35a0e860aa234b8013d1c385d1fcb4583fc4472bedfdd69a53c71006ec9f8b33724b719a50aa137814f4d0c00caff4e1da0d9856a957e7") + val fundingNonce = IndividualNonce(hex"a49ff67b08c720b993c946556cde1be1c3b664bc847c4792135dfd6ef0986e00e9871808c6620b0420567dad525b27431453d4434fd326f8ac56496639b72326eb5d") val fundingRate = LiquidityAds.FundingRate(25_000 sat, 250_000 sat, 750, 150, 50 sat, 500 sat) val testCases = Seq( TxAddInput(channelId1, UInt64(561), Some(tx1), 1, 5) -> hex"0042 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000231 00f7 020000000001014ade359c5deb7c1cde2e94f401854658f97d7fa31c17ce9a831db253120a0a410100000017160014eb9a5bd79194a23d19d6ec473c768fb74f9ed32cffffffff021ca408000000000017a914946118f24bb7b37d5e9e39579e4a411e70f5b6a08763e703000000000017a9143638b2602d11f934c04abc6adb1494f69d1f14af8702473044022059ddd943b399211e4266a349f26b3289979e29f9b067792c6cfa8cc5ae25f44602204d627a5a5b603d0562e7969011fb3d64908af90a3ec7c876eaa9baf61e1958af012102f5188df1da92ed818581c29778047800ed6635788aa09d9469f7d17628f7323300000000 00000001 00000005", TxAddInput(channelId2, UInt64(0), Some(tx2), 2, 0) -> hex"0042 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 0000000000000000 0100 0200000000010142180a8812fc79a3da7fb2471eff3e22d7faee990604c2ba7f2fc8dfb15b550a0200000000feffffff030f241800000000001976a9146774040642a78ca3b8b395e70f8391b21ec026fc88ac4a155801000000001600148d2e0b57adcb8869e603fd35b5179caf053361253b1d010000000000160014e032f4f4b9f8611df0d30a20648c190c263bbc33024730440220506005aa347f5b698542cafcb4f1a10250aeb52a609d6fd67ef68f9c1a5d954302206b9bb844343f4012bccd9d08a0f5430afb9549555a3252e499be7df97aae477a012103976d6b3eea3de4b056cd88cdfd50a22daf121e0fb5c6e45ba0f40e1effbd275a00000000 00000002 00000000", TxAddInput(channelId1, UInt64(561), Some(tx1), 0, 0) -> hex"0042 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000231 00f7 020000000001014ade359c5deb7c1cde2e94f401854658f97d7fa31c17ce9a831db253120a0a410100000017160014eb9a5bd79194a23d19d6ec473c768fb74f9ed32cffffffff021ca408000000000017a914946118f24bb7b37d5e9e39579e4a411e70f5b6a08763e703000000000017a9143638b2602d11f934c04abc6adb1494f69d1f14af8702473044022059ddd943b399211e4266a349f26b3289979e29f9b067792c6cfa8cc5ae25f44602204d627a5a5b603d0562e7969011fb3d64908af90a3ec7c876eaa9baf61e1958af012102f5188df1da92ed818581c29778047800ed6635788aa09d9469f7d17628f7323300000000 00000000 00000000", TxAddInput(channelId1, UInt64(561), OutPoint(tx1, 1), 5) -> hex"0042 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000231 0000 00000001 00000005 fd0451201f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106", + TxAddInput(channelId1, UInt64(561), None, 1, 0xfffffffdL, TlvStream(TxAddInputTlv.PrevTxOut(tx2.txid, 22_549_834 sat, hex"00148d2e0b57adcb8869e603fd35b5179caf05336125"))) -> hex"0042 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000231 0000 00000001 fffffffd fd04573efc7aa8845f192959202c1b7ff704e7cbddded463c05e844676a94ccb4bed69f1000000000158154a00148d2e0b57adcb8869e603fd35b5179caf05336125", TxAddOutput(channelId1, UInt64(1105), 2047 sat, hex"00149357014afd0ccd265658c9ae81efa995e771f472") -> hex"0043 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000451 00000000000007ff 0016 00149357014afd0ccd265658c9ae81efa995e771f472", TxAddOutput(channelId1, UInt64(1105), 2047 sat, hex"00149357014afd0ccd265658c9ae81efa995e771f472", TlvStream(Set.empty[TxAddOutputTlv], Set(GenericTlv(UInt64(301), hex"2a")))) -> hex"0043 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000451 00000000000007ff 0016 00149357014afd0ccd265658c9ae81efa995e771f472 fd012d012a", TxRemoveInput(channelId2, UInt64(561)) -> hex"0044 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 0000000000000231", TxRemoveOutput(channelId1, UInt64(1)) -> hex"0045 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000001", TxComplete(channelId1) -> hex"0046 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + TxComplete(channelId1, nonce, nextNonce, None) -> hex"0046 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 04 84 2062534ccb3be5a8997843f3b6bc530a94cbc60eceb538674ceedd62d8be07f2dfa5df6acf3ded7444268d56925bb2c33afe71a55f4fa88f3985451a681415930f6b b218b34786408f0a1aee2b35a0e860aa234b8013d1c385d1fcb4583fc4472bedfdd69a53c71006ec9f8b33724b719a50aa137814f4d0c00caff4e1da0d9856a957e7", + TxComplete(channelId1, nonce, nextNonce, Some(fundingNonce)) -> hex"0046 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 04842062534ccb3be5a8997843f3b6bc530a94cbc60eceb538674ceedd62d8be07f2dfa5df6acf3ded7444268d56925bb2c33afe71a55f4fa88f3985451a681415930f6bb218b34786408f0a1aee2b35a0e860aa234b8013d1c385d1fcb4583fc4472bedfdd69a53c71006ec9f8b33724b719a50aa137814f4d0c00caff4e1da0d9856a957e7 0642a49ff67b08c720b993c946556cde1be1c3b664bc847c4792135dfd6ef0986e00e9871808c6620b0420567dad525b27431453d4434fd326f8ac56496639b72326eb5d", TxComplete(channelId1, TlvStream(Set.empty[TxCompleteTlv], Set(GenericTlv(UInt64(231), hex"deadbeef"), GenericTlv(UInt64(507), hex"")))) -> hex"0046 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa e704deadbeef fd01fb00", TxSignatures(channelId1, tx2, Seq(ScriptWitness(Seq(hex"68656c6c6f2074686572652c2074686973206973206120626974636f6e212121", hex"82012088a820add57dfe5277079d069ca4ad4893c96de91f88ffb981fdc6a2a34d5336c66aff87")), ScriptWitness(Seq(hex"304402207de9ba56bb9f641372e805782575ee840a899e61021c8b1572b3ec1d5b5950e9022069e9ba998915dae193d3c25cb89b5e64370e6a3a7755e7f31cf6d7cbc2a49f6d01", hex"034695f5b7864c580bf11f9f8cb1a94eb336f2ce9ef872d2ae1a90ee276c772484"))), None) -> hex"0047 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa fc7aa8845f192959202c1b7ff704e7cbddded463c05e844676a94ccb4bed69f1 0002 004a 022068656c6c6f2074686572652c2074686973206973206120626974636f6e2121212782012088a820add57dfe5277079d069ca4ad4893c96de91f88ffb981fdc6a2a34d5336c66aff87 006b 0247304402207de9ba56bb9f641372e805782575ee840a899e61021c8b1572b3ec1d5b5950e9022069e9ba998915dae193d3c25cb89b5e64370e6a3a7755e7f31cf6d7cbc2a49f6d0121034695f5b7864c580bf11f9f8cb1a94eb336f2ce9ef872d2ae1a90ee276c772484", TxSignatures(channelId2, tx1, Nil, None) -> hex"0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000", - TxSignatures(channelId2, tx1, Nil, Some(signature)) -> hex"0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd0259 40 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + TxSignatures(channelId2, tx1, Nil, Some(IndividualSignature(signature))) -> hex"0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd0259 40 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + TxSignatures(channelId2, tx1, Nil, Some(PartialSignatureWithNonce(partialSig, fundingNonce))) -> hex"0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 02 62 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb a49ff67b08c720b993c946556cde1be1c3b664bc847c4792135dfd6ef0986e00e9871808c6620b0420567dad525b27431453d4434fd326f8ac56496639b72326eb5d", TxInitRbf(channelId1, 8388607, FeeratePerKw(4000 sat)) -> hex"0048 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 007fffff 00000fa0", TxInitRbf(channelId1, 0, FeeratePerKw(4000 sat), 1_500_000 sat, requireConfirmedInputs = true, None) -> hex"0048 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000 00000fa0 0008000000000016e360 0200", TxInitRbf(channelId1, 0, FeeratePerKw(4000 sat), 0 sat, requireConfirmedInputs = false, None) -> hex"0048 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000 00000fa0 00080000000000000000", @@ -239,6 +261,7 @@ class LightningMessageCodecsSpec extends AnyFunSuite { test("encode/decode open_channel") { val defaultOpen = OpenChannel(BlockHash(ByteVector32.Zeroes), ByteVector32.Zeroes, 1 sat, 1 msat, 1 sat, UInt64(1), 1 sat, 1 msat, FeeratePerKw(1 sat), CltvExpiryDelta(1), 1, publicKey(1), point(2), point(3), point(4), point(5), point(6), ChannelFlags(announceChannel = false)) + val nonce = IndividualNonce(hex"2062534ccb3be5a8997843f3b6bc530a94cbc60eceb538674ceedd62d8be07f2dfa5df6acf3ded7444268d56925bb2c33afe71a55f4fa88f3985451a681415930f6b") // Legacy encoding that omits the upfront_shutdown_script and trailing tlv stream. // To allow extending all messages with TLV streams, the upfront_shutdown_script was moved to a TLV stream extension // in https://github.com/lightningnetwork/lightning-rfc/pull/714 and made mandatory when including a TLV stream. @@ -258,29 +281,31 @@ class LightningMessageCodecsSpec extends AnyFunSuite { // non-empty upfront_shutdown_script + unknown odd tlv records defaultEncoded ++ hex"0002 1234 0303010203" -> defaultOpen.copy(tlvStream = TlvStream(Set[OpenChannelTlv](ChannelTlv.UpfrontShutdownScriptTlv(hex"1234")), Set(GenericTlv(UInt64(3), hex"010203")))), // empty upfront_shutdown_script + default channel type - defaultEncoded ++ hex"0000" ++ hex"0100" -> defaultOpen.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(ByteVector.empty), ChannelTlv.ChannelTypeTlv(ChannelTypes.Standard()))), + defaultEncoded ++ hex"0000" ++ hex"0100" -> defaultOpen.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(ByteVector.empty), ChannelTlv.ChannelTypeTlv(ChannelTypes.UnsupportedChannelType(Features.empty)))), // empty upfront_shutdown_script + unsupported channel type defaultEncoded ++ hex"0000" ++ hex"0103501000" -> defaultOpen.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(ByteVector.empty), ChannelTlv.ChannelTypeTlv(ChannelTypes.UnsupportedChannelType(Features(Features.StaticRemoteKey -> FeatureSupport.Mandatory, Features.AnchorOutputs -> FeatureSupport.Mandatory, Features.AnchorOutputsZeroFeeHtlcTx -> FeatureSupport.Mandatory))))), // empty upfront_shutdown_script + channel type - defaultEncoded ++ hex"0000" ++ hex"01021000" -> defaultOpen.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(ByteVector.empty), ChannelTlv.ChannelTypeTlv(ChannelTypes.StaticRemoteKey()))), + defaultEncoded ++ hex"0000" ++ hex"01021000" -> defaultOpen.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(ByteVector.empty), ChannelTlv.ChannelTypeTlv(ChannelTypes.UnsupportedChannelType(Features(Features.StaticRemoteKey -> FeatureSupport.Mandatory))))), // non-empty upfront_shutdown_script + channel type defaultEncoded ++ hex"0004 01abcdef" ++ hex"0103101000" -> defaultOpen.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(hex"01abcdef"), ChannelTlv.ChannelTypeTlv(ChannelTypes.AnchorOutputs()))), defaultEncoded ++ hex"0002 abcd" ++ hex"0103401000" -> defaultOpen.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(hex"abcd"), ChannelTlv.ChannelTypeTlv(ChannelTypes.AnchorOutputsZeroFeeHtlcTx()))), // empty upfront_shutdown_script + channel type (scid-alias) - defaultEncoded ++ hex"0000" ++ hex"0106 400000000000" -> defaultOpen.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(ByteVector.empty), ChannelTlv.ChannelTypeTlv(ChannelTypes.Standard(scidAlias = true)))), - defaultEncoded ++ hex"0000" ++ hex"0106 400000001000" -> defaultOpen.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(ByteVector.empty), ChannelTlv.ChannelTypeTlv(ChannelTypes.StaticRemoteKey(scidAlias = true)))), + defaultEncoded ++ hex"0000" ++ hex"0106 400000000000" -> defaultOpen.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(ByteVector.empty), ChannelTlv.ChannelTypeTlv(ChannelTypes.UnsupportedChannelType(Features(Features.ScidAlias -> FeatureSupport.Mandatory))))), + defaultEncoded ++ hex"0000" ++ hex"0106 400000001000" -> defaultOpen.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(ByteVector.empty), ChannelTlv.ChannelTypeTlv(ChannelTypes.UnsupportedChannelType(Features(Features.StaticRemoteKey -> FeatureSupport.Mandatory, Features.ScidAlias -> FeatureSupport.Mandatory))))), defaultEncoded ++ hex"0000" ++ hex"0106 400000101000" -> defaultOpen.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(ByteVector.empty), ChannelTlv.ChannelTypeTlv(ChannelTypes.AnchorOutputs(scidAlias = true)))), defaultEncoded ++ hex"0000" ++ hex"0106 400000401000" -> defaultOpen.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(ByteVector.empty), ChannelTlv.ChannelTypeTlv(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = true)))), // empty upfront_shutdown_script + channel type (zeroconf) - defaultEncoded ++ hex"0000" ++ hex"0107 04000000000000" -> defaultOpen.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(ByteVector.empty), ChannelTlv.ChannelTypeTlv(ChannelTypes.Standard(zeroConf = true)))), - defaultEncoded ++ hex"0000" ++ hex"0107 04000000001000" -> defaultOpen.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(ByteVector.empty), ChannelTlv.ChannelTypeTlv(ChannelTypes.StaticRemoteKey(zeroConf = true)))), + defaultEncoded ++ hex"0000" ++ hex"0107 04000000000000" -> defaultOpen.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(ByteVector.empty), ChannelTlv.ChannelTypeTlv(ChannelTypes.UnsupportedChannelType(Features(Features.ZeroConf -> FeatureSupport.Mandatory))))), + defaultEncoded ++ hex"0000" ++ hex"0107 04000000001000" -> defaultOpen.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(ByteVector.empty), ChannelTlv.ChannelTypeTlv(ChannelTypes.UnsupportedChannelType(Features(Features.StaticRemoteKey -> FeatureSupport.Mandatory, Features.ZeroConf -> FeatureSupport.Mandatory))))), defaultEncoded ++ hex"0000" ++ hex"0107 04000000101000" -> defaultOpen.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(ByteVector.empty), ChannelTlv.ChannelTypeTlv(ChannelTypes.AnchorOutputs(zeroConf = true)))), defaultEncoded ++ hex"0000" ++ hex"0107 04000000401000" -> defaultOpen.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(ByteVector.empty), ChannelTlv.ChannelTypeTlv(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(zeroConf = true)))), // empty upfront_shutdown_script + channel type (scid-alias + zeroconf) - defaultEncoded ++ hex"0000" ++ hex"0107 04400000000000" -> defaultOpen.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(ByteVector.empty), ChannelTlv.ChannelTypeTlv(ChannelTypes.Standard(scidAlias = true, zeroConf = true)))), - defaultEncoded ++ hex"0000" ++ hex"0107 04400000001000" -> defaultOpen.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(ByteVector.empty), ChannelTlv.ChannelTypeTlv(ChannelTypes.StaticRemoteKey(scidAlias = true, zeroConf = true)))), + defaultEncoded ++ hex"0000" ++ hex"0107 04400000000000" -> defaultOpen.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(ByteVector.empty), ChannelTlv.ChannelTypeTlv(ChannelTypes.UnsupportedChannelType(Features(Features.ScidAlias -> FeatureSupport.Mandatory, Features.ZeroConf -> FeatureSupport.Mandatory))))), + defaultEncoded ++ hex"0000" ++ hex"0107 04400000001000" -> defaultOpen.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(ByteVector.empty), ChannelTlv.ChannelTypeTlv(ChannelTypes.UnsupportedChannelType(Features(Features.StaticRemoteKey -> FeatureSupport.Mandatory, Features.ScidAlias -> FeatureSupport.Mandatory, Features.ZeroConf -> FeatureSupport.Mandatory))))), defaultEncoded ++ hex"0000" ++ hex"0107 04400000101000" -> defaultOpen.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(ByteVector.empty), ChannelTlv.ChannelTypeTlv(ChannelTypes.AnchorOutputs(scidAlias = true, zeroConf = true)))), defaultEncoded ++ hex"0000" ++ hex"0107 04400000401000" -> defaultOpen.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(ByteVector.empty), ChannelTlv.ChannelTypeTlv(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = true, zeroConf = true)))), + // taproot channel type + nonce + defaultEncoded ++ hex"0000" ++ hex"01 17 1000000000000000000000000000000000400000000000" ++ hex"04 42 2062534ccb3be5a8997843f3b6bc530a94cbc60eceb538674ceedd62d8be07f2dfa5df6acf3ded7444268d56925bb2c33afe71a55f4fa88f3985451a681415930f6b" -> defaultOpen.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(ByteVector.empty), ChannelTlv.ChannelTypeTlv(ChannelTypes.SimpleTaprootChannelsStaging(scidAlias = true)), ChannelTlv.NextLocalNonceTlv(nonce))) ) for ((encoded, expected) <- testCases) { @@ -343,6 +368,7 @@ class LightningMessageCodecsSpec extends AnyFunSuite { test("encode/decode accept_channel") { val defaultAccept = AcceptChannel(ByteVector32.Zeroes, 1 sat, UInt64(1), 1 sat, 1 msat, 1, CltvExpiryDelta(1), 1, publicKey(1), point(2), point(3), point(4), point(5), point(6)) + val nonce = IndividualNonce(hex"2062534ccb3be5a8997843f3b6bc530a94cbc60eceb538674ceedd62d8be07f2dfa5df6acf3ded7444268d56925bb2c33afe71a55f4fa88f3985451a681415930f6b") // Legacy encoding that omits the upfront_shutdown_script and trailing tlv stream. // To allow extending all messages with TLV streams, the upfront_shutdown_script was moved to a TLV stream extension // in https://github.com/lightningnetwork/lightning-rfc/pull/714 and made mandatory when including a TLV stream. @@ -351,12 +377,13 @@ class LightningMessageCodecsSpec extends AnyFunSuite { val testCases = Map( defaultEncoded -> defaultAccept, // legacy encoding without upfront_shutdown_script defaultEncoded ++ hex"0000" -> defaultAccept.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(ByteVector.empty))), // empty upfront_shutdown_script - defaultEncoded ++ hex"0000" ++ hex"0100" -> defaultAccept.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(ByteVector.empty), ChannelTlv.ChannelTypeTlv(ChannelTypes.Standard()))), // empty upfront_shutdown_script with channel type + defaultEncoded ++ hex"0000" ++ hex"0100" -> defaultAccept.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(ByteVector.empty), ChannelTlv.ChannelTypeTlv(ChannelTypes.UnsupportedChannelType(Features.empty)))), // empty upfront_shutdown_script with channel type defaultEncoded ++ hex"0004 01abcdef" -> defaultAccept.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(hex"01abcdef"))), // non-empty upfront_shutdown_script - defaultEncoded ++ hex"0004 01abcdef" ++ hex"01021000" -> defaultAccept.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(hex"01abcdef"), ChannelTlv.ChannelTypeTlv(ChannelTypes.StaticRemoteKey()))), // non-empty upfront_shutdown_script with channel type + defaultEncoded ++ hex"0000" ++ hex"01 17 1000000000000000000000000000000000000000000000" ++ hex"04 42 2062534ccb3be5a8997843f3b6bc530a94cbc60eceb538674ceedd62d8be07f2dfa5df6acf3ded7444268d56925bb2c33afe71a55f4fa88f3985451a681415930f6b" -> defaultAccept.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(ByteVector.empty), ChannelTlv.ChannelTypeTlv(ChannelTypes.SimpleTaprootChannelsStaging()), ChannelTlv.NextLocalNonceTlv(nonce))), // empty upfront_shutdown_script with taproot channel type and nonce + defaultEncoded ++ hex"0004 01abcdef" ++ hex"0103401000" -> defaultAccept.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(hex"01abcdef"), ChannelTlv.ChannelTypeTlv(ChannelTypes.AnchorOutputsZeroFeeHtlcTx()))), // non-empty upfront_shutdown_script with channel type defaultEncoded ++ hex"0000 0302002a 050102" -> defaultAccept.copy(tlvStream = TlvStream(Set[AcceptChannelTlv](ChannelTlv.UpfrontShutdownScriptTlv(ByteVector.empty)), Set(GenericTlv(UInt64(3), hex"002a"), GenericTlv(UInt64(5), hex"02")))), // empty upfront_shutdown_script + unknown odd tlv records defaultEncoded ++ hex"0002 1234 0303010203" -> defaultAccept.copy(tlvStream = TlvStream(Set[AcceptChannelTlv](ChannelTlv.UpfrontShutdownScriptTlv(hex"1234")), Set(GenericTlv(UInt64(3), hex"010203")))), // non-empty upfront_shutdown_script + unknown odd tlv records - defaultEncoded ++ hex"0303010203 05020123" -> defaultAccept.copy(tlvStream = TlvStream(Set.empty[AcceptChannelTlv], Set(GenericTlv(UInt64(3), hex"010203"), GenericTlv(UInt64(5), hex"0123")))) // no upfront_shutdown_script + unknown odd tlv records + defaultEncoded ++ hex"0303010203 05020123" -> defaultAccept.copy(tlvStream = TlvStream(Set.empty[AcceptChannelTlv], Set(GenericTlv(UInt64(3), hex"010203"), GenericTlv(UInt64(5), hex"0123")))), // no upfront_shutdown_script + unknown odd tlv records ) for ((encoded, expected) <- testCases) { @@ -372,9 +399,7 @@ class LightningMessageCodecsSpec extends AnyFunSuite { val defaultEncoded = hex"0041 0100000000000000000000000000000000000000000000000000000000000000 000000000000c350 00000000000001d9 0000000005f5e100 0000000000000001 00000006 0090 0032 031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f 024d4b6cd1361032ca9bd2aeb9d900aa4d45d9ead80ac9423374c451a7254d0766 02531fe6068134503d2723133227c867ac8fa6c83c537e9a44c3c5bdbdcb1fe337 03462779ad4aad39514614751a71085f2f10e1c7a593e4e030efb5b8721ce55b0b 0362c0a046dacce86ddd0343c6d3c7c79c2208ba0d9c9cf24a6d046d21d21f90f7 03f006a18d5653c4edf5391ff23a61f03ff83d237e880ee61187fa9f379a028e0a 02989c0b76cb563971fdc9bef31ec06c3560f3249d6ee9e5d83c57625596e05f6f" val testCases = Seq( defaultAccept -> defaultEncoded, - defaultAccept.copy(tlvStream = TlvStream(ChannelTypeTlv(ChannelTypes.StaticRemoteKey()))) -> (defaultEncoded ++ hex"01021000"), defaultAccept.copy(tlvStream = TlvStream(ChannelTypeTlv(ChannelTypes.AnchorOutputsZeroFeeHtlcTx()), PushAmountTlv(1729 msat))) -> (defaultEncoded ++ hex"0103401000 fe470000070206c1"), - defaultAccept.copy(tlvStream = TlvStream(ChannelTypeTlv(ChannelTypes.StaticRemoteKey()), RequireConfirmedInputsTlv())) -> (defaultEncoded ++ hex"01021000 0200"), defaultAccept.copy(tlvStream = TlvStream(ChannelTypeTlv(ChannelTypes.AnchorOutputsZeroFeeHtlcTx()), FeeCreditUsedTlv(0 msat))) -> (defaultEncoded ++ hex"0103401000 fda05200"), defaultAccept.copy(tlvStream = TlvStream(ChannelTypeTlv(ChannelTypes.AnchorOutputsZeroFeeHtlcTx()), FeeCreditUsedTlv(1729 msat))) -> (defaultEncoded ++ hex"0103401000 fda0520206c1"), ) @@ -398,6 +423,7 @@ class LightningMessageCodecsSpec extends AnyFunSuite { SpliceInit(channelId, 0 sat, FeeratePerKw(500 sat), 0, fundingPubkey) -> hex"9088 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000000 000001f4 00000000 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", SpliceInit(channelId, (-50_000).sat, FeeratePerKw(500 sat), 0, fundingPubkey) -> hex"9088 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ffffffffffff3cb0 000001f4 00000000 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", SpliceInit(channelId, 100_000 sat, 100, FeeratePerKw(2500 sat), fundingPubkey, 0 msat, requireConfirmedInputs = false, Some(LiquidityAds.RequestFunding(100_000 sat, fundingRate, LiquidityAds.PaymentDetails.FromChannelBalance))) -> hex"9088 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000000186a0 000009c4 00000064 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 fd053b1e00000000000186a0000186a0000186a00190009600000000000000000000", + SpliceInit(channelId, 100_000 sat, 100, FeeratePerKw(2500 sat), fundingPubkey, 0 msat, requireConfirmedInputs = false, None, Some(SimpleTaprootChannelsPhoenix)) -> hex"9088 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000000186a0 000009c400000064 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 fe47000011 47 1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000", SpliceAck(channelId, 25_000 sat, fundingPubkey) -> hex"908a aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000000061a8 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", SpliceAck(channelId, 40_000 sat, fundingPubkey, 10_000_000 msat, requireConfirmedInputs = false, None, None) -> hex"908a aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000009c40 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 fe4700000703989680", SpliceAck(channelId, 0 sat, fundingPubkey) -> hex"908a aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000000 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", @@ -405,7 +431,7 @@ class LightningMessageCodecsSpec extends AnyFunSuite { SpliceAck(channelId, 25_000 sat, fundingPubkey, 0 msat, requireConfirmedInputs = false, Some(LiquidityAds.WillFund(fundingRate, hex"deadbeef", ByteVector64.Zeroes)), None) -> hex"908a aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000000061a8 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 fd053b5a000186a0000186a00190009600000000000000000004deadbeef00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", SpliceAck(channelId, 25_000 sat, fundingPubkey, TlvStream(ChannelTlv.FeeCreditUsedTlv(0 msat))) -> hex"908a aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000000061a8 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 fda05200", SpliceAck(channelId, 25_000 sat, fundingPubkey, TlvStream(ChannelTlv.FeeCreditUsedTlv(1729 msat))) -> hex"908a aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000000061a8 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 fda0520206c1", - SpliceLocked(channelId, fundingTxId) -> hex"908c aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 24e1b2c94c4e734dd5b9c5f3c910fbb6b3b436ced6382c7186056a5a23f14566", + SpliceAck(channelId, 25_000 sat, fundingPubkey, 0 msat, requireConfirmedInputs = false, None, None, Some(SimpleTaprootChannelsPhoenix)) -> hex"908a aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000000061a8 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 fe47000011 47 1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000", SpliceLocked(channelId, fundingTxId) -> hex"908c aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 24e1b2c94c4e734dd5b9c5f3c910fbb6b3b436ced6382c7186056a5a23f14566", // @formatter:on ) testCases.foreach { case (message, bin) => @@ -524,8 +550,14 @@ class LightningMessageCodecsSpec extends AnyFunSuite { test("encode/decode closing messages") { val channelId = ByteVector32(hex"58a00a6f14e69a2e97b18cf76f755c8551fea9947cf7b6ece9d641013eba5f86") val sig1 = ByteVector64(hex"01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101") + val partialSig1 = ByteVector32(hex"0101010101010101010101010101010101010101010101010101010101010101") + val nonce1 = IndividualNonce(hex"52682593fd0783ea60657ed2d118e8f958c4a7a198237749b6729eccf963be1bc559531ec4b83bcfc42009cd08f7e95747146cec2fd09571b3fa76656e3012a4c97a") val sig2 = ByteVector64(hex"02020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202") + val partialSig2 = ByteVector32(hex"0202020202020202020202020202020202020202020202020202020202020202") + val nonce2 = IndividualNonce(hex"585b2fe8ca7a969bbda11ee9cbc95386abfddcc901967f84da4011c2a7cb5ada1dae51bdcd93a8b2933fcec7b2cda5a3f43ea2d0a29eb126bd329d4735d5389fe703") val sig3 = ByteVector64(hex"03030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303") + val partialSig3 = ByteVector32(hex"0303030303030303030303030303030303030303030303030303030303030303") + val nonce3 = IndividualNonce(hex"19bed0825ceb5acf504cddea72e37a75505290a22850c183725963edfe2dfb9f26e27180b210c05635987b80b3de3b7d01732653565b9f25ec23f7aff26122e00bff") val closerScript = hex"deadbeef" val closeeScript = hex"d43db3ef1234" val testCases = Seq( @@ -534,11 +566,15 @@ class LightningMessageCodecsSpec extends AnyFunSuite { hex"0028 58a00a6f14e69a2e97b18cf76f755c8551fea9947cf7b6ece9d641013eba5f86 0004deadbeef 0006d43db3ef1234 0000000000000451 00000000 034001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101" -> ClosingComplete(channelId, closerScript, closeeScript, 1105 sat, 0, TlvStream(ClosingTlv.CloserAndCloseeOutputs(sig1))), hex"0028 58a00a6f14e69a2e97b18cf76f755c8551fea9947cf7b6ece9d641013eba5f86 0004deadbeef 0006d43db3ef1234 0000000000000451 00000000 014001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 034002020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202" -> ClosingComplete(channelId, closerScript, closeeScript, 1105 sat, 0, TlvStream(ClosingTlv.CloserOutputOnly(sig1), ClosingTlv.CloserAndCloseeOutputs(sig2))), hex"0028 58a00a6f14e69a2e97b18cf76f755c8551fea9947cf7b6ece9d641013eba5f86 0004deadbeef 0006d43db3ef1234 0000000000000451 00000000 014001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 024002020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202 034003030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303" -> ClosingComplete(channelId, closerScript, closeeScript, 1105 sat, 0, TlvStream(ClosingTlv.CloserOutputOnly(sig1), ClosingTlv.CloseeOutputOnly(sig2), ClosingTlv.CloserAndCloseeOutputs(sig3))), + hex"0028 58a00a6f14e69a2e97b18cf76f755c8551fea9947cf7b6ece9d641013eba5f86 0004deadbeef 0006d43db3ef1234 0000000000000451 00000000 06620202020202020202020202020202020202020202020202020202020202020202585b2fe8ca7a969bbda11ee9cbc95386abfddcc901967f84da4011c2a7cb5ada1dae51bdcd93a8b2933fcec7b2cda5a3f43ea2d0a29eb126bd329d4735d5389fe703" -> ClosingComplete(channelId, closerScript, closeeScript, 1105 sat, 0, TlvStream(ClosingCompleteTlv.CloseeOutputOnlyPartialSignature(PartialSignatureWithNonce(partialSig2, nonce2)))), + hex"0028 58a00a6f14e69a2e97b18cf76f755c8551fea9947cf7b6ece9d641013eba5f86 0004deadbeef 0006d43db3ef1234 0000000000000451 00000000 0562010101010101010101010101010101010101010101010101010101010101010152682593fd0783ea60657ed2d118e8f958c4a7a198237749b6729eccf963be1bc559531ec4b83bcfc42009cd08f7e95747146cec2fd09571b3fa76656e3012a4c97a 0762030303030303030303030303030303030303030303030303030303030303030319bed0825ceb5acf504cddea72e37a75505290a22850c183725963edfe2dfb9f26e27180b210c05635987b80b3de3b7d01732653565b9f25ec23f7aff26122e00bff" -> ClosingComplete(channelId, closerScript, closeeScript, 1105 sat, 0, TlvStream(ClosingCompleteTlv.CloserOutputOnlyPartialSignature(PartialSignatureWithNonce(partialSig1, nonce1)), ClosingCompleteTlv.CloserAndCloseeOutputsPartialSignature(PartialSignatureWithNonce(partialSig3, nonce3)))), hex"0029 58a00a6f14e69a2e97b18cf76f755c8551fea9947cf7b6ece9d641013eba5f86 0004deadbeef 0006d43db3ef1234 0000000000000451 00000000" -> ClosingSig(channelId, closerScript, closeeScript, 1105 sat, 0), hex"0029 58a00a6f14e69a2e97b18cf76f755c8551fea9947cf7b6ece9d641013eba5f86 0004deadbeef 0006d43db3ef1234 0000000000000451 00000000 024001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101" -> ClosingSig(channelId, closerScript, closeeScript, 1105 sat, 0, TlvStream(ClosingTlv.CloseeOutputOnly(sig1))), hex"0029 58a00a6f14e69a2e97b18cf76f755c8551fea9947cf7b6ece9d641013eba5f86 0004deadbeef 0006d43db3ef1234 0000000000000451 00000000 034001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101" -> ClosingSig(channelId, closerScript, closeeScript, 1105 sat, 0, TlvStream(ClosingTlv.CloserAndCloseeOutputs(sig1))), hex"0029 58a00a6f14e69a2e97b18cf76f755c8551fea9947cf7b6ece9d641013eba5f86 0004deadbeef 0006d43db3ef1234 0000000000000451 00000000 014001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 034002020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202" -> ClosingSig(channelId, closerScript, closeeScript, 1105 sat, 0, TlvStream(ClosingTlv.CloserOutputOnly(sig1), ClosingTlv.CloserAndCloseeOutputs(sig2))), hex"0029 58a00a6f14e69a2e97b18cf76f755c8551fea9947cf7b6ece9d641013eba5f86 0004deadbeef 0006d43db3ef1234 0000000000000451 00000000 014001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 024002020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202 034003030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303" -> ClosingSig(channelId, closerScript, closeeScript, 1105 sat, 0, TlvStream(ClosingTlv.CloserOutputOnly(sig1), ClosingTlv.CloseeOutputOnly(sig2), ClosingTlv.CloserAndCloseeOutputs(sig3))), + hex"0029 58a00a6f14e69a2e97b18cf76f755c8551fea9947cf7b6ece9d641013eba5f86 0004deadbeef 0006d43db3ef1234 0000000000000451 00000000 05200101010101010101010101010101010101010101010101010101010101010101" -> ClosingSig(channelId, closerScript, closeeScript, 1105 sat, 0, TlvStream(ClosingSigTlv.CloserOutputOnlyPartialSignature(partialSig1))), + hex"0029 58a00a6f14e69a2e97b18cf76f755c8551fea9947cf7b6ece9d641013eba5f86 0004deadbeef 0006d43db3ef1234 0000000000000451 00000000 06200202020202020202020202020202020202020202020202020202020202020202 07200303030303030303030303030303030303030303030303030303030303030303" -> ClosingSig(channelId, closerScript, closeeScript, 1105 sat, 0, TlvStream(ClosingSigTlv.CloseeOutputOnlyPartialSignature(partialSig2), ClosingSigTlv.CloserAndCloseeOutputsPartialSignature(partialSig3))), ) for ((encoded, expected) <- testCases) { val decoded = lightningMessageCodec.decode(encoded.bits).require.value @@ -556,15 +592,16 @@ class LightningMessageCodecsSpec extends AnyFunSuite { FundingCreated(randomBytes32(), TxId(ByteVector32.Zeroes), 3, randomBytes64()), FundingSigned(randomBytes32(), randomBytes64()), ChannelReady(randomBytes32(), point(2)), - ChannelReady(randomBytes32(), point(2), TlvStream(ChannelReadyTlv.ShortChannelIdTlv(Alias(123456)))), + ChannelReady(randomBytes32(), point(2), Alias(123456)), + ChannelReady(randomBytes32(), point(2), Alias(123456), IndividualNonce(randomBytes(66))), UpdateFee(randomBytes32(), FeeratePerKw(2 sat)), Shutdown(randomBytes32(), bin(47, 0)), ClosingSigned(randomBytes32(), 2 sat, randomBytes64()), - UpdateAddHtlc(randomBytes32(), 2, 3 msat, bin32(0), CltvExpiry(4), TestConstants.emptyOnionPacket, None, 1.0, None), + UpdateAddHtlc(randomBytes32(), 2, 3 msat, bin32(0), CltvExpiry(4), TestConstants.emptyOnionPacket, None, Reputation.maxEndorsement, None), UpdateFulfillHtlc(randomBytes32(), 2, bin32(0)), UpdateFailHtlc(randomBytes32(), 2, bin(154, 0)), UpdateFailMalformedHtlc(randomBytes32(), 2, randomBytes32(), 1111), - CommitSig(randomBytes32(), randomBytes64(), randomBytes64() :: randomBytes64() :: randomBytes64() :: Nil), + CommitSig(randomBytes32(), IndividualSignature(randomBytes64()), randomBytes64() :: randomBytes64() :: randomBytes64() :: Nil), RevokeAndAck(randomBytes32(), scalar(0), point(1)), ChannelAnnouncement(randomBytes64(), randomBytes64(), randomBytes64(), randomBytes64(), Features(bin(7, 9)), Block.RegtestGenesisBlock.hash, RealShortChannelId(1), randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey), NodeAnnouncement(randomBytes64(), Features(DataLossProtect -> Optional), 1 unixsec, randomKey().publicKey, Color(100.toByte, 200.toByte, 300.toByte), "node-alias", IPv4(InetAddress.getByAddress(Array[Byte](192.toByte, 168.toByte, 1.toByte, 42.toByte)).asInstanceOf[Inet4Address], 42000) :: Nil), @@ -624,7 +661,7 @@ class LightningMessageCodecsSpec extends AnyFunSuite { val paymentHash2 = ByteVector32(hex"3213a810a0bfc54566d9be09da1484538b5d19229e928dfa8b692966a8df6785") val fundingFee = LiquidityAds.FundingFee(5_000_100 msat, TxId(TxHash(ByteVector32(hex"24e1b2c94c4e734dd5b9c5f3c910fbb6b3b436ced6382c7186056a5a23f14566")))) val testCases = Seq( - UpdateAddHtlc(channelId, 7, 75_000_000 msat, paymentHash1, CltvExpiry(840_000), TestConstants.emptyOnionPacket, pathKey_opt = None, confidence = 0, fundingFee_opt = Some(fundingFee)) -> hex"0080 c11b8fbd682b3c6ee11f9d7268e22bb5887cd4d3bf3338bfcc340583f685733c 0000000000000007 00000000047868c0 80417c0c91deb72606958425ea1552a045a55a250e91870231b486dcb2106734 000cd140 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 fda0512800000000004c4ba424e1b2c94c4e734dd5b9c5f3c910fbb6b3b436ced6382c7186056a5a23f14566 fe0001a1470100", + UpdateAddHtlc(channelId, 7, 75_000_000 msat, paymentHash1, CltvExpiry(840_000), TestConstants.emptyOnionPacket, pathKey_opt = None, endorsement = 0, fundingFee_opt = Some(fundingFee)) -> hex"0080 c11b8fbd682b3c6ee11f9d7268e22bb5887cd4d3bf3338bfcc340583f685733c 0000000000000007 00000000047868c0 80417c0c91deb72606958425ea1552a045a55a250e91870231b486dcb2106734 000cd140 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 fda0512800000000004c4ba424e1b2c94c4e734dd5b9c5f3c910fbb6b3b436ced6382c7186056a5a23f14566 fe0001a1470100", WillAddHtlc(Block.RegtestGenesisBlock.hash, paymentId, 50_000_000 msat, paymentHash1, CltvExpiry(840_000), TestConstants.emptyOnionPacket, pathKey_opt = None) -> hex"a051 06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f 3118a7954088c27b19923894ed27923c297f88ec3734f90b2b4aafcb11238503 0000000002faf080 80417c0c91deb72606958425ea1552a045a55a250e91870231b486dcb2106734 000cd140 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", WillAddHtlc(Block.RegtestGenesisBlock.hash, paymentId, 50_000_000 msat, paymentHash1, CltvExpiry(840_000), TestConstants.emptyOnionPacket, pathKey_opt = Some(pathKey)) -> hex"a051 06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f 3118a7954088c27b19923894ed27923c297f88ec3734f90b2b4aafcb11238503 0000000002faf080 80417c0c91deb72606958425ea1552a045a55a250e91870231b486dcb2106734 000cd140 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 00210296d5c32655a5eaa8be086479d7bcff967b6e9ca8319b69565747ae16ff20fad6", WillFailHtlc(paymentId, paymentHash1, hex"deadbeef") -> hex"a052 3118a7954088c27b19923894ed27923c297f88ec3734f90b2b4aafcb11238503 80417c0c91deb72606958425ea1552a045a55a250e91870231b486dcb2106734 0004 deadbeef", diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/LiquidityAdsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/LiquidityAdsSpec.scala index 54610e73c6..0af254dcdd 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/LiquidityAdsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/LiquidityAdsSpec.scala @@ -31,11 +31,11 @@ class LiquidityAdsSpec extends AnyFunSuite { assert(nodeKey.publicKey == PublicKey(hex"03ca9b880627d2d4e3b33164f66946349f820d26aa9572fe0e525e534850cbd413")) val fundingRate = LiquidityAds.FundingRate(100_000 sat, 1_000_000 sat, 500, 100, 10 sat, 1000 sat) - assert(fundingRate.fees(FeeratePerKw(FeeratePerByte(5 sat)), 500_000 sat, 500_000 sat, isChannelCreation = false).total == 5635.sat) - assert(fundingRate.fees(FeeratePerKw(FeeratePerByte(5 sat)), 500_000 sat, 600_000 sat, isChannelCreation = false).total == 5635.sat) - assert(fundingRate.fees(FeeratePerKw(FeeratePerByte(5 sat)), 500_000 sat, 600_000 sat, isChannelCreation = true).total == 6635.sat) - assert(fundingRate.fees(FeeratePerKw(FeeratePerByte(5 sat)), 500_000 sat, 400_000 sat, isChannelCreation = false).total == 4635.sat) - assert(fundingRate.fees(FeeratePerKw(FeeratePerByte(10 sat)), 500_000 sat, 500_000 sat, isChannelCreation = false).total == 6260.sat) + assert(fundingRate.fees(FeeratePerByte(5 sat).perKw, 500_000 sat, 500_000 sat, isChannelCreation = false).total == 5635.sat) + assert(fundingRate.fees(FeeratePerByte(5 sat).perKw, 500_000 sat, 600_000 sat, isChannelCreation = false).total == 5635.sat) + assert(fundingRate.fees(FeeratePerByte(5 sat).perKw, 500_000 sat, 600_000 sat, isChannelCreation = true).total == 6635.sat) + assert(fundingRate.fees(FeeratePerByte(5 sat).perKw, 500_000 sat, 400_000 sat, isChannelCreation = false).total == 4635.sat) + assert(fundingRate.fees(FeeratePerByte(10 sat).perKw, 500_000 sat, 500_000 sat, isChannelCreation = false).total == 6260.sat) val fundingRates = LiquidityAds.WillFundRates(fundingRate :: Nil, Set(LiquidityAds.PaymentType.FromChannelBalance)) val Some(request) = LiquidityAds.requestFunding(500_000 sat, LiquidityAds.PaymentDetails.FromChannelBalance, fundingRates) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/MessageOnionCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/MessageOnionCodecsSpec.scala index 14db533845..ed4f5d767f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/MessageOnionCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/MessageOnionCodecsSpec.scala @@ -95,7 +95,7 @@ class MessageOnionCodecsSpec extends AnyFunSuiteLike { val payerKey = randomKey() val request = OfferTypes.InvoiceRequest(offer, 100_000 msat, 1, Features.empty, payerKey, Block.LivenetGenesisBlock.hash) val selfPayload = blindedRouteDataCodec.encode(TlvStream(PathId(randomBytes32()), PaymentConstraints(CltvExpiry(1234567), 0 msat), AllowedFeatures(Features.empty))).require.bytes - val route = PaymentBlindedRoute(Sphinx.RouteBlinding.create(randomKey(), Seq(nodeKey.publicKey), Seq(selfPayload)).route, PaymentInfo(1 msat, 2, CltvExpiryDelta(3), 4 msat, 5 msat, Features.empty)) + val route = PaymentBlindedRoute(Sphinx.RouteBlinding.create(randomKey(), Seq(nodeKey.publicKey), Seq(selfPayload)).route, PaymentInfo(1 msat, 2, CltvExpiryDelta(3), 4 msat, 5 msat, ByteVector.empty)) val invoice = Bolt12Invoice(request, randomBytes32(), nodeKey, 300 seconds, Features.empty, Seq(route)) val testCasesInvalid = Seq[TlvStream[OnionMessagePayloadTlv]]( diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/OfferTypesSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/OfferTypesSpec.scala index b0a541ce7a..7bc7ef185a 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/OfferTypesSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/OfferTypesSpec.scala @@ -22,13 +22,15 @@ import fr.acinq.bitcoin.scalacompat.{Block, BlockHash, ByteVector32} import fr.acinq.eclair.FeatureSupport.{Mandatory, Optional} import fr.acinq.eclair.Features.BasicMultiPartPayment import fr.acinq.eclair.crypto.Sphinx.RouteBlinding.{BlindedHop, BlindedRoute} +import fr.acinq.eclair.wire.protocol.CommonCodecs.varintoverflow import fr.acinq.eclair.wire.protocol.OfferCodecs.{invoiceRequestTlvCodec, offerTlvCodec} import fr.acinq.eclair.wire.protocol.OfferTypes._ import fr.acinq.eclair.{BlockHeight, EncodedNodeId, Features, MilliSatoshiLong, RealShortChannelId, randomBytes32, randomKey} import org.json4s.DefaultFormats import org.json4s.jackson.JsonMethods import org.scalatest.funsuite.AnyFunSuite -import scodec.bits.{ByteVector, HexStringSyntax} +import scodec.bits.{BitVector, ByteVector, HexStringSyntax} +import scodec.codecs.{utf8, variableSizeBytesLong} import java.io.File import scala.io.Source @@ -65,7 +67,7 @@ class OfferTypesSpec extends AnyFunSuite { test("offer with amount and quantity") { val offer = Offer(TlvStream[OfferTlv]( OfferChains(Seq(Block.Testnet3GenesisBlock.hash)), - OfferAmount(50 msat), + OfferAmount(50), OfferDescription("offer with quantity"), OfferIssuer("alice@bigshop.com"), OfferQuantityMax(0), @@ -131,7 +133,7 @@ class OfferTypesSpec extends AnyFunSuite { test("check that invoice request matches offer (chain compatibility)") { { - val offer = Offer(TlvStream(OfferAmount(100 msat), OfferDescription("offer without chains"), OfferNodeId(randomKey().publicKey))) + val offer = Offer(TlvStream(OfferAmount(100), OfferDescription("offer without chains"), OfferNodeId(randomKey().publicKey))) val payerKey = randomKey() val request = { val tlvs: Set[InvoiceRequestTlv] = offer.records.records ++ Set( @@ -152,7 +154,7 @@ class OfferTypesSpec extends AnyFunSuite { } { val (chain1, chain2) = (BlockHash(randomBytes32()), BlockHash(randomBytes32())) - val offer = Offer(TlvStream(OfferChains(Seq(chain1, chain2)), OfferAmount(100 msat), OfferDescription("offer with chains"), OfferNodeId(randomKey().publicKey))) + val offer = Offer(TlvStream(OfferChains(Seq(chain1, chain2)), OfferAmount(100), OfferDescription("offer with chains"), OfferNodeId(randomKey().publicKey))) val payerKey = randomKey() val request1 = InvoiceRequest(offer, 100 msat, 1, Features.empty, payerKey, chain1) assert(request1.isValid) @@ -169,7 +171,7 @@ class OfferTypesSpec extends AnyFunSuite { test("check that invoice request matches offer (multiple items)") { val offer = Offer(TlvStream( - OfferAmount(500 msat), + OfferAmount(500), OfferDescription("offer for multiple items"), OfferNodeId(randomKey().publicKey), OfferQuantityMax(10), @@ -314,13 +316,8 @@ class OfferTypesSpec extends AnyFunSuite { val testVectors = JsonMethods.parse(src.mkString).extract[Seq[TestVector]] src.close() for (vector <- testVectors) { - if (vector.description == "with currency") { - // We don't support currency conversion yet. - assert(Offer.decode(vector.bolt12).isFailure) - } else { - val offer = Offer.decode(vector.bolt12) - assert((offer.isSuccess && offer.get.features.unknown.forall(_.bitIndex % 2 == 1)) == vector.valid, vector.description) - } + val offer = Offer.decode(vector.bolt12) + assert((offer.isSuccess && offer.get.features.unknown.forall(_.bitIndex % 2 == 1)) == vector.valid, vector.description) } } @@ -336,4 +333,31 @@ class OfferTypesSpec extends AnyFunSuite { assert(Offer.decode(vector.string).isSuccess == vector.valid, vector.comment) } } + + test("offer currency") { + def encode(s: String): BitVector = variableSizeBytesLong(varintoverflow, utf8).encode(s).require + + assert(OfferCodecs.offerCurrency.decode(encode("EUR")).isSuccessful) + assert(OfferCodecs.offerCurrency.decode(encode("USD")).isSuccessful) + assert(OfferCodecs.offerCurrency.decode(encode("CHF")).isSuccessful) + assert(OfferCodecs.offerCurrency.decode(encode("JOD")).isSuccessful) + assert(OfferCodecs.offerCurrency.decode(encode("CNY")).isSuccessful) + assert(OfferCodecs.offerCurrency.decode(encode("GBP")).isSuccessful) + assert(OfferCodecs.offerCurrency.decode(encode("JPY")).isSuccessful) + assert(OfferCodecs.offerCurrency.decode(encode("EURO")).isFailure) + assert(OfferCodecs.offerCurrency.decode(encode("eur")).isFailure) + assert(OfferCodecs.offerCurrency.decode(encode("BTC")).isFailure) + assert(OfferCodecs.offerCurrency.decode(encode("XAU")).isFailure) + assert(OfferCodecs.offerCurrency.decode(hex"ffffff".bits).isFailure) + } + + test("empty fields") { + val invalidOffers = Seq( + Offer(TlvStream(OfferPaths(Nil))), + Offer(TlvStream(OfferNodeId(randomKey().publicKey), OfferChains(Nil))), + ) + for (invalidOffer <- invalidOffers) { + assert(Offer.decode(invalidOffer.toString).isFailure) + } + } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/PaymentOnionSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/PaymentOnionSpec.scala index 0ec96b7b15..d933fb2175 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/PaymentOnionSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/PaymentOnionSpec.scala @@ -185,7 +185,7 @@ class PaymentOnionSpec extends AnyFunSuite { PublicKey(hex"0232882c4982576e00f0d6bd4998f5b3e92d47ecc8fbad5b6a5e7521819d891d9e"), Seq(RouteBlinding.BlindedHop(PublicKey(hex"03823aa560d631e9d7b686be4a9227e577009afb5173023b458a6a6aff056ac980"), hex"")) ) - val path = PaymentBlindedRoute(blindedRoute, OfferTypes.PaymentInfo(1000 msat, 678, CltvExpiryDelta(82), 300 msat, 4000000 msat, Features.empty)) + val path = PaymentBlindedRoute(blindedRoute, OfferTypes.PaymentInfo(1000 msat, 678, CltvExpiryDelta(82), 300 msat, 4000000 msat, ByteVector.empty)) val expected = TlvStream[OnionPaymentPayloadTlv](AmountToForward(341 msat), OutgoingCltv(CltvExpiry(826483)), OutgoingBlindedPaths(Seq(path)), InvoiceFeatures(features)) val bin = hex"82 02020155 04030c9c73 fe0001023103020000 fe000102366a0100000000000001d40232882c4982576e00f0d6bd4998f5b3e92d47ecc8fbad5b6a5e7521819d891d9e0103823aa560d631e9d7b686be4a9227e577009afb5173023b458a6a6aff056ac9800000000003e8000002a60052000000000000012c00000000003d09000000" diff --git a/eclair-front/pom.xml b/eclair-front/pom.xml index ab6f218ed3..4aa1b66c5a 100644 --- a/eclair-front/pom.xml +++ b/eclair-front/pom.xml @@ -21,7 +21,7 @@ fr.acinq.eclair eclair_2.13 - 0.13.0-SNAPSHOT + 0.14.0-SNAPSHOT eclair-front_2.13 diff --git a/eclair-node/pom.xml b/eclair-node/pom.xml index 09ca9a42e8..851e897bb2 100644 --- a/eclair-node/pom.xml +++ b/eclair-node/pom.xml @@ -21,7 +21,7 @@ fr.acinq.eclair eclair_2.13 - 0.13.0-SNAPSHOT + 0.14.0-SNAPSHOT eclair-node_2.13 diff --git a/eclair-node/src/main/scala/fr/acinq/eclair/Boot.scala b/eclair-node/src/main/scala/fr/acinq/eclair/Boot.scala index d966500b6b..1d6c4ec134 100644 --- a/eclair-node/src/main/scala/fr/acinq/eclair/Boot.scala +++ b/eclair-node/src/main/scala/fr/acinq/eclair/Boot.scala @@ -33,6 +33,10 @@ import scala.util.{Failure, Success} */ object Boot extends App with Logging { try { + if (!System.getProperty("eclair.allow-unsafe-startup", "false").toBooleanOption.contains(true)) { + throw new RuntimeException("This version of eclair is unsafe to use: please wait for the next official release to update your node.") + } + val datadir = new File(System.getProperty("eclair.datadir", System.getProperty("user.home") + "/.eclair")) val config = NodeParams.loadConfiguration(datadir) @@ -63,7 +67,7 @@ object Boot extends App with Logging { /** * Starts the http APIs service if enabled in the configuration */ - def startApiServiceIfEnabled(kit: Kit, providers: Seq[RouteProvider] = Nil)(implicit system: ActorSystem, ec: ExecutionContext) = { + private def startApiServiceIfEnabled(kit: Kit, providers: Seq[RouteProvider] = Nil)(implicit system: ActorSystem, ec: ExecutionContext) = { val config = system.settings.config.getConfig("eclair") if (config.getBoolean("api.enabled")) { logger.info(s"json API enabled on port=${config.getInt("api.port")}") @@ -84,7 +88,7 @@ object Boot extends App with Logging { } } - def onError(t: Throwable): Unit = { + private def onError(t: Throwable): Unit = { val errorMsg = if (t.getMessage != null) t.getMessage else t.getClass.getSimpleName System.err.println(s"fatal error: $errorMsg") logger.error(s"fatal error: $errorMsg", t) diff --git a/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Channel.scala b/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Channel.scala index fd5dd468b5..d598dc2f97 100644 --- a/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Channel.scala +++ b/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Channel.scala @@ -22,7 +22,7 @@ import fr.acinq.bitcoin.scalacompat.{Satoshi, Script} import fr.acinq.eclair.api.Service import fr.acinq.eclair.api.directives.EclairDirectives import fr.acinq.eclair.api.serde.FormParamExtractors._ -import fr.acinq.eclair.blockchain.fee.{ConfirmationTarget, FeeratePerByte, FeeratePerKw} +import fr.acinq.eclair.blockchain.fee.{ConfirmationTarget, FeeratePerByte} import fr.acinq.eclair.channel.{ChannelTypes, ClosingFeerates} import fr.acinq.eclair.{MilliSatoshi, Paginated} import scodec.bits.ByteVector @@ -32,23 +32,15 @@ trait Channel { import fr.acinq.eclair.api.serde.JsonSupport.{formats, marshaller, serialization} - val supportedChannelTypes = Set( - ChannelTypes.Standard(), - ChannelTypes.Standard(zeroConf = true), - ChannelTypes.Standard(scidAlias = true), - ChannelTypes.Standard(scidAlias = true, zeroConf = true), - ChannelTypes.StaticRemoteKey(), - ChannelTypes.StaticRemoteKey(zeroConf = true), - ChannelTypes.StaticRemoteKey(scidAlias = true), - ChannelTypes.StaticRemoteKey(scidAlias = true, zeroConf = true), - ChannelTypes.AnchorOutputs(), - ChannelTypes.AnchorOutputs(zeroConf = true), - ChannelTypes.AnchorOutputs(scidAlias = true), - ChannelTypes.AnchorOutputs(scidAlias = true, zeroConf = true), + private val supportedChannelTypes = Set( ChannelTypes.AnchorOutputsZeroFeeHtlcTx(), ChannelTypes.AnchorOutputsZeroFeeHtlcTx(zeroConf = true), ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = true), - ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = true, zeroConf = true) + ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = true, zeroConf = true), + ChannelTypes.SimpleTaprootChannelsStaging(), + ChannelTypes.SimpleTaprootChannelsStaging(zeroConf = true), + ChannelTypes.SimpleTaprootChannelsStaging(scidAlias = true), + ChannelTypes.SimpleTaprootChannelsStaging(scidAlias = true, zeroConf = true), ).map(ct => ct.toString -> ct).toMap // we use the toString method as name in the api val open: Route = postRequest("open") { implicit t => @@ -71,28 +63,28 @@ trait Channel { val rbfOpen: Route = postRequest("rbfopen") { implicit f => formFields(channelIdFormParam, "targetFeerateSatByte".as[FeeratePerByte], "fundingFeeBudgetSatoshis".as[Satoshi], "lockTime".as[Long].?) { - (channelId, targetFeerateSatByte, fundingFeeBudget, lockTime_opt) => complete(eclairApi.rbfOpen(channelId, FeeratePerKw(targetFeerateSatByte), fundingFeeBudget, lockTime_opt)) + (channelId, targetFeerateSatByte, fundingFeeBudget, lockTime_opt) => complete(eclairApi.rbfOpen(channelId, targetFeerateSatByte.perKw, fundingFeeBudget, lockTime_opt)) } } val spliceIn: Route = postRequest("splicein") { implicit f => formFields(channelIdFormParam, "amountIn".as[Satoshi], "pushMsat".as[MilliSatoshi].?) { - (channelId, amountIn, pushMsat_opt) => complete(eclairApi.spliceIn(channelId, amountIn, pushMsat_opt)) + (channelId, amountIn, pushMsat_opt) => complete(eclairApi.spliceIn(channelId, amountIn, pushMsat_opt, None)) } } val spliceOut: Route = postRequest("spliceout") { implicit f => formFields(channelIdFormParam, "amountOut".as[Satoshi], "scriptPubKey".as[ByteVector](bytesUnmarshaller)) { - (channelId, amountOut, scriptPubKey) => complete(eclairApi.spliceOut(channelId, amountOut, Left(scriptPubKey))) + (channelId, amountOut, scriptPubKey) => complete(eclairApi.spliceOut(channelId, amountOut, Left(scriptPubKey), None)) } ~ formFields(channelIdFormParam, "amountOut".as[Satoshi], "address".as[String]) { - (channelId, amountOut, address) => complete(eclairApi.spliceOut(channelId, amountOut, Right(address))) + (channelId, amountOut, address) => complete(eclairApi.spliceOut(channelId, amountOut, Right(address), None)) } } val rbfSplice: Route = postRequest("rbfsplice") { implicit f => formFields(channelIdFormParam, "targetFeerateSatByte".as[FeeratePerByte], "fundingFeeBudgetSatoshis".as[Satoshi], "lockTime".as[Long].?) { - (channelId, targetFeerateSatByte, fundingFeeBudget, lockTime_opt) => complete(eclairApi.rbfSplice(channelId, FeeratePerKw(targetFeerateSatByte), fundingFeeBudget, lockTime_opt)) + (channelId, targetFeerateSatByte, fundingFeeBudget, lockTime_opt) => complete(eclairApi.rbfSplice(channelId, targetFeerateSatByte.perKw, fundingFeeBudget, lockTime_opt)) } } @@ -101,9 +93,9 @@ trait Channel { formFields("scriptPubKey".as[ByteVector](bytesUnmarshaller).?, "preferredFeerateSatByte".as[FeeratePerByte].?, "minFeerateSatByte".as[FeeratePerByte].?, "maxFeerateSatByte".as[FeeratePerByte].?) { (scriptPubKey_opt, preferredFeerate_opt, minFeerate_opt, maxFeerate_opt) => val closingFeerates = preferredFeerate_opt.map(preferredPerByte => { - val preferredFeerate = FeeratePerKw(preferredPerByte) - val minFeerate = minFeerate_opt.map(feerate => FeeratePerKw(feerate)).getOrElse(preferredFeerate / 2) - val maxFeerate = maxFeerate_opt.map(feerate => FeeratePerKw(feerate)).getOrElse(preferredFeerate * 2) + val preferredFeerate = preferredPerByte.perKw + val minFeerate = minFeerate_opt.map(feerate => feerate.perKw).getOrElse(preferredFeerate / 2) + val maxFeerate = maxFeerate_opt.map(feerate => feerate.perKw).getOrElse(preferredFeerate * 2) ClosingFeerates(preferredFeerate, minFeerate, maxFeerate) }) if (scriptPubKey_opt.forall(Script.isNativeWitnessScript)) { @@ -117,7 +109,9 @@ trait Channel { val forceClose: Route = postRequest("forceclose") { implicit t => withChannelsIdentifier { channels => - complete(eclairApi.forceClose(channels)) + formFields("maxClosingFeerateSatByte".as[FeeratePerByte].?) { maxClosingFeerate_opt => + complete(eclairApi.forceClose(channels, maxClosingFeerate_opt.map(_.perKw))) + } } } diff --git a/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Control.scala b/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Control.scala index bffb9b1185..f5892a7628 100644 --- a/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Control.scala +++ b/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Control.scala @@ -57,7 +57,7 @@ trait Control { val spendFromChannelAddressPrep: Route = postRequest("spendfromchanneladdressprep") { implicit t => formFields("t".as[ByteVector32], "o".as[Int], "kp", "fi".as[Int], "address", "f".as[FeeratePerByte]) { (txId, outputIndex, keyPath, fundingTxIndex, address, feerate) => - complete(eclairApi.spendFromChannelAddressPrep(OutPoint(TxId(txId), outputIndex), KeyPath(keyPath), fundingTxIndex, address, FeeratePerKw(feerate))) + complete(eclairApi.spendFromChannelAddressPrep(OutPoint(TxId(txId), outputIndex), KeyPath(keyPath), fundingTxIndex, address, feerate.perKw)) } } diff --git a/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Offer.scala b/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Offer.scala index 96949c9d34..5d37d128e7 100644 --- a/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Offer.scala +++ b/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Offer.scala @@ -45,6 +45,12 @@ trait Offer { } } - val offerRoutes: Route = createOffer ~ disableOffer ~ listoffers + val parseOffer: Route = postRequest("parseoffer") { implicit t => + formFields(offerFormParam) { offer => + complete(offer) + } + } + + val offerRoutes: Route = createOffer ~ disableOffer ~ listoffers ~ parseOffer } diff --git a/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala b/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala index 348d4b7d07..691703ca13 100644 --- a/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala +++ b/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala @@ -294,7 +294,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM } } - test("'open' channels with bad channelType") { + test("'open' channels with unknown channelType") { val nodeId = PublicKey(hex"030bb6a5e0c6b203c7e2180fb78c7ba4bdce46126761d8201b91ddac089cdecc87") val eclair = mock[Eclair] @@ -310,48 +310,6 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM } } - test("'open' channels with standard channelType") { - val nodeId = PublicKey(hex"030bb6a5e0c6b203c7e2180fb78c7ba4bdce46126761d8201b91ddac089cdecc87") - val channelId = ByteVector32(hex"56d7d6eda04d80138270c49709f1eadb5ab4939e5061309ccdacdb98ce637d0e") - val fundingTxId = TxId.fromValidHex("a86b3f93c1b2ea3f221159869d6f556cae1ba2622cc8c7eb71c7f4f64e0fbca4") - - val eclair = mock[Eclair] - eclair.open(any, any, any, any, any, any, any, any)(any[Timeout]) returns Future.successful(OpenChannelResponse.Created(channelId, fundingTxId, 0 sat)) - val mockService = new MockService(eclair) - - Post("/open", FormData("nodeId" -> nodeId.toString(), "fundingSatoshis" -> "25000", "channelType" -> "standard").toEntity) ~> - addCredentials(BasicHttpCredentials("", mockApi().password)) ~> - addHeader("Content-Type", "application/json") ~> - Route.seal(mockService.route) ~> - check { - assert(handled) - assert(status == OK) - assert(entityAs[String] == "\"created channel 56d7d6eda04d80138270c49709f1eadb5ab4939e5061309ccdacdb98ce637d0e with fundingTxId=a86b3f93c1b2ea3f221159869d6f556cae1ba2622cc8c7eb71c7f4f64e0fbca4 and fees=0 sat\"") - eclair.open(nodeId, 25000 sat, None, Some(ChannelTypes.Standard()), None, None, None, None)(any[Timeout]).wasCalled(once) - } - } - - test("'open' channels with static_remotekey channelType") { - val nodeId = PublicKey(hex"030bb6a5e0c6b203c7e2180fb78c7ba4bdce46126761d8201b91ddac089cdecc87") - val channelId = ByteVector32(hex"56d7d6eda04d80138270c49709f1eadb5ab4939e5061309ccdacdb98ce637d0e") - val fundingTxId = TxId.fromValidHex("a86b3f93c1b2ea3f221159869d6f556cae1ba2622cc8c7eb71c7f4f64e0fbca4") - - val eclair = mock[Eclair] - eclair.open(any, any, any, any, any, any, any, any)(any[Timeout]) returns Future.successful(OpenChannelResponse.Created(channelId, fundingTxId, 1 sat)) - val mockService = new MockService(eclair) - - Post("/open", FormData("nodeId" -> nodeId.toString(), "fundingSatoshis" -> "25000", "channelType" -> "static_remotekey").toEntity) ~> - addCredentials(BasicHttpCredentials("", mockApi().password)) ~> - addHeader("Content-Type", "application/json") ~> - Route.seal(mockService.route) ~> - check { - assert(handled) - assert(status == OK) - assert(entityAs[String] == "\"created channel 56d7d6eda04d80138270c49709f1eadb5ab4939e5061309ccdacdb98ce637d0e with fundingTxId=a86b3f93c1b2ea3f221159869d6f556cae1ba2622cc8c7eb71c7f4f64e0fbca4 and fees=1 sat\"") - eclair.open(nodeId, 25000 sat, None, Some(ChannelTypes.StaticRemoteKey()), None, None, None, None)(any[Timeout]).wasCalled(once) - } - } - test("'open' channels with anchor_outputs channelType") { val nodeId = PublicKey(hex"030bb6a5e0c6b203c7e2180fb78c7ba4bdce46126761d8201b91ddac089cdecc87") val channelId = ByteVector32(hex"56d7d6eda04d80138270c49709f1eadb5ab4939e5061309ccdacdb98ce637d0e") @@ -361,17 +319,6 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM eclair.open(any, any, any, any, any, any, any, any)(any[Timeout]) returns Future.successful(OpenChannelResponse.Created(channelId, fundingTxId, 500 sat)) val mockService = new MockService(eclair) - Post("/open", FormData("nodeId" -> nodeId.toString(), "fundingSatoshis" -> "25000", "channelType" -> "anchor_outputs").toEntity) ~> - addCredentials(BasicHttpCredentials("", mockApi().password)) ~> - addHeader("Content-Type", "application/json") ~> - Route.seal(mockService.route) ~> - check { - assert(handled) - assert(status == OK) - assert(entityAs[String] == "\"created channel 56d7d6eda04d80138270c49709f1eadb5ab4939e5061309ccdacdb98ce637d0e with fundingTxId=a86b3f93c1b2ea3f221159869d6f556cae1ba2622cc8c7eb71c7f4f64e0fbca4 and fees=500 sat\"") - eclair.open(nodeId, 25000 sat, None, Some(ChannelTypes.AnchorOutputs()), None, None, None, None)(any[Timeout]).wasCalled(once) - } - Post("/open", FormData("nodeId" -> nodeId.toString(), "fundingSatoshis" -> "25000", "channelType" -> "anchor_outputs_zero_fee_htlc_tx").toEntity) ~> addCredentials(BasicHttpCredentials("", mockApi().password)) ~> addHeader("Content-Type", "application/json") ~> @@ -676,7 +623,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM val mockService = new MockService(eclair) val uuid = UUID.fromString("487da196-a4dc-4b1e-92b4-3e5e905e9f3f") - val paymentSent = PaymentSent(uuid, ByteVector32.Zeroes, ByteVector32.One, 25 msat, aliceNodeId, Seq(PaymentSent.PartialPayment(uuid, 21 msat, 1 msat, ByteVector32.Zeroes, None, TimestampMilli(1553784337711L)))) + val paymentSent = PaymentSent(uuid, ByteVector32.Zeroes, ByteVector32.One, 25 msat, aliceNodeId, Seq(PaymentSent.PartialPayment(uuid, 21 msat, 1 msat, ByteVector32.Zeroes, None, TimestampMilli(1553784337711L))), None) eclair.sendBlocking(any, any, any, any, any, any, any)(any[Timeout]).returns(Future.successful(paymentSent)) Post("/payinvoice", FormData("invoice" -> invoice, "blocking" -> "true").toEntity) ~> addCredentials(BasicHttpCredentials("", mockApi().password)) ~> @@ -1180,14 +1127,14 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM system.eventStream.publish(pf) wsClient.expectMessage(expectedSerializedPf) - val ps = PaymentSent(fixedUUID, ByteVector32.Zeroes, ByteVector32.One, 25 msat, aliceNodeId, Seq(PaymentSent.PartialPayment(fixedUUID, 21 msat, 1 msat, ByteVector32.Zeroes, None, TimestampMilli(1553784337711L)))) + val ps = PaymentSent(fixedUUID, ByteVector32.Zeroes, ByteVector32.One, 25 msat, aliceNodeId, Seq(PaymentSent.PartialPayment(fixedUUID, 21 msat, 1 msat, ByteVector32.Zeroes, None, TimestampMilli(1553784337711L))), None) val expectedSerializedPs = """{"type":"payment-sent","id":"487da196-a4dc-4b1e-92b4-3e5e905e9f3f","paymentHash":"0000000000000000000000000000000000000000000000000000000000000000","paymentPreimage":"0100000000000000000000000000000000000000000000000000000000000000","recipientAmount":25,"recipientNodeId":"03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0","parts":[{"id":"487da196-a4dc-4b1e-92b4-3e5e905e9f3f","amount":21,"feesPaid":1,"toChannelId":"0000000000000000000000000000000000000000000000000000000000000000","timestamp":{"iso":"2019-03-28T14:45:37.711Z","unix":1553784337}}]}""" assert(serialization.write(ps) == expectedSerializedPs) system.eventStream.publish(ps) wsClient.expectMessage(expectedSerializedPs) val prel = ChannelPaymentRelayed(21 msat, 20 msat, ByteVector32.Zeroes, ByteVector32.Zeroes, ByteVector32.One, TimestampMilli(1553784961048L), TimestampMilli(1553784963659L)) - val expectedSerializedPrel = """{"type":"payment-relayed","amountIn":21,"amountOut":20,"paymentHash":"0000000000000000000000000000000000000000000000000000000000000000","fromChannelId":"0000000000000000000000000000000000000000000000000000000000000000","toChannelId":"0100000000000000000000000000000000000000000000000000000000000000","startedAt":{"iso":"2019-03-28T14:56:01.048Z","unix":1553784961},"settledAt":{"iso":"2019-03-28T14:56:03.659Z","unix":1553784963}}""" + val expectedSerializedPrel = """{"type":"payment-relayed","amountIn":21,"amountOut":20,"paymentHash":"0000000000000000000000000000000000000000000000000000000000000000","fromChannelId":"0000000000000000000000000000000000000000000000000000000000000000","toChannelId":"0100000000000000000000000000000000000000000000000000000000000000","receivedAt":{"iso":"2019-03-28T14:56:01.048Z","unix":1553784961},"settledAt":{"iso":"2019-03-28T14:56:03.659Z","unix":1553784963}}""" assert(serialization.write(prel) == expectedSerializedPrel) system.eventStream.publish(prel) wsClient.expectMessage(expectedSerializedPrel) diff --git a/pom.xml b/pom.xml index 1054c7e8f4..63c266d7c8 100644 --- a/pom.xml +++ b/pom.xml @@ -20,7 +20,7 @@ fr.acinq.eclair eclair_2.13 - 0.13.0-SNAPSHOT + 0.14.0-SNAPSHOT pom @@ -71,7 +71,7 @@ 2.6.20 10.2.7 3.8.16 - 0.36 + 0.45 32.1.1-jre 2.7.4 1.0.18 @@ -270,8 +270,15 @@ - sonatype snapshots - https://oss.sonatype.org/content/repositories/snapshots/ + central-snapshots + Sonatype Central Portal (snapshots) + https://central.sonatype.com/repository/maven-snapshots + + false + + + true +