Skip to content

Commit

Permalink
registry: Add runtime-specific staking thresholds
Browse files Browse the repository at this point in the history
  • Loading branch information
kostko committed Jun 11, 2020
1 parent aa5a865 commit 9ac7171
Show file tree
Hide file tree
Showing 25 changed files with 828 additions and 134 deletions.
1 change: 1 addition & 0 deletions .changelog/2995.breaking.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
registry: Add runtime-specific staking thresholds
14 changes: 12 additions & 2 deletions docs/consensus/registry.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,15 +138,25 @@ The node descriptor structure MUST be signed by all of the following keys:
* P2P key.

Registering a node may require sufficient stake in the owning entity's
[escrow account]. The exact stake threshold is a consensus parameter (see
[`Thresholds` in staking consensus parameters]).
[escrow account]. There are two kinds of thresholds that the node may need to
satisfy:

* Global thresholds are the same for all runtimes and are defined by the
consensus parameters (see [`Thresholds` in staking consensus parameters]).

* In _addition_ to the global thresholds, each runtime the node is registering
for may define their own thresholds. In case the node is registering for
multiple runtimes, it needs to satisfy the maximum threshold of all the
runtimes it is registering for. The runtime-specific thresholds are defined
in the [`Staking` field] in the runtime descriptor.

<!-- markdownlint-disable line-length -->
[`NewRegisterNodeTx`]: https://pkg.go.dev/github.com/oasisprotocol/oasis-core/go/registry/api?tab=doc#NewRegisterNodeTx
[`MultiSignedNode`]: https://pkg.go.dev/github.com/oasisprotocol/oasis-core/go/common/node?tab=doc#MultiSignedNode
[`Node`]: https://pkg.go.dev/github.com/oasisprotocol/oasis-core/go/common/node?tab=doc#Node
[multi-signed envelope]: ../crypto.md#multi-signed-envelope
[`Thresholds` in staking consensus parameters]: https://pkg.go.dev/github.com/oasisprotocol/oasis-core/go/staking/api?tab=doc#ConsensusParameters.Thresholds
[`Staking` field]: https://pkg.go.dev/github.com/oasisprotocol/oasis-core/go/registry/api?tab=doc#Runtime.Staking
<!-- markdownlint-enable line-length -->

### Unfreeze Node
Expand Down
9 changes: 9 additions & 0 deletions go/common/quantity/quantity.go
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,15 @@ func NewQuantity() (q *Quantity) {
return &Quantity{}
}

// NewFromUint64 creates a new Quantity from an uint64 or panics.
func NewFromUint64(n uint64) *Quantity {
var q Quantity
if err := q.FromUint64(n); err != nil {
panic(err)
}
return &q
}

func isValid(n *big.Int) bool {
return n.Cmp(&zero) >= 0
}
Expand Down
9 changes: 7 additions & 2 deletions go/consensus/tendermint/apps/registry/transactions.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,12 @@ func (app *registryApplication) registerEntity(
}

if !params.DebugBypassStake {
if err = stakingState.AddStakeClaim(ctx, ent.ID, registry.StakeClaimRegisterEntity, []staking.ThresholdKind{staking.KindEntity}); err != nil {
if err = stakingState.AddStakeClaim(
ctx,
ent.ID,
registry.StakeClaimRegisterEntity,
staking.GlobalStakeThresholds(staking.KindEntity),
); err != nil {
ctx.Logger().Error("RegisterEntity: Insufficent stake",
"err", err,
"id", ent.ID,
Expand Down Expand Up @@ -306,7 +311,7 @@ func (app *registryApplication) registerNode( // nolint: gocyclo
}

claim := registry.StakeClaimForNode(newNode.ID)
thresholds := registry.StakeThresholdsForNode(newNode)
thresholds := registry.StakeThresholdsForNode(newNode, paidRuntimes)

if err = stakeAcc.AddStakeClaim(newNode.EntityID, claim, thresholds); err != nil {
ctx.Logger().Error("RegisterNode: insufficient stake for new node",
Expand Down
245 changes: 245 additions & 0 deletions go/consensus/tendermint/apps/registry/transactions_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
package registry

import (
"testing"
"time"

requirePkg "github.com/stretchr/testify/require"

"github.com/oasisprotocol/oasis-core/go/common"
"github.com/oasisprotocol/oasis-core/go/common/crypto/signature"
memorySigner "github.com/oasisprotocol/oasis-core/go/common/crypto/signature/signers/memory"
"github.com/oasisprotocol/oasis-core/go/common/entity"
"github.com/oasisprotocol/oasis-core/go/common/node"
"github.com/oasisprotocol/oasis-core/go/common/quantity"
abciAPI "github.com/oasisprotocol/oasis-core/go/consensus/tendermint/api"
registryState "github.com/oasisprotocol/oasis-core/go/consensus/tendermint/apps/registry/state"
stakingState "github.com/oasisprotocol/oasis-core/go/consensus/tendermint/apps/staking/state"
registry "github.com/oasisprotocol/oasis-core/go/registry/api"
staking "github.com/oasisprotocol/oasis-core/go/staking/api"
)

func TestRegisterNode(t *testing.T) {
require := requirePkg.New(t)

now := time.Unix(1580461674, 0)
appState := abciAPI.NewMockApplicationState(abciAPI.MockApplicationStateConfig{})
ctx := appState.NewContext(abciAPI.ContextDeliverTx, now)
defer ctx.Close()

app := registryApplication{appState}
state := registryState.NewMutableState(ctx.State())
stakeState := stakingState.NewMutableState(ctx.State())

// Set up default staking consensus parameters.
defaultStakeParameters := staking.ConsensusParameters{
Thresholds: map[staking.ThresholdKind]quantity.Quantity{
staking.KindEntity: *quantity.NewFromUint64(0),
staking.KindNodeValidator: *quantity.NewFromUint64(0),
staking.KindNodeCompute: *quantity.NewFromUint64(0),
staking.KindNodeStorage: *quantity.NewFromUint64(0),
staking.KindNodeKeyManager: *quantity.NewFromUint64(0),
staking.KindRuntimeCompute: *quantity.NewFromUint64(0),
staking.KindRuntimeKeyManager: *quantity.NewFromUint64(0),
},
}
// Set up registry consensus parameters.
err := state.SetConsensusParameters(ctx, &registry.ConsensusParameters{
MaxNodeExpiration: 5,
})
require.NoError(err, "registry.SetConsensusParameters")

tcs := []struct {
name string
prepareFn func(n *node.Node) []signature.Signer
stakeParams *staking.ConsensusParameters
valid bool
}{
// Node without any roles.
{"WithoutRoles", nil, nil, false},
// A simple validator node.
{
"Validator",
func(n *node.Node) []signature.Signer {
n.AddRoles(node.RoleValidator)
return nil
},
nil,
true,
},
// An expired validator node.
{
"ExpiredValidator",
func(n *node.Node) []signature.Signer {
n.AddRoles(node.RoleValidator)
n.Expiration = 0
return nil
},
nil,
false,
},
// Validator without enough stake.
{
"ValidatorWithoutStake",
func(n *node.Node) []signature.Signer {
n.AddRoles(node.RoleValidator)
return nil
},
&staking.ConsensusParameters{
Thresholds: map[staking.ThresholdKind]quantity.Quantity{
staking.KindEntity: *quantity.NewFromUint64(0),
staking.KindNodeValidator: *quantity.NewFromUint64(1000),
staking.KindNodeCompute: *quantity.NewFromUint64(0),
staking.KindNodeStorage: *quantity.NewFromUint64(0),
staking.KindNodeKeyManager: *quantity.NewFromUint64(0),
staking.KindRuntimeCompute: *quantity.NewFromUint64(0),
staking.KindRuntimeKeyManager: *quantity.NewFromUint64(0),
},
},
false,
},
// Compute node.
{
"ComputeNode",
func(n *node.Node) []signature.Signer {
// Create a new runtime.
rtSigner := memorySigner.NewTestSigner("consensus/tendermint/apps/registry: runtime signer: ComputeNode")
rt := registry.Runtime{
DescriptorVersion: registry.LatestRuntimeDescriptorVersion,
ID: common.NewTestNamespaceFromSeed([]byte("consensus/tendermint/apps/registry: runtime: ComputeNode"), 0),
Kind: registry.KindCompute,
}
sigRt, _ := registry.SignRuntime(rtSigner, registry.RegisterRuntimeSignatureContext, &rt)
_ = state.SetRuntime(ctx, &rt, sigRt, false)

n.AddRoles(node.RoleComputeWorker)
n.Runtimes = []*node.Runtime{
&node.Runtime{ID: rt.ID},
}
return nil
},
nil,
true,
},
// Compute node without per-runtime stake.
{
"ComputeNodeWithoutPerRuntimeStake",
func(n *node.Node) []signature.Signer {
// Create a new runtime.
rtSigner := memorySigner.NewTestSigner("consensus/tendermint/apps/registry: runtime signer: ComputeNodeWithoutPerRuntimeStake")
rt := registry.Runtime{
DescriptorVersion: registry.LatestRuntimeDescriptorVersion,
ID: common.NewTestNamespaceFromSeed([]byte("consensus/tendermint/apps/registry: runtime: ComputeNodeWithoutPerRuntimeStake"), 0),
Kind: registry.KindCompute,
Staking: registry.RuntimeStakingParameters{
Thresholds: map[staking.ThresholdKind]quantity.Quantity{
staking.KindNodeCompute: *quantity.NewFromUint64(1000),
},
},
}
sigRt, _ := registry.SignRuntime(rtSigner, registry.RegisterRuntimeSignatureContext, &rt)
_ = state.SetRuntime(ctx, &rt, sigRt, false)

n.AddRoles(node.RoleComputeWorker)
n.Runtimes = []*node.Runtime{
&node.Runtime{ID: rt.ID},
}
return nil
},
nil,
false,
},
}

for _, tc := range tcs {
t.Run(tc.name, func(t *testing.T) {
require = requirePkg.New(t)

// Reset staking consensus parameters.
stakeParams := tc.stakeParams
if stakeParams == nil {
stakeParams = &defaultStakeParameters
}
err = stakeState.SetConsensusParameters(ctx, stakeParams)
require.NoError(err, "staking.SetConsensusParameters")

// Prepare default signers.
entitySigner := memorySigner.NewTestSigner("consensus/tendermint/apps/registry: entity signer: " + tc.name)
nodeSigner := memorySigner.NewTestSigner("consensus/tendermint/apps/registry: node signer: " + tc.name)
consensusSigner := memorySigner.NewTestSigner("consensus/tendermint/apps/registry: consensus signer: " + tc.name)
p2pSigner := memorySigner.NewTestSigner("consensus/tendermint/apps/registry: p2p signer: " + tc.name)
tlsSigner := memorySigner.NewTestSigner("consensus/tendermint/apps/registry: tls signer: " + tc.name)

// Prepare a test entity that owns the nodes.
ent := entity.Entity{
DescriptorVersion: entity.LatestEntityDescriptorVersion,
ID: entitySigner.Public(),
Nodes: []signature.PublicKey{nodeSigner.Public()},
}
sigEnt, err := entity.SignEntity(entitySigner, registry.RegisterEntitySignatureContext, &ent)
require.NoError(err, "SignEntity")
err = state.SetEntity(ctx, &ent, sigEnt)
require.NoError(err, "SetEntity")

// Prepare a new minimal node.
var address node.Address
err = address.UnmarshalText([]byte("8.8.8.8:1234"))
require.NoError(err, "address.UnmarshalText")

n := node.Node{
DescriptorVersion: node.LatestNodeDescriptorVersion,
ID: nodeSigner.Public(),
EntityID: ent.ID,
Expiration: 3,
P2P: node.P2PInfo{
ID: p2pSigner.Public(),
Addresses: []node.Address{address},
},
Consensus: node.ConsensusInfo{
ID: consensusSigner.Public(),
Addresses: []node.ConsensusAddress{
{ID: consensusSigner.Public(), Address: address},
},
},
TLS: node.TLSInfo{
PubKey: tlsSigner.Public(),
Addresses: []node.TLSAddress{
{PubKey: tlsSigner.Public(), Address: address},
},
},
}
var signers []signature.Signer
if tc.prepareFn != nil {
signers = tc.prepareFn(&n)
}
if signers == nil {
signers = []signature.Signer{nodeSigner, p2pSigner, consensusSigner, tlsSigner}
}

// Sign the node.
sigNode, err := node.MultiSignNode(signers, registry.RegisterNodeSignatureContext, &n)
require.NoError(err, "MultiSignNode")

// Attempt to register the node.
ctx.SetTxSigner(nodeSigner.Public())
err = app.registerNode(ctx, state, sigNode)
switch tc.valid {
case true:
require.NoError(err, "node registration should succeed")

// Make sure the node has been registered.
var regNode *node.Node
regNode, err = state.Node(ctx, n.ID)
require.NoError(err, "node should be registered")
require.EqualValues(&n, regNode, "registered node descriptor should be correct")
case false:
require.Error(err, "node registration should fail")

// Make sure the state has not changed.
_, err = state.Node(ctx, n.ID)
require.Error(err, "node should not be registered")
require.Equal(registry.ErrNoSuchNode, err)
}
})
}
}
4 changes: 2 additions & 2 deletions go/consensus/tendermint/apps/staking/state/accumulator.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ func (c *StakeAccumulatorCache) CheckStakeClaims(id signature.PublicKey) error {
//
// In case there is insufficient stake to cover the claim or an error occurrs, no modifications are
// made to the stake accumulator.
func (c *StakeAccumulatorCache) AddStakeClaim(id signature.PublicKey, claim staking.StakeClaim, thresholds []staking.ThresholdKind) error {
func (c *StakeAccumulatorCache) AddStakeClaim(id signature.PublicKey, claim staking.StakeClaim, thresholds []staking.StakeThreshold) error {
acct, err := c.getAccount(id)
if err != nil {
return err
Expand Down Expand Up @@ -118,7 +118,7 @@ func NewStakeAccumulatorCache(ctx *abciAPI.Context) (*StakeAccumulatorCache, err
//
// In case there is no errors, the added claim is automatically committed. The caller must ensure
// that this does not overwrite any outstanding account updates.
func AddStakeClaim(ctx *abciAPI.Context, id signature.PublicKey, claim staking.StakeClaim, thresholds []staking.ThresholdKind) error {
func AddStakeClaim(ctx *abciAPI.Context, id signature.PublicKey, claim staking.StakeClaim, thresholds []staking.StakeThreshold) error {
sa, err := NewStakeAccumulatorCache(ctx)
if err != nil {
return err
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ func TestStakeAccumulatorCache(t *testing.T) {
err = stakeState.SetAccount(ctx, ent.ID, &acct)
require.NoError(err, "SetAccount")

err = acc.AddStakeClaim(ent.ID, staking.StakeClaim("claim"), []staking.ThresholdKind{staking.KindEntity})
err = acc.AddStakeClaim(ent.ID, staking.StakeClaim("claim"), staking.GlobalStakeThresholds(staking.KindEntity))
require.NoError(err, "AddStakeClaim")

err = acc.CheckStakeClaims(ent.ID)
Expand Down Expand Up @@ -82,7 +82,7 @@ func TestStakeAccumulatorCache(t *testing.T) {
require.Len(acct2.Escrow.StakeAccumulator.Claims, 1, "claims should not be updated")

// Test convenience functions.
err = AddStakeClaim(ctx, ent.ID, staking.StakeClaim("claim"), []staking.ThresholdKind{staking.KindEntity})
err = AddStakeClaim(ctx, ent.ID, staking.StakeClaim("claim"), staking.GlobalStakeThresholds(staking.KindEntity))
require.NoError(err, "AddStakeClaim")
err = RemoveStakeClaim(ctx, ent.ID, staking.StakeClaim("claim"))
require.NoError(err, "RemoveStakeClaim")
Expand Down
Loading

0 comments on commit 9ac7171

Please sign in to comment.