Skip to content

Commit

Permalink
multi: add pre-paid bonds (#2730)
Browse files Browse the repository at this point in the history
* add prepaid bonds
  • Loading branch information
buck54321 committed Apr 30, 2024
1 parent 1149ac5 commit 952f574
Show file tree
Hide file tree
Showing 33 changed files with 595 additions and 61 deletions.
140 changes: 136 additions & 4 deletions client/core/bond.go
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)
}
}
} 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
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
}
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
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
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
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
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
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
Expand Up @@ -42,7 +42,6 @@
<form class="d-hide" id="walletWait">
{{template "waitingForWalletForm"}}
</form>

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

0 comments on commit 952f574

Please sign in to comment.