1
0
Fork 0
mirror of https://github.com/ACINQ/eclair.git synced 2025-02-24 06:47:46 +01:00

create a tx that steals their revoked commit tx

This commit is contained in:
sstone 2016-07-01 16:22:34 +02:00
parent bfb4c98937
commit 59d1dead71
4 changed files with 155 additions and 3 deletions

View file

@ -117,6 +117,9 @@ final case class OurChannelParams(delay: locktime, commitPrivKey: BinaryData, fi
val finalPubKey: BinaryData = Crypto.publicKeyFromPrivateKey(finalPrivKey) val finalPubKey: BinaryData = Crypto.publicKeyFromPrivateKey(finalPrivKey)
} }
final case class TheirChannelParams(delay: locktime, commitPubKey: BinaryData, finalPubKey: BinaryData, minDepth: Option[Int], initialFeeRate: Long) 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 sealed trait Direction
case object IN extends Direction case object IN extends Direction

View file

@ -147,4 +147,62 @@ object Helpers {
def revocationPreimage(seed: BinaryData, index: Long): BinaryData = ShaChain.shaChainFromSeed(seed, 0xFFFFFFFFFFFFFFFFL - index) def revocationPreimage(seed: BinaryData, index: Long): BinaryData = ShaChain.shaChainFromSeed(seed, 0xFFFFFFFFFFFFFFFFL - index)
def revocationHash(seed: BinaryData, index: Long): BinaryData = Crypto.sha256(revocationPreimage(seed, 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
}
}
} }

View file

@ -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)
}
}

View file

@ -1,7 +1,9 @@
package fr.acinq.eclair.channel 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.blockchain.{MakeAnchor, Publish}
import fr.acinq.eclair.crypto.ShaChain
import fr.acinq.eclair._
import lightning.locktime import lightning.locktime
import lightning.locktime.Locktime.Blocks import lightning.locktime.Locktime.Blocks
@ -11,20 +13,53 @@ import lightning.locktime.Locktime.Blocks
object TestConstants { object TestConstants {
val anchorAmount = 1000000L val anchorAmount = 1000000L
lazy val anchorOutput = TxOut(Satoshi(anchorAmount), publicKeyScript = Scripts.anchorPubkeyScript(Alice.channelParams.commitPubKey, Bob.channelParams.commitPubKey))
// Alice is funder, Bob is not // Alice is funder, Bob is not
object Alice { object Alice {
val (Base58.Prefix.SecretKeyTestnet, commitPrivKey) = Base58Check.decode("cQPmcNr6pwBQPyGfab3SksE9nTCtx9ism9T4dkS9dETNU2KKtJHk") val (Base58.Prefix.SecretKeyTestnet, commitPrivKey) = Base58Check.decode("cQPmcNr6pwBQPyGfab3SksE9nTCtx9ism9T4dkS9dETNU2KKtJHk")
val (Base58.Prefix.SecretKeyTestnet, finalPrivKey) = Base58Check.decode("cUrAtLtV7GGddqdkhUxnbZVDWGJBTducpPoon3eKp9Vnr1zxs6BG") 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 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 { object Bob {
val (Base58.Prefix.SecretKeyTestnet, commitPrivKey) = Base58Check.decode("cSUwLtdZ2tht9ZmHhdQue48pfe7tY2GT2TGWJDtjoZgo6FHrubGk") val (Base58.Prefix.SecretKeyTestnet, commitPrivKey) = Base58Check.decode("cSUwLtdZ2tht9ZmHhdQue48pfe7tY2GT2TGWJDtjoZgo6FHrubGk")
val (Base58.Prefix.SecretKeyTestnet, finalPrivKey) = Base58Check.decode("cPR7ZgXpUaDPA3GwGceMDS5pfnSm955yvks3yELf3wMJwegsdGTg") 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 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)
} }
} }