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:
commit
c275482c2c
41 changed files with 551 additions and 253 deletions
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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.
|
||||
|
|
|
@ -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")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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)
|
||||
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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)
|
||||
)
|
||||
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)))
|
||||
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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) ::
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
|
@ -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") {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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 {
|
||||
|
|
2
pom.xml
2
pom.xml
|
@ -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>
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue