diff --git a/routes/admin_transaction.go b/routes/admin_transaction.go index eea78f06..3643e759 100644 --- a/routes/admin_transaction.go +++ b/routes/admin_transaction.go @@ -31,6 +31,26 @@ type GetGlobalParamsResponse struct { // The maximum number of copies a single NFT can have. MaxCopiesPerNFT uint64 `safeForLogging:"true"` + + // StakeLockupEpochDuration is the number of epochs that a + // user must wait before unlocking their unstaked stake. + StakeLockupEpochDuration uint64 `safeForLogging:"true"` + + // ValidatorJailEpochDuration is the number of epochs that a validator must + // wait after being jailed before submitting an UnjailValidator txn. + ValidatorJailEpochDuration uint64 `safeForLogging:"true"` + + // LeaderScheduleMaxNumValidators is the maximum number of validators that + // are included when generating a new Proof-of-Stake leader schedule. + LeaderScheduleMaxNumValidators uint64 `safeForLogging:"true"` + + // EpochDurationNumBlocks is the number of blocks included in one epoch. + EpochDurationNumBlocks uint64 `safeForLogging:"true"` + + // JailInactiveValidatorGracePeriodEpochs is the number of epochs we + // allow a validator to be inactive for (neither voting nor proposing + // blocks) before they are jailed. + JailInactiveValidatorGracePeriodEpochs uint64 `safeForLogging:"true"` } func (fes *APIServer) GetGlobalParams(ww http.ResponseWriter, req *http.Request) { @@ -50,11 +70,16 @@ func (fes *APIServer) GetGlobalParams(ww http.ResponseWriter, req *http.Request) globalParamsEntry := utxoView.GlobalParamsEntry // Return all the data associated with the transaction in the response res := GetGlobalParamsResponse{ - USDCentsPerBitcoin: globalParamsEntry.USDCentsPerBitcoin, - CreateProfileFeeNanos: globalParamsEntry.CreateProfileFeeNanos, - MinimumNetworkFeeNanosPerKB: globalParamsEntry.MinimumNetworkFeeNanosPerKB, - CreateNFTFeeNanos: globalParamsEntry.CreateNFTFeeNanos, - MaxCopiesPerNFT: globalParamsEntry.MaxCopiesPerNFT, + USDCentsPerBitcoin: globalParamsEntry.USDCentsPerBitcoin, + CreateProfileFeeNanos: globalParamsEntry.CreateProfileFeeNanos, + MinimumNetworkFeeNanosPerKB: globalParamsEntry.MinimumNetworkFeeNanosPerKB, + CreateNFTFeeNanos: globalParamsEntry.CreateNFTFeeNanos, + MaxCopiesPerNFT: globalParamsEntry.MaxCopiesPerNFT, + StakeLockupEpochDuration: utxoView.GetCurrentGlobalParam(lib.StakeLockupEpochDuration), + ValidatorJailEpochDuration: utxoView.GetCurrentGlobalParam(lib.ValidatorJailEpochDuration), + LeaderScheduleMaxNumValidators: utxoView.GetCurrentGlobalParam(lib.LeaderScheduleMaxNumValidators), + EpochDurationNumBlocks: utxoView.GetCurrentGlobalParam(lib.EpochDurationNumBlocks), + JailInactiveValidatorGracePeriodEpochs: utxoView.GetCurrentGlobalParam(lib.JailInactiveValidatorGracePeriodEpochs), } if err := json.NewEncoder(ww).Encode(res); err != nil { _AddBadRequestError(ww, fmt.Sprintf("GetGlobalParams: Problem encoding response as JSON: %v", err)) @@ -84,6 +109,26 @@ type UpdateGlobalParamsRequest struct { // heights on nonces. MaxNonceExpirationBlockHeightOffset int64 `safeForLogging:"true"` + // StakeLockupEpochDuration is the number of epochs that a + // user must wait before unlocking their unstaked stake. + StakeLockupEpochDuration uint64 `safeForLogging:"true"` + + // ValidatorJailEpochDuration is the number of epochs that a validator must + // wait after being jailed before submitting an UnjailValidator txn. + ValidatorJailEpochDuration uint64 `safeForLogging:"true"` + + // LeaderScheduleMaxNumValidators is the maximum number of validators that + // are included when generating a new Proof-of-Stake leader schedule. + LeaderScheduleMaxNumValidators uint64 `safeForLogging:"true"` + + // EpochDurationNumBlocks is the number of blocks included in one epoch. + EpochDurationNumBlocks uint64 `safeForLogging:"true"` + + // JailInactiveValidatorGracePeriodEpochs is the number of epochs we + // allow a validator to be inactive for (neither voting nor proposing + // blocks) before they are jailed. + JailInactiveValidatorGracePeriodEpochs uint64 `safeForLogging:"true"` + MinFeeRateNanosPerKB uint64 `safeForLogging:"true"` // No need to specify ProfileEntryResponse in each TransactionFee @@ -172,6 +217,34 @@ func (fes *APIServer) UpdateGlobalParams(ww http.ResponseWriter, req *http.Reque maxNonceExpirationBlockHeightOffset = requestData.MaxNonceExpirationBlockHeightOffset } + extraData := make(map[string][]byte) + + // Update Proof of Stake consensus related global params if they have changed. + if requestData.StakeLockupEpochDuration > 0 && + requestData.StakeLockupEpochDuration != utxoView.GetCurrentGlobalParam(lib.StakeLockupEpochDuration) { + extraData[lib.StakeLockupEpochDuration.ToString()] = lib.UintToBuf(requestData.StakeLockupEpochDuration) + } + + if requestData.ValidatorJailEpochDuration > 0 && + requestData.ValidatorJailEpochDuration != utxoView.GetCurrentGlobalParam(lib.ValidatorJailEpochDuration) { + extraData[lib.ValidatorJailEpochDuration.ToString()] = lib.UintToBuf(requestData.ValidatorJailEpochDuration) + } + + if requestData.LeaderScheduleMaxNumValidators > 0 && + requestData.LeaderScheduleMaxNumValidators != utxoView.GetCurrentGlobalParam(lib.LeaderScheduleMaxNumValidators) { + extraData[lib.LeaderScheduleMaxNumValidators.ToString()] = lib.UintToBuf(requestData.LeaderScheduleMaxNumValidators) + } + + if requestData.EpochDurationNumBlocks > 0 && + requestData.EpochDurationNumBlocks != utxoView.GetCurrentGlobalParam(lib.EpochDurationNumBlocks) { + extraData[lib.EpochDurationNumBlocks.ToString()] = lib.UintToBuf(requestData.EpochDurationNumBlocks) + } + + if requestData.JailInactiveValidatorGracePeriodEpochs > 0 && + requestData.JailInactiveValidatorGracePeriodEpochs != utxoView.GetCurrentGlobalParam(lib.JailInactiveValidatorGracePeriodEpochs) { + extraData[lib.JailInactiveValidatorGracePeriodEpochs.ToString()] = lib.UintToBuf(requestData.JailInactiveValidatorGracePeriodEpochs) + } + // Try and create the update txn for the user. txn, totalInput, changeAmount, fees, err := fes.blockchain.CreateUpdateGlobalParamsTxn( updaterPkBytes, @@ -182,6 +255,7 @@ func (fes *APIServer) UpdateGlobalParams(ww http.ResponseWriter, req *http.Reque minimumNetworkFeeNanosPerKb, []byte{}, maxNonceExpirationBlockHeightOffset, + extraData, requestData.MinFeeRateNanosPerKB, fes.backendServer.GetMempool(), additionalOutputs) if err != nil { diff --git a/routes/admin_transaction_test.go b/routes/admin_transaction_test.go new file mode 100644 index 00000000..06d67f7f --- /dev/null +++ b/routes/admin_transaction_test.go @@ -0,0 +1,137 @@ +package routes + +import ( + "bytes" + "encoding/json" + "github.com/deso-protocol/core/lib" + "github.com/stretchr/testify/require" + "io" + "net/http" + "net/http/httptest" + "testing" +) + +func TestUpdateGlobalParams(t *testing.T) { + // Hard-coded test constants + adminPublicKeyBase58Check := "tBCKWVydPvhXyxSVhntXCw7wUev2fUx64h84FLAfz4JStsdBAq4v9r" + adminJWT := "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE2NDQ3MDU1Mzh9.LXA2uT8tm-6DXDwTXaCRyqqbFNa96jLl_02LXyAwq58PbVPe28hrICP3P-D5g9mktPJolSVXK_UebRcL5oYCWg" + + // Init api server + apiServer := newTestApiServer(t) + apiServer.Config.SuperAdminPublicKeys = []string{adminPublicKeyBase58Check} + senderPkBytes, _, err := lib.Base58CheckDecode(senderPkString) + require.NoError(t, err) + apiServer.Params.ExtraRegtestParamUpdaterKeys[lib.MakePkMapKey(senderPkBytes)] = true + + // Helper utils + getGlobalParams := func() *GetGlobalParamsResponse { + // Send POST request. + body := GetGlobalParamsRequest{} + bodyJSON, err := json.Marshal(body) + require.NoError(t, err) + request, _ := http.NewRequest("POST", RoutePathGetGlobalParams, bytes.NewBuffer(bodyJSON)) + request.Header.Set("Content-Type", "application/json") + response := httptest.NewRecorder() + apiServer.router.ServeHTTP(response, request) + require.NotContains(t, string(response.Body.Bytes()), "error") + + // Decode response. + decoder := json.NewDecoder(io.LimitReader(response.Body, MaxRequestBodySizeBytes)) + globalParams := GetGlobalParamsResponse{} + err = decoder.Decode(&globalParams) + return &globalParams + } + + updateGlobalParams := func(body *UpdateGlobalParamsRequest) { + // Add JWT auth to body of request. + type MergedBody struct { + AdminRequest + UpdateGlobalParamsRequest + } + mergedBody := MergedBody{ + AdminRequest: AdminRequest{ + JWT: adminJWT, AdminPublicKey: adminPublicKeyBase58Check, + }, + UpdateGlobalParamsRequest: *body, + } + + // Send POST request. + bodyJSON, err := json.Marshal(mergedBody) + require.NoError(t, err) + request, _ := http.NewRequest("POST", RoutePathUpdateGlobalParams, bytes.NewBuffer(bodyJSON)) + request.Header.Set("Content-Type", "application/json") + response := httptest.NewRecorder() + apiServer.router.ServeHTTP(response, request) + require.NotContains(t, string(response.Body.Bytes()), "error") + + // Decode response. + decoder := json.NewDecoder(io.LimitReader(response.Body, MaxRequestBodySizeBytes)) + updateGlobalParamsResponse := UpdateGlobalParamsResponse{} + err = decoder.Decode(&updateGlobalParamsResponse) + require.NoError(t, err) + txn := updateGlobalParamsResponse.Transaction + + // Sign txn. + require.Nil(t, txn.Signature.Sign) + signTxn(t, txn, senderPrivString) + require.NotNil(t, txn.Signature.Sign) + + // Submit txn. + _, err = submitTxn(t, apiServer, txn) + require.NoError(t, err) + } + + // Tests + { + // Confirm default GlobalParams. + globalParams := getGlobalParams() + require.Zero(t, globalParams.MinimumNetworkFeeNanosPerKB) + require.Equal(t, globalParams.StakeLockupEpochDuration, uint64(3)) + require.Equal(t, globalParams.ValidatorJailEpochDuration, uint64(3)) + require.Equal(t, globalParams.LeaderScheduleMaxNumValidators, uint64(100)) + require.Equal(t, globalParams.EpochDurationNumBlocks, uint64(3600)) + require.Equal(t, globalParams.JailInactiveValidatorGracePeriodEpochs, uint64(48)) + } + { + // Update all GlobalParam fields. + updateGlobalParams(&UpdateGlobalParamsRequest{ + UpdaterPublicKeyBase58Check: senderPkString, + MinimumNetworkFeeNanosPerKB: 99, + StakeLockupEpochDuration: 4, + ValidatorJailEpochDuration: 4, + LeaderScheduleMaxNumValidators: 101, + EpochDurationNumBlocks: 3601, + JailInactiveValidatorGracePeriodEpochs: 49, + MinFeeRateNanosPerKB: 99, + }) + } + { + // Verify all updated GlobalParam fields. + globalParams := getGlobalParams() + require.Equal(t, globalParams.MinimumNetworkFeeNanosPerKB, uint64(99)) + require.Equal(t, globalParams.StakeLockupEpochDuration, uint64(4)) + require.Equal(t, globalParams.ValidatorJailEpochDuration, uint64(4)) + require.Equal(t, globalParams.LeaderScheduleMaxNumValidators, uint64(101)) + require.Equal(t, globalParams.EpochDurationNumBlocks, uint64(3601)) + require.Equal(t, globalParams.JailInactiveValidatorGracePeriodEpochs, uint64(49)) + } + { + // Update only one GlobalParam field. + updateGlobalParams(&UpdateGlobalParamsRequest{ + UpdaterPublicKeyBase58Check: senderPkString, + MinimumNetworkFeeNanosPerKB: 99, + JailInactiveValidatorGracePeriodEpochs: 50, + MinFeeRateNanosPerKB: 99, + }) + } + { + // Verify updated GlobalParam field. And other fields retain old values. + globalParams := getGlobalParams() + require.Equal(t, globalParams.MinimumNetworkFeeNanosPerKB, uint64(99)) + require.Equal(t, globalParams.StakeLockupEpochDuration, uint64(4)) + require.Equal(t, globalParams.ValidatorJailEpochDuration, uint64(4)) + require.Equal(t, globalParams.LeaderScheduleMaxNumValidators, uint64(101)) + require.Equal(t, globalParams.EpochDurationNumBlocks, uint64(3601)) + require.Equal(t, globalParams.JailInactiveValidatorGracePeriodEpochs, uint64(50)) + } +} diff --git a/test.Dockerfile b/test.Dockerfile index ce28ac59..19d7e5ad 100644 --- a/test.Dockerfile +++ b/test.Dockerfile @@ -12,8 +12,9 @@ WORKDIR /deso/src RUN git clone https://github.com/deso-protocol/core.git WORKDIR /deso/src/core -RUN git checkout mf/add-bls-signature-utils && \ - git pull origin mf/add-bls-signature-utils # TODO: Revert to `git pull` once core PR is merged. +RUN git pull && \ + git checkout mf/pos-merge-20230605 && \ + git pull origin mf/pos-merge-20230605 # TODO: Revert to `git pull` once core PR is merged. RUN go mod download RUN ./scripts/install-relic.sh