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

multi: add pre-paid bonds #2730

Merged
merged 3 commits into from
Apr 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
140 changes: 136 additions & 4 deletions client/core/bond.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"decred.org/dcrdex/dex"
"decred.org/dcrdex/dex/keygen"
"decred.org/dcrdex/dex/msgjson"
"decred.org/dcrdex/server/account"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/decred/dcrd/hdkeychain/v3"
)
Expand Down Expand Up @@ -807,20 +808,21 @@ func (c *Core) postBond(dc *dexConnection, bond *asset.Bond) (*msgjson.PostBondR
return nil, codedError(registerErr, err)
}

dc.acct.authMtx.Lock()
dc.updateReputation(postBondRes.Reputation, postBondRes.Tier, nil, nil)
dc.acct.authMtx.Unlock()

// Check the response signature.
err = dc.acct.checkSig(postBondRes.Serialize(), postBondRes.Sig)
if err != nil {
c.log.Warnf("postbond: DEX signature validation error: %v", err)
}

if !bytes.Equal(postBondRes.BondID, bondCoin) {
return nil, fmt.Errorf("server reported bond coin ID %v, expected %v", coinIDString(assetID, postBondRes.BondID),
bondCoinStr)
}

dc.acct.authMtx.Lock()
dc.updateReputation(postBondRes.Reputation, postBondRes.Tier, nil, nil)
dc.acct.authMtx.Unlock()

return postBondRes, nil
}

Expand Down Expand Up @@ -926,6 +928,136 @@ func (c *Core) monitorBondConfs(dc *dexConnection, bond *asset.Bond, reqConfs ui
})
}

// RedeemPrepaidBond redeems a pre-paid bond for a dcrdex host server.
func (c *Core) RedeemPrepaidBond(appPW []byte, code []byte, host string, certI any) (tier uint64, err error) {
// Make sure the app has been initialized.
if !c.IsInitialized() {
return 0, fmt.Errorf("app not initialized")
}

// Check the app password.
crypter, err := c.encryptionKey(appPW)
if err != nil {
return 0, codedError(passwordErr, err)
}
defer crypter.Close()

var success, acctExists bool

c.connMtx.RLock()
dc, found := c.conns[host]
c.connMtx.RUnlock()
if found {
acctExists = !dc.acct.isViewOnly()
if acctExists {
if dc.acct.locked() { // require authDEX first to reconcile any existing bond statuses
return 0, newError(acctKeyErr, "acct locked %s (login first)", host)
}
}
Comment on lines +952 to +956
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can remove the nested if: if acctExists && dc.acct.locked() { return 0, newError(acctKeyErr, "acct locked %s (login first)", host) }

} else {
// New DEX connection.
cert, err := parseCert(host, certI, c.net)
if err != nil {
return 0, newError(fileReadErr, "failed to read certificate file from %s: %v", cert, err)
}
dc, err = c.connectDEX(&db.AccountInfo{
Host: host,
Cert: cert,
// bond maintenance options set below.
})
if err != nil {
return 0, codedError(connectionErr, err)
}

// Close the connection to the dex server if the registration fails.
defer func() {
if !success {
dc.connMaster.Disconnect()
}
}()
}

if !acctExists { // new dex connection or pre-existing view-only connection
_, err := c.discoverAccount(dc, crypter)
if err != nil {
return 0, err
}
}

pkBytes := dc.acct.pubKey()
if len(pkBytes) == 0 {
return 0, fmt.Errorf("account keys not decrypted")
}

// Do a postbond request with the raw bytes of the unsigned tx, the bond
// script, and our account pubkey.
postBond := &msgjson.PostBond{
AcctPubKey: pkBytes,
AssetID: account.PrepaidBondID,
// Version: 0,
CoinID: code,
}
postBondRes := new(msgjson.PostBondResult)
if err = dc.signAndRequest(postBond, msgjson.PostBondRoute, postBondRes, DefaultResponseTimeout); err != nil {
return 0, codedError(registerErr, err)
}

// Check the response signature.
err = dc.acct.checkSig(postBondRes.Serialize(), postBondRes.Sig)
if err != nil {
c.log.Warnf("postbond: DEX signature validation error: %v", err)
}

lockTime := postBondRes.Expiry + dc.config().BondExpiry

dbBond := &db.Bond{
// Version: 0,
AssetID: account.PrepaidBondID,
CoinID: code,
LockTime: lockTime,
Strength: postBondRes.Strength,
Confirmed: true,
}

dc.acct.authMtx.Lock()
dc.updateReputation(postBondRes.Reputation, postBondRes.Tier, nil, nil)
dc.acct.bonds = append(dc.acct.bonds, dbBond)
dc.acct.authMtx.Unlock()

if !acctExists {
dc.acct.keyMtx.RLock()
ai := &db.AccountInfo{
Host: dc.acct.host,
Cert: dc.acct.cert,
DEXPubKey: dc.acct.dexPubKey,
EncKeyV2: dc.acct.encKey,
Bonds: []*db.Bond{dbBond},
}
dc.acct.keyMtx.RUnlock()

if err = c.dbCreateOrUpdateAccount(dc, ai); err != nil {
return 0, fmt.Errorf("failed to store pre-paid account for dex %s: %w", host, err)
}
c.addDexConnection(dc)
}

success = true // Don't disconnect anymore.

if err = c.db.AddBond(dc.acct.host, dbBond); err != nil {
return 0, fmt.Errorf("failed to store pre-paid bond for dex %s: %w", host, err)
}

if err = c.bondConfirmed(dc, account.PrepaidBondID, code, postBondRes); err != nil {
return 0, fmt.Errorf("bond redeemed, but failed to auth: %v", err)
}

c.updateBondReserves()

c.notify(newBondAuthUpdate(dc.acct.host, c.exchangeAuth(dc)))

return uint64(postBondRes.Strength), nil
}

