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:
parent
bfb4c98937
commit
59d1dead71
4 changed files with 155 additions and 3 deletions
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue