Compare commits
2 Commits
applicatio
...
wip-textui
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d50889ad7c | ||
|
|
eb34134830 |
@ -299,11 +299,12 @@ class Channel(val nodeParams: NodeParams, remoteNodeId: PublicKey, blockchain: A
|
||||
blockchain ! WatchSpent(self, commitInput.outPoint.txid, commitInput.outPoint.index.toInt, BITCOIN_FUNDING_SPENT) // TODO: should we wait for an acknowledgment from the watcher?
|
||||
blockchain ! WatchConfirmed(self, commitInput.outPoint.txid, nodeParams.minDepthBlocks, BITCOIN_FUNDING_DEPTHOK)
|
||||
|
||||
|
||||
val commitments = Commitments(localParams, remoteParams,
|
||||
LocalCommit(0, localSpec, PublishableTxs(signedLocalCommitTx, Nil), null), RemoteCommit(0, remoteSpec, remoteCommitTx.tx.txid, remoteFirstPerCommitmentPoint),
|
||||
LocalCommit(0, localSpec, PublishableTxs(signedLocalCommitTx, Nil)), RemoteCommit(0, remoteSpec, remoteCommitTx.tx.txid, remoteFirstPerCommitmentPoint),
|
||||
LocalChanges(Nil, Nil, Nil), RemoteChanges(Nil, Nil, Nil),
|
||||
localNextHtlcId = 0L, remoteNextHtlcId = 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,
|
||||
remoteNextCommitInfo = Right(randomKey.publicKey), // TODO: we will receive their next per-commitment point in the next message, so we temporarily put an empty byte array,
|
||||
unackedMessages = Nil,
|
||||
commitInput, ShaChain.init, channelId = channelId)
|
||||
context.parent ! ChannelIdAssigned(self, temporaryChannelId, channelId) // we notify the peer asap so it knows how to route messages
|
||||
@ -337,10 +338,10 @@ class Channel(val nodeParams: NodeParams, remoteNodeId: PublicKey, blockchain: A
|
||||
blockchain ! PublishAsap(fundingTx)
|
||||
|
||||
val commitments = Commitments(localParams, remoteParams,
|
||||
LocalCommit(0, localSpec, PublishableTxs(signedLocalCommitTx, Nil), null), remoteCommit,
|
||||
LocalCommit(0, localSpec, PublishableTxs(signedLocalCommitTx, Nil)), remoteCommit,
|
||||
LocalChanges(Nil, Nil, Nil), RemoteChanges(Nil, Nil, Nil),
|
||||
localNextHtlcId = 0L, remoteNextHtlcId = 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
|
||||
remoteNextCommitInfo = Right(randomKey.publicKey), // TODO: we will receive their next per-commitment point in the next message, so we temporarily put an empty byte array
|
||||
unackedMessages = Nil,
|
||||
commitInput, ShaChain.init, channelId = channelId)
|
||||
context.system.eventStream.publish(ChannelSignatureReceived(self, commitments))
|
||||
|
||||
@ -126,9 +126,9 @@ trait HasCommitments extends Data {
|
||||
def channelId = commitments.channelId
|
||||
}
|
||||
|
||||
case class LocalCommitPublished(commitTx: Transaction, claimMainDelayedOutputTx: Option[Transaction], htlcSuccessTxs: Seq[Transaction], htlcTimeoutTxs: Seq[Transaction], claimHtlcDelayedTx: Seq[Transaction])
|
||||
case class RemoteCommitPublished(commitTx: Transaction, claimMainOutputTx: Option[Transaction], claimHtlcSuccessTxs: Seq[Transaction], claimHtlcTimeoutTxs: Seq[Transaction])
|
||||
case class RevokedCommitPublished(commitTx: Transaction, claimMainOutputTx: Option[Transaction], mainPenaltyTx: Option[Transaction], claimHtlcTimeoutTxs: Seq[Transaction], htlcTimeoutTxs: Seq[Transaction], htlcPenaltyTxs: Seq[Transaction])
|
||||
case class LocalCommitPublished(commitTx: Transaction, claimMainDelayedOutputTx: Option[Transaction], htlcSuccessTxs: List[Transaction], htlcTimeoutTxs: List[Transaction], claimHtlcDelayedTx: List[Transaction])
|
||||
case class RemoteCommitPublished(commitTx: Transaction, claimMainOutputTx: Option[Transaction], claimHtlcSuccessTxs: List[Transaction], claimHtlcTimeoutTxs: List[Transaction])
|
||||
case class RevokedCommitPublished(commitTx: Transaction, claimMainOutputTx: Option[Transaction], mainPenaltyTx: Option[Transaction], claimHtlcTimeoutTxs: List[Transaction], htlcTimeoutTxs: List[Transaction], htlcPenaltyTxs: List[Transaction])
|
||||
|
||||
final case class DATA_WAIT_FOR_OPEN_CHANNEL(initFundee: INPUT_INIT_FUNDEE) extends Data
|
||||
final case class DATA_WAIT_FOR_ACCEPT_CHANNEL(initFunder: INPUT_INIT_FUNDER, lastSent: OpenChannel) extends Data
|
||||
@ -148,7 +148,7 @@ final case class DATA_CLOSING(commitments: Commitments,
|
||||
localCommitPublished: Option[LocalCommitPublished] = None,
|
||||
remoteCommitPublished: Option[RemoteCommitPublished] = None,
|
||||
nextRemoteCommitPublished: Option[RemoteCommitPublished] = None,
|
||||
revokedCommitPublished: Seq[RevokedCommitPublished] = Nil) extends Data with HasCommitments {
|
||||
revokedCommitPublished: List[RevokedCommitPublished] = Nil) extends Data with HasCommitments {
|
||||
require(mutualClosePublished.isDefined || localCommitPublished.isDefined || remoteCommitPublished.isDefined || nextRemoteCommitPublished.isDefined || revokedCommitPublished.size > 0, "there should be at least one tx published in this state")
|
||||
}
|
||||
|
||||
|
||||
@ -16,8 +16,8 @@ case class LocalChanges(proposed: List[UpdateMessage], signed: List[UpdateMessag
|
||||
case class RemoteChanges(proposed: List[UpdateMessage], acked: List[UpdateMessage], signed: List[UpdateMessage])
|
||||
case class Changes(ourChanges: LocalChanges, theirChanges: RemoteChanges)
|
||||
case class HtlcTxAndSigs(txinfo: TransactionWithInputInfo, localSig: BinaryData, remoteSig: BinaryData)
|
||||
case class PublishableTxs(commitTx: CommitTx, htlcTxsAndSigs: Seq[HtlcTxAndSigs])
|
||||
case class LocalCommit(index: Long, spec: CommitmentSpec, publishableTxs: PublishableTxs, commit: CommitSig)
|
||||
case class PublishableTxs(commitTx: CommitTx, htlcTxsAndSigs: List[HtlcTxAndSigs])
|
||||
case class LocalCommit(index: Long, spec: CommitmentSpec, publishableTxs: PublishableTxs/*, commit: CommitSig*/)
|
||||
case class RemoteCommit(index: Long, spec: CommitmentSpec, txid: BinaryData, remotePerCommitmentPoint: Point)
|
||||
case class WaitingForRevocation(nextRemoteCommit: RemoteCommit, sent: CommitSig, reSignAsap: Boolean = false)
|
||||
// @formatter:on
|
||||
@ -35,7 +35,7 @@ case class Commitments(localParams: LocalParams, remoteParams: RemoteParams,
|
||||
localChanges: LocalChanges, remoteChanges: RemoteChanges,
|
||||
localNextHtlcId: Long, remoteNextHtlcId: Long,
|
||||
remoteNextCommitInfo: Either[WaitingForRevocation, Point],
|
||||
unackedMessages: Seq[LightningMessage],
|
||||
unackedMessages: List[LightningMessage],
|
||||
commitInput: InputInfo,
|
||||
remotePerCommitmentSecrets: ShaChain, channelId: BinaryData) {
|
||||
|
||||
@ -359,7 +359,9 @@ object Commitments extends Logging {
|
||||
}
|
||||
}
|
||||
|
||||
def isOldCommit(commitments: Commitments, commit: CommitSig): Boolean = commitments.localCommit.commit == commit
|
||||
def isOldCommit(commitments: Commitments, commit: CommitSig): Boolean =
|
||||
// TODO: FUGLY!!!!!
|
||||
Transaction.write(commitments.localCommit.publishableTxs.commitTx.tx).toString().contains(commit.signature.toString())
|
||||
|
||||
def receiveCommit(commitments: Commitments, commit: CommitSig): Either[Commitments, (Commitments, RevokeAndAck)] =
|
||||
isOldCommit(commitments, commit) match {
|
||||
@ -424,8 +426,7 @@ object Commitments extends Logging {
|
||||
val ourCommit1 = LocalCommit(
|
||||
index = localCommit.index + 1,
|
||||
spec,
|
||||
publishableTxs = PublishableTxs(signedCommitTx, htlcTxsAndSigs),
|
||||
commit = commit)
|
||||
publishableTxs = PublishableTxs(signedCommitTx, htlcTxsAndSigs))
|
||||
val ourChanges1 = localChanges.copy(acked = Nil)
|
||||
val theirChanges1 = remoteChanges.copy(proposed = Nil, acked = remoteChanges.acked ++ remoteChanges.proposed)
|
||||
// they have received our previous revocation (otherwise they wouldn't have sent a commit)
|
||||
|
||||
@ -379,8 +379,8 @@ object Helpers {
|
||||
RemoteCommitPublished(
|
||||
commitTx = tx,
|
||||
claimMainOutputTx = mainTx.map(_.tx),
|
||||
claimHtlcSuccessTxs = txes.collect { case c: ClaimHtlcSuccessTx => c.tx },
|
||||
claimHtlcTimeoutTxs = txes.collect { case c: ClaimHtlcTimeoutTx => c.tx }
|
||||
claimHtlcSuccessTxs = txes.toList.collect { case c: ClaimHtlcSuccessTx => c.tx },
|
||||
claimHtlcTimeoutTxs = txes.toList.collect { case c: ClaimHtlcTimeoutTx => c.tx }
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
@ -8,10 +8,11 @@ import akka.util.ByteString
|
||||
import fr.acinq.bitcoin.Crypto.PublicKey
|
||||
import fr.acinq.bitcoin.{BinaryData, Protocol}
|
||||
import fr.acinq.eclair.crypto.Noise._
|
||||
import scodec.bits.BitVector
|
||||
import scodec.{Attempt, Codec, DecodeResult}
|
||||
|
||||
import scala.annotation.tailrec
|
||||
import scala.reflect.ClassTag
|
||||
import scala.util.{Failure, Success, Try}
|
||||
|
||||
/**
|
||||
* see BOLT #8
|
||||
@ -22,11 +23,11 @@ import scala.util.{Failure, Success, Try}
|
||||
* Once the initial handshake has been completed successfully, the handler will create a listener actor with the
|
||||
* provided factory, and will forward it all decrypted messages
|
||||
*
|
||||
* @param keyPair private/public key pair for this node
|
||||
* @param rs remote node static public key (which must be known before we initiate communication)
|
||||
* @param connection actor that represents the other node's
|
||||
* @param keyPair private/public key pair for this node
|
||||
* @param rs remote node static public key (which must be known before we initiate communication)
|
||||
* @param connection actor that represents the other node's
|
||||
*/
|
||||
class TransportHandler[T: ClassTag](keyPair: KeyPair, rs: Option[BinaryData], connection: ActorRef, serializer: TransportHandler.Serializer[T]) extends Actor with FSM[TransportHandler.State, TransportHandler.Data] {
|
||||
class TransportHandler[T: ClassTag](keyPair: KeyPair, rs: Option[BinaryData], connection: ActorRef, codec: Codec[T]) extends Actor with FSM[TransportHandler.State, TransportHandler.Data] {
|
||||
|
||||
import TransportHandler._
|
||||
|
||||
@ -49,10 +50,9 @@ class TransportHandler[T: ClassTag](keyPair: KeyPair, rs: Option[BinaryData], co
|
||||
|
||||
def sendToListener(listener: ActorRef, plaintextMessages: Seq[BinaryData]) = {
|
||||
plaintextMessages.map(plaintext => {
|
||||
Try(serializer.deserialize(plaintext)) match {
|
||||
case Success(message) => listener ! message
|
||||
case Failure(t) =>
|
||||
log.error(t, s"cannot deserialize $plaintext")
|
||||
codec.decode(BitVector(plaintext.data)) match {
|
||||
case Attempt.Successful(DecodeResult(message, _)) => listener ! message
|
||||
case Attempt.Failure(err) => log.error(s"cannot deserialize $plaintext: $err")
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -118,7 +118,7 @@ class TransportHandler[T: ClassTag](keyPair: KeyPair, rs: Option[BinaryData], co
|
||||
stay using nextStateData
|
||||
|
||||
case Event(t: T, WaitingForCyphertextData(enc, dec, length, buffer, listener)) =>
|
||||
val blob = serializer.serialize(t)
|
||||
val blob = codec.encode(t).require.toByteArray
|
||||
val (enc1, ciphertext) = TransportHandler.encrypt(enc, blob)
|
||||
connection ! Write(ByteString.fromArray(ciphertext.toArray))
|
||||
stay using WaitingForCyphertextData(enc1, dec, length, buffer, listener)
|
||||
@ -210,6 +210,7 @@ object TransportHandler {
|
||||
// @formatter:on
|
||||
|
||||
case class Listener(listener: ActorRef)
|
||||
|
||||
case class HandshakeCompleted(transport: ActorRef, remoteNodeId: PublicKey)
|
||||
|
||||
sealed trait Data
|
||||
@ -283,16 +284,4 @@ object TransportHandler {
|
||||
}
|
||||
}
|
||||
|
||||
trait Serializer[T] {
|
||||
def serialize(t: T): BinaryData
|
||||
|
||||
def deserialize(bin: BinaryData): T
|
||||
}
|
||||
|
||||
object Noop extends Serializer[BinaryData] {
|
||||
override def serialize(t: BinaryData): BinaryData = t
|
||||
|
||||
override def deserialize(bin: BinaryData): BinaryData = bin
|
||||
}
|
||||
|
||||
}
|
||||
@ -3,9 +3,8 @@ package fr.acinq.eclair.db
|
||||
import fr.acinq.bitcoin.BinaryData
|
||||
import fr.acinq.bitcoin.Crypto.PublicKey
|
||||
import fr.acinq.eclair.channel.Data
|
||||
import fr.acinq.eclair.crypto.TransportHandler.Serializer
|
||||
import fr.acinq.eclair.io.{LightningMessageSerializer, PeerRecord}
|
||||
import fr.acinq.eclair.wire.LightningMessage
|
||||
import fr.acinq.eclair.io.PeerRecord
|
||||
import fr.acinq.eclair.wire.{ChannelCodecs, LightningMessage, LightningMessageCodecs}
|
||||
|
||||
/**
|
||||
* Created by PM on 28/02/2017.
|
||||
@ -20,11 +19,7 @@ object Dbs {
|
||||
new SimpleTypedDb[BinaryData, Data](
|
||||
channelid2String,
|
||||
string2channelid,
|
||||
new Serializer[Data] {
|
||||
override def serialize(t: Data): BinaryData = JavaSerializer.serialize(t)
|
||||
|
||||
override def deserialize(bin: BinaryData): Data = JavaSerializer.deserialize[Data](bin)
|
||||
},
|
||||
ChannelCodecs.stateDataCodec,
|
||||
db
|
||||
)
|
||||
}
|
||||
@ -34,7 +29,7 @@ object Dbs {
|
||||
new SimpleTypedDb[String, LightningMessage](
|
||||
s => s,
|
||||
s => if (s.startsWith("ann-")) Some(s) else None,
|
||||
LightningMessageSerializer,
|
||||
LightningMessageCodecs.lightningMessageCodec,
|
||||
db
|
||||
)
|
||||
}
|
||||
@ -47,11 +42,7 @@ object Dbs {
|
||||
new SimpleTypedDb[PublicKey, PeerRecord](
|
||||
peerid2String,
|
||||
string2peerid,
|
||||
new Serializer[PeerRecord] {
|
||||
override def serialize(t: PeerRecord): BinaryData = JavaSerializer.serialize(t)
|
||||
|
||||
override def deserialize(bin: BinaryData): PeerRecord = JavaSerializer.deserialize[PeerRecord](bin)
|
||||
},
|
||||
ChannelCodecs.peerRecordCodec,
|
||||
db
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,24 +0,0 @@
|
||||
package fr.acinq.eclair.db
|
||||
|
||||
import java.io.{ByteArrayInputStream, ByteArrayOutputStream, ObjectInputStream, ObjectOutputStream}
|
||||
|
||||
import fr.acinq.bitcoin.BinaryData
|
||||
|
||||
|
||||
/**
|
||||
* Created by fabrice on 17/02/17.
|
||||
*/
|
||||
object JavaSerializer {
|
||||
def serialize[T](cs: T): BinaryData = {
|
||||
val bos = new ByteArrayOutputStream()
|
||||
val oos = new ObjectOutputStream(bos)
|
||||
oos.writeObject(cs)
|
||||
bos.toByteArray
|
||||
}
|
||||
|
||||
def deserialize[T](input: BinaryData): T = {
|
||||
val bis = new ByteArrayInputStream(input)
|
||||
val osi = new ObjectInputStream(bis)
|
||||
osi.readObject().asInstanceOf[T]
|
||||
}
|
||||
}
|
||||
@ -1,7 +1,9 @@
|
||||
package fr.acinq.eclair.db
|
||||
|
||||
import fr.acinq.bitcoin.BinaryData
|
||||
import fr.acinq.eclair.crypto.TransportHandler
|
||||
import scodec.Codec
|
||||
import scodec.bits.BitVector
|
||||
|
||||
|
||||
/**
|
||||
* Created by fabrice on 25/02/17.
|
||||
@ -17,10 +19,10 @@ trait SimpleDb {
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
class SimpleTypedDb[K, V](id2string: K => String, string2id: String => Option[K], serializer: TransportHandler.Serializer[V], db: SimpleDb) {
|
||||
class SimpleTypedDb[K, V](id2string: K => String, string2id: String => Option[K], codec: Codec[V], db: SimpleDb) {
|
||||
// @formatter:off
|
||||
def put(k: K, v: V) = db.put(id2string(k), serializer.serialize(v))
|
||||
def get(k: K): Option[V] = db.get(id2string(k)).map(serializer.deserialize)
|
||||
def put(k: K, v: V) = db.put(id2string(k), codec.encode(v).require.toByteArray)
|
||||
def get(k: K): Option[V] = db.get(id2string(k)).map(bin => codec.decodeValue(BitVector(bin.data)).require)
|
||||
def delete(k: K) : Boolean = db.delete(id2string(k))
|
||||
def keys: Seq[K] = db.keys.map(string2id).flatten
|
||||
def values: Seq[V] = keys.map(get).flatten
|
||||
|
||||
@ -10,7 +10,7 @@ import fr.acinq.eclair.{Globals, NodeParams}
|
||||
import fr.acinq.eclair.crypto.Noise.KeyPair
|
||||
import fr.acinq.eclair.crypto.TransportHandler
|
||||
import fr.acinq.eclair.crypto.TransportHandler.HandshakeCompleted
|
||||
import fr.acinq.eclair.wire.LightningMessage
|
||||
import fr.acinq.eclair.wire.{LightningMessage, LightningMessageCodecs}
|
||||
|
||||
/**
|
||||
* Created by PM on 27/10/2015.
|
||||
@ -35,7 +35,7 @@ class Client(nodeParams: NodeParams, switchboard: ActorRef, address: InetSocketA
|
||||
KeyPair(nodeParams.privateKey.publicKey.toBin, nodeParams.privateKey.toBin),
|
||||
Some(remoteNodeId),
|
||||
connection = connection,
|
||||
serializer = LightningMessageSerializer)))
|
||||
codec = LightningMessageCodecs.lightningMessageCodec)))
|
||||
context watch transport
|
||||
context become authenticating(transport)
|
||||
}
|
||||
|
||||
@ -1,25 +0,0 @@
|
||||
package fr.acinq.eclair.io
|
||||
|
||||
import fr.acinq.bitcoin.BinaryData
|
||||
import fr.acinq.eclair.crypto.TransportHandler
|
||||
import fr.acinq.eclair.wire.{LightningMessageCodecs, LightningMessage}
|
||||
import scodec.bits.BitVector
|
||||
import scodec.{Attempt, DecodeResult}
|
||||
|
||||
/**
|
||||
* Created by fabrice on 16/01/17.
|
||||
*/
|
||||
object LightningMessageSerializer extends TransportHandler.Serializer[LightningMessage] {
|
||||
|
||||
override def serialize(t: LightningMessage): BinaryData =
|
||||
LightningMessageCodecs.lightningMessageCodec.encode(t) match {
|
||||
case Attempt.Successful(bitVector) => BinaryData(bitVector.toByteArray)
|
||||
case Attempt.Failure(cause) => throw new RuntimeException(s"serialization error: $cause")
|
||||
}
|
||||
|
||||
override def deserialize(bin: BinaryData): LightningMessage =
|
||||
LightningMessageCodecs.lightningMessageCodec.decode(BitVector(bin.data)) match {
|
||||
case Attempt.Successful(DecodeResult(msg, _)) => msg
|
||||
case Attempt.Failure(cause) => throw new RuntimeException(s"deserialization error: $cause")
|
||||
}
|
||||
}
|
||||
@ -9,7 +9,7 @@ import fr.acinq.eclair.NodeParams
|
||||
import fr.acinq.eclair.crypto.Noise.KeyPair
|
||||
import fr.acinq.eclair.crypto.TransportHandler
|
||||
import fr.acinq.eclair.crypto.TransportHandler.HandshakeCompleted
|
||||
import fr.acinq.eclair.wire.LightningMessage
|
||||
import fr.acinq.eclair.wire.{LightningMessage, LightningMessageCodecs}
|
||||
|
||||
import scala.concurrent.Promise
|
||||
|
||||
@ -40,7 +40,7 @@ class Server(nodeParams: NodeParams, switchboard: ActorRef, address: InetSocketA
|
||||
KeyPair(nodeParams.privateKey.publicKey.toBin, nodeParams.privateKey.toBin),
|
||||
None,
|
||||
connection = connection,
|
||||
serializer = LightningMessageSerializer)))
|
||||
codec = LightningMessageCodecs.lightningMessageCodec)))
|
||||
|
||||
case h: HandshakeCompleted =>
|
||||
log.info(s"handshake completed with ${h.remoteNodeId}")
|
||||
|
||||
@ -0,0 +1,213 @@
|
||||
package fr.acinq.eclair.wire
|
||||
|
||||
import fr.acinq.bitcoin.{OutPoint, Transaction, TxOut}
|
||||
import fr.acinq.eclair.channel._
|
||||
import fr.acinq.eclair.crypto.ShaChain
|
||||
import fr.acinq.eclair.io.PeerRecord
|
||||
import fr.acinq.eclair.transactions.Transactions._
|
||||
import fr.acinq.eclair.transactions._
|
||||
import fr.acinq.eclair.wire.LightningMessageCodecs._
|
||||
import scodec.bits.{BitVector, ByteVector}
|
||||
import scodec.codecs._
|
||||
import scodec.{Attempt, Codec}
|
||||
|
||||
/**
|
||||
* Created by PM on 02/06/2017.
|
||||
*/
|
||||
object ChannelCodecs {
|
||||
|
||||
val localParamsCodec: Codec[LocalParams] = (
|
||||
("nodeId" | publicKey) ::
|
||||
("dustLimitSatoshis" | uint64) ::
|
||||
("maxHtlcValueInFlightMsat" | uint64) ::
|
||||
("channelReserveSatoshis" | uint64) ::
|
||||
("htlcMinimumMsat" | uint32) ::
|
||||
("toSelfDelay" | uint16) ::
|
||||
("maxAcceptedHtlcs" | uint16) ::
|
||||
("fundingPrivKey" | privateKey) ::
|
||||
("revocationSecret" | scalar) ::
|
||||
("paymentKey" | privateKey) ::
|
||||
("delayedPaymentKey" | scalar) ::
|
||||
("defaultFinalScriptPubKey" | varsizebinarydata) ::
|
||||
("shaSeed" | varsizebinarydata) ::
|
||||
("isFunder" | bool) ::
|
||||
("globalFeatures" | varsizebinarydata) ::
|
||||
("localFeatures" | varsizebinarydata)).as[LocalParams]
|
||||
|
||||
val remoteParamsCodec: Codec[RemoteParams] = (
|
||||
("nodeId" | publicKey) ::
|
||||
("dustLimitSatoshis" | uint64) ::
|
||||
("maxHtlcValueInFlightMsat" | uint64) ::
|
||||
("channelReserveSatoshis" | uint64) ::
|
||||
("htlcMinimumMsat" | uint32) ::
|
||||
("toSelfDelay" | uint16) ::
|
||||
("maxAcceptedHtlcs" | uint16) ::
|
||||
("fundingPubKey" | publicKey) ::
|
||||
("revocationBasepoint" | point) ::
|
||||
("paymentBasepoint" | point) ::
|
||||
("delayedPaymentBasepoint" | point) ::
|
||||
("globalFeatures" | varsizebinarydata) ::
|
||||
("localFeatures" | varsizebinarydata)).as[RemoteParams]
|
||||
|
||||
val directionCodec: Codec[Direction] = Codec[Direction](
|
||||
(dir: Direction) => bool.encode(dir == IN),
|
||||
(wire: BitVector) => bool.decode(wire).map(_.map(b => if (b) IN else OUT))
|
||||
)
|
||||
|
||||
val htlcCodec: Codec[Htlc] = (
|
||||
("direction" | directionCodec) ::
|
||||
("add" | updateAddHtlcCodec) ::
|
||||
("previousChannelId" | optional(bool, varsizebinarydata))).as[Htlc]
|
||||
|
||||
def setCodec[T](codec: Codec[T]): Codec[Set[T]] = Codec[Set[T]](
|
||||
(elems: Set[T]) => listOfN(uint16, codec).encode(elems.toList),
|
||||
(wire: BitVector) => listOfN(uint16, codec).decode(wire).map(_.map(_.toSet))
|
||||
)
|
||||
|
||||
val commitmentSpecCodec: Codec[CommitmentSpec] = (
|
||||
("htlcs" | setCodec(htlcCodec)) ::
|
||||
("feeratePerKw" | uint32) ::
|
||||
("toLocalMsat" | uint64) ::
|
||||
("toRemoteMsat" | uint64)).as[CommitmentSpec]
|
||||
|
||||
def outPointCodec: Codec[OutPoint] = variableSizeBytes(uint16, bytes.xmap(d => OutPoint.read(d.toArray), d => ByteVector(OutPoint.write(d).data)))
|
||||
|
||||
def txOutCodec: Codec[TxOut] = variableSizeBytes(uint16, bytes.xmap(d => TxOut.read(d.toArray), d => ByteVector(TxOut.write(d).data)))
|
||||
|
||||
def txCodec: Codec[Transaction] = variableSizeBytes(uint16, bytes.xmap(d => Transaction.read(d.toArray), d => ByteVector(Transaction.write(d).data)))
|
||||
|
||||
val inputInfoCodec: Codec[InputInfo] = (
|
||||
("outPoint" | outPointCodec) ::
|
||||
("txOut" | txOutCodec) ::
|
||||
("redeemScript" | varsizebinarydata)).as[InputInfo]
|
||||
|
||||
val txWithInputInfoCodec: Codec[TransactionWithInputInfo] = discriminated[TransactionWithInputInfo].by(uint16)
|
||||
.typecase(0x01, (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[CommitTx])
|
||||
.typecase(0x02, (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec) :: ("paymentHash" | binarydata(32))).as[HtlcSuccessTx])
|
||||
.typecase(0x03, (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[HtlcTimeoutTx])
|
||||
.typecase(0x04, (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[ClaimHtlcSuccessTx])
|
||||
.typecase(0x05, (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[ClaimHtlcTimeoutTx])
|
||||
.typecase(0x06, (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[ClaimP2WPKHOutputTx])
|
||||
.typecase(0x07, (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[ClaimDelayedOutputTx])
|
||||
.typecase(0x08, (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[MainPenaltyTx])
|
||||
.typecase(0x09, (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[HtlcPenaltyTx])
|
||||
.typecase(0x10, (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[ClosingTx])
|
||||
|
||||
val htlcTxAndSigsCodec: Codec[HtlcTxAndSigs] = (
|
||||
("txinfo" | txWithInputInfoCodec) ::
|
||||
("localSig" | varsizebinarydata) ::
|
||||
("remoteSig" | varsizebinarydata)).as[HtlcTxAndSigs]
|
||||
|
||||
val publishableTxsCodec: Codec[PublishableTxs] = (
|
||||
("commitTx" | (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[CommitTx]) ::
|
||||
("htlcTxsAndSigs" | listOfN(uint16, htlcTxAndSigsCodec))).as[PublishableTxs]
|
||||
|
||||
val localCommitCodec: Codec[LocalCommit] = (
|
||||
("index" | uint64) ::
|
||||
("spec" | commitmentSpecCodec) ::
|
||||
("publishableTxs" | publishableTxsCodec)).as[LocalCommit]
|
||||
|
||||
val remoteCommitCodec: Codec[RemoteCommit] = (
|
||||
("index" | uint64) ::
|
||||
("spec" | commitmentSpecCodec) ::
|
||||
("txid" | binarydata(32)) ::
|
||||
("remotePerCommitmentPoint" | point)).as[RemoteCommit]
|
||||
|
||||
val updateMessageCodec: Codec[UpdateMessage] = lightningMessageCodec.narrow(f => Attempt.successful(f.asInstanceOf[UpdateMessage]), g => g)
|
||||
|
||||
val localChangesCodec: Codec[LocalChanges] = (
|
||||
("proposed" | listOfN(uint16, updateMessageCodec)) ::
|
||||
("signed" | listOfN(uint16, updateMessageCodec)) ::
|
||||
("acked" | listOfN(uint16, updateMessageCodec))).as[LocalChanges]
|
||||
|
||||
val remoteChangesCodec: Codec[RemoteChanges] = (
|
||||
("proposed" | listOfN(uint16, updateMessageCodec)) ::
|
||||
("acked" | listOfN(uint16, updateMessageCodec)) ::
|
||||
("signed" | listOfN(uint16, updateMessageCodec))).as[RemoteChanges]
|
||||
|
||||
val waitingForRevocationCodec: Codec[WaitingForRevocation] = (
|
||||
("nextRemoteCommit" | remoteCommitCodec) ::
|
||||
("sent" | commitSigCodec) ::
|
||||
("reSignAsap" | bool)).as[WaitingForRevocation]
|
||||
|
||||
val commitmentsCodec: Codec[Commitments] = (
|
||||
("localParams" | localParamsCodec) ::
|
||||
("remoteParams" | remoteParamsCodec) ::
|
||||
("localCommit" | localCommitCodec) ::
|
||||
("remoteCommit" | remoteCommitCodec) ::
|
||||
("localChanges" | localChangesCodec) ::
|
||||
("remoteChanges" | remoteChangesCodec) ::
|
||||
("localNextHtlcId" | uint64) ::
|
||||
("remoteNextHtlcId" | uint64) ::
|
||||
("remoteNextCommitInfo" | either(bool, waitingForRevocationCodec, point)) ::
|
||||
("unackedMessages" | listOfN(uint16, lightningMessageCodec)) ::
|
||||
("commitInput" | inputInfoCodec) ::
|
||||
("remotePerCommitmentSecrets" | ShaChain.shaChainCodec) ::
|
||||
("channelId" | binarydata(32))).as[Commitments]
|
||||
|
||||
val localCommitPublishedCodec: Codec[LocalCommitPublished] = (
|
||||
("commitTx" | txCodec) ::
|
||||
("claimMainDelayedOutputTx" | optional(bool, txCodec)) ::
|
||||
("htlcSuccessTxs" | listOfN(uint16, txCodec)) ::
|
||||
("htlcTimeoutTxs" | listOfN(uint16, txCodec)) ::
|
||||
("claimHtlcDelayedTx" | listOfN(uint16, txCodec))).as[LocalCommitPublished]
|
||||
|
||||
val remoteCommitPublishedCodec: Codec[RemoteCommitPublished] = (
|
||||
("commitTx" | txCodec) ::
|
||||
("claimMainOutputTx" | optional(bool, txCodec)) ::
|
||||
("claimHtlcSuccessTxs" | listOfN(uint16, txCodec)) ::
|
||||
("claimHtlcTimeoutTxs" | listOfN(uint16, txCodec))).as[RemoteCommitPublished]
|
||||
|
||||
val revokedCommitPublishedCodec: Codec[RevokedCommitPublished] = (
|
||||
("commitTx" | txCodec) ::
|
||||
("claimMainOutputTx" | optional(bool, txCodec)) ::
|
||||
("mainPenaltyTx" | optional(bool, txCodec)) ::
|
||||
("claimHtlcTimeoutTxs" | listOfN(uint16, txCodec)) ::
|
||||
("htlcTimeoutTxs" | listOfN(uint16, txCodec)) ::
|
||||
("htlcPenaltyTxs" | listOfN(uint16, txCodec))).as[RevokedCommitPublished]
|
||||
|
||||
val DATA_WAIT_FOR_FUNDING_CONFIRMED_Codec: Codec[DATA_WAIT_FOR_FUNDING_CONFIRMED] = (
|
||||
("commitments" | commitmentsCodec) ::
|
||||
("deferred" | optional(bool, fundingLockedCodec)) ::
|
||||
("lastSent" | either(bool, fundingCreatedCodec, fundingSignedCodec))).as[DATA_WAIT_FOR_FUNDING_CONFIRMED]
|
||||
|
||||
val DATA_WAIT_FOR_FUNDING_LOCKED_Codec: Codec[DATA_WAIT_FOR_FUNDING_LOCKED] = (
|
||||
("commitments" | commitmentsCodec) ::
|
||||
("lastSent" | fundingLockedCodec)).as[DATA_WAIT_FOR_FUNDING_LOCKED]
|
||||
|
||||
val DATA_NORMAL_Codec: Codec[DATA_NORMAL] = (
|
||||
("commitments" | commitmentsCodec) ::
|
||||
("shortChannelId" | optional(bool, uint64))).as[DATA_NORMAL]
|
||||
|
||||
val DATA_SHUTDOWN_Codec: Codec[DATA_SHUTDOWN] = (
|
||||
("commitments" | commitmentsCodec) ::
|
||||
("localShutdown" | shutdownCodec) ::
|
||||
("remoteShutdown" | shutdownCodec)).as[DATA_SHUTDOWN]
|
||||
|
||||
val DATA_NEGOTIATING_Codec: Codec[DATA_NEGOTIATING] = (
|
||||
("commitments" | commitmentsCodec) ::
|
||||
("localShutdown" | shutdownCodec) ::
|
||||
("remoteShutdown" | shutdownCodec) ::
|
||||
("localClosingSigned" | closingSignedCodec)).as[DATA_NEGOTIATING]
|
||||
|
||||
val DATA_CLOSING_Codec: Codec[DATA_CLOSING] = (
|
||||
("commitments" | commitmentsCodec) ::
|
||||
("mutualClosePublished" | optional(bool, txCodec)) ::
|
||||
("localCommitPublished" | optional(bool, localCommitPublishedCodec)) ::
|
||||
("remoteCommitPublished" | optional(bool, remoteCommitPublishedCodec)) ::
|
||||
("nextRemoteCommitPublished" | optional(bool, remoteCommitPublishedCodec)) ::
|
||||
("revokedCommitPublished" | listOfN(uint16, revokedCommitPublishedCodec))).as[DATA_CLOSING]
|
||||
|
||||
val stateDataCodec: Codec[Data] = ("version" | constant(0x00)) ~> discriminated[Data].by(uint16)
|
||||
.typecase(0x01, DATA_WAIT_FOR_FUNDING_CONFIRMED_Codec)
|
||||
.typecase(0x02, DATA_WAIT_FOR_FUNDING_LOCKED_Codec)
|
||||
.typecase(0x03, DATA_NORMAL_Codec)
|
||||
.typecase(0x04, DATA_SHUTDOWN_Codec)
|
||||
.typecase(0x05, DATA_NEGOTIATING_Codec)
|
||||
.typecase(0x06, DATA_CLOSING_Codec)
|
||||
|
||||
val peerRecordCodec: Codec[PeerRecord] = (
|
||||
("id" | publicKey) ::
|
||||
("address" | socketaddress)).as[PeerRecord]
|
||||
|
||||
}
|
||||
@ -0,0 +1,55 @@
|
||||
package fr.acinq.eclair.wire
|
||||
|
||||
import scodec.bits.{BitVector, ByteVector}
|
||||
import scodec.{Attempt, Codec, DecodeResult, Err, SizeBound, codecs}
|
||||
|
||||
/**
|
||||
*
|
||||
* REMOVE THIS A NEW VERSION OF SCODEC IS RELEASED THAT INCLUDES CHANGES MADE IN
|
||||
* https://github.com/scodec/scodec/pull/99/files
|
||||
*
|
||||
* Created by PM on 02/06/2017.
|
||||
*/
|
||||
final class FixedSizeStrictCodec[A](size: Long, codec: Codec[A]) extends Codec[A] {
|
||||
|
||||
override def sizeBound = SizeBound.exact(size)
|
||||
|
||||
override def encode(a: A) = for {
|
||||
encoded <- codec.encode(a)
|
||||
result <- {
|
||||
if (encoded.size != size)
|
||||
Attempt.failure(Err(s"[$a] requires ${encoded.size} bits but field is fixed size of exactly $size bits"))
|
||||
else
|
||||
Attempt.successful(encoded.padTo(size))
|
||||
}
|
||||
} yield result
|
||||
|
||||
override def decode(buffer: BitVector) = {
|
||||
if (buffer.size == size) {
|
||||
codec.decode(buffer.take(size)) map { res =>
|
||||
DecodeResult(res.value, buffer.drop(size))
|
||||
}
|
||||
} else {
|
||||
Attempt.failure(Err(s"expected exactly $size bits but got ${buffer.size} bits"))
|
||||
}
|
||||
}
|
||||
|
||||
override def toString = s"fixedSizeBitsStrict($size, $codec)"
|
||||
}
|
||||
|
||||
object FixedSizeStrictCodec {
|
||||
/**
|
||||
* Encodes by returning the supplied byte vector if its length is `size` bytes, otherwise returning error;
|
||||
* decodes by taking `size * 8` bits from the supplied bit vector and converting to a byte vector.
|
||||
*
|
||||
* @param size number of bits to encode/decode
|
||||
* @group bits
|
||||
*/
|
||||
def bytesStrict(size: Int): Codec[ByteVector] = new Codec[ByteVector] {
|
||||
private val codec = new FixedSizeStrictCodec(size * 8L, codecs.bits).xmap[ByteVector](_.toByteVector, _.toBitVector)
|
||||
def sizeBound = codec.sizeBound
|
||||
def encode(b: ByteVector) = codec.encode(b)
|
||||
def decode(b: BitVector) = codec.decode(b)
|
||||
override def toString = s"bytesStrict($size)"
|
||||
}
|
||||
}
|
||||
@ -3,10 +3,11 @@ package fr.acinq.eclair.wire
|
||||
import java.math.BigInteger
|
||||
import java.net.{Inet4Address, Inet6Address, InetAddress, InetSocketAddress}
|
||||
|
||||
import fr.acinq.bitcoin.Crypto.{Point, PublicKey, Scalar}
|
||||
import fr.acinq.bitcoin.Crypto.{Point, PrivateKey, PublicKey, Scalar}
|
||||
import fr.acinq.bitcoin.{BinaryData, Crypto}
|
||||
import fr.acinq.eclair.crypto.{Generators, Sphinx}
|
||||
import fr.acinq.eclair.wire
|
||||
import fr.acinq.eclair.wire.FixedSizeStrictCodec.bytesStrict
|
||||
import scodec.bits.{BitVector, ByteVector}
|
||||
import scodec.codecs._
|
||||
import scodec.{Attempt, Codec, Err}
|
||||
@ -21,7 +22,7 @@ object LightningMessageCodecs {
|
||||
// (for something smarter see https://github.com/yzernik/bitcoin-scodec/blob/master/src/main/scala/io/github/yzernik/bitcoinscodec/structures/UInt64.scala)
|
||||
val uint64: Codec[Long] = int64.narrow(l => if (l >= 0) Attempt.Successful(l) else Attempt.failure(Err(s"overflow for value $l")), l => l)
|
||||
|
||||
def binarydata(size: Int): Codec[BinaryData] = bytes(size).xmap(d => BinaryData(d.toArray), d => ByteVector(d.data))
|
||||
def binarydata(size: Int): Codec[BinaryData] = limitedSizeBytes(size, bytesStrict(size).xmap(d => BinaryData(d.toArray), d => ByteVector(d.data)))
|
||||
|
||||
def varsizebinarydata: Codec[BinaryData] = variableSizeBytes(uint16, bytes.xmap(d => BinaryData(d.toArray), d => ByteVector(d.data)))
|
||||
|
||||
@ -50,12 +51,17 @@ object LightningMessageCodecs {
|
||||
)
|
||||
|
||||
def point: Codec[Point] = Codec[Point](
|
||||
(point: Point) => bytes(33).encode(ByteVector(point.toBin(true).toArray)),
|
||||
(point: Point) => bytes(33).encode(ByteVector(point.toBin(compressed = true).toArray)),
|
||||
(wire: BitVector) => bytes(33).decode(wire).map(_.map(b => Point(b.toArray)))
|
||||
)
|
||||
|
||||
def privateKey: Codec[PrivateKey] = Codec[PrivateKey](
|
||||
(priv: PrivateKey) => bytes(32).encode(ByteVector(priv.value.toBin.toArray)),
|
||||
(wire: BitVector) => bytes(32).decode(wire).map(_.map(b => PrivateKey(b.toArray, compressed = true)))
|
||||
)
|
||||
|
||||
def publicKey: Codec[PublicKey] = Codec[PublicKey](
|
||||
(pub: PublicKey) => bytes(33).encode(ByteVector(pub.value.toBin(true).toArray)),
|
||||
(pub: PublicKey) => bytes(33).encode(ByteVector(pub.value.toBin(compressed = true).toArray)),
|
||||
(wire: BitVector) => bytes(33).decode(wire).map(_.map(b => PublicKey(b.toArray)))
|
||||
)
|
||||
|
||||
|
||||
@ -6,6 +6,7 @@ import fr.acinq.bitcoin.{BinaryData, Crypto, OutPoint, Script, Transaction, TxIn
|
||||
import fr.acinq.eclair.TestConstants.{Alice, Bob}
|
||||
import fr.acinq.eclair.blockchain._
|
||||
import fr.acinq.eclair.channel._
|
||||
import fr.acinq.eclair.crypto.Sphinx
|
||||
import fr.acinq.eclair.payment.PaymentLifecycle
|
||||
import fr.acinq.eclair.router.Hop
|
||||
import fr.acinq.eclair.transactions.Scripts
|
||||
@ -19,6 +20,8 @@ import scala.util.Random
|
||||
*/
|
||||
trait StateTestsHelperMethods extends TestKitBase {
|
||||
|
||||
def defaultOnion: BinaryData = "00" * Sphinx.PacketLength
|
||||
|
||||
case class Setup(alice: TestFSMRef[State, Data, Channel],
|
||||
bob: TestFSMRef[State, Data, Channel],
|
||||
alice2bob: TestProbe,
|
||||
|
||||
@ -229,7 +229,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
|
||||
test("recv UpdateAddHtlc") { case (_, bob, _, _, _, _, _) =>
|
||||
within(30 seconds) {
|
||||
val initialData = bob.stateData.asInstanceOf[DATA_NORMAL]
|
||||
val htlc = UpdateAddHtlc("00" * 32, 0, 150000, 400144, BinaryData("42" * 32), "")
|
||||
val htlc = UpdateAddHtlc("00" * 32, 0, 150000, 400144, BinaryData("42" * 32), defaultOnion)
|
||||
bob ! htlc
|
||||
awaitCond(bob.stateData == initialData.copy(commitments = initialData.commitments.copy(remoteChanges = initialData.commitments.remoteChanges.copy(proposed = initialData.commitments.remoteChanges.proposed :+ htlc), remoteNextHtlcId = 1)))
|
||||
}
|
||||
@ -238,7 +238,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
|
||||
test("recv UpdateAddHtlc (unexpected id)") { case (_, bob, _, bob2alice, _, bob2blockchain, _) =>
|
||||
within(30 seconds) {
|
||||
val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx
|
||||
val htlc = UpdateAddHtlc("00" * 32, 42, 150000, 400144, BinaryData("42" * 32), "")
|
||||
val htlc = UpdateAddHtlc("00" * 32, 42, 150000, 400144, BinaryData("42" * 32), defaultOnion)
|
||||
bob ! htlc.copy(id = 0)
|
||||
bob ! htlc.copy(id = 1)
|
||||
bob ! htlc.copy(id = 2)
|
||||
@ -255,7 +255,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
|
||||
test("recv UpdateAddHtlc (invalid payment hash)") { case (_, bob, alice2bob, bob2alice, _, bob2blockchain, _) =>
|
||||
within(30 seconds) {
|
||||
val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx
|
||||
val htlc = UpdateAddHtlc("00" * 32, 0, 150000, 400144, "11" * 42, "")
|
||||
val htlc = UpdateAddHtlc("00" * 32, 0, 150000, 400144, "11" * 42, defaultOnion)
|
||||
alice2bob.forward(bob, htlc)
|
||||
val error = bob2alice.expectMsgType[Error]
|
||||
assert(new String(error.data) === InvalidPaymentHash.getMessage)
|
||||
@ -268,7 +268,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
|
||||
test("recv UpdateAddHtlc (expiry too small)") { case (_, bob, alice2bob, bob2alice, _, bob2blockchain, _) =>
|
||||
within(30 seconds) {
|
||||
val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx
|
||||
val htlc = UpdateAddHtlc("00" * 32, 0, 150000, expiry = 1, BinaryData("42" * 32), "")
|
||||
val htlc = UpdateAddHtlc("00" * 32, 0, 150000, expiry = 1, BinaryData("42" * 32), defaultOnion)
|
||||
alice2bob.forward(bob, htlc)
|
||||
val error = bob2alice.expectMsgType[Error]
|
||||
assert(new String(error.data) === ExpiryTooSmall(minimum = 400003, actual = 1, blockCount = 400000).getMessage)
|
||||
@ -281,7 +281,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
|
||||
test("recv UpdateAddHtlc (value too small)") { case (_, bob, alice2bob, bob2alice, _, bob2blockchain, _) =>
|
||||
within(30 seconds) {
|
||||
val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx
|
||||
val htlc = UpdateAddHtlc("00" * 32, 0, 150, expiry = 400144, BinaryData("42" * 32), "")
|
||||
val htlc = UpdateAddHtlc("00" * 32, 0, 150, expiry = 400144, BinaryData("42" * 32), defaultOnion)
|
||||
alice2bob.forward(bob, htlc)
|
||||
val error = bob2alice.expectMsgType[Error]
|
||||
assert(new String(error.data) === HtlcValueTooSmall(minimum = 1000, actual = 150).getMessage)
|
||||
@ -294,7 +294,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
|
||||
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("00" * 32, 0, Long.MaxValue, 400144, BinaryData("42" * 32), "")
|
||||
val htlc = UpdateAddHtlc("00" * 32, 0, Long.MaxValue, 400144, BinaryData("42" * 32), defaultOnion)
|
||||
alice2bob.forward(bob, htlc)
|
||||
val error = bob2alice.expectMsgType[Error]
|
||||
assert(new String(error.data) === InsufficientFunds(amountMsat = Long.MaxValue, missingSatoshis = 9223372036083735L, reserveSatoshis = 20000, feesSatoshis = 8960).getMessage)
|
||||
@ -307,10 +307,10 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
|
||||
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("00" * 32, 0, 400000000, 400144, "11" * 32, ""))
|
||||
alice2bob.forward(bob, UpdateAddHtlc("00" * 32, 1, 200000000, 400144, "22" * 32, ""))
|
||||
alice2bob.forward(bob, UpdateAddHtlc("00" * 32, 2, 167600000, 400144, "33" * 32, ""))
|
||||
alice2bob.forward(bob, UpdateAddHtlc("00" * 32, 3, 10000000, 400144, "44" * 32, ""))
|
||||
alice2bob.forward(bob, UpdateAddHtlc("00" * 32, 0, 400000000, 400144, "11" * 32, defaultOnion))
|
||||
alice2bob.forward(bob, UpdateAddHtlc("00" * 32, 1, 200000000, 400144, "22" * 32, defaultOnion))
|
||||
alice2bob.forward(bob, UpdateAddHtlc("00" * 32, 2, 167600000, 400144, "33" * 32, defaultOnion))
|
||||
alice2bob.forward(bob, UpdateAddHtlc("00" * 32, 3, 10000000, 400144, "44" * 32, defaultOnion))
|
||||
val error = bob2alice.expectMsgType[Error]
|
||||
assert(new String(error.data) === InsufficientFunds(amountMsat = 10000000, missingSatoshis = 11720, reserveSatoshis = 20000, feesSatoshis = 14120).getMessage)
|
||||
awaitCond(bob.stateName == CLOSING)
|
||||
@ -322,9 +322,9 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
|
||||
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("00" * 32, 0, 300000000, 400144, "11" * 32, ""))
|
||||
alice2bob.forward(bob, UpdateAddHtlc("00" * 32, 1, 300000000, 400144, "22" * 32, ""))
|
||||
alice2bob.forward(bob, UpdateAddHtlc("00" * 32, 2, 500000000, 400144, "33" * 32, ""))
|
||||
alice2bob.forward(bob, UpdateAddHtlc("00" * 32, 0, 300000000, 400144, "11" * 32, defaultOnion))
|
||||
alice2bob.forward(bob, UpdateAddHtlc("00" * 32, 1, 300000000, 400144, "22" * 32, defaultOnion))
|
||||
alice2bob.forward(bob, UpdateAddHtlc("00" * 32, 2, 500000000, 400144, "33" * 32, defaultOnion))
|
||||
val error = bob2alice.expectMsgType[Error]
|
||||
assert(new String(error.data) === InsufficientFunds(amountMsat = 500000000, missingSatoshis = 332400, reserveSatoshis = 20000, feesSatoshis = 12400).getMessage)
|
||||
awaitCond(bob.stateName == CLOSING)
|
||||
@ -336,7 +336,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
|
||||
test("recv UpdateAddHtlc (over max inflight htlc value)") { case (alice, _, alice2bob, _, alice2blockchain, _, _) =>
|
||||
within(30 seconds) {
|
||||
val tx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx
|
||||
alice2bob.forward(alice, UpdateAddHtlc("00" * 32, 0, 151000000, 400144, "11" * 32, ""))
|
||||
alice2bob.forward(alice, UpdateAddHtlc("00" * 32, 0, 151000000, 400144, "11" * 32, defaultOnion))
|
||||
val error = alice2bob.expectMsgType[Error]
|
||||
assert(new String(error.data) === HtlcValueTooHighInFlight(maximum = 150000000, actual = 151000000).getMessage)
|
||||
awaitCond(alice.stateName == CLOSING)
|
||||
@ -350,9 +350,9 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
|
||||
val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx
|
||||
// Bob accepts a maximum of 30 htlcs
|
||||
for (i <- 0 until 30) {
|
||||
alice2bob.forward(bob, UpdateAddHtlc("00" * 32, i, 1000000, 400144, "11" * 32, ""))
|
||||
alice2bob.forward(bob, UpdateAddHtlc("00" * 32, i, 1000000, 400144, "11" * 32, defaultOnion))
|
||||
}
|
||||
alice2bob.forward(bob, UpdateAddHtlc("00" * 32, 30, 1000000, 400144, "11" * 32, ""))
|
||||
alice2bob.forward(bob, UpdateAddHtlc("00" * 32, 30, 1000000, 400144, "11" * 32, defaultOnion))
|
||||
val error = bob2alice.expectMsgType[Error]
|
||||
assert(new String(error.data) === TooManyAcceptedHtlcs(maximum = 30).getMessage)
|
||||
awaitCond(bob.stateName == CLOSING)
|
||||
|
||||
@ -1,16 +1,19 @@
|
||||
package fr.acinq.eclair.crypto
|
||||
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.nio.charset.Charset
|
||||
|
||||
import akka.actor.{Actor, ActorRef, ActorSystem, IllegalActorStateException, OneForOneStrategy, Props, Stash, SupervisorStrategy, Terminated}
|
||||
import akka.actor.{Actor, ActorRef, ActorSystem, OneForOneStrategy, Props, Stash, SupervisorStrategy, Terminated}
|
||||
import akka.io.Tcp.{Received, Write}
|
||||
import akka.testkit.{TestActorRef, TestFSMRef, TestKit, TestProbe}
|
||||
import fr.acinq.bitcoin.{Base58Check, BinaryData}
|
||||
import fr.acinq.bitcoin.BinaryData
|
||||
import fr.acinq.eclair.crypto.Noise.{Chacha20Poly1305CipherFunctions, CipherState}
|
||||
import fr.acinq.eclair.crypto.TransportHandler.{ExtendedCipherState, Listener}
|
||||
import fr.acinq.eclair.wire.LightningMessageCodecs
|
||||
import org.junit.runner.RunWith
|
||||
import org.scalatest.junit.JUnitRunner
|
||||
import org.scalatest.{BeforeAndAfterAll, FunSuiteLike}
|
||||
import scodec.Codec
|
||||
import scodec.codecs._
|
||||
|
||||
import scala.annotation.tailrec
|
||||
import scala.concurrent.duration._
|
||||
@ -32,8 +35,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, TransportHandler.Noop))
|
||||
val responder = TestFSMRef(new TransportHandler(Responder.s, None, pipe, TransportHandler.Noop))
|
||||
val initiator = TestFSMRef(new TransportHandler(Initiator.s, Some(Responder.s.pub), pipe, LightningMessageCodecs.varsizebinarydata))
|
||||
val responder = TestFSMRef(new TransportHandler(Responder.s, None, pipe, LightningMessageCodecs.varsizebinarydata))
|
||||
pipe ! (initiator, responder)
|
||||
|
||||
awaitCond(initiator.stateName == TransportHandler.WaitingForListener)
|
||||
@ -60,22 +63,12 @@ class TransportHandlerSpec extends TestKit(ActorSystem("test")) with FunSuiteLik
|
||||
|
||||
test("succesfull handshake with custom serializer") {
|
||||
case class MyMessage(payload: String)
|
||||
|
||||
val mySerializer = new TransportHandler.Serializer[MyMessage] {
|
||||
override def serialize(t: MyMessage): BinaryData = {
|
||||
Base58Check.encode(0.toByte, t.payload.getBytes).getBytes
|
||||
}
|
||||
|
||||
override def deserialize(bin: BinaryData): MyMessage = {
|
||||
val (_, data) = Base58Check.decode(new String(bin.toArray))
|
||||
MyMessage(new String(data))
|
||||
}
|
||||
}
|
||||
val mycodec: Codec[MyMessage] = ("payload" | scodec.codecs.string32L(Charset.defaultCharset())).as[MyMessage]
|
||||
val pipe = system.actorOf(Props[MyPipe])
|
||||
val probe1 = TestProbe()
|
||||
val probe2 = TestProbe()
|
||||
val initiator = TestFSMRef(new TransportHandler(Initiator.s, Some(Responder.s.pub), pipe, mySerializer))
|
||||
val responder = TestFSMRef(new TransportHandler(Responder.s, None, pipe, mySerializer))
|
||||
val initiator = TestFSMRef(new TransportHandler(Initiator.s, Some(Responder.s.pub), pipe, mycodec))
|
||||
val responder = TestFSMRef(new TransportHandler(Responder.s, None, pipe, mycodec))
|
||||
pipe ! (initiator, responder)
|
||||
|
||||
awaitCond(initiator.stateName == TransportHandler.WaitingForListener)
|
||||
@ -104,8 +97,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, TransportHandler.Noop))
|
||||
val responder = TestFSMRef(new TransportHandler(Responder.s, None, pipe, TransportHandler.Noop))
|
||||
val initiator = TestFSMRef(new TransportHandler(Initiator.s, Some(Responder.s.pub), pipe, LightningMessageCodecs.varsizebinarydata))
|
||||
val responder = TestFSMRef(new TransportHandler(Responder.s, None, pipe, LightningMessageCodecs.varsizebinarydata))
|
||||
pipe ! (initiator, responder)
|
||||
|
||||
awaitCond(initiator.stateName == TransportHandler.WaitingForListener)
|
||||
@ -135,8 +128,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, TransportHandler.Noop), supervisor, "ini")
|
||||
val responder = TestFSMRef(new TransportHandler(Responder.s, None, pipe, TransportHandler.Noop), supervisor, "res")
|
||||
val initiator = TestFSMRef(new TransportHandler(Initiator.s, Some(Initiator.s.pub), pipe, LightningMessageCodecs.varsizebinarydata), supervisor, "ini")
|
||||
val responder = TestFSMRef(new TransportHandler(Responder.s, None, pipe, LightningMessageCodecs.varsizebinarydata), supervisor, "res")
|
||||
probe1.watch(responder)
|
||||
pipe ! (initiator, responder)
|
||||
|
||||
|
||||
@ -4,11 +4,11 @@ import fr.acinq.bitcoin.Crypto.{PrivateKey, Scalar}
|
||||
import fr.acinq.bitcoin.{BinaryData, Crypto, MilliSatoshi, Satoshi, Transaction}
|
||||
import fr.acinq.eclair.channel.Helpers.Funding
|
||||
import fr.acinq.eclair.channel._
|
||||
import fr.acinq.eclair.crypto.ShaChain
|
||||
import fr.acinq.eclair.crypto.{ShaChain, Sphinx}
|
||||
import fr.acinq.eclair.randomKey
|
||||
import fr.acinq.eclair.transactions.Transactions.CommitTx
|
||||
import fr.acinq.eclair.transactions._
|
||||
import fr.acinq.eclair.wire.{CommitSig, UpdateAddHtlc}
|
||||
import fr.acinq.eclair.wire.{ChannelCodecs, CommitSig, UpdateAddHtlc}
|
||||
import org.junit.runner.RunWith
|
||||
import org.scalatest.FunSuite
|
||||
import org.scalatest.junit.JUnitRunner
|
||||
@ -23,9 +23,10 @@ class ChannelStateSpec extends FunSuite {
|
||||
|
||||
test("basic serialization test (NORMAL)") {
|
||||
val data = normal
|
||||
val bin = JavaSerializer.serialize(data)
|
||||
val check = JavaSerializer.deserialize[DATA_NORMAL](bin)
|
||||
assert(data == check)
|
||||
val bin = ChannelCodecs.DATA_NORMAL_Codec.encode(data).require
|
||||
val check = ChannelCodecs.DATA_NORMAL_Codec.decodeValue(bin).require
|
||||
assert(data.commitments.localCommit.spec === check.commitments.localCommit.spec)
|
||||
assert(data === check)
|
||||
}
|
||||
}
|
||||
|
||||
@ -72,22 +73,22 @@ object ChannelStateSpec {
|
||||
)
|
||||
|
||||
val htlcs = Seq(
|
||||
Htlc(IN, UpdateAddHtlc("00" * 32, 0, MilliSatoshi(1000000).amount, 500, Crypto.sha256(paymentPreimages(0)), BinaryData("")), None),
|
||||
Htlc(IN, UpdateAddHtlc("00" * 32, 0, MilliSatoshi(2000000).amount, 501, Crypto.sha256(paymentPreimages(1)), BinaryData("")), None),
|
||||
Htlc(OUT, UpdateAddHtlc("00" * 32, 0, MilliSatoshi(2000000).amount, 502, Crypto.sha256(paymentPreimages(2)), BinaryData("")), None),
|
||||
Htlc(OUT, UpdateAddHtlc("00" * 32, 0, MilliSatoshi(3000000).amount, 503, Crypto.sha256(paymentPreimages(3)), BinaryData("")), None),
|
||||
Htlc(IN, UpdateAddHtlc("00" * 32, 0, MilliSatoshi(4000000).amount, 504, Crypto.sha256(paymentPreimages(4)), BinaryData("")), None)
|
||||
Htlc(IN, UpdateAddHtlc("00" * 32, 0, MilliSatoshi(1000000).amount, 500, Crypto.sha256(paymentPreimages(0)), BinaryData("00" * Sphinx.PacketLength)), None),
|
||||
Htlc(IN, UpdateAddHtlc("00" * 32, 0, MilliSatoshi(2000000).amount, 501, Crypto.sha256(paymentPreimages(1)), BinaryData("00" * Sphinx.PacketLength)), None),
|
||||
Htlc(OUT, UpdateAddHtlc("00" * 32, 0, MilliSatoshi(2000000).amount, 502, Crypto.sha256(paymentPreimages(2)), BinaryData("00" * Sphinx.PacketLength)), None),
|
||||
Htlc(OUT, UpdateAddHtlc("00" * 32, 0, MilliSatoshi(3000000).amount, 503, Crypto.sha256(paymentPreimages(3)), BinaryData("00" * Sphinx.PacketLength)), None),
|
||||
Htlc(IN, UpdateAddHtlc("00" * 32, 0, MilliSatoshi(4000000).amount, 504, Crypto.sha256(paymentPreimages(4)), BinaryData("00" * Sphinx.PacketLength)), None)
|
||||
)
|
||||
|
||||
val fundingTx = Transaction.read("0200000001adbb20ea41a8423ea937e76e8151636bf6093b70eaff942930d20576600521fd000000006b48304502210090587b6201e166ad6af0227d3036a9454223d49a1f11839c1a362184340ef0240220577f7cd5cca78719405cbf1de7414ac027f0239ef6e214c90fcaab0454d84b3b012103535b32d5eb0a6ed0982a0479bbadc9868d9836f6ba94dd5a63be16d875069184ffffffff028096980000000000220020c015c4a6be010e21657068fc2e6a9d02b27ebe4d490a25846f7237f104d1a3cd20256d29010000001600143ca33c2e4446f4a305f23c80df8ad1afdcf652f900000000")
|
||||
val fundingAmount = fundingTx.txOut(0).amount
|
||||
val commitmentInput = Funding.makeFundingInputInfo(fundingTx.hash, 0, fundingAmount, localParams.fundingPrivKey.publicKey, remoteParams.fundingPubKey)
|
||||
|
||||
val localCommit = LocalCommit(0, CommitmentSpec(htlcs.toSet, 1500, 50000, 700000), PublishableTxs(CommitTx(commitmentInput, Transaction(2, Nil, Nil, 0)), Nil), CommitSig("42" * 32, BinaryData("01" * 64), Nil))
|
||||
val localCommit = LocalCommit(0, CommitmentSpec(htlcs.toSet, 1500, 50000, 700000), PublishableTxs(CommitTx(commitmentInput, Transaction(2, Nil, Nil, 0)), Nil))
|
||||
val remoteCommit = RemoteCommit(0, CommitmentSpec(htlcs.toSet, 1500, 50000, 700000), BinaryData("0303030303030303030303030303030303030303030303030303030303030303"), Scalar(BinaryData("04" * 32)).toPoint)
|
||||
val commitments = Commitments(localParams, remoteParams, localCommit, remoteCommit, LocalChanges(Nil, Nil, Nil), RemoteChanges(Nil, Nil, Nil),
|
||||
localNextHtlcId = 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
|
||||
remoteNextCommitInfo = Right(randomKey.publicKey), // TODO: we will receive their next per-commitment point in the next message, so we temporarily put an empty byte array
|
||||
remoteNextHtlcId = 0L,
|
||||
commitInput = commitmentInput, remotePerCommitmentSecrets = ShaChain.init, channelId = "00" * 32, unackedMessages = Nil)
|
||||
|
||||
|
||||
@ -0,0 +1,126 @@
|
||||
package fr.acinq.eclair.wire
|
||||
|
||||
import java.nio.charset.Charset
|
||||
|
||||
import fr.acinq.bitcoin.BinaryData
|
||||
import fr.acinq.eclair.channel.{LocalParams, RemoteParams}
|
||||
import fr.acinq.eclair.crypto.Sphinx
|
||||
import fr.acinq.eclair.randomKey
|
||||
import fr.acinq.eclair.transactions._
|
||||
import fr.acinq.eclair.wire.ChannelCodecs._
|
||||
import fr.acinq.eclair.wire.LightningMessageCodecs._
|
||||
import org.junit.runner.RunWith
|
||||
import org.scalatest.FunSuite
|
||||
import org.scalatest.junit.JUnitRunner
|
||||
import scodec.Codec
|
||||
import scodec.bits.BitVector
|
||||
import scodec.codecs._
|
||||
|
||||
import scala.util.Random
|
||||
|
||||
/**
|
||||
* Created by PM on 31/05/2016.
|
||||
*/
|
||||
@RunWith(classOf[JUnitRunner])
|
||||
class ChannelCodecsSpec extends FunSuite {
|
||||
|
||||
def randomBytes(size: Int): BinaryData = {
|
||||
val bin = new Array[Byte](size)
|
||||
Random.nextBytes(bin)
|
||||
bin
|
||||
}
|
||||
|
||||
test("encode/decode localparams") {
|
||||
val o = LocalParams(
|
||||
nodeId = randomKey.publicKey,
|
||||
dustLimitSatoshis = Random.nextInt(Int.MaxValue),
|
||||
maxHtlcValueInFlightMsat = Random.nextInt(Int.MaxValue),
|
||||
channelReserveSatoshis = Random.nextInt(Int.MaxValue),
|
||||
htlcMinimumMsat = Random.nextInt(Int.MaxValue),
|
||||
toSelfDelay = Random.nextInt(Short.MaxValue),
|
||||
maxAcceptedHtlcs = Random.nextInt(Short.MaxValue),
|
||||
fundingPrivKey = randomKey,
|
||||
revocationSecret = randomKey.value,
|
||||
paymentKey = randomKey,
|
||||
delayedPaymentKey = randomKey.value,
|
||||
defaultFinalScriptPubKey = randomBytes(10 + Random.nextInt(200)),
|
||||
shaSeed = randomBytes(32),
|
||||
isFunder = Random.nextBoolean(),
|
||||
globalFeatures = randomBytes(256),
|
||||
localFeatures = randomBytes(256))
|
||||
val encoded = localParamsCodec.encode(o).require
|
||||
val decoded = localParamsCodec.decode(encoded).require
|
||||
assert(o === decoded.value)
|
||||
}
|
||||
|
||||
test("encode/decode remoteparams") {
|
||||
val o = RemoteParams(
|
||||
nodeId = randomKey.publicKey,
|
||||
dustLimitSatoshis = Random.nextInt(Int.MaxValue),
|
||||
maxHtlcValueInFlightMsat = Random.nextInt(Int.MaxValue),
|
||||
channelReserveSatoshis = Random.nextInt(Int.MaxValue),
|
||||
htlcMinimumMsat = Random.nextInt(Int.MaxValue),
|
||||
toSelfDelay = Random.nextInt(Short.MaxValue),
|
||||
maxAcceptedHtlcs = Random.nextInt(Short.MaxValue),
|
||||
fundingPubKey = randomKey.publicKey,
|
||||
revocationBasepoint = randomKey.publicKey.value,
|
||||
paymentBasepoint = randomKey.publicKey.value,
|
||||
delayedPaymentBasepoint = randomKey.publicKey.value,
|
||||
globalFeatures = randomBytes(256),
|
||||
localFeatures = randomBytes(256))
|
||||
val encoded = remoteParamsCodec.encode(o).require
|
||||
val decoded = remoteParamsCodec.decodeValue(encoded).require
|
||||
assert(o === decoded)
|
||||
}
|
||||
|
||||
test("encode/decode direction") {
|
||||
directionCodec.decodeValue(directionCodec.encode(IN).require).require == IN
|
||||
directionCodec.decodeValue(directionCodec.encode(OUT).require).require == OUT
|
||||
}
|
||||
|
||||
test("encode/decode htlc") {
|
||||
val add = UpdateAddHtlc(
|
||||
channelId = randomBytes(32),
|
||||
id = Random.nextInt(Int.MaxValue),
|
||||
amountMsat = Random.nextInt(Int.MaxValue),
|
||||
expiry = Random.nextInt(Int.MaxValue),
|
||||
paymentHash = randomBytes(32),
|
||||
onionRoutingPacket = randomBytes(Sphinx.PacketLength))
|
||||
val htlc1 = Htlc(direction = IN, add = add, previousChannelId = Some(randomBytes(32)))
|
||||
val htlc2 = Htlc(direction = OUT, add = add, previousChannelId = None)
|
||||
htlcCodec.decodeValue(htlcCodec.encode(htlc1).require).require == htlc1
|
||||
htlcCodec.decodeValue(htlcCodec.encode(htlc2).require).require == htlc2
|
||||
}
|
||||
|
||||
test("encode/decode commitment spec") {
|
||||
val add1 = UpdateAddHtlc(
|
||||
channelId = randomBytes(32),
|
||||
id = Random.nextInt(Int.MaxValue),
|
||||
amountMsat = Random.nextInt(Int.MaxValue),
|
||||
expiry = Random.nextInt(Int.MaxValue),
|
||||
paymentHash = randomBytes(32),
|
||||
onionRoutingPacket = randomBytes(Sphinx.PacketLength))
|
||||
val add2 = UpdateAddHtlc(
|
||||
channelId = randomBytes(32),
|
||||
id = Random.nextInt(Int.MaxValue),
|
||||
amountMsat = Random.nextInt(Int.MaxValue),
|
||||
expiry = Random.nextInt(Int.MaxValue),
|
||||
paymentHash = randomBytes(32),
|
||||
onionRoutingPacket = randomBytes(Sphinx.PacketLength))
|
||||
val htlc1 = Htlc(direction = IN, add = add1, previousChannelId = Some(randomBytes(32)))
|
||||
val htlc2 = Htlc(direction = OUT, add = add2, previousChannelId = None)
|
||||
val htlcs = Set(htlc1, htlc2)
|
||||
setCodec(htlcCodec).decodeValue(setCodec(htlcCodec).encode(htlcs).require).require == htlcs
|
||||
val o = CommitmentSpec(
|
||||
htlcs = Set(htlc1, htlc2),
|
||||
feeratePerKw = Random.nextInt(Int.MaxValue),
|
||||
toLocalMsat = Random.nextInt(Int.MaxValue),
|
||||
toRemoteMsat = Random.nextInt(Int.MaxValue)
|
||||
)
|
||||
val encoded = commitmentSpecCodec.encode(o).require
|
||||
val decoded = commitmentSpecCodec.decode(encoded).require
|
||||
assert(o === decoded.value)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@ -90,5 +90,10 @@
|
||||
<artifactId>janino</artifactId>
|
||||
<version>2.5.10</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.googlecode.lanterna</groupId>
|
||||
<artifactId>lanterna</artifactId>
|
||||
<version>3.0.0-rc1</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
|
||||
@ -9,19 +9,22 @@ import grizzled.slf4j.Logging
|
||||
*/
|
||||
object Boot extends App with Logging {
|
||||
|
||||
case class CmdLineConfig(datadir: File = new File(System.getProperty("user.home"), ".eclair"))
|
||||
case class CmdLineConfig(datadir: File = new File(System.getProperty("user.home"), ".eclair"), textui: Boolean = false)
|
||||
|
||||
val parser = new scopt.OptionParser[CmdLineConfig]("eclair") {
|
||||
head("eclair", s"${getClass.getPackage.getImplementationVersion} (commit: ${getClass.getPackage.getSpecificationVersion})")
|
||||
help("help").abbr("h").text("display usage text")
|
||||
opt[File]("datadir").optional().valueName("<file>").action((x, c) => c.copy(datadir = x)).text("optional data directory, default is ~/.eclair")
|
||||
opt[Unit]("textui").optional().action((_, c) => c.copy(textui = true)).text("runs eclair with a text-based ui")
|
||||
}
|
||||
|
||||
try {
|
||||
parser.parse(args, CmdLineConfig()) match {
|
||||
case Some(config) =>
|
||||
LogSetup.logTo(config.datadir)
|
||||
new Setup(config.datadir.getAbsolutePath).boostrap
|
||||
val setup = new Setup(config.datadir.getAbsolutePath)
|
||||
if (config.textui) new Textui(setup)
|
||||
setup.boostrap
|
||||
case None => System.exit(0)
|
||||
}
|
||||
} catch {
|
||||
|
||||
130
eclair-node/src/main/scala/fr/acinq/eclair/Textui.scala
Normal file
130
eclair-node/src/main/scala/fr/acinq/eclair/Textui.scala
Normal file
@ -0,0 +1,130 @@
|
||||
package fr.acinq.eclair
|
||||
|
||||
import java.awt.Font
|
||||
import java.net.InetSocketAddress
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
import akka.actor.{ActorRef, Props, SupervisorStrategy}
|
||||
import com.googlecode.lanterna.graphics.SimpleTheme
|
||||
import com.googlecode.lanterna.{TerminalPosition, TerminalSize}
|
||||
import com.googlecode.lanterna.gui2.dialogs.TextInputDialogBuilder
|
||||
import com.googlecode.lanterna.input.KeyStroke
|
||||
import com.googlecode.lanterna.terminal.swing.SwingTerminalFontConfiguration
|
||||
import fr.acinq.bitcoin.Crypto.PublicKey
|
||||
import fr.acinq.bitcoin.{BinaryData, MilliSatoshi, Satoshi}
|
||||
import fr.acinq.eclair.channel.State
|
||||
import fr.acinq.eclair.io.Switchboard.{NewChannel, NewConnection}
|
||||
import grizzled.slf4j.Logging
|
||||
|
||||
import scala.collection.JavaConversions._
|
||||
|
||||
/**
|
||||
* Created by PM on 05/06/2017.
|
||||
*/
|
||||
class Textui(setup: Setup) extends Logging {
|
||||
|
||||
import com.googlecode.lanterna.TextColor
|
||||
import com.googlecode.lanterna.gui2._
|
||||
import com.googlecode.lanterna.screen.TerminalScreen
|
||||
import com.googlecode.lanterna.terminal.DefaultTerminalFactory
|
||||
// Setup terminal and screen layers// Setup terminal and screen layers
|
||||
|
||||
val terminal = new DefaultTerminalFactory().createTerminal
|
||||
val screen = new TerminalScreen(terminal)
|
||||
screen.startScreen()
|
||||
|
||||
// Create panel to hold components
|
||||
val mainPanel = new Panel()
|
||||
mainPanel.setLayoutManager(new BorderLayout())
|
||||
|
||||
val channelsPanel = new Panel()
|
||||
channelsPanel.setLayoutManager(new LinearLayout(Direction.VERTICAL))
|
||||
channelsPanel.setLayoutData(BorderLayout.Location.TOP)
|
||||
mainPanel.addComponent(channelsPanel)
|
||||
channelsPanel.addComponent(new Label("channels"))
|
||||
|
||||
val channels = collection.mutable.Map[ActorRef, Panel]()
|
||||
|
||||
def addChannel(channel: ActorRef, channelId: BinaryData, remoteNodeId: PublicKey, state: State, balance: Satoshi, capacity: Satoshi): Unit = {
|
||||
val channelPanel = new Panel()
|
||||
channelPanel.setLayoutManager(new LinearLayout(Direction.HORIZONTAL))
|
||||
val channelDataPanel = new Panel()
|
||||
channelDataPanel.setLayoutManager(new GridLayout(2))
|
||||
channelDataPanel.addComponent(new Label(s"$channelId"))
|
||||
channelDataPanel.addComponent(new Label(s"${state.toString}"))
|
||||
channelDataPanel.addComponent(new Label(s"$remoteNodeId"))
|
||||
channelDataPanel.addComponent(new EmptySpace(new TerminalSize(0, 0))) // Empty space underneath labels
|
||||
channelDataPanel.addComponent(new Separator(Direction.HORIZONTAL)) // Empty space underneath labels
|
||||
channelPanel.addComponent(channelDataPanel)
|
||||
val pb = new ProgressBar(0, 100)
|
||||
pb.setLabelFormat(s"$balance")
|
||||
pb.setValue((balance.amount * 100 / capacity.amount).toInt)
|
||||
pb.setPreferredWidth(100)
|
||||
channelPanel.addComponent(pb)
|
||||
channelsPanel.addComponent(channelPanel)
|
||||
channels.put(channel, channelPanel)
|
||||
}
|
||||
|
||||
def updateState(channel: ActorRef, state: State): Unit = {
|
||||
val panel = channels(channel)
|
||||
val channelDataPanel = panel.getChildren.iterator().next().asInstanceOf[Panel]
|
||||
channelDataPanel.getChildren.toList(1).asInstanceOf[Label].setText(s"$state")
|
||||
}
|
||||
|
||||
/*val shortcutsPanel = new Panel()
|
||||
shortcutsPanel.setLayoutManager(new LinearLayout(Direction.HORIZONTAL))
|
||||
shortcutsPanel.addComponent(new Label("(N)ew channel"))
|
||||
shortcutsPanel.addComponent(new Separator(Direction.VERTICAL))
|
||||
shortcutsPanel.setLayoutData(BorderLayout.Location.BOTTOM)
|
||||
mainPanel.addComponent(shortcutsPanel)*/
|
||||
|
||||
//addChannel(randomBytes(32), randomKey.publicKey, NORMAL, Satoshi(Random.nextInt(1000)), Satoshi(1000))
|
||||
//addChannel(randomBytes(32), randomKey.publicKey, NORMAL, Satoshi(Random.nextInt(1000)), Satoshi(1000))
|
||||
//addChannel(randomBytes(32), randomKey.publicKey, NORMAL, Satoshi(Random.nextInt(1000)), Satoshi(1000))
|
||||
|
||||
//val theme = new SimpleTheme(TextColor.ANSI.DEFAULT, TextColor.ANSI.BLACK)
|
||||
|
||||
// Create window to hold the panel
|
||||
val window = new BasicWindow
|
||||
window.setComponent(mainPanel)
|
||||
//window.setTheme(theme)
|
||||
window.setHints(/*Window.Hint.FULL_SCREEN :: */ Window.Hint.NO_DECORATIONS :: Nil)
|
||||
|
||||
|
||||
val textuiUpdater = setup.system.actorOf(SimpleSupervisor.props(Props(classOf[TextuiUpdater], this), "textui-updater", SupervisorStrategy.Resume))
|
||||
// Create gui and start gui
|
||||
val runnable = new Runnable {
|
||||
override def run(): Unit = {
|
||||
val gui = new MultiWindowTextGUI(screen, new DefaultWindowManager, new EmptySpace(TextColor.ANSI.BLUE))
|
||||
window.addWindowListener(new WindowListener {
|
||||
override def onMoved(window: Window, terminalPosition: TerminalPosition, terminalPosition1: TerminalPosition): Unit = {}
|
||||
|
||||
override def onResized(window: Window, terminalSize: TerminalSize, terminalSize1: TerminalSize): Unit = {}
|
||||
|
||||
override def onUnhandledInput(t: Window, keyStroke: KeyStroke, atomicBoolean: AtomicBoolean): Unit = {}
|
||||
|
||||
override def onInput(t: Window, keyStroke: KeyStroke, atomicBoolean: AtomicBoolean): Unit = {
|
||||
if (keyStroke.getCharacter == 'n') {
|
||||
val input = new TextInputDialogBuilder()
|
||||
.setTitle("Open a new channel")
|
||||
.setDescription("Node URI:")
|
||||
//.setValidationPattern(Pattern.compile("[0-9]"), "You didn't enter a single number!")
|
||||
.build()
|
||||
.showDialog(gui)
|
||||
val hostRegex = """([a-fA-F0-9]{66})@([a-zA-Z0-9:\.\-_]+):([0-9]+)""".r
|
||||
try {
|
||||
val hostRegex(nodeId, host, port) = input
|
||||
setup.switchboard ! NewConnection(PublicKey(BinaryData(nodeId)), new InetSocketAddress(host, port.toInt), Some(NewChannel(Satoshi(10000), MilliSatoshi(0))))
|
||||
} catch {
|
||||
case t: Throwable => logger.error("", t)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
gui.addWindowAndWait(window)
|
||||
setup.system.terminate()
|
||||
}
|
||||
}
|
||||
new Thread(runnable).start()
|
||||
|
||||
}
|
||||
@ -0,0 +1,27 @@
|
||||
package fr.acinq.eclair
|
||||
|
||||
import akka.actor.Actor
|
||||
import fr.acinq.bitcoin.Satoshi
|
||||
import fr.acinq.eclair.channel._
|
||||
import fr.acinq.eclair.payment.PaymentEvent
|
||||
import fr.acinq.eclair.router.NetworkEvent
|
||||
|
||||
/**
|
||||
* Created by PM on 31/05/2017.
|
||||
*/
|
||||
class TextuiUpdater(textui: Textui) extends Actor {
|
||||
context.system.eventStream.subscribe(self, classOf[ChannelEvent])
|
||||
context.system.eventStream.subscribe(self, classOf[NetworkEvent])
|
||||
context.system.eventStream.subscribe(self, classOf[PaymentEvent])
|
||||
|
||||
override def receive: Receive = {
|
||||
case ChannelCreated(channel, _, remoteNodeId, _, temporaryChannelId) =>
|
||||
textui.addChannel(channel, temporaryChannelId, remoteNodeId, WAIT_FOR_INIT_INTERNAL, Satoshi(0), Satoshi(1))
|
||||
|
||||
case ChannelRestored(channel, _, remoteNodeId, _, channelId, data) =>
|
||||
textui.addChannel(channel, channelId, remoteNodeId, OFFLINE, Satoshi(33), Satoshi(100))
|
||||
|
||||
case ChannelStateChanged(channel, _, _, _, state, _) =>
|
||||
textui.updateState(channel, state)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user