diff --git a/lib/block_view.go b/lib/block_view.go index e9f55bde5..0ce1a150e 100644 --- a/lib/block_view.go +++ b/lib/block_view.go @@ -1373,6 +1373,10 @@ func (bav *UtxoView) DisconnectTransaction(currentTxn *MsgDeSoTxn, txnHash *Bloc case TxnTypeUnlockStake: return bav._disconnectUnlockStake( OperationTypeUnlockStake, currentTxn, txnHash, utxoOpsForTxn, blockHeight) + + case TxnTypeUnjailValidator: + return bav._disconnectUnjailValidator( + OperationTypeUnjailValidator, currentTxn, txnHash, utxoOpsForTxn, blockHeight) } return fmt.Errorf("DisconnectBlock: Unimplemented txn type %v", currentTxn.TxnMeta.GetTxnType().String()) @@ -3311,6 +3315,9 @@ func (bav *UtxoView) _connectTransaction(txn *MsgDeSoTxn, txHash *BlockHash, case TxnTypeUnlockStake: totalInput, totalOutput, utxoOpsForTxn, err = bav._connectUnlockStake(txn, txHash, blockHeight, verifySignatures) + case TxnTypeUnjailValidator: + totalInput, totalOutput, utxoOpsForTxn, err = bav._connectUnjailValidator(txn, txHash, blockHeight, verifySignatures) + default: err = fmt.Errorf("ConnectTransaction: Unimplemented txn type %v", txn.TxnMeta.GetTxnType().String()) } diff --git a/lib/block_view_stake_test.go b/lib/block_view_stake_test.go index 8cb71fbc8..fc8360997 100644 --- a/lib/block_view_stake_test.go +++ b/lib/block_view_stake_test.go @@ -12,7 +12,6 @@ import ( func TestStaking(t *testing.T) { _testStaking(t, false) _testStaking(t, true) - _testStakingWithDerivedKey(t) } func _testStaking(t *testing.T, flushToDB bool) { @@ -103,9 +102,7 @@ func _testStaking(t *testing.T, flushToDB bool) { registerAsValidatorMetadata := &RegisterAsValidatorMetadata{ Domains: [][]byte{[]byte("https://example.com")}, } - _, _, _, err = _submitRegisterAsValidatorTxn( - testMeta, m0Pub, m0Priv, registerAsValidatorMetadata, nil, flushToDB, - ) + _, err = _submitRegisterAsValidatorTxn(testMeta, m0Pub, m0Priv, registerAsValidatorMetadata, nil, flushToDB) require.NoError(t, err) validatorEntry, err := utxoView().GetValidatorByPKID(m0PKID) @@ -755,7 +752,7 @@ func _submitUnlockStakeTxn( return fees, nil } -func _testStakingWithDerivedKey(t *testing.T) { +func TestStakingWithDerivedKey(t *testing.T) { var derivedKeyPriv string var err error @@ -957,9 +954,7 @@ func _testStakingWithDerivedKey(t *testing.T) { registerAsValidatorMetadata := &RegisterAsValidatorMetadata{ Domains: [][]byte{[]byte("https://example1.com")}, } - _, _, _, err = _submitRegisterAsValidatorTxn( - testMeta, m0Pub, m0Priv, registerAsValidatorMetadata, nil, true, - ) + _, err = _submitRegisterAsValidatorTxn(testMeta, m0Pub, m0Priv, registerAsValidatorMetadata, nil, true) require.NoError(t, err) } { @@ -967,9 +962,7 @@ func _testStakingWithDerivedKey(t *testing.T) { registerAsValidatorMetadata := &RegisterAsValidatorMetadata{ Domains: [][]byte{[]byte("https://example2.com")}, } - _, _, _, err = _submitRegisterAsValidatorTxn( - testMeta, m1Pub, m1Priv, registerAsValidatorMetadata, nil, true, - ) + _, err = _submitRegisterAsValidatorTxn(testMeta, m1Pub, m1Priv, registerAsValidatorMetadata, nil, true) require.NoError(t, err) } { @@ -1783,7 +1776,6 @@ func TestStakeLockupEpochDuration(t *testing.T) { chain.snapshot = nil // For these tests, we set StakeLockupEpochDuration to 3. - // We test the lockup logic in a separate test. params.StakeLockupEpochDuration = 3 // Mine a few blocks to give the senderPkString some money. @@ -1843,7 +1835,7 @@ func TestStakeLockupEpochDuration(t *testing.T) { registerMetadata := &RegisterAsValidatorMetadata{ Domains: [][]byte{[]byte("https://m1.com")}, } - _, _, _, err = _submitRegisterAsValidatorTxn(testMeta, m0Pub, m0Priv, registerMetadata, nil, true) + _, err = _submitRegisterAsValidatorTxn(testMeta, m0Pub, m0Priv, registerMetadata, nil, true) require.NoError(t, err) validatorEntry, err := newUtxoView().GetValidatorByPKID(m0PKID) diff --git a/lib/block_view_types.go b/lib/block_view_types.go index 679d737ec..9aa041678 100644 --- a/lib/block_view_types.go +++ b/lib/block_view_types.go @@ -156,9 +156,10 @@ const ( EncoderTypeStakeTxindexMetadata EncoderType = 1000032 EncoderTypeUnstakeTxindexMetadata EncoderType = 1000033 EncoderTypeUnlockStakeTxindexMetadata EncoderType = 1000034 + EncoderTypeUnjailValidatorTxindexMetadata EncoderType = 1000035 // EncoderTypeEndTxIndex encoder type should be at the end and is used for automated tests. - EncoderTypeEndTxIndex EncoderType = 1000035 + EncoderTypeEndTxIndex EncoderType = 1000036 ) // This function translates the EncoderType into an empty DeSoEncoder struct. @@ -327,6 +328,8 @@ func (encoderType EncoderType) New() DeSoEncoder { return &UnstakeTxindexMetadata{} case EncoderTypeUnlockStakeTxindexMetadata: return &UnlockStakeTxindexMetadata{} + case EncoderTypeUnjailValidatorTxindexMetadata: + return &UnjailValidatorTxindexMetadata{} default: return nil } @@ -626,8 +629,9 @@ const ( OperationTypeStake OperationType = 41 OperationTypeUnstake OperationType = 42 OperationTypeUnlockStake OperationType = 43 + OperationTypeUnjailValidator OperationType = 44 - // NEXT_TAG = 44 + // NEXT_TAG = 45 ) func (op OperationType) String() string { @@ -718,6 +722,8 @@ func (op OperationType) String() string { return "OperationTypeUnstake" case OperationTypeUnlockStake: return "OperationTypeUnlockStake" + case OperationTypeUnjailValidator: + return "OperationTypeUnjailValidator" } return "OperationTypeUNKNOWN" } diff --git a/lib/block_view_validator.go b/lib/block_view_validator.go index 14aaa2c28..8993dbdc7 100644 --- a/lib/block_view_validator.go +++ b/lib/block_view_validator.go @@ -324,6 +324,28 @@ func (txnData *UnregisterAsValidatorMetadata) New() DeSoTxnMetadata { return &UnregisterAsValidatorMetadata{} } +// +// TYPES: UnjailValidatorMetadata +// + +type UnjailValidatorMetadata struct{} + +func (txnData *UnjailValidatorMetadata) GetTxnType() TxnType { + return TxnTypeUnjailValidator +} + +func (txnData *UnjailValidatorMetadata) ToBytes(preSignature bool) ([]byte, error) { + return []byte{}, nil +} + +func (txnData *UnjailValidatorMetadata) FromBytes(data []byte) error { + return nil +} + +func (txnData *UnjailValidatorMetadata) New() DeSoTxnMetadata { + return &UnjailValidatorMetadata{} +} + // // TYPES: RegisterAsValidatorTxindexMetadata // @@ -506,6 +528,29 @@ func (txindexMetadata *UnregisterAsValidatorTxindexMetadata) GetEncoderType() En return EncoderTypeUnregisterAsValidatorTxindexMetadata } +// +// TYPES: UnjailValidatorTxindexMetadata +// + +type UnjailValidatorTxindexMetadata struct { +} + +func (txindexMetadata *UnjailValidatorTxindexMetadata) RawEncodeWithoutMetadata(blockHeight uint64, skipMetadata ...bool) []byte { + return []byte{} +} + +func (txindexMetadata *UnjailValidatorTxindexMetadata) RawDecodeWithoutMetadata(blockHeight uint64, rr *bytes.Reader) error { + return nil +} + +func (txindexMetadata *UnjailValidatorTxindexMetadata) GetVersionByte(blockHeight uint64) byte { + return 0 +} + +func (txindexMetadata *UnjailValidatorTxindexMetadata) GetEncoderType() EncoderType { + return EncoderTypeUnjailValidatorTxindexMetadata +} + // // DB UTILS // @@ -832,7 +877,7 @@ func (bc *Blockchain) CreateUnregisterAsValidatorTxn( } // Validate txn metadata. - if err = utxoView.IsValidUnregisterAsValidatorMetadata(transactorPublicKey, metadata); err != nil { + if err = utxoView.IsValidUnregisterAsValidatorMetadata(transactorPublicKey); err != nil { return nil, 0, 0, 0, errors.Wrapf( err, "Blockchain.CreateUnregisterAsValidatorTxn: invalid txn metadata: ", ) @@ -866,6 +911,82 @@ func (bc *Blockchain) CreateUnregisterAsValidatorTxn( return txn, totalInput, changeAmount, fees, nil } +func (bc *Blockchain) CreateUnjailValidatorTxn( + transactorPublicKey []byte, + metadata *UnjailValidatorMetadata, + 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 UnjailValidator 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.CreateUnjailValidatorTxn: problem creating new utxo view: ", + ) + } + if mempool != nil { + utxoView, err = mempool.GetAugmentedUniversalView() + if err != nil { + return nil, 0, 0, 0, errors.Wrapf( + err, "Blockchain.CreateUnjailValidatorTxn: problem getting augmented utxo view from mempool: ", + ) + } + } + + // Validate txn metadata. + if err = utxoView.IsValidUnjailValidatorMetadata(transactorPublicKey); err != nil { + return nil, 0, 0, 0, errors.Wrapf( + err, "Blockchain.CreateUnjailValidatorTxn: 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.CreateUnjailValidatorTxn: 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.CreateUnjailValidatorTxn: 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.CreateUnjailValidatorTxn: spend amount is non-zero: %d", spendAmount, + ) + } + return txn, totalInput, changeAmount, fees, nil +} + // // UTXO VIEW UTILS // @@ -1107,11 +1228,8 @@ func (bav *UtxoView) _connectUnregisterAsValidator( // public key so there is no need to verify anything further. } - // Grab the txn metadata. - txMeta := txn.TxnMeta.(*UnregisterAsValidatorMetadata) - - // Validate the txn metadata. - if err = bav.IsValidUnregisterAsValidatorMetadata(txn.PublicKey, txMeta); err != nil { + // Validate the transactor. + if err = bav.IsValidUnregisterAsValidatorMetadata(txn.PublicKey); err != nil { return 0, 0, nil, errors.Wrapf(err, "_connectUnregisterAsValidator: ") } @@ -1327,6 +1445,156 @@ func (bav *UtxoView) _disconnectUnregisterAsValidator( ) } +func (bav *UtxoView) _connectUnjailValidator( + 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 { + return 0, 0, nil, errors.Wrapf(RuleErrorProofofStakeTxnBeforeBlockHeight, "_connectUnjailValidator: ") + } + + // Validate the txn TxnType. + if txn.TxnMeta.GetTxnType() != TxnTypeUnjailValidator { + return 0, 0, nil, fmt.Errorf( + "_connectUnjailValidator: 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, "_connectUnjailValidator: ") + } + 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. + } + + // Validate the transactor. + if err = bav.IsValidUnjailValidatorMetadata(txn.PublicKey); err != nil { + return 0, 0, nil, errors.Wrapf(err, "_connectUnjailValidator: ") + } + + // At this point, we have validated in IsValidUnjailValidatorMetadata() + // that the ValidatorEntry exists, belongs to the transactor, is jailed, + // and a sufficient number of epochs have elapsed for this validator to + // be unjailed. + + // Convert TransactorPublicKey to TransactorPKID. + transactorPKIDEntry := bav.GetPKIDForPublicKey(txn.PublicKey) + if transactorPKIDEntry == nil || transactorPKIDEntry.isDeleted { + return 0, 0, nil, errors.Wrapf(RuleErrorInvalidValidatorPKID, "_connectUnjailValidator: ") + } + + // Retrieve the existing ValidatorEntry that will be overwritten. + // This ValidatorEntry will be restored if we disconnect this txn. + prevValidatorEntry, err := bav.GetValidatorByPKID(transactorPKIDEntry.PKID) + if err != nil { + return 0, 0, nil, errors.Wrapf(err, "_connectUnjailValidator: ") + } + if prevValidatorEntry == nil || prevValidatorEntry.isDeleted { + return 0, 0, nil, errors.Wrapf(RuleErrorValidatorNotFound, "_connectUnjailValidator: ") + } + + // Copy the existing ValidatorEntry. + currentValidatorEntry := prevValidatorEntry.Copy() + + // Retrieve the CurrentEpochNumber. + currentEpochNumber, err := bav.GetCurrentEpochNumber() + if err != nil { + return 0, 0, nil, errors.Wrapf(err, "_connectUnjailValidator: error retrieving CurrentEpochNumber: ") + } + + // Update LastActiveAtEpochNumber to CurrentEpochNumber. + currentValidatorEntry.LastActiveAtEpochNumber = currentEpochNumber + + // Reset JailedAtEpochNumber to zero. + currentValidatorEntry.JailedAtEpochNumber = 0 + + // Merge ExtraData with existing ExtraData. + currentValidatorEntry.ExtraData = mergeExtraData(prevValidatorEntry.ExtraData, txn.ExtraData) + + // Delete the PrevValidatorEntry. + bav._deleteValidatorEntryMappings(prevValidatorEntry) + + // Set the CurrentValidatorEntry. + bav._setValidatorEntryMappings(currentValidatorEntry) + + // Add a UTXO operation + utxoOpsForTxn = append(utxoOpsForTxn, &UtxoOperation{ + Type: OperationTypeUnjailValidator, + PrevValidatorEntry: prevValidatorEntry, + }) + return totalInput, totalOutput, utxoOpsForTxn, nil +} + +func (bav *UtxoView) _disconnectUnjailValidator( + operationType OperationType, + currentTxn *MsgDeSoTxn, + txHash *BlockHash, + utxoOpsForTxn []*UtxoOperation, + blockHeight uint32, +) error { + // Validate the starting block height. + if blockHeight < bav.Params.ForkHeights.ProofOfStakeNewTxnTypesBlockHeight { + return errors.Wrapf(RuleErrorProofofStakeTxnBeforeBlockHeight, "_disconnectUnjailValidator: ") + } + + // Validate the last operation is an UnjailValidator operation. + if len(utxoOpsForTxn) == 0 { + return fmt.Errorf("_disconnectUnjailValidator: utxoOperations are missing") + } + operationIndex := len(utxoOpsForTxn) - 1 + operationData := utxoOpsForTxn[operationIndex] + if operationData.Type != OperationTypeUnjailValidator { + return fmt.Errorf( + "_disconnectUnjailValidator: trying to revert %v but found %v", + OperationTypeUnjailValidator, + operationData.Type, + ) + } + + // Convert TransactorPublicKey to TransactorPKID. + transactorPKIDEntry := bav.GetPKIDForPublicKey(currentTxn.PublicKey) + if transactorPKIDEntry == nil || transactorPKIDEntry.isDeleted { + return errors.Wrapf(RuleErrorInvalidValidatorPKID, "_disconnectUnjailValidator: ") + } + + // Delete the current ValidatorEntry. + currentValidatorEntry, err := bav.GetValidatorByPKID(transactorPKIDEntry.PKID) + if err != nil { + return errors.Wrapf(err, "_disconnectUnjailValidator: ") + } + if currentValidatorEntry == nil || currentValidatorEntry.isDeleted { + return errors.Wrapf(RuleErrorValidatorNotFound, "_disconnectUnjailValidator: ") + } + bav._deleteValidatorEntryMappings(currentValidatorEntry) + + // Restore the PrevValidatorEntry. + prevValidatorEntry := operationData.PrevValidatorEntry + if prevValidatorEntry == nil { + return errors.New("_disconnectUnjailValidator: PrevValidatorEntry is nil") + } + bav._setValidatorEntryMappings(prevValidatorEntry) + + // Disconnect the BasicTransfer. + return bav._disconnectBasicTransfer( + currentTxn, txHash, utxoOpsForTxn[:operationIndex], blockHeight, + ) +} + func (bav *UtxoView) IsValidRegisterAsValidatorMetadata(transactorPublicKey []byte, metadata *RegisterAsValidatorMetadata) error { // Validate ValidatorPKID. transactorPKIDEntry := bav.GetPKIDForPublicKey(transactorPublicKey) @@ -1384,7 +1652,7 @@ func (bav *UtxoView) IsValidRegisterAsValidatorMetadata(transactorPublicKey []by return nil } -func (bav *UtxoView) IsValidUnregisterAsValidatorMetadata(transactorPublicKey []byte, metadata *UnregisterAsValidatorMetadata) error { +func (bav *UtxoView) IsValidUnregisterAsValidatorMetadata(transactorPublicKey []byte) error { // Validate ValidatorPKID. transactorPKIDEntry := bav.GetPKIDForPublicKey(transactorPublicKey) if transactorPKIDEntry == nil || transactorPKIDEntry.isDeleted { @@ -1396,13 +1664,48 @@ func (bav *UtxoView) IsValidUnregisterAsValidatorMetadata(transactorPublicKey [] if err != nil { return errors.Wrapf(err, "UtxoView.IsValidUnregisterAsValidatorMetadata: ") } - if validatorEntry == nil { + if validatorEntry == nil || validatorEntry.isDeleted { return errors.Wrapf(RuleErrorValidatorNotFound, "UtxoView.IsValidUnregisterAsValidatorMetadata: ") } return nil } +func (bav *UtxoView) IsValidUnjailValidatorMetadata(transactorPublicKey []byte) error { + // Validate ValidatorPKID. + transactorPKIDEntry := bav.GetPKIDForPublicKey(transactorPublicKey) + if transactorPKIDEntry == nil || transactorPKIDEntry.isDeleted { + return errors.Wrapf(RuleErrorInvalidValidatorPKID, "UtxoView.IsValidUnjailValidatorMetadata: ") + } + + // Validate ValidatorEntry exists. + validatorEntry, err := bav.GetValidatorByPKID(transactorPKIDEntry.PKID) + if err != nil { + return errors.Wrapf(err, "UtxoView.IsValidUnjailValidatorMetadata: ") + } + if validatorEntry == nil || validatorEntry.isDeleted { + return errors.Wrapf(RuleErrorValidatorNotFound, "UtxoView.IsValidUnjailValidatorMetadata: ") + } + + // Validate ValidatorEntry is jailed. + if validatorEntry.Status() != ValidatorStatusJailed { + return errors.Wrapf(RuleErrorUnjailingNonjailedValidator, "UtxoView.IsValidUnjailValidatorMetadata: ") + } + + // Retrieve CurrentEpochNumber. + currentEpochNumber, err := bav.GetCurrentEpochNumber() + if err != nil { + return errors.Wrapf(err, "UtxoView.IsValidUnjailValidatorMetadata: error retrieving CurrentEpochNumber: ") + } + + // Validate sufficient epochs have elapsed for validator to be unjailed. + if validatorEntry.JailedAtEpochNumber+bav.Params.ValidatorJailEpochDuration > currentEpochNumber { + return errors.Wrapf(RuleErrorUnjailingValidatorTooEarly, "UtxoView.IsValidUnjailValidatorMetadata: ") + } + + return nil +} + func (bav *UtxoView) GetValidatorByPKID(pkid *PKID) (*ValidatorEntry, error) { // First check the UtxoView. @@ -1707,6 +2010,27 @@ func (bav *UtxoView) CreateUnregisterAsValidatorTxindexMetadata( return txindexMetadata, affectedPublicKeys } +func (bav *UtxoView) CreateUnjailValidatorTxindexMetadata( + utxoOp *UtxoOperation, + txn *MsgDeSoTxn, +) ( + *UnjailValidatorTxindexMetadata, + []*AffectedPublicKey, +) { + // Cast ValidatorPublicKey to ValidatorPublicKeyBase58Check. + validatorPublicKeyBase58Check := PkToString(txn.PublicKey, bav.Params) + + // Construct AffectedPublicKeys. + affectedPublicKeys := []*AffectedPublicKey{ + { + PublicKeyBase58Check: validatorPublicKeyBase58Check, + Metadata: "UnjailedValidatorPublicKeyBase58Check", + }, + } + + return &UnjailValidatorTxindexMetadata{}, affectedPublicKeys +} + // // CONSTANTS // @@ -1719,5 +2043,7 @@ const RuleErrorValidatorInvalidDomain RuleError = "RuleErrorValidatorInvalidDoma const RuleErrorValidatorDuplicateDomains RuleError = "RuleErrorValidatorDuplicateDomains" const RuleErrorValidatorNotFound RuleError = "RuleErrorValidatorNotFound" const RuleErrorValidatorDisablingExistingDelegatedStakers RuleError = "RuleErrorValidatorDisablingExistingDelegatedStakers" +const RuleErrorUnjailingNonjailedValidator RuleError = "RuleErrorUnjailingNonjailedValidator" +const RuleErrorUnjailingValidatorTooEarly RuleError = "RuleErrorUnjailingValidatorTooEarly" const MaxValidatorNumDomains int = 12 diff --git a/lib/block_view_validator_test.go b/lib/block_view_validator_test.go index 1ce1e2c9c..0848f404f 100644 --- a/lib/block_view_validator_test.go +++ b/lib/block_view_validator_test.go @@ -13,7 +13,6 @@ import ( func TestValidatorRegistration(t *testing.T) { _testValidatorRegistration(t, false) _testValidatorRegistration(t, true) - _testValidatorRegistrationWithDerivedKey(t) } func _testValidatorRegistration(t *testing.T, flushToDB bool) { @@ -93,9 +92,7 @@ func _testValidatorRegistration(t *testing.T, flushToDB bool) { Domains: [][]byte{[]byte("https://example.com")}, DisableDelegatedStake: false, } - _, _, _, err = _submitRegisterAsValidatorTxn( - testMeta, m0Pub, m0Priv, registerMetadata, nil, flushToDB, - ) + _, err = _submitRegisterAsValidatorTxn(testMeta, m0Pub, m0Priv, registerMetadata, nil, flushToDB) require.Error(t, err) require.Contains(t, err.Error(), RuleErrorProofofStakeTxnBeforeBlockHeight) @@ -109,9 +106,7 @@ func _testValidatorRegistration(t *testing.T, flushToDB bool) { Domains: [][]byte{}, DisableDelegatedStake: false, } - _, _, _, err = _submitRegisterAsValidatorTxn( - testMeta, m0Pub, m0Priv, registerMetadata, nil, flushToDB, - ) + _, err = _submitRegisterAsValidatorTxn(testMeta, m0Pub, m0Priv, registerMetadata, nil, flushToDB) require.Error(t, err) require.Contains(t, err.Error(), RuleErrorValidatorNoDomains) } @@ -125,9 +120,7 @@ func _testValidatorRegistration(t *testing.T, flushToDB bool) { Domains: domains, DisableDelegatedStake: false, } - _, _, _, err = _submitRegisterAsValidatorTxn( - testMeta, m0Pub, m0Priv, registerMetadata, nil, flushToDB, - ) + _, err = _submitRegisterAsValidatorTxn(testMeta, m0Pub, m0Priv, registerMetadata, nil, flushToDB) require.Error(t, err) require.Contains(t, err.Error(), RuleErrorValidatorTooManyDomains) } @@ -137,9 +130,7 @@ func _testValidatorRegistration(t *testing.T, flushToDB bool) { Domains: [][]byte{[]byte("InvalidURL")}, DisableDelegatedStake: false, } - _, _, _, err = _submitRegisterAsValidatorTxn( - testMeta, m0Pub, m0Priv, registerMetadata, nil, flushToDB, - ) + _, err = _submitRegisterAsValidatorTxn(testMeta, m0Pub, m0Priv, registerMetadata, nil, flushToDB) require.Error(t, err) require.Contains(t, err.Error(), RuleErrorValidatorInvalidDomain) } @@ -149,9 +140,7 @@ func _testValidatorRegistration(t *testing.T, flushToDB bool) { Domains: [][]byte{[]byte("https://example.com"), []byte("https://example.com")}, DisableDelegatedStake: false, } - _, _, _, err = _submitRegisterAsValidatorTxn( - testMeta, m0Pub, m0Priv, registerMetadata, nil, flushToDB, - ) + _, err = _submitRegisterAsValidatorTxn(testMeta, m0Pub, m0Priv, registerMetadata, nil, flushToDB) require.Error(t, err) require.Contains(t, err.Error(), RuleErrorValidatorDuplicateDomains) } @@ -162,9 +151,7 @@ func _testValidatorRegistration(t *testing.T, flushToDB bool) { DisableDelegatedStake: false, } extraData := map[string][]byte{"TestKey": []byte("TestValue1")} - _, _, _, err = _submitRegisterAsValidatorTxn( - testMeta, m0Pub, m0Priv, registerMetadata, extraData, flushToDB, - ) + _, err = _submitRegisterAsValidatorTxn(testMeta, m0Pub, m0Priv, registerMetadata, extraData, flushToDB) require.NoError(t, err) } { @@ -201,9 +188,7 @@ func _testValidatorRegistration(t *testing.T, flushToDB bool) { DisableDelegatedStake: false, } extraData := map[string][]byte{"TestKey": []byte("TestValue2")} - _, _, _, err = _submitRegisterAsValidatorTxn( - testMeta, m0Pub, m0Priv, registerMetadata, extraData, flushToDB, - ) + _, err = _submitRegisterAsValidatorTxn(testMeta, m0Pub, m0Priv, registerMetadata, extraData, flushToDB) require.NoError(t, err) } { @@ -219,18 +204,18 @@ func _testValidatorRegistration(t *testing.T, flushToDB bool) { } { // Sad path: unregister validator that doesn't exist - _, _, _, err = _submitUnregisterAsValidatorTxn(testMeta, m1Pub, m1Priv, flushToDB) + _, err = _submitUnregisterAsValidatorTxn(testMeta, m1Pub, m1Priv, flushToDB) require.Error(t, err) require.Contains(t, err.Error(), RuleErrorValidatorNotFound) } { // Happy path: unregister validator - _, _, _, err = _submitUnregisterAsValidatorTxn(testMeta, m0Pub, m0Priv, flushToDB) + _, err = _submitUnregisterAsValidatorTxn(testMeta, m0Pub, m0Priv, flushToDB) require.NoError(t, err) } { // Sad path: unregister validator that doesn't exist - _, _, _, err = _submitUnregisterAsValidatorTxn(testMeta, m0Pub, m0Priv, flushToDB) + _, err = _submitUnregisterAsValidatorTxn(testMeta, m0Pub, m0Priv, flushToDB) require.Error(t, err) require.Contains(t, err.Error(), RuleErrorValidatorNotFound) } @@ -265,7 +250,7 @@ func _submitRegisterAsValidatorTxn( metadata *RegisterAsValidatorMetadata, extraData map[string][]byte, flushToDB bool, -) (_utxoOps []*UtxoOperation, _txn *MsgDeSoTxn, _height uint32, _err error) { +) (_fees uint64, _err error) { // Record transactor's prevBalance. prevBalance := _getBalance(testMeta.t, testMeta.chain, testMeta.mempool, transactorPublicKeyBase58Check) @@ -283,7 +268,7 @@ func _submitRegisterAsValidatorTxn( []*DeSoOutput{}, ) if err != nil { - return nil, nil, 0, err + return 0, err } require.Equal(testMeta.t, totalInputMake, changeAmountMake+feesMake) @@ -300,7 +285,7 @@ func _submitRegisterAsValidatorTxn( false, ) if err != nil { - return nil, nil, 0, err + return 0, err } require.Equal(testMeta.t, totalInput, totalOutput+fees) require.Equal(testMeta.t, totalInput, totalInputMake) @@ -314,7 +299,7 @@ func _submitRegisterAsValidatorTxn( testMeta.expectedSenderBalances = append(testMeta.expectedSenderBalances, prevBalance) testMeta.txnOps = append(testMeta.txnOps, utxoOps) testMeta.txns = append(testMeta.txns, txn) - return utxoOps, txn, testMeta.savedHeight, nil + return fees, nil } func _submitUnregisterAsValidatorTxn( @@ -322,7 +307,7 @@ func _submitUnregisterAsValidatorTxn( transactorPublicKeyBase58Check string, transactorPrivateKeyBase58Check string, flushToDB bool, -) (_utxoOps []*UtxoOperation, _txn *MsgDeSoTxn, _height uint32, _err error) { +) (_fees uint64, _err error) { // Record transactor's prevBalance. prevBalance := _getBalance(testMeta.t, testMeta.chain, testMeta.mempool, transactorPublicKeyBase58Check) @@ -340,7 +325,7 @@ func _submitUnregisterAsValidatorTxn( []*DeSoOutput{}, ) if err != nil { - return nil, nil, 0, err + return 0, err } require.Equal(testMeta.t, totalInputMake, changeAmountMake+feesMake) @@ -357,7 +342,7 @@ func _submitUnregisterAsValidatorTxn( false, ) if err != nil { - return nil, nil, 0, err + return 0, err } require.Equal(testMeta.t, totalInput, totalOutput+fees) require.Equal(testMeta.t, totalInput, totalInputMake) @@ -371,10 +356,10 @@ func _submitUnregisterAsValidatorTxn( testMeta.expectedSenderBalances = append(testMeta.expectedSenderBalances, prevBalance) testMeta.txnOps = append(testMeta.txnOps, utxoOps) testMeta.txns = append(testMeta.txns, txn) - return utxoOps, txn, testMeta.savedHeight, nil + return fees, nil } -func _testValidatorRegistrationWithDerivedKey(t *testing.T) { +func TestValidatorRegistrationWithDerivedKey(t *testing.T) { var err error // Initialize balance model fork heights. @@ -719,9 +704,7 @@ func _testGetTopActiveValidatorsByStake(t *testing.T, flushToDB bool) { registerMetadata := &RegisterAsValidatorMetadata{ Domains: [][]byte{[]byte("https://m0.com")}, } - _, _, _, err = _submitRegisterAsValidatorTxn( - testMeta, m0Pub, m0Priv, registerMetadata, nil, flushToDB, - ) + _, err = _submitRegisterAsValidatorTxn(testMeta, m0Pub, m0Priv, registerMetadata, nil, flushToDB) require.NoError(t, err) // Verify top validators. @@ -736,9 +719,7 @@ func _testGetTopActiveValidatorsByStake(t *testing.T, flushToDB bool) { registerMetadata := &RegisterAsValidatorMetadata{ Domains: [][]byte{[]byte("https://m1.com")}, } - _, _, _, err = _submitRegisterAsValidatorTxn( - testMeta, m1Pub, m1Priv, registerMetadata, nil, flushToDB, - ) + _, err = _submitRegisterAsValidatorTxn(testMeta, m1Pub, m1Priv, registerMetadata, nil, flushToDB) require.NoError(t, err) // Verify top validators. @@ -751,9 +732,7 @@ func _testGetTopActiveValidatorsByStake(t *testing.T, flushToDB bool) { registerMetadata := &RegisterAsValidatorMetadata{ Domains: [][]byte{[]byte("https://m2.com")}, } - _, _, _, err = _submitRegisterAsValidatorTxn( - testMeta, m2Pub, m2Priv, registerMetadata, nil, flushToDB, - ) + _, err = _submitRegisterAsValidatorTxn(testMeta, m2Pub, m2Priv, registerMetadata, nil, flushToDB) require.NoError(t, err) // Verify top validators. @@ -837,7 +816,7 @@ func _testGetTopActiveValidatorsByStake(t *testing.T, flushToDB bool) { } { // m2 unregisters as validator. - _, _, _, err = _submitUnregisterAsValidatorTxn(testMeta, m2Pub, m2Priv, flushToDB) + _, err = _submitUnregisterAsValidatorTxn(testMeta, m2Pub, m2Priv, flushToDB) require.NoError(t, err) // Verify top validators. @@ -1143,9 +1122,7 @@ func _testUpdatingValidatorDisableDelegatedStake(t *testing.T, flushToDB bool) { Domains: [][]byte{[]byte("https://m0.com")}, DisableDelegatedStake: false, } - _, _, _, err = _submitRegisterAsValidatorTxn( - testMeta, m0Pub, m0Priv, registerMetadata, nil, flushToDB, - ) + _, err = _submitRegisterAsValidatorTxn(testMeta, m0Pub, m0Priv, registerMetadata, nil, flushToDB) require.NoError(t, err) validatorEntry, err = utxoView().GetValidatorByPKID(m0PKID) @@ -1163,9 +1140,7 @@ func _testUpdatingValidatorDisableDelegatedStake(t *testing.T, flushToDB bool) { Domains: [][]byte{[]byte("https://m0.com")}, DisableDelegatedStake: true, } - _, _, _, err = _submitRegisterAsValidatorTxn( - testMeta, m0Pub, m0Priv, registerMetadata, nil, flushToDB, - ) + _, err = _submitRegisterAsValidatorTxn(testMeta, m0Pub, m0Priv, registerMetadata, nil, flushToDB) require.NoError(t, err) validatorEntry, err = utxoView().GetValidatorByPKID(m0PKID) @@ -1207,9 +1182,7 @@ func _testUpdatingValidatorDisableDelegatedStake(t *testing.T, flushToDB bool) { Domains: [][]byte{[]byte("https://m0.com")}, DisableDelegatedStake: false, } - _, _, _, err = _submitRegisterAsValidatorTxn( - testMeta, m0Pub, m0Priv, registerMetadata, nil, flushToDB, - ) + _, err = _submitRegisterAsValidatorTxn(testMeta, m0Pub, m0Priv, registerMetadata, nil, flushToDB) require.NoError(t, err) validatorEntry, err = utxoView().GetValidatorByPKID(m0PKID) @@ -1238,9 +1211,7 @@ func _testUpdatingValidatorDisableDelegatedStake(t *testing.T, flushToDB bool) { Domains: [][]byte{[]byte("https://m0.com")}, DisableDelegatedStake: true, } - _, _, _, err = _submitRegisterAsValidatorTxn( - testMeta, m0Pub, m0Priv, registerMetadata, nil, flushToDB, - ) + _, err = _submitRegisterAsValidatorTxn(testMeta, m0Pub, m0Priv, registerMetadata, nil, flushToDB) require.Error(t, err) require.Contains(t, err.Error(), RuleErrorValidatorDisablingExistingDelegatedStakers) } @@ -1336,9 +1307,7 @@ func _testUnregisterAsValidator(t *testing.T, flushToDB bool) { registerMetadata := &RegisterAsValidatorMetadata{ Domains: [][]byte{[]byte("https://m0.com")}, } - _, _, _, err = _submitRegisterAsValidatorTxn( - testMeta, m0Pub, m0Priv, registerMetadata, nil, flushToDB, - ) + _, err = _submitRegisterAsValidatorTxn(testMeta, m0Pub, m0Priv, registerMetadata, nil, flushToDB) require.NoError(t, err) validatorEntry, err = utxoView().GetValidatorByPKID(m0PKID) @@ -1414,7 +1383,7 @@ func _testUnregisterAsValidator(t *testing.T, flushToDB bool) { } { // m0 unregisters as a validator. - _, _, _, err = _submitUnregisterAsValidatorTxn(testMeta, m0Pub, m0Priv, flushToDB) + _, err = _submitUnregisterAsValidatorTxn(testMeta, m0Pub, m0Priv, flushToDB) require.NoError(t, err) // m0's ValidatorEntry is deleted. @@ -1454,3 +1423,530 @@ func _testUnregisterAsValidator(t *testing.T, flushToDB bool) { require.NoError(t, mempool.universalUtxoView.FlushToDb(blockHeight)) _executeAllTestRollbackAndFlush(testMeta) } + +func TestUnjailValidator(t *testing.T) { + _testUnjailValidator(t, false) + _testUnjailValidator(t, true) +} + +func _testUnjailValidator(t *testing.T, flushToDB bool) { + var validatorEntry *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) + chain.snapshot = nil + + // For these tests, we set ValidatorJailEpochDuration to 3. + params.ValidatorJailEpochDuration = 3 + + 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, "", senderPkString, paramUpdaterPub, senderPrivString, 1e3) + + m0PKID := DBGetPKIDEntryForPublicKey(db, chain.snapshot, m0PkBytes).PKID + m1PKID := DBGetPKIDEntryForPublicKey(db, chain.snapshot, m1PkBytes).PKID + + // Seed a CurrentEpochEntry. + epochUtxoView, err := NewUtxoView(db, params, chain.postgres, chain.snapshot) + require.NoError(t, err) + epochUtxoView._setCurrentEpochEntry(&EpochEntry{EpochNumber: 1, FinalBlockHeight: blockHeight + 10}) + require.NoError(t, epochUtxoView.FlushToDb(blockHeight)) + currentEpochNumber, err := utxoView().GetCurrentEpochNumber() + require.NoError(t, err) + + { + // 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://example.com")}, + } + extraData := map[string][]byte{"TestKey": []byte("TestValue1")} + _, err = _submitRegisterAsValidatorTxn(testMeta, m0Pub, m0Priv, registerMetadata, extraData, flushToDB) + require.NoError(t, err) + + validatorEntry, err = utxoView().GetValidatorByPKID(m0PKID) + require.NoError(t, err) + require.NotNil(t, validatorEntry) + require.Equal(t, validatorEntry.ExtraData["TestKey"], []byte("TestValue1")) + } + { + // RuleErrorUnjailingNonjailedValidator + _, err = _submitUnjailValidatorTxn(testMeta, m0Pub, m0Priv, nil, flushToDB) + require.Error(t, err) + require.Contains(t, err.Error(), RuleErrorUnjailingNonjailedValidator) + } + { + // m0 is jailed. Since this update takes place outside a transaction, + // we cannot test rollbacks. We will run into an error where m0 is + // trying to unjail himself, but he was never jailed. + + // Delete m0's ValidatorEntry from the UtxoView. + delete(mempool.universalUtxoView.ValidatorMapKeyToValidatorEntry, validatorEntry.ToMapKey()) + delete(mempool.readOnlyUtxoView.ValidatorMapKeyToValidatorEntry, validatorEntry.ToMapKey()) + + // Set JailedAtEpochNumber. + validatorEntry.JailedAtEpochNumber = currentEpochNumber + + // Store m0's ValidatorEntry in the db. + tmpUtxoView, err := NewUtxoView(db, params, chain.postgres, chain.snapshot) + require.NoError(t, err) + tmpUtxoView._setValidatorEntryMappings(validatorEntry) + require.NoError(t, tmpUtxoView.FlushToDb(blockHeight)) + + // Verify m0 is jailed. + validatorEntry, err = utxoView().GetValidatorByPKID(m0PKID) + require.NoError(t, err) + require.NotNil(t, validatorEntry) + require.Equal(t, validatorEntry.Status(), ValidatorStatusJailed) + } + { + // m1 stakes with m0. Succeeds. You can stake to a jailed validator. + stakeMetadata := &StakeMetadata{ + ValidatorPublicKey: NewPublicKey(m0PkBytes), + StakeAmountNanos: uint256.NewInt().SetUint64(100), + } + _, err = _submitStakeTxn(testMeta, m1Pub, m1Priv, stakeMetadata, nil, flushToDB) + require.NoError(t, err) + + stakeEntry, err := utxoView().GetStakeEntry(m0PKID, m1PKID) + require.NoError(t, err) + require.NotNil(t, stakeEntry) + } + { + // m1 unstakes from m0. Succeeds. You can unstake from a jailed validator. + unstakeMetadata := &UnstakeMetadata{ + ValidatorPublicKey: NewPublicKey(m0PkBytes), + UnstakeAmountNanos: uint256.NewInt().SetUint64(100), + } + _, err = _submitUnstakeTxn(testMeta, m1Pub, m1Priv, unstakeMetadata, nil, flushToDB) + require.NoError(t, err) + + stakeEntry, err := utxoView().GetStakeEntry(m0PKID, m1PKID) + require.NoError(t, err) + require.Nil(t, stakeEntry) + + lockedStakeEntry, err := utxoView().GetLockedStakeEntry(m0PKID, m1PKID, currentEpochNumber) + require.NoError(t, err) + require.NotNil(t, lockedStakeEntry) + } + { + // RuleErrorValidatorNotFound + _, err = _submitUnjailValidatorTxn(testMeta, m1Pub, m1Priv, nil, flushToDB) + require.Error(t, err) + require.Contains(t, err.Error(), RuleErrorValidatorNotFound) + } + { + // RuleErrorUnjailingValidatorTooEarly + _, err = _submitUnjailValidatorTxn(testMeta, m0Pub, m0Priv, nil, flushToDB) + require.Error(t, err) + require.Contains(t, err.Error(), RuleErrorUnjailingValidatorTooEarly) + } + { + // Simulate three epochs passing by seeding a new CurrentEpochEntry. + + // Delete the CurrentEpochEntry from the UtxoView. + mempool.universalUtxoView.CurrentEpochEntry = nil + mempool.readOnlyUtxoView.CurrentEpochEntry = nil + + // Store a new CurrentEpochEntry in the db. + epochUtxoView, err = NewUtxoView(db, params, chain.postgres, chain.snapshot) + require.NoError(t, err) + epochUtxoView._setCurrentEpochEntry( + &EpochEntry{EpochNumber: currentEpochNumber + 3, FinalBlockHeight: blockHeight + 10}, + ) + require.NoError(t, epochUtxoView.FlushToDb(blockHeight)) + + // Verify CurrentEpochNumber. + currentEpochNumber, err = utxoView().GetCurrentEpochNumber() + require.NoError(t, err) + require.Equal(t, currentEpochNumber, uint64(4)) + } + { + // RuleErrorProofofStakeTxnBeforeBlockHeight + params.ForkHeights.ProofOfStakeNewTxnTypesBlockHeight = math.MaxUint32 + GlobalDeSoParams.EncoderMigrationHeights = GetEncoderMigrationHeights(¶ms.ForkHeights) + GlobalDeSoParams.EncoderMigrationHeightsList = GetEncoderMigrationHeightsList(¶ms.ForkHeights) + + _, err = _submitUnjailValidatorTxn(testMeta, m0Pub, m0Priv, 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) + } + { + // m0 unjails himself. + validatorEntry, err = utxoView().GetValidatorByPKID(m0PKID) + require.NoError(t, err) + require.NotNil(t, validatorEntry) + require.Equal(t, validatorEntry.Status(), ValidatorStatusJailed) + require.Equal(t, validatorEntry.LastActiveAtEpochNumber, uint64(1)) + + extraData := map[string][]byte{"TestKey": []byte("TestValue2")} + _, err = _submitUnjailValidatorTxn(testMeta, m0Pub, m0Priv, extraData, flushToDB) + require.NoError(t, err) + + validatorEntry, err = utxoView().GetValidatorByPKID(m0PKID) + require.NoError(t, err) + require.NotNil(t, validatorEntry) + require.Equal(t, validatorEntry.Status(), ValidatorStatusActive) + require.Equal(t, validatorEntry.LastActiveAtEpochNumber, uint64(4)) + require.Equal(t, validatorEntry.ExtraData["TestKey"], []byte("TestValue2")) + } +} + +func TestUnjailValidatorWithDerivedKey(t *testing.T) { + var validatorEntry *ValidatorEntry + 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.ProofOfStakeNewTxnTypesBlockHeight = uint32(1) + GlobalDeSoParams.EncoderMigrationHeights = GetEncoderMigrationHeights(¶ms.ForkHeights) + GlobalDeSoParams.EncoderMigrationHeightsList = GetEncoderMigrationHeightsList(¶ms.ForkHeights) + + // 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, "", senderPkString, paramUpdaterPub, senderPrivString, 1e3) + + 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 + } + + _submitAuthorizeDerivedKeyUnjailValidatorTxn := func(count uint64) (string, error) { + utxoView := newUtxoView() + + txnSpendingLimit := &TransactionSpendingLimit{ + GlobalDESOLimit: NanosPerUnit, // 1 $DESO spending limit + TransactionCountLimitMap: map[TxnType]uint64{ + TxnTypeAuthorizeDerivedKey: 1, + TxnTypeUnjailValidator: count, + }, + } + + 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 + } + + _submitUnjailValidatorTxnWithDerivedKey := func(transactorPkBytes []byte, derivedKeyPrivBase58Check string) error { + utxoView := newUtxoView() + // Construct txn. + txn, _, _, _, err := testMeta.chain.CreateUnjailValidatorTxn( + transactorPkBytes, + &UnjailValidatorMetadata{}, + make(map[string][]byte), + testMeta.feeRateNanosPerKb, + mempool, + []*DeSoOutput{}, + ) + if err != nil { + return 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, _, _, _, err := utxoView.ConnectTransaction( + txn, + txn.Hash(), + getTxnSize(*txn), + testMeta.savedHeight, + true, + false, + ) + if err != nil { + return 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 nil + } + + // Seed a CurrentEpochEntry. + epochUtxoView := newUtxoView() + epochUtxoView._setCurrentEpochEntry(&EpochEntry{EpochNumber: 1, FinalBlockHeight: blockHeight + 10}) + require.NoError(t, epochUtxoView.FlushToDb(blockHeight)) + currentEpochNumber, err := newUtxoView().GetCurrentEpochNumber() + require.NoError(t, err) + + { + // ParamUpdater set min fee rate + params.ExtraRegtestParamUpdaterKeys[MakePkMapKey(paramUpdaterPkBytes)] = true + _updateGlobalParamsEntryWithTestMeta( + testMeta, + testMeta.feeRateNanosPerKb, + paramUpdaterPub, + paramUpdaterPriv, + -1, + int64(testMeta.feeRateNanosPerKb), + -1, + -1, + -1, + ) + } + { + // sender registers as a validator. + registerMetadata := &RegisterAsValidatorMetadata{ + Domains: [][]byte{[]byte("https://example.com")}, + } + _, err = _submitRegisterAsValidatorTxn(testMeta, senderPkString, senderPrivString, registerMetadata, nil, true) + require.NoError(t, err) + + validatorEntry, err = newUtxoView().GetValidatorByPKID(senderPKID) + require.NoError(t, err) + require.NotNil(t, validatorEntry) + } + { + // sender is jailed. Since this update takes place outside a transaction, + // we cannot test rollbacks. We will run into an error where sender is + // trying to unjail himself, but he was never jailed. + + // Delete sender's ValidatorEntry from the UtxoView. + delete(mempool.universalUtxoView.ValidatorMapKeyToValidatorEntry, validatorEntry.ToMapKey()) + delete(mempool.readOnlyUtxoView.ValidatorMapKeyToValidatorEntry, validatorEntry.ToMapKey()) + + // Set JailedAtEpochNumber. + validatorEntry.JailedAtEpochNumber = currentEpochNumber + + // Store sender's ValidatorEntry in the db. + tmpUtxoView, err := NewUtxoView(db, params, chain.postgres, chain.snapshot) + require.NoError(t, err) + tmpUtxoView._setValidatorEntryMappings(validatorEntry) + require.NoError(t, tmpUtxoView.FlushToDb(blockHeight)) + + // Verify sender is jailed. + validatorEntry, err = newUtxoView().GetValidatorByPKID(senderPKID) + require.NoError(t, err) + require.NotNil(t, validatorEntry) + require.Equal(t, validatorEntry.Status(), ValidatorStatusJailed) + } + { + // sender creates a DerivedKey that can perform one UnjailValidator txn. + derivedKeyPriv, err = _submitAuthorizeDerivedKeyUnjailValidatorTxn(1) + require.NoError(t, err) + } + { + // RuleErrorUnjailingValidatorTooEarly + err = _submitUnjailValidatorTxnWithDerivedKey(senderPkBytes, derivedKeyPriv) + require.Error(t, err) + require.Contains(t, err.Error(), RuleErrorUnjailingValidatorTooEarly) + } + { + // Simulate three epochs passing by seeding a new CurrentEpochEntry. + + // Delete the CurrentEpochEntry from the UtxoView. + mempool.universalUtxoView.CurrentEpochEntry = nil + mempool.readOnlyUtxoView.CurrentEpochEntry = nil + + // Store a new CurrentEpochEntry in the db. + epochUtxoView, err = NewUtxoView(db, params, chain.postgres, chain.snapshot) + require.NoError(t, err) + epochUtxoView._setCurrentEpochEntry( + &EpochEntry{EpochNumber: currentEpochNumber + 3, FinalBlockHeight: blockHeight + 10}, + ) + require.NoError(t, epochUtxoView.FlushToDb(blockHeight)) + + // Verify CurrentEpochNumber. + currentEpochNumber, err = newUtxoView().GetCurrentEpochNumber() + require.NoError(t, err) + require.Equal(t, currentEpochNumber, uint64(4)) + } + { + // sender unjails himself using a DerivedKey. + validatorEntry, err = newUtxoView().GetValidatorByPKID(senderPKID) + require.NoError(t, err) + require.NotNil(t, validatorEntry) + require.Equal(t, validatorEntry.Status(), ValidatorStatusJailed) + require.Equal(t, validatorEntry.LastActiveAtEpochNumber, uint64(1)) + + err = _submitUnjailValidatorTxnWithDerivedKey(senderPkBytes, derivedKeyPriv) + require.NoError(t, err) + + validatorEntry, err = newUtxoView().GetValidatorByPKID(senderPKID) + require.NoError(t, err) + require.NotNil(t, validatorEntry) + require.Equal(t, validatorEntry.Status(), ValidatorStatusActive) + require.Equal(t, validatorEntry.LastActiveAtEpochNumber, uint64(4)) + } +} + +func _submitUnjailValidatorTxn( + testMeta *TestMeta, + transactorPublicKeyBase58Check string, + transactorPrivateKeyBase58Check string, + 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.CreateUnjailValidatorTxn( + updaterPkBytes, + &UnjailValidatorMetadata{}, + 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, OperationTypeUnjailValidator, 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 +} diff --git a/lib/constants.go b/lib/constants.go index e8fd7360d..48e745653 100644 --- a/lib/constants.go +++ b/lib/constants.go @@ -601,6 +601,11 @@ type DeSoParams struct { // TODO: Move this to GlobalParamsEntry. StakeLockupEpochDuration uint64 + // ValidatorJailEpochDuration is the number of epochs that a validator must + // wait after being jailed before submitting an UnjailValidator txn. + // TODO: Move this to GlobalParamsEntry. + ValidatorJailEpochDuration uint64 + ForkHeights ForkHeights EncoderMigrationHeights *EncoderMigrationHeights @@ -976,6 +981,9 @@ var DeSoMainnetParams = DeSoParams{ // Unstaked stake can be unlocked after a minimum of N elapsed epochs. StakeLockupEpochDuration: uint64(3), + // Jailed validators can be unjailed after a minimum of N elapsed epochs. + ValidatorJailEpochDuration: uint64(3), + ForkHeights: MainnetForkHeights, EncoderMigrationHeights: GetEncoderMigrationHeights(&MainnetForkHeights), EncoderMigrationHeightsList: GetEncoderMigrationHeightsList(&MainnetForkHeights), @@ -1207,6 +1215,9 @@ var DeSoTestnetParams = DeSoParams{ // Unstaked stake can be unlocked after a minimum of N elapsed epochs. StakeLockupEpochDuration: uint64(3), + // Jailed validators can be unjailed after a minimum of N elapsed epochs. + ValidatorJailEpochDuration: uint64(3), + ForkHeights: TestnetForkHeights, EncoderMigrationHeights: GetEncoderMigrationHeights(&TestnetForkHeights), EncoderMigrationHeightsList: GetEncoderMigrationHeightsList(&TestnetForkHeights), diff --git a/lib/db_utils.go b/lib/db_utils.go index f0f21f79e..cb3892d59 100644 --- a/lib/db_utils.go +++ b/lib/db_utils.go @@ -6804,6 +6804,7 @@ type TransactionMetadata struct { StakeTxindexMetadata *StakeTxindexMetadata `json:",omitempty"` UnstakeTxindexMetadata *UnstakeTxindexMetadata `json:",omitempty"` UnlockStakeTxindexMetadata *UnlockStakeTxindexMetadata `json:",omitempty"` + UnjailValidatorTxindexMetadata *UnjailValidatorTxindexMetadata `json:",omitempty"` } func (txnMeta *TransactionMetadata) RawEncodeWithoutMetadata(blockHeight uint64, skipMetadata ...bool) []byte { @@ -6896,6 +6897,8 @@ func (txnMeta *TransactionMetadata) RawEncodeWithoutMetadata(blockHeight uint64, data = append(data, EncodeToBytes(blockHeight, txnMeta.UnstakeTxindexMetadata, skipMetadata...)...) // encoding UnlockStakeTxindexMetadata data = append(data, EncodeToBytes(blockHeight, txnMeta.UnlockStakeTxindexMetadata, skipMetadata...)...) + // encoding UnjailValidatorTxindexMetadata + data = append(data, EncodeToBytes(blockHeight, txnMeta.UnjailValidatorTxindexMetadata, skipMetadata...)...) } return data @@ -7150,23 +7153,27 @@ func (txnMeta *TransactionMetadata) RawDecodeWithoutMetadata(blockHeight uint64, if MigrationTriggered(blockHeight, ProofOfStakeNewTxnTypesMigration) { // decoding RegisterAsValidatorTxindexMetadata if txnMeta.RegisterAsValidatorTxindexMetadata, err = DecodeDeSoEncoder(&RegisterAsValidatorTxindexMetadata{}, rr); err != nil { - return errors.Wrapf(err, "TransactionMetadata.Decode: Problem reading RegisterAsValidatorTxindexMetadata") + return errors.Wrapf(err, "TransactionMetadata.Decode: Problem reading RegisterAsValidatorTxindexMetadata: ") } // decoding UnregisterAsValidatorTxindexMetadata if txnMeta.UnregisterAsValidatorTxindexMetadata, err = DecodeDeSoEncoder(&UnregisterAsValidatorTxindexMetadata{}, rr); err != nil { - return errors.Wrapf(err, "TransactionMetadata.Decode: Problem reading UnregisterAsValidatorTxindexMetadata") + 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") + 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") + 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 errors.Wrapf(err, "TransactionMetadata.Decode: Problem reading UnlockStakeTxindexMetadata: ") + } + // decoding UnjailValidatorTxindexMetadata + if txnMeta.UnjailValidatorTxindexMetadata, err = DecodeDeSoEncoder(&UnjailValidatorTxindexMetadata{}, rr); err != nil { + return errors.Wrapf(err, "TransactionMetadata.Decode: Problem reading UnjailValidatorTxindexMetadata: ") } } diff --git a/lib/mempool.go b/lib/mempool.go index c5d3626d4..6e7373986 100644 --- a/lib/mempool.go +++ b/lib/mempool.go @@ -1961,6 +1961,10 @@ func ComputeTransactionMetadata(txn *MsgDeSoTxn, utxoView *UtxoView, blockHash * txindexMetadata, affectedPublicKeys := utxoView.CreateUnlockStakeTxindexMetadata(utxoOps[len(utxoOps)-1], txn) txnMeta.UnlockStakeTxindexMetadata = txindexMetadata txnMeta.AffectedPublicKeys = append(txnMeta.AffectedPublicKeys, affectedPublicKeys...) + case TxnTypeUnjailValidator: + txindexMetadata, affectedPublicKeys := utxoView.CreateUnjailValidatorTxindexMetadata(utxoOps[len(utxoOps)-1], txn) + txnMeta.UnjailValidatorTxindexMetadata = txindexMetadata + txnMeta.AffectedPublicKeys = append(txnMeta.AffectedPublicKeys, affectedPublicKeys...) } return txnMeta } diff --git a/lib/network.go b/lib/network.go index ddaa1e342..c096f701f 100644 --- a/lib/network.go +++ b/lib/network.go @@ -244,8 +244,9 @@ const ( TxnTypeStake TxnType = 36 TxnTypeUnstake TxnType = 37 TxnTypeUnlockStake TxnType = 38 + TxnTypeUnjailValidator TxnType = 39 - // NEXT_ID = 39 + // NEXT_ID = 40 ) type TxnString string @@ -290,6 +291,7 @@ const ( TxnStringStake TxnString = "STAKE" TxnStringUnstake TxnString = "UNSTAKE" TxnStringUnlockStake TxnString = "UNLOCK_STAKE" + TxnStringUnjailValidator TxnString = "UNJAIL_VALIDATOR" ) var ( @@ -302,7 +304,7 @@ var ( TxnTypeDAOCoin, TxnTypeDAOCoinTransfer, TxnTypeDAOCoinLimitOrder, TxnTypeCreateUserAssociation, TxnTypeDeleteUserAssociation, TxnTypeCreatePostAssociation, TxnTypeDeletePostAssociation, TxnTypeAccessGroup, TxnTypeAccessGroupMembers, TxnTypeNewMessage, TxnTypeRegisterAsValidator, - TxnTypeUnregisterAsValidator, TxnTypeStake, TxnTypeUnstake, TxnTypeUnlockStake, + TxnTypeUnregisterAsValidator, TxnTypeStake, TxnTypeUnstake, TxnTypeUnlockStake, TxnTypeUnjailValidator, } AllTxnString = []TxnString{ TxnStringUnset, TxnStringBlockReward, TxnStringBasicTransfer, TxnStringBitcoinExchange, TxnStringPrivateMessage, @@ -313,7 +315,7 @@ var ( TxnStringDAOCoin, TxnStringDAOCoinTransfer, TxnStringDAOCoinLimitOrder, TxnStringCreateUserAssociation, TxnStringDeleteUserAssociation, TxnStringCreatePostAssociation, TxnStringDeletePostAssociation, TxnStringAccessGroup, TxnStringAccessGroupMembers, TxnStringNewMessage, TxnStringRegisterAsValidator, - TxnStringUnregisterAsValidator, TxnStringStake, TxnStringUnstake, TxnStringUnlockStake, + TxnStringUnregisterAsValidator, TxnStringStake, TxnStringUnstake, TxnStringUnlockStake, TxnStringUnjailValidator, } ) @@ -403,6 +405,8 @@ func (txnType TxnType) GetTxnString() TxnString { return TxnStringUnstake case TxnTypeUnlockStake: return TxnStringUnlockStake + case TxnTypeUnjailValidator: + return TxnStringUnjailValidator default: return TxnStringUndefined } @@ -486,6 +490,8 @@ func GetTxnTypeFromString(txnString TxnString) TxnType { return TxnTypeUnstake case TxnStringUnlockStake: return TxnTypeUnlockStake + case TxnStringUnjailValidator: + return TxnTypeUnjailValidator default: // TxnTypeUnset means we couldn't find a matching txn type return TxnTypeUnset @@ -577,6 +583,8 @@ func NewTxnMetadata(txType TxnType) (DeSoTxnMetadata, error) { return (&UnstakeMetadata{}).New(), nil case TxnTypeUnlockStake: return (&UnlockStakeMetadata{}).New(), nil + case TxnTypeUnjailValidator: + return (&UnjailValidatorMetadata{}).New(), nil default: return nil, fmt.Errorf("NewTxnMetadata: Unrecognized TxnType: %v; make sure you add the new type of transaction to NewTxnMetadata", txType) }