From 79c7bb029b9f89cccb198dc400dffddc0fa97a19 Mon Sep 17 00:00:00 2001 From: Wilmer Paulino Date: Tue, 1 Dec 2020 16:56:49 -0800 Subject: [PATCH] account: add support for account renewals --- account/manager.go | 148 ++++++++++++++++++++++++++++++++------------- 1 file changed, 107 insertions(+), 41 deletions(-) diff --git a/account/manager.go b/account/manager.go index ad14dc5..c1edb9f 100644 --- a/account/manager.go +++ b/account/manager.go @@ -1064,15 +1064,14 @@ func (m *Manager) DepositAccount(ctx context.Context, // required new value of the account as part of the deposit. The // selected inputs, along with a change output if needed, will then be // included in the deposit transaction we'll broadcast. - witnessType := determineWitnessType(account, bestHeight) inputs, releaseInputs, changeOutput, err := m.inputsForDeposit( - ctx, depositAmount, witnessType, feeRate, + ctx, depositAmount, multiSigWitness, feeRate, ) if err != nil { return nil, nil, err } newAccountOutput, modifiers, err := createNewAccountOutput( - account, newAccountValue, + account, newAccountValue, nil, ) if err != nil { releaseInputs() @@ -1089,7 +1088,7 @@ func (m *Manager) DepositAccount(ctx context.Context, } modifiers = append(modifiers, StateModifier(StatePendingUpdate)) modifiedAccount, spendPkg, err := m.spendAccount( - ctx, account, inputs, outputs, witnessType, modifiers, false, + ctx, account, inputs, outputs, multiSigWitness, modifiers, false, bestHeight, ) if err != nil { @@ -1122,15 +1121,14 @@ func (m *Manager) WithdrawAccount(ctx context.Context, // To start, we'll need to determine the new value of the account after // creating the outputs specified as part of the withdrawal, which we'll // then use to create the new account output. - witnessType := determineWitnessType(account, bestHeight) - newAccountValue, err := valueAfterWithdrawal( - account, outputs, witnessType, feeRate, + newAccountValue, err := valueAfterAccountUpdate( + account, outputs, multiSigWitness, feeRate, ) if err != nil { return nil, nil, err } newAccountOutput, modifiers, err := createNewAccountOutput( - account, newAccountValue, + account, newAccountValue, nil, ) if err != nil { return nil, nil, err @@ -1143,7 +1141,7 @@ func (m *Manager) WithdrawAccount(ctx context.Context, outputs = append(outputs, newAccountOutput) modifiers = append(modifiers, StateModifier(StatePendingUpdate)) modifiedAccount, spendPkg, err := m.spendAccount( - ctx, account, nil, outputs, witnessType, modifiers, false, + ctx, account, nil, outputs, multiSigWitness, modifiers, false, bestHeight, ) if err != nil { @@ -1153,6 +1151,70 @@ func (m *Manager) WithdrawAccount(ctx context.Context, return modifiedAccount, spendPkg.tx, nil } +// RenewAccount updates the expiration of an open/expired account. This will +// always require a signature from the auctioneer, even after the account has +// expired, to ensure the auctioneer is aware the account is being renewed. +func (m *Manager) RenewAccount(ctx context.Context, + traderKey *btcec.PublicKey, newExpiry uint32, + feeRate chainfee.SatPerKWeight, bestHeight uint32) (*Account, + *wire.MsgTx, error) { + + // The account can only have its expiry updated if it has confirmed + // and/or has expired. + account, err := m.cfg.Store.Account(traderKey) + if err != nil { + return nil, nil, err + } + switch account.State { + case StateOpen, StatePendingBatch, StateExpired: + default: + return nil, nil, fmt.Errorf("account must be in either of %v "+ + "to be renewed", + []State{StateOpen, StatePendingBatch, StateExpired}) + } + + // Validate the new expiry. + if err := validateAccountExpiry(newExpiry, bestHeight); err != nil { + return nil, nil, err + } + + // Determine the new account output after attempting the expiry update. + newAccountValue, err := valueAfterAccountUpdate( + account, nil, multiSigWitness, feeRate, + ) + if err != nil { + return nil, nil, err + } + newAccountOutput, modifiers, err := createNewAccountOutput( + account, newAccountValue, &newExpiry, + ) + if err != nil { + return nil, nil, err + } + + // With the output created, we'll tack on an additional + // `StatePendingUpdate` modifier to our account and proceed with the + // rest of the flow. This should request a signature from the auctioneer + // and assuming it's valid, broadcast the update transaction. + modifiers = append(modifiers, StateModifier(StatePendingUpdate)) + modifiedAccount, spendPkg, err := m.spendAccount( + ctx, account, nil, []*wire.TxOut{newAccountOutput}, + multiSigWitness, modifiers, false, bestHeight, + ) + if err != nil { + return nil, nil, err + } + + // Begin to track the new account expiration, which will overwrite the + // existing expiration request. + err = m.watcher.WatchAccountExpiration(traderKey, modifiedAccount.Expiry) + if err != nil { + return nil, nil, err + } + + return modifiedAccount, spendPkg.tx, nil +} + // BumpAccountFee attempts to bump the fee of an account's most recent // transaction. This is done by locating an eligible output for lnd to CPFP, // otherwise the fee bump will not succeed. Further invocations of this call for @@ -1285,8 +1347,6 @@ func (m *Manager) spendAccount(ctx context.Context, account *Account, ) switch witnessType { case expiryWitness: - // TODO(wilmer): Support modifications through the expiry path. - // This will require a new account expiration. if !isClose { return nil, nil, errors.New("modifications for expired " + "accounts are not currently supported") @@ -1298,6 +1358,9 @@ func (m *Manager) spendAccount(ctx context.Context, account *Account, case multiSigWitness: spendPkg, err = m.createSpendTx(ctx, account, inputs, outputs, 0) + + default: + err = fmt.Errorf("unhandled witness type: %v", witnessType) } if err != nil { return nil, nil, err @@ -1573,11 +1636,11 @@ func addBaseAccountModificationWeight(weightEstimator *input.TxWeightEstimator, return nil } -// valueAfterWithdrawal determines the new value of an account after processing -// a withdrawal to the specified outputs at the provided fee rate. -func valueAfterWithdrawal(accountBeforeWithdrawl *Account, - withdrawalOutputs []*wire.TxOut, witnessType witnessType, - feeRate chainfee.SatPerKWeight) (btcutil.Amount, error) { +// valueAfterAccountUpdate determines the new value of an account after +// processing a withdrawal to the specified outputs at the provided fee rate. +func valueAfterAccountUpdate(account *Account, outputs []*wire.TxOut, + witnessType witnessType, feeRate chainfee.SatPerKWeight) ( + btcutil.Amount, error) { // To determine the new value of the account, we'll need to subtract the // values of all additional outputs and the resulting fee of the @@ -1595,7 +1658,7 @@ func valueAfterWithdrawal(accountBeforeWithdrawl *Account, // We'll then add the weight estimates for any additional outputs // provided, keeping track of the total output value sum as we go. var outputTotal btcutil.Amount - for _, out := range withdrawalOutputs { + for _, out := range outputs { // To determine the proper weight of the output, we'll need to // know its type. pkScript, err := txscript.ParsePkScript(out.PkScript) @@ -1623,7 +1686,7 @@ func valueAfterWithdrawal(accountBeforeWithdrawl *Account, // from our input total and ensure our new account value isn't below our // required minimum. fee := feeRate.FeeForWeight(int64(weightEstimator.Weight())) - newAccountValue := accountBeforeWithdrawl.Value - outputTotal - fee + newAccountValue := account.Value - outputTotal - fee if newAccountValue < MinAccountValue { return 0, fmt.Errorf("new account value is below accepted "+ "minimum of %v", MinAccountValue) @@ -1738,24 +1801,22 @@ coinSelection: } // createNewAccountOutput creates the next account output in the sequence using -// the new account value. -func createNewAccountOutput(account *Account, newAccountValue btcutil.Amount) ( - *wire.TxOut, []Modifier, error) { +// the new account value and optional new account expiry. +func createNewAccountOutput(account *Account, newAccountValue btcutil.Amount, + newAccountExpiry *uint32) (*wire.TxOut, []Modifier, error) { - // Use the next output script in the sequence to avoid script reuse. - newPkScript, err := account.NextOutputScript() - if err != nil { - return nil, nil, err - } - - newAccountOutput := &wire.TxOut{ - Value: int64(newAccountValue), - PkScript: newPkScript, - } modifiers := []Modifier{ ValueModifier(newAccountValue), IncrementBatchKey(), } + if newAccountExpiry != nil { + modifiers = append(modifiers, ExpiryModifier(*newAccountExpiry)) + } + + newAccountOutput, err := account.Copy(modifiers...).Output() + if err != nil { + return nil, nil, err + } return newAccountOutput, modifiers, nil } @@ -1898,16 +1959,9 @@ func validateAccountValue(value, maxValue btcutil.Amount) error { return nil } -// validateAccountParams ensures that a trader has provided sane parameters for -// the creation of a new account. -func validateAccountParams(value, maxValue btcutil.Amount, expiry, - bestHeight uint32) error { - - err := validateAccountValue(value, maxValue) - if err != nil { - return err - } - +// validateAccountExpiry ensures that a trader has provided a sane account expiry +// for the creation/modification of an account. +func validateAccountExpiry(expiry, bestHeight uint32) error { if expiry < bestHeight+minAccountExpiry { return fmt.Errorf("current minimum account expiry allowed is "+ "height %v", bestHeight+minAccountExpiry) @@ -1920,6 +1974,18 @@ func validateAccountParams(value, maxValue btcutil.Amount, expiry, return nil } +// validateAccountParams ensures that a trader has provided sane parameters for +// the creation of a new account. +func validateAccountParams(value, maxValue btcutil.Amount, expiry, + bestHeight uint32) error { + + err := validateAccountValue(value, maxValue) + if err != nil { + return err + } + return validateAccountExpiry(expiry, bestHeight) +} + // numConfsForValue chooses an appropriate number of confirmations to wait for // an account based on its initial value. //