Skip to content

Conversation

chappjc
Copy link
Member

@chappjc chappjc commented Jul 2, 2021

Summary

Status: Working. Tests need fixing. Some frontend issues to address.

See TODOs.

Replaces PR #1017, tossing out 2,306 loc for a better solution.

This PR replaces the registration fee, which was paid to an address provided by the server operator, with a time-locked fidelity bond, which is redeemable by the user who posted the bond after a certain time. This creates a time cost to use DCRDEX instead of a monetary cost.

The DEX considers a user's bond as active when the bond script's lockTime (when it can be redeemed by the user) is at least bondExpiry in the future. That is, the bond remains locked for a period even after it becomes inactive for DEX purposes.

This also introduces an account tier system. Previously, paying the registration fee would mark the account paid, and it could be closed permanently if the user's violations exceed a certain score (binary and permanent). Now, posting a bond increases a user's tier, which offsets violations and in future work will also scale user order limits. When a bond expires, their tier is reduced. To trade, a user must have a tier > 0.

Bond Transaction Structure

There must be at least two outputs: the bond output (P2SH) and an account commitment (nulldata).

See:

Time-locked Bond (output 0)

Output 0 of the fidelity bond transaction must be a P2SH output paying to the bond script.

The redeem script looks similar to the refund path of an atomic swap script:

<locktime[4]> OP_CHECKLOCKTIMEVERIFY OP_DROP <pubkey[33]> OP_CHECKSIG

The above uses P2PK, and we can do P2PKH instead, but since part of validation requires the pubkey we probably don't gain anything from this:

<locktime[4]> OP_CHECKLOCKTIMEVERIFY OP_DROP OP_DUP OP_HASH160 <pubkeyhash[20]> OP_EQUALVERIFY OP_CHECKSIG

I've tested both formats, and I will probably switch to the non-hashed version because validation becomes even more asset-specific given hash160 is not a standard (e.g. ripemd160 of either sha256 for Bitcoin or blake256 for Decred).

Server validates that this P2SH output actually pays to the provided redeem script.

The pubkey referenced by the script need not be controlled by the user's wallet, but in this implementation it comes from their wallet. The client could even use their account pubkey in the script, but for security reasons it is best not to reuse pubkeys in this bond outputs, which are unspent on the blockchain for long periods.

The lockTime must be after a bond expiry time, the lockTimeThreshold. A bond becomes expired for a DEX account when duration until lockTime is less than a bondExpiry duration, which is on the order of a month. This, a user should create a bond with a lockTime at least bondExpiry in the future, but a practical lockTime would be more like 2x bondExpiry in the future so the account is active on the DEX for bondExpiry.

User must demonstrate ownership of the locked funds: sign something with private key corresponding to the pubkey in the TL redeemscript.

DEX Account commitment (output 1)

The account commitment OP_RETURN output should reference the account ID that corresponds to the account pubkey in the postbond request.

Having it in the raw like this allows the txn alone to identify the account, and without requiring the bond output pay to the account's private keys.

Protocol

New postbond route replaces register, but with no fee address and instead a txn with time-locked output (P2SH) and a DEX account commitment output (OP_RETURN).

postbond need not be used only for new accounts. Existing accounts may top-up / supplement their bond.

The config response should be consulted for bond requirements, including: expiry time, supported bond assets, amounts, and required confirmations.

The user may submit an initial postbond prior to broadcasting the bond transaction request containing the raw unsigned transaction for validation. The bond script to which the P2SH bond output pays must also be provided in the postbond request for validation by the DEX (correct script structure and lock time). The payload:

// PostBond should include the unsigned bond transaction for validation prior to
// broadcasting the signed transaction so the client hasn't needlessly locked
// funds if the transaction is rejected.
type PostBond struct {
	Signature
	AcctPubKey Bytes  `json:"acctPubKey"` // acctID = blake256(blake256(acctPubKey))
	AssetID    uint32 `json:"assetID"`
	BondTx     Bytes  `json:"bondTx"`
	BondScript Bytes  `json:"bondScript"`
	BondSig    Bytes  `json:"bondSig"` // account id signed by key from bond script's pubkey

	// LegacyFeeRefundAddr is an optional field for the client to specify a
	// return address so they may *request* a refund of their legacy
	// registration fee, if they had paid it.
	LegacyFeeRefundAddr string `json:"legacyFeeRefundAddress,omitempty"`
}