func deriveBondKey(bondXPriv *hdkeychain.ExtendedKey, assetID, bondIndex uint32) (*secp256k1.PrivateKey, error) {
kids := []uint32{
assetID + hdkeychain.HardenedKeyStart,
Expand Down
44 changes: 43 additions & 1 deletion client/core/core.go
Original file line number Diff line number Diff line change
Expand Up @@ -4033,7 +4033,7 @@ func (c *Core) GetDEXConfig(dexAddr string, certI any) (*Exchange, error) {
// activity without setting up account keys or communicating account identity
// with the DEX. DiscoverAccount, Post Bond or Register (deprecated) may be used
// to set up a trading account for this DEX if required.
func (c *Core) AddDEX(dexAddr string, certI any) error {
func (c *Core) AddDEX(appPW []byte, dexAddr string, certI any) error {
if !c.IsInitialized() { // TODO: Allow adding view-only DEX without init.
return fmt.Errorf("cannot register DEX because app has not been initialized")
}
Expand Down Expand Up @@ -4097,6 +4097,23 @@ func (c *Core) AddDEX(dexAddr string, certI any) error {
c.conns[dc.acct.host] = dc
c.connMtx.Unlock()

// If a password was provided, try discoverAccount, but OK if we don't find
// it.
if len(appPW) > 0 {
crypter, err := c.encryptionKey(appPW)
if err != nil {
return codedError(passwordErr, err)
}
defer crypter.Close()

paid, err := c.discoverAccount(dc, crypter)
if err != nil {
c.log.Errorf("discoverAccount error during AddDEX: %v", err)
} else if paid {
c.upgradeConnection(dc)
}
}

return nil
}

Expand Down Expand Up @@ -7075,6 +7092,10 @@ func (c *Core) findBondKeyIdx(pkhEqualFn func(bondKey *secp256k1.PrivateKey) boo
// findBond will attempt to find an unknown bond and add it to the live bonds
// slice and db for refunding later. Returns the bond strength if no error.
func (c *Core) findBond(dc *dexConnection, bond *msgjson.Bond) (strength, bondAssetID uint32) {
if bond.AssetID == account.PrepaidBondID {
c.insertPrepaidBond(dc, bond)
return bond.Strength, bond.AssetID
Comment on lines +7095 to +7097
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason why we insert instead of checking db? I assume the redeemed prepaid bond was added to the db, no?

}
symb := dex.BipIDSymbol(bond.AssetID)
bondIDStr := coinIDString(bond.AssetID, bond.CoinID)
c.log.Warnf("Unknown bond reported by server: %v (%s)", bondIDStr, symb)
Expand Down Expand Up @@ -7140,6 +7161,27 @@ func (c *Core) findBond(dc *dexConnection, bond *msgjson.Bond) (strength, bondAs
return strength, bondDetails.AssetID
}

func (c *Core) insertPrepaidBond(dc *dexConnection, bond *msgjson.Bond) {
lockTime := bond.Expiry + dc.config().BondExpiry
dbBond := &db.Bond{
Version: bond.Version,
AssetID: bond.AssetID,
CoinID: bond.CoinID,
LockTime: lockTime,
Strength: bond.Strength,
Confirmed: true,
}

err := c.db.AddBond(dc.acct.host, dbBond)
if err != nil {
c.log.Errorf("Failed to store pre-paid bond dex %v: %w", dc.acct.host, err)
}

dc.acct.authMtx.Lock()
dc.acct.bonds = append(dc.acct.bonds, dbBond)
dc.acct.authMtx.Unlock()
}

func (dc *dexConnection) maxScore() uint32 {
if maxScore := dc.config().MaxScore; maxScore > 0 {
return maxScore
Expand Down
3 changes: 3 additions & 0 deletions client/core/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -1116,6 +1116,9 @@ func token(id []byte) string {
// coinIDString converts a coin ID to a human-readable string. If an error is
// encountered the value starting with "<invalid coin>:" prefix is returned.
func coinIDString(assetID uint32, coinID []byte) string {
if assetID == account.PrepaidBondID {
return "prepaid-bond:" + hex.EncodeToString(coinID)
}
coinStr, err := asset.DecodeCoinID(assetID, coinID)
if err != nil {
// Logging error here with fmt.Printf is better than dropping it. It's not
Expand Down
41 changes: 39 additions & 2 deletions client/webserver/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,15 @@ func (s *WebServer) apiAddDEX(w http.ResponseWriter, r *http.Request) {
if !readPost(w, r, form) {
return
}
cert := []byte(form.Cert)
err := s.core.AddDEX(form.Addr, cert)
defer form.AppPW.Clear()
appPW, err := s.resolvePass(form.AppPW, r)
if err != nil {
s.writeAPIError(w, fmt.Errorf("password error: %w", err))
return
}
cert := []byte(form.Cert)

if err = s.core.AddDEX(appPW, form.Addr, cert); err != nil {
s.writeAPIError(w, err)
return
}
Expand Down Expand Up @@ -469,6 +475,37 @@ func (s *WebServer) apiUpdateBondOptions(w http.ResponseWriter, r *http.Request)
writeJSON(w, simpleAck(), s.indent)
}

func (s *WebServer) apiRedeemPrepaidBond(w http.ResponseWriter, r *http.Request) {
var req struct {
Host string `json:"host"`
Code dex.Bytes `json:"code"`
AppPW encode.PassBytes `json:"appPW"`
Cert string `json:"cert"`
}
defer req.AppPW.Clear()
if !readPost(w, r, &req) {
return
}
appPW, err := s.resolvePass(req.AppPW, r)
if err != nil {
s.writeAPIError(w, fmt.Errorf("password error: %w", err))
return
}
tier, err := s.core.RedeemPrepaidBond(appPW, req.Code, req.Host, req.Cert)
if err != nil {
s.writeAPIError(w, err)
return
}
resp := &struct {
OK bool `json:"ok"`
Tier uint64 `json:"tier"`
}{
OK: true,
Tier: tier,
}
writeJSON(w, resp, s.indent)
}

// apiNewWallet is the handler for the '/newwallet' API request.
func (s *WebServer) apiNewWallet(w http.ResponseWriter, r *http.Request) {
form := new(newWalletForm)
Expand Down
5 changes: 4 additions & 1 deletion client/webserver/live_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -638,7 +638,7 @@ func (c *TCore) GetDEXConfig(host string, certI any) (*core.Exchange, error) {
return tExchanges[firstDEX], nil
}

func (c *TCore) AddDEX(dexAddr string, certI any) error {
func (c *TCore) AddDEX(appPW []byte, dexAddr string, certI any) error {
randomDelay()
if initErrors {
return fmt.Errorf("forced init error")
Expand Down Expand Up @@ -681,6 +681,9 @@ func (c *TCore) PostBond(form *core.PostBondForm) (*core.PostBondResult, error)
ReqConfirms: uint16(ba.Confs),
}, nil
}
func (c *TCore) RedeemPrepaidBond(appPW []byte, code []byte, host string, certI any) (tier uint64, err error) {
return 1, nil
}
func (c *TCore) UpdateBondOptions(form *core.BondOptionsForm) error {
xc := tExchanges[form.Host]
xc.ViewOnly = false
Expand Down
2 changes: 1 addition & 1 deletion client/webserver/site/src/html/bodybuilder.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
</head>
<body >
<div class="popup-notes d-hide" id="popupNotes">
<span data-tmpl="note fs15">
<span data-tmpl="note" class="fs15">
<div class="note-indicator d-inline-block" data-tmpl="indicator"></div>
<span data-tmpl="text"></span>
</span>
Expand Down
22 changes: 21 additions & 1 deletion client/webserver/site/src/html/forms.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,7 @@
<div data-tmpl="assetForm">
<h3 class="text-center">[[[Select your bond asset]]]</h3>
<div data-tmpl="regAssetErr" class="fs14 text-danger flex-center"></div>
<div data-tmpl="bondAssets" class="mt-2">
<div data-tmpl="bondAssets">

<div data-tmpl="bondAssetTmpl" class="border rounded3 d-flex align-items-stretch p-2 hoverbg pointer mt-3">
<div class="flex-center pe-4">
Expand All @@ -287,6 +287,10 @@
</div>

</div>
<button type="button" data-tmpl="usePrepaidBond" class="w-100 mt-3">
<span class="ico-ticket fs22 me-2"></span>
<span class="fs22">Use a pre-paid bond</span>
</button>
<div data-tmpl="whatsABond" class="flex-center fs18 hoverbg pointer underline pt-2 pb-1 mt-1">[[[what_s_a_bond]]]</div>
</div>

Expand Down Expand Up @@ -431,6 +435,22 @@
</tbody>
</table>
</div>
<div data-tmpl="prepaidBonds" class="d-hide">
<div class="py-2 fs14 grey">
<span class="hoverbg pointer" data-tmpl="ppbGoBack"><span class="ico-arrowback fs12 me-1"></span> go back</span>
</div>
<h3>Redeem Pre-paid Bond</h3>
<label for="prepaidBondCode">Code</label>
<input type="text" data-tmpl="prepaidBondCode" autocomplete="off">
<div data-tmpl="prepaidBondPWBox">
<label for="ppbAppPW">[[[Password]]]</label>
<input type="password" data-tmpl="prepaidBondPW" autocomplete="off">
</div>
<div data-tmpl="prepaidBondErr" class="pt-2 px-2 text-danger d-hide"></div>
<div class="text-left mt-2">
<button data-tmpl="submitPrepaidBond" type="button" class="go">[[[Submit]]]</button>
</div>
</div>
{{end}}

{{define "loginForm"}}
Expand Down
1 change: 0 additions & 1 deletion client/webserver/site/src/html/register.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@
<form class="d-hide" id="walletWait">
{{template "waitingForWalletForm"}}
</form>

</div>
</div>
{{template "bottom"}}
Expand Down