Skip to content

Commit f922acb

Browse files
feat(pulse): optimize gas while keeping requests on-chain (#2519)
* feat: gas optimizations * fix: remove unnecessary concrete impl * add benchmarking for variable feeds * test: add out of order fulfillment benchmark * update benchmark tests * fix test
1 parent a1ad7f3 commit f922acb

File tree

10 files changed

+452
-207
lines changed

10 files changed

+452
-207
lines changed

target_chains/ethereum/contracts/contracts/pulse/IPulse.sol

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ interface IPulse is PulseEvents {
5555
address provider,
5656
uint64 publishTime,
5757
bytes32[] calldata priceIds,
58-
uint256 callbackGasLimit
58+
uint32 callbackGasLimit
5959
) external payable returns (uint64 sequenceNumber);
6060

6161
/**
@@ -80,7 +80,7 @@ interface IPulse is PulseEvents {
8080
* @dev This is a fixed fee per request that goes to the Pyth protocol, separate from gas costs
8181
* @return pythFeeInWei The base fee in wei that every request must pay
8282
*/
83-
function getPythFeeInWei() external view returns (uint128 pythFeeInWei);
83+
function getPythFeeInWei() external view returns (uint96 pythFeeInWei);
8484

8585
/**
8686
* @notice Calculates the total fee required for a price update request
@@ -92,9 +92,9 @@ interface IPulse is PulseEvents {
9292
*/
9393
function getFee(
9494
address provider,
95-
uint256 callbackGasLimit,
95+
uint32 callbackGasLimit,
9696
bytes32[] calldata priceIds
97-
) external view returns (uint128 feeAmount);
97+
) external view returns (uint96 feeAmount);
9898

9999
function getAccruedPythFees()
100100
external
@@ -116,16 +116,16 @@ interface IPulse is PulseEvents {
116116
function withdrawAsFeeManager(address provider, uint128 amount) external;
117117

118118
function registerProvider(
119-
uint128 baseFeeInWei,
120-
uint128 feePerFeedInWei,
121-
uint128 feePerGasInWei
119+
uint96 baseFeeInWei,
120+
uint96 feePerFeedInWei,
121+
uint96 feePerGasInWei
122122
) external;
123123

124124
function setProviderFee(
125125
address provider,
126-
uint128 newBaseFeeInWei,
127-
uint128 newFeePerFeedInWei,
128-
uint128 newFeePerGasInWei
126+
uint96 newBaseFeeInWei,
127+
uint96 newFeePerFeedInWei,
128+
uint96 newFeePerGasInWei
129129
) external;
130130

131131
function getProviderInfo(
@@ -136,9 +136,9 @@ interface IPulse is PulseEvents {
136136

137137
function setDefaultProvider(address provider) external;
138138

139-
function setExclusivityPeriod(uint256 periodSeconds) external;
139+
function setExclusivityPeriod(uint32 periodSeconds) external;
140140

141-
function getExclusivityPeriod() external view returns (uint256);
141+
function getExclusivityPeriod() external view returns (uint32);
142142

143143
/**
144144
* @notice Gets the first N active requests

target_chains/ethereum/contracts/contracts/pulse/Pulse.sol

Lines changed: 48 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,11 @@ import "./PulseErrors.sol";
1111
abstract contract Pulse is IPulse, PulseState {
1212
function _initialize(
1313
address admin,
14-
uint128 pythFeeInWei,
14+
uint96 pythFeeInWei,
1515
address pythAddress,
1616
address defaultProvider,
1717
bool prefillRequestStorage,
18-
uint256 exclusivityPeriodSeconds
18+
uint32 exclusivityPeriodSeconds
1919
) internal {
2020
require(admin != address(0), "admin is zero address");
2121
require(pythAddress != address(0), "pyth is zero address");
@@ -44,11 +44,6 @@ abstract contract Pulse is IPulse, PulseState {
4444
req.publishTime = 1;
4545
req.callbackGasLimit = 1;
4646
req.requester = address(1);
47-
req.numPriceIds = 0;
48-
// Pre-warm the priceIds array storage
49-
for (uint8 j = 0; j < MAX_PRICE_IDS; j++) {
50-
req.priceIds[j] = bytes32(0);
51-
}
5247
}
5348
}
5449
}
@@ -58,7 +53,7 @@ abstract contract Pulse is IPulse, PulseState {
5853
address provider,
5954
uint64 publishTime,
6055
bytes32[] calldata priceIds,
61-
uint256 callbackGasLimit
56+
uint32 callbackGasLimit
6257
) external payable override returns (uint64 requestSequenceNumber) {
6358
require(
6459
_state.providers[provider].isRegistered,
@@ -77,21 +72,29 @@ abstract contract Pulse is IPulse, PulseState {
7772
}
7873
requestSequenceNumber = _state.currentSequenceNumber++;
7974

80-
uint128 requiredFee = getFee(provider, callbackGasLimit, priceIds);
75+
uint96 requiredFee = getFee(provider, callbackGasLimit, priceIds);
8176
if (msg.value < requiredFee) revert InsufficientFee();
8277

8378
Request storage req = allocRequest(requestSequenceNumber);
8479
req.sequenceNumber = requestSequenceNumber;
8580
req.publishTime = publishTime;
8681
req.callbackGasLimit = callbackGasLimit;
8782
req.requester = msg.sender;
88-
req.numPriceIds = uint8(priceIds.length);
8983
req.provider = provider;
90-
req.fee = SafeCast.toUint128(msg.value - _state.pythFeeInWei);
84+
req.fee = SafeCast.toUint96(msg.value - _state.pythFeeInWei);
9185

92-
// Copy price IDs to storage
86+
// Create array with the right size
87+
req.priceIdPrefixes = new bytes8[](priceIds.length);
88+
89+
// Copy only the first 8 bytes of each price ID to storage
9390
for (uint8 i = 0; i < priceIds.length; i++) {
94-
req.priceIds[i] = priceIds[i];
91+
// Extract first 8 bytes of the price ID
92+
bytes32 priceId = priceIds[i];
93+
bytes8 prefix;
94+
assembly {
95+
prefix := priceId
96+
}
97+
req.priceIdPrefixes[i] = prefix;
9598
}
9699
_state.accruedFeesInWei += _state.pythFeeInWei;
97100

@@ -119,12 +122,21 @@ abstract contract Pulse is IPulse, PulseState {
119122

120123
// Verify priceIds match
121124
require(
122-
priceIds.length == req.numPriceIds,
125+
priceIds.length == req.priceIdPrefixes.length,
123126
"Price IDs length mismatch"
124127
);
125-
for (uint8 i = 0; i < req.numPriceIds; i++) {
126-
if (priceIds[i] != req.priceIds[i]) {
127-
revert InvalidPriceIds(priceIds[i], req.priceIds[i]);
128+
for (uint8 i = 0; i < req.priceIdPrefixes.length; i++) {
129+
// Extract first 8 bytes of the provided price ID
130+
bytes32 priceId = priceIds[i];
131+
bytes8 prefix;
132+
assembly {
133+
prefix := priceId
134+
}
135+
136+
// Compare with stored prefix
137+
if (prefix != req.priceIdPrefixes[i]) {
138+
// Now we can directly use the bytes8 prefix in the error
139+
revert InvalidPriceIds(priceIds[i], req.priceIdPrefixes[i]);
128140
}
129141
}
130142

@@ -222,31 +234,31 @@ abstract contract Pulse is IPulse, PulseState {
222234

223235
function getFee(
224236
address provider,
225-
uint256 callbackGasLimit,
237+
uint32 callbackGasLimit,
226238
bytes32[] calldata priceIds
227-
) public view override returns (uint128 feeAmount) {
228-
uint128 baseFee = _state.pythFeeInWei; // Fixed fee to Pyth
239+
) public view override returns (uint96 feeAmount) {
240+
uint96 baseFee = _state.pythFeeInWei; // Fixed fee to Pyth
229241
// Note: The provider needs to set its fees to include the fee charged by the Pyth contract.
230242
// Ideally, we would be able to automatically compute the pyth fees from the priceIds, but the
231243
// fee computation on IPyth assumes it has the full updated data.
232-
uint128 providerBaseFee = _state.providers[provider].baseFeeInWei;
233-
uint128 providerFeedFee = SafeCast.toUint128(
244+
uint96 providerBaseFee = _state.providers[provider].baseFeeInWei;
245+
uint96 providerFeedFee = SafeCast.toUint96(
234246
priceIds.length * _state.providers[provider].feePerFeedInWei
235247
);
236-
uint128 providerFeeInWei = _state.providers[provider].feePerGasInWei; // Provider's per-gas rate
248+
uint96 providerFeeInWei = _state.providers[provider].feePerGasInWei; // Provider's per-gas rate
237249
uint256 gasFee = callbackGasLimit * providerFeeInWei; // Total provider fee based on gas
238250
feeAmount =
239251
baseFee +
240252
providerBaseFee +
241253
providerFeedFee +
242-
SafeCast.toUint128(gasFee); // Total fee user needs to pay
254+
SafeCast.toUint96(gasFee); // Total fee user needs to pay
243255
}
244256

245257
function getPythFeeInWei()
246258
public
247259
view
248260
override
249-
returns (uint128 pythFeeInWei)
261+
returns (uint96 pythFeeInWei)
250262
{
251263
pythFeeInWei = _state.pythFeeInWei;
252264
}
@@ -367,9 +379,9 @@ abstract contract Pulse is IPulse, PulseState {
367379
}
368380

369381
function registerProvider(
370-
uint128 baseFeeInWei,
371-
uint128 feePerFeedInWei,
372-
uint128 feePerGasInWei
382+
uint96 baseFeeInWei,
383+
uint96 feePerFeedInWei,
384+
uint96 feePerGasInWei
373385
) external override {
374386
ProviderInfo storage provider = _state.providers[msg.sender];
375387
require(!provider.isRegistered, "Provider already registered");
@@ -382,9 +394,9 @@ abstract contract Pulse is IPulse, PulseState {
382394

383395
function setProviderFee(
384396
address provider,
385-
uint128 newBaseFeeInWei,
386-
uint128 newFeePerFeedInWei,
387-
uint128 newFeePerGasInWei
397+
uint96 newBaseFeeInWei,
398+
uint96 newFeePerFeedInWei,
399+
uint96 newFeePerGasInWei
388400
) external override {
389401
require(
390402
_state.providers[provider].isRegistered,
@@ -396,9 +408,9 @@ abstract contract Pulse is IPulse, PulseState {
396408
"Only provider or fee manager can invoke this method"
397409
);
398410

399-
uint128 oldBaseFee = _state.providers[provider].baseFeeInWei;
400-
uint128 oldFeePerFeed = _state.providers[provider].feePerFeedInWei;
401-
uint128 oldFeePerGas = _state.providers[provider].feePerGasInWei;
411+
uint96 oldBaseFee = _state.providers[provider].baseFeeInWei;
412+
uint96 oldFeePerFeed = _state.providers[provider].feePerFeedInWei;
413+
uint96 oldFeePerGas = _state.providers[provider].feePerGasInWei;
402414
_state.providers[provider].baseFeeInWei = newBaseFeeInWei;
403415
_state.providers[provider].feePerFeedInWei = newFeePerFeedInWei;
404416
_state.providers[provider].feePerGasInWei = newFeePerGasInWei;
@@ -437,7 +449,7 @@ abstract contract Pulse is IPulse, PulseState {
437449
emit DefaultProviderUpdated(oldProvider, provider);
438450
}
439451

440-
function setExclusivityPeriod(uint256 periodSeconds) external override {
452+
function setExclusivityPeriod(uint32 periodSeconds) external override {
441453
require(
442454
msg.sender == _state.admin,
443455
"Only admin can set exclusivity period"
@@ -447,7 +459,7 @@ abstract contract Pulse is IPulse, PulseState {
447459
emit ExclusivityPeriodUpdated(oldPeriod, periodSeconds);
448460
}
449461

450-
function getExclusivityPeriod() external view override returns (uint256) {
462+
function getExclusivityPeriod() external view override returns (uint32) {
451463
return _state.exclusivityPeriodSeconds;
452464
}
453465

target_chains/ethereum/contracts/contracts/pulse/PulseErrors.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ error InsufficientFee();
99
error Unauthorized();
1010
error InvalidCallbackGas();
1111
error CallbackFailed();
12-
error InvalidPriceIds(bytes32 providedPriceIdsHash, bytes32 storedPriceIdsHash);
12+
error InvalidPriceIds(bytes32 providedPriceId, bytes8 storedPriceId);
1313
error InvalidCallbackGasLimit(uint256 requested, uint256 stored);
1414
error ExceedsMaxPrices(uint32 requested, uint32 maxAllowed);
1515
error TooManyPriceIds(uint256 provided, uint256 maximum);

target_chains/ethereum/contracts/contracts/pulse/PulseEvents.sol

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,15 +32,15 @@ interface PulseEvents {
3232
address newFeeManager
3333
);
3434

35-
event ProviderRegistered(address indexed provider, uint128 feeInWei);
35+
event ProviderRegistered(address indexed provider, uint96 feeInWei);
3636
event ProviderFeeUpdated(
3737
address indexed provider,
38-
uint128 oldBaseFee,
39-
uint128 oldFeePerFeed,
40-
uint128 oldFeePerGas,
41-
uint128 newBaseFee,
42-
uint128 newFeePerFeed,
43-
uint128 newFeePerGas
38+
uint96 oldBaseFee,
39+
uint96 oldFeePerFeed,
40+
uint96 oldFeePerGas,
41+
uint96 newBaseFee,
42+
uint96 newFeePerFeed,
43+
uint96 newFeePerGas
4444
);
4545
event DefaultProviderUpdated(address oldProvider, address newProvider);
4646

target_chains/ethereum/contracts/contracts/pulse/PulseState.sol

Lines changed: 37 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,41 +10,62 @@ contract PulseState {
1010
uint8 public constant MAX_PRICE_IDS = 10;
1111

1212
struct Request {
13+
// Slot 1: 8 + 8 + 4 + 12 = 32 bytes
1314
uint64 sequenceNumber;
1415
uint64 publishTime;
15-
// TODO: this is going to absolutely explode gas costs. Need to do something smarter here.
16-
// possible solution is to hash the price ids and store the hash instead.
17-
// The ids themselves can be retrieved from the event.
18-
bytes32[MAX_PRICE_IDS] priceIds;
19-
uint8 numPriceIds; // Actual number of price IDs used
20-
uint256 callbackGasLimit;
16+
uint32 callbackGasLimit;
17+
uint96 fee;
18+
// Slot 2: 20 + 12 = 32 bytes
2119
address requester;
20+
// 12 bytes padding
21+
22+
// Slot 3: 20 + 12 = 32 bytes
2223
address provider;
23-
uint128 fee;
24+
// 12 bytes padding
25+
26+
// Dynamic array starts at its own slot
27+
// Store only first 8 bytes of each price ID to save gas
28+
bytes8[] priceIdPrefixes;
2429
}
2530

2631
struct ProviderInfo {
27-
uint128 baseFeeInWei;
28-
uint128 feePerFeedInWei;
29-
uint128 feePerGasInWei;
32+
// Slot 1: 12 + 12 + 8 = 32 bytes
33+
uint96 baseFeeInWei;
34+
uint96 feePerFeedInWei;
35+
// 8 bytes padding
36+
37+
// Slot 2: 12 + 16 + 4 = 32 bytes
38+
uint96 feePerGasInWei;
3039
uint128 accruedFeesInWei;
40+
// 4 bytes padding
41+
42+
// Slot 3: 20 + 1 + 11 = 32 bytes
3143
address feeManager;
3244
bool isRegistered;
45+
// 11 bytes padding
3346
}
3447

3548
struct State {
49+
// Slot 1: 20 + 4 + 8 = 32 bytes
3650
address admin;
37-
uint128 pythFeeInWei;
38-
uint128 accruedFeesInWei;
39-
address pyth;
51+
uint32 exclusivityPeriodSeconds;
4052
uint64 currentSequenceNumber;
53+
// Slot 2: 20 + 8 + 4 = 32 bytes
54+
address pyth;
55+
uint64 firstUnfulfilledSeq;
56+
// 4 bytes padding
57+
58+
// Slot 3: 20 + 12 = 32 bytes
4159
address defaultProvider;
42-
uint256 exclusivityPeriodSeconds;
60+
uint96 pythFeeInWei;
61+
// Slot 4: 16 + 16 = 32 bytes
62+
uint128 accruedFeesInWei;
63+
// 16 bytes padding
64+
65+
// These take their own slots regardless of ordering
4366
Request[NUM_REQUESTS] requests;
4467
mapping(bytes32 => Request) requestsOverflow;
4568
mapping(address => ProviderInfo) providers;
46-
uint64 firstUnfulfilledSeq; // All sequences before this are fulfilled
4769
}
48-
4970
State internal _state;
5071
}

target_chains/ethereum/contracts/contracts/pulse/PulseUpgradeable.sol

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,11 @@ contract PulseUpgradeable is
2121
function initialize(
2222
address owner,
2323
address admin,
24-
uint128 pythFeeInWei,
24+
uint96 pythFeeInWei,
2525
address pythAddress,
2626
address defaultProvider,
2727
bool prefillRequestStorage,
28-
uint256 exclusivityPeriodSeconds
28+
uint32 exclusivityPeriodSeconds
2929
) external initializer {
3030
require(owner != address(0), "owner is zero address");
3131
require(admin != address(0), "admin is zero address");

0 commit comments

Comments
 (0)