├── .gitignore ├── README.rst ├── addresses.txt ├── contracts ├── IntegratedEncryptionScheme.sol ├── codec │ ├── ECCConversion.sol │ ├── LICENSE │ └── RLP.sol └── crypto │ ├── Curve.sol │ ├── ECCMath.sol │ ├── LICENSE │ └── Secp256k1.sol ├── locations.txt ├── populus.json ├── pydkg ├── __init__.py ├── __main__.py ├── crypto.py ├── db.py ├── ecdkg.py ├── networking.py ├── rpc_interface.py └── util.py ├── setup.py ├── tests ├── conftest.py ├── test_crypto.py └── test_ecdkg.py └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # Jupyter Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # SageMath parsed files 80 | *.sage.py 81 | 82 | # dotenv 83 | .env 84 | 85 | # virtualenv 86 | .venv 87 | venv/ 88 | ENV/ 89 | 90 | # Spyder project settings 91 | .spyderproject 92 | .spyproject 93 | 94 | # Rope project settings 95 | .ropeproject 96 | 97 | # mkdocs documentation 98 | /site 99 | 100 | # mypy 101 | .mypy_cache/ 102 | 103 | # project specific files 104 | private.key* 105 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ##### 2 | pydkg 3 | ##### 4 | 5 | A python implementation of distributed key generation over secp256k1 6 | -------------------------------------------------------------------------------- /addresses.txt: -------------------------------------------------------------------------------- 1 | 0x96c3697dd523d9f38ba1e8e853eaa14ddc8e3205 2 | 0x288e81cffdd14464b91367068fa58e376ec3082e 3 | -------------------------------------------------------------------------------- /contracts/IntegratedEncryptionScheme.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.0; 2 | 3 | import {ECCMath} from "github.com/androlo/standard-contracts/contracts/src/crypto/ECCMath.sol"; 4 | import {Secp256k1} from "github.com/androlo/standard-contracts/contracts/src/crypto/Secp256k1.sol"; 5 | 6 | 7 | library IntegratedEncryptionScheme { 8 | function decrypt(bytes ciphertext, uint deckey) returns (uint) { 9 | // util.validate_private_value(deckey) 10 | if(deckey >= 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141) 11 | throw; 12 | 13 | // R = tuple(int.from_bytes(ciphertext[i:i+32], byteorder='big') for i in (0, 32)) 14 | uint[2] memory R; 15 | assembly { 16 | // bytes 0x00 to 0x1f encode length of ciphertext 17 | // so shift everything by 0x20 18 | mstore(R, mload(add(ciphertext, 0x20))) 19 | mstore(add(R, 0x20), mload(add(ciphertext, 0x40))) 20 | } 21 | 22 | // util.validate_curve_point(R) 23 | if(!Secp256k1.onCurve(R)) 24 | throw; 25 | 26 | 27 | // S = bitcoin.fast_multiply(R, deckey) 28 | uint[3] memory S = Secp256k1._mul(deckey, R); 29 | ECCMath.toZ1(S, 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F); 30 | 31 | // num_trunc_bytes = ord(ciphertext[64:65]) 32 | uint8 numTruncBytes = uint8(ciphertext[64]); 33 | 34 | // iv = ciphertext[65:97] 35 | uint iv; 36 | assembly { 37 | iv := mload(add(ciphertext, 0x61)) 38 | } 39 | 40 | // c = ciphertext[97:-32] 41 | // if len(c) % 32 != 0: 42 | // raise ValueError('enciphered message not properly aligned') 43 | if(ciphertext.length < 129 || ciphertext.length % 32 != 1) 44 | throw; 45 | 46 | // kEkM = sha3.keccak_256(S[0].to_bytes(32, byteorder='big')).digest() 47 | // kE, kM = kEkM[0:16], kEkM[16:32] 48 | uint kEkM = uint(keccak256(S[0])); 49 | uint128 kE = uint128(kEkM >> 128); 50 | uint128 kM = uint128(kEkM & 2**128-1); 51 | 52 | // num_chunks = len(c) // 32 53 | // message = b''.join(( 54 | // int.from_bytes(c[32*i:32*(i+1)], 'big') ^ 55 | // int.from_bytes(sha3.keccak_256(kE + iv + i.to_bytes(32, 'big')).digest(), 'big') 56 | // ).to_bytes(32, byteorder='big') for i in range(num_chunks)) 57 | bytes memory message = new bytes(ciphertext.length - 129); 58 | for(uint i = 0; i < message.length / 32; ++i) { 59 | uint off = 0x20 + i * 32; 60 | uint blockCipher = uint(keccak256(kE, iv, i)); 61 | assembly { 62 | mstore(add(message, off), xor( 63 | mload(add(ciphertext, add(97, off))), 64 | blockCipher 65 | )) 66 | } 67 | } 68 | 69 | // if num_trunc_bytes > 0: 70 | // message, padding = message[:-num_trunc_bytes], message[-num_trunc_bytes:] 71 | // if padding != b'\0' * num_trunc_bytes: 72 | // raise ValueError('invalid padding') 73 | 74 | if(numTruncBytes > 0) { 75 | for(i = 0; i < numTruncBytes; i++) { 76 | if(uint8(message[message.length - i - 1]) != 0) 77 | throw; 78 | } 79 | assembly { 80 | mstore(message, sub(mload(message), numTruncBytes)) 81 | } 82 | } 83 | 84 | // d = ciphertext[-32:] 85 | assembly { 86 | kEkM := mload(add(ciphertext, mload(ciphertext))) 87 | // HACK: Format slice as a bytes object and reassign name ciphertext 88 | mstore(add(ciphertext, 97), sub(mload(ciphertext), 129)) 89 | ciphertext := add(ciphertext, 97) 90 | } 91 | 92 | // if d != sha3.keccak_256(kM + c).digest(): 93 | // raise ValueError('message authentication code does not match') 94 | if(kEkM != uint(keccak256(kM, ciphertext))) 95 | throw; 96 | 97 | // return message 98 | return uint(keccak256(message)); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /contracts/codec/ECCConversion.sol: -------------------------------------------------------------------------------- 1 | import {Curve} from "github.com/androlo/standard-contracts/contracts/src/crypto/Curve.sol"; 2 | /** 3 | * @title ECCConversion 4 | * 5 | * Methods for to converting elliptic-curve cryptographic data between 6 | * different formats in accordance with SEC 1 (www.secg.org/sec1-v2.pdf). 7 | * 8 | * @author Andreas Olofsson (androlo1980@gmail.com) 9 | */ 10 | contract ECCConversion { 11 | /// @dev Convert a curve point to an array of bytes (octets). 12 | /// Ref: SEC 1: 2.3.3 13 | /// @param curve The curve. 14 | /// @param P The point 15 | /// @param compress Whether or not to compress the point. 16 | /// @return The bytes. 17 | function curvePointToBytes(Curve curve, uint[2] memory P, bool compress) internal constant returns (bytes memory bts) { 18 | if(P[0] == 0 && P[1] == 0) 19 | bts = new bytes(1); 20 | else if (!compress) { 21 | bts = new bytes(65); 22 | bts[0] = 4; 23 | uint Px = P[0]; 24 | uint Py = P[1]; 25 | assembly { 26 | mstore(add(bts, 0x21), Px) 27 | mstore(add(bts, 0x41), Py) 28 | } 29 | } 30 | else { 31 | bts = new bytes(33); 32 | var (yBit, x) = curve.compress(P); 33 | bts[0] = yBit & 1 == 1 ? byte(3) : byte(2); 34 | assembly { 35 | mstore(add(bts, 0x21), mload(P)) 36 | } 37 | } 38 | } 39 | 40 | /// @dev Convert bytes into an elliptic curve point. 41 | /// Ref: SEC 1: 2.3.4 42 | /// @param curve The curve. 43 | /// @param bts The bytes. 44 | /// @return The point. 45 | function bytesToCurvePoint(Curve curve, bytes memory bts) internal constant returns (uint[2] memory P) { 46 | if (bts.length == 0 && bts[0] == 0) 47 | return; 48 | else if (bts.length == 65 && bts[0] == 4) { 49 | // This is an uncompressed point. 50 | assembly { 51 | mstore(P, mload(add(bts, 0x21))) 52 | mstore(add(P, 0x20), mload(add(bts, 0x41))) 53 | } 54 | } 55 | else if (bts.length == 33) { 56 | byte b0 = bts[0]; 57 | if (b0 != 2 && b0 != 3) 58 | throw; 59 | uint8 yBit = uint8(b0) - 2; 60 | uint Px; 61 | assembly { 62 | Px := mload(add(bts, 0x21)) 63 | } 64 | P = curve.decompress(yBit, Px); 65 | } 66 | else 67 | throw; // Invalid format. 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /contracts/codec/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Andreas Olofsson (andreas@ohalo.co) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /contracts/codec/RLP.sol: -------------------------------------------------------------------------------- 1 | /** 2 | * @title RLPReader 3 | * 4 | * RLPReader is used to read and parse RLP encoded data in memory. 5 | * 6 | * @author Andreas Olofsson (androlo1980@gmail.com) 7 | */ 8 | library RLP { 9 | 10 | uint constant DATA_SHORT_START = 0x80; 11 | uint constant DATA_LONG_START = 0xB8; 12 | uint constant LIST_SHORT_START = 0xC0; 13 | uint constant LIST_LONG_START = 0xF8; 14 | 15 | uint constant DATA_LONG_OFFSET = 0xB7; 16 | uint constant LIST_LONG_OFFSET = 0xF7; 17 | 18 | 19 | struct RLPItem { 20 | uint _unsafe_memPtr; // Pointer to the RLP-encoded bytes. 21 | uint _unsafe_length; // Number of bytes. This is the full length of the string. 22 | } 23 | 24 | struct Iterator { 25 | RLPItem _unsafe_item; // Item that's being iterated over. 26 | uint _unsafe_nextPtr; // Position of the next item in the list. 27 | } 28 | 29 | /* Iterator */ 30 | 31 | function next(Iterator memory self) internal constant returns (RLPItem memory subItem) { 32 | if(hasNext(self)) { 33 | var ptr = self._unsafe_nextPtr; 34 | var itemLength = _itemLength(ptr); 35 | subItem._unsafe_memPtr = ptr; 36 | subItem._unsafe_length = itemLength; 37 | self._unsafe_nextPtr = ptr + itemLength; 38 | } 39 | else 40 | throw; 41 | } 42 | 43 | function next(Iterator memory self, bool strict) internal constant returns (RLPItem memory subItem) { 44 | subItem = next(self); 45 | if(strict && !_validate(subItem)) 46 | throw; 47 | return; 48 | } 49 | 50 | function hasNext(Iterator memory self) internal constant returns (bool) { 51 | var item = self._unsafe_item; 52 | return self._unsafe_nextPtr < item._unsafe_memPtr + item._unsafe_length; 53 | } 54 | 55 | /* RLPItem */ 56 | 57 | /// @dev Creates an RLPItem from an array of RLP encoded bytes. 58 | /// @param self The RLP encoded bytes. 59 | /// @return An RLPItem 60 | function toRLPItem(bytes memory self) internal constant returns (RLPItem memory) { 61 | uint len = self.length; 62 | if (len == 0) { 63 | return RLPItem(0, 0); 64 | } 65 | uint memPtr; 66 | assembly { 67 | memPtr := add(self, 0x20) 68 | } 69 | return RLPItem(memPtr, len); 70 | } 71 | 72 | /// @dev Creates an RLPItem from an array of RLP encoded bytes. 73 | /// @param self The RLP encoded bytes. 74 | /// @param strict Will throw if the data is not RLP encoded. 75 | /// @return An RLPItem 76 | function toRLPItem(bytes memory self, bool strict) internal constant returns (RLPItem memory) { 77 | var item = toRLPItem(self); 78 | if(strict) { 79 | uint len = self.length; 80 | if(_payloadOffset(item) > len) 81 | throw; 82 | if(_itemLength(item._unsafe_memPtr) != len) 83 | throw; 84 | if(!_validate(item)) 85 | throw; 86 | } 87 | return item; 88 | } 89 | 90 | /// @dev Check if the RLP item is null. 91 | /// @param self The RLP item. 92 | /// @return 'true' if the item is null. 93 | function isNull(RLPItem memory self) internal constant returns (bool ret) { 94 | return self._unsafe_length == 0; 95 | } 96 | 97 | /// @dev Check if the RLP item is a list. 98 | /// @param self The RLP item. 99 | /// @return 'true' if the item is a list. 100 | function isList(RLPItem memory self) internal constant returns (bool ret) { 101 | if (self._unsafe_length == 0) 102 | return false; 103 | uint memPtr = self._unsafe_memPtr; 104 | assembly { 105 | ret := iszero(lt(byte(0, mload(memPtr)), 0xC0)) 106 | } 107 | } 108 | 109 | /// @dev Check if the RLP item is data. 110 | /// @param self The RLP item. 111 | /// @return 'true' if the item is data. 112 | function isData(RLPItem memory self) internal constant returns (bool ret) { 113 | if (self._unsafe_length == 0) 114 | return false; 115 | uint memPtr = self._unsafe_memPtr; 116 | assembly { 117 | ret := lt(byte(0, mload(memPtr)), 0xC0) 118 | } 119 | } 120 | 121 | /// @dev Check if the RLP item is empty (string or list). 122 | /// @param self The RLP item. 123 | /// @return 'true' if the item is null. 124 | function isEmpty(RLPItem memory self) internal constant returns (bool ret) { 125 | if(isNull(self)) 126 | return false; 127 | uint b0; 128 | uint memPtr = self._unsafe_memPtr; 129 | assembly { 130 | b0 := byte(0, mload(memPtr)) 131 | } 132 | return (b0 == DATA_SHORT_START || b0 == LIST_SHORT_START); 133 | } 134 | 135 | /// @dev Get the number of items in an RLP encoded list. 136 | /// @param self The RLP item. 137 | /// @return The number of items. 138 | function items(RLPItem memory self) internal constant returns (uint) { 139 | if (!isList(self)) 140 | return 0; 141 | uint b0; 142 | uint memPtr = self._unsafe_memPtr; 143 | assembly { 144 | b0 := byte(0, mload(memPtr)) 145 | } 146 | uint pos = memPtr + _payloadOffset(self); 147 | uint last = memPtr + self._unsafe_length - 1; 148 | uint itms; 149 | while(pos <= last) { 150 | pos += _itemLength(pos); 151 | itms++; 152 | } 153 | return itms; 154 | } 155 | 156 | /// @dev Create an iterator. 157 | /// @param self The RLP item. 158 | /// @return An 'Iterator' over the item. 159 | function iterator(RLPItem memory self) internal constant returns (Iterator memory it) { 160 | if (!isList(self)) 161 | throw; 162 | uint ptr = self._unsafe_memPtr + _payloadOffset(self); 163 | it._unsafe_item = self; 164 | it._unsafe_nextPtr = ptr; 165 | } 166 | 167 | /// @dev Return the RLP encoded bytes. 168 | /// @param self The RLPItem. 169 | /// @return The bytes. 170 | function toBytes(RLPItem memory self) internal constant returns (bytes memory bts) { 171 | var len = self._unsafe_length; 172 | if (len == 0) 173 | return; 174 | bts = new bytes(len); 175 | _copyToBytes(self._unsafe_memPtr, bts, len); 176 | } 177 | 178 | /// @dev Decode an RLPItem into bytes. This will not work if the 179 | /// RLPItem is a list. 180 | /// @param self The RLPItem. 181 | /// @return The decoded string. 182 | function toData(RLPItem memory self) internal constant returns (bytes memory bts) { 183 | if(!isData(self)) 184 | throw; 185 | var (rStartPos, len) = _decode(self); 186 | bts = new bytes(len); 187 | _copyToBytes(rStartPos, bts, len); 188 | } 189 | 190 | /// @dev Get the list of sub-items from an RLP encoded list. 191 | /// Warning: This is inefficient, as it requires that the list is read twice. 192 | /// @param self The RLP item. 193 | /// @return Array of RLPItems. 194 | function toList(RLPItem memory self) internal constant returns (RLPItem[] memory list) { 195 | if(!isList(self)) 196 | throw; 197 | var numItems = items(self); 198 | list = new RLPItem[](numItems); 199 | var it = iterator(self); 200 | uint idx; 201 | while(hasNext(it)) { 202 | list[idx] = next(it); 203 | idx++; 204 | } 205 | } 206 | 207 | /// @dev Decode an RLPItem into an ascii string. This will not work if the 208 | /// RLPItem is a list. 209 | /// @param self The RLPItem. 210 | /// @return The decoded string. 211 | function toAscii(RLPItem memory self) internal constant returns (string memory str) { 212 | if(!isData(self)) 213 | throw; 214 | var (rStartPos, len) = _decode(self); 215 | bytes memory bts = new bytes(len); 216 | _copyToBytes(rStartPos, bts, len); 217 | str = string(bts); 218 | } 219 | 220 | /// @dev Decode an RLPItem into a uint. This will not work if the 221 | /// RLPItem is a list. 222 | /// @param self The RLPItem. 223 | /// @return The decoded string. 224 | function toUint(RLPItem memory self) internal constant returns (uint data) { 225 | if(!isData(self)) 226 | throw; 227 | var (rStartPos, len) = _decode(self); 228 | if (len > 32 || len == 0) 229 | throw; 230 | assembly { 231 | data := div(mload(rStartPos), exp(256, sub(32, len))) 232 | } 233 | } 234 | 235 | /// @dev Decode an RLPItem into a boolean. This will not work if the 236 | /// RLPItem is a list. 237 | /// @param self The RLPItem. 238 | /// @return The decoded string. 239 | function toBool(RLPItem memory self) internal constant returns (bool data) { 240 | if(!isData(self)) 241 | throw; 242 | var (rStartPos, len) = _decode(self); 243 | if (len != 1) 244 | throw; 245 | uint temp; 246 | assembly { 247 | temp := byte(0, mload(rStartPos)) 248 | } 249 | if (temp > 1) 250 | throw; 251 | return temp == 1 ? true : false; 252 | } 253 | 254 | /// @dev Decode an RLPItem into a byte. This will not work if the 255 | /// RLPItem is a list. 256 | /// @param self The RLPItem. 257 | /// @return The decoded string. 258 | function toByte(RLPItem memory self) internal constant returns (byte data) { 259 | if(!isData(self)) 260 | throw; 261 | var (rStartPos, len) = _decode(self); 262 | if (len != 1) 263 | throw; 264 | uint temp; 265 | assembly { 266 | temp := byte(0, mload(rStartPos)) 267 | } 268 | return byte(temp); 269 | } 270 | 271 | /// @dev Decode an RLPItem into an int. This will not work if the 272 | /// RLPItem is a list. 273 | /// @param self The RLPItem. 274 | /// @return The decoded string. 275 | function toInt(RLPItem memory self) internal constant returns (int data) { 276 | return int(toUint(self)); 277 | } 278 | 279 | /// @dev Decode an RLPItem into a bytes32. This will not work if the 280 | /// RLPItem is a list. 281 | /// @param self The RLPItem. 282 | /// @return The decoded string. 283 | function toBytes32(RLPItem memory self) internal constant returns (bytes32 data) { 284 | return bytes32(toUint(self)); 285 | } 286 | 287 | /// @dev Decode an RLPItem into an address. This will not work if the 288 | /// RLPItem is a list. 289 | /// @param self The RLPItem. 290 | /// @return The decoded string. 291 | function toAddress(RLPItem memory self) internal constant returns (address data) { 292 | if(!isData(self)) 293 | throw; 294 | var (rStartPos, len) = _decode(self); 295 | if (len != 20) 296 | throw; 297 | assembly { 298 | data := div(mload(rStartPos), exp(256, 12)) 299 | } 300 | } 301 | 302 | // Get the payload offset. 303 | function _payloadOffset(RLPItem memory self) private constant returns (uint) { 304 | if(self._unsafe_length == 0) 305 | return 0; 306 | uint b0; 307 | uint memPtr = self._unsafe_memPtr; 308 | assembly { 309 | b0 := byte(0, mload(memPtr)) 310 | } 311 | if(b0 < DATA_SHORT_START) 312 | return 0; 313 | if(b0 < DATA_LONG_START || (b0 >= LIST_SHORT_START && b0 < LIST_LONG_START)) 314 | return 1; 315 | if(b0 < LIST_SHORT_START) 316 | return b0 - DATA_LONG_OFFSET + 1; 317 | return b0 - LIST_LONG_OFFSET + 1; 318 | } 319 | 320 | // Get the full length of an RLP item. 321 | function _itemLength(uint memPtr) private constant returns (uint len) { 322 | uint b0; 323 | assembly { 324 | b0 := byte(0, mload(memPtr)) 325 | } 326 | if (b0 < DATA_SHORT_START) 327 | len = 1; 328 | else if (b0 < DATA_LONG_START) 329 | len = b0 - DATA_SHORT_START + 1; 330 | else if (b0 < LIST_SHORT_START) { 331 | assembly { 332 | let bLen := sub(b0, 0xB7) // bytes length (DATA_LONG_OFFSET) 333 | let dLen := div(mload(add(memPtr, 1)), exp(256, sub(32, bLen))) // data length 334 | len := add(1, add(bLen, dLen)) // total length 335 | } 336 | } 337 | else if (b0 < LIST_LONG_START) 338 | len = b0 - LIST_SHORT_START + 1; 339 | else { 340 | assembly { 341 | let bLen := sub(b0, 0xF7) // bytes length (LIST_LONG_OFFSET) 342 | let dLen := div(mload(add(memPtr, 1)), exp(256, sub(32, bLen))) // data length 343 | len := add(1, add(bLen, dLen)) // total length 344 | } 345 | } 346 | } 347 | 348 | // Get start position and length of the data. 349 | function _decode(RLPItem memory self) private constant returns (uint memPtr, uint len) { 350 | if(!isData(self)) 351 | throw; 352 | uint b0; 353 | uint start = self._unsafe_memPtr; 354 | assembly { 355 | b0 := byte(0, mload(start)) 356 | } 357 | if (b0 < DATA_SHORT_START) { 358 | memPtr = start; 359 | len = 1; 360 | return; 361 | } 362 | if (b0 < DATA_LONG_START) { 363 | len = self._unsafe_length - 1; 364 | memPtr = start + 1; 365 | } else { 366 | uint bLen; 367 | assembly { 368 | bLen := sub(b0, 0xB7) // DATA_LONG_OFFSET 369 | } 370 | len = self._unsafe_length - 1 - bLen; 371 | memPtr = start + bLen + 1; 372 | } 373 | return; 374 | } 375 | 376 | // Assumes that enough memory has been allocated to store in target. 377 | function _copyToBytes(uint btsPtr, bytes memory tgt, uint btsLen) private constant { 378 | // Exploiting the fact that 'tgt' was the last thing to be allocated, 379 | // we can write entire words, and just overwrite any excess. 380 | assembly { 381 | { 382 | let i := 0 // Start at arr + 0x20 383 | let words := div(add(btsLen, 31), 32) 384 | let rOffset := btsPtr 385 | let wOffset := add(tgt, 0x20) 386 | tag_loop: 387 | jumpi(end, eq(i, words)) 388 | { 389 | let offset := mul(i, 0x20) 390 | mstore(add(wOffset, offset), mload(add(rOffset, offset))) 391 | i := add(i, 1) 392 | } 393 | jump(tag_loop) 394 | end: 395 | mstore(add(tgt, add(0x20, mload(tgt))), 0) 396 | } 397 | } 398 | } 399 | 400 | // Check that an RLP item is valid. 401 | function _validate(RLPItem memory self) private constant returns (bool ret) { 402 | // Check that RLP is well-formed. 403 | uint b0; 404 | uint b1; 405 | uint memPtr = self._unsafe_memPtr; 406 | assembly { 407 | b0 := byte(0, mload(memPtr)) 408 | b1 := byte(1, mload(memPtr)) 409 | } 410 | if(b0 == DATA_SHORT_START + 1 && b1 < DATA_SHORT_START) 411 | return false; 412 | return true; 413 | } 414 | } -------------------------------------------------------------------------------- /contracts/crypto/Curve.sol: -------------------------------------------------------------------------------- 1 | /** 2 | * @title Curve 3 | * 4 | * Interface for elliptic curves and related crypto primitives. 5 | * 6 | * @author Andreas Olofsson (androlo1980@gmail.com) 7 | */ 8 | contract Curve { 9 | 10 | /// @dev Check whether the input point is on the curve. 11 | /// SEC 1: 3.2.3.1 12 | /// @param P The point. 13 | /// @return True if the point is on the curve. 14 | function onCurve(uint[2] P) constant returns (bool); 15 | 16 | /// @dev Check if the given point is a valid public key. 17 | /// SEC 1: 3.2.2.1 18 | /// @param P The point. 19 | /// @return True if the point is on the curve. 20 | function isPubKey(uint[2] P) constant returns (bool onc); 21 | 22 | /// @dev Validate the signature 'rs' of 'h = H(message)' against the public key Q. 23 | /// SEC 1: 4.1.4 24 | /// @param h The hash of the message. 25 | /// @param rs The signature (r, s) 26 | /// @param Q The public key to validate against. 27 | /// @return True if the point is on the curve. 28 | function validateSignature(bytes32 h, uint[2] rs, uint[2] Q) constant returns (bool); 29 | 30 | /// @dev compress a point 'P = (Px, Py)' on the curve, giving 'C(P) = (yBit, Px)' 31 | /// SEC 1: 2.3.3 - but only the curve-dependent code. 32 | /// @param P The point. 33 | /// @return The compressed y coordinate (yBit) and the x coordinate. 34 | function compress(uint[2] P) constant returns (uint8 yBit, uint x); 35 | 36 | /// @dev decompress a point 'Px', giving 'Py' for 'P = (Px, Py)' 37 | /// 'yBit' is 1 if 'Qy' is odd, otherwise 0. 38 | /// SEC 1: 2.3.4 - but only the curve-dependent code. 39 | /// @param yBit The compressed y-coordinate (One bit) 40 | /// @param Px The x-coordinate. 41 | /// @return True if the point is on the curve. 42 | function decompress(uint8 yBit, uint Px) constant returns (uint[2] Q); 43 | 44 | } -------------------------------------------------------------------------------- /contracts/crypto/ECCMath.sol: -------------------------------------------------------------------------------- 1 | /** 2 | * @title ECCMath 3 | * 4 | * Functions for working with integers, curve-points, etc. 5 | * 6 | * @author Andreas Olofsson (androlo1980@gmail.com) 7 | */ 8 | library ECCMath { 9 | /// @dev Modular inverse of a (mod p) using euclid. 10 | /// 'a' and 'p' must be co-prime. 11 | /// @param a The number. 12 | /// @param p The mmodulus. 13 | /// @return x such that ax = 1 (mod p) 14 | function invmod(uint a, uint p) internal constant returns (uint) { 15 | if (a == 0 || a == p || p == 0) 16 | throw; 17 | if (a > p) 18 | a = a % p; 19 | int t1; 20 | int t2 = 1; 21 | uint r1 = p; 22 | uint r2 = a; 23 | uint q; 24 | while (r2 != 0) { 25 | q = r1 / r2; 26 | (t1, t2, r1, r2) = (t2, t1 - int(q) * t2, r2, r1 - q * r2); 27 | } 28 | if (t1 < 0) 29 | return (p - uint(-t1)); 30 | return uint(t1); 31 | } 32 | 33 | /// @dev Modular exponentiation, b^e % m 34 | /// Basically the same as can be found here: 35 | /// https://github.com/ethereum/serpent/blob/develop/examples/ecc/modexp.se 36 | /// @param b The base. 37 | /// @param e The exponent. 38 | /// @param m The modulus. 39 | /// @return x such that x = b**e (mod m) 40 | function expmod(uint b, uint e, uint m) internal constant returns (uint r) { 41 | if (b == 0) 42 | return 0; 43 | if (e == 0) 44 | return 1; 45 | if (m == 0) 46 | throw; 47 | r = 1; 48 | uint bit = 2 ** 255; 49 | assembly { 50 | loop: 51 | jumpi(end, iszero(bit)) 52 | r := mulmod(mulmod(r, r, m), exp(b, iszero(iszero(and(e, bit)))), m) 53 | r := mulmod(mulmod(r, r, m), exp(b, iszero(iszero(and(e, div(bit, 2))))), m) 54 | r := mulmod(mulmod(r, r, m), exp(b, iszero(iszero(and(e, div(bit, 4))))), m) 55 | r := mulmod(mulmod(r, r, m), exp(b, iszero(iszero(and(e, div(bit, 8))))), m) 56 | bit := div(bit, 16) 57 | jump(loop) 58 | end: 59 | } 60 | } 61 | 62 | /// @dev Converts a point (Px, Py, Pz) expressed in Jacobian coordinates to (Px', Py', 1). 63 | /// Mutates P. 64 | /// @param P The point. 65 | /// @param zInv The modular inverse of 'Pz'. 66 | /// @param z2Inv The square of zInv 67 | /// @param prime The prime modulus. 68 | /// @return (Px', Py', 1) 69 | function toZ1(uint[3] memory P, uint zInv, uint z2Inv, uint prime) internal constant { 70 | P[0] = mulmod(P[0], z2Inv, prime); 71 | P[1] = mulmod(P[1], mulmod(zInv, z2Inv, prime), prime); 72 | P[2] = 1; 73 | } 74 | 75 | /// @dev See _toZ1(uint[3], uint, uint). 76 | /// Warning: Computes a modular inverse. 77 | /// @param PJ The point. 78 | /// @param prime The prime modulus. 79 | /// @return (Px', Py', 1) 80 | function toZ1(uint[3] PJ, uint prime) internal constant { 81 | uint zInv = invmod(PJ[2], prime); 82 | uint zInv2 = mulmod(zInv, zInv, prime); 83 | PJ[0] = mulmod(PJ[0], zInv2, prime); 84 | PJ[1] = mulmod(PJ[1], mulmod(zInv, zInv2, prime), prime); 85 | PJ[2] = 1; 86 | } 87 | 88 | } -------------------------------------------------------------------------------- /contracts/crypto/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Andreas Olofsson (andreas@ohalo.co) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /contracts/crypto/Secp256k1.sol: -------------------------------------------------------------------------------- 1 | import {ECCMath} from "github.com/androlo/standard-contracts/contracts/src/crypto/ECCMath.sol"; 2 | 3 | /** 4 | * @title Secp256k1 5 | * 6 | * secp256k1 implementation. 7 | * 8 | * The library implements 'Curve' and 'codec/ECCConversion', but since it's a library 9 | * it does not actually extend the contracts. This is a Solidity thing and will be 10 | * dealt with later. 11 | * 12 | * @author Andreas Olofsson (androlo1980@gmail.com) 13 | */ 14 | library Secp256k1 { 15 | 16 | // TODO separate curve from crypto primitives? 17 | 18 | // Field size 19 | uint constant pp = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F; 20 | 21 | // Base point (generator) G 22 | uint constant Gx = 0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798; 23 | uint constant Gy = 0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8; 24 | 25 | // Order of G 26 | uint constant nn = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141; 27 | 28 | // Cofactor 29 | // uint constant hh = 1; 30 | 31 | // Maximum value of s 32 | uint constant lowSmax = 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0; 33 | 34 | // For later 35 | // uint constant lambda = "0x5363ad4cc05c30e0a5261c028812645a122e22ea20816678df02967c1b23bd72"; 36 | // uint constant beta = "0x7ae96a2b657c07106e64479eac3434e99cf0497512f58995c1396c28719501ee"; 37 | 38 | /// @dev See Curve.onCurve 39 | function onCurve(uint[2] P) constant returns (bool) { 40 | uint p = pp; 41 | if (0 == P[0] || P[0] == p || 0 == P[1] || P[1] == p) 42 | return false; 43 | uint LHS = mulmod(P[1], P[1], p); 44 | uint RHS = addmod(mulmod(mulmod(P[0], P[0], p), P[0], p), 7, p); 45 | return LHS == RHS; 46 | } 47 | 48 | /// @dev See Curve.isPubKey 49 | function isPubKey(uint[2] memory P) constant returns (bool isPK) { 50 | isPK = onCurve(P); 51 | } 52 | 53 | /// @dev See Curve.validateSignature 54 | function validateSignature(bytes32 message, uint[2] rs, uint[2] Q) constant returns (bool) { 55 | uint n = nn; 56 | uint p = pp; 57 | if(rs[0] == 0 || rs[0] >= n || rs[1] == 0 || rs[1] > lowSmax) 58 | return false; 59 | if (!isPubKey(Q)) 60 | return false; 61 | 62 | uint sInv = ECCMath.invmod(rs[1], n); 63 | uint[3] memory u1G = _mul(mulmod(uint(message), sInv, n), [Gx, Gy]); 64 | uint[3] memory u2Q = _mul(mulmod(rs[0], sInv, n), Q); 65 | uint[3] memory P = _add(u1G, u2Q); 66 | 67 | if (P[2] == 0) 68 | return false; 69 | 70 | uint Px = ECCMath.invmod(P[2], p); // need Px/Pz^2 71 | Px = mulmod(P[0], mulmod(Px, Px, p), p); 72 | return Px % n == rs[0]; 73 | } 74 | 75 | /// @dev See Curve.compress 76 | function compress(uint[2] P) constant returns (uint8 yBit, uint x) { 77 | x = P[0]; 78 | yBit = P[1] & 1 == 1 ? 1 : 0; 79 | } 80 | 81 | /// @dev See Curve.decompress 82 | function decompress(uint8 yBit, uint x) constant returns (uint[2] P) { 83 | uint p = pp; 84 | var y2 = addmod(mulmod(x, mulmod(x, x, p), p), 7, p); 85 | var y_ = ECCMath.expmod(y2, (p + 1) / 4, p); 86 | uint cmp = yBit ^ y_ & 1; 87 | P[0] = x; 88 | P[1] = (cmp == 0) ? y_ : p - y_; 89 | } 90 | 91 | // Point addition, P + Q 92 | // inData: Px, Py, Pz, Qx, Qy, Qz 93 | // outData: Rx, Ry, Rz 94 | function _add(uint[3] memory P, uint[3] memory Q) constant returns (uint[3] memory R) { 95 | if(P[2] == 0) 96 | return Q; 97 | if(Q[2] == 0) 98 | return P; 99 | uint p = pp; 100 | uint[4] memory zs; // Pz^2, Pz^3, Qz^2, Qz^3 101 | zs[0] = mulmod(P[2], P[2], p); 102 | zs[1] = mulmod(P[2], zs[0], p); 103 | zs[2] = mulmod(Q[2], Q[2], p); 104 | zs[3] = mulmod(Q[2], zs[2], p); 105 | uint[4] memory us = [ 106 | mulmod(P[0], zs[2], p), 107 | mulmod(P[1], zs[3], p), 108 | mulmod(Q[0], zs[0], p), 109 | mulmod(Q[1], zs[1], p) 110 | ]; // Pu, Ps, Qu, Qs 111 | if (us[0] == us[2]) { 112 | if (us[1] != us[3]) 113 | return; 114 | else { 115 | return _double(P); 116 | } 117 | } 118 | uint h = addmod(us[2], p - us[0], p); 119 | uint r = addmod(us[3], p - us[1], p); 120 | uint h2 = mulmod(h, h, p); 121 | uint h3 = mulmod(h2, h, p); 122 | uint Rx = addmod(mulmod(r, r, p), p - h3, p); 123 | Rx = addmod(Rx, p - mulmod(2, mulmod(us[0], h2, p), p), p); 124 | R[0] = Rx; 125 | R[1] = mulmod(r, addmod(mulmod(us[0], h2, p), p - Rx, p), p); 126 | R[1] = addmod(R[1], p - mulmod(us[1], h3, p), p); 127 | R[2] = mulmod(h, mulmod(P[2], Q[2], p), p); 128 | } 129 | 130 | // Point addition, P + Q. P Jacobian, Q affine. 131 | // inData: Px, Py, Pz, Qx, Qy 132 | // outData: Rx, Ry, Rz 133 | function _addMixed(uint[3] memory P, uint[2] memory Q) constant returns (uint[3] memory R) { 134 | if(P[2] == 0) 135 | return [Q[0], Q[1], 1]; 136 | if(Q[1] == 0) 137 | return P; 138 | uint p = pp; 139 | uint[2] memory zs; // Pz^2, Pz^3, Qz^2, Qz^3 140 | zs[0] = mulmod(P[2], P[2], p); 141 | zs[1] = mulmod(P[2], zs[0], p); 142 | uint[4] memory us = [ 143 | P[0], 144 | P[1], 145 | mulmod(Q[0], zs[0], p), 146 | mulmod(Q[1], zs[1], p) 147 | ]; // Pu, Ps, Qu, Qs 148 | if (us[0] == us[2]) { 149 | if (us[1] != us[3]) { 150 | P[0] = 0; 151 | P[1] = 0; 152 | P[2] = 0; 153 | return; 154 | } 155 | else { 156 | _double(P); 157 | return; 158 | } 159 | } 160 | uint h = addmod(us[2], p - us[0], p); 161 | uint r = addmod(us[3], p - us[1], p); 162 | uint h2 = mulmod(h, h, p); 163 | uint h3 = mulmod(h2, h, p); 164 | uint Rx = addmod(mulmod(r, r, p), p - h3, p); 165 | Rx = addmod(Rx, p - mulmod(2, mulmod(us[0], h2, p), p), p); 166 | R[0] = Rx; 167 | R[1] = mulmod(r, addmod(mulmod(us[0], h2, p), p - Rx, p), p); 168 | R[1] = addmod(R[1], p - mulmod(us[1], h3, p), p); 169 | R[2] = mulmod(h, P[2], p); 170 | } 171 | 172 | // Same as addMixed but params are different and mutates P. 173 | function _addMixedM(uint[3] memory P, uint[2] memory Q) constant { 174 | if(P[1] == 0) { 175 | P[0] = Q[0]; 176 | P[1] = Q[1]; 177 | P[2] = 1; 178 | return; 179 | } 180 | if(Q[1] == 0) 181 | return; 182 | uint p = pp; 183 | uint[2] memory zs; // Pz^2, Pz^3, Qz^2, Qz^3 184 | zs[0] = mulmod(P[2], P[2], p); 185 | zs[1] = mulmod(P[2], zs[0], p); 186 | uint[4] memory us = [ 187 | P[0], 188 | P[1], 189 | mulmod(Q[0], zs[0], p), 190 | mulmod(Q[1], zs[1], p) 191 | ]; // Pu, Ps, Qu, Qs 192 | if (us[0] == us[2]) { 193 | if (us[1] != us[3]) { 194 | P[0] = 0; 195 | P[1] = 0; 196 | P[2] = 0; 197 | return; 198 | } 199 | else { 200 | _doubleM(P); 201 | return; 202 | } 203 | } 204 | uint h = addmod(us[2], p - us[0], p); 205 | uint r = addmod(us[3], p - us[1], p); 206 | uint h2 = mulmod(h, h, p); 207 | uint h3 = mulmod(h2, h, p); 208 | uint Rx = addmod(mulmod(r, r, p), p - h3, p); 209 | Rx = addmod(Rx, p - mulmod(2, mulmod(us[0], h2, p), p), p); 210 | P[0] = Rx; 211 | P[1] = mulmod(r, addmod(mulmod(us[0], h2, p), p - Rx, p), p); 212 | P[1] = addmod(P[1], p - mulmod(us[1], h3, p), p); 213 | P[2] = mulmod(h, P[2], p); 214 | } 215 | 216 | // Point doubling, 2*P 217 | // Params: Px, Py, Pz 218 | // Not concerned about the 1 extra mulmod. 219 | function _double(uint[3] memory P) constant returns (uint[3] memory Q) { 220 | uint p = pp; 221 | if (P[2] == 0) 222 | return; 223 | uint Px = P[0]; 224 | uint Py = P[1]; 225 | uint Py2 = mulmod(Py, Py, p); 226 | uint s = mulmod(4, mulmod(Px, Py2, p), p); 227 | uint m = mulmod(3, mulmod(Px, Px, p), p); 228 | var Qx = addmod(mulmod(m, m, p), p - addmod(s, s, p), p); 229 | Q[0] = Qx; 230 | Q[1] = addmod(mulmod(m, addmod(s, p - Qx, p), p), p - mulmod(8, mulmod(Py2, Py2, p), p), p); 231 | Q[2] = mulmod(2, mulmod(Py, P[2], p), p); 232 | } 233 | 234 | // Same as double but mutates P and is only. 235 | function _doubleM(uint[3] memory P) constant { 236 | uint p = pp; 237 | if (P[2] == 0) 238 | return; 239 | uint Px = P[0]; 240 | uint Py = P[1]; 241 | uint Py2 = mulmod(Py, Py, p); 242 | uint s = mulmod(4, mulmod(Px, Py2, p), p); 243 | uint m = mulmod(3, mulmod(Px, Px, p), p); 244 | var PxTemp = addmod(mulmod(m, m, p), p - addmod(s, s, p), p); 245 | P[0] = PxTemp; 246 | P[1] = addmod(mulmod(m, addmod(s, p - PxTemp, p), p), p - mulmod(8, mulmod(Py2, Py2, p), p), p); 247 | P[2] = mulmod(2, mulmod(Py, P[2], p), p); 248 | } 249 | 250 | // Multiplication dP. P affine, wNAF: w=5 251 | // Params: d, Px, Py 252 | // Output: Jacobian Q 253 | function _mul(uint d, uint[2] memory P) constant returns (uint[3] memory Q) { 254 | uint p = pp; 255 | if (d == 0) // TODO 256 | return; 257 | uint dwPtr; // points to array of NAF coefficients. 258 | uint i; 259 | 260 | // wNAF 261 | assembly 262 | { 263 | let dm := 0 264 | dwPtr := mload(0x40) 265 | mstore(0x40, add(dwPtr, 512)) // Should lower this. 266 | loop: 267 | jumpi(loop_end, iszero(d)) 268 | jumpi(even, iszero(and(d, 1))) 269 | dm := mod(d, 32) 270 | mstore8(add(dwPtr, i), dm) // Don't store as signed - convert when reading. 271 | d := add(sub(d, dm), mul(gt(dm, 16), 32)) 272 | even: 273 | d := div(d, 2) 274 | i := add(i, 1) 275 | jump(loop) 276 | loop_end: 277 | } 278 | 279 | // Pre calculation 280 | uint[3][8] memory PREC; // P, 3P, 5P, 7P, 9P, 11P, 13P, 15P 281 | PREC[0] = [P[0], P[1], 1]; 282 | var X = _double(PREC[0]); 283 | PREC[1] = _addMixed(X, P); 284 | PREC[2] = _add(X, PREC[1]); 285 | PREC[3] = _add(X, PREC[2]); 286 | PREC[4] = _add(X, PREC[3]); 287 | PREC[5] = _add(X, PREC[4]); 288 | PREC[6] = _add(X, PREC[5]); 289 | PREC[7] = _add(X, PREC[6]); 290 | 291 | uint[16] memory INV; 292 | INV[0] = PREC[1][2]; // a1 293 | INV[1] = mulmod(PREC[2][2], INV[0], p); // a2 294 | INV[2] = mulmod(PREC[3][2], INV[1], p); // a3 295 | INV[3] = mulmod(PREC[4][2], INV[2], p); // a4 296 | INV[4] = mulmod(PREC[5][2], INV[3], p); // a5 297 | INV[5] = mulmod(PREC[6][2], INV[4], p); // a6 298 | INV[6] = mulmod(PREC[7][2], INV[5], p); // a7 299 | 300 | INV[7] = ECCMath.invmod(INV[6], p); // a7inv 301 | INV[8] = INV[7]; // aNinv (a7inv) 302 | 303 | INV[15] = mulmod(INV[5], INV[8], p); // z7inv 304 | for(uint k = 6; k >= 2; k--) { // z6inv to z2inv 305 | INV[8] = mulmod(PREC[k + 1][2], INV[8], p); 306 | INV[8 + k] = mulmod(INV[k - 2], INV[8], p); 307 | } 308 | INV[9] = mulmod(PREC[2][2], INV[8], p); // z1Inv 309 | for(k = 0; k < 7; k++) { 310 | ECCMath.toZ1(PREC[k + 1], INV[k + 9], mulmod(INV[k + 9], INV[k + 9], p), p); 311 | } 312 | 313 | // Mult loop 314 | while(i > 0) { 315 | uint dj; 316 | uint pIdx; 317 | i--; 318 | assembly { 319 | dj := byte(0, mload(add(dwPtr, i))) 320 | } 321 | _doubleM(Q); 322 | if (dj > 16) { 323 | pIdx = (31 - dj) / 2; // These are the "negative ones", so invert y. 324 | _addMixedM(Q, [PREC[pIdx][0], p - PREC[pIdx][1]]); 325 | } 326 | else if (dj > 0) { 327 | pIdx = (dj - 1) / 2; 328 | _addMixedM(Q, [PREC[pIdx][0], PREC[pIdx][1]]); 329 | } 330 | } 331 | } 332 | 333 | } 334 | -------------------------------------------------------------------------------- /locations.txt: -------------------------------------------------------------------------------- 1 | localhost:8000 2 | localhost:8001 3 | localhost:8002 4 | localhost:8003 5 | -------------------------------------------------------------------------------- /populus.json: -------------------------------------------------------------------------------- 1 | { 2 | "chains": { 3 | "mainnet": { 4 | "chain": { 5 | "class": "populus.chain.geth.MainnetChain" 6 | }, 7 | "contracts": { 8 | "backends": { 9 | "JSONFile": { 10 | "$ref": "contracts.backends.JSONFile" 11 | }, 12 | "Memory": { 13 | "$ref": "contracts.backends.Memory" 14 | }, 15 | "ProjectContracts": { 16 | "$ref": "contracts.backends.ProjectContracts" 17 | }, 18 | "TestContracts": { 19 | "$ref": "contracts.backends.TestContracts" 20 | } 21 | } 22 | }, 23 | "web3": { 24 | "$ref": "web3.GethIPC" 25 | } 26 | }, 27 | "ropsten": { 28 | "chain": { 29 | "class": "populus.chain.geth.TestnetChain" 30 | }, 31 | "contracts": { 32 | "backends": { 33 | "JSONFile": { 34 | "$ref": "contracts.backends.JSONFile" 35 | }, 36 | "Memory": { 37 | "$ref": "contracts.backends.Memory" 38 | }, 39 | "ProjectContracts": { 40 | "$ref": "contracts.backends.ProjectContracts" 41 | }, 42 | "TestContracts": { 43 | "$ref": "contracts.backends.TestContracts" 44 | } 45 | } 46 | }, 47 | "web3": { 48 | "$ref": "web3.GethIPC" 49 | } 50 | }, 51 | "temp": { 52 | "chain": { 53 | "class": "populus.chain.geth.TemporaryGethChain" 54 | }, 55 | "contracts": { 56 | "backends": { 57 | "Memory": { 58 | "$ref": "contracts.backends.Memory" 59 | }, 60 | "ProjectContracts": { 61 | "$ref": "contracts.backends.ProjectContracts" 62 | }, 63 | "TestContracts": { 64 | "$ref": "contracts.backends.TestContracts" 65 | } 66 | } 67 | }, 68 | "web3": { 69 | "$ref": "web3.GethIPC" 70 | } 71 | }, 72 | "tester": { 73 | "chain": { 74 | "class": "populus.chain.tester.TesterChain" 75 | }, 76 | "contracts": { 77 | "backends": { 78 | "Memory": { 79 | "$ref": "contracts.backends.Memory" 80 | }, 81 | "ProjectContracts": { 82 | "$ref": "contracts.backends.ProjectContracts" 83 | }, 84 | "TestContracts": { 85 | "$ref": "contracts.backends.TestContracts" 86 | } 87 | } 88 | }, 89 | "web3": { 90 | "$ref": "web3.Tester" 91 | } 92 | }, 93 | "testrpc": { 94 | "chain": { 95 | "class": "populus.chain.testrpc.TestRPCChain" 96 | }, 97 | "contracts": { 98 | "backends": { 99 | "Memory": { 100 | "$ref": "contracts.backends.Memory" 101 | }, 102 | "ProjectContracts": { 103 | "$ref": "contracts.backends.ProjectContracts" 104 | }, 105 | "TestContracts": { 106 | "$ref": "contracts.backends.TestContracts" 107 | } 108 | } 109 | }, 110 | "web3": { 111 | "$ref": "web3.TestRPC" 112 | } 113 | } 114 | }, 115 | "compilation": { 116 | "backend": { 117 | "$ref": "compilation.backends.SolcAutoBackend" 118 | }, 119 | "backends": { 120 | "SolcAutoBackend": { 121 | "class": "populus.compilation.backends.SolcAutoBackend", 122 | "settings": { 123 | "optimize": true, 124 | "output_values": [ 125 | "abi", 126 | "bin", 127 | "bin-runtime", 128 | "metadata" 129 | ] 130 | } 131 | }, 132 | "SolcCombinedJSON": { 133 | "class": "populus.compilation.backends.SolcCombinedJSONBackend", 134 | "settings": { 135 | "optimize": true, 136 | "output_values": [ 137 | "abi", 138 | "bin", 139 | "bin-runtime", 140 | "metadata" 141 | ] 142 | } 143 | }, 144 | "SolcStandardJSON": { 145 | "class": "populus.compilation.backends.SolcStandardJSONBackend", 146 | "settings": { 147 | "stdin": { 148 | "optimizer": { 149 | "enabled": true, 150 | "runs": 500 151 | }, 152 | "outputSelection": { 153 | "*": { 154 | "*": [ 155 | "abi", 156 | "metadata", 157 | "evm" 158 | ] 159 | } 160 | } 161 | } 162 | } 163 | } 164 | }, 165 | "contracts_source_dirs": [ 166 | "./contracts" 167 | ], 168 | "import_remappings": [ 169 | "github.com/androlo/standard-contracts/contracts/src/=contracts/" 170 | ] 171 | }, 172 | "contracts": { 173 | "backends": { 174 | "JSONFile": { 175 | "class": "populus.contracts.backends.filesystem.JSONFileBackend", 176 | "priority": 10, 177 | "settings": { 178 | "file_path": "./registrar.json" 179 | } 180 | }, 181 | "Memory": { 182 | "class": "populus.contracts.backends.memory.MemoryBackend", 183 | "priority": 50 184 | }, 185 | "ProjectContracts": { 186 | "class": "populus.contracts.backends.project.ProjectContractsBackend", 187 | "priority": 20 188 | }, 189 | "TestContracts": { 190 | "class": "populus.contracts.backends.testing.TestContractsBackend", 191 | "priority": 40 192 | } 193 | } 194 | }, 195 | "version": "6", 196 | "web3": { 197 | "GethIPC": { 198 | "provider": { 199 | "class": "web3.providers.ipc.IPCProvider" 200 | } 201 | }, 202 | "InfuraMainnet": { 203 | "eth": { 204 | "default_account": "0x0000000000000000000000000000000000000001" 205 | }, 206 | "provider": { 207 | "class": "web3.providers.rpc.HTTPProvider", 208 | "settings": { 209 | "endpoint_uri": "https://mainnet.infura.io" 210 | } 211 | } 212 | }, 213 | "InfuraRopsten": { 214 | "eth": { 215 | "default_account": "0x0000000000000000000000000000000000000001" 216 | }, 217 | "provider": { 218 | "class": "web3.providers.rpc.HTTPProvider", 219 | "settings": { 220 | "endpoint_uri": "https://ropsten.infura.io" 221 | } 222 | } 223 | }, 224 | "TestRPC": { 225 | "provider": { 226 | "class": "web3.providers.tester.TestRPCProvider" 227 | } 228 | }, 229 | "Tester": { 230 | "provider": { 231 | "class": "web3.providers.tester.EthereumTesterProvider" 232 | } 233 | } 234 | } 235 | } -------------------------------------------------------------------------------- /pydkg/__init__.py: -------------------------------------------------------------------------------- 1 | # # KD 2 | # import pydkg 3 | # import functools 4 | # import itertools 5 | # from py_ecc.secp256k1 import secp256k1 6 | 7 | # num_participants = 8 8 | # threshold = 5 9 | # poly1 = pydkg.random_polynomial(threshold) 10 | # poly2 = pydkg.random_polynomial(threshold) 11 | # addresses = tuple(pydkg.random.randrange(2**160) for _ in range(num_participants)) 12 | # secret_shares1 = tuple(pydkg.eval_polynomial(poly1, addr) for addr in addresses) 13 | # secret_shares2 = tuple(pydkg.eval_polynomial(poly2, addr) for addr in addresses) 14 | # public_shares = tuple( 15 | # secp256k1.add(secp256k1.multiply(secp256k1.G, a), secp256k1.multiply(pydkg.G2, b)) 16 | # for a, b in zip(poly1, poly2)) 17 | 18 | # # KV 19 | # verification_lhs = tuple( 20 | # secp256k1.add(secp256k1.multiply(secp256k1.G, sh1), secp256k1.multiply(pydkg.G2, sh2)) 21 | # for sh1, sh2 in zip(secret_shares1, secret_shares2)) 22 | # verification_rhs = tuple( 23 | # functools.reduce(secp256k1.add, ( 24 | # secp256k1.multiply(ps, pow(addr, k, secp256k1.N)) 25 | # for k, ps in enumerate(public_shares))) 26 | # for addr in addresses) 27 | 28 | # verification_lhs == verification_rhs 29 | 30 | # # KC 31 | # # will need network protocol to test 32 | 33 | # # KG 34 | # public_key_part = secp256k1.multiply(secp256k1.G, poly1[0]) 35 | 36 | # # Lagrangian interpolation test 37 | # secp256k1.multiply(secp256k1.G, tuple( 38 | # sum(sh * functools.reduce( 39 | # lambda a, b: a * b % secp256k1.N, 40 | # (addr2 * pow(addr2 - addr, secp256k1.N-2, secp256k1.N) for (addr2, _) in addr_sh_combos if addr2 != addr)) 41 | # for (addr, sh) in addr_sh_combos) % secp256k1.N 42 | # for addr_sh_combos in itertools.combinations(zip(addresses, secret_shares1), threshold))[0]) == public_key_part 43 | -------------------------------------------------------------------------------- /pydkg/__main__.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import asyncio 3 | import logging 4 | import signal 5 | import sys 6 | 7 | from py_ecc.secp256k1 import secp256k1 8 | 9 | from . import util, networking, ecdkg, db 10 | 11 | 12 | def main(): 13 | parser = argparse.ArgumentParser(prog='pydkg', description='Distributedly generate some keys yo') 14 | parser.add_argument('--host', nargs='?', default='0.0.0.0', 15 | help='Hostname to serve on (default: %(default)s)') 16 | parser.add_argument('-p', '--port', type=int, nargs='?', default=8000, 17 | help='Port no. to serve on (default: %(default)s)') 18 | parser.add_argument('--log-level', type=int, nargs='?', default=logging.INFO, 19 | help='Logging level (default: %(default)s)') 20 | parser.add_argument('--log-format', nargs='?', default='%(message)s', 21 | help='Logging message format (default: %(default)s)') 22 | parser.add_argument('--private-key-file', nargs='?', default='private.key', 23 | help='File to load private key from (default: %(default)s)') 24 | parser.add_argument('--addresses-file', nargs='?', default='addresses.txt', 25 | help='File to load accepted eth addresses from (default: %(default)s)') 26 | parser.add_argument('--locations-file', nargs='?', default='locations.txt', 27 | help='File containing network locations to attempt connecting with (default: %(default)s)') 28 | args = parser.parse_args() 29 | 30 | # args parsed; begin getting config stuff 31 | logging.basicConfig(level=args.log_level, format=args.log_format) 32 | 33 | private_key = util.get_or_generate_private_value(args.private_key_file) 34 | accepted_addresses = util.get_addresses(args.addresses_file) 35 | locations = util.get_locations(args.locations_file) 36 | 37 | # initialize some stuff 38 | db.init() 39 | 40 | node = ecdkg.ECDKGNode.get_by_private_key(private_key) 41 | accepted_addresses.difference_update((node.address,)) 42 | 43 | # display some info and initialize stuff 44 | logging.debug('own pubkey: ({0[0]:064x}, {0[1]:064x})'.format(node.public_key)) 45 | logging.info('own address: {:040x}'.format(node.address)) 46 | if accepted_addresses: 47 | logging.info('accepted addresses: {{\n {}\n}}'.format( 48 | '\n '.join('{:040x}'.format(a) for a in accepted_addresses))) 49 | else: 50 | logging.warn('not accepting any addresses') 51 | 52 | # get the asyncio event loop 53 | loop = asyncio.get_event_loop() 54 | 55 | # setup shutdown handlers 56 | def shutdown(): 57 | logging.info('\nShutting down...') 58 | loop.stop() 59 | 60 | for signum in (signal.SIGINT, signal.SIGTERM): 61 | loop.add_signal_handler(signum, shutdown) 62 | 63 | # main program loop 64 | loop.run_until_complete(networking.server(args.host, args.port, node, accepted_addresses, loop=loop)) 65 | for hostname, port in locations: 66 | loop.create_task(networking.attempt_to_establish_channel(hostname, port, node, accepted_addresses)) 67 | loop.create_task(networking.emit_heartbeats()) 68 | 69 | try: 70 | loop.run_forever() 71 | finally: 72 | for task in asyncio.Task.all_tasks(loop): 73 | task.cancel() 74 | loop.run_until_complete(loop.shutdown_asyncgens()) 75 | loop.close() 76 | logging.info('Goodbye') 77 | 78 | 79 | if __name__ == '__main__': 80 | main() 81 | -------------------------------------------------------------------------------- /pydkg/crypto.py: -------------------------------------------------------------------------------- 1 | from py_ecc.secp256k1 import secp256k1 2 | import sha3 3 | from . import util 4 | 5 | 6 | def encrypt(message: bytes, enckey: (int, int)) -> bytes: 7 | # This is an implementation of ECIES 8 | # https://en.wikipedia.org/wiki/Integrated_Encryption_Scheme 9 | util.validate_curve_point(enckey) 10 | r = util.random_private_value() 11 | R = secp256k1.multiply(secp256k1.G, r) 12 | S = secp256k1.multiply(enckey, r) 13 | kEkM = sha3.keccak_256(S[0].to_bytes(32, byteorder='big')).digest() 14 | kE, kM = kEkM[0:16], kEkM[16:32] 15 | 16 | # Use CTR mode to do encryption 256 bits at a time 17 | # https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#CTR 18 | num_trunc_bytes = (32 - len(message)) % 32 19 | iv = util.random.randrange(2**256).to_bytes(32, byteorder='big') 20 | num_chunks = len(message) // 32 + (1 if num_trunc_bytes > 0 else 0) 21 | c = b''.join(( 22 | int.from_bytes(message[32*i:32*(i+1)].ljust(32, b'\0'), 'big') ^ 23 | int.from_bytes(sha3.keccak_256(kE + iv + i.to_bytes(32, 'big')).digest(), 'big') 24 | ).to_bytes(32, byteorder='big') for i in range(num_chunks)) 25 | 26 | # Quote from http://keccak.noekeon.org/: 27 | # Unlike SHA-1 and SHA-2, Keccak does not have the length-extension weakness, 28 | # hence does not need the HMAC nested construction. Instead, MAC computation 29 | # can be performed by simply prepending the message with the key. 30 | d = sha3.keccak_256(kM + c).digest() 31 | return (util.curve_point_to_bytes(R) + # 64 byte ephemeral key 32 | bytes((num_trunc_bytes,)) + # 1 byte truncation descriptor 33 | iv + # 32 byte initialization vector 34 | c + # arbitrary length 32 byte aligned enciphered message 35 | d) # 32 byte message authentication code (MAC) 36 | 37 | 38 | def decrypt(ciphertext: bytes, deckey: int, foo=False) -> bytes: 39 | util.validate_private_value(deckey) 40 | R = util.bytes_to_curve_point(ciphertext[:64]) 41 | S = secp256k1.multiply(R, deckey) 42 | num_trunc_bytes = ord(ciphertext[64:65]) 43 | iv = ciphertext[65:97] 44 | c = ciphertext[97:-32] 45 | 46 | if len(c) % 32 != 0: 47 | raise ValueError('enciphered message not properly aligned') 48 | 49 | kEkM = sha3.keccak_256(S[0].to_bytes(32, byteorder='big')).digest() 50 | kE, kM = kEkM[0:16], kEkM[16:32] 51 | 52 | num_chunks = len(c) // 32 53 | message = b''.join(( 54 | int.from_bytes(c[32*i:32*(i+1)], 'big') ^ 55 | int.from_bytes(sha3.keccak_256(kE + iv + i.to_bytes(32, 'big')).digest(), 'big') 56 | ).to_bytes(32, byteorder='big') for i in range(num_chunks)) 57 | 58 | if num_trunc_bytes > 0: 59 | message, padding = message[:-num_trunc_bytes], message[-num_trunc_bytes:] 60 | 61 | if padding != b'\0' * num_trunc_bytes: 62 | raise ValueError('invalid padding') 63 | 64 | d = ciphertext[-32:] 65 | if d != sha3.keccak_256(kM + c).digest(): 66 | raise ValueError('message authentication code does not match') 67 | 68 | if foo: 69 | return int.from_bytes(sha3.keccak_256(message).digest(), 'big') 70 | 71 | return message 72 | -------------------------------------------------------------------------------- /pydkg/db.py: -------------------------------------------------------------------------------- 1 | import sqlalchemy.types as types 2 | from sqlalchemy import create_engine 3 | from sqlalchemy.orm import scoped_session, sessionmaker 4 | from sqlalchemy import Column, Integer 5 | from sqlalchemy.ext.declarative import declarative_base, declared_attr 6 | 7 | from . import util 8 | 9 | 10 | class Base(object): 11 | @declared_attr 12 | def __tablename__(cls): 13 | return cls.__name__.lower() 14 | 15 | id = Column(Integer, primary_key=True) 16 | 17 | 18 | Base = declarative_base(cls=Base) 19 | 20 | 21 | def init(): 22 | global engine, Session 23 | engine = create_engine('sqlite:///:memory:') 24 | Session = scoped_session(sessionmaker(engine)) 25 | Base.metadata.create_all(engine) 26 | 27 | 28 | class PrivateValue(types.TypeDecorator): 29 | impl = types.LargeBinary 30 | python_type = int 31 | 32 | def process_bind_param(self, value, dialect): 33 | if value is not None: 34 | return util.private_value_to_bytes(value) 35 | 36 | def process_result_value(self, value, dialect): 37 | if value is not None: 38 | return util.bytes_to_private_value(value) 39 | 40 | 41 | class CurvePoint(types.TypeDecorator): 42 | impl = types.LargeBinary 43 | python_type = tuple # (int, int) 44 | 45 | def process_bind_param(self, value, dialect): 46 | if value is not None: 47 | return util.curve_point_to_bytes(value) 48 | 49 | def process_result_value(self, value, dialect): 50 | if value is not None: 51 | return util.bytes_to_curve_point(value) 52 | 53 | 54 | class Signature(types.TypeDecorator): 55 | impl = types.LargeBinary 56 | python_type = tuple # rsv (int, int, int) 57 | 58 | def process_bind_param(self, value, dialect): 59 | if value is not None: 60 | return util.signature_to_bytes(value) 61 | 62 | def process_result_value(self, value, dialect): 63 | if value is not None: 64 | return util.bytes_to_signature(value) 65 | 66 | 67 | class EthAddress(types.TypeDecorator): 68 | impl = types.LargeBinary 69 | python_type = int 70 | 71 | def process_bind_param(self, value, dialect): 72 | if value is not None: 73 | return util.address_to_bytes(value) 74 | 75 | def process_result_value(self, value, dialect): 76 | if value is not None: 77 | return util.bytes_to_address(value) 78 | 79 | 80 | class Polynomial(types.TypeDecorator): 81 | impl = types.LargeBinary 82 | python_type = tuple 83 | 84 | def process_bind_param(self, value, dialect): 85 | if value is not None: 86 | return util.polynomial_to_bytes(value) 87 | 88 | def process_result_value(self, value, dialect): 89 | if value is not None: 90 | return util.bytes_to_polynomial(value) 91 | 92 | 93 | class CurvePointTuple(types.TypeDecorator): 94 | impl = types.LargeBinary 95 | python_type = tuple # of int pairs 96 | 97 | def process_bind_param(self, value, dialect): 98 | if value is not None: 99 | return util.curve_point_tuple_to_bytes(value) 100 | 101 | def process_result_value(self, value, dialect): 102 | if value is not None: 103 | return util.bytes_to_curve_point_tuple(value) 104 | -------------------------------------------------------------------------------- /pydkg/ecdkg.py: -------------------------------------------------------------------------------- 1 | import enum 2 | import functools 3 | import logging 4 | import math 5 | 6 | from py_ecc.secp256k1 import secp256k1 7 | from sqlalchemy import types 8 | from sqlalchemy.schema import Column, ForeignKey, UniqueConstraint 9 | from sqlalchemy.orm import relationship 10 | 11 | from . import db, util, networking 12 | 13 | COMS_TIMEOUT = 10 14 | THRESHOLD_FACTOR = .5 15 | # NOTE: As soon as I end the python shell session that I created this in 16 | # and the RAM for that session gets reused, the scalar used to produce 17 | # this point probably won't come into existence again. 18 | # TODO: reroll this point in dark ritual ala Zcash zkSNARK toxic waste thing 19 | # ... not that this parameter creates _much_ more security for this 20 | # protocol, but it's applicable and could be hilarious if you don't 21 | # believe the above note. 22 | G2 = (0xb25b5ea8b8b230e5574fec0182e809e3455701323968c602ab56b458d0ba96bf, 23 | 0x13edfe75e1c88e030eda220ffc74802144aec67c4e51cb49699d4401c122e19c) 24 | util.validate_curve_point(G2) 25 | 26 | 27 | def random_polynomial(order: int) -> tuple: 28 | return tuple(util.random_private_value() for _ in range(order)) 29 | 30 | 31 | def eval_polynomial(poly: tuple, x: int) -> int: 32 | return sum(c * pow(x, k, secp256k1.N) for k, c in enumerate(poly)) % secp256k1.N 33 | 34 | 35 | def generate_public_shares(poly1, poly2): 36 | if len(poly1) != len(poly2): 37 | raise ValueError('polynomial lengths must match ({} != {})'.format(len(poly1), len(poly2))) 38 | 39 | return (secp256k1.add(secp256k1.multiply(secp256k1.G, a), secp256k1.multiply(G2, b)) for a, b in zip(poly1, poly2)) 40 | 41 | 42 | class ECDKGNode(db.Base): 43 | __tablename__ = 'ecdkg_node' 44 | 45 | private_key = Column(db.PrivateValue, index=True, unique=True) 46 | protocol_instances = relationship('ECDKG', back_populates='node') 47 | 48 | @property 49 | def public_key(self): 50 | return secp256k1.multiply(secp256k1.G, self.private_key) 51 | 52 | @property 53 | def address(self): 54 | return util.curve_point_to_eth_address(self.public_key) 55 | 56 | @classmethod 57 | def get_by_private_key(cls, private_key: int) -> 'ECDKGNode': 58 | node = ( 59 | db.Session 60 | .query(cls) 61 | .filter(cls.private_key == private_key) 62 | .scalar() 63 | ) 64 | 65 | if node is None: 66 | node = ECDKGNode(private_key=private_key) 67 | db.Session.add(node) 68 | db.Session.commit() 69 | 70 | return node 71 | 72 | def get_protocol_instance_by_decryption_condition(self, decryption_condition: str) -> 'ECDKG': 73 | decryption_condition = util.normalize_decryption_condition(decryption_condition) 74 | ecdkg_obj = ( 75 | db.Session 76 | .query(ECDKG) 77 | .filter( 78 | ECDKG.node_id == self.id, 79 | ECDKG.decryption_condition == decryption_condition, 80 | ) 81 | .scalar() 82 | ) 83 | 84 | if ecdkg_obj is None: 85 | ecdkg_obj = ECDKG(node_id=self.id, decryption_condition=decryption_condition) 86 | db.Session.add(ecdkg_obj) 87 | ecdkg_obj.init() 88 | db.Session.commit() 89 | 90 | return ecdkg_obj 91 | 92 | 93 | @enum.unique 94 | class ECDKGPhase(enum.IntEnum): 95 | uninitialized = 0 96 | key_distribution = 1 97 | key_verification = 2 98 | key_check = 3 99 | key_generation = 4 100 | key_publication = 5 101 | complete = 6 102 | 103 | 104 | class ECDKG(db.Base): 105 | __tablename__ = 'ecdkg' 106 | 107 | node_id = Column(types.Integer, ForeignKey('ecdkg_node.id')) 108 | node = relationship('ECDKGNode', back_populates='protocol_instances') 109 | 110 | decryption_condition = Column(types.String(32), index=True) 111 | phase = Column(types.Enum(ECDKGPhase), nullable=False, default=ECDKGPhase.uninitialized) 112 | threshold = Column(types.Integer) 113 | encryption_key = Column(db.CurvePoint) 114 | decryption_key = Column(db.PrivateValue) 115 | participants = relationship('ECDKGParticipant', back_populates='ecdkg') 116 | 117 | secret_poly1 = Column(db.Polynomial) 118 | secret_poly2 = Column(db.Polynomial) 119 | verification_points = Column(db.CurvePointTuple) 120 | encryption_key_vector = Column(db.CurvePointTuple) 121 | 122 | __table_args__ = (UniqueConstraint('node_id', 'decryption_condition'),) 123 | 124 | def init(self): 125 | for addr in networking.channels.keys(): 126 | self.get_or_create_participant_by_address(addr) 127 | 128 | # everyone should on agree on participants 129 | self.threshold = math.ceil(THRESHOLD_FACTOR * (len(self.participants)+1)) 130 | 131 | spoly1 = random_polynomial(self.threshold) 132 | spoly2 = random_polynomial(self.threshold) 133 | 134 | self.secret_poly1 = spoly1 135 | self.secret_poly2 = spoly2 136 | 137 | self.encryption_key_vector = tuple(secp256k1.multiply(secp256k1.G, coeff) for coeff in self.secret_poly1) 138 | 139 | self.verification_points = tuple( 140 | secp256k1.add(secp256k1.multiply(secp256k1.G, a), secp256k1.multiply(G2, b)) 141 | for a, b in zip(spoly1, spoly2) 142 | ) 143 | 144 | self.phase = ECDKGPhase.key_distribution 145 | 146 | def process_advance_to_phase(self, target_phase: ECDKGPhase): 147 | if self.phase < target_phase: 148 | self.phase = target_phase 149 | db.Session.commit() 150 | 151 | def process_secret_shares(self, sender_address: int, secret_shares: (int, int), signature: 'rsv triplet'): 152 | own_address = self.node.address 153 | participant = self.get_participant_by_address(sender_address) 154 | share1, share2 = secret_shares 155 | 156 | msg_bytes = ( 157 | b'SECRETSHARES' + 158 | self.decryption_condition.encode() + 159 | util.address_to_bytes(own_address) + 160 | util.private_value_to_bytes(share1) + 161 | util.private_value_to_bytes(share2) 162 | ) 163 | 164 | recovered_address = util.address_from_message_and_signature(msg_bytes, signature) 165 | 166 | if sender_address != recovered_address: 167 | raise ValueError( 168 | 'sender address {:040x} does not match recovered address {:040x}' 169 | .format(sender_address, recovered_address) 170 | ) 171 | 172 | if participant.secret_share1 is None and participant.secret_share2 is None: 173 | participant.secret_share1 = share1 174 | participant.secret_share2 = share2 175 | participant.shares_signature = signature 176 | 177 | db.Session.commit() 178 | 179 | if participant.verification_points is not None: 180 | self.process_secret_share_verification(sender_address) 181 | 182 | elif participant.secret_share1 != share1 or participant.secret_share2 != share2: 183 | participant.get_or_create_complaint_by_complainer_address(own_address) 184 | raise ValueError( 185 | '{:040x} sent shares for {} which do not match: {} != {}' 186 | .format( 187 | sender_address, 188 | self.decryption_condition, 189 | (participant.secret_share1, participant.secret_share2), 190 | (share1, share2), 191 | ) 192 | ) 193 | 194 | def process_verification_points(self, sender_address: int, verification_points: tuple, signature: 'rsv triplet'): 195 | own_address = self.node.address 196 | participant = self.get_participant_by_address(sender_address) 197 | 198 | msg_bytes = ( 199 | b'VERIFICATIONPOINTS' + 200 | self.decryption_condition.encode() + 201 | util.curve_point_tuple_to_bytes(verification_points) 202 | ) 203 | 204 | recovered_address = util.address_from_message_and_signature(msg_bytes, signature) 205 | 206 | if sender_address != recovered_address: 207 | raise ValueError( 208 | 'sender address {:040x} does not match recovered address {:040x}' 209 | .format(sender_address, recovered_address) 210 | ) 211 | 212 | if participant.verification_points is None: 213 | participant.verification_points = verification_points 214 | participant.verification_points_signature = signature 215 | 216 | db.Session.commit() 217 | 218 | if participant.secret_share1 is not None and participant.secret_share2 is not None: 219 | self.process_secret_share_verification(sender_address) 220 | 221 | elif participant.verification_points != verification_points: 222 | participant.get_or_create_complaint_by_complainer_address(own_address) 223 | raise ValueError( 224 | '{:040x} sent verification points for {} which do not match: {} != {}' 225 | .format( 226 | sender_address, 227 | self.decryption_condition, 228 | participant.verification_points, 229 | verification_points, 230 | ) 231 | ) 232 | 233 | def process_secret_share_verification(self, address: int): 234 | own_address = self.node.address 235 | participant = self.get_participant_by_address(address) 236 | 237 | share1 = participant.secret_share1 238 | share2 = participant.secret_share2 239 | 240 | vlhs = secp256k1.add(secp256k1.multiply(secp256k1.G, share1), 241 | secp256k1.multiply(G2, share2)) 242 | vrhs = functools.reduce( 243 | secp256k1.add, 244 | (secp256k1.multiply(ps, pow(own_address, k, secp256k1.N)) 245 | for k, ps in enumerate(participant.verification_points))) 246 | 247 | if vlhs == vrhs: 248 | return 249 | 250 | participant.get_or_create_complaint_by_complainer_address(own_address) 251 | 252 | def process_encryption_key_vector(self, 253 | sender_address: int, 254 | encryption_key_vector: tuple, 255 | signature: 'rsv triplet'): 256 | own_address = self.node.address 257 | participant = self.get_participant_by_address(sender_address) 258 | 259 | msg_bytes = ( 260 | b'ENCRYPTIONKEYPART' + 261 | self.decryption_condition.encode() + 262 | util.curve_point_tuple_to_bytes(encryption_key_vector) 263 | ) 264 | 265 | recovered_address = util.address_from_message_and_signature(msg_bytes, signature) 266 | 267 | if sender_address != recovered_address: 268 | raise ValueError( 269 | 'sender address {:040x} does not match recovered address {:040x}' 270 | .format(sender_address, recovered_address) 271 | ) 272 | 273 | if participant.encryption_key_vector is None: 274 | lhs = secp256k1.multiply(secp256k1.G, participant.secret_share1) 275 | rhs = functools.reduce( 276 | secp256k1.add, 277 | (secp256k1.multiply(ps, pow(own_address, k, secp256k1.N)) 278 | for k, ps in enumerate(encryption_key_vector))) 279 | if lhs != rhs: 280 | participant.get_or_create_complaint_by_complainer_address(own_address) 281 | raise ValueError( 282 | '{:040x} sent enc key vector which does not match previously sent secret share' 283 | .format(sender_address) 284 | ) 285 | 286 | participant.encryption_key_vector = encryption_key_vector 287 | participant.encryption_key_vector_signature = signature 288 | 289 | if all(p.encryption_key_vector is not None for p in self.participants): 290 | self.encryption_key = functools.reduce( 291 | secp256k1.add, 292 | (p.encryption_key_vector[0] for p in self.participants), 293 | self.encryption_key_vector[0] 294 | ) 295 | 296 | db.Session.commit() 297 | elif participant.encryption_key_vector != encryption_key_vector: 298 | participant.get_or_create_complaint_by_complainer_address(own_address) 299 | raise ValueError( 300 | '{:040x} sent encryption key part for {} which do not match: {} != {}' 301 | .format( 302 | sender_address, 303 | self.decryption_condition, 304 | participant.encryption_key_vector, 305 | encryption_key_vector, 306 | ) 307 | ) 308 | 309 | def process_decryption_key_part(self, 310 | sender_address: int, 311 | decryption_key_part: int, 312 | signature: 'rsv triplet'): 313 | participant = self.get_participant_by_address(sender_address) 314 | 315 | msg_bytes = ( 316 | b'DECRYPTIONKEYPART' + 317 | self.decryption_condition.encode() + 318 | util.private_value_to_bytes(decryption_key_part) 319 | ) 320 | 321 | recovered_address = util.address_from_message_and_signature(msg_bytes, signature) 322 | 323 | if sender_address != recovered_address: 324 | raise ValueError( 325 | 'sender address {:040x} does not match recovered address {:040x}' 326 | .format(sender_address, recovered_address) 327 | ) 328 | 329 | if participant.decryption_key_part is None: 330 | if secp256k1.multiply(secp256k1.G, decryption_key_part) != participant.encryption_key_vector[0]: 331 | participant.get_or_create_complaint_by_complainer_address(own_address) 332 | raise ValueError( 333 | '{:040x} sent dec key part which does not match previously sent enc key vector' 334 | .format(sender_address) 335 | ) 336 | 337 | participant.decryption_key_part = decryption_key_part 338 | participant.decryption_key_part_signature = signature 339 | 340 | if all(p.decryption_key_part is not None for p in self.participants): 341 | self.decryption_key = ( 342 | sum(p.decryption_key_part for p in self.participants) + 343 | self.secret_poly1[0] 344 | ) % secp256k1.N 345 | 346 | db.Session.commit() 347 | elif participant.decryption_key_part != decryption_key_part: 348 | participant.get_or_create_complaint_by_complainer_address(own_address) 349 | raise ValueError( 350 | '{:040x} sent decryption key part for {} which do not match: {} != {}' 351 | .format( 352 | sender_address, 353 | self.decryption_condition, 354 | participant.decryption_key_part, 355 | decryption_key_part, 356 | ) 357 | ) 358 | 359 | async def run_until_phase(self, target_phase: ECDKGPhase): 360 | while self.phase < target_phase: 361 | logging.info('handling {} phase...'.format(self.phase.name)) 362 | await getattr(self, 'handle_{}_phase'.format(self.phase.name))() 363 | 364 | async def handle_key_distribution_phase(self): 365 | signed_secret_shares = await networking.broadcast_jsonrpc_call_on_all_channels( 366 | 'get_signed_secret_shares', self.decryption_condition) 367 | 368 | for participant in self.participants: 369 | address = participant.eth_address 370 | 371 | if address not in signed_secret_shares: 372 | logging.warning('missing shares from address {:040x}'.format(address)) 373 | continue 374 | 375 | try: 376 | self.process_secret_shares(address, *signed_secret_shares[address]) 377 | except Exception as e: 378 | logging.warning( 379 | 'exception occurred while processing secret shares from {:040x}: {}' 380 | .format(address, e) 381 | ) 382 | 383 | logging.info('set all secret shares') 384 | signed_verification_points = await networking.broadcast_jsonrpc_call_on_all_channels( 385 | 'get_signed_verification_points', self.decryption_condition) 386 | 387 | for participant in self.participants: 388 | address = participant.eth_address 389 | 390 | if address not in signed_verification_points: 391 | logging.warning('missing verification points from address {:040x}'.format(address)) 392 | continue 393 | 394 | try: 395 | self.process_verification_points(address, *signed_verification_points[address]) 396 | except Exception as e: 397 | logging.warning( 398 | 'exception occurred while processing verification points from {:040x}: {}' 399 | .format(address, e) 400 | ) 401 | 402 | self.process_advance_to_phase(ECDKGPhase.key_verification) 403 | 404 | async def handle_key_verification_phase(self): 405 | self.process_advance_to_phase(ECDKGPhase.key_check) 406 | 407 | async def handle_key_check_phase(self): 408 | complaints = await networking.broadcast_jsonrpc_call_on_all_channels( 409 | 'get_complaints', self.decryption_condition) 410 | 411 | for participant in self.participants: 412 | complainer_address = participant.eth_address 413 | 414 | if complainer_address not in complaints: 415 | logging.warning('missing complaints from address {:040x}'.format(complainer_address)) 416 | continue 417 | 418 | # TODO: Add complaints and collect responses to complaints 419 | 420 | self.process_advance_to_phase(ECDKGPhase.key_generation) 421 | 422 | async def handle_key_generation_phase(self): 423 | signed_encryption_key_vectors = await networking.broadcast_jsonrpc_call_on_all_channels( 424 | 'get_signed_encryption_key_vector', self.decryption_condition) 425 | 426 | for participant in self.participants: 427 | address = participant.eth_address 428 | 429 | if address not in signed_encryption_key_vectors: 430 | # TODO: this is supposed to be broadcast... maybe try getting it from other nodes instead? 431 | logging.warning('missing encryption key part from address {:040x}'.format(address)) 432 | continue 433 | 434 | try: 435 | self.process_encryption_key_vector(address, *signed_encryption_key_vectors[address]) 436 | except Exception as e: 437 | logging.warning( 438 | 'exception occurred while processing encryption key part from {:040x}: {}' 439 | .format(address, e) 440 | ) 441 | 442 | self.process_advance_to_phase(ECDKGPhase.key_publication) 443 | 444 | async def handle_key_publication_phase(self): 445 | await util.decryption_condition_satisfied(self.decryption_condition) 446 | 447 | signed_decryption_key_parts = await networking.broadcast_jsonrpc_call_on_all_channels( 448 | 'get_signed_decryption_key_part', self.decryption_condition) 449 | 450 | for p in self.participants: 451 | address = p.eth_address 452 | 453 | if address not in signed_decryption_key_parts: 454 | # TODO: switch to interpolation of secret shares if waiting doesn't work 455 | logging.warning('missing decryption key part from address {:040x}'.format(address)) 456 | continue 457 | 458 | try: 459 | self.process_decryption_key_part(address, *signed_decryption_key_parts[address]) 460 | except Exception as e: 461 | logging.warning( 462 | 'exception occurred while processing decryption key part from {:040x}: {}' 463 | .format(address, e) 464 | ) 465 | 466 | self.process_advance_to_phase(ECDKGPhase.complete) 467 | 468 | def get_participant_by_address(self, address: int) -> 'ECDKGParticipant': 469 | participant = ( 470 | db.Session 471 | .query(ECDKGParticipant) 472 | .filter(ECDKGParticipant.ecdkg_id == self.id, 473 | ECDKGParticipant.eth_address == address) 474 | .scalar() 475 | ) 476 | 477 | if participant is None: 478 | raise ValueError('could not find participant with address {:040x}'.format(address)) 479 | 480 | return participant 481 | 482 | def get_or_create_participant_by_address(self, address: int) -> 'ECDKGParticipant': 483 | try: 484 | return self.get_participant_by_address(address) 485 | except ValueError: 486 | participant = ECDKGParticipant(ecdkg_id=self.id, eth_address=address) 487 | db.Session.add(participant) 488 | db.Session.commit() 489 | return participant 490 | 491 | def get_signed_secret_shares(self, address: int) -> ((int, int), 'rsv triplet'): 492 | private_key = self.node.private_key 493 | 494 | secret_shares = (eval_polynomial(self.secret_poly1, address), 495 | eval_polynomial(self.secret_poly2, address)) 496 | 497 | msg_bytes = ( 498 | b'SECRETSHARES' + 499 | self.decryption_condition.encode() + 500 | util.address_to_bytes(address) + 501 | util.private_value_to_bytes(secret_shares[0]) + 502 | util.private_value_to_bytes(secret_shares[1]) 503 | ) 504 | 505 | signature = util.sign_with_key(msg_bytes, private_key) 506 | 507 | return (secret_shares, signature) 508 | 509 | def get_signed_verification_points(self) -> (tuple, 'rsv triplet'): 510 | private_key = self.node.private_key 511 | 512 | msg_bytes = ( 513 | b'VERIFICATIONPOINTS' + 514 | self.decryption_condition.encode() + 515 | util.curve_point_tuple_to_bytes(self.verification_points) 516 | ) 517 | 518 | signature = util.sign_with_key(msg_bytes, private_key) 519 | 520 | return (self.verification_points, signature) 521 | 522 | def get_signed_encryption_key_vector(self) -> ((int, int), 'rsv triplet'): 523 | private_key = self.node.private_key 524 | 525 | msg_bytes = ( 526 | b'ENCRYPTIONKEYPART' + 527 | self.decryption_condition.encode() + 528 | util.curve_point_tuple_to_bytes(self.encryption_key_vector) 529 | ) 530 | 531 | signature = util.sign_with_key(msg_bytes, private_key) 532 | 533 | return (self.encryption_key_vector, signature) 534 | 535 | def get_signed_decryption_key_part(self) -> (int, 'rsv triplet'): 536 | private_key = self.node.private_key 537 | 538 | msg_bytes = ( 539 | b'DECRYPTIONKEYPART' + 540 | self.decryption_condition.encode() + 541 | util.private_value_to_bytes(self.secret_poly1[0]) 542 | ) 543 | 544 | signature = util.sign_with_key(msg_bytes, private_key) 545 | 546 | return (self.secret_poly1[0], signature) 547 | 548 | def get_complaints_by(self, address: int) -> dict: 549 | return ( 550 | db.Session 551 | .query(ECDKGComplaint) 552 | .filter( # ECDKGComplaint.participant.ecdkg_id == self.id, 553 | ECDKGComplaint.complainer_address == address) 554 | .all() 555 | ) 556 | 557 | def to_state_message(self) -> dict: 558 | own_address = self.node.address 559 | 560 | msg = {'address': '{:040x}'.format(own_address)} 561 | 562 | for attr in ('decryption_condition', 'phase', 'threshold'): 563 | val = getattr(self, attr) 564 | if val is not None: 565 | msg[attr] = val 566 | 567 | msg['participants'] = {'{:040x}'.format(p.eth_address): p.to_state_message() for p in self.participants} 568 | 569 | for attr in ('encryption_key',): 570 | val = getattr(self, attr) 571 | if val is not None: 572 | msg[attr] = '{0[0]:064x}{0[1]:064x}'.format(val) 573 | 574 | for attr in ('verification_points', 'encryption_key_vector'): 575 | val = getattr(self, attr) 576 | if val is not None: 577 | msg[attr] = tuple('{0[0]:064x}{0[1]:064x}'.format(pt) for pt in val) 578 | 579 | return msg 580 | 581 | 582 | class ECDKGParticipant(db.Base): 583 | __tablename__ = 'ecdkg_participant' 584 | 585 | ecdkg_id = Column(types.Integer, ForeignKey('ecdkg.id')) 586 | ecdkg = relationship('ECDKG', back_populates='participants') 587 | eth_address = Column(db.EthAddress, index=True) 588 | 589 | encryption_key_vector = Column(db.CurvePointTuple) 590 | encryption_key_vector_signature = Column(db.Signature) 591 | 592 | decryption_key_part = Column(db.PrivateValue) 593 | decryption_key_part_signature = Column(db.Signature) 594 | 595 | verification_points = Column(db.CurvePointTuple) 596 | verification_points_signature = Column(db.Signature) 597 | 598 | secret_share1 = Column(db.PrivateValue) 599 | secret_share2 = Column(db.PrivateValue) 600 | shares_signature = Column(db.Signature) 601 | 602 | complaints = relationship('ECDKGComplaint', back_populates='participant') 603 | 604 | __table_args__ = (UniqueConstraint('ecdkg_id', 'eth_address'),) 605 | 606 | def get_or_create_complaint_by_complainer_address(self, address: int) -> 'ECDKGComplaint': 607 | complaint = ( 608 | db.Session 609 | .query(ECDKGComplaint) 610 | .filter(ECDKGComplaint.participant_id == self.id, 611 | ECDKGComplaint.complainer_address == address) 612 | .scalar() 613 | ) 614 | 615 | if complaint is None: 616 | complaint = ECDKGComplaint(participant_id=self.id, complainer_address=address) 617 | db.Session.add(complaint) 618 | db.Session.commit() 619 | 620 | return complaint 621 | 622 | def to_state_message(self, address: int = None) -> dict: 623 | msg = {} 624 | 625 | for attr in ('verification_points', 'encryption_key_vector'): 626 | val = getattr(self, attr) 627 | if val is not None: 628 | msg[attr] = tuple('{0[0]:064x}{0[1]:064x}'.format(pt) for pt in val) 629 | 630 | return msg 631 | 632 | 633 | class ECDKGComplaint(db.Base): 634 | __tablename__ = 'ecdkg_complaint' 635 | 636 | participant_id = Column(types.Integer, ForeignKey('ecdkg_participant.id')) 637 | participant = relationship('ECDKGParticipant', back_populates='complaints') 638 | complainer_address = Column(db.EthAddress, index=True) 639 | 640 | __table_args__ = (UniqueConstraint('participant_id', 'complainer_address'),) 641 | -------------------------------------------------------------------------------- /pydkg/networking.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import collections 3 | import json 4 | import logging 5 | import uuid 6 | 7 | from http.server import BaseHTTPRequestHandler 8 | 9 | from jsonrpc import JSONRPCResponseManager 10 | 11 | from . import util, ecdkg, rpc_interface, db 12 | 13 | 14 | DEFAULT_TIMEOUT = 120 15 | HEARTBEAT_INTERVAL = 30 16 | 17 | LineReader = collections.namedtuple('LineReader', ('readline')) 18 | 19 | 20 | # Adapted from http://stackoverflow.com/a/5955949 21 | # but made to work (synchronously) with asyncio.StreamReader 22 | class HTTPRequest(BaseHTTPRequestHandler): 23 | def __init__(self, raw_requestline, stream_reader): 24 | self.raw_requestline = raw_requestline 25 | self.stream_reader = stream_reader 26 | self.error_code = self.error_message = None 27 | 28 | def rfile_readline(_): 29 | gen = self.stream_reader.readline() 30 | try: 31 | while True: 32 | next(gen) 33 | except StopIteration as stop: 34 | return stop.value 35 | self.rfile = LineReader(readline=rfile_readline) 36 | self.parse_request() 37 | 38 | def send_error(self, code, message): 39 | self.error_code = code 40 | self.error_message = message 41 | 42 | def __repr__(self): 43 | return '<{module}.{classname}\n {desc}>'.format( 44 | module=__name__, 45 | classname=self.__class__.__name__, 46 | desc='\n '.join( 47 | '{}={}'.format( 48 | attr, 49 | ('\n '+' '*len(attr)).join(filter(bool, str(getattr(self, attr, None)).split('\n')))) 50 | for attr in ('command', 'path', 'headers'))) 51 | 52 | 53 | channels = {} 54 | response_futures = collections.OrderedDict() 55 | 56 | 57 | async def json_lines_with_timeout(reader: asyncio.StreamReader, timeout: 'seconds' = DEFAULT_TIMEOUT): 58 | while not reader.at_eof(): 59 | line = await asyncio.wait_for(reader.readline(), timeout) 60 | try: 61 | yield json.loads(line) 62 | except json.JSONDecodeError as e: 63 | pass 64 | 65 | 66 | async def establish_channel(eth_address: int, 67 | reader: asyncio.StreamReader, 68 | writer: asyncio.StreamWriter, 69 | node: ecdkg.ECDKGNode, 70 | location: (str, int) = None): 71 | if eth_address not in channels: 72 | channels[eth_address] = {} 73 | 74 | if 'writer' in channels[eth_address]: 75 | logging.debug('channel for {:040x} already exists; reestablishing channel...'.format(eth_address)) 76 | channels[eth_address]['writer'].close() 77 | else: 78 | logging.info('establishing new channel for {:040x}'.format(eth_address)) 79 | 80 | channels[eth_address]['reader'] = reader 81 | channels[eth_address]['writer'] = writer 82 | channels[eth_address]['rpcdispatcher'] = rpc_interface.create_dispatcher(node, eth_address) 83 | if location is not None: 84 | channels[eth_address]['location'] = location 85 | 86 | try: 87 | async for msg in json_lines_with_timeout(reader): 88 | logging.debug('received message {} from {:040x}'.format(msg, eth_address)) 89 | if msg is None: 90 | logging.warning('message should not be None!') 91 | elif 'method' in msg: 92 | res = JSONRPCResponseManager.handle(json.dumps(msg), channels[eth_address]['rpcdispatcher']) 93 | if res is not None: 94 | res_data = await get_response_data(res) 95 | res_str = json.dumps(res_data) 96 | logging.debug('sending response {}'.format(res_str)) 97 | writer.write(res_str.encode()) 98 | writer.write(b'\n') 99 | elif 'id' in msg: 100 | fut_id = msg['id'] 101 | if fut_id in response_futures: 102 | response_future = response_futures.pop(fut_id) 103 | if 'result' in msg: 104 | response_future.set_result(msg['result']) 105 | elif 'error' in msg: 106 | response_future.set_exception(msg['error']) 107 | else: 108 | response_future.cancel() 109 | else: 110 | logging.warning('Response with id {} has no corresponding future'.format(fut_id)) 111 | except asyncio.TimeoutError: 112 | logging.warn('channel for {:040x} timed out'.format(eth_address)) 113 | finally: 114 | writer.close() 115 | if writer is channels[eth_address].get('writer'): 116 | logging.info('removing channel for {:040x}'.format(eth_address)) 117 | del channels[eth_address]['reader'] 118 | del channels[eth_address]['writer'] 119 | 120 | 121 | def make_jsonrpc_call(cinfo: 'channel info', 122 | method_name: str, *args, 123 | is_notification: bool = False, 124 | loop: asyncio.AbstractEventLoop = None): 125 | if loop is None: 126 | loop = asyncio.get_event_loop() 127 | 128 | msg = { 129 | 'method': method_name, 130 | 'params': args, 131 | 'jsonrpc': '2.0', 132 | } 133 | 134 | if 'writer' in cinfo: 135 | if not is_notification: 136 | reqid = str(uuid.uuid4()) 137 | msg['id'] = reqid 138 | response_futures[reqid] = asyncio.Future(loop=loop) 139 | 140 | writer = cinfo['writer'] 141 | msg_str = json.dumps(msg) 142 | logging.debug('sending message: {}'.format(msg_str)) 143 | writer.write(msg_str.encode()) 144 | writer.write(b'\n') 145 | 146 | if not is_notification: 147 | return response_futures[reqid] 148 | else: 149 | logging.warning('cannot send message {} because channel {} has no writer'.format(msg, cinfo)) 150 | 151 | 152 | async def get_response_data(res: 'jsonrpc response', timeout: 'seconds' = DEFAULT_TIMEOUT) -> dict: 153 | if res is not None: 154 | res_data = res.data 155 | if 'result' in res_data: 156 | if (asyncio.iscoroutine(res_data['result']) or isinstance(res_data['result'], asyncio.Future)): 157 | res_data['result'] = await asyncio.wait_for(res_data['result'], timeout) 158 | return res_data 159 | 160 | 161 | async def broadcast_jsonrpc_call_on_all_channels(method_name: str, *args, 162 | timeout: 'seconds' = DEFAULT_TIMEOUT, 163 | is_notification: bool = False, 164 | loop: asyncio.AbstractEventLoop = None) -> dict: 165 | res_futures = {} 166 | 167 | for addr, cinfo in channels.items(): 168 | res_futures[addr] = make_jsonrpc_call(cinfo, method_name, *args, 169 | is_notification=is_notification, 170 | loop=loop) 171 | 172 | if is_notification: 173 | return 174 | 175 | await asyncio.wait(res_futures.values(), timeout=timeout) 176 | 177 | res = {} 178 | 179 | for addr, res_future in res_futures.items(): 180 | if res_future.done(): 181 | try: 182 | temp = res_future.result() 183 | except Exception as e: 184 | logging.error(e) 185 | else: 186 | res[addr] = temp 187 | 188 | return res 189 | 190 | 191 | async def determine_address_via_nonce(reader: asyncio.StreamReader, 192 | writer: asyncio.StreamWriter, 193 | timeout: 'seconds' = DEFAULT_TIMEOUT) -> int: 194 | # TODO: Make a nonce registry for extra security 195 | nonce = util.random.randrange(2**256) 196 | noncebytes = nonce.to_bytes(32, byteorder='big') 197 | logging.debug('sending nonce {:064x}'.format(nonce)) 198 | writer.write(nonce.to_bytes(32, byteorder='big')) 199 | 200 | rsv_bytes = await asyncio.wait_for(reader.read(65), timeout) 201 | signature = util.bytes_to_signature(rsv_bytes) 202 | logging.debug('received signature rsv ({:064x}, {:064x}, {:02x})'.format(*signature)) 203 | 204 | try: 205 | address = util.address_from_message_and_signature(noncebytes, signature, hash=None) 206 | except ValueError as err: 207 | logging.debug('could not recover address: {}'.format(err)) 208 | return None 209 | 210 | logging.debug('got address: {:040x}'.format(address)) 211 | return address 212 | 213 | 214 | async def respond_to_nonce_with_signature(reader: asyncio.StreamReader, 215 | writer: asyncio.StreamWriter, 216 | private_key: int, 217 | timeout: 'seconds' = DEFAULT_TIMEOUT): 218 | noncebytes = await asyncio.wait_for(reader.read(32), timeout) 219 | nonce = int.from_bytes(noncebytes, byteorder='big') 220 | logging.debug('got nonce: {:064x}'.format(nonce)) 221 | 222 | signature = util.sign_with_key(noncebytes, private_key, hash=None) 223 | logging.debug('sending nonce signature rsv ({:064x}, {:064x}, {:02x})'.format(*signature)) 224 | writer.write(util.signature_to_bytes(signature)) 225 | 226 | 227 | ################################################################################ 228 | 229 | async def emit_heartbeats(): 230 | while True: 231 | for addr, cinfo in channels.items(): 232 | if 'writer' in cinfo: 233 | cinfo['writer'].write(b'\n') 234 | await asyncio.sleep(HEARTBEAT_INTERVAL) 235 | 236 | 237 | ################################################################################ 238 | 239 | async def server(host: str, port: int, node: ecdkg.ECDKGNode, accepted_addresses: set, *, 240 | timeout: 'seconds' = DEFAULT_TIMEOUT, 241 | loop: asyncio.AbstractEventLoop): 242 | 243 | default_dispatcher = rpc_interface.create_dispatcher(node) 244 | 245 | async def handle_connection(reader: asyncio.StreamReader, writer: asyncio.StreamWriter): 246 | try: 247 | cliipaddr = writer.get_extra_info('peername') 248 | ownipaddr = writer.get_extra_info('sockname') 249 | logging.debug('{} <-- {}'.format(ownipaddr, cliipaddr)) 250 | 251 | protocol_indicator = await asyncio.wait_for(reader.read(4), timeout) 252 | 253 | if protocol_indicator == b'DKG ': 254 | await respond_to_nonce_with_signature(reader, writer, node.private_key, timeout) 255 | cliethaddr = await determine_address_via_nonce(reader, writer, timeout) 256 | 257 | if cliethaddr is None: 258 | logging.debug('(s) could not verify client signature; closing connection') 259 | return 260 | 261 | if cliethaddr not in accepted_addresses: 262 | logging.debug('(s) client address {:40x} not accepted'.format(cliethaddr)) 263 | return 264 | 265 | await establish_channel(cliethaddr, reader, writer, node) 266 | 267 | elif len(protocol_indicator) > 0: 268 | req = HTTPRequest(protocol_indicator + await asyncio.wait_for(reader.readline(), timeout), reader) 269 | contentlen = req.headers.get('Content-Length') 270 | if contentlen is not None: 271 | contentlen = int(contentlen) 272 | req.body = await reader.read(contentlen) 273 | 274 | res = JSONRPCResponseManager.handle(req.body, default_dispatcher) 275 | res_data = await get_response_data(res, timeout) 276 | db.Session.remove() 277 | 278 | if res_data is None: 279 | writer.write(b'HTTP/1.1 204 No Content\r\n\r\n') 280 | else: 281 | res_str = json.dumps(res_data, indent=2, sort_keys=True).encode('UTF-8') 282 | 283 | writer.write(b'HTTP/1.1 200 OK\r\n' 284 | b'Content-Type: application/json; charset=UTF-8\r\n' 285 | b'Content-Length: ') 286 | writer.write(str(len(res_str) + 1).encode('UTF-8')) 287 | writer.write(b'\r\n\r\n') 288 | writer.write(res_str) 289 | writer.write(b'\n') 290 | finally: 291 | writer.close() 292 | 293 | logging.debug('(s) serving on {}:{}'.format(host, port)) 294 | await asyncio.start_server(handle_connection, 295 | host, port, loop=loop) 296 | 297 | 298 | ################################################################################ 299 | 300 | async def attempt_to_establish_channel(host: str, port: int, node: ecdkg.ECDKGNode, accepted_addresses: set, *, 301 | timeout: 'seconds' = DEFAULT_TIMEOUT, 302 | num_tries: int = 6): 303 | 304 | logging.debug('(c) attempting to connect to {}:{}'.format(host, port)) 305 | for i in range(num_tries): 306 | try: 307 | reader, writer = await asyncio.open_connection(host, port) 308 | except OSError as e: 309 | if e.errno == 111 or '[Errno 111]' in str(e): # connection refused 310 | if i < num_tries - 1: 311 | wait_time = 5 * 2**i 312 | logging.debug('(c) connection to {}:{} refused; trying again in {}s'.format(host, port, wait_time)) 313 | await asyncio.sleep(wait_time) 314 | else: 315 | raise 316 | else: 317 | srvipaddr = writer.get_extra_info('peername') 318 | ownipaddr = writer.get_extra_info('sockname') 319 | logging.debug('{} --> {}'.format(ownipaddr, srvipaddr)) 320 | break 321 | else: 322 | logging.warning('could not connect to {}:{} after {} tries'.format(host, port, num_tries)) 323 | return 324 | 325 | try: 326 | writer.write(b'DKG ') 327 | 328 | srvethaddr = await determine_address_via_nonce(reader, writer, timeout) 329 | 330 | if srvethaddr is None: 331 | logging.info('(c) could not determine server address; closing connection...') 332 | return 333 | 334 | if srvethaddr not in accepted_addresses: 335 | logging.debug('(c) server eth address {:040x} not accepted'.format(srvethaddr)) 336 | return 337 | 338 | if srvethaddr in channels and 'writer' in channels[srvethaddr]: 339 | logging.info('(c) already connected to {:040x}; ending connection attempt'.format(srvethaddr)) 340 | return 341 | 342 | await respond_to_nonce_with_signature(reader, writer, node.private_key, timeout) 343 | 344 | await establish_channel(srvethaddr, reader, writer, node, srvipaddr) 345 | finally: 346 | writer.close() 347 | -------------------------------------------------------------------------------- /pydkg/rpc_interface.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import functools 3 | import logging 4 | 5 | from jsonrpc.dispatcher import Dispatcher 6 | 7 | from . import ecdkg, util 8 | 9 | 10 | class ProtocolError(Exception): 11 | pass 12 | 13 | 14 | def create_dispatcher(node: ecdkg.ECDKGNode, address: int = None): 15 | # TODO: make loop use uniform 16 | loop = asyncio.get_event_loop() 17 | 18 | dispatcher = Dispatcher() 19 | 20 | dispatcher['echo'] = lambda value: value 21 | 22 | def dispatcher_add_async_method(corofunc): 23 | @functools.wraps(corofunc) 24 | def wrapper(*args, **kwargs): 25 | return loop.create_task(corofunc(*args, **kwargs)) 26 | return dispatcher.add_method(wrapper) 27 | 28 | @dispatcher_add_async_method 29 | async def get_ecdkg_state(decryption_condition: str, phase: ecdkg.ECDKGPhase = ecdkg.ECDKGPhase.uninitialized): 30 | ecdkg_obj = node.get_protocol_instance_by_decryption_condition(decryption_condition) 31 | return ecdkg_obj.to_state_message() 32 | 33 | @dispatcher_add_async_method 34 | async def get_encryption_key(decryption_condition, notify_others=True): 35 | ecdkg_obj = node.get_protocol_instance_by_decryption_condition(decryption_condition) 36 | await ecdkg_obj.run_until_phase(ecdkg.ECDKGPhase.key_publication) 37 | return '{0[0]:064x}{0[1]:064x}'.format(ecdkg_obj.encryption_key) 38 | 39 | @dispatcher_add_async_method 40 | async def get_signed_decryption_key_part(decryption_condition): 41 | # TODO: Is running the get enc key even necessary now? 42 | await get_encryption_key(decryption_condition) 43 | await util.decryption_condition_satisfied(decryption_condition) 44 | ecdkg_obj = node.get_protocol_instance_by_decryption_condition(decryption_condition) 45 | return ecdkg_obj.get_signed_decryption_key_part() 46 | 47 | @dispatcher_add_async_method 48 | async def get_decryption_key(decryption_condition): 49 | ecdkg_obj = node.get_protocol_instance_by_decryption_condition(decryption_condition) 50 | await ecdkg_obj.run_until_phase(ecdkg.ECDKGPhase.complete) 51 | return '{:064x}'.format(ecdkg_obj.decryption_key) 52 | 53 | @dispatcher_add_async_method 54 | async def get_signed_verification_points(decryption_condition): 55 | logging.info('sending vpoints to {:040x}'.format(address)) 56 | ecdkg_obj = node.get_protocol_instance_by_decryption_condition(decryption_condition) 57 | await ecdkg_obj.run_until_phase(ecdkg.ECDKGPhase.key_distribution) 58 | return ecdkg_obj.get_signed_verification_points() 59 | 60 | @dispatcher_add_async_method 61 | async def get_signed_encryption_key_vector(decryption_condition): 62 | ecdkg_obj = node.get_protocol_instance_by_decryption_condition(decryption_condition) 63 | # await ecdkg_obj.run_until_phase(ecdkg.ECDKGPhase.key_generation) 64 | await ecdkg_obj.run_until_phase(ecdkg.ECDKGPhase.key_distribution) 65 | return ecdkg_obj.get_signed_encryption_key_vector() 66 | 67 | @dispatcher_add_async_method 68 | async def get_complaints(decryption_condition): 69 | ecdkg_obj = node.get_protocol_instance_by_decryption_condition(decryption_condition) 70 | # await ecdkg_obj.run_until_phase(ecdkg.ECDKGPhase.key_check) 71 | await ecdkg_obj.run_until_phase(ecdkg.ECDKGPhase.key_distribution) 72 | complaints = ecdkg_obj.get_complaints_by(node.address) 73 | logging.info('sending complaints: {}'.format(complaints)) 74 | return complaints 75 | 76 | if address is not None: 77 | @dispatcher_add_async_method 78 | async def get_signed_secret_shares(decryption_condition): 79 | logging.info('sending shares to {:040x}'.format(address)) 80 | ecdkg_obj = node.get_protocol_instance_by_decryption_condition(decryption_condition) 81 | await ecdkg_obj.run_until_phase(ecdkg.ECDKGPhase.key_distribution) 82 | return ecdkg_obj.get_signed_secret_shares(address) 83 | 84 | return dispatcher 85 | -------------------------------------------------------------------------------- /pydkg/util.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import functools 3 | import logging 4 | import os 5 | import re 6 | 7 | from datetime import datetime 8 | 9 | from py_ecc.secp256k1 import secp256k1 10 | import sha3 11 | 12 | from dateutil.parser import parse as parse_datetime 13 | from dateutil.tz import tzutc 14 | 15 | try: 16 | from secrets import SystemRandom 17 | random = SystemRandom() 18 | except ImportError: 19 | try: 20 | from random import SystemRandom 21 | random = SystemRandom() 22 | except ImportError: 23 | logging.warninging('Could not obtain randomness source suitable for crypto') 24 | import random 25 | 26 | 27 | ######################## 28 | # Validation utilities # 29 | ######################## 30 | 31 | 32 | def validate_private_value(value: int): 33 | if value < 0 or value >= secp256k1.N: 34 | raise ValueError('invalid EC private value {:064x}'.format(value)) 35 | 36 | 37 | def validate_polynomial(polynomial: int): 38 | for i, coeff in enumerate(polynomial): 39 | try: 40 | validate_private_value(coeff) 41 | except ValueError: 42 | raise ValueError('invalid x^{} coefficient {:064x}'.format(i, coeff)) 43 | 44 | 45 | def validate_curve_point(point: (int, int)): 46 | if ( 47 | any(coord < 0 or coord >= secp256k1.P for coord in point) or 48 | pow(point[1], 2, secp256k1.P) != (pow(point[0], 3, secp256k1.P) + 7) % secp256k1.P 49 | ) and point != (0, 0): # (0, 0) is used to represent group identity element 50 | raise ValueError('invalid EC point {}'.format(point)) 51 | 52 | 53 | def validate_eth_address(addr: int): 54 | if addr < 0 or addr >= 2**160: 55 | raise ValueError('invalid Ethereum address {:040x}'.format(addr)) 56 | 57 | 58 | def validate_signature(signature: 'rsv triplet'): 59 | r, s, v = signature 60 | if (any(coord < 0 or coord >= secp256k1.P for coord in (r, s)) or 61 | v not in (27, 28)): 62 | raise ValueError('invalid signature {}'.format(signature)) 63 | 64 | 65 | ######################## 66 | # Conversion utilities # 67 | ######################## 68 | 69 | 70 | def private_value_to_bytes(value: int) -> bytes: 71 | validate_private_value(value) 72 | return value.to_bytes(32, byteorder='big') 73 | 74 | 75 | def bytes_to_private_value(bts: bytes) -> int: 76 | priv = int.from_bytes(bts, byteorder='big') 77 | validate_private_value(priv) 78 | return priv 79 | 80 | 81 | def curve_point_to_bytes(point: (int, int)) -> bytes: 82 | validate_curve_point(point) 83 | return sequence_256_bit_values_to_bytes(point) 84 | 85 | 86 | def bytes_to_curve_point(bts: bytes) -> (int, int): 87 | if len(bts) != 64: 88 | raise ValueError('unexpected length {} bytes'.format(len(bts))) 89 | point = tuple(int.from_bytes(bts[i:i+32], byteorder='big') for i in (0, 32)) 90 | validate_curve_point(point) 91 | return point 92 | 93 | 94 | def signature_to_bytes(signature: 'rsv triplet') -> bytes: 95 | validate_signature(signature) 96 | return b''.join(int.to_bytes(part, partsize, byteorder='big') for part, partsize in zip(signature, (32, 32, 1))) 97 | 98 | 99 | def bytes_to_signature(bts: bytes) -> 'rsv triplet': 100 | if len(bts) != 65: 101 | raise ValueError('unexpected length {} bytes'.format(len(bts))) 102 | signature = tuple(int.from_bytes(bs, byteorder='big') for bs in (bts[0:32], bts[32:64], bts[64:])) 103 | validate_signature(signature) 104 | return signature 105 | 106 | 107 | def address_to_bytes(addr: int) -> bytes: 108 | validate_eth_address(addr) 109 | return addr.to_bytes(20, byteorder='big') 110 | 111 | 112 | def bytes_to_address(bts: bytes) -> int: 113 | if len(bts) != 20: 114 | raise ValueError('unexpected length {} bytes'.format(len(bts))) 115 | addr = int.from_bytes(bts, byteorder='big') 116 | validate_eth_address(addr) 117 | return addr 118 | 119 | 120 | def polynomial_to_bytes(polynomial: tuple) -> bytes: 121 | validate_polynomial(polynomial) 122 | return sequence_256_bit_values_to_bytes(polynomial) 123 | 124 | 125 | def bytes_to_polynomial(bts: bytes) -> tuple: 126 | if len(bts) % 32 != 0: 127 | raise ValueError('length {} not divisible by 32 bytes'.format(len(bts))) 128 | polynomial = tuple(int.from_bytes(bts[i:i+32], byteorder='big') for i in range(0, len(bts), 32)) 129 | validate_polynomial(polynomial) 130 | return polynomial 131 | 132 | 133 | def curve_point_tuple_to_bytes(points: tuple) -> bytes: 134 | return b''.join(curve_point_to_bytes(point) for point in points) 135 | 136 | 137 | def bytes_to_curve_point_tuple(bts: bytes) -> tuple: 138 | if len(bts) % 64 != 0: 139 | raise ValueError('length {} not divisible by 64 bytes'.format(len(bts))) 140 | return tuple(bytes_to_curve_point(bts[i:i+64]) for i in range(0, len(bts), 64)) 141 | 142 | 143 | def sequence_256_bit_values_to_bytes(sequence: tuple) -> bytes: 144 | return b''.join(map(functools.partial(int.to_bytes, length=32, byteorder='big'), sequence)) 145 | 146 | 147 | def private_value_to_eth_address(private_value: int) -> int: 148 | return curve_point_to_eth_address(secp256k1.multiply(secp256k1.G, private_value)) 149 | 150 | 151 | def curve_point_to_eth_address(curve_point: (int, int)) -> int: 152 | return int.from_bytes(sha3.keccak_256(curve_point_to_bytes(curve_point)).digest()[-20:], byteorder='big') 153 | 154 | 155 | ########################### 156 | # Configuration utilities # 157 | ########################### 158 | 159 | PRIVATE_VALUE_RE = re.compile(r'(?P0x)?(?P[0-9A-Fa-f]{64})') 160 | ADDRESS_RE = re.compile(r'(?P0x)?(?P[0-9A-Fa-f]{40})') 161 | LOCATION_RE = re.compile(r'(?P[^:]*)(?::(?P\d+))?') 162 | DEFAULT_PORT = 80 163 | 164 | 165 | def get_or_generate_private_value(filepath: str) -> int: 166 | if os.path.isfile(filepath): 167 | with open(filepath) as private_key_fp: 168 | private_key_str = private_key_fp.read().strip() 169 | private_key_match = PRIVATE_VALUE_RE.fullmatch(private_key_str) 170 | if private_key_match: 171 | private_key = int(private_key_match.group('value'), 16) 172 | validate_private_value(private_key) 173 | return private_key 174 | 175 | logging.warning('could not read key from private key file {}; generating new value...'.format(filepath)) 176 | with open(filepath, 'w') as private_key_fp: 177 | private_key = random_private_value() 178 | private_key_fp.write('{:064x}\n'.format(private_key)) 179 | return private_key 180 | 181 | 182 | def get_addresses(filepath: str) -> set: 183 | with open(filepath, 'r') as f: 184 | return set( 185 | int(m.group('value'), 16) 186 | for m in filter(lambda v: v is not None, (ADDRESS_RE.fullmatch(l.strip()) for l in f)) 187 | ) 188 | 189 | 190 | def get_locations(filepath: str) -> list: 191 | with open(filepath, 'r') as f: 192 | return list( 193 | (m.group('hostname'), int(m.group('port') or DEFAULT_PORT)) 194 | for m in filter( 195 | lambda v: v is not None, 196 | (LOCATION_RE.fullmatch(l.strip()) for l in f if not l.startswith('#')) 197 | ) 198 | ) 199 | 200 | 201 | ################### 202 | # Other utilities # 203 | ################### 204 | 205 | 206 | def random_private_value() -> int: 207 | return random.randrange(secp256k1.N) 208 | 209 | 210 | def address_from_message_and_signature(message: bytes, 211 | signature: 'rsv triplet', 212 | hash: 'hash class' = sha3.keccak_256) -> int: 213 | if hash is None: 214 | value = message 215 | else: 216 | value = hash(message).digest() 217 | 218 | if len(value) != 32: 219 | raise ValueError('value must have length 32 but got length {} ({})'.format(len(value), value)) 220 | 221 | (r, s, v) = signature 222 | 223 | pubkey = secp256k1.ecdsa_raw_recover(value, (v, r, s)) 224 | 225 | if not pubkey: 226 | raise ValueError('ECDSA public key recovery failed with bytes {} and signature {}'.format(value, signature)) 227 | 228 | return curve_point_to_eth_address(pubkey) 229 | 230 | 231 | def sign_with_key(message: bytes, key: int, hash: 'hash class' = sha3.keccak_256) -> 'rsv triplet': 232 | if hash is None: 233 | value = message 234 | else: 235 | value = hash(message).digest() 236 | 237 | if len(value) != 32: 238 | raise ValueError('value must have length 32 but got length {} ({})'.format(len(value), value)) 239 | 240 | v, r, s = secp256k1.ecdsa_raw_sign(value, private_value_to_bytes(key)) 241 | return (r, s, v) 242 | 243 | 244 | def normalize_decryption_condition(deccond: str, return_obj: bool = False): 245 | prefix = 'past ' 246 | if deccond.startswith(prefix): 247 | try: 248 | dt = parse_datetime(deccond[len(prefix):]) 249 | except ValueError as e: 250 | raise ValueError('could not parse date for "past" condition from string "{}"'.format(deccond[len(prefix):])) 251 | 252 | # All time values internally UTC 253 | if dt.tzinfo is not None: 254 | dt = dt.astimezone(tzutc()) 255 | 256 | # Strip out subsecond info and make naive 257 | dt = dt.replace(microsecond=0, tzinfo=None) 258 | 259 | if return_obj: 260 | return (prefix, dt) 261 | 262 | return prefix + dt.isoformat() 263 | 264 | raise ValueError('invalid decryption condition {}'.format(deccond)) 265 | 266 | 267 | async def decryption_condition_satisfied(deccond: str) -> bool: 268 | prefix, obj = normalize_decryption_condition(deccond, True) 269 | if prefix == 'past ': 270 | while datetime.utcnow() < obj: 271 | await asyncio.sleep(max(0, (obj - datetime.utcnow()).total_seconds())) 272 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | from codecs import open 3 | from os import path 4 | 5 | here = path.abspath(path.dirname(__file__)) 6 | 7 | # Get the long description from the README file 8 | with open(path.join(here, 'README.rst'), encoding='utf-8') as f: 9 | long_description = f.read() 10 | 11 | setup( 12 | name='pydkg', 13 | version='0.1.0.dev1', 14 | description='Distributed key generation', 15 | long_description=long_description, 16 | url='http://github.com/gnosis/pydkg', 17 | author='Alan Lu', 18 | author_email='alan.lu@gnosis.pm', 19 | classifiers=[ 20 | 'Development Status :: 2 - Pre-Alpha', 21 | 22 | 'Environment :: No Input/Output (Daemon)', 23 | 24 | 'Intended Audience :: Developers', 25 | 'Intended Audience :: Science/Research', 26 | 27 | 'Programming Language :: Python :: 3', 28 | 'Programming Language :: Python :: 3.6', 29 | ], 30 | 31 | packages=['pydkg'], 32 | install_requires=[ 33 | 'cryptography', 34 | 'json-rpc', 35 | 'py_ecc', 36 | 'python-dateutil', 37 | 'sqlalchemy', 38 | ], 39 | extras_require={ 40 | 'test': [ 41 | 'flake8', 42 | 'populus', 43 | 'psutil', 44 | 'pytest', 45 | 'requests', 46 | ], 47 | }, 48 | 49 | entry_points={ 50 | 'console_scripts': [ 51 | 'pydkg=pydkg.__main__:main', 52 | ], 53 | }, 54 | ) 55 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | def pytest_addoption(parser): 5 | parser.addoption( 6 | "--num-ecdkg-nodes", action="store", default=5, type=int, 7 | help="number of ecdkg nodes %(default)s") 8 | parser.addoption( 9 | "--request-timeout", action="store", default=5, type=float, 10 | help="request timeout %(default)s") 11 | 12 | 13 | @pytest.fixture 14 | def num_ecdkg_nodes(request): 15 | return request.config.getoption("--num-ecdkg-nodes") 16 | 17 | 18 | @pytest.fixture 19 | def request_timeout(request): 20 | return request.config.getoption("--request-timeout") 21 | -------------------------------------------------------------------------------- /tests/test_crypto.py: -------------------------------------------------------------------------------- 1 | import os 2 | from py_ecc.secp256k1 import secp256k1 3 | from pydkg import util, crypto 4 | 5 | 6 | def test_can_decrypt_encrypted_random_messages(chain): 7 | # TODO: Fix populus compat 8 | ies, _ = chain.provider.get_or_deploy_contract('IntegratedEncryptionScheme') 9 | num_runs = 10 10 | average_gas_cost = 0 11 | for _ in range(num_runs): 12 | message = os.urandom(util.random.randrange(1, 100)) 13 | deckey = util.random_private_value() 14 | enckey = secp256k1.multiply(secp256k1.G, deckey) 15 | ciphertext = crypto.encrypt(message, enckey) 16 | assert message == crypto.decrypt(ciphertext, deckey) 17 | assert crypto.decrypt(ciphertext, deckey, foo=True) == ies.call().decrypt(ciphertext, deckey) 18 | average_gas_cost += ies.estimateGas().decrypt(ciphertext, deckey) 19 | 20 | average_gas_cost /= num_runs 21 | # assert average_gas_cost == 0 22 | -------------------------------------------------------------------------------- /tests/test_ecdkg.py: -------------------------------------------------------------------------------- 1 | import collections 2 | import functools 3 | import json 4 | import logging 5 | import os 6 | import requests 7 | import signal 8 | import subprocess 9 | import tempfile 10 | import time 11 | 12 | from contextlib import ExitStack, contextmanager 13 | from datetime import datetime 14 | 15 | from py_ecc.secp256k1 import secp256k1 16 | import psutil 17 | import pytest 18 | 19 | from pydkg import util 20 | 21 | 22 | BIN_NAME = 'pydkg' 23 | PORTS_START = 59828 24 | 25 | NodeInfo = collections.namedtuple('NodeInfo', ( 26 | 'process', 27 | 'private_key', 28 | 'port', 29 | )) 30 | 31 | 32 | @contextmanager 33 | def Popen_with_interrupt_at_exit(cmdargs, *args, **kwargs): 34 | p = None 35 | try: 36 | p = psutil.Popen(cmdargs, *args, **kwargs) 37 | yield p 38 | finally: 39 | if p is not None: 40 | # start by trying to end process gently, but escalate 41 | for endfn in (functools.partial(p.send_signal, signal.SIGINT), p.terminate, p.kill): 42 | if p.poll() is None: 43 | endfn() 44 | try: 45 | # TODO: switch to asyncio??? 46 | p.wait(timeout=2) 47 | except subprocess.TimeoutExpired: 48 | continue 49 | 50 | 51 | @pytest.fixture 52 | def nodes(num_ecdkg_nodes, request_timeout): 53 | subprocess.check_call((BIN_NAME, '-h'), stdout=subprocess.DEVNULL) 54 | with ExitStack() as exitstack: 55 | proc_dir = exitstack.enter_context(tempfile.TemporaryDirectory()) 56 | proc_dir_file = functools.partial(os.path.join, proc_dir) 57 | 58 | private_keys = tuple(util.get_or_generate_private_value( 59 | proc_dir_file('private.key.{}'.format(i))) 60 | for i in range(num_ecdkg_nodes)) 61 | 62 | with open(proc_dir_file('addresses.txt'), 'w') as addrf: 63 | for privkey in private_keys: 64 | addrf.write("{:040x}\n".format(util.private_value_to_eth_address(privkey))) 65 | 66 | with open(proc_dir_file('locations.txt'), 'w') as locf: 67 | for i in range(num_ecdkg_nodes): 68 | locf.write("localhost:{}\n".format(PORTS_START+i)) 69 | 70 | processes = [] 71 | for i in range(num_ecdkg_nodes): 72 | processes.append(exitstack.enter_context(Popen_with_interrupt_at_exit(( 73 | BIN_NAME, 74 | '--port', str(PORTS_START+i), 75 | '--private-key-file', proc_dir_file('private.key.{}'.format(i)), 76 | '--addresses-file', proc_dir_file('addresses.txt'), 77 | '--locations', proc_dir_file('locations.txt'), 78 | '--log-level', str(logging.DEBUG), 79 | '--log-format', '[{}]: %(message)s'.format(i), 80 | )))) 81 | # TODO: Figure out why channels randomly do not get set up with tighter timing 82 | time.sleep(0.1) 83 | 84 | yield tuple( 85 | NodeInfo(process=proc, private_key=privkey, port=PORTS_START+i) 86 | for i, (proc, privkey) in enumerate(zip(processes, private_keys)) 87 | ) 88 | 89 | 90 | def is_node_listening(node: NodeInfo): 91 | return any(True for con in node.process.connections() if con.status == psutil.CONN_LISTEN) 92 | 93 | 94 | def wait_for_all_nodes_listening(nodes, timeout): 95 | # TODO: Do something better than spinlock maybe? 96 | # This could maybe be improved if transitioning to an asyncio version 97 | # but then would lose psutil interop 98 | timelimit = time.perf_counter() + timeout 99 | 100 | while any(not is_node_listening(n) for n in nodes): 101 | if time.perf_counter() >= timelimit: 102 | print('wait_for_all_nodes_listening took longer than {} seconds'.format(timeout)) 103 | print_diagnostics(nodes) 104 | break 105 | 106 | 107 | def wait_for_all_nodes_connected(nodes, timeout): 108 | # each node connects to each other node 109 | timelimit = time.perf_counter() + timeout 110 | 111 | while any( 112 | sum(1 for con in n.process.connections() if con.status == psutil.CONN_ESTABLISHED) != 113 | len(nodes)-1 114 | for n in nodes 115 | ): 116 | if time.perf_counter() >= timelimit: 117 | print('wait_for_all_nodes_connected took longer than {} seconds'.format(timeout)) 118 | print_diagnostics(nodes) 119 | break 120 | 121 | time.sleep(.1) # TODO: remove requirement for waiting for channel establishment 122 | 123 | 124 | def print_diagnostics(nodes): 125 | portmap = {} 126 | node_conns = [[c for c in n.process.connections() if c.status == psutil.CONN_ESTABLISHED] for n in nodes] 127 | for i, (n, conns) in enumerate(zip(nodes, node_conns)): 128 | for c in conns: 129 | portmap[c.laddr[1]] = i 130 | 131 | node_conn_sets = [] 132 | unestablished_connections = set() 133 | for i, (n, conns) in enumerate(zip(nodes, node_conns)): 134 | outset = set() 135 | inset = set() 136 | 137 | for c in conns: 138 | if c.laddr[1] == n.port: 139 | inset.add(portmap.get(c.raddr[1], -1)) 140 | else: 141 | outset.add(portmap.get(c.raddr[1], -1)) 142 | 143 | node_conn_sets.append((sorted(inset), sorted(outset))) 144 | for elem in range(len(nodes)): 145 | if i != elem and elem not in inset and elem not in outset: 146 | unestablished_connections.add(tuple(sorted((i, elem)))) 147 | 148 | for i, (n, conns, (ins, outs)) in enumerate(zip(nodes, node_conns, node_conn_sets)): 149 | print(i, '- num connections:', len(conns)) 150 | print(' - out', outs) 151 | print(' - in ', ins) 152 | 153 | for a, b in sorted(unestablished_connections): 154 | print(a, '-X-', b) 155 | 156 | 157 | def test_nodes_match_state(nodes, request_timeout): 158 | wait_for_all_nodes_listening(nodes, request_timeout) 159 | 160 | deccond = 'past {}'.format(datetime.utcnow().isoformat()) 161 | responses = [requests.post( 162 | 'http://localhost:{}'.format(n.port), 163 | verify=False, 164 | timeout=request_timeout, 165 | data=json.dumps({ 166 | 'id': 'honk', 167 | 'method': 'get_ecdkg_state', 168 | 'params': [deccond], 169 | })).json()['result'] for n in nodes] 170 | 171 | assert(all(r['decryption_condition'] == responses[0]['decryption_condition'] for r in responses[1:])) 172 | 173 | 174 | def test_nodes_match_enckey_and_deckeys(nodes, request_timeout): 175 | wait_for_all_nodes_connected(nodes, request_timeout) 176 | 177 | deccond = 'past {}'.format(datetime.utcnow().isoformat()) 178 | enckeys = [requests.post( 179 | 'http://localhost:{}'.format(n.port), 180 | verify=False, 181 | timeout=request_timeout, 182 | data=json.dumps({ 183 | 'id': 'honk', 184 | 'method': 'get_encryption_key', 185 | 'params': [deccond], 186 | })).json()['result'] for n in nodes] 187 | 188 | enckeys = [tuple(int(ek[i:i+64], 16) for i in (0, 64)) for ek in enckeys] 189 | 190 | for ek in enckeys: 191 | util.validate_curve_point(ek) 192 | 193 | assert(all(ek == enckeys[0] for ek in enckeys[1:])) 194 | 195 | deckeys = [requests.post( 196 | 'http://localhost:{}'.format(n.port), 197 | verify=False, 198 | timeout=request_timeout, 199 | data=json.dumps({ 200 | 'id': 'honk', 201 | 'method': 'get_decryption_key', 202 | 'params': [deccond], 203 | })).json()['result'] for n in nodes] 204 | 205 | deckeys = [int(dk, 16) for dk in deckeys] 206 | 207 | for dk in deckeys: 208 | util.validate_private_value(dk) 209 | 210 | assert(all(dk == deckeys[0] for dk in deckeys[1:])) 211 | 212 | assert(secp256k1.multiply(secp256k1.G, deckeys[0]) == enckeys[0]) 213 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py{35,36} 3 | 4 | [testenv] 5 | deps= 6 | flake8 7 | pytest 8 | populus 9 | psutil 10 | requests 11 | commands= 12 | pytest 13 | flake8 14 | 15 | [flake8] 16 | max-line-length = 120 17 | --------------------------------------------------------------------------------