Skip to content

Commit

Permalink
feat(x/tally): executor payout in case of divergent gas reporting
Browse files Browse the repository at this point in the history
  • Loading branch information
hacheigriega committed Jan 14, 2025
1 parent efd1cb0 commit 9c23dc0
Show file tree
Hide file tree
Showing 5 changed files with 199 additions and 70 deletions.
49 changes: 31 additions & 18 deletions x/tally/keeper/endblock.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"strconv"
"strings"

"cosmossdk.io/math"
sdk "github.com/cosmos/cosmos-sdk/types"

"github.com/sedaprotocol/seda-wasm-vm/tallyvm/v2"
Expand Down Expand Up @@ -97,7 +98,8 @@ func (k Keeper) ProcessTallies(ctx sdk.Context, coreContract sdk.AccAddress) err
}

var distMsgs types.DistributionMessages
if len(req.Commits) < int(req.ReplicationFactor) {
switch {
case len(req.Commits) < int(req.ReplicationFactor):
dataResults[i].Result = []byte(fmt.Sprintf("need %d commits; received %d", req.ReplicationFactor, len(req.Commits)))
dataResults[i].ExitCode = TallyExitCodeNotEnoughCommits
k.Logger(ctx).Info("data request's number of commits did not meet replication factor", "request_id", req.ID)
Expand All @@ -106,8 +108,22 @@ func (k Keeper) ProcessTallies(ctx sdk.Context, coreContract sdk.AccAddress) err
if err != nil {
return err
}
} else {
_, tallyResults[i], distMsgs = k.FilterAndTally(ctx, req, params)
case len(req.Reveals) == 0:
dataResults[i].Result = []byte(fmt.Sprintf("no reveals"))
dataResults[i].ExitCode = TallyExitCodeNoReveals
k.Logger(ctx).Info("data request has no reveals", "request_id", req.ID)

distMsgs, err = k.CalculateCommitterPayouts(ctx, req)
if err != nil {
return err
}
default:
gasPriceInt, ok := math.NewIntFromString(req.GasPrice)
if !ok {
return fmt.Errorf("invalid gas price: %s", req.GasPrice) // TODO improve error handling
}

_, tallyResults[i], distMsgs = k.FilterAndTally(ctx, req, params, gasPriceInt)
dataResults[i].Result = tallyResults[i].Result
//nolint:gosec // G115: We shouldn't get negative exit code anyway.
dataResults[i].ExitCode = uint32(tallyResults[i].ExitInfo.ExitCode)
Expand Down Expand Up @@ -175,10 +191,10 @@ type TallyResult struct {

// FilterAndTally builds and applies filter, executes tally program,
// and calculates payouts.
func (k Keeper) FilterAndTally(ctx sdk.Context, req types.Request, params types.Params) (FilterResult, TallyResult, types.DistributionMessages) {
func (k Keeper) FilterAndTally(ctx sdk.Context, req types.Request, params types.Params, gasPrice math.Int) (FilterResult, TallyResult, types.DistributionMessages) {
var result TallyResult

// Sort the reveals by their keys.
// Sort the reveals by their keys (executors).
keys := make([]string, len(req.Reveals))
i := 0
for k := range req.Reveals {
Expand Down Expand Up @@ -222,21 +238,18 @@ func (k Keeper) FilterAndTally(ctx sdk.Context, req types.Request, params types.
}

// Phase III: Calculate Payouts
// TODO guarantee: len(reveals) > 0
var distMsgs types.DistributionMessages
var gasUsed uint64
var err error
if req.ReplicationFactor == 1 || areGasReportsUniform(reveals) {
distMsgs.Messages, gasUsed, err = CalculateUniformPayouts(reveals, req.ExecGasLimit, req.ReplicationFactor, req.GasPrice)
} else {
distMsgs.Messages, gasUsed, err = CalculateDivergentPayouts(reveals, req.ExecGasLimit, req.ReplicationFactor, req.GasPrice)
}
if err != nil {
return filterResult, result, types.DistributionMessages{} // TODO
if filterErr == nil || errors.Is(filterErr, types.ErrConsensusInError) {
var gasUsed uint64
if req.ReplicationFactor == 1 || areGasReportsUniform(reveals) {
distMsgs.Messages, gasUsed = CalculateUniformPayouts(keys, reveals[0].GasUsed, req.ExecGasLimit, req.ReplicationFactor, gasPrice)
} else {
distMsgs.Messages, gasUsed = CalculateDivergentPayouts(keys, reveals, req.ExecGasLimit, req.ReplicationFactor, gasPrice)
}
distMsgs.RefundType = types.DistributionTypeExecutorReward // TODO double check
result.ExecGasUsed = gasUsed
}
distMsgs.RefundType = types.DistributionTypeNoConsensus // TODO check
result.ExecGasUsed = gasUsed
// TODO: Requestor refund
// TODO: else pay committers?

return filterResult, result, distMsgs
}
Expand Down
4 changes: 2 additions & 2 deletions x/tally/keeper/endblock_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,13 +73,13 @@ func TestEndBlock(t *testing.T) {
expExitCode: keeper.TallyExitCodeNotEnoughCommits,
},
{
name: "reveal timeout",
name: "reveal timeout with no reveals",
memo: "cmV2ZWFsIHRpbWVvdXQ=",
replicationFactor: 2,
numCommits: 2,
numReveals: 0,
timeout: true,
expExitCode: keeper.TallyExitCodeFilterError,
expExitCode: keeper.TallyExitCodeNoReveals,
},
{
name: "reveal timeout with 2 reveals",
Expand Down
144 changes: 111 additions & 33 deletions x/tally/keeper/filter_and_tally_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"encoding/hex"
"testing"

"cosmossdk.io/math"
"github.com/stretchr/testify/require"

"github.com/sedaprotocol/seda-chain/x/tally/keeper"
Expand All @@ -26,7 +27,7 @@ func TestFilterAndTally(t *testing.T) {
replicationFactor uint16
consensus bool
consPubKeys []string // expected proxy public keys in basic consensus
gasUsed uint64
filterGasUsed uint64
exitCode int
filterErr error
}{
Expand All @@ -43,7 +44,7 @@ func TestFilterAndTally(t *testing.T) {
replicationFactor: 5,
consensus: true,
consPubKeys: nil,
gasUsed: defaultParams.FilterGasCostNone,
filterGasUsed: defaultParams.FilterGasCostNone,
exitCode: keeper.TallyExitCodeExecError, // since tally program does not exist
filterErr: nil,
},
Expand All @@ -57,7 +58,7 @@ func TestFilterAndTally(t *testing.T) {
replicationFactor: 5,
consensus: false,
consPubKeys: nil,
gasUsed: defaultParams.FilterGasCostNone,
filterGasUsed: defaultParams.FilterGasCostNone,
exitCode: keeper.TallyExitCodeFilterError,
filterErr: types.ErrNoBasicConsensus,
},
Expand All @@ -74,7 +75,7 @@ func TestFilterAndTally(t *testing.T) {
replicationFactor: 5,
consensus: true,
consPubKeys: nil,
gasUsed: defaultParams.FilterGasCostMultiplierMode * 5,
filterGasUsed: defaultParams.FilterGasCostMultiplierMode * 5,
exitCode: keeper.TallyExitCodeExecError, // since tally program does not exist
filterErr: nil,
},
Expand All @@ -88,19 +89,7 @@ func TestFilterAndTally(t *testing.T) {
replicationFactor: 5,
consensus: false,
consPubKeys: nil,
gasUsed: defaultParams.FilterGasCostMultiplierMode * 5,
exitCode: keeper.TallyExitCodeFilterError,
filterErr: types.ErrNoBasicConsensus,
},
{
name: "Mode filter - No reveals",
tallyInputAsHex: "01000000000000000D242E726573756C742E74657874", // json_path = $.result.text
outliers: []bool{},
reveals: map[string]types.RevealBody{},
replicationFactor: 5,
consensus: false,
consPubKeys: nil,
gasUsed: defaultParams.FilterGasCostMultiplierMode * 5,
filterGasUsed: defaultParams.FilterGasCostMultiplierMode * 5,
exitCode: keeper.TallyExitCodeFilterError,
filterErr: types.ErrNoBasicConsensus,
},
Expand All @@ -117,7 +106,7 @@ func TestFilterAndTally(t *testing.T) {
replicationFactor: 5,
consensus: true,
consPubKeys: nil,
gasUsed: defaultParams.FilterGasCostMultiplierStdDev * 5,
filterGasUsed: defaultParams.FilterGasCostMultiplierStdDev * 5,
exitCode: keeper.TallyExitCodeExecError, // since tally program does not exist
filterErr: nil,
},
Expand All @@ -131,19 +120,7 @@ func TestFilterAndTally(t *testing.T) {
replicationFactor: 5,
consensus: false,
consPubKeys: nil,
gasUsed: defaultParams.FilterGasCostMultiplierStdDev * 5,
exitCode: keeper.TallyExitCodeFilterError,
filterErr: types.ErrNoBasicConsensus,
},
{
name: "Standard deviation filter - No reveals",
tallyInputAsHex: "02000000000016E36001000000000000000D242E726573756C742E74657874", // max_sigma = 1.5, number_type = int64, json_path = $.result.text
outliers: []bool{},
reveals: map[string]types.RevealBody{},
replicationFactor: 5,
consensus: false,
consPubKeys: nil,
gasUsed: defaultParams.FilterGasCostMultiplierStdDev * 5,
filterGasUsed: defaultParams.FilterGasCostMultiplierStdDev * 5,
exitCode: keeper.TallyExitCodeFilterError,
filterErr: types.ErrNoBasicConsensus,
},
Expand All @@ -157,17 +134,20 @@ func TestFilterAndTally(t *testing.T) {
for k, v := range tt.reveals {
revealBody := v
revealBody.Reveal = base64.StdEncoding.EncodeToString([]byte(v.Reveal))
revealBody.GasUsed = v.GasUsed
reveals[k] = revealBody
}

filterRes, tallyRes, _ := f.tallyKeeper.FilterAndTally(f.Context(), types.Request{
Reveals: reveals,
ReplicationFactor: tt.replicationFactor,
ConsensusFilter: base64.StdEncoding.EncodeToString(filterInput),
}, types.DefaultParams())
GasPrice: "1000000000000000000", // 1e18
ExecGasLimit: 100000,
}, types.DefaultParams(), math.NewInt(1000000000000000000))

require.Equal(t, tt.outliers, filterRes.Outliers)
require.Equal(t, tt.gasUsed, filterRes.GasUsed)
require.Equal(t, tt.filterGasUsed, filterRes.GasUsed)
require.Equal(t, tt.consensus, filterRes.Consensus)
require.Equal(t, tt.consensus, tallyRes.Consensus)
require.Equal(t, tt.exitCode, tallyRes.ExitInfo.ExitCode)
Expand All @@ -185,3 +165,101 @@ func TestFilterAndTally(t *testing.T) {
})
}
}

func TestExecutorPayout(t *testing.T) {
f := initFixture(t)

defaultParams := types.DefaultParams()
err := f.tallyKeeper.SetParams(f.Context(), defaultParams)
require.NoError(t, err)

tests := []struct {
name string
tallyInputAsHex string
reveals map[string]types.RevealBody
replicationFactor uint16
execGasLimit uint64
expExecGasUsed uint64
expExecutorRewards map[string]math.Int
}{
{
name: "3/3 - Uniform gas reporting",
tallyInputAsHex: "00",
reveals: map[string]types.RevealBody{
"a": {ExitCode: 0, Reveal: `{"result": {"text": "A"}}`, GasUsed: 30000},
"b": {ExitCode: 0, Reveal: `{"result": {"text": "A"}}`, GasUsed: 30000},
"c": {ExitCode: 0, Reveal: `{"result": {"text": "A"}}`, GasUsed: 30000},
},
replicationFactor: 3,
execGasLimit: 30000,
expExecGasUsed: 90000,
expExecutorRewards: map[string]math.Int{
"a": math.NewIntWithDecimal(30000, 18),
"b": math.NewIntWithDecimal(30000, 18),
"c": math.NewIntWithDecimal(30000, 18),
},
},
{
name: "3/3 - Divergent gas reporting (1)",
tallyInputAsHex: "00",
reveals: map[string]types.RevealBody{
"a": {ExitCode: 0, Reveal: `{"result": {"text": "A"}}`, GasUsed: 28000},
"b": {ExitCode: 0, Reveal: `{"result": {"text": "A"}}`, GasUsed: 30000},
"c": {ExitCode: 0, Reveal: `{"result": {"text": "A"}}`, GasUsed: 32000},
},
replicationFactor: 3,
execGasLimit: 90000,
expExecGasUsed: 90000,
expExecutorRewards: map[string]math.Int{
"a": math.NewIntWithDecimal(43448, 18),
"b": math.NewIntWithDecimal(23275, 18),
"c": math.NewIntWithDecimal(23275, 18),
},
},
{
name: "3/3 - Divergent gas reporting (2)",
tallyInputAsHex: "00",
reveals: map[string]types.RevealBody{
"a": {ExitCode: 0, Reveal: `{"result": {"text": "A"}}`, GasUsed: 8000},
"b": {ExitCode: 0, Reveal: `{"result": {"text": "A"}}`, GasUsed: 20000},
"c": {ExitCode: 0, Reveal: `{"result": {"text": "A"}}`, GasUsed: 35000},
},
replicationFactor: 3,
execGasLimit: 90000,
expExecGasUsed: 56000,
expExecutorRewards: map[string]math.Int{
"a": math.NewIntWithDecimal(16000, 18),
"b": math.NewIntWithDecimal(20000, 18),
"c": math.NewIntWithDecimal(20000, 18),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
filterInput, err := hex.DecodeString(tt.tallyInputAsHex)
require.NoError(t, err)

reveals := make(map[string]types.RevealBody)
for k, v := range tt.reveals {
revealBody := v
revealBody.Reveal = base64.StdEncoding.EncodeToString([]byte(v.Reveal))
revealBody.GasUsed = v.GasUsed
reveals[k] = revealBody
}

_, tallyRes, distMsgs := f.tallyKeeper.FilterAndTally(f.Context(), types.Request{
Reveals: reveals,
ReplicationFactor: tt.replicationFactor,
ConsensusFilter: base64.StdEncoding.EncodeToString(filterInput),
GasPrice: "1000000000000000000", // 1e18
ExecGasLimit: tt.execGasLimit,
}, types.DefaultParams(), math.NewInt(1000000000000000000))

require.Equal(t, tt.expExecGasUsed, tallyRes.ExecGasUsed)
for _, distMsg := range distMsgs.Messages {
require.Equal(t, tt.expExecutorRewards[distMsg.Kind.ExecutorReward.Identity], distMsg.Kind.ExecutorReward.Amount)
require.Equal(t, types.DistributionTypeExecutorReward, distMsg.Type)
}
})
}
}
Loading

0 comments on commit 9c23dc0

Please sign in to comment.