Skip to content
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

wallet: add asset coin locking #431

Merged
merged 12 commits into from
Aug 16, 2023
Merged
2 changes: 2 additions & 0 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,8 @@ type Config struct {

AssetWallet tapfreighter.Wallet

CoinSelect *tapfreighter.CoinSelect

ChainPorter tapfreighter.Porter

BaseUniverse *universe.MintingArchive
Expand Down
17 changes: 16 additions & 1 deletion itest/assertions.go
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,19 @@ func confirmAndAssetOutboundTransferWithOutputs(t *harnessTest,
assetID []byte, expectedAmounts []uint64, currentTransferIdx,
numTransfers, numOutputs int) {

assertAssetOutboundTransferWithOutputs(
t, sender, sendResp, assetID, expectedAmounts,
currentTransferIdx, numTransfers, numOutputs, true,
)
}

// assertAssetOutboundTransferWithOutputs makes sure the given outbound transfer
// has the correct state and number of outputs.
func assertAssetOutboundTransferWithOutputs(t *harnessTest,
sender *tapdHarness, sendResp *taprpc.SendAssetResponse,
assetID []byte, expectedAmounts []uint64, currentTransferIdx,
numTransfers, numOutputs int, confirm bool) {

ctxb := context.Background()

// Check that we now have two new outputs, and that they differ
Expand All @@ -459,7 +472,9 @@ func confirmAndAssetOutboundTransferWithOutputs(t *harnessTest,
t.Logf("Got response from sending assets: %v", sendRespJSON)

// Mine a block to force the send event to complete (confirm on-chain).
_ = mineBlocks(t, t.lndHarness, 1, 1)
if confirm {
_ = mineBlocks(t, t.lndHarness, 1, 1)
}

// Confirm that we can externally view the transfer.
require.Eventually(t.t, func() bool {
Expand Down
101 changes: 101 additions & 0 deletions itest/send_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -687,3 +687,104 @@ func testMultiInputSendNonInteractiveSingleID(t *harnessTest) {
_ = sendProof(t, bobTapd, t.tapd, addr.ScriptKey, genInfo)
assertNonInteractiveRecvComplete(t, t.tapd, 1)
}

// testSendMultipleCoins tests that we can send multiple transfers at the same
// time if we have multiple managed UTXOs/asset coins available.
func testSendMultipleCoins(t *harnessTest) {
ctxb := context.Background()

// First, we'll make a normal assets with enough units to allow us to
// send it to different UTXOs
rpcAssets := mintAssetsConfirmBatch(
t, t.tapd, []*mintrpc.MintAssetRequest{simpleAssets[0]},
)

genInfo := rpcAssets[0].AssetGenesis

// Now that we have the asset created, we'll make a new node that'll
// serve as the node which'll receive the assets. The existing tapd
// node will be used to synchronize universe state.
secondTapd := setupTapdHarness(
t.t, t, t.lndHarness.Bob, t.universeServer,
func(params *tapdHarnessParams) {
params.startupSyncNode = t.tapd
params.startupSyncNumAssets = len(rpcAssets)
},
)
defer func() {
require.NoError(t.t, secondTapd.stop(!*noDelete))
}()

// Next, we split the asset into 5 different UTXOs, each with 1k units.
const (
numParts = 5
unitsPerPart = 1000
)
addrs := make([]*taprpc.Addr, numParts)
for i := 0; i < numParts; i++ {
newAddr, err := t.tapd.NewAddr(ctxb, &taprpc.NewAddrRequest{
AssetId: genInfo.AssetId,
Amt: unitsPerPart,
})
require.NoError(t.t, err)

assertAddrCreated(t.t, t.tapd, rpcAssets[0], newAddr)
addrs[i] = newAddr
}

// We created 5 addresses in our first node now, so we can initiate the
// transfer to send the coins back to our wallet in 5 pieces now.
sendResp := sendAssetsToAddr(t, t.tapd, addrs...)
confirmAndAssetOutboundTransferWithOutputs(
t, t.tapd, sendResp, genInfo.AssetId, []uint64{
0, unitsPerPart, unitsPerPart, unitsPerPart,
unitsPerPart, unitsPerPart,
}, 0, 1, numParts+1,
)
assertNonInteractiveRecvComplete(t, t.tapd, 5)

// Next, we'll attempt to complete 5 parallel transfers with distinct
// addresses from our main node to Bob.
bobAddrs := make([]*taprpc.Addr, numParts)
for i := 0; i < numParts; i++ {
var err error
bobAddrs[i], err = secondTapd.NewAddr(
ctxb, &taprpc.NewAddrRequest{
AssetId: genInfo.AssetId,
Amt: unitsPerPart,
},
)
require.NoError(t.t, err)

sendResp := sendAssetsToAddr(t, t.tapd, bobAddrs[i])
assertAssetOutboundTransferWithOutputs(
t, t.tapd, sendResp, genInfo.AssetId,
[]uint64{0, unitsPerPart}, i+1, i+2, 2, false,
)
}

// Before we mine the next block, we'll make sure that we get a proper
// error message when trying to send more assets (there are currently no
// asset UTXOs available).
bobAddr, err := secondTapd.NewAddr(ctxb, &taprpc.NewAddrRequest{
AssetId: genInfo.AssetId,
Amt: 1,
})
require.NoError(t.t, err)

_, err = t.tapd.SendAsset(ctxb, &taprpc.SendAssetRequest{
TapAddrs: []string{bobAddr.Encoded},
})
require.ErrorContains(
t.t, err, "failed to find coin(s) that satisfy given "+
"constraints",
)

// Now we confirm the 5 transfers and make sure they complete as
// expected.
_ = mineBlocks(t, t.lndHarness, 1, 5)
for _, addr := range bobAddrs {
_ = sendProof(t, t.tapd, secondTapd, addr.ScriptKey, genInfo)
}
assertNonInteractiveRecvComplete(t, secondTapd, 5)
}
4 changes: 4 additions & 0 deletions itest/test_list_on_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ var testCases = []*testCase{
test: testBasicSendPassiveAsset,
proofCourierType: proof.ApertureCourier,
},
{
name: "send multiple coins",
test: testSendMultipleCoins,
},
{
name: "multi input send non-interactive single ID",
test: testMultiInputSendNonInteractiveSingleID,
Expand Down
4 changes: 4 additions & 0 deletions perms/perms.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,10 @@ var (
Entity: "assets",
Action: "read",
}},
"/assetwalletrpc.AssetWallet/RemoveUTXOLease": {{
Entity: "assets",
Action: "write",
}},
"/mintrpc.Mint/MintAsset": {{
Entity: "mint",
Action: "write",
Expand Down
59 changes: 50 additions & 9 deletions rpcserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -536,8 +536,14 @@ func (r *rpcServer) checkBalanceOverflow(ctx context.Context,
func (r *rpcServer) ListAssets(ctx context.Context,
req *taprpc.ListAssetRequest) (*taprpc.ListAssetResponse, error) {

switch {
case req.IncludeSpent && req.IncludeLeased:
return nil, fmt.Errorf("cannot specify both include_spent " +
"and include_leased")
}

rpcAssets, err := r.fetchRpcAssets(
ctx, req.WithWitness, req.IncludeSpent,
ctx, req.WithWitness, req.IncludeSpent, req.IncludeLeased,
)
if err != nil {
return nil, err
Expand All @@ -548,10 +554,12 @@ func (r *rpcServer) ListAssets(ctx context.Context,
}, nil
}

func (r *rpcServer) fetchRpcAssets(ctx context.Context,
withWitness, includeSpent bool) ([]*taprpc.Asset, error) {
func (r *rpcServer) fetchRpcAssets(ctx context.Context, withWitness,
includeSpent, includeLeased bool) ([]*taprpc.Asset, error) {

assets, err := r.cfg.AssetStore.FetchAllAssets(ctx, includeSpent, nil)
assets, err := r.cfg.AssetStore.FetchAllAssets(
ctx, includeSpent, includeLeased, nil,
)
if err != nil {
return nil, fmt.Errorf("unable to read chain assets: %w", err)
}
Expand Down Expand Up @@ -600,6 +608,11 @@ func (r *rpcServer) marshalChainAsset(ctx context.Context, a *tapdb.ChainAsset,
BlockHeight: a.AnchorBlockHeight,
}

if a.AnchorLeaseOwner != [32]byte{} {
rpcAsset.LeaseOwner = a.AnchorLeaseOwner[:]
rpcAsset.LeaseExpiry = a.AnchorLeaseExpiry.UTC().Unix()
}

return rpcAsset, nil
}

Expand Down Expand Up @@ -763,9 +776,9 @@ func (r *rpcServer) listBalancesByGroupKey(ctx context.Context,
// ListUtxos lists the UTXOs managed by the target daemon, and the assets they
// hold.
func (r *rpcServer) ListUtxos(ctx context.Context,
_ *taprpc.ListUtxosRequest) (*taprpc.ListUtxosResponse, error) {
req *taprpc.ListUtxosRequest) (*taprpc.ListUtxosResponse, error) {

rpcAssets, err := r.fetchRpcAssets(ctx, false, false)
rpcAssets, err := r.fetchRpcAssets(ctx, false, false, req.IncludeLeased)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -1566,7 +1579,7 @@ func (r *rpcServer) AnchorVirtualPsbts(ctx context.Context,
prevID := vPacket.Inputs[0].PrevID
inputCommitment, err := r.cfg.AssetStore.FetchCommitment(
ctx, inputAsset.ID(), prevID.OutPoint, inputAsset.GroupKey,
&inputAsset.ScriptKey,
&inputAsset.ScriptKey, true,
)
if err != nil {
return nil, fmt.Errorf("error fetching input commitment: %w",
Expand Down Expand Up @@ -2794,7 +2807,7 @@ func (r *rpcServer) QueryProof(ctx context.Context,
return r.marshalIssuanceProof(ctx, req, proof)
}

// unmarsalAssetLeaf unmarshals an asset leaf from the RPC form.
// unmarshalAssetLeaf unmarshals an asset leaf from the RPC form.
func unmarshalAssetLeaf(leaf *unirpc.AssetLeaf) (*universe.MintingLeaf, error) {
// We'll just pull the asset details from the serialized issuance proof
// itself.
Expand Down Expand Up @@ -3101,7 +3114,7 @@ func (r *rpcServer) ProveAssetOwnership(ctx context.Context,
inputAsset := lastSnapshot.Asset
inputCommitment, err := r.cfg.AssetStore.FetchCommitment(
ctx, inputAsset.ID(), lastSnapshot.OutPoint,
inputAsset.GroupKey, &inputAsset.ScriptKey,
inputAsset.GroupKey, &inputAsset.ScriptKey, false,
)
if err != nil {
return nil, fmt.Errorf("error fetching commitment: %w", err)
Expand Down Expand Up @@ -3322,3 +3335,31 @@ func (r *rpcServer) QueryEvents(ctx context.Context,

return rpcStats, nil
}

// RemoveUTXOLease removes the lease/lock/reservation of the given managed
// UTXO.
func (r *rpcServer) RemoveUTXOLease(ctx context.Context,
in *wrpc.RemoveUTXOLeaseRequest) (*wrpc.RemoveUTXOLeaseResponse,
error) {

if in.Outpoint == nil {
return nil, fmt.Errorf("outpoint must be specified")
}

hash, err := chainhash.NewHash(in.Outpoint.Txid)
if err != nil {
return nil, fmt.Errorf("error parsing txid: %w", err)
}

outPoint := wire.OutPoint{
Hash: *hash,
Index: in.Outpoint.OutputIndex,
}

err = r.cfg.CoinSelect.ReleaseCoins(ctx, outPoint)
if err != nil {
return nil, err
}

return &wrpc.RemoveUTXOLeaseResponse{}, nil
}
15 changes: 8 additions & 7 deletions tapcfg/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ func genServerConfig(cfg *Config, cfgLogger btclog.Logger,
return nil, fmt.Errorf("unable to open database: %v", err)
}

defaultClock := clock.NewDefaultClock()
rksDB := tapdb.NewTransactionExecutor(
db, func(tx *sql.Tx) tapdb.KeyStore {
return db.WithTx(tx)
Expand All @@ -86,7 +87,7 @@ func genServerConfig(cfg *Config, cfgLogger btclog.Logger,
)
tapChainParams := address.ParamsForChain(cfg.ActiveNetParams.Name)
tapdbAddrBook := tapdb.NewTapAddressBook(
addrBookDB, &tapChainParams,
addrBookDB, &tapChainParams, defaultClock,
)

keyRing := tap.NewLndRpcKeyRing(lndServices)
Expand All @@ -100,7 +101,7 @@ func genServerConfig(cfg *Config, cfgLogger btclog.Logger,
Chain: tapChainParams,
})

assetStore := tapdb.NewAssetStore(assetDB)
assetStore := tapdb.NewAssetStore(assetDB, defaultClock)

uniDB := tapdb.NewTransactionExecutor(
db, func(tx *sql.Tx) tapdb.BaseUniverseStore {
Expand All @@ -119,9 +120,7 @@ func genServerConfig(cfg *Config, cfgLogger btclog.Logger,
return db.WithTx(tx)
},
)
universeStats := tapdb.NewUniverseStats(
uniStatsDB, clock.NewDefaultClock(),
)
universeStats := tapdb.NewUniverseStats(uniStatsDB, defaultClock)

headerVerifier := tapgarden.GenHeaderVerifier(
context.Background(), chainBridge,
Expand All @@ -142,7 +141,9 @@ func genServerConfig(cfg *Config, cfgLogger btclog.Logger,
return db.WithTx(tx)
},
)
federationDB := tapdb.NewUniverseFederationDB(federationStore)
federationDB := tapdb.NewUniverseFederationDB(
federationStore, defaultClock,
)

proofFileStore, err := proof.NewFileArchiver(cfg.networkDir)
if err != nil {
Expand Down Expand Up @@ -261,9 +262,9 @@ func genServerConfig(cfg *Config, cfgLogger btclog.Logger,
AddrBook: addrBook,
ProofArchive: proofArchive,
AssetWallet: assetWallet,
CoinSelect: coinSelect,
ChainPorter: tapfreighter.NewChainPorter(
&tapfreighter.ChainPorterConfig{
CoinSelector: coinSelect,
Signer: virtualTxSigner,
TxValidator: &tap.ValidatorV0{},
ExportLog: assetStore,
Expand Down
Loading