Skip to content

[tapdb]: add script key type enum, run Golang based post migration checks #1198

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 19 commits into from
May 1, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/main.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ env:

GO_VERSION: '1.23.6'

LITD_ITEST_BRANCH: 'master'
LITD_ITEST_BRANCH: 'script-key-migrations'

jobs:
#######################
Expand Down
21 changes: 11 additions & 10 deletions address/book.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,13 +134,9 @@ type Storage interface {

// InsertScriptKey inserts an address related script key into the
// database, so it can be recognized as belonging to the wallet when a
// transfer comes in later on. The script key can be declared as known
// if it contains an internal key that isn't derived by the backing
// wallet (e.g. NUMS key) but it should still be recognized as a key
// being relevant for the local wallet (e.g. show assets received on
// this key in the asset list and balances).
// transfer comes in later on.
InsertScriptKey(ctx context.Context, scriptKey asset.ScriptKey,
declareAsKnown bool) error
keyType asset.ScriptKeyType) error
}

// KeyRing is used to create script and internal keys for Taproot Asset
Expand Down Expand Up @@ -401,7 +397,12 @@ func (b *Book) NewAddressWithKeys(ctx context.Context, addrVersion Version,
if err != nil {
return nil, fmt.Errorf("unable to insert internal key: %w", err)
}
err = b.cfg.Store.InsertScriptKey(ctx, scriptKey, true)

// We might not know the type of script key, if it was given to us
// through an RPC call. So we make a guess here.
keyType := scriptKey.DetermineType()

err = b.cfg.Store.InsertScriptKey(ctx, scriptKey, keyType)
if err != nil {
return nil, fmt.Errorf("unable to insert script key: %w", err)
}
Expand Down Expand Up @@ -438,9 +439,9 @@ func (b *Book) IsLocalKey(ctx context.Context,

// InsertScriptKey inserts an address related script key into the database.
func (b *Book) InsertScriptKey(ctx context.Context, scriptKey asset.ScriptKey,
declareAsKnown bool) error {
keyType asset.ScriptKeyType) error {

return b.cfg.Store.InsertScriptKey(ctx, scriptKey, declareAsKnown)
return b.cfg.Store.InsertScriptKey(ctx, scriptKey, keyType)
}

// NextInternalKey derives then inserts an internal key into the database to
Expand Down Expand Up @@ -475,7 +476,7 @@ func (b *Book) NextScriptKey(ctx context.Context,
}

scriptKey := asset.NewScriptKeyBip86(keyDesc)
err = b.cfg.Store.InsertScriptKey(ctx, scriptKey, true)
err = b.cfg.Store.InsertScriptKey(ctx, scriptKey, asset.ScriptKeyBip86)
if err != nil {
return asset.ScriptKey{}, err
}
Expand Down
114 changes: 94 additions & 20 deletions asset/asset.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,50 @@ const (
EncodeSegwit
)

// ScriptKeyType denotes the type of script key used for an asset. This type is
// serialized to the database, so we don't use iota for the values to ensure
// they don't change by accident.
type ScriptKeyType uint8

const (
// ScriptKeyUnknown is the default script key type used for assets that
// we don't know the type of. This should only be stored for assets
// where we don't know the internal key of the script key (e.g. for
// imported proofs).
ScriptKeyUnknown ScriptKeyType = 0

// ScriptKeyBip86 is the script key type used for assets that use the
// BIP86 style tweak (e.g. an empty tweak).
ScriptKeyBip86 ScriptKeyType = 1

// ScriptKeyScriptPathExternal is the script key type used for assets
// that use a script path that is defined by an external application.
// Keys with script paths are normally not shown in asset balances and
// by default aren't used for coin selection unless specifically
// requested.
ScriptKeyScriptPathExternal ScriptKeyType = 2

// ScriptKeyBurn is the script key type used for assets that are burned
// and not spendable.
ScriptKeyBurn ScriptKeyType = 3

// ScriptKeyTombstone is the script key type used for assets that are
// not spendable and have been marked as tombstones. This is only the
// case for zero-value assets that result from a non-interactive (TAP
// address) send where no change was left over. The script key used for
// this is a NUMS key that is not spendable.
ScriptKeyTombstone ScriptKeyType = 4

// ScriptKeyScriptPathChannel is the script key type used for assets
// that use a script path that is somehow related to Taproot Asset
// Channels. That means the script key is either a funding key
// (OP_TRUE), a commitment output key (to_local, to_remote, htlc), or a
// HTLC second-level transaction output key.
// Keys related to channels are not shown in asset balances (unless
// specifically requested) and are _never_ used for coin selection.
ScriptKeyScriptPathChannel ScriptKeyType = 5
)

var (
// ZeroPrevID is the blank prev ID used for genesis assets and also
// asset split leaves.
Expand Down Expand Up @@ -1005,14 +1049,8 @@ type TweakedScriptKey struct {
// public key. If this is nil, then a BIP-0086 tweak is assumed.
Tweak []byte

// DeclaredKnown indicates that this script key has been explicitly
// declared as being important to the local wallet, even if it might not
// be fully known to the local wallet. This could perhaps also be named
// "imported", though that might imply that the corresponding private
// key was also somehow imported and available. The only relevance this
// flag has is that assets with a declared key are shown in the asset
// list/balance.
DeclaredKnown bool
// Type is the type of script key that is being used.
Type ScriptKeyType
}

// IsEqual returns true is this tweaked script key is exactly equivalent to the
Expand Down Expand Up @@ -1083,12 +1121,9 @@ func (s *ScriptKey) IsEqual(otherScriptKey *ScriptKey) bool {
// the local wallet or was explicitly declared to be known by using the
// DeclareScriptKey RPC. Knowing the key conceptually means the key belongs to
// the local wallet or is at least known by a software that operates on the
// local wallet. The DeclaredAsKnown flag is never serialized in proofs, so this
// is never explicitly set for keys foreign to the local wallet. Therefore, if
// this method returns true for a script key, it means the asset with the script
// key will be shown in the wallet balance.
// local wallet.
func (s *ScriptKey) DeclaredAsKnown() bool {
return s.TweakedScriptKey != nil && s.TweakedScriptKey.DeclaredKnown
return s.TweakedScriptKey != nil && s.Type != ScriptKeyUnknown
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe ScriptKeyUnknown should be renamed to ScriptKeyTypeUnknown, because the type of script key is the thing that is unknown, even for a tracked script key. Have i understood that correctly?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I guess so. But then as a consequence we should also put the word Type into the values of the pseudo-enum, so ScriptKeyTypeBip86, ScriptKeyTypeScriptPathExternal and so on, which makes them even longer.
But since we have an explicit Golang type that also has the word Type in it, that also provides some context to it. So as long as we don't have any naming collision I'm opting to keep the current naming.

}

// HasScriptPath returns true if we know the internals of the script key and
Expand All @@ -1098,15 +1133,55 @@ func (s *ScriptKey) HasScriptPath() bool {
return s.TweakedScriptKey != nil && len(s.TweakedScriptKey.Tweak) > 0
}

// DetermineType attempts to determine the type of the script key based on the
// information available. This method will only return ScriptKeyUnknown if the
// following condition is met:
// - The script key doesn't have a script path, but the final Taproot output
// key doesn't match a BIP-0086 key derived from the internal key. This will
// be the case for "foreign" script keys we import from proofs, where we set
// the internal key to the same key as the tweaked script key (because we
// don't know the internal key, as it's not part of the proof encoding).
func (s *ScriptKey) DetermineType() ScriptKeyType {
// If we have an explicit script key type set, we can return that.
if s.TweakedScriptKey != nil &&
s.TweakedScriptKey.Type != ScriptKeyUnknown {

return s.TweakedScriptKey.Type
}

// If there is a known tweak, then we know that this is a script path
// key. We never return the channel type, since those keys should always
// be declared properly, and we never should need to guess their type.
if s.HasScriptPath() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems that for tap channel keys, we'd miss out on a more precise label here. Unless we always take a code path that skips this routine.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that's correct. For already existing channel keys (excluding the funding OP_TRUE key, as that's detectable), we couldn't assign the correct value.
But those should only include force close outputs. And they should be spent already, so hopefully not causing too much confusion for users.

return ScriptKeyScriptPathExternal
}

// Is it the known NUMS key? Then this is a tombstone output.
if s.PubKey != nil && s.PubKey.IsEqual(NUMSPubKey) {
return ScriptKeyTombstone
}

// Do we know the internal key? Then we can check whether it is a
// BIP-0086 key.
if s.PubKey != nil && s.TweakedScriptKey != nil &&
s.TweakedScriptKey.RawKey.PubKey != nil {

bip86 := NewScriptKeyBip86(s.TweakedScriptKey.RawKey)
if bip86.PubKey.IsEqual(s.PubKey) {
return ScriptKeyBip86
}
}

return ScriptKeyUnknown
}

// NewScriptKey constructs a ScriptKey with only the publicly available
// information. This resulting key may or may not have a tweak applied to it.
func NewScriptKey(key *btcec.PublicKey) ScriptKey {
// Since we'll never query lnd for a tweaked key, it doesn't matter if
// we lose the parity information here. And this will only ever be
// serialized on chain in a 32-bit representation as well.
key, _ = schnorr.ParsePubKey(
schnorr.SerializePubKey(key),
)
key, _ = schnorr.ParsePubKey(schnorr.SerializePubKey(key))
return ScriptKey{
PubKey: key,
}
Expand All @@ -1118,9 +1193,7 @@ func NewScriptKey(key *btcec.PublicKey) ScriptKey {
func NewScriptKeyBip86(rawKey keychain.KeyDescriptor) ScriptKey {
// Tweak the script key BIP-0086 style (such that we only commit to the
// internal key when signing).
tweakedPubKey := txscript.ComputeTaprootKeyNoScript(
rawKey.PubKey,
)
tweakedPubKey := txscript.ComputeTaprootKeyNoScript(rawKey.PubKey)

// Since we'll never query lnd for a tweaked key, it doesn't matter if
// we lose the parity information here. And this will only ever be
Expand All @@ -1133,6 +1206,7 @@ func NewScriptKeyBip86(rawKey keychain.KeyDescriptor) ScriptKey {
PubKey: tweakedPubKey,
TweakedScriptKey: &TweakedScriptKey{
RawKey: rawKey,
Type: ScriptKeyBip86,
},
}
}
Expand Down Expand Up @@ -1526,7 +1600,7 @@ func (a *Asset) Copy() *Asset {

if a.ScriptKey.TweakedScriptKey != nil {
assetCopy.ScriptKey.TweakedScriptKey = &TweakedScriptKey{
DeclaredKnown: a.ScriptKey.DeclaredKnown,
Type: a.ScriptKey.Type,
}
assetCopy.ScriptKey.RawKey = a.ScriptKey.RawKey

Expand Down
8 changes: 5 additions & 3 deletions asset/generators.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,11 @@ var (
})
TweakedScriptKeyGen = rapid.Custom(func(t *rapid.T) TweakedScriptKey {
return TweakedScriptKey{
RawKey: KeyDescGen.Draw(t, "raw_key"),
Tweak: HashBytesGen.Draw(t, "tweak"),
DeclaredKnown: rapid.Bool().Draw(t, "declared_known"),
RawKey: KeyDescGen.Draw(t, "raw_key"),
Tweak: HashBytesGen.Draw(t, "tweak"),
Type: ScriptKeyType(
rapid.Int16().Draw(t, "script_key_type"),
),
}
})
ScriptKeyGen = rapid.Custom(func(t *rapid.T) ScriptKey {
Expand Down
Loading
Loading