-
Notifications
You must be signed in to change notification settings - Fork 133
bugfix: fix two long-existing edge case bugs #1741
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
Pull Request Test Coverage Report for Build 17062493703Details
💛 - Coveralls |
40acfa2
to
af5f93f
Compare
Is this the cause of step 10 (the second problem) in #1736 ? |
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.
An initial pass, still trying to grok the exact gaps here which led to this bug. I also plan to add some additional documentation here for posterity.
Here's an extra unit test that we can add (at the `tapdb` level, covers the issue where import proof didn't write the split commit info):// createTestProof creates an annotated proof for testing.
func createTestProof(t *testing.T,
testAsset *asset.Asset) *proof.AnnotatedProof {
return createTestProofWithAnchor(t, testAsset, 0)
}
// createTestProofWithAnchor creates an annotated proof with specific anchor
// index.
func createTestProofWithAnchor(t *testing.T, testAsset *asset.Asset,
anchorIndex uint32) *proof.AnnotatedProof {
var blockHash chainhash.Hash
_, err := rand.Read(blockHash[:])
require.NoError(t, err)
anchorTx := wire.NewMsgTx(2)
anchorTx.AddTxIn(&wire.TxIn{})
// Add enough outputs to cover the requested index
for i := uint32(0); i <= anchorIndex; i++ {
anchorTx.AddTxOut(&wire.TxOut{
PkScript: bytes.Repeat([]byte{0x01}, 34),
Value: 10,
})
}
assetRoot, err := commitment.NewAssetCommitment(testAsset)
require.NoError(t, err)
commitVersion := test.RandFlip(nil, fn.Ptr(commitment.TapCommitmentV2))
taprootAssetRoot, err := commitment.NewTapCommitment(
commitVersion, assetRoot,
)
require.NoError(t, err)
testProof := randProof(t, testAsset)
proofBlob, err := testProof.Bytes()
require.NoError(t, err)
assetID := testAsset.ID()
anchorPoint := wire.OutPoint{
Hash: anchorTx.TxHash(),
Index: anchorIndex,
}
return &proof.AnnotatedProof{
Locator: proof.Locator{
AssetID: &assetID,
ScriptKey: *testAsset.ScriptKey.PubKey,
},
Blob: proofBlob,
AssetSnapshot: &proof.AssetSnapshot{
Asset: testAsset,
OutPoint: anchorPoint,
AnchorBlockHash: blockHash,
AnchorBlockHeight: uint32(test.RandIntn(1000) + 1),
AnchorTxIndex: test.RandInt[uint32](),
AnchorTx: anchorTx,
OutputIndex: anchorIndex,
InternalKey: test.RandPubKey(t),
ScriptRoot: taprootAssetRoot,
},
}
}
// TestUpsertAssetsWithSplitCommitments tests that split commitment data is
// properly stored and retrieved during asset upsert operations.
func TestUpsertAssetsWithSplitCommitments(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
hasSplitCommit bool
splitValue uint64
}{
{
name: "asset_with_split_commitment",
hasSplitCommit: true,
splitValue: 12345,
},
{
name: "asset_without_split_commitment",
hasSplitCommit: false,
},
}
for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
ctx := context.Background()
dbHandle := NewDbHandle(t)
testAsset := randAsset(t)
var expectedHash mssmt.NodeHash
if tc.hasSplitCommit {
splitHash := test.RandBytes(32)
copy(expectedHash[:], splitHash)
testAsset.SplitCommitmentRoot = mssmt.NewComputedNode( //nolint:lll
expectedHash, tc.splitValue,
)
}
annotatedProof := createTestProof(
t, testAsset,
)
err := dbHandle.AssetStore.ImportProofs(
ctx, proof.MockVerifierCtx, false,
annotatedProof,
)
require.NoError(t, err)
assets, err := dbHandle.AssetStore.FetchAllAssets(ctx, false, false, nil)
require.NoError(t, err)
require.Len(t, assets, 1)
dbAsset := assets[0].Asset
if tc.hasSplitCommit {
require.NotNil(
t, dbAsset.SplitCommitmentRoot,
)
gotHash := dbAsset.SplitCommitmentRoot.NodeHash()
require.Equal(t, expectedHash, gotHash)
require.Equal(
t, tc.splitValue,
dbAsset.SplitCommitmentRoot.NodeSum(),
)
} else {
require.Nil(
t, dbAsset.SplitCommitmentRoot,
"Split commitment root should be nil",
)
}
})
}
} |
Yes, I'm pretty sure that's the bug you ran into. At least the error message is identical (but different cases could potentially lead to that error message, but hoping this is the one fixed here, otherwise please make sure to report again). |
Fixes #1740. We haven't ever tested creating a split asset that then becomes a passive asset in a subsequent transfer. And that logic did have a bug in it: Because we used the asset from the trimmed input commitment (meaning we called commitment.TrimSplitWitnesses) because we wanted to make sure it matches the output script where we always trim, it never had a split commitment in the witness. So when signing we signed without the split commitment. But when verifying, we properly used the input proof's asset which did have the split commitment, so the signature ended up being incorrect. This commit also contains a minimal reproduction case that failed before this fix.
This is the integration test that initially discovered the issue with split outputs that become passive assets in a subsequent transfer. We add it here to make sure the bug is properly fixed.
Because we don't need to be able to fully predict the asset leaf as it is put on chain (which was the case for V0 and V1 addresses), we can optimize chain usage by avoiding the creation of a tombstone output on full value sends (meaning we use an interactive transfer scheme for V2 addresses, since the receiver will learn about the proofs from the auth mailbox and doesn't need to predict the leave(s) themselves). That means we no longer have to create a zero-value change output that locks 1k sats until we implement garbage collection if the user wants to send all assets. So fewer outputs created on-chain and fewer sats locked for the user.
af5f93f
to
ae38d8a
Compare
Fixes the following error that occurred sometimes: unable to fund address send: error funding packet: unable to list eligible coins: unable to query commitments: mismatch of managed utxo and constructed tap commitment root This would happen when we inserted the split _ROOT_ output as a proof file (instead of going through the transfer flow which didn't have this issue), because the split commitment root wasn't properly written to the database in that case. Which means, not a single itest so far did send out the split root output to the counterparty as part of an interactive transfer. This was only discovered after turning the address v2 send flow into an interactive send, which does allow the split root output to be sent to the counterparty instead of only using it as the local change output.
ae38d8a
to
5488ad7
Compare
Awesome, thanks you! Added. |
If this issue has been around for a while, should we re-write the itest to do a non-group key send? I was able to reproduce the issue sending the individual asset tranches using a non-group key V1 address, but I did not try to do the multi asset id send with V1 address. Perhaps that could be done by creating 3 V1 addresses and then paying to them all in the same invocation of taproot-assets/taprpc/taprootassets.proto Lines 1524 to 1531 in ca434e4
|
No, I don't think that will work. Because you need multiple split outputs being in the same on-chain output. And you can't do that with V0/V1 addresses, each address will get its own on-chain output. |
So this was the only way that this bug could have been previously exposed? You say this PR fixes two long standing edge cases, but before V2 addresses actually getting them to happen would be very hard for the average user and only a really advanced user could have even found them? |
Correct, so we're lucky nobody ran into these two cases yet. |
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.
LGTM, ty for the fixes! 💯
Fixes #1740.
Fixes two bugs that have been in the code base for a long time but only have been triggered recently by the test mentioned here: #1740
invalid transfer asset witness
error. The fix is using the correct input asset from the input proof instead of the asset from the trimmed input commitment.unable to fund address send: error funding packet: unable to list eligible coins: unable to query commitments: mismatch of managed utxo and constructed tap commitment root
.I've also included the integration test from ZZiigguurraatt@d6ef2ac which reproduced the first bug reliably (thanks a lot, @ZZiigguurraatt, that was super helpful!).
cc @bhandras, not sure if you perhaps ran into either of these bugs in your work?