Compare commits

..

583 Commits

Author SHA1 Message Date
pm47
00894bae64 moved akka conf to eclair-node application.conf 2017-11-24 16:15:12 +01:00
Fabrice Drouin
4a4640bc86 Bitcoin rpc client: queue requests locally (#223)
use a local queue for outgoing rpc requests. this should be a better solution than
inceasing the number of concurrent requests.
see https://doc.akka.io/docs/akka-http/current/scala/http/client-side/host-level.html#using-the-host-level-api-with-a-queue
for more information.
2017-11-22 14:48:23 +01:00
Pierre-Marie Padiou
bfa3e1c2ca Reformat + optimized imports (#222)
* Reformat + optimized imports

* Fixed unwanted modifications
2017-11-21 20:08:15 +01:00
Pierre-Marie Padiou
df67157119 fix doc for api call connect/open (#220)
Fixes #215.
2017-11-21 19:11:07 +01:00
Pierre-Marie Padiou
875dc04d39
Support for electrumx API (#208)
This is a rework of #184 with numerous improvements and bugfixes.

* re-enabled `WatchSpentBasic`

* fixed several issues in watcher

* fixed pattern matching for INPUT_RECONNECTED event in CLOSING

* reduced logback_colors log level

* connect txes even if they arrive out of order

* wallet: send confidence event as soon as a tx is confirmed

* fixed 5985148f2fc727dadbe9ffece596ed61331435b6 and improve events

* added `NewWalletReceiveAddress` event

* cleaned up electrum testnet seeds

* added a test on dumping routing state

* removed WAIT_FOR_FUNDING_PUBLISHED state and clarified funding tx publish assumptions

* wallet: use BIP49 derivation and 24 words mnemonic codes
we use segwit with p2sh-of-p2wkh so we should use BIP49 derivation
instead of BIP44 (same path with m/49'/... instead of m/44'/...)

* added a rollback function to `EclairWallet`

This rollback is called whenever we know we won't publish the funding tx,
so that we tell the wallet to release locks on utxos.

* fundee now checks feerates at `open_channel` reception

* proper handling of electrum connection/disconnection

* moved bitcoinj test to its own package

* make electrum wallet advertise address at startup

* set version to 0.2-SNAPSHOT
2017-11-21 18:12:45 +01:00
Pierre-Marie Padiou
68cbcf74e3
Prune stale network announcements (#219)
See https://github.com/lightningnetwork/lightning-rfc/blob/master/07-routing-gossip.md#recommendation-on-pruning-stale-entries.

* send a new `channel_update` every 24h as keepalive

* use case object instead of symbol for ticks

* minor improvements in router init

* prune stale channels

Note that we don't want to prune brand new channels for which we didn't
yet receive a channel update, so we consider stale a channel that:
(1) is older than 2 weeks (2*7*144 = 2016 blocks)
AND
(2) didn't have an update during the last 2 weeks.

Pruning is triggered every day.

Also renamed event `BITCOIN_FUNDING_OTHER_CHANNEL_SPENT` to
`BITCOIN_FUNDING_EXTERNAL_CHANNEL_SPENT`.

* filter out duplicate announcements before checking sig

* changed routing table dump parameters
2017-11-21 16:47:02 +01:00
Pierre-Marie Padiou
1ce7b8791c
Improved fees management (#216)
* main feerate source is now earn.com (21.co) instead of bitpay insight
* if main feerate source is unavailable, we now fallback to default values
* we retrieve feerates for a set of block delays instead of just one
* we now use different block delays depending on transactions:
  - `block_delay`=`1` for txes that compete with others (eg: commitment
    tx, htlc tx, penalty tx)
  - `block_delay`=`6` for other txes (eg: funding tx, closing tx, delayed
    output tx)
2017-11-21 15:59:01 +01:00
Pierre-Marie Padiou
340e00fb6b
Use a separate htlc_key to sign 2nd stage htlc txs (#213)
We previously used the `payment_key` both for our main output, and to sign
the local `htlc_success`/`htlc_timeout` transactions.

With this change we can keep the `payment_privkey` offline, which is far
better from a security point of view because an attacker getting control
of a node wouldn't be able to just publish the current commitment
transaction and get the funds. The attacker would only be able to get our
`htlc_privkey`, which we only use in a 2-of-2 multisig with our
counterparty, so it is useless except if the attacker and the counterparty
are the same person, and even in that case only the pending htlcs would be
at risk.

Note that this implementation is a first step and actually keeps the
payment key to spend our outputs in non-mutual close scenarios.
2017-11-17 15:44:44 +01:00
Pierre-Marie Padiou
fcb5bf2549
Delay announcement_signatures when received early (#217)
* delay `announcement_signatures` in state `WAIT_FOR_FUNDING_LOCKED`
* delay `announcement_signatures` in state `WAIT_FOR_FUNDING_CONFIRMED`
* always re-send our `announcement_signatures` in response to theirs
2017-11-17 14:52:00 +01:00
Pierre-Marie Padiou
dd642c961d
Handle remote error in SYNCING state (#205)
This closes #203.
2017-11-14 18:44:25 +01:00
Pierre-Marie Padiou
eff7a8b986
Better handle big routing table (#194)
* increased tcp send buffer x100

* throttle announcement messages when dumping the table

* set router throttling to chunkSize=10 delay=50ms
2017-11-14 18:44:10 +01:00
Pierre-Marie Padiou
ac64cc285a
Reworked channel closing logic (#204)
When doing an unilateral close (local or remote), we previously weren't
watching htlc outputs to decide whether the commit was finished or not.
This was incorrect because we didn't make sure the htlc-related
transactions had indeed been confirmed on the blockchain, making us
potentially lose money.

This is not trivial, because htlc transactions may be double-spent by the
counterparty, dependending on scenarios (ex: `htlc-timeout` vs
`claim-success`). On top of that, there may be several different kind of
commits in competition at the same time.

With this change, we now:
- put `WatchConfirm` watches on the commitment tx, and on all outputs only
  us control (eg: our main output) ;
- put `WatchSpent` watches on the outputs that may be double spent by the
  counterparty; when such an output is spent, we put a `WatchConfirm` on
  the corresponding transaction and keep track of all outpoints spent ;
- every time a new transaction is confirmed, we find out if there are some
  remaining transactions waiting for confirmation, taking into account the
  fact that some 2nd/3rd-stage txs may never confirm because their input
  has been doublespent.

We also don't rely anymore on dedicated `BITCOIN_CLOSE_DONE`,
`BITCOIN_LOCALCOMMIT_DONE`, ... events.
2017-11-14 18:43:33 +01:00
Pierre-Marie Padiou
f71f3da027
Rework preimage handling (#183)
* properly handle new htlc requests when closing

When in NORMAL state and a `shutdown` message has already been
sent or received, then any subsequent `CMD_ADD_HTLC` fails and
the relayer is notified of the failure.

Same in SHUTDOWN state.

This fixes a possible race condition when a channel just switched
to SHUTDOWN, and the relayer keeps sending it new htlcs before
being notified of the state change.

* renamed Htlc->DirectedHtlc + cleanup

* storing origin of htlcs in the channel state

Currently this information is handled in the relayer, which is not
persisted. As a consequence, if eclair is shut down and there are
pending (signed) incoming htlcs, those will always expire (time out
and fail) even if the corresponding outgoing htlc is fulfilled, because
we lose the lookup table (the relayer's `bindings` map).

Storing the origin in the channel (as opposed to persisting the state
of the relayer) makes sense because we want to store the origin if and
only if an outgoing htlc was successfully sent and signed in a channel.

It is also probably more performant because we only need to do one disk
operation (which we have to do at signing anyway) instead of two
distinct operations.

* removed bindings from relayer

Instead, we rely on the origin stored in the actor state.

* preimages are now persisted and acknowledged

Upon reception of an `UpdateFulfillHtlc`, the relayer forwards it
immediately to the origin channel, *and* it stores the preimage in
a `PreimagesDb`.

When the origin channel has irrevocably committed the fulfill in a
`CommitSig`, it sends an `AckFulfillCmd` back to the relayer, which
will then remove the preimage from its database.

In addition to that, the relayer will re-send all pending fulfills
when it is notified that a channel reaches NORMAL, SHUTDOWN, or
CLOSING state. That way we make sure that the origin channel will
always get the fulfill eventually, even if it currently OFFLINE for
example. This fixes #146.

Also, the relayer now relies on the register to forward messages to
channels based on `channelId` or `shortChannelId`. This simplifies
the relayer but adds one hop when forwarding messages.

* modified `PaymentRelayed` event

Replaced `amountIn` and `feesEarned` by more explicit `amountIn`
and `amountOut`. `feesEarned` are simply the difference.

TODO:
- when local/remote closing a channel, we currently do not wait
for htlc-related transactions, we consider the channel CLOSED when
the commitment transactions has been buried deeply enough; this is
wrong because it wouldn't let us time to extract payment preimages
in certain cases
2017-11-14 17:21:02 +01:00
Dominique
a68a06fd38 Readme: added help for options syntax (#212)
* (README) updated link to release readme
* (README) added a link to HOCON readme for options syntax

This closes #209
2017-11-14 10:56:10 +01:00
Fabrice Drouin
340a16fa78 Lock utxos when creating a funding transaction with Bitcoin Core (#211)
This mitigates issues when opening channels in parallel and having unpublished transactions reuse utxos that are already used by other txs.

Note that utxo will stay locked if counterparty disconnects after the `funding_created` message is sent, and before a `funding_signed` is received. In that case we should release locked utxos by calling `lockunspent` RPC function.
2017-11-10 21:55:13 +01:00
Fabrice Drouin
02683dfb43 Use min_final_cltv_expiry included in payment request (if any) (#210) 2017-11-10 19:45:41 +01:00
Anton Kumaigorodski
d0e33f23e9 Support building payments with extra hops (#198)
* Support building of outgoing payments with optional extra hops from payment requests

* Add test for route calculation with extra hops

* Simplify pattern matching in `buildExtra`

* `buildPayment` now uses a reverse Seq[Hop] to create a Seq[ExtraHop]

Since `Router` currently stores `ChannelUpdate`'s for non-public channels, it is possible to use it not only to get a route from payer to payee but also to get a "reverse" assisted route from payee when creating a `PaymentRequest`.

In principle this could be used to even generate a full reverse path to a payer which does not have an access to routing table for some reason.

* Can create `PaymentRequest`s with `RoutingInfoTag`s

* Bugfix and update test with data from live payment testing

* Move ExtraHop to PaymentRequest.scala
2017-11-07 12:08:05 +01:00
Fabrice Drouin
e17335931b Add an optional 'minimum htlc expiry' tag (#202) 2017-11-06 19:56:07 +01:00
Pierre-Marie Padiou
3be40a1fab
Always store channel state when a rev is received (#201)
The way the store data currently doesn't allow for easy testing of this.
It will be improved in a later iteration.

This fixes #200.
2017-10-30 14:54:34 +01:00
Pierre-Marie Padiou
1ba311379b
Add primary key to channel_updates table (#199)
This table was missing a primary key, which caused it to grow
indefinitely.

Also added duplication tests to other tables.
2017-10-30 14:53:47 +01:00
Pierre-Marie Padiou
a605790be5
Have channels subscribe to blockchain events at creation (#195)
Instead of only subscribing to those events when we reach certain states,
we now always subscribe to them at startup. It doesn't cost a lot because
we receive an event only when a new block appears, and is much simpler.

Note that we previously didn't even bother unsubscribing when we weren't
interested anymore in the events, and were ignoring the messages in the
`whenUnhandled` block. So it is more consistent to have the same behavior
during the whole lifetime of the channel.

This fixes #187.
2017-10-30 14:53:16 +01:00
Fabrice Drouin
b8a5884847 payment request: encode expiry as a varlen unsigned long (#188)
* payment request expiry encoding: add Anton's test
it shows that we don't encode/decode values which would take up more than 2 5-bits value

* payment request: encode expiry as a varlen unsigned value
fixes #186
2017-10-25 10:20:49 +02:00
Anton Kumaigorodski
5becef6fc6 Support multiple hops in RoutingInfoTag (#185)
* Support multiple hops in RoutingInfoTag

* Change `HiddenHop` to `ExtraHop`, `channelId: BinaryData` to `shortChannelId: Long`
2017-10-23 15:11:49 +02:00
Pierre-Marie Padiou
f13e07850b Store state when a sig is received in SHUTDOWN (#181)
This fixes #173 (for good this time)
2017-10-17 13:09:58 +02:00
Pierre-Marie Padiou
4969845401 Base checks in sendAdd on the *last* sent sig (#180)
fixes #175
2017-10-17 13:09:15 +02:00
Pierre-Marie Padiou
41d1fc26a9 Reworked handling of shutdown messages (#176)
Current version attempted to do everything at once, and would always
leave the NORMAL state after processing the `shutdown` message. In
addition to being overly complicated, it had bugs because it is just
not always possible to do so: for example when we have unsigned outgoing
`update_add_htlc` and are already in the process of signing older changes,
the only thing we can do is wait for the counterparty's `revoke_and_ack`
and sign again right away when we receive it. Only after that can we
send our `shutdown` message, and we can't accept new `update_add_htlc`
in the meantime.

Another issue with the current implementation was that we considered
unsigned outgoing *changes*, when only unsigned outgoing `update_add_htlc`
are relevant in the context of `shutdown` logic.

We now also fail to route htlcs in SHUTDOWN state, as recommended by BOLT 2.

This fixes #173.
2017-10-11 17:53:23 +02:00
Pierre-Marie Padiou
9356ad8d0d Added check amount>dust on ClaimDelayedOutputTx (#177)
The current trimming logic [1] as defined in the spec only takes
care of 2nd level txes, making sure that their outputs are greater
than the configured dust limit. However, it is very possible that
depending on the current `feeRatePerKw`, 3rd level transactions
(such as those claiming delayed outputs) are below the dust limit.

Current implementation wasn't taking care of that, and was happily
generating transactions with negative amounts, as reported in #164.

This fixes #164 by rejecting transactions with an amount below the
dust limit, similarly to what happens when the parent output is
trimmed for 2nd level txes.

[1] https://github.com/lightningnetwork/lightning-rfc/blob/master/03-transactions.md#trimmed-outputs
2017-10-11 16:27:25 +02:00
Pierre-Marie Padiou
6a15b8832d Added ACK-based TCP write back-pressure (#174)
Current implementation was simplistic, which resulted in writes
being rejected when OS buffer was full. This happened especially
right after connection, when dumping a large routing table.

It is not clear whether we need read throttling too.
2017-10-11 16:26:18 +02:00
Pierre-Marie Padiou
0d180032a4 Added an integration test on revoked tx handling (#172)
The scenario was already tested at a lower level, but this is
more realistic, with a real bitcoin core.

Note that we currently only steal the counterparty's *main output*,
we ignore pending htlcs. From an incentive point-of-view, it is an
acceptable tradeoff because the amount of in-flight htlcs should
typically be far less than the main outputs (and can be configured
with `max-htlc-value-in-flight-msat`).
2017-10-03 18:43:36 +02:00
Pierre-Marie Padiou
2fc1d7096f Handle update_fail_malformed_htlc in payment FSM (#170)
* handling `update_fail_malformed` messages in payment fsm

* added check on failure code for malformed htlc errors

Spec says that the `update_fail_malformed_htlc`.`failure_code`
must have the BADONION bit set.

* removed hard-coded actor names in fuzzy tests
2017-09-25 16:13:20 +02:00
Pierre-Marie Padiou
a79f60fdbe Re-send htlc/sigs after funding_locked (#169)
We previously skipped the `handleSync` function when we had to re-send
`funding_locked` messages on reconnection. This didn't take into account
the fact that we might have been disconnected right after sending the
very first `commit_sig` in the channel. In that case we need to first
re-send `funding_locked`, then re-send whatever updates were included in
the lost signature, and finally re-send the same `commit_sig`.

Note that the specification doesn't require to re-send the exact same
updates and signatures on reconnection. But doing this allows for a single
commitment history and allows us not to keep track of all signatures
sent to the other party.

Closes #165
2017-09-22 16:01:18 +02:00
Fabrice Drouin
8c71b80e0c Process update_fail_malformed_htlc properly (#168)
* add a test that fails and shows that we don't process `update_fail_malformed` properly
* remove HTLCs failed with update_fail_malformed
* fixes #167
2017-09-22 11:50:51 +02:00
Fabrice Drouin
1f336772b2 back to 0.2-SNAPHOT (#166)
use scala plugin 3.3.1 (mvn scala:console now works)
add Dominique to the list of developpers
2017-09-20 15:16:49 +02:00
pm47
a97fa39fef set version to 0.2-alpha5 2017-09-14 18:41:01 +02:00
pm47
1ae7885eea now using bitcoinj 0.15-rc4 2017-09-14 18:28:03 +02:00
Pierre-Marie Padiou
9e3fbbe5da Minor fixes (#163)
* improved router/payment-lifecycle logging
* now periodically publish CurrentFeerate events
* added a reference to `channelId` in `ChannelException`
2017-09-13 18:35:22 +02:00
Pierre-Marie Padiou
a18fac135d Set watchermode=true for bitcoinj watcher (#162) 2017-09-12 15:07:30 +02:00
Joe Miyamoto
fbabb7d6a2 write profiles for macos (#157) 2017-09-12 14:51:43 +02:00
Fabrice Drouin
4b7cae47ff Sphinx: fix version byte (#161)
we were using version=1, it should be 0 instead (see BOLT 04)
2017-09-12 14:49:12 +02:00
Fabrice Drouin
a8b97d9c05 SPV watcher: set a timestamp on watched script (#160)
this way we don't disable checkpoint optimizations.
2017-09-11 18:40:57 +02:00
Pierre-Marie Padiou
7b2303af80 Now requiring spv nodes to be Segwit-compatible (#159)
* now requiring spv nodes to be 0.13+
* properly setting bitcoinj Context
* disconnect peers which do not provide witness data
* waiting for bitcoinj to be initialized before going further in the setup
2017-09-08 18:06:59 +02:00
Pierre-Marie Padiou
e478f77e5a Replaced sqlite result iterators by lists (#158)
This is less performant but our ResultSet->Iterator implementation
was buggy due to java/scala iterators requiring look-ahead capabilities
when iterating over the result, which ResultSet does not support.

This is a quick fix in the meantime.
2017-09-08 14:43:43 +02:00
Pierre-Marie Padiou
ebe93538ca Better handling of connection lifecycle in Peer (#155)
* better handling of connection lifecycle in Peer
* replaced `StateTimeout` by scheduled message for channel termination
2017-09-08 14:32:28 +02:00
Pierre-Marie Padiou
93739eb3f1 Temporarily exclude channels from routes (#154)
When a node return a `TemporaryChannelFailure` for its outgoing
channel, along with an unchanged `ChannelUpdate`, we temporarily
exclude this channel from route calculation.

Note that the exclusion is directed, as we expect this will mostly
happen when all funds are on one side of the channel.

The duration of the exclusion can be configured by setting the
key `eclair.channel-exclude-duration`. Default value is 60s.
2017-09-08 13:52:17 +02:00
dpad85
c24b9f0c22 (gui) Remove the millisatoshis part from milli-bitcoin amounts (#153)
* (gui) Added localized decimal patterns for amounts
* balance and capacity in channel pane are formatted with the milliBTC pattern
* (gui) using vm default locale is fine with DecimalFormat
2017-09-07 17:27:48 +02:00
Pierre-Marie Padiou
c94cb13dd3 Add an experimental SPV mode with bitcoinj (#152) 2017-09-07 17:20:36 +02:00
Fabrice Drouin
1d29d28a2a Fix byte order of chain hash (#151)
* fix order of chain hash (see https://github.com/lightningnetwork/lightning-rfc/issues/237)
* use known chain hashes instead of querying bitcoind
2017-09-06 17:29:39 +02:00
Fabrice Drouin
a066e0d042 Switch to java environment variables instead of command line options (#149)
* added an eclair.printToconsole is defined log to stdout
* datadir is now eclair.datadir if defined, or user.home/.eclair
* update README.md
2017-08-30 13:42:58 +02:00
Pierre-Marie Padiou
a1509673a6 Use Sqlite as local database (#143)
* network announcements are now stored in sqlite
* store channels and peers in sqlite
* added tests to peers and channels db
2017-08-29 15:35:28 +02:00
dpad85
45a3993e7a Added a compatibility check with DB files when booting application (#148) 2017-08-28 16:37:44 +02:00
Fabrice Drouin
9a736e2396 Add chain hash to gossip messages, increase max error message size (#145)
see rfc PR 203 and 227
2017-08-28 14:42:44 +02:00
Pierre-Marie Padiou
edab8b004a Use logback's MDC to display channelId in logs (#142) 2017-08-25 16:30:00 +02:00
Anton Kumaigorodski
4cd081c11c Add API method to accept requests with custom amount (closes #134) (#135)
* Add API method to accept requests with custom amount
- can be used to send up to 2x higher amount than requested according to BOLT11
- should be used for payment requests without amounts
* Refactor 'send' method in API
* Add comments and description for 'send' API method
2017-08-25 13:38:14 +02:00
Michael
76f744c57c Replace getinfo with getnetworkinfo (#131) 2017-08-24 11:15:13 +02:00
dpad85
305420bd37 Renamed javafx module to eclair-node-gui and updated binaries name (#137)
* Renamed eclair-node-javafx module to eclair-node-gui

* (build) javafx installer uses project version

* (build) Streamlined capsule names

* (build) mvn generates installer only with `installer` profile. The windows installer does not need to be created in common cases. Decreases `eclair-node-gui` module building time by ~ 1 min

* (readme) updated the `run eclair` commands with the new capsule names
2017-08-23 19:28:18 +02:00
Fabrice Drouin
012d804474 Fix node announcement encoding (closes #124) (#136) 2017-08-23 16:58:05 +02:00
Pierre-Marie Padiou
00aef5c438 Updated bitcoin-lib to 0.9.13 (closes #110) (#132) 2017-08-23 14:16:06 +02:00
dpad85
7acb75d50c (API) Added a method to return all known channels (closes #126) (#133)
* Added an 'allchannels' call that returns local and non-local channels
* Added API calls `allchannels` and `allnodes` to documentation
2017-08-23 14:14:33 +02:00
Pierre-Marie Padiou
43d6c80f9e Bugfix: regression in init (#120)
Eclair wasn't stopping anymore when two instances were started with the
same ports.

Note: we should probably go one step further and put a lock in the datadir
directory. For now we just check if the main TCP port is in use and fail fast.
2017-08-23 12:13:18 +02:00
Fabrice Drouin
f2560e2c9c Reorder features fields to match the BOLTS (#127)
see spec change a257554456cda98afd1532c302c0e5e84de0455e
2017-08-23 11:29:46 +02:00
dpad85
3e81fce898 (gui) Fix direction icons in the list of the known channels in network (#119)
* (gui) channel direction is built from current item state

* using an objectproperty binding with the cell value instead of a string so
  that we can directly access the state within the factory

* (gui) Using option for direction boolean instead of dedicated state

* Tell explicitly in readme that `eclair.conf` must be manually created (#129)

* also added an explicit link to the binaries
* fix #125
2017-08-17 13:04:43 +02:00
Pierre-Marie Padiou
d0a18c0649 improved reconnection logic and fuzzy tests (#123)
Most notably, we do not anymore discard previously signed updates.
Instead, we re-send them and re-send the exact same signature. For that to
work, we had to be careful to re-send rev/sig in the same order, because
that impacts whatever is signed.

NB: this breaks storage serialization backward compatibility
2017-08-10 16:24:05 +02:00
Pierre-Marie Padiou
0440096f95 Clarified node/node-javafx distinction in README 2017-07-26 19:11:44 +02:00
Pierre-Marie Padiou
c6dc33e8bf Bugfixes and minor improvements (#117)
* reworked payment lifecycle

* fixed retry logic (infinite loop in some cases)
* check update signature
* keep track of the list of errors and routes tried

* added support for sending bolt11 payment request in the API

* updated eclair-cli and deleted deprecated TESTING.md (closes #112)

* removed useless application.conf in eclair-node

* now handling CMD_CLOSE in shutdown/negotiating/closing states

* added no-op handlers for FundingLocked and CurrentFeeRate messages

* cleaning up stale announcements on restart

* more informative/less spam logs in Channel

* (gui) Wrapping payment events to display date of event

* Also added controls to item content in cell factory overrides. This
  should prevent prevent duplicates as reported in #115
2017-07-26 18:57:31 +02:00
Pierre-Marie Padiou
f321d3e4ec Fixed links in README 2017-07-23 17:11:08 +02:00
rem0g
75ef04ac23 API: Add description to receive in help menu (#114) 2017-07-22 18:16:17 +02:00
rem0g
f51be77ab1 Update README with description to receive method (closed #111) (#113)
Description is mandatory when using receive as method to generate payment request.
2017-07-22 18:14:52 +02:00
sstone
905aebebbd set version back to 0.2-SNAPSHOT 2017-07-19 17:15:04 +02:00
sstone
c85823f5b4 set version to 0.2-alpha4 2017-07-19 17:12:21 +02:00
sstone
ecce374e1b use bitcoin-lib 0.9.12 2017-07-19 16:33:19 +02:00
sstone
ec1cd2b9ff make sure we sign data that is 32 bytes long, with valid private keys 2017-07-19 16:32:10 +02:00
Pierre-Marie Padiou
8c00efb9b6 Parse max_htlc_value_in_flight_msat as an unsigned long (closes #103) (#109)
* handle overflow for field 'max-htlc-value-in-flight'
* moved serialization of uint64 to codecs class
* moved constraint check
* fix configuration (was using hard coded values)
* using UInt64.MaxValue in test constants
* added a default handler for 'ok' messages in Channel
2017-07-17 19:36:49 +02:00
Pierre-Marie Padiou
5c53bf5f4d Reproduced and fixed issue with different dust limits (closes #97) (#108) 2017-07-17 19:18:27 +02:00
Pierre-Marie Padiou
1c3b1bfed5 Added support for multiple external ip in node announcements (#106)
* supported (possibly empty) list of external ips
* fixed new conf value for integration tests
2017-07-17 15:41:46 +02:00
Pierre-Marie Padiou
7d669630ca Simplified computation of closing fee (#107) 2017-07-17 15:11:31 +02:00
Pierre-Marie Padiou
073705a957 Implement state-counter retransmission (#105) 2017-07-17 14:28:07 +02:00
Fabrice Drouin
21d1d7c667 Fix update_add_htlc encoding (#104)
* update_add_htlc wire message: swap expiry and payment hash fields
see BOLT 2

* fix accept_channel wire encoding
'min depth' and 'hlc min msat' fields we swapped

* gui: set ip to "unknown" when it has not been set in node announcements
2017-07-12 20:05:51 +02:00
Pierre-Marie Padiou
e7833055bd Added support for BOLT 11 payment request (#102)
* implement BOLT 11 (payment request)
* (javafx) Added QR Code in receive payment modal, using zxing to generate QR code
2017-07-12 20:03:41 +02:00
Fabrice Drouin
7427cdb27b Interpret feature bits as per rfc PR 156-169-178 (#101) 2017-07-11 17:41:14 +02:00
Fabrice Drouin
83238b6299 Use 8 bytes for msat amounts (#99)
(see RFC PR 175)
2017-06-27 10:48:56 +02:00
Pierre-Marie Padiou
53243b5e74 Use an exponential backoff timeout for connection retry (#96)
* reduced logging for Reconnect events
* peer is now able to switch transports, in case of undetected disconnection
* now using an exponential backoff retry when reconnecting to peers
* put the arguments of the 'open' http method in the same order as the uri
2017-06-27 10:47:35 +02:00
Fabrice Drouin
12c90a7efe api: add a simple getinfo method (closes #98) (#100)
* api: add a simple getinfo method returning node id, node alias, port, chain hash and current block height
* api: add short description of getinfo [ci skip]
2017-06-27 10:46:30 +02:00
Fabrice Drouin
c0ad616a32 Upgrade to bitcoin-lib 0.9.11 (#95)
we now use spongycastle instead of bouncycastle
2017-06-09 17:39:03 +02:00
Pierre-Marie Padiou
582c327e04 Minor tweaks (#94)
* using scodec BitVector for interpretation of features
* datadir is now a file instead of a string
* (minor) made option mapping more explicit
2017-06-08 18:47:59 +02:00
Pierre-Marie Padiou
1a81ab1945 Replaced java serialization by scodec (#92) 2017-06-08 15:21:07 +02:00
Pierre-Marie Padiou
d86dd72d78 Separate code into modules (#91) (closes #88) 2017-06-06 18:37:34 +02:00
Pierre-Marie Padiou
7f747d55fd Added support for channel_disabled flag and updated failure codes (#90)
* added support for 'disable' flag in announcements (see lightningnetwork/lightning-rfc/pull/143)
* now responding TemporaryChannelFailure errors to CMD_ADD_HTLC in OFFLINE
* added helper methods for interpreting ChannelAnnouncement flags
* (gui) added image depicting flags of announcement in channels table
* implemented all failures as per lightningnetwork/lightning-rfc/pull/167
* router: don't use channels that have been announced as disabled
2017-06-02 20:14:21 +02:00
pm47
6297054a79 added validation checks on payment hash 2017-05-16 14:57:52 +02:00
pm47
0a597adc5a peer now keeps track of both temporary and final channel ids 2017-05-12 13:27:20 +02:00
Pierre-Marie Padiou
766af1484e Update README.md 2017-05-02 11:12:57 +02:00
pm47
c7b5953b64 send INPUT_RECONNECTED right away when restoring a channel and peer is already connected 2017-04-28 18:40:07 +02:00
pm47
80281bbf78 (minor) ignoring Reconnect messages when peer is a listener 2017-04-28 17:54:52 +02:00
pm47
aafa3f63a6 fixed race condition when registering tcp client listener 2017-04-28 17:54:52 +02:00
pm47
b981c67d37 now using SecureRandom instead of Random 2017-04-28 17:54:52 +02:00
pm47
2366804ff7 peer is now stopped and cleaned up upon reconnect timeout if there are no channels 2017-04-28 17:54:52 +02:00
pm47
d8e4af95fa quick and dirty fix to updates the node announcement on startup 2017-04-28 17:54:52 +02:00
pm47
6c196f8733 peer now reconnects on new/restored channels instead of relying on initial stateTimeout 2017-04-28 17:54:52 +02:00
sstone
cadf0bf4e9 use hash of block #0 as chain hash 2017-04-28 17:16:23 +02:00
Pierre-Marie Padiou
4c6f8695cb Make router not broadcast announcements back to sender (#82)
* replaced system scheduler by FSM timers
* added a basic way of not sending routing messages to origin peer when broadcasting
2017-04-28 17:03:25 +02:00
anton
cd9cf95643 Combine sigs in a more conscise way 2017-04-28 16:16:47 +02:00
dpad85
4f9b964bae Fixed type cast issue with peers rpc service (#79)
* (gui) fixed inconsistency with channel id copy
* Fixed cast issue with `peers` rpc service
* also renamed `channelIdHex` to `channelId`
2017-04-26 16:37:29 +02:00
pm47
097f51f1c5 back to SNAPSHOT 2017-04-25 18:37:26 +02:00
pm47
598c7a99ee set version to 0.2-alpha3 2017-04-25 18:04:48 +02:00
pm47
8d9ed6de8f client must die when transport dies (regression caused in 75e4923002) 2017-04-25 18:00:42 +02:00
sstone
645cb657d5 funding: remove redundant address test 2017-04-25 13:06:13 +02:00
pm47
8589294933 fixed naming inconsistencies pubkey->nodeId 2017-04-25 10:33:41 +02:00
Pierre-Marie Padiou
66cb538ed3 clarify version requirement of bitcoin core 2017-04-25 10:20:21 +02:00
Fabrice Drouin
14cdf8f345 BOLT4: update Sphinx to match the latest specs (#76)
* Sphinx: implement BOLT PR 145
see https://github.com/lightningnetwork/lightning-rfc/pull/145
* Sphinx: use simplified onion proposed in PR145
address and hop-payload fields have fused, and we are now routing based on channel short ids
* Updated relayer to take advantage of shortChannelId
* Sphinx: use more meaningful class names
* BOLT 4: use 32 bytes MAC in reply error packet
instead of 20
2017-04-24 15:57:13 +02:00
Fabrice Drouin
50429da0ed Wire: add chainHash field to open message (#69)
* wire: add chainHash field to open message (see https://github.com/lightningnetwork/lightning-rfc/pull/135)
* split validateParams() into "funder" and "fundee" versions (fundee also needs to check the chain hash funder sent in their open message)
2017-04-20 17:27:53 +02:00
dpad85
fe22572976 New PaymentRequest object + HTLC verification (#72)
* Added a PaymentRequest object
* A `PaymentRequest` can be serialized/deserialized with `write`/`read`
  static functions in companion
* Amount validation is handled in constructor
* `ReceivePayment` message in payment handler generates a `PaymentRequest`
* Updated tests
* HTLC succeeds if amount is equal or greater than requested amount
* If the amount paid is more than twice the amount expected, the HTLC fails
* (gui) display payment failed cause in notification
* Improved payment request validation messages
2017-04-20 17:26:30 +02:00
Fabrice Drouin
75e4923002 BOLT 4: use proper cipher stream for reply messages (#75)
* more logs on connection establishment, added initialize() to FSMs
2017-04-20 17:08:23 +02:00
Pierre-Marie Padiou
da78ae5356 Better router/channel exception handling (#71)
* added specific channel exceptions
* added specific router exceptions
* retry payment when an error occurs at the first channel
2017-04-20 11:47:38 +02:00
Anton Kumaigorodski
2eded652e5 Validity of remote final script is not checked when a Shutdown was already sent (#74)
* Superfluous mapping as we're only interested in collection sizes
* Add a shutdown with invalid final script test
2017-04-20 11:12:12 +02:00
Anton Kumaigorodski
12ca54bc90 both localCommit & remoteCommit htlcs are now checked on Shutdown (#73) 2017-04-19 16:50:01 +02:00
Fabrice Drouin
378a332954 LocalParams should not include maxFeerateMismatch (#70)
as it is not a direct protocol parameter
2017-04-12 15:17:01 +02:00
Pierre-Marie Padiou
102f76e401 Automatic reconnection (#64)
* automatically reconnect every minute (optional, enabled by default)
* (gui) removed reconnect
* use stateTimeout to send ping messages
2017-04-11 17:17:26 +02:00
pm47
7885acd4af fatal error messages are now sent to stderr in headless mode 2017-04-11 17:05:03 +02:00
dpad85
3f404025da Added a receive method to the API to generate payment requests (#63) 2017-04-11 16:47:09 +02:00
Fabrice Drouin
fd56d35073 make max-feerate-mismatch configurable and set default value to 500% (#62)
* make max-feerate-mismatch configurable and set default value to 500%
there can be a significant gap between the fee rate estimated by different bitcoin
clients (see estimatesmartfee) so we must set a reasonably high value for the threshold
above which we consider htat local and remote fee rates are too different and close the
channel.

* use difference/average to compare feerate
it becomes symmetrical and easier to reason with, and also more forgiving.

* make "UpdateFee" minimum ratio configurable
2017-04-11 16:45:03 +02:00
pm47
e2350ca8b1 added tests for state WAIT_FOR_FUNDING_PARENT 2017-04-11 16:43:38 +02:00
dpad85
9a2bdb31a1 (GUI) Added an option in the GUI to open a simple connection to a node (#68)
* (gui) added option to open a simple connection to a node in gui
* added checkbox in open channel modal window
* when the 'simple connection' checkbox is selected, the `fundingSatoshis`
  and `pushMsat` fields are ignored
* (gui) channelid is displayed as hexa
* (gui) fixed testnet color
* (gui) increased the inital size of the main window
* (gui) display the node alias in channel pane when the node is announced: alias is appended to node id; using option to open channel
2017-04-11 12:07:09 +02:00
pm47
5bcf2f4ae7 setting watches when restoring in CLOSING state 2017-04-11 10:18:12 +02:00
pm47
c94972f7ce made transport actor a FSM instead of a LoggingFSM 2017-04-11 10:11:39 +02:00
Pierre-Marie Padiou
47310c1534 Fixed racing issues in integration tests making travis fail (#61)
* integ tests: removed sleeps, waiting for watches before generating blocks
* fixed a (rarely occuring) race condition related to bulk-generation of test blocks
2017-04-10 16:58:17 +02:00
Pierre-Marie Padiou
6e98fccc5d replaced compliance status by a link to releases 2017-04-07 12:04:07 +02:00
pm47
8fedd5acd9 back to SNAPSHOT 2017-04-07 11:48:10 +02:00
pm47
1e792f63d7 set version to 0.2-alpha2 2017-04-07 11:27:05 +02:00
pm47
e8485128e5 increased integration test timeouts 2017-04-07 11:26:28 +02:00
pm47
6be1644bb4 improved route calculation errors 2017-04-07 11:26:17 +02:00
Fabrice Drouin
54a8b56c3d Mitigate testnet malleability attacks (#59)
* malleability fix: we wait the parent of the funding tx or its malleated version to be confirmed,
then we create the funding tx

* set default min depth to 2

* disconnection during WAIT_FOR_FUNDING_CREATED now closes the channel
2017-04-06 17:55:16 +02:00
pm47
4991a3bc96 reduced log verbosity 2017-04-05 18:30:29 +02:00
Fabrice Drouin
07a1cb0b99 add ping and pong messages (#56)
* add ping and pong messages
* add setting for ping interval to configuration file
* channels can now be restored when peer is connected
2017-04-05 18:06:09 +02:00
pm47
a1b9860483 added timestamp to test logging 2017-04-05 15:30:31 +02:00
dpad85
e18fcbab47 ZMQ connection monitoring (#57)
* made zeromq listener non-blocking and monitors connection status
* now throwing an exception at startup in case of zmq connection issues
* (gui) added a blocking modal in main window for ZMQ events
* made boot error exit the application in headless mode
2017-04-05 15:24:05 +02:00
pm47
edefb8d235 increased connection timeout in tests 2017-04-05 15:02:26 +02:00
Pierre-Marie Padiou
ede49d0a66 added gitter badge (#58) 2017-04-05 14:42:14 +02:00
pm47
bcbc40e7ad sending a sig on reconnection, because last CMD_SIGN might have been ignored 2017-04-04 15:51:17 +02:00
pm47
9125742900 removed another debug log line 2017-04-04 14:47:58 +02:00
pm47
70d62a5c78 removed debug log line 2017-04-04 14:45:09 +02:00
Pierre-Marie Padiou
1d53e53dd2 channel validation is now made in batch, using parallel JsonRPC requests (#53) 2017-04-04 14:26:50 +02:00
Fabrice Drouin
e8be919fe5 fix fuzzy test (#54)
* re-enabled fuzzy test
* fix fuzzy test: Bob was using his own public key instead of Alice's  when sending HTLCs to Alice
2017-04-04 11:22:39 +02:00
dpad85
00c9aa6200 (gui) Shortened notification and emphasized message (#55)
* fixed uncaught escape key hiding notification scene
* message notification should be straightforward and as brief as possible
* updated notifications icons
2017-04-04 10:43:41 +02:00
Fabrice Drouin
3e53a1fc2c Use CPFP to create our funding tx (#50)
* use CPFP to create our funding tx
ask bitcoin-core to fund a standard tx which has a segwit output, and spend it to
create our funding tx. the parent and the funding tx are published at the same time
and should end up in the same block.
this should give us some protection against malleability attacks: it should improve the chances
of our funding tx being mined, and if the parent tx loses the race against it malleated version
then our fuding tx will not be published, which is much better than having a conflicted funding
tx

* set default fee-rate-per-kw to 10000

* guestimate that feerate-per-kw is feerate-per-kb / 2 for a standard commit tx

* channel: estimate the fee for the parent of our funding tx
instead of hardcoding it

* integration test: increase channel capacities
because we use a larger feerate-per-kw now

* added a feerateKB2Kw method
2017-03-31 20:28:51 +02:00
dpad85
34677f0ed6 Added an activity tab to the GUI (#52)
* (gui) added Activity tab (payment sent, received, relayed)
* GUIUpdater listens to PaymentEvent
* payments are listed in separate tableviews (sent, received, relayed)
* added Payment[Sent, Relayed, Received] events
* (gui) Handling Relayed and Sent payments in Activity Tab
* (gui) fixed amount columns in activity
* (gui) Added formatting to monetary columns of activity tables
* (gui) payments are prepended to activity tables
2017-03-31 20:03:36 +02:00
dpad85
c3bfbf4a14 Added warning for openJDK in README.md (#48) 2017-03-31 18:19:39 +02:00
Pierre-Marie Padiou
72a14914fc Added integration tests on htlc relaying in on-chain scenarios (#51)
* using scodec for failure messages

* we now update routing info and retry payments in case of Update failure

* properly implemented BOLT4/'Receiving Failure Codes' (also fixed a bug with UnknownPaymentHash)

* added integration tests on htlc timeout

* relayer now sends an UpdateFail to downstream when an htlc timeout hits the blockchain

* added integration tests on extracting preimage from blockchain

* cleaning up of htlc bindings in relayer

* router now doesn't send a 2nd request to bitcoind when it is notified of a new channel twice in a short amount of time

* peerclient now asks for witness when retrieving blocks from bitcoind

* getTxBlockHash now returns None instead of an error when tx is in the mempool

* we now look into the mempool when checking for WatchSpent

* fixed FundingSigned been sent twice when deferring an early FundingLocked

* added zeromq listener, removed predefined eclair.conf files in test, limited concurrent rpc calls to 5 for the router

* Update README.md with ZMQ configuration

* Added a warning and link to alpha2 install instructions

* removed PeerClient

* improved PeerWatcher performance
2017-03-31 18:08:59 +02:00
Pierre-Marie Padiou
eba5915f8c Use chain getblockchaininfo.chain to infer the magic value (#45)
* use chain getblockchaininfo.chain to infer the magic value, remove bitcoind.network conf parameter (closes #43)

* minor: renamed socket->socketAddress, also rebased
2017-03-28 14:05:52 +02:00
Pierre-Marie Padiou
16d3960f75 Automated retry of payments (#44)
* using scodec for failure messages

* we now update routing info and retry payments in case of Update failure

* properly implemented BOLT4/'Receiving Failure Codes' (also fixed a bug with UnknownPaymentHash)
2017-03-27 13:29:27 +02:00
dpad85
e728cad4ac Access to version/commit id at runtime (#42)
* Added help option in command line; added version in logs

* Version and commit id added in manifest (use Specification-Version for commitId)

* (gui) version in about is now dynamic; ESC key closes about

* Excluded unused protobuf dependency

* Update README.md
2017-03-27 11:14:54 +02:00
pm47
d16fe045cc clarify that we only support java 1.8, not 1.9 (closes #47) 2017-03-26 14:08:04 +02:00
Pierre-Marie Padiou
fa050da12c Removed the 'windows installer' part
it will be generated at 'mvn package' if Inno Setup is installed
2017-03-22 14:45:51 +01:00
Pierre-Marie Padiou
9d962e6571 Updated README 2017-03-21 17:32:15 +01:00
pm47
15f6fbdca4 back to 0.2-SNAPSHOT 2017-03-21 17:17:48 +01:00
pm47
05607a01e5 setting pom version to 0.2-alpha1 2017-03-21 16:52:38 +01:00
pm47
79f2882ac8 fixed init error in package app 2017-03-21 16:47:24 +01:00
pm47
7106e2e2c9 fixed warning in scaladoc 2017-03-21 15:57:11 +01:00
pm47
6c268a9df1 fixed error handling during initialization 2017-03-21 15:55:41 +01:00
pm47
0c580528b7 made build process less verbose 2017-03-21 14:55:16 +01:00
pm47
17df27a01f fixed comment 2017-03-21 14:55:01 +01:00
sstone
2e2a47e5fd fix local/remote delay inversion 2017-03-21 14:37:04 +01:00
dpad85
eb78f07e2d (gui) fix issue with initial size of modals in ubuntu 2017-03-21 14:36:55 +01:00
Pierre-Marie Padiou
f984e55c68 fixed how we interpret feature bits (#37) 2017-03-21 13:16:12 +01:00
Fabrice Drouin
516ce96c0c BOLT 3: fix computation of obscured tx number (#36)
* BOLT 3: fix computation of obscured tx number
* BOLT 3: fix computation of obscured tx number
2017-03-21 12:02:36 +01:00
dpad85
ca2b678fd4 (gui) updated about 2017-03-21 11:58:57 +01:00
dpad85
03cf8616c5 (gui) More concise about 2017-03-21 11:58:57 +01:00
dpad85
d3e521d9e3 Fixed RPC calls
* help was not up-to-date
* send returns a PaymentResult
* get channel must use a BinaryData
2017-03-21 11:58:57 +01:00
Pierre-Marie Padiou
a59b4ad2ed Fixed non-compliant wire messages (#34)
* fixed fundingOutputIndex uint8->uint16

* removed HTLC-timeout sigs from RevokeAndAck

* added an (unused yet) features field to channel announcement
2017-03-20 16:18:46 +01:00
dpad85
f9bdd72b24 Wip gui wording (#33)
* (gui) removed headings in tabs listing global nodes/channels

* (gui) wording node id

* (gui) title of network announcement tabs is updated with list changes
2017-03-20 15:56:20 +01:00
sstone
ae34bbaf89 tcp client: use keepalive=true 2017-03-19 22:01:04 +01:00
sstone
22e579cf82 handle case where bitcoind estimatesmartfee returns -1 2017-03-19 22:00:12 +01:00
dpad85
2b11a048fc (gui) add announcement only if absent from network list 2017-03-17 20:04:17 +01:00
Fabrice Drouin
2612c4845c Update README.md 2017-03-17 19:08:24 +01:00
Fabrice Drouin
c0a8edb598 Update README.md and add build.md 2017-03-17 19:07:29 +01:00
Pierre-Marie Padiou
f7e8d531fa added a supervision strategy to all actors (#31) 2017-03-17 18:00:43 +01:00
pm47
6797f5e3e9 made windows installer generation run at package phase 2017-03-17 17:51:29 +01:00
Fabrice Drouin
dc3c49fbeb BOLT 4: propagate htlc failures upstream (#30)
* BOLT 4: create return packets for failure messages

* bolt 4: add low-level methods for returning error messages

* BOLT 4: return shared secrets along with the onion packet

* codecs: minor fix the UpdateFailHtlc codec

* BOLT 4: return htlc failures
create and propagate htlc failures as per BOLT 4

* update tests

* channel: publish short channel id when restoring data
and not on reconnection

* channel: remove "channel update" parameters from Commitments.sendAdd()

* channel: send "add htlc failed" to relayer
and a more informative message to the sender through handleCommandError

* keep shared secrets local to PaymentLifeCycle
CMD_ADD_HTLC should just know the onion packet (as before), not the shared secrets

* relayer: handle failure cases
wrong expiry, amount, ...

* added a simple test on the fail workflow

* added a 'channel-capacity-exceeded' test

* make PaymentSucceeded and PaymentFailed children of PaymentResult

* relayer: ChannelUpdates should be stored in a Map, not an Option

* reply with update_fail_malformed_htlc when the onion is not parsable

* channel: handle 'update_fail_malformed_htlc' in Shutdown state
and add tests

* minor: AddHtlcSucceeded -> AddHtlcSuccess

* use bitcoin-lib 0.9.10
2017-03-17 17:01:14 +01:00
pm47
9253267c69 put back CONSOLE appender to logback (still disabled) 2017-03-17 15:35:17 +01:00
pm47
33ef4c816c set more lax timeouts for travis 2017-03-16 17:43:46 +01:00
Pierre-Marie Padiou
71f62eead9 Added support for UpdateFee (#29)
* clarified which fee rate we are using in calculations, also feeRate->feerate

* added support for UpdateFee

* fixed closing workflow when starting with different fees

* attempt at solving travis issues

* fixed feeratePerKw not cleaned up after tests

* added test on invalid initial feerate

* fixed bitcoind api calls mixup, tested on mainnet

* rebased from wip-bolts
2017-03-16 17:31:38 +01:00
pm47
a67e1fe6c7 fixed incorrect log message in PeerWatcher 2017-03-16 17:10:02 +01:00
pm47
b0eb2ffb1d better cleanup after integration test 2017-03-16 17:04:01 +01:00
pm47
815ce15f5f temporary test files are now written to target/ directory 2017-03-16 16:46:54 +01:00
pm47
db342e8db3 basic 3-hops integration test (setup+send htlc) 2017-03-16 16:19:15 +01:00
dpad85
19c65790e2 (gui) Added a Preloader when application starts with a gui
* The splash window is now a javafx preloader
* Node setup is now handled during the JavaFX application init phase
* During this initialization the preloader is shown (handled by FX).
* Setup events are dispatched from main App to preloader with notifyPreloader
* When setup errors, display them in the preloader. Main gui is not loaded
2017-03-16 13:36:48 +01:00
pm47
a9a60fe1c2 first shot at multi-hops integration test (WIP!!) 2017-03-16 00:13:00 +01:00
pm47
21402dabb3 make config load eclair subsection 2017-03-15 23:45:11 +01:00
pm47
b190d0df39 quick fix: config in boot was only reading default parameters 2017-03-15 23:37:49 +01:00
pm47
a2eab64785 fixed bug in router where we were adding spent channels 2017-03-15 18:52:05 +01:00
pm47
dc51943621 db files are now in <datadir>/db/ 2017-03-15 18:51:40 +01:00
pm47
05e2bd34d4 now storing conf, seed and db in a datadir (default ~/.eclair) 2017-03-15 18:06:02 +01:00
pm47
92819f3835 added windows standalone installer 2017-03-15 17:54:50 +01:00
pm47
6f09f2cfe2 removed a.db (mistakenly committed) 2017-03-10 17:07:07 +01:00
pm47
a65d411f5b added safety measures to PeerWatcher 2017-03-10 17:07:07 +01:00
pm47
ade9ca0121 ignoring INPUT_DISCONNECTED in CLOSED state 2017-03-10 17:07:07 +01:00
pm47
e66d25a561 fixed potentially no-unique channel actor name 2017-03-10 17:07:07 +01:00
pm47
40e22f3dec channel updates are now cleaned up when channel is torn down 2017-03-10 17:07:06 +01:00
dpad85
7ca2431511 (gui) improved status bar resizing behaviour 2017-03-10 16:00:26 +01:00
Pierre-Marie Padiou
f2c49c275a Switch to 32B channel-id (#28)
* implemented long channel-id + delayed announcements

* we now handle the case when disconnected before having sent announcement sigs

* channel-id is now computed as a txHash ^ outputIndex
2017-03-10 11:47:19 +01:00
dpad85
876d6f61fd (gui) added node alias and RGB in status bar 2017-03-09 19:22:51 +01:00
dpad85
0b2993ea6a (gui) set node IP column width 2017-03-09 19:03:31 +01:00
dpad85
4b5fa0c92b (gui) fixed missing parenthesis in rgb 2017-03-09 19:02:53 +01:00
dpad85
02dd347158 (gui) nodes/channels tables use Announcement as underlying datas
Also added a IP column to nodes table
2017-03-09 18:55:19 +01:00
dpad85
af691d6e9b (gui) refactored variable names of nodes/channels in network 2017-03-09 18:55:03 +01:00
pm47
045dfe589a fixed bug in router where spent channels wheren't cleaned up 2017-03-09 18:44:59 +01:00
pm47
746bf08963 replaced map by collect 2017-03-09 17:45:53 +01:00
pm47
394c0caf0a watcher is now replaying txes since parent confirmed when an output has already been spent 2017-03-09 17:24:52 +01:00
pm47
f840ec9835 made the disctinction between binding-ip and public-ip in conf 2017-03-09 17:24:52 +01:00
dpad85
d511796f90 (gui) when channel is offline, close button label is 'Force close' 2017-03-09 16:46:23 +01:00
dpad85
245ed99baa (gui) send payment parameters are checked asap 2017-03-09 16:44:29 +01:00
dpad85
8a9d14e61c Handling gui logs when logging with colors 2017-03-09 15:04:11 +01:00
dpad85
dc63f74e0b (gui) Removing terminated local channels from list 2017-03-09 15:04:11 +01:00
pm47
144bfe7760 CMD_CLOSE in OFFLINE now results in an unilateral close 2017-03-09 15:03:38 +01:00
pm47
fe0df3d97d shutdown message is now acknowledged by closing_signed 2017-03-09 14:52:17 +01:00
pm47
3454fb784a Register now uses 'forward' instead of '!' 2017-03-09 14:06:46 +01:00
pm47
0283631fdd refactored CMD_CLOSE handling 2017-03-09 12:03:54 +01:00
pm47
4ea6fdcea7 fixed bug when channel disappears 2017-03-09 11:51:44 +01:00
sstone
4d3a344a69 bolt 3: update test vectors 2017-03-08 10:48:24 +01:00
sstone
c1cf96e4f8 BOLT 3: check that we can directly spend htlc outputs 2017-03-07 23:06:26 +01:00
sstone
08bf37c1dc make htlc output directly spendable with the revocation key
see https://github.com/lightningnetwork/lightning-rfc/pull/105
and https://github.com/lightningnetwork/lightning-rfc/pull/123
test vectors pending
2017-03-07 17:44:40 +01:00
sstone
076011bd35 channel: fix init -> offline transition
don't persist and don't send anything when transitioning from INIT to OFFLINE
2017-03-07 14:52:49 +01:00
Pierre-Marie Padiou
a24bebf666 Implemented long channel-id + delayed announcements (#27)
* implemented long channel-id + delayed announcements

* added a ShortChannelIdAssigned event
2017-03-06 17:19:55 +01:00
sstone
e18a12c565 forward payments through register
we ask register, which maintains of active channels, to forward payments instead
of relying on actor selection/actor path.
2017-03-06 15:25:01 +01:00
pm47
f3db1ea15c moved extractOutgoingMessages to Helpers 2017-03-02 17:03:50 +01:00
pm47
c017a9a217 routing announcements are now stored individually 2017-03-01 20:09:22 +01:00
pm47
b05444fa77 peersDb is now a simple cache containing valid ip addresses 2017-03-01 19:01:09 +01:00
pm47
453dc699ed removed ChannelState and moved dbs declarations to Dbs 2017-03-01 18:27:20 +01:00
pm47
be73ba7900 reworked Forwarder and made it manage OFFLINE->X transitions 2017-03-01 16:21:05 +01:00
sstone
adda1a4a58 channel: stay in CLOSING mode when disconnected
there is no need to switch to OFFLINE
2017-03-01 10:11:21 +01:00
sstone
7bf5331802 router: start with an empty state if nothing was saved
if there is no persisted router record, start with an empty state
2017-03-01 10:11:21 +01:00
dpad85
074b8f7263 (gui) enable reconnect button if offline and funder 2017-02-28 19:18:56 +01:00
sstone
1172281248 Merge branch 'wip-bolts' of https://github.com/ACINQ/eclair into wip-bolts 2017-02-28 18:06:26 +01:00
sstone
254eb80b95 channel: switch to CLOSING state when restoring a CLOSING channel 2017-02-28 18:03:53 +01:00
dpad85
eb8d3022b4 (gui) host regex accepts 2017-02-28 17:25:46 +01:00
dpad85
5dc01d6922 (gui) use futures to handle new connection params
* InetAddress creation can take some time (network traffic) and should not
  be handled in the JavaFX thread (UI freeze)
2017-02-28 17:22:53 +01:00
sstone
14da69a612 router: consistency fixes
- use the same signature for main and mainWithLogs
- remove useless fields from Router.State
- start in 'uninitialized' mode and wait for State message (either empty or retrieved from db)
2017-02-28 15:14:03 +01:00
pm47
b4af63b728 Merge branch 'wip-persist3-pm' into wip-bolts 2017-02-28 14:19:53 +01:00
pm47
4796ad8bc4 removed ChannelParams 2017-02-27 23:52:08 +01:00
pm47
074f79f83b merged from wip-persist3 2017-02-27 23:24:13 +01:00
pm47
103daf90aa reworked channel events 2017-02-27 23:17:06 +01:00
sstone
df6633eed0 peer watcher: confirmed already spent txs rihgt away
When processing WatchConfirmed messages, check if the tx has already been confirmed and
trigger the watch immediately if possible. This will let us detect if a channel has been
confirmed while the node was offline or turned off
2017-02-27 19:23:57 +01:00
dpad85
698808b90f (gui) channel pane context always built in JavaFX thread 2017-02-27 19:20:25 +01:00
sstone
f5ea399f71 channel: use a "forwarder" to store and send messages on transition 2017-02-27 19:19:32 +01:00
sstone
eaca287922 move db into nodeParams 2017-02-27 18:26:41 +01:00
sstone
6a94a62a69 testnet chain is called "test", not "testnet"
the chain returned by getblockchaininfo is "test" in testnet mode
2017-02-27 18:09:39 +01:00
sstone
817704f5da Merge branch 'wip-bolts' into wip-persist3 2017-02-27 16:35:01 +01:00
pm47
b8379ed461 replace absolute reserveSatoshi in nodeParams by a ratio 2017-02-27 15:45:21 +01:00
sstone
6c834289ec minor fixes
- call setup.bootsrap in headless mode
- don't save router transient fields
2017-02-27 15:38:46 +01:00
pm47
5b718bffac factored state tests initialization 2017-02-27 15:26:04 +01:00
sstone
f6289795d2 ShaChain: add scodec serializer 2017-02-26 23:26:16 +01:00
sstone
2a3d96b745 restore watches on re-connection 2017-02-26 20:29:34 +01:00
sstone
967404a82d persist channels, peers, and router
- channels are pesisted using the transition callback
2017-02-26 19:30:13 +01:00
pm47
24a3801961 removed Globals, nodeParams are now passed in constructors 2017-02-26 17:47:19 +01:00
pm47
89db03fe91 added a NodeParams class 2017-02-26 16:06:41 +01:00
sstone
27b2c4f42c make codec output serializable 2017-02-26 13:17:31 +01:00
sstone
071f705df4 channel: use chanel id in outgoing error messages 2017-02-25 21:31:17 +01:00
sstone
1826fb6e6a channel: use transition change callback to send outgoing messages 2017-02-25 21:25:21 +01:00
pm47
810aed301d added capacity to ChannelDiscovered event 2017-02-24 16:01:21 +01:00
dpad85
4d99e39184 (gui) Notifications use a PopupWindow instead of a Stage 2017-02-24 14:46:49 +01:00
dpad85
4e2bf2b047 (gui) reconnect is enabled if state=offline and node is funder 2017-02-24 14:46:49 +01:00
dpad85
ed70d0299c (gui) Improved channelpane structure and responsive behaviour 2017-02-24 14:46:48 +01:00
dpad85
41f17471d4 (gui) fixed context menu of channel panel 2017-02-24 14:46:48 +01:00
pm47
550707004d using bits instead of bytes for features 2017-02-24 13:23:43 +01:00
pm47
c9d6a20954 updated api 2017-02-23 18:46:19 +01:00
pm47
3d08c7e391 improved watcher logs 2017-02-23 15:27:41 +01:00
pm47
71a38e5e95 made function names consistent 2017-02-23 15:12:08 +01:00
pm47
a33d5ef1a0 mitigated race condition between FundingLocked and AnnouncementSignatures 2017-02-23 14:52:10 +01:00
pm47
a8c25e8cf9 reformatting 2017-02-23 14:51:31 +01:00
pm47
4d180003e9 improved uniclose logs 2017-02-23 14:07:33 +01:00
pm47
c2b3f727ba added HasChannelId trait to Error message 2017-02-23 13:47:56 +01:00
pm47
107beee854 merged from wip-disconnect 2017-02-23 11:56:27 +01:00
pm47
6ebe5b4ff2 made commitment functions no-op when replaying known messages, improved fuzzy tests 2017-02-23 11:50:58 +01:00
pm47
1ef1a4a6a1 added (manual) test on spending of csv tx 2017-02-22 17:03:09 +01:00
pm47
169add4c02 now handling the case where remote publishes its 'next' commit 2017-02-22 13:57:13 +01:00
dpad85
9835bd6b52 (gui) Inline stage transparency (loading css can take some time) 2017-02-22 12:12:15 +01:00
pm47
b8fac57ac0 fixed issue with state data in ChannelChangedState (stateData->nextStateData) 2017-02-21 11:39:33 +01:00
dpad85
5255d68f13 (gui) typos 2017-02-20 18:02:15 +01:00
dpad85
6c97d82e95 (gui) Handle non fatal exception when creating connection
* pubkey conversion to binary from string is prone to failure
2017-02-20 17:09:55 +01:00
dpad85
8c7f6967e8 (gui) validator pubkey must be 66 chars 2017-02-20 15:22:07 +01:00
dpad85
aa71968ec8 (gui) set min size to stages 2017-02-20 15:21:14 +01:00
dpad85
2dab76e2eb (gui) notification border changes color with type 2017-02-20 14:25:58 +01:00
dpad85
0e49dc537e (gui) Fixed notif close button; added app name in title 2017-02-20 14:19:29 +01:00
dpad85
bf03423818 (gui) renamed eclair icons 2017-02-20 14:18:26 +01:00
dpad85
543567bf12 (gui) smaller icon in notifications 2017-02-20 14:13:42 +01:00
dpad85
861fce351a (gui) color of close notification button 2017-02-20 14:08:32 +01:00
dpad85
e39f52c360 (gui) Notification is dismissed after a few seconds 2017-02-20 12:08:31 +01:00
sstone
27b41a580f bolt 3: match against latest tx test vectors 2017-02-20 11:39:01 +01:00
sstone
620fdd78d1 Merge branch 'wip-bolts' of https://github.com/ACINQ/eclair into wip-bolts 2017-02-20 10:43:22 +01:00
pm47
1eea11cf44 NORMAL<->OFFLINE now fully supported, added fuzzy tests 2017-02-20 00:31:21 +01:00
pm47
3e1b0fe202 added fuzzy disconnection test (wip) 2017-02-17 20:46:35 +01:00
dpad85
07298ff740 Use custom JavaFX Notification System
* Removed all AWT dependency (tray icon) because of stability issues
* Notifications are handled in the Notifications Stage
* Notifications type can be SUCCESS, INFO, ERROR with NONE by default.
* Notification icon changes with type
2017-02-17 20:10:24 +01:00
pm47
7e9d07b9b4 merged from wip-bolts 2017-02-17 19:07:54 +01:00
pm47
559afff522 automatically sign back on revocation when necessary, removed scheduled sig 2017-02-17 18:52:07 +01:00
pm47
2e3031fafd Merge branch 'wip-bolts' into wip-disconnect 2017-02-17 16:30:06 +01:00
pm47
f061f59803 only forward cross-signed htlcs 2017-02-17 16:10:19 +01:00
pm47
a0318d6ade added support for disconnections (wip) 2017-02-16 19:03:02 +01:00
dpad85
3b65269c9a (gui) set limit to column width in network tables 2017-02-16 16:16:37 +01:00
dpad85
e1051a8743 (gui) Removed duplicate stage icon in splash 2017-02-16 16:16:36 +01:00
pm47
27572c519e DATA_WAIT_FOR_FUNDING_LOCKED_INTERNAL -> DATA_WAIT_FOR_FUNDING_CONFIRMED 2017-02-16 15:15:03 +01:00
pm47
002464d2be simplified error handling when there are no commitments yet 2017-02-16 15:04:48 +01:00
dpad85
b4c0494d9f (gui) using system tray and os notifications only if os is Windows/MacOs
Prevent issues with AWT in Linux
2017-02-16 12:25:14 +01:00
dpad85
c355dd4578 (gui) Fixed responsive behaviour of status bar 2017-02-16 12:25:14 +01:00
sstone
4bfec41041 bolt 3 test vectors: formatting fixes 2017-02-16 10:28:57 +01:00
pm47
1e58a69918 WAIT_FOR_FUNDING_LOCKED_INTERNAL->WAIT_FOR_FUNDING_CONFIRMED 2017-02-15 19:46:58 +01:00
pm47
a43dc181c6 removed special handling of CMD_CLOSE from WAIT_FOR_FUNDING_LOCKED_INTERNAL->NORMAL 2017-02-15 19:39:18 +01:00
pm47
4e7021d7c8 fixed fee not taken into account when offering htlcs 2017-02-15 18:37:02 +01:00
pm47
7da6df008a merged from wip-bolts 2017-02-15 18:13:22 +01:00
pm47
7d79f65bf2 channels are now multiplexed over single connections between nodes 2017-02-15 18:08:44 +01:00
dpad85
c22165ec0d Removed timestamp from sendPayment notification 2017-02-15 16:01:29 +01:00
sstone
fc9ccf8524 bolt 3: improve reference test
format test output to match the reference test vectors
2017-02-15 14:20:51 +01:00
sstone
8589b8847d update htlc success/timeout tx weights
see https://github.com/lightningnetwork/lightning-rfc/pull/104
2017-02-15 14:17:55 +01:00
pm47
e2fc3fbecc added support for 'initial_routing_sync' feature bit 2017-02-13 16:15:32 +01:00
pm47
6bd14bb089 added AnnouncementSignatures message, added support for feature bits 2017-02-13 14:45:26 +01:00
pm47
e8a74899f3 now take fees into account when accepting htlcs 2017-02-13 12:04:06 +01:00
Pierre-Marie Padiou
a3b906898b Refactored fee calculation (added trim functions) (#25)
* refactored fee calculation (added trim functions)

* now running BOLT 2 fee tests automatically
2017-02-13 11:11:54 +01:00
pm47
c72a2a5d8d router now checks announcement signatures 2017-02-10 18:05:06 +01:00
pm47
8334350576 separated NodeDiscovered/NodeUpdated events 2017-02-10 17:06:46 +01:00
pm47
e3e93dc95f removing channel updates when channel disappears 2017-02-10 17:01:45 +01:00
pm47
2b479aade6 enforcing sequential zero-based htlc ids 2017-02-10 16:55:43 +01:00
pm47
cb5189b3c7 added support for htlc-minimum-msat parameter 2017-02-10 16:01:34 +01:00
pm47
2d223ea51c added support for max-accepted-htlcs parameter 2017-02-10 15:50:07 +01:00
pm47
2a035bd005 added support for max-htlc-value-in-flight-msat parameter 2017-02-10 15:22:43 +01:00
pm47
8122b58229 refusing to boot if bitcoind's sync progress is <= 0.99 2017-02-10 14:41:42 +01:00
pm47
1e10074f27 added support for channel-reserve parameters 2017-02-10 14:36:03 +01:00
dpad85
0b2b3ea106 Simplified display of validation error display in gui
* reordered FXML elements
* removed dedicated error rows
2017-02-09 19:26:36 +01:00
dpad85
ef58ef0935 Code clean up 2017-02-09 16:34:39 +01:00
dpad85
105c7950bb Fixed context menu of channel 2017-02-09 16:34:39 +01:00
pm47
522c7e52f1 removed unused code 2017-02-09 15:56:58 +01:00
pm47
2d5d8a00a9 punishment -> penalty 2017-02-09 15:56:58 +01:00
dpad85
b9ec691dcd Fixed regex matching in GUIValidators 2017-02-09 15:06:45 +01:00
dpad85
a623810518 Handling of decimal amount when generating payment request
* if the amount unit is milliBTC or Satoshi, the input accepts decimal
  value. The amount must then be parsed and converted to mSat
* if the unit is milli satoshi, no decimal is allowed
* the decimal character is `.`
* improves the UX
2017-02-09 14:13:49 +01:00
dpad85
e5d73eec45 Updated host regex in gui 2017-02-09 14:13:49 +01:00
dpad85
654e4bfad3 Set log level to DEBUG for gui package 2017-02-09 14:13:49 +01:00
pm47
a481ea1bf3 improved logback_colors 2017-02-09 14:04:08 +01:00
pm47
51fd685595 fixed bug in payment-preimage extraction, handle failures in uniclose tx generation 2017-02-09 14:04:08 +01:00
pm47
8bc4e8349e PeerHandler now asks for segwit tx, fixed split bug in PeerClient 2017-02-09 14:04:07 +01:00
pm47
583b5af15c keeping last 1000 txes in memory so that WatchSpent can look in the past 2017-02-09 13:58:16 +01:00
pm47
f3fb3c2ae8 extended bitcoin client now returns 0 conf tx 2017-02-09 13:58:16 +01:00
pm47
f19231e883 make it an error to have an htlc expiry < 3 blocks 2017-02-09 13:58:15 +01:00
sstone
3b271cb6c2 make PeerClient ask for witness tx 2017-02-09 11:44:27 +01:00
sstone
20e91c1710 remove dead code 2017-02-09 11:44:27 +01:00
sstone
4b6dca7a0a make SignTransactionResponse a static class 2017-02-09 11:44:27 +01:00
sstone
f75706df9d PeerHandler: fix tcp framing issue 2017-02-09 11:44:27 +01:00
dpad85
958e39d4e7 Select the payment request when it is generated 2017-02-08 19:29:57 +01:00
dpad85
729fdf7df5 Ignored ENTER and TAB in send payment textarea 2017-02-08 19:26:17 +01:00
dpad85
1c18a5b819 Updated tray icon wording (removed alias) 2017-02-08 18:46:32 +01:00
dpad85
cdd0f59e9b Added channel local params to gui controller 2017-02-08 18:37:34 +01:00
dpad85
f1227f25a9 Display a system notification when payment sent
* send payment handler can now handle the success/failure
* paymentlifecycle send failure reason in WAITING_FOR_PAYMENT_COMPLETE
2017-02-08 18:37:34 +01:00
dpad85
54d43f03c0 Added a System Tray Icon
* enables notifications (in handlers class)
* added menu to show/minimize app when right click on tray icon
2017-02-08 18:37:34 +01:00
dpad85
7df8e97aa4 Added root style 2017-02-08 18:37:34 +01:00
dpad85
5bcbf15a69 Added unit selection when receiving payment 2017-02-08 18:37:34 +01:00
pm47
ba515a921a relayer now extracts preimages from the blockchain 2017-02-07 15:12:31 +01:00
pm47
08281dc109 updated relayer actor name 2017-02-07 15:05:43 +01:00
pm47
80356cefdc updated logback_colors 2017-02-07 15:05:43 +01:00
Pierre-Marie Padiou
0d763e8551 close channel when an htlc times out in either commitment tx (#24) 2017-02-07 15:04:59 +01:00
sstone
dadc52b4c4 bolt 3: update tx test vectors to match the specs 2017-02-07 11:27:27 +01:00
sstone
85e94b97ec bolt3 test vectors: swap local and remote funding privkeys 2017-02-07 09:51:40 +01:00
dpad85
1858cdf01b Added context actions on nodes/channels lists 2017-02-06 16:38:14 +01:00
sstone
7810abf08a bolt3 test vectors: add tests
tests are copied from Rusty's PR
2017-02-05 22:28:53 +01:00
sstone
06a610a413 fix fee computations 2017-02-05 22:28:27 +01:00
dpad85
de282aea48 Added Export to DOT action in menu 2017-02-03 20:13:43 +01:00
dpad85
a2ceafed63 Added node1 & node2 in channels table; added color in nodes table 2017-02-03 20:13:37 +01:00
sstone
be9ebee471 BOLT 3: add test vector generation 2017-02-03 20:11:59 +01:00
pm47
5fb2b9a86b removed BufferedWriter and added a test 2017-02-03 19:52:11 +01:00
pm47
ea3480b9f8 dot graph are now exported as a string 2017-02-03 19:46:18 +01:00
pm47
3a3b8c6eb1 fixed bug when remote close during opening of the channel 2017-02-03 19:14:52 +01:00
pm47
1b74f1c6cb moved funding tx creation to Transactions 2017-02-03 18:24:38 +01:00
pm47
6d28879ebb added dot generation in router 2017-02-03 18:18:31 +01:00
pm47
d182d9db6e watch for network channel close and properly announce events 2017-02-03 18:18:30 +01:00
sstone
64561a6fc0 fix transport handler test 2017-02-03 17:46:33 +01:00
sstone
365476c5ae transport handler: do not stop when the connection is closed
When the connection to the peer is closed, send an error to the channel
and wait. when the channel stops, the corresponding transport handler will
stop too.
2017-02-03 17:25:31 +01:00
sstone
e3ac554bd7 add an isTransactionOuputSpendable method to our bitcoin client 2017-02-03 17:25:31 +01:00
dpad85
b9d031dea2 Added 2 tabs to monitor network nodes and channels
* Using NodeDiscovered, NodeLost, ChannelDiscovered and ChannelLost events
  to update node and channel tables
2017-02-03 15:44:47 +01:00
sstone
85f96e5cf6 update dependencies 2017-02-03 13:55:25 +01:00
sstone
1435fb4257 fix htlc timeout/success tx
output should be local-delayedkey, not local-key
2017-02-03 13:06:39 +01:00
sstone
8b275cfd5c htlc success/timeout tx: input sequence should be 0 2017-02-03 13:06:39 +01:00
pm47
92d297b3d9 announcement are now signed, better handling of relayed htlcs 2017-02-03 11:55:06 +01:00
pm47
a33f2dc703 Merge branch 'wip-bolts' of github.com:ACINQ/eclair into wip-bolts 2017-02-02 17:58:59 +01:00
dpad85
de52502da7 Using Long for amount in CreatePayment 2017-02-02 17:41:20 +01:00
dpad85
71c555e469 Added amount max validation in sending payment window 2017-02-02 17:29:31 +01:00
dpad85
47f28855c5 Max Funding is 2^24, max Push mSat is 1000 * 2^24 2017-02-02 16:43:57 +01:00
dpad85
c3772268c9 Received amount must no be greater than 4 294 967 295 msat 2017-02-02 16:39:52 +01:00
pm47
015e56456f improved transport-level serializer 2017-02-02 16:26:16 +01:00
dpad85
2d7d9a7528 Increased font size of error/description messages 2017-02-02 16:09:49 +01:00
dpad85
9955f7cdbc Added working links in About, and changed wording
* also removed obsolete modal.css
2017-02-02 15:14:29 +01:00
pm47
f7c558871c now sending network events (Node/Channel Lost not yet implemented) 2017-02-02 15:01:41 +01:00
pm47
7a8420c170 added error handlers to NORMAL/SHUTDOWN/NEGOTIATION states 2017-02-02 14:24:14 +01:00
pm47
f325d2a3f8 renamed *Testkit.scala -> *StateSpec.scala 2017-02-02 14:17:40 +01:00
dpad85
f62e7553bb Fixed selected tab style 2017-02-02 13:22:54 +01:00
dpad85
15445125b4 Added details on splash error message 2017-02-02 13:22:26 +01:00
dpad85
f39ac0bdff Updated readme screenshot 2017-02-01 19:47:50 +01:00
dpad85
4fcee8ee07 Updated gui to a more standard design
* flat styles are centralised in themes/flat.css, not used for now
2017-02-01 19:20:35 +01:00
pm47
5efa71eb5d Merge branch 'wip-bolt7' into wip-bolts 2017-02-01 19:13:23 +01:00
pm47
b7bd2c80bf re-enabled payment relay 2017-02-01 19:12:50 +01:00
pm47
3fad3fd012 Blob -> Bob 2017-02-01 19:12:49 +01:00
dpad85
bece607b8f Added validation rules in GUI
* opening channel: 0 < funding <= 0.1 BTC
* receive payment: 0 < amount <= 0.042 BTC
2017-01-31 18:53:11 +01:00
dpad85
827ea63abf Increased context separator padding 2017-01-31 16:56:47 +01:00
dpad85
625bf64800 Made channels and form data selectable in GUI
* Using TextField instead of Label to make text selectable
* Improved channel's labels behaviour when resizing
2017-01-31 16:40:36 +01:00
pm47
5eec190bf6 Merge branch 'wip-bolt7' of github.com:ACINQ/eclair into wip-bolt7 2017-01-31 16:24:36 +01:00
pm47
85c52b2efa removed old protobuf dependency and related module 2017-01-31 16:23:35 +01:00
pm47
c02c17b6b0 re-enabled payment state machine and tests 2017-01-31 15:52:49 +01:00
pm47
287bb60d94 build the onion 2017-01-31 11:25:47 +01:00
sstone
055d64122e BOLT 3 test vectors: remove old test vectors 2017-01-30 19:41:12 +01:00
sstone
686789b389 Merge branch 'wip-bolts' of https://github.com/ACINQ/eclair into wip-bolts 2017-01-30 18:28:52 +01:00
sstone
0bfec544ba BOLT3: use local-key instead of local-delayed-key in HTLC outputs 2017-01-30 18:28:06 +01:00
dpad85
cae7eb0274 Updated logo in readme 2017-01-30 17:12:24 +01:00
dpad85
d04b7de90a Added description in receive payment modal 2017-01-30 16:51:39 +01:00
dpad85
b553cc27d2 Added border to all buttons
* default colors is background color
* keeps buttons' height consistent even if border is not visible
2017-01-30 16:42:11 +01:00
dpad85
fd47c7cfb5 Removed min height condition on main window 2017-01-30 16:34:42 +01:00
dpad85
521a0ed117 Merged channel pane context in one 2017-01-30 16:22:33 +01:00
dpad85
dda0c0c4f2 Improved handling of node id context in main window 2017-01-30 16:02:42 +01:00
dpad85
167efe3cb9 Added channels count in Tab label 2017-01-30 15:27:40 +01:00
dpad85
e38cbcaba1 Increased GUI main window preferred height 2017-01-30 15:07:17 +01:00
pm47
1a0ab56d66 merged from wip-bolts 2017-01-30 14:56:31 +01:00
dpad85
de99fdb566 Added pushMsat param when connecting to a channel
* pushMsat is an optional field in millisatoshi, default is 0
* In channel creation, `amount` parameter has been renamed to fundingSatoshis
  to make code more readable
* Added pushMsat field in open channel modal GUI
* Added description label in open channel modal to improve user experience
2017-01-30 14:47:59 +01:00
Pierre-Marie Padiou
0d4a117f94 fixed typo in assert message 2017-01-30 14:21:25 +01:00
sstone
2ca7e72f44 BOLT 3: use P2WPKH instead of P2PKH
see https://github.com/lightningnetwork/lightning-rfc/pull/94
2017-01-30 11:38:08 +01:00
pm47
2d2ecdabf2 improved logging in router 2017-01-27 20:28:38 +01:00
pm47
69b82990e9 re-enabled route computation, next step build the onion 2017-01-27 20:22:42 +01:00
pm47
9dbddf748d removed irc dependency 2017-01-27 20:22:41 +01:00
pm47
37b00f708c small scaladoc fixes 2017-01-27 20:22:41 +01:00
pm47
2d45f6aea6 removed unused protobuf types 2017-01-27 20:22:41 +01:00
pm47
488b854a2a routing announcements are transmitted as soon as connection is open 2017-01-27 20:22:41 +01:00
pm47
5df27783da fixed typo in log 2017-01-27 20:22:41 +01:00
dpad85
b03d3a71e2 Added Copy URI in context menu 2017-01-27 19:47:32 +01:00
sstone
a914b88bc1 replace assert with require 2017-01-27 19:42:50 +01:00
dpad85
9996af4e61 Modified buildCopyContext to handle multiple copy actions
* Added a utility CopyAction class to describe the action
2017-01-27 19:32:56 +01:00
dpad85
ba8cff0b26 Added a label parameter to buildCopyContext 2017-01-27 19:03:18 +01:00
dpad85
d5dae98c2e Wording 2017-01-27 18:37:51 +01:00
dpad85
5ab892ca97 Fixed width of open channel modal 2017-01-27 18:36:44 +01:00
dpad85
67c518db7c FXML namespace version set to 8 2017-01-27 18:13:45 +01:00
dpad85
6d387f6ea6 Removed unused imports 2017-01-27 18:02:35 +01:00
pm47
ec07cc46ab implemented basic announcement broadcast 2017-01-27 17:28:02 +01:00
pm47
6d16f315fe updated node announcement message format 2017-01-27 17:23:54 +01:00
sstone
b3e2258aff TransportHandler: process all incoming messages asap
This fixes an issue where, if a received packet contains the last handshake message plus
encrypted payloads, we would not process the payloads immediately
2017-01-27 16:30:37 +01:00
sstone
a38989e40f remove unused code 2017-01-27 16:30:20 +01:00
sstone
50e3145371 gui: new node url format pubkey@host:port 2017-01-26 17:05:08 +01:00
sstone
81072bfe9f bitcoin client: add support for short tx id (heigt + index) 2017-01-26 17:03:05 +01:00
sstone
7f68e67efb update tests because to-remote is a P2PKH output 2017-01-25 14:41:07 +01:00
sstone
9eb6bdc2f9 fix bolt3 test vectors
- to-remote is a P2PKH output
- sign transactions
2017-01-25 14:28:32 +01:00
sstone
637f4767e6 commit tx: to-remote is a P2PKH output (not a P2WPKH output) 2017-01-25 14:00:27 +01:00
sstone
966f0f2e0b bolt 3 test vectors: label private keys properly, and display the associated public keys 2017-01-25 11:22:01 +01:00
sstone
96e0d8f88b in fee-per-kw, kilo is 1000 not 1024
see 18a083b628
2017-01-25 11:19:41 +01:00
sstone
08f1c893ff minor fixes 2017-01-23 15:53:35 +01:00
sstone
b83cf24e09 improve TransportHandler 2017-01-23 15:46:33 +01:00
pm47
ef586fe4ad now checking final scriptpubkey validity 2017-01-20 19:03:35 +01:00
pm47
2badd3d7c2 closing fees are now properly estimated 2017-01-20 18:41:34 +01:00
pm47
4d1264d130 optimized imports 2017-01-20 18:02:57 +01:00
Fabrice Drouin
85a8ec5867 use improved private/public key types from bitcoin-lib 0.9.9 (#23)
* use bitcoin-lib 0.9.9

* use scalar/point/public key types instead of binary data

* minor fix: scalar are encoded on 32 bytes

* fix number encoding error
numbers between 0 and 16 were not encoded properly in redeem scripts

* removed some useless calls to constructors
2017-01-20 17:47:45 +01:00
pm47
373df9cdd0 removed Publish, now we always use PublishAsap 2017-01-20 14:09:34 +01:00
pm47
8a29a7cd02 reduced 'expectNoMsg' delay from 3s to 1s in tests 2017-01-20 13:26:11 +01:00
pm47
e4edd7b596 main outputs are now sent to finalPubkeyScript in all cases 2017-01-20 13:23:43 +01:00
pm47
bcc5cb8f7b renamed ClaimHtlcDelayedTx to ClaimDelayedOutputTx 2017-01-19 17:30:20 +01:00
pm47
22789d9395 added logs and slightly changed signature of claimRevokedRemoteCommitTxOutputs 2017-01-19 17:02:07 +01:00
pm47
a58befd1ed minor: removed useless statements 2017-01-19 16:42:34 +01:00
pm47
fbe2867001 re-enabled isFunder label in GUI 2017-01-19 16:00:29 +01:00
pm47
bba07d0881 fixed htlc workflow (relay still disabled), re-enabled autosign interval 2017-01-19 16:00:29 +01:00
pm47
8763258a5b set TransportHandler log level to INFO 2017-01-19 16:00:29 +01:00
pm47
1c8bae6336 minor: fixed comments 2017-01-19 16:00:29 +01:00
pm47
11935151a2 using a different seed for alice and bob in tests 2017-01-19 16:00:28 +01:00
pm47
b4cb612f4a final adress now pays back to bitcoind 2017-01-19 16:00:28 +01:00
sstone
a12f7068c8 commitment index should be a Long (48 bits), not an Int 2017-01-18 18:58:20 +01:00
sstone
b333de0d03 fix compilation warnings 2017-01-18 18:42:38 +01:00
pm47
692e47f3ea optimized imports 2017-01-18 14:53:36 +01:00
pm47
b5b7f37ab7 Merge branch 'wip-bolt3' into wip-bolts 2017-01-18 13:04:58 +01:00
pm47
3cc3438037 fixed c8eaf6f5ee to make it consistent with existing code 2017-01-18 12:31:10 +01:00
pm47
b5ae50b316 updated tests as per c8eaf6f5ee 2017-01-17 19:25:43 +01:00
pm47
6d4e8d1547 re-enabled travis tests 2017-01-17 19:09:06 +01:00
pm47
e1dbdb70e7 removed useless tests, re-enabled rustytests 2017-01-17 19:09:06 +01:00
pm47
6fe60c73f6 re-enabled tests on CLOSING state 2017-01-17 19:09:06 +01:00
sstone
09c6832359 Merge branch 'wip-bolt3' of https://github.com/ACINQ/eclair into wip-bolt3 2017-01-17 17:36:25 +01:00
pm47
d938920ca4 re-enabled tests on NEGOTIATING state 2017-01-17 17:31:14 +01:00
sstone
c8eaf6f5ee add fee calculation for 'htlc delayed' and 'main penalty' tx 2017-01-17 17:31:00 +01:00
pm47
8d04c414ce re-enabled tests on SHUTDOWN state 2017-01-17 15:49:09 +01:00
pm47
261e0b936c added 'spendability' checks to uniclose tests 2017-01-17 14:46:55 +01:00
pm47
34dd7e20c8 added handling of local current commit 2017-01-17 11:58:40 +01:00
pm47
4002365d4e added (limited) handling of remote revoked commit 2017-01-16 17:23:13 +01:00
pm47
98be1efb39 added encoding/decoding of txnumber in sequence+locktime 2017-01-16 15:30:09 +01:00
pm47
af30b268ee added handling of remote current commit 2017-01-16 15:30:09 +01:00
pm47
26767054fb workaround in PeerWatcher so that WatchConfirmed is not triggered multiple times in tests 2017-01-16 15:30:08 +01:00
pm47
3ddefdb124 added CMD_CLOSE/Shutdown tests to NORMAL 2017-01-16 15:30:08 +01:00
sstone
e04a7d47c3 add key derivation test vectors 2017-01-16 15:02:41 +01:00
pm47
d5566a485f enabled back tests on NORMAL->Closing 2017-01-06 20:02:01 +01:00
pm47
3ae139934b fixed UpdateFailHtlc tests 2017-01-06 17:53:04 +01:00
pm47
f96f261902 added more tests on RevokeAndAck 2017-01-06 17:30:29 +01:00
pm47
d0980cb315 added CommitSig tests with multiple htlcs 2017-01-06 16:58:09 +01:00
pm47
8e85d4ab63 now check signature of htlc-success txs 2017-01-06 14:54:12 +01:00
pm47
14a71b01e3 now using a non-zero pushMsat by default in tests 2017-01-06 11:45:01 +01:00
pm47
df4e9b57cc fixed CommitSig test (htlc amount) 2017-01-05 18:22:11 +01:00
pm47
2fa70e2000 merged 2017-01-03 18:04:24 +01:00
pm47
c5260e8414 fixed fee issue 2017-01-03 17:59:51 +01:00
sstone
ec0920143c bolt3: implement "obscured commit tx number" 2017-01-03 14:54:01 +01:00
pm47
e1773d464b added an isFunder attribute 2017-01-03 12:01:10 +01:00
pm47
eca496e041 added tx fees (wip) 2016-12-21 14:18:15 +01:00
pm47
91f5d7d87e using MilliSatoshi class 2016-12-20 14:46:55 +01:00
pm47
83bd8d6f3c implemented signature (wip) 2016-12-19 19:26:31 +01:00
sstone
1bf264bbc5 bolt 3: add fee computation example 2016-12-19 18:11:17 +01:00
pm47
e1ae6d2a2e updated bitcoin-lib to 0.9.8 2016-12-19 16:15:50 +01:00
pm47
4fb26c6c89 removed Common/Input/Output scripts subsections 2016-12-19 12:39:25 +01:00
pm47
8447270445 replaced TxTemplate by TransactionWithInputInfo 2016-12-16 19:25:11 +01:00
pm47
d334fe068e first step towards htlc tx sigs (wip) 2016-12-14 11:42:09 +01:00
pm47
2a666919b6 first shot at generating success/timeout templates 2016-12-13 18:55:03 +01:00
pm47
2cf61dd95a added htlc output templates 2016-12-13 17:08:04 +01:00
pm47
1e97d4e0a6 moved OldScripts into Scripts 2016-12-13 14:23:11 +01:00
pm47
b05f52b412 Merge branch 'wip-bolt2' into wip-bolts 2016-12-13 14:15:23 +01:00
pm47
2a95767089 fixed NodeAnnouncement.alias encoding and added tests 2016-12-13 13:37:39 +01:00
pm47
0581449929 added routing-related messages 2016-12-12 19:38:52 +01:00
pm47
28818d9e5e fixed some NORMAL state tests 2016-12-12 13:47:25 +01:00
pm47
76afa1a4bb making TestBitcoinClient send BITCOIN_FUNDING_DEPTHOK 2016-12-12 11:40:25 +01:00
pm47
eef13e369c fixed tests covering OPEN->NORMAL 2016-12-09 17:08:16 +01:00
pm47
0e40569a7d disabled travis tests 2016-12-08 19:39:42 +01:00
pm47
e8f0749864 replaced BinaryData by Point/Scalar 2016-12-08 19:38:07 +01:00
pm47
f5b770448a fixed WaitForAcceptChannel and WaitForOpenChannel tests 2016-12-08 18:02:29 +01:00
pm47
f213f79821 merged from wip-bolt3 2016-12-08 17:36:23 +01:00
pm47
4665ce8d4b completed rewiring of OPEN->NORMAL 2016-12-08 14:57:50 +01:00
pm47
4e1980b41f working on channel opening 2016-12-08 11:12:36 +01:00
pm47
f7183968eb removed all parameters 2016-12-07 18:28:22 +01:00
sstone
fa9096655b add local/remote/delayed key derivation function 2016-12-07 15:39:15 +01:00
pm47
a8cb35f744 updated wire messages 2016-12-07 14:34:49 +01:00
pm47
046cd9d788 moved Scripts to OldScripts 2016-12-07 14:07:13 +01:00
pm47
3bfe7292c4 project-wide auto-reformatting 2016-12-07 12:52:59 +01:00
pm47
81739cb0e6 removed unused functions from Helpers 2016-12-07 12:49:28 +01:00
pm47
84650f8147 separated Commitments, CommitmentSpec and TxTemplate 2016-12-07 12:27:40 +01:00
pm47
5f7a2718ef removed Change type, separated TxTemplate from Channel 2016-12-07 11:11:16 +01:00
pm47
6e21988156 removed unused states 2016-12-06 18:22:07 +01:00
pm47
21c5b44b80 renamed tests so that they match states 2016-12-06 17:47:24 +01:00
pm47
91d913d442 removed all protobuf messages but auth & routing 2016-12-06 17:37:48 +01:00
pm47
ca59994481 project now compiles (still wip!) 2016-12-05 16:22:06 +01:00
sstone
49ec2e8aca bolt 3: add Rusty's shachain tests 2016-12-05 16:14:32 +01:00
pm47
a274867136 added init and error messages 2016-12-05 12:10:24 +01:00
pm47
ed56401333 using 2 bytes to code message type 2016-12-05 11:59:55 +01:00
pm47
e182afd6b7 wip 2016-12-02 15:08:59 +01:00
sstone
9225d5a796 use bip69 ordering for tx inputs/outputs 2016-11-30 17:21:50 +01:00
sstone
21d90905a0 fix labels in bolt3 tests 2016-11-29 14:11:58 +01:00
sstone
4e29d2b9fe implement revocation key derivation 2016-11-28 18:39:13 +01:00
sstone
1fe8b8feb2 compute tx base size, total size and weight 2016-11-28 17:32:45 +01:00
sstone
7ff9e4cb19 bolt 3 tests: fix amounts
commit tx total output was wrong
2016-11-25 15:34:58 +01:00
pm47
6d63ba8dc4 added firstPerCommitmentPoint to OpenChannel 2016-11-25 10:54:13 +01:00
sstone
ae60464663 bolt3 tests: print tx size 2016-11-24 18:03:07 +01:00
pm47
6d3c6eda41 wip (does not work yet!) 2016-11-24 17:55:05 +01:00
pm47
a3ab3a7857 updated types and added htlc-related messages 2016-11-24 11:54:19 +01:00
sstone
b767edad10 use CHECK(MULTI)SIG instead of CHECK(MULTI)SIGVERIFY
it makes the witness scripts nicer
2016-11-23 18:11:20 +01:00
sstone
67e7124b46 bolt 3: implement missing use cases 2016-11-23 18:02:12 +01:00
sstone
84cf1a9c94 dump tx hex (so they can be tested on regtest) 2016-11-22 17:48:38 +01:00
sstone
d09286631d implement commit tx, HTLC success tx and HTLC timeout tx 2016-11-22 14:47:55 +01:00
sstone
6387afe2a7 (very) partial implementation of bolt 3 2016-11-21 18:58:28 +01:00
sstone
e8b7e2e780 update pom.xml (use bitcoin-lib 0.9.7) 2016-11-21 18:56:43 +01:00
pm47
a8eca868a2 renamed types file to Types.scala and removed bitcoin-lib-style serialization 2016-11-17 13:48:22 +01:00
pm47
cc11ec8d14 moved wire from a module to a package 2016-11-17 13:12:28 +01:00
pm47
0a5bf62aae added channel messages 2016-11-16 17:59:57 +01:00
Pierre-Marie Padiou
e57704c688 added warning about wip status 2016-11-16 15:14:34 +01:00
pm47
0b20b134af implemented OpenChannel message 2016-11-16 14:43:09 +01:00
pm47
23a1749d9a merged from master 2016-11-15 17:22:27 +01:00
Pierre-Marie Padiou
0ad95746d5 added example command line with jvm args (see #21) 2016-11-14 17:19:29 +01:00
pm47
810fe1fc99 better handle bad bitcoind rpc credentials (see #21) 2016-11-14 17:07:53 +01:00
pm47
3fc25da461 now using akka-http-json and akka-http-client (removed acinq-tools dependency) 2016-11-08 19:32:01 +01:00
pm47
6cd0198fc3 better manage initialization errors 2016-11-08 14:33:53 +01:00
pm47
bf57cb338b use default receive method 2016-11-08 14:33:06 +01:00
pm47
f69f9ea74d fixed typo 2016-11-08 14:32:35 +01:00
pm47
ab1229432c formatting 2016-11-08 14:19:27 +01:00
pm47
cbe7d46442 bumped acinq-tools version to 1.3 2016-11-08 11:32:22 +01:00
pm47
e6973e7d36 moved payment-related stuff to a dedicated package 2016-11-08 11:32:10 +01:00
pm47
71c082bc7c PaymentSpawner->PaymentInitiator PaymentManager->PaymentLifecycle 2016-11-08 11:10:42 +01:00
pm47
f70eba79fb fixed imports in GUIUpdater 2016-11-03 16:25:52 +01:00
pm47
ce3892f654 fixed unit bug 2016-11-03 16:25:10 +01:00
pm47
2d85118615 now using a *fake* bitcoin client (and cleaned up real bitcoin client) 2016-11-03 16:00:47 +01:00
pm47
529d55150f temporary disabled SecureRandom (linux bug) 2016-11-03 15:42:07 +01:00
pm47
8c05b270c1 added license to about 2016-11-03 15:37:25 +01:00
dpad85
d6514e87ca Updated README; fixed urls, added API section 2016-10-18 16:37:32 +02:00
dpad85
be445595b7 Updated README
* Added logo image

* Enriched overview section (demo screenshot)

* Added Development Setup section
2016-10-18 16:23:31 +02:00
dpad85
8845be794a Balance and capacity are now in bits (mBTC) by default 2016-10-18 14:13:11 +02:00
dpad85
e5195460c0 Fixed dynamic length of node id in status bar 2016-10-18 14:11:43 +02:00
pm47
fae0663c45 first commit 2016-10-17 17:39:42 +02:00
dpad85
cff5ed2d3f Moved eclair setup out of fx thread 2016-10-17 16:25:23 +02:00
dpad85
2dd76c3b7b Removed bg color in tcp/http 2016-10-17 16:23:26 +02:00
dpad85
f0d55c36be Condensed channel display 2016-10-17 16:22:39 +02:00
dpad85
e6a262a4b0 Improved look of error message in splash window 2016-10-17 16:21:16 +02:00
pm47
e12e5316cf merged from master 2016-10-17 14:23:40 +02:00
pm47
29b6df7bed Merge branch 'master' into wip-gui 2016-10-17 14:10:49 +02:00
pm47
cc066369f1 removed initial_amount_* in CommitmentSpec 2016-10-17 13:59:55 +02:00
pm47
ac3ec81578 changed default port to 9735 2016-10-17 13:59:02 +02:00
sstone
2d3b500778 travis: disable interop test 2016-10-17 13:49:11 +02:00
dpad85
a4971723c6 Added missing close image 2016-10-17 13:28:39 +02:00
dpad85
4d7bc214c6 WIP Graphic User Interface
* Moved all interface code into FXML/CSS files (resources/gui)

* Moved all logic into controllers

* Added Validators to forms (open/send/receive)

* Added a context menu action (right click) on some labels (node id,
channel id) to copy the value in clipboard

* Added an 'About' modal window

* Reworked the look of the UI

* Added a blur animation to splash window
2016-09-28 19:41:14 +02:00
337 changed files with 29366 additions and 13113 deletions

4
.gitignore vendored
View File

@ -25,7 +25,3 @@ target/
project/target
DeleteMe*.*
*~
result.txt
*.gv
*.dot

BIN
.readme/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
.readme/screen-1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

View File

@ -2,9 +2,11 @@ sudo: required
dist: trusty
language: scala
scala:
- 2.11.8
- 2.11.11
env:
- export LD_LIBRARY_PATH=/usr/local/lib
script:
- mvn install
- mvn install
jdk:
- oraclejdk8
notifications:

21
BUILD.md Normal file
View File

@ -0,0 +1,21 @@
# Building Eclair
## Requirements
- [Java Development Kit](http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html) 1.8
- [Maven](https://maven.apache.org/download.cgi) 3.3.x
- [Inno Setup](http://www.jrsoftware.org/isdl.php) 5.5.9 (optional, if you want to generate the windows installer)
## Build
To build the project, simply run:
```shell
$ mvn package
```
To skip the tests, run:
```shell
$ mvn package -DskipTests
```
To generate the windows installer along with the build, run the following command:
```shell
$ mvn package -DskipTests -Pinstaller
```
The generated installer will be located in `eclair-node-gui/target/jfx/installer`

167
README.md
View File

@ -1,82 +1,145 @@
![Eclair Logo](.readme/logo.png)
[![Build Status](https://travis-ci.org/ACINQ/eclair.svg?branch=master)](https://travis-ci.org/ACINQ/eclair)
[![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](LICENSE)
[![Gitter chat](https://img.shields.io/badge/chat-on%20gitter-rose.svg)](https://gitter.im/ACINQ/eclair)
# eclair
**Eclair** (french for Lightning) is a scala implementation of the Lightning Network. It can run with or without a GUI, and a JSON-RPC API is also available.
A scala implementation of the Lightning Network. Eclair is french for Lightning.
This software follows the [Lightning Network Specifications (BOLTs)](https://github.com/lightningnetwork/lightning-rfc). Other implementations include [lightning-c], [lit], and [lnd].
---
:construction: Both the BOLTs and Eclair itself are a work in progress. Expect things to break/change!
:warning: Eclair currently only runs on regtest or testnet. We recommend testing in regtest, as it allows you to generate blocks manually and not wait for confirmations.
:rotating_light: We had reports of Eclair being tested on various segwit-enabled blockchains. Keep in mind that Eclair is still alpha quality software, by using it with actual coins you are putting your funds at risk!
This software follows the [BOLT specifications](https://github.com/rustyrussell/lightning-rfc), therefore it is compatible with Blockstream's [lightning-c](https://github.com/ElementsProject/lightning).
---
## Lightning Network Specification Compliance
Please see the latest [release note](https://github.com/ACINQ/eclair/releases) for detailed information on BOLT compliance.
## Overview
The general idea is to have an actor per channel, everything being non-blocking.
A "blockchain watcher" is responsible for monitoring the blockchain, and sending events (eg. when the anchor is spent).
![Eclair Demo](.readme/screen-1.png)
## Modules
* lightning-types: scala code generation using protobuf's compiler (wire protocol)
* eclair-node: actual implementation
## Installation
## Usage
:warning: **Those are valid for the most up-to-date, unreleased, version of eclair. Here are the [instructions for Eclair 0.2-alpha5](https://github.com/ACINQ/eclair/blob/v0.2-alpha5/README.md#installation)**.
Prerequisites:
- A JRE or JDK depending on wether you want to compile yourself or not (preferably > 1.8)
- A running bitcoin-demo (testnet or regtest)
### Configuring Bitcoin Core
:warning: **eclair currently runs on segnet only. Do not try and modify it to run on bitcoin mainnet!**
Eclair needs a _synchronized_, _segwit-ready_, **_zeromq-enabled_**, _non-pruning_, _tx-indexing_ [Bitcoin Core](https://github.com/bitcoin/bitcoin) node. This means that on Windows you will need Bitcoin Core 0.14+.
Either run from source:
Run bitcoind with the following minimal `bitcoin.conf`:
```
mvn exec:java -Dexec.mainClass=fr.acinq.eclair.Boot
```
Or grab the latest released jar and run:
```
java -jar eclair-core_2.11-*-capsule-fat.jar
regtest=1
server=1
rpcuser=XXX
rpcpassword=XXX
txindex=1
zmqpubrawblock=tcp://127.0.0.1:29000
zmqpubrawtx=tcp://127.0.0.1:29000
```
*See [TESTING.md](TESTING.md) for more details on how to use this software.*
### Installing Eclair
Available jvm options (see `application.conf` for full reference):
The released binaries can be downloaded [here](https://github.com/ACINQ/eclair/releases).
#### Windows
Just use the windows installer, it should create a shortcut on your desktop.
#### Linux, macOS or manual install on Windows
You need to first install java, more precisely a [JRE 1.8](http://www.oracle.com/technetwork/java/javase/downloads/jre8-downloads-2133155.html).
:warning: If you are using the OpenJDK JRE, you will need to build OpenJFX yourself, or run the application in headless mode (see below).
Then download the latest fat jar and depending on whether or not you want a GUI run the following command:
* with GUI:
```shell
java -jar eclair-node-gui-<version>-<commit_id>.jar
```
eclair.server.port (default: 45000)
eclair.http.port (default: 8080)
eclair.bitcoind.rpcuser (default: foo)
eclair.bitcoind.rpcpassword (default: bar)
* without GUI:
```shell
java -jar eclair-node-<version>-<commit_id>.jar
```
### Configuring Eclair
#### Configuration file
Eclair reads its configuration file, and write its logs, to a `datadir` directory, located in `~/.eclair` by default.
To change your node's configuration, create a file named `eclair.conf` in `datadir`. Here's an example configuration file:
```
eclair.server.port=9735
eclair.node-alias=eclair
eclair.node-color=49daaa
```
Here are some of the most common options:
name | description | default value
-----------------------------|---------------------------|--------------
eclair.server.port | Lightning TCP port | 9735
eclair.api.port | API HTTP port | 8080
eclair.bitcoind.rpcuser | Bitcoin Core RPC user | foo
eclair.bitcoind.rpcpassword | Bitcoin Core RPC password | bar
eclair.bitcoind.zmq | Bitcoin Core ZMQ address | tcp://127.0.0.1:29000
Quotes are not required unless the value contains special characters. Full syntax guide [here](https://github.com/lightbend/config/blob/master/HOCON.md).
&rarr; see [`reference.conf`](eclair-core/src/main/resources/reference.conf) for full reference. There are many more options!
#### Java Environment Variables
Some advanced parameters can be changed with java environment variables. Most users won't need this and can skip this section.
:warning: Using separate `datadir` is mandatory if you want to run **several instances of eclair** on the same machine. You will also have to change ports in eclair.conf (see above).
name | description | default value
----------------------|--------------------------------------------|--------------
eclair.datadir | Path to the data directory | ~/.eclair
eclair.headless | Run eclair without a GUI |
eclair.printToConsole | Log to stdout (in addition to eclair.log) |
For example, to specify a different data directory you would run the following command:
```shell
java -Declair.datadir=/tmp/node1 -jar eclair-node-gui-<version>-<commit_id>.jar
```
## JSON-RPC API
method | params | description
-------------|-------------------------------------|-----------------------------------------------------------
connect | host, port, anchor_amount | opens a channel with another eclair or lightningd instance
list | | lists existing channels
addhtlc | channel_id, amount, rhash, locktime | sends an htlc
fulfillhtlc | channel_id, r | fulfills an htlc
close | channel_id | closes a channel
help | | displays available methods
## Status
- [X] Network
- [X] Routing (simple IRC prototype)
- [X] Channel protocol
- [X] HTLC Scripts
- [X] Unilateral close handling
- [X] Relaying Payment
- [ ] Fee management
- [X] Blockchain watcher
- [ ] Storing states in a database
method | params | description
-------------|-----------------------------------------------|-----------------------------------------------------------
getinfo | | return basic node information (id, chain hash, current block height)
connect | nodeId, host, port | connect to another lightning node through a secure connection
open | nodeId, host, port, fundingSatoshis, pushMsat | opens a channel with another lightning node
peers | | list existing local peers
channels | | list existing local channels
channel | channelId | retrieve detailed information about a given channel
allnodes | | list all known nodes
allchannels | | list all known channels
receive | amountMsat, description | generate a payment request for a given amount
send | amountMsat, paymentHash, nodeId | send a payment to a lightning node
send | paymentRequest | send a payment to a lightning node using a BOLT11 payment request
send | paymentRequest, amountMsat | send a payment to a lightning node using a BOLT11 payment request and a custom amount
close | channelId | close a channel
close | channelId, scriptPubKey (optional) | close a channel and send the funds to the given scriptPubKey
help | | display available methods
## Resources
- [1] [The Bitcoin Lightning Network: Scalable Off-Chain Instant Payments](https://lightning.network/lightning-network-paper.pdf) by Joseph Poon and Thaddeus Dryja
- [2] [Reaching The Ground With Lightning](https://github.com/ElementsProject/lightning/raw/master/doc/deployable-lightning.pdf) by Rusty Russell
## Other implementations
Name | Language | Compatible
-------------|----------|------------
[Amiko-Pay] | Python | no
[lightning-c]| C | yes
[lnd] | Go | no
[Thunder] | Java | no
[Amiko-Pay]: https://github.com/cornwarecjp/amiko-pay
[lightning-c]: https://github.com/ElementsProject/lightning
[lnd]: https://github.com/LightningNetwork/lnd
[lit]: https://github.com/mit-dci/lit
[Thunder]: https://github.com/blockchain/thunder

View File

@ -1,120 +0,0 @@
# Testing eclair and lightningd
## Configure bitcoind to run in regtest mode
Important: you need a segwit version of bitcoin core for this test (see https://github.com/sipa/bitcoin/tree/segwit-master).
Make sure that bitcoin-cli is on the path and edit ~/.bitcoin/bitcoin.conf and add:
```shell
server=1
regtest=1
rpcuser=***
rpcpassword=***
```
To check that segwit is enabled run:
```shell
bitcoin-cli getblockchaininfo
```
and check bip9_softforks:
```
...
"bip9_softforks": {
"csv": {
"status": "active",
"startTime": 0,
"timeout": 999999999999
},
"witness": {
"status": "active",
"startTime": 0,
"timeout": 999999999999
}
}
```
## Start bitcoind
Mine enough blocks to activate segwit blocks:
```shell
bitcoin-cli generate 500
```
##
Start lightningd (here well use port 46000)
```shell
lightningd --port 46000
```
##
Start eclair:
```shell
mvn exec:java -Dexec.mainClass=fr.acinq.eclair.Boot
```
## Tell eclair to connect to lightningd
```shell
curl -X POST -H "Content-Type: application/json" -d '{
"method": "connect",
"params" : [ "localhost", 46000, 3000000 ]
}' http://localhost:8080
```
Since eclair is funder, it will create and publish the anchor tx
Mine a few blocks to confirm the anchor tx:
```shell
bitcoin-cli generate 10
```
eclair and lightningd are now both in NORMAL state.
You can check this by running:
```shell
lightning-cli getpeers
```
or
```shell
curl -X POST -H "Content-Type: application/json" -d '{
"method": "list",
"params" : [ ]
}' http://localhost:8080
```
## Tell eclair to send a htlc
Well use the following values for R and H:
```
R = 0102030405060708010203040506070801020304050607080102030405060708
H = 8cf3e5f40cf025a984d8e00b307bbab2b520c91b2bde6fa86958f8f4e7d8a609
```
Youll need a unix timestamp that is not too far into the future. Now + 100000 is fine:
```shell
curl -X POST -H "Content-Type: application/json" -d "{
\"method\": \"addhtlc\",
\"params\" : [ 70000000, \"8cf3e5f40cf025a984d8e00b307bbab2b520c91b2bde6fa86958f8f4e7d8a609\", $((`date +%s` + 100000)), \"021acf75c92318d3723098294d2a6a4b08d9abba2ebb5f2df2b4a8e9153e96a5f4\" ]
}" http://localhost:8080
```
## Tell eclair to commit its changes
```shell
curl -X POST -H "Content-Type: application/json" -d "{
\"method\": \"sign\",
\"params\" : [ \"d3f056a084e266ad06ea1ca28a1e080ca07c6b61fac7ce116e48a5c31d688eee\" ]
}" http://localhost:8080
```
## Tell lightningd to fulfill the HTLC:
```shell
./lightning-cli fulfillhtlc 03befb4f8ad1d87d4c41acbb316791fe157f305caf2123c848f448975aaf85c1bb 0102030405060708010203040506070801020304050607080102030405060708
```
Check balances on both eclair and lightningd
## Close the channel
```shell
./lightning-cli close 03befb4f8ad1d87d4c41acbb316791fe157f305caf2123c848f448975aaf85c1bb
```
Mine a few blocks to bury the closing tx
```shell
bitcoin-cli generate 10
```
The channel is now in CLOSED state

42
eclair-core/eclair-cli Normal file
View File

@ -0,0 +1,42 @@
#!/bin/bash
[ -z "$1" ] && (
echo "usage: "
echo " eclair-cli help"
) && exit 1
URL="http://localhost:8080"
CURL_OPTS="-sS -X POST -H \"Content-Type: application/json\""
case $1 in
"help")
eval curl "$CURL_OPTS -d '{ \"method\": \"help\", \"params\" : [] }' $URL" | jq -r ".result[]"
;;
"getinfo")
eval curl "$CURL_OPTS -d '{ \"method\": \"getinfo\", \"params\" : [] }' $URL" | jq ".result"
;;
"channels")
eval curl "$CURL_OPTS -d '{ \"method\": \"channels\", \"params\" : [] }' $URL" | jq ".result[]"
;;
"channel")
eval curl "$CURL_OPTS -d '{ \"method\": \"channel\", \"params\" : [\"${2?"missing channel id"}\"] }' $URL" | jq ".result | { nodeid, channelId, state, balanceMsat: .data.commitments.localCommit.spec.toLocalMsat, capacitySat: .data.commitments.commitInput.txOut.amount.amount }"
;;
"open")
eval curl "$CURL_OPTS -d '{ \"method\": \"open\", \"params\" : [\"${2?"missing node id"}\", \"${3?"missing ip"}\", ${4?"missing port"}, ${5?"missing amount (sat)"}, ${6?"missing push amount (msat)"}] }' $URL" | jq -r "if .error == null then .result else .error.message end"
;;
"close")
eval curl "$CURL_OPTS -d '{ \"method\": \"close\", \"params\" : [\"${2?"missing channel id"}\"] }' $URL"
;;
"receive")
eval curl "$CURL_OPTS -d '{ \"method\": \"receive\", \"params\" : [${2?"missing amount"}, \"something\"] }' $URL" | jq -r "if .error == null then .result else .error.message end"
;;
"send")
eval curl "$CURL_OPTS -d '{ \"method\": \"send\", \"params\" : [\"${2?"missing request"}\"] }' $URL" | jq -r "if .error == null then .result else .error.message end"
;;
"network")
eval curl "$CURL_OPTS -d '{ \"method\": \"network\", \"params\" : [] }' $URL" | jq ".result"
;;
"peers")
eval curl "$CURL_OPTS -d '{ \"method\": \"peers\", \"params\" : [] }' $URL" | jq ".result"
;;
esac

216
eclair-core/pom.xml Normal file
View File

@ -0,0 +1,216 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>fr.acinq.eclair</groupId>
<artifactId>eclair_2.11</artifactId>
<version>0.2-SNAPSHOT</version>
</parent>
<artifactId>eclair-core_2.11</artifactId>
<packaging>jar</packaging>
<name>${project.artifactId}</name>
<build>
<plugins>
<plugin>
<groupId>pl.project13.maven</groupId>
<artifactId>git-commit-id-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>revision</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>com.googlecode.maven-download-plugin</groupId>
<artifactId>download-maven-plugin</artifactId>
<version>1.3.0</version>
<executions>
<execution>
<id>download-bitcoind</id>
<phase>generate-test-resources</phase>
<goals>
<goal>wget</goal>
</goals>
<configuration>
<url>${bitcoind.url}</url>
<unpack>true</unpack>
<outputDirectory>${project.build.directory}</outputDirectory>
<md5>${bitcoind.md5}</md5>
<sha1>${bitcoind.sha1}</sha1>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifest>
<addClasspath>true</addClasspath>
<addDefaultImplementationEntries>true</addDefaultImplementationEntries>
</manifest>
<manifestEntries>
<!-- we hide the git commit in the Specification-Version standard field-->
<Specification-Version>${git.commit.id}</Specification-Version>
<Url>${project.parent.url}</Url>
</manifestEntries>
</archive>
</configuration>
</plugin>
</plugins>
</build>
<profiles>
<profile>
<id>default</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<properties>
<bitcoind.url>https://bitcoin.org/bin/bitcoin-core-0.14.0/bitcoin-0.14.0-x86_64-linux-gnu.tar.gz
</bitcoind.url>
<bitcoind.md5>c811c157d4d618f7d7f4b9f24834551c</bitcoind.md5>
<bitcoind.sha1>3ab7e537bd00bf35e6a78fca108d0d886f8289c1</bitcoind.sha1>
</properties>
</profile>
<profile>
<id>Mac</id>
<activation>
<os>
<family>mac</family>
</os>
</activation>
<properties>
<bitcoind.url>https://bitcoin.org/bin/bitcoin-core-0.14.0/bitcoin-0.14.0-osx64.tar.gz
</bitcoind.url>
<bitcoind.md5>1521e1d0901169004b9c1c9b552868b7</bitcoind.md5>
<bitcoind.sha1>7216298f77162618f322fdf499f1f1b67a0048b7</bitcoind.sha1>
</properties>
</profile>
<profile>
<id>Windows</id>
<activation>
<os>
<family>Windows</family>
</os>
</activation>
<properties>
<bitcoind.url>https://bitcoin.org/bin/bitcoin-core-0.14.0/bitcoin-0.14.0-win64.zip</bitcoind.url>
<bitcoind.md5>e84bc3a81ad3d1776299419eb7a04935</bitcoind.md5>
<bitcoind.sha1>d2e64fcabf6f85d56d64a52c76e007b6defc32ef</bitcoind.sha1>
</properties>
</profile>
</profiles>
<dependencies>
<!-- AKKA -->
<dependency>
<groupId>com.typesafe.akka</groupId>
<artifactId>akka-actor_${scala.version.short}</artifactId>
<version>${akka.version}</version>
</dependency>
<dependency>
<groupId>com.typesafe.akka</groupId>
<artifactId>akka-slf4j_${scala.version.short}</artifactId>
<version>${akka.version}</version>
</dependency>
<dependency>
<groupId>com.typesafe.akka</groupId>
<artifactId>akka-http-core_${scala.version.short}</artifactId>
<version>10.0.7</version>
</dependency>
<!-- JSON -->
<dependency>
<groupId>org.json4s</groupId>
<artifactId>json4s-jackson_${scala.version.short}</artifactId>
<version>3.5.2</version>
</dependency>
<dependency>
<groupId>de.heikoseeberger</groupId>
<artifactId>akka-http-json4s_${scala.version.short}</artifactId>
<version>1.16.1</version>
</dependency>
<!-- BITCOIN -->
<dependency>
<groupId>fr.acinq</groupId>
<artifactId>bitcoin-lib_${scala.version.short}</artifactId>
<version>${bitcoinlib.version}</version>
<exclusions>
<exclusion>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.zeromq</groupId>
<artifactId>jeromq</artifactId>
<version>0.4.0</version>
</dependency>
<dependency>
<groupId>fr.acinq</groupId>
<artifactId>bitcoinj-core</artifactId>
<version>${bitcoinj.version}</version>
</dependency>
<!-- SERIALIZATION -->
<dependency>
<groupId>org.scodec</groupId>
<artifactId>scodec-core_${scala.version.short}</artifactId>
<version>1.10.3</version>
</dependency>
<!-- LOGGING -->
<dependency>
<groupId>org.clapper</groupId>
<artifactId>grizzled-slf4j_${scala.version.short}</artifactId>
<version>1.3.1</version>
</dependency>
<!-- OTHER -->
<dependency>
<groupId>org.xerial</groupId>
<artifactId>sqlite-jdbc</artifactId>
<version>3.20.0</version>
</dependency>
<dependency>
<groupId>org.jgrapht</groupId>
<artifactId>jgrapht-core</artifactId>
<version>1.0.1</version>
</dependency>
<dependency>
<groupId>org.jgrapht</groupId>
<artifactId>jgrapht-ext</artifactId>
<version>1.0.1</version>
<exclusions>
<exclusion>
<groupId>org.tinyjee.jgraphx</groupId>
<artifactId>jgraphx</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<!-- This is to get rid of '[WARNING] warning: Class javax.annotation.Nonnull not found - continuing with a stub.' compile errors -->
<groupId>com.google.code.findbugs</groupId>
<artifactId>jsr305</artifactId>
<version>3.0.2</version>
</dependency>
<!-- TESTS -->
<dependency>
<groupId>com.typesafe.akka</groupId>
<artifactId>akka-testkit_${scala.version.short}</artifactId>
<version>${akka.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,10 @@
{
"127.0.0.1": {
"t": "51001",
"s": "51002"
},
"10.0.2.2": {
"t": "51001",
"s": "51002"
}
}

View File

@ -0,0 +1,14 @@
{
"testnetnode.arihanc.com": {
"t": "51001",
"s": "51002"
},
"testnet.hsmiths.com": {
"t": "53011",
"s": "53012"
},
"electrum.akinbo.org": {
"t": "51001",
"s": "51002"
}
}

View File

@ -0,0 +1,84 @@
eclair {
chain = "test" // "regtest" for regtest, "test" for testnet. Livenet is not supported.
server {
public-ips = [] // external ips, will be announced on the network
binding-ip = "0.0.0.0"
port = 9735
}
api {
binding-ip = "127.0.0.1"
port = 8080
}
watcher-type = "bitcoind" // other *experimental* values include "bitcoinj" or "electrum"
bitcoind {
host = "localhost"
rpcport = 18332
rpcuser = "foo"
rpcpassword = "bar"
zmq = "tcp://127.0.0.1:29000"
}
bitcoinj {
static-peers = [
#{ // currently used in integration tests to override default port
# host = "localhost"
# port = 28333
#}
]
}
default-feerates { // those are in satoshis per byte
delay-blocks {
1 = 210
2 = 180
6 = 150
12 = 110
36 = 50
72 = 20
}
}
node-alias = "eclair"
node-color = "49daaa"
global-features = ""
local-features = "08" // initial_routing_sync
channel-flags = 1 // announce channels
dust-limit-satoshis = 542
default-feerate-per-kb = 20000 // default bitcoin core value
max-htlc-value-in-flight-msat = 100000000000 // 1 BTC ~= unlimited
htlc-minimum-msat = 1000000
max-accepted-htlcs = 30
reserve-to-funding-ratio = 0.01 // recommended by BOLT #2
max-reserve-to-funding-ratio = 0.05 // channel reserve can't be more than 5% of the funding amount (recommended: 1%)
delay-blocks = 144
mindepth-blocks = 2
expiry-delta-blocks = 144
fee-base-msat = 546000
fee-proportional-millionth = 10
// maximum local vs remote feerate mismatch; 1.0 means 100%
// actual check is abs((local feerate - remote fee rate) / (local fee rate + remote fee rate)/2) > fee rate mismatch
max-feerate-mismatch = 1.5
// funder will send an UpdateFee message if the difference between current commitment fee and actual current network fee is greater
// than this ratio.
update-fee_min-diff-ratio = 0.1
channel-exclude-duration = 60 seconds // when a temporary channel failure is returned, we exclude the channel from our payment routes for this duration
router-broadcast-interval = 10 seconds // this should be 60 seconds on mainnet
router-validate-interval = 2 seconds // this should be high enough to have a decent level of parallelism
ping-interval = 30 seconds
auto-reconnect = true
payment-handler = "local"
}

View File

@ -0,0 +1,21 @@
package fr.acinq.eclair
import grizzled.slf4j.Logging
import scala.util.{Failure, Success, Try}
object DBCompatChecker extends Logging {
/**
* Tests if the DB files are compatible with the current version of eclair; throws an exception if incompatible.
*
* @param nodeParams
*/
def checkDBCompatibility(nodeParams: NodeParams): Unit =
Try(nodeParams.networkDb.listChannels() ++ nodeParams.networkDb.listNodes() ++ nodeParams.peersDb.listPeers() ++ nodeParams.channelsDb.listChannels()) match {
case Success(_) => {}
case Failure(_) => throw IncompatibleDBException
}
}
case object IncompatibleDBException extends RuntimeException("DB files are not compatible with this version of eclair.")

View File

@ -0,0 +1,26 @@
package fr.acinq.eclair
import akka.actor.{Actor, FSM}
import akka.event.{DiagnosticLoggingAdapter, LoggingAdapter}
/**
* A version of akka.actor.DiagnosticActorLogging compatible with an FSM
* See https://groups.google.com/forum/#!topic/akka-user/0CxR8CImr4Q
*/
trait FSMDiagnosticActorLogging[S, D] extends FSM[S, D] {
import akka.event.Logging._
val diagLog: DiagnosticLoggingAdapter = akka.event.Logging(this)
def mdc(currentMessage: Any): MDC = emptyMDC
override def log: LoggingAdapter = diagLog
override def aroundReceive(receive: Actor.Receive, msg: Any): Unit = try {
diagLog.mdc(mdc(msg))
super.aroundReceive(receive, msg)
} finally {
diagLog.clearMDC()
}
}

View File

@ -0,0 +1,48 @@
package fr.acinq.eclair
import java.util.BitSet
import fr.acinq.bitcoin.BinaryData
/**
* Created by PM on 13/02/2017.
*/
object Features {
// reserved but not used as per lightningnetwork/lightning-rfc/pull/178
val INITIAL_ROUTING_SYNC_BIT_MANDATORY = 2
val INITIAL_ROUTING_SYNC_BIT_OPTIONAL = 3
/**
*
* @param features feature bits
* @return true if an initial dump of the routing table is requested
*/
def initialRoutingSync(features: BitSet): Boolean = features.get(INITIAL_ROUTING_SYNC_BIT_OPTIONAL)
/**
*
* @param features feature bits
* @return true if an initial dump of the routing table is requested
*/
def initialRoutingSync(features: BinaryData): Boolean = initialRoutingSync(BitSet.valueOf(features.reverse.toArray))
/**
* Check that the features that we understand are correctly specified, and that there are no mandatory features that
* we don't understand (even bits)
*/
def areSupported(bitset: BitSet): Boolean = {
// for now there is no mandatory feature bit, so we don't support features with any even bit set
for (i <- 0 until bitset.length() by 2) {
if (bitset.get(i)) return false
}
return true
}
/**
* A feature set is supported if all even bits are supported.
* We just ignore unknown odd bits.
*/
def areSupported(features: BinaryData): Boolean = areSupported(BitSet.valueOf(features.reverse.toArray))
}

View File

@ -0,0 +1,32 @@
package fr.acinq.eclair
import java.util.concurrent.atomic.{AtomicLong, AtomicReference}
import fr.acinq.eclair.blockchain.fee.{FeeratesPerByte, FeeratesPerKw}
/**
* Created by PM on 25/01/2016.
*/
object Globals {
/**
* This counter holds the current blockchain height.
* It is mainly used to calculate htlc expiries.
* The value is read by all actors, hence it needs to be thread-safe.
*/
val blockCount = new AtomicLong(0)
/**
* This holds the current feerates, in satoshi-per-bytes.
* The value is read by all actors, hence it needs to be thread-safe.
*/
val feeratesPerByte = new AtomicReference[FeeratesPerByte](null)
/**
* This holds the current feerates, in satoshi-per-kw.
* The value is read by all actors, hence it needs to be thread-safe.
*/
val feeratesPerKw = new AtomicReference[FeeratesPerKw](null)
}

View File

@ -0,0 +1,152 @@
package fr.acinq.eclair
import java.io.File
import java.net.InetSocketAddress
import java.nio.file.Files
import java.sql.DriverManager
import java.util.concurrent.TimeUnit
import com.typesafe.config.{Config, ConfigFactory}
import fr.acinq.bitcoin.Crypto.PrivateKey
import fr.acinq.bitcoin.DeterministicWallet.ExtendedPrivateKey
import fr.acinq.bitcoin.{BinaryData, Block, DeterministicWallet}
import fr.acinq.eclair.NodeParams.WatcherType
import fr.acinq.eclair.db._
import fr.acinq.eclair.db.sqlite.{SqliteChannelsDb, SqliteNetworkDb, SqlitePeersDb, SqlitePreimagesDb}
import scala.collection.JavaConversions._
import scala.concurrent.duration.FiniteDuration
/**
* Created by PM on 26/02/2017.
*/
case class NodeParams(extendedPrivateKey: ExtendedPrivateKey,
privateKey: PrivateKey,
alias: String,
color: (Byte, Byte, Byte),
publicAddresses: List[InetSocketAddress],
globalFeatures: BinaryData,
localFeatures: BinaryData,
dustLimitSatoshis: Long,
maxHtlcValueInFlightMsat: UInt64,
maxAcceptedHtlcs: Int,
expiryDeltaBlocks: Int,
htlcMinimumMsat: Int,
delayBlocks: Int,
minDepthBlocks: Int,
smartfeeNBlocks: Int,
feeBaseMsat: Int,
feeProportionalMillionth: Int,
reserveToFundingRatio: Double,
maxReserveToFundingRatio: Double,
channelsDb: ChannelsDb,
peersDb: PeersDb,
networkDb: NetworkDb,
preimagesDb: PreimagesDb,
routerBroadcastInterval: FiniteDuration,
routerValidateInterval: FiniteDuration,
pingInterval: FiniteDuration,
maxFeerateMismatch: Double,
updateFeeMinDiffRatio: Double,
autoReconnect: Boolean,
chainHash: BinaryData,
channelFlags: Byte,
channelExcludeDuration: FiniteDuration,
watcherType: WatcherType)
object NodeParams {
sealed trait WatcherType
object BITCOIND extends WatcherType
object BITCOINJ extends WatcherType
object ELECTRUM extends WatcherType
/**
* Order of precedence for the configuration parameters:
* 1) Java environment variables (-D...)
* 2) Configuration file eclair.conf
* 3) Optionally provided config
* 4) Default values in reference.conf
*/
def loadConfiguration(datadir: File, overrideDefaults: Config = ConfigFactory.empty()) =
ConfigFactory.parseProperties(System.getProperties)
.withFallback(ConfigFactory.parseFile(new File(datadir, "eclair.conf")))
.withFallback(overrideDefaults)
.withFallback(ConfigFactory.load()).getConfig("eclair")
def makeNodeParams(datadir: File, config: Config): NodeParams = {
datadir.mkdirs()
val seedPath = new File(datadir, "seed.dat")
val seed: BinaryData = seedPath.exists() match {
case true => Files.readAllBytes(seedPath.toPath)
case false =>
val seed = randomKey.toBin
Files.write(seedPath.toPath, seed)
seed
}
val master = DeterministicWallet.generate(seed)
val extendedPrivateKey = DeterministicWallet.derivePrivateKey(master, DeterministicWallet.hardened(46) :: DeterministicWallet.hardened(0) :: Nil)
val chain = config.getString("chain")
val chainHash = chain match {
case "test" => Block.TestnetGenesisBlock.hash
case "regtest" => Block.RegtestGenesisBlock.hash
case _ => throw new RuntimeException("only regtest and testnet are supported for now")
}
val sqlite = DriverManager.getConnection(s"jdbc:sqlite:${new File(datadir, "eclair.sqlite")}")
val channelsDb = new SqliteChannelsDb(sqlite)
val peersDb = new SqlitePeersDb(sqlite)
val networkDb = new SqliteNetworkDb(sqlite)
val preimagesDb = new SqlitePreimagesDb(sqlite)
val color = BinaryData(config.getString("node-color"))
require(color.size == 3, "color should be a 3-bytes hex buffer")
val watcherType = config.getString("watcher-type") match {
case "bitcoinj" => BITCOINJ
case "electrum" => ELECTRUM
case _ => BITCOIND
}
NodeParams(
extendedPrivateKey = extendedPrivateKey,
privateKey = extendedPrivateKey.privateKey,
alias = config.getString("node-alias").take(32),
color = (color.data(0), color.data(1), color.data(2)),
publicAddresses = config.getStringList("server.public-ips").toList.map(ip => new InetSocketAddress(ip, config.getInt("server.port"))),
globalFeatures = BinaryData(config.getString("global-features")),
localFeatures = BinaryData(config.getString("local-features")),
dustLimitSatoshis = config.getLong("dust-limit-satoshis"),
maxHtlcValueInFlightMsat = UInt64(config.getLong("max-htlc-value-in-flight-msat")),
maxAcceptedHtlcs = config.getInt("max-accepted-htlcs"),
expiryDeltaBlocks = config.getInt("expiry-delta-blocks"),
htlcMinimumMsat = config.getInt("htlc-minimum-msat"),
delayBlocks = config.getInt("delay-blocks"),
minDepthBlocks = config.getInt("mindepth-blocks"),
smartfeeNBlocks = 3,
feeBaseMsat = config.getInt("fee-base-msat"),
feeProportionalMillionth = config.getInt("fee-proportional-millionth"),
reserveToFundingRatio = config.getDouble("reserve-to-funding-ratio"),
maxReserveToFundingRatio = config.getDouble("max-reserve-to-funding-ratio"),
channelsDb = channelsDb,
peersDb = peersDb,
networkDb = networkDb,
preimagesDb = preimagesDb,
routerBroadcastInterval = FiniteDuration(config.getDuration("router-broadcast-interval").getSeconds, TimeUnit.SECONDS),
routerValidateInterval = FiniteDuration(config.getDuration("router-validate-interval").getSeconds, TimeUnit.SECONDS),
pingInterval = FiniteDuration(config.getDuration("ping-interval").getSeconds, TimeUnit.SECONDS),
maxFeerateMismatch = config.getDouble("max-feerate-mismatch"),
updateFeeMinDiffRatio = config.getDouble("update-fee_min-diff-ratio"),
autoReconnect = config.getBoolean("auto-reconnect"),
chainHash = chainHash,
channelFlags = config.getInt("channel-flags").toByte,
channelExcludeDuration = FiniteDuration(config.getDuration("channel-exclude-duration").getSeconds, TimeUnit.SECONDS),
watcherType = watcherType)
}
}

View File

@ -0,0 +1,26 @@
package fr.acinq.eclair
import java.net.{InetAddress, ServerSocket}
import scala.util.{Failure, Success, Try}
object PortChecker {
/**
* Tests if a port is open
* See https://stackoverflow.com/questions/434718/sockets-discover-port-availability-using-java#435579
*
* @return
*/
def checkAvailable(host: String, port: Int): Unit = {
Try(new ServerSocket(port, 50, InetAddress.getByName(host))) match {
case Success(socket) =>
Try(socket.close())
case Failure(_) =>
throw TCPBindException(port)
}
}
}
case class TCPBindException(port: Int) extends RuntimeException

View File

@ -0,0 +1,215 @@
package fr.acinq.eclair
import java.io.File
import java.net.InetSocketAddress
import akka.actor.{ActorRef, ActorSystem, Props, SupervisorStrategy}
import akka.http.scaladsl.Http
import akka.pattern.after
import akka.stream.{ActorMaterializer, BindFailedException}
import akka.util.Timeout
import com.typesafe.config.{Config, ConfigFactory}
import fr.acinq.bitcoin.{BinaryData, Block}
import fr.acinq.eclair.NodeParams.{BITCOIND, BITCOINJ, ELECTRUM}
import fr.acinq.eclair.api.{GetInfoResponse, Service}
import fr.acinq.eclair.blockchain.bitcoind.rpc.{BitcoinJsonRPCClient, ExtendedBitcoinClient}
import fr.acinq.eclair.blockchain.bitcoind.zmq.ZMQActor
import fr.acinq.eclair.blockchain.bitcoind.{BitcoinCoreWallet, ZmqWatcher}
import fr.acinq.eclair.blockchain.bitcoinj.{BitcoinjKit, BitcoinjWallet, BitcoinjWatcher}
import fr.acinq.eclair.blockchain.electrum.{ElectrumClient, ElectrumEclairWallet, ElectrumWallet, ElectrumWatcher}
import fr.acinq.eclair.blockchain.fee.{ConstantFeeProvider, _}
import fr.acinq.eclair.blockchain.{EclairWallet, _}
import fr.acinq.eclair.channel.Register
import fr.acinq.eclair.io.{Server, Switchboard}
import fr.acinq.eclair.payment._
import fr.acinq.eclair.router._
import grizzled.slf4j.Logging
import scala.collection.JavaConversions._
import scala.concurrent.duration._
import scala.concurrent.{Await, ExecutionContext, Future, Promise}
/**
* Created by PM on 25/01/2016.
*/
class Setup(datadir: File, overrideDefaults: Config = ConfigFactory.empty(), actorSystem: ActorSystem = ActorSystem()) extends Logging {
logger.info(s"hello!")
logger.info(s"version=${getClass.getPackage.getImplementationVersion} commit=${getClass.getPackage.getSpecificationVersion}")
val config = NodeParams.loadConfiguration(datadir, overrideDefaults)
val nodeParams = NodeParams.makeNodeParams(datadir, config)
val chain = config.getString("chain")
// early checks
DBCompatChecker.checkDBCompatibility(nodeParams)
PortChecker.checkAvailable(config.getString("server.binding-ip"), config.getInt("server.port"))
logger.info(s"nodeid=${nodeParams.privateKey.publicKey.toBin} alias=${nodeParams.alias}")
logger.info(s"using chain=$chain chainHash=${nodeParams.chainHash}")
logger.info(s"initializing secure random generator")
// this will force the secure random instance to initialize itself right now, making sure it doesn't hang later (see comment in package.scala)
secureRandom.nextInt()
implicit val system = actorSystem
implicit val materializer = ActorMaterializer()
implicit val timeout = Timeout(30 seconds)
implicit val formats = org.json4s.DefaultFormats
implicit val ec = ExecutionContext.Implicits.global
val bitcoin = nodeParams.watcherType match {
case BITCOIND =>
val bitcoinClient = new ExtendedBitcoinClient(new BitcoinJsonRPCClient(
user = config.getString("bitcoind.rpcuser"),
password = config.getString("bitcoind.rpcpassword"),
host = config.getString("bitcoind.host"),
port = config.getInt("bitcoind.rpcport")))
val future = for {
json <- bitcoinClient.rpcClient.invoke("getblockchaininfo").recover { case _ => throw BitcoinRPCConnectionException }
progress = (json \ "verificationprogress").extract[Double]
chainHash <- bitcoinClient.rpcClient.invoke("getblockhash", 0).map(_.extract[String]).map(BinaryData(_)).map(x => BinaryData(x.reverse))
bitcoinVersion <- bitcoinClient.rpcClient.invoke("getnetworkinfo").map(json => (json \ "version")).map(_.extract[String])
} yield (progress, chainHash, bitcoinVersion)
// blocking sanity checks
val (progress, chainHash, bitcoinVersion) = Await.result(future, 10 seconds)
assert(chainHash == nodeParams.chainHash, s"chainHash mismatch (conf=${nodeParams.chainHash} != bitcoind=$chainHash)")
assert(progress > 0.99, "bitcoind should be synchronized")
// TODO: add a check on bitcoin version?
Bitcoind(bitcoinClient)
case BITCOINJ =>
logger.warn("EXPERIMENTAL BITCOINJ MODE ENABLED!!!")
val staticPeers = config.getConfigList("bitcoinj.static-peers").map(c => new InetSocketAddress(c.getString("host"), c.getInt("port"))).toList
logger.info(s"using staticPeers=$staticPeers")
val bitcoinjKit = new BitcoinjKit(chain, datadir, staticPeers)
bitcoinjKit.startAsync()
Await.ready(bitcoinjKit.initialized, 10 seconds)
Bitcoinj(bitcoinjKit)
case ELECTRUM =>
logger.warn("EXPERIMENTAL ELECTRUM MODE ENABLED!!!")
val addressesFile = chain match {
case "test" => "/electrum/servers_testnet.json"
case "regtest" => "/electrum/servers_regtest.json"
}
val stream = classOf[Setup].getResourceAsStream(addressesFile)
val addresses = ElectrumClient.readServerAddresses(stream)
val electrumClient = system.actorOf(SimpleSupervisor.props(Props(new ElectrumClient(addresses)), "electrum-client", SupervisorStrategy.Resume))
Electrum(electrumClient)
}
def bootstrap: Future[Kit] = {
val zmqConnected = Promise[Boolean]()
val tcpBound = Promise[Unit]()
val defaultFeerates = FeeratesPerByte(block_1 = config.getLong("default-feerates.delay-blocks.1"), blocks_2 = config.getLong("default-feerates.delay-blocks.2"), blocks_6 = config.getLong("default-feerates.delay-blocks.6"), blocks_12 = config.getLong("default-feerates.delay-blocks.12"), blocks_36 = config.getLong("default-feerates.delay-blocks.36"), blocks_72 = config.getLong("default-feerates.delay-blocks.72"))
Globals.feeratesPerByte.set(defaultFeerates)
Globals.feeratesPerKw.set(FeeratesPerKw(defaultFeerates))
logger.info(s"initial feeratesPerByte=${Globals.feeratesPerByte.get()}")
val feeProvider = (chain, bitcoin) match {
case ("regtest", _) => new ConstantFeeProvider(defaultFeerates)
case (_, Bitcoind(client)) => new FallbackFeeProvider(new EarnDotComFeeProvider() :: new BitcoinCoreFeeProvider(client.rpcClient, defaultFeerates) :: new ConstantFeeProvider(defaultFeerates) :: Nil) // order matters!
case _ => new FallbackFeeProvider(new EarnDotComFeeProvider() :: new ConstantFeeProvider(defaultFeerates) :: Nil) // order matters!
}
system.scheduler.schedule(0 seconds, 10 minutes)(feeProvider.getFeerates.map {
case feerates: FeeratesPerByte =>
Globals.feeratesPerByte.set(feerates)
Globals.feeratesPerKw.set(FeeratesPerKw(defaultFeerates))
system.eventStream.publish(CurrentFeerates(Globals.feeratesPerKw.get))
logger.info(s"current feeratesPerByte=${Globals.feeratesPerByte.get()}")
})
val watcher = bitcoin match {
case Bitcoind(bitcoinClient) =>
system.actorOf(SimpleSupervisor.props(Props(new ZMQActor(config.getString("bitcoind.zmq"), Some(zmqConnected))), "zmq", SupervisorStrategy.Restart))
system.actorOf(SimpleSupervisor.props(ZmqWatcher.props(bitcoinClient), "watcher", SupervisorStrategy.Resume))
case Bitcoinj(bitcoinj) =>
zmqConnected.success(true)
system.actorOf(SimpleSupervisor.props(BitcoinjWatcher.props(bitcoinj), "watcher", SupervisorStrategy.Resume))
case Electrum(electrumClient) =>
zmqConnected.success(true)
system.actorOf(SimpleSupervisor.props(Props(new ElectrumWatcher(electrumClient)), "watcher", SupervisorStrategy.Resume))
}
val wallet = bitcoin match {
case Bitcoind(bitcoinClient) => new BitcoinCoreWallet(bitcoinClient.rpcClient, watcher)
case Bitcoinj(bitcoinj) => new BitcoinjWallet(bitcoinj.initialized.map(_ => bitcoinj.wallet()))
case Electrum(electrumClient) =>
val electrumSeedPath = new File(datadir, "electrum_seed.dat")
val electrumWallet = system.actorOf(ElectrumWallet.props(electrumSeedPath, electrumClient, ElectrumWallet.WalletParameters(Block.RegtestGenesisBlock.hash, allowSpendUnconfirmed = true)), "electrum-wallet")
new ElectrumEclairWallet(electrumWallet)
}
wallet.getFinalAddress.map {
case address => logger.info(s"initial wallet address=$address")
}
val paymentHandler = system.actorOf(SimpleSupervisor.props(config.getString("payment-handler") match {
case "local" => LocalPaymentHandler.props(nodeParams)
case "noop" => Props[NoopPaymentHandler]
}, "payment-handler", SupervisorStrategy.Resume))
val register = system.actorOf(SimpleSupervisor.props(Props(new Register), "register", SupervisorStrategy.Resume))
val relayer = system.actorOf(SimpleSupervisor.props(Relayer.props(nodeParams, register, paymentHandler), "relayer", SupervisorStrategy.Resume))
val router = system.actorOf(SimpleSupervisor.props(Router.props(nodeParams, watcher), "router", SupervisorStrategy.Resume))
val switchboard = system.actorOf(SimpleSupervisor.props(Switchboard.props(nodeParams, watcher, router, relayer, wallet), "switchboard", SupervisorStrategy.Resume))
val paymentInitiator = system.actorOf(SimpleSupervisor.props(PaymentInitiator.props(nodeParams.privateKey.publicKey, router, register), "payment-initiator", SupervisorStrategy.Restart))
val server = system.actorOf(SimpleSupervisor.props(Server.props(nodeParams, switchboard, new InetSocketAddress(config.getString("server.binding-ip"), config.getInt("server.port")), Some(tcpBound)), "server", SupervisorStrategy.Restart))
val kit = Kit(
nodeParams = nodeParams,
system = system,
watcher = watcher,
paymentHandler = paymentHandler,
register = register,
relayer = relayer,
router = router,
switchboard = switchboard,
paymentInitiator = paymentInitiator,
server = server,
wallet = wallet)
val api = new Service {
override def getInfoResponse: Future[GetInfoResponse] = Future.successful(GetInfoResponse(nodeId = nodeParams.privateKey.publicKey, alias = nodeParams.alias, port = config.getInt("server.port"), chainHash = nodeParams.chainHash, blockHeight = Globals.blockCount.intValue()))
override def appKit = kit
}
val httpBound = Http().bindAndHandle(api.route, config.getString("api.binding-ip"), config.getInt("api.port")).recover {
case _: BindFailedException => throw TCPBindException(config.getInt("api.port"))
}
val zmqTimeout = after(5 seconds, using = system.scheduler)(Future.failed(BitcoinZMQConnectionTimeoutException))
val tcpTimeout = after(5 seconds, using = system.scheduler)(Future.failed(TCPBindException(config.getInt("server.port"))))
val httpTimeout = after(5 seconds, using = system.scheduler)(Future.failed(TCPBindException(config.getInt("api.port"))))
for {
_ <- Future.firstCompletedOf(zmqConnected.future :: zmqTimeout :: Nil)
_ <- Future.firstCompletedOf(tcpBound.future :: tcpTimeout :: Nil)
_ <- Future.firstCompletedOf(httpBound :: httpTimeout :: Nil)
} yield kit
}
}
// @formatter:off
sealed trait Bitcoin
case class Bitcoind(extendedBitcoinClient: ExtendedBitcoinClient) extends Bitcoin
case class Bitcoinj(bitcoinjKit: BitcoinjKit) extends Bitcoin
case class Electrum(electrumClient: ActorRef) extends Bitcoin
// @formatter:on
case class Kit(nodeParams: NodeParams,
system: ActorSystem,
watcher: ActorRef,
paymentHandler: ActorRef,
register: ActorRef,
relayer: ActorRef,
router: ActorRef,
switchboard: ActorRef,
paymentInitiator: ActorRef,
server: ActorRef,
wallet: EclairWallet)
case object BitcoinZMQConnectionTimeoutException extends RuntimeException("could not connect to bitcoind using zeromq")
case object BitcoinRPCConnectionException extends RuntimeException("could not connect to bitcoind using json-rpc")

View File

@ -0,0 +1,29 @@
package fr.acinq.eclair
import akka.actor.{Actor, ActorLogging, OneForOneStrategy, Props, SupervisorStrategy}
import scala.concurrent.duration._
/**
* This supervisor will supervise a single child actor using the provided SupervisorStrategy
* All incoming messages will be forwarded to the child actor.
*
* Created by PM on 17/03/2017.
*/
class SimpleSupervisor(childProps: Props, childName: String, strategy: SupervisorStrategy.Directive) extends Actor with ActorLogging {
val child = context.actorOf(childProps, childName)
override def receive: Receive = {
case msg => child forward msg
}
// we allow at most <maxNrOfRetries> within <withinTimeRange>, otherwise the child actor is not restarted (this avoids restart loops)
override val supervisorStrategy = OneForOneStrategy(loggingEnabled = true, maxNrOfRetries = 100, withinTimeRange = 1 minute) { case _ => strategy }
}
object SimpleSupervisor {
def props(childProps: Props, childName: String, strategy: SupervisorStrategy.Directive) = Props(new SimpleSupervisor(childProps, childName, strategy))
}

View File

@ -0,0 +1,35 @@
package fr.acinq.eclair
import java.math.BigInteger
import fr.acinq.bitcoin.BinaryData
case class UInt64(underlying: BigInt) extends Ordered[UInt64] {
require(underlying >= 0, s"uint64 must be positive (actual=$underlying)")
require(underlying <= UInt64.MaxValueBigInt, s"uint64 must be < 2^64 -1 (actual=$underlying)")
override def compare(o: UInt64): Int = underlying.compare(o.underlying)
override def toString: String = underlying.toString
}
object UInt64 {
private val MaxValueBigInt = BigInt(new BigInteger("ffffffffffffffff", 16))
val MaxValue = UInt64(MaxValueBigInt)
def apply(bin: BinaryData) = new UInt64(new BigInteger(1, bin))
def apply(value: Long) = new UInt64(BigInt(value))
object Conversions {
implicit def intToUint64(l: Int) = UInt64(l)
implicit def longToUint64(l: Long) = UInt64(l)
}
}

View File

@ -0,0 +1,76 @@
package fr.acinq.eclair.api
import fr.acinq.bitcoin.Crypto.{Point, PrivateKey, PublicKey, Scalar}
import fr.acinq.bitcoin.{BinaryData, Transaction}
import fr.acinq.eclair.channel.State
import fr.acinq.eclair.crypto.ShaChain
import fr.acinq.eclair.transactions.Transactions.TransactionWithInputInfo
import org.json4s.CustomSerializer
import org.json4s.JsonAST.{JNull, JString}
/**
* Created by PM on 28/01/2016.
*/
class BinaryDataSerializer extends CustomSerializer[BinaryData](format => ( {
case JString(hex) if (false) => // NOT IMPLEMENTED
???
}, {
case x: BinaryData => JString(x.toString())
}
))
class StateSerializer extends CustomSerializer[State](format => ( {
case JString(x) if (false) => // NOT IMPLEMENTED
???
}, {
case x: State => JString(x.toString())
}
))
class ShaChainSerializer extends CustomSerializer[ShaChain](format => ( {
case JString(x) if (false) => // NOT IMPLEMENTED
???
}, {
case x: ShaChain => JNull
}
))
class PublicKeySerializer extends CustomSerializer[PublicKey](format => ( {
case JString(x) if (false) => // NOT IMPLEMENTED
???
}, {
case x: PublicKey => JString(x.toString())
}
))
class PrivateKeySerializer extends CustomSerializer[PrivateKey](format => ( {
case JString(x) if (false) => // NOT IMPLEMENTED
???
}, {
case x: PrivateKey => JString("XXX")
}
))
class PointSerializer extends CustomSerializer[Point](format => ( {
case JString(x) if (false) => // NOT IMPLEMENTED
???
}, {
case x: Point => JString(x.toString())
}
))
class ScalarSerializer extends CustomSerializer[Scalar](format => ( {
case JString(x) if (false) => // NOT IMPLEMENTED
???
}, {
case x: Scalar => JString("XXX")
}
))
class TransactionWithInputInfoSerializer extends CustomSerializer[TransactionWithInputInfo](format => ( {
case JString(x) if (false) => // NOT IMPLEMENTED
???
}, {
case x: TransactionWithInputInfo => JString(Transaction.write(x.tx).toString())
}
))

View File

@ -0,0 +1,148 @@
package fr.acinq.eclair.api
import java.net.InetSocketAddress
import akka.actor.ActorRef
import akka.http.scaladsl.model.HttpMethods._
import akka.http.scaladsl.model.StatusCodes
import akka.http.scaladsl.model.headers.CacheDirectives.{`max-age`, `no-store`, public}
import akka.http.scaladsl.model.headers.HttpOriginRange.*
import akka.http.scaladsl.model.headers._
import akka.http.scaladsl.server.Directives._
import akka.pattern.ask
import akka.util.Timeout
import de.heikoseeberger.akkahttpjson4s.Json4sSupport
import de.heikoseeberger.akkahttpjson4s.Json4sSupport.ShouldWritePretty
import fr.acinq.bitcoin.Crypto.PublicKey
import fr.acinq.bitcoin.{BinaryData, MilliSatoshi, Satoshi}
import fr.acinq.eclair.Kit
import fr.acinq.eclair.channel._
import fr.acinq.eclair.io.Switchboard.{NewChannel, NewConnection}
import fr.acinq.eclair.payment.{PaymentRequest, PaymentResult, ReceivePayment, SendPayment}
import fr.acinq.eclair.wire.{ChannelAnnouncement, NodeAnnouncement}
import grizzled.slf4j.Logging
import org.json4s.JsonAST.{JInt, JString}
import org.json4s.{JValue, jackson}
import scala.concurrent.duration._
import scala.concurrent.{ExecutionContext, Future}
import scala.util.{Failure, Success}
/**
* Created by PM on 25/01/2016.
*/
// @formatter:off
case class JsonRPCBody(jsonrpc: String = "1.0", id: String = "scala-client", method: String, params: Seq[JValue])
case class Error(code: Int, message: String)
case class JsonRPCRes(result: AnyRef, error: Option[Error], id: String)
case class Status(node_id: String)
case class GetInfoResponse(nodeId: PublicKey, alias: String, port: Int, chainHash: BinaryData, blockHeight: Int)
case class ChannelInfo(shortChannelId: String, nodeId1: PublicKey , nodeId2: PublicKey)
// @formatter:on
trait Service extends Logging {
implicit def ec: ExecutionContext = ExecutionContext.Implicits.global
implicit val serialization = jackson.Serialization
implicit val formats = org.json4s.DefaultFormats + new BinaryDataSerializer + new StateSerializer + new ShaChainSerializer + new PublicKeySerializer + new PrivateKeySerializer + new ScalarSerializer + new PointSerializer + new TransactionWithInputInfoSerializer
implicit val timeout = Timeout(30 seconds)
implicit val shouldWritePretty: ShouldWritePretty = ShouldWritePretty.True
import Json4sSupport.{marshaller, unmarshaller}
def appKit: Kit
def getInfoResponse: Future[GetInfoResponse]
val customHeaders = `Access-Control-Allow-Origin`(*) ::
`Access-Control-Allow-Headers`("Content-Type, Authorization") ::
`Access-Control-Allow-Methods`(PUT, GET, POST, DELETE, OPTIONS) ::
`Cache-Control`(public, `no-store`, `max-age`(0)) ::
`Access-Control-Allow-Headers`("x-requested-with") :: Nil
def getChannel(channelId: String): Future[ActorRef] =
for {
channels <- (appKit.register ? 'channels).mapTo[Map[BinaryData, ActorRef]]
} yield channels.get(BinaryData(channelId)).getOrElse(throw new RuntimeException("unknown channel"))
val route =
respondWithDefaultHeaders(customHeaders) {
pathSingleSlash {
post {
entity(as[JsonRPCBody]) {
req =>
val kit = appKit
import kit._
val f_res: Future[AnyRef] = req match {
case JsonRPCBody(_, _, "getinfo", _) => getInfoResponse
case JsonRPCBody(_, _, "connect", JString(nodeId) :: JString(host) :: JInt(port) :: Nil) =>
(switchboard ? NewConnection(PublicKey(nodeId), new InetSocketAddress(host, port.toInt), None)).mapTo[String]
case JsonRPCBody(_, _, "open", JString(nodeId) :: JString(host) :: JInt(port) :: JInt(fundingSatoshi) :: JInt(pushMsat) :: options) =>
val channelFlags = options match {
case JInt(value) :: Nil => Some(value.toByte)
case _ => None // TODO: too lax?
}
(switchboard ? NewConnection(PublicKey(nodeId), new InetSocketAddress(host, port.toInt), Some(NewChannel(Satoshi(fundingSatoshi.toLong), MilliSatoshi(pushMsat.toLong), channelFlags)))).mapTo[String]
case JsonRPCBody(_, _, "peers", _) =>
(switchboard ? 'peers).mapTo[Map[PublicKey, ActorRef]].map(_.map(_._1.toBin))
case JsonRPCBody(_, _, "channels", _) =>
(register ? 'channels).mapTo[Map[Long, ActorRef]].map(_.keys)
case JsonRPCBody(_, _, "channel", JString(channelId) :: Nil) =>
getChannel(channelId).flatMap(_ ? CMD_GETINFO).mapTo[RES_GETINFO]
case JsonRPCBody(_, _, "allnodes", _) =>
(router ? 'nodes).mapTo[Iterable[NodeAnnouncement]].map(_.map(_.nodeId))
case JsonRPCBody(_, _, "allchannels", _) =>
(router ? 'channels).mapTo[Iterable[ChannelAnnouncement]].map(_.map(c => ChannelInfo(c.shortChannelId.toHexString, c.nodeId1, c.nodeId2)))
case JsonRPCBody(_, _, "receive", JInt(amountMsat) :: JString(description) :: Nil) =>
(paymentHandler ? ReceivePayment(MilliSatoshi(amountMsat.toLong), description)).mapTo[PaymentRequest].map(PaymentRequest.write)
case JsonRPCBody(_, _, "send", JInt(amountMsat) :: JString(paymentHash) :: JString(nodeId) :: Nil) =>
(paymentInitiator ? SendPayment(amountMsat.toLong, paymentHash, PublicKey(nodeId))).mapTo[PaymentResult]
case JsonRPCBody(_, _, "send", JString(paymentRequest) :: rest) =>
for {
req <- Future(PaymentRequest.read(paymentRequest))
amount = (req.amount, rest) match {
case (Some(_), JInt(amt) :: Nil) => amt.toLong // overriding payment request amount with the one provided
case (Some(amt), _) => amt.amount
case (None, JInt(amt) :: Nil) => amt.toLong // amount wasn't specified in request, using custom one
case (None, _) => throw new RuntimeException("you need to manually specify an amount for this payment request")
}
sendPayment = req.minFinalCltvExpiry match {
case None => SendPayment(amount, req.paymentHash, req.nodeId)
case Some(value) => SendPayment(amount, req.paymentHash, req.nodeId, value)
}
res <- (paymentInitiator ? sendPayment).mapTo[PaymentResult]
} yield res
case JsonRPCBody(_, _, "close", JString(channelId) :: JString(scriptPubKey) :: Nil) =>
getChannel(channelId).flatMap(_ ? CMD_CLOSE(scriptPubKey = Some(scriptPubKey))).mapTo[String]
case JsonRPCBody(_, _, "close", JString(channelId) :: Nil) =>
getChannel(channelId).flatMap(_ ? CMD_CLOSE(scriptPubKey = None)).mapTo[String]
case JsonRPCBody(_, _, "help", _) =>
Future.successful(List(
"connect (nodeId, host, port): connect to another lightning node through a secure connection",
"open (nodeId, host, port, fundingSatoshi, pushMsat, channelFlags = 0x01): open a channel with another lightning node",
"peers: list existing local peers",
"channels: list existing local channels",
"channel (channelId): retrieve detailed information about a given channel",
"allnodes: list all known nodes",
"allchannels: list all known channels",
"receive (amountMsat, description): generate a payment request for a given amount",
"send (amountMsat, paymentHash, nodeId): send a payment to a lightning node",
"send (paymentRequest): send a payment to a lightning node using a BOLT11 payment request",
"send (paymentRequest, amountMsat): send a payment to a lightning node using a BOLT11 payment request and a custom amount",
"close (channelId): close a channel",
"close (channelId, scriptPubKey): close a channel and send the funds to the given scriptPubKey",
"help: display this message"))
case _ => Future.failed(new RuntimeException("method not found"))
}
onComplete(f_res) {
case Success(res) => complete(JsonRPCRes(res, None, req.id))
case Failure(t) => complete(StatusCodes.InternalServerError, JsonRPCRes(null, Some(Error(-1, t.getMessage)), req.id))
}
}
}
}
}
}

View File

@ -1,6 +1,7 @@
package fr.acinq.eclair.blockchain.peer
package fr.acinq.eclair.blockchain
import fr.acinq.bitcoin.{Block, Transaction}
import fr.acinq.eclair.blockchain.fee.FeeratesPerKw
/**
* Created by PM on 24/08/2016.
@ -12,4 +13,6 @@ case class NewBlock(block: Block) extends BlockchainEvent
case class NewTransaction(tx: Transaction) extends BlockchainEvent
case class CurrentBlockCount(blockcount: Long) extends BlockchainEvent
case class CurrentBlockCount(blockCount: Long) extends BlockchainEvent
case class CurrentFeerates(feeratesPerKw: FeeratesPerKw) extends BlockchainEvent

View File

@ -0,0 +1,41 @@
package fr.acinq.eclair.blockchain
import fr.acinq.bitcoin.{BinaryData, Satoshi, Transaction}
import scala.concurrent.Future
/**
* Created by PM on 06/07/2017.
*/
trait EclairWallet {
def getBalance: Future[Satoshi]
def getFinalAddress: Future[String]
def makeFundingTx(pubkeyScript: BinaryData, amount: Satoshi, feeRatePerKw: Long): Future[MakeFundingTxResponse]
/**
* Committing *must* include publishing the transaction on the network.
*
* We need to be very careful here, we don't want to consider a commit 'failed' if we are not absolutely sure that the
* funding tx won't end up on the blockchain: if that happens and we have cancelled the channel, then we would lose our
* funds!
*
* @param tx
* @return true if success
* false IF AND ONLY IF *HAS NOT BEEN PUBLISHED* otherwise funds are at risk!!!
*/
def commit(tx: Transaction): Future[Boolean]
/**
* Cancels this transaction: this probably translates to "release locks on utxos".
*
* @param tx
* @return
*/
def rollback(tx: Transaction): Future[Boolean]
}
final case class MakeFundingTxResponse(fundingTx: Transaction, fundingTxOutputIndex: Int)

View File

@ -0,0 +1,67 @@
package fr.acinq.eclair.blockchain
import akka.actor.ActorRef
import fr.acinq.bitcoin.Crypto.PublicKey
import fr.acinq.bitcoin.{BinaryData, Script, ScriptWitness, Transaction}
import fr.acinq.eclair.channel.BitcoinEvent
import fr.acinq.eclair.wire.ChannelAnnouncement
import scala.util.{Failure, Success, Try}
/**
* Created by PM on 19/01/2016.
*/
// @formatter:off
sealed trait Watch {
def channel: ActorRef
def event: BitcoinEvent
}
// we need a public key script to use bitcoinj or electrum apis
final case class WatchConfirmed(channel: ActorRef, txId: BinaryData, publicKeyScript: BinaryData, minDepth: Long, event: BitcoinEvent) extends Watch
object WatchConfirmed {
// if we have the entire transaction, we can get the redeemScript from the witness, and re-compute the publicKeyScript
// we support both p2pkh and p2wpkh scripts
def apply(channel: ActorRef, tx: Transaction, minDepth: Long, event: BitcoinEvent): WatchConfirmed = WatchConfirmed(channel, tx.txid, extractPublicKeyScript(tx.txIn.head.witness), minDepth, event)
def extractPublicKeyScript(witness: ScriptWitness): BinaryData = Try(PublicKey(witness.stack.last)) match {
case Success(pubKey) =>
// if last element of the witness is a public key, then this is a p2wpkh
Script.write(Script.pay2wpkh(pubKey))
case Failure(_) =>
// otherwise this is a p2wsh
witness.stack.last
}
}
final case class WatchSpent(channel: ActorRef, txId: BinaryData, outputIndex: Int, publicKeyScript: BinaryData, event: BitcoinEvent) extends Watch
object WatchSpent {
// if we have the entire transaction, we can get the publicKeyScript from the relevant output
def apply(channel: ActorRef, tx: Transaction, outputIndex: Int, event: BitcoinEvent): WatchSpent = WatchSpent(channel, tx.txid, outputIndex, tx.txOut(outputIndex).publicKeyScript, event)
}
final case class WatchSpentBasic(channel: ActorRef, txId: BinaryData, outputIndex: Int, publicKeyScript: BinaryData, event: BitcoinEvent) extends Watch // we use this when we don't care about the spending tx, and we also assume txid already exists
object WatchSpentBasic {
// if we have the entire transaction, we can get the publicKeyScript from the relevant output
def apply(channel: ActorRef, tx: Transaction, outputIndex: Int, event: BitcoinEvent): WatchSpentBasic = WatchSpentBasic(channel, tx.txid, outputIndex, tx.txOut(outputIndex).publicKeyScript, event)
}
// TODO: notify me if confirmation number gets below minDepth?
final case class WatchLost(channel: ActorRef, txId: BinaryData, minDepth: Long, event: BitcoinEvent) extends Watch
trait WatchEvent {
def event: BitcoinEvent
}
final case class WatchEventConfirmed(event: BitcoinEvent, blockHeight: Int, txIndex: Int) extends WatchEvent
final case class WatchEventSpent(event: BitcoinEvent, tx: Transaction) extends WatchEvent
final case class WatchEventSpentBasic(event: BitcoinEvent) extends WatchEvent
final case class WatchEventLost(event: BitcoinEvent) extends WatchEvent
/**
* Publish the provided tx as soon as possible depending on locktime and csv
*/
final case class PublishAsap(tx: Transaction)
final case class ParallelGetRequest(ann: Seq[ChannelAnnouncement])
final case class IndividualResult(c: ChannelAnnouncement, tx: Option[Transaction], unspent: Boolean)
final case class ParallelGetResponse(r: Seq[IndividualResult])
// @formatter:on

View File

@ -0,0 +1,214 @@
package fr.acinq.eclair.blockchain.bitcoind
import akka.actor.{Actor, ActorRef, ActorSystem, Props}
import fr.acinq.bitcoin.Crypto.PrivateKey
import fr.acinq.bitcoin.{Base58Check, BinaryData, OP_PUSHDATA, OutPoint, SIGHASH_ALL, Satoshi, Script, ScriptFlags, ScriptWitness, SigVersion, Transaction, TxIn, TxOut}
import fr.acinq.eclair.blockchain._
import fr.acinq.eclair.blockchain.bitcoind.rpc.{BitcoinJsonRPCClient, JsonRPCError}
import fr.acinq.eclair.channel.{BITCOIN_OUTPUT_SPENT, BITCOIN_TX_CONFIRMED}
import fr.acinq.eclair.transactions.Transactions
import grizzled.slf4j.Logging
import org.json4s.JsonAST.{JBool, JDouble, JInt, JString}
import scala.concurrent.duration._
import scala.concurrent.{ExecutionContext, Future, Promise}
/**
* Due to bitcoin-core wallet not fully supporting segwit txes yet, our current scheme is:
* utxos <- parent-tx <- funding-tx
*
* With:
* - utxos may be non-segwit
* - parent-tx pays to a p2wpkh segwit output
* - funding-tx is a segwit tx
*
* Created by PM on 06/07/2017.
*/
class BitcoinCoreWallet(rpcClient: BitcoinJsonRPCClient, watcher: ActorRef)(implicit system: ActorSystem, ec: ExecutionContext) extends EclairWallet with Logging {
override def getBalance: Future[Satoshi] = ???
override def getFinalAddress: Future[String] = rpcClient.invoke("getnewaddress").map(json => {
val JString(address) = json
address
})
case class FundTransactionResponse(tx: Transaction, changepos: Int, fee: Double)
case class SignTransactionResponse(tx: Transaction, complete: Boolean)
case class MakeFundingTxResponseWithParent(parentTx: Transaction, fundingTx: Transaction, fundingTxOutputIndex: Int, priv: PrivateKey)
def fundTransaction(hex: String, lockUnspents: Boolean): Future[FundTransactionResponse] = {
rpcClient.invoke("fundrawtransaction", hex, BitcoinCoreWallet.Options(lockUnspents)).map(json => {
val JString(hex) = json \ "hex"
val JInt(changepos) = json \ "changepos"
val JDouble(fee) = json \ "fee"
FundTransactionResponse(Transaction.read(hex), changepos.intValue(), fee)
})
}
def fundTransaction(tx: Transaction, lockUnspents: Boolean): Future[FundTransactionResponse] =
fundTransaction(Transaction.write(tx).toString(), lockUnspents)
def signTransaction(hex: String): Future[SignTransactionResponse] =
rpcClient.invoke("signrawtransaction", hex).map(json => {
val JString(hex) = json \ "hex"
val JBool(complete) = json \ "complete"
SignTransactionResponse(Transaction.read(hex), complete)
})
def signTransaction(tx: Transaction): Future[SignTransactionResponse] =
signTransaction(Transaction.write(tx).toString())
def getTransaction(txid: BinaryData): Future[Transaction] = {
rpcClient.invoke("getrawtransaction", txid.toString()).map(json => {
val JString(hex) = json
Transaction.read(hex)
})
}
def publishTransaction(tx: Transaction)(implicit ec: ExecutionContext): Future[String] =
publishTransaction(Transaction.write(tx).toString())
def publishTransaction(hex: String)(implicit ec: ExecutionContext): Future[String] =
rpcClient.invoke("sendrawtransaction", hex) collect {
case JString(txid) => txid
}
/**
*
* @param fundingTxResponse a funding tx response
* @return an updated funding tx response that is properly sign
*/
def sign(fundingTxResponse: MakeFundingTxResponseWithParent): MakeFundingTxResponseWithParent = {
// find the output that we are spending from
val utxo = fundingTxResponse.parentTx.txOut(fundingTxResponse.fundingTx.txIn(0).outPoint.index.toInt)
val pub = fundingTxResponse.priv.publicKey
val pubKeyScript = Script.pay2pkh(pub)
val sig = Transaction.signInput(fundingTxResponse.fundingTx, 0, pubKeyScript, SIGHASH_ALL, utxo.amount, SigVersion.SIGVERSION_WITNESS_V0, fundingTxResponse.priv)
val witness = ScriptWitness(Seq(sig, pub.toBin))
val fundingTx1 = fundingTxResponse.fundingTx.updateSigScript(0, OP_PUSHDATA(Script.write(Script.pay2wpkh(pub))) :: Nil).updateWitness(0, witness)
Transaction.correctlySpends(fundingTx1, fundingTxResponse.parentTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
fundingTxResponse.copy(fundingTx = fundingTx1)
}
/**
*
* @param fundingTxResponse funding transaction response, which includes a funding tx, its parent, and the private key
* that we need to re-sign the funding
* @param newParentTx new parent tx
* @return an updated funding transaction response where the funding tx now spends from newParentTx
*/
def replaceParent(fundingTxResponse: MakeFundingTxResponseWithParent, newParentTx: Transaction): MakeFundingTxResponseWithParent = {
// find the output that we are spending from
val utxo = newParentTx.txOut(fundingTxResponse.fundingTx.txIn(0).outPoint.index.toInt)
// check that it matches what we expect, which is a P2WPKH output to our public key
require(utxo.publicKeyScript == Script.write(Script.pay2sh(Script.pay2wpkh(fundingTxResponse.priv.publicKey))))
// update our tx input we the hash of the new parent
val input = fundingTxResponse.fundingTx.txIn(0)
val input1 = input.copy(outPoint = input.outPoint.copy(hash = newParentTx.hash))
val unsignedFundingTx = fundingTxResponse.fundingTx.copy(txIn = Seq(input1))
// and re-sign it
sign(MakeFundingTxResponseWithParent(newParentTx, unsignedFundingTx, fundingTxResponse.fundingTxOutputIndex, fundingTxResponse.priv))
}
def makeParentAndFundingTx(pubkeyScript: BinaryData, amount: Satoshi, feeRatePerKw: Long): Future[MakeFundingTxResponseWithParent] =
for {
// ask for a new address and the corresponding private key
JString(address) <- rpcClient.invoke("getnewaddress")
JString(wif) <- rpcClient.invoke("dumpprivkey", address)
JString(segwitAddress) <- rpcClient.invoke("addwitnessaddress", address)
(prefix, raw) = Base58Check.decode(wif)
priv = PrivateKey(raw, compressed = true)
pub = priv.publicKey
// create a tx that sends money to a P2SH(WPKH) output that matches our private key
parentFee = Satoshi(250 * 2 * 2 * feeRatePerKw / 1024)
partialParentTx = Transaction(
version = 2,
txIn = Nil,
txOut = TxOut(amount + parentFee, Script.pay2sh(Script.pay2wpkh(pub))) :: Nil,
lockTime = 0L)
FundTransactionResponse(unsignedParentTx, _, _) <- fundTransaction(partialParentTx, lockUnspents = true)
// this is the first tx that we will publish, a standard tx which send money to our p2wpkh address
SignTransactionResponse(parentTx, true) <- signTransaction(unsignedParentTx)
// now we create the funding tx
partialFundingTx = Transaction(
version = 2,
txIn = Seq.empty[TxIn],
txOut = TxOut(amount, pubkeyScript) :: Nil,
lockTime = 0)
// and update it to spend from our segwit tx
pos = Transactions.findPubKeyScriptIndex(parentTx, Script.pay2sh(Script.pay2wpkh(pub)))
unsignedFundingTx = partialFundingTx.copy(txIn = TxIn(OutPoint(parentTx, pos), sequence = TxIn.SEQUENCE_FINAL, signatureScript = Nil) :: Nil)
} yield sign(MakeFundingTxResponseWithParent(parentTx, unsignedFundingTx, 0, priv))
/**
* This is a workaround for malleability
*
* @param pubkeyScript
* @param amount
* @param feeRatePerKw
* @return
*/
override def makeFundingTx(pubkeyScript: BinaryData, amount: Satoshi, feeRatePerKw: Long): Future[MakeFundingTxResponse] = {
val promise = Promise[MakeFundingTxResponse]()
(for {
fundingTxResponse@MakeFundingTxResponseWithParent(parentTx, _, _, _) <- makeParentAndFundingTx(pubkeyScript, amount, feeRatePerKw)
input0 = parentTx.txIn.head
parentOfParentTx <- getTransaction(input0.outPoint.txid)
_ = logger.debug(s"built parentTxid=${parentTx.txid}, initializing temporary actor")
tempActor = system.actorOf(Props(new Actor {
override def receive: Receive = {
case WatchEventSpent(BITCOIN_OUTPUT_SPENT, spendingTx) =>
if (parentTx.txid != spendingTx.txid) {
// an input of our parent tx was spent by a tx that we're not aware of (i.e. a malleated version of our parent tx)
// set a new watch; if it is confirmed, we'll use it as the new parent for our funding tx
logger.warn(s"parent tx has been malleated: originalParentTxid=${parentTx.txid} malleated=${spendingTx.txid}")
}
watcher ! WatchConfirmed(self, spendingTx.txid, spendingTx.txOut(0).publicKeyScript, minDepth = 1, BITCOIN_TX_CONFIRMED(spendingTx))
case WatchEventConfirmed(BITCOIN_TX_CONFIRMED(tx), _, _) =>
// a potential parent for our funding tx has been confirmed, let's update our funding tx
val finalFundingTx = replaceParent(fundingTxResponse, tx)
promise.success(MakeFundingTxResponse(finalFundingTx.fundingTx, finalFundingTx.fundingTxOutputIndex))
}
}))
// we watch the first input of the parent tx, so that we can detect when it is spent by a malleated avatar
_ = watcher ! WatchSpent(tempActor, input0.outPoint.txid, input0.outPoint.index.toInt, parentOfParentTx.txOut(input0.outPoint.index.toInt).publicKeyScript, BITCOIN_OUTPUT_SPENT)
// and we publish the parent tx
_ = logger.info(s"publishing parent tx: txid=${parentTx.txid} tx=${Transaction.write(parentTx)}")
// we use a small delay so that we are sure Publish doesn't race with WatchSpent (which is ok but generates unnecessary warnings)
_ = system.scheduler.scheduleOnce(100 milliseconds, watcher, PublishAsap(parentTx))
} yield {}) onFailure {
case t: Throwable => promise.failure(t)
}
promise.future
}
override def commit(tx: Transaction): Future[Boolean] = publishTransaction(tx)
.map(_ => true) // if bitcoind says OK, then we consider the tx succesfully published
.recoverWith { case JsonRPCError(_) => getTransaction(tx.txid).map(_ => true).recover { case _ => false } } // if we get a parseable error from bitcoind AND the tx is NOT in the mempool/blockchain, then we consider that the tx was not published
.recover { case _ => true } // in all other cases we consider that the tx has been published
/**
* We currently only put a lock on the parent tx inputs, and we publish the parent tx immediately so there is nothing
* to do here.
*
* @param tx
* @return
*/
override def rollback(tx: Transaction): Future[Boolean] = Future.successful(true)
}
object BitcoinCoreWallet {
case class Options(lockUnspents: Boolean)
}

View File

@ -0,0 +1,209 @@
package fr.acinq.eclair.blockchain.bitcoind
import java.util.concurrent.Executors
import akka.actor.{Actor, ActorLogging, Cancellable, Props, Terminated}
import akka.pattern.pipe
import fr.acinq.bitcoin._
import fr.acinq.eclair.Globals
import fr.acinq.eclair.blockchain._
import fr.acinq.eclair.blockchain.bitcoind.rpc.ExtendedBitcoinClient
import fr.acinq.eclair.channel.BITCOIN_PARENT_TX_CONFIRMED
import fr.acinq.eclair.transactions.Scripts
import scala.collection.SortedMap
import scala.concurrent.duration._
import scala.concurrent.{ExecutionContext, Future}
import scala.util.Try
/**
* A blockchain watcher that:
* - receives bitcoin events (new blocks and new txes) directly from the bitcoin network
* - also uses bitcoin-core rpc api, most notably for tx confirmation count and blockcount (because reorgs)
* Created by PM on 21/02/2016.
*/
class ZmqWatcher(client: ExtendedBitcoinClient)(implicit ec: ExecutionContext = ExecutionContext.global) extends Actor with ActorLogging {
import ZmqWatcher.TickNewBlock
context.system.eventStream.subscribe(self, classOf[BlockchainEvent])
// this is to initialize block count
self ! TickNewBlock
case class TriggerEvent(w: Watch, e: WatchEvent)
def receive: Receive = watching(Set(), SortedMap(), None)
def watching(watches: Set[Watch], block2tx: SortedMap[Long, Seq[Transaction]], nextTick: Option[Cancellable]): Receive = {
case NewTransaction(tx) =>
//log.debug(s"analyzing txid=${tx.txid} tx=${Transaction.write(tx)}")
watches.collect {
case w@WatchSpentBasic(_, txid, outputIndex, _, event) if tx.txIn.exists(i => i.outPoint.txid == txid && i.outPoint.index == outputIndex) =>
self ! TriggerEvent(w, WatchEventSpentBasic(event))
case w@WatchSpent(_, txid, outputIndex, _, event) if tx.txIn.exists(i => i.outPoint.txid == txid && i.outPoint.index == outputIndex) =>
self ! TriggerEvent(w, WatchEventSpent(event, tx))
}
case NewBlock(block) =>
// using a Try because in tests we generate fake blocks
log.debug(s"received blockid=${Try(block.blockId).getOrElse(BinaryData(""))}")
nextTick.map(_.cancel()) // this may fail or succeed, worse case scenario we will have two ticks in a row (no big deal)
log.debug(s"scheduling a new task to check on tx confirmations")
// we do this to avoid herd effects in testing when generating a lots of blocks in a row
val task = context.system.scheduler.scheduleOnce(2 seconds, self, TickNewBlock)
context become watching(watches, block2tx, Some(task))
case TickNewBlock =>
client.getBlockCount.map {
case count =>
log.debug(s"setting blockCount=$count")
Globals.blockCount.set(count)
context.system.eventStream.publish(CurrentBlockCount(count))
}
/*client.estimateSmartFee(nodeParams.smartfeeNBlocks).map {
case feeratePerKB if feeratePerKB > 0 =>
val feeratePerKw = feerateKB2Kw(feeratePerKB)
log.debug(s"setting feeratePerKB=$feeratePerKB -> feeratePerKw=$feeratePerKw")
Globals.feeratePerKw.set(feeratePerKw)
context.system.eventStream.publish(CurrentFeerate(feeratePerKw))
case _ => () // bitcoind cannot estimate feerate
}*/
// TODO: beware of the herd effect
watches.collect {
case w@WatchConfirmed(_, txId, _, minDepth, event) =>
log.debug(s"checking confirmations of txid=$txId")
client.getTxConfirmations(txId.toString).map {
case Some(confirmations) if confirmations >= minDepth =>
client.getTransactionShortId(txId.toString).map {
case (height, index) => self ! TriggerEvent(w, WatchEventConfirmed(event, height, index))
}
}
}
context become (watching(watches, block2tx, None))
case TriggerEvent(w, e) if watches.contains(w) =>
log.info(s"triggering $w")
w.channel ! e
// NB: WatchSpent are permanent because we need to detect multiple spending of the funding tx
// They are never cleaned up but it is not a big deal for now (1 channel == 1 watch)
if (!w.isInstanceOf[WatchSpent]) context.become(watching(watches - w, block2tx, None))
case CurrentBlockCount(count) => {
val toPublish = block2tx.filterKeys(_ <= count)
toPublish.values.flatten.map(tx => publish(tx))
context.become(watching(watches, block2tx -- toPublish.keys, None))
}
case w: Watch if !watches.contains(w) => addWatch(w, watches, block2tx)
case PublishAsap(tx) =>
val blockCount = Globals.blockCount.get()
val cltvTimeout = Scripts.cltvTimeout(tx)
val csvTimeout = Scripts.csvTimeout(tx)
if (csvTimeout > 0) {
require(tx.txIn.size == 1, s"watcher only supports tx with 1 input, this tx has ${tx.txIn.size} inputs")
val parentTxid = tx.txIn(0).outPoint.txid
log.info(s"txid=${tx.txid} has a relative timeout of $csvTimeout blocks, watching parenttxid=$parentTxid tx=${Transaction.write(tx)}")
val parentPublicKey = fr.acinq.bitcoin.Script.write(fr.acinq.bitcoin.Script.pay2wsh(tx.txIn.head.witness.stack.last))
self ! WatchConfirmed(self, parentTxid, parentPublicKey, minDepth = 1, BITCOIN_PARENT_TX_CONFIRMED(tx))
} else if (cltvTimeout > blockCount) {
log.info(s"delaying publication of txid=${tx.txid} until block=$cltvTimeout (curblock=$blockCount)")
val block2tx1 = block2tx.updated(cltvTimeout, block2tx.getOrElse(cltvTimeout, Seq.empty[Transaction]) :+ tx)
context.become(watching(watches, block2tx1, None))
} else publish(tx)
case WatchEventConfirmed(BITCOIN_PARENT_TX_CONFIRMED(tx), blockHeight, _) =>
log.info(s"parent tx of txid=${tx.txid} has been confirmed")
val blockCount = Globals.blockCount.get()
val csvTimeout = Scripts.csvTimeout(tx)
val absTimeout = blockHeight + csvTimeout
if (absTimeout > blockCount) {
log.info(s"delaying publication of txid=${tx.txid} until block=$absTimeout (curblock=$blockCount)")
val block2tx1 = block2tx.updated(absTimeout, block2tx.getOrElse(absTimeout, Seq.empty[Transaction]) :+ tx)
context.become(watching(watches, block2tx1, None))
} else publish(tx)
case ParallelGetRequest(ann) => client.getParallel(ann).pipeTo(sender)
case Terminated(channel) =>
// we remove watches associated to dead actor
val deprecatedWatches = watches.filter(_.channel == channel)
context.become(watching(watches -- deprecatedWatches, block2tx, None))
case 'watches => sender ! watches
}
def addWatch(w: Watch, watches: Set[Watch], block2tx: SortedMap[Long, Seq[Transaction]]) = {
w match {
case WatchSpentBasic(_, txid, outputIndex, _, _) =>
// not: we assume parent tx was published, we just need to make sure this particular output has not been spent
client.isTransactionOuputSpendable(txid.toString(), outputIndex, true).collect {
case false =>
log.warning(s"output=$outputIndex of txid=$txid has already been spent")
self ! TriggerEvent(w, WatchEventSpentBasic(w.event))
}
case w@WatchSpent(_, txid, outputIndex, _, _) =>
// first let's see if the parent tx was published or not
client.getTxConfirmations(txid.toString()).collect {
case Some(_) =>
// parent tx was published, we need to make sure this particular output has not been spent
client.isTransactionOuputSpendable(txid.toString(), outputIndex, true).collect {
case false =>
log.warning(s"output=$outputIndex of txid=$txid has already been spent")
client.getTxBlockHash(txid.toString()).collect {
case Some(blockhash) =>
log.warning(s"getting all transactions since blockhash=$blockhash")
client.getTxsSinceBlockHash(blockhash).map {
case txs =>
log.warning(s"found ${txs.size} txs since blockhash=$blockhash")
txs.foreach(tx => self ! NewTransaction(tx))
} onFailure {
case t: Throwable => log.error(t, "")
}
}
client.getMempool().map {
case txs =>
log.warning(s"found ${txs.size} txs in the mempool")
txs.foreach(tx => self ! NewTransaction(tx))
}
}
}
case w: WatchConfirmed => self ! TickNewBlock
case w => log.warning(s"ignoring $w (not implemented)")
}
log.debug(s"adding watch $w for $sender")
context.watch(w.channel)
context.become(watching(watches + w, block2tx, None))
}
// NOTE: we use a single thread to publish transactions so that it preserves order.
// CHANGING THIS WILL RESULT IN CONCURRENCY ISSUES WHILE PUBLISHING PARENT AND CHILD TXS
val singleThreadExecutionContext = ExecutionContext.fromExecutor(Executors.newSingleThreadExecutor())
def publish(tx: Transaction, isRetry: Boolean = false): Unit = {
log.info(s"publishing tx (isRetry=$isRetry): txid=${tx.txid} tx=${Transaction.write(tx)}")
client.publishTransaction(tx)(singleThreadExecutionContext).recover {
case t: Throwable if t.getMessage.contains("-25") && !isRetry => // we retry only once
import akka.pattern.after
import scala.concurrent.duration._
after(3 seconds, context.system.scheduler)(Future.successful({})).map(x => publish(tx, isRetry = true))
case t: Throwable => log.error(s"cannot publish tx: reason=${t.getMessage} txid=${tx.txid} tx=${BinaryData(Transaction.write(tx))}")
}
}
}
object ZmqWatcher {
def props(client: ExtendedBitcoinClient)(implicit ec: ExecutionContext = ExecutionContext.global) = Props(new ZmqWatcher(client)(ec))
case object TickNewBlock
}

View File

@ -0,0 +1,81 @@
package fr.acinq.eclair.blockchain.bitcoind.rpc
import java.io.IOException
import akka.actor.ActorSystem
import akka.http.scaladsl.Http
import akka.http.scaladsl.marshalling.Marshal
import akka.http.scaladsl.model._
import akka.http.scaladsl.model.headers.{Authorization, BasicHttpCredentials}
import akka.http.scaladsl.unmarshalling.Unmarshal
import akka.stream.{ActorMaterializer, OverflowStrategy, QueueOfferResult}
import akka.stream.scaladsl.{Keep, Sink, Source}
import de.heikoseeberger.akkahttpjson4s.Json4sSupport._
import org.json4s.JsonAST.JValue
import org.json4s.{DefaultFormats, jackson}
import scala.concurrent.{ExecutionContext, Future, Promise}
import scala.concurrent.ExecutionContext.Implicits.global
import scala.util.{Failure, Success}
// @formatter:off
case class JsonRPCRequest(jsonrpc: String = "1.0", id: String = "scala-client", method: String, params: Seq[Any])
case class Error(code: Int, message: String)
case class JsonRPCResponse(result: JValue, error: Option[Error], id: String)
case class JsonRPCError(error: Error) extends IOException(s"${error.message} (code: ${error.code})")
// @formatter:on
class BitcoinJsonRPCClient(user: String, password: String, host: String = "127.0.0.1", port: Int = 8332, ssl: Boolean = false)(implicit system: ActorSystem) {
val scheme = if (ssl) "https" else "http"
val uri = Uri(s"$scheme://$host:$port")
implicit val serialization = jackson.Serialization
implicit val formats = DefaultFormats
implicit val materializer = ActorMaterializer()
val httpClientFlow = Http().cachedHostConnectionPool[Promise[HttpResponse]](host, port)
val queueSize = 512
val queue = Source.queue[(HttpRequest, Promise[HttpResponse])](queueSize, OverflowStrategy.dropNew)
.via(httpClientFlow)
.toMat(Sink.foreach({
case ((Success(resp), p)) => p.success(resp)
case ((Failure(e), p)) => p.failure(e)
}))(Keep.left)
.run()
def queueRequest(request: HttpRequest): Future[HttpResponse] = {
val responsePromise = Promise[HttpResponse]()
queue.offer(request -> responsePromise).flatMap {
case QueueOfferResult.Enqueued => responsePromise.future
case QueueOfferResult.Dropped => Future.failed(new RuntimeException("Queue overflowed. Try again later."))
case QueueOfferResult.Failure(ex) => Future.failed(ex)
case QueueOfferResult.QueueClosed => Future.failed(new RuntimeException("Queue was closed (pool shut down) while running the request. Try again later."))
}
}
def invoke(method: String, params: Any*)(implicit ec: ExecutionContext): Future[JValue] =
for {
entity <- Marshal(JsonRPCRequest(method = method, params = params)).to[RequestEntity]
httpRes <- queueRequest(HttpRequest(uri = "/", method = HttpMethods.POST).addHeader(Authorization(BasicHttpCredentials(user, password))).withEntity(entity))
jsonRpcRes <- Unmarshal(httpRes).to[JsonRPCResponse].map {
case JsonRPCResponse(_, Some(error), _) => throw JsonRPCError(error)
case o => o
} recover {
case t: Throwable if httpRes.status == StatusCodes.Unauthorized => throw new RuntimeException("bitcoind replied with 401/Unauthorized (bad user/password?)", t)
}
} yield jsonRpcRes.result
def invoke(request: Seq[(String, Seq[Any])])(implicit ec: ExecutionContext): Future[Seq[JValue]] =
for {
entity <- Marshal(request.map(r => JsonRPCRequest(method = r._1, params = r._2))).to[RequestEntity]
httpRes <- queueRequest(HttpRequest(uri = "/", method = HttpMethods.POST).addHeader(Authorization(BasicHttpCredentials(user, password))).withEntity(entity))
jsonRpcRes <- Unmarshal(httpRes).to[Seq[JsonRPCResponse]].map {
//case JsonRPCResponse(_, Some(error), _) => throw JsonRPCError(error)
case o => o
} recover {
case t: Throwable if httpRes.status == StatusCodes.Unauthorized => throw new RuntimeException("bitcoind replied with 401/Unauthorized (bad user/password?)", t)
}
} yield jsonRpcRes.map(_.result)
}

View File

@ -0,0 +1,195 @@
package fr.acinq.eclair.blockchain.bitcoind.rpc
import fr.acinq.bitcoin._
import fr.acinq.eclair.blockchain.{IndividualResult, ParallelGetResponse}
import fr.acinq.eclair.fromShortId
import fr.acinq.eclair.wire.ChannelAnnouncement
import org.json4s.JsonAST._
import scala.concurrent.{ExecutionContext, Future}
import scala.util.Try
/**
* Created by PM on 26/04/2016.
*/
class ExtendedBitcoinClient(val rpcClient: BitcoinJsonRPCClient) {
implicit val formats = org.json4s.DefaultFormats
// TODO: this will probably not be needed once segwit is merged into core
val protocolVersion = Protocol.PROTOCOL_VERSION
def tx2Hex(tx: Transaction): String = toHexString(Transaction.write(tx, protocolVersion))
def hex2tx(hex: String): Transaction = Transaction.read(hex, protocolVersion)
def getTxConfirmations(txId: String)(implicit ec: ExecutionContext): Future[Option[Int]] =
rpcClient.invoke("getrawtransaction", txId, 1) // we choose verbose output to get the number of confirmations
.map(json => Some((json \ "confirmations").extractOrElse[Int](0)))
.recover {
case t: JsonRPCError if t.error.code == -5 => None
}
def getTxBlockHash(txId: String)(implicit ec: ExecutionContext): Future[Option[String]] =
rpcClient.invoke("getrawtransaction", txId, 1) // we choose verbose output to get the number of confirmations
.map(json => (json \ "blockhash").extractOpt[String])
.recover {
case t: JsonRPCError if t.error.code == -5 => None
}
def getBlockHashesSinceBlockHash(blockHash: String, previous: Seq[String] = Nil)(implicit ec: ExecutionContext): Future[Seq[String]] =
for {
nextblockhash_opt <- rpcClient.invoke("getblock", blockHash).map(json => ((json \ "nextblockhash").extractOpt[String]))
res <- nextblockhash_opt match {
case Some(nextBlockHash) => getBlockHashesSinceBlockHash(nextBlockHash, previous :+ nextBlockHash)
case None => Future.successful(previous)
}
} yield res
def getTxsSinceBlockHash(blockHash: String, previous: Seq[Transaction] = Nil)(implicit ec: ExecutionContext): Future[Seq[Transaction]] =
for {
(nextblockhash_opt, txids) <- rpcClient.invoke("getblock", blockHash).map(json => ((json \ "nextblockhash").extractOpt[String], (json \ "tx").extract[List[String]]))
next <- Future.sequence(txids.map(getTransaction(_)))
res <- nextblockhash_opt match {
case Some(nextBlockHash) => getTxsSinceBlockHash(nextBlockHash, previous ++ next)
case None => Future.successful(previous ++ next)
}
} yield res
def getMempool()(implicit ec: ExecutionContext): Future[Seq[Transaction]] =
for {
txids <- rpcClient.invoke("getrawmempool").map(json => json.extract[List[String]])
txs <- Future.sequence(txids.map(getTransaction(_)))
} yield txs
/**
* *used in interop test*
* tell bitcoind to sent bitcoins from a specific local account
*
* @param account name of the local account to send bitcoins from
* @param destination destination address
* @param amount amount in BTC (not milliBTC, not Satoshis !!)
* @param ec execution context
* @return a Future[txid] where txid (a String) is the is of the tx that sends the bitcoins
*/
def sendFromAccount(account: String, destination: String, amount: Double)(implicit ec: ExecutionContext): Future[String] =
rpcClient.invoke("sendfrom", account, destination, amount) collect {
case JString(txid) => txid
}
/**
* @param txId
* @param ec
* @return
*/
def getRawTransaction(txId: String)(implicit ec: ExecutionContext): Future[String] =
rpcClient.invoke("getrawtransaction", txId) collect {
case JString(raw) => raw
}
def getTransaction(txId: String)(implicit ec: ExecutionContext): Future[Transaction] =
getRawTransaction(txId).map(raw => Transaction.read(raw))
def getTransaction(height: Int, index: Int)(implicit ec: ExecutionContext): Future[Transaction] =
for {
hash <- rpcClient.invoke("getblockhash", height).map(json => json.extract[String])
json <- rpcClient.invoke("getblock", hash)
JArray(txs) = json \ "tx"
txid = txs(index).extract[String]
tx <- getTransaction(txid)
} yield tx
def isTransactionOuputSpendable(txId: String, ouputIndex: Int, includeMempool: Boolean)(implicit ec: ExecutionContext): Future[Boolean] =
for {
json <- rpcClient.invoke("gettxout", txId, ouputIndex, includeMempool)
} yield json != JNull
/**
*
* @param txId transaction id
* @param ec
* @return a Future[height, index] where height is the height of the block where this transaction was published, and index is
* the index of the transaction in that block
*/
def getTransactionShortId(txId: String)(implicit ec: ExecutionContext): Future[(Int, Int)] = {
val future = for {
Some(blockHash) <- getTxBlockHash(txId)
json <- rpcClient.invoke("getblock", blockHash)
JInt(height) = json \ "height"
JString(hash) = json \ "hash"
JArray(txs) = json \ "tx"
index = txs.indexOf(JString(txId))
} yield (height.toInt, index)
future
}
def publishTransaction(hex: String)(implicit ec: ExecutionContext): Future[String] =
rpcClient.invoke("sendrawtransaction", hex) collect {
case JString(txid) => txid
}
def publishTransaction(tx: Transaction)(implicit ec: ExecutionContext): Future[String] =
publishTransaction(tx2Hex(tx))
/**
* We need this to compute absolute timeouts expressed in number of blocks (where getBlockCount would be equivalent
* to time.now())
*
* @param ec
* @return the current number of blocks in the active chain
*/
def getBlockCount(implicit ec: ExecutionContext): Future[Long] =
rpcClient.invoke("getblockcount") collect {
case JInt(count) => count.toLong
}
def getParallel(awaiting: Seq[ChannelAnnouncement]): Future[ParallelGetResponse] = {
case class TxCoordinate(blockHeight: Int, txIndex: Int, outputIndex: Int)
val coordinates = awaiting.map {
case c =>
val (blockHeight, txIndex, outputIndex) = fromShortId(c.shortChannelId)
TxCoordinate(blockHeight, txIndex, outputIndex)
}.zipWithIndex
import ExecutionContext.Implicits.global
implicit val formats = org.json4s.DefaultFormats
for {
blockHashes: Seq[String] <- rpcClient.invoke(coordinates.map(coord => ("getblockhash", coord._1.blockHeight :: Nil))).map(_.map(_.extractOrElse[String]("00" * 32)))
txids: Seq[String] <- rpcClient.invoke(blockHashes.map(h => ("getblock", h :: Nil)))
.map(_.zipWithIndex)
.map(_.map {
case (json, idx) => Try {
val JArray(txs) = json \ "tx"
txs(coordinates(idx)._1.txIndex).extract[String]
} getOrElse ("00" * 32)
})
txs <- rpcClient.invoke(txids.map(txid => ("getrawtransaction", txid :: Nil))).map(_.map {
case JString(raw) => Some(Transaction.read(raw))
case _ => None
})
unspent <- rpcClient.invoke(txids.zipWithIndex.map(txid => ("gettxout", txid._1 :: coordinates(txid._2)._1.outputIndex :: true :: Nil))).map(_.map(_ != JNull))
} yield ParallelGetResponse(awaiting.zip(txs.zip(unspent)).map(x => IndividualResult(x._1, x._2._1, x._2._2)))
}
}
/*object Test extends App {
import scala.concurrent.duration._
import ExecutionContext.Implicits.global
implicit val system = ActorSystem()
implicit val timeout = Timeout(30 seconds)
val bitcoin_client = new ExtendedBitcoinClient(new BitcoinJsonRPCClient(
user = "foo",
password = "bar",
host = "localhost",
port = 28332))
println(Await.result(bitcoin_client.getTxBlockHash("dcb0abfa822402ce379fedd7bbbb2c824e53ef300313594c39282da1efd35f17"), 10 seconds))
}*/

View File

@ -0,0 +1,89 @@
package fr.acinq.eclair.blockchain.bitcoind.zmq
import akka.actor.{Actor, ActorLogging}
import fr.acinq.bitcoin.{Block, Transaction}
import fr.acinq.eclair.blockchain.{NewBlock, NewTransaction}
import org.zeromq.ZMQ.Event
import org.zeromq.{ZContext, ZMQ, ZMsg}
import scala.concurrent.Promise
import scala.concurrent.duration._
import scala.util.Try
/**
* Created by PM on 04/04/2017.
*/
class ZMQActor(address: String, connected: Option[Promise[Boolean]] = None) extends Actor with ActorLogging {
import ZMQActor._
val ctx = new ZContext
val subscriber = ctx.createSocket(ZMQ.SUB)
subscriber.monitor("inproc://events", ZMQ.EVENT_CONNECTED | ZMQ.EVENT_DISCONNECTED)
subscriber.connect(address)
subscriber.subscribe("rawblock".getBytes(ZMQ.CHARSET))
subscriber.subscribe("rawtx".getBytes(ZMQ.CHARSET))
val monitor = ctx.createSocket(ZMQ.PAIR)
monitor.connect("inproc://events")
import scala.concurrent.ExecutionContext.Implicits.global
// we check messages in a non-blocking manner with an interval, making sure to retrieve all messages before waiting again
def checkEvent: Unit = Option(Event.recv(monitor, ZMQ.DONTWAIT)) match {
case Some(event) =>
self ! event
checkEvent
case None =>
context.system.scheduler.scheduleOnce(1 second)(checkEvent)
}
def checkMsg: Unit = Option(ZMsg.recvMsg(subscriber, ZMQ.DONTWAIT)) match {
case Some(msg) =>
self ! msg
checkMsg
case None =>
context.system.scheduler.scheduleOnce(1 second)(checkMsg)
}
checkEvent
checkMsg
override def receive: Receive = {
case event: Event => event.getEvent match {
case ZMQ.EVENT_CONNECTED =>
log.info(s"connected to ${event.getAddress}")
Try(connected.map(_.success(true)))
context.system.eventStream.publish(ZMQConnected)
case ZMQ.EVENT_DISCONNECTED =>
log.warning(s"disconnected from ${event.getAddress}")
context.system.eventStream.publish(ZMQDisconnected)
case x => log.error(s"unexpected event $x")
}
case msg: ZMsg => msg.popString() match {
case "rawblock" =>
val block = Block.read(msg.pop().getData)
log.debug(s"received blockid=${block.blockId}")
context.system.eventStream.publish(NewBlock(block))
case "rawtx" =>
val tx = Transaction.read(msg.pop().getData)
log.debug(s"received txid=${tx.txid}")
context.system.eventStream.publish(NewTransaction(tx))
case topic => log.warning(s"unexpected topic=$topic")
}
}
}
object ZMQActor {
// @formatter:off
sealed trait ZMQEvent
case object ZMQConnected extends ZMQEvent
case object ZMQDisconnected extends ZMQEvent
// @formatter:on
}

View File

@ -0,0 +1,152 @@
package fr.acinq.eclair.blockchain.bitcoinj
import java.io.File
import java.net.InetSocketAddress
import akka.actor.ActorSystem
import com.google.common.util.concurrent.{FutureCallback, Futures}
import fr.acinq.bitcoin.Transaction
import fr.acinq.eclair.Globals
import fr.acinq.eclair.blockchain.CurrentBlockCount
import fr.acinq.eclair.blockchain.bitcoinj.BitcoinjKit._
import grizzled.slf4j.Logging
import org.bitcoinj.core.TransactionConfidence.ConfidenceType
import org.bitcoinj.core.listeners._
import org.bitcoinj.core.{Block, Context, FilteredBlock, NetworkParameters, Peer, PeerAddress, StoredBlock, VersionMessage, Transaction => BitcoinjTransaction}
import org.bitcoinj.kits.WalletAppKit
import org.bitcoinj.params.{RegTestParams, TestNet3Params}
import org.bitcoinj.utils.Threading
import org.bitcoinj.wallet.Wallet
import scala.collection.JavaConversions._
import scala.concurrent.Promise
import scala.util.Try
/**
* Created by PM on 09/07/2017.
*/
class BitcoinjKit(chain: String, datadir: File, staticPeers: List[InetSocketAddress] = Nil)(implicit system: ActorSystem) extends WalletAppKit(chain2Params(chain), datadir, "bitcoinj", true) with Logging {
if (staticPeers.size > 0) {
logger.info(s"using staticPeers=${staticPeers.mkString(",")}")
setPeerNodes(staticPeers.map(addr => new PeerAddress(params, addr)).head)
}
// tells us when the peerGroup/chain/wallet are accessible
private val initializedPromise = Promise[Boolean]()
val initialized = initializedPromise.future
// tells us as soon as we know the current block height
private val atCurrentHeightPromise = Promise[Boolean]()
val atCurrentHeight = atCurrentHeightPromise.future
// tells us when we are at current block height
// private val syncedPromise = Promise[Boolean]()
// val synced = syncedPromise.future
private def updateBlockCount(blockCount: Int) = {
// when synchronizing we don't want to advertise previous blocks
if (Globals.blockCount.get() < blockCount) {
logger.debug(s"current blockchain height=$blockCount")
system.eventStream.publish(CurrentBlockCount(blockCount))
Globals.blockCount.set(blockCount)
}
}
override def onSetupCompleted(): Unit = {
logger.info(s"peerGroup.getMinBroadcastConnections==${peerGroup().getMinBroadcastConnections}")
logger.info(s"peerGroup.getMinBroadcastConnections==${peerGroup().getMinBroadcastConnections}")
peerGroup().setMinRequiredProtocolVersion(70015) // bitcoin core 0.13
wallet().watchMode = true
// setDownloadListener(new DownloadProgressTracker {
// override def doneDownload(): Unit = {
// super.doneDownload()
// // may be called multiple times
// syncedPromise.trySuccess(true)
// }
// })
// we set the blockcount to the previous stored block height
updateBlockCount(chain().getBestChainHeight)
// as soon as we are connected the peers will tell us their current height and we will advertise it immediately
peerGroup().addConnectedEventListener(new PeerConnectedEventListener {
override def onPeerConnected(peer: Peer, peerCount: Int): Unit = {
if ((peer.getPeerVersionMessage.localServices & VersionMessage.NODE_WITNESS) == 0) {
peer.close()
} else {
Context.propagate(wallet.getContext)
// we wait for at least 3 peers before relying on the information they are giving, but we trust localhost
if (peer.getAddress.getAddr.isLoopbackAddress || peerCount > 3) {
updateBlockCount(peerGroup().getMostCommonChainHeight)
// may be called multiple times
atCurrentHeightPromise.trySuccess(true)
}
}
}
})
peerGroup.addBlocksDownloadedEventListener(new BlocksDownloadedEventListener {
override def onBlocksDownloaded(peer: Peer, block: Block, filteredBlock: FilteredBlock, blocksLeft: Int): Unit = {
Context.propagate(wallet.getContext)
logger.debug(s"received block=${block.getHashAsString} (size=${block.bitcoinSerialize().size} txs=${Try(block.getTransactions.size).getOrElse(-1)}) filteredBlock=${Try(filteredBlock.getHash.toString).getOrElse("N/A")} (size=${Try(block.bitcoinSerialize().size).getOrElse(-1)} txs=${Try(filteredBlock.getTransactionCount).getOrElse(-1)})")
Try {
if (filteredBlock.getAssociatedTransactions.size() > 0) {
logger.info(s"retrieving full block ${block.getHashAsString}")
Futures.addCallback(peer.getBlock(block.getHash), new FutureCallback[Block] {
override def onFailure(throwable: Throwable) = logger.error(s"could not retrieve full block=${block.getHashAsString}")
override def onSuccess(fullBlock: Block) = {
Try {
Context.propagate(wallet.getContext)
fullBlock.getTransactions.foreach {
case tx =>
logger.debug(s"received tx=${tx.getHashAsString} witness=${Transaction.read(tx.bitcoinSerialize()).txIn(0).witness.stack.size} from fullBlock=${fullBlock.getHash} confidence=${tx.getConfidence}")
val depthInBlocks = tx.getConfidence.getConfidenceType match {
case ConfidenceType.DEAD => -1
case _ => tx.getConfidence.getDepthInBlocks
}
system.eventStream.publish(NewConfidenceLevel(Transaction.read(tx.bitcoinSerialize()), 0, depthInBlocks))
}
}
}
}, Threading.USER_THREAD)
}
}
}
})
chain().addNewBestBlockListener(new NewBestBlockListener {
override def notifyNewBestBlock(storedBlock: StoredBlock): Unit =
updateBlockCount(storedBlock.getHeight)
})
wallet().addTransactionConfidenceEventListener(new TransactionConfidenceEventListener {
override def onTransactionConfidenceChanged(wallet: Wallet, bitcoinjTx: BitcoinjTransaction): Unit = {
Context.propagate(wallet.getContext)
val tx = Transaction.read(bitcoinjTx.bitcoinSerialize())
logger.info(s"tx confidence changed for txid=${tx.txid} confidence=${bitcoinjTx.getConfidence} witness=${bitcoinjTx.getWitness(0)}")
val (blockHeight, confirmations) = bitcoinjTx.getConfidence.getConfidenceType match {
case ConfidenceType.DEAD => (-1, -1)
case ConfidenceType.BUILDING => (bitcoinjTx.getConfidence.getAppearedAtChainHeight, bitcoinjTx.getConfidence.getDepthInBlocks)
case _ => (-1, bitcoinjTx.getConfidence.getDepthInBlocks)
}
system.eventStream.publish(NewConfidenceLevel(tx, blockHeight, confirmations))
}
})
initializedPromise.success(true)
}
}
object BitcoinjKit {
def chain2Params(chain: String): NetworkParameters = chain match {
case "regtest" => RegTestParams.get()
case "test" => TestNet3Params.get()
}
}

View File

@ -0,0 +1,68 @@
package fr.acinq.eclair.blockchain.bitcoinj
import fr.acinq.bitcoin.{BinaryData, Satoshi, Transaction}
import fr.acinq.eclair.blockchain.{EclairWallet, MakeFundingTxResponse}
import grizzled.slf4j.Logging
import org.bitcoinj.core.{Coin, Context, Transaction => BitcoinjTransaction}
import org.bitcoinj.script.Script
import org.bitcoinj.wallet.{SendRequest, Wallet}
import scala.collection.JavaConversions._
import scala.concurrent.{ExecutionContext, Future}
/**
* Created by PM on 08/07/2017.
*/
class BitcoinjWallet(val fWallet: Future[Wallet])(implicit ec: ExecutionContext) extends EclairWallet with Logging {
fWallet.map(wallet => wallet.allowSpendingUnconfirmedTransactions())
override def getBalance: Future[Satoshi] = for {
wallet <- fWallet
} yield {
Context.propagate(wallet.getContext)
Satoshi(wallet.getBalance.longValue())
}
override def getFinalAddress: Future[String] = for {
wallet <- fWallet
} yield {
Context.propagate(wallet.getContext)
wallet.currentReceiveAddress().toBase58
}
override def makeFundingTx(pubkeyScript: BinaryData, amount: Satoshi, feeRatePerKw: Long): Future[MakeFundingTxResponse] = for {
wallet <- fWallet
} yield {
logger.info(s"building funding tx")
Context.propagate(wallet.getContext)
val script = new Script(pubkeyScript)
val tx = new BitcoinjTransaction(wallet.getParams)
tx.addOutput(Coin.valueOf(amount.amount), script)
val req = SendRequest.forTx(tx)
wallet.completeTx(req)
val txOutputIndex = tx.getOutputs.find(_.getScriptPubKey.equals(script)).get.getIndex
MakeFundingTxResponse(Transaction.read(tx.bitcoinSerialize()), txOutputIndex)
}
override def commit(tx: Transaction): Future[Boolean] = {
// we make sure that we haven't double spent our own tx (eg by opening 2 channels at the same time)
val serializedTx = Transaction.write(tx)
logger.info(s"committing tx: txid=${tx.txid} tx=$serializedTx")
for {
wallet <- fWallet
_ = Context.propagate(wallet.getContext)
bitcoinjTx = new org.bitcoinj.core.Transaction(wallet.getParams(), serializedTx)
canCommit = wallet.maybeCommitTx(bitcoinjTx)
_ = logger.info(s"commit txid=${tx.txid} result=$canCommit")
} yield canCommit
}
/**
* There are no locks on bitcoinj, this is a no-op
*
* @param tx
* @return
*/
override def rollback(tx: Transaction) = Future.successful(true)
}

View File

@ -0,0 +1,193 @@
package fr.acinq.eclair.blockchain.bitcoinj
import akka.actor.{Actor, ActorLogging, Props, Terminated}
import com.google.common.collect.ImmutableList
import com.google.common.util.concurrent.{FutureCallback, Futures}
import fr.acinq.bitcoin.Crypto.PublicKey
import fr.acinq.bitcoin.Script.{pay2wsh, write}
import fr.acinq.bitcoin.{BinaryData, Satoshi, Transaction, TxIn, TxOut}
import fr.acinq.eclair.blockchain._
import fr.acinq.eclair.channel.BITCOIN_PARENT_TX_CONFIRMED
import fr.acinq.eclair.transactions.Scripts
import fr.acinq.eclair.{Globals, fromShortId}
import org.bitcoinj.core.{Context, Transaction => BitcoinjTransaction}
import org.bitcoinj.kits.WalletAppKit
import org.bitcoinj.script.Script
import scala.collection.SortedMap
import scala.concurrent.ExecutionContext
import scala.util.{Failure, Success, Try}
final case class NewConfidenceLevel(tx: Transaction, blockHeight: Int, confirmations: Int) extends BlockchainEvent
/**
* A blockchain watcher that:
* - receives bitcoin events (new blocks and new txes) directly from the bitcoin network
* - also uses bitcoin-core rpc api, most notably for tx confirmation count and blockcount (because reorgs)
* Created by PM on 21/02/2016.
*/
class BitcoinjWatcher(val kit: WalletAppKit)(implicit ec: ExecutionContext = ExecutionContext.global) extends Actor with ActorLogging {
context.system.eventStream.subscribe(self, classOf[BlockchainEvent])
context.system.eventStream.subscribe(self, classOf[NewConfidenceLevel])
val broadcaster = context.actorOf(Props(new Broadcaster(kit: WalletAppKit)), name = "broadcaster")
case class TriggerEvent(w: Watch, e: WatchEvent)
def receive: Receive = watching(Set(), SortedMap(), Nil, Nil)
def watching(watches: Set[Watch], block2tx: SortedMap[Long, Seq[Transaction]], oldEvents: Seq[NewConfidenceLevel], sent: Seq[TriggerEvent]): Receive = {
case event@NewConfidenceLevel(tx, blockHeight, confirmations) =>
log.debug(s"analyzing txid=${tx.txid} confirmations=$confirmations tx=${Transaction.write(tx)}")
watches.collect {
case w@WatchSpentBasic(_, txid, outputIndex, _, event) if tx.txIn.exists(i => i.outPoint.txid == txid && i.outPoint.index == outputIndex) =>
self ! TriggerEvent(w, WatchEventSpentBasic(event))
case w@WatchSpent(_, txid, outputIndex, _, event) if tx.txIn.exists(i => i.outPoint.txid == txid && i.outPoint.index == outputIndex) =>
self ! TriggerEvent(w, WatchEventSpent(event, tx))
case w@WatchConfirmed(_, txId, _, minDepth, event) if txId == tx.txid && confirmations >= minDepth =>
self ! TriggerEvent(w, WatchEventConfirmed(event, blockHeight, 0))
}
context become watching(watches, block2tx, oldEvents.filterNot(_.tx.txid == tx.txid) :+ event, sent)
case t@TriggerEvent(w, e) if watches.contains(w) && !sent.contains(t) =>
log.info(s"triggering $w")
w.channel ! e
// NB: WatchSpent are permanent because we need to detect multiple spending of the funding tx
// They are never cleaned up but it is not a big deal for now (1 channel == 1 watch)
val newWatches = if (!w.isInstanceOf[WatchSpent]) watches - w else watches
context.become(watching(newWatches, block2tx, oldEvents, sent :+ t))
case CurrentBlockCount(count) => {
val toPublish = block2tx.filterKeys(_ <= count)
toPublish.values.flatten.map(tx => publish(tx))
context.become(watching(watches, block2tx -- toPublish.keys, oldEvents, sent))
}
case w: Watch if !watches.contains(w) =>
w match {
case w: WatchConfirmed => addHint(w.publicKeyScript)
case w: WatchSpent => addHint(w.publicKeyScript)
case w: WatchSpentBasic => addHint(w.publicKeyScript)
case _ => ()
}
log.debug(s"adding watch $w for $sender")
log.info(s"resending ${oldEvents.size} events!")
oldEvents.foreach(self ! _)
context.watch(w.channel)
context.become(watching(watches + w, block2tx, oldEvents, sent))
case PublishAsap(tx) =>
val blockCount = Globals.blockCount.get()
val cltvTimeout = Scripts.cltvTimeout(tx)
val csvTimeout = Scripts.csvTimeout(tx)
if (csvTimeout > 0) {
require(tx.txIn.size == 1, s"watcher only supports tx with 1 input, this tx has ${tx.txIn.size} inputs")
val parentTxid = tx.txIn(0).outPoint.txid
log.info(s"txid=${tx.txid} has a relative timeout of $csvTimeout blocks, watching parenttxid=$parentTxid tx=${Transaction.write(tx)}")
val parentPublicKey = fr.acinq.bitcoin.Script.write(fr.acinq.bitcoin.Script.pay2wsh(tx.txIn.head.witness.stack.last))
self ! WatchConfirmed(self, parentTxid, parentPublicKey, minDepth = 1, BITCOIN_PARENT_TX_CONFIRMED(tx))
} else if (cltvTimeout > blockCount) {
log.info(s"delaying publication of txid=${tx.txid} until block=$cltvTimeout (curblock=$blockCount)")
val block2tx1 = block2tx.updated(cltvTimeout, block2tx.getOrElse(cltvTimeout, Seq.empty[Transaction]) :+ tx)
context.become(watching(watches, block2tx1, oldEvents, sent))
} else publish(tx)
case WatchEventConfirmed(BITCOIN_PARENT_TX_CONFIRMED(tx), blockHeight, _) =>
log.info(s"parent tx of txid=${tx.txid} has been confirmed")
val blockCount = Globals.blockCount.get()
val csvTimeout = Scripts.csvTimeout(tx)
val absTimeout = blockHeight + csvTimeout
if (absTimeout > blockCount) {
log.info(s"delaying publication of txid=${tx.txid} until block=$absTimeout (curblock=$blockCount)")
val block2tx1 = block2tx.updated(absTimeout, block2tx.getOrElse(absTimeout, Seq.empty[Transaction]) :+ tx)
context.become(watching(watches, block2tx1, oldEvents, sent))
} else publish(tx)
case ParallelGetRequest(announcements) => sender ! ParallelGetResponse(announcements.map {
case c =>
log.info(s"blindly validating channel=$c")
val pubkeyScript = write(pay2wsh(Scripts.multiSig2of2(PublicKey(c.bitcoinKey1), PublicKey(c.bitcoinKey2))))
val (_, _, outputIndex) = fromShortId(c.shortChannelId)
val fakeFundingTx = Transaction(
version = 2,
txIn = Seq.empty[TxIn],
txOut = List.fill(outputIndex + 1)(TxOut(Satoshi(0), pubkeyScript)), // quick and dirty way to be sure that the outputIndex'th output is of the expected format
lockTime = 0)
IndividualResult(c, Some(fakeFundingTx), true)
})
case Terminated(channel) =>
// we remove watches associated to dead actor
val deprecatedWatches = watches.filter(_.channel == channel)
context.become(watching(watches -- deprecatedWatches, block2tx, oldEvents, sent))
case 'watches => sender ! watches
}
/**
* Bitcoinj needs hints to be able to detect transactions
*
* @param pubkeyScript
* @return
*/
def addHint(pubkeyScript: BinaryData) = {
Context.propagate(kit.wallet.getContext)
val script = new Script(pubkeyScript)
// set creation time to 2017/09/01, so bitcoinj can still use its checkpoints optimizations
script.setCreationTimeSeconds(1501538400L) // 2017-09-01
kit.wallet().addWatchedScripts(ImmutableList.of(script))
}
def publish(tx: Transaction): Unit = broadcaster ! tx
}
object BitcoinjWatcher {
def props(kit: WalletAppKit)(implicit ec: ExecutionContext = ExecutionContext.global) = Props(new BitcoinjWatcher(kit)(ec))
}
class Broadcaster(kit: WalletAppKit) extends Actor with ActorLogging {
override def receive: Receive = {
case tx: Transaction =>
broadcast(tx)
context become waiting(Nil)
}
def waiting(stash: Seq[Transaction]): Receive = {
case BroadcastResult(tx, result) =>
result match {
case Success(_) => log.info(s"broadcast success for txid=${tx.txid}")
case Failure(t) => log.error(t, s"broadcast failure for txid=${tx.txid}: ")
}
stash match {
case head :: rest =>
broadcast(head)
context become waiting(rest)
case Nil => context become receive
}
case tx: Transaction =>
log.info(s"stashing txid=${tx.txid} for broadcast")
context become waiting(stash :+ tx)
}
case class BroadcastResult(tx: Transaction, result: Try[Boolean])
def broadcast(tx: Transaction) = {
Context.propagate(kit.wallet().getContext)
val bitcoinjTx = new org.bitcoinj.core.Transaction(kit.wallet().getParams, Transaction.write(tx))
log.info(s"broadcasting txid=${tx.txid}")
Futures.addCallback(kit.peerGroup().broadcastTransaction(bitcoinjTx).future(), new FutureCallback[BitcoinjTransaction] {
override def onFailure(t: Throwable): Unit = self ! BroadcastResult(tx, Failure(t))
override def onSuccess(v: BitcoinjTransaction): Unit = self ! BroadcastResult(tx, Success(true))
}, context.dispatcher)
}
}

View File

@ -0,0 +1,488 @@
package fr.acinq.eclair.blockchain.electrum
import java.io.InputStream
import java.net.InetSocketAddress
import akka.actor.{Actor, ActorLogging, ActorRef, Stash, Terminated}
import akka.io.{IO, Tcp}
import akka.util.ByteString
import fr.acinq.bitcoin._
import fr.acinq.eclair.Globals
import fr.acinq.eclair.blockchain.CurrentBlockCount
import fr.acinq.eclair.blockchain.bitcoind.rpc.{Error, JsonRPCRequest, JsonRPCResponse}
import org.json4s.JsonAST._
import org.json4s.jackson.JsonMethods
import org.json4s.{DefaultFormats, JInt, JLong, JString}
import org.spongycastle.util.encoders.Hex
import scala.annotation.tailrec
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration._
import scala.util.Random
class ElectrumClient(serverAddresses: Seq[InetSocketAddress]) extends Actor with Stash with ActorLogging {
import ElectrumClient._
import context.system
implicit val formats = DefaultFormats
val newline = "\n"
val connectionFailures = collection.mutable.HashMap.empty[InetSocketAddress, Long]
val version = ServerVersion("2.1.7", "1.1")
// we need to regularly send a ping in order not to get disconnected
context.system.scheduler.schedule(30 seconds, 30 seconds, self, version)
override def unhandled(message: Any): Unit = {
message match {
case _: Tcp.ConnectionClosed =>
val nextAddress = nextPeer()
log.warning(s"connection failed, trying $nextAddress")
self ! Tcp.Connect(nextAddress)
statusListeners.map(_ ! ElectrumDisconnected)
context.system.eventStream.publish(ElectrumDisconnected)
context become disconnected
case Terminated(deadActor) =>
val removeMe = addressSubscriptions collect {
case (address, actor) if actor == deadActor => address
}
addressSubscriptions --= removeMe
val removeMe1 = scriptHashSubscriptions collect {
case (scriptHash, actor) if actor == deadActor => scriptHash
}
scriptHashSubscriptions --= removeMe1
statusListeners -= deadActor
headerSubscriptions -= deadActor
case _: ServerVersion => () // we only handle this when connected
case _: ServerVersionResponse => () // we just ignore these messages, they are used as pings
case _ => log.warning(s"unhandled $message")
}
}
val statusListeners = collection.mutable.HashSet.empty[ActorRef]
def send(connection: ActorRef, request: JsonRPCRequest): Unit = {
import org.json4s.JsonDSL._
import org.json4s._
import org.json4s.jackson.JsonMethods._
log.debug(s"sending $request")
val json = ("method" -> request.method) ~ ("params" -> request.params.map {
case s: String => new JString(s)
case b: BinaryData => new JString(b.toString())
case t: Int => new JInt(t)
case t: Long => new JLong(t)
case t: Double => new JDouble(t)
}) ~ ("id" -> request.id) ~ ("jsonrpc" -> request.jsonrpc)
val serialized = compact(render(json))
val bytes = (serialized + newline).getBytes
connection ! Tcp.Write(ByteString.fromArray(bytes))
}
private def nextPeer() = {
val nextPos = Random.nextInt(serverAddresses.size)
serverAddresses(nextPos)
}
private def updateBlockCount(blockCount: Long) = {
// when synchronizing we don't want to advertise previous blocks
if (Globals.blockCount.get() < blockCount) {
log.debug(s"current blockchain height=$blockCount")
system.eventStream.publish(CurrentBlockCount(blockCount))
Globals.blockCount.set(blockCount)
}
}
val addressSubscriptions = collection.mutable.HashMap.empty[String, Set[ActorRef]]
val scriptHashSubscriptions = collection.mutable.HashMap.empty[BinaryData, Set[ActorRef]]
val headerSubscriptions = collection.mutable.HashSet.empty[ActorRef]
context.system.eventStream.publish(ElectrumDisconnected)
self ! Tcp.Connect(serverAddresses.head)
var reqId = 0L
def receive = disconnected
def disconnected: Receive = {
case c: Tcp.Connect =>
log.info(s"connecting to $c")
IO(Tcp) ! c
case Tcp.Connected(remote, _) =>
log.info(s"connected to $remote")
connectionFailures.clear()
val connection = sender()
connection ! Tcp.Register(self)
val request = version
send(connection, makeRequest(request, "" + reqId))
reqId = reqId + 1
context become waitingForVersion(connection, remote)
case AddStatusListener(actor) => statusListeners += actor
case Tcp.CommandFailed(Tcp.Connect(remoteAddress, _, _, _, _)) =>
val nextAddress = nextPeer()
log.warning(s"connection to $remoteAddress failed, trying $nextAddress")
connectionFailures.put(remoteAddress, connectionFailures.getOrElse(remoteAddress, 0L) + 1L)
val count = connectionFailures.getOrElse(nextAddress, 0L)
val delay = Math.min(Math.pow(2.0, count), 60.0) seconds;
context.system.scheduler.scheduleOnce(delay, self, Tcp.Connect(nextAddress))
}
def waitingForVersion(connection: ActorRef, remote: InetSocketAddress): Receive = {
case Tcp.Received(data) =>
val response = parseResponse(new String(data.toArray)).right.get
val serverVersion = parseJsonResponse(version, response)
log.debug(s"serverVersion=$serverVersion")
val request = HeaderSubscription(self)
send(connection, makeRequest(request, "" + reqId))
headerSubscriptions += self
log.debug("waiting for tip")
reqId = reqId + 1
context become waitingForTip(connection, remote: InetSocketAddress)
case AddStatusListener(actor) => statusListeners += actor
}
def waitingForTip(connection: ActorRef, remote: InetSocketAddress): Receive = {
case Tcp.Received(data) =>
val response = parseResponse(new String(data.toArray)).right.get
val header = parseHeader(response.result)
log.debug(s"connected, tip = ${header.block_hash} $header")
updateBlockCount(header.block_height)
statusListeners.map(_ ! ElectrumReady)
context.system.eventStream.publish(ElectrumConnected)
context become connected(connection, remote, header, "", Map.empty)
case AddStatusListener(actor) => statusListeners += actor
}
def connected(connection: ActorRef, remoteAddress: InetSocketAddress, tip: Header, buffer: String, requests: Map[String, (Request, ActorRef)]): Receive = {
case AddStatusListener(actor) =>
statusListeners += actor
actor ! ElectrumReady
case HeaderSubscription(actor) =>
headerSubscriptions += actor
actor ! HeaderSubscriptionResponse(tip)
context watch actor
case request: Request =>
val curReqId = "" + reqId
send(connection, makeRequest(request, curReqId))
request match {
case AddressSubscription(address, actor) =>
addressSubscriptions.update(address, addressSubscriptions.getOrElse(address, Set()) + actor)
context watch actor
case ScriptHashSubscription(scriptHash, actor) =>
scriptHashSubscriptions.update(scriptHash, scriptHashSubscriptions.getOrElse(scriptHash, Set()) + actor)
context watch actor
case _ => ()
}
reqId = reqId + 1
context become connected(connection, remoteAddress, tip, buffer, requests + (curReqId -> (request, sender())))
case Tcp.Received(data) =>
val buffer1 = buffer + new String(data.toArray)
val (jsons, buffer2) = buffer1.split(newline) match {
case chunks if buffer1.endsWith(newline) => (chunks, "")
case chunks => (chunks.dropRight(1), chunks.last)
}
jsons.map(parseResponse(_)).map(self ! _)
context become connected(connection, remoteAddress, tip, buffer2, requests)
case Right(json: JsonRPCResponse) =>
requests.get(json.id) match {
case Some((request, requestor)) =>
val response = parseJsonResponse(request, json)
log.debug(s"got response for reqId=${json.id} request=$request response=$response")
requestor ! response
case None =>
log.warning(s"could not find requestor for reqId=${json.id} response=$json")
}
context become connected(connection, remoteAddress, tip, buffer, requests - json.id)
case Left(response: HeaderSubscriptionResponse) => headerSubscriptions.map(_ ! response)
case Left(response: AddressSubscriptionResponse) => addressSubscriptions.get(response.address).map(listeners => listeners.map(_ ! response))
case Left(response: ScriptHashSubscriptionResponse) => scriptHashSubscriptions.get(response.scriptHash).map(listeners => listeners.map(_ ! response))
case HeaderSubscriptionResponse(newtip) =>
log.info(s"new tip $newtip")
updateBlockCount(newtip.block_height)
context become connected(connection, remoteAddress, newtip, buffer, requests)
}
}
object ElectrumClient {
def apply(addresses: java.util.List[InetSocketAddress]): ElectrumClient = {
import collection.JavaConversions._
new ElectrumClient(addresses)
}
/**
* Utility function to converts a publicKeyScript to electrum's scripthash
*
* @param publicKeyScript public key script
* @return the hash of the public key script, as used by ElectrumX's hash-based methods
*/
def computeScriptHash(publicKeyScript: BinaryData): BinaryData = Crypto.sha256(publicKeyScript).reverse
// @formatter:off
sealed trait Request
sealed trait Response
case class ServerVersion(clientName: String, protocolVersion: String) extends Request
case class ServerVersionResponse(clientName: String, protocolVersion: String) extends Response
case class GetAddressHistory(address: String) extends Request
case class TransactionHistoryItem(height: Long, tx_hash: BinaryData)
case class GetAddressHistoryResponse(address: String, history: Seq[TransactionHistoryItem]) extends Response
case class GetScriptHashHistory(scriptHash: BinaryData) extends Request
case class GetScriptHashHistoryResponse(scriptHash: BinaryData, history: Seq[TransactionHistoryItem]) extends Response
case class AddressListUnspent(address: String) extends Request
case class UnspentItem(tx_hash: BinaryData, tx_pos: Int, value: Long, height: Long) {
lazy val outPoint = OutPoint(tx_hash.reverse, tx_pos)
}
case class AddressListUnspentResponse(address: String, unspents: Seq[UnspentItem]) extends Response
case class ScriptHashListUnspent(scriptHash: BinaryData) extends Request
case class ScriptHashListUnspentResponse(scriptHash: BinaryData, unspents: Seq[UnspentItem]) extends Response
case class BroadcastTransaction(tx: Transaction) extends Request
case class BroadcastTransactionResponse(tx: Transaction, error: Option[Error]) extends Response
case class GetTransaction(txid: BinaryData) extends Request
case class GetTransactionResponse(tx: Transaction) extends Response
case class GetMerkle(txid: BinaryData, height: Long) extends Request
case class GetMerkleResponse(txid: BinaryData, merkle: Seq[BinaryData], block_height: Long, pos: Int) extends Response {
lazy val root: BinaryData = {
@tailrec
def loop(pos: Int, hashes: Seq[BinaryData]): BinaryData = {
if (hashes.length == 1) hashes(0).reverse
else {
val h = if (pos % 2 == 1) Crypto.hash256(hashes(1) ++ hashes(0)) else Crypto.hash256(hashes(0) ++ hashes(1))
loop(pos / 2, h +: hashes.drop(2))
}
}
loop(pos, BinaryData(txid.reverse) +: merkle.map(b => BinaryData(b.reverse)))
}
}
case class AddressSubscription(address: String, actor: ActorRef) extends Request
case class AddressSubscriptionResponse(address: String, status: String) extends Response
case class ScriptHashSubscription(scriptHash: BinaryData, actor: ActorRef) extends Request
case class ScriptHashSubscriptionResponse(scriptHash: BinaryData, status: String) extends Response
case class HeaderSubscription(actor: ActorRef) extends Request
case class HeaderSubscriptionResponse(header: Header) extends Response
case class Header(block_height: Long, version: Long, prev_block_hash: BinaryData, merkle_root: BinaryData, timestamp: Long, bits: Long, nonce: Long) {
lazy val block_hash: BinaryData = {
val blockHeader = BlockHeader(version, prev_block_hash.reverse, merkle_root.reverse, timestamp, bits, nonce)
blockHeader.hash.reverse
}
}
object Header {
def makeHeader(height: Long, header: BlockHeader) = ElectrumClient.Header(0, header.version, header.hashPreviousBlock, header.hashMerkleRoot, header.time, header.bits, header.nonce)
val RegtestGenesisHeader = makeHeader(0, Block.RegtestGenesisBlock.header)
val TestnetGenesisHeader = makeHeader(0, Block.TestnetGenesisBlock.header)
}
case class TransactionHistory(history: Seq[TransactionHistoryItem]) extends Response
case class AddressStatus(address: String, status: String) extends Response
case class ServerError(request: Request, error: Error) extends Response
case class AddStatusListener(actor: ActorRef) extends Response
sealed trait ElectrumEvent
case object ElectrumConnected extends ElectrumEvent
case object ElectrumReady extends ElectrumEvent
case object ElectrumDisconnected extends ElectrumEvent
// @formatter:on
def parseResponse(input: String): Either[Response, JsonRPCResponse] = {
implicit val formats = DefaultFormats
val json = JsonMethods.parse(new String(input))
json \ "method" match {
case JString(method) =>
// this is a jsonrpc request, i.e. a subscription response
val JArray(params) = json \ "params"
Left(((method, params): @unchecked) match {
case ("blockchain.headers.subscribe", header :: Nil) => HeaderSubscriptionResponse(parseHeader(header))
case ("blockchain.address.subscribe", JString(address) :: JNull :: Nil) => AddressSubscriptionResponse(address, "")
case ("blockchain.address.subscribe", JString(address) :: JString(status) :: Nil) => AddressSubscriptionResponse(address, status)
case ("blockchain.scripthash.subscribe", JString(scriptHashHex) :: JNull :: Nil) => ScriptHashSubscriptionResponse(BinaryData(scriptHashHex), "")
case ("blockchain.scripthash.subscribe", JString(scriptHashHex) :: JString(status) :: Nil) => ScriptHashSubscriptionResponse(BinaryData(scriptHashHex), status)
})
case _ => Right(parseJsonRpcResponse(json))
}
}
def parseJsonRpcResponse(json: JValue): JsonRPCResponse = {
implicit val formats = DefaultFormats
val result = json \ "result"
val error = json \ "error" match {
case JNull => None
case JNothing => None
case other =>
val message = other \ "message" match {
case JString(value) => value
case _ => ""
}
val code = other \ " code" match {
case JInt(value) => value.intValue()
case JLong(value) => value.intValue()
case _ => 0
}
Some(Error(code, message))
}
val id = json \ "id" match {
case JString(value) => value
case JInt(value) => value.toString()
case JLong(value) => value.toString
case _ => ""
}
JsonRPCResponse(result, error, id)
}
def longField(jvalue: JValue, field: String): Long = (jvalue \ field: @unchecked) match {
case JLong(value) => value.longValue()
case JInt(value) => value.longValue()
}
def intField(jvalue: JValue, field: String): Int = (jvalue \ field: @unchecked) match {
case JLong(value) => value.intValue()
case JInt(value) => value.intValue()
}
def parseHeader(json: JValue): Header = {
val block_height = longField(json, "block_height")
val version = longField(json, "version")
val timestamp = longField(json, "timestamp")
val bits = longField(json, "bits")
val nonce = longField(json, "nonce")
val JString(prev_block_hash) = json \ "prev_block_hash"
val JString(merkle_root) = json \ "merkle_root"
Header(block_height, version, prev_block_hash, merkle_root, timestamp, bits, nonce)
}
def makeRequest(request: Request, reqId: String): JsonRPCRequest = request match {
case ServerVersion(clientName, protocolVersion) => JsonRPCRequest(id = reqId, method = "server.version", params = clientName :: protocolVersion :: Nil)
case GetAddressHistory(address) => JsonRPCRequest(id = reqId, method = "blockchain.address.get_history", params = address :: Nil)
case GetScriptHashHistory(scripthash) => JsonRPCRequest(id = reqId, method = "blockchain.scripthash.get_history", params = scripthash.toString() :: Nil)
case AddressListUnspent(address) => JsonRPCRequest(id = reqId, method = "blockchain.address.listunspent", params = address :: Nil)
case ScriptHashListUnspent(scripthash) => JsonRPCRequest(id = reqId, method = "blockchain.scripthash.listunspent", params = scripthash.toString() :: Nil)
case AddressSubscription(address, _) => JsonRPCRequest(id = reqId, method = "blockchain.address.subscribe", params = address :: Nil)
case ScriptHashSubscription(scriptHash, _) => JsonRPCRequest(id = reqId, method = "blockchain.scripthash.subscribe", params = scriptHash.toString() :: Nil)
case BroadcastTransaction(tx) => JsonRPCRequest(id = reqId, method = "blockchain.transaction.broadcast", params = Hex.toHexString(Transaction.write(tx)) :: Nil)
case GetTransaction(txid: BinaryData) => JsonRPCRequest(id = reqId, method = "blockchain.transaction.get", params = txid :: Nil)
case HeaderSubscription(_) => JsonRPCRequest(id = reqId, method = "blockchain.headers.subscribe", params = Nil)
case GetMerkle(txid, height) => JsonRPCRequest(id = reqId, method = "blockchain.transaction.get_merkle", params = txid :: height :: Nil)
}
def parseJsonResponse(request: Request, json: JsonRPCResponse): Response = {
implicit val formats = DefaultFormats
json.error match {
case Some(error) => (request: @unchecked) match {
case BroadcastTransaction(tx) => BroadcastTransactionResponse(tx, Some(error)) // for this request type, error are considered a "normal" response
case _ => ServerError(request, error)
}
case None => (request: @unchecked) match {
case s: ServerVersion =>
val JArray(jitems) = json.result
val JString(clientName) = jitems(0)
val JString(protocolVersion) = jitems(1)
ServerVersionResponse(clientName, protocolVersion)
case GetAddressHistory(address) =>
val JArray(jitems) = json.result
val items = jitems.map(jvalue => {
val JString(tx_hash) = jvalue \ "tx_hash"
val height = longField(jvalue, "height")
TransactionHistoryItem(height, tx_hash)
})
GetAddressHistoryResponse(address, items)
case GetScriptHashHistory(scripthash) =>
val JArray(jitems) = json.result
val items = jitems.map(jvalue => {
val JString(tx_hash) = jvalue \ "tx_hash"
val height = longField(jvalue, "height")
TransactionHistoryItem(height, tx_hash)
})
GetScriptHashHistoryResponse(scripthash, items)
case AddressListUnspent(address) =>
val JArray(jitems) = json.result
val items = jitems.map(jvalue => {
val JString(tx_hash) = jvalue \ "tx_hash"
val tx_pos = intField(jvalue, "tx_pos")
val height = longField(jvalue, "height")
val value = longField(jvalue, "value")
UnspentItem(tx_hash, tx_pos, value, height)
})
AddressListUnspentResponse(address, items)
case ScriptHashListUnspent(scripthash) =>
val JArray(jitems) = json.result
val items = jitems.map(jvalue => {
val JString(tx_hash) = jvalue \ "tx_hash"
val tx_pos = intField(jvalue, "tx_pos")
val height = longField(jvalue, "height")
val value = longField(jvalue, "value")
UnspentItem(tx_hash, tx_pos, value, height)
})
ScriptHashListUnspentResponse(scripthash, items)
case GetTransaction(_) =>
val JString(hex) = json.result
GetTransactionResponse(Transaction.read(hex))
case AddressSubscription(address, _) => json.result match {
case JString(status) => AddressSubscriptionResponse(address, status)
case _ => AddressSubscriptionResponse(address, "")
}
case ScriptHashSubscription(scriptHash, _) => json.result match {
case JString(status) => ScriptHashSubscriptionResponse(scriptHash, status)
case _ => ScriptHashSubscriptionResponse(scriptHash, "")
}
case BroadcastTransaction(tx) =>
val JString(txid) = json.result
require(BinaryData(txid) == tx.txid)
BroadcastTransactionResponse(tx, None)
case GetMerkle(txid, height) =>
val JArray(hashes) = json.result \ "merkle"
val leaves = hashes collect { case JString(value) => BinaryData(value) }
val blockHeight = longField(json.result, "block_height")
val JInt(pos) = json.result \ "pos"
GetMerkleResponse(txid, leaves, blockHeight, pos.toInt)
}
}
}
def readServerAddresses(stream: InputStream): Seq[InetSocketAddress] = try {
val JObject(values) = JsonMethods.parse(stream)
val addresses = values.map {
case (name, fields) =>
val JString(port) = fields \ "t"
new InetSocketAddress(name, port.toInt)
}
val randomized = Random.shuffle(addresses)
randomized
} finally {
stream.close()
}
}

View File

@ -0,0 +1,69 @@
package fr.acinq.eclair.blockchain.electrum
import akka.actor.{ActorRef, ActorSystem}
import akka.pattern.ask
import fr.acinq.bitcoin.{Base58, Base58Check, BinaryData, OP_EQUAL, OP_HASH160, OP_PUSHDATA, Satoshi, Script, Transaction, TxOut}
import fr.acinq.eclair.blockchain.electrum.ElectrumClient.BroadcastTransaction
import fr.acinq.eclair.blockchain.electrum.ElectrumWallet._
import fr.acinq.eclair.blockchain.{EclairWallet, MakeFundingTxResponse}
import grizzled.slf4j.Logging
import scala.concurrent.{ExecutionContext, Future}
class ElectrumEclairWallet(val wallet: ActorRef)(implicit system: ActorSystem, ec: ExecutionContext, timeout: akka.util.Timeout) extends EclairWallet with Logging {
override def getBalance = (wallet ? GetBalance).mapTo[GetBalanceResponse].map(balance => balance.confirmed + balance.unconfirmed)
override def getFinalAddress = (wallet ? GetCurrentReceiveAddress).mapTo[GetCurrentReceiveAddressResponse].map(_.address)
override def makeFundingTx(pubkeyScript: BinaryData, amount: Satoshi, feeRatePerKw: Long) = {
val tx = Transaction(version = 2, txIn = Nil, txOut = TxOut(amount, pubkeyScript) :: Nil, lockTime = 0)
(wallet ? CompleteTransaction(tx, feeRatePerKw)).mapTo[CompleteTransactionResponse].map(response => response match {
case CompleteTransactionResponse(tx1, None) => MakeFundingTxResponse(tx1, 0)
case CompleteTransactionResponse(_, Some(error)) => throw error
})
}
override def commit(tx: Transaction): Future[Boolean] =
(wallet ? BroadcastTransaction(tx)) flatMap {
case ElectrumClient.BroadcastTransactionResponse(tx, None) =>
//tx broadcast successfully: commit tx
wallet ? CommitTransaction(tx)
case ElectrumClient.BroadcastTransactionResponse(tx, Some(error)) if error.message.contains("transaction already in block chain") =>
// tx was already in the blockchain, that's weird but it is OK
wallet ? CommitTransaction(tx)
case ElectrumClient.BroadcastTransactionResponse(_, Some(error)) =>
//tx broadcast failed: cancel tx
logger.error(s"cannot broadcast tx ${tx.txid}: $error")
wallet ? CancelTransaction(tx)
case ElectrumClient.ServerError(ElectrumClient.BroadcastTransaction(tx), error) =>
//tx broadcast failed: cancel tx
logger.error(s"cannot broadcast tx ${tx.txid}: $error")
wallet ? CancelTransaction(tx)
} map {
case CommitTransactionResponse(_) => true
case CancelTransactionResponse(_) => false
}
def sendPayment(amount: Satoshi, address: String, feeRatePerKw: Long): Future[String] = {
val publicKeyScript = Base58Check.decode(address) match {
case (Base58.Prefix.PubkeyAddressTestnet, pubKeyHash) => Script.pay2pkh(pubKeyHash)
case (Base58.Prefix.ScriptAddressTestnet, scriptHash) => OP_HASH160 :: OP_PUSHDATA(scriptHash) :: OP_EQUAL :: Nil
}
val tx = Transaction(version = 2, txIn = Nil, txOut = TxOut(amount, publicKeyScript) :: Nil, lockTime = 0)
(wallet ? CompleteTransaction(tx, feeRatePerKw))
.mapTo[CompleteTransactionResponse]
.flatMap {
case CompleteTransactionResponse(tx, None) => commit(tx).map {
case true => tx.txid.toString()
case false => throw new RuntimeException(s"could not commit tx=${Transaction.write(tx)}")
}
case CompleteTransactionResponse(_, Some(error)) => throw error
}
}
def getMnemonics: Future[Seq[String]] = (wallet ? GetMnemonicCode).mapTo[GetMnemonicCodeResponse].map(_.mnemonics)
override def rollback(tx: Transaction): Future[Boolean] = (wallet ? CancelTransaction(tx)).map(_ => true)
}

View File

@ -0,0 +1,693 @@
package fr.acinq.eclair.blockchain.electrum
import java.io.File
import akka.actor.{ActorRef, LoggingFSM, Props}
import com.google.common.io.Files
import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey}
import fr.acinq.bitcoin.DeterministicWallet.{ExtendedPrivateKey, derivePrivateKey, hardened}
import fr.acinq.bitcoin.{Base58, Base58Check, BinaryData, Block, Crypto, DeterministicWallet, MnemonicCode, OP_PUSHDATA, OutPoint, SIGHASH_ALL, Satoshi, Script, ScriptFlags, ScriptWitness, SigVersion, Transaction, TxIn, TxOut}
import fr.acinq.eclair.blockchain.bitcoind.rpc.Error
import fr.acinq.eclair.blockchain.electrum.ElectrumClient.{GetTransaction, GetTransactionResponse, TransactionHistoryItem, computeScriptHash}
import fr.acinq.eclair.randomBytes
import grizzled.slf4j.Logging
import scala.annotation.tailrec
import scala.util.{Failure, Success, Try}
/**
* Simple electrum wallet
*
* Typical workflow:
*
* client ---- header update ----> wallet
* client ---- status update ----> wallet
* client <--- ask history ----- wallet
* client ---- history ----> wallet
* client <--- ask tx ----- wallet
* client ---- tx ----> wallet
*
* @param mnemonics
* @param client
* @param params
*/
class ElectrumWallet(mnemonics: Seq[String], client: ActorRef, params: ElectrumWallet.WalletParameters) extends LoggingFSM[ElectrumWallet.State, ElectrumWallet.Data] {
import ElectrumWallet._
import params._
val seed = MnemonicCode.toSeed(mnemonics, "")
val master = DeterministicWallet.generate(seed)
val accountMaster = accountKey(master)
val changeMaster = changeKey(master)
client ! ElectrumClient.AddStatusListener(self)
// disconnected --> waitingForTip --> running --
// ^ |
// | |
// --------------------------------------------
startWith(DISCONNECTED, {
val header = chainHash match {
case Block.RegtestGenesisBlock.hash => ElectrumClient.Header.RegtestGenesisHeader
case Block.TestnetGenesisBlock.hash => ElectrumClient.Header.TestnetGenesisHeader
}
val firstAccountKeys = (0 until params.swipeRange).map(i => derivePrivateKey(accountMaster, i)).toVector
val firstChangeKeys = (0 until params.swipeRange).map(i => derivePrivateKey(changeMaster, i)).toVector
val data = Data(params, header, firstAccountKeys, firstChangeKeys)
context.system.eventStream.publish(NewWalletReceiveAddress(data.currentReceiveAddress))
data
})
when(DISCONNECTED) {
case Event(ElectrumClient.ElectrumReady, data) =>
client ! ElectrumClient.HeaderSubscription(self)
goto(WAITING_FOR_TIP) using data
}
when(WAITING_FOR_TIP) {
case Event(ElectrumClient.HeaderSubscriptionResponse(header), data) =>
data.accountKeys.foreach(key => client ! ElectrumClient.ScriptHashSubscription(computeScriptHashFromPublicKey(key.publicKey), self))
data.changeKeys.foreach(key => client ! ElectrumClient.ScriptHashSubscription(computeScriptHashFromPublicKey(key.publicKey), self))
goto(RUNNING) using data.copy(tip = header)
case Event(ElectrumClient.ElectrumDisconnected, data) =>
log.info(s"wallet got disconnected")
goto(DISCONNECTED) using data
}
when(RUNNING) {
case Event(ElectrumClient.HeaderSubscriptionResponse(header), data) if data.tip == header => stay
case Event(ElectrumClient.HeaderSubscriptionResponse(header), data) =>
log.info(s"got new tip ${header.block_hash} at ${header.block_height}")
data.heights.collect {
case (txid, height) if height > 0 =>
val confirmations = computeDepth(header.block_height, height)
context.system.eventStream.publish(TransactionConfidenceChanged(txid, confirmations))
}
stay using data.copy(tip = header)
case Event(ElectrumClient.ScriptHashSubscriptionResponse(scriptHash, status), data) if data.status.get(scriptHash) == Some(status) => stay // we already have it
case Event(ElectrumClient.ScriptHashSubscriptionResponse(scriptHash, status), data) if !data.accountKeyMap.contains(scriptHash) && !data.changeKeyMap.contains(scriptHash) =>
log.warning(s"received status=$status for scriptHash=$scriptHash which does not match any of our keys")
stay
case Event(ElectrumClient.ScriptHashSubscriptionResponse(scriptHash, status), data) if status == "" =>
val data1 = data.copy(status = data.status + (scriptHash -> status)) // empty status, nothing to do
goto(stateName) using data1
case Event(ElectrumClient.ScriptHashSubscriptionResponse(scriptHash, status), data) =>
val key = data.accountKeyMap.getOrElse(scriptHash, data.changeKeyMap(scriptHash))
val isChange = data.changeKeyMap.contains(scriptHash)
log.info(s"received status=$status for scriptHash=$scriptHash key=${segwitAddress(key)} isChange=$isChange")
// let's retrieve the tx history for this key
client ! ElectrumClient.GetScriptHashHistory(scriptHash)
val (newAccountKeys, newChangeKeys) = data.status.get(status) match {
case None =>
// first time this script hash is used, need to generate a new key
val newKey = if (isChange) derivePrivateKey(changeMaster, data.changeKeys.last.path.lastChildNumber + 1) else derivePrivateKey(accountMaster, data.accountKeys.last.path.lastChildNumber + 1)
val newScriptHash = computeScriptHashFromPublicKey(newKey.publicKey)
log.info(s"generated key with index=${newKey.path.lastChildNumber} scriptHash=$newScriptHash key=${segwitAddress(newKey)} isChange=$isChange")
// listens to changes for the newly generated key
client ! ElectrumClient.ScriptHashSubscription(newScriptHash, self)
if (isChange) (data.accountKeys, data.changeKeys :+ newKey) else (data.accountKeys :+ newKey, data.changeKeys)
case Some(_) => (data.accountKeys, data.changeKeys)
}
val data1 = data.copy(
accountKeys = newAccountKeys,
changeKeys = newChangeKeys,
status = data.status + (scriptHash -> status),
pendingHistoryRequests = data.pendingHistoryRequests + scriptHash)
goto(stateName) using data1 // goto instead of stay because we want to fire transitions
case Event(ElectrumClient.GetScriptHashHistoryResponse(scriptHash, history), data) =>
log.debug(s"scriptHash=$scriptHash has history=$history")
val (heights1, pendingTransactionRequests1) = history.foldLeft((data.heights, data.pendingTransactionRequests)) {
case ((heights, hashes), item) if !data.transactions.contains(item.tx_hash) && !data.pendingTransactionRequests.contains(item.tx_hash) =>
// we retrieve the tx if we don't have it and haven't yet requested it
client ! GetTransaction(item.tx_hash)
(heights + (item.tx_hash -> item.height), hashes + item.tx_hash)
case ((heights, hashes), item) =>
// otherwise we just update the height
(heights + (item.tx_hash -> item.height), hashes)
}
// we now have updated height for all our transactions,
heights1.collect {
case (txid, height) =>
val confirmations = if (height <= 0) 0 else computeDepth(data.tip.block_height, height)
(data.heights.get(txid), height) match {
case (None, height) if height <= 0 =>
// height=0 => unconfirmed, height=-1 => unconfirmed and one input is unconfirmed
case (None, height) if height > 0 =>
// first time we get a height for this tx: either it was just confirmed, or we restarted the wallet
context.system.eventStream.publish(TransactionConfidenceChanged(txid, confirmations))
case (Some(previousHeight), height) if previousHeight != height =>
// there was a reorg
context.system.eventStream.publish(TransactionConfidenceChanged(txid, confirmations))
case (Some(previousHeight), height) if previousHeight == height =>
// no reorg, nothing to do
}
}
val data1 = data.copy(heights = heights1, history = data.history + (scriptHash -> history), pendingHistoryRequests = data.pendingHistoryRequests - scriptHash, pendingTransactionRequests = pendingTransactionRequests1)
goto(stateName) using data1 // goto instead of stay because we want to fire transitions
case Event(GetTransactionResponse(tx), data) =>
log.debug(s"received transaction ${tx.txid}")
data.computeTransactionDelta(tx) match {
case Some((received, sent, fee_opt)) =>
log.info(s"successfully connected txid=${tx.txid}")
context.system.eventStream.publish(TransactionReceived(tx, data.computeTransactionDepth(tx.txid), received, sent, fee_opt))
// when we have successfully processed a new tx, we retry all pending txes to see if they can be added now
data.pendingTransactions.foreach(self ! GetTransactionResponse(_))
val data1 = data.copy(transactions = data.transactions + (tx.txid -> tx), pendingTransactionRequests = data.pendingTransactionRequests - tx.txid, pendingTransactions = Nil)
goto(stateName) using data1 // goto instead of stay because we want to fire transitions
case None =>
// missing parents
log.info(s"couldn't connect txid=${tx.txid}")
val data1 = data.copy(pendingTransactions = data.pendingTransactions :+ tx)
stay using data1
}
case Event(CompleteTransaction(tx, feeRatePerKw), data) =>
Try(data.completeTransaction(tx, feeRatePerKw, minimumFee, dustLimit, allowSpendUnconfirmed)) match {
case Success((data1, tx1)) => stay using data1 replying CompleteTransactionResponse(tx1, None)
case Failure(t) => stay replying CompleteTransactionResponse(tx, Some(t))
}
case Event(CommitTransaction(tx), data) =>
log.info(s"committing txid=${tx.txid}")
val data1 = data.commitTransaction(tx)
// we use the initial state to compute the effect of the tx
// note: we know that computeTransactionDelta and the fee will be defined, because we built the tx ourselves so
// we know all the parents
val (received, sent, Some(fee)) = data.computeTransactionDelta(tx).get
// we notify here because the tx won't be downloaded again (it has been added to the state at commit)
context.system.eventStream.publish(TransactionReceived(tx, data1.computeTransactionDepth(tx.txid), received, sent, Some(fee)))
goto(stateName) using data1 replying CommitTransactionResponse(tx) // goto instead of stay because we want to fire transitions
case Event(CancelTransaction(tx), data) =>
log.info(s"cancelling txid=${tx.txid}")
stay using data.cancelTransaction(tx) replying CancelTransactionResponse(tx)
case Event(bc@ElectrumClient.BroadcastTransaction(tx), _) =>
log.info(s"broadcasting txid=${tx.txid}")
client forward bc
stay
case Event(ElectrumClient.ElectrumDisconnected, data) =>
log.info(s"wallet got disconnected")
goto(DISCONNECTED) using data
}
whenUnhandled {
case Event(GetMnemonicCode, _) => stay replying GetMnemonicCodeResponse(mnemonics)
case Event(GetCurrentReceiveAddress, data) => stay replying GetCurrentReceiveAddressResponse(data.currentReceiveAddress)
case Event(GetBalance, data) =>
val (confirmed, unconfirmed) = data.balance
stay replying GetBalanceResponse(confirmed, unconfirmed)
case Event(GetData, data) => stay replying GetDataResponse(data)
case Event(ElectrumClient.BroadcastTransaction(tx), _) => stay replying ElectrumClient.BroadcastTransactionResponse(tx, Some(Error(-1, "wallet is not connected")))
}
onTransition {
case _ -> _ if nextStateData.isReady(params.swipeRange) =>
val ready = nextStateData.readyMessage
log.info(s"wallet is ready with $ready")
context.system.eventStream.publish(ready)
context.system.eventStream.publish(NewWalletReceiveAddress(nextStateData.currentReceiveAddress))
}
initialize()
}
object ElectrumWallet {
// use 32 bytes seed, which will generate a 24 words mnemonic code
val SEED_BYTES_LENGTH = 32
def props(mnemonics: Seq[String], client: ActorRef, params: WalletParameters): Props = Props(new ElectrumWallet(mnemonics, client, params))
def props(file: File, client: ActorRef, params: WalletParameters): Props = {
val entropy: BinaryData = (file.exists(), file.canRead(), file.isFile) match {
case (true, true, true) => Files.toByteArray(file)
case (false, _, _) =>
val buffer = randomBytes(SEED_BYTES_LENGTH)
Files.write(buffer, file)
buffer
case _ => throw new IllegalArgumentException(s"cannot create wallet:$file exist but cannot read from")
}
val mnemonics = MnemonicCode.toMnemonics(entropy)
Props(new ElectrumWallet(mnemonics, client, params))
}
case class WalletParameters(chainHash: BinaryData, minimumFee: Satoshi = Satoshi(2000), dustLimit: Satoshi = Satoshi(546), swipeRange: Int = 10, allowSpendUnconfirmed: Boolean = true)
// @formatter:off
sealed trait State
case object DISCONNECTED extends State
case object WAITING_FOR_TIP extends State
case object RUNNING extends State
sealed trait Request
sealed trait Response
case object GetMnemonicCode extends RuntimeException
case class GetMnemonicCodeResponse(mnemonics: Seq[String]) extends Response
case object GetBalance extends Request
case class GetBalanceResponse(confirmed: Satoshi, unconfirmed: Satoshi) extends Response
case object GetCurrentReceiveAddress extends Request
case class GetCurrentReceiveAddressResponse(address: String) extends Response
case object GetData extends Request
case class GetDataResponse(state: Data) extends Response
case class CompleteTransaction(tx: Transaction, feeRatePerKw: Long) extends Request
case class CompleteTransactionResponse(tx: Transaction, error: Option[Throwable]) extends Response
case class CommitTransaction(tx: Transaction) extends Request
case class CommitTransactionResponse(tx: Transaction) extends Response
case class SendTransaction(tx: Transaction) extends Request
case class SendTransactionReponse(tx: Transaction) extends Response
case class CancelTransaction(tx: Transaction) extends Request
case class CancelTransactionResponse(tx: Transaction) extends Response
case object InsufficientFunds extends Response
case class AmountBelowDustLimit(dustLimit: Satoshi) extends Response
case class GetPrivateKey(address: String) extends Request
case class GetPrivateKeyResponse(address: String, key: Option[ExtendedPrivateKey]) extends Response
sealed trait WalletEvent
/**
*
* @param tx
* @param depth
* @param received
* @param sent
* @param feeOpt is set only when we know it (i.e. for outgoing transactions)
*/
case class TransactionReceived(tx: Transaction, depth: Long, received: Satoshi, sent: Satoshi, feeOpt: Option[Satoshi]) extends WalletEvent
case class TransactionConfidenceChanged(txid: BinaryData, depth: Long) extends WalletEvent
case class NewWalletReceiveAddress(address: String) extends WalletEvent
case class WalletReady(confirmedBalance: Satoshi, unconfirmedBalance: Satoshi, height: Long) extends WalletEvent
// @formatter:on
/**
*
* @param key public key
* @return the address of the p2sh-of-p2wpkh script for this key
*/
def segwitAddress(key: PublicKey): String = {
val script = Script.pay2wpkh(key)
val hash = Crypto.hash160(Script.write(script))
Base58Check.encode(Base58.Prefix.ScriptAddressTestnet, hash)
}
def segwitAddress(key: ExtendedPrivateKey): String = segwitAddress(key.publicKey)
def segwitAddress(key: PrivateKey): String = segwitAddress(key.publicKey)
/**
*
* @param key public key
* @return a p2sh-of-p2wpkh script for this key
*/
def computePublicKeyScript(key: PublicKey) = Script.pay2sh(Script.pay2wpkh(key))
/**
*
* @param key public key
* @return the hash of the public key script for this key, as used by ElectrumX's hash-based methods
*/
def computeScriptHashFromPublicKey(key: PublicKey): BinaryData = Crypto.sha256(Script.write(computePublicKeyScript(key))).reverse
/**
* use BIP49 (and not BIP44) since we use p2sh-of-p2wpkh
*
* @param master master key
* @return the BIP49 account key for this master key: m/49'/1'/0'/0
*/
def accountKey(master: ExtendedPrivateKey) = DeterministicWallet.derivePrivateKey(master, hardened(49) :: hardened(1) :: hardened(0) :: 0L :: Nil)
/**
* use BIP49 (and not BIP44) since we use p2sh-of-p2wpkh
*
* @param master master key
* @return the BIP49 change key for this master key: m/49'/1'/0'/1
*/
def changeKey(master: ExtendedPrivateKey) = DeterministicWallet.derivePrivateKey(master, hardened(49) :: hardened(1) :: hardened(0) :: 1L :: Nil)
def totalAmount(utxos: Seq[Utxo]): Satoshi = Satoshi(utxos.map(_.item.value).sum)
def totalAmount(utxos: Set[Utxo]): Satoshi = totalAmount(utxos.toSeq)
/**
*
* @param weight transaction weight
* @param feeRatePerKw fee rate
* @return the fee for this tx weight
*/
def computeFee(weight: Int, feeRatePerKw: Long): Satoshi = Satoshi((weight * feeRatePerKw) / 1000)
/**
*
* @param txIn transaction input
* @return Some(pubkey) if this tx input spends a p2sh-of-p2wpkh(pub), None otherwise
*/
def extractPubKeySpentFrom(txIn: TxIn): Option[PublicKey] = {
Try {
// we're looking for tx that spend a pay2sh-of-p2wkph output
require(txIn.witness.stack.size == 2)
val sig = txIn.witness.stack(0)
val pub = txIn.witness.stack(1)
val OP_PUSHDATA(script, _) :: Nil = Script.parse(txIn.signatureScript)
val publicKey = PublicKey(pub)
if (Script.write(Script.pay2wpkh(publicKey)) == script) {
Some(publicKey)
} else None
} getOrElse None
}
def computeDepth(currentHeight: Long, txHeight: Long): Long = currentHeight - txHeight + 1
case class Utxo(key: ExtendedPrivateKey, item: ElectrumClient.UnspentItem) {
def outPoint: OutPoint = item.outPoint
}
/**
* Wallet state, which stores data returned by EletrumX servers.
* Most items are indexed by script hash (i.e. by pubkey script sha256 hash).
* Height follow ElectrumX's conventions:
* - h > 0 means that the tx was confirmed at block #h
* - 0 means unconfirmed, but all input are confirmed
* < 0 means unconfirmed, and sonme inputs are unconfirmed as well
*
* @param tip current blockchain tip
* @param accountKeys account keys
* @param changeKeys change keys
* @param status script hash -> status; "" means that the script hash has not been used
* yet
* @param transactions wallet transactions
* @param heights transactions heights
* @param history script hash -> history
* @param locks transactions which lock some of our utxos.
*/
case class Data(tip: ElectrumClient.Header,
accountKeys: Vector[ExtendedPrivateKey],
changeKeys: Vector[ExtendedPrivateKey],
status: Map[BinaryData, String],
transactions: Map[BinaryData, Transaction],
heights: Map[BinaryData, Long],
history: Map[BinaryData, Seq[ElectrumClient.TransactionHistoryItem]],
locks: Set[Transaction],
pendingHistoryRequests: Set[BinaryData],
pendingTransactionRequests: Set[BinaryData],
pendingTransactions: Seq[Transaction]) extends Logging {
lazy val accountKeyMap = accountKeys.map(key => computeScriptHashFromPublicKey(key.publicKey) -> key).toMap
lazy val changeKeyMap = changeKeys.map(key => computeScriptHashFromPublicKey(key.publicKey) -> key).toMap
lazy val firstUnusedAccountKeys = accountKeys.find(key => status.get(computeScriptHashFromPublicKey(key.publicKey)) == Some(""))
lazy val firstUnusedChangeKeys = changeKeys.find(key => status.get(computeScriptHashFromPublicKey(key.publicKey)) == Some(""))
lazy val publicScriptMap = (accountKeys ++ changeKeys).map(key => Script.write(computePublicKeyScript(key.publicKey)) -> key).toMap
lazy val utxos = history.keys.toSeq.map(scriptHash => getUtxos(scriptHash)).flatten
/**
* The wallet is ready if all current keys have an empty status, and we don't have
* any history/tx request pending
* NB: swipeRange * 2 because we have account keys and change keys
*/
def isReady(swipeRange: Int) = status.filter(_._2 == "").size >= swipeRange * 2 && pendingHistoryRequests.isEmpty && pendingTransactionRequests.isEmpty
def readyMessage: WalletReady = {
val (confirmed, unconfirmed) = balance
WalletReady(confirmed, unconfirmed, tip.block_height)
}
/**
*
* @return the current receive key. In most cases it will be a key that has not
* been used yet but it may be possible that we are still looking for
* unused keys and none is available yet. In this case we will return
* the latest account key.
*/
def currentReceiveKey = firstUnusedAccountKeys.headOption.getOrElse {
// bad luck we are still looking for unused keys
// use the first account key
accountKeys.head
}
def currentReceiveAddress = segwitAddress(currentReceiveKey)
/**
*
* @return the current change key. In most cases it will be a key that has not
* been used yet but it may be possible that we are still looking for
* unused keys and none is available yet. In this case we will return
* the latest change key.
*/
def currentChangeKey = firstUnusedChangeKeys.headOption.getOrElse {
// bad luck we are still looking for unused keys
// use the first account key
changeKeys.head
}
def currentChangeAddress = segwitAddress(currentChangeKey)
def isMine(txIn: TxIn): Boolean = extractPubKeySpentFrom(txIn).exists(pub => publicScriptMap.contains(Script.write(computePublicKeyScript(pub))))
def isSpend(txIn: TxIn, publicKey: PublicKey): Boolean = extractPubKeySpentFrom(txIn).contains(publicKey)
/**
*
* @param txIn
* @param scriptHash
* @return true if txIn spends from an address that matches scriptHash
*/
def isSpend(txIn: TxIn, scriptHash: BinaryData): Boolean = extractPubKeySpentFrom(txIn).exists(pub => computeScriptHashFromPublicKey(pub) == scriptHash)
def isReceive(txOut: TxOut, scriptHash: BinaryData): Boolean = publicScriptMap.get(txOut.publicKeyScript).exists(key => computeScriptHashFromPublicKey(key.publicKey) == scriptHash)
def isMine(txOut: TxOut): Boolean = publicScriptMap.contains(txOut.publicKeyScript)
def computeTransactionDepth(txid: BinaryData): Long = heights.get(txid).map(height => if (height > 0) computeDepth(tip.block_height, height) else 0).getOrElse(0)
/**
*
* @param scriptHash script hash
* @return the list of UTXOs for this script hash (including unconfirmed UTXOs)
*/
def getUtxos(scriptHash: BinaryData) = {
history.get(scriptHash) match {
case None => Seq()
case Some(items) if items.isEmpty => Seq()
case Some(items) =>
// this is the private key for this script hash
val key = accountKeyMap.getOrElse(scriptHash, changeKeyMap(scriptHash))
// find all transactions that send to or receive from this script hash
// we use collect because we may not yet have received all transactions in the history
val txs = items collect { case item if transactions.contains(item.tx_hash) => transactions(item.tx_hash) }
// find all tx outputs that send to our script hash
val unspents = items collect { case item if transactions.contains(item.tx_hash) =>
val tx = transactions(item.tx_hash)
val outputs = tx.txOut.zipWithIndex.filter { case (txOut, index) => isReceive(txOut, scriptHash) }
outputs.map { case (txOut, index) => Utxo(key, ElectrumClient.UnspentItem(item.tx_hash, index, txOut.amount.toLong, item.height)) }
} flatten
// and remove the outputs that are being spent. this is needed because we may have unconfirmed UTXOs
// that are spend by unconfirmed transactions
unspents.filterNot(utxo => txs.exists(tx => tx.txIn.exists(_.outPoint == utxo.outPoint)))
}
}
/**
*
* @param scriptHash script hash
* @return the (confirmed, unconfirmed) balance for this script hash. This balance may not
* be up-to-date if we have not received all data we've asked for yet.
*/
def balance(scriptHash: BinaryData): (Satoshi, Satoshi) = {
history.get(scriptHash) match {
case None => (Satoshi(0), Satoshi(0))
case Some(items) if items.isEmpty => (Satoshi(0), Satoshi(0))
case Some(items) =>
val (confirmedItems, unconfirmedItems) = items.partition(_.height > 0)
val confirmedTxs = confirmedItems.collect { case item if transactions.contains(item.tx_hash) => transactions(item.tx_hash) }
val unconfirmedTxs = unconfirmedItems.collect { case item if transactions.contains(item.tx_hash) => transactions(item.tx_hash) }
if (confirmedTxs.size + unconfirmedTxs.size < confirmedItems.size + unconfirmedItems.size) logger.warn(s"we have not received all transactions yet, balance will not be up to date")
def findOurSpentOutputs(txs: Seq[Transaction]): Seq[TxOut] = {
val inputs = txs.map(_.txIn).flatten.filter(txIn => isSpend(txIn, scriptHash))
val spentOutputs = inputs.map(_.outPoint).map(outPoint => transactions.get(outPoint.txid).map(_.txOut(outPoint.index.toInt))).flatten
spentOutputs
}
val confirmedSpents = findOurSpentOutputs(confirmedTxs)
val confirmedReceived = confirmedTxs.map(_.txOut).flatten.filter(txOut => isReceive(txOut, scriptHash))
val unconfirmedSpents = findOurSpentOutputs(unconfirmedTxs)
val unconfirmedReceived = unconfirmedTxs.map(_.txOut).flatten.filter(txOut => isReceive(txOut, scriptHash))
val confirmedBalance = confirmedReceived.map(_.amount).sum - confirmedSpents.map(_.amount).sum
val unconfirmedBalance = unconfirmedReceived.map(_.amount).sum - unconfirmedSpents.map(_.amount).sum
(confirmedBalance, unconfirmedBalance)
}
}
/**
*
* @return the (confirmed, unconfirmed) balance for this wallet. This balance may not
* be up-to-date if we have not received all data we've asked for yet.
*/
lazy val balance: (Satoshi, Satoshi) = {
(accountKeyMap.keys ++ changeKeyMap.keys).map(scriptHash => balance(scriptHash)).foldLeft((Satoshi(0), Satoshi(0))) {
case ((confirmed, unconfirmed), (confirmed1, unconfirmed1)) => (confirmed + confirmed1, unconfirmed + unconfirmed1)
}
}
/**
* Computes the effect of this transaction on the wallet
*
* @param tx input transaction
* @return an option:
* - Some(received, sent, fee) where sent if what the tx spends from us, received is what the tx sends to us,
* and fee is the fee for the tx) tuple where sent if what the tx spends from us, and received is what the tx sends to us
* - None if we are missing one or more parent txs
*/
def computeTransactionDelta(tx: Transaction): Option[(Satoshi, Satoshi, Option[Satoshi])] = {
val ourInputs = tx.txIn.filter(isMine)
// we need to make sure that for all inputs spending an output we control, we already have the parent tx
// (otherwise we can't estimate our balance)
val missingParent = ourInputs.exists(txIn => !transactions.contains(txIn.outPoint.txid))
if (missingParent) {
None
} else {
val sent = ourInputs.map(txIn => transactions(txIn.outPoint.txid).txOut(txIn.outPoint.index.toInt)).map(_.amount).sum
val received = tx.txOut.filter(isMine).map(_.amount).sum
// if all the inputs were ours, we can compute the fee, otherwise we can't
val fee_opt = if (ourInputs.size == tx.txIn.size) Some(sent - tx.txOut.map(_.amount).sum) else None
Some((received, sent, fee_opt))
}
}
/**
*
* @param tx input tx that has no inputs
* @param feeRatePerKw fee rate per kiloweight
* @param minimumFee minimum fee
* @param dustLimit dust limit
* @return a (state, tx) tuple where state has been updated and tx is a complete,
* fully signed transaction that can be broadcast.
* our utxos spent by this tx are locked and won't be available for spending
* until the tx has been cancelled. If the tx is committed, they will be removed
*/
def completeTransaction(tx: Transaction, feeRatePerKw: Long, minimumFee: Satoshi, dustLimit: Satoshi, allowSpendUnconfirmed: Boolean): (Data, Transaction) = {
require(tx.txIn.isEmpty, "cannot complete a tx that already has inputs")
require(feeRatePerKw >= 0, "fee rate cannot be negative")
val amount = tx.txOut.map(_.amount).sum
require(amount > dustLimit, "amount to send is below dust limit")
val fee = {
val estimatedFee = computeFee(700, feeRatePerKw)
if (estimatedFee < minimumFee) minimumFee else estimatedFee
}
@tailrec
def select(chooseFrom: Seq[Utxo], selected: Set[Utxo]): Set[Utxo] = {
if (totalAmount(selected) >= amount + fee) selected
else if (chooseFrom.isEmpty) Set()
else select(chooseFrom.tail, selected + chooseFrom.head)
}
// select utxos that are not locked by pending txs
val lockedOutputs = locks.map(_.txIn.map(_.outPoint)).flatten
val unlocked = utxos.filterNot(utxo => lockedOutputs.contains(utxo.outPoint))
val unlocked1 = if (allowSpendUnconfirmed) unlocked else unlocked.filter(_.item.height > 0)
val selected = select(unlocked1, Set()).toSeq
require(totalAmount(selected) >= amount + fee, "insufficient funds")
// add inputs
var tx1 = tx.copy(txIn = selected.map(utxo => TxIn(utxo.outPoint, Nil, TxIn.SEQUENCE_FINAL)))
// add change output
val change = totalAmount(selected) - amount - fee
if (change >= dustLimit) tx1 = tx1.addOutput(TxOut(change, computePublicKeyScript(currentChangeKey.publicKey)))
// sign
for (i <- 0 until tx1.txIn.size) {
val key = selected(i).key
val sig = Transaction.signInput(tx1, i, Script.pay2pkh(key.publicKey), SIGHASH_ALL, Satoshi(selected(i).item.value), SigVersion.SIGVERSION_WITNESS_V0, key.privateKey)
tx1 = tx1.updateWitness(i, ScriptWitness(sig :: key.publicKey.toBin :: Nil)).updateSigScript(i, OP_PUSHDATA(Script.write(Script.pay2wpkh(key.publicKey))) :: Nil)
}
Transaction.correctlySpends(tx1, selected.map(utxo => utxo.outPoint -> TxOut(Satoshi(utxo.item.value), computePublicKeyScript(utxo.key.publicKey))).toMap, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
val data1 = this.copy(locks = this.locks + tx1)
(data1, tx1)
}
/**
* unlocks input locked by a pending tx. call this method if the tx will not be used after all
*
* @param tx pending transaction
* @return an updated state
*/
def cancelTransaction(tx: Transaction): Data = this.copy(locks = this.locks - tx)
/**
* remove all our utxos spent by this tx. call this method if the tx was broadcast successfully
*
* @param tx pending transaction
* @return an updated state
*/
def commitTransaction(tx: Transaction): Data = {
// HACK! since we base our utxos computation on the history as seen by the electrum server (so that it is
// reorg-proof out of the box), we need to update the history right away if we want to be able to build chained
// unconfirmed transactions. A few seconds later electrum will notify us and the entry will be overwritten.
// Note that we need to take into account both inputs and outputs, because there may be change.
val history1 = (tx.txIn.filter(isMine).map(extractPubKeySpentFrom).flatten.map(computeScriptHashFromPublicKey) ++ tx.txOut.filter(isMine).map(_.publicKeyScript).map(computeScriptHash))
.foldLeft(this.history) {
case (history, scriptHash) =>
val entry = history.get(scriptHash) match {
case None => Seq(TransactionHistoryItem(0, tx.txid))
case Some(items) if items.map(_.tx_hash).contains(tx.txid) => items
case Some(items) => items :+ TransactionHistoryItem(0, tx.txid)
}
history + (scriptHash -> entry)
}
this.copy(locks = this.locks - tx, transactions = this.transactions + (tx.txid -> tx), heights = this.heights + (tx.txid -> 0L), history = history1)
}
}
object Data {
def apply(params: ElectrumWallet.WalletParameters, tip: ElectrumClient.Header, accountKeys: Vector[ExtendedPrivateKey], changeKeys: Vector[ExtendedPrivateKey]): Data
= Data(tip, accountKeys, changeKeys, Map(), Map(), Map(), Map(), Set(), Set(), Set(), Seq())
}
}

View File

@ -0,0 +1,225 @@
package fr.acinq.eclair.blockchain.electrum
import java.net.InetSocketAddress
import akka.actor.{Actor, ActorLogging, ActorRef, ActorSystem, Props, Stash, Terminated}
import fr.acinq.bitcoin.Crypto.PublicKey
import fr.acinq.bitcoin.{BinaryData, Satoshi, Script, Transaction, TxIn, TxOut}
import fr.acinq.eclair.blockchain._
import fr.acinq.eclair.blockchain.electrum.ElectrumClient._
import fr.acinq.eclair.channel.{BITCOIN_FUNDING_DEPTHOK, BITCOIN_FUNDING_SPENT, BITCOIN_PARENT_TX_CONFIRMED}
import fr.acinq.eclair.transactions.Scripts
import fr.acinq.eclair.{Globals, fromShortId}
import scala.collection.SortedMap
class ElectrumWatcher(client: ActorRef) extends Actor with Stash with ActorLogging {
client ! ElectrumClient.AddStatusListener(self)
override def unhandled(message: Any): Unit = message match {
case ParallelGetRequest(announcements) => sender ! ParallelGetResponse(announcements.map {
case c =>
log.info(s"blindly validating channel=$c")
val pubkeyScript = Script.write(Script.pay2wsh(Scripts.multiSig2of2(PublicKey(c.bitcoinKey1), PublicKey(c.bitcoinKey2))))
val (_, _, outputIndex) = fromShortId(c.shortChannelId)
val fakeFundingTx = Transaction(
version = 2,
txIn = Seq.empty[TxIn],
txOut = List.fill(outputIndex + 1)(TxOut(Satoshi(0), pubkeyScript)), // quick and dirty way to be sure that the outputIndex'th output is of the expected format
lockTime = 0)
IndividualResult(c, Some(fakeFundingTx), true)
})
case _ => log.warning(s"unhandled message $message")
}
def receive = disconnected(Set.empty, Nil, SortedMap.empty)
def disconnected(watches: Set[Watch], publishQueue: Seq[PublishAsap], block2tx: SortedMap[Long, Seq[Transaction]]): Receive = {
case ElectrumClient.ElectrumReady =>
client ! ElectrumClient.HeaderSubscription(self)
case ElectrumClient.HeaderSubscriptionResponse(header) =>
watches.map(self ! _)
publishQueue.map(self ! _)
context become running(header, Set(), Map(), block2tx, Nil)
case watch: Watch => context become disconnected(watches + watch, publishQueue, block2tx)
case publish: PublishAsap => context become disconnected(watches, publishQueue :+ publish, block2tx)
}
def running(tip: ElectrumClient.Header, watches: Set[Watch], scriptHashStatus: Map[BinaryData, String], block2tx: SortedMap[Long, Seq[Transaction]], sent: Seq[Transaction]): Receive = {
case ElectrumClient.HeaderSubscriptionResponse(newtip) if tip == newtip => ()
case ElectrumClient.HeaderSubscriptionResponse(newtip) =>
log.info(s"new tip: ${newtip.block_hash} $newtip")
watches collect {
case watch: WatchConfirmed =>
val scriptHash = computeScriptHash(watch.publicKeyScript)
client ! ElectrumClient.GetScriptHashHistory(scriptHash)
}
val toPublish = block2tx.filterKeys(_ <= newtip.block_height)
toPublish.values.flatten.foreach(tx => self ! PublishAsap(tx))
context become running(newtip, watches, scriptHashStatus, block2tx -- toPublish.keys, sent)
case watch: Watch if watches.contains(watch) => ()
case watch@WatchSpent(_, txid, outputIndex, publicKeyScript, _) =>
val scriptHash = computeScriptHash(publicKeyScript)
log.info(s"added watch-spent on output=$txid:$outputIndex scriptHash=$scriptHash")
client ! ElectrumClient.ScriptHashSubscription(scriptHash, self)
context.watch(watch.channel)
context become running(tip, watches + watch, scriptHashStatus, block2tx, sent)
case watch@WatchSpentBasic(_, txid, outputIndex, publicKeyScript, _) =>
val scriptHash = computeScriptHash(publicKeyScript)
log.info(s"added watch-spent-basic on output=$txid:$outputIndex scriptHash=$scriptHash")
client ! ElectrumClient.ScriptHashSubscription(scriptHash, self)
context.watch(watch.channel)
context become running(tip, watches + watch, scriptHashStatus, block2tx, sent)
case watch@WatchConfirmed(_, txid, publicKeyScript, _, _) =>
val scriptHash = computeScriptHash(publicKeyScript)
log.info(s"added watch-confirmed on txid=$txid scriptHash=$scriptHash")
client ! ElectrumClient.GetScriptHashHistory(scriptHash)
context.watch(watch.channel)
context become running(tip, watches + watch, scriptHashStatus, block2tx, sent)
case Terminated(actor) =>
val watches1 = watches.filterNot(_.channel == actor)
context become running(tip, watches1, scriptHashStatus, block2tx, sent)
case ElectrumClient.ScriptHashSubscriptionResponse(scriptHash, status) =>
scriptHashStatus.get(scriptHash) match {
case Some(s) if s == status => log.debug(s"already have status=$status for scriptHash=$scriptHash")
case _ if status.isEmpty => log.info(s"empty status for scriptHash=$scriptHash")
case _ =>
log.info(s"new status=$status for scriptHash=$scriptHash")
client ! ElectrumClient.GetScriptHashHistory(scriptHash)
}
context become running(tip, watches, scriptHashStatus + (scriptHash -> status), block2tx, sent)
case ElectrumClient.GetScriptHashHistoryResponse(_, history) =>
// this is for WatchSpent/WatchSpentBasic
history.filter(_.height >= 0).map(item => client ! ElectrumClient.GetTransaction(item.tx_hash))
// this is for WatchConfirmed
history.collect {
case ElectrumClient.TransactionHistoryItem(height, tx_hash) if height > 0 => watches.collect {
case WatchConfirmed(_, txid, _, minDepth, _) if txid == tx_hash =>
val confirmations = tip.block_height - height + 1
log.info(s"txid=$txid was confirmed at height=$height and now has confirmations=$confirmations (currentHeight=${tip.block_height})")
if (confirmations >= minDepth) {
// we need to get the tx position in the block
client ! GetMerkle(tx_hash, height)
}
}
}
case ElectrumClient.GetMerkleResponse(tx_hash, _, height, pos) =>
val confirmations = tip.block_height - height + 1
val triggered = watches.collect {
case w@WatchConfirmed(channel, txid, _, minDepth, event) if txid == tx_hash && confirmations >= minDepth =>
log.info(s"txid=$txid had confirmations=$confirmations in block=$height pos=$pos")
channel ! WatchEventConfirmed(event, height.toInt, pos)
w
}
context become running(tip, watches -- triggered, scriptHashStatus, block2tx, sent)
case ElectrumClient.GetTransactionResponse(spendingTx) =>
val triggered = spendingTx.txIn.map(_.outPoint).flatMap(outPoint => watches.collect {
case WatchSpent(channel, txid, pos, _, event) if txid == outPoint.txid && pos == outPoint.index.toInt =>
log.info(s"output $txid:$pos spent by transaction ${spendingTx.txid}")
channel ! WatchEventSpent(event, spendingTx)
// NB: WatchSpent are permanent because we need to detect multiple spending of the funding tx
// They are never cleaned up but it is not a big deal for now (1 channel == 1 watch)
None
case w@WatchSpentBasic(channel, txid, pos, _, event) if txid == outPoint.txid && pos == outPoint.index.toInt =>
log.info(s"output $txid:$pos spent by transaction ${spendingTx.txid}")
channel ! WatchEventSpentBasic(event)
Some(w)
}).flatten
context become running(tip, watches -- triggered, scriptHashStatus, block2tx, sent)
case PublishAsap(tx) =>
val blockCount = Globals.blockCount.get()
val cltvTimeout = Scripts.cltvTimeout(tx)
val csvTimeout = Scripts.csvTimeout(tx)
if (csvTimeout > 0) {
require(tx.txIn.size == 1, s"watcher only supports tx with 1 input, this tx has ${tx.txIn.size} inputs")
val parentTxid = tx.txIn(0).outPoint.txid
log.info(s"txid=${tx.txid} has a relative timeout of $csvTimeout blocks, watching parenttxid=$parentTxid tx=${Transaction.write(tx)}")
val parentPublicKeyScript = WatchConfirmed.extractPublicKeyScript(tx.txIn.head.witness)
self ! WatchConfirmed(self, parentTxid, parentPublicKeyScript, minDepth = 1, BITCOIN_PARENT_TX_CONFIRMED(tx))
} else if (cltvTimeout > blockCount) {
log.info(s"delaying publication of txid=${tx.txid} until block=$cltvTimeout (curblock=$blockCount)")
val block2tx1 = block2tx.updated(cltvTimeout, block2tx.getOrElse(cltvTimeout, Seq.empty[Transaction]) :+ tx)
context become running(tip, watches, scriptHashStatus, block2tx1, sent)
} else {
log.info(s"publishing tx=${Transaction.write(tx)}")
client ! BroadcastTransaction(tx)
context become running(tip, watches, scriptHashStatus, block2tx, sent :+ tx)
}
case WatchEventConfirmed(BITCOIN_PARENT_TX_CONFIRMED(tx), blockHeight, _) =>
log.info(s"parent tx of txid=${tx.txid} has been confirmed")
val blockCount = Globals.blockCount.get()
val csvTimeout = Scripts.csvTimeout(tx)
val absTimeout = blockHeight + csvTimeout
if (absTimeout > blockCount) {
log.info(s"delaying publication of txid=${tx.txid} until block=$absTimeout (curblock=$blockCount)")
val block2tx1 = block2tx.updated(absTimeout, block2tx.getOrElse(absTimeout, Seq.empty[Transaction]) :+ tx)
context become running(tip, watches, scriptHashStatus, block2tx1, sent)
} else {
log.info(s"publishing tx=${Transaction.write(tx)}")
client ! BroadcastTransaction(tx)
context become running(tip, watches, scriptHashStatus, block2tx, sent :+ tx)
}
case ElectrumClient.BroadcastTransactionResponse(tx, error_opt) =>
error_opt match {
case None => log.info(s"broadcast succeeded for txid=${tx.txid} tx=${Transaction.write(tx)}")
case Some(error) if error.message.contains("transaction already in block chain") => log.info(s"broadcast ignored for txid=${tx.txid} tx=${Transaction.write(tx)} (tx was already in blockchain)")
case Some(error) => log.error(s"broadcast failed for txid=${tx.txid} tx=${Transaction.write(tx)} with error=$error")
}
context become running(tip, watches, scriptHashStatus, block2tx, sent diff Seq(tx))
case ElectrumClient.ElectrumDisconnected =>
// we remember watches and keep track of tx that have not yet been published
// we also re-send the txes that we previsouly sent but hadn't yet received the confirmation
context become disconnected(watches, sent.map(PublishAsap(_)), block2tx)
}
}
object ElectrumWatcher extends App {
val system = ActorSystem()
class Root extends Actor with ActorLogging {
val serverAddresses = Seq(new InetSocketAddress("localhost", 51000), new InetSocketAddress("localhost", 51001))
val client = context.actorOf(Props(new ElectrumClient(serverAddresses)), "client")
client ! ElectrumClient.AddStatusListener(self)
override def unhandled(message: Any): Unit = {
super.unhandled(message)
log.warning(s"unhandled message $message")
}
def receive = {
case ElectrumClient.ElectrumReady =>
log.info(s"starting watcher")
context become running(context.actorOf(Props(new ElectrumWatcher(client)), "watcher"))
}
def running(watcher: ActorRef): Receive = {
case watch: Watch => watcher forward watch
}
}
val root = system.actorOf(Props[Root], "root")
val scanner = new java.util.Scanner(System.in)
while (true) {
val tx = Transaction.read(scanner.nextLine())
root ! WatchSpent(root, tx.txid, 0, tx.txOut(0).publicKeyScript, BITCOIN_FUNDING_SPENT)
root ! WatchConfirmed(root, tx.txid, tx.txOut(0).publicKeyScript, 4L, BITCOIN_FUNDING_DEPTHOK)
}
}

View File

@ -0,0 +1,44 @@
package fr.acinq.eclair.blockchain.fee
import fr.acinq.bitcoin.Btc
import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinJsonRPCClient
import org.json4s.JsonAST.{JDouble, JInt}
import scala.concurrent.{ExecutionContext, Future}
/**
* Created by PM on 09/07/2017.
*/
class BitcoinCoreFeeProvider(rpcClient: BitcoinJsonRPCClient, defaultFeerates: FeeratesPerByte)(implicit ec: ExecutionContext) extends FeeProvider {
/**
* We need this to keep commitment tx fees in sync with the state of the network
*
* @param nBlocks number of blocks until tx is confirmed
* @return the current
*/
def estimateSmartFee(nBlocks: Int): Future[Long] =
rpcClient.invoke("estimatesmartfee", nBlocks).map(json => {
json \ "feerate" match {
case JDouble(feerate) => Btc(feerate).toLong
case JInt(feerate) if feerate.toLong < 0 => feerate.toLong
case JInt(feerate) => Btc(feerate.toLong).toLong
}
})
override def getFeerates: Future[FeeratesPerByte] = for {
block_1 <- estimateSmartFee(1)
blocks_2 <- estimateSmartFee(2)
blocks_6 <- estimateSmartFee(6)
blocks_12 <- estimateSmartFee(12)
blocks_36 <- estimateSmartFee(36)
blocks_72 <- estimateSmartFee(72)
} yield FeeratesPerByte(
block_1 = if (block_1 > 0) block_1 else defaultFeerates.block_1,
blocks_2 = if (blocks_2 > 0) blocks_2 else defaultFeerates.blocks_2,
blocks_6 = if (blocks_6 > 0) blocks_6 else defaultFeerates.blocks_6,
blocks_12 = if (blocks_12 > 0) blocks_12 else defaultFeerates.blocks_12,
blocks_36 = if (blocks_36 > 0) blocks_36 else defaultFeerates.blocks_36,
blocks_72 = if (blocks_72 > 0) blocks_72 else defaultFeerates.blocks_72)
}

View File

@ -0,0 +1,12 @@
package fr.acinq.eclair.blockchain.fee
import scala.concurrent.Future
/**
* Created by PM on 09/07/2017.
*/
class ConstantFeeProvider(feerates: FeeratesPerByte) extends FeeProvider {
override def getFeerates: Future[FeeratesPerByte] = Future.successful(feerates)
}

View File

@ -0,0 +1,66 @@
package fr.acinq.eclair.blockchain.fee
import akka.actor.ActorSystem
import akka.http.scaladsl.Http
import akka.http.scaladsl.model._
import akka.http.scaladsl.unmarshalling.Unmarshal
import akka.stream.ActorMaterializer
import de.heikoseeberger.akkahttpjson4s.Json4sSupport._
import org.json4s.JsonAST.{JArray, JInt, JValue}
import org.json4s.{DefaultFormats, jackson}
import scala.concurrent.{ExecutionContext, Future}
/**
* Created by PM on 16/11/2017.
*/
class EarnDotComFeeProvider(implicit system: ActorSystem, ec: ExecutionContext) extends FeeProvider {
import EarnDotComFeeProvider._
implicit val materializer = ActorMaterializer()
val httpClient = Http(system)
implicit val serialization = jackson.Serialization
implicit val formats = DefaultFormats
override def getFeerates: Future[FeeratesPerByte] =
for {
httpRes <- httpClient.singleRequest(HttpRequest(uri = Uri("https://bitcoinfees.earn.com/api/v1/fees/list"), method = HttpMethods.GET))
json <- Unmarshal(httpRes).to[JValue]
feeRanges = parseFeeRanges(json)
} yield extractFeerates(feeRanges)
}
object EarnDotComFeeProvider {
case class FeeRange(minFee: Long, maxFee: Long, memCount: Long, minDelay: Long, maxDelay: Long)
def parseFeeRanges(json: JValue): Seq[FeeRange] = {
val JArray(items) = json \ "fees"
items.map(item => {
val JInt(minFee) = item \ "minFee"
val JInt(maxFee) = item \ "maxFee"
val JInt(memCount) = item \ "memCount"
val JInt(minDelay) = item \ "minDelay"
val JInt(maxDelay) = item \ "maxDelay"
FeeRange(minFee = minFee.toLong, maxFee = maxFee.toLong, memCount = memCount.toLong, minDelay = minDelay.toLong, maxDelay = maxDelay.toLong)
})
}
def extractFeerate(feeRanges: Seq[FeeRange], maxBlockDelay: Int): Long = {
// first we keep only fee ranges with a max block delay below the limit
val belowLimit = feeRanges.filter(_.maxDelay <= maxBlockDelay)
// out of all the remaining fee ranges, we select the one with the minimum higher bound
belowLimit.minBy(_.maxFee).maxFee
}
def extractFeerates(feeRanges: Seq[FeeRange]): FeeratesPerByte =
FeeratesPerByte(
block_1 = extractFeerate(feeRanges, 1),
blocks_2 = extractFeerate(feeRanges, 2),
blocks_6 = extractFeerate(feeRanges, 6),
blocks_12 = extractFeerate(feeRanges, 12),
blocks_36 = extractFeerate(feeRanges, 36),
blocks_72 = extractFeerate(feeRanges, 72))
}

View File

@ -0,0 +1,20 @@
package fr.acinq.eclair.blockchain.fee
import scala.concurrent.{ExecutionContext, Future}
/**
* This provider will try all child providers in sequence, until one of them works
*/
class FallbackFeeProvider(providers: Seq[FeeProvider])(implicit ec: ExecutionContext) extends FeeProvider {
require(providers.size >= 1, "need at least one fee provider")
def getFeerates(fallbacks: Seq[FeeProvider]): Future[FeeratesPerByte] =
fallbacks match {
case last +: Nil => last.getFeerates
case head +: remaining => head.getFeerates.recoverWith { case _ => getFeerates(remaining) }
}
override def getFeerates: Future[FeeratesPerByte] = getFeerates(providers)
}

View File

@ -0,0 +1,42 @@
package fr.acinq.eclair.blockchain.fee
import fr.acinq.eclair.feerateByte2Kw
import scala.concurrent.Future
/**
* Created by PM on 09/07/2017.
*/
trait FeeProvider {
def getFeerates: Future[FeeratesPerByte]
}
case class FeeratesPerByte(block_1: Long, blocks_2: Long, blocks_6: Long, blocks_12: Long, blocks_36: Long, blocks_72: Long)
case class FeeratesPerKw(block_1: Long, blocks_2: Long, blocks_6: Long, blocks_12: Long, blocks_36: Long, blocks_72: Long)
object FeeratesPerKw {
def apply(feerates: FeeratesPerByte): FeeratesPerKw = FeeratesPerKw(
block_1 = feerateByte2Kw(feerates.block_1),
blocks_2 = feerateByte2Kw(feerates.blocks_2),
blocks_6 = feerateByte2Kw(feerates.blocks_6),
blocks_12 = feerateByte2Kw(feerates.blocks_12),
blocks_36 = feerateByte2Kw(feerates.blocks_36),
blocks_72 = feerateByte2Kw(feerates.blocks_72))
/**
* Used in tests
*
* @param feeratePerKw
* @return
*/
def single(feeratePerKw: Long): FeeratesPerKw = FeeratesPerKw(
block_1 = feeratePerKw,
blocks_2 = feeratePerKw,
blocks_6 = feeratePerKw,
blocks_12 = feeratePerKw,
blocks_36 = feeratePerKw,
blocks_72 = feeratePerKw)
}

View File

@ -9,6 +9,7 @@ import akka.actor.{Actor, ActorLogging, ActorRef}
/**
* Purpose of this actor is to be an alias for its origin actor.
* It allows to reference the using {{{system.actorSelection()}}} with a meaningful name
*
* @param origin aliased actor
*/
class AliasActor(origin: ActorRef) extends Actor with ActorLogging {

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,23 @@
package fr.acinq.eclair.channel
import akka.actor.ActorRef
import fr.acinq.bitcoin.BinaryData
import fr.acinq.bitcoin.Crypto.PublicKey
/**
* Created by PM on 17/08/2016.
*/
trait ChannelEvent
case class ChannelCreated(channel: ActorRef, peer: ActorRef, remoteNodeId: PublicKey, isFunder: Boolean, temporaryChannelId: BinaryData) extends ChannelEvent
case class ChannelRestored(channel: ActorRef, peer: ActorRef, remoteNodeId: PublicKey, isFunder: Boolean, channelId: BinaryData, currentData: HasCommitments) extends ChannelEvent
case class ChannelIdAssigned(channel: ActorRef, temporaryChannelId: BinaryData, channelId: BinaryData) extends ChannelEvent
case class ShortChannelIdAssigned(channel: ActorRef, channelId: BinaryData, shortChannelId: Long) extends ChannelEvent
case class ChannelStateChanged(channel: ActorRef, peer: ActorRef, remoteNodeId: PublicKey, previousState: State, currentState: State, currentData: Data) extends ChannelEvent
case class ChannelSignatureReceived(channel: ActorRef, Commitments: Commitments) extends ChannelEvent

View File

@ -0,0 +1,43 @@
package fr.acinq.eclair.channel
import fr.acinq.bitcoin.BinaryData
import fr.acinq.eclair.UInt64
/**
* Created by PM on 11/04/2017.
*/
class ChannelException(channelId: BinaryData, message: String) extends RuntimeException(message)
// @formatter:off
case class DebugTriggeredException (channelId: BinaryData) extends ChannelException(channelId, "debug-mode triggered failure")
case class ChannelReserveTooHigh (channelId: BinaryData, channelReserveSatoshis: Long, reserveToFundingRatio: Double, maxReserveToFundingRatio: Double) extends ChannelException(channelId, s"channelReserveSatoshis too high: reserve=$channelReserveSatoshis fundingRatio=$reserveToFundingRatio maxFundingRatio=$maxReserveToFundingRatio")
case class ClosingInProgress (channelId: BinaryData) extends ChannelException(channelId, "cannot send new htlcs, closing in progress")
case class ClosingAlreadyInProgress (channelId: BinaryData) extends ChannelException(channelId, "closing already in progress")
case class CannotCloseWithUnsignedOutgoingHtlcs(channelId: BinaryData) extends ChannelException(channelId, "cannot close when there are unsigned outgoing htlcs")
case class ChannelUnavailable (channelId: BinaryData) extends ChannelException(channelId, "channel is unavailable (offline or closing)")
case class InvalidFinalScript (channelId: BinaryData) extends ChannelException(channelId, "invalid final script")
case class HtlcTimedout (channelId: BinaryData) extends ChannelException(channelId, s"one or more htlcs timed out")
case class FeerateTooDifferent (channelId: BinaryData, localFeeratePerKw: Long, remoteFeeratePerKw: Long) extends ChannelException(channelId, s"local/remote feerates are too different: remoteFeeratePerKw=$remoteFeeratePerKw localFeeratePerKw=$localFeeratePerKw")
case class InvalidCloseSignature (channelId: BinaryData) extends ChannelException(channelId, "cannot verify their close signature")
case class InvalidCommitmentSignature (channelId: BinaryData) extends ChannelException(channelId, "invalid commitment signature")
case class ForcedLocalCommit (channelId: BinaryData, reason: String) extends ChannelException(channelId, s"forced local commit: reason")
case class UnexpectedHtlcId (channelId: BinaryData, expected: Long, actual: Long) extends ChannelException(channelId, s"unexpected htlc id: expected=$expected actual=$actual")
case class InvalidPaymentHash (channelId: BinaryData) extends ChannelException(channelId, "invalid payment hash")
case class ExpiryTooSmall (channelId: BinaryData, minimum: Long, actual: Long, blockCount: Long) extends ChannelException(channelId, s"expiry too small: required=$minimum actual=$actual blockCount=$blockCount")
case class ExpiryCannotBeInThePast (channelId: BinaryData, expiry: Long, blockCount: Long) extends ChannelException(channelId, s"expiry can't be in the past: expiry=$expiry blockCount=$blockCount")
case class HtlcValueTooSmall (channelId: BinaryData, minimum: Long, actual: Long) extends ChannelException(channelId, s"htlc value too small: minimum=$minimum actual=$actual")
case class HtlcValueTooHighInFlight (channelId: BinaryData, maximum: UInt64, actual: UInt64) extends ChannelException(channelId, s"in-flight htlcs hold too much value: maximum=$maximum actual=$actual")
case class TooManyAcceptedHtlcs (channelId: BinaryData, maximum: Long) extends ChannelException(channelId, s"too many accepted htlcs: maximum=$maximum")
case class InsufficientFunds (channelId: BinaryData, amountMsat: Long, missingSatoshis: Long, reserveSatoshis: Long, feesSatoshis: Long) extends ChannelException(channelId, s"insufficient funds: missingSatoshis=$missingSatoshis reserveSatoshis=$reserveSatoshis fees=$feesSatoshis")
case class InvalidHtlcPreimage (channelId: BinaryData, id: Long) extends ChannelException(channelId, s"invalid htlc preimage for htlc id=$id")
case class UnknownHtlcId (channelId: BinaryData, id: Long) extends ChannelException(channelId, s"unknown htlc id=$id")
case class FundeeCannotSendUpdateFee (channelId: BinaryData) extends ChannelException(channelId, s"only the funder should send update_fee messages")
case class CannotAffordFees (channelId: BinaryData, missingSatoshis: Long, reserveSatoshis: Long, feesSatoshis: Long) extends ChannelException(channelId, s"can't pay the fee: missingSatoshis=$missingSatoshis reserveSatoshis=$reserveSatoshis feesSatoshis=$feesSatoshis")
case class CannotSignWithoutChanges (channelId: BinaryData) extends ChannelException(channelId, "cannot sign when there are no changes")
case class CannotSignBeforeRevocation (channelId: BinaryData) extends ChannelException(channelId, "cannot sign until next revocation hash is received")
case class UnexpectedRevocation (channelId: BinaryData) extends ChannelException(channelId, "received unexpected RevokeAndAck message")
case class InvalidRevocation (channelId: BinaryData) extends ChannelException(channelId, "invalid revocation")
case class CommitmentSyncError (channelId: BinaryData) extends ChannelException(channelId, "commitment sync error")
case class RevocationSyncError (channelId: BinaryData) extends ChannelException(channelId, "revocation sync error")
case class InvalidFailureCode (channelId: BinaryData) extends ChannelException(channelId, "UpdateFailMalformedHtlc message doesn't have BADONION bit set")
// @formatter:on

View File

@ -0,0 +1,198 @@
package fr.acinq.eclair.channel
import akka.actor.ActorRef
import fr.acinq.bitcoin.Crypto.{Point, PrivateKey, PublicKey, Scalar}
import fr.acinq.bitcoin.{BinaryData, OutPoint, Transaction}
import fr.acinq.eclair.UInt64
import fr.acinq.eclair.crypto.Sphinx
import fr.acinq.eclair.transactions.CommitmentSpec
import fr.acinq.eclair.transactions.Transactions.CommitTx
import fr.acinq.eclair.wire.{AcceptChannel, AnnouncementSignatures, ClosingSigned, FailureMessage, FundingCreated, FundingLocked, FundingSigned, Init, OpenChannel, Shutdown, UpdateAddHtlc}
/**
* Created by PM on 20/05/2016.
*/
// @formatter:off
/*
.d8888b. 88888888888 d8888 88888888888 8888888888 .d8888b.
d88P Y88b 888 d88888 888 888 d88P Y88b
Y88b. 888 d88P888 888 888 Y88b.
"Y888b. 888 d88P 888 888 8888888 "Y888b.
"Y88b. 888 d88P 888 888 888 "Y88b.
"888 888 d88P 888 888 888 "888
Y88b d88P 888 d8888888888 888 888 Y88b d88P
"Y8888P" 888 d88P 888 888 8888888888 "Y8888P"
*/
sealed trait State
case object WAIT_FOR_INIT_INTERNAL extends State
case object WAIT_FOR_OPEN_CHANNEL extends State
case object WAIT_FOR_ACCEPT_CHANNEL extends State
case object WAIT_FOR_FUNDING_INTERNAL extends State
case object WAIT_FOR_FUNDING_CREATED extends State
case object WAIT_FOR_FUNDING_SIGNED extends State
case object WAIT_FOR_FUNDING_PUBLISHED extends State
case object WAIT_FOR_FUNDING_CONFIRMED extends State
case object WAIT_FOR_FUNDING_LOCKED extends State
case object NORMAL extends State
case object SHUTDOWN extends State
case object NEGOTIATING extends State
case object CLOSING extends State
case object CLOSED extends State
case object OFFLINE extends State
case object SYNCING extends State
case object ERR_FUNDING_PUBLISH_FAILED extends State
case object ERR_FUNDING_LOST extends State
case object ERR_FUNDING_TIMEOUT extends State
case object ERR_INFORMATION_LEAK extends State
/*
8888888888 888 888 8888888888 888b 888 88888888888 .d8888b.
888 888 888 888 8888b 888 888 d88P Y88b
888 888 888 888 88888b 888 888 Y88b.
8888888 Y88b d88P 8888888 888Y88b 888 888 "Y888b.
888 Y88b d88P 888 888 Y88b888 888 "Y88b.
888 Y88o88P 888 888 Y88888 888 "888
888 Y888P 888 888 Y8888 888 Y88b d88P
8888888888 Y8P 8888888888 888 Y888 888 "Y8888P"
*/
case class INPUT_INIT_FUNDER(temporaryChannelId: BinaryData, fundingSatoshis: Long, pushMsat: Long, initialFeeratePerKw: Long, localParams: LocalParams, remote: ActorRef, remoteInit: Init, channelFlags: Byte)
case class INPUT_INIT_FUNDEE(temporaryChannelId: BinaryData, localParams: LocalParams, remote: ActorRef, remoteInit: Init)
case object INPUT_CLOSE_COMPLETE_TIMEOUT // when requesting a mutual close, we wait for as much as this timeout, then unilateral close
case object INPUT_PUBLISH_LOCALCOMMIT // used in tests
case object INPUT_DISCONNECTED
case class INPUT_RECONNECTED(remote: ActorRef)
case class INPUT_RESTORED(data: HasCommitments)
sealed trait BitcoinEvent
case object BITCOIN_FUNDING_PUBLISH_FAILED extends BitcoinEvent
case object BITCOIN_FUNDING_DEPTHOK extends BitcoinEvent
case object BITCOIN_FUNDING_DEEPLYBURIED extends BitcoinEvent
case object BITCOIN_FUNDING_LOST extends BitcoinEvent
case object BITCOIN_FUNDING_TIMEOUT extends BitcoinEvent
case object BITCOIN_FUNDING_SPENT extends BitcoinEvent
case object BITCOIN_OUTPUT_SPENT extends BitcoinEvent
case class BITCOIN_TX_CONFIRMED(tx: Transaction) extends BitcoinEvent
case class BITCOIN_FUNDING_EXTERNAL_CHANNEL_SPENT(shortChannelId: Long) extends BitcoinEvent
case class BITCOIN_PARENT_TX_CONFIRMED(childTx: Transaction) extends BitcoinEvent
/*
.d8888b. .d88888b. 888b d888 888b d888 d8888 888b 888 8888888b. .d8888b.
d88P Y88b d88P" "Y88b 8888b d8888 8888b d8888 d88888 8888b 888 888 "Y88b d88P Y88b
888 888 888 888 88888b.d88888 88888b.d88888 d88P888 88888b 888 888 888 Y88b.
888 888 888 888Y88888P888 888Y88888P888 d88P 888 888Y88b 888 888 888 "Y888b.
888 888 888 888 Y888P 888 888 Y888P 888 d88P 888 888 Y88b888 888 888 "Y88b.
888 888 888 888 888 Y8P 888 888 Y8P 888 d88P 888 888 Y88888 888 888 "888
Y88b d88P Y88b. .d88P 888 " 888 888 " 888 d8888888888 888 Y8888 888 .d88P Y88b d88P
"Y8888P" "Y88888P" 888 888 888 888 d88P 888 888 Y888 8888888P" "Y8888P"
*/
sealed trait Command
final case class CMD_ADD_HTLC(amountMsat: Long, paymentHash: BinaryData, expiry: Long, onion: BinaryData = Sphinx.LAST_PACKET.serialize, upstream_opt: Option[UpdateAddHtlc] = None, commit: Boolean = false) extends Command
final case class CMD_FULFILL_HTLC(id: Long, r: BinaryData, commit: Boolean = false) extends Command
final case class CMD_FAIL_HTLC(id: Long, reason: Either[BinaryData, FailureMessage], commit: Boolean = false) extends Command
final case class CMD_FAIL_MALFORMED_HTLC(id: Long, onionHash: BinaryData, failureCode: Int, commit: Boolean = false) extends Command
final case class CMD_UPDATE_FEE(feeratePerKw: Long, commit: Boolean = false) extends Command
case object CMD_SIGN extends Command
final case class CMD_CLOSE(scriptPubKey: Option[BinaryData]) extends Command
case object CMD_GETSTATE extends Command
case object CMD_GETSTATEDATA extends Command
case object CMD_GETINFO extends Command
final case class RES_GETINFO(nodeid: BinaryData, channelId: BinaryData, state: State, data: Data)
/*
8888888b. d8888 88888888888 d8888
888 "Y88b d88888 888 d88888
888 888 d88P888 888 d88P888
888 888 d88P 888 888 d88P 888
888 888 d88P 888 888 d88P 888
888 888 d88P 888 888 d88P 888
888 .d88P d8888888888 888 d8888888888
8888888P" d88P 888 888 d88P 888
*/
sealed trait Data
case object Nothing extends Data
trait HasCommitments extends Data {
def commitments: Commitments
def channelId = commitments.channelId
}
case class LocalCommitPublished(commitTx: Transaction, claimMainDelayedOutputTx: Option[Transaction], htlcSuccessTxs: List[Transaction], htlcTimeoutTxs: List[Transaction], claimHtlcDelayedTx: List[Transaction], spent: Map[OutPoint, BinaryData])
case class RemoteCommitPublished(commitTx: Transaction, claimMainOutputTx: Option[Transaction], claimHtlcSuccessTxs: List[Transaction], claimHtlcTimeoutTxs: List[Transaction], spent: Map[OutPoint, BinaryData])
case class RevokedCommitPublished(commitTx: Transaction, claimMainOutputTx: Option[Transaction], mainPenaltyTx: Option[Transaction], claimHtlcTimeoutTxs: List[Transaction], htlcTimeoutTxs: List[Transaction], htlcPenaltyTxs: List[Transaction], spent: Map[OutPoint, BinaryData])
final case class DATA_WAIT_FOR_OPEN_CHANNEL(initFundee: INPUT_INIT_FUNDEE) extends Data
final case class DATA_WAIT_FOR_ACCEPT_CHANNEL(initFunder: INPUT_INIT_FUNDER, lastSent: OpenChannel) extends Data
final case class DATA_WAIT_FOR_FUNDING_INTERNAL(temporaryChannelId: BinaryData, localParams: LocalParams, remoteParams: RemoteParams, fundingSatoshis: Long, pushMsat: Long, initialFeeratePerKw: Long, remoteFirstPerCommitmentPoint: Point, lastSent: OpenChannel) extends Data
final case class DATA_WAIT_FOR_FUNDING_CREATED(temporaryChannelId: BinaryData, localParams: LocalParams, remoteParams: RemoteParams, fundingSatoshis: Long, pushMsat: Long, initialFeeratePerKw: Long, remoteFirstPerCommitmentPoint: Point, channelFlags: Byte, lastSent: AcceptChannel) extends Data
final case class DATA_WAIT_FOR_FUNDING_SIGNED(channelId: BinaryData, localParams: LocalParams, remoteParams: RemoteParams, fundingTx: Transaction, localSpec: CommitmentSpec, localCommitTx: CommitTx, remoteCommit: RemoteCommit, channelFlags: Byte, lastSent: FundingCreated) extends Data
final case class DATA_WAIT_FOR_FUNDING_CONFIRMED(commitments: Commitments, deferred: Option[FundingLocked], lastSent: Either[FundingCreated, FundingSigned]) extends Data with HasCommitments
final case class DATA_WAIT_FOR_FUNDING_LOCKED(commitments: Commitments, lastSent: FundingLocked) extends Data with HasCommitments
final case class DATA_NORMAL(commitments: Commitments,
shortChannelId: Option[Long],
localAnnouncementSignatures: Option[AnnouncementSignatures],
localShutdown: Option[Shutdown],
remoteShutdown: Option[Shutdown]) extends Data with HasCommitments
final case class DATA_SHUTDOWN(commitments: Commitments,
localShutdown: Shutdown, remoteShutdown: Shutdown) extends Data with HasCommitments
final case class DATA_NEGOTIATING(commitments: Commitments,
localShutdown: Shutdown, remoteShutdown: Shutdown, localClosingSigned: ClosingSigned) extends Data with HasCommitments
final case class DATA_CLOSING(commitments: Commitments,
mutualClosePublished: Option[Transaction] = None,
localCommitPublished: Option[LocalCommitPublished] = None,
remoteCommitPublished: Option[RemoteCommitPublished] = None,
nextRemoteCommitPublished: Option[RemoteCommitPublished] = None,
revokedCommitPublished: List[RevokedCommitPublished] = Nil) extends Data with HasCommitments {
require(mutualClosePublished.isDefined || localCommitPublished.isDefined || remoteCommitPublished.isDefined || nextRemoteCommitPublished.isDefined || revokedCommitPublished.size > 0, "there should be at least one tx published in this state")
}
final case class LocalParams(nodeId: PublicKey,
dustLimitSatoshis: Long,
maxHtlcValueInFlightMsat: UInt64,
channelReserveSatoshis: Long,
htlcMinimumMsat: Long,
toSelfDelay: Int,
maxAcceptedHtlcs: Int,
fundingPrivKey: PrivateKey,
revocationSecret: Scalar,
paymentKey: Scalar,
delayedPaymentKey: Scalar,
htlcKey: Scalar,
defaultFinalScriptPubKey: BinaryData,
shaSeed: BinaryData,
isFunder: Boolean,
globalFeatures: BinaryData,
localFeatures: BinaryData) {
// precomputed for performance reasons
val paymentBasepoint = paymentKey.toPoint
val delayedPaymentBasepoint = delayedPaymentKey.toPoint
val revocationBasepoint = revocationSecret.toPoint
val htlcBasepoint = htlcKey.toPoint
}
final case class RemoteParams(nodeId: PublicKey,
dustLimitSatoshis: Long,
maxHtlcValueInFlightMsat: UInt64,
channelReserveSatoshis: Long,
htlcMinimumMsat: Long,
toSelfDelay: Int,
maxAcceptedHtlcs: Int,
fundingPubKey: PublicKey,
revocationBasepoint: Point,
paymentBasepoint: Point,
delayedPaymentBasepoint: Point,
htlcBasepoint: Point,
globalFeatures: BinaryData,
localFeatures: BinaryData)
object ChannelFlags {
val AnnounceChannel = 0x01.toByte
val Empty = 0x00.toByte
}
// @formatter:on

View File

@ -0,0 +1,524 @@
package fr.acinq.eclair.channel
import fr.acinq.bitcoin.Crypto.{Point, PrivateKey, sha256}
import fr.acinq.bitcoin.{BinaryData, Crypto, Satoshi, Transaction}
import fr.acinq.eclair.crypto.{Generators, ShaChain, Sphinx}
import fr.acinq.eclair.payment.Origin
import fr.acinq.eclair.transactions.Transactions._
import fr.acinq.eclair.transactions._
import fr.acinq.eclair.wire._
import fr.acinq.eclair.{Globals, UInt64}
import grizzled.slf4j.Logging
// @formatter:off
case class LocalChanges(proposed: List[UpdateMessage], signed: List[UpdateMessage], acked: List[UpdateMessage]) {
def all: List[UpdateMessage] = proposed ++ signed ++ acked
}
case class RemoteChanges(proposed: List[UpdateMessage], acked: List[UpdateMessage], signed: List[UpdateMessage])
case class Changes(ourChanges: LocalChanges, theirChanges: RemoteChanges)
case class HtlcTxAndSigs(txinfo: TransactionWithInputInfo, localSig: BinaryData, remoteSig: BinaryData)
case class PublishableTxs(commitTx: CommitTx, htlcTxsAndSigs: List[HtlcTxAndSigs])
case class LocalCommit(index: Long, spec: CommitmentSpec, publishableTxs: PublishableTxs)
case class RemoteCommit(index: Long, spec: CommitmentSpec, txid: BinaryData, remotePerCommitmentPoint: Point)
case class WaitingForRevocation(nextRemoteCommit: RemoteCommit, sent: CommitSig, sentAfterLocalCommitIndex: Long, reSignAsap: Boolean = false)
// @formatter:on
/**
* about remoteNextCommitInfo:
* we either:
* - have built and signed their next commit tx with their next revocation hash which can now be discarded
* - have their next per-commitment point
* So, when we've signed and sent a commit message and are waiting for their revocation message,
* theirNextCommitInfo is their next commit tx. The rest of the time, it is their next per-commitment point
*/
case class Commitments(localParams: LocalParams, remoteParams: RemoteParams,
channelFlags: Byte,
localCommit: LocalCommit, remoteCommit: RemoteCommit,
localChanges: LocalChanges, remoteChanges: RemoteChanges,
localNextHtlcId: Long, remoteNextHtlcId: Long,
originChannels: Map[Long, Origin], // for outgoing htlcs relayed through us, the id of the previous channel
remoteNextCommitInfo: Either[WaitingForRevocation, Point],
commitInput: InputInfo,
remotePerCommitmentSecrets: ShaChain, channelId: BinaryData) {
def hasNoPendingHtlcs: Boolean = localCommit.spec.htlcs.isEmpty && remoteCommit.spec.htlcs.isEmpty && remoteNextCommitInfo.isRight
def hasTimedoutOutgoingHtlcs(blockheight: Long): Boolean =
localCommit.spec.htlcs.exists(htlc => htlc.direction == OUT && blockheight >= htlc.add.expiry) ||
remoteCommit.spec.htlcs.exists(htlc => htlc.direction == IN && blockheight >= htlc.add.expiry)
def addLocalProposal(proposal: UpdateMessage): Commitments = Commitments.addLocalProposal(this, proposal)
def addRemoteProposal(proposal: UpdateMessage): Commitments = Commitments.addRemoteProposal(this, proposal)
def announceChannel: Boolean = (channelFlags & 0x01) != 0
}
object Commitments extends Logging {
/**
* add a change to our proposed change list
*
* @param commitments
* @param proposal
* @return an updated commitment instance
*/
private def addLocalProposal(commitments: Commitments, proposal: UpdateMessage): Commitments =
commitments.copy(localChanges = commitments.localChanges.copy(proposed = commitments.localChanges.proposed :+ proposal))
private def addRemoteProposal(commitments: Commitments, proposal: UpdateMessage): Commitments =
commitments.copy(remoteChanges = commitments.remoteChanges.copy(proposed = commitments.remoteChanges.proposed :+ proposal))
/**
*
* @param commitments current commitments
* @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(commitments: Commitments, cmd: CMD_ADD_HTLC, origin: Origin): Either[ChannelException, (Commitments, UpdateAddHtlc)] = {
if (cmd.paymentHash.size != 32) {
return Left(InvalidPaymentHash(commitments.channelId))
}
val blockCount = Globals.blockCount.get()
if (cmd.expiry <= blockCount) {
return Left(ExpiryCannotBeInThePast(commitments.channelId, cmd.expiry, blockCount))
}
if (cmd.amountMsat < commitments.remoteParams.htlcMinimumMsat) {
return Left(HtlcValueTooSmall(commitments.channelId, minimum = commitments.remoteParams.htlcMinimumMsat, actual = cmd.amountMsat))
}
// let's compute the current commitment *as seen by them* with this change taken into account
val add = UpdateAddHtlc(commitments.channelId, commitments.localNextHtlcId, cmd.amountMsat, cmd.paymentHash, cmd.expiry, cmd.onion)
// we increment the local htlc index and add an entry to the origins map
val commitments1 = addLocalProposal(commitments, add).copy(localNextHtlcId = commitments.localNextHtlcId + 1, originChannels = commitments.originChannels + (add.id -> origin))
// 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 = commitments1.remoteNextCommitInfo.left.toOption.map(_.nextRemoteCommit).getOrElse(commitments1.remoteCommit)
val reduced = CommitmentSpec.reduce(remoteCommit1.spec, commitments1.remoteChanges.acked, commitments1.localChanges.proposed)
val htlcValueInFlight = UInt64(reduced.htlcs.map(_.add.amountMsat).sum)
if (htlcValueInFlight > commitments1.remoteParams.maxHtlcValueInFlightMsat) {
// TODO: this should be a specific UPDATE error
return Left(HtlcValueTooHighInFlight(commitments.channelId, maximum = commitments1.remoteParams.maxHtlcValueInFlightMsat, actual = htlcValueInFlight))
}
// the HTLC we are about to create is outgoing, but from their point of view it is incoming
val acceptedHtlcs = reduced.htlcs.count(_.direction == IN)
if (acceptedHtlcs > commitments1.remoteParams.maxAcceptedHtlcs) {
return Left(TooManyAcceptedHtlcs(commitments.channelId, maximum = commitments1.remoteParams.maxAcceptedHtlcs))
}
// 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 funder remote doesn't pay the fees
val fees = if (commitments1.localParams.isFunder) Transactions.commitTxFee(Satoshi(commitments1.remoteParams.dustLimitSatoshis), reduced).amount else 0
val missing = reduced.toRemoteMsat / 1000 - commitments1.remoteParams.channelReserveSatoshis - fees
if (missing < 0) {
return Left(InsufficientFunds(commitments.channelId, amountMsat = cmd.amountMsat, missingSatoshis = -1 * missing, reserveSatoshis = commitments1.remoteParams.channelReserveSatoshis, feesSatoshis = fees))
}
Right(commitments1, add)
}
def receiveAdd(commitments: Commitments, add: UpdateAddHtlc): Commitments = {
if (add.id != commitments.remoteNextHtlcId) {
throw UnexpectedHtlcId(commitments.channelId, expected = commitments.remoteNextHtlcId, actual = add.id)
}
if (add.paymentHash.size != 32) {
throw InvalidPaymentHash(commitments.channelId)
}
val blockCount = Globals.blockCount.get()
// we need a reasonable amount of time to pull the funds before the sender can get refunded
val minExpiry = blockCount + 3
if (add.expiry < minExpiry) {
throw ExpiryTooSmall(commitments.channelId, minimum = minExpiry, actual = add.expiry, blockCount = blockCount)
}
if (add.amountMsat < commitments.localParams.htlcMinimumMsat) {
throw HtlcValueTooSmall(commitments.channelId, minimum = commitments.localParams.htlcMinimumMsat, actual = add.amountMsat)
}
// let's compute the current commitment *as seen by us* including this change
val commitments1 = addRemoteProposal(commitments, add).copy(remoteNextHtlcId = commitments.remoteNextHtlcId + 1)
val reduced = CommitmentSpec.reduce(commitments1.localCommit.spec, commitments1.localChanges.acked, commitments1.remoteChanges.proposed)
val htlcValueInFlight = UInt64(reduced.htlcs.map(_.add.amountMsat).sum)
if (htlcValueInFlight > commitments1.localParams.maxHtlcValueInFlightMsat) {
throw HtlcValueTooHighInFlight(commitments.channelId, maximum = commitments1.localParams.maxHtlcValueInFlightMsat, actual = htlcValueInFlight)
}
val acceptedHtlcs = reduced.htlcs.count(_.direction == IN)
if (acceptedHtlcs > commitments1.localParams.maxAcceptedHtlcs) {
throw TooManyAcceptedHtlcs(commitments.channelId, maximum = commitments1.localParams.maxAcceptedHtlcs)
}
// 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 = if (commitments1.localParams.isFunder) 0 else Transactions.commitTxFee(Satoshi(commitments1.localParams.dustLimitSatoshis), reduced).amount
val missing = reduced.toRemoteMsat / 1000 - commitments1.localParams.channelReserveSatoshis - fees
if (missing < 0) {
throw InsufficientFunds(commitments.channelId, amountMsat = add.amountMsat, missingSatoshis = -1 * missing, reserveSatoshis = commitments1.localParams.channelReserveSatoshis, feesSatoshis = fees)
}
commitments1
}
def getHtlcCrossSigned(commitments: Commitments, directionRelativeToLocal: Direction, htlcId: Long): Option[UpdateAddHtlc] = {
val remoteSigned = commitments.localCommit.spec.htlcs.find(htlc => htlc.direction == directionRelativeToLocal && htlc.add.id == htlcId)
val localSigned = commitments.remoteNextCommitInfo.left.toOption.map(_.nextRemoteCommit).getOrElse(commitments.remoteCommit)
.spec.htlcs.find(htlc => htlc.direction == directionRelativeToLocal.opposite && htlc.add.id == htlcId)
for {
htlc_out <- remoteSigned
htlc_in <- localSigned
} yield htlc_in.add
}
def sendFulfill(commitments: Commitments, cmd: CMD_FULFILL_HTLC): (Commitments, UpdateFulfillHtlc) =
getHtlcCrossSigned(commitments, IN, cmd.id) match {
case Some(htlc) if commitments.localChanges.proposed.exists {
case u: UpdateFulfillHtlc if htlc.id == u.id => true
case u: UpdateFailHtlc if htlc.id == u.id => true
case u: UpdateFailMalformedHtlc if htlc.id == u.id => true
case _ => false
} =>
// we have already sent a fail/fulfill for this htlc
throw UnknownHtlcId(commitments.channelId, cmd.id)
case Some(htlc) if htlc.paymentHash == sha256(cmd.r) =>
val fulfill = UpdateFulfillHtlc(commitments.channelId, cmd.id, cmd.r)
val commitments1 = addLocalProposal(commitments, fulfill)
(commitments1, fulfill)
case Some(htlc) => throw InvalidHtlcPreimage(commitments.channelId, cmd.id)
case None => throw UnknownHtlcId(commitments.channelId, cmd.id)
}
def receiveFulfill(commitments: Commitments, fulfill: UpdateFulfillHtlc): Either[Commitments, (Commitments, Origin)] =
getHtlcCrossSigned(commitments, OUT, fulfill.id) match {
case Some(htlc) if htlc.paymentHash == sha256(fulfill.paymentPreimage) => Right((addRemoteProposal(commitments, fulfill), commitments.originChannels(fulfill.id)))
case Some(htlc) => throw InvalidHtlcPreimage(commitments.channelId, fulfill.id)
case None => throw UnknownHtlcId(commitments.channelId, fulfill.id)
}
def sendFail(commitments: Commitments, cmd: CMD_FAIL_HTLC, nodeSecret: PrivateKey): (Commitments, UpdateFailHtlc) =
getHtlcCrossSigned(commitments, IN, cmd.id) match {
case Some(htlc) if commitments.localChanges.proposed.exists {
case u: UpdateFulfillHtlc if htlc.id == u.id => true
case u: UpdateFailHtlc if htlc.id == u.id => true
case u: UpdateFailMalformedHtlc if htlc.id == u.id => true
case _ => false
} =>
// we have already sent a fail/fulfill for this htlc
throw UnknownHtlcId(commitments.channelId, cmd.id)
case Some(htlc) =>
// we need the shared secret to build the error packet
val sharedSecret = Sphinx.parsePacket(nodeSecret, htlc.paymentHash, htlc.onionRoutingPacket).sharedSecret
val reason = cmd.reason match {
case Left(forwarded) => Sphinx.forwardErrorPacket(forwarded, sharedSecret)
case Right(failure) => Sphinx.createErrorPacket(sharedSecret, failure)
}
val fail = UpdateFailHtlc(commitments.channelId, cmd.id, reason)
val commitments1 = addLocalProposal(commitments, fail)
(commitments1, fail)
case None => throw UnknownHtlcId(commitments.channelId, cmd.id)
}
def sendFailMalformed(commitments: Commitments, cmd: CMD_FAIL_MALFORMED_HTLC): (Commitments, UpdateFailMalformedHtlc) = {
// BADONION bit must be set in failure_code
if ((cmd.failureCode & FailureMessageCodecs.BADONION) == 0) {
throw InvalidFailureCode(commitments.channelId)
}
getHtlcCrossSigned(commitments, IN, cmd.id) match {
case Some(htlc) if commitments.localChanges.proposed.exists {
case u: UpdateFulfillHtlc if htlc.id == u.id => true
case u: UpdateFailHtlc if htlc.id == u.id => true
case u: UpdateFailMalformedHtlc if htlc.id == u.id => true
case _ => false
} =>
// we have already sent a fail/fulfill for this htlc
throw UnknownHtlcId(commitments.channelId, cmd.id)
case Some(htlc) =>
val fail = UpdateFailMalformedHtlc(commitments.channelId, cmd.id, cmd.onionHash, cmd.failureCode)
val commitments1 = addLocalProposal(commitments, fail)
(commitments1, fail)
case None => throw UnknownHtlcId(commitments.channelId, cmd.id)
}
}
def receiveFail(commitments: Commitments, fail: UpdateFailHtlc): Either[Commitments, (Commitments, Origin)] =
getHtlcCrossSigned(commitments, OUT, fail.id) match {
case Some(htlc) => Right((addRemoteProposal(commitments, fail), commitments.originChannels(fail.id)))
case None => throw UnknownHtlcId(commitments.channelId, fail.id)
}
def receiveFailMalformed(commitments: Commitments, fail: UpdateFailMalformedHtlc): Either[Commitments, (Commitments, Origin)] = {
// A receiving node MUST fail the channel if the BADONION bit in failure_code is not set for update_fail_malformed_htlc.
if ((fail.failureCode & FailureMessageCodecs.BADONION) == 0) {
throw InvalidFailureCode(commitments.channelId)
}
getHtlcCrossSigned(commitments, OUT, fail.id) match {
case Some(htlc) => Right((addRemoteProposal(commitments, fail), commitments.originChannels(fail.id)))
case None => throw UnknownHtlcId(commitments.channelId, fail.id)
}
}
def sendFee(commitments: Commitments, cmd: CMD_UPDATE_FEE): (Commitments, UpdateFee) = {
if (!commitments.localParams.isFunder) {
throw FundeeCannotSendUpdateFee(commitments.channelId)
}
// let's compute the current commitment *as seen by them* with this change taken into account
val fee = UpdateFee(commitments.channelId, cmd.feeratePerKw)
val commitments1 = addLocalProposal(commitments, fee)
val reduced = CommitmentSpec.reduce(commitments1.remoteCommit.spec, commitments1.remoteChanges.acked, commitments1.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 funder remote doesn't pay the fees
val fees = Transactions.commitTxFee(Satoshi(commitments1.remoteParams.dustLimitSatoshis), reduced).amount
val missing = reduced.toRemoteMsat / 1000 - commitments1.remoteParams.channelReserveSatoshis - fees
if (missing < 0) {
throw CannotAffordFees(commitments.channelId, missingSatoshis = -1 * missing, reserveSatoshis = commitments1.localParams.channelReserveSatoshis, feesSatoshis = fees)
}
(commitments1, fee)
}
def receiveFee(commitments: Commitments, fee: UpdateFee, maxFeerateMismatch: Double): Commitments = {
if (commitments.localParams.isFunder) {
throw FundeeCannotSendUpdateFee(commitments.channelId)
}
val localFeeratePerKw = Globals.feeratesPerKw.get.block_1
if (Helpers.isFeeDiffTooHigh(fee.feeratePerKw, localFeeratePerKw, maxFeerateMismatch)) {
throw FeerateTooDifferent(commitments.channelId, localFeeratePerKw = localFeeratePerKw, remoteFeeratePerKw = fee.feeratePerKw)
}
// NB: we check that the funder can afford this new fee even if spec allows to do it at next signature
// It is easier to do it here because under certain (race) conditions spec allows a lower-than-normal fee to be paid,
// and it would be tricky to check if the conditions are met at signing
// (it also means that we need to check the fee of the initial commitment tx somewhere)
// let's compute the current commitment *as seen by us* including this change
val commitments1 = addRemoteProposal(commitments, fee)
val reduced = CommitmentSpec.reduce(commitments1.localCommit.spec, commitments1.localChanges.acked, commitments1.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 = Transactions.commitTxFee(Satoshi(commitments1.remoteParams.dustLimitSatoshis), reduced).amount
val missing = reduced.toRemoteMsat / 1000 - commitments1.localParams.channelReserveSatoshis - fees
if (missing < 0) {
throw CannotAffordFees(commitments.channelId, missingSatoshis = -1 * missing, reserveSatoshis = commitments1.localParams.channelReserveSatoshis, feesSatoshis = fees)
}
commitments1
}
def localHasUnsignedOutgoingHtlcs(commitments: Commitments): Boolean = commitments.localChanges.proposed.collectFirst { case u: UpdateAddHtlc => u }.isDefined
def remoteHasUnsignedOutgoingHtlcs(commitments: Commitments): Boolean = commitments.remoteChanges.proposed.collectFirst { case u: UpdateAddHtlc => u }.isDefined
def localHasChanges(commitments: Commitments): Boolean = commitments.remoteChanges.acked.size > 0 || commitments.localChanges.proposed.size > 0
def remoteHasChanges(commitments: Commitments): Boolean = commitments.localChanges.acked.size > 0 || commitments.remoteChanges.proposed.size > 0
def revocationPreimage(seed: BinaryData, index: Long): BinaryData = ShaChain.shaChainFromSeed(seed, 0xFFFFFFFFFFFFFFFFL - index)
def revocationHash(seed: BinaryData, index: Long): BinaryData = Crypto.sha256(revocationPreimage(seed, index))
def sendCommit(commitments: Commitments): (Commitments, CommitSig) = {
import commitments._
commitments.remoteNextCommitInfo match {
case Right(_) if !localHasChanges(commitments) =>
throw CannotSignWithoutChanges(commitments.channelId)
case Right(remoteNextPerCommitmentPoint) =>
// remote commitment will includes all local changes + remote acked changes
val spec = CommitmentSpec.reduce(remoteCommit.spec, remoteChanges.acked, localChanges.proposed)
val (remoteCommitTx, htlcTimeoutTxs, htlcSuccessTxs) = makeRemoteTxs(remoteCommit.index + 1, localParams, remoteParams, commitInput, remoteNextPerCommitmentPoint, spec)
val sig = Transactions.sign(remoteCommitTx, localParams.fundingPrivKey)
val sortedHtlcTxs: Seq[TransactionWithInputInfo] = (htlcTimeoutTxs ++ htlcSuccessTxs).sortBy(_.input.outPoint.index)
val htlcKey = Generators.derivePrivKey(localParams.htlcKey, remoteNextPerCommitmentPoint)
val htlcSigs = sortedHtlcTxs.map(Transactions.sign(_, htlcKey))
// don't sign if they don't get paid
val commitSig = CommitSig(
channelId = commitments.channelId,
signature = sig,
htlcSignatures = htlcSigs.toList
)
val commitments1 = commitments.copy(
remoteNextCommitInfo = Left(WaitingForRevocation(RemoteCommit(remoteCommit.index + 1, spec, remoteCommitTx.tx.txid, remoteNextPerCommitmentPoint), commitSig, commitments.localCommit.index)),
localChanges = localChanges.copy(proposed = Nil, signed = localChanges.proposed),
remoteChanges = remoteChanges.copy(acked = Nil, signed = remoteChanges.acked))
(commitments1, commitSig)
case Left(_) =>
throw CannotSignBeforeRevocation(commitments.channelId)
}
}
def receiveCommit(commitments: Commitments, commit: CommitSig): (Commitments, RevokeAndAck) = {
import commitments._
// 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
// ourCommit.index + 1 -> our next revocation hash, used by *them* to build the sig we've just received, and which
// is about to become our current revocation hash
// ourCommit.index + 2 -> which is about to become our next revocation hash
// 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
if (!remoteHasChanges(commitments))
throw CannotSignWithoutChanges(commitments.channelId)
// check that their signature is valid
// signatures are now optional in the commit message, and will be sent only if the other party is actually
// receiving money i.e its commit tx has one output for them
val spec = CommitmentSpec.reduce(localCommit.spec, localChanges.acked, remoteChanges.proposed)
val localPerCommitmentPoint = Generators.perCommitPoint(localParams.shaSeed, commitments.localCommit.index + 1)
val (localCommitTx, htlcTimeoutTxs, htlcSuccessTxs) = makeLocalTxs(localCommit.index + 1, localParams, remoteParams, commitInput, localPerCommitmentPoint, spec)
val sig = Transactions.sign(localCommitTx, localParams.fundingPrivKey)
// TODO: should we have optional sig? (original comment: this tx will NOT be signed if our output is empty)
// no need to compute htlc sigs if commit sig doesn't check out
val signedCommitTx = Transactions.addSigs(localCommitTx, localParams.fundingPrivKey.publicKey, remoteParams.fundingPubKey, sig, commit.signature)
if (Transactions.checkSpendable(signedCommitTx).isFailure) {
throw InvalidCommitmentSignature(commitments.channelId)
}
val sortedHtlcTxs: Seq[TransactionWithInputInfo] = (htlcTimeoutTxs ++ htlcSuccessTxs).sortBy(_.input.outPoint.index)
require(commit.htlcSignatures.size == sortedHtlcTxs.size, s"htlc sig count mismatch (received=${commit.htlcSignatures.size}, expected=${sortedHtlcTxs.size})")
val localHtlcKey = Generators.derivePrivKey(localParams.htlcKey, localPerCommitmentPoint)
val htlcSigs = sortedHtlcTxs.map(Transactions.sign(_, localHtlcKey))
val remoteHtlcPubkey = Generators.derivePubKey(remoteParams.htlcBasepoint, localPerCommitmentPoint)
// combine the sigs to make signed txes
val htlcTxsAndSigs = (sortedHtlcTxs, htlcSigs, commit.htlcSignatures).zipped.toList.collect {
case (htlcTx: HtlcTimeoutTx, localSig, remoteSig) =>
require(Transactions.checkSpendable(Transactions.addSigs(htlcTx, localSig, remoteSig)).isSuccess, "bad sig")
HtlcTxAndSigs(htlcTx, localSig, remoteSig)
case (htlcTx: HtlcSuccessTx, localSig, remoteSig) =>
// we can't check that htlc-success tx are spendable because we need the payment preimage; thus we only check the remote sig
require(Transactions.checkSig(htlcTx, remoteSig, remoteHtlcPubkey), "bad sig")
HtlcTxAndSigs(htlcTx, localSig, remoteSig)
}
// we will send our revocation preimage + our next revocation hash
val localPerCommitmentSecret = Generators.perCommitSecret(localParams.shaSeed, commitments.localCommit.index)
val localNextPerCommitmentPoint = Generators.perCommitPoint(localParams.shaSeed, commitments.localCommit.index + 2)
val revocation = RevokeAndAck(
channelId = commitments.channelId,
perCommitmentSecret = localPerCommitmentSecret,
nextPerCommitmentPoint = localNextPerCommitmentPoint
)
// update our commitment data
val localCommit1 = LocalCommit(
index = localCommit.index + 1,
spec,
publishableTxs = PublishableTxs(signedCommitTx, htlcTxsAndSigs))
val ourChanges1 = localChanges.copy(acked = Nil)
val theirChanges1 = remoteChanges.copy(proposed = Nil, acked = remoteChanges.acked ++ remoteChanges.proposed)
// the outgoing following htlcs have been completed (fulfilled or failed) when we received this sig
val completedOutgoingHtlcs = commitments.localCommit.spec.htlcs.filter(_.direction == OUT).map(_.add.id) -- localCommit1.spec.htlcs.filter(_.direction == OUT).map(_.add.id)
// we remove the newly completed htlcs from the origin map
val originChannels1 = commitments.originChannels -- completedOutgoingHtlcs
val commitments1 = commitments.copy(localCommit = localCommit1, localChanges = ourChanges1, remoteChanges = theirChanges1, originChannels = originChannels1)
logger.debug(s"current commit: index=${localCommit1.index} htlc_in=${localCommit1.spec.htlcs.filter(_.direction == IN).size} htlc_out=${localCommit1.spec.htlcs.filter(_.direction == OUT).size} txid=${localCommit1.publishableTxs.commitTx.tx.txid} tx=${Transaction.write(localCommit1.publishableTxs.commitTx.tx)}")
(commitments1, revocation)
}
def receiveRevocation(commitments: Commitments, revocation: RevokeAndAck): Commitments = {
import commitments._
// we receive a revocation because we just sent them a sig for their next commit tx
remoteNextCommitInfo match {
case Left(_) if revocation.perCommitmentSecret.toPoint != remoteCommit.remotePerCommitmentPoint =>
throw InvalidRevocation(commitments.channelId)
case Left(WaitingForRevocation(theirNextCommit, _, _, _)) =>
val commitments1 = commitments.copy(
localChanges = localChanges.copy(signed = Nil, acked = localChanges.acked ++ localChanges.signed),
remoteChanges = remoteChanges.copy(signed = Nil),
remoteCommit = theirNextCommit,
remoteNextCommitInfo = Right(revocation.nextPerCommitmentPoint),
remotePerCommitmentSecrets = commitments.remotePerCommitmentSecrets.addHash(revocation.perCommitmentSecret, 0xFFFFFFFFFFFFL - commitments.remoteCommit.index))
commitments1
case Right(_) =>
throw UnexpectedRevocation(commitments.channelId)
}
}
def makeLocalTxs(commitTxNumber: Long, localParams: LocalParams, remoteParams: RemoteParams, commitmentInput: InputInfo, localPerCommitmentPoint: Point, spec: CommitmentSpec): (CommitTx, Seq[HtlcTimeoutTx], Seq[HtlcSuccessTx]) = {
val localPaymentPubkey = Generators.derivePubKey(localParams.paymentBasepoint, localPerCommitmentPoint)
val localDelayedPaymentPubkey = Generators.derivePubKey(localParams.delayedPaymentBasepoint, localPerCommitmentPoint)
val localHtlcPubkey = Generators.derivePubKey(localParams.htlcBasepoint, localPerCommitmentPoint)
val remotePaymentPubkey = Generators.derivePubKey(remoteParams.paymentBasepoint, localPerCommitmentPoint)
val remoteHtlcPubkey = Generators.derivePubKey(remoteParams.htlcBasepoint, localPerCommitmentPoint)
val localRevocationPubkey = Generators.revocationPubKey(remoteParams.revocationBasepoint, localPerCommitmentPoint)
val commitTx = Transactions.makeCommitTx(commitmentInput, commitTxNumber, localParams.paymentBasepoint, remoteParams.paymentBasepoint, localParams.isFunder, Satoshi(localParams.dustLimitSatoshis), localRevocationPubkey, remoteParams.toSelfDelay, localDelayedPaymentPubkey, remotePaymentPubkey, localHtlcPubkey, remoteHtlcPubkey, spec)
val (htlcTimeoutTxs, htlcSuccessTxs) = Transactions.makeHtlcTxs(commitTx.tx, Satoshi(localParams.dustLimitSatoshis), localRevocationPubkey, remoteParams.toSelfDelay, localDelayedPaymentPubkey, localHtlcPubkey, remoteHtlcPubkey, spec)
(commitTx, htlcTimeoutTxs, htlcSuccessTxs)
}
def makeRemoteTxs(commitTxNumber: Long, localParams: LocalParams, remoteParams: RemoteParams, commitmentInput: InputInfo, remotePerCommitmentPoint: Point, spec: CommitmentSpec): (CommitTx, Seq[HtlcTimeoutTx], Seq[HtlcSuccessTx]) = {
val localPaymentPubkey = Generators.derivePubKey(localParams.paymentBasepoint, remotePerCommitmentPoint)
val localHtlcPubkey = Generators.derivePubKey(localParams.htlcBasepoint, remotePerCommitmentPoint)
val remotePaymentPubkey = Generators.derivePubKey(remoteParams.paymentBasepoint, remotePerCommitmentPoint)
val remoteDelayedPaymentPubkey = Generators.derivePubKey(remoteParams.delayedPaymentBasepoint, remotePerCommitmentPoint)
val remoteHtlcPubkey = Generators.derivePubKey(remoteParams.htlcBasepoint, remotePerCommitmentPoint)
val remoteRevocationPubkey = Generators.revocationPubKey(localParams.revocationBasepoint, remotePerCommitmentPoint)
val commitTx = Transactions.makeCommitTx(commitmentInput, commitTxNumber, remoteParams.paymentBasepoint, localParams.paymentBasepoint, !localParams.isFunder, Satoshi(remoteParams.dustLimitSatoshis), remoteRevocationPubkey, localParams.toSelfDelay, remoteDelayedPaymentPubkey, localPaymentPubkey, remoteHtlcPubkey, localHtlcPubkey, spec)
val (htlcTimeoutTxs, htlcSuccessTxs) = Transactions.makeHtlcTxs(commitTx.tx, Satoshi(remoteParams.dustLimitSatoshis), remoteRevocationPubkey, localParams.toSelfDelay, remoteDelayedPaymentPubkey, remoteHtlcPubkey, localHtlcPubkey, spec)
(commitTx, htlcTimeoutTxs, htlcSuccessTxs)
}
def msg2String(msg: LightningMessage): String = msg match {
case u: UpdateAddHtlc => s"add-${u.id}"
case u: UpdateFulfillHtlc => s"ful-${u.id}"
case u: UpdateFailHtlc => s"fail-${u.id}"
case _: UpdateFee => s"fee"
case _: CommitSig => s"sig"
case _: RevokeAndAck => s"rev"
case _: Error => s"err"
case _: FundingLocked => s"funding_locked"
case _ => "???"
}
def changes2String(commitments: Commitments): String = {
import commitments._
s"""commitments:
| localChanges:
| proposed: ${localChanges.proposed.map(msg2String(_)).mkString(" ")}
| signed: ${localChanges.signed.map(msg2String(_)).mkString(" ")}
| acked: ${localChanges.acked.map(msg2String(_)).mkString(" ")}
| remoteChanges:
| proposed: ${remoteChanges.proposed.map(msg2String(_)).mkString(" ")}
| acked: ${remoteChanges.acked.map(msg2String(_)).mkString(" ")}
| signed: ${remoteChanges.signed.map(msg2String(_)).mkString(" ")}
| nextHtlcId:
| local: $localNextHtlcId
| remote: $remoteNextHtlcId""".stripMargin
}
def specs2String(commitments: Commitments): String = {
s"""specs:
|localcommit:
| toLocal: ${commitments.localCommit.spec.toLocalMsat}
| toRemote: ${commitments.localCommit.spec.toRemoteMsat}
| htlcs:
|${commitments.localCommit.spec.htlcs.map(h => s" ${h.direction} ${h.add.id} ${h.add.expiry}").mkString("\n")}
|remotecommit:
| toLocal: ${commitments.remoteCommit.spec.toLocalMsat}
| toRemote: ${commitments.remoteCommit.spec.toRemoteMsat}
| htlcs:
|${commitments.remoteCommit.spec.htlcs.map(h => s" ${h.direction} ${h.add.id} ${h.add.expiry}").mkString("\n")}
|next remotecommit:
| toLocal: ${commitments.remoteNextCommitInfo.left.toOption.map(_.nextRemoteCommit.spec.toLocalMsat).getOrElse("N/A")}
| toRemote: ${commitments.remoteNextCommitInfo.left.toOption.map(_.nextRemoteCommit.spec.toRemoteMsat).getOrElse("N/A")}
| htlcs:
|${commitments.remoteNextCommitInfo.left.toOption.map(_.nextRemoteCommit.spec.htlcs.map(h => s" ${h.direction} ${h.add.id} ${h.add.expiry}").mkString("\n")).getOrElse("N/A")}""".stripMargin
}
}

View File

@ -0,0 +1,24 @@
package fr.acinq.eclair.channel
import akka.actor.{Actor, ActorLogging, ActorRef}
import fr.acinq.eclair.NodeParams
import fr.acinq.eclair.wire.LightningMessage
/**
* Created by fabrice on 27/02/17.
*/
class Forwarder(nodeParams: NodeParams) extends Actor with ActorLogging {
// caller is responsible for sending the destination before anything else
// the general case is that destination can die anytime and it is managed by the caller
def receive = main(context.system.deadLetters)
def main(destination: ActorRef): Receive = {
case destination: ActorRef => context become main(destination)
case msg: LightningMessage => destination forward msg
}
}

View File

@ -0,0 +1,545 @@
package fr.acinq.eclair.channel
import fr.acinq.bitcoin.Crypto.{Point, PublicKey, Scalar, sha256}
import fr.acinq.bitcoin.Script._
import fr.acinq.bitcoin.{OutPoint, _}
import fr.acinq.eclair.blockchain.EclairWallet
import fr.acinq.eclair.crypto.Generators
import fr.acinq.eclair.router.Announcements
import fr.acinq.eclair.transactions.Scripts._
import fr.acinq.eclair.transactions.Transactions._
import fr.acinq.eclair.transactions._
import fr.acinq.eclair.wire.{AnnouncementSignatures, ClosingSigned, UpdateAddHtlc, UpdateFulfillHtlc}
import fr.acinq.eclair.{Globals, NodeParams}
import grizzled.slf4j.Logging
import scala.concurrent.Await
import scala.util.{Failure, Success, Try}
/**
* Created by PM on 20/05/2016.
*/
object Helpers {
/**
* Depending on the state, returns the current temporaryChannelId or channelId
*
* @param stateData
* @return
*/
def getChannelId(stateData: Data): BinaryData = stateData match {
case Nothing => BinaryData("00" * 32)
case d: DATA_WAIT_FOR_OPEN_CHANNEL => d.initFundee.temporaryChannelId
case d: DATA_WAIT_FOR_ACCEPT_CHANNEL => d.initFunder.temporaryChannelId
case d: DATA_WAIT_FOR_FUNDING_INTERNAL => d.temporaryChannelId
case d: DATA_WAIT_FOR_FUNDING_CREATED => d.temporaryChannelId
case d: DATA_WAIT_FOR_FUNDING_SIGNED => d.channelId
case d: HasCommitments => d.channelId
}
def validateParamsFunder(temporaryChannelId: BinaryData, nodeParams: NodeParams, channelReserveSatoshis: Long, fundingSatoshis: Long): Unit = {
val reserveToFundingRatio = channelReserveSatoshis.toDouble / fundingSatoshis
if (reserveToFundingRatio > nodeParams.maxReserveToFundingRatio) {
throw new ChannelReserveTooHigh(temporaryChannelId, channelReserveSatoshis, reserveToFundingRatio, nodeParams.maxReserveToFundingRatio)
}
}
def validateParamsFundee(temporaryChannelId: BinaryData, nodeParams: NodeParams, channelReserveSatoshis: Long, fundingSatoshis: Long, chainHash: BinaryData, initialFeeratePerKw: Long): Unit = {
require(nodeParams.chainHash == chainHash, s"invalid chain hash $chainHash (we are on ${nodeParams.chainHash})")
val localFeeratePerKw = Globals.feeratesPerKw.get.block_1
// we are fundee => initialFeeratePerKw has been set by remote
if (isFeeDiffTooHigh(initialFeeratePerKw, localFeeratePerKw, nodeParams.maxFeerateMismatch)) {
throw new FeerateTooDifferent(temporaryChannelId, localFeeratePerKw, initialFeeratePerKw)
}
validateParamsFunder(temporaryChannelId, nodeParams, channelReserveSatoshis, fundingSatoshis)
}
/**
*
* @param remoteFeeratePerKw remote fee rate per kiloweight
* @param localFeeratePerKw local fee rate per kiloweight
* @return the "normalized" difference between local and remote fee rate, i.e. |remote - local| / avg(local, remote)
*/
def feeRateMismatch(remoteFeeratePerKw: Long, localFeeratePerKw: Long): Double =
Math.abs((2.0 * (remoteFeeratePerKw - localFeeratePerKw)) / (localFeeratePerKw + remoteFeeratePerKw))
def shouldUpdateFee(commitmentFeeratePerKw: Long, networkFeeratePerKw: Long, updateFeeMinDiffRatio: Double): Boolean =
// negative feerate can happen in regtest mode
networkFeeratePerKw > 0 && feeRateMismatch(networkFeeratePerKw, commitmentFeeratePerKw) > updateFeeMinDiffRatio
/**
*
* @param remoteFeeratePerKw remote fee rate per kiloweight
* @param localFeeratePerKw local fee rate per kiloweight
* @param maxFeerateMismatchRatio maximum fee rate mismatch ratio
* @return true if the difference between local and remote fee rates is too high.
* the actual check is |remote - local| / avg(local, remote) > mismatch ratio
*/
def isFeeDiffTooHigh(remoteFeeratePerKw: Long, localFeeratePerKw: Long, maxFeerateMismatchRatio: Double): Boolean = {
// negative feerate can happen in regtest mode
remoteFeeratePerKw > 0 && feeRateMismatch(remoteFeeratePerKw, localFeeratePerKw) > maxFeerateMismatchRatio
}
def makeAnnouncementSignatures(nodeParams: NodeParams, commitments: Commitments, shortChannelId: Long) = {
// TODO: empty features
val features = BinaryData("")
val (localNodeSig, localBitcoinSig) = Announcements.signChannelAnnouncement(nodeParams.chainHash, shortChannelId, nodeParams.privateKey, commitments.remoteParams.nodeId, commitments.localParams.fundingPrivKey, commitments.remoteParams.fundingPubKey, features)
AnnouncementSignatures(commitments.channelId, shortChannelId, localNodeSig, localBitcoinSig)
}
def getFinalScriptPubKey(wallet: EclairWallet): BinaryData = {
import scala.concurrent.duration._
val finalAddress = Await.result(wallet.getFinalAddress, 40 seconds)
val finalScriptPubKey = Base58Check.decode(finalAddress) match {
case (Base58.Prefix.PubkeyAddressTestnet, hash) => Script.write(OP_DUP :: OP_HASH160 :: OP_PUSHDATA(hash) :: OP_EQUALVERIFY :: OP_CHECKSIG :: Nil)
case (Base58.Prefix.ScriptAddressTestnet, hash) => Script.write(OP_HASH160 :: OP_PUSHDATA(hash) :: OP_EQUAL :: Nil)
}
finalScriptPubKey
}
object Funding {
def makeFundingInputInfo(fundingTxId: BinaryData, fundingTxOutputIndex: Int, fundingSatoshis: Satoshi, fundingPubkey1: PublicKey, fundingPubkey2: PublicKey): InputInfo = {
val fundingScript = multiSig2of2(fundingPubkey1, fundingPubkey2)
val fundingTxOut = TxOut(fundingSatoshis, pay2wsh(fundingScript))
InputInfo(OutPoint(fundingTxId, fundingTxOutputIndex), fundingTxOut, write(fundingScript))
}
/**
* Creates both sides's first commitment transaction
*
* @param localParams
* @param remoteParams
* @param pushMsat
* @param fundingTxHash
* @param fundingTxOutputIndex
* @param remoteFirstPerCommitmentPoint
* @return (localSpec, localTx, remoteSpec, remoteTx, fundingTxOutput)
*/
def makeFirstCommitTxs(temporaryChannelId: BinaryData, localParams: LocalParams, remoteParams: RemoteParams, fundingSatoshis: Long, pushMsat: Long, initialFeeratePerKw: Long, fundingTxHash: BinaryData, fundingTxOutputIndex: Int, remoteFirstPerCommitmentPoint: Point, maxFeerateMismatch: Double): (CommitmentSpec, CommitTx, CommitmentSpec, CommitTx) = {
val toLocalMsat = if (localParams.isFunder) fundingSatoshis * 1000 - pushMsat else pushMsat
val toRemoteMsat = if (localParams.isFunder) pushMsat else fundingSatoshis * 1000 - pushMsat
val localSpec = CommitmentSpec(Set.empty[DirectedHtlc], feeratePerKw = initialFeeratePerKw, toLocalMsat = toLocalMsat, toRemoteMsat = toRemoteMsat)
val remoteSpec = CommitmentSpec(Set.empty[DirectedHtlc], feeratePerKw = initialFeeratePerKw, toLocalMsat = toRemoteMsat, toRemoteMsat = toLocalMsat)
if (!localParams.isFunder) {
// they are funder, therefore they pay the fee: we need to make sure they can afford it!
val toRemoteMsat = remoteSpec.toLocalMsat
val fees = Transactions.commitTxFee(Satoshi(remoteParams.dustLimitSatoshis), remoteSpec).amount
val missing = toRemoteMsat / 1000 - localParams.channelReserveSatoshis - fees
if (missing < 0) {
throw CannotAffordFees(temporaryChannelId, missingSatoshis = -1 * missing, reserveSatoshis = localParams.channelReserveSatoshis, feesSatoshis = fees)
}
}
val commitmentInput = makeFundingInputInfo(fundingTxHash, fundingTxOutputIndex, Satoshi(fundingSatoshis), localParams.fundingPrivKey.publicKey, remoteParams.fundingPubKey)
val localPerCommitmentPoint = Generators.perCommitPoint(localParams.shaSeed, 0)
val (localCommitTx, _, _) = Commitments.makeLocalTxs(0, localParams, remoteParams, commitmentInput, localPerCommitmentPoint, localSpec)
val (remoteCommitTx, _, _) = Commitments.makeRemoteTxs(0, localParams, remoteParams, commitmentInput, remoteFirstPerCommitmentPoint, remoteSpec)
(localSpec, localCommitTx, remoteSpec, remoteCommitTx)
}
}
object Closing extends Logging {
def isValidFinalScriptPubkey(scriptPubKey: BinaryData): Boolean = {
Try(Script.parse(scriptPubKey)) match {
case Success(OP_DUP :: OP_HASH160 :: OP_PUSHDATA(pubkeyHash, _) :: OP_EQUALVERIFY :: OP_CHECKSIG :: Nil) if pubkeyHash.size == 20 => true
case Success(OP_HASH160 :: OP_PUSHDATA(scriptHash, _) :: OP_EQUAL :: Nil) if scriptHash.size == 20 => true
case Success(OP_0 :: OP_PUSHDATA(pubkeyHash, _) :: Nil) if pubkeyHash.size == 20 => true
case Success(OP_0 :: OP_PUSHDATA(scriptHash, _) :: Nil) if scriptHash.size == 32 => true
case _ => false
}
}
def makeFirstClosingTx(commitments: Commitments, localScriptPubkey: BinaryData, remoteScriptPubkey: BinaryData): ClosingSigned = {
logger.debug(s"making first closing tx with commitments:\n${Commitments.specs2String(commitments)}")
import commitments._
val closingFee = {
// this is just to estimate the weight, it depends on size of the pubkey scripts
val dummyClosingTx = Transactions.makeClosingTx(commitInput, localScriptPubkey, remoteScriptPubkey, localParams.isFunder, Satoshi(0), Satoshi(0), localCommit.spec)
val closingWeight = Transaction.weight(Transactions.addSigs(dummyClosingTx, localParams.fundingPrivKey.publicKey, remoteParams.fundingPubKey, "aa" * 71, "bb" * 71).tx)
// no need to use a very high fee here
val feeratePerKw = Globals.feeratesPerKw.get.blocks_6
logger.info(s"using feeratePerKw=$feeratePerKw for closing tx")
Transactions.weight2fee(feeratePerKw, closingWeight)
}
val (_, closingSigned) = makeClosingTx(commitments, localScriptPubkey, remoteScriptPubkey, closingFee)
closingSigned
}
def makeClosingTx(commitments: Commitments, localScriptPubkey: BinaryData, remoteScriptPubkey: BinaryData, closingFee: Satoshi): (ClosingTx, ClosingSigned) = {
import commitments._
require(isValidFinalScriptPubkey(localScriptPubkey), "invalid localScriptPubkey")
require(isValidFinalScriptPubkey(remoteScriptPubkey), "invalid remoteScriptPubkey")
// TODO: check that
val dustLimitSatoshis = Satoshi(Math.max(localParams.dustLimitSatoshis, remoteParams.dustLimitSatoshis))
val closingTx = Transactions.makeClosingTx(commitInput, localScriptPubkey, remoteScriptPubkey, localParams.isFunder, dustLimitSatoshis, closingFee, localCommit.spec)
val localClosingSig = Transactions.sign(closingTx, commitments.localParams.fundingPrivKey)
val closingSigned = ClosingSigned(channelId, closingFee.amount, localClosingSig)
logger.debug(s"closingTx=${Transaction.write(closingTx.tx)}")
(closingTx, closingSigned)
}
def checkClosingSignature(commitments: Commitments, localScriptPubkey: BinaryData, remoteScriptPubkey: BinaryData, remoteClosingFee: Satoshi, remoteClosingSig: BinaryData): Try[Transaction] = {
import commitments._
val (closingTx, closingSigned) = makeClosingTx(commitments, localScriptPubkey, remoteScriptPubkey, remoteClosingFee)
val signedClosingTx = Transactions.addSigs(closingTx, localParams.fundingPrivKey.publicKey, remoteParams.fundingPubKey, closingSigned.signature, remoteClosingSig)
Transactions.checkSpendable(signedClosingTx).map(x => signedClosingTx.tx)
}
def nextClosingFee(localClosingFee: Satoshi, remoteClosingFee: Satoshi): Satoshi = ((localClosingFee + remoteClosingFee) / 4) * 2
def generateTx(desc: String)(attempt: Try[TransactionWithInputInfo]): Option[TransactionWithInputInfo] = {
attempt match {
case Success(txinfo) =>
logger.warn(s"tx generation success: desc=$desc txid=${txinfo.tx.txid} amount=${txinfo.tx.txOut.map(_.amount.amount).sum} tx=${Transaction.write(txinfo.tx)}")
Some(txinfo)
case Failure(t) =>
logger.warn(s"tx generation failure: desc=$desc reason: ${t.getMessage}")
None
}
}
/**
*
* Claim all the HTLCs that we've received from our current commit tx. This will be
* done using 2nd stage HTLC transactions
*
* @param commitments our commitment data, which include payment preimages
* @return a list of transactions (one per HTLC that we can claim)
*/
def claimCurrentLocalCommitTxOutputs(commitments: Commitments, tx: Transaction): LocalCommitPublished = {
import commitments._
require(localCommit.publishableTxs.commitTx.tx.txid == tx.txid, "txid mismatch, provided tx is not the current local commit tx")
val localPerCommitmentPoint = Generators.perCommitPoint(localParams.shaSeed, commitments.localCommit.index.toInt)
val localRevocationPubkey = Generators.revocationPubKey(remoteParams.revocationBasepoint, localPerCommitmentPoint)
val localDelayedPrivkey = Generators.derivePrivKey(localParams.delayedPaymentKey, localPerCommitmentPoint)
// no need to use a high fee rate for delayed transactions (we are the only one who can spend them)
val feeratePerKwDelayed = Globals.feeratesPerKw.get.blocks_6
// first we will claim our main output as soon as the delay is over
val mainDelayedTx = generateTx("main-delayed-output")(Try {
val claimDelayed = Transactions.makeClaimDelayedOutputTx(tx, Satoshi(localParams.dustLimitSatoshis), localRevocationPubkey, localParams.toSelfDelay, localDelayedPrivkey.publicKey, localParams.defaultFinalScriptPubKey, feeratePerKwDelayed)
val sig = Transactions.sign(claimDelayed, localDelayedPrivkey)
Transactions.addSigs(claimDelayed, sig)
})
// those are the preimages to existing received htlcs
val preimages = commitments.localChanges.all.collect { case u: UpdateFulfillHtlc => u.paymentPreimage }
val htlcTxes = localCommit.publishableTxs.htlcTxsAndSigs.collect {
// incoming htlc for which we have the preimage: we spend it directly
case HtlcTxAndSigs(txinfo@HtlcSuccessTx(_, _, paymentHash), localSig, remoteSig) if preimages.exists(r => sha256(r) == paymentHash) =>
generateTx("htlc-success")(Try {
val preimage = preimages.find(r => sha256(r) == paymentHash).get
Transactions.addSigs(txinfo, localSig, remoteSig, preimage)
})
// (incoming htlc for which we don't have the preimage: nothing to do, it will timeout eventually and they will get their funds back)
// outgoing htlc: they may or may not have the preimage, the only thing to do is try to get back our funds after timeout
case HtlcTxAndSigs(txinfo: HtlcTimeoutTx, localSig, remoteSig) =>
generateTx("htlc-timeout")(Try {
Transactions.addSigs(txinfo, localSig, remoteSig)
})
}.flatten
// all htlc output to us are delayed, so we need to claim them as soon as the delay is over
val htlcDelayedTxes = htlcTxes.map {
case txinfo: TransactionWithInputInfo => generateTx("claim-delayed-output")(Try {
// TODO: we should use the current fee rate, not the initial fee rate that we get from localParams
val claimDelayed = Transactions.makeClaimDelayedOutputTx(txinfo.tx, Satoshi(localParams.dustLimitSatoshis), localRevocationPubkey, localParams.toSelfDelay, localDelayedPrivkey.publicKey, localParams.defaultFinalScriptPubKey, feeratePerKwDelayed)
val sig = Transactions.sign(claimDelayed, localDelayedPrivkey)
Transactions.addSigs(claimDelayed, sig)
})
}.flatten
// OPTIONAL: let's check transactions are actually spendable
//val txes = mainDelayedTx +: (htlcTxes ++ htlcDelayedTxes)
//require(txes.forall(Transactions.checkSpendable(_).isSuccess), "the tx we produced are not spendable!")
LocalCommitPublished(
commitTx = tx,
claimMainDelayedOutputTx = mainDelayedTx.map(_.tx),
htlcSuccessTxs = htlcTxes.collect { case c: HtlcSuccessTx => c.tx },
htlcTimeoutTxs = htlcTxes.collect { case c: HtlcTimeoutTx => c.tx },
claimHtlcDelayedTx = htlcDelayedTxes.map(_.tx),
spent = Map.empty)
}
/**
*
* Claim all the HTLCs that we've received from their current commit tx
*
* @param commitments our commitment data, which include payment preimages
* @return a list of transactions (one per HTLC that we can claim)
*/
def claimRemoteCommitTxOutputs(commitments: Commitments, remoteCommit: RemoteCommit, tx: Transaction): RemoteCommitPublished = {
import commitments.{commitInput, localParams, remoteParams}
require(remoteCommit.txid == tx.txid, "txid mismatch, provided tx is not the current remote commit tx")
val (remoteCommitTx, htlcTimeoutTxs, htlcSuccessTxs) = Commitments.makeRemoteTxs(remoteCommit.index, localParams, remoteParams, commitInput, remoteCommit.remotePerCommitmentPoint, remoteCommit.spec)
require(remoteCommitTx.tx.txid == tx.txid, "txid mismatch, cannot recompute the current remote commit tx")
val localPaymentPrivkey = Generators.derivePrivKey(localParams.paymentKey, remoteCommit.remotePerCommitmentPoint)
val localHtlcPrivkey = Generators.derivePrivKey(localParams.htlcKey, remoteCommit.remotePerCommitmentPoint)
val remoteHtlcPubkey = Generators.derivePubKey(remoteParams.htlcBasepoint, remoteCommit.remotePerCommitmentPoint)
val localPerCommitmentPoint = Generators.perCommitPoint(localParams.shaSeed, commitments.localCommit.index.toInt)
val localRevocationPubKey = Generators.revocationPubKey(remoteParams.revocationBasepoint, localPerCommitmentPoint)
val remoteRevocationPubkey = Generators.revocationPubKey(localParams.revocationBasepoint, remoteCommit.remotePerCommitmentPoint)
// no need to use a high fee rate for our main output (we are the only one who can spend it)
val feeratePerKwMain = Globals.feeratesPerKw.get.blocks_6
// we need to use a rather high fee for htlc-claim because we compete with the counterparty
val feeratePerKwHtlc = Globals.feeratesPerKw.get.block_1
// first we will claim our main output right away
val mainTx = generateTx("claim-p2wpkh-output")(Try {
val claimMain = Transactions.makeClaimP2WPKHOutputTx(tx, Satoshi(localParams.dustLimitSatoshis), localPaymentPrivkey.publicKey, localParams.defaultFinalScriptPubKey, feeratePerKwMain)
val sig = Transactions.sign(claimMain, localPaymentPrivkey)
Transactions.addSigs(claimMain, localPaymentPrivkey.publicKey, sig)
})
// those are the preimages to existing received htlcs
val preimages = commitments.localChanges.all.collect { case u: UpdateFulfillHtlc => u.paymentPreimage }
// remember we are looking at the remote commitment so IN for them is really OUT for us and vice versa
val txes = commitments.remoteCommit.spec.htlcs.collect {
// incoming htlc for which we have the preimage: we spend it directly
case DirectedHtlc(OUT, add: UpdateAddHtlc) if preimages.exists(r => sha256(r) == add.paymentHash) => generateTx("claim-htlc-success")(Try {
val preimage = preimages.find(r => sha256(r) == add.paymentHash).get
val tx = Transactions.makeClaimHtlcSuccessTx(remoteCommitTx.tx, Satoshi(localParams.dustLimitSatoshis), localHtlcPrivkey.publicKey, remoteHtlcPubkey, remoteRevocationPubkey, localParams.defaultFinalScriptPubKey, add, feeratePerKwHtlc)
val sig = Transactions.sign(tx, localHtlcPrivkey)
Transactions.addSigs(tx, sig, preimage)
})
// (incoming htlc for which we don't have the preimage: nothing to do, it will timeout eventually and they will get their funds back)
// outgoing htlc: they may or may not have the preimage, the only thing to do is try to get back our funds after timeout
case DirectedHtlc(IN, add: UpdateAddHtlc) => generateTx("claim-htlc-timeout")(Try {
val tx = Transactions.makeClaimHtlcTimeoutTx(remoteCommitTx.tx, Satoshi(localParams.dustLimitSatoshis), localHtlcPrivkey.publicKey, remoteHtlcPubkey, remoteRevocationPubkey, localParams.defaultFinalScriptPubKey, add, feeratePerKwHtlc)
val sig = Transactions.sign(tx, localHtlcPrivkey)
Transactions.addSigs(tx, sig)
})
}.toSeq.flatten
// OPTIONAL: let's check transactions are actually spendable
//require(txes.forall(Transactions.checkSpendable(_).isSuccess), "the tx we produced are not spendable!")
RemoteCommitPublished(
commitTx = tx,
claimMainOutputTx = mainTx.map(_.tx),
claimHtlcSuccessTxs = txes.toList.collect { case c: ClaimHtlcSuccessTx => c.tx },
claimHtlcTimeoutTxs = txes.toList.collect { case c: ClaimHtlcTimeoutTx => c.tx },
spent = Map.empty
)
}
/**
* When an unexpected transaction spending the funding tx is detected:
* 1) we find out if the published transaction is one of remote's revoked txs
* 2) and then:
* a) if it is a revoked tx we build a set of transactions that will punish them by stealing all their funds
* b) otherwise there is nothing we can do
*
* @return a [[RevokedCommitPublished]] object containing penalty transactions if the tx is a revoked commitment
*/
def claimRevokedRemoteCommitTxOutputs(commitments: Commitments, tx: Transaction): Option[RevokedCommitPublished] = {
import commitments._
require(tx.txIn.size == 1, "commitment tx should have 1 input")
val obscuredTxNumber = Transactions.decodeTxNumber(tx.txIn(0).sequence, tx.lockTime)
// this tx has been published by remote, so we need to invert local/remote params
val txnumber = Transactions.obscuredCommitTxNumber(obscuredTxNumber, !localParams.isFunder, remoteParams.paymentBasepoint, localParams.paymentBasepoint)
require(txnumber <= 0xffffffffffffL, "txnumber must be lesser than 48 bits long")
logger.warn(s"counterparty has published revoked commit txnumber=$txnumber")
// 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 => Scalar(d))
.map { remotePerCommitmentSecret =>
val remotePerCommitmentPoint = remotePerCommitmentSecret.toPoint
val remoteDelayedPaymentPubkey = Generators.derivePubKey(remoteParams.delayedPaymentBasepoint, remotePerCommitmentPoint)
val remoteRevocationPrivkey = Generators.revocationPrivKey(localParams.revocationSecret, remotePerCommitmentSecret)
val localPrivkey = Generators.derivePrivKey(localParams.paymentKey, remotePerCommitmentPoint)
// no need to use a high fee rate for our main output (we are the only one who can spend it)
val feeratePerKwMain = Globals.feeratesPerKw.get.blocks_6
// we need to use a high fee here for punishment txes because after a delay they can be spent by the counterparty
val feeratePerKwPenalty = Globals.feeratesPerKw.get.block_1
// first we will claim our main output right away
val mainTx = generateTx("claim-p2wpkh-output")(Try {
val claimMain = Transactions.makeClaimP2WPKHOutputTx(tx, Satoshi(localParams.dustLimitSatoshis), localPrivkey.publicKey, localParams.defaultFinalScriptPubKey, feeratePerKwMain)
val sig = Transactions.sign(claimMain, localPrivkey)
Transactions.addSigs(claimMain, localPrivkey.publicKey, sig)
})
// then we punish them by stealing their main output
val mainPenaltyTx = generateTx("main-penalty")(Try {
// TODO: we should use the current fee rate, not the initial fee rate that we get from localParams
val txinfo = Transactions.makeMainPenaltyTx(tx, Satoshi(localParams.dustLimitSatoshis), remoteRevocationPrivkey.publicKey, localParams.defaultFinalScriptPubKey, remoteParams.toSelfDelay, remoteDelayedPaymentPubkey, feeratePerKwPenalty)
val sig = Transactions.sign(txinfo, remoteRevocationPrivkey)
Transactions.addSigs(txinfo, sig)
})
// TODO: we don't claim htlcs outputs yet
// OPTIONAL: let's check transactions are actually spendable
//val txes = mainDelayedRevokedTx :: Nil
//require(txes.forall(Transactions.checkSpendable(_).isSuccess), "the tx we produced are not spendable!")
RevokedCommitPublished(
commitTx = tx,
claimMainOutputTx = mainTx.map(_.tx),
mainPenaltyTx = mainPenaltyTx.map(_.tx),
claimHtlcTimeoutTxs = Nil,
htlcTimeoutTxs = Nil,
htlcPenaltyTxs = Nil,
spent = Map.empty
)
}
}
/**
* In CLOSING state, when we are notified that a transaction has been confirmed, we check if this tx belongs in the
* local commit scenario and keep track of it.
*
* We need to keep track of all transactions spending the outputs of the commitment tx, because some outputs can be
* spent both by us and our counterparty. Because of that, some of our transactions may never confirm and we don't
* want to wait forever before declaring that the channel is CLOSED.
*
* @param localCommitPublished
* @return
*/
def updateLocalCommitPublished(localCommitPublished: LocalCommitPublished, tx: Transaction) = {
// even if our txes 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 txes? (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.claimHtlcDelayedTx.map(_.txid).contains(outPoint.txid)
isCommitTx || spendsTheCommitTx || is3rdStageDelayedTx
}
// then we add the relevant outpoints to the map keeping track of which txid spends which outpoint
localCommitPublished.copy(spent = localCommitPublished.spent ++ relevantOutpoints.map(o => (o -> tx.txid)).toMap)
}
/**
* In CLOSING state, when we are notified that a transaction has been confirmed, we check if this tx belongs in the
* remote commit scenario and keep track of it.
*
* We need to keep track of all transactions spending the outputs of the commitment tx, because some outputs can be
* spent both by us and our counterparty. Because of that, some of our transactions may never confirm and we don't
* want to wait forever before declaring that the channel is CLOSED.
*
* @param remoteCommitPublished
* @return
*/
def updateRemoteCommitPublished(remoteCommitPublished: RemoteCommitPublished, tx: Transaction) = {
// even if our txes 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 local commitment tx?
val spendsTheCommitTx = remoteCommitPublished.commitTx.txid == outPoint.txid
// TODO: we don't currently spend htlc transactions
isCommitTx || spendsTheCommitTx
}
// then we add the relevant outpoints to the map keeping track of which txid spends which outpoint
remoteCommitPublished.copy(spent = remoteCommitPublished.spent ++ relevantOutpoints.map(o => (o -> tx.txid)).toMap)
}
/**
* In CLOSING state, when we are notified that a transaction has been confirmed, we check if this tx belongs in the
* revoked commit scenario and keep track of it.
*
* We need to keep track of all transactions spending the outputs of the commitment tx, because some outputs can be
* spent both by us and our counterparty. Because of that, some of our transactions may never confirm and we don't
* want to wait forever before declaring that the channel is CLOSED.
*
* @param revokedCommitPublished
* @return
*/
def updateRevokedCommitPublished(revokedCommitPublished: RevokedCommitPublished, tx: Transaction) = {
// even if our txes 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 local commitment tx?
val spendsTheCommitTx = revokedCommitPublished.commitTx.txid == outPoint.txid
isCommitTx || spendsTheCommitTx
}
// then we add the relevant outpoints to the map keeping track of which txid spends which outpoint
revokedCommitPublished.copy(spent = revokedCommitPublished.spent ++ relevantOutpoints.map(o => (o -> tx.txid)).toMap)
}
/**
* 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 txes (txes spending htlc txes) have been confirmed
*
* @param localCommitPublished
* @return
*/
def isLocalCommitDone(localCommitPublished: LocalCommitPublished) = {
// is the commitment tx buried? (we need to check this because we may not have nay outputs)
val isCommitTxConfirmed = localCommitPublished.spent.values.toSet.contains(localCommitPublished.commitTx.txid)
// are there remaining spendable outputs from the commitment tx? we just substract all known spent outputs from the ones we control
val commitOutputsSpendableByUs = (localCommitPublished.claimMainDelayedOutputTx.toSeq ++ localCommitPublished.htlcSuccessTxs ++ localCommitPublished.htlcTimeoutTxs)
.flatMap(_.txIn.map(_.outPoint)).toSet -- localCommitPublished.spent.keys
// which htlc delayed txes can we expect to be confirmed?
val unconfirmedHtlcDelayedTxes = localCommitPublished.claimHtlcDelayedTx
.filter(tx => (tx.txIn.map(_.outPoint.txid).toSet -- localCommitPublished.spent.values).isEmpty) // only the txes which parents are already confirmed may get confirmed (note that this also eliminates outputs that have been double-spent by a competing tx)
.filterNot(tx => localCommitPublished.spent.values.toSet.contains(tx.txid)) // has the tx already been confirmed?
isCommitTxConfirmed && commitOutputsSpendableByUs.isEmpty && unconfirmedHtlcDelayedTxes.isEmpty
}
/**
* 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).
*
* @param remoteCommitPublished
* @return
*/
def isRemoteCommitDone(remoteCommitPublished: RemoteCommitPublished) = {
// is the commitment tx buried? (we need to check this because we may not have nay outputs)
val isCommitTxConfirmed = remoteCommitPublished.spent.values.toSet.contains(remoteCommitPublished.commitTx.txid)
// are there remaining spendable outputs from the commitment tx?
val commitOutputsSpendableByUs = (remoteCommitPublished.claimMainOutputTx.toSeq ++ remoteCommitPublished.claimHtlcSuccessTxs ++ remoteCommitPublished.claimHtlcTimeoutTxs)
.flatMap(_.txIn.map(_.outPoint)).toSet -- remoteCommitPublished.spent.keys
isCommitTxConfirmed && commitOutputsSpendableByUs.isEmpty
}
/**
* 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).
*
* @param revokedCommitPublished
* @return
*/
def isRevokedCommitDone(revokedCommitPublished: RevokedCommitPublished) = {
// is the commitment tx buried? (we need to check this because we may not have nay outputs)
val isCommitTxConfirmed = revokedCommitPublished.spent.values.toSet.contains(revokedCommitPublished.commitTx.txid)
// are there remaining spendable outputs from the commitment tx?
val commitOutputsSpendableByUs = (revokedCommitPublished.claimMainOutputTx.toSeq ++ revokedCommitPublished.mainPenaltyTx)
.flatMap(_.txIn.map(_.outPoint)).toSet -- revokedCommitPublished.spent.keys
// TODO: we don't currently spend htlc transactions
isCommitTxConfirmed && commitOutputsSpendableByUs.isEmpty
}
}
}

View File

@ -0,0 +1,68 @@
package fr.acinq.eclair.channel
import akka.actor.Status.Failure
import akka.actor.{Actor, ActorLogging, ActorRef, Terminated}
import fr.acinq.bitcoin.BinaryData
import fr.acinq.eclair.channel.Register.{Forward, ForwardFailure, ForwardShortId, ForwardShortIdFailure}
/**
* Created by PM on 26/01/2016.
*/
class Register extends Actor with ActorLogging {
context.system.eventStream.subscribe(self, classOf[ChannelCreated])
context.system.eventStream.subscribe(self, classOf[ChannelRestored])
context.system.eventStream.subscribe(self, classOf[ChannelIdAssigned])
context.system.eventStream.subscribe(self, classOf[ShortChannelIdAssigned])
override def receive: Receive = main(Map.empty, Map.empty)
def main(channels: Map[BinaryData, ActorRef], shortIds: Map[Long, BinaryData]): Receive = {
case ChannelCreated(channel, _, _, _, temporaryChannelId) =>
context.watch(channel)
context become main(channels + (temporaryChannelId -> channel), shortIds)
case ChannelRestored(channel, _, _, _, channelId, _) =>
context.watch(channel)
context become main(channels + (channelId -> channel), shortIds)
case ChannelIdAssigned(channel, temporaryChannelId, channelId) =>
context become main(channels + (channelId -> channel) - temporaryChannelId, shortIds)
case ShortChannelIdAssigned(channel, channelId, shortChannelId) =>
context become main(channels, shortIds + (shortChannelId -> channelId))
case Terminated(actor) if channels.values.toSet.contains(actor) =>
val channelId = channels.find(_._2 == actor).get._1
val shortChannelId = shortIds.find(_._2 == channelId).map(_._1).getOrElse(0L)
context become main(channels - channelId, shortIds - shortChannelId)
case 'channels => sender ! channels
case 'shortIds => sender ! shortIds
case fwd@Forward(channelId, msg) =>
channels.get(channelId) match {
case Some(channel) => channel forward msg
case None => sender ! Failure(ForwardFailure(fwd))
}
case fwd@ForwardShortId(shortChannelId, msg) =>
shortIds.get(shortChannelId).flatMap(channels.get(_)) match {
case Some(channel) => channel forward msg
case None => sender ! Failure(ForwardShortIdFailure(fwd))
}
}
}
object Register {
// @formatter:off
case class Forward[T](channelId: BinaryData, message: T)
case class ForwardShortId[T](shortChannelId: Long, message: T)
case class ForwardFailure[T](fwd: Forward[T]) extends RuntimeException(s"channel ${fwd.channelId} not found")
case class ForwardShortIdFailure[T](fwd: ForwardShortId[T]) extends RuntimeException(s"channel ${fwd.shortChannelId} not found")
// @formatter:on
}

View File

@ -0,0 +1,161 @@
package fr.acinq.eclair.crypto
import org.spongycastle.util.encoders.Hex
import scala.annotation.tailrec
/**
* Bit stream that can be written to and read at both ends (i.e. you can read from the end or the beginning of the stream)
*
* @param bytes bits packed as bytes, the last byte is padded with 0s
* @param offstart offset at which the first bit is in the first byte
* @param offend offset at which the last bit is in the last byte
*/
case class BitStream(bytes: Vector[Byte], offstart: Int, offend: Int) {
// offstart: 0 1 2 3 4 5 6 7
// offend: 7 6 5 4 3 2 1 0
import BitStream._
def bitCount = 8 * bytes.length - offstart - offend
def isEmpty = bitCount == 0
/**
* append a byte to a bitstream
*
* @param input byte to append
* @return an updated bitstream
*/
def writeByte(input: Byte): BitStream = offend match {
case 0 => this.copy(bytes = this.bytes :+ input)
case shift =>
val input1 = input & 0xff
val last = ((bytes.last | (input1 >>> (8 - shift))) & 0xff).toByte
val next = ((input1 << shift) & 0xff).toByte
this.copy(bytes = bytes.dropRight(1) ++ Vector(last, next))
}
/**
* append bytes to a bitstream
*
* @param input bytes to append
* @return an udpdate bitstream
*/
def writeBytes(input: Seq[Byte]): BitStream = input.foldLeft(this) { case (bs, b) => bs.writeByte(b) }
/**
* append a bit to a bistream
*
* @param bit bit to append
* @return an update bitstream
*/
def writeBit(bit: Bit): BitStream = offend match {
case 0 if bit =>
BitStream(bytes :+ 0x80.toByte, offstart, 7)
case 0 =>
BitStream(bytes :+ 0x00.toByte, offstart, 7)
case n if bit =>
val last = (bytes.last + (1 << (offend - 1))).toByte
BitStream(bytes.updated(bytes.length - 1, last), offstart, offend - 1)
case n =>
BitStream(bytes, offstart, offend - 1)
}
/**
* append bits to a bistream
*
* @param input bits to append
* @return an update bitstream
*/
def writeBits(input: Seq[Bit]): BitStream = input.foldLeft(this) { case (bs, b) => bs.writeBit(b) }
/**
* read the last bit from a bitstream
*
* @return a (stream, bit) pair where stream is an updated bitstream and bit is the last bit
*/
def popBit: (BitStream, Bit) = offend match {
case 7 => BitStream(bytes.dropRight(1), offstart, 0) -> lastBit
case n =>
val shift = n + 1
val last = (bytes.last >>> shift) << shift
BitStream(bytes.updated(bytes.length - 1, last.toByte), offstart, offend + 1) -> lastBit
}
/**
* read the last byte from a bitstream
*
* @return a (stream, byte) pair where stream is an updated bitstream and byte is the last byte
*/
def popByte: (BitStream, Byte) = offend match {
case 0 => BitStream(bytes.dropRight(1), offstart, offend) -> bytes.last
case shift =>
val a = bytes(bytes.length - 2) & 0xff
val b = bytes(bytes.length - 1) & 0xff
val byte = ((a << (8 - shift)) | (b >>> shift)) & 0xff
val a1 = (a >>> shift) << shift
BitStream(bytes.dropRight(2) :+ a1.toByte, offstart, offend) -> byte.toByte
}
def popBytes(n: Int): (BitStream, Seq[Byte]) = {
@tailrec
def loop(stream: BitStream, acc: Seq[Byte]): (BitStream, Seq[Byte]) =
if (acc.length == n) (stream, acc) else {
val (stream1, value) = stream.popByte
loop(stream1, acc :+ value)
}
loop(this, Nil)
}
/**
* read the first bit from a bitstream
*
* @return
*/
def readBit: (BitStream, Bit) = offstart match {
case 7 => BitStream(bytes.tail, 0, offend) -> firstBit
case _ => BitStream(bytes, offstart + 1, offend) -> firstBit
}
def readBits(count: Int): (BitStream, Seq[Bit]) = {
@tailrec
def loop(stream: BitStream, acc: Seq[Bit]): (BitStream, Seq[Bit]) = if (acc.length == count) (stream, acc) else {
val (stream1, bit) = stream.readBit
loop(stream1, acc :+ bit)
}
loop(this, Nil)
}
/**
* read the first byte from a bitstream
*
* @return
*/
def readByte: (BitStream, Byte) = {
val byte = ((bytes(0) << offstart) | (bytes(1) >>> (7 - offstart))) & 0xff
BitStream(bytes.tail, offstart, offend) -> byte.toByte
}
def isSet(pos: Int): Boolean = {
val pos1 = pos + offstart
(bytes(pos1 / 8) & (1 << (7 - (pos1 % 8)))) != 0
}
def firstBit = (bytes.head & (1 << (7 - offstart))) != 0
def lastBit = (bytes.last & (1 << offend)) != 0
def toBinString: String = "0b" + (for (i <- 0 until bitCount) yield if (isSet(i)) '1' else '0').mkString
def toHexString: String = "0x" + Hex.toHexString(bytes.toArray).toLowerCase
}
object BitStream {
type Bit = Boolean
val Zero = false
val One = true
val empty = BitStream(Vector.empty[Byte], 0, 0)
}

View File

@ -0,0 +1,181 @@
package fr.acinq.eclair.crypto
import java.nio.ByteOrder
import fr.acinq.bitcoin.{BinaryData, Protocol}
import grizzled.slf4j.Logging
import org.spongycastle.crypto.engines.{ChaCha7539Engine, ChaChaEngine}
import org.spongycastle.crypto.params.{KeyParameter, ParametersWithIV}
/**
* Poly1305 authenticator
* see https://tools.ietf.org/html/rfc7539#section-2.5
*/
object Poly1305 {
/**
*
* @param key input key
* @param data input data
* @return a 16 byte authentication tag
*/
def mac(key: BinaryData, data: BinaryData): BinaryData = {
val out = new Array[Byte](16)
val poly = new org.spongycastle.crypto.macs.Poly1305()
poly.init(new KeyParameter(key))
poly.update(data, 0, data.length)
poly.doFinal(out, 0)
out
}
}
/**
* ChaCha20 block cipher
* see https://tools.ietf.org/html/rfc7539#section-2.5
*/
object ChaCha20 {
def encrypt(plaintext: BinaryData, key: BinaryData, nonce: BinaryData, counter: Int = 0): BinaryData = {
val engine = new ChaCha7539Engine()
engine.init(true, new ParametersWithIV(new KeyParameter(key), nonce))
val ciphertext: BinaryData = new Array[Byte](plaintext.length)
counter match {
case 0 => ()
case 1 =>
// skip 1 block == set counter to 1 instead of 0
val dummy = new Array[Byte](64)
engine.processBytes(new Array[Byte](64), 0, 64, dummy, 0)
case _ => throw new RuntimeException(s"chacha20 counter must be 0 or 1")
}
val len = engine.processBytes(plaintext.toArray, 0, plaintext.length, ciphertext, 0)
require(len == plaintext.length, "ChaCha20 encryption failed")
ciphertext
}
def decrypt(ciphertext: BinaryData, key: BinaryData, nonce: BinaryData, counter: Int = 0): BinaryData = {
val engine = new ChaCha7539Engine
engine.init(false, new ParametersWithIV(new KeyParameter(key), nonce))
val plaintext: BinaryData = new Array[Byte](ciphertext.length)
counter match {
case 0 => ()
case 1 =>
// skip 1 block == set counter to 1 instead of 0
val dummy = new Array[Byte](64)
engine.processBytes(new Array[Byte](64), 0, 64, dummy, 0)
case _ => throw new RuntimeException(s"chacha20 counter must be 0 or 1")
}
val len = engine.processBytes(ciphertext.toArray, 0, ciphertext.length, plaintext, 0)
require(len == ciphertext.length, "ChaCha20 decryption failed")
plaintext
}
}
/**
* ChaCha20Poly1305 AEAD (Authenticated Encryption with Additional Data) algorithm
* see https://tools.ietf.org/html/rfc7539#section-2.5
*
* This what we should be using (see BOLT #8)
*/
object ChaCha20Poly1305 extends Logging {
/**
*
* @param key 32 bytes encryption key
* @param nonce 12 bytes nonce
* @param plaintext plain text
* @param aad additional authentication data. can be empty
* @return a (ciphertext, mac) tuple
*/
def encrypt(key: BinaryData, nonce: BinaryData, plaintext: BinaryData, aad: BinaryData): (BinaryData, BinaryData) = {
val polykey: BinaryData = ChaCha20.encrypt(new Array[Byte](32), key, nonce)
val ciphertext = ChaCha20.encrypt(plaintext, key, nonce, 1)
val data = aad ++ pad16(aad) ++ ciphertext ++ pad16(ciphertext) ++ Protocol.writeUInt64(aad.length, ByteOrder.LITTLE_ENDIAN) ++ Protocol.writeUInt64(ciphertext.length, ByteOrder.LITTLE_ENDIAN)
val tag = Poly1305.mac(polykey, data)
logger.debug(s"encrypt($key, $nonce, $aad, $plaintext) = ($ciphertext, $tag)")
(ciphertext, tag)
}
/**
*
* @param key 32 bytes decryption key
* @param nonce 12 bytes nonce
* @param ciphertext ciphertext
* @param aad additional authentication data. can be empty
* @param mac authentication mac
* @return the decrypted plaintext if the mac is valid.
*/
def decrypt(key: BinaryData, nonce: BinaryData, ciphertext: BinaryData, aad: BinaryData, mac: BinaryData): BinaryData = {
val polykey: BinaryData = ChaCha20.encrypt(new Array[Byte](32), key, nonce)
val data = aad ++ pad16(aad) ++ ciphertext ++ pad16(ciphertext) ++ Protocol.writeUInt64(aad.length, ByteOrder.LITTLE_ENDIAN) ++ Protocol.writeUInt64(ciphertext.length, ByteOrder.LITTLE_ENDIAN)
val tag = Poly1305.mac(polykey, data)
require(tag == mac, "invalid mac")
val plaintext = ChaCha20.decrypt(ciphertext, key, nonce, 1)
logger.debug(s"decrypt($key, $nonce, $aad, $ciphertext, $mac) = $plaintext")
plaintext
}
def pad16(data: Seq[Byte]): Seq[Byte] =
if (data.size % 16 == 0)
Seq.empty[Byte]
else
Seq.fill[Byte](16 - (data.size % 16))(0)
}
object ChaCha20Legacy {
def encrypt(plaintext: BinaryData, key: BinaryData, nonce: BinaryData, counter: Int = 0): BinaryData = {
val engine = new ChaChaEngine(20)
engine.init(true, new ParametersWithIV(new KeyParameter(key), nonce))
val ciphertext: BinaryData = new Array[Byte](plaintext.length)
counter match {
case 0 => ()
case 1 =>
// skip 1 block == set counter to 1 instead of 0
val dummy = new Array[Byte](64)
engine.processBytes(new Array[Byte](64), 0, 64, dummy, 0)
case _ => throw new RuntimeException(s"chacha20 counter must be 0 or 1")
}
val len = engine.processBytes(plaintext.toArray, 0, plaintext.length, ciphertext, 0)
require(len == plaintext.length, "ChaCha20Legacy encryption failed")
ciphertext
}
def decrypt(ciphertext: BinaryData, key: BinaryData, nonce: BinaryData, counter: Int = 0): BinaryData = {
val engine = new ChaChaEngine(20)
engine.init(false, new ParametersWithIV(new KeyParameter(key), nonce))
val plaintext: BinaryData = new Array[Byte](ciphertext.length)
counter match {
case 0 => ()
case 1 =>
// skip 1 block == set counter to 1 instead of 0
val dummy = new Array[Byte](64)
engine.processBytes(new Array[Byte](64), 0, 64, dummy, 0)
case _ => throw new RuntimeException(s"chacha20 counter must be 0 or 1")
}
val len = engine.processBytes(ciphertext.toArray, 0, ciphertext.length, plaintext, 0)
require(len == ciphertext.length, "ChaCha20Legacy decryption failed")
plaintext
}
}
/**
* Legacy implementation of ChaCha20Poly1305
* Nonce is 8 bytes instead of 12 and the output tag computation is different
*
* Used in our first interop tests with lightning-c, should not be needed anymore
*/
object Chacha20Poly1305Legacy {
def encrypt(key: BinaryData, nonce: BinaryData, plaintext: BinaryData, aad: BinaryData): (BinaryData, BinaryData) = {
val polykey: BinaryData = ChaCha20Legacy.encrypt(new Array[Byte](32), key, nonce)
val ciphertext = ChaCha20Legacy.encrypt(plaintext, key, nonce, 1)
val data = aad ++ Protocol.writeUInt64(aad.length, ByteOrder.LITTLE_ENDIAN) ++ ciphertext ++ Protocol.writeUInt64(ciphertext.length, ByteOrder.LITTLE_ENDIAN)
val tag = Poly1305.mac(polykey, data)
(ciphertext, tag)
}
def decrypt(key: BinaryData, nonce: BinaryData, ciphertext: BinaryData, aad: BinaryData, mac: BinaryData): BinaryData = {
val polykey: BinaryData = ChaCha20Legacy.encrypt(new Array[Byte](32), key, nonce)
val data = aad ++ Protocol.writeUInt64(aad.length, ByteOrder.LITTLE_ENDIAN) ++ ciphertext ++ Protocol.writeUInt64(ciphertext.length, ByteOrder.LITTLE_ENDIAN)
val tag = Poly1305.mac(polykey, data)
require(tag == mac, "invalid mac")
val plaintext = ChaCha20Legacy.decrypt(ciphertext, key, nonce, 1)
plaintext
}
}

View File

@ -0,0 +1,43 @@
package fr.acinq.eclair.crypto
import fr.acinq.bitcoin.Crypto.{Point, PrivateKey, PublicKey, Scalar}
import fr.acinq.bitcoin.{BinaryData, Crypto}
/**
* Created by PM on 07/12/2016.
*/
object Generators {
def fixSize(data: BinaryData): BinaryData = data.length match {
case 32 => data
case length if length < 32 => Array.fill(32 - length)(0.toByte) ++ data
}
def perCommitSecret(seed: BinaryData, index: Long): Scalar = Scalar(ShaChain.shaChainFromSeed(seed, 0xFFFFFFFFFFFFL - index))
def perCommitPoint(seed: BinaryData, index: Long): Point = perCommitSecret(seed, index).toPoint
def derivePrivKey(secret: Scalar, perCommitPoint: Point): PrivateKey = {
// secretkey = basepoint-secret + SHA256(per-commitment-point || basepoint)
PrivateKey(secret.add(Scalar(Crypto.sha256(perCommitPoint.toBin(true) ++ secret.toPoint.toBin(true)))), true)
}
def derivePubKey(basePoint: Point, perCommitPoint: Point): PublicKey = {
//pubkey = basepoint + SHA256(per-commitment-point || basepoint)*G
val a = Scalar(Crypto.sha256(perCommitPoint.toBin(true) ++ basePoint.toBin(true)))
PublicKey(basePoint.add(a.toPoint))
}
def revocationPubKey(basePoint: Point, perCommitPoint: Point): PublicKey = {
val a = Scalar(Crypto.sha256(basePoint.toBin(true) ++ perCommitPoint.toBin(true)))
val b = Scalar(Crypto.sha256(perCommitPoint.toBin(true) ++ basePoint.toBin(true)))
PublicKey(basePoint.multiply(a).add(perCommitPoint.multiply(b)))
}
def revocationPrivKey(secret: Scalar, perCommitSecret: Scalar): PrivateKey = {
val a = Scalar(Crypto.sha256(secret.toPoint.toBin(true) ++ perCommitSecret.toPoint.toBin(true)))
val b = Scalar(Crypto.sha256(perCommitSecret.toPoint.toBin(true) ++ secret.toPoint.toBin(true)))
PrivateKey(secret.multiply(a).add(perCommitSecret.multiply(b)), true)
}
}

View File

@ -0,0 +1,449 @@
package fr.acinq.eclair.crypto
import java.math.BigInteger
import java.nio.ByteOrder
import fr.acinq.bitcoin.{BinaryData, Crypto, Protocol}
import fr.acinq.eclair.randomBytes
import grizzled.slf4j.Logging
import org.spongycastle.crypto.digests.SHA256Digest
import org.spongycastle.crypto.macs.HMac
import org.spongycastle.crypto.params.KeyParameter
/**
* see http://noiseprotocol.org/
*/
object Noise {
case class KeyPair(pub: BinaryData, priv: BinaryData)
/**
* Diffie-Helmann functions
*/
trait DHFunctions {
def name: String
def generateKeyPair(priv: BinaryData): KeyPair
def dh(keyPair: KeyPair, publicKey: BinaryData): BinaryData
def dhLen: Int
def pubKeyLen: Int
}
object Secp256k1DHFunctions extends DHFunctions {
override val name = "secp256k1"
override def generateKeyPair(priv: BinaryData): KeyPair = {
require(priv.length == 32)
KeyPair(Crypto.publicKeyFromPrivateKey(priv :+ 1.toByte), priv)
}
/**
* this is what secp256k1's secp256k1_ecdh() returns
*
* @param keyPair
* @param publicKey
* @return sha256(publicKey * keyPair.priv in compressed format)
*/
override def dh(keyPair: KeyPair, publicKey: BinaryData): BinaryData = {
val point = Crypto.curve.getCurve.decodePoint(publicKey)
val scalar = new BigInteger(1, keyPair.priv.take(32).toArray)
val point1 = point.multiply(scalar).normalize()
Crypto.sha256(point1.getEncoded(true))
}
override def dhLen: Int = 32
override def pubKeyLen: Int = 33
}
/**
* Cipher functions
*/
trait CipherFunctions {
def name: String
//Encrypts plaintext using the cipher key k of 32 bytes and an 8-byte unsigned integer nonce n which must be unique
// for the key k. Returns the ciphertext. Encryption must be done with an "AEAD" encryption mode with the associated
// data ad (using the terminology from [1]) and returns a ciphertext that is the same size as the plaintext
// plus 16 bytes for authentication data. The entire ciphertext must be indistinguishable from random if the key is secret.
def encrypt(k: BinaryData, n: Long, ad: BinaryData, plaintext: BinaryData): BinaryData
// Decrypts ciphertext using a cipher key k of 32 bytes, an 8-byte unsigned integer nonce n, and associated data ad.
// Returns the plaintext, unless authentication fails, in which case an error is signaled to the caller.
def decrypt(k: BinaryData, n: Long, ad: BinaryData, ciphertext: BinaryData): BinaryData
}
object Chacha20Poly1305CipherFunctions extends CipherFunctions {
override val name = "ChaChaPoly"
// as specified in BOLT #8
def nonce(n: Long): BinaryData = BinaryData("00000000") ++ Protocol.writeUInt64(n, ByteOrder.LITTLE_ENDIAN)
//Encrypts plaintext using the cipher key k of 32 bytes and an 8-byte unsigned integer nonce n which must be unique
override def encrypt(k: BinaryData, n: Long, ad: BinaryData, plaintext: BinaryData): BinaryData = {
val (ciphertext, mac) = ChaCha20Poly1305.encrypt(k, nonce(n), plaintext, ad)
ciphertext ++ mac
}
// Decrypts ciphertext using a cipher key k of 32 bytes, an 8-byte unsigned integer nonce n, and associated data ad.
override def decrypt(k: BinaryData, n: Long, ad: BinaryData, ciphertextAndMac: BinaryData): BinaryData = {
val ciphertext: BinaryData = ciphertextAndMac.dropRight(16)
val mac: BinaryData = ciphertextAndMac.takeRight(16)
ChaCha20Poly1305.decrypt(k, nonce(n), ciphertext, ad, mac)
}
}
/**
* Hash functions
*/
trait HashFunctions extends Logging {
def name: String
// Hashes some arbitrary-length data with a collision-resistant cryptographic hash function and returns an output of HASHLEN bytes.
def hash(data: BinaryData): BinaryData
// A constant specifying the size in bytes of the hash output. Must be 32 or 64.
def hashLen: Int
// A constant specifying the size in bytes that the hash function uses internally to divide its input for iterative processing. This is needed to use the hash function with HMAC (BLOCKLEN is B in [2]).
def blockLen: Int
// Applies HMAC from [2] using the HASH() function. This function is only called as part of HKDF(), below.
def hmacHash(key: BinaryData, data: BinaryData): BinaryData
// Takes a chaining_key byte sequence of length HASHLEN, and an input_key_material byte sequence with length either zero bytes, 32 bytes, or DHLEN bytes. Returns two byte sequences of length HASHLEN, as follows:
// Sets temp_key = HMAC-HASH(chaining_key, input_key_material).
// Sets output1 = HMAC-HASH(temp_key, byte(0x01)).
// Sets output2 = HMAC-HASH(temp_key, output1 || byte(0x02)).
// Returns the pair (output1, output2).
def hkdf(chainingKey: BinaryData, inputMaterial: BinaryData): (BinaryData, BinaryData) = {
val tempkey = hmacHash(chainingKey, inputMaterial)
val output1 = hmacHash(tempkey, Seq(0x01.toByte))
val output2 = hmacHash(tempkey, output1 ++ Seq(0x02.toByte))
logger.debug(s"HKDF($chainingKey, $inputMaterial) = ($output1, $output2)")
(output1, output2)
}
}
object SHA256HashFunctions extends HashFunctions {
override val name = "SHA256"
override val hashLen = 32
override val blockLen = 64
override def hash(data: BinaryData) = Crypto.sha256(data)
override def hmacHash(key: BinaryData, data: BinaryData) = {
val mac = new HMac(new SHA256Digest())
mac.init(new KeyParameter(key.toArray))
mac.update(data.toArray, 0, data.length)
val out = new Array[Byte](32)
mac.doFinal(out, 0)
out
}
}
/**
* Cipher state
*/
trait CipherState {
def cipher: CipherFunctions
def initializeKey(key: BinaryData): CipherState = CipherState(key, cipher)
def hasKey: Boolean
def encryptWithAd(ad: BinaryData, plaintext: BinaryData): (CipherState, BinaryData)
def decryptWithAd(ad: BinaryData, ciphertext: BinaryData): (CipherState, BinaryData)
}
/**
* Uninitialized cipher state. Encrypt and decrypt do nothing (ciphertext = plaintext)
*
* @param cipher cipher functions
*/
case class UnitializedCipherState(cipher: CipherFunctions) extends CipherState {
override val hasKey = false
override def encryptWithAd(ad: BinaryData, plaintext: BinaryData): (CipherState, BinaryData) = (this, plaintext)
override def decryptWithAd(ad: BinaryData, ciphertext: BinaryData): (CipherState, BinaryData) = (this, ciphertext)
}
/**
* Initialized cipher state
*
* @param k key
* @param n nonce
* @param cipher cipher functions
*/
case class InitializedCipherState(k: BinaryData, n: Long, cipher: CipherFunctions) extends CipherState {
require(k.length == 32)
def hasKey = true
def encryptWithAd(ad: BinaryData, plaintext: BinaryData): (CipherState, BinaryData) = {
(this.copy(n = this.n + 1), cipher.encrypt(k, n, ad, plaintext))
}
def decryptWithAd(ad: BinaryData, ciphertext: BinaryData): (CipherState, BinaryData) = (this.copy(n = this.n + 1), cipher.decrypt(k, n, ad, ciphertext))
}
object CipherState {
def apply(k: BinaryData, cipher: CipherFunctions): CipherState = k.length match {
case 0 => UnitializedCipherState(cipher)
case 32 => InitializedCipherState(k, 0, cipher)
}
def apply(cipher: CipherFunctions): CipherState = UnitializedCipherState(cipher)
}
/**
*
* @param cipherState cipher state
* @param ck chaining key
* @param h hash
* @param hashFunctions hash functions
*/
case class SymmetricState(cipherState: CipherState, ck: BinaryData, h: BinaryData, hashFunctions: HashFunctions) extends Logging {
def mixKey(inputKeyMaterial: BinaryData): SymmetricState = {
logger.debug(s"ss = 0x$inputKeyMaterial")
val (ck1, tempk) = hashFunctions.hkdf(ck, inputKeyMaterial)
val tempk1: BinaryData = hashFunctions.hashLen match {
case 32 => tempk
case 64 => tempk.take(32)
}
this.copy(cipherState = cipherState.initializeKey(tempk1), ck = ck1)
}
def mixHash(data: BinaryData): SymmetricState = {
this.copy(h = hashFunctions.hash(h ++ data))
}
def encryptAndHash(plaintext: BinaryData): (SymmetricState, BinaryData) = {
val (cipherstate1, ciphertext) = cipherState.encryptWithAd(h, plaintext)
(this.copy(cipherState = cipherstate1).mixHash(ciphertext), ciphertext)
}
def decryptAndHash(ciphertext: BinaryData): (SymmetricState, BinaryData) = {
val (cipherstate1, plaintext) = cipherState.decryptWithAd(h, ciphertext)
(this.copy(cipherState = cipherstate1).mixHash(ciphertext), plaintext)
}
def split: (CipherState, CipherState, BinaryData) = {
val (tempk1, tempk2) = hashFunctions.hkdf(ck, BinaryData.empty)
(cipherState.initializeKey(tempk1.take(32)), cipherState.initializeKey(tempk2.take(32)), ck)
}
}
object SymmetricState {
def apply(protocolName: BinaryData, cipherFunctions: CipherFunctions, hashFunctions: HashFunctions): SymmetricState = {
val h: BinaryData = if (protocolName.length <= hashFunctions.hashLen)
protocolName ++ Seq.fill[Byte](hashFunctions.hashLen - protocolName.length)(0)
else hashFunctions.hash(protocolName)
new SymmetricState(CipherState(cipherFunctions), ck = h, h = h, hashFunctions)
}
}
sealed trait MessagePattern
case object S extends MessagePattern
case object E extends MessagePattern
case object EE extends MessagePattern
case object ES extends MessagePattern
case object SE extends MessagePattern
case object SS extends MessagePattern
type MessagePatterns = List[MessagePattern]
object HandshakePattern {
val validInitiatorPatterns: Set[MessagePatterns] = Set(Nil, E :: Nil, S :: Nil, E :: S :: Nil)
def isValidInitiator(initiator: MessagePatterns): Boolean = validInitiatorPatterns.contains(initiator)
}
case class HandshakePattern(name: String, initiatorPreMessages: MessagePatterns, responderPreMessages: MessagePatterns, messages: List[MessagePatterns]) {
import HandshakePattern._
require(isValidInitiator(initiatorPreMessages))
require(isValidInitiator(responderPreMessages))
}
/**
* standard handshake patterns
*/
val handshakePatternNN = HandshakePattern("NN", initiatorPreMessages = Nil, responderPreMessages = Nil, messages = List(E :: Nil, E :: EE :: Nil))
val handshakePatternXK = HandshakePattern("XK", initiatorPreMessages = Nil, responderPreMessages = S :: Nil, messages = List(E :: ES :: Nil, E :: EE :: Nil, S :: SE :: Nil))
trait ByteStream {
def nextBytes(length: Int): BinaryData
}
object RandomBytes extends ByteStream {
override def nextBytes(length: Int) = randomBytes(length)
}
sealed trait HandshakeState
case class HandshakeStateWriter(messages: List[MessagePatterns], state: SymmetricState, s: KeyPair, e: KeyPair, rs: BinaryData, re: BinaryData, dh: DHFunctions, byteStream: ByteStream) extends HandshakeState with Logging {
def toReader: HandshakeStateReader = HandshakeStateReader(messages, state, s, e, rs, re, dh, byteStream)
/**
*
* @param payload input message (can be empty)
* @return a (reader, output, Option[(cipherstate, cipherstate)] tuple.
* The output will be sent to the other side, and we will read its answer using the returned reader instance
* When the handshake is over (i.e. there are no more handshake patterns to process) the last item will
* contain 2 cipherstates than can be used to encrypt/decrypt further communication
*/
def write(payload: BinaryData): (HandshakeStateReader, BinaryData, Option[(CipherState, CipherState, BinaryData)]) = {
require(!messages.isEmpty)
logger.debug(s"write($payload)")
val (writer1, buffer1) = messages.head.foldLeft(this -> BinaryData.empty) {
case ((writer, buffer), pattern) => pattern match {
case E =>
val e1 = dh.generateKeyPair(byteStream.nextBytes(dh.dhLen))
val state1 = writer.state.mixHash(e1.pub)
(writer.copy(state = state1, e = e1), buffer ++ e1.pub)
case S =>
val (state1, ciphertext) = writer.state.encryptAndHash(s.pub)
(writer.copy(state = state1), buffer ++ ciphertext)
case EE =>
val state1 = writer.state.mixKey(dh.dh(writer.e, writer.re))
(writer.copy(state = state1), buffer)
case SS =>
val state1 = writer.state.mixKey(dh.dh(writer.s, writer.rs))
(writer.copy(state = state1), buffer)
case ES =>
val state1 = writer.state.mixKey(dh.dh(writer.e, writer.rs))
(writer.copy(state = state1), buffer)
case SE =>
val state1 = writer.state.mixKey(dh.dh(writer.s, writer.re))
(writer.copy(state = state1), buffer)
}
}
val (state1, ciphertext) = writer1.state.encryptAndHash(payload)
val buffer2 = buffer1 ++ ciphertext
val writer2 = writer1.copy(messages = messages.tail, state = state1)
logger.debug(s"h = 0x${state1.h}")
logger.debug(s"output: 0x${BinaryData(buffer2)}")
(writer2.toReader, buffer2, if (messages.tail.isEmpty) Some(writer2.state.split) else None)
}
}
object HandshakeStateWriter {
def apply(messages: List[MessagePatterns], state: SymmetricState, s: KeyPair, e: KeyPair, rs: BinaryData, re: BinaryData, dh: DHFunctions): HandshakeStateWriter = new HandshakeStateWriter(messages, state, s, e, rs, re, dh, RandomBytes)
}
case class HandshakeStateReader(messages: List[MessagePatterns], state: SymmetricState, s: KeyPair, e: KeyPair, rs: BinaryData, re: BinaryData, dh: DHFunctions, byteStream: ByteStream) extends HandshakeState with Logging {
def toWriter: HandshakeStateWriter = HandshakeStateWriter(messages, state, s, e, rs, re, dh, byteStream)
/** *
*
* @param message input message
* @return a (writer, payload, Option[(cipherstate, cipherstate)] tuple.
* The payload contains the original payload used be the sender and a writer that will be used to create the
* next message. When the handshake is over (i.e. there are no more handshake patterns to process) the last item will
* contain 2 cipherstates than can be used to encrypt/decrypt further communication
*/
def read(message: BinaryData): (HandshakeStateWriter, BinaryData, Option[(CipherState, CipherState, BinaryData)]) = {
logger.debug(s"input: 0x$message")
val (reader1, buffer1) = messages.head.foldLeft(this -> message) {
case ((reader, buffer), pattern) => pattern match {
case E =>
val (re1, buffer1) = buffer.splitAt(dh.pubKeyLen)
val state1 = reader.state.mixHash(re1)
(reader.copy(state = state1, re = re1), buffer1)
case S =>
val len = if (reader.state.cipherState.hasKey) dh.pubKeyLen + 16 else dh.pubKeyLen
val (temp, buffer1) = buffer.splitAt(len)
val (state1, rs1) = reader.state.decryptAndHash(temp)
logger.debug(s"rs = $rs1")
logger.debug(s"h = ${state1.h}")
(reader.copy(state = state1, rs = rs1), buffer1)
case EE =>
val state1 = reader.state.mixKey(dh.dh(reader.e, reader.re))
(reader.copy(state = state1), buffer)
case SS =>
val state1 = reader.state.mixKey(dh.dh(reader.s, reader.rs))
(reader.copy(state = state1), buffer)
case ES =>
val ss = dh.dh(reader.s, reader.re)
val state1 = reader.state.mixKey(ss)
(reader.copy(state = state1), buffer)
case SE =>
val state1 = reader.state.mixKey(dh.dh(reader.e, reader.rs))
(reader.copy(state = state1), buffer)
}
}
val (state1, payload) = reader1.state.decryptAndHash(buffer1)
logger.debug(s"h = 0x${state1.h}")
val reader2 = reader1.copy(messages = messages.tail, state = state1)
(reader2.toWriter, payload, if (messages.tail.isEmpty) Some(reader2.state.split) else None)
}
}
object HandshakeStateReader {
def apply(messages: List[MessagePatterns], state: SymmetricState, s: KeyPair, e: KeyPair, rs: BinaryData, re: BinaryData, dh: DHFunctions): HandshakeStateReader = new HandshakeStateReader(messages, state, s, e, rs, re, dh, RandomBytes)
}
object HandshakeState {
private def makeSymmetricState(handshakePattern: HandshakePattern, prologue: BinaryData, dh: DHFunctions, cipher: CipherFunctions, hash: HashFunctions): SymmetricState = {
val name = "Noise_" + handshakePattern.name + "_" + dh.name + "_" + cipher.name + "_" + hash.name
val symmetricState = SymmetricState(name.getBytes("UTF-8"), cipher, hash)
symmetricState.mixHash(prologue)
}
def initializeWriter(handshakePattern: HandshakePattern, prologue: BinaryData, s: KeyPair, e: KeyPair, rs: BinaryData, re: BinaryData, dh: DHFunctions, cipher: CipherFunctions, hash: HashFunctions, byteStream: ByteStream = RandomBytes): HandshakeStateWriter = {
val symmetricState = makeSymmetricState(handshakePattern, prologue, dh, cipher, hash)
val symmetricState1 = (handshakePattern.initiatorPreMessages).foldLeft(symmetricState) {
case (state, E) => state.mixHash(e.pub)
case (state, S) => state.mixHash(s.pub)
case _ => throw new RuntimeException("invalid pre-message")
}
val symmetricState2 = (handshakePattern.responderPreMessages).foldLeft(symmetricState1) {
case (state, E) => state.mixHash(re)
case (state, S) => state.mixHash(rs)
case _ => throw new RuntimeException("invalid pre-message")
}
HandshakeStateWriter(handshakePattern.messages, symmetricState2, s, e, rs, re, dh, byteStream)
}
def initializeReader(handshakePattern: HandshakePattern, prologue: BinaryData, s: KeyPair, e: KeyPair, rs: BinaryData, re: BinaryData, dh: DHFunctions, cipher: CipherFunctions, hash: HashFunctions, byteStream: ByteStream = RandomBytes): HandshakeStateReader = {
val symmetricState = makeSymmetricState(handshakePattern, prologue, dh, cipher, hash)
val symmetricState1 = handshakePattern.initiatorPreMessages.foldLeft(symmetricState) {
case (state, E) => state.mixHash(re)
case (state, S) => state.mixHash(rs)
case _ => throw new RuntimeException("invalid pre-message")
}
val symmetricState2 = handshakePattern.responderPreMessages.foldLeft(symmetricState1) {
case (state, E) => state.mixHash(e.pub)
case (state, S) => state.mixHash(s.pub)
case _ => throw new RuntimeException("invalid pre-message")
}
HandshakeStateReader(handshakePattern.messages, symmetricState2, s, e, rs, re, dh, byteStream)
}
}
}

View File

@ -1,6 +1,8 @@
package fr.acinq.eclair.crypto
import fr.acinq.bitcoin._
import fr.acinq.eclair.wire.LightningMessageCodecs
import scodec.Codec
import scala.annotation.tailrec
@ -9,7 +11,9 @@ import scala.annotation.tailrec
*/
object ShaChain {
case class Node(value: BinaryData, height: Int, parent: Option[Node])
case class Node(value: BinaryData, height: Int, parent: Option[Node]) {
require(value.length == 32)
}
def flip(in: BinaryData, index: Int): BinaryData = in.data.updated(index / 8, (in.data(index / 8) ^ (1 << index % 8)).toByte)
@ -18,7 +22,7 @@ object ShaChain {
* @param index 64-bit integer
* @return a binary representation of index as a sequence of 64 booleans. Each bool represents a move down the tree
*/
def moves(index: Long): Seq[Boolean] = for (i <- 63 to 0 by -1) yield (index & (1L << i)) != 0
def moves(index: Long): Vector[Boolean] = (for (i <- 63 to 0 by -1) yield (index & (1L << i)) != 0).toVector
/**
*
@ -37,7 +41,7 @@ object ShaChain {
def shaChainFromSeed(hash: BinaryData, index: Long) = derive(Node(hash, 0, None), index).value
type Index = Seq[Boolean]
type Index = Vector[Boolean]
val empty = ShaChain(Map.empty[Index, BinaryData])
@ -51,7 +55,7 @@ object ShaChain {
val parentIndex = index.dropRight(1)
// hashes are supposed to be received in reverse order so we already have parent :+ true
// which we should be able to recompute (it's a left node so its hash is the same as its parent's hash)
assert(getHash(receiver, parentIndex :+ true) == Some(derive(Node(hash, parentIndex.length, None), true).value))
require(getHash(receiver, parentIndex :+ true) == Some(derive(Node(hash, parentIndex.length, None), true).value), "invalid hash")
val nodes1 = receiver.knownHashes - (parentIndex :+ false) - (parentIndex :+ true)
addHash(receiver.copy(knownHashes = nodes1), hash, parentIndex)
}
@ -91,6 +95,26 @@ object ShaChain {
}
}
}
val shaChainCodec: Codec[ShaChain] = {
import scodec.Codec
import scodec.bits.BitVector
import scodec.codecs._
// codec for a single map entry (i.e. Vector[Boolean] -> BinaryData
val entryCodec = vectorOfN(uint16, bool) ~ LightningMessageCodecs.varsizebinarydata
// codec for a Map[Vector[Boolean], BinaryData]: write all k ->v pairs using the codec defined above
val mapCodec: Codec[Map[Vector[Boolean], BinaryData]] = Codec[Map[Vector[Boolean], BinaryData]](
(m: Map[Vector[Boolean], BinaryData]) => vectorOfN(uint16, entryCodec).encode(m.toVector),
(b: BitVector) => vectorOfN(uint16, entryCodec).decode(b).map(_.map(_.toMap))
)
// our shachain codec
(("knownHashes" | mapCodec) :: ("lastIndex" | optional(bool, int64))).as[ShaChain]
}
}
/**
@ -100,7 +124,7 @@ object ShaChain {
* @param lastIndex index of the last known hash. Hashes are supposed to be added in reverse order i.e.
* from 0xFFFFFFFFFFFFFFFF down to 0
*/
case class ShaChain(knownHashes: Map[Seq[Boolean], BinaryData], lastIndex: Option[Long] = None) {
case class ShaChain(knownHashes: Map[Vector[Boolean], BinaryData], lastIndex: Option[Long] = None) {
def addHash(hash: BinaryData, index: Long): ShaChain = ShaChain.addHash(this, hash, index)
def getHash(index: Long) = ShaChain.getHash(this, index)

View File

@ -0,0 +1,355 @@
package fr.acinq.eclair.crypto
import java.io.{ByteArrayInputStream, ByteArrayOutputStream, InputStream, OutputStream}
import java.nio.ByteOrder
import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey}
import fr.acinq.bitcoin.{BinaryData, Crypto, Protocol}
import fr.acinq.eclair.wire.{FailureMessage, FailureMessageCodecs}
import grizzled.slf4j.Logging
import org.spongycastle.crypto.digests.SHA256Digest
import org.spongycastle.crypto.macs.HMac
import org.spongycastle.crypto.params.KeyParameter
import scodec.bits.BitVector
import scala.annotation.tailrec
/**
* Created by fabrice on 13/01/17.
* see https://github.com/lightningnetwork/lightning-rfc/blob/master/04-onion-routing.md
*/
object Sphinx extends Logging {
val Version = 0.toByte
// length of a MAC
val MacLength = 32
// length of a payload: 33 bytes (1 bytes for realm, 32 bytes for a realm-specific packet)
val PayloadLength = 33
// max number of hops
val MaxHops = 20
// onion packet length
val PacketLength = 1 + 33 + MacLength + MaxHops * (PayloadLength + MacLength)
// last packet (all zeroes except for the version byte)
val LAST_PACKET = Packet(Version, zeroes(33), zeroes(MacLength), zeroes(MaxHops * (PayloadLength + MacLength)))
def hmac256(key: Seq[Byte], message: Seq[Byte]): Seq[Byte] = {
val mac = new HMac(new SHA256Digest())
mac.init(new KeyParameter(key.toArray))
mac.update(message.toArray, 0, message.length)
val output = new Array[Byte](32)
mac.doFinal(output, 0)
output
}
def mac(key: BinaryData, message: BinaryData): BinaryData = hmac256(key, message).take(MacLength)
def xor(a: Seq[Byte], b: Seq[Byte]): Seq[Byte] = a.zip(b).map { case (x, y) => ((x ^ y) & 0xff).toByte }
def generateKey(keyType: BinaryData, secret: BinaryData): BinaryData = {
require(secret.length == 32, "secret must be 32 bytes")
hmac256(keyType, secret)
}
def generateKey(keyType: String, secret: BinaryData): BinaryData = generateKey(keyType.getBytes("UTF-8"), secret)
def zeroes(length: Int): BinaryData = Seq.fill[Byte](length)(0)
def generateStream(key: BinaryData, length: Int): BinaryData = ChaCha20Legacy.encrypt(zeroes(length), key, zeroes(8))
def computeSharedSecret(pub: PublicKey, secret: PrivateKey): BinaryData = Crypto.sha256(pub.multiply(secret).normalize().getEncoded(true))
def computeblindingFactor(pub: PublicKey, secret: BinaryData): BinaryData = Crypto.sha256(pub.toBin ++ secret)
def blind(pub: PublicKey, blindingFactor: BinaryData): PublicKey = PublicKey(pub.multiply(blindingFactor).normalize(), compressed = true)
def blind(pub: PublicKey, blindingFactors: Seq[BinaryData]): PublicKey = blindingFactors.foldLeft(pub)(blind)
/**
* computes the ephemereal public keys and shared secrets for all nodes on the route.
*
* @param sessionKey this node's session key
* @param publicKeys public keys of each node on the route
* @return a tuple (ephemereal public keys, shared secrets)
*/
def computeEphemerealPublicKeysAndSharedSecrets(sessionKey: PrivateKey, publicKeys: Seq[PublicKey]): (Seq[PublicKey], Seq[BinaryData]) = {
val ephemerealPublicKey0 = blind(PublicKey(Crypto.curve.getG, compressed = true), sessionKey.value)
val secret0 = computeSharedSecret(publicKeys(0), sessionKey)
val blindingFactor0 = computeblindingFactor(ephemerealPublicKey0, secret0)
computeEphemerealPublicKeysAndSharedSecrets(sessionKey, publicKeys.tail, Seq(ephemerealPublicKey0), Seq(blindingFactor0), Seq(secret0))
}
@tailrec
def computeEphemerealPublicKeysAndSharedSecrets(sessionKey: PrivateKey, publicKeys: Seq[PublicKey], ephemerealPublicKeys: Seq[PublicKey], blindingFactors: Seq[BinaryData], sharedSecrets: Seq[BinaryData]): (Seq[PublicKey], Seq[BinaryData]) = {
if (publicKeys.isEmpty)
(ephemerealPublicKeys, sharedSecrets)
else {
val ephemerealPublicKey = blind(ephemerealPublicKeys.last, blindingFactors.last)
val secret = computeSharedSecret(blind(publicKeys.head, blindingFactors), sessionKey)
val blindingFactor = computeblindingFactor(ephemerealPublicKey, secret)
computeEphemerealPublicKeysAndSharedSecrets(sessionKey, publicKeys.tail, ephemerealPublicKeys :+ ephemerealPublicKey, blindingFactors :+ blindingFactor, sharedSecrets :+ secret)
}
}
def generateFiller(keyType: String, sharedSecrets: Seq[BinaryData], hopSize: Int, maxNumberOfHops: Int = MaxHops): BinaryData = {
sharedSecrets.foldLeft(Seq.empty[Byte])((padding, secret) => {
val key = generateKey(keyType, secret)
val padding1 = padding ++ zeroes(hopSize)
val stream = generateStream(key, hopSize * (maxNumberOfHops + 1)).takeRight(padding1.length)
xor(padding1, stream)
})
}
case class Packet(version: Int, publicKey: BinaryData, hmac: BinaryData, routingInfo: BinaryData) {
require(publicKey.length == 33, "onion packet public key length should be 33")
require(hmac.length == MacLength, s"onion packet hmac length should be $MacLength")
require(routingInfo.length == MaxHops * (PayloadLength + MacLength), s"onion packet routing info length should be ${MaxHops * (PayloadLength + MacLength)}")
def isLastPacket: Boolean = hmac == zeroes(MacLength)
def serialize: BinaryData = Packet.write(this)
}
object Packet {
def read(in: InputStream): Packet = {
val version = in.read
val publicKey = new Array[Byte](33)
in.read(publicKey)
val routingInfo = new Array[Byte](MaxHops * (PayloadLength + MacLength))
in.read(routingInfo)
val hmac = new Array[Byte](MacLength)
in.read(hmac)
Packet(version, publicKey, hmac, routingInfo)
}
def read(in: BinaryData): Packet = read(new ByteArrayInputStream(in))
def write(packet: Packet, out: OutputStream): OutputStream = {
out.write(packet.version)
out.write(packet.publicKey)
out.write(packet.routingInfo)
out.write(packet.hmac)
out
}
def write(packet: Packet): BinaryData = {
val out = new ByteArrayOutputStream(PacketLength)
write(packet, out)
out.toByteArray
}
def isLastPacket(packet: BinaryData): Boolean = Packet.read(packet).hmac == zeroes(MacLength)
}
/**
*
* @param payload payload for this node
* @param nextPacket packet for the next node
* @param sharedSecret shared secret for the sending node, which we will need to return error messages
*/
case class ParsedPacket(payload: BinaryData, nextPacket: Packet, sharedSecret: BinaryData)
/**
*
* @param privateKey this node's private key
* @param associatedData associated data
* @param rawPacket packet received by this node
* @return a ParsedPacket(payload, packet, shared secret) object where:
* - payload is the per-hop payload for this node
* - packet is the next packet, to be forwarded using the info that is given in payload (channel id for now)
* - shared secret is the secret we share with the node that sent the packet. We need it to propagate failure
* messages upstream.
*/
def parsePacket(privateKey: PrivateKey, associatedData: BinaryData, rawPacket: BinaryData): ParsedPacket = {
require(rawPacket.length == PacketLength, s"onion packet length is ${rawPacket.length}, it should be ${PacketLength}")
val packet = Packet.read(rawPacket)
val sharedSecret = computeSharedSecret(PublicKey(packet.publicKey), privateKey)
val mu = generateKey("mu", sharedSecret)
val check: BinaryData = mac(mu, packet.routingInfo ++ associatedData)
require(check == packet.hmac, "invalid header mac")
val rho = generateKey("rho", sharedSecret)
val bin = xor(packet.routingInfo ++ zeroes(PayloadLength + MacLength), generateStream(rho, PayloadLength + MacLength + MaxHops * (PayloadLength + MacLength)))
val payload = bin.take(PayloadLength)
val hmac = bin.slice(PayloadLength, PayloadLength + MacLength)
val nextRoutinfo = bin.drop(PayloadLength + MacLength)
val nextPubKey = blind(PublicKey(packet.publicKey), computeblindingFactor(PublicKey(packet.publicKey), sharedSecret))
ParsedPacket(payload, Packet(Version, nextPubKey, hmac, nextRoutinfo), sharedSecret)
}
@tailrec
def extractSharedSecrets(packet: BinaryData, privateKey: PrivateKey, associatedData: BinaryData, acc: Seq[BinaryData] = Nil): Seq[BinaryData] = {
parsePacket(privateKey, associatedData, packet) match {
case ParsedPacket(_, nextPacket, sharedSecret) if nextPacket.isLastPacket => acc :+ sharedSecret
case ParsedPacket(_, nextPacket, sharedSecret) => extractSharedSecrets(nextPacket.serialize, privateKey, associatedData, acc :+ sharedSecret)
}
}
/**
* Compute the next packet from the current packet and node parameters.
* Packets are constructed in reverse order:
* - you first build the last packet
* - then you call makeNextPacket(...) until you've build the final onion packet that will be sent to the first node
* in the route
*
* @param payload payload for this packed
* @param associatedData associated data
* @param ephemerealPublicKey ephemereal key for this packed
* @param sharedSecret shared secret
* @param packet current packet (1 + all zeroes if this is the last packet)
* @param routingInfoFiller optional routing info filler, needed only when you're constructing the last packet
* @return the next packet
*/
def makeNextPacket(payload: BinaryData, associatedData: BinaryData, ephemerealPublicKey: BinaryData, sharedSecret: BinaryData, packet: Packet, routingInfoFiller: BinaryData = BinaryData.empty): Packet = {
require(payload.length == PayloadLength)
val nextRoutingInfo = {
val routingInfo1 = payload ++ packet.hmac ++ packet.routingInfo.dropRight(PayloadLength + MacLength)
val routingInfo2 = xor(routingInfo1, generateStream(generateKey("rho", sharedSecret), MaxHops * (PayloadLength + MacLength)))
routingInfo2.dropRight(routingInfoFiller.length) ++ routingInfoFiller
}
val nextHmac: BinaryData = mac(generateKey("mu", sharedSecret), nextRoutingInfo ++ associatedData)
val nextPacket = Packet(Version, ephemerealPublicKey, nextHmac, nextRoutingInfo)
nextPacket
}
/**
*
* @param packet onion packet
* @param sharedSecrets shared secrets (one per node in the route). Known (and needed) only if you're creating the
* packet. Empty if you're just forwarding the packet to the next node
*/
case class PacketAndSecrets(packet: Packet, sharedSecrets: Seq[(BinaryData, PublicKey)])
/**
* A properly decoded error from a node in the route
*
* @param originNode
* @param failureMessage
*/
case class ErrorPacket(originNode: PublicKey, failureMessage: FailureMessage)
/**
* Builds an encrypted onion packet that contains payloads and routing information for all nodes in the list
*
* @param sessionKey session key
* @param publicKeys node public keys (one per node)
* @param payloads payloads (one per node)
* @param associatedData associated data
* @return an OnionPacket(onion packet, shared secrets). the onion packet can be sent to the first node in the list, and the
* shared secrets (one per node) can be used to parse returned error messages if needed
*/
def makePacket(sessionKey: PrivateKey, publicKeys: Seq[PublicKey], payloads: Seq[BinaryData], associatedData: BinaryData): PacketAndSecrets = {
val (ephemerealPublicKeys, sharedsecrets) = computeEphemerealPublicKeysAndSharedSecrets(sessionKey, publicKeys)
val filler = generateFiller("rho", sharedsecrets.dropRight(1), PayloadLength + MacLength, MaxHops)
val lastPacket = makeNextPacket(payloads.last, associatedData, ephemerealPublicKeys.last, sharedsecrets.last, LAST_PACKET, filler)
@tailrec
def loop(hoppayloads: Seq[BinaryData], ephkeys: Seq[PublicKey], sharedSecrets: Seq[BinaryData], packet: Packet): Packet = {
if (hoppayloads.isEmpty) packet else {
val nextPacket = makeNextPacket(hoppayloads.last, associatedData, ephkeys.last, sharedSecrets.last, packet)
loop(hoppayloads.dropRight(1), ephkeys.dropRight(1), sharedSecrets.dropRight(1), nextPacket)
}
}
val packet = loop(payloads.dropRight(1), ephemerealPublicKeys.dropRight(1), sharedsecrets.dropRight(1), lastPacket)
PacketAndSecrets(packet, sharedsecrets.zip(publicKeys))
}
/*
error packet format:
+----------------+----------------------------------+-----------------+----------------------+-----+
| HMAC(32 bytes) | failure message length (2 bytes) | failure message | pad length (2 bytes) | pad |
+----------------+----------------------------------+-----------------+----------------------+-----+
with failure message length + pad length = 256
*/
val MaxErrorPayloadLength = 256
val ErrorPacketLength = MacLength + MaxErrorPayloadLength + 2 + 2
/**
*
* @param sharedSecret destination node's shared secret that was computed when the original onion for the HTLC
* was created or forwarded: see makePacket() and makeNextPacket()
* @param failure failure message
* @return an error packet that can be sent to the destination node
*/
def createErrorPacket(sharedSecret: BinaryData, failure: FailureMessage): BinaryData = {
val message: BinaryData = FailureMessageCodecs.failureMessageCodec.encode(failure).require.toByteArray
require(message.length <= MaxErrorPayloadLength, s"error message length is ${message.length}, it must be less than $MaxErrorPayloadLength")
val um = Sphinx.generateKey("um", sharedSecret)
val padlen = MaxErrorPayloadLength - message.length
val payload = Protocol.writeUInt16(message.length, ByteOrder.BIG_ENDIAN) ++ message ++ Protocol.writeUInt16(padlen, ByteOrder.BIG_ENDIAN) ++ Sphinx.zeroes(padlen)
logger.debug(s"um key: $um")
logger.debug(s"error payload: ${BinaryData(payload)}")
logger.debug(s"raw error packet: ${BinaryData(Sphinx.mac(um, payload) ++ payload)}")
forwardErrorPacket(Sphinx.mac(um, payload) ++ payload, sharedSecret)
}
/**
*
* @param packet error packet
* @return the failure message that is embedded in the error packet
*/
def extractFailureMessage(packet: BinaryData): FailureMessage = {
require(packet.length == ErrorPacketLength, s"invalid error packet length ${packet.length}, must be $ErrorPacketLength")
val (mac, payload) = packet.splitAt(Sphinx.MacLength)
val len = Protocol.uint16(payload, ByteOrder.BIG_ENDIAN)
require((len >= 0) && (len <= MaxErrorPayloadLength), s"message length must be less than $MaxErrorPayloadLength")
FailureMessageCodecs.failureMessageCodec.decode(BitVector(payload.drop(2).take(len))).require.value
}
/**
*
* @param packet error packet
* @param sharedSecret destination node's shared secret
* @return an obfuscated error packet that can be sent to the destination node
*/
def forwardErrorPacket(packet: BinaryData, sharedSecret: BinaryData): BinaryData = {
require(packet.length == ErrorPacketLength, s"invalid error packet length ${packet.length}, must be $ErrorPacketLength")
val key = generateKey("ammag", sharedSecret)
val stream = generateStream(key, ErrorPacketLength)
logger.debug(s"ammag key: $key")
logger.debug(s"error stream: $stream")
Sphinx.xor(packet, stream)
}
/**
*
* @param sharedSecret this node's share secret
* @param packet error packet
* @return true if the packet's mac is valid, which means that it has been properly de-obfuscated
*/
def checkMac(sharedSecret: BinaryData, packet: BinaryData): Boolean = {
val (mac, payload) = packet.splitAt(Sphinx.MacLength)
val um = Sphinx.generateKey("um", sharedSecret)
BinaryData(mac) == Sphinx.mac(um, payload)
}
/**
* Parse and de-obfuscate an error packet. Node shared secrets are applied until the packet's MAC becomes valid,
* which means that it was sent by the corresponding node.
*
* @param packet error packet
* @param sharedSecrets nodes shared secrets
* @return Some(secret, failure message) if the origin of the packet could be identified and the packet de-obfuscated, none otherwise
*/
@tailrec
def parseErrorPacket(packet: BinaryData, sharedSecrets: Seq[(BinaryData, PublicKey)]): Option[ErrorPacket] = {
require(packet.length == ErrorPacketLength, s"invalid error packet length ${packet.length}, must be $ErrorPacketLength")
sharedSecrets match {
case Nil => None
case (secret, pubkey) :: tail =>
val packet1 = forwardErrorPacket(packet, secret)
if (checkMac(secret, packet1)) Some(ErrorPacket(pubkey, extractFailureMessage(packet1))) else parseErrorPacket(packet1, tail)
}
}
}

View File

@ -0,0 +1,292 @@
package fr.acinq.eclair.crypto
import java.nio.ByteOrder
import akka.actor.{Actor, ActorRef, FSM, Props, Terminated}
import akka.io.Tcp.{PeerClosed, _}
import akka.util.ByteString
import fr.acinq.bitcoin.Crypto.PublicKey
import fr.acinq.bitcoin.{BinaryData, Protocol}
import fr.acinq.eclair.crypto.Noise._
import fr.acinq.eclair.io.WriteAckSender
import scodec.bits.BitVector
import scodec.{Attempt, Codec, DecodeResult}
import scala.annotation.tailrec
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)
* - encryption/decryption of messages
*
* Once the initial handshake has been completed successfully, the handler will create a listener actor with the
* provided factory, and will forward it all decrypted messages
*
* @param keyPair private/public key pair for this node
* @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[BinaryData], connection: ActorRef, codec: Codec[T]) extends Actor with FSM[TransportHandler.State, TransportHandler.Data] {
import TransportHandler._
connection ! akka.io.Tcp.Register(self)
val out = context.actorOf(Props(new WriteAckSender(connection)))
def buf(message: BinaryData): ByteString = ByteString.fromArray(message)
// it means we initiate the dialog
val isWriter = rs.isDefined
context.watch(connection)
val reader = if (isWriter) {
val state = makeWriter(keyPair, rs.get)
val (state1, message, None) = state.write(BinaryData.empty)
log.debug(s"sending prefix + $message")
out ! buf(TransportHandler.prefix +: message)
state1
} else {
makeReader(keyPair)
}
def sendToListener(listener: ActorRef, plaintextMessages: Seq[BinaryData]) = {
plaintextMessages.map(plaintext => {
codec.decode(BitVector(plaintext.data)) match {
case Attempt.Successful(DecodeResult(message, _)) => listener ! message
case Attempt.Failure(err) => log.error(s"cannot deserialize $plaintext: $err")
}
})
}
startWith(Handshake, HandshakeData(reader))
when(Handshake) {
case Event(Received(data), HandshakeData(reader, buffer)) =>
log.debug(s"received ${BinaryData(data)}")
val buffer1 = buffer ++ data
if (buffer1.length < expectedLength(reader))
stay using HandshakeData(reader, buffer1)
else {
require(buffer1.head == TransportHandler.prefix, s"invalid transport prefix ${buffer1.head}")
val (payload, remainder) = buffer1.tail.splitAt(expectedLength(reader) - 1)
reader.read(payload) match {
case (writer, _, Some((dec, enc, ck))) =>
val remoteNodeId = PublicKey(writer.rs)
context.parent ! HandshakeCompleted(self, remoteNodeId)
val nextStateData = WaitingForListenerData(ExtendedCipherState(enc, ck), ExtendedCipherState(dec, ck), remainder)
goto(WaitingForListener) using nextStateData
case (writer, _, None) => {
writer.write(BinaryData.empty) match {
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
require(remainder.isEmpty, "unexpected additional data received during handshake")
out ! buf(TransportHandler.prefix +: message)
stay using HandshakeData(reader1, remainder)
}
case (_, message, Some((enc, dec, ck))) => {
out ! buf(TransportHandler.prefix +: message)
val remoteNodeId = PublicKey(writer.rs)
context.parent ! HandshakeCompleted(self, remoteNodeId)
val nextStateData = WaitingForListenerData(ExtendedCipherState(enc, ck), ExtendedCipherState(dec, ck), remainder)
goto(WaitingForListener) using nextStateData
}
}
}
}
}
}
when(WaitingForListener) {
case Event(Received(data), currentStateData@WaitingForListenerData(enc, dec, buffer)) =>
stay using currentStateData.copy(buffer = buffer ++ data)
case Event(Listener(listener), WaitingForListenerData(enc, dec, buffer)) =>
val (nextStateData, plaintextMessages) = WaitingForCyphertextData(enc, dec, None, buffer, listener).decrypt
context.watch(listener)
sendToListener(listener, plaintextMessages)
goto(WaitingForCyphertext) using nextStateData
}
when(WaitingForCyphertext) {
case Event(Received(data), currentStateData@WaitingForCyphertextData(enc, dec, length, buffer, listener)) =>
val (nextStateData, plaintextMessages) = WaitingForCyphertextData.decrypt(currentStateData.copy(buffer = buffer ++ data))
sendToListener(listener, plaintextMessages)
stay using nextStateData
case Event(t: T, WaitingForCyphertextData(enc, dec, length, buffer, listener)) =>
val blob = codec.encode(t).require.toByteArray
val (enc1, ciphertext) = TransportHandler.encrypt(enc, blob)
out ! buf(ciphertext)
stay using WaitingForCyphertextData(enc1, dec, length, buffer, listener)
}
whenUnhandled {
case Event(ErrorClosed(cause), _) =>
// we transform connection closed events into application error so that it triggers a uniclose
log.warning(s"tcp connection error: $cause")
stop(FSM.Normal)
case Event(PeerClosed, _) =>
log.warning(s"connection closed")
stop(FSM.Normal)
case Event(Terminated(actor), _) if actor == connection =>
log.warning(s"connection terminated, stopping the transport")
// this can be the connection or the listener, either way it is a cause of death
stop(FSM.Normal)
}
override def aroundPostStop(): Unit = connection ! Close
initialize()
}
object TransportHandler {
// see BOLT #8
// this prefix is prepended to all Noise messages sent during the hanshake phase
val prefix: Byte = 0
val prologue = "lightning".getBytes("UTF-8")
/**
* See BOLT #8: during the handshake phase we are expecting 3 messages of 50, 50 and 66 bytes (including the prefix)
*
* @param reader handshake state reader
* @return the size of the message the reader is expecting
*/
def expectedLength(reader: Noise.HandshakeStateReader) = reader.messages.length match {
case 3 | 2 => 50
case 1 => 66
}
/**
* see BOLT #8
* +-------------------------------
* |2-byte encrypted message length|
* +-------------------------------
* | 16-byte MAC of the encrypted |
* | message length |
* +-------------------------------
* | |
* | |
* | encrypted lightning |
* | message |
* | |
* +-------------------------------
* | 16-byte MAC of the |
* | lightning message |
* +-------------------------------
*
* @param enc cipherstate
* @param plaintext plaintext
* @return a (cipherstate, ciphertext) tuple where ciphertext is encrypted according to BOLT #8
*/
def encrypt(enc: CipherState, plaintext: BinaryData): (CipherState, BinaryData) = {
val (enc1, ciphertext1) = enc.encryptWithAd(BinaryData.empty, Protocol.writeUInt16(plaintext.length, ByteOrder.BIG_ENDIAN))
val (enc2, ciphertext2) = enc1.encryptWithAd(BinaryData.empty, plaintext)
(enc2, ciphertext1 ++ ciphertext2)
}
def makeWriter(localStatic: KeyPair, remoteStatic: BinaryData) = Noise.HandshakeState.initializeWriter(
Noise.handshakePatternXK, prologue,
localStatic, KeyPair(BinaryData.empty, BinaryData.empty), remoteStatic, BinaryData.empty,
Noise.Secp256k1DHFunctions, Noise.Chacha20Poly1305CipherFunctions, Noise.SHA256HashFunctions)
def makeReader(localStatic: KeyPair) = Noise.HandshakeState.initializeReader(
Noise.handshakePatternXK, prologue,
localStatic, KeyPair(BinaryData.empty, BinaryData.empty), BinaryData.empty, BinaryData.empty,
Noise.Secp256k1DHFunctions, Noise.Chacha20Poly1305CipherFunctions, Noise.SHA256HashFunctions)
// @formatter:off
sealed trait State
case object Handshake extends State
case object WaitingForListener extends State
case object WaitingForCyphertext extends State
// @formatter:on
case class Listener(listener: ActorRef)
case class HandshakeCompleted(transport: ActorRef, remoteNodeId: PublicKey)
sealed trait Data
case class HandshakeData(reader: Noise.HandshakeStateReader, buffer: ByteString = ByteString.empty) extends Data
/**
* extended cipher state which implements key rotation as per BOLT #8
*
* @param cs cipher state
* @param ck chaining key
*/
case class ExtendedCipherState(cs: CipherState, ck: BinaryData) extends CipherState {
override def cipher: CipherFunctions = cs.cipher
override def hasKey: Boolean = cs.hasKey
override def encryptWithAd(ad: BinaryData, plaintext: BinaryData): (CipherState, BinaryData) = {
cs match {
case UnitializedCipherState(_) => (this, plaintext)
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, _) => {
val (cs1, ciphertext) = cs.encryptWithAd(ad, plaintext)
(this.copy(cs = cs1), ciphertext)
}
}
}
override def decryptWithAd(ad: BinaryData, ciphertext: BinaryData): (CipherState, BinaryData) = {
cs match {
case UnitializedCipherState(_) => (this, ciphertext)
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, _) => {
val (cs1, plaintext) = cs.decryptWithAd(ad, ciphertext)
(this.copy(cs = cs1), plaintext)
}
}
}
}
case class WaitingForListenerData(enc: CipherState, dec: CipherState, buffer: ByteString) extends Data
case class WaitingForCyphertextData(enc: CipherState, dec: CipherState, ciphertextLength: Option[Int], buffer: ByteString, listener: ActorRef) extends Data {
def decrypt: (WaitingForCyphertextData, Seq[BinaryData]) = WaitingForCyphertextData.decrypt(this)
}
object WaitingForCyphertextData {
@tailrec
def decrypt(state: WaitingForCyphertextData, acc: Seq[BinaryData] = Nil): (WaitingForCyphertextData, Seq[BinaryData]) = {
(state.ciphertextLength, state.buffer.length) match {
case (None, length) if length < 18 => (state, acc)
case (None, _) =>
val (ciphertext, remainder) = state.buffer.splitAt(18)
val (dec1, plaintext) = state.dec.decryptWithAd(BinaryData.empty, ciphertext)
val length = Protocol.uint16(plaintext, ByteOrder.BIG_ENDIAN)
decrypt(state.copy(dec = dec1, ciphertextLength = Some(length), buffer = remainder), acc)
case (Some(expectedLength), length) if length < expectedLength + 16 => (state, acc)
case (Some(expectedLength), _) =>
val (ciphertext, remainder) = state.buffer.splitAt(expectedLength + 16)
val (dec1, plaintext) = state.dec.decryptWithAd(BinaryData.empty, ciphertext)
decrypt(state.copy(dec = dec1, ciphertextLength = None, buffer = remainder), acc :+ plaintext)
}
}
}
}

View File

@ -0,0 +1,14 @@
package fr.acinq.eclair.db
import fr.acinq.bitcoin.BinaryData
import fr.acinq.eclair.channel.HasCommitments
trait ChannelsDb {
def addOrUpdateChannel(state: HasCommitments)
def removeChannel(channelId: BinaryData)
def listChannels(): List[HasCommitments]
}

View File

@ -0,0 +1,34 @@
package fr.acinq.eclair.db
import fr.acinq.bitcoin.Crypto.PublicKey
import fr.acinq.eclair.wire.{ChannelAnnouncement, ChannelUpdate, NodeAnnouncement}
trait NetworkDb {
def addNode(n: NodeAnnouncement)
def updateNode(n: NodeAnnouncement)
def removeNode(nodeId: PublicKey)
def listNodes(): List[NodeAnnouncement]
def addChannel(c: ChannelAnnouncement)
/**
* This method removes 1 channel announcement and 2 channel updates (at both ends of the same channel)
*
* @param shortChannelId
* @return
*/
def removeChannel(shortChannelId: Long)
def listChannels(): List[ChannelAnnouncement]
def addChannelUpdate(u: ChannelUpdate)
def updateChannelUpdate(u: ChannelUpdate)
def listChannelUpdates(): List[ChannelUpdate]
}

View File

@ -0,0 +1,15 @@
package fr.acinq.eclair.db
import java.net.InetSocketAddress
import fr.acinq.bitcoin.Crypto.PublicKey
trait PeersDb {
def addOrUpdatePeer(nodeId: PublicKey, address: InetSocketAddress)
def removePeer(nodeId: PublicKey)
def listPeers(): List[(PublicKey, InetSocketAddress)]
}

View File

@ -0,0 +1,25 @@
package fr.acinq.eclair.db
import fr.acinq.bitcoin.BinaryData
/**
* This database stores the preimages that we have received from downstream
* (either directly via UpdateFulfillHtlc or by extracting the value from the
* blockchain).
*
* This means that this database is only used in the context of *relaying* payments.
*
* We need to be sure that if downstream is able to pulls funds from us, we can always
* do the same from upstream, otherwise we lose money. Hence the need for persistence
* to handle all corner cases.
*
*/
trait PreimagesDb {
def addPreimage(channelId: BinaryData, htlcId: Long, paymentPreimage: BinaryData)
def removePreimage(channelId: BinaryData, htlcId: Long)
def listPreimages(channelId: BinaryData): List[(BinaryData, Long, BinaryData)]
}

View File

@ -0,0 +1,46 @@
package fr.acinq.eclair.db.sqlite
import java.sql.Connection
import fr.acinq.bitcoin.BinaryData
import fr.acinq.eclair.channel.HasCommitments
import fr.acinq.eclair.db.ChannelsDb
import fr.acinq.eclair.wire.ChannelCodecs.stateDataCodec
class SqliteChannelsDb(sqlite: Connection) extends ChannelsDb {
import SqliteUtils._
{
val statement = sqlite.createStatement
statement.executeUpdate("CREATE TABLE IF NOT EXISTS local_channels (channel_id BLOB NOT NULL PRIMARY KEY, data BLOB NOT NULL)")
}
override def addOrUpdateChannel(state: HasCommitments): Unit = {
val data = stateDataCodec.encode(state).require.toByteArray
val update = sqlite.prepareStatement("UPDATE local_channels SET data=? WHERE channel_id=?")
update.setBytes(1, data)
update.setBytes(2, state.channelId)
if (update.executeUpdate() == 0) {
val statement = sqlite.prepareStatement("INSERT INTO local_channels VALUES (?, ?)")
statement.setBytes(1, state.channelId)
statement.setBytes(2, data)
statement.executeUpdate()
}
}
override def removeChannel(channelId: BinaryData): Unit = {
val statement1 = sqlite.prepareStatement("DELETE FROM preimages WHERE channel_id=?")
statement1.setBytes(1, channelId)
statement1.executeUpdate()
val statement2 = sqlite.prepareStatement("DELETE FROM local_channels WHERE channel_id=?")
statement2.setBytes(1, channelId)
statement2.executeUpdate()
}
override def listChannels(): List[HasCommitments] = {
val rs = sqlite.createStatement.executeQuery("SELECT data FROM local_channels")
codecList(rs, stateDataCodec)
}
}

View File

@ -0,0 +1,90 @@
package fr.acinq.eclair.db.sqlite
import java.sql.Connection
import fr.acinq.bitcoin.Crypto
import fr.acinq.eclair.db.NetworkDb
import fr.acinq.eclair.router.Announcements
import fr.acinq.eclair.wire.LightningMessageCodecs.{channelAnnouncementCodec, channelUpdateCodec, nodeAnnouncementCodec}
import fr.acinq.eclair.wire.{ChannelAnnouncement, ChannelUpdate, NodeAnnouncement}
class SqliteNetworkDb(sqlite: Connection) extends NetworkDb {
import SqliteUtils._
{
val statement = sqlite.createStatement
statement.execute("PRAGMA foreign_keys = ON")
statement.executeUpdate("CREATE TABLE IF NOT EXISTS nodes (node_id BLOB NOT NULL PRIMARY KEY, data BLOB NOT NULL)")
statement.executeUpdate("CREATE TABLE IF NOT EXISTS channels (short_channel_id INTEGER NOT NULL PRIMARY KEY, data BLOB NOT NULL)")
statement.executeUpdate("CREATE TABLE IF NOT EXISTS channel_updates (short_channel_id INTEGER NOT NULL, node_flag INTEGER NOT NULL, data BLOB NOT NULL, PRIMARY KEY(short_channel_id, node_flag), FOREIGN KEY(short_channel_id) REFERENCES channels(short_channel_id))")
statement.executeUpdate("CREATE INDEX IF NOT EXISTS channel_updates_idx ON channel_updates(short_channel_id)")
}
override def addNode(n: NodeAnnouncement): Unit = {
val statement = sqlite.prepareStatement("INSERT OR IGNORE INTO nodes VALUES (?, ?)")
statement.setBytes(1, n.nodeId.toBin)
statement.setBytes(2, nodeAnnouncementCodec.encode(n).require.toByteArray)
statement.executeUpdate()
}
override def updateNode(n: NodeAnnouncement): Unit = {
val statement = sqlite.prepareStatement("UPDATE nodes SET data=? WHERE node_id=?")
statement.setBytes(1, nodeAnnouncementCodec.encode(n).require.toByteArray)
statement.setBytes(2, n.nodeId.toBin)
statement.executeUpdate()
}
override def removeNode(nodeId: Crypto.PublicKey): Unit = {
val statement = sqlite.prepareStatement("DELETE FROM nodes WHERE node_id=?")
statement.setBytes(1, nodeId.toBin)
statement.executeUpdate()
}
override def listNodes(): List[NodeAnnouncement] = {
val rs = sqlite.createStatement.executeQuery("SELECT data FROM nodes")
codecList(rs, nodeAnnouncementCodec)
}
override def addChannel(c: ChannelAnnouncement): Unit = {
val statement = sqlite.prepareStatement("INSERT OR IGNORE INTO channels VALUES (?, ?)")
statement.setLong(1, c.shortChannelId)
statement.setBytes(2, channelAnnouncementCodec.encode(c).require.toByteArray)
statement.executeUpdate()
}
override def removeChannel(shortChannelId: Long): Unit = {
val statement = sqlite.createStatement
statement.execute("BEGIN TRANSACTION")
statement.executeUpdate(s"DELETE FROM channel_updates WHERE short_channel_id=$shortChannelId")
statement.executeUpdate(s"DELETE FROM channels WHERE short_channel_id=$shortChannelId")
statement.execute("COMMIT TRANSACTION")
}
override def listChannels(): List[ChannelAnnouncement] = {
val rs = sqlite.createStatement.executeQuery("SELECT data FROM channels")
codecList(rs, channelAnnouncementCodec)
}
override def addChannelUpdate(u: ChannelUpdate): Unit = {
val statement = sqlite.prepareStatement("INSERT OR IGNORE INTO channel_updates VALUES (?, ?, ?)")
statement.setLong(1, u.shortChannelId)
statement.setBoolean(2, Announcements.isNode1(u.flags))
statement.setBytes(3, channelUpdateCodec.encode(u).require.toByteArray)
statement.executeUpdate()
}
override def updateChannelUpdate(u: ChannelUpdate): Unit = {
val statement = sqlite.prepareStatement("UPDATE channel_updates SET data=? WHERE short_channel_id=? AND node_flag=?")
statement.setBytes(1, channelUpdateCodec.encode(u).require.toByteArray)
statement.setLong(2, u.shortChannelId)
statement.setBoolean(3, Announcements.isNode1(u.flags))
statement.executeUpdate()
}
override def listChannelUpdates(): List[ChannelUpdate] = {
val rs = sqlite.createStatement.executeQuery("SELECT data FROM channel_updates")
codecList(rs, channelUpdateCodec)
}
}

View File

@ -0,0 +1,46 @@
package fr.acinq.eclair.db.sqlite
import java.net.InetSocketAddress
import java.sql.Connection
import fr.acinq.bitcoin.Crypto
import fr.acinq.bitcoin.Crypto.PublicKey
import fr.acinq.eclair.db.PeersDb
import fr.acinq.eclair.wire.LightningMessageCodecs.socketaddress
import scodec.bits.BitVector
class SqlitePeersDb(sqlite: Connection) extends PeersDb {
{
val statement = sqlite.createStatement
statement.executeUpdate("CREATE TABLE IF NOT EXISTS peers (node_id BLOB NOT NULL PRIMARY KEY, data BLOB NOT NULL)")
}
override def addOrUpdatePeer(nodeId: Crypto.PublicKey, address: InetSocketAddress): Unit = {
val data = socketaddress.encode(address).require.toByteArray
val update = sqlite.prepareStatement("UPDATE peers SET data=? WHERE node_id=?")
update.setBytes(1, data)
update.setBytes(2, nodeId.toBin)
if (update.executeUpdate() == 0) {
val statement = sqlite.prepareStatement("INSERT INTO peers VALUES (?, ?)")
statement.setBytes(1, nodeId.toBin)
statement.setBytes(2, data)
statement.executeUpdate()
}
}
override def removePeer(nodeId: Crypto.PublicKey): Unit = {
val statement = sqlite.prepareStatement("DELETE FROM peers WHERE node_id=?")
statement.setBytes(1, nodeId.toBin)
statement.executeUpdate()
}
override def listPeers(): List[(PublicKey, InetSocketAddress)] = {
val rs = sqlite.createStatement.executeQuery("SELECT node_id, data FROM peers")
var l: List[(PublicKey, InetSocketAddress)] = Nil
while (rs.next()) {
l = l :+ (PublicKey(rs.getBytes("node_id")), socketaddress.decode(BitVector(rs.getBytes("data"))).require.value)
}
l
}
}

View File

@ -0,0 +1,41 @@
package fr.acinq.eclair.db.sqlite
import java.sql.Connection
import fr.acinq.bitcoin.BinaryData
import fr.acinq.eclair.db.PreimagesDb
class SqlitePreimagesDb(sqlite: Connection) extends PreimagesDb {
{
val statement = sqlite.createStatement
// note: should we use a foreign key to local_channels table here?
statement.executeUpdate("CREATE TABLE IF NOT EXISTS preimages (channel_id BLOB NOT NULL, htlc_id INTEGER NOT NULL, preimage BLOB NOT NULL, PRIMARY KEY(channel_id, htlc_id))")
}
override def addPreimage(channelId: BinaryData, htlcId: Long, paymentPreimage: BinaryData): Unit = {
val statement = sqlite.prepareStatement("INSERT OR IGNORE INTO preimages VALUES (?, ?, ?)")
statement.setBytes(1, channelId)
statement.setLong(2, htlcId)
statement.setBytes(3, paymentPreimage)
statement.executeUpdate()
}
override def removePreimage(channelId: BinaryData, htlcId: Long): Unit = {
val statement = sqlite.prepareStatement("DELETE FROM preimages WHERE channel_id=? AND htlc_id=?")
statement.setBytes(1, channelId)
statement.setLong(2, htlcId)
statement.executeUpdate()
}
override def listPreimages(channelId: BinaryData): List[(BinaryData, Long, BinaryData)] = {
val statement = sqlite.prepareStatement("SELECT htlc_id, preimage FROM preimages WHERE channel_id=?")
statement.setBytes(1, channelId)
val rs = statement.executeQuery()
var l: List[(BinaryData, Long, BinaryData)] = Nil
while (rs.next()) {
l = l :+ (channelId, rs.getLong("htlc_id"), BinaryData(rs.getBytes("preimage")))
}
l
}
}

View File

@ -0,0 +1,27 @@
package fr.acinq.eclair.db.sqlite
import java.sql.ResultSet
import scodec.Codec
import scodec.bits.BitVector
object SqliteUtils {
/**
* This helper assumes that there is a "data" column available, decodable with the provided codec
*
* TODO: we should use an scala.Iterator instead
*
* @param rs
* @param codec
* @tparam T
* @return
*/
def codecList[T](rs: ResultSet, codec: Codec[T]): List[T] = {
var l: List[T] = Nil
while (rs.next()) {
l = l :+ codec.decode(BitVector(rs.getBytes("data"))).require.value
}
l
}
}

View File

@ -0,0 +1,70 @@
package fr.acinq.eclair.io
import java.net.InetSocketAddress
import akka.actor.{Props, _}
import akka.io.Tcp.SO.KeepAlive
import akka.io.{IO, Tcp}
import fr.acinq.bitcoin.Crypto.PublicKey
import fr.acinq.eclair.NodeParams
import fr.acinq.eclair.crypto.Noise.KeyPair
import fr.acinq.eclair.crypto.TransportHandler
import fr.acinq.eclair.crypto.TransportHandler.HandshakeCompleted
import fr.acinq.eclair.wire.{LightningMessage, LightningMessageCodecs}
/**
* Created by PM on 27/10/2015.
*/
class Client(nodeParams: NodeParams, switchboard: ActorRef, address: InetSocketAddress, remoteNodeId: PublicKey, origin: ActorRef) extends Actor with ActorLogging {
import Tcp._
import context.system
IO(Tcp) ! Connect(address, options = KeepAlive(true) :: Nil)
def receive = {
case CommandFailed(_: Connect) =>
origin ! Status.Failure(new RuntimeException("connection failed"))
context stop self
case Connected(remote, _) =>
log.info(s"connected to $remote")
val connection = sender
val transport = context.actorOf(Props(
new TransportHandler[LightningMessage](
KeyPair(nodeParams.privateKey.publicKey.toBin, nodeParams.privateKey.toBin),
Some(remoteNodeId),
connection = connection,
codec = LightningMessageCodecs.lightningMessageCodec)))
context watch transport
context become authenticating(transport)
}
def authenticating(transport: ActorRef): Receive = {
case Terminated(actor) if actor == transport =>
origin ! Status.Failure(new RuntimeException("authentication failed"))
context stop self
case h: HandshakeCompleted =>
log.info(s"handshake completed with ${h.remoteNodeId}")
origin ! "connected"
switchboard ! h
context become connected(transport)
}
def connected(transport: ActorRef): Receive = {
case Terminated(actor) if actor == transport =>
context stop self
case msg => log.warning(s"unexpected message $msg")
}
// we should not restart a failing transport
override val supervisorStrategy = OneForOneStrategy(loggingEnabled = true) { case _ => SupervisorStrategy.Stop }
}
object Client extends App {
def props(nodeParams: NodeParams, switchboard: ActorRef, address: InetSocketAddress, remoteNodeId: PublicKey, origin: ActorRef): Props = Props(new Client(nodeParams, switchboard, address, remoteNodeId, origin))
}

View File

@ -0,0 +1,289 @@
package fr.acinq.eclair.io
import java.net.InetSocketAddress
import akka.actor.{ActorRef, LoggingFSM, OneForOneStrategy, PoisonPill, Props, SupervisorStrategy, Terminated}
import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey}
import fr.acinq.bitcoin.{BinaryData, Crypto, DeterministicWallet}
import fr.acinq.eclair._
import fr.acinq.eclair.blockchain.EclairWallet
import fr.acinq.eclair.channel._
import fr.acinq.eclair.crypto.TransportHandler.{HandshakeCompleted, Listener}
import fr.acinq.eclair.io.Switchboard.{NewChannel, NewConnection}
import fr.acinq.eclair.router.{Rebroadcast, SendRoutingState}
import fr.acinq.eclair.wire._
import scala.concurrent.duration._
import scala.util.Random
// @formatter:off
case object Reconnect
case object Disconnect
sealed trait OfflineChannel
case class BrandNewChannel(c: NewChannel) extends OfflineChannel
case class HotChannel(channelId: ChannelId, a: ActorRef) extends OfflineChannel
sealed trait ChannelId
case class TemporaryChannelId(id: BinaryData) extends ChannelId
case class FinalChannelId(id: BinaryData) extends ChannelId
sealed trait Data
case class DisconnectedData(offlineChannels: Set[OfflineChannel], attempts: Int = 0) extends Data
case class InitializingData(transport: ActorRef, offlineChannels: Set[OfflineChannel]) extends Data
case class ConnectedData(transport: ActorRef, remoteInit: Init, channels: Map[ChannelId, ActorRef]) extends Data
sealed trait State
case object DISCONNECTED extends State
case object INITIALIZING extends State
case object CONNECTED extends State
// @formatter:on
/**
* Created by PM on 26/08/2016.
*/
class Peer(nodeParams: NodeParams, remoteNodeId: PublicKey, address_opt: Option[InetSocketAddress], watcher: ActorRef, router: ActorRef, relayer: ActorRef, wallet: EclairWallet, storedChannels: Set[HasCommitments]) extends LoggingFSM[State, Data] {
import Peer._
val RECONNECT_TIMER = "reconnect"
startWith(DISCONNECTED, DisconnectedData(offlineChannels = storedChannels.map { state =>
val channel = spawnChannel(nodeParams, context.system.deadLetters)
channel ! INPUT_RESTORED(state)
HotChannel(FinalChannelId(state.channelId), channel)
}, attempts = 0))
when(DISCONNECTED) {
case Event(c: NewChannel, d@DisconnectedData(offlineChannels, _)) =>
stay using d.copy(offlineChannels = offlineChannels + BrandNewChannel(c))
case Event(Reconnect, d@DisconnectedData(_, attempts)) =>
address_opt match {
case None => stay // no-op (this peer didn't initiate the connection and doesn't have the ip of the counterparty)
case _ if d.offlineChannels.size == 0 => stay // no-op (no more channels with this peer)
case Some(address) =>
context.parent forward NewConnection(remoteNodeId, address, None)
// exponential backoff retry with a finite max
setTimer(RECONNECT_TIMER, Reconnect, Math.min(Math.pow(2, attempts), 60) seconds, repeat = false)
stay using d.copy(attempts = attempts + 1)
}
case Event(HandshakeCompleted(transport, _), DisconnectedData(offlineChannels, _)) =>
log.info(s"registering as a listener to $transport")
transport ! Listener(self)
context watch transport
transport ! Init(globalFeatures = nodeParams.globalFeatures, localFeatures = nodeParams.localFeatures)
// we store the ip upon successful connection, keeping only the most recent one
address_opt.map(address => nodeParams.peersDb.addOrUpdatePeer(remoteNodeId, address))
goto(INITIALIZING) using InitializingData(transport, offlineChannels)
case Event(Terminated(actor), d@DisconnectedData(offlineChannels, _)) if offlineChannels.collect { case h: HotChannel if h.a == actor => h }.size >= 0 =>
val h = offlineChannels.collect { case h: HotChannel if h.a == actor => h }
log.info(s"channel closed: channelId=${h.map(_.channelId).mkString("/")}")
stay using d.copy(offlineChannels = offlineChannels -- h)
case Event(_: Rebroadcast | "connected", _) => stay // ignored
}
when(INITIALIZING) {
case Event(c: NewChannel, d@InitializingData(_, offlineChannels)) =>
stay using d.copy(offlineChannels = offlineChannels + BrandNewChannel(c))
case Event(remoteInit: Init, InitializingData(transport, offlineChannels)) =>
log.info(s"$remoteNodeId has features: initialRoutingSync=${Features.initialRoutingSync(remoteInit.localFeatures)}")
if (Features.areSupported(remoteInit.localFeatures)) {
if (Features.initialRoutingSync(remoteInit.localFeatures)) {
router ! SendRoutingState(transport)
}
// let's bring existing/requested channels online
val channels: Map[ChannelId, ActorRef] = offlineChannels.map {
case BrandNewChannel(c) =>
self ! c
None
case HotChannel(channelId, channel) =>
channel ! INPUT_RECONNECTED(transport)
Some((channelId -> channel))
}.flatten.toMap
goto(CONNECTED) using ConnectedData(transport, remoteInit, channels)
} else {
log.warning(s"incompatible features, disconnecting")
transport ! PoisonPill
stay
}
case Event(Terminated(actor), InitializingData(transport, offlineChannels)) if actor == transport =>
log.warning(s"lost connection to $remoteNodeId")
goto(DISCONNECTED) using DisconnectedData(offlineChannels)
case Event(Terminated(actor), d@InitializingData(_, offlineChannels)) if offlineChannels.collect { case h: HotChannel if h.a == actor => h }.size > 0 =>
val h = offlineChannels.collect { case h: HotChannel if h.a == actor => h }
log.info(s"channel closed: channelId=${h.map(_.channelId).mkString("/")}")
stay using d.copy(offlineChannels = offlineChannels -- h)
}
when(CONNECTED, stateTimeout = nodeParams.pingInterval) {
case Event(StateTimeout, ConnectedData(transport, _, _)) =>
// no need to use secure random here
val pingSize = Random.nextInt(1000)
val pongSize = Random.nextInt(1000)
transport ! Ping(pongSize, BinaryData("00" * pingSize))
stay
case Event(Ping(pongLength, _), ConnectedData(transport, _, _)) =>
// TODO: (optional) check against the expected data size tat we requested when we sent ping messages
if (pongLength > 0) {
transport ! Pong(BinaryData("00" * pongLength))
}
stay
case Event(Pong(data), ConnectedData(transport, _, _)) =>
// TODO: compute latency for remote peer ?
log.debug(s"received pong with ${data.length} bytes")
stay
case Event(err@Error(channelId, reason), ConnectedData(transport, _, channels)) if channelId == CHANNELID_ZERO =>
log.error(s"connection-level error, failing all channels! reason=${new String(reason)}")
channels.values.foreach(_ forward err)
transport ! PoisonPill
stay
case Event(msg: Error, ConnectedData(_, _, channels)) =>
// error messages are a bit special because they can contain either temporaryChannelId or channelId (see BOLT 1)
channels.get(TemporaryChannelId(msg.channelId)).orElse(channels.get(FinalChannelId(msg.channelId))) match {
case Some(channel) => channel forward msg
case None => log.warning(s"couldn't resolve channel for $msg")
}
stay
case Event(msg: HasTemporaryChannelId, ConnectedData(_, _, channels)) if channels.contains(TemporaryChannelId(msg.temporaryChannelId)) =>
val channel = channels(TemporaryChannelId(msg.temporaryChannelId))
channel forward msg
stay
case Event(msg: HasChannelId, ConnectedData(_, _, channels)) if channels.contains(FinalChannelId(msg.channelId)) =>
val channel = channels(FinalChannelId(msg.channelId))
channel forward msg
stay
case Event(ChannelIdAssigned(channel, temporaryChannelId, channelId), d@ConnectedData(_, _, channels)) if channels.contains(TemporaryChannelId(temporaryChannelId)) =>
log.info(s"channel id switch: previousId=$temporaryChannelId nextId=$channelId")
// NB: we keep the temporary channel id because the switch is not always acknowledged at this point (see https://github.com/lightningnetwork/lightning-rfc/pull/151)
// we won't clean it up, but we won't remember the temporary id on channel termination
stay using d.copy(channels = channels + (FinalChannelId(channelId) -> channel))
case Event(c: NewChannel, d@ConnectedData(transport, remoteInit, channels)) =>
log.info(s"requesting a new channel to $remoteNodeId with fundingSatoshis=${c.fundingSatoshis} and pushMsat=${c.pushMsat}")
val (channel, localParams) = createChannel(nodeParams, transport, funder = true, c.fundingSatoshis.toLong)
val temporaryChannelId = randomBytes(32)
val networkFeeratePerKw = Globals.feeratesPerKw.get.block_1
channel ! INPUT_INIT_FUNDER(temporaryChannelId, c.fundingSatoshis.amount, c.pushMsat.amount, networkFeeratePerKw, localParams, transport, remoteInit, c.channelFlags.getOrElse(nodeParams.channelFlags))
stay using d.copy(channels = channels + (TemporaryChannelId(temporaryChannelId) -> channel))
case Event(msg: OpenChannel, d@ConnectedData(transport, remoteInit, channels)) if !channels.contains(TemporaryChannelId(msg.temporaryChannelId)) =>
log.info(s"accepting a new channel to $remoteNodeId")
val (channel, localParams) = createChannel(nodeParams, transport, funder = false, fundingSatoshis = msg.fundingSatoshis)
val temporaryChannelId = msg.temporaryChannelId
channel ! INPUT_INIT_FUNDEE(temporaryChannelId, localParams, transport, remoteInit)
channel ! msg
stay using d.copy(channels = channels + (TemporaryChannelId(temporaryChannelId) -> channel))
case Event(Rebroadcast(announcements, origins), ConnectedData(transport, _, _)) =>
// we filter out announcements that we received from this node
announcements.filterNot(ann => origins.getOrElse(ann, context.system.deadLetters) == self).foreach(transport forward _)
stay
case Event(msg: RoutingMessage, _) =>
router forward msg
stay
case Event(Disconnect, ConnectedData(transport, _, _)) =>
transport ! PoisonPill
stay
case Event(Terminated(actor), ConnectedData(transport, _, channels)) if actor == transport =>
log.warning(s"lost connection to $remoteNodeId")
channels.values.foreach(_ ! INPUT_DISCONNECTED)
val c: Set[OfflineChannel] = channels.map(c => HotChannel(c._1, c._2)).toSet
goto(DISCONNECTED) using DisconnectedData(c)
case Event(Terminated(actor), d@ConnectedData(transport, _, channels)) if channels.values.toSet.contains(actor) =>
// we will have at most 2 ids: a TemporaryChannelId and a FinalChannelId
val channelIds = channels.filter(_._2 == actor).map(_._1)
log.info(s"channel closed: channelId=${channelIds.mkString("/")}")
if (channels.values.toSet - actor == Set.empty) {
log.info(s"that was the last open channel, closing the connection")
transport ! PoisonPill
}
stay using d.copy(channels = channels -- channelIds)
case Event(h: HandshakeCompleted, ConnectedData(oldTransport, _, channels)) =>
log.info(s"got new transport while already connected, switching to new transport")
context unwatch oldTransport
oldTransport ! PoisonPill
channels.values.foreach(_ ! INPUT_DISCONNECTED)
val c: Set[OfflineChannel] = channels.map(c => HotChannel(c._1, c._2)).toSet
self ! h
goto(DISCONNECTED) using DisconnectedData(c)
}
onTransition {
case _ -> DISCONNECTED if nodeParams.autoReconnect && address_opt.isDefined => setTimer(RECONNECT_TIMER, Reconnect, 1 second, repeat = false)
case DISCONNECTED -> _ if nodeParams.autoReconnect && address_opt.isDefined => cancelTimer(RECONNECT_TIMER)
}
def createChannel(nodeParams: NodeParams, transport: ActorRef, funder: Boolean, fundingSatoshis: Long): (ActorRef, LocalParams) = {
val defaultFinalScriptPubKey = Helpers.getFinalScriptPubKey(wallet)
val localParams = makeChannelParams(nodeParams, defaultFinalScriptPubKey, funder, fundingSatoshis)
val channel = spawnChannel(nodeParams, transport)
(channel, localParams)
}
def spawnChannel(nodeParams: NodeParams, transport: ActorRef): ActorRef = {
val channel = context.actorOf(Channel.props(nodeParams, wallet, remoteNodeId, watcher, router, relayer))
context watch channel
channel
}
// a failing channel won't be restarted, it should handle its states
override val supervisorStrategy = OneForOneStrategy(loggingEnabled = true) { case _ => SupervisorStrategy.Stop }
initialize()
}
object Peer {
val CHANNELID_ZERO = BinaryData("00" * 32)
def props(nodeParams: NodeParams, remoteNodeId: PublicKey, address_opt: Option[InetSocketAddress], watcher: ActorRef, router: ActorRef, relayer: ActorRef, wallet: EclairWallet, storedChannels: Set[HasCommitments]) = Props(new Peer(nodeParams, remoteNodeId, address_opt, watcher, router, relayer, wallet: EclairWallet, storedChannels))
def generateKey(nodeParams: NodeParams, keyPath: Seq[Long]): PrivateKey = DeterministicWallet.derivePrivateKey(nodeParams.extendedPrivateKey, keyPath).privateKey
def makeChannelParams(nodeParams: NodeParams, defaultFinalScriptPubKey: BinaryData, isFunder: Boolean, fundingSatoshis: Long): LocalParams = {
// all secrets are generated from the main seed
// TODO: check this
val keyIndex = secureRandom.nextInt(1000).toLong
LocalParams(
nodeId = nodeParams.privateKey.publicKey,
dustLimitSatoshis = nodeParams.dustLimitSatoshis,
maxHtlcValueInFlightMsat = nodeParams.maxHtlcValueInFlightMsat,
channelReserveSatoshis = (nodeParams.reserveToFundingRatio * fundingSatoshis).toLong,
htlcMinimumMsat = nodeParams.htlcMinimumMsat,
toSelfDelay = nodeParams.delayBlocks,
maxAcceptedHtlcs = nodeParams.maxAcceptedHtlcs,
fundingPrivKey = generateKey(nodeParams, keyIndex :: 0L :: Nil),
revocationSecret = generateKey(nodeParams, keyIndex :: 1L :: Nil),
paymentKey = generateKey(nodeParams, keyIndex :: 2L :: Nil),
delayedPaymentKey = generateKey(nodeParams, keyIndex :: 3L :: Nil),
htlcKey = generateKey(nodeParams, keyIndex :: 4L :: Nil),
defaultFinalScriptPubKey = defaultFinalScriptPubKey,
shaSeed = Crypto.sha256(generateKey(nodeParams, keyIndex :: 5L :: Nil).toBin), // TODO: check that
isFunder = isFunder,
globalFeatures = nodeParams.globalFeatures,
localFeatures = nodeParams.localFeatures)
}
}

View File

@ -0,0 +1,59 @@
package fr.acinq.eclair.io
import java.net.InetSocketAddress
import akka.actor.{Actor, ActorLogging, ActorRef, OneForOneStrategy, Props, SupervisorStrategy}
import akka.io.Tcp.SO.KeepAlive
import akka.io.{IO, Tcp}
import fr.acinq.eclair.NodeParams
import fr.acinq.eclair.crypto.Noise.KeyPair
import fr.acinq.eclair.crypto.TransportHandler
import fr.acinq.eclair.crypto.TransportHandler.HandshakeCompleted
import fr.acinq.eclair.wire.{LightningMessage, LightningMessageCodecs}
import scala.concurrent.Promise
/**
* Created by PM on 27/10/2015.
*/
class Server(nodeParams: NodeParams, switchboard: ActorRef, address: InetSocketAddress, bound: Option[Promise[Unit]] = None) extends Actor with ActorLogging {
import Tcp._
import context.system
IO(Tcp) ! Bind(self, address, options = KeepAlive(true) :: Nil)
def receive() = {
case Bound(localAddress) =>
bound.map(_.success(Unit))
log.info(s"bound on $localAddress")
case CommandFailed(_: Bind) =>
bound.map(_.failure(new RuntimeException("TCP bind failed")))
context stop self
case Connected(remote, _) =>
log.info(s"connected to $remote")
val connection = sender
context.actorOf(Props(
new TransportHandler[LightningMessage](
KeyPair(nodeParams.privateKey.publicKey.toBin, nodeParams.privateKey.toBin),
None,
connection = connection,
codec = LightningMessageCodecs.lightningMessageCodec)))
case h: HandshakeCompleted =>
log.info(s"handshake completed with ${h.remoteNodeId}")
switchboard ! h
}
// we should not restart a failing transport
override val supervisorStrategy = OneForOneStrategy(loggingEnabled = true) { case _ => SupervisorStrategy.Stop }
}
object Server {
def props(nodeParams: NodeParams, switchboard: ActorRef, address: InetSocketAddress, bound: Option[Promise[Unit]] = None): Props = Props(new Server(nodeParams, switchboard, address, bound))
}

View File

@ -0,0 +1,108 @@
package fr.acinq.eclair.io
import java.net.InetSocketAddress
import akka.actor.{Actor, ActorLogging, ActorRef, OneForOneStrategy, Props, Status, SupervisorStrategy, Terminated}
import fr.acinq.bitcoin.Crypto.PublicKey
import fr.acinq.bitcoin.{MilliSatoshi, Satoshi}
import fr.acinq.eclair.NodeParams
import fr.acinq.eclair.blockchain.EclairWallet
import fr.acinq.eclair.channel.HasCommitments
import fr.acinq.eclair.crypto.TransportHandler.HandshakeCompleted
import fr.acinq.eclair.router.Rebroadcast
/**
* Ties network connections to peers.
* Created by PM on 14/02/2017.
*/
class Switchboard(nodeParams: NodeParams, watcher: ActorRef, router: ActorRef, relayer: ActorRef, wallet: EclairWallet) extends Actor with ActorLogging {
import Switchboard._
// we load peers and channels from database
val initialPeers = {
val channels = nodeParams.channelsDb.listChannels().toList.groupBy(_.commitments.remoteParams.nodeId)
val peers = nodeParams.peersDb.listPeers().toMap
channels
.map {
case (remoteNodeId, states) => (remoteNodeId, states, peers.get(remoteNodeId))
}
.map {
case (remoteNodeId, states, address_opt) =>
// we might not have an address if we didn't initiate the connection in the first place
val peer = createOrGetPeer(Map(), remoteNodeId, address_opt, states.toSet)
(remoteNodeId -> peer)
}.toMap
}
def receive: Receive = main(initialPeers, Map())
def main(peers: Map[PublicKey, ActorRef], connections: Map[PublicKey, ActorRef]): Receive = {
case NewConnection(publicKey, _, _) if publicKey == nodeParams.privateKey.publicKey =>
sender ! Status.Failure(new RuntimeException("cannot open connection with oneself"))
case NewConnection(remoteNodeId, address, newChannel_opt) =>
val connection = connections.get(remoteNodeId) match {
case Some(connection) =>
log.info(s"already connected to nodeId=$remoteNodeId")
sender ! s"already connected to nodeId=$remoteNodeId"
connection
case None =>
log.info(s"connecting to $remoteNodeId @ $address on behalf of $sender")
val connection = context.actorOf(Client.props(nodeParams, self, address, remoteNodeId, sender))
context watch (connection)
connection
}
val peer = createOrGetPeer(peers, remoteNodeId, Some(address), Set.empty)
newChannel_opt.foreach(peer forward _)
context become main(peers + (remoteNodeId -> peer), connections + (remoteNodeId -> connection))
case Terminated(actor) if connections.values.toSet.contains(actor) =>
log.info(s"$actor is dead, removing from connections")
val remoteNodeId = connections.find(_._2 == actor).get._1
context become main(peers, connections - remoteNodeId)
case Terminated(actor) if peers.values.toSet.contains(actor) =>
log.info(s"$actor is dead, removing from peers/connections/db")
val remoteNodeId = peers.find(_._2 == actor).get._1
nodeParams.peersDb.removePeer(remoteNodeId)
context become main(peers - remoteNodeId, connections - remoteNodeId)
case h@HandshakeCompleted(_, remoteNodeId) =>
val peer = createOrGetPeer(peers, remoteNodeId, None, Set.empty)
peer forward h
context become main(peers + (remoteNodeId -> peer), connections)
case r: Rebroadcast => peers.values.foreach(_ forward r)
case 'peers => sender ! peers
case 'connections => sender ! connections
}
def createOrGetPeer(peers: Map[PublicKey, ActorRef], remoteNodeId: PublicKey, address_opt: Option[InetSocketAddress], offlineChannels: Set[HasCommitments]) = {
peers.get(remoteNodeId) match {
case Some(peer) => peer
case None =>
val peer = context.actorOf(Peer.props(nodeParams, remoteNodeId, address_opt, watcher, router, relayer, wallet, offlineChannels), name = s"peer-$remoteNodeId")
context watch (peer)
peer
}
}
// we resume failing peers because they may have open channels that we don't want to close abruptly
override val supervisorStrategy = OneForOneStrategy(loggingEnabled = true) { case _ => SupervisorStrategy.Resume }
}
object Switchboard {
def props(nodeParams: NodeParams, watcher: ActorRef, router: ActorRef, relayer: ActorRef, wallet: EclairWallet) = Props(new Switchboard(nodeParams, watcher, router, relayer, wallet))
// @formatter:off
case class NewChannel(fundingSatoshis: Satoshi, pushMsat: MilliSatoshi, channelFlags: Option[Byte])
case class NewConnection(remoteNodeId: PublicKey, address: InetSocketAddress, newChannel_opt: Option[NewChannel])
// @formatter:on
}

View File

@ -0,0 +1,48 @@
package fr.acinq.eclair.io
import akka.actor.{Actor, ActorLogging, ActorRef, PoisonPill}
import akka.io.Tcp
import akka.util.ByteString
/**
* This implements an ACK-based throttling mechanism
* See https://doc.akka.io/docs/akka/snapshot/scala/io-tcp.html#throttling-reads-and-writes
*/
class WriteAckSender(connection: ActorRef) extends Actor with ActorLogging {
// Note: this actor should be killed if connection dies
case object Ack extends Tcp.Event
override def receive = idle
def idle: Receive = {
case data: ByteString =>
connection ! Tcp.Write(data, Ack)
context become buffering(Vector.empty[ByteString])
}
def buffering(buffer: Vector[ByteString]): Receive = {
case _: ByteString if buffer.size > MAX_BUFFERED =>
log.warning(s"buffer overrun, closing connection")
connection ! PoisonPill
case data: ByteString =>
log.debug(s"buffering write $data")
context become buffering(buffer :+ data)
case Ack =>
buffer.headOption match {
case Some(data) =>
connection ! Tcp.Write(data, Ack)
context become buffering(buffer.drop(1))
case None =>
log.debug(s"got last ack, back to idle")
context become idle
}
}
override def unhandled(message: Any): Unit = log.warning(s"unhandled message $message")
val MAX_BUFFERED = 100000L
}

View File

@ -0,0 +1,67 @@
package fr.acinq
import java.security.SecureRandom
import fr.acinq.bitcoin.Crypto.PrivateKey
import fr.acinq.bitcoin.{BinaryData, _}
import scodec.Attempt
import scodec.bits.BitVector
package object eclair {
/**
* We are using 'new SecureRandom()' instead of 'SecureRandom.getInstanceStrong()' because the latter can hang on Linux
* See http://bugs.java.com/view_bug.do?bug_id=6521844 and https://tersesystems.com/2015/12/17/the-right-way-to-use-securerandom/
*/
val secureRandom = new SecureRandom()
def randomBytes(length: Int): BinaryData = {
val buffer = new Array[Byte](length)
secureRandom.nextBytes(buffer)
buffer
}
def randomKey: PrivateKey = PrivateKey(randomBytes(32), compressed = true)
def toLongId(fundingTxHash: BinaryData, fundingOutputIndex: Int): BinaryData = {
require(fundingOutputIndex < 65536, "fundingOutputIndex must not be greater than FFFF")
require(fundingTxHash.size == 32, "fundingTxHash must be of length 32B")
val channelId = fundingTxHash.take(30) :+ (fundingTxHash.data(30) ^ (fundingOutputIndex >> 8)).toByte :+ (fundingTxHash.data(31) ^ fundingOutputIndex).toByte
BinaryData(channelId)
}
/**
* Creates a unique index assigned to a channel (== an unspent multisig 2-of-2 output)
*
* @param blockHeight
* @param txIndex
* @param outputIndex
* @return channelId
*/
def toShortId(blockHeight: Int, txIndex: Int, outputIndex: Int): Long =
((blockHeight & 0xFFFFFFL) << 40) | ((txIndex & 0xFFFFFFL) << 16) | (outputIndex & 0xFFFFL)
/**
*
* @param id
* @return (blockHeight, txIndex, outputIndex)
*/
def fromShortId(id: Long): (Int, Int, Int) =
(((id >> 40) & 0xFFFFFF).toInt, ((id >> 16) & 0xFFFFFF).toInt, (id & 0xFFFF).toInt)
def serializationResult(attempt: Attempt[BitVector]): BinaryData = attempt match {
case Attempt.Successful(bin) => BinaryData(bin.toByteArray)
case Attempt.Failure(cause) => throw new RuntimeException(s"serialization error: $cause")
}
/**
* Converts feerate in satoshi-per-bytes to feerate in satoshi-per-kw
*
* @param feeratePerByte feerate in satoshi-per-bytes
* @return feerate in satoshi-per-kw
*/
def feerateByte2Kw(feeratePerByte: Long): Long = feeratePerByte * 1024 / 4
}

View File

@ -0,0 +1,59 @@
package fr.acinq.eclair.payment
import akka.actor.{Actor, ActorLogging, Props, Status}
import fr.acinq.bitcoin.{BinaryData, Crypto, MilliSatoshi}
import fr.acinq.eclair.channel.{CMD_FAIL_HTLC, CMD_FULFILL_HTLC}
import fr.acinq.eclair.wire._
import fr.acinq.eclair.{NodeParams, randomBytes}
import scala.util.{Failure, Success, Try}
/**
* Created by PM on 17/06/2016.
*/
class LocalPaymentHandler(nodeParams: NodeParams) extends Actor with ActorLogging {
override def receive: Receive = run(Map())
def run(h2r: Map[BinaryData, (BinaryData, PaymentRequest)]): Receive = {
case ReceivePayment(amount, desc) =>
Try {
val paymentPreimage = randomBytes(32)
val paymentHash = Crypto.sha256(paymentPreimage)
(paymentPreimage, paymentHash, PaymentRequest(nodeParams.chainHash, Some(amount), paymentHash, nodeParams.privateKey, desc))
} match {
case Success((r, h, pr)) =>
log.debug(s"generated payment request=${PaymentRequest.write(pr)} from amount=$amount")
sender ! pr
context.become(run(h2r + (h -> (r, pr))))
case Failure(t) =>
sender ! Status.Failure(t)
}
case htlc: UpdateAddHtlc =>
if (h2r.contains(htlc.paymentHash)) {
val r = h2r(htlc.paymentHash)._1
val pr = h2r(htlc.paymentHash)._2
// The htlc amount must be equal or greater than the requested amount. A slight overpaying is permitted, however
// it must not be greater than two times the requested amount.
// see https://github.com/lightningnetwork/lightning-rfc/blob/master/04-onion-routing.md#failure-messages
pr.amount match {
case Some(amount) if MilliSatoshi(htlc.amountMsat) < amount => sender ! CMD_FAIL_HTLC(htlc.id, Right(IncorrectPaymentAmount), commit = true)
case Some(amount) if MilliSatoshi(htlc.amountMsat) > amount * 2 => sender ! CMD_FAIL_HTLC(htlc.id, Right(IncorrectPaymentAmount), commit = true)
case _ =>
log.info(s"received payment for paymentHash=${htlc.paymentHash} amountMsat=${htlc.amountMsat}")
// amount is correct or was not specified in the payment request
sender ! CMD_FULFILL_HTLC(htlc.id, r, commit = true)
context.system.eventStream.publish(PaymentReceived(MilliSatoshi(htlc.amountMsat), htlc.paymentHash))
context.become(run(h2r - htlc.paymentHash))
}
} else {
sender ! CMD_FAIL_HTLC(htlc.id, Right(UnknownPaymentHash), commit = true)
}
}
}
object LocalPaymentHandler {
def props(nodeParams: NodeParams) = Props(new LocalPaymentHandler(nodeParams))
}

View File

@ -1,4 +1,4 @@
package fr.acinq.eclair.channel
package fr.acinq.eclair.payment
import akka.actor.{Actor, ActorLogging, ActorRef}
@ -7,15 +7,12 @@ import akka.actor.{Actor, ActorLogging, ActorRef}
*/
class NoopPaymentHandler extends Actor with ActorLogging {
override def receive: Receive = {
case handler: ActorRef => {
log.info(s"registering actor $handler as payment handler")
context.become(forward(handler))
}
case _ => {} // no-op
}
override def receive: Receive = forward(context.system.deadLetters)
def forward(handler: ActorRef): Receive = {
case newHandler: ActorRef =>
log.info(s"registering actor $handler as payment handler")
context become forward(newHandler)
case msg => handler forward msg
}

View File

@ -0,0 +1,16 @@
package fr.acinq.eclair.payment
import fr.acinq.bitcoin.{BinaryData, MilliSatoshi}
/**
* Created by PM on 01/02/2017.
*/
sealed trait PaymentEvent {
val paymentHash: BinaryData
}
case class PaymentSent(amount: MilliSatoshi, feesPaid: MilliSatoshi, paymentHash: BinaryData) extends PaymentEvent
case class PaymentRelayed(amountIn: MilliSatoshi, amountOut: MilliSatoshi, paymentHash: BinaryData) extends PaymentEvent
case class PaymentReceived(amount: MilliSatoshi, paymentHash: BinaryData) extends PaymentEvent

View File

@ -0,0 +1,46 @@
package fr.acinq.eclair.payment
import fr.acinq.bitcoin.Crypto.PublicKey
import fr.acinq.eclair.payment.PaymentRequest.ExtraHop
import fr.acinq.eclair.wire.ChannelUpdate
object PaymentHop {
/**
*
* @param baseMsat fixed fee
* @param proportional proportional fee
* @param msat amount in millisatoshi
* @return the fee (in msat) that a node should be paid to forward an HTLC of 'amount' millisatoshis
*/
def nodeFee(baseMsat: Long, proportional: Long, msat: Long): Long = baseMsat + (proportional * msat) / 1000000
/**
*
* @param reversePath sequence of Hops from recipient to a start of assisted path
* @param msat an amount to send to a payment recipient
* @return a sequence of extra hops with a pre-calculated fee for a given msat amount
*/
def buildExtra(reversePath: Seq[Hop], msat: Long): Seq[ExtraHop] = (List.empty[ExtraHop] /: reversePath) {
case (Nil, hop) => ExtraHop(hop.nodeId, hop.shortChannelId, hop.nextFee(msat), hop.cltvExpiryDelta) :: Nil
case (head :: rest, hop) => ExtraHop(hop.nodeId, hop.shortChannelId, hop.nextFee(msat + head.fee), hop.cltvExpiryDelta) :: head :: rest
}
}
trait PaymentHop {
def nextFee(msat: Long): Long
def shortChannelId: Long
def cltvExpiryDelta: Int
def nodeId: PublicKey
}
case class Hop(nodeId: PublicKey, nextNodeId: PublicKey, lastUpdate: ChannelUpdate) extends PaymentHop {
def nextFee(msat: Long): Long = PaymentHop.nodeFee(lastUpdate.feeBaseMsat, lastUpdate.feeProportionalMillionths, msat)
def cltvExpiryDelta: Int = lastUpdate.cltvExpiryDelta
def shortChannelId: Long = lastUpdate.shortChannelId
}

View File

@ -0,0 +1,21 @@
package fr.acinq.eclair.payment
import akka.actor.{Actor, ActorLogging, ActorRef, Props}
import fr.acinq.bitcoin.Crypto.PublicKey
/**
* Created by PM on 29/08/2016.
*/
class PaymentInitiator(sourceNodeId: PublicKey, router: ActorRef, register: ActorRef) extends Actor with ActorLogging {
override def receive: Receive = {
case c: SendPayment =>
val payFsm = context.actorOf(PaymentLifecycle.props(sourceNodeId, router, register))
payFsm forward c
}
}
object PaymentInitiator {
def props(sourceNodeId: PublicKey, router: ActorRef, register: ActorRef) = Props(classOf[PaymentInitiator], sourceNodeId, router, register)
}

View File

@ -0,0 +1,201 @@
package fr.acinq.eclair.payment
import akka.actor.{ActorRef, FSM, LoggingFSM, Props, Status}
import fr.acinq.bitcoin.Crypto.PublicKey
import fr.acinq.bitcoin.{BinaryData, MilliSatoshi}
import fr.acinq.eclair._
import fr.acinq.eclair.channel.{CMD_ADD_HTLC, Register}
import fr.acinq.eclair.crypto.Sphinx
import fr.acinq.eclair.crypto.Sphinx.{ErrorPacket, Packet}
import fr.acinq.eclair.router._
import fr.acinq.eclair.wire._
import scodec.Attempt
// @formatter:off
case class ReceivePayment(amountMsat: MilliSatoshi, description: String)
case class SendPayment(amountMsat: Long, paymentHash: BinaryData, targetNodeId: PublicKey, minFinalCltvExpiry: Long = PaymentLifecycle.defaultMinFinalCltvExpiry, maxAttempts: Int = 5)
sealed trait PaymentResult
case class PaymentSucceeded(route: Seq[Hop], paymentPreimage: BinaryData) extends PaymentResult
sealed trait PaymentFailure
case class LocalFailure(t: Throwable) extends PaymentFailure
case class RemoteFailure(route: Seq[Hop], e: ErrorPacket) extends PaymentFailure
case class UnreadableRemoteFailure(route: Seq[Hop]) extends PaymentFailure
case class PaymentFailed(paymentHash: BinaryData, failures: Seq[PaymentFailure]) extends PaymentResult
sealed trait Data
case object WaitingForRequest extends Data
case class WaitingForRoute(sender: ActorRef, c: SendPayment, failures: Seq[PaymentFailure]) extends Data
case class WaitingForComplete(sender: ActorRef, c: SendPayment, cmd: CMD_ADD_HTLC, failures: Seq[PaymentFailure], sharedSecrets: Seq[(BinaryData, PublicKey)], ignoreNodes: Set[PublicKey], ignoreChannels: Set[Long], hops: Seq[Hop]) extends Data
sealed trait State
case object WAITING_FOR_REQUEST extends State
case object WAITING_FOR_ROUTE extends State
case object WAITING_FOR_PAYMENT_COMPLETE extends State
// @formatter:on
/**
* Created by PM on 26/08/2016.
*/
class PaymentLifecycle(sourceNodeId: PublicKey, router: ActorRef, register: ActorRef) extends LoggingFSM[State, Data] {
import PaymentLifecycle._
startWith(WAITING_FOR_REQUEST, WaitingForRequest)
when(WAITING_FOR_REQUEST) {
case Event(c: SendPayment, WaitingForRequest) =>
router ! RouteRequest(sourceNodeId, c.targetNodeId)
goto(WAITING_FOR_ROUTE) using WaitingForRoute(sender, c, failures = Nil)
}
when(WAITING_FOR_ROUTE) {
case Event(RouteResponse(hops, ignoreNodes, ignoreChannels), WaitingForRoute(s, c, failures)) =>
log.info(s"route found: attempt=${failures.size + 1}/${c.maxAttempts} route=${hops.map(_.nextNodeId).mkString("->")} channels=${hops.map(_.lastUpdate.shortChannelId.toHexString).mkString("->")}")
val firstHop = hops.head
val finalExpiry = Globals.blockCount.get().toInt + c.minFinalCltvExpiry.toInt
val (cmd, sharedSecrets) = buildCommand(c.amountMsat, finalExpiry, c.paymentHash, hops)
// TODO: HACK!!!! see Router.scala (we actually store the first node id in the sig)
if (firstHop.lastUpdate.signature.size == 32) {
register ! Register.Forward(firstHop.lastUpdate.signature, cmd)
} else {
register ! Register.ForwardShortId(firstHop.lastUpdate.shortChannelId, cmd)
}
goto(WAITING_FOR_PAYMENT_COMPLETE) using WaitingForComplete(s, c, cmd, failures, sharedSecrets, ignoreNodes, ignoreChannels, hops)
case Event(Status.Failure(t), WaitingForRoute(s, c, failures)) =>
s ! PaymentFailed(c.paymentHash, failures = failures :+ LocalFailure(t))
stop(FSM.Normal)
}
when(WAITING_FOR_PAYMENT_COMPLETE) {
case Event("ok", _) => stay()
case Event(fulfill: UpdateFulfillHtlc, w: WaitingForComplete) =>
w.sender ! PaymentSucceeded(w.hops, fulfill.paymentPreimage)
context.system.eventStream.publish(PaymentSent(MilliSatoshi(w.c.amountMsat), MilliSatoshi(w.cmd.amountMsat - w.c.amountMsat), w.cmd.paymentHash))
stop(FSM.Normal)
case Event(fail: UpdateFailHtlc, WaitingForComplete(s, c, _, failures, sharedSecrets, ignoreNodes, ignoreChannels, hops)) =>
Sphinx.parseErrorPacket(fail.reason, sharedSecrets) match {
case None =>
log.warning(s"cannot parse returned error ${fail.reason}")
s ! PaymentFailed(c.paymentHash, failures = failures :+ UnreadableRemoteFailure(hops))
stop(FSM.Normal)
case Some(e@ErrorPacket(nodeId, failureMessage)) if nodeId == c.targetNodeId =>
log.warning(s"received an error message from target nodeId=$nodeId, failing the payment (failure=$failureMessage)")
s ! PaymentFailed(c.paymentHash, failures = failures :+ RemoteFailure(hops, e))
stop(FSM.Normal)
case Some(e@ErrorPacket(nodeId, failureMessage)) if failures.size + 1 >= c.maxAttempts =>
log.info(s"received an error message from nodeId=$nodeId (failure=$failureMessage)")
log.warning(s"too many failed attempts, failing the payment")
s ! PaymentFailed(c.paymentHash, failures = failures :+ RemoteFailure(hops, e))
stop(FSM.Normal)
case Some(e@ErrorPacket(nodeId, failureMessage: Node)) =>
log.info(s"received an error message from nodeId=$nodeId, trying to route around it (failure=$failureMessage)")
// let's try to route around this node
router ! RouteRequest(sourceNodeId, c.targetNodeId, ignoreNodes + nodeId, ignoreChannels)
goto(WAITING_FOR_ROUTE) using WaitingForRoute(s, c, failures :+ RemoteFailure(hops, e))
case Some(e@ErrorPacket(nodeId, failureMessage: Update)) =>
log.info(s"received 'Update' type error message from nodeId=$nodeId, retrying payment (failure=$failureMessage)")
if (Announcements.checkSig(failureMessage.update, nodeId)) {
// note that we check the sig, but we don't make sure that this update was for the exact channel we required
// the reason is that we don't want to prevent relaying nodes to use another channel to the same N+1 node if they deem necessary
failureMessage match {
case _: TemporaryChannelFailure =>
// node indicates that its outgoing channel is experiencing a transient issue (eg. channel capacity reached, too many in-flight htlc)
hops.find(_.nodeId == nodeId).map(_.lastUpdate) match {
case Some(u) if u.copy(signature = BinaryData.empty, timestamp = 0) == failureMessage.update.copy(signature = BinaryData.empty, timestamp = 0) =>
// node returned the exact same update we used: in that case, let's temporarily exclude the channel from future routes, giving it time to recover
val nextNodeId = hops.find(_.nodeId == nodeId).get.nextNodeId
router ! ExcludeChannel(ChannelDesc(failureMessage.update.shortChannelId, nodeId, nextNodeId))
case _ => // node returned a different update, maybe the payment will go through next time...
}
case _ => {}
}
// in any case, we forward the update to the router
router ! failureMessage.update
// let's try again, router will have updated its state
router ! RouteRequest(sourceNodeId, c.targetNodeId, ignoreNodes, ignoreChannels)
} else {
// this node is fishy, it gave us a bad sig!! let's filter it out
log.warning(s"got bad signature from node=$nodeId update=${failureMessage.update}")
router ! RouteRequest(sourceNodeId, c.targetNodeId, ignoreNodes + nodeId, ignoreChannels)
}
goto(WAITING_FOR_ROUTE) using WaitingForRoute(s, c, failures :+ RemoteFailure(hops, e))
case Some(e@ErrorPacket(nodeId, failureMessage)) =>
log.info(s"received an error message from nodeId=$nodeId, trying to use a different channel (failure=$failureMessage)")
// let's try again without the channel outgoing from nodeId
val faultyChannel = hops.find(_.nodeId == nodeId).map(_.lastUpdate.shortChannelId)
router ! RouteRequest(sourceNodeId, c.targetNodeId, ignoreNodes, ignoreChannels ++ faultyChannel.toSet)
goto(WAITING_FOR_ROUTE) using WaitingForRoute(s, c, failures :+ RemoteFailure(hops, e))
}
case Event(fail: UpdateFailMalformedHtlc, _) =>
log.info(s"first node in the route couldn't parse our htlc: fail=$fail")
// this is a corner case, that can only happen when the *first* node in the route cannot parse the onion
// (if this happens higher up in the route, the error would be wrapped in an UpdateFailHtlc and handled above)
// let's consider it a local error and treat is as such
self ! Status.Failure(new RuntimeException("first hop returned an UpdateFailMalformedHtlc message"))
stay
case Event(Status.Failure(t), WaitingForComplete(s, c, _, failures, _, ignoreNodes, ignoreChannels, hops)) =>
if (failures.size + 1 >= c.maxAttempts) {
s ! PaymentFailed(c.paymentHash, failures :+ LocalFailure(t))
stop(FSM.Normal)
} else {
log.info(s"received an error message from local, trying to use a different channel (failure=${t.getMessage})")
router ! RouteRequest(sourceNodeId, c.targetNodeId, ignoreNodes, ignoreChannels + hops.head.lastUpdate.shortChannelId)
goto(WAITING_FOR_ROUTE) using WaitingForRoute(s, c, failures :+ LocalFailure(t))
}
}
initialize()
}
object PaymentLifecycle {
def props(sourceNodeId: PublicKey, router: ActorRef, register: ActorRef) = Props(classOf[PaymentLifecycle], sourceNodeId, router, register)
def buildOnion(nodes: Seq[PublicKey], payloads: Seq[PerHopPayload], associatedData: BinaryData): Sphinx.PacketAndSecrets = {
require(nodes.size == payloads.size)
val sessionKey = randomKey
val payloadsbin: Seq[BinaryData] = payloads
.map(LightningMessageCodecs.perHopPayloadCodec.encode(_))
.map {
case Attempt.Successful(bitVector) => BinaryData(bitVector.toByteArray)
case Attempt.Failure(cause) => throw new RuntimeException(s"serialization error: $cause")
}
Sphinx.makePacket(sessionKey, nodes, payloadsbin, associatedData)
}
/**
*
* @param finalAmountMsat the final htlc amount in millisatoshis
* @param finalExpiry the final htlc expiry in number of blocks
* @param hops the hops as computed by the router + extra routes from payment request
* @return a (firstAmountMsat, firstExpiry, payloads) tuple where:
* - firstAmountMsat is the amount for the first htlc in the route
* - firstExpiry is the cltv expiry for the first htlc in the route
* - a sequence of payloads that will be used to build the onion
*/
def buildPayloads(finalAmountMsat: Long, finalExpiry: Int, hops: Seq[PaymentHop]): (Long, Int, Seq[PerHopPayload]) =
hops.reverse.foldLeft((finalAmountMsat, finalExpiry, PerHopPayload(0L, finalAmountMsat, finalExpiry) :: Nil)) {
case ((msat, expiry, payloads), hop) =>
(msat + hop.nextFee(msat), expiry + hop.cltvExpiryDelta, PerHopPayload(hop.shortChannelId, msat, expiry) +: payloads)
}
// this is defined in BOLT 11
val defaultMinFinalCltvExpiry = 9
def buildCommand(finalAmountMsat: Long, finalExpiry: Int, paymentHash: BinaryData, hops: Seq[Hop]): (CMD_ADD_HTLC, Seq[(BinaryData, PublicKey)]) = {
val (firstAmountMsat, firstExpiry, payloads) = buildPayloads(finalAmountMsat, finalExpiry, hops.drop(1))
val nodes = hops.map(_.nextNodeId)
// BOLT 2 requires that associatedData == paymentHash
val onion = buildOnion(nodes, payloads, paymentHash)
CMD_ADD_HTLC(firstAmountMsat, paymentHash, firstExpiry, Packet.write(onion.packet), upstream_opt = None, commit = true) -> onion.sharedSecrets
}
}

View File

@ -0,0 +1,513 @@
package fr.acinq.eclair.payment
import java.math.BigInteger
import java.nio.ByteOrder
import fr.acinq.bitcoin.Bech32.Int5
import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey}
import fr.acinq.bitcoin.{BinaryData, MilliSatoshi, _}
import fr.acinq.eclair.crypto.BitStream
import fr.acinq.eclair.crypto.BitStream.Bit
import fr.acinq.eclair.payment.PaymentRequest.{Amount, RoutingInfoTag, Timestamp}
import scala.annotation.tailrec
import scala.util.Try
/**
* Lightning Payment Request
* see https://github.com/lightningnetwork/lightning-rfc/pull/183
*
* @param prefix currency prefix; lnbc for bitcoin, lntb for bitcoin testnet
* @param amount amount to pay (empty string means no amount is specified)
* @param timestamp request timestamp (UNIX format)
* @param nodeId id of the node emitting the payment request
* @param tags payment tags; must include a single PaymentHash tag
* @param signature request signature that will be checked against node id
*/
case class PaymentRequest(prefix: String, amount: Option[MilliSatoshi], timestamp: Long, nodeId: PublicKey, tags: List[PaymentRequest.Tag], signature: BinaryData) {
amount.map(a => require(a > MilliSatoshi(0) && a <= PaymentRequest.maxAmount, s"amount is not valid"))
require(tags.collect { case _: PaymentRequest.PaymentHashTag => {} }.size == 1, "there must be exactly one payment hash tag")
require(tags.collect { case PaymentRequest.DescriptionTag(_) | PaymentRequest.DescriptionHashTag(_) => {} }.size == 1, "there must be exactly one description tag or one description hash tag")
/**
*
* @return the payment hash
*/
def paymentHash = tags.collectFirst { case p: PaymentRequest.PaymentHashTag => p }.get.hash
/**
*
* @return the description of the payment, or its hash
*/
def description: Either[String, BinaryData] = tags.collectFirst {
case PaymentRequest.DescriptionTag(d) => Left(d)
case PaymentRequest.DescriptionHashTag(h) => Right(h)
}.get
/**
*
* @return the fallback address if any. It could be a script address, pubkey address, ..
*/
def fallbackAddress(): Option[String] = tags.collectFirst {
case PaymentRequest.FallbackAddressTag(17, hash) if prefix == "lnbc" => Base58Check.encode(Base58.Prefix.PubkeyAddress, hash)
case PaymentRequest.FallbackAddressTag(18, hash) if prefix == "lnbc" => Base58Check.encode(Base58.Prefix.ScriptAddress, hash)
case PaymentRequest.FallbackAddressTag(17, hash) if prefix == "lntb" => Base58Check.encode(Base58.Prefix.PubkeyAddressTestnet, hash)
case PaymentRequest.FallbackAddressTag(18, hash) if prefix == "lntb" => Base58Check.encode(Base58.Prefix.ScriptAddressTestnet, hash)
case PaymentRequest.FallbackAddressTag(version, hash) if prefix == "lnbc" => Bech32.encodeWitnessAddress("bc", version, hash)
case PaymentRequest.FallbackAddressTag(version, hash) if prefix == "lntb" => Bech32.encodeWitnessAddress("tb", version, hash)
}
def routingInfo(): Seq[RoutingInfoTag] = tags.collect { case t: RoutingInfoTag => t }
def expiry: Option[Long] = tags.collectFirst {
case PaymentRequest.ExpiryTag(seconds) => seconds
}
def minFinalCltvExpiry: Option[Long] = tags.collectFirst {
case PaymentRequest.MinFinalCltvExpiryTag(expiry) => expiry
}
/**
*
* @return a representation of this payment request, without its signature, as a bit stream. This is what will be signed.
*/
def stream: BitStream = {
val stream = BitStream.empty
val int5s = Timestamp.encode(timestamp) ++ (tags.map(_.toInt5s).flatten)
val stream1 = int5s.foldLeft(stream)(PaymentRequest.write5)
stream1
}
/**
*
* @return the hash of this payment request
*/
def hash: BinaryData = Crypto.sha256(s"${prefix}${Amount.encode(amount)}".getBytes("UTF-8") ++ stream.bytes)
/**
*
* @param priv private key
* @return a signed payment request
*/
def sign(priv: PrivateKey): PaymentRequest = {
val (r, s) = Crypto.sign(hash, priv)
val (pub1, pub2) = Crypto.recoverPublicKey((r, s), hash)
val recid = if (nodeId == pub1) 0.toByte else 1.toByte
val signature = PaymentRequest.Signature.encode(r, s, recid)
this.copy(signature = signature)
}
}
object PaymentRequest {
// https://github.com/lightningnetwork/lightning-rfc/blob/master/02-peer-protocol.md#adding-an-htlc-update_add_htlc
val maxAmount = MilliSatoshi(4294967296L)
def apply(chainHash: BinaryData, amount: Option[MilliSatoshi], paymentHash: BinaryData, privateKey: PrivateKey,
description: String, fallbackAddress: Option[String] = None, expirySeconds: Option[Long] = None,
extraHops: Seq[Seq[ExtraHop]] = Nil, timestamp: Long = System.currentTimeMillis() / 1000L): PaymentRequest = {
val prefix = chainHash match {
case Block.RegtestGenesisBlock.hash => "lntb"
case Block.TestnetGenesisBlock.hash => "lntb"
case Block.LivenetGenesisBlock.hash => "lnbc"
}
PaymentRequest(
prefix = prefix,
amount = amount,
timestamp = timestamp,
nodeId = privateKey.publicKey,
tags = List(
Some(PaymentHashTag(paymentHash)),
Some(DescriptionTag(description)),
expirySeconds.map(ExpiryTag(_))
).flatten ++ extraHops.map(RoutingInfoTag(_)),
signature = BinaryData.empty)
.sign(privateKey)
}
sealed trait Tag {
def toInt5s: Seq[Int5]
}
/**
* Payment Hash Tag
*
* @param hash payment hash
*/
case class PaymentHashTag(hash: BinaryData) extends Tag {
override def toInt5s = {
val ints = Bech32.eight2five(hash)
Seq(Bech32.map('p'), (ints.length / 32).toByte, (ints.length % 32).toByte) ++ ints
}
}
/**
* Description Tag
*
* @param description a free-format string that will be included in the payment request
*/
case class DescriptionTag(description: String) extends Tag {
override def toInt5s = {
val ints = Bech32.eight2five(description.getBytes("UTF-8"))
Seq(Bech32.map('d'), (ints.length / 32).toByte, (ints.length % 32).toByte) ++ ints
}
}
/**
* Hash Tag
*
* @param hash hash that will be included in the payment request, and can be checked against the hash of a
* long description, an invoice, ...
*/
case class DescriptionHashTag(hash: BinaryData) extends Tag {
override def toInt5s = {
val ints = Bech32.eight2five(hash)
Seq(Bech32.map('h'), (ints.length / 32).toByte, (ints.length % 32).toByte) ++ ints
}
}
/**
* Fallback Payment Tag that specifies a fallback payment address to be used if LN payment cannot be processed
*
* @param version address version; valid values are
* - 17 (pubkey hash)
* - 18 (script hash)
* - 0 (segwit hash: p2wpkh (20 bytes) or p2wsh (32 bytes))
* @param hash address hash
*/
case class FallbackAddressTag(version: Byte, hash: BinaryData) extends Tag {
override def toInt5s = {
val ints = version +: Bech32.eight2five(hash)
Seq(Bech32.map('f'), (ints.length / 32).toByte, (ints.length % 32).toByte) ++ ints
}
}
object FallbackAddressTag {
/**
*
* @param address valid base58 or bech32 address
* @return a FallbackAddressTag instance
*/
def apply(address: String): FallbackAddressTag = {
Try(fromBase58Address(address)).orElse(Try(fromBech32Address(address))).get
}
def fromBase58Address(address: String): FallbackAddressTag = {
val (prefix, hash) = Base58Check.decode(address)
prefix match {
case Base58.Prefix.PubkeyAddress => FallbackAddressTag(17, hash)
case Base58.Prefix.PubkeyAddressTestnet => FallbackAddressTag(17, hash)
case Base58.Prefix.ScriptAddress => FallbackAddressTag(18, hash)
case Base58.Prefix.ScriptAddressTestnet => FallbackAddressTag(18, hash)
}
}
def fromBech32Address(address: String): FallbackAddressTag = {
val (prefix, hash) = Bech32.decodeWitnessAddress(address)
FallbackAddressTag(prefix, hash)
}
}
/**
* Extra hop contained in RoutingInfoTag
*
* @param nodeId node id
* @param shortChannelId channel id
* @param fee node fee
* @param cltvExpiryDelta node cltv expiry delta
*/
case class ExtraHop(nodeId: PublicKey, shortChannelId: Long, fee: Long, cltvExpiryDelta: Int) extends PaymentHop {
def pack: Seq[Byte] = nodeId.toBin ++ Protocol.writeUInt64(shortChannelId, ByteOrder.BIG_ENDIAN) ++
Protocol.writeUInt64(fee, ByteOrder.BIG_ENDIAN) ++ Protocol.writeUInt16(cltvExpiryDelta, ByteOrder.BIG_ENDIAN)
// Fee is already pre-calculated for extra hops
def nextFee(msat: Long): Long = fee
}
/**
* Routing Info Tag
*
* @param path one or more entries containing extra routing information for a private route
*/
case class RoutingInfoTag(path: Seq[ExtraHop]) extends Tag {
override def toInt5s = {
val ints = Bech32.eight2five(path.flatMap(_.pack))
Seq(Bech32.map('r'), (ints.length / 32).toByte, (ints.length % 32).toByte) ++ ints
}
}
object RoutingInfoTag {
def parse(data: Seq[Byte]) = {
val pubkey = data.slice(0, 33)
val shortChannelId = Protocol.uint64(data.slice(33, 33 + 8), ByteOrder.BIG_ENDIAN)
val fee = Protocol.uint64(data.slice(33 + 8, 33 + 8 + 8), ByteOrder.BIG_ENDIAN)
val cltv = Protocol.uint16(data.slice(33 + 8 + 8, chunkLength), ByteOrder.BIG_ENDIAN)
ExtraHop(PublicKey(pubkey), shortChannelId, fee, cltv)
}
def parseAll(data: Seq[Byte]): Seq[ExtraHop] =
data.grouped(chunkLength).map(parse).toList
val chunkLength: Int = 33 + 8 + 8 + 2
}
/**
* Expiry Date
*
* @param seconds expiry data for this payment request
*/
case class ExpiryTag(seconds: Long) extends Tag {
override def toInt5s = {
val ints = writeUnsignedLong(seconds)
Bech32.map('x') +: (writeSize(ints.size) ++ ints)
}
}
/**
* Min final CLTV expiry
*
* @param blocks min final cltv expiry, in blocks
*/
case class MinFinalCltvExpiryTag(blocks: Long) extends Tag {
override def toInt5s = {
val ints = writeUnsignedLong(blocks)
Bech32.map('c') +: (writeSize(ints.size) ++ ints)
}
}
object Amount {
/**
* @param amount
* @return the unit allowing for the shortest representation possible
*/
def unit(amount: MilliSatoshi): Char = amount.amount * 10 match { // 1 milli-satoshis == 10 pico-bitcoin
case pico if pico % 1000 > 0 => 'p'
case pico if pico % 1000000 > 0 => 'n'
case pico if pico % 1000000000 > 0 => 'u'
case _ => 'm'
}
def decode(input: String): Option[MilliSatoshi] =
input match {
case "" => None
case a if a.last == 'p' => Some(MilliSatoshi(a.dropRight(1).toLong / 10L)) // 1 pico-bitcoin == 10 milli-satoshis
case a if a.last == 'n' => Some(MilliSatoshi(a.dropRight(1).toLong * 100L))
case a if a.last == 'u' => Some(MilliSatoshi(a.dropRight(1).toLong * 100000L))
case a if a.last == 'm' => Some(MilliSatoshi(a.dropRight(1).toLong * 100000000L))
}
def encode(amount: Option[MilliSatoshi]): String = {
amount match {
case None => ""
case Some(amt) if unit(amt) == 'p' => s"${amt.amount * 10L}p" // 1 pico-bitcoin == 10 milli-satoshis
case Some(amt) if unit(amt) == 'n' => s"${amt.amount / 100L}n"
case Some(amt) if unit(amt) == 'u' => s"${amt.amount / 100000L}u"
case Some(amt) if unit(amt) == 'm' => s"${amt.amount / 100000000L}m"
}
}
}
object Tag {
def parse(input: Seq[Byte]): Tag = {
val tag = input(0)
val len = input(1) * 32 + input(2)
tag match {
case p if p == Bech32.map('p') =>
val hash = Bech32.five2eight(input.drop(3).take(52))
PaymentHashTag(hash)
case d if d == Bech32.map('d') =>
val description = new String(Bech32.five2eight(input.drop(3).take(len)).toArray, "UTF-8")
DescriptionTag(description)
case h if h == Bech32.map('h') =>
val hash: BinaryData = Bech32.five2eight(input.drop(3).take(len))
DescriptionHashTag(hash)
case f if f == Bech32.map('f') =>
val version = input(3)
val prog = Bech32.five2eight(input.drop(4).take(len - 1))
version match {
case v if v >= 0 && v <= 16 =>
FallbackAddressTag(version, prog)
case 17 | 18 =>
FallbackAddressTag(version, prog)
}
case r if r == Bech32.map('r') =>
val data = Bech32.five2eight(input.drop(3).take(len))
val path = RoutingInfoTag.parseAll(data)
RoutingInfoTag(path)
case x if x == Bech32.map('x') =>
val expiry = readUnsignedLong(len, input.drop(3).take(len))
ExpiryTag(expiry)
case c if c == Bech32.map('c') =>
val expiry = readUnsignedLong(len, input.drop(3).take(len))
MinFinalCltvExpiryTag(expiry)
}
}
}
object Timestamp {
def decode(data: Seq[Int5]): Long = data.take(7).foldLeft(0L)((a, b) => a * 32 + b)
def encode(timestamp: Long, acc: Seq[Int5] = Nil): Seq[Int5] = if (acc.length == 7) acc else {
encode(timestamp / 32, (timestamp % 32).toByte +: acc)
}
}
object Signature {
/**
*
* @param signature 65-bytes signatyre: r (32 bytes) | s (32 bytes) | recid (1 bytes)
* @return a (r, s, recoveryId)
*/
def decode(signature: BinaryData): (BigInteger, BigInteger, Byte) = {
require(signature.length == 65)
val r = new BigInteger(1, signature.take(32).toArray)
val s = new BigInteger(1, signature.drop(32).take(32).toArray)
val recid = signature.last
(r, s, recid)
}
/**
*
* @return a 65 bytes representation of (r, s, recid)
*/
def encode(r: BigInteger, s: BigInteger, recid: Byte): BinaryData = {
Crypto.fixSize(r.toByteArray.dropWhile(_ == 0.toByte)) ++ Crypto.fixSize(s.toByteArray.dropWhile(_ == 0.toByte)) :+ recid
}
}
def toBits(value: Int5): Seq[Bit] = Seq((value & 16) != 0, (value & 8) != 0, (value & 4) != 0, (value & 2) != 0, (value & 1) != 0)
/**
* write a 5bits integer to a stream
*
* @param stream stream to write to
* @param value a 5bits value
* @return an upated stream
*/
def write5(stream: BitStream, value: Int5): BitStream = stream.writeBits(toBits(value))
/**
* read a 5bits value from a stream
*
* @param stream stream to read from
* @return a (stream, value) pair
*/
def read5(stream: BitStream): (BitStream, Int5) = {
val (stream1, bits) = stream.readBits(5)
val value = (if (bits(0)) 1 << 4 else 0) + (if (bits(1)) 1 << 3 else 0) + (if (bits(2)) 1 << 2 else 0) + (if (bits(3)) 1 << 1 else 0) + (if (bits(4)) 1 << 0 else 0)
(stream1, (value & 0xff).toByte)
}
/**
* splits a bit stream into 5bits values
*
* @param stream
* @param acc
* @return a sequence of 5bits values
*/
@tailrec
def toInt5s(stream: BitStream, acc: Seq[Int5] = Nil): Seq[Int5] = if (stream.bitCount == 0) acc else {
val (stream1, value) = read5(stream)
toInt5s(stream1, acc :+ value)
}
/**
* prepend an unsigned long value to a sequence of Int5s
*
* @param value input value
* @param acc sequence of Int5 values
* @return an update sequence of Int5s
*/
@tailrec
def writeUnsignedLong(value: Long, acc: Seq[Int5] = Nil): Seq[Int5] = {
require(value >= 0)
if (value == 0) acc
else writeUnsignedLong(value / 32, (value % 32).toByte +: acc)
}
/**
* convert a tag data size to a sequence of Int5s. It * must * fit on a sequence
* of 2 Int5 values
*
* @param size data size
* @return size as a sequence of exactly 2 Int5 values
*/
def writeSize(size: Long): Seq[Int5] = {
val output = writeUnsignedLong(size)
// make sure that size is encoded on 2 int5 values
output.length match {
case 0 => Seq(0.toByte, 0.toByte)
case 1 => 0.toByte +: output
case 2 => output
case n => throw new IllegalArgumentException("tag data length field must be encoded on 2 5-bits integers")
}
}
/**
* reads an unsigned long value from a sequence of Int5s
*
* @param length length of the sequence
* @param ints sequence of Int5s
* @return an unsigned long value
*/
def readUnsignedLong(length: Int, ints: Seq[Int5]): Long = ints.take(length).foldLeft(0L) { case (acc, i) => acc * 32 + i }
/**
*
* @param input bech32-encoded payment request
* @return a payment request
*/
def read(input: String): PaymentRequest = {
val (hrp, data) = Bech32.decode(input)
val stream = data.foldLeft(BitStream.empty)(write5)
require(stream.bitCount >= 65 * 8, "data is too short to contain a 65 bytes signature")
val (stream1, sig) = stream.popBytes(65)
val data0 = toInt5s(stream1)
val timestamp = Timestamp.decode(data0)
val data1 = data0.drop(7)
@tailrec
def loop(data: Seq[Int5], tags: Seq[Seq[Int5]] = Nil): Seq[Seq[Int5]] = if (data.isEmpty) tags else {
// 104 is the size of a signature
val len = 1 + 2 + 32 * data(1) + data(2)
loop(data.drop(len), tags :+ data.take(len))
}
val rawtags = loop(data1)
val tags = rawtags.map(Tag.parse)
val signature = sig.reverse
val r = new BigInteger(1, signature.take(32).toArray)
val s = new BigInteger(1, signature.drop(32).take(32).toArray)
val recid = signature.last
val message: BinaryData = hrp.getBytes ++ stream1.bytes
val (pub1, pub2) = Crypto.recoverPublicKey((r, s), Crypto.sha256(message))
val pub = if (recid % 2 != 0) pub2 else pub1
val prefix = hrp.take(4)
val amount_opt = Amount.decode(hrp.drop(4))
val pr = PaymentRequest(prefix, amount_opt, timestamp, pub, tags.toList, signature)
val validSig = Crypto.verifySignature(Crypto.sha256(message), (r, s), pub)
require(validSig, "invalid signature")
pr
}
/**
*
* @param pr payment request
* @return a bech32-encoded payment request
*/
def write(pr: PaymentRequest): String = {
// currency unit is Satoshi, but we compute amounts in Millisatoshis
val hramount = Amount.encode(pr.amount)
val hrp = s"${pr.prefix}$hramount"
val stream = pr.stream.writeBytes(pr.signature)
val checksum = Bech32.checksum(hrp, toInt5s(stream))
hrp + "1" + new String((toInt5s(stream) ++ checksum).map(i => Bech32.pam(i)).toArray)
}
}

View File

@ -0,0 +1,153 @@
package fr.acinq.eclair.payment
import akka.actor.{Actor, ActorLogging, ActorRef, Props, Status}
import fr.acinq.bitcoin.{BinaryData, Crypto, MilliSatoshi}
import fr.acinq.eclair.channel._
import fr.acinq.eclair.crypto.Sphinx
import fr.acinq.eclair.router.Announcements
import fr.acinq.eclair.wire._
import fr.acinq.eclair.{Globals, NodeParams}
import scodec.bits.BitVector
import scodec.{Attempt, DecodeResult}
import scala.util.{Failure, Success, Try}
// @formatter:off
sealed trait Origin
case class Local(sender: Option[ActorRef]) extends Origin // we don't persist reference to local actors
case class Relayed(originChannelId: BinaryData, originHtlcId: Long, amountMsatIn: Long, amountMsatOut: Long) extends Origin
case class ForwardAdd(add: UpdateAddHtlc)
case class ForwardFulfill(fulfill: UpdateFulfillHtlc, to: Origin)
case class ForwardLocalFail(error: Throwable, to: Origin) // happens when the failure happened in a local channel (and not in some downstream channel)
case class ForwardFail(fail: UpdateFailHtlc, to: Origin)
case class ForwardFailMalformed(fail: UpdateFailMalformedHtlc, to: Origin)
case class AckFulfillCmd(channelId: BinaryData, htlcId: Long)
// @formatter:on
/**
* Created by PM on 01/02/2017.
*/
class Relayer(nodeParams: NodeParams, register: ActorRef, paymentHandler: ActorRef) extends Actor with ActorLogging {
import nodeParams.preimagesDb
context.system.eventStream.subscribe(self, classOf[ChannelStateChanged])
override def receive: Receive = main(Map())
def main(channelUpdates: Map[Long, ChannelUpdate]): Receive = {
case ChannelStateChanged(channel, _, _, _, NORMAL | SHUTDOWN | CLOSING, d: HasCommitments) =>
import d.channelId
preimagesDb.listPreimages(channelId) match {
case Nil => {}
case preimages =>
log.info(s"re-sending ${preimages.size} unacked fulfills to channel $channelId")
preimages.map(p => CMD_FULFILL_HTLC(p._2, p._3, commit = false)).foreach(channel ! _)
// better to sign once instead of after each fulfill
channel ! CMD_SIGN
}
case channelUpdate: ChannelUpdate =>
log.info(s"updating relay parameters with channelUpdate=$channelUpdate")
context become main(channelUpdates + (channelUpdate.shortChannelId -> channelUpdate))
case ForwardAdd(add) =>
Try(Sphinx.parsePacket(nodeParams.privateKey, add.paymentHash, add.onionRoutingPacket))
.map {
case Sphinx.ParsedPacket(payload, nextPacket, sharedSecret) => (LightningMessageCodecs.perHopPayloadCodec.decode(BitVector(payload.data)), nextPacket, sharedSecret)
} match {
case Success((Attempt.Successful(DecodeResult(perHopPayload, _)), nextPacket, _)) if nextPacket.isLastPacket =>
log.info(s"looks like we are the final recipient of htlc #${add.id}")
perHopPayload match {
case PerHopPayload(_, finalAmountToForward, _) if finalAmountToForward > add.amountMsat =>
sender ! CMD_FAIL_HTLC(add.id, Right(FinalIncorrectHtlcAmount(add.amountMsat)), commit = true)
case PerHopPayload(_, _, finalOutgoingCltvValue) if finalOutgoingCltvValue != add.expiry =>
sender ! CMD_FAIL_HTLC(add.id, Right(FinalIncorrectCltvExpiry(add.expiry)), commit = true)
case _ if add.expiry < Globals.blockCount.get() + 3 => // TODO: check hardcoded value
sender ! CMD_FAIL_HTLC(add.id, Right(FinalExpiryTooSoon), commit = true)
case _ =>
paymentHandler forward add
}
case Success((Attempt.Successful(DecodeResult(perHopPayload, _)), nextPacket, _)) =>
val channelUpdate_opt = channelUpdates.get(perHopPayload.channel_id)
channelUpdate_opt match {
case None =>
// TODO: clarify what we're supposed to do in the specs
sender ! CMD_FAIL_HTLC(add.id, Right(TemporaryNodeFailure), commit = true)
case Some(channelUpdate) if !Announcements.isEnabled(channelUpdate.flags) =>
sender ! CMD_FAIL_HTLC(add.id, Right(ChannelDisabled(channelUpdate.flags, channelUpdate)), commit = true)
case Some(channelUpdate) if add.amountMsat < channelUpdate.htlcMinimumMsat =>
sender ! CMD_FAIL_HTLC(add.id, Right(AmountBelowMinimum(add.amountMsat, channelUpdate)), commit = true)
case Some(channelUpdate) if add.expiry != perHopPayload.outgoingCltvValue + channelUpdate.cltvExpiryDelta =>
sender ! CMD_FAIL_HTLC(add.id, Right(IncorrectCltvExpiry(add.expiry, channelUpdate)), commit = true)
case Some(channelUpdate) if add.expiry < Globals.blockCount.get() + 3 => // TODO: hardcoded value
sender ! CMD_FAIL_HTLC(add.id, Right(ExpiryTooSoon(channelUpdate)), commit = true)
case _ =>
log.info(s"forwarding htlc #${add.id} to shortChannelId=${perHopPayload.channel_id}")
register forward Register.ForwardShortId(perHopPayload.channel_id, CMD_ADD_HTLC(perHopPayload.amtToForward, add.paymentHash, perHopPayload.outgoingCltvValue, nextPacket.serialize, upstream_opt = Some(add), commit = true))
}
case Success((Attempt.Failure(cause), _, _)) =>
log.error(s"couldn't parse payload: $cause")
sender ! CMD_FAIL_HTLC(add.id, Right(PermanentNodeFailure), commit = true)
case Failure(t) =>
log.error(t, "couldn't parse onion: ")
// we cannot even parse the onion packet
sender ! CMD_FAIL_MALFORMED_HTLC(add.id, Crypto.sha256(add.onionRoutingPacket), failureCode = FailureMessageCodecs.BADONION, commit = true)
}
case Register.ForwardShortIdFailure(Register.ForwardShortId(shortChannelId, CMD_ADD_HTLC(_, _, _, _, Some(add), _))) =>
log.warning(s"couldn't resolve downstream channel $shortChannelId, failing htlc #${add.id}")
register ! Register.Forward(add.channelId, CMD_FAIL_HTLC(add.id, Right(UnknownNextPeer), commit = true))
case ForwardFulfill(fulfill, Local(Some(sender))) =>
sender ! fulfill
case ForwardFulfill(fulfill, Relayed(originChannelId, originHtlcId, amountMsatIn, amountMsatOut)) =>
val cmd = CMD_FULFILL_HTLC(originHtlcId, fulfill.paymentPreimage, commit = true)
register ! Register.Forward(originChannelId, cmd)
context.system.eventStream.publish(PaymentRelayed(MilliSatoshi(amountMsatIn), MilliSatoshi(amountMsatOut), Crypto.sha256(fulfill.paymentPreimage)))
// we also store the preimage in a db (note that this happens *after* forwarding the fulfill to the channel, so we don't add latency)
preimagesDb.addPreimage(originChannelId, originHtlcId, fulfill.paymentPreimage)
case AckFulfillCmd(channelId, htlcId) =>
log.debug(s"fulfill acked for channelId=$channelId htlcId=$htlcId")
preimagesDb.removePreimage(channelId, htlcId)
case ForwardLocalFail(error, Local(Some(sender))) =>
sender ! Status.Failure(error)
case ForwardLocalFail(error, Relayed(originChannelId, originHtlcId, _, _)) =>
// TODO: clarify what we're supposed to do in the specs depending on the error
val failure = error match {
case HtlcTimedout(_) => PermanentChannelFailure
case _ => TemporaryNodeFailure
}
val cmd = CMD_FAIL_HTLC(originHtlcId, Right(failure), commit = true)
register ! Register.Forward(originChannelId, cmd)
case ForwardFail(fail, Local(Some(sender))) =>
sender ! fail
case ForwardFail(fail, Relayed(originChannelId, originHtlcId, _, _)) =>
val cmd = CMD_FAIL_HTLC(originHtlcId, Left(fail.reason), commit = true)
register ! Register.Forward(originChannelId, cmd)
case ForwardFailMalformed(fail, Local(Some(sender))) =>
sender ! fail
case ForwardFailMalformed(fail, Relayed(originChannelId, originHtlcId, _, _)) =>
val cmd = CMD_FAIL_MALFORMED_HTLC(originHtlcId, fail.onionHash, fail.failureCode, commit = true)
register ! Register.Forward(originChannelId, cmd)
}
}
object Relayer {
def props(nodeParams: NodeParams, register: ActorRef, paymentHandler: ActorRef) = Props(classOf[Relayer], nodeParams, register, paymentHandler)
}

View File

@ -0,0 +1,142 @@
package fr.acinq.eclair.router
import java.net.InetSocketAddress
import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey, sha256, verifySignature}
import fr.acinq.bitcoin.{BinaryData, Crypto, LexicographicalOrdering}
import fr.acinq.eclair.serializationResult
import fr.acinq.eclair.wire.{ChannelAnnouncement, ChannelUpdate, LightningMessageCodecs, NodeAnnouncement}
import scodec.bits.BitVector
import shapeless.HNil
import scala.compat.Platform
/**
* Created by PM on 03/02/2017.
*/
object Announcements {
def channelAnnouncementWitnessEncode(chainHash: BinaryData, shortChannelId: Long, nodeId1: PublicKey, nodeId2: PublicKey, bitcoinKey1: PublicKey, bitcoinKey2: PublicKey, features: BinaryData): BinaryData =
sha256(sha256(serializationResult(LightningMessageCodecs.channelAnnouncementWitnessCodec.encode(features :: chainHash :: shortChannelId :: nodeId1 :: nodeId2 :: bitcoinKey1 :: bitcoinKey2 :: HNil))))
def nodeAnnouncementWitnessEncode(timestamp: Long, nodeId: PublicKey, rgbColor: (Byte, Byte, Byte), alias: String, features: BinaryData, addresses: List[InetSocketAddress]): BinaryData =
sha256(sha256(serializationResult(LightningMessageCodecs.nodeAnnouncementWitnessCodec.encode(features :: timestamp :: nodeId :: rgbColor :: alias :: addresses :: HNil))))
def channelUpdateWitnessEncode(chainHash: BinaryData, shortChannelId: Long, timestamp: Long, flags: BinaryData, cltvExpiryDelta: Int, htlcMinimumMsat: Long, feeBaseMsat: Long, feeProportionalMillionths: Long): BinaryData =
sha256(sha256(serializationResult(LightningMessageCodecs.channelUpdateWitnessCodec.encode(chainHash :: shortChannelId :: timestamp :: flags :: cltvExpiryDelta :: htlcMinimumMsat :: feeBaseMsat :: feeProportionalMillionths :: HNil))))
def signChannelAnnouncement(chainHash: BinaryData, shortChannelId: Long, localNodeSecret: PrivateKey, remoteNodeId: PublicKey, localFundingPrivKey: PrivateKey, remoteFundingKey: PublicKey, features: BinaryData): (BinaryData, BinaryData) = {
val witness = if (isNode1(localNodeSecret.publicKey.toBin, remoteNodeId.toBin)) {
channelAnnouncementWitnessEncode(chainHash, shortChannelId, localNodeSecret.publicKey, remoteNodeId, localFundingPrivKey.publicKey, remoteFundingKey, features)
} else {
channelAnnouncementWitnessEncode(chainHash, shortChannelId, remoteNodeId, localNodeSecret.publicKey, remoteFundingKey, localFundingPrivKey.publicKey, features)
}
val nodeSig = Crypto.encodeSignature(Crypto.sign(witness, localNodeSecret)) :+ 1.toByte
val bitcoinSig = Crypto.encodeSignature(Crypto.sign(witness, localFundingPrivKey)) :+ 1.toByte
(nodeSig, bitcoinSig)
}
def makeChannelAnnouncement(chainHash: BinaryData, shortChannelId: Long, localNodeId: PublicKey, remoteNodeId: PublicKey, localFundingKey: PublicKey, remoteFundingKey: PublicKey, localNodeSignature: BinaryData, remoteNodeSignature: BinaryData, localBitcoinSignature: BinaryData, remoteBitcoinSignature: BinaryData): ChannelAnnouncement = {
val (nodeId1, nodeId2, bitcoinKey1, bitcoinKey2, nodeSignature1, nodeSignature2, bitcoinSignature1, bitcoinSignature2) =
if (isNode1(localNodeId.toBin, remoteNodeId.toBin)) {
(localNodeId, remoteNodeId, localFundingKey, remoteFundingKey, localNodeSignature, remoteNodeSignature, localBitcoinSignature, remoteBitcoinSignature)
} else {
(remoteNodeId, localNodeId, remoteFundingKey, localFundingKey, remoteNodeSignature, localNodeSignature, remoteBitcoinSignature, localBitcoinSignature)
}
ChannelAnnouncement(
nodeSignature1 = nodeSignature1,
nodeSignature2 = nodeSignature2,
bitcoinSignature1 = bitcoinSignature1,
bitcoinSignature2 = bitcoinSignature2,
shortChannelId = shortChannelId,
nodeId1 = nodeId1,
nodeId2 = nodeId2,
bitcoinKey1 = bitcoinKey1,
bitcoinKey2 = bitcoinKey2,
features = BinaryData(""),
chainHash = chainHash
)
}
def makeNodeAnnouncement(nodeSecret: PrivateKey, alias: String, color: (Byte, Byte, Byte), addresses: List[InetSocketAddress], timestamp: Long = Platform.currentTime / 1000): NodeAnnouncement = {
require(alias.size <= 32)
val witness = nodeAnnouncementWitnessEncode(timestamp, nodeSecret.publicKey, color, alias, "", addresses)
val sig = Crypto.encodeSignature(Crypto.sign(witness, nodeSecret)) :+ 1.toByte
NodeAnnouncement(
signature = sig,
timestamp = timestamp,
nodeId = nodeSecret.publicKey,
rgbColor = color,
alias = alias,
features = "",
addresses = addresses
)
}
/**
* BOLT 7:
* The creating node MUST set node-id-1 and node-id-2 to the public keys of the
* two nodes who are operating the channel, such that node-id-1 is the numerically-lesser
* of the two DER encoded keys sorted in ascending numerical order,
*
* @return true if localNodeId is node1
*/
def isNode1(localNodeId: BinaryData, remoteNodeId: BinaryData) = LexicographicalOrdering.isLessThan(localNodeId, remoteNodeId)
/**
* BOLT 7:
* The creating node [...] MUST set the direction bit of flags to 0 if
* the creating node is node-id-1 in that message, otherwise 1.
*
* @return true if the node who sent these flags is node1
*/
def isNode1(flags: BinaryData) = !BitVector(flags.data).reverse.get(0)
/**
* A node MAY create and send a channel_update with the disable bit set to
* signal the temporary unavailability of a channel
*
* @return
*/
def isEnabled(flags: BinaryData) = !BitVector(flags.data).reverse.get(1)
def makeFlags(isNode1: Boolean, enable: Boolean): BinaryData = BitVector.bits(!enable :: !isNode1 :: Nil).padLeft(16).toByteArray
def makeChannelUpdate(chainHash: BinaryData, nodeSecret: PrivateKey, remoteNodeId: PublicKey, shortChannelId: Long, cltvExpiryDelta: Int, htlcMinimumMsat: Long, feeBaseMsat: Long, feeProportionalMillionths: Long, enable: Boolean = true, timestamp: Long = Platform.currentTime / 1000): ChannelUpdate = {
val flags = makeFlags(isNode1 = isNode1(nodeSecret.publicKey.toBin, remoteNodeId.toBin), enable = enable)
require(flags.size == 2, "flags must be a 2-bytes field")
val witness = channelUpdateWitnessEncode(chainHash, shortChannelId, timestamp, flags, cltvExpiryDelta, htlcMinimumMsat, feeBaseMsat, feeProportionalMillionths)
val sig = Crypto.encodeSignature(Crypto.sign(witness, nodeSecret)) :+ 1.toByte
ChannelUpdate(
signature = sig,
chainHash = chainHash,
shortChannelId = shortChannelId,
timestamp = timestamp,
flags = flags,
cltvExpiryDelta = cltvExpiryDelta,
htlcMinimumMsat = htlcMinimumMsat,
feeBaseMsat = feeBaseMsat,
feeProportionalMillionths = feeProportionalMillionths
)
}
def checkSigs(ann: ChannelAnnouncement): Boolean = {
val witness = channelAnnouncementWitnessEncode(ann.chainHash, ann.shortChannelId, ann.nodeId1, ann.nodeId2, ann.bitcoinKey1, ann.bitcoinKey2, ann.features)
verifySignature(witness, ann.nodeSignature1, ann.nodeId1) &&
verifySignature(witness, ann.nodeSignature2, ann.nodeId2) &&
verifySignature(witness, ann.bitcoinSignature1, ann.bitcoinKey1) &&
verifySignature(witness, ann.bitcoinSignature2, ann.bitcoinKey2)
}
def checkSig(ann: NodeAnnouncement): Boolean = {
val witness = nodeAnnouncementWitnessEncode(ann.timestamp, ann.nodeId, ann.rgbColor, ann.alias, ann.features, ann.addresses)
verifySignature(witness, ann.signature, ann.nodeId)
}
def checkSig(ann: ChannelUpdate, nodeId: PublicKey): Boolean = {
val witness = channelUpdateWitnessEncode(ann.chainHash, ann.shortChannelId, ann.timestamp, ann.flags, ann.cltvExpiryDelta, ann.htlcMinimumMsat, ann.feeBaseMsat, ann.feeProportionalMillionths)
verifySignature(witness, ann.signature, nodeId)
}
}

View File

@ -0,0 +1,22 @@
package fr.acinq.eclair.router
import fr.acinq.bitcoin.Crypto.PublicKey
import fr.acinq.bitcoin.Satoshi
import fr.acinq.eclair.wire.{ChannelAnnouncement, ChannelUpdate, NodeAnnouncement}
/**
* Created by PM on 02/02/2017.
*/
trait NetworkEvent
case class NodeDiscovered(ann: NodeAnnouncement) extends NetworkEvent
case class NodeUpdated(ann: NodeAnnouncement) extends NetworkEvent
case class NodeLost(nodeId: PublicKey) extends NetworkEvent
case class ChannelDiscovered(ann: ChannelAnnouncement, capacity: Satoshi) extends NetworkEvent
case class ChannelLost(channelId: Long) extends NetworkEvent
case class ChannelUpdateReceived(ann: ChannelUpdate) extends NetworkEvent

View File

@ -0,0 +1,443 @@
package fr.acinq.eclair.router
import java.io.StringWriter
import akka.actor.{ActorRef, FSM, Props}
import akka.pattern.pipe
import fr.acinq.bitcoin.BinaryData
import fr.acinq.bitcoin.Crypto.PublicKey
import fr.acinq.bitcoin.Script.{pay2wsh, write}
import fr.acinq.eclair._
import fr.acinq.eclair.blockchain._
import fr.acinq.eclair.channel._
import fr.acinq.eclair.io.Peer
import fr.acinq.eclair.payment.Hop
import fr.acinq.eclair.transactions.Scripts
import fr.acinq.eclair.wire._
import org.jgrapht.alg.shortestpath.DijkstraShortestPath
import org.jgrapht.ext._
import org.jgrapht.graph.{DefaultDirectedGraph, DefaultEdge, SimpleGraph}
import scala.collection.JavaConversions._
import scala.compat.Platform
import scala.concurrent.duration._
import scala.concurrent.{ExecutionContext, Future}
import scala.util.{Random, Success, Try}
// @formatter:off
case class ChannelDesc(id: Long, a: PublicKey, b: PublicKey)
case class RouteRequest(source: PublicKey, target: PublicKey, ignoreNodes: Set[PublicKey] = Set.empty, ignoreChannels: Set[Long] = Set.empty)
case class RouteResponse(hops: Seq[Hop], ignoreNodes: Set[PublicKey], ignoreChannels: Set[Long]) { require(hops.size > 0, "route cannot be empty") }
case class ExcludeChannel(desc: ChannelDesc) // this is used when we get a TemporaryChannelFailure, to give time for the channel to recover (note that exclusions are directed)
case class LiftChannelExclusion(desc: ChannelDesc)
case class SendRoutingState(to: ActorRef)
case class Rebroadcast(ann: Seq[RoutingMessage], origins: Map[RoutingMessage, ActorRef])
case class Data(nodes: Map[PublicKey, NodeAnnouncement],
channels: Map[Long, ChannelAnnouncement],
updates: Map[ChannelDesc, ChannelUpdate],
rebroadcast: Seq[RoutingMessage],
stash: Seq[RoutingMessage],
awaiting: Seq[ChannelAnnouncement],
origins: Map[RoutingMessage, ActorRef],
localChannels: Map[BinaryData, PublicKey],
excludedChannels: Set[ChannelDesc]) // those channels are temporarily excluded from route calculation, because their node returned a TemporaryChannelFailure
sealed trait State
case object NORMAL extends State
case object WAITING_FOR_VALIDATION extends State
case object TickBroadcast
case object TickValidate
case object TickPruneStaleChannels
// @formatter:on
/**
* Created by PM on 24/05/2016.
*/
class Router(nodeParams: NodeParams, watcher: ActorRef) extends FSM[State, Data] {
import Router._
import ExecutionContext.Implicits.global
context.system.eventStream.subscribe(self, classOf[ChannelStateChanged])
setTimer(TickBroadcast.toString, TickBroadcast, nodeParams.routerBroadcastInterval, repeat = true)
setTimer(TickValidate.toString, TickValidate, nodeParams.routerValidateInterval, repeat = true)
setTimer(TickPruneStaleChannels.toString, TickPruneStaleChannels, 1 day, repeat = true)
val db = nodeParams.networkDb
// Note: We go through the whole validation process instead of directly loading into memory, because the channels
// could have been closed while we were shutdown, and if someone connects to us right after startup we don't want to
// advertise invalid channels. We could optimize this (at least not fetch txes from the blockchain, and not check sigs)
log.info(s"loading network announcements from db...")
db.listChannels().map(self ! _)
db.listNodes().map(self ! _)
db.listChannelUpdates().map(self ! _)
if (db.listChannels().size > 0) {
val nodeAnn = Announcements.makeNodeAnnouncement(nodeParams.privateKey, nodeParams.alias, nodeParams.color, nodeParams.publicAddresses, Platform.currentTime / 1000)
self ! nodeAnn
}
log.info(s"starting state machine")
startWith(NORMAL, Data(Map.empty, Map.empty, Map.empty, Nil, Nil, Nil, Map.empty, Map.empty, Set.empty))
when(NORMAL) {
case Event(TickValidate, d) =>
require(d.awaiting.size == 0)
var i = 0
// we extract a batch of channel announcements from the stash
val (channelAnns: Seq[ChannelAnnouncement]@unchecked, otherAnns) = d.stash.partition {
case _: ChannelAnnouncement =>
i = i + 1
i <= MAX_PARALLEL_JSONRPC_REQUESTS
case _ => false
}
if (channelAnns.size > 0) {
log.info(s"validating a batch of ${channelAnns.size} channels")
watcher ! ParallelGetRequest(channelAnns)
goto(WAITING_FOR_VALIDATION) using d.copy(stash = otherAnns, awaiting = channelAnns)
} else stay
}
when(WAITING_FOR_VALIDATION) {
case Event(ParallelGetResponse(results), d) =>
val validated = results.map {
case IndividualResult(c, Some(tx), true) =>
// TODO: blacklisting
val (_, _, outputIndex) = fromShortId(c.shortChannelId)
// let's check that the output is indeed a P2WSH multisig 2-of-2 of nodeid1 and nodeid2)
val fundingOutputScript = write(pay2wsh(Scripts.multiSig2of2(PublicKey(c.bitcoinKey1), PublicKey(c.bitcoinKey2))))
if (tx.txOut.size < outputIndex + 1) {
log.error(s"invalid script for shortChannelId=${c.shortChannelId}: txid=${tx.txid} does not have outputIndex=$outputIndex ann=$c")
None
} else if (fundingOutputScript != tx.txOut(outputIndex).publicKeyScript) {
log.error(s"invalid script for shortChannelId=${c.shortChannelId} txid=${tx.txid} ann=$c")
None
} else {
watcher ! WatchSpentBasic(self, tx, outputIndex, BITCOIN_FUNDING_EXTERNAL_CHANNEL_SPENT(c.shortChannelId))
// TODO: check feature bit set
log.debug(s"added channel channelId=${c.shortChannelId}")
context.system.eventStream.publish(ChannelDiscovered(c, tx.txOut(outputIndex).amount))
db.addChannel(c)
Some(c)
}
case IndividualResult(c, Some(tx), false) =>
// TODO: vulnerability if they flood us with spent funding tx?
log.warning(s"ignoring shortChannelId=${c.shortChannelId} tx=${tx.txid} (funding tx not found in utxo)")
// there may be a record if we have just restarted
db.removeChannel(c.shortChannelId)
None
case IndividualResult(c, None, _) =>
// TODO: blacklist?
log.warning(s"could not retrieve tx for shortChannelId=${c.shortChannelId}")
None
}.flatten
// we reprocess node and channel-update announcements that may have been validated
val (resend, stash1) = d.stash.partition {
case n: NodeAnnouncement => results.exists(r => isRelatedTo(r.c, n.nodeId))
case u: ChannelUpdate => results.exists(r => r.c.shortChannelId == u.shortChannelId)
case _ => false
}
resend.foreach(self ! _)
goto(NORMAL) using d.copy(channels = d.channels ++ validated.map(c => (c.shortChannelId -> c)), rebroadcast = d.rebroadcast ++ validated, stash = stash1, awaiting = Nil)
}
whenUnhandled {
case Event(ChannelStateChanged(_, _, _, _, channel.NORMAL, d: DATA_NORMAL), d1) =>
stay using d1.copy(localChannels = d1.localChannels + (d.commitments.channelId -> d.commitments.remoteParams.nodeId))
case Event(ChannelStateChanged(_, _, _, channel.NORMAL, _, d: DATA_NEGOTIATING), d1) =>
stay using d1.copy(localChannels = d1.localChannels - d.commitments.channelId)
case Event(_: ChannelStateChanged, _) => stay
case Event(SendRoutingState(remote), Data(nodes, channels, updates, _, _, _, _, _, _)) =>
log.debug(s"info sending all announcements to $remote: channels=${channels.size} nodes=${nodes.size} updates=${updates.size}")
// we group and add delays to leave room for channel messages
context.actorOf(ThrottleForwarder.props(remote, channels.values ++ nodes.values ++ updates.values, 100, 100 millis))
stay
case Event(c: ChannelAnnouncement, d) =>
log.debug(s"received channel announcement for shortChannelId=${c.shortChannelId} nodeId1=${c.nodeId1} nodeId2=${c.nodeId2}")
if (d.channels.containsKey(c.shortChannelId) || d.awaiting.exists(_.shortChannelId == c.shortChannelId) || d.stash.contains(c)) {
log.debug(s"ignoring $c (duplicate)")
stay
} else if (!Announcements.checkSigs(c)) {
log.error(s"bad signature for announcement $c")
sender ! Error(Peer.CHANNELID_ZERO, "bad announcement sig!!!".getBytes())
stay
} else {
log.debug(s"stashing $c")
stay using d.copy(stash = d.stash :+ c, origins = d.origins + (c -> sender))
}
case Event(n: NodeAnnouncement, d: Data) =>
if (d.nodes.containsKey(n.nodeId) && d.nodes(n.nodeId).timestamp >= n.timestamp) {
log.debug(s"ignoring announcement $n (old timestamp or duplicate)")
stay
} else if (!Announcements.checkSig(n)) {
log.error(s"bad signature for announcement $n")
sender ! Error(Peer.CHANNELID_ZERO, "bad announcement sig!!!".getBytes())
stay
} else if (d.nodes.containsKey(n.nodeId)) {
log.debug(s"updated node nodeId=${n.nodeId}")
context.system.eventStream.publish(NodeUpdated(n))
db.updateNode(n)
stay using d.copy(nodes = d.nodes + (n.nodeId -> n), rebroadcast = d.rebroadcast :+ n, origins = d.origins + (n -> sender))
} else if (d.channels.values.exists(c => isRelatedTo(c, n.nodeId))) {
log.debug(s"added node nodeId=${n.nodeId}")
context.system.eventStream.publish(NodeDiscovered(n))
db.addNode(n)
stay using d.copy(nodes = d.nodes + (n.nodeId -> n), rebroadcast = d.rebroadcast :+ n, origins = d.origins + (n -> sender))
} else if (d.awaiting.exists(c => isRelatedTo(c, n.nodeId)) || d.stash.collectFirst { case c: ChannelAnnouncement if isRelatedTo(c, n.nodeId) => c }.isDefined) {
log.debug(s"stashing $n")
stay using d.copy(stash = d.stash :+ n, origins = d.origins + (n -> sender))
} else {
log.warning(s"ignoring $n (no related channel found)")
// there may be a record if we have just restarted
db.removeNode(n.nodeId)
stay
}
case Event(u: ChannelUpdate, d: Data) =>
if (d.channels.contains(u.shortChannelId)) {
val c = d.channels(u.shortChannelId)
val desc = getDesc(u, c)
if (d.updates.contains(desc) && d.updates(desc).timestamp >= u.timestamp) {
log.debug(s"ignoring $u (old timestamp or duplicate)")
stay
} else if (!Announcements.checkSig(u, getDesc(u, d.channels(u.shortChannelId)).a)) {
// TODO: (dirty) this will make the origin channel close the connection
log.error(s"bad signature for announcement $u")
sender ! Error(Peer.CHANNELID_ZERO, "bad announcement sig!!!".getBytes())
stay
} else if (d.updates.contains(desc)) {
log.debug(s"updated $u")
context.system.eventStream.publish(ChannelUpdateReceived(u))
db.updateChannelUpdate(u)
stay using d.copy(updates = d.updates + (desc -> u), rebroadcast = d.rebroadcast :+ u, origins = d.origins + (u -> sender))
} else {
log.debug(s"added $u")
context.system.eventStream.publish(ChannelUpdateReceived(u))
db.addChannelUpdate(u)
stay using d.copy(updates = d.updates + (desc -> u), rebroadcast = d.rebroadcast :+ u, origins = d.origins + (u -> sender))
}
} else if (d.awaiting.exists(c => c.shortChannelId == u.shortChannelId) || d.stash.collectFirst { case c: ChannelAnnouncement if c.shortChannelId == u.shortChannelId => c }.isDefined) {
log.debug(s"stashing $u")
stay using d.copy(stash = d.stash :+ u, origins = d.origins + (u -> sender))
} else {
log.warning(s"ignoring announcement $u (unknown channel)")
stay
}
case Event(WatchEventSpentBasic(BITCOIN_FUNDING_EXTERNAL_CHANNEL_SPENT(shortChannelId)), d)
if d.channels.containsKey(shortChannelId) =>
val lostChannel = d.channels(shortChannelId)
log.info(s"funding tx of channelId=$shortChannelId has been spent")
// we need to remove nodes that aren't tied to any channels anymore
val channels1 = d.channels - lostChannel.shortChannelId
val lostNodes = Seq(lostChannel.nodeId1, lostChannel.nodeId2).filterNot(nodeId => hasChannels(nodeId, channels1.values))
// let's clean the db and send the events
log.info(s"pruning shortChannelId=$shortChannelId (spent)")
db.removeChannel(shortChannelId) // NB: this also removes channel updates
context.system.eventStream.publish(ChannelLost(shortChannelId))
lostNodes.foreach {
case nodeId =>
log.info(s"pruning nodeId=$nodeId (spent)")
db.removeNode(nodeId)
context.system.eventStream.publish(NodeLost(nodeId))
}
stay using d.copy(nodes = d.nodes -- lostNodes, channels = d.channels - shortChannelId, updates = d.updates.filterKeys(_.id != shortChannelId))
case Event(TickValidate, d) => stay // ignored
case Event(TickBroadcast, d) =>
d.rebroadcast match {
case Nil => stay using d.copy(origins = Map.empty)
case _ =>
log.info(s"broadcasting ${d.rebroadcast.size} routing messages")
context.actorSelection(context.system / "*" / "switchboard") ! Rebroadcast(d.rebroadcast, d.origins)
stay using d.copy(rebroadcast = Nil, origins = Map.empty)
}
case Event(TickPruneStaleChannels, d) =>
// first we select channels that we will prune
val staleChannels = getStaleChannels(d.channels, d.updates)
// then we clean up the related channel updates
val staleUpdates = d.updates.keys.filter(desc => staleChannels.contains(desc.id))
// finally we remove nodes that aren't tied to any channels anymore
val channels1 = d.channels -- staleChannels
val staleNodes = d.nodes.keys.filterNot(nodeId => hasChannels(nodeId, channels1.values))
// let's clean the db and send the events
staleChannels.foreach {
case shortChannelId =>
log.info(s"pruning shortChannelId=$shortChannelId (stale)")
db.removeChannel(shortChannelId) // NB: this also removes channel updates
context.system.eventStream.publish(ChannelLost(shortChannelId))
}
staleNodes.foreach {
case nodeId =>
log.info(s"pruning nodeId=$nodeId (stale)")
db.removeNode(nodeId)
context.system.eventStream.publish(NodeLost(nodeId))
}
stay using d.copy(nodes = d.nodes -- staleNodes, channels = channels1, updates = d.updates -- staleUpdates)
case Event(ExcludeChannel(desc@ChannelDesc(shortChannelId, nodeId, _)), d) =>
val banDuration = nodeParams.channelExcludeDuration
log.info(s"excluding shortChannelId=$shortChannelId from nodeId=$nodeId for duration=$banDuration")
context.system.scheduler.scheduleOnce(banDuration, self, LiftChannelExclusion(desc))
stay using d.copy(excludedChannels = d.excludedChannels + desc)
case Event(LiftChannelExclusion(desc@ChannelDesc(shortChannelId, nodeId, _)), d) =>
log.info(s"reinstating shortChannelId=$shortChannelId from nodeId=$nodeId")
stay using d.copy(excludedChannels = d.excludedChannels - desc)
case Event('nodes, d) =>
sender ! d.nodes.values
stay
case Event('channels, d) =>
sender ! d.channels.values
stay
case Event('updates, d) =>
sender ! d.updates.values
stay
case Event('dot, d) =>
graph2dot(d.nodes, d.channels) pipeTo sender
stay
case Event(RouteRequest(start, end, ignoreNodes, ignoreChannels), d) =>
val localNodeId = nodeParams.privateKey.publicKey
// TODO: HACK!!!!! the following is a workaround to make our routing work with private/not-yet-announced channels, that do not have a channelUpdate
val fakeUpdates = d.localChannels.map { case (channelId, remoteNodeId) =>
// note that this id is deterministic, otherwise filterUpdates would not work
val fakeShortId = BigInt(channelId.take(7).toArray).toLong
val channelDesc = ChannelDesc(fakeShortId, localNodeId, remoteNodeId)
// note that we store the channelId in the sig, other values are not used because if it is selected this will be the first channel in the route
val channelUpdate = ChannelUpdate(signature = channelId, chainHash = nodeParams.chainHash, fakeShortId, 0, "0000", 0, 0, 0, 0)
(channelDesc -> channelUpdate)
}
// we replace local channelUpdates (we have them for regular public already-announced channels) by the ones we just generated
val updates1 = d.updates.filterKeys(_.a != localNodeId) ++ fakeUpdates
// we then filter out the currently excluded channels
val updates2 = updates1.filterKeys(!d.excludedChannels.contains(_))
// we also filter out excluded channels
val updates3 = filterUpdates(updates2, ignoreNodes, ignoreChannels)
log.info(s"finding a route $start->$end with ignoreNodes=${ignoreNodes.map(_.toBin).mkString(",")} ignoreChannels=${ignoreChannels.map(_.toHexString).mkString(",")}")
findRoute(start, end, updates3).map(r => RouteResponse(r, ignoreNodes, ignoreChannels)) pipeTo sender
stay
}
onTransition {
case _ -> NORMAL => log.info(s"current status channels=${nextStateData.channels.size} nodes=${nextStateData.nodes.size} updates=${nextStateData.updates.size}")
}
initialize()
}
object Router {
val MAX_PARALLEL_JSONRPC_REQUESTS = 50
def props(nodeParams: NodeParams, watcher: ActorRef) = Props(new Router(nodeParams, watcher))
def getDesc(u: ChannelUpdate, channel: ChannelAnnouncement): ChannelDesc = {
require(u.flags.data.size == 2, s"invalid flags length ${u.flags.data.size} != 2")
// the least significant bit tells us if it is node1 or node2
if (Announcements.isNode1(u.flags)) ChannelDesc(u.shortChannelId, channel.nodeId1, channel.nodeId2) else ChannelDesc(u.shortChannelId, channel.nodeId2, channel.nodeId1)
}
def isRelatedTo(c: ChannelAnnouncement, nodeId: PublicKey) = nodeId == c.nodeId1 || nodeId == c.nodeId2
def hasChannels(nodeId: PublicKey, channels: Iterable[ChannelAnnouncement]): Boolean = channels.exists(c => isRelatedTo(c, nodeId))
def getStaleChannels(channels: Map[Long, ChannelAnnouncement], updates: Map[ChannelDesc, ChannelUpdate]): Iterable[Long] = {
// BOLT 7: "nodes MAY prune channels should the timestamp of the latest channel_update be older than 2 weeks (1209600 seconds)"
// but we don't want to prune brand new channels for which we didn't yet receive a channel update
// so we consider stale a channel that:
// (1) is older than 2 weeks (2*7*144 = 2016 blocks)
// AND
// (2) didn't have an update during the last 2 weeks
val staleThresholdSeconds = Platform.currentTime / 1000 - 1209600
val staleThresholdBlocks = Globals.blockCount.get() - 2016
val staleChannels = channels
.filterKeys(shortChannelId => fromShortId(shortChannelId)._1 < staleThresholdBlocks) // consider only channels older than 2 weeks
.filterKeys(shortChannelId => !updates.values.exists(u => u.shortChannelId == shortChannelId && u.timestamp >= staleThresholdSeconds)) // no update in the past 2 weeks
staleChannels.keys
}
/**
* This method is used after a payment failed, and we want to exclude some nodes/channels that we know are failing
*/
def filterUpdates(updates: Map[ChannelDesc, ChannelUpdate], ignoreNodes: Set[PublicKey], ignoreChannels: Set[Long]) =
updates
.filterNot(u => ignoreNodes.map(_.toBin).contains(u._1.a) || ignoreNodes.map(_.toBin).contains(u._1.b))
.filterNot(u => ignoreChannels.contains(u._1.id))
.filterNot(u => !Announcements.isEnabled(u._2.flags))
def findRouteDijkstra(localNodeId: PublicKey, targetNodeId: PublicKey, channels: Iterable[ChannelDesc]): Seq[ChannelDesc] = {
if (localNodeId == targetNodeId) throw CannotRouteToSelf
case class DescEdge(desc: ChannelDesc) extends DefaultEdge
val g = new DefaultDirectedGraph[PublicKey, DescEdge](classOf[DescEdge])
Random.shuffle(channels).foreach(d => {
g.addVertex(d.a)
g.addVertex(d.b)
g.addEdge(d.a, d.b, new DescEdge(d))
})
Try(Option(DijkstraShortestPath.findPathBetween(g, localNodeId, targetNodeId))) match {
case Success(Some(path)) => path.getEdgeList.map(_.desc)
case _ => throw RouteNotFound
}
}
def findRoute(localNodeId: PublicKey, targetNodeId: PublicKey, updates: Map[ChannelDesc, ChannelUpdate])(implicit ec: ExecutionContext): Future[Seq[Hop]] = Future {
findRouteDijkstra(localNodeId, targetNodeId, updates.keys)
.map(desc => Hop(desc.a, desc.b, updates(desc)))
}
def graph2dot(nodes: Map[PublicKey, NodeAnnouncement], channels: Map[Long, ChannelAnnouncement])(implicit ec: ExecutionContext): Future[String] = Future {
case class DescEdge(channelId: Long) extends DefaultEdge
val g = new SimpleGraph[PublicKey, DescEdge](classOf[DescEdge])
channels.foreach(d => {
g.addVertex(d._2.nodeId1)
g.addVertex(d._2.nodeId2)
g.addEdge(d._2.nodeId1, d._2.nodeId2, new DescEdge(d._1))
})
val vertexIDProvider = new ComponentNameProvider[PublicKey]() {
override def getName(nodeId: PublicKey): String = "\"" + nodeId.toString() + "\""
}
val edgeLabelProvider = new ComponentNameProvider[DescEdge]() {
override def getName(e: DescEdge): String = e.channelId.toString
}
val vertexAttributeProvider = new ComponentAttributeProvider[PublicKey]() {
override def getComponentAttributes(nodeId: PublicKey): java.util.Map[String, String] =
nodes.get(nodeId) match {
case Some(ann) => Map("label" -> ann.alias, "color" -> f"#${ann.rgbColor._1}%02x${ann.rgbColor._2}%02x${ann.rgbColor._3}%02x")
case None => Map.empty[String, String]
}
}
val exporter = new DOTExporter[PublicKey, DescEdge](vertexIDProvider, null, edgeLabelProvider, vertexAttributeProvider, null)
val writer = new StringWriter()
try {
exporter.exportGraph(g, writer)
writer.toString
} finally {
writer.close()
}
}
}

View File

@ -0,0 +1,11 @@
package fr.acinq.eclair.router
/**
* Created by PM on 12/04/2017.
*/
class RouterException(message: String) extends RuntimeException(message)
object RouteNotFound extends RouterException("Route not found")
object CannotRouteToSelf extends RouterException("Cannot route to self")

View File

@ -0,0 +1,47 @@
package fr.acinq.eclair.router
import akka.actor.{Actor, ActorLogging, ActorRef, Props}
import scala.concurrent.duration.{FiniteDuration, _}
/**
* This actor forwards messages to another actor, but groups them and introduces
* delays between each groups.
*
* If A wants to send a lot of lower importance messages to B, it is useful to let
* higher importance messages go in the stream.
*/
class ThrottleForwarder(target: ActorRef, messages: Iterable[Any], chunkSize: Int, delay: FiniteDuration) extends Actor with ActorLogging {
import ThrottleForwarder.Tick
import scala.concurrent.ExecutionContext.Implicits.global
val clock = context.system.scheduler.schedule(0 second, delay, self, Tick)
log.debug(s"sending messages=${messages.size} with chunkSize=$chunkSize and delay=$delay")
override def receive = group(messages)
def group(messages: Iterable[Any]): Receive = {
case Tick =>
messages.splitAt(chunkSize) match {
case (Nil, _) =>
clock.cancel()
log.debug(s"sent messages=${messages.size} with chunkSize=$chunkSize and delay=$delay")
context stop self
case (chunk, rest) =>
chunk.foreach(target ! _)
context become group(rest)
}
}
}
object ThrottleForwarder {
def props(target: ActorRef, messages: Iterable[Any], groupSize: Int, delay: FiniteDuration) = Props(new ThrottleForwarder(target, messages, groupSize, delay))
case object Tick
}

View File

@ -0,0 +1,81 @@
package fr.acinq.eclair.transactions
import fr.acinq.eclair.wire._
/**
* Created by PM on 07/12/2016.
*/
// @formatter:off
sealed trait Direction { def opposite: Direction }
case object IN extends Direction { def opposite = OUT }
case object OUT extends Direction { def opposite = IN }
// @formatter:on
case class DirectedHtlc(direction: Direction, add: UpdateAddHtlc)
final case class CommitmentSpec(htlcs: Set[DirectedHtlc], feeratePerKw: Long, toLocalMsat: Long, toRemoteMsat: Long) {
val totalFunds = toLocalMsat + toRemoteMsat + htlcs.toSeq.map(_.add.amountMsat).sum
}
object CommitmentSpec {
def removeHtlc(changes: List[UpdateMessage], id: Long): List[UpdateMessage] = changes.filterNot(_ match {
case u: UpdateAddHtlc if u.id == id => true
case _ => false
})
def addHtlc(spec: CommitmentSpec, direction: Direction, update: UpdateAddHtlc): CommitmentSpec = {
val htlc = DirectedHtlc(direction, update)
direction match {
case OUT => spec.copy(toLocalMsat = spec.toLocalMsat - htlc.add.amountMsat, htlcs = spec.htlcs + htlc)
case IN => spec.copy(toRemoteMsat = spec.toRemoteMsat - htlc.add.amountMsat, htlcs = spec.htlcs + htlc)
}
}
// OUT means we are sending an UpdateFulfillHtlc message which means that we are fulfilling an HTLC that they sent
def fulfillHtlc(spec: CommitmentSpec, direction: Direction, htlcId: Long): CommitmentSpec = {
spec.htlcs.find(htlc => htlc.direction != direction && htlc.add.id == htlcId) match {
case Some(htlc) if direction == OUT => spec.copy(toLocalMsat = spec.toLocalMsat + htlc.add.amountMsat, htlcs = spec.htlcs - htlc)
case Some(htlc) if direction == IN => spec.copy(toRemoteMsat = spec.toRemoteMsat + htlc.add.amountMsat, htlcs = spec.htlcs - htlc)
case None => throw new RuntimeException(s"cannot find htlc id=${htlcId}")
}
}
// OUT means we are sending an UpdateFailHtlc message which means that we are failing an HTLC that they sent
def failHtlc(spec: CommitmentSpec, direction: Direction, htlcId: Long): CommitmentSpec = {
spec.htlcs.find(htlc => htlc.direction != direction && htlc.add.id == htlcId) match {
case Some(htlc) if direction == OUT => spec.copy(toRemoteMsat = spec.toRemoteMsat + htlc.add.amountMsat, htlcs = spec.htlcs - htlc)
case Some(htlc) if direction == IN => spec.copy(toLocalMsat = spec.toLocalMsat + htlc.add.amountMsat, htlcs = spec.htlcs - htlc)
case None => throw new RuntimeException(s"cannot find htlc id=${htlcId}")
}
}
def reduce(localCommitSpec: CommitmentSpec, localChanges: List[UpdateMessage], remoteChanges: List[UpdateMessage]): CommitmentSpec = {
val spec1 = localChanges.foldLeft(localCommitSpec) {
case (spec, u: UpdateAddHtlc) => addHtlc(spec, OUT, u)
case (spec, _) => spec
}
val spec2 = remoteChanges.foldLeft(spec1) {
case (spec, u: UpdateAddHtlc) => addHtlc(spec, IN, u)
case (spec, _) => spec
}
val spec3 = localChanges.foldLeft(spec2) {
case (spec, u: UpdateFulfillHtlc) => fulfillHtlc(spec, OUT, u.id)
case (spec, u: UpdateFailHtlc) => failHtlc(spec, OUT, u.id)
case (spec, u: UpdateFailMalformedHtlc) => failHtlc(spec, OUT, u.id)
case (spec, _) => spec
}
val spec4 = remoteChanges.foldLeft(spec3) {
case (spec, u: UpdateFulfillHtlc) => fulfillHtlc(spec, IN, u.id)
case (spec, u: UpdateFailHtlc) => failHtlc(spec, IN, u.id)
case (spec, u: UpdateFailMalformedHtlc) => failHtlc(spec, IN, u.id)
case (spec, _) => spec
}
val spec5 = (localChanges ++ remoteChanges).foldLeft(spec4) {
case (spec, u: UpdateFee) => spec.copy(feeratePerKw = u.feeratePerKw)
case (spec, _) => spec
}
spec5
}
}

View File

@ -0,0 +1,250 @@
package fr.acinq.eclair.transactions
import fr.acinq.bitcoin.Crypto.{PublicKey, ripemd160}
import fr.acinq.bitcoin.Script._
import fr.acinq.bitcoin.{BinaryData, LexicographicalOrdering, LockTimeThreshold, OP_0, OP_1, OP_1NEGATE, OP_2, OP_2DROP, OP_ADD, OP_CHECKLOCKTIMEVERIFY, OP_CHECKMULTISIG, OP_CHECKSEQUENCEVERIFY, OP_CHECKSIG, OP_DROP, OP_DUP, OP_ELSE, OP_ENDIF, OP_EQUAL, OP_EQUALVERIFY, OP_HASH160, OP_IF, OP_NOTIF, OP_PUSHDATA, OP_SIZE, OP_SWAP, Satoshi, Script, ScriptElt, ScriptWitness, Transaction, TxIn}
/**
* Created by PM on 02/12/2016.
*/
object Scripts {
def toSelfDelay2csv(in: Int): Long = ???
/*in match {
case locktime(Blocks(blocks)) => blocks
case locktime(Seconds(seconds)) => TxIn.SEQUENCE_LOCKTIME_TYPE_FLAG | (seconds >> TxIn.SEQUENCE_LOCKTIME_GRANULARITY)
}*/
def expiry2cltv(in: Long): Long = ???
/*in match {
case locktime(Blocks(blocks)) => blocks
case locktime(Seconds(seconds)) => seconds
}*/
def multiSig2of2(pubkey1: PublicKey, pubkey2: PublicKey): Seq[ScriptElt] = if (LexicographicalOrdering.isLessThan(pubkey1.toBin, pubkey2.toBin))
Script.createMultiSigMofN(2, Seq(pubkey1, pubkey2))
else
Script.createMultiSigMofN(2, Seq(pubkey2, pubkey1))
/**
*
* @param sig1
* @param sig2
* @param pubkey1
* @param pubkey2
* @return a script witness that matches the msig 2-of-2 pubkey script for pubkey1 and pubkey2
*/
def witness2of2(sig1: BinaryData, sig2: BinaryData, pubkey1: PublicKey, pubkey2: PublicKey): ScriptWitness = {
if (LexicographicalOrdering.isLessThan(pubkey1.toBin, pubkey2.toBin))
ScriptWitness(Seq(BinaryData.empty, sig1, sig2, write(multiSig2of2(pubkey1, pubkey2))))
else
ScriptWitness(Seq(BinaryData.empty, sig2, sig1, write(multiSig2of2(pubkey1, pubkey2))))
}
/**
* minimal encoding of a number into a script element:
* - OP_0 to OP_16 if 0 <= n <= 16
* - OP_PUSHDATA(encodeNumber(n)) otherwise
*
* @param n input number
* @return a script element that represents n
*/
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)
case _ => OP_PUSHDATA(Script.encodeNumber(n))
}
def redeemSecretOrDelay(delayedKey: BinaryData, reltimeout: Long, keyIfSecretKnown: BinaryData, hashOfSecret: BinaryData): Seq[ScriptElt] = {
// @formatter:off
OP_HASH160 :: OP_PUSHDATA(ripemd160(hashOfSecret)) :: OP_EQUAL ::
OP_IF ::
OP_PUSHDATA(keyIfSecretKnown) ::
OP_ELSE ::
encodeNumber(reltimeout):: OP_CHECKSEQUENCEVERIFY :: OP_DROP :: OP_PUSHDATA(delayedKey) ::
OP_ENDIF ::
OP_CHECKSIG :: Nil
// @formatter:on
}
def scriptPubKeyHtlcSend(ourkey: BinaryData, theirkey: BinaryData, abstimeout: Long, reltimeout: Long, rhash: BinaryData, commit_revoke: BinaryData): Seq[ScriptElt] = {
// values lesser than 16 should be encoded using OP_0..OP_16 instead of OP_PUSHDATA
require(abstimeout > 16, s"abstimeout=$abstimeout must be greater than 16")
// @formatter:off
OP_SIZE :: encodeNumber(32) :: OP_EQUALVERIFY ::
OP_HASH160 :: OP_DUP ::
OP_PUSHDATA(ripemd160(rhash)) :: OP_EQUAL ::
OP_SWAP :: OP_PUSHDATA(ripemd160(commit_revoke)) :: OP_EQUAL :: OP_ADD ::
OP_IF ::
OP_PUSHDATA(theirkey) ::
OP_ELSE ::
encodeNumber(abstimeout) :: OP_CHECKLOCKTIMEVERIFY :: encodeNumber(reltimeout) :: OP_CHECKSEQUENCEVERIFY :: OP_2DROP :: OP_PUSHDATA(ourkey) ::
OP_ENDIF ::
OP_CHECKSIG :: Nil
// @formatter:on
}
def scriptPubKeyHtlcReceive(ourkey: BinaryData, theirkey: BinaryData, abstimeout: Long, reltimeout: Long, rhash: BinaryData, commit_revoke: BinaryData): Seq[ScriptElt] = {
// values lesser than 16 should be encoded using OP_0..OP_16 instead of OP_PUSHDATA
require(abstimeout > 16, s"abstimeout=$abstimeout must be greater than 16")
// @formatter:off
OP_SIZE :: encodeNumber(32) :: OP_EQUALVERIFY ::
OP_HASH160 :: OP_DUP ::
OP_PUSHDATA(ripemd160(rhash)) :: OP_EQUAL ::
OP_IF ::
encodeNumber(reltimeout) :: OP_CHECKSEQUENCEVERIFY :: OP_2DROP :: OP_PUSHDATA(ourkey) ::
OP_ELSE ::
OP_PUSHDATA(ripemd160(commit_revoke)) :: OP_EQUAL ::
OP_NOTIF ::
encodeNumber(abstimeout) :: OP_CHECKLOCKTIMEVERIFY :: OP_DROP ::
OP_ENDIF ::
OP_PUSHDATA(theirkey) ::
OP_ENDIF ::
OP_CHECKSIG :: Nil
// @formatter:on
}
def applyFees(amount_us: Satoshi, amount_them: Satoshi, fee: Satoshi) = {
val (amount_us1: Satoshi, amount_them1: Satoshi) = (amount_us, amount_them) match {
case (Satoshi(us), Satoshi(them)) if us >= fee.toLong / 2 && them >= fee.toLong / 2 => (Satoshi(us - fee.toLong / 2), Satoshi(them - fee.toLong / 2))
case (Satoshi(us), Satoshi(them)) if us < fee.toLong / 2 => (Satoshi(0L), Satoshi(Math.max(0L, them - fee.toLong + us)))
case (Satoshi(us), Satoshi(them)) if them < fee.toLong / 2 => (Satoshi(Math.max(us - fee.toLong + them, 0L)), Satoshi(0L))
}
(amount_us1, amount_them1)
}
/**
* 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.
* This function does not support the case when the locktime is a number of seconds that is not way in the past.
* NB: We use this property in lightning to store data in this field.
*
* @return the block height before which this tx cannot be published.
*/
def cltvTimeout(tx: Transaction): Long = {
if (tx.lockTime <= LockTimeThreshold) {
// locktime is a number of blocks
tx.lockTime
}
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
0
}
}
/**
*
* @param tx
* @return the number of confirmations of the tx parent before which it can be published
*/
def csvTimeout(tx: Transaction): Long = {
def sequenceToBlockHeight(sequence: Long): Long = {
if ((sequence & TxIn.SEQUENCE_LOCKTIME_DISABLE_FLAG) != 0) 0
else {
require((sequence & TxIn.SEQUENCE_LOCKTIME_TYPE_FLAG) == 0, "CSV timeout must use block heights, not block times")
sequence & TxIn.SEQUENCE_LOCKTIME_MASK
}
}
if (tx.version < 2) 0
else tx.txIn.map(_.sequence).map(sequenceToBlockHeight).max
}
def toLocalDelayed(revocationPubkey: PublicKey, toSelfDelay: Int, localDelayedPaymentPubkey: PublicKey) = {
// @formatter:off
OP_IF ::
OP_PUSHDATA(revocationPubkey) ::
OP_ELSE ::
encodeNumber(toSelfDelay) :: OP_CHECKSEQUENCEVERIFY :: OP_DROP ::
OP_PUSHDATA(localDelayedPaymentPubkey) ::
OP_ENDIF ::
OP_CHECKSIG :: Nil
// @formatter:on
}
/**
* This witness script spends a [[toLocalDelayed]] output using a local sig after a delay
*/
def witnessToLocalDelayedAfterDelay(localSig: BinaryData, toLocalDelayedScript: BinaryData) =
ScriptWitness(localSig :: BinaryData.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: BinaryData, toLocalScript: BinaryData) =
ScriptWitness(revocationSig :: BinaryData("01") :: toLocalScript :: Nil)
def htlcOffered(localHtlcPubkey: PublicKey, remoteHtlcPubkey: PublicKey, revocationPubKey: PublicKey, paymentHash: BinaryData): Seq[ScriptElt] = {
// @formatter:off
// To you with revocation key
OP_DUP :: OP_HASH160 :: OP_PUSHDATA(revocationPubKey.hash160) :: OP_EQUAL ::
OP_IF ::
OP_CHECKSIG ::
OP_ELSE ::
OP_PUSHDATA(remoteHtlcPubkey) :: 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_ELSE ::
OP_HASH160 :: OP_PUSHDATA(paymentHash) :: OP_EQUALVERIFY ::
OP_CHECKSIG ::
OP_ENDIF ::
OP_ENDIF :: Nil
// @formatter:on
}
/**
* This is the witness script of the 2nd-stage HTLC Success transaction (consumes htlcOffered script from commit tx)
*/
def witnessHtlcSuccess(localSig: BinaryData, remoteSig: BinaryData, paymentPreimage: BinaryData, htlcOfferedScript: BinaryData) =
ScriptWitness(BinaryData.empty :: remoteSig :: localSig :: paymentPreimage :: htlcOfferedScript :: Nil)
/**
* If local publishes its commit tx where there was a local->remote htlc, then remote uses this script to
* claim its funds using a payment preimage (consumes htlcOffered script from commit tx)
*/
def witnessClaimHtlcSuccessFromCommitTx(localSig: BinaryData, paymentPreimage: BinaryData, htlcOfferedScript: BinaryData) =
ScriptWitness(localSig :: paymentPreimage :: htlcOfferedScript :: Nil)
def htlcReceived(localHtlcPubkey: PublicKey, remoteHtlcPubkey: PublicKey, revocationPubKey: PublicKey, paymentHash: BinaryData, lockTime: Long) = {
// @formatter:off
// To you with revocation key
OP_DUP :: OP_HASH160 :: OP_PUSHDATA(revocationPubKey.hash160) :: OP_EQUAL ::
OP_IF ::
OP_CHECKSIG ::
OP_ELSE ::
OP_PUSHDATA(remoteHtlcPubkey) :: 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_ELSE ::
// To you after timeout.
OP_DROP :: encodeNumber(lockTime) :: OP_CHECKLOCKTIMEVERIFY :: OP_DROP ::
OP_CHECKSIG ::
OP_ENDIF ::
OP_ENDIF :: Nil
// @formatter:on
}
/**
* This is the witness script of the 2nd-stage HTLC Timeout transaction (consumes htlcReceived script from commit tx)
*/
def witnessHtlcTimeout(localSig: BinaryData, remoteSig: BinaryData, htlcReceivedScript: BinaryData) =
ScriptWitness(BinaryData.empty :: remoteSig :: localSig :: BinaryData.empty :: htlcReceivedScript :: Nil)
/**
* If local publishes its commit tx where there was a remote->local htlc, then remote uses this script to
* claim its funds after timeout (consumes htlcReceived script from commit tx)
*/
def witnessClaimHtlcTimeoutFromCommitTx(localSig: BinaryData, htlcReceivedScript: BinaryData) =
ScriptWitness(localSig :: BinaryData.empty :: htlcReceivedScript :: Nil)
}

View File

@ -0,0 +1,391 @@
package fr.acinq.eclair.transactions
import java.nio.ByteOrder
import fr.acinq.bitcoin.Crypto.{Point, PrivateKey, PublicKey, ripemd160}
import fr.acinq.bitcoin.Script._
import fr.acinq.bitcoin.SigVersion._
import fr.acinq.bitcoin.{BinaryData, Crypto, LexicographicalOrdering, MilliSatoshi, OutPoint, Protocol, SIGHASH_ALL, Satoshi, Script, ScriptElt, ScriptFlags, ScriptWitness, Transaction, TxIn, TxOut, millisatoshi2satoshi}
import fr.acinq.eclair.transactions.Scripts._
import fr.acinq.eclair.wire.UpdateAddHtlc
import scala.util.Try
/**
* Created by PM on 15/12/2016.
*/
object Transactions {
// @formatter:off
case class InputInfo(outPoint: OutPoint, txOut: TxOut, redeemScript: BinaryData)
object InputInfo {
def apply(outPoint: OutPoint, txOut: TxOut, redeemScript: Seq[ScriptElt]) = new InputInfo(outPoint, txOut, Script.write(redeemScript))
}
sealed trait TransactionWithInputInfo {
def input: InputInfo
def tx: Transaction
}
case class CommitTx(input: InputInfo, tx: Transaction) extends TransactionWithInputInfo
case class HtlcSuccessTx(input: InputInfo, tx: Transaction, paymentHash: BinaryData) extends TransactionWithInputInfo
case class HtlcTimeoutTx(input: InputInfo, tx: Transaction) extends TransactionWithInputInfo
case class ClaimHtlcSuccessTx(input: InputInfo, tx: Transaction) extends TransactionWithInputInfo
case class ClaimHtlcTimeoutTx(input: InputInfo, tx: Transaction) extends TransactionWithInputInfo
case class ClaimP2WPKHOutputTx(input: InputInfo, tx: Transaction) extends TransactionWithInputInfo
case class ClaimDelayedOutputTx(input: InputInfo, tx: Transaction) extends TransactionWithInputInfo
case class MainPenaltyTx(input: InputInfo, tx: Transaction) extends TransactionWithInputInfo
case class HtlcPenaltyTx(input: InputInfo, tx: Transaction) extends TransactionWithInputInfo
case class ClosingTx(input: InputInfo, tx: Transaction) extends TransactionWithInputInfo
// @formatter:on
/**
* When *local* *current* [[CommitTx]] is published:
* - [[ClaimDelayedOutputTx]] spends to-local output of [[CommitTx]] after a delay
* - [[HtlcSuccessTx]] spends htlc-received outputs of [[CommitTx]] for which we have the preimage
* - [[ClaimDelayedOutputTx]] spends [[HtlcSuccessTx]] after a delay
* - [[HtlcTimeoutTx]] spends htlc-sent outputs of [[CommitTx]] after a timeout
* - [[ClaimDelayedOutputTx]] spends [[HtlcTimeoutTx]] after a delay
*
* When *remote* *current* [[CommitTx]] is published:
* - [[ClaimP2WPKHOutputTx]] spends to-local output 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:
* - [[ClaimP2WPKHOutputTx]] spends to-local output 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)
* - [[HtlcPenaltyTx]] spends [[HtlcSuccessTx]] using the per-commitment secret
* - [[ClaimHtlcTimeoutTx]] spends htlc-sent outputs of [[CommitTx]] after a timeout
* - [[HtlcTimeoutTx]] spends htlc-received outputs of [[CommitTx]] after a timeout (published by local or remote)
* - [[HtlcPenaltyTx]] spends [[HtlcTimeoutTx]] using the per-commitment secret
*/
val commitWeight = 724
val htlcTimeoutWeight = 663
val htlcSuccessWeight = 703
val claimP2WPKHOutputWeight = 437
val claimHtlcDelayedWeight = 482
val claimHtlcSuccessWeight = 570
val claimHtlcTimeoutWeight = 544
val mainPenaltyWeight = 483
def weight2fee(feeratePerKw: Long, weight: Int) = Satoshi((feeratePerKw * weight) / 1000)
def trimOfferedHtlcs(dustLimit: Satoshi, spec: CommitmentSpec): Seq[DirectedHtlc] = {
val htlcTimeoutFee = weight2fee(spec.feeratePerKw, htlcTimeoutWeight)
spec.htlcs
.filter(_.direction == OUT)
.filter(htlc => MilliSatoshi(htlc.add.amountMsat) >= (dustLimit + htlcTimeoutFee))
.toSeq
}
def trimReceivedHtlcs(dustLimit: Satoshi, spec: CommitmentSpec): Seq[DirectedHtlc] = {
val htlcSuccessFee = weight2fee(spec.feeratePerKw, htlcSuccessWeight)
spec.htlcs
.filter(_.direction == IN)
.filter(htlc => MilliSatoshi(htlc.add.amountMsat) >= (dustLimit + htlcSuccessFee))
.toSeq
}
def commitTxFee(dustLimit: Satoshi, spec: CommitmentSpec): Satoshi = {
val trimmedOfferedHtlcs = trimOfferedHtlcs(dustLimit, spec)
val trimmedReceivedHtlcs = trimReceivedHtlcs(dustLimit, spec)
val weight = commitWeight + 172 * (trimmedOfferedHtlcs.size + trimmedReceivedHtlcs.size)
weight2fee(spec.feeratePerKw, weight)
}
/**
*
* @param commitTxNumber commit tx number
* @param isFunder true if local node is funder
* @param localPaymentBasePoint local payment base point
* @param remotePaymentBasePoint remote payment base point
* @return the obscured tx number as defined in BOLT #3 (a 48 bits integer)
*/
def obscuredCommitTxNumber(commitTxNumber: Long, isFunder: Boolean, localPaymentBasePoint: Point, remotePaymentBasePoint: Point): Long = {
// from BOLT 3: SHA256(payment-basepoint from open_channel || payment-basepoint from accept_channel)
val h = if (isFunder)
Crypto.sha256(localPaymentBasePoint.toBin(true) ++ remotePaymentBasePoint.toBin(true))
else
Crypto.sha256(remotePaymentBasePoint.toBin(true) ++ localPaymentBasePoint.toBin(true))
val blind = Protocol.uint64(h.takeRight(6).reverse ++ BinaryData("0x0000"), ByteOrder.LITTLE_ENDIAN)
commitTxNumber ^ blind
}
/**
*
* @param commitTx commit tx
* @param isFunder true if local node is funder
* @param localPaymentBasePoint local payment base point
* @param remotePaymentBasePoint remote payment base point
* @return the actual commit tx number that was blinded and stored in locktime and sequence fields
*/
def getCommitTxNumber(commitTx: Transaction, isFunder: Boolean, localPaymentBasePoint: Point, remotePaymentBasePoint: Point): Long = {
val blind = obscuredCommitTxNumber(0, isFunder, localPaymentBasePoint, remotePaymentBasePoint)
val obscured = decodeTxNumber(commitTx.txIn(0).sequence, commitTx.lockTime)
obscured ^ blind
}
/**
* This is a trick to split and encode a 48-bit txnumber into the sequence and locktime fields of a tx
*
* @param txnumber
* @return (sequence, locktime)
*/
def encodeTxNumber(txnumber: Long) = {
require(txnumber <= 0xffffffffffffL, "txnumber must be lesser than 48 bits long")
(0x80000000L | (txnumber >> 24), (txnumber & 0xffffffL) | 0x20000000)
}
def decodeTxNumber(sequence: Long, locktime: Long) = ((sequence & 0xffffffL) << 24) + (locktime & 0xffffffL)
def makeCommitTx(commitTxInput: InputInfo, commitTxNumber: Long, localPaymentBasePoint: Point, remotePaymentBasePoint: Point, localIsFunder: Boolean, localDustLimit: Satoshi, localRevocationPubkey: PublicKey, toLocalDelay: Int, localDelayedPaymentPubkey: PublicKey, remotePaymentPubkey: PublicKey, localHtlcPubkey: PublicKey, remoteHtlcPubkey: PublicKey, spec: CommitmentSpec): CommitTx = {
val commitFee = commitTxFee(localDustLimit, spec)
val (toLocalAmount: Satoshi, toRemoteAmount: Satoshi) = localIsFunder match {
case true => (millisatoshi2satoshi(MilliSatoshi(spec.toLocalMsat)) - commitFee, millisatoshi2satoshi(MilliSatoshi(spec.toRemoteMsat)))
case false => (millisatoshi2satoshi(MilliSatoshi(spec.toLocalMsat)), millisatoshi2satoshi(MilliSatoshi(spec.toRemoteMsat)) - commitFee)
} // NB: we don't care if values are < 0, they will be trimmed if they are < dust limit anyway
val toLocalDelayedOutput_opt = if (toLocalAmount >= localDustLimit) Some(TxOut(toLocalAmount, pay2wsh(toLocalDelayed(localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey)))) else None
val toRemoteOutput_opt = if (toRemoteAmount >= localDustLimit) Some(TxOut(toRemoteAmount, pay2wpkh(remotePaymentPubkey))) else None
val htlcOfferedOutputs = trimOfferedHtlcs(localDustLimit, spec)
.map(htlc => TxOut(MilliSatoshi(htlc.add.amountMsat), pay2wsh(htlcOffered(localHtlcPubkey, remoteHtlcPubkey, localRevocationPubkey, ripemd160(htlc.add.paymentHash)))))
val htlcReceivedOutputs = trimReceivedHtlcs(localDustLimit, spec)
.map(htlc => TxOut(MilliSatoshi(htlc.add.amountMsat), pay2wsh(htlcReceived(localHtlcPubkey, remoteHtlcPubkey, localRevocationPubkey, ripemd160(htlc.add.paymentHash), htlc.add.expiry))))
val txnumber = obscuredCommitTxNumber(commitTxNumber, localIsFunder, localPaymentBasePoint, remotePaymentBasePoint)
val (sequence, locktime) = encodeTxNumber(txnumber)
val tx = Transaction(
version = 2,
txIn = TxIn(commitTxInput.outPoint, Array.emptyByteArray, sequence = sequence) :: Nil,
txOut = toLocalDelayedOutput_opt.toSeq ++ toRemoteOutput_opt.toSeq ++ htlcOfferedOutputs ++ htlcReceivedOutputs,
lockTime = locktime)
CommitTx(commitTxInput, LexicographicalOrdering.sort(tx))
}
def makeHtlcTimeoutTx(commitTx: Transaction, localDustLimit: Satoshi, localRevocationPubkey: PublicKey, toLocalDelay: Int, localDelayedPaymentPubkey: PublicKey, localHtlcPubkey: PublicKey, remoteHtlcPubkey: PublicKey, feeratePerKw: Long, htlc: UpdateAddHtlc): HtlcTimeoutTx = {
val fee = weight2fee(feeratePerKw, htlcTimeoutWeight)
val redeemScript = htlcOffered(localHtlcPubkey, remoteHtlcPubkey, localRevocationPubkey, ripemd160(htlc.paymentHash))
val pubkeyScript = write(pay2wsh(redeemScript))
val outputIndex = findPubKeyScriptIndex(commitTx, pubkeyScript)
require(outputIndex >= 0, "output not found (was trimmed?)")
val amount = MilliSatoshi(htlc.amountMsat) - fee
require(amount >= localDustLimit, "amount lesser than dust limit")
val input = InputInfo(OutPoint(commitTx, outputIndex), commitTx.txOut(outputIndex), write(redeemScript))
HtlcTimeoutTx(input, Transaction(
version = 2,
txIn = TxIn(input.outPoint, Array.emptyByteArray, 0) :: Nil,
txOut = TxOut(amount, pay2wsh(toLocalDelayed(localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey))) :: Nil,
lockTime = htlc.expiry))
}
def makeHtlcSuccessTx(commitTx: Transaction, localDustLimit: Satoshi, localRevocationPubkey: PublicKey, toLocalDelay: Int, localDelayedPaymentPubkey: PublicKey, localHtlcPubkey: PublicKey, remoteHtlcPubkey: PublicKey, feeratePerKw: Long, htlc: UpdateAddHtlc): HtlcSuccessTx = {
val fee = weight2fee(feeratePerKw, htlcSuccessWeight)
val redeemScript = htlcReceived(localHtlcPubkey, remoteHtlcPubkey, localRevocationPubkey, ripemd160(htlc.paymentHash), htlc.expiry)
val pubkeyScript = write(pay2wsh(redeemScript))
val outputIndex = findPubKeyScriptIndex(commitTx, pubkeyScript)
require(outputIndex >= 0, "output not found (was trimmed?)")
val amount = MilliSatoshi(htlc.amountMsat) - fee
require(amount >= localDustLimit, "amount lesser than dust limit")
val input = InputInfo(OutPoint(commitTx, outputIndex), commitTx.txOut(outputIndex), write(redeemScript))
HtlcSuccessTx(input, Transaction(
version = 2,
txIn = TxIn(input.outPoint, Array.emptyByteArray, 0) :: Nil,
txOut = TxOut(amount, pay2wsh(toLocalDelayed(localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey))) :: Nil,
lockTime = 0), htlc.paymentHash)
}
def makeHtlcTxs(commitTx: Transaction, localDustLimit: Satoshi, localRevocationPubkey: PublicKey, toLocalDelay: Int, localDelayedPaymentPubkey: PublicKey, localHtlcPubkey: PublicKey, remoteHtlcPubkey: PublicKey, spec: CommitmentSpec): (Seq[HtlcTimeoutTx], Seq[HtlcSuccessTx]) = {
val htlcTimeoutTxs = trimOfferedHtlcs(localDustLimit, spec)
.map(htlc => makeHtlcTimeoutTx(commitTx, localDustLimit, localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey, localHtlcPubkey, remoteHtlcPubkey, spec.feeratePerKw, htlc.add))
val htlcSuccessTxs = trimReceivedHtlcs(localDustLimit, spec)
.map(htlc => makeHtlcSuccessTx(commitTx, localDustLimit, localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey, localHtlcPubkey, remoteHtlcPubkey, spec.feeratePerKw, htlc.add))
(htlcTimeoutTxs, htlcSuccessTxs)
}
def makeClaimHtlcSuccessTx(commitTx: Transaction, localDustLimit: Satoshi, localHtlcPubkey: PublicKey, remoteHtlcPubkey: PublicKey, remoteRevocationPubkey: PublicKey, localFinalScriptPubKey: BinaryData, htlc: UpdateAddHtlc, feeratePerKw: Long): ClaimHtlcSuccessTx = {
val fee = weight2fee(feeratePerKw, claimHtlcSuccessWeight)
val redeemScript = htlcOffered(remoteHtlcPubkey, localHtlcPubkey, remoteRevocationPubkey, ripemd160(htlc.paymentHash))
val pubkeyScript = write(pay2wsh(redeemScript))
val outputIndex = findPubKeyScriptIndex(commitTx, pubkeyScript)
require(outputIndex >= 0, "output not found (was trimmed?)")
val input = InputInfo(OutPoint(commitTx, outputIndex), commitTx.txOut(outputIndex), write(redeemScript))
val amount = input.txOut.amount - fee
require(amount >= localDustLimit, "amount lesser than dust limit")
ClaimHtlcSuccessTx(input, Transaction(
version = 2,
txIn = TxIn(input.outPoint, Array.emptyByteArray, 0xffffffffL) :: Nil,
txOut = TxOut(amount, localFinalScriptPubKey) :: Nil,
lockTime = 0))
}
def makeClaimHtlcTimeoutTx(commitTx: Transaction, localDustLimit: Satoshi, localHtlcPubkey: PublicKey, remoteHtlcPubkey: PublicKey, remoteRevocationPubkey: PublicKey, localFinalScriptPubKey: BinaryData, htlc: UpdateAddHtlc, feeratePerKw: Long): ClaimHtlcTimeoutTx = {
val fee = weight2fee(feeratePerKw, claimHtlcTimeoutWeight)
val redeemScript = htlcReceived(remoteHtlcPubkey, localHtlcPubkey, remoteRevocationPubkey, ripemd160(htlc.paymentHash), htlc.expiry)
val pubkeyScript = write(pay2wsh(redeemScript))
val outputIndex = findPubKeyScriptIndex(commitTx, pubkeyScript)
require(outputIndex >= 0, "output not found (was trimmed?)")
val input = InputInfo(OutPoint(commitTx, outputIndex), commitTx.txOut(outputIndex), write(redeemScript))
val amount = input.txOut.amount - fee
require(amount >= localDustLimit, "amount lesser than dust limit")
ClaimHtlcTimeoutTx(input, Transaction(
version = 2,
txIn = TxIn(input.outPoint, Array.emptyByteArray, 0x00000000L) :: Nil,
txOut = TxOut(amount, localFinalScriptPubKey) :: Nil,
lockTime = htlc.expiry))
}
def makeClaimP2WPKHOutputTx(delayedOutputTx: Transaction, localDustLimit: Satoshi, localPaymentPubkey: PublicKey, localFinalScriptPubKey: BinaryData, feeratePerKw: Long): ClaimP2WPKHOutputTx = {
val fee = weight2fee(feeratePerKw, claimP2WPKHOutputWeight)
val redeemScript = Script.pay2pkh(localPaymentPubkey)
val pubkeyScript = write(pay2wpkh(localPaymentPubkey))
val outputIndex = findPubKeyScriptIndex(delayedOutputTx, pubkeyScript)
require(outputIndex >= 0, "output not found (was trimmed?)")
val input = InputInfo(OutPoint(delayedOutputTx, outputIndex), delayedOutputTx.txOut(outputIndex), write(redeemScript))
val amount = input.txOut.amount - fee
require(amount >= localDustLimit, "amount lesser than dust limit")
ClaimP2WPKHOutputTx(input, Transaction(
version = 2,
txIn = TxIn(input.outPoint, Array.emptyByteArray, 0x00000000L) :: Nil,
txOut = TxOut(amount, localFinalScriptPubKey) :: Nil,
lockTime = 0))
}
def makeClaimDelayedOutputTx(delayedOutputTx: Transaction, localDustLimit: Satoshi, localRevocationPubkey: PublicKey, toLocalDelay: Int, localDelayedPaymentPubkey: PublicKey, localFinalScriptPubKey: BinaryData, feeratePerKw: Long): ClaimDelayedOutputTx = {
val fee = weight2fee(feeratePerKw, claimHtlcDelayedWeight)
val redeemScript = toLocalDelayed(localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey)
val pubkeyScript = write(pay2wsh(redeemScript))
val outputIndex = findPubKeyScriptIndex(delayedOutputTx, pubkeyScript)
require(outputIndex >= 0, "output not found (was trimmed?)")
val input = InputInfo(OutPoint(delayedOutputTx, outputIndex), delayedOutputTx.txOut(outputIndex), write(redeemScript))
val amount = input.txOut.amount - fee
require(amount >= localDustLimit, "amount lesser than dust limit")
ClaimDelayedOutputTx(input, Transaction(
version = 2,
txIn = TxIn(input.outPoint, Array.emptyByteArray, toLocalDelay) :: Nil,
txOut = TxOut(amount, localFinalScriptPubKey) :: Nil,
lockTime = 0))
}
def makeMainPenaltyTx(commitTx: Transaction, localDustLimit: Satoshi, remoteRevocationPubkey: PublicKey, localFinalScriptPubKey: BinaryData, toRemoteDelay: Int, remoteDelayedPaymentPubkey: PublicKey, feeratePerKw: Long): MainPenaltyTx = {
val fee = weight2fee(feeratePerKw, mainPenaltyWeight)
val redeemScript = toLocalDelayed(remoteRevocationPubkey, toRemoteDelay, remoteDelayedPaymentPubkey)
val pubkeyScript = write(pay2wsh(redeemScript))
val outputIndex = findPubKeyScriptIndex(commitTx, pubkeyScript)
require(outputIndex >= 0, "output not found (was trimmed?)")
val input = InputInfo(OutPoint(commitTx, outputIndex), commitTx.txOut(outputIndex), write(redeemScript))
val amount = input.txOut.amount - fee
require(amount >= localDustLimit, "amount lesser than dust limit")
MainPenaltyTx(input, Transaction(
version = 2,
txIn = TxIn(input.outPoint, Array.emptyByteArray, 0xffffffffL) :: Nil,
txOut = TxOut(amount, localFinalScriptPubKey) :: Nil,
lockTime = 0))
}
def makeHtlcPenaltyTx(commitTx: Transaction, localDustLimit: Satoshi): HtlcPenaltyTx = ???
def makeClosingTx(commitTxInput: InputInfo, localScriptPubKey: BinaryData, remoteScriptPubKey: BinaryData, localIsFunder: Boolean, dustLimit: Satoshi, closingFee: Satoshi, spec: CommitmentSpec): ClosingTx = {
require(spec.htlcs.size == 0, "there shouldn't be any pending htlcs")
val (toLocalAmount: Satoshi, toRemoteAmount: Satoshi) = localIsFunder match {
case true => (millisatoshi2satoshi(MilliSatoshi(spec.toLocalMsat)) - closingFee, millisatoshi2satoshi(MilliSatoshi(spec.toRemoteMsat)))
case false => (millisatoshi2satoshi(MilliSatoshi(spec.toLocalMsat)), millisatoshi2satoshi(MilliSatoshi(spec.toRemoteMsat)) - 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 = Transaction(
version = 2,
txIn = TxIn(commitTxInput.outPoint, Array.emptyByteArray, sequence = 0xffffffffL) :: Nil,
txOut = toLocalOutput_opt.toSeq ++ toRemoteOutput_opt.toSeq ++ Nil,
lockTime = 0)
ClosingTx(commitTxInput, LexicographicalOrdering.sort(tx))
}
def findPubKeyScriptIndex(tx: Transaction, pubkeyScript: BinaryData): Int = tx.txOut.indexWhere(_.publicKeyScript == pubkeyScript)
def findPubKeyScriptIndex(tx: Transaction, pubkeyScript: Seq[ScriptElt]): Int = findPubKeyScriptIndex(tx, write(pubkeyScript))
def sign(tx: Transaction, inputIndex: Int, redeemScript: BinaryData, amount: Satoshi, key: PrivateKey): BinaryData = {
Transaction.signInput(tx, inputIndex, redeemScript, SIGHASH_ALL, amount, SIGVERSION_WITNESS_V0, key)
}
// when the amount is not specified, we used the legacy (pre-segwit) signature scheme
// this is only used to spend the to-remote output of a commit tx, which is the only non-segwit output
// that we use
// TODO: change this if the decide to use P2WPKH in the to-remote output
def sign(tx: Transaction, inputIndex: Int, redeemScript: BinaryData, key: PrivateKey): BinaryData = {
Transaction.signInput(tx, inputIndex, redeemScript, SIGHASH_ALL, Satoshi(0), SIGVERSION_BASE, key)
}
def sign(txinfo: TransactionWithInputInfo, key: PrivateKey): BinaryData = {
require(txinfo.tx.txIn.size == 1, "only one input allowed")
sign(txinfo.tx, inputIndex = 0, txinfo.input.redeemScript, txinfo.input.txOut.amount, key)
}
def addSigs(commitTx: CommitTx, localFundingPubkey: PublicKey, remoteFundingPubkey: PublicKey, localSig: BinaryData, remoteSig: BinaryData): CommitTx = {
val witness = Scripts.witness2of2(localSig, remoteSig, localFundingPubkey, remoteFundingPubkey)
commitTx.copy(tx = commitTx.tx.updateWitness(0, witness))
}
def addSigs(claimMainDelayedRevokedTx: MainPenaltyTx, revocationSig: BinaryData): MainPenaltyTx = {
val witness = Scripts.witnessToLocalDelayedWithRevocationSig(revocationSig, claimMainDelayedRevokedTx.input.redeemScript)
claimMainDelayedRevokedTx.copy(tx = claimMainDelayedRevokedTx.tx.updateWitness(0, witness))
}
def addSigs(htlcSuccessTx: HtlcSuccessTx, localSig: BinaryData, remoteSig: BinaryData, paymentPreimage: BinaryData): HtlcSuccessTx = {
val witness = witnessHtlcSuccess(localSig, remoteSig, paymentPreimage, htlcSuccessTx.input.redeemScript)
htlcSuccessTx.copy(tx = htlcSuccessTx.tx.updateWitness(0, witness))
}
def addSigs(htlcTimeoutTx: HtlcTimeoutTx, localSig: BinaryData, remoteSig: BinaryData): HtlcTimeoutTx = {
val witness = witnessHtlcTimeout(localSig, remoteSig, htlcTimeoutTx.input.redeemScript)
htlcTimeoutTx.copy(tx = htlcTimeoutTx.tx.updateWitness(0, witness))
}
def addSigs(claimHtlcSuccessTx: ClaimHtlcSuccessTx, localSig: BinaryData, paymentPreimage: BinaryData): ClaimHtlcSuccessTx = {
val witness = witnessClaimHtlcSuccessFromCommitTx(localSig, paymentPreimage, claimHtlcSuccessTx.input.redeemScript)
claimHtlcSuccessTx.copy(tx = claimHtlcSuccessTx.tx.updateWitness(0, witness))
}
def addSigs(claimHtlcTimeoutTx: ClaimHtlcTimeoutTx, localSig: BinaryData): ClaimHtlcTimeoutTx = {
val witness = witnessClaimHtlcTimeoutFromCommitTx(localSig, claimHtlcTimeoutTx.input.redeemScript)
claimHtlcTimeoutTx.copy(tx = claimHtlcTimeoutTx.tx.updateWitness(0, witness))
}
def addSigs(claimP2WPKHOutputTx: ClaimP2WPKHOutputTx, localPaymentPubkey: BinaryData, localSig: BinaryData): ClaimP2WPKHOutputTx = {
val witness = ScriptWitness(Seq(localSig, localPaymentPubkey))
claimP2WPKHOutputTx.copy(tx = claimP2WPKHOutputTx.tx.updateWitness(0, witness))
}
def addSigs(claimHtlcDelayed: ClaimDelayedOutputTx, localSig: BinaryData): ClaimDelayedOutputTx = {
val witness = witnessToLocalDelayedAfterDelay(localSig, claimHtlcDelayed.input.redeemScript)
claimHtlcDelayed.copy(tx = claimHtlcDelayed.tx.updateWitness(0, witness))
}
def addSigs(closingTx: ClosingTx, localFundingPubkey: PublicKey, remoteFundingPubkey: PublicKey, localSig: BinaryData, remoteSig: BinaryData): ClosingTx = {
val witness = Scripts.witness2of2(localSig, remoteSig, localFundingPubkey, remoteFundingPubkey)
closingTx.copy(tx = closingTx.tx.updateWitness(0, witness))
}
def checkSpendable(parent: Transaction, child: Transaction): Try[Unit] =
Try(Transaction.correctlySpends(child, parent :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS))
def checkSpendable(txinfo: TransactionWithInputInfo): Try[Unit] =
Try(Transaction.correctlySpends(txinfo.tx, Map(txinfo.tx.txIn(0).outPoint -> txinfo.input.txOut), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS))
def checkSig(txinfo: TransactionWithInputInfo, sig: BinaryData, pubKey: PublicKey): Boolean = {
val data = Transaction.hashForSigning(txinfo.tx, inputIndex = 0, txinfo.input.redeemScript, SIGHASH_ALL, txinfo.input.txOut.amount, SIGVERSION_WITNESS_V0)
Crypto.verifySignature(data, sig, pubKey)
}
}

View File

@ -0,0 +1,235 @@
package fr.acinq.eclair.wire
import fr.acinq.bitcoin.{BinaryData, OutPoint, Transaction, TxOut}
import fr.acinq.eclair.channel._
import fr.acinq.eclair.crypto.ShaChain
import fr.acinq.eclair.payment.{Local, Origin, Relayed}
import fr.acinq.eclair.transactions.Transactions._
import fr.acinq.eclair.transactions._
import fr.acinq.eclair.wire.LightningMessageCodecs._
import scodec.bits.{BitVector, ByteVector}
import scodec.codecs._
import scodec.{Attempt, Codec}
/**
* Created by PM on 02/06/2017.
*/
object ChannelCodecs {
val localParamsCodec: Codec[LocalParams] = (
("nodeId" | publicKey) ::
("dustLimitSatoshis" | uint64) ::
("maxHtlcValueInFlightMsat" | uint64ex) ::
("channelReserveSatoshis" | uint64) ::
("htlcMinimumMsat" | uint64) ::
("toSelfDelay" | uint16) ::
("maxAcceptedHtlcs" | uint16) ::
("fundingPrivKey" | privateKey) ::
("revocationSecret" | scalar) ::
("paymentKey" | scalar) ::
("delayedPaymentKey" | scalar) ::
("htlcKey" | scalar) ::
("defaultFinalScriptPubKey" | varsizebinarydata) ::
("shaSeed" | varsizebinarydata) ::
("isFunder" | bool) ::
("globalFeatures" | varsizebinarydata) ::
("localFeatures" | varsizebinarydata)).as[LocalParams]
val remoteParamsCodec: Codec[RemoteParams] = (
("nodeId" | publicKey) ::
("dustLimitSatoshis" | uint64) ::
("maxHtlcValueInFlightMsat" | uint64ex) ::
("channelReserveSatoshis" | uint64) ::
("htlcMinimumMsat" | uint64) ::
("toSelfDelay" | uint16) ::
("maxAcceptedHtlcs" | uint16) ::
("fundingPubKey" | publicKey) ::
("revocationBasepoint" | point) ::
("paymentBasepoint" | point) ::
("delayedPaymentBasepoint" | point) ::
("htlcBasepoint" | point) ::
("globalFeatures" | varsizebinarydata) ::
("localFeatures" | varsizebinarydata)).as[RemoteParams]
val directionCodec: Codec[Direction] = Codec[Direction](
(dir: Direction) => bool.encode(dir == IN),
(wire: BitVector) => bool.decode(wire).map(_.map(b => if (b) IN else OUT))
)
val htlcCodec: Codec[DirectedHtlc] = (
("direction" | directionCodec) ::
("add" | updateAddHtlcCodec)).as[DirectedHtlc]
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" | uint32) ::
("toLocalMsat" | uint64) ::
("toRemoteMsat" | uint64)).as[CommitmentSpec]
def outPointCodec: Codec[OutPoint] = variableSizeBytes(uint16, bytes.xmap(d => OutPoint.read(d.toArray), d => ByteVector(OutPoint.write(d).data)))
def txOutCodec: Codec[TxOut] = variableSizeBytes(uint16, bytes.xmap(d => TxOut.read(d.toArray), d => ByteVector(TxOut.write(d).data)))
def txCodec: Codec[Transaction] = variableSizeBytes(uint16, bytes.xmap(d => Transaction.read(d.toArray), d => ByteVector(Transaction.write(d).data)))
val inputInfoCodec: Codec[InputInfo] = (
("outPoint" | outPointCodec) ::
("txOut" | txOutCodec) ::
("redeemScript" | varsizebinarydata)).as[InputInfo]
val txWithInputInfoCodec: Codec[TransactionWithInputInfo] = discriminated[TransactionWithInputInfo].by(uint16)
.typecase(0x01, (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[CommitTx])
.typecase(0x02, (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec) :: ("paymentHash" | binarydata(32))).as[HtlcSuccessTx])
.typecase(0x03, (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[HtlcTimeoutTx])
.typecase(0x04, (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[ClaimHtlcSuccessTx])
.typecase(0x05, (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[ClaimHtlcTimeoutTx])
.typecase(0x06, (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[ClaimP2WPKHOutputTx])
.typecase(0x07, (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[ClaimDelayedOutputTx])
.typecase(0x08, (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[MainPenaltyTx])
.typecase(0x09, (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[HtlcPenaltyTx])
.typecase(0x10, (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[ClosingTx])
val htlcTxAndSigsCodec: Codec[HtlcTxAndSigs] = (
("txinfo" | txWithInputInfoCodec) ::
("localSig" | varsizebinarydata) ::
("remoteSig" | varsizebinarydata)).as[HtlcTxAndSigs]
val publishableTxsCodec: Codec[PublishableTxs] = (
("commitTx" | (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[CommitTx]) ::
("htlcTxsAndSigs" | listOfN(uint16, htlcTxAndSigsCodec))).as[PublishableTxs]
val localCommitCodec: Codec[LocalCommit] = (
("index" | uint64) ::
("spec" | commitmentSpecCodec) ::
("publishableTxs" | publishableTxsCodec)).as[LocalCommit]
val remoteCommitCodec: Codec[RemoteCommit] = (
("index" | uint64) ::
("spec" | commitmentSpecCodec) ::
("txid" | binarydata(32)) ::
("remotePerCommitmentPoint" | point)).as[RemoteCommit]
val updateMessageCodec: Codec[UpdateMessage] = lightningMessageCodec.narrow(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[WaitingForRevocation] = (
("nextRemoteCommit" | remoteCommitCodec) ::
("sent" | commitSigCodec) ::
("sentAfterLocalCommitIndex" | uint64) ::
("reSignAsap" | bool)).as[WaitingForRevocation]
val relayedCodec: Codec[Relayed] = (
("originChannelId" | binarydata(32)) ::
("originHtlcId" | int64) ::
("amountMsatIn" | uint64) ::
("amountMsatOut" | uint64)).as[Relayed]
val originCodec: Codec[Origin] = discriminated[Origin].by(uint16)
.typecase(0x01, provide(Local(None)))
.typecase(0x02, relayedCodec)
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 commitmentsCodec: Codec[Commitments] = (
("localParams" | localParamsCodec) ::
("remoteParams" | remoteParamsCodec) ::
("channelFlags" | byte) ::
("localCommit" | localCommitCodec) ::
("remoteCommit" | remoteCommitCodec) ::
("localChanges" | localChangesCodec) ::
("remoteChanges" | remoteChangesCodec) ::
("localNextHtlcId" | uint64) ::
("remoteNextHtlcId" | uint64) ::
("originChannels" | originsMapCodec) ::
("remoteNextCommitInfo" | either(bool, waitingForRevocationCodec, point)) ::
("commitInput" | inputInfoCodec) ::
("remotePerCommitmentSecrets" | ShaChain.shaChainCodec) ::
("channelId" | binarydata(32))).as[Commitments]
val localCommitPublishedCodec: Codec[LocalCommitPublished] = (
("commitTx" | txCodec) ::
("claimMainDelayedOutputTx" | optional(bool, txCodec)) ::
("htlcSuccessTxs" | listOfN(uint16, txCodec)) ::
("htlcTimeoutTxs" | listOfN(uint16, txCodec)) ::
("claimHtlcDelayedTx" | listOfN(uint16, txCodec)) ::
("spent" | provide(Map.empty[OutPoint, BinaryData]))).as[LocalCommitPublished]
val remoteCommitPublishedCodec: Codec[RemoteCommitPublished] = (
("commitTx" | txCodec) ::
("claimMainOutputTx" | optional(bool, txCodec)) ::
("claimHtlcSuccessTxs" | listOfN(uint16, txCodec)) ::
("claimHtlcTimeoutTxs" | listOfN(uint16, txCodec)) ::
("spent" | provide(Map.empty[OutPoint, BinaryData]))).as[RemoteCommitPublished]
val revokedCommitPublishedCodec: Codec[RevokedCommitPublished] = (
("commitTx" | txCodec) ::
("claimMainOutputTx" | optional(bool, txCodec)) ::
("mainPenaltyTx" | optional(bool, txCodec)) ::
("claimHtlcTimeoutTxs" | listOfN(uint16, txCodec)) ::
("htlcTimeoutTxs" | listOfN(uint16, txCodec)) ::
("htlcPenaltyTxs" | listOfN(uint16, txCodec)) ::
("spent" | provide(Map.empty[OutPoint, BinaryData]))).as[RevokedCommitPublished]
val DATA_WAIT_FOR_FUNDING_CONFIRMED_Codec: Codec[DATA_WAIT_FOR_FUNDING_CONFIRMED] = (
("commitments" | commitmentsCodec) ::
("deferred" | optional(bool, fundingLockedCodec)) ::
("lastSent" | either(bool, fundingCreatedCodec, fundingSignedCodec))).as[DATA_WAIT_FOR_FUNDING_CONFIRMED]
val DATA_WAIT_FOR_FUNDING_LOCKED_Codec: Codec[DATA_WAIT_FOR_FUNDING_LOCKED] = (
("commitments" | commitmentsCodec) ::
("lastSent" | fundingLockedCodec)).as[DATA_WAIT_FOR_FUNDING_LOCKED]
val DATA_NORMAL_Codec: Codec[DATA_NORMAL] = (
("commitments" | commitmentsCodec) ::
("shortChannelId" | optional(bool, uint64)) ::
("localAnnouncementSignatures" | optional(bool, announcementSignaturesCodec)) ::
("localShutdown" | optional(bool, shutdownCodec)) ::
("remoteShutdown" | optional(bool, shutdownCodec))).as[DATA_NORMAL]
val DATA_SHUTDOWN_Codec: Codec[DATA_SHUTDOWN] = (
("commitments" | commitmentsCodec) ::
("localShutdown" | shutdownCodec) ::
("remoteShutdown" | shutdownCodec)).as[DATA_SHUTDOWN]
val DATA_NEGOTIATING_Codec: Codec[DATA_NEGOTIATING] = (
("commitments" | commitmentsCodec) ::
("localShutdown" | shutdownCodec) ::
("remoteShutdown" | shutdownCodec) ::
("localClosingSigned" | closingSignedCodec)).as[DATA_NEGOTIATING]
val DATA_CLOSING_Codec: Codec[DATA_CLOSING] = (
("commitments" | commitmentsCodec) ::
("mutualClosePublished" | optional(bool, txCodec)) ::
("localCommitPublished" | optional(bool, localCommitPublishedCodec)) ::
("remoteCommitPublished" | optional(bool, remoteCommitPublishedCodec)) ::
("nextRemoteCommitPublished" | optional(bool, remoteCommitPublishedCodec)) ::
("revokedCommitPublished" | listOfN(uint16, revokedCommitPublishedCodec))).as[DATA_CLOSING]
val stateDataCodec: Codec[HasCommitments] = ("version" | constant(0x00)) ~> discriminated[HasCommitments].by(uint16)
.typecase(0x01, DATA_WAIT_FOR_FUNDING_CONFIRMED_Codec)
.typecase(0x02, DATA_WAIT_FOR_FUNDING_LOCKED_Codec)
.typecase(0x03, DATA_NORMAL_Codec)
.typecase(0x04, DATA_SHUTDOWN_Codec)
.typecase(0x05, DATA_NEGOTIATING_Codec)
.typecase(0x06, DATA_CLOSING_Codec)
}

View File

@ -0,0 +1,74 @@
package fr.acinq.eclair.wire
import fr.acinq.bitcoin.BinaryData
import fr.acinq.eclair.wire.LightningMessageCodecs.{binarydata, channelUpdateCodec, uint64}
import scodec.Codec
import scodec.codecs._
/**
* see https://github.com/lightningnetwork/lightning-rfc/blob/master/04-onion-routing.md
* Created by fabrice on 14/03/17.
*/
// @formatter:off
sealed trait FailureMessage
sealed trait BadOnion extends FailureMessage { def onionHash: BinaryData }
sealed trait Perm extends FailureMessage
sealed trait Node extends FailureMessage
sealed trait Update extends FailureMessage { def update: ChannelUpdate }
case object InvalidRealm extends Perm
case object TemporaryNodeFailure extends Node
case object PermanentNodeFailure extends Perm with Node
case object RequiredNodeFeatureMissing extends Perm with Node
case class InvalidOnionVersion(onionHash: BinaryData) extends BadOnion with Perm
case class InvalidOnionHmac(onionHash: BinaryData) extends BadOnion with Perm
case class InvalidOnionKey(onionHash: BinaryData) extends BadOnion with Perm
case class TemporaryChannelFailure(update: ChannelUpdate) extends Update
case object PermanentChannelFailure extends Perm
case object RequiredChannelFeatureMissing extends Perm
case object UnknownNextPeer extends Perm
case class AmountBelowMinimum(amountMsat: Long, update: ChannelUpdate) extends Update
case class FeeInsufficient(amountMsat: Long, update: ChannelUpdate) extends Update
case class IncorrectCltvExpiry(expiry: Long, update: ChannelUpdate) extends Update
case class ExpiryTooSoon(update: ChannelUpdate) extends Update
case class ChannelDisabled(flags: BinaryData, update: ChannelUpdate) extends Update
case object UnknownPaymentHash extends Perm
case object IncorrectPaymentAmount extends Perm
case object FinalExpiryTooSoon extends FailureMessage
case class FinalIncorrectCltvExpiry(expiry: Long) extends FailureMessage
case class FinalIncorrectHtlcAmount(amountMsat: Long) extends FailureMessage
// @formatter:on
object FailureMessageCodecs {
val BADONION = 0x8000
val PERM = 0x4000
val NODE = 0x2000
val UPDATE = 0x1000
val sha256Codec: Codec[BinaryData] = ("sha256Codec" | binarydata(32))
val failureMessageCodec = discriminated[FailureMessage].by(uint16)
.typecase(PERM | 1, provide(InvalidRealm))
.typecase(NODE | 2, provide(TemporaryNodeFailure))
.typecase(PERM | 2, provide(PermanentNodeFailure))
.typecase(PERM | NODE | 3, provide(RequiredNodeFeatureMissing))
.typecase(BADONION | PERM | 4, sha256Codec.as[InvalidOnionVersion])
.typecase(BADONION | PERM | 5, sha256Codec.as[InvalidOnionHmac])
.typecase(BADONION | PERM | 6, sha256Codec.as[InvalidOnionKey])
.typecase(UPDATE | 7, (("channelUpdate" | channelUpdateCodec)).as[TemporaryChannelFailure])
.typecase(PERM | 8, provide(PermanentChannelFailure))
.typecase(PERM | 9, provide(RequiredChannelFeatureMissing))
.typecase(PERM | 10, provide(UnknownNextPeer))
.typecase(UPDATE | 11, (("amountMsat" | uint64) :: ("channelUpdate" | channelUpdateCodec)).as[AmountBelowMinimum])
.typecase(UPDATE | 12, (("amountMsat" | uint64) :: ("channelUpdate" | channelUpdateCodec)).as[FeeInsufficient])
.typecase(UPDATE | 13, (("expiry" | uint32) :: ("channelUpdate" | channelUpdateCodec)).as[IncorrectCltvExpiry])
.typecase(UPDATE | 14, (("channelUpdate" | channelUpdateCodec)).as[ExpiryTooSoon])
.typecase(UPDATE | 20, (("flags" | binarydata(2)) :: ("channelUpdate" | channelUpdateCodec)).as[ChannelDisabled])
.typecase(PERM | 15, provide(UnknownPaymentHash))
.typecase(PERM | 16, provide(IncorrectPaymentAmount))
.typecase(17, provide(FinalExpiryTooSoon))
.typecase(18, (("expiry" | uint32)).as[FinalIncorrectCltvExpiry])
.typecase(19, (("amountMsat" | uint32)).as[FinalIncorrectHtlcAmount])
}

View File

@ -0,0 +1,59 @@
package fr.acinq.eclair.wire
import scodec.bits.{BitVector, ByteVector}
import scodec.{Attempt, Codec, DecodeResult, Err, SizeBound, codecs}
/**
*
* REMOVE THIS A NEW VERSION OF SCODEC IS RELEASED THAT INCLUDES CHANGES MADE IN
* https://github.com/scodec/scodec/pull/99/files
*
* Created by PM on 02/06/2017.
*/
final class FixedSizeStrictCodec[A](size: Long, codec: Codec[A]) extends Codec[A] {
override def sizeBound = SizeBound.exact(size)
override def encode(a: A) = for {
encoded <- codec.encode(a)
result <- {
if (encoded.size != size)
Attempt.failure(Err(s"[$a] requires ${encoded.size} bits but field is fixed size of exactly $size bits"))
else
Attempt.successful(encoded.padTo(size))
}
} yield result
override def decode(buffer: BitVector) = {
if (buffer.size == size) {
codec.decode(buffer.take(size)) map { res =>
DecodeResult(res.value, buffer.drop(size))
}
} else {
Attempt.failure(Err(s"expected exactly $size bits but got ${buffer.size} bits"))
}
}
override def toString = s"fixedSizeBitsStrict($size, $codec)"
}
object FixedSizeStrictCodec {
/**
* Encodes by returning the supplied byte vector if its length is `size` bytes, otherwise returning error;
* decodes by taking `size * 8` bits from the supplied bit vector and converting to a byte vector.
*
* @param size number of bits to encode/decode
* @group bits
*/
def bytesStrict(size: Int): Codec[ByteVector] = new Codec[ByteVector] {
private val codec = new FixedSizeStrictCodec(size * 8L, codecs.bits).xmap[ByteVector](_.toByteVector, _.toBitVector)
def sizeBound = codec.sizeBound
def encode(b: ByteVector) = codec.encode(b)
def decode(b: BitVector) = codec.decode(b)
override def toString = s"bytesStrict($size)"
}
}

View File

@ -0,0 +1,300 @@
package fr.acinq.eclair.wire
import java.math.BigInteger
import java.net.{Inet4Address, Inet6Address, InetAddress, InetSocketAddress}
import fr.acinq.bitcoin.Crypto.{Point, PrivateKey, PublicKey, Scalar}
import fr.acinq.bitcoin.{BinaryData, Crypto}
import fr.acinq.eclair.crypto.{Generators, Sphinx}
import fr.acinq.eclair.wire.FixedSizeStrictCodec.bytesStrict
import fr.acinq.eclair.{UInt64, wire}
import scodec.bits.{BitVector, ByteVector}
import scodec.codecs._
import scodec.{Attempt, Codec, Err}
/**
* Created by PM on 15/11/2016.
*/
object LightningMessageCodecs {
// this codec can be safely used for values < 2^63 and will fail otherwise
// (for something smarter see https://github.com/yzernik/bitcoin-scodec/blob/master/src/main/scala/io/github/yzernik/bitcoinscodec/structures/UInt64.scala)
val uint64: Codec[Long] = int64.narrow(l => if (l >= 0) Attempt.Successful(l) else Attempt.failure(Err(s"overflow for value $l")), l => l)
val uint64ex: Codec[UInt64] = bytes(8).xmap(b => UInt64(b.toArray), a => ByteVector(a.underlying.toByteArray).takeRight(8).padLeft(8))
def binarydata(size: Int): Codec[BinaryData] = limitedSizeBytes(size, bytesStrict(size).xmap(d => BinaryData(d.toArray), d => ByteVector(d.data)))
def varsizebinarydata: Codec[BinaryData] = variableSizeBytes(uint16, bytes.xmap(d => BinaryData(d.toArray), d => ByteVector(d.data)))
def listofsignatures: Codec[List[BinaryData]] = listOfN(uint16, signature)
def ipv4address: Codec[Inet4Address] = bytes(4).xmap(b => InetAddress.getByAddress(b.toArray).asInstanceOf[Inet4Address], a => ByteVector(a.getAddress))
def ipv6address: Codec[Inet6Address] = bytes(16).xmap(b => InetAddress.getByAddress(b.toArray).asInstanceOf[Inet6Address], a => ByteVector(a.getAddress))
def socketaddress: Codec[InetSocketAddress] =
(discriminated[InetAddress].by(uint8)
.typecase(1, ipv4address)
.typecase(2, ipv6address) ~ uint16)
.xmap(x => new InetSocketAddress(x._1, x._2), x => (x.getAddress, x.getPort))
// this one is a bit different from most other codecs: the first 'len' element is * not * the number of items
// in the list but rather the number of bytes of the encoded list. The rationale is once we've read this
// number of bytes we can just skip to the next field
def listofsocketaddresses: Codec[List[InetSocketAddress]] = variableSizeBytes(uint16, list(socketaddress))
def signature: Codec[BinaryData] = Codec[BinaryData](
(der: BinaryData) => bytes(64).encode(ByteVector(der2wire(der).toArray)),
(wire: BitVector) => bytes(64).decode(wire).map(_.map(b => wire2der(b.toArray)))
)
def scalar: Codec[Scalar] = Codec[Scalar](
(value: Scalar) => bytes(32).encode(ByteVector(value.toBin.toArray)),
(wire: BitVector) => bytes(32).decode(wire).map(_.map(b => Scalar(b.toArray)))
)
def point: Codec[Point] = Codec[Point](
(point: Point) => bytes(33).encode(ByteVector(point.toBin(compressed = true).toArray)),
(wire: BitVector) => bytes(33).decode(wire).map(_.map(b => Point(b.toArray)))
)
def privateKey: Codec[PrivateKey] = Codec[PrivateKey](
(priv: PrivateKey) => bytes(32).encode(ByteVector(priv.value.toBin.toArray)),
(wire: BitVector) => bytes(32).decode(wire).map(_.map(b => PrivateKey(b.toArray, compressed = true)))
)
def publicKey: Codec[PublicKey] = Codec[PublicKey](
(pub: PublicKey) => bytes(33).encode(ByteVector(pub.value.toBin(compressed = true).toArray)),
(wire: BitVector) => bytes(33).decode(wire).map(_.map(b => PublicKey(b.toArray)))
)
def optionalSignature: Codec[Option[BinaryData]] = Codec[Option[BinaryData]](
(der: Option[BinaryData]) => der match {
case Some(sig) => bytes(64).encode(ByteVector(der2wire(sig).toArray))
case None => bytes(64).encode(ByteVector.fill[Byte](64)(0))
},
(wire: BitVector) => bytes(64).decode(wire).map(_.map(b => {
val a = b.toArray
if (a.exists(_ != 0)) Some(wire2der(a)) else None
}))
)
def rgb: Codec[(Byte, Byte, Byte)] = bytes(3).xmap(buf => (buf(0), buf(1), buf(2)), t => ByteVector(t._1, t._2, t._3))
def zeropaddedstring(size: Int): Codec[String] = fixedSizeBytes(32, utf8).xmap(s => s.takeWhile(_ != '\u0000'), s => s)
def der2wire(signature: BinaryData): BinaryData = {
require(Crypto.isDERSignature(signature), s"invalid DER signature $signature")
val (r, s) = Crypto.decodeSignature(signature)
Generators.fixSize(r.toByteArray.dropWhile(_ == 0)) ++ Generators.fixSize(s.toByteArray.dropWhile(_ == 0))
}
def wire2der(sig: BinaryData): BinaryData = {
require(sig.length == 64, "wire signature length must be 64")
val r = new BigInteger(1, sig.take(32).toArray)
val s = new BigInteger(1, sig.takeRight(32).toArray)
Crypto.encodeSignature(r, s) :+ fr.acinq.bitcoin.SIGHASH_ALL.toByte // wtf ??
}
val initCodec: Codec[Init] = (
("globalFeatures" | varsizebinarydata) ::
("localFeatures" | varsizebinarydata)).as[Init]
val errorCodec: Codec[Error] = (
("channelId" | binarydata(32)) ::
("data" | varsizebinarydata)).as[Error]
val pingCodec: Codec[Ping] = (
("pongLength" | uint16) ::
("data" | varsizebinarydata)).as[Ping]
val pongCodec: Codec[Pong] =
("data" | varsizebinarydata).as[Pong]
val channelReestablishCodec: Codec[ChannelReestablish] = (
("channelId" | binarydata(32)) ::
("nextLocalCommitmentNumber" | uint64) ::
("nextRemoteRevocationNumber" | uint64)).as[ChannelReestablish]
val openChannelCodec: Codec[OpenChannel] = (
("chainHash" | binarydata(32)) ::
("temporaryChannelId" | binarydata(32)) ::
("fundingSatoshis" | uint64) ::
("pushMsat" | uint64) ::
("dustLimitSatoshis" | uint64) ::
("maxHtlcValueInFlightMsat" | uint64ex) ::
("channelReserveSatoshis" | uint64) ::
("htlcMinimumMsat" | uint64) ::
("feeratePerKw" | uint32) ::
("toSelfDelay" | uint16) ::
("maxAcceptedHtlcs" | uint16) ::
("fundingPubkey" | publicKey) ::
("revocationBasepoint" | point) ::
("paymentBasepoint" | point) ::
("delayedPaymentBasepoint" | point) ::
("htlcBasepoint" | point) ::
("firstPerCommitmentPoint" | point) ::
("channelFlags" | byte)).as[OpenChannel]
val acceptChannelCodec: Codec[AcceptChannel] = (
("temporaryChannelId" | binarydata(32)) ::
("dustLimitSatoshis" | uint64) ::
("maxHtlcValueInFlightMsat" | uint64ex) ::
("channelReserveSatoshis" | uint64) ::
("htlcMinimumMsat" | uint64) ::
("minimumDepth" | uint32) ::
("toSelfDelay" | uint16) ::
("maxAcceptedHtlcs" | uint16) ::
("fundingPubkey" | publicKey) ::
("revocationBasepoint" | point) ::
("paymentBasepoint" | point) ::
("delayedPaymentBasepoint" | point) ::
("htlcBasepoint" | point) ::
("firstPerCommitmentPoint" | point)).as[AcceptChannel]
val fundingCreatedCodec: Codec[FundingCreated] = (
("temporaryChannelId" | binarydata(32)) ::
("fundingTxid" | binarydata(32)) ::
("fundingOutputIndex" | uint16) ::
("signature" | signature)).as[FundingCreated]
val fundingSignedCodec: Codec[FundingSigned] = (
("channelId" | binarydata(32)) ::
("signature" | signature)).as[FundingSigned]
val fundingLockedCodec: Codec[FundingLocked] = (
("channelId" | binarydata(32)) ::
("nextPerCommitmentPoint" | point)).as[FundingLocked]
val shutdownCodec: Codec[wire.Shutdown] = (
("channelId" | binarydata(32)) ::
("scriptPubKey" | varsizebinarydata)).as[Shutdown]
val closingSignedCodec: Codec[ClosingSigned] = (
("channelId" | binarydata(32)) ::
("feeSatoshis" | uint64) ::
("signature" | signature)).as[ClosingSigned]
val updateAddHtlcCodec: Codec[UpdateAddHtlc] = (
("channelId" | binarydata(32)) ::
("id" | uint64) ::
("amountMsat" | uint64) ::
("paymentHash" | binarydata(32)) ::
("expiry" | uint32) ::
("onionRoutingPacket" | binarydata(Sphinx.PacketLength))).as[UpdateAddHtlc]
val updateFulfillHtlcCodec: Codec[UpdateFulfillHtlc] = (
("channelId" | binarydata(32)) ::
("id" | uint64) ::
("paymentPreimage" | binarydata(32))).as[UpdateFulfillHtlc]
val updateFailHtlcCodec: Codec[UpdateFailHtlc] = (
("channelId" | binarydata(32)) ::
("id" | uint64) ::
("reason" | varsizebinarydata)).as[UpdateFailHtlc]
val updateFailMalformedHtlcCodec: Codec[UpdateFailMalformedHtlc] = (
("channelId" | binarydata(32)) ::
("id" | uint64) ::
("onionHash" | binarydata(32)) ::
("failureCode" | uint16)).as[UpdateFailMalformedHtlc]
val commitSigCodec: Codec[CommitSig] = (
("channelId" | binarydata(32)) ::
("signature" | signature) ::
("htlcSignatures" | listofsignatures)).as[CommitSig]
val revokeAndAckCodec: Codec[RevokeAndAck] = (
("channelId" | binarydata(32)) ::
("perCommitmentSecret" | scalar) ::
("nextPerCommitmentPoint" | point)
).as[RevokeAndAck]
val updateFeeCodec: Codec[UpdateFee] = (
("channelId" | binarydata(32)) ::
("feeratePerKw" | uint32)).as[UpdateFee]
val announcementSignaturesCodec: Codec[AnnouncementSignatures] = (
("channelId" | binarydata(32)) ::
("shortChannelId" | int64) ::
("nodeSignature" | signature) ::
("bitcoinSignature" | signature)).as[AnnouncementSignatures]
val channelAnnouncementWitnessCodec = (
("features" | varsizebinarydata) ::
("chainHash" | binarydata(32)) ::
("shortChannelId" | int64) ::
("nodeId1" | publicKey) ::
("nodeId2" | publicKey) ::
("bitcoinKey1" | publicKey) ::
("bitcoinKey2" | publicKey))
val channelAnnouncementCodec: Codec[ChannelAnnouncement] = (
("nodeSignature1" | signature) ::
("nodeSignature2" | signature) ::
("bitcoinSignature1" | signature) ::
("bitcoinSignature2" | signature) ::
channelAnnouncementWitnessCodec).as[ChannelAnnouncement]
val nodeAnnouncementWitnessCodec = (
("features" | varsizebinarydata) ::
("timestamp" | uint32) ::
("nodeId" | publicKey) ::
("rgbColor" | rgb) ::
("alias" | zeropaddedstring(32)) ::
("addresses" | listofsocketaddresses))
val nodeAnnouncementCodec: Codec[NodeAnnouncement] = (
("signature" | signature) ::
nodeAnnouncementWitnessCodec).as[NodeAnnouncement]
val channelUpdateWitnessCodec = (
("chainHash" | binarydata(32)) ::
("shortChannelId" | int64) ::
("timestamp" | uint32) ::
("flags" | binarydata(2)) ::
("cltvExpiryDelta" | uint16) ::
("htlcMinimumMsat" | uint64) ::
("feeBaseMsat" | uint32) ::
("feeProportionalMillionths" | uint32))
val channelUpdateCodec: Codec[ChannelUpdate] = (
("signature" | signature) ::
channelUpdateWitnessCodec).as[ChannelUpdate]
val lightningMessageCodec = discriminated[LightningMessage].by(uint16)
.typecase(16, initCodec)
.typecase(17, errorCodec)
.typecase(18, pingCodec)
.typecase(19, pongCodec)
.typecase(32, openChannelCodec)
.typecase(33, acceptChannelCodec)
.typecase(34, fundingCreatedCodec)
.typecase(35, fundingSignedCodec)
.typecase(36, fundingLockedCodec)
.typecase(38, shutdownCodec)
.typecase(39, closingSignedCodec)
.typecase(128, updateAddHtlcCodec)
.typecase(130, updateFulfillHtlcCodec)
.typecase(131, updateFailHtlcCodec)
.typecase(132, commitSigCodec)
.typecase(133, revokeAndAckCodec)
.typecase(134, updateFeeCodec)
.typecase(135, updateFailMalformedHtlcCodec)
.typecase(136, channelReestablishCodec)
.typecase(256, channelAnnouncementCodec)
.typecase(257, nodeAnnouncementCodec)
.typecase(258, channelUpdateCodec)
.typecase(259, announcementSignaturesCodec)
val perHopPayloadCodec: Codec[PerHopPayload] = (
("realm" | constant(ByteVector.fromByte(0))) ::
("channel_id" | uint64) ::
("amt_to_forward" | uint64) ::
("outgoing_cltv_value" | int32) :: // we use a signed int32, it is enough to store cltv for 40 000 years
("unused_with_v0_version_on_header" | ignore(8 * 12))).as[PerHopPayload]
}

View File

@ -0,0 +1,160 @@
package fr.acinq.eclair.wire
import java.net.InetSocketAddress
import fr.acinq.bitcoin.BinaryData
import fr.acinq.bitcoin.Crypto.{Point, PublicKey, Scalar}
import fr.acinq.eclair.UInt64
/**
* Created by PM on 15/11/2016.
*/
// @formatter:off
sealed trait LightningMessage
sealed trait SetupMessage extends LightningMessage
sealed trait ChannelMessage extends LightningMessage
sealed trait HtlcMessage extends LightningMessage
sealed trait RoutingMessage extends LightningMessage
sealed trait HasTemporaryChannelId extends LightningMessage { def temporaryChannelId: BinaryData } // <- not in the spec
sealed trait HasChannelId extends LightningMessage { def channelId: BinaryData } // <- not in the spec
sealed trait UpdateMessage extends HtlcMessage // <- not in the spec
// @formatter:on
case class Init(globalFeatures: BinaryData,
localFeatures: BinaryData) extends SetupMessage
case class Error(channelId: BinaryData,
data: BinaryData) extends SetupMessage with HasChannelId
case class Ping(pongLength: Int, data: BinaryData) extends SetupMessage
case class Pong(data: BinaryData) extends SetupMessage
case class ChannelReestablish(
channelId: BinaryData,
nextLocalCommitmentNumber: Long,
nextRemoteRevocationNumber: Long) extends ChannelMessage with HasChannelId
case class OpenChannel(chainHash: BinaryData,
temporaryChannelId: BinaryData,
fundingSatoshis: Long,
pushMsat: Long,
dustLimitSatoshis: Long,
maxHtlcValueInFlightMsat: UInt64,
channelReserveSatoshis: Long,
htlcMinimumMsat: Long,
feeratePerKw: Long,
toSelfDelay: Int,
maxAcceptedHtlcs: Int,
fundingPubkey: PublicKey,
revocationBasepoint: Point,
paymentBasepoint: Point,
delayedPaymentBasepoint: Point,
htlcBasepoint: Point,
firstPerCommitmentPoint: Point,
channelFlags: Byte) extends ChannelMessage with HasTemporaryChannelId
case class AcceptChannel(temporaryChannelId: BinaryData,
dustLimitSatoshis: Long,
maxHtlcValueInFlightMsat: UInt64,
channelReserveSatoshis: Long,
htlcMinimumMsat: Long,
minimumDepth: Long,
toSelfDelay: Int,
maxAcceptedHtlcs: Int,
fundingPubkey: PublicKey,
revocationBasepoint: Point,
paymentBasepoint: Point,
delayedPaymentBasepoint: Point,
htlcBasepoint: Point,
firstPerCommitmentPoint: Point) extends ChannelMessage with HasTemporaryChannelId
case class FundingCreated(temporaryChannelId: BinaryData,
fundingTxid: BinaryData,
fundingOutputIndex: Int,
signature: BinaryData) extends ChannelMessage with HasTemporaryChannelId
case class FundingSigned(channelId: BinaryData,
signature: BinaryData) extends ChannelMessage with HasChannelId
case class FundingLocked(channelId: BinaryData,
nextPerCommitmentPoint: Point) extends ChannelMessage with HasChannelId
case class Shutdown(channelId: BinaryData,
scriptPubKey: BinaryData) extends ChannelMessage with HasChannelId
case class ClosingSigned(channelId: BinaryData,
feeSatoshis: Long,
signature: BinaryData) extends ChannelMessage with HasChannelId
case class UpdateAddHtlc(channelId: BinaryData,
id: Long,
amountMsat: Long,
paymentHash: BinaryData,
expiry: Long,
onionRoutingPacket: BinaryData) extends HtlcMessage with UpdateMessage with HasChannelId
case class UpdateFulfillHtlc(channelId: BinaryData,
id: Long,
paymentPreimage: BinaryData) extends HtlcMessage with UpdateMessage with HasChannelId
case class UpdateFailHtlc(channelId: BinaryData,
id: Long,
reason: BinaryData) extends HtlcMessage with UpdateMessage with HasChannelId
case class UpdateFailMalformedHtlc(channelId: BinaryData,
id: Long,
onionHash: BinaryData,
failureCode: Int) extends HtlcMessage with UpdateMessage with HasChannelId
case class CommitSig(channelId: BinaryData,
signature: BinaryData,
htlcSignatures: List[BinaryData]) extends HtlcMessage with HasChannelId
case class RevokeAndAck(channelId: BinaryData,
perCommitmentSecret: Scalar,
nextPerCommitmentPoint: Point) extends HtlcMessage with HasChannelId
case class UpdateFee(channelId: BinaryData,
feeratePerKw: Long) extends ChannelMessage with UpdateMessage with HasChannelId
case class AnnouncementSignatures(channelId: BinaryData,
shortChannelId: Long,
nodeSignature: BinaryData,
bitcoinSignature: BinaryData) extends RoutingMessage with HasChannelId
case class ChannelAnnouncement(nodeSignature1: BinaryData,
nodeSignature2: BinaryData,
bitcoinSignature1: BinaryData,
bitcoinSignature2: BinaryData,
features: BinaryData,
chainHash: BinaryData,
shortChannelId: Long,
nodeId1: PublicKey,
nodeId2: PublicKey,
bitcoinKey1: PublicKey,
bitcoinKey2: PublicKey) extends RoutingMessage
case class NodeAnnouncement(signature: BinaryData,
features: BinaryData,
timestamp: Long,
nodeId: PublicKey,
rgbColor: (Byte, Byte, Byte),
alias: String,
// TODO: check address order + support padding data (type 0)
addresses: List[InetSocketAddress]) extends RoutingMessage
case class ChannelUpdate(signature: BinaryData,
chainHash: BinaryData,
shortChannelId: Long,
timestamp: Long,
flags: BinaryData,
cltvExpiryDelta: Int,
htlcMinimumMsat: Long,
feeBaseMsat: Long,
feeProportionalMillionths: Long) extends RoutingMessage
case class PerHopPayload(channel_id: Long,
amtToForward: Long,
outgoingCltvValue: Int)

View File

@ -0,0 +1,519 @@
package fr.acinq.eclair.crypto;
/*
* Copyright (C) 2016 Southern Storm Software, Pty Ltd.
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included
* in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*/
import java.util.Arrays;
/**
* Implementation of the Curve25519 elliptic curve algorithm.
* <p>
* This implementation is based on that from arduinolibs:
* https://github.com/rweather/arduinolibs
* <p>
* Differences in this version are due to using 26-bit limbs for the
* representation instead of the 8/16/32-bit limbs in the original.
* <p>
* References: http://cr.yp.to/ecdh.html, RFC 7748
*/
public final class Curve25519 {
// Numbers modulo 2^255 - 19 are broken up into ten 26-bit words.
private static final int NUM_LIMBS_255BIT = 10;
private static final int NUM_LIMBS_510BIT = 20;
private int[] x_1;
private int[] x_2;
private int[] x_3;
private int[] z_2;
private int[] z_3;
private int[] A;
private int[] B;
private int[] C;
private int[] D;
private int[] E;
private int[] AA;
private int[] BB;
private int[] DA;
private int[] CB;
private long[] t1;
private int[] t2;
/**
* Constructs the temporary state holder for Curve25519 evaluation.
*/
private Curve25519() {
// Allocate memory for all of the temporary variables we will need.
x_1 = new int[NUM_LIMBS_255BIT];
x_2 = new int[NUM_LIMBS_255BIT];
x_3 = new int[NUM_LIMBS_255BIT];
z_2 = new int[NUM_LIMBS_255BIT];
z_3 = new int[NUM_LIMBS_255BIT];
A = new int[NUM_LIMBS_255BIT];
B = new int[NUM_LIMBS_255BIT];
C = new int[NUM_LIMBS_255BIT];
D = new int[NUM_LIMBS_255BIT];
E = new int[NUM_LIMBS_255BIT];
AA = new int[NUM_LIMBS_255BIT];
BB = new int[NUM_LIMBS_255BIT];
DA = new int[NUM_LIMBS_255BIT];
CB = new int[NUM_LIMBS_255BIT];
t1 = new long[NUM_LIMBS_510BIT];
t2 = new int[NUM_LIMBS_510BIT];
}
/**
* Destroy all sensitive data in this object.
*/
private void destroy() {
// Destroy all temporary variables.
Arrays.fill(x_1, 0);
Arrays.fill(x_2, 0);
Arrays.fill(x_3, 0);
Arrays.fill(z_2, 0);
Arrays.fill(z_3, 0);
Arrays.fill(A, 0);
Arrays.fill(B, 0);
Arrays.fill(C, 0);
Arrays.fill(D, 0);
Arrays.fill(E, 0);
Arrays.fill(AA, 0);
Arrays.fill(BB, 0);
Arrays.fill(DA, 0);
Arrays.fill(CB, 0);
Arrays.fill(t1, 0L);
Arrays.fill(t2, 0);
}
/**
* Reduces a number modulo 2^255 - 19 where it is known that the
* number can be reduced with only 1 trial subtraction.
*
* @param x The number to reduce, and the result.
*/
private void reduceQuick(int[] x) {
int index, carry;
// Perform a trial subtraction of (2^255 - 19) from "x" which is
// equivalent to adding 19 and subtracting 2^255. We add 19 here;
// the subtraction of 2^255 occurs in the next step.
carry = 19;
for (index = 0; index < NUM_LIMBS_255BIT; ++index) {
carry += x[index];
t2[index] = carry & 0x03FFFFFF;
carry >>= 26;
}
// If there was a borrow, then the original "x" is the correct answer.
// If there was no borrow, then "t2" is the correct answer. Select the
// correct answer but do it in a way that instruction timing will not
// reveal which value was selected. Borrow will occur if bit 21 of
// "t2" is zero. Turn the bit into a selection mask.
int mask = -((t2[NUM_LIMBS_255BIT - 1] >> 21) & 0x01);
int nmask = ~mask;
t2[NUM_LIMBS_255BIT - 1] &= 0x001FFFFF;
for (index = 0; index < NUM_LIMBS_255BIT; ++index)
x[index] = (x[index] & nmask) | (t2[index] & mask);
}
/**
* Reduce a number modulo 2^255 - 19.
*
* @param result The result.
* @param x The value to be reduced. This array will be
* modified during the reduction.
* @param size The number of limbs in the high order half of x.
*/
private void reduce(int[] result, int[] x, int size) {
int index, limb, carry;
// Calculate (x mod 2^255) + ((x / 2^255) * 19) which will
// either produce the answer we want or it will produce a
// value of the form "answer + j * (2^255 - 19)". There are
// 5 left-over bits in the top-most limb of the bottom half.
carry = 0;
limb = x[NUM_LIMBS_255BIT - 1] >> 21;
x[NUM_LIMBS_255BIT - 1] &= 0x001FFFFF;
for (index = 0; index < size; ++index) {
limb += x[NUM_LIMBS_255BIT + index] << 5;
carry += (limb & 0x03FFFFFF) * 19 + x[index];
x[index] = carry & 0x03FFFFFF;
limb >>= 26;
carry >>= 26;
}
if (size < NUM_LIMBS_255BIT) {
// The high order half of the number is short; e.g. for mulA24().
// Propagate the carry through the rest of the low order part.
for (index = size; index < NUM_LIMBS_255BIT; ++index) {
carry += x[index];
x[index] = carry & 0x03FFFFFF;
carry >>= 26;
}
}
// The "j" value may still be too large due to the final carry-out.
// We must repeat the reduction. If we already have the answer,
// then this won't do any harm but we must still do the calculation
// to preserve the overall timing. The "j" value will be between
// 0 and 19, which means that the carry we care about is in the
// top 5 bits of the highest limb of the bottom half.
carry = (x[NUM_LIMBS_255BIT - 1] >> 21) * 19;
x[NUM_LIMBS_255BIT - 1] &= 0x001FFFFF;
for (index = 0; index < NUM_LIMBS_255BIT; ++index) {
carry += x[index];
result[index] = carry & 0x03FFFFFF;
carry >>= 26;
}
// At this point "x" will either be the answer or it will be the
// answer plus (2^255 - 19). Perform a trial subtraction to
// complete the reduction process.
reduceQuick(result);
}
/**
* Multiplies two numbers modulo 2^255 - 19.
*
* @param result The result.
* @param x The first number to multiply.
* @param y The second number to multiply.
*/
private void mul(int[] result, int[] x, int[] y) {
int i, j;
// Multiply the two numbers to create the intermediate result.
long v = x[0];
for (i = 0; i < NUM_LIMBS_255BIT; ++i) {
t1[i] = v * y[i];
}
for (i = 1; i < NUM_LIMBS_255BIT; ++i) {
v = x[i];
for (j = 0; j < (NUM_LIMBS_255BIT - 1); ++j) {
t1[i + j] += v * y[j];
}
t1[i + NUM_LIMBS_255BIT - 1] = v * y[NUM_LIMBS_255BIT - 1];
}
// Propagate carries and convert back into 26-bit words.
v = t1[0];
t2[0] = ((int) v) & 0x03FFFFFF;
for (i = 1; i < NUM_LIMBS_510BIT; ++i) {
v = (v >> 26) + t1[i];
t2[i] = ((int) v) & 0x03FFFFFF;
}
// Reduce the result modulo 2^255 - 19.
reduce(result, t2, NUM_LIMBS_255BIT);
}
/**
* Squares a number modulo 2^255 - 19.
*
* @param result The result.
* @param x The number to square.
*/
private void square(int[] result, int[] x) {
mul(result, x, x);
}
/**
* Multiplies a number by the a24 constant, modulo 2^255 - 19.
*
* @param result The result.
* @param x The number to multiply by a24.
*/
private void mulA24(int[] result, int[] x) {
long a24 = 121665;
long carry = 0;
int index;
for (index = 0; index < NUM_LIMBS_255BIT; ++index) {
carry += a24 * x[index];
t2[index] = ((int) carry) & 0x03FFFFFF;
carry >>= 26;
}
t2[NUM_LIMBS_255BIT] = ((int) carry) & 0x03FFFFFF;
reduce(result, t2, 1);
}
/**
* Adds two numbers modulo 2^255 - 19.
*
* @param result The result.
* @param x The first number to add.
* @param y The second number to add.
*/
private void add(int[] result, int[] x, int[] y) {
int index, carry;
carry = x[0] + y[0];
result[0] = carry & 0x03FFFFFF;
for (index = 1; index < NUM_LIMBS_255BIT; ++index) {
carry = (carry >> 26) + x[index] + y[index];
result[index] = carry & 0x03FFFFFF;
}
reduceQuick(result);
}
/**
* Subtracts two numbers modulo 2^255 - 19.
*
* @param result The result.
* @param x The first number to subtract.
* @param y The second number to subtract.
*/
private void sub(int[] result, int[] x, int[] y) {
int index, borrow;
// Subtract y from x to generate the intermediate result.
borrow = 0;
for (index = 0; index < NUM_LIMBS_255BIT; ++index) {
borrow = x[index] - y[index] - ((borrow >> 26) & 0x01);
result[index] = borrow & 0x03FFFFFF;
}
// If we had a borrow, then the result has gone negative and we
// have to add 2^255 - 19 to the result to make it positive again.
// The top bits of "borrow" will be all 1's if there is a borrow
// or it will be all 0's if there was no borrow. Easiest is to
// conditionally subtract 19 and then mask off the high bits.
borrow = result[0] - ((-((borrow >> 26) & 0x01)) & 19);
result[0] = borrow & 0x03FFFFFF;
for (index = 1; index < NUM_LIMBS_255BIT; ++index) {
borrow = result[index] - ((borrow >> 26) & 0x01);
result[index] = borrow & 0x03FFFFFF;
}
result[NUM_LIMBS_255BIT - 1] &= 0x001FFFFF;
}
/**
* Conditional swap of two values.
*
* @param select Set to 1 to swap, 0 to leave as-is.
* @param x The first value.
* @param y The second value.
*/
private static void cswap(int select, int[] x, int[] y) {
int dummy;
select = -select;
for (int index = 0; index < NUM_LIMBS_255BIT; ++index) {
dummy = select & (x[index] ^ y[index]);
x[index] ^= dummy;
y[index] ^= dummy;
}
}
/**
* Raise x to the power of (2^250 - 1).
*
* @param result The result. Must not overlap with x.
* @param x The argument.
*/
private void pow250(int[] result, int[] x) {
int i, j;
// The big-endian hexadecimal expansion of (2^250 - 1) is:
// 03FFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF
//
// The naive implementation needs to do 2 multiplications per 1 bit and
// 1 multiplication per 0 bit. We can improve upon this by creating a
// pattern 0000000001 ... 0000000001. If we square and multiply the
// pattern by itself we can turn the pattern into the partial results
// 0000000011 ... 0000000011, 0000000111 ... 0000000111, etc.
// This averages out to about 1.1 multiplications per 1 bit instead of 2.
// Build a pattern of 250 bits in length of repeated copies of 0000000001.
square(A, x);
for (j = 0; j < 9; ++j)
square(A, A);
mul(result, A, x);
for (i = 0; i < 23; ++i) {
for (j = 0; j < 10; ++j)
square(A, A);
mul(result, result, A);
}
// Multiply bit-shifted versions of the 0000000001 pattern into
// the result to "fill in" the gaps in the pattern.
square(A, result);
mul(result, result, A);
for (j = 0; j < 8; ++j) {
square(A, A);
mul(result, result, A);
}
}
/**
* Computes the reciprocal of a number modulo 2^255 - 19.
*
* @param result The result. Must not overlap with x.
* @param x The argument.
*/
private void recip(int[] result, int[] x) {
// The reciprocal is the same as x ^ (p - 2) where p = 2^255 - 19.
// The big-endian hexadecimal expansion of (p - 2) is:
// 7FFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFEB
// Start with the 250 upper bits of the expansion of (p - 2).
pow250(result, x);
// Deal with the 5 lowest bits of (p - 2), 01011, from highest to lowest.
square(result, result);
square(result, result);
mul(result, result, x);
square(result, result);
square(result, result);
mul(result, result, x);
square(result, result);
mul(result, result, x);
}
/**
* Evaluates the curve for every bit in a secret key.
*
* @param s The 32-byte secret key.
*/
private void evalCurve(byte[] s) {
int sposn = 31;
int sbit = 6;
int svalue = s[sposn] | 0x40;
int swap = 0;
int select;
// Iterate over all 255 bits of "s" from the highest to the lowest.
// We ignore the high bit of the 256-bit representation of "s".
for (; ; ) {
// Conditional swaps on entry to this bit but only if we
// didn't swap on the previous bit.
select = (svalue >> sbit) & 0x01;
swap ^= select;
cswap(swap, x_2, x_3);
cswap(swap, z_2, z_3);
swap = select;
// Evaluate the curve.
add(A, x_2, z_2); // A = x_2 + z_2
square(AA, A); // AA = A^2
sub(B, x_2, z_2); // B = x_2 - z_2
square(BB, B); // BB = B^2
sub(E, AA, BB); // E = AA - BB
add(C, x_3, z_3); // C = x_3 + z_3
sub(D, x_3, z_3); // D = x_3 - z_3
mul(DA, D, A); // DA = D * A
mul(CB, C, B); // CB = C * B
add(x_3, DA, CB); // x_3 = (DA + CB)^2
square(x_3, x_3);
sub(z_3, DA, CB); // z_3 = x_1 * (DA - CB)^2
square(z_3, z_3);
mul(z_3, z_3, x_1);
mul(x_2, AA, BB); // x_2 = AA * BB
mulA24(z_2, E); // z_2 = E * (AA + a24 * E)
add(z_2, z_2, AA);
mul(z_2, z_2, E);
// Move onto the next lower bit of "s".
if (sbit > 0) {
--sbit;
} else if (sposn == 0) {
break;
} else if (sposn == 1) {
--sposn;
svalue = s[sposn] & 0xF8;
sbit = 7;
} else {
--sposn;
svalue = s[sposn];
sbit = 7;
}
}
// Final conditional swaps.
cswap(swap, x_2, x_3);
cswap(swap, z_2, z_3);
}
/**
* Evaluates the Curve25519 curve.
*
* @param result Buffer to place the result of the evaluation into.
* @param offset Offset into the result buffer.
* @param privateKey The private key to use in the evaluation.
* @param publicKey The public key to use in the evaluation, or null
* if the base point of the curve should be used.
*/
public static void eval(byte[] result, int offset, byte[] privateKey, byte[] publicKey) {
Curve25519 state = new Curve25519();
try {
// Unpack the public key value. If null, use 9 as the base point.
Arrays.fill(state.x_1, 0);
if (publicKey != null) {
// Convert the input value from little-endian into 26-bit limbs.
for (int index = 0; index < 32; ++index) {
int bit = (index * 8) % 26;
int word = (index * 8) / 26;
int value = publicKey[index] & 0xFF;
if (bit <= (26 - 8)) {
state.x_1[word] |= value << bit;
} else {
state.x_1[word] |= value << bit;
state.x_1[word] &= 0x03FFFFFF;
state.x_1[word + 1] |= value >> (26 - bit);
}
}
// Just in case, we reduce the number modulo 2^255 - 19 to
// make sure that it is in range of the field before we start.
// This eliminates values between 2^255 - 19 and 2^256 - 1.
state.reduceQuick(state.x_1);
state.reduceQuick(state.x_1);
} else {
state.x_1[0] = 9;
}
// Initialize the other temporary variables.
Arrays.fill(state.x_2, 0); // x_2 = 1
state.x_2[0] = 1;
Arrays.fill(state.z_2, 0); // z_2 = 0
System.arraycopy(state.x_1, 0, state.x_3, 0, state.x_1.length); // x_3 = x_1
Arrays.fill(state.z_3, 0); // z_3 = 1
state.z_3[0] = 1;
// Evaluate the curve for every bit of the private key.
state.evalCurve(privateKey);
// Compute x_2 * (z_2 ^ (p - 2)) where p = 2^255 - 19.
state.recip(state.z_3, state.z_2);
state.mul(state.x_2, state.x_2, state.z_3);
// Convert x_2 into little-endian in the result buffer.
for (int index = 0; index < 32; ++index) {
int bit = (index * 8) % 26;
int word = (index * 8) / 26;
if (bit <= (26 - 8))
result[offset + index] = (byte) (state.x_2[word] >> bit);
else
result[offset + index] = (byte) ((state.x_2[word] >> bit) | (state.x_2[word + 1] << (26 - bit)));
}
} finally {
// Clean up all temporary state before we exit.
state.destroy();
}
}
}

View File

@ -0,0 +1,342 @@
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 5 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 0)
remote_htlc_signature = 304402206a6e59f18764a5bf8d4fa45eebc591566689441229c918b480fb2af8cc6a4aeb02205248f273be447684b33e3c8d1d85a8e0ca9fa0bae9ae33f0527ada9c162919a6
# signature for output 1 (htlc 2)
remote_htlc_signature = 3045022100d5275b3619953cb0c3b5aa577f04bc512380e60fa551762ce3d7a1bb7401cff9022037237ab0dac3fe100cde094e82e2bed9ba0ed1bb40154b48e56aa70f259e608b
# signature for output 2 (htlc 1)
remote_htlc_signature = 304402201b63ec807771baf4fdff523c644080de17f1da478989308ad13a58b51db91d360220568939d38c9ce295adba15665fa68f51d967e8ed14a007b751540a80b325f202
# signature for output 3 (htlc 3)
remote_htlc_signature = 3045022100daee1808f9861b6c3ecd14f7b707eca02dd6bdfc714ba2f33bc8cdba507bb182022026654bf8863af77d74f51f4e0b62d461a019561bb12acb120d3f7195d148a554
# signature for output 4 (htlc 4)
remote_htlc_signature = 304402207e0410e45454b0978a623f36a10626ef17b27d9ad44e2760f98cfa3efb37924f0220220bd8acd43ecaa916a80bd4f919c495a2c58982ce7c8625153f8596692a801d
# local_signature = 304402207cb324fa0de88f452ffa9389678127ebcf4cabe1dd848b8e076c1a1962bf34720220116ed922b12311bd602d67e60d2529917f21c5b82f25ff6506c0f87886b4dfd5
output htlc_success_tx 0: 020000000001018154ecccf11a5fb56c39654c4deb4d2296f83c69268280b94d021370c94e219700000000000000000001e8030000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e050047304402206a6e59f18764a5bf8d4fa45eebc591566689441229c918b480fb2af8cc6a4aeb02205248f273be447684b33e3c8d1d85a8e0ca9fa0bae9ae33f0527ada9c162919a60147304402207cb324fa0de88f452ffa9389678127ebcf4cabe1dd848b8e076c1a1962bf34720220116ed922b12311bd602d67e60d2529917f21c5b82f25ff6506c0f87886b4dfd5012000000000000000000000000000000000000000000000000000000000000000008a76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a914b8bcb07f6344b42ab04250c86a6e8b75d3fdbbc688527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f401b175ac686800000000
# local_signature = 3045022100c89172099507ff50f4c925e6c5150e871fb6e83dd73ff9fbb72f6ce829a9633f02203a63821d9162e99f9be712a68f9e589483994feae2661e4546cd5b6cec007be5
output htlc_timeout_tx 2: 020000000001018154ecccf11a5fb56c39654c4deb4d2296f83c69268280b94d021370c94e219701000000000000000001d0070000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100d5275b3619953cb0c3b5aa577f04bc512380e60fa551762ce3d7a1bb7401cff9022037237ab0dac3fe100cde094e82e2bed9ba0ed1bb40154b48e56aa70f259e608b01483045022100c89172099507ff50f4c925e6c5150e871fb6e83dd73ff9fbb72f6ce829a9633f02203a63821d9162e99f9be712a68f9e589483994feae2661e4546cd5b6cec007be501008576a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a914b43e1b38138a41b37f7cd9a1d274bc63e3a9b5d188ac6868f6010000
# local_signature = 3045022100def389deab09cee69eaa1ec14d9428770e45bcbe9feb46468ecf481371165c2f022015d2e3c46600b2ebba8dcc899768874cc6851fd1ecb3fffd15db1cc3de7e10da
output htlc_success_tx 1: 020000000001018154ecccf11a5fb56c39654c4deb4d2296f83c69268280b94d021370c94e219702000000000000000001d0070000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e050047304402201b63ec807771baf4fdff523c644080de17f1da478989308ad13a58b51db91d360220568939d38c9ce295adba15665fa68f51d967e8ed14a007b751540a80b325f20201483045022100def389deab09cee69eaa1ec14d9428770e45bcbe9feb46468ecf481371165c2f022015d2e3c46600b2ebba8dcc899768874cc6851fd1ecb3fffd15db1cc3de7e10da012001010101010101010101010101010101010101010101010101010101010101018a76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a9144b6b2e5444c2639cc0fb7bcea5afba3f3cdce23988527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f501b175ac686800000000
# local_signature = 30440220643aacb19bbb72bd2b635bc3f7375481f5981bace78cdd8319b2988ffcc6704202203d27784ec8ad51ed3bd517a05525a5139bb0b755dd719e0054332d186ac08727
output htlc_timeout_tx 3: 020000000001018154ecccf11a5fb56c39654c4deb4d2296f83c69268280b94d021370c94e219703000000000000000001b80b0000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100daee1808f9861b6c3ecd14f7b707eca02dd6bdfc714ba2f33bc8cdba507bb182022026654bf8863af77d74f51f4e0b62d461a019561bb12acb120d3f7195d148a554014730440220643aacb19bbb72bd2b635bc3f7375481f5981bace78cdd8319b2988ffcc6704202203d27784ec8ad51ed3bd517a05525a5139bb0b755dd719e0054332d186ac0872701008576a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9148a486ff2e31d6158bf39e2608864d63fefd09d5b88ac6868f7010000
# local_signature = 30440220549e80b4496803cbc4a1d09d46df50109f546d43fbbf86cd90b174b1484acd5402205f12a4f995cb9bded597eabfee195a285986aa6d93ae5bb72507ebc6a4e2349e
output htlc_success_tx 4: 020000000001018154ecccf11a5fb56c39654c4deb4d2296f83c69268280b94d021370c94e219704000000000000000001a00f0000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e050047304402207e0410e45454b0978a623f36a10626ef17b27d9ad44e2760f98cfa3efb37924f0220220bd8acd43ecaa916a80bd4f919c495a2c58982ce7c8625153f8596692a801d014730440220549e80b4496803cbc4a1d09d46df50109f546d43fbbf86cd90b174b1484acd5402205f12a4f995cb9bded597eabfee195a285986aa6d93ae5bb72507ebc6a4e2349e012004040404040404040404040404040404040404040404040404040404040404048a76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac686800000000
name: commitment tx with 7 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 0)
remote_htlc_signature = 30440220385a5afe75632f50128cbb029ee95c80156b5b4744beddc729ad339c9ca432c802202ba5f48550cad3379ac75b9b4fedb86a35baa6947f16ba5037fb8b11ab343740
# signature for output 1 (htlc 2)
remote_htlc_signature = 304402207ceb6678d4db33d2401fdc409959e57c16a6cb97a30261d9c61f29b8c58d34b90220084b4a17b4ca0e86f2d798b3698ca52de5621f2ce86f80bed79afa66874511b0
# signature for output 2 (htlc 1)
remote_htlc_signature = 304402206a401b29a0dff0d18ec903502c13d83e7ec019450113f4a7655a4ce40d1f65ba0220217723a084e727b6ca0cc8b6c69c014a7e4a01fcdcba3e3993f462a3c574d833
# signature for output 3 (htlc 3)
remote_htlc_signature = 30450221009b1c987ba599ee3bde1dbca776b85481d70a78b681a8d84206723e2795c7cac002207aac84ad910f8598c4d1c0ea2e3399cf6627a4e3e90131315bc9f038451ce39d
# signature for output 4 (htlc 4)
remote_htlc_signature = 3045022100cc28030b59f0914f45b84caa983b6f8effa900c952310708c2b5b00781117022022027ba2ccdf94d03c6d48b327f183f6e28c8a214d089b9227f94ac4f85315274f0
# local_signature = 304402205999590b8a79fa346e003a68fd40366397119b2b0cdf37b149968d6bc6fbcc4702202b1e1fb5ab7864931caed4e732c359e0fe3d86a548b557be2246efb1708d579a
output htlc_success_tx 0: 020000000001018323148ce2419f21ca3d6780053747715832e18ac780931a514b187768882bb60000000000000000000122020000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e05004730440220385a5afe75632f50128cbb029ee95c80156b5b4744beddc729ad339c9ca432c802202ba5f48550cad3379ac75b9b4fedb86a35baa6947f16ba5037fb8b11ab3437400147304402205999590b8a79fa346e003a68fd40366397119b2b0cdf37b149968d6bc6fbcc4702202b1e1fb5ab7864931caed4e732c359e0fe3d86a548b557be2246efb1708d579a012000000000000000000000000000000000000000000000000000000000000000008a76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a914b8bcb07f6344b42ab04250c86a6e8b75d3fdbbc688527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f401b175ac686800000000
# local_signature = 304402207ff03eb0127fc7c6cae49cc29e2a586b98d1e8969cf4a17dfa50b9c2647720b902205e2ecfda2252956c0ca32f175080e75e4e390e433feb1f8ce9f2ba55648a1dac
output htlc_timeout_tx 2: 020000000001018323148ce2419f21ca3d6780053747715832e18ac780931a514b187768882bb60100000000000000000124060000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e050047304402207ceb6678d4db33d2401fdc409959e57c16a6cb97a30261d9c61f29b8c58d34b90220084b4a17b4ca0e86f2d798b3698ca52de5621f2ce86f80bed79afa66874511b00147304402207ff03eb0127fc7c6cae49cc29e2a586b98d1e8969cf4a17dfa50b9c2647720b902205e2ecfda2252956c0ca32f175080e75e4e390e433feb1f8ce9f2ba55648a1dac01008576a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a914b43e1b38138a41b37f7cd9a1d274bc63e3a9b5d188ac6868f6010000
# local_signature = 3045022100d50d067ca625d54e62df533a8f9291736678d0b86c28a61bb2a80cf42e702d6e02202373dde7e00218eacdafb9415fe0e1071beec1857d1af3c6a201a44cbc47c877
output htlc_success_tx 1: 020000000001018323148ce2419f21ca3d6780053747715832e18ac780931a514b187768882bb6020000000000000000010a060000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e050047304402206a401b29a0dff0d18ec903502c13d83e7ec019450113f4a7655a4ce40d1f65ba0220217723a084e727b6ca0cc8b6c69c014a7e4a01fcdcba3e3993f462a3c574d83301483045022100d50d067ca625d54e62df533a8f9291736678d0b86c28a61bb2a80cf42e702d6e02202373dde7e00218eacdafb9415fe0e1071beec1857d1af3c6a201a44cbc47c877012001010101010101010101010101010101010101010101010101010101010101018a76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a9144b6b2e5444c2639cc0fb7bcea5afba3f3cdce23988527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f501b175ac686800000000
# local_signature = 3045022100db9dc65291077a52728c622987e9895b7241d4394d6dcb916d7600a3e8728c22022036ee3ee717ba0bb5c45ee84bc7bbf85c0f90f26ae4e4a25a6b4241afa8a3f1cb
output htlc_timeout_tx 3: 020000000001018323148ce2419f21ca3d6780053747715832e18ac780931a514b187768882bb6030000000000000000010c0a0000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e05004830450221009b1c987ba599ee3bde1dbca776b85481d70a78b681a8d84206723e2795c7cac002207aac84ad910f8598c4d1c0ea2e3399cf6627a4e3e90131315bc9f038451ce39d01483045022100db9dc65291077a52728c622987e9895b7241d4394d6dcb916d7600a3e8728c22022036ee3ee717ba0bb5c45ee84bc7bbf85c0f90f26ae4e4a25a6b4241afa8a3f1cb01008576a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9148a486ff2e31d6158bf39e2608864d63fefd09d5b88ac6868f7010000
# local_signature = 304402202d1a3c0d31200265d2a2def2753ead4959ae20b4083e19553acfffa5dfab60bf022020ede134149504e15b88ab261a066de49848411e15e70f9e6a5462aec2949f8f
output htlc_success_tx 4: 020000000001018323148ce2419f21ca3d6780053747715832e18ac780931a514b187768882bb604000000000000000001da0d0000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100cc28030b59f0914f45b84caa983b6f8effa900c952310708c2b5b00781117022022027ba2ccdf94d03c6d48b327f183f6e28c8a214d089b9227f94ac4f85315274f00147304402202d1a3c0d31200265d2a2def2753ead4959ae20b4083e19553acfffa5dfab60bf022020ede134149504e15b88ab261a066de49848411e15e70f9e6a5462aec2949f8f012004040404040404040404040404040404040404040404040404040404040404048a76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac686800000000
name: commitment tx with 6 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 2)
remote_htlc_signature = 3044022062ef2e77591409d60d7817d9bb1e71d3c4a2931d1a6c7c8307422c84f001a251022022dad9726b0ae3fe92bda745a06f2c00f92342a186d84518588cf65f4dfaada8
# signature for output 1 (htlc 1)
remote_htlc_signature = 3045022100e968cbbb5f402ed389fdc7f6cd2a80ed650bb42c79aeb2a5678444af94f6c78502204b47a1cb24ab5b0b6fe69fe9cfc7dba07b9dd0d8b95f372c1d9435146a88f8d4
# signature for output 2 (htlc 3)
remote_htlc_signature = 3045022100aa91932e305292cf9969cc23502bbf6cef83a5df39c95ad04a707c4f4fed5c7702207099fc0f3a9bfe1e7683c0e9aa5e76c5432eb20693bf4cb182f04d383dc9c8c2
# signature for output 3 (htlc 4)
remote_htlc_signature = 3044022035cac88040a5bba420b1c4257235d5015309113460bc33f2853cd81ca36e632402202fc94fd3e81e9d34a9d01782a0284f3044370d03d60f3fc041e2da088d2de58f
# local_signature = 3045022100a4c574f00411dd2f978ca5cdc1b848c311cd7849c087ad2f21a5bce5e8cc5ae90220090ae39a9bce2fb8bc879d7e9f9022df249f41e25e51f1a9bf6447a9eeffc098
output htlc_timeout_tx 2: 02000000000101579c183eca9e8236a5d7f5dcd79cfec32c497fdc0ec61533cde99ecd436cadd10000000000000000000123060000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500473044022062ef2e77591409d60d7817d9bb1e71d3c4a2931d1a6c7c8307422c84f001a251022022dad9726b0ae3fe92bda745a06f2c00f92342a186d84518588cf65f4dfaada801483045022100a4c574f00411dd2f978ca5cdc1b848c311cd7849c087ad2f21a5bce5e8cc5ae90220090ae39a9bce2fb8bc879d7e9f9022df249f41e25e51f1a9bf6447a9eeffc09801008576a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a914b43e1b38138a41b37f7cd9a1d274bc63e3a9b5d188ac6868f6010000
# local_signature = 304402207679cf19790bea76a733d2fa0672bd43ab455687a068f815a3d237581f57139a0220683a1a799e102071c206b207735ca80f627ab83d6616b4bcd017c5d79ef3e7d0
output htlc_success_tx 1: 02000000000101579c183eca9e8236a5d7f5dcd79cfec32c497fdc0ec61533cde99ecd436cadd10100000000000000000109060000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100e968cbbb5f402ed389fdc7f6cd2a80ed650bb42c79aeb2a5678444af94f6c78502204b47a1cb24ab5b0b6fe69fe9cfc7dba07b9dd0d8b95f372c1d9435146a88f8d40147304402207679cf19790bea76a733d2fa0672bd43ab455687a068f815a3d237581f57139a0220683a1a799e102071c206b207735ca80f627ab83d6616b4bcd017c5d79ef3e7d0012001010101010101010101010101010101010101010101010101010101010101018a76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a9144b6b2e5444c2639cc0fb7bcea5afba3f3cdce23988527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f501b175ac686800000000
# local_signature = 304402200df76fea718745f3c529bac7fd37923e7309ce38b25c0781e4cf514dd9ef8dc802204172295739dbae9fe0474dcee3608e3433b4b2af3a2e6787108b02f894dcdda3
output htlc_timeout_tx 3: 02000000000101579c183eca9e8236a5d7f5dcd79cfec32c497fdc0ec61533cde99ecd436cadd1020000000000000000010b0a0000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100aa91932e305292cf9969cc23502bbf6cef83a5df39c95ad04a707c4f4fed5c7702207099fc0f3a9bfe1e7683c0e9aa5e76c5432eb20693bf4cb182f04d383dc9c8c20147304402200df76fea718745f3c529bac7fd37923e7309ce38b25c0781e4cf514dd9ef8dc802204172295739dbae9fe0474dcee3608e3433b4b2af3a2e6787108b02f894dcdda301008576a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9148a486ff2e31d6158bf39e2608864d63fefd09d5b88ac6868f7010000
# local_signature = 304402200daf2eb7afd355b4caf6fb08387b5f031940ea29d1a9f35071288a839c9039e4022067201b562456e7948616c13acb876b386b511599b58ac1d94d127f91c50463a6
output htlc_success_tx 4: 02000000000101579c183eca9e8236a5d7f5dcd79cfec32c497fdc0ec61533cde99ecd436cadd103000000000000000001d90d0000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500473044022035cac88040a5bba420b1c4257235d5015309113460bc33f2853cd81ca36e632402202fc94fd3e81e9d34a9d01782a0284f3044370d03d60f3fc041e2da088d2de58f0147304402200daf2eb7afd355b4caf6fb08387b5f031940ea29d1a9f35071288a839c9039e4022067201b562456e7948616c13acb876b386b511599b58ac1d94d127f91c50463a6012004040404040404040404040404040404040404040404040404040404040404048a76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac686800000000
name: commitment tx with 6 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 2)
remote_htlc_signature = 3045022100d1cf354de41c1369336cf85b225ed033f1f8982a01be503668df756a7e668b66022001254144fb4d0eecc61908fccc3388891ba17c5d7a1a8c62bdd307e5a513f992
# signature for output 1 (htlc 1)
remote_htlc_signature = 3045022100d065569dcb94f090345402736385efeb8ea265131804beac06dd84d15dd2d6880220664feb0b4b2eb985fadb6ec7dc58c9334ea88ce599a9be760554a2d4b3b5d9f4
# signature for output 2 (htlc 3)
remote_htlc_signature = 3045022100d4e69d363de993684eae7b37853c40722a4c1b4a7b588ad7b5d8a9b5006137a102207a069c628170ee34be5612747051bdcc087466dbaa68d5756ea81c10155aef18
# signature for output 3 (htlc 4)
remote_htlc_signature = 30450221008ec888e36e4a4b3dc2ed6b823319855b2ae03006ca6ae0d9aa7e24bfc1d6f07102203b0f78885472a67ff4fe5916c0bb669487d659527509516fc3a08e87a2cc0a7c
# local_signature = 3044022056eb1af429660e45a1b0b66568cb8c4a3aa7e4c9c292d5d6c47f86ebf2c8838f022065c3ac4ebe980ca7a41148569be4ad8751b0a724a41405697ec55035dae66402
output htlc_timeout_tx 2: 02000000000101ca94a9ad516ebc0c4bdd7b6254871babfa978d5accafb554214137d398bfcf6a0000000000000000000175020000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100d1cf354de41c1369336cf85b225ed033f1f8982a01be503668df756a7e668b66022001254144fb4d0eecc61908fccc3388891ba17c5d7a1a8c62bdd307e5a513f99201473044022056eb1af429660e45a1b0b66568cb8c4a3aa7e4c9c292d5d6c47f86ebf2c8838f022065c3ac4ebe980ca7a41148569be4ad8751b0a724a41405697ec55035dae6640201008576a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a914b43e1b38138a41b37f7cd9a1d274bc63e3a9b5d188ac6868f6010000
# local_signature = 3045022100914bb232cd4b2690ee3d6cb8c3713c4ac9c4fb925323068d8b07f67c8541f8d9022057152f5f1615b793d2d45aac7518989ae4fe970f28b9b5c77504799d25433f7f
output htlc_success_tx 1: 02000000000101ca94a9ad516ebc0c4bdd7b6254871babfa978d5accafb554214137d398bfcf6a0100000000000000000122020000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100d065569dcb94f090345402736385efeb8ea265131804beac06dd84d15dd2d6880220664feb0b4b2eb985fadb6ec7dc58c9334ea88ce599a9be760554a2d4b3b5d9f401483045022100914bb232cd4b2690ee3d6cb8c3713c4ac9c4fb925323068d8b07f67c8541f8d9022057152f5f1615b793d2d45aac7518989ae4fe970f28b9b5c77504799d25433f7f012001010101010101010101010101010101010101010101010101010101010101018a76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a9144b6b2e5444c2639cc0fb7bcea5afba3f3cdce23988527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f501b175ac686800000000
# local_signature = 304402200e362443f7af830b419771e8e1614fc391db3a4eb799989abfc5ab26d6fcd032022039ab0cad1c14dfbe9446bf847965e56fe016e0cbcf719fd18c1bfbf53ecbd9f9
output htlc_timeout_tx 3: 02000000000101ca94a9ad516ebc0c4bdd7b6254871babfa978d5accafb554214137d398bfcf6a020000000000000000015d060000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100d4e69d363de993684eae7b37853c40722a4c1b4a7b588ad7b5d8a9b5006137a102207a069c628170ee34be5612747051bdcc087466dbaa68d5756ea81c10155aef180147304402200e362443f7af830b419771e8e1614fc391db3a4eb799989abfc5ab26d6fcd032022039ab0cad1c14dfbe9446bf847965e56fe016e0cbcf719fd18c1bfbf53ecbd9f901008576a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9148a486ff2e31d6158bf39e2608864d63fefd09d5b88ac6868f7010000
# local_signature = 304402202c3e14282b84b02705dfd00a6da396c9fe8a8bcb1d3fdb4b20a4feba09440e8b02202b058b39aa9b0c865b22095edcd9ff1f71bbfe20aa4993755e54d042755ed0d5
output htlc_success_tx 4: 02000000000101ca94a9ad516ebc0c4bdd7b6254871babfa978d5accafb554214137d398bfcf6a03000000000000000001f2090000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e05004830450221008ec888e36e4a4b3dc2ed6b823319855b2ae03006ca6ae0d9aa7e24bfc1d6f07102203b0f78885472a67ff4fe5916c0bb669487d659527509516fc3a08e87a2cc0a7c0147304402202c3e14282b84b02705dfd00a6da396c9fe8a8bcb1d3fdb4b20a4feba09440e8b02202b058b39aa9b0c865b22095edcd9ff1f71bbfe20aa4993755e54d042755ed0d5012004040404040404040404040404040404040404040404040404040404040404048a76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac686800000000
name: commitment tx with 5 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 2)
remote_htlc_signature = 3045022100eed143b1ee4bed5dc3cde40afa5db3e7354cbf9c44054b5f713f729356f08cf7022077161d171c2bbd9badf3c9934de65a4918de03bbac1450f715275f75b103f891
# signature for output 1 (htlc 3)
remote_htlc_signature = 3044022071e9357619fd8d29a411dc053b326a5224c5d11268070e88ecb981b174747c7a02202b763ae29a9d0732fa8836dd8597439460b50472183f420021b768981b4f7cf6
# signature for output 2 (htlc 4)
remote_htlc_signature = 3045022100c9458a4d2cbb741705577deb0a890e5cb90ee141be0400d3162e533727c9cb2102206edcf765c5dc5e5f9b976ea8149bf8607b5a0efb30691138e1231302b640d2a4
# local_signature = 3045022100a0d043ed533e7fb1911e0553d31a8e2f3e6de19dbc035257f29d747c5e02f1f5022030cd38d8e84282175d49c1ebe0470db3ebd59768cf40780a784e248a43904fb8
output htlc_timeout_tx 2: 0200000000010140a83ce364747ff277f4d7595d8d15f708418798922c40bc2b056aca5485a2180000000000000000000174020000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100eed143b1ee4bed5dc3cde40afa5db3e7354cbf9c44054b5f713f729356f08cf7022077161d171c2bbd9badf3c9934de65a4918de03bbac1450f715275f75b103f89101483045022100a0d043ed533e7fb1911e0553d31a8e2f3e6de19dbc035257f29d747c5e02f1f5022030cd38d8e84282175d49c1ebe0470db3ebd59768cf40780a784e248a43904fb801008576a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a914b43e1b38138a41b37f7cd9a1d274bc63e3a9b5d188ac6868f6010000
# local_signature = 3045022100adb1d679f65f96178b59f23ed37d3b70443118f345224a07ecb043eee2acc157022034d24524fe857144a3bcfff3065a9994d0a6ec5f11c681e49431d573e242612d
output htlc_timeout_tx 3: 0200000000010140a83ce364747ff277f4d7595d8d15f708418798922c40bc2b056aca5485a218010000000000000000015c060000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500473044022071e9357619fd8d29a411dc053b326a5224c5d11268070e88ecb981b174747c7a02202b763ae29a9d0732fa8836dd8597439460b50472183f420021b768981b4f7cf601483045022100adb1d679f65f96178b59f23ed37d3b70443118f345224a07ecb043eee2acc157022034d24524fe857144a3bcfff3065a9994d0a6ec5f11c681e49431d573e242612d01008576a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9148a486ff2e31d6158bf39e2608864d63fefd09d5b88ac6868f7010000
# local_signature = 304402200831422aa4e1ee6d55e0b894201770a8f8817a189356f2d70be76633ffa6a6f602200dd1b84a4855dc6727dd46c98daae43dfc70889d1ba7ef0087529a57c06e5e04
output htlc_success_tx 4: 0200000000010140a83ce364747ff277f4d7595d8d15f708418798922c40bc2b056aca5485a21802000000000000000001f1090000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100c9458a4d2cbb741705577deb0a890e5cb90ee141be0400d3162e533727c9cb2102206edcf765c5dc5e5f9b976ea8149bf8607b5a0efb30691138e1231302b640d2a40147304402200831422aa4e1ee6d55e0b894201770a8f8817a189356f2d70be76633ffa6a6f602200dd1b84a4855dc6727dd46c98daae43dfc70889d1ba7ef0087529a57c06e5e04012004040404040404040404040404040404040404040404040404040404040404048a76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac686800000000
name: commitment tx with 5 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 2)
remote_htlc_signature = 30450221009ed2f0a67f99e29c3c8cf45c08207b765980697781bb727fe0b1416de0e7622902206052684229bc171419ed290f4b615c943f819c0262414e43c5b91dcf72ddcf44
# signature for output 1 (htlc 3)
remote_htlc_signature = 30440220155d3b90c67c33a8321996a9be5b82431b0c126613be751d400669da9d5c696702204318448bcd48824439d2c6a70be6e5747446be47ff45977cf41672bdc9b6b12d
# signature for output 2 (htlc 4)
remote_htlc_signature = 3045022100a12a9a473ece548584aabdd051779025a5ed4077c4b7aa376ec7a0b1645e5a48022039490b333f53b5b3e2ddde1d809e492cba2b3e5fc3a436cd3ffb4cd3d500fa5a
# local_signature = 3044022004ad5f04ae69c71b3b141d4db9d0d4c38d84009fb3cfeeae6efdad414487a9a0022042d3fe1388c1ff517d1da7fb4025663d372c14728ed52dc88608363450ff6a2f
output htlc_timeout_tx 2: 02000000000101fb824d4e4dafc0f567789dee3a6bce8d411fe80f5563d8cdfdcc7d7e4447d43a0000000000000000000122020000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e05004830450221009ed2f0a67f99e29c3c8cf45c08207b765980697781bb727fe0b1416de0e7622902206052684229bc171419ed290f4b615c943f819c0262414e43c5b91dcf72ddcf4401473044022004ad5f04ae69c71b3b141d4db9d0d4c38d84009fb3cfeeae6efdad414487a9a0022042d3fe1388c1ff517d1da7fb4025663d372c14728ed52dc88608363450ff6a2f01008576a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a914b43e1b38138a41b37f7cd9a1d274bc63e3a9b5d188ac6868f6010000
# local_signature = 304402201707050c870c1f77cc3ed58d6d71bf281de239e9eabd8ef0955bad0d7fe38dcc02204d36d80d0019b3a71e646a08fa4a5607761d341ae8be371946ebe437c289c915
output htlc_timeout_tx 3: 02000000000101fb824d4e4dafc0f567789dee3a6bce8d411fe80f5563d8cdfdcc7d7e4447d43a010000000000000000010a060000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e05004730440220155d3b90c67c33a8321996a9be5b82431b0c126613be751d400669da9d5c696702204318448bcd48824439d2c6a70be6e5747446be47ff45977cf41672bdc9b6b12d0147304402201707050c870c1f77cc3ed58d6d71bf281de239e9eabd8ef0955bad0d7fe38dcc02204d36d80d0019b3a71e646a08fa4a5607761d341ae8be371946ebe437c289c91501008576a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9148a486ff2e31d6158bf39e2608864d63fefd09d5b88ac6868f7010000
# local_signature = 3045022100ff200bc934ab26ce9a559e998ceb0aee53bc40368e114ab9d3054d9960546e2802202496856ca163ac12c143110b6b3ac9d598df7254f2e17b3b94c3ab5301f4c3b0
output htlc_success_tx 4: 02000000000101fb824d4e4dafc0f567789dee3a6bce8d411fe80f5563d8cdfdcc7d7e4447d43a020000000000000000019a090000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100a12a9a473ece548584aabdd051779025a5ed4077c4b7aa376ec7a0b1645e5a48022039490b333f53b5b3e2ddde1d809e492cba2b3e5fc3a436cd3ffb4cd3d500fa5a01483045022100ff200bc934ab26ce9a559e998ceb0aee53bc40368e114ab9d3054d9960546e2802202496856ca163ac12c143110b6b3ac9d598df7254f2e17b3b94c3ab5301f4c3b0012004040404040404040404040404040404040404040404040404040404040404048a76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac686800000000
name: commitment tx with 4 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 3)
remote_htlc_signature = 3045022100a8a78fa1016a5c5c3704f2e8908715a3cef66723fb95f3132ec4d2d05cd84fb4022025ac49287b0861ec21932405f5600cbce94313dbde0e6c5d5af1b3366d8afbfc
# signature for output 1 (htlc 4)
remote_htlc_signature = 3045022100e769cb156aa2f7515d126cef7a69968629620ce82afcaa9e210969de6850df4602200b16b3f3486a229a48aadde520dbee31ae340dbadaffae74fbb56681fef27b92
# local_signature = 3045022100be6ae1977fd7b630a53623f3f25c542317ccfc2b971782802a4f1ef538eb22b402207edc4d0408f8f38fd3c7365d1cfc26511b7cd2d4fecd8b005fba3cd5bc704390
output htlc_timeout_tx 3: 020000000001014e16c488fa158431c1a82e8f661240ec0a71ba0ce92f2721a6538c510226ad5c0000000000000000000109060000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100a8a78fa1016a5c5c3704f2e8908715a3cef66723fb95f3132ec4d2d05cd84fb4022025ac49287b0861ec21932405f5600cbce94313dbde0e6c5d5af1b3366d8afbfc01483045022100be6ae1977fd7b630a53623f3f25c542317ccfc2b971782802a4f1ef538eb22b402207edc4d0408f8f38fd3c7365d1cfc26511b7cd2d4fecd8b005fba3cd5bc70439001008576a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9148a486ff2e31d6158bf39e2608864d63fefd09d5b88ac6868f7010000
# local_signature = 30440220665b9cb4a978c09d1ca8977a534999bc8a49da624d0c5439451dd69cde1a003d022070eae0620f01f3c1bd029cc1488da13fb40fdab76f396ccd335479a11c5276d8
output htlc_success_tx 4: 020000000001014e16c488fa158431c1a82e8f661240ec0a71ba0ce92f2721a6538c510226ad5c0100000000000000000199090000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100e769cb156aa2f7515d126cef7a69968629620ce82afcaa9e210969de6850df4602200b16b3f3486a229a48aadde520dbee31ae340dbadaffae74fbb56681fef27b92014730440220665b9cb4a978c09d1ca8977a534999bc8a49da624d0c5439451dd69cde1a003d022070eae0620f01f3c1bd029cc1488da13fb40fdab76f396ccd335479a11c5276d8012004040404040404040404040404040404040404040404040404040404040404048a76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac686800000000
name: commitment tx with 4 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 3)
remote_htlc_signature = 3045022100dfb73b4fe961b31a859b2bb1f4f15cabab9265016dd0272323dc6a9e85885c54022059a7b87c02861ee70662907f25ce11597d7b68d3399443a831ae40e777b76bdb
# signature for output 1 (htlc 4)
remote_htlc_signature = 3045022100ea9dc2a7c3c3640334dab733bb4e036e32a3106dc707b24227874fa4f7da746802204d672f7ac0fe765931a8df10b81e53a3242dd32bd9dc9331eb4a596da87954e9
# local_signature = 304402202765b9c9ece4f127fa5407faf66da4c5ce2719cdbe47cd3175fc7d48b482e43d02205605125925e07bad1e41c618a4b434d72c88a164981c4b8af5eaf4ee9142ec3a
output htlc_timeout_tx 3: 02000000000101b8de11eb51c22498fe39722c7227b6e55ff1a94146cf638458cb9bc6a060d3a30000000000000000000122020000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100dfb73b4fe961b31a859b2bb1f4f15cabab9265016dd0272323dc6a9e85885c54022059a7b87c02861ee70662907f25ce11597d7b68d3399443a831ae40e777b76bdb0147304402202765b9c9ece4f127fa5407faf66da4c5ce2719cdbe47cd3175fc7d48b482e43d02205605125925e07bad1e41c618a4b434d72c88a164981c4b8af5eaf4ee9142ec3a01008576a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9148a486ff2e31d6158bf39e2608864d63fefd09d5b88ac6868f7010000
# local_signature = 30440220048a41c660c4841693de037d00a407810389f4574b3286afb7bc392a438fa3f802200401d71fa87c64fe621b49ac07e3bf85157ac680acb977124da28652cc7f1a5c
output htlc_success_tx 4: 02000000000101b8de11eb51c22498fe39722c7227b6e55ff1a94146cf638458cb9bc6a060d3a30100000000000000000176050000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100ea9dc2a7c3c3640334dab733bb4e036e32a3106dc707b24227874fa4f7da746802204d672f7ac0fe765931a8df10b81e53a3242dd32bd9dc9331eb4a596da87954e9014730440220048a41c660c4841693de037d00a407810389f4574b3286afb7bc392a438fa3f802200401d71fa87c64fe621b49ac07e3bf85157ac680acb977124da28652cc7f1a5c012004040404040404040404040404040404040404040404040404040404040404048a76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac686800000000
name: commitment tx with 3 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 4)
remote_htlc_signature = 3044022044f65cf833afdcb9d18795ca93f7230005777662539815b8a601eeb3e57129a902206a4bf3e53392affbba52640627defa8dc8af61c958c9e827b2798ab45828abdd
# local_signature = 3045022100b94d931a811b32eeb885c28ddcf999ae1981893b21dd1329929543fe87ce793002206370107fdd151c5f2384f9ceb71b3107c69c74c8ed5a28a94a4ab2d27d3b0724
output htlc_success_tx 4: 020000000001011c076aa7fb3d7460d10df69432c904227ea84bbf3134d4ceee5fb0f135ef206d0000000000000000000175050000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500473044022044f65cf833afdcb9d18795ca93f7230005777662539815b8a601eeb3e57129a902206a4bf3e53392affbba52640627defa8dc8af61c958c9e827b2798ab45828abdd01483045022100b94d931a811b32eeb885c28ddcf999ae1981893b21dd1329929543fe87ce793002206370107fdd151c5f2384f9ceb71b3107c69c74c8ed5a28a94a4ab2d27d3b0724012004040404040404040404040404040404040404040404040404040404040404048a76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac686800000000
name: commitment tx with 3 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 4)
remote_htlc_signature = 3045022100fcb38506bfa11c02874092a843d0cc0a8613c23b639832564a5f69020cb0f6ba02206508b9e91eaa001425c190c68ee5f887e1ad5b1b314002e74db9dbd9e42dbecf
# local_signature = 304502210086e76b460ddd3cea10525fba298405d3fe11383e56966a5091811368362f689a02200f72ee75657915e0ede89c28709acd113ede9e1b7be520e3bc5cda425ecd6e68
output htlc_success_tx 4: 0200000000010110a3fdcbcd5db477cd3ad465e7f501ffa8c437e8301f00a6061138590add757f0000000000000000000122020000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100fcb38506bfa11c02874092a843d0cc0a8613c23b639832564a5f69020cb0f6ba02206508b9e91eaa001425c190c68ee5f887e1ad5b1b314002e74db9dbd9e42dbecf0148304502210086e76b460ddd3cea10525fba298405d3fe11383e56966a5091811368362f689a02200f72ee75657915e0ede89c28709acd113ede9e1b7be520e3bc5cda425ecd6e68012004040404040404040404040404040404040404040404040404040404040404048a76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac686800000000
name: commitment tx with 2 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 2 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 1 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

View File

@ -0,0 +1,10 @@
regtest=1
server=1
port=28333
rpcport=28332
rpcuser=foo
rpcpassword=bar
txindex=1
zmqpubrawblock=tcp://127.0.0.1:28334
zmqpubrawtx=tcp://127.0.0.1:28334
rpcworkqueue=64

View File

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" debug="false">
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<target>System.out</target>
<encoder>
<pattern>%date{HH:mm:ss.SSS} %highlight(%-5level) %X{akkaSource} - %msg%ex{12}%n</pattern>
</encoder>
</appender>
<!--appender name="CONSOLEWARN" class="ch.qos.logback.core.ConsoleAppender">
<target>System.out</target>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>WARN</level>
</filter>
<encoder>
<pattern>%-5level %X{akkaSource} - %msg%ex{12}%n</pattern>
</encoder>
</appender-->
<!--appender name="FILE" class="ch.qos.logback.core.FileAppender">
<file>eclair.log</file>
<append>false</append>
<encoder>
<pattern>%-5level %X{akkaSource} - %msg%ex{12}%n</pattern>
</encoder>
</appender-->
<!--logger name="fr.acinq.eclair.channel" level="DEBUG"/-->
<root level="INFO">
<!--appender-ref ref="FILE"/>
<appender-ref ref="CONSOLEWARN"/-->
<appender-ref ref="CONSOLE"/>
</root>
</configuration>

View File

@ -1,6 +1,6 @@
# Simple test that we can commit an HTLC
# Initial state: A=1000000 sat, B=0, both fee rates=10000 sat
A:offer 1,1000000,9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08
# Initial state: A=1000000 sat, B=1000000 sat, both fee rates=10000 sat
A:offer 1000000,9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08
B:recvoffer
A:commit
B:recvcommit

Some files were not shown because too many files have changed in this diff Show More