Skip to content

Commit 177e907

Browse files
authored
feat: compress all bridged ancillary data (#4816)
Signed-off-by: Reinis Martinsons <[email protected]>
1 parent 9dcde67 commit 177e907

37 files changed

+2033
-157
lines changed
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
// SPDX-License-Identifier: AGPL-3.0-only
2+
pragma solidity ^0.8.0;
3+
4+
import { AncillaryData } from "../common/implementation/AncillaryData.sol";
5+
6+
/**
7+
* @title Library for compressing ancillary data when bridging DVM price requests to mainnet.
8+
* @notice This provides internal method for origin chain oracles to compress ancillary data by replacing it with the
9+
* hash of the original ancillary data and adding additional information to track back the original ancillary data on
10+
* mainnet.
11+
*/
12+
library AncillaryDataCompression {
13+
/**
14+
* @notice Compresses ancillary data by providing sufficient information to track back the original ancillary data
15+
* on mainnet.
16+
* @dev The compression replaces original ancillary data with its hash and adds address of origin chain oracle and
17+
* block number so that it is more efficient to fetch original ancillary data from PriceRequestBridged event on
18+
* origin chain indexed by parentRequestId. This parentRequestId can be reconstructed by taking keccak256 hash of
19+
* ABI encoded price identifier, time and compressed ancillary data.
20+
* @param ancillaryData original ancillary data to be processed.
21+
* @param requester address of the requester who initiated the price request.
22+
* @param requestBlockNumber block number when the price request was initiated.
23+
* @return compressed ancillary data.
24+
*/
25+
function compress(
26+
bytes memory ancillaryData,
27+
address requester,
28+
uint256 requestBlockNumber
29+
) internal view returns (bytes memory) {
30+
return
31+
AncillaryData.appendKeyValueUint(
32+
AncillaryData.appendKeyValueAddress(
33+
AncillaryData.appendKeyValueAddress(
34+
AncillaryData.appendKeyValueUint(
35+
AncillaryData.appendKeyValueBytes32("", "ancillaryDataHash", keccak256(ancillaryData)),
36+
"childBlockNumber",
37+
requestBlockNumber
38+
),
39+
"childOracle",
40+
address(this)
41+
),
42+
"childRequester",
43+
requester
44+
),
45+
"childChainId",
46+
block.chainid
47+
);
48+
}
49+
}

packages/core/contracts/cross-chain-oracle/OracleBase.sol

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@ abstract contract OracleBase is HasFinder {
4040
uint256 time,
4141
bytes memory ancillaryData
4242
) internal returns (bool) {
43-
require(ancillaryData.length <= OptimisticOracleConstraints.ancillaryBytesLimit, "Invalid ancillary data");
4443
bytes32 priceRequestId = _encodePriceRequest(identifier, time, ancillaryData);
4544
Price storage lookup = prices[priceRequestId];
4645
if (lookup.state == RequestState.NeverRequested) {

packages/core/contracts/cross-chain-oracle/OracleHub.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ contract OracleHub is OracleBase, ParentMessengerConsumerInterface, Ownable, Loc
126126
* @dev The caller must pay a final fee and have approved this contract to pull final fee from it.
127127
* @dev If the price request params including the ancillary data does not match exactly the price request submitted
128128
* on the child chain, then the child chain's price request will not resolve. The caller is recommended to use the
129-
* `stampAncillaryData` method on the OracleSpoke to reconstruct the ancillary data.
129+
* `compressAncillaryData` method on the OracleSpoke to reconstruct the ancillary data.
130130
* @param identifier Identifier for price request.
131131
* @param time time for price request.
132132
* @param ancillaryData Extra data for price request.

packages/core/contracts/cross-chain-oracle/OracleSpoke.sol

Lines changed: 117 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ pragma solidity ^0.8.0;
44
import "../data-verification-mechanism/interfaces/OracleAncillaryInterface.sol";
55
import "../data-verification-mechanism/interfaces/OracleInterface.sol";
66
import "../data-verification-mechanism/interfaces/RegistryInterface.sol";
7+
import "./AncillaryDataCompression.sol";
78
import "./OracleBase.sol";
89
import "../common/implementation/AncillaryData.sol";
910
import "../common/implementation/Lockable.sol";
@@ -29,6 +30,28 @@ contract OracleSpoke is
2930
ChildMessengerConsumerInterface,
3031
Lockable
3132
{
33+
using AncillaryDataCompression for bytes;
34+
35+
// Mapping of parent request ID to child request ID.
36+
mapping(bytes32 => bytes32) public childRequestIds;
37+
38+
event PriceRequestBridged(
39+
address indexed requester,
40+
bytes32 identifier,
41+
uint256 time,
42+
bytes ancillaryData,
43+
bytes32 indexed childRequestId,
44+
bytes32 indexed parentRequestId
45+
);
46+
event ResolvedLegacyRequest(
47+
bytes32 indexed identifier,
48+
uint256 time,
49+
bytes ancillaryData,
50+
int256 price,
51+
bytes32 indexed requestHash,
52+
bytes32 indexed legacyRequestHash
53+
);
54+
3255
constructor(address _finderAddress) HasFinder(_finderAddress) {}
3356

3457
// This assumes that the local network has a Registry that resembles the mainnet registry.
@@ -55,21 +78,43 @@ contract OracleSpoke is
5578
uint256 time,
5679
bytes memory ancillaryData
5780
) public override nonReentrant() onlyRegisteredContract() {
58-
bool newPriceRequested = _requestPrice(identifier, time, _stampAncillaryData(ancillaryData));
59-
if (newPriceRequested) {
60-
getChildMessenger().sendMessageToParent(abi.encode(identifier, time, _stampAncillaryData(ancillaryData)));
61-
}
81+
_requestPriceSpoke(identifier, time, ancillaryData);
6282
}
6383

6484
/**
6585
* @notice Overloaded function to provide backwards compatibility for legacy financial contracts that do not use
6686
* ancillary data.
6787
*/
6888
function requestPrice(bytes32 identifier, uint256 time) public override nonReentrant() onlyRegisteredContract() {
69-
bool newPriceRequested = _requestPrice(identifier, time, _stampAncillaryData(""));
70-
if (newPriceRequested) {
71-
getChildMessenger().sendMessageToParent(abi.encode(identifier, time, _stampAncillaryData("")));
72-
}
89+
_requestPriceSpoke(identifier, time, "");
90+
}
91+
92+
function _requestPriceSpoke(
93+
bytes32 identifier,
94+
uint256 time,
95+
bytes memory ancillaryData
96+
) internal {
97+
address requester = msg.sender;
98+
bytes32 childRequestId = _encodePriceRequest(identifier, time, ancillaryData);
99+
Price storage lookup = prices[childRequestId];
100+
101+
// Send the request to mainnet only if it has not been requested yet.
102+
if (lookup.state != RequestState.NeverRequested) return;
103+
lookup.state = RequestState.Requested;
104+
105+
// Only the compressed ancillary data is sent to the mainnet. As it includes the request block number that is
106+
// not available when getting the resolved price, we map the derived request ID.
107+
bytes memory parentAncillaryData = ancillaryData.compress(requester, block.number);
108+
bytes32 parentRequestId = _encodePriceRequest(identifier, time, parentAncillaryData);
109+
childRequestIds[parentRequestId] = childRequestId;
110+
111+
// Emit all required information so that voters on mainnet can track the origin of the request and full
112+
// ancillary data by using the parentRequestId that is derived from identifier, time and ancillary data as
113+
// observed on mainnet.
114+
emit PriceRequestBridged(requester, identifier, time, ancillaryData, childRequestId, parentRequestId);
115+
emit PriceRequestAdded(identifier, time, parentAncillaryData, parentRequestId);
116+
117+
getChildMessenger().sendMessageToParent(abi.encode(identifier, time, parentAncillaryData));
73118
}
74119

75120
/**
@@ -81,7 +126,50 @@ contract OracleSpoke is
81126
function processMessageFromParent(bytes memory data) public override nonReentrant() onlyMessenger() {
82127
(bytes32 identifier, uint256 time, bytes memory ancillaryData, int256 price) =
83128
abi.decode(data, (bytes32, uint256, bytes, int256));
84-
_publishPrice(identifier, time, ancillaryData, price);
129+
bytes32 parentRequestId = _encodePriceRequest(identifier, time, ancillaryData);
130+
131+
// Resolve the requestId used when requesting and checking the price. The childRequestIds value in the mapping
132+
// could be uninitialized if the request was originated from:
133+
// - the previous implementation of this contract, or
134+
// - another chain and was pushed to this chain by mistake.
135+
bytes32 priceRequestId = childRequestIds[parentRequestId];
136+
if (priceRequestId == bytes32(0)) priceRequestId = parentRequestId;
137+
Price storage lookup = prices[priceRequestId];
138+
139+
// In order to support resolving the requests initiated from the previous implementation of this contract, we
140+
// only update the state and emit an event if it has not yet been resolved.
141+
if (lookup.state == RequestState.Resolved) return;
142+
lookup.price = price;
143+
lookup.state = RequestState.Resolved;
144+
emit PushedPrice(identifier, time, ancillaryData, price, priceRequestId);
145+
}
146+
147+
/**
148+
* @notice This method handles a special case when a price request was originated on the previous implementation of
149+
* this contract, but was not settled before the upgrade.
150+
* @dev Duplicates the resolved state from the legacy request to the new request where original ancillary data is
151+
* used for request ID derivation. Will revert if the legacy request has not been pushed from mainnet.
152+
* @param identifier Identifier of price request to resolve.
153+
* @param time Timestamp of price request to resolve.
154+
* @param ancillaryData Original ancillary data passed by the requester before stamping by the legacy spoke.
155+
*/
156+
function resolveLegacyRequest(
157+
bytes32 identifier,
158+
uint256 time,
159+
bytes memory ancillaryData
160+
) external {
161+
bytes32 legacyRequestId = _encodePriceRequest(identifier, time, _legacyStampAncillaryData(ancillaryData));
162+
Price storage legacyLookup = prices[legacyRequestId];
163+
require(legacyLookup.state == RequestState.Resolved, "Price has not been resolved");
164+
165+
bytes32 priceRequestId = _encodePriceRequest(identifier, time, ancillaryData);
166+
Price storage lookup = prices[priceRequestId];
167+
168+
// Update the state and emit an event only if the legacy request has not been resolved yet.
169+
if (lookup.state == RequestState.Resolved) return;
170+
lookup.price = legacyLookup.price;
171+
lookup.state = RequestState.Resolved;
172+
emit ResolvedLegacyRequest(identifier, time, ancillaryData, lookup.price, priceRequestId, legacyRequestId);
85173
}
86174

87175
/**
@@ -96,7 +184,7 @@ contract OracleSpoke is
96184
uint256 time,
97185
bytes memory ancillaryData
98186
) public view override nonReentrantView() onlyRegisteredContract() returns (bool) {
99-
bytes32 priceRequestId = _encodePriceRequest(identifier, time, _stampAncillaryData(ancillaryData));
187+
bytes32 priceRequestId = _encodePriceRequest(identifier, time, ancillaryData);
100188
return prices[priceRequestId].state == RequestState.Resolved;
101189
}
102190

@@ -112,7 +200,7 @@ contract OracleSpoke is
112200
onlyRegisteredContract()
113201
returns (bool)
114202
{
115-
bytes32 priceRequestId = _encodePriceRequest(identifier, time, _stampAncillaryData(""));
203+
bytes32 priceRequestId = _encodePriceRequest(identifier, time, "");
116204
return prices[priceRequestId].state == RequestState.Resolved;
117205
}
118206

@@ -128,7 +216,7 @@ contract OracleSpoke is
128216
uint256 time,
129217
bytes memory ancillaryData
130218
) public view override nonReentrantView() onlyRegisteredContract() returns (int256) {
131-
bytes32 priceRequestId = _encodePriceRequest(identifier, time, _stampAncillaryData(ancillaryData));
219+
bytes32 priceRequestId = _encodePriceRequest(identifier, time, ancillaryData);
132220
Price storage lookup = prices[priceRequestId];
133221
require(lookup.state == RequestState.Resolved, "Price has not been resolved");
134222
return lookup.price;
@@ -146,27 +234,34 @@ contract OracleSpoke is
146234
onlyRegisteredContract()
147235
returns (int256)
148236
{
149-
bytes32 priceRequestId = _encodePriceRequest(identifier, time, _stampAncillaryData(""));
237+
bytes32 priceRequestId = _encodePriceRequest(identifier, time, "");
150238
Price storage lookup = prices[priceRequestId];
151239
require(lookup.state == RequestState.Resolved, "Price has not been resolved");
152240
return lookup.price;
153241
}
154242

155243
/**
156-
* @notice Generates stamped ancillary data in the format that it would be used in the case of a price request.
157-
* @param ancillaryData ancillary data of the price being requested.
158-
* @return the stamped ancillary bytes.
244+
* @notice Compresses ancillary data by providing sufficient information to track back the original ancillary data
245+
* mainnet.
246+
* @dev This is expected to be used in offchain infrastructure when speeding up requests to the mainnet.
247+
* @param ancillaryData original ancillary data to be processed.
248+
* @param requester address of the requester who initiated the price request.
249+
* @param requestBlockNumber block number when the price request was initiated.
250+
* @return compressed ancillary data.
159251
*/
160-
function stampAncillaryData(bytes memory ancillaryData) public view nonReentrantView() returns (bytes memory) {
161-
return _stampAncillaryData(ancillaryData);
252+
function compressAncillaryData(
253+
bytes memory ancillaryData,
254+
address requester,
255+
uint256 requestBlockNumber
256+
) external view returns (bytes memory) {
257+
return ancillaryData.compress(requester, requestBlockNumber);
162258
}
163259

164260
/**
165-
* @dev We don't handle specifically the case where `ancillaryData` is not already readily translatable in utf8.
166-
* For those cases, we assume that the client will be able to strip out the utf8-translatable part of the
167-
* ancillary data that this contract stamps.
261+
* @dev This replicates the implementation of `_stampAncillaryData` from the previous version of this contract for
262+
* the purpose of resolving legacy requests if they had not been resolved before the upgrade.
168263
*/
169-
function _stampAncillaryData(bytes memory ancillaryData) internal view returns (bytes memory) {
264+
function _legacyStampAncillaryData(bytes memory ancillaryData) internal view returns (bytes memory) {
170265
// This contract should stamp the child network's ID so that voters on the parent network can
171266
// deterministically track unique price requests back to this contract.
172267
return AncillaryData.appendKeyValueUint(ancillaryData, "childChainId", block.chainid);

packages/core/contracts/polygon-cross-chain-oracle/OracleBaseTunnel.sol

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,6 @@ abstract contract OracleBaseTunnel {
1515
int256 price;
1616
}
1717

18-
// This value must be <= the Voting contract's `ancillaryBytesLimit` value otherwise it is possible
19-
// that a price can be requested to this contract successfully, but cannot be resolved by the DVM which refuses
20-
// to accept a price request made with ancillary data length over a certain size.
21-
uint256 public constant ancillaryBytesLimit = 8192;
22-
2318
// Mapping of encoded price requests {identifier, time, ancillaryData} to Price objects.
2419
mapping(bytes32 => Price) internal prices;
2520

@@ -52,7 +47,6 @@ abstract contract OracleBaseTunnel {
5247
uint256 time,
5348
bytes memory ancillaryData
5449
) internal {
55-
require(ancillaryData.length <= ancillaryBytesLimit, "Invalid ancillary data");
5650
bytes32 priceRequestId = _encodePriceRequest(identifier, time, ancillaryData);
5751
Price storage lookup = prices[priceRequestId];
5852
if (lookup.state == RequestState.NeverRequested) {
@@ -72,11 +66,10 @@ abstract contract OracleBaseTunnel {
7266
) internal {
7367
bytes32 priceRequestId = _encodePriceRequest(identifier, time, ancillaryData);
7468
Price storage lookup = prices[priceRequestId];
75-
if (lookup.state == RequestState.Requested) {
76-
lookup.price = price;
77-
lookup.state = RequestState.Resolved;
78-
emit PushedPrice(identifier, time, ancillaryData, lookup.price, priceRequestId);
79-
}
69+
if (lookup.state == RequestState.Resolved) return;
70+
lookup.price = price;
71+
lookup.state = RequestState.Resolved;
72+
emit PushedPrice(identifier, time, ancillaryData, lookup.price, priceRequestId);
8073
}
8174

8275
/**

0 commit comments

Comments
 (0)