bee icon indicating copy to clipboard operation
bee copied to clipboard

Gas fee limit for storage incentive transactions

Open gacevicljubisa opened this issue 6 months ago • 6 comments

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.

gacevicljubisa avatar Jul 02 '25 12:07 gacevicljubisa

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.

nugaon avatar Jul 02 '25 14:07 nugaon

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?

gacevicljubisa avatar Jul 02 '25 14:07 gacevicljubisa

Regarding retry mechanism, for redistribution game, we could retry until phase ends?

gacevicljubisa avatar Jul 02 '25 14:07 gacevicljubisa

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 avatar Jul 02 '25 17:07 0xCardiE

@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?

gacevicljubisa avatar Jul 04 '25 15:07 gacevicljubisa

3 ways you can get this from postageStamp contract

Image

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"}]`

0xCardiE avatar Jul 04 '25 18:07 0xCardiE