Merge pull request #6347 from yyforyongyu/5388-rm-revc-log

lnwallet+channeldb: store minimal info in revocation log bucket
This commit is contained in:
Olaoluwa Osuntokun authored and GitHub committed 2022-05-05 15:52:59 -07:00
commit 0ec88b5346
33 files changed
+3635 -562

No files matched your search

+324 -142
View File
@@ -96,6 +96,17 @@ var (
// both parties can retrieve their funds.
ErrCommitSyncRemoteDataLoss = fmt.Errorf("possible remote commitment " +
"state data loss")
// ErrNoRevocationLogFound is returned when both the returned logs are
// nil from querying the revocation log bucket. In theory this should
// never happen as the query will return `ErrLogEntryNotFound`, yet
// we'd still perform a sanity check to make sure at least one of the
// logs is non-nil.
ErrNoRevocationLogFound = errors.New("no revocation log found")
// ErrOutputIndexOutOfRange is returned when an output index is greater
// than or equal to the length of a given transaction's outputs.
ErrOutputIndexOutOfRange = errors.New("output index is out of range")
)
// ErrCommitSyncLocalDataLoss is returned in the case that we receive a valid
@@ -1036,26 +1047,29 @@ type updateLog struct {
logIndex uint64
// htlcCounter is a monotonically increasing integer that tracks the
// total number of offered HTLC's by the owner of this update log. We
// use a distinct index for this purpose, as update's that remove
// entries from the log will be indexed using this counter.
// total number of offered HTLC's by the owner of this update log,
// hence the `Add` update type. We use a distinct index for this
// purpose, as update's that remove entries from the log will be
// indexed using this counter.
htlcCounter uint64
// List is the updatelog itself, we embed this value so updateLog has
// access to all the method of a list.List.
*list.List
// updateIndex is an index that maps a particular entries index to the
// list element within the list.List above.
// updateIndex maps a `logIndex` to a particular update entry. It
// deals with the four update types:
// `Fail|MalformedFail|Settle|FeeUpdate`
updateIndex map[uint64]*list.Element
// offerIndex is an index that maps the counter for offered HTLC's to
// their list element within the main list.List.
// htlcIndex maps a `htlcCounter` to an offered HTLC entry, hence the
// `Add` update.
htlcIndex map[uint64]*list.Element
// modifiedHtlcs is a set that keeps track of all the current modified
// htlcs. A modified HTLC is one that's present in the log, and has as
// a pending fail or settle that's attempting to consume it.
// htlcs, hence update types `Fail|MalformedFail|Settle`. A modified
// HTLC is one that's present in the log, and has as a pending fail or
// settle that's attempting to consume it.
modifiedHtlcs map[uint64]struct{}
}
@@ -2210,10 +2224,10 @@ type HtlcRetribution struct {
// transaction. The BreachRetribution is then sent over the ContractBreach
// channel in order to allow the subscriber of the channel to dispatch justice.
type BreachRetribution struct {
// BreachTransaction is the transaction which breached the channel
// BreachTxHash is the transaction hash which breached the channel
// contract by spending from the funding multi-sig with a revoked
// commitment transaction.
BreachTransaction *wire.MsgTx
BreachTxHash chainhash.Hash
// BreachHeight records the block height confirming the breach
// transaction, used as a height hint when registering for
@@ -2228,13 +2242,9 @@ type BreachRetribution struct {
// RevokedStateNum is the revoked state number which was broadcast.
RevokedStateNum uint64
// PendingHTLCs is a slice of the HTLCs which were pending at this
// point within the channel's history transcript.
PendingHTLCs []channeldb.HTLC
// LocalOutputSignDesc is a SignDescriptor which is capable of
// generating the signature necessary to sweep the output within the
// BreachTransaction that pays directly us.
// breach transaction that pays directly us.
//
// NOTE: A nil value indicates that the local output is considered dust
// according to the remote party's dust limit.
@@ -2279,16 +2289,22 @@ type BreachRetribution struct {
// passed channel, at a particular revoked state number, and one which targets
// the passed commitment transaction.
func NewBreachRetribution(chanState *channeldb.OpenChannel, stateNum uint64,
breachHeight uint32) (*BreachRetribution, error) {
breachHeight uint32, spendTx *wire.MsgTx) (*BreachRetribution, error) {
// Query the on-disk revocation log for the snapshot which was recorded
// at this particular state num.
revokedSnapshot, err := chanState.FindPreviousState(stateNum)
// at this particular state num. Based on whether a legacy revocation
// log is returned or not, we will process them differently.
revokedLog, revokedLogLegacy, err := chanState.FindPreviousState(
stateNum,
)
if err != nil {
return nil, err
}
commitHash := revokedSnapshot.CommitTx.TxHash()
// Sanity check that at least one of the logs is returned.
if revokedLog == nil && revokedLogLegacy == nil {
return nil, ErrNoRevocationLogFound
}
// With the state number broadcast known, we can now derive/restore the
// proper revocation preimage necessary to sweep the remote party's
@@ -2311,22 +2327,14 @@ func NewBreachRetribution(chanState *channeldb.OpenChannel, stateNum uint64,
// Next, reconstruct the scripts as they were present at this state
// number so we can have the proper witness script to sign and include
// within the final witness.
theirDelay := uint32(chanState.RemoteChanCfg.CsvDelay)
isRemoteInitiator := !chanState.IsInitiator
var leaseExpiry uint32
if chanState.ChanType.HasLeaseExpiration() {
leaseExpiry = chanState.ThawHeight
}
theirScript, err := CommitScriptToSelf(
chanState.ChanType, isRemoteInitiator, keyRing.ToLocalKey,
keyRing.RevocationKey, theirDelay, leaseExpiry,
)
if err != nil {
return nil, err
}
// Since it is the remote breach we are reconstructing, the output going
// to us will be a to-remote script with our local params.
// Since it is the remote breach we are reconstructing, the output
// going to us will be a to-remote script with our local params.
isRemoteInitiator := !chanState.IsInitiator
ourScript, ourDelay, err := CommitScriptToRemote(
chanState.ChanType, isRemoteInitiator, keyRing.ToRemoteKey,
leaseExpiry,
@@ -2335,15 +2343,248 @@ func NewBreachRetribution(chanState *channeldb.OpenChannel, stateNum uint64,
return nil, err
}
// In order to fully populate the breach retribution struct, we'll need
// to find the exact index of the commitment outputs.
theirDelay := uint32(chanState.RemoteChanCfg.CsvDelay)
theirScript, err := CommitScriptToSelf(
chanState.ChanType, isRemoteInitiator, keyRing.ToLocalKey,
keyRing.RevocationKey, theirDelay, leaseExpiry,
)
if err != nil {
return nil, err
}
// Define an empty breach retribution that will be overwritten based on
// different version of the revocation log found.
var br *BreachRetribution
// Define our and their amounts, that will be overwritten below.
var ourAmt, theirAmt int64
// If the returned *RevocationLog is non-nil, use it to derive the info
// we need.
if revokedLog != nil {
br, ourAmt, theirAmt, err = createBreachRetribution(
revokedLog, spendTx, chanState, keyRing,
commitmentSecret, leaseExpiry,
)
if err != nil {
return nil, err
}
} else {
// The returned revocation log is in legacy format, which is a
// *ChannelCommitment.
//
// NOTE: this branch is kept for compatibility such that for
// old nodes which refuse to migrate the legacy revocation log
// data can still function. This branch can be deleted once we
// are confident that no legacy format is in use.
br, ourAmt, theirAmt, err = createBreachRetributionLegacy(
revokedLogLegacy, chanState, keyRing, commitmentSecret,
ourScript, theirScript, leaseExpiry,
)
if err != nil {
return nil, err
}
}
// Conditionally instantiate a sign descriptor for each of the
// commitment outputs. If either is considered dust using the remote
// party's dust limit, the respective sign descriptor will be nil.
//
// If our balance exceeds the remote party's dust limit, instantiate
// the sign descriptor for our output.
if ourAmt >= int64(chanState.RemoteChanCfg.DustLimit) {
br.LocalOutputSignDesc = &input.SignDescriptor{
SingleTweak: keyRing.LocalCommitKeyTweak,
KeyDesc: chanState.LocalChanCfg.PaymentBasePoint,
WitnessScript: ourScript.WitnessScript,
Output: &wire.TxOut{
PkScript: ourScript.PkScript,
Value: ourAmt,
},
HashType: txscript.SigHashAll,
}
}
// Similarly, if their balance exceeds the remote party's dust limit,
// assemble the sign descriptor for their output, which we can sweep.
if theirAmt >= int64(chanState.RemoteChanCfg.DustLimit) {
br.RemoteOutputSignDesc = &input.SignDescriptor{
KeyDesc: chanState.LocalChanCfg.
RevocationBasePoint,
DoubleTweak: commitmentSecret,
WitnessScript: theirScript.WitnessScript,
Output: &wire.TxOut{
PkScript: theirScript.PkScript,
Value: theirAmt,
},
HashType: txscript.SigHashAll,
}
}
// Finally, with all the necessary data constructed, we can pad the
// BreachRetribution struct which houses all the data necessary to
// swiftly bring justice to the cheating remote party.
br.BreachHeight = breachHeight
br.RevokedStateNum = stateNum
br.LocalDelay = ourDelay
br.RemoteDelay = theirDelay
return br, nil
}
// createHtlcRetribution is a helper function to construct an HtlcRetribution
// based on the passed params.
func createHtlcRetribution(chanState *channeldb.OpenChannel,
keyRing *CommitmentKeyRing, commitHash chainhash.Hash,
commitmentSecret *btcec.PrivateKey, leaseExpiry uint32,
htlc *channeldb.HTLCEntry) (HtlcRetribution, error) {
var emptyRetribution HtlcRetribution
theirDelay := uint32(chanState.RemoteChanCfg.CsvDelay)
isRemoteInitiator := !chanState.IsInitiator
// We'll generate the original second level witness script now, as
// we'll need it if we're revoking an HTLC output on the remote
// commitment transaction, and *they* go to the second level.
secondLevelScript, err := SecondLevelHtlcScript(
chanState.ChanType, isRemoteInitiator,
keyRing.RevocationKey, keyRing.ToLocalKey, theirDelay,
leaseExpiry,
)
if err != nil {
return emptyRetribution, err
}
// If this is an incoming HTLC, then this means that they were the
// sender of the HTLC (relative to us). So we'll re-generate the sender
// HTLC script. Otherwise, is this was an outgoing HTLC that we sent,
// then from the PoV of the remote commitment state, they're the
// receiver of this HTLC.
htlcPkScript, htlcWitnessScript, err := genHtlcScript(
chanState.ChanType, htlc.Incoming, false,
htlc.RefundTimeout, htlc.RHash, keyRing,
)
if err != nil {
return emptyRetribution, err
}
return HtlcRetribution{
SignDesc: input.SignDescriptor{
KeyDesc: chanState.LocalChanCfg.
RevocationBasePoint,
DoubleTweak: commitmentSecret,
WitnessScript: htlcWitnessScript,
Output: &wire.TxOut{
PkScript: htlcPkScript,
Value: int64(htlc.Amt),
},
HashType: txscript.SigHashAll,
},
OutPoint: wire.OutPoint{
Hash: commitHash,
Index: uint32(htlc.OutputIndex),
},
SecondLevelWitnessScript: secondLevelScript.WitnessScript,
IsIncoming: htlc.Incoming,
}, nil
}
// createBreachRetribution creates a partially initiated BreachRetribution
// using a RevocationLog. Returns the constructed retribution, our amount,
// their amount, and a possible non-nil error.
func createBreachRetribution(revokedLog *channeldb.RevocationLog,
spendTx *wire.MsgTx, chanState *channeldb.OpenChannel,
keyRing *CommitmentKeyRing, commitmentSecret *btcec.PrivateKey,
leaseExpiry uint32) (*BreachRetribution, int64, int64, error) {
commitHash := revokedLog.CommitTxHash
// Create the htlc retributions.
htlcRetributions := make([]HtlcRetribution, len(revokedLog.HTLCEntries))
for i, htlc := range revokedLog.HTLCEntries {
hr, err := createHtlcRetribution(
chanState, keyRing, commitHash,
commitmentSecret, leaseExpiry, htlc,
)
if err != nil {
return nil, 0, 0, err
}
htlcRetributions[i] = hr
}
var ourAmt, theirAmt int64
// Construct the our outpoint.
ourOutpoint := wire.OutPoint{
Hash: commitHash,
}
if revokedLog.OurOutputIndex != channeldb.OutputIndexEmpty {
ourOutpoint.Index = uint32(revokedLog.OurOutputIndex)
// Sanity check that OurOutputIndex is within range.
if int(ourOutpoint.Index) >= len(spendTx.TxOut) {
return nil, 0, 0, fmt.Errorf("%w: ours=%v, "+
"len(TxOut)=%v", ErrOutputIndexOutOfRange,
ourOutpoint.Index, len(spendTx.TxOut),
)
}
// Read the amounts from the breach transaction.
//
// NOTE: ourAmt here includes commit fee and anchor amount(if
// enabled).
ourAmt = spendTx.TxOut[ourOutpoint.Index].Value
}
// Construct the their outpoint.
theirOutpoint := wire.OutPoint{
Hash: commitHash,
}
if revokedLog.TheirOutputIndex != channeldb.OutputIndexEmpty {
theirOutpoint.Index = uint32(revokedLog.TheirOutputIndex)
// Sanity check that TheirOutputIndex is within range.
if int(revokedLog.TheirOutputIndex) >= len(spendTx.TxOut) {
return nil, 0, 0, fmt.Errorf("%w: theirs=%v, "+
"len(TxOut)=%v", ErrOutputIndexOutOfRange,
revokedLog.TheirOutputIndex, len(spendTx.TxOut),
)
}
// Read the amounts from the breach transaction.
theirAmt = spendTx.TxOut[theirOutpoint.Index].Value
}
return &BreachRetribution{
BreachTxHash: commitHash,
ChainHash: chanState.ChainHash,
LocalOutpoint: ourOutpoint,
RemoteOutpoint: theirOutpoint,
HtlcRetributions: htlcRetributions,
KeyRing: keyRing,
}, ourAmt, theirAmt, nil
}
// createBreachRetributionLegacy creates a partially initiated
// BreachRetribution using a ChannelCommitment. Returns the constructed
// retribution, our amount, their amount, and a possible non-nil error.
func createBreachRetributionLegacy(revokedLog *channeldb.ChannelCommitment,
chanState *channeldb.OpenChannel, keyRing *CommitmentKeyRing,
commitmentSecret *btcec.PrivateKey,
ourScript, theirScript *ScriptInfo,
leaseExpiry uint32) (*BreachRetribution, int64, int64, error) {
commitHash := revokedLog.CommitTx.TxHash()
ourOutpoint := wire.OutPoint{
Hash: commitHash,
}
theirOutpoint := wire.OutPoint{
Hash: commitHash,
}
for i, txOut := range revokedSnapshot.CommitTx.TxOut {
// In order to fully populate the breach retribution struct, we'll need
// to find the exact index of the commitment outputs.
for i, txOut := range revokedLog.CommitTx.TxOut {
switch {
case bytes.Equal(txOut.PkScript, ourScript.PkScript):
ourOutpoint.Index = uint32(i)
@@ -2352,127 +2593,52 @@ func NewBreachRetribution(chanState *channeldb.OpenChannel, stateNum uint64,
}
}
// Conditionally instantiate a sign descriptor for each of the
// commitment outputs. If either is considered dust using the remote
// party's dust limit, the respective sign descriptor will be nil.
var (
ourSignDesc *input.SignDescriptor
theirSignDesc *input.SignDescriptor
)
// Compute the balances in satoshis.
ourAmt := revokedSnapshot.LocalBalance.ToSatoshis()
theirAmt := revokedSnapshot.RemoteBalance.ToSatoshis()
// If our balance exceeds the remote party's dust limit, instantiate
// the sign descriptor for our output.
if ourAmt >= chanState.RemoteChanCfg.DustLimit {
ourSignDesc = &input.SignDescriptor{
SingleTweak: keyRing.LocalCommitKeyTweak,
KeyDesc: chanState.LocalChanCfg.PaymentBasePoint,
WitnessScript: ourScript.WitnessScript,
Output: &wire.TxOut{
PkScript: ourScript.PkScript,
Value: int64(ourAmt),
},
HashType: txscript.SigHashAll,
}
}
// Similarly, if their balance exceeds the remote party's dust limit,
// assemble the sign descriptor for their output, which we can sweep.
if theirAmt >= chanState.RemoteChanCfg.DustLimit {
theirSignDesc = &input.SignDescriptor{
KeyDesc: chanState.LocalChanCfg.RevocationBasePoint,
DoubleTweak: commitmentSecret,
WitnessScript: theirScript.WitnessScript,
Output: &wire.TxOut{
PkScript: theirScript.PkScript,
Value: int64(theirAmt),
},
HashType: txscript.SigHashAll,
}
}
// With the commitment outputs located, we'll now generate all the
// retribution structs for each of the HTLC transactions active on the
// remote commitment transaction.
htlcRetributions := make([]HtlcRetribution, 0, len(revokedSnapshot.Htlcs))
for _, htlc := range revokedSnapshot.Htlcs {
htlcRetributions := make([]HtlcRetribution, len(revokedLog.Htlcs))
for i, htlc := range revokedLog.Htlcs {
// If the HTLC is dust, then we'll skip it as it doesn't have
// an output on the commitment transaction.
if HtlcIsDust(
chanState.ChanType, htlc.Incoming, false,
chainfee.SatPerKWeight(revokedSnapshot.FeePerKw),
htlc.Amt.ToSatoshis(), chanState.RemoteChanCfg.DustLimit,
chainfee.SatPerKWeight(revokedLog.FeePerKw),
htlc.Amt.ToSatoshis(),
chanState.RemoteChanCfg.DustLimit,
) {
continue
}
// We'll generate the original second level witness script now,
// as we'll need it if we're revoking an HTLC output on the
// remote commitment transaction, and *they* go to the second
// level.
secondLevelScript, err := SecondLevelHtlcScript(
chanState.ChanType, isRemoteInitiator,
keyRing.RevocationKey, keyRing.ToLocalKey, theirDelay,
leaseExpiry,
entry := &channeldb.HTLCEntry{
RHash: htlc.RHash,
RefundTimeout: htlc.RefundTimeout,
OutputIndex: uint16(htlc.OutputIndex),
Incoming: htlc.Incoming,
Amt: htlc.Amt.ToSatoshis(),
}
hr, err := createHtlcRetribution(
chanState, keyRing, commitHash,
commitmentSecret, leaseExpiry, entry,
)
if err != nil {
return nil, err
return nil, 0, 0, err
}
// If this is an incoming HTLC, then this means that they were
// the sender of the HTLC (relative to us). So we'll
// re-generate the sender HTLC script. Otherwise, is this was
// an outgoing HTLC that we sent, then from the PoV of the
// remote commitment state, they're the receiver of this HTLC.
htlcPkScript, htlcWitnessScript, err := genHtlcScript(
chanState.ChanType, htlc.Incoming, false,
htlc.RefundTimeout, htlc.RHash, keyRing,
)
if err != nil {
return nil, err
}
htlcRetributions = append(htlcRetributions, HtlcRetribution{
SignDesc: input.SignDescriptor{
KeyDesc: chanState.LocalChanCfg.RevocationBasePoint,
DoubleTweak: commitmentSecret,
WitnessScript: htlcWitnessScript,
Output: &wire.TxOut{
PkScript: htlcPkScript,
Value: int64(htlc.Amt.ToSatoshis()),
},
HashType: txscript.SigHashAll,
},
OutPoint: wire.OutPoint{
Hash: commitHash,
Index: uint32(htlc.OutputIndex),
},
SecondLevelWitnessScript: secondLevelScript.WitnessScript,
IsIncoming: htlc.Incoming,
})
htlcRetributions[i] = hr
}
// Finally, with all the necessary data constructed, we can create the
// BreachRetribution struct which houses all the data necessary to
// swiftly bring justice to the cheating remote party.
// Compute the balances in satoshis.
ourAmt := int64(revokedLog.LocalBalance.ToSatoshis())
theirAmt := int64(revokedLog.RemoteBalance.ToSatoshis())
return &BreachRetribution{
ChainHash: chanState.ChainHash,
BreachTransaction: revokedSnapshot.CommitTx,
BreachHeight: breachHeight,
RevokedStateNum: stateNum,
PendingHTLCs: revokedSnapshot.Htlcs,
LocalOutpoint: ourOutpoint,
LocalOutputSignDesc: ourSignDesc,
LocalDelay: ourDelay,
RemoteOutpoint: theirOutpoint,
RemoteOutputSignDesc: theirSignDesc,
RemoteDelay: theirDelay,
HtlcRetributions: htlcRetributions,
KeyRing: keyRing,
}, nil
BreachTxHash: commitHash,
ChainHash: chanState.ChainHash,
LocalOutpoint: ourOutpoint,
RemoteOutpoint: theirOutpoint,
HtlcRetributions: htlcRetributions,
KeyRing: keyRing,
}, ourAmt, theirAmt, nil
}
// HtlcIsDust determines if an HTLC output is dust or not depending on two
@@ -3822,7 +3988,7 @@ func (lc *LightningChannel) ProcessChanSyncMsg(
// but died before the signature was sent. We re-transmit our
// revocation, but also initiate a state transition to re-sync
// them.
if lc.OweCommitment(true) {
if lc.OweCommitment() {
commitSig, htlcSigs, _, err := lc.SignNextCommitment()
switch {
@@ -4536,11 +4702,11 @@ func (lc *LightningChannel) IsChannelClean() bool {
// out a commitment signature because there are outstanding local updates and/or
// updates in the local commit tx that aren't reflected in the remote commit tx
// yet.
func (lc *LightningChannel) OweCommitment(local bool) bool {
func (lc *LightningChannel) OweCommitment() bool {
lc.RLock()
defer lc.RUnlock()
return lc.oweCommitment(local)
return lc.oweCommitment(true)
}
// oweCommitment is the internal version of OweCommitment. This function expects
@@ -4874,12 +5040,28 @@ func (lc *LightningChannel) ReceiveRevocation(revMsg *lnwire.RevokeAndAck) (
source, remoteChainTail, addUpdates, settleFailUpdates,
)
// We will soon be saving the current remote commitment to revocation
// log bucket, which is `lc.channelState.RemoteCommitment`. After that,
// the `RemoteCommitment` will be replaced with a newer version found
// in `CommitDiff`. Thus we need to compute the output indexes here
// before the change since the indexes are meant for the current,
// revoked remote commitment.
ourOutputIndex, theirOutputIndex, err := findOutputIndexesFromRemote(
revocation, lc.channelState,
)
if err != nil {
return nil, nil, nil, nil, err
}
// At this point, the revocation has been accepted, and we've rotated
// the current revocation key+hash for the remote party. Therefore we
// sync now to ensure the revocation producer state is consistent with
// the current commitment height and also to advance the on-disk
// commitment chain.
err = lc.channelState.AdvanceCommitChainTail(fwdPkg, localPeerUpdates)
err = lc.channelState.AdvanceCommitChainTail(
fwdPkg, localPeerUpdates,
ourOutputIndex, theirOutputIndex,
)
if err != nil {
return nil, nil, nil, nil, err
}