GardenBlockchain

Paginate contract reads before the collection grows

A Solidity 0.8.3 experiment showing why a view call can still exceed execution or response limits.

Returning an entire on-chain array becomes expensive as the array grows. The old experiment compared an unrestricted getter with a page described by an offset and limit.

An off-chain eth_call does not charge the caller an on-chain transaction fee, but the node still executes the call and applies resource limits. Memory allocation and response size also grow with the result. The recorded failure was:

call to TestPlayerRead.getPlayers
call to TestPlayerRead.getPlayers errored: Error: Internal JSON-RPC error. { "code": -32000, "message": "out of gas" }

The historical contract

// SPDX-License-Identifier: MIT
pragma solidity 0.8.3;

contract TestPlayerRead {

    struct Player {
        uint id;
    }

    Player[] public playerList;
    mapping(uint => Player) public playerMap;

    constructor () {}

    function addPlayers(uint more) public {
        uint currentSize = getPlayerSize();
        for (uint i = currentSize; i < currentSize + more; i++) {
            uint id = i + 1;
            Player memory player = Player(id);
            playerList.push(player);
            playerMap[id] = player;
        }
    }
    
    function getPlayerSize() public view returns (uint length) {
        return playerList.length;
    }
    
    function getPlayers() public view returns (Player[] memory players) {
        return playerList;
    }
    
    function getPlayersPaging(uint offset, uint limit) public view returns (Player[] memory players, uint nextOffset, uint total) {
        uint totalPlayers = playerList.length;
        if(limit == 0) {
            limit = 1;
        }
        
        if (limit > totalPlayers- offset) {
            limit = totalPlayers - offset;
        }

        Player[] memory values = new Player[] (limit);
        for (uint i = 0; i < limit; i++) {
            values[i] = playerList[offset + i];
        }

        return (values, offset + limit, totalPlayers);
    }
}

The page getter returns the values, next offset, and total count. However, totalPlayers - offset reverts under Solidity 0.8.3 checked arithmetic when offset exceeds the total. This source is preserved with that defect visible; it is not a corrected pagination library. A zero limit is also converted to one rather than producing an empty page.

Bound the work, then define consistency

A caller should not be able to turn a page into an unbounded allocation. The meaning of an offset also depends on whether entries can be inserted, deleted, or reordered between calls. For a stable read, block selection and collection behavior matter alongside page size.

The original notebook observed trouble around 5,600 records, smaller reads around 1,600, and write batches of 500. Those values depend on the contract, node, chain, and settings. Its dated gas-price calculation and universal gas-limit claim are omitted. No contract was compiled or called for this revision.

← Back to the garden