-
Notifications
You must be signed in to change notification settings - Fork 58
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
1 changed file
with
356 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,356 @@ | ||
<pre> | ||
DIP: aj-stamped-denominations | ||
Title: Securing Denominated Send by using Dust as Transaction Stamps | ||
Authors: coolaj86 | ||
Special-Thanks: Rion Gull | ||
Comments-Summary: No comments yet. | ||
Status: Draft | ||
Type: Standard | ||
Created: 2023-06-28 | ||
License: CC0-1.0 | ||
</pre> | ||
|
||
## Table of Contents | ||
|
||
- [Table of Contents](#table-of-contents) | ||
- [Abstract](#abstract) | ||
- [Prior Art](#prior-art) | ||
- [Motivation](#motivation) | ||
- [Specification](#specification) | ||
- [Turning Dust into Transaction Fee Stamps](#turning-dust-into-transaction-fee-stamps) | ||
- [Copyright](#copyright) | ||
|
||
## Abstract | ||
|
||
This DIP proposes to use the dust created by denominating coins as stamps for | ||
transaction fees, rather than being immediately donated to the network. | ||
|
||
This applies to TWO categories of software: | ||
|
||
- Software which ONLY denominates in special cases, like CoinJoin \ | ||
(for example, Private Send DOES NOT denominate its outputs) | ||
- Software which ALWAYS denominates, except in special cases, like DashWallet \ | ||
(always denominates inputs and outputs, except when sending via legacy protocols) | ||
|
||
Current: | ||
|
||
Lots of DASH is simply wasted, and the fee "stamps" don't actually cover the fee | ||
of a single coin in many cases. XPub Sends are not possible. | ||
|
||
```text | ||
5.000 => | ||
4x 1.0000 1000 | ||
9x 0. 10000 100 | ||
9x 0.0 10000 10 | ||
9x 0.00 10000 1 | ||
Spendable Dash: 4.999 | ||
Transactions per Denom: < 1 | ||
Dash Wasted: (0.00049900 + 0.00050010) - 1077 (0.019%) | ||
``` | ||
|
||
Proposed: | ||
|
||
Almost no DASH is wasted, and each coin has several fee "stamps" that are | ||
guaranteed to cover the cost of many transactions. This means that XPub Sends do | ||
not undo any mixing that may have taken place. | ||
|
||
```text | ||
5.000 => | ||
4x 1.000 03200 | ||
9x 0.100 03200 | ||
9x 0.010 03200 | ||
5x 0.001 03200 | ||
4x 0.001 03000 | ||
Spendable Dash: 4.999 | ||
Transactions per Denom: 15+ | ||
Dash Wasted: 0.00000160 (0.000%) | ||
``` | ||
|
||
This is **NOT** specific to CoinJoin / "mixing" / PrivateSend, but **ALL** | ||
current and future forms of denominated DASH. | ||
|
||
keywords: Stamp Dust Transaction Tx Fee Denominated coins CoinJoin PrivateSend | ||
mixing coin mix | ||
|
||
## Prior Art | ||
|
||
- [Dash Glossary: Denominations](https://docs.dash.org/projects/core/en/stable/docs/resources/glossary.html#denominations) | ||
|
||
[lextx]: https://github.com/bitcoin/bips/blob/master/bip-0069.mediawiki | ||
|
||
**DashCore Code References** | ||
|
||
These are some areas that would need to be refactored as a semver-breaking | ||
change to apply this DIP: | ||
|
||
- [IsValidDenomination](https://github.com/dashpay/dash/blob/549e347b742cb4dc63807a292729e658218d7d0f/src/coinjoin/coinjoin.h#L409C10-L409C10) | ||
- [DenominationToAmount](https://github.com/dashpay/dash/blob/549e347b742cb4dc63807a292729e658218d7d0f/src/coinjoin/coinjoin.h#L431) | ||
- [vecStandardDenominations](https://github.com/dashpay/dash/blob/549e347b742cb4dc63807a292729e658218d7d0f/src/coinjoin/coinjoin.h#L391) | ||
|
||
## Motivation | ||
|
||
Currently the only type of denominated coins that DashCore supports have several | ||
drawbacks (and vulnerabilities in leaking user information) related to how they | ||
are are split and "stamped" for fees: | ||
|
||
- much of the denomination value is **burned as dust**: \ | ||
`5.0000 0000` burns about `0.00093761` at time of denomination \ | ||
(which could have been used for over 468 single-coin transactions) | ||
- small single coins have a stamp value **too low to pay fees** (min fee is | ||
`193`): | ||
```text | ||
0. 10000 100 | ||
0.0 10000 10 | ||
0.00 10000 1 | ||
``` | ||
- large coins waste DASH that could have been used to pay fees elsewhere: | ||
```text | ||
10.000 1000 | ||
``` | ||
- they must always be de-denominated and de-mixed to be spent \ | ||
(XPub Send is not feasible) | ||
|
||
```text | ||
To Pay 0.221, I'd need 6 coins: | ||
(one of which is entirely wasted) | ||
0. 10000 100 \ | ||
0. 10000 100 -\ | ||
0.0 10000 10 --\ | ||
0.0 10000 10 ---- 0.2220 0222 -> 0.2210 0000 + 938 (fee) | ||
0.00 10000 1 --/ | ||
0.00 10000 1 -/ | ||
``` | ||
|
||
Similar issues are also discussed in | ||
[DashCore: CoinJoin Fees](https://docs.dash.org/projects/core/en/stable/docs/guide/dash-features-coinjoin.html#fees). | ||
|
||
## Specification | ||
|
||
This makes assumptions based on present conditions of DASH: | ||
|
||
- Symbolic "fees" are required for each transaction | ||
- The minimum fee value is 193. | ||
|
||
Given those assumptions, the specification is as follows: | ||
|
||
- All denominations (CoinJoin, XPub, or otherwise) MUST redistribute "stamp" | ||
values". | ||
- These stamp values SHOULD be denominated themselves. | ||
- A single stamp value MUST be large enough to pay for a single transaction. \ | ||
(at least the minimum guaranteed fee - currently 193 sats, \ | ||
preferably a multiple of the lowest denomination, such as 200) | ||
- Remainder values for stamps SHOULD be redistributed with a minimum stamp | ||
value. \ | ||
(minimum value MAY change based on network conditions) | ||
- Stamp values MUST NOT be distributed in a way that leaks user data. \ | ||
(such as apply one user's stamp values only to that user's coins) | ||
|
||
**How to Declare Acceptable Denominations** | ||
|
||
A denomination has two distinct components: | ||
|
||
- The user-visible value | ||
- The embedded stamp value (for paying fees) | ||
|
||
A denomination MUST include a minimum number of "stamps" for paying fees. | ||
|
||
Pseudocode: | ||
|
||
```js | ||
// 1. Declare the stamp value | ||
const STAMP_DENOM = 200; | ||
const MIN_STAMPS_VALUE = 3 * STAMP_DENOM; | ||
``` | ||
|
||
```js | ||
// 2. Declare and store the denominations | ||
// (in descending order for efficiency and ease-of-use) | ||
const LOWEST_DENOM = 10_000; | ||
let DENOMS = [ | ||
// 10.0 | ||
10_0000_0000, | ||
// 1.0 | ||
1_0000_0000, | ||
// 0.1 | ||
1000_0000, | ||
// 0.01 | ||
100_0000, | ||
// 0.001 | ||
LOWEST_DENOM, | ||
]; | ||
``` | ||
|
||
**How to Determine a Coin is Denominated** | ||
|
||
To test whether or not a coin is denominated: | ||
|
||
- subtract the lowest denomination | ||
- check if the result is greater than the lowest denomination | ||
- if not, check if the result is a valid stamp value | ||
- if not, reject the coin as not denominated | ||
|
||
Pseudocode: | ||
|
||
```js | ||
// 3. Check if the coin is denominated | ||
function isDenominated(sats) { | ||
if (sats < LOWEST_DENOM) { | ||
return false; | ||
} | ||
|
||
sats -= LOWEST_DENOM; | ||
// note: 2x the lowest denomination *may* be considered denominated | ||
if (sats > LOWEST_DENOM) { | ||
return false; | ||
} | ||
|
||
let remainder = sats % STAMP_DENOM; | ||
if (remainder !== 0) { | ||
return false; | ||
} | ||
|
||
let hasMinimumStamps = remainder >= MIN_STAMPS_VALUE; | ||
if (!hasMinimumStamps) { | ||
return false; | ||
} | ||
|
||
return true; | ||
} | ||
``` | ||
|
||
**How to Distribute Stamp (Fee) Values to Outputs** | ||
|
||
For a coin to be considered denominated, it must have a minimum stamp value. | ||
|
||
TODO | ||
|
||
The remaining stamp value may not always be even, but MUST be **distributed | ||
deterministicly**. | ||
|
||
Remaining stamps MUST be distributed to remaining outputs, ordered by | ||
[Lexicographical Indexing of Transaction Inputs and Outputs][lextx], in | ||
round-robin fashion. | ||
|
||
```js | ||
// Distributes extra stamps to sorted outputs in round-robin fashion | ||
function distributeExtraStamps(outputs, extraStamps) { | ||
outputs.sort(byLexOutIndex); | ||
|
||
for (;;) { | ||
outputs.satoshis; | ||
} | ||
} | ||
``` | ||
|
||
```js | ||
// Sorts outputs by script order | ||
function byLexOutIndex(a, b) { | ||
if (a.amount < b.amount) { | ||
return 1; | ||
} | ||
if (a.amount > b.amount) { | ||
return -1; | ||
} | ||
|
||
let aScript; | ||
let bScript; | ||
|
||
if (a.script && b.script) { | ||
aScript = a.script; | ||
bScript = b.script; | ||
} else if (a.pubKeyHash && b.pubKeyHash) { | ||
aScript = a.pubKeyHash; | ||
bScript = b.pubKeyHash; | ||
} else if (a.address && b.address) { | ||
aScript = a.address; | ||
bScript = b.address; | ||
} else { | ||
console.error(a); | ||
console.error(b); | ||
throw new Error("cannot compare incompatible lockscript types"); | ||
} | ||
|
||
if (aScript < bScript) { | ||
return 1; | ||
} | ||
if (aScript > bScript) { | ||
return -1; | ||
} | ||
|
||
return 0; | ||
} | ||
``` | ||
|
||
**Guidance on How to Account for Received Transactions** | ||
|
||
When sending to an XPub the user agent SHOULD send denominated coins to | ||
individual XPub addresses in a 1:1 fashion (less fees, and accounting for stamp | ||
redistribution): | ||
|
||
```text | ||
10000 5000 => 1 0000 4000 | ||
5000 5200 => 5000 4200 | ||
100 2800 => 100 4200 | ||
(fee is 559, which rounds to 600, | ||
or 3 stamps consuming 41 dust) | ||
``` | ||
|
||
In this case, the "transaction amount" displayed to the user SHOULD BE be the | ||
SINGLE TRANSACTION's whole amount, NOT the individual UTXOs. | ||
|
||
The stamp/fee values (values below the lowest denomination) SHOULD NOT be shown | ||
in the calculation to the user. | ||
|
||
Consider this example where the `LOWEST_DENOM` is (for the purpose of | ||
illustration) set to `10000` (`0.0001`) and each coin will be stamped with 20 to | ||
21 transaction fees of `200`: | ||
|
||
1. The receiver requests `1.5100` | ||
2. The outputs received as as follows: | ||
```text | ||
outputs: | ||
1.00004000 | ||
0.50004200 | ||
0.01004200 | ||
``` | ||
3. The following calculations are made | ||
```text | ||
Sat value: 1 51012400 | ||
Shown to User: 1.5100 (NOT 1.5101, NOT 3 transactions) | ||
``` | ||
|
||
**Guidance for Sending to Legacy (non-XPub) Addresses** | ||
|
||
There are two cases which break denominated sending: | ||
|
||
1. Sending an exact, 8-decimal place (single sat denominated) amount to a legacy | ||
protocol | ||
2. Sending a denominated amount to a single, legacy address | ||
|
||
In these cases the sum total of the stamp values may add up to one or more whole | ||
denominated, stamped coins. | ||
|
||
Since the constraints which prevent leaked data are already broken in these | ||
cases anyway, the user agent MAY prompt the user as to whether or not they would | ||
like denominated return change. | ||
|
||
The user agent SHOULD NOT return non-denominated change. | ||
|
||
## Copyright | ||
|
||
Copyright 2023 AJ ONeal. | ||
|
||
DIP to Secure Transactions - Patch Data Leak Vulnerability via Deterministic | ||
Index Ordering | ||
|
||
Written in 2023 by AJ ONeal <[email protected]> \ | ||
To the extent possible under law, the author(s) have dedicated all copyright \ | ||
and related and neighboring rights to this software to the public domain \ | ||
worldwide. This software is distributed without any warranty. \ | ||
|
||
You should have received a copy of the CC0 Public Domain Dedication along with \ | ||
this software. If not, see <https://creativecommons.org/publicdomain/zero/1.0/>. |