Server must use provided account pubkey to verify signed postbond request, otherwise the user could be spamming or putting up a bond for a bogus account or someone else's account.

Further validation is described in subsequent sections of this PR description.

The response payload:

// PostBondResult is the response to the client's PostBond request. If Confs is
// -1, the bond tx was not found on the network, but was otherwise validated.
type PostBondResult struct {
	Signature        // message is BondID | AccountID
	AccountID Bytes  `json:"accountid"`
	AssetID   uint32 `json:"assetID"`
	Amt       uint64 `json:"amt"`
	Expiry    int64  `json:"expiry"` // not locktime, but time when bond expires for dex
	BondID    Bytes  `json:"bondID"`
	Confs     int64  `json:"confs"`
	Tier      int64  `json:"tier"`
}

After validation of the provided data, the DEX attempts to locate the bond transaction on the asset network:

  • If it is not found, a PostBondResult is sent immediately with Confs=-1 set. The bond is NOT stored in the DB. This was a courtesy pre-validation. The user should then broadcast the transaction, wait for the required number of confirmations, and send postbond again.
  • If the located transaction has the required number of confirmations, the bond is stored in the DB as active, and a response is sent.
  • Otherwise, a response is sent with the observed confirmation count, and a coin waiter is started to watch for the txn to reach the required number of confirmations. The bond is stored in DB as pending. When the transaction reaches the required confirmations, it is flagged active in the DB and a bondconfirmed notification is sent.

When the client receives the initial successful response prior to broadcast, they should then broadcast the bond txn, wait for the required confirmations, and resubmit postbond.

Server must record all known bond txns and their amounts and expiry times. There is no explicit invalidation of bonds due to conduct, and instead a user's tier is a balance between bond "strength" (a function of the active bond total amounts) and their conduct score.

TODOs

Auto add bond in client.Core. There are comments in the code regarding an option to automatically postbond when previous bonds are about to expire on DEX. There are questions pertaining to timing, UI, and UX. Presently this PR requires using dexcctl to call the addbond RPC (or a direct http API call to the endpoint of the same name, which is created for said UI plans).

Bond amount. It's pretty clear the bond amount has to be at least an order of magnitude more than the previous registration fee since you get it back, and it's only an opportunity cost having it locked up. I'm thinking like 5 DCR per tier (the bond increment).

Related to bond amount, the violation weights can use rebalancing. Specifically, increase weight for "no swap as taker" (currently 11, increase to 18 or 20) and "no redeem as maker" (currently 7, increase to 12 to 14), the violations that lock counterparty funds (20hr / 8 hr, respectively). Maybe even higher.

Make the tier system scale up order size limits. This was planned outside of fidelity bond work, in terms of conduct score, but it's more intuitive in terms of tier, which is a function of both account bond level and score.

Scale penalty (score change) with amount locked due to violation (e.g. lots/10 * violation score). This was planned regardless of fidelity bonds, but it dovetails nicely with the tier system.

Open Questions

What problems would be solved and/or create by using OP_CHECKSEQUENCEVERIFY (relative time lock) instead of OP_CHECKLOCKTIMEVERIFY?

When and if accounts go away, would output 1 commit to a magic DEX network key instead of account ID?

Is this transaction structure and account commitment output sufficient to prevent txns from being dual purposed for other services with similar TL output structure?

@davecgh
Copy link
Member

davecgh commented Jul 3, 2021

The above uses P2PK, and we can do P2PKH instead, but since part of validation requires the pubkey we probably don't gain anything from this:

There are two primary benefits of using P2PKH over P2PK for outputs (when not wrapped in a P2SH):

  • The public key is not revealed until the output is redeemed
  • The utxoset is smaller when the size of the hash + the additional 2 opcodes is smaller than the size of a public key

Given that, I would argue that it is generally strictly worse to wrap a P2PKH in a P2SH because:

  • The redeem script is generally not revealed until the output is redeemed, which means the public key isn't either
  • The output size is fixed by the P2SH output, so what it's wrapping has no bearing on the size of the utxoset
  • The redemption of the wrapped P2PKH ultimately takes up more space on the chain, given it needs the additional hash and opcodes

One case where wrapped P2PKH can make sense is if the redeem script must be revealed publicly prior to redemption, but the public key(s) it refers to are not needed until redemption time. However, in this particular case, as you noted, the public key is required for the DEX instance(s) to validate the bond. That implies both the redeem script and the public key must be revealed to those DEX instance(s) via an out-of-band mechanism, and therefore wrapped P2PKH would provide no additional security in that regard. The redeem script and public keys will only be known to the originator and the DEX instance(s) that are validating them, so as long as neither reveals them publicly, the public keys will also only known to them.

@chappjc
Copy link
Member Author

chappjc commented Jul 3, 2021

Thanks for providing that input. It confirms what I had concluded that there is actually nothing to be gained from wrapping p2pkh in p2sh (assuming server can't be trusted to keep the pubkey secret). I had started with p2pk instead of p2pkh and will revert it for the reasons you cited, but also because it simplifies part of the validation (direct pubkey comparision after RecoverCompact, rather than having to hash160 (Decred's way!) the pubkey).

The redeem script and public keys will only be known to the originator and the DEX instance(s) that are validating them, so as long as neither reveals them publicly, the public keys will also only known to them

I think this is what even made me consider wrapping p2pkh, but at the end of the day, I don't think "trusting" the server to not reveal the pubkey is smart. Plus in a mesh config, it's likely going to be necessary to have it propagated between DEX nodes.

@davecgh
Copy link
Member

davecgh commented Jul 4, 2021

think this is what even made me consider wrapping p2pkh, but at the end of the day, I don't think "trusting" the server to not reveal the pubkey is smart. Plus in a mesh config, it's likely going to be necessary to have it propagated between DEX nodes.

Right. I didn't explicitly state it, but what I also meant to imply by that final statement is that the server needs the public key to validate things anyway, so if they are going to reveal the public keys by way of the P2PK redeem script, they can just reveal the public keys directly too. It's the same trust model regardless.

@chappjc
Copy link
Member Author

chappjc commented Aug 9, 2021

Just did a challenging rebase after HD keys and stdaddr PRs were merged. I've not retested with this however, so this is a more "drafty" than before. It's probably broken at the moment.

The (*Core).register method is removed since it's primary functions were: (1) switch between legacy and hd account creation, and (2) deal with suspended accounts by defaulting to legacy keys. Both of these are slated for removal in the release containing this PR. This is because the server's 'config' response will contain the required pubkey, and the tier system removes the notion of account suspension entirely.

(*Core).PreRegister remains (below AddBond), although it was not in the initial draft of this PR. I kept it because the UX issue it solved with the introduction of HD keys still exists regardless of the change from fee => bond. However, there is no longer an inprocessRegistration since the new protocol does not use the legacy 'register' route and there is no longer a fee address (and the supported bond assets and min amounts are in the 'config' response). Existing account discovery is done by attempting 'connect' via authDEX. PreRegister uses setupCryptoV2 exclusively when attempting to connect to a new host, where "new" means not in the c.conns map and thus not in the DB as this map is populated on Core startup.

Also, PreRegister no longer returns a paid bool, as this notion has changed with bond. Consumers are intended to consult the returned *core.Exchange structure that includes Tier int64 and BondsPending bool fields. This is documented.

@chappjc chappjc removed this from the 0.4 milestone Nov 17, 2021
@chappjc chappjc changed the title fidelity bonds Proof of concept Feb 20, 2022
@chappjc chappjc changed the title Proof of concept fidelity bonds Feb 20, 2022
@chappjc chappjc changed the title fidelity bonds fidelity bonds (proof of concept) Feb 20, 2022
@chappjc
Copy link
Member Author

chappjc commented Feb 20, 2022

Superseded by #1480
This PR is retained for reference.

@chappjc chappjc closed this Feb 20, 2022
@chappjc chappjc added the bonds fidelity bonds label Feb 20, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bonds fidelity bonds
Projects
Development

Successfully merging this pull request may close these issues.

2 participants