mirror of
https://github.com/ACINQ/eclair.git
synced 2025-02-22 14:22:39 +01:00
use bitcoind to create our anchor tx
This commit is contained in:
parent
6287ff700e
commit
07053b7d22
4 changed files with 169 additions and 30 deletions
|
@ -1,8 +1,10 @@
|
|||
package fr.acinq.eclair.blockchain
|
||||
|
||||
import akka.actor.{Cancellable, Actor, ActorLogging}
|
||||
import fr.acinq.bitcoin.{JsonRPCError, BitcoinJsonRPCClient}
|
||||
import org.json4s.JsonAST.JNull
|
||||
import fr.acinq.bitcoin.{Transaction, JsonRPCError, BitcoinJsonRPCClient}
|
||||
import org.bouncycastle.util.encoders.Hex
|
||||
import org.json4s.{FieldSerializer, CustomSerializer}
|
||||
import org.json4s.JsonAST._
|
||||
import scala.concurrent.{Future, ExecutionContext}
|
||||
import scala.concurrent.duration._
|
||||
|
||||
|
@ -27,23 +29,25 @@ class PollingWatcher(client: BitcoinJsonRPCClient)(implicit ec: ExecutionContext
|
|||
case w: Watch if !watches.contains(w) =>
|
||||
log.info(s"adding watch $w for $sender")
|
||||
val cancellable = context.system.scheduler.schedule(2 seconds, 30 seconds)(w match {
|
||||
case w@WatchConfirmed(channel, txId, minDepth, event) =>
|
||||
getTxConfirmations(client, txId.toString).map(_ match {
|
||||
case Some(confirmations) if confirmations >= minDepth =>
|
||||
channel ! event
|
||||
self !('remove, w)
|
||||
case _ => {}
|
||||
})
|
||||
case w@WatchSpent(channel, txId, outputIndex, minDepth, event) =>
|
||||
for {
|
||||
conf <- getTxConfirmations(client, txId.toString)
|
||||
unspent <- isUnspent(client, txId.toString, outputIndex)
|
||||
} yield {if (conf.isDefined && !unspent) {
|
||||
// NOTE : isSpent=!isUnspent only works if the parent transaction actually exists (which we assume to be true)
|
||||
channel ! event
|
||||
self !('remove, w)
|
||||
} else {}}
|
||||
})
|
||||
case w@WatchConfirmed(channel, txId, minDepth, event) =>
|
||||
getTxConfirmations(client, txId.toString).map(_ match {
|
||||
case Some(confirmations) if confirmations >= minDepth =>
|
||||
channel ! event
|
||||
self !('remove, w)
|
||||
case _ => {}
|
||||
})
|
||||
case w@WatchSpent(channel, txId, outputIndex, minDepth, event) =>
|
||||
for {
|
||||
conf <- getTxConfirmations(client, txId.toString)
|
||||
unspent <- isUnspent(client, txId.toString, outputIndex)
|
||||
} yield {
|
||||
if (conf.isDefined && !unspent) {
|
||||
// NOTE : isSpent=!isUnspent only works if the parent transaction actually exists (which we assume to be true)
|
||||
channel ! event
|
||||
self !('remove, w)
|
||||
} else {}
|
||||
}
|
||||
})
|
||||
context.become(watching(watches + (w -> cancellable)))
|
||||
|
||||
case ('remove, w: Watch) if watches.contains(w) =>
|
||||
|
@ -57,18 +61,73 @@ class PollingWatcher(client: BitcoinJsonRPCClient)(implicit ec: ExecutionContext
|
|||
|
||||
object PollingWatcher {
|
||||
|
||||
implicit val formats = org.json4s.DefaultFormats
|
||||
implicit val formats = org.json4s.DefaultFormats + new TransactionSerializer
|
||||
|
||||
class TransactionSerializer extends CustomSerializer[Transaction](format => (
|
||||
{
|
||||
case JString(x) => Transaction.read(x)
|
||||
},
|
||||
{
|
||||
case x: Transaction => JString(Hex.toHexString(Transaction.write(x)))
|
||||
}
|
||||
))
|
||||
|
||||
def getTxConfirmations(client: BitcoinJsonRPCClient, txId: String)(implicit ec: ExecutionContext): Future[Option[Int]] =
|
||||
client.invoke("getrawtransaction", txId, 1) // we choose verbose output to get the number of confirmations
|
||||
.map(json => Some((json \ "confirmations").extract[Int]))
|
||||
.recover {
|
||||
case t: JsonRPCError if t.code == -5 => None
|
||||
}
|
||||
client.invoke("getrawtransaction", txId, 1) // we choose verbose output to get the number of confirmations
|
||||
.map(json => Some((json \ "confirmations").extract[Int]))
|
||||
.recover {
|
||||
case t: JsonRPCError if t.code == -5 => None
|
||||
}
|
||||
|
||||
def isUnspent(client: BitcoinJsonRPCClient, txId: String, outputIndex: Int)(implicit ec: ExecutionContext): Future[Boolean] =
|
||||
client.invoke("gettxout", txId, outputIndex, true) // mempool=true so that we are warned as soon as possible
|
||||
.map(json => json != JNull)
|
||||
|
||||
/**
|
||||
* tell bitcoind to sent bitcoins from a specific local account
|
||||
*
|
||||
* @param client bitcoind client
|
||||
* @param account name of the local account to send bitcoins from
|
||||
* @param destination destination address
|
||||
* @param amount amount in BTC (not milliBTC, not Satoshis !!)
|
||||
* @param ec execution context
|
||||
* @return a Future[txid] where txid (a String) is the is of the tx that sends the bitcoins
|
||||
*/
|
||||
def sendFromAccount(client: BitcoinJsonRPCClient, account: String, destination: String, amount: Double)(implicit ec: ExecutionContext): Future[String] =
|
||||
client.invoke("sendfrom", account, destination, amount).map {
|
||||
case JString(txid) => txid
|
||||
}
|
||||
|
||||
def getTransaction(client: BitcoinJsonRPCClient, txId: String)(implicit ec: ExecutionContext): Future[Transaction] =
|
||||
client.invoke("getrawtransaction", txId) map {
|
||||
case JString(raw) => Transaction.read(raw)
|
||||
}
|
||||
|
||||
case class FundTransactionResponse(tx: Transaction, changepos: Int, fee: Double)
|
||||
|
||||
def fundTransaction(client: BitcoinJsonRPCClient, hex: String)(implicit ec: ExecutionContext): Future[FundTransactionResponse] = {
|
||||
client.invoke("fundrawtransaction", hex).map(json => {
|
||||
val JString(hex) = json \ "hex"
|
||||
val JInt(changepos) = json \ "changepos"
|
||||
val JDouble(fee) = json \ "fee"
|
||||
FundTransactionResponse(Transaction.read(hex), changepos.intValue(), fee)
|
||||
})
|
||||
}
|
||||
|
||||
def fundTransaction(client: BitcoinJsonRPCClient, tx: Transaction)(implicit ec: ExecutionContext): Future[FundTransactionResponse] =
|
||||
fundTransaction(client, Hex.toHexString(Transaction.write(tx)))
|
||||
|
||||
case class SignTransactionResponse(tx: Transaction, complete: Boolean)
|
||||
|
||||
def signTransaction(client: BitcoinJsonRPCClient, hex: String)(implicit ec: ExecutionContext): Future[SignTransactionResponse] =
|
||||
client.invoke("signrawtransaction", hex).map(json => {
|
||||
val JString(hex) = json \ "hex"
|
||||
val JBool(complete) = json \ "complete"
|
||||
SignTransactionResponse(Transaction.read(hex), complete)
|
||||
})
|
||||
|
||||
def signTransaction(client: BitcoinJsonRPCClient, tx: Transaction)(implicit ec: ExecutionContext): Future[SignTransactionResponse] =
|
||||
signTransaction(client, Hex.toHexString(Transaction.write(tx)))
|
||||
}
|
||||
|
||||
/*object MyTest extends App {
|
||||
|
|
|
@ -0,0 +1,73 @@
|
|||
package fr.acinq.eclair.channel
|
||||
|
||||
import com.typesafe.config.ConfigFactory
|
||||
import fr.acinq.bitcoin._
|
||||
import fr.acinq.eclair.blockchain.PollingWatcher
|
||||
import fr.acinq.eclair.blockchain.PollingWatcher.{SignTransactionResponse, FundTransactionResponse}
|
||||
import fr.acinq.eclair.channel
|
||||
import org.bouncycastle.util.encoders.Hex
|
||||
|
||||
import scala.concurrent.{Future, ExecutionContext, Await}
|
||||
import scala.concurrent.duration._
|
||||
|
||||
/**
|
||||
* Created by fabrice on 03/02/16.
|
||||
*/
|
||||
object Anchor extends App {
|
||||
|
||||
def makeAnchorTx(bitcoind: BitcoinJsonRPCClient, ourCommitPub: BinaryData, theirCommitPub: BinaryData, amount: Long)(implicit ec: ExecutionContext): Future[(Transaction, Int)] = {
|
||||
val anchorOutputScript = channel.Scripts.anchorPubkeyScript(ourCommitPub, theirCommitPub)
|
||||
val tx = Transaction(version = 1, txIn = Seq.empty[TxIn], txOut = TxOut(amount, anchorOutputScript) :: Nil, lockTime = 0)
|
||||
val future = for {
|
||||
FundTransactionResponse(tx1, changepos, fee) <- PollingWatcher.fundTransaction(bitcoind, tx)
|
||||
SignTransactionResponse(anchorTx, true) <- PollingWatcher.signTransaction(bitcoind, tx1)
|
||||
Some(pos) = Scripts.findPublicKeyScriptIndex(anchorTx, anchorOutputScript)
|
||||
} yield (anchorTx, pos)
|
||||
|
||||
future
|
||||
}
|
||||
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
|
||||
object Alice {
|
||||
val (Base58.Prefix.SecretKeyTestnet, commitPriv) = Base58Check.decode("cQPmcNr6pwBQPyGfab3SksE9nTCtx9ism9T4dkS9dETNU2KKtJHk")
|
||||
val commitPub = Crypto.publicKeyFromPrivateKey(commitPriv)
|
||||
val (Base58.Prefix.SecretKeyTestnet, finalPriv) = Base58Check.decode("cUrAtLtV7GGddqdkhUxnbZVDWGJBTducpPoon3eKp9Vnr1zxs6BG")
|
||||
val finalPub = Crypto.publicKeyFromPrivateKey(finalPriv)
|
||||
val R: BinaryData = "secret".getBytes("UTF-8")
|
||||
val H: BinaryData = Crypto.sha256(R)
|
||||
}
|
||||
|
||||
object Bob {
|
||||
val (Base58.Prefix.SecretKeyTestnet, commitPriv) = Base58Check.decode("cSUwLtdZ2tht9ZmHhdQue48pfe7tY2GT2TGWJDtjoZgo6FHrubGk")
|
||||
val commitPub = Crypto.publicKeyFromPrivateKey(commitPriv)
|
||||
val (Base58.Prefix.SecretKeyTestnet, finalPriv) = Base58Check.decode("cPR7ZgXpUaDPA3GwGceMDS5pfnSm955yvks3yELf3wMJwegsdGTg")
|
||||
val finalPub = Crypto.publicKeyFromPrivateKey(finalPriv)
|
||||
}
|
||||
|
||||
val amount = (0.5 * Coin).asInstanceOf[Long]
|
||||
|
||||
val config = ConfigFactory.load()
|
||||
val bitcoind = new BitcoinJsonRPCClient(
|
||||
user = config.getString("eclair.bitcoind.rpcuser"),
|
||||
password = config.getString("eclair.bitcoind.rpcpassword"),
|
||||
host = config.getString("eclair.bitcoind.address"),
|
||||
port = config.getInt("eclair.bitcoind.port"))
|
||||
|
||||
val (anchorTx, pos) = Await.result(makeAnchorTx(bitcoind, Alice.commitPub, Bob.commitPub, amount), 10 seconds)
|
||||
println(anchorTx)
|
||||
println(Hex.toHexString(Transaction.write(anchorTx)))
|
||||
bitcoind.client.close()
|
||||
|
||||
val spending = Transaction(version = 1,
|
||||
txIn = TxIn(OutPoint(anchorTx, pos), Array.emptyByteArray, 0xffffffffL) :: Nil,
|
||||
txOut = TxOut(10, OP_DUP :: OP_HASH160 :: OP_PUSHDATA(Crypto.hash160(Alice.commitPub)) :: OP_EQUALVERIFY :: OP_CHECKSIG :: Nil) :: Nil,
|
||||
lockTime = 0)
|
||||
|
||||
val redeemScript = Scripts.multiSig2of2(Alice.commitPub, Bob.commitPub)
|
||||
val sig1 = Transaction.signInput(spending, 0, redeemScript, SIGHASH_ALL, Alice.commitPriv, randomize = false)
|
||||
val sig2 = Transaction.signInput(spending, 0, redeemScript, SIGHASH_ALL, Bob.commitPriv, randomize = false)
|
||||
val scriptSig = Scripts.sigScript2of2(sig1, sig2, Alice.commitPub, Bob.commitPub)
|
||||
val signedTx = spending.updateSigScript(0, scriptSig)
|
||||
Transaction.correctlySpends(signedTx, Seq(anchorTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
|
||||
}
|
|
@ -154,7 +154,10 @@ final case class RES_GETINFO(name: String, state: State, data: Data)
|
|||
sealed trait Data
|
||||
case object Nothing extends Data
|
||||
final case class AnchorInput(amount: Long, previousTxOutput: OutPoint, signData: SignData) extends Data
|
||||
final case class OurChannelParams(delay: locktime, commitPrivKey: BinaryData, finalPrivKey: BinaryData, minDepth: Int, commitmentFee: Long, shaSeed: BinaryData)
|
||||
final case class OurChannelParams(delay: locktime, commitPrivKey: BinaryData, finalPrivKey: BinaryData, minDepth: Int, commitmentFee: Long, shaSeed: BinaryData) {
|
||||
val commitPubKey: BinaryData = Crypto.publicKeyFromPrivateKey(commitPrivKey)
|
||||
val finalPubKey: BinaryData = Crypto.publicKeyFromPrivateKey(finalPrivKey)
|
||||
}
|
||||
final case class TheirChannelParams(delay: locktime, commitPubKey: BinaryData, finalPubKey: BinaryData, minDepth: Option[Int], commitmentFee: Long)
|
||||
final case class Commitment(index: Long, tx: Transaction, state: ChannelState, theirRevocationHash: sha256_hash)
|
||||
final case class UpdateProposal(index: Long, state: ChannelState)
|
||||
|
@ -186,11 +189,11 @@ final case class DATA_CLOSING(ourParams: OurChannelParams, theirParams: TheirCha
|
|||
|
||||
class Channel(val blockchain: ActorRef, val params: OurChannelParams, val anchorDataOpt: Option[AnchorInput]) extends LoggingFSM[State, Data] with Stash {
|
||||
|
||||
val ourCommitPubKey = bitcoin_pubkey(ByteString.copyFrom(Crypto.publicKeyFromPrivateKey(params.commitPrivKey.key.toByteArray)))
|
||||
val ourFinalPubKey = bitcoin_pubkey(ByteString.copyFrom(Crypto.publicKeyFromPrivateKey(params.finalPrivKey.key.toByteArray)))
|
||||
val ourCommitPubKey = bitcoin_pubkey(ByteString.copyFrom(params.commitPubKey))
|
||||
val ourFinalPubKey = bitcoin_pubkey(ByteString.copyFrom(params.finalPubKey))
|
||||
|
||||
log.info(s"commit pubkey: ${BinaryData(Crypto.publicKeyFromPrivateKey(params.commitPrivKey.key.toByteArray))}")
|
||||
log.info(s"final pubkey: ${BinaryData(Crypto.publicKeyFromPrivateKey(params.finalPrivKey.key.toByteArray))}")
|
||||
log.info(s"commit pubkey: ${params.commitPubKey}")
|
||||
log.info(s"final pubkey: ${params.finalPubKey}")
|
||||
|
||||
// TODO
|
||||
var them: ActorRef = null
|
||||
|
|
|
@ -163,4 +163,8 @@ object Scripts {
|
|||
if (isFunder(a)) ChannelState(c1, c2) else ChannelState(c2, c1)
|
||||
}
|
||||
|
||||
def findPublicKeyScriptIndex(tx: Transaction, publicKeyScript: BinaryData): Option[Int] =
|
||||
tx.txOut.zipWithIndex.find {
|
||||
case (TxOut(_, script), _) => script == publicKeyScript
|
||||
} map (_._2)
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue