This repository has been archived by the owner on Jun 20, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 15
/
OnDemandSPV.sol
375 lines (338 loc) · 14.6 KB
/
OnDemandSPV.sol
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
pragma solidity ^0.5.10;
/** @title OnDemandSPV */
/** @author Summa (https://summa.one) */
import {Relay} from "./Relay.sol";
import {ISPVRequestManager, ISPVConsumer} from "./Interfaces.sol";
import {TypedMemView} from "@summa-tx/bitcoin-spv-sol/contracts/TypedMemView.sol";
import {ViewBTC} from "@summa-tx/bitcoin-spv-sol/contracts/ViewBTC.sol";
import {ViewSPV} from "@summa-tx/bitcoin-spv-sol/contracts/ViewSPV.sol";
import {SafeMath} from "@summa-tx/bitcoin-spv-sol/contracts/SafeMath.sol";
contract OnDemandSPV is ISPVRequestManager, Relay {
using SafeMath for uint256;
using TypedMemView for bytes;
using TypedMemView for bytes29;
using ViewBTC for bytes29;
using ViewSPV for bytes29;
struct ProofRequest {
bytes32 spends;
bytes32 pays;
uint256 notBefore;
address consumer;
uint64 paysValue;
uint8 numConfs;
address owner;
RequestStates state;
}
enum RequestStates { NONE, ACTIVE, CLOSED }
mapping (bytes32 => bool) internal validatedTxns; // authenticated tx store
mapping (uint256 => ProofRequest) internal requests; // request info
uint256 public constant BASE_COST = 24 * 60 * 60; // 1 day
uint256 public nextID;
bytes32 public latestValidatedTx;
uint256 public remoteGasAllowance = 500000; // maximum gas for callback call
/// @notice Gives a starting point for the relay
/// @dev We don't check this AT ALL really. Don't use relays with bad genesis
/// @param _genesisHeader The starting header
/// @param _height The starting height
/// @param _periodStart The hash of the first header in the genesis epoch
constructor(
bytes memory _genesisHeader,
uint256 _height,
bytes32 _periodStart,
uint256 _firstID
) Relay(
_genesisHeader,
_height,
_periodStart
) public {
nextID = _firstID;
}
/// @notice Cancel a bitcoin event request.
/// @dev Prevents the relay from forwarding tx infromation
/// @param _requestID The ID of the request to be cancelled
/// @return True if succesful, error otherwise
function cancelRequest(uint256 _requestID) external returns (bool) {
ProofRequest storage _req = requests[_requestID];
require(_req.state == RequestStates.ACTIVE, "Request not active");
require(msg.sender == _req.consumer || msg.sender == _req.owner, "Can only be cancelled by owner or consumer");
_req.state = RequestStates.CLOSED;
emit RequestClosed(_requestID);
return true;
}
function getLatestValidatedTx() external view returns (bytes32) {
return latestValidatedTx;
}
/// @notice Retrieve info about a request
/// @dev Requests ids are numerical
/// @param _requestID The numerical ID of the request
/// @return A tuple representation of the request struct
function getRequest(
uint256 _requestID
) external view returns (
bytes32 spends,
bytes32 pays,
uint64 paysValue,
uint8 state,
address consumer,
address owner,
uint8 numConfs,
uint256 notBefore
) {
ProofRequest storage _req = requests[_requestID];
spends = _req.spends;
pays = _req.pays;
paysValue = _req.paysValue;
state = uint8(_req.state);
consumer = _req.consumer;
owner = _req.owner;
numConfs = _req.numConfs;
notBefore = _req.notBefore;
}
/// @notice Subscribe to a feed of Bitcoin txns matching a request
/// @dev The request can be a spent utxo and/or a created utxo
/// @param _spends An outpoint that must be spent in acceptable txns (optional)
/// @param _pays An output script that must be paid in acceptable txns (optional)
/// @param _paysValue A minimum value that must be paid to the output script (optional)
/// @param _consumer The address of a ISPVConsumer exposing spv
/// @param _numConfs The minimum number of Bitcoin confirmations to accept
/// @param _notBefore A timestamp before which proofs are not accepted
/// @return A unique request ID.
function request(
bytes calldata _spends,
bytes calldata _pays,
uint64 _paysValue,
address _consumer,
uint8 _numConfs,
uint256 _notBefore
) external returns (uint256) {
return _request(_spends, _pays, _paysValue, _consumer, _numConfs, _notBefore);
}
/// @notice Subscribe to a feed of Bitcoin txns matching a request
/// @dev The request can be a spent utxo and/or a created utxo
/// @param _spendsBytes An outpoint that must be spent in acceptable txns (optional)
/// @param _paysBytes An output script that must be paid in acceptable txns (optional)
/// @param _paysValue A minimum value that must be paid to the output script (optional)
/// @param _consumer The address of a ISPVConsumer exposing spv
/// @param _numConfs The minimum number of Bitcoin confirmations to accept
/// @param _notBefore A timestamp before which proofs are not accepted
/// @return A unique request ID
function _request(
bytes memory _spendsBytes,
bytes memory _paysBytes,
uint64 _paysValue,
address _consumer,
uint8 _numConfs,
uint256 _notBefore
) internal returns (uint256) {
bytes29 _maybePays = _paysBytes.ref(0).tryAsSPK();
bytes29 _maybeSpends = _spendsBytes.ref(0).castTo(uint40(ViewBTC.BTCTypes.Outpoint));
uint256 _requestID = nextID;
nextID = nextID + 1;
ProofRequest storage _req = requests[_requestID];
_req.owner = msg.sender;
// First add critical qualities
if (_maybeSpends.len() > 0) {
require(_maybeSpends.len() == 36, "Not a valid UTXO");
_req.spends = _maybeSpends.keccak();
}
if (_maybePays.isValid()) {
require(
_maybePays.payload().notNull() || // standard output OR
_maybePays.opReturnPayload().notNull(), // OP_RETURN output
"Not a standard output type");
_req.pays = _maybePays.keccak();
}
require(
_req.spends != bytes32(0) || _req.pays != bytes32(0),
"No request specified"
);
// Then fill in request details
if (_paysValue > 0) {
_req.paysValue = _paysValue;
}
if (_numConfs > 0 && _numConfs < 241) { //241 is arbitray. 40 hours
_req.numConfs = _numConfs;
}
if (_notBefore > 0) {
_req.notBefore = _notBefore;
}
_req.consumer = _consumer;
_req.state = RequestStates.ACTIVE;
emit NewProofRequest(msg.sender, _requestID, _paysValue, _spendsBytes, _paysBytes);
return _requestID;
}
/// @notice Provide a proof of a tx that satisfies some request
/// @dev The caller must specify which inputs, which outputs, and which request
/// @param _header The header containing the merkleroot committing to the tx
/// @param _proof The merkle proof intermediate nodes
/// @param _version The tx version, always the first 4 bytes of the tx
/// @param _locktime The tx locktime, always the last 4 bytes of the tx
/// @param _index The index of the tx in the merkle tree's leaves
/// @param _reqIndices The input and output index to check against the request, packed
/// @param _vin The tx input vector
/// @param _vout The tx output vector
/// @param _requestID The id of the request that has been triggered
/// @return True if succesful, error otherwise
function provideProof(
bytes calldata _header,
bytes calldata _proof,
bytes4 _version,
bytes4 _locktime,
uint256 _index,
uint16 _reqIndices,
bytes calldata _vin,
bytes calldata _vout,
uint256 _requestID
) external returns (bool) {
return _provideProof(_header, _proof, _version, _locktime, _index, _reqIndices, _vin, _vout, _requestID);
}
function _provideProof(
bytes memory _header,
bytes memory _proof,
bytes4 _version,
bytes4 _locktime,
uint256 _index,
uint16 _reqIndices,
bytes memory _vin,
bytes memory _vout,
uint256 _requestID
) internal returns (bool) {
bytes32 _txid = abi.encodePacked(_version, _vin, _vout, _locktime).ref(0).hash256();
/*
NB: This shortcuts validation of any txn we've seen before.
Repeats can omit header, proof, and index
*/
if (!validatedTxns[_txid]) {
_checkInclusion(
_header.ref(0).tryAsHeader().assertValid(),
_proof.ref(0).tryAsMerkleArray().assertValid(),
_index,
_txid,
_requestID);
validatedTxns[_txid] = true;
latestValidatedTx = _txid;
}
_checkRequests(_reqIndices, _vin, _vout, _requestID);
_callCallback(_txid, _reqIndices, _vin, _vout, _requestID);
return true;
}
/// @notice Notify a consumer that one of its requests has been triggered
/// @dev We include information about the tx that triggered it, so the consumer can take actions
/// @param _vin The tx input vector
/// @param _vout The tx output vector
/// @param _txid The transaction ID
/// @param _requestID The id of the request that has been triggered
function _callCallback(
bytes32 _txid,
uint16 _reqIndices,
bytes memory _vin,
bytes memory _vout,
uint256 _requestID
) internal returns (bool) {
ProofRequest storage _req = requests[_requestID];
ISPVConsumer c = ISPVConsumer(_req.consumer);
uint8 _inputIndex = uint8(_reqIndices >> 8);
uint8 _outputIndex = uint8(_reqIndices & 0xff);
/*
NB:
We want to make the remote call, but we don't care about results
We use the low-level call so that we can ignore reverts and set gas
*/
address(c).call.gas(remoteGasAllowance)(
abi.encodePacked(
c.spv.selector,
abi.encode(_txid, _vin, _vout, _requestID, _inputIndex, _outputIndex)
)
);
emit RequestFilled(_txid, _requestID);
return true;
}
/// @notice Verifies inclusion of a tx in a header, and that header in the Relay chain
/// @dev Specifically we check that both the best tip and the heaviest common header confirm it
/// @param _header The header containing the merkleroot committing to the tx
/// @param _proof The merkle proof intermediate nodes
/// @param _index The index of the tx in the merkle tree's leaves
/// @param _txid The txid that is the proof leaf
/// @param _requestID The ID of the request to check against
function _checkInclusion(
bytes29 _header, // Header
bytes29 _proof, // MerkleArray
uint256 _index,
bytes32 _txid,
uint256 _requestID
) internal view returns (bool) {
require(
ViewSPV.prove(
_txid,
_header.merkleRoot(),
_proof,
_index),
"Bad inclusion proof");
bytes32 _headerHash = _header.hash256();
bytes32 _GCD = getLastReorgCommonAncestor();
require(
_isAncestor(
_headerHash,
_GCD,
240),
"GCD does not confirm header");
uint8 _numConfs = requests[_requestID].numConfs;
require(
_getConfs(_headerHash) >= _numConfs,
"Insufficient confirmations");
return true;
}
/// @notice Finds the number of headers on top of the argument
/// @dev Bounded to 6400 gas (8 looksups) max
/// @param _headerHash The LE double-sha2 header hash
/// @return The number of headers on top
function _getConfs(bytes32 _headerHash) internal view returns (uint8) {
return uint8(_findHeight(bestKnownDigest) - _findHeight(_headerHash));
}
/// @notice Verifies that a tx meets the requester's request
/// @dev Requests can be specify an input, and output, and/or an output value
/// @param _reqIndices The input and output index to check against the request, packed
/// @param _vinBytes The tx input vector
/// @param _voutBytes The tx output vector
/// @param _requestID The id of the request to check
function _checkRequests (
uint16 _reqIndices,
bytes memory _vinBytes,
bytes memory _voutBytes,
uint256 _requestID
) internal view returns (bool) {
bytes29 _vin = _vinBytes.ref(0).tryAsVin();
bytes29 _vout = _voutBytes.ref(0).tryAsVout();
require(_vin.notNull(), "Vin is malformatted");
require(_vout.notNull(), "Vout is malformatted");
uint8 _inputIndex = uint8(_reqIndices >> 8);
uint8 _outputIndex = uint8(_reqIndices & 0xff);
ProofRequest storage _req = requests[_requestID];
require(_req.notBefore <= block.timestamp, "Request is submitted too early");
require(_req.state == RequestStates.ACTIVE, "Request is not active");
bytes32 _pays = _req.pays;
bool _hasPays = _pays != bytes32(0);
if (_hasPays) {
bytes29 _out = _vout.indexVout(_outputIndex);
bytes29 _scriptPubkey = _out.scriptPubkey();
require(
_scriptPubkey.keccak() == _pays,
"Does not match pays request");
uint64 _paysValue = _req.paysValue;
require(
_paysValue == 0 ||
_out.value() >= _paysValue,
"Does not match value request");
}
bytes32 _spends = _req.spends;
bool _hasSpends = _spends != bytes32(0);
if (_hasSpends) {
bytes29 _in = _vin.indexVin(_inputIndex);
require(
!_hasSpends ||
_in.outpoint().keccak() == _spends,
"Does not match spends request");
}
return true;
}
}