diff --git a/eclair-demo/src/main/resources/logback.xml b/eclair-demo/src/main/resources/logback.xml index 9a9e8409f..27232f2a5 100644 --- a/eclair-demo/src/main/resources/logback.xml +++ b/eclair-demo/src/main/resources/logback.xml @@ -6,7 +6,7 @@ false - return logger.contains("Node"); + return logger.contains("Node") || logger.contains("BlockchainWatcher"); DENY ACCEPT @@ -46,9 +46,25 @@ + + System.out + false + + + return ((String) mdc.get("akkaSource")).contains("blockchain"); + + DENY + ACCEPT + + + %yellow(${HOSTNAME} %d) %highlight(%-5level) %logger{36} %X{akkaSource} - %boldGreen(%msg) %ex{12}%n + + + + diff --git a/eclair-demo/src/main/scala/fr/acinq/eclair/BlockchainWatcher.scala b/eclair-demo/src/main/scala/fr/acinq/eclair/BlockchainWatcher.scala new file mode 100644 index 000000000..073037a58 --- /dev/null +++ b/eclair-demo/src/main/scala/fr/acinq/eclair/BlockchainWatcher.scala @@ -0,0 +1,43 @@ +package fr.acinq.eclair + +import akka.actor.{ActorRef, Actor, ActorLogging} +import fr.acinq.bitcoin.{Transaction, BinaryData} +import fr.acinq.lightning._ + +// @formatter:off + +final case class Watch(txId: BinaryData) +final case class Unwatch(txId: BinaryData) +final case class TxConfirmed(txId: BinaryData, blockId: BinaryData, confirmations: Int) +final case class Publish(tx: Transaction) + +// @formatter:on + +/** + * Created by PM on 28/08/2015. + */ +class BlockchainWatcher extends Actor with ActorLogging { + + context.become(watching(Map())) + + override def receive: Receive = ??? + + def watching(m: Map[BinaryData, ActorRef]): Receive = { + case Watch(txId) => + log.info(s"watching tx $txId for $sender") + // instant confirmation for testing + (0 until 3) foreach(i => self ! TxConfirmed(txId, "5deedc4c7f4c8e3250a486f340e57a565cda908eef7b7df2c1cd61b8ad6b42e6", i)) + context.become(watching(m + (txId -> sender))) + + case Unwatch(txId) => + context.become(watching(m - txId)) + + case TxConfirmed(txId, blockId, confirmations) if m.contains(txId) => + val channel = m(txId) + channel ! BITCOIN_TX_CONFIRMED(blockId, confirmations) + + + case Publish(tx) => + log.info(s"publishing tx $tx") + } +} diff --git a/eclair-demo/src/main/scala/fr/acinq/eclair/Boot.scala b/eclair-demo/src/main/scala/fr/acinq/eclair/Boot.scala index f11aaa891..a3e5cd85a 100644 --- a/eclair-demo/src/main/scala/fr/acinq/eclair/Boot.scala +++ b/eclair-demo/src/main/scala/fr/acinq/eclair/Boot.scala @@ -14,21 +14,20 @@ object Boot extends App { val system = ActorSystem() - val anchorInput = AnchorInput(1000L, OutPoint("bff676222800bf24bbf32f5a0fc83c4ddd5782f6ba23b4b352b3a6ddf0fe0b95", 0), SignData("76a914763518984abc129ab5825b8c14b6f4c8fa16f9c988ac", Base58Check.decode("cRqkWfx32NcfJjkutHqLrfbuY8HhTeCNLa7NLBpWy4bpk7bEYQYg")._2)) + val anchorInput = AnchorInput(100100000L, OutPoint(Hex.decode("7727730d21428276a4d6b0e16f3a3e6f3a07a07dc67151e6a88d4a8c3e8edb24").reverse, 1), SignData("76a914e093fbc19866b98e0fbc25d79d0ad0f0375170af88ac", Base58Check.decode("cU1YgK56oUKAtV6XXHZeJQjEx1KGXkZS1pGiKpyW4mUyKYFJwWFg")._2)) - val alice = system.actorOf(Props(new Node(Hex.decode("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), Hex.decode("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"), 1, Some(anchorInput))), name = "alice") - val bob = system.actorOf(Props(new Node(Hex.decode("cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"), Hex.decode("dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"), 2, None)), name = "bob") + val alice_commit_priv = Base58Check.decode("cQPmcNr6pwBQPyGfab3SksE9nTCtx9ism9T4dkS9dETNU2KKtJHk")._2 + val alice_final_priv = Base58Check.decode("cUrAtLtV7GGddqdkhUxnbZVDWGJBTducpPoon3eKp9Vnr1zxs6BG")._2 + val bob_commit_priv = Base58Check.decode("cSUwLtdZ2tht9ZmHhdQue48pfe7tY2GT2TGWJDtjoZgo6FHrubGk")._2 + val bob_final_priv = Base58Check.decode("cPR7ZgXpUaDPA3GwGceMDS5pfnSm955yvks3yELf3wMJwegsdGTg")._2 + + val blockchain = system.actorOf(Props(new BlockchainWatcher), name = "blockchain") + val alice = system.actorOf(Props(new Node(blockchain, alice_commit_priv, alice_final_priv, 1, Some(anchorInput))), name = "alice") + val bob = system.actorOf(Props(new Node(blockchain, bob_commit_priv, bob_final_priv, 2, None)), name = "bob") bob.tell(INPUT_NONE, alice) alice.tell(INPUT_NONE, bob) - Thread.sleep(500) - alice ! TxConfirmed(sha256_hash(1, 2, 3, 4), 1) - bob ! TxConfirmed(sha256_hash(1, 2, 3, 4), 1) - - Thread.sleep(500) - bob ! TxConfirmed(sha256_hash(1, 2, 3, 4), 2) - Thread.sleep(1000) val r = sha256_hash(7, 7, 7, 7) diff --git a/eclair-demo/src/main/scala/fr/acinq/eclair/Node.scala b/eclair-demo/src/main/scala/fr/acinq/eclair/Node.scala index ea3caef62..8c9b6e231 100644 --- a/eclair-demo/src/main/scala/fr/acinq/eclair/Node.scala +++ b/eclair-demo/src/main/scala/fr/acinq/eclair/Node.scala @@ -11,7 +11,6 @@ import lightning.update_decline_htlc.Reason.{CannotRoute, InsufficientFunds} import org.bouncycastle.util.encoders.Hex import scala.util.Try -import scala.concurrent.duration._ /** * Created by PM on 20/08/2015. @@ -45,7 +44,7 @@ case object CLOSED extends State case object INPUT_NONE sealed trait BlockchainEvent -final case class TxConfirmed(blockId: sha256_hash, confirmations: Int) extends BlockchainEvent +final case class BITCOIN_TX_CONFIRMED(blockId: sha256_hash, confirmations: Int) extends BlockchainEvent case object BITCOIN_CLOSE_DONE sealed trait Command @@ -77,10 +76,10 @@ final case class DATA_WAIT_FOR_UPDATE_COMPLETE(ourParams: ChannelParams, theirPa // @formatter:on -class Node(val commitPrivKey: BinaryData, val finalPrivKey: BinaryData, val minDepth: Int, val anchorDataOpt: Option[AnchorInput]) extends LoggingFSM[State, Data] with Stash { +class Node(val blockchain: ActorRef, val commitPrivKey: BinaryData, val finalPrivKey: BinaryData, val minDepth: Int, val anchorDataOpt: Option[AnchorInput]) extends LoggingFSM[State, Data] with Stash { val DEFAULT_delay = locktime(Blocks(10)) - val DEFAULT_commitmentFee = 100 + val DEFAULT_commitmentFee = 100000 val commitPubKey = bitcoin_pubkey(ByteString.copyFrom(Crypto.publicKeyFromPrivateKey(commitPrivKey.key.toByteArray))) val finalPubKey = bitcoin_pubkey(ByteString.copyFrom(Crypto.publicKeyFromPrivateKey(finalPrivKey.key.toByteArray))) @@ -123,10 +122,11 @@ class Node(val commitPrivKey: BinaryData, val finalPrivKey: BinaryData, val minD case Event(open_channel(delay, theirRevocationHash, commitKey, finalKey, WONT_CREATE_ANCHOR, minDepth, commitmentFee), DATA_OPEN_WAIT_FOR_OPEN_WITHANCHOR(ourParams, anchorInput, ourRevocationHashPreimage)) => val theirParams = ChannelParams(delay, commitKey, finalKey, minDepth.get, commitmentFee) val anchorTx = makeAnchorTx(ourParams.commitKey, theirParams.commitKey, anchorInput.amount, anchorInput.previousTxOutput, anchorInput.signData) + log.info(s"anchor txid=${anchorTx.hash}") //TODO : anchorOutputIndex might not always be zero if there are multiple outputs val anchorOutputIndex = 0 // we fund the channel with the anchor tx, so the money is ours - val state = ChannelState(them = ChannelOneSide(0, 0, Seq()), us = ChannelOneSide(anchorInput.amount, 0, Seq())) + val state = ChannelState(them = ChannelOneSide(0, 0, Seq()), us = ChannelOneSide(anchorInput.amount - DEFAULT_commitmentFee, 0, Seq())) // we build our commitment tx, leaving it unsigned val ourCommitTx = makeCommitTx(ourParams.finalKey, theirParams.finalKey, theirParams.delay, anchorTx.hash, anchorOutputIndex, theirRevocationHash, state) // then we build their commitment tx and sign it @@ -139,7 +139,7 @@ class Node(val commitPrivKey: BinaryData, val finalPrivKey: BinaryData, val minD when(OPEN_WAIT_FOR_ANCHOR) { case Event(open_anchor(anchorTxid, anchorOutputIndex, anchorAmount, theirSig), DATA_OPEN_WAIT_FOR_ANCHOR(ourParams, theirParams, ourRevocationHashPreimage, theirRevocationHash)) => // they fund the channel with their anchor tx, so the money is theirs - val state = ChannelState(them = ChannelOneSide(anchorAmount, 0, Seq()), us = ChannelOneSide(0, 0, Seq())) + val state = ChannelState(them = ChannelOneSide(anchorAmount - DEFAULT_commitmentFee, 0, Seq()), us = ChannelOneSide(0, 0, Seq())) // we build our commitment tx, sign it and check that it is spendable using the counterparty's sig val ourCommitTx = makeCommitTx(ourParams.finalKey, theirParams.finalKey, theirParams.delay, anchorTxid, anchorOutputIndex, Crypto.sha256(ourRevocationHashPreimage), state) // TODO : Transaction.sign(...) should handle multisig @@ -151,7 +151,7 @@ class Node(val commitPrivKey: BinaryData, val finalPrivKey: BinaryData, val minD val theirCommitTx = makeCommitTx(theirParams.finalKey, ourParams.finalKey, ourParams.delay, anchorTxid, anchorOutputIndex, theirRevocationHash, state.reverse) val ourSigForThem = bin2signature(Transaction.signInput(theirCommitTx, 0, multiSig2of2(ourParams.commitKey, theirParams.commitKey), SIGHASH_ALL, pubkey2bin(commitPrivKey))) them ! open_commit_sig(ourSigForThem) - // TODO : register for confirmations of anchor tx on the bitcoin network + blockchain ! Watch(anchorTxid) goto(OPEN_WAITING_THEIRANCHOR) using DATA_OPEN_WAITING(ourParams, theirParams, CommitmentTx(signedCommitTx, state, ourRevocationHashPreimage, theirRevocationHash)) } @@ -162,16 +162,17 @@ class Node(val commitPrivKey: BinaryData, val finalPrivKey: BinaryData, val minD val signedCommitTx = newCommitTx.tx.updateSigScript(0, sigScript2of2(theirSig, ourSig, theirParams.commitKey, ourParams.commitKey)) val ok = Try(Transaction.correctlySpends(signedCommitTx, Map(OutPoint(anchorTx.hash, anchorOutputIndex) -> multiSig2of2(ourParams.commitKey, theirParams.commitKey)), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)).isSuccess // TODO : return Error and close channel if !ok - log.info(s"publishing anchor tx ${new BinaryData(Transaction.write(anchorTx))}") + blockchain ! Watch(anchorTx.hash) + blockchain ! Publish(anchorTx) goto(OPEN_WAITING_OURANCHOR) using DATA_OPEN_WAITING(ourParams, theirParams, newCommitTx.copy(tx = signedCommitTx)) } when(OPEN_WAITING_THEIRANCHOR) { - case Event(TxConfirmed(blockId, confirmations), DATA_OPEN_WAITING(ourParams, _, _)) if confirmations < ourParams.minDepth => + case Event(BITCOIN_TX_CONFIRMED(blockId, confirmations), DATA_OPEN_WAITING(ourParams, _, _)) if confirmations < ourParams.minDepth => log.info(s"got $confirmations confirmation(s) for anchor tx") stay - case Event(TxConfirmed(blockId, confirmations), d@DATA_OPEN_WAITING(ourParams, _, _)) if confirmations >= ourParams.minDepth => + case Event(BITCOIN_TX_CONFIRMED(blockId, confirmations), d@DATA_OPEN_WAITING(ourParams, _, _)) if confirmations >= ourParams.minDepth => log.info(s"got $confirmations confirmation(s) for anchor tx, minDepth reached") them ! open_complete(Some(blockId)) unstashAll() @@ -184,11 +185,11 @@ class Node(val commitPrivKey: BinaryData, val finalPrivKey: BinaryData, val minD } when(OPEN_WAITING_OURANCHOR) { - case Event(TxConfirmed(blockId, confirmations), DATA_OPEN_WAITING(ourParams, _, _)) if confirmations < ourParams.minDepth => + case Event(BITCOIN_TX_CONFIRMED(blockId, confirmations), DATA_OPEN_WAITING(ourParams, _, _)) if confirmations < ourParams.minDepth => log.info(s"got $confirmations confirmation(s) for anchor tx") stay - case Event(TxConfirmed(blockId, confirmations), d@DATA_OPEN_WAITING(ourParams, _, _)) if confirmations >= ourParams.minDepth => + case Event(BITCOIN_TX_CONFIRMED(blockId, confirmations), d@DATA_OPEN_WAITING(ourParams, _, _)) if confirmations >= ourParams.minDepth => log.info(s"got $confirmations confirmation(s) for anchor tx, minDepth reached") them ! open_complete(Some(blockId)) unstashAll() @@ -277,8 +278,9 @@ class Node(val commitPrivKey: BinaryData, val finalPrivKey: BinaryData, val minD goto(WAIT_FOR_UPDATE_SIG) using DATA_WAIT_FOR_UPDATE_SIG(ourParams, theirParams, p, CommitmentTx(ourCommitTx, newState, ourRevocationHashPreimage, theirRevocationHash)) case Event(CMD_CLOSE(fee), DATA_NORMAL(ourParams, theirParams, CommitmentTx(commitmentTx, state, _, _))) => - val finalTx = makeFinalTx(commitmentTx.txIn, ourParams.finalKey, theirParams.finalKey, state) - val ourSigForThem = bin2signature(Transaction.signInput(finalTx, 0, multiSig2of2(ourParams.commitKey, theirParams.commitKey), SIGHASH_ALL, pubkey2bin(commitPrivKey))) + // the only difference between their final tx and ours is the order of the outputs, because state is symmetric + val theirFinalTx = makeFinalTx(commitmentTx.txIn, theirParams.finalKey, ourParams.finalKey, state.reverse) + val ourSigForThem = bin2signature(Transaction.signInput(theirFinalTx, 0, multiSig2of2(ourParams.commitKey, theirParams.commitKey), SIGHASH_ALL, pubkey2bin(commitPrivKey))) them ! close_channel(ourSigForThem, fee) goto(WAIT_FOR_CLOSE_COMPLETE) @@ -287,9 +289,9 @@ class Node(val commitPrivKey: BinaryData, val finalPrivKey: BinaryData, val minD val theirFinalTx = makeFinalTx(commitmentTx.txIn, ourParams.finalKey, theirParams.finalKey, state.reverse) val ourSigForThem = bin2signature(Transaction.signInput(theirFinalTx, 0, multiSig2of2(ourParams.commitKey, theirParams.commitKey), SIGHASH_ALL, pubkey2bin(commitPrivKey))) val ourFinalTx = makeFinalTx(commitmentTx.txIn, ourParams.finalKey, theirParams.finalKey, state) - val ourSig = bin2signature(Transaction.signInput(ourFinalTx, 0, multiSig2of2(ourParams.commitKey, theirParams.commitKey), SIGHASH_ALL, pubkey2bin(commitPrivKey))) + val ourSig = Transaction.signInput(ourFinalTx, 0, multiSig2of2(ourParams.commitKey, theirParams.commitKey), SIGHASH_ALL, pubkey2bin(commitPrivKey)) val signedFinaltx = ourFinalTx.updateSigScript(0, sigScript2of2(theirSig, ourSig, theirParams.commitKey, ourParams.commitKey)) - log.debug(s"final tx : ${Hex.toHexString(Transaction.write(signedFinaltx))}") + log.debug(s"*** final tx : ${Hex.toHexString(Transaction.write(signedFinaltx))}") // ok now we can broadcast the final tx if we want them ! close_channel_complete(ourSigForThem) goto(WAIT_FOR_CLOSE_ACK) @@ -382,4 +384,11 @@ class Node(val commitPrivKey: BinaryData, val finalPrivKey: BinaryData, val minD case _ => stay } + whenUnhandled { + case Event(e@BITCOIN_TX_CONFIRMED(_, confirmations), _) => + log.debug(s"dropped $e") + // drops silently, we don't care for confirmations above minDepth + stay + } + } diff --git a/eclair-demo/src/main/scala/fr/acinq/lightning/package.scala b/eclair-demo/src/main/scala/fr/acinq/lightning/package.scala index a417dfb51..55afc551f 100644 --- a/eclair-demo/src/main/scala/fr/acinq/lightning/package.scala +++ b/eclair-demo/src/main/scala/fr/acinq/lightning/package.scala @@ -15,24 +15,6 @@ import scala.annotation.tailrec package lightning { -/* - -BOB POV !! - -ChannelState(ALICE: ChannelOneSide(pay = 500, fee: Long, htlcs: Seq()), BOB: ChannelOneSide(pay = 500, fee: Long, htlcs = Seq())) - -bob received update_add_htlc(100) - -OPTION 1 ChannelState(ALICE: ChannelOneSide(pay = 500, fee: Long, htlcs: Seq(update_add_htlc(-100))), BOB: ChannelOneSide(pay = 500, fee: Long, htlcs = Seq(update_add_htlc(100)))) - -OPTION 2 ChannelState(ALICE: ChannelOneSide(pay = 400, fee: Long, htlcs: Seq()), BOB: ChannelOneSide(pay = 500, fee: Long, htlcs = Seq(update_add_htlc(100)))) - -bob received r from CAROL and sends a update_complete_htlc to ALICE - -ChannelState(ALICE: ChannelOneSide(pay = 400, fee: Long, htlcs: Seq()), BOB: ChannelOneSide(pay = 600, fee: Long, htlcs = Seq())) - - */ - case class ChannelOneSide(pay: Long, fee: Long, htlcs: Seq[update_add_htlc]) case class ChannelState(us: ChannelOneSide, them: ChannelOneSide) { @@ -148,7 +130,7 @@ package object lightning { Protocol.writeUInt64(in.s3, sbos) Protocol.writeUInt64(in.s4, sbos) val s = new BigInteger(1, sbos.toByteArray) - Crypto.encodeSignature(r, s) + Crypto.encodeSignature(r, s) :+ SIGHASH_ALL.toByte } implicit def locktime2long(in: locktime): Long = in match {