Skip to content

Commit

Permalink
[Supplier] Implement supplier revenue share (#729)
Browse files Browse the repository at this point in the history
## Summary

This PR implements rev share feature for supplier rewards.
* Adds a `Supplier.RevShare` slice property.
* Adds the corresponding supplier staking configuration parser along
with its tests.
* Updates the tests to include the mandatory `Supplier.RevShare`
property.
* Includes revshare testing in the tokenomics test suite.
* Updates the supplier staking config documentation

_Note: ~1100LOC are protobuf autogenerated code._

## Issue

- #496 

## Type of change

Select one or more:

- [x] New feature, functionality or library
- [ ] Bug fix
- [ ] Code health or cleanup
- [ ] Documentation
- [ ] Other (specify)

## Testing

**Documentation changes** (only if making doc changes)
- [x] `make docusaurus_start`; only needed if you make doc changes

**Local Testing** (only if making code changes)
- [x] **Unit Tests**: `make go_develop_and_test`
- [x] **LocalNet E2E Tests**: `make test_e2e`
- See [quickstart
guide](https://dev.poktroll.com/developer_guide/quickstart) for
instructions

**PR Testing** (only if making code changes)
- [ ] **DevNet E2E Tests**: Add the `devnet-test-e2e` label to the PR.
- **THIS IS VERY EXPENSIVE**, so only do it after all the reviews are
complete.
- Optionally run `make trigger_ci` if you want to re-trigger tests
without any code changes
- If tests fail, try re-running failed tests only using the GitHub UI as
shown
[here](https://github.com/pokt-network/poktroll/assets/1892194/607984e9-0615-4569-9452-4c730190c1d2)


## Sanity Checklist

- [x] I have tested my changes using the available tooling
- [x] I have commented my code
- [x] I have performed a self-review of my own code; both comments &
source code
- [ ] I create and reference any new tickets, if applicable
- [ ] I have left TODOs throughout the codebase, if applicable


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

## Summary by CodeRabbit

- **New Features**
- Introduced flexible revenue sharing configurations for suppliers,
allowing for detailed management of service revenue shares.
- Added new fields to configuration files and structures to support
default and service-specific revenue share percentages.
- Implemented stricter validation for revenue share entries to ensure
compliance with defined criteria.

- **Bug Fixes**
- Enhanced error handling related to invalid revenue shares and owner
addresses in configurations.

- **Tests**
- Expanded test coverage to include revenue sharing logic, ensuring
robust validation of configurations and expected behaviors.
- Added new test cases for various scenarios, including configurations
with default and service-specific revenue shares.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Daniel Olshansky <[email protected]>
  • Loading branch information
red-0ne and Olshansk authored Aug 15, 2024
1 parent 5be5d83 commit 83c3766
Show file tree
Hide file tree
Showing 22 changed files with 1,828 additions and 134 deletions.
733 changes: 706 additions & 27 deletions api/poktroll/shared/service.pulsar.go

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -178,12 +178,18 @@ genesis:
- configs: []
rpc_type: JSON_RPC
url: http://relayminer1:8545
rev_share:
- address: pokt19a3t4yunp0dlpfjrp7qwnzwlrzd5fzs2gjaaaj
rev_share_percentage: "100"
- service:
id: ollama
endpoints:
- configs: []
rpc_type: REST
url: http://relayminer1:8545
rev_share:
- address: pokt19a3t4yunp0dlpfjrp7qwnzwlrzd5fzs2gjaaaj
rev_share_percentage: "100"
stake:
# NB: This value should be exactly 1upokt smaller than the value in
# `application1_stake_config.yaml` so that the stake command causes a state change.
Expand Down
80 changes: 80 additions & 0 deletions docusaurus/docs/operate/configs/supplier_staking_config.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,15 @@ a stake transaction required to provide RPC services on Pocket Network._
- [Reference Example](#reference-example)
- [Usage](#usage)
- [Configuration](#configuration)
- [`owner_address`](#owner_address)
- [`stake_amount`](#stake_amount)
- [`default_rev_share_percent`](#default_rev_share_percent)
- [`services`](#services)
- [`service_id`](#service_id)
- [`endpoints`](#endpoints)
- [`publicly_exposed_url`](#publicly_exposed_url)
- [`rpc_type`](#rpc_type)
- [`rev_share_percent`](#rev_share_percent)

## Reference Example

Expand All @@ -43,6 +46,18 @@ poktrolld tx supplier stake-supplier \

## Configuration

### `owner_address`

_`Required`_, _`Non-empty`_

```yaml
owner_address: <address>
```
The `owner_address` is used as the unique shareholder address for the `Supplier`
if none of `default_rev_share_percent` or `rev_share_percent` is defined in the
configuration file.

### `stake_amount`

_`Required`_, _`Non-empty`_
Expand Down Expand Up @@ -72,6 +87,43 @@ sybil or flooding attacks on the network.

:::

### `default_rev_share_percent`

_`Optional`_, _`Non-empty`_

```yaml
default_rev_share_percent:
<shareholder_address>: <float>
```

`default_rev_share_percent` is an optional map that defines the default the revenue
share percentage for all the `service`s that do not have their specific `rev_share_percent`
entry defined.

This field is useful if the `Supplier` owner wants to set a default revenue share
for all the `service`s entries that do not provide one. This way, the operator
does not have to repeat the same values for each `service` in the `services` section.

This map cannot be empty but can be omitted, in which case the default revenue
share falls back to `100%` of the rewards allocated to the `Supplier`'s `owner_address`.

:::note

The `shareholder_address`s MUST be valid Pocket addresses.

The revenue share values MUST be strictly positive floats with a maximum value of
100 and a total sum of 100 across all the `shareholder_address`es.

:::

:::warning

If `default_rev_share_percent` is defined, then the `owner_address` of the `Supplier`
MUST be **explicitly** defined in the map if they are to receive a share on the
`service`s that fall back to the default.

:::

### `services`

_`Required`_, _`Non-empty`_
Expand All @@ -82,6 +134,8 @@ services:
endpoints:
- publicly_exposed_url: <protocol>://<hostname>:<port>
rpc_type: <string>
rev_share_percent:
<shareholder_address>: <float>
```

`services` define the list of services that the `Supplier` wants to provide.
Expand Down Expand Up @@ -154,3 +208,29 @@ endpoints:
:::
The `rpc_type` MUST be one of the [supported types found here](https://github.com/pokt-network/poktroll/tree/main/pkg/relayer/config/types.go#L8).
#### `rev_share_percent`
`rev_share_percent` is an optional map that defines the `service`'s specific revenue
share percentage.

It overrides the `default_rev_share_percent` if defined for the `service`.

This map cannot be empty but can be omitted, in which case it falls back to the
`default_rev_share_percent` top-level configuration entry.

:::note

The `shareholder_address`s MUST be valid Pocket addresses.

The revenue share values MUST be strictly positive decimals with a maximum value
of 100 and a total sum of 100 across all the `shareholder_address`es.

:::

:::warning

If `rev_share_percent` is defined for a `service`, then the `owner_address` of the
`Supplier` MUST be **explicitly** defined in the map if they are to receive a share.

:::
14 changes: 14 additions & 0 deletions localnet/poktrolld/config/supplier1_stake_config.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
owner_address: pokt1mrqt5f7qh8uxs27cjm9t7v9e74a9vvdnq5jva4
# NB: The stake amount is exactly 1upokt greater than the value in genesis.json
# so that the stake command causes a state change.
stake_amount: 1000069upokt
# If default_rev_share_percent is omitted, the owner receives 100% of the rewards.
# default_rev_share_percent cannot be empty - it must either be omitted completely
# or include at least one item.
default_rev_share_percent:
# The sum of all shares must equal 100%. Staking will fail otherwise.
- pokt1mrqt5f7qh8uxs27cjm9t7v9e74a9vvdnq5jva4: 80.5
- pokt1eeeksh2tvkh7wzmfrljnhw4wrhs55lcuvmekkw: 19.5
services:
# The endpoint URL for the Anvil service is provided via the RelayMiner.
# The RelayMiner acts as a proxy, forwarding requests to the actual Anvil data node behind it.
Expand All @@ -10,6 +18,12 @@ services:
- publicly_exposed_url: http://relayminer1:8545
rpc_type: JSON_RPC
- service_id: ollama
# Service specific rev share, if rev_share_percent is omitted for a specific
# service, default_rev_share_percent is used.
# The sum of all shares must equal 100%. Staking will fail otherwise.
rev_share_percent:
- pokt1mrqt5f7qh8uxs27cjm9t7v9e74a9vvdnq5jva4: 50
- pokt1eeeksh2tvkh7wzmfrljnhw4wrhs55lcuvmekkw: 50
endpoints:
- publicly_exposed_url: http://relayminer1:8545
rpc_type: REST
7 changes: 7 additions & 0 deletions proto/poktroll/shared/service.proto
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ message SupplierServiceConfig {
// TODO_MAINNET: Avoid embedding the full Service because we just need the ID.
Service service = 1; // The Service for which the supplier is configured
repeated SupplierEndpoint endpoints = 2; // List of endpoints for the service
repeated ServiceRevenueShare rev_share = 3; // List of revenue share configurations for the service
// TODO_MAINNET: There is an opportunity for supplier to advertise the min
// they're willing to earn for a certain configuration/price, but this is outside of scope.
}
Expand All @@ -54,6 +55,12 @@ message SupplierEndpoint {
repeated ConfigOption configs = 3; // Additional configuration options for the endpoint
}

// ServiceRevenueShare message to hold revenue share configuration details
message ServiceRevenueShare {
string address = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"]; // The Bech32 address of the revenue share recipient
float rev_share_percentage = 2; // The percentage of revenue share the recipient will receive
}

// Enum to define RPC types
enum RPCType {
UNKNOWN_RPC = 0; // Undefined RPC type
Expand Down
6 changes: 6 additions & 0 deletions testutil/integration/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -534,6 +534,12 @@ func NewCompleteIntegrationApp(t *testing.T) *App {
Stake: &supplierStake,
Services: []*sharedtypes.SupplierServiceConfig{
{
RevShare: []*sharedtypes.ServiceRevenueShare{
{
Address: sample.AccAddress(),
RevSharePercentage: 100,
},
},
Service: &defaultService,
},
},
Expand Down
16 changes: 14 additions & 2 deletions testutil/keeper/tokenomics.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,10 +122,22 @@ func TokenomicsKeeperWithActorAddrs(t testing.TB) (
}

// Prepare the test supplier.
supplierOwnerAddr := sample.AccAddress()
supplier := sharedtypes.Supplier{
OwnerAddress: sample.AccAddress(),
OperatorAddress: sample.AccAddress(),
OwnerAddress: supplierOwnerAddr,
OperatorAddress: supplierOwnerAddr,
Stake: &sdk.Coin{Denom: "upokt", Amount: math.NewInt(100000)},
Services: []*sharedtypes.SupplierServiceConfig{
{
Service: service,
RevShare: []*sharedtypes.ServiceRevenueShare{
{
Address: supplierOwnerAddr,
RevSharePercentage: 100,
},
},
},
},
}

ctrl := gomock.NewController(t)
Expand Down
56 changes: 56 additions & 0 deletions x/shared/helpers/service_configs.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,15 @@ package helpers
import (
"fmt"

sdk "github.com/cosmos/cosmos-sdk/types"

sharedtypes "github.com/pokt-network/poktroll/x/shared/types"
)

const (
requiredRevSharePercentageSum = 100
)

// ValidateAppServiceConfigs returns an error if any of the application service configs are invalid
func ValidateAppServiceConfigs(services []*sharedtypes.ApplicationServiceConfig) error {
if len(services) == 0 {
Expand Down Expand Up @@ -78,6 +84,56 @@ func ValidateSupplierServiceConfigs(services []*sharedtypes.SupplierServiceConfi
// return fmt.Errorf("endpoint.Configs must have at least one entry: %v", serviceConfig)
// }
}

if err := ValidateServiceRevShare(serviceConfig.RevShare); err != nil {
return err
}
}

return nil
}

// ValidateServiceRevShare validates the supplier's service revenue share,
// ensuring that the sum of the revenue share percentages is 100.
// NB: This function is unit tested via the supplier staking config tests.
func ValidateServiceRevShare(revShareList []*sharedtypes.ServiceRevenueShare) error {
revSharePercentageSum := float32(0)

if len(revShareList) == 0 {
return sharedtypes.ErrSharedInvalidRevShare.Wrap("no rev share configurations")
}

for _, revShare := range revShareList {
if revShare == nil {
return sharedtypes.ErrSharedInvalidRevShare.Wrap("rev share cannot be nil")
}

// Validate the revshare address
if revShare.Address == "" {
return sharedtypes.ErrSharedInvalidRevShare.Wrapf("rev share address cannot be empty: %v", revShare)
}

if _, err := sdk.AccAddressFromBech32(revShare.Address); err != nil {
return sharedtypes.ErrSharedInvalidRevShare.Wrapf("invalid rev share address %s; (%v)", revShare.Address, err)
}

if revShare.RevSharePercentage <= 0 || revShare.RevSharePercentage > 100 {
return sharedtypes.ErrSharedInvalidRevShare.Wrapf(
"invalid rev share value %v; must be between 0 and 100",
revShare.RevSharePercentage,
)
}

revSharePercentageSum += revShare.RevSharePercentage
}

if revSharePercentageSum != requiredRevSharePercentageSum {
return sharedtypes.ErrSharedInvalidRevShare.Wrapf(
"invalid rev share percentage sum %v; must be equal to %v",
revSharePercentageSum,
requiredRevSharePercentageSum,
)
}

return nil
}
1 change: 1 addition & 0 deletions x/shared/types/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,5 @@ var (
ErrSharedParamInvalid = sdkerrors.Register(ModuleName, 1103, "the provided param is invalid")
ErrSharedEmitEvent = sdkerrors.Register(ModuleName, 1104, "failed to emit event")
ErrSharedUnauthorizedSupplierUpdate = sdkerrors.Register(ModuleName, 1105, "unauthorized supplier update")
ErrSharedInvalidRevShare = sdkerrors.Register(ModuleName, 1106, "invalid revenue share configuration")
)
Loading

0 comments on commit 83c3766

Please sign in to comment.