Skip to content

Commit

Permalink
Merge pull request #419 from lightninglabs/reorg-safe
Browse files Browse the repository at this point in the history
re-org safety: watch asset transactions and re-create proofs when necessary
  • Loading branch information
guggero authored Aug 17, 2023
2 parents 6bbe56a + 0fee0e4 commit 6412264
Show file tree
Hide file tree
Showing 37 changed files with 2,300 additions and 189 deletions.
18 changes: 15 additions & 3 deletions chain_bridge.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,13 @@ func NewLndRpcChainBridge(lnd *lndclient.LndServices) *LndRpcChainBridge {
// txid reaches numConfs confirmations.
func (l *LndRpcChainBridge) RegisterConfirmationsNtfn(ctx context.Context,
txid *chainhash.Hash, pkScript []byte, numConfs, heightHint uint32,
includeBlock bool) (*chainntnfs.ConfirmationEvent, chan error, error) {
includeBlock bool,
reOrgChan chan struct{}) (*chainntnfs.ConfirmationEvent, chan error,
error) {

var opts []lndclient.NotifierOption
opts := []lndclient.NotifierOption{
lndclient.WithReOrgChan(reOrgChan),
}
if includeBlock {
opts = append(opts, lndclient.WithIncludeBlock())
}
Expand All @@ -55,6 +59,14 @@ func (l *LndRpcChainBridge) RegisterConfirmationsNtfn(ctx context.Context,
}, errChan, nil
}

// RegisterBlockEpochNtfn registers an intent to be notified of each new block
// connected to the main chain.
func (l *LndRpcChainBridge) RegisterBlockEpochNtfn(
ctx context.Context) (chan int32, chan error, error) {

return l.lnd.ChainNotifier.RegisterBlockEpochNtfn(ctx)
}

// GetBlock returns a chain block given its hash.
func (l *LndRpcChainBridge) GetBlock(ctx context.Context,
hash chainhash.Hash) (*wire.MsgBlock, error) {
Expand Down Expand Up @@ -105,7 +117,7 @@ func (l *LndRpcChainBridge) VerifyBlock(ctx context.Context,
expectedHash := header.BlockHash()
if hash != expectedHash {
return fmt.Errorf("block hash and block height "+
"mismatch; (height: %x, hashAtHeight: %s, "+
"mismatch; (height: %d, hashAtHeight: %s, "+
"expectedHash: %s)", height, hash, expectedHash)
}

Expand Down
2 changes: 2 additions & 0 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ type Config struct {

SignalInterceptor signal.Interceptor

ReOrgWatcher *tapgarden.ReOrgWatcher

AssetMinter tapgarden.Planter

AssetCustodian *tapgarden.Custodian
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ require (
github.com/lib/pq v1.10.3
github.com/lightninglabs/aperture v0.1.20-beta
github.com/lightninglabs/lightning-node-connect/hashmailrpc v1.0.2
github.com/lightninglabs/lndclient v0.16.0-11
github.com/lightninglabs/lndclient v0.16.0-15
github.com/lightninglabs/neutrino/cache v1.1.1
github.com/lightningnetwork/lnd v0.16.0-beta.rc5.0.20230803213632-03b26bb909bb
github.com/lightningnetwork/lnd/cert v1.2.1
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -459,8 +459,8 @@ github.com/lightninglabs/gozmq v0.0.0-20191113021534-d20a764486bf h1:HZKvJUHlcXI
github.com/lightninglabs/gozmq v0.0.0-20191113021534-d20a764486bf/go.mod h1:vxmQPeIQxPf6Jf9rM8R+B4rKBqLA2AjttNxkFBL2Plk=
github.com/lightninglabs/lightning-node-connect/hashmailrpc v1.0.2 h1:Er1miPZD2XZwcfE4xoS5AILqP1mj7kqnhbBSxW9BDxY=
github.com/lightninglabs/lightning-node-connect/hashmailrpc v1.0.2/go.mod h1:antQGRDRJiuyQF6l+k6NECCSImgCpwaZapATth2Chv4=
github.com/lightninglabs/lndclient v0.16.0-11 h1:jDHz/FZ3FBbVPZ8LhLGsC9Z9fsq+3jZrfWXwuxrXwRg=
github.com/lightninglabs/lndclient v0.16.0-11/go.mod h1:mqY0znSNa+M40HZowwKfno29RyZnmxoqo++BlYP82EY=
github.com/lightninglabs/lndclient v0.16.0-15 h1:U4GXByrHAPqZqTaEbzC+J1aCjp3otVI7bemXrf5NB7c=
github.com/lightninglabs/lndclient v0.16.0-15/go.mod h1:mqY0znSNa+M40HZowwKfno29RyZnmxoqo++BlYP82EY=
github.com/lightninglabs/neutrino v0.15.1-0.20230710222839-9fd0fc551366 h1:++HuI+fNJ61HWobNkj0BvFs477R2Ar3TJABI0gendI8=
github.com/lightninglabs/neutrino v0.15.1-0.20230710222839-9fd0fc551366/go.mod h1:pmjwElN/091TErtSE9Vd5W4hpxoG2/+xlb+HoPm9Gug=
github.com/lightninglabs/neutrino/cache v1.1.1 h1:TllWOSlkABhpgbWJfzsrdUaDH2fBy/54VSIB4vVqV8M=
Expand Down
102 changes: 93 additions & 9 deletions itest/assertions.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,13 +68,13 @@ func assetAnchorCheck(txid, blockHash chainhash.Hash) assetCheck {

if a.ChainAnchor.AnchorTxid != txid.String() {
return fmt.Errorf("unexpected asset anchor TXID, got "+
"%x wanted %x", a.ChainAnchor.AnchorTxid,
"%v wanted %x", a.ChainAnchor.AnchorTxid,
txid[:])
}

if a.ChainAnchor.AnchorBlockHash != blockHash.String() {
return fmt.Errorf("unexpected asset anchor block "+
"hash, got %x wanted %x",
"hash, got %v wanted %x",
a.ChainAnchor.AnchorBlockHash, blockHash[:])
}

Expand Down Expand Up @@ -201,6 +201,39 @@ func commitmentKey(t *testing.T, rpcAsset *taprpc.Asset) [32]byte {
return asset.AssetCommitmentKey(assetID, scriptKey, groupKey == nil)
}

// waitForProofUpdate polls until the proof for the given asset has been
// updated, which is detected by checking the block height of the last proof.
func waitForProofUpdate(t *testing.T, tapd *tapdHarness, a *taprpc.Asset,
blockHeight int32) {

t.Helper()

ctxb := context.Background()
ctxt, cancel := context.WithTimeout(ctxb, defaultWaitTimeout*2)
defer cancel()

require.Eventually(t, func() bool {
// Export the proof, then decode it.
exportResp, err := tapd.ExportProof(
ctxt, &taprpc.ExportProofRequest{
AssetId: a.AssetGenesis.AssetId,
ScriptKey: a.ScriptKey,
},
)
require.NoError(t, err)

f := &proof.File{}
require.NoError(
t, f.Decode(bytes.NewReader(exportResp.RawProof)),
)
lastProof, err := f.LastProof()
require.NoError(t, err)

// Check the block height of the proof.
return lastProof.BlockHeight == uint32(blockHeight)
}, defaultWaitTimeout, 200*time.Millisecond)
}

// assertAssetProofs makes sure the proofs for the given asset can be retrieved
// from the given daemon and can be fully validated.
func assertAssetProofs(t *testing.T, tapd *tapdHarness,
Expand Down Expand Up @@ -233,6 +266,34 @@ func assertAssetProofs(t *testing.T, tapd *tapdHarness,
return exportResp.RawProof
}

// assertAssetProofsInvalid makes sure the proofs for the given asset can be
// retrieved from the given daemon but fail to validate.
func assertAssetProofsInvalid(t *testing.T, tapd *tapdHarness,
a *taprpc.Asset) {

t.Helper()

ctxb := context.Background()
ctxt, cancel := context.WithTimeout(ctxb, defaultWaitTimeout)
defer cancel()

exportResp, err := tapd.ExportProof(ctxt, &taprpc.ExportProofRequest{
AssetId: a.AssetGenesis.AssetId,
ScriptKey: a.ScriptKey,
})
require.NoError(t, err)

f := &proof.File{}
require.NoError(t, f.Decode(bytes.NewReader(exportResp.RawProof)))

// Also make sure that the RPC can verify the proof as well.
verifyResp, err := tapd.VerifyProof(ctxt, &taprpc.ProofFile{
RawProof: exportResp.RawProof,
})
require.NoError(t, err)
require.False(t, verifyResp.Valid)
}

// verifyProofBlob parses the given proof blob into a file, verifies it and
// returns the resulting last asset snapshot together with the parsed file.
func verifyProofBlob(t *testing.T, tapd *tapdHarness, a *taprpc.Asset,
Expand Down Expand Up @@ -287,7 +348,7 @@ func verifyProofBlob(t *testing.T, tapd *tapdHarness, a *taprpc.Asset,
expectedHash := hash
if heightHash != expectedHash {
return fmt.Errorf("block hash and block height "+
"mismatch; (height: %x, hashAtHeight: %s, "+
"mismatch; (height: %d, hashAtHeight: %s, "+
"expectedHash: %s)", height, heightHash,
expectedHash)
}
Expand Down Expand Up @@ -421,9 +482,10 @@ func assertAddrEventByStatus(t *testing.T, tapd *tapdHarness,
// with the node.
func confirmAndAssertOutboundTransfer(t *harnessTest, sender *tapdHarness,
sendResp *taprpc.SendAssetResponse, assetID []byte,
expectedAmounts []uint64, currentTransferIdx, numTransfers int) {
expectedAmounts []uint64, currentTransferIdx,
numTransfers int) *wire.MsgBlock {

confirmAndAssetOutboundTransferWithOutputs(
return confirmAndAssetOutboundTransferWithOutputs(
t, sender, sendResp, assetID, expectedAmounts,
currentTransferIdx, numTransfers, 2,
)
Expand All @@ -435,9 +497,9 @@ func confirmAndAssertOutboundTransfer(t *harnessTest, sender *tapdHarness,
func confirmAndAssetOutboundTransferWithOutputs(t *harnessTest,
sender *tapdHarness, sendResp *taprpc.SendAssetResponse,
assetID []byte, expectedAmounts []uint64, currentTransferIdx,
numTransfers, numOutputs int) {
numTransfers, numOutputs int) *wire.MsgBlock {

assertAssetOutboundTransferWithOutputs(
return assertAssetOutboundTransferWithOutputs(
t, sender, sendResp, assetID, expectedAmounts,
currentTransferIdx, numTransfers, numOutputs, true,
)
Expand All @@ -448,7 +510,7 @@ func confirmAndAssetOutboundTransferWithOutputs(t *harnessTest,
func assertAssetOutboundTransferWithOutputs(t *harnessTest,
sender *tapdHarness, sendResp *taprpc.SendAssetResponse,
assetID []byte, expectedAmounts []uint64, currentTransferIdx,
numTransfers, numOutputs int, confirm bool) {
numTransfers, numOutputs int, confirm bool) *wire.MsgBlock {

ctxb := context.Background()

Expand All @@ -472,8 +534,9 @@ func assertAssetOutboundTransferWithOutputs(t *harnessTest,
t.Logf("Got response from sending assets: %v", sendRespJSON)

// Mine a block to force the send event to complete (confirm on-chain).
var newBlock *wire.MsgBlock
if confirm {
_ = mineBlocks(t, t.lndHarness, 1, 1)
newBlock = mineBlocks(t, t.lndHarness, 1, 1)[0]
}

// Confirm that we can externally view the transfer.
Expand Down Expand Up @@ -509,6 +572,8 @@ func assertAssetOutboundTransferWithOutputs(t *harnessTest,
transferRespJSON, err := formatProtoJSON(transferResp)
require.NoError(t.t, err)
t.Logf("Got response from list transfers: %v", transferRespJSON)

return newBlock
}

// assertNonInteractiveRecvComplete makes sure the given receiver has the
Expand Down Expand Up @@ -795,6 +860,25 @@ func assertListAssets(t *harnessTest, ctx context.Context, tapd *tapdHarness,
}
}

// assertUniverseRootEquality checks that the universe roots returned by two
// daemons are either equal or not, depending on the expectedEquality parameter.
func assertUniverseRootEquality(t *testing.T, a, b *tapdHarness,
expectedEquality bool) {

ctxb := context.Background()
ctxt, cancel := context.WithTimeout(ctxb, defaultWaitTimeout)
defer cancel()

rootRequest := &unirpc.AssetRootRequest{}
universeRootsAlice, err := a.AssetRoots(ctxt, rootRequest)
require.NoError(t, err)
universeRootsBob, err := b.AssetRoots(ctxt, rootRequest)
require.NoError(t, err)
require.Equal(t, expectedEquality, assertUniverseRootsEqual(
universeRootsAlice, universeRootsBob,
))
}

func assertUniverseRoot(t *testing.T, tapd *tapdHarness, sum int,
assetID []byte, groupKey []byte) error {

Expand Down
45 changes: 40 additions & 5 deletions itest/assets_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,11 +150,12 @@ func withMintingTimeout(timeout time.Duration) mintOption {
}
}

// mintAssetsConfirmBatch mints all given assets in the same batch, confirms the
// batch and verifies all asset proofs of the minted assets.
func mintAssetsConfirmBatch(t *harnessTest, tapd *tapdHarness,
// mintAssetUnconfirmed is a helper function that mints a batch of assets and
// waits until the minting transaction is in the mempool but does not mine a
// block.
func mintAssetUnconfirmed(t *harnessTest, tapd *tapdHarness,
assetRequests []*mintrpc.MintAssetRequest,
opts ...mintOption) []*taprpc.Asset {
opts ...mintOption) chainhash.Hash {

options := defaultMintOptions()
for _, opt := range opts {
Expand Down Expand Up @@ -210,6 +211,26 @@ func mintAssetsConfirmBatch(t *harnessTest, tapd *tapdHarness,
)
}

return *hashes[0]
}

// mintAssetsConfirmBatch mints all given assets in the same batch, confirms the
// batch and verifies all asset proofs of the minted assets.
func mintAssetsConfirmBatch(t *harnessTest, tapd *tapdHarness,
assetRequests []*mintrpc.MintAssetRequest,
opts ...mintOption) []*taprpc.Asset {

mintTXID := mintAssetUnconfirmed(t, tapd, assetRequests, opts...)

options := defaultMintOptions()
for _, opt := range opts {
opt(options)
}

ctxb := context.Background()
ctxt, cancel := context.WithTimeout(ctxb, options.mintingTimeout)
defer cancel()

// Mine a block to confirm the assets.
block := mineBlocks(t, t.lndHarness, 1, 1)[0]
blockHash := block.BlockHash()
Expand All @@ -218,6 +239,20 @@ func mintAssetsConfirmBatch(t *harnessTest, tapd *tapdHarness,
mintrpc.BatchState_BATCH_STATE_FINALIZED,
)

return assertAssetsMinted(t, tapd, assetRequests, mintTXID, blockHash)
}

// assertAssetsMinted makes sure all assets in the minting request were in fact
// minted in the given anchor TX and block. The function returns the list of
// minted assets.
func assertAssetsMinted(t *harnessTest, tapd *tapdHarness,
assetRequests []*mintrpc.MintAssetRequest, mintTXID,
blockHash chainhash.Hash) []*taprpc.Asset {

ctxb := context.Background()
ctxt, cancel := context.WithTimeout(ctxb, defaultWaitTimeout)
defer cancel()

// The rest of the anchor information should now be populated as well.
// We also check that the anchor outpoint of all assets is the same,
// since they were all minted in the same batch.
Expand All @@ -238,7 +273,7 @@ func mintAssetsConfirmBatch(t *harnessTest, tapd *tapdHarness,
}).MetaHash()
mintedAsset := assertAssetState(
t, confirmedAssets, assetRequest.Asset.Name,
metaHash[:], assetAnchorCheck(*hashes[0], blockHash),
metaHash[:], assetAnchorCheck(mintTXID, blockHash),
assetScriptKeyIsLocalCheck(true),
func(a *taprpc.Asset) error {
anchor := a.ChainAnchor
Expand Down
Loading

0 comments on commit 6412264

Please sign in to comment.