-
Notifications
You must be signed in to change notification settings - Fork 130
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
[tapdb]: add script key type enum, run Golang based post migration checks #1198
base: main
Are you sure you want to change the base?
Conversation
33717eb
to
1b9a8cc
Compare
1b9a8cc
to
d85ebb9
Compare
Pull Request Test Coverage Report for Build 14270907739Details
💛 - Coveralls |
d85ebb9
to
c63abbe
Compare
I finally got this down to a single TODO: Add more integration tests. |
With this commit we add a new numeric type for the type of a script key. This will be a Golang enum/const that's going to be assigned manually when declaring a key as known. For existing keys, we'll add a new mechanism in the following commits that runs on startup after we detect a SQL migration was applied that will attempt the detection of the type of those keys.
With this commit we add a type enum to the script key. This will mostly be used on the database level to filter assets when listing or calculating balances. We can only be 100% certain about the type of a key in the case of BIP-0086 or where we explicitly create them in the tapchannel package. Anything else is a bit of a guess, since the keys are coming over the RPC interface. So we just assume any key that has a non-empty script path is an externally-defined (external app) script spend. This heuristic should be good enough to filter out channel related assets or assets that can't be spent without external instructions in balance RPC calls.
In order for burn/tombstone keys to be stored with the correct type, we need to detect them before storing the transfer outputs to the database. We can't set the script key type before, because we're carrying around the script key in a virtual packet, where that information isn't available.
We'll want to re-use that function in an upcoming commit, without needing to instantiate a full asset store.
The previous commit forced us to update some gRPC/REST related libraries, which cause a diff in the generated RPC stubs.
Specifying a script key as known now requires the user to set a specific type.
Since we can now declare more than just knowledge of a script key by giving it a specific type, we no longer need the boolean flag that was added as a temporary workaround.
With an explicit type for a script key now being used, we can turn the key based matching into a type based matching, which will also be more generic for unique funding script keys that are required for grouped asset channel funding.
To make sure our itests still work, we need to query assets with all possible script key types.
We add a new AssetBalances function that makes sure that the output from all the following RPCs is aligned: - ListAssets - ListBalances (both grouped by asset ID or group key) - ListUtxos
c63abbe
to
450bb48
Compare
Addressed the final TODO by asserting the new balances in multiple integration test steps. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we need to fork the migrate
package (guggero/migrate@f82a988) It would be nice not to have to maintain a fork.
Can't we get away with just reformulating applyMigrations
in tapdb/migrations.go
?
Roughly:
sqlMigrate, err := migrate.NewWithInstance(
"migrations", migrateFileServer, dbName, driver,
)
// We will iter over `opts.postStepCallbacks` in ascending version index.
// Get next post migration callback index from `opts.postStepCallbacks`:
// `nextVersion`.
//
// Migrate to that version.
sqlMigrate.Migrate(nextVersion)
// Run callback function using the same driver that we passed into
// `migrate.NewWithInstance`.
//
// Flag version as dirty before callback exec.
driver.SetVersion(nextVersion, true)
err := opts.postStepCallbacks[nextVersion](driver)
if err != nil {...}
// Set version clean given no callback error.
driver.SetVersion(nextVersion, false)
// Handle the next version in `opts.postStepCallbacks` or until target migration version is reached.
The high level idea is to migrate using the migrate
in stages and pause to apply post migration callbacks.
With your formulation, what happens if we upgrade to version 3 from version 2 (2->3), then as we're executing the call back for post version 2, the daemon crashes? IIUC, we'll start from version 3, and skip the post step call back of version 2. What we want here is that the call back is executed in the exact same context as the migration to version 2 in order to retain atomicity. The db shouldn't move to version 2 until the call back has successfully been executed. |
@Roasbeef I agree that my approach isn't atomic, since the callback wouldn't run within the same database transaction. But I believe that's also the case with the current state of the migration package fork solution. That said, the fork solution does allow the callback to run while holding the same driver lock, which is still valuable. So I think forking the package is probably the best path forward. EDIT: The callback is executed within a transaction, but that doesn't happen within |
tapdb/assets_common.go
Outdated
result = asset.ScriptKey{ | ||
TweakedScriptKey: &asset.TweakedScriptKey{ | ||
RawKey: keychain.KeyDescriptor{ | ||
KeyLocator: locator, | ||
}, | ||
Tweak: sk.Tweak, | ||
DeclaredKnown: extractBool(sk.DeclaredKnown), | ||
}, | ||
}, | ||
DeclaredKnown: extractBool(dbKey.DeclaredKnown), | ||
} | ||
err error | ||
) | ||
|
||
if len(sk.TweakedScriptKey) == 0 { | ||
return result, fmt.Errorf("tweaked script key is empty") | ||
} | ||
|
||
if len(ik.RawKey) == 0 { | ||
return result, fmt.Errorf("internal raw key is empty") | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe that when returning an error, the asset.ScriptKey
return value should remain unset. In this case, result
is at least partially populated.
Also, I think you should move these function argument checks to the top of the function, before declaring locator
and result
.
-- will mean the type is not known. Existing script keys at the time of this | ||
-- migration will be updated at startup after the migration is applied. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this is true anymore:
Existing script keys at the time of this migration will be updated at startup after the migration is applied.
The script key type value will be set as part of migration.
// GuessType tries to guess the type of the script key based on the information | ||
// available. | ||
func (s *ScriptKey) GuessType() ScriptKeyType { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think "guess" is misleading here. This method appears to classify decisively whenever possible, but it never seems to make a prediction when uncertainty would be warranted. I suggest we rename to Type()
.
// Burn and tombstone keys are the only keys that we don't | ||
// explicitly store in the DB before this point. But we'll want | ||
// them to have the correct type when creating the transfer, so | ||
// we'll set that now. | ||
detectUnSpendableKeys(currentPkg.VirtualPackets) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems like an odd place to set the script type in the vout. Can't we just do that when we're constructing the vout (or assigning the burn/tombstone script key to the vout)?
replace google.golang.org/protobuf => github.com/lightninglabs/protobuf-go-hex-display v1.33.0-hex-display | ||
replace google.golang.org/protobuf => github.com/lightninglabs/protobuf-go-hex-display v1.34.2-hex-display | ||
|
||
replace github.com/golang-migrate/migrate/v4 => github.com/guggero/migrate/v4 v4.0.0-20250328181905-0e91936b8a3b |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should be a fork under the LL org, IMO. Similar to github.com/lightninglabs/protobuf-go-hex-display I suppose. Would be nice to standardise how LL handles its forked packages.
if _, ok := burnKeys[serializedKey]; ok { | ||
newType = asset.ScriptKeyBurn | ||
} else { | ||
guessedType := scriptKey.GuessType() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here with the term "guess". Is there an aspect of this that's uncertain that I'm not seeing?
postStepCallbacks[version] = func(m *migrate.Migration, | ||
_ database.Driver) error { | ||
|
||
return txDb.ExecTx( | ||
ctx, &writeTxOpts, func(q sqlc.Querier) error { | ||
return runCheck(m, q) | ||
}, | ||
) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ah nice! The callback is executed in a transaction here.
func (s *ScriptKey) DeclaredAsKnown() bool { | ||
return s.TweakedScriptKey != nil && s.TweakedScriptKey.DeclaredKnown | ||
return s.TweakedScriptKey != nil && s.Type != ScriptKeyUnknown |
There was a problem hiding this comment.
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?
@@ -2302,21 +2302,21 @@ func (r *rpcServer) FundVirtualPsbt(ctx context.Context, | |||
|
|||
// unmarshalCoinSelectType converts an RPC select type into a native one. | |||
func unmarshalCoinSelectType( | |||
coinSelectType wrpc.CoinSelectType) (tapsend.CoinSelectType, error) { | |||
coinSelectType wrpc.CoinSelectType) (fn.Option[asset.ScriptKeyType], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
func doc comment could be updated
tapdb/assets_store.go
Outdated
assetBalancesFilter.ExcludeScriptKeyType = sqlInt16( | ||
asset.ScriptKeyScriptPathChannel, | ||
) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we also need to exclude burn
and tombstone
here? I would expect ExcludeScriptKeyType
to be a list—same in the SQL query. But perhaps we can defer making it a list for now and still remain correct?
Depends on golang-migrate/migrate#1253 (might need to maintain our own fork if we decide to go with the approach).
Fixes #1458.
Fixes #1162.
Adds a specific
key_type
field to the script key table, which allows us to distinguish between the following types of keys:declared_known
boolean, which can now be synthesized withkey_type != Unknown
)This allows us to correctly filter out channel related keys when showing balances (e.g.
ListBalances
,ListUtxos
orListAssets
) and also allows us to do more custom coin selection.This is required for cleanly filter out channel related script keys implemented in #1413.
cc @GeorgeTsagk: this mechanism can be used to add asset burns to the new table retroactively.