Summary
The LLMOracleCoordinator's request
function accepts unbounded byte arrays for input and model data, storing them directly in contract storage. Without size limits, this can consume excessive gas due to storage operations and memory expansion, potentially reaching block gas limits. Large requests could use up to 50% of a block's gas limit (>15M gas), enabling denial of service attacks and network congestion. Additionally, storing large data on-chain makes oracle response processing expensive due to storage read costs.
The request
function accepts unbounded byte arrays for input and model data, which when combined with state changes and mapped storage, can lead to potential block gas limit issues.
Let's analyze the gas-critical operations in the request function:
https://github.com/Cyfrin/2024-10-swan-dria/blob/main/contracts/llm/LLMOracleCoordinator.sol#L150
function request(
bytes32 protocol,
bytes memory input,
bytes memory models,
LLMOracleTaskParameters calldata parameters
) public onlyValidParameters(parameters) returns (uint256) {
uint256 taskId = nextTaskId;
unchecked {
++nextTaskId;
}
requests[taskId] = TaskRequest({
requester: msg.sender,
protocol: protocol,
input: input,
parameters: parameters,
status: TaskStatus.PendingGeneration,
generatorFee: generatorFee,
validatorFee: validatorFee,
platformFee: platformFee,
models: models
});
emit Request(taskId, msg.sender, protocol);
emit StatusUpdate(taskId, protocol, TaskStatus.None, TaskStatus.PendingGeneration);
return taskId;
}
Gas Consumption Analysis
Let's break down the gas costs:
-
Storage Costs:
-
Memory Expansion:
memory_cost = 3 * words + (words * words) / 512
-
Event Emission:
emit Request(taskId, msg.sender, protocol);
Problematic Scenario
bytes memory largeInput = new bytes(1_000_000);
bytes memory largeModels = new bytes(500_000);
coordinator.request(
"protocol",
largeInput,
largeModels,
parameters
);
Current Ethereum block gas limit: ~30M gas
Potential gas consumption of a large request: >15M gas (50% of block)
Impact Examples
Network Congestion:
for(uint i = 0; i < 3; i++) {
coordinator.request(
"protocol",
largeInput,
largeModels,
parameters
);
}
Oracle Processing:
function respond(uint256 taskId, uint256 nonce, bytes calldata output, bytes calldata metadata)
public
onlyRegistered(LLMOracleKind.Generator)
{
TaskRequest storage task = requests[taskId];
}
Proposed Fix
contract LLMOracleCoordinator {
uint256 public constant MAX_INPUT_SIZE = 128 * 1024;
uint256 public constant MAX_MODELS_SIZE = 32 * 1024;
function request(
bytes32 protocol,
bytes memory input,
bytes memory models,
LLMOracleTaskParameters calldata parameters
) public onlyValidParameters(parameters) returns (uint256) {
require(input.length <= MAX_INPUT_SIZE, "Input too large");
require(models.length <= MAX_MODELS_SIZE, "Models too large");
uint256 estimatedGas =
21000 +
(input.length / 32) * 20000 +
(models.length / 32) * 20000 +
50000;
require(estimatedGas <= block.gaslimit / 3, "Request too large");
}
}
Gas-Optimized Alternative Architecture
contract LLMOracleCoordinator {
struct TaskRequest {
address requester;
bytes32 protocol;
bytes32 inputHash;
bytes32 modelsHash;
LLMOracleTaskParameters parameters;
TaskStatus status;
uint256 generatorFee;
uint256 validatorFee;
uint256 platformFee;
}
function request(
bytes32 protocol,
bytes32 inputHash,
bytes32 modelsHash,
LLMOracleTaskParameters calldata parameters
) public returns (uint256) {
requests[taskId] = TaskRequest({
inputHash: inputHash,
modelsHash: modelsHash,
});
}
}