nursery_store: refactors ifaces to be more atomic

Also includes:
 * improved error handling when pruning channels
 * more aggressively signals errors when enumerating height outputs
 * removes use of variadic functions in Incubate, AwardDiplomas
 * renames AwardDiplomas to GraduateKinder
 * short circuits channel maturity test after finding 1 non-grad output
 * replaces prefixed outputs in height buckets with files, instead of
     empty buckets
This commit is contained in:
Conner Fromknecht 2017-10-12 05:03:25 -07:00
parent f02f1355e7
commit 23e36a58f0
No known key found for this signature in database
GPG Key ID: 39DE78FBE6ACB0EF

@ -16,22 +16,11 @@ import (
// its contents, and persisting state transitions detected by the utxo nursery. // its contents, and persisting state transitions detected by the utxo nursery.
type NurseryStore interface { type NurseryStore interface {
// Incubation Entry Points. // Incubate registers a commitment output and a slice of htlc outputs to
// be swept back into the user's wallet. The event is persisted to disk,
// EnterCrib accepts a new htlc output that the nursery will incubate // such that the nursery can resume the incubation process after a
// through its two-stage process of sweeping funds back to the user's // potential crash.
// wallet. These outputs are persisted in the nursery store's crib Incubate(*kidOutput, []babyOutput) error
// bucket, and will be revisited after the output's CLTV has expired.
EnterCrib(*babyOutput) error
// EnterPreschool accepts a new commitment output that the nursery will
// incubate through a single stage before sweeping. Outputs are stored
// in the preschool bucket until the commitment transaction has been
// confirmed, at which point they will be moved to the kindergarten
// bucket.
EnterPreschool(*kidOutput) error
// On-chain Driven State Transtitions.
// CribToKinder atomically moves a babyOutput in the crib bucket to the // CribToKinder atomically moves a babyOutput in the crib bucket to the
// kindergarten bucket. The now mature kidOutput contained in the // kindergarten bucket. The now mature kidOutput contained in the
@ -44,40 +33,34 @@ type NurseryStore interface {
// transaction. // transaction.
PreschoolToKinder(*kidOutput) error PreschoolToKinder(*kidOutput) error
// AwardDiplomas accepts a variadic number of kidOutputs from the // GraduateKinder accepts a slice of kidOutputs from the kindergarten
// kindergarten bucket, and removes their corresponding entries from the // bucket, and removes their corresponding entries from the height and
// height and channel indexes. If this method detects that all outputs // channel indexes. If this method detects that all outputs for a
// for a particular contract have been incubated, it returns the channel // particular contract have been incubated, it returns the channel
// points that are ready to be marked as fully closed. // points that are ready to be marked as fully closed.
// TODO: make this handle one output at a time? // TODO: make this handle one output at a time?
AwardDiplomas(...kidOutput) ([]wire.OutPoint, error) GraduateKinder([]kidOutput) error
IsMatureChannel(*wire.OutPoint) (bool, error) // FinalizeHeight accepts a block height as a parameter and purges its
// TryFinalizeClass accepts a block height as a parameter and purges its
// persistent state for all outputs at that height. During a restart, // persistent state for all outputs at that height. During a restart,
// the utxo nursery will begin it's recovery procedure from the next // the utxo nursery will begin it's recovery procedure from the next
// height that has yet to be finalized. This block height should lag // height that has yet to be finalized. This block height should lag
// beyond the best height for this chain as a measure of reorg // beyond the best height for this chain as a measure of reorg
// protection. // protection.
TryFinalizeClass(height uint32) error FinalizeHeight(height uint32) error
// State Bucket Enumeration. // LastFinalizedHeight returns the last block height for which the
// nursery store has purged all persistent state.
LastFinalizedHeight() (uint32, error)
// FetchCribs returns a list of babyOutputs in the crib bucket whose // FetchClass returns a list of babyOutputs in the crib bucket whose
// CLTV delay expires at the provided block height. // CLTV delay expires at the provided block height.
FetchCribs(height uint32) ([]babyOutput, error) FetchClass(height uint32) ([]kidOutput, []babyOutput, error)
// FetchKindergartens returns a list of kidOutputs in the kindergarten
// bucket whose CSV delay expires at the provided block height.
FetchKindergartens(height uint32) ([]kidOutput, error)
// FetchPreschools returns a list of all outputs currently stored in the // FetchPreschools returns a list of all outputs currently stored in the
// preschool bucket. // preschool bucket.
FetchPreschools() ([]kidOutput, error) FetchPreschools() ([]kidOutput, error)
// Channel Output Enumeration.
// ForChanOutputs iterates over all outputs being incubated for a // ForChanOutputs iterates over all outputs being incubated for a
// particular channel point. This method accepts a callback that allows // particular channel point. This method accepts a callback that allows
// the caller to process each key-value pair. The key will be a prefixed // the caller to process each key-value pair. The key will be a prefixed
@ -85,13 +68,13 @@ type NurseryStore interface {
// whose type should be inferred from the key's prefix. // whose type should be inferred from the key's prefix.
ForChanOutputs(*wire.OutPoint, func([]byte, []byte) error) error ForChanOutputs(*wire.OutPoint, func([]byte, []byte) error) error
// IsMatureChannel determines the whether or not all of the outputs in a
// particular channel bucket have been marked as graduated.
IsMatureChannel(*wire.OutPoint) (bool, error)
// RemoveChannel channel erases all entries from the channel bucket for
// the provided channel point.
RemoveChannel(*wire.OutPoint) error RemoveChannel(*wire.OutPoint) error
// The Point of No Return.
// LastFinalizedHeight returns the last block height for which the
// nursery store has purged all persistent state.
LastFinalizedHeight() (uint32, error)
} }
// prefixChainKey creates the root level keys for the nursery store. The keys // prefixChainKey creates the root level keys for the nursery store. The keys
@ -169,6 +152,10 @@ var (
// timelock. // timelock.
kndrPrefix = []byte("kndr") kndrPrefix = []byte("kndr")
// gradPrefix is the state prefix given to all outputs that have been
// completely incubated. Once all outputs have been marked as graduated,
// this serves as a persistent marker that the nursery should mark the
// channel fully closed in the channeldb.
gradPrefix = []byte("grad") gradPrefix = []byte("grad")
) )
@ -197,7 +184,7 @@ var (
// | // |
// | The channel index contains a directory for each channel that has a // | The channel index contains a directory for each channel that has a
// | non-zero number of outputs being tracked by the nursery store. // | non-zero number of outputs being tracked by the nursery store.
// | Inside each channel directory are files contains serialized spendable // | Inside each channel directory are files containing serialized spendable
// | outputs that are awaiting some state transition. The name of each file // | outputs that are awaiting some state transition. The name of each file
// | contains the outpoint of the spendable output in the file, and is // | contains the outpoint of the spendable output in the file, and is
// | prefixed with 4-byte state prefix, indicating whether the spendable // | prefixed with 4-byte state prefix, indicating whether the spendable
@ -206,12 +193,12 @@ var (
// | which is useful in constructing nursery reports. // | which is useful in constructing nursery reports.
// | // |
// ├── channel-index-key/ // ├── channel-index-key/
// │   ├── <chain-point-1>/ <- CHANNEL BUCKET // │   ├── <chan-point-1>/ <- CHANNEL BUCKET
// | |   ├── <state-prefix><outpoint-1>: <spendable-output-1> // | |   ├── <state-prefix><outpoint-1>: <spendable-output-1>
// | |   └── <state-prefix><outpoint-2>: <spendable-output-2> // | |   └── <state-prefix><outpoint-2>: <spendable-output-2>
// │   ├── <chain-point-2>/ // │   ├── <chan-point-2>/
// | |   └── <state-prefix><outpoint-3>: <spendable-output-3> // | |   └── <state-prefix><outpoint-3>: <spendable-output-3>
// │   └── <chain-point-3>/ // │   └── <chan-point-3>/
// |    ├── <state-prefix><outpoint-4>: <spendable-output-4> // |    ├── <state-prefix><outpoint-4>: <spendable-output-4>
// |    └── <state-prefix><outpoint-5>: <spendable-output-5> // |    └── <state-prefix><outpoint-5>: <spendable-output-5>
// | // |
@ -219,13 +206,12 @@ var (
// | // |
// | The height index contains a directory for each height at which the // | The height index contains a directory for each height at which the
// | nursery still has uncompleted actions. If an output is a crib or // | nursery still has uncompleted actions. If an output is a crib or
// | kindergarten output, // | kindergarten output, it will have an associated entry in the height
// | it will have an associated entry in the height index. Inside a // | index. Inside a particular height directory, the structure is similar
// | particular height directory, the structure is similar to that of the // | to that of the channel index, containing multiple channel directories,
// | channel index, containing multiple channel directories, each of which // | each of which contains subdirectories named with a prefixed outpoint
// | contains subdirectories named with a prefixed outpoint belonging to // | belonging to the channel. Enumerating these combinations yields a
// | the channel. Enumerating these combinations yields a relative file // | relative file path:
// | path:
// | e.g. <chan-point-3>/<prefix><outpoint-2>/ // | e.g. <chan-point-3>/<prefix><outpoint-2>/
// | that can be queried in the channel index to retrieve the serialized // | that can be queried in the channel index to retrieve the serialized
// | output. // | output.
@ -233,22 +219,22 @@ var (
// └── height-index-key/ // └── height-index-key/
//    ├── <height-1>/ <- HEIGHT BUCKET //    ├── <height-1>/ <- HEIGHT BUCKET
// |   └── <chan-point-3>/ <- HEIGHT-CHANNEL BUCKET // |   └── <chan-point-3>/ <- HEIGHT-CHANNEL BUCKET
// | |    ├── <state-prefix><outpoint-4>/ <- PREFIXED OUTPOINT // | |    ├── <state-prefix><outpoint-4>: "" <- PREFIXED OUTPOINT
// | |    └── <state-prefix><outpoint-5>/ // | |    └── <state-prefix><outpoint-5>: ""
// |   └── <chan-point-2>/ // |   └── <chan-point-2>/
// |    └── <state-prefix><outpoint-3>/ // |    └── <state-prefix><outpoint-3>: ""
//    └── <height-2>/ //    └── <height-2>/
//    └── <chan-point-1>/ //    └── <chan-point-1>/
//    └── <state-prefix><outpoint-1>/ //    └── <state-prefix><outpoint-1>: ""
//    └── <state-prefix><outpoint-2>/ //    └── <state-prefix><outpoint-2>: ""
// nurseryStore is a concrete instantiation of a NurseryStore that is backed by // nurseryStore is a concrete instantiation of a NurseryStore that is backed by
// a channeldb.DB instance. // a channeldb.DB instance.
type nurseryStore struct { type nurseryStore struct {
chainHash chainhash.Hash chainHash chainhash.Hash
pfxChainKey []byte
db *channeldb.DB db *channeldb.DB
pfxChainKey []byte
} }
// newNurseryStore accepts a chain hash and a channeldb.DB instance, returning // newNurseryStore accepts a chain hash and a channeldb.DB instance, returning
@ -267,105 +253,33 @@ func newNurseryStore(chainHash *chainhash.Hash,
return &nurseryStore{ return &nurseryStore{
chainHash: *chainHash, chainHash: *chainHash,
pfxChainKey: pfxChainKey,
db: db, db: db,
pfxChainKey: pfxChainKey,
}, nil }, nil
} }
// Incubation Entry Points. // Incubate initiates the incubation process for the CSV-delayed commitment
// output and any number of CLTV-delayed htlc outputs.
// EnterCrib accepts a new htlc output that the nursery will incubate through func (ns *nurseryStore) Incubate(kid *kidOutput, babies []babyOutput) error {
// its two-stage process of sweeping funds back to the user's wallet. These
// outputs are persisted in the nursery store's crib bucket, and will be
// revisited after the output's CLTV has expired.
func (ns *nurseryStore) EnterCrib(bby *babyOutput) error {
return ns.db.Update(func(tx *bolt.Tx) error { return ns.db.Update(func(tx *bolt.Tx) error {
// Store commitment output in preschool bucket if not nil.
// First, retrieve or create the channel bucket corresponding to if kid != nil {
// the baby output's origin channel point. if err := ns.enterPreschool(tx, kid); err != nil {
chanPoint := bby.OriginChanPoint()
chanBucket, err := ns.createChannelBucket(tx, chanPoint)
if err != nil {
return err return err
} }
// Next, retrieve or create the height-channel bucket located in
// the height bucket corresponding to the baby output's CLTV
// expiry height.
hghtChanBucket, err := ns.createHeightChanBucket(tx,
bby.expiry, chanPoint)
if err != nil {
return err
} }
// Since we are inserting this output into the crib bucket, we // Add all htlc outputs to the crib bucket.
// create a key that prefixes the baby output's outpoint with for _, baby := range babies {
// the crib prefix. if err := ns.enterCrib(tx, &baby); err != nil {
pfxOutputKey, err := prefixOutputKey(cribPrefix, bby.OutPoint())
if err != nil {
return err return err
} }
// Serialize the baby output so that it can be written to the
// underlying key-value store.
var babyBuffer bytes.Buffer
if err := bby.Encode(&babyBuffer); err != nil {
return err
}
babyBytes := babyBuffer.Bytes()
// Now, insert the serialized output into its channel bucket
// under the prefixed key created above.
if err := chanBucket.Put(pfxOutputKey, babyBytes); err != nil {
return err
} }
// Finally, create a corresponding bucket in the height-channel return nil
// bucket for this crib output. The existence of this bucket
// indicates that the serialized output can be retrieved from
// the channel bucket using the same prefix key.
_, err = hghtChanBucket.CreateBucketIfNotExists(pfxOutputKey)
return err
}) })
} }
// EnterPreschool accepts a new commitment output that the nursery will
// incubate through a single stage before sweeping. Outputs are stored in the
// preschool bucket until the commitment transaction has been confirmed, at
// which point they will be moved to the kindergarten bucket.
func (ns *nurseryStore) EnterPreschool(kid *kidOutput) error {
return ns.db.Update(func(tx *bolt.Tx) error {
// First, retrieve or create the channel bucket corresponding to
// the baby output's origin channel point.
chanPoint := kid.OriginChanPoint()
chanBucket, err := ns.createChannelBucket(tx, chanPoint)
if err != nil {
return err
}
// Since the babyOutput is being inserted into the preschool
// bucket, we create a key that prefixes its outpoint with the
// preschool prefix.
pfxOutputKey, err := prefixOutputKey(psclPrefix, kid.OutPoint())
if err != nil {
return err
}
// Serialize the kidOutput and insert it into the channel
// bucket.
var kidBuffer bytes.Buffer
if err := kid.Encode(&kidBuffer); err != nil {
return err
}
return chanBucket.Put(pfxOutputKey, kidBuffer.Bytes())
})
}
// On-chain Drive State Transitions.
// CribToKinder atomically moves a babyOutput in the crib bucket to the // CribToKinder atomically moves a babyOutput in the crib bucket to the
// kindergarten bucket. The now mature kidOutput contained in the babyOutput // kindergarten bucket. The now mature kidOutput contained in the babyOutput
// will be stored as it waits out the kidOutput's CSV delay. // will be stored as it waits out the kidOutput's CSV delay.
@ -406,10 +320,22 @@ func (ns *nurseryStore) CribToKinder(bby *babyOutput) error {
// We successfully located an existing height chan // We successfully located an existing height chan
// bucket at this babyOutput's expiry height, proceed by // bucket at this babyOutput's expiry height, proceed by
// removing it from the index. // removing it from the index.
err := hghtChanBucketCltv.DeleteBucket(pfxOutputKey) err := hghtChanBucketCltv.Delete(pfxOutputKey)
if err != nil { if err != nil {
return err return err
} }
// Since we removed a crib output from the height index,
// we opportunistically prune the height bucket
// corresponding to the babyOutput's CLTV delay. This
// allows us to clean up any persistent state as outputs
// are progressed through the incubation process.
pruned, err := ns.pruneHeight(tx, bby.expiry)
if err != nil && err != ErrBucketNotEmpty {
return err
} else if err == nil && pruned {
utxnLog.Infof("Height bucket %d pruned", bby.expiry)
}
} }
// Since we are moving this output from the crib bucket to the // Since we are moving this output from the crib bucket to the
@ -449,25 +375,7 @@ func (ns *nurseryStore) CribToKinder(bby *babyOutput) error {
// height-channel bucket corresponding to its maturity height. // height-channel bucket corresponding to its maturity height.
// This informs the utxo nursery that it should attempt to spend // This informs the utxo nursery that it should attempt to spend
// this output when the blockchain reaches the maturity height. // this output when the blockchain reaches the maturity height.
_, err = hghtChanBucketCsv.CreateBucketIfNotExists(pfxOutputKey) return hghtChanBucketCsv.Put(pfxOutputKey, []byte{})
if err != nil {
return err
}
// Finally, since we removed a crib output from the height
// index, we opportunistically prune the height bucket
// corresponding to the babyOutput's CLTV delay. This allows us
// to clean up any persistent state as outputs are progressed
// through the incubation process.
err = ns.pruneHeight(tx, bby.expiry)
switch err {
case nil, ErrBucketDoesNotExist:
return nil
case ErrBucketNotEmpty:
return nil
default:
return err
}
}) })
} }
@ -550,7 +458,7 @@ func (ns *nurseryStore) PreschoolToKinder(kid *kidOutput) error {
}) })
} }
// AwardDiplomas accepts a list of kidOutputs in the kindergarten bucket, // GraduateKinder accepts a list of kidOutputs in the kindergarten bucket,
// removing their corresponding entries from the height and channel indexes. // removing their corresponding entries from the height and channel indexes.
// If this method detects that all outputs for a particular contract have been // If this method detects that all outputs for a particular contract have been
// incubated, it returns the channel points that are ready to be marked as // incubated, it returns the channel points that are ready to be marked as
@ -560,16 +468,8 @@ func (ns *nurseryStore) PreschoolToKinder(kid *kidOutput) error {
// empty. // empty.
// 2) Prune the channel bucket belonging to the kid's origin channel point, if // 2) Prune the channel bucket belonging to the kid's origin channel point, if
// it is empty. // it is empty.
func (ns *nurseryStore) AwardDiplomas( func (ns *nurseryStore) GraduateKinder(kids []kidOutput) error {
kids ...kidOutput) ([]wire.OutPoint, error) {
// As we iterate over the kids, we will build a list of the channels
// which have been pruned entirely from the nursery store. We will
// return this list to the caller, the utxo nursery, so that it can
// proceed to mark the channels as closed.
// TODO(conner): write list of closed channels to separate bucket so
// that they can be replayed on restart?
var possibleCloseSet = make(map[wire.OutPoint]struct{})
if err := ns.db.Update(func(tx *bolt.Tx) error { if err := ns.db.Update(func(tx *bolt.Tx) error {
for _, kid := range kids { for _, kid := range kids {
@ -578,40 +478,29 @@ func (ns *nurseryStore) AwardDiplomas(
outpoint := kid.OutPoint() outpoint := kid.OutPoint()
chanPoint := kid.OriginChanPoint() chanPoint := kid.OriginChanPoint()
// Remove output from kindergarten bucket. // Construct the key under which the output is currently
// stored height and channel indexes.
pfxOutputKey, err := prefixOutputKey(kndrPrefix, outpoint) pfxOutputKey, err := prefixOutputKey(kndrPrefix, outpoint)
if err != nil { if err != nil {
return err return err
} }
// Load the height-channel bucket, remove this output,
// and attempt to prune the bucket if it empty.
hghtChanBucket := ns.getHeightChanBucket(tx, confHeight, chanPoint) hghtChanBucket := ns.getHeightChanBucket(tx, confHeight, chanPoint)
if hghtChanBucket != nil { if hghtChanBucket != nil {
if err := hghtChanBucket.DeleteBucket(pfxOutputKey); err != nil { if err := hghtChanBucket.Delete(pfxOutputKey); err != nil {
return err return err
} }
// Attempt to prune the height bucket matching the kid // Attempt to prune the height bucket matching the kid
// output's confirmation height if it contains no active // output's confirmation height if it contains no active
// outputs. // outputs.
err := ns.pruneHeight(tx, confHeight) pruned, err := ns.pruneHeight(tx, confHeight)
switch err { if err != nil && err != ErrBucketNotEmpty {
case ErrBucketNotEmpty:
// Bucket still has active outputs, proceed to
// prune channel bucket.
case ErrBucketDoesNotExist:
// Bucket was previously pruned by another
// graduating output.
case nil:
// Bucket was pruned successfully and no errors
// were encounter.
utxnLog.Infof("Height bucket %d pruned", confHeight)
default:
// Unexpected database error.
return err return err
} else if err == nil && pruned {
utxnLog.Infof("Height bucket %d pruned", confHeight)
} }
} }
@ -620,6 +509,7 @@ func (ns *nurseryStore) AwardDiplomas(
return err return err
} }
// Remove previous output with kindergarten prefix.
if err := chanBucket.Delete(pfxOutputKey); err != nil { if err := chanBucket.Delete(pfxOutputKey); err != nil {
return err return err
} }
@ -632,104 +522,75 @@ func (ns *nurseryStore) AwardDiplomas(
return err return err
} }
// Insert serialized output into channel bucket using
// kindergarten-prefixed key.
err = chanBucket.Put(pfxOutputKey, gradBuffer.Bytes()) err = chanBucket.Put(pfxOutputKey, gradBuffer.Bytes())
if err != nil { if err != nil {
return err return err
} }
// If we've arrived here, we have encountered no
// database errors and a bucket was either successfully
// pruned or already has been. Thus it is safe to add it
// to our set of closed channels to be closed, since
// these may need to be replayed to ensure the channel
// database is aware that incubation has completed.
possibleCloseSet[*chanPoint] = struct{}{}
} }
return nil return nil
}); err != nil { }); err != nil {
return nil, err return err
} }
// Convert our set of channels to be closed into a list. return nil
possibleCloses := make([]wire.OutPoint, 0, len(possibleCloseSet))
for chanPoint := range possibleCloseSet {
possibleCloses = append(possibleCloses, chanPoint)
}
utxnLog.Infof("Possible channels to be marked fully closed: %v",
possibleCloses)
return possibleCloses, nil
} }
// TryFinalizeClass accepts a block height as a parameter and purges its // FinalizeHeight accepts a block height as a parameter and purges its
// persistent state for all outputs at that height. During a restart, the utxo // persistent state for all outputs at that height. During a restart, the utxo
// nursery will begin it's recovery procedure from the next height that has // nursery will begin it's recovery procedure from the next height that has
// yet to be finalized. // yet to be finalized.
func (ns *nurseryStore) TryFinalizeClass(height uint32) error { func (ns *nurseryStore) FinalizeHeight(height uint32) error {
return ns.db.Update(func(tx *bolt.Tx) error { return ns.db.Update(func(tx *bolt.Tx) error {
utxnLog.Infof("Attempting to finalize class at height %v", height) if err := ns.deleteHeightBucket(tx, height); err != nil {
lastHeight, err := ns.getNextLastFinalizedHeight(tx, height)
if err != nil {
return err return err
} }
utxnLog.Infof("Finalizing class at height %v", lastHeight)
return ns.putLastFinalizedHeight(tx, lastHeight) return ns.putLastFinalizedHeight(tx, height)
}) })
} }
// State Bucket Enumeration. // FetchClass returns a list of babyOutputs in the crib bucket whose CLTV
// FetchCribs returns a list of babyOutputs in the crib bucket whose CLTV
// delay expires at the provided block height. // delay expires at the provided block height.
func (ns *nurseryStore) FetchCribs(height uint32) ([]babyOutput, error) { func (ns *nurseryStore) FetchClass(height uint32) ([]kidOutput, []babyOutput, error) {
// Construct a list of all babyOutputs that need TLC at the provided // Construct list of all crib and kindergarten outputs that need TLC at
// block height. // the provided block height.
var kids []kidOutput
var babies []babyOutput var babies []babyOutput
if err := ns.forEachHeightPrefix(cribPrefix, height, if err := ns.db.View(func(tx *bolt.Tx) error {
func(buf []byte) error { if err := ns.forEachHeightPrefix(tx, cribPrefix, height, func(buf []byte) error {
// We will attempt to deserialize all outputs stored // We will attempt to deserialize all outputs
// with the crib prefix into babyOutputs, since this is // stored with the crib prefix into babyOutputs,
// the expected type that would have been serialized // since this is the expected type that would
// previously. // have been serialized previously.
var bby babyOutput var baby babyOutput
if err := bby.Decode(bytes.NewReader(buf)); err != nil { babyReader := bytes.NewReader(buf)
if err := baby.Decode(babyReader); err != nil {
return err return err
} }
// Append the deserialized object to our list of // Append the deserialized object to our list of
// babyOutputs. // babyOutputs.
babies = append(babies, bby) babies = append(babies, baby)
return nil return nil
}); err != nil { }); err != nil {
return nil, err return err
} }
return babies, nil return ns.forEachHeightPrefix(tx, kndrPrefix, height, func(buf []byte) error {
}
// FetchKindergartens returns a list of kidOutputs in the kindergarten bucket
// whose CSV delay expires at the provided block height.
func (ns *nurseryStore) FetchKindergartens(height uint32) ([]kidOutput, error) {
// Construct a list of all kidOutputs that mature at the provided block
// height.
utxnLog.Infof("Fetching kinders")
var kids []kidOutput
if err := ns.forEachHeightPrefix(kndrPrefix, height,
func(buf []byte) error {
utxnLog.Infof("Inside kinder")
// We will attempt to deserialize all outputs stored // We will attempt to deserialize all outputs stored
// with the kindergarten prefix into kidOutputs, since // with the kindergarten prefix into kidOutputs, since
// this is the expected type that would have been // this is the expected type that would have been
// serialized previously. // serialized previously.
var kid kidOutput var kid kidOutput
if err := kid.Decode(bytes.NewReader(buf)); err != nil { kidReader := bytes.NewReader(buf)
if err := kid.Decode(kidReader); err != nil {
return err return err
} }
@ -739,12 +600,13 @@ func (ns *nurseryStore) FetchKindergartens(height uint32) ([]kidOutput, error) {
return nil return nil
}); err != nil { })
return nil, err
}
utxnLog.Infof("Returning kinders")
return kids, nil }); err != nil {
return nil, nil, err
}
return kids, babies, nil
} }
// FetchPreschools returns a list of all outputs currently stored in the // FetchPreschools returns a list of all outputs currently stored in the
@ -827,8 +689,6 @@ func (ns *nurseryStore) FetchPreschools() ([]kidOutput, error) {
return kids, nil return kids, nil
} }
// Channel Output Enumberation.
// ForChanOutputs iterates over all outputs being incubated for a particular // ForChanOutputs iterates over all outputs being incubated for a particular
// channel point. This method accepts a callback that allows the caller to // channel point. This method accepts a callback that allows the caller to
// process each key-value pair. The key will be a prefixed outpoint, and the // process each key-value pair. The key will be a prefixed outpoint, and the
@ -843,150 +703,158 @@ func (ns *nurseryStore) ForChanOutputs(chanPoint *wire.OutPoint,
return ns.forChanOutputs(tx, chanPoint, callback) return ns.forChanOutputs(tx, chanPoint, callback)
}) })
} }
func (ns *nurseryStore) forChanOutputs(tx *bolt.Tx, chanPoint *wire.OutPoint,
callback func([]byte, []byte) error) error {
chanBucket := ns.getChannelBucket(tx, chanPoint) // errImmatureChannel signals that not all outputs in a channel bucket have
if chanBucket == nil { // graduated.
return ErrContractNotFound var errImmatureChannel = errors.New("channel has non-graduated outputs")
// IsMatureChannel determines the whether or not all of the outputs in a
// particular channel bucket have been marked as graduated.
func (ns *nurseryStore) IsMatureChannel(chanPoint *wire.OutPoint) (bool, error) {
if err := ns.db.View(func(tx *bolt.Tx) error {
// Iterate over the contents of the channel bucket, computing
// both total number of outputs, and those that have the grad
// prefix.
return ns.forChanOutputs(tx, chanPoint, func(pfxKey, _ []byte) error {
if string(pfxKey[:4]) != string(gradPrefix) {
utxnLog.Infof("Found non-graduated output: %x", pfxKey)
return errImmatureChannel
}
return nil
})
}); err != nil && err != errImmatureChannel {
return false, err
} else {
return err == nil, nil
} }
return chanBucket.ForEach(callback)
} }
func (ns *nurseryStore) IsMatureChannel(chanPoint *wire.OutPoint) (bool, error) { // RemoveChannel channel erases all entries from the channel bucket for the
var isMature bool // provided channel point.
if err := ns.db.View(func(tx *bolt.Tx) error { func (ns *nurseryStore) RemoveChannel(chanPoint *wire.OutPoint) error {
var nOutputs, nGrads int return ns.db.Update(func(tx *bolt.Tx) error {
if err := ns.forChanOutputs(tx, chanPoint, func(pfxKey, _ []byte) error { // Retrieve the existing chain bucket for this nursery store.
// Count total and number of grad outputs chainBucket := tx.Bucket(ns.pfxChainKey)
if string(pfxKey[:4]) == string(gradPrefix) { if chainBucket == nil {
nGrads++
}
nOutputs++
return nil return nil
}
}); err != nil { // Retrieve the channel index stored in the chain bucket.
chanIndex := chainBucket.Bucket(channelIndexKey)
if chanIndex == nil {
return nil
}
// Serialize the provided channel point, such that we can delete
// the mature channel bucket.
var chanBuffer bytes.Buffer
if err := writeOutpoint(&chanBuffer, chanPoint); err != nil {
return err return err
} }
utxnLog.Infof("Found %d graduated outputs out of %d", nGrads, nOutputs) err := chanIndex.DeleteBucket(chanBuffer.Bytes())
// Channel is mature if all outputs are graduated. if err != nil {
if nGrads == nOutputs { return err
isMature = true
} }
return nil return nil
}); err != nil { })
return false, err
}
return isMature, nil
} }
// The Point of No Return.
// LastFinalizedHeight returns the last block height for which the nursery // LastFinalizedHeight returns the last block height for which the nursery
// store has purged all persistent state. This occurs after a fixed interval // store has purged all persistent state. This occurs after a fixed interval
// for reorg safety. // for reorg safety.
func (ns *nurseryStore) LastFinalizedHeight() (uint32, error) { func (ns *nurseryStore) LastFinalizedHeight() (uint32, error) {
var lastFinalizedHeight uint32 var lastFinalizedHeight uint32
err := ns.db.View(func(tx *bolt.Tx) error { err := ns.db.View(func(tx *bolt.Tx) error {
lastHeight, err := ns.getLastFinalizedHeight(tx) var err error
if err != nil { lastFinalizedHeight, err = ns.getLastFinalizedHeight(tx)
return err return err
}
lastFinalizedHeight = lastHeight
return nil
}) })
return lastFinalizedHeight, err return lastFinalizedHeight, err
} }
// getLastFinalizedHeight is a helper method that retrieves the last height for // Helper Methods
// which the database finalized its persistent state.
func (ns *nurseryStore) getLastFinalizedHeight(tx *bolt.Tx) (uint32, error) {
// Retrieve the chain bucket associated with the given nursery store.
chainBucket := tx.Bucket(ns.pfxChainKey)
if chainBucket == nil {
return 0, nil
}
// Lookup the last finalized height in the top-level chain bucket. // enterCrib accepts a new htlc output that the nursery will incubate through
heightBytes := chainBucket.Get(lastFinalizedHeightKey) // its two-stage process of sweeping funds back to the user's wallet. These
// outputs are persisted in the nursery store's crib bucket, and will be
// If the resulting bytes are not sized like a uint32, then we have // revisited after the output's CLTV has expired.
// never finalized, so we return 0. func (ns *nurseryStore) enterCrib(tx *bolt.Tx, baby *babyOutput) error {
if len(heightBytes) != 4 { // First, retrieve or create the channel bucket corresponding to the
return 0, nil // baby output's origin channel point.
} chanPoint := baby.OriginChanPoint()
chanBucket, err := ns.createChannelBucket(tx, chanPoint)
// Otherwise, parse the bytes and return the last finalized height.
return byteOrder.Uint32(heightBytes), nil
}
func (ns *nurseryStore) getNextLastFinalizedHeight(tx *bolt.Tx, height uint32) (uint32, error) {
// Retrieve the previous last finalized height.
lastHeight, err := ns.getLastFinalizedHeight(tx)
if err != nil {
return 0, err
}
// TODO(conner): add lower bound after which all state is purged.
// Retrieve the existing chain bucket for this nursery store.
chainBucket := tx.Bucket(ns.pfxChainKey)
if chainBucket == nil {
return lastHeight, nil
}
// Retrieve the existing height index.
hghtIndex := chainBucket.Bucket(heightIndexKey)
if hghtIndex == nil {
return lastHeight, nil
}
for curHeight := lastHeight + 1; curHeight <= height; curHeight++ {
var curHeightBytes [4]byte
byteOrder.PutUint32(curHeightBytes[:], curHeight)
hghtBucket := hghtIndex.Bucket(curHeightBytes[:])
if hghtBucket == nil {
continue
}
nChildren, err := ns.numChildrenInBucket(hghtBucket)
if err != nil {
return 0, err
}
if nChildren > 0 {
return curHeight - 1, nil
}
}
return height, nil
}
// pubLastFinalizedHeight is a helper method that writes the provided height
// under the last finalized height key.
func (ns *nurseryStore) putLastFinalizedHeight(tx *bolt.Tx,
height uint32) error {
// Ensure that the chain bucket for this nursery store exists.
chainBucket, err := tx.CreateBucketIfNotExists(ns.pfxChainKey)
if err != nil { if err != nil {
return err return err
} }
// Serialize the provided last-finalized height, and store it in the // Next, retrieve or create the height-channel bucket located in the
// top-level chain bucket for this nursery store. // height bucket corresponding to the baby output's CLTV expiry height.
var lastHeightBytes [4]byte hghtChanBucket, err := ns.createHeightChanBucket(tx,
byteOrder.PutUint32(lastHeightBytes[:], height) baby.expiry, chanPoint)
if err != nil {
return err
}
return chainBucket.Put(lastFinalizedHeightKey, lastHeightBytes[:]) // Since we are inserting this output into the crib bucket, we create a
// key that prefixes the baby output's outpoint with the crib prefix.
pfxOutputKey, err := prefixOutputKey(cribPrefix, baby.OutPoint())
if err != nil {
return err
}
// Serialize the baby output so that it can be written to the underlying
// key-value store.
var babyBuffer bytes.Buffer
if err := baby.Encode(&babyBuffer); err != nil {
return err
}
babyBytes := babyBuffer.Bytes()
// Now, insert the serialized output into its channel bucket under the
// prefixed key created above.
if err := chanBucket.Put(pfxOutputKey, babyBytes); err != nil {
return err
}
// Finally, create a corresponding bucket in the height-channel bucket
// for this crib output. The existence of this bucket indicates that the
// serialized output can be retrieved from the channel bucket using the
// same prefix key.
return hghtChanBucket.Put(pfxOutputKey, []byte{})
}
// enterPreschool accepts a new commitment output that the nursery will incubate
// through a single stage before sweeping. Outputs are stored in the preschool
// bucket until the commitment transaction has been confirmed, at which point
// they will be moved to the kindergarten bucket.
func (ns *nurseryStore) enterPreschool(tx *bolt.Tx, kid *kidOutput) error {
// First, retrieve or create the channel bucket corresponding to the
// baby output's origin channel point.
chanPoint := kid.OriginChanPoint()
chanBucket, err := ns.createChannelBucket(tx, chanPoint)
if err != nil {
return err
}
// Since the babyOutput is being inserted into the preschool bucket, we
// create a key that prefixes its outpoint with the preschool prefix.
pfxOutputKey, err := prefixOutputKey(psclPrefix, kid.OutPoint())
if err != nil {
return err
}
// Serialize the kidOutput and insert it into the channel bucket.
var kidBuffer bytes.Buffer
if err := kid.Encode(&kidBuffer); err != nil {
return err
}
return chanBucket.Put(pfxOutputKey, kidBuffer.Bytes())
} }
// createChannelBucket creates or retrieves a channel bucket for the provided // createChannelBucket creates or retrieves a channel bucket for the provided
@ -1074,22 +942,22 @@ func (ns *nurseryStore) createHeightBucket(tx *bolt.Tx,
return hghtIndex.CreateBucketIfNotExists(heightBytes[:]) return hghtIndex.CreateBucketIfNotExists(heightBytes[:])
} }
// getHeightBucket retrieves an existing height bucket from the nursery store, // getHeightBucketPath retrieves an existing height bucket from the nursery store,
// using the provided block height. If the bucket does not exist, or any bucket // using the provided block height. If the bucket does not exist, or any bucket
// along its path does not exist, a nil value is returned. // along its path does not exist, a nil value is returned.
func (ns *nurseryStore) getHeightBucket(tx *bolt.Tx, func (ns *nurseryStore) getHeightBucketPath(tx *bolt.Tx,
height uint32) *bolt.Bucket { height uint32) (*bolt.Bucket, *bolt.Bucket, *bolt.Bucket) {
// Retrieve the existing chain bucket for this nursery store. // Retrieve the existing chain bucket for this nursery store.
chainBucket := tx.Bucket(ns.pfxChainKey) chainBucket := tx.Bucket(ns.pfxChainKey)
if chainBucket == nil { if chainBucket == nil {
return nil return nil, nil, nil
} }
// Retrieve the existing channel index. // Retrieve the existing channel index.
hghtIndex := chainBucket.Bucket(heightIndexKey) hghtIndex := chainBucket.Bucket(heightIndexKey)
if hghtIndex == nil { if hghtIndex == nil {
return nil return nil, nil, nil
} }
// Serialize the provided block height and return the bucket matching // Serialize the provided block height and return the bucket matching
@ -1097,7 +965,47 @@ func (ns *nurseryStore) getHeightBucket(tx *bolt.Tx,
var heightBytes [4]byte var heightBytes [4]byte
byteOrder.PutUint32(heightBytes[:], height) byteOrder.PutUint32(heightBytes[:], height)
return hghtIndex.Bucket(heightBytes[:]) return chainBucket, hghtIndex, hghtIndex.Bucket(heightBytes[:])
}
// getHeightBucket retrieves an existing height bucket from the nursery store,
// using the provided block height. If the bucket does not exist, or any bucket
// along its path does not exist, a nil value is returned.
func (ns *nurseryStore) getHeightBucket(tx *bolt.Tx,
height uint32) *bolt.Bucket {
_, _, hghtBucket := ns.getHeightBucketPath(tx, height)
return hghtBucket
}
// deleteHeightBucket ensures that the height bucket at the provided index is
// purged from the nursery store.
func (ns *nurseryStore) deleteHeightBucket(tx *bolt.Tx, height uint32) error {
// Ensure that the chain bucket for this nursery store exists.
chainBucket := tx.Bucket(ns.pfxChainKey)
if chainBucket == nil {
return nil
}
// Ensure that the height index has been properly initialized for this
// chain.
hghtIndex := chainBucket.Bucket(heightIndexKey)
if hghtIndex == nil {
return nil
}
// Serialize the provided height, as this will form the name of the
// bucket.
var heightBytes [4]byte
byteOrder.PutUint32(heightBytes[:], height)
// Finally, delete the bucket in question.
err := hghtIndex.DeleteBucket(heightBytes[:])
if err != nil && err != bolt.ErrBucketNotFound {
return err
}
return nil
} }
// createHeightChanBucket creates or retrieves an existing height-channel bucket // createHeightChanBucket creates or retrieves an existing height-channel bucket
@ -1156,23 +1064,21 @@ func (ns *nurseryStore) getHeightChanBucket(tx *bolt.Tx,
// enumerate crib and kindergarten outputs at a particular height. The callback // enumerate crib and kindergarten outputs at a particular height. The callback
// is invoked with serialized bytes retrieved for each output of interest, // is invoked with serialized bytes retrieved for each output of interest,
// allowing the caller to deserialize them into the appropriate type. // allowing the caller to deserialize them into the appropriate type.
func (ns *nurseryStore) forEachHeightPrefix(prefix []byte, height uint32, func (ns *nurseryStore) forEachHeightPrefix(tx *bolt.Tx, prefix []byte,
callback func([]byte) error) error { height uint32, callback func([]byte) error) error {
return ns.db.View(func(tx *bolt.Tx) error { // Start by retrieving the height bucket corresponding to the provided
// Start by retrieving the height bucket corresponding to the // block height.
// provided block height. chainBucket, _, hghtBucket := ns.getHeightBucketPath(tx, height)
hghtBucket := ns.getHeightBucket(tx, height)
if hghtBucket == nil { if hghtBucket == nil {
return nil return nil
} }
// Using the height bucket as a starting point, we will traverse // Using the height bucket as a starting point, we will traverse its
// its entire two-tier directory structure, and filter for // entire two-tier directory structure, and filter for outputs that have
// outputs that have the provided prefix. The first layer of the // the provided prefix. The first layer of the height bucket contains
// height bucket contains buckets identified by a channel point, // buckets identified by a channel point, thus we first create list of
// thus we first create list of channels contained in this // channels contained in this height bucket.
// height bucket.
var channelsAtHeight [][]byte var channelsAtHeight [][]byte
if err := hghtBucket.ForEach(func(chanBytes, _ []byte) error { if err := hghtBucket.ForEach(func(chanBytes, _ []byte) error {
channelsAtHeight = append(channelsAtHeight, chanBytes) channelsAtHeight = append(channelsAtHeight, chanBytes)
@ -1181,83 +1087,126 @@ func (ns *nurseryStore) forEachHeightPrefix(prefix []byte, height uint32,
return err return err
} }
// As we enumerate the outputs referenced in this height bucket, // Additionally, grab the chain index, which we will facilitate queries
// we will need to load the serialized value of each output, // for each of the channel buckets of each of the channels in the list
// which is ultimately stored its respective channel bucket. To // we assembled above.
// do so, we first load the top-level chain bucket, which should
// already be created if we are at this point.
chainBucket := tx.Bucket(ns.pfxChainKey)
if chainBucket == nil {
return nil
}
// Additionally, grab the chain index, which we will facilitate
// queries for each of the channel buckets of each of the
// channels in the list we assembled above.
chanIndex := chainBucket.Bucket(channelIndexKey) chanIndex := chainBucket.Bucket(channelIndexKey)
if chanIndex == nil { if chanIndex == nil {
return nil return errors.New("unable to retrieve channel index")
} }
// Now, we are ready to enumerate all outputs with the desired // Now, we are ready to enumerate all outputs with the desired prefix at
// prefix t this block height. We do so by iterating over our // this block height. We do so by iterating over our list of channels at
// list of channels at this height, filtering for outputs in // this height, filtering for outputs in each height-channel bucket that
// each height-channel bucket that begin with the given prefix, // begin with the given prefix, and then retrieving the serialized
// and then retrieving the serialized outputs from the // outputs from the appropriate channel bucket.
// appropriate channel bucket.
for _, chanBytes := range channelsAtHeight { for _, chanBytes := range channelsAtHeight {
// Retrieve the height-channel bucket for this channel, // Retrieve the height-channel bucket for this channel, which
// which holds a sub-bucket for all outputs maturing at // holds a sub-bucket for all outputs maturing at this height.
// this height.
hghtChanBucket := hghtBucket.Bucket(chanBytes) hghtChanBucket := hghtBucket.Bucket(chanBytes)
if hghtChanBucket == nil { if hghtChanBucket == nil {
continue return errors.New("unable to retrieve height-channel bucket")
} }
// Load the appropriate channel bucket from the channel // Load the appropriate channel bucket from the channel index,
// index, this will allow us to retrieve the individual // this will allow us to retrieve the individual serialized
// serialized outputs. // outputs.
chanBucket := chanIndex.Bucket(chanBytes) chanBucket := chanIndex.Bucket(chanBytes)
if chanBucket == nil { if chanBucket == nil {
continue return errors.New("unable to retrieve channel bucket")
} }
// Since all of the outputs of interest will start with // Since all of the outputs of interest will start with the same
// the same prefix, we will perform a prefix scan of the // prefix, we will perform a prefix scan of the buckets
// buckets contained in the height-channel bucket, // contained in the height-channel bucket, efficiently
// efficiently enumerating the desired outputs. // enumerating the desired outputs.
c := hghtChanBucket.Cursor() c := hghtChanBucket.Cursor()
// Seek to and iterate over all entries starting with // Seek to and iterate over all entries starting with the given
// the given prefix. // prefix.
var pfxOutputKey, _ = c.Seek(prefix) pfxOutputKey, _ := c.Seek(prefix)
for bytes.HasPrefix(pfxOutputKey, prefix) { for bytes.HasPrefix(pfxOutputKey, prefix) {
// Use the prefix output key emitted from our // Use the prefix output key emitted from our scan to
// scan to load the serialized babyOutput from // load the serialized babyOutput from the appropriate
// the appropriate channel bucket. // channel bucket.
outputBytes := chanBucket.Get(pfxOutputKey) outputBytes := chanBucket.Get(pfxOutputKey)
if outputBytes == nil { if outputBytes == nil {
pfxOutputKey, _ = c.Next() return errors.New("unable to retrieve output")
continue
} }
// Present the serialized bytes to our call back // Present the serialized bytes to our call back
// function, which is responsible for // function, which is responsible for deserializing the
// deserializing the bytes into the appropriate // bytes into the appropriate type.
// type.
if err := callback(outputBytes); err != nil { if err := callback(outputBytes); err != nil {
return err return err
} }
// Lastly, advance our prefix output key for the // Lastly, advance our prefix output key for the next
// next iteration. // iteration.
pfxOutputKey, _ = c.Next() pfxOutputKey, _ = c.Next()
} }
} }
return nil return nil
}) }
// forChanOutputs enumerates the outputs contained in a channel bucket to the
// provided callback. The callback accepts a key-value pair of byte slices
// corresponding to the prefixed-output key and the serialized output,
// respectively.
func (ns *nurseryStore) forChanOutputs(tx *bolt.Tx, chanPoint *wire.OutPoint,
callback func([]byte, []byte) error) error {
chanBucket := ns.getChannelBucket(tx, chanPoint)
if chanBucket == nil {
return ErrContractNotFound
}
return chanBucket.ForEach(callback)
}
// getLastFinalizedHeight is a helper method that retrieves the last height for
// which the database finalized its persistent state.
func (ns *nurseryStore) getLastFinalizedHeight(tx *bolt.Tx) (uint32, error) {
// Retrieve the chain bucket associated with the given nursery store.
chainBucket := tx.Bucket(ns.pfxChainKey)
if chainBucket == nil {
return 0, nil
}
// Lookup the last finalized height in the top-level chain bucket.
heightBytes := chainBucket.Get(lastFinalizedHeightKey)
// If the resulting bytes are not sized like a uint32, then we have
// never finalized, so we return 0.
if len(heightBytes) != 4 {
return 0, nil
}
// Otherwise, parse the bytes and return the last finalized height.
return byteOrder.Uint32(heightBytes), nil
}
// pubLastFinalizedHeight is a helper method that writes the provided height
// under the last finalized height key.
func (ns *nurseryStore) putLastFinalizedHeight(tx *bolt.Tx,
height uint32) error {
// Ensure that the chain bucket for this nursery store exists.
chainBucket, err := tx.CreateBucketIfNotExists(ns.pfxChainKey)
if err != nil {
return err
}
// TODO(conner): purge all state below reorg depth.
// Serialize the provided last-finalized height, and store it in the
// top-level chain bucket for this nursery store.
var lastHeightBytes [4]byte
byteOrder.PutUint32(lastHeightBytes[:], height)
return chainBucket.Put(lastFinalizedHeightKey, lastHeightBytes[:])
} }
var ( var (
@ -1270,206 +1219,90 @@ var (
ErrBucketNotEmpty = errors.New("bucket is not empty, cannot be pruned") ErrBucketNotEmpty = errors.New("bucket is not empty, cannot be pruned")
) )
// deleteAndPruneHeight removes an output from a channel bucket matching the // pruneHeight removes the height bucket at the provided height if and only if
// provided channel point. The output is assumed top be in the kindergarten // all active outputs at this height have been removed from their respective
// bucket, since pruning should never occur for any other type of output. If // height-channel buckets.
// after deletion, the channel bucket is empty, this method will attempt to func (ns *nurseryStore) pruneHeight(tx *bolt.Tx, height uint32) (bool, error) {
// delete the bucket as well. // Fetch the existing height index and height bucket.
// NOTE: This method returns two concrete errors apart from those returned by _, hghtIndex, hghtBucket := ns.getHeightBucketPath(tx, height)
// the underlying database: ErrBucketDoesNotExist and ErrBucketNotEmpty. These
// should be handled in the context of the caller so as they may be benign
// depending on context. Errors returned other than these two should be
// interpreted as database errors.
func (ns *nurseryStore) deleteAndPruneChannel(tx *bolt.Tx,
chanPoint, outpoint *wire.OutPoint) error {
// Retrieve the existing chain bucket for this nursery store.
chainBucket := tx.Bucket(ns.pfxChainKey)
if chainBucket == nil {
return nil
}
// Retrieve the channel index stored in the chain bucket.
chanIndex := chainBucket.Bucket(channelIndexKey)
if chanIndex == nil {
return nil
}
// Serialize the provided channel point, such that we can retrieve the
// desired channel bucket.
var chanBuffer bytes.Buffer
if err := writeOutpoint(&chanBuffer, chanPoint); err != nil {
return err
}
chanBytes := chanBuffer.Bytes()
// Retrieve the existing channel bucket. If none exists, then our job is
// complete and it is safe to return nil.
chanBucket := chanIndex.Bucket(chanBytes)
if chanBucket == nil {
return nil
}
// Otherwise, the bucket still exists. Serialize the outpoint that needs
// deletion, prefixing the key with kindergarten prefix. Since all
// outputs eventually make their way to becoming kindergarten outputs,
// we can safely assume that they will be stored with a kindergarten
// prefix.
pfxOutputBytes, err := prefixOutputKey(kndrPrefix, outpoint)
if err != nil {
return err
}
// Remove the output in question using the kindergarten-prefixed key we
// generated above.
if err := chanBucket.Delete(pfxOutputBytes); err != nil {
return err
}
// Finally, now that the outpoint has been removed from this channel
// bucket, try to remove the channel bucket altogether if it is now
// empty.
return ns.removeBucketIfEmpty(chanIndex, chanBytes)
}
func (ns *nurseryStore) RemoveChannel(chanPoint *wire.OutPoint) error {
return ns.db.Update(func(tx *bolt.Tx) error {
// Retrieve the existing chain bucket for this nursery store.
chainBucket := tx.Bucket(ns.pfxChainKey)
if chainBucket == nil {
return nil
}
// Retrieve the channel index stored in the chain bucket.
chanIndex := chainBucket.Bucket(channelIndexKey)
if chanIndex == nil {
return nil
}
// Serialize the provided channel point, such that we can delete
// the mature channel bucket.
var chanBuffer bytes.Buffer
if err := writeOutpoint(&chanBuffer, chanPoint); err != nil {
return err
}
return chanIndex.DeleteBucket(chanBuffer.Bytes())
})
}
// pruneHeight
// NOTE: This method returns two concrete errors apart from those returned by
// the underlying database: ErrBucketDoesNotExist and ErrBucketNotEmpty. These
// should be handled in the context of the caller so as they may be benign
// depending on context. Errors returned other than these two should be
// interpreted as database errors.
func (ns *nurseryStore) pruneHeight(tx *bolt.Tx, height uint32) error {
// Fetch the existing chain bucket for this nursery store.
chainBucket := tx.Bucket(ns.pfxChainKey)
if chainBucket == nil {
return nil
}
// Load the existing height bucket for the height in question.
hghtIndex := chainBucket.Bucket(heightIndexKey)
if hghtIndex == nil {
return nil
}
// Serialize the provided block height, such that it can be used as the
// key to locate the desired height bucket.
var heightBytes [4]byte
byteOrder.PutUint32(heightBytes[:], height)
// Retrieve the height bucket using the serialized height as the bucket
// name.
hghtBucket := hghtIndex.Bucket(heightBytes[:])
if hghtBucket == nil { if hghtBucket == nil {
return ErrBucketDoesNotExist return false, nil
} }
// Iterate over the contents of this height bucket, which is comprised // TODO(conner): fix this comment
// of sub-buckets named after the channel points that need attention at
// this block height. We will attempt to remove each one if they are // this block height. We will attempt to remove each one if they are
// empty, keeping track of the number of height-channel buckets that // empty, keeping track of the number of height-channel buckets that
// still have active outputs. // still have active outputs.
var nActiveBuckets int var nActiveBuckets int
if err := hghtBucket.ForEach(func(chanBytes, _ []byte) error { if err := hghtBucket.ForEach(func(chanBytes, _ []byte) error {
// Attempt to each height-channel bucket from the height bucket // Attempt to each height-channel bucket from the height bucket
// located above. // located above.
err := ns.removeBucketIfEmpty(hghtBucket, chanBytes) _, err := ns.removeBucketIfEmpty(hghtBucket, chanBytes)
switch err { if err != nil && err != ErrBucketNotEmpty {
case nil:
// The height-channel bucket was removed successfully!
return nil
case ErrBucketDoesNotExist:
// The height-channel bucket could not be located--no
// harm, no foul.
return nil
case ErrBucketNotEmpty:
// The bucket still has active outputs at this height,
// increment our number of still active height-channel
// buckets.
nActiveBuckets++
return nil
default:
// Database error!
return err return err
} else if err == ErrBucketNotEmpty {
nActiveBuckets++
} }
return nil
}); err != nil { }); err != nil {
return err return false, err
} }
// If we located any height-channel buckets that still have active // If we located any height-channel buckets that still have active
// outputs, it is unsafe to delete this height bucket. Signal this event // outputs, it is unsafe to delete this height bucket. Signal this event
// to the caller so that they can determine the appropriate action. // to the caller so that they can determine the appropriate action.
if nActiveBuckets > 0 { if nActiveBuckets > 0 {
return ErrBucketNotEmpty return false, ErrBucketNotEmpty
} }
// Serialize the provided block height, such that it can be used as the
// key to delete desired height bucket.
var heightBytes [4]byte
byteOrder.PutUint32(heightBytes[:], height)
// All of the height-channel buckets are empty or have been previously // All of the height-channel buckets are empty or have been previously
// removed, proceed by attempting to remove the height bucket // removed, proceed by removing the height bucket
// altogether. // altogether.
return ns.removeBucketIfEmpty(hghtIndex, heightBytes[:]) if err := hghtIndex.DeleteBucket(heightBytes[:]); err != nil {
return false, err
}
return true, nil
} }
// removeBucketIfEmpty attempts to delete a bucket specified by name from the // removeBucketIfEmpty attempts to delete a bucket specified by name from the
// provided parent bucket. // provided parent bucket.
// NOTE: This method returns two concrete errors apart from those returned by
// the underlying database: ErrBucketDoesNotExist and ErrBucketNotEmpty. These
// should be handled in the context of the caller so as they may be benign
// depending on context. Errors returned other than these two should be
// interpreted as database errors.
func (ns *nurseryStore) removeBucketIfEmpty(parent *bolt.Bucket, func (ns *nurseryStore) removeBucketIfEmpty(parent *bolt.Bucket,
bktName []byte) error { bktName []byte) (bool, error) {
// Attempt to fetch the named bucket from its parent. // Attempt to fetch the named bucket from its parent.
bkt := parent.Bucket(bktName) bkt := parent.Bucket(bktName)
if bkt == nil { if bkt == nil {
// No bucket was found, signal this to the caller. // No bucket was found, signal this to the caller.
return ErrBucketDoesNotExist return false, nil
} }
// The bucket exists, now compute how many children *it* has. // The bucket exists, now compute how many children *it* has.
nChildren, err := ns.numChildrenInBucket(bkt) nChildren, err := ns.numChildrenInBucket(bkt)
if err != nil { if err != nil {
return err return false, nil
} }
// If the number of children is non-zero, alert the caller that the // If the number of children is non-zero, alert the caller that the
// named bucket is not being removed. // named bucket is not being removed.
if nChildren > 0 { if nChildren > 0 {
return ErrBucketNotEmpty return false, nil
} }
// Otherwise, remove the empty bucket from its parent. // Otherwise, remove the empty bucket from its parent.
return parent.DeleteBucket(bktName) err = parent.DeleteBucket(bktName)
if err != nil {
return false, err
}
return true, nil
} }
// numChildrenInBucket computes the number of children contained in the given // numChildrenInBucket computes the number of children contained in the given