Browse Source

onchain: implement penalty transaction.

Fixes: #242
Signed-off-by: Rusty Russell <rusty@rustcorp.com.au>
ppa-0.6.1
Rusty Russell 7 years ago
parent
commit
1ac9e0b532
  1. 359
      onchaind/onchain.c
  2. 6
      onchaind/onchain_types.h
  3. 126
      tests/test_lightningd.py

359
onchaind/onchain.c

@ -45,6 +45,12 @@ static struct pubkey our_wallet_pubkey;
/* Private keys for spending HTLC outputs via HTLC txs, and directly. */
static struct privkey delayed_payment_privkey, payment_privkey;
/* Private keys for spending HTLC for penalty (only if they cheated). */
static struct privkey *revocation_privkey;
/* one value is useful for a few witness scripts */
static const u8 ONE = 0x1;
/* If we broadcast a tx, or need a delay to resolve the output. */
struct proposed_resolution {
/* This can be NULL if our proposal is to simply ignore it after depth */
@ -197,6 +203,8 @@ static const char *output_type_name(enum output_type output_type)
* 1. to-us output spend (`<local_delayedsig> 0`)
* 2. the their-commitment, our HTLC timeout case (`<remotesig> 0`),
* 3. the their-commitment, our HTLC redeem case (`<remotesig> <payment_preimage>`)
* 4. the their-revoked-commitment, to-local (`<revocation_sig> 1`)
* 5. the their-revoked-commitment, htlc (`<revocation_sig> <revocationkey>`)
*/
static struct bitcoin_tx *tx_to_us(const tal_t *ctx,
struct tracked_output *out,
@ -621,6 +629,35 @@ static void resolve_htlc_tx(struct tracked_output ***outs,
OUR_DELAYED_RETURN_TO_WALLET);
}
/* BOLT #5:
*
* 5. _B's HTLC-timeout transaction_: The node MUST *resolve* this by
* spending using the revocation key.
*/
/* BOLT #5:
*
* 6. _B's HTLC-success transaction_: The node MUST *resolve* this by
* spending using the revocation key. The node SHOULD extract
* the payment preimage from the transaction input witness if not
* already known.
*/
static void steal_htlc_tx(struct tracked_output *out)
{
struct bitcoin_tx *tx;
/* BOLT #3:
*
* To spend this via penalty, the remote node uses a witness stack
* `<revocationsig> 1`
*/
tx = tx_to_us(out, out, 0xFFFFFFFF, 0,
&ONE, sizeof(ONE),
out->wscript,
revocation_privkey,
&keyset->self_revocation_key);
propose_resolution(out, tx, 0, OUR_PENALTY_TX);
}
/* An output has been spent: see if it resolves something we care about. */
static void output_spent(struct tracked_output ***outs,
const struct bitcoin_tx *tx,
@ -658,13 +695,26 @@ static void output_spent(struct tracked_output ***outs,
break;
case THEIR_HTLC:
if (out->tx_type == THEIR_REVOKED_UNILATERAL) {
steal_htlc_tx(out);
} else {
/* We ignore this timeout tx, since we should
* resolve by ignoring once we reach depth. */
}
break;
case OUR_HTLC:
/* The only way they can spend this: fulfill */
/* The only way they can spend this: fulfill; even
* if it's revoked: */
/* BOLT #5:
*
* 6. _B's HTLC-success transaction_: ... The node
* SHOULD extract the payment preimage from the
* transaction input witness if not already known.
*/
handle_htlc_onchain_fulfill(out, tx);
if (out->tx_type == THEIR_REVOKED_UNILATERAL)
steal_htlc_tx(out);
break;
case FUNDING_OUTPUT:
@ -701,7 +751,9 @@ static void tx_new_depth(struct tracked_output **outs,
/* Is this tx resolving an output? */
if (outs[i]->resolved) {
if (structeq(&outs[i]->resolved->txid, txid)) {
status_trace("%s depth %u",
status_trace("%s/%s->%s depth %u",
tx_type_name(outs[i]->tx_type),
output_type_name(outs[i]->output_type),
tx_type_name(outs[i]->resolved->tx_type),
depth);
outs[i]->resolved->depth = depth;
@ -1222,14 +1274,304 @@ static void handle_our_unilateral(const struct bitcoin_tx *tx,
tal_free(tmpctx);
}
/* We produce individual penalty txs. It's less efficient, but avoids them
* using HTLC txs to block our penalties for long enough to pass the CSV
* delay */
static void steal_to_them_output(struct tracked_output *out)
{
const tal_t *tmpctx = tal_tmpctx(NULL);
u8 *wscript;
struct bitcoin_tx *tx;
/* BOLT #3:
*
* If a revoked commitment transaction is published, the other party
* can spend this output immediately with the following witness:
*
* <revocation_sig> 1
*/
wscript = bitcoin_wscript_to_local(tmpctx, to_self_delay[REMOTE],
&keyset->self_revocation_key,
&keyset->self_delayed_payment_key);
tx = tx_to_us(tmpctx, out, 0xFFFFFFFF, 0,
&ONE, sizeof(ONE),
wscript,
revocation_privkey,
&keyset->self_revocation_key);
propose_resolution(out, tx, 0, OUR_PENALTY_TX);
tal_free(tmpctx);
}
static void steal_htlc(struct tracked_output *out)
{
struct bitcoin_tx *tx;
u8 der[PUBKEY_DER_LEN];
/* BOLT #3:
*
* If a revoked commitment transaction is published, the remote node
* can spend this output immediately with the following witness:
*
* <revocation_sig> <revocationkey>
*/
pubkey_to_der(der, &keyset->self_revocation_key);
tx = tx_to_us(out, out, 0xFFFFFFFF, 0,
der, sizeof(der),
out->wscript,
revocation_privkey,
&keyset->self_revocation_key);
propose_resolution(out, tx, 0, OUR_PENALTY_TX);
}
/* BOLT #5:
*
* If a node tries to broadcast old state, we can use the revocation key to
* claim all the funds.
*/
static void handle_their_cheat(const struct bitcoin_tx *tx,
u64 commit_index,
const struct sha256_double *txid,
u32 tx_blockheight,
const struct sha256 *revocation_preimage,
const struct secrets *secrets,
const struct pubkey *local_revocation_basepoint,
const struct pubkey *local_payment_basepoint,
const struct pubkey *remote_payment_basepoint,
const struct pubkey *remote_delayed_payment_basepoint,
u64 commit_num,
const struct htlc_stub *htlcs,
struct tracked_output **outs)
{
const tal_t *tmpctx = tal_tmpctx(NULL);
u8 **htlc_scripts;
u8 *remote_wscript, *script[NUM_SIDES];
struct keyset *ks;
size_t i;
struct secret per_commitment_secret;
struct privkey per_commitment_privkey;
struct pubkey per_commitment_point;
set_state(ONCHAIND_CHEATED);
init_feerate_range(outs[0]->satoshi, tx);
/* BOLT #5:
*
* If a node sees a *commitment transaction* for which it has a
* revocation key, that *resolves* the funding transaction output.
*/
resolved_by_other(outs[0], txid, THEIR_REVOKED_UNILATERAL);
/* FIXME: Types. */
BUILD_ASSERT(sizeof(per_commitment_secret)
== sizeof(*revocation_preimage));
memcpy(&per_commitment_secret, revocation_preimage,
sizeof(per_commitment_secret));
BUILD_ASSERT(sizeof(per_commitment_privkey)
== sizeof(*revocation_preimage));
memcpy(&per_commitment_privkey, revocation_preimage,
sizeof(per_commitment_privkey));
if (!pubkey_from_privkey(&per_commitment_privkey, &per_commitment_point))
status_failed(STATUS_FAIL_INTERNAL_ERROR,
"Failed derivea from per_commitment_secret %s",
type_to_string(trc, struct privkey,
&per_commitment_privkey));
status_trace("Deriving keyset %"PRIu64
": per_commit_point=%s"
" self_payment_basepoint=%s"
" other_payment_basepoint=%s"
" self_delayed_basepoint=%s"
" other_revocation_basepoint=%s",
commit_num,
type_to_string(trc, struct pubkey,
&per_commitment_point),
type_to_string(trc, struct pubkey,
remote_payment_basepoint),
type_to_string(trc, struct pubkey,
local_payment_basepoint),
type_to_string(trc, struct pubkey,
remote_delayed_payment_basepoint),
type_to_string(trc, struct pubkey,
local_revocation_basepoint));
/* keyset is const, we need a non-const ptr to set it up */
keyset = ks = tal(tx, struct keyset);
if (!derive_keyset(&per_commitment_point,
remote_payment_basepoint,
local_payment_basepoint,
remote_delayed_payment_basepoint,
local_revocation_basepoint,
ks))
status_failed(STATUS_FAIL_INTERNAL_ERROR,
"FIXME: Implement penalty transaction");
"Deriving keyset for %"PRIu64, commit_num);
status_trace("Deconstructing revoked unilateral tx: %"PRIu64
" using keyset: "
" self_revocation_key: %s"
" self_delayed_payment_key: %s"
" self_payment_key: %s"
" other_payment_key: %s",
commit_num,
type_to_string(trc, struct pubkey,
&keyset->self_revocation_key),
type_to_string(trc, struct pubkey,
&keyset->self_delayed_payment_key),
type_to_string(trc, struct pubkey,
&keyset->self_payment_key),
type_to_string(trc, struct pubkey,
&keyset->other_payment_key));
revocation_privkey = tal(tx, struct privkey);
if (!derive_revocation_privkey(&secrets->revocation_basepoint_secret,
&per_commitment_secret,
local_revocation_basepoint,
&per_commitment_point,
revocation_privkey))
status_failed(STATUS_FAIL_INTERNAL_ERROR,
"Deriving revocation_privkey for %"PRIu64,
commit_num);
remote_wscript = to_self_wscript(tmpctx, to_self_delay[REMOTE], keyset);
/* Figure out what to-them output looks like. */
script[REMOTE] = scriptpubkey_p2wsh(tmpctx, remote_wscript);
/* Figure out what direct to-us output looks like. */
script[LOCAL] = scriptpubkey_p2wpkh(tmpctx, &keyset->other_payment_key);
/* Calculate all the HTLC scripts so we can match them */
htlc_scripts = derive_htlc_scripts(htlcs, REMOTE);
status_trace("Script to-them: %u: %s (%s)",
to_self_delay[REMOTE],
tal_hex(trc, script[REMOTE]),
tal_hex(trc, remote_wscript));
status_trace("Script to-me: %s",
tal_hex(trc, script[LOCAL]));
for (i = 0; i < tal_count(tx->output); i++) {
status_trace("Output %zu: %s",
i, tal_hex(trc, tx->output[i].script));
}
/* BOLT #5:
*
* A node MUST resolve all unresolved outputs as follows:
*
* 1. _A's main output_: No action is required; this is a simple
* P2WPKH output. This output is considered *resolved* by the
* *commitment transaction*.
*
* 2. _B's main output_: The node MUST *resolve* this by spending
* using the revocation key.
*
* 3. _A's offered HTLCs_: The node MUST *resolve* this in one of three
* ways by spending:
* * the *commitment tx* using the payment revocation
* * the *commitment tx* using the payment preimage if known
* * the *HTLC-timeout tx* if B publishes them
*
* 4. _B's offered HTLCs_: The node MUST *resolve* this in one of two
* ways by spending:
* * the *commitment tx* using the payment revocation
* * the *commitment tx* once the HTLC timeout has passed.
*
* 5. _B's HTLC-timeout transaction_: The node MUST *resolve* this by
* spending using the revocation key.
*
* 6. _B's HTLC-success transaction_: The node MUST *resolve* this by
* spending using the revocation key. The node SHOULD extract
* the payment preimage from the transaction input witness if not
* already known.
*/
for (i = 0; i < tal_count(tx->output); i++) {
struct tracked_output *out;
int j;
if (script[LOCAL]
&& scripteq(tx->output[i].script, script[LOCAL])) {
/* BOLT #5:
*
* 1. _A's main output_: No action is required; this
* is a simple P2WPKH output. This output is
* considered *resolved* by the *commitment
* transaction* itself.
*/
out = new_tracked_output(&outs, txid, tx_blockheight,
THEIR_REVOKED_UNILATERAL,
i, tx->output[i].amount,
OUTPUT_TO_US, NULL, NULL, NULL);
ignore_output(out);
script[LOCAL] = NULL;
continue;
}
if (script[REMOTE]
&& scripteq(tx->output[i].script, script[REMOTE])) {
/* BOLT #5:
*
* 2. _B's main output_: The node MUST *resolve* this
* by spending using the revocation key. */
out = new_tracked_output(&outs, txid, tx_blockheight,
THEIR_REVOKED_UNILATERAL, i,
tx->output[i].amount,
DELAYED_OUTPUT_TO_THEM,
NULL, NULL, NULL);
steal_to_them_output(out);
script[REMOTE] = NULL;
continue;
}
j = match_htlc_output(tx, i, htlc_scripts);
if (j == -1)
status_failed(STATUS_FAIL_INTERNAL_ERROR,
"Could not find resolution for output %zu",
i);
if (htlcs[j].owner == LOCAL) {
/* BOLT #5:
*
* 3. _A's offered HTLCs_: The node MUST *resolve* this
* in one of three ways by spending:
* * the *commitment tx* using the payment revocation
* * the *commitment tx* using the payment preimage if
* known
* * the *HTLC-timeout tx* if B publishes them
*/
out = new_tracked_output(&outs, txid,
tx_blockheight,
THEIR_REVOKED_UNILATERAL, i,
tx->output[i].amount,
OUR_HTLC,
&htlcs[j], htlc_scripts[j],
NULL);
steal_htlc(out);
} else {
out = new_tracked_output(&outs, txid,
tx_blockheight,
THEIR_REVOKED_UNILATERAL, i,
tx->output[i].amount,
THEIR_HTLC,
&htlcs[j], htlc_scripts[j],
NULL);
/* BOLT #5:
*
* 4. _B's offered HTLCs_: The node MUST *resolve*
* this in one of two ways by spending:
*
* * the *commitment tx* using the payment revocation
* * the *commitment tx* once the HTLC timeout has
* passed.
*/
steal_htlc(out);
}
htlc_scripts[j] = NULL;
}
wait_for_resolved(outs);
tal_free(tmpctx);
}
static void handle_their_unilateral(const struct bitcoin_tx *tx,
@ -1568,8 +1910,15 @@ int main(int argc, char *argv[])
else if (shachain_get_hash(&shachain,
shachain_index(commit_num),
&revocation_preimage)) {
handle_their_cheat(tx, commit_num,
handle_their_cheat(tx, &txid,
tx_blockheight,
&revocation_preimage,
&secrets,
&basepoints.revocation,
&basepoints.payment,
&remote_payment_basepoint,
&remote_delayed_payment_basepoint,
commit_num,
htlcs, outs);
/* BOLT #5:
*

6
onchaind/onchain_types.h

@ -16,6 +16,9 @@ enum tx_type {
/* Our unilateral: spends funding */
OUR_UNILATERAL,
/* Their old unilateral: spends funding */
THEIR_REVOKED_UNILATERAL,
/* The 2 different types of HTLC transaction, each way */
THEIR_HTLC_TIMEOUT_TO_THEM,
THEIR_HTLC_FULFILL_TO_US,
@ -29,6 +32,9 @@ enum tx_type {
/* When we spend a delayed output (after cltv_expiry) */
OUR_DELAYED_RETURN_TO_WALLET,
/* When we use revocation key to take output. */
OUR_PENALTY_TX,
/* Special type for marking outputs as resolved by self. */
SELF,

126
tests/test_lightningd.py

@ -443,8 +443,8 @@ class LightningDTests(BaseLightningDTests):
bitcoind.rpc.generate(94)
l1.daemon.wait_for_log('onchaind complete, forgetting peer')
# Now, 100 blocks it should be done.
bitcoind.rpc.generate(100)
# Now, 100 blocks l2 should be done.
bitcoind.rpc.generate(6)
l2.daemon.wait_for_log('onchaind complete, forgetting peer')
def test_onchain_middleman(self):
@ -521,6 +521,128 @@ class LightningDTests(BaseLightningDTests):
l1.bitcoin.rpc.generate(100)
l2.daemon.wait_for_log('onchaind complete, forgetting peer')
def test_penalty_inhtlc(self):
"""Test penalty transaction with an incoming HTLC"""
# We suppress each one after first commit; HTLC gets added not fulfilled.
l1 = self.node_factory.get_node(disconnect=['_WIRE_COMMITMENT_SIGNED'])
l2 = self.node_factory.get_node(disconnect=['_WIRE_COMMITMENT_SIGNED'])
l1.rpc.connect('localhost', l2.info['port'], l2.info['id'])
self.fund_channel(l1, l2, 10**6)
# Now, this will get stuck due to l1 commit being disabled..
t = self.pay(l1,l2,100000000,async=True)
# They should both have commitments blocked now.
l1.daemon.wait_for_log('_WIRE_COMMITMENT_SIGNED')
l2.daemon.wait_for_log('_WIRE_COMMITMENT_SIGNED')
# Make sure l1 got l2's commitment to the HTLC, and sent to master.
l1.daemon.wait_for_log('UPDATE WIRE_CHANNEL_GOT_COMMITSIG')
# Take our snapshot.
tx = l1.rpc.dev_sign_last_tx(l2.info['id'])['tx']
# Let them continue
l1.rpc.dev_reenable_commit(l2.info['id'])
l2.rpc.dev_reenable_commit(l1.info['id'])
# Should fulfill.
l1.daemon.wait_for_log('peer_in WIRE_UPDATE_FULFILL_HTLC')
l1.daemon.wait_for_log('peer_out WIRE_REVOKE_AND_ACK')
l2.daemon.wait_for_log('peer_out WIRE_UPDATE_FULFILL_HTLC')
l1.daemon.wait_for_log('peer_in WIRE_REVOKE_AND_ACK')
# Payment should now complete.
t.result(timeout=10)
# Now we really mess things up!
bitcoind.rpc.sendrawtransaction(tx)
bitcoind.rpc.generate(1)
l2.daemon.wait_for_log('-> ONCHAIND_CHEATED')
# FIXME: l1 should try to stumble along!
l1.has_failed()
# l2 should spend all of the outputs (except to-us).
# Could happen in any order, depending on commitment tx.
l2.daemon.wait_for_logs(['Propose handling THEIR_REVOKED_UNILATERAL/DELAYED_OUTPUT_TO_THEM by OUR_PENALTY_TX .* in 0 blocks',
'sendrawtx exit 0',
'Propose handling THEIR_REVOKED_UNILATERAL/THEIR_HTLC by OUR_PENALTY_TX .* in 0 blocks',
'sendrawtx exit 0'])
# FIXME: test HTLC tx race!
# 100 blocks later, all resolved.
bitcoind.rpc.generate(100)
# FIXME: Test wallet balance...
l2.daemon.wait_for_log('onchaind complete, forgetting peer')
def test_penalty_outhtlc(self):
"""Test penalty transaction with an outgoing HTLC"""
# First we need to get funds to l2, so suppress after second.
l1 = self.node_factory.get_node(disconnect=['_WIRE_COMMITMENT_SIGNED*3'])
l2 = self.node_factory.get_node(disconnect=['_WIRE_COMMITMENT_SIGNED*3'])
l1.rpc.connect('localhost', l2.info['port'], l2.info['id'])
self.fund_channel(l1, l2, 10**6)
# Move some across to l2.
self.pay(l1,l2,200000000)
assert not l1.daemon.is_in_log('_WIRE_COMMITMENT_SIGNED')
assert not l2.daemon.is_in_log('_WIRE_COMMITMENT_SIGNED')
# Now, this will get stuck due to l1 commit being disabled..
t = self.pay(l2,l1,100000000,async=True)
# Make sure we get signature from them.
l1.daemon.wait_for_log('peer_in WIRE_UPDATE_ADD_HTLC')
l1.daemon.wait_for_log('peer_in WIRE_COMMITMENT_SIGNED')
# They should both have commitments blocked now.
l1.daemon.wait_for_log('dev_disconnect: _WIRE_COMMITMENT_SIGNED')
l2.daemon.wait_for_log('dev_disconnect: _WIRE_COMMITMENT_SIGNED')
# Take our snapshot.
tx = l1.rpc.dev_sign_last_tx(l2.info['id'])['tx']
# Let the continue
l1.rpc.dev_reenable_commit(l2.info['id'])
l2.rpc.dev_reenable_commit(l1.info['id'])
# Thread should complete.
t.result(timeout=10)
# Make sure both sides got revoke_and_ack for final.
l1.daemon.wait_for_log('peer_in WIRE_REVOKE_AND_ACK')
l2.daemon.wait_for_log('peer_in WIRE_REVOKE_AND_ACK')
# Now we really mess things up!
bitcoind.rpc.sendrawtransaction(tx)
bitcoind.rpc.generate(1)
l2.daemon.wait_for_log('-> ONCHAIND_CHEATED')
# FIXME: l1 should try to stumble along!
l1.has_failed()
# l2 should spend all of the outputs (except to-us).
# Could happen in any order, depending on commitment tx.
l2.daemon.wait_for_logs(['Ignoring output.*: THEIR_REVOKED_UNILATERAL/OUTPUT_TO_US',
'Propose handling THEIR_REVOKED_UNILATERAL/DELAYED_OUTPUT_TO_THEM by OUR_PENALTY_TX .* in 0 blocks',
'sendrawtx exit 0',
'Propose handling THEIR_REVOKED_UNILATERAL/OUR_HTLC by OUR_PENALTY_TX .* in 0 blocks',
'sendrawtx exit 0'])
# FIXME: test HTLC tx race!
# 100 blocks later, all resolved.
bitcoind.rpc.generate(100)
# FIXME: Test wallet balance...
l2.daemon.wait_for_log('onchaind complete, forgetting peer')
def test_permfail_new_commit(self):
# Test case where we have two possible commits: it will use new one.
disconnects = ['-WIRE_REVOKE_AND_ACK', 'permfail']

Loading…
Cancel
Save