1
0
Fork 0
mirror of https://github.com/ACINQ/eclair.git synced 2025-03-13 11:35:47 +01:00

Merge branch 'wip-routing-sync' into android

This commit is contained in:
sstone 2018-05-04 17:56:03 +02:00
commit c275482c2c
41 changed files with 551 additions and 253 deletions

View file

@ -152,18 +152,18 @@ java -Declair.datadir=/tmp/node1 -jar eclair-node-gui-<version>-<commit_id>.jar
## Docker
A [Dockerfile](Dockerfile) image is built on each commit on [docker hub](https://hub.docker.com/r/ACINQ/eclair) for running a dockerized eclair-node.
A [Dockerfile](Dockerfile) image is built on each commit on [docker hub](https://hub.docker.com/r/acinq/eclair) for running a dockerized eclair-node.
You can use the `JAVA_OPTS` environment variable to set arguments to `eclair-node`.
```
docker run -ti --rm -e "JAVA_OPTS=-Xmx512m -Declair.api.binding-ip=0.0.0.0 -Declair.node-alias=node-pm -Declair.printToConsole" acinq\eclair
docker run -ti --rm -e "JAVA_OPTS=-Xmx512m -Declair.api.binding-ip=0.0.0.0 -Declair.node-alias=node-pm -Declair.printToConsole" acinq/eclair
```
If you want to persist the data directory, you can make the volume to your host with the `-v` argument, as the following example:
```
docker run -ti --rm -v "/path_on_host:/data" -e "JAVA_OPTS=-Declair.printToConsole" acinq\eclair
docker run -ti --rm -v "/path_on_host:/data" -e "JAVA_OPTS=-Declair.printToConsole" acinq/eclair
```
## Mainnet usage

View file

@ -25,14 +25,14 @@ eclair {
zmq = "tcp://127.0.0.1:29000"
}
default-feerates { // those are in satoshis per byte
default-feerates { // those are in satoshis per kilobyte
delay-blocks {
1 = 210
2 = 180
6 = 150
12 = 110
36 = 50
72 = 20
1 = 210000
2 = 180000
6 = 150000
12 = 110000
36 = 50000
72 = 20000
}
}
min-feerate = 1 // minimum feerate in satoshis per byte (same default value as bitcoin core's minimum relay fee)
@ -77,4 +77,5 @@ eclair {
payment-request-expiry = 1 hour // default expiry for payment requests generated by this node
max-pending-payment-requests = 10000000
max-payment-fee = 0.03 // max total fee for outgoing payments, in percentage: sending a payment will not be attempted if the cheapest route found is more expensive than that
min-funding-satoshis = 100000
}

View file

@ -18,7 +18,7 @@ package fr.acinq.eclair
import java.util.concurrent.atomic.{AtomicLong, AtomicReference}
import fr.acinq.eclair.blockchain.fee.{FeeratesPerByte, FeeratesPerKw}
import fr.acinq.eclair.blockchain.fee.{FeeratesPerKB, FeeratesPerKw}
/**
* Created by PM on 25/01/2016.
@ -33,10 +33,10 @@ object Globals {
val blockCount = new AtomicLong(0)
/**
* This holds the current feerates, in satoshi-per-bytes.
* This holds the current feerates, in satoshi-per-kilobytes.
* The value is read by all actors, hence it needs to be thread-safe.
*/
val feeratesPerByte = new AtomicReference[FeeratesPerByte](null)
val feeratesPerKB = new AtomicReference[FeeratesPerKB](null)
/**
* This holds the current feerates, in satoshi-per-kw.

View file

@ -72,7 +72,8 @@ case class NodeParams(keyManager: KeyManager,
watcherType: WatcherType,
paymentRequestExpiry: FiniteDuration,
maxPendingPaymentRequests: Int,
maxPaymentFee: Double) {
maxPaymentFee: Double,
minFundingSatoshis: Long) {
val privateKey = keyManager.nodeKey.privateKey
val nodeId = keyManager.nodeId
}
@ -189,6 +190,8 @@ object NodeParams {
watcherType = watcherType,
paymentRequestExpiry = FiniteDuration(config.getDuration("payment-request-expiry", TimeUnit.SECONDS), TimeUnit.SECONDS),
maxPendingPaymentRequests = config.getInt("max-pending-payment-requests"),
maxPaymentFee = config.getDouble("max-payment-fee"))
maxPaymentFee = config.getDouble("max-payment-fee"),
minFundingSatoshis = config.getLong("min-funding-satoshis")
)
}
}

View file

@ -87,7 +87,7 @@ class Setup(datadir: File, wallet_opt: Option[EclairWallet] = None, overrideDefa
case _ => ???
}
defaultFeerates = FeeratesPerByte(
defaultFeerates = FeeratesPerKB(
block_1 = config.getLong("default-feerates.delay-blocks.1"),
blocks_2 = config.getLong("default-feerates.delay-blocks.2"),
blocks_6 = config.getLong("default-feerates.delay-blocks.6"),
@ -101,11 +101,11 @@ class Setup(datadir: File, wallet_opt: Option[EclairWallet] = None, overrideDefa
case _ => new FallbackFeeProvider(new BitgoFeeProvider(nodeParams.chainHash) :: new EarnDotComFeeProvider() :: new ConstantFeeProvider(defaultFeerates) :: Nil, minFeeratePerByte) // order matters!
}
_ = system.scheduler.schedule(0 seconds, 10 minutes)(feeProvider.getFeerates.map {
case feerates: FeeratesPerByte =>
Globals.feeratesPerByte.set(feerates)
case feerates: FeeratesPerKB =>
Globals.feeratesPerKB.set(feerates)
Globals.feeratesPerKw.set(FeeratesPerKw(feerates))
system.eventStream.publish(CurrentFeerates(Globals.feeratesPerKw.get))
logger.info(s"current feeratesPerByte=${Globals.feeratesPerByte.get()}")
logger.info(s"current feeratesPerKB=${Globals.feeratesPerKB.get()} feeratesPerKw=${Globals.feeratesPerKw.get()}")
feeratesRetrieved.trySuccess(true)
})
_ <- feeratesRetrieved.future

View file

@ -60,7 +60,7 @@ class BitcoinCoreWallet(rpcClient: BitcoinJsonRPCClient)(implicit system: ActorS
def publishTransaction(hex: String)(implicit ec: ExecutionContext): Future[String] = rpcClient.invoke("sendrawtransaction", hex) collect { case JString(txid) => txid }
def unlockOutpoint(outPoints: List[OutPoint])(implicit ec: ExecutionContext): Future[Boolean] = rpcClient.invoke("lockunspent", true, outPoints.map(outPoint => Utxo(outPoint.txid.toString, outPoint.index))) collect { case JBool(result) => result }
def unlockOutpoints(outPoints: Seq[OutPoint])(implicit ec: ExecutionContext): Future[Boolean] = rpcClient.invoke("lockunspent", true, outPoints.toList.map(outPoint => Utxo(outPoint.txid.toString, outPoint.index))) collect { case JBool(result) => result }
override def getBalance: Future[Satoshi] = rpcClient.invoke("getbalance") collect { case JDouble(balance) => Satoshi((balance * 10e8).toLong) }
@ -69,6 +69,17 @@ class BitcoinCoreWallet(rpcClient: BitcoinJsonRPCClient)(implicit system: ActorS
JString(address) <- rpcClient.invoke("getnewaddress")
} yield address
private def signTransactionOrUnlock(tx: Transaction): Future[SignTransactionResponse] = {
val f = signTransaction(tx)
// if signature fails (e.g. because wallet is uncrypted) we need to unlock the utxos
f.recoverWith { case _ =>
unlockOutpoints(tx.txIn.map(_.outPoint))
.recover { case t: Throwable => logger.warn(s"Cannot unlock failed transaction's UTXOs txid=${tx.txid}", t); t } // no-op, just add a log in case of failure
.flatMap { case _ => f } // return signTransaction error
.recoverWith { case _ => f } // return signTransaction error
}
}
override def makeFundingTx(pubkeyScript: BinaryData, amount: Satoshi, feeRatePerKw: Long): Future[MakeFundingTxResponse] = {
// partial funding tx
val partialFundingTx = Transaction(
@ -80,7 +91,7 @@ class BitcoinCoreWallet(rpcClient: BitcoinJsonRPCClient)(implicit system: ActorS
// we ask bitcoin core to add inputs to the funding tx, and use the specified change address
FundTransactionResponse(unsignedFundingTx, changepos, fee) <- fundTransaction(partialFundingTx, lockUnspents = true)
// now let's sign the funding tx
SignTransactionResponse(fundingTx, _) <- signTransaction(unsignedFundingTx)
SignTransactionResponse(fundingTx, _) <- signTransactionOrUnlock(unsignedFundingTx)
// there will probably be a change output, so we need to find which output is ours
outputIndex = Transactions.findPubKeyScriptIndex(fundingTx, pubkeyScript, outputsAlreadyUsed = Set.empty, amount_opt = None)
_ = logger.debug(s"created funding txid=${fundingTx.txid} outputIndex=$outputIndex fee=$fee")
@ -95,8 +106,7 @@ class BitcoinCoreWallet(rpcClient: BitcoinJsonRPCClient)(implicit system: ActorS
}
.recover { case _ => true } // in all other cases we consider that the tx has been published
override def rollback(tx: Transaction): Future[Boolean] = unlockOutpoint(tx.txIn.map(_.outPoint).toList) // we unlock all utxos used by the tx
override def rollback(tx: Transaction): Future[Boolean] = unlockOutpoints(tx.txIn.map(_.outPoint)) // we unlock all utxos used by the tx
}
object BitcoinCoreWallet {

View file

@ -33,6 +33,8 @@ class ElectrumEclairWallet(val wallet: ActorRef, chainHash: BinaryData)(implicit
override def getFinalAddress = (wallet ? GetCurrentReceiveAddress).mapTo[GetCurrentReceiveAddressResponse].map(_.address)
def getXpub: Future[GetXpubResponse] = (wallet ? GetXpub).mapTo[GetXpubResponse]
override def makeFundingTx(pubkeyScript: BinaryData, amount: Satoshi, feeRatePerKw: Long) = {
val tx = Transaction(version = 2, txIn = Nil, txOut = TxOut(amount, pubkeyScript) :: Nil, lockTime = 0)
(wallet ? CompleteTransaction(tx, feeRatePerKw)).mapTo[CompleteTransactionResponse].map(response => response match {

View file

@ -266,6 +266,11 @@ class ElectrumWallet(seed: BinaryData, client: ActorRef, params: ElectrumWallet.
case Event(GetData, data) => stay replying GetDataResponse(data)
case Event(GetXpub ,_) => {
val (xpub, path) = computeXpub(master, chainHash)
stay replying GetXpubResponse(xpub, path)
}
case Event(ElectrumClient.BroadcastTransaction(tx), _) => stay replying ElectrumClient.BroadcastTransactionResponse(tx, Some(Error(-1, "wallet is not connected")))
}
@ -294,6 +299,9 @@ object ElectrumWallet {
case object GetBalance extends Request
case class GetBalanceResponse(confirmed: Satoshi, unconfirmed: Satoshi) extends Response
case object GetXpub extends Request
case class GetXpubResponse(xpub: String, path: String) extends Response
case object GetCurrentReceiveAddress extends Request
case class GetCurrentReceiveAddressResponse(address: String) extends Response
@ -369,32 +377,43 @@ object ElectrumWallet {
*/
def computeScriptHashFromPublicKey(key: PublicKey): BinaryData = Crypto.sha256(Script.write(computePublicKeyScript(key))).reverse
def accountPath(chainHash: BinaryData) : List[Long] = chainHash match {
case Block.RegtestGenesisBlock.hash | Block.TestnetGenesisBlock.hash => hardened(49) :: hardened(1) :: hardened(0) :: Nil
case Block.LivenetGenesisBlock.hash => hardened(49) :: hardened(0) :: hardened(0) :: Nil
}
/**
* use BIP49 (and not BIP44) since we use p2sh-of-p2wpkh
*
* @param master master key
* @return the BIP49 account key for this master key: m/49'/1'/0'/0 on testnet/regtest, m/49'/0'/0'/0 on mainnet
*/
def accountKey(master: ExtendedPrivateKey, chainHash: BinaryData) = chainHash match {
case Block.RegtestGenesisBlock.hash | Block.TestnetGenesisBlock.hash =>
DeterministicWallet.derivePrivateKey(master, hardened(49) :: hardened(1) :: hardened(0) :: 0L :: Nil)
case Block.LivenetGenesisBlock.hash =>
DeterministicWallet.derivePrivateKey(master, hardened(49) :: hardened(0) :: hardened(0) :: 0L :: Nil)
}
def accountKey(master: ExtendedPrivateKey, chainHash: BinaryData) = DeterministicWallet.derivePrivateKey(master, accountPath(chainHash) ::: 0L :: Nil)
/**
* Compute the wallet's xpub
* @param master master key
* @param chainHash chain hash
* @return a (xpub, path) tuple where xpub is the encoded account public key, and path is the derivation path for the account key
*/
def computeXpub(master: ExtendedPrivateKey, chainHash: BinaryData) : (String, String) = {
val xpub = DeterministicWallet.publicKey(DeterministicWallet.derivePrivateKey(master, accountPath(chainHash)))
// we use the tpub/xpub prefix instead of upub/ypub because it is more widely understood
val prefix = chainHash match {
case Block.LivenetGenesisBlock.hash => DeterministicWallet.xpub
case Block.RegtestGenesisBlock.hash | Block.TestnetGenesisBlock.hash => DeterministicWallet.tpub
}
(DeterministicWallet.encode(xpub, prefix), xpub.path.toString())
}
/**
* use BIP49 (and not BIP44) since we use p2sh-of-p2wpkh
*
* @param master master key
* @return the BIP49 change key for this master key: m/49'/1'/0'/1 on testnet/regtest, m/49'/0'/0'/1 on mainnet
*/
def changeKey(master: ExtendedPrivateKey, chainHash: BinaryData) = chainHash match {
case Block.RegtestGenesisBlock.hash | Block.TestnetGenesisBlock.hash =>
DeterministicWallet.derivePrivateKey(master, hardened(49) :: hardened(1) :: hardened(0) :: 1L :: Nil)
case Block.LivenetGenesisBlock.hash =>
DeterministicWallet.derivePrivateKey(master, hardened(49) :: hardened(0) :: hardened(0) :: 1L :: Nil)
}
def changeKey(master: ExtendedPrivateKey, chainHash: BinaryData) = DeterministicWallet.derivePrivateKey(master, accountPath(chainHash) ::: 1L :: Nil)
def totalAmount(utxos: Seq[Utxo]): Satoshi = Satoshi(utxos.map(_.item.value).sum)

View file

@ -25,37 +25,37 @@ import scala.concurrent.{ExecutionContext, Future}
/**
* Created by PM on 09/07/2017.
*/
class BitcoinCoreFeeProvider(rpcClient: BitcoinJsonRPCClient, defaultFeerates: FeeratesPerByte)(implicit ec: ExecutionContext) extends FeeProvider {
class BitcoinCoreFeeProvider(rpcClient: BitcoinJsonRPCClient, defaultFeerates: FeeratesPerKB)(implicit ec: ExecutionContext) extends FeeProvider {
/**
* We need this to keep commitment tx fees in sync with the state of the network
*
* @param nBlocks number of blocks until tx is confirmed
* @return the current fee estimate in Satoshi/Byte
* @return the current fee estimate in Satoshi/KB
*/
def estimateSmartFee(nBlocks: Int): Future[Long] =
rpcClient.invoke("estimatesmartfee", nBlocks).map(json => {
json \ "feerate" match {
case JDouble(feerate) =>
// estimatesmartfee returns a fee rate in Btc/Kb
btc2satoshi(Btc(feerate)).amount / 1024
// estimatesmartfee returns a fee rate in Btc/KB
btc2satoshi(Btc(feerate)).amount
case JInt(feerate) if feerate.toLong < 0 =>
// negative value means failure
feerate.toLong
case JInt(feerate) =>
// should (hopefully) never happen
btc2satoshi(Btc(feerate.toLong)).amount / 1024
btc2satoshi(Btc(feerate.toLong)).amount
}
})
override def getFeerates: Future[FeeratesPerByte] = for {
override def getFeerates: Future[FeeratesPerKB] = for {
block_1 <- estimateSmartFee(1)
blocks_2 <- estimateSmartFee(2)
blocks_6 <- estimateSmartFee(6)
blocks_12 <- estimateSmartFee(12)
blocks_36 <- estimateSmartFee(36)
blocks_72 <- estimateSmartFee(72)
} yield FeeratesPerByte(
} yield FeeratesPerKB(
block_1 = if (block_1 > 0) block_1 else defaultFeerates.block_1,
blocks_2 = if (blocks_2 > 0) blocks_2 else defaultFeerates.blocks_2,
blocks_6 = if (blocks_6 > 0) blocks_6 else defaultFeerates.blocks_6,

View file

@ -19,7 +19,6 @@ package fr.acinq.eclair.blockchain.fee
import akka.actor.ActorSystem
import fr.acinq.bitcoin.{BinaryData, Block}
import fr.acinq.eclair.HttpHelper.get
import fr.acinq.eclair.feerateKbToByte
import org.json4s.JsonAST.{JInt, JValue}
import scala.concurrent.{ExecutionContext, Future}
@ -33,7 +32,7 @@ class BitgoFeeProvider(chainHash: BinaryData)(implicit system: ActorSystem, ec:
case _ => "https://test.bitgo.com/api/v2/tbtc/tx/fee"
}
override def getFeerates: Future[FeeratesPerByte] =
override def getFeerates: Future[FeeratesPerKB] =
for {
json <- get(uri)
feeRanges = parseFeeRanges(json)
@ -47,8 +46,8 @@ object BitgoFeeProvider {
def parseFeeRanges(json: JValue): Seq[BlockTarget] = {
val blockTargets = json \ "feeByBlockTarget"
blockTargets.foldField(Seq.empty[BlockTarget]) {
// we divide by 1024 because bitgo returns estimates in Satoshi/Kb and we use estimates in Satoshi/Byte
case (list, (strBlockTarget, JInt(feePerKb))) => list :+ BlockTarget(strBlockTarget.toInt, feerateKbToByte(feePerKb.longValue()))
// BitGo returns estimates in Satoshi/KB, which is what we want
case (list, (strBlockTarget, JInt(feePerKB))) => list :+ BlockTarget(strBlockTarget.toInt, feePerKB.longValue())
}
}
@ -59,8 +58,8 @@ object BitgoFeeProvider {
belowLimit.map(_.fee).min
}
def extractFeerates(feeRanges: Seq[BlockTarget]): FeeratesPerByte =
FeeratesPerByte(
def extractFeerates(feeRanges: Seq[BlockTarget]): FeeratesPerKB =
FeeratesPerKB(
block_1 = extractFeerate(feeRanges, 1),
blocks_2 = extractFeerate(feeRanges, 2),
blocks_6 = extractFeerate(feeRanges, 6),

View file

@ -21,8 +21,8 @@ import scala.concurrent.Future
/**
* Created by PM on 09/07/2017.
*/
class ConstantFeeProvider(feerates: FeeratesPerByte) extends FeeProvider {
class ConstantFeeProvider(feerates: FeeratesPerKB) extends FeeProvider {
override def getFeerates: Future[FeeratesPerByte] = Future.successful(feerates)
override def getFeerates: Future[FeeratesPerKB] = Future.successful(feerates)
}

View file

@ -29,7 +29,7 @@ class EarnDotComFeeProvider(implicit system: ActorSystem, ec: ExecutionContext)
import EarnDotComFeeProvider._
override def getFeerates: Future[FeeratesPerByte] =
override def getFeerates: Future[FeeratesPerKB] =
for {
json <- get("https://bitcoinfees.earn.com/api/v1/fees/list")
feeRanges = parseFeeRanges(json)
@ -48,7 +48,8 @@ object EarnDotComFeeProvider {
val JInt(memCount) = item \ "memCount"
val JInt(minDelay) = item \ "minDelay"
val JInt(maxDelay) = item \ "maxDelay"
FeeRange(minFee = minFee.toLong, maxFee = maxFee.toLong, memCount = memCount.toLong, minDelay = minDelay.toLong, maxDelay = maxDelay.toLong)
// earn.com returns fees in Satoshi/byte and we want Satoshi/KiloByte
FeeRange(minFee = 1000 * minFee.toLong, maxFee = 1000 * maxFee.toLong, memCount = memCount.toLong, minDelay = minDelay.toLong, maxDelay = maxDelay.toLong)
})
}
@ -59,8 +60,8 @@ object EarnDotComFeeProvider {
Math.max(belowLimit.minBy(_.maxFee).maxFee, 1)
}
def extractFeerates(feeRanges: Seq[FeeRange]): FeeratesPerByte =
FeeratesPerByte(
def extractFeerates(feeRanges: Seq[FeeRange]): FeeratesPerKB =
FeeratesPerKB(
block_1 = extractFeerate(feeRanges, 1),
blocks_2 = extractFeerate(feeRanges, 2),
blocks_6 = extractFeerate(feeRanges, 6),

View file

@ -29,25 +29,25 @@ class FallbackFeeProvider(providers: Seq[FeeProvider], minFeeratePerByte: Long)(
require(providers.size >= 1, "need at least one fee provider")
require(minFeeratePerByte > 0, "minimum fee rate must be strictly greater than 0")
def getFeerates(fallbacks: Seq[FeeProvider]): Future[FeeratesPerByte] =
def getFeerates(fallbacks: Seq[FeeProvider]): Future[FeeratesPerKB] =
fallbacks match {
case last +: Nil => last.getFeerates
case head +: remaining => head.getFeerates.recoverWith { case _ => getFeerates(remaining) }
}
override def getFeerates: Future[FeeratesPerByte] = getFeerates(providers).map(FallbackFeeProvider.enforceMinimumFeerate(_, minFeeratePerByte))
override def getFeerates: Future[FeeratesPerKB] = getFeerates(providers).map(FallbackFeeProvider.enforceMinimumFeerate(_, minFeeratePerByte))
}
object FallbackFeeProvider {
def enforceMinimumFeerate(feeratesPerByte: FeeratesPerByte, minFeeratePerByte: Long) : FeeratesPerByte = feeratesPerByte.copy(
block_1 = Math.max(feeratesPerByte.block_1, minFeeratePerByte),
blocks_2 = Math.max(feeratesPerByte.blocks_2, minFeeratePerByte),
blocks_6 = Math.max(feeratesPerByte.blocks_6, minFeeratePerByte),
blocks_12 = Math.max(feeratesPerByte.blocks_12, minFeeratePerByte),
blocks_36 = Math.max(feeratesPerByte.blocks_36, minFeeratePerByte),
blocks_72 = Math.max(feeratesPerByte.blocks_72, minFeeratePerByte)
def enforceMinimumFeerate(feeratesPerKB: FeeratesPerKB, minFeeratePerByte: Long) : FeeratesPerKB = feeratesPerKB.copy(
block_1 = Math.max(feeratesPerKB.block_1, minFeeratePerByte * 1000),
blocks_2 = Math.max(feeratesPerKB.blocks_2, minFeeratePerByte * 1000),
blocks_6 = Math.max(feeratesPerKB.blocks_6, minFeeratePerByte * 1000),
blocks_12 = Math.max(feeratesPerKB.blocks_12, minFeeratePerByte * 1000),
blocks_36 = Math.max(feeratesPerKB.blocks_36, minFeeratePerByte * 1000),
blocks_72 = Math.max(feeratesPerKB.blocks_72, minFeeratePerByte * 1000)
)
}

View file

@ -16,7 +16,7 @@
package fr.acinq.eclair.blockchain.fee
import fr.acinq.eclair.feerateByte2Kw
import fr.acinq.eclair._
import scala.concurrent.Future
@ -25,26 +25,28 @@ import scala.concurrent.Future
*/
trait FeeProvider {
def getFeerates: Future[FeeratesPerByte]
def getFeerates: Future[FeeratesPerKB]
}
case class FeeratesPerByte(block_1: Long, blocks_2: Long, blocks_6: Long, blocks_12: Long, blocks_36: Long, blocks_72: Long) {
// stores fee rate in satoshi/kb (1 kb = 1000 bytes)
case class FeeratesPerKB(block_1: Long, blocks_2: Long, blocks_6: Long, blocks_12: Long, blocks_36: Long, blocks_72: Long) {
require(block_1 > 0 && blocks_2 > 0 && blocks_6 > 0 && blocks_12 > 0 && blocks_36 > 0 && blocks_72 > 0, "all feerates must be strictly greater than 0")
}
// stores fee rate in satoshi/kw (1 kw = 1000 weight units)
case class FeeratesPerKw(block_1: Long, blocks_2: Long, blocks_6: Long, blocks_12: Long, blocks_36: Long, blocks_72: Long) {
require(block_1 > 0 && blocks_2 > 0 && blocks_6 > 0 && blocks_12 > 0 && blocks_36 > 0 && blocks_72 > 0, "all feerates must be strictly greater than 0")
}
object FeeratesPerKw {
def apply(feerates: FeeratesPerByte): FeeratesPerKw = FeeratesPerKw(
block_1 = feerateByte2Kw(feerates.block_1),
blocks_2 = feerateByte2Kw(feerates.blocks_2),
blocks_6 = feerateByte2Kw(feerates.blocks_6),
blocks_12 = feerateByte2Kw(feerates.blocks_12),
blocks_36 = feerateByte2Kw(feerates.blocks_36),
blocks_72 = feerateByte2Kw(feerates.blocks_72))
def apply(feerates: FeeratesPerKB): FeeratesPerKw = FeeratesPerKw(
block_1 = feerateKB2Kw(feerates.block_1),
blocks_2 = feerateKB2Kw(feerates.blocks_2),
blocks_6 = feerateKB2Kw(feerates.blocks_6),
blocks_12 = feerateKB2Kw(feerates.blocks_12),
blocks_36 = feerateKB2Kw(feerates.blocks_36),
blocks_72 = feerateKB2Kw(feerates.blocks_72))
/**
* Used in tests
@ -60,3 +62,4 @@ object FeeratesPerKw {
blocks_36 = feeratePerKw,
blocks_72 = feeratePerKw)
}

View file

@ -51,7 +51,6 @@ object Channel {
// https://github.com/lightningnetwork/lightning-rfc/blob/master/02-peer-protocol.md#requirements
val MAX_FUNDING_SATOSHIS = 16777216L // = 2^24
val MIN_FUNDING_SATOSHIS = 1000
val MAX_ACCEPTED_HTLCS = 483
// we don't want the counterparty to use a dust limit lower than that, because they wouldn't only hurt themselves we may need them to publish their commit tx in certain cases (backup/restore)
@ -1092,12 +1091,12 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
}
case Event(WatchEventSpent(BITCOIN_FUNDING_SPENT, tx), d: DATA_CLOSING) =>
if (d.mutualCloseProposed.map(_.txid).contains(tx.txid)) {
if (d.mutualClosePublished.map(_.txid).contains(tx.txid)) {
// we already know about this tx, probably because we have published it ourselves after successful negotiation
stay
} else if (d.mutualCloseProposed.map(_.txid).contains(tx.txid)) {
// at any time they can publish a closing tx with any sig we sent them
handleMutualClose(tx, Right(d))
} else if (d.mutualClosePublished.map(_.txid).contains(tx.txid)) {
// we have published a closing tx which isn't one that we proposed, and used it instead of our last commitment when an error happened
handleMutualClose(tx, Right(d))
} else if (Some(tx.txid) == d.localCommitPublished.map(_.commitTx.txid)) {
// this is because WatchSpent watches never expire and we are notified multiple times
stay

View file

@ -59,7 +59,7 @@ object Helpers {
*/
def validateParamsFundee(nodeParams: NodeParams, open: OpenChannel): Unit = {
if (nodeParams.chainHash != open.chainHash) throw InvalidChainHash(open.temporaryChannelId, local = nodeParams.chainHash, remote = open.chainHash)
if (open.fundingSatoshis < Channel.MIN_FUNDING_SATOSHIS || open.fundingSatoshis >= Channel.MAX_FUNDING_SATOSHIS) throw InvalidFundingAmount(open.temporaryChannelId, open.fundingSatoshis, Channel.MIN_FUNDING_SATOSHIS, Channel.MAX_FUNDING_SATOSHIS)
if (open.fundingSatoshis < nodeParams.minFundingSatoshis || open.fundingSatoshis >= Channel.MAX_FUNDING_SATOSHIS) throw InvalidFundingAmount(open.temporaryChannelId, open.fundingSatoshis, nodeParams.minFundingSatoshis, Channel.MAX_FUNDING_SATOSHIS)
if (open.pushMsat > 1000 * open.fundingSatoshis) throw InvalidPushAmount(open.temporaryChannelId, open.pushMsat, 1000 * open.fundingSatoshis)
val localFeeratePerKw = Globals.feeratesPerKw.get.blocks_2
if (isFeeDiffTooHigh(open.feeratePerKw, localFeeratePerKw, nodeParams.maxFeerateMismatch)) throw FeerateTooDifferent(open.temporaryChannelId, localFeeratePerKw, open.feeratePerKw)

View file

@ -25,7 +25,6 @@ import fr.acinq.bitcoin.Crypto.PublicKey
import fr.acinq.bitcoin.{BinaryData, Protocol}
import fr.acinq.eclair.crypto.Noise._
import fr.acinq.eclair.wire.{ChannelAnnouncement, ChannelUpdate, NodeAnnouncement}
import scodec.Attempt.Successful
import scodec.bits.BitVector
import scodec.{Attempt, Codec, DecodeResult}

View file

@ -16,14 +16,14 @@
package fr.acinq.eclair.db.sqlite
import java.net.InetSocketAddress
import java.net.{Inet4Address, Inet6Address, InetSocketAddress}
import java.sql.Connection
import fr.acinq.bitcoin.Crypto
import fr.acinq.bitcoin.Crypto.PublicKey
import fr.acinq.eclair.db.PeersDb
import fr.acinq.eclair.db.sqlite.SqliteUtils.{getVersion, using}
import fr.acinq.eclair.wire.LightningMessageCodecs.socketaddress
import fr.acinq.eclair.wire.{IPv4, IPv6, LightningMessageCodecs, NodeAddress}
import scodec.bits.BitVector
class SqlitePeersDb(sqlite: Connection) extends PeersDb {
@ -37,7 +37,8 @@ class SqlitePeersDb(sqlite: Connection) extends PeersDb {
}
override def addOrUpdatePeer(nodeId: Crypto.PublicKey, address: InetSocketAddress): Unit = {
val data = socketaddress.encode(address).require.toByteArray
val nodeaddress = NodeAddress(address)
val data = LightningMessageCodecs.nodeaddress.encode(nodeaddress).require.toByteArray
using(sqlite.prepareStatement("UPDATE peers SET data=? WHERE node_id=?")) { update =>
update.setBytes(1, data)
update.setBytes(2, nodeId.toBin)
@ -63,7 +64,13 @@ class SqlitePeersDb(sqlite: Connection) extends PeersDb {
val rs = statement.executeQuery("SELECT node_id, data FROM peers")
var m: Map[PublicKey, InetSocketAddress] = Map()
while (rs.next()) {
m += (PublicKey(rs.getBytes("node_id")) -> socketaddress.decode(BitVector(rs.getBytes("data"))).require.value)
val nodeid = PublicKey(rs.getBytes("node_id"))
val nodeaddress = LightningMessageCodecs.nodeaddress.decode(BitVector(rs.getBytes("data"))).require.value match {
case IPv4(ipv4, port) => new InetSocketAddress(ipv4, port)
case IPv6(ipv6, port) => new InetSocketAddress(ipv6, port)
case _ => ???
}
m += (nodeid -> nodeaddress)
}
m
}

View file

@ -46,7 +46,7 @@ class Authenticator(nodeParams: NodeParams) extends Actor with ActorLogging {
KeyPair(nodeParams.nodeId.toBin, nodeParams.privateKey.toBin),
remoteNodeId_opt.map(_.toBin),
connection = connection,
codec = LightningMessageCodecs.lightningMessageCodec))
codec = LightningMessageCodecs.cachedLightningMessageCodec))
context watch transport
context become (ready(switchboard, authenticating + (transport -> pending)))

View file

@ -53,15 +53,35 @@ package object eclair {
case Attempt.Failure(cause) => throw new RuntimeException(s"serialization error: $cause")
}
def feerateKbToByte(feeratePerKb: Long): Long = Math.max(feeratePerKb / 1024, 1)
/**
* Converts feerate in satoshi-per-bytes to feerate in satoshi-per-kw
*
* @param feeratePerByte feerate in satoshi-per-bytes
* @param feeratePerByte fee rate in satoshi-per-bytes
* @return feerate in satoshi-per-kw
*/
def feerateByte2Kw(feeratePerByte: Long): Long = feeratePerByte * 1024 / 4
def feerateByte2Kw(feeratePerByte: Long): Long = feerateKB2Kw(feeratePerByte * 1000)
/**
*
* @param feeratesPerKw fee rate in satoshiper-kw
* @return fee rate in satoshi-per-byte
*/
def feerateKw2Byte(feeratesPerKw: Long): Long = feeratesPerKw / 250
/**
* Converts feerate in satoshi-per-kilobytes to feerate in satoshi-per-kw
*
* @param feeratePerKB fee rate in satoshi-per-kilobytes
* @return feerate in satoshi-per-kw
*/
def feerateKB2Kw(feeratePerKB: Long): Long = feeratePerKB / 4
/**
*
* @param feeratesPerKw fee rate in satoshi-per-kw
* @return fee rate in satoshi-per-kilobyte
*/
def feerateKw2KB(feeratesPerKw: Long): Long = feeratesPerKw * 4
def isPay2PubkeyHash(address: String): Boolean = address.startsWith("1") || address.startsWith("m") || address.startsWith("n")
@ -84,7 +104,7 @@ package object eclair {
/**
*
* @param address base58 of bech32 address
* @param address base58 of bech32 address
* @param chainHash hash of the chain we're on, which will be checked against the input address
* @return the public key script that matches the input address.
*/

View file

@ -140,6 +140,7 @@ object PaymentRequest {
tags = List(
Some(PaymentHashTag(paymentHash)),
Some(DescriptionTag(description)),
fallbackAddress.map(FallbackAddressTag(_)),
expirySeconds.map(ExpiryTag)
).flatten ++ extraHops.map(RoutingInfoTag(_)),
signature = BinaryData.empty)

View file

@ -36,7 +36,7 @@ object Announcements {
def channelAnnouncementWitnessEncode(chainHash: BinaryData, shortChannelId: ShortChannelId, nodeId1: PublicKey, nodeId2: PublicKey, bitcoinKey1: PublicKey, bitcoinKey2: PublicKey, features: BinaryData): BinaryData =
sha256(sha256(serializationResult(LightningMessageCodecs.channelAnnouncementWitnessCodec.encode(features :: chainHash :: shortChannelId :: nodeId1 :: nodeId2 :: bitcoinKey1 :: bitcoinKey2 :: HNil))))
def nodeAnnouncementWitnessEncode(timestamp: Long, nodeId: PublicKey, rgbColor: Color, alias: String, features: BinaryData, addresses: List[InetSocketAddress]): BinaryData =
def nodeAnnouncementWitnessEncode(timestamp: Long, nodeId: PublicKey, rgbColor: Color, alias: String, features: BinaryData, addresses: List[NodeAddress]): BinaryData =
sha256(sha256(serializationResult(LightningMessageCodecs.nodeAnnouncementWitnessCodec.encode(features :: timestamp :: nodeId :: (rgbColor) :: alias :: addresses :: HNil))))
def channelUpdateWitnessEncode(chainHash: BinaryData, shortChannelId: ShortChannelId, timestamp: Long, flags: BinaryData, cltvExpiryDelta: Int, htlcMinimumMsat: Long, feeBaseMsat: Long, feeProportionalMillionths: Long): BinaryData =
@ -77,7 +77,8 @@ object Announcements {
def makeNodeAnnouncement(nodeSecret: PrivateKey, alias: String, color: Color, addresses: List[InetSocketAddress], timestamp: Long = Platform.currentTime / 1000): NodeAnnouncement = {
require(alias.size <= 32)
val witness = nodeAnnouncementWitnessEncode(timestamp, nodeSecret.publicKey, color, alias, "", addresses)
val nodeAddresses = addresses.map(NodeAddress(_))
val witness = nodeAnnouncementWitnessEncode(timestamp, nodeSecret.publicKey, color, alias, "", nodeAddresses)
val sig = Crypto.encodeSignature(Crypto.sign(witness, nodeSecret)) :+ 1.toByte
NodeAnnouncement(
signature = sig,
@ -86,7 +87,7 @@ object Announcements {
rgbColor = color,
alias = alias,
features = "",
addresses = addresses
addresses = nodeAddresses
)
}

View file

@ -16,9 +16,9 @@
package fr.acinq.eclair.router
import java.io.{ByteArrayInputStream, ByteArrayOutputStream}
import java.io.{ByteArrayInputStream, ByteArrayOutputStream, InputStream}
import java.nio.ByteOrder
import java.util.zip.{GZIPInputStream, GZIPOutputStream}
import java.util.zip.{DeflaterOutputStream, GZIPInputStream, GZIPOutputStream, InflaterInputStream}
import fr.acinq.bitcoin.{BinaryData, Protocol}
import fr.acinq.eclair.ShortChannelId
@ -28,22 +28,40 @@ import scala.annotation.tailrec
object ChannelRangeQueries {
val UNCOMPRESSED_FORMAT = 0.toByte
val GZIP_FORMAT = 1.toByte
val ZLIB_FORMAT = 1.toByte
case class ShortChannelIdsBlock(val firstBlock: Int, val numBlocks: Int, shortChannelIds: BinaryData)
/**
* Compressed a sequence of *sorted* short channel id.
*
* @param shortChannelIds must be sorted beforehand
* @return
* @return a sequence of encoded short channel ids
*/
def encodeShortChannelIds(format: Byte, shortChannelIds: Iterable[ShortChannelId]): BinaryData = {
def encodeShortChannelIds(firstBlockIn: Int, numBlocksIn: Int, shortChannelIds: Iterable[ShortChannelId], format: Byte, useGzip: Boolean = false): List[ShortChannelIdsBlock] = {
// LN messages must fit in 65 Kb so we split ids into groups to make sure that the output message will be valid
val count = format match {
case UNCOMPRESSED_FORMAT => 7000
case ZLIB_FORMAT => 12000 // TODO: do something less simplistic...
}
shortChannelIds.grouped(count).map(ids => {
val (firstBlock, numBlocks) = if (ids.isEmpty) (firstBlockIn, numBlocksIn) else {
val firstBlock = ShortChannelId.coordinates(ids.head).blockHeight
val numBlocks = ShortChannelId.coordinates(ids.last).blockHeight - firstBlock + 1
(firstBlock, numBlocks)
}
val encoded = encodeShortChannelIdsSingle(ids, format, useGzip)
ShortChannelIdsBlock(firstBlock, numBlocks, encoded)
}).toList
}
def encodeShortChannelIdsSingle(shortChannelIds: Iterable[ShortChannelId], format: Byte, useGzip: Boolean): BinaryData = {
val bos = new ByteArrayOutputStream()
bos.write(format)
format match {
case UNCOMPRESSED_FORMAT =>
shortChannelIds.foreach(id => Protocol.writeUInt64(id.toLong, bos, ByteOrder.BIG_ENDIAN))
case GZIP_FORMAT =>
val output = new GZIPOutputStream(bos)
case ZLIB_FORMAT =>
val output = if (useGzip) new GZIPOutputStream(bos) else new DeflaterOutputStream(bos)
shortChannelIds.foreach(id => Protocol.writeUInt64(id.toLong, output, ByteOrder.BIG_ENDIAN))
output.finish()
}
@ -51,43 +69,53 @@ object ChannelRangeQueries {
}
/**
* Uncompresses a zipped sequence of sorted short channel ids.
* Decompress a zipped sequence of sorted short channel ids.
*
* Does *not* preserve the order, we dontt need it and a Set is better suited to our access patterns
* Does *not* preserve the order, we don't need it and a Set is better suited to our access patterns
*
* @param data
* @return
*/
def decodeShortChannelIds(data: BinaryData): (Byte, Set[ShortChannelId]) = {
def decodeShortChannelIds(data: BinaryData): (Byte, Set[ShortChannelId], Boolean) = {
val format = data.head
val bis = new ByteArrayInputStream(data.tail.toArray)
val input = format match {
case UNCOMPRESSED_FORMAT => bis
case GZIP_FORMAT => new GZIPInputStream(bis)
}
val buffer = new Array[Byte](8)
// read 8 bytes from input
// zipped input stream often returns less bytes than what you want to read
@tailrec
def read8(offset: Int = 0): Int = input.read(buffer, offset, 8 - offset) match {
def read8(input: InputStream, offset: Int = 0): Int = input.read(buffer, offset, 8 - offset) match {
case len if len <= 0 => len
case 8 => 8
case len if offset + len == 8 => 8
case len => read8(offset + len)
case len => read8(input, offset + len)
}
// read until there's nothing left
@tailrec
def loop(acc: Set[ShortChannelId]): Set[ShortChannelId] = {
if (read8() <= 0) acc else loop(acc + ShortChannelId(Protocol.uint64(buffer, ByteOrder.BIG_ENDIAN)))
def loop(input: InputStream, acc: Set[ShortChannelId]): Set[ShortChannelId] = {
if (read8(input) <= 0) acc else loop(input, acc + ShortChannelId(Protocol.uint64(buffer, ByteOrder.BIG_ENDIAN)))
}
def readAll(useGzip: Boolean) = {
val bis = new ByteArrayInputStream(data.tail.toArray)
val input = format match {
case UNCOMPRESSED_FORMAT => bis
case ZLIB_FORMAT if useGzip => new GZIPInputStream(bis)
case ZLIB_FORMAT => new InflaterInputStream(bis)
}
try {
(format, loop(input, Set.empty[ShortChannelId]), useGzip)
}
finally {
input.close()
}
}
try {
(format, loop(Set.empty[ShortChannelId]))
readAll(useGzip = false)
}
finally {
input.close()
catch {
case t: Throwable if format == ZLIB_FORMAT => readAll(useGzip = true)
}
}
}

View file

@ -322,11 +322,12 @@ class Router(nodeParams: NodeParams, watcher: ActorRef) extends FSM[State, Data]
if (chainHash != nodeParams.chainHash) {
log.warning("received reply_channel_range message for chain {}, we're on {}", chainHash, nodeParams.chainHash)
} else {
val (_, theirShortChannelIds) = ChannelRangeQueries.decodeShortChannelIds(data)
val (_, theirShortChannelIds, useGzip) = ChannelRangeQueries.decodeShortChannelIds(data)
val ourShortChannelIds = d.channels.keys.filter(keep(firstBlockNum, numberOfBlocks, _, d.channels, d.updates)) // note: order is preserved
val missing = theirShortChannelIds -- ourShortChannelIds
log.info("we received their reply, we're missing {} channel announcements/updates", missing.size)
sender ! QueryShortChannelIds(chainHash, ChannelRangeQueries.encodeShortChannelIds(ChannelRangeQueries.GZIP_FORMAT, missing))
log.info("we received their reply, we're missing {} channel announcements/updates, useGzip={}", missing.size, useGzip)
val blocks = ChannelRangeQueries.encodeShortChannelIds(firstBlockNum, numberOfBlocks, missing, ChannelRangeQueries.ZLIB_FORMAT, useGzip)
blocks.foreach(block => sender ! QueryShortChannelIds(chainHash, block.shortChannelIds))
}
stay
@ -340,7 +341,7 @@ class Router(nodeParams: NodeParams, watcher: ActorRef) extends FSM[State, Data]
if (chainHash != nodeParams.chainHash) {
log.warning("received reply_short_channel_ids_end message for chain {}, we're on {}", chainHash, nodeParams.chainHash)
} else {
// TODO: how do we use this message ?
log.debug("we've received {}", end)
}
stay
}

View file

@ -19,6 +19,7 @@ package fr.acinq.eclair.wire
import java.math.BigInteger
import java.net.{Inet4Address, Inet6Address, InetAddress, InetSocketAddress}
import com.google.common.cache.{CacheBuilder, CacheLoader}
import fr.acinq.bitcoin.Crypto.{Point, PrivateKey, PublicKey, Scalar}
import fr.acinq.bitcoin.{BinaryData, Crypto}
import fr.acinq.eclair.crypto.{Generators, Sphinx}
@ -26,7 +27,9 @@ import fr.acinq.eclair.wire.FixedSizeStrictCodec.bytesStrict
import fr.acinq.eclair.{ShortChannelId, UInt64, wire}
import scodec.bits.{BitVector, ByteVector}
import scodec.codecs._
import scodec.{Attempt, Codec, Err}
import scodec.{Attempt, Codec, DecodeResult, Err, SizeBound}
import scala.util.{Failure, Success, Try}
/**
@ -34,6 +37,11 @@ import scodec.{Attempt, Codec, Err}
*/
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)
@ -48,18 +56,20 @@ object LightningMessageCodecs {
def ipv4address: Codec[Inet4Address] = bytes(4).xmap(b => InetAddress.getByAddress(b.toArray).asInstanceOf[Inet4Address], a => ByteVector(a.getAddress))
def ipv6address: Codec[Inet6Address] = bytes(16).xmap(b => InetAddress.getByAddress(b.toArray).asInstanceOf[Inet6Address], a => ByteVector(a.getAddress))
def ipv6address: Codec[Inet6Address] = bytes(16).exmap(b => attemptFromTry(Inet6Address.getByAddress(null, b.toArray, null)), a => attemptFromTry(ByteVector(a.getAddress)))
def socketaddress: Codec[InetSocketAddress] =
(discriminated[InetAddress].by(uint8)
.typecase(1, ipv4address)
.typecase(2, ipv6address) ~ uint16)
.xmap(x => new InetSocketAddress(x._1, x._2), x => (x.getAddress, x.getPort))
def nodeaddress: Codec[NodeAddress] =
discriminated[NodeAddress].by(uint8)
.typecase(0, provide(Padding))
.typecase(1, (ipv4address ~ uint16).xmap[IPv4](x => new IPv4(x._1, x._2), x => (x.ipv4, x.port)))
.typecase(2, (ipv6address ~ uint16).xmap[IPv6](x => new IPv6(x._1, x._2), x => (x.ipv6, x.port)))
.typecase(3, (binarydata(10) ~ uint16).xmap[Tor2](x => new Tor2(x._1, x._2), x => (x.tor2, x.port)))
.typecase(4, (binarydata(35) ~ uint16).xmap[Tor3](x => new Tor3(x._1, x._2), x => (x.tor3, x.port)))
// this one is a bit different from most other codecs: the first 'len' element is * not * the number of items
// in the list but rather the number of bytes of the encoded list. The rationale is once we've read this
// number of bytes we can just skip to the next field
def listofsocketaddresses: Codec[List[InetSocketAddress]] = variableSizeBytes(uint16, list(socketaddress))
def listofnodeaddresses: Codec[List[NodeAddress]] = variableSizeBytes(uint16, list(nodeaddress))
def shortchannelid: Codec[ShortChannelId] = int64.xmap(l => ShortChannelId(l), s => s.toLong)
@ -264,7 +274,7 @@ object LightningMessageCodecs {
("nodeId" | publicKey) ::
("rgbColor" | rgb) ::
("alias" | zeropaddedstring(32)) ::
("addresses" | listofsocketaddresses))
("addresses" | listofnodeaddresses))
val nodeAnnouncementCodec: Codec[NodeAnnouncement] = (
("signature" | signature) ::
@ -344,6 +354,31 @@ object LightningMessageCodecs {
.typecase(264, replyChannelRangeCodec)
.typecase(265, gossipTimeRangeCodec)
/**
* 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) ::

View file

@ -16,7 +16,7 @@
package fr.acinq.eclair.wire
import java.net.InetSocketAddress
import java.net.{Inet4Address, Inet6Address, InetSocketAddress}
import fr.acinq.bitcoin.BinaryData
import fr.acinq.bitcoin.Crypto.{Point, PublicKey, Scalar}
@ -158,14 +158,34 @@ case class Color(r: Byte, g: Byte, b: Byte) {
override def toString: String = f"#$r%02x$g%02x$b%02x" // to hexa s"# ${r}%02x ${r & 0xFF}${g & 0xFF}${b & 0xFF}"
}
// @formatter:off
sealed trait NodeAddress
case object NodeAddress {
def apply(inetSocketAddress: InetSocketAddress): NodeAddress = inetSocketAddress.getAddress match {
case a: Inet4Address => IPv4(a, inetSocketAddress.getPort)
case a: Inet6Address => IPv6(a, inetSocketAddress.getPort)
case _ => ??? // there are no other implementations of InetAddress
}
}
case object Padding extends NodeAddress
case class IPv4(ipv4: Inet4Address, port: Int) extends NodeAddress
case class IPv6(ipv6: Inet6Address, port: Int) extends NodeAddress
case class Tor2(tor2: BinaryData, port: Int) extends NodeAddress { require(tor2.size == 10) }
case class Tor3(tor3: BinaryData, port: Int) extends NodeAddress { require(tor3.size == 35) }
// @formatter:on
case class NodeAnnouncement(signature: BinaryData,
features: BinaryData,
timestamp: Long,
nodeId: PublicKey,
rgbColor: Color,
alias: String,
// TODO: check address order + support padding data (type 0)
addresses: List[InetSocketAddress]) extends RoutingMessage with HasTimestamp
addresses: List[NodeAddress]) extends RoutingMessage with HasTimestamp {
def socketAddresses: List[InetSocketAddress] = addresses.collect {
case IPv4(a, port) => new InetSocketAddress(a, port)
case IPv6(a, port) => new InetSocketAddress(a, port)
}
}
case class ChannelUpdate(signature: BinaryData,
chainHash: BinaryData,

View file

@ -8,4 +8,10 @@ akka {
fsm = on
}
}
test {
# factor by which to scale timeouts during tests, e.g. to account for shared
# build system load
timefactor = 3.0
}
}

View file

@ -80,7 +80,8 @@ object TestConstants {
watcherType = BITCOIND,
paymentRequestExpiry = 1 hour,
maxPendingPaymentRequests = 10000000,
maxPaymentFee = 0.03)
maxPaymentFee = 0.03,
minFundingSatoshis = 1000L)
def channelParams = Peer.makeChannelParams(
nodeParams = nodeParams,
@ -133,7 +134,8 @@ object TestConstants {
watcherType = BITCOIND,
paymentRequestExpiry = 1 hour,
maxPendingPaymentRequests = 10000000,
maxPaymentFee = 0.03)
maxPaymentFee = 0.03,
minFundingSatoshis = 1000L)
def channelParams = Peer.makeChannelParams(
nodeParams = nodeParams,

View file

@ -20,13 +20,14 @@ import java.io.File
import java.nio.file.Files
import java.util.UUID
import akka.actor.Status.Failure
import akka.actor.{Actor, ActorRef, ActorSystem, Props}
import akka.pattern.pipe
import akka.testkit.{TestKit, TestProbe}
import com.typesafe.config.ConfigFactory
import fr.acinq.bitcoin.{Block, MilliBtc, Satoshi, Script}
import fr.acinq.eclair.blockchain._
import fr.acinq.eclair.blockchain.bitcoind.rpc.BasicBitcoinJsonRPCClient
import fr.acinq.eclair.blockchain.bitcoind.rpc.{BasicBitcoinJsonRPCClient, JsonRPCError}
import fr.acinq.eclair.transactions.Scripts
import fr.acinq.eclair.{addressToPublicKeyScript, randomKey}
import grizzled.slf4j.Logging
@ -39,7 +40,8 @@ import org.scalatest.{BeforeAndAfterAll, FunSuiteLike}
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration._
import scala.sys.process.{Process, _}
import scala.util.Try
import scala.util.{Random, Try}
import collection.JavaConversions._
@RunWith(classOf[JUnitRunner])
class BitcoinCoreWalletSpec extends TestKit(ActorSystem("test")) with FunSuiteLike with BeforeAndAfterAll with Logging {
@ -54,6 +56,8 @@ class BitcoinCoreWalletSpec extends TestKit(ActorSystem("test")) with FunSuiteLi
var bitcoinrpcclient: BasicBitcoinJsonRPCClient = null
var bitcoincli: ActorRef = null
val walletPassword = Random.alphanumeric.take(8).mkString
implicit val formats = DefaultFormats
case class BitcoinReq(method: String, params: Any*)
@ -62,40 +66,18 @@ class BitcoinCoreWalletSpec extends TestKit(ActorSystem("test")) with FunSuiteLi
Files.createDirectories(PATH_BITCOIND_DATADIR.toPath)
Files.copy(classOf[BitcoinCoreWalletSpec].getResourceAsStream("/integration/bitcoin.conf"), new File(PATH_BITCOIND_DATADIR.toString, "bitcoin.conf").toPath)
bitcoind = s"$PATH_BITCOIND -datadir=$PATH_BITCOIND_DATADIR".run()
bitcoinrpcclient = new BasicBitcoinJsonRPCClient(user = "foo", password = "bar", host = "localhost", port = 28332)
bitcoincli = system.actorOf(Props(new Actor {
override def receive: Receive = {
case BitcoinReq(method) => bitcoinrpcclient.invoke(method) pipeTo sender
case BitcoinReq(method, params) => bitcoinrpcclient.invoke(method, params) pipeTo sender
case BitcoinReq(method, param1, param2) => bitcoinrpcclient.invoke(method, param1, param2) pipeTo sender
}
}))
startBitcoind()
}
override def afterAll(): Unit = {
// gracefully stopping bitcoin will make it store its state cleanly to disk, which is good for later debugging
logger.info(s"stopping bitcoind")
val sender = TestProbe()
sender.send(bitcoincli, BitcoinReq("stop"))
sender.expectMsgType[JValue]
bitcoind.exitValue()
stopBitcoind()
}
test("wait bitcoind ready") {
val sender = TestProbe()
logger.info(s"waiting for bitcoind to initialize...")
awaitCond({
sender.send(bitcoincli, BitcoinReq("getnetworkinfo"))
sender.receiveOne(5 second).isInstanceOf[JValue]
}, max = 30 seconds, interval = 500 millis)
logger.info(s"generating initial blocks...")
sender.send(bitcoincli, BitcoinReq("generate", 500))
sender.expectMsgType[JValue](30 seconds)
waitForBitcoindReady()
}
test("create/commit/rollback funding txes") {
import collection.JavaConversions._
val commonConfig = ConfigFactory.parseMap(Map("eclair.chain" -> "regtest", "eclair.spv" -> false, "eclair.server.public-ips.1" -> "localhost", "eclair.bitcoind.port" -> 28333, "eclair.bitcoind.rpcport" -> 28332, "eclair.bitcoind.zmq" -> "tcp://127.0.0.1:28334", "eclair.router-broadcast-interval" -> "2 second", "eclair.auto-reconnect" -> false))
val config = ConfigFactory.load(commonConfig).getConfig("eclair")
val bitcoinClient = new BasicBitcoinJsonRPCClient(
@ -148,4 +130,88 @@ class BitcoinCoreWalletSpec extends TestKit(ActorSystem("test")) with FunSuiteLi
}
test("encrypt wallet") {
val sender = TestProbe()
sender.send(bitcoincli, BitcoinReq("encryptwallet", walletPassword))
stopBitcoind()
startBitcoind()
waitForBitcoindReady()
}
test("unlock failed funding txes") {
val commonConfig = ConfigFactory.parseMap(Map("eclair.chain" -> "regtest", "eclair.spv" -> false, "eclair.server.public-ips.1" -> "localhost", "eclair.bitcoind.port" -> 28333, "eclair.bitcoind.rpcport" -> 28332, "eclair.bitcoind.zmq" -> "tcp://127.0.0.1:28334", "eclair.router-broadcast-interval" -> "2 second", "eclair.auto-reconnect" -> false))
val config = ConfigFactory.load(commonConfig).getConfig("eclair")
val bitcoinClient = new BasicBitcoinJsonRPCClient(
user = config.getString("bitcoind.rpcuser"),
password = config.getString("bitcoind.rpcpassword"),
host = config.getString("bitcoind.host"),
port = config.getInt("bitcoind.rpcport"))
val wallet = new BitcoinCoreWallet(bitcoinClient)
val sender = TestProbe()
wallet.getBalance.pipeTo(sender.ref)
assert(sender.expectMsgType[Satoshi] > Satoshi(0))
wallet.getFinalAddress.pipeTo(sender.ref)
val address = sender.expectMsgType[String]
assert(Try(addressToPublicKeyScript(address, Block.RegtestGenesisBlock.hash)).isSuccess)
sender.send(bitcoincli, BitcoinReq("listlockunspent"))
assert(sender.expectMsgType[JValue](10 seconds).children.size === 0)
val pubkeyScript = Script.write(Script.pay2wsh(Scripts.multiSig2of2(randomKey.publicKey, randomKey.publicKey)))
wallet.makeFundingTx(pubkeyScript, MilliBtc(50), 10000).pipeTo(sender.ref)
assert(sender.expectMsgType[Failure].cause.asInstanceOf[JsonRPCError].error.message.contains("Please enter the wallet passphrase with walletpassphrase first."))
sender.send(bitcoincli, BitcoinReq("listlockunspent"))
assert(sender.expectMsgType[JValue](10 seconds).children.size === 0)
sender.send(bitcoincli, BitcoinReq("walletpassphrase", walletPassword, 10))
sender.expectMsgType[JValue]
wallet.makeFundingTx(pubkeyScript, MilliBtc(50), 10000).pipeTo(sender.ref)
val MakeFundingTxResponse(fundingTx, _) = sender.expectMsgType[MakeFundingTxResponse]
wallet.commit(fundingTx).pipeTo(sender.ref)
assert(sender.expectMsgType[Boolean])
wallet.getBalance.pipeTo(sender.ref)
assert(sender.expectMsgType[Satoshi] > Satoshi(0))
}
private def startBitcoind(): Unit = {
bitcoind = s"$PATH_BITCOIND -datadir=$PATH_BITCOIND_DATADIR".run()
bitcoinrpcclient = new BasicBitcoinJsonRPCClient(user = "foo", password = "bar", host = "localhost", port = 28332)
bitcoincli = system.actorOf(Props(new Actor {
override def receive: Receive = {
case BitcoinReq(method) => bitcoinrpcclient.invoke(method) pipeTo sender
case BitcoinReq(method, params) => bitcoinrpcclient.invoke(method, params) pipeTo sender
case BitcoinReq(method, param1, param2) => bitcoinrpcclient.invoke(method, param1, param2) pipeTo sender
}
}))
}
private def stopBitcoind(): Int = {
// gracefully stopping bitcoin will make it store its state cleanly to disk, which is good for later debugging
logger.info(s"stopping bitcoind")
val sender = TestProbe()
sender.send(bitcoincli, BitcoinReq("stop"))
sender.expectMsgType[JValue]
bitcoind.exitValue()
}
private def waitForBitcoindReady(): Unit = {
val sender = TestProbe()
logger.info(s"waiting for bitcoind to initialize...")
awaitCond({
sender.send(bitcoincli, BitcoinReq("getnetworkinfo"))
sender.receiveOne(5 second).isInstanceOf[JValue]
}, max = 30 seconds, interval = 500 millis)
logger.info(s"generating initial blocks...")
sender.send(bitcoincli, BitcoinReq("generate", 500))
sender.expectMsgType[JValue](30 seconds)
}
}

View file

@ -20,7 +20,7 @@ import akka.actor.{Actor, ActorSystem, Props}
import akka.testkit.{TestFSMRef, TestKit, TestProbe}
import fr.acinq.bitcoin.{BinaryData, Block, MnemonicCode, Satoshi}
import fr.acinq.eclair.blockchain.electrum.ElectrumClient.{ScriptHashSubscription, ScriptHashSubscriptionResponse}
import fr.acinq.eclair.blockchain.electrum.ElectrumWallet.{NewWalletReceiveAddress, WalletEvent, WalletParameters, WalletReady}
import fr.acinq.eclair.blockchain.electrum.ElectrumWallet._
import org.junit.runner.RunWith
import org.scalatest.FunSuiteLike
import org.scalatest.junit.JUnitRunner
@ -63,12 +63,17 @@ class ElectrumWalletSimulatedClientSpec extends TestKit(ActorSystem("test")) wit
awaitCond(wallet.stateName == ElectrumWallet.RUNNING)
assert(listener.expectMsgType[WalletReady].timestamp == header1.timestamp)
listener.expectMsgType[NewWalletReceiveAddress]
listener.send(wallet, GetXpub)
val GetXpubResponse(xpub, path) = listener.expectMsgType[GetXpubResponse]
assert(xpub == "tpubDCY62b4okoTERzMurvrtoCMgkswfLufejmhwfShqAKDBN2PPNUWpwx72cvyt4R8enGstorHvXNGS8StbkAsPb7XSbYFER8Wo6zPf1Z6m9w4")
assert(path == "m/49'/1'/0'")
}
test("tell wallet is ready when a new block comes in, even if nothing else has changed") {
sender.send(wallet, ElectrumClient.HeaderSubscriptionResponse(header2))
assert(listener.expectMsgType[WalletReady].timestamp == header2.timestamp)
listener.expectMsgType[NewWalletReceiveAddress]
val NewWalletReceiveAddress(address) = listener.expectMsgType[NewWalletReceiveAddress]
assert(address == "2NDjBqJugL3gCtjWTToDgaWWogq9nYuYw31")
}
test("tell wallet is ready when it is reconnected, even if nothing has changed") {

View file

@ -18,14 +18,11 @@ package fr.acinq.eclair.blockchain.fee
import akka.actor.ActorSystem
import akka.util.Timeout
import fr.acinq.bitcoin.Block
import org.json4s.DefaultFormats
import org.junit.runner.RunWith
import org.scalatest.FunSuite
import org.scalatest.junit.JUnitRunner
import scala.concurrent.Await
/**
* Created by PM on 27/01/2017.
*/
@ -52,30 +49,27 @@ class BitgoFeeProviderSpec extends FunSuite {
val json = parse(sample_response)
val feeRanges = parseFeeRanges(json)
val fee = extractFeerate(feeRanges, 6)
assert(fee === 103)
assert(fee === 105566)
}
test("extract all fees") {
val json = parse(sample_response)
val feeRanges = parseFeeRanges(json)
val feerates = extractFeerates(feeRanges)
val ref = FeeratesPerByte(
block_1 = 145,
blocks_2 = 133,
blocks_6 = 103,
blocks_12 = 93,
blocks_36 = 69,
blocks_72 = 66)
val ref = FeeratesPerKB(
block_1 = 149453,
blocks_2 = 136797,
blocks_6 = 105566,
blocks_12 = 96254,
blocks_36 = 71098,
blocks_72 = 68182)
assert(feerates === ref)
}
test("make sure API hasn't changed") {
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration._
implicit val system = ActorSystem()
implicit val timeout = Timeout(30 seconds)
Await.result(new BitgoFeeProvider(Block.LivenetGenesisBlock.hash).getFeerates, 10 seconds)
Await.result(new BitgoFeeProvider(Block.TestnetGenesisBlock.hash).getFeerates, 10 seconds)
}
}

View file

@ -51,20 +51,20 @@ class EarnDotComFeeProviderSpec extends FunSuite {
val json = parse(sample_response)
val feeRanges = parseFeeRanges(json)
val fee = extractFeerate(feeRanges, 6)
assert(fee === 230)
assert(fee === 230 * 1000)
}
test("extract all fees") {
val json = parse(sample_response)
val feeRanges = parseFeeRanges(json)
val feerates = extractFeerates(feeRanges)
val ref = FeeratesPerByte(
block_1 = 400,
blocks_2 = 350,
blocks_6 = 230,
blocks_12 = 140,
blocks_36 = 60,
blocks_72 = 40)
val ref = FeeratesPerKB(
block_1 = 400 * 1000,
blocks_2 = 350 * 1000,
blocks_6 = 230 * 1000,
blocks_12 = 140 * 1000,
blocks_36 = 60 * 1000,
blocks_72 = 40 * 1000)
assert(feerates === ref)
}
@ -74,7 +74,7 @@ class EarnDotComFeeProviderSpec extends FunSuite {
implicit val system = ActorSystem()
implicit val timeout = Timeout(30 seconds)
val provider = new EarnDotComFeeProvider()
Await.result(provider.getFeerates, 10 seconds)
println("earn.com livenet fees: " + Await.result(provider.getFeerates, 10 seconds))
}
}

View file

@ -33,19 +33,19 @@ class FallbackFeeProviderSpec extends FunSuite {
* This provider returns a constant value, but fails after ttl tries
*
* @param ttl
* @param feeratesPerByte
* @param feeratesPerKB
*/
class FailingFeeProvider(ttl: Int, val feeratesPerByte: FeeratesPerByte) extends FeeProvider {
class FailingFeeProvider(ttl: Int, val feeratesPerKB: FeeratesPerKB) extends FeeProvider {
var i = 0
override def getFeerates: Future[FeeratesPerByte] =
override def getFeerates: Future[FeeratesPerKB] =
if (i < ttl) {
i = i + 1
Future.successful(feeratesPerByte)
Future.successful(feeratesPerKB)
} else Future.failed(new RuntimeException())
}
def dummyFeerates = FeeratesPerByte(Random.nextInt(10000), Random.nextInt(10000), Random.nextInt(10000), Random.nextInt(10000), Random.nextInt(10000), Random.nextInt(10000))
def dummyFeerates = FeeratesPerKB(1000 + Random.nextInt(10000), 1000 + Random.nextInt(10000), 1000 + Random.nextInt(10000), 1000 + Random.nextInt(10000), 1000 + Random.nextInt(10000), 1000 + Random.nextInt(10000))
def await[T](f: Future[T]): T = Await.result(f, 3 seconds)
@ -58,26 +58,26 @@ class FallbackFeeProviderSpec extends FunSuite {
val fallbackFeeProvider = new FallbackFeeProvider(provider0 :: provider1 :: provider3 :: provider5 :: provider7 :: Nil, 1)
assert(await(fallbackFeeProvider.getFeerates) === provider1.feeratesPerByte)
assert(await(fallbackFeeProvider.getFeerates) === provider1.feeratesPerKB)
assert(await(fallbackFeeProvider.getFeerates) === provider3.feeratesPerByte)
assert(await(fallbackFeeProvider.getFeerates) === provider3.feeratesPerByte)
assert(await(fallbackFeeProvider.getFeerates) === provider3.feeratesPerByte)
assert(await(fallbackFeeProvider.getFeerates) === provider3.feeratesPerKB)
assert(await(fallbackFeeProvider.getFeerates) === provider3.feeratesPerKB)
assert(await(fallbackFeeProvider.getFeerates) === provider3.feeratesPerKB)
assert(await(fallbackFeeProvider.getFeerates) === provider5.feeratesPerByte)
assert(await(fallbackFeeProvider.getFeerates) === provider5.feeratesPerByte)
assert(await(fallbackFeeProvider.getFeerates) === provider5.feeratesPerByte)
assert(await(fallbackFeeProvider.getFeerates) === provider5.feeratesPerByte)
assert(await(fallbackFeeProvider.getFeerates) === provider5.feeratesPerByte)
assert(await(fallbackFeeProvider.getFeerates) === provider5.feeratesPerKB)
assert(await(fallbackFeeProvider.getFeerates) === provider5.feeratesPerKB)
assert(await(fallbackFeeProvider.getFeerates) === provider5.feeratesPerKB)
assert(await(fallbackFeeProvider.getFeerates) === provider5.feeratesPerKB)
assert(await(fallbackFeeProvider.getFeerates) === provider5.feeratesPerKB)
assert(await(fallbackFeeProvider.getFeerates) === provider7.feeratesPerByte)
assert(await(fallbackFeeProvider.getFeerates) === provider7.feeratesPerKB)
}
test("ensure minimum feerate") {
val constantFeeProvider = new ConstantFeeProvider(FeeratesPerByte(1, 1, 1, 1, 1, 1))
val constantFeeProvider = new ConstantFeeProvider(FeeratesPerKB(1000, 1000, 1000, 1000, 1000, 1000))
val fallbackFeeProvider = new FallbackFeeProvider(constantFeeProvider :: Nil, 2)
assert(await(fallbackFeeProvider.getFeerates) === FeeratesPerByte(2, 2, 2, 2, 2, 2))
assert(await(fallbackFeeProvider.getFeerates) === FeeratesPerKB(2000, 2000, 2000, 2000, 2000, 2000))
}

View file

@ -75,7 +75,7 @@ class WaitForOpenChannelStateSpec extends TestkitBaseClass with StateTestsHelper
val lowFundingMsat = 100
bob ! open.copy(fundingSatoshis = lowFundingMsat)
val error = bob2alice.expectMsgType[Error]
assert(error === Error(open.temporaryChannelId, new InvalidFundingAmount(open.temporaryChannelId, lowFundingMsat, Channel.MIN_FUNDING_SATOSHIS, Channel.MAX_FUNDING_SATOSHIS).getMessage.getBytes("UTF-8")))
assert(error === Error(open.temporaryChannelId, new InvalidFundingAmount(open.temporaryChannelId, lowFundingMsat, Bob.nodeParams.minFundingSatoshis, Channel.MAX_FUNDING_SATOSHIS).getMessage.getBytes("UTF-8")))
awaitCond(bob.stateName == CLOSED)
}
}
@ -86,7 +86,7 @@ class WaitForOpenChannelStateSpec extends TestkitBaseClass with StateTestsHelper
val highFundingMsat = 100000000
bob ! open.copy(fundingSatoshis = highFundingMsat)
val error = bob2alice.expectMsgType[Error]
assert(error === Error(open.temporaryChannelId, new InvalidFundingAmount(open.temporaryChannelId, highFundingMsat, Channel.MIN_FUNDING_SATOSHIS, Channel.MAX_FUNDING_SATOSHIS).getMessage.getBytes("UTF-8")))
assert(error === Error(open.temporaryChannelId, new InvalidFundingAmount(open.temporaryChannelId, highFundingMsat, Bob.nodeParams.minFundingSatoshis, Channel.MAX_FUNDING_SATOSHIS).getMessage.getBytes("UTF-8")))
awaitCond(bob.stateName == CLOSED)
}
}

View file

@ -52,7 +52,7 @@ object ChannelStateSpec {
val keyManager = new LocalKeyManager("01" * 32, Block.RegtestGenesisBlock.hash)
val localParams = LocalParams(
keyManager.nodeId,
channelKeyPath = DeterministicWallet.KeyPath(Seq(42)),
channelKeyPath = DeterministicWallet.KeyPath(Seq(42L)),
dustLimitSatoshis = Satoshi(546).toLong,
maxHtlcValueInFlightMsat = UInt64(50),
channelReserveSatoshis = 10000,

View file

@ -19,14 +19,14 @@ package fr.acinq.eclair.io
import akka.actor.ActorRef
import akka.testkit.TestProbe
import fr.acinq.bitcoin.Block
import fr.acinq.eclair.{ShortChannelId, TestkitBaseClass}
import fr.acinq.eclair.TestkitBaseClass
import fr.acinq.eclair.router.RoutingSyncSpec.makeFakeRoutingInfo
import fr.acinq.eclair.router.{ChannelRangeQueriesSpec, Rebroadcast}
import fr.acinq.eclair.wire.GossipTimeRange
import org.scalatest.Outcome
class PeerSpec extends TestkitBaseClass {
val shortChannelIds = ChannelRangeQueriesSpec.readShortChannelIds().take(100).map(id => ShortChannelId(id))
val shortChannelIds = ChannelRangeQueriesSpec.shortChannelIds.take(100)
val fakeRoutingInfo = shortChannelIds.map(makeFakeRoutingInfo)
val channels = fakeRoutingInfo.map(_._1)
val updates = fakeRoutingInfo.map(_._2) ++ fakeRoutingInfo.map(_._3)

View file

@ -16,46 +16,52 @@
package fr.acinq.eclair.router
import java.nio.ByteOrder
import fr.acinq.bitcoin.{Block, Protocol}
import fr.acinq.bitcoin.Block
import fr.acinq.eclair.ShortChannelId
import fr.acinq.eclair.wire.ReplyChannelRange
import org.junit.runner.RunWith
import org.scalatest.FunSuite
import org.scalatest.junit.JUnitRunner
import scala.annotation.tailrec
@RunWith(classOf[JUnitRunner])
class ChannelRangeQueriesSpec extends FunSuite {
val shortChannelIds = ChannelRangeQueriesSpec.readShortChannelIds().map(id => ShortChannelId(id))
import ChannelRangeQueriesSpec._
test("create `reply_channel_range` messages (uncompressed format)") {
val reply = ReplyChannelRange(Block.RegtestGenesisBlock.blockId, 0, 2000000, 1, ChannelRangeQueries.encodeShortChannelIds(ChannelRangeQueries.UNCOMPRESSED_FORMAT, shortChannelIds))
val (_, decoded) = ChannelRangeQueries.decodeShortChannelIds(reply.data)
val blocks = ChannelRangeQueries.encodeShortChannelIds(400000, 20000, shortChannelIds, ChannelRangeQueries.UNCOMPRESSED_FORMAT)
val replies = blocks.map(block => ReplyChannelRange(Block.RegtestGenesisBlock.blockId, block.firstBlock, block.numBlocks, 1, block.shortChannelIds))
var decoded = Set.empty[ShortChannelId]
replies.foreach(reply => decoded = decoded ++ ChannelRangeQueries.decodeShortChannelIds(reply.data)._2)
assert(decoded == shortChannelIds.toSet)
}
test("create `reply_channel_range` messages (ZLIB format)") {
val blocks = ChannelRangeQueries.encodeShortChannelIds(400000, 20000, shortChannelIds, ChannelRangeQueries.ZLIB_FORMAT, useGzip = false)
val replies = blocks.map(block => ReplyChannelRange(Block.RegtestGenesisBlock.blockId, block.firstBlock, block.numBlocks, 1, block.shortChannelIds))
var decoded = Set.empty[ShortChannelId]
replies.foreach(reply => decoded = decoded ++ {
val (ChannelRangeQueries.ZLIB_FORMAT, ids, false) = ChannelRangeQueries.decodeShortChannelIds(reply.data)
ids
})
assert(decoded == shortChannelIds.toSet)
}
test("create `reply_channel_range` messages (GZIP format)") {
val reply = ReplyChannelRange(Block.RegtestGenesisBlock.blockId, 0, 2000000, 1, ChannelRangeQueries.encodeShortChannelIds(ChannelRangeQueries.GZIP_FORMAT, shortChannelIds))
val (_, decoded) = ChannelRangeQueries.decodeShortChannelIds(reply.data)
val blocks = ChannelRangeQueries.encodeShortChannelIds(400000, 20000, shortChannelIds, ChannelRangeQueries.ZLIB_FORMAT, useGzip = true)
val replies = blocks.map(block => ReplyChannelRange(Block.RegtestGenesisBlock.blockId, block.firstBlock, block.numBlocks, 1, block.shortChannelIds))
var decoded = Set.empty[ShortChannelId]
replies.foreach(reply => decoded = decoded ++ {
val (ChannelRangeQueries.ZLIB_FORMAT, ids, true) = ChannelRangeQueries.decodeShortChannelIds(reply.data)
ids
})
assert(decoded == shortChannelIds.toSet)
}
}
object ChannelRangeQueriesSpec {
def readShortChannelIds() = {
val stream = classOf[ChannelRangeQueriesSpec].getResourceAsStream("/short_channels-mainnet.422")
@tailrec
def loop(acc: Vector[Long] = Vector()): Vector[Long] = if (stream.available() == 0) acc else loop(acc :+ Protocol.uint64(stream, ByteOrder.BIG_ENDIAN))
try {
loop()
}
finally {
stream.close()
}
}
lazy val shortChannelIds = for {
block <- 400000 to 420000
txindex <- 0 to 5
outputIndex <- 0 to 1
} yield ShortChannelId(block, txindex, outputIndex)
}

View file

@ -20,12 +20,12 @@ import akka.actor.{Actor, ActorRef, Props}
import akka.testkit.TestProbe
import fr.acinq.bitcoin.{BinaryData, Block, Satoshi, Script, Transaction, TxOut}
import fr.acinq.eclair.TestConstants.{Alice, Bob}
import fr.acinq.eclair._
import fr.acinq.eclair.blockchain.{ValidateRequest, ValidateResult, WatchSpentBasic}
import fr.acinq.eclair.router.Announcements.{makeChannelUpdate, makeNodeAnnouncement}
import fr.acinq.eclair.router.BaseRouterSpec.channelAnnouncement
import fr.acinq.eclair.transactions.Scripts
import fr.acinq.eclair.wire.{ChannelAnnouncement, ChannelUpdate, NodeAnnouncement}
import fr.acinq.eclair._
import org.junit.runner.RunWith
import org.scalatest.junit.JUnitRunner
@ -40,13 +40,13 @@ class RoutingSyncSpec extends TestkitBaseClass {
type FixtureParam = Tuple2[ActorRef, ActorRef]
val shortChannelIds = ChannelRangeQueriesSpec.readShortChannelIds().take(100).map(id => ShortChannelId(id))
val shortChannelIds = ChannelRangeQueriesSpec.shortChannelIds.take(500)
val fakeRoutingInfo = shortChannelIds.map(makeFakeRoutingInfo)
// A will be missing the last 20 items
val routingInfoA = fakeRoutingInfo.dropRight(20)
// and B will be missing the first 20 items
val routingInfoB = fakeRoutingInfo.drop(20)
// A will be missing the last 1000 items
val routingInfoA = fakeRoutingInfo.dropRight(100)
// and B will be missing the first 1000 items
val routingInfoB = fakeRoutingInfo.drop(100)
class FakeWatcher extends Actor {
def receive = {

View file

@ -43,7 +43,7 @@ class ChannelCodecsSpec extends FunSuite {
}
test("encode/decode key paths (all 0s)") {
val keyPath = KeyPath(Seq(0, 0, 0, 0))
val keyPath = KeyPath(Seq(0L, 0L, 0L, 0L))
val encoded = keyPathCodec.encode(keyPath).require
val decoded = keyPathCodec.decode(encoded).require
assert(keyPath === decoded.value)
@ -59,7 +59,7 @@ class ChannelCodecsSpec extends FunSuite {
test("encode/decode localparams") {
val o = LocalParams(
nodeId = randomKey.publicKey,
channelKeyPath = DeterministicWallet.KeyPath(Seq(42)),
channelKeyPath = DeterministicWallet.KeyPath(Seq(42L)),
dustLimitSatoshis = Random.nextInt(Int.MaxValue),
maxHtlcValueInFlightMsat = UInt64(Random.nextInt(Int.MaxValue)),
channelReserveSatoshis = Random.nextInt(Int.MaxValue),

View file

@ -16,8 +16,9 @@
package fr.acinq.eclair.wire
import java.net.{InetAddress, InetSocketAddress}
import java.net.{Inet4Address, Inet6Address, InetAddress}
import com.google.common.net.InetAddresses
import fr.acinq.bitcoin.Crypto.{PrivateKey, Scalar}
import fr.acinq.bitcoin.{BinaryData, Block, Crypto}
import fr.acinq.eclair.crypto.Sphinx
@ -26,13 +27,14 @@ import fr.acinq.eclair.{ShortChannelId, UInt64, randomBytes, randomKey}
import org.junit.runner.RunWith
import org.scalatest.FunSuite
import org.scalatest.junit.JUnitRunner
import scodec.bits.{BitVector, HexStringSyntax}
import scodec.bits.{BitVector, ByteVector, HexStringSyntax}
/**
* Created by PM on 31/05/2016.
*/
@RunWith(classOf[JUnitRunner])
class LightningMessageCodecsSpec extends FunSuite {
import LightningMessageCodecsSpec._
def bin(size: Int, fill: Byte): BinaryData = Array.fill[Byte](size)(fill)
@ -65,22 +67,40 @@ class LightningMessageCodecsSpec extends FunSuite {
assert(color === color2)
}
test("encode/decode with socketaddress codec") {
test("encode/decode all kind of IPv6 addresses with ipv6address codec") {
{
val ipv4addr = InetAddress.getByAddress(Array[Byte](192.toByte, 168.toByte, 1.toByte, 42.toByte))
val isa = new InetSocketAddress(ipv4addr, 4231)
val bin = socketaddress.encode(isa).require
// 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 isa2 = socketaddress.decode(bin).require.value
assert(isa === isa2)
val nodeaddr2 = nodeaddress.decode(bin).require.value
assert(nodeaddr === nodeaddr2)
}
{
val ipv6addr = InetAddress.getByAddress(hex"2001 0db8 0000 85a3 0000 0000 ac1f 8001".toArray)
val isa = new InetSocketAddress(ipv6addr, 4231)
val bin = socketaddress.encode(isa).require
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 isa2 = socketaddress.decode(bin).require.value
assert(isa === isa2)
val nodeaddr2 = nodeaddress.decode(bin).require.value
assert(nodeaddr === nodeaddr2)
}
}
@ -165,6 +185,24 @@ class LightningMessageCodecsSpec extends FunSuite {
})
}
test("encode/decode live node_announcements") {
val anns = List(
BinaryData("a58338c9660d135fd7d087eb62afd24a33562c54507a9334e79f0dc4f17d407e6d7c61f0e2f3d0d38599502f61704cf1ae93608df027014ade7ff592f27ce26900005acdf50702d2eabbbacc7c25bbd73b39e65d28237705f7bde76f557e94fb41cb18a9ec00841122116c6e302e646563656e7465722e776f726c64000000000000000000000000000000130200000000000000000000ffffae8a0b082607")
//BinaryData("d5bfb0be26412eed9bbab186772bd3885610e289ed305e729869a5bcbd97ea431863b6fa884b021162ed5e66264c4087630e4403669bab29f3c533c4089e508c00005ab521eb030e9226f19cd3ba8a58fb280d00f5f94f3c10f1b4618a5f9bffd43534c966ebd4030e9256495247494e41574f4c465f3200000000000000000000000000000000000000000f03cec0cb03c68094bbb48792002608")
//BinaryData("9746cd4d25a5cf2b04f3d986a073973b0318282e32e2758939b6650cd13cf65e4225ceaa98b02f070614e907661278a1479542afb12b9867511e0d31d995209800005ab646a302dc523b9db431de52d7adb79cf81dd3d780002f4ce952706053edc9da30d9b9f702dc5256495247494e41574f4c460000000000000000000000000000000000000000000016031bb5481aa82769f4446e1002260701584473f82607"),
//BinaryData("a483677744b63d892a85fb7460fd6cb0504f802600956eb18cfaad05fbbe775328e4a7060476d2c0f3b7a6d505bb4de9377a55b27d1477baf14c367287c3de7900005abb440002dc523b9db431de52d7adb79cf81dd3d780002f4ce952706053edc9da30d9b9f702dc5256495247494e41574f4c460000000000000000000000000000000000000000000016031bb5481aa82769f4446e1002260701584473f82607"),
//BinaryData("3ecfd85bcb3bafb5bad14ab7f6323a2df33e161c37c2897e576762fa90ffe46078d231ebbf7dce3eff4b440d091a10ea9d092e698a321bb9c6b30869e2782c9900005abbebe202dc523b9db431de52d7adb79cf81dd3d780002f4ce952706053edc9da30d9b9f702dc5256495247494e41574f4c460000000000000000000000000000000000000000000016031bb5481aa82769f4446e1002260701584473f82607"),
//BinaryData("ad40baf5c7151777cc8896bc70ad2d0fd2afff47f4befb3883a78911b781a829441382d82625b77a47b9c2c71d201aab7187a6dc80e7d2d036dcb1186bac273c00005abffc330341f5ff2992997613aff5675d6796232a63ab7f30136219774da8aba431df37c80341f563377a6763723364776d777a7a3261652e6f6e696f6e00000000000000000000000f0317f2614763b32d9ce804fc002607")
)
anns.foreach { ann =>
val bin = ByteVector(ann.data.toArray).toBitVector
val node = nodeAnnouncementCodec.decode(bin).require.value
val bin2 = nodeAnnouncementCodec.encode(node).require
assert(bin === bin2)
}
}
test("encode/decode all channel messages") {
val open = OpenChannel(randomBytes(32), randomBytes(32), 3, 4, 5, UInt64(6), 7, 8, 9, 10, 11, publicKey(1), point(2), point(3), point(4), point(5), point(6), 0.toByte)
@ -182,7 +220,7 @@ class LightningMessageCodecsSpec extends FunSuite {
val commit_sig = CommitSig(randomBytes(32), randomSignature, randomSignature :: randomSignature :: randomSignature :: Nil)
val revoke_and_ack = RevokeAndAck(randomBytes(32), scalar(0), point(1))
val channel_announcement = ChannelAnnouncement(randomSignature, randomSignature, randomSignature, randomSignature, bin(7, 9), Block.RegtestGenesisBlock.hash, ShortChannelId(1), randomKey.publicKey, randomKey.publicKey, randomKey.publicKey, randomKey.publicKey)
val node_announcement = NodeAnnouncement(randomSignature, bin(0, 0), 1, randomKey.publicKey, Color(100.toByte, 200.toByte, 300.toByte), "node-alias", new InetSocketAddress(InetAddress.getByAddress(Array[Byte](192.toByte, 168.toByte, 1.toByte, 42.toByte)), 42000) :: Nil)
val node_announcement = NodeAnnouncement(randomSignature, 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_update = ChannelUpdate(randomSignature, Block.RegtestGenesisBlock.hash, ShortChannelId(1), 2, bin(2, 2), 3, 4, 5, 6)
val announcement_signatures = AnnouncementSignatures(randomBytes(32), ShortChannelId(42), randomSignature, randomSignature)
val query_short_channel_id = QueryShortChannelIds(Block.RegtestGenesisBlock.blockId, randomBytes(7515))
@ -220,6 +258,38 @@ class LightningMessageCodecsSpec extends FunSuite {
assert(payload2 === payload1)
}
}
test("encode/decode using cached codec") {
val codec = cachedLightningMessageCodec
val commit_sig = CommitSig(randomBytes(32), randomSignature, randomSignature :: randomSignature :: randomSignature :: Nil)
val revoke_and_ack = RevokeAndAck(randomBytes(32), scalar(0), point(1))
val channel_announcement = ChannelAnnouncement(randomSignature, randomSignature, randomSignature, randomSignature, bin(7, 9), Block.RegtestGenesisBlock.hash, ShortChannelId(1), randomKey.publicKey, randomKey.publicKey, randomKey.publicKey, randomKey.publicKey)
val node_announcement = NodeAnnouncement(randomSignature, 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_update = ChannelUpdate(randomSignature, Block.RegtestGenesisBlock.hash, ShortChannelId(1), 2, bin(2, 2), 3, 4, 5, 6)
val announcement_signatures = AnnouncementSignatures(randomBytes(32), ShortChannelId(42), randomSignature, randomSignature)
val ping = Ping(100, BinaryData("01" * 10))
val pong = Pong(BinaryData("01" * 10))
val cached = channel_announcement :: node_announcement :: channel_update :: 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)))
}
}
object LightningMessageCodecsSpec {

View file

@ -65,7 +65,7 @@
<scala.version>2.11.11</scala.version>
<scala.version.short>2.11</scala.version.short>
<akka.version>2.3.14</akka.version>
<bitcoinlib.version>0.9.16</bitcoinlib.version>
<bitcoinlib.version>0.9.17</bitcoinlib.version>
<guava.version>24.0-android</guava.version>
</properties>