XLS-5 - XRP Ledger Proposed Standard #5 Title: Tagged Addresses Author: Nikolaos D. Bougalis Affiliation: Ripple Created: 2019-05-15 Original: https://github.com/xrp-community/standards-drafts/issues/6
Destination tags provide a way for exchanges, payment processors, corporates or entities which accept incoming payments, escrows, checks and similar transcations to use a single receiving wallet while being able to disambiguate incoming transactions by instructing the senders to include a destination tag.
This draft introduces the concept of a tagged address: an address that contains both the target wallet as well as a destination tag as a single unit.
Although flexible, destination tags suffer from several drawbacks.
Communicating a destination tag to users can be a problem for a number of reasons:
- In the absence of a standard format to represent an (address, tag) pair, different users of destination tags communicate the information in different ways.
- Users needs to enter two items, usually in two distinct input fields which may be inconsistently or confusingly named (e.g. one implementation refers to the destination tag as a "PIN").
- Programmers need to decide on how, when or even if the "Destination Tag" field must be surfaced, complicating panel layouts and increasing the potential for user confusion.
- The tag is an opaque identifier, generated by and meaningful only to the entity that will be receiving funds. Tags do not natively include any kind of checksum or error checking to prevent against common issues such as a user accidentially omitting or transposing digits or otherwiseincorrectly enter a tag.
This proposal seeks to address this problem by defining a standard format to represent an (address, tag) which:
- Is expressed as a single string that shares all the desirable properties of existing addresses, including the ability to be selected by double-clicking on them.
- Eliminates the need for a separate "Destination Tag" field by allowing a single format to represent addresses both with and without tags.
- Includes a built-in checksum as a form of error-checking to reduce the probability that a typographical error will generate a correct address.
We are not looking to change the on-ledger format; that is, the new style addresses can't be used for fields where an AccountID
is expected in the binary format. Instead, the packed address will be detected and decoded at higher levels (for example, by the client sofware, ripple-lib
or the RPC
and WebSocket
APIs in rippled
), verified and then split into distinct fields (e.g. sfDestination
and sfDestinationTag
) as appropriate, to assemble the underlying transaction.
“The nature of Bitcoin is such that once version 0.1 was released, the core design was set in stone for the rest of its lifetime.”
— Satoshi Nakamoto
While we can propose a standard, we need to contend with the fact that, by design, the basics of the protocol (and that includes account addresses) were fixed the moment the ledger was instantiated. While not completely inflexible, account addresses are deeply embedded into the protocol and even if they aren't part of the "core design" changing them would involve a huge amount of pain.
Not all is lost, however and there are things we can do. In this case, we can define a new address style which incorporates the destination tag and software can be understand such packed addresses, improving the UX for users, while intelligently unpacking such addresses into their constituent parts for the underlying system.
Although we have options in developing a new format, including what encoding to use, ideally the resulting addresses will be similar to existing addresses to reduce the likelihood of user confusion as much as possible and, ideally, not requiring developers to implement a new codec.
Given this constrain, we need to use the Base58Check encoding, leaving us with two options:
- A "tightly packed" format, where the address and tag are encoded using Base58Check as a single unit.
- A "loosely packed" format, where the address is encoded using Base58Check and the destination tag is encoded separately and appended to the address.
The advantage of the "loose" format is that a tagged address will precisely match the classic address, up to the tag. The "tight" format results in an address that shares no common prefix with the classic address, except, perhaps, by chance.
The disadvantage of the "loose" format is that a tagged address is more complex to detect, encode and decode.
On balance, we feel that although the "loose" could allow for classic and tagged addresses to co-exist, we believe that the "tight" format is a better choice overall.
This is only a proposal. It is my hope that it will generate discussion between developers, community members and other interested parties, and that we will reach consensus on the way forward.
Comments, criticisms, suggestions and improvements are welcome!
For a better introduction to addresses and tags, it may help to reference https://developers.ripple.com/accounts.html.
Currently, an address is generated by base58-encoding (with a checksum) a 21 byte buffer:
[← 1 byte prefix →|← 160 bits of account ID →]
The chosen byte prefix is TokenType::AccountID
(value 0)
In the proposed format, the address and destination tag are combined into a single 30 byte buffer which is then encoded using the familiar Base58Check algorithm as a single unit, using a new two-byte prefix:
[← 2 byte prefix →|← 160 bits of account ID →|← 8 bits of flags →|← 64 bits of tag →]
The tag shall always be encoded as a little endian two's complement 64 bit integer.
The standard proposes using different prefixes, making addresses encoded for use on the mainnet have a different initial character than addresses encoded for use on the testnet, making it possible for users and tools to differentiate addresses.
Network | Prefix | Initial Character |
---|---|---|
mainnet | 0x05 0x44 |
X |
testnet | 0x04 0x93 |
T |
It is important to note: an address encoded for mainnet use could still be used on the testnet and vice versa; tools that understand the new format are encouraged to implement protections.
Adding a flags field allow us to make this format slightly more flexible than it would otherwise be. At this time, only 3 flags are specified, one of which is reserved and may not be used in practice.
Name | Value | Description |
---|---|---|
NO_TAG |
0x00 |
The address is untagged and treated as if no tag had been specified. The remaining 8 bytes MUST be zero. |
TAG_32 |
0x01 |
The address contains a 32-bit tag. The tag is encoded in little-endian form in the next 4 bytes. The remaining 4 bytes MUST be zero. |
TAG_64 |
0x02 |
The address contains a 64-bit tag. The tag is encoded in little-endian form in the next 8 bytes. This flag is reserved in case 64-bit tags ever become supported. |
Exactly one of NO_TAG
, TAG_32
and TAG_64
MUST be set; implementations should mask of any other fields when checking which of these flags is set.
All flag values not explicitly defined are reserved for future extensions and MUST be set to 0. Implementations that encounter flag values that are unknown should return an error and treat the address as invalid.
Below we present how the classic address rGWrZyQqhTp9Xu7G5Pkayo7bXjH4k4QYpf
would be encoded under this spec to include various destination tags. The tag, if any, is on the left most column. The raw bytes, prior to encoding are shown in the second column. Lastly, we show the resulting base58 encoded address.
Tag | Address | Raw Bytes |
---|---|---|
XVLhHMPHU98es4dbozjVtdWzVrDjtV5fdx1mHp98tDMoQXb | 44AA066C988C712815CC37AF71472B7CBBBD4E2A0A000000000000000000 |
|
0 | XVLhHMPHU98es4dbozjVtdWzVrDjtV8AqEL4xcZj5whKbmc | 44AA066C988C712815CC37AF71472B7CBBBD4E2A0A010000000000000000 |
1 | XVLhHMPHU98es4dbozjVtdWzVrDjtV8xvjGQTYPiAx6gwDC | 44AA066C988C712815CC37AF71472B7CBBBD4E2A0A010100000000000000 |
2 | XVLhHMPHU98es4dbozjVtdWzVrDjtV8zpDURx7DzBCkrQE7 | 44AA066C988C712815CC37AF71472B7CBBBD4E2A0A010200000000000000 |
32 | XVLhHMPHU98es4dbozjVtdWzVrDjtVoYiC9UvKfjKar4LJe | 44AA066C988C712815CC37AF71472B7CBBBD4E2A0A012000000000000000 |
276 | XVLhHMPHU98es4dbozjVtdWzVrDjtVoKj3MnFGMXEFMnvJV | 44AA066C988C712815CC37AF71472B7CBBBD4E2A0A011401000000000000 |
65591 | XVLhHMPHU98es4dbozjVtdWzVrDjtVozpjdhPQVdt3ghaWw | 44AA066C988C712815CC37AF71472B7CBBBD4E2A0A013700010000000000 |
16781933 | XVLhHMPHU98es4dbozjVtdWzVrDjtVqrDUk2vDpkTjPsY73 | 44AA066C988C712815CC37AF71472B7CBBBD4E2A0A016D12000100000000 |
4294967294 | XVLhHMPHU98es4dbozjVtdWzVrDjtV1kAsixQTdMjbWi39u | 44AA066C988C712815CC37AF71472B7CBBBD4E2A0A01FEFFFFFF00000000 |
4294967295 | XVLhHMPHU98es4dbozjVtdWzVrDjtV18pX8yuPT7y4xaEHi | 44AA066C988C712815CC37AF71472B7CBBBD4E2A0A01FFFFFFFF00000000 |
Tag | Address | Raw Bytes |
---|---|---|
none | TVE26TYGhfLC7tQDno7G8dGtxSkYQn49b3qD26PK7FcGSKE | 93AA066C988C712815CC37AF71472B7CBBBD4E2A0A000000000000000000 |
0 | TVE26TYGhfLC7tQDno7G8dGtxSkYQnSy8RHqGHoGJ59spi2 | 93AA066C988C712815CC37AF71472B7CBBBD4E2A0A010000000000000000 |
1 | TVE26TYGhfLC7tQDno7G8dGtxSkYQnSz1uDimDdPYXzSpyw | 93AA066C988C712815CC37AF71472B7CBBBD4E2A0A010100000000000000 |
2 | TVE26TYGhfLC7tQDno7G8dGtxSkYQnTryP9tG9TW8GeMBmd | 93AA066C988C712815CC37AF71472B7CBBBD4E2A0A010200000000000000 |
32 | TVE26TYGhfLC7tQDno7G8dGtxSkYQnT2oqaCDzMEuCDAj1j | 93AA066C988C712815CC37AF71472B7CBBBD4E2A0A012000000000000000 |
276 | TVE26TYGhfLC7tQDno7G8dGtxSkYQnTMgJJYfAbsiPsc6Zg | 93AA066C988C712815CC37AF71472B7CBBBD4E2A0A011401000000000000 |
65591 | TVE26TYGhfLC7tQDno7G8dGtxSkYQn7ryu2W6njw7mT1jmS | 93AA066C988C712815CC37AF71472B7CBBBD4E2A0A013700010000000000 |
16781933 | TVE26TYGhfLC7tQDno7G8dGtxSkYQnVsw45sDtGHhLi27Qa | 93AA066C988C712815CC37AF71472B7CBBBD4E2A0A016D12000100000000 |
4294967294 | TVE26TYGhfLC7tQDno7G8dGtxSkYQnX8tDFQ53itLNqs6vU | 93AA066C988C712815CC37AF71472B7CBBBD4E2A0A01FEFFFFFF00000000 |
4294967295 | TVE26TYGhfLC7tQDno7G8dGtxSkYQnXoy6kSDh6rZzApc69 | 93AA066C988C712815CC37AF71472B7CBBBD4E2A0A01FFFFFFFF00000000 |
- Both the account ID and the destination tag are protected by the 32-bit checksum performed by the Base58Check format.
- We can determine whether the address is packed by casual visual examination of the address, as well by examining the first byte of the decoded data.
- Any given pair of tagged addresses referencing the same underlying address but with different destination tags will share a long common prefix when encoded.
- The address is visually distinct from any addresses already in use.
The primary disadvantage is the untagged address may appear in several places, including things such as trust lines, responses from the rippled
API endpoints and more and that can be confusing for users.
Consider the following transaction submission:
rippled submit secret '{
"TransactionType":"Payment",
"Account":"r3kmLJN5D28dHuH8vZNUZpMC43pEHpaocV",
"Amount":"200000000",
"Destination":"XVLhHMPHU98es4dbozjVtdWzVrDjtV8zpDURx7DzBCkrQE7"
}'
The server would unpack XVLhHMPHU98es4dbozjVtdWzVrDjtV8zpDURx7DzBCkrQE7
to rGWrZyQqhTp9Xu7G5Pkayo7bXjH4k4QYpf
and a destination tag of 2, and process the submission as if it had been:
rippled submit secret '{
"TransactionType":"Payment",
"Account":"r3kmLJN5D28dHuH8vZNUZpMC43pEHpaocV",
"Amount":"200000000",
"Destination":"rGWrZyQqhTp9Xu7G5Pkayo7bXjH4k4QYpf",
"DestinationTag":2
}'
Obviously the server cannot do this when presented with a pre-signed transaction, although assembling and signing a transaction using an unexpanded packed address shouldn't be possible, since the binary encoding of an AccountID
requires exactly 20 bytes.
Third party tools that accept user input should allow users to enter such addresses and transparently expand them prior to signing and/or submitting.
Below is a list of questions to be addressed as a result of this requirements document:
Probably not. It's better to simply state that the new format specifies an "address and tag" and to allow such addresses to be used both as a source and as a destination, and to decide the tag's type (source or destination) based on the field.
Not trivially. The Base58 format is quirky and doesn't behave the same way as "power-of-two" encodings, like Base32 or Base16. We could have retained a common prefix by encoding the address and destination tag separately and fusing the two components, but that significantly complicated the decoding process, which is something we sought to avoid.
Users may not understand the semantics of a packed address sufficiently. This means that errors or confusion are possible as a result. Two exchange users might wonder why their deposit addresses are now different. Or one user may ask a friend what the deposit address is and, unknowingly, deposit funds into their friend's account. The first is obviously not a problem with existing addresses, and the second is less likely to be an issue than these single addresses. The UX of third parties that choose to use the new format will have to make it clear to users that the address is unique for them.
The server could add information to the metadata associated with a transaction to help tools to map addresses. For example, the server could add the following:
"TaggedAddresses": [
{
"XVLhHMPHU98es4dbozjVtdWzVrDjtV8zpDURx7DzBCkrQE7": {
"Address": "rGWrZyQqhTp9Xu7G5Pkayo7bXjH4k4QYpf",
"Tag": 2
}
}
]
The BECH32 format is great and has several advantages over Base58, including speed. We are defining a new address encoding here, so why not just go for that? It's an option, but we decided to be conservative and not require new code to be written or incorporated into servers, clients and libraries.
The short answer is that they can't. The serialization field only allows exactly 20 bytes, so only the account itself can fit in. This format is only for base58 encoded addresses and is purely a convenience for users.
No. It would be a huge change and, potentially, breaking one and there aren't any advantages that I can see. There are many risks. Allowing APIs to understand the new format sufficiently to decompose it into its two constituent fields should be sufficient.