Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions cmd/bee/cmd/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@ const (
optionSkipPostageSnapshot = "skip-postage-snapshot"
optionNameMinimumGasTipCap = "minimum-gas-tip-cap"
optionNameGasLimitFallback = "gas-limit-fallback"
optionNameMaxTxCost = "max-tx-cost"
optionNameMaxTxCostTolerancePercent = "max-tx-cost-tolerance-percent"
optionNameP2PWSSEnable = "p2p-wss-enable"
optionP2PWSSAddr = "p2p-wss-addr"
optionNATWSSAddr = "nat-wss-addr"
Expand Down Expand Up @@ -341,6 +343,21 @@ func (c *command) setAllFlags(cmd *cobra.Command) {
cmd.Flags().Bool(optionSkipPostageSnapshot, false, "skip postage snapshot")
cmd.Flags().Uint64(optionNameMinimumGasTipCap, 0, "minimum gas tip cap in wei for transactions, 0 means use suggested gas tip cap")
cmd.Flags().Uint64(optionNameGasLimitFallback, 500_000, "gas limit fallback when estimation fails for contract transactions")
// Default 1.5e15 wei (= 0.0015 xDAI = 1.5 mxDAI) is calibrated against the
// upper-bound formula used by isTxCostAcceptable:
// estimated = gasUnits × (2·baseFee + boostedTip)
// where gasUnits = 500_000 (gas-limit-fallback when not overridden via ctx),
// boostedTip = suggestedTip × 1.5 (BoostTipPercent=50, ≈0.15 gwei on Gnosis).
// With max-tx-cost-tolerance-percent=10 the effective threshold is 1.65e15 wei,
// which permits a gasFeeCap of up to ≈3.3 gwei → roughly baseFee ≤ ~1.5 gwei.
// Observed network-wide effective gas prices over the last 10 rounds were
// 0.3–0.5 gwei in normal conditions and peaked at ≈1.5 gwei during a spike;
// this default keeps commit/reveal flowing through ordinary operation and
// only rejects sustained spikes above ≈1.5 gwei baseFee. Claim has its own
// override path (see ClaimOpts) and is unaffected by spikes when the
// expected reward covers the upper-bound cost.
cmd.Flags().Uint64(optionNameMaxTxCost, 1_500_000_000_000_000, "maximum total cost in wei per redistribution transaction (gas limit × max fee per gas); 0 means no limit. Default 1.5e15 wei (= 0.0015 xDAI = 1.5 mxDAI) is calibrated for typical Gnosis baseFee up to ~1.5 gwei with default gas-limit-fallback=500000 and 10% tolerance.")
cmd.Flags().Uint64(optionNameMaxTxCostTolerancePercent, 10, "percentage above max-tx-cost within which the transaction is still allowed (effective threshold = max-tx-cost × (1 + tol/100))")
cmd.Flags().Int(optionNameTransactionRetryMaxRetries, 5, "maximum broadcast attempts for SendWithRetry (e.g. redistribution txs)")
cmd.Flags().Duration(optionNameTransactionRetryDelay, time.Minute, "how long to wait for a receipt before escalating fees in transactions with retry")
cmd.Flags().Int(optionNameTransactionRetryGasIncreasePercent, 20, "percent increase applied to priority fee after each transactions with retry escalation step")
Expand Down
2 changes: 2 additions & 0 deletions cmd/bee/cmd/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,8 @@ func buildBeeNode(ctx context.Context, c *command, cmd *cobra.Command, logger lo
WarmupTime: c.config.GetDuration(optionWarmUpTime),
WelcomeMessage: c.config.GetString(optionWelcomeMessage),
WhitelistedWithdrawalAddress: c.config.GetStringSlice(optionNameWhitelistedWithdrawalAddress),
MaxTxCost: c.config.GetUint64(optionNameMaxTxCost),
MaxTxCostTolerancePercent: c.config.GetUint64(optionNameMaxTxCostTolerancePercent),
})

return b, err
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ require (
github.com/ethereum/go-ethereum v1.16.9
github.com/ethersphere/batch-archive v0.0.6
github.com/ethersphere/go-price-oracle-abi v0.6.9
github.com/ethersphere/go-storage-incentives-abi v0.9.4
github.com/ethersphere/go-storage-incentives-abi v0.9.3-rc4
github.com/ethersphere/go-sw3-abi v0.6.9
github.com/ethersphere/langos v1.0.0
github.com/go-playground/validator/v10 v10.19.0
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -254,8 +254,8 @@ github.com/ethersphere/batch-archive v0.0.6 h1:Nt9mundj8CXT42qgGdq1sqKIVOk4KkKC4
github.com/ethersphere/batch-archive v0.0.6/go.mod h1:41BPb192NoK9CYjNB8BAE1J2MtiI/5aq0Wtas5O7A7Q=
github.com/ethersphere/go-price-oracle-abi v0.6.9 h1:bseen6he3PZv5GHOm+KD6s4awaFmVSD9LFx+HpB6rCU=
github.com/ethersphere/go-price-oracle-abi v0.6.9/go.mod h1:sI/Qj4/zJ23/b1enzwMMv0/hLTpPNVNacEwCWjo6yBk=
github.com/ethersphere/go-storage-incentives-abi v0.9.4 h1:mSIWXQXg5OQmH10QvXMV5w0vbSibFMaRlBL37gPLTM0=
github.com/ethersphere/go-storage-incentives-abi v0.9.4/go.mod h1:SXvJVtM4sEsaSKD0jc1ClpDLw8ErPoROZDme4Wrc/Nc=
github.com/ethersphere/go-storage-incentives-abi v0.9.3-rc4 h1:YK9FpiQz29ctU5V46CuwMt+4X5Xn8FTBwy6E2v/ix8s=
github.com/ethersphere/go-storage-incentives-abi v0.9.3-rc4/go.mod h1:SXvJVtM4sEsaSKD0jc1ClpDLw8ErPoROZDme4Wrc/Nc=
github.com/ethersphere/go-sw3-abi v0.6.9 h1:TnWLnYkWE5UvC17mQBdUmdkzhPhO8GcqvWy4wvd1QJQ=
github.com/ethersphere/go-sw3-abi v0.6.9/go.mod h1:BmpsvJ8idQZdYEtWnvxA8POYQ8Rl/NhyCdF0zLMOOJU=
github.com/ethersphere/langos v1.0.0 h1:NBtNKzXTTRSue95uOlzPN4py7Aofs0xWPzyj4AI1Vcc=
Expand Down
2 changes: 1 addition & 1 deletion pkg/api/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -752,7 +752,7 @@ func (m *mockContract) IsWinner(context.Context) (bool, error) {
return false, nil
}

func (m *mockContract) Claim(context.Context, redistribution.ChunkInclusionProofs) (common.Hash, error) {
func (m *mockContract) Claim(context.Context, redistribution.ChunkInclusionProofs, *redistribution.ClaimOpts) (common.Hash, error) {
m.mtx.Lock()
defer m.mtx.Unlock()
m.callsList = append(m.callsList, claimCall)
Expand Down
3 changes: 3 additions & 0 deletions pkg/node/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,8 @@
WarmupTime time.Duration
WelcomeMessage string
WhitelistedWithdrawalAddress []string
MaxTxCost uint64
MaxTxCostTolerancePercent uint64
}

func txRetryConfigFromOptions(o *Options) transaction.TransactionsRetryConfig {
Expand Down Expand Up @@ -1350,6 +1352,7 @@

if agent != nil {
apiService.MustRegisterMetrics(agent.Metrics()...)
apiService.MustRegisterMetrics(redistribution.Metrics()...)

Check failure on line 1355 in pkg/node/node.go

View workflow job for this annotation

GitHub Actions / Init

undefined: redistribution.Metrics

Check failure on line 1355 in pkg/node/node.go

View workflow job for this annotation

GitHub Actions / Test (macos-latest)

undefined: redistribution.Metrics

Check failure on line 1355 in pkg/node/node.go

View workflow job for this annotation

GitHub Actions / Test (ubuntu-latest)

undefined: redistribution.Metrics
}

apiService.MustRegisterMetrics(pushSyncProtocol.Metrics()...)
Expand Down
14 changes: 14 additions & 0 deletions pkg/postage/postagecontract/contract.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ type Interface interface {
TopUpBatch(ctx context.Context, batchID []byte, topupBalance *big.Int) (common.Hash, error)
DiluteBatch(ctx context.Context, batchID []byte, newDepth uint8) (common.Hash, error)
Paused(ctx context.Context) (bool, error)
ExpectedReward(ctx context.Context) (*big.Int, error)
PostageBatchExpirer
}

Expand Down Expand Up @@ -338,6 +339,15 @@ func (c *postageContract) getProperty(ctx context.Context, propertyName string,
return nil
}

// ExpectedReward returns the current redistribution pot (totalPot) from the postage stamp contract.
func (c *postageContract) ExpectedReward(ctx context.Context) (*big.Int, error) {
pot := new(big.Int)
if err := c.getProperty(ctx, "totalPot", pot); err != nil {
return nil, fmt.Errorf("totalPot: %w", err)
}
return pot, nil
}

func (c *postageContract) getMinInitialBalance(ctx context.Context) (uint64, error) {
var lastPrice uint64
err := c.getProperty(ctx, "lastPrice", &lastPrice)
Expand Down Expand Up @@ -556,6 +566,10 @@ func (m *noOpPostageContract) ExpireBatches(context.Context) error {
return ErrChainDisabled
}

func (m *noOpPostageContract) ExpectedReward(context.Context) (*big.Int, error) {
return nil, ErrChainDisabled
}

func LookupERC20Address(ctx context.Context, transactionService transaction.Service, postageStampContractAddress common.Address, postageStampContractABI abi.ABI, chainEnabled bool) (common.Address, error) {
if !chainEnabled {
return common.Address{}, nil
Expand Down
24 changes: 19 additions & 5 deletions pkg/postage/postagecontract/mock/contract.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,12 @@ import (
)

type contractMock struct {
createBatch func(ctx context.Context, initialBalance *big.Int, depth uint8, immutable bool, label string) (common.Hash, []byte, error)
topupBatch func(ctx context.Context, id []byte, amount *big.Int) (common.Hash, error)
diluteBatch func(ctx context.Context, id []byte, newDepth uint8) (common.Hash, error)
expireBatches func(ctx context.Context) error
paused func(ctx context.Context) (bool, error)
createBatch func(ctx context.Context, initialBalance *big.Int, depth uint8, immutable bool, label string) (common.Hash, []byte, error)
topupBatch func(ctx context.Context, id []byte, amount *big.Int) (common.Hash, error)
diluteBatch func(ctx context.Context, id []byte, newDepth uint8) (common.Hash, error)
expireBatches func(ctx context.Context) error
paused func(ctx context.Context) (bool, error)
expectedReward func(ctx context.Context) (*big.Int, error)
}

func (c *contractMock) CreateBatch(ctx context.Context, initialBalance *big.Int, depth uint8, immutable bool, label string) (common.Hash, []byte, error) {
Expand All @@ -40,6 +41,13 @@ func (s *contractMock) Paused(ctx context.Context) (bool, error) {
return s.paused(ctx)
}

func (c *contractMock) ExpectedReward(ctx context.Context) (*big.Int, error) {
if c.expectedReward != nil {
return c.expectedReward(ctx)
}
return big.NewInt(1_000_000), nil
}

// Option is an option passed to New
type Option func(*contractMock)

Expand Down Expand Up @@ -83,3 +91,9 @@ func WithPaused(f func(ctx context.Context) (bool, error)) Option {
mock.paused = f
}
}

func WithExpectedRewardFunc(f func(ctx context.Context) (*big.Int, error)) Option {
return func(m *contractMock) {
m.expectedReward = f
}
}
53 changes: 42 additions & 11 deletions pkg/storageincentives/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ const (

// average tx gas used by transactions issued from agent
avgTxGas = 250_000

// forceClaimBlocksBeforeEnd is how many blocks before round end claim may
// bypass max-tx-cost when economics justify it (see redistribution.ClaimOpts).
forceClaimBlocksBeforeEnd = 10
)

type ChainBackend interface {
Expand All @@ -59,8 +63,9 @@ type Agent struct {
metrics metrics
backend ChainBackend
blocksPerRound uint64
blockTime time.Duration
contract redistribution.Contract
batchExpirer postagecontract.PostageBatchExpirer
postageContract postagecontract.Interface
redistributionStatuser staking.RedistributionStatuser
store storer.Reserve
fullSyncedFunc func() bool
Expand All @@ -78,7 +83,7 @@ func New(overlay swarm.Address,
ethAddress common.Address,
backend ChainBackend,
contract redistribution.Contract,
batchExpirer postagecontract.PostageBatchExpirer,
postageContract postagecontract.Interface,
redistributionStatuser staking.RedistributionStatuser,
store storer.Reserve,
fullSyncedFunc func() bool,
Expand All @@ -98,10 +103,11 @@ func New(overlay swarm.Address,
backend: backend,
logger: logger.WithName(loggerName).Register(),
contract: contract,
batchExpirer: batchExpirer,
postageContract: postageContract,
store: store,
fullSyncedFunc: fullSyncedFunc,
blocksPerRound: blocksPerRound,
blockTime: blockTime,
quit: make(chan struct{}),
redistributionStatuser: redistributionStatuser,
health: health,
Expand All @@ -116,7 +122,7 @@ func New(overlay swarm.Address,
a.state = state

a.wg.Add(1)
go a.start(blockTime, a.blocksPerRound, blocksPerPhase)
go a.start(a.blockTime, a.blocksPerRound, blocksPerPhase)

return a, nil
}
Expand Down Expand Up @@ -311,7 +317,7 @@ func (a *Agent) handleReveal(ctx context.Context, round uint64) error {
a.metrics.ErrReveal.Inc()
return err
}
a.state.AddFee(ctx, txHash)
a.state.AddRoundFee(ctx, round, txHash)

a.state.SetHasRevealed(round)

Expand Down Expand Up @@ -344,7 +350,7 @@ func (a *Agent) handleClaim(ctx context.Context, round uint64) error {

// In case when there are too many expired batches, Claim trx could runs out of gas.
// To prevent this, node should first expire batches before Claiming a reward.
err = a.batchExpirer.ExpireBatches(ctx)
err = a.postageContract.ExpireBatches(ctx)
if err != nil {
a.logger.Info("expire batches failed", "err", err)
// Even when error happens, proceed with claim handler
Expand All @@ -353,7 +359,7 @@ func (a *Agent) handleClaim(ctx context.Context, round uint64) error {

errBalance := a.state.SetBalance(ctx)
if errBalance != nil {
a.logger.Info("could not set balance", "err", err)
a.logger.Info("could not set balance", "err", errBalance)
}

sampleData, exists := a.state.SampleData(round - 1)
Expand All @@ -371,8 +377,33 @@ func (a *Agent) handleClaim(ctx context.Context, round uint64) error {
return fmt.Errorf("making inclusion proofs: %w", err)
}

txHash, err := a.contract.Claim(ctx, proofs)
claimCtx := ctx
phaseEndBlock := (round+1)*a.blocksPerRound - 1
if rem := int64(phaseEndBlock) - int64(a.state.currentBlock()); rem > 0 {
var cancel context.CancelFunc
claimCtx, cancel = context.WithDeadline(ctx, time.Now().Add(time.Duration(rem)*a.blockTime))
defer cancel()
}

reward, err := a.postageContract.ExpectedReward(ctx)
if err != nil {
a.logger.Warning("could not estimate claim reward, override max_tx_cost option will be disabled", "error", err)
}

opts := &redistribution.ClaimOpts{
OverrideAfterBlock: (round+1)*a.blocksPerRound - forceClaimBlocksBeforeEnd,
CurrentBlockFn: func() uint64 { return a.state.currentBlock() },
ExpectedReward: reward,
RoundFees: a.state.RoundFees(round),
}

txHash, err := a.contract.Claim(claimCtx, proofs, opts)
if err != nil {
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
a.logger.Info("claim aborted by context", "round", round, "err", err)
a.metrics.SkippedExpensivePhase.Inc()
return nil
}
a.metrics.ErrClaim.Inc()
return fmt.Errorf("claiming win: %w", err)
}
Expand All @@ -382,11 +413,11 @@ func (a *Agent) handleClaim(ctx context.Context, round uint64) error {
if errBalance == nil {
errReward := a.state.CalculateWinnerReward(ctx)
if errReward != nil {
a.logger.Info("calculate winner reward", "err", err)
a.logger.Info("calculate winner reward", "err", errReward)
}
}

a.state.AddFee(ctx, txHash)
a.state.AddRoundFee(ctx, round, txHash)

return nil
}
Expand Down Expand Up @@ -539,7 +570,7 @@ func (a *Agent) commit(ctx context.Context, sample SampleData, round uint64) err
a.metrics.ErrCommit.Inc()
return err
}
a.state.AddFee(ctx, txHash)
a.state.AddRoundFee(ctx, round, txHash)

a.state.SetCommitKey(round, key)

Expand Down
2 changes: 1 addition & 1 deletion pkg/storageincentives/agent_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,7 @@ func (m *mockContract) IsWinner(context.Context) (bool, error) {
return false, nil
}

func (m *mockContract) Claim(context.Context, redistribution.ChunkInclusionProofs) (common.Hash, error) {
func (m *mockContract) Claim(context.Context, redistribution.ChunkInclusionProofs, *redistribution.ClaimOpts) (common.Hash, error) {
m.mtx.Lock()
defer m.mtx.Unlock()
m.callsList = append(m.callsList, claimCall)
Expand Down
9 changes: 9 additions & 0 deletions pkg/storageincentives/metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ type metrics struct {
ErrClaim prometheus.Counter
ErrWinner prometheus.Counter
ErrCheckIsPlaying prometheus.Counter

// cost control metrics
SkippedExpensivePhase prometheus.Counter
}

func newMetrics() metrics {
Expand Down Expand Up @@ -137,6 +140,12 @@ func newMetrics() metrics {
Name: "is_playing_errors",
Help: "total neighborhood selected errors while processing",
}),
SkippedExpensivePhase: prometheus.NewCounter(prometheus.CounterOpts{
Namespace: m.Namespace,
Subsystem: subsystem,
Name: "skipped_expensive_phase",
Help: "Count of phases skipped because estimated tx cost exceeded configured limit.",
}),
}
}

Expand Down
Loading
Loading