Repository: wizardsardine/liana
Pull request #605 (2023-08-11 10:17:01)

Add labels support to lianad


Patch:
diff --git a/doc/API.md b/doc/API.md
index 68c7a1f6..887cfc4d 100644
--- a/doc/API.md
+++ b/doc/API.md
@@ -20,6 +20,8 @@ Commands must be sent as valid JSONRPC 2.0 requests, ending with a `\n`.
 | [`listconfirmed`](#listconfirmed)                           | List of confirmed transactions of incoming and outgoing funds |
 | [`listtransactions`](#listtransactions)                     | List of transactions with the given txids                     |
 | [`createrecovery`](#createrecovery)                         | Create a recovery transaction to sweep expired coins          |
+| [`updatelabels`](#updatelabels)                             | Update the labels                                             |
+| [`getlabels`](#getlabels)                                   | Get the labels for the given addresses, txids and outpoints   |
 
 # Reference
 
@@ -92,6 +94,7 @@ This command does not take any parameter for now.
 
 | Field          | Type          | Description                                                                                                        |
 | -------------- | ------------- | ------------------------------------------------------------------------------------------------------------------ |
+| `address`      | string        | Address containing the script pubkey of the coin                                                                   |
 | `amount`       | int           | Value of the TxO in satoshis.                                                                                      |
 | `outpoint`     | string        | Transaction id and output index of this coin.                                                                      |
 | `block_height` | int or null   | Block height the transaction was confirmed at, or `null`.                                                          |
@@ -300,3 +303,31 @@ cover the requested feerate.
 | Field          | Type      | Description                                          |
 | -------------- | --------- | ---------------------------------------------------- |
 | `psbt`         | string    | PSBT of the recovery transaction, encoded as base64. |
+
+### `updatelabels`
+
+Update the labels from a given map of key/value, with the labelled bitcoin addresses, txids and outpoints as keys
+and the label as value. If a label already exist for the given item, the new label overrides the previous one. 
+
+#### Request
+
+| Field    | Type   | Description                                                                                                           |
+| -------- | ------ | --------------------------------------------------------------------------------------------------------------------- |
+| `labels` | object | A mapping from an item to be labelled (an address, a txid or an outpoint) to a label string (at most 100 chars long). |
+
+### `getlabels`
+
+Retrieve a map of items and their respective labels from a list of addresses, txids and outpoints.
+Items without labels are not present in the response map.
+
+#### Request
+
+| Field   | Type         | Description                                                    |
+| --------| ------------ | -------------------------------------------------------------- |
+| `items` | string array | Items (address, txid or outpoint) of which to fetch the label. | 
+
+#### Response
+
+| Field    | Type   | Description                                                                      |
+| -------- | ------ | -------------------------------------------------------------------------------- |
+| `labels` | object | A mapping of bitcoin addresses, txids and oupoints as keys, and string as values |
diff --git a/doc/RECOVER.md b/doc/RECOVER.md
deleted file mode 100644
index 36ee185a..00000000
--- a/doc/RECOVER.md
+++ /dev/null
@@ -1,169 +0,0 @@
-Here a step by step walkthrough to recover a Liana wallet only using bitcoin-core and HWI utility.
-Commands are given for signet, but you can run it in mainnet simply by not using the signet options.
-
-We assume you already have a bitcoind/bitcoin-cli running on your machine.
-
-### Create wallet in bitcoin core
-
-```shell
-bitcoin-cli -signet createwallet 'liana' true 
-```
-
-output:
-```
-{
-  "name": "liana"
-}
-```
-
-### Process & import descriptor
-
-As PR[#22838](https://github.com/bitcoin/bitcoin/pull/22838) not (yet) merged in bitcoin core, we need to 'split'
-descriptor in 2: 
- - 1 for receive addresses 
- - 1 for changes addresses.
-
-#### Original descriptor:
- - wsh(or_d(pk([a5c6b76e/48'/1'/0'/2']tpubDF5861hj6vR3iJr3aPjGJz4rNbqDCRujQ21mczzKT5SiedaQqNVgHC8HT9ceyxvMFRoPMx4P6HAcL3NZrUPhRUbwCyj3TKSa64bAfnE3sLh/`<0;1>`/\*),and_v(v:pkh([c477fd13/48'/1'/0'/2']tpubDFn7iPbFqGrTQ2aRACNsUK1MXQR4Z6dYfU2nD1WA9ifSaia642j3Wah4n5pBUEpERNWGJsyv3Dv5qwBabC9TLQrwSboKzukw9wmurGu7XVH/`<0;1>`/\*),older(3))))`#8ldsjayd`
-
-Here we need to process 2 descriptors from the original one, replacing `<0;1>` by `0` for the receive descriptor and `1` for change descriptor, checksum might to be updated for each ones.
-
-#### Processed descriptors:
-
- - wsh(or_d(pk([a5c6b76e/48'/1'/0'/2']tpubDF5861hj6vR3iJr3aPjGJz4rNbqDCRujQ21mczzKT5SiedaQqNVgHC8HT9ceyxvMFRoPMx4P6HAcL3NZrUPhRUbwCyj3TKSa64bAfnE3sLh/`0`/\*),and_v(v:pkh([c477fd13/48'/1'/0'/2']tpubDFn7iPbFqGrTQ2aRACNsUK1MXQR4Z6dYfU2nD1WA9ifSaia642j3Wah4n5pBUEpERNWGJsyv3Dv5qwBabC9TLQrwSboKzukw9wmurGu7XVH/`0`/\*),older(3))))
-
-
- - wsh(or_d(pk([a5c6b76e/48'/1'/0'/2']tpubDF5861hj6vR3iJr3aPjGJz4rNbqDCRujQ21mczzKT5SiedaQqNVgHC8HT9ceyxvMFRoPMx4P6HAcL3NZrUPhRUbwCyj3TKSa64bAfnE3sLh/`1`/\*),and_v(v:pkh([c477fd13/48'/1'/0'/2']tpubDFn7iPbFqGrTQ2aRACNsUK1MXQR4Z6dYfU2nD1WA9ifSaia642j3Wah4n5pBUEpERNWGJsyv3Dv5qwBabC9TLQrwSboKzukw9wmurGu7XVH/`1`/\*),older(3))))
-
-Now we got the descriptors without checksums, we can process it using bitcoin-cli getdescriptorinfo method:
-
-```shell
-bitcoin-cli -signet getdescriptorinfo "wsh(or_d(pk([a5c6b76e/48'/1'/0'/2']tpubDF5861hj6vR3iJr3aPjGJz4rNbqDCRujQ21mczzKT5SiedaQqNVgHC8HT9ceyxvMFRoPMx4P6HAcL3NZrUPhRUbwCyj3TKSa64bAfnE3sLh/0/*),and_v(v:pkh([c477fd13/48'/1'/0'/2']tpubDFn7iPbFqGrTQ2aRACNsUK1MXQR4Z6dYfU2nD1WA9ifSaia642j3Wah4n5pBUEpERNWGJsyv3Dv5qwBabC9TLQrwSboKzukw9wmurGu7XVH/0/*),older(3))))"
-```
-
-output:
-``` 
-{
-  "descriptor": "wsh(or_d(pk([a5c6b76e/48'/1'/0'/2']tpubDF5861hj6vR3iJr3aPjGJz4rNbqDCRujQ21mczzKT5SiedaQqNVgHC8HT9ceyxvMFRoPMx4P6HAcL3NZrUPhRUbwCyj3TKSa64bAfnE3sLh/0/*),and_v(v:pkh([c477fd13/48'/1'/0'/2']tpubDFn7iPbFqGrTQ2aRACNsUK1MXQR4Z6dYfU2nD1WA9ifSaia642j3Wah4n5pBUEpERNWGJsyv3Dv5qwBabC9TLQrwSboKzukw9wmurGu7XVH/0/*),older(3))))#nhtumqkr",
-  "checksum": "nhtumqkr",
-  "isrange": true,
-  "issolvable": true,
-  "hasprivatekeys": false
-}
-```
-
-```shell
-bitcoin-cli -signet getdescriptorinfo "wsh(or_d(pk([a5c6b76e/48'/1'/0'/2']tpubDF5861hj6vR3iJr3aPjGJz4rNbqDCRujQ21mczzKT5SiedaQqNVgHC8HT9ceyxvMFRoPMx4P6HAcL3NZrUPhRUbwCyj3TKSa64bAfnE3sLh/1/*),and_v(v:pkh([c477fd13/48'/1'/0'/2']tpubDFn7iPbFqGrTQ2aRACNsUK1MXQR4Z6dYfU2nD1WA9ifSaia642j3Wah4n5pBUEpERNWGJsyv3Dv5qwBabC9TLQrwSboKzukw9wmurGu7XVH/1/*),older(3))))"
-```
-
-output:
-``` 
-{
-  "descriptor": "wsh(or_d(pk([a5c6b76e/48'/1'/0'/2']tpubDF5861hj6vR3iJr3aPjGJz4rNbqDCRujQ21mczzKT5SiedaQqNVgHC8HT9ceyxvMFRoPMx4P6HAcL3NZrUPhRUbwCyj3TKSa64bAfnE3sLh/1/*),and_v(v:pkh([c477fd13/48'/1'/0'/2']tpubDFn7iPbFqGrTQ2aRACNsUK1MXQR4Z6dYfU2nD1WA9ifSaia642j3Wah4n5pBUEpERNWGJsyv3Dv5qwBabC9TLQrwSboKzukw9wmurGu7XVH/1/*),older(3))))#vpa5k5p6",
-  "checksum": "vpa5k5p6",
-  "isrange": true,
-  "issolvable": true,
-  "hasprivatekeys": false
-}
-```
-
-The command to pass to bitcoin-cli to import descriptors should look like this:
-
-bitcoin-cli -signet -rpcwallet=liana importdescriptors "[{\"desc\": \"`<descriptor_receive>`\", \"range\": [0,10000], \"timestamp\": `<wallet_birthdate>`}, {\"desc\": \"`<descriptor_change>`\", \"range\": [0,10000], \"timestamp\": `<wallet_birthdate>`}]"
-
- - `<wallet_birthdate>` should be replaced by unix timestamp of your wallet birthdate(if you don't know about the wallet birthdate you can use  1671577200 -Liana V0.1 birthdate-, note that using a lower timestamp than the wallet birthdate will increase the rescan duration)
- - `<descriptor_receive>` and `<descriptor_change>` sould be replaced by relevant descriptors (including new generated checksums):
-
-```shell
-bitcoin-cli -signet -rpcwallet=liana importdescriptors "[{\"desc\": \"wsh(or_d(pk([a5c6b76e/48'/1'/0'/2']tpubDF5861hj6vR3iJr3aPjGJz4rNbqDCRujQ21mczzKT5SiedaQqNVgHC8HT9ceyxvMFRoPMx4P6HAcL3NZrUPhRUbwCyj3TKSa64bAfnE3sLh/0/*),and_v(v:pkh([c477fd13/48'/1'/0'/2']tpubDFn7iPbFqGrTQ2aRACNsUK1MXQR4Z6dYfU2nD1WA9ifSaia642j3Wah4n5pBUEpERNWGJsyv3Dv5qwBabC9TLQrwSboKzukw9wmurGu7XVH/0/*),older(3))))#nhtumqkr\", \"range\": [0,10000], \"timestamp\": 1682920310, \"active\": true, \"internal\":false}, {\"desc\": \"wsh(or_d(pk([a5c6b76e/48'/1'/0'/2']tpubDF5861hj6vR3iJr3aPjGJz4rNbqDCRujQ21mczzKT5SiedaQqNVgHC8HT9ceyxvMFRoPMx4P6HAcL3NZrUPhRUbwCyj3TKSa64bAfnE3sLh/1/*),and_v(v:pkh([c477fd13/48'/1'/0'/2']tpubDFn7iPbFqGrTQ2aRACNsUK1MXQR4Z6dYfU2nD1WA9ifSaia642j3Wah4n5pBUEpERNWGJsyv3Dv5qwBabC9TLQrwSboKzukw9wmurGu7XVH/1/*),older(3))))#vpa5k5p6\", \"range\": [0,10000], \"timestamp\": 1682920310, \"active\": true, \"internal\":true}]"
-```
-
-output:
-```shell
-[
-  {
-    "success": true
-  },
-  {
-    "success": true
-  }
-]
-```
-
-### Build the sweep transaction
-
-```shell 
-bitcoin-cli -signet -rpcwallet=liana sendall '["tb1qed7lyessqcecav5uxultf6zc8nefd9kalgaa7dwrglcc6ld5vd3qe20spe"]' null "unset" 20
-```
-(here we define fee at `20 sats/VBytes`)
-
-output:
-```
-{
-  "psbt": "cHNidP8BAF4CAAAAATEgOFWAFkRN09sCK0V1ES9UyMiUQRJrb0AG4j1y0q++AQAAAAD9////ARg4DwAAAAAAIgAgy33yZhAGM46ynDc+tOhYPPKWlt36O981w0fxjX20Y2IAAAAAAAEAfQIAAAABnk7wzbybA7Z6kvlO7hFYl5x2BN3L0otR5M5udXzGN/oBAAAAAP7///8CrKE2EQAAAAAWABRnUU8zi4M3eMplKigSvOdhtGuSdUBCDwAAAAAAIgAgxBhOa53udP66VGgn+vs7sboB8aXyJj4WddV6PsUjJvHoZAIAAQErQEIPAAAAAAAiACDEGE5rne50/rpUaCf6+zuxugHxpfImPhZ11Xo+xSMm8QEFQSEDFPd+lcxYMUvbhW51TN6Fzc5LFtQVTYbZBUxuEEpHHoCsc2R2qRRX/S6WeWl7J1PJvN+73IONRKHJYYitU7JoIgYDFPd+lcxYMUvbhW51TN6Fzc5LFtQVTYbZBUxuEEpHHoAcpca3bjAAAIABAACAAAAAgAIAAIAAAAAAAAAAACIGA2u7xjrIFe7ShVfXgPOqQwzKtxYkFeV52rNpNyzHfrNdHMR3/RMwAACAAQAAgAAAAIACAACAAAAAAAAAAAAAAA==",
-  "complete": false
-}
-```
-
-### Install hwi
-As [PR647(wallet policies)](https://github.com/bitcoin-core/HWI/pull/647) is not (yet) merged into HWI we need to install it from git:
-
-#### Download the sources
-```shell
-git clone https://github.com/bitcoin-core/HWI
-cd HWI
-git fetch origin pull/647/head
-git checkout FETCH_HEAD
-```
-
-#### Virtual environment & install
-```shell
-python3 -m venv venv
-. venv/bin/activate
-pip install poetry
-poetry install
-```
-
-### Register policy on ledger nano:
-
-Here we need to do (one more time) some processing on the descriptor format: the XPUBs might be extracted from descriptor and supplied as a list of keys (fingerprint + derivation path might be specified only for te key stored in the ledger nano)
-
-```shell
-python3 -m hwi --chain test --device-type ledger registerpolicy --policy "wsh(or_d(pk(@0/**),and_v(v:pkh(@1/**),older(3))))" --name "Liana" --keys "[\"[a5c6b76e/48'/1'/0'/2']tpubDF5861hj6vR3iJr3aPjGJz4rNbqDCRujQ21mczzKT5SiedaQqNVgHC8HT9ceyxvMFRoPMx4P6HAcL3NZrUPhRUbwCyj3TKSa64bAfnE3sLh\",\"tpubDFn7iPbFqGrTQ2aRACNsUK1MXQR4Z6dYfU2nD1WA9ifSaia642j3Wah4n5pBUEpERNWGJsyv3Dv5qwBabC9TLQrwSboKzukw9wmurGu7XVH\"]"
-```
-
-output
-``` 
-{"proof_of_registration": "34602db12b76200ea96386c4cd10b8b1d60b84e060d2d6120073c06367a9506d"}
-```
-
-### Sign PSBT with ledger nano:
-
-```shell
-python3 -m hwi --chain test --device-type ledger signtx cHNidP8BAF4CAAAAATEgOFWAFkRN09sCK0V1ES9UyMiUQRJrb0AG4j1y0q++AQAAAAD9////ARg4DwAAAAAAIgAgy33yZhAGM46ynDc+tOhYPPKWlt36O981w0fxjX20Y2IAAAAAAAEAfQIAAAABnk7wzbybA7Z6kvlO7hFYl5x2BN3L0otR5M5udXzGN/oBAAAAAP7///8CrKE2EQAAAAAWABRnUU8zi4M3eMplKigSvOdhtGuSdUBCDwAAAAAAIgAgxBhOa53udP66VGgn+vs7sboB8aXyJj4WddV6PsUjJvHoZAIAAQErQEIPAAAAAAAiACDEGE5rne50/rpUaCf6+zuxugHxpfImPhZ11Xo+xSMm8QEFQSEDFPd+lcxYMUvbhW51TN6Fzc5LFtQVTYbZBUxuEEpHHoCsc2R2qRRX/S6WeWl7J1PJvN+73IONRKHJYYitU7JoIgYDFPd+lcxYMUvbhW51TN6Fzc5LFtQVTYbZBUxuEEpHHoAcpca3bjAAAIABAACAAAAAgAIAAIAAAAAAAAAAACIGA2u7xjrIFe7ShVfXgPOqQwzKtxYkFeV52rNpNyzHfrNdHMR3/RMwAACAAQAAgAAAAIACAACAAAAAAAAAAAAAAA== --policy "wsh(or_d(pk(@0/**),and_v(v:pkh(@1/**),older(3))))" --name "Liana" --keys "[\"[a5c6b76e/48'/1'/0'/2']tpubDF5861hj6vR3iJr3aPjGJz4rNbqDCRujQ21mczzKT5SiedaQqNVgHC8HT9ceyxvMFRoPMx4P6HAcL3NZrUPhRUbwCyj3TKSa64bAfnE3sLh\",\"tpubDFn7iPbFqGrTQ2aRACNsUK1MXQR4Z6dYfU2nD1WA9ifSaia642j3Wah4n5pBUEpERNWGJsyv3Dv5qwBabC9TLQrwSboKzukw9wmurGu7XVH\"]" --extra "{\"proof_of_registration\": \"34602db12b76200ea96386c4cd10b8b1d60b84e060d2d6120073c06367a9506d\"}"
-```
-
-output:
-``` 
-{"psbt": "cHNidP8BAF4CAAAAATEgOFWAFkRN09sCK0V1ES9UyMiUQRJrb0AG4j1y0q++AQAAAAD9////ARg4DwAAAAAAIgAgy33yZhAGM46ynDc+tOhYPPKWlt36O981w0fxjX20Y2IAAAAAAAEAfQIAAAABnk7wzbybA7Z6kvlO7hFYl5x2BN3L0otR5M5udXzGN/oBAAAAAP7///8CrKE2EQAAAAAWABRnUU8zi4M3eMplKigSvOdhtGuSdUBCDwAAAAAAIgAgxBhOa53udP66VGgn+vs7sboB8aXyJj4WddV6PsUjJvHoZAIAAQErQEIPAAAAAAAiACDEGE5rne50/rpUaCf6+zuxugHxpfImPhZ11Xo+xSMm8SICAxT3fpXMWDFL24VudUzehc3OSxbUFU2G2QVMbhBKRx6ASDBFAiEA1h9wI6XI+3Jz61f3rrR9L/6guHbcG7/Dl0a2CmEhWlICIFcwZKqKWe96gUkIQctln6K9R9U4nHx3qJxrHUhmkuf/AQEFQSEDFPd+lcxYMUvbhW51TN6Fzc5LFtQVTYbZBUxuEEpHHoCsc2R2qRRX/S6WeWl7J1PJvN+73IONRKHJYYitU7JoIgYDFPd+lcxYMUvbhW51TN6Fzc5LFtQVTYbZBUxuEEpHHoAcpca3bjAAAIABAACAAAAAgAIAAIAAAAAAAAAAACIGA2u7xjrIFe7ShVfXgPOqQwzKtxYkFeV52rNpNyzHfrNdHMR3/RMwAACAAQAAgAAAAIACAACAAAAAAAAAAAAAAA==", "signed": true}
-```
-
-### Finalize PSBT:
-
-```shell
-bitcoin-cli -signet finalizepsbt cHNidP8BAF4CAAAAATEgOFWAFkRN09sCK0V1ES9UyMiUQRJrb0AG4j1y0q++AQAAAAD9////ARg4DwAAAAAAIgAgy33yZhAGM46ynDc+tOhYPPKWlt36O981w0fxjX20Y2IAAAAAAAEAfQIAAAABnk7wzbybA7Z6kvlO7hFYl5x2BN3L0otR5M5udXzGN/oBAAAAAP7///8CrKE2EQAAAAAWABRnUU8zi4M3eMplKigSvOdhtGuSdUBCDwAAAAAAIgAgxBhOa53udP66VGgn+vs7sboB8aXyJj4WddV6PsUjJvHoZAIAAQErQEIPAAAAAAAiACDEGE5rne50/rpUaCf6+zuxugHxpfImPhZ11Xo+xSMm8SICAxT3fpXMWDFL24VudUzehc3OSxbUFU2G2QVMbhBKRx6ASDBFAiEA1h9wI6XI+3Jz61f3rrR9L/6guHbcG7/Dl0a2CmEhWlICIFcwZKqKWe96gUkIQctln6K9R9U4nHx3qJxrHUhmkuf/AQEFQSEDFPd+lcxYMUvbhW51TN6Fzc5LFtQVTYbZBUxuEEpHHoCsc2R2qRRX/S6WeWl7J1PJvN+73IONRKHJYYitU7JoIgYDFPd+lcxYMUvbhW51TN6Fzc5LFtQVTYbZBUxuEEpHHoAcpca3bjAAAIABAACAAAAAgAIAAIAAAAAAAAAAACIGA2u7xjrIFe7ShVfXgPOqQwzKtxYkFeV52rNpNyzHfrNdHMR3/RMwAACAAQAAgAAAAIACAACAAAAAAAAAAAAAAA==
-```
-
-output:
-``` 
-{
-  "hex": "02000000000101312038558016444dd3db022b4575112f54c8c89441126b6f4006e23d72d2afbe0100000000fdffffff0118380f0000000000220020cb7df2661006338eb29c373eb4e8583cf29696ddfa3bdf35c347f18d7db4636202483045022100d61f7023a5c8fb7273eb57f7aeb47d2ffea0b876dc1bbfc39746b60a61215a520220573064aa8a59ef7a81490841cb659fa2bd47d5389c7c77a89c6b1d486692e7ff0141210314f77e95cc58314bdb856e754cde85cdce4b16d4154d86d9054c6e104a471e80ac736476a91457fd2e9679697b2753c9bcdfbbdc838d44a1c96188ad53b26800000000",
-  "complete": true
-}
-```
-
-### Broadcast PSBT:
-
-```shell 
-bitcoin-cli -signet sendrawtransaction 02000000000101312038558016444dd3db022b4575112f54c8c89441126b6f4006e23d72d2afbe0100000000fdffffff0118380f0000000000220020cb7df2661006338eb29c373eb4e8583cf29696ddfa3bdf35c347f18d7db4636202483045022100d61f7023a5c8fb7273eb57f7aeb47d2ffea0b876dc1bbfc39746b60a61215a520220573064aa8a59ef7a81490841cb659fa2bd47d5389c7c77a89c6b1d486692e7ff0141210314f77e95cc58314bdb856e754cde85cdce4b16d4154d86d9054c6e104a471e80ac736476a91457fd2e9679697b2753c9bcdfbbdc838d44a1c96188ad53b26800000000
-```
\ No newline at end of file
diff --git a/src/commands/mod.rs b/src/commands/mod.rs
index 168b1f1f..4c264a77 100644
--- a/src/commands/mod.rs
+++ b/src/commands/mod.rs
@@ -10,13 +10,15 @@ use crate::{
     descriptors, DaemonControl, VERSION,
 };
 
+pub use crate::database::LabelItem;
+
 use utils::{
     deser_addr_assume_checked, deser_amount_from_sats, deser_fromstr, deser_hex, ser_amount,
     ser_hex, ser_to_string,
 };
 
 use std::{
-    collections::{hash_map, BTreeMap, HashMap},
+    collections::{hash_map, BTreeMap, HashMap, HashSet},
     convert::TryInto,
     fmt,
 };
@@ -308,7 +310,11 @@ impl DaemonControl {
                     height: spend_block.map(|b| b.height),
                 });
                 let block_height = block_info.map(|b| b.height);
+                let address = self
+                    .derived_desc(&coin)
+                    .address(self.config.bitcoin_config.network);
                 ListCoinsEntry {
+                    address,
                     amount,
                     outpoint,
                     block_height,
@@ -575,6 +581,18 @@ impl DaemonControl {
         Ok(())
     }
 
+    pub fn update_labels(&self, items: &HashMap<LabelItem, String>) {
+        let mut db_conn = self.db.connection();
+        db_conn.update_labels(items);
+    }
+
+    pub fn get_labels(&self, items: &HashSet<LabelItem>) -> GetLabelsResult {
+        let mut db_conn = self.db.connection();
+        GetLabelsResult {
+            labels: db_conn.labels(items),
+        }
+    }
+
     pub fn list_spend(&self) -> ListSpendResult {
         let mut db_conn = self.db.connection();
         let spend_txs = db_conn
@@ -820,6 +838,11 @@ pub struct GetAddressResult {
     address: bitcoin::Address,
 }
 
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct GetLabelsResult {
+    pub labels: HashMap<String, String>,
+}
+
 impl GetAddressResult {
     pub fn new(address: bitcoin::Address) -> Self {
         Self { address }
@@ -837,7 +860,7 @@ pub struct LCSpendInfo {
     pub height: Option<i32>,
 }
 
-#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
+#[derive(Debug, Clone, Serialize, Deserialize)]
 pub struct ListCoinsEntry {
     #[serde(
         serialize_with = "ser_amount",
@@ -845,6 +868,11 @@ pub struct ListCoinsEntry {
     )]
     pub amount: bitcoin::Amount,
     pub outpoint: bitcoin::OutPoint,
+    #[serde(
+        serialize_with = "ser_to_string",
+        deserialize_with = "deser_addr_assume_checked"
+    )]
+    pub address: bitcoin::Address,
     pub block_height: Option<i32>,
     /// Information about the transaction spending this coin.
     pub spend_info: Option<LCSpendInfo>,
diff --git a/src/database/mod.rs b/src/database/mod.rs
index 21b5a32d..355d8024 100644
--- a/src/database/mod.rs
+++ b/src/database/mod.rs
@@ -12,7 +12,13 @@ use crate::{
     },
 };
 
-use std::{collections::HashMap, sync};
+use std::{
+    collections::{HashMap, HashSet},
+    fmt::Display,
+    iter::FromIterator,
+    str::FromStr,
+    sync,
+};
 
 use miniscript::bitcoin::{self, bip32, psbt::PartiallySignedTransaction as Psbt, secp256k1};
 
@@ -119,6 +125,10 @@ pub trait DatabaseConnection {
     /// Delete a Spend transaction from database.
     fn delete_spend(&mut self, txid: &bitcoin::Txid);
 
+    fn update_labels(&mut self, items: &HashMap<LabelItem, String>);
+
+    fn labels(&mut self, labels: &HashSet<LabelItem>) -> HashMap<String, String>;
+
     /// Mark the given tip as the new best seen block. Update stored data accordingly.
     fn rollback_tip(&mut self, new_tip: &BlockChainTip);
 
@@ -257,6 +267,15 @@ impl DatabaseConnection for SqliteConn {
         self.delete_spend(txid)
     }
 
+    fn update_labels(&mut self, items: &HashMap<LabelItem, String>) {
+        self.update_labels(items)
+    }
+
+    fn labels(&mut self, items: &HashSet<LabelItem>) -> HashMap<String, String> {
+        let labels = self.db_labels(items);
+        HashMap::from_iter(labels.into_iter().map(|label| (label.item, label.value)))
+    }
+
     fn rollback_tip(&mut self, new_tip: &BlockChainTip) {
         self.rollback_tip(new_tip)
     }
@@ -335,3 +354,56 @@ pub enum CoinType {
     Unspent,
     Spent,
 }
+
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub enum LabelItem {
+    Address(bitcoin::Address),
+    Txid(bitcoin::Txid),
+    OutPoint(bitcoin::OutPoint),
+}
+
+impl From<bitcoin::Address> for LabelItem {
+    fn from(value: bitcoin::Address) -> Self {
+        Self::Address(value)
+    }
+}
+
+impl From<bitcoin::Txid> for LabelItem {
+    fn from(value: bitcoin::Txid) -> Self {
+        Self::Txid(value)
+    }
+}
+
+impl From<bitcoin::OutPoint> for LabelItem {
+    fn from(value: bitcoin::OutPoint) -> Self {
+        Self::OutPoint(value)
+    }
+}
+
+impl Display for LabelItem {
+    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+        match self {
+            LabelItem::Address(a) => write!(f, "{}", a),
+            LabelItem::Txid(a) => write!(f, "{}", a),
+            LabelItem::OutPoint(a) => write!(f, "{}", a),
+        }
+    }
+}
+
+impl LabelItem {
+    pub fn from_str(s: &str, network: bitcoin::Network) -> Option<LabelItem> {
+        if let Ok(addr) = bitcoin::Address::from_str(s) {
+            if !addr.is_valid_for_network(network) {
+                None
+            } else {
+                Some(LabelItem::Address(addr.assume_checked()))
+            }
+        } else if let Ok(txid) = bitcoin::Txid::from_str(s) {
+            Some(LabelItem::Txid(txid))
+        } else if let Ok(outpoint) = bitcoin::OutPoint::from_str(s) {
+            Some(LabelItem::OutPoint(outpoint))
+        } else {
+            None
+        }
+    }
+}
diff --git a/src/database/sqlite/mod.rs b/src/database/sqlite/mod.rs
index d05e4663..93b124bb 100644
--- a/src/database/sqlite/mod.rs
+++ b/src/database/sqlite/mod.rs
@@ -14,18 +14,26 @@ use crate::{
     bitcoin::BlockChainTip,
     database::{
         sqlite::{
-            schema::{DbAddress, DbCoin, DbSpendTransaction, DbTip, DbWallet, SCHEMA},
+            schema::{
+                DbAddress, DbCoin, DbLabel, DbLabelledKind, DbSpendTransaction, DbTip, DbWallet,
+                SCHEMA,
+            },
             utils::{
                 create_fresh_db, curr_timestamp, db_exec, db_query, db_tx_query, db_version,
                 maybe_apply_migration, LOOK_AHEAD_LIMIT,
             },
         },
-        Coin, CoinType,
+        Coin, CoinType, LabelItem,
     },
     descriptors::LianaDescriptor,
 };
 
-use std::{cmp, convert::TryInto, fmt, io, path};
+use std::{
+    cmp,
+    collections::{HashMap, HashSet},
+    convert::TryInto,
+    fmt, io, path,
+};
 
 use miniscript::bitcoin::{
     self, bip32,
@@ -35,7 +43,7 @@ use miniscript::bitcoin::{
     secp256k1,
 };
 
-const DB_VERSION: i64 = 2;
+const DB_VERSION: i64 = 3;
 
 #[derive(Debug)]
 pub enum SqliteDbError {
@@ -554,6 +562,43 @@ impl SqliteConn {
         .expect("Db must not fail")
     }
 
+    pub fn update_labels(&mut self, items: &HashMap<LabelItem, String>) {
+        db_exec(&mut self.conn, |db_tx| {
+            for (labelled, kind, value) in items
+                .iter()
+                .map(|(a, v)| {
+                     match a {
+                         LabelItem::Address(a) =>(a.to_string(), DbLabelledKind::Address, v),
+                         LabelItem::Txid(a) =>(a.to_string(), DbLabelledKind::Txid, v),
+                         LabelItem::OutPoint(a) =>(a.to_string(), DbLabelledKind::OutPoint, v),
+                     }
+                }) {
+                db_tx.execute(
+                    "INSERT INTO labels (wallet_id, item, item_kind, value) VALUES (?1, ?2, ?3, ?4) \
+                    ON CONFLICT DO UPDATE SET value=excluded.value",
+                    rusqlite::params![WALLET_ID, labelled, kind as i64, value],
+                )?;
+            }
+            Ok(())
+        })
+        .expect("Db must not fail")
+    }
+
+    pub fn db_labels(&mut self, items: &HashSet<LabelItem>) -> Vec<DbLabel> {
+        let query = format!(
+            "SELECT * FROM labels where item in ({})",
+            items
+                .iter()
+                .map(|a| format!("'{}'", a))
+                .collect::<Vec<String>>()
+                .join(",")
+        );
+        db_query(&mut self.conn, &query, rusqlite::params![], |row| {
+            row.try_into()
+        })
+        .expect("Db must not fail")
+    }
+
     /// Retrieves a limited and ordered list of transactions ids that happened during the given
     /// range.
     pub fn db_list_txids(&mut self, start: u32, end: u32, limit: u64) -> Vec<bitcoin::Txid> {
@@ -812,6 +857,38 @@ CREATE TABLE spend_transactions (
         fs::remove_dir_all(tmp_dir).unwrap();
     }
 
+    #[test]
+    fn db_labels_update() {
+        let (tmp_dir, _, _, db) = dummy_db();
+
+        {
+            let txid_str = "0c62a990d20d54429e70859292e82374ba6b1b951a3ab60f26bb65fee5724ff7";
+            let txid = LabelItem::from_str(txid_str, bitcoin::Network::Bitcoin).unwrap();
+            let mut items = HashSet::new();
+            items.insert(txid.clone());
+
+            let mut conn = db.connection().unwrap();
+            let db_labels = conn.db_labels(&items);
+            assert!(db_labels.is_empty());
+
+            let mut txids_labels = HashMap::new();
+            txids_labels.insert(txid.clone(), "hello".to_string());
+
+            conn.update_labels(&txids_labels);
+
+            let db_labels = conn.db_labels(&items);
+            assert_eq!(db_labels[0].value, "hello");
+
+            txids_labels.insert(txid, "hello again".to_string());
+            conn.update_labels(&txids_labels);
+
+            let db_labels = conn.db_labels(&items);
+            assert_eq!(db_labels[0].value, "hello again");
+        }
+
+        fs::remove_dir_all(tmp_dir).unwrap();
+    }
+
     #[test]
     fn db_coins_update() {
         let (tmp_dir, _, _, db) = dummy_db();
@@ -1639,4 +1716,42 @@ CREATE TABLE spend_transactions (
 
         fs::remove_dir_all(tmp_dir).unwrap();
     }
+
+    #[test]
+    fn v0_to_v3_migration() {
+        let secp = secp256k1::Secp256k1::verification_only();
+
+        // Create a database with version 0, using the old schema.
+        let tmp_dir = tmp_dir();
+        fs::create_dir_all(&tmp_dir).unwrap();
+        let db_path: path::PathBuf = [tmp_dir.as_path(), path::Path::new("lianad_v0.sqlite3")]
+            .iter()
+            .collect();
+        let mut options = dummy_options();
+        options.schema = V0_SCHEMA;
+        options.version = 0;
+        create_fresh_db(&db_path, options, &secp).unwrap();
+
+        // SqliteDb new is doing the migration.
+        let db = SqliteDb::new(db_path, None, &secp).unwrap();
+
+        {
+            let mut conn = db.connection().unwrap();
+            let version = conn.db_version();
+            assert_eq!(version, 3);
+
+            let txid_str = "0c62a990d20d54429e70859292e82374ba6b1b951a3ab60f26bb65fee5724ff7";
+            let txid = LabelItem::from_str(txid_str, bitcoin::Network::Bitcoin).unwrap();
+            let mut txids_labels = HashMap::new();
+            txids_labels.insert(txid.clone(), "hello".to_string());
+            conn.update_labels(&txids_labels);
+
+            let mut items = HashSet::new();
+            items.insert(txid);
+            let db_labels = conn.db_labels(&items);
+            assert_eq!(db_labels[0].value, "hello");
+        }
+
+        fs::remove_dir_all(tmp_dir).unwrap();
+    }
 }
diff --git a/src/database/sqlite/schema.rs b/src/database/sqlite/schema.rs
index b4b4b7b7..7ebb280b 100644
--- a/src/database/sqlite/schema.rs
+++ b/src/database/sqlite/schema.rs
@@ -81,6 +81,15 @@ CREATE TABLE spend_transactions (
     txid BLOB UNIQUE NOT NULL,
     updated_at INTEGER
 );
+
+/* Labels applied on addresses (0), outpoints (1), txids (2) */
+CREATE TABLE labels (
+    id INTEGER PRIMARY KEY NOT NULL,
+    wallet_id INTEGER NOT NULL,
+    item_kind INTEGER NOT NULL CHECK (item_kind IN (0,1,2)),
+    item TEXT UNIQUE NOT NULL,
+    value TEXT NOT NULL
+);
 ";
 
 /// A row in the "tip" table.
@@ -293,3 +302,54 @@ impl TryFrom<&rusqlite::Row<'_>> for DbSpendTransaction {
         })
     }
 }
+
+/// A row in the "labels" table
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct DbLabel {
+    pub id: i64,
+    pub wallet_id: i64,
+    pub item_kind: DbLabelledKind,
+    pub item: String,
+    pub value: String,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+#[repr(i64)]
+pub enum DbLabelledKind {
+    Address = 0,
+    OutPoint = 1,
+    Txid = 2,
+}
+
+impl From<i64> for DbLabelledKind {
+    fn from(value: i64) -> Self {
+        if value == 0 {
+            Self::Address
+        } else if value == 1 {
+            Self::OutPoint
+        } else {
+            assert_eq!(value, 2);
+            Self::Txid
+        }
+    }
+}
+
+impl TryFrom<&rusqlite::Row<'_>> for DbLabel {
+    type Error = rusqlite::Error;
+
+    fn try_from(row: &rusqlite::Row) -> Result<Self, Self::Error> {
+        let id: i64 = row.get(0)?;
+        let wallet_id: i64 = row.get(1)?;
+        let item_kind: i64 = row.get(2)?;
+        let item: String = row.get(3)?;
+        let value: String = row.get(4)?;
+
+        Ok(DbLabel {
+            id,
+            wallet_id,
+            item_kind: item_kind.into(),
+            item,
+            value,
+        })
+    }
+}
diff --git a/src/database/sqlite/utils.rs b/src/database/sqlite/utils.rs
index 407f05f5..efe92644 100644
--- a/src/database/sqlite/utils.rs
+++ b/src/database/sqlite/utils.rs
@@ -180,6 +180,20 @@ fn migrate_v1_to_v2(conn: &mut rusqlite::Connection) -> Result<(), SqliteDbError
     Ok(())
 }
 
+// After Liana 1.1 we upgraded the schema to add the labels table.
+fn migrate_v2_to_v3(conn: &mut rusqlite::Connection) -> Result<(), SqliteDbError> {
+    db_exec(conn, |tx| {
+        tx.execute(
+            "CREATE TABLE labels (id INTEGER PRIMARY KEY NOT NULL, wallet_id INTEGER NOT NULL, item_kind INTEGER NOT NULL CHECK (item_kind IN (0,1,2)), item TEXT UNIQUE NOT NULL, value TEXT NOT NULL)",
+            rusqlite::params![],
+        )?;
+        tx.execute("UPDATE version SET version = 3", rusqlite::params![])?;
+        Ok(())
+    })?;
+
+    Ok(())
+}
+
 /// Check the database version and if necessary apply the migrations to upgrade it to the current
 /// one.
 pub fn maybe_apply_migration(db_path: &path::Path) -> Result<(), SqliteDbError> {
@@ -203,6 +217,11 @@ pub fn maybe_apply_migration(db_path: &path::Path) -> Result<(), SqliteDbError>
                 migrate_v1_to_v2(&mut conn)?;
                 log::warn!("Migration from database version 1 to version 2 successful.");
             }
+            2 => {
+                log::warn!("Upgrading database from version 2 to version 3.");
+                migrate_v2_to_v3(&mut conn)?;
+                log::warn!("Migration from database version 2 to version 3 successful.");
+            }
             _ => return Err(SqliteDbError::UnsupportedVersion(version)),
         }
     }
diff --git a/src/jsonrpc/api.rs b/src/jsonrpc/api.rs
index 7d54c85f..fc920bbf 100644
--- a/src/jsonrpc/api.rs
+++ b/src/jsonrpc/api.rs
@@ -1,9 +1,14 @@
 use crate::{
+    commands::LabelItem,
     jsonrpc::{Error, Params, Request, Response},
     DaemonControl,
 };
 
-use std::{collections::HashMap, convert::TryInto, str::FromStr};
+use std::{
+    collections::{HashMap, HashSet},
+    convert::TryInto,
+    str::FromStr,
+};
 
 use miniscript::bitcoin::{self, psbt::PartiallySignedTransaction as Psbt};
 
@@ -160,6 +165,68 @@ fn create_recovery(control: &DaemonControl, params: Params) -> Result<serde_json
     Ok(serde_json::json!(&res))
 }
 
+fn update_labels(control: &DaemonControl, params: Params) -> Result<serde_json::Value, Error> {
+    let mut items = HashMap::new();
+    for (item, value) in params
+        .get(0, "labels")
+        .ok_or_else(|| Error::invalid_params("Missing 'labels' parameter."))?
+        .as_object()
+        .ok_or_else(|| Error::invalid_params("Invalid 'labels' parameter."))?
+        .iter()
+    {
+        let value = value
+            .as_str()
+            .map(|s| s.to_string())
+            .ok_or_else(|| Error::invalid_params(format!("Invalid 'labels.{}' value.", item)))?;
+        if value.len() > 100 {
+            return Err(Error::invalid_params(format!(
+                "Invalid 'labels.{}' value length: must be less or equal than 100 characters",
+                item
+            )));
+        }
+        let item =
+            LabelItem::from_str(item, control.config.bitcoin_config.network).ok_or_else(|| {
+                Error::invalid_params(format!(
+                    "Invalid 'labels.{}' parameter: must be an address, a txid or an outpoint",
+                    item
+                ))
+            })?;
+        items.insert(item, value);
+    }
+
+    control.update_labels(&items);
+    Ok(serde_json::json!({}))
+}
+
+fn get_labels(control: &DaemonControl, params: Params) -> Result<serde_json::Value, Error> {
+    let mut items = HashSet::new();
+    for item in params
+        .get(0, "items")
+        .ok_or_else(|| Error::invalid_params("Missing 'items' parameter."))?
+        .as_array()
+        .ok_or_else(|| Error::invalid_params("Invalid 'items' parameter."))?
+        .iter()
+    {
+        let item = item.as_str().ok_or_else(|| {
+            Error::invalid_params(format!(
+                "Invalid item {} format: must be an address, a txid or an outpoint",
+                item
+            ))
+        })?;
+
+        let item =
+            LabelItem::from_str(item, control.config.bitcoin_config.network).ok_or_else(|| {
+                Error::invalid_params(format!(
+                    "Invalid item {} format: must be an address, a txid or an outpoint",
+                    item
+                ))
+            })?;
+        items.insert(item);
+    }
+
+    Ok(serde_json::json!(control.get_labels(&items)))
+}
+
 /// Handle an incoming JSONRPC2 request.
 pub fn handle_request(control: &DaemonControl, req: Request) -> Result<Response, Error> {
     let result = match req.method.as_str() {
@@ -222,6 +289,18 @@ pub fn handle_request(control: &DaemonControl, req: Request) -> Result<Response,
                 .ok_or_else(|| Error::invalid_params("Missing 'psbt' parameter."))?;
             update_spend(control, params)?
         }
+        "updatelabels" => {
+            let params = req
+                .params
+                .ok_or_else(|| Error::invalid_params("Missing 'labels' parameter."))?;
+            update_labels(control, params)?
+        }
+        "getlabels" => {
+            let params = req
+                .params
+                .ok_or_else(|| Error::invalid_params("Missing 'items' parameter."))?;
+            get_labels(control, params)?
+        }
         _ => {
             return Err(Error::method_not_found());
         }


Test patch:
diff --git a/src/testutils.rs b/src/testutils.rs
index 4f739183..6bb854c4 100644
--- a/src/testutils.rs
+++ b/src/testutils.rs
@@ -1,11 +1,16 @@
 use crate::{
     bitcoin::{BitcoinInterface, Block, BlockChainTip, UTxO},
     config::{BitcoinConfig, Config},
-    database::{BlockInfo, Coin, CoinType, DatabaseConnection, DatabaseInterface},
+    database::{BlockInfo, Coin, CoinType, DatabaseConnection, DatabaseInterface, LabelItem},
     descriptors, DaemonHandle,
 };
 
-use std::{collections::HashMap, env, fs, io, path, process, str::FromStr, sync, thread, time};
+use std::{
+    collections::{HashMap, HashSet},
+    env, fs, io, path, process,
+    str::FromStr,
+    sync, thread, time,
+};
 
 use miniscript::{
     bitcoin::{
@@ -334,6 +339,14 @@ impl DatabaseConnection for DummyDatabase {
         todo!()
     }
 
+    fn update_labels(&mut self, _items: &HashMap<LabelItem, String>) {
+        todo!()
+    }
+
+    fn labels(&mut self, _items: &HashSet<LabelItem>) -> HashMap<String, String> {
+        todo!()
+    }
+
     fn list_txids(&mut self, start: u32, end: u32, limit: u64) -> Vec<bitcoin::Txid> {
         let mut txids_and_time = Vec::new();
         let coins = &self.db.read().unwrap().coins;
diff --git a/tests/test_rpc.py b/tests/test_rpc.py
index 119b04c9..7a599945 100644
--- a/tests/test_rpc.py
+++ b/tests/test_rpc.py
@@ -596,3 +596,116 @@ def test_create_recovery(lianad, bitcoind):
     assert len(reco_psbt.tx.vout) == 1
     assert int(0.39999 * COIN) < int(reco_psbt.tx.vout[0].nValue) < int(0.4 * COIN)
     sign_and_broadcast(lianad, bitcoind, reco_psbt, recovery=True)
+
+
+def test_labels(lianad, bitcoind):
+    """Test the creation and updating of labels."""
+    # We can set a label for an address.
+    addr = lianad.rpc.getnewaddress()["address"]
+    lianad.rpc.updatelabels({addr: "first-addr"})
+    assert lianad.rpc.getlabels([addr])["labels"] == {addr: "first-addr"}
+    # And also update it.
+    lianad.rpc.updatelabels({addr: "first-addr-1"})
+    assert lianad.rpc.getlabels([addr])["labels"] == {addr: "first-addr-1"}
+    # But we can't set a label larger than 100 characters
+    with pytest.raises(RpcError, match=".*must be less or equal than 100 characters"):
+        lianad.rpc.updatelabels({addr: "".join("a" for _ in range(101))})
+
+    # We can set a label for a coin.
+    sec_addr = lianad.rpc.getnewaddress()["address"]
+    txid = bitcoind.rpc.sendtoaddress(sec_addr, 1)
+    wait_for(lambda: len(lianad.rpc.listcoins()["coins"]) == 1)
+    coin = lianad.rpc.listcoins()["coins"][0]
+    lianad.rpc.updatelabels({coin["outpoint"]: "first-coin"})
+    assert lianad.rpc.getlabels([coin["outpoint"]])["labels"] == {
+        coin["outpoint"]: "first-coin"
+    }
+    # And also update it.
+    lianad.rpc.updatelabels({coin["outpoint"]: "first-coin-1"})
+    assert lianad.rpc.getlabels([coin["outpoint"]])["labels"] == {
+        coin["outpoint"]: "first-coin-1"
+    }
+    # Its address though has no label.
+    assert lianad.rpc.getlabels([sec_addr])["labels"] == {}
+    # But we can receive a coin to the address that has a label set, and query both.
+    sec_txid = bitcoind.rpc.sendtoaddress(addr, 1)
+    wait_for(lambda: len(lianad.rpc.listcoins()["coins"]) == 2)
+    sec_coin = next(
+        c for c in lianad.rpc.listcoins()["coins"] if sec_txid in c["outpoint"]
+    )
+    lianad.rpc.updatelabels({sec_coin["outpoint"]: "sec-coin"})
+    res = lianad.rpc.getlabels([sec_coin["outpoint"], addr])["labels"]
+    assert len(res) == 2
+    assert res[sec_coin["outpoint"]] == "sec-coin"
+    assert res[addr] == "first-addr-1"
+    # We can also query the labels for both coins, of course.
+    res = lianad.rpc.getlabels([coin["outpoint"], sec_coin["outpoint"]])["labels"]
+    assert len(res) == 2
+    assert res[coin["outpoint"]] == "first-coin-1"
+    assert res[sec_coin["outpoint"]] == "sec-coin"
+
+    # We can set, update and query labels for deposit transactions.
+    lianad.rpc.updatelabels({txid: "first-deposit"})
+    assert lianad.rpc.getlabels([txid, sec_txid])["labels"] == {txid: "first-deposit"}
+    lianad.rpc.updatelabels({txid: "first-deposit-1", sec_txid: "second-deposit"})
+    res = lianad.rpc.getlabels([txid, sec_txid])["labels"]
+    assert len(res) == 2
+    assert res[txid] == "first-deposit-1"
+    assert res[sec_txid] == "second-deposit"
+
+    # We can set and update a label for a spend transaction.
+    spend_txid = get_txid(spend_coins(lianad, bitcoind, [coin, sec_coin]))
+    lianad.rpc.updatelabels({spend_txid: "spend-tx"})
+    assert lianad.rpc.getlabels([spend_txid])["labels"] == {spend_txid: "spend-tx"}
+    lianad.rpc.updatelabels({spend_txid: "spend-tx-1"})
+    assert lianad.rpc.getlabels([spend_txid])["labels"] == {spend_txid: "spend-tx-1"}
+
+    # We can set labels for inexistent stuff, as long as the format of the item being
+    # labelled is valid.
+    inexistent_txid = "".join("0" for _ in range(64))
+    inexistent_outpoint = "".join("1" for _ in range(64)) + ":42"
+    random_address = bitcoind.rpc.getnewaddress()
+    lianad.rpc.updatelabels(
+        {
+            inexistent_txid: "inex_txid",
+            inexistent_outpoint: "inex_outpoint",
+            random_address: "bitcoind-addr",
+        }
+    )
+    res = lianad.rpc.getlabels([inexistent_txid, inexistent_outpoint, random_address])[
+        "labels"
+    ]
+    assert len(res) == 3
+    assert res[inexistent_txid] == "inex_txid"
+    assert res[inexistent_outpoint] == "inex_outpoint"
+    assert res[random_address] == "bitcoind-addr"
+
+    # We'll confirm everything, shouldn't affect any of the labels.
+    bitcoind.generate_block(1, wait_for_mempool=spend_txid)
+    wait_for(
+        lambda: bitcoind.rpc.getblockcount() == lianad.rpc.getinfo()["block_height"]
+    )
+    res = lianad.rpc.getlabels(
+        [
+            addr,
+            sec_addr,  # No label for this one.
+            txid,
+            sec_txid,
+            coin["outpoint"],
+            sec_coin["outpoint"],
+            spend_txid,
+            inexistent_txid,
+            inexistent_outpoint,
+            random_address,
+        ]
+    )["labels"]
+    assert len(res) == 9
+    assert res[sec_coin["outpoint"]] == "sec-coin"
+    assert res[addr] == "first-addr-1"
+    assert res[coin["outpoint"]] == "first-coin-1"
+    assert res[txid] == "first-deposit-1"
+    assert res[sec_txid] == "second-deposit"
+    assert res[spend_txid] == "spend-tx-1"
+    assert res[inexistent_txid] == "inex_txid"
+    assert res[inexistent_outpoint] == "inex_outpoint"
+    assert res[random_address] == "bitcoind-addr"