Compare commits

...

2 Commits

Author SHA1 Message Date
pm47
d50889ad7c wip 2017-06-07 16:03:48 +02:00
pm47
eb34134830 replaced java serialization by scodec 2017-06-07 16:03:05 +02:00
23 changed files with 662 additions and 165 deletions

View File

@ -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))

View File

@ -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")
}

View File

@ -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)

View File

@ -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 }
)
}

View File

@ -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
}
}

View File

@ -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
)
}

View File

@ -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]
}
}

View File

@ -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

View File

@ -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)
}

View File

@ -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")
}
}

View File

@ -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}")

View File

@ -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]
}

View File

@ -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)"
}
}

View File

@ -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)))
)

View File

@ -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,

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)
}
}

View File

@ -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>

View File

@ -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 {

View 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()
}

View File

@ -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)
}
}