-
Notifications
You must be signed in to change notification settings - Fork 131
[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
Conversation
33717eb
to
1b9a8cc
Compare
1b9a8cc
to
d85ebb9
Compare
Pull Request Test Coverage Report for Build 14775742039Details
💛 - Coveralls |
d85ebb9
to
c63abbe
Compare
I finally got this down to a single TODO: Add more integration tests. |
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 |
tapfreighter/chain_porter.go
Outdated
// 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)?
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.
Such a key might come from the RPC interface, set by the user. So it's hard to determine an exact point before that we could reliably do this. But I moved the logic even closer to the database logic, so it is more clear that this only affects what is stored in the DB and not really the transfer itself.
tapdb/post_migration_checks.go
Outdated
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?
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.
There is some uncertainty to it. For example with channel related keys. We can detect the funding output key, since that's the static OP_TRUE
(at least before we added the universe collision avoidance that now creates unique keys for each asset ID, but we assume that only happens in the release that also contains this PR, where we explicitly set the type on key creation). But we can't detect any to_local or to_remote or HTLC related channel keys. Those will be shown as ScriptKeyScriptPathExternal
which isn't 100% correct.
So users who had taproot asset channels that force closed before this PR will see such script keys. But at least they should also show as "spent", so hopefully that won't bother anyone.
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?
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.
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.
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?
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 would argue no. Burn keys are something the user explicitly creates, so IMO they should be shown if the user selects with_spent
. And tombstones are created behind the scenes, BUT they have have a relevance to the user as they might lock up BTC in an otherwise empty UTXO (see #1458 that is fixed by this PR).
The only type of script key that IMO should always be hidden from the user (as otherwise they will cause confusion and questions) are channel related keys and their assets. Sure, the user creates those channels, so it is the result of a user action. But for any balance related calls we should not include them, as otherwise the assets might be counted twice (once off-chain and once on-chain). And especially force close output related script keys should be hidden, as they are always swept into "normal" outputs anyway under the hood.
de58c89
to
31347c7
Compare
@guggero, remember to re-request review from reviewers when ready |
31347c7
to
03cdc3b
Compare
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.
LGTM 💥
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.
Great work! Very comprehensive change, and now we have a method of doing optional Go migrations as well.
Only potential blocker I identified is if we need to change the defaults on the CLI due to the new filtration semantics. We may also need to modify some balance checks/assertions in the litd channel tests as well. Can do both/either of those as a follow up.
LGTM 🦃
@@ -135,6 +135,9 @@ SELECT seedling_id, asset_name, asset_type, asset_version, asset_supply, | |||
sqlc.embed(assets_meta), | |||
emission_enabled, batch_id, | |||
group_genesis_id, group_anchor_id, group_tapscript_root, | |||
-- TODO(guggero): We should use sqlc.embed() for the script key and internal | |||
-- key fields, but we can't because it's a LEFT JOIN. We should check if the | |||
-- LEFT JOIN is actually necessary or if we always have keys for seedlings. |
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.
Good q...
Perhaps this is from before we had an explicit asset meta?
Looking at it now, the only portion that def needs a left join is for the group keys, as not every seedling actually has a group key.
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 this is a quirk of the minting logic. At some point during the minting process there can be seedlings that don't yet have a script key defined, but we still want to be able to store them. I changed this to a normal join and tests started failing, so I didn't look into it more closely.
-- will mean the type is not known. Existing script keys will be inspected by | ||
-- the post-migration step Golang code that determines the type of each script | ||
-- key. | ||
ALTER TABLE script_keys ADD COLUMN key_type SMALLINT; |
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.
Non-blocking, but an alternative here would be the enum table pattern. So a new script_key_type
table w/ set values (burn, transfer, channel, etc) and this new column refs that. This let's us put the mapping of string type to integer within the db itself.
Tradeoff is potentially a join to match thing sup if just the primary key is stored here. Adding new types also then is a migration, albeit just one that inserts a single row into this new table.
Just using an integer type here and putting the mapping in the Go layer as is don here certainly simpler.
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.
Right, but what would the benefits of that be? IMO it doesn't make the data more easy to read "by eye", meaning that without the join it would still be a non-self-explanatory number.
And on top of that we'd need to string matching to map the values to Golang constants. Or would the idea be to use string-based constants on the Golang level?
// 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() { |
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.
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.
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.
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.
assetBalancesFilter.ExcludeKey = excludeKey.PubKey.SerializeCompressed() | ||
// If the user explicitly wants to see the channel related asset | ||
// balances, we need to set the exclude type to NULL. | ||
if t == 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.
But this doesn't allow them to only query for the channel related balances?
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.
Hmm, not sure I understand... We always set the ExcludeScriptKeyType
to channel keys above. But if the user explicitly wants to see channel keys, we set the ExcludeScriptKeyType
to NULL
, disabling the exclude.
) | ||
allAssets, err := t.tapd.ListAssets(ctxt, &taprpc.ListAssetRequest{ | ||
IncludeSpent: true, | ||
ScriptKeyType: allScriptKeysQuery, |
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 need to change CLI defaults as a result of this?
Also something to call out in the release notes as a change in behavior here.
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.
No, the defaults remain the same. But I added a new --script_key_type
flag to all relevant commands so the users can also filter on the command line.
a67a5e9
to
c4317f3
Compare
Had to add a commit to point to the @Roasbeef feel free to merge this PR if you feel like your questions have been addressed satisfactorily. |
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
This commit adds a new --script_key_type flag to the asset related commands, where supported by the corresponding RPC. If nothing is specified, the previous "show all assets" behavior is kept.
We need to update the way we assert balances in the integration tests, because with the changes in this PR we no longer show channel funding related asset outputs by default.
c4317f3
to
7b38305
Compare
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.