diff --git a/OLD-API-DOCS.md b/OLD-API-DOCS.md
deleted file mode 100644
index 1c7f30abe..000000000
--- a/OLD-API-DOCS.md
+++ /dev/null
@@ -1,40 +0,0 @@
- ## JSON-RPC API
-
- :warning: Note this interface is being deprecated.
-
- method | params | description
- ------------- |----------------------------------------------------------------------------------------|-----------------------------------------------------------
- getinfo | | return basic node information (id, chain hash, current block height)
- connect | nodeId, host, port | open a secure connection to a lightning node
- connect | uri | open a secure connection to a lightning node
- open | nodeId, fundingSatoshis, pushMsat = 0, feerateSatPerByte = ?, channelFlags = 0x01 | open a channel with another lightning node, by default push = 0, feerate for the funding tx targets 6 blocks, and channel is announced
- updaterelayfee | channelId, feeBaseMsat, feeProportionalMillionths | update relay fee for payments going through this channel
- peers | | list existing local peers
- channels | | list existing local channels
- channels | nodeId | list existing local channels opened with a particular nodeId
- channel | channelId | retrieve detailed information about a given channel
- channelstats | | retrieves statistics about channel usage (fees, number and average amount of payments)
- allnodes | | list all known nodes
- allchannels | | list all known channels
- allupdates | | list all channels updates
- allupdates | nodeId | list all channels updates for this nodeId
- receive | description | generate a payment request without a required amount (can be useful for donations)
- receive | amountMsat, description | generate a payment request for a given amount
- receive | amountMsat, description, expirySeconds | generate a payment request for a given amount that expires after given number of seconds
- parseinvoice | paymentRequest | returns node, amount and payment hash in a payment request
- findroute | paymentRequest | returns nodes and channels of the route for this payment request if there is any
- findroute | paymentRequest, amountMsat | returns nodes and channels of the route for this payment request and amount, if there is any
- findroute | nodeId, amountMsat | returns nodes and channels of the route to the nodeId, if there is any
- send | amountMsat, paymentHash, nodeId | send a payment to a lightning node
- send | paymentRequest | send a payment to a lightning node using a BOLT11 payment request
- send | paymentRequest, amountMsat | send a payment to a lightning node using a BOLT11 payment request and a custom amount
- checkpayment | paymentHash | returns true if the payment has been received, false otherwise
- checkpayment | paymentRequest | returns true if the payment has been received, false otherwise
- close | channelId | close a channel
- close | channelId, scriptPubKey | close a channel and send the funds to the given scriptPubKey
- forceclose | channelId | force-close a channel by publishing the local commitment tx (careful: this is more expensive than a regular close and will incur a delay before funds are spendable)"
- audit | | list all send/received/relayed payments
- audit | from, to | list send/received/relayed payments in that interval (from <= timestamp < to)
- networkfees | | list all network fees paid to the miners, by transaction
- networkfees |from, to | list network fees paid to the miners, by transaction, in that interval (from <= timestamp < to)
- help | | display available methods
diff --git a/README.md b/README.md
index 39d1bc593..e5c086d72 100644
--- a/README.md
+++ b/README.md
@@ -32,8 +32,6 @@ Eclair offers a feature rich HTTP API that enables application developers to eas
For more information please visit the [API documentation website](https://acinq.github.io/eclair).
-:warning: You can still use the old API by setting the `eclair.api.use-old-api=true` parameter, but it is now deprecated and will soon be removed. The old documentation is still available [here](OLD-API-DOCS.md).
-
## Installation
### Configuring Bitcoin Core
@@ -60,7 +58,7 @@ Eclair is developed in [Scala](https://www.scala-lang.org/), a powerful function
* eclair-node, which is a headless application that you can run on servers and desktops, and control from the command line
* eclair-node-gui, which also includes a JavaFX GUI
-To run Eclair, you first need to install Java, we recommend that you use [OpenJDK 11](https://jdk.java.net/11/). Eclair will also run on Oracle JDK 1.8, Oracle JDK 11, and other versions of OpenJDK but we don't recommend using them.
+To run Eclair, you first need to install Java, we recommend that you use [OpenJDK 11](https://adoptopenjdk.net/?variant=openjdk11&jvmVariant=hotspot). Eclair will also run on Oracle JDK 1.8, Oracle JDK 11, and other versions of OpenJDK but we don't recommend using them.
Then download our latest [release](https://github.com/ACINQ/eclair/releases) and depending on whether or not you want a GUI run the following command:
* with GUI:
diff --git a/eclair-core/src/main/resources/reference.conf b/eclair-core/src/main/resources/reference.conf
index e9acd4ef1..f6fe1fed0 100644
--- a/eclair-core/src/main/resources/reference.conf
+++ b/eclair-core/src/main/resources/reference.conf
@@ -13,7 +13,6 @@ eclair {
binding-ip = "127.0.0.1"
port = 8080
password = "" // password for basic auth, must be non empty if json-rpc api is enabled
- use-old-api = false
}
watcher-type = "electrum"
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala
index 7c425328c..36ce47b51 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala
@@ -23,19 +23,20 @@ import akka.pattern._
import akka.util.Timeout
import fr.acinq.bitcoin.Crypto.PublicKey
import fr.acinq.bitcoin.{ByteVector32, MilliSatoshi, Satoshi}
+import fr.acinq.eclair.TimestampQueryFilters._
import fr.acinq.eclair.channel.Register.{Forward, ForwardShortId}
import fr.acinq.eclair.channel._
import fr.acinq.eclair.db.{IncomingPayment, NetworkFee, OutgoingPayment, Stats}
import fr.acinq.eclair.io.Peer.{GetPeerInfo, PeerInfo}
-import fr.acinq.eclair.io.{NodeURI, Peer, Switchboard}
+import fr.acinq.eclair.io.{NodeURI, Peer}
import fr.acinq.eclair.payment.PaymentLifecycle._
+import fr.acinq.eclair.payment._
import fr.acinq.eclair.router.{ChannelDesc, RouteRequest, RouteResponse, Router}
+import fr.acinq.eclair.wire.{ChannelAnnouncement, ChannelUpdate, NodeAddress, NodeAnnouncement}
import scodec.bits.ByteVector
+
import scala.concurrent.Future
import scala.concurrent.duration._
-import fr.acinq.eclair.payment.{PaymentReceived, PaymentRelayed, PaymentRequest, PaymentSent}
-import fr.acinq.eclair.wire.{ChannelAnnouncement, ChannelUpdate, NodeAddress, NodeAnnouncement}
-import TimestampQueryFilters._
case class GetInfoResponse(nodeId: PublicKey, alias: String, chainHash: ByteVector32, blockHeight: Int, publicAddresses: Seq[NodeAddress])
@@ -105,6 +106,7 @@ trait Eclair {
def getInfoResponse()(implicit timeout: Timeout): Future[GetInfoResponse]
+ def usableBalances()(implicit timeout: Timeout): Future[Iterable[UsableBalances]]
}
class EclairImpl(appKit: Kit) extends Eclair {
@@ -269,4 +271,5 @@ class EclairImpl(appKit: Kit) extends Eclair {
publicAddresses = appKit.nodeParams.publicAddresses)
)
+ override def usableBalances()(implicit timeout: Timeout): Future[Iterable[UsableBalances]] = (appKit.relayer ? GetUsableBalances).mapTo[Iterable[UsableBalances]]
}
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala
index 3fb404410..135b70843 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala
@@ -108,6 +108,7 @@ object NodeParams {
seedPath.exists() match {
case true => ByteVector(Files.toByteArray(seedPath))
case false =>
+ datadir.mkdirs()
val seed = randomBytes32
Files.write(seed.toArray, seedPath)
seed
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/electrum/ElectrumClient.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/electrum/ElectrumClient.scala
index 3be66f0a7..271d8039d 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/electrum/ElectrumClient.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/electrum/ElectrumClient.scala
@@ -91,7 +91,7 @@ class ElectrumClient(serverAddress: InetSocketAddress, ssl: SSL)(implicit val ec
val channelOpenFuture = b.connect(serverAddress.getHostName, serverAddress.getPort)
- def errorHandler(t: Throwable) = {
+ def errorHandler(t: Throwable): Unit = {
log.info("server={} connection error (reason={})", serverAddress, t.getMessage)
self ! Close
}
@@ -169,6 +169,7 @@ class ElectrumClient(serverAddress: InetSocketAddress, ssl: SSL)(implicit val ec
val json = ("method" -> request.method) ~ ("params" -> request.params.map {
case s: String => new JString(s)
case b: ByteVector32 => new JString(b.toHex)
+ case b: Boolean => new JBool(b)
case t: Int => new JInt(t)
case t: Long => new JLong(t)
case t: Double => new JDouble(t)
@@ -182,8 +183,6 @@ class ElectrumClient(serverAddress: InetSocketAddress, ssl: SSL)(implicit val ec
/**
* Forwards incoming messages to the underlying actor
- *
- * @param actor
*/
class ActorHandler(actor: ActorRef) extends ChannelInboundHandlerAdapter {
@@ -220,7 +219,7 @@ class ElectrumClient(serverAddress: InetSocketAddress, ssl: SSL)(implicit val ec
case PingResponse => ()
case Close =>
- statusListeners.map(_ ! ElectrumDisconnected)
+ statusListeners.foreach(_ ! ElectrumDisconnected)
context.stop(self)
case _ => log.warning("server={} unhandled message {}", serverAddress, message)
@@ -282,7 +281,7 @@ class ElectrumClient(serverAddress: InetSocketAddress, ssl: SSL)(implicit val ec
case Right(json: JsonRPCResponse) =>
val (height, header) = parseBlockHeader(json.result)
log.debug("connected to server={}, tip={} height={}", serverAddress, header.hash, height)
- statusListeners.map(_ ! ElectrumReady(height, header, serverAddress))
+ statusListeners.foreach(_ ! ElectrumReady(height, header, serverAddress))
context become connected(ctx, height, header, Map())
case AddStatusListener(actor) => statusListeners += actor
@@ -322,11 +321,11 @@ class ElectrumClient(serverAddress: InetSocketAddress, ssl: SSL)(implicit val ec
}
context become connected(ctx, height, tip, requests - json.id)
- case Left(response: HeaderSubscriptionResponse) => headerSubscriptions.map(_ ! response)
+ case Left(response: HeaderSubscriptionResponse) => headerSubscriptions.foreach(_ ! response)
- case Left(response: AddressSubscriptionResponse) => addressSubscriptions.get(response.address).map(listeners => listeners.map(_ ! response))
+ case Left(response: AddressSubscriptionResponse) => addressSubscriptions.get(response.address).foreach(listeners => listeners.foreach(_ ! response))
- case Left(response: ScriptHashSubscriptionResponse) => scriptHashSubscriptions.get(response.scriptHash).map(listeners => listeners.map(_ ! response))
+ case Left(response: ScriptHashSubscriptionResponse) => scriptHashSubscriptions.get(response.scriptHash).foreach(listeners => listeners.foreach(_ ! response))
case HeaderSubscriptionResponse(height, newtip) =>
log.info("server={} new tip={}", serverAddress, newtip)
@@ -381,6 +380,9 @@ object ElectrumClient {
case class BroadcastTransaction(tx: Transaction) extends Request
case class BroadcastTransactionResponse(tx: Transaction, error: Option[Error]) extends Response
+ case class GetTransactionIdFromPosition(height: Int, tx_pos: Int, merkle: Boolean = false) extends Request
+ case class GetTransactionIdFromPositionResponse(txid: ByteVector32, height: Int, tx_pos: Int, merkle: Seq[ByteVector32]) extends Response
+
case class GetTransaction(txid: ByteVector32) extends Request
case class GetTransactionResponse(tx: Transaction) extends Response
@@ -533,10 +535,11 @@ object ElectrumClient {
case AddressSubscription(address, _) => JsonRPCRequest(id = reqId, method = "blockchain.address.subscribe", params = address :: Nil)
case ScriptHashSubscription(scriptHash, _) => JsonRPCRequest(id = reqId, method = "blockchain.scripthash.subscribe", params = scriptHash.toString() :: Nil)
case BroadcastTransaction(tx) => JsonRPCRequest(id = reqId, method = "blockchain.transaction.broadcast", params = Transaction.write(tx).toHex :: Nil)
+ case GetTransactionIdFromPosition(height, tx_pos, merkle) => JsonRPCRequest(id = reqId, method = "blockchain.transaction.id_from_pos", params = height :: tx_pos :: merkle :: Nil)
case GetTransaction(txid) => JsonRPCRequest(id = reqId, method = "blockchain.transaction.get", params = txid :: Nil)
case HeaderSubscription(_) => JsonRPCRequest(id = reqId, method = "blockchain.headers.subscribe", params = Nil)
case GetHeader(height) => JsonRPCRequest(id = reqId, method = "blockchain.block.header", params = height :: Nil)
- case GetHeaders(start_height, count, cp_height) => JsonRPCRequest(id = reqId, method = "blockchain.block.headers", params = start_height :: count :: Nil)
+ case GetHeaders(start_height, count, _) => JsonRPCRequest(id = reqId, method = "blockchain.block.headers", params = start_height :: count :: Nil)
case GetMerkle(txid, height) => JsonRPCRequest(id = reqId, method = "blockchain.transaction.get_merkle", params = txid :: height :: Nil)
}
@@ -548,7 +551,7 @@ object ElectrumClient {
case _ => ServerError(request, error)
}
case None => (request: @unchecked) match {
- case s: ServerVersion =>
+ case _: ServerVersion =>
val JArray(jitems) = json.result
val JString(clientName) = jitems(0)
val JString(protocolVersion) = jitems(1)
@@ -590,6 +593,14 @@ object ElectrumClient {
UnspentItem(ByteVector32.fromValidHex(tx_hash), tx_pos, value, height)
})
ScriptHashListUnspentResponse(scripthash, items)
+ case GetTransactionIdFromPosition(height, tx_pos, false) =>
+ val JString(tx_hash) = json.result
+ GetTransactionIdFromPositionResponse(ByteVector32.fromValidHex(tx_hash), height, tx_pos, Nil)
+ case GetTransactionIdFromPosition(height, tx_pos, true) =>
+ val JString(tx_hash) = json.result \ "tx_hash"
+ val JArray(hashes) = json.result \ "merkle"
+ val leaves = hashes collect { case JString(value) => ByteVector32.fromValidHex(value) }
+ GetTransactionIdFromPositionResponse(ByteVector32.fromValidHex(tx_hash), height, tx_pos, leaves)
case GetTransaction(_) =>
val JString(hex) = json.result
GetTransactionResponse(Transaction.read(hex))
@@ -614,16 +625,15 @@ object ElectrumClient {
case GetHeader(height) =>
val JString(hex) = json.result
GetHeaderResponse(height, BlockHeader.read(hex))
- case GetHeaders(start_height, count, cp_height) =>
- val count = intField(json.result, "count")
+ case GetHeaders(start_height, _, _) =>
val max = intField(json.result, "max")
val JString(hex) = json.result \ "hex"
val bin = ByteVector.fromValidHex(hex).toArray
val blockHeaders = bin.grouped(80).map(BlockHeader.read).toList
GetHeadersResponse(start_height, blockHeaders, max)
- case GetMerkle(txid, height) =>
+ case GetMerkle(txid, _) =>
val JArray(hashes) = json.result \ "merkle"
- val leaves = hashes collect { case JString(value) => ByteVector32.fromValidHex((value)) }
+ val leaves = hashes collect { case JString(value) => ByteVector32.fromValidHex(value) }
val blockHeight = intField(json.result, "block_height")
val JInt(pos) = json.result \ "pos"
GetMerkleResponse(txid, leaves, blockHeight, pos.toInt)
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/electrum/ElectrumWallet.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/electrum/ElectrumWallet.scala
index 912fd1206..d9f3c1215 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/electrum/ElectrumWallet.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/electrum/ElectrumWallet.scala
@@ -739,7 +739,7 @@ object ElectrumWallet {
}
/**
- * @scriptHash script hash
+ *
* @return the ids of transactions that belong to our wallet history for this script hash but that we don't have
* and have no pending requests for.
*/
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/electrum/db/sqlite/SqliteWalletDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/electrum/db/sqlite/SqliteWalletDb.scala
index ca7b4fbbb..907cd45f7 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/electrum/db/sqlite/SqliteWalletDb.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/electrum/db/sqlite/SqliteWalletDb.scala
@@ -136,7 +136,7 @@ class SqliteWalletDb(sqlite: Connection) extends WalletDb {
object SqliteWalletDb {
import fr.acinq.eclair.wire.ChannelCodecs._
- import fr.acinq.eclair.wire.LightningMessageCodecs._
+ import fr.acinq.eclair.wire.CommonCodecs._
import scodec.Codec
import scodec.bits.BitVector
import scodec.codecs._
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala
index a3fa9852b..9197e396c 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala
@@ -1644,7 +1644,10 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
case Event(Status.Failure(_: CannotAffordFees), _) => stay
// funding tx was confirmed in time, let's just ignore this
- case Event(BITCOIN_FUNDING_TIMEOUT, d: HasCommitments) => stay
+ case Event(BITCOIN_FUNDING_TIMEOUT, _: HasCommitments) => stay
+
+ // peer doesn't cancel the timer
+ case Event(TickChannelOpenTimeout, _) => stay
case Event(WatchEventSpent(BITCOIN_FUNDING_SPENT, tx), d: HasCommitments) if tx.txid == d.commitments.localCommit.publishableTxs.commitTx.tx.txid =>
log.warning(s"processing local commit spent in catch-all handler")
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala
index 8d15614fb..fd044ee1c 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala
@@ -71,12 +71,18 @@ case class Commitments(localParams: LocalParams, remoteParams: RemoteParams,
def addRemoteProposal(proposal: UpdateMessage): Commitments = Commitments.addRemoteProposal(this, proposal)
- def announceChannel: Boolean = (channelFlags & 0x01) != 0
+ val announceChannel: Boolean = (channelFlags & 0x01) != 0
- def availableBalanceForSendMsat: Long = {
+ lazy val availableBalanceForSendMsat: Long = {
val reduced = CommitmentSpec.reduce(remoteCommit.spec, remoteChanges.acked, localChanges.proposed)
val feesMsat = if (localParams.isFunder) Transactions.commitTxFee(Satoshi(remoteParams.dustLimitSatoshis), reduced).amount * 1000 else 0
- reduced.toRemoteMsat - remoteParams.channelReserveSatoshis * 1000 - feesMsat
+ math.max(reduced.toRemoteMsat - remoteParams.channelReserveSatoshis * 1000 - feesMsat, 0)
+ }
+
+ lazy val availableBalanceForReceiveMsat: Long = {
+ val reduced = CommitmentSpec.reduce(localCommit.spec, localChanges.acked, remoteChanges.proposed)
+ val feesMsat = if (localParams.isFunder) 0 else Transactions.commitTxFee(Satoshi(localParams.dustLimitSatoshis), reduced).amount * 1000
+ math.max(reduced.toRemoteMsat - localParams.channelReserveSatoshis * 1000 - feesMsat, 0)
}
}
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/ShaChain.scala b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/ShaChain.scala
index 985136ae8..94438b65e 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/ShaChain.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/ShaChain.scala
@@ -17,7 +17,7 @@
package fr.acinq.eclair.crypto
import fr.acinq.bitcoin._
-import fr.acinq.eclair.wire.LightningMessageCodecs
+import fr.acinq.eclair.wire.CommonCodecs
import scodec.Codec
import scala.annotation.tailrec
@@ -117,7 +117,7 @@ object ShaChain {
import scodec.codecs._
// codec for a single map entry (i.e. Vector[Boolean] -> ByteVector
- val entryCodec = vectorOfN(uint16, bool) ~ variableSizeBytes(uint16, LightningMessageCodecs.bytes32)
+ val entryCodec = vectorOfN(uint16, bool) ~ variableSizeBytes(uint16, CommonCodecs.bytes32)
// codec for a Map[Vector[Boolean], ByteVector]: write all k -> v pairs using the codec defined above
val mapCodec: Codec[Map[Vector[Boolean], ByteVector32]] = Codec[Map[Vector[Boolean], ByteVector32]](
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePeersDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePeersDb.scala
index f34d98d50..8d9e828ba 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePeersDb.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePeersDb.scala
@@ -38,7 +38,7 @@ import SqliteUtils.ExtendedResultSet._
}
override def addOrUpdatePeer(nodeId: Crypto.PublicKey, nodeaddress: NodeAddress): Unit = {
- val data = LightningMessageCodecs.nodeaddress.encode(nodeaddress).require.toByteArray
+ val data = CommonCodecs.nodeaddress.encode(nodeaddress).require.toByteArray
using(sqlite.prepareStatement("UPDATE peers SET data=? WHERE node_id=?")) { update =>
update.setBytes(1, data)
update.setBytes(2, nodeId.value.toArray)
@@ -65,7 +65,7 @@ import SqliteUtils.ExtendedResultSet._
var m: Map[PublicKey, NodeAddress] = Map()
while (rs.next()) {
val nodeid = PublicKey(rs.getByteVector("node_id"))
- val nodeaddress = LightningMessageCodecs.nodeaddress.decode(BitVector(rs.getBytes("data"))).require.value
+ val nodeaddress = CommonCodecs.nodeaddress.decode(BitVector(rs.getBytes("data"))).require.value
m += (nodeid -> nodeaddress)
}
m
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/Authenticator.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/Authenticator.scala
index 1aebf9c0f..f77b26354 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/io/Authenticator.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/Authenticator.scala
@@ -47,9 +47,9 @@ class Authenticator(nodeParams: NodeParams) extends Actor with DiagnosticActorLo
KeyPair(nodeParams.nodeId.value, nodeParams.privateKey.value),
remoteNodeId_opt.map(_.value),
connection = connection,
- codec = LightningMessageCodecs.cachedLightningMessageCodec))
+ codec = LightningMessageCodecs.lightningMessageCodec))
context watch transport
- context become (ready(switchboard, authenticating + (transport -> pending)))
+ context become ready(switchboard, authenticating + (transport -> pending))
case HandshakeCompleted(connection, transport, remoteNodeId) if authenticating.contains(transport) =>
val pendingAuth = authenticating(transport)
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala
index 4d2d92dc3..e50177f98 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala
@@ -29,13 +29,11 @@ import fr.acinq.bitcoin.{ByteVector32, DeterministicWallet, MilliSatoshi, Protoc
import fr.acinq.eclair.blockchain.EclairWallet
import fr.acinq.eclair.channel._
import fr.acinq.eclair.crypto.TransportHandler
-import fr.acinq.eclair.secureRandom
import fr.acinq.eclair.router._
import fr.acinq.eclair.wire._
-import fr.acinq.eclair.{wire, _}
+import fr.acinq.eclair.{secureRandom, wire, _}
import scodec.Attempt
import scodec.bits.ByteVector
-
import scala.compat.Platform
import scala.concurrent.duration._
import scala.util.Random
@@ -88,7 +86,7 @@ class Peer(nodeParams: NodeParams, remoteNodeId: PublicKey, authenticator: Actor
context.actorOf(Client.props(nodeParams, authenticator, address, remoteNodeId, origin_opt = None))
log.info(s"reconnecting to $address")
// exponential backoff retry with a finite max
- setTimer(RECONNECT_TIMER, Reconnect, Math.min(10 + Math.pow(2, d.attempts), 20) seconds, repeat = false)
+ setTimer(RECONNECT_TIMER, Reconnect, Math.min(10 + Math.pow(2, d.attempts), 3600) seconds, repeat = false)
stay using d.copy(attempts = d.attempts + 1)
}
@@ -207,6 +205,11 @@ class Peer(nodeParams: NodeParams, remoteNodeId: PublicKey, authenticator: Actor
d.transport ! PoisonPill
stay
+ case Event(unhandledMsg: LightningMessage, d: InitializingData) =>
+ // we ack unhandled messages because we don't want to block further reads on the connection
+ d.transport ! TransportHandler.ReadAck(unhandledMsg)
+ log.warning(s"acking unhandled message $unhandledMsg")
+ stay
}
when(CONNECTED) {
@@ -474,6 +477,12 @@ class Peer(nodeParams: NodeParams, remoteNodeId: PublicKey, authenticator: Actor
d.channels.values.toSet[ActorRef].foreach(_ ! INPUT_DISCONNECTED) // we deduplicate with toSet because there might be two entries per channel (tmp id and final id)
self ! h
goto(DISCONNECTED) using DisconnectedData(d.address_opt, d.channels.collect { case (k: FinalChannelId, v) => (k, v) })
+
+ case Event(unhandledMsg: LightningMessage, d: ConnectedData) =>
+ // we ack unhandled messages because we don't want to block further reads on the connection
+ d.transport ! TransportHandler.ReadAck(unhandledMsg)
+ log.warning(s"acking unhandled message $unhandledMsg")
+ stay
}
whenUnhandled {
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentRequest.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentRequest.scala
index e34628560..b35b0d7b1 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentRequest.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentRequest.scala
@@ -16,8 +16,6 @@
package fr.acinq.eclair.payment
-import java.math.BigInteger
-
import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey}
import fr.acinq.bitcoin.{MilliSatoshi, _}
import fr.acinq.eclair.ShortChannelId
@@ -303,7 +301,7 @@ object PaymentRequest {
object Codecs {
- import fr.acinq.eclair.wire.LightningMessageCodecs._
+ import fr.acinq.eclair.wire.CommonCodecs._
import scodec.bits.BitVector
import scodec.codecs._
import scodec.{Attempt, Codec, DecodeResult}
@@ -399,6 +397,7 @@ object PaymentRequest {
case a if a.last == 'n' => Some(MilliSatoshi(a.dropRight(1).toLong * 100L))
case a if a.last == 'u' => Some(MilliSatoshi(a.dropRight(1).toLong * 100000L))
case a if a.last == 'm' => Some(MilliSatoshi(a.dropRight(1).toLong * 100000000L))
+ case a => Some(MilliSatoshi(a.toLong * 100000000000L))
}
def encode(amount: Option[MilliSatoshi]): String = {
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Relayer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Relayer.scala
index 0d0b55fe4..dd27dc096 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Relayer.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Relayer.scala
@@ -47,6 +47,9 @@ case class ForwardFulfill(fulfill: UpdateFulfillHtlc, to: Origin, htlc: UpdateAd
case class ForwardFail(fail: UpdateFailHtlc, to: Origin, htlc: UpdateAddHtlc) extends ForwardMessage
case class ForwardFailMalformed(fail: UpdateFailMalformedHtlc, to: Origin, htlc: UpdateAddHtlc) extends ForwardMessage
+case object GetUsableBalances
+case class UsableBalances(remoteNodeId: PublicKey, shortChannelId: ShortChannelId, canSendMsat: Long, canReceiveMsat: Long, isPublic: Boolean)
+
// @formatter:on
@@ -69,10 +72,20 @@ class Relayer(nodeParams: NodeParams, register: ActorRef, paymentHandler: ActorR
override def receive: Receive = main(Map.empty, new mutable.HashMap[PublicKey, mutable.Set[ShortChannelId]] with mutable.MultiMap[PublicKey, ShortChannelId])
def main(channelUpdates: Map[ShortChannelId, OutgoingChannel], node2channels: mutable.HashMap[PublicKey, mutable.Set[ShortChannelId]] with mutable.MultiMap[PublicKey, ShortChannelId]): Receive = {
+ case GetUsableBalances =>
+ sender ! channelUpdates.values
+ .filter(o => Announcements.isEnabled(o.channelUpdate.channelFlags))
+ .map(o => UsableBalances(
+ remoteNodeId = o.nextNodeId,
+ shortChannelId = o.channelUpdate.shortChannelId,
+ canSendMsat = o.commitments.availableBalanceForSendMsat,
+ canReceiveMsat = o.commitments.availableBalanceForReceiveMsat,
+ isPublic = o.commitments.announceChannel))
case LocalChannelUpdate(_, channelId, shortChannelId, remoteNodeId, _, channelUpdate, commitments) =>
log.debug(s"updating local channel info for channelId=$channelId shortChannelId=$shortChannelId remoteNodeId=$remoteNodeId channelUpdate={} commitments={}", channelUpdate, commitments)
- context become main(channelUpdates + (channelUpdate.shortChannelId -> OutgoingChannel(remoteNodeId, channelUpdate, commitments.availableBalanceForSendMsat)), node2channels.addBinding(remoteNodeId, channelUpdate.shortChannelId))
+ val channelUpdates1 = channelUpdates + (channelUpdate.shortChannelId -> OutgoingChannel(remoteNodeId, channelUpdate, commitments))
+ context become main(channelUpdates1, node2channels.addBinding(remoteNodeId, channelUpdate.shortChannelId))
case LocalChannelDown(_, channelId, shortChannelId, remoteNodeId) =>
log.debug(s"removed local channel info for channelId=$channelId shortChannelId=$shortChannelId")
@@ -80,7 +93,7 @@ class Relayer(nodeParams: NodeParams, register: ActorRef, paymentHandler: ActorR
case AvailableBalanceChanged(_, _, shortChannelId, _, commitments) =>
val channelUpdates1 = channelUpdates.get(shortChannelId) match {
- case Some(c: OutgoingChannel) => channelUpdates + (shortChannelId -> c.copy(availableBalanceMsat = commitments.availableBalanceForSendMsat))
+ case Some(c: OutgoingChannel) => channelUpdates + (shortChannelId -> c.copy(commitments = commitments))
case None => channelUpdates // we only consider the balance if we have the channel_update
}
context become main(channelUpdates1, node2channels)
@@ -197,7 +210,7 @@ class Relayer(nodeParams: NodeParams, register: ActorRef, paymentHandler: ActorR
object Relayer {
def props(nodeParams: NodeParams, register: ActorRef, paymentHandler: ActorRef) = Props(classOf[Relayer], nodeParams, register, paymentHandler)
- case class OutgoingChannel(nextNodeId: PublicKey, channelUpdate: ChannelUpdate, availableBalanceMsat: Long)
+ case class OutgoingChannel(nextNodeId: PublicKey, channelUpdate: ChannelUpdate, commitments: Commitments)
// @formatter:off
sealed trait NextPayload
@@ -302,10 +315,10 @@ object Relayer {
val channelInfo_opt = channelUpdates.get(shortChannelId)
val channelUpdate_opt = channelInfo_opt.map(_.channelUpdate)
val relayResult = relayOrFail(relayPayload, channelUpdate_opt)
- log.debug(s"candidate channel for htlc #${add.id} paymentHash=${add.paymentHash}: shortChannelId={} balanceMsat={} channelUpdate={} relayResult={}", shortChannelId, channelInfo_opt.map(_.availableBalanceMsat).getOrElse(""), channelUpdate_opt.getOrElse(""), relayResult)
+ log.debug(s"candidate channel for htlc #${add.id} paymentHash=${add.paymentHash}: shortChannelId={} balanceMsat={} channelUpdate={} relayResult={}", shortChannelId, channelInfo_opt.map(_.commitments.availableBalanceForSendMsat).getOrElse(""), channelUpdate_opt.getOrElse(""), relayResult)
(shortChannelId, channelInfo_opt, relayResult)
}
- .collect { case (shortChannelId, Some(channelInfo), Right(_)) => (shortChannelId, channelInfo.availableBalanceMsat) }
+ .collect { case (shortChannelId, Some(channelInfo), Right(_)) => (shortChannelId, channelInfo.commitments.availableBalanceForSendMsat) }
.filter(_._2 > relayPayload.payload.amtToForward) // we only keep channels that have enough balance to handle this payment
.toList // needed for ordering
.sortBy(_._2) // we want to use the channel with the lowest available balance that can process the payment
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/Announcements.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/Announcements.scala
index 723fb8876..28b4389cf 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/router/Announcements.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/Announcements.scala
@@ -124,6 +124,7 @@ object Announcements {
* @return true if channel updates are "equal"
*/
def areSame(u1: ChannelUpdate, u2: ChannelUpdate): Boolean =
+ // NB: On Android, we don't compare chain_hash and signature, because they are stripped
u1.copy(chainHash = ByteVector32.Zeroes, signature = ByteVector64.Zeroes, timestamp = 0) == u2.copy(chainHash = ByteVector32.Zeroes, signature = ByteVector64.Zeroes, timestamp = 0) // README: on Android we discard chainHash too
def makeMessageFlags(hasOptionChannelHtlcMax: Boolean): Byte = BitVector.bits(hasOptionChannelHtlcMax :: Nil).padLeft(8).toByte()
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala
index 8b479c78d..282ab7293 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala
@@ -20,6 +20,7 @@ import akka.Done
import akka.actor.{ActorRef, Props, Status}
import akka.event.Logging.MDC
import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Satoshi}
+import fr.acinq.bitcoin.{ByteVector32, ByteVector64}
import fr.acinq.bitcoin.Crypto.PublicKey
import fr.acinq.bitcoin.Script.{pay2wsh, write}
import fr.acinq.eclair._
@@ -140,6 +141,8 @@ class Router(nodeParams: NodeParams, watcher: ActorRef, initialized: Option[Prom
// this will be used to calculate routes
val graph = DirectedGraph.makeGraph(initChannelUpdates)
+ // On Android we don't watch the funding tx outputs of public channels
+
log.info(s"initialization completed, ready to process messages")
Try(initialized.map(_.success(Done)))
startWith(NORMAL, Data(Map.empty, initChannels, initChannelUpdates, Stash(Map.empty, Map.empty), awaiting = Map.empty, privateChannels = Map.empty, privateUpdates = Map.empty, excludedChannels = Set.empty, graph, sync = Map.empty))
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/ChannelCodecs.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/ChannelCodecs.scala
index 66569bc30..b5164b485 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/ChannelCodecs.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/ChannelCodecs.scala
@@ -26,6 +26,7 @@ import fr.acinq.eclair.crypto.ShaChain
import fr.acinq.eclair.payment.{Local, Origin, Relayed}
import fr.acinq.eclair.transactions.Transactions._
import fr.acinq.eclair.transactions._
+import fr.acinq.eclair.wire.CommonCodecs._
import fr.acinq.eclair.wire.LightningMessageCodecs._
import grizzled.slf4j.Logging
import scodec.bits.BitVector
@@ -35,7 +36,6 @@ import scodec.{Attempt, Codec}
import scala.compat.Platform
import scala.concurrent.duration._
-
/**
* Created by PM on 02/06/2017.
*/
@@ -53,10 +53,10 @@ object ChannelCodecs extends Logging {
val localParamsCodec: Codec[LocalParams] = (
("nodeId" | publicKey) ::
("channelPath" | keyPathCodec) ::
- ("dustLimitSatoshis" | uint64) ::
- ("maxHtlcValueInFlightMsat" | uint64ex) ::
- ("channelReserveSatoshis" | uint64) ::
- ("htlcMinimumMsat" | uint64) ::
+ ("dustLimitSatoshis" | uint64overflow) ::
+ ("maxHtlcValueInFlightMsat" | uint64) ::
+ ("channelReserveSatoshis" | uint64overflow) ::
+ ("htlcMinimumMsat" | uint64overflow) ::
("toSelfDelay" | uint16) ::
("maxAcceptedHtlcs" | uint16) ::
("isFunder" | bool) ::
@@ -66,10 +66,10 @@ object ChannelCodecs extends Logging {
val remoteParamsCodec: Codec[RemoteParams] = (
("nodeId" | publicKey) ::
- ("dustLimitSatoshis" | uint64) ::
- ("maxHtlcValueInFlightMsat" | uint64ex) ::
- ("channelReserveSatoshis" | uint64) ::
- ("htlcMinimumMsat" | uint64) ::
+ ("dustLimitSatoshis" | uint64overflow) ::
+ ("maxHtlcValueInFlightMsat" | uint64) ::
+ ("channelReserveSatoshis" | uint64overflow) ::
+ ("htlcMinimumMsat" | uint64overflow) ::
("toSelfDelay" | uint16) ::
("maxAcceptedHtlcs" | uint16) ::
("fundingPubKey" | publicKey) ::
@@ -97,14 +97,14 @@ object ChannelCodecs extends Logging {
val commitmentSpecCodec: Codec[CommitmentSpec] = (
("htlcs" | setCodec(htlcCodec)) ::
("feeratePerKw" | uint32) ::
- ("toLocalMsat" | uint64) ::
- ("toRemoteMsat" | uint64)).as[CommitmentSpec]
+ ("toLocalMsat" | uint64overflow) ::
+ ("toRemoteMsat" | uint64overflow)).as[CommitmentSpec]
- def outPointCodec: Codec[OutPoint] = variableSizeBytes(uint16, bytes.xmap(d => OutPoint.read(d.toArray), d => OutPoint.write(d)))
+ val outPointCodec: Codec[OutPoint] = variableSizeBytes(uint16, bytes.xmap(d => OutPoint.read(d.toArray), d => OutPoint.write(d)))
- def txOutCodec: Codec[TxOut] = variableSizeBytes(uint16, bytes.xmap(d => TxOut.read(d.toArray), d => TxOut.write(d)))
+ val txOutCodec: Codec[TxOut] = variableSizeBytes(uint16, bytes.xmap(d => TxOut.read(d.toArray), d => TxOut.write(d)))
- def txCodec: Codec[Transaction] = variableSizeBytes(uint16, bytes.xmap(d => Transaction.read(d.toArray), d => Transaction.write(d)))
+ val txCodec: Codec[Transaction] = variableSizeBytes(uint16, bytes.xmap(d => Transaction.read(d.toArray), d => Transaction.write(d)))
val inputInfoCodec: Codec[InputInfo] = (
("outPoint" | outPointCodec) ::
@@ -142,12 +142,12 @@ object ChannelCodecs extends Logging {
("htlcTxsAndSigs" | listOfN(uint16, htlcTxAndSigsCodec))).as[PublishableTxs]
val localCommitCodec: Codec[LocalCommit] = (
- ("index" | uint64) ::
+ ("index" | uint64overflow) ::
("spec" | commitmentSpecCodec) ::
("publishableTxs" | publishableTxsCodec)).as[LocalCommit]
val remoteCommitCodec: Codec[RemoteCommit] = (
- ("index" | uint64) ::
+ ("index" | uint64overflow) ::
("spec" | commitmentSpecCodec) ::
("txid" | bytes32) ::
("remotePerCommitmentPoint" | publicKey)).as[RemoteCommit]
@@ -167,7 +167,7 @@ object ChannelCodecs extends Logging {
val waitingForRevocationCodec: Codec[WaitingForRevocation] = (
("nextRemoteCommit" | remoteCommitCodec) ::
("sent" | commitSigCodec) ::
- ("sentAfterLocalCommitIndex" | uint64) ::
+ ("sentAfterLocalCommitIndex" | uint64overflow) ::
("reSignAsap" | bool)).as[WaitingForRevocation]
val localCodec: Codec[Local] = (
@@ -178,8 +178,8 @@ object ChannelCodecs extends Logging {
val relayedCodec: Codec[Relayed] = (
("originChannelId" | bytes32) ::
("originHtlcId" | int64) ::
- ("amountMsatIn" | uint64) ::
- ("amountMsatOut" | uint64)).as[Relayed]
+ ("amountMsatIn" | uint64overflow) ::
+ ("amountMsatOut" | uint64overflow)).as[Relayed]
// this is for backward compatibility to handle legacy payments that didn't have identifiers
val UNKNOWN_UUID = UUID.fromString("00000000-0000-0000-0000-000000000000")
@@ -211,8 +211,8 @@ object ChannelCodecs extends Logging {
("remoteCommit" | remoteCommitCodec) ::
("localChanges" | localChangesCodec) ::
("remoteChanges" | remoteChangesCodec) ::
- ("localNextHtlcId" | uint64) ::
- ("remoteNextHtlcId" | uint64) ::
+ ("localNextHtlcId" | uint64overflow) ::
+ ("remoteNextHtlcId" | uint64overflow) ::
("originChannels" | originsMapCodec) ::
("remoteNextCommitInfo" | either(bool, waitingForRevocationCodec, publicKey)) ::
("commitInput" | inputInfoCodec) ::
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/CommandCodecs.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/CommandCodecs.scala
index e70677aa5..dac5191ae 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/CommandCodecs.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/CommandCodecs.scala
@@ -17,8 +17,8 @@
package fr.acinq.eclair.wire
import fr.acinq.eclair.channel.{CMD_FAIL_HTLC, CMD_FAIL_MALFORMED_HTLC, CMD_FULFILL_HTLC, Command}
+import fr.acinq.eclair.wire.CommonCodecs._
import fr.acinq.eclair.wire.FailureMessageCodecs.failureMessageCodec
-import fr.acinq.eclair.wire.LightningMessageCodecs._
import scodec.Codec
import scodec.codecs._
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/CommonCodecs.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/CommonCodecs.scala
new file mode 100644
index 000000000..143a51666
--- /dev/null
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/CommonCodecs.scala
@@ -0,0 +1,128 @@
+/*
+ * Copyright 2019 ACINQ SAS
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package fr.acinq.eclair.wire
+
+import java.net.{Inet4Address, Inet6Address, InetAddress}
+
+import fr.acinq.bitcoin.{ByteVector32, ByteVector64}
+import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey}
+import fr.acinq.eclair.{ShortChannelId, UInt64}
+import org.apache.commons.codec.binary.Base32
+import scodec.{Attempt, Codec, DecodeResult, Err, SizeBound}
+import scodec.bits.{BitVector, ByteVector}
+import scodec.codecs._
+
+import scala.util.Try
+
+/**
+ * Created by t-bast on 20/06/2019.
+ */
+
+object CommonCodecs {
+
+ /**
+ * Discriminator codec with a default fallback codec (of the same type).
+ */
+ def discriminatorWithDefault[A](discriminator: Codec[A], fallback: Codec[A]): Codec[A] = new Codec[A] {
+ def sizeBound: SizeBound = discriminator.sizeBound | fallback.sizeBound
+
+ def encode(e: A): Attempt[BitVector] = discriminator.encode(e).recoverWith { case _ => fallback.encode(e) }
+
+ def decode(b: BitVector): Attempt[DecodeResult[A]] = discriminator.decode(b).recoverWith {
+ case _: KnownDiscriminatorType[_]#UnknownDiscriminator => fallback.decode(b)
+ }
+ }
+
+ // this codec can be safely used for values < 2^63 and will fail otherwise
+ // (for something smarter see https://github.com/yzernik/bitcoin-scodec/blob/master/src/main/scala/io/github/yzernik/bitcoinscodec/structures/UInt64.scala)
+ val uint64overflow: Codec[Long] = int64.narrow(l => if (l >= 0) Attempt.Successful(l) else Attempt.failure(Err(s"overflow for value $l")), l => l)
+
+ val uint64: Codec[UInt64] = bytes(8).xmap(b => UInt64(b), a => a.toByteVector.padLeft(8))
+
+ val uint64L: Codec[UInt64] = bytes(8).xmap(b => UInt64(b.reverse), a => a.toByteVector.padLeft(8).reverse)
+
+ /**
+ * We impose a minimal encoding on varint values to ensure that signed hashes can be reproduced easily.
+ * If a value could be encoded with less bytes, it's considered invalid and results in a failed decoding attempt.
+ *
+ * @param codec the integer codec (depends on the value).
+ * @param min the minimal value that should be encoded.
+ */
+ def uint64min(codec: Codec[UInt64], min: UInt64): Codec[UInt64] = codec.exmap({
+ case i if i < min => Attempt.failure(Err("varint was not minimally encoded"))
+ case i => Attempt.successful(i)
+ }, Attempt.successful)
+
+ // Bitcoin-style varint codec (CompactSize).
+ // See https://bitcoin.org/en/developer-reference#compactsize-unsigned-integers for reference.
+ val varint: Codec[UInt64] = discriminatorWithDefault(
+ discriminated[UInt64].by(uint8L)
+ .\(0xff) { case i if i >= UInt64(0x100000000L) => i }(uint64min(uint64L, UInt64(0x100000000L)))
+ .\(0xfe) { case i if i >= UInt64(0x10000) => i }(uint64min(uint32L.xmap(UInt64(_), _.toBigInt.toLong), UInt64(0x10000)))
+ .\(0xfd) { case i if i >= UInt64(0xfd) => i }(uint64min(uint16L.xmap(UInt64(_), _.toBigInt.toInt), UInt64(0xfd))),
+ uint8L.xmap(UInt64(_), _.toBigInt.toInt)
+ )
+
+ // This codec can be safely used for values < 2^63 and will fail otherwise.
+ // It is useful in combination with variableSizeBytesLong to encode/decode TLV lengths because those will always be < 2^63.
+ val varintoverflow: Codec[Long] = varint.narrow(l => if (l <= UInt64(Long.MaxValue)) Attempt.successful(l.toBigInt.toLong) else Attempt.failure(Err(s"overflow for value $l")), l => UInt64(l))
+
+ val bytes32: Codec[ByteVector32] = limitedSizeBytes(32, bytesStrict(32).xmap(d => ByteVector32(d), d => d.bytes))
+
+ val bytes64: Codec[ByteVector64] = limitedSizeBytes(64, bytesStrict(64).xmap(d => ByteVector64(d), d => d.bytes))
+
+ val sha256: Codec[ByteVector32] = bytes32
+
+ val varsizebinarydata: Codec[ByteVector] = variableSizeBytes(uint16, bytes)
+
+ val listofsignatures: Codec[List[ByteVector64]] = listOfN(uint16, bytes64)
+
+ val ipv4address: Codec[Inet4Address] = bytes(4).xmap(b => InetAddress.getByAddress(b.toArray).asInstanceOf[Inet4Address], a => ByteVector(a.getAddress))
+
+ val ipv6address: Codec[Inet6Address] = bytes(16).exmap(b => Attempt.fromTry(Try(Inet6Address.getByAddress(null, b.toArray, null))), a => Attempt.fromTry(Try(ByteVector(a.getAddress))))
+
+ def base32(size: Int): Codec[String] = bytes(size).xmap(b => new Base32().encodeAsString(b.toArray).toLowerCase, a => ByteVector(new Base32().decode(a.toUpperCase())))
+
+ val nodeaddress: Codec[NodeAddress] =
+ discriminated[NodeAddress].by(uint8)
+ .typecase(1, (ipv4address :: uint16).as[IPv4])
+ .typecase(2, (ipv6address :: uint16).as[IPv6])
+ .typecase(3, (base32(10) :: uint16).as[Tor2])
+ .typecase(4, (base32(35) :: uint16).as[Tor3])
+
+ // this one is a bit different from most other codecs: the first 'len' element is *not* the number of items
+ // in the list but rather the number of bytes of the encoded list. The rationale is once we've read this
+ // number of bytes we can just skip to the next field
+ val listofnodeaddresses: Codec[List[NodeAddress]] = variableSizeBytes(uint16, list(nodeaddress))
+
+ val shortchannelid: Codec[ShortChannelId] = int64.xmap(l => ShortChannelId(l), s => s.toLong)
+
+ val privateKey: Codec[PrivateKey] = Codec[PrivateKey](
+ (priv: PrivateKey) => bytes(32).encode(priv.value),
+ (wire: BitVector) => bytes(32).decode(wire).map(_.map(b => PrivateKey(b)))
+ )
+
+ val publicKey: Codec[PublicKey] = Codec[PublicKey](
+ (pub: PublicKey) => bytes(33).encode(pub.value),
+ (wire: BitVector) => bytes(33).decode(wire).map(_.map(b => PublicKey(b)))
+ )
+
+ val rgb: Codec[Color] = bytes(3).xmap(buf => Color(buf(0), buf(1), buf(2)), t => ByteVector(t.r, t.g, t.b))
+
+ def zeropaddedstring(size: Int): Codec[String] = fixedSizeBytes(32, utf8).xmap(s => s.takeWhile(_ != '\u0000'), s => s)
+
+}
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/FailureMessage.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/FailureMessage.scala
index 7039f0036..fc1233283 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/FailureMessage.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/FailureMessage.scala
@@ -17,9 +17,10 @@
package fr.acinq.eclair.wire
import fr.acinq.bitcoin.ByteVector32
-import fr.acinq.eclair.wire.LightningMessageCodecs.{bytes32, channelUpdateCodec, uint64}
+import fr.acinq.eclair.wire.CommonCodecs.{sha256, uint64overflow}
+import fr.acinq.eclair.wire.LightningMessageCodecs.channelUpdateCodec
import scodec.codecs._
-import scodec.{Attempt, Codec}
+import scodec.Attempt
/**
* see https://github.com/lightningnetwork/lightning-rfc/blob/master/04-onion-routing.md
@@ -63,8 +64,6 @@ object FailureMessageCodecs {
val NODE = 0x2000
val UPDATE = 0x1000
- val sha256Codec: Codec[ByteVector32] = ("sha256Codec" | bytes32)
-
val channelUpdateCodecWithType = LightningMessageCodecs.lightningMessageCodec.narrow[ChannelUpdate](f => Attempt.successful(f.asInstanceOf[ChannelUpdate]), g => g)
// NB: for historical reasons some implementations were including/ommitting the message type (258 for ChannelUpdate)
@@ -76,22 +75,22 @@ object FailureMessageCodecs {
.typecase(NODE | 2, provide(TemporaryNodeFailure))
.typecase(PERM | 2, provide(PermanentNodeFailure))
.typecase(PERM | NODE | 3, provide(RequiredNodeFeatureMissing))
- .typecase(BADONION | PERM | 4, sha256Codec.as[InvalidOnionVersion])
- .typecase(BADONION | PERM | 5, sha256Codec.as[InvalidOnionHmac])
- .typecase(BADONION | PERM | 6, sha256Codec.as[InvalidOnionKey])
- .typecase(UPDATE | 7, (("channelUpdate" | channelUpdateWithLengthCodec)).as[TemporaryChannelFailure])
+ .typecase(BADONION | PERM | 4, sha256.as[InvalidOnionVersion])
+ .typecase(BADONION | PERM | 5, sha256.as[InvalidOnionHmac])
+ .typecase(BADONION | PERM | 6, sha256.as[InvalidOnionKey])
+ .typecase(UPDATE | 7, ("channelUpdate" | channelUpdateWithLengthCodec).as[TemporaryChannelFailure])
.typecase(PERM | 8, provide(PermanentChannelFailure))
.typecase(PERM | 9, provide(RequiredChannelFeatureMissing))
.typecase(PERM | 10, provide(UnknownNextPeer))
- .typecase(UPDATE | 11, (("amountMsat" | uint64) :: ("channelUpdate" | channelUpdateWithLengthCodec)).as[AmountBelowMinimum])
- .typecase(UPDATE | 12, (("amountMsat" | uint64) :: ("channelUpdate" | channelUpdateWithLengthCodec)).as[FeeInsufficient])
+ .typecase(UPDATE | 11, (("amountMsat" | uint64overflow) :: ("channelUpdate" | channelUpdateWithLengthCodec)).as[AmountBelowMinimum])
+ .typecase(UPDATE | 12, (("amountMsat" | uint64overflow) :: ("channelUpdate" | channelUpdateWithLengthCodec)).as[FeeInsufficient])
.typecase(UPDATE | 13, (("expiry" | uint32) :: ("channelUpdate" | channelUpdateWithLengthCodec)).as[IncorrectCltvExpiry])
- .typecase(UPDATE | 14, (("channelUpdate" | channelUpdateWithLengthCodec)).as[ExpiryTooSoon])
+ .typecase(UPDATE | 14, ("channelUpdate" | channelUpdateWithLengthCodec).as[ExpiryTooSoon])
.typecase(UPDATE | 20, (("messageFlags" | byte) :: ("channelFlags" | byte) :: ("channelUpdate" | channelUpdateWithLengthCodec)).as[ChannelDisabled])
- .typecase(PERM | 15, (("amountMsat" | withDefaultValue(optional(bitsRemaining, uint64), 0L))).as[IncorrectOrUnknownPaymentDetails])
+ .typecase(PERM | 15, ("amountMsat" | withDefaultValue(optional(bitsRemaining, uint64overflow), 0L)).as[IncorrectOrUnknownPaymentDetails])
.typecase(PERM | 16, provide(IncorrectPaymentAmount))
.typecase(17, provide(FinalExpiryTooSoon))
- .typecase(18, (("expiry" | uint32)).as[FinalIncorrectCltvExpiry])
- .typecase(19, (("amountMsat" | uint64)).as[FinalIncorrectHtlcAmount])
+ .typecase(18, ("expiry" | uint32).as[FinalIncorrectCltvExpiry])
+ .typecase(19, ("amountMsat" | uint64overflow).as[FinalIncorrectHtlcAmount])
.typecase(21, provide(ExpiryTooFar))
}
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/FixedSizeStrictCodec.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/FixedSizeStrictCodec.scala
deleted file mode 100644
index d5afa984c..000000000
--- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/FixedSizeStrictCodec.scala
+++ /dev/null
@@ -1,75 +0,0 @@
-/*
- * Copyright 2019 ACINQ SAS
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-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)"
- }
-}
\ No newline at end of file
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/LightningMessageCodecs.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/LightningMessageCodecs.scala
index c1669d8dc..468573788 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/LightningMessageCodecs.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/LightningMessageCodecs.scala
@@ -16,80 +16,18 @@
package fr.acinq.eclair.wire
-import java.net.{Inet4Address, Inet6Address, InetAddress}
-
-import com.google.common.cache.{CacheBuilder, CacheLoader}
-import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey}
-import fr.acinq.bitcoin.{ByteVector32, ByteVector64}
import fr.acinq.eclair.crypto.Sphinx
-import fr.acinq.eclair.wire.FixedSizeStrictCodec.bytesStrict
-import fr.acinq.eclair.{ShortChannelId, UInt64, wire}
-import org.apache.commons.codec.binary.Base32
-import scodec.bits.{BitVector, ByteVector}
+import fr.acinq.eclair.wire
+import fr.acinq.eclair.wire.CommonCodecs._
+import scodec.bits.ByteVector
import scodec.codecs._
-import scodec.{Attempt, Codec, DecodeResult, Err, SizeBound}
-
-import scala.util.{Failure, Success, Try}
-
+import scodec.Codec
/**
* Created by PM on 15/11/2016.
*/
object LightningMessageCodecs {
- def attemptFromTry[T](f: => T): Attempt[T] = Try(f) match {
- case Success(t) => Attempt.successful(t)
- case Failure(t) => Attempt.failure(Err(s"deserialization error: ${t.getMessage}"))
- }
-
- // this codec can be safely used for values < 2^63 and will fail otherwise
- // (for something smarter see https://github.com/yzernik/bitcoin-scodec/blob/master/src/main/scala/io/github/yzernik/bitcoinscodec/structures/UInt64.scala)
- val uint64: Codec[Long] = int64.narrow(l => if (l >= 0) Attempt.Successful(l) else Attempt.failure(Err(s"overflow for value $l")), l => l)
-
- val uint64ex: Codec[UInt64] = bytes(8).xmap(b => UInt64(b), a => a.toByteVector.padLeft(8))
-
- def bytes32: Codec[ByteVector32] = limitedSizeBytes(32, bytesStrict(32).xmap(d => ByteVector32(d), d => d.bytes))
-
- def bytes64: Codec[ByteVector64] = limitedSizeBytes(64, bytesStrict(64).xmap(d => ByteVector64(d), d => d.bytes))
-
- def varsizebinarydata: Codec[ByteVector] = variableSizeBytes(uint16, bytes)
-
- def listofsignatures: Codec[List[ByteVector64]] = listOfN(uint16, bytes64)
-
- def ipv4address: Codec[Inet4Address] = bytes(4).xmap(b => InetAddress.getByAddress(b.toArray).asInstanceOf[Inet4Address], a => ByteVector(a.getAddress))
-
- def ipv6address: Codec[Inet6Address] = bytes(16).exmap(b => attemptFromTry(Inet6Address.getByAddress(null, b.toArray, null)), a => attemptFromTry(ByteVector(a.getAddress)))
-
- def base32(size: Int): Codec[String] = bytes(size).xmap(b => new Base32().encodeAsString(b.toArray).toLowerCase, a => ByteVector(new Base32().decode(a.toUpperCase())))
-
- def nodeaddress: Codec[NodeAddress] =
- discriminated[NodeAddress].by(uint8)
- .typecase(1, (ipv4address :: uint16).as[IPv4])
- .typecase(2, (ipv6address :: uint16).as[IPv6])
- .typecase(3, (base32(10) :: uint16).as[Tor2])
- .typecase(4, (base32(35) :: uint16).as[Tor3])
-
- // this one is a bit different from most other codecs: the first 'len' element is *not* the number of items
- // in the list but rather the number of bytes of the encoded list. The rationale is once we've read this
- // number of bytes we can just skip to the next field
- def listofnodeaddresses: Codec[List[NodeAddress]] = variableSizeBytes(uint16, list(nodeaddress))
-
- def shortchannelid: Codec[ShortChannelId] = int64.xmap(l => ShortChannelId(l), s => s.toLong)
-
- def privateKey: Codec[PrivateKey] = Codec[PrivateKey](
- (priv: PrivateKey) => bytes(32).encode(priv.value),
- (wire: BitVector) => bytes(32).decode(wire).map(_.map(b => PrivateKey(b)))
- )
-
- def publicKey: Codec[PublicKey] = Codec[PublicKey](
- (pub: PublicKey) => bytes(33).encode(pub.value),
- (wire: BitVector) => bytes(33).decode(wire).map(_.map(b => PublicKey(b)))
- )
-
- def rgb: Codec[Color] = bytes(3).xmap(buf => Color(buf(0), buf(1), buf(2)), t => ByteVector(t.r, t.g, t.b))
-
- def zeropaddedstring(size: Int): Codec[String] = fixedSizeBytes(32, utf8).xmap(s => s.takeWhile(_ != '\u0000'), s => s)
-
val initCodec: Codec[Init] = (
("globalFeatures" | varsizebinarydata) ::
("localFeatures" | varsizebinarydata)).as[Init]
@@ -107,20 +45,20 @@ object LightningMessageCodecs {
val channelReestablishCodec: Codec[ChannelReestablish] = (
("channelId" | bytes32) ::
- ("nextLocalCommitmentNumber" | uint64) ::
- ("nextRemoteRevocationNumber" | uint64) ::
+ ("nextLocalCommitmentNumber" | uint64overflow) ::
+ ("nextRemoteRevocationNumber" | uint64overflow) ::
("yourLastPerCommitmentSecret" | optional(bitsRemaining, privateKey)) ::
("myCurrentPerCommitmentPoint" | optional(bitsRemaining, publicKey))).as[ChannelReestablish]
val openChannelCodec: Codec[OpenChannel] = (
("chainHash" | bytes32) ::
("temporaryChannelId" | bytes32) ::
- ("fundingSatoshis" | uint64) ::
- ("pushMsat" | uint64) ::
- ("dustLimitSatoshis" | uint64) ::
- ("maxHtlcValueInFlightMsat" | uint64ex) ::
- ("channelReserveSatoshis" | uint64) ::
- ("htlcMinimumMsat" | uint64) ::
+ ("fundingSatoshis" | uint64overflow) ::
+ ("pushMsat" | uint64overflow) ::
+ ("dustLimitSatoshis" | uint64overflow) ::
+ ("maxHtlcValueInFlightMsat" | uint64) ::
+ ("channelReserveSatoshis" | uint64overflow) ::
+ ("htlcMinimumMsat" | uint64overflow) ::
("feeratePerKw" | uint32) ::
("toSelfDelay" | uint16) ::
("maxAcceptedHtlcs" | uint16) ::
@@ -134,10 +72,10 @@ object LightningMessageCodecs {
val acceptChannelCodec: Codec[AcceptChannel] = (
("temporaryChannelId" | bytes32) ::
- ("dustLimitSatoshis" | uint64) ::
- ("maxHtlcValueInFlightMsat" | uint64ex) ::
- ("channelReserveSatoshis" | uint64) ::
- ("htlcMinimumMsat" | uint64) ::
+ ("dustLimitSatoshis" | uint64overflow) ::
+ ("maxHtlcValueInFlightMsat" | uint64) ::
+ ("channelReserveSatoshis" | uint64overflow) ::
+ ("htlcMinimumMsat" | uint64overflow) ::
("minimumDepth" | uint32) ::
("toSelfDelay" | uint16) ::
("maxAcceptedHtlcs" | uint16) ::
@@ -168,30 +106,30 @@ object LightningMessageCodecs {
val closingSignedCodec: Codec[ClosingSigned] = (
("channelId" | bytes32) ::
- ("feeSatoshis" | uint64) ::
+ ("feeSatoshis" | uint64overflow) ::
("signature" | bytes64)).as[ClosingSigned]
val updateAddHtlcCodec: Codec[UpdateAddHtlc] = (
("channelId" | bytes32) ::
- ("id" | uint64) ::
- ("amountMsat" | uint64) ::
+ ("id" | uint64overflow) ::
+ ("amountMsat" | uint64overflow) ::
("paymentHash" | bytes32) ::
("expiry" | uint32) ::
("onionRoutingPacket" | bytes(Sphinx.PacketLength))).as[UpdateAddHtlc]
val updateFulfillHtlcCodec: Codec[UpdateFulfillHtlc] = (
("channelId" | bytes32) ::
- ("id" | uint64) ::
+ ("id" | uint64overflow) ::
("paymentPreimage" | bytes32)).as[UpdateFulfillHtlc]
val updateFailHtlcCodec: Codec[UpdateFailHtlc] = (
("channelId" | bytes32) ::
- ("id" | uint64) ::
+ ("id" | uint64overflow) ::
("reason" | varsizebinarydata)).as[UpdateFailHtlc]
val updateFailMalformedHtlcCodec: Codec[UpdateFailMalformedHtlc] = (
("channelId" | bytes32) ::
- ("id" | uint64) ::
+ ("id" | uint64overflow) ::
("onionHash" | bytes32) ::
("failureCode" | uint16)).as[UpdateFailMalformedHtlc]
@@ -216,14 +154,13 @@ object LightningMessageCodecs {
("nodeSignature" | bytes64) ::
("bitcoinSignature" | bytes64)).as[AnnouncementSignatures]
- val channelAnnouncementWitnessCodec = (
- ("features" | varsizebinarydata) ::
- ("chainHash" | bytes32) ::
- ("shortChannelId" | shortchannelid) ::
- ("nodeId1" | publicKey) ::
- ("nodeId2" | publicKey) ::
- ("bitcoinKey1" | publicKey) ::
- ("bitcoinKey2" | publicKey))
+ val channelAnnouncementWitnessCodec = ("features" | varsizebinarydata) ::
+ ("chainHash" | bytes32) ::
+ ("shortChannelId" | shortchannelid) ::
+ ("nodeId1" | publicKey) ::
+ ("nodeId2" | publicKey) ::
+ ("bitcoinKey1" | publicKey) ::
+ ("bitcoinKey2" | publicKey)
val channelAnnouncementCodec: Codec[ChannelAnnouncement] = (
("nodeSignature1" | bytes64) ::
@@ -232,13 +169,12 @@ object LightningMessageCodecs {
("bitcoinSignature2" | bytes64) ::
channelAnnouncementWitnessCodec).as[ChannelAnnouncement]
- val nodeAnnouncementWitnessCodec = (
- ("features" | varsizebinarydata) ::
- ("timestamp" | uint32) ::
- ("nodeId" | publicKey) ::
- ("rgbColor" | rgb) ::
- ("alias" | zeropaddedstring(32)) ::
- ("addresses" | listofnodeaddresses))
+ val nodeAnnouncementWitnessCodec = ("features" | varsizebinarydata) ::
+ ("timestamp" | uint32) ::
+ ("nodeId" | publicKey) ::
+ ("rgbColor" | rgb) ::
+ ("alias" | zeropaddedstring(32)) ::
+ ("addresses" | listofnodeaddresses)
val nodeAnnouncementCodec: Codec[NodeAnnouncement] = (
("signature" | bytes64) ::
@@ -251,10 +187,10 @@ object LightningMessageCodecs {
(("messageFlags" | byte) >>:~ { messageFlags =>
("channelFlags" | byte) ::
("cltvExpiryDelta" | uint16) ::
- ("htlcMinimumMsat" | uint64) ::
+ ("htlcMinimumMsat" | uint64overflow) ::
("feeBaseMsat" | uint32) ::
("feeProportionalMillionths" | uint32) ::
- ("htlcMaximumMsat" | conditional((messageFlags & 1) != 0, uint64))
+ ("htlcMaximumMsat" | conditional((messageFlags & 1) != 0, uint64overflow))
})
val channelUpdateCodec: Codec[ChannelUpdate] = (
@@ -285,31 +221,6 @@ object LightningMessageCodecs {
("data" | varsizebinarydata)
).as[ReplyChannelRange]
- val queryShortChannelIdsExCodec: Codec[QueryShortChannelIdsEx] = (
- ("chainHash" | bytes32) ::
- ("flag" | byte) ::
- ("data" | varsizebinarydata)
- ).as[QueryShortChannelIdsEx]
-
- val replyShortChanelIdsEndExCodec: Codec[ReplyShortChannelIdsEndEx] = (
- ("chainHash" | bytes32) ::
- ("complete" | byte)
- ).as[ReplyShortChannelIdsEndEx]
-
- val queryChannelRangeExCodec: Codec[QueryChannelRangeEx] = (
- ("chainHash" | bytes32) ::
- ("firstBlockNum" | uint32) ::
- ("numberOfBlocks" | uint32)
- ).as[QueryChannelRangeEx]
-
- val replyChannelRangeExCodec: Codec[ReplyChannelRangeEx] = (
- ("chainHash" | bytes32) ::
- ("firstBlockNum" | uint32) ::
- ("numberOfBlocks" | uint32) ::
- ("complete" | byte) ::
- ("data" | varsizebinarydata)
- ).as[ReplyChannelRangeEx]
-
val gossipTimestampFilterCodec: Codec[GossipTimestampFilter] = (
("chainHash" | bytes32) ::
("firstTimestamp" | uint32) ::
@@ -345,40 +256,11 @@ object LightningMessageCodecs {
.typecase(263, queryChannelRangeCodec)
.typecase(264, replyChannelRangeCodec)
.typecase(265, gossipTimestampFilterCodec)
- .typecase(1001, queryShortChannelIdsExCodec)
- .typecase(1002, replyShortChanelIdsEndExCodec)
- .typecase(1003, queryChannelRangeExCodec)
- .typecase(1004, replyChannelRangeExCodec)
-
-
- /**
- * A codec that caches serialized routing messages
- */
- val cachedLightningMessageCodec = new Codec[LightningMessage] {
-
- override def sizeBound: SizeBound = lightningMessageCodec.sizeBound
-
- val cache = CacheBuilder
- .newBuilder
- .weakKeys() // will cleanup values when keys are garbage collected
- .build(new CacheLoader[LightningMessage, Attempt[BitVector]] {
- override def load(key: LightningMessage): Attempt[BitVector] = lightningMessageCodec.encode(key)
- })
-
- override def encode(value: LightningMessage): Attempt[BitVector] = value match {
- case _: ChannelAnnouncement => cache.get(value) // we only cache serialized routing messages
- case _: NodeAnnouncement => cache.get(value) // we only cache serialized routing messages
- case _: ChannelUpdate => cache.get(value) // we only cache serialized routing messages
- case _ => lightningMessageCodec.encode(value)
- }
-
- override def decode(bits: BitVector): Attempt[DecodeResult[LightningMessage]] = lightningMessageCodec.decode(bits)
- }
val perHopPayloadCodec: Codec[PerHopPayload] = (
("realm" | constant(ByteVector.fromByte(0))) ::
("short_channel_id" | shortchannelid) ::
- ("amt_to_forward" | uint64) ::
+ ("amt_to_forward" | uint64overflow) ::
("outgoing_cltv_value" | uint32) ::
("unused_with_v0_version_on_header" | ignore(8 * 12))).as[PerHopPayload]
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/LightningMessageTypes.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/LightningMessageTypes.scala
index 0844283f4..24cba57df 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/LightningMessageTypes.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/LightningMessageTypes.scala
@@ -181,10 +181,6 @@ object NodeAddress {
*
* We don't attempt to resolve onion addresses (it will be done by the tor proxy), so we just recognize them based on
* the .onion TLD and rely on their length to separate v2/v3.
- *
- * @param host
- * @param port
- * @return
*/
def fromParts(host: String, port: Int): Try[NodeAddress] = Try {
host match {
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/TlvCodecs.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/TlvCodecs.scala
new file mode 100644
index 000000000..986f7c1d3
--- /dev/null
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/TlvCodecs.scala
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2019 ACINQ SAS
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package fr.acinq.eclair.wire
+
+import fr.acinq.eclair.wire.CommonCodecs._
+import scodec.{Attempt, Codec}
+import scodec.codecs._
+
+import scala.util.Try
+
+/**
+ * Created by t-bast on 20/06/2019.
+ */
+
+object TlvCodecs {
+
+ val genericTlv: Codec[GenericTlv] = (("type" | varint) :: variableSizeBytesLong(varintoverflow, bytes)).as[GenericTlv]
+
+ def tlvFallback(codec: Codec[Tlv]): Codec[Tlv] = discriminatorFallback(genericTlv, codec).xmap({
+ case Left(l) => l
+ case Right(r) => r
+ }, {
+ case g: GenericTlv => Left(g)
+ case o => Right(o)
+ })
+
+ /**
+ * A tlv stream codec relies on an underlying tlv codec.
+ * This allows tlv streams to have different namespaces, increasing the total number of tlv types available.
+ *
+ * @param codec codec used for the tlv records contained in the stream.
+ */
+ def tlvStream(codec: Codec[Tlv]): Codec[TlvStream] = list(codec).exmap(
+ records => Attempt.fromTry(Try(TlvStream(records))),
+ stream => Attempt.successful(stream.records.toList)
+ )
+
+}
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/TlvTypes.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/TlvTypes.scala
new file mode 100644
index 000000000..84d38a090
--- /dev/null
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/TlvTypes.scala
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2019 ACINQ SAS
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package fr.acinq.eclair.wire
+
+import fr.acinq.eclair.UInt64
+import scodec.bits.ByteVector
+
+import scala.annotation.tailrec
+
+/**
+ * Created by t-bast on 20/06/2019.
+ */
+
+// @formatter:off
+trait Tlv {
+ val `type`: UInt64
+}
+sealed trait OnionTlv extends Tlv
+// @formatter:on
+
+/**
+ * Generic tlv type we fallback to if we don't understand the incoming type.
+ *
+ * @param `type` tlv type.
+ * @param value tlv value (length is implicit, and encoded as a varint).
+ */
+case class GenericTlv(`type`: UInt64, value: ByteVector) extends Tlv
+
+/**
+ * A tlv stream is a collection of tlv records.
+ * A tlv stream is part of a given namespace that dictates how to parse the tlv records.
+ * That namespace is indicated by a trait extending the top-level tlv trait.
+ *
+ * @param records tlv records.
+ */
+case class TlvStream(records: Seq[Tlv]) {
+
+ records.foldLeft(Option.empty[Tlv]) {
+ case (None, record) =>
+ require(!record.isInstanceOf[GenericTlv] || record.`type`.toBigInt % 2 != 0, "tlv streams must not contain unknown even tlv types")
+ Some(record)
+ case (Some(previousRecord), record) =>
+ require(record.`type` != previousRecord.`type`, "tlv streams must not contain duplicate records")
+ require(record.`type` > previousRecord.`type`, "tlv records must be ordered by monotonically-increasing types")
+ require(!record.isInstanceOf[GenericTlv] || record.`type`.toBigInt % 2 != 0, "tlv streams must not contain unknown even tlv types")
+ Some(record)
+ }
+
+}
\ No newline at end of file
diff --git a/eclair-core/src/test/resources/api/usablebalances b/eclair-core/src/test/resources/api/usablebalances
new file mode 100644
index 000000000..edbd4e5a9
--- /dev/null
+++ b/eclair-core/src/test/resources/api/usablebalances
@@ -0,0 +1 @@
+[{"remoteNodeId":"03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0","shortChannelId":"0x0x1","canSendMsat":100000000,"canReceiveMsat":20000000,"isPublic":true},{"remoteNodeId":"03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0","shortChannelId":"0x0x2","canSendMsat":400000000,"canReceiveMsat":30000000,"isPublic":false}]
\ No newline at end of file
diff --git a/eclair-core/src/test/resources/integration/bitcoin.conf b/eclair-core/src/test/resources/integration/bitcoin.conf
index 29775744a..8676e0b42 100644
--- a/eclair-core/src/test/resources/integration/bitcoin.conf
+++ b/eclair-core/src/test/resources/integration/bitcoin.conf
@@ -1,7 +1,6 @@
regtest=1
noprinttoconsole=1
server=1
-port=28333
rpcuser=foo
rpcpassword=bar
txindex=1
@@ -10,4 +9,6 @@ zmqpubrawtx=tcp://127.0.0.1:28335
rpcworkqueue=64
addresstype=bech32
[regtest]
+bind=127.0.0.1
+port=28333
rpcport=28332
diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/TestUtils.scala b/eclair-core/src/test/scala/fr/acinq/eclair/TestUtils.scala
index e42ec10ce..4f5ecdcb6 100644
--- a/eclair-core/src/test/scala/fr/acinq/eclair/TestUtils.scala
+++ b/eclair-core/src/test/scala/fr/acinq/eclair/TestUtils.scala
@@ -27,5 +27,4 @@ object TestUtils {
.props
.get("buildDirectory") // this is defined if we run from maven
.getOrElse(new File(sys.props("user.dir"), "target").getAbsolutePath) // otherwise we probably are in intellij, so we build it manually assuming that user.dir == path to the module
-
}
diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/ElectrumClientSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/ElectrumClientSpec.scala
index d2d4606ae..ebe8e9090 100644
--- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/ElectrumClientSpec.scala
+++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/ElectrumClientSpec.scala
@@ -28,7 +28,6 @@ import scodec.bits._
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration._
-
class ElectrumClientSpec extends TestKit(ActorSystem("test")) with FunSuiteLike with Logging with BeforeAndAfterAll {
import ElectrumClient._
@@ -38,6 +37,22 @@ class ElectrumClientSpec extends TestKit(ActorSystem("test")) with FunSuiteLike
// this is tx #2690 of block #500000
val referenceTx = Transaction.read("0200000001983c5b32ced1de5ae97d3ce9b7436f8bb0487d15bf81e5cae97b1e238dc395c6000000006a47304402205957c75766e391350eba2c7b752f0056cb34b353648ecd0992a8a81fc9bcfe980220629c286592842d152cdde71177cd83086619744a533f262473298cacf60193500121021b8b51f74dbf0ac1e766d162c8707b5e8d89fc59da0796f3b4505e7c0fb4cf31feffffff0276bd0101000000001976a914219de672ba773aa0bc2e15cdd9d2e69b734138fa88ac3e692001000000001976a914301706dede031e9fb4b60836e073a4761855f6b188ac09a10700")
val scriptHash = Crypto.sha256(referenceTx.txOut(0).publicKeyScript).reverse
+ val height = 500000
+ val position = 2690
+ val merkleProof = List(
+ hex"b500cd85cd6c7e0e570b82728dd516646536a477b61cc82056505d84a5820dc3",
+ hex"c98798c2e576566a92b23d2405f59d95c506966a6e26fecfb356d6447a199546",
+ hex"930d95c428546812fd11f8242904a9a1ba05d2140cd3a83be0e2ed794821c9ec",
+ hex"90c97965b12f4262fe9bf95bc37ff7d6362902745eaa822ecf0cf85801fa8b48",
+ hex"23792d51fddd6e439ed4c92ad9f19a9b73fc9d5c52bdd69039be70ad6619a1aa",
+ hex"4b73075f29a0abdcec2c83c2cfafc5f304d2c19dcacb50a88a023df725468760",
+ hex"f80225a32a5ce4ef0703822c6aa29692431a816dec77d9b1baa5b09c3ba29bfb",
+ hex"4858ac33f2022383d3b4dd674666a0880557d02a155073be93231a02ecbb81f4",
+ hex"eb5b142030ed4e0b55a8ba5a7b5b783a0a24e0c2fd67c1cfa2f7b308db00c38a",
+ hex"86858812c3837d209110f7ea79de485abdfd22039467a8aa15a8d85856ee7d30",
+ hex"de20eb85f2e9ad525a6fb5c618682b6bdce2fa83df836a698f31575c4e5b3d38",
+ hex"98bd1048e04ff1b0af5856d9890cd708d8d67ad6f3a01f777130fbc16810eeb3")
+ .map(ByteVector32(_))
override protected def beforeAll(): Unit = {
client = system.actorOf(Props(new ElectrumClient(new InetSocketAddress("electrum.acinq.co", 50002), SSL.STRICT)), "electrum-client")
@@ -52,6 +67,16 @@ class ElectrumClientSpec extends TestKit(ActorSystem("test")) with FunSuiteLike
probe.expectMsgType[ElectrumReady](15 seconds)
}
+ test("get transaction id from position") {
+ probe.send(client, GetTransactionIdFromPosition(height, position))
+ probe.expectMsg(GetTransactionIdFromPositionResponse(referenceTx.txid, height, position, Nil))
+ }
+
+ test("get transaction id from position with merkle proof") {
+ probe.send(client, GetTransactionIdFromPosition(height, position, merkle = true))
+ probe.expectMsg(GetTransactionIdFromPositionResponse(referenceTx.txid, height, position, merkleProof))
+ }
+
test("get transaction") {
probe.send(client, GetTransaction(referenceTx.txid))
val GetTransactionResponse(tx) = probe.expectMsgType[GetTransactionResponse]
@@ -98,7 +123,7 @@ class ElectrumClientSpec extends TestKit(ActorSystem("test")) with FunSuiteLike
test("get scripthash history") {
probe.send(client, GetScriptHashHistory(scriptHash))
val GetScriptHashHistoryResponse(scriptHash1, history) = probe.expectMsgType[GetScriptHashHistoryResponse]
- assert(history.contains((TransactionHistoryItem(500000, referenceTx.txid))))
+ assert(history.contains(TransactionHistoryItem(500000, referenceTx.txid)))
}
test("list script unspents") {
@@ -106,4 +131,5 @@ class ElectrumClientSpec extends TestKit(ActorSystem("test")) with FunSuiteLike
val ScriptHashListUnspentResponse(scriptHash1, unspents) = probe.expectMsgType[ScriptHashListUnspentResponse]
assert(unspents.isEmpty)
}
+
}
diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/StateTestsHelperMethods.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/StateTestsHelperMethods.scala
index bcab510f4..28e5a3920 100644
--- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/StateTestsHelperMethods.scala
+++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/StateTestsHelperMethods.scala
@@ -103,7 +103,7 @@ trait StateTestsHelperMethods extends TestKitBase {
bob2blockchain.expectMsgType[WatchConfirmed] // deeply buried
awaitCond(alice.stateName == NORMAL)
awaitCond(bob.stateName == NORMAL)
- assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.availableBalanceForSendMsat == pushMsat - TestConstants.Alice.channelParams.channelReserveSatoshis * 1000)
+ assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.availableBalanceForSendMsat == math.max(pushMsat - TestConstants.Alice.channelParams.channelReserveSatoshis * 1000, 0))
// x2 because alice and bob share the same relayer
channelUpdateListener.expectMsgType[LocalChannelUpdate]
channelUpdateListener.expectMsgType[LocalChannelUpdate]
diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/TransportHandlerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/TransportHandlerSpec.scala
index 8a38d6ee4..f549bb1b3 100644
--- a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/TransportHandlerSpec.scala
+++ b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/TransportHandlerSpec.scala
@@ -23,7 +23,7 @@ import akka.io.Tcp
import akka.testkit.{TestActorRef, TestFSMRef, TestKit, TestProbe}
import fr.acinq.eclair.crypto.Noise.{Chacha20Poly1305CipherFunctions, CipherState}
import fr.acinq.eclair.crypto.TransportHandler.{Encryptor, ExtendedCipherState, Listener}
-import fr.acinq.eclair.wire.LightningMessageCodecs
+import fr.acinq.eclair.wire.CommonCodecs
import org.scalatest.{BeforeAndAfterAll, FunSuiteLike}
import scodec.Codec
import scodec.bits._
@@ -49,8 +49,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, LightningMessageCodecs.varsizebinarydata))
- val responder = TestFSMRef(new TransportHandler(Responder.s, None, pipe, LightningMessageCodecs.varsizebinarydata))
+ val initiator = TestFSMRef(new TransportHandler(Initiator.s, Some(Responder.s.pub), pipe, CommonCodecs.varsizebinarydata))
+ val responder = TestFSMRef(new TransportHandler(Responder.s, None, pipe, CommonCodecs.varsizebinarydata))
pipe ! (initiator, responder)
awaitCond(initiator.stateName == TransportHandler.WaitingForListener)
@@ -111,8 +111,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, LightningMessageCodecs.varsizebinarydata))
- val responder = TestFSMRef(new TransportHandler(Responder.s, None, pipe, LightningMessageCodecs.varsizebinarydata))
+ val initiator = TestFSMRef(new TransportHandler(Initiator.s, Some(Responder.s.pub), pipe, CommonCodecs.varsizebinarydata))
+ val responder = TestFSMRef(new TransportHandler(Responder.s, None, pipe, CommonCodecs.varsizebinarydata))
pipe ! (initiator, responder)
awaitCond(initiator.stateName == TransportHandler.WaitingForListener)
diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala
index ab2db732b..43cda573a 100644
--- a/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala
+++ b/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala
@@ -109,7 +109,7 @@ class PeerSpec extends TestkitBaseClass {
probe.expectMsg(s"no address found")
}
- // on Andoird we don't store node announcements
+ // On Android we don't store node announcements
ignore("if no address was specified during connection use the one from node_announcement", Tag("with_node_announcements")) { f =>
import f._
diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/ChannelSelectionSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/ChannelSelectionSpec.scala
index 7a7508ef0..8faa2e864 100644
--- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/ChannelSelectionSpec.scala
+++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/ChannelSelectionSpec.scala
@@ -16,7 +16,7 @@
package fr.acinq.eclair.payment
-import fr.acinq.bitcoin.Block
+import fr.acinq.bitcoin.{Block, ByteVector32}
import fr.acinq.bitcoin.Crypto.PublicKey
import fr.acinq.eclair.channel.{AddHtlcFailed, CMD_ADD_HTLC, CMD_FAIL_HTLC}
import fr.acinq.eclair.crypto.Sphinx
@@ -24,6 +24,7 @@ import fr.acinq.eclair.payment.Relayer.{OutgoingChannel, RelayPayload}
import fr.acinq.eclair.router.Announcements
import fr.acinq.eclair.wire._
import fr.acinq.eclair.{ShortChannelId, randomBytes32, randomKey}
+import fr.acinq.eclair.payment.HtlcGenerationSpec.makeCommitments
import org.scalatest.FunSuite
import scodec.bits.ByteVector
@@ -81,11 +82,11 @@ class ChannelSelectionSpec extends FunSuite {
val channelUpdate = dummyUpdate(ShortChannelId(12345), 10, 100, 1000, 100, 10000000, true)
val channelUpdates = Map(
- ShortChannelId(11111) -> OutgoingChannel(a, channelUpdate, 100000000),
- ShortChannelId(12345) -> OutgoingChannel(a, channelUpdate, 20000000),
- ShortChannelId(22222) -> OutgoingChannel(a, channelUpdate, 10000000),
- ShortChannelId(33333) -> OutgoingChannel(a, channelUpdate, 100000),
- ShortChannelId(44444) -> OutgoingChannel(b, channelUpdate, 1000000)
+ ShortChannelId(11111) -> OutgoingChannel(a, channelUpdate, makeCommitments(ByteVector32.Zeroes, 100000000)),
+ ShortChannelId(12345) -> OutgoingChannel(a, channelUpdate, makeCommitments(ByteVector32.Zeroes, 20000000)),
+ ShortChannelId(22222) -> OutgoingChannel(a, channelUpdate, makeCommitments(ByteVector32.Zeroes, 10000000)),
+ ShortChannelId(33333) -> OutgoingChannel(a, channelUpdate, makeCommitments(ByteVector32.Zeroes, 100000)),
+ ShortChannelId(44444) -> OutgoingChannel(b, channelUpdate, makeCommitments(ByteVector32.Zeroes, 1000000))
)
val node2channels = new mutable.HashMap[PublicKey, mutable.Set[ShortChannelId]] with mutable.MultiMap[PublicKey, ShortChannelId]
diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/HtlcGenerationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/HtlcGenerationSpec.scala
index fb4fb7718..2110bbc83 100644
--- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/HtlcGenerationSpec.scala
+++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/HtlcGenerationSpec.scala
@@ -19,8 +19,8 @@ package fr.acinq.eclair.payment
import java.util.UUID
import fr.acinq.bitcoin.DeterministicWallet.ExtendedPrivateKey
-import fr.acinq.bitcoin.{Block, Crypto, DeterministicWallet}
-import fr.acinq.eclair.channel.Channel
+import fr.acinq.bitcoin.{Block, ByteVector32, Crypto, DeterministicWallet}
+import fr.acinq.eclair.channel.{Channel, Commitments}
import fr.acinq.eclair.crypto.Sphinx
import fr.acinq.eclair.crypto.Sphinx.{PacketAndSecrets, ParsedPacket}
import fr.acinq.eclair.payment.PaymentLifecycle._
@@ -151,6 +151,12 @@ class HtlcGenerationSpec extends FunSuite {
object HtlcGenerationSpec {
+ def makeCommitments(channelId: ByteVector32, availableBalanceForSend: Long = 50000000L, availableBalanceForReceive: Long = 50000000L) =
+ new Commitments(null, null, 0.toByte, null, null, null, null, 0, 0, Map.empty, null, null, null, channelId) {
+ override lazy val availableBalanceForSendMsat: Long = availableBalanceForSend.max(0)
+ override lazy val availableBalanceForReceiveMsat: Long = availableBalanceForReceive.max(0)
+ }
+
def randomExtendedPrivateKey: ExtendedPrivateKey = DeterministicWallet.generate(randomBytes32)
val (priv_a, priv_b, priv_c, priv_d, priv_e) = (TestConstants.Alice.keyManager.nodeKey, TestConstants.Bob.keyManager.nodeKey, randomExtendedPrivateKey, randomExtendedPrivateKey, randomExtendedPrivateKey)
diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentRequestSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentRequestSpec.scala
index 9acd85b61..f67bd0e07 100644
--- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentRequestSpec.scala
+++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentRequestSpec.scala
@@ -262,6 +262,12 @@ class PaymentRequestSpec extends FunSuite {
assert(PaymentRequest.write(PaymentRequest.read(input.toUpperCase())) == input)
}
+ test("Pay 1 BTC without multiplier") {
+ val ref = "lnbc11pdkmqhupp5n2ees808r98m0rh4472yyth0c5fptzcxmexcjznrzmq8xald0cgqdqsf4ujqarfwqsxymmccqp2xvtsv5tc743wgctlza8k3zlpxucl7f3kvjnjptv7xz0nkaww307sdyrvgke2w8kmq7dgz4lkasfn0zvplc9aa4gp8fnhrwfjny0j59sq42x9gp"
+ val pr = PaymentRequest.read(ref)
+ assert(pr.amount.contains(MilliSatoshi(100000000000L)))
+ }
+
test("nonreg") {
val requests = List(
"lnbc40n1pw9qjvwpp5qq3w2ln6krepcslqszkrsfzwy49y0407hvks30ec6pu9s07jur3sdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwdencqzysxqrrss7ju0s4dwx6w8a95a9p2xc5vudl09gjl0w2n02sjrvffde632nxwh2l4w35nqepj4j5njhh4z65wyfc724yj6dn9wajvajfn5j7em6wsq2elakl",
diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/RelayerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/RelayerSpec.scala
index a71efb14e..91de08211 100644
--- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/RelayerSpec.scala
+++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/RelayerSpec.scala
@@ -25,9 +25,8 @@ import fr.acinq.eclair.channel._
import fr.acinq.eclair.crypto.Sphinx
import fr.acinq.eclair.payment.PaymentLifecycle.buildCommand
import fr.acinq.eclair.router.Announcements
-import fr.acinq.eclair.transactions.CommitmentSpec
import fr.acinq.eclair.wire._
-import fr.acinq.eclair.{ShortChannelId, TestConstants, TestkitBaseClass, UInt64, randomBytes32, randomKey}
+import fr.acinq.eclair._
import org.scalatest.Outcome
import scodec.bits.ByteVector
@@ -58,11 +57,6 @@ class RelayerSpec extends TestkitBaseClass {
val channelId_ab = randomBytes32
val channelId_bc = randomBytes32
- def makeCommitments(channelId: ByteVector32, availableBalanceMsat: Long = 50000000L) = new Commitments(null, null, 0.toByte, null, null,
- null, null, 0, 0, Map.empty, null, null, null, channelId) {
- override def availableBalanceForSendMsat: Long = availableBalanceMsat
- }
-
test("relay an htlc-add") { f =>
import f._
val sender = TestProbe()
@@ -97,7 +91,7 @@ class RelayerSpec extends TestkitBaseClass {
// this is another channel B-C, with less balance (it will be preferred)
val (channelId_bc_1, channelUpdate_bc_1) = (randomBytes32, channelUpdate_bc.copy(shortChannelId = ShortChannelId("500000x1x1")))
- relayer ! LocalChannelUpdate(null, channelId_bc_1, channelUpdate_bc_1.shortChannelId, c, None, channelUpdate_bc_1, makeCommitments(channelId_bc_1, availableBalanceMsat = 49000000L))
+ relayer ! LocalChannelUpdate(null, channelId_bc_1, channelUpdate_bc_1.shortChannelId, c, None, channelUpdate_bc_1, makeCommitments(channelId_bc_1, 49000000L))
sender.send(relayer, ForwardAdd(add_ab))
@@ -416,4 +410,37 @@ class RelayerSpec extends TestkitBaseClass {
assert(fwd.channelId === origin.originChannelId)
assert(fwd.message.id === origin.originHtlcId)
}
+
+ test("get usable balances") { f =>
+ import f._
+ val sender = TestProbe()
+ relayer ! LocalChannelUpdate(null, channelId_ab, channelUpdate_ab.shortChannelId, a, None, channelUpdate_ab, makeCommitments(channelId_ab, -2000, 300000))
+ relayer ! LocalChannelUpdate(null, channelId_bc, channelUpdate_bc.shortChannelId, c, None, channelUpdate_bc, makeCommitments(channelId_bc, 400000, -5000))
+ sender.send(relayer, GetUsableBalances)
+ val usableBalances1 = sender.expectMsgType[Iterable[UsableBalances]]
+ assert(usableBalances1.size === 2)
+ assert(usableBalances1.head.canSendMsat === 0 && usableBalances1.head.canReceiveMsat === 300000 && usableBalances1.head.shortChannelId == channelUpdate_ab.shortChannelId)
+ assert(usableBalances1.last.canReceiveMsat === 0 && usableBalances1.last.canSendMsat === 400000 && usableBalances1.last.shortChannelId == channelUpdate_bc.shortChannelId)
+
+ relayer ! AvailableBalanceChanged(null, channelId_bc, channelUpdate_bc.shortChannelId, 0, makeCommitments(channelId_bc, 200000, 500000))
+ sender.send(relayer, GetUsableBalances)
+ val usableBalances2 = sender.expectMsgType[Iterable[UsableBalances]]
+ assert(usableBalances2.last.canReceiveMsat === 500000 && usableBalances2.last.canSendMsat === 200000)
+
+ relayer ! AvailableBalanceChanged(null, channelId_ab, channelUpdate_ab.shortChannelId, 0, makeCommitments(channelId_ab, 100000, 200000))
+ relayer ! LocalChannelDown(null, channelId_bc, channelUpdate_bc.shortChannelId, c)
+ sender.send(relayer, GetUsableBalances)
+ val usableBalances3 = sender.expectMsgType[Iterable[UsableBalances]]
+ assert(usableBalances3.size === 1 && usableBalances3.head.canSendMsat === 100000)
+
+ relayer ! LocalChannelUpdate(null, channelId_ab, channelUpdate_ab.shortChannelId, a, None, channelUpdate_ab.copy(channelFlags = 2), makeCommitments(channelId_ab, 100000, 200000))
+ sender.send(relayer, GetUsableBalances)
+ val usableBalances4 = sender.expectMsgType[Iterable[UsableBalances]]
+ assert(usableBalances4.isEmpty)
+
+ relayer ! LocalChannelUpdate(null, channelId_ab, channelUpdate_ab.shortChannelId, a, None, channelUpdate_ab, makeCommitments(channelId_ab, 100000, 200000))
+ sender.send(relayer, GetUsableBalances)
+ val usableBalances5 = sender.expectMsgType[Iterable[UsableBalances]]
+ assert(usableBalances5.size === 1)
+ }
}
diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/CommonCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/CommonCodecsSpec.scala
new file mode 100644
index 000000000..f17bc4578
--- /dev/null
+++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/CommonCodecsSpec.scala
@@ -0,0 +1,253 @@
+/*
+ * Copyright 2019 ACINQ SAS
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package fr.acinq.eclair.wire
+
+import java.net.{Inet4Address, Inet6Address, InetAddress}
+
+import com.google.common.net.InetAddresses
+import fr.acinq.bitcoin.Crypto.PrivateKey
+import fr.acinq.eclair.{UInt64, randomBytes32}
+import fr.acinq.eclair.wire.CommonCodecs._
+import org.scalatest.FunSuite
+import scodec.bits.{BitVector, HexStringSyntax}
+
+/**
+ * Created by t-bast on 20/06/2019.
+ */
+
+class CommonCodecsSpec extends FunSuite {
+
+ test("encode/decode with uint64 codec") {
+ val expected = Map(
+ UInt64(0) -> hex"00 00 00 00 00 00 00 00",
+ UInt64(42) -> hex"00 00 00 00 00 00 00 2a",
+ UInt64(6211610197754262546L) -> hex"56 34 12 90 78 56 34 12",
+ UInt64(hex"ff ff ff ff ff ff ff ff") -> hex"ff ff ff ff ff ff ff ff"
+ ).mapValues(_.toBitVector)
+
+ for ((uint, ref) <- expected) {
+ val encoded = uint64.encode(uint).require
+ assert(ref === encoded)
+ val decoded = uint64.decode(encoded).require.value
+ assert(uint === decoded)
+ }
+ }
+
+ test("encode/decode with uint64L codec") {
+ val expected = Map(
+ UInt64(0) -> hex"00 00 00 00 00 00 00 00",
+ UInt64(42) -> hex"2a 00 00 00 00 00 00 00",
+ UInt64(6211610197754262546L) -> hex"12 34 56 78 90 12 34 56",
+ UInt64(hex"ff ff ff ff ff ff ff ff") -> hex"ff ff ff ff ff ff ff ff"
+ ).mapValues(_.toBitVector)
+
+ for ((uint, ref) <- expected) {
+ val encoded = uint64L.encode(uint).require
+ assert(ref === encoded)
+ val decoded = uint64L.decode(encoded).require.value
+ assert(uint === decoded)
+ }
+ }
+
+ test("encode/decode with varint codec") {
+ val expected = Map(
+ UInt64(0L) -> hex"00",
+ UInt64(42L) -> hex"2a",
+ UInt64(253L) -> hex"fd fd 00",
+ UInt64(254L) -> hex"fd fe 00",
+ UInt64(255L) -> hex"fd ff 00",
+ UInt64(550L) -> hex"fd 26 02",
+ UInt64(998000L) -> hex"fe 70 3a 0f 00",
+ UInt64(6211610197754262546L) -> hex"ff 12 34 56 78 90 12 34 56",
+ UInt64.MaxValue -> hex"ff ff ff ff ff ff ff ff ff"
+ ).mapValues(_.toBitVector)
+
+ for ((uint, ref) <- expected) {
+ val encoded = varint.encode(uint).require
+ assert(ref === encoded, ref)
+ val decoded = varint.decode(encoded).require.value
+ assert(uint === decoded, uint)
+ }
+ }
+
+ test("decode invalid varint") {
+ val testCases = Seq(
+ hex"fd", // truncated
+ hex"fe 01", // truncated
+ hex"fe", // truncated
+ hex"fe 12 34", // truncated
+ hex"ff", // truncated
+ hex"ff 12 34 56 78", // truncated
+ hex"fd 00 00", // not minimally-encoded
+ hex"fd fc 00", // not minimally-encoded
+ hex"fe 00 00 00 00", // not minimally-encoded
+ hex"fe ff ff 00 00", // not minimally-encoded
+ hex"ff 00 00 00 00 00 00 00 00", // not minimally-encoded
+ hex"ff ff ff ff 01 00 00 00 00", // not minimally-encoded
+ hex"ff ff ff ff ff 00 00 00 00" // not minimally-encoded
+ ).map(_.toBitVector)
+
+ for (testCase <- testCases) {
+ assert(varint.decode(testCase).isFailure, testCase.toByteVector)
+ }
+ }
+
+ test("encode/decode with varlong codec") {
+ val expected = Map(
+ 0L -> hex"00",
+ 42L -> hex"2a",
+ 253L -> hex"fd fd 00",
+ 254L -> hex"fd fe 00",
+ 255L -> hex"fd ff 00",
+ 550L -> hex"fd 26 02",
+ 998000L -> hex"fe 70 3a 0f 00",
+ 6211610197754262546L -> hex"ff 12 34 56 78 90 12 34 56",
+ Long.MaxValue -> hex"ff ff ff ff ff ff ff ff 7f"
+ ).mapValues(_.toBitVector)
+
+ for ((long, ref) <- expected) {
+ val encoded = varintoverflow.encode(long).require
+ assert(ref === encoded, ref)
+ val decoded = varintoverflow.decode(encoded).require.value
+ assert(long === decoded, long)
+ }
+ }
+
+ test("decode invalid varlong") {
+ val testCases = Seq(
+ hex"ff 00 00 00 00 00 00 00 80",
+ hex"ff ff ff ff ff ff ff ff ff"
+ ).map(_.toBitVector)
+
+ for (testCase <- testCases) {
+ assert(varintoverflow.decode(testCase).isFailure, testCase.toByteVector)
+ }
+ }
+
+ test("encode/decode with rgb codec") {
+ val color = Color(47.toByte, 255.toByte, 142.toByte)
+ val bin = rgb.encode(color).require
+ assert(bin === hex"2f ff 8e".toBitVector)
+ val color2 = rgb.decode(bin).require.value
+ assert(color === color2)
+ }
+
+ test("encode/decode all kind of IPv6 addresses with ipv6address codec") {
+ {
+ // IPv4 mapped
+ val bin = hex"00000000000000000000ffffae8a0b08".toBitVector
+ val ipv6 = Inet6Address.getByAddress(null, bin.toByteArray, null)
+ val bin2 = ipv6address.encode(ipv6).require
+ assert(bin === bin2)
+ }
+
+ {
+ // regular IPv6 address
+ val ipv6 = InetAddresses.forString("1080:0:0:0:8:800:200C:417A").asInstanceOf[Inet6Address]
+ val bin = ipv6address.encode(ipv6).require
+ val ipv62 = ipv6address.decode(bin).require.value
+ assert(ipv6 === ipv62)
+ }
+ }
+
+ test("encode/decode with nodeaddress codec") {
+ {
+ val ipv4addr = InetAddress.getByAddress(Array[Byte](192.toByte, 168.toByte, 1.toByte, 42.toByte)).asInstanceOf[Inet4Address]
+ val nodeaddr = IPv4(ipv4addr, 4231)
+ val bin = nodeaddress.encode(nodeaddr).require
+ assert(bin === hex"01 C0 A8 01 2A 10 87".toBitVector)
+ val nodeaddr2 = nodeaddress.decode(bin).require.value
+ assert(nodeaddr === nodeaddr2)
+ }
+ {
+ val ipv6addr = InetAddress.getByAddress(hex"2001 0db8 0000 85a3 0000 0000 ac1f 8001".toArray).asInstanceOf[Inet6Address]
+ val nodeaddr = IPv6(ipv6addr, 4231)
+ val bin = nodeaddress.encode(nodeaddr).require
+ assert(bin === hex"02 2001 0db8 0000 85a3 0000 0000 ac1f 8001 1087".toBitVector)
+ val nodeaddr2 = nodeaddress.decode(bin).require.value
+ assert(nodeaddr === nodeaddr2)
+ }
+ {
+ val nodeaddr = Tor2("z4zif3fy7fe7bpg3", 4231)
+ val bin = nodeaddress.encode(nodeaddr).require
+ assert(bin === hex"03 cf3282ecb8f949f0bcdb 1087".toBitVector)
+ val nodeaddr2 = nodeaddress.decode(bin).require.value
+ assert(nodeaddr === nodeaddr2)
+ }
+ {
+ val nodeaddr = Tor3("mrl2d3ilhctt2vw4qzvmz3etzjvpnc6dczliq5chrxetthgbuczuggyd", 4231)
+ val bin = nodeaddress.encode(nodeaddr).require
+ assert(bin === hex"04 6457a1ed0b38a73d56dc866accec93ca6af68bc316568874478dc9399cc1a0b3431b03 1087".toBitVector)
+ val nodeaddr2 = nodeaddress.decode(bin).require.value
+ assert(nodeaddr === nodeaddr2)
+ }
+ }
+
+ test("encode/decode with private key codec") {
+ val value = PrivateKey(randomBytes32)
+ val wire = privateKey.encode(value).require
+ assert(wire.length == 256)
+ val value1 = privateKey.decode(wire).require.value
+ assert(value1 == value)
+ }
+
+ test("encode/decode with public key codec") {
+ val value = PrivateKey(randomBytes32).publicKey
+ val wire = CommonCodecs.publicKey.encode(value).require
+ assert(wire.length == 33 * 8)
+ val value1 = CommonCodecs.publicKey.decode(wire).require.value
+ assert(value1 == value)
+ }
+
+ test("encode/decode with zeropaddedstring codec") {
+ val c = zeropaddedstring(32)
+
+ {
+ val alias = "IRATEMONK"
+ val bin = c.encode(alias).require
+ assert(bin === BitVector(alias.getBytes("UTF-8") ++ Array.fill[Byte](32 - alias.length)(0)))
+ val alias2 = c.decode(bin).require.value
+ assert(alias === alias2)
+ }
+
+ {
+ val alias = "this-alias-is-exactly-32-B-long."
+ val bin = c.encode(alias).require
+ assert(bin === BitVector(alias.getBytes("UTF-8") ++ Array.fill[Byte](32 - alias.length)(0)))
+ val alias2 = c.decode(bin).require.value
+ assert(alias === alias2)
+ }
+
+ {
+ val alias = "this-alias-is-far-too-long-because-we-are-limited-to-32-bytes"
+ assert(c.encode(alias).isFailure)
+ }
+ }
+
+ test("encode/decode UInt64") {
+ val codec = uint64
+ Seq(
+ UInt64(hex"ffffffffffffffff"),
+ UInt64(hex"fffffffffffffffe"),
+ UInt64(hex"efffffffffffffff"),
+ UInt64(hex"effffffffffffffe")
+ ).map(value => {
+ assert(codec.decode(codec.encode(value).require).require.value === value)
+ })
+ }
+
+}
diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/LightningMessageCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/LightningMessageCodecsSpec.scala
index 057ff1fe4..700b55297 100644
--- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/LightningMessageCodecsSpec.scala
+++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/LightningMessageCodecsSpec.scala
@@ -16,9 +16,8 @@
package fr.acinq.eclair.wire
-import java.net.{Inet4Address, Inet6Address, InetAddress}
+import java.net.{Inet4Address, InetAddress}
-import com.google.common.net.InetAddresses
import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey}
import fr.acinq.bitcoin.{Block, ByteVector32, ByteVector64}
import fr.acinq.eclair._
@@ -44,132 +43,6 @@ class LightningMessageCodecsSpec extends FunSuite {
def publicKey(fill: Byte) = PrivateKey(ByteVector.fill(32)(fill)).publicKey
- test("encode/decode with uint64 codec") {
- val expected = Map(
- UInt64(0) -> hex"00 00 00 00 00 00 00 00",
- UInt64(42) -> hex"00 00 00 00 00 00 00 2a",
- UInt64(hex"ffffffffffffffff") -> hex"ff ff ff ff ff ff ff ff"
- ).mapValues(_.toBitVector)
- for ((uint, ref) <- expected) {
- val encoded = uint64ex.encode(uint).require
- assert(ref === encoded)
- val decoded = uint64ex.decode(encoded).require.value
- assert(uint === decoded)
- }
- }
-
- test("encode/decode with rgb codec") {
- val color = Color(47.toByte, 255.toByte, 142.toByte)
- val bin = rgb.encode(color).require
- assert(bin === hex"2f ff 8e".toBitVector)
- val color2 = rgb.decode(bin).require.value
- assert(color === color2)
- }
-
- test("encode/decode all kind of IPv6 addresses with ipv6address codec") {
- {
- // IPv4 mapped
- val bin = hex"00000000000000000000ffffae8a0b08".toBitVector
- val ipv6 = Inet6Address.getByAddress(null, bin.toByteArray, null)
- val bin2 = ipv6address.encode(ipv6).require
- assert(bin === bin2)
- }
-
- {
- // regular IPv6 address
- val ipv6 = InetAddresses.forString("1080:0:0:0:8:800:200C:417A").asInstanceOf[Inet6Address]
- val bin = ipv6address.encode(ipv6).require
- val ipv62 = ipv6address.decode(bin).require.value
- assert(ipv6 === ipv62)
- }
- }
-
- test("encode/decode with nodeaddress codec") {
- {
- val ipv4addr = InetAddress.getByAddress(Array[Byte](192.toByte, 168.toByte, 1.toByte, 42.toByte)).asInstanceOf[Inet4Address]
- val nodeaddr = IPv4(ipv4addr, 4231)
- val bin = nodeaddress.encode(nodeaddr).require
- assert(bin === hex"01 C0 A8 01 2A 10 87".toBitVector)
- val nodeaddr2 = nodeaddress.decode(bin).require.value
- assert(nodeaddr === nodeaddr2)
- }
- {
- val ipv6addr = InetAddress.getByAddress(hex"2001 0db8 0000 85a3 0000 0000 ac1f 8001".toArray).asInstanceOf[Inet6Address]
- val nodeaddr = IPv6(ipv6addr, 4231)
- val bin = nodeaddress.encode(nodeaddr).require
- assert(bin === hex"02 2001 0db8 0000 85a3 0000 0000 ac1f 8001 1087".toBitVector)
- val nodeaddr2 = nodeaddress.decode(bin).require.value
- assert(nodeaddr === nodeaddr2)
- }
- {
- val nodeaddr = Tor2("z4zif3fy7fe7bpg3", 4231)
- val bin = nodeaddress.encode(nodeaddr).require
- assert(bin === hex"03 cf3282ecb8f949f0bcdb 1087".toBitVector)
- val nodeaddr2 = nodeaddress.decode(bin).require.value
- assert(nodeaddr === nodeaddr2)
- }
- {
- val nodeaddr = Tor3("mrl2d3ilhctt2vw4qzvmz3etzjvpnc6dczliq5chrxetthgbuczuggyd", 4231)
- val bin = nodeaddress.encode(nodeaddr).require
- assert(bin === hex"04 6457a1ed0b38a73d56dc866accec93ca6af68bc316568874478dc9399cc1a0b3431b03 1087".toBitVector)
- val nodeaddr2 = nodeaddress.decode(bin).require.value
- assert(nodeaddr === nodeaddr2)
- }
- }
-
- test("encode/decode with private key codec") {
- val value = PrivateKey(randomBytes32)
- val wire = LightningMessageCodecs.privateKey.encode(value).require
- assert(wire.length == 256)
- val value1 = LightningMessageCodecs.privateKey.decode(wire).require.value
- assert(value1 == value)
- }
-
- test("encode/decode with public key codec") {
- val value = PrivateKey(randomBytes32).publicKey
- val wire = LightningMessageCodecs.publicKey.encode(value).require
- assert(wire.length == 33 * 8)
- val value1 = LightningMessageCodecs.publicKey.decode(wire).require.value
- assert(value1 == value)
- }
-
- test("encode/decode with zeropaddedstring codec") {
- val c = zeropaddedstring(32)
-
- {
- val alias = "IRATEMONK"
- val bin = c.encode(alias).require
- assert(bin === BitVector(alias.getBytes("UTF-8") ++ Array.fill[Byte](32 - alias.size)(0)))
- val alias2 = c.decode(bin).require.value
- assert(alias === alias2)
- }
-
- {
- val alias = "this-alias-is-exactly-32-B-long."
- val bin = c.encode(alias).require
- assert(bin === BitVector(alias.getBytes("UTF-8") ++ Array.fill[Byte](32 - alias.size)(0)))
- val alias2 = c.decode(bin).require.value
- assert(alias === alias2)
- }
-
- {
- val alias = "this-alias-is-far-too-long-because-we-are-limited-to-32-bytes"
- assert(c.encode(alias).isFailure)
- }
- }
-
- test("encode/decode UInt64") {
- val codec = uint64ex
- Seq(
- UInt64(hex"ffffffffffffffff"),
- UInt64(hex"fffffffffffffffe"),
- UInt64(hex"efffffffffffffff"),
- UInt64(hex"effffffffffffffe")
- ).map(value => {
- assert(codec.decode(codec.encode(value).require).require.value === value)
- })
- }
-
test("encode/decode live node_announcements") {
val anns = List(
hex"a58338c9660d135fd7d087eb62afd24a33562c54507a9334e79f0dc4f17d407e6d7c61f0e2f3d0d38599502f61704cf1ae93608df027014ade7ff592f27ce26900005acdf50702d2eabbbacc7c25bbd73b39e65d28237705f7bde76f557e94fb41cb18a9ec00841122116c6e302e646563656e7465722e776f726c64000000000000000000000000000000130200000000000000000000ffffae8a0b082607"
@@ -189,7 +62,6 @@ class LightningMessageCodecsSpec extends FunSuite {
}
test("encode/decode all channel messages") {
-
val open = OpenChannel(randomBytes32, randomBytes32, 3, 4, 5, UInt64(6), 7, 8, 9, 10, 11, publicKey(1), point(2), point(3), point(4), point(5), point(6), 0.toByte)
val accept = AcceptChannel(randomBytes32, 3, UInt64(4), 5, 6, 7, 8, 9, publicKey(1), point(2), point(3), point(4), point(5), point(6))
val funding_created = FundingCreated(randomBytes32, bin32(0), 3, randomBytes64)
@@ -222,7 +94,7 @@ class LightningMessageCodecsSpec extends FunSuite {
channel_announcement :: node_announcement :: channel_update :: gossip_timestamp_filter :: query_short_channel_id :: query_channel_range :: reply_channel_range :: announcement_signatures :: ping :: pong :: channel_reestablish :: Nil
msgs.foreach {
- case msg => {
+ msg => {
val encoded = lightningMessageCodec.encode(msg).require
val decoded = lightningMessageCodec.decode(encoded).require
assert(msg === decoded.value)
@@ -245,38 +117,6 @@ class LightningMessageCodecsSpec extends FunSuite {
}
}
- test("encode/decode using cached codec") {
- val codec = cachedLightningMessageCodec
-
- val commit_sig = CommitSig(randomBytes32, randomBytes64, randomBytes64 :: randomBytes64 :: randomBytes64 :: Nil)
- val revoke_and_ack = RevokeAndAck(randomBytes32, scalar(0), point(1))
- val channel_announcement = ChannelAnnouncement(randomBytes64, randomBytes64, randomBytes64, randomBytes64, bin(7, 9), Block.RegtestGenesisBlock.hash, ShortChannelId(1), randomKey.publicKey, randomKey.publicKey, randomKey.publicKey, randomKey.publicKey)
- val node_announcement = NodeAnnouncement(randomBytes64, bin(0, 0), 1, randomKey.publicKey, Color(100.toByte, 200.toByte, 300.toByte), "node-alias", IPv4(InetAddress.getByAddress(Array[Byte](192.toByte, 168.toByte, 1.toByte, 42.toByte)).asInstanceOf[Inet4Address], 42000) :: Nil)
- val channel_update1 = ChannelUpdate(randomBytes64, Block.RegtestGenesisBlock.hash, ShortChannelId(1), 2, 1, 0, 3, 4, 5, 6, Some(50000000L))
- val channel_update2 = ChannelUpdate(randomBytes64, Block.RegtestGenesisBlock.hash, ShortChannelId(1), 2, 0, 0, 3, 4, 5, 6, None)
- val announcement_signatures = AnnouncementSignatures(randomBytes32, ShortChannelId(42), randomBytes64, randomBytes64)
- val ping = Ping(100, bin(10, 1))
- val pong = Pong(bin(10, 1))
-
- val cached = channel_announcement :: node_announcement :: channel_update1 :: channel_update2 :: Nil
- val nonCached = commit_sig :: revoke_and_ack :: announcement_signatures :: ping :: pong :: Nil
- val msgs: List[LightningMessage] = cached ::: nonCached
-
- msgs.foreach {
- case msg => {
- val encoded = codec.encode(msg).require
- val decoded = codec.decode(encoded).require
- assert(msg === decoded.value)
- }
- }
-
- import scala.language.reflectiveCalls
- val cachedKeys = codec.cache.asMap().keySet()
- assert(cached.forall(msg => cachedKeys.contains(msg)))
- assert(nonCached.forall(msg => !cachedKeys.contains(msg)))
-
- }
-
test("decode channel_update with htlc_maximum_msat") {
// this was generated by c-lightning
val bin = hex"010258fff7d0e987e2cdd560e3bb5a046b4efe7b26c969c2f51da1dceec7bcb8ae1b634790503d5290c1a6c51d681cf8f4211d27ed33a257dcc1102862571bf1792306226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f0005a100000200005bc75919010100060000000000000001000000010000000a000000003a699d00"
diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/TlvCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/TlvCodecsSpec.scala
new file mode 100644
index 000000000..a4c4541b8
--- /dev/null
+++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/TlvCodecsSpec.scala
@@ -0,0 +1,157 @@
+/*
+ * Copyright 2019 ACINQ SAS
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package fr.acinq.eclair.wire
+
+import fr.acinq.bitcoin.Crypto.PublicKey
+import fr.acinq.eclair.{ShortChannelId, UInt64}
+import fr.acinq.eclair.UInt64.Conversions._
+import fr.acinq.eclair.wire.CommonCodecs.{publicKey, shortchannelid, uint64, varint}
+import fr.acinq.eclair.wire.TlvCodecs._
+import org.scalatest.FunSuite
+import scodec.bits.HexStringSyntax
+import scodec.codecs._
+import scodec.Codec
+
+/**
+ * Created by t-bast on 20/06/2019.
+ */
+
+class TlvCodecsSpec extends FunSuite {
+
+ import TlvCodecsSpec._
+
+ test("encode/decode tlv") {
+ val testCases = Seq(
+ (hex"01 08 000000000000002a", TestType1(42)),
+ (hex"02 08 0000000000000226", TestType2(ShortChannelId(550))),
+ (hex"03 31 02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619 0000000000000231 0000000000000451", TestType3(PublicKey(hex"02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619"), 561, 1105)),
+ (hex"ff1234567890123456 fd0001 10101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010010101010101", GenericTlv(6211610197754262546L, hex"10101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010010101010101"))
+ )
+
+ for ((bin, expected) <- testCases) {
+ val decoded = testTlvCodec.decode(bin.toBitVector).require.value.asInstanceOf[Tlv]
+ assert(decoded === expected)
+ val encoded = testTlvCodec.encode(expected).require.toByteVector
+ assert(encoded === bin)
+ }
+ }
+
+ test("decode invalid tlv") {
+ val testCases = Seq(
+ hex"fd02", // type truncated
+ hex"fd022a", // truncated after type
+ hex"fd0100", // not minimally encoded type
+ hex"2a fd02", // length truncated
+ hex"2a fd0226", // truncated after length
+ hex"2a fe01010000", // not minimally encoded length
+ hex"2a fd2602 0231", // value truncated
+ hex"02 01 2a", // short channel id too short
+ hex"02 09 010101010101010101", // short channel id length too big
+ hex"2a ff0000000000000080" // invalid length (too big to fit inside a long)
+ )
+
+ for (testCase <- testCases) {
+ assert(testTlvCodec.decode(testCase.toBitVector).isFailure)
+ }
+ }
+
+ test("decode invalid tlv stream") {
+ val testCases = Seq(
+ hex"0108000000000000002a 02", // valid tlv record followed by invalid tlv record (only type, length and value are missing)
+ hex"02080000000000000226 0108000000000000002a", // valid tlv records but invalid ordering
+ hex"02080000000000000231 02080000000000000451", // duplicate tlv type
+ hex"0108000000000000002a 2a0101", // unknown even type
+ hex"0a080000000000000231 0b0400000451" // valid tlv records but from different namespace
+ )
+
+ for (testCase <- testCases) {
+ assert(tlvStream(testTlvCodec).decode(testCase.toBitVector).isFailure, testCase)
+ }
+ }
+
+ test("create invalid tlv stream") {
+ assertThrows[IllegalArgumentException](TlvStream(Seq(GenericTlv(42, hex"2a")))) // unknown even type
+ assertThrows[IllegalArgumentException](TlvStream(Seq(TestType1(561), TestType2(ShortChannelId(1105)), GenericTlv(42, hex"2a")))) // unknown even type
+ assertThrows[IllegalArgumentException](TlvStream(Seq(TestType1(561), TestType1(1105)))) // duplicate type
+ assertThrows[IllegalArgumentException](TlvStream(Seq(TestType2(ShortChannelId(1105)), TestType1(561)))) // invalid ordering
+ }
+
+ test("encode/decode tlv stream") {
+ val bin = hex"01080000000000000231 02080000000000000451 033102eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f28368661900000000000002310000000000000451"
+ val expected = Seq(
+ TestType1(561),
+ TestType2(ShortChannelId(1105)),
+ TestType3(PublicKey(hex"02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619"), 561, 1105)
+ )
+
+ val decoded = tlvStream(testTlvCodec).decode(bin.toBitVector).require.value
+ assert(decoded === TlvStream(expected))
+
+ val encoded = tlvStream(testTlvCodec).encode(TlvStream(expected)).require.toByteVector
+ assert(encoded === bin)
+ }
+
+ test("encode/decode tlv stream with unknown odd type") {
+ val bin = hex"01080000000000000231 0b0400000451 0d02002a"
+ val expected = Seq(
+ TestType1(561),
+ GenericTlv(11, hex"00000451"),
+ TestType13(42)
+ )
+
+ val decoded = tlvStream(testTlvCodec).decode(bin.toBitVector).require.value
+ assert(decoded === TlvStream(expected))
+
+ val encoded = tlvStream(testTlvCodec).encode(TlvStream(expected)).require.toByteVector
+ assert(encoded === bin)
+ }
+
+}
+
+object TlvCodecsSpec {
+
+ // @formatter:off
+ sealed trait TestTlv extends Tlv
+ case class TestType1(uintValue: UInt64) extends TestTlv { override val `type` = UInt64(1) }
+ case class TestType2(shortChannelId: ShortChannelId) extends TestTlv { override val `type` = UInt64(2) }
+ case class TestType3(nodeId: PublicKey, value1: UInt64, value2: UInt64) extends TestTlv { override val `type` = UInt64(3) }
+ case class TestType13(intValue: Int) extends TestTlv { override val `type` = UInt64(13) }
+
+ val testCodec1: Codec[TestType1] = (("length" | constant(hex"08")) :: ("value" | uint64)).as[TestType1]
+ val testCodec2: Codec[TestType2] = (("length" | constant(hex"08")) :: ("short_channel_id" | shortchannelid)).as[TestType2]
+ val testCodec3: Codec[TestType3] = (("length" | constant(hex"31")) :: ("node_id" | publicKey) :: ("value_1" | uint64) :: ("value_2" | uint64)).as[TestType3]
+ val testCodec13: Codec[TestType13] = (("length" | constant(hex"02")) :: ("value" | uint16)).as[TestType13]
+ val testTlvCodec = tlvFallback(discriminated[Tlv].by(varint)
+ .typecase(1, testCodec1)
+ .typecase(2, testCodec2)
+ .typecase(3, testCodec3)
+ .typecase(13, testCodec13)
+ )
+
+ sealed trait OtherTlv extends Tlv
+ case class OtherType1(uintValue: UInt64) extends OtherTlv { override val `type` = UInt64(10) }
+ case class OtherType2(smallValue: Long) extends OtherTlv { override val `type` = UInt64(11) }
+
+ val otherCodec1: Codec[OtherType1] = (("length" | constant(hex"08")) :: ("value" | uint64)).as[OtherType1]
+ val otherCodec2: Codec[OtherType2] = (("length" | constant(hex"04")) :: ("value" | uint32)).as[OtherType2]
+ val otherTlvCodec = tlvFallback(discriminated[Tlv].by(varint)
+ .typecase(10, otherCodec1)
+ .typecase(11, otherCodec2)
+ )
+ // @formatter:on
+
+}
diff --git a/pom.xml b/pom.xml
index e1820065c..e13541671 100644
--- a/pom.xml
+++ b/pom.xml
@@ -128,6 +128,11 @@
-nobootcp
+
+ -Xmx1024m
+ -Xms1024m
+ -Xss32m
+
${scala.version.short}