diff --git a/lib/block_view.go b/lib/block_view.go index 9a616b90d..2e1841849 100644 --- a/lib/block_view.go +++ b/lib/block_view.go @@ -120,6 +120,12 @@ type UtxoView struct { // Global stake across validators GlobalStakeAmountNanos *uint256.Int + // Stake mappings + StakeMapKeyToStakeEntry map[StakeMapKey]*StakeEntry + + // Locked stake mappings + LockedStakeMapKeyToLockedStakeEntry map[LockedStakeMapKey]*LockedStakeEntry + // The hash of the tip the view is currently referencing. Mainly used // for error-checking when doing a bulk operation on the view. TipHash *BlockHash @@ -212,8 +218,16 @@ func (bav *UtxoView) _ResetViewMappingsAfterFlush() { // ValidatorEntries bav.ValidatorMapKeyToValidatorEntry = make(map[ValidatorMapKey]*ValidatorEntry) - // Global stake across validators - bav.GlobalStakeAmountNanos = uint256.NewInt() + // Global stake across validators. We deliberately want this to initialize to nil and not zero + // since a zero value will overwrite an existing GlobalStakeAmountNanos value in the db, whereas + // a nil GlobalStakeAmountNanos value signifies that this value was never set. + bav.GlobalStakeAmountNanos = nil + + // StakeEntries + bav.StakeMapKeyToStakeEntry = make(map[StakeMapKey]*StakeEntry) + + // LockedStakeEntries + bav.LockedStakeMapKeyToLockedStakeEntry = make(map[LockedStakeMapKey]*LockedStakeEntry) } func (bav *UtxoView) CopyUtxoView() (*UtxoView, error) { @@ -473,7 +487,23 @@ func (bav *UtxoView) CopyUtxoView() (*UtxoView, error) { } // Copy the GlobalStakeAmountNanos. - newView.GlobalStakeAmountNanos = bav.GlobalStakeAmountNanos.Clone() + if bav.GlobalStakeAmountNanos != nil { + newView.GlobalStakeAmountNanos = bav.GlobalStakeAmountNanos.Clone() + } + + // Copy the StakeEntries + newView.StakeMapKeyToStakeEntry = make(map[StakeMapKey]*StakeEntry, len(bav.StakeMapKeyToStakeEntry)) + for entryKey, entry := range bav.StakeMapKeyToStakeEntry { + newView.StakeMapKeyToStakeEntry[entryKey] = entry.Copy() + } + + // Copy the LockedStakeEntries + newView.LockedStakeMapKeyToLockedStakeEntry = make( + map[LockedStakeMapKey]*LockedStakeEntry, len(bav.LockedStakeMapKeyToLockedStakeEntry), + ) + for entryKey, entry := range bav.LockedStakeMapKeyToLockedStakeEntry { + newView.LockedStakeMapKeyToLockedStakeEntry[entryKey] = entry.Copy() + } return newView, nil } @@ -1320,6 +1350,18 @@ func (bav *UtxoView) DisconnectTransaction(currentTxn *MsgDeSoTxn, txnHash *Bloc case TxnTypeUnregisterAsValidator: return bav._disconnectUnregisterAsValidator( OperationTypeUnregisterAsValidator, currentTxn, txnHash, utxoOpsForTxn, blockHeight) + + case TxnTypeStake: + return bav._disconnectStake( + OperationTypeStake, currentTxn, txnHash, utxoOpsForTxn, blockHeight) + + case TxnTypeUnstake: + return bav._disconnectUnstake( + OperationTypeUnstake, currentTxn, txnHash, utxoOpsForTxn, blockHeight) + + case TxnTypeUnlockStake: + return bav._disconnectUnlockStake( + OperationTypeUnlockStake, currentTxn, txnHash, utxoOpsForTxn, blockHeight) } return fmt.Errorf("DisconnectBlock: Unimplemented txn type %v", currentTxn.TxnMeta.GetTxnType().String()) @@ -2267,6 +2309,24 @@ func (bav *UtxoView) _checkAndUpdateDerivedKeySpendingLimit( derivedKeyEntry, txnMeta); err != nil { return utxoOpsForTxn, err } + case TxnTypeStake: + txnMeta := txn.TxnMeta.(*StakeMetadata) + if derivedKeyEntry, err = bav._checkStakeTxnSpendingLimitAndUpdateDerivedKey( + derivedKeyEntry, txn.PublicKey, txnMeta); err != nil { + return utxoOpsForTxn, err + } + case TxnTypeUnstake: + txnMeta := txn.TxnMeta.(*UnstakeMetadata) + if derivedKeyEntry, err = bav._checkUnstakeTxnSpendingLimitAndUpdateDerivedKey( + derivedKeyEntry, txn.PublicKey, txnMeta); err != nil { + return utxoOpsForTxn, err + } + case TxnTypeUnlockStake: + txnMeta := txn.TxnMeta.(*UnlockStakeMetadata) + if derivedKeyEntry, err = bav._checkUnlockStakeTxnSpendingLimitAndUpdateDerivedKey( + derivedKeyEntry, txn.PublicKey, txnMeta); err != nil { + return utxoOpsForTxn, err + } default: // If we get here, it means we're dealing with a txn that doesn't have any special // granular limits to deal with. This means we just check whether we have @@ -3229,6 +3289,15 @@ func (bav *UtxoView) _connectTransaction(txn *MsgDeSoTxn, txHash *BlockHash, case TxnTypeUnregisterAsValidator: totalInput, totalOutput, utxoOpsForTxn, err = bav._connectUnregisterAsValidator(txn, txHash, blockHeight, verifySignatures) + case TxnTypeStake: + totalInput, totalOutput, utxoOpsForTxn, err = bav._connectStake(txn, txHash, blockHeight, verifySignatures) + + case TxnTypeUnstake: + totalInput, totalOutput, utxoOpsForTxn, err = bav._connectUnstake(txn, txHash, blockHeight, verifySignatures) + + case TxnTypeUnlockStake: + totalInput, totalOutput, utxoOpsForTxn, err = bav._connectUnlockStake(txn, txHash, blockHeight, verifySignatures) + default: err = fmt.Errorf("ConnectTransaction: Unimplemented txn type %v", txn.TxnMeta.GetTxnType().String()) } @@ -3311,6 +3380,29 @@ func (bav *UtxoView) _connectTransaction(txn *MsgDeSoTxn, txHash *BlockHash, ) } } + if txn.TxnMeta.GetTxnType() == TxnTypeUnlockStake { + if len(utxoOpsForTxn) == 0 { + return nil, 0, 0, 0, errors.New( + "ConnectTransaction: TxnTypeUnlockStake must return UtxoOpsForTxn", + ) + } + utxoOp := utxoOpsForTxn[len(utxoOpsForTxn)-1] + if utxoOp == nil || utxoOp.Type != OperationTypeUnlockStake { + return nil, 0, 0, 0, errors.New( + "ConnectTransaction: TxnTypeUnlockStake must correspond to OperationTypeUnlockStake", + ) + } + totalLockedAmountNanos := uint256.NewInt() + for _, prevLockedStakeEntry := range utxoOp.PrevLockedStakeEntries { + totalLockedAmountNanos, err = SafeUint256().Add( + totalLockedAmountNanos, prevLockedStakeEntry.LockedAmountNanos, + ) + if err != nil { + return nil, 0, 0, 0, errors.Wrapf(err, "ConnectTransaction: error computing TotalLockedAmountNanos: ") + } + } + desoLockedDelta = big.NewInt(0).Neg(totalLockedAmountNanos.ToBig()) + } if big.NewInt(0).Add(balanceDelta, desoLockedDelta).Sign() > 0 { return nil, 0, 0, 0, RuleErrorBalanceChangeGreaterThanZero } @@ -3985,6 +4077,16 @@ func (bav *UtxoView) GetSpendableDeSoBalanceNanosForPublicKey(pkBytes []byte, return spendableBalanceNanos, nil } +func copyExtraData(extraData map[string][]byte) map[string][]byte { + extraDataCopy := make(map[string][]byte) + for key, value := range extraData { + valueCopy := make([]byte, len(value)) + copy(valueCopy, value) + extraDataCopy[key] = valueCopy + } + return extraDataCopy +} + func mergeExtraData(oldMap map[string][]byte, newMap map[string][]byte) map[string][]byte { // Always create the map from scratch, since modifying the map on // newMap could modify the map on the oldMap otherwise. diff --git a/lib/block_view_derived_key.go b/lib/block_view_derived_key.go index 84ef41540..c39055ab8 100644 --- a/lib/block_view_derived_key.go +++ b/lib/block_view_derived_key.go @@ -4,6 +4,7 @@ import ( "bytes" "fmt" "github.com/btcsuite/btcd/btcec" + "github.com/holiman/uint256" "github.com/pkg/errors" "reflect" "strconv" @@ -188,6 +189,9 @@ func (bav *UtxoView) _connectAuthorizeDerivedKey( AssociationLimitMap: make(map[AssociationLimitKey]uint64), AccessGroupMap: make(map[AccessGroupLimitKey]uint64), AccessGroupMemberMap: make(map[AccessGroupMemberLimitKey]uint64), + StakeLimitMap: make(map[StakeLimitKey]*uint256.Int), + UnstakeLimitMap: make(map[StakeLimitKey]*uint256.Int), + UnlockStakeLimitMap: make(map[StakeLimitKey]uint64), } if prevDerivedKeyEntry != nil && !prevDerivedKeyEntry.isDeleted { // Copy the existing transaction spending limit. @@ -305,6 +309,43 @@ func (bav *UtxoView) _connectAuthorizeDerivedKey( } } } + + // ====== Proof of Stake New Txn Types Fork ====== + if blockHeight >= bav.Params.ForkHeights.ProofOfStakeNewTxnTypesBlockHeight { + // StakeLimitMap + for stakeLimitKey, stakingLimit := range transactionSpendingLimit.StakeLimitMap { + if err = bav.IsValidStakeLimitKey(txn.PublicKey, stakeLimitKey); err != nil { + return 0, 0, nil, err + } + if stakingLimit.IsZero() { + delete(newTransactionSpendingLimit.StakeLimitMap, stakeLimitKey) + } else { + newTransactionSpendingLimit.StakeLimitMap[stakeLimitKey] = stakingLimit + } + } + // UnstakeLimitMap + for unstakeLimitKey, unstakingLimit := range transactionSpendingLimit.UnstakeLimitMap { + if err = bav.IsValidStakeLimitKey(txn.PublicKey, unstakeLimitKey); err != nil { + return 0, 0, nil, err + } + if unstakingLimit.IsZero() { + delete(newTransactionSpendingLimit.UnstakeLimitMap, unstakeLimitKey) + } else { + newTransactionSpendingLimit.UnstakeLimitMap[unstakeLimitKey] = unstakingLimit + } + } + // UnlockStakeLimitMap + for unlockStakeLimitKey, transactionCount := range transactionSpendingLimit.UnlockStakeLimitMap { + if err = bav.IsValidStakeLimitKey(txn.PublicKey, unlockStakeLimitKey); err != nil { + return 0, 0, nil, err + } + if transactionCount == 0 { + delete(newTransactionSpendingLimit.UnlockStakeLimitMap, unlockStakeLimitKey) + } else { + newTransactionSpendingLimit.UnlockStakeLimitMap[unlockStakeLimitKey] = transactionCount + } + } + } } } } diff --git a/lib/block_view_derived_key_test.go b/lib/block_view_derived_key_test.go index dc98430a7..65d39fd29 100644 --- a/lib/block_view_derived_key_test.go +++ b/lib/block_view_derived_key_test.go @@ -832,7 +832,7 @@ func _doAuthorizeTxnWithExtraDataAndSpendingLimits(testMeta *TestMeta, utxoView // Sign the transaction now that its inputs are set up. // We have to set the solution byte because we're signing // the transaction with derived key on behalf of the owner. - _signTxnWithDerivedKey(t, txn, derivedPrivBase58Check) + _signTxnWithDerivedKeyAndType(t, txn, derivedPrivBase58Check, 1) txHash := txn.Hash() utxoOps, totalInput, totalOutput, fees, err := diff --git a/lib/block_view_flush.go b/lib/block_view_flush.go index 5ee8ab26a..bb9148328 100644 --- a/lib/block_view_flush.go +++ b/lib/block_view_flush.go @@ -146,6 +146,12 @@ func (bav *UtxoView) FlushToDbWithTxn(txn *badger.Txn, blockHeight uint64) error if err := bav._flushGlobalStakeAmountNanosToDbWithTxn(txn, blockHeight); err != nil { return err } + if err := bav._flushStakeEntriesToDbWithTxn(txn, blockHeight); err != nil { + return err + } + if err := bav._flushLockedStakeEntriesToDbWithTxn(txn, blockHeight); err != nil { + return err + } return nil } diff --git a/lib/block_view_stake.go b/lib/block_view_stake.go new file mode 100644 index 000000000..f5a3dd70a --- /dev/null +++ b/lib/block_view_stake.go @@ -0,0 +1,2514 @@ +package lib + +import ( + "bytes" + "fmt" + "github.com/dgraph-io/badger/v3" + "github.com/golang/glog" + "github.com/holiman/uint256" + "github.com/pkg/errors" + "sort" +) + +// +// TYPES: StakeEntry +// + +type StakeEntry struct { + StakeID *BlockHash + StakerPKID *PKID + ValidatorPKID *PKID + StakeAmountNanos *uint256.Int + ExtraData map[string][]byte + isDeleted bool +} + +type StakeMapKey struct { + ValidatorPKID PKID + StakerPKID PKID +} + +func (stakeEntry *StakeEntry) Copy() *StakeEntry { + return &StakeEntry{ + StakeID: stakeEntry.StakeID.NewBlockHash(), + StakerPKID: stakeEntry.StakerPKID.NewPKID(), + ValidatorPKID: stakeEntry.ValidatorPKID.NewPKID(), + StakeAmountNanos: stakeEntry.StakeAmountNanos.Clone(), + ExtraData: copyExtraData(stakeEntry.ExtraData), + isDeleted: stakeEntry.isDeleted, + } +} + +func (stakeEntry *StakeEntry) Eq(other *StakeEntry) bool { + return stakeEntry.StakeID.IsEqual(other.StakeID) +} + +func (stakeEntry *StakeEntry) ToMapKey() StakeMapKey { + return StakeMapKey{ + StakerPKID: *stakeEntry.StakerPKID, + ValidatorPKID: *stakeEntry.ValidatorPKID, + } +} + +func (stakeEntry *StakeEntry) RawEncodeWithoutMetadata(blockHeight uint64, skipMetadata ...bool) []byte { + var data []byte + data = append(data, EncodeToBytes(blockHeight, stakeEntry.StakeID, skipMetadata...)...) + data = append(data, EncodeToBytes(blockHeight, stakeEntry.StakerPKID, skipMetadata...)...) + data = append(data, EncodeToBytes(blockHeight, stakeEntry.ValidatorPKID, skipMetadata...)...) + data = append(data, EncodeUint256(stakeEntry.StakeAmountNanos)...) + data = append(data, EncodeExtraData(stakeEntry.ExtraData)...) + return data +} + +func (stakeEntry *StakeEntry) RawDecodeWithoutMetadata(blockHeight uint64, rr *bytes.Reader) error { + var err error + + // StakeID + stakeEntry.StakeID, err = DecodeDeSoEncoder(&BlockHash{}, rr) + if err != nil { + return errors.Wrapf(err, "StakeEntry.Decode: Problem reading StakeID: ") + } + + // StakerPKID + stakeEntry.StakerPKID, err = DecodeDeSoEncoder(&PKID{}, rr) + if err != nil { + return errors.Wrapf(err, "StakeEntry.Decode: Problem reading StakerPKID: ") + } + + // ValidatorPKID + stakeEntry.ValidatorPKID, err = DecodeDeSoEncoder(&PKID{}, rr) + if err != nil { + return errors.Wrapf(err, "StakeEntry.Decode: Problem reading ValidatorPKID: ") + } + + // StakeAmountNanos + stakeEntry.StakeAmountNanos, err = DecodeUint256(rr) + if err != nil { + return errors.Wrapf(err, "StakeEntry.Decode: Problem reading StakeAmountNanos: ") + } + + // ExtraData + stakeEntry.ExtraData, err = DecodeExtraData(rr) + if err != nil { + return errors.Wrapf(err, "StakeEntry.Decode: Problem reading ExtraData: ") + } + + return err +} + +func (stakeEntry *StakeEntry) GetVersionByte(blockHeight uint64) byte { + return 0 +} + +func (stakeEntry *StakeEntry) GetEncoderType() EncoderType { + return EncoderTypeStakeEntry +} + +// +// TYPES: LockedStakeEntry +// + +type LockedStakeEntry struct { + LockedStakeID *BlockHash + StakerPKID *PKID + ValidatorPKID *PKID + LockedAmountNanos *uint256.Int + LockedAtEpochNumber uint64 + ExtraData map[string][]byte + isDeleted bool +} + +type LockedStakeMapKey struct { + ValidatorPKID PKID + StakerPKID PKID + LockedAtEpochNumber uint64 +} + +func (lockedStakeEntry *LockedStakeEntry) Copy() *LockedStakeEntry { + return &LockedStakeEntry{ + LockedStakeID: lockedStakeEntry.LockedStakeID.NewBlockHash(), + StakerPKID: lockedStakeEntry.StakerPKID.NewPKID(), + ValidatorPKID: lockedStakeEntry.ValidatorPKID.NewPKID(), + LockedAmountNanos: lockedStakeEntry.LockedAmountNanos.Clone(), + LockedAtEpochNumber: lockedStakeEntry.LockedAtEpochNumber, + ExtraData: copyExtraData(lockedStakeEntry.ExtraData), + isDeleted: lockedStakeEntry.isDeleted, + } +} + +func (lockedStakeEntry *LockedStakeEntry) Eq(other *LockedStakeEntry) bool { + return lockedStakeEntry.LockedStakeID.IsEqual(other.LockedStakeID) +} + +func (lockedStakeEntry *LockedStakeEntry) ToMapKey() LockedStakeMapKey { + return LockedStakeMapKey{ + StakerPKID: *lockedStakeEntry.StakerPKID, + ValidatorPKID: *lockedStakeEntry.ValidatorPKID, + LockedAtEpochNumber: lockedStakeEntry.LockedAtEpochNumber, + } +} + +func (lockedStakeEntry *LockedStakeEntry) RawEncodeWithoutMetadata(blockHeight uint64, skipMetadata ...bool) []byte { + var data []byte + data = append(data, EncodeToBytes(blockHeight, lockedStakeEntry.LockedStakeID, skipMetadata...)...) + data = append(data, EncodeToBytes(blockHeight, lockedStakeEntry.StakerPKID, skipMetadata...)...) + data = append(data, EncodeToBytes(blockHeight, lockedStakeEntry.ValidatorPKID, skipMetadata...)...) + data = append(data, EncodeUint256(lockedStakeEntry.LockedAmountNanos)...) + data = append(data, UintToBuf(lockedStakeEntry.LockedAtEpochNumber)...) + data = append(data, EncodeExtraData(lockedStakeEntry.ExtraData)...) + return data +} + +func (lockedStakeEntry *LockedStakeEntry) RawDecodeWithoutMetadata(blockHeight uint64, rr *bytes.Reader) error { + var err error + + // LockedStakeID + lockedStakeEntry.LockedStakeID, err = DecodeDeSoEncoder(&BlockHash{}, rr) + if err != nil { + return errors.Wrapf(err, "LockedStakeEntry.Decode: Problem reading LockedStakeID: ") + } + + // StakerPKID + lockedStakeEntry.StakerPKID, err = DecodeDeSoEncoder(&PKID{}, rr) + if err != nil { + return errors.Wrapf(err, "LockedStakeEntry.Decode: Problem reading StakerPKID: ") + } + + // ValidatorPKID + lockedStakeEntry.ValidatorPKID, err = DecodeDeSoEncoder(&PKID{}, rr) + if err != nil { + return errors.Wrapf(err, "LockedStakeEntry.Decode: Problem reading ValidatorPKID: ") + } + + // LockedAmountNanos + lockedStakeEntry.LockedAmountNanos, err = DecodeUint256(rr) + if err != nil { + return errors.Wrapf(err, "LockedStakeEntry.Decode: Problem reading LockedAmountNanos: ") + } + + // LockedAtEpochNumber + lockedStakeEntry.LockedAtEpochNumber, err = ReadUvarint(rr) + if err != nil { + return errors.Wrapf(err, "LockedStakeEntry.Decode: Problem reading LockedAtEpochNumber: ") + } + + // ExtraData + lockedStakeEntry.ExtraData, err = DecodeExtraData(rr) + if err != nil { + return errors.Wrapf(err, "LockedStakeEntry.Decode: Problem reading ExtraData: ") + } + + return err +} + +func (lockedStakeEntry *LockedStakeEntry) GetVersionByte(blockHeight uint64) byte { + return 0 +} + +func (lockedStakeEntry *LockedStakeEntry) GetEncoderType() EncoderType { + return EncoderTypeLockedStakeEntry +} + +// +// TYPES: StakeMetadata +// + +type StakeMetadata struct { + ValidatorPublicKey *PublicKey + StakeAmountNanos *uint256.Int +} + +func (txnData *StakeMetadata) GetTxnType() TxnType { + return TxnTypeStake +} + +func (txnData *StakeMetadata) ToBytes(preSignature bool) ([]byte, error) { + var data []byte + data = append(data, EncodeByteArray(txnData.ValidatorPublicKey.ToBytes())...) + data = append(data, EncodeUint256(txnData.StakeAmountNanos)...) + return data, nil +} + +func (txnData *StakeMetadata) FromBytes(data []byte) error { + rr := bytes.NewReader(data) + + // ValidatorPublicKey + validatorPublicKeyBytes, err := DecodeByteArray(rr) + if err != nil { + return errors.Wrapf(err, "StakeMetadata.FromBytes: Problem reading ValidatorPublicKey: ") + } + txnData.ValidatorPublicKey = NewPublicKey(validatorPublicKeyBytes) + + // StakeAmountNanos + txnData.StakeAmountNanos, err = DecodeUint256(rr) + if err != nil { + return errors.Wrapf(err, "StakeMetadata.FromBytes: Problem reading StakeAmountNanos: ") + } + + return nil +} + +func (txnData *StakeMetadata) New() DeSoTxnMetadata { + return &StakeMetadata{} +} + +// +// TYPES: UnstakeMetadata +// + +type UnstakeMetadata struct { + ValidatorPublicKey *PublicKey + UnstakeAmountNanos *uint256.Int +} + +func (txnData *UnstakeMetadata) GetTxnType() TxnType { + return TxnTypeUnstake +} + +func (txnData *UnstakeMetadata) ToBytes(preSignature bool) ([]byte, error) { + var data []byte + data = append(data, EncodeByteArray(txnData.ValidatorPublicKey.ToBytes())...) + data = append(data, EncodeUint256(txnData.UnstakeAmountNanos)...) + return data, nil +} + +func (txnData *UnstakeMetadata) FromBytes(data []byte) error { + rr := bytes.NewReader(data) + + // ValidatorPublicKey + validatorPublicKeyBytes, err := DecodeByteArray(rr) + if err != nil { + return errors.Wrapf(err, "UnstakeMetadata.FromBytes: Problem reading ValidatorPublicKey: ") + } + txnData.ValidatorPublicKey = NewPublicKey(validatorPublicKeyBytes) + + // UnstakeAmountNanos + txnData.UnstakeAmountNanos, err = DecodeUint256(rr) + if err != nil { + return errors.Wrapf(err, "UnstakeMetadata.FromBytes: Problem reading UnstakeAmountNanos: ") + } + + return nil +} + +func (txnData *UnstakeMetadata) New() DeSoTxnMetadata { + return &UnstakeMetadata{} +} + +// +// TYPES: UnlockStakeMetadata +// + +type UnlockStakeMetadata struct { + ValidatorPublicKey *PublicKey + StartEpochNumber uint64 + EndEpochNumber uint64 +} + +func (txnData *UnlockStakeMetadata) GetTxnType() TxnType { + return TxnTypeUnlockStake +} + +func (txnData *UnlockStakeMetadata) ToBytes(preSignature bool) ([]byte, error) { + var data []byte + data = append(data, EncodeByteArray(txnData.ValidatorPublicKey.ToBytes())...) + data = append(data, UintToBuf(txnData.StartEpochNumber)...) + data = append(data, UintToBuf(txnData.EndEpochNumber)...) + return data, nil +} + +func (txnData *UnlockStakeMetadata) FromBytes(data []byte) error { + rr := bytes.NewReader(data) + + // ValidatorPublicKey + validatorPublicKeyBytes, err := DecodeByteArray(rr) + if err != nil { + return errors.Wrapf(err, "UnlockStakeMetadata.FromBytes: Problem reading ValidatorPublicKey: ") + } + txnData.ValidatorPublicKey = NewPublicKey(validatorPublicKeyBytes) + + // StartEpochNumber + txnData.StartEpochNumber, err = ReadUvarint(rr) + if err != nil { + return errors.Wrapf(err, "UnlockStakeMetadata.FromBytes: Problem reading StartEpochNumber: ") + } + + // EndEpochNumber + txnData.EndEpochNumber, err = ReadUvarint(rr) + if err != nil { + return errors.Wrapf(err, "UnlockStakeMetadata.FromBytes: Problem reading EndEpochNumber: ") + } + + return nil +} + +func (txnData *UnlockStakeMetadata) New() DeSoTxnMetadata { + return &UnlockStakeMetadata{} +} + +// +// TYPES: StakeTxindexMetadata +// + +type StakeTxindexMetadata struct { + StakerPublicKeyBase58Check string + ValidatorPublicKeyBase58Check string + StakeAmountNanos *uint256.Int +} + +func (txindexMetadata *StakeTxindexMetadata) RawEncodeWithoutMetadata(blockHeight uint64, skipMetadata ...bool) []byte { + var data []byte + data = append(data, EncodeByteArray([]byte(txindexMetadata.StakerPublicKeyBase58Check))...) + data = append(data, EncodeByteArray([]byte(txindexMetadata.ValidatorPublicKeyBase58Check))...) + data = append(data, EncodeUint256(txindexMetadata.StakeAmountNanos)...) + return data +} + +func (txindexMetadata *StakeTxindexMetadata) RawDecodeWithoutMetadata(blockHeight uint64, rr *bytes.Reader) error { + var err error + + // StakerPublicKeyBase58Check + stakerPublicKeyBase58CheckBytes, err := DecodeByteArray(rr) + if err != nil { + return errors.Wrapf(err, "StakeTxindexMetadata.Decode: Problem reading StakerPublicKeyBase58Check: ") + } + txindexMetadata.StakerPublicKeyBase58Check = string(stakerPublicKeyBase58CheckBytes) + + // ValidatorPublicKeyBase58Check + validatorPublicKeyBase58CheckBytes, err := DecodeByteArray(rr) + if err != nil { + return errors.Wrapf(err, "StakeTxindexMetadata.Decode: Problem reading ValidatorPublicKeyBase58Check: ") + } + txindexMetadata.ValidatorPublicKeyBase58Check = string(validatorPublicKeyBase58CheckBytes) + + // StakeAmountNanos + txindexMetadata.StakeAmountNanos, err = DecodeUint256(rr) + if err != nil { + return errors.Wrapf(err, "StakeTxindexMetadata.Decode: Problem reading StakeAmountNanos: ") + } + + return nil +} + +func (txindexMetadata *StakeTxindexMetadata) GetVersionByte(blockHeight uint64) byte { + return 0 +} + +func (txindexMetadata *StakeTxindexMetadata) GetEncoderType() EncoderType { + return EncoderTypeStakeTxindexMetadata +} + +// +// TYPES: UnstakeTxindexMetadata +// + +type UnstakeTxindexMetadata struct { + StakerPublicKeyBase58Check string + ValidatorPublicKeyBase58Check string + UnstakeAmountNanos *uint256.Int +} + +func (txindexMetadata *UnstakeTxindexMetadata) RawEncodeWithoutMetadata(blockHeight uint64, skipMetadata ...bool) []byte { + var data []byte + data = append(data, EncodeByteArray([]byte(txindexMetadata.StakerPublicKeyBase58Check))...) + data = append(data, EncodeByteArray([]byte(txindexMetadata.ValidatorPublicKeyBase58Check))...) + data = append(data, EncodeUint256(txindexMetadata.UnstakeAmountNanos)...) + return data +} + +func (txindexMetadata *UnstakeTxindexMetadata) RawDecodeWithoutMetadata(blockHeight uint64, rr *bytes.Reader) error { + var err error + + // StakerPublicKeyBase58Check + stakerPublicKeyBase58CheckBytes, err := DecodeByteArray(rr) + if err != nil { + return errors.Wrapf(err, "UnstakeTxindexMetadata.Decode: Problem reading StakerPublicKeyBase58Check: ") + } + txindexMetadata.StakerPublicKeyBase58Check = string(stakerPublicKeyBase58CheckBytes) + + // ValidatorPublicKeyBase58Check + validatorPublicKeyBase58CheckBytes, err := DecodeByteArray(rr) + if err != nil { + return errors.Wrapf(err, "UnstakeTxindexMetadata.Decode: Problem reading ValidatorPublicKeyBase58Check: ") + } + txindexMetadata.ValidatorPublicKeyBase58Check = string(validatorPublicKeyBase58CheckBytes) + + // UnstakeAmountNanos + txindexMetadata.UnstakeAmountNanos, err = DecodeUint256(rr) + if err != nil { + return errors.Wrapf(err, "UnstakeTxindexMetadata.Decode: Problem reading UnstakeAmountNanos: ") + } + + return nil +} + +func (txindexMetadata *UnstakeTxindexMetadata) GetVersionByte(blockHeight uint64) byte { + return 0 +} + +func (txindexMetadata *UnstakeTxindexMetadata) GetEncoderType() EncoderType { + return EncoderTypeUnstakeTxindexMetadata +} + +// +// TYPES: UnlockStakeTxindexMetadata +// + +type UnlockStakeTxindexMetadata struct { + StakerPublicKeyBase58Check string + ValidatorPublicKeyBase58Check string + StartEpochNumber uint64 + EndEpochNumber uint64 + TotalUnlockedAmountNanos *uint256.Int +} + +func (txindexMetadata *UnlockStakeTxindexMetadata) RawEncodeWithoutMetadata(blockHeight uint64, skipMetadata ...bool) []byte { + var data []byte + data = append(data, EncodeByteArray([]byte(txindexMetadata.StakerPublicKeyBase58Check))...) + data = append(data, EncodeByteArray([]byte(txindexMetadata.ValidatorPublicKeyBase58Check))...) + data = append(data, UintToBuf(txindexMetadata.StartEpochNumber)...) + data = append(data, UintToBuf(txindexMetadata.EndEpochNumber)...) + data = append(data, EncodeUint256(txindexMetadata.TotalUnlockedAmountNanos)...) + return data +} + +func (txindexMetadata *UnlockStakeTxindexMetadata) RawDecodeWithoutMetadata(blockHeight uint64, rr *bytes.Reader) error { + var err error + + // StakerPublicKeyBase58Check + stakerPublicKeyBase58CheckBytes, err := DecodeByteArray(rr) + if err != nil { + return errors.Wrapf(err, "UnlockStakeTxindexMetadata.Decode: Problem reading StakerPublicKeyBase58Check: ") + } + txindexMetadata.StakerPublicKeyBase58Check = string(stakerPublicKeyBase58CheckBytes) + + // ValidatorPublicKeyBase58Check + validatorPublicKeyBase58CheckBytes, err := DecodeByteArray(rr) + if err != nil { + return errors.Wrapf(err, "UnlockStakeTxindexMetadata.Decode: Problem reading ValidatorPublicKeyBase58Check: ") + } + txindexMetadata.ValidatorPublicKeyBase58Check = string(validatorPublicKeyBase58CheckBytes) + + // StartEpochNumber + txindexMetadata.StartEpochNumber, err = ReadUvarint(rr) + if err != nil { + return errors.Wrapf(err, "UnlockStakeTxindexMetadata.Decode: Problem reading StartEpochNumber: ") + } + + // EndEpochNumber + txindexMetadata.EndEpochNumber, err = ReadUvarint(rr) + if err != nil { + return errors.Wrapf(err, "UnlockStakeTxindexMetadata.Decode: Problem reading EndEpochNumber: ") + } + + // TotalUnlockedAmountNanos + txindexMetadata.TotalUnlockedAmountNanos, err = DecodeUint256(rr) + if err != nil { + return errors.Wrapf(err, "UnlockStakeTxindexMetadata.Decode: Problem reading TotalUnlockedAmountNanos: ") + } + + return nil +} + +func (txindexMetadata *UnlockStakeTxindexMetadata) GetVersionByte(blockHeight uint64) byte { + return 0 +} + +func (txindexMetadata *UnlockStakeTxindexMetadata) GetEncoderType() EncoderType { + return EncoderTypeUnlockStakeTxindexMetadata +} + +// +// DB UTILS +// + +func DBKeyForStakeByValidatorByStaker(stakeEntry *StakeEntry) []byte { + var data []byte + data = append(data, Prefixes.PrefixStakeByValidatorByStaker...) + data = append(data, stakeEntry.ValidatorPKID.ToBytes()...) + data = append(data, stakeEntry.StakerPKID.ToBytes()...) + return data +} + +func DBKeyForLockedStakeByValidatorByStakerByLockedAt(lockedStakeEntry *LockedStakeEntry) []byte { + data := DBPrefixKeyForLockedStakeByValidatorByStaker(lockedStakeEntry) + data = append(data, UintToBuf(lockedStakeEntry.LockedAtEpochNumber)...) + return data +} + +func DBPrefixKeyForLockedStakeByValidatorByStaker(lockedStakeEntry *LockedStakeEntry) []byte { + var data []byte + data = append(data, Prefixes.PrefixLockedStakeByValidatorByStakerByLockedAt...) + data = append(data, lockedStakeEntry.ValidatorPKID.ToBytes()...) + data = append(data, lockedStakeEntry.StakerPKID.ToBytes()...) + return data +} + +func DBGetStakeEntry( + handle *badger.DB, + snap *Snapshot, + validatorPKID *PKID, + stakerPKID *PKID, +) (*StakeEntry, error) { + var ret *StakeEntry + err := handle.View(func(txn *badger.Txn) error { + var innerErr error + ret, innerErr = DBGetStakeEntryWithTxn(txn, snap, validatorPKID, stakerPKID) + return innerErr + }) + return ret, err +} + +func DBGetStakeEntryWithTxn( + txn *badger.Txn, + snap *Snapshot, + validatorPKID *PKID, + stakerPKID *PKID, +) (*StakeEntry, error) { + // Retrieve StakeEntry from db. + key := DBKeyForStakeByValidatorByStaker(&StakeEntry{ValidatorPKID: validatorPKID, StakerPKID: stakerPKID}) + stakeEntryBytes, err := DBGetWithTxn(txn, snap, key) + if err != nil { + // We don't want to error if the key isn't found. Instead, return nil. + if err == badger.ErrKeyNotFound { + return nil, nil + } + return nil, errors.Wrapf(err, "DBGetStakeByValidatorByStaker: problem retrieving StakeEntry: ") + } + + // Decode StakeEntry from bytes. + rr := bytes.NewReader(stakeEntryBytes) + stakeEntry, err := DecodeDeSoEncoder(&StakeEntry{}, rr) + if err != nil { + return nil, errors.Wrapf(err, "DBGetStakeByValidatorByStaker: problem decoding StakeEntry: ") + } + return stakeEntry, nil +} + +func DBGetLockedStakeEntry( + handle *badger.DB, + snap *Snapshot, + validatorPKID *PKID, + stakerPKID *PKID, + lockedAtEpochNumber uint64, +) (*LockedStakeEntry, error) { + var ret *LockedStakeEntry + err := handle.View(func(txn *badger.Txn) error { + var innerErr error + ret, innerErr = DBGetLockedStakeEntryWithTxn( + txn, snap, validatorPKID, stakerPKID, lockedAtEpochNumber, + ) + return innerErr + }) + return ret, err +} + +func DBGetLockedStakeEntryWithTxn( + txn *badger.Txn, + snap *Snapshot, + validatorPKID *PKID, + stakerPKID *PKID, + lockedAtEpochNumber uint64, +) (*LockedStakeEntry, error) { + // Retrieve LockedStakeEntry from db. + key := DBKeyForLockedStakeByValidatorByStakerByLockedAt(&LockedStakeEntry{ + ValidatorPKID: validatorPKID, + StakerPKID: stakerPKID, + LockedAtEpochNumber: lockedAtEpochNumber, + }) + lockedStakeEntryBytes, err := DBGetWithTxn(txn, snap, key) + if err != nil { + // We don't want to error if the key isn't found. Instead, return nil. + if err == badger.ErrKeyNotFound { + return nil, nil + } + return nil, errors.Wrapf( + err, "DBGetLockedStakeByValidatorByStakerByLockedAt: problem retrieving LockedStakeEntry: ", + ) + } + + // Decode LockedStakeEntry from bytes. + rr := bytes.NewReader(lockedStakeEntryBytes) + lockedStakeEntry, err := DecodeDeSoEncoder(&LockedStakeEntry{}, rr) + if err != nil { + return nil, errors.Wrapf( + err, "DBGetLockedStakeByValidatorByStakerByLockedAt: problem decoding LockedStakeEntry: ", + ) + } + return lockedStakeEntry, nil +} + +func DBGetLockedStakeEntriesInRange( + handle *badger.DB, + snap *Snapshot, + validatorPKID *PKID, + stakerPKID *PKID, + startEpochNumber uint64, + endEpochNumber uint64, +) ([]*LockedStakeEntry, error) { + var ret []*LockedStakeEntry + var err error + handle.View(func(txn *badger.Txn) error { + ret, err = DBGetLockedStakeEntriesInRangeWithTxn( + txn, snap, validatorPKID, stakerPKID, startEpochNumber, endEpochNumber, + ) + return nil + }) + return ret, err +} + +func DBGetLockedStakeEntriesInRangeWithTxn( + txn *badger.Txn, + snap *Snapshot, + validatorPKID *PKID, + stakerPKID *PKID, + startEpochNumber uint64, + endEpochNumber uint64, +) ([]*LockedStakeEntry, error) { + // Retrieve LockedStakeEntries from db matching ValidatorPKID, StakerPKID, and + // StartEpochNumber <= LockedAtEpochNumber <= EndEpochNumber. + + // Start at the StartEpochNumber. + startKey := DBKeyForLockedStakeByValidatorByStakerByLockedAt(&LockedStakeEntry{ + ValidatorPKID: validatorPKID, + StakerPKID: stakerPKID, + LockedAtEpochNumber: startEpochNumber, + }) + + // Consider only LockedStakeEntries for this ValidatorPKID, StakerPKID. + prefixKey := DBPrefixKeyForLockedStakeByValidatorByStaker(&LockedStakeEntry{ + ValidatorPKID: validatorPKID, + StakerPKID: stakerPKID, + }) + + // Create an iterator. + iterator := txn.NewIterator(badger.DefaultIteratorOptions) + defer iterator.Close() + + // Store matching LockedStakeEntries to return. + var lockedStakeEntries []*LockedStakeEntry + + // Loop. + for iterator.Seek(startKey); iterator.ValidForPrefix(prefixKey); iterator.Next() { + // Retrieve the LockedStakeEntryBytes. + lockedStakeEntryBytes, err := iterator.Item().ValueCopy(nil) + if err != nil { + return nil, errors.Wrapf(err, "DBGetLockedStakeEntriesInRange: error retrieving LockedStakeEntry: ") + } + + // Convert LockedStakeEntryBytes to LockedStakeEntry. + rr := bytes.NewReader(lockedStakeEntryBytes) + lockedStakeEntry, err := DecodeDeSoEncoder(&LockedStakeEntry{}, rr) + if err != nil { + return nil, errors.Wrapf(err, "DBGetLockedStakeEntriesInRange: error decoding LockedStakeEntry: ") + } + + // Break if LockedStakeEntry.LockedAtEpochNumber > EndEpochNumber. + if lockedStakeEntry.LockedAtEpochNumber > endEpochNumber { + break + } + + // Add LockedStakeEntry to return slice. + lockedStakeEntries = append(lockedStakeEntries, lockedStakeEntry) + } + + return lockedStakeEntries, nil +} + +func DBPutStakeEntryWithTxn( + txn *badger.Txn, + snap *Snapshot, + stakeEntry *StakeEntry, + blockHeight uint64, +) error { + if stakeEntry == nil { + return nil + } + + // Set StakeEntry in PrefixStakeByValidatorByStaker. + key := DBKeyForStakeByValidatorByStaker(stakeEntry) + if err := DBSetWithTxn(txn, snap, key, EncodeToBytes(blockHeight, stakeEntry)); err != nil { + return errors.Wrapf( + err, "DBPutStakeWithTxn: problem storing StakeEntry in index PrefixStakeByValidatorByStaker", + ) + } + + return nil +} + +func DBPutLockedStakeEntryWithTxn( + txn *badger.Txn, + snap *Snapshot, + lockedStakeEntry *LockedStakeEntry, + blockHeight uint64, +) error { + if lockedStakeEntry == nil { + return nil + } + + // Set LockedStakeEntry in PrefixLockedStakeByValidatorByStakerByLockedAt. + key := DBKeyForLockedStakeByValidatorByStakerByLockedAt(lockedStakeEntry) + if err := DBSetWithTxn(txn, snap, key, EncodeToBytes(blockHeight, lockedStakeEntry)); err != nil { + return errors.Wrapf( + err, "DBPutLockedStakeWithTxn: problem storing LockedStakeEntry in index PrefixLockedStakeByValidatorByStakerByLockedAt", + ) + } + + return nil +} + +func DBDeleteStakeEntryWithTxn( + txn *badger.Txn, + snap *Snapshot, + stakeEntry *StakeEntry, + blockHeight uint64, +) error { + if stakeEntry == nil { + return nil + } + + // Delete StakeEntry from PrefixStakeByValidatorByStaker. + key := DBKeyForStakeByValidatorByStaker(stakeEntry) + if err := DBDeleteWithTxn(txn, snap, key); err != nil { + return errors.Wrapf( + err, "DBDeleteStakeWithTxn: problem deleting StakeEntry from index PrefixStakeByValidatorByStaker", + ) + } + + return nil +} + +func DBDeleteLockedStakeEntryWithTxn( + txn *badger.Txn, + snap *Snapshot, + lockedStakeEntry *LockedStakeEntry, + blockHeight uint64, +) error { + if lockedStakeEntry == nil { + return nil + } + + // Delete LockedStakeEntry from PrefixLockedStakeByValidatorByStakerByLockedAt. + key := DBKeyForLockedStakeByValidatorByStakerByLockedAt(lockedStakeEntry) + if err := DBDeleteWithTxn(txn, snap, key); err != nil { + return errors.Wrapf( + err, "DBDeleteLockedStakeWithTxn: problem deleting StakeEntry from index PrefixLockedStakeByValidatorByStakerByLockedAt", + ) + } + + return nil +} + +// +// BLOCKCHAIN UTILS +// + +func (bc *Blockchain) CreateStakeTxn( + transactorPublicKey []byte, + metadata *StakeMetadata, + extraData map[string][]byte, + minFeeRateNanosPerKB uint64, + mempool *DeSoMempool, + additionalOutputs []*DeSoOutput, +) ( + _txn *MsgDeSoTxn, + _totalInput uint64, + _changeAmount uint64, + _fees uint64, + _err error, +) { + // Create a txn containing the metadata fields. + txn := &MsgDeSoTxn{ + PublicKey: transactorPublicKey, + TxnMeta: metadata, + TxOutputs: additionalOutputs, + ExtraData: extraData, + // We wait to compute the signature until + // we've added all the inputs and change. + } + + // Create a new UtxoView. If we have access to a mempool object, use + // it to get an augmented view that factors in pending transactions. + utxoView, err := NewUtxoView(bc.db, bc.params, bc.postgres, bc.snapshot) + if err != nil { + return nil, 0, 0, 0, errors.Wrap( + err, "Blockchain.CreateStakeTxn: problem creating new utxo view: ", + ) + } + if mempool != nil { + utxoView, err = mempool.GetAugmentedUniversalView() + if err != nil { + return nil, 0, 0, 0, errors.Wrapf( + err, "Blockchain.CreateStakeTxn: problem getting augmented utxo view from mempool: ", + ) + } + } + + // Validate txn metadata. + blockHeight := bc.blockTip().Height + 1 + if err = utxoView.IsValidStakeMetadata(transactorPublicKey, metadata, blockHeight); err != nil { + return nil, 0, 0, 0, errors.Wrapf( + err, "Blockchain.CreateStakeTxn: invalid txn metadata: ", + ) + } + + // We don't need to make any tweaks to the amount because + // it's basically a standard "pay per kilobyte" transaction. + totalInput, spendAmount, changeAmount, fees, err := bc.AddInputsAndChangeToTransaction( + txn, minFeeRateNanosPerKB, mempool, + ) + if err != nil { + return nil, 0, 0, 0, errors.Wrapf( + err, "Blockchain.CreateStakeTxn: problem adding inputs: ", + ) + } + + // Validate that the transaction has at least one input, even if it all goes + // to change. This ensures that the transaction will not be "replayable." + if len(txn.TxInputs) == 0 && bc.blockTip().Height+1 < bc.params.ForkHeights.BalanceModelBlockHeight { + return nil, 0, 0, 0, errors.New( + "Blockchain.CreateStakeTxn: txn has zero inputs, try increasing the fee rate", + ) + } + + // Sanity-check that the spendAmount is zero. + if spendAmount != 0 { + return nil, 0, 0, 0, fmt.Errorf( + "Blockchain.CreateStakeTxn: spend amount is non-zero: %d", spendAmount, + ) + } + return txn, totalInput, changeAmount, fees, nil +} + +func (bc *Blockchain) CreateUnstakeTxn( + transactorPublicKey []byte, + metadata *UnstakeMetadata, + extraData map[string][]byte, + minFeeRateNanosPerKB uint64, + mempool *DeSoMempool, + additionalOutputs []*DeSoOutput, +) ( + _txn *MsgDeSoTxn, + _totalInput uint64, + _changeAmount uint64, + _fees uint64, + _err error, +) { + // Create a txn containing the metadata fields. + txn := &MsgDeSoTxn{ + PublicKey: transactorPublicKey, + TxnMeta: metadata, + TxOutputs: additionalOutputs, + ExtraData: extraData, + // We wait to compute the signature until + // we've added all the inputs and change. + } + + // Create a new UtxoView. If we have access to a mempool object, use + // it to get an augmented view that factors in pending transactions. + utxoView, err := NewUtxoView(bc.db, bc.params, bc.postgres, bc.snapshot) + if err != nil { + return nil, 0, 0, 0, errors.Wrap( + err, "Blockchain.CreateUnstakeTxn: problem creating new utxo view: ", + ) + } + if mempool != nil { + utxoView, err = mempool.GetAugmentedUniversalView() + if err != nil { + return nil, 0, 0, 0, errors.Wrapf( + err, "Blockchain.CreateUnstakeTxn: problem getting augmented utxo view from mempool: ", + ) + } + } + + // Validate txn metadata. + if err = utxoView.IsValidUnstakeMetadata(transactorPublicKey, metadata); err != nil { + return nil, 0, 0, 0, errors.Wrapf( + err, "Blockchain.CreateUnstakeTxn: invalid txn metadata: ", + ) + } + + // We don't need to make any tweaks to the amount because + // it's basically a standard "pay per kilobyte" transaction. + totalInput, spendAmount, changeAmount, fees, err := bc.AddInputsAndChangeToTransaction( + txn, minFeeRateNanosPerKB, mempool, + ) + if err != nil { + return nil, 0, 0, 0, errors.Wrapf( + err, "Blockchain.CreateUnstakeTxn: problem adding inputs: ", + ) + } + + // Validate that the transaction has at least one input, even if it all goes + // to change. This ensures that the transaction will not be "replayable." + if len(txn.TxInputs) == 0 && bc.blockTip().Height+1 < bc.params.ForkHeights.BalanceModelBlockHeight { + return nil, 0, 0, 0, errors.New( + "Blockchain.CreateUnstakeTxn: txn has zero inputs, try increasing the fee rate", + ) + } + + // Sanity-check that the spendAmount is zero. + if spendAmount != 0 { + return nil, 0, 0, 0, fmt.Errorf( + "Blockchain.CreateUnstakeTxn: spend amount is non-zero: %d", spendAmount, + ) + } + return txn, totalInput, changeAmount, fees, nil +} + +func (bc *Blockchain) CreateUnlockStakeTxn( + transactorPublicKey []byte, + metadata *UnlockStakeMetadata, + extraData map[string][]byte, + minFeeRateNanosPerKB uint64, + mempool *DeSoMempool, + additionalOutputs []*DeSoOutput, +) ( + _txn *MsgDeSoTxn, + _totalInput uint64, + _changeAmount uint64, + _fees uint64, + _err error, +) { + // Create a txn containing the metadata fields. + txn := &MsgDeSoTxn{ + PublicKey: transactorPublicKey, + TxnMeta: metadata, + TxOutputs: additionalOutputs, + ExtraData: extraData, + // We wait to compute the signature until + // we've added all the inputs and change. + } + + // Create a new UtxoView. If we have access to a mempool object, use + // it to get an augmented view that factors in pending transactions. + utxoView, err := NewUtxoView(bc.db, bc.params, bc.postgres, bc.snapshot) + if err != nil { + return nil, 0, 0, 0, errors.Wrap( + err, "Blockchain.CreateUnlockStakeTxn: problem creating new utxo view: ", + ) + } + if mempool != nil { + utxoView, err = mempool.GetAugmentedUniversalView() + if err != nil { + return nil, 0, 0, 0, errors.Wrapf( + err, "Blockchain.CreateUnlockStakeTxn: problem getting augmented utxo view from mempool: ", + ) + } + } + + // Validate txn metadata. + if err = utxoView.IsValidUnlockStakeMetadata(transactorPublicKey, metadata); err != nil { + return nil, 0, 0, 0, errors.Wrapf( + err, "Blockchain.CreateUnlockStakeTxn: invalid txn metadata: ", + ) + } + + // We don't need to make any tweaks to the amount because + // it's basically a standard "pay per kilobyte" transaction. + totalInput, spendAmount, changeAmount, fees, err := bc.AddInputsAndChangeToTransaction( + txn, minFeeRateNanosPerKB, mempool, + ) + if err != nil { + return nil, 0, 0, 0, errors.Wrapf( + err, "Blockchain.CreateUnlockStakeTxn: problem adding inputs: ", + ) + } + + // Validate that the transaction has at least one input, even if it all goes + // to change. This ensures that the transaction will not be "replayable." + if len(txn.TxInputs) == 0 && bc.blockTip().Height+1 < bc.params.ForkHeights.BalanceModelBlockHeight { + return nil, 0, 0, 0, errors.New( + "Blockchain.CreateUnlockStakeTxn: txn has zero inputs, try increasing the fee rate", + ) + } + + // Sanity-check that the spendAmount is zero. + if spendAmount != 0 { + return nil, 0, 0, 0, fmt.Errorf( + "Blockchain.CreateUnlockStakeTxn: spend amount is non-zero: %d", spendAmount, + ) + } + return txn, totalInput, changeAmount, fees, nil +} + +// +// UTXO VIEW UTILS +// + +func (bav *UtxoView) _connectStake( + txn *MsgDeSoTxn, + txHash *BlockHash, + blockHeight uint32, + verifySignatures bool, +) ( + _totalInput uint64, + _totalOutput uint64, + _utxoOps []*UtxoOperation, + _err error, +) { + // Validate the starting block height. + if blockHeight < bav.Params.ForkHeights.ProofOfStakeNewTxnTypesBlockHeight || + blockHeight < bav.Params.ForkHeights.BalanceModelBlockHeight { + return 0, 0, nil, errors.Wrapf(RuleErrorProofofStakeTxnBeforeBlockHeight, "_connectStake: ") + } + + // Validate the txn TxnType. + if txn.TxnMeta.GetTxnType() != TxnTypeStake { + return 0, 0, nil, fmt.Errorf( + "_connectStake: called with bad TxnType %s", txn.TxnMeta.GetTxnType().String(), + ) + } + + // Grab the txn metadata. + txMeta := txn.TxnMeta.(*StakeMetadata) + + // Validate the txn metadata. + if err := bav.IsValidStakeMetadata(txn.PublicKey, txMeta, blockHeight); err != nil { + return 0, 0, nil, errors.Wrapf(err, "_connectStake: ") + } + + // Convert TransactorPublicKey to TransactorPKID. + transactorPKIDEntry := bav.GetPKIDForPublicKey(txn.PublicKey) + if transactorPKIDEntry == nil || transactorPKIDEntry.isDeleted { + return 0, 0, nil, errors.Wrapf(RuleErrorInvalidStakerPKID, "_connectStake: ") + } + + // Retrieve the existing ValidatorEntry. It must exist. The PrevValidatorEntry + // will be restored if we disconnect this transaction. + prevValidatorEntry, err := bav.GetValidatorByPublicKey(txMeta.ValidatorPublicKey) + if err != nil { + return 0, 0, nil, errors.Wrapf(err, "_connectStake: ") + } + if prevValidatorEntry == nil || prevValidatorEntry.isDeleted || prevValidatorEntry.DisableDelegatedStake { + return 0, 0, nil, errors.Wrapf(RuleErrorInvalidValidatorPKID, "_connectStake: ") + } + + // Convert StakeAmountNanos *uint256.Int to StakeAmountNanosUint64 uint64. + if txMeta.StakeAmountNanos == nil || !txMeta.StakeAmountNanos.IsUint64() { + return 0, 0, nil, errors.Wrapf(RuleErrorInvalidStakeAmountNanos, "_connectStake: ") + } + stakeAmountNanosUint64 := txMeta.StakeAmountNanos.Uint64() + + // Connect a BasicTransfer to get the total input and the + // total output without considering the txn metadata. This + // BasicTransfer also includes the extra spend associated + // with the amount the transactor is staking. + totalInput, totalOutput, utxoOpsForTxn, err := bav._connectBasicTransferWithExtraSpend( + txn, txHash, blockHeight, stakeAmountNanosUint64, verifySignatures, + ) + if err != nil { + return 0, 0, nil, errors.Wrapf(err, "_connectStake: ") + } + if verifySignatures { + // _connectBasicTransfer has already checked that the txn is signed + // by the top-level public key, which we take to be the sender's + // public key so there is no need to verify anything further. + } + + // Check if there is an existing StakeEntry that will be updated. + // The existing StakeEntry will be restored if we disconnect this transaction. + prevStakeEntry, err := bav.GetStakeEntry(prevValidatorEntry.ValidatorPKID, transactorPKIDEntry.PKID) + if err != nil { + return 0, 0, nil, errors.Wrapf(err, "_connectStake: ") + } + // Delete the existing StakeEntry, if exists. + if prevStakeEntry != nil { + bav._deleteStakeEntryMappings(prevStakeEntry) + } + + // Set StakeID only if this is a new StakeEntry. + stakeID := txHash + if prevStakeEntry != nil { + stakeID = prevStakeEntry.StakeID + } + + // Calculate StakeAmountNanos. + stakeAmountNanos := txMeta.StakeAmountNanos.Clone() + if prevStakeEntry != nil { + stakeAmountNanos, err = SafeUint256().Add(stakeAmountNanos, prevStakeEntry.StakeAmountNanos) + if err != nil { + return 0, 0, nil, errors.Wrapf(err, "_connectStake: error adding StakeAmountNanos to existing StakeAmountNanos: ") + } + } + + // Retrieve existing ExtraData to merge with any new ExtraData. + var prevExtraData map[string][]byte + if prevStakeEntry != nil { + prevExtraData = prevStakeEntry.ExtraData + } + + // Construct new StakeEntry from metadata. + currentStakeEntry := &StakeEntry{ + StakeID: stakeID, + StakerPKID: transactorPKIDEntry.PKID, + ValidatorPKID: prevValidatorEntry.ValidatorPKID, + StakeAmountNanos: stakeAmountNanos, + ExtraData: mergeExtraData(prevExtraData, txn.ExtraData), + } + // Set the new StakeEntry. + bav._setStakeEntryMappings(currentStakeEntry) + + // Update the ValidatorEntry.TotalStakeAmountNanos. + // 1. Copy the existing ValidatorEntry. + currentValidatorEntry := prevValidatorEntry.Copy() + // 2. Delete the existing ValidatorEntry. + bav._deleteValidatorEntryMappings(prevValidatorEntry) + // 3. Update the new ValidatorEntry's TotalStakeAmountNanos. + currentValidatorEntry.TotalStakeAmountNanos, err = SafeUint256().Add( + currentValidatorEntry.TotalStakeAmountNanos, txMeta.StakeAmountNanos, + ) + if err != nil { + return 0, 0, nil, errors.Wrapf(err, "_connectStake: error adding StakeAmountNanos to TotalStakeAmountNanos: ") + } + // 4. Set the new ValidatorEntry. + bav._setValidatorEntryMappings(currentValidatorEntry) + + // Increase the GlobalStakeAmountNanos. + // Retrieve the existing GlobalStakeAmountNanos. + // The PrevGlobalStakeAmountNanos will be restored if we disconnect this transaction. + prevGlobalStakeAmountNanos, err := bav.GetGlobalStakeAmountNanos() + if err != nil { + return 0, 0, nil, errors.Wrapf(err, "_connectStake: error retrieving GlobalStakeAmountNanos: ") + } + globalStakeAmountNanos, err := SafeUint256().Add(prevGlobalStakeAmountNanos, txMeta.StakeAmountNanos) + if err != nil { + return 0, 0, nil, errors.Wrapf(err, "_connectStake: error adding StakeAmountNanos to GlobalStakeAmountNanos: ") + } + // Set the new GlobalStakeAmountNanos. + bav._setGlobalStakeAmountNanos(globalStakeAmountNanos) + + // Add the StakeAmountNanos to TotalOutput. The coins being staked are already + // part of the TotalInput. But they are not burned, so they are an implicit + // output even though they do not go to a specific public key's balance. + totalOutput, err = SafeUint64().Add(totalOutput, stakeAmountNanosUint64) + if err != nil { + return 0, 0, nil, errors.Wrapf(err, "_connectStake: error adding StakeAmountNanos to TotalOutput: ") + } + + // Add a UTXO operation + utxoOpsForTxn = append(utxoOpsForTxn, &UtxoOperation{ + Type: OperationTypeStake, + PrevValidatorEntry: prevValidatorEntry, + PrevGlobalStakeAmountNanos: prevGlobalStakeAmountNanos, + PrevStakeEntries: []*StakeEntry{prevStakeEntry}, + }) + return totalInput, totalOutput, utxoOpsForTxn, nil +} + +func (bav *UtxoView) _disconnectStake( + operationType OperationType, + currentTxn *MsgDeSoTxn, + txHash *BlockHash, + utxoOpsForTxn []*UtxoOperation, + blockHeight uint32, +) error { + // Validate the starting block height. + if blockHeight < bav.Params.ForkHeights.ProofOfStakeNewTxnTypesBlockHeight || + blockHeight < bav.Params.ForkHeights.BalanceModelBlockHeight { + return errors.Wrapf(RuleErrorProofofStakeTxnBeforeBlockHeight, "_disconnectStake: ") + } + + // Validate the last operation is a Stake operation. + if len(utxoOpsForTxn) == 0 { + return fmt.Errorf("_disconnectStake: utxoOperations are missing") + } + operationIndex := len(utxoOpsForTxn) - 1 + operationData := utxoOpsForTxn[operationIndex] + if operationData.Type != OperationTypeStake { + return fmt.Errorf( + "_disconnectStake: trying to revert %v but found %v", + OperationTypeStake, + operationData.Type, + ) + } + txMeta := currentTxn.TxnMeta.(*StakeMetadata) + + // Convert TransactorPublicKey to TransactorPKID. + transactorPKIDEntry := bav.GetPKIDForPublicKey(currentTxn.PublicKey) + if transactorPKIDEntry == nil || transactorPKIDEntry.isDeleted { + return errors.Wrapf(RuleErrorInvalidStakerPKID, "_disconnectStake: ") + } + + // Restore the PrevValidatorEntry. + prevValidatorEntry := operationData.PrevValidatorEntry + if prevValidatorEntry == nil { + return fmt.Errorf( + "_disconnectStake: no prev ValidatorEntry found for %v", txMeta.ValidatorPublicKey, + ) + } + // 1. Delete the CurrentValidatorEntry. + currentValidatorEntry, err := bav.GetValidatorByPKID(prevValidatorEntry.ValidatorPKID) + if err != nil { + return errors.Wrapf(err, "_disconnectStake: ") + } + if currentValidatorEntry == nil { + return fmt.Errorf( + "_disconnectStake: no current ValidatorEntry found for %v", txMeta.ValidatorPublicKey, + ) + } + bav._deleteValidatorEntryMappings(currentValidatorEntry) + // 2. Set the PrevValidatorEntry. + bav._setValidatorEntryMappings(prevValidatorEntry) + + // Restore the PrevStakeEntry. + // 1. Delete the CurrentStakeEntry. + currentStakeEntry, err := bav.GetStakeEntry(prevValidatorEntry.ValidatorPKID, transactorPKIDEntry.PKID) + if err != nil { + return errors.Wrapf(err, "_disconnectStake: ") + } + if currentStakeEntry == nil { + return fmt.Errorf("_disconnectStake: no current StakeEntry found for %v", currentTxn.PublicKey) + } + bav._deleteStakeEntryMappings(currentStakeEntry) + // 2. Set the PrevStakeEntry, if exists. The PrevStakeEntry will exist if the transactor + // was adding stake to an existing StakeEntry. It will not exist if this is the first + // stake the transactor has staked with this validator. + if len(operationData.PrevStakeEntries) > 1 { + return fmt.Errorf("_disconnectStake: more than one prev StakeEntry found for %v", currentTxn.PublicKey) + } else if len(operationData.PrevStakeEntries) == 1 { + bav._setStakeEntryMappings(operationData.PrevStakeEntries[0]) + } + + // Restore the PrevGlobalStakeAmountNanos. + bav._setGlobalStakeAmountNanos(operationData.PrevGlobalStakeAmountNanos) + + // Disconnect the BasicTransfer. Disconnecting the BasicTransfer also returns + // the extra spend associated with the amount the transactor staked. + return bav._disconnectBasicTransfer( + currentTxn, txHash, utxoOpsForTxn[:operationIndex], blockHeight, + ) +} + +func (bav *UtxoView) _connectUnstake( + txn *MsgDeSoTxn, + txHash *BlockHash, + blockHeight uint32, + verifySignatures bool, +) ( + _totalInput uint64, + _totalOutput uint64, + _utxoOps []*UtxoOperation, + _err error, +) { + // Validate the starting block height. + if blockHeight < bav.Params.ForkHeights.ProofOfStakeNewTxnTypesBlockHeight || + blockHeight < bav.Params.ForkHeights.BalanceModelBlockHeight { + return 0, 0, nil, errors.Wrapf(RuleErrorProofofStakeTxnBeforeBlockHeight, "_connectUnstake: ") + } + + // Validate the txn TxnType. + if txn.TxnMeta.GetTxnType() != TxnTypeUnstake { + return 0, 0, nil, fmt.Errorf( + "_connectUnstake: called with bad TxnType %s", txn.TxnMeta.GetTxnType().String(), + ) + } + + // Connect a basic transfer to get the total input and the + // total output without considering the txn metadata. + totalInput, totalOutput, utxoOpsForTxn, err := bav._connectBasicTransfer( + txn, txHash, blockHeight, verifySignatures, + ) + if err != nil { + return 0, 0, nil, errors.Wrapf(err, "_connectUnstake: ") + } + if verifySignatures { + // _connectBasicTransfer has already checked that the txn is signed + // by the top-level public key, which we take to be the sender's + // public key so there is no need to verify anything further. + } + + // Grab the txn metadata. + txMeta := txn.TxnMeta.(*UnstakeMetadata) + + // Validate the txn metadata. + if err = bav.IsValidUnstakeMetadata(txn.PublicKey, txMeta); err != nil { + return 0, 0, nil, errors.Wrapf(err, "_connectUnstake: ") + } + + // Convert TransactorPublicKey to TransactorPKID. + transactorPKIDEntry := bav.GetPKIDForPublicKey(txn.PublicKey) + if transactorPKIDEntry == nil || transactorPKIDEntry.isDeleted { + return 0, 0, nil, errors.Wrapf(RuleErrorInvalidStakerPKID, "_connectUnstake: ") + } + + // Retrieve PrevValidatorEntry. This will be restored if we disconnect the txn. + prevValidatorEntry, err := bav.GetValidatorByPublicKey(txMeta.ValidatorPublicKey) + if err != nil { + return 0, 0, nil, errors.Wrapf(err, "_connectUnstake: ") + } + if prevValidatorEntry == nil || prevValidatorEntry.isDeleted { + return 0, 0, nil, errors.Wrapf(RuleErrorInvalidValidatorPKID, "_connectUnstake: ") + } + + // Retrieve PrevStakeEntry. This will be restored if we disconnect the txn. + prevStakeEntry, err := bav.GetStakeEntry(prevValidatorEntry.ValidatorPKID, transactorPKIDEntry.PKID) + if err != nil { + return 0, 0, nil, errors.Wrapf(err, "_connectUnstake: ") + } + if prevStakeEntry == nil || prevStakeEntry.isDeleted { + return 0, 0, nil, errors.Wrapf(RuleErrorInvalidUnstakeNoStakeFound, "_connectUnstake: ") + } + if prevStakeEntry.StakeAmountNanos.Cmp(txMeta.UnstakeAmountNanos) < 0 { + return 0, 0, nil, errors.Wrapf(RuleErrorInvalidUnstakeInsufficientStakeFound, "_connectUnstake: ") + } + + // Update the StakeEntry, decreasing the StakeAmountNanos. + // 1. Calculate the updated StakeAmountNanos. + stakeAmountNanos, err := SafeUint256().Sub(prevStakeEntry.StakeAmountNanos, txMeta.UnstakeAmountNanos) + if err != nil { + return 0, 0, nil, errors.Wrapf(err, "_connectUnstake: error subtracting UnstakeAmountNanos from StakeAmountNanos: ") + } + // 2. Create a CurrentStakeEntry, if updated StakeAmountNanos > 0. + var currentStakeEntry *StakeEntry + if stakeAmountNanos.Cmp(uint256.NewInt()) > 0 { + currentStakeEntry = prevStakeEntry.Copy() + currentStakeEntry.StakeAmountNanos = stakeAmountNanos.Clone() + } + // 3. Delete the PrevStakeEntry. + bav._deleteStakeEntryMappings(prevStakeEntry) + // 4. Set the CurrentStakeEntry, if exists. The CurrentStakeEntry will not exist + // if the transactor has unstaked all stake assigned to this validator. + if currentStakeEntry != nil { + bav._setStakeEntryMappings(currentStakeEntry) + } + + // Update the ValidatorEntry.TotalStakeAmountNanos. + // 1. Copy the existing ValidatorEntry. + currentValidatorEntry := prevValidatorEntry.Copy() + // 2. Delete the existing ValidatorEntry. + bav._deleteValidatorEntryMappings(prevValidatorEntry) + // 3. Update the new ValidatorEntry's TotalStakeAmountNanos. + currentValidatorEntry.TotalStakeAmountNanos, err = SafeUint256().Sub( + currentValidatorEntry.TotalStakeAmountNanos, txMeta.UnstakeAmountNanos, + ) + if err != nil { + return 0, 0, nil, errors.Wrapf(err, "_connectUnstake: error subtracting UnstakeAmountNanos from TotalStakeAmountNanos: ") + } + // 4. Set the new ValidatorEntry. + bav._setValidatorEntryMappings(currentValidatorEntry) + + // Decrease the GlobalStakeAmountNanos. + // 1. Retrieve the existing GlobalStakeAmountNanos. This will be restored if we disconnect this txn. + prevGlobalStakeAmountNanos, err := bav.GetGlobalStakeAmountNanos() + if err != nil { + return 0, 0, nil, errors.Wrapf(err, "_connectUnstake: error retrieving GlobalStakeAmountNanos: ") + } + globalStakeAmountNanos, err := SafeUint256().Sub(prevGlobalStakeAmountNanos, txMeta.UnstakeAmountNanos) + if err != nil { + return 0, 0, nil, errors.Wrapf(err, "_connectUnstake: error subtracting UnstakeAmountNanos from GlobalStakeAmountNanos: ") + } + // 2. Set the new GlobalStakeAmountNanos. + bav._setGlobalStakeAmountNanos(globalStakeAmountNanos) + + // Update the LockedStakeEntry, if exists. Create if not. + currentEpochNumber := uint64(0) // TODO: set this + // 1. Retrieve the PrevLockedStakeEntry. This will be restored if we disconnect this txn. + prevLockedStakeEntry, err := bav.GetLockedStakeEntry( + prevValidatorEntry.ValidatorPKID, transactorPKIDEntry.PKID, currentEpochNumber, + ) + if err != nil { + return 0, 0, nil, errors.Wrapf(err, "_connectUnstake: ") + } + // 2. Create a CurrrentLockedStakeEntry. + var currentLockedStakeEntry *LockedStakeEntry + if prevLockedStakeEntry != nil { + // Update the existing LockedStakeEntry. + currentLockedStakeEntry = prevLockedStakeEntry.Copy() + currentLockedStakeEntry.LockedAmountNanos, err = SafeUint256().Add( + prevLockedStakeEntry.LockedAmountNanos, txMeta.UnstakeAmountNanos, + ) + if err != nil { + return 0, 0, nil, errors.Wrapf(err, "_connectUnstake: error adding UnstakeAmountNanos to LockedAmountNanos") + } + currentLockedStakeEntry.ExtraData = mergeExtraData(prevLockedStakeEntry.ExtraData, txn.ExtraData) + } else { + // Create a new LockedStakeEntry. + currentLockedStakeEntry = &LockedStakeEntry{ + LockedStakeID: txn.Hash(), + StakerPKID: transactorPKIDEntry.PKID, + ValidatorPKID: prevValidatorEntry.ValidatorPKID, + LockedAmountNanos: txMeta.UnstakeAmountNanos, + LockedAtEpochNumber: currentEpochNumber, + ExtraData: txn.ExtraData, + } + } + // 3. Delete the PrevLockedStakeEntry, if exists. + if prevLockedStakeEntry != nil { + bav._deleteLockedStakeEntryMappings(prevLockedStakeEntry) + } + // 4. Set the CurrentLockedStakeEntry. + bav._setLockedStakeEntryMappings(currentLockedStakeEntry) + + // Add a UTXO operation + utxoOpsForTxn = append(utxoOpsForTxn, &UtxoOperation{ + Type: OperationTypeUnstake, + PrevValidatorEntry: prevValidatorEntry, + PrevGlobalStakeAmountNanos: prevGlobalStakeAmountNanos, + PrevStakeEntries: []*StakeEntry{prevStakeEntry}, + PrevLockedStakeEntries: []*LockedStakeEntry{prevLockedStakeEntry}, + }) + return totalInput, totalOutput, utxoOpsForTxn, nil +} + +func (bav *UtxoView) _disconnectUnstake( + operationType OperationType, + currentTxn *MsgDeSoTxn, + txHash *BlockHash, + utxoOpsForTxn []*UtxoOperation, + blockHeight uint32, +) error { + // Validate the starting block height. + if blockHeight < bav.Params.ForkHeights.ProofOfStakeNewTxnTypesBlockHeight || + blockHeight < bav.Params.ForkHeights.BalanceModelBlockHeight { + return errors.Wrapf(RuleErrorProofofStakeTxnBeforeBlockHeight, "_disconnectUnstake: ") + } + + // Validate the last operation is an Unstake operation. + if len(utxoOpsForTxn) == 0 { + return fmt.Errorf("_disconnectUnstake: utxoOperations are missing") + } + operationIndex := len(utxoOpsForTxn) - 1 + operationData := utxoOpsForTxn[operationIndex] + if operationData.Type != OperationTypeUnstake { + return fmt.Errorf( + "_disconnectUnstake: trying to revert %v but found %v", + OperationTypeUnstake, + operationData.Type, + ) + } + txMeta := currentTxn.TxnMeta.(*UnstakeMetadata) + + // Convert TransactorPublicKey to TransactorPKID. + transactorPKIDEntry := bav.GetPKIDForPublicKey(currentTxn.PublicKey) + if transactorPKIDEntry == nil || transactorPKIDEntry.isDeleted { + return errors.Wrapf(RuleErrorInvalidStakerPKID, "_disconnectUnstake: ") + } + + // Restore the PrevValidatorEntry. + prevValidatorEntry := operationData.PrevValidatorEntry + if prevValidatorEntry == nil { + return fmt.Errorf( + "_disconnectUnstake: no prev ValidatorEntry found for %v", txMeta.ValidatorPublicKey, + ) + } + // 1. Delete the CurrentValidatorEntry. + currentValidatorEntry, err := bav.GetValidatorByPKID(prevValidatorEntry.ValidatorPKID) + if err != nil { + return errors.Wrapf(err, "_disconnectUnstake: ") + } + if currentValidatorEntry == nil { + return fmt.Errorf( + "_disconnectUnstake: no current ValidatorEntry found for %v", txMeta.ValidatorPublicKey, + ) + } + bav._deleteValidatorEntryMappings(currentValidatorEntry) + // 2. Set the PrevValidatorEntry. + bav._setValidatorEntryMappings(prevValidatorEntry) + + // Restore the PrevStakeEntry. + // 1. Delete the CurrentStakeEntry, if exists. The CurrentStakeEntry will exist if the transactor + // still has stake assigned to this validator. The CurrentStakeEntry will not exist if the + // transactor unstaked all stake. + currentStakeEntry, err := bav.GetStakeEntry(prevValidatorEntry.ValidatorPKID, transactorPKIDEntry.PKID) + if err != nil { + return errors.Wrapf(err, "_disconnectUnstake: ") + } + if currentStakeEntry != nil { + bav._deleteStakeEntryMappings(currentStakeEntry) + } + // 2. Set the PrevStakeEntry. + if len(operationData.PrevStakeEntries) < 1 { + return fmt.Errorf("_disconnectUnstake: no prev StakeEntry found for %v", currentTxn.PublicKey) + } + if len(operationData.PrevStakeEntries) > 1 { + return fmt.Errorf("_disconnectUnstake: more than one prev StakeEntry found for %v", currentTxn.PublicKey) + } + bav._setStakeEntryMappings(operationData.PrevStakeEntries[0]) + + // Restore the PrevGlobalStakeAmountNanos. + bav._setGlobalStakeAmountNanos(operationData.PrevGlobalStakeAmountNanos) + + // Restore the PrevLockedStakeEntry, if exists. The PrevLockedStakeEntry will exist if the + // transactor has previously unstaked stake assigned to this validator within the same epoch. + // The PrevLockedStakeEntry will not exist otherwise. + currentEpochNumber := uint64(0) // TODO: set this + // 1. Retrieve the CurrentLockedStakeEntry. + currentLockedStakeEntry, err := bav.GetLockedStakeEntry( + prevValidatorEntry.ValidatorPKID, transactorPKIDEntry.PKID, currentEpochNumber, + ) + if err != nil { + return errors.Wrapf(err, "_disconnectUnstake: ") + } + // 2. Delete the CurrentLockedStakeEntry. + bav._deleteLockedStakeEntryMappings(currentLockedStakeEntry) + // 3. Set the PrevLockedStakeEntry, if exists. + if len(operationData.PrevLockedStakeEntries) > 1 { + return fmt.Errorf("_disconnectUnstake: more than one prev LockedStakeEntry found for %v", currentTxn.PublicKey) + } + if len(operationData.PrevLockedStakeEntries) == 1 { + bav._setLockedStakeEntryMappings(operationData.PrevLockedStakeEntries[0]) + } + + // Disconnect the basic transfer. + return bav._disconnectBasicTransfer( + currentTxn, txHash, utxoOpsForTxn[:operationIndex], blockHeight, + ) +} + +func (bav *UtxoView) _connectUnlockStake( + txn *MsgDeSoTxn, + txHash *BlockHash, + blockHeight uint32, + verifySignatures bool, +) ( + _totalInput uint64, + _totalOutput uint64, + _utxoOps []*UtxoOperation, + _err error, +) { + // Validate the starting block height. + if blockHeight < bav.Params.ForkHeights.ProofOfStakeNewTxnTypesBlockHeight || + blockHeight < bav.Params.ForkHeights.BalanceModelBlockHeight { + return 0, 0, nil, errors.Wrapf(RuleErrorProofofStakeTxnBeforeBlockHeight, "_connectUnlockStake: ") + } + + // Validate the txn TxnType. + if txn.TxnMeta.GetTxnType() != TxnTypeUnlockStake { + return 0, 0, nil, fmt.Errorf( + "_connectUnlockStake: called with bad TxnType %s", txn.TxnMeta.GetTxnType().String(), + ) + } + + // Grab the txn metadata. + txMeta := txn.TxnMeta.(*UnlockStakeMetadata) + + // Validate the txn metadata. + if err := bav.IsValidUnlockStakeMetadata(txn.PublicKey, txMeta); err != nil { + return 0, 0, nil, errors.Wrapf(err, "_connectUnlockStake: ") + } + + // Convert TransactorPublicKey to TransactorPKID. + transactorPKIDEntry := bav.GetPKIDForPublicKey(txn.PublicKey) + if transactorPKIDEntry == nil || transactorPKIDEntry.isDeleted { + return 0, 0, nil, errors.Wrapf(RuleErrorInvalidStakerPKID, "_connectUnlockStake: ") + } + + // Convert ValidatorPublicKey to ValidatorPKID. + validatorPKIDEntry := bav.GetPKIDForPublicKey(txMeta.ValidatorPublicKey.ToBytes()) + if validatorPKIDEntry == nil || validatorPKIDEntry.isDeleted { + return 0, 0, nil, errors.Wrapf(RuleErrorInvalidValidatorPKID, "_connectUnlockStake: ") + } + + // Retrieve the PrevLockedStakeEntries. These will be restored if we disconnect this txn. + prevLockedStakeEntries, err := bav.GetLockedStakeEntriesInRange( + validatorPKIDEntry.PKID, transactorPKIDEntry.PKID, txMeta.StartEpochNumber, txMeta.EndEpochNumber, + ) + if err != nil { + return 0, 0, nil, errors.Wrapf(err, "_connectUnlockStake: ") + } + if len(prevLockedStakeEntries) == 0 { + return 0, 0, nil, errors.Wrapf(RuleErrorInvalidUnlockStakeNoUnlockableStakeFound, "_connectUnlockStake: ") + } + + // Connect a basic transfer to get the total input and the + // total output without considering the txn metadata. + totalInput, totalOutput, utxoOpsForTxn, err := bav._connectBasicTransfer( + txn, txHash, blockHeight, verifySignatures, + ) + if err != nil { + return 0, 0, nil, errors.Wrapf(err, "_connectUnlockStake: ") + } + if verifySignatures { + // _connectBasicTransfer has already checked that the txn is signed + // by the top-level public key, which we take to be the sender's + // public key so there is no need to verify anything further. + } + + // Calculate the TotalUnlockedAmountNanos and delete the PrevLockedStakeEntries. + totalUnlockedAmountNanos := uint256.NewInt() + for _, prevLockedStakeEntry := range prevLockedStakeEntries { + totalUnlockedAmountNanos, err = SafeUint256().Add( + totalUnlockedAmountNanos, prevLockedStakeEntry.LockedAmountNanos, + ) + if err != nil { + return 0, 0, nil, errors.Wrapf(err, "_connectUnlockStake: ") + } + bav._deleteLockedStakeEntryMappings(prevLockedStakeEntry) + } + if !totalUnlockedAmountNanos.IsUint64() { + return 0, 0, nil, errors.Wrapf(RuleErrorInvalidUnlockStakeUnlockableStakeOverflowsUint64, "_connectUnlockStake: ") + } + totalUnlockedAmountNanosUint64 := totalUnlockedAmountNanos.Uint64() + + // Add TotalUnlockedAmountNanos to TotalInput. The unlocked coins are an + // implicit input even though they do not come from a specific public key. + totalInput, err = SafeUint64().Add(totalInput, totalUnlockedAmountNanosUint64) + if err != nil { + return 0, 0, nil, errors.Wrapf(err, "_connectUnlockStake: error adding TotalUnlockedAmountNanos to TotalInput: ") + } + + // Add TotalUnlockedAmountNanos to TotalOutput. The unlocked + // coins being sent to the transactor are an implicit output. + totalOutput, err = SafeUint64().Add(totalOutput, totalUnlockedAmountNanosUint64) + if err != nil { + return 0, 0, nil, errors.Wrapf(err, "_connectUnlockStake: error adding TotalUnlockedAmountNanos to TotalOutput: ") + } + + // Return TotalUnlockedAmountNanos back to the transactor. We can use + // _addBalance here since we validate that connectUnlockStake can only + // occur after the BalanceModelBlockHeight. + utxoOp, err := bav._addBalance(totalUnlockedAmountNanosUint64, txn.PublicKey) + if err != nil { + return 0, 0, nil, errors.Wrapf( + err, "_connectUnlockStake: error adding TotalUnlockedAmountNanos to the transactor balance: ", + ) + } + utxoOpsForTxn = append(utxoOpsForTxn, utxoOp) + + // Add a UTXO operation + utxoOpsForTxn = append(utxoOpsForTxn, &UtxoOperation{ + Type: OperationTypeUnlockStake, + PrevLockedStakeEntries: prevLockedStakeEntries, + }) + return totalInput, totalOutput, utxoOpsForTxn, nil +} + +func (bav *UtxoView) _disconnectUnlockStake( + operationType OperationType, + currentTxn *MsgDeSoTxn, + txHash *BlockHash, + utxoOpsForTxn []*UtxoOperation, + blockHeight uint32, +) error { + // Validate the starting block height. + if blockHeight < bav.Params.ForkHeights.ProofOfStakeNewTxnTypesBlockHeight || + blockHeight < bav.Params.ForkHeights.BalanceModelBlockHeight { + return errors.Wrapf(RuleErrorProofofStakeTxnBeforeBlockHeight, "_disconnectUnlockStake: ") + } + + // Validate the last operation is an UnlockStake operation. + if len(utxoOpsForTxn) == 0 { + return fmt.Errorf("_disconnectUnlockStake: utxoOperations are missing") + } + operationIndex := len(utxoOpsForTxn) - 1 + operationData := utxoOpsForTxn[operationIndex] + if operationData.Type != OperationTypeUnlockStake { + return fmt.Errorf( + "_disconnectUnlockStake: trying to revert %v but found %v", + OperationTypeUnlockStake, + operationData.Type, + ) + } + + // Convert TransactorPublicKey to TransactorPKID. + transactorPKIDEntry := bav.GetPKIDForPublicKey(currentTxn.PublicKey) + if transactorPKIDEntry == nil || transactorPKIDEntry.isDeleted { + return errors.Wrapf(RuleErrorInvalidStakerPKID, "_disconnectUnlockStake: ") + } + + // Calculate the TotalUnlockedAmountNanos. + totalUnlockedAmountNanos := uint256.NewInt() + var err error + for _, prevLockedStakeEntry := range operationData.PrevLockedStakeEntries { + totalUnlockedAmountNanos, err = SafeUint256().Add( + totalUnlockedAmountNanos, prevLockedStakeEntry.LockedAmountNanos, + ) + if err != nil { + return errors.Wrapf(err, "_disconnectUnlockStake: ") + } + } + if !totalUnlockedAmountNanos.IsUint64() { + return errors.Wrapf(RuleErrorInvalidUnlockStakeUnlockableStakeOverflowsUint64, "_disconnectUnlockStake: ") + } + + // Unadd TotalUnlockedAmountNanos from the transactor. + err = bav._unAddBalance(totalUnlockedAmountNanos.Uint64(), currentTxn.PublicKey) + if err != nil { + return errors.Wrapf(err, "_disconnectUnlockStake: error unadding TotalUnlockedAmountNanos from the transactor balance: ") + } + + // Restore the PrevLockedStakeEntries. + for _, prevLockedStakeEntry := range operationData.PrevLockedStakeEntries { + bav._setLockedStakeEntryMappings(prevLockedStakeEntry) + } + + // Disconnect the basic transfer. + return bav._disconnectBasicTransfer( + currentTxn, txHash, utxoOpsForTxn[:operationIndex], blockHeight, + ) +} + +func (bav *UtxoView) IsValidStakeMetadata(transactorPkBytes []byte, metadata *StakeMetadata, blockHeight uint32) error { + // Validate TransactorPublicKey. + transactorPKIDEntry := bav.GetPKIDForPublicKey(transactorPkBytes) + if transactorPKIDEntry == nil || transactorPKIDEntry.isDeleted { + return errors.Wrapf(RuleErrorInvalidStakerPKID, "UtxoView.IsValidStakeMetadata: ") + } + + // Validate ValidatorPublicKey. + validatorEntry, err := bav.GetValidatorByPublicKey(metadata.ValidatorPublicKey) + if err != nil { + return errors.Wrapf(err, "UtxoView.IsValidStakeMetadata: ") + } + if validatorEntry == nil || validatorEntry.isDeleted || validatorEntry.DisableDelegatedStake { + return errors.Wrapf(RuleErrorInvalidValidatorPKID, "UtxoView.IsValidStakeMetadata: ") + } + + // Validate 0 < StakeAmountNanos <= transactor's DESO Balance. We ignore + // the txn fees in this check. The StakeAmountNanos will be validated to + // be less than the transactor's DESO balance net of txn fees in the call + // to connectBasicTransferWithExtraSpend. + if metadata.StakeAmountNanos == nil || + metadata.StakeAmountNanos.IsZero() || + !metadata.StakeAmountNanos.IsUint64() { + return errors.Wrapf(RuleErrorInvalidStakeAmountNanos, "UtxoView.IsValidStakeMetadata: ") + } + transactorDeSoBalanceNanos, err := bav.GetSpendableDeSoBalanceNanosForPublicKey(transactorPkBytes, blockHeight-1) + if err != nil { + return errors.Wrapf(err, "UtxoView.IsValidStakeMetadata: ") + } + if uint256.NewInt().SetUint64(transactorDeSoBalanceNanos).Cmp(metadata.StakeAmountNanos) < 0 { + return errors.Wrapf(RuleErrorInvalidStakeInsufficientBalance, "UtxoView.IsValidStakeMetadata: ") + } + + return nil +} + +func (bav *UtxoView) IsValidUnstakeMetadata(transactorPkBytes []byte, metadata *UnstakeMetadata) error { + // Validate TransactorPublicKey. + transactorPKIDEntry := bav.GetPKIDForPublicKey(transactorPkBytes) + if transactorPKIDEntry == nil || transactorPKIDEntry.isDeleted { + return errors.Wrapf(RuleErrorInvalidStakerPKID, "UtxoView.IsValidUnstakeMetadata: ") + } + + // Validate ValidatorPublicKey. + validatorEntry, err := bav.GetValidatorByPublicKey(metadata.ValidatorPublicKey) + if err != nil { + return errors.Wrapf(err, "IsValidUnstakeMetadata: ") + } + if validatorEntry == nil || validatorEntry.isDeleted { + return errors.Wrapf(RuleErrorInvalidValidatorPKID, "UtxoView.IsValidUnstakeMetadata: ") + } + + // Validate StakeEntry exists. + stakeEntry, err := bav.GetStakeEntry(validatorEntry.ValidatorPKID, transactorPKIDEntry.PKID) + if err != nil { + return errors.Wrapf(err, "UtxoView.IsValidUnstakeMetadata: ") + } + if stakeEntry == nil || stakeEntry.isDeleted { + return errors.Wrapf(RuleErrorInvalidUnstakeNoStakeFound, "UtxoView.IsValidUnstakeMetadata: ") + } + + // Validate 0 < UnstakeAmountNanos <= StakeEntry.StakeAmountNanos. + if metadata.UnstakeAmountNanos == nil || metadata.UnstakeAmountNanos.IsZero() { + return errors.Wrapf(RuleErrorInvalidUnstakeAmountNanos, "UtxoView.IsValidUnstakeMetadata: ") + } + if stakeEntry.StakeAmountNanos.Cmp(metadata.UnstakeAmountNanos) < 0 { + return errors.Wrapf(RuleErrorInvalidUnstakeInsufficientStakeFound, "UtxoView.IsValidUnstakeMetadata: ") + } + + return nil +} + +func (bav *UtxoView) IsValidUnlockStakeMetadata(transactorPkBytes []byte, metadata *UnlockStakeMetadata) error { + // Validate TransactorPublicKey. + transactorPKIDEntry := bav.GetPKIDForPublicKey(transactorPkBytes) + if transactorPKIDEntry == nil || transactorPKIDEntry.isDeleted { + return errors.Wrapf(RuleErrorInvalidStakerPKID, "UtxoView.IsValidUnlockStakeMetadata: ") + } + + // Validate ValidatorPublicKey. + validatorEntry, err := bav.GetValidatorByPublicKey(metadata.ValidatorPublicKey) + if err != nil { + return errors.Wrapf(err, "UtxoView.IsValidUnlockStakeMetadata: ") + } + if validatorEntry == nil || validatorEntry.isDeleted { + return errors.Wrapf(RuleErrorInvalidValidatorPKID, "UtxoView.IsValidUnlockStakeMetadata: ") + } + + // Validate StartEpochNumber and EndEpochNumber. + if metadata.StartEpochNumber > metadata.EndEpochNumber { + return errors.Wrapf(RuleErrorInvalidUnlockStakeEpochRange, "UtxoView.IsValidUnlockStakeMetadata: ") + } + // TODO: validate EndEpochNumber is <= CurrentEpochNumber - 2 + + // Validate LockedStakeEntries exist. + lockedStakeEntries, err := bav.GetLockedStakeEntriesInRange( + validatorEntry.ValidatorPKID, transactorPKIDEntry.PKID, metadata.StartEpochNumber, metadata.EndEpochNumber, + ) + existsLockedStakeEntries := false + for _, lockedStakeEntry := range lockedStakeEntries { + if lockedStakeEntry != nil && !lockedStakeEntry.isDeleted { + existsLockedStakeEntries = true + break + } + } + if !existsLockedStakeEntries { + return errors.Wrapf(RuleErrorInvalidUnlockStakeNoUnlockableStakeFound, "UtxoView.IsValidUnlockStakeMetadata: ") + } + + return nil +} + +func (bav *UtxoView) GetStakeEntry(validatorPKID *PKID, stakerPKID *PKID) (*StakeEntry, error) { + // Error if either input is nil. + if validatorPKID == nil { + return nil, errors.New("UtxoView.GetStakeEntry: nil ValidatorPKID provided as input") + } + if stakerPKID == nil { + return nil, errors.New("UtxoView.GetStakeEntry: nil StakerPKID provided as input") + } + // First, check the UtxoView. + stakeMapKey := StakeMapKey{ValidatorPKID: *validatorPKID, StakerPKID: *stakerPKID} + if stakeEntry, exists := bav.StakeMapKeyToStakeEntry[stakeMapKey]; exists { + // If StakeEntry.isDeleted, return nil. + if stakeEntry.isDeleted { + return nil, nil + } + return stakeEntry, nil + } + // Then, check the database. + stakeEntry, err := DBGetStakeEntry(bav.Handle, bav.Snapshot, validatorPKID, stakerPKID) + if err != nil { + return nil, errors.Wrapf(err, "UtxoView.GetStakeEntry: ") + } + if stakeEntry != nil { + // Cache the StakeEntry in the UtxoView if exists. + bav._setStakeEntryMappings(stakeEntry) + } + return stakeEntry, nil +} + +func (bav *UtxoView) GetLockedStakeEntry( + validatorPKID *PKID, + stakerPKID *PKID, + lockedAtEpochNumber uint64, +) (*LockedStakeEntry, error) { + // Error if either input is nil. + if validatorPKID == nil { + return nil, errors.New("UtxoView.GetLockedStakeEntry: nil ValidatorPKID provided as input") + } + if stakerPKID == nil { + return nil, errors.New("UtxoView.GetLockedStakeEntry: nil StakerPKID provided as input") + } + // First, check the UtxoView. + lockedStakeMapKey := LockedStakeMapKey{ + ValidatorPKID: *validatorPKID, + StakerPKID: *stakerPKID, + LockedAtEpochNumber: lockedAtEpochNumber, + } + if lockedStakeEntry, exists := bav.LockedStakeMapKeyToLockedStakeEntry[lockedStakeMapKey]; exists { + // If LockedStakeEntry.isDeleted, return nil. + if lockedStakeEntry.isDeleted { + return nil, nil + } + return lockedStakeEntry, nil + } + // Then, check the database. + lockedStakeEntry, err := DBGetLockedStakeEntry(bav.Handle, bav.Snapshot, validatorPKID, stakerPKID, lockedAtEpochNumber) + if err != nil { + return nil, errors.Wrapf(err, "UtxoView.GetLockedStakeEntry: ") + } + if lockedStakeEntry != nil { + // Cache the LockedStakeEntry in the UtxoView if exists. + bav._setLockedStakeEntryMappings(lockedStakeEntry) + } + return lockedStakeEntry, nil +} + +func (bav *UtxoView) GetLockedStakeEntriesInRange( + validatorPKID *PKID, + stakerPKID *PKID, + startEpochNumber uint64, + endEpochNumber uint64, +) ([]*LockedStakeEntry, error) { + // Validate inputs. + if validatorPKID == nil { + return nil, errors.New("UtxoView.GetLockedStakeEntriesInRange: nil ValidatorPKID provided as input") + } + if stakerPKID == nil { + return nil, errors.New("UtxoView.GetLockedStakeEntriesInRange: nil StakerPKID provided as input") + } + if startEpochNumber > endEpochNumber { + return nil, errors.New("UtxoView.GetLockedStakeEntriesInRange: invalid LockedAtEpochNumber range provided as input") + } + + // Store matching LockedStakeEntries in a map to prevent + // returning duplicates between the db and UtxoView. + lockedStakeEntriesMap := make(map[LockedStakeMapKey]*LockedStakeEntry) + + // First, pull matching LockedStakeEntries from the db. + dbLockedStakeEntries, err := DBGetLockedStakeEntriesInRange( + bav.Handle, bav.Snapshot, validatorPKID, stakerPKID, startEpochNumber, endEpochNumber, + ) + if err != nil { + return nil, errors.Wrapf(err, "UtxoView.GetLockedStakeEntriesInRange: ") + } + for _, lockedStakeEntry := range dbLockedStakeEntries { + lockedStakeEntriesMap[lockedStakeEntry.ToMapKey()] = lockedStakeEntry + } + + // Then, pull matching LockedStakeEntries from the UtxoView. + // Loop through all LockedStakeEntries in the UtxoView. + for _, lockedStakeEntry := range bav.LockedStakeMapKeyToLockedStakeEntry { + // Filter to matching LockedStakeEntries. + if !lockedStakeEntry.ValidatorPKID.Eq(validatorPKID) || + !lockedStakeEntry.StakerPKID.Eq(stakerPKID) || + lockedStakeEntry.LockedAtEpochNumber < startEpochNumber || + lockedStakeEntry.LockedAtEpochNumber > endEpochNumber { + continue + } + + if lockedStakeEntry.isDeleted { + // Remove from map if isDeleted. + delete(lockedStakeEntriesMap, lockedStakeEntry.ToMapKey()) + } else { + // Otherwise, add to map. + lockedStakeEntriesMap[lockedStakeEntry.ToMapKey()] = lockedStakeEntry + } + } + + // Convert LockedStakeEntries map to slice, sorted by LockedAtEpochNumber ASC. + var lockedStakeEntries []*LockedStakeEntry + for _, lockedStakeEntry := range lockedStakeEntriesMap { + lockedStakeEntries = append(lockedStakeEntries, lockedStakeEntry) + } + sort.Slice(lockedStakeEntries, func(ii, jj int) bool { + return lockedStakeEntries[ii].LockedAtEpochNumber < lockedStakeEntries[jj].LockedAtEpochNumber + }) + return lockedStakeEntries, nil +} + +func (bav *UtxoView) _setStakeEntryMappings(stakeEntry *StakeEntry) { + // This function shouldn't be called with nil. + if stakeEntry == nil { + glog.Errorf("_setStakeEntryMappings: called with nil entry, this should never happen") + return + } + bav.StakeMapKeyToStakeEntry[stakeEntry.ToMapKey()] = stakeEntry +} + +func (bav *UtxoView) _setLockedStakeEntryMappings(lockedStakeEntry *LockedStakeEntry) { + // This function shouldn't be called with nil. + if lockedStakeEntry == nil { + glog.Errorf("_setLockedStakeEntryMappings: called with nil entry, this should never happen") + return + } + bav.LockedStakeMapKeyToLockedStakeEntry[lockedStakeEntry.ToMapKey()] = lockedStakeEntry +} + +func (bav *UtxoView) _deleteStakeEntryMappings(stakeEntry *StakeEntry) { + // This function shouldn't be called with nil. + if stakeEntry == nil { + glog.Errorf("_deleteStakeEntryMappings: called with nil entry, this should never happen") + return + } + // Create a tombstone entry. + tombstoneEntry := *stakeEntry + tombstoneEntry.isDeleted = true + // Set the mappings to the point to the tombstone entry. + bav._setStakeEntryMappings(&tombstoneEntry) +} + +func (bav *UtxoView) _deleteLockedStakeEntryMappings(lockedStakeEntry *LockedStakeEntry) { + // This function shouldn't be called with nil. + if lockedStakeEntry == nil { + glog.Errorf("_deleteLockedStakeEntryMappings: called with nil entry, this should never happen") + return + } + // Create a tombstone entry. + tombstoneEntry := *lockedStakeEntry + tombstoneEntry.isDeleted = true + // Set the mappings to the point to the tombstone entry. + bav._setLockedStakeEntryMappings(&tombstoneEntry) +} + +func (bav *UtxoView) _flushStakeEntriesToDbWithTxn(txn *badger.Txn, blockHeight uint64) error { + // Delete all entries in the UtxoView map. + for mapKeyIter, entryIter := range bav.StakeMapKeyToStakeEntry { + // Make a copy of the iterators since we make references to them below. + mapKey := mapKeyIter + entry := *entryIter + + // Sanity-check that the entry matches the map key. + mapKeyInEntry := entry.ToMapKey() + if mapKeyInEntry != mapKey { + return fmt.Errorf( + "_flushStakeEntriesToDbWithTxn: StakeEntry key %v doesn't match MapKey %v", + &mapKeyInEntry, + &mapKey, + ) + } + + // Delete the existing mappings in the db for this MapKey. They will be + // re-added if the corresponding entry in-memory has isDeleted=false. + if err := DBDeleteStakeEntryWithTxn(txn, bav.Snapshot, &entry, blockHeight); err != nil { + return errors.Wrapf(err, "_flushStakeEntriesToDbWithTxn: ") + } + } + + // Set any !isDeleted entries in the UtxoView map. + for _, entryIter := range bav.StakeMapKeyToStakeEntry { + entry := *entryIter + if entry.isDeleted { + // If isDeleted then there's nothing to do because + // we already deleted the entry above. + } else { + // If !isDeleted then we put the corresponding + // mappings for it into the db. + if err := DBPutStakeEntryWithTxn(txn, bav.Snapshot, &entry, blockHeight); err != nil { + return errors.Wrapf(err, "_flushStakeEntriesToDbWithTxn: ") + } + } + } + + return nil +} + +func (bav *UtxoView) _flushLockedStakeEntriesToDbWithTxn(txn *badger.Txn, blockHeight uint64) error { + // Delete all entries in the UtxoView map. + for mapKeyIter, entryIter := range bav.LockedStakeMapKeyToLockedStakeEntry { + // Make a copy of the iterators since we make references to them below. + mapKey := mapKeyIter + entry := *entryIter + + // Sanity-check that the entry matches the map key. + mapKeyInEntry := entry.ToMapKey() + if mapKeyInEntry != mapKey { + return fmt.Errorf( + "_flushLockedStakeEntriesToDbWithTxn: LockedStakeEntry key %v doesn't match MapKey %v", + &mapKeyInEntry, + &mapKey, + ) + } + + // Delete the existing mappings in the db for this MapKey. They will be + // re-added if the corresponding entry in-memory has isDeleted=false. + if err := DBDeleteLockedStakeEntryWithTxn(txn, bav.Snapshot, &entry, blockHeight); err != nil { + return errors.Wrapf(err, "_flushLockedStakeEntriesToDbWithTxn: ") + } + } + + // Set any !isDeleted entries in the UtxoView map. + for _, entryIter := range bav.LockedStakeMapKeyToLockedStakeEntry { + entry := *entryIter + if entry.isDeleted { + // If isDeleted then there's nothing to do because + // we already deleted the entry above. + } else { + // If !isDeleted then we put the corresponding + // mappings for it into the db. + if err := DBPutLockedStakeEntryWithTxn(txn, bav.Snapshot, &entry, blockHeight); err != nil { + return errors.Wrapf(err, "_flushLockedStakeEntriesToDbWithTxn: ") + } + } + } + + return nil +} + +// +// MEMPOOL UTILS +// + +func (bav *UtxoView) CreateStakeTxindexMetadata(utxoOp *UtxoOperation, txn *MsgDeSoTxn) (*StakeTxindexMetadata, []*AffectedPublicKey) { + metadata := txn.TxnMeta.(*StakeMetadata) + + // Convert TransactorPublicKeyBytes to StakerPublicKeyBase58Check. + stakerPublicKeyBase58Check := PkToString(txn.PublicKey, bav.Params) + + // Convert ValidatorPublicKey to ValidatorPublicKeyBase58Check. + validatorPublicKeyBase58Check := PkToString(metadata.ValidatorPublicKey.ToBytes(), bav.Params) + + // Construct TxindexMetadata. + txindexMetadata := &StakeTxindexMetadata{ + StakerPublicKeyBase58Check: stakerPublicKeyBase58Check, + ValidatorPublicKeyBase58Check: validatorPublicKeyBase58Check, + StakeAmountNanos: metadata.StakeAmountNanos, + } + + // Construct AffectedPublicKeys. + affectedPublicKeys := []*AffectedPublicKey{ + { + PublicKeyBase58Check: stakerPublicKeyBase58Check, + Metadata: "StakerPublicKeyBase58Check", + }, + { + PublicKeyBase58Check: validatorPublicKeyBase58Check, + Metadata: "ValidatorStakedToPublicKeyBase58Check", + }, + } + + return txindexMetadata, affectedPublicKeys +} + +func (bav *UtxoView) CreateUnstakeTxindexMetadata(utxoOp *UtxoOperation, txn *MsgDeSoTxn) (*UnstakeTxindexMetadata, []*AffectedPublicKey) { + metadata := txn.TxnMeta.(*UnstakeMetadata) + + // Convert TransactorPublicKeyBytes to StakerPublicKeyBase58Check. + stakerPublicKeyBase58Check := PkToString(txn.PublicKey, bav.Params) + + // Convert ValidatorPublicKey to ValidatorPublicKeyBase58Check. + validatorPublicKeyBase58Check := PkToString(metadata.ValidatorPublicKey.ToBytes(), bav.Params) + + // Construct TxindexMetadata. + txindexMetadata := &UnstakeTxindexMetadata{ + StakerPublicKeyBase58Check: stakerPublicKeyBase58Check, + ValidatorPublicKeyBase58Check: validatorPublicKeyBase58Check, + UnstakeAmountNanos: metadata.UnstakeAmountNanos, + } + + // Construct AffectedPublicKeys. + affectedPublicKeys := []*AffectedPublicKey{ + { + PublicKeyBase58Check: stakerPublicKeyBase58Check, + Metadata: "UnstakerPublicKeyBase58Check", + }, + { + PublicKeyBase58Check: validatorPublicKeyBase58Check, + Metadata: "ValidatorUnstakedFromPublicKeyBase58Check", + }, + } + + return txindexMetadata, affectedPublicKeys +} + +func (bav *UtxoView) CreateUnlockStakeTxindexMetadata(utxoOp *UtxoOperation, txn *MsgDeSoTxn) (*UnlockStakeTxindexMetadata, []*AffectedPublicKey) { + metadata := txn.TxnMeta.(*UnlockStakeMetadata) + + // Convert TransactorPublicKeyBytes to StakerPublicKeyBase58Check. + stakerPublicKeyBase58Check := PkToString(txn.PublicKey, bav.Params) + + // Convert ValidatorPublicKey to ValidatorPublicKeyBase58Check. + validatorPublicKeyBase58Check := PkToString(metadata.ValidatorPublicKey.ToBytes(), bav.Params) + + // Calculate TotalUnlockedAmountNanos. + totalUnlockedAmountNanos := uint256.NewInt() + var err error + for _, prevLockedStakeEntry := range utxoOp.PrevLockedStakeEntries { + totalUnlockedAmountNanos, err = SafeUint256().Add( + totalUnlockedAmountNanos, prevLockedStakeEntry.LockedAmountNanos, + ) + if err != nil { + glog.Errorf("CreateUnlockStakeTxindexMetadata: error calculating TotalUnlockedAmountNanos: %v", err) + totalUnlockedAmountNanos = uint256.NewInt() + break + } + } + + // Construct TxindexMetadata. + txindexMetadata := &UnlockStakeTxindexMetadata{ + StakerPublicKeyBase58Check: stakerPublicKeyBase58Check, + ValidatorPublicKeyBase58Check: validatorPublicKeyBase58Check, + StartEpochNumber: metadata.StartEpochNumber, + EndEpochNumber: metadata.EndEpochNumber, + TotalUnlockedAmountNanos: totalUnlockedAmountNanos, + } + + // Construct AffectedPublicKeys. + affectedPublicKeys := []*AffectedPublicKey{ + { + PublicKeyBase58Check: stakerPublicKeyBase58Check, + Metadata: "UnlockedStakerPublicKeyBase58Check", + }, + } + + return txindexMetadata, affectedPublicKeys +} + +// +// TRANSACTION SPENDING LIMITS +// + +type StakeLimitKey struct { + ValidatorPKID PKID + StakerPKID PKID +} + +func MakeStakeLimitKey(validatorPKID *PKID, stakerPKID *PKID) StakeLimitKey { + return StakeLimitKey{ + ValidatorPKID: *validatorPKID, + StakerPKID: *stakerPKID, + } +} + +func (stakeLimitKey *StakeLimitKey) Encode() []byte { + var data []byte + data = append(data, stakeLimitKey.ValidatorPKID.ToBytes()...) + data = append(data, stakeLimitKey.StakerPKID.ToBytes()...) + return data +} + +func (stakeLimitKey *StakeLimitKey) Decode(rr *bytes.Reader) error { + var err error + + // ValidatorPKID + validatorPKID := &PKID{} + if err = validatorPKID.FromBytes(rr); err != nil { + return errors.Wrap(err, "StakeLimitKey.Decode: Problem reading ValidatorPKID: ") + } + stakeLimitKey.ValidatorPKID = *validatorPKID + + // StakerPKID + stakerPKID := &PKID{} + if err = stakerPKID.FromBytes(rr); err != nil { + return errors.Wrap(err, "StakeLimitKey.Decode: Problem reading StakerPKID: ") + } + stakeLimitKey.StakerPKID = *stakerPKID + + return nil +} + +func (bav *UtxoView) _checkStakeTxnSpendingLimitAndUpdateDerivedKey( + derivedKeyEntry DerivedKeyEntry, + transactorPublicKeyBytes []byte, + txMeta *StakeMetadata, +) (DerivedKeyEntry, error) { + // The DerivedKeyEntry.TransactionSpendingLimit for staking maps + // ValidatorPKID || StakerPKID to the amount of stake-able DESO + // nanos allowed for this derived key. + + // Convert TransactorPublicKeyBytes to StakerPKID. + stakerPKIDEntry := bav.GetPKIDForPublicKey(transactorPublicKeyBytes) + if stakerPKIDEntry == nil || stakerPKIDEntry.isDeleted { + return derivedKeyEntry, errors.Wrapf(RuleErrorInvalidStakerPKID, "UtxoView._checkStakeTxnSpendingLimitAndUpdateDerivedKey: ") + } + + // Convert ValidatorPublicKey to ValidatorPKID. + validatorEntry, err := bav.GetValidatorByPublicKey(txMeta.ValidatorPublicKey) + if err != nil { + return derivedKeyEntry, errors.Wrapf(err, "_checkStakeTxnSpendingLimitAndUpdateDerivedKey: ") + } + if validatorEntry == nil || validatorEntry.isDeleted { + return derivedKeyEntry, errors.Wrapf(RuleErrorInvalidValidatorPKID, "UtxoView._checkStakeTxnSpendingLimitAndUpdateDerivedKey: ") + } + + // Check spending limit for this validator. + // If not found, check spending limit for any validator. + isSpendingLimitExceeded := false + + for _, validatorPKID := range []*PKID{validatorEntry.ValidatorPKID, &ZeroPKID} { + // Retrieve DerivedKeyEntry.TransactionSpendingLimit. + stakeLimitKey := MakeStakeLimitKey(validatorPKID, stakerPKIDEntry.PKID) + spendingLimit, exists := derivedKeyEntry.TransactionSpendingLimitTracker.StakeLimitMap[stakeLimitKey] + if !exists { + continue + } + spendingLimitCmp := spendingLimit.Cmp(txMeta.StakeAmountNanos) + + // If the amount being staked exceeds the spending limit, note it, and skip this spending limit. + // This solves for the case where the amount being staked is greater than the spending limit + // scoped to a specific validator but may be within the limit scoped to any validator. + if spendingLimitCmp < 0 { + isSpendingLimitExceeded = true + continue + } + + // If the spending limit exceeds the amount being staked, update the spending limit. + if spendingLimitCmp > 0 { + updatedSpendingLimit, err := SafeUint256().Sub(spendingLimit, txMeta.StakeAmountNanos) + if err != nil { + return derivedKeyEntry, errors.Wrapf(err, "_checkStakeTxnSpendingLimitAndUpdateDerivedKey: ") + } + if !updatedSpendingLimit.IsUint64() { + // This should never happen, but good to double-check. + return derivedKeyEntry, errors.New( + "_checkStakeTxnSpendingLimitAndUpdateDerivedKey: updated spending limit exceeds uint64", + ) + } + derivedKeyEntry.TransactionSpendingLimitTracker.StakeLimitMap[stakeLimitKey] = updatedSpendingLimit + return derivedKeyEntry, nil + } + + // If we get to this point, the spending limit exactly equals + // the amount being staked. Delete the spending limit. + delete(derivedKeyEntry.TransactionSpendingLimitTracker.StakeLimitMap, stakeLimitKey) + return derivedKeyEntry, nil + } + + // Error if the spending limit was found but the staking limit was exceeded. + if isSpendingLimitExceeded { + return derivedKeyEntry, errors.Wrapf(RuleErrorStakeTransactionSpendingLimitExceeded, "UtxoView._checkStakeTxnSpendingLimitAndUpdateDerivedKey: ") + } + + // If we get to this point, we didn't find a matching spending limit. + return derivedKeyEntry, errors.Wrapf(RuleErrorStakeTransactionSpendingLimitNotFound, "UtxoView._checkStakeTxnSpendingLimitAndUpdateDerivedKey: ") +} + +func (bav *UtxoView) _checkUnstakeTxnSpendingLimitAndUpdateDerivedKey( + derivedKeyEntry DerivedKeyEntry, + transactorPublicKeyBytes []byte, + txMeta *UnstakeMetadata, +) (DerivedKeyEntry, error) { + // The DerivedKeyEntry.TransactionSpendingLimit for unstaking maps + // ValidatorPKID || StakerPKID to the amount of unstake-able DESO + // nanos allowed for this derived key. + + // Convert TransactorPublicKeyBytes to StakerPKID. + stakerPKIDEntry := bav.GetPKIDForPublicKey(transactorPublicKeyBytes) + if stakerPKIDEntry == nil || stakerPKIDEntry.isDeleted { + return derivedKeyEntry, errors.Wrapf(RuleErrorInvalidStakerPKID, "UtxoView._checkUnstakeTxnSpendingLimitAndUpdateDerivedKey: ") + } + + // Convert ValidatorPublicKey to ValidatorPKID. + validatorEntry, err := bav.GetValidatorByPublicKey(txMeta.ValidatorPublicKey) + if err != nil { + return derivedKeyEntry, errors.Wrapf(err, "_checkUnstakeTxnSpendingLimitAndUpdateDerivedKey: ") + } + if validatorEntry == nil || validatorEntry.isDeleted { + return derivedKeyEntry, errors.Wrapf(RuleErrorInvalidValidatorPKID, "UtxoView._checkUnstakeTxnSpendingLimitAndUpdateDerivedKey: ") + } + + // Check spending limit for this validator. + // If not found, check spending limit for any validator. + isSpendingLimitExceeded := false + + for _, validatorPKID := range []*PKID{validatorEntry.ValidatorPKID, &ZeroPKID} { + // Retrieve DerivedKeyEntry.TransactionSpendingLimit. + stakeLimitKey := MakeStakeLimitKey(validatorPKID, stakerPKIDEntry.PKID) + spendingLimit, exists := derivedKeyEntry.TransactionSpendingLimitTracker.UnstakeLimitMap[stakeLimitKey] + if !exists { + continue + } + spendingLimitCmp := spendingLimit.Cmp(txMeta.UnstakeAmountNanos) + + // If the amount being unstaked exceeds the spending limit, note it, and skip this spending limit. + // This solves for the case where the amount being unstaked is greater than the spending limit + // scoped to a specific validator but may be within the limit scoped to any validator. + if spendingLimitCmp < 0 { + isSpendingLimitExceeded = true + continue + } + + // If the spending limit exceeds the amount being unstaked, update the spending limit. + if spendingLimitCmp > 0 { + updatedSpendingLimit, err := SafeUint256().Sub(spendingLimit, txMeta.UnstakeAmountNanos) + if err != nil { + return derivedKeyEntry, errors.Wrapf(err, "_checkUnstakeTxnSpendingLimitAndUpdateDerivedKey: ") + } + if !updatedSpendingLimit.IsUint64() { + // This should never happen, but good to double-check. + return derivedKeyEntry, errors.New( + "_checkUnstakeTxnSpendingLimitAndUpdateDerivedKey: updated spending limit exceeds uint64", + ) + } + derivedKeyEntry.TransactionSpendingLimitTracker.UnstakeLimitMap[stakeLimitKey] = updatedSpendingLimit + return derivedKeyEntry, nil + } + + // If we get to this point, the spending limit exactly equals + // the amount being unstaked. Delete the spending limit. + delete(derivedKeyEntry.TransactionSpendingLimitTracker.UnstakeLimitMap, stakeLimitKey) + return derivedKeyEntry, nil + } + + // Error if the spending limit was found but the unstaking limit was exceeded. + if isSpendingLimitExceeded { + return derivedKeyEntry, errors.Wrapf(RuleErrorUnstakeTransactionSpendingLimitExceeded, "UtxoView._checkUnstakeTxnSpendingLimitAndUpdateDerivedKey: ") + } + + // If we get to this point, we didn't find a matching spending limit. + return derivedKeyEntry, errors.Wrapf(RuleErrorUnstakeTransactionSpendingLimitNotFound, "UtxoView._checkUnstakeTxnSpendingLimitAndUpdateDerivedKey: ") +} + +func (bav *UtxoView) _checkUnlockStakeTxnSpendingLimitAndUpdateDerivedKey( + derivedKeyEntry DerivedKeyEntry, + transactorPublicKeyBytes []byte, + txMeta *UnlockStakeMetadata, +) (DerivedKeyEntry, error) { + // The DerivedKeyEntry.TransactionSpendingLimit for unlocking stake maps + // ValidatorPKID || StakerPKID to the number of UnlockStake transactions + // this derived key is allowed to perform. + + // Convert TransactorPublicKeyBytes to StakerPKID. + stakerPKIDEntry := bav.GetPKIDForPublicKey(transactorPublicKeyBytes) + if stakerPKIDEntry == nil || stakerPKIDEntry.isDeleted { + return derivedKeyEntry, errors.Wrapf(RuleErrorInvalidStakerPKID, "UtxoView._checkUnlockStakeTxnSpendingLimitAndUpdateDerivedKey: ") + } + + // Convert ValidatorPublicKey to ValidatorPKID. + validatorEntry, err := bav.GetValidatorByPublicKey(txMeta.ValidatorPublicKey) + if err != nil { + return derivedKeyEntry, errors.Wrapf(err, "_checkUnlockStakeTxnSpendingLimitAndUpdateDerivedKey: ") + } + if validatorEntry == nil || validatorEntry.isDeleted { + return derivedKeyEntry, errors.Wrapf(RuleErrorInvalidValidatorPKID, "UtxoView._checkUnlockStakeTxnSpendingLimitAndUpdateDerivedKey: ") + } + + // Check spending limit for this validator. + // If not found, check spending limit for any validator. + for _, validatorPKID := range []*PKID{validatorEntry.ValidatorPKID, &ZeroPKID} { + // Retrieve DerivedKeyEntry.TransactionSpendingLimit. + stakeLimitKey := MakeStakeLimitKey(validatorPKID, stakerPKIDEntry.PKID) + spendingLimit, exists := derivedKeyEntry.TransactionSpendingLimitTracker.UnlockStakeLimitMap[stakeLimitKey] + if !exists || spendingLimit <= 0 { + continue + } + + // Delete the spending limit if we've exhausted the spending limit for this key. + if spendingLimit == 1 { + delete(derivedKeyEntry.TransactionSpendingLimitTracker.UnlockStakeLimitMap, stakeLimitKey) + } else { + // Otherwise decrement it by 1. + derivedKeyEntry.TransactionSpendingLimitTracker.UnlockStakeLimitMap[stakeLimitKey]-- + } + + // If we get to this point, we found a matching spending limit which we either deleted or decremented. + return derivedKeyEntry, nil + } + + // If we get to this point, we didn't find a matching spending limit. + return derivedKeyEntry, errors.Wrapf(RuleErrorUnlockStakeTransactionSpendingLimitNotFound, "UtxoView._checkUnlockStakeTxnSpendingLimitAndUpdateDerivedKey: ") +} + +func (bav *UtxoView) IsValidStakeLimitKey(transactorPublicKeyBytes []byte, stakeLimitKey StakeLimitKey) error { + // Convert TransactorPublicKeyBytes to TransactorPKID. + transactorPKIDEntry := bav.GetPKIDForPublicKey(transactorPublicKeyBytes) + if transactorPKIDEntry == nil || transactorPKIDEntry.isDeleted { + return errors.Wrapf(RuleErrorTransactionSpendingLimitInvalidStaker, "UtxoView.IsValidStakeLimitKey: ") + } + + // Verify TransactorPKID == StakerPKID. + if !transactorPKIDEntry.PKID.Eq(&stakeLimitKey.StakerPKID) { + return errors.Wrapf(RuleErrorTransactionSpendingLimitInvalidStaker, "UtxoView.IsValidStakeLimitKey: ") + } + + // Verify ValidatorEntry. + if stakeLimitKey.ValidatorPKID.IsZeroPKID() { + // The ZeroPKID is a special case that indicates that the spending limit + // applies to any validator. In this case, we don't need to check that the + // validator exists, as there is no validator registered for the ZeroPKID. + return nil + } + validatorEntry, err := bav.GetValidatorByPKID(&stakeLimitKey.ValidatorPKID) + if err != nil { + return errors.Wrapf(err, "IsValidStakeLimitKey: ") + } + if validatorEntry == nil || validatorEntry.isDeleted || validatorEntry.DisableDelegatedStake { + return errors.Wrapf(RuleErrorTransactionSpendingLimitInvalidValidator, "UtxoView.IsValidStakeLimitKey: ") + } + + return nil +} + +// +// CONSTANTS +// + +const RuleErrorInvalidStakerPKID RuleError = "RuleErrorInvalidStakerPKID" +const RuleErrorInvalidStakeAmountNanos RuleError = "RuleErrorInvalidStakeAmountNanos" +const RuleErrorInvalidStakeInsufficientBalance RuleError = "RuleErrorInvalidStakeInsufficientBalance" +const RuleErrorInvalidUnstakeNoStakeFound RuleError = "RuleErrorInvalidUnstakeNoStakeFound" +const RuleErrorInvalidUnstakeAmountNanos RuleError = "RuleErrorInvalidUnstakeAmountNanos" +const RuleErrorInvalidUnstakeInsufficientStakeFound RuleError = "RuleErrorInvalidUnstakeInsufficientStakeFound" +const RuleErrorInvalidUnlockStakeEpochRange RuleError = "RuleErrorInvalidUnlockStakeEpochRange" +const RuleErrorInvalidUnlockStakeNoUnlockableStakeFound RuleError = "RuleErrorInvalidUnlockStakeNoUnlockableStakeFound" +const RuleErrorInvalidUnlockStakeUnlockableStakeOverflowsUint64 RuleError = "RuleErrorInvalidUnlockStakeUnlockableStakeOverflowsUint64" +const RuleErrorStakeTransactionSpendingLimitNotFound RuleError = "RuleErrorStakeTransactionSpendingLimitNotFound" +const RuleErrorStakeTransactionSpendingLimitExceeded RuleError = "RuleErrorStakeTransactionSpendingLimitExceeded" +const RuleErrorUnstakeTransactionSpendingLimitNotFound RuleError = "RuleErrorUnstakeTransactionSpendingLimitNotFound" +const RuleErrorUnstakeTransactionSpendingLimitExceeded RuleError = "RuleErrorUnstakeTransactionSpendingLimitExceeded" +const RuleErrorUnlockStakeTransactionSpendingLimitNotFound RuleError = "RuleErrorUnlockStakeTransactionSpendingLimitNotFound" +const RuleErrorTransactionSpendingLimitInvalidStaker RuleError = "RuleErrorTransactionSpendingLimitInvalidStaker" +const RuleErrorTransactionSpendingLimitInvalidValidator RuleError = "RuleErrorTransactionSpendingLimitInvalidValidator" diff --git a/lib/block_view_stake_test.go b/lib/block_view_stake_test.go new file mode 100644 index 000000000..2f3d0cd98 --- /dev/null +++ b/lib/block_view_stake_test.go @@ -0,0 +1,1756 @@ +package lib + +import ( + "errors" + "github.com/btcsuite/btcd/btcec" + "github.com/holiman/uint256" + "github.com/stretchr/testify/require" + "math" + "testing" +) + +func TestStaking(t *testing.T) { + _testStaking(t, false) + _testStaking(t, true) + _testStakingWithDerivedKey(t) +} + +func _testStaking(t *testing.T, flushToDB bool) { + // Local variables + var err error + + // Initialize fork heights. + setBalanceModelBlockHeights() + defer resetBalanceModelBlockHeights() + + // Initialize test chain and miner. + chain, params, db := NewLowDifficultyBlockchain(t) + mempool, miner := NewTestMiner(t, chain, params, true) + chain.snapshot = nil + + // Mine a few blocks to give the senderPkString some money. + for ii := 0; ii < 10; ii++ { + _, err = miner.MineAndProcessSingleBlock(0, mempool) + require.NoError(t, err) + } + + // We build the testMeta obj after mining blocks so that we save the correct block height. + blockHeight := uint64(chain.blockTip().Height + 1) + testMeta := &TestMeta{ + t: t, + chain: chain, + params: params, + db: db, + mempool: mempool, + miner: miner, + savedHeight: uint32(blockHeight), + feeRateNanosPerKb: uint64(101), + } + + _registerOrTransferWithTestMeta(testMeta, "m0", senderPkString, m0Pub, senderPrivString, 1e3) + _registerOrTransferWithTestMeta(testMeta, "m1", senderPkString, m1Pub, senderPrivString, 1e3) + _registerOrTransferWithTestMeta(testMeta, "m2", senderPkString, m2Pub, senderPrivString, 1e3) + _registerOrTransferWithTestMeta(testMeta, "m3", senderPkString, m3Pub, senderPrivString, 1e3) + _registerOrTransferWithTestMeta(testMeta, "m4", senderPkString, m4Pub, senderPrivString, 1e3) + _registerOrTransferWithTestMeta(testMeta, "", senderPkString, paramUpdaterPub, senderPrivString, 1e3) + + m0PKID := DBGetPKIDEntryForPublicKey(db, chain.snapshot, m0PkBytes).PKID + m1PKID := DBGetPKIDEntryForPublicKey(db, chain.snapshot, m1PkBytes).PKID + m2PKID := DBGetPKIDEntryForPublicKey(db, chain.snapshot, m2PkBytes).PKID + m3PKID := DBGetPKIDEntryForPublicKey(db, chain.snapshot, m3PkBytes).PKID + m4PKID := DBGetPKIDEntryForPublicKey(db, chain.snapshot, m4PkBytes).PKID + _, _, _, _, _ = m0PKID, m1PKID, m2PKID, m3PKID, m4PKID + + // Helper utils + utxoView := func() *UtxoView { + newUtxoView, err := mempool.GetAugmentedUniversalView() + require.NoError(t, err) + return newUtxoView + } + + getDESOBalanceNanos := func(publicKeyBytes []byte) uint64 { + desoBalanceNanos, err := utxoView().GetDeSoBalanceNanosForPublicKey(publicKeyBytes) + require.NoError(t, err) + return desoBalanceNanos + } + + { + // Param Updater set min fee rate to 101 nanos per KB + params.ExtraRegtestParamUpdaterKeys[MakePkMapKey(paramUpdaterPkBytes)] = true + _updateGlobalParamsEntryWithTestMeta( + testMeta, + testMeta.feeRateNanosPerKb, + paramUpdaterPub, + paramUpdaterPriv, + -1, + int64(testMeta.feeRateNanosPerKb), + -1, + -1, + -1, + ) + } + { + // m0 registers as a validator. + params.ForkHeights.ProofOfStakeNewTxnTypesBlockHeight = uint32(1) + GlobalDeSoParams.EncoderMigrationHeights = GetEncoderMigrationHeights(¶ms.ForkHeights) + GlobalDeSoParams.EncoderMigrationHeightsList = GetEncoderMigrationHeightsList(¶ms.ForkHeights) + + registerAsValidatorMetadata := &RegisterAsValidatorMetadata{ + Domains: [][]byte{[]byte("https://example.com")}, + } + _, _, _, err = _submitRegisterAsValidatorTxn( + testMeta, m0Pub, m0Priv, registerAsValidatorMetadata, nil, flushToDB, + ) + require.NoError(t, err) + + validatorEntry, err := utxoView().GetValidatorByPKID(m0PKID) + require.NoError(t, err) + require.NotNil(t, validatorEntry) + require.Len(t, validatorEntry.Domains, 1) + require.Equal(t, validatorEntry.Domains[0], []byte("https://example.com")) + require.True(t, validatorEntry.TotalStakeAmountNanos.IsZero()) + } + // + // STAKING + // + { + // RuleErrorProofOfStakeTxnBeforeBlockHeight + params.ForkHeights.ProofOfStakeNewTxnTypesBlockHeight = math.MaxUint32 + GlobalDeSoParams.EncoderMigrationHeights = GetEncoderMigrationHeights(¶ms.ForkHeights) + GlobalDeSoParams.EncoderMigrationHeightsList = GetEncoderMigrationHeightsList(¶ms.ForkHeights) + + stakeMetadata := &StakeMetadata{ + ValidatorPublicKey: NewPublicKey(m0PkBytes), + StakeAmountNanos: uint256.NewInt().SetUint64(100), + } + _, err = _submitStakeTxn( + testMeta, m1Pub, m1Priv, stakeMetadata, nil, flushToDB, + ) + require.Error(t, err) + require.Contains(t, err.Error(), RuleErrorProofofStakeTxnBeforeBlockHeight) + + params.ForkHeights.ProofOfStakeNewTxnTypesBlockHeight = uint32(1) + GlobalDeSoParams.EncoderMigrationHeights = GetEncoderMigrationHeights(¶ms.ForkHeights) + GlobalDeSoParams.EncoderMigrationHeightsList = GetEncoderMigrationHeightsList(¶ms.ForkHeights) + } + { + // RuleErrorInvalidValidatorPKID + stakeMetadata := &StakeMetadata{ + ValidatorPublicKey: NewPublicKey(m2PkBytes), + StakeAmountNanos: uint256.NewInt(), + } + _, err = _submitStakeTxn( + testMeta, m1Pub, m1Priv, stakeMetadata, nil, flushToDB, + ) + require.Error(t, err) + require.Contains(t, err.Error(), RuleErrorInvalidValidatorPKID) + } + { + // RuleErrorInvalidStakeAmountNanos + stakeMetadata := &StakeMetadata{ + ValidatorPublicKey: NewPublicKey(m0PkBytes), + StakeAmountNanos: nil, + } + _, err = _submitStakeTxn( + testMeta, m1Pub, m1Priv, stakeMetadata, nil, flushToDB, + ) + require.Error(t, err) + require.Contains(t, err.Error(), RuleErrorInvalidStakeAmountNanos) + } + { + // RuleErrorInvalidStakeAmountNanos + stakeMetadata := &StakeMetadata{ + ValidatorPublicKey: NewPublicKey(m0PkBytes), + StakeAmountNanos: uint256.NewInt(), + } + _, err = _submitStakeTxn( + testMeta, m1Pub, m1Priv, stakeMetadata, nil, flushToDB, + ) + require.Error(t, err) + require.Contains(t, err.Error(), RuleErrorInvalidStakeAmountNanos) + } + { + // RuleErrorInvalidStakeAmountNanos + stakeMetadata := &StakeMetadata{ + ValidatorPublicKey: NewPublicKey(m0PkBytes), + StakeAmountNanos: MaxUint256, + } + _, err = _submitStakeTxn( + testMeta, m1Pub, m1Priv, stakeMetadata, nil, flushToDB, + ) + require.Error(t, err) + require.Contains(t, err.Error(), RuleErrorInvalidStakeAmountNanos) + } + { + // RuleErrorInvalidStakeInsufficientBalance + stakeMetadata := &StakeMetadata{ + ValidatorPublicKey: NewPublicKey(m0PkBytes), + StakeAmountNanos: uint256.NewInt().SetUint64(math.MaxUint64), + } + _, err = _submitStakeTxn( + testMeta, m1Pub, m1Priv, stakeMetadata, nil, flushToDB, + ) + require.Error(t, err) + require.Contains(t, err.Error(), RuleErrorInvalidStakeInsufficientBalance) + } + { + // m1 stakes with m0. + m1OldDESOBalanceNanos := getDESOBalanceNanos(m1PkBytes) + stakeMetadata := &StakeMetadata{ + ValidatorPublicKey: NewPublicKey(m0PkBytes), + StakeAmountNanos: uint256.NewInt().SetUint64(100), + } + extraData := map[string][]byte{"TestKey": []byte("TestValue")} + feeNanos, err := _submitStakeTxn( + testMeta, m1Pub, m1Priv, stakeMetadata, extraData, flushToDB, + ) + require.NoError(t, err) + + // Verify StakeEntry.StakeAmountNanos. + stakeEntry, err := utxoView().GetStakeEntry(m0PKID, m1PKID) + require.NoError(t, err) + require.NotNil(t, stakeEntry) + require.Equal(t, stakeEntry.StakeAmountNanos, uint256.NewInt().SetUint64(100)) + require.Equal(t, stakeEntry.ExtraData["TestKey"], []byte("TestValue")) + + // Verify ValidatorEntry.TotalStakeAmountNanos. + validatorEntry, err := utxoView().GetValidatorByPKID(m0PKID) + require.NoError(t, err) + require.NotNil(t, validatorEntry) + require.Equal(t, validatorEntry.TotalStakeAmountNanos, uint256.NewInt().SetUint64(100)) + + // Verify GlobalStakeAmountNanos. + globalStakeAmountNanos, err := utxoView().GetGlobalStakeAmountNanos() + require.NoError(t, err) + require.Equal(t, globalStakeAmountNanos, uint256.NewInt().SetUint64(100)) + + // Verify m1's DESO balance decreases by StakeAmountNanos (net of fees). + m1NewDESOBalanceNanos := getDESOBalanceNanos(m1PkBytes) + require.Equal(t, m1OldDESOBalanceNanos-feeNanos-stakeMetadata.StakeAmountNanos.Uint64(), m1NewDESOBalanceNanos) + } + { + // m1 stakes more with m0. + m1OldDESOBalanceNanos := getDESOBalanceNanos(m1PkBytes) + stakeMetadata := &StakeMetadata{ + ValidatorPublicKey: NewPublicKey(m0PkBytes), + StakeAmountNanos: uint256.NewInt().SetUint64(50), + } + extraData := map[string][]byte{"TestKey": []byte("TestValue2")} + feeNanos, err := _submitStakeTxn( + testMeta, m1Pub, m1Priv, stakeMetadata, extraData, flushToDB, + ) + require.NoError(t, err) + + // Verify StakeEntry.StakeAmountNanos. + stakeEntry, err := utxoView().GetStakeEntry(m0PKID, m1PKID) + require.NoError(t, err) + require.NotNil(t, stakeEntry) + require.Equal(t, stakeEntry.StakeAmountNanos, uint256.NewInt().SetUint64(150)) + require.Equal(t, stakeEntry.ExtraData["TestKey"], []byte("TestValue2")) + + // Verify ValidatorEntry.TotalStakeAmountNanos. + validatorEntry, err := utxoView().GetValidatorByPKID(m0PKID) + require.NoError(t, err) + require.NotNil(t, validatorEntry) + require.Equal(t, validatorEntry.TotalStakeAmountNanos, uint256.NewInt().SetUint64(150)) + + // Verify GlobalStakeAmountNanos. + globalStakeAmountNanos, err := utxoView().GetGlobalStakeAmountNanos() + require.NoError(t, err) + require.Equal(t, globalStakeAmountNanos, uint256.NewInt().SetUint64(150)) + + // Verify m1's DESO balance decreases by StakeAmountNanos (net of fees). + m1NewDESOBalanceNanos := getDESOBalanceNanos(m1PkBytes) + require.Equal(t, m1OldDESOBalanceNanos-feeNanos-stakeMetadata.StakeAmountNanos.Uint64(), m1NewDESOBalanceNanos) + } + // + // UNSTAKING + // + { + // RuleErrorProofOfStakeTxnBeforeBlockHeight + params.ForkHeights.ProofOfStakeNewTxnTypesBlockHeight = math.MaxUint32 + GlobalDeSoParams.EncoderMigrationHeights = GetEncoderMigrationHeights(¶ms.ForkHeights) + GlobalDeSoParams.EncoderMigrationHeightsList = GetEncoderMigrationHeightsList(¶ms.ForkHeights) + + unstakeMetadata := &UnstakeMetadata{ + ValidatorPublicKey: NewPublicKey(m0PkBytes), + UnstakeAmountNanos: uint256.NewInt().SetUint64(40), + } + _, err = _submitUnstakeTxn( + testMeta, m1Pub, m1Priv, unstakeMetadata, nil, flushToDB, + ) + require.Error(t, err) + require.Contains(t, err.Error(), RuleErrorProofofStakeTxnBeforeBlockHeight) + + params.ForkHeights.ProofOfStakeNewTxnTypesBlockHeight = uint32(1) + GlobalDeSoParams.EncoderMigrationHeights = GetEncoderMigrationHeights(¶ms.ForkHeights) + GlobalDeSoParams.EncoderMigrationHeightsList = GetEncoderMigrationHeightsList(¶ms.ForkHeights) + } + { + // RuleErrorInvalidValidatorPKID + unstakeMetadata := &UnstakeMetadata{ + ValidatorPublicKey: NewPublicKey(m2PkBytes), + UnstakeAmountNanos: uint256.NewInt().SetUint64(40), + } + _, err = _submitUnstakeTxn( + testMeta, m1Pub, m1Priv, unstakeMetadata, nil, flushToDB, + ) + require.Error(t, err) + require.Contains(t, err.Error(), RuleErrorInvalidValidatorPKID) + } + { + // RuleErrorInvalidUnstakeNoStakeFound + unstakeMetadata := &UnstakeMetadata{ + ValidatorPublicKey: NewPublicKey(m0PkBytes), + UnstakeAmountNanos: uint256.NewInt().SetUint64(40), + } + _, err = _submitUnstakeTxn( + testMeta, m2Pub, m2Priv, unstakeMetadata, nil, flushToDB, + ) + require.Error(t, err) + require.Contains(t, err.Error(), RuleErrorInvalidUnstakeNoStakeFound) + } + { + // RuleErrorInvalidUnstakeAmountNanos + unstakeMetadata := &UnstakeMetadata{ + ValidatorPublicKey: NewPublicKey(m0PkBytes), + UnstakeAmountNanos: nil, + } + _, err = _submitUnstakeTxn( + testMeta, m1Pub, m1Priv, unstakeMetadata, nil, flushToDB, + ) + require.Error(t, err) + require.Contains(t, err.Error(), RuleErrorInvalidUnstakeAmountNanos) + } + { + // RuleErrorInvalidUnstakeAmountNanos + unstakeMetadata := &UnstakeMetadata{ + ValidatorPublicKey: NewPublicKey(m0PkBytes), + UnstakeAmountNanos: uint256.NewInt(), + } + _, err = _submitUnstakeTxn( + testMeta, m1Pub, m1Priv, unstakeMetadata, nil, flushToDB, + ) + require.Error(t, err) + require.Contains(t, err.Error(), RuleErrorInvalidUnstakeAmountNanos) + } + { + // RuleErrorInvalidUnstakeInsufficientStakeFound + unstakeMetadata := &UnstakeMetadata{ + ValidatorPublicKey: NewPublicKey(m0PkBytes), + UnstakeAmountNanos: MaxUint256, + } + _, err = _submitUnstakeTxn( + testMeta, m1Pub, m1Priv, unstakeMetadata, nil, flushToDB, + ) + require.Error(t, err) + require.Contains(t, err.Error(), RuleErrorInvalidUnstakeInsufficientStakeFound) + } + { + // m1 unstakes from m0. + m1OldDESOBalanceNanos := getDESOBalanceNanos(m1PkBytes) + unstakeMetadata := &UnstakeMetadata{ + ValidatorPublicKey: NewPublicKey(m0PkBytes), + UnstakeAmountNanos: uint256.NewInt().SetUint64(40), + } + extraData := map[string][]byte{"TestKey": []byte("TestValue")} + feeNanos, err := _submitUnstakeTxn( + testMeta, m1Pub, m1Priv, unstakeMetadata, extraData, flushToDB, + ) + require.NoError(t, err) + + // Verify StakeEntry.StakeAmountNanos. + stakeEntry, err := utxoView().GetStakeEntry(m0PKID, m1PKID) + require.NoError(t, err) + require.Equal(t, stakeEntry.StakeAmountNanos, uint256.NewInt().SetUint64(110)) + + // Verify ValidatorEntry.TotalStakeAmountNanos. + validatorEntry, err := utxoView().GetValidatorByPKID(m0PKID) + require.NoError(t, err) + require.Equal(t, validatorEntry.TotalStakeAmountNanos, uint256.NewInt().SetUint64(110)) + + // Verify GlobalStakeAmountNanos. + globalStakeAmountNanos, err := utxoView().GetGlobalStakeAmountNanos() + require.NoError(t, err) + require.Equal(t, globalStakeAmountNanos, uint256.NewInt().SetUint64(110)) + + // Verify LockedStakeEntry.UnstakeAmountNanos. + currentEpochNumber := uint64(0) // TODO: get epoch number from db. + lockedStakeEntry, err := utxoView().GetLockedStakeEntry(m0PKID, m1PKID, currentEpochNumber) + require.NoError(t, err) + require.Equal(t, lockedStakeEntry.LockedAmountNanos, uint256.NewInt().SetUint64(40)) + require.Equal(t, lockedStakeEntry.ExtraData["TestKey"], []byte("TestValue")) + + // Verify m1's balance stays the same (net of fees). + m1NewDESOBalanceNanos := getDESOBalanceNanos(m1PkBytes) + require.Equal(t, m1OldDESOBalanceNanos-feeNanos, m1NewDESOBalanceNanos) + } + { + // m1 unstakes more from m0. + m1OldDESOBalanceNanos := getDESOBalanceNanos(m1PkBytes) + unstakeMetadata := &UnstakeMetadata{ + ValidatorPublicKey: NewPublicKey(m0PkBytes), + UnstakeAmountNanos: uint256.NewInt().SetUint64(30), + } + extraData := map[string][]byte{"TestKey": []byte("TestValue2")} + feeNanos, err := _submitUnstakeTxn( + testMeta, m1Pub, m1Priv, unstakeMetadata, extraData, flushToDB, + ) + require.NoError(t, err) + + // Verify StakeEntry.StakeAmountNanos. + stakeEntry, err := utxoView().GetStakeEntry(m0PKID, m1PKID) + require.NoError(t, err) + require.Equal(t, stakeEntry.StakeAmountNanos, uint256.NewInt().SetUint64(80)) + + // Verify ValidatorEntry.TotalStakeAmountNanos. + validatorEntry, err := utxoView().GetValidatorByPKID(m0PKID) + require.NoError(t, err) + require.Equal(t, validatorEntry.TotalStakeAmountNanos, uint256.NewInt().SetUint64(80)) + + // Verify GlobalStakeAmountNanos. + globalStakeAmountNanos, err := utxoView().GetGlobalStakeAmountNanos() + require.NoError(t, err) + require.Equal(t, globalStakeAmountNanos, uint256.NewInt().SetUint64(80)) + + // Verify LockedStakeEntry.UnstakeAmountNanos. + currentEpochNumber := uint64(0) // TODO: get epoch number from db. + lockedStakeEntry, err := utxoView().GetLockedStakeEntry(m0PKID, m1PKID, currentEpochNumber) + require.NoError(t, err) + require.Equal(t, lockedStakeEntry.LockedAmountNanos, uint256.NewInt().SetUint64(70)) + require.Equal(t, lockedStakeEntry.ExtraData["TestKey"], []byte("TestValue2")) + + // Verify m1's balance stays the same (net of fees). + m1NewDESOBalanceNanos := getDESOBalanceNanos(m1PkBytes) + require.Equal(t, m1OldDESOBalanceNanos-feeNanos, m1NewDESOBalanceNanos) + } + { + // m1 unstakes the rest of their stake with m0. + m1OldDESOBalanceNanos := getDESOBalanceNanos(m1PkBytes) + unstakeMetadata := &UnstakeMetadata{ + ValidatorPublicKey: NewPublicKey(m0PkBytes), + UnstakeAmountNanos: uint256.NewInt().SetUint64(80), + } + feeNanos, err := _submitUnstakeTxn( + testMeta, m1Pub, m1Priv, unstakeMetadata, nil, flushToDB, + ) + require.NoError(t, err) + + // Verify StakeEntry.isDeleted. + stakeEntry, err := utxoView().GetStakeEntry(m0PKID, m1PKID) + require.NoError(t, err) + require.Nil(t, stakeEntry) + + // Verify ValidatorEntry.TotalStakeAmountNanos. + validatorEntry, err := utxoView().GetValidatorByPKID(m0PKID) + require.NoError(t, err) + require.Equal(t, validatorEntry.TotalStakeAmountNanos, uint256.NewInt()) + + // Verify GlobalStakeAmountNanos. + globalStakeAmountNanos, err := utxoView().GetGlobalStakeAmountNanos() + require.NoError(t, err) + require.Equal(t, globalStakeAmountNanos, uint256.NewInt()) + + // Verify LockedStakeEntry.UnstakeAmountNanos. + currentEpochNumber := uint64(0) // TODO: get epoch number from db. + lockedStakeEntry, err := utxoView().GetLockedStakeEntry(m0PKID, m1PKID, currentEpochNumber) + require.NoError(t, err) + require.Equal(t, lockedStakeEntry.LockedAmountNanos, uint256.NewInt().SetUint64(150)) + require.Equal(t, lockedStakeEntry.ExtraData["TestKey"], []byte("TestValue2")) + + // Verify m1's balance stays the same (net of fees). + m1NewDESOBalanceNanos := getDESOBalanceNanos(m1PkBytes) + require.Equal(t, m1OldDESOBalanceNanos-feeNanos, m1NewDESOBalanceNanos) + } + // + // UNLOCK STAKE + // + { + // RuleErrorProofOfStakeTxnBeforeBlockHeight + params.ForkHeights.ProofOfStakeNewTxnTypesBlockHeight = math.MaxUint32 + GlobalDeSoParams.EncoderMigrationHeights = GetEncoderMigrationHeights(¶ms.ForkHeights) + GlobalDeSoParams.EncoderMigrationHeightsList = GetEncoderMigrationHeightsList(¶ms.ForkHeights) + + unlockStakeMetadata := &UnlockStakeMetadata{ + ValidatorPublicKey: NewPublicKey(m0PkBytes), + StartEpochNumber: 0, + EndEpochNumber: 0, + } + _, err = _submitUnlockStakeTxn( + testMeta, m1Pub, m1Priv, unlockStakeMetadata, nil, flushToDB, + ) + require.Error(t, err) + require.Contains(t, err.Error(), RuleErrorProofofStakeTxnBeforeBlockHeight) + + params.ForkHeights.ProofOfStakeNewTxnTypesBlockHeight = uint32(1) + GlobalDeSoParams.EncoderMigrationHeights = GetEncoderMigrationHeights(¶ms.ForkHeights) + GlobalDeSoParams.EncoderMigrationHeightsList = GetEncoderMigrationHeightsList(¶ms.ForkHeights) + } + { + // RuleErrorInvalidValidatorPKID + unlockStakeMetadata := &UnlockStakeMetadata{ + ValidatorPublicKey: NewPublicKey(m2PkBytes), + StartEpochNumber: 0, + EndEpochNumber: 0, + } + _, err = _submitUnlockStakeTxn( + testMeta, m1Pub, m1Priv, unlockStakeMetadata, nil, flushToDB, + ) + require.Error(t, err) + require.Contains(t, err.Error(), RuleErrorInvalidValidatorPKID) + } + { + // RuleErrorInvalidUnlockStakeEpochRange + unlockStakeMetadata := &UnlockStakeMetadata{ + ValidatorPublicKey: NewPublicKey(m0PkBytes), + StartEpochNumber: 1, + EndEpochNumber: 0, + } + _, err = _submitUnlockStakeTxn( + testMeta, m1Pub, m1Priv, unlockStakeMetadata, nil, flushToDB, + ) + require.Error(t, err) + require.Contains(t, err.Error(), RuleErrorInvalidUnlockStakeEpochRange) + } + { + // m1 unlocks stake that was assigned to m0. + lockedStakeEntries, err := utxoView().GetLockedStakeEntriesInRange(m0PKID, m1PKID, 0, 0) + require.NoError(t, err) + require.Equal(t, len(lockedStakeEntries), 1) + require.Equal(t, lockedStakeEntries[0].LockedAmountNanos, uint256.NewInt().SetUint64(150)) + + m1OldDESOBalanceNanos := getDESOBalanceNanos(m1PkBytes) + unlockStakeMetadata := &UnlockStakeMetadata{ + ValidatorPublicKey: NewPublicKey(m0PkBytes), + StartEpochNumber: 0, + EndEpochNumber: 0, + } + feeNanos, err := _submitUnlockStakeTxn( + testMeta, m1Pub, m1Priv, unlockStakeMetadata, nil, flushToDB, + ) + require.NoError(t, err) + + // Verify StakeEntry.isDeleted. + stakeEntry, err := utxoView().GetStakeEntry(m0PKID, m1PKID) + require.NoError(t, err) + require.Nil(t, stakeEntry) + + // Verify ValidatorEntry.TotalStakeAmountNanos. + validatorEntry, err := utxoView().GetValidatorByPKID(m0PKID) + require.NoError(t, err) + require.Equal(t, validatorEntry.TotalStakeAmountNanos, uint256.NewInt()) + + // Verify GlobalStakeAmountNanos. + globalStakeAmountNanos, err := utxoView().GetGlobalStakeAmountNanos() + require.NoError(t, err) + require.Equal(t, globalStakeAmountNanos, uint256.NewInt()) + + // Verify LockedStakeEntry.isDeleted. + currentEpochNumber := uint64(0) // TODO: get epoch number from db. + lockedStakeEntry, err := utxoView().GetLockedStakeEntry(m0PKID, m1PKID, currentEpochNumber) + require.NoError(t, err) + require.Nil(t, lockedStakeEntry) + + // Verify m1's DESO balance increases by LockedAmountNanos (net of fees). + m1NewDESOBalanceNanos := getDESOBalanceNanos(m1PkBytes) + require.Equal(t, m1OldDESOBalanceNanos-feeNanos+uint64(150), m1NewDESOBalanceNanos) + } + { + // RuleErrorInvalidUnlockStakeNoUnlockableStakeFound + unlockStakeMetadata := &UnlockStakeMetadata{ + ValidatorPublicKey: NewPublicKey(m0PkBytes), + StartEpochNumber: 0, + EndEpochNumber: 0, + } + _, err = _submitUnlockStakeTxn( + testMeta, m1Pub, m1Priv, unlockStakeMetadata, nil, flushToDB, + ) + require.Error(t, err) + require.Contains(t, err.Error(), RuleErrorInvalidUnlockStakeNoUnlockableStakeFound) + } + + // Flush mempool to the db and test rollbacks. + require.NoError(t, mempool.universalUtxoView.FlushToDb(blockHeight)) + _executeAllTestRollbackAndFlush(testMeta) +} + +func _submitStakeTxn( + testMeta *TestMeta, + transactorPublicKeyBase58Check string, + transactorPrivateKeyBase58Check string, + metadata *StakeMetadata, + extraData map[string][]byte, + flushToDB bool, +) (_fees uint64, _err error) { + // Record transactor's prevBalance. + prevBalance := _getBalance(testMeta.t, testMeta.chain, testMeta.mempool, transactorPublicKeyBase58Check) + + // Convert PublicKeyBase58Check to PkBytes. + updaterPkBytes, _, err := Base58CheckDecode(transactorPublicKeyBase58Check) + require.NoError(testMeta.t, err) + + // Create the transaction. + txn, totalInputMake, changeAmountMake, feesMake, err := testMeta.chain.CreateStakeTxn( + updaterPkBytes, + metadata, + extraData, + testMeta.feeRateNanosPerKb, + testMeta.mempool, + []*DeSoOutput{}, + ) + if err != nil { + return 0, err + } + require.Equal(testMeta.t, totalInputMake, changeAmountMake+feesMake) + + // Sign the transaction now that its inputs are set up. + _signTxn(testMeta.t, txn, transactorPrivateKeyBase58Check) + + // Connect the transaction. + utxoOps, totalInput, totalOutput, fees, err := testMeta.mempool.universalUtxoView.ConnectTransaction( + txn, + txn.Hash(), + getTxnSize(*txn), + testMeta.savedHeight, + true, + false, + ) + if err != nil { + return 0, err + } + require.Equal(testMeta.t, totalInput, totalOutput+fees) + require.Equal(testMeta.t, totalInput, totalInputMake+metadata.StakeAmountNanos.Uint64()) + require.Equal(testMeta.t, OperationTypeStake, utxoOps[len(utxoOps)-1].Type) + if flushToDB { + require.NoError(testMeta.t, testMeta.mempool.universalUtxoView.FlushToDb(uint64(testMeta.savedHeight))) + } + require.NoError(testMeta.t, testMeta.mempool.RegenerateReadOnlyView()) + + // Record the txn. + testMeta.expectedSenderBalances = append(testMeta.expectedSenderBalances, prevBalance) + testMeta.txnOps = append(testMeta.txnOps, utxoOps) + testMeta.txns = append(testMeta.txns, txn) + return fees, nil +} + +func _submitUnstakeTxn( + testMeta *TestMeta, + transactorPublicKeyBase58Check string, + transactorPrivateKeyBase58Check string, + metadata *UnstakeMetadata, + extraData map[string][]byte, + flushToDB bool, +) (_fees uint64, _err error) { + // Record transactor's prevBalance. + prevBalance := _getBalance(testMeta.t, testMeta.chain, testMeta.mempool, transactorPublicKeyBase58Check) + + // Convert PublicKeyBase58Check to PkBytes. + updaterPkBytes, _, err := Base58CheckDecode(transactorPublicKeyBase58Check) + require.NoError(testMeta.t, err) + + // Create the transaction. + txn, totalInputMake, changeAmountMake, feesMake, err := testMeta.chain.CreateUnstakeTxn( + updaterPkBytes, + metadata, + extraData, + testMeta.feeRateNanosPerKb, + testMeta.mempool, + []*DeSoOutput{}, + ) + if err != nil { + return 0, err + } + require.Equal(testMeta.t, totalInputMake, changeAmountMake+feesMake) + + // Sign the transaction now that its inputs are set up. + _signTxn(testMeta.t, txn, transactorPrivateKeyBase58Check) + + // Connect the transaction. + utxoOps, totalInput, totalOutput, fees, err := testMeta.mempool.universalUtxoView.ConnectTransaction( + txn, + txn.Hash(), + getTxnSize(*txn), + testMeta.savedHeight, + true, + false, + ) + if err != nil { + return 0, err + } + require.Equal(testMeta.t, totalInput, totalOutput+fees) + require.Equal(testMeta.t, totalInput, totalInputMake) + require.Equal(testMeta.t, OperationTypeUnstake, utxoOps[len(utxoOps)-1].Type) + if flushToDB { + require.NoError(testMeta.t, testMeta.mempool.universalUtxoView.FlushToDb(uint64(testMeta.savedHeight))) + } + require.NoError(testMeta.t, testMeta.mempool.RegenerateReadOnlyView()) + + // Record the txn. + testMeta.expectedSenderBalances = append(testMeta.expectedSenderBalances, prevBalance) + testMeta.txnOps = append(testMeta.txnOps, utxoOps) + testMeta.txns = append(testMeta.txns, txn) + return fees, nil +} + +func _submitUnlockStakeTxn( + testMeta *TestMeta, + transactorPublicKeyBase58Check string, + transactorPrivateKeyBase58Check string, + metadata *UnlockStakeMetadata, + extraData map[string][]byte, + flushToDB bool, +) (_fees uint64, _err error) { + // Record transactor's prevBalance. + prevBalance := _getBalance(testMeta.t, testMeta.chain, testMeta.mempool, transactorPublicKeyBase58Check) + + // Convert PublicKeyBase58Check to PkBytes. + updaterPkBytes, _, err := Base58CheckDecode(transactorPublicKeyBase58Check) + require.NoError(testMeta.t, err) + + // Create the transaction. + txn, totalInputMake, changeAmountMake, feesMake, err := testMeta.chain.CreateUnlockStakeTxn( + updaterPkBytes, + metadata, + extraData, + testMeta.feeRateNanosPerKb, + testMeta.mempool, + []*DeSoOutput{}, + ) + if err != nil { + return 0, err + } + require.Equal(testMeta.t, totalInputMake, changeAmountMake+feesMake) + + // Sign the transaction now that its inputs are set up. + _signTxn(testMeta.t, txn, transactorPrivateKeyBase58Check) + + // Connect the transaction. + utxoOps, totalInput, totalOutput, fees, err := testMeta.mempool.universalUtxoView.ConnectTransaction( + txn, + txn.Hash(), + getTxnSize(*txn), + testMeta.savedHeight, + true, + false, + ) + if err != nil { + return 0, err + } + require.Equal(testMeta.t, totalInput, totalOutput+fees) + // TotalInput = TotalInputMake + TotalUnlockedAmountNanos + require.True(testMeta.t, totalInput > totalInputMake) + require.Equal(testMeta.t, OperationTypeUnlockStake, utxoOps[len(utxoOps)-1].Type) + if flushToDB { + require.NoError(testMeta.t, testMeta.mempool.universalUtxoView.FlushToDb(uint64(testMeta.savedHeight))) + } + require.NoError(testMeta.t, testMeta.mempool.RegenerateReadOnlyView()) + + // Record the txn. + testMeta.expectedSenderBalances = append(testMeta.expectedSenderBalances, prevBalance) + testMeta.txnOps = append(testMeta.txnOps, utxoOps) + testMeta.txns = append(testMeta.txns, txn) + return fees, nil +} + +func _testStakingWithDerivedKey(t *testing.T) { + var derivedKeyPriv string + var err error + + // Initialize balance model fork heights. + setBalanceModelBlockHeights() + defer resetBalanceModelBlockHeights() + + // Initialize test chain and miner. + chain, params, db := NewLowDifficultyBlockchain(t) + mempool, miner := NewTestMiner(t, chain, params, true) + + // Initialize fork heights. + params.ForkHeights.DeSoUnlimitedDerivedKeysBlockHeight = uint32(0) + params.ForkHeights.ProofOfStakeNewTxnTypesBlockHeight = uint32(1) + GlobalDeSoParams.EncoderMigrationHeights = GetEncoderMigrationHeights(¶ms.ForkHeights) + GlobalDeSoParams.EncoderMigrationHeightsList = GetEncoderMigrationHeightsList(¶ms.ForkHeights) + chain.snapshot = nil + + // Mine a few blocks to give the senderPkString some money. + for ii := 0; ii < 10; ii++ { + _, err = miner.MineAndProcessSingleBlock(0, mempool) + require.NoError(t, err) + } + + // We build the testMeta obj after mining blocks so that we save the correct block height. + blockHeight := uint64(chain.blockTip().Height) + 1 + testMeta := &TestMeta{ + t: t, + chain: chain, + params: params, + db: db, + mempool: mempool, + miner: miner, + savedHeight: uint32(blockHeight), + feeRateNanosPerKb: uint64(101), + } + + _registerOrTransferWithTestMeta(testMeta, "m0", senderPkString, m0Pub, senderPrivString, 1e3) + _registerOrTransferWithTestMeta(testMeta, "m1", senderPkString, m1Pub, senderPrivString, 1e3) + _registerOrTransferWithTestMeta(testMeta, "", senderPkString, paramUpdaterPub, senderPrivString, 1e3) + + m0PKID := DBGetPKIDEntryForPublicKey(db, chain.snapshot, m0PkBytes).PKID + m1PKID := DBGetPKIDEntryForPublicKey(db, chain.snapshot, m1PkBytes).PKID + m2PKID := DBGetPKIDEntryForPublicKey(db, chain.snapshot, m2PkBytes).PKID + + senderPkBytes, _, err := Base58CheckDecode(senderPkString) + require.NoError(t, err) + senderPrivBytes, _, err := Base58CheckDecode(senderPrivString) + require.NoError(t, err) + senderPrivKey, _ := btcec.PrivKeyFromBytes(btcec.S256(), senderPrivBytes) + senderPKID := DBGetPKIDEntryForPublicKey(db, chain.snapshot, senderPkBytes).PKID + + newUtxoView := func() *UtxoView { + utxoView, err := NewUtxoView(db, params, chain.postgres, chain.snapshot) + require.NoError(t, err) + return utxoView + } + + getDESOBalanceNanos := func(publicKeyBytes []byte) uint64 { + desoBalanceNanos, err := newUtxoView().GetDeSoBalanceNanosForPublicKey(publicKeyBytes) + require.NoError(t, err) + return desoBalanceNanos + } + + _submitAuthorizeDerivedKeyTxn := func(txnSpendingLimit *TransactionSpendingLimit) (string, error) { + utxoView := newUtxoView() + derivedKeyMetadata, derivedKeyAuthPriv := _getAuthorizeDerivedKeyMetadataWithTransactionSpendingLimit( + t, senderPrivKey, blockHeight+5, txnSpendingLimit, false, blockHeight, + ) + derivedKeyAuthPrivBase58Check := Base58CheckEncode(derivedKeyAuthPriv.Serialize(), true, params) + + prevBalance := _getBalance(testMeta.t, testMeta.chain, testMeta.mempool, senderPkString) + + utxoOps, txn, _, err := _doAuthorizeTxnWithExtraDataAndSpendingLimits( + testMeta, + utxoView, + testMeta.feeRateNanosPerKb, + senderPkBytes, + derivedKeyMetadata.DerivedPublicKey, + derivedKeyAuthPrivBase58Check, + derivedKeyMetadata.ExpirationBlock, + derivedKeyMetadata.AccessSignature, + false, + nil, + nil, + txnSpendingLimit, + ) + if err != nil { + return "", err + } + require.NoError(t, utxoView.FlushToDb(blockHeight)) + testMeta.expectedSenderBalances = append(testMeta.expectedSenderBalances, prevBalance) + testMeta.txnOps = append(testMeta.txnOps, utxoOps) + testMeta.txns = append(testMeta.txns, txn) + + err = utxoView.ValidateDerivedKey( + senderPkBytes, derivedKeyMetadata.DerivedPublicKey, blockHeight, + ) + require.NoError(t, err) + return derivedKeyAuthPrivBase58Check, nil + } + + _submitStakeTxnWithDerivedKey := func( + transactorPkBytes []byte, derivedKeyPrivBase58Check string, inputTxn MsgDeSoTxn, + ) (_fees uint64, _err error) { + utxoView := newUtxoView() + var txn *MsgDeSoTxn + + switch inputTxn.TxnMeta.GetTxnType() { + // Construct txn. + case TxnTypeStake: + txn, _, _, _, err = testMeta.chain.CreateStakeTxn( + transactorPkBytes, + inputTxn.TxnMeta.(*StakeMetadata), + make(map[string][]byte), + testMeta.feeRateNanosPerKb, + mempool, + []*DeSoOutput{}, + ) + case TxnTypeUnstake: + txn, _, _, _, err = testMeta.chain.CreateUnstakeTxn( + transactorPkBytes, + inputTxn.TxnMeta.(*UnstakeMetadata), + make(map[string][]byte), + testMeta.feeRateNanosPerKb, + mempool, + []*DeSoOutput{}, + ) + case TxnTypeUnlockStake: + txn, _, _, _, err = testMeta.chain.CreateUnlockStakeTxn( + transactorPkBytes, + inputTxn.TxnMeta.(*UnlockStakeMetadata), + make(map[string][]byte), + testMeta.feeRateNanosPerKb, + mempool, + []*DeSoOutput{}, + ) + default: + return 0, errors.New("invalid txn type") + } + if err != nil { + return 0, err + } + // Sign txn. + _signTxnWithDerivedKeyAndType(t, txn, derivedKeyPrivBase58Check, 1) + // Store the original transactor balance. + transactorPublicKeyBase58Check := Base58CheckEncode(transactorPkBytes, false, params) + prevBalance := _getBalance(testMeta.t, testMeta.chain, testMeta.mempool, transactorPublicKeyBase58Check) + // Connect txn. + utxoOps, _, _, fees, err := utxoView.ConnectTransaction( + txn, + txn.Hash(), + getTxnSize(*txn), + testMeta.savedHeight, + true, + false, + ) + if err != nil { + return 0, err + } + // Flush UTXO view to the db. + require.NoError(t, utxoView.FlushToDb(blockHeight)) + // Track txn for rolling back. + testMeta.expectedSenderBalances = append(testMeta.expectedSenderBalances, prevBalance) + testMeta.txnOps = append(testMeta.txnOps, utxoOps) + testMeta.txns = append(testMeta.txns, txn) + return fees, nil + } + + { + // ParamUpdater set min fee rate + params.ExtraRegtestParamUpdaterKeys[MakePkMapKey(paramUpdaterPkBytes)] = true + _updateGlobalParamsEntryWithTestMeta( + testMeta, + testMeta.feeRateNanosPerKb, + paramUpdaterPub, + paramUpdaterPriv, + -1, + int64(testMeta.feeRateNanosPerKb), + -1, + -1, + -1, + ) + } + { + // m0 registers as a validator. + registerAsValidatorMetadata := &RegisterAsValidatorMetadata{ + Domains: [][]byte{[]byte("https://example1.com")}, + } + _, _, _, err = _submitRegisterAsValidatorTxn( + testMeta, m0Pub, m0Priv, registerAsValidatorMetadata, nil, true, + ) + require.NoError(t, err) + } + { + // m1 registers as a validator. + registerAsValidatorMetadata := &RegisterAsValidatorMetadata{ + Domains: [][]byte{[]byte("https://example2.com")}, + } + _, _, _, err = _submitRegisterAsValidatorTxn( + testMeta, m1Pub, m1Priv, registerAsValidatorMetadata, nil, true, + ) + require.NoError(t, err) + } + { + // RuleErrorTransactionSpendingLimitInvalidStaker + // sender tries to create a DerivedKey that would allow + // m1 to stake 100 $DESO nanos with m0. Errors. + stakeLimitKey := MakeStakeLimitKey(m0PKID, m1PKID) + txnSpendingLimit := &TransactionSpendingLimit{ + GlobalDESOLimit: NanosPerUnit, // 1 $DESO spending limit + TransactionCountLimitMap: map[TxnType]uint64{ + TxnTypeAuthorizeDerivedKey: 1, + }, + StakeLimitMap: map[StakeLimitKey]*uint256.Int{stakeLimitKey: uint256.NewInt().SetUint64(100)}, + } + derivedKeyPriv, err = _submitAuthorizeDerivedKeyTxn(txnSpendingLimit) + require.Error(t, err) + } + { + // RuleErrorTransactionSpendingLimitInvalidValidator + // sender tries to create a DerivedKey to stake with m2. Validator doesn't exist. Errors. + stakeLimitKey := MakeStakeLimitKey(m2PKID, senderPKID) + txnSpendingLimit := &TransactionSpendingLimit{ + GlobalDESOLimit: NanosPerUnit, // 1 $DESO spending limit + TransactionCountLimitMap: map[TxnType]uint64{ + TxnTypeAuthorizeDerivedKey: 1, + }, + StakeLimitMap: map[StakeLimitKey]*uint256.Int{stakeLimitKey: uint256.NewInt().SetUint64(100)}, + } + derivedKeyPriv, err = _submitAuthorizeDerivedKeyTxn(txnSpendingLimit) + require.Error(t, err) + } + { + // RuleErrorTransactionSpendingLimitInvalidStaker + // sender tries to create a DerivedKey that would allow + // m1 to unstake 100 $DESO nanos from m0. Errors. + stakeLimitKey := MakeStakeLimitKey(m0PKID, m1PKID) + txnSpendingLimit := &TransactionSpendingLimit{ + GlobalDESOLimit: NanosPerUnit, // 1 $DESO spending limit + TransactionCountLimitMap: map[TxnType]uint64{ + TxnTypeAuthorizeDerivedKey: 1, + }, + UnstakeLimitMap: map[StakeLimitKey]*uint256.Int{stakeLimitKey: uint256.NewInt().SetUint64(100)}, + } + derivedKeyPriv, err = _submitAuthorizeDerivedKeyTxn(txnSpendingLimit) + require.Error(t, err) + } + { + // RuleErrorTransactionSpendingLimitInvalidValidator + // sender tries to create a DerivedKey to unstake from m2. Validator doesn't exist. Errors. + stakeLimitKey := MakeStakeLimitKey(m2PKID, senderPKID) + txnSpendingLimit := &TransactionSpendingLimit{ + GlobalDESOLimit: NanosPerUnit, // 1 $DESO spending limit + TransactionCountLimitMap: map[TxnType]uint64{ + TxnTypeAuthorizeDerivedKey: 1, + }, + UnstakeLimitMap: map[StakeLimitKey]*uint256.Int{stakeLimitKey: uint256.NewInt().SetUint64(100)}, + } + derivedKeyPriv, err = _submitAuthorizeDerivedKeyTxn(txnSpendingLimit) + require.Error(t, err) + } + { + // RuleErrorTransactionSpendingLimitInvalidStaker + // sender tries to create a DerivedKey that would allow + // m1 to unlock stake from m0. Errors. + stakeLimitKey := MakeStakeLimitKey(m0PKID, m1PKID) + txnSpendingLimit := &TransactionSpendingLimit{ + GlobalDESOLimit: NanosPerUnit, // 1 $DESO spending limit + TransactionCountLimitMap: map[TxnType]uint64{ + TxnTypeAuthorizeDerivedKey: 1, + }, + UnlockStakeLimitMap: map[StakeLimitKey]uint64{stakeLimitKey: 100}, + } + derivedKeyPriv, err = _submitAuthorizeDerivedKeyTxn(txnSpendingLimit) + require.Error(t, err) + } + { + // RuleErrorTransactionSpendingLimitInvalidValidator + // sender tries to create a DerivedKey to stake with m2. Validator doesn't exist. Errors. + stakeLimitKey := MakeStakeLimitKey(m2PKID, senderPKID) + txnSpendingLimit := &TransactionSpendingLimit{ + GlobalDESOLimit: NanosPerUnit, // 1 $DESO spending limit + TransactionCountLimitMap: map[TxnType]uint64{ + TxnTypeAuthorizeDerivedKey: 1, + }, + UnlockStakeLimitMap: map[StakeLimitKey]uint64{stakeLimitKey: 100}, + } + derivedKeyPriv, err = _submitAuthorizeDerivedKeyTxn(txnSpendingLimit) + require.Error(t, err) + } + { + // sender stakes with m0 using a DerivedKey. + + // sender creates a DerivedKey to stake up to 100 $DESO nanos with m0. + stakeLimitKey := MakeStakeLimitKey(m0PKID, senderPKID) + txnSpendingLimit := &TransactionSpendingLimit{ + GlobalDESOLimit: NanosPerUnit, // 1 $DESO spending limit + TransactionCountLimitMap: map[TxnType]uint64{ + TxnTypeAuthorizeDerivedKey: 1, + }, + StakeLimitMap: map[StakeLimitKey]*uint256.Int{stakeLimitKey: uint256.NewInt().SetUint64(100)}, + } + derivedKeyPriv, err = _submitAuthorizeDerivedKeyTxn(txnSpendingLimit) + require.NoError(t, err) + + // sender tries to stake 100 $DESO nanos with m1 using the DerivedKey. Errors. + stakeMetadata := &StakeMetadata{ + ValidatorPublicKey: NewPublicKey(m1PkBytes), + StakeAmountNanos: uint256.NewInt().SetUint64(100), + } + _, err = _submitStakeTxnWithDerivedKey( + senderPkBytes, derivedKeyPriv, MsgDeSoTxn{TxnMeta: stakeMetadata}, + ) + require.Error(t, err) + require.Contains(t, err.Error(), RuleErrorStakeTransactionSpendingLimitNotFound) + + // sender tries to stake 200 $DESO nanos with m0 using the DerivedKey. Errors. + stakeMetadata = &StakeMetadata{ + ValidatorPublicKey: NewPublicKey(m0PkBytes), + StakeAmountNanos: uint256.NewInt().SetUint64(200), + } + _, err = _submitStakeTxnWithDerivedKey( + senderPkBytes, derivedKeyPriv, MsgDeSoTxn{TxnMeta: stakeMetadata}, + ) + require.Error(t, err) + require.Contains(t, err.Error(), RuleErrorStakeTransactionSpendingLimitExceeded) + + // sender stakes 100 $DESO nanos with m0 using the DerivedKey. Succeeds. + senderOldDESOBalanceNanos := getDESOBalanceNanos(senderPkBytes) + stakeMetadata = &StakeMetadata{ + ValidatorPublicKey: NewPublicKey(m0PkBytes), + StakeAmountNanos: uint256.NewInt().SetUint64(100), + } + feeNanos, err := _submitStakeTxnWithDerivedKey( + senderPkBytes, derivedKeyPriv, MsgDeSoTxn{TxnMeta: stakeMetadata}, + ) + require.NoError(t, err) + + // StakeEntry was created. + stakeEntry, err := newUtxoView().GetStakeEntry(m0PKID, senderPKID) + require.NoError(t, err) + require.NotNil(t, stakeEntry) + require.Equal(t, stakeEntry.StakeAmountNanos, uint256.NewInt().SetUint64(100)) + + // Verify sender's DESO balance is reduced by StakeAmountNanos (net of fees). + senderNewDESOBalanceNanos := getDESOBalanceNanos(senderPkBytes) + require.Equal(t, senderOldDESOBalanceNanos-feeNanos-stakeMetadata.StakeAmountNanos.Uint64(), senderNewDESOBalanceNanos) + } + { + // sender unstakes from m0 using a DerivedKey. + + // sender creates a DerivedKey to unstake up to 50 $DESO nanos from m0. + stakeLimitKey := MakeStakeLimitKey(m0PKID, senderPKID) + txnSpendingLimit := &TransactionSpendingLimit{ + GlobalDESOLimit: NanosPerUnit, // 1 $DESO spending limit + TransactionCountLimitMap: map[TxnType]uint64{ + TxnTypeAuthorizeDerivedKey: 1, + }, + UnstakeLimitMap: map[StakeLimitKey]*uint256.Int{stakeLimitKey: uint256.NewInt().SetUint64(50)}, + } + derivedKeyPriv, err = _submitAuthorizeDerivedKeyTxn(txnSpendingLimit) + require.NoError(t, err) + + // sender tries to unstake 50 $DESO nanos from m1 using the DerivedKey. Errors. + unstakeMetadata := &UnstakeMetadata{ + ValidatorPublicKey: NewPublicKey(m1PkBytes), + UnstakeAmountNanos: uint256.NewInt().SetUint64(50), + } + _, err = _submitStakeTxnWithDerivedKey( + senderPkBytes, derivedKeyPriv, MsgDeSoTxn{TxnMeta: unstakeMetadata}, + ) + require.Error(t, err) + require.Contains(t, err.Error(), RuleErrorInvalidUnstakeNoStakeFound) + + // sender stakes 50 $DESO nanos with m1. + stakeMetadata := &StakeMetadata{ + ValidatorPublicKey: NewPublicKey(m1PkBytes), + StakeAmountNanos: uint256.NewInt().SetUint64(50), + } + _, err = _submitStakeTxn( + testMeta, senderPkString, senderPrivString, stakeMetadata, nil, true, + ) + require.NoError(t, err) + + // sender tries to unstake 50 $DESO nanos from m1 using the DerivedKey. Errors. + unstakeMetadata = &UnstakeMetadata{ + ValidatorPublicKey: NewPublicKey(m1PkBytes), + UnstakeAmountNanos: uint256.NewInt().SetUint64(50), + } + _, err = _submitStakeTxnWithDerivedKey( + senderPkBytes, derivedKeyPriv, MsgDeSoTxn{TxnMeta: unstakeMetadata}, + ) + require.Error(t, err) + require.Contains(t, err.Error(), RuleErrorUnstakeTransactionSpendingLimitNotFound) + + // sender tries to unstake 200 $DESO nanos from m0 using the DerivedKey. Errors. + unstakeMetadata = &UnstakeMetadata{ + ValidatorPublicKey: NewPublicKey(m0PkBytes), + UnstakeAmountNanos: uint256.NewInt().SetUint64(200), + } + _, err = _submitStakeTxnWithDerivedKey( + senderPkBytes, derivedKeyPriv, MsgDeSoTxn{TxnMeta: unstakeMetadata}, + ) + require.Error(t, err) + require.Contains(t, err.Error(), RuleErrorInvalidUnstakeInsufficientStakeFound) + + // sender tries to unstake 100 $DESO nanos from m0 using the DerivedKey. Errors. + unstakeMetadata = &UnstakeMetadata{ + ValidatorPublicKey: NewPublicKey(m0PkBytes), + UnstakeAmountNanos: uint256.NewInt().SetUint64(100), + } + _, err = _submitStakeTxnWithDerivedKey( + senderPkBytes, derivedKeyPriv, MsgDeSoTxn{TxnMeta: unstakeMetadata}, + ) + require.Error(t, err) + require.Contains(t, err.Error(), RuleErrorUnstakeTransactionSpendingLimitExceeded) + + // sender unstakes 50 $DESO nanos from m0 using the DerivedKey. Succeeds. + unstakeMetadata = &UnstakeMetadata{ + ValidatorPublicKey: NewPublicKey(m0PkBytes), + UnstakeAmountNanos: uint256.NewInt().SetUint64(50), + } + _, err = _submitStakeTxnWithDerivedKey( + senderPkBytes, derivedKeyPriv, MsgDeSoTxn{TxnMeta: unstakeMetadata}, + ) + require.NoError(t, err) + + // StakeEntry was updated. + stakeEntry, err := newUtxoView().GetStakeEntry(m0PKID, senderPKID) + require.NoError(t, err) + require.NotNil(t, stakeEntry) + require.Equal(t, stakeEntry.StakeAmountNanos, uint256.NewInt().SetUint64(50)) + + // LockedStakeEntry was created. + epochNumber := uint64(0) // TODO: get epoch number from db. + lockedStakeEntry, err := newUtxoView().GetLockedStakeEntry(m0PKID, senderPKID, epochNumber) + require.NoError(t, err) + require.NotNil(t, lockedStakeEntry) + require.Equal(t, lockedStakeEntry.LockedAmountNanos, uint256.NewInt().SetUint64(50)) + } + { + // sender unlocks stake using a DerivedKey. + + // sender creates a DerivedKey to perform 1 unlock stake operation with m0. + stakeLimitKey := MakeStakeLimitKey(m0PKID, senderPKID) + txnSpendingLimit := &TransactionSpendingLimit{ + GlobalDESOLimit: NanosPerUnit, // 1 $DESO spending limit + TransactionCountLimitMap: map[TxnType]uint64{ + TxnTypeAuthorizeDerivedKey: 1, + }, + UnlockStakeLimitMap: map[StakeLimitKey]uint64{stakeLimitKey: 1}, + } + derivedKeyPriv, err = _submitAuthorizeDerivedKeyTxn(txnSpendingLimit) + require.NoError(t, err) + + // sender tries to unlock all stake from m1 using the DerivedKey. Errors. + epochNumber := uint64(0) // TODO: get epoch number from db. + unlockStakeMetadata := &UnlockStakeMetadata{ + ValidatorPublicKey: NewPublicKey(m1PkBytes), + StartEpochNumber: epochNumber, + EndEpochNumber: epochNumber, + } + _, err = _submitStakeTxnWithDerivedKey( + senderPkBytes, derivedKeyPriv, MsgDeSoTxn{TxnMeta: unlockStakeMetadata}, + ) + require.Error(t, err) + require.Contains(t, err.Error(), RuleErrorInvalidUnlockStakeNoUnlockableStakeFound) + + // sender unstakes 50 $DESO nanos from m1. + unstakeMetadata := &UnstakeMetadata{ + ValidatorPublicKey: NewPublicKey(m1PkBytes), + UnstakeAmountNanos: uint256.NewInt().SetUint64(50), + } + _, err = _submitUnstakeTxn( + testMeta, senderPkString, senderPrivString, unstakeMetadata, nil, true, + ) + require.NoError(t, err) + + // sender tries to unlock all stake from m1 using the DerivedKey. Errors. + unlockStakeMetadata = &UnlockStakeMetadata{ + ValidatorPublicKey: NewPublicKey(m1PkBytes), + StartEpochNumber: epochNumber, + EndEpochNumber: epochNumber, + } + _, err = _submitStakeTxnWithDerivedKey( + senderPkBytes, derivedKeyPriv, MsgDeSoTxn{TxnMeta: unlockStakeMetadata}, + ) + require.Error(t, err) + require.Contains(t, err.Error(), RuleErrorUnlockStakeTransactionSpendingLimitNotFound) + + // sender unlocks all stake from m0 using the DerivedKey. Succeeds. + senderOldDESOBalanceNanos := getDESOBalanceNanos(senderPkBytes) + unlockStakeMetadata = &UnlockStakeMetadata{ + ValidatorPublicKey: NewPublicKey(m0PkBytes), + StartEpochNumber: epochNumber, + EndEpochNumber: epochNumber, + } + feeNanos, err := _submitStakeTxnWithDerivedKey( + senderPkBytes, derivedKeyPriv, MsgDeSoTxn{TxnMeta: unlockStakeMetadata}, + ) + require.NoError(t, err) + + // LockedStakeEntry was deleted. + lockedStakeEntry, err := newUtxoView().GetLockedStakeEntry(m0PKID, senderPKID, epochNumber) + require.NoError(t, err) + require.Nil(t, lockedStakeEntry) + + // Verify sender's DESO balance was increased by 50 DESO nanos (net of fees). + senderNewDESOBalanceNanos := getDESOBalanceNanos(senderPkBytes) + require.Equal(t, senderOldDESOBalanceNanos-feeNanos+uint64(50), senderNewDESOBalanceNanos) + + // sender stakes + unstakes 50 $DESO nanos with m0. + stakeMetadata := &StakeMetadata{ + ValidatorPublicKey: NewPublicKey(m0PkBytes), + StakeAmountNanos: uint256.NewInt().SetUint64(50), + } + _, err = _submitStakeTxn( + testMeta, senderPkString, senderPrivString, stakeMetadata, nil, true, + ) + require.NoError(t, err) + unstakeMetadata = &UnstakeMetadata{ + ValidatorPublicKey: NewPublicKey(m0PkBytes), + UnstakeAmountNanos: uint256.NewInt().SetUint64(50), + } + _, err = _submitUnstakeTxn( + testMeta, senderPkString, senderPrivString, unstakeMetadata, nil, true, + ) + require.NoError(t, err) + + // sender tries to unlock all stake from m0 using the DerivedKey. Errors. + unlockStakeMetadata = &UnlockStakeMetadata{ + ValidatorPublicKey: NewPublicKey(m0PkBytes), + StartEpochNumber: epochNumber, + EndEpochNumber: epochNumber, + } + _, err = _submitStakeTxnWithDerivedKey( + senderPkBytes, derivedKeyPriv, MsgDeSoTxn{TxnMeta: unlockStakeMetadata}, + ) + require.Error(t, err) + require.Contains(t, err.Error(), RuleErrorUnlockStakeTransactionSpendingLimitNotFound) + } + { + // sender stakes, unstakes, and unlocks stake using a DerivedKey scoped to any validator. + + // sender creates a DerivedKey that can stake, unstake, and unlock stake with any validator. + stakeLimitKey := MakeStakeLimitKey(&ZeroPKID, senderPKID) + txnSpendingLimit := &TransactionSpendingLimit{ + GlobalDESOLimit: NanosPerUnit, // 1 $DESO spending limit + TransactionCountLimitMap: map[TxnType]uint64{ + TxnTypeAuthorizeDerivedKey: 1, + }, + StakeLimitMap: map[StakeLimitKey]*uint256.Int{stakeLimitKey: uint256.NewInt().SetUint64(50)}, + UnstakeLimitMap: map[StakeLimitKey]*uint256.Int{stakeLimitKey: uint256.NewInt().SetUint64(50)}, + UnlockStakeLimitMap: map[StakeLimitKey]uint64{stakeLimitKey: 2}, + } + derivedKeyPriv, err = _submitAuthorizeDerivedKeyTxn(txnSpendingLimit) + require.NoError(t, err) + + // sender stakes with m0 using the DerivedKey. + stakeMetadata := &StakeMetadata{ + ValidatorPublicKey: NewPublicKey(m0PkBytes), + StakeAmountNanos: uint256.NewInt().SetUint64(25), + } + _, err = _submitStakeTxn( + testMeta, senderPkString, senderPrivString, stakeMetadata, nil, true, + ) + require.NoError(t, err) + + // sender stakes with m1 using the DerivedKey. + stakeMetadata = &StakeMetadata{ + ValidatorPublicKey: NewPublicKey(m1PkBytes), + StakeAmountNanos: uint256.NewInt().SetUint64(25), + } + _, err = _submitStakeTxn( + testMeta, senderPkString, senderPrivString, stakeMetadata, nil, true, + ) + require.NoError(t, err) + + // sender unstakes from m0 using the DerivedKey. + unstakeMetadata := &UnstakeMetadata{ + ValidatorPublicKey: NewPublicKey(m0PkBytes), + UnstakeAmountNanos: uint256.NewInt().SetUint64(25), + } + _, err = _submitUnstakeTxn( + testMeta, senderPkString, senderPrivString, unstakeMetadata, nil, true, + ) + require.NoError(t, err) + + // sender unstakes from m1 using the DerivedKey. + unstakeMetadata = &UnstakeMetadata{ + ValidatorPublicKey: NewPublicKey(m1PkBytes), + UnstakeAmountNanos: uint256.NewInt().SetUint64(25), + } + _, err = _submitUnstakeTxn( + testMeta, senderPkString, senderPrivString, unstakeMetadata, nil, true, + ) + require.NoError(t, err) + + // sender unlocks stake from m0 using the DerivedKey. + epochNumber := uint64(0) // TODO: get epoch number from db. + unlockStakeMetadata := &UnlockStakeMetadata{ + ValidatorPublicKey: NewPublicKey(m0PkBytes), + StartEpochNumber: epochNumber, + EndEpochNumber: epochNumber, + } + _, err = _submitStakeTxnWithDerivedKey( + senderPkBytes, derivedKeyPriv, MsgDeSoTxn{TxnMeta: unlockStakeMetadata}, + ) + require.NoError(t, err) + + // sender unlocks stake from m1 using the DerivedKey. + unlockStakeMetadata = &UnlockStakeMetadata{ + ValidatorPublicKey: NewPublicKey(m1PkBytes), + StartEpochNumber: epochNumber, + EndEpochNumber: epochNumber, + } + _, err = _submitStakeTxnWithDerivedKey( + senderPkBytes, derivedKeyPriv, MsgDeSoTxn{TxnMeta: unlockStakeMetadata}, + ) + require.NoError(t, err) + } + { + // sender stakes, unstakes, and unlocks stake using an IsUnlimited DerivedKey. + + // sender creates an IsUnlimited DerivedKey. + txnSpendingLimit := &TransactionSpendingLimit{ + GlobalDESOLimit: 0, + IsUnlimited: true, + } + derivedKeyPriv, err = _submitAuthorizeDerivedKeyTxn(txnSpendingLimit) + require.NoError(t, err) + + // sender stakes with m0 using the DerivedKey. + stakeMetadata := &StakeMetadata{ + ValidatorPublicKey: NewPublicKey(m0PkBytes), + StakeAmountNanos: uint256.NewInt().SetUint64(25), + } + _, err = _submitStakeTxn( + testMeta, senderPkString, senderPrivString, stakeMetadata, nil, true, + ) + require.NoError(t, err) + + // sender stakes with m1 using the DerivedKey. + stakeMetadata = &StakeMetadata{ + ValidatorPublicKey: NewPublicKey(m1PkBytes), + StakeAmountNanos: uint256.NewInt().SetUint64(25), + } + _, err = _submitStakeTxn( + testMeta, senderPkString, senderPrivString, stakeMetadata, nil, true, + ) + require.NoError(t, err) + + // sender unstakes from m0 using the DerivedKey. + unstakeMetadata := &UnstakeMetadata{ + ValidatorPublicKey: NewPublicKey(m0PkBytes), + UnstakeAmountNanos: uint256.NewInt().SetUint64(25), + } + _, err = _submitUnstakeTxn( + testMeta, senderPkString, senderPrivString, unstakeMetadata, nil, true, + ) + require.NoError(t, err) + + // sender unstakes from m1 using the DerivedKey. + unstakeMetadata = &UnstakeMetadata{ + ValidatorPublicKey: NewPublicKey(m1PkBytes), + UnstakeAmountNanos: uint256.NewInt().SetUint64(25), + } + _, err = _submitUnstakeTxn( + testMeta, senderPkString, senderPrivString, unstakeMetadata, nil, true, + ) + require.NoError(t, err) + + // sender unlocks stake from m0 using the DerivedKey. + epochNumber := uint64(0) // TODO: get epoch number from db. + unlockStakeMetadata := &UnlockStakeMetadata{ + ValidatorPublicKey: NewPublicKey(m0PkBytes), + StartEpochNumber: epochNumber, + EndEpochNumber: epochNumber, + } + _, err = _submitStakeTxnWithDerivedKey( + senderPkBytes, derivedKeyPriv, MsgDeSoTxn{TxnMeta: unlockStakeMetadata}, + ) + require.NoError(t, err) + + // sender unlocks stake from m1 using the DerivedKey. + unlockStakeMetadata = &UnlockStakeMetadata{ + ValidatorPublicKey: NewPublicKey(m1PkBytes), + StartEpochNumber: epochNumber, + EndEpochNumber: epochNumber, + } + _, err = _submitStakeTxnWithDerivedKey( + senderPkBytes, derivedKeyPriv, MsgDeSoTxn{TxnMeta: unlockStakeMetadata}, + ) + require.NoError(t, err) + } + { + // sender exhausts a TransactionSpendingLimit scoped to a single validator. + // We fall back to check if there is a TransactionSpendingLimit scoped to + // any validator to cover their staking + unstaking + unlocking stake txns. + + // sender creates a DerivedKey to stake, unstake, and unlock stake with m1 or any validator. + scopedStakeLimitKey := MakeStakeLimitKey(m1PKID, senderPKID) + globalStakeLimitKey := MakeStakeLimitKey(&ZeroPKID, senderPKID) + txnSpendingLimit := &TransactionSpendingLimit{ + GlobalDESOLimit: NanosPerUnit, // 1 $DESO spending limit + TransactionCountLimitMap: map[TxnType]uint64{ + TxnTypeAuthorizeDerivedKey: 1, + }, + StakeLimitMap: map[StakeLimitKey]*uint256.Int{ + scopedStakeLimitKey: uint256.NewInt().SetUint64(100), + globalStakeLimitKey: uint256.NewInt().SetUint64(200), + }, + UnstakeLimitMap: map[StakeLimitKey]*uint256.Int{ + scopedStakeLimitKey: uint256.NewInt().SetUint64(100), + globalStakeLimitKey: uint256.NewInt().SetUint64(200), + }, + UnlockStakeLimitMap: map[StakeLimitKey]uint64{scopedStakeLimitKey: 1, globalStakeLimitKey: 1}, + } + derivedKeyPriv, err = _submitAuthorizeDerivedKeyTxn(txnSpendingLimit) + require.NoError(t, err) + + // sender stakes with m1 using the global TransactionSpendingLimit. + stakeMetadata := &StakeMetadata{ + ValidatorPublicKey: NewPublicKey(m1PkBytes), + StakeAmountNanos: uint256.NewInt().SetUint64(200), + } + _, err = _submitStakeTxnWithDerivedKey( + senderPkBytes, derivedKeyPriv, MsgDeSoTxn{TxnMeta: stakeMetadata}, + ) + require.NoError(t, err) + + // sender unstakes from m1 using the global TransactionSpendingLimit. + unstakeMetadata := &UnstakeMetadata{ + ValidatorPublicKey: NewPublicKey(m1PkBytes), + UnstakeAmountNanos: uint256.NewInt().SetUint64(200), + } + _, err = _submitStakeTxnWithDerivedKey( + senderPkBytes, derivedKeyPriv, MsgDeSoTxn{TxnMeta: unstakeMetadata}, + ) + require.NoError(t, err) + + // sender unlocks stake from m1 using the scoped TransactionSpendingLimit. + epochNumber := uint64(0) // TODO: get epoch number from db. + unlockStakeMetadata := &UnlockStakeMetadata{ + ValidatorPublicKey: NewPublicKey(m1PkBytes), + StartEpochNumber: epochNumber, + EndEpochNumber: epochNumber, + } + _, err = _submitStakeTxnWithDerivedKey( + senderPkBytes, derivedKeyPriv, MsgDeSoTxn{TxnMeta: unlockStakeMetadata}, + ) + require.NoError(t, err) + + // sender stakes with m1 using the scoped TransactionSpendingLimit. + stakeMetadata = &StakeMetadata{ + ValidatorPublicKey: NewPublicKey(m1PkBytes), + StakeAmountNanos: uint256.NewInt().SetUint64(100), + } + _, err = _submitStakeTxnWithDerivedKey( + senderPkBytes, derivedKeyPriv, MsgDeSoTxn{TxnMeta: stakeMetadata}, + ) + require.NoError(t, err) + + // sender unstakes from m1 using the scoped TransactionSpendingLimit. + unstakeMetadata = &UnstakeMetadata{ + ValidatorPublicKey: NewPublicKey(m1PkBytes), + UnstakeAmountNanos: uint256.NewInt().SetUint64(100), + } + _, err = _submitStakeTxnWithDerivedKey( + senderPkBytes, derivedKeyPriv, MsgDeSoTxn{TxnMeta: unstakeMetadata}, + ) + require.NoError(t, err) + + // sender unlocks stake from m1 using the global TransactionSpendingLimit. + epochNumber = uint64(0) // TODO: get epoch number from db. + unlockStakeMetadata = &UnlockStakeMetadata{ + ValidatorPublicKey: NewPublicKey(m1PkBytes), + StartEpochNumber: epochNumber, + EndEpochNumber: epochNumber, + } + _, err = _submitStakeTxnWithDerivedKey( + senderPkBytes, derivedKeyPriv, MsgDeSoTxn{TxnMeta: unlockStakeMetadata}, + ) + require.NoError(t, err) + } + { + // Test TransactionSpendingLimit.ToMetamaskString() scoped to one validator. + stakeLimitKey1 := MakeStakeLimitKey(m0PKID, senderPKID) + stakeLimitKey2 := MakeStakeLimitKey(m1PKID, senderPKID) + txnSpendingLimit := &TransactionSpendingLimit{ + GlobalDESOLimit: NanosPerUnit, // 1 $DESO spending limit + TransactionCountLimitMap: map[TxnType]uint64{ + TxnTypeAuthorizeDerivedKey: 1, + }, + StakeLimitMap: map[StakeLimitKey]*uint256.Int{ + stakeLimitKey1: uint256.NewInt().SetUint64(uint64(1.5 * float64(NanosPerUnit))), + stakeLimitKey2: uint256.NewInt().SetUint64(uint64(2.0 * float64(NanosPerUnit))), + }, + UnstakeLimitMap: map[StakeLimitKey]*uint256.Int{ + stakeLimitKey1: uint256.NewInt().SetUint64(uint64(3.25 * float64(NanosPerUnit))), + }, + UnlockStakeLimitMap: map[StakeLimitKey]uint64{stakeLimitKey1: 2, stakeLimitKey2: 3}, + } + metamaskStr := txnSpendingLimit.ToMetamaskString(params) + require.Equal(t, metamaskStr, + "Spending limits on the derived key:\n"+ + "\tTotal $DESO Limit: 1.0 $DESO\n"+ + "\tTransaction Count Limit: \n"+ + "\t\tAUTHORIZE_DERIVED_KEY: 1\n"+ + "\tStaking Restrictions:\n"+ + "\t\t[\n"+ + "\t\t\tValidator PKID: "+m0Pub+"\n"+ + "\t\t\tStaker PKID: "+senderPkString+"\n"+ + "\t\t\tStaking Limit: 1.50 $DESO\n"+ + "\t\t]\n"+ + "\t\t[\n"+ + "\t\t\tValidator PKID: "+m1Pub+"\n"+ + "\t\t\tStaker PKID: "+senderPkString+"\n"+ + "\t\t\tStaking Limit: 2.00 $DESO\n"+ + "\t\t]\n"+ + "\tUnstaking Restrictions:\n"+ + "\t\t[\n"+ + "\t\t\tValidator PKID: "+m0Pub+"\n"+ + "\t\t\tStaker PKID: "+senderPkString+"\n"+ + "\t\t\tUnstaking Limit: 3.25 $DESO\n"+ + "\t\t]\n"+ + "\tUnlocking Stake Restrictions:\n"+ + "\t\t[\n"+ + "\t\t\tValidator PKID: "+m0Pub+"\n"+ + "\t\t\tStaker PKID: "+senderPkString+"\n"+ + "\t\t\tTransaction Count: 2\n"+ + "\t\t]\n"+ + "\t\t[\n"+ + "\t\t\tValidator PKID: "+m1Pub+"\n"+ + "\t\t\tStaker PKID: "+senderPkString+"\n"+ + "\t\t\tTransaction Count: 3\n"+ + "\t\t]\n", + ) + } + { + // Test TransactionSpendingLimit.ToMetamaskString() scoped to any validator. + stakeLimitKey := MakeStakeLimitKey(&ZeroPKID, senderPKID) + txnSpendingLimit := &TransactionSpendingLimit{ + GlobalDESOLimit: NanosPerUnit, // 1 $DESO spending limit + TransactionCountLimitMap: map[TxnType]uint64{ + TxnTypeAuthorizeDerivedKey: 1, + }, + StakeLimitMap: map[StakeLimitKey]*uint256.Int{ + stakeLimitKey: uint256.NewInt().SetUint64(uint64(0.65 * float64(NanosPerUnit))), + }, + UnstakeLimitMap: map[StakeLimitKey]*uint256.Int{ + stakeLimitKey: uint256.NewInt().SetUint64(uint64(2.1 * float64(NanosPerUnit))), + }, + UnlockStakeLimitMap: map[StakeLimitKey]uint64{stakeLimitKey: 1}, + } + metamaskStr := txnSpendingLimit.ToMetamaskString(params) + require.Equal(t, metamaskStr, + "Spending limits on the derived key:\n"+ + "\tTotal $DESO Limit: 1.0 $DESO\n"+ + "\tTransaction Count Limit: \n"+ + "\t\tAUTHORIZE_DERIVED_KEY: 1\n"+ + "\tStaking Restrictions:\n"+ + "\t\t[\n"+ + "\t\t\tValidator PKID: Any\n"+ + "\t\t\tStaker PKID: "+senderPkString+"\n"+ + "\t\t\tStaking Limit: 0.65 $DESO\n"+ + "\t\t]\n"+ + "\tUnstaking Restrictions:\n"+ + "\t\t[\n"+ + "\t\t\tValidator PKID: Any\n"+ + "\t\t\tStaker PKID: "+senderPkString+"\n"+ + "\t\t\tUnstaking Limit: 2.10 $DESO\n"+ + "\t\t]\n"+ + "\tUnlocking Stake Restrictions:\n"+ + "\t\t[\n"+ + "\t\t\tValidator PKID: Any\n"+ + "\t\t\tStaker PKID: "+senderPkString+"\n"+ + "\t\t\tTransaction Count: 1\n"+ + "\t\t]\n", + ) + } + + // Flush mempool to the db and test rollbacks. + require.NoError(t, mempool.universalUtxoView.FlushToDb(blockHeight)) + _executeAllTestRollbackAndFlush(testMeta) +} + +func TestGetLockedStakeEntriesInRange(t *testing.T) { + // For this test, we manually place LockedStakeEntries in the database and + // UtxoView to test merging the two to GetLockedStakeEntriesInRange. + + // Initialize test chain and UtxoView. + chain, params, db := NewLowDifficultyBlockchain(t) + utxoView, err := NewUtxoView(db, params, chain.postgres, chain.snapshot) + require.NoError(t, err) + blockHeight := uint64(chain.blockTip().Height + 1) + + m0PKID := DBGetPKIDEntryForPublicKey(db, chain.snapshot, m0PkBytes).PKID + m1PKID := DBGetPKIDEntryForPublicKey(db, chain.snapshot, m1PkBytes).PKID + + // Set a LockedStakeEntry in the db. + lockedStakeEntry := &LockedStakeEntry{ + ValidatorPKID: m0PKID, + StakerPKID: m0PKID, + LockedAtEpochNumber: 1, + } + utxoView._setLockedStakeEntryMappings(lockedStakeEntry) + require.NoError(t, utxoView.FlushToDb(blockHeight)) + + // Verify LockedStakeEntry is in the db. + lockedStakeEntry, err = DBGetLockedStakeEntry(db, chain.snapshot, m0PKID, m0PKID, 1) + require.NoError(t, err) + require.NotNil(t, lockedStakeEntry) + + // Verify LockedStakeEntry is not in the UtxoView. + require.Empty(t, utxoView.LockedStakeMapKeyToLockedStakeEntry) + + // Set another LockedStakeEntry in the db. + lockedStakeEntry = &LockedStakeEntry{ + ValidatorPKID: m0PKID, + StakerPKID: m0PKID, + LockedAtEpochNumber: 2, + } + utxoView._setLockedStakeEntryMappings(lockedStakeEntry) + require.NoError(t, utxoView.FlushToDb(blockHeight)) + + // Fetch the LockedStakeEntry so it is also cached in the UtxoView. + lockedStakeEntry, err = utxoView.GetLockedStakeEntry(m0PKID, m0PKID, 2) + require.NoError(t, err) + require.NotNil(t, lockedStakeEntry) + + // Verify the LockedStakeEntry is in the db. + lockedStakeEntry, err = DBGetLockedStakeEntry(db, chain.snapshot, m0PKID, m0PKID, 2) + require.NoError(t, err) + require.NotNil(t, lockedStakeEntry) + + // Verify the LockedStakeEntry is also in the UtxoView. + require.Len(t, utxoView.LockedStakeMapKeyToLockedStakeEntry, 1) + require.NotNil(t, utxoView.LockedStakeMapKeyToLockedStakeEntry[lockedStakeEntry.ToMapKey()]) + + // Set another LockedStakeEntry in the UtxoView. + utxoViewLockedStakeEntry := &LockedStakeEntry{ + ValidatorPKID: m0PKID, + StakerPKID: m0PKID, + LockedAtEpochNumber: 3, + } + utxoView._setLockedStakeEntryMappings(utxoViewLockedStakeEntry) + + // Verify the LockedStakeEntry is not in the db. + lockedStakeEntry, err = DBGetLockedStakeEntry(db, chain.snapshot, m0PKID, m0PKID, 3) + require.NoError(t, err) + require.Nil(t, lockedStakeEntry) + + // Verify the LockedStakeEntry is in the UtxoView. + require.Len(t, utxoView.LockedStakeMapKeyToLockedStakeEntry, 2) + require.NotNil(t, utxoView.LockedStakeMapKeyToLockedStakeEntry[utxoViewLockedStakeEntry.ToMapKey()]) + + // Verify GetLockedStakeEntriesInRange. + lockedStakeEntries, err := utxoView.GetLockedStakeEntriesInRange(m0PKID, m0PKID, 1, 3) + require.NoError(t, err) + require.Len(t, lockedStakeEntries, 3) + require.Equal(t, lockedStakeEntries[0].LockedAtEpochNumber, uint64(1)) + require.Equal(t, lockedStakeEntries[1].LockedAtEpochNumber, uint64(2)) + require.Equal(t, lockedStakeEntries[2].LockedAtEpochNumber, uint64(3)) + + // A few more edge case tests for GetLockedStakeEntriesInRange. + lockedStakeEntries, err = utxoView.GetLockedStakeEntriesInRange(m0PKID, m0PKID, 0, 4) + require.NoError(t, err) + require.Len(t, lockedStakeEntries, 3) + + // Nil ValidatorPKID. + lockedStakeEntries, err = utxoView.GetLockedStakeEntriesInRange(nil, m0PKID, 1, 3) + require.Error(t, err) + require.Contains(t, err.Error(), "nil ValidatorPKID provided as input") + + // Nil StakerPKID. + lockedStakeEntries, err = utxoView.GetLockedStakeEntriesInRange(m0PKID, nil, 1, 3) + require.Error(t, err) + require.Contains(t, err.Error(), "nil StakerPKID provided as input") + + // StartEpochNumber > EndEpochNumber. + lockedStakeEntries, err = utxoView.GetLockedStakeEntriesInRange(m0PKID, m0PKID, 3, 1) + require.Error(t, err) + require.Contains(t, err.Error(), "invalid LockedAtEpochNumber range provided as input") + + // None found for this ValidatorPKID. + lockedStakeEntries, err = utxoView.GetLockedStakeEntriesInRange(m1PKID, m0PKID, 1, 3) + require.NoError(t, err) + require.Empty(t, lockedStakeEntries) + + // None found for this StakerPKID. + lockedStakeEntries, err = utxoView.GetLockedStakeEntriesInRange(m0PKID, m1PKID, 1, 3) + require.NoError(t, err) + require.Empty(t, lockedStakeEntries) + + // None found for this LockedAtEpochNumber range. + lockedStakeEntries, err = utxoView.GetLockedStakeEntriesInRange(m0PKID, m0PKID, 5, 6) + require.NoError(t, err) + require.Empty(t, lockedStakeEntries) +} diff --git a/lib/block_view_types.go b/lib/block_view_types.go index 3aca53fff..4c6e3fd2f 100644 --- a/lib/block_view_types.go +++ b/lib/block_view_types.go @@ -111,9 +111,11 @@ const ( EncoderTypeDeSoNonce EncoderType = 38 EncoderTypeTransactorNonceEntry EncoderType = 39 EncoderTypeValidatorEntry EncoderType = 40 + EncoderTypeStakeEntry EncoderType = 41 + EncoderTypeLockedStakeEntry EncoderType = 42 // EncoderTypeEndBlockView encoder type should be at the end and is used for automated tests. - EncoderTypeEndBlockView EncoderType = 41 + EncoderTypeEndBlockView EncoderType = 43 ) // Txindex encoder types. @@ -150,9 +152,12 @@ const ( EncoderTypeNewMessageTxindexMetadata EncoderType = 1000029 EncoderTypeRegisterAsValidatorTxindexMetadata EncoderType = 1000030 EncoderTypeUnregisterAsValidatorTxindexMetadata EncoderType = 1000031 + EncoderTypeStakeTxindexMetadata EncoderType = 1000032 + EncoderTypeUnstakeTxindexMetadata EncoderType = 1000033 + EncoderTypeUnlockStakeTxindexMetadata EncoderType = 1000034 // EncoderTypeEndTxIndex encoder type should be at the end and is used for automated tests. - EncoderTypeEndTxIndex EncoderType = 1000032 + EncoderTypeEndTxIndex EncoderType = 1000035 ) // This function translates the EncoderType into an empty DeSoEncoder struct. @@ -241,6 +246,10 @@ func (encoderType EncoderType) New() DeSoEncoder { return &TransactorNonceEntry{} case EncoderTypeValidatorEntry: return &ValidatorEntry{} + case EncoderTypeStakeEntry: + return &StakeEntry{} + case EncoderTypeLockedStakeEntry: + return &LockedStakeEntry{} } // Txindex encoder types @@ -309,6 +318,12 @@ func (encoderType EncoderType) New() DeSoEncoder { return &RegisterAsValidatorTxindexMetadata{} case EncoderTypeUnregisterAsValidatorTxindexMetadata: return &UnregisterAsValidatorTxindexMetadata{} + case EncoderTypeStakeTxindexMetadata: + return &StakeTxindexMetadata{} + case EncoderTypeUnstakeTxindexMetadata: + return &UnstakeTxindexMetadata{} + case EncoderTypeUnlockStakeTxindexMetadata: + return &UnlockStakeTxindexMetadata{} default: return nil } @@ -605,8 +620,11 @@ const ( OperationTypeDeleteExpiredNonces OperationType = 38 OperationTypeRegisterAsValidator OperationType = 39 OperationTypeUnregisterAsValidator OperationType = 40 + OperationTypeStake OperationType = 41 + OperationTypeUnstake OperationType = 42 + OperationTypeUnlockStake OperationType = 43 - // NEXT_TAG = 41 + // NEXT_TAG = 44 ) func (op OperationType) String() string { @@ -691,6 +709,12 @@ func (op OperationType) String() string { return "OperationTypeRegisterAsValidator" case OperationTypeUnregisterAsValidator: return "OperationTypeUnregisterAsValidator" + case OperationTypeStake: + return "OperationTypeStake" + case OperationTypeUnstake: + return "OperationTypeUnstake" + case OperationTypeUnlockStake: + return "OperationTypeUnlockStake" } return "OperationTypeUNKNOWN" } @@ -876,8 +900,21 @@ type UtxoOperation struct { // When we connect a block, we delete expired nonce entries. PrevNonceEntries []*TransactorNonceEntry - // PrevValidatorEntry is the previous ValidatorEntry prior to a register or unregister txn. + // PrevValidatorEntry is the previous ValidatorEntry prior to a + // register, unregister, stake, or unstake txn. PrevValidatorEntry *ValidatorEntry + + // PrevGlobalStakeAmountNanos is the previous GlobalStakeAmountNanos + // prior to a stake or unstake operation txn. + PrevGlobalStakeAmountNanos *uint256.Int + + // PrevStakeEntries is a slice of StakeEntries prior to + // a register, unregister, stake, or unstake txn. + PrevStakeEntries []*StakeEntry + + // PrevLockedStakeEntries is a slice of LockedStakeEntries + // prior to a unstake or unlock stake txn. + PrevLockedStakeEntries []*LockedStakeEntry } func (op *UtxoOperation) RawEncodeWithoutMetadata(blockHeight uint64, skipMetadata ...bool) []byte { @@ -1198,6 +1235,15 @@ func (op *UtxoOperation) RawEncodeWithoutMetadata(blockHeight uint64, skipMetada if MigrationTriggered(blockHeight, ProofOfStakeNewTxnTypesMigration) { // PrevValidatorEntry data = append(data, EncodeToBytes(blockHeight, op.PrevValidatorEntry, skipMetadata...)...) + + // PrevGlobalStakeAmountNanos + data = append(data, EncodeUint256(op.PrevGlobalStakeAmountNanos)...) + + // PrevStakeEntries + data = append(data, EncodeDeSoEncoderSlice(op.PrevStakeEntries, blockHeight, skipMetadata...)...) + + // PrevLockedStakeEntries + data = append(data, EncodeDeSoEncoderSlice(op.PrevLockedStakeEntries, blockHeight, skipMetadata...)...) } return data @@ -1817,11 +1863,25 @@ func (op *UtxoOperation) RawDecodeWithoutMetadata(blockHeight uint64, rr *bytes. if MigrationTriggered(blockHeight, ProofOfStakeNewTxnTypesMigration) { // PrevValidatorEntry - prevValidatorEntry := &ValidatorEntry{} - if exist, err := DecodeFromBytes(prevValidatorEntry, rr); exist && err == nil { - op.PrevValidatorEntry = prevValidatorEntry - } else if err != nil { - return errors.Wrapf(err, "UtxoOperation.Decode: Problem reading PrevValidatorEntry") + if op.PrevValidatorEntry, err = DecodeDeSoEncoder(&ValidatorEntry{}, rr); err != nil { + return errors.Wrapf(err, "UtxoOperation.Decode: Problem reading PrevValidatorEntry: ") + } + + // PrevGlobalStakeAmountNanos + if prevGlobalStakeAmountNanos, err := DecodeUint256(rr); err == nil { + op.PrevGlobalStakeAmountNanos = prevGlobalStakeAmountNanos + } else { + return errors.Wrapf(err, "UtxoOperation.Decode: Problem reading PrevGlobalStakeAmountNanos: ") + } + + // PrevStakeEntries + if op.PrevStakeEntries, err = DecodeDeSoEncoderSlice[*StakeEntry](rr); err != nil { + return errors.Wrapf(err, "UtxoOperation.Decode: Problem reading PrevStakeEntries: ") + } + + // PrevLockedStakeEntries + if op.PrevLockedStakeEntries, err = DecodeDeSoEncoderSlice[*LockedStakeEntry](rr); err != nil { + return errors.Wrapf(err, "UtxoOperation.Decode: Problem reading PrevLockedStakeEntries: ") } } @@ -4843,6 +4903,11 @@ func DecodeMapStringUint64(rr *bytes.Reader) (map[string]uint64, error) { return nil, nil } +// EncodeUint256 is useful for space-efficient encoding of uint256s. +// It does not guarantee fixed-width encoding, so should not be used +// in BadgerDB keys. Use EncodeOptionalUint256 instead, which does +// guarantee fixed-width encoding. Both EncodeUint256 and +// EncodeOptionalUint256 can handle nil inputs. func EncodeUint256(number *uint256.Int) []byte { var data []byte if number != nil { diff --git a/lib/block_view_types_test.go b/lib/block_view_types_test.go index c52e52a20..7448697d1 100644 --- a/lib/block_view_types_test.go +++ b/lib/block_view_types_test.go @@ -4,6 +4,7 @@ import ( "bytes" "encoding/hex" "github.com/brianvoe/gofakeit" + "github.com/holiman/uint256" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "reflect" @@ -390,3 +391,60 @@ func TestUtxoEntryEncodeDecode(t *testing.T) { } }) } + +func TestEncodingUint256s(t *testing.T) { + // Create three uint256.Ints. + num1 := uint256.NewInt() + num2 := uint256.NewInt().SetUint64(598128756) + num3 := MaxUint256 + + // Encode them to bytes using EncodeUint256. + encoded1 := EncodeUint256(num1) + encoded2 := EncodeUint256(num2) + encoded3 := EncodeUint256(num3) + + // Decode them from bytes using DecodeUint256. Verify values. + rr := bytes.NewReader(encoded1) + decoded1, err := DecodeUint256(rr) + require.NoError(t, err) + require.True(t, num1.Eq(decoded1)) + + rr = bytes.NewReader(encoded2) + decoded2, err := DecodeUint256(rr) + require.NoError(t, err) + require.True(t, num2.Eq(decoded2)) + + rr = bytes.NewReader(encoded3) + decoded3, err := DecodeUint256(rr) + require.NoError(t, err) + require.True(t, num3.Eq(decoded3)) + + // Test that EncodeUint256 does not provide a fixed-width byte encoding. + require.NotEqual(t, len(encoded1), len(encoded2)) + require.NotEqual(t, len(encoded1), len(encoded3)) + + // Encode them to bytes using EncodeOptionalUint256. + encoded1 = EncodeOptionalUint256(num1) + encoded2 = EncodeOptionalUint256(num2) + encoded3 = EncodeOptionalUint256(num3) + + // Decode them from bytes using ReadOptionalUint256. Verify values. + rr = bytes.NewReader(encoded1) + decoded1, err = ReadOptionalUint256(rr) + require.NoError(t, err) + require.True(t, num1.Eq(decoded1)) + + rr = bytes.NewReader(encoded2) + decoded2, err = ReadOptionalUint256(rr) + require.NoError(t, err) + require.True(t, num2.Eq(decoded2)) + + rr = bytes.NewReader(encoded3) + decoded3, err = ReadOptionalUint256(rr) + require.NoError(t, err) + require.True(t, num3.Eq(decoded3)) + + // Test that EncodeOptionalUint256 provides a fixed-width byte encoding. + require.Equal(t, len(encoded1), len(encoded2)) + require.Equal(t, len(encoded1), len(encoded3)) +} diff --git a/lib/block_view_validator.go b/lib/block_view_validator.go index 7dd592069..344b60c50 100644 --- a/lib/block_view_validator.go +++ b/lib/block_view_validator.go @@ -50,14 +50,6 @@ func (validatorEntry *ValidatorEntry) Copy() *ValidatorEntry { domainsCopy = append(domainsCopy, append([]byte{}, domain...)) // Makes a copy. } - // Copy ExtraData. - extraDataCopy := make(map[string][]byte) - for key, value := range validatorEntry.ExtraData { - valueCopy := make([]byte, len(value)) - copy(valueCopy, value) - extraDataCopy[key] = valueCopy - } - // Return new ValidatorEntry. return &ValidatorEntry{ ValidatorID: validatorEntry.ValidatorID.NewBlockHash(), @@ -69,7 +61,7 @@ func (validatorEntry *ValidatorEntry) Copy() *ValidatorEntry { VotingSignatureBlockHeight: validatorEntry.VotingSignatureBlockHeight, TotalStakeAmountNanos: validatorEntry.TotalStakeAmountNanos.Clone(), RegisteredAtBlockHeight: validatorEntry.RegisteredAtBlockHeight, - ExtraData: extraDataCopy, + ExtraData: copyExtraData(validatorEntry.ExtraData), isDeleted: validatorEntry.isDeleted, } } @@ -505,8 +497,9 @@ func DBKeyForValidatorByPKID(validatorEntry *ValidatorEntry) []byte { func DBKeyForValidatorByStake(validatorEntry *ValidatorEntry) []byte { key := append([]byte{}, Prefixes.PrefixValidatorByStake...) - // FIXME: ensure that this left-pads the uint256 to be equal width - key = append(key, EncodeUint256(validatorEntry.TotalStakeAmountNanos)...) // Highest stake first + // TotalStakeAmountNanos will never be nil here, but EncodeOptionalUint256 + // is used because it provides a fixed-width encoding of uint256.Ints. + key = append(key, EncodeOptionalUint256(validatorEntry.TotalStakeAmountNanos)...) // Highest stake first key = append(key, EncodeUint64(math.MaxUint64-validatorEntry.RegisteredAtBlockHeight)...) // Oldest first key = append(key, validatorEntry.ValidatorPKID.ToBytes()...) return key @@ -867,7 +860,7 @@ func (bav *UtxoView) _connectRegisterAsValidator( ) { // Validate the starting block height. if blockHeight < bav.Params.ForkHeights.ProofOfStakeNewTxnTypesBlockHeight { - return 0, 0, nil, RuleErrorProofofStakeTxnBeforeBlockHeight + return 0, 0, nil, errors.Wrapf(RuleErrorProofofStakeTxnBeforeBlockHeight, "_connectRegisterAsValidator: ") } // Validate the txn TxnType. @@ -902,7 +895,7 @@ func (bav *UtxoView) _connectRegisterAsValidator( // Convert TransactorPublicKey to TransactorPKID. transactorPKIDEntry := bav.GetPKIDForPublicKey(txn.PublicKey) if transactorPKIDEntry == nil || transactorPKIDEntry.isDeleted { - return 0, 0, nil, RuleErrorInvalidValidatorPKID + return 0, 0, nil, errors.Wrapf(RuleErrorInvalidValidatorPKID, "_connectRegisterAsValidator: ") } // Check if there is an existing ValidatorEntry that will be overwritten. @@ -1006,7 +999,7 @@ func (bav *UtxoView) _disconnectRegisterAsValidator( // Convert TransactorPublicKey to TransactorPKID. transactorPKIDEntry := bav.GetPKIDForPublicKey(currentTxn.PublicKey) if transactorPKIDEntry == nil || transactorPKIDEntry.isDeleted { - return RuleErrorInvalidValidatorPKID + return errors.Wrapf(RuleErrorInvalidValidatorPKID, "_disconnectRegisterAsValidator: ") } // Delete the current ValidatorEntry. @@ -1053,7 +1046,7 @@ func (bav *UtxoView) _connectUnregisterAsValidator( ) { // Validate the starting block height. if blockHeight < bav.Params.ForkHeights.ProofOfStakeNewTxnTypesBlockHeight { - return 0, 0, nil, RuleErrorProofofStakeTxnBeforeBlockHeight + return 0, 0, nil, errors.Wrapf(RuleErrorProofofStakeTxnBeforeBlockHeight, "_connectUnregisterAsValidator: ") } // Validate the txn TxnType. @@ -1088,7 +1081,7 @@ func (bav *UtxoView) _connectUnregisterAsValidator( // Convert TransactorPublicKey to TransactorPKID. transactorPKIDEntry := bav.GetPKIDForPublicKey(txn.PublicKey) if transactorPKIDEntry == nil || transactorPKIDEntry.isDeleted { - return 0, 0, nil, RuleErrorInvalidValidatorPKID + return 0, 0, nil, errors.Wrapf(RuleErrorInvalidValidatorPKID, "_connectUnregisterAsValidator: ") } // TODO: In subsequent PR, unstake all StakeEntries for this validator. @@ -1101,7 +1094,7 @@ func (bav *UtxoView) _connectUnregisterAsValidator( } // Note that we don't need to check isDeleted because the Get returns nil if isDeleted=true. if prevValidatorEntry == nil { - return 0, 0, nil, RuleErrorValidatorNotFound + return 0, 0, nil, errors.Wrapf(RuleErrorValidatorNotFound, "_connectUnregisterAsValidator: ") } bav._deleteValidatorEntryMappings(prevValidatorEntry) @@ -1163,26 +1156,26 @@ func (bav *UtxoView) IsValidRegisterAsValidatorMetadata(transactorPublicKey []by // Validate ValidatorPKID. transactorPKIDEntry := bav.GetPKIDForPublicKey(transactorPublicKey) if transactorPKIDEntry == nil || transactorPKIDEntry.isDeleted { - return RuleErrorInvalidValidatorPKID + return errors.Wrapf(RuleErrorInvalidValidatorPKID, "UtxoView.IsValidRegisterAsValidatorMetadata: ") } // Validate Domains. if len(metadata.Domains) < 1 { - return RuleErrorValidatorNoDomains + return errors.Wrapf(RuleErrorValidatorNoDomains, "UtxoView.IsValidRegisterAsValidatorMetadata: ") } if len(metadata.Domains) > MaxValidatorNumDomains { - return RuleErrorValidatorTooManyDomains + return errors.Wrapf(RuleErrorValidatorTooManyDomains, "UtxoView.IsValidRegisterAsValidatorMetadata: ") } var domainStrings []string for _, domain := range metadata.Domains { _, err := url.ParseRequestURI(string(domain)) if err != nil { - return fmt.Errorf("%s: %v", RuleErrorValidatorInvalidDomain, domain) + return fmt.Errorf("UtxoView.IsValidRegisterAsValidatorMetadata: %s: %v", RuleErrorValidatorInvalidDomain, domain) } domainStrings = append(domainStrings, string(domain)) } if len(NewSet(domainStrings).ToSlice()) != len(domainStrings) { - return RuleErrorValidatorDuplicateDomains + return errors.Wrapf(RuleErrorValidatorDuplicateDomains, "UtxoView.IsValidRegisterAsValidatorMetadata: ") } // TODO: In subsequent PR, validate VotingPublicKey, VotingPublicKeySignature, and VotingSignatureBlockHeight. @@ -1193,16 +1186,16 @@ func (bav *UtxoView) IsValidUnregisterAsValidatorMetadata(transactorPublicKey [] // Validate ValidatorPKID. transactorPKIDEntry := bav.GetPKIDForPublicKey(transactorPublicKey) if transactorPKIDEntry == nil || transactorPKIDEntry.isDeleted { - return RuleErrorInvalidValidatorPKID + return errors.Wrapf(RuleErrorInvalidValidatorPKID, "UtxoView.IsValidUnregisterAsValidatorMetadata: ") } // Validate ValidatorEntry exists. validatorEntry, err := bav.GetValidatorByPKID(transactorPKIDEntry.PKID) if err != nil { - return errors.Wrapf(err, "IsValidUnregisterAsValidatorMetadata: ") + return errors.Wrapf(err, "UtxoView.IsValidUnregisterAsValidatorMetadata: ") } if validatorEntry == nil { - return RuleErrorValidatorNotFound + return errors.Wrapf(RuleErrorValidatorNotFound, "UtxoView.IsValidUnregisterAsValidatorMetadata: ") } return nil @@ -1247,13 +1240,30 @@ func (bav *UtxoView) GetValidatorByPKID(pkid *PKID) (*ValidatorEntry, error) { // in the UtxoView for the given PKID, check the database. dbValidatorEntry, err := DBGetValidatorByPKID(bav.Handle, bav.Snapshot, pkid) if err != nil { - return nil, err + return nil, errors.Wrapf(err, "UtxoView.GetValidatorByPKID: ") + } + if dbValidatorEntry != nil { + // Cache the ValidatorEntry from the db in the UtxoView. + bav._setValidatorEntryMappings(dbValidatorEntry) } - // Cache the ValidatorEntry from the db in the UtxoView. - bav._setValidatorEntryMappings(dbValidatorEntry) return dbValidatorEntry, nil } +func (bav *UtxoView) GetValidatorByPublicKey(validatorPublicKey *PublicKey) (*ValidatorEntry, error) { + validatorPKIDEntry := bav.GetPKIDForPublicKey(validatorPublicKey.ToBytes()) + if validatorPKIDEntry == nil || validatorPKIDEntry.isDeleted { + return nil, errors.Wrapf(RuleErrorInvalidValidatorPKID, "UtxoView.GetValidatorByPublicKey: ") + } + validatorEntry, err := bav.GetValidatorByPKID(validatorPKIDEntry.PKID) + if err != nil { + return nil, err + } + if validatorEntry == nil || validatorEntry.isDeleted { + return nil, errors.Wrapf(RuleErrorInvalidValidatorPKID, "UtxoView.GetValidatorByPublicKey: ") + } + return validatorEntry, nil +} + func (bav *UtxoView) GetTopValidatorsByStake(limit int) ([]*ValidatorEntry, error) { // Validate limit param. if limit <= 0 { @@ -1272,7 +1282,7 @@ func (bav *UtxoView) GetTopValidatorsByStake(limit int) ([]*ValidatorEntry, erro // Pull top N ValidatorEntries from the database (not present in the UtxoView). validatorEntries, err := DBGetTopValidatorsByStake(bav.Handle, bav.Snapshot, limit, utxoViewValidatorEntries) if err != nil { - return nil, errors.Wrapf(err, "GetTopValidatorsByStake: error retrieving entries from db: ") + return nil, errors.Wrapf(err, "UtxoView.GetTopValidatorsByStake: error retrieving entries from db: ") } // Add !isDeleted ValidatorEntries from the UtxoView to the ValidatorEntries from the db. for _, validatorEntry := range utxoViewValidatorEntries { @@ -1297,10 +1307,11 @@ func (bav *UtxoView) GetGlobalStakeAmountNanos() (*uint256.Int, error) { globalStakeAmountNanos = bav.GlobalStakeAmountNanos.Clone() } // If not set, read the GlobalStakeAmountNanos from the db. + // TODO: Confirm if the GlobalStakeAmountNanos.IsZero() that we should look in the db. if globalStakeAmountNanos == nil || globalStakeAmountNanos.IsZero() { globalStakeAmountNanos, err = DBGetGlobalStakeAmountNanos(bav.Handle, bav.Snapshot) if err != nil { - return nil, err + return nil, errors.Wrapf(err, "UtxoView.GetGlobalStakeAmountNanos: ") } if globalStakeAmountNanos == nil { globalStakeAmountNanos = uint256.NewInt() @@ -1385,6 +1396,12 @@ func (bav *UtxoView) _flushValidatorEntriesToDbWithTxn(txn *badger.Txn, blockHei } func (bav *UtxoView) _flushGlobalStakeAmountNanosToDbWithTxn(txn *badger.Txn, blockHeight uint64) error { + // If GlobalStakeAmountNanos is nil, then it was never + // set and shouldn't overwrite the value in the db. + if bav.GlobalStakeAmountNanos == nil { + return nil + } + return DBPutGlobalStakeAmountNanosWithTxn(txn, bav.Snapshot, bav.GlobalStakeAmountNanos, blockHeight) } diff --git a/lib/block_view_validator_test.go b/lib/block_view_validator_test.go index be1fc3909..5c16ebefc 100644 --- a/lib/block_view_validator_test.go +++ b/lib/block_view_validator_test.go @@ -16,6 +16,11 @@ func TestValidatorRegistration(t *testing.T) { _testValidatorRegistrationWithDerivedKey(t) } +func TestGetTopValidatorsByStake(t *testing.T) { + _testGetTopValidatorsByStake(t, false) + _testGetTopValidatorsByStake(t, true) +} + func _testValidatorRegistration(t *testing.T, flushToDB bool) { // Local variables var registerMetadata *RegisterAsValidatorMetadata @@ -24,15 +29,14 @@ func _testValidatorRegistration(t *testing.T, flushToDB bool) { var globalStakeAmountNanos *uint256.Int var err error + // Initialize fork heights. + setBalanceModelBlockHeights() + defer resetBalanceModelBlockHeights() + // Initialize test chain and miner. chain, params, db := NewLowDifficultyBlockchain(t) mempool, miner := NewTestMiner(t, chain, params, true) - // Initialize fork heights. - params.ForkHeights.BalanceModelBlockHeight = uint32(1) - GlobalDeSoParams.EncoderMigrationHeights = GetEncoderMigrationHeights(¶ms.ForkHeights) - GlobalDeSoParams.EncoderMigrationHeightsList = GetEncoderMigrationHeightsList(¶ms.ForkHeights) - utxoView := func() *UtxoView { newUtxoView, err := mempool.GetAugmentedUniversalView() require.NoError(t, err) @@ -74,7 +78,7 @@ func _testValidatorRegistration(t *testing.T, flushToDB bool) { _, _, _, _, _ = m0PKID, m1PKID, m2PKID, m3PKID, m4PKID { - // Param Updater set min fee rate to 101 nanos per KB + // ParamUpdater set min fee rate params.ExtraRegtestParamUpdaterKeys[MakePkMapKey(paramUpdaterPkBytes)] = true _updateGlobalParamsEntryWithTestMeta( testMeta, @@ -382,18 +386,15 @@ func _submitUnregisterAsValidatorTxn( func _testValidatorRegistrationWithDerivedKey(t *testing.T) { var err error + // Initialize balance model fork heights. + setBalanceModelBlockHeights() + defer resetBalanceModelBlockHeights() + // Initialize test chain and miner. chain, params, db := NewLowDifficultyBlockchain(t) mempool, miner := NewTestMiner(t, chain, params, true) // Initialize fork heights. - params.ForkHeights.NFTTransferOrBurnAndDerivedKeysBlockHeight = uint32(0) - params.ForkHeights.DerivedKeySetSpendingLimitsBlockHeight = uint32(0) - params.ForkHeights.DerivedKeyTrackSpendingLimitsBlockHeight = uint32(0) - params.ForkHeights.DerivedKeyEthSignatureCompatibilityBlockHeight = uint32(0) - params.ForkHeights.ExtraDataOnEntriesBlockHeight = uint32(0) - params.ForkHeights.AssociationsAndAccessGroupsBlockHeight = uint32(0) - params.ForkHeights.BalanceModelBlockHeight = uint32(1) params.ForkHeights.ProofOfStakeNewTxnTypesBlockHeight = uint32(1) GlobalDeSoParams.EncoderMigrationHeights = GetEncoderMigrationHeights(¶ms.ForkHeights) GlobalDeSoParams.EncoderMigrationHeightsList = GetEncoderMigrationHeightsList(¶ms.ForkHeights) @@ -508,7 +509,7 @@ func _testValidatorRegistrationWithDerivedKey(t *testing.T) { return err } // Sign txn. - _signTxnWithDerivedKey(t, txn, derivedKeyPrivBase58Check) + _signTxnWithDerivedKeyAndType(t, txn, derivedKeyPrivBase58Check, 1) // Store the original transactor balance. transactorPublicKeyBase58Check := Base58CheckEncode(transactorPkBytes, false, params) prevBalance := _getBalance(testMeta.t, testMeta.chain, testMeta.mempool, transactorPublicKeyBase58Check) @@ -533,6 +534,21 @@ func _testValidatorRegistrationWithDerivedKey(t *testing.T) { return nil } + { + // ParamUpdater set min fee rate + params.ExtraRegtestParamUpdaterKeys[MakePkMapKey(paramUpdaterPkBytes)] = true + _updateGlobalParamsEntryWithTestMeta( + testMeta, + testMeta.feeRateNanosPerKb, + paramUpdaterPub, + paramUpdaterPriv, + -1, + int64(testMeta.feeRateNanosPerKb), + -1, + -1, + -1, + ) + } { // Submit a RegisterAsValidator txn using a DerivedKey. @@ -617,3 +633,352 @@ func _testValidatorRegistrationWithDerivedKey(t *testing.T) { require.NoError(t, mempool.universalUtxoView.FlushToDb(blockHeight)) _executeAllTestRollbackAndFlush(testMeta) } + +func _testGetTopValidatorsByStake(t *testing.T, flushToDB bool) { + var validatorEntries []*ValidatorEntry + var err error + + // Initialize balance model fork heights. + setBalanceModelBlockHeights() + defer resetBalanceModelBlockHeights() + + // Initialize test chain and miner. + chain, params, db := NewLowDifficultyBlockchain(t) + mempool, miner := NewTestMiner(t, chain, params, true) + + // Initialize PoS fork height. + params.ForkHeights.ProofOfStakeNewTxnTypesBlockHeight = uint32(1) + GlobalDeSoParams.EncoderMigrationHeights = GetEncoderMigrationHeights(¶ms.ForkHeights) + GlobalDeSoParams.EncoderMigrationHeightsList = GetEncoderMigrationHeightsList(¶ms.ForkHeights) + + utxoView := func() *UtxoView { + newUtxoView, err := mempool.GetAugmentedUniversalView() + require.NoError(t, err) + return newUtxoView + } + + // Mine a few blocks to give the senderPkString some money. + for ii := 0; ii < 10; ii++ { + _, err = miner.MineAndProcessSingleBlock(0, mempool) + require.NoError(t, err) + } + + // We build the testMeta obj after mining blocks so that we save the correct block height. + blockHeight := uint64(chain.blockTip().Height) + 1 + testMeta := &TestMeta{ + t: t, + chain: chain, + params: params, + db: db, + mempool: mempool, + miner: miner, + savedHeight: uint32(blockHeight), + feeRateNanosPerKb: uint64(101), + } + + _registerOrTransferWithTestMeta(testMeta, "m0", senderPkString, m0Pub, senderPrivString, 1e3) + _registerOrTransferWithTestMeta(testMeta, "m1", senderPkString, m1Pub, senderPrivString, 1e3) + _registerOrTransferWithTestMeta(testMeta, "m2", senderPkString, m2Pub, senderPrivString, 1e3) + _registerOrTransferWithTestMeta(testMeta, "m3", senderPkString, m3Pub, senderPrivString, 1e3) + _registerOrTransferWithTestMeta(testMeta, "m4", senderPkString, m4Pub, senderPrivString, 1e3) + _registerOrTransferWithTestMeta(testMeta, "", senderPkString, paramUpdaterPub, senderPrivString, 1e3) + + m0PKID := DBGetPKIDEntryForPublicKey(db, chain.snapshot, m0PkBytes).PKID + m1PKID := DBGetPKIDEntryForPublicKey(db, chain.snapshot, m1PkBytes).PKID + m2PKID := DBGetPKIDEntryForPublicKey(db, chain.snapshot, m2PkBytes).PKID + m3PKID := DBGetPKIDEntryForPublicKey(db, chain.snapshot, m3PkBytes).PKID + m4PKID := DBGetPKIDEntryForPublicKey(db, chain.snapshot, m4PkBytes).PKID + _, _, _, _, _ = m0PKID, m1PKID, m2PKID, m3PKID, m4PKID + + { + // ParamUpdater set min fee rate + params.ExtraRegtestParamUpdaterKeys[MakePkMapKey(paramUpdaterPkBytes)] = true + _updateGlobalParamsEntryWithTestMeta( + testMeta, + testMeta.feeRateNanosPerKb, + paramUpdaterPub, + paramUpdaterPriv, + -1, + int64(testMeta.feeRateNanosPerKb), + -1, + -1, + -1, + ) + } + { + // m0 registers as a validator. + registerMetadata := &RegisterAsValidatorMetadata{ + Domains: [][]byte{[]byte("https://m0.com")}, + } + _, _, _, err = _submitRegisterAsValidatorTxn( + testMeta, m0Pub, m0Priv, registerMetadata, nil, flushToDB, + ) + require.NoError(t, err) + + // Verify top validators. + validatorEntries, err = utxoView().GetTopValidatorsByStake(10) + require.NoError(t, err) + require.Len(t, validatorEntries, 1) + require.Equal(t, validatorEntries[0].ValidatorPKID, m0PKID) + require.Equal(t, validatorEntries[0].TotalStakeAmountNanos, uint256.NewInt()) + } + { + // m1 registers as a validator. + registerMetadata := &RegisterAsValidatorMetadata{ + Domains: [][]byte{[]byte("https://m1.com")}, + } + _, _, _, err = _submitRegisterAsValidatorTxn( + testMeta, m1Pub, m1Priv, registerMetadata, nil, flushToDB, + ) + require.NoError(t, err) + + // Verify top validators. + validatorEntries, err = utxoView().GetTopValidatorsByStake(10) + require.NoError(t, err) + require.Len(t, validatorEntries, 2) + } + { + // m2 registers as a validator. + registerMetadata := &RegisterAsValidatorMetadata{ + Domains: [][]byte{[]byte("https://m2.com")}, + } + _, _, _, err = _submitRegisterAsValidatorTxn( + testMeta, m2Pub, m2Priv, registerMetadata, nil, flushToDB, + ) + require.NoError(t, err) + + // Verify top validators. + validatorEntries, err = utxoView().GetTopValidatorsByStake(10) + require.NoError(t, err) + require.Len(t, validatorEntries, 3) + } + { + // m3 stakes 100 DESO nanos with m0. + stakeMetadata := &StakeMetadata{ + ValidatorPublicKey: NewPublicKey(m0PkBytes), + StakeAmountNanos: uint256.NewInt().SetUint64(100), + } + _, err = _submitStakeTxn(testMeta, m3Pub, m3Priv, stakeMetadata, nil, flushToDB) + require.NoError(t, err) + + // m3 stakes 200 DESO nanos with m1. + stakeMetadata = &StakeMetadata{ + ValidatorPublicKey: NewPublicKey(m1PkBytes), + StakeAmountNanos: uint256.NewInt().SetUint64(200), + } + _, err = _submitStakeTxn(testMeta, m3Pub, m3Priv, stakeMetadata, nil, flushToDB) + require.NoError(t, err) + + // m3 stakes 300 DESO nanos with m2. + stakeMetadata = &StakeMetadata{ + ValidatorPublicKey: NewPublicKey(m2PkBytes), + StakeAmountNanos: uint256.NewInt().SetUint64(300), + } + _, err = _submitStakeTxn(testMeta, m3Pub, m3Priv, stakeMetadata, nil, flushToDB) + require.NoError(t, err) + + // Verify top validators. + validatorEntries, err = utxoView().GetTopValidatorsByStake(10) + require.NoError(t, err) + require.Len(t, validatorEntries, 3) + require.Equal(t, validatorEntries[0].ValidatorPKID, m2PKID) + require.Equal(t, validatorEntries[0].TotalStakeAmountNanos, uint256.NewInt().SetUint64(300)) + require.Equal(t, validatorEntries[1].ValidatorPKID, m1PKID) + require.Equal(t, validatorEntries[1].TotalStakeAmountNanos, uint256.NewInt().SetUint64(200)) + require.Equal(t, validatorEntries[2].ValidatorPKID, m0PKID) + require.Equal(t, validatorEntries[2].TotalStakeAmountNanos, uint256.NewInt().SetUint64(100)) + } + { + // m3 unstakes from m1. + unstakeMetadata := &UnstakeMetadata{ + ValidatorPublicKey: NewPublicKey(m1PkBytes), + UnstakeAmountNanos: uint256.NewInt().SetUint64(150), + } + _, err = _submitUnstakeTxn(testMeta, m3Pub, m3Priv, unstakeMetadata, nil, flushToDB) + + // Verify top validators. + validatorEntries, err = utxoView().GetTopValidatorsByStake(10) + require.NoError(t, err) + require.Len(t, validatorEntries, 3) + require.Equal(t, validatorEntries[0].ValidatorPKID, m2PKID) + require.Equal(t, validatorEntries[0].TotalStakeAmountNanos, uint256.NewInt().SetUint64(300)) + require.Equal(t, validatorEntries[1].ValidatorPKID, m0PKID) + require.Equal(t, validatorEntries[1].TotalStakeAmountNanos, uint256.NewInt().SetUint64(100)) + require.Equal(t, validatorEntries[2].ValidatorPKID, m1PKID) + require.Equal(t, validatorEntries[2].TotalStakeAmountNanos, uint256.NewInt().SetUint64(50)) + } + { + // m3 unstakes more from m1. + unstakeMetadata := &UnstakeMetadata{ + ValidatorPublicKey: NewPublicKey(m1PkBytes), + UnstakeAmountNanos: uint256.NewInt().SetUint64(50), + } + _, err = _submitUnstakeTxn(testMeta, m3Pub, m3Priv, unstakeMetadata, nil, flushToDB) + + // Verify top validators. + validatorEntries, err = utxoView().GetTopValidatorsByStake(10) + require.NoError(t, err) + require.Len(t, validatorEntries, 3) + require.Equal(t, validatorEntries[0].ValidatorPKID, m2PKID) + require.Equal(t, validatorEntries[0].TotalStakeAmountNanos, uint256.NewInt().SetUint64(300)) + require.Equal(t, validatorEntries[1].ValidatorPKID, m0PKID) + require.Equal(t, validatorEntries[1].TotalStakeAmountNanos, uint256.NewInt().SetUint64(100)) + require.Equal(t, validatorEntries[2].ValidatorPKID, m1PKID) + require.Equal(t, validatorEntries[2].TotalStakeAmountNanos, uint256.NewInt().SetUint64(0)) + } + { + // m2 unregisters as validator. + _, _, _, err = _submitUnregisterAsValidatorTxn(testMeta, m2Pub, m2Priv, flushToDB) + require.NoError(t, err) + + // Verify top validators. + validatorEntries, err = utxoView().GetTopValidatorsByStake(10) + require.NoError(t, err) + require.Len(t, validatorEntries, 2) + require.Equal(t, validatorEntries[0].ValidatorPKID, m0PKID) + require.Equal(t, validatorEntries[0].TotalStakeAmountNanos, uint256.NewInt().SetUint64(100)) + require.Equal(t, validatorEntries[1].ValidatorPKID, m1PKID) + require.Equal(t, validatorEntries[1].TotalStakeAmountNanos, uint256.NewInt().SetUint64(0)) + } + { + // m4 stakes with m1. + stakeMetadata := &StakeMetadata{ + ValidatorPublicKey: NewPublicKey(m1PkBytes), + StakeAmountNanos: uint256.NewInt().SetUint64(150), + } + _, err = _submitStakeTxn(testMeta, m4Pub, m4Priv, stakeMetadata, nil, flushToDB) + require.NoError(t, err) + + // Verify top validators. + validatorEntries, err = utxoView().GetTopValidatorsByStake(10) + require.NoError(t, err) + require.Len(t, validatorEntries, 2) + require.Equal(t, validatorEntries[0].ValidatorPKID, m1PKID) + require.Equal(t, validatorEntries[0].TotalStakeAmountNanos, uint256.NewInt().SetUint64(150)) + require.Equal(t, validatorEntries[1].ValidatorPKID, m0PKID) + require.Equal(t, validatorEntries[1].TotalStakeAmountNanos, uint256.NewInt().SetUint64(100)) + } + { + // m4 stakes more with m1. + stakeMetadata := &StakeMetadata{ + ValidatorPublicKey: NewPublicKey(m1PkBytes), + StakeAmountNanos: uint256.NewInt().SetUint64(100), + } + _, err = _submitStakeTxn(testMeta, m4Pub, m4Priv, stakeMetadata, nil, flushToDB) + require.NoError(t, err) + + // Verify top validators. + validatorEntries, err = utxoView().GetTopValidatorsByStake(10) + require.NoError(t, err) + require.Len(t, validatorEntries, 2) + require.Equal(t, validatorEntries[0].ValidatorPKID, m1PKID) + require.Equal(t, validatorEntries[0].TotalStakeAmountNanos, uint256.NewInt().SetUint64(250)) + require.Equal(t, validatorEntries[1].ValidatorPKID, m0PKID) + require.Equal(t, validatorEntries[1].TotalStakeAmountNanos, uint256.NewInt().SetUint64(100)) + } + { + // Verify top validators with LIMIT. + validatorEntries, err = utxoView().GetTopValidatorsByStake(1) + require.NoError(t, err) + require.Len(t, validatorEntries, 1) + require.Equal(t, validatorEntries[0].ValidatorPKID, m1PKID) + require.Equal(t, validatorEntries[0].TotalStakeAmountNanos, uint256.NewInt().SetUint64(250)) + } + + // Flush mempool to the db and test rollbacks. + require.NoError(t, mempool.universalUtxoView.FlushToDb(blockHeight)) + _executeAllTestRollbackAndFlush(testMeta) +} + +func TestGetTopValidatorsByStakeMergingDbAndUtxoView(t *testing.T) { + // For this test, we manually place ValidatorEntries in the database and + // UtxoView to test merging the two to determine the TopValidatorsByStake. + + // Initialize test chain and UtxoView. + chain, params, db := NewLowDifficultyBlockchain(t) + utxoView, err := NewUtxoView(db, params, chain.postgres, chain.snapshot) + require.NoError(t, err) + blockHeight := uint64(chain.blockTip().Height + 1) + + m0PKID := DBGetPKIDEntryForPublicKey(db, chain.snapshot, m0PkBytes).PKID + m1PKID := DBGetPKIDEntryForPublicKey(db, chain.snapshot, m1PkBytes).PKID + m2PKID := DBGetPKIDEntryForPublicKey(db, chain.snapshot, m2PkBytes).PKID + + // Store m0's ValidatorEntry in the db with TotalStake = 100 nanos. + validatorEntry := &ValidatorEntry{ + ValidatorPKID: m0PKID, + TotalStakeAmountNanos: uint256.NewInt().SetUint64(100), + } + utxoView._setValidatorEntryMappings(validatorEntry) + require.NoError(t, utxoView.FlushToDb(blockHeight)) + + // Verify m0 is stored in the db. + validatorEntry, err = DBGetValidatorByPKID(db, chain.snapshot, m0PKID) + require.NoError(t, err) + require.NotNil(t, validatorEntry) + require.Equal(t, validatorEntry.TotalStakeAmountNanos, uint256.NewInt().SetUint64(100)) + + // Verify m0 is not stored in the UtxoView. + require.Empty(t, utxoView.ValidatorMapKeyToValidatorEntry) + + // Store m1's ValidatorEntry in the database with TotalStake = 200 nanos. + validatorEntry = &ValidatorEntry{ + ValidatorPKID: m1PKID, + TotalStakeAmountNanos: uint256.NewInt().SetUint64(200), + } + utxoView._setValidatorEntryMappings(validatorEntry) + require.NoError(t, utxoView.FlushToDb(blockHeight)) + + // Fetch m1 so it is also cached in the UtxoView. + validatorEntry, err = utxoView.GetValidatorByPKID(m1PKID) + require.NoError(t, err) + require.NotNil(t, validatorEntry) + + // Verify m1 is stored in the db. + validatorEntry, err = DBGetValidatorByPKID(db, chain.snapshot, m1PKID) + require.NoError(t, err) + require.NotNil(t, validatorEntry) + require.Equal(t, validatorEntry.TotalStakeAmountNanos, uint256.NewInt().SetUint64(200)) + + // Verify m1 is also stored in the UtxoView. + require.Len(t, utxoView.ValidatorMapKeyToValidatorEntry, 1) + require.Equal(t, utxoView.ValidatorMapKeyToValidatorEntry[validatorEntry.ToMapKey()].ValidatorPKID, m1PKID) + require.Equal( + t, + utxoView.ValidatorMapKeyToValidatorEntry[validatorEntry.ToMapKey()].TotalStakeAmountNanos, + uint256.NewInt().SetUint64(200), + ) + + // Store m2's ValidatorEntry in the UtxoView. + m2ValidatorEntry := &ValidatorEntry{ + ValidatorPKID: m2PKID, + TotalStakeAmountNanos: uint256.NewInt().SetUint64(50), + } + utxoView._setValidatorEntryMappings(m2ValidatorEntry) + + // Verify m2 is not stored in the db. + validatorEntry, err = DBGetValidatorByPKID(db, chain.snapshot, m2PKID) + require.NoError(t, err) + require.Nil(t, validatorEntry) + + // Verify m2 is stored in the UtxoView. + require.Len(t, utxoView.ValidatorMapKeyToValidatorEntry, 2) + + require.Equal(t, utxoView.ValidatorMapKeyToValidatorEntry[m2ValidatorEntry.ToMapKey()].ValidatorPKID, m2PKID) + require.Equal( + t, + utxoView.ValidatorMapKeyToValidatorEntry[m2ValidatorEntry.ToMapKey()].TotalStakeAmountNanos, + uint256.NewInt().SetUint64(50), + ) + + // Fetch TopValidatorsByStake merging ValidatorEntries from the db and UtxoView. + validatorEntries, err := utxoView.GetTopValidatorsByStake(3) + require.NoError(t, err) + require.Len(t, validatorEntries, 3) + require.Equal(t, validatorEntries[0].ValidatorPKID, m1PKID) + require.Equal(t, validatorEntries[0].TotalStakeAmountNanos, uint256.NewInt().SetUint64(200)) + require.Equal(t, validatorEntries[1].ValidatorPKID, m0PKID) + require.Equal(t, validatorEntries[1].TotalStakeAmountNanos, uint256.NewInt().SetUint64(100)) + require.Equal(t, validatorEntries[2].ValidatorPKID, m2PKID) + require.Equal(t, validatorEntries[2].TotalStakeAmountNanos, uint256.NewInt().SetUint64(50)) +} diff --git a/lib/db_utils.go b/lib/db_utils.go index be208b4ee..c29d1d3d9 100644 --- a/lib/db_utils.go +++ b/lib/db_utils.go @@ -494,7 +494,15 @@ type DBPrefixes struct { // Prefix -> *uint256.Int PrefixGlobalStakeAmountNanos []byte `prefix_id:"[80]" is_state:"true"` - // NEXT_TAG: 81 + // PrefixStakeByValidatorByStaker: Retrieve a StakeEntry. + // Prefix, ValidatorPKID, StakerPKID -> StakeEntry + PrefixStakeByValidatorByStaker []byte `prefix_id:"[81]" is_state:"true"` + + // PrefixLockedStakeByValidatorByStakerByLockedAt: Retrieve a LockedStakeEntry. + // Prefix, ValidatorPKID, StakerPKID, LockedAtEpochNumber -> LockedStakeEntry + PrefixLockedStakeByValidatorByStakerByLockedAt []byte `prefix_id:"[82]" is_state:"true"` + + // NEXT_TAG: 83 } // StatePrefixToDeSoEncoder maps each state prefix to a DeSoEncoder type that is stored under that prefix. @@ -705,6 +713,12 @@ func StatePrefixToDeSoEncoder(prefix []byte) (_isEncoder bool, _encoder DeSoEnco } else if bytes.Equal(prefix, Prefixes.PrefixGlobalStakeAmountNanos) { // prefix_id:"[80]" return false, nil + } else if bytes.Equal(prefix, Prefixes.PrefixStakeByValidatorByStaker) { + // prefix_id:"[81]" + return true, &StakeEntry{} + } else if bytes.Equal(prefix, Prefixes.PrefixLockedStakeByValidatorByStakerByLockedAt) { + // prefix_id:"[82]" + return true, &LockedStakeEntry{} } return true, nil @@ -6763,6 +6777,9 @@ type TransactionMetadata struct { NewMessageTxindexMetadata *NewMessageTxindexMetadata `json:",omitempty"` RegisterAsValidatorTxindexMetadata *RegisterAsValidatorTxindexMetadata `json:",omitempty"` UnregisterAsValidatorTxindexMetadata *UnregisterAsValidatorTxindexMetadata `json:",omitempty"` + StakeTxindexMetadata *StakeTxindexMetadata `json:",omitempty"` + UnstakeTxindexMetadata *UnstakeTxindexMetadata `json:",omitempty"` + UnlockStakeTxindexMetadata *UnlockStakeTxindexMetadata `json:",omitempty"` } func (txnMeta *TransactionMetadata) RawEncodeWithoutMetadata(blockHeight uint64, skipMetadata ...bool) []byte { @@ -6849,6 +6866,12 @@ func (txnMeta *TransactionMetadata) RawEncodeWithoutMetadata(blockHeight uint64, data = append(data, EncodeToBytes(blockHeight, txnMeta.RegisterAsValidatorTxindexMetadata, skipMetadata...)...) // encoding UnregisterAsValidatorTxindexMetadata data = append(data, EncodeToBytes(blockHeight, txnMeta.UnregisterAsValidatorTxindexMetadata, skipMetadata...)...) + // encoding StakeTxindexMetadata + data = append(data, EncodeToBytes(blockHeight, txnMeta.StakeTxindexMetadata, skipMetadata...)...) + // encoding UnstakeTxindexMetadata + data = append(data, EncodeToBytes(blockHeight, txnMeta.UnstakeTxindexMetadata, skipMetadata...)...) + // encoding UnlockStakeTxindexMetadata + data = append(data, EncodeToBytes(blockHeight, txnMeta.UnlockStakeTxindexMetadata, skipMetadata...)...) } return data @@ -7102,19 +7125,25 @@ func (txnMeta *TransactionMetadata) RawDecodeWithoutMetadata(blockHeight uint64, if MigrationTriggered(blockHeight, ProofOfStakeNewTxnTypesMigration) { // decoding RegisterAsValidatorTxindexMetadata - CopyRegisterAsValidatorTxindexMetadata := &RegisterAsValidatorTxindexMetadata{} - if exist, err := DecodeFromBytes(CopyRegisterAsValidatorTxindexMetadata, rr); exist && err == nil { - txnMeta.RegisterAsValidatorTxindexMetadata = CopyRegisterAsValidatorTxindexMetadata - } else { + if txnMeta.RegisterAsValidatorTxindexMetadata, err = DecodeDeSoEncoder(&RegisterAsValidatorTxindexMetadata{}, rr); err != nil { return errors.Wrapf(err, "TransactionMetadata.Decode: Problem reading RegisterAsValidatorTxindexMetadata") } // decoding UnregisterAsValidatorTxindexMetadata - CopyUnregisterAsValidatorTxindexMetadata := &UnregisterAsValidatorTxindexMetadata{} - if exist, err := DecodeFromBytes(CopyUnregisterAsValidatorTxindexMetadata, rr); exist && err == nil { - txnMeta.UnregisterAsValidatorTxindexMetadata = CopyUnregisterAsValidatorTxindexMetadata - } else { + if txnMeta.UnregisterAsValidatorTxindexMetadata, err = DecodeDeSoEncoder(&UnregisterAsValidatorTxindexMetadata{}, rr); err != nil { return errors.Wrapf(err, "TransactionMetadata.Decode: Problem reading UnregisterAsValidatorTxindexMetadata") } + // decoding StakeTxindexMetadata + if txnMeta.StakeTxindexMetadata, err = DecodeDeSoEncoder(&StakeTxindexMetadata{}, rr); err != nil { + return errors.Wrapf(err, "TransactionMetadata.Decode: Problem reading StakeTxindexMetadata") + } + // decoding UnstakeTxindexMetadata + if txnMeta.UnstakeTxindexMetadata, err = DecodeDeSoEncoder(&UnstakeTxindexMetadata{}, rr); err != nil { + return errors.Wrapf(err, "TransactionMetadata.Decode: Problem reading UnstakeTxindexMetadata") + } + // decoding UnlockStakeTxindexMetadata + if txnMeta.UnlockStakeTxindexMetadata, err = DecodeDeSoEncoder(&UnlockStakeTxindexMetadata{}, rr); err != nil { + return errors.Wrapf(err, "TransactionMetadata.Decode: Problem reading UnlockStakeTxindexMetadata") + } } return nil diff --git a/lib/generics.go b/lib/generics.go index 4e8d59b7e..3b8dba907 100644 --- a/lib/generics.go +++ b/lib/generics.go @@ -1,5 +1,10 @@ package lib +import ( + "bytes" + "github.com/pkg/errors" +) + // Generic Set object. Retains the order elements are addd to the set. type Set[T comparable] struct { _innerMap map[T]struct{} @@ -71,3 +76,47 @@ func MapSet[T comparable, K any](set *Set[T], mapFunc func(elem T) (K, error)) ( } return results, nil } + +func DecodeDeSoEncoder[T DeSoEncoder](entry T, rr *bytes.Reader) (T, error) { + var emptyEntry T + exist, err := DecodeFromBytes(entry, rr) + if err != nil { + return emptyEntry, errors.Wrapf(err, "DecodeDeSoEncoder: Problem decoding from bytes") + } + if !exist { + return emptyEntry, nil + } + return entry, nil +} + +func EncodeDeSoEncoderSlice[T DeSoEncoder](inputSlice []T, blockHeight uint64, skipMetadata ...bool) []byte { + var data []byte + numItems := uint64(len(inputSlice)) + data = append(data, UintToBuf(numItems)...) + for _, item := range inputSlice { + data = append(data, EncodeToBytes(blockHeight, item, skipMetadata...)...) + } + return data +} + +func DecodeDeSoEncoderSlice[T DeSoEncoder](rr *bytes.Reader) ([]T, error) { + numItems, err := ReadUvarint(rr) + if err != nil { + return nil, errors.Wrapf(err, "DecodeDeSoEncoderSlice: Problem decoding numItems") + } + // Note: is it more efficient to do a make with specific length and set at each index? + inputs, err := SafeMakeSliceWithLength[T](numItems) + if err != nil { + return nil, errors.Wrapf(err, "DecodeDeSoEncoderSlice: Problem making slice with length %d", numItems) + } + var results []T + for ii := uint64(0); ii < numItems; ii++ { + prevEntry := inputs[ii].GetEncoderType().New() + if exist, err := DecodeFromBytes(prevEntry, rr); exist && err == nil { + results = append(results, prevEntry.(T)) + } else if err != nil { + return nil, errors.Wrapf(err, "DecodeDeSoEncoderSlice: Problem decoding item %d of %d", ii, numItems) + } + } + return results, nil +} diff --git a/lib/generics_test.go b/lib/generics_test.go index f00f6fafb..67beb30e0 100644 --- a/lib/generics_test.go +++ b/lib/generics_test.go @@ -1,6 +1,7 @@ package lib import ( + "bytes" "github.com/pkg/errors" "github.com/stretchr/testify/require" "testing" @@ -41,3 +42,42 @@ func TestSet(t *testing.T) { require.Equal(t, err.Error(), "TESTERROR") require.Nil(t, nilSet) } + +func TestGenericDeSoEncoderAndDecode(t *testing.T) { + + tne := &TransactorNonceEntry{ + TransactorPKID: &PKID{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32}, + Nonce: &DeSoNonce{ + ExpirationBlockHeight: 1723, + PartialID: 142, + }, + } + encoded := EncodeToBytes(0, tne, false) + var decoded *TransactorNonceEntry + var err error + decoded, err = DecodeDeSoEncoder(&TransactorNonceEntry{}, bytes.NewReader(encoded)) + + require.NoError(t, err) + require.True(t, decoded.TransactorPKID.Eq(tne.TransactorPKID)) + require.Equal(t, decoded.Nonce.ExpirationBlockHeight, tne.Nonce.ExpirationBlockHeight) + require.Equal(t, decoded.Nonce.PartialID, tne.Nonce.PartialID) + + tneSlice := []*TransactorNonceEntry{tne} + for i := 0; i < 10; i++ { + copiedTNE := tne.Copy() + copiedTNE.Nonce.ExpirationBlockHeight += 10 + copiedTNE.Nonce.PartialID += 10 + tneSlice = append(tneSlice, tne) + } + + encodedSlice := EncodeDeSoEncoderSlice[*TransactorNonceEntry](tneSlice, 0, false) + decodedSlice, err := DecodeDeSoEncoderSlice[*TransactorNonceEntry](bytes.NewReader(encodedSlice)) + + require.NoError(t, err) + require.Equal(t, len(decodedSlice), len(tneSlice)) + for i := 0; i < len(decodedSlice); i++ { + require.True(t, decodedSlice[i].TransactorPKID.Eq(tneSlice[i].TransactorPKID)) + require.Equal(t, decodedSlice[i].Nonce.ExpirationBlockHeight, tneSlice[i].Nonce.ExpirationBlockHeight) + require.Equal(t, decodedSlice[i].Nonce.PartialID, tneSlice[i].Nonce.PartialID) + } +} diff --git a/lib/mempool.go b/lib/mempool.go index 7f5c07f4e..548118e73 100644 --- a/lib/mempool.go +++ b/lib/mempool.go @@ -1946,6 +1946,18 @@ func ComputeTransactionMetadata(txn *MsgDeSoTxn, utxoView *UtxoView, blockHash * txindexMetadata, affectedPublicKeys := utxoView.CreateUnregisterAsValidatorTxindexMetadata(utxoOps[len(utxoOps)-1], txn) txnMeta.UnregisterAsValidatorTxindexMetadata = txindexMetadata txnMeta.AffectedPublicKeys = append(txnMeta.AffectedPublicKeys, affectedPublicKeys...) + case TxnTypeStake: + txindexMetadata, affectedPublicKeys := utxoView.CreateStakeTxindexMetadata(utxoOps[len(utxoOps)-1], txn) + txnMeta.StakeTxindexMetadata = txindexMetadata + txnMeta.AffectedPublicKeys = append(txnMeta.AffectedPublicKeys, affectedPublicKeys...) + case TxnTypeUnstake: + txindexMetadata, affectedPublicKeys := utxoView.CreateUnstakeTxindexMetadata(utxoOps[len(utxoOps)-1], txn) + txnMeta.UnstakeTxindexMetadata = txindexMetadata + txnMeta.AffectedPublicKeys = append(txnMeta.AffectedPublicKeys, affectedPublicKeys...) + case TxnTypeUnlockStake: + txindexMetadata, affectedPublicKeys := utxoView.CreateUnlockStakeTxindexMetadata(utxoOps[len(utxoOps)-1], txn) + txnMeta.UnlockStakeTxindexMetadata = txindexMetadata + txnMeta.AffectedPublicKeys = append(txnMeta.AffectedPublicKeys, affectedPublicKeys...) } return txnMeta } diff --git a/lib/network.go b/lib/network.go index 4a81b112b..ddaa1e342 100644 --- a/lib/network.go +++ b/lib/network.go @@ -241,8 +241,11 @@ const ( TxnTypeNewMessage TxnType = 33 TxnTypeRegisterAsValidator TxnType = 34 TxnTypeUnregisterAsValidator TxnType = 35 + TxnTypeStake TxnType = 36 + TxnTypeUnstake TxnType = 37 + TxnTypeUnlockStake TxnType = 38 - // NEXT_ID = 36 + // NEXT_ID = 39 ) type TxnString string @@ -284,6 +287,9 @@ const ( TxnStringNewMessage TxnString = "NEW_MESSAGE" TxnStringRegisterAsValidator TxnString = "REGISTER_AS_VALIDATOR" TxnStringUnregisterAsValidator TxnString = "UNREGISTER_AS_VALIDATOR" + TxnStringStake TxnString = "STAKE" + TxnStringUnstake TxnString = "UNSTAKE" + TxnStringUnlockStake TxnString = "UNLOCK_STAKE" ) var ( @@ -296,7 +302,7 @@ var ( TxnTypeDAOCoin, TxnTypeDAOCoinTransfer, TxnTypeDAOCoinLimitOrder, TxnTypeCreateUserAssociation, TxnTypeDeleteUserAssociation, TxnTypeCreatePostAssociation, TxnTypeDeletePostAssociation, TxnTypeAccessGroup, TxnTypeAccessGroupMembers, TxnTypeNewMessage, TxnTypeRegisterAsValidator, - TxnTypeUnregisterAsValidator, + TxnTypeUnregisterAsValidator, TxnTypeStake, TxnTypeUnstake, TxnTypeUnlockStake, } AllTxnString = []TxnString{ TxnStringUnset, TxnStringBlockReward, TxnStringBasicTransfer, TxnStringBitcoinExchange, TxnStringPrivateMessage, @@ -307,7 +313,7 @@ var ( TxnStringDAOCoin, TxnStringDAOCoinTransfer, TxnStringDAOCoinLimitOrder, TxnStringCreateUserAssociation, TxnStringDeleteUserAssociation, TxnStringCreatePostAssociation, TxnStringDeletePostAssociation, TxnStringAccessGroup, TxnStringAccessGroupMembers, TxnStringNewMessage, TxnStringRegisterAsValidator, - TxnStringUnregisterAsValidator, + TxnStringUnregisterAsValidator, TxnStringStake, TxnStringUnstake, TxnStringUnlockStake, } ) @@ -391,6 +397,12 @@ func (txnType TxnType) GetTxnString() TxnString { return TxnStringRegisterAsValidator case TxnTypeUnregisterAsValidator: return TxnStringUnregisterAsValidator + case TxnTypeStake: + return TxnStringStake + case TxnTypeUnstake: + return TxnStringUnstake + case TxnTypeUnlockStake: + return TxnStringUnlockStake default: return TxnStringUndefined } @@ -468,6 +480,12 @@ func GetTxnTypeFromString(txnString TxnString) TxnType { return TxnTypeRegisterAsValidator case TxnStringUnregisterAsValidator: return TxnTypeUnregisterAsValidator + case TxnStringStake: + return TxnTypeStake + case TxnStringUnstake: + return TxnTypeUnstake + case TxnStringUnlockStake: + return TxnTypeUnlockStake default: // TxnTypeUnset means we couldn't find a matching txn type return TxnTypeUnset @@ -553,6 +571,12 @@ func NewTxnMetadata(txType TxnType) (DeSoTxnMetadata, error) { return (&RegisterAsValidatorMetadata{}).New(), nil case TxnTypeUnregisterAsValidator: return (&UnregisterAsValidatorMetadata{}).New(), nil + case TxnTypeStake: + return (&StakeMetadata{}).New(), nil + case TxnTypeUnstake: + return (&UnstakeMetadata{}).New(), nil + case TxnTypeUnlockStake: + return (&UnlockStakeMetadata{}).New(), nil default: return nil, fmt.Errorf("NewTxnMetadata: Unrecognized TxnType: %v; make sure you add the new type of transaction to NewTxnMetadata", txType) } @@ -5355,6 +5379,20 @@ type TransactionSpendingLimit struct { // - AppScopeType: one of { Any, Scoped } // - AssociationOperation: one of { Any, Create, Delete } AssociationLimitMap map[AssociationLimitKey]uint64 + + // ===== ENCODER MIGRATION ProofOfStakeNewTxnTypesMigration ===== + // ValidatorPKID || StakerPKID to amount of stake-able $DESO. + // Note that this is not a limit on the number of Stake txns that + // this derived key can perform but instead a limit on the amount + // of $DESO this derived key can stake. + StakeLimitMap map[StakeLimitKey]*uint256.Int + // ValidatorPKID || StakerPKID to amount of unstake-able DESO. + // Note that this is not a limit on the number of Unstake txns that + // this derived key can perform but instead a limit on the amount + // of $DESO this derived key can unstake. + UnstakeLimitMap map[StakeLimitKey]*uint256.Int + // ValidatorPKID || StakerPKID to number of UnlockStake transactions. + UnlockStakeLimitMap map[StakeLimitKey]uint64 } // ToMetamaskString encodes the TransactionSpendingLimit into a Metamask-compatible string. The encoded string will @@ -5599,6 +5637,102 @@ func (tsl *TransactionSpendingLimit) ToMetamaskString(params *DeSoParams) string indentationCounter-- } + // StakeLimitMap + if len(tsl.StakeLimitMap) > 0 { + var stakeLimitStr []string + str += _indt(indentationCounter) + "Staking Restrictions:\n" + indentationCounter++ + for limitKey, limit := range tsl.StakeLimitMap { + opString := _indt(indentationCounter) + "[\n" + + indentationCounter++ + // ValidatorPKID + validatorPublicKeyBase58Check := "Any" + if !limitKey.ValidatorPKID.Eq(&ZeroPKID) { + validatorPublicKeyBase58Check = Base58CheckEncode(limitKey.ValidatorPKID.ToBytes(), false, params) + } + opString += _indt(indentationCounter) + "Validator PKID: " + validatorPublicKeyBase58Check + "\n" + // StakerPKID + stakerPublicKeyBase58Check := Base58CheckEncode(limitKey.StakerPKID.ToBytes(), false, params) + opString += _indt(indentationCounter) + "Staker PKID: " + stakerPublicKeyBase58Check + "\n" + // StakeLimit + stakeLimitDESO := NewFloat().Quo( + NewFloat().SetInt(limit.ToBig()), NewFloat().SetUint64(NanosPerUnit), + ) + opString += _indt(indentationCounter) + fmt.Sprintf("Staking Limit: %.2f $DESO\n", stakeLimitDESO) + + indentationCounter-- + opString += _indt(indentationCounter) + "]\n" + stakeLimitStr = append(stakeLimitStr, opString) + } + // Ensure deterministic ordering of the transaction count limit strings by doing a lexicographical sort. + sortStringsAndAddToLimitStr(stakeLimitStr) + indentationCounter-- + } + + // UnstakeLimitMap + if len(tsl.UnstakeLimitMap) > 0 { + var unstakeLimitStr []string + str += _indt(indentationCounter) + "Unstaking Restrictions:\n" + indentationCounter++ + for limitKey, limit := range tsl.UnstakeLimitMap { + opString := _indt(indentationCounter) + "[\n" + + indentationCounter++ + // ValidatorPKID + validatorPublicKeyBase58Check := "Any" + if !limitKey.ValidatorPKID.Eq(&ZeroPKID) { + validatorPublicKeyBase58Check = Base58CheckEncode(limitKey.ValidatorPKID.ToBytes(), false, params) + } + opString += _indt(indentationCounter) + "Validator PKID: " + validatorPublicKeyBase58Check + "\n" + // StakerPKID + stakerPublicKeyBase58Check := Base58CheckEncode(limitKey.StakerPKID.ToBytes(), false, params) + opString += _indt(indentationCounter) + "Staker PKID: " + stakerPublicKeyBase58Check + "\n" + // UnstakeLimit + unstakeLimitDESO := NewFloat().Quo( + NewFloat().SetInt(limit.ToBig()), NewFloat().SetUint64(NanosPerUnit), + ) + opString += _indt(indentationCounter) + fmt.Sprintf("Unstaking Limit: %.2f $DESO\n", unstakeLimitDESO) + + indentationCounter-- + opString += _indt(indentationCounter) + "]\n" + unstakeLimitStr = append(unstakeLimitStr, opString) + } + // Ensure deterministic ordering of the transaction count limit strings by doing a lexicographical sort. + sortStringsAndAddToLimitStr(unstakeLimitStr) + indentationCounter-- + } + + // UnlockStakeLimitMap + if len(tsl.UnlockStakeLimitMap) > 0 { + var unlockStakeLimitStr []string + str += _indt(indentationCounter) + "Unlocking Stake Restrictions:\n" + indentationCounter++ + for limitKey, limit := range tsl.UnlockStakeLimitMap { + opString := _indt(indentationCounter) + "[\n" + + indentationCounter++ + // ValidatorPKID + validatorPublicKeyBase58Check := "Any" + if !limitKey.ValidatorPKID.Eq(&ZeroPKID) { + validatorPublicKeyBase58Check = Base58CheckEncode(limitKey.ValidatorPKID.ToBytes(), false, params) + } + opString += _indt(indentationCounter) + "Validator PKID: " + validatorPublicKeyBase58Check + "\n" + // StakerPKID + stakerPublicKeyBase58Check := Base58CheckEncode(limitKey.StakerPKID.ToBytes(), false, params) + opString += _indt(indentationCounter) + "Staker PKID: " + stakerPublicKeyBase58Check + "\n" + // UnlockStakeLimit + opString += _indt(indentationCounter) + "Transaction Count: " + strconv.FormatUint(limit, 10) + "\n" + + indentationCounter-- + opString += _indt(indentationCounter) + "]\n" + unlockStakeLimitStr = append(unlockStakeLimitStr, opString) + } + // Ensure deterministic ordering of the transaction count limit strings by doing a lexicographical sort. + sortStringsAndAddToLimitStr(unlockStakeLimitStr) + indentationCounter-- + } + // IsUnlimited if tsl.IsUnlimited { str += "Unlimited" @@ -5812,6 +5946,69 @@ func (tsl *TransactionSpendingLimit) ToBytes(blockHeight uint64) ([]byte, error) data = append(data, accessGroupsBytes...) } + // StakeLimitMap, UnstakeLimitMap, and UnlockStakeLimitMap, gated by the encoder migration. + if MigrationTriggered(blockHeight, ProofOfStakeNewTxnTypesMigration) { + // StakeLimitMap + stakeLimitMapLength := uint64(len(tsl.StakeLimitMap)) + data = append(data, UintToBuf(stakeLimitMapLength)...) + if stakeLimitMapLength > 0 { + keys, err := SafeMakeSliceWithLengthAndCapacity[StakeLimitKey](0, stakeLimitMapLength) + if err != nil { + return nil, err + } + for key := range tsl.StakeLimitMap { + keys = append(keys, key) + } + sort.Slice(keys, func(ii, jj int) bool { + return hex.EncodeToString(keys[ii].Encode()) < hex.EncodeToString(keys[jj].Encode()) + }) + for _, key := range keys { + data = append(data, key.Encode()...) + data = append(data, EncodeUint256(tsl.StakeLimitMap[key])...) + } + } + + // UnstakeLimitMap + unstakeLimitMapLength := uint64(len(tsl.UnstakeLimitMap)) + data = append(data, UintToBuf(unstakeLimitMapLength)...) + if unstakeLimitMapLength > 0 { + keys, err := SafeMakeSliceWithLengthAndCapacity[StakeLimitKey](0, unstakeLimitMapLength) + if err != nil { + return nil, err + } + for key := range tsl.UnstakeLimitMap { + keys = append(keys, key) + } + sort.Slice(keys, func(ii, jj int) bool { + return hex.EncodeToString(keys[ii].Encode()) < hex.EncodeToString(keys[jj].Encode()) + }) + for _, key := range keys { + data = append(data, key.Encode()...) + data = append(data, EncodeUint256(tsl.UnstakeLimitMap[key])...) + } + } + + // UnlockStakeLimitMap + unlockStakeLimitMapLength := uint64(len(tsl.UnlockStakeLimitMap)) + data = append(data, UintToBuf(unlockStakeLimitMapLength)...) + if unlockStakeLimitMapLength > 0 { + keys, err := SafeMakeSliceWithLengthAndCapacity[StakeLimitKey](0, unlockStakeLimitMapLength) + if err != nil { + return nil, err + } + for key := range tsl.UnlockStakeLimitMap { + keys = append(keys, key) + } + sort.Slice(keys, func(ii, jj int) bool { + return hex.EncodeToString(keys[ii].Encode()) < hex.EncodeToString(keys[jj].Encode()) + }) + for _, key := range keys { + data = append(data, key.Encode()...) + data = append(data, UintToBuf(tsl.UnlockStakeLimitMap[key])...) + } + } + } + return data, nil } @@ -6021,6 +6218,81 @@ func (tsl *TransactionSpendingLimit) FromBytes(blockHeight uint64, rr *bytes.Rea } } + // StakeLimitMap, UnstakeLimitMap, and UnlockStakeLimitMap, gated by the encoder migration. + if MigrationTriggered(blockHeight, ProofOfStakeNewTxnTypesMigration) { + // StakeLimitMap + stakeLimitMapLen, err := ReadUvarint(rr) + if err != nil { + return err + } + tsl.StakeLimitMap = make(map[StakeLimitKey]*uint256.Int) + if stakeLimitMapLen > 0 { + for ii := uint64(0); ii < stakeLimitMapLen; ii++ { + stakeLimitKey := &StakeLimitKey{} + if err = stakeLimitKey.Decode(rr); err != nil { + return errors.Wrap(err, "Error decoding StakeLimitKey: ") + } + var stakeLimitDESONanos *uint256.Int + stakeLimitDESONanos, err = DecodeUint256(rr) + if err != nil { + return err + } + if _, exists := tsl.StakeLimitMap[*stakeLimitKey]; exists { + return errors.New("StakeLimitKey already exists in StakeLimitMap") + } + tsl.StakeLimitMap[*stakeLimitKey] = stakeLimitDESONanos + } + } + + // UnstakeLimitMap + unstakeLimitMapLen, err := ReadUvarint(rr) + if err != nil { + return err + } + tsl.UnstakeLimitMap = make(map[StakeLimitKey]*uint256.Int) + if unstakeLimitMapLen > 0 { + for ii := uint64(0); ii < unstakeLimitMapLen; ii++ { + stakeLimitKey := &StakeLimitKey{} + if err = stakeLimitKey.Decode(rr); err != nil { + return errors.Wrap(err, "Error decoding StakeLimitKey: ") + } + var unstakeLimitDESONanos *uint256.Int + unstakeLimitDESONanos, err = DecodeUint256(rr) + if err != nil { + return err + } + if _, exists := tsl.UnstakeLimitMap[*stakeLimitKey]; exists { + return errors.New("StakeLimitKey already exists in UnstakeLimitMap") + } + tsl.UnstakeLimitMap[*stakeLimitKey] = unstakeLimitDESONanos + } + } + + // UnlockStakeLimitMap + unlockStakeLimitMapLen, err := ReadUvarint(rr) + if err != nil { + return err + } + tsl.UnlockStakeLimitMap = make(map[StakeLimitKey]uint64) + if unlockStakeLimitMapLen > 0 { + for ii := uint64(0); ii < unlockStakeLimitMapLen; ii++ { + stakeLimitKey := &StakeLimitKey{} + if err = stakeLimitKey.Decode(rr); err != nil { + return errors.Wrap(err, "Error decoding StakeLimitKey: ") + } + var operationCount uint64 + operationCount, err = ReadUvarint(rr) + if err != nil { + return err + } + if _, exists := tsl.UnlockStakeLimitMap[*stakeLimitKey]; exists { + return errors.New("StakeLimitKey already exists in UnlockStakeLimitMap") + } + tsl.UnlockStakeLimitMap[*stakeLimitKey] = operationCount + } + } + } + return nil } @@ -6077,6 +6349,9 @@ func (tsl *TransactionSpendingLimit) Copy() *TransactionSpendingLimit { DAOCoinLimitOrderLimitMap: make(map[DAOCoinLimitOrderLimitKey]uint64), AccessGroupMap: make(map[AccessGroupLimitKey]uint64), AccessGroupMemberMap: make(map[AccessGroupMemberLimitKey]uint64), + StakeLimitMap: make(map[StakeLimitKey]*uint256.Int), + UnstakeLimitMap: make(map[StakeLimitKey]*uint256.Int), + UnlockStakeLimitMap: make(map[StakeLimitKey]uint64), IsUnlimited: tsl.IsUnlimited, } @@ -6117,11 +6392,23 @@ func (tsl *TransactionSpendingLimit) Copy() *TransactionSpendingLimit { copyTSL.AccessGroupMemberMap[accessGroupMemberLimitKey] = accessGroupMemberCount } + for stakeLimitKey, stakeLimitDESONanos := range tsl.StakeLimitMap { + copyTSL.StakeLimitMap[stakeLimitKey] = stakeLimitDESONanos + } + + for stakeLimitKey, unstakeLimitDESONanos := range tsl.UnstakeLimitMap { + copyTSL.UnstakeLimitMap[stakeLimitKey] = unstakeLimitDESONanos + } + + for stakeLimitKey, unlockStakeOperationCount := range tsl.UnlockStakeLimitMap { + copyTSL.UnlockStakeLimitMap[stakeLimitKey] = unlockStakeOperationCount + } + return copyTSL } func (bav *UtxoView) CheckIfValidUnlimitedSpendingLimit(tsl *TransactionSpendingLimit, blockHeight uint32) (_isUnlimited bool, _err error) { - AssertDependencyStructFieldNumbers(&TransactionSpendingLimit{}, 10) + AssertDependencyStructFieldNumbers(&TransactionSpendingLimit{}, 13) if tsl.IsUnlimited && blockHeight < bav.Params.ForkHeights.DeSoUnlimitedDerivedKeysBlockHeight { return false, RuleErrorUnlimitedDerivedKeyBeforeBlockHeight @@ -6137,7 +6424,10 @@ func (bav *UtxoView) CheckIfValidUnlimitedSpendingLimit(tsl *TransactionSpending len(tsl.DAOCoinLimitOrderLimitMap) > 0 || len(tsl.AssociationLimitMap) > 0 || len(tsl.AccessGroupMap) > 0 || - len(tsl.AccessGroupMemberMap) > 0) { + len(tsl.AccessGroupMemberMap) > 0 || + len(tsl.StakeLimitMap) > 0 || + len(tsl.UnstakeLimitMap) > 0 || + len(tsl.UnlockStakeLimitMap) > 0) { return tsl.IsUnlimited, RuleErrorUnlimitedDerivedKeyNonEmptySpendingLimits } diff --git a/lib/network_test.go b/lib/network_test.go index f83d99d89..b99595c94 100644 --- a/lib/network_test.go +++ b/lib/network_test.go @@ -1555,7 +1555,7 @@ func TestUnlimitedSpendingLimitMetamaskEncoding(t *testing.T) { // Test the spending limit encoding using the standard scheme. spendingLimitBytes, err := spendingLimit.ToBytes(1) require.NoError(err) - require.Equal(true, reflect.DeepEqual(spendingLimitBytes, []byte{0, 0, 0, 0, 0, 0, 1, 0, 0, 0})) + require.Equal(true, reflect.DeepEqual(spendingLimitBytes, []byte{0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0})) // Test the spending limit encoding using the metamask scheme. require.Equal(true, reflect.DeepEqual( diff --git a/lib/types.go b/lib/types.go index cb5a9f654..71bb6630f 100644 --- a/lib/types.go +++ b/lib/types.go @@ -273,6 +273,10 @@ func ReadOptionalBlockHash(rr *bytes.Reader) (*BlockHash, error) { return nil, nil } +// EncodeOptionalUint256 guarantees fixed-width encoding which is useful +// in BadgerDB keys. It is less space-efficient than EncodeUint256, +// which should be used elsewhere. Both EncodeUint256 and +// EncodeOptionalUint256 can handle nil inputs. func EncodeOptionalUint256(val *uint256.Int) []byte { if val == nil { return UintToBuf(uint64(0))