Gas fee limit for storage incentive transactions
Summary
Add a new setting in the Bee client to let node operators set a maximum gas fee for storage incentive transactions in the redistribution game (commit, reveal, claim phases). It can be an array with a limit for each phase, with default values that make sense.
Motivation
Node operators in the Swarm network join the redistribution game to earn rewards, but gas fees can get high and make it too expensive. Setting a gas fee limit for each phase helps operators control costs and avoid surprises, especially when gas prices spike.
Implementation
Add a new setting called max-gas-fee-per-phase to the Bee config file.
Example:
max-gas-fee-per-phase:
- commit: 20000000000 # 20 Gwei for commit
- reveal: 20000000000 # 20 Gwei for reveal
- claim: 30000000000 # 30 Gwei for claim
Drawbacks
If gas fees go above the limit, operators might miss out on rewards because transactions won’t go through.
I think it would be useful on other blockchain transactions as well such as purchasing postage batches. I'm unsure about how important is to have different limits on phases, I would just use one value for all. additionally, retry mechanism could be added when the transaction fails because of hitting the limitation.
For the purchase of postage batches, there is a Gas-Price header, which allows user to specify the value. But it doeesn't represent limit. So, I agree, we can use it there as well, or maybe on all transactions together?
Regarding retry mechanism, for redistribution game, we could retry until phase ends?
I am ok with multiple values, lets say users could go more heavy on Claim, or at least until it makes sense in $. As long as there is sane defaults so users can have both, simple and advanced if they want to thinker.
Definitely for having at least one default limit so users never pay above value that they could gain from claim.
@0xCardiE how can we calculate the possible reward before the claim phase? Maybe we shouldn't play the game if the possibile transaction expanses can be higher then reward?
3 ways you can get this from postageStamp contract
Last one would be most precise. Some code here to get it with JS
class AccuratePotCalculator {
constructor(postageStampContract, bzzTokenContract) {
this.postageStamp = postageStampContract;
this.bzzToken = bzzTokenContract;
this.cachedBatchIds = null;
}
async getCachedBatchIds() {
if (!this.cachedBatchIds) {
const filter = this.postageStamp.filters.BatchCreated();
const events = await this.postageStamp.queryFilter(filter, 0);
this.cachedBatchIds = events.map(event => event.args.batchId);
}
return this.cachedBatchIds;
}
async getAccuratePot() {
const [
storedPot,
lastExpiryBalance,
currentTotalOutPayment,
validChunkCount,
contractBalance,
batchIds
] = await Promise.all([
this.postageStamp.pot(),
this.postageStamp.lastExpiryBalance(),
this.postageStamp.currentTotalOutPayment(),
this.postageStamp.validChunkCount(),
this.bzzToken.balanceOf(this.postageStamp.address),
this.getCachedBatchIds()
]);
let newlyExpiredFunds = 0;
// Batch process for efficiency
const batchPromises = batchIds.map(id => this.postageStamp.batches(id));
const batches = await Promise.all(batchPromises);
for (const batch of batches) {
if (batch.owner === "0x0000000000000000000000000000000000000000") continue;
if (batch.normalisedBalance <= currentTotalOutPayment &&
batch.normalisedBalance > lastExpiryBalance) {
const batchSize = 2 ** batch.depth;
newlyExpiredFunds += batchSize * (batch.normalisedBalance - lastExpiryBalance);
}
}
const ongoingPayments = validChunkCount * (currentTotalOutPayment - lastExpiryBalance);
const accuratePot = storedPot + newlyExpiredFunds + ongoingPayments;
return Math.min(accuratePot, contractBalance);
}
}
// Usage
const calculator = new AccuratePotCalculator(postageStampContract, bzzTokenContract);
const accuratePot = await calculator.getAccuratePot();
console.log(`Accurate pot: ${accuratePot} BZZ`);
Tried with AI to get golang code, dont know if it works, you check it :)
package main
import (
"context"
"fmt"
"log"
"math/big"
"sync"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/ethclient"
)
// BatchCreatedEvent represents the BatchCreated event structure
type BatchCreatedEvent struct {
BatchId [32]byte
TotalAmount *big.Int
NormalisedBalance *big.Int
Owner common.Address
Depth uint8
BucketDepth uint8
ImmutableFlag bool
}
// Batch represents the batch struct from the contract
type Batch struct {
Owner common.Address
Depth uint8
BucketDepth uint8
ImmutableFlag bool
NormalisedBalance *big.Int
LastUpdatedBlockNumber *big.Int
}
// AccuratePotCalculator handles pot calculations
type AccuratePotCalculator struct {
client *ethclient.Client
postageStamp *PostageStampContract
bzzToken *ERC20Contract
postageStampAddr common.Address
bzzTokenAddr common.Address
cachedBatchIds [][32]byte
mu sync.RWMutex
}
// PostageStampContract interface for the PostageStamp contract
type PostageStampContract struct {
contract *bind.BoundContract
address common.Address
}
// ERC20Contract interface for the BZZ token contract
type ERC20Contract struct {
contract *bind.BoundContract
}
// NewAccuratePotCalculator creates a new calculator instance
func NewAccuratePotCalculator(
client *ethclient.Client,
postageStampAddr, bzzTokenAddr common.Address,
) (*AccuratePotCalculator, error) {
// Parse the contract ABIs (you'll need to include the actual ABI JSON)
postageStampABI, err := abi.JSON(strings.NewReader(PostageStampABI))
if err != nil {
return nil, fmt.Errorf("failed to parse PostageStamp ABI: %w", err)
}
bzzTokenABI, err := abi.JSON(strings.NewReader(ERC20ABI))
if err != nil {
return nil, fmt.Errorf("failed to parse ERC20 ABI: %w", err)
}
postageStampContract := &PostageStampContract{
contract: bind.NewBoundContract(postageStampAddr, postageStampABI, client, client, client),
address: postageStampAddr,
}
bzzTokenContract := &ERC20Contract{
contract: bind.NewBoundContract(bzzTokenAddr, bzzTokenABI, client, client, client),
}
return &AccuratePotCalculator{
client: client,
postageStamp: postageStampContract,
bzzToken: bzzTokenContract,
postageStampAddr: postageStampAddr,
bzzTokenAddr: bzzTokenAddr,
}, nil
}
// GetCachedBatchIds retrieves and caches all batch IDs from events
func (calc *AccuratePotCalculator) GetCachedBatchIds(ctx context.Context) ([][32]byte, error) {
calc.mu.RLock()
if calc.cachedBatchIds != nil {
defer calc.mu.RUnlock()
return calc.cachedBatchIds, nil
}
calc.mu.RUnlock()
calc.mu.Lock()
defer calc.mu.Unlock()
// Double-check after acquiring write lock
if calc.cachedBatchIds != nil {
return calc.cachedBatchIds, nil
}
// Query BatchCreated events from contract deployment
query := ethereum.FilterQuery{
FromBlock: big.NewInt(0), // From contract deployment
ToBlock: nil, // Latest block
Addresses: []common.Address{calc.postageStampAddr},
Topics: [][]common.Hash{
{common.HexToHash("0x...")}, // BatchCreated event signature hash
},
}
logs, err := calc.client.FilterLogs(ctx, query)
if err != nil {
return nil, fmt.Errorf("failed to fetch BatchCreated events: %w", err)
}
var batchIds [][32]byte
for _, vLog := range logs {
// Parse the event data
event := &BatchCreatedEvent{}
err := calc.postageStamp.contract.UnpackLog(event, "BatchCreated", vLog)
if err != nil {
log.Printf("Failed to unpack log: %v", err)
continue
}
batchIds = append(batchIds, event.BatchId)
}
calc.cachedBatchIds = batchIds
return batchIds, nil
}
// GetAccuratePot calculates the accurate pot value
func (calc *AccuratePotCalculator) GetAccuratePot(ctx context.Context) (*big.Int, error) {
// Use goroutines to fetch data concurrently
type result struct {
storedPot *big.Int
lastExpiryBalance *big.Int
currentTotalOutPayment *big.Int
validChunkCount *big.Int
contractBalance *big.Int
batchIds [][32]byte
err error
}
resultChan := make(chan result, 1)
go func() {
var r result
var wg sync.WaitGroup
var mu sync.Mutex
// Fetch all required data concurrently
wg.Add(6)
// Get stored pot
go func() {
defer wg.Done()
pot, err := calc.GetPot(ctx)
mu.Lock()
if err != nil && r.err == nil {
r.err = fmt.Errorf("failed to get pot: %w", err)
}
r.storedPot = pot
mu.Unlock()
}()
// Get last expiry balance
go func() {
defer wg.Done()
balance, err := calc.GetLastExpiryBalance(ctx)
mu.Lock()
if err != nil && r.err == nil {
r.err = fmt.Errorf("failed to get last expiry balance: %w", err)
}
r.lastExpiryBalance = balance
mu.Unlock()
}()
// Get current total out payment
go func() {
defer wg.Done()
payment, err := calc.GetCurrentTotalOutPayment(ctx)
mu.Lock()
if err != nil && r.err == nil {
r.err = fmt.Errorf("failed to get current total out payment: %w", err)
}
r.currentTotalOutPayment = payment
mu.Unlock()
}()
// Get valid chunk count
go func() {
defer wg.Done()
count, err := calc.GetValidChunkCount(ctx)
mu.Lock()
if err != nil && r.err == nil {
r.err = fmt.Errorf("failed to get valid chunk count: %w", err)
}
r.validChunkCount = count
mu.Unlock()
}()
// Get contract balance
go func() {
defer wg.Done()
balance, err := calc.GetBzzBalance(ctx, calc.postageStampAddr)
mu.Lock()
if err != nil && r.err == nil {
r.err = fmt.Errorf("failed to get contract balance: %w", err)
}
r.contractBalance = balance
mu.Unlock()
}()
// Get cached batch IDs
go func() {
defer wg.Done()
ids, err := calc.GetCachedBatchIds(ctx)
mu.Lock()
if err != nil && r.err == nil {
r.err = fmt.Errorf("failed to get batch IDs: %w", err)
}
r.batchIds = ids
mu.Unlock()
}()
wg.Wait()
resultChan <- r
}()
r := <-resultChan
if r.err != nil {
return nil, r.err
}
// Calculate newly expired funds
newlyExpiredFunds, err := calc.calculateNewlyExpiredFunds(
ctx,
r.batchIds,
r.currentTotalOutPayment,
r.lastExpiryBalance,
)
if err != nil {
return nil, fmt.Errorf("failed to calculate newly expired funds: %w", err)
}
// Calculate ongoing payments
ongoingPayments := new(big.Int).Mul(
r.validChunkCount,
new(big.Int).Sub(r.currentTotalOutPayment, r.lastExpiryBalance),
)
// Calculate accurate pot
accuratePot := new(big.Int).Add(r.storedPot, newlyExpiredFunds)
accuratePot.Add(accuratePot, ongoingPayments)
// Cap at contract balance
if accuratePot.Cmp(r.contractBalance) > 0 {
return r.contractBalance, nil
}
return accuratePot, nil
}
// calculateNewlyExpiredFunds calculates funds from newly expired batches
func (calc *AccuratePotCalculator) calculateNewlyExpiredFunds(
ctx context.Context,
batchIds [][32]byte,
currentTotalOutPayment, lastExpiryBalance *big.Int,
) (*big.Int, error) {
newlyExpiredFunds := big.NewInt(0)
// Process batches concurrently
const maxConcurrent = 10
semaphore := make(chan struct{}, maxConcurrent)
var wg sync.WaitGroup
var mu sync.Mutex
var processErr error
for _, batchId := range batchIds {
if processErr != nil {
break
}
wg.Add(1)
go func(id [32]byte) {
defer wg.Done()
semaphore <- struct{}{} // Acquire
defer func() { <-semaphore }() // Release
batch, err := calc.GetBatch(ctx, id)
if err != nil {
mu.Lock()
if processErr == nil {
processErr = fmt.Errorf("failed to get batch %x: %w", id, err)
}
mu.Unlock()
return
}
// Skip deleted batches (owner is zero address)
if batch.Owner == (common.Address{}) {
return
}
// Check if batch expired since last expiry
if batch.NormalisedBalance.Cmp(currentTotalOutPayment) <= 0 &&
batch.NormalisedBalance.Cmp(lastExpiryBalance) > 0 {
// Calculate batchSize = 2^depth
batchSize := new(big.Int).Exp(big.NewInt(2), big.NewInt(int64(batch.Depth)), nil)
// Calculate expired funds for this batch
expiredAmount := new(big.Int).Sub(batch.NormalisedBalance, lastExpiryBalance)
expiredFunds := new(big.Int).Mul(batchSize, expiredAmount)
mu.Lock()
newlyExpiredFunds.Add(newlyExpiredFunds, expiredFunds)
mu.Unlock()
}
}(batchId)
}
wg.Wait()
if processErr != nil {
return nil, processErr
}
return newlyExpiredFunds, nil
}
// Contract interaction methods
func (calc *AccuratePotCalculator) GetPot(ctx context.Context) (*big.Int, error) {
var result []*big.Int
err := calc.postageStamp.contract.Call(&bind.CallOpts{Context: ctx}, &result, "pot")
if err != nil {
return nil, err
}
return result[0], nil
}
func (calc *AccuratePotCalculator) GetLastExpiryBalance(ctx context.Context) (*big.Int, error) {
var result []*big.Int
err := calc.postageStamp.contract.Call(&bind.CallOpts{Context: ctx}, &result, "lastExpiryBalance")
if err != nil {
return nil, err
}
return result[0], nil
}
func (calc *AccuratePotCalculator) GetCurrentTotalOutPayment(ctx context.Context) (*big.Int, error) {
var result []*big.Int
err := calc.postageStamp.contract.Call(&bind.CallOpts{Context: ctx}, &result, "currentTotalOutPayment")
if err != nil {
return nil, err
}
return result[0], nil
}
func (calc *AccuratePotCalculator) GetValidChunkCount(ctx context.Context) (*big.Int, error) {
var result []*big.Int
err := calc.postageStamp.contract.Call(&bind.CallOpts{Context: ctx}, &result, "validChunkCount")
if err != nil {
return nil, err
}
return result[0], nil
}
func (calc *AccuratePotCalculator) GetBatch(ctx context.Context, batchId [32]byte) (*Batch, error) {
var result []interface{}
err := calc.postageStamp.contract.Call(&bind.CallOpts{Context: ctx}, &result, "batches", batchId)
if err != nil {
return nil, err
}
return &Batch{
Owner: result[0].(common.Address),
Depth: result[1].(uint8),
BucketDepth: result[2].(uint8),
ImmutableFlag: result[3].(bool),
NormalisedBalance: result[4].(*big.Int),
LastUpdatedBlockNumber: result[5].(*big.Int),
}, nil
}
func (calc *AccuratePotCalculator) GetBzzBalance(ctx context.Context, address common.Address) (*big.Int, error) {
var result []*big.Int
err := calc.bzzToken.contract.Call(&bind.CallOpts{Context: ctx}, &result, "balanceOf", address)
if err != nil {
return nil, err
}
return result[0], nil
}
// Usage example
func main() {
// Connect to Ethereum client
client, err := ethclient.Dial("https://mainnet.infura.io/v3/YOUR_PROJECT_ID")
if err != nil {
log.Fatal(err)
}
defer client.Close()
// Contract addresses (replace with actual addresses)
postageStampAddr := common.HexToAddress("0x...")
bzzTokenAddr := common.HexToAddress("0x...")
// Create calculator
calculator, err := NewAccuratePotCalculator(client, postageStampAddr, bzzTokenAddr)
if err != nil {
log.Fatal(err)
}
// Calculate accurate pot
ctx := context.Background()
accuratePot, err := calculator.GetAccuratePot(ctx)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Accurate pot: %s BZZ\n", accuratePot.String())
}
// You'll need to include the actual contract ABIs
const PostageStampABI = `[...ABI JSON...]`
const ERC20ABI = `[{"constant":true,"inputs":[{"name":"_owner","type":"address"}],"name":"balanceOf","outputs":[{"name":"balance","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"}]`