From 59d1dead71e6cd5140def9c4c3e050d8783996e4 Mon Sep 17 00:00:00 2001 From: sstone Date: Fri, 1 Jul 2016 16:22:34 +0200 Subject: [PATCH] create a tx that steals their revoked commit tx --- .../acinq/eclair/channel/ChannelTypes.scala | 3 + .../fr/acinq/eclair/channel/Helpers.scala | 58 +++++++++++++++++++ .../channel/StealRevokedCommitmentSpec.scala | 56 ++++++++++++++++++ .../acinq/eclair/channel/TestConstants.scala | 41 ++++++++++++- 4 files changed, 155 insertions(+), 3 deletions(-) create mode 100644 eclair-demo/src/test/scala/fr/acinq/eclair/channel/StealRevokedCommitmentSpec.scala diff --git a/eclair-demo/src/main/scala/fr/acinq/eclair/channel/ChannelTypes.scala b/eclair-demo/src/main/scala/fr/acinq/eclair/channel/ChannelTypes.scala index c7c38144a..578e48915 100644 --- a/eclair-demo/src/main/scala/fr/acinq/eclair/channel/ChannelTypes.scala +++ b/eclair-demo/src/main/scala/fr/acinq/eclair/channel/ChannelTypes.scala @@ -117,6 +117,9 @@ final case class OurChannelParams(delay: locktime, commitPrivKey: BinaryData, fi val finalPubKey: BinaryData = Crypto.publicKeyFromPrivateKey(finalPrivKey) } final case class TheirChannelParams(delay: locktime, commitPubKey: BinaryData, finalPubKey: BinaryData, minDepth: Option[Int], initialFeeRate: Long) +object TheirChannelParams { + def apply(params: OurChannelParams) = new TheirChannelParams(params.delay, params.commitPubKey, params.finalPubKey, Some(params.minDepth), params.initialFeeRate) +} sealed trait Direction case object IN extends Direction diff --git a/eclair-demo/src/main/scala/fr/acinq/eclair/channel/Helpers.scala b/eclair-demo/src/main/scala/fr/acinq/eclair/channel/Helpers.scala index d43ea9319..d50217108 100644 --- a/eclair-demo/src/main/scala/fr/acinq/eclair/channel/Helpers.scala +++ b/eclair-demo/src/main/scala/fr/acinq/eclair/channel/Helpers.scala @@ -147,4 +147,62 @@ object Helpers { def revocationPreimage(seed: BinaryData, index: Long): BinaryData = ShaChain.shaChainFromSeed(seed, 0xFFFFFFFFFFFFFFFFL - index) def revocationHash(seed: BinaryData, index: Long): BinaryData = Crypto.sha256(revocationPreimage(seed, index)) + + /** + * Claim their revoked commit tx. If they published a revoked commit tx, we should be able to "steal" it with one + * of the revocation preimages that we received. + * Remainder: their commit tx sends: + * - our money to our final key + * - their money to (their final key + our delay) OR (our final key + secret) + * We don't have anything to do about our output (which should probably show up in our bitcoin wallet), can steal their + * money if we can find the preimage. + * We use a basic brute force algorithm: try all the preimages that we have until we find a match + * + * @param commitTx their revoked commit tx + * @param commitments our commitment data + * @return an optional transaction which "steals" their output + */ + def claimTheirRevokedCommit(commitTx: Transaction, commitments: Commitments): Option[Transaction] = { + + // this is what their output script looks like + def theirOutputScript(preimage: BinaryData) = { + val revocationHash = Crypto.sha256(preimage) + redeemSecretOrDelay(commitments.theirParams.finalPubKey, locktime2long_csv(commitments.theirParams.delay), commitments.ourParams.finalPubKey, revocationHash) + } + + // find an output that we can claim with one of our preimages + // the only that we're looking for is a pay-to-script (pay2wsh) so for each output that we try we need to generate + // all possible output scripts, hash them and see if they match + def findTheirOutputPreimage: Option[(Int, BinaryData)] = { + for (i <- 0 until commitTx.txOut.length) { + val actual = Script.parse(commitTx.txOut(i).publicKeyScript) + val preimage = commitments.theirPreimages.iterator.find(preimage => { + val expected = theirOutputScript(preimage) + val hashOfExpected = pay2wsh(expected) + hashOfExpected == actual + }) + preimage.map(value => return Some(i, value)) + } + None + } + + findTheirOutputPreimage map { + case (index, preimage) => + // TODO: substract network fee + val amount = commitTx.txOut(index).amount + val tx = Transaction(version = 2, + txIn = TxIn(OutPoint(commitTx, index), BinaryData.empty, TxIn.SEQUENCE_FINAL) :: Nil, + txOut = TxOut(amount, pay2pkh(commitments.ourParams.finalPubKey)) :: Nil, + lockTime = 0xffffffffL) + val redeemScript: BinaryData = Script.write(theirOutputScript(preimage)) + val sig: BinaryData = Transaction.signInput(tx, 0, redeemScript, SIGHASH_ALL, amount, 1, commitments.ourParams.finalPrivKey, randomize = false) + val witness = ScriptWitness(sig :: preimage :: redeemScript :: Nil) + val tx1 = tx.copy(witness = Seq(witness)) + + // check that we can actually spend the commit tx + Transaction.correctlySpends(tx1, commitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + + tx1 + } + } } diff --git a/eclair-demo/src/test/scala/fr/acinq/eclair/channel/StealRevokedCommitmentSpec.scala b/eclair-demo/src/test/scala/fr/acinq/eclair/channel/StealRevokedCommitmentSpec.scala new file mode 100644 index 000000000..93d9a3913 --- /dev/null +++ b/eclair-demo/src/test/scala/fr/acinq/eclair/channel/StealRevokedCommitmentSpec.scala @@ -0,0 +1,56 @@ +package fr.acinq.eclair.channel + +import fr.acinq.bitcoin.{BinaryData, Crypto, ScriptFlags, Transaction} +import fr.acinq.eclair._ +import lightning.locktime.Locktime.Blocks +import lightning.{locktime, routing, update_add_htlc} +import org.junit.runner.RunWith +import org.scalatest.FunSuite +import org.scalatest.junit.JUnitRunner +import TestConstants._ + +@RunWith(classOf[JUnitRunner]) +class StealRevokedCommitmentSpec extends FunSuite { + + def signAndReceiveRevocation(sender: Commitments, receiver: Commitments): (Commitments, Commitments) = { + val (sender1, commit1) = Commitments.sendCommit(sender) + val (receiver1, rev1) = Commitments.receiveCommit(receiver, commit1) + val sender2 = Commitments.receiveRevocation(sender1, rev1) + (sender2, receiver1) + } + + def addHtlc(sender: Commitments, receiver: Commitments, htlc: update_add_htlc): (Commitments, Commitments) = { + (Commitments.addOurProposal(sender, htlc), Commitments.addTheirProposal(receiver, htlc)) + } + + def fulfillHtlc(sender: Commitments, receiver: Commitments, id: Long, paymentPreimage: BinaryData): (Commitments, Commitments) = { + val (sender1, fulfill) = Commitments.sendFulfill(sender, CMD_FULFILL_HTLC(id, paymentPreimage)) + val receiver1 = Commitments.receiveFulfill(receiver, fulfill) + (sender1, receiver1) + } + + test("steal a revoked commit tx") { + val alice = Alice.commitments + val bob = Bob.commitments + + val R: BinaryData = "0102030405060708010203040506070801020304050607080102030405060708" + val H = Crypto.sha256(R) + + val htlc = update_add_htlc(1, 70000000, H, locktime(Blocks(400)), routing.defaultInstance) + val (alice1, bob1) = addHtlc(alice, bob, htlc) + val (alice2, bob2) = signAndReceiveRevocation(alice1, bob1) + + val (bob3, alice3) = signAndReceiveRevocation(bob2, alice2) + val (bob4, alice4) = fulfillHtlc(bob3, alice3, 1, R) + val (bob5, alice5) = signAndReceiveRevocation(bob4, alice4) + + // now what if Alice published a revoked commit tx ? + Seq(alice1, alice2, alice3, alice4).map(alice => { + val stealTx = Helpers.claimTheirRevokedCommit(alice.ourCommit.publishableTx, bob5) + Transaction.correctlySpends(stealTx.get, Seq(alice.ourCommit.publishableTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + }) + + // but we cannot steal Alice's current commit tx + assert(Helpers.claimTheirRevokedCommit(alice5.ourCommit.publishableTx, bob5) == None) + } +} diff --git a/eclair-demo/src/test/scala/fr/acinq/eclair/channel/TestConstants.scala b/eclair-demo/src/test/scala/fr/acinq/eclair/channel/TestConstants.scala index ca3b08d28..f5353b918 100644 --- a/eclair-demo/src/test/scala/fr/acinq/eclair/channel/TestConstants.scala +++ b/eclair-demo/src/test/scala/fr/acinq/eclair/channel/TestConstants.scala @@ -1,7 +1,9 @@ package fr.acinq.eclair.channel -import fr.acinq.bitcoin.{Base58, Base58Check, Crypto, Transaction, TxIn, TxOut} +import fr.acinq.bitcoin.{Base58, Base58Check, Crypto, Hash, OutPoint, Satoshi, Transaction, TxIn, TxOut} import fr.acinq.eclair.blockchain.{MakeAnchor, Publish} +import fr.acinq.eclair.crypto.ShaChain +import fr.acinq.eclair._ import lightning.locktime import lightning.locktime.Locktime.Blocks @@ -11,20 +13,53 @@ import lightning.locktime.Locktime.Blocks object TestConstants { val anchorAmount = 1000000L + lazy val anchorOutput = TxOut(Satoshi(anchorAmount), publicKeyScript = Scripts.anchorPubkeyScript(Alice.channelParams.commitPubKey, Bob.channelParams.commitPubKey)) + // Alice is funder, Bob is not object Alice { val (Base58.Prefix.SecretKeyTestnet, commitPrivKey) = Base58Check.decode("cQPmcNr6pwBQPyGfab3SksE9nTCtx9ism9T4dkS9dETNU2KKtJHk") val (Base58.Prefix.SecretKeyTestnet, finalPrivKey) = Base58Check.decode("cUrAtLtV7GGddqdkhUxnbZVDWGJBTducpPoon3eKp9Vnr1zxs6BG") - val channelParams = OurChannelParams(locktime(Blocks(10)), commitPrivKey, finalPrivKey, 1, 10000, Crypto.sha256("alice-seed".getBytes()), Some(anchorAmount)) + val channelParams = OurChannelParams(locktime(Blocks(4)), commitPrivKey, finalPrivKey, 1, 10000, Crypto.sha256("alice-seed".getBytes()), Some(anchorAmount)) val finalPubKey = channelParams.finalPubKey + + def revocationHash(index: Long) = Helpers.revocationHash(channelParams.shaSeed, index) + + def ourSpec = CommitmentSpec(Set.empty[Htlc], feeRate = Alice.channelParams.initialFeeRate, initial_amount_them_msat = 0, initial_amount_us_msat = anchorAmount * 1000, amount_them_msat = 0, amount_us_msat = anchorAmount * 1000) + + def theirSpec = CommitmentSpec(Set.empty[Htlc], feeRate = Bob.channelParams.initialFeeRate, initial_amount_them_msat = anchorAmount * 1000, initial_amount_us_msat = 0, amount_them_msat = anchorAmount * 1000, amount_us_msat = 0) + + val ourTx = Helpers.makeOurTx(channelParams, TheirChannelParams(Bob.channelParams), TxIn(OutPoint(Hash.One, 0), Array.emptyByteArray, 0xffffffffL) :: Nil, revocationHash(0), ourSpec) + + val commitments = Commitments( + Alice.channelParams, + TheirChannelParams(Bob.channelParams), + OurCommit(0, ourSpec, ourTx), TheirCommit(0, theirSpec, Bob.revocationHash(0)), + OurChanges(Nil, Nil, Nil), TheirChanges(Nil, Nil), + Right(Bob.revocationHash(1)), anchorOutput, ShaChain.init) + } object Bob { val (Base58.Prefix.SecretKeyTestnet, commitPrivKey) = Base58Check.decode("cSUwLtdZ2tht9ZmHhdQue48pfe7tY2GT2TGWJDtjoZgo6FHrubGk") val (Base58.Prefix.SecretKeyTestnet, finalPrivKey) = Base58Check.decode("cPR7ZgXpUaDPA3GwGceMDS5pfnSm955yvks3yELf3wMJwegsdGTg") - val channelParams = OurChannelParams(locktime(Blocks(10)), commitPrivKey, finalPrivKey, 2, 10000, Crypto.sha256("bob-seed".getBytes()), None) + val channelParams = OurChannelParams(locktime(Blocks(4)), commitPrivKey, finalPrivKey, 2, 10000, Crypto.sha256("bob-seed".getBytes()), None) val finalPubKey = channelParams.finalPubKey + + def revocationHash(index: Long) = Helpers.revocationHash(channelParams.shaSeed, index) + + def ourSpec = Alice.theirSpec + + def theirSpec = Alice.ourSpec + + val ourTx = Helpers.makeOurTx(channelParams, TheirChannelParams(Alice.channelParams), TxIn(OutPoint(Hash.One, 0), Array.emptyByteArray, 0xffffffffL) :: Nil, revocationHash(0), ourSpec) + + val commitments = Commitments( + Bob.channelParams, + TheirChannelParams(Alice.channelParams), + OurCommit(0, ourSpec, ourTx), TheirCommit(0, theirSpec, Alice.revocationHash(0)), + OurChanges(Nil, Nil, Nil), TheirChanges(Nil, Nil), + Right(Alice.revocationHash(1)), anchorOutput, ShaChain.init) } }