From: Jeffrey Czyz Date: Tue, 4 May 2021 00:21:10 +0000 (-0700) Subject: Merge pull request #4 from valentinewallace/sample-v0 X-Git-Url: http://git.bitcoin.ninja/index.cgi?a=commitdiff_plain;h=6199433ab8f13e4f78b12e13a683eb33c999114b;hp=70311fe79bc633c0abfd1c27abbc1d1c05cb1ecf;p=ldk-sample Merge pull request #4 from valentinewallace/sample-v0 Sample v0 --- diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..1a70ea0 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,27 @@ +name: Continuous Integration Checks + +on: [push, pull_request] + +jobs: + build: + strategy: + matrix: + toolchain: [ stable, beta ] + include: + - toolchain: stable + check-fmt: true + runs-on: ubuntu-latest + steps: + - name: Checkout source code + uses: actions/checkout@v2 + - name: Install Rust ${{ matrix.toolchain }} toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: ${{ matrix.toolchain }} + override: true + profile: minimal + - name: Build on Rust ${{ matrix.toolchain }} + run: cargo build --verbose --color always + - name: Check formatting + if: matrix.check-fmt + run: rustup component add rustfmt && cargo fmt --all -- --check diff --git a/.gitignore b/.gitignore index 088ba6b..bf68587 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ Cargo.lock # These are backup files generated by rustfmt **/*.rs.bk +.ldk diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..243c2c0 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,793 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +[[package]] +name = "autocfg" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" + +[[package]] +name = "base-x" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4521f3e3d031370679b3b140beb36dfe4801b09ac77e30c61941f97df3ef28b" + +[[package]] +name = "base64" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" + +[[package]] +name = "bech32" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4408a9bf5c378a42ca9039e4ef3e3d8df3443f76d2ebe249fd720a2c5e17d2da" + +[[package]] +name = "bech32" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdcf67bb7ba7797a081cd19009948ab533af7c355d5caf1d08c777582d351e9c" + +[[package]] +name = "bitcoin" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec5f88a446d66e7474a3b8fa2e348320b574463fb78d799d90ba68f79f48e0e" +dependencies = [ + "bech32 0.7.2", + "bitcoin_hashes", + "secp256k1", +] + +[[package]] +name = "bitcoin-bech32" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5791779c83cf6bde070a92c22505f2faa06487fc8f372e4d485c41a71351105" +dependencies = [ + "bech32 0.4.1", +] + +[[package]] +name = "bitcoin_hashes" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0aaf87b776808e26ae93289bc7d025092b6d909c193f0cdee0b3a86e7bd3c776" + +[[package]] +name = "bumpalo" +version = "3.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "099e596ef14349721d9016f6b80dd3419ea1bf289ab9b44df8e4dfd3a005d5d9" + +[[package]] +name = "bytes" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b700ce4376041dcd0a327fd0097c41095743c4c8af8887265942faf1100bd040" + +[[package]] +name = "cc" +version = "1.0.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c0496836a84f8d0495758516b8621a622beb77c0fed418570e50764093ced48" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chunked_transfer" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fff857943da45f546682664a79488be82e69e43c1a7a2307679ab9afb3a66d2e" + +[[package]] +name = "const_fn" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28b9d6de7f49e22cf97ad17fc4036ece69300032f45f78f30b4a4482cdc3f4a6" + +[[package]] +name = "discard" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "212d0f5754cb6769937f4501cc0e67f4f4483c8d2c3e1e922ee9edbe4ab4c7c0" + +[[package]] +name = "fuchsia-cprng" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" + +[[package]] +name = "futures" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da9052a1a50244d8d5aa9bf55cbc2fb6f357c86cc52e46c62ed390a7180cf150" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2d31b7ec7efab6eefc7c57233bb10b847986139d88cc2f5a02a1ae6871a1846" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79e5145dde8da7d1b3892dad07a9c98fc04bc39892b1ecc9692cf53e2b780a65" + +[[package]] +name = "futures-executor" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9e59fdc009a4b3096bf94f740a0f2424c082521f20a9b08c5c07c48d90fd9b9" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28be053525281ad8259d47e4de5de657b25e7bac113458555bb4b70bc6870500" + +[[package]] +name = "futures-macro" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c287d25add322d9f9abdcdc5927ca398917996600182178774032e9f8258fedd" +dependencies = [ + "proc-macro-hack", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caf5c69029bda2e743fddd0582d1083951d65cc9539aebf8812f36c3491342d6" + +[[package]] +name = "futures-task" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13de07eb8ea81ae445aca7b69f5f7bf15d7bf4912d8ca37d6645c77ae8a58d86" +dependencies = [ + "once_cell", +] + +[[package]] +name = "futures-util" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "632a8cd0f2a4b3fdea1657f08bde063848c3bd00f9bbf6e256b8be78802e624b" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "proc-macro-hack", + "proc-macro-nested", + "slab", +] + +[[package]] +name = "hermit-abi" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "322f4de77956e22ed0e5032c359a0f1273f1f7f0d79bfa3b8ffbc730d7fbcc5c" +dependencies = [ + "libc", +] + +[[package]] +name = "hex" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "805026a5d0141ffc30abb3be3173848ad46a1b1664fe632428479619a3644d77" + +[[package]] +name = "itoa" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736" + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "ldk-tutorial-node" +version = "0.1.0" +dependencies = [ + "base64", + "bech32 0.7.2", + "bitcoin", + "bitcoin-bech32", + "hex", + "lightning", + "lightning-background-processor", + "lightning-block-sync", + "lightning-invoice", + "lightning-net-tokio", + "lightning-persister", + "rand", + "serde_json", + "time", + "tokio", +] + +[[package]] +name = "libc" +version = "0.2.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7282d924be3275cec7f6756ff4121987bc6481325397dde6ba3e7802b1a8b1c" + +[[package]] +name = "lightning" +version = "0.0.13" +source = "git+https://github.com/rust-bitcoin/rust-lightning?rev=d4d322580994857b1222488f8467311d6db61482#d4d322580994857b1222488f8467311d6db61482" +dependencies = [ + "bitcoin", +] + +[[package]] +name = "lightning-background-processor" +version = "0.0.13" +source = "git+https://github.com/rust-bitcoin/rust-lightning?rev=d4d322580994857b1222488f8467311d6db61482#d4d322580994857b1222488f8467311d6db61482" +dependencies = [ + "bitcoin", + "lightning", + "lightning-persister", +] + +[[package]] +name = "lightning-block-sync" +version = "0.0.13" +source = "git+https://github.com/rust-bitcoin/rust-lightning?rev=d4d322580994857b1222488f8467311d6db61482#d4d322580994857b1222488f8467311d6db61482" +dependencies = [ + "bitcoin", + "chunked_transfer", + "futures", + "lightning", + "serde", + "serde_json", +] + +[[package]] +name = "lightning-invoice" +version = "0.4.0" +source = "git+https://github.com/rust-bitcoin/rust-lightning?rev=d4d322580994857b1222488f8467311d6db61482#d4d322580994857b1222488f8467311d6db61482" +dependencies = [ + "bech32 0.7.2", + "bitcoin_hashes", + "lightning", + "num-traits", + "secp256k1", +] + +[[package]] +name = "lightning-net-tokio" +version = "0.0.13" +source = "git+https://github.com/rust-bitcoin/rust-lightning?rev=d4d322580994857b1222488f8467311d6db61482#d4d322580994857b1222488f8467311d6db61482" +dependencies = [ + "bitcoin", + "lightning", + "tokio", +] + +[[package]] +name = "lightning-persister" +version = "0.0.13" +source = "git+https://github.com/rust-bitcoin/rust-lightning?rev=d4d322580994857b1222488f8467311d6db61482#d4d322580994857b1222488f8467311d6db61482" +dependencies = [ + "bitcoin", + "libc", + "lightning", + "winapi", +] + +[[package]] +name = "log" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "memchr" +version = "2.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525" + +[[package]] +name = "mio" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e50ae3f04d169fcc9bde0b547d1c205219b7157e07ded9c5aff03e0637cb3ed7" +dependencies = [ + "libc", + "log", + "miow", + "ntapi", + "winapi", +] + +[[package]] +name = "miow" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a33c1b55807fbed163481b5ba66db4b2fa6cde694a5027be10fb724206c5897" +dependencies = [ + "socket2", + "winapi", +] + +[[package]] +name = "ntapi" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f6bb902e437b6d86e03cce10a7e2af662292c5dfef23b65899ea3ac9354ad44" +dependencies = [ + "winapi", +] + +[[package]] +name = "num-traits" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "once_cell" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13bd41f508810a131401606d54ac32a467c97172d74ba7662562ebba5ad07fa0" + +[[package]] +name = "pin-project-lite" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439697af366c49a6d0a010c56a0d97685bc140ce0d377b13a2ea2aa42d64a827" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "proc-macro-hack" +version = "0.5.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" + +[[package]] +name = "proc-macro-nested" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc881b2c22681370c6a780e47af9840ef841837bc98118431d4e1868bd0c1086" + +[[package]] +name = "proc-macro2" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "quote" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "991431c3519a3f36861882da93630ce66b52918dcf1b8e2fd66b397fc96f28df" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293" +dependencies = [ + "fuchsia-cprng", + "libc", + "rand_core 0.3.1", + "rdrand", + "winapi", +] + +[[package]] +name = "rand_core" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" +dependencies = [ + "rand_core 0.4.2", +] + +[[package]] +name = "rand_core" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" + +[[package]] +name = "rdrand" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" +dependencies = [ + "rand_core 0.3.1", +] + +[[package]] +name = "rustc_version" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" +dependencies = [ + "semver", +] + +[[package]] +name = "ryu" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" + +[[package]] +name = "secp256k1" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "733b114f058f260c0af7591434eef4272ae1a8ec2751766d3cb89c6df8d5e450" +dependencies = [ + "secp256k1-sys", +] + +[[package]] +name = "secp256k1-sys" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67e4b6455ee49f5901c8985b88f98fb0a0e1d90a6661f5a03f4888bd987dad29" +dependencies = [ + "cc", +] + +[[package]] +name = "semver" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" +dependencies = [ + "semver-parser", +] + +[[package]] +name = "semver-parser" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" + +[[package]] +name = "serde" +version = "1.0.123" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d5161132722baa40d802cc70b15262b98258453e85e5d1d365c757c73869ae" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.123" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9391c295d64fc0abb2c556bad848f33cb8296276b1ad2677d1ae1ace4f258f31" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.62" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea1c6153794552ea7cf7cf63b1231a25de00ec90db326ba6264440fa08e31486" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha1" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2579985fda508104f7587689507983eadd6a6e84dd35d6d115361f530916fa0d" + +[[package]] +name = "slab" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8" + +[[package]] +name = "socket2" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "122e570113d28d773067fab24266b66753f6ea915758651696b6e35e49f88d6e" +dependencies = [ + "cfg-if", + "libc", + "winapi", +] + +[[package]] +name = "standback" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2beb4d1860a61f571530b3f855a1b538d0200f7871c63331ecd6f17b1f014f8" +dependencies = [ + "version_check", +] + +[[package]] +name = "stdweb" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d022496b16281348b52d0e30ae99e01a73d737b2f45d38fed4edf79f9325a1d5" +dependencies = [ + "discard", + "rustc_version", + "stdweb-derive", + "stdweb-internal-macros", + "stdweb-internal-runtime", + "wasm-bindgen", +] + +[[package]] +name = "stdweb-derive" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c87a60a40fccc84bef0652345bbbbbe20a605bf5d0ce81719fc476f5c03b50ef" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "serde_derive", + "syn", +] + +[[package]] +name = "stdweb-internal-macros" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58fa5ff6ad0d98d1ffa8cb115892b6e69d67799f6763e162a1c9db421dc22e11" +dependencies = [ + "base-x", + "proc-macro2", + "quote", + "serde", + "serde_derive", + "serde_json", + "sha1", + "syn", +] + +[[package]] +name = "stdweb-internal-runtime" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "213701ba3370744dcd1a12960caa4843b3d68b4d1c0a5d575e0d65b2ee9d16c0" + +[[package]] +name = "syn" +version = "1.0.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c700597eca8a5a762beb35753ef6b94df201c81cca676604f547495a0d7f0081" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "time" +version = "0.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1195b046942c221454c2539395f85413b33383a067449d78aab2b7b052a142f7" +dependencies = [ + "const_fn", + "libc", + "standback", + "stdweb", + "time-macros", + "version_check", + "winapi", +] + +[[package]] +name = "time-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "957e9c6e26f12cb6d0dd7fc776bb67a706312e7299aed74c8dd5b17ebb27e2f1" +dependencies = [ + "proc-macro-hack", + "time-macros-impl", +] + +[[package]] +name = "time-macros-impl" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5c3be1edfad6027c69f5491cf4cb310d1a71ecd6af742788c6ff8bced86b8fa" +dependencies = [ + "proc-macro-hack", + "proc-macro2", + "quote", + "standback", + "syn", +] + +[[package]] +name = "tokio" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8190d04c665ea9e6b6a0dc45523ade572c088d2e6566244c1122671dbf4ae3a" +dependencies = [ + "autocfg", + "bytes", + "libc", + "memchr", + "mio", + "num_cpus", + "pin-project-lite", + "tokio-macros", +] + +[[package]] +name = "tokio-macros" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caf7b11a536f46a809a8a9f0bb4237020f70ecbf115b842360afb127ea2fda57" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-xid" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" + +[[package]] +name = "version_check" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed" + +[[package]] +name = "wasm-bindgen" +version = "0.2.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55c0f7123de74f0dab9b7d00fd614e7b19349cd1e2f5252bbe9b1754b59433be" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bc45447f0d4573f3d65720f636bbcc3dd6ce920ed704670118650bcd47764c7" +dependencies = [ + "bumpalo", + "lazy_static", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b8853882eef39593ad4174dd26fc9865a64e84026d223f63bb2c42affcbba2c" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4133b5e7f2a531fa413b3a1695e925038a05a71cf67e87dafa295cb645a01385" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd4945e4943ae02d15c13962b38a5b1e81eadd4b71214eee75af64a4d6a4fd64" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..b32be57 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,43 @@ +[package] +name = "ldk-tutorial-node" +version = "0.1.0" +authors = ["Valentine Wallace "] +license = "MIT OR Apache-2.0" +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +# lightning-background-processor = { git = "https://github.com/rust-bitcoin/rust-lightning", branch = "main" } +lightning-background-processor = { git = "https://github.com/rust-bitcoin/rust-lightning", rev = "d4d322580994857b1222488f8467311d6db61482" } +# lightning-background-processor = { path = "../rust-lightning/lightning-background-processor" } +base64 = "0.13.0" +bitcoin = "0.26" +bitcoin-bech32 = "0.7" +bech32 = "0.7" +hex = "0.3" + +# lightning = { git = "https://github.com/rust-bitcoin/rust-lightning", branch = "main" } +lightning = { git = "https://github.com/rust-bitcoin/rust-lightning", rev = "d4d322580994857b1222488f8467311d6db61482" } +# lightning = { path = "../rust-lightning/lightning" } + +# lightning-block-sync = { git = "https://github.com/rust-bitcoin/rust-lightning", features = ["rpc-client"], branch = "main" } +lightning-block-sync = { git = "https://github.com/rust-bitcoin/rust-lightning", features = ["rpc-client"], rev = "d4d322580994857b1222488f8467311d6db61482" } +# lightning-block-sync = { path = "../rust-lightning/lightning-block-sync", features = ["rpc-client"] } + +# lightning-invoice = { git = "https://github.com/rust-bitcoin/rust-lightning", branch = "main" } +lightning-invoice = { git = "https://github.com/rust-bitcoin/rust-lightning", rev = "d4d322580994857b1222488f8467311d6db61482" } +# lightning-invoice = { path = "../rust-lightning/lightning-invoice" } + +# lightning-net-tokio = { git = "https://github.com/rust-bitcoin/rust-lightning", branch = "main" } +lightning-net-tokio = { git = "https://github.com/rust-bitcoin/rust-lightning", rev = "d4d322580994857b1222488f8467311d6db61482" } +# lightning-net-tokio = { path = "../rust-lightning/lightning-net-tokio" } + +# lightning-persister = { git = "https://github.com/rust-bitcoin/rust-lightning", branch = "main" } +lightning-persister = { git = "https://github.com/rust-bitcoin/rust-lightning", rev = "d4d322580994857b1222488f8467311d6db61482" } +# lightning-persister = { path = "../rust-lightning/lightning-persister" } + +time = "0.2" +rand = "0.4" +serde_json = { version = "1.0" } +tokio = { version = "1.0", features = [ "io-util", "macros", "rt", "rt-multi-thread", "sync", "net", "time" ] } diff --git a/README.md b/README.md index 6358f4f..5dda263 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,25 @@ # ldk-sample -sample node implementation using LDK +Sample node implementation using LDK. + +## Installation +``` +git clone git@github.com:lightningdevkit/ldk-sample.git +``` + +## Usage +``` +cd ldk-sample +cargo run :@: [] [bitcoin-network] +``` +`bitcoind`'s RPC username and password likely can be found through `cat ~/.bitcoin/.cookie`. + +`bitcoin-network`: defaults to `testnet`. Options: `testnet`, `regtest`. + +`ldk-peer-listening-port`: defaults to 9735. ## License -Licensed under either of +Licensed under either: * Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) * MIT License ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..c00f655 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,5 @@ +hard_tabs = true # use tab characters for indentation, spaces for alignment +use_field_init_shorthand = true +max_width = 100 +use_small_heuristics = "Max" +fn_args_layout = "Compressed" diff --git a/src/bitcoind_client.rs b/src/bitcoind_client.rs new file mode 100644 index 0000000..58510ae --- /dev/null +++ b/src/bitcoind_client.rs @@ -0,0 +1,237 @@ +use crate::convert::{BlockchainInfo, FeeResponse, FundedTx, NewAddress, RawTx, SignedTx}; +use base64; +use bitcoin::blockdata::block::Block; +use bitcoin::blockdata::transaction::Transaction; +use bitcoin::consensus::encode; +use bitcoin::hash_types::BlockHash; +use bitcoin::util::address::Address; +use lightning::chain::chaininterface::{BroadcasterInterface, ConfirmationTarget, FeeEstimator}; +use lightning_block_sync::http::HttpEndpoint; +use lightning_block_sync::rpc::RpcClient; +use lightning_block_sync::{AsyncBlockSourceResult, BlockHeaderData, BlockSource}; +use serde_json; +use std::collections::HashMap; +use std::str::FromStr; +use std::sync::atomic::{AtomicU32, Ordering}; +use std::sync::Arc; +use std::time::Duration; +use tokio::sync::Mutex; + +pub struct BitcoindClient { + bitcoind_rpc_client: Arc>, + host: String, + port: u16, + rpc_user: String, + rpc_password: String, + fees: Arc>, +} + +#[derive(Clone, Eq, Hash, PartialEq)] +pub enum Target { + Background, + Normal, + HighPriority, +} + +impl BlockSource for &BitcoindClient { + fn get_header<'a>( + &'a mut self, header_hash: &'a BlockHash, height_hint: Option, + ) -> AsyncBlockSourceResult<'a, BlockHeaderData> { + Box::pin(async move { + let mut rpc = self.bitcoind_rpc_client.lock().await; + rpc.get_header(header_hash, height_hint).await + }) + } + + fn get_block<'a>( + &'a mut self, header_hash: &'a BlockHash, + ) -> AsyncBlockSourceResult<'a, Block> { + Box::pin(async move { + let mut rpc = self.bitcoind_rpc_client.lock().await; + rpc.get_block(header_hash).await + }) + } + + fn get_best_block<'a>(&'a mut self) -> AsyncBlockSourceResult<(BlockHash, Option)> { + Box::pin(async move { + let mut rpc = self.bitcoind_rpc_client.lock().await; + rpc.get_best_block().await + }) + } +} + +impl BitcoindClient { + pub async fn new( + host: String, port: u16, rpc_user: String, rpc_password: String, + ) -> std::io::Result { + let http_endpoint = HttpEndpoint::for_host(host.clone()).with_port(port); + let rpc_credentials = + base64::encode(format!("{}:{}", rpc_user.clone(), rpc_password.clone())); + let bitcoind_rpc_client = RpcClient::new(&rpc_credentials, http_endpoint)?; + let mut fees: HashMap = HashMap::new(); + fees.insert(Target::Background, AtomicU32::new(253)); + fees.insert(Target::Normal, AtomicU32::new(2000)); + fees.insert(Target::HighPriority, AtomicU32::new(5000)); + let client = Self { + bitcoind_rpc_client: Arc::new(Mutex::new(bitcoind_rpc_client)), + host, + port, + rpc_user, + rpc_password, + fees: Arc::new(fees), + }; + BitcoindClient::poll_for_fee_estimates( + client.fees.clone(), + client.bitcoind_rpc_client.clone(), + ) + .await; + Ok(client) + } + + async fn poll_for_fee_estimates( + fees: Arc>, rpc_client: Arc>, + ) { + tokio::spawn(async move { + loop { + let background_estimate = { + let mut rpc = rpc_client.lock().await; + let background_conf_target = serde_json::json!(144); + let background_estimate_mode = serde_json::json!("ECONOMICAL"); + let resp = rpc + .call_method::( + "estimatesmartfee", + &vec![background_conf_target, background_estimate_mode], + ) + .await + .unwrap(); + match resp.feerate { + Some(fee) => fee, + None => 253, + } + }; + // if background_estimate. + + let normal_estimate = { + let mut rpc = rpc_client.lock().await; + let normal_conf_target = serde_json::json!(18); + let normal_estimate_mode = serde_json::json!("ECONOMICAL"); + let resp = rpc + .call_method::( + "estimatesmartfee", + &vec![normal_conf_target, normal_estimate_mode], + ) + .await + .unwrap(); + match resp.feerate { + Some(fee) => fee, + None => 2000, + } + }; + + let high_prio_estimate = { + let mut rpc = rpc_client.lock().await; + let high_prio_conf_target = serde_json::json!(6); + let high_prio_estimate_mode = serde_json::json!("CONSERVATIVE"); + let resp = rpc + .call_method::( + "estimatesmartfee", + &vec![high_prio_conf_target, high_prio_estimate_mode], + ) + .await + .unwrap(); + + match resp.feerate { + Some(fee) => fee, + None => 5000, + } + }; + + fees.get(&Target::Background) + .unwrap() + .store(background_estimate, Ordering::Release); + fees.get(&Target::Normal).unwrap().store(normal_estimate, Ordering::Release); + fees.get(&Target::HighPriority) + .unwrap() + .store(high_prio_estimate, Ordering::Release); + tokio::time::sleep(Duration::from_secs(60)).await; + } + }); + } + + pub fn get_new_rpc_client(&self) -> std::io::Result { + let http_endpoint = HttpEndpoint::for_host(self.host.clone()).with_port(self.port); + let rpc_credentials = + base64::encode(format!("{}:{}", self.rpc_user.clone(), self.rpc_password.clone())); + RpcClient::new(&rpc_credentials, http_endpoint) + } + + pub async fn create_raw_transaction(&self, outputs: Vec>) -> RawTx { + let mut rpc = self.bitcoind_rpc_client.lock().await; + + let outputs_json = serde_json::json!(outputs); + rpc.call_method::("createrawtransaction", &vec![serde_json::json!([]), outputs_json]) + .await + .unwrap() + } + + pub async fn fund_raw_transaction(&self, raw_tx: RawTx) -> FundedTx { + let mut rpc = self.bitcoind_rpc_client.lock().await; + + let raw_tx_json = serde_json::json!(raw_tx.0); + rpc.call_method("fundrawtransaction", &[raw_tx_json]).await.unwrap() + } + + pub async fn send_raw_transaction(&self, raw_tx: RawTx) { + let mut rpc = self.bitcoind_rpc_client.lock().await; + + let raw_tx_json = serde_json::json!(raw_tx.0); + rpc.call_method::("sendrawtransaction", &[raw_tx_json]).await.unwrap(); + } + + pub async fn sign_raw_transaction_with_wallet(&self, tx_hex: String) -> SignedTx { + let mut rpc = self.bitcoind_rpc_client.lock().await; + + let tx_hex_json = serde_json::json!(tx_hex); + rpc.call_method("signrawtransactionwithwallet", &vec![tx_hex_json]).await.unwrap() + } + + pub async fn get_new_address(&self) -> Address { + let mut rpc = self.bitcoind_rpc_client.lock().await; + + let addr_args = vec![serde_json::json!("LDK output address")]; + let addr = rpc.call_method::("getnewaddress", &addr_args).await.unwrap(); + Address::from_str(addr.0.as_str()).unwrap() + } + + pub async fn get_blockchain_info(&self) -> BlockchainInfo { + let mut rpc = self.bitcoind_rpc_client.lock().await; + rpc.call_method::("getblockchaininfo", &vec![]).await.unwrap() + } +} + +impl FeeEstimator for BitcoindClient { + fn get_est_sat_per_1000_weight(&self, confirmation_target: ConfirmationTarget) -> u32 { + match confirmation_target { + ConfirmationTarget::Background => { + self.fees.get(&Target::Background).unwrap().load(Ordering::Acquire) + } + ConfirmationTarget::Normal => { + self.fees.get(&Target::Normal).unwrap().load(Ordering::Acquire) + } + ConfirmationTarget::HighPriority => { + self.fees.get(&Target::HighPriority).unwrap().load(Ordering::Acquire) + } + } + } +} + +impl BroadcasterInterface for BitcoindClient { + fn broadcast_transaction(&self, tx: &Transaction) { + let bitcoind_rpc_client = self.bitcoind_rpc_client.clone(); + let tx_serialized = serde_json::json!(encode::serialize_hex(tx)); + tokio::spawn(async move { + let mut rpc = bitcoind_rpc_client.lock().await; + rpc.call_method::("sendrawtransaction", &vec![tx_serialized]).await.unwrap(); + }); + } +} diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 0000000..c5dda5f --- /dev/null +++ b/src/cli.rs @@ -0,0 +1,647 @@ +use crate::disk; +use crate::hex_utils; +use crate::{ + ChannelManager, FilesystemLogger, HTLCStatus, MillisatAmount, PaymentInfo, PaymentInfoStorage, + PeerManager, +}; +use bitcoin::network::constants::Network; +use bitcoin::secp256k1::key::PublicKey; +use lightning::chain; +use lightning::chain::keysinterface::KeysManager; +use lightning::ln::features::InvoiceFeatures; +use lightning::ln::{PaymentHash, PaymentSecret}; +use lightning::routing::network_graph::NetGraphMsgHandler; +use lightning::routing::router; +use lightning::routing::router::RouteHintHop; +use lightning::util::config::UserConfig; +use lightning_invoice::{utils, Currency, Invoice}; +use std::env; +use std::io; +use std::io::{BufRead, Write}; +use std::net::{SocketAddr, TcpStream}; +use std::ops::Deref; +use std::path::Path; +use std::str::FromStr; +use std::sync::Arc; +use std::time::Duration; +use tokio::sync::mpsc; + +pub(crate) struct LdkUserInfo { + pub(crate) bitcoind_rpc_username: String, + pub(crate) bitcoind_rpc_password: String, + pub(crate) bitcoind_rpc_port: u16, + pub(crate) bitcoind_rpc_host: String, + pub(crate) ldk_storage_dir_path: String, + pub(crate) ldk_peer_listening_port: u16, + pub(crate) network: Network, +} + +pub(crate) fn parse_startup_args() -> Result { + if env::args().len() < 3 { + println!("ldk-tutorial-node requires 3 arguments: `cargo run :@: ldk_storage_directory_path [] [bitcoin-network]`"); + return Err(()); + } + let bitcoind_rpc_info = env::args().skip(1).next().unwrap(); + let bitcoind_rpc_info_parts: Vec<&str> = bitcoind_rpc_info.split("@").collect(); + if bitcoind_rpc_info_parts.len() != 2 { + println!("ERROR: bad bitcoind RPC URL provided"); + return Err(()); + } + let rpc_user_and_password: Vec<&str> = bitcoind_rpc_info_parts[0].split(":").collect(); + if rpc_user_and_password.len() != 2 { + println!("ERROR: bad bitcoind RPC username/password combo provided"); + return Err(()); + } + let bitcoind_rpc_username = rpc_user_and_password[0].to_string(); + let bitcoind_rpc_password = rpc_user_and_password[1].to_string(); + let bitcoind_rpc_path: Vec<&str> = bitcoind_rpc_info_parts[1].split(":").collect(); + if bitcoind_rpc_path.len() != 2 { + println!("ERROR: bad bitcoind RPC path provided"); + return Err(()); + } + let bitcoind_rpc_host = bitcoind_rpc_path[0].to_string(); + let bitcoind_rpc_port = bitcoind_rpc_path[1].parse::().unwrap(); + + let ldk_storage_dir_path = env::args().skip(2).next().unwrap(); + + let mut ldk_peer_port_set = true; + let ldk_peer_listening_port: u16 = match env::args().skip(3).next().map(|p| p.parse()) { + Some(Ok(p)) => p, + Some(Err(e)) => panic!("{}", e), + None => { + ldk_peer_port_set = false; + 9735 + } + }; + + let arg_idx = match ldk_peer_port_set { + true => 4, + false => 3, + }; + let network: Network = match env::args().skip(arg_idx).next().as_ref().map(String::as_str) { + Some("testnet") => Network::Testnet, + Some("regtest") => Network::Regtest, + Some(_) => panic!("Unsupported network provided. Options are: `regtest`, `testnet`"), + None => Network::Testnet, + }; + Ok(LdkUserInfo { + bitcoind_rpc_username, + bitcoind_rpc_password, + bitcoind_rpc_host, + bitcoind_rpc_port, + ldk_storage_dir_path, + ldk_peer_listening_port, + network, + }) +} + +pub(crate) async fn poll_for_user_input( + peer_manager: Arc, channel_manager: Arc, + keys_manager: Arc, + router: Arc, Arc>>, + inbound_payments: PaymentInfoStorage, outbound_payments: PaymentInfoStorage, + event_notifier: mpsc::Sender<()>, ldk_data_dir: String, logger: Arc, + network: Network, +) { + println!("LDK startup successful. To view available commands: \"help\".\nLDK logs are available at /.ldk/logs"); + let stdin = io::stdin(); + print!("> "); + io::stdout().flush().unwrap(); // Without flushing, the `>` doesn't print + for line in stdin.lock().lines() { + let _ = event_notifier.try_send(()); + let line = line.unwrap(); + let mut words = line.split_whitespace(); + if let Some(word) = words.next() { + match word { + "help" => help(), + "openchannel" => { + let peer_pubkey_and_ip_addr = words.next(); + let channel_value_sat = words.next(); + if peer_pubkey_and_ip_addr.is_none() || channel_value_sat.is_none() { + println!("ERROR: openchannel has 2 required arguments: `openchannel pubkey@host:port channel_amt_satoshis` [--public]"); + print!("> "); + io::stdout().flush().unwrap(); + continue; + } + let peer_pubkey_and_ip_addr = peer_pubkey_and_ip_addr.unwrap(); + let (pubkey, peer_addr) = + match parse_peer_info(peer_pubkey_and_ip_addr.to_string()) { + Ok(info) => info, + Err(e) => { + println!("{:?}", e.into_inner().unwrap()); + print!("> "); + io::stdout().flush().unwrap(); + continue; + } + }; + + let chan_amt_sat: Result = channel_value_sat.unwrap().parse(); + if chan_amt_sat.is_err() { + println!("ERROR: channel amount must be a number"); + print!("> "); + io::stdout().flush().unwrap(); + continue; + } + + if connect_peer_if_necessary( + pubkey, + peer_addr, + peer_manager.clone(), + event_notifier.clone(), + ) + .is_err() + { + print!("> "); + io::stdout().flush().unwrap(); + continue; + }; + + let announce_channel = match words.next() { + Some("--public") | Some("--public=true") => true, + Some("--public=false") => false, + Some(_) => { + println!("ERROR: invalid `--public` command format. Valid formats: `--public`, `--public=true` `--public=false`"); + print!("> "); + io::stdout().flush().unwrap(); + continue; + } + None => false, + }; + + if open_channel( + pubkey, + chan_amt_sat.unwrap(), + announce_channel, + channel_manager.clone(), + ) + .is_ok() + { + let peer_data_path = format!("{}/channel_peer_data", ldk_data_dir.clone()); + let _ = disk::persist_channel_peer( + Path::new(&peer_data_path), + peer_pubkey_and_ip_addr, + ); + } + } + "sendpayment" => { + let invoice_str = words.next(); + if invoice_str.is_none() { + println!("ERROR: sendpayment requires an invoice: `sendpayment `"); + print!("> "); + io::stdout().flush().unwrap(); + continue; + } + + let invoice = match Invoice::from_str(invoice_str.unwrap()) { + Ok(inv) => inv, + Err(e) => { + println!("ERROR: invalid invoice: {:?}", e); + print!("> "); + io::stdout().flush().unwrap(); + continue; + } + }; + let mut route_hints = invoice.routes().clone(); + let mut last_hops = Vec::new(); + for hint in route_hints.drain(..) { + last_hops.push(hint[hint.len() - 1].clone()); + } + + let amt_pico_btc = invoice.amount_pico_btc(); + if amt_pico_btc.is_none() { + println!("ERROR: invalid invoice: must contain amount to pay"); + print!("> "); + io::stdout().flush().unwrap(); + continue; + } + let amt_msat = amt_pico_btc.unwrap() / 10; + + let payee_pubkey = invoice.recover_payee_pub_key(); + let final_cltv = invoice.min_final_cltv_expiry() as u32; + + let mut payment_hash = PaymentHash([0; 32]); + payment_hash.0.copy_from_slice(&invoice.payment_hash().as_ref()[0..32]); + + let payment_secret = match invoice.payment_secret() { + Some(secret) => { + let mut payment_secret = PaymentSecret([0; 32]); + payment_secret.0.copy_from_slice(&secret.0); + Some(payment_secret) + } + None => None, + }; + + let invoice_features = match invoice.features() { + Some(feat) => Some(feat.clone()), + None => None, + }; + + send_payment( + payee_pubkey, + amt_msat, + final_cltv, + payment_hash, + payment_secret, + invoice_features, + last_hops, + router.clone(), + channel_manager.clone(), + outbound_payments.clone(), + logger.clone(), + ); + } + "getinvoice" => { + let amt_str = words.next(); + if amt_str.is_none() { + println!("ERROR: getinvoice requires an amount in millisatoshis"); + print!("> "); + io::stdout().flush().unwrap(); + continue; + } + + let amt_msat: Result = amt_str.unwrap().parse(); + if amt_msat.is_err() { + println!("ERROR: getinvoice provided payment amount was not a number"); + print!("> "); + io::stdout().flush().unwrap(); + continue; + } + get_invoice( + amt_msat.unwrap(), + inbound_payments.clone(), + channel_manager.clone(), + keys_manager.clone(), + network, + ); + } + "connectpeer" => { + let peer_pubkey_and_ip_addr = words.next(); + if peer_pubkey_and_ip_addr.is_none() { + println!("ERROR: connectpeer requires peer connection info: `connectpeer pubkey@host:port`"); + print!("> "); + io::stdout().flush().unwrap(); + continue; + } + let (pubkey, peer_addr) = + match parse_peer_info(peer_pubkey_and_ip_addr.unwrap().to_string()) { + Ok(info) => info, + Err(e) => { + println!("{:?}", e.into_inner().unwrap()); + print!("> "); + io::stdout().flush().unwrap(); + continue; + } + }; + if connect_peer_if_necessary( + pubkey, + peer_addr, + peer_manager.clone(), + event_notifier.clone(), + ) + .is_ok() + { + println!("SUCCESS: connected to peer {}", pubkey); + } + } + "listchannels" => list_channels(channel_manager.clone()), + "listpayments" => { + list_payments(inbound_payments.clone(), outbound_payments.clone()) + } + "closechannel" => { + let channel_id_str = words.next(); + if channel_id_str.is_none() { + println!("ERROR: closechannel requires a channel ID: `closechannel `"); + print!("> "); + io::stdout().flush().unwrap(); + continue; + } + let channel_id_vec = hex_utils::to_vec(channel_id_str.unwrap()); + if channel_id_vec.is_none() { + println!("ERROR: couldn't parse channel_id as hex"); + print!("> "); + io::stdout().flush().unwrap(); + continue; + } + let mut channel_id = [0; 32]; + channel_id.copy_from_slice(&channel_id_vec.unwrap()); + close_channel(channel_id, channel_manager.clone()); + } + "forceclosechannel" => { + let channel_id_str = words.next(); + if channel_id_str.is_none() { + println!("ERROR: forceclosechannel requires a channel ID: `forceclosechannel `"); + print!("> "); + io::stdout().flush().unwrap(); + continue; + } + let channel_id_vec = hex_utils::to_vec(channel_id_str.unwrap()); + if channel_id_vec.is_none() { + println!("ERROR: couldn't parse channel_id as hex"); + print!("> "); + io::stdout().flush().unwrap(); + continue; + } + let mut channel_id = [0; 32]; + channel_id.copy_from_slice(&channel_id_vec.unwrap()); + force_close_channel(channel_id, channel_manager.clone()); + } + "nodeinfo" => node_info(channel_manager.clone(), peer_manager.clone()), + "listpeers" => list_peers(peer_manager.clone()), + _ => println!("Unknown command. See `\"help\" for available commands."), + } + } + print!("> "); + io::stdout().flush().unwrap(); + } +} + +fn help() { + println!("openchannel pubkey@host:port "); + println!("sendpayment "); + println!("getinvoice "); + println!("connectpeer pubkey@host:port"); + println!("listchannels"); + println!("listpayments"); + println!("closechannel "); + println!("forceclosechannel "); +} + +fn node_info(channel_manager: Arc, peer_manager: Arc) { + println!("\t{{"); + println!("\t\t node_pubkey: {}", channel_manager.get_our_node_id()); + println!("\t\t num_channels: {}", channel_manager.list_channels().len()); + println!("\t\t num_usable_channels: {}", channel_manager.list_usable_channels().len()); + println!("\t\t num_peers: {}", peer_manager.get_peer_node_ids().len()); + println!("\t}},"); +} + +fn list_peers(peer_manager: Arc) { + println!("\t{{"); + for pubkey in peer_manager.get_peer_node_ids() { + println!("\t\t pubkey: {}", pubkey); + } + println!("\t}},"); +} + +fn list_channels(channel_manager: Arc) { + print!("["); + for chan_info in channel_manager.list_channels() { + println!(""); + println!("\t{{"); + println!("\t\tchannel_id: {},", hex_utils::hex_str(&chan_info.channel_id[..])); + println!( + "\t\tpeer_pubkey: {},", + hex_utils::hex_str(&chan_info.remote_network_id.serialize()) + ); + let mut pending_channel = false; + match chan_info.short_channel_id { + Some(id) => println!("\t\tshort_channel_id: {},", id), + None => { + pending_channel = true; + } + } + println!("\t\tpending_open: {},", pending_channel); + println!("\t\tchannel_value_satoshis: {},", chan_info.channel_value_satoshis); + println!("\t\tchannel_can_send_payments: {},", chan_info.is_live); + println!("\t}},"); + } + println!("]"); +} + +fn list_payments(inbound_payments: PaymentInfoStorage, outbound_payments: PaymentInfoStorage) { + let inbound = inbound_payments.lock().unwrap(); + let outbound = outbound_payments.lock().unwrap(); + print!("["); + for (payment_hash, payment_info) in inbound.deref() { + println!(""); + println!("\t{{"); + println!("\t\tamount_millisatoshis: {},", payment_info.amt_msat); + println!("\t\tpayment_hash: {},", hex_utils::hex_str(&payment_hash.0)); + println!("\t\thtlc_direction: inbound,"); + println!( + "\t\thtlc_status: {},", + match payment_info.status { + HTLCStatus::Pending => "pending", + HTLCStatus::Succeeded => "succeeded", + HTLCStatus::Failed => "failed", + } + ); + + println!("\t}},"); + } + + for (payment_hash, payment_info) in outbound.deref() { + println!(""); + println!("\t{{"); + println!("\t\tamount_millisatoshis: {},", payment_info.amt_msat); + println!("\t\tpayment_hash: {},", hex_utils::hex_str(&payment_hash.0)); + println!("\t\thtlc_direction: outbound,"); + println!( + "\t\thtlc_status: {},", + match payment_info.status { + HTLCStatus::Pending => "pending", + HTLCStatus::Succeeded => "succeeded", + HTLCStatus::Failed => "failed", + } + ); + + println!("\t}},"); + } + println!("]"); +} + +pub(crate) fn connect_peer_if_necessary( + pubkey: PublicKey, peer_addr: SocketAddr, peer_manager: Arc, + event_notifier: mpsc::Sender<()>, +) -> Result<(), ()> { + for node_pubkey in peer_manager.get_peer_node_ids() { + if node_pubkey == pubkey { + return Ok(()); + } + } + match TcpStream::connect_timeout(&peer_addr, Duration::from_secs(10)) { + Ok(stream) => { + let peer_mgr = peer_manager.clone(); + let event_ntfns = event_notifier.clone(); + tokio::spawn(async move { + lightning_net_tokio::setup_outbound(peer_mgr, event_ntfns, pubkey, stream).await; + }); + let mut peer_connected = false; + while !peer_connected { + for node_pubkey in peer_manager.get_peer_node_ids() { + if node_pubkey == pubkey { + peer_connected = true; + } + } + } + } + Err(e) => { + println!("ERROR: failed to connect to peer: {:?}", e); + return Err(()); + } + } + Ok(()) +} + +fn open_channel( + peer_pubkey: PublicKey, channel_amt_sat: u64, announce_channel: bool, + channel_manager: Arc, +) -> Result<(), ()> { + let mut config = UserConfig::default(); + if announce_channel { + config.channel_options.announced_channel = true; + } + // lnd's max to_self_delay is 2016, so we want to be compatible. + config.peer_channel_config_limits.their_to_self_delay = 2016; + match channel_manager.create_channel(peer_pubkey, channel_amt_sat, 0, 0, None) { + Ok(_) => { + println!("EVENT: initiated channel with peer {}. ", peer_pubkey); + return Ok(()); + } + Err(e) => { + println!("ERROR: failed to open channel: {:?}", e); + return Err(()); + } + } +} + +fn send_payment( + payee: PublicKey, amt_msat: u64, final_cltv: u32, payment_hash: PaymentHash, + payment_secret: Option, payee_features: Option, + route_hints: Vec, + router: Arc, Arc>>, + channel_manager: Arc, payment_storage: PaymentInfoStorage, + logger: Arc, +) { + let network_graph = router.network_graph.read().unwrap(); + let first_hops = channel_manager.list_usable_channels(); + let payer_pubkey = channel_manager.get_our_node_id(); + + let route = router::get_route( + &payer_pubkey, + &network_graph, + &payee, + payee_features, + Some(&first_hops.iter().collect::>()), + &route_hints.iter().collect::>(), + amt_msat, + final_cltv, + logger, + ); + if let Err(e) = route { + println!("ERROR: failed to find route: {}", e.err); + return; + } + let status = match channel_manager.send_payment(&route.unwrap(), payment_hash, &payment_secret) + { + Ok(()) => { + println!("EVENT: initiated sending {} msats to {}", amt_msat, payee); + HTLCStatus::Pending + } + Err(e) => { + println!("ERROR: failed to send payment: {:?}", e); + HTLCStatus::Failed + } + }; + let mut payments = payment_storage.lock().unwrap(); + payments.insert( + payment_hash, + PaymentInfo { + preimage: None, + secret: payment_secret, + status, + amt_msat: MillisatAmount(Some(amt_msat)), + }, + ); +} + +fn get_invoice( + amt_msat: u64, payment_storage: PaymentInfoStorage, channel_manager: Arc, + keys_manager: Arc, network: Network, +) { + let mut payments = payment_storage.lock().unwrap(); + let currency = match network { + Network::Bitcoin => Currency::Bitcoin, + Network::Testnet => Currency::BitcoinTestnet, + Network::Regtest => Currency::Regtest, + Network::Signet => panic!("Signet unsupported"), + }; + let invoice = match utils::create_invoice_from_channelmanager( + &channel_manager, + keys_manager, + currency, + Some(amt_msat), + "ldk-tutorial-node".to_string(), + ) { + Ok(inv) => { + println!("SUCCESS: generated invoice: {}", inv); + inv + } + Err(e) => { + println!("ERROR: failed to create invoice: {:?}", e); + return; + } + }; + + let mut payment_hash = PaymentHash([0; 32]); + payment_hash.0.copy_from_slice(&invoice.payment_hash().as_ref()[0..32]); + payments.insert( + payment_hash, + PaymentInfo { + preimage: None, + // We can't add payment secrets to invoices until we support features in invoices. + // Otherwise lnd errors with "destination hop doesn't understand payment addresses" + // (for context, lnd calls payment secrets "payment addresses"). + secret: Some(invoice.payment_secret().unwrap().clone()), + status: HTLCStatus::Pending, + amt_msat: MillisatAmount(Some(amt_msat)), + }, + ); +} + +fn close_channel(channel_id: [u8; 32], channel_manager: Arc) { + match channel_manager.close_channel(&channel_id) { + Ok(()) => println!("EVENT: initiating channel close"), + Err(e) => println!("ERROR: failed to close channel: {:?}", e), + } +} + +fn force_close_channel(channel_id: [u8; 32], channel_manager: Arc) { + match channel_manager.force_close_channel(&channel_id) { + Ok(()) => println!("EVENT: initiating channel force-close"), + Err(e) => println!("ERROR: failed to force-close channel: {:?}", e), + } +} + +pub(crate) fn parse_peer_info( + peer_pubkey_and_ip_addr: String, +) -> Result<(PublicKey, SocketAddr), std::io::Error> { + let mut pubkey_and_addr = peer_pubkey_and_ip_addr.split("@"); + let pubkey = pubkey_and_addr.next(); + let peer_addr_str = pubkey_and_addr.next(); + if peer_addr_str.is_none() || peer_addr_str.is_none() { + return Err(std::io::Error::new( + std::io::ErrorKind::Other, + "ERROR: incorrectly formatted peer + info. Should be formatted as: `pubkey@host:port`", + )); + } + + let peer_addr: Result = peer_addr_str.unwrap().parse(); + if peer_addr.is_err() { + return Err(std::io::Error::new( + std::io::ErrorKind::Other, + "ERROR: couldn't parse pubkey@host:port into a socket address", + )); + } + + let pubkey = hex_utils::to_compressed_pubkey(pubkey.unwrap()); + if pubkey.is_none() { + return Err(std::io::Error::new( + std::io::ErrorKind::Other, + "ERROR: unable to parse given pubkey for node", + )); + } + + Ok((pubkey.unwrap(), peer_addr.unwrap())) +} diff --git a/src/convert.rs b/src/convert.rs new file mode 100644 index 0000000..7dbd8cc --- /dev/null +++ b/src/convert.rs @@ -0,0 +1,86 @@ +use bitcoin::hashes::hex::FromHex; +use bitcoin::BlockHash; +use lightning_block_sync::http::JsonResponse; +use std::convert::TryInto; + +pub struct FundedTx { + pub changepos: i64, + pub hex: String, +} + +impl TryInto for JsonResponse { + type Error = std::io::Error; + fn try_into(self) -> std::io::Result { + Ok(FundedTx { + changepos: self.0["changepos"].as_i64().unwrap(), + hex: self.0["hex"].as_str().unwrap().to_string(), + }) + } +} + +pub struct RawTx(pub String); + +impl TryInto for JsonResponse { + type Error = std::io::Error; + fn try_into(self) -> std::io::Result { + Ok(RawTx(self.0.as_str().unwrap().to_string())) + } +} + +pub struct SignedTx { + pub complete: bool, + pub hex: String, +} + +impl TryInto for JsonResponse { + type Error = std::io::Error; + fn try_into(self) -> std::io::Result { + Ok(SignedTx { + hex: self.0["hex"].as_str().unwrap().to_string(), + complete: self.0["complete"].as_bool().unwrap(), + }) + } +} + +pub struct NewAddress(pub String); +impl TryInto for JsonResponse { + type Error = std::io::Error; + fn try_into(self) -> std::io::Result { + Ok(NewAddress(self.0.as_str().unwrap().to_string())) + } +} + +pub struct FeeResponse { + pub feerate: Option, + pub errored: bool, +} + +impl TryInto for JsonResponse { + type Error = std::io::Error; + fn try_into(self) -> std::io::Result { + let errored = !self.0["errors"].is_null(); + Ok(FeeResponse { + errored, + feerate: match self.0["feerate"].as_f64() { + Some(fee) => Some((fee * 100_000_000.0).round() as u32), + None => None, + }, + }) + } +} + +pub struct BlockchainInfo { + pub latest_height: usize, + pub latest_blockhash: BlockHash, +} + +impl TryInto for JsonResponse { + type Error = std::io::Error; + fn try_into(self) -> std::io::Result { + Ok(BlockchainInfo { + latest_height: self.0["blocks"].as_u64().unwrap() as usize, + latest_blockhash: BlockHash::from_hex(self.0["bestblockhash"].as_str().unwrap()) + .unwrap(), + }) + } +} diff --git a/src/disk.rs b/src/disk.rs new file mode 100644 index 0000000..827cb7e --- /dev/null +++ b/src/disk.rs @@ -0,0 +1,67 @@ +use crate::cli; +use bitcoin::secp256k1::key::PublicKey; +use lightning::util::logger::{Logger, Record}; +use lightning::util::ser::Writer; +use std::collections::HashMap; +use std::fs; +use std::fs::File; +use std::io::{BufRead, BufReader}; +use std::net::SocketAddr; +use std::path::Path; +use time::OffsetDateTime; + +pub(crate) struct FilesystemLogger { + data_dir: String, +} +impl FilesystemLogger { + pub(crate) fn new(data_dir: String) -> Self { + let logs_path = format!("{}/logs", data_dir); + fs::create_dir_all(logs_path.clone()).unwrap(); + Self { data_dir: logs_path } + } +} +impl Logger for FilesystemLogger { + fn log(&self, record: &Record) { + let raw_log = record.args.to_string(); + let log = format!( + "{} {:<5} [{}:{}] {}\n", + OffsetDateTime::now_utc().format("%F %T"), + record.level.to_string(), + record.module_path, + record.line, + raw_log + ); + let logs_file_path = format!("{}/logs.txt", self.data_dir.clone()); + fs::OpenOptions::new() + .create(true) + .append(true) + .open(logs_file_path) + .unwrap() + .write_all(log.as_bytes()) + .unwrap(); + } +} +pub(crate) fn persist_channel_peer(path: &Path, peer_info: &str) -> std::io::Result<()> { + let mut file = fs::OpenOptions::new().create(true).append(true).open(path)?; + file.write_all(format!("{}\n", peer_info).as_bytes()) +} + +pub(crate) fn read_channel_peer_data( + path: &Path, +) -> Result, std::io::Error> { + let mut peer_data = HashMap::new(); + if !Path::new(&path).exists() { + return Ok(HashMap::new()); + } + let file = File::open(path)?; + let reader = BufReader::new(file); + for line in reader.lines() { + match cli::parse_peer_info(line.unwrap()) { + Ok((pubkey, socket_addr)) => { + peer_data.insert(pubkey, socket_addr); + } + Err(e) => return Err(e), + } + } + Ok(peer_data) +} diff --git a/src/hex_utils.rs b/src/hex_utils.rs new file mode 100644 index 0000000..70fe28b --- /dev/null +++ b/src/hex_utils.rs @@ -0,0 +1,42 @@ +use bitcoin::secp256k1::key::PublicKey; + +pub fn to_vec(hex: &str) -> Option> { + let mut out = Vec::with_capacity(hex.len() / 2); + + let mut b = 0; + for (idx, c) in hex.as_bytes().iter().enumerate() { + b <<= 4; + match *c { + b'A'..=b'F' => b |= c - b'A' + 10, + b'a'..=b'f' => b |= c - b'a' + 10, + b'0'..=b'9' => b |= c - b'0', + _ => return None, + } + if (idx & 1) == 1 { + out.push(b); + b = 0; + } + } + + Some(out) +} + +#[inline] +pub fn hex_str(value: &[u8]) -> String { + let mut res = String::with_capacity(64); + for v in value { + res += &format!("{:02x}", v); + } + res +} + +pub fn to_compressed_pubkey(hex: &str) -> Option { + let data = match to_vec(&hex[0..33 * 2]) { + Some(bytes) => bytes, + None => return None, + }; + match PublicKey::from_slice(&data) { + Ok(pk) => Some(pk), + Err(_) => None, + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..b024e93 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,577 @@ +pub mod bitcoind_client; +mod cli; +mod convert; +mod disk; +mod hex_utils; + +use crate::bitcoind_client::BitcoindClient; +use crate::disk::FilesystemLogger; +use bitcoin::blockdata::constants::genesis_block; +use bitcoin::blockdata::transaction::Transaction; +use bitcoin::consensus::encode; +use bitcoin::hashes::sha256::Hash as Sha256; +use bitcoin::hashes::Hash; +use bitcoin::network::constants::Network; +use bitcoin::secp256k1::Secp256k1; +use bitcoin::BlockHash; +use bitcoin_bech32::WitnessProgram; +use lightning::chain; +use lightning::chain::chaininterface::{BroadcasterInterface, ConfirmationTarget, FeeEstimator}; +use lightning::chain::chainmonitor; +use lightning::chain::keysinterface::{InMemorySigner, KeysInterface, KeysManager}; +use lightning::chain::Filter; +use lightning::chain::Watch; +use lightning::ln::channelmanager; +use lightning::ln::channelmanager::{ + BestBlock, ChainParameters, ChannelManagerReadArgs, SimpleArcChannelManager, +}; +use lightning::ln::peer_handler::{MessageHandler, SimpleArcPeerManager}; +use lightning::ln::{PaymentHash, PaymentPreimage, PaymentSecret}; +use lightning::routing::network_graph::NetGraphMsgHandler; +use lightning::util::config::UserConfig; +use lightning::util::events::{Event, EventsProvider}; +use lightning::util::ser::ReadableArgs; +use lightning_background_processor::BackgroundProcessor; +use lightning_block_sync::init; +use lightning_block_sync::poll; +use lightning_block_sync::SpvClient; +use lightning_block_sync::UnboundedCache; +use lightning_net_tokio::SocketDescriptor; +use lightning_persister::FilesystemPersister; +use rand::{thread_rng, Rng}; +use std::collections::hash_map::Entry; +use std::collections::HashMap; +use std::fmt; +use std::fs; +use std::fs::File; +use std::io; +use std::io::Write; +use std::ops::Deref; +use std::path::Path; +use std::sync::{Arc, Mutex}; +use std::time::{Duration, SystemTime}; +use tokio::sync::mpsc; +use tokio::sync::mpsc::Receiver; + +pub(crate) enum HTLCStatus { + Pending, + Succeeded, + Failed, +} + +pub(crate) struct MillisatAmount(Option); + +impl fmt::Display for MillisatAmount { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self.0 { + Some(amt) => write!(f, "{}", amt), + None => write!(f, "unknown"), + } + } +} + +pub(crate) struct PaymentInfo { + preimage: Option, + secret: Option, + status: HTLCStatus, + amt_msat: MillisatAmount, +} + +pub(crate) type PaymentInfoStorage = Arc>>; + +type ChainMonitor = chainmonitor::ChainMonitor< + InMemorySigner, + Arc, + Arc, + Arc, + Arc, + Arc, +>; + +pub(crate) type PeerManager = SimpleArcPeerManager< + SocketDescriptor, + ChainMonitor, + BitcoindClient, + BitcoindClient, + dyn chain::Access + Send + Sync, + FilesystemLogger, +>; + +pub(crate) type ChannelManager = + SimpleArcChannelManager; + +async fn handle_ldk_events( + channel_manager: Arc, chain_monitor: Arc, + bitcoind_client: Arc, keys_manager: Arc, + inbound_payments: PaymentInfoStorage, outbound_payments: PaymentInfoStorage, network: Network, + mut event_receiver: Receiver<()>, +) { + loop { + let received = event_receiver.recv(); + if received.await.is_none() { + println!("LDK Event channel closed!"); + return; + } + let loop_channel_manager = channel_manager.clone(); + let mut events = channel_manager.get_and_clear_pending_events(); + events.append(&mut chain_monitor.get_and_clear_pending_events()); + for event in events { + match event { + Event::FundingGenerationReady { + temporary_channel_id, + channel_value_satoshis, + output_script, + .. + } => { + // Construct the raw transaction with one output, that is paid the amount of the + // channel. + let addr = WitnessProgram::from_scriptpubkey( + &output_script[..], + match network { + Network::Bitcoin => bitcoin_bech32::constants::Network::Bitcoin, + Network::Testnet => bitcoin_bech32::constants::Network::Testnet, + Network::Regtest => bitcoin_bech32::constants::Network::Regtest, + Network::Signet => panic!("Signet unsupported"), + }, + ) + .expect("Lightning funding tx should always be to a SegWit output") + .to_address(); + let mut outputs = vec![HashMap::with_capacity(1)]; + outputs[0].insert(addr, channel_value_satoshis as f64 / 100_000_000.0); + let raw_tx = bitcoind_client.create_raw_transaction(outputs).await; + + // Have your wallet put the inputs into the transaction such that the output is + // satisfied. + let funded_tx = bitcoind_client.fund_raw_transaction(raw_tx).await; + let change_output_position = funded_tx.changepos; + assert!(change_output_position == 0 || change_output_position == 1); + + // Sign the final funding transaction and broadcast it. + let signed_tx = + bitcoind_client.sign_raw_transaction_with_wallet(funded_tx.hex).await; + assert_eq!(signed_tx.complete, true); + let final_tx: Transaction = + encode::deserialize(&hex_utils::to_vec(&signed_tx.hex).unwrap()).unwrap(); + // Give the funding transaction back to LDK for opening the channel. + loop_channel_manager + .funding_transaction_generated(&temporary_channel_id, final_tx) + .unwrap(); + } + Event::PaymentReceived { + payment_hash, + payment_preimage, + payment_secret, + amt, + .. + } => { + let mut payments = inbound_payments.lock().unwrap(); + let status = match loop_channel_manager.claim_funds(payment_preimage.unwrap()) { + true => { + println!( + "\nEVENT: received payment from payment hash {} of {} millisatoshis", + hex_utils::hex_str(&payment_hash.0), + amt + ); + print!("> "); + io::stdout().flush().unwrap(); + HTLCStatus::Succeeded + } + _ => HTLCStatus::Failed, + }; + match payments.entry(payment_hash) { + Entry::Occupied(mut e) => { + let payment = e.get_mut(); + payment.status = status; + payment.preimage = Some(payment_preimage.unwrap()); + payment.secret = Some(payment_secret); + } + Entry::Vacant(e) => { + e.insert(PaymentInfo { + preimage: Some(payment_preimage.unwrap()), + secret: Some(payment_secret), + status, + amt_msat: MillisatAmount(Some(amt)), + }); + } + } + } + Event::PaymentSent { payment_preimage } => { + let hashed = PaymentHash(Sha256::hash(&payment_preimage.0).into_inner()); + let mut payments = outbound_payments.lock().unwrap(); + for (payment_hash, payment) in payments.iter_mut() { + if *payment_hash == hashed { + payment.preimage = Some(payment_preimage); + payment.status = HTLCStatus::Succeeded; + println!( + "\nEVENT: successfully sent payment of {} millisatoshis from \ + payment hash {:?} with preimage {:?}", + payment.amt_msat, + hex_utils::hex_str(&payment_hash.0), + hex_utils::hex_str(&payment_preimage.0) + ); + print!("> "); + io::stdout().flush().unwrap(); + } + } + } + Event::PaymentFailed { payment_hash, rejected_by_dest } => { + print!( + "\nEVENT: Failed to send payment to payment hash {:?}: ", + hex_utils::hex_str(&payment_hash.0) + ); + if rejected_by_dest { + println!("rejected by destination node"); + } else { + println!("route failed"); + } + print!("> "); + io::stdout().flush().unwrap(); + + let mut payments = outbound_payments.lock().unwrap(); + if payments.contains_key(&payment_hash) { + let payment = payments.get_mut(&payment_hash).unwrap(); + payment.status = HTLCStatus::Failed; + } + } + Event::PendingHTLCsForwardable { time_forwardable } => { + let forwarding_channel_manager = loop_channel_manager.clone(); + tokio::spawn(async move { + let min = time_forwardable.as_millis() as u64; + let millis_to_sleep = thread_rng().gen_range(min, min * 5) as u64; + tokio::time::sleep(Duration::from_millis(millis_to_sleep)).await; + forwarding_channel_manager.process_pending_htlc_forwards(); + }); + } + Event::SpendableOutputs { outputs } => { + let destination_address = bitcoind_client.get_new_address().await; + let output_descriptors = &outputs.iter().map(|a| a).collect::>(); + let tx_feerate = + bitcoind_client.get_est_sat_per_1000_weight(ConfirmationTarget::Normal); + let spending_tx = keys_manager + .spend_spendable_outputs( + output_descriptors, + Vec::new(), + destination_address.script_pubkey(), + tx_feerate, + &Secp256k1::new(), + ) + .unwrap(); + bitcoind_client.broadcast_transaction(&spending_tx); + } + } + } + tokio::time::sleep(Duration::from_secs(1)).await; + } +} + +async fn start_ldk() { + let args = match cli::parse_startup_args() { + Ok(user_args) => user_args, + Err(()) => return, + }; + + // Initialize the LDK data directory if necessary. + let ldk_data_dir = format!("{}/.ldk", args.ldk_storage_dir_path); + fs::create_dir_all(ldk_data_dir.clone()).unwrap(); + + // Initialize our bitcoind client. + let bitcoind_client = match BitcoindClient::new( + args.bitcoind_rpc_host.clone(), + args.bitcoind_rpc_port, + args.bitcoind_rpc_username.clone(), + args.bitcoind_rpc_password.clone(), + ) + .await + { + Ok(client) => Arc::new(client), + Err(e) => { + println!("Failed to connect to bitcoind client: {}", e); + return; + } + }; + + // ## Setup + // Step 1: Initialize the FeeEstimator + + // BitcoindClient implements the FeeEstimator trait, so it'll act as our fee estimator. + let fee_estimator = bitcoind_client.clone(); + + // Step 2: Initialize the Logger + let logger = Arc::new(FilesystemLogger::new(ldk_data_dir.clone())); + + // Step 3: Initialize the BroadcasterInterface + + // BitcoindClient implements the BroadcasterInterface trait, so it'll act as our transaction + // broadcaster. + let broadcaster = bitcoind_client.clone(); + + // Step 4: Initialize Persist + let persister = Arc::new(FilesystemPersister::new(ldk_data_dir.clone())); + + // Step 5: Initialize the ChainMonitor + let chain_monitor: Arc = Arc::new(chainmonitor::ChainMonitor::new( + None, + broadcaster.clone(), + logger.clone(), + fee_estimator.clone(), + persister.clone(), + )); + + // Step 6: Initialize the KeysManager + + // The key seed that we use to derive the node privkey (that corresponds to the node pubkey) and + // other secret key material. + let keys_seed_path = format!("{}/keys_seed", ldk_data_dir.clone()); + let keys_seed = if let Ok(seed) = fs::read(keys_seed_path.clone()) { + assert_eq!(seed.len(), 32); + let mut key = [0; 32]; + key.copy_from_slice(&seed); + key + } else { + let mut key = [0; 32]; + thread_rng().fill_bytes(&mut key); + match File::create(keys_seed_path.clone()) { + Ok(mut f) => { + f.write_all(&key).expect("Failed to write node keys seed to disk"); + f.sync_all().expect("Failed to sync node keys seed to disk"); + } + Err(e) => { + println!("ERROR: Unable to create keys seed file {}: {}", keys_seed_path, e); + return; + } + } + key + }; + let cur = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap(); + let keys_manager = Arc::new(KeysManager::new(&keys_seed, cur.as_secs(), cur.subsec_nanos())); + + // Step 7: Read ChannelMonitor state from disk + let mut channelmonitors = persister.read_channelmonitors(keys_manager.clone()).unwrap(); + + // Step 8: Initialize the ChannelManager + let user_config = UserConfig::default(); + let mut restarting_node = true; + let (channel_manager_blockhash, mut channel_manager) = { + if let Ok(mut f) = fs::File::open(format!("{}/manager", ldk_data_dir.clone())) { + let mut channel_monitor_mut_references = Vec::new(); + for (_, channel_monitor) in channelmonitors.iter_mut() { + channel_monitor_mut_references.push(channel_monitor); + } + let read_args = ChannelManagerReadArgs::new( + keys_manager.clone(), + fee_estimator.clone(), + chain_monitor.clone(), + broadcaster.clone(), + logger.clone(), + user_config, + channel_monitor_mut_references, + ); + <(BlockHash, ChannelManager)>::read(&mut f, read_args).unwrap() + } else { + // We're starting a fresh node. + restarting_node = false; + let getinfo_resp = bitcoind_client.get_blockchain_info().await; + + let chain_params = ChainParameters { + network: args.network, + best_block: BestBlock::new( + getinfo_resp.latest_blockhash, + getinfo_resp.latest_height as u32, + ), + }; + let fresh_channel_manager = channelmanager::ChannelManager::new( + fee_estimator.clone(), + chain_monitor.clone(), + broadcaster.clone(), + logger.clone(), + keys_manager.clone(), + user_config, + chain_params, + ); + (getinfo_resp.latest_blockhash, fresh_channel_manager) + } + }; + + // Step 9: Sync ChannelMonitors and ChannelManager to chain tip + let mut chain_listener_channel_monitors = Vec::new(); + let mut cache = UnboundedCache::new(); + let mut chain_tip: Option = None; + if restarting_node { + let mut chain_listeners = + vec![(channel_manager_blockhash, &mut channel_manager as &mut dyn chain::Listen)]; + + for (blockhash, channel_monitor) in channelmonitors.drain(..) { + let outpoint = channel_monitor.get_funding_txo().0; + chain_listener_channel_monitors.push(( + blockhash, + (channel_monitor, broadcaster.clone(), fee_estimator.clone(), logger.clone()), + outpoint, + )); + } + + for monitor_listener_info in chain_listener_channel_monitors.iter_mut() { + chain_listeners.push(( + monitor_listener_info.0, + &mut monitor_listener_info.1 as &mut dyn chain::Listen, + )); + } + chain_tip = Some( + init::synchronize_listeners( + &mut bitcoind_client.deref(), + args.network, + &mut cache, + chain_listeners, + ) + .await + .unwrap(), + ); + } + + // Step 10: Give ChannelMonitors to ChainMonitor + for item in chain_listener_channel_monitors.drain(..) { + let channel_monitor = item.1 .0; + let funding_outpoint = item.2; + chain_monitor.watch_channel(funding_outpoint, channel_monitor).unwrap(); + } + + // Step 11: Optional: Initialize the NetGraphMsgHandler + // XXX persist routing data + let genesis = genesis_block(args.network).header.block_hash(); + let router = Arc::new(NetGraphMsgHandler::new( + genesis, + None::>, + logger.clone(), + )); + + // Step 12: Initialize the PeerManager + let channel_manager: Arc = Arc::new(channel_manager); + let mut ephemeral_bytes = [0; 32]; + rand::thread_rng().fill_bytes(&mut ephemeral_bytes); + let lightning_msg_handler = + MessageHandler { chan_handler: channel_manager.clone(), route_handler: router.clone() }; + let peer_manager: Arc = Arc::new(PeerManager::new( + lightning_msg_handler, + keys_manager.get_node_secret(), + &ephemeral_bytes, + logger.clone(), + )); + + // ## Running LDK + // Step 13: Initialize networking + + // We poll for events in handle_ldk_events(..) rather than waiting for them over the + // mpsc::channel, so we can leave the event receiver as unused. + let (event_ntfn_sender, event_ntfn_receiver) = mpsc::channel(2); + let peer_manager_connection_handler = peer_manager.clone(); + let event_notifier = event_ntfn_sender.clone(); + let listening_port = args.ldk_peer_listening_port; + tokio::spawn(async move { + let listener = std::net::TcpListener::bind(format!("0.0.0.0:{}", listening_port)).unwrap(); + loop { + let peer_mgr = peer_manager_connection_handler.clone(); + let notifier = event_notifier.clone(); + let tcp_stream = listener.accept().unwrap().0; + tokio::spawn(async move { + lightning_net_tokio::setup_inbound(peer_mgr.clone(), notifier.clone(), tcp_stream) + .await; + }); + } + }); + + // Step 14: Connect and Disconnect Blocks + if chain_tip.is_none() { + chain_tip = + Some(init::validate_best_block_header(&mut bitcoind_client.deref()).await.unwrap()); + } + let channel_manager_listener = channel_manager.clone(); + let chain_monitor_listener = chain_monitor.clone(); + let bitcoind_block_source = bitcoind_client.clone(); + let network = args.network; + tokio::spawn(async move { + let mut derefed = bitcoind_block_source.deref(); + let chain_poller = poll::ChainPoller::new(&mut derefed, network); + let chain_listener = (chain_monitor_listener, channel_manager_listener); + let mut spv_client = + SpvClient::new(chain_tip.unwrap(), chain_poller, &mut cache, &chain_listener); + loop { + spv_client.poll_best_tip().await.unwrap(); + tokio::time::sleep(Duration::from_secs(1)).await; + } + }); + + // Step 15: Initialize LDK Event Handling + let channel_manager_event_listener = channel_manager.clone(); + let chain_monitor_event_listener = chain_monitor.clone(); + let keys_manager_listener = keys_manager.clone(); + // TODO: persist payment info to disk + let inbound_payments: PaymentInfoStorage = Arc::new(Mutex::new(HashMap::new())); + let outbound_payments: PaymentInfoStorage = Arc::new(Mutex::new(HashMap::new())); + let inbound_pmts_for_events = inbound_payments.clone(); + let outbound_pmts_for_events = outbound_payments.clone(); + let network = args.network; + let bitcoind_rpc = bitcoind_client.clone(); + tokio::spawn(async move { + handle_ldk_events( + channel_manager_event_listener, + chain_monitor_event_listener, + bitcoind_rpc, + keys_manager_listener, + inbound_pmts_for_events, + outbound_pmts_for_events, + network, + event_ntfn_receiver, + ) + .await; + }); + + // Step 16 & 17: Persist ChannelManager & Background Processing + let data_dir = ldk_data_dir.clone(); + let persist_channel_manager_callback = + move |node: &ChannelManager| FilesystemPersister::persist_manager(data_dir.clone(), &*node); + BackgroundProcessor::start( + persist_channel_manager_callback, + channel_manager.clone(), + peer_manager.clone(), + logger.clone(), + ); + + // Reconnect to channel peers if possible. + let peer_data_path = format!("{}/channel_peer_data", ldk_data_dir.clone()); + match disk::read_channel_peer_data(Path::new(&peer_data_path)) { + Ok(mut info) => { + for (pubkey, peer_addr) in info.drain() { + for chan_info in channel_manager.list_channels() { + if pubkey == chan_info.remote_network_id { + let _ = cli::connect_peer_if_necessary( + pubkey, + peer_addr, + peer_manager.clone(), + event_ntfn_sender.clone(), + ); + } + } + } + } + Err(e) => println!("ERROR: errored reading channel peer info from disk: {:?}", e), + } + + // Start the CLI. + cli::poll_for_user_input( + peer_manager.clone(), + channel_manager.clone(), + keys_manager.clone(), + router.clone(), + inbound_payments, + outbound_payments, + event_ntfn_sender, + ldk_data_dir.clone(), + logger.clone(), + args.network, + ) + .await; +} + +#[tokio::main] +pub async fn main() { + start_ldk().await; +}