-
Notifications
You must be signed in to change notification settings - Fork 5
/
Copy pathpeg.sol
358 lines (281 loc) · 12.9 KB
/
peg.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
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.5.10;
import "https://github.com/summa-tx/bitcoin-spv/blob/master/solidity/contracts/ViewBTC.sol";
import "https://github.com/summa-tx/bitcoin-spv/blob/master/solidity/contracts/ViewSPV.sol";
contract Peg {
//
// Security Parameters
//
// Minimum work required in a headers chain
uint256 MIN_WORK;
// Minimum headers chain length
uint32 MIN_CHAIN_LENGTH;
// Minimum value required for a peg-in
uint32 MIN_VALUE;
constructor(uint256 minWork, uint32 minChainLength, uint32 minValue) public {
MIN_WORK = minWork;
MIN_CHAIN_LENGTH = minChainLength;
MIN_VALUE = minValue;
}
//
// Constants to define valid bitcoin token transactions
//
// The tx version must be 2
bytes4 constant VERSION = 0x02000000;
// All inputs must have 41 bytes == TXID(32) + vout(4) + publicScriptLen(1) + nSequence(4)
uint32 constant VIN_LENGTH = 41;
// All inputs must have segregated witnesses
bytes1 constant PUBKEY_SCRIPT_LENGTH = 0x00;
// nSequence must be constant
bytes4 constant SEQUENCE = 0xffffffff;
// A tx must have exactly three outputs
bytes1 constant VOUT_COUNT = 0x03;
// All outputs must be p2wpkh (for now) // TODO: add p2pwsh and p2tr outputs
bytes3 constant P2WPKH_PREFIX = 0x160014;
// The BTC value of the op_return output must be zero
bytes8 constant OPRETURN_BTC_VALUE = 0x0000000000000000;
// A tx op_return encodes exactly 4 bytes (the token value of output_0)
bytes3 constant OPRETURN_PREFIX = 0x066a04;
// The locktime must be zero
bytes4 constant LOCKTIME = 0x00000000;
// A burn tx must have exactly one input
bytes1 constant BURN_VIN_COUNT = 0x01;
// A burn tx must have exactly one output
bytes1 constant BURN_VOUT_COUNT = 0x01;
// A burn tx op_return encodes exactly 20 bytes (the Ethereum address for the peg-out)
bytes3 constant BURN_OPRETURN_PREFIX = 0x166a14;
// Bitcoin headers are 80 bytes long
uint32 constant HEADER_LENGTH = 80;
// Minimum token unit for "common" values to fit into 32 bytes
uint64 constant MIN_TOKEN_UNIT = 10000000000;
// Maximum token value in uint32
uint64 constant MAX_VALUE = 0xffffffff * MIN_TOKEN_UNIT;
//
// Type definitions
//
using TypedMemView for bytes;
using TypedMemView for bytes29;
using ViewBTC for bytes29;
using ViewSPV for bytes29;
event PegInEvent(bytes32 txid, uint value);
//
// Global state
//
// The genesis outputs and their token values
mapping(uint256 => uint32) public genesisOutpoints;
// The Bitcoin Token utxo set
mapping(uint256 => uint32) public utxoSet;
// The used burn TXIDs
mapping(bytes32 => bool) public usedBurnTxs;
function pegIn(uint256 txid, uint32 vout) external payable {
// Verify the minimum peg-in value
require(msg.value >= MIN_TOKEN_UNIT, "Minimum peg-in value required");
// Verify the outpoint is unused. Overwriting it could be dangerous!
uint256 outpoint = txid + vout;
require(genesisOutpoints[outpoint] == 0, "Outputs can be used only once");
// Cast the value to 32 bits. Warning! This is dangerous.
// We have to limit the values to 2^32
require(msg.value <= MAX_VALUE, "Maximum token value exceeded");
uint32 value = uint32(msg.value / MIN_TOKEN_UNIT);
// Set the outpoint's value to the value we received
genesisOutpoints[outpoint] = value;
// Tell the world about the peg-in
emit PegInEvent(bytes32(outpoint), msg.value);
}
function pegOut(bytes memory chain, bytes memory proof, uint index, uint32 fundingVout, bytes memory history) public {
// Verify the token history
uint256 fundingTxid = verifyHistory(history);
// Compile the burn TX
bytes memory vin = abi.encodePacked(BURN_VIN_COUNT, fundingTxid, fundingVout, PUBKEY_SCRIPT_LENGTH, SEQUENCE);
// Ensure the sender's Ethereum address is referenced in the op_return
bytes memory vout = abi.encodePacked(BURN_VOUT_COUNT, OPRETURN_BTC_VALUE, BURN_OPRETURN_PREFIX, msg.sender );
// Calculate the burnTxid
bytes32 burnTxid = abi.encodePacked(VERSION, vin, vout, LOCKTIME).ref(0).hash256();
// Verify this burnTxid is unused
require( usedBurnTxs[burnTxid] == false, "These coins were already redeemed");
// Store this burnTxid as used
usedBurnTxs[burnTxid] = true;
// Verify the headers chain
bytes32 merkleRoot = verifyChain(chain);
// Verify the inclusion proof for this burnTxid
bytes29 proofRef = proof.ref(0).tryAsMerkleArray().assertValid();
require( ViewSPV.prove(burnTxid, merkleRoot, proofRef, index), "Invalid inclusion proof. Ensure you're calling the contract from the address referenced in your burn TX");
// Get the burned token value from the UTXO set
uint32 tokenValue = utxoSet[fundingTxid+fundingVout];
// The prover passed all checks, so we give them the requested amount.
msg.sender.transfer(tokenValue * MIN_TOKEN_UNIT);
}
function verifyChain(bytes memory chain) private returns (bytes32){
// Compute the total work in the chain
bytes29 headers = chain.ref(0).tryAsHeaderArray().assertValid();
uint256 totalWork = headers.checkChain();
// Verify that no error occured
require(totalWork != ViewSPV.getErrBadLength(), "Bad length");
require(totalWork != ViewSPV.getErrLowWork(), "Work too low");
require(totalWork != ViewSPV.getErrInvalidChain(), "Invalid chain");
// Verify the minimum amount of work in the chain
require(totalWork > MIN_WORK, "More proof-of-work required");
// Verify the minimum number of headers in the chain
require(chain.length >= HEADER_LENGTH * MIN_CHAIN_LENGTH, "More headers required");
// Get the first header in the chain
bytes29 header = headers.indexHeaderArray(0);
// Return its merkle root
return header.merkleRoot();
}
function verifyHistory(bytes memory history) private returns (uint256) {
uint256 currTxid;
cursor = 0;
while(cursor < history.length){
// Read vin count
uint8 vinCount = readUint8(history);
// Allocate memory for all vins
bytes memory vins = new bytes(VIN_LENGTH * vinCount);
// Compute sum of all inputs and compile them into one byte array
uint32 inputTokenValueSum = 0;
for(uint32 i = 0; i < vinCount; i++){
uint256 txidIn = readUint256(history);
// TODO: Cast uint8 to uint32 here
uint32 vout = readUint32(history);
bytes memory vin = abi.encodePacked(txidIn, vout, PUBKEY_SCRIPT_LENGTH, SEQUENCE);
uint256 outpoint = txidIn + reverse(vout);
// Read token value from utxo set
// TODO: use safe math here!
inputTokenValueSum += utxoSet[outpoint];
// Add genesis value if there is one for this outpoint
inputTokenValueSum += genesisOutpoints[outpoint];
// Delete the utxo from our set
utxoSet[outpoint] = 0;
// Copy into vins
uint32 offset = i * VIN_LENGTH;
for(uint32 j = 0; j < vin.length; j++){
vins[offset + j] = vin[j];
}
}
// Read output0
uint160 pubkeyhash0 = readUint160(history);
uint64 btcValue0 = readUint64(history);
// Read output1
uint160 pubkeyhash1 = readUint160(history);
uint64 btcValue1 = readUint64(history);
// Read token value0
uint32 tokenValue0 = readUint32(history);
// Verify token value0 is not creating tokens out of thin air
require(tokenValue0 <= inputTokenValueSum, "Invalid token value");
// Compute token value1
uint32 tokenValue1 = inputTokenValueSum - tokenValue0;
// Compile the TX and calculate the TXID
currTxid = uint256(
abi.encodePacked(
abi.encodePacked(
VERSION, vinCount, vins, VOUT_COUNT,
btcValue0, P2WPKH_PREFIX, pubkeyhash0),
abi.encodePacked(
btcValue1, P2WPKH_PREFIX, pubkeyhash1,
OPRETURN_BTC_VALUE, OPRETURN_PREFIX, tokenValue0,
LOCKTIME)
).ref(0).hash256());
// Insert outpoints into the UTXO set
// TODO: what if someone misses a token input in a tx because it was not required to prove his output's (output0) value?
utxoSet[currTxid] = tokenValue0;
utxoSet[currTxid+1] = tokenValue1;
}
return currTxid;
}
function reverse(uint32 input) internal pure returns (uint32 v) {
v = input;
// Swap bytes
v = ((v & 0xFF00FF00) >> 8) |
((v & 0x00FF00FF) << 8);
// Swap 2-byte long pairs
v = (v >> 16) | (v << 16);
}
// WARNING! UGLY HACK.
// Our bytes library. lol
// This is ugly code smell and probably not how bytes should be handled in Solidity.
// TODO: Clean this mess up!
uint256 cursor = 0;
function readUint8(bytes memory _bytes) internal returns (uint8) {
require(_bytes.length >= cursor + 1 , "toUint8_outOfBounds");
uint8 tempUint;
uint256 _cursor = cursor;
assembly {
tempUint := mload(add(add(_bytes, 0x1), _cursor))
}
cursor += 1;
return tempUint;
}
function readUint16(bytes memory _bytes) internal returns (uint16) {
require(_bytes.length >= cursor + 2, "toUint16_outOfBounds");
uint16 tempUint;
uint256 _cursor = cursor;
assembly {
tempUint := mload(add(add(_bytes, 0x2), _cursor))
}
cursor += 2;
return tempUint;
}
function readUint32(bytes memory _bytes) internal returns (uint32) {
require(_bytes.length >= cursor + 4, "toUint32_outOfBounds");
uint32 tempUint;
uint256 _cursor = cursor;
assembly {
tempUint := mload(add(add(_bytes, 0x4), _cursor))
}
cursor += 4;
return tempUint;
}
function readUint64(bytes memory _bytes) internal returns (uint64) {
require(_bytes.length >= cursor + 8, "toUint64_outOfBounds");
uint64 tempUint;
uint256 _cursor = cursor;
assembly {
tempUint := mload(add(add(_bytes, 0x8), _cursor))
}
cursor += 8;
return tempUint;
}
function readUint160(bytes memory _bytes) internal returns (uint160) {
require(_bytes.length >= cursor + 20, "toUint160_outOfBounds");
uint160 tempUint;
uint256 _cursor = cursor;
assembly {
tempUint := mload(add(add(_bytes, 0x14), _cursor))
}
cursor += 20;
return tempUint;
}
function readUint256(bytes memory _bytes) internal returns (uint256) {
require(_bytes.length >= cursor + 32, "toUint256_outOfBounds");
uint256 tempUint;
uint256 _cursor = cursor;
assembly {
tempUint := mload(add(add(_bytes, 0x20), _cursor))
}
cursor += 32;
return tempUint;
}
function readBytes32(bytes memory _bytes) internal returns (bytes32) {
require(_bytes.length >= cursor + 32, "toBytes32_outOfBounds");
bytes32 tempBytes32;
uint256 _cursor = cursor;
assembly {
tempBytes32 := mload(add(add(_bytes, 0x20), _cursor))
}
cursor += 32;
return tempBytes32;
}
}
// IDEA: let every peg-in commit to the current token UTXO set and a current bitcoin block hash.
// This utxo set commitment is verified client-side by the token recipient in bitcoin.
// This ensures, to forge a token history of value X, an attacker has to spend, for example, 1.1 * X.
// In addition to PoW, this is another economic game to find consensus on the entire token history.
// Another succinct proof, that is very expensive for attackers, but very cheap for honest users.
// It is used in peg-outs to compress the token history.
// Older tokens have to reference those newer tokens and thus, confirm their token history.
// PROBLEM: Attacker's don't lose money if they make an invalid commitment and then peg-out
// because the contract doesn't know the truth
// IDEA: use locktime to prove a minimum block height for every TX
// This helps to develop a more robust notion of bitcoin block times
// We can require the locktimes increase every transaction
// This also allows us to set minimum times for token existence
// e.g. burnTx.locktime > 100 + genesisTx.locktime