1
0
Fork 0
mirror of https://github.com/ACINQ/eclair.git synced 2025-02-22 06:21:42 +01:00

Merge branch 'wip-bolt7' into wip-bolts

This commit is contained in:
pm47 2017-02-01 19:13:23 +01:00
commit 5efa71eb5d
94 changed files with 1975 additions and 2331 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View file

@ -82,26 +82,6 @@
<version>${bitcoinlib.version}</version>
</dependency>
<!-- SERIALIZATION -->
<dependency>
<groupId>fr.acinq.eclair</groupId>
<artifactId>lightning-types_${scala.version.short}</artifactId>
<version>${project.parent.version}</version>
</dependency>
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
<version>2.6.1</version>
</dependency>
<dependency>
<groupId>com.trueaccord.scalapb</groupId>
<artifactId>scalapb-runtime_${scala.version.short}</artifactId>
<version>${scalapb.version}</version>
</dependency>
<dependency>
<groupId>com.trueaccord.lenses</groupId>
<artifactId>lenses_${scala.version.short}</artifactId>
<version>0.4</version>
</dependency>
<dependency>
<groupId>org.scodec</groupId>
<artifactId>scodec-core_${scala.version.short}</artifactId>
@ -125,20 +105,15 @@
<version>2.5.10</version>
</dependency>
<!-- OTHER -->
<dependency>
<groupId>org.kitteh.irc</groupId>
<artifactId>client-lib</artifactId>
<version>2.2.0</version>
</dependency>
<dependency>
<groupId>org.jgrapht</groupId>
<artifactId>jgrapht-core</artifactId>
<version>0.9.2</version>
<version>1.0.1</version>
</dependency>
<dependency>
<groupId>org.jgrapht</groupId>
<artifactId>jgrapht-ext</artifactId>
<version>0.9.2</version>
<version>1.0.1</version>
</dependency>
<dependency>
<!-- This is to get rid of '[WARNING] warning: Class javax.annotation.Nonnull not found - continuing with a stub.' compile errors -->

View file

@ -17,11 +17,19 @@ eclair {
}
node {
seed = 0102030405060708010203040506070801020304050607080102030405060708
alias = "eclair"
color {
r = 73
g = 218
b = 170
}
}
delay-blocks = 144
mindepth-blocks = 3
base-fee = 546000
proportional-fee = 10
expiry-delta-blocks = 144
htlc-minimum-msat = 1000000
fee-base-msat = 546000
fee-proportional-msat = 10
payment-handler = "local"
}
akka {

View file

@ -4,6 +4,8 @@
-fx-padding: 1em;
}
/* ------------- TEXT FIELD ------------- */
.text-field, .text-area {
-fx-padding: .6em;
-fx-background-insets: 0;
@ -12,16 +14,33 @@
-fx-background-radius: 0;
-fx-background-color: rgb(249,249,249);
}
.text-field:focused {
-fx-border-color: rgb(63,179,234);
}
/* ------------- TEXT AREA ------------- */
.ta.text-area {
-fx-border-width: 1px;
-fx-padding: .4em .2em;
-fx-font-family: "Consolas", "Lucida Console", Monaco, monospace;
}
.ta.text-area *.scroll-pane > *.viewport,
.ta.text-area *.scroll-pane > *.viewport *.content {
-fx-background-color: transparent;
}
.text-field:focused {
-fx-border-color: rgb(63,179,234);
/* ------------- NOT EDITABLE ------------- */
/* Java FX text can only be selected if in a TextField or TextArea */
/* Make it look like a standard label with the editable = false prop and a special styling */
.text-area.noteditable,
.text-field.noteditable,
.text-field.noteditable:hover,
.text-field.noteditable:focused,
.noteditable {
-fx-background-color: transparent;
-fx-border-width: 0;
-fx-border-color: transparent;
-fx-padding: 0;
}

View file

@ -21,13 +21,17 @@
-fx-background-radius: 0;
-fx-background-color: rgb(63,179,234);
-fx-cursor: hand;
-fx-border-color: rgb(63,179,234);
-fx-border-width: 1px;
}
.button:hover,
.button:focused {
-fx-background-color: rgb(24,158,222);
-fx-border-color: rgb(24,158,222);
}
.button:pressed {
-fx-background-color: rgb(0,131,195);
-fx-border-color: rgb(0,131,195);
}
.button .text {
-fx-fill: white;
@ -57,10 +61,12 @@
}
.button.grey {
-fx-background-color: rgb(230,230,230);
-fx-border-color: rgb(230,230,230);
}
.button.grey:hover,
.button.grey:pressed {
-fx-background-color: rgb(220,220,220);
-fx-border-color: rgb(220,220,220);
}
.button.grey .text {
-fx-fill: rgb(95,95,95);
@ -76,4 +82,23 @@
}
.text-error.text-error-upward {
-fx-translate-y: -6px;
}
}
.label-description {
-fx-text-fill: rgb(156,159,161);
-fx-font-size: 10px;
}
.context-menu {
-fx-padding: 4px;
-fx-font-weight: normal;
}
.context-menu .menu-item:focused {
-fx-background-color: rgb(63,179,234);
}
.context-menu .menu-item:focused .label {
-fx-text-fill: white;
}
.context-menu .separator {
-fx-padding: 2px 0;
}

View file

@ -18,12 +18,14 @@
-fx-background-color: white;
-fx-background-image: url("../commons/images/close.png");
-fx-background-repeat: no-repeat;
-fx-background-size: .7em .7em;
-fx-background-position: 1em center;
-fx-padding: .5em 1em .5em 2.2em;
-fx-background-size: 5px;
-fx-background-position: 8px center;
-fx-border-color: transparent;
-fx-padding: 0px 5px 0px 17px;
}
.button.close-channel:hover {
-fx-background-color: rgb(249,243,243);
-fx-border-color: rgb(249,243,243);
}
.button.close-channel .text {

View file

@ -3,43 +3,47 @@
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import java.net.URL?>
<VBox styleClass="channel" xmlns="http://javafx.com/javafx/8.0.60" xmlns:fx="http://javafx.com/fxml/1">
<VBox styleClass="channel" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1"
onContextMenuRequested="#openChannelContext" onMouseClicked="#closeChannelContext">
<children>
<GridPane maxWidth="1.7976931348623157E308" styleClass="grid">
<GridPane styleClass="grid" prefWidth="400.0">
<columnConstraints>
<ColumnConstraints hgrow="SOMETIMES" maxWidth="125.0" minWidth="10.0" prefWidth="125.0"/>
<ColumnConstraints hgrow="SOMETIMES" maxWidth="114.0" minWidth="10.0" prefWidth="114.0"/>
<ColumnConstraints hgrow="SOMETIMES" maxWidth="95.0" minWidth="10.0" prefWidth="70.0"/>
<ColumnConstraints hgrow="SOMETIMES" minWidth="10.0" prefWidth="100.0"/>
<ColumnConstraints hgrow="SOMETIMES" minWidth="10.0" prefWidth="100.0"/>
<ColumnConstraints hgrow="SOMETIMES" minWidth="100.0" prefWidth="140.0" maxWidth="140.0"/>
<ColumnConstraints hgrow="SOMETIMES" minWidth="10.0" prefWidth="30.0" />
<ColumnConstraints hgrow="SOMETIMES" minWidth="40.0" prefWidth="60.0" maxWidth="60.0"/>
<ColumnConstraints hgrow="SOMETIMES" minWidth="10.0" prefWidth="40.0" />
<ColumnConstraints hgrow="SOMETIMES" minWidth="10.0" prefWidth="15.0"/>
</columnConstraints>
<rowConstraints>
<RowConstraints minHeight="10.0" vgrow="SOMETIMES"/>
<RowConstraints minHeight="8.0" vgrow="SOMETIMES"/>
<RowConstraints minHeight="4.0" vgrow="SOMETIMES"/>
<RowConstraints minHeight="10.0" vgrow="SOMETIMES"/>
<RowConstraints minHeight="10.0" vgrow="SOMETIMES"/>
<RowConstraints minHeight="10.0" vgrow="SOMETIMES"/>
</rowConstraints>
<children>
<Label styleClass="text-muted" text="Node id" GridPane.rowIndex="2"/>
<Label styleClass="text-muted" text="Capacity (millibits)" GridPane.rowIndex="4"/>
<Label styleClass="text-muted" text="Capacity (milliBTC)" GridPane.rowIndex="4"/>
<Label styleClass="text-muted" text="Funder" GridPane.columnIndex="2" GridPane.rowIndex="3"/>
<Label styleClass="text-muted" text="State" GridPane.columnIndex="2" GridPane.rowIndex="4"/>
<Label styleClass="text-muted" text="Your balance (milliBTC)" GridPane.rowIndex="3"/>
<Button fx:id="close" mnemonicParsing="false" styleClass="close-channel" text="Close"
GridPane.columnIndex="4" GridPane.halignment="RIGHT"/>
<Label fx:id="nodeId" onContextMenuRequested="#handleTheirNodeIdContext" text="N/A"
GridPane.columnIndex="1" GridPane.columnSpan="4" GridPane.rowIndex="2"/>
<Label fx:id="capacity" text="N/A" GridPane.columnIndex="1" GridPane.rowIndex="4"/>
<Label fx:id="funder" text="N/A" GridPane.columnIndex="3" GridPane.columnSpan="2"
GridPane.rowIndex="3"/>
<Label fx:id="state" text="N/A" GridPane.columnIndex="3" GridPane.columnSpan="2" GridPane.rowIndex="4"/>
<Label fx:id="channelId" onContextMenuRequested="#handleChannelIdContext" styleClass="text-strong"
text="N/A" GridPane.columnSpan="4"/>
<Label fx:id="amountUs" prefWidth="100.0" text="N/A" GridPane.columnIndex="1" GridPane.rowIndex="3"/>
<ProgressBar fx:id="balanceBar" maxWidth="1.7976931348623157E308" minHeight="5.0" prefHeight="8.0"
prefWidth="1045.0" progress="0.0" snapToPixel="false" GridPane.columnSpan="5"
<TextField fx:id="nodeId" text="N/A" editable="false" styleClass="noteditable"
GridPane.columnIndex="1" GridPane.columnSpan="4" GridPane.rowIndex="2"/>
<TextField fx:id="capacity" text="N/A" editable="false" styleClass="noteditable"
GridPane.columnIndex="1" GridPane.rowIndex="4"/>
<TextField fx:id="funder" text="N/A" editable="false" styleClass="noteditable"
GridPane.columnIndex="3" GridPane.columnSpan="2" GridPane.rowIndex="3"/>
<TextField fx:id="state" text="N/A" editable="false" styleClass="noteditable"
GridPane.columnIndex="3" GridPane.columnSpan="2" GridPane.rowIndex="4"/>
<TextField fx:id="channelId" text="N/A" editable="false" styleClass="noteditable, text-strong"
GridPane.columnSpan="4"/>
<TextField fx:id="amountUs" text="N/A" editable="false" styleClass="noteditable"
GridPane.columnIndex="1" GridPane.rowIndex="3"/>
<ProgressBar fx:id="balanceBar" minHeight="4.0" prefHeight="4.0" maxWidth="1.7976931348623157E308"
progress="0.0" snapToPixel="false" GridPane.columnSpan="5"
GridPane.hgrow="ALWAYS" GridPane.rowIndex="1"/>
<Label styleClass="text-muted" text="Your balance (millibits)" GridPane.rowIndex="3"/>
</children>
</GridPane>
<HBox styleClass="channel-separator"/>

View file

@ -74,5 +74,5 @@
}
.tab-content-area {
-fx-background-color: white;
-fx-padding: .5em 1em;
-fx-padding: .5em 1em 0;
}

View file

@ -3,16 +3,16 @@
<?import javafx.scene.control.*?>
<?import javafx.scene.image.Image?>
<?import javafx.scene.image.ImageView?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.layout.*?>
<?import java.lang.String?>
<?import java.net.URL?>
<BorderPane fx:id="root" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity"
prefHeight="346.0" prefWidth="650.0" xmlns="http://javafx.com/javafx/8.0.60"
<BorderPane fx:id="root" maxHeight="-Infinity" maxWidth="-Infinity" minWidth="-Infinity"
prefHeight="400.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/8"
xmlns:fx="http://javafx.com/fxml/1">
<center>
<TabPane prefHeight="250.0" tabClosingPolicy="UNAVAILABLE" BorderPane.alignment="CENTER">
<TabPane tabClosingPolicy="UNAVAILABLE" BorderPane.alignment="CENTER">
<tabs>
<Tab closable="false" text="Channels">
<Tab fx:id="channelsTab" closable="false" text="Channels">
<content>
<StackPane>
<children>
@ -51,7 +51,7 @@
<RowConstraints minHeight="10.0" vgrow="SOMETIMES"/>
</rowConstraints>
<children>
<HBox alignment="CENTER_LEFT">
<HBox alignment="CENTER_LEFT" onContextMenuRequested="#openNodeIdContext" onMouseClicked="#closeNodeIdContext">
<children>
<ImageView fitHeight="16.0" fitWidth="27.0" opacity="0.52" pickOnBounds="true"
preserveRatio="true">
@ -59,7 +59,7 @@
<Image url="@../commons/images/eclair-shape.png"/>
</image>
</ImageView>
<Label fx:id="labelNodeId" onContextMenuRequested="#handleNodeIdContext" text="N/A"/>
<Label fx:id="labelNodeId" text="N/A"/>
</children>
</HBox>
<HBox alignment="CENTER_RIGHT" GridPane.columnIndex="1">

View file

@ -7,7 +7,7 @@
<?import java.lang.*?>
<?import java.net.URL?>
<GridPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity"
xmlns="http://javafx.com/javafx/8.0.60" xmlns:fx="http://javafx.com/fxml/1">
xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1">
<columnConstraints>
<ColumnConstraints halignment="LEFT" hgrow="SOMETIMES" minWidth="10.0"/>
<ColumnConstraints halignment="LEFT" hgrow="SOMETIMES" minWidth="10.0"/>

View file

@ -10,11 +10,13 @@
<?import javafx.scene.layout.ColumnConstraints?>
<?import javafx.scene.layout.GridPane?>
<?import javafx.scene.layout.RowConstraints?>
<?import javafx.scene.layout.VBox?>
<GridPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="216.0" prefWidth="982.0" styleClass="grid" xmlns="http://javafx.com/javafx/8.0.121" xmlns:fx="http://javafx.com/fxml/1">
<GridPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefWidth="528.0"
styleClass="grid" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1">
<columnConstraints>
<ColumnConstraints halignment="RIGHT" hgrow="SOMETIMES" maxWidth="358.0" minWidth="10.0" prefWidth="137.0" />
<ColumnConstraints hgrow="SOMETIMES" maxWidth="618.0" minWidth="10.0" prefWidth="618.0" />
<ColumnConstraints hgrow="SOMETIMES" maxWidth="258.0" minWidth="10.0" prefWidth="157.0" halignment="RIGHT" />
<ColumnConstraints hgrow="SOMETIMES" maxWidth="263.0" minWidth="10.0" prefWidth="176.0" />
<ColumnConstraints hgrow="SOMETIMES" maxWidth="171.0" minWidth="10.0" prefWidth="145.0" />
</columnConstraints>
<rowConstraints>
@ -22,11 +24,13 @@
<RowConstraints vgrow="SOMETIMES" />
<RowConstraints minHeight="1.0" prefHeight="3.0" vgrow="SOMETIMES" />
<RowConstraints vgrow="SOMETIMES" />
<RowConstraints minHeight="1.0" prefHeight="3.0" vgrow="SOMETIMES" />
<RowConstraints vgrow="SOMETIMES" />
<RowConstraints minHeight="10.0" prefHeight="50.0" vgrow="SOMETIMES" />
</rowConstraints>
<children>
<TextField fx:id="host" prefHeight="38.0" prefWidth="742.0" promptText="pubkey@host:port1" GridPane.columnIndex="1" GridPane.columnSpan="2" GridPane.rowIndex="1" />
<TextField fx:id="amount" prefHeight="31.0" prefWidth="123.0" GridPane.columnIndex="1" GridPane.rowIndex="3" />
<TextField fx:id="host" prefWidth="313.0" promptText="pubkey@host:port1" GridPane.columnIndex="1" GridPane.columnSpan="2" GridPane.rowIndex="1" />
<TextField fx:id="fundingSatoshis" prefWidth="313.0" GridPane.columnIndex="1" GridPane.rowIndex="3" />
<ComboBox fx:id="unit" prefWidth="150.0" GridPane.columnIndex="2" GridPane.rowIndex="3">
<items>
<FXCollections fx:factory="observableArrayList">
@ -36,12 +40,35 @@
</FXCollections>
</items>
</ComboBox>
<Button fx:id="button" defaultButton="true" mnemonicParsing="false" onAction="#handleOpen" text="Connect" GridPane.columnIndex="1" GridPane.rowIndex="4" GridPane.valignment="BOTTOM" />
<Label text="Target Node URL" GridPane.rowIndex="1" />
<Label text="Amount" GridPane.rowIndex="3" />
<Label fx:id="hostError" opacity="0.0" styleClass="text-error" text="Please use a valid url (pubkey@host:port)" GridPane.columnIndex="1" GridPane.columnSpan="2" />
<Label fx:id="amountError" opacity="0.0" styleClass="text-error" text="Please use a valid amount" GridPane.columnIndex="1" GridPane.columnSpan="2" GridPane.rowIndex="2" />
<Button cancelButton="true" mnemonicParsing="false" onAction="#handleClose" styleClass="cancel" text="Cancel" GridPane.columnIndex="2" GridPane.halignment="RIGHT" GridPane.rowIndex="4" GridPane.valignment="BOTTOM" />
<TextField fx:id="pushMsat" layoutX="193.0" layoutY="112.0" prefWidth="313.0" GridPane.columnIndex="1" GridPane.rowIndex="5" />
<Button fx:id="button" defaultButton="true" mnemonicParsing="false" onAction="#handleOpen" text="Connect"
GridPane.columnIndex="1" GridPane.rowIndex="6" GridPane.valignment="BOTTOM" />
<Button cancelButton="true" mnemonicParsing="false" onAction="#handleClose" styleClass="cancel" text="Cancel"
GridPane.columnIndex="2" GridPane.halignment="RIGHT" GridPane.rowIndex="6" GridPane.valignment="BOTTOM" />
<VBox alignment="CENTER_RIGHT" GridPane.rowIndex="3">
<children>
<Label styleClass="text-strong" text="Capacity *" />
<Label styleClass="label-description" text="Funding capacity of the channel." textAlignment="RIGHT" wrapText="true" />
</children>
</VBox>
<Label fx:id="hostError" opacity="0.0" styleClass="text-error" text="Generic Invalid URI"
GridPane.columnIndex="1" GridPane.columnSpan="2" />
<Label fx:id="fundingSatoshisError" opacity="0.0" styleClass="text-error" text="Generic Invalid Funding"
GridPane.columnIndex="1" GridPane.columnSpan="2" GridPane.rowIndex="2" />
<Label fx:id="pushMsatError" layoutX="193.0" layoutY="89.0" opacity="0.0" styleClass="text-error" text="Generic Invalid Push"
GridPane.columnIndex="1" GridPane.rowIndex="4" />
<VBox alignment="CENTER_RIGHT" layoutX="24.0" layoutY="112.0" GridPane.rowIndex="5">
<children>
<Label styleClass="text-strong" text="Push Amount" />
<Label styleClass="label-description" text="Optional amount in millisatoshi. Sent when opening channel" textAlignment="RIGHT" wrapText="true" />
</children>
</VBox>
<VBox alignment="CENTER_RIGHT" layoutX="24.0" layoutY="112.0" GridPane.rowIndex="1">
<children>
<Label styleClass="text-strong" text="Target Node URI *" />
<Label styleClass="label-description" text="Address of the node" textAlignment="RIGHT" wrapText="true" />
</children>
</VBox>
</children>
<stylesheets>
<URL value="@modals.css" />

View file

@ -5,10 +5,10 @@
<?import javafx.scene.text.Text?>
<?import java.net.URL?>
<GridPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" styleClass="grid"
xmlns="http://javafx.com/javafx/8.0.60" xmlns:fx="http://javafx.com/fxml/1">
xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1">
<columnConstraints>
<ColumnConstraints halignment="RIGHT" hgrow="SOMETIMES" maxWidth="274.0" minWidth="10.0" prefWidth="242.0"/>
<ColumnConstraints hgrow="SOMETIMES" minWidth="10.0"/>
<ColumnConstraints halignment="RIGHT" hgrow="SOMETIMES" maxWidth="210.0" minWidth="10.0" prefWidth="200.0"/>
<ColumnConstraints hgrow="SOMETIMES" minWidth="10.0" prefWidth="100.0" maxWidth="100.0"/>
<ColumnConstraints hgrow="SOMETIMES" minWidth="10.0"/>
</columnConstraints>
<rowConstraints>
@ -19,19 +19,24 @@
<RowConstraints minHeight="10.0" valignment="TOP" vgrow="ALWAYS"/>
</rowConstraints>
<children>
<Label text="Amount (mSat)" GridPane.rowIndex="1"/>
<VBox alignment="CENTER_RIGHT" GridPane.rowIndex="1">
<children>
<Label styleClass="text-strong" text="Amount (mSat)" />
<Label styleClass="label-description" text="Amount to send, in milli satoshi." />
</children>
</VBox>
<TextField fx:id="amount" GridPane.columnIndex="1" GridPane.columnSpan="2" GridPane.rowIndex="1"/>
<Button defaultButton="true" mnemonicParsing="false" onAction="#handleGenerate" prefHeight="29.0"
prefWidth="89.0" text="Generate" GridPane.columnIndex="1" GridPane.rowIndex="2"/>
prefWidth="95.0" text="Generate" GridPane.columnIndex="1" GridPane.rowIndex="2"/>
<TextArea fx:id="paymentRequest" editable="false" prefHeight="150.0" prefWidth="275.0" styleClass="ta"
wrapText="true" GridPane.columnIndex="1" GridPane.columnSpan="2" GridPane.rowIndex="4"/>
<Separator prefHeight="1.0" GridPane.columnSpan="3" GridPane.rowIndex="3"/>
<Text strokeType="OUTSIDE" strokeWidth="0.0"
text="Copy the generated Payment Request and send it to the person owing you the amount above"
text="Send this Payment Request to the person owing you the amount above"
textAlignment="RIGHT" wrappingWidth="188.9560546875" GridPane.rowIndex="4"/>
<Label fx:id="amountError" opacity="0.0" styleClass="text-error" text="Please use a valid payment request"
GridPane.columnIndex="1"/>
<Button cancelButton="true" mnemonicParsing="false" onAction="#handleClose" styleClass="cancel" text="Cancel"
<Label fx:id="amountError" opacity="0.0" styleClass="text-error" text="Generic Invalid Amount"
GridPane.columnIndex="1" GridPane.columnSpan="2"/>
<Button cancelButton="true" mnemonicParsing="false" onAction="#handleClose" styleClass="cancel" text="Close"
GridPane.columnIndex="2" GridPane.halignment="RIGHT" GridPane.rowIndex="2"/>
</children>
<stylesheets>

View file

@ -5,10 +5,10 @@
<?import java.lang.String?>
<?import java.net.URL?>
<GridPane fx:id="nodeId" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity"
prefWidth="395.0" xmlns="http://javafx.com/javafx/8.0.60" xmlns:fx="http://javafx.com/fxml/1">
prefWidth="395.0" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1">
<columnConstraints>
<ColumnConstraints halignment="LEFT" hgrow="SOMETIMES" minWidth="10.0" prefWidth="150.0"/>
<ColumnConstraints halignment="LEFT" hgrow="SOMETIMES" maxWidth="292.0" minWidth="10.0" prefWidth="241.0"/>
<ColumnConstraints halignment="LEFT" hgrow="SOMETIMES" minWidth="10.0" prefWidth="110.0"/>
<ColumnConstraints halignment="LEFT" hgrow="SOMETIMES" minWidth="10.0" prefWidth="250.0"/>
</columnConstraints>
<rowConstraints>
<RowConstraints minHeight="10.0" vgrow="SOMETIMES"/>
@ -27,7 +27,7 @@
GridPane.columnSpan="2" GridPane.rowIndex="1" GridPane.vgrow="ALWAYS"/>
<Button fx:id="handleSend" defaultButton="true" mnemonicParsing="false" onAction="#handleSend" text="Send"
GridPane.rowIndex="7"/>
<Label fx:id="paymentRequestError" opacity="0.0" text="Please use a valid payment request"
<Label fx:id="paymentRequestError" opacity="0.0" text="Generic Invalid Payment Request"
GridPane.columnSpan="2" GridPane.rowIndex="2">
<styleClass>
<String fx:value="text-error"/>
@ -37,10 +37,10 @@
<Label styleClass="text-muted" text="Amount (mSat)" GridPane.halignment="RIGHT" GridPane.rowIndex="3"/>
<Separator GridPane.columnSpan="2" GridPane.rowIndex="6"/>
<Label styleClass="text-muted" text="Node Id" GridPane.halignment="RIGHT" GridPane.rowIndex="4"/>
<Label fx:id="amountLabel" text="0" GridPane.columnIndex="1" GridPane.rowIndex="3"/>
<Label fx:id="nodeIdLabel" text="N/A" GridPane.columnIndex="1" GridPane.rowIndex="4"/>
<TextField fx:id="amountField" editable="false" styleClass="noteditable" text="0" GridPane.columnIndex="1" GridPane.rowIndex="3"/>
<TextField fx:id="nodeIdField" editable="false" styleClass="noteditable" text="N/A" GridPane.columnIndex="1" GridPane.rowIndex="4"/>
<TextField fx:id="hashField" editable="false" styleClass="noteditable" text="N/A" GridPane.columnIndex="1" GridPane.rowIndex="5"/>
<Label styleClass="text-muted" text="hash" GridPane.halignment="RIGHT" GridPane.rowIndex="5"/>
<Label fx:id="hashLabel" text="N/A" GridPane.columnIndex="1" GridPane.rowIndex="5"/>
<Button cancelButton="true" mnemonicParsing="false" onAction="#handleClose" styleClass="cancel" text="Cancel"
GridPane.columnIndex="1" GridPane.halignment="RIGHT" GridPane.rowIndex="7"/>
</children>

View file

@ -9,7 +9,7 @@
<?import java.lang.String?>
<?import java.net.URL?>
<Pane fx:id="splash" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity"
prefHeight="457.0" prefWidth="760.0" styleClass="transparent" xmlns="http://javafx.com/javafx/8.0.60"
prefHeight="457.0" prefWidth="760.0" styleClass="transparent" xmlns="http://javafx.com/javafx/8"
xmlns:fx="http://javafx.com/fxml/1" fx:controller="fr.acinq.eclair.gui.controllers.SplashController">
<children>
<ImageView fx:id="imgBlurred" fitHeight="0" fitWidth="409.0" layoutX="176.0" layoutY="115.0" pickOnBounds="true"

View file

@ -4,13 +4,14 @@
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<target>System.out</target>
<encoder>
<pattern>${HOSTNAME} %d %-5level %logger{36} %X{akkaSource} - %msg%ex{12}%n</pattern>
<pattern>${HOSTNAME} %d %-5level %logger{36} %X{akkaSource} - %msg%ex{24}%n</pattern>
</encoder>
</appender>
<logger name="fr.acinq.eclair.channel" level="DEBUG"/>
<logger name="fr.acinq.eclair.channel.Register" level="DEBUG"/>
<logger name="fr.acinq.eclair.crypto.TransportHandler" level="INFO"/>
<logger name="fr.acinq.eclair.router" level="DEBUG"/>
<root level="INFO">
<appender-ref ref="CONSOLE"/>

View file

@ -1,5 +1,6 @@
package fr.acinq.eclair
import java.net.InetSocketAddress
import javafx.application.{Application, Platform}
import akka.actor.{Actor, ActorRef, ActorSystem, Props}
@ -7,7 +8,7 @@ import akka.http.scaladsl.Http
import akka.stream.ActorMaterializer
import akka.util.Timeout
import com.typesafe.config.ConfigFactory
import fr.acinq.bitcoin.{Base58Check, BinaryData, OP_CHECKSIG, OP_DUP, OP_EQUALVERIFY, OP_HASH160, OP_PUSHDATA, Satoshi}
import fr.acinq.bitcoin.{Base58Check, BinaryData, MilliSatoshi, OP_CHECKSIG, OP_DUP, OP_EQUALVERIFY, OP_HASH160, OP_PUSHDATA, Satoshi}
import fr.acinq.eclair.api.Service
import fr.acinq.eclair.blockchain.peer.PeerClient
import fr.acinq.eclair.blockchain.rpc.BitcoinJsonRPCClient
@ -15,7 +16,7 @@ import fr.acinq.eclair.blockchain.{ExtendedBitcoinClient, PeerWatcher}
import fr.acinq.eclair.channel._
import fr.acinq.eclair.gui.FxApp
import fr.acinq.eclair.io.{Client, Server}
import fr.acinq.eclair.payment.{LocalPaymentHandler, NoopPaymentHandler, PaymentInitiator}
import fr.acinq.eclair.payment._
import fr.acinq.eclair.router._
import grizzled.slf4j.Logging
import org.json4s.JsonAST.JString
@ -42,7 +43,7 @@ object Boot extends App with Logging {
class Setup() extends Logging {
logger.info(s"hello!")
logger.info(s"nodeid=${Globals.Node.publicKey.toBin}")
logger.info(s"nodeid=${Globals.Node.publicKey.toBin} alias=${Globals.Node.alias}")
val config = ConfigFactory.load()
implicit lazy val system = ActorSystem()
@ -66,6 +67,7 @@ class Setup() extends Logging {
// TODO: we should use p2wpkh instead of p2pkh as soon as bitcoind supports it
//val finalScriptPubKey = OP_0 :: OP_PUSHDATA(Base58Check.decode(finalAddress)._2) :: Nil
val finalScriptPubKey = OP_DUP :: OP_HASH160 :: OP_PUSHDATA(Base58Check.decode(finalAddress)._2) :: OP_EQUALVERIFY :: OP_CHECKSIG :: Nil
val socket = new InetSocketAddress(config.getString("eclair.server.host"), config.getInt("eclair.server.port"))
val fatalEventPromise = Promise[FatalEvent]()
system.actorOf(Props(new Actor {
@ -84,11 +86,10 @@ class Setup() extends Logging {
case "local" => system.actorOf(Props[LocalPaymentHandler], name = "payment-handler")
case "noop" => system.actorOf(Props[NoopPaymentHandler], name = "payment-handler")
}
val register = system.actorOf(Register.props(watcher, paymentHandler, finalScriptPubKey), name = "register")
val selector = system.actorOf(Props[ChannelSelector], name = "selector")
val router = system.actorOf(Props[Router], name = "router")
val ircWatcher = system.actorOf(Props[IRCWatcher], "irc")
val paymentInitiator = system.actorOf(PaymentInitiator.props(router, selector, blockCount), "payment-spawner")
val relayer = system.actorOf(Relayer.props(Globals.Node.privateKey, paymentHandler), name = "propagator")
val router = system.actorOf(Router.props(watcher, Globals.Node.announcement), name = "router")
val paymentInitiator = system.actorOf(PaymentInitiator.props(Globals.Node.publicKey, router, blockCount), "payment-initiator")
val register = system.actorOf(Register.props(watcher, router, relayer, finalScriptPubKey), name = "register")
val server = system.actorOf(Server.props(config.getString("eclair.server.host"), config.getInt("eclair.server.port"), register), "server")
val _setup = this
@ -98,7 +99,7 @@ class Setup() extends Logging {
override val paymentHandler: ActorRef = _setup.paymentHandler
override val paymentInitiator: ActorRef = _setup.paymentInitiator
override def connect(host: String, port: Int, pubkey: BinaryData, amount: Satoshi): Unit = system.actorOf(Client.props(host, port, pubkey, amount, register))
override def connect(host: String, port: Int, pubkey: BinaryData, fundingSatoshis: Satoshi, pushMsat: MilliSatoshi): Unit = system.actorOf(Client.props(host, port, pubkey, fundingSatoshis, pushMsat, register))
}
Http().bindAndHandle(api.route, config.getString("eclair.api.host"), config.getInt("eclair.api.port")) onFailure {
case t: Throwable => system.eventStream.publish(HTTPBindError)

View file

@ -1,8 +1,12 @@
package fr.acinq.eclair
import java.net.InetSocketAddress
import com.typesafe.config.ConfigFactory
import fr.acinq.bitcoin.{BinaryData, DeterministicWallet}
import fr.acinq.eclair.router.Router
import scala.compat.Platform
import scala.concurrent.duration._
@ -20,13 +24,20 @@ object Globals {
val extendedPublicKey = DeterministicWallet.publicKey(extendedPrivateKey)
val publicKey = extendedPublicKey.publicKey
val id = publicKey.toBin.toString()
val alias = config.getString("node.alias").take(32)
val color: (Byte, Byte, Byte) = (config.getInt("node.color.r").toByte, config.getInt("node.color.g").toByte, config.getInt("node.color.b").toByte)
val address = new InetSocketAddress(config.getString("server.host"), config.getInt("server.port"))
val announcement = Router.makeNodeAnnouncement(privateKey, alias, color, address :: Nil, Platform.currentTime / 1000)
}
val default_delay_blocks = config.getInt("delay-blocks")
val default_mindepth_blocks = config.getInt("mindepth-blocks")
val default_feeratePerKw = 10000
val base_fee = config.getInt("base-fee")
val proportional_fee = config.getInt("proportional-fee")
val expiry_delta_blocks = config.getInt("expiry-delta-blocks")
val htlc_minimum_msat = config.getInt("htlc-minimum-msat")
val delay_blocks = config.getInt("delay-blocks")
val mindepth_blocks = config.getInt("mindepth-blocks")
val feeratePerKw = 10000
val fee_base_msat = config.getInt("fee-base-msat")
val fee_proportional_msat = config.getInt("fee-proportional-msat")
val default_anchor_amount = 1000000
val autosign_interval = 300 milliseconds
}

View file

@ -11,7 +11,7 @@ import akka.pattern.ask
import akka.util.Timeout
import de.heikoseeberger.akkahttpjson4s.Json4sSupport
import de.heikoseeberger.akkahttpjson4s.Json4sSupport.ShouldWritePretty
import fr.acinq.bitcoin.{BinaryData, Satoshi}
import fr.acinq.bitcoin.{BinaryData, MilliSatoshi, Satoshi}
import fr.acinq.eclair._
import fr.acinq.eclair.channel.Register.{ListChannels, SendCommand}
import fr.acinq.eclair.channel._
@ -47,7 +47,7 @@ trait Service extends Logging {
import Json4sSupport.{json4sMarshaller, json4sUnmarshaller}
def connect(host: String, port: Int, pubkey: BinaryData, amount: Satoshi): Unit
def connect(host: String, port: Int, pubkey: BinaryData, fundingSatoshis: Satoshi, pushMsat: MilliSatoshi): Unit
def register: ActorRef
@ -71,7 +71,7 @@ trait Service extends Logging {
req =>
val f_res: Future[AnyRef] = req match {
case JsonRPCBody(_, _, "connect", JString(host) :: JInt(port) :: JString(pubkey) :: JInt(anchor_amount) :: Nil) =>
connect(host, port.toInt, BinaryData(pubkey), Satoshi(anchor_amount.toLong))
connect(host, port.toInt, BinaryData(pubkey), Satoshi(anchor_amount.toLong), MilliSatoshi(0))
Future.successful("ok")
case JsonRPCBody(_, _, "info", _) =>
Future.successful(Status(Globals.Node.id))
@ -79,19 +79,19 @@ trait Service extends Logging {
(register ? ListChannels).mapTo[Iterable[ActorRef]]
.flatMap(l => Future.sequence(l.map(c => c ? CMD_GETINFO)))
case JsonRPCBody(_, _, "network", _) =>
(router ? 'network).mapTo[Iterable[ChannelDesc]]
(router ? 'channels).mapTo[Iterable[ChannelDesc]]
case JsonRPCBody(_, _, "addhtlc", JInt(amount) :: JString(rhash) :: JString(nodeId) :: Nil) =>
(paymentInitiator ? CreatePayment(amount.toInt, BinaryData(rhash), BinaryData(nodeId))).mapTo[ChannelEvent]
case JsonRPCBody(_, _, "genh", _) =>
(paymentHandler ? 'genh).mapTo[BinaryData]
case JsonRPCBody(_, _, "sign", JString(channel) :: Nil) =>
(register ? SendCommand(channel, CMD_SIGN)).mapTo[ActorRef].map(_ => "ok")
case JsonRPCBody(_, _, "sign", JInt(channel) :: Nil) =>
(register ? SendCommand(channel.toLong, CMD_SIGN)).mapTo[ActorRef].map(_ => "ok")
case JsonRPCBody(_, _, "fulfillhtlc", JString(channel) :: JDouble(id) :: JString(r) :: Nil) =>
(register ? SendCommand(channel, CMD_FULFILL_HTLC(id.toLong, BinaryData(r), commit = true))).mapTo[ActorRef].map(_ => "ok")
(register ? SendCommand(channel.toLong, CMD_FULFILL_HTLC(id.toLong, BinaryData(r), commit = true))).mapTo[ActorRef].map(_ => "ok")
case JsonRPCBody(_, _, "close", JString(channel) :: JString(scriptPubKey) :: Nil) =>
(register ? SendCommand(channel, CMD_CLOSE(Some(scriptPubKey)))).mapTo[ActorRef].map(_ => "ok")
(register ? SendCommand(channel.toLong, CMD_CLOSE(Some(scriptPubKey)))).mapTo[ActorRef].map(_ => "ok")
case JsonRPCBody(_, _, "close", JString(channel) :: Nil) =>
(register ? SendCommand(channel, CMD_CLOSE(None))).mapTo[ActorRef].map(_ => "ok")
(register ? SendCommand(channel.toLong, CMD_CLOSE(None))).mapTo[ActorRef].map(_ => "ok")
case JsonRPCBody(_, _, "help", _) =>
Future.successful(List(
"info: display basic node information",

View file

@ -28,7 +28,7 @@ class PeerWatcher(client: ExtendedBitcoinClient, blockCount: Long)(implicit ec:
val triggeredWatches = watches.collect {
case w@WatchSpent(channel, txid, outputIndex, minDepth, event)
if tx.txIn.exists(i => i.outPoint.txid == txid && i.outPoint.index == outputIndex) =>
channel ! (BITCOIN_FUNDING_SPENT, tx)
channel ! WatchEventSpent(BITCOIN_FUNDING_SPENT, tx)
w
}
context.become(watching(watches -- triggeredWatches, block2tx, currentBlockCount))
@ -38,19 +38,23 @@ class PeerWatcher(client: ExtendedBitcoinClient, blockCount: Long)(implicit ec:
// TODO: beware of the herd effect
watches.collect {
case w@WatchConfirmed(channel, txId, minDepth, event) =>
client.getTxConfirmations(txId.toString).collect {
// TODO: this is a workaround to not have WatchConfirmed triggered multiple times in testing
// the reason is that we cannot fo a become(watches - w, ...) because it happens in the future callback
case Some(confirmations) if confirmations >= minDepth => self ! ('trigger, w)
client.getTxConfirmations(txId.toString).map {
case Some(confirmations) if confirmations >= minDepth =>
client.getTransactionShortId(txId.toString).map {
// TODO: this is a workaround to not have WatchConfirmed triggered multiple times in testing
// the reason is that we cannot do a become(watches - w, ...) because it happens in the future callback
case (height, index) => self ! ('trigger, w, WatchEventConfirmed(w.event, height, index))
}
}
}
case ('trigger, w: WatchConfirmed) if watches.contains(w) =>
case ('trigger, w: WatchConfirmed, e: WatchEvent) if watches.contains(w) =>
log.info(s"triggering $w")
w.channel ! w.event
w.channel ! e
context.become(watching(watches - w, block2tx, currentBlockCount))
case ('trigger, w: WatchConfirmed) if !watches.contains(w) => {}
case ('trigger, w: WatchConfirmed, e: WatchEvent) if !watches.contains(w) => {}
case CurrentBlockCount(count) => {
val toPublish = block2tx.filterKeys(_ <= count)

View file

@ -16,9 +16,16 @@ trait Watch {
}
final case class WatchConfirmed(channel: ActorRef, txId: BinaryData, minDepth: Long, event: BitcoinEvent) extends Watch
final case class WatchSpent(channel: ActorRef, txId: BinaryData, outputIndex: Int, minDepth: Int, event: BitcoinEvent) extends Watch
// notify me if confirmation number gets below minDepth
// 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 WatchEventLost(event: BitcoinEvent) extends WatchEvent
/**
* Publish the provided tx as soon as possible depending on locktime and csv
*/

View file

@ -1,12 +1,13 @@
package fr.acinq.eclair.channel
import akka.actor.{ActorRef, FSM, LoggingFSM, Props}
import fr.acinq.bitcoin.Crypto.Point
import fr.acinq.bitcoin.Crypto.PublicKey
import fr.acinq.bitcoin._
import fr.acinq.eclair._
import fr.acinq.eclair.blockchain._
import fr.acinq.eclair.channel.Helpers.{Closing, Funding}
import fr.acinq.eclair.crypto.{Generators, ShaChain}
import fr.acinq.eclair.payment.Binding
import fr.acinq.eclair.transactions._
import fr.acinq.eclair.wire._
@ -21,10 +22,10 @@ import scala.util.{Failure, Success, Try}
*/
object Channel {
def props(them: ActorRef, blockchain: ActorRef, paymentHandler: ActorRef, localParams: LocalParams, theirNodeId: String, autoSignInterval: Option[FiniteDuration] = None) = Props(new Channel(them, blockchain, paymentHandler, localParams, theirNodeId, autoSignInterval))
def props(them: ActorRef, blockchain: ActorRef, router: ActorRef, relayer: ActorRef, localParams: LocalParams, theirNodeId: PublicKey, autoSignInterval: Option[FiniteDuration] = None) = Props(new Channel(them, blockchain, router, relayer, localParams, theirNodeId, autoSignInterval))
}
class Channel(val them: ActorRef, val blockchain: ActorRef, paymentHandler: ActorRef, val localParams: LocalParams, theirNodeId: String, autoSignInterval: Option[FiniteDuration] = None)(implicit ec: ExecutionContext = ExecutionContext.Implicits.global) extends LoggingFSM[State, Data] {
class Channel(val them: ActorRef, val blockchain: ActorRef, router: ActorRef, relayer: ActorRef, val localParams: LocalParams, theirNodeId: PublicKey, autoSignInterval: Option[FiniteDuration] = None)(implicit ec: ExecutionContext = ExecutionContext.Implicits.global) extends LoggingFSM[State, Data] {
context.system.eventStream.publish(ChannelCreated(self, localParams, theirNodeId))
@ -95,7 +96,7 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, paymentHandler: Acto
case Event(open: OpenChannel, DATA_WAIT_FOR_OPEN_CHANNEL(localParams, autoSignInterval)) =>
// TODO: here we should check if remote parameters suit us
// TODO: maybe also check uniqueness of temporary channel id
val minimumDepth = Globals.default_mindepth_blocks
val minimumDepth = Globals.mindepth_blocks
val firstPerCommitmentPoint = Generators.perCommitPoint(localParams.shaSeed, 0)
them ! AcceptChannel(temporaryChannelId = Platform.currentTime,
dustLimitSatoshis = localParams.dustLimitSatoshis,
@ -229,7 +230,7 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, paymentHandler: Acto
LocalChanges(Nil, Nil, Nil), RemoteChanges(Nil, Nil),
localCurrentHtlcId = 0L,
remoteNextCommitInfo = Right(null), // TODO: we will receive their next per-commitment point in the next message, so we temporarily put an empty byte array
commitInput, ShaChain.init, channelId = 0)
commitInput, ShaChain.init, channelId = 0) // TODO: we will compute the channelId at the next step, so we temporarily put 0
context.system.eventStream.publish(ChannelIdAssigned(self, commitments.anchorId, Satoshi(params.fundingSatoshis)))
goto(WAIT_FOR_FUNDING_LOCKED_INTERNAL) using DATA_WAIT_FOR_FUNDING_LOCKED_INTERNAL(temporaryChannelId, params, commitments, None)
}
@ -280,23 +281,23 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, paymentHandler: Acto
log.info(s"received their FundingLocked, deferring message")
stay using d.copy(deferred = Some(msg))
case Event(BITCOIN_FUNDING_DEPTHOK, d@DATA_WAIT_FOR_FUNDING_LOCKED_INTERNAL(temporaryChannelId, params, commitments, deferred)) =>
// TODO: set channelId
val channelId = 0L
case Event(WatchEventConfirmed(BITCOIN_FUNDING_DEPTHOK, blockHeight, txIndex), d@DATA_WAIT_FOR_FUNDING_LOCKED_INTERNAL(temporaryChannelId, params, commitments, deferred)) =>
val channelId = toShortId(blockHeight, txIndex, commitments.commitInput.outPoint.index.toInt)
blockchain ! WatchLost(self, commitments.anchorId, params.minimumDepth, BITCOIN_FUNDING_LOST)
val nextPerCommitmentPoint = Generators.perCommitPoint(localParams.shaSeed, 1)
them ! FundingLocked(channelId, 0L, None, None, nextPerCommitmentPoint) // TODO: routing announcements disabled
them ! FundingLocked(temporaryChannelId, channelId, None, None, nextPerCommitmentPoint) // TODO: routing announcements disabled
deferred.map(self ! _)
// TODO: htlcIdx should not be 0 when resuming connection
goto(WAIT_FOR_FUNDING_LOCKED) using DATA_NORMAL(channelId, params, commitments.copy(channelId = channelId), None, Map())
goto(WAIT_FOR_FUNDING_LOCKED) using DATA_NORMAL(params, commitments.copy(channelId = channelId), None)
// TODO: not implemented, maybe should be done with a state timer and not a blockchain watch?
case Event(BITCOIN_FUNDING_TIMEOUT, _) =>
them ! Error(0, "Funding tx timed out".getBytes)
goto(CLOSED)
case Event((BITCOIN_FUNDING_SPENT, tx: Transaction), d: DATA_WAIT_FOR_FUNDING_LOCKED_INTERNAL) if tx.txid == d.commitments.remoteCommit.txid => handleRemoteSpentCurrent(tx, d)
case Event(WatchEventSpent(BITCOIN_FUNDING_SPENT, tx: Transaction), d: DATA_WAIT_FOR_FUNDING_LOCKED_INTERNAL) if tx.txid == d.commitments.remoteCommit.txid => handleRemoteSpentCurrent(tx, d)
case Event((BITCOIN_FUNDING_SPENT, _), d: DATA_WAIT_FOR_FUNDING_LOCKED_INTERNAL) => handleInformationLeak(d)
case Event(WatchEventSpent(BITCOIN_FUNDING_SPENT, _), d: DATA_WAIT_FOR_FUNDING_LOCKED_INTERNAL) => handleInformationLeak(d)
case Event(cmd: CMD_CLOSE, d: DATA_WAIT_FOR_FUNDING_LOCKED_INTERNAL) =>
blockchain ! PublishAsap(d.commitments.localCommit.publishableTxs.commitTx.tx)
@ -316,14 +317,18 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, paymentHandler: Acto
})
when(WAIT_FOR_FUNDING_LOCKED)(handleExceptions {
case Event(FundingLocked(temporaryChannelId, channelId, _, _, nextPerCommitmentPoint), d: DATA_NORMAL) =>
// TODO: check channelId matches ours
Register.create_alias(theirNodeId, d.commitments.anchorId)
case Event(FundingLocked(_, remoteChannelId, _, _, nextPerCommitmentPoint), d: DATA_NORMAL) if remoteChannelId != d.channelId =>
// TODO: channel id mismatch, can happen if minDepth is to low, negotiation not suported yet
handleLocalError(new RuntimeException(s"channel id mismatch local=${d.channelId} remote=$remoteChannelId"), d)
case Event(FundingLocked(_, remoteChannelId, _, _, nextPerCommitmentPoint), d: DATA_NORMAL) =>
log.info(s"channel ready with channelId=${java.lang.Long.toUnsignedString(d.channelId)}")
Register.createAlias(theirNodeId.hash160, d.channelId)
goto(NORMAL) using d.copy(commitments = d.commitments.copy(remoteNextCommitInfo = Right(nextPerCommitmentPoint)))
case Event((BITCOIN_FUNDING_SPENT, tx: Transaction), d: DATA_NORMAL) if tx.txid == d.commitments.remoteCommit.txid => handleRemoteSpentCurrent(tx, d)
case Event(WatchEventSpent(BITCOIN_FUNDING_SPENT, tx: Transaction), d: DATA_NORMAL) if tx.txid == d.commitments.remoteCommit.txid => handleRemoteSpentCurrent(tx, d)
case Event((BITCOIN_FUNDING_SPENT, _), d: DATA_NORMAL) => handleInformationLeak(d)
case Event(WatchEventSpent(BITCOIN_FUNDING_SPENT, _), d: DATA_NORMAL) => handleInformationLeak(d)
case Event(cmd: CMD_CLOSE, d: DATA_NORMAL) =>
blockchain ! PublishAsap(d.commitments.localCommit.publishableTxs.commitTx.tx)
@ -353,15 +358,16 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, paymentHandler: Acto
case Event(c: CMD_ADD_HTLC, d: DATA_NORMAL) if d.localShutdown.isDefined =>
handleCommandError(sender, new RuntimeException("cannot send new htlcs, closing in progress"))
case Event(c@CMD_ADD_HTLC(amountMsat, rHash, expiry, route, origin, id_opt, do_commit), d@DATA_NORMAL(channelId, params, commitments, _, downstreams)) =>
case Event(c@CMD_ADD_HTLC(amountMsat, rHash, expiry, route, origin_opt, id_opt, do_commit), d@DATA_NORMAL(params, commitments, _)) =>
Try(Commitments.sendAdd(commitments, c)) match {
case Success((commitments1, add)) =>
origin_opt.map(origin => relayer ! Binding(origin, add))
if (do_commit) self ! CMD_SIGN
handleCommandSuccess(sender, add, d.copy(commitments = commitments1, downstreams = downstreams + (add.id -> origin)))
handleCommandSuccess(sender, add, d.copy(commitments = commitments1))
case Failure(cause) => handleCommandError(sender, cause)
}
case Event(add: UpdateAddHtlc, d@DATA_NORMAL(_, params, commitments, _, _)) =>
case Event(add: UpdateAddHtlc, d@DATA_NORMAL(params, commitments, _)) =>
Try(Commitments.receiveAdd(commitments, add)) match {
case Success(commitments1) =>
import scala.concurrent.ExecutionContext.Implicits.global
@ -378,13 +384,13 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, paymentHandler: Acto
case Failure(cause) => handleCommandError(sender, cause)
}
case Event(fulfill@UpdateFulfillHtlc(_, id, r), d@DATA_NORMAL(channelId, params, commitments, _, downstreams)) =>
case Event(fulfill@UpdateFulfillHtlc(_, id, r), d@DATA_NORMAL(params, commitments, _)) =>
Try(Commitments.receiveFulfill(d.commitments, fulfill)) match {
case Success((commitments1, htlc)) =>
propagateDownstream(htlc, Right(fulfill), downstreams(id))
relayer ! (htlc, fulfill)
import scala.concurrent.ExecutionContext.Implicits.global
params.autoSignInterval.map(interval => context.system.scheduler.scheduleOnce(interval, self, CMD_SIGN))
stay using d.copy(commitments = commitments1, downstreams = downstreams - id)
stay using d.copy(commitments = commitments1)
case Failure(cause) => handleLocalError(cause, d)
}
@ -396,13 +402,13 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, paymentHandler: Acto
case Failure(cause) => handleCommandError(sender, cause)
}
case Event(fail@UpdateFailHtlc(_, id, reason), d@DATA_NORMAL(channelId, params, commitments, _, downstreams)) =>
case Event(fail@UpdateFailHtlc(_, id, reason), d@DATA_NORMAL(params, commitments, _)) =>
Try(Commitments.receiveFail(d.commitments, fail)) match {
case Success((commitments1, htlc)) =>
propagateDownstream(htlc, Left(fail), downstreams(id))
relayer ! (htlc, fail)
import scala.concurrent.ExecutionContext.Implicits.global
params.autoSignInterval.map(interval => context.system.scheduler.scheduleOnce(interval, self, CMD_SIGN))
stay using d.copy(commitments = commitments1, downstreams = downstreams - id)
stay using d.copy(commitments = commitments1)
case Failure(cause) => handleLocalError(cause, d)
}
@ -430,7 +436,7 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, paymentHandler: Acto
// now that we have their sig, we should propagate the htlcs newly received
(commitments1.localCommit.spec.htlcs -- d.commitments.localCommit.spec.htlcs)
.filter(_.direction == IN)
.foreach(htlc => propagateUpstream(htlc.add, d.commitments.anchorId))
.foreach(htlc => relayer ! htlc.add)
context.system.eventStream.publish(ChannelSignatureReceived(self, commitments1))
stay using d.copy(commitments = commitments1)
case Failure(cause) => handleLocalError(cause, d)
@ -460,10 +466,10 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, paymentHandler: Acto
case _ => handleCommandError(sender, new RuntimeException("invalid final script"))
}
case Event(remoteShutdown@Shutdown(_, remoteScriptPubKey), d@DATA_NORMAL(channelId, params, commitments, ourShutdownOpt, downstreams)) if commitments.remoteChanges.proposed.size > 0 =>
case Event(remoteShutdown@Shutdown(_, remoteScriptPubKey), d@DATA_NORMAL(params, commitments, ourShutdownOpt)) if commitments.remoteChanges.proposed.size > 0 =>
handleLocalError(new RuntimeException("it is illegal to send a shutdown while having unsigned changes"), d)
case Event(remoteShutdown@Shutdown(_, remoteScriptPubKey), d@DATA_NORMAL(channelId, params, commitments, ourShutdownOpt, downstreams)) =>
case Event(remoteShutdown@Shutdown(_, remoteScriptPubKey), d@DATA_NORMAL(params, commitments, ourShutdownOpt)) =>
Try(ourShutdownOpt.map(s => (s, commitments)).getOrElse {
require(Closing.isValidFinalScriptPubkey(remoteScriptPubKey), "invalid final script")
// first if we have pending changes, we need to commit them
@ -472,7 +478,7 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, paymentHandler: Acto
them ! commit
commitments1
} else commitments
val shutdown = Shutdown(channelId, Script.write(params.localParams.defaultFinalScriptPubKey))
val shutdown = Shutdown(d.channelId, Script.write(params.localParams.defaultFinalScriptPubKey))
them ! shutdown
(shutdown, commitments2)
}) match {
@ -481,15 +487,15 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, paymentHandler: Acto
|| (commitments3.remoteNextCommitInfo.isLeft && commitments3.localCommit.spec.htlcs.size == 0 && commitments3.remoteNextCommitInfo.left.get.spec.htlcs.size == 0) =>
val closingSigned = Closing.makeFirstClosingTx(params, commitments3, localShutdown.scriptPubKey, remoteShutdown.scriptPubKey)
them ! closingSigned
goto(NEGOTIATING) using DATA_NEGOTIATING(channelId, params, commitments3, localShutdown, remoteShutdown, closingSigned)
goto(NEGOTIATING) using DATA_NEGOTIATING(params, commitments3, localShutdown, remoteShutdown, closingSigned)
case Success((localShutdown, commitments3)) =>
goto(SHUTDOWN) using DATA_SHUTDOWN(channelId, params, commitments3, localShutdown, remoteShutdown, downstreams)
goto(SHUTDOWN) using DATA_SHUTDOWN(params, commitments3, localShutdown, remoteShutdown)
case Failure(cause) => handleLocalError(cause, d)
}
case Event((BITCOIN_FUNDING_SPENT, tx: Transaction), d: DATA_NORMAL) if tx.txid == d.commitments.remoteCommit.txid => handleRemoteSpentCurrent(tx, d)
case Event(WatchEventSpent(BITCOIN_FUNDING_SPENT, tx: Transaction), d: DATA_NORMAL) if tx.txid == d.commitments.remoteCommit.txid => handleRemoteSpentCurrent(tx, d)
case Event((BITCOIN_FUNDING_SPENT, tx: Transaction), d: DATA_NORMAL) => handleRemoteSpentOther(tx, d)
case Event(WatchEventSpent(BITCOIN_FUNDING_SPENT, tx: Transaction), d: DATA_NORMAL) => handleRemoteSpentOther(tx, d)
case Event(e: Error, d: DATA_NORMAL) => handleRemoteError(e, d)
@ -516,8 +522,8 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, paymentHandler: Acto
case Event(fulfill@UpdateFulfillHtlc(_, id, r), d: DATA_SHUTDOWN) =>
Try(Commitments.receiveFulfill(d.commitments, fulfill)) match {
case Success((commitments1, htlc)) =>
propagateDownstream(htlc, Right(fulfill), d.downstreams(id))
stay using d.copy(commitments = commitments1, downstreams = d.downstreams - id)
relayer ! (htlc, fulfill)
stay using d.copy(commitments = commitments1)
case Failure(cause) => handleLocalError(cause, d)
}
@ -530,8 +536,8 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, paymentHandler: Acto
case Event(fail@UpdateFailHtlc(_, id, reason), d: DATA_SHUTDOWN) =>
Try(Commitments.receiveFail(d.commitments, fail)) match {
case Success((commitments1, htlc)) =>
propagateDownstream(htlc, Left(fail), d.downstreams(id))
stay using d.copy(commitments = commitments1, downstreams = d.downstreams - id)
relayer ! (htlc, fail)
stay using d.copy(commitments = commitments1)
case Failure(cause) => handleLocalError(cause, d)
}
@ -552,36 +558,36 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, paymentHandler: Acto
case Failure(cause) => handleCommandError(sender, cause)
}
case Event(msg@CommitSig(_, theirSig, theirHtlcSigs), d@DATA_SHUTDOWN(channelId, params, commitments, localShutdown, remoteShutdown, _)) =>
case Event(msg@CommitSig(_, theirSig, theirHtlcSigs), d@DATA_SHUTDOWN(params, commitments, localShutdown, remoteShutdown)) =>
// TODO: we might have to propagate htlcs upstream depending on the outcome of https://github.com/ElementsProject/lightning/issues/29
Try(Commitments.receiveCommit(d.commitments, msg)) match {
case Success((commitments1, revocation)) if commitments1.hasNoPendingHtlcs =>
them ! revocation
val closingSigned = Closing.makeFirstClosingTx(params, commitments1, localShutdown.scriptPubKey, remoteShutdown.scriptPubKey)
them ! closingSigned
goto(NEGOTIATING) using DATA_NEGOTIATING(channelId, params, commitments1, localShutdown, remoteShutdown, closingSigned)
goto(NEGOTIATING) using DATA_NEGOTIATING(params, commitments1, localShutdown, remoteShutdown, closingSigned)
case Success((commitments1, revocation)) =>
them ! revocation
stay using d.copy(commitments = commitments1)
case Failure(cause) => handleLocalError(cause, d)
}
case Event(msg: RevokeAndAck, d@DATA_SHUTDOWN(channelId, params, commitments, localShutdown, remoteShutdown, _)) =>
case Event(msg: RevokeAndAck, d@DATA_SHUTDOWN(params, commitments, localShutdown, remoteShutdown)) =>
// we received a revocation because we sent a signature
// => all our changes have been acked
Try(Commitments.receiveRevocation(d.commitments, msg)) match {
case Success(commitments1) if commitments1.hasNoPendingHtlcs =>
val closingSigned = Closing.makeFirstClosingTx(params, commitments, localShutdown.scriptPubKey, remoteShutdown.scriptPubKey)
them ! closingSigned
goto(NEGOTIATING) using DATA_NEGOTIATING(channelId, params, commitments1, localShutdown, remoteShutdown, closingSigned)
goto(NEGOTIATING) using DATA_NEGOTIATING(params, commitments1, localShutdown, remoteShutdown, closingSigned)
case Success(commitments1) =>
stay using d.copy(commitments = commitments1)
case Failure(cause) => handleLocalError(cause, d)
}
case Event((BITCOIN_FUNDING_SPENT, tx: Transaction), d: DATA_SHUTDOWN) if tx.txid == d.commitments.remoteCommit.txid => handleRemoteSpentCurrent(tx, d)
case Event(WatchEventSpent(BITCOIN_FUNDING_SPENT, tx: Transaction), d: DATA_SHUTDOWN) if tx.txid == d.commitments.remoteCommit.txid => handleRemoteSpentCurrent(tx, d)
case Event((BITCOIN_FUNDING_SPENT, tx: Transaction), d: DATA_SHUTDOWN) => handleRemoteSpentOther(tx, d)
case Event(WatchEventSpent(BITCOIN_FUNDING_SPENT, tx: Transaction), d: DATA_SHUTDOWN) => handleRemoteSpentOther(tx, d)
case Event(e: Error, d: DATA_SHUTDOWN) => handleRemoteError(e, d)
}
@ -614,13 +620,13 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, paymentHandler: Acto
throw new RuntimeException("cannot verify their close signature", cause)
}
case Event((BITCOIN_FUNDING_SPENT, tx: Transaction), d: DATA_NEGOTIATING) if tx.txid == Closing.makeClosingTx(d.params, d.commitments, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, Satoshi(d.localClosingSigned.feeSatoshis))._1.tx.txid =>
case Event(WatchEventSpent(BITCOIN_FUNDING_SPENT, tx: Transaction), d: DATA_NEGOTIATING) if tx.txid == Closing.makeClosingTx(d.params, d.commitments, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, Satoshi(d.localClosingSigned.feeSatoshis))._1.tx.txid =>
// happens when we agreed on a closeSig, but we don't know it yet: we receive the watcher notification before their ClosingSigned (which will match ours)
stay()
case Event((BITCOIN_FUNDING_SPENT, tx: Transaction), d: DATA_NEGOTIATING) if tx.txid == d.commitments.remoteCommit.txid => handleRemoteSpentCurrent(tx, d)
case Event(WatchEventSpent(BITCOIN_FUNDING_SPENT, tx: Transaction), d: DATA_NEGOTIATING) if tx.txid == d.commitments.remoteCommit.txid => handleRemoteSpentCurrent(tx, d)
case Event((BITCOIN_FUNDING_SPENT, tx: Transaction), d: DATA_NEGOTIATING) => handleRemoteSpentOther(tx, d)
case Event(WatchEventSpent(BITCOIN_FUNDING_SPENT, tx: Transaction), d: DATA_NEGOTIATING) => handleRemoteSpentOther(tx, d)
case Event(e: Error, d: DATA_NEGOTIATING) => handleRemoteError(e, d)
@ -628,29 +634,29 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, paymentHandler: Acto
when(CLOSING) {
case Event((BITCOIN_FUNDING_SPENT, tx: Transaction), d: DATA_CLOSING) if tx.txid == d.commitments.localCommit.publishableTxs.commitTx.tx.txid =>
case Event(WatchEventSpent(BITCOIN_FUNDING_SPENT, tx: Transaction), d: DATA_CLOSING) if tx.txid == d.commitments.localCommit.publishableTxs.commitTx.tx.txid =>
// we just initiated a uniclose moments ago and are now receiving the blockchain notification, there is nothing to do
stay()
case Event((BITCOIN_FUNDING_SPENT, tx: Transaction), d: DATA_CLOSING) if Some(tx.txid) == d.mutualClosePublished.map(_.txid) =>
case Event(WatchEventSpent(BITCOIN_FUNDING_SPENT, tx: Transaction), d: DATA_CLOSING) if Some(tx.txid) == d.mutualClosePublished.map(_.txid) =>
// we just published a mutual close tx, we are notified but it's alright
stay()
case Event((BITCOIN_FUNDING_SPENT, tx: Transaction), d: DATA_CLOSING) if tx.txid == d.commitments.remoteCommit.txid =>
case Event(WatchEventSpent(BITCOIN_FUNDING_SPENT, tx: Transaction), d: DATA_CLOSING) if tx.txid == d.commitments.remoteCommit.txid =>
// counterparty may attempt to spend its last commit tx at any time
handleRemoteSpentCurrent(tx, d)
case Event((BITCOIN_FUNDING_SPENT, tx: Transaction), d: DATA_CLOSING) =>
case Event(WatchEventSpent(BITCOIN_FUNDING_SPENT, tx: Transaction), d: DATA_CLOSING) =>
// counterparty may attempt to spend a revoked commit tx at any time
handleRemoteSpentOther(tx, d)
case Event(BITCOIN_CLOSE_DONE, d: DATA_CLOSING) if d.mutualClosePublished.isDefined => goto(CLOSED)
case Event(WatchEventConfirmed(BITCOIN_CLOSE_DONE, _, _), d: DATA_CLOSING) if d.mutualClosePublished.isDefined => goto(CLOSED)
case Event(BITCOIN_SPEND_OURS_DONE, d: DATA_CLOSING) if d.localCommitPublished.isDefined => goto(CLOSED)
case Event(WatchEventConfirmed(BITCOIN_SPEND_OURS_DONE, _, _), d: DATA_CLOSING) if d.localCommitPublished.isDefined => goto(CLOSED)
case Event(BITCOIN_SPEND_THEIRS_DONE, d: DATA_CLOSING) if d.remoteCommitPublished.isDefined => goto(CLOSED)
case Event(WatchEventConfirmed(BITCOIN_SPEND_THEIRS_DONE, _, _), d: DATA_CLOSING) if d.remoteCommitPublished.isDefined => goto(CLOSED)
case Event(BITCOIN_PUNISHMENT_DONE, d: DATA_CLOSING) if d.revokedCommitPublished.size > 0 => goto(CLOSED)
case Event(WatchEventConfirmed(BITCOIN_PUNISHMENT_DONE, _, _), d: DATA_CLOSING) if d.revokedCommitPublished.size > 0 => goto(CLOSED)
case Event(e: Error, d: DATA_CLOSING) => stay // nothing to do, there is already a spending tx published
}
@ -669,7 +675,11 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, paymentHandler: Acto
whenUnhandled {
case Event(BITCOIN_FUNDING_LOST, _) => goto(ERR_FUNDING_LOST)
case Event(msg: RoutingMessage, _) =>
router forward msg
stay
case Event(WatchEventLost(BITCOIN_FUNDING_LOST), _) => goto(ERR_FUNDING_LOST)
case Event(CMD_GETSTATE, _) =>
sender ! stateName
@ -686,7 +696,7 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, paymentHandler: Acto
case c: DATA_WAIT_FOR_ACCEPT_CHANNEL => c.temporaryChannelId
case c: DATA_WAIT_FOR_FUNDING_CREATED => c.temporaryChannelId
case c: DATA_WAIT_FOR_FUNDING_LOCKED_INTERNAL => c.temporaryChannelId
case c: DATA_NORMAL => c.channelId
case c: DATA_NORMAL => c.commitments.channelId
case c: DATA_SHUTDOWN => c.channelId
case c: DATA_NEGOTIATING => c.channelId
case c: DATA_CLOSING => 0L
@ -699,7 +709,7 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, paymentHandler: Acto
}
onTransition {
case previousState -> currentState => context.system.eventStream.publish(ChannelChangedState(self, theirNodeId, previousState, currentState, stateData))
case previousState -> currentState => context.system.eventStream.publish(ChannelChangedState(self, context.parent, theirNodeId, previousState, currentState, stateData))
}
/*
@ -713,55 +723,6 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, paymentHandler: Acto
888 888 d88P 888 888 Y888 8888888P" 88888888 8888888888 888 T88b "Y8888P"
*/
def propagateUpstream(add: UpdateAddHtlc, anchorId: BinaryData) = {
// TODO: relaying of payments is disabled
/*val r = route.parseFrom(add.onionRoutingPacket)
r.steps match {
case route_step(amountMsat, Next.Bitcoin(nextNodeId)) +: rest =>
log.debug(s"propagating htlc #${add.id} to $nextNodeId")
import ExecutionContext.Implicits.global
context.system.actorSelection(Register.actorPathToNodeId(nextNodeId))
.resolveOne(3 seconds)
.onComplete {
case Success(upstream) =>
log.info(s"forwarding htlc #${add.id} to upstream=$upstream")
val upstream_route = route(rest)
// TODO: we should decrement expiry !!
upstream ! CMD_ADD_HTLC(amountMsat, add.paymentHash, add.expiry, upstream_route, Some(Origin(anchorId, add.id)))
upstream ! CMD_SIGN
case Failure(t: Throwable) =>
// TODO: send "fail route error"
log.warning(s"couldn't resolve upstream node, htlc #${add.id} will timeout", t)
}
case route_step(amount, Next.End(true)) +: rest =>*/
log.info(s"we are the final recipient of htlc #${add.id}")
context.system.eventStream.publish(PaymentReceived(self, add.paymentHash))
paymentHandler ! add
//}
}
def propagateDownstream(htlc: UpdateAddHtlc, fail_or_fulfill: Either[UpdateFailHtlc, UpdateFulfillHtlc], origin_opt: Option[Origin]) = {
(origin_opt, fail_or_fulfill) match {
// TODO: relaying of payments is disabled
/*case (Some(origin), Left(fail)) =>
val downstream = context.system.actorSelection(Register.actorPathToChannelId(origin.channelId))
downstream ! CMD_SIGN
downstream ! CMD_FAIL_HTLC(origin.htlc_id, fail.reason.toStringUtf8)
downstream ! CMD_SIGN
case (Some(origin), Right(fulfill)) =>
val downstream = context.system.actorSelection(Register.actorPathToChannelId(origin.channelId))
downstream ! CMD_SIGN
downstream ! CMD_FULFILL_HTLC(origin.htlc_id, fulfill.paymentPreimage)
downstream ! CMD_SIGN*/
case (None, Left(fail)) =>
log.info(s"we were the origin payer for htlc #${htlc.id}")
context.system.eventStream.publish(PaymentFailed(self, htlc.paymentHash, fail.reason.toStringUtf8))
case (None, Right(fulfill)) =>
log.info(s"we were the origin payer for htlc #${htlc.id}")
context.system.eventStream.publish(PaymentSent(self, htlc.paymentHash))
}
}
def handleCommandSuccess(sender: ActorRef, msg: LightningMessage, newData: Data) = {
them ! msg
sender ! "ok"

View file

@ -1,6 +1,7 @@
package fr.acinq.eclair.channel
import akka.actor.ActorRef
import fr.acinq.bitcoin.Crypto.PublicKey
import fr.acinq.bitcoin.{BinaryData, Satoshi}
/**
@ -9,16 +10,10 @@ import fr.acinq.bitcoin.{BinaryData, Satoshi}
trait ChannelEvent
case class ChannelCreated(channel: ActorRef, params: LocalParams, theirNodeId: String) extends ChannelEvent
case class ChannelCreated(channel: ActorRef, params: LocalParams, remoteNodeId: PublicKey) extends ChannelEvent
case class ChannelIdAssigned(channel: ActorRef, channelId: BinaryData, amount: Satoshi) extends ChannelEvent
case class ChannelChangedState(channel: ActorRef, theirNodeId: BinaryData, previousState: State, currentState: State, currentData: Data) extends ChannelEvent
case class ChannelChangedState(channel: ActorRef, transport: ActorRef, remoteNodeId: PublicKey, previousState: State, currentState: State, currentData: Data) extends ChannelEvent
case class ChannelSignatureReceived(channel: ActorRef, Commitments: Commitments) extends ChannelEvent
case class PaymentSent(channel: ActorRef, h: BinaryData) extends ChannelEvent
case class PaymentFailed(channel: ActorRef, h: BinaryData, reason: String) extends ChannelEvent
case class PaymentReceived(channel: ActorRef, h: BinaryData) extends ChannelEvent

View file

@ -4,8 +4,7 @@ import fr.acinq.bitcoin.Crypto.{Point, PrivateKey, PublicKey, Scalar}
import fr.acinq.bitcoin.{BinaryData, ScriptElt, Transaction}
import fr.acinq.eclair.transactions.CommitmentSpec
import fr.acinq.eclair.transactions.Transactions.CommitTx
import fr.acinq.eclair.wire.{ClosingSigned, FundingLocked, Shutdown}
import lightning.{route, route_step}
import fr.acinq.eclair.wire.{ClosingSigned, FundingLocked, Shutdown, UpdateAddHtlc}
import scala.concurrent.duration.FiniteDuration
@ -85,14 +84,10 @@ case object BITCOIN_CLOSE_DONE extends BitcoinEvent
sealed trait Command
/*
Used when relaying payments
*/
final case class Origin(channelId: BinaryData, htlc_id: Long)
/**
* @param id should only be provided in tests otherwise it will be assigned automatically
*/
final case class CMD_ADD_HTLC(amountMsat: Long, paymentHash: BinaryData, expiry: Long, payment_route: route = route(route_step(0, next = route_step.Next.End(true)) :: Nil), origin: Option[Origin] = None, id: Option[Long] = None, commit: Boolean = false) extends Command
final case class CMD_ADD_HTLC(amountMsat: Long, paymentHash: BinaryData, expiry: Long, onion: BinaryData = BinaryData("00" * 1254), origin: Option[UpdateAddHtlc] = None, id: Option[Long] = 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: String, commit: Boolean = false) extends Command
case object CMD_SIGN extends Command
@ -119,6 +114,7 @@ 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: Seq[Transaction], htlcTimeoutTxs: Seq[Transaction], claimHtlcDelayedTx: Seq[Transaction])
@ -131,11 +127,10 @@ final case class DATA_WAIT_FOR_FUNDING_INTERNAL(temporaryChannelId: Long, params
final case class DATA_WAIT_FOR_FUNDING_CREATED(temporaryChannelId: Long, params: ChannelParams, pushMsat: Long, remoteFirstPerCommitmentPoint: Point) extends Data
final case class DATA_WAIT_FOR_FUNDING_SIGNED(temporaryChannelId: Long, params: ChannelParams, fundingTx: Transaction, localSpec: CommitmentSpec, localCommitTx: CommitTx, remoteCommit: RemoteCommit) extends Data
final case class DATA_WAIT_FOR_FUNDING_LOCKED_INTERNAL(temporaryChannelId: Long, params: ChannelParams, commitments: Commitments, deferred: Option[FundingLocked]) extends Data with HasCommitments
final case class DATA_NORMAL(channelId: Long, params: ChannelParams, commitments: Commitments, localShutdown: Option[Shutdown], downstreams: Map[Long, Option[Origin]]) extends Data with HasCommitments
final case class DATA_SHUTDOWN(channelId: Long, params: ChannelParams, commitments: Commitments,
localShutdown: Shutdown, remoteShutdown: Shutdown,
downstreams: Map[Long, Option[Origin]]) extends Data with HasCommitments
final case class DATA_NEGOTIATING(channelId: Long, params: ChannelParams, commitments: Commitments,
final case class DATA_NORMAL(params: ChannelParams, commitments: Commitments, localShutdown: Option[Shutdown]) extends Data with HasCommitments
final case class DATA_SHUTDOWN(params: ChannelParams, commitments: Commitments,
localShutdown: Shutdown, remoteShutdown: Shutdown) extends Data with HasCommitments
final case class DATA_NEGOTIATING(params: ChannelParams, commitments: Commitments,
localShutdown: Shutdown, remoteShutdown: Shutdown, localClosingSigned: ClosingSigned) extends Data with HasCommitments
final case class DATA_CLOSING(commitments: Commitments,
ourSignature: Option[ClosingSigned] = None,

View file

@ -67,8 +67,7 @@ object Commitments extends Logging {
throw new RuntimeException(s"insufficient funds (available=$available msat)")
} else {
val id = cmd.id.getOrElse(commitments.localCurrentHtlcId + 1)
// TODO: fix routing
val add: UpdateAddHtlc = UpdateAddHtlc(commitments.channelId, id, cmd.amountMsat, cmd.expiry, cmd.paymentHash, "" /*routing(ByteString.copyFrom(cmd.payment_route.toByteArray))*/)
val add = UpdateAddHtlc(commitments.channelId, id, cmd.amountMsat, cmd.expiry, cmd.paymentHash, cmd.onion)
val commitments1 = addLocalProposal(commitments, add).copy(localCurrentHtlcId = id)
(commitments1, add)
}

View file

@ -224,10 +224,10 @@ object Helpers {
* 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
* 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 list of transactions (one per HTLC that we can claim) if the tx is a revoked commitment, [[None]] otherwise
* @return a [[RevokedCommitPublished]] object containing punishment transactions if the tx is a revoked commitment
*/
def claimRevokedRemoteCommitTxOutputs(commitments: Commitments, tx: Transaction): Option[RevokedCommitPublished] = {
import commitments._

View file

@ -2,8 +2,8 @@ package fr.acinq.eclair.channel
import akka.actor.{Props, _}
import akka.util.Timeout
import fr.acinq.bitcoin.Crypto.PrivateKey
import fr.acinq.bitcoin.{BinaryData, DeterministicWallet, Satoshi, ScriptElt}
import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey}
import fr.acinq.bitcoin.{BinaryData, DeterministicWallet, MilliSatoshi, Satoshi, ScriptElt}
import fr.acinq.eclair.Globals
import fr.acinq.eclair.crypto.Noise.KeyPair
import fr.acinq.eclair.crypto.TransportHandler
@ -32,14 +32,14 @@ import scala.concurrent.duration._
* ├── client (0..m, transient)
* └── api
*/
class Register(blockchain: ActorRef, paymentHandler: ActorRef, defaultFinalScriptPubKey: Seq[ScriptElt]) extends Actor with ActorLogging {
class Register(watcher: ActorRef, router: ActorRef, relayer: ActorRef, defaultFinalScriptPubKey: Seq[ScriptElt]) extends Actor with ActorLogging {
import Register._
def receive: Receive = main(0L)
def main(counter: Long): Receive = {
case CreateChannel(connection, pubkey, amount_opt) =>
case CreateChannel(connection, pubkey, funding_opt, pushmsat_opt) =>
def generateKey(index: Long): PrivateKey = DeterministicWallet.derivePrivateKey(Globals.Node.extendedPrivateKey, index :: counter :: Nil).privateKey
val localParams = LocalParams(
@ -47,8 +47,8 @@ class Register(blockchain: ActorRef, paymentHandler: ActorRef, defaultFinalScrip
maxHtlcValueInFlightMsat = Long.MaxValue,
channelReserveSatoshis = 0,
htlcMinimumMsat = 0,
feeratePerKw = Globals.default_feeratePerKw,
toSelfDelay = Globals.default_delay_blocks,
feeratePerKw = Globals.feeratePerKw,
toSelfDelay = Globals.delay_blocks,
maxAcceptedHtlcs = 100,
fundingPrivKey = generateKey(0),
revocationSecret = generateKey(1),
@ -56,13 +56,17 @@ class Register(blockchain: ActorRef, paymentHandler: ActorRef, defaultFinalScrip
delayedPaymentKey = generateKey(3),
defaultFinalScriptPubKey = defaultFinalScriptPubKey,
shaSeed = Globals.Node.seed,
isFunder = amount_opt.isDefined
isFunder = funding_opt.isDefined
)
def makeChannel(conn: ActorRef, publicKey: BinaryData): ActorRef = {
val channel = context.actorOf(Channel.props(conn, blockchain, paymentHandler, localParams, publicKey.toString(), Some(Globals.autosign_interval)), s"channel-$counter")
amount_opt match {
case Some(amount) => channel ! INPUT_INIT_FUNDER(amount.amount, 0)
def makeChannel(conn: ActorRef, publicKey: PublicKey, ctx: ActorContext): ActorRef = {
// note that we use transport's context and not register's context
val channel = ctx.actorOf(Channel.props(conn, watcher, router, relayer, localParams, publicKey, Some(Globals.autosign_interval)), "channel")
funding_opt match {
case Some(funding) => pushmsat_opt match {
case Some(pushmsat) => channel ! INPUT_INIT_FUNDER(funding.amount, pushmsat.amount)
case None => channel ! INPUT_INIT_FUNDER(funding.amount, 0)
}
case None => channel ! INPUT_INIT_FUNDEE()
}
channel
@ -72,7 +76,7 @@ class Register(blockchain: ActorRef, paymentHandler: ActorRef, defaultFinalScrip
new TransportHandler[LightningMessage](
KeyPair(Globals.Node.publicKey.toBin, Globals.Node.privateKey.toBin),
pubkey,
isWriter = amount_opt.isDefined,
isWriter = funding_opt.isDefined,
them = connection,
listenerFactory = makeChannel,
serializer = LightningMessageSerializer)),
@ -95,14 +99,14 @@ class Register(blockchain: ActorRef, paymentHandler: ActorRef, defaultFinalScrip
object Register {
def props(blockchain: ActorRef, paymentHandler: ActorRef, defaultFinalScriptPubKey: Seq[ScriptElt]) = Props(classOf[Register], blockchain, paymentHandler, defaultFinalScriptPubKey)
def props(blockchain: ActorRef, router: ActorRef, relayer: ActorRef, defaultFinalScriptPubKey: Seq[ScriptElt]) = Props(classOf[Register], blockchain, router, relayer, defaultFinalScriptPubKey)
// @formatter:off
case class CreateChannel(connection: ActorRef, pubkey: Option[BinaryData], anchorAmount: Option[Satoshi])
case class CreateChannel(connection: ActorRef, pubkey: Option[BinaryData], fundingSatoshis: Option[Satoshi], pushMsat: Option[MilliSatoshi])
case class ListChannels()
case class SendCommand(channelId: String, cmd: Command)
case class SendCommand(channelId: Long, cmd: Command)
// @formatter:on
@ -110,22 +114,22 @@ object Register {
* Once it reaches NORMAL state, channel creates a [[fr.acinq.eclair.channel.AliasActor]]
* which name is counterparty_id-anchor_id
*/
def create_alias(node_id: BinaryData, anchor_id: BinaryData)(implicit context: ActorContext) =
context.actorOf(Props(new AliasActor(context.self)), name = s"$node_id-$anchor_id")
def createAlias(nodeAddress: BinaryData, channelId: Long)(implicit context: ActorContext) =
context.actorOf(Props(new AliasActor(context.self)), name = s"$nodeAddress-${java.lang.Long.toUnsignedString(channelId)}")
def actorPathToNodeId(system: ActorSystem, nodeId: BinaryData): ActorPath =
system / "register" / "auth-handler-*" / "channel" / s"${nodeId}-*"
def actorPathToNodeAddress(system: ActorSystem, nodeAddress: BinaryData): ActorPath =
system / "register" / "transport-handler-*" / "channel" / s"$nodeAddress-*"
def actorPathToNodeId(nodeId: BinaryData)(implicit context: ActorContext): ActorPath = actorPathToNodeId(context.system, nodeId)
def actorPathToNodeAddress(nodeAddress: BinaryData)(implicit context: ActorContext): ActorPath = actorPathToNodeAddress(context.system, nodeAddress)
def actorPathToChannelId(system: ActorSystem, channelId: BinaryData): ActorPath =
system / "register" / "auth-handler-*" / "channel" / s"*-${channelId}"
def actorPathToChannelId(system: ActorSystem, channelId: Long): ActorPath =
system / "register" / "transport-handler-*" / "channel" / s"*-${channelId}"
def actorPathToChannelId(channelId: BinaryData)(implicit context: ActorContext): ActorPath = actorPathToChannelId(context.system, channelId)
def actorPathToChannelId(channelId: Long)(implicit context: ActorContext): ActorPath = actorPathToChannelId(context.system, channelId)
def actorPathToChannels()(implicit context: ActorContext): ActorPath =
context.system / "register" / "auth-handler-*" / "channel"
context.system / "register" / "transport-handler-*" / "channel"
def actorPathToHandlers()(implicit context: ActorContext): ActorPath =
context.system / "register" / "auth-handler-*"
def actorPathToTransportHandlers()(implicit context: ActorContext): ActorPath =
context.system / "register" / "transport-handler-*"
}

View file

@ -101,7 +101,6 @@ object ChaCha20Poly1305 extends Logging {
* @param aad additional authentication data. can be empty
* @param mac authentication mac
* @return the decrypted plaintext if the mac is valid.
* @throws IllegalArgumentException error if the mac is invalid
*/
def decrypt(key: BinaryData, nonce: BinaryData, ciphertext: BinaryData, aad: BinaryData, mac: BinaryData): BinaryData = {
val polykey: BinaryData = ChaCha20.encrypt(new Array[Byte](32), key, nonce)

View file

@ -107,7 +107,7 @@ object Sphinx {
* @param associatedData associated data
* @param packet packet received by this node
* @return a (payload, address, packet) tuple where:
* - payload is the teh per-hop payload for this node
* - payload is the per-hop payload for this node
* - address is the next destination. 0x0000000000000000000000000000000000000000 means this node was the final
* destination
* - packet is the next packet, to be forwarded to address

View file

@ -2,9 +2,11 @@ package fr.acinq.eclair.crypto
import java.nio.ByteOrder
import akka.actor.{Actor, ActorContext, ActorRef, LoggingFSM, Terminated}
import akka.actor.{Actor, ActorRef, LoggingFSM, OneForOneStrategy, SupervisorStrategy, 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.channel.{CMD_CLOSE, Command}
import fr.acinq.eclair.crypto.Noise._
@ -29,7 +31,7 @@ import scala.util.{Failure, Success, Try}
* @param listenerFactory factory that will be used to create the listener that will receive decrypted messages once the
* handshake phase as been completed. Its parameters are a tuple (transport handler, remote public key)
*/
class TransportHandler[T: ClassTag](keyPair: KeyPair, rs: Option[BinaryData], them: ActorRef, isWriter: Boolean, listenerFactory: (ActorRef, BinaryData) => ActorRef, serializer: TransportHandler.Serializer[T]) extends Actor with LoggingFSM[TransportHandler.State, TransportHandler.Data] {
class TransportHandler[T: ClassTag](keyPair: KeyPair, rs: Option[BinaryData], them: ActorRef, isWriter: Boolean, listenerFactory: (ActorRef, PublicKey, ActorContext) => ActorRef, serializer: TransportHandler.Serializer[T]) extends Actor with LoggingFSM[TransportHandler.State, TransportHandler.Data] {
import TransportHandler._
@ -70,7 +72,7 @@ class TransportHandler[T: ClassTag](keyPair: KeyPair, rs: Option[BinaryData], th
reader.read(payload) match {
case (writer, _, Some((dec, enc, ck))) =>
val listener = listenerFactory(self, writer.rs)
val listener = listenerFactory(self, PublicKey(writer.rs), context)
context.watch(listener)
val (nextStateData, plaintextMessages) = WaitingForCyphertextData(ExtendedCipherState(enc, ck), ExtendedCipherState(dec, ck), None, remainder, listener).decrypt
sendToListener(listener, plaintextMessages)
@ -87,7 +89,7 @@ class TransportHandler[T: ClassTag](keyPair: KeyPair, rs: Option[BinaryData], th
}
case (_, message, Some((enc, dec, ck))) => {
them ! Write(TransportHandler.prefix +: message)
val listener = listenerFactory(self, writer.rs)
val listener = listenerFactory(self, PublicKey(writer.rs), context)
context.watch(listener)
val (nextStateData, plaintextMessages) = WaitingForCyphertextData(ExtendedCipherState(enc, ck), ExtendedCipherState(dec, ck), None, remainder, listener).decrypt
sendToListener(listener, plaintextMessages)

View file

@ -15,7 +15,6 @@ import fr.acinq.eclair.Setup
import fr.acinq.eclair.channel.ChannelEvent
import fr.acinq.eclair.gui.controllers.MainController
import fr.acinq.eclair.gui.stages.SplashStage
import fr.acinq.eclair.router.NetworkEvent
import grizzled.slf4j.Logging
/**
@ -40,7 +39,6 @@ class FxApp extends Application with Logging {
val controller = new MainController(handlers, primaryStage, setup)
val guiUpdater = setup.system.actorOf(Props(classOf[GUIUpdater], primaryStage, controller, setup), "gui-updater")
setup.system.eventStream.subscribe(guiUpdater, classOf[ChannelEvent])
setup.system.eventStream.subscribe(guiUpdater, classOf[NetworkEvent])
import scala.concurrent.ExecutionContext.Implicits.global
setup.fatalEventFuture onSuccess {

View file

@ -7,14 +7,10 @@ import javafx.scene.layout.VBox
import javafx.stage.Stage
import akka.actor.{Actor, ActorLogging, ActorRef}
import com.mxgraph.layout.mxCircleLayout
import com.mxgraph.swing.mxGraphComponent
import fr.acinq.bitcoin._
import fr.acinq.eclair.channel._
import fr.acinq.eclair.gui.controllers.{ChannelPaneController, MainController}
import fr.acinq.eclair.router.{ChannelDesc, ChannelDiscovered}
import fr.acinq.eclair.{Globals, Setup}
import org.jgrapht.ext.JGraphXAdapter
import org.jgrapht.graph.{DefaultEdge, SimpleGraph}
@ -38,7 +34,7 @@ class GUIUpdater(primaryStage: Stage, mainController: MainController, setup: Set
log.info(s"new channel: $channel")
val loader = new FXMLLoader(getClass.getResource("/gui/main/channelPane.fxml"))
val channelPaneController = new ChannelPaneController(theirNodeId)
val channelPaneController = new ChannelPaneController(theirNodeId.toBin.toString())
loader.setController(channelPaneController)
val root = loader.load[VBox]
@ -69,7 +65,7 @@ class GUIUpdater(primaryStage: Stage, mainController: MainController, setup: Set
}
})
case ChannelChangedState(channel, _, previousState, currentState, currentData) =>
case ChannelChangedState(channel, _, _, previousState, currentState, currentData) =>
val channelPane = m(channel)
Platform.runLater(new Runnable() {
override def run(): Unit = {
@ -87,7 +83,8 @@ class GUIUpdater(primaryStage: Stage, mainController: MainController, setup: Set
}
})
case ChannelDiscovered(ChannelDesc(id, a, b)) =>
// TODO
/*case ChannelDiscovered(ChannelDesc(id, a, b)) =>
graph.addVertex(BinaryData(a))
graph.addVertex(BinaryData(b))
graph.addEdge(a, b, new NamedEdge(id))
@ -100,7 +97,7 @@ class GUIUpdater(primaryStage: Stage, mainController: MainController, setup: Set
lay.execute(jgxAdapter.getDefaultParent())
mainController.swingNode.setContent(component)
}
})
})*/
}
}

View file

@ -4,7 +4,7 @@ package fr.acinq.eclair.gui
import javafx.application.Platform
import javafx.scene.control.{TextArea, TextField}
import fr.acinq.bitcoin.{BinaryData, Satoshi}
import fr.acinq.bitcoin.{BinaryData, MilliSatoshi, Satoshi}
import fr.acinq.eclair._
import fr.acinq.eclair.io.Client
import fr.acinq.eclair.payment.CreatePayment
@ -17,12 +17,12 @@ class Handlers(setup: Setup) extends Logging {
import setup._
def open(hostPort: String, amount: Satoshi) = {
def open(hostPort: String, fundingSatoshis: Satoshi, pushMsat: MilliSatoshi) = {
val regex = "([a-fA-F0-9]+)@([a-zA-Z0-9\\.\\-_]+):([0-9]+)".r
hostPort match {
case regex(pubkey, host, port) =>
logger.info(s"connecting to $host:$port")
system.actorOf(Client.props(host, port.toInt, pubkey, amount, register))
system.actorOf(Client.props(host, port.toInt, pubkey, fundingSatoshis, pushMsat, register))
case _ => {}
}
}

View file

@ -1,10 +1,10 @@
package fr.acinq.eclair.gui.controllers
import javafx.fxml.FXML
import javafx.scene.control.{Button, ContextMenu, Label, ProgressBar}
import javafx.scene.input.ContextMenuEvent
import javafx.scene.control.{Button, ContextMenu, TextField, ProgressBar}
import javafx.scene.input.{ContextMenuEvent, MouseEvent}
import fr.acinq.eclair.gui.utils.ContextMenuUtils
import fr.acinq.eclair.gui.utils.{ContextMenuUtils, CopyAction}
import grizzled.slf4j.Logging
/**
@ -13,26 +13,27 @@ import grizzled.slf4j.Logging
class ChannelPaneController(theirNodeIdValue: String) extends Logging {
var channelIdValue = ""
@FXML var channelId: Label = _
@FXML var channelId: TextField = _
@FXML var balanceBar: ProgressBar = _
@FXML var amountUs: Label = _
@FXML var nodeId: Label = _
@FXML var capacity: Label = _
@FXML var funder: Label = _
@FXML var state: Label = _
@FXML var amountUs: TextField = _
@FXML var nodeId: TextField = _
@FXML var capacity: TextField = _
@FXML var funder: TextField = _
@FXML var state: TextField = _
@FXML var close: Button = _
var contextMenu: ContextMenu = _
@FXML def handleChannelIdContext(event: ContextMenuEvent): Unit = {
@FXML def openChannelContext(event: ContextMenuEvent): Unit = {
if (contextMenu != null) contextMenu.hide()
contextMenu = ContextMenuUtils.buildCopyContext(channelIdValue)
contextMenu = ContextMenuUtils.buildCopyContext(List(
new CopyAction("Copy Channel Id", channelIdValue),
new CopyAction("Copy Node Pubkey", theirNodeIdValue)
))
contextMenu.show(channelId, event.getScreenX, event.getScreenY)
}
@FXML def handleTheirNodeIdContext(event: ContextMenuEvent): Unit = {
@FXML def closeChannelContext(event: MouseEvent): Unit = {
if (contextMenu != null) contextMenu.hide()
contextMenu = ContextMenuUtils.buildCopyContext(theirNodeIdValue)
contextMenu.show(nodeId, event.getScreenX, event.getScreenY)
}
}

View file

@ -4,14 +4,14 @@ import javafx.beans.value.{ChangeListener, ObservableValue}
import javafx.embed.swing.SwingNode
import javafx.fxml.FXML
import javafx.scene.control.{ContextMenu, Label, MenuItem, Tab}
import javafx.scene.input.ContextMenuEvent
import javafx.scene.input.{ContextMenuEvent, MouseEvent}
import javafx.scene.layout.{BorderPane, TilePane, VBox}
import javafx.stage.Stage
import com.mxgraph.swing.mxGraphComponent
import fr.acinq.eclair.gui.Handlers
import fr.acinq.eclair.gui.stages.{AboutStage, OpenChannelStage, ReceivePaymentStage, SendPaymentStage}
import fr.acinq.eclair.gui.utils.ContextMenuUtils
import fr.acinq.eclair.gui.utils.{ContextMenuUtils, CopyAction}
import fr.acinq.eclair.{Globals, Setup}
import grizzled.slf4j.Logging
@ -33,6 +33,7 @@ class MainController(val handlers: Handlers, val stage: Stage, val setup: Setup)
@FXML var channelInfo: VBox = _
@FXML var channelBox: VBox = _
@FXML var tilePane: TilePane = _
@FXML var channelsTab: Tab = _
@FXML var graphTab: Tab = _
val swingNode: SwingNode = new SwingNode()
@ -49,7 +50,9 @@ class MainController(val handlers: Handlers, val stage: Stage, val setup: Setup)
bitcoinChain.getStyleClass.add(setup.chain)
graphTab.setContent(swingNode)
contextMenu = ContextMenuUtils.buildCopyContext(Globals.Node.id)
contextMenu = ContextMenuUtils.buildCopyContext(List(
new CopyAction("Copy Pubkey", Globals.Node.id),
new CopyAction("Copy URI", s"${Globals.Node.id}@${Globals.Node.address.getHostString}:${Globals.Node.address.getPort}" )))
if (channelBox.getChildren.size() > 0) {
channelInfo.setScaleY(0)
@ -61,7 +64,11 @@ class MainController(val handlers: Handlers, val stage: Stage, val setup: Setup)
if (channelBox.getChildren.size() > 0) {
channelInfo.setScaleY(0)
channelInfo.setOpacity(0)
} else {
channelInfo.setScaleY(1)
channelInfo.setOpacity(1)
}
channelsTab.setText(s"Channels (${channelBox.getChildren.size})")
}
})
}
@ -107,10 +114,14 @@ class MainController(val handlers: Handlers, val stage: Stage, val setup: Setup)
}
}
@FXML def handleNodeIdContext(event: ContextMenuEvent): Unit = {
@FXML def openNodeIdContext(event: ContextMenuEvent): Unit = {
contextMenu.show(labelNodeId, event.getScreenX, event.getScreenY)
}
@FXML def closeNodeIdContext(event: MouseEvent): Unit = {
contextMenu.hide()
}
def positionAtCenter(childStage: Stage): Unit = {
childStage.setX(stage.getX() + stage.getWidth() / 2 - childStage.getWidth() / 2)
childStage.setY(stage.getY() + stage.getHeight() / 2 - childStage.getHeight() / 2)

View file

@ -5,7 +5,7 @@ import javafx.fxml.FXML
import javafx.scene.control.{Button, ComboBox, Label, TextField}
import javafx.stage.Stage
import fr.acinq.bitcoin.Satoshi
import fr.acinq.bitcoin.{MilliSatoshi, Satoshi}
import fr.acinq.eclair.Setup
import fr.acinq.eclair.gui.Handlers
import fr.acinq.eclair.gui.utils.GUIValidators
@ -18,8 +18,10 @@ class OpenChannelController(val handlers: Handlers, val stage: Stage, val setup:
@FXML var host: TextField = _
@FXML var hostError: Label = _
@FXML var amount: TextField = _
@FXML var amountError: Label = _
@FXML var fundingSatoshis: TextField = _
@FXML var fundingSatoshisError: Label = _
@FXML var pushMsat: TextField = _
@FXML var pushMsatError: Label = _
@FXML var unit: ComboBox[String] = _
@FXML var button: Button = _
@ -28,16 +30,28 @@ class OpenChannelController(val handlers: Handlers, val stage: Stage, val setup:
}
@FXML def handleOpen(event: ActionEvent): Unit = {
if (GUIValidators.validate(host.getText, hostError, GUIValidators.hostRegex)
&& GUIValidators.validate(amount.getText, amountError, GUIValidators.amountRegex)) {
val raw = amount.getText.toLong
val smartAmount = unit.getValue match {
case "milliBTC" => Satoshi(raw * 100000L)
case "Satoshi" => Satoshi(raw)
case "milliSatoshi" => Satoshi(raw / 1000L)
if (GUIValidators.validate(host.getText, hostError, "Please use a valid url (pubkey@host:port)", GUIValidators.hostRegex)
& GUIValidators.validate(fundingSatoshis.getText, fundingSatoshisError, "Funding must be numeric", GUIValidators.amountRegex)
&& GUIValidators.validate(fundingSatoshisError, "Funding must be greater than 0", fundingSatoshis.getText.toLong > 0)) {
val rawFunding = fundingSatoshis.getText.toLong
val smartFunding = unit.getValue match {
case "milliBTC" => Satoshi(rawFunding * 100000L)
case "Satoshi" => Satoshi(rawFunding)
case "milliSatoshi" => Satoshi(rawFunding / 1000L)
}
if (GUIValidators.validate(fundingSatoshisError, "Funding must be 0.1 BTC or less", smartFunding.toLong <= 10000000)) {
if (!pushMsat.getText.isEmpty) {
// pushMsat is optional, so we validate field only if it isn't empty
if (GUIValidators.validate(pushMsat.getText, pushMsatError, "Push mSat must be numeric", GUIValidators.amountRegex)) {
handlers.open(host.getText, smartFunding, MilliSatoshi(pushMsat.getText.toLong))
stage.close()
}
} else {
handlers.open(host.getText, smartFunding, Satoshi(0))
stage.close()
}
}
handlers.open(host.getText, smartAmount)
stage.close()
}
}

View file

@ -27,7 +27,9 @@ class ReceivePaymentController(val handlers: Handlers, val stage: Stage, val set
}
@FXML def handleGenerate(event: ActionEvent): Unit = {
if (GUIValidators.validate(amount.getText, amountError, GUIValidators.amountRegex)) {
if (GUIValidators.validate(amount.getText, amountError, "Amount must be numeric", GUIValidators.amountRegex)
&& GUIValidators.validate(amountError, "Amount must be greater than 0", amount.getText.toLong > 0)
&& GUIValidators.validate(amountError, "Amount must be 4200000 satoshis or less", amount.getText.toLong <= 4200000)) {
Try(amount.getText.toLong) match {
case Success(amountMsat) => handlers.getPaymentRequest(amount.getText.toLong, paymentRequest)
case _ => {}

View file

@ -3,7 +3,7 @@ package fr.acinq.eclair.gui.controllers
import javafx.beans.value.{ChangeListener, ObservableValue}
import javafx.event.ActionEvent
import javafx.fxml.FXML
import javafx.scene.control.{Label, TextArea}
import javafx.scene.control.{Label, TextArea, TextField}
import javafx.stage.Stage
import fr.acinq.eclair.Setup
@ -19,37 +19,36 @@ class SendPaymentController(val handlers: Handlers, val stage: Stage, val setup:
@FXML var paymentRequest: TextArea = _
@FXML var paymentRequestError: Label = _
@FXML var nodeIdLabel: Label = _
@FXML var amountLabel: Label = _
@FXML var hashLabel: Label = _
@FXML var nodeIdField: TextField = _
@FXML var amountField: TextField = _
@FXML var hashField: TextField = _
@FXML def initialize(): Unit = {
paymentRequest.textProperty().addListener(new ChangeListener[String] {
def changed(observable: ObservableValue[_ <: String], oldValue: String, newValue: String): Unit = {
if (GUIValidators.validate(paymentRequest.getText, paymentRequestError, GUIValidators.paymentRequestRegex)) {
if (GUIValidators.validate(paymentRequest.getText, paymentRequestError, "Please use a valid payment request", GUIValidators.paymentRequestRegex)) {
val Array(nodeId, amount, hash) = paymentRequest.getText.split(":")
amountLabel.setText(amount)
nodeIdLabel.setText(nodeId)
hashLabel.setText(hash)
amountField.setText(amount)
nodeIdField.setText(nodeId)
hashField.setText(hash)
} else {
amountLabel.setText("0")
nodeIdLabel.setText("N/A")
hashLabel.setText("N/A")
amountField.setText("0")
nodeIdField.setText("N/A")
hashField.setText("N/A")
}
}
})
}
@FXML def handleSend(event: ActionEvent): Unit = {
if (GUIValidators.validate(paymentRequest.getText, paymentRequestError, GUIValidators.paymentRequestRegex)) {
if (GUIValidators.validate(paymentRequest.getText, paymentRequestError, "Please use a valid payment request", GUIValidators.paymentRequestRegex)) {
val Array(nodeId, amount, hash) = paymentRequest.getText.split(":")
handlers.send(nodeId, hash, amount)
stage.close()
}
}
@FXML def handleClose(event: ActionEvent): Unit = {
stage.close()
}

View file

@ -17,8 +17,8 @@ class OpenChannelStage(handlers: Handlers, setup: Setup) extends Stage() {
initStyle(StageStyle.DECORATED)
getIcons().add(new Image("/gui/commons/images/eclair02.png", false))
setTitle("Open a new channel")
setWidth(1100)
setHeight(250)
setWidth(550)
setHeight(350)
setResizable(false)
// get fxml/controller

View file

@ -4,29 +4,43 @@ import javafx.event.{ActionEvent, EventHandler}
import javafx.scene.control.{ContextMenu, MenuItem}
import javafx.scene.input.{Clipboard, ClipboardContent}
import scala.collection.immutable.List
/**
* Created by DPA on 28/09/2016.
*/
/**
* Action to copy a value
*
* @param label label of the copy action in the context menu, defaults to copy value
* @param value the value to copy
*/
case class CopyAction(label: String = "Copy Value", value: String)
object ContextMenuUtils {
val clip = Clipboard.getSystemClipboard
/**
* Builds a Context Menu with a single Copy action.
* Builds a Context Menu containing a list of copy actions.
*
* @param valueToCopy the value to copy to clipboard
* @param actions list of copy action (label + value)
* @return javafx context menu
*/
def buildCopyContext(valueToCopy: String): ContextMenu = {
def buildCopyContext (actions: List[CopyAction]): ContextMenu = {
val context = new ContextMenu()
val copyItem = new MenuItem("Copy Value")
copyItem.setOnAction(new EventHandler[ActionEvent] {
override def handle(event: ActionEvent): Unit = {
val clipContent = new ClipboardContent
clipContent.putString(valueToCopy)
clip.setContent(clipContent)
}
})
context.getItems.addAll(copyItem)
return context
for (action <- actions ) {
val copyItem = new MenuItem(action.label)
copyItem.setOnAction(new EventHandler[ActionEvent] {
override def handle(event: ActionEvent): Unit = {
val clipContent = new ClipboardContent
clipContent.putString(action.value)
clip.setContent(clipContent)
}
})
context.getItems.addAll(copyItem)
}
context
}
}

View file

@ -17,22 +17,34 @@ object GUIValidators {
/**
* Validate a field against a regex. If field does not match regex, validatorLabel is shown.
*
* @param field String content of the field to validate
* @param validatorLabel JFX label associated to the field
* @param regex Scala regex that the field must match
* @return true if field is valid, false otherwise
* @param field String content of the field to validate
* @param validatorLabel JFX label associated to the field.
* @param validatorMessage Message displayed if the field is invalid. It should describe the cause of
* the validation failure
* @param regex Scala regex that the field must match
* @return true if field is valid, false otherwise
*/
def validate(field: String, validatorLabel: Label, regex: Regex): Boolean = {
def validate(field: String, validatorLabel: Label, validatorMessage: String, regex: Regex): Boolean = {
return field match {
case regex() => {
validatorLabel.setOpacity(0)
return true
return validate(validatorLabel, validatorMessage, true)
}
case _ => {
validatorLabel.setOpacity(1)
return false
return validate(validatorLabel, validatorMessage, false)
}
}
}
/**
* Displays a label with an error message.
*
* @param errorLabel JFX label containing an error messsage
* @param validCondition if true the label is hidden, else it is shown
* @return true if field is valid, false otherwise
*/
def validate(errorLabel: Label, errorMessage: String, validCondition: Boolean): Boolean = {
errorLabel.setOpacity( if (validCondition) 0 else 1 )
errorLabel.setText(errorMessage)
validCondition
}
}

View file

@ -4,13 +4,13 @@ import java.net.InetSocketAddress
import akka.actor._
import akka.io.{IO, Tcp}
import fr.acinq.bitcoin.{BinaryData, Satoshi}
import fr.acinq.bitcoin.{BinaryData, MilliSatoshi, Satoshi}
import fr.acinq.eclair.channel.Register.CreateChannel
/**
* Created by PM on 27/10/2015.
*/
class Client(remote: InetSocketAddress, pubkey: BinaryData, amount: Satoshi, register: ActorRef) extends Actor with ActorLogging {
class Client(remote: InetSocketAddress, pubkey: BinaryData, fundingSatoshis: Satoshi, pushMsat: MilliSatoshi, register: ActorRef) extends Actor with ActorLogging {
import Tcp._
import context.system
@ -23,15 +23,15 @@ class Client(remote: InetSocketAddress, pubkey: BinaryData, amount: Satoshi, reg
case c@Connected(remote, local) =>
log.info(s"connected to $remote")
val connection = sender()
register ! CreateChannel(connection, Some(pubkey), Some(amount))
register ! CreateChannel(connection, Some(pubkey), Some(fundingSatoshis), Some(pushMsat))
// TODO: kill this actor ?
}
}
object Client extends App {
def props(address: InetSocketAddress, pubkey: BinaryData, amount: Satoshi, register: ActorRef): Props = Props(classOf[Client], address, pubkey, amount, register)
def props(address: InetSocketAddress, pubkey: BinaryData, fundingSatoshis: Satoshi, pushMsat: MilliSatoshi, register: ActorRef): Props = Props(classOf[Client], address, pubkey, fundingSatoshis, pushMsat, register)
def props(host: String, port: Int, pubkey: BinaryData, amount: Satoshi, register: ActorRef): Props = Props(classOf[Client], new InetSocketAddress(host, port), pubkey, amount, register)
def props(host: String, port: Int, pubkey: BinaryData, fundingSatoshis: Satoshi, pushMsat: MilliSatoshi, register: ActorRef): Props = Props(classOf[Client], new InetSocketAddress(host, port), pubkey, fundingSatoshis, pushMsat, register)
}

View file

@ -28,7 +28,7 @@ class Server(address: InetSocketAddress, register: ActorRef) extends Actor with
case c@Connected(remote, local) =>
log.info(s"connected to $remote")
val connection = sender()
register ! CreateChannel(connection, None, None)
register ! CreateChannel(connection, None, None, None)
}
}

View file

@ -1,27 +1,23 @@
package fr.acinq
import com.google.protobuf.ByteString
import fr.acinq.bitcoin.Crypto.PrivateKey
import fr.acinq.bitcoin._
import lightning.bitcoin_pubkey
import scala.util.Random
package object eclair {
implicit def bin2pubkey(in: BinaryData) = bitcoin_pubkey(ByteString.copyFrom(in))
def toShortId(blockHeight: Int, txIndex: Int, outputIndex: Int): Long =
((blockHeight & 0xFFFFFFL) << 40) | ((txIndex & 0xFFFFFFL) << 16) | (outputIndex & 0xFFFFL)
implicit def array2pubkey(in: Array[Byte]) = bin2pubkey(in)
def fromShortId(id: Long): (Int, Int, Int) =
(((id >> 40) & 0xFFFFFF).toInt, ((id >> 16) & 0xFFFFFF).toInt, (id & 0xFFFF).toInt)
implicit def pubkey2bin(in: bitcoin_pubkey): BinaryData = in.key.toByteArray
def randomKey: PrivateKey = PrivateKey({
val bin = Array.fill[Byte](32)(0)
// TODO: use secure random
Random.nextBytes(bin)
bin
}, compressed = true)
implicit def bytestring2bin(in: ByteString): BinaryData = in.toByteArray
implicit def bin2bytestring(in: BinaryData): ByteString = ByteString.copyFrom(in)
/**
*
* @param base 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(base: Long, proportional: Long, msat: Long): Long = base + (proportional * msat) / 1000000
}

View file

@ -0,0 +1,15 @@
package fr.acinq.eclair.payment
import akka.actor.ActorRef
import fr.acinq.bitcoin.BinaryData
/**
* Created by PM on 01/02/2017.
*/
class PaymentEvent
case class PaymentSent(channel: ActorRef, h: BinaryData) extends PaymentEvent
case class PaymentFailed(channel: ActorRef, h: BinaryData, reason: String) extends PaymentEvent
case class PaymentReceived(channel: ActorRef, h: BinaryData) extends PaymentEvent

View file

@ -1,26 +1,27 @@
package fr.acinq.eclair.payment
import akka.actor.{Actor, ActorLogging, ActorRef, Props}
import fr.acinq.bitcoin.BinaryData
import fr.acinq.eclair.blockchain.peer.CurrentBlockCount
/**
* Created by PM on 29/08/2016.
*/
class PaymentInitiator(router: ActorRef, selector: ActorRef, initialBlockCount: Long) extends Actor with ActorLogging {
class PaymentInitiator(sourceNodeId: BinaryData, router: ActorRef, initialBlockCount: Long) extends Actor with ActorLogging {
context.system.eventStream.subscribe(self, classOf[CurrentBlockCount])
override def receive: Receive = main(initialBlockCount)
def main(currentBlockCount: Long): Receive = {
case CurrentBlockCount(count) => context.become(main(currentBlockCount))
case CurrentBlockCount(count) => context.become(main(count))
case c: CreatePayment =>
val payFsm = context.actorOf(PaymentLifecycle.props(router, selector, initialBlockCount))
val payFsm = context.actorOf(PaymentLifecycle.props(sourceNodeId, router, initialBlockCount))
payFsm forward c
}
}
object PaymentInitiator {
def props(router: ActorRef, selector: ActorRef, initialBlockCount: Long) = Props(classOf[PaymentInitiator], router, selector, initialBlockCount)
def props(sourceNodeId: BinaryData, router: ActorRef, initialBlockCount: Long) = Props(classOf[PaymentInitiator], sourceNodeId, router, initialBlockCount)
}

View file

@ -3,27 +3,26 @@ package fr.acinq.eclair.payment
import akka.actor.Status.Failure
import akka.actor.{ActorRef, FSM, LoggingFSM, Props, Status}
import fr.acinq.bitcoin.BinaryData
import fr.acinq.bitcoin.Crypto.PublicKey
import fr.acinq.eclair._
import fr.acinq.eclair.blockchain.peer.CurrentBlockCount
import fr.acinq.eclair.channel.{CMD_ADD_HTLC, PaymentFailed, PaymentSent}
import fr.acinq.eclair.channel.{CMD_ADD_HTLC, Register}
import fr.acinq.eclair.crypto.Sphinx
import fr.acinq.eclair.router._
import lightning.route_step
import lightning.route_step.Next
import fr.acinq.eclair.wire.{Codecs, PerHopPayload}
import scodec.Attempt
// @formatter:off
case class CreatePayment(amountMsat: Int, h: BinaryData, targetNodeId: BinaryData)
case class CreatePayment(amountMsat: Int, paymentHash: BinaryData, targetNodeId: BinaryData)
sealed trait Data
case class WaitingForRequest(currentBlockCount: Long) extends Data
case class WaitingForRoute(sender: ActorRef, c: CreatePayment, currentBlockCount: Long) extends Data
case class WaitingForChannel(sender: ActorRef,c: CreatePayment, r: Seq[BinaryData], currentBlockCount: Long) extends Data
case class WaitingForComplete(sender: ActorRef,c: CMD_ADD_HTLC, channel: ActorRef) extends Data
case class WaitingForComplete(sender: ActorRef,c: CMD_ADD_HTLC) extends Data
sealed trait State
case object WAITING_FOR_REQUEST extends State
case object WAITING_FOR_ROUTE extends State
case object WAITING_FOR_CHANNEL extends State
case object WAITING_FOR_PAYMENT_COMPLETE extends State
// @formatter:on
@ -31,84 +30,101 @@ case object WAITING_FOR_PAYMENT_COMPLETE extends State
/**
* Created by PM on 26/08/2016.
*/
class PaymentLifecycle(router: ActorRef, selector: ActorRef, initialBlockCount: Long) extends LoggingFSM[State, Data] {
class PaymentLifecycle(sourceNodeId: BinaryData, router: ActorRef, currentBlockCount: Long) extends LoggingFSM[State, Data] {
import PaymentLifecycle._
context.system.eventStream.subscribe(self, classOf[CurrentBlockCount])
startWith(WAITING_FOR_REQUEST, WaitingForRequest(initialBlockCount))
startWith(WAITING_FOR_REQUEST, WaitingForRequest(currentBlockCount))
when(WAITING_FOR_REQUEST) {
case Event(c: CreatePayment, WaitingForRequest(currentBlockCount)) =>
router ! RouteRequest(Globals.Node.publicKey, c.targetNodeId)
router ! RouteRequest(sourceNodeId, c.targetNodeId)
goto(WAITING_FOR_ROUTE) using WaitingForRoute(sender, c, currentBlockCount)
case Event(CurrentBlockCount(currentBlockCount), d: WaitingForRequest) =>
stay using d.copy(currentBlockCount = currentBlockCount)
}
when(WAITING_FOR_ROUTE) {
case Event(RouteResponse(r), WaitingForRoute(s, c, currentBlockCount)) =>
selector ! SelectChannelRequest(r.drop(1).head)
goto(WAITING_FOR_CHANNEL) using WaitingForChannel(s, c, r, currentBlockCount)
case Event(RouteResponse(hops), WaitingForRoute(s, c, currentBlockCount)) =>
val firstHop = hops.head
val cmd = buildCommand(c.amountMsat, c.paymentHash, hops, currentBlockCount.toInt)
context.system.eventStream.subscribe(self, classOf[PaymentEvent])
context.actorSelection(Register.actorPathToChannelId(firstHop.lastUpdate.channelId)) ! cmd
goto(WAITING_FOR_PAYMENT_COMPLETE) using WaitingForComplete(s, cmd)
case Event(f@Failure(t), WaitingForRoute(s, _, _)) =>
s ! f
stop(FSM.Failure(t))
case Event(CurrentBlockCount(currentBlockCount), d: WaitingForRoute) =>
stay using d.copy(currentBlockCount = currentBlockCount)
}
when(WAITING_FOR_CHANNEL) {
case Event(SelectChannelResponse(Some(channel)), WaitingForChannel(s, c, r, currentBlockCount)) =>
val next = r.drop(1).head
val others = r.drop(2)
val route = buildRoute(c.amountMsat, next +: others)
val cmd = CMD_ADD_HTLC(route.steps(0).amount, c.h, currentBlockCount.toInt + 100 + route.steps.size - 2, route.copy(steps = route.steps.tail), commit = true)
context.system.eventStream.subscribe(self, classOf[PaymentSent])
context.system.eventStream.subscribe(self, classOf[PaymentFailed])
context.system.eventStream.unsubscribe(self, classOf[CurrentBlockCount])
channel ! cmd
goto(WAITING_FOR_PAYMENT_COMPLETE) using WaitingForComplete(s, cmd, channel)
case Event(CurrentBlockCount(currentBlockCount), d: WaitingForChannel) =>
stay using d.copy(currentBlockCount = currentBlockCount)
}
when(WAITING_FOR_PAYMENT_COMPLETE) {
case Event("ok", _) => stay()
case Event(e@PaymentSent(_, h), WaitingForComplete(s, cmd, channel)) if h == cmd.paymentHash =>
case Event(e@PaymentSent(_, h), WaitingForComplete(s, cmd)) if h == cmd.paymentHash =>
s ! "sent"
stop(FSM.Normal)
case Event(e@PaymentFailed(_, h, reason), WaitingForComplete(s, cmd, channel)) if h == cmd.paymentHash =>
case Event(e@PaymentFailed(_, h, reason), WaitingForComplete(s, cmd)) if h == cmd.paymentHash =>
s ! Status.Failure(new RuntimeException(reason))
stop(FSM.Failure(reason))
case Event(CurrentBlockCount(_), _) =>
stay
}
}
object PaymentLifecycle {
def props(router: ActorRef, selector: ActorRef, initialBlockCount: Long) = Props(classOf[PaymentLifecycle], router, selector, initialBlockCount)
def props(sourceNodeId: BinaryData, router: ActorRef, initialBlockCount: Long) = Props(classOf[PaymentLifecycle], sourceNodeId, router, initialBlockCount)
def buildRoute(finalAmountMsat: Int, nodeIds: Seq[BinaryData]): lightning.route = {
/**
*
* @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
// TODO: use actual fee parameters that are specific to each node
def fee(amountMsat: Int) = nodeFee(Globals.base_fee, Globals.proportional_fee, amountMsat).toInt
def buildOnion(nodes: Seq[BinaryData], payloads: Seq[PerHopPayload], associatedData: BinaryData): BinaryData = {
require(nodes.size == payloads.size + 1, s"count mismatch: there should be one less payload than nodes (nodes=${nodes.size} payloads=${payloads.size})")
var amountMsat = finalAmountMsat
val steps = nodeIds.reverse.map(nodeId => {
val step = route_step(amountMsat, next = Next.Bitcoin(nodeId))
amountMsat = amountMsat + fee(amountMsat)
step
})
lightning.route(steps.reverse :+ route_step(0, next = route_step.Next.End(true)))
val pubkeys = nodes.map(PublicKey(_))
val sessionKey = randomKey
val payloadsbin: Seq[BinaryData] = payloads
.map(Codecs.perHopPayloadCodec.encode(_))
.map {
case Attempt.Successful(bitVector) => BinaryData(bitVector.toByteArray)
case Attempt.Failure(cause) => throw new RuntimeException(s"serialization error: $cause")
} :+ BinaryData("00" * 20)
Sphinx.makePacket(sessionKey, pubkeys, payloadsbin, associatedData)
}
}
/**
*
* @param finalAmountMsat the final htlc amount in millisatoshis
* @param hops the hops as computed by the router
* @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 buildRoute(finalAmountMsat: Long, hops: Seq[Hop], currentBlockCount: Int): (Long, Int, Seq[PerHopPayload]) =
hops.reverse.foldLeft((finalAmountMsat, currentBlockCount + defaultHtlcExpiry, Seq.empty[PerHopPayload])) {
case ((msat, expiry, payloads), hop) =>
val feeMsat = nodeFee(hop.lastUpdate.feeBaseMsat, hop.lastUpdate.feeProportionalMillionths, msat)
val expiryDelta = hop.lastUpdate.cltvExpiryDelta
(msat + feeMsat, expiry + expiryDelta, PerHopPayload(msat, expiry) +: payloads)
}
// TODO: set correct initial expiry
val defaultHtlcExpiry = 10
def buildCommand(finalAmountMsat: Long, paymentHash: BinaryData, hops: Seq[Hop], currentBlockCount: Int): CMD_ADD_HTLC = {
val (firstAmountMsat, firstExpiry, payloads) = buildRoute(finalAmountMsat, hops.drop(1), currentBlockCount)
val nodes = hops.map(_.nextNodeId)
// BOLT 2 requires that associatedData == paymentHash
val onion = buildOnion(nodes, payloads, paymentHash)
CMD_ADD_HTLC(firstAmountMsat, paymentHash, firstExpiry, onion, commit = true)
}
}

View file

@ -0,0 +1,116 @@
package fr.acinq.eclair.payment
import akka.actor.{Actor, ActorLogging, ActorRef, Props}
import fr.acinq.bitcoin.BinaryData
import fr.acinq.bitcoin.Crypto.PrivateKey
import fr.acinq.eclair.Globals
import fr.acinq.eclair.channel._
import fr.acinq.eclair.crypto.Sphinx
import fr.acinq.eclair.wire._
import scodec.bits.BitVector
import scodec.{Attempt, DecodeResult}
import scala.util.{Failure, Success, Try}
// @formatter:off
case class OutgoingChannel(channelId: Long, channel: ActorRef, nodeAddress: BinaryData)
case class Binding(downstream: UpdateAddHtlc, upstream: UpdateAddHtlc)
// @formatter:on
/**
* Created by PM on 01/02/2017.
*/
class Relayer(nodeSecret: PrivateKey, paymentHandler: ActorRef) extends Actor with ActorLogging {
context.system.eventStream.subscribe(self, classOf[ChannelChangedState])
override def receive: Receive = main(Set(), Map())
def main(upstreams: Set[OutgoingChannel], bindings: Map[UpdateAddHtlc, UpdateAddHtlc]): Receive = {
case ChannelChangedState(channel, _, remoteNodeId, _, NORMAL, d: DATA_NORMAL) =>
import d.commitments.channelId
log.info(s"adding channel $channelId to available upstreams")
context become main(upstreams + OutgoingChannel(channelId, channel, remoteNodeId.hash160), bindings)
case ChannelChangedState(channel, _, remoteNodeId, _, NEGOTIATING, d: DATA_NEGOTIATING) =>
import d.commitments.channelId
log.info(s"removing channel $channelId from upstreams/downstreams (mutual close)")
// TODO: cleanup bindings
context become main(upstreams - OutgoingChannel(channelId, channel, remoteNodeId.hash160), bindings)
case ChannelChangedState(channel, _, remoteNodeId, _, CLOSING, d: DATA_CLOSING) =>
import d.commitments.channelId
log.info(s"removing channel $channelId from upstreams/downstreams (unilateral close)")
// TODO: cleanup bindings
context become main(upstreams - OutgoingChannel(channelId, channel, remoteNodeId.hash160), bindings)
case add: UpdateAddHtlc =>
Try(Sphinx.parsePacket(nodeSecret, add.paymentHash, add.onionRoutingPacket))
.map {
case (payload, nextNodeAddress, nextPacket) => (Codecs.perHopPayloadCodec.decode(BitVector(payload.data)), nextNodeAddress, nextPacket)
} match {
case Success((_, nextNodeAddress, _)) if nextNodeAddress.forall(_ == 0) =>
log.info(s"we are the final recipient of htlc #${add.id}")
context.system.eventStream.publish(PaymentReceived(self, add.paymentHash))
paymentHandler forward add
case Success((Attempt.Successful(DecodeResult(payload, _)), nextNodeAddress, nextPacket)) if upstreams.exists(_.nodeAddress == nextNodeAddress) =>
val upstream = upstreams.find(_.nodeAddress == nextNodeAddress).get.channel
log.info(s"forwarding htlc #${add.id} to upstream=$upstream")
upstream ! CMD_ADD_HTLC(payload.amt_to_forward, add.paymentHash, payload.outgoing_cltv_value, nextPacket, origin = Some(add), commit = true)
context become main(upstreams, bindings)
case Success((Attempt.Successful(DecodeResult(_, _)), nextNodeAddress, _)) =>
log.warning(s"couldn't resolve upstream node address $nextNodeAddress, failing htlc #${add.id}")
sender ! CMD_FAIL_HTLC(add.id, "route error", commit = true)
case Success((Attempt.Failure(cause), _, _)) =>
log.error(s"couldn't parse payload: $cause")
sender ! CMD_FAIL_HTLC(add.id, "payload parsing error", commit = true)
case Failure(t) =>
log.error(t, "couldn't parse onion: ")
sender ! CMD_FAIL_HTLC(add.id, "onion parsing error", commit = true)
}
case Binding(downstream: UpdateAddHtlc, upstream: UpdateAddHtlc) =>
log.info(s"relayed htlc ${downstream.channelId}/${downstream.id} to ${upstream.channelId}/${upstream.id}")
context become main(upstreams, bindings + (upstream -> downstream))
case (add: UpdateAddHtlc, fulfill: UpdateFulfillHtlc) =>
bindings.get(add) match {
case Some(origin) if upstreams.exists(_.channelId == origin.channelId) =>
val downstream = upstreams.find(_.channelId == origin.channelId).get.channel
downstream ! CMD_SIGN
downstream ! CMD_FULFILL_HTLC(origin.id, fulfill.paymentPreimage)
downstream ! CMD_SIGN
case Some(origin) =>
log.warning(s"origin channel ${origin.channelId} has disappeared in the meantime")
case None =>
log.info(s"we were the origin payer for htlc #${fulfill.id}")
context.system.eventStream.publish(PaymentSent(self, add.paymentHash))
}
case (add: UpdateAddHtlc, fail: UpdateFailHtlc) =>
bindings.get(add) match {
case Some(origin) if upstreams.exists(_.channelId == origin.channelId) =>
val downstream = upstreams.find(_.channelId == origin.channelId).get.channel
downstream ! CMD_SIGN
// TODO: fix new String(fail.reason)
downstream ! CMD_FAIL_HTLC(origin.id, new String(fail.reason))
downstream ! CMD_SIGN
case Some(origin) =>
log.warning(s"origin channel ${origin.channelId} has disappeared in the meantime")
case None =>
log.info(s"we were the origin payer for htlc #${fail.id}")
// TODO: fix new String(fail.reason)
context.system.eventStream.publish(PaymentFailed(self, add.paymentHash, new String(fail.reason)))
}
case 'upstreams => sender ! upstreams
}
}
object Relayer {
def props(nodeSecret: PrivateKey, paymentHandler: ActorRef) = Props(classOf[Relayer], nodeSecret: PrivateKey, paymentHandler)
}

View file

@ -1,46 +0,0 @@
package fr.acinq.eclair.router
import akka.actor.{Actor, ActorLogging, ActorRef}
import fr.acinq.bitcoin.BinaryData
import fr.acinq.eclair.channel.{ChannelChangedState, ChannelSignatureReceived, DATA_NORMAL, NORMAL}
/**
* Created by PM on 26/08/2016.
*/
class ChannelSelector extends Actor with ActorLogging {
context.system.eventStream.subscribe(self, classOf[ChannelChangedState])
context.system.eventStream.subscribe(self, classOf[ChannelSignatureReceived])
override def receive: Receive = main(Map(), Map())
def main(node2channels: Map[BinaryData, Set[ActorRef]], channel2balance: Map[ActorRef, Long]): Receive = {
case ChannelChangedState(channel, theirNodeId, _, NORMAL, d: DATA_NORMAL) =>
val bal = d.commitments.remoteCommit.spec.toRemoteMsat
log.info(s"new channel to $theirNodeId with availableMsat=$bal")
val channels = node2channels.get(theirNodeId).getOrElse(Set()) + channel
context become main(node2channels + (theirNodeId -> channels), channel2balance + (channel -> bal))
case ChannelSignatureReceived(channel, commitments) =>
val bal = commitments.remoteCommit.spec.toRemoteMsat
log.info(s"channel $channel now has availableMsat=$bal")
context become main(node2channels, channel2balance + (channel -> bal))
case SelectChannelRequest(targetNodeId) if node2channels.contains(targetNodeId) =>
// getting a set of all channels pointing to targetNodeId
val candidates = node2channels(targetNodeId)
// selecting the channel with the highest available balance
val selected = candidates.map(c => (c, channel2balance(c))).toList.sortBy(_._2).last._1
sender ! SelectChannelResponse(Some(selected))
case SelectChannelRequest(targetNodeId) =>
sender ! SelectChannelResponse(None)
}
}
case class SelectChannelRequest(targetNodeId: BinaryData)
case class SelectChannelResponse(channel_opt: Option[ActorRef])

View file

@ -1,111 +0,0 @@
package fr.acinq.eclair.router
import akka.actor.{Actor, ActorContext, ActorLogging, ActorRef}
import fr.acinq.eclair.Globals
import fr.acinq.eclair.channel.{ChannelChangedState, DATA_NORMAL, NORMAL}
import grizzled.slf4j.Logging
import org.kitteh.irc.client.library.Client
import org.kitteh.irc.client.library.event.channel.ChannelUsersUpdatedEvent
import org.kitteh.irc.client.library.event.client.ClientConnectedEvent
import org.kitteh.irc.client.library.event.helper.ChannelUserListChangeEvent
import org.kitteh.irc.client.library.event.helper.ChannelUserListChangeEvent.Change
import org.kitteh.irc.client.library.event.user.PrivateMessageEvent
import org.kitteh.irc.lib.net.engio.mbassy.listener.Handler
import scala.collection.JavaConversions._
import scala.util.Random
/**
* Created by PM on 25/08/2016.
*/
class IRCWatcher extends Actor with ActorLogging {
val ircChannel = "#eclair-gossip"
context.system.eventStream.subscribe(self, classOf[ChannelChangedState])
val client = Client.builder().nick(s"node-${Globals.Node.id.take(8)}").serverHost("irc.freenode.net").build()
client.getEventManager().registerEventListener(new NodeIRCListener())
client.addChannel(ircChannel)
// we can't use channel id as nickname because there would be conflicts (a channel has two ends)
val rand = new Random()
override def receive: Receive = main(Map(), Map())
def main(channels: Map[String, ChannelDesc], localChannels: Map[ActorRef, Client]): Receive = {
case ChannelChangedState(channel, theirNodeId, _, NORMAL, d: DATA_NORMAL) =>
val channelDesc = ChannelDesc(d.commitments.anchorId, Globals.Node.publicKey, theirNodeId)
val channelClient = Client.builder().nick(f"chan-${rand.nextInt(1000000)}%06d").realName(channelDesc.id.toString()).serverHost("irc.freenode.net").build()
channelClient.getEventManager().registerEventListener(new ChannelIRCListener(channelDesc))
channelClient.addChannel(ircChannel)
context become main(channels, localChannels + (channel -> channelClient))
case ChannelChangedState(channel, theirNodeId, NORMAL, _, _) if localChannels.contains(channel) =>
localChannels(channel).shutdown()
context become main(channels, localChannels - channel)
case ('add, nick: String, desc: ChannelDesc) if !channels.contains(nick) && !channels.values.map(_.id).contains(desc.id) =>
context.system.eventStream.publish(ChannelDiscovered(desc))
context become main(channels + (nick -> desc), localChannels)
case ('remove, nick: String) if channels.contains(nick) =>
context.system.eventStream.publish(ChannelLost(channels(nick)))
context become main(channels - nick, localChannels)
}
}
class NodeIRCListener(implicit context: ActorContext) extends Logging {
@Handler
def onClientConnected(event: ClientConnectedEvent) {
logger.info(s"connected to IRC: ${event.getServerInfo}")
}
@Handler
def onChannelUsersUpdated(event: ChannelUsersUpdatedEvent) {
logger.debug(s"users updated: $event")
event.getChannel.getUsers
.filter(_.getNick.startsWith("chan"))
.map(chanUser => chanUser.sendMessage("desc"))
}
@Handler
def onChannelUserListChangeEvent(event: ChannelUserListChangeEvent) {
logger.debug(s"${event.getChange} ${event.getUser}")
event.getChange match {
case Change.JOIN if event.getUser.getNick.startsWith("chan") => event.getUser.sendMessage("desc")
case Change.LEAVE if event.getUser.getNick.startsWith("chan") => context.self ! ('remove, event.getUser.getNick)
case _ => {}
}
}
val r = """([0-9a-f]{64}): ([0-9a-f]{66})-([0-9a-f]{66})""".r
@Handler
def onPrivateMessage(event: PrivateMessageEvent) {
logger.debug(s"got private message: ${event.getMessage}")
event.getMessage match {
case r(id, a, b) => context.self ! ('add, event.getActor.getNick, ChannelDesc(id, a, b))
case _ => {}
}
}
}
class ChannelIRCListener(channelDesc: ChannelDesc) extends Logging {
@Handler
def onClientConnected(event: ClientConnectedEvent) {
logger.info(s"channel=${channelDesc.id} connected to IRC: ${event.getServerInfo}")
}
@Handler
def onPrivateMessage(event: PrivateMessageEvent) {
logger.debug(s"got private message: ${event.getMessage}")
event.getMessage match {
case "desc" => event.sendReply(s"${channelDesc.id}: ${channelDesc.a}-${channelDesc.b}")
case "kill" => event.getClient.shutdown()
case _ => {}
}
}
}

View file

@ -1,11 +0,0 @@
package fr.acinq.eclair.router
/**
* Created by PM on 26/08/2016.
*/
trait NetworkEvent
case class ChannelDiscovered(c: ChannelDesc) extends NetworkEvent
case class ChannelLost(c: ChannelDesc) extends NetworkEvent

View file

@ -1,69 +1,237 @@
package fr.acinq.eclair.router
import akka.actor.{Actor, ActorLogging}
import java.net.InetSocketAddress
import akka.actor.{Actor, ActorLogging, ActorRef, Props}
import akka.pattern.pipe
import fr.acinq.bitcoin.BinaryData
import org.jgrapht.alg.DijkstraShortestPath
import org.jgrapht.graph.{DefaultEdge, SimpleGraph}
import fr.acinq.bitcoin.Crypto.PrivateKey
import fr.acinq.bitcoin.{BinaryData, LexicographicalOrdering}
import fr.acinq.eclair.Globals
import fr.acinq.eclair.channel._
import fr.acinq.eclair.wire._
import org.jgrapht.alg.shortestpath.DijkstraShortestPath
import org.jgrapht.graph.{DefaultDirectedGraph, DefaultEdge}
import scala.collection.JavaConversions._
import scala.compat.Platform
import scala.concurrent.duration._
import scala.concurrent.{ExecutionContext, Future}
// @formatter:off
case class ChannelDesc(id: Long, a: BinaryData, b: BinaryData)
case class Hop(nodeId: BinaryData, nextNodeId: BinaryData, lastUpdate: ChannelUpdate)
case class RouteRequest(source: BinaryData, target: BinaryData)
case class RouteResponse(hops: Seq[Hop]) { require(hops.size > 0, "route cannot be empty") }
// @formatter:on
/**
* Created by PM on 24/05/2016.
*/
class Router extends Actor with ActorLogging {
context.system.eventStream.subscribe(self, classOf[NetworkEvent])
class Router(watcher: ActorRef, announcement: NodeAnnouncement) extends Actor with ActorLogging {
import Router._
import ExecutionContext.Implicits.global
def receive: Receive = main(Map())
context.system.eventStream.subscribe(self, classOf[ChannelChangedState])
context.system.scheduler.schedule(10 seconds, 60 seconds, self, 'tick_broadcast)
def main(channels: Map[BinaryData, ChannelDesc]): Receive = {
case ChannelDiscovered(c) =>
log.info(s"added channel ${c.id} to available routes")
context become main(channels + (c.id -> c))
case ChannelLost(c) =>
log.info(s"removed channel ${c.id} from available routes")
context become main(channels - c.id)
case 'network => sender ! channels.values
case RouteRequest(start, end) => findRoute(start, end, channels) map (RouteResponse(_)) pipeTo sender
def receive: Receive = main(local = announcement, nodes = Map(announcement.nodeId -> announcement), channels = Map(), updates = Map(), rebroadcast = Nil)
def main(
local: NodeAnnouncement,
nodes: Map[BinaryData, NodeAnnouncement],
channels: Map[Long, ChannelAnnouncement],
updates: Map[ChannelDesc, ChannelUpdate],
rebroadcast: Seq[RoutingMessage]): Receive = {
case ChannelChangedState(_, transport, _, WAIT_FOR_INIT_INTERNAL, _, _) =>
// we send all known announcements to the new peer as soon as the connection is opened
channels.values.foreach(transport ! _)
nodes.values.foreach(transport ! _)
updates.values.foreach(transport ! _)
case ChannelChangedState(_, _, remoteNodeId, _, NORMAL, d: DATA_NORMAL) =>
val (c, u) = if (LexicographicalOrdering.isLessThan(local.nodeId, remoteNodeId.toBin)) {
(
makeChannelAnnouncement(d.commitments.channelId, local.nodeId, remoteNodeId, d.params.localParams.fundingPrivKey.publicKey.toBin, d.params.remoteParams.fundingPubKey.toBin),
makeChannelUpdate(Globals.Node.privateKey, d.commitments.channelId, true, Platform.currentTime / 1000)
)
} else {
(
makeChannelAnnouncement(d.commitments.channelId, remoteNodeId, local.nodeId, d.params.remoteParams.fundingPubKey.toBin, d.params.localParams.fundingPrivKey.publicKey.toBin),
makeChannelUpdate(Globals.Node.privateKey, d.commitments.channelId, false, Platform.currentTime / 1000)
)
}
log.info(s"added channel channelId=${c.channelId} (nodes=${nodes.size} channels=${channels.size + 1})")
// let's trigger the broadcast immediately so that we don't wait for 60 seconds to announce our newly created channel
self ! 'tick_broadcast
context become main(local, nodes, channels + (c.channelId -> c), updates, rebroadcast :+ c :+ local :+ u)
case s: ChannelChangedState =>
// other channel changed state messages are ignored
case c: ChannelAnnouncement if channels.containsKey(c.channelId) =>
log.debug(s"ignoring $c (duplicate)")
case c: ChannelAnnouncement =>
// TODO: check channel output = P2WSH(nodeid1, nodeid2)
// TODO: check sigs
// TODO: blacklist if already received same channel id and different node ids
// TODO: check feature bit set
// TODO: forget channel once funding tx spent (add watch)
//watcher ! WatchSpent(self, txId: BinaryData, outputIndex: Int, minDepth: Int, event: BitcoinEvent)
log.info(s"added channel channelId=${c.channelId} (nodes=${nodes.size} channels=${channels.size + 1})")
context become main(local, nodes, channels + (c.channelId -> c), updates, rebroadcast :+ c)
case n: NodeAnnouncement if !checkSig(n) =>
// TODO: fail connection (should probably be done in the auth handler or channel)
case n: NodeAnnouncement if !channels.values.exists(c => c.nodeId1 == n.nodeId || c.nodeId2 == n.nodeId) =>
log.debug(s"ignoring $n (no related channel found)")
case n: NodeAnnouncement if nodes.containsKey(n.nodeId) && nodes(n.nodeId).timestamp >= n.timestamp =>
log.debug(s"ignoring announcement $n (old timestamp or duplicate)")
case n: NodeAnnouncement =>
log.info(s"added/replaced node nodeId=${n.nodeId} (nodes=${nodes.size + 1} channels=${channels.size})")
context become main(local, nodes + (n.nodeId -> n), channels, updates, rebroadcast :+ n)
case u: ChannelUpdate if !channels.contains(u.channelId) =>
log.debug(s"ignoring $u (no related channel found)")
case u: ChannelUpdate if !checkSig(u, getDesc(u, channels(u.channelId)).a) =>
// TODO: fail connection (should probably be done in the auth handler or channel)
case u: ChannelUpdate =>
val channel = channels(u.channelId)
val desc = getDesc(u, channel)
if (updates.contains(desc) && updates(desc).timestamp >= u.timestamp) {
log.debug(s"ignoring $u (old timestamp or duplicate)")
} else {
context become main(local, nodes, channels, updates + (desc -> u), rebroadcast :+ u)
}
case 'tick_broadcast if rebroadcast.size == 0 =>
// no-op
case 'tick_broadcast =>
log.info(s"broadcasting ${rebroadcast.size} routing messages")
rebroadcast.foreach(context.actorSelection(Register.actorPathToTransportHandlers) ! _)
context become main(local, nodes, channels, updates, Nil)
case 'nodes => sender ! nodes.values
case 'channels => sender ! channels.values
case 'updates => sender ! updates.values
case RouteRequest(start, end) => findRoute(start, end, updates).map(RouteResponse(_)) pipeTo sender
case other => log.warning(s"unhandled message $other")
}
}
object Router {
def findRouteDijkstra(myNodeId: BinaryData, targetNodeId: BinaryData, channels: Map[BinaryData, ChannelDesc]): Seq[BinaryData] = {
class NamedEdge(val id: BinaryData) extends DefaultEdge
val g = new SimpleGraph[BinaryData, NamedEdge](classOf[NamedEdge])
channels.values.foreach(x => {
g.addVertex(x.a)
g.addVertex(x.b)
g.addEdge(x.a, x.b, new NamedEdge(x.id))
def props(watcher: ActorRef, announcement: NodeAnnouncement) = Props(classOf[Router], watcher, announcement)
// TODO: placeholder for signatures, we don't actually sign for now
val DUMMY_SIG = BinaryData("3045022100e0a180fdd0fe38037cc878c03832861b40a29d32bd7b40b10c9e1efc8c1468a002205ae06d1624896d0d29f4b31e32772ea3cb1b4d7ed4e077e5da28dcc33c0e781201")
def makeChannelAnnouncement(channelId: Long, nodeId1: BinaryData, nodeId2: BinaryData, fundingKey1: BinaryData, fundingKey2: BinaryData): ChannelAnnouncement = {
val unsigned = ChannelAnnouncement(
nodeSignature1 = DUMMY_SIG,
nodeSignature2 = DUMMY_SIG,
channelId = channelId,
bitcoinSignature1 = DUMMY_SIG,
bitcoinSignature2 = DUMMY_SIG,
nodeId1 = nodeId1,
nodeId2 = nodeId2,
bitcoinKey1 = fundingKey1,
bitcoinKey2 = fundingKey2
)
unsigned
}
def makeNodeAnnouncement(secret: PrivateKey, alias: String, color: (Byte, Byte, Byte), addresses: List[InetSocketAddress], timestamp: Long): NodeAnnouncement = {
require(alias.size <= 32)
val unsigned = NodeAnnouncement(
signature = DUMMY_SIG,
timestamp = timestamp,
nodeId = secret.publicKey.toBin,
rgbColor = color,
alias = alias,
features = "",
addresses = addresses
)
unsigned
/*val bin = Codecs.nodeAnnouncementCodec.encode(unsigned).toOption.map(_.toByteArray).getOrElse(throw new RuntimeException(s"cannot encode $unsigned"))
val hash = sha256(sha256(bin.drop(64)))
val sig = encodeSignature(sign(hash, secret))
unsigned.copy(signature = sig)*/
}
def makeChannelUpdate(secret: PrivateKey, channelId: Long, isNodeId1: Boolean, timestamp: Long): ChannelUpdate = {
val unsigned = ChannelUpdate(
signature = DUMMY_SIG,
channelId = channelId,
timestamp = timestamp,
flags = if (isNodeId1) "0000" else "0001",
cltvExpiryDelta = Globals.expiry_delta_blocks,
htlcMinimumMsat = Globals.htlc_minimum_msat,
feeBaseMsat = Globals.fee_base_msat,
feeProportionalMillionths = Globals.fee_proportional_msat
)
unsigned
/*val bin = Codecs.channelUpdateCodec.encode(unsigned).toOption.map(_.toByteArray).getOrElse(throw new RuntimeException(s"cannot encode $unsigned"))
val hash = sha256(sha256(bin.drop(64)))
val sig = encodeSignature(sign(hash, secret))
unsigned.copy(signature = sig)*/
}
def checkSig(ann: NodeAnnouncement): Boolean = true
/*{
val bin = Codecs.nodeAnnouncementCodec.encode(ann).toOption.map(_.toByteArray).getOrElse(throw new RuntimeException(s"cannot encode $ann"))
val hash = sha256(sha256(bin.drop(64)))
verifySignature(hash, ann.signature, PublicKey(ann.nodeId))
}*/
def checkSig(ann: ChannelUpdate, nodeId: BinaryData): Boolean = true
/*{
val bin = Codecs.channelUpdateCodec.encode(ann).toOption.map(_.toByteArray).getOrElse(throw new RuntimeException(s"cannot encode $ann"))
val hash = sha256(sha256(bin.drop(64)))
verifySignature(hash, ann.signature, PublicKey(nodeId))
}*/
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 (u.flags.data(1) % 2 == 0) ChannelDesc(u.channelId, channel.nodeId1, channel.nodeId2) else ChannelDesc(u.channelId, channel.nodeId2, channel.nodeId1)
}
def findRouteDijkstra(localNodeId: BinaryData, targetNodeId: BinaryData, channels: Iterable[ChannelDesc]): Seq[ChannelDesc] = {
require(localNodeId != targetNodeId, "cannot route to self")
case class DescEdge(desc: ChannelDesc) extends DefaultEdge
val g = new DefaultDirectedGraph[BinaryData, DescEdge](classOf[DescEdge])
channels.foreach(d => {
g.addVertex(d.a)
g.addVertex(d.b)
g.addEdge(d.a, d.b, new DescEdge(d))
})
Option(new DijkstraShortestPath(g, myNodeId, targetNodeId).getPath) match {
case Some(path) => {
val vertices = path.getEdgeList.foldLeft(List(path.getStartVertex)) {
case (rest :+ v, edge) if g.getEdgeSource(edge) == v => rest :+ v :+ g.getEdgeTarget(edge)
case (rest :+ v, edge) if g.getEdgeTarget(edge) == v => rest :+ v :+ g.getEdgeSource(edge)
}
vertices
}
Option(DijkstraShortestPath.findPathBetween(g, localNodeId, targetNodeId)) match {
case Some(path) => path.getEdgeList.map(_.desc)
case None => throw new RuntimeException("route not found")
}
}
def findRoute(myNodeId: BinaryData, targetNodeId: BinaryData, channels: Map[BinaryData, ChannelDesc])(implicit ec: ExecutionContext): Future[Seq[BinaryData]] = Future {
findRouteDijkstra(myNodeId, targetNodeId, channels)
def findRoute(localNodeId: BinaryData, targetNodeId: BinaryData, 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)))
}
}
case class ChannelDesc(id: BinaryData, a: BinaryData, b: BinaryData)
case class RouteRequest(source: BinaryData, target: BinaryData)
case class RouteResponse(route: Seq[BinaryData])

View file

@ -1,13 +1,13 @@
package fr.acinq.eclair.wire
import java.math.BigInteger
import java.net.{Inet4Address, Inet6Address, InetAddress}
import java.net.{Inet4Address, Inet6Address, InetAddress, InetSocketAddress}
import fr.acinq.bitcoin.Crypto.{Point, PublicKey, Scalar}
import fr.acinq.bitcoin.{BinaryData, Crypto}
import fr.acinq.eclair.crypto.Generators
import fr.acinq.eclair.wire
import scodec.bits.{BitVector, ByteVector, HexStringSyntax}
import scodec.bits.{BitVector, ByteVector}
import scodec.codecs._
import scodec.{Attempt, Codec, Err}
@ -25,17 +25,19 @@ object Codecs {
def varsizebinarydata: Codec[BinaryData] = variableSizeBytes(uint16, bytes.xmap(d => BinaryData(d.toSeq), d => ByteVector(d.data)))
def listofbinarydata(size: Int): Codec[List[BinaryData]] = listOfN(uint16, binarydata(size))
def listofsignatures: Codec[List[BinaryData]] = listOfN(uint16, signature)
def ipv6: Codec[InetAddress] = Codec[InetAddress](
(ia: InetAddress) => ia match {
case a: Inet4Address => bytes(16).encode(hex"00 00 00 00 00 00 00 00 00 00 FF FF" ++ ByteVector(a.getAddress))
case a: Inet6Address => bytes(16).encode(ByteVector(a.getAddress))
},
(buf: BitVector) => bytes(16).decode(buf).map(_.map(b => InetAddress.getByAddress(b.toArray)))
)
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))
def listofsocketaddresses: Codec[List[InetSocketAddress]] = listOfN(uint16, socketaddress)
def signature: Codec[BinaryData] = Codec[BinaryData](
(der: BinaryData) => bytes(64).encode(ByteVector(der2wire(der).toArray)),
@ -128,7 +130,7 @@ object Codecs {
val fundingCreatedCodec: Codec[FundingCreated] = (
("temporaryChannelId" | int64) ::
("txid" | binarydata(32)) ::
("outputIndex" | uint16) ::
("outputIndex" | uint8) ::
("signature" | signature)).as[FundingCreated]
val fundingSignedCodec: Codec[FundingSigned] = (
@ -178,7 +180,7 @@ object Codecs {
("channelId" | int64) ::
("perCommitmentSecret" | scalar) ::
("nextPerCommitmentPoint" | point) ::
("padding" | ignore(3)) ::
("padding" | ignore(8 * 3)) ::
("htlcTimeoutSignature" | listofsignatures)
).as[RevokeAndAck]
@ -200,18 +202,18 @@ object Codecs {
val nodeAnnouncementCodec: Codec[NodeAnnouncement] = (
("signature" | signature) ::
("timestamp" | uint32) ::
("ip" | ipv6) ::
("port" | uint16) ::
("nodeId" | binarydata(33)) ::
("rgbColor" | rgb) ::
("alias" | zeropaddedstring(32))).as[NodeAnnouncement]
("alias" | zeropaddedstring(32)) ::
("features" | varsizebinarydata) ::
("addresses" | listofsocketaddresses)).as[NodeAnnouncement]
val channelUpdateCodec: Codec[ChannelUpdate] = (
("signature" | signature) ::
("channelId" | int64) ::
("timestamp" | uint32) ::
("flags" | binarydata(2)) ::
("expiry" | uint16) ::
("cltvExpiryDelta" | uint16) ::
("htlcMinimumMsat" | uint32) ::
("feeBaseMsat" | uint32) ::
("feeProportionalMillionths" | uint32)).as[ChannelUpdate]
@ -236,4 +238,10 @@ object Codecs {
.typecase(257, nodeAnnouncementCodec)
.typecase(258, channelUpdateCodec)
val perHopPayloadCodec: Codec[PerHopPayload] = (
("realm" | ignore(8 * 1)) ::
("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 * 7))).as[PerHopPayload]
}

View file

@ -1,6 +1,6 @@
package fr.acinq.eclair.wire
import java.net.InetAddress
import java.net.InetSocketAddress
import fr.acinq.bitcoin.BinaryData
import fr.acinq.bitcoin.Crypto.{Point, PublicKey, Scalar}
@ -114,17 +114,21 @@ case class ChannelAnnouncement(nodeSignature1: BinaryData,
case class NodeAnnouncement(signature: BinaryData,
timestamp: Long,
ip: InetAddress,
port: Int,
nodeId: BinaryData,
rgbColor: (Byte, Byte, Byte),
alias: String) extends RoutingMessage
alias: String,
features: BinaryData,
// TODO: check address order + support padding data (type 0)
addresses: List[InetSocketAddress]) extends RoutingMessage
case class ChannelUpdate(signature: BinaryData,
channelId: Long,
timestamp: Long,
flags: BinaryData,
expiry: Int,
cltvExpiryDelta: Int,
htlcMinimumMsat: Long,
feeBaseMsat: Long,
feeProportionalMillionths: Long) extends RoutingMessage
feeProportionalMillionths: Long) extends RoutingMessage
case class PerHopPayload(amt_to_forward: Long,
outgoing_cltv_value: Int)

View file

@ -1,31 +1,36 @@
eclair {
server {
address = "localhost"
port = 45000
host = "localhost"
port = 9735
}
api {
address = "localhost"
host = "localhost"
port = 8080
}
bitcoind {
address = "localhost"
port = 18444 # regtest
network = "regtest"
host = "localhost"
port = 18444
rpcport = 18332
rpcuser = "foo"
rpcpassword = "bar"
}
node {
seed = 0102030405060708010203040506070801020304050607080102030405060708
alias = "eclair"
color {
r = 73
g = 218
b = 170
}
}
delay-blocks = 144
mindepth-blocks = 3
commit-fee = 80000
closing-fee = 10000
payment-handler = "noop"
}
interop-test {
bitcoin-path = ""
lightning-path = ""
expiry-delta-blocks = 144
htlc-minimum-msat = 1000000
fee-base-msat = 546000
fee-proportional-msat = 10
payment-handler = "local"
}
akka {
loggers = ["akka.event.slf4j.Slf4jLogger"]

View file

@ -0,0 +1,20 @@
package fr.acinq.eclair
import org.junit.runner.RunWith
import org.scalatest.FunSuite
import org.scalatest.junit.JUnitRunner
/**
* Created by PM on 27/01/2017.
*/
@RunWith(classOf[JUnitRunner])
class PackageSpec extends FunSuite {
test("calculate simple route") {
val blockHeight = 42000
val txIndex = 27
val outputIndex = 3
assert(fromShortId(toShortId(blockHeight, txIndex, outputIndex)) === (blockHeight, txIndex, outputIndex))
}
}

View file

@ -1,123 +0,0 @@
package fr.acinq.eclair
import akka.actor.FSM.{CurrentState, SubscribeTransitionCallBack, Transition}
import akka.actor.{ActorSystem, Props, Status}
import akka.testkit.{TestKit, TestProbe}
import fr.acinq.bitcoin.BinaryData
import fr.acinq.bitcoin.Crypto.PrivateKey
import fr.acinq.eclair.payment._
import fr.acinq.eclair.router._
import org.junit.runner.RunWith
import org.scalatest.junit.JUnitRunner
import org.scalatest.{BeforeAndAfterAll, FunSuiteLike}
/**
* Created by PM on 29/08/2016.
*/
@RunWith(classOf[JUnitRunner])
class PaymentFSMSpec extends TestKit(ActorSystem("test")) with FunSuiteLike with BeforeAndAfterAll {
override def afterAll {
TestKit.shutdownActorSystem(system)
}
test("route not available") {
val router = system.actorOf(Props[Router])
val selector = system.actorOf(Props[ChannelSelector])
val channel00 = TestProbe()
val channel01 = TestProbe()
// network: aaaa -> bbbbbbb -> cccc
val node_a = Globals.Node.publicKey
val node_b = PrivateKey(BinaryData("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"), compressed = true).publicKey
val node_c = PrivateKey(BinaryData("cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"), compressed = true).publicKey
val node_d = PrivateKey(BinaryData("dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"), compressed = true).publicKey
// no route b -> c
router ! ChannelDiscovered(ChannelDesc("01", node_a, node_b))
router ! ChannelDiscovered(ChannelDesc("02", node_c, node_d))
val paymentFsm = system.actorOf(PaymentLifecycle.props(router, selector, 1440))
val monitor = TestProbe()
paymentFsm ! SubscribeTransitionCallBack(monitor.ref)
val CurrentState(_, WAITING_FOR_REQUEST) = monitor.expectMsgClass(classOf[CurrentState[_]])
val sender = TestProbe()
sender.send(paymentFsm, CreatePayment(42000000, BinaryData("00112233445566778899aabbccddeeff"), node_c))
val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]])
sender.expectMsgType[Status.Failure]
}
//TODO re-enable
/*test("payment succeeded") {
val router = system.actorOf(Props[Router])
val selector = system.actorOf(Props[ChannelSelector])
val channel00 = TestProbe()
val channel01 = TestProbe()
// network: aaaa -> bbbbbbb -> cccc
val node_a = Globals.Node.publicKey
val node_b = BinaryData("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb")
val node_c = BinaryData("ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc")
router ! ChannelDiscovered(ChannelDesc("01", node_a, node_b))
router ! ChannelDiscovered(ChannelDesc("02", node_b, node_c))
selector ! ChannelChangedState(channel00.ref, node_b, OPEN_WAIT_FOR_COMPLETE_OURFUNDING, NORMAL, DATA_NORMAL_2(0, Commitments(null, null, null, TheirCommit(0L, CommitmentSpec(Set(), 0L, 0L, 100000), null, null), null, null, 0L, null, null, null, null, null), null, null))
selector ! ChannelChangedState(channel01.ref, node_b, OPEN_WAIT_FOR_COMPLETE_OURFUNDING, NORMAL, DATA_NORMAL(Commitments(null, null, null, TheirCommit(0L, CommitmentSpec(Set(), 0L, 0L, 100000000), null, null), null, null, 0L, null, null, null, null, null), null, null))
val paymentFsm = system.actorOf(PaymentLifecycle.props(router, selector, 1440))
val monitor = TestProbe()
paymentFsm ! SubscribeTransitionCallBack(monitor.ref)
val CurrentState(_, WAITING_FOR_REQUEST) = monitor.expectMsgClass(classOf[CurrentState[_]])
val sender = TestProbe()
val req = CreatePayment(42000000, BinaryData("00112233445566778899aabbccddeeff"), node_c)
sender.send(paymentFsm, req)
val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]])
val Transition(_, WAITING_FOR_ROUTE, WAITING_FOR_CHANNEL) = monitor.expectMsgClass(classOf[Transition[_]])
val Transition(_, WAITING_FOR_CHANNEL, WAITING_FOR_PAYMENT_COMPLETE) = monitor.expectMsgClass(classOf[Transition[_]])
channel01.expectMsgType[CMD_ADD_HTLC]
sender.send(paymentFsm, PaymentSent(channel01.ref, req.h))
sender.expectMsg("sent")
}*/
//TODO re-enable
/*test("payment failed") {
val router = system.actorOf(Props[Router])
val selector = system.actorOf(Props[ChannelSelector])
val channel00 = TestProbe()
val channel01 = TestProbe()
// network: aaaa -> bbbbbbb -> cccc
val node_a = Globals.Node.publicKey
val node_b = BinaryData("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb")
val node_c = BinaryData("ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc")
router ! ChannelDiscovered(ChannelDesc("01", node_a, node_b))
router ! ChannelDiscovered(ChannelDesc("02", node_b, node_c))
selector ! ChannelChangedState(channel00.ref, node_b, OPEN_WAIT_FOR_COMPLETE_OURFUNDING, NORMAL, DATA_NORMAL(Commitments(null, null, null, TheirCommit(0L, CommitmentSpec(Set(), 0L, 0L, 100000), null, null), null, null, 0L, null, null, null, null, null), null, null))
selector ! ChannelChangedState(channel01.ref, node_b, OPEN_WAIT_FOR_COMPLETE_OURFUNDING, NORMAL, DATA_NORMAL(Commitments(null, null, null, TheirCommit(0L, CommitmentSpec(Set(), 0L, 0L, 100000000), null, null), null, null, 0L, null, null, null, null, null), null, null))
val paymentFsm = system.actorOf(PaymentLifecycle.props(router, selector, 1440))
val monitor = TestProbe()
paymentFsm ! SubscribeTransitionCallBack(monitor.ref)
val CurrentState(_, WAITING_FOR_REQUEST) = monitor.expectMsgClass(classOf[CurrentState[_]])
val sender = TestProbe()
val req = CreatePayment(42000000, BinaryData("00112233445566778899aabbccddeeff"), node_c)
sender.send(paymentFsm, req)
val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]])
val Transition(_, WAITING_FOR_ROUTE, WAITING_FOR_CHANNEL) = monitor.expectMsgClass(classOf[Transition[_]])
val Transition(_, WAITING_FOR_CHANNEL, WAITING_FOR_PAYMENT_COMPLETE) = monitor.expectMsgClass(classOf[Transition[_]])
channel01.expectMsgType[CMD_ADD_HTLC]
sender.send(paymentFsm, PaymentFailed(channel01.ref, req.h, "some reason"))
sender.expectMsgType[Status.Failure]
}*/
}

View file

@ -1,109 +0,0 @@
package fr.acinq.eclair
import fr.acinq.bitcoin.BinaryData
import fr.acinq.eclair.payment.PaymentLifecycle
import fr.acinq.eclair.router.{ChannelDesc, Router}
import lightning.route_step
import org.junit.runner.RunWith
import org.scalatest.FunSuite
import org.scalatest.junit.JUnitRunner
/**
* Created by PM on 31/05/2016.
*/
@RunWith(classOf[JUnitRunner])
class RouterSpec extends FunSuite {
test("calculate simple route") {
val channels: Map[BinaryData, ChannelDesc] = Map(
BinaryData("0a") -> ChannelDesc(BinaryData("0a"), BinaryData("01"), BinaryData("02")),
BinaryData("0b") -> ChannelDesc(BinaryData("0b"), BinaryData("03"), BinaryData("02")),
BinaryData("0c") -> ChannelDesc(BinaryData("0c"), BinaryData("03"), BinaryData("04")),
BinaryData("0d") -> ChannelDesc(BinaryData("0d"), BinaryData("04"), BinaryData("05"))
)
val route = Router.findRouteDijkstra(BinaryData("01"), BinaryData("05"), channels)
assert(route === BinaryData("01") :: BinaryData("02") :: BinaryData("03") :: BinaryData("04") :: BinaryData("05") :: Nil)
}
test("calculate simple route 2") {
val channels: Map[BinaryData, ChannelDesc] = Map(
BinaryData("99e542c274b073d215af02e57f814f3d16a2373a00ac52b49ef4a1949c912609") -> ChannelDesc(BinaryData("99e542c274b073d215af02e57f814f3d16a2373a00ac52b49ef4a1949c912609"), BinaryData("032b2e37d202658eb5216a698e52da665c25c5d04de0faf1d29aa2af7fb374a003"), BinaryData("0382887856e9f10a8a1ffade96b4009769141e5f3692f2ffc35fd4221f6057643b")),
BinaryData("44d7f822e0498e21473a8a40045c9b7e7bd2e78730b5274cb5836e64bc0b6125") -> ChannelDesc(BinaryData("44d7f822e0498e21473a8a40045c9b7e7bd2e78730b5274cb5836e64bc0b6125"), BinaryData("023cda4e9506ce0a5fd3e156fc6d1bff16873375c8e823ee18aa36fa6844c0ae61"), BinaryData("0382887856e9f10a8a1ffade96b4009769141e5f3692f2ffc35fd4221f6057643b"))
)
val route = Router.findRouteDijkstra(BinaryData("032b2e37d202658eb5216a698e52da665c25c5d04de0faf1d29aa2af7fb374a003"), BinaryData("023cda4e9506ce0a5fd3e156fc6d1bff16873375c8e823ee18aa36fa6844c0ae61"), channels)
assert(route === BinaryData("032b2e37d202658eb5216a698e52da665c25c5d04de0faf1d29aa2af7fb374a003") :: BinaryData("0382887856e9f10a8a1ffade96b4009769141e5f3692f2ffc35fd4221f6057643b") :: BinaryData("023cda4e9506ce0a5fd3e156fc6d1bff16873375c8e823ee18aa36fa6844c0ae61") :: Nil)
}
test("calculate simple route 3") {
val channels: Map[BinaryData, ChannelDesc] = Map(
BinaryData("178c78f5ec3ffa8cc15d9fa8119ec0d1ff7d4e4ff33297df5e68319fbc34b1bb") -> ChannelDesc(BinaryData("178c78f5ec3ffa8cc15d9fa8119ec0d1ff7d4e4ff33297df5e68319fbc34b1bb"), BinaryData("02f298bcafa3b0aa1d51a552d759a5add7d189222fe068d4ff2417dce43ea9daa1"), BinaryData("021acf75c92318d3723098294d2a6a4b08d9abba2ebb5f2df2b4a8e9153e96a5f4")),
BinaryData("e9c89b691f80494e631a4eb7169c981a0be9484cdd51a2cb9975f8aba85a77c8") -> ChannelDesc(BinaryData("e9c89b691f80494e631a4eb7169c981a0be9484cdd51a2cb9975f8aba85a77c8"), BinaryData("031ef3016e2994eb40b75c051ba9089238dcb138fe4148169d6f3f8e114b446cc1"), BinaryData("03befb4f8ad1d87d4c41acbb316791fe157f305caf2123c848f448975aaf85c1bb")),
BinaryData("2c2e2b5f1b70d9ab2ea30f5dd0f61d62edad10881d2add1a2796d94eac93e07e") -> ChannelDesc(BinaryData("2c2e2b5f1b70d9ab2ea30f5dd0f61d62edad10881d2add1a2796d94eac93e07e"), BinaryData("02f298bcafa3b0aa1d51a552d759a5add7d189222fe068d4ff2417dce43ea9daa1"), BinaryData("031ef3016e2994eb40b75c051ba9089238dcb138fe4148169d6f3f8e114b446cc1"))
)
val route = Router.findRouteDijkstra(BinaryData("03befb4f8ad1d87d4c41acbb316791fe157f305caf2123c848f448975aaf85c1bb"), BinaryData("021acf75c92318d3723098294d2a6a4b08d9abba2ebb5f2df2b4a8e9153e96a5f4"), channels)
assert(route === BinaryData("03befb4f8ad1d87d4c41acbb316791fe157f305caf2123c848f448975aaf85c1bb") :: BinaryData("031ef3016e2994eb40b75c051ba9089238dcb138fe4148169d6f3f8e114b446cc1") :: BinaryData("02f298bcafa3b0aa1d51a552d759a5add7d189222fe068d4ff2417dce43ea9daa1") :: BinaryData("021acf75c92318d3723098294d2a6a4b08d9abba2ebb5f2df2b4a8e9153e96a5f4") :: Nil)
}
test("route not found") {
val channels: Map[BinaryData, ChannelDesc] = Map(
BinaryData("178c78f5ec3ffa8cc15d9fa8119ec0d1ff7d4e4ff33297df5e68319fbc34b1bb") -> ChannelDesc(BinaryData("178c78f5ec3ffa8cc15d9fa8119ec0d1ff7d4e4ff33297df5e68319fbc34b1bb"), BinaryData("02f298bcafa3b0aa1d51a552d759a5add7d189222fe068d4ff2417dce43ea9daa1"), BinaryData("021acf75c92318d3723098294d2a6a4b08d9abba2ebb5f2df2b4a8e9153e96a5f4")),
BinaryData("e9c89b691f80494e631a4eb7169c981a0be9484cdd51a2cb9975f8aba85a77c8") -> ChannelDesc(BinaryData("e9c89b691f80494e631a4eb7169c981a0be9484cdd51a2cb9975f8aba85a77c8"), BinaryData("031ef3016e2994eb40b75c051ba9089238dcb138fe4148169d6f3f8e114b446cc1"), BinaryData("03befb4f8ad1d87d4c41acbb316791fe157f305caf2123c848f448975aaf85c1bb"))
)
intercept[RuntimeException] {
Router.findRouteDijkstra(BinaryData("03befb4f8ad1d87d4c41acbb316791fe157f305caf2123c848f448975aaf85c1bb"), BinaryData("021acf75c92318d3723098294d2a6a4b08d9abba2ebb5f2df2b4a8e9153e96a5f4"), channels)
}
}
test("route to self") {
val channels: Map[BinaryData, ChannelDesc] = Map(
BinaryData("0a") -> ChannelDesc(BinaryData("0a"), BinaryData("01"), BinaryData("02")),
BinaryData("0b") -> ChannelDesc(BinaryData("0b"), BinaryData("01"), BinaryData("01"))
)
intercept[RuntimeException] {
Router.findRouteDijkstra(BinaryData("01"), BinaryData("01"), channels)
}
}
test("compute fees 2") {
val nodeIds = Seq(BinaryData("00"), BinaryData("01"), BinaryData("02"))
val amountMsat = 1000000
val route = PaymentLifecycle.buildRoute(amountMsat, nodeIds)
assert(route.steps.length == 4 && route.steps.last == route_step(0, next = route_step.Next.End(true)))
assert(route.steps(2).amount == amountMsat)
assert(route.steps.dropRight(1).map(_.next.bitcoin.get.key).map(bytestring2bin) == nodeIds)
assert(route.steps(0).amount - route.steps(1).amount == nodeFee(Globals.base_fee, Globals.proportional_fee, route.steps(1).amount))
}
test("route to neighbor") {
val channels: Map[BinaryData, ChannelDesc] = Map(
BinaryData("0a") -> ChannelDesc(BinaryData("0a"), BinaryData("01"), BinaryData("02"))
)
Router.findRouteDijkstra(BinaryData("01"), BinaryData("02"), channels)
}
test("compute fees") {
val nodeIds = Seq(BinaryData("00"), BinaryData("01"))
val amountMsat = 300000000
val route = PaymentLifecycle.buildRoute(amountMsat, nodeIds)
assert(route.steps.length == 3 && route.steps.last == route_step(0, next = route_step.Next.End(true)))
assert(route.steps(1).amount == amountMsat)
assert(route.steps.dropRight(1).map(_.next.bitcoin.get.key).map(bytestring2bin) == nodeIds)
assert(route.steps(0).amount - route.steps(1).amount == nodeFee(Globals.base_fee, Globals.proportional_fee, route.steps(1).amount))
}
}

View file

@ -44,4 +44,6 @@ class TestBitcoinClient()(implicit system: ActorSystem) extends ExtendedBitcoinC
override def signTransaction(tx: Transaction)(implicit ec: ExecutionContext): Future[SignTransactionResponse] = ???
override def getTransactionShortId(txId: String)(implicit ec: ExecutionContext): Future[(Int, Int)] = Future.successful((42000, 42))
}

View file

@ -13,6 +13,7 @@ object TestConstants {
val pushMsat = 200000000L
object Alice {
val id = randomKey.publicKey
val channelParams = LocalParams(
dustLimitSatoshis = 542,
maxHtlcValueInFlightMsat = Long.MaxValue,
@ -32,6 +33,7 @@ object TestConstants {
}
object Bob {
val id = randomKey.publicKey
val channelParams = LocalParams(
dustLimitSatoshis = 542,
maxHtlcValueInFlightMsat = Long.MaxValue,

View file

@ -1,4 +1,4 @@
package fr.acinq.eclair.channel.states
package fr.acinq.eclair
import akka.actor.{ActorNotFound, ActorSystem, PoisonPill}
import akka.testkit.TestKit
@ -7,9 +7,10 @@ import org.scalatest.{BeforeAndAfterAll, BeforeAndAfterEach, fixture}
import scala.concurrent.Await
/**
* This base class kills all actor between each tests.
* Created by PM on 06/09/2016.
*/
abstract class StateSpecBaseClass extends TestKit(ActorSystem("test")) with fixture.FunSuiteLike with BeforeAndAfterEach with BeforeAndAfterAll {
abstract class TestkitBaseClass extends TestKit(ActorSystem("test")) with fixture.FunSuiteLike with BeforeAndAfterEach with BeforeAndAfterAll {
override def afterEach() {
system.actorSelection(system / "*") ! PoisonPill

View file

@ -8,6 +8,7 @@ import fr.acinq.bitcoin.{BinaryData, Crypto}
import fr.acinq.eclair.TestConstants.{Alice, Bob}
import fr.acinq.eclair._
import fr.acinq.eclair.blockchain._
import fr.acinq.eclair.payment.Relayer
import fr.acinq.eclair.wire.UpdateAddHtlc
import org.junit.runner.RunWith
import org.scalatest.FunSuite
@ -51,13 +52,15 @@ class ThroughputSpec extends FunSuite {
context.become(run(h2r - htlc.paymentHash))
}
}), "payment-handler")
val alice = system.actorOf(Channel.props(pipe, blockchain, paymentHandler, Alice.channelParams, "B"), "a")
val bob = system.actorOf(Channel.props(pipe, blockchain, paymentHandler, Bob.channelParams, "A"), "b")
val router: ActorRef = ???
val relayer = system.actorOf(Relayer.props(Globals.Node.privateKey, paymentHandler))
val alice = system.actorOf(Channel.props(pipe, blockchain, ???, relayer, Alice.channelParams, Bob.id), "a")
val bob = system.actorOf(Channel.props(pipe, blockchain, ???, relayer, Bob.channelParams, Alice.id), "b")
val latch = new CountDownLatch(2)
val listener = system.actorOf(Props(new Actor {
override def receive: Receive = {
case ChannelChangedState(_, _, _, NORMAL, _) => latch.countDown()
case ChannelChangedState(_, _, _, _, NORMAL, _) => latch.countDown()
}
}), "listener")
system.eventStream.subscribe(listener, classOf[ChannelEvent])

View file

@ -41,7 +41,7 @@ trait StateTestsHelperMethods extends TestKitBase {
alice2blockchain.forward(blockchainA)
bob2blockchain.expectMsgType[WatchSpent]
bob2blockchain.expectMsgType[WatchConfirmed]
bob ! BITCOIN_FUNDING_DEPTHOK
bob ! WatchEventConfirmed(BITCOIN_FUNDING_DEPTHOK, 42000, 42)
alice2blockchain.expectMsgType[WatchLost]
bob2blockchain.expectMsgType[WatchLost]
alice2bob.expectMsgType[FundingLocked]

View file

@ -4,10 +4,9 @@ import akka.actor.{ActorRef, Props}
import akka.testkit.{TestFSMRef, TestProbe}
import fr.acinq.eclair.TestConstants.{Alice, Bob}
import fr.acinq.eclair.blockchain.PeerWatcher
import fr.acinq.eclair.channel.states.StateSpecBaseClass
import fr.acinq.eclair.channel.{WAIT_FOR_FUNDING_CREATED_INTERNAL, _}
import fr.acinq.eclair.wire.{AcceptChannel, Error, OpenChannel}
import fr.acinq.eclair.{TestBitcoinClient, TestConstants}
import fr.acinq.eclair.{TestkitBaseClass, TestBitcoinClient, TestConstants}
import org.junit.runner.RunWith
import org.scalatest.junit.JUnitRunner
@ -17,7 +16,7 @@ import scala.concurrent.duration._
* Created by PM on 05/07/2016.
*/
@RunWith(classOf[JUnitRunner])
class WaitForAcceptChannelStateSpec extends StateSpecBaseClass {
class WaitForAcceptChannelTestkit extends TestkitBaseClass {
type FixtureParam = Tuple5[TestFSMRef[State, Data, Channel], TestProbe, TestProbe, TestProbe, ActorRef]
@ -27,9 +26,10 @@ class WaitForAcceptChannelStateSpec extends StateSpecBaseClass {
val alice2blockchain = TestProbe()
val blockchainA = system.actorOf(Props(new PeerWatcher(new TestBitcoinClient(), 300)))
val bob2blockchain = TestProbe()
val paymentHandler = TestProbe()
val alice: TestFSMRef[State, Data, Channel] = TestFSMRef(new Channel(alice2bob.ref, alice2blockchain.ref, paymentHandler.ref, Alice.channelParams, "0B"))
val bob: TestFSMRef[State, Data, Channel] = TestFSMRef(new Channel(bob2alice.ref, bob2blockchain.ref, paymentHandler.ref, Bob.channelParams, "0A"))
val relayer = TestProbe()
val router = TestProbe()
val alice: TestFSMRef[State, Data, Channel] = TestFSMRef(new Channel(alice2bob.ref, alice2blockchain.ref, router.ref, relayer.ref, Alice.channelParams, Bob.id))
val bob: TestFSMRef[State, Data, Channel] = TestFSMRef(new Channel(bob2alice.ref, bob2blockchain.ref, router.ref, relayer.ref, Bob.channelParams, Alice.id))
alice ! INPUT_INIT_FUNDER(TestConstants.fundingSatoshis, TestConstants.pushMsat)
bob ! INPUT_INIT_FUNDEE()
within(30 seconds) {

View file

@ -1,10 +1,9 @@
package fr.acinq.eclair.channel.states.a
import akka.testkit.{TestFSMRef, TestProbe}
import fr.acinq.eclair.TestConstants
import fr.acinq.eclair.{TestkitBaseClass, TestConstants}
import fr.acinq.eclair.TestConstants.{Alice, Bob}
import fr.acinq.eclair.channel._
import fr.acinq.eclair.channel.states.StateSpecBaseClass
import fr.acinq.eclair.wire.{Error, OpenChannel}
import org.junit.runner.RunWith
import org.scalatest.junit.JUnitRunner
@ -15,7 +14,7 @@ import scala.concurrent.duration._
* Created by PM on 05/07/2016.
*/
@RunWith(classOf[JUnitRunner])
class WaitForOpenChannelStateSpec extends StateSpecBaseClass {
class WaitForOpenChannelTestkit extends TestkitBaseClass {
type FixtureParam = Tuple4[TestFSMRef[State, Data, Channel], TestProbe, TestProbe, TestProbe]
@ -24,9 +23,10 @@ class WaitForOpenChannelStateSpec extends StateSpecBaseClass {
val bob2alice = TestProbe()
val alice2blockchain = TestProbe()
val bob2blockchain = TestProbe()
val paymentHandler = TestProbe()
val alice: TestFSMRef[State, Data, Channel] = TestFSMRef(new Channel(alice2bob.ref, alice2blockchain.ref, paymentHandler.ref, Alice.channelParams, "0B"))
val bob: TestFSMRef[State, Data, Channel] = TestFSMRef(new Channel(bob2alice.ref, bob2blockchain.ref, paymentHandler.ref, Bob.channelParams, "0A"))
val relayer = TestProbe()
val router = TestProbe()
val alice: TestFSMRef[State, Data, Channel] = TestFSMRef(new Channel(alice2bob.ref, alice2blockchain.ref, router.ref, relayer.ref, Alice.channelParams, Bob.id))
val bob: TestFSMRef[State, Data, Channel] = TestFSMRef(new Channel(bob2alice.ref, bob2blockchain.ref, router.ref, relayer.ref, Bob.channelParams, Alice.id))
alice ! INPUT_INIT_FUNDER(TestConstants.fundingSatoshis, TestConstants.pushMsat)
bob ! INPUT_INIT_FUNDEE()
within(30 seconds) {

View file

@ -5,9 +5,8 @@ import akka.testkit.{TestFSMRef, TestProbe}
import fr.acinq.eclair.TestConstants.{Alice, Bob}
import fr.acinq.eclair.blockchain.{MakeFundingTx, PeerWatcher}
import fr.acinq.eclair.channel._
import fr.acinq.eclair.channel.states.StateSpecBaseClass
import fr.acinq.eclair.wire.{AcceptChannel, Error, FundingCreated, OpenChannel}
import fr.acinq.eclair.{TestBitcoinClient, TestConstants}
import fr.acinq.eclair.{TestkitBaseClass, TestBitcoinClient, TestConstants}
import org.junit.runner.RunWith
import org.scalatest.junit.JUnitRunner
@ -17,7 +16,7 @@ import scala.concurrent.duration._
* Created by PM on 05/07/2016.
*/
@RunWith(classOf[JUnitRunner])
class WaitForFundingCreatedInternalStateSpec extends StateSpecBaseClass {
class WaitForFundingCreatedInternalTestkit extends TestkitBaseClass {
type FixtureParam = Tuple5[TestFSMRef[State, Data, Channel], TestProbe, TestProbe, TestProbe, ActorRef]
@ -27,9 +26,10 @@ class WaitForFundingCreatedInternalStateSpec extends StateSpecBaseClass {
val alice2blockchain = TestProbe()
val blockchainA = system.actorOf(Props(new PeerWatcher(new TestBitcoinClient(), 300)))
val bob2blockchain = TestProbe()
val paymentHandler = TestProbe()
val alice: TestFSMRef[State, Data, Channel] = TestFSMRef(new Channel(alice2bob.ref, alice2blockchain.ref, paymentHandler.ref, Alice.channelParams, "0B"))
val bob: TestFSMRef[State, Data, Channel] = TestFSMRef(new Channel(bob2alice.ref, bob2blockchain.ref, paymentHandler.ref, Bob.channelParams, "0A"))
val relayer = TestProbe()
val router = TestProbe()
val alice: TestFSMRef[State, Data, Channel] = TestFSMRef(new Channel(alice2bob.ref, alice2blockchain.ref, router.ref, relayer.ref, Alice.channelParams, Bob.id))
val bob: TestFSMRef[State, Data, Channel] = TestFSMRef(new Channel(bob2alice.ref, bob2blockchain.ref, router.ref, relayer.ref, Bob.channelParams, Alice.id))
alice ! INPUT_INIT_FUNDER(TestConstants.fundingSatoshis, TestConstants.pushMsat)
bob ! INPUT_INIT_FUNDEE()
within(30 seconds) {

View file

@ -5,9 +5,8 @@ import akka.testkit.{TestFSMRef, TestProbe}
import fr.acinq.eclair.TestConstants.{Alice, Bob}
import fr.acinq.eclair.blockchain.{PeerWatcher, WatchConfirmed, WatchSpent}
import fr.acinq.eclair.channel._
import fr.acinq.eclair.channel.states.StateSpecBaseClass
import fr.acinq.eclair.wire._
import fr.acinq.eclair.{TestBitcoinClient, TestConstants}
import fr.acinq.eclair.{TestkitBaseClass, TestBitcoinClient, TestConstants}
import org.junit.runner.RunWith
import org.scalatest.junit.JUnitRunner
@ -17,7 +16,7 @@ import scala.concurrent.duration._
* Created by PM on 05/07/2016.
*/
@RunWith(classOf[JUnitRunner])
class WaitForFundingCreatedStateSpec extends StateSpecBaseClass {
class WaitForFundingCreatedTestkit extends TestkitBaseClass {
type FixtureParam = Tuple4[TestFSMRef[State, Data, Channel], TestProbe, TestProbe, TestProbe]
@ -26,9 +25,10 @@ class WaitForFundingCreatedStateSpec extends StateSpecBaseClass {
val bob2alice = TestProbe()
val blockchainA = system.actorOf(Props(new PeerWatcher(new TestBitcoinClient(), 300)))
val bob2blockchain = TestProbe()
val paymentHandler = TestProbe()
val alice: TestFSMRef[State, Data, Channel] = TestFSMRef(new Channel(alice2bob.ref, blockchainA, paymentHandler.ref, Alice.channelParams, "0B"))
val bob: TestFSMRef[State, Data, Channel] = TestFSMRef(new Channel(bob2alice.ref, bob2blockchain.ref, paymentHandler.ref, Bob.channelParams, "0A"))
val relayer = TestProbe()
val router = TestProbe()
val alice: TestFSMRef[State, Data, Channel] = TestFSMRef(new Channel(alice2bob.ref, blockchainA, router.ref, relayer.ref, Alice.channelParams, Bob.id))
val bob: TestFSMRef[State, Data, Channel] = TestFSMRef(new Channel(bob2alice.ref, bob2blockchain.ref, router.ref, relayer.ref, Bob.channelParams, Alice.id))
alice ! INPUT_INIT_FUNDER(TestConstants.fundingSatoshis, TestConstants.pushMsat)
bob ! INPUT_INIT_FUNDEE()
within(30 seconds) {

View file

@ -6,9 +6,8 @@ import fr.acinq.bitcoin.BinaryData
import fr.acinq.eclair.TestConstants.{Alice, Bob}
import fr.acinq.eclair.blockchain._
import fr.acinq.eclair.channel._
import fr.acinq.eclair.channel.states.StateSpecBaseClass
import fr.acinq.eclair.wire.{AcceptChannel, Error, FundingCreated, FundingSigned, OpenChannel}
import fr.acinq.eclair.{TestBitcoinClient, TestConstants}
import fr.acinq.eclair.{TestkitBaseClass, TestBitcoinClient, TestConstants}
import org.junit.runner.RunWith
import org.scalatest.junit.JUnitRunner
@ -18,7 +17,7 @@ import scala.concurrent.duration._
* Created by PM on 05/07/2016.
*/
@RunWith(classOf[JUnitRunner])
class WaitForFundingSignedStateSpec extends StateSpecBaseClass {
class WaitForFundingSignedTestkit extends TestkitBaseClass {
type FixtureParam = Tuple5[TestFSMRef[State, Data, Channel], TestProbe, TestProbe, TestProbe, ActorRef]
@ -28,9 +27,10 @@ class WaitForFundingSignedStateSpec extends StateSpecBaseClass {
val alice2blockchain = TestProbe()
val blockchainA = system.actorOf(Props(new PeerWatcher(new TestBitcoinClient(), 300)))
val bob2blockchain = TestProbe()
val paymentHandler = TestProbe()
val alice: TestFSMRef[State, Data, Channel] = TestFSMRef(new Channel(alice2bob.ref, alice2blockchain.ref, paymentHandler.ref, Alice.channelParams, "0B"))
val bob: TestFSMRef[State, Data, Channel] = TestFSMRef(new Channel(bob2alice.ref, bob2blockchain.ref, paymentHandler.ref, Bob.channelParams, "0A"))
val relayer = TestProbe()
val router = TestProbe()
val alice: TestFSMRef[State, Data, Channel] = TestFSMRef(new Channel(alice2bob.ref, alice2blockchain.ref, router.ref, relayer.ref, Alice.channelParams, Bob.id))
val bob: TestFSMRef[State, Data, Channel] = TestFSMRef(new Channel(bob2alice.ref, bob2blockchain.ref, router.ref, relayer.ref, Bob.channelParams, Alice.id))
alice ! INPUT_INIT_FUNDER(TestConstants.fundingSatoshis, TestConstants.pushMsat)
bob ! INPUT_INIT_FUNDEE()
within(30 seconds) {

View file

@ -5,9 +5,8 @@ import akka.testkit.{TestFSMRef, TestProbe}
import fr.acinq.eclair.TestConstants.{Alice, Bob}
import fr.acinq.eclair.blockchain._
import fr.acinq.eclair.channel._
import fr.acinq.eclair.channel.states.StateSpecBaseClass
import fr.acinq.eclair.wire.{AcceptChannel, Error, FundingCreated, FundingLocked, FundingSigned, OpenChannel}
import fr.acinq.eclair.{TestBitcoinClient, TestConstants}
import fr.acinq.eclair.{TestkitBaseClass, TestBitcoinClient, TestConstants}
import org.junit.runner.RunWith
import org.scalatest.junit.JUnitRunner
@ -17,7 +16,7 @@ import scala.concurrent.duration._
* Created by PM on 05/07/2016.
*/
@RunWith(classOf[JUnitRunner])
class WaitForFundingLockedInternalStateSpec extends StateSpecBaseClass {
class WaitForFundingLockedInternalTestkit extends TestkitBaseClass {
type FixtureParam = Tuple6[TestFSMRef[State, Data, Channel], TestFSMRef[State, Data, Channel], TestProbe, TestProbe, TestProbe, ActorRef]
@ -27,9 +26,10 @@ class WaitForFundingLockedInternalStateSpec extends StateSpecBaseClass {
val alice2blockchain = TestProbe()
val blockchainA = system.actorOf(Props(new PeerWatcher(new TestBitcoinClient(), 300)))
val bob2blockchain = TestProbe()
val paymentHandler = TestProbe()
val alice: TestFSMRef[State, Data, Channel] = TestFSMRef(new Channel(alice2bob.ref, alice2blockchain.ref, paymentHandler.ref, Alice.channelParams, "0B"))
val bob: TestFSMRef[State, Data, Channel] = TestFSMRef(new Channel(bob2alice.ref, bob2blockchain.ref, paymentHandler.ref, Bob.channelParams, "0A"))
val relayer = TestProbe()
val router = TestProbe()
val alice: TestFSMRef[State, Data, Channel] = TestFSMRef(new Channel(alice2bob.ref, alice2blockchain.ref, router.ref, relayer.ref, Alice.channelParams, Bob.id))
val bob: TestFSMRef[State, Data, Channel] = TestFSMRef(new Channel(bob2alice.ref, bob2blockchain.ref, router.ref, relayer.ref, Bob.channelParams, Alice.id))
alice ! INPUT_INIT_FUNDER(TestConstants.fundingSatoshis, TestConstants.pushMsat)
bob ! INPUT_INIT_FUNDEE()
within(30 seconds) {
@ -54,7 +54,7 @@ class WaitForFundingLockedInternalStateSpec extends StateSpecBaseClass {
test("recv FundingLocked") { case (alice, bob, alice2bob, bob2alice, alice2blockchain, _) =>
within(30 seconds) {
// make bob send a FundingLocked msg
bob ! BITCOIN_FUNDING_DEPTHOK
bob ! WatchEventConfirmed(BITCOIN_FUNDING_DEPTHOK, 42000, 42)
val msg = bob2alice.expectMsgType[FundingLocked]
bob2alice.forward(alice)
awaitCond(alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_LOCKED_INTERNAL].deferred == Some(msg))
@ -64,7 +64,7 @@ class WaitForFundingLockedInternalStateSpec extends StateSpecBaseClass {
test("recv BITCOIN_FUNDING_DEPTHOK") { case (alice, _, alice2bob, bob2alice, alice2blockchain, _) =>
within(30 seconds) {
alice ! BITCOIN_FUNDING_DEPTHOK
alice ! WatchEventConfirmed(BITCOIN_FUNDING_DEPTHOK, 42000, 42)
awaitCond(alice.stateName == WAIT_FOR_FUNDING_LOCKED)
alice2blockchain.expectMsgType[WatchLost]
alice2bob.expectMsgType[FundingLocked]
@ -83,7 +83,7 @@ class WaitForFundingLockedInternalStateSpec extends StateSpecBaseClass {
within(30 seconds) {
// bob publishes his commitment tx
val tx = bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_LOCKED_INTERNAL].commitments.localCommit.publishableTxs.commitTx.tx
alice ! (BITCOIN_FUNDING_SPENT, tx)
alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, tx)
alice2blockchain.expectMsgType[WatchConfirmed]
awaitCond(alice.stateName == CLOSING)
}
@ -92,7 +92,7 @@ class WaitForFundingLockedInternalStateSpec extends StateSpecBaseClass {
test("recv BITCOIN_FUNDING_SPENT (other commit)") { case (alice, _, alice2bob, bob2alice, alice2blockchain, _) =>
within(30 seconds) {
val tx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_LOCKED_INTERNAL].commitments.localCommit.publishableTxs.commitTx.tx
alice ! (BITCOIN_FUNDING_SPENT, null)
alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, null)
alice2bob.expectMsgType[Error]
alice2blockchain.expectMsg(PublishAsap(tx))
awaitCond(alice.stateName == ERR_INFORMATION_LEAK)

View file

@ -5,9 +5,8 @@ import akka.testkit.{TestFSMRef, TestProbe}
import fr.acinq.eclair.TestConstants.{Alice, Bob}
import fr.acinq.eclair.blockchain._
import fr.acinq.eclair.channel._
import fr.acinq.eclair.channel.states.StateSpecBaseClass
import fr.acinq.eclair.wire._
import fr.acinq.eclair.{TestBitcoinClient, TestConstants}
import fr.acinq.eclair.{TestkitBaseClass, TestBitcoinClient, TestConstants}
import org.junit.runner.RunWith
import org.scalatest.junit.JUnitRunner
@ -17,7 +16,7 @@ import scala.concurrent.duration._
* Created by PM on 05/07/2016.
*/
@RunWith(classOf[JUnitRunner])
class WaitForFundingLockedStateSpec extends StateSpecBaseClass {
class WaitForFundingLockedTestkit extends TestkitBaseClass {
type FixtureParam = Tuple6[TestFSMRef[State, Data, Channel], TestFSMRef[State, Data, Channel], TestProbe, TestProbe, TestProbe, ActorRef]
@ -27,9 +26,10 @@ class WaitForFundingLockedStateSpec extends StateSpecBaseClass {
val alice2blockchain = TestProbe()
val blockchainA = system.actorOf(Props(new PeerWatcher(new TestBitcoinClient(), 300)))
val bob2blockchain = TestProbe()
val paymentHandler = TestProbe()
val alice: TestFSMRef[State, Data, Channel] = TestFSMRef(new Channel(alice2bob.ref, alice2blockchain.ref, paymentHandler.ref, Alice.channelParams, "0B"))
val bob: TestFSMRef[State, Data, Channel] = TestFSMRef(new Channel(bob2alice.ref, bob2blockchain.ref, paymentHandler.ref, Bob.channelParams, "0A"))
val relayer = TestProbe()
val router = TestProbe()
val alice: TestFSMRef[State, Data, Channel] = TestFSMRef(new Channel(alice2bob.ref, alice2blockchain.ref, router.ref, relayer.ref, Alice.channelParams, Bob.id))
val bob: TestFSMRef[State, Data, Channel] = TestFSMRef(new Channel(bob2alice.ref, bob2blockchain.ref, router.ref, relayer.ref, Bob.channelParams, Alice.id))
alice ! INPUT_INIT_FUNDER(TestConstants.fundingSatoshis, TestConstants.pushMsat)
bob ! INPUT_INIT_FUNDEE()
within(30 seconds) {
@ -50,7 +50,7 @@ class WaitForFundingLockedStateSpec extends StateSpecBaseClass {
alice2blockchain.forward(blockchainA)
bob2blockchain.expectMsgType[WatchSpent]
bob2blockchain.expectMsgType[WatchConfirmed]
bob ! BITCOIN_FUNDING_DEPTHOK
bob ! WatchEventConfirmed(BITCOIN_FUNDING_DEPTHOK, 42000, 42)
alice2blockchain.expectMsgType[WatchLost]
bob2blockchain.expectMsgType[WatchLost]
alice2bob.expectMsgType[FundingLocked]
@ -68,11 +68,23 @@ class WaitForFundingLockedStateSpec extends StateSpecBaseClass {
}
}
test("recv FundingLocked (channel id mismatch") { case (alice, _, alice2bob, bob2alice, alice2blockchain, _) =>
within(30 seconds) {
val tx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx
val fundingLocked = bob2alice.expectMsgType[FundingLocked]
alice ! fundingLocked.copy(channelId = 42)
alice2bob.expectMsgType[Error]
awaitCond(alice.stateName == CLOSING)
alice2blockchain.expectMsg(PublishAsap(tx))
alice2blockchain.expectMsgType[WatchConfirmed]
}
}
test("recv BITCOIN_FUNDING_SPENT (remote commit)") { case (alice, bob, alice2bob, bob2alice, alice2blockchain, _) =>
within(30 seconds) {
// bob publishes his commitment tx
val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx
alice ! (BITCOIN_FUNDING_SPENT, tx)
alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, tx)
alice2blockchain.expectMsgType[WatchConfirmed]
awaitCond(alice.stateName == CLOSING)
}
@ -81,7 +93,7 @@ class WaitForFundingLockedStateSpec extends StateSpecBaseClass {
test("recv BITCOIN_FUNDING_SPENT (other commit)") { case (alice, _, alice2bob, bob2alice, alice2blockchain, _) =>
within(30 seconds) {
val tx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx
alice ! (BITCOIN_FUNDING_SPENT, null)
alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, null)
alice2bob.expectMsgType[Error]
alice2blockchain.expectMsg(PublishAsap(tx))
awaitCond(alice.stateName == ERR_INFORMATION_LEAK)

View file

@ -6,11 +6,12 @@ import fr.acinq.bitcoin.Crypto.{Point, Scalar}
import fr.acinq.bitcoin.{BinaryData, Crypto, Satoshi, Script, ScriptFlags, Transaction}
import fr.acinq.eclair.TestConstants.{Alice, Bob}
import fr.acinq.eclair.blockchain._
import fr.acinq.eclair.channel.states.{StateSpecBaseClass, StateTestsHelperMethods}
import fr.acinq.eclair.channel.states.StateTestsHelperMethods
import fr.acinq.eclair.channel.{Data, State, _}
import fr.acinq.eclair.payment.Binding
import fr.acinq.eclair.transactions.{IN, OUT}
import fr.acinq.eclair.wire.{ClosingSigned, CommitSig, Error, RevokeAndAck, Shutdown, UpdateAddHtlc, UpdateFailHtlc, UpdateFulfillHtlc}
import fr.acinq.eclair.{TestBitcoinClient, TestConstants}
import fr.acinq.eclair.{TestBitcoinClient, TestConstants, TestkitBaseClass}
import org.junit.runner.RunWith
import org.scalatest.junit.JUnitRunner
@ -20,9 +21,9 @@ import scala.concurrent.duration._
* Created by PM on 05/07/2016.
*/
@RunWith(classOf[JUnitRunner])
class NormalStateSpec extends StateSpecBaseClass with StateTestsHelperMethods {
class NormalTestkit extends TestkitBaseClass with StateTestsHelperMethods {
type FixtureParam = Tuple6[TestFSMRef[State, Data, Channel], TestFSMRef[State, Data, Channel], TestProbe, TestProbe, TestProbe, TestProbe]
type FixtureParam = Tuple7[TestFSMRef[State, Data, Channel], TestFSMRef[State, Data, Channel], TestProbe, TestProbe, TestProbe, TestProbe, TestProbe]
override def withFixture(test: OneArgTest) = {
val alice2bob = TestProbe()
@ -30,33 +31,52 @@ class NormalStateSpec extends StateSpecBaseClass with StateTestsHelperMethods {
val alice2blockchain = TestProbe()
val blockchainA = system.actorOf(Props(new PeerWatcher(new TestBitcoinClient(), 300)))
val bob2blockchain = TestProbe()
val paymentHandler = TestProbe()
val alice: TestFSMRef[State, Data, Channel] = TestFSMRef(new Channel(alice2bob.ref, alice2blockchain.ref, paymentHandler.ref, Alice.channelParams, "0B"))
val bob: TestFSMRef[State, Data, Channel] = TestFSMRef(new Channel(bob2alice.ref, bob2blockchain.ref, paymentHandler.ref, Bob.channelParams, "0A"))
val relayer = TestProbe()
val router = TestProbe()
val alice: TestFSMRef[State, Data, Channel] = TestFSMRef(new Channel(alice2bob.ref, alice2blockchain.ref, router.ref, relayer.ref, Alice.channelParams, Bob.id))
val bob: TestFSMRef[State, Data, Channel] = TestFSMRef(new Channel(bob2alice.ref, bob2blockchain.ref, router.ref, relayer.ref, Bob.channelParams, Alice.id))
within(30 seconds) {
reachNormal(alice, bob, alice2bob, bob2alice, blockchainA, alice2blockchain, bob2blockchain)
awaitCond(alice.stateName == NORMAL)
awaitCond(bob.stateName == NORMAL)
test((alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain))
test((alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain, relayer))
}
}
test("recv CMD_ADD_HTLC") { case (alice, _, alice2bob, _, _, _) =>
test("recv CMD_ADD_HTLC (empty origin)") { case (alice, _, alice2bob, _, _, _, relayer) =>
within(30 seconds) {
val initialState = alice.stateData.asInstanceOf[DATA_NORMAL]
val sender = TestProbe()
val h = BinaryData("00112233445566778899aabbccddeeff")
sender.send(alice, CMD_ADD_HTLC(50000000, h, 144))
sender.send(alice, CMD_ADD_HTLC(50000000, h, 144, origin = None))
sender.expectMsg("ok")
val htlc = alice2bob.expectMsgType[UpdateAddHtlc]
assert(htlc.id == 1 && htlc.paymentHash == h)
awaitCond(alice.stateData == initialState.copy(
commitments = initialState.commitments.copy(localCurrentHtlcId = 1, localChanges = initialState.commitments.localChanges.copy(proposed = htlc :: Nil)),
downstreams = Map(htlc.id -> None)))
commitments = initialState.commitments.copy(localCurrentHtlcId = 1, localChanges = initialState.commitments.localChanges.copy(proposed = htlc :: Nil))))
relayer.expectNoMsg()
}
}
test("recv CMD_ADD_HTLC (insufficient funds)") { case (alice, _, alice2bob, _, _, _) =>
test("recv CMD_ADD_HTLC (relayed htlc)") { case (alice, _, alice2bob, _, _, _, relayer) =>
within(30 seconds) {
val initialState = alice.stateData.asInstanceOf[DATA_NORMAL]
val sender = TestProbe()
val h = BinaryData("00112233445566778899aabbccddeeff")
val originHtlc = UpdateAddHtlc(channelId = 4298564, id = 5656, amountMsat = 50000000, expiry = 144, paymentHash = h, onionRoutingPacket = "00" * 1254)
val cmd = CMD_ADD_HTLC(originHtlc.amountMsat - 10000, h, originHtlc.expiry - 7, origin = Some(originHtlc))
sender.send(alice, cmd)
sender.expectMsg("ok")
val htlc = alice2bob.expectMsgType[UpdateAddHtlc]
assert(htlc.id == 1 && htlc.paymentHash == h)
awaitCond(alice.stateData == initialState.copy(
commitments = initialState.commitments.copy(localCurrentHtlcId = 1, localChanges = initialState.commitments.localChanges.copy(proposed = htlc :: Nil))))
val binding = relayer.expectMsgType[Binding]
assert(binding === Binding(downstream = originHtlc, upstream = htlc))
}
}
test("recv CMD_ADD_HTLC (insufficient funds)") { case (alice, _, alice2bob, _, _, _, _) =>
within(30 seconds) {
val sender = TestProbe()
sender.send(alice, CMD_ADD_HTLC(Int.MaxValue, "11" * 32, 144))
@ -64,7 +84,7 @@ class NormalStateSpec extends StateSpecBaseClass with StateTestsHelperMethods {
}
}
test("recv CMD_ADD_HTLC (insufficient funds w/ pending htlcs 1/2)") { case (alice, _, alice2bob, _, _, _) =>
test("recv CMD_ADD_HTLC (insufficient funds w/ pending htlcs 1/2)") { case (alice, _, alice2bob, _, _, _, _) =>
within(30 seconds) {
val sender = TestProbe()
sender.send(alice, CMD_ADD_HTLC(500000000, "11" * 32, 144))
@ -76,7 +96,7 @@ class NormalStateSpec extends StateSpecBaseClass with StateTestsHelperMethods {
}
}
test("recv CMD_ADD_HTLC (insufficient funds w/ pending htlcs 2/2)") { case (alice, _, alice2bob, _, _, _) =>
test("recv CMD_ADD_HTLC (insufficient funds w/ pending htlcs 2/2)") { case (alice, _, alice2bob, _, _, _, _) =>
within(30 seconds) {
val sender = TestProbe()
sender.send(alice, CMD_ADD_HTLC(300000000, "11" * 32, 144))
@ -88,7 +108,7 @@ class NormalStateSpec extends StateSpecBaseClass with StateTestsHelperMethods {
}
}
test("recv CMD_ADD_HTLC (while waiting for Shutdown)") { case (alice, _, alice2bob, _, alice2blockchain, _) =>
test("recv CMD_ADD_HTLC (while waiting for Shutdown)") { case (alice, _, alice2bob, _, alice2blockchain, _, _) =>
within(30 seconds) {
val sender = TestProbe()
sender.send(alice, CMD_CLOSE(None))
@ -102,7 +122,7 @@ class NormalStateSpec extends StateSpecBaseClass with StateTestsHelperMethods {
}
}
test("recv UpdateAddHtlc") { case (_, bob, alice2bob, _, _, _) =>
test("recv UpdateAddHtlc") { case (_, bob, alice2bob, _, _, _, _) =>
within(30 seconds) {
val initialData = bob.stateData.asInstanceOf[DATA_NORMAL]
val htlc = UpdateAddHtlc(0, 42, 150, 144, BinaryData("00112233445566778899aabbccddeeff"), "")
@ -111,7 +131,7 @@ class NormalStateSpec extends StateSpecBaseClass with StateTestsHelperMethods {
}
}
test("recv UpdateAddHtlc (insufficient funds)") { case (_, bob, alice2bob, bob2alice, _, bob2blockchain) =>
test("recv UpdateAddHtlc (insufficient funds)") { case (_, bob, alice2bob, bob2alice, _, bob2blockchain, _) =>
within(30 seconds) {
val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx
val htlc = UpdateAddHtlc(0, 42, Long.MaxValue, 144, BinaryData("00112233445566778899aabbccddeeff"), "")
@ -123,7 +143,7 @@ class NormalStateSpec extends StateSpecBaseClass with StateTestsHelperMethods {
}
}
test("recv UpdateAddHtlc (insufficient funds w/ pending htlcs 1/2)") { case (_, bob, alice2bob, bob2alice, _, bob2blockchain) =>
test("recv UpdateAddHtlc (insufficient funds w/ pending htlcs 1/2)") { case (_, bob, alice2bob, bob2alice, _, bob2blockchain, _) =>
within(30 seconds) {
val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx
alice2bob.forward(bob, UpdateAddHtlc(0, 42, 500000000, 144, "11" * 32, ""))
@ -136,7 +156,7 @@ class NormalStateSpec extends StateSpecBaseClass with StateTestsHelperMethods {
}
}
test("recv UpdateAddHtlc (insufficient funds w/ pending htlcs 2/2)") { case (_, bob, alice2bob, bob2alice, _, bob2blockchain) =>
test("recv UpdateAddHtlc (insufficient funds w/ pending htlcs 2/2)") { case (_, bob, alice2bob, bob2alice, _, bob2blockchain, _) =>
within(30 seconds) {
val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx
alice2bob.forward(bob, UpdateAddHtlc(0, 42, 300000000, 144, "11" * 32, ""))
@ -149,7 +169,7 @@ class NormalStateSpec extends StateSpecBaseClass with StateTestsHelperMethods {
}
}
test("recv CMD_SIGN") { case (alice, bob, alice2bob, bob2alice, _, _) =>
test("recv CMD_SIGN") { case (alice, bob, alice2bob, bob2alice, _, _, _) =>
within(30 seconds) {
val sender = TestProbe()
val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice)
@ -160,7 +180,7 @@ class NormalStateSpec extends StateSpecBaseClass with StateTestsHelperMethods {
}
}
test("recv CMD_SIGN (no changes)") { case (alice, bob, alice2bob, bob2alice, alice2blockchain, _) =>
test("recv CMD_SIGN (no changes)") { case (alice, bob, alice2bob, bob2alice, alice2blockchain, _, _) =>
within(30 seconds) {
val sender = TestProbe()
sender.send(alice, CMD_SIGN)
@ -169,7 +189,7 @@ class NormalStateSpec extends StateSpecBaseClass with StateTestsHelperMethods {
}
}
ignore("recv CMD_SIGN (while waiting for RevokeAndAck)") { case (alice, bob, alice2bob, bob2alice, alice2blockchain, _) =>
ignore("recv CMD_SIGN (while waiting for RevokeAndAck)") { case (alice, bob, alice2bob, bob2alice, alice2blockchain, _, _) =>
within(30 seconds) {
val tx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx
val sender = TestProbe()
@ -184,7 +204,7 @@ class NormalStateSpec extends StateSpecBaseClass with StateTestsHelperMethods {
}
}
test("recv CommitSig (one htlc received)") { case (alice, bob, alice2bob, bob2alice, _, _) =>
test("recv CommitSig (one htlc received)") { case (alice, bob, alice2bob, bob2alice, _, _, _) =>
within(30 seconds) {
val sender = TestProbe()
@ -206,7 +226,7 @@ class NormalStateSpec extends StateSpecBaseClass with StateTestsHelperMethods {
}
}
test("recv CommitSig (one htlc sent)") { case (alice, bob, alice2bob, bob2alice, _, _) =>
test("recv CommitSig (one htlc sent)") { case (alice, bob, alice2bob, bob2alice, _, _, _) =>
within(30 seconds) {
val sender = TestProbe()
@ -232,7 +252,7 @@ class NormalStateSpec extends StateSpecBaseClass with StateTestsHelperMethods {
}
}
test("recv CommitSig (multiple htlcs in both directions)") { case (alice, bob, alice2bob, bob2alice, _, _) =>
test("recv CommitSig (multiple htlcs in both directions)") { case (alice, bob, alice2bob, bob2alice, _, _, _) =>
within(30 seconds) {
val sender = TestProbe()
@ -269,7 +289,7 @@ class NormalStateSpec extends StateSpecBaseClass with StateTestsHelperMethods {
}
// TODO: maybe should be illegal?
ignore("recv CommitSig (two htlcs received with same r)") { case (alice, bob, alice2bob, bob2alice, _, _) =>
ignore("recv CommitSig (two htlcs received with same r)") { case (alice, bob, alice2bob, bob2alice, _, _, _) =>
within(30 seconds) {
val sender = TestProbe()
val r = BinaryData("00112233445566778899aabbccddeeff")
@ -296,7 +316,7 @@ class NormalStateSpec extends StateSpecBaseClass with StateTestsHelperMethods {
}
}
test("recv CommitSig (no changes)") { case (alice, bob, alice2bob, bob2alice, _, bob2blockchain) =>
test("recv CommitSig (no changes)") { case (alice, bob, alice2bob, bob2alice, _, bob2blockchain, _) =>
within(30 seconds) {
val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx
val sender = TestProbe()
@ -309,7 +329,7 @@ class NormalStateSpec extends StateSpecBaseClass with StateTestsHelperMethods {
}
}
test("recv CommitSig (invalid signature)") { case (alice, bob, alice2bob, bob2alice, _, bob2blockchain) =>
test("recv CommitSig (invalid signature)") { case (alice, bob, alice2bob, bob2alice, _, bob2blockchain, _) =>
within(30 seconds) {
val sender = TestProbe()
val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice)
@ -324,7 +344,7 @@ class NormalStateSpec extends StateSpecBaseClass with StateTestsHelperMethods {
}
}
test("recv RevokeAndAck (one htlc sent)") { case (alice, bob, alice2bob, bob2alice, _, bob2blockchain) =>
test("recv RevokeAndAck (one htlc sent)") { case (alice, bob, alice2bob, bob2alice, _, bob2blockchain, _) =>
within(30 seconds) {
val sender = TestProbe()
val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice)
@ -343,7 +363,7 @@ class NormalStateSpec extends StateSpecBaseClass with StateTestsHelperMethods {
}
}
test("recv RevokeAndAck (one htlc received)") { case (alice, bob, alice2bob, bob2alice, _, bob2blockchain) =>
test("recv RevokeAndAck (one htlc received)") { case (alice, bob, alice2bob, bob2alice, _, bob2blockchain, _) =>
within(30 seconds) {
val sender = TestProbe()
val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice)
@ -368,7 +388,7 @@ class NormalStateSpec extends StateSpecBaseClass with StateTestsHelperMethods {
}
}
test("recv RevokeAndAck (multiple htlcs in both directions)") { case (alice, bob, alice2bob, bob2alice, _, bob2blockchain) =>
test("recv RevokeAndAck (multiple htlcs in both directions)") { case (alice, bob, alice2bob, bob2alice, _, bob2blockchain, _) =>
within(30 seconds) {
val sender = TestProbe()
val (r1, htlc1) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) // a->b (regular)
@ -408,7 +428,7 @@ class NormalStateSpec extends StateSpecBaseClass with StateTestsHelperMethods {
}
}
test("recv RevokeAndAck (invalid preimage)") { case (alice, bob, alice2bob, bob2alice, alice2blockchain, _) =>
test("recv RevokeAndAck (invalid preimage)") { case (alice, bob, alice2bob, bob2alice, alice2blockchain, _, _) =>
within(30 seconds) {
val tx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx
val sender = TestProbe()
@ -429,7 +449,7 @@ class NormalStateSpec extends StateSpecBaseClass with StateTestsHelperMethods {
}
}
test("recv RevokeAndAck (unexpectedly)") { case (alice, bob, alice2bob, bob2alice, alice2blockchain, _) =>
test("recv RevokeAndAck (unexpectedly)") { case (alice, bob, alice2bob, bob2alice, alice2blockchain, _, _) =>
within(30 seconds) {
val tx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx
val sender = TestProbe()
@ -442,7 +462,7 @@ class NormalStateSpec extends StateSpecBaseClass with StateTestsHelperMethods {
}
}
test("recv CMD_FULFILL_HTLC") { case (alice, bob, alice2bob, bob2alice, _, _) =>
test("recv CMD_FULFILL_HTLC") { case (alice, bob, alice2bob, bob2alice, _, _, _) =>
within(30 seconds) {
val sender = TestProbe()
val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice)
@ -457,7 +477,7 @@ class NormalStateSpec extends StateSpecBaseClass with StateTestsHelperMethods {
}
}
test("recv CMD_FULFILL_HTLC (unknown htlc id)") { case (alice, bob, alice2bob, bob2alice, _, _) =>
test("recv CMD_FULFILL_HTLC (unknown htlc id)") { case (alice, bob, alice2bob, bob2alice, _, _, _) =>
within(30 seconds) {
val sender = TestProbe()
val r: BinaryData = "11" * 32
@ -469,7 +489,7 @@ class NormalStateSpec extends StateSpecBaseClass with StateTestsHelperMethods {
}
}
test("recv CMD_FULFILL_HTLC (invalid preimage)") { case (alice, bob, alice2bob, bob2alice, _, _) =>
test("recv CMD_FULFILL_HTLC (invalid preimage)") { case (alice, bob, alice2bob, bob2alice, _, _, _) =>
within(30 seconds) {
val sender = TestProbe()
val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice)
@ -483,7 +503,7 @@ class NormalStateSpec extends StateSpecBaseClass with StateTestsHelperMethods {
}
}
test("recv UpdateFulfillHtlc (sender has not signed)") { case (alice, bob, alice2bob, bob2alice, _, _) =>
test("recv UpdateFulfillHtlc (sender has not signed)") { case (alice, bob, alice2bob, bob2alice, _, _, _) =>
within(30 seconds) {
val sender = TestProbe()
val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice)
@ -497,12 +517,11 @@ class NormalStateSpec extends StateSpecBaseClass with StateTestsHelperMethods {
val initialState = alice.stateData.asInstanceOf[DATA_NORMAL]
bob2alice.forward(alice)
awaitCond(alice.stateData == initialState.copy(
commitments = initialState.commitments.copy(remoteChanges = initialState.commitments.remoteChanges.copy(initialState.commitments.remoteChanges.proposed :+ fulfill)),
downstreams = Map()))
commitments = initialState.commitments.copy(remoteChanges = initialState.commitments.remoteChanges.copy(initialState.commitments.remoteChanges.proposed :+ fulfill))))
}
}
test("recv UpdateFulfillHtlc (sender has signed)") { case (alice, bob, alice2bob, bob2alice, _, _) =>
test("recv UpdateFulfillHtlc (sender has signed)") { case (alice, bob, alice2bob, bob2alice, _, _, _) =>
within(30 seconds) {
val sender = TestProbe()
val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice)
@ -516,12 +535,11 @@ class NormalStateSpec extends StateSpecBaseClass with StateTestsHelperMethods {
val initialState = alice.stateData.asInstanceOf[DATA_NORMAL]
bob2alice.forward(alice)
awaitCond(alice.stateData == initialState.copy(
commitments = initialState.commitments.copy(remoteChanges = initialState.commitments.remoteChanges.copy(initialState.commitments.remoteChanges.proposed :+ fulfill)),
downstreams = Map()))
commitments = initialState.commitments.copy(remoteChanges = initialState.commitments.remoteChanges.copy(initialState.commitments.remoteChanges.proposed :+ fulfill))))
}
}
test("recv UpdateFulfillHtlc (unknown htlc id)") { case (alice, _, alice2bob, _, alice2blockchain, _) =>
test("recv UpdateFulfillHtlc (unknown htlc id)") { case (alice, _, alice2bob, _, alice2blockchain, _, _) =>
within(30 seconds) {
val tx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx
val sender = TestProbe()
@ -533,7 +551,7 @@ class NormalStateSpec extends StateSpecBaseClass with StateTestsHelperMethods {
}
}
test("recv UpdateFulfillHtlc (invalid preimage)") { case (alice, bob, alice2bob, bob2alice, alice2blockchain, _) =>
test("recv UpdateFulfillHtlc (invalid preimage)") { case (alice, bob, alice2bob, bob2alice, alice2blockchain, _, _) =>
within(30 seconds) {
val tx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx
val sender = TestProbe()
@ -549,7 +567,7 @@ class NormalStateSpec extends StateSpecBaseClass with StateTestsHelperMethods {
}
}
test("recv CMD_FAIL_HTLC") { case (alice, bob, alice2bob, bob2alice, _, _) =>
test("recv CMD_FAIL_HTLC") { case (alice, bob, alice2bob, bob2alice, _, _, _) =>
within(30 seconds) {
val sender = TestProbe()
val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice)
@ -564,7 +582,7 @@ class NormalStateSpec extends StateSpecBaseClass with StateTestsHelperMethods {
}
}
test("recv CMD_FAIL_HTLC (unknown htlc id)") { case (alice, bob, alice2bob, bob2alice, _, _) =>
test("recv CMD_FAIL_HTLC (unknown htlc id)") { case (alice, bob, alice2bob, bob2alice, _, _, _) =>
within(30 seconds) {
val sender = TestProbe()
val r: BinaryData = "11" * 32
@ -576,7 +594,7 @@ class NormalStateSpec extends StateSpecBaseClass with StateTestsHelperMethods {
}
}
test("recv UpdateFailHtlc (sender has not signed)") { case (alice, bob, alice2bob, bob2alice, _, _) =>
test("recv UpdateFailHtlc (sender has not signed)") { case (alice, bob, alice2bob, bob2alice, _, _, _) =>
within(30 seconds) {
val sender = TestProbe()
val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice)
@ -590,12 +608,11 @@ class NormalStateSpec extends StateSpecBaseClass with StateTestsHelperMethods {
val initialState = alice.stateData.asInstanceOf[DATA_NORMAL]
bob2alice.forward(alice)
awaitCond(alice.stateData == initialState.copy(
commitments = initialState.commitments.copy(remoteChanges = initialState.commitments.remoteChanges.copy(initialState.commitments.remoteChanges.proposed :+ fulfill)),
downstreams = Map()))
commitments = initialState.commitments.copy(remoteChanges = initialState.commitments.remoteChanges.copy(initialState.commitments.remoteChanges.proposed :+ fulfill))))
}
}
test("recv UpdateFailHtlc (sender has signed") { case (alice, bob, alice2bob, bob2alice, _, _) =>
test("recv UpdateFailHtlc (sender has signed") { case (alice, bob, alice2bob, bob2alice, _, _, _) =>
within(30 seconds) {
val sender = TestProbe()
val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice)
@ -610,12 +627,11 @@ class NormalStateSpec extends StateSpecBaseClass with StateTestsHelperMethods {
val initialState = alice.stateData.asInstanceOf[DATA_NORMAL]
bob2alice.forward(alice)
awaitCond(alice.stateData == initialState.copy(
commitments = initialState.commitments.copy(remoteChanges = initialState.commitments.remoteChanges.copy(initialState.commitments.remoteChanges.proposed :+ fulfill)),
downstreams = Map()))
commitments = initialState.commitments.copy(remoteChanges = initialState.commitments.remoteChanges.copy(initialState.commitments.remoteChanges.proposed :+ fulfill))))
}
}
test("recv UpdateFailHtlc (unknown htlc id)") { case (alice, _, alice2bob, _, alice2blockchain, _) =>
test("recv UpdateFailHtlc (unknown htlc id)") { case (alice, _, alice2bob, _, alice2blockchain, _, _) =>
within(30 seconds) {
val tx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx
val sender = TestProbe()
@ -627,7 +643,7 @@ class NormalStateSpec extends StateSpecBaseClass with StateTestsHelperMethods {
}
}
test("recv CMD_CLOSE (no pending htlcs)") { case (alice, _, alice2bob, _, alice2blockchain, _) =>
test("recv CMD_CLOSE (no pending htlcs)") { case (alice, _, alice2bob, _, alice2blockchain, _, _) =>
within(30 seconds) {
val sender = TestProbe()
awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].localShutdown.isEmpty)
@ -639,7 +655,7 @@ class NormalStateSpec extends StateSpecBaseClass with StateTestsHelperMethods {
}
}
test("recv CMD_CLOSE (with unacked sent htlcs)") { case (alice, bob, alice2bob, bob2alice, _, _) =>
test("recv CMD_CLOSE (with unacked sent htlcs)") { case (alice, bob, alice2bob, bob2alice, _, _, _) =>
within(30 seconds) {
val sender = TestProbe()
val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice)
@ -648,7 +664,7 @@ class NormalStateSpec extends StateSpecBaseClass with StateTestsHelperMethods {
}
}
test("recv CMD_CLOSE (with invalid final script)") { case (alice, bob, alice2bob, bob2alice, _, _) =>
test("recv CMD_CLOSE (with invalid final script)") { case (alice, bob, alice2bob, bob2alice, _, _, _) =>
within(30 seconds) {
val sender = TestProbe()
sender.send(alice, CMD_CLOSE(Some(BinaryData("00112233445566778899"))))
@ -656,7 +672,7 @@ class NormalStateSpec extends StateSpecBaseClass with StateTestsHelperMethods {
}
}
test("recv CMD_CLOSE (with signed sent htlcs)") { case (alice, bob, alice2bob, bob2alice, _, _) =>
test("recv CMD_CLOSE (with signed sent htlcs)") { case (alice, bob, alice2bob, bob2alice, _, _, _) =>
within(30 seconds) {
val sender = TestProbe()
val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice)
@ -669,7 +685,7 @@ class NormalStateSpec extends StateSpecBaseClass with StateTestsHelperMethods {
}
}
test("recv CMD_CLOSE (two in a row)") { case (alice, _, alice2bob, _, alice2blockchain, _) =>
test("recv CMD_CLOSE (two in a row)") { case (alice, _, alice2bob, _, alice2blockchain, _, _) =>
within(30 seconds) {
val sender = TestProbe()
awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].localShutdown.isEmpty)
@ -683,7 +699,7 @@ class NormalStateSpec extends StateSpecBaseClass with StateTestsHelperMethods {
}
}
test("recv CMD_CLOSE (while waiting for a RevokeAndAck)") { case (alice, bob, alice2bob, bob2alice, alice2blockchain, _) =>
test("recv CMD_CLOSE (while waiting for a RevokeAndAck)") { case (alice, bob, alice2bob, bob2alice, alice2blockchain, _, _) =>
within(30 seconds) {
val sender = TestProbe()
val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice)
@ -698,7 +714,7 @@ class NormalStateSpec extends StateSpecBaseClass with StateTestsHelperMethods {
}
}
test("recv Shutdown (no pending htlcs)") { case (alice, _, alice2bob, _, alice2blockchain, _) =>
test("recv Shutdown (no pending htlcs)") { case (alice, _, alice2bob, _, alice2blockchain, _, _) =>
within(30 seconds) {
val sender = TestProbe()
sender.send(alice, Shutdown(0, Script.write(Bob.channelParams.defaultFinalScriptPubKey)))
@ -708,7 +724,7 @@ class NormalStateSpec extends StateSpecBaseClass with StateTestsHelperMethods {
}
}
test("recv Shutdown (with unacked sent htlcs)") { case (alice, bob, alice2bob, bob2alice, alice2blockchain, _) =>
test("recv Shutdown (with unacked sent htlcs)") { case (alice, bob, alice2bob, bob2alice, alice2blockchain, _, _) =>
within(30 seconds) {
val sender = TestProbe()
val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice)
@ -722,7 +738,7 @@ class NormalStateSpec extends StateSpecBaseClass with StateTestsHelperMethods {
}
}
test("recv Shutdown (with unacked received htlcs)") { case (alice, bob, alice2bob, bob2alice, _, bob2blockchain) =>
test("recv Shutdown (with unacked received htlcs)") { case (alice, bob, alice2bob, bob2alice, _, bob2blockchain, _) =>
within(30 seconds) {
val sender = TestProbe()
val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice)
@ -735,7 +751,7 @@ class NormalStateSpec extends StateSpecBaseClass with StateTestsHelperMethods {
}
}
test("recv Shutdown (with invalid final script)") { case (alice, bob, alice2bob, bob2alice, _, bob2blockchain) =>
test("recv Shutdown (with invalid final script)") { case (alice, bob, alice2bob, bob2alice, _, bob2blockchain, _) =>
within(30 seconds) {
val sender = TestProbe()
sender.send(bob, Shutdown(0, BinaryData("00112233445566778899")))
@ -746,7 +762,7 @@ class NormalStateSpec extends StateSpecBaseClass with StateTestsHelperMethods {
}
}
test("recv Shutdown (with signed htlcs)") { case (alice, bob, alice2bob, bob2alice, alice2blockchain, _) =>
test("recv Shutdown (with signed htlcs)") { case (alice, bob, alice2bob, bob2alice, alice2blockchain, _, _) =>
within(30 seconds) {
val sender = TestProbe()
val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice)
@ -760,7 +776,7 @@ class NormalStateSpec extends StateSpecBaseClass with StateTestsHelperMethods {
}
}
test("recv Shutdown (while waiting for a RevokeAndAck)") { case (alice, bob, alice2bob, bob2alice, alice2blockchain, _) =>
test("recv Shutdown (while waiting for a RevokeAndAck)") { case (alice, bob, alice2bob, bob2alice, alice2blockchain, _, _) =>
within(30 seconds) {
val sender = TestProbe()
val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice)
@ -776,7 +792,7 @@ class NormalStateSpec extends StateSpecBaseClass with StateTestsHelperMethods {
}
}
test("recv BITCOIN_FUNDING_SPENT (their commit w/ htlc)") { case (alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain) =>
test("recv BITCOIN_FUNDING_SPENT (their commit w/ htlc)") { case (alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain, _) =>
within(30 seconds) {
val sender = TestProbe()
@ -803,7 +819,7 @@ class NormalStateSpec extends StateSpecBaseClass with StateTestsHelperMethods {
// bob publishes his current commit tx
val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx
assert(bobCommitTx.txOut.size == 6) // two main outputs and 4 pending htlcs
alice ! (BITCOIN_FUNDING_SPENT, bobCommitTx)
alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, bobCommitTx)
alice2blockchain.expectMsgType[WatchConfirmed].txId == bobCommitTx.txid
@ -826,7 +842,7 @@ class NormalStateSpec extends StateSpecBaseClass with StateTestsHelperMethods {
}
}
test("recv BITCOIN_FUNDING_SPENT (revoked commit)") { case (alice, bob, alice2bob, bob2alice, alice2blockchain, _) =>
test("recv BITCOIN_FUNDING_SPENT (revoked commit)") { case (alice, bob, alice2bob, bob2alice, alice2blockchain, _, _) =>
within(30 seconds) {
val sender = TestProbe()
@ -855,7 +871,7 @@ class NormalStateSpec extends StateSpecBaseClass with StateTestsHelperMethods {
// a->b = 10 000
// two main outputs + 4 htlc
assert(revokedTx.txOut.size == 6)
alice ! (BITCOIN_FUNDING_SPENT, revokedTx)
alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, revokedTx)
alice2bob.expectMsgType[Error]
alice2blockchain.expectMsgType[WatchConfirmed]
@ -876,7 +892,7 @@ class NormalStateSpec extends StateSpecBaseClass with StateTestsHelperMethods {
}
}
test("recv Error") { case (alice, bob, alice2bob, bob2alice, alice2blockchain, _) =>
test("recv Error") { case (alice, bob, alice2bob, bob2alice, alice2blockchain, _, _) =>
within(30 seconds) {
val (ra1, htlca1) = addHtlc(250000000, alice, bob, alice2bob, bob2alice)
val (ra2, htlca2) = addHtlc(100000000, alice, bob, alice2bob, bob2alice)

View file

@ -4,10 +4,10 @@ import akka.actor.Props
import akka.testkit.{TestFSMRef, TestProbe}
import fr.acinq.bitcoin.Crypto.Scalar
import fr.acinq.bitcoin.{BinaryData, Crypto, Satoshi, ScriptFlags, Transaction}
import fr.acinq.eclair.TestBitcoinClient
import fr.acinq.eclair.{TestkitBaseClass, TestBitcoinClient}
import fr.acinq.eclair.TestConstants.{Alice, Bob}
import fr.acinq.eclair.blockchain._
import fr.acinq.eclair.channel.states.{StateSpecBaseClass, StateTestsHelperMethods}
import fr.acinq.eclair.channel.states.StateTestsHelperMethods
import fr.acinq.eclair.channel.{Data, State, _}
import fr.acinq.eclair.wire.{CommitSig, Error, RevokeAndAck, Shutdown, UpdateAddHtlc, UpdateFailHtlc, UpdateFulfillHtlc}
import org.junit.runner.RunWith
@ -19,7 +19,7 @@ import scala.concurrent.duration._
* Created by PM on 05/07/2016.
*/
@RunWith(classOf[JUnitRunner])
class ShutdownStateSpec extends StateSpecBaseClass with StateTestsHelperMethods {
class ShutdownTestkit extends TestkitBaseClass with StateTestsHelperMethods {
type FixtureParam = Tuple6[TestFSMRef[State, Data, Channel], TestFSMRef[State, Data, Channel], TestProbe, TestProbe, TestProbe, TestProbe]
@ -29,9 +29,10 @@ class ShutdownStateSpec extends StateSpecBaseClass with StateTestsHelperMethods
val alice2blockchain = TestProbe()
val blockchainA = system.actorOf(Props(new PeerWatcher(new TestBitcoinClient(), 300)))
val bob2blockchain = TestProbe()
val paymentHandler = TestProbe()
val alice: TestFSMRef[State, Data, Channel] = TestFSMRef(new Channel(alice2bob.ref, alice2blockchain.ref, paymentHandler.ref, Alice.channelParams, "0B"))
val bob: TestFSMRef[State, Data, Channel] = TestFSMRef(new Channel(bob2alice.ref, bob2blockchain.ref, paymentHandler.ref, Bob.channelParams, "0A"))
val relayer = TestProbe()
val router = TestProbe()
val alice: TestFSMRef[State, Data, Channel] = TestFSMRef(new Channel(alice2bob.ref, alice2blockchain.ref, router.ref, relayer.ref, Alice.channelParams, Bob.id))
val bob: TestFSMRef[State, Data, Channel] = TestFSMRef(new Channel(bob2alice.ref, bob2blockchain.ref, router.ref, relayer.ref, Bob.channelParams, Alice.id))
within(30 seconds) {
reachNormal(alice, bob, alice2bob, bob2alice, blockchainA, alice2blockchain, bob2blockchain)
val sender = TestProbe()
@ -356,7 +357,7 @@ class ShutdownStateSpec extends StateSpecBaseClass with StateTestsHelperMethods
// bob publishes his current commit tx, which contains two pending htlcs alice->bob
val bobCommitTx = bob.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.localCommit.publishableTxs.commitTx.tx
assert(bobCommitTx.txOut.size == 4) // two main outputs and 2 pending htlcs
alice ! (BITCOIN_FUNDING_SPENT, bobCommitTx)
alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, bobCommitTx)
alice2blockchain.expectMsgType[WatchConfirmed].txId == bobCommitTx.txid
@ -392,7 +393,7 @@ class ShutdownStateSpec extends StateSpecBaseClass with StateTestsHelperMethods
// bob now has a new commitment tx
// bob published the revoked tx
alice ! (BITCOIN_FUNDING_SPENT, revokedTx)
alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, revokedTx)
alice2bob.expectMsgType[Error]
alice2blockchain.expectMsgType[WatchConfirmed]

View file

@ -2,10 +2,10 @@ package fr.acinq.eclair.channel.states.g
import akka.actor.Props
import akka.testkit.{TestFSMRef, TestProbe}
import fr.acinq.eclair.TestBitcoinClient
import fr.acinq.eclair.{TestkitBaseClass, TestBitcoinClient}
import fr.acinq.eclair.TestConstants.{Alice, Bob}
import fr.acinq.eclair.blockchain._
import fr.acinq.eclair.channel.states.{StateSpecBaseClass, StateTestsHelperMethods}
import fr.acinq.eclair.channel.states.StateTestsHelperMethods
import fr.acinq.eclair.channel.{Data, State, _}
import fr.acinq.eclair.wire.{ClosingSigned, Error, Shutdown}
import org.junit.runner.RunWith
@ -17,7 +17,7 @@ import scala.concurrent.duration._
* Created by PM on 05/07/2016.
*/
@RunWith(classOf[JUnitRunner])
class NegotiatingStateSpec extends StateSpecBaseClass with StateTestsHelperMethods {
class NegotiatingTestkit extends TestkitBaseClass with StateTestsHelperMethods {
type FixtureParam = Tuple6[TestFSMRef[State, Data, Channel], TestFSMRef[State, Data, Channel], TestProbe, TestProbe, TestProbe, TestProbe]
@ -27,10 +27,11 @@ class NegotiatingStateSpec extends StateSpecBaseClass with StateTestsHelperMetho
val alice2blockchain = TestProbe()
val blockchainA = system.actorOf(Props(new PeerWatcher(new TestBitcoinClient(), 300)))
val bob2blockchain = TestProbe()
val paymentHandler = TestProbe()
val relayer = TestProbe()
// note that alice.initialFeeRate != bob.initialFeeRate
val alice: TestFSMRef[State, Data, Channel] = TestFSMRef(new Channel(alice2bob.ref, alice2blockchain.ref, paymentHandler.ref, Alice.channelParams, "0B"))
val bob: TestFSMRef[State, Data, Channel] = TestFSMRef(new Channel(bob2alice.ref, bob2blockchain.ref, paymentHandler.ref, Bob.channelParams, "0A"))
val router = TestProbe()
val alice: TestFSMRef[State, Data, Channel] = TestFSMRef(new Channel(alice2bob.ref, alice2blockchain.ref, router.ref, relayer.ref, Alice.channelParams, Bob.id))
val bob: TestFSMRef[State, Data, Channel] = TestFSMRef(new Channel(bob2alice.ref, bob2blockchain.ref, router.ref, relayer.ref, Bob.channelParams, Alice.id))
within(30 seconds) {
reachNormal(alice, bob, alice2bob, bob2alice, blockchainA, alice2blockchain, bob2blockchain)
val sender = TestProbe()
@ -91,7 +92,7 @@ class NegotiatingStateSpec extends StateSpecBaseClass with StateTestsHelperMetho
assert(alice.stateName == NEGOTIATING)
val mutualCloseTx = bob2blockchain.expectMsgType[PublishAsap].tx
bob2blockchain.expectMsgType[WatchConfirmed]
alice ! (BITCOIN_FUNDING_SPENT, mutualCloseTx)
alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, mutualCloseTx)
alice2blockchain.expectNoMsg(1 second)
assert(alice.stateName == NEGOTIATING)
}

View file

@ -3,10 +3,10 @@ package fr.acinq.eclair.channel.states.h
import akka.actor.Props
import akka.testkit.{TestFSMRef, TestProbe}
import fr.acinq.bitcoin.Transaction
import fr.acinq.eclair.TestBitcoinClient
import fr.acinq.eclair.{TestkitBaseClass, TestBitcoinClient}
import fr.acinq.eclair.TestConstants.{Alice, Bob}
import fr.acinq.eclair.blockchain._
import fr.acinq.eclair.channel.states.{StateSpecBaseClass, StateTestsHelperMethods}
import fr.acinq.eclair.channel.states.StateTestsHelperMethods
import fr.acinq.eclair.channel.{Data, State, _}
import fr.acinq.eclair.wire._
import org.junit.runner.RunWith
@ -18,7 +18,7 @@ import scala.concurrent.duration._
* Created by PM on 05/07/2016.
*/
@RunWith(classOf[JUnitRunner])
class ClosingStateSpec extends StateSpecBaseClass with StateTestsHelperMethods {
class ClosingTestkit extends TestkitBaseClass with StateTestsHelperMethods {
type FixtureParam = Tuple7[TestFSMRef[State, Data, Channel], TestFSMRef[State, Data, Channel], TestProbe, TestProbe, TestProbe, TestProbe, List[Transaction]]
@ -28,9 +28,10 @@ class ClosingStateSpec extends StateSpecBaseClass with StateTestsHelperMethods {
val alice2blockchain = TestProbe()
val blockchainA = system.actorOf(Props(new PeerWatcher(new TestBitcoinClient(), 300)))
val bob2blockchain = TestProbe()
val paymentHandler = TestProbe()
val alice: TestFSMRef[State, Data, Channel] = TestFSMRef(new Channel(alice2bob.ref, alice2blockchain.ref, paymentHandler.ref, Alice.channelParams, "0B"))
val bob: TestFSMRef[State, Data, Channel] = TestFSMRef(new Channel(bob2alice.ref, bob2blockchain.ref, paymentHandler.ref, Bob.channelParams, "0A"))
val relayer = TestProbe()
val router = TestProbe()
val alice: TestFSMRef[State, Data, Channel] = TestFSMRef(new Channel(alice2bob.ref, alice2blockchain.ref, router.ref, relayer.ref, Alice.channelParams, Bob.id))
val bob: TestFSMRef[State, Data, Channel] = TestFSMRef(new Channel(bob2alice.ref, bob2blockchain.ref, router.ref, relayer.ref, Bob.channelParams, Alice.id))
within(30 seconds) {
reachNormal(alice, bob, alice2bob, bob2alice, blockchainA, alice2blockchain, bob2blockchain)
@ -96,7 +97,7 @@ class ClosingStateSpec extends StateSpecBaseClass with StateTestsHelperMethods {
mutualClose(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain)
// actual test starts here
alice ! BITCOIN_CLOSE_DONE
alice ! WatchEventConfirmed(BITCOIN_CLOSE_DONE, 0, 0)
awaitCond(alice.stateName == CLOSED)
}
}
@ -114,7 +115,7 @@ class ClosingStateSpec extends StateSpecBaseClass with StateTestsHelperMethods {
// actual test starts here
// we are notified afterwards from our watcher about the tx that we just published
alice ! (BITCOIN_FUNDING_SPENT, aliceCommitTx)
alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, aliceCommitTx)
assert(alice.stateData == initialState) // this was a no-op
}
}
@ -130,7 +131,7 @@ class ClosingStateSpec extends StateSpecBaseClass with StateTestsHelperMethods {
assert(alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.isDefined)
// actual test starts here
alice ! BITCOIN_SPEND_OURS_DONE
alice ! WatchEventConfirmed(BITCOIN_SPEND_OURS_DONE, 0, 0)
awaitCond(alice.stateName == CLOSED)
}
}
@ -142,7 +143,7 @@ class ClosingStateSpec extends StateSpecBaseClass with StateTestsHelperMethods {
// bob publishes his last current commit tx, the one it had when entering NEGOTIATING state
val bobCommitTx = bobCommitTxes.last
assert(bobCommitTx.txOut.size == 2) // two main outputs
alice ! (BITCOIN_FUNDING_SPENT, bobCommitTx)
alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, bobCommitTx)
alice2blockchain.expectMsgType[WatchConfirmed].txId == bobCommitTx.txid
@ -158,14 +159,14 @@ class ClosingStateSpec extends StateSpecBaseClass with StateTestsHelperMethods {
// bob publishes his last current commit tx, the one it had when entering NEGOTIATING state
val bobCommitTx = bobCommitTxes.last
assert(bobCommitTx.txOut.size == 2) // two main outputs
alice ! (BITCOIN_FUNDING_SPENT, bobCommitTx)
alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, bobCommitTx)
alice2blockchain.expectMsgType[WatchConfirmed].txId == bobCommitTx.txid
awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].remoteCommitPublished.isDefined)
assert(alice.stateData.asInstanceOf[DATA_CLOSING].copy(remoteCommitPublished = None) == initialState)
// actual test starts here
alice ! BITCOIN_SPEND_THEIRS_DONE
alice ! WatchEventConfirmed(BITCOIN_SPEND_THEIRS_DONE, 0, 0)
awaitCond(alice.stateName == CLOSED)
}
}
@ -176,7 +177,7 @@ class ClosingStateSpec extends StateSpecBaseClass with StateTestsHelperMethods {
val initialState = alice.stateData.asInstanceOf[DATA_CLOSING]
// bob publishes one of his revoked txes
val bobRevokedTx = bobCommitTxes.head
alice ! (BITCOIN_FUNDING_SPENT, bobRevokedTx)
alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, bobRevokedTx)
// alice publishes and watches the punishment tx
alice2blockchain.expectMsgType[WatchConfirmed]
@ -192,7 +193,7 @@ class ClosingStateSpec extends StateSpecBaseClass with StateTestsHelperMethods {
mutualClose(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain)
// bob publishes multiple revoked txes (last one isn't revoked)
for (bobRevokedTx <- bobCommitTxes.dropRight(1)) {
alice ! (BITCOIN_FUNDING_SPENT, bobRevokedTx)
alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, bobRevokedTx)
// alice publishes and watches the punishment tx
alice2blockchain.expectMsgType[WatchConfirmed]
alice2blockchain.expectMsgType[PublishAsap]
@ -208,7 +209,7 @@ class ClosingStateSpec extends StateSpecBaseClass with StateTestsHelperMethods {
val initialState = alice.stateData.asInstanceOf[DATA_CLOSING]
// bob publishes one of his revoked txes
val bobRevokedTx = bobCommitTxes.head
alice ! (BITCOIN_FUNDING_SPENT, bobRevokedTx)
alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, bobRevokedTx)
// alice publishes and watches the punishment tx
alice2blockchain.expectMsgType[WatchConfirmed]
alice2blockchain.expectMsgType[PublishAsap]
@ -216,7 +217,7 @@ class ClosingStateSpec extends StateSpecBaseClass with StateTestsHelperMethods {
// awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING] == initialState.copy(revokedCommitPublished = Seq(RevokedCommitPublished(bobRevokedTx))))
// actual test starts here
alice ! BITCOIN_PUNISHMENT_DONE
alice ! WatchEventConfirmed(BITCOIN_PUNISHMENT_DONE, 0, 0)
awaitCond(alice.stateName == CLOSED)
}
}

View file

@ -1,6 +1,6 @@
package fr.acinq.eclair.crypto
import fr.acinq.bitcoin.Crypto.{Point, PrivateKey, PublicKey, Scalar}
import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey}
import fr.acinq.bitcoin.{BinaryData, Crypto}
import org.junit.runner.RunWith
import org.scalatest.FunSuite

View file

@ -32,8 +32,8 @@ class TransportHandlerSpec extends TestKit(ActorSystem("test")) with FunSuiteLik
val pipe = system.actorOf(Props[MyPipe])
val probe1 = TestProbe()
val probe2 = TestProbe()
val initiator = TestFSMRef(new TransportHandler(Initiator.s, Some(Responder.s.pub), pipe, true, (conn, _) => probe1.ref, TransportHandler.Noop))
val responder = TestFSMRef(new TransportHandler(Responder.s, None, pipe, false, (conn, _) => probe2.ref, TransportHandler.Noop))
val initiator = TestFSMRef(new TransportHandler(Initiator.s, Some(Responder.s.pub), pipe, true, (conn, _, _) => probe1.ref, TransportHandler.Noop))
val responder = TestFSMRef(new TransportHandler(Responder.s, None, pipe, false, (conn, _, _) => probe2.ref, TransportHandler.Noop))
pipe ! (initiator, responder)
awaitCond(initiator.stateName == TransportHandler.WaitingForCyphertext)
@ -68,8 +68,8 @@ class TransportHandlerSpec extends TestKit(ActorSystem("test")) with FunSuiteLik
val pipe = system.actorOf(Props[MyPipe])
val probe1 = TestProbe()
val probe2 = TestProbe()
val initiator = TestFSMRef(new TransportHandler(Initiator.s, Some(Responder.s.pub), pipe, true, (conn, _) => probe1.ref, mySerializer))
val responder = TestFSMRef(new TransportHandler(Responder.s, None, pipe, false, (conn, _) => probe2.ref, mySerializer))
val initiator = TestFSMRef(new TransportHandler(Initiator.s, Some(Responder.s.pub), pipe, true, (conn, _, _) => probe1.ref, mySerializer))
val responder = TestFSMRef(new TransportHandler(Responder.s, None, pipe, false, (conn, _, _) => probe2.ref, mySerializer))
pipe ! (initiator, responder)
awaitCond(initiator.stateName == TransportHandler.WaitingForCyphertext)
@ -92,8 +92,8 @@ class TransportHandlerSpec extends TestKit(ActorSystem("test")) with FunSuiteLik
val pipe = system.actorOf(Props[MyPipeSplitter])
val probe1 = TestProbe()
val probe2 = TestProbe()
val initiator = TestFSMRef(new TransportHandler(Initiator.s, Some(Responder.s.pub), pipe, true, (conn, _) => probe1.ref, TransportHandler.Noop))
val responder = TestFSMRef(new TransportHandler(Responder.s, None, pipe, false, (conn, _) => probe2.ref, TransportHandler.Noop))
val initiator = TestFSMRef(new TransportHandler(Initiator.s, Some(Responder.s.pub), pipe, true, (conn, _, _) => probe1.ref, TransportHandler.Noop))
val responder = TestFSMRef(new TransportHandler(Responder.s, None, pipe, false, (conn, _, _) => probe2.ref, TransportHandler.Noop))
pipe ! (initiator, responder)
awaitCond(initiator.stateName == TransportHandler.WaitingForCyphertext)
@ -117,8 +117,8 @@ class TransportHandlerSpec extends TestKit(ActorSystem("test")) with FunSuiteLik
val probe1 = TestProbe()
val probe2 = TestProbe()
val supervisor = TestActorRef(Props(new MySupervisor()))
val initiator = TestFSMRef(new TransportHandler(Initiator.s, Some(Initiator.s.pub), pipe, true, (conn, _) => probe1.ref, TransportHandler.Noop), supervisor)
val responder = TestFSMRef(new TransportHandler(Responder.s, None, pipe, false, (conn, _) => probe2.ref, TransportHandler.Noop), supervisor)
val initiator = TestFSMRef(new TransportHandler(Initiator.s, Some(Initiator.s.pub), pipe, true, (conn, _, _) => probe1.ref, TransportHandler.Noop), supervisor)
val responder = TestFSMRef(new TransportHandler(Responder.s, None, pipe, false, (conn, _, _) => probe2.ref, TransportHandler.Noop), supervisor)
probe1.watch(initiator)
pipe ! (initiator, responder)

View file

@ -90,7 +90,7 @@ class InteroperabilitySpec extends FunSuite with BeforeAndAfterAll {
super.afterAll()
}
def sendCommand(channelId: String, cmd: Command): Future[String] = {
def sendCommand(channelId: Long, cmd: Command): Future[String] = {
system.actorSelection(Register.actorPathToChannelId(system, channelId)).resolveOne().map(actor => {
actor ! cmd
"ok"
@ -145,7 +145,7 @@ class InteroperabilitySpec extends FunSuite with BeforeAndAfterAll {
def now: Int = (System.currentTimeMillis() / 1000).toInt
val future = for {
channelId <- listChannels.map(_.head).map(_.channelId.toString)
channelId <- listChannels.map(_.head).map(_.channelId)
peer = lncli.getPeers.head
// lightningd sends us a htlc
blockcount <- btccli.getBlockCount

View file

@ -4,11 +4,11 @@ import java.io.File
import java.util.concurrent.{CountDownLatch, TimeUnit}
import akka.actor.{ActorRef, ActorSystem, Props}
import akka.testkit.{TestFSMRef, TestKit}
import akka.testkit.{TestFSMRef, TestKit, TestProbe}
import fr.acinq.eclair.TestConstants.{Alice, Bob}
import fr.acinq.eclair.blockchain.PeerWatcher
import fr.acinq.eclair.channel._
import fr.acinq.eclair.payment.NoopPaymentHandler
import fr.acinq.eclair.payment.{NoopPaymentHandler, Relayer}
import fr.acinq.eclair.{TestBitcoinClient, TestConstants}
import org.junit.runner.RunWith
import org.scalatest.junit.JUnitRunner
@ -31,8 +31,11 @@ class RustyTestsSpec extends TestKit(ActorSystem("test")) with Matchers with fix
val blockchainA = system.actorOf(Props(new PeerWatcher(new TestBitcoinClient(), 300)))
val blockchainB = system.actorOf(Props(new PeerWatcher(new TestBitcoinClient(), 300)))
val paymentHandler = system.actorOf(Props(new NoopPaymentHandler()))
val alice: TestFSMRef[State, Data, Channel] = TestFSMRef(new Channel(pipe, blockchainA, paymentHandler, Alice.channelParams, "0B"))
val bob: TestFSMRef[State, Data, Channel] = TestFSMRef(new Channel(pipe, blockchainB, paymentHandler, Bob.channelParams, "0A"))
// we just bypass the relayer for this test
val relayer = paymentHandler
val router = TestProbe()
val alice: TestFSMRef[State, Data, Channel] = TestFSMRef(new Channel(pipe, blockchainA, router.ref, relayer, Alice.channelParams, Bob.id))
val bob: TestFSMRef[State, Data, Channel] = TestFSMRef(new Channel(pipe, blockchainB, router.ref, relayer, Bob.channelParams, Alice.id))
alice ! INPUT_INIT_FUNDER(TestConstants.fundingSatoshis, 0)
bob ! INPUT_INIT_FUNDEE()
pipe ! (alice, bob)

View file

@ -0,0 +1,169 @@
package fr.acinq.eclair.payment
import fr.acinq.bitcoin.BinaryData
import fr.acinq.eclair.crypto.Sphinx
import fr.acinq.eclair.payment.PaymentLifecycle._
import fr.acinq.eclair.randomKey
import fr.acinq.eclair.router.Hop
import fr.acinq.eclair.wire.{ChannelUpdate, Codecs, PerHopPayload}
import org.junit.runner.RunWith
import org.scalatest.FunSuite
import org.scalatest.junit.JUnitRunner
import scodec.bits.BitVector
/**
* Created by PM on 31/05/2016.
*/
@RunWith(classOf[JUnitRunner])
class HtlcGenerationSpec extends FunSuite {
test("compute fees") {
val feeBaseMsat = 150000L
val feeProportionalMillionth = 4L
val htlcAmountMsat = 42000000
// spec: fee-base-msat + htlc-amount-msat * fee-proportional-millionths / 1000000
val ref = feeBaseMsat + htlcAmountMsat * feeProportionalMillionth / 1000000
val fee = nodeFee(feeBaseMsat, feeProportionalMillionth, htlcAmountMsat)
assert(ref === fee)
}
import HtlcGenerationSpec._
test("compute route with fees and expiry delta") {
val (firstAmountMsat, firstExpiry, payloads) = buildRoute(finalAmountMsat, hops.drop(1), currentBlockCount)
assert(firstAmountMsat === amount_ab)
assert(firstExpiry === expiry_ab)
assert(payloads ===
PerHopPayload(amount_bc, expiry_bc) ::
PerHopPayload(amount_cd, expiry_cd) ::
PerHopPayload(amount_de, expiry_de) :: Nil)
}
test("build onion") {
val (_, _, payloads) = buildRoute(finalAmountMsat, hops.drop(1), currentBlockCount)
val nodes = hops.map(_.nextNodeId)
val packet_b = buildOnion(nodes, payloads, paymentHash)
assert(packet_b.size === 1254)
// let's peel the onion
val (bin_b, address_c, packet_c) = Sphinx.parsePacket(priv_b, paymentHash, packet_b)
val payload_b = Codecs.perHopPayloadCodec.decode(BitVector(bin_b.data)).toOption.get.value
assert(address_c === c.hash160)
assert(packet_c.size === 1254)
assert(payload_b.amt_to_forward === amount_bc)
assert(payload_b.outgoing_cltv_value === expiry_bc)
val (bin_c, address_d, packet_d) = Sphinx.parsePacket(priv_c, paymentHash, packet_c)
val payload_c = Codecs.perHopPayloadCodec.decode(BitVector(bin_c.data)).toOption.get.value
assert(address_d === d.hash160)
assert(packet_d.size === 1254)
assert(payload_c.amt_to_forward === amount_cd)
assert(payload_c.outgoing_cltv_value === expiry_cd)
val (bin_d, address_e, packet_e) = Sphinx.parsePacket(priv_d, paymentHash, packet_d)
val payload_d = Codecs.perHopPayloadCodec.decode(BitVector(bin_d.data)).toOption.get.value
assert(address_e === e.hash160)
assert(packet_e.size === 1254)
assert(payload_d.amt_to_forward === amount_de)
assert(payload_d.outgoing_cltv_value === expiry_de)
val (bin_e, address_null, packet_random) = Sphinx.parsePacket(priv_e, paymentHash, packet_e)
assert(bin_e === BinaryData("00" * 20))
assert(address_null === BinaryData("00" * 20))
assert(packet_random.size === 1254)
}
test("build a command including the onion") {
val add = buildCommand(finalAmountMsat, paymentHash, hops, currentBlockCount)
assert(add.amountMsat > finalAmountMsat)
assert(add.expiry === currentBlockCount + defaultHtlcExpiry + channelUpdate_de.cltvExpiryDelta + channelUpdate_cd.cltvExpiryDelta + channelUpdate_bc.cltvExpiryDelta)
assert(add.paymentHash === paymentHash)
assert(add.onion.size === 1254)
// let's peel the onion
val (bin_b, address_c, packet_c) = Sphinx.parsePacket(priv_b, paymentHash, add.onion)
val payload_b = Codecs.perHopPayloadCodec.decode(BitVector(bin_b.data)).toOption.get.value
assert(address_c === c.hash160)
assert(packet_c.size === 1254)
assert(payload_b.amt_to_forward === amount_bc)
assert(payload_b.outgoing_cltv_value === expiry_bc)
val (bin_c, address_d, packet_d) = Sphinx.parsePacket(priv_c, paymentHash, packet_c)
val payload_c = Codecs.perHopPayloadCodec.decode(BitVector(bin_c.data)).toOption.get.value
assert(address_d === d.hash160)
assert(packet_d.size === 1254)
assert(payload_c.amt_to_forward === amount_cd)
assert(payload_c.outgoing_cltv_value === expiry_cd)
val (bin_d, address_e, packet_e) = Sphinx.parsePacket(priv_d, paymentHash, packet_d)
val payload_d = Codecs.perHopPayloadCodec.decode(BitVector(bin_d.data)).toOption.get.value
assert(address_e === e.hash160)
assert(packet_e.size === 1254)
assert(payload_d.amt_to_forward === amount_de)
assert(payload_d.outgoing_cltv_value === expiry_de)
val (bin_e, address_null, packet_random) = Sphinx.parsePacket(priv_e, paymentHash, packet_e)
assert(bin_e === BinaryData("00" * 20))
assert(address_null === BinaryData("00" * 20))
assert(packet_random.size === 1254)
}
test("build a command with no hops") {
val add = buildCommand(finalAmountMsat, paymentHash, hops.take(1), currentBlockCount)
assert(add.amountMsat === finalAmountMsat)
assert(add.expiry === currentBlockCount + defaultHtlcExpiry)
assert(add.paymentHash === paymentHash)
assert(add.onion.size === 1254)
// let's peel the onion
val (bin_b, address_null, packet_random) = Sphinx.parsePacket(priv_b, paymentHash, add.onion)
assert(bin_b === BinaryData("00" * 20))
assert(address_null === BinaryData("00" * 20))
assert(packet_random.size === 1254)
}
}
object HtlcGenerationSpec {
val (priv_a, priv_b, priv_c, priv_d, priv_e) = (randomKey, randomKey, randomKey, randomKey, randomKey)
val (a, b, c, d, e) = (priv_a.publicKey, priv_b.publicKey, priv_c.publicKey, priv_d.publicKey, priv_e.publicKey)
val defaultChannelUpdate = ChannelUpdate("00" * 64, 0, 0, "0000", 0, 0, 0, 0)
val channelUpdate_ab = defaultChannelUpdate.copy(cltvExpiryDelta = 4, feeBaseMsat = 642000, feeProportionalMillionths = 7)
val channelUpdate_bc = defaultChannelUpdate.copy(cltvExpiryDelta = 5, feeBaseMsat = 153000, feeProportionalMillionths = 4)
val channelUpdate_cd = defaultChannelUpdate.copy(cltvExpiryDelta = 10, feeBaseMsat = 60000, feeProportionalMillionths = 1)
val channelUpdate_de = defaultChannelUpdate.copy(cltvExpiryDelta = 7, feeBaseMsat = 766000, feeProportionalMillionths = 10)
// simple route a -> b -> c -> d -> e
val hops =
Hop(a, b, channelUpdate_ab) ::
Hop(b, c, channelUpdate_bc) ::
Hop(c, d, channelUpdate_cd) ::
Hop(d, e, channelUpdate_de) :: Nil
val finalAmountMsat = 42000000L
val paymentHash = BinaryData("42" * 32)
val currentBlockCount = 420000
val expiry_de = currentBlockCount + defaultHtlcExpiry
val amount_de = finalAmountMsat
val fee_d = nodeFee(channelUpdate_de.feeBaseMsat, channelUpdate_de.feeProportionalMillionths, amount_de)
val expiry_cd = expiry_de + channelUpdate_de.cltvExpiryDelta
val amount_cd = amount_de + fee_d
val fee_c = nodeFee(channelUpdate_cd.feeBaseMsat, channelUpdate_cd.feeProportionalMillionths, amount_cd)
val expiry_bc = expiry_cd + channelUpdate_cd.cltvExpiryDelta
val amount_bc = amount_cd + fee_c
val fee_b = nodeFee(channelUpdate_bc.feeBaseMsat, channelUpdate_bc.feeProportionalMillionths, amount_bc)
val expiry_ab = expiry_bc + channelUpdate_bc.cltvExpiryDelta
val amount_ab = amount_bc + fee_b
}

View file

@ -0,0 +1,72 @@
package fr.acinq.eclair.payment
import akka.actor.FSM.{CurrentState, SubscribeTransitionCallBack, Transition}
import akka.actor.Status.Failure
import akka.testkit.TestProbe
import fr.acinq.eclair.router.BaseRouterSpec
import org.junit.runner.RunWith
import org.scalatest.junit.JUnitRunner
/**
* Created by PM on 29/08/2016.
*/
@RunWith(classOf[JUnitRunner])
class PaymentLifecycleSpec extends BaseRouterSpec {
val initialBlockCount = 420000
test("payment failed (route not found)") { case (router, _) =>
val paymentFSM = system.actorOf(PaymentLifecycle.props(a, router, initialBlockCount))
val monitor = TestProbe()
val sender = TestProbe()
paymentFSM ! SubscribeTransitionCallBack(monitor.ref)
val CurrentState(_, WAITING_FOR_REQUEST) = monitor.expectMsgClass(classOf[CurrentState[_]])
val request = CreatePayment(142000, "42" * 32, f)
sender.send(paymentFSM, request)
val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]])
val res = sender.expectMsgType[Failure]
assert(res.cause.getMessage === "route not found")
}
test("payment failed (htlc failed)") { case (router, _) =>
val paymentFSM = system.actorOf(PaymentLifecycle.props(a, router, initialBlockCount))
val monitor = TestProbe()
val sender = TestProbe()
paymentFSM ! SubscribeTransitionCallBack(monitor.ref)
val CurrentState(_, WAITING_FOR_REQUEST) = monitor.expectMsgClass(classOf[CurrentState[_]])
val request = CreatePayment(142000, "42" * 32, d)
sender.send(paymentFSM, request)
val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]])
val Transition(_, WAITING_FOR_ROUTE, WAITING_FOR_PAYMENT_COMPLETE) = monitor.expectMsgClass(classOf[Transition[_]])
sender.send(paymentFSM, PaymentFailed(null, request.paymentHash, "some reason"))
val res = sender.expectMsgType[Failure]
assert(res.cause.getMessage === "some reason")
}
test("payment succeeded") { case (router, _) =>
val paymentFSM = system.actorOf(PaymentLifecycle.props(a, router, initialBlockCount))
val monitor = TestProbe()
val sender = TestProbe()
paymentFSM ! SubscribeTransitionCallBack(monitor.ref)
val CurrentState(_, WAITING_FOR_REQUEST) = monitor.expectMsgClass(classOf[CurrentState[_]])
val request = CreatePayment(142000, "42" * 32, d)
sender.send(paymentFSM, request)
val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]])
val Transition(_, WAITING_FOR_ROUTE, WAITING_FOR_PAYMENT_COMPLETE) = monitor.expectMsgClass(classOf[Transition[_]])
sender.send(paymentFSM, PaymentSent(null, request.paymentHash))
val res = sender.expectMsgType[String]
assert(res === "sent")
}
}

View file

@ -0,0 +1,265 @@
package fr.acinq.eclair.payment
import akka.actor.ActorRef
import akka.testkit.TestProbe
import fr.acinq.bitcoin.Crypto.PublicKey
import fr.acinq.eclair.TestkitBaseClass
import fr.acinq.eclair.channel._
import fr.acinq.eclair.payment.PaymentLifecycle.buildCommand
import fr.acinq.eclair.wire.{UpdateAddHtlc, UpdateFailHtlc, UpdateFulfillHtlc}
import org.junit.runner.RunWith
import org.scalatest.junit.JUnitRunner
import scala.concurrent.duration._
/**
* Created by PM on 29/08/2016.
*/
@RunWith(classOf[JUnitRunner])
class RelayerSpec extends TestkitBaseClass {
// let's reuse the existing test data
import HtlcGenerationSpec._
type FixtureParam = Tuple3[ActorRef, TestProbe, TestProbe]
override def withFixture(test: OneArgTest) = {
within(30 seconds) {
val paymentHandler = TestProbe()
val paymentListener = TestProbe()
system.eventStream.subscribe(paymentListener.ref, classOf[PaymentEvent])
// we are node B in the route A -> B -> C -> ....
val relayer = system.actorOf(Relayer.props(priv_b, paymentHandler.ref))
test((relayer, paymentHandler, paymentListener))
}
}
// node c is the next node in the route
val nodeId_a = PublicKey(a)
val nodeId_c = PublicKey(c)
val channelId_ab = 981408633
val channelId_bc = 237534
test("add a channel") { case (relayer, _, _) =>
val sender = TestProbe()
val channel_bc = TestProbe()
sender.send(relayer, ChannelChangedState(channel_bc.ref, null, nodeId_c, WAIT_FOR_FUNDING_LOCKED, NORMAL, DATA_NORMAL(null, Commitments(null, null, null, null, null, null, 0, null, null, null, channelId_bc), null)))
sender.send(relayer, 'upstreams)
val upstreams = sender.expectMsgType[Set[OutgoingChannel]]
assert(upstreams === Set(OutgoingChannel(channelId_bc, channel_bc.ref, nodeId_c.hash160)))
}
test("remove a channel (mutual close)") { case (relayer, _, paymentListener) =>
val sender = TestProbe()
val channel_bc = TestProbe()
sender.send(relayer, ChannelChangedState(channel_bc.ref, null, nodeId_c, WAIT_FOR_FUNDING_LOCKED, NORMAL, DATA_NORMAL(null, Commitments(null, null, null, null, null, null, 0, null, null, null, channelId_bc), null)))
sender.send(relayer, 'upstreams)
val upstreams1 = sender.expectMsgType[Set[OutgoingChannel]]
assert(upstreams1 === Set(OutgoingChannel(channelId_bc, channel_bc.ref, nodeId_c.hash160)))
sender.send(relayer, ChannelChangedState(channel_bc.ref, null, nodeId_c, SHUTDOWN, NEGOTIATING, DATA_NEGOTIATING(null, Commitments(null, null, null, null, null, null, 0, null, null, null, channelId_bc), null, null, null)))
sender.send(relayer, 'upstreams)
val upstreams2 = sender.expectMsgType[Set[OutgoingChannel]]
assert(upstreams2 === Set.empty)
}
test("remove a channel (unilateral close)") { case (relayer, _, paymentListener) =>
val sender = TestProbe()
val channel_bc = TestProbe()
sender.send(relayer, ChannelChangedState(channel_bc.ref, null, nodeId_c, WAIT_FOR_FUNDING_LOCKED, NORMAL, DATA_NORMAL(null, Commitments(null, null, null, null, null, null, 0, null, null, null, channelId_bc), null)))
sender.send(relayer, 'upstreams)
val upstreams1 = sender.expectMsgType[Set[OutgoingChannel]]
assert(upstreams1 === Set(OutgoingChannel(channelId_bc, channel_bc.ref, nodeId_c.hash160)))
sender.send(relayer, ChannelChangedState(channel_bc.ref, null, nodeId_c, NORMAL, CLOSING, DATA_CLOSING(Commitments(null, null, null, null, null, null, 0, null, null, null, channelId_bc), None, Some(null), None, None, Nil)))
sender.send(relayer, 'upstreams)
val upstreams2 = sender.expectMsgType[Set[OutgoingChannel]]
assert(upstreams2 === Set.empty)
}
test("send an event when we receive a payment") { case (relayer, paymentHandler, paymentListener) =>
val sender = TestProbe()
val add_ab = {
val cmd = buildCommand(finalAmountMsat, paymentHash, hops.take(1), currentBlockCount)
// and then manually build an htlc
UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amountMsat, cmd.expiry, cmd.paymentHash, cmd.onion)
}
sender.send(relayer, add_ab)
val add1 = paymentHandler.expectMsgType[UpdateAddHtlc]
paymentListener.expectMsgType[PaymentReceived]
assert(add1 === add_ab)
}
test("relay an htlc-add") { case (relayer, paymentHandler, paymentListener) =>
val sender = TestProbe()
val channel_bc = TestProbe()
val add_ab = {
val cmd = buildCommand(finalAmountMsat, paymentHash, hops, currentBlockCount)
// and then manually build an htlc
UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amountMsat, cmd.expiry, cmd.paymentHash, cmd.onion)
}
sender.send(relayer, ChannelChangedState(channel_bc.ref, null, nodeId_c, WAIT_FOR_FUNDING_LOCKED, NORMAL, DATA_NORMAL(null, Commitments(null, null, null, null, null, null, 0, null, null, null, channelId_bc), null)))
sender.send(relayer, add_ab)
sender.expectNoMsg(1 second)
val cmd_bc = channel_bc.expectMsgType[CMD_ADD_HTLC]
paymentHandler.expectNoMsg(1 second)
assert(cmd_bc.origin === Some(add_ab))
}
test("fail to relay an htlc-add when there is no available upstream channel") { case (relayer, paymentHandler, _) =>
val sender = TestProbe()
val channel_bc = TestProbe()
val add_ab = {
val cmd = buildCommand(finalAmountMsat, paymentHash, hops, currentBlockCount)
// and then manually build an htlc
UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amountMsat, cmd.expiry, cmd.paymentHash, cmd.onion)
}
sender.send(relayer, add_ab)
val fail = sender.expectMsgType[CMD_FAIL_HTLC]
channel_bc.expectNoMsg(1 second)
paymentHandler.expectNoMsg(1 second)
assert(fail.id === add_ab.id)
}
test("fail to relay an htlc-add when the onion is malformed") { case (relayer, paymentHandler, _) =>
// TODO: we should use the new update_fail_malformed_htlc message (see BOLT 2)
val sender = TestProbe()
val channel_bc = TestProbe()
val add_ab = {
val cmd = buildCommand(finalAmountMsat, paymentHash, hops, currentBlockCount)
// and then manually build an htlc
UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amountMsat, cmd.expiry, cmd.paymentHash, "00" * 1254)
}
sender.send(relayer, add_ab)
val fail = sender.expectMsgType[CMD_FAIL_HTLC]
channel_bc.expectNoMsg(1 second)
paymentHandler.expectNoMsg(1 second)
assert(fail.id === add_ab.id)
}
test("relay an htlc-fulfill") { case (relayer, paymentHandler, paymentListener) =>
val sender = TestProbe()
val channel_ab = TestProbe()
val channel_bc = TestProbe()
val add_ab = {
val cmd = buildCommand(finalAmountMsat, paymentHash, hops, currentBlockCount)
// and then manually build an htlc
UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amountMsat, cmd.expiry, cmd.paymentHash, cmd.onion)
}
sender.send(relayer, ChannelChangedState(channel_ab.ref, null, nodeId_a, WAIT_FOR_FUNDING_LOCKED, NORMAL, DATA_NORMAL(null, Commitments(null, null, null, null, null, null, 0, null, null, null, channelId_ab), null)))
sender.send(relayer, ChannelChangedState(channel_bc.ref, null, nodeId_c, WAIT_FOR_FUNDING_LOCKED, NORMAL, DATA_NORMAL(null, Commitments(null, null, null, null, null, null, 0, null, null, null, channelId_bc), null)))
sender.send(relayer, add_ab)
val cmd_bc = channel_bc.expectMsgType[CMD_ADD_HTLC]
val add_bc = UpdateAddHtlc(channelId = channelId_bc, id = 987451, amountMsat = cmd_bc.amountMsat, expiry = cmd_bc.expiry, paymentHash = cmd_bc.paymentHash, onionRoutingPacket = cmd_bc.onion)
sender.send(relayer, Binding(add_ab, add_bc))
// preimage is wrong, does not matter here
val fulfill_cb = UpdateFulfillHtlc(channelId = add_bc.channelId, id = add_bc.id, paymentPreimage = "00" * 32)
sender.send(relayer, (add_bc, fulfill_cb))
channel_ab.expectMsg(CMD_SIGN)
val fulfill_ba = channel_ab.expectMsgType[CMD_FULFILL_HTLC]
channel_ab.expectMsg(CMD_SIGN)
paymentListener.expectNoMsg(1 second)
assert(fulfill_ba.id === add_ab.id)
}
test("send an event when we receive an htlc-fulfill and we were the initiator") { case (relayer, paymentHandler, paymentListener) =>
val sender = TestProbe()
val channel_ab = TestProbe()
val channel_bc = TestProbe()
// note we simulate this by not having a binding for this channel
val add_ab = {
val cmd = buildCommand(finalAmountMsat, paymentHash, hops.take(1), currentBlockCount)
// and then manually build an htlc
UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amountMsat, cmd.expiry, cmd.paymentHash, cmd.onion)
}
// preimage is wrong, does not matter here
val fulfill_cb = UpdateFulfillHtlc(channelId = add_ab.channelId, id = add_ab.id, paymentPreimage = "00" * 32)
sender.send(relayer, (add_ab, fulfill_cb))
channel_ab.expectNoMsg(1 second)
paymentListener.expectMsgType[PaymentSent]
}
test("relay an htlc-fail") { case (relayer, paymentHandler, paymentListener) =>
val sender = TestProbe()
val channel_ab = TestProbe()
val channel_bc = TestProbe()
val add_ab = {
val cmd = buildCommand(finalAmountMsat, paymentHash, hops, currentBlockCount)
// and then manually build an htlc
UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amountMsat, cmd.expiry, cmd.paymentHash, cmd.onion)
}
sender.send(relayer, ChannelChangedState(channel_ab.ref, null, nodeId_a, WAIT_FOR_FUNDING_LOCKED, NORMAL, DATA_NORMAL(null, Commitments(null, null, null, null, null, null, 0, null, null, null, channelId_ab), null)))
sender.send(relayer, ChannelChangedState(channel_bc.ref, null, nodeId_c, WAIT_FOR_FUNDING_LOCKED, NORMAL, DATA_NORMAL(null, Commitments(null, null, null, null, null, null, 0, null, null, null, channelId_bc), null)))
sender.send(relayer, add_ab)
val cmd_bc = channel_bc.expectMsgType[CMD_ADD_HTLC]
val add_bc = UpdateAddHtlc(channelId = channelId_bc, id = 987451, amountMsat = cmd_bc.amountMsat, expiry = cmd_bc.expiry, paymentHash = cmd_bc.paymentHash, onionRoutingPacket = cmd_bc.onion)
sender.send(relayer, Binding(add_ab, add_bc))
val fail_cb = UpdateFailHtlc(channelId = add_bc.channelId, id = add_bc.id, reason = "some reason".getBytes())
sender.send(relayer, (add_bc, fail_cb))
channel_ab.expectMsg(CMD_SIGN)
val fulfill_ba = channel_ab.expectMsgType[CMD_FAIL_HTLC]
channel_ab.expectMsg(CMD_SIGN)
paymentListener.expectNoMsg(1 second)
assert(fulfill_ba.id === add_ab.id)
}
test("send an event when we receive an htlc-fail and we were the initiator") { case (relayer, paymentHandler, paymentListener) =>
val sender = TestProbe()
val channel_ab = TestProbe()
val channel_bc = TestProbe()
// note we simulate this by not having a binding for this channel
val add_ab = {
val cmd = buildCommand(finalAmountMsat, paymentHash, hops, currentBlockCount)
// and then manually build an htlc
UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amountMsat, cmd.expiry, cmd.paymentHash, cmd.onion)
}
val fail_cb = UpdateFailHtlc(channelId = add_ab.channelId, id = add_ab.id, reason = "some reason".getBytes())
sender.send(relayer, (add_ab, fail_cb))
channel_ab.expectNoMsg(1 second)
paymentListener.expectMsgType[PaymentFailed]
}
}

View file

@ -0,0 +1,89 @@
package fr.acinq.eclair.router
import akka.actor.ActorRef
import akka.testkit.TestProbe
import fr.acinq.eclair.router.Router.DUMMY_SIG
import fr.acinq.eclair.wire._
import fr.acinq.eclair.{TestkitBaseClass, randomKey}
import org.junit.runner.RunWith
import org.scalatest.junit.JUnitRunner
import scala.concurrent.duration._
/**
* Base class for router testing.
* It is re-used in payment FSM tests
* Created by PM on 29/08/2016.
*/
@RunWith(classOf[JUnitRunner])
abstract class BaseRouterSpec extends TestkitBaseClass {
type FixtureParam = Tuple2[ActorRef, TestProbe]
def randomPubkey = randomKey.publicKey
val (a, b, c, d, e, f) = (randomPubkey, randomPubkey, randomPubkey, randomPubkey, randomPubkey, randomPubkey)
val ann_a = NodeAnnouncement(DUMMY_SIG, 0, a, (0, 0, 0), "node-A", "0000", Nil)
val ann_b = NodeAnnouncement(DUMMY_SIG, 0, b, (0, 0, 0), "node-B", "0000", Nil)
val ann_c = NodeAnnouncement(DUMMY_SIG, 0, c, (0, 0, 0), "node-C", "0000", Nil)
val ann_d = NodeAnnouncement(DUMMY_SIG, 0, d, (0, 0, 0), "node-D", "0000", Nil)
val ann_e = NodeAnnouncement(DUMMY_SIG, 0, e, (0, 0, 0), "node-E", "0000", Nil)
val ann_f = NodeAnnouncement(DUMMY_SIG, 0, f, (0, 0, 0), "node-F", "0000", Nil)
val chan_ab = ChannelAnnouncement(DUMMY_SIG, DUMMY_SIG, channelId = 1, DUMMY_SIG, DUMMY_SIG, a, b, "", "")
val chan_bc = ChannelAnnouncement(DUMMY_SIG, DUMMY_SIG, channelId = 2, DUMMY_SIG, DUMMY_SIG, b, c, "", "")
val chan_cd = ChannelAnnouncement(DUMMY_SIG, DUMMY_SIG, channelId = 3, DUMMY_SIG, DUMMY_SIG, c, d, "", "")
val chan_ef = ChannelAnnouncement(DUMMY_SIG, DUMMY_SIG, channelId = 4, DUMMY_SIG, DUMMY_SIG, e, f, "", "")
val defaultChannelUpdate = ChannelUpdate(Router.DUMMY_SIG, 0, 0, "0000", 0, 0, 0, 0)
val channelUpdate_ab = ChannelUpdate(Router.DUMMY_SIG, channelId = 1, 0, "0000", cltvExpiryDelta = 7, 0, feeBaseMsat = 766000, feeProportionalMillionths = 10)
val channelUpdate_bc = ChannelUpdate(Router.DUMMY_SIG, channelId = 2, 0, "0000", cltvExpiryDelta = 5, 0, feeBaseMsat = 233000, feeProportionalMillionths = 1)
val channelUpdate_cd = ChannelUpdate(Router.DUMMY_SIG, channelId = 3, 0, "0000", cltvExpiryDelta = 3, 0, feeBaseMsat = 153000, feeProportionalMillionths = 4)
val channelUpdate_ef = ChannelUpdate(Router.DUMMY_SIG, channelId = 4, 0, "0000", cltvExpiryDelta = 9, 0, feeBaseMsat = 786000, feeProportionalMillionths = 8)
override def withFixture(test: OneArgTest) = {
// the network will be a --(1)--> b ---(2)--> c --(3)--> d and e --(4)--> f (we are a)
within(30 seconds) {
// first we set up the router
val watcher = TestProbe()
val router = system.actorOf(Router.props(watcher.ref, ann_a))
// we announce channels
router ! chan_ab
router ! chan_bc
router ! chan_cd
router ! chan_ef
// then nodes
router ! ann_a
router ! ann_b
router ! ann_c
router ! ann_d
router ! ann_e
router ! ann_f
// then channel updates
router ! channelUpdate_ab
router ! channelUpdate_bc
router ! channelUpdate_cd
router ! channelUpdate_ef
val sender = TestProbe()
sender.send(router, 'nodes)
val nodes = sender.expectMsgType[Iterable[NodeAnnouncement]]
assert(nodes.size === 6)
sender.send(router, 'channels)
val channels = sender.expectMsgType[Iterable[ChannelAnnouncement]]
assert(channels.size === 4)
sender.send(router, 'updates)
val updates = sender.expectMsgType[Iterable[ChannelUpdate]]
assert(updates.size === 4)
test((router, watcher))
}
}
}

View file

@ -0,0 +1,127 @@
package fr.acinq.eclair.router
import fr.acinq.bitcoin.Crypto.PrivateKey
import fr.acinq.bitcoin.{BinaryData, Crypto}
import fr.acinq.eclair.wire.ChannelUpdate
import org.junit.runner.RunWith
import org.scalatest.FunSuite
import org.scalatest.junit.JUnitRunner
import scala.concurrent.Await
import scala.concurrent.duration._
/**
* Created by PM on 31/05/2016.
*/
@RunWith(classOf[JUnitRunner])
class RouteCalculationSpec extends FunSuite {
val (a, b, c, d, e) = (BinaryData("aa" * 33), BinaryData("bb" * 33), BinaryData("cc" * 33), BinaryData("dd" * 33), BinaryData("ee" * 33))
test("calculate simple route") {
val channels = List(
ChannelDesc(1L, a, b),
ChannelDesc(2L, b, c),
ChannelDesc(3L, c, d),
ChannelDesc(4L, d, e)
)
val route = Router.findRouteDijkstra(a, e, channels)
assert(route.map(_.id) === 1 :: 2 :: 3 :: 4 :: Nil)
}
test("route not found") {
val channels = List(
ChannelDesc(1L, a, b),
ChannelDesc(2L, b, c),
ChannelDesc(4L, d, e)
)
intercept[RuntimeException] {
Router.findRouteDijkstra(a, e, channels)
}
}
test("route to self") {
val channels = List(
ChannelDesc(1L, a, b),
ChannelDesc(2L, b, c),
ChannelDesc(3L, c, d),
ChannelDesc(4L, d, e)
)
intercept[RuntimeException] {
Router.findRouteDijkstra(a, a, channels)
}
}
test("route to immediate neighbor") {
val channels = List(
ChannelDesc(1L, a, b),
ChannelDesc(2L, b, c),
ChannelDesc(3L, c, d),
ChannelDesc(4L, d, e)
)
val route = Router.findRouteDijkstra(a, b, channels)
assert(route.map(_.id) === 1 :: Nil)
}
test("directed graph") {
val channels = List(
ChannelDesc(1L, a, b),
ChannelDesc(2L, b, c),
ChannelDesc(3L, c, d),
ChannelDesc(4L, d, e)
)
// a->e works, e->a fails
Router.findRouteDijkstra(a, e, channels)
intercept[RuntimeException] {
Router.findRouteDijkstra(e, a, channels)
}
}
test("compute an example sig") {
val data = BinaryData("00" * 32)
val key = PrivateKey(BinaryData("11" * 32))
val sig = Crypto.encodeSignature(Crypto.sign(data, key))
assert(Crypto.isDERSignature(sig :+ 1.toByte))
}
test("calculate route and return metadata") {
val uab = ChannelUpdate(Router.DUMMY_SIG, 1L, 0L, "0000", 1, 42, 2500, 140)
val uba = ChannelUpdate(Router.DUMMY_SIG, 1L, 1L, "0001", 1, 43, 2501, 141)
val ubc = ChannelUpdate(Router.DUMMY_SIG, 2L, 1L, "0000", 1, 44, 2502, 142)
val ucb = ChannelUpdate(Router.DUMMY_SIG, 2L, 1L, "0001", 1, 45, 2503, 143)
val ucd = ChannelUpdate(Router.DUMMY_SIG, 3L, 1L, "0000", 1, 46, 2504, 144)
val udc = ChannelUpdate(Router.DUMMY_SIG, 3L, 1L, "0001", 1, 47, 2505, 145)
val ude = ChannelUpdate(Router.DUMMY_SIG, 4L, 1L, "0000", 1, 48, 2506, 146)
val ued = ChannelUpdate(Router.DUMMY_SIG, 4L, 1L, "0001", 1, 49, 2507, 147)
val updates = Map(
ChannelDesc(1L, a, b) -> uab,
ChannelDesc(1L, b, a) -> uba,
ChannelDesc(2L, b, c) -> ubc,
ChannelDesc(2L, c, b) -> ucb,
ChannelDesc(3L, c, d) -> ucd,
ChannelDesc(3L, d, c) -> udc,
ChannelDesc(4L, d, e) -> ude,
ChannelDesc(4L, e, d) -> ued
)
import scala.concurrent.ExecutionContext.Implicits.global
val hops = Await.result(Router.findRoute(a, e, updates), 3 seconds)
assert(hops === Hop(a, b, uab) :: Hop(b, c, ubc) :: Hop(c, d, ucd) :: Hop(d, e, ude) :: Nil)
}
}

View file

@ -0,0 +1,46 @@
package fr.acinq.eclair.router
import akka.actor.Status.Failure
import akka.testkit.TestProbe
import org.junit.runner.RunWith
import org.scalatest.junit.JUnitRunner
/**
* Created by PM on 29/08/2016.
*/
@RunWith(classOf[JUnitRunner])
class RouterSpec extends BaseRouterSpec {
test("route not found (unreachable target)") { case (router, watcher) =>
val sender = TestProbe()
// no route a->f
sender.send(router, RouteRequest(a, f))
val res = sender.expectMsgType[Failure]
assert(res.cause.getMessage === "route not found")
}
test("route not found (non-existing source)") { case (router, watcher) =>
val sender = TestProbe()
// no route a->f
sender.send(router, RouteRequest(randomPubkey, f))
val res = sender.expectMsgType[Failure]
assert(res.cause.getMessage === "graph must contain the source vertex")
}
test("route not found (non-existing target)") { case (router, watcher) =>
val sender = TestProbe()
// no route a->f
sender.send(router, RouteRequest(a, randomPubkey))
val res = sender.expectMsgType[Failure]
assert(res.cause.getMessage === "graph must contain the sink vertex")
}
test("route found") { case (router, watcher) =>
val sender = TestProbe()
sender.send(router, RouteRequest(a, d))
val res = sender.expectMsgType[RouteResponse]
assert(res.hops.map(_.nodeId).toList === a.toBin :: b.toBin :: c.toBin :: Nil)
assert(res.hops.last.nextNodeId === d.toBin)
}
}

View file

@ -103,14 +103,14 @@ class ClaimSentHtlcSpec extends FunSuite {
assert(e.getMessage === "unsatisfied CSV lock time")
}
test("Blob can spend this HTLC if he knows the payment hash") {
test("Bob can spend this HTLC if he knows the payment hash") {
val sig = Transaction.signInput(tx1, 0, redeemScript, SIGHASH_ALL, tx.txOut(0).amount, 1, Bob.finalKey)
val witness = ScriptWitness(sig :: Alice.R :: redeemScript :: Nil)
val tx2 = tx1.updateWitness(0, witness)
Transaction.correctlySpends(tx2, Seq(tx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
}
test("Blob can spend this HTLC if he knows the revocation hash") {
test("Bob can spend this HTLC if he knows the revocation hash") {
val sig = Transaction.signInput(tx1, 0, redeemScript, SIGHASH_ALL, tx.txOut(0).amount, 1, Bob.finalKey)
val witness = ScriptWitness(sig :: Alice.revokeCommit :: redeemScript :: Nil)
val tx2 = tx1.updateWitness(0, witness)

View file

@ -1,10 +1,10 @@
package fr.acinq.eclair.wire
import java.net.InetAddress
import java.net.{InetAddress, InetSocketAddress}
import fr.acinq.bitcoin.Crypto.{PrivateKey, Scalar}
import fr.acinq.bitcoin.{BinaryData, Crypto}
import fr.acinq.eclair.wire.Codecs.{ipv6, lightningMessageCodec, rgb, zeropaddedstring}
import fr.acinq.eclair.wire.Codecs.{socketaddress, lightningMessageCodec, rgb, zeropaddedstring}
import org.junit.runner.RunWith
import org.scalatest.FunSuite
import org.scalatest.junit.JUnitRunner
@ -47,20 +47,22 @@ class CodecsSpec extends FunSuite {
assert(color === color2)
}
test("encode/decode with ipv6 codec") {
test("encode/decode with socketaddress codec") {
{
val ipv4addr = InetAddress.getByAddress(Array[Byte](192.toByte, 168.toByte, 1.toByte, 42.toByte))
val bin = ipv6.encode(ipv4addr).toOption.get
assert(bin === hex"00 00 00 00 00 00 00 00 00 00 FF FF C0 A8 01 2A".toBitVector)
val ipv4addr2 = ipv6.decode(bin).toOption.get.value
assert(ipv4addr === ipv4addr2)
val isa = new InetSocketAddress(ipv4addr, 4231)
val bin = socketaddress.encode(isa).toOption.get
assert(bin === hex"01 C0 A8 01 2A 10 87".toBitVector)
val isa2 = socketaddress.decode(bin).toOption.get.value
assert(isa === isa2)
}
{
val ipv6addr = InetAddress.getByAddress(hex"2001 0db8 0000 85a3 0000 0000 ac1f 8001".toArray)
val bin = ipv6.encode(ipv6addr).toOption.get
assert(bin === hex"2001 0db8 0000 85a3 0000 0000 ac1f 8001".toBitVector)
val ipv6addr2 = ipv6.decode(bin).toOption.get.value
assert(ipv6addr === ipv6addr2)
val isa = new InetSocketAddress(ipv6addr, 4231)
val bin = socketaddress.encode(isa).toOption.get
assert(bin === hex"02 2001 0db8 0000 85a3 0000 0000 ac1f 8001 1087".toBitVector)
val isa2 = socketaddress.decode(bin).toOption.get.value
assert(isa === isa2)
}
}
@ -149,7 +151,7 @@ class CodecsSpec extends FunSuite {
val commit_sig = CommitSig(1, randomSignature, randomSignature :: randomSignature :: randomSignature :: Nil)
val revoke_and_ack = RevokeAndAck(1, scalar(0), point(1), randomSignature :: randomSignature :: randomSignature :: randomSignature :: randomSignature :: Nil)
val channel_announcement = ChannelAnnouncement(randomSignature, randomSignature, 1, randomSignature, randomSignature, bin(33, 5), bin(33, 6), bin(33, 7), bin(33, 8))
val node_announcement = NodeAnnouncement(randomSignature, 1, InetAddress.getByAddress(Array[Byte](192.toByte, 168.toByte, 1.toByte, 42.toByte)), 2, bin(33, 2), (100.toByte, 200.toByte, 300.toByte), "node-alias")
val node_announcement = NodeAnnouncement(randomSignature, 1, bin(33, 2), (100.toByte, 200.toByte, 300.toByte), "node-alias", bin(0, 0), new InetSocketAddress(InetAddress.getByAddress(Array[Byte](192.toByte, 168.toByte, 1.toByte, 42.toByte)), 42000) :: Nil)
val channel_update = ChannelUpdate(randomSignature, 1, 2, bin(2, 2), 3, 4, 5, 6)
val msgs: List[LightningMessage] =
@ -165,4 +167,12 @@ class CodecsSpec extends FunSuite {
}
}
}
test("encode/decode per-hop payload") {
val payload = PerHopPayload(amt_to_forward = 142000, outgoing_cltv_value = 500000)
val bin = Codecs.perHopPayloadCodec.encode(payload).toOption.get
assert(bin.toByteVector.size === 20)
val payload1 = Codecs.perHopPayloadCodec.decode(bin).toOption.get.value
assert(payload === payload1)
}
}

View file

@ -1,124 +0,0 @@
<?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>lightning-types_2.11</artifactId>
<packaging>jar</packaging>
<name>${project.artifactId}</name>
<build>
<plugins>
<plugin>
<groupId>com.googlecode.maven-download-plugin</groupId>
<artifactId>download-maven-plugin</artifactId>
<version>1.3.0</version>
<executions>
<execution>
<id>download-scalapbc</id>
<phase>initialize</phase>
<goals>
<goal>wget</goal>
</goals>
<configuration>
<url>https://github.com/trueaccord/ScalaPB/releases/download/v${scalapb.version}/scalapbc-${scalapb.version}.zip</url>
<md5>9fef6a23b9e717c14e1bb3924ac0e156</md5>
<unpack>true</unpack>
<outputDirectory>${project.build.directory}</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-antrun-plugin</artifactId>
<version>1.7</version>
<executions>
<execution>
<id>mkdir-generated-sources</id>
<phase>generate-sources</phase>
<goals>
<goal>run</goal>
</goals>
<configuration>
<target>
<mkdir dir="${project.build.directory}/generated-sources/scala"/>
</target>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>1.4.0</version>
<executions>
<execution>
<phase>generate-sources</phase>
<goals>
<goal>exec</goal>
</goals>
</execution>
</executions>
<configuration>
<executable>${project.build.directory}/scalapbc-${scalapb.version}/bin/scalapbc${script.extension}</executable>
<commandlineArgs>--proto_path=${project.basedir}/src/main/protobuf --scala_out=${project.build.directory}/generated-sources/scala ${project.basedir}/src/main/protobuf/lightning.proto</commandlineArgs>
</configuration>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
<executions>
<execution>
<phase>generate-sources</phase>
<goals>
<goal>add-source</goal>
</goals>
<configuration>
<sources>
<source>${project.build.directory}/generated-sources/scala</source>
</sources>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>com.trueaccord.scalapb</groupId>
<artifactId>scalapb-runtime_${scala.version.short}</artifactId>
<version>${scalapb.version}</version>
</dependency>
</dependencies>
<profiles>
<profile>
<id>default</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<properties>
<script.extension></script.extension>
</properties>
</profile>
<profile>
<id>Windows</id>
<activation>
<os>
<family>Windows</family>
</os>
</activation>
<properties>
<script.extension>.bat</script.extension>
</properties>
</profile>
</profiles>
</project>

View file

@ -1,147 +0,0 @@
// Protocol Buffers - Google's data interchange format
// Copyright 2008 Google Inc. All rights reserved.
// https://developers.google.com/protocol-buffers/
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following disclaimer
// in the documentation and/or other materials provided with the
// distribution.
// * Neither the name of Google Inc. nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
// Author: kenton@google.com (Kenton Varda)
//
// WARNING: The plugin interface is currently EXPERIMENTAL and is subject to
// change.
//
// protoc (aka the Protocol Compiler) can be extended via plugins. A plugin is
// just a program that reads a CodeGeneratorRequest from stdin and writes a
// CodeGeneratorResponse to stdout.
//
// Plugins written using C++ can use google/protobuf/compiler/plugin.h instead
// of dealing with the raw protocol defined here.
//
// A plugin executable needs only to be placed somewhere in the path. The
// plugin should be named "protoc-gen-$NAME", and will then be used when the
// flag "--${NAME}_out" is passed to protoc.
package google.protobuf.compiler;
option java_package = "com.google.protobuf.compiler";
option java_outer_classname = "PluginProtos";
import "google/protobuf/descriptor.proto";
// An encoded CodeGeneratorRequest is written to the plugin's stdin.
message CodeGeneratorRequest {
// The .proto files that were explicitly listed on the command-line. The
// code generator should generate code only for these files. Each file's
// descriptor will be included in proto_file, below.
repeated string file_to_generate = 1;
// The generator parameter passed on the command-line.
optional string parameter = 2;
// FileDescriptorProtos for all files in files_to_generate and everything
// they import. The files will appear in topological order, so each file
// appears before any file that imports it.
//
// protoc guarantees that all proto_files will be written after
// the fields above, even though this is not technically guaranteed by the
// protobuf wire format. This theoretically could allow a plugin to stream
// in the FileDescriptorProtos and handle them one by one rather than read
// the entire set into memory at once. However, as of this writing, this
// is not similarly optimized on protoc's end -- it will store all fields in
// memory at once before sending them to the plugin.
repeated FileDescriptorProto proto_file = 15;
}
// The plugin writes an encoded CodeGeneratorResponse to stdout.
message CodeGeneratorResponse {
// Error message. If non-empty, code generation failed. The plugin process
// should exit with status code zero even if it reports an error in this way.
//
// This should be used to indicate errors in .proto files which prevent the
// code generator from generating correct code. Errors which indicate a
// problem in protoc itself -- such as the input CodeGeneratorRequest being
// unparseable -- should be reported by writing a message to stderr and
// exiting with a non-zero status code.
optional string error = 1;
// Represents a single generated file.
message File {
// The file name, relative to the output directory. The name must not
// contain "." or ".." components and must be relative, not be absolute (so,
// the file cannot lie outside the output directory). "/" must be used as
// the path separator, not "\".
//
// If the name is omitted, the content will be appended to the previous
// file. This allows the generator to break large files into small chunks,
// and allows the generated text to be streamed back to protoc so that large
// files need not reside completely in memory at one time. Note that as of
// this writing protoc does not optimize for this -- it will read the entire
// CodeGeneratorResponse before writing files to disk.
optional string name = 1;
// If non-empty, indicates that the named file should already exist, and the
// content here is to be inserted into that file at a defined insertion
// point. This feature allows a code generator to extend the output
// produced by another code generator. The original generator may provide
// insertion points by placing special annotations in the file that look
// like:
// @@protoc_insertion_point(NAME)
// The annotation can have arbitrary text before and after it on the line,
// which allows it to be placed in a comment. NAME should be replaced with
// an identifier naming the point -- this is what other generators will use
// as the insertion_point. Code inserted at this point will be placed
// immediately above the line containing the insertion point (thus multiple
// insertions to the same point will come out in the order they were added).
// The double-@ is intended to make it unlikely that the generated code
// could contain things that look like insertion points by accident.
//
// For example, the C++ code generator places the following line in the
// .pb.h files that it generates:
// // @@protoc_insertion_point(namespace_scope)
// This line appears within the scope of the file's package namespace, but
// outside of any particular class. Another plugin can then specify the
// insertion_point "namespace_scope" to generate additional classes or
// other declarations that should be placed in this scope.
//
// Note that if the line containing the insertion point begins with
// whitespace, the same whitespace will be added to every line of the
// inserted text. This is useful for languages like Python, where
// indentation matters. In these languages, the insertion point comment
// should be indented the same amount as any inserted code will need to be
// in order to work correctly in that context.
//
// The code generator that generates the initial file and the one which
// inserts into it must both run as part of a single invocation of protoc.
// Code generators are executed in the order in which they appear on the
// command line.
//
// If |insertion_point| is present, |name| must also be present.
optional string insertion_point = 2;
// The file contents.
optional string content = 15;
}
repeated File file = 15;
}

View file

@ -1,687 +0,0 @@
// Protocol Buffers - Google's data interchange format
// Copyright 2008 Google Inc. All rights reserved.
// https://developers.google.com/protocol-buffers/
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following disclaimer
// in the documentation and/or other materials provided with the
// distribution.
// * Neither the name of Google Inc. nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
// Author: kenton@google.com (Kenton Varda)
// Based on original Protocol Buffers design by
// Sanjay Ghemawat, Jeff Dean, and others.
//
// The messages in this file describe the definitions found in .proto files.
// A valid .proto file can be translated directly to a FileDescriptorProto
// without any other information (e.g. without reading its imports).
package google.protobuf;
option java_package = "com.google.protobuf";
option java_outer_classname = "DescriptorProtos";
// descriptor.proto must be optimized for speed because reflection-based
// algorithms don't work during bootstrapping.
option optimize_for = SPEED;
// The protocol compiler can output a FileDescriptorSet containing the .proto
// files it parses.
message FileDescriptorSet {
repeated FileDescriptorProto file = 1;
}
// Describes a complete .proto file.
message FileDescriptorProto {
optional string name = 1; // file name, relative to root of source tree
optional string package = 2; // e.g. "foo", "foo.bar", etc.
// Names of files imported by this file.
repeated string dependency = 3;
// Indexes of the public imported files in the dependency list above.
repeated int32 public_dependency = 10;
// Indexes of the weak imported files in the dependency list.
// For Google-internal migration only. Do not use.
repeated int32 weak_dependency = 11;
// All top-level definitions in this file.
repeated DescriptorProto message_type = 4;
repeated EnumDescriptorProto enum_type = 5;
repeated ServiceDescriptorProto service = 6;
repeated FieldDescriptorProto extension = 7;
optional FileOptions options = 8;
// This field contains optional information about the original source code.
// You may safely remove this entire field whithout harming runtime
// functionality of the descriptors -- the information is needed only by
// development tools.
optional SourceCodeInfo source_code_info = 9;
}
// Describes a message type.
message DescriptorProto {
optional string name = 1;
repeated FieldDescriptorProto field = 2;
repeated FieldDescriptorProto extension = 6;
repeated DescriptorProto nested_type = 3;
repeated EnumDescriptorProto enum_type = 4;
message ExtensionRange {
optional int32 start = 1;
optional int32 end = 2;
}
repeated ExtensionRange extension_range = 5;
repeated OneofDescriptorProto oneof_decl = 8;
optional MessageOptions options = 7;
}
// Describes a field within a message.
message FieldDescriptorProto {
enum Type {
// 0 is reserved for errors.
// Order is weird for historical reasons.
TYPE_DOUBLE = 1;
TYPE_FLOAT = 2;
// Not ZigZag encoded. Negative numbers take 10 bytes. Use TYPE_SINT64 if
// negative values are likely.
TYPE_INT64 = 3;
TYPE_UINT64 = 4;
// Not ZigZag encoded. Negative numbers take 10 bytes. Use TYPE_SINT32 if
// negative values are likely.
TYPE_INT32 = 5;
TYPE_FIXED64 = 6;
TYPE_FIXED32 = 7;
TYPE_BOOL = 8;
TYPE_STRING = 9;
TYPE_GROUP = 10; // Tag-delimited aggregate.
TYPE_MESSAGE = 11; // Length-delimited aggregate.
// New in version 2.
TYPE_BYTES = 12;
TYPE_UINT32 = 13;
TYPE_ENUM = 14;
TYPE_SFIXED32 = 15;
TYPE_SFIXED64 = 16;
TYPE_SINT32 = 17; // Uses ZigZag encoding.
TYPE_SINT64 = 18; // Uses ZigZag encoding.
};
enum Label {
// 0 is reserved for errors
LABEL_OPTIONAL = 1;
LABEL_REQUIRED = 2;
LABEL_REPEATED = 3;
// TODO(sanjay): Should we add LABEL_MAP?
};
optional string name = 1;
optional int32 number = 3;
optional Label label = 4;
// If type_name is set, this need not be set. If both this and type_name
// are set, this must be one of TYPE_ENUM, TYPE_MESSAGE or TYPE_GROUP.
optional Type type = 5;
// For message and enum types, this is the name of the type. If the name
// starts with a '.', it is fully-qualified. Otherwise, C++-like scoping
// rules are used to find the type (i.e. first the nested types within this
// message are searched, then within the parent, on up to the root
// namespace).
optional string type_name = 6;
// For extensions, this is the name of the type being extended. It is
// resolved in the same manner as type_name.
optional string extendee = 2;
// For numeric types, contains the original text representation of the value.
// For booleans, "true" or "false".
// For strings, contains the default text contents (not escaped in any way).
// For bytes, contains the C escaped value. All bytes >= 128 are escaped.
// TODO(kenton): Base-64 encode?
optional string default_value = 7;
// If set, gives the index of a oneof in the containing type's oneof_decl
// list. This field is a member of that oneof. Extensions of a oneof should
// not set this since the oneof to which they belong will be inferred based
// on the extension range containing the extension's field number.
optional int32 oneof_index = 9;
optional FieldOptions options = 8;
}
// Describes a oneof.
message OneofDescriptorProto {
optional string name = 1;
}
// Describes an enum type.
message EnumDescriptorProto {
optional string name = 1;
repeated EnumValueDescriptorProto value = 2;
optional EnumOptions options = 3;
}
// Describes a value within an enum.
message EnumValueDescriptorProto {
optional string name = 1;
optional int32 number = 2;
optional EnumValueOptions options = 3;
}
// Describes a service.
message ServiceDescriptorProto {
optional string name = 1;
repeated MethodDescriptorProto method = 2;
optional ServiceOptions options = 3;
}
// Describes a method of a service.
message MethodDescriptorProto {
optional string name = 1;
// Input and output type names. These are resolved in the same way as
// FieldDescriptorProto.type_name, but must refer to a message type.
optional string input_type = 2;
optional string output_type = 3;
optional MethodOptions options = 4;
}
// ===================================================================
// Options
// Each of the definitions above may have "options" attached. These are
// just annotations which may cause code to be generated slightly differently
// or may contain hints for code that manipulates protocol messages.
//
// Clients may define custom options as extensions of the *Options messages.
// These extensions may not yet be known at parsing time, so the parser cannot
// store the values in them. Instead it stores them in a field in the *Options
// message called uninterpreted_option. This field must have the same name
// across all *Options messages. We then use this field to populate the
// extensions when we build a descriptor, at which point all protos have been
// parsed and so all extensions are known.
//
// Extension numbers for custom options may be chosen as follows:
// * For options which will only be used within a single application or
// organization, or for experimental options, use field numbers 50000
// through 99999. It is up to you to ensure that you do not use the
// same number for multiple options.
// * For options which will be published and used publicly by multiple
// independent entities, e-mail protobuf-global-extension-registry@google.com
// to reserve extension numbers. Simply provide your project name (e.g.
// Object-C plugin) and your porject website (if available) -- there's no need
// to explain how you intend to use them. Usually you only need one extension
// number. You can declare multiple options with only one extension number by
// putting them in a sub-message. See the Custom Options section of the docs
// for examples:
// https://developers.google.com/protocol-buffers/docs/proto#options
// If this turns out to be popular, a web service will be set up
// to automatically assign option numbers.
message FileOptions {
// Sets the Java package where classes generated from this .proto will be
// placed. By default, the proto package is used, but this is often
// inappropriate because proto packages do not normally start with backwards
// domain names.
optional string java_package = 1;
// If set, all the classes from the .proto file are wrapped in a single
// outer class with the given name. This applies to both Proto1
// (equivalent to the old "--one_java_file" option) and Proto2 (where
// a .proto always translates to a single class, but you may want to
// explicitly choose the class name).
optional string java_outer_classname = 8;
// If set true, then the Java code generator will generate a separate .java
// file for each top-level message, enum, and service defined in the .proto
// file. Thus, these types will *not* be nested inside the outer class
// named by java_outer_classname. However, the outer class will still be
// generated to contain the file's getDescriptor() method as well as any
// top-level extensions defined in the file.
optional bool java_multiple_files = 10 [default=false];
// If set true, then the Java code generator will generate equals() and
// hashCode() methods for all messages defined in the .proto file.
// - In the full runtime, this is purely a speed optimization, as the
// AbstractMessage base class includes reflection-based implementations of
// these methods.
//- In the lite runtime, setting this option changes the semantics of
// equals() and hashCode() to more closely match those of the full runtime;
// the generated methods compute their results based on field values rather
// than object identity. (Implementations should not assume that hashcodes
// will be consistent across runtimes or versions of the protocol compiler.)
optional bool java_generate_equals_and_hash = 20 [default=false];
// If set true, then the Java2 code generator will generate code that
// throws an exception whenever an attempt is made to assign a non-UTF-8
// byte sequence to a string field.
// Message reflection will do the same.
// However, an extension field still accepts non-UTF-8 byte sequences.
// This option has no effect on when used with the lite runtime.
optional bool java_string_check_utf8 = 27 [default=false];
// Generated classes can be optimized for speed or code size.
enum OptimizeMode {
SPEED = 1; // Generate complete code for parsing, serialization,
// etc.
CODE_SIZE = 2; // Use ReflectionOps to implement these methods.
LITE_RUNTIME = 3; // Generate code using MessageLite and the lite runtime.
}
optional OptimizeMode optimize_for = 9 [default=SPEED];
// Sets the Go package where structs generated from this .proto will be
// placed. There is no default.
optional string go_package = 11;
// Should generic services be generated in each language? "Generic" services
// are not specific to any particular RPC system. They are generated by the
// main code generators in each language (without additional plugins).
// Generic services were the only kind of service generation supported by
// early versions of proto2.
//
// Generic services are now considered deprecated in favor of using plugins
// that generate code specific to your particular RPC system. Therefore,
// these default to false. Old code which depends on generic services should
// explicitly set them to true.
optional bool cc_generic_services = 16 [default=false];
optional bool java_generic_services = 17 [default=false];
optional bool py_generic_services = 18 [default=false];
// Is this file deprecated?
// Depending on the target platform, this can emit Deprecated annotations
// for everything in the file, or it will be completely ignored; in the very
// least, this is a formalization for deprecating files.
optional bool deprecated = 23 [default=false];
// The parser stores options it doesn't recognize here. See above.
repeated UninterpretedOption uninterpreted_option = 999;
// Clients can define custom options in extensions of this message. See above.
extensions 1000 to max;
}
message MessageOptions {
// Set true to use the old proto1 MessageSet wire format for extensions.
// This is provided for backwards-compatibility with the MessageSet wire
// format. You should not use this for any other reason: It's less
// efficient, has fewer features, and is more complicated.
//
// The message must be defined exactly as follows:
// message Foo {
// option message_set_wire_format = true;
// extensions 4 to max;
// }
// Note that the message cannot have any defined fields; MessageSets only
// have extensions.
//
// All extensions of your type must be singular messages; e.g. they cannot
// be int32s, enums, or repeated messages.
//
// Because this is an option, the above two restrictions are not enforced by
// the protocol compiler.
optional bool message_set_wire_format = 1 [default=false];
// Disables the generation of the standard "descriptor()" accessor, which can
// conflict with a field of the same name. This is meant to make migration
// from proto1 easier; new code should avoid fields named "descriptor".
optional bool no_standard_descriptor_accessor = 2 [default=false];
// Is this message deprecated?
// Depending on the target platform, this can emit Deprecated annotations
// for the message, or it will be completely ignored; in the very least,
// this is a formalization for deprecating messages.
optional bool deprecated = 3 [default=false];
// The parser stores options it doesn't recognize here. See above.
repeated UninterpretedOption uninterpreted_option = 999;
// Clients can define custom options in extensions of this message. See above.
extensions 1000 to max;
}
message FieldOptions {
// The ctype option instructs the C++ code generator to use a different
// representation of the field than it normally would. See the specific
// options below. This option is not yet implemented in the open source
// release -- sorry, we'll try to include it in a future version!
optional CType ctype = 1 [default = STRING];
enum CType {
// Default mode.
STRING = 0;
CORD = 1;
STRING_PIECE = 2;
}
// The packed option can be enabled for repeated primitive fields to enable
// a more efficient representation on the wire. Rather than repeatedly
// writing the tag and type for each element, the entire array is encoded as
// a single length-delimited blob.
optional bool packed = 2;
// Should this field be parsed lazily? Lazy applies only to message-type
// fields. It means that when the outer message is initially parsed, the
// inner message's contents will not be parsed but instead stored in encoded
// form. The inner message will actually be parsed when it is first accessed.
//
// This is only a hint. Implementations are free to choose whether to use
// eager or lazy parsing regardless of the value of this option. However,
// setting this option true suggests that the protocol author believes that
// using lazy parsing on this field is worth the additional bookkeeping
// overhead typically needed to implement it.
//
// This option does not affect the public interface of any generated code;
// all method signatures remain the same. Furthermore, thread-safety of the
// interface is not affected by this option; const methods remain safe to
// call from multiple threads concurrently, while non-const methods continue
// to require exclusive access.
//
//
// Note that implementations may choose not to check required fields within
// a lazy sub-message. That is, calling IsInitialized() on the outher message
// may return true even if the inner message has missing required fields.
// This is necessary because otherwise the inner message would have to be
// parsed in order to perform the check, defeating the purpose of lazy
// parsing. An implementation which chooses not to check required fields
// must be consistent about it. That is, for any particular sub-message, the
// implementation must either *always* check its required fields, or *never*
// check its required fields, regardless of whether or not the message has
// been parsed.
optional bool lazy = 5 [default=false];
// Is this field deprecated?
// Depending on the target platform, this can emit Deprecated annotations
// for accessors, or it will be completely ignored; in the very least, this
// is a formalization for deprecating fields.
optional bool deprecated = 3 [default=false];
// EXPERIMENTAL. DO NOT USE.
// For "map" fields, the name of the field in the enclosed type that
// is the key for this map. For example, suppose we have:
// message Item {
// required string name = 1;
// required string value = 2;
// }
// message Config {
// repeated Item items = 1 [experimental_map_key="name"];
// }
// In this situation, the map key for Item will be set to "name".
// TODO: Fully-implement this, then remove the "experimental_" prefix.
optional string experimental_map_key = 9;
// For Google-internal migration only. Do not use.
optional bool weak = 10 [default=false];
// The parser stores options it doesn't recognize here. See above.
repeated UninterpretedOption uninterpreted_option = 999;
// Clients can define custom options in extensions of this message. See above.
extensions 1000 to max;
}
message EnumOptions {
// Set this option to true to allow mapping different tag names to the same
// value.
optional bool allow_alias = 2;
// Is this enum deprecated?
// Depending on the target platform, this can emit Deprecated annotations
// for the enum, or it will be completely ignored; in the very least, this
// is a formalization for deprecating enums.
optional bool deprecated = 3 [default=false];
// The parser stores options it doesn't recognize here. See above.
repeated UninterpretedOption uninterpreted_option = 999;
// Clients can define custom options in extensions of this message. See above.
extensions 1000 to max;
}
message EnumValueOptions {
// Is this enum value deprecated?
// Depending on the target platform, this can emit Deprecated annotations
// for the enum value, or it will be completely ignored; in the very least,
// this is a formalization for deprecating enum values.
optional bool deprecated = 1 [default=false];
// The parser stores options it doesn't recognize here. See above.
repeated UninterpretedOption uninterpreted_option = 999;
// Clients can define custom options in extensions of this message. See above.
extensions 1000 to max;
}
message ServiceOptions {
// Note: Field numbers 1 through 32 are reserved for Google's internal RPC
// framework. We apologize for hoarding these numbers to ourselves, but
// we were already using them long before we decided to release Protocol
// Buffers.
// Is this service deprecated?
// Depending on the target platform, this can emit Deprecated annotations
// for the service, or it will be completely ignored; in the very least,
// this is a formalization for deprecating services.
optional bool deprecated = 33 [default=false];
// The parser stores options it doesn't recognize here. See above.
repeated UninterpretedOption uninterpreted_option = 999;
// Clients can define custom options in extensions of this message. See above.
extensions 1000 to max;
}
message MethodOptions {
// Note: Field numbers 1 through 32 are reserved for Google's internal RPC
// framework. We apologize for hoarding these numbers to ourselves, but
// we were already using them long before we decided to release Protocol
// Buffers.
// Is this method deprecated?
// Depending on the target platform, this can emit Deprecated annotations
// for the method, or it will be completely ignored; in the very least,
// this is a formalization for deprecating methods.
optional bool deprecated = 33 [default=false];
// The parser stores options it doesn't recognize here. See above.
repeated UninterpretedOption uninterpreted_option = 999;
// Clients can define custom options in extensions of this message. See above.
extensions 1000 to max;
}
// A message representing a option the parser does not recognize. This only
// appears in options protos created by the compiler::Parser class.
// DescriptorPool resolves these when building Descriptor objects. Therefore,
// options protos in descriptor objects (e.g. returned by Descriptor::options(),
// or produced by Descriptor::CopyTo()) will never have UninterpretedOptions
// in them.
message UninterpretedOption {
// The name of the uninterpreted option. Each string represents a segment in
// a dot-separated name. is_extension is true iff a segment represents an
// extension (denoted with parentheses in options specs in .proto files).
// E.g.,{ ["foo", false], ["bar.baz", true], ["qux", false] } represents
// "foo.(bar.baz).qux".
message NamePart {
required string name_part = 1;
required bool is_extension = 2;
}
repeated NamePart name = 2;
// The value of the uninterpreted option, in whatever type the tokenizer
// identified it as during parsing. Exactly one of these should be set.
optional string identifier_value = 3;
optional uint64 positive_int_value = 4;
optional int64 negative_int_value = 5;
optional double double_value = 6;
optional bytes string_value = 7;
optional string aggregate_value = 8;
}
// ===================================================================
// Optional source code info
// Encapsulates information about the original source file from which a
// FileDescriptorProto was generated.
message SourceCodeInfo {
// A Location identifies a piece of source code in a .proto file which
// corresponds to a particular definition. This information is intended
// to be useful to IDEs, code indexers, documentation generators, and similar
// tools.
//
// For example, say we have a file like:
// message Foo {
// optional string foo = 1;
// }
// Let's look at just the field definition:
// optional string foo = 1;
// ^ ^^ ^^ ^ ^^^
// a bc de f ghi
// We have the following locations:
// span path represents
// [a,i) [ 4, 0, 2, 0 ] The whole field definition.
// [a,b) [ 4, 0, 2, 0, 4 ] The label (optional).
// [c,d) [ 4, 0, 2, 0, 5 ] The type (string).
// [e,f) [ 4, 0, 2, 0, 1 ] The name (foo).
// [g,h) [ 4, 0, 2, 0, 3 ] The number (1).
//
// Notes:
// - A location may refer to a repeated field itself (i.e. not to any
// particular index within it). This is used whenever a set of elements are
// logically enclosed in a single code segment. For example, an entire
// extend block (possibly containing multiple extension definitions) will
// have an outer location whose path refers to the "extensions" repeated
// field without an index.
// - Multiple locations may have the same path. This happens when a single
// logical declaration is spread out across multiple places. The most
// obvious example is the "extend" block again -- there may be multiple
// extend blocks in the same scope, each of which will have the same path.
// - A location's span is not always a subset of its parent's span. For
// example, the "extendee" of an extension declaration appears at the
// beginning of the "extend" block and is shared by all extensions within
// the block.
// - Just because a location's span is a subset of some other location's span
// does not mean that it is a descendent. For example, a "group" defines
// both a type and a field in a single declaration. Thus, the locations
// corresponding to the type and field and their components will overlap.
// - Code which tries to interpret locations should probably be designed to
// ignore those that it doesn't understand, as more types of locations could
// be recorded in the future.
repeated Location location = 1;
message Location {
// Identifies which part of the FileDescriptorProto was defined at this
// location.
//
// Each element is a field number or an index. They form a path from
// the root FileDescriptorProto to the place where the definition. For
// example, this path:
// [ 4, 3, 2, 7, 1 ]
// refers to:
// file.message_type(3) // 4, 3
// .field(7) // 2, 7
// .name() // 1
// This is because FileDescriptorProto.message_type has field number 4:
// repeated DescriptorProto message_type = 4;
// and DescriptorProto.field has field number 2:
// repeated FieldDescriptorProto field = 2;
// and FieldDescriptorProto.name has field number 1:
// optional string name = 1;
//
// Thus, the above path gives the location of a field name. If we removed
// the last element:
// [ 4, 3, 2, 7 ]
// this path refers to the whole field declaration (from the beginning
// of the label to the terminating semicolon).
repeated int32 path = 1 [packed=true];
// Always has exactly three or four elements: start line, start column,
// end line (optional, otherwise assumed same as start line), end column.
// These are packed into a single field for efficiency. Note that line
// and column numbers are zero-based -- typically you will want to add
// 1 to each before displaying to a user.
repeated int32 span = 2 [packed=true];
// If this SourceCodeInfo represents a complete declaration, these are any
// comments appearing before and after the declaration which appear to be
// attached to the declaration.
//
// A series of line comments appearing on consecutive lines, with no other
// tokens appearing on those lines, will be treated as a single comment.
//
// Only the comment content is provided; comment markers (e.g. //) are
// stripped out. For block comments, leading whitespace and an asterisk
// will be stripped from the beginning of each line other than the first.
// Newlines are included in the output.
//
// Examples:
//
// optional int32 foo = 1; // Comment attached to foo.
// // Comment attached to bar.
// optional int32 bar = 2;
//
// optional string baz = 3;
// // Comment attached to baz.
// // Another line attached to baz.
//
// // Comment attached to qux.
// //
// // Another line attached to qux.
// optional double qux = 4;
//
// optional string corge = 5;
// /* Block comment attached
// * to corge. Leading asterisks
// * will be removed. */
// /* Block comment attached to
// * grault. */
// optional int32 grault = 6;
optional string leading_comments = 3;
optional string trailing_comments = 4;
}
}

View file

@ -1,72 +0,0 @@
syntax = "proto2";
import "scalapb/scalapb.proto";
// The outer layer handles encryption, authentication and message
// boundaries.
//
// Helper Types
//
// Protobufs don't have fixed-length fields, so these are a hack.
message signature {
option (scalapb.message).extends = "lightning.SignatureToString";
required fixed64 r1 = 1;
required fixed64 r2 = 2;
required fixed64 r3 = 3;
required fixed64 r4 = 4;
required fixed64 s1 = 5;
required fixed64 s2 = 6;
required fixed64 s3 = 7;
required fixed64 s4 = 8;
}
// Pubkey for commitment transaction input.
message bitcoin_pubkey {
option (scalapb.message).extends = "lightning.PubkeyToString";
// Must be 33 bytes.
required bytes key = 1;
}
message route_step {
// Where to next?
oneof next {
// Actually, this is the last one
bool end = 1;
// Next lightning node.
bitcoin_pubkey bitcoin = 2;
// Other realms go here...
}
// How much to forward (difference is fee)
required uint32 amount = 4;
};
message route {
repeated route_step steps = 1;
};
message routing {
required bytes info = 1;
}
//
// Packet Types
//
// Set channel params.
message authenticate {
// Which node this is.
required bitcoin_pubkey node_id = 1;
// Signature of your session key. */
required signature session_sig = 2;
};
// This is the union which defines all of them
message pkt {
oneof pkt {
// Start of connection
authenticate auth = 50;
}
}

View file

@ -1,49 +0,0 @@
syntax = "proto2";
package scalapb;
option java_package = "com.trueaccord.scalapb";
import "google/protobuf/descriptor.proto";
message ScalaPbOptions {
// If set then it overrides the java_package and package.
optional string package_name = 1;
// If true, the compiler does not append the proto base file name
// into the generated package name. If false (the default), the
// generated scala package name is the package_name.basename where
// basename is the proto file name without the .proto extension.
optional bool flat_package = 2;
// Adds the following imports at the top of the file (this is meant
// to provide implicit TypeMappers)
repeated string import = 3;
}
extend google.protobuf.FileOptions {
// File-level optionals for ScalaPB.
// Extension number officially assigned by protobuf-global-extension-registry@google.com
optional ScalaPbOptions options = 1020;
}
message MessageOptions {
// additional classes and traits to mix in to the case class.
repeated string extends = 1;
}
extend google.protobuf.MessageOptions {
// Message-level optionals for ScalaPB.
// Extension number officially assigned by protobuf-global-extension-registry@google.com
optional MessageOptions message = 1020;
}
message FieldOptions {
optional string type = 1;
}
extend google.protobuf.FieldOptions {
// File-level optionals for ScalaPB.
// Extension number officially assigned by protobuf-global-extension-registry@google.com
optional FieldOptions field = 1020;
}

View file

@ -1,107 +0,0 @@
/**
* Created by PM on 18/02/2016.
*/
package lightning
import java.io.{ByteArrayOutputStream, OutputStream}
import java.math.BigInteger
import javax.xml.bind.DatatypeConverter
import com.google.protobuf.ByteString
object ToStrings {
def writeUInt8(input: Long, out: OutputStream): Unit = out.write((input & 0xff).asInstanceOf[Int])
def writeUInt64(input: Long, out: OutputStream): Unit = {
writeUInt8((input) & 0xff, out)
writeUInt8((input >>> 8) & 0xff, out)
writeUInt8((input >>> 16) & 0xff, out)
writeUInt8((input >>> 24) & 0xff, out)
writeUInt8((input >>> 32) & 0xff, out)
writeUInt8((input >>> 40) & 0xff, out)
writeUInt8((input >>> 48) & 0xff, out)
writeUInt8((input >>> 56) & 0xff, out)
}
}
trait Sha256ToString {
// @formatter:off
def a: Long
def b: Long
def c: Long
def d: Long
// @formatter:on
override def toString = {
import ToStrings._
val bos = new ByteArrayOutputStream()
writeUInt64(a, bos)
writeUInt64(b, bos)
writeUInt64(c, bos)
writeUInt64(d, bos)
s"sha256_hash(${DatatypeConverter.printHexBinary(bos.toByteArray)})"
}
}
trait RvalToString {
// @formatter:off
def a: Long
def b: Long
def c: Long
def d: Long
// @formatter:on
override def toString = {
import ToStrings._
val bos = new ByteArrayOutputStream()
writeUInt64(a, bos)
writeUInt64(b, bos)
writeUInt64(c, bos)
writeUInt64(d, bos)
s"rval(${DatatypeConverter.printHexBinary(bos.toByteArray)})"
}
}
trait SignatureToString {
// @formatter:off
def r1: Long
def r2: Long
def r3: Long
def r4: Long
def s1: Long
def s2: Long
def s3: Long
def s4: Long
// @formatter:on
override def toString = {
import ToStrings._
val rbos = new ByteArrayOutputStream()
writeUInt64(r1, rbos)
writeUInt64(r2, rbos)
writeUInt64(r3, rbos)
writeUInt64(r4, rbos)
val r = new BigInteger(1, rbos.toByteArray.reverse)
val sbos = new ByteArrayOutputStream()
writeUInt64(s1, sbos)
writeUInt64(s2, sbos)
writeUInt64(s3, sbos)
writeUInt64(s4, sbos)
val s = new BigInteger(1, sbos.toByteArray.reverse)
s"signature(r=${DatatypeConverter.printHexBinary(r.toByteArray)},s=${DatatypeConverter.printHexBinary(s.toByteArray)})"
}
}
trait PubkeyToString {
def key: ByteString
override def toString = s"bitcoin_pubkey(${DatatypeConverter.printHexBinary(key.toByteArray)})"
}

View file

@ -1,17 +0,0 @@
package lightning
import org.junit.runner.RunWith
import org.scalatest.FunSuite
import org.scalatest.junit.JUnitRunner
/**
* Created by PM on 06/07/2016.
*/
@RunWith(classOf[JUnitRunner])
class NonRegSpec extends FunSuite {
test("check signature ToString extensions") {
val sig = signature(1, 2, 3, 4, 5, 6, 7, 8)
assert(sig.isInstanceOf[SignatureToString])
}
}

View file

@ -8,7 +8,6 @@
<packaging>pom</packaging>
<modules>
<module>lightning-types</module>
<module>eclair-node</module>
</modules>
@ -49,7 +48,6 @@
<acinqtools.version>1.2</acinqtools.version>
<akka.version>2.4.12</akka.version>
<bitcoinlib.version>0.9.9</bitcoinlib.version>
<scalapb.version>0.4.21</scalapb.version>
</properties>
<build>

View file

@ -1,16 +1,4 @@
pushd .
# protobuf 2.6.1
cd
wget https://github.com/google/protobuf/releases/download/v2.6.1/protobuf-2.6.1.tar.bz2
tar xjvf protobuf-2.6.1.tar.bz2
cd protobuf-2.6.1
./autogen.sh && ./configure && make && sudo make install
# protobuf-c
cd
export LD_LIBRARY_PATH=/usr/local/lib/
git clone https://github.com/protobuf-c/protobuf-c.git
cd protobuf-c
./autogen.sh && ./configure && make && sudo make install
# lightning deps
sudo add-apt-repository -y ppa:chris-lea/libsodium
sudo apt-get update