├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── assets └── logo.png ├── mimblewimble ├── __init__.py ├── blockchain.py ├── codecs.py ├── consensus.py ├── crypto │ ├── age.py │ ├── aggsig.py │ ├── bulletproof.py │ ├── commitment.py │ ├── pedersen.py │ ├── public_key.py │ ├── public_keys.py │ ├── rangeproof.py │ ├── secret_key.py │ └── signature.py ├── entity │ └── __init__.py ├── genesis.py ├── helpers │ ├── __init__.py │ ├── encryption.py │ ├── fee.py │ ├── slate.py │ └── tor.py ├── keychain.py ├── mmr │ ├── __init__.py │ └── index.py ├── mnemonic.py ├── models │ ├── fee.py │ ├── short_id.py │ ├── slatepack │ │ ├── address.py │ │ ├── message.py │ │ └── metadata.py │ └── transaction.py ├── serializer.py ├── slatebuilder │ ├── __init__.py │ ├── finalize.py │ ├── receive.py │ ├── send.py │ ├── slate.py │ └── slate_payment_proof.py ├── static │ ├── __init__.py │ └── wordlist.json └── wallet.py ├── requirements.txt ├── setup.py └── tests ├── __init__.py ├── test_age.py ├── test_aggsig.py ├── test_blockheader.py ├── test_chain.py ├── test_commitments.py ├── test_ed25519.py ├── test_extended_keys.py ├── test_genesis.py ├── test_key_derivation.py ├── test_mnemonic.py ├── test_rewindbulletproof.py ├── test_shortid.py ├── test_slate_field_status.py ├── test_slate_serialization.py ├── test_slatepack_address.py ├── test_slatepack_armor.py ├── test_slatepack_exchange_encryption.py ├── test_slatepack_message.py ├── test_transaction_building.py ├── test_utils.py └── test_wallet.py /.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 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "3.8" 4 | install: pip3 install -r requirements.txt 5 | # command to run tests 6 | script: pytest -v 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Marek 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 | -------------------------------------------------------------------------------- /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grinventions/mimblewimble-py/5d904d2f0a0f3629210db5e0f634599b97e5283a/assets/logo.png -------------------------------------------------------------------------------- /mimblewimble/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grinventions/mimblewimble-py/5d904d2f0a0f3629210db5e0f634599b97e5283a/mimblewimble/__init__.py -------------------------------------------------------------------------------- /mimblewimble/blockchain.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | import json # TODO remove after debugging 3 | from io import BytesIO 4 | 5 | from mimblewimble.mmr.index import MMRIndex 6 | from mimblewimble.serializer import Serializer 7 | 8 | from mimblewimble.consensus import Consensus 9 | from mimblewimble.models.short_id import ShortId 10 | 11 | from mimblewimble.models.transaction import TransactionInput 12 | from mimblewimble.models.transaction import TransactionOutput 13 | from mimblewimble.models.transaction import TransactionBody 14 | from mimblewimble.models.transaction import BlindingFactor 15 | 16 | 17 | class ProofOfWork: 18 | def __init__(self, edgeBits, proofNonces): 19 | self.edgeBits = edgeBits # 1 byte 20 | self.proofNonces = proofNonces 21 | 22 | def getEdgeBits(self): 23 | return self.edgeBits 24 | 25 | def getProofNonces(self): 26 | return self.proofNonces 27 | 28 | def isPrimary(self): 29 | return Consensus.isPrimary(self.edgeBits) 30 | 31 | def isSecondary(self): 32 | return Consensus.isSecondary(self.edgeBits) 33 | 34 | def serialize(self, serializer): 35 | serializer.write(self.getEdgeBits().to_bytes(1, 'big')) 36 | serializer.write(self.serializeCycle()) 37 | 38 | def serializeCycle(self): 39 | bytes_len = int(((self.getEdgeBits()*Consensus.proofsize)+7)/8) 40 | serialized_bytes = bytearray(bytes_len) 41 | for n in range(len(self.getProofNonces())): 42 | for bit in range(int(self.getEdgeBits())): 43 | nonce = self.proofNonces[n] 44 | if nonce & (1 << bit) != 0: 45 | positionTemp = (n*self.edgeBits)+bit 46 | p = int(positionTemp/8) 47 | serialized_bytes[p] |= (1 << (positionTemp % 8)) 48 | return serialized_bytes 49 | 50 | @classmethod 51 | def deserialize(self, B): 52 | edgeBits = int.from_bytes(B.read(1), 'big') 53 | bytes_len = int(((edgeBits*Consensus.proofsize)+7)/8) 54 | bits = B.read(bytes_len) 55 | proofNonces = self.deserializeProofNonces(bits, edgeBits) 56 | return ProofOfWork(edgeBits, proofNonces) 57 | 58 | def deserializeProofNonces(bits, edgeBits): 59 | if edgeBits == 0 or edgeBits > 63: 60 | raise ValueError('Invalid number of edge bits {0}'.format(str(edgeBits))) 61 | uint8_t1 = b'\x00\x00\x00\x00\x00\x00\x00\x01' 62 | proofNonces = [] 63 | for n in range(Consensus.proofsize): 64 | proofNonce = 0 65 | for bit in range(edgeBits): 66 | positionTemp = (n*edgeBits)+bit 67 | p = int(positionTemp/8) 68 | if int(bits[p]) & (1 << (positionTemp % 8)): 69 | proofNonce |= 1 << bit 70 | proofNonces.append(proofNonce) 71 | return proofNonces 72 | 73 | def getHash(self): 74 | cycle = self.serializeCycle() 75 | return hashlib.blake2b(cycle, digest_size=32).digest() 76 | 77 | 78 | class BlockHeader: 79 | def __init__(self, 80 | version, 81 | height, 82 | timestamp, 83 | previousBlockHash, 84 | previousRoot, 85 | outputRoot, 86 | rangeProofRoot, 87 | kernelRoot, 88 | totalKernelOffset, 89 | outputMMRSize, 90 | kernelMMRSize, 91 | totalDifficulty, 92 | scalingDifficulty, 93 | nonce, 94 | proofOfWork): 95 | self.version = version 96 | self.height = height 97 | self.timestamp = timestamp 98 | self.previousBlockHash = previousBlockHash 99 | self.previousRoot = previousRoot 100 | self.outputRoot = outputRoot 101 | self.rangeProofRoot = rangeProofRoot 102 | self.kernelRoot = kernelRoot 103 | self.totalKernelOffset = totalKernelOffset 104 | self.outputMMRSize = outputMMRSize 105 | self.kernelMMRSize = kernelMMRSize 106 | self.totalDifficulty = totalDifficulty 107 | self.scalingDifficulty = scalingDifficulty 108 | self.nonce = nonce 109 | self.proofOfWork = proofOfWork 110 | 111 | # getters 112 | 113 | def getVersion(self): 114 | return self.version 115 | 116 | def getHeight(self): 117 | return self.height 118 | 119 | def getPreviousHash(self): 120 | return self.previousBlockHash 121 | 122 | def getPreviousRoot(self): 123 | return self.previousRoot 124 | 125 | def getTimestamp(self): 126 | return self.timestamp 127 | 128 | def getTotalDifficulty(self): 129 | return self.totalDifficulty 130 | 131 | def getScalingDifficulty(self): 132 | return self.scalingDifficulty 133 | 134 | def getTotalScalingDifficulty(self): 135 | return self.scalingDifficulty 136 | 137 | def getTotalKernelOffset(self): 138 | return self.totalKernelOffset 139 | 140 | def getNonce(self): 141 | return self.nonce 142 | 143 | # PoW 144 | 145 | def getProofOfWork(self): 146 | return self.proofOfWork 147 | 148 | def getEdgeBits(self): 149 | return self.proofOfWork.getEdgeBits() 150 | 151 | def getProofNonces(self): 152 | return self.proofOfWork.getProofNonces() 153 | 154 | def isPrimaryPoW(self): 155 | return self.proofOfWork.isPrimary() 156 | 157 | def isSecondaryPoW(self): 158 | return self.proofOfWork.isSecondary() 159 | 160 | # Merklish root stuffz 161 | 162 | def getOutputRoot(self): 163 | return self.outputRoot 164 | 165 | def getRangeProofRoot(self): 166 | return self.rangeProofRoot 167 | 168 | def getKernelRoot(self): 169 | return self.kernelRoot 170 | 171 | # Merkle Mountain Range Sizes 172 | 173 | def getOutputMMRSize(self): 174 | return self.outputMMRSize 175 | 176 | def getKernelMMRSize(self): 177 | return self.kernelMMRSize 178 | 179 | def getNumOutputs(self): 180 | return MMRIndex.at(self.outputMMRSize).getLeafIndex() 181 | 182 | def getNumKernels(self): 183 | return MMRIndex.at(self.kernelMMRSize).getLeafIndex() 184 | 185 | # serialization / deserialization 186 | 187 | def serialize(self, serializer: Serializer): 188 | serializer.write(self.version.to_bytes(2, 'big')) 189 | serializer.write(self.height.to_bytes(8, 'big')) 190 | serializer.write(self.timestamp.to_bytes(8, 'big')) 191 | serializer.write(self.previousBlockHash) 192 | serializer.write(self.previousRoot) 193 | serializer.write(self.outputRoot) 194 | serializer.write(self.rangeProofRoot) 195 | serializer.write(self.kernelRoot) 196 | serializer.write(self.totalKernelOffset.serialize()) # blinding factor 197 | serializer.write(self.outputMMRSize.to_bytes(8, 'big')) 198 | serializer.write(self.kernelMMRSize.to_bytes(8, 'big')) 199 | serializer.write(self.totalDifficulty.to_bytes(8, 'big')) 200 | serializer.write(self.scalingDifficulty.to_bytes(4, 'big')) 201 | serializer.write(self.nonce.to_bytes(8, 'big')) 202 | self.proofOfWork.serialize(serializer) 203 | 204 | @classmethod 205 | def deserialize(self, B: Serializer): 206 | version = int.from_bytes(B.read(2), 'big') 207 | height = int.from_bytes(B.read(8), 'big') 208 | timestamp = int.from_bytes(B.read(8), 'big') 209 | previousBlockHash = B.read(32) 210 | previousRoot = B.read(32) 211 | outputRoot = B.read(32) 212 | rangeProofRoot = B.read(32) 213 | kernelRoot = B.read(32) 214 | 215 | totalKernelOffset = BlindingFactor.deserialize(B.read(32)) 216 | 217 | outputMMRSize = int.from_bytes(B.read(8), 'big') 218 | kernelMMRSize = int.from_bytes(B.read(8), 'big') 219 | totalDifficulty = int.from_bytes(B.read(8), 'big') 220 | scalingDifficulty = int.from_bytes(B.read(4), 'big') 221 | nonce = int.from_bytes(B.read(8), 'big') 222 | 223 | proofOfWork = ProofOfWork.deserialize(B) 224 | 225 | return BlockHeader(version, 226 | height, 227 | timestamp, 228 | previousBlockHash, 229 | previousRoot, 230 | outputRoot, 231 | rangeProofRoot, 232 | kernelRoot, 233 | totalKernelOffset, 234 | outputMMRSize, 235 | kernelMMRSize, 236 | totalDifficulty, 237 | scalingDifficulty, 238 | nonce, 239 | proofOfWork) 240 | 241 | 242 | def toJSON(self): 243 | cuckooSolution = b'' 244 | for proofNonce in self.getProofOfWork().getProofNonces(): 245 | cuckooSolution += proofNonce.to_bytes(8, 'big') 246 | 247 | return { 248 | 'height': self.getHeight(), 249 | 'hash': self.getHash().hex(), 250 | 'version': self.getVersion(), 251 | 'timestamp_raw': self.getTimestamp(), 252 | 'timestamp_local': self.getTimestamp(), # TODO convert to local 253 | 'timestamp': self.getTimestamp(), # TODO convert to UTC 254 | 'previous': self.getPreviousHash().hex(), 255 | 'prev_root': self.getPreviousRoot().hex(), 256 | 'kernel_root': self.getKernelRoot().hex(), 257 | 'output_root': self.getOutputRoot().hex(), 258 | 'range_proof_root': self.getRangeProofRoot().hex(), 259 | 'output_mmr_size': self.getOutputMMRSize(), 260 | 'kernel_mmr_size': self.getKernelMMRSize(), 261 | 'total_kernel_offset': self.getTotalKernelOffset().hex(), 262 | 'secondary_scaling': self.getScalingDifficulty(), 263 | 'total_difficulty': self.getTotalDifficulty(), 264 | 'nonce': self.getNonce(), 265 | 'edge_bits': self.getProofOfWork().getEdgeBits(), 266 | 'cuckoo_solution': cuckooSolution.hex() 267 | } 268 | 269 | @classmethod 270 | def fromJSON(jsonString: str): 271 | O = json.loads(jsonString) 272 | return BlockHeader(O['version'], 273 | O['height'], 274 | O['timestamp'], 275 | O['previousBlockHash'], 276 | O['previousRoot'], 277 | O['rangeProofRoot'], 278 | O['kernelRoot'], 279 | O['totalKernelOffset'], 280 | O['outputMMRSize'], 281 | O['kernelMMRSize'], 282 | O['totalDifficulty'], 283 | O['scalingDifficulty'], 284 | O['nonce'], 285 | ProofOfWork.deserialize(O['proofOfWork'])) 286 | 287 | def getPreProofOfWork(self, serializer): 288 | serializer = BytesIO() 289 | self.serialize(serializer) 290 | return serializer.getvalue() 291 | 292 | # hashing 293 | 294 | def getHash(self): 295 | return self.proofOfWork.getHash() 296 | 297 | def shortHash(self): 298 | # TODO 299 | pass 300 | 301 | 302 | class FullBlock: 303 | def __init__(self, header: BlockHeader, body: TransactionBody, validated=False): 304 | self.header = header 305 | self.body = body 306 | self.validated = validated 307 | 308 | def getHeader(self): 309 | return self.header 310 | 311 | def getTransactionBody(self): 312 | return self.body 313 | 314 | def getInputs(self): 315 | return self.body.getInputs() 316 | 317 | def getOutputs(self): 318 | return self.body.getOutputs() 319 | 320 | def getKernels(self): 321 | return self.body.getKernels() 322 | 323 | def getInputCommitments(self): 324 | return [_input.getCommitment() for _input in self.getInputs()] 325 | 326 | def getOutputCommitments(self): 327 | return [_output.getCommitment() for _output in self.getOutputs()] 328 | 329 | def getTotalFees(self): 330 | return self.body.calcFee() 331 | 332 | def calcWeight(self): 333 | return self.body.calcWeight(self.getHeight()) 334 | 335 | def getHeight(self): 336 | return self.header.getHeight() 337 | 338 | def getPreviousHash(self): 339 | return self.header.getPreviousHash() 340 | 341 | def getTotalDifficulty(self): 342 | return self.header.getTotalDifficulty() 343 | 344 | def getTotalKernelOffset(self): 345 | return self.header.getTotalKernelOffset() 346 | 347 | def serialize(self): 348 | serializer = Serializer() 349 | self.header.serialize(serializer) 350 | self.body.serialize(serializer) 351 | return serializer.getvalue() 352 | 353 | @classmethod 354 | def deserialize(self, serializer: Serializer): 355 | header = BlockHeader.deserialize(serializer) 356 | body = TransactionBody.deserialize(serializer) 357 | return FullBlock(header, body) 358 | 359 | def toJSON(self): 360 | # transaction outputs 361 | outputs = [] 362 | for _output in self.getOutputs(): 363 | output_json = _output.toJSON() 364 | output_json['block_height'] = self.getHeight() 365 | outputs.append(output_json) 366 | 367 | return { 368 | 'header': self.header.toJSON(), 369 | 'inputs': [_input.toJSON() for _input in self.getInputs()], 370 | 'outputs': outputs, 371 | 'kernels': [kernel.toJSON() for kernel in self.getKernels()] 372 | } 373 | 374 | def getHash(self): 375 | return self.header.getHash() 376 | 377 | def wasValidated(self): 378 | return self.validated 379 | 380 | def markAsValidated(self): 381 | self.validated = True 382 | 383 | 384 | class CompactBlock: 385 | def __init__(self, header, nonce, fullOutputs, fullKernels, shortIds): 386 | self.header = header 387 | self.nonce = nonce 388 | self.outputs = fullOutputs 389 | self.kernels = fullKernels 390 | self.short_ids = shoftIds 391 | 392 | # getters 393 | 394 | def getHeader(self): 395 | return self.header 396 | 397 | def getNonce(self): 398 | return self.nonce 399 | 400 | def getOutputs(self): 401 | return self.outputs 402 | 403 | def getKernels(self): 404 | return self.kernels 405 | 406 | def getShortIds(self): 407 | return self.short_ids 408 | 409 | def getPreviousHash(self): 410 | return self.header.getPreviousHash() 411 | 412 | def getHeight(self): 413 | return self.header.getHeight() 414 | 415 | def getTotalDifficulty(self): 416 | return self.header.getTotalDifficulty() 417 | 418 | # serialization / deserialization 419 | 420 | def serialize(): 421 | # TODO check if there is a need to set byteorder='big' 422 | bytes_nonce = self.nonce.to_bytes(64) 423 | 424 | bytes_num_outputs = len(self.getOutputs()).to_bytes(64) 425 | bytes_num_kernels = len(self.getKernels()).to_bytes(64) 426 | bytes_num_short_ids = len(self.getShortIds()).to_bytes(64) 427 | 428 | outputs = self.getOutputs() 429 | bytes_outputs = outputs[0].serialize() 430 | for _output in outputs[1:]: 431 | bytes_outputs += _output.serialize() 432 | 433 | kernels = self.getKernels() 434 | bytes_kernels = kernels[0].serialize() 435 | for _kernel in kernels[1:]: 436 | bytes_kernels += _kernel.serialize() 437 | 438 | short_ids = self.getShortIds() 439 | bytes_short_ids = short_ids[0].serialize() 440 | for _short_id in short_ids[1:]: 441 | bytes_short_ids += _short_id.serialize() 442 | 443 | return bytes_nonce + bytes_num_otputs + bytes_num_kernels + bytes_num_short_ids + bytes_outputs + bytes_kernels + bytes_short_ids 444 | 445 | @classmethod 446 | def deserialize(byteString: bytes): 447 | nonce = int(byteString[0:64]) 448 | 449 | numOutputs = int(byteString[65:128]) 450 | numKernels = int(byteString[129:192]) 451 | numShortIds = int(byteString[193:256]) 452 | 453 | # TODO byteString should be shifted accordingly 454 | # TODO check how much it should be shifted at each iteration 455 | outputs = [] 456 | for i in range(numOutputs): 457 | outputs.prepend(TransactionOutput.deserialize(byteString)) 458 | 459 | kernels = [] 460 | for i in range(numKernels): 461 | kernels.prepend(TransactionKernel.deserialize(byteString)) 462 | 463 | shortIds = [] 464 | for i in range(numShortIds): 465 | shortIds.prepend(ShortId.deserialize(byteString)) 466 | 467 | return CompactBlock(nonce, outputs, kernels, shortIds) 468 | 469 | def toJSON(): 470 | # transaction outputs 471 | outputs = [] 472 | for _output in self.getOutputs(): 473 | output_json = _output.toJSON() 474 | output_json['block_height'] = self.getHeight() 475 | outputs.append(output_json) 476 | 477 | return { 478 | 'header': self.header.toJSON(), 479 | 'inputs': [_input.toJSON() for _input in self.getInputs()], 480 | 'outputs': outputs, 481 | 'kernels': [kernel.toJSON() for kernel in self.getKernels()] 482 | } 483 | 484 | # hashing 485 | def __hash__(): 486 | return hash(self.header) 487 | 488 | -------------------------------------------------------------------------------- /mimblewimble/codecs.py: -------------------------------------------------------------------------------- 1 | import base58 2 | import base64 3 | 4 | 5 | class Codec: 6 | @classmethod 7 | def encode(self, data: bytes, encoding='utf-8') -> str: 8 | raise Exception('unimplemented') 9 | 10 | @classmethod 11 | def decode(self, data: str) -> bytes: 12 | raise Exception('unimplemented') 13 | 14 | 15 | class HexCodec(Codec): 16 | @classmethod 17 | def encode(self, data: bytes, encoding='utf-8') -> str: 18 | return data.hex() 19 | 20 | @classmethod 21 | def decode(self, data: str) -> bytes: 22 | return bytes.fromhex(data) 23 | 24 | 25 | class Base58Codec(Codec): 26 | @classmethod 27 | def encode(self, data: bytes, encoding='utf-8') -> str: 28 | return base58.b58encode(data).decode(encoding) 29 | 30 | @classmethod 31 | def decode(self, data: str) -> bytes: 32 | return base58.b58decode(data) 33 | 34 | 35 | class Base64Codec(Codec): 36 | @classmethod 37 | def encode(self, data: bytes, encoding='utf-8') -> str: 38 | return base64.b64encode(data).decode(encoding) 39 | 40 | @classmethod 41 | def decode(self, data: str) -> bytes: 42 | return base64.b64decode(data) 43 | -------------------------------------------------------------------------------- /mimblewimble/consensus.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | 4 | class Consensus: 5 | # A grin is divisible to 10^9, following the SI prefixes 6 | grin_base = 1000000000 7 | 8 | # Milligrin, a thousand of a grin 9 | milli_grin = grin_base/1000 10 | 11 | # Microgrin, a thousand of a milligrin 12 | micro_grin = milli_grin/1000 13 | 14 | # Nanogrin, smallest unit, takes a billion to make a grin 15 | nano_grin = 1 16 | 17 | # The block subsidy amount, one grin per second on average 18 | reward = 60*grin_base 19 | 20 | # Weight of an input when counted against the max block weight capacity 21 | input_weight = 1 22 | 23 | # Weight of an output when counted against the max block weight capacity 24 | output_weight = 21 25 | 26 | # Weight of a kernel when counted against the max block weight capacity 27 | kernel_weight = 3 28 | 29 | # Total maximum block weight 30 | max_block_weight = 40000 31 | 32 | @classmethod 33 | def calculateWeightV4(self, num_inputs, num_outputs, num_kernels): 34 | return -1*num_inputs + 4*num_outputs + num_kernels 35 | 36 | @classmethod 37 | def calculateWeightV5(self, num_inputs, num_outputs, num_kernels): 38 | return num_inputs*Consensus.input_weight + num_outputs*Consensus.output_weight + num_kernels*Consensus.kernel_weight 39 | 40 | # Block interval, in seconds 41 | block_time_sec = 60 42 | 43 | # Nominal height for standards time intervals, hour is 60 blocks 44 | hour_height = 3600 / block_time_sec 45 | 46 | @classmethod 47 | def hours(self, num_hours): 48 | return num_hours*hour_height 49 | 50 | # A day is 1440 blocks 51 | day_height = 24*hour_height 52 | 53 | @classmethod 54 | def days(self, num_days): 55 | return num_days*day_height 56 | 57 | # A week is 10,080 blocks 58 | week_height = 7*day_height 59 | 60 | @classmethod 61 | def weeks(self, num_weeks): 62 | return num_weeks*week_height 63 | 64 | # A year is 524,160 blocks 65 | year_height = 52*week_height 66 | 67 | @classmethod 68 | def years(self, num_years): 69 | return num_years*year_height 70 | 71 | # Number of blocks before a coinbase matures and can be spent 72 | # set to nominal number of block in one day (1440 with 1-minute blocks) 73 | coinbase_maturity = (24*60*60)/block_time_sec 74 | 75 | @classmethod 76 | def getMaxCoinbaseHeight(blockHeight, automated_testing=False): 77 | if automated_testing: 78 | return math.max(blockHeight, 25)-20 79 | return math.max(blockHeight, coinbase_maturity)-coinbase_maturity 80 | 81 | # Default number of blocks in the past when cross-block cut-through will start happening 82 | cut_through_horizon = week_height 83 | 84 | @classmethod 85 | def getHorizonHeight(block_height): 86 | return math.max(block_height, cut_through_horizon) - cut_through_horizon 87 | 88 | # Default number of blocks in the past to determine the height where we request a txhashset 89 | state_sync_threshold = 2*day_height 90 | 91 | # Time window in blocks to calculate block time median 92 | median_time_window = 11 93 | 94 | # Index at half the desired median 95 | median_time_index = median_time_window/2 96 | 97 | # Number of blocks used to calculate difficulty adjustments 98 | difficulty_adjust_window = hour_height 99 | 100 | # Average time span of the difficulty adjustment window 101 | block_time_window = difficulty_adjust_window*block_time_sec 102 | 103 | # Maximum size time window used for difficulty adjustments 104 | upper_time_bound = block_time_window*2 105 | 106 | # Minimum size time window used for difficulty adjustments 107 | lower_time_bound = block_time_window/2 108 | 109 | # default Future Time Limit (FTL) of 5 minutes 110 | default_future_time_limit_sec = 5*block_time_sec 111 | 112 | # Refuse blocks more than 12 block intervals in the future. 113 | @classmethod 114 | def getMaxBlockTime(current_time): 115 | return currentTime + default_future_time_limit_sec 116 | 117 | # Difficulty adjustment half life (actually, 60s * number of 0s-blocks to raise diff by factor e) is 4 hours 118 | wtema_half_life = 4*3600 119 | 120 | # Cuckoo-cycle proof size (cycle length) 121 | proofsize = 42 122 | 123 | # Default Cuckoo Cycle size shift used for mining and validating. 124 | default_min_edge_bits = 31 125 | 126 | # Secondary proof-of-work size shift, meant to be ASIC resistant. 127 | second_pow_edge_bits = 29 128 | 129 | # Original reference edge_bits to compute difficulty factors for higher Cuckoo graph sizes, changing this would hard fork 130 | base_edge_bits = 24 131 | 132 | # Clamp factor to use for difficulty adjustment 133 | # Limit value to within this factor of goal 134 | clamp_factor = 2 135 | 136 | # Dampening factor to use for difficulty adjustment 137 | dma_damp_factor = 3 138 | 139 | # Dampening factor to use for AR scale calculation. 140 | ar_scale_damp_factor = 13 141 | 142 | # Minimum scaling factor for AR pow, enforced in diff retargetting 143 | # avoids getting stuck when trying to increase ar_scale subject to dampening 144 | min_ar_scale = ar_scale_damp_factor 145 | 146 | # Minimum difficulty, enforced in diff retargetting 147 | # avoids getting stuck when trying to increase difficulty subject to dampening 148 | min_dma_difficulty = dma_damp_factor 149 | 150 | # Compute weight of a graph as number of siphash bits defining the graph 151 | # Must be made dependent on height to phase out smaller size over the years 152 | @classmethod 153 | def graphWeight(self, height, edge_bits): 154 | expiry_height = int(year_height) 155 | xpr_edge_bits = int(edge_bits) 156 | if edge_bits == 31 and height >= expiry_height: 157 | xpr_edge_bits -= math.min(xpr_edge_bits, 1+(height-expiry_height)/week_height) 158 | return 2 << (edge_bits-base_edge_bits)*xpr_edge_bits 159 | 160 | # Initial mining secondary scale 161 | @classmethod 162 | def initialGraphWeight(self): 163 | return graphWeight(0, second_pow_edge_bits) 164 | 165 | # Move value linearly toward a goal 166 | @classmethod 167 | def damp(actual, goal, damp_factor): 168 | return (actual + (damp_factor-1))/damp_factor 169 | 170 | # limit value to be within some factor from a goal 171 | @classmethod 172 | def clamp(actual, goal, clamp_factor): 173 | return math.max(goal/clamp_factor, math.min(actual, goal*clamp_factor)) 174 | 175 | # Ratio the secondary proof of work should take over the primary, as a function of block height (time). 176 | # Starts at 90% losing a percent approximately every week. Represented as an integer between 0 and 100. 177 | @classmethod 178 | def secondaryPOWRatio(self, height): 179 | return 90-math.min(90, (height/(2*year_height/90))) 180 | 181 | @classmethod 182 | def scalingDifficulty(self, edgeBits): 183 | # TODO find a pure python way to ensure "2" is of uint64_t type 184 | return 2 << (edgeBits-base_edge_bits)*edgeBits 185 | 186 | # minimum solution difficulty after HardFork4 when PoW becomes primary only Cuckatoo32+ 187 | # TODO find a pure python way to ensure "2" is of uint64_t type 188 | c32_graph_weight = 2 << (32-base_edge_bits)*32 189 | 190 | def min_wtema_graph_weight(self, testnet=False): 191 | if testnet: 192 | return self.GraphWeight(0, second_pow_edge_bits) 193 | return c32_graph_weight 194 | 195 | # Fork every 6 months. 196 | hard_fork_interval = year_height / 2 197 | 198 | # Floonet-only hardforks 199 | floonet_first_hard_fork = 185040 200 | floonet_second_hard_fork = 298080 201 | floonet_third_hard_fork = 552960 202 | floonet_fourth_hard_fork = 642240 203 | 204 | def getHeaderVersion(height, testnet=False): 205 | if testnet: 206 | if height < Consensus.floonet_first_hard_fork: 207 | return 1 208 | elif height < Consensus.floonet_second_hard_fork: 209 | return 2 210 | elif height < Consensus.floonet_third_hard_fork: 211 | return 3 212 | elif height < Consensus.floonet_fourth_hard_fork: 213 | return 4 214 | else: 215 | if height < Consensus.hard_fork_interval: 216 | return 1 217 | elif height < 2*Consensus.hard_fork_interval: 218 | return 2 219 | elif height < 3*Consensus.hard_fork_interval: 220 | return 3 221 | elif height < 4*Consensus.hard_fork_interval: 222 | return 4 223 | return 5 224 | 225 | def __init__(self): 226 | pass 227 | 228 | @classmethod 229 | def isPrimary(edgeBits): 230 | pass 231 | 232 | @classmethod 233 | def isSecondary(edgeBits): 234 | pass 235 | -------------------------------------------------------------------------------- /mimblewimble/crypto/age.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from mimblewimble.serializer import Serializer 4 | 5 | AGE_INTRO = b'age-encryption.org/v1' 6 | AGE_RECIPIENT_PREFIX = b'->' 7 | AGE_FOOTER_PREFIX = b'---' 8 | AGE_AEAD = b'ChaChaPoly' 9 | 10 | 11 | class AgeRecipientBody: 12 | def __init__(self, body: bytes): 13 | self.body = body 14 | 15 | def serialize(self, serializer: Serializer): 16 | serializer.write(self.body + b'\n') 17 | 18 | @classmethod 19 | def deserialize(self, serializer: Serializer): 20 | pnt = serializer.pnt 21 | line = serializer.readline(clean_newline=True) 22 | 23 | is_recipient = line.startswith( 24 | AGE_RECIPIENT_PREFIX) 25 | is_footer = line.startswith( 26 | AGE_FOOTER_PREFIX) 27 | 28 | if is_recipient or is_footer or line == b'': 29 | serializer.resetPointer(n=pnt) 30 | return None 31 | 32 | return AgeRecipientBody(line) 33 | 34 | 35 | class AgeRecipient: 36 | def __init__(self, _type, args=[], body=[]): 37 | self._type = _type 38 | self.args = args 39 | self.body = body 40 | 41 | def append_body(self, body: AgeRecipientBody): 42 | self.body.append(body) 43 | 44 | def serialize(self, serializer: Serializer): 45 | serializer.write( 46 | b'-> ' + self._type + b' ' + b' '.join( 47 | self.args) + b'\n') 48 | if len(self.body) > 0: 49 | for body in self.body: 50 | body.serialize(serializer) 51 | 52 | @classmethod 53 | def deserialize(self, serializer: Serializer): 54 | pnt = serializer.pnt 55 | line = serializer.readline(clean_newline=True) 56 | 57 | if not line.startswith( 58 | AGE_RECIPIENT_PREFIX): 59 | serializer.resetPointer(n=pnt) 60 | return None 61 | 62 | splitted = line.split() 63 | if len(splitted) < 2: 64 | serializer.resetPointer(n=pnt) 65 | return None 66 | 67 | _type, *args = splitted[1:] 68 | recipient = AgeRecipient(_type, args, body=[]) 69 | 70 | body = AgeRecipientBody.deserialize(serializer) 71 | while body is not None: 72 | recipient.append_body(body) 73 | body = AgeRecipientBody.deserialize( 74 | serializer) 75 | 76 | return recipient 77 | 78 | 79 | class AgeHeader: 80 | def __init__(self, recipients=[]): 81 | self.recipients = recipients 82 | 83 | def serialize(self, serializer: Serializer): 84 | for recipient in self.recipients: 85 | recipient.serialize(serializer) 86 | 87 | @classmethod 88 | def deserialize(self, serializer: Serializer): 89 | recipients = [] 90 | 91 | recipient = AgeRecipient.deserialize(serializer) 92 | while recipient is not None: 93 | recipients.append(recipient) 94 | recipient = AgeRecipient.deserialize( 95 | serializer) 96 | 97 | return AgeHeader(recipients=recipients) 98 | 99 | 100 | class AgeBody: 101 | def __init__(self, body: bytes): 102 | self.body = body 103 | 104 | def serialize(self, serializer: Serializer): 105 | serializer.write( 106 | b'\n' + AGE_FOOTER_PREFIX + b' ' + self.body) 107 | 108 | @classmethod 109 | def deserialize(self, serializer: Serializer): 110 | remaining = serializer.readremaining() 111 | splitted = remaining.split(AGE_FOOTER_PREFIX) 112 | if len(splitted) < 2: 113 | return None 114 | return AgeBody(splitted[1].strip()) 115 | 116 | 117 | class AgeMessage: 118 | def __init__(self, pre: bytes, header: AgeHeader, body: AgeBody): 119 | self.pre = pre 120 | self.header = header 121 | self.body = body 122 | 123 | def serialize(self, serializer: Serializer): 124 | serializer.write(self.pre + AGE_INTRO + b'\n') 125 | self.header.serialize(serializer) 126 | self.body.serialize(serializer) 127 | 128 | @classmethod 129 | def deserialize(self, serializer: Serializer): 130 | line = serializer.readline(clean_newline=True) 131 | if not line.find(AGE_INTRO): 132 | return None 133 | splitted = line.split(AGE_INTRO) 134 | if len(splitted) > 1: 135 | pre = splitted[0] 136 | else: 137 | pre = b'' 138 | header = AgeHeader.deserialize(serializer) 139 | if header is None: 140 | return None 141 | body = AgeBody.deserialize(serializer) 142 | if body is None: 143 | return None 144 | return AgeMessage(pre, header, body) 145 | -------------------------------------------------------------------------------- /mimblewimble/crypto/aggsig.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from secp256k1_zkp_mw import SECP256K1_CONTEXT_SIGN 4 | from secp256k1_zkp_mw import SECP256K1_CONTEXT_VERIFY 5 | 6 | from secp256k1_zkp_mw import secp256k1_context_create 7 | from secp256k1_zkp_mw import secp256k1_context_destroy 8 | from secp256k1_zkp_mw import secp256k1_context_randomize 9 | 10 | from secp256k1_zkp_mw import secp256k1_aggsig_export_secnonce_single 11 | from secp256k1_zkp_mw import secp256k1_aggsig_sign_single 12 | from secp256k1_zkp_mw import secp256k1_aggsig_verify_single 13 | from secp256k1_zkp_mw import secp256k1_aggsig_add_signatures_single 14 | 15 | from secp256k1_zkp_mw import secp256k1_pedersen_commitment_parse 16 | from secp256k1_zkp_mw import secp256k1_pedersen_commitment_to_pubkey 17 | 18 | from secp256k1_zkp_mw import secp256k1_ec_pubkey_parse 19 | 20 | from secp256k1_zkp_mw import secp256k1_schnorrsig_parse 21 | from secp256k1_zkp_mw import secp256k1_schnorrsig_verify_batch 22 | 23 | from secp256k1_zkp_mw import secp256k1_ecdsa_signature_parse_der 24 | from secp256k1_zkp_mw import secp256k1_ecdsa_signature_parse_compact 25 | from secp256k1_zkp_mw import secp256k1_ecdsa_signature_serialize_compact 26 | 27 | from mimblewimble.crypto.commitment import Commitment 28 | from mimblewimble.crypto.secret_key import SecretKey 29 | from mimblewimble.crypto.public_key import PublicKey 30 | from mimblewimble.crypto.signature import Signature 31 | 32 | class AggSig: 33 | def __init__(self): 34 | self.ctx = secp256k1_context_create( 35 | SECP256K1_CONTEXT_SIGN | SECP256K1_CONTEXT_VERIFY) 36 | self.MAX_WIDTH = 1 << 20 37 | self.SCRATCH_SPACE_SIZE = 256 * self.MAX_WIDTH 38 | 39 | def __del__(self): 40 | secp256k1_context_destroy(self.ctx) 41 | 42 | def generateSecureNonce(self): 43 | seed = os.urandom(32) 44 | nonce = secp256k1_aggsig_export_secnonce_single( 45 | self.ctx, seed) 46 | return SecretKey(nonce) 47 | 48 | def buildSignature( 49 | self, secret_key: SecretKey, 50 | commitment: Commitment, message: bytes): 51 | seed = os.urandom(32) 52 | secp256k1_context_randomize(self.ctx, seed) 53 | parsed_commitment = secp256k1_pedersen_commitment_parse( 54 | self.ctx, commitment.getBytes()) 55 | pubkey = secp256k1_pedersen_commitment_to_pubkey( 56 | self.ctx, parsed_commitment) 57 | signature = secp256k1_aggsig_sign_single( 58 | self.ctx, message, secret_key.getBytes(), 59 | None, None, None, None, pubkey, seed) 60 | compact_signature = secp256k1_ecdsa_signature_serialize_compact( 61 | self.ctx, signature) 62 | return Signature(compact_signature, compact=True) 63 | 64 | def calculatePartialSignature( 65 | self, secretKey: SecretKey, secretNonce: SecretKey, 66 | sumPubKeys: PublicKey, sumPubNonces: PublicKey, 67 | message: bytes): 68 | seed = os.urandom(32) 69 | secp256k1_context_randomize(self.ctx, seed) 70 | pubKeyForE = secp256k1_ec_pubkey_parse( 71 | self.ctx, sumPubKeys.getBytes()) 72 | pubNoncesForE = secp256k1_ec_pubkey_parse( 73 | self.ctx, sumPubNonces.getBytes()) 74 | signature = secp256k1_aggsig_sign_single( 75 | self.ctx, message, 76 | secretKey.getBytes(), secretNonce.getBytes(), 77 | None, pubNoncesForE, pubNoncesForE, pubKeyForE, seed) 78 | compact_signature = secp256k1_ecdsa_signature_serialize_compact( 79 | self.ctx, signature) 80 | return Signature(compact_signature, compact=True) 81 | 82 | def verifyPartialSignature( 83 | self, partialSignature: Signature, 84 | publicKey: PublicKey, sumPubKeys: PublicKey, 85 | sumPubNonces: PublicKey, message: bytes): 86 | signature = secp256k1_ecdsa_signature_parse_compact( 87 | self.ctx, partialSignature.getSignatureBytes()) 88 | pubkey = secp256k1_ec_pubkey_parse( 89 | self.ctx, publicKey.getBytes()) 90 | sumPubKey = secp256k1_ec_pubkey_parse( 91 | self.ctx, sumPubKeys.getBytes()) 92 | sumNoncesPubKey = secp256k1_ec_pubkey_parse( 93 | self.ctx, sumPubNonces.getBytes()) 94 | is_valid = secp256k1_aggsig_verify_single( 95 | self.ctx, signature, message, 96 | sumNoncesPubKey, pubkey, sumPubKey, None, True) 97 | return is_valid 98 | 99 | def aggregateSignatures(self, signatures, sumPubNonces: PublicKey): 100 | pubNonces = secp256k1_ec_pubkey_parse( 101 | self.ctx, sumPubNonces.getBytes()) 102 | parsed_signatures = self.parseCompactSignatures(signatures) 103 | aggregate = secp256k1_aggsig_add_signatures_single( 104 | self.ctx, parsed_signatures, pubNonces) 105 | return Signature(aggregate) 106 | 107 | def verifyAggregateSignatures(self, signatures, commitments, messages): 108 | parsedPubKeys = [] 109 | for commitment in commitments: 110 | parsed_commitment = secp256k1_pedersen_commitment_parse( 111 | self.ctx, commitment.getBytes()) 112 | pubkey = secp256k1_pedersen_commitment_to_pubkey( 113 | self.ctx, parsed_commitment) 114 | parsedPubKeys.append(pubkey) 115 | parsedSignatures = [] 116 | for signature in signatures: 117 | parsed_signature = secp256k1_schnorrsig_parse( 118 | self.ctx, signature.getBytes()) 119 | parsedSignatures.append(parsed_signature) 120 | scratch = secp256k1_scratch_space_create( 121 | self.ctx, self.SCRATCH_SPACE_SIZE) 122 | is_valid = secp256k1_schnorrsig_verify_batch( 123 | self.ctx, scratch, parsedSignatures, messages, parsedPubKeys) 124 | return is_valid 125 | 126 | def verifyAggregateSignature( 127 | self, signature: Signature, publicKey: PublicKey, message: bytes): 128 | parsedPubKey = secp256k1_ec_pubkey_parse( 129 | self.ctx, publicKey.getBytes()) 130 | is_valid = secp256k1_aggsig_verify_single( 131 | self.ctx, signature.getSignatureBytes(), message, 132 | None, parsedPubKey, parsedPubKey, None, False) 133 | return is_valid 134 | 135 | def parseCompactSignatures(self, signatures): 136 | parsed_signatures = [] 137 | for signature in signatures: 138 | parsed_signature = secp256k1_ecdsa_signature_parse_compact( 139 | self.ctx, signature.getSignatureBytes()) 140 | parsed_signatures.append(parsed_signature) 141 | return parsed_signatures 142 | 143 | def toCompact(self, signature: Signature): 144 | assert not signature.isCompact() 145 | parsed_signature = secp256k1_ecdsa_signature_parse_der( 146 | self.ctx, signature.getSignatureBytes()) 147 | compact_signature = secp256k1_ecdsa_signature_serialize_compact( 148 | self.ctx, parsed_signature) 149 | return Signature(compact_signature, compact=True) 150 | -------------------------------------------------------------------------------- /mimblewimble/crypto/bulletproof.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from typing import List, Tuple 4 | from enum import Enum 5 | 6 | from secp256k1_zkp_mw import SECP256K1_CONTEXT_SIGN 7 | from secp256k1_zkp_mw import SECP256K1_CONTEXT_VERIFY 8 | 9 | from secp256k1_zkp_mw import secp256k1_generator_const_h 10 | from secp256k1_zkp_mw import secp256k1_generator_const_g 11 | 12 | from secp256k1_zkp_mw import secp256k1_context_create 13 | from secp256k1_zkp_mw import secp256k1_context_destroy 14 | from secp256k1_zkp_mw import secp256k1_context_randomize 15 | 16 | from secp256k1_zkp_mw import secp256k1_scratch_space_create 17 | from secp256k1_zkp_mw import secp256k1_scratch_space_destroy 18 | 19 | from secp256k1_zkp_mw import secp256k1_bulletproof_generators_create 20 | from secp256k1_zkp_mw import secp256k1_bulletproof_rangeproof_verify_multi 21 | from secp256k1_zkp_mw import secp256k1_bulletproof_rangeproof_prove 22 | from secp256k1_zkp_mw import secp256k1_bulletproof_rangeproof_rewind 23 | 24 | from mimblewimble.crypto.commitment import Commitment 25 | from mimblewimble.crypto.secret_key import SecretKey 26 | from mimblewimble.crypto.rangeproof import RangeProof 27 | from mimblewimble.crypto.pedersen import Pedersen 28 | 29 | from mimblewimble.models.transaction import BlindingFactor 30 | 31 | 32 | class EBulletproofType(Enum): 33 | ORIGINAL = 0 34 | ENHANCED = 1 35 | 36 | 37 | class ProofMessage: 38 | def __init__(self, proof_message: bytes): 39 | self.proof_message = proof_message 40 | 41 | def getBytes(self): 42 | return self.proof_message 43 | 44 | @classmethod 45 | def fromKeyIndices( 46 | self, key_indices: List[int], bulletproof_type: EBulletproofType): 47 | padded_path = [0x00 for i in range(20)] 48 | if bulletproof_type == EBulletproofType.ENHANCED: 49 | # padded_path[0] 0x00 reserved 50 | # padded_path[1] 0x00 is wallet type 51 | padded_path[2] = 0x01 # switch commits 52 | padded_path[3] = len(key_indices) 53 | 54 | i = 4 55 | for j, key_index in enumerate(key_indices): 56 | padded_path[j + 4] = key_index 57 | 58 | return ProofMessage(bytes(padded_path)) 59 | 60 | def toKeyIndices(self, bulletproof_type: EBulletproofType): 61 | proof_message = list(self.proof_message) 62 | length = 3 63 | if bulletproof_type == EBulletproofType.ENHANCED: 64 | if proof_message[0] != 0x0: 65 | raise ValueError( 66 | 'Reserved, first value of proof message must be zero') 67 | wallet_type = proof_message[1] 68 | switch_commits = proof_message[2] 69 | length = proof_message[3] 70 | else: 71 | try: 72 | for j in range(4): 73 | assert proof_message[j] == 0x0 74 | except: 75 | raise ValueError( 76 | 'Expected first 4 bytes of proof message to be 0x00') 77 | i = 4 78 | if length == 0: 79 | length = 3 80 | 81 | key_indices = [0x00 for j in range(length)] 82 | for j in range(length): 83 | key_indices[j] = proof_message[i + j] 84 | return key_indices 85 | 86 | 87 | class RewoundProof: 88 | def __init__( 89 | self, amount, blinding_factor: SecretKey, message: ProofMessage): 90 | self.amount = amount 91 | self.blinding_factor = blinding_factor 92 | self.message = message 93 | 94 | def getAmount(self): 95 | return self.amount 96 | 97 | def getBlindingFactor(self): 98 | return self.blinding_factor 99 | 100 | def getProofMessage(self): 101 | return self.message 102 | 103 | def toKeyIndices(self, bulletproof_type: EBulletproofType): 104 | return self.message.toKeyIndices(bulletproof_type) 105 | 106 | 107 | class Bulletproof: 108 | def __init__(self): 109 | self.cache = [] 110 | self.ctx = secp256k1_context_create( 111 | SECP256K1_CONTEXT_SIGN | SECP256K1_CONTEXT_VERIFY) 112 | 113 | self.MAX_WIDTH = 1 << 20 114 | self.SCRATCH_SPACE_SIZE = 256 * self.MAX_WIDTH 115 | self.MAX_GENERATORS = 256; 116 | 117 | self.generators = secp256k1_bulletproof_generators_create( 118 | self.ctx, secp256k1_generator_const_g, self.MAX_GENERATORS); 119 | 120 | def flushCache(self): 121 | self.cache = [] 122 | 123 | def __del__(self): 124 | secp256k1_context_destroy(self.ctx) 125 | 126 | def verifyBulletproofs( 127 | self, range_proofs: List[Tuple[Commitment, RangeProof]]): 128 | num_bits = 64 129 | 130 | _, first_rangeproof = range_proofs[0] 131 | proof_length = len(first_rangeproof.getProofBytes()) 132 | 133 | commitments = [] 134 | bulletproofs = [] 135 | for commitment, rangeproof in range_proofs: 136 | if commitment.getBytes() in self.cache: 137 | continue 138 | commitments.append(commitment) 139 | bulletproofs.append(rangeproof.getProofBytes()) 140 | 141 | if len(commitments) == 0: 142 | return True 143 | 144 | # array of generator multiplied by value in pedersen commitments 145 | # (cannot be NULL) 146 | value_generators = secp256k1_bulletproof_generators_create( 147 | self.ctx, secp256k1_generator_const_h, len(commitments)) 148 | 149 | parsed_commitments = Pedersen.convertCommitments(self.ctx, commitments) 150 | scratch = secp256k1_scratch_space_create(self.ctx, self.SCRATCH_SPACE_SIZE) 151 | is_valid = secp256k1_bulletproof_rangeproof_verify_multi( 152 | self.ctx, scratch, value_generators, 153 | bulletproofs, None, 154 | parsed_commitments, len(commitments), 155 | proof_length, 156 | [secp256k1_generator_const_h, secp256k1_generator_const_h], None) 157 | secp256k1_scratch_space_destroy(scratch) 158 | 159 | if not is_valid: 160 | return False 161 | 162 | for commitment in commitments: 163 | commitment_bytes = commitment.getBytes() 164 | if commitment_bytes not in self.cache: 165 | self.cache.append(commitment_bytes) 166 | 167 | return True 168 | 169 | def generateRangeProof( 170 | self, amount: int, 171 | key: SecretKey, 172 | private_nonce: SecretKey, 173 | rewind_nonce: SecretKey, 174 | proof_message: ProofMessage): 175 | seed = os.urandom(32) 176 | secp256k1_context_randomize(self.ctx, seed) 177 | 178 | scratch = secp256k1_scratch_space_create( 179 | self.ctx, self.SCRATCH_SPACE_SIZE) 180 | proof_bytes = secp256k1_bulletproof_rangeproof_prove( 181 | self.ctx, 182 | scratch, 183 | self.generators, 184 | None, 185 | None, 186 | None, 187 | [amount], 188 | None, 189 | [key.getBytes()], 190 | None, 191 | secp256k1_generator_const_h, 192 | 64, 193 | rewind_nonce.getBytes(), 194 | private_nonce.getBytes(), 195 | None, 196 | proof_message.getBytes() 197 | ) 198 | secp256k1_scratch_space_destroy(scratch) 199 | return RangeProof(proof_bytes) 200 | 201 | def rewindProof( 202 | self, 203 | commitment: Commitment, rangeproof: RangeProof, nonce: SecretKey): 204 | parsed_commitments = Pedersen.convertCommitments(self.ctx, [commitment]) 205 | commit = parsed_commitments[0] 206 | 207 | value, blind, message_bytes = secp256k1_bulletproof_rangeproof_rewind( 208 | self.ctx, rangeproof.getProofBytes(), 0, commit, 209 | secp256k1_generator_const_h, nonce.getBytes(), None) 210 | 211 | blinding_factor = BlindingFactor(blind) 212 | message = ProofMessage(message_bytes) 213 | 214 | return RewoundProof(value, blinding_factor, message) 215 | -------------------------------------------------------------------------------- /mimblewimble/crypto/commitment.py: -------------------------------------------------------------------------------- 1 | from mimblewimble.serializer import Serializer 2 | 3 | from mimblewimble.crypto.public_key import PublicKey 4 | 5 | 6 | class Commitment: 7 | def __init__(self, commitmentBytes: bytes): 8 | self.commitmentBytes = commitmentBytes 9 | 10 | # operators 11 | 12 | def __lt__(self, other): 13 | return self.getBytes() < other.getBytes() 14 | 15 | def __eq__(self, other): 16 | return self.getBytes() == other.getBytes() 17 | 18 | def __ne__(self, other): 19 | return self.getBytes() != other.getBytes() 20 | 21 | # getters 22 | 23 | def getBytes(self): 24 | return self.commitmentBytes 25 | 26 | def __int__(self): 27 | return int(self.commitmentBytes) 28 | 29 | # serialization / deserialization 30 | 31 | def serialize(self, serializer: Serializer): 32 | assert len(self.getBytes()) == 33 33 | serializer.write(self.getBytes()) 34 | 35 | @classmethod 36 | def deserialize(self, serializer: Serializer): 37 | return Commitment(serializer.read(33)) 38 | 39 | def hex(self): 40 | return self.getBytes().hex() 41 | 42 | @classmethod 43 | def fromHex(self, _hex: str): 44 | return Commitment(bytes.fromhex(_hex)) 45 | 46 | @classmethod 47 | def fromPublicKey(self, pk: PublicKey): 48 | # TODO 49 | return Commitment(b'\x00') 50 | 51 | def toJSON(self): 52 | return self.hex() 53 | 54 | def format(self): 55 | return 'Commitment{' + self.hex() + '}' 56 | 57 | 58 | -------------------------------------------------------------------------------- /mimblewimble/crypto/pedersen.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | from ecdsa import SigningKey, SECP256k1 3 | 4 | from secp256k1_zkp_mw import SECP256K1_CONTEXT_SIGN 5 | from secp256k1_zkp_mw import SECP256K1_CONTEXT_VERIFY 6 | 7 | from secp256k1_zkp_mw import secp256k1_generator_const_h 8 | from secp256k1_zkp_mw import secp256k1_generator_const_g 9 | 10 | from secp256k1_zkp_mw import secp256k1_context_create 11 | from secp256k1_zkp_mw import secp256k1_context_destroy 12 | from secp256k1_zkp_mw import secp256k1_pedersen_commit 13 | from secp256k1_zkp_mw import secp256k1_pedersen_commitment_serialize 14 | from secp256k1_zkp_mw import secp256k1_pedersen_commitment_parse 15 | from secp256k1_zkp_mw import secp256k1_pedersen_commit_sum 16 | from secp256k1_zkp_mw import secp256k1_pedersen_blind_sum 17 | from secp256k1_zkp_mw import secp256k1_blind_switch 18 | from secp256k1_zkp_mw import secp256k1_ec_pubkey_parse 19 | from secp256k1_zkp_mw import secp256k1_pubkey_to_pedersen_commitment 20 | 21 | from mimblewimble.models.transaction import BlindingFactor 22 | 23 | from mimblewimble.crypto.commitment import Commitment 24 | from mimblewimble.crypto.secret_key import SecretKey 25 | from mimblewimble.crypto.public_key import PublicKey 26 | 27 | 28 | class Pedersen: 29 | def __init__(self): 30 | self.ctx = secp256k1_context_create( 31 | SECP256K1_CONTEXT_SIGN | SECP256K1_CONTEXT_VERIFY) 32 | 33 | # see 34 | # https://github.com/mimblewimble/rust-secp256k1-zkp/blob/304ef7264a0dd9ef4efaaa0e28480cb030c18265/src/constants.rs#L138-L188 35 | # https://github.com/GrinPlusPlus/GrinPlusPlus/blob/b7b06a1ab6c0e7f7263878f788ea7e4203dcab7c/src/Crypto/SwitchGeneratorPoint.h#L5-L15 36 | x = bytes.fromhex( 37 | 'b860f56795fc03f3c21685383d1b5a2f2954f49b7e398b8d2a0193933621155f') 38 | y = bytes.fromhex( 39 | 'a43f09d32caa8f53423f427403a56a3165a5a69a74cf56fc5901a2dca6c5c43a') 40 | generator_j_pub_bytes = bytes([0x04]) + x + y 41 | self.generator_j_pub = secp256k1_ec_pubkey_parse( 42 | self.ctx, generator_j_pub_bytes) 43 | 44 | def __del__(self): 45 | secp256k1_context_destroy(self.ctx) 46 | 47 | def commit(self, value: int, blindingFactor: BlindingFactor): 48 | commitment = secp256k1_pedersen_commit( 49 | self.ctx, 50 | blindingFactor.getBytes(), 51 | value, 52 | secp256k1_generator_const_h, 53 | secp256k1_generator_const_g) 54 | serialized = secp256k1_pedersen_commitment_serialize( 55 | self.ctx, commitment) 56 | return Commitment(serialized) 57 | 58 | def commitSum(self, positive: List[Commitment], negative: List[Commitment]): 59 | positive_commitments = Pedersen.convertCommitments(self.ctx, positive) 60 | negative_commitments = Pedersen.convertCommitments(self.ctx, negative) 61 | commitment = secp256k1_pedersen_commit_sum( 62 | self.ctx, positive_commitments, negative_commitments) 63 | serialized = secp256k1_pedersen_commitment_serialize( 64 | self.ctx, commitment) 65 | return Commitment(serialized) 66 | 67 | def blindSum(self, positive: List[Commitment], negative: List[Commitment]): 68 | positives = [p.getBytes() for p in positive] 69 | negatives = [p.getBytes() for p in negative] 70 | blinding_factors = positives + negatives 71 | summed = secp256k1_pedersen_blind_sum( 72 | self.ctx, blinding_factors, len(positive)) 73 | return BlindingFactor(summed) 74 | 75 | def blindSwitch(self, blinding_factor: SecretKey, amount: int): 76 | blind_switch = secp256k1_blind_switch( 77 | self.ctx, 78 | blinding_factor.getBytes(), 79 | amount, 80 | secp256k1_generator_const_h, 81 | secp256k1_generator_const_g, 82 | self.generator_j_pub) 83 | return SecretKey(blind_switch) 84 | 85 | def toCommitment(self, public: PublicKey): 86 | pk = secp256k1_ec_pubkey_parse( 87 | self.ctx, public.getBytes()) 88 | commitment = secp256k1_pubkey_to_pedersen_commitment( 89 | self.ctx, pk) 90 | serialized = secp256k1_pedersen_commitment_serialize( 91 | self.ctx, commitment) 92 | return Commitment(serialized) 93 | 94 | @classmethod 95 | def convertCommitments(self, ctx, commitments: List[Commitment]): 96 | converted = [] 97 | for commitment in commitments: 98 | parsed = secp256k1_pedersen_commitment_parse( 99 | ctx, commitment.getBytes()) 100 | converted.append(parsed) 101 | return converted 102 | -------------------------------------------------------------------------------- /mimblewimble/crypto/public_key.py: -------------------------------------------------------------------------------- 1 | from mimblewimble.serializer import Serializer 2 | 3 | 4 | class PublicKey: 5 | def __init__(self, key: bytes): 6 | self.compressed_key = key 7 | 8 | # getters and setters 9 | 10 | def getBytes(self): 11 | return self.compressed_key 12 | 13 | # operators 14 | 15 | def __eq__(self, other): 16 | return self.compressed_key == other.compressed_key 17 | 18 | def __ne__(self, other): 19 | return self.compressed_key != other.compressed_key 20 | 21 | def __len__(self): 22 | return len(self.compressed_key) 23 | 24 | # serialization / deserialization 25 | 26 | def serialize(self, serializer: Serializer): 27 | serializer.write(self.compressed_key) 28 | 29 | @classmethod 30 | def deserialize(self, serializer: Serializer, NUM_BYTES=33): 31 | compressed_key = serializer.read(NUM_BYTES) 32 | return PublicKey(compressed_key) 33 | 34 | @classmethod 35 | def fromHex(self, _hex: str): 36 | return PublicKey(bytes.fromhex(_hex)) 37 | 38 | def format(self): 39 | return 'PublicKey{' + self.compressed_key.hex() + '}' 40 | 41 | -------------------------------------------------------------------------------- /mimblewimble/crypto/public_keys.py: -------------------------------------------------------------------------------- 1 | from secp256k1_zkp_mw import secp256k1_context_create 2 | from secp256k1_zkp_mw import secp256k1_context_destroy 3 | 4 | from secp256k1_zkp_mw import secp256k1_ec_seckey_verify 5 | from secp256k1_zkp_mw import secp256k1_ec_pubkey_create 6 | from secp256k1_zkp_mw import secp256k1_ec_pubkey_parse 7 | from secp256k1_zkp_mw import secp256k1_ec_pubkey_serialize 8 | from secp256k1_zkp_mw import secp256k1_ec_pubkey_combine 9 | 10 | from secp256k1_zkp_mw import SECP256K1_CONTEXT_SIGN 11 | from secp256k1_zkp_mw import SECP256K1_CONTEXT_VERIFY 12 | from secp256k1_zkp_mw import SECP256K1_EC_COMPRESSED 13 | 14 | from mimblewimble.crypto.secret_key import SecretKey 15 | from mimblewimble.crypto.public_key import PublicKey 16 | 17 | class PublicKeys: 18 | def __init__(self): 19 | self.ctx = secp256k1_context_create( 20 | SECP256K1_CONTEXT_SIGN | SECP256K1_CONTEXT_VERIFY) 21 | 22 | def __del__(self): 23 | secp256k1_context_destroy(self.ctx) 24 | 25 | def isSecretKeyValid(self, sk: SecretKey): 26 | is_valid = secp256k1_ec_seckey_verify(self.ctx, sk.getBytes()) 27 | return is_valid 28 | 29 | def calculatePublicKey(self, sk: SecretKey, compressed=True): 30 | assert self.isSecretKeyValid(sk) 31 | pk = secp256k1_ec_pubkey_create(self.ctx, sk.getBytes()) 32 | pk_serialized = secp256k1_ec_pubkey_serialize( 33 | self.ctx, pk, SECP256K1_EC_COMPRESSED) 34 | return PublicKey(pk_serialized) 35 | 36 | def publicKeySum(self, pks, compressed=False): 37 | parsed_pks = [] 38 | for pk in pks: 39 | parsed = secp256k1_ec_pubkey_parse(self.ctx, pk.getBytes()) 40 | parsed_pks.append(parsed) 41 | combined = secp256k1_ec_pubkey_combine(self.ctx, parsed_pks) 42 | pk_serialized = secp256k1_ec_pubkey_serialize( 43 | self.ctx, combined, SECP256K1_EC_COMPRESSED) 44 | return PublicKey(pk_serialized) 45 | -------------------------------------------------------------------------------- /mimblewimble/crypto/rangeproof.py: -------------------------------------------------------------------------------- 1 | from io import BytesIO 2 | 3 | from mimblewimble.serializer import Serializer 4 | 5 | MAX_PROOF_SIZE = 675; 6 | 7 | class RangeProof: 8 | def __init__(self, proofBytes: bytes): 9 | self.proofBytes = proofBytes 10 | 11 | # operators 12 | 13 | def __eq__(self, other): 14 | return self.getProofBytes() == other.getProofBytes() 15 | 16 | # getters 17 | 18 | def getProofBytes(self): 19 | return self.proofBytes 20 | 21 | # serialization / deserialization 22 | 23 | def serialize(self, serializer: Serializer): 24 | serializer.write((len(self.getProofBytes())).to_bytes(8, 'big')) 25 | for proof_bytes in self.getProofBytes(): 26 | serializer.write(proof_bytes.to_bytes(1, 'big')) 27 | 28 | @classmethod 29 | def deserialize(self, serializer: Serializer): 30 | proofSize = int.from_bytes(serializer.read(8), 'big') 31 | if proofSize > MAX_PROOF_SIZE: 32 | raise ValueError('Proof of size {0} exceeds the maximum'.format(str(len(proofSize)))) 33 | proof_bytes = serializer.read(proofSize) 34 | return RangeProof(proof_bytes) 35 | 36 | def hex(self): 37 | serializer = BytesIO() 38 | self.serialize(serializer) 39 | return serializer.getvalue().hex() 40 | 41 | def toJSON(self): 42 | return self.hex() 43 | 44 | @classmethod 45 | def fromhex(self, _hex: str): 46 | return RangeProof(bytes.fromhex(_hex)) 47 | 48 | def format(self): 49 | return self.hex() 50 | -------------------------------------------------------------------------------- /mimblewimble/crypto/secret_key.py: -------------------------------------------------------------------------------- 1 | from io import BytesIO 2 | 3 | 4 | class SecretKey: 5 | def __init__(self, seed: bytes): 6 | self.seed = seed 7 | self.NUM_BYTES = len(seed) 8 | 9 | # getters and setters 10 | 11 | def getBytes(self): 12 | return self.seed 13 | 14 | # operators 15 | 16 | def __eq__(self, other): 17 | return self.seed == other.seed 18 | 19 | def __ne__(self, other): 20 | return self.seed != other.seed 21 | 22 | # serialization / deserialization 23 | 24 | def serialize(self, serializer): 25 | serializer.write(self.seed) 26 | 27 | @classmethod 28 | def deserialize(self, byteBuffer: BytesIO, NUM_BYTES=32): 29 | return SecretKey(byteBuffer.read(NUM_BYTES)) 30 | 31 | -------------------------------------------------------------------------------- /mimblewimble/crypto/signature.py: -------------------------------------------------------------------------------- 1 | from io import BytesIO 2 | 3 | from mimblewimble.serializer import Serializer 4 | 5 | 6 | class Signature: 7 | def __init__(self, signatureBytes, compact=False): 8 | self.signatureBytes = signatureBytes 9 | self.compact = compact 10 | 11 | # operators 12 | 13 | def __eq__(self, other): 14 | return self.getSignatureBytes() == other.getSignatureBytes() 15 | 16 | # getters 17 | 18 | def isCompact(self): 19 | return self.compact 20 | 21 | def getSignatureBytes(self): 22 | return self.signatureBytes 23 | 24 | # serialization / deserialization 25 | 26 | def serialize(self, serializer: Serializer): 27 | assert len(self.getSignatureBytes()) == 64 28 | for signature_byte in self.getSignatureBytes(): 29 | serializer.write(signature_byte.to_bytes(1, 'big')) 30 | 31 | @classmethod 32 | def deserialize(self, serializer: Serializer): 33 | signature_bytes = serializer.read(64) 34 | return Signature(signature_bytes) 35 | 36 | def serialize_compressed(self, serializer: Serializer): 37 | signature_bytes = self.getSignatureBytes() 38 | assert len(signature_bytes) == 64 39 | r = signature_bytes[0:32] 40 | r = bytearray(r) 41 | r.reverse() 42 | r = bytes(r) 43 | 44 | s = signature_bytes[32:64] 45 | s = bytearray(s) 46 | s.reverse() 47 | s = bytes(s) 48 | 49 | serializer.write(r) 50 | serializer.write(s) 51 | 52 | @classmethod 53 | def deserialize_compressed(self, serializer: Serializer): 54 | r = serializer.read(32) 55 | r = bytearray(r) 56 | r.reverse() 57 | r = bytes(r) 58 | 59 | s = serializer.read(32) 60 | s = bytearray(s) 61 | s.reverse() 62 | s = bytes(s) 63 | 64 | signature_bytes = r + s 65 | return Signature(signature_bytes, compact=True) 66 | 67 | def hex(self): 68 | serializer = BytesIO() 69 | self.serialize(serializer) 70 | return serializer.read().hex() 71 | 72 | def format(self): 73 | return 'RawSig{' + self.hex() + '}' 74 | -------------------------------------------------------------------------------- /mimblewimble/entity/__init__.py: -------------------------------------------------------------------------------- 1 | from mimblewimble.models.transaction import EOutputStatus 2 | from mimblewimble.models.transaction import BlindingFactor 3 | from mimblewimble.models.transaction import TransactionOutput 4 | 5 | 6 | class OutputDataEntity: 7 | def __init__( 8 | self, 9 | path: str, 10 | blinding_factor: BlindingFactor, 11 | output: TransactionOutput, 12 | amount: int, 13 | status: EOutputStatus, 14 | mmr_index=None, block_height=None, wallet_tx_id=None): 15 | self.path = path 16 | self.blinding_factor = blinding_factor 17 | self.output = output 18 | self.amount = amount 19 | self.status = status 20 | self.mmr_index = mmr_index 21 | self.block_height = block_height 22 | self.wallet_tx_id = wallet_tx_id 23 | 24 | def getAmount(self): 25 | return self.amount 26 | 27 | def getBlindingFactor(self): 28 | return self.blinding_factor 29 | 30 | def getCommitment(self): 31 | return self.output.getCommitment() 32 | 33 | def getFeatures(self): 34 | return self.output.getFeatures() 35 | 36 | def getRangeProof(self): 37 | return self.output.getRangeProof() 38 | -------------------------------------------------------------------------------- /mimblewimble/genesis.py: -------------------------------------------------------------------------------- 1 | from mimblewimble.crypto.commitment import Commitment 2 | from mimblewimble.crypto.rangeproof import RangeProof 3 | from mimblewimble.crypto.signature import Signature 4 | 5 | from mimblewimble.models.fee import Fee 6 | from mimblewimble.models.transaction import EOutputFeatures, EKernelFeatures, BlindingFactor 7 | from mimblewimble.models.transaction import TransactionBody, TransactionOutput, TransactionKernel 8 | from mimblewimble.blockchain import FullBlock, BlockHeader, ProofOfWork 9 | 10 | 11 | floonet = FullBlock( 12 | BlockHeader( 13 | 1, # version 14 | 0, # height 15 | 1546030084, # timestamp 16 | bytearray(32), # previous hash 17 | bytes.fromhex('00000000000000000017ff4903ef366c8f62e3151ba74e41b8332a126542f538'), # previous root 18 | bytes.fromhex('73b5e0a05ea9e1e4e33b8f1c723bc5c10d17f07042c2af7644f4dbb61f4bc556'), # output root 19 | bytes.fromhex('667a3ba22f237a875f67c9933037c8564097fa57a3e75be507916de28fc0da26'), # range proof root 20 | bytes.fromhex('cfdddfe2d938d0026f8b1304442655bbdddde175ff45ddf44cb03bcb0071a72d'), # kernel root 21 | BlindingFactor.deserialize(bytearray(32)), # total kernel offset 22 | 1, # output MMR size 23 | 1, # kernel MMR size 24 | 100000, # total difficulty 25 | 1856, # scaling difficulty 26 | 23, # nonce 27 | ProofOfWork( 28 | 29, # edge bits 29 | [ 30 | 16994232, 22975978, 32664019, 44016212, 50238216, 57272481, 85779161, 31 | 124272202, 125203242, 133907662, 140522149, 145870823, 147481297, 164952795, 32 | 177186722, 183382201, 197418356, 211393794, 239282197, 239323031, 250757611, 33 | 281414565, 305112109, 308151499, 357235186, 374041407, 389924708, 390768911, 34 | 401322239, 401886855, 406986280, 416797005, 418935317, 429007407, 439527429, 35 | 484809502, 486257104, 495589543, 495892390, 525019296, 529899691, 531685572])), 36 | TransactionBody( 37 | [], # no transaction inputs 38 | [TransactionOutput( 39 | EOutputFeatures.COINBASE_OUTPUT, # features 40 | Commitment(bytes.fromhex('08c12007af16d1ee55fffe92cef808c77e318dae70c3bc70cb6361f49d517f1b68')), 41 | RangeProof([159, 156, 202, 179, 128, 169, 14, 227, 176, 79, 118, 180, 62, 164, 2, 234, 123, 30, 42 | 77, 126, 232, 124, 42, 186, 239, 208, 21, 217, 228, 246, 148, 74, 100, 25, 247, 43 | 251, 82, 100, 37, 16, 146, 122, 164, 5, 2, 165, 212, 192, 221, 167, 199, 8, 231, 44 | 149, 158, 216, 194, 200, 62, 15, 53, 200, 188, 207, 0, 79, 211, 88, 194, 211, 54, 45 | 1, 206, 53, 72, 118, 155, 184, 233, 166, 245, 224, 16, 254, 209, 235, 153, 85, 53, 46 | 145, 33, 186, 218, 118, 144, 35, 189, 241, 63, 229, 52, 237, 231, 39, 176, 202, 93, 47 | 247, 85, 131, 16, 193, 247, 180, 33, 138, 255, 102, 190, 213, 129, 174, 182, 167, 48 | 3, 126, 184, 221, 99, 114, 238, 219, 157, 125, 230, 179, 160, 89, 202, 230, 16, 91, 49 | 199, 57, 158, 225, 142, 125, 12, 211, 164, 78, 9, 4, 155, 106, 157, 41, 233, 188, 50 | 237, 205, 184, 53, 0, 190, 24, 215, 42, 44, 184, 120, 58, 196, 198, 190, 114, 50, 51 | 98, 240, 15, 213, 77, 163, 24, 3, 212, 125, 93, 175, 169, 249, 24, 27, 191, 113, 52 | 89, 59, 169, 40, 87, 250, 144, 159, 118, 171, 232, 92, 217, 5, 179, 152, 249, 247, 53 | 71, 239, 26, 180, 82, 177, 226, 132, 185, 3, 33, 162, 120, 98, 87, 109, 57, 100, 54 | 202, 162, 57, 230, 44, 31, 63, 213, 30, 222, 241, 78, 162, 118, 120, 70, 196, 128, 55 | 72, 223, 110, 5, 17, 151, 97, 214, 43, 57, 157, 1, 59, 87, 96, 17, 159, 174, 144, 56 | 217, 159, 87, 36, 113, 41, 155, 186, 252, 162, 46, 22, 80, 133, 3, 113, 248, 11, 57 | 118, 144, 155, 188, 77, 166, 40, 119, 107, 15, 233, 47, 47, 101, 77, 167, 141, 235, 58 | 148, 34, 218, 164, 168, 71, 20, 239, 71, 24, 12, 109, 146, 232, 243, 65, 31, 72, 59 | 186, 131, 190, 43, 227, 157, 41, 49, 126, 136, 51, 41, 50, 213, 37, 186, 223, 87, 60 | 248, 34, 43, 132, 34, 0, 143, 75, 79, 43, 74, 183, 26, 2, 168, 53, 203, 208, 159, 61 | 69, 107, 124, 33, 68, 113, 206, 127, 216, 158, 15, 52, 206, 1, 101, 109, 199, 13, 62 | 131, 122, 29, 131, 133, 125, 219, 70, 69, 144, 133, 68, 233, 67, 203, 132, 160, 63 | 143, 101, 84, 110, 15, 175, 111, 124, 24, 185, 222, 154, 238, 77, 241, 105, 8, 224, 64 | 230, 43, 178, 49, 95, 137, 33, 227, 118, 207, 239, 56, 21, 51, 220, 22, 48, 162, 65 | 22, 118, 229, 215, 248, 112, 198, 126, 180, 27, 161, 237, 56, 2, 220, 129, 126, 11, 66 | 104, 8, 133, 190, 162, 204, 3, 63, 249, 173, 210, 152, 252, 143, 157, 79, 228, 232, 67 | 230, 72, 164, 131, 183, 151, 230, 219, 186, 21, 34, 154, 219, 215, 231, 179, 47, 68 | 217, 44, 115, 203, 157, 35, 195, 113, 235, 194, 102, 96, 205, 24, 221, 213, 147, 69 | 120, 178, 221, 153, 146, 44, 172, 131, 77, 21, 61, 15, 5, 6, 205, 164, 203, 76, 70 | 228, 29, 126, 136, 88, 230, 210, 62, 164, 103, 125, 55, 231, 129, 89, 61, 222, 50, 71 | 71, 71, 75, 230, 70, 80, 85, 193, 136, 183, 222, 146, 46, 235, 0, 222, 118, 32, 70, 72 | 85, 39, 92, 233, 211, 169, 159, 207, 145, 13, 206, 125, 3, 45, 51, 64, 167, 179, 73 | 133, 83, 57, 190, 51, 239, 211, 74, 116, 75, 71, 248, 249, 184, 13, 31, 129, 107, 74 | 104, 179, 76, 194, 186, 4, 13, 122, 167, 254, 126, 153, 50, 8, 1, 200, 203, 213, 75 | 230, 217, 97, 105, 50, 208, 126, 180, 113, 81, 152, 238, 123, 157, 232, 19, 164, 76 | 159, 164, 89, 75, 33, 70, 140, 204, 158, 236, 10, 226, 102, 14, 88, 134, 82, 131, 77 | 36, 195, 127, 158, 81, 252, 223, 165, 11, 52, 105, 245, 245, 228, 235, 168, 175, 78 | 52, 175, 76, 157, 120, 208, 99, 135, 210, 81, 114, 230, 181]))], 79 | [TransactionKernel( 80 | EKernelFeatures.COINBASE_KERNEL, 81 | Fee(0, 0), 82 | 0, # lock height 83 | Commitment(bytes.fromhex('08df2f1d996cee37715d9ac0a0f3b13aae508d1101945acb8044954aee30960be9')), 84 | Signature([25, 176, 52, 246, 172, 1, 12, 220, 247, 111, 73, 101, 13, 16, 157, 130, 110, 196, 123, 85 | 217, 246, 137, 45, 110, 106, 186, 0, 151, 255, 193, 233, 178, 103, 26, 210, 215, 200, 86 | 89, 146, 188, 9, 161, 28, 212, 227, 143, 82, 54, 5, 223, 16, 65, 237, 132, 196, 241, 87 | 39, 76, 133, 45, 252, 131, 88, 0]) 88 | )])) 89 | 90 | mainnet = FullBlock( 91 | BlockHeader( 92 | 1, # version 93 | 0, # height 94 | 1547568086, # timestamp 95 | bytearray(32), # previous hash 96 | bytes.fromhex('0000000000000000002a8bc32f43277fe9c063b9c99ea252b483941dcd06e217'), # previous root 97 | bytes.fromhex('fa7566d275006c6c467876758f2bc87e4cebd2020ae9cf9f294c6217828d6872'), # output root 98 | bytes.fromhex('1b7fff259aee3edfb5867c4775e4e1717826b843cda6685e5140442ece7bfc2e'), # range proof root 99 | bytes.fromhex('e8bb096a73cbe6e099968965f5342fc1702ee2802802902286dcf0f279e326bf'), # kernel root 100 | BlindingFactor.deserialize(bytearray(32)), # total kernel offset 101 | 1, # output MMR size 102 | 1, # kernel MMR size 103 | 2**34, # total difficulty 104 | 1856, # scaling difficulty 105 | 41, # nonce 106 | ProofOfWork( 107 | 29, # edge bits 108 | [ 109 | 4391451, 36730677, 38198400, 38797304, 60700446, 72910191, 73050441, 110099816, 140885802, 110 | 145512513, 149311222, 149994636, 157557529, 160778700, 162870981, 179649435, 194194460, 111 | 227378628, 230933064, 252046196, 272053956, 277878683, 288331253, 290266880, 293973036, 112 | 305315023, 321927758, 353841539, 356489212, 373843111, 381697287, 389274717, 403108317, 113 | 409994705, 411629694, 431823422, 441976653, 521469643, 521868369, 523044572, 524964447, 530250249])), 114 | TransactionBody( 115 | [], # no transaction inputs 116 | [TransactionOutput( 117 | EOutputFeatures.COINBASE_OUTPUT, # features 118 | Commitment(bytes.fromhex('08b7e57c448db5ef25aa119dde2312c64d7ff1b890c416c6dda5ec73cbfed2edea')), 119 | RangeProof([147, 48, 173, 140, 222, 32, 95, 49, 124, 101, 55, 236, 169, 107, 134, 98, 147, 160, 72, 150, 21, 169, 120 | 162, 119, 180, 211, 165, 151, 200, 115, 84, 76, 130, 71, 73, 50, 182, 65, 224, 106, 200, 113, 150, 4, 121 | 238, 82, 232, 149, 232, 205, 70, 33, 182, 191, 184, 87, 128, 205, 155, 236, 206, 20, 208, 112, 11, 131, 122 | 166, 100, 219, 47, 82, 162, 108, 66, 95, 215, 119, 173, 136, 148, 76, 223, 255, 56, 4, 58, 39, 147, 237, 123 | 77, 154, 166, 126, 54, 203, 253, 85, 133, 87, 159, 198, 157, 218, 147, 4, 24, 175, 94, 175, 96, 54, 84, 124 | 246, 247, 81, 37, 141, 45, 252, 140, 33, 19, 193, 113, 225, 48, 243, 30, 193, 230, 204, 226, 167, 24, 228, 125 | 53, 41, 143, 206, 93, 100, 255, 225, 189, 52, 100, 253, 124, 135, 207, 169, 32, 147, 133, 91, 224, 52, 191, 126 | 228, 67, 158, 146, 139, 217, 42, 215, 127, 208, 160, 224, 3, 85, 238, 29, 26, 156, 235, 30, 208, 196, 8, 127 | 220, 253, 186, 140, 88, 62, 117, 152, 220, 112, 10, 170, 159, 145, 67, 32, 151, 37, 154, 64, 95, 91, 115, 128 | 21, 162, 247, 101, 136, 97, 227, 52, 155, 176, 220, 139, 248, 131, 114, 106, 33, 95, 1, 73, 222, 214, 97, 129 | 62, 90, 192, 103, 12, 12, 82, 2, 36, 125, 124, 39, 200, 167, 208, 59, 219, 3, 201, 207, 84, 85, 70, 63, 155, 130 | 66, 207, 135, 64, 62, 49, 248, 56, 60, 196, 244, 154, 52, 198, 42, 228, 89, 245, 128, 26, 158, 237, 79, 14, 131 | 227, 223, 213, 245, 91, 112, 17, 192, 202, 227, 147, 196, 116, 171, 214, 248, 199, 150, 91, 155, 95, 255, 132 | 49, 4, 221, 78, 57, 84, 32, 119, 192, 200, 221, 47, 143, 252, 235, 107, 181, 152, 81, 45, 144, 80, 109, 10, 133 | 113, 132, 242, 15, 20, 152, 207, 69, 135, 135, 242, 50, 132, 181, 72, 136, 201, 190, 65, 109, 16, 63, 118, 134 | 4, 6, 53, 122, 22, 182, 216, 65, 163, 3, 213, 201, 91, 107, 71, 77, 45, 127, 15, 234, 10, 42, 118, 200, 151, 135 | 221, 33, 16, 233, 48, 63, 84, 104, 65, 105, 66, 17, 71, 104, 76, 111, 24, 25, 195, 60, 239, 63, 56, 236, 153, 136 | 90, 80, 132, 80, 192, 44, 209, 135, 47, 128, 101, 253, 238, 114, 49, 9, 193, 139, 29, 210, 221, 222, 117, 130, 137 | 85, 70, 236, 240, 223, 7, 147, 195, 83, 178, 12, 148, 108, 214, 65, 34, 206, 168, 193, 22, 244, 50, 51, 104, 138 | 153, 161, 106, 210, 74, 42, 175, 203, 143, 144, 14, 9, 161, 20, 113, 53, 252, 242, 165, 76, 191, 129, 219, 48, 139 | 138, 71, 160, 138, 73, 199, 124, 19, 14, 93, 197, 230, 97, 205, 85, 165, 204, 105, 230, 7, 5, 90, 91, 8, 17, 140 | 27, 246, 26, 98, 234, 87, 120, 248, 81, 25, 4, 54, 51, 241, 202, 184, 199, 86, 215, 86, 197, 163, 72, 81, 2, 141 | 74, 195, 17, 165, 150, 177, 205, 145, 155, 188, 164, 50, 38, 240, 186, 5, 127, 107, 87, 222, 47, 105, 85, 176, 142 | 130, 60, 56, 38, 222, 127, 96, 150, 193, 193, 182, 185, 184, 228, 6, 62, 22, 69, 192, 191, 243, 47, 128, 86, 26, 143 | 170, 149, 157, 151, 18, 15, 188, 46, 205, 157, 43, 226, 139, 208, 193, 120, 17, 220, 89, 168, 128, 73, 246, 216, 144 | 149, 46, 233, 160, 160, 32, 118, 147, 200, 156, 163, 173, 17, 151, 233, 191, 223, 192, 59, 233, 216, 69, 174, 145 | 168, 214, 99, 150, 146, 23, 227, 180, 148, 206, 233, 230, 82, 188, 159, 135, 19, 226, 253, 92, 177, 132, 56, 72, 146 | 244, 108, 58, 106, 176, 36, 208, 227, 213, 124, 164, 84, 84, 205, 189, 164, 20, 173, 170, 131, 95, 161, 71, 222, 147 | 180, 255, 183, 18, 156, 243, 168, 216, 103, 38, 160, 20, 71, 148]))], 148 | [TransactionKernel( 149 | EKernelFeatures.COINBASE_KERNEL, 150 | Fee(0, 0), 151 | 0, # lock height 152 | Commitment(bytes.fromhex('096385d86c5cfda718aa0b7295be0adf7e5ac051edfe130593a2a257f09f78a3b1')), 153 | Signature([80, 208, 41, 171, 28, 224, 250, 121, 60, 192, 213, 232, 111, 199, 111, 105, 18, 22, 54, 165, 107, 154 | 33, 186, 113, 186, 100, 12, 42, 72, 106, 42, 20, 67, 253, 188, 178, 228, 246, 21, 168, 253, 18, 22, 155 | 179, 41, 63, 250, 218, 80, 132, 75, 67, 244, 11, 108, 27, 188, 251, 212, 166, 233, 103, 117, 237]) 156 | )])) 157 | -------------------------------------------------------------------------------- /mimblewimble/helpers/__init__.py: -------------------------------------------------------------------------------- 1 | def fillOnesToRight(input: int): 2 | x = input 3 | x = x | (x >> 1) 4 | x = x | (x >> 2) 5 | x = x | (x >> 4) 6 | x = x | (x >> 8) 7 | x = x | (x >> 16) 8 | x = x | (x >> 32) 9 | return x 10 | -------------------------------------------------------------------------------- /mimblewimble/helpers/encryption.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from typing import List 4 | from hashlib import sha256 5 | from io import BytesIO 6 | 7 | from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey 8 | from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey 9 | 10 | from pyrage import decrypt as age_decrypt 11 | from pyrage import encrypt as age_encrypt 12 | from pyrage import x25519 as age_x25519 13 | 14 | from hashlib import pbkdf2_hmac 15 | from Crypto.Cipher import ChaCha20_Poly1305 16 | 17 | 18 | def encrypt(plaintext: bytes, passphrase: bytes, nonce=None, salt=None): 19 | if salt is None: 20 | salt = os.urandom(8) 21 | if nonce is None: 22 | nonce = os.urandom(12) 23 | key = pbkdf2_hmac('sha512', passphrase, salt, 100) 24 | cipher = ChaCha20_Poly1305.new(key=key[0:32], nonce=nonce) 25 | ciphertext = cipher.encrypt(plaintext) 26 | tag = cipher.digest() 27 | return ciphertext, tag, nonce, salt 28 | 29 | 30 | def decrypt( 31 | ciphertext: bytes, 32 | passphrase: bytes, 33 | tag: bytes, 34 | salt: bytes, 35 | nonce: bytes): 36 | key = pbkdf2_hmac('sha512', passphrase, salt, 100) 37 | cipher = ChaCha20_Poly1305.new(key=key[0:32], nonce=nonce) 38 | plaintext = cipher.decrypt_and_verify(ciphertext, tag) 39 | return plaintext 40 | 41 | 42 | def ageX25519Encrypt( 43 | plaintext: bytes, 44 | _recipients: List[str]): 45 | recipients = [ 46 | x25519.Recipient.from_str(r) for r in _recipients] 47 | return age_encrypt(plaintext, recipients) 48 | 49 | 50 | def ageX25519Decrypt( 51 | ciphertext: bytes, age_secret_key: str): 52 | receiver = age_x25519.Identity.from_str( 53 | age_secret_key) 54 | return age_decrypt(ciphertext, [receiver]) 55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /mimblewimble/helpers/fee.py: -------------------------------------------------------------------------------- 1 | from mimblewimble.consensus import Consensus 2 | 3 | 4 | def calculateFee(fee_base: int, num_inputs: int, num_outputs: int, num_kernels: int): 5 | return fee_base*Consensus.calculateWeightV5(num_inputs, num_outputs, num_kernels) 6 | -------------------------------------------------------------------------------- /mimblewimble/helpers/slate.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from mimblewimble.entity import OutputDataEntity 4 | 5 | from mimblewimble.models.transaction import BlindingFactor 6 | 7 | from mimblewimble.crypto.aggsig import AggSig 8 | from mimblewimble.crypto.pedersen import Pedersen 9 | from mimblewimble.crypto.public_keys import PublicKeys 10 | 11 | 12 | def calculateSigningKeys( 13 | inputs: List[OutputDataEntity], 14 | outputs: List[OutputDataEntity], 15 | tx_offset: BlindingFactor): 16 | # cryptography utilities 17 | p = Pedersen() 18 | pks = PublicKeys() 19 | agg = AggSig() 20 | 21 | # calculate sum inputs blinding factors xI. 22 | input_bf_sum = p.blindSum([inp.getBlindingFactor() for inp in inputs], []) 23 | 24 | # calculate sum change outputs blinding factors xC. 25 | output_bf_sum = p.blindSum([out.getBlindingFactor() for out in outputs], []) 26 | 27 | # calculate total blinding excess sum for all inputs and outputs xS1 = xC - xI 28 | total_blind_excess = p.blindSum([output_bf_sum], [input_bf_sum]) 29 | 30 | # subtract random kernel offset oS from xS1. Calculate xS = xS1 - oS 31 | secret_key = p.blindSum([total_blind_excess], [tx_offset]).toSecretKey() 32 | public_key = pks.calculatePublicKey(secret_key) 33 | 34 | # select a random nonce kS 35 | secret_nonce = agg.generateSecureNonce() 36 | public_nonce = pks.calculatePublicKey(secret_nonce) 37 | 38 | # clean-up 39 | del p 40 | del pks 41 | del agg 42 | 43 | # done! 44 | return secret_key, public_key, secret_nonce, public_nonce 45 | -------------------------------------------------------------------------------- /mimblewimble/helpers/tor.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import hashlib 3 | 4 | from nacl import bindings 5 | 6 | 7 | class TorAddress: 8 | def __init__(self, public_key): 9 | self.public_key = public_key 10 | 11 | def toOnion(self, version=3): 12 | version_bytes = version.to_bytes(1, 'big') 13 | checksum = TorAddress.checksum(self.public_key, version_bytes) 14 | address = base64.b32encode(self.public_key + checksum[0:2] + version_bytes).decode() 15 | return address.lower() + '.onion' 16 | 17 | @classmethod 18 | def checksum(self, public_key: bytes, version: bytes): 19 | preimage = '.onion checksum'.encode('ascii') 20 | preimage += public_key 21 | preimage += version 22 | return hashlib.sha3_256(preimage).digest() 23 | 24 | @classmethod 25 | def parse(self, tor_address): 26 | without_onion = tor_address.replace('.onion', '').upper() 27 | decoded = base64.b32decode(without_onion) 28 | public_key = decoded[0:32] 29 | check = decoded[32:34] 30 | version_bytes = decoded[34:35] 31 | version = int.from_bytes(version_bytes, 'big') 32 | 33 | # validate version 34 | if version != 3: 35 | raise ValueError('Only V3 .onion addresses supported') 36 | 37 | # validate the public key 38 | try: 39 | bindings.crypto_sign_ed25519_pk_to_curve25519(public_key) 40 | except: 41 | raise ValueError('.onion address includes invalid ed25519 public key') 42 | 43 | # compute expected checksum 44 | expected_checksum = TorAddress.checksum(public_key, version_bytes) 45 | if expected_checksum[0:2] != check: 46 | raise ValueError('.onion address checksum mismatch') 47 | 48 | return TorAddress(public_key) 49 | 50 | -------------------------------------------------------------------------------- /mimblewimble/keychain.py: -------------------------------------------------------------------------------- 1 | import hmac 2 | import os 3 | 4 | from typing import Union 5 | 6 | from bip32 import BIP32 7 | from bip_utils import Bech32Encoder 8 | 9 | from hashlib import blake2b, pbkdf2_hmac, sha512 10 | 11 | from nacl import bindings 12 | from nacl.signing import SigningKey, VerifyKey 13 | 14 | from mimblewimble.crypto.bulletproof import EBulletproofType 15 | 16 | from mimblewimble.crypto.commitment import Commitment 17 | from mimblewimble.crypto.secret_key import SecretKey 18 | from mimblewimble.crypto.public_keys import PublicKeys 19 | from mimblewimble.crypto.rangeproof import RangeProof 20 | from mimblewimble.crypto.pedersen import Pedersen 21 | 22 | from mimblewimble.crypto.bulletproof import EBulletproofType 23 | from mimblewimble.crypto.bulletproof import ProofMessage 24 | from mimblewimble.crypto.bulletproof import RewoundProof 25 | from mimblewimble.crypto.bulletproof import Bulletproof 26 | 27 | from mimblewimble.models.slatepack.address import SlatepackAddress 28 | 29 | from mimblewimble.helpers.encryption import ageX25519Decrypt 30 | 31 | 32 | class KeyChain: 33 | def __init__(self, master_key: bytes, bulletproof_nonce: SecretKey): 34 | self.master_key = master_key 35 | self.bulletproof_nonce = bulletproof_nonce 36 | 37 | def derivePrivateKey(self, path: str): 38 | # derive the seed at the path 39 | master_key = self.master_key.getBytes() 40 | bip32 = BIP32( 41 | chaincode=master_key[32:], privkey=master_key[:32]) 42 | sk = bip32.get_privkey_from_path(path) 43 | return SecretKey(sk) 44 | 45 | def derivePrivateKeyAmount(self, path: str, amount: int): 46 | p = Pedersen() 47 | sk = self.derivePrivateKey(path) 48 | ska = p.blindSwitch(sk, amount) 49 | return ska 50 | 51 | def deriveED25519Seed(self, path: str): 52 | sk_der = self.derivePrivateKey(path) 53 | 54 | # compute the blake2 hash of that key and that is ed25519 seed 55 | seed_blake = blake2b(sk_der.getBytes(), digest_size=32).digest() 56 | return seed_blake 57 | 58 | def deriveED25519SecretKey(self, path: str): 59 | # get the seed 60 | seed_blake = self.deriveED25519Seed(path) 61 | 62 | # get the ed25519 secret key and public key from it 63 | pk, sk = bindings.crypto_sign_seed_keypair(seed_blake) 64 | return sk 65 | 66 | def deriveED25519PublicKey(self, path: str): 67 | # get the seed 68 | seed_blake = self.deriveED25519Seed(path) 69 | 70 | # get the ed25519 public key and public key from it 71 | pk, sk = bindings.crypto_sign_seed_keypair(seed_blake) 72 | return pk 73 | 74 | def deriveX25519PublicKey(self, path: str): 75 | ed25519_pk = self.deriveED25519PublicKey(path=path) 76 | 77 | x25519_pk = bindings.crypto_sign_ed25519_pk_to_curve25519(ed25519_pk) 78 | 79 | return x25519_pk 80 | 81 | def deriveX25519SecretKey(self, path: str): 82 | # get the seed 83 | # seed_blake = self.deriveED25519Seed(path) 84 | 85 | # get the ed25519 public key and public key from it 86 | # ed25519_pk, ed25519_sk = bindings.crypto_sign_seed_keypair(seed_blake) 87 | 88 | ed25519_sk = self.deriveED25519SecretKey(path=path) 89 | 90 | # construct the x25519 secret key 91 | x25519_sk = bindings.crypto_sign_ed25519_sk_to_curve25519(ed25519_sk) 92 | 93 | return x25519_sk 94 | 95 | def deriveAgeSecretKey(self, path: str): 96 | x25519_sk = self.deriveX25519SecretKey(path) 97 | 98 | age_secret_key = Bech32Encoder.Encode('age-secret-key-', x25519_sk) 99 | return age_secret_key.upper() 100 | 101 | def signED25519(self, message: bytes, path: str) -> bytes: 102 | sk = self.deriveED25519SecretKey(path) 103 | signature = bindings.crypto_sign(message, sk) 104 | del sk 105 | return signature 106 | 107 | @classmethod 108 | def verifyED25519( 109 | self, 110 | public_key: Union[bytes, str], 111 | signature: bytes, 112 | message: bytes) -> bool: 113 | public_key_bytes = public_key 114 | if isinstance(public_key, str): 115 | public_key_bytes = KeyChain.slatepackAddressToED25519PublicKey( 116 | public_key) 117 | recovered = bindings.crypto_sign_open(signature, public_key_bytes) 118 | return recovered == message 119 | 120 | def ageDecrypt( 121 | self, 122 | ciphertext: bytes, 123 | path: str): 124 | age_secret_key = self.deriveAgeSecretKey(path) 125 | 126 | return ageX25519Decrypt( 127 | ciphertext, age_secret_key) 128 | 129 | def deriveSlatepackAddress(self, path: str, testnet=False): 130 | pk = self.deriveED25519PublicKey(path) 131 | slatepack_address = SlatepackAddress(pk) 132 | return slatepack_address.toBech32(testnet=testnet) 133 | 134 | def deriveOnionAddress(self, path: str): 135 | pk = self.deriveED25519PublicKey(path) 136 | return KeyChain.slatepackAddressToOnion(pk) 137 | 138 | @classmethod 139 | def slatepackAddressToED25519PublicKey( 140 | self, address: str, testnet=False) -> bytes: 141 | slatepack_address = SlatepackAddress.fromBech32( 142 | address, testnet=testnet) 143 | return slatepack_address.toED25519() 144 | 145 | @classmethod 146 | def slatepackAddressToOnion(self, public_key: Union[bytes, str]): 147 | slatepack_address = SlatepackAddress(public_key) 148 | if isinstance(public_key, str): 149 | slatepack_address = SlatepackAddress.fromBech32( 150 | public_key) 151 | return slatepack_address.toOnion(version=3) 152 | 153 | def rewindRangeProof( 154 | self, 155 | commitment: Commitment, 156 | rangeproof: RangeProof, 157 | bulletproof_type: EBulletproofType): 158 | b = Bulletproof() 159 | if bulletproof_type == EBulletproofType.ORIGINAL: 160 | nonce = KeyChain.createNonce( 161 | commitment, self.bulletproof_nonce) 162 | return b.rewindProof( 163 | commitment, rangeproof, nonce) 164 | elif bulletproof_type == EBulletproofType.ENHANCED: 165 | pks = PublicKeys() 166 | master_public_key = pks.calculatePublicKey(self.master_key) 167 | 168 | rewind_nonce_hash = SecretKey(blake2b( 169 | master_public_key.getBytes(), 170 | digest_size=32).digest()) 171 | 172 | return b.rewindProof( 173 | commitment, 174 | rangeproof, 175 | KeyChain.createNonce(commitment, rewind_nonce_hash)) 176 | raise ValueError('Unimplemented bulletproof type') 177 | 178 | 179 | def generateRangeProof( 180 | self, 181 | path: str, 182 | amount: int, 183 | commitment: Commitment, 184 | blinding_factor: SecretKey, 185 | bulletproof_type: EBulletproofType): 186 | 187 | key_indices = KeyChain.getKeyIndices(path) 188 | proof_message = ProofMessage.fromKeyIndices( 189 | key_indices, bulletproof_type) 190 | 191 | b = Bulletproof() 192 | if bulletproof_type == EBulletproofType.ORIGINAL: 193 | nonce = KeyChain.createNonce( 194 | commitment, self.bulletproof_nonce) 195 | return b.generateRangeProof( 196 | amount, blinding_factor, nonce, nonce, proof_message) 197 | elif bulletproof_type == EBulletproofType.ENHANCED: 198 | private_nonce_hash = SecretKey(blake2b( 199 | self.master_key.getBytes(), 200 | digest_size=32).digest()) 201 | 202 | pks = PublicKeys() 203 | master_public_key = pks.calculatePublicKey(self.master_key) 204 | 205 | rewind_nonce_hash = SecretKey(blake2b( 206 | master_public_key.getBytes(), 207 | digest_size=32).digest()) 208 | 209 | return b.generateRangeProof( 210 | amount, 211 | blinding_factor, 212 | KeyChain.createNonce(commitment, private_nonce_hash), 213 | KeyChain.createNonce(commitment, rewind_nonce_hash), 214 | proof_message) 215 | raise ValueError('Unimplemented bulletproof type') 216 | 217 | @classmethod 218 | def getKeyIndices(self, path: str): 219 | key_indices = [] 220 | for v in path.split('/'): 221 | try: 222 | i = int(v) 223 | except: 224 | continue 225 | key_indices.append(i) 226 | return key_indices 227 | 228 | @classmethod 229 | def createNonce(self, commitment: Commitment, nonce_hash: SecretKey): 230 | nonce = blake2b( 231 | commitment.getBytes() + nonce_hash.getBytes(), 232 | digest_size=32).digest() 233 | return SecretKey(nonce) 234 | 235 | @classmethod 236 | def fromSeed(self, master_seed): 237 | # I AM VOLDEMORT 238 | m = hmac.new('IamVoldemort'.encode('utf8'), digestmod=sha512) 239 | m.update(master_seed) 240 | master_key = SecretKey(m.digest()) 241 | p = Pedersen() 242 | bulletproof_nonce = p.blindSwitch(master_key, 0) 243 | return KeyChain(master_key, bulletproof_nonce) 244 | 245 | @classmethod 246 | def fromRandom(self): 247 | master_seed = os.urandom(32) 248 | return KeyChain.fromSeed(master_seed) 249 | -------------------------------------------------------------------------------- /mimblewimble/mmr/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grinventions/mimblewimble-py/5d904d2f0a0f3629210db5e0f634599b97e5283a/mimblewimble/mmr/__init__.py -------------------------------------------------------------------------------- /mimblewimble/mmr/index.py: -------------------------------------------------------------------------------- 1 | from mimblewimble.helpers import fillOnesToRight 2 | 3 | 4 | class MMRIndex: 5 | def __init__(self, position: int, height: int): 6 | self.position = position 7 | self.height = height 8 | 9 | def isLeaf(self): 10 | return self.height == 0 11 | 12 | @classmethod 13 | def at(self, position: int): 14 | return MMRIndex(position, self.calculateHeight(position)) 15 | 16 | # operators 17 | def __eq__(self, other): 18 | return self.position == other.position and self.height == other.height 19 | 20 | def __ne__(self, other): 21 | return self.position != other.position or self.height != other.height 22 | 23 | def __lt__(self, other): 24 | return self.position < other.position 25 | 26 | def __leq__(self, other): 27 | return self.position <= other.position 28 | 29 | def __gt__(self, other): 30 | return self.position > other.position 31 | 32 | def __geq__(self, other): 33 | return self.position >= other.position 34 | 35 | def getLeafIndex(self): 36 | assert self.isLeaf() 37 | return self.calculateLeafIndex(self.position) 38 | 39 | def getParent(self): 40 | if self.calculateHeight(self.position + 1) == self.height + 1: 41 | return MMRIndex(self.position + 1, self.height + 1) 42 | else: 43 | return MMRIndex(self.position + (1 << (self.height + 1)), self.height + 1) 44 | 45 | def getSibling(self): 46 | if self.calculateHeight(self.position + 1) == self.height + 1: 47 | return MMRIndex(self.position + 1 - (1 << (self.height + 1)), self.height) 48 | else: 49 | return MMRIndex(self.position + (1 << (self.height + 1)) - 1, self.height + 1) 50 | 51 | def getLeftChild(self): 52 | assert self.height > 0 53 | return MMRIndex(self.position - (1 << height), self.height - 1) 54 | 55 | def getRightChild(self): 56 | assert self.height > 0 57 | return MMRIndex(self.position - 1, self.height - 1) 58 | 59 | @classmethod 60 | def calculateHeight(self, position: int): 61 | height = position 62 | peakSize = fillOnesToRight(position + 1) 63 | while peakSize != 0: 64 | if height >= peakSize: 65 | height -= peakSize 66 | peakSize >>= 1 67 | return height 68 | 69 | def calculateLeafIndex(self, position: int): 70 | leafIndex = 0 71 | peakSize = fillOnesToRight(position) 72 | numLeft = position 73 | while peakSize != 0: 74 | if numLeft >= peakSize: 75 | leafIndex += ((peakSize + 1)/2) 76 | numLeft -= peakSize 77 | peakSize >>= 1 78 | return leafIndex 79 | -------------------------------------------------------------------------------- /mimblewimble/mnemonic.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | import json 3 | 4 | from hashlib import pbkdf2_hmac 5 | 6 | from math import ceil 7 | 8 | try: 9 | import importlib.resources as pkg_resources 10 | except ImportError: 11 | # Try backported to PY<37 `importlib_resources`. 12 | import importlib_resources as pkg_resources 13 | 14 | from mimblewimble import static 15 | 16 | 17 | class Mnemonic: 18 | def __init__(self): 19 | # load the wordlist 20 | f = pkg_resources.open_binary(static, 'wordlist.json') 21 | self.words = json.load(f) 22 | 23 | def mnemonicFromEntropy(self, entropy: bytes): 24 | entropy_len = len(entropy) 25 | 26 | if entropy_len % 4 != 0: 27 | raise ValueError('Entropy was of incorrect length.') 28 | 29 | entropyBits = int(entropy_len * 8) 30 | checksumBits = int(entropyBits / 32) 31 | numWords = ceil((entropyBits + checksumBits) / 11) 32 | 33 | wordIndices = [0x0 for j in range(numWords)] 34 | 35 | loc = 0 36 | for i in range(entropy_len): 37 | byte = entropy[i] 38 | for j in reversed(range(1, 9)): 39 | bit = 1 40 | if (byte & (1 << (j - 1))) == 0: 41 | bit = 0 42 | wordIndices[int(loc / 11)] |= bit << (10 - (loc % 11)) 43 | loc += 1 44 | 45 | mask = ((1 << checksumBits) - 1) 46 | lhs = hashlib.sha256(entropy).digest()[0] 47 | checksum = (lhs >> (8 - checksumBits)) & mask 48 | 49 | for i in reversed(range(1, checksumBits+1)): 50 | bit = 1 51 | if (checksum & (1 << (i - 1))) == 0: 52 | bit = 0 53 | wordIndices[int(loc / 11)] |= bit << (10 - (loc % 11)) 54 | loc += 1 55 | 56 | phrase = '' 57 | for i in range(numWords): 58 | if i > 0: 59 | phrase += ' ' 60 | phrase += self.words[wordIndices[i]] 61 | 62 | return phrase 63 | 64 | 65 | def entropyFromMnemonic(self, wallet_words: str): 66 | words = wallet_words.split(' ') 67 | 68 | numWords = len(words) 69 | 70 | if numWords < 12 or numWords > 24 or numWords % 3 != 0: 71 | raise ValueError('Invalid number of words provided.') 72 | 73 | wordIndices = [None for i in range(numWords)] 74 | 75 | for i, word in enumerate(words): 76 | try: 77 | j = self.words.index(word) 78 | except ValueError: 79 | raise ValueError('Word not found: ' + word) 80 | 81 | wordIndices[i] = j; 82 | 83 | checksumBits = int(numWords / 3) 84 | mask = (1 << checksumBits) - 1 85 | expectedChecksum = wordIndices[-1] & mask; 86 | 87 | dataLength = int((((11 * numWords) - checksumBits) / 8) - 1) 88 | 89 | entropy = [0x0 for j in range(dataLength + 1)] 90 | entropy[-1] = wordIndices[-1] >> checksumBits 91 | 92 | loc = 11 - checksumBits 93 | 94 | for i in reversed(range(1, numWords)): 95 | for k in range(11): 96 | bit = 1 97 | if wordIndices[i - 1] & (1 << k) == 0: 98 | bit = 0 99 | entropy[dataLength - int(loc / 8)] |= bit << (loc % 8) 100 | loc += 1 101 | 102 | entropy = bytes(entropy) 103 | 104 | lhs = hashlib.sha256(entropy).digest()[0] 105 | actualChecksum = (lhs >> (8 - checksumBits)) & mask 106 | 107 | if actualChecksum != expectedChecksum: 108 | raise ValueError('Invalid checksum.') 109 | 110 | return entropy 111 | 112 | 113 | def toSeed(self, entropy, passphrase=''): 114 | salt = bytes('mnemonic' + passphrase, 'utf-8') 115 | dk = pbkdf2_hmac('sha512', entropy, salt, 2048) 116 | return dk.hex() 117 | 118 | 119 | 120 | 121 | 122 | -------------------------------------------------------------------------------- /mimblewimble/models/fee.py: -------------------------------------------------------------------------------- 1 | from mimblewimble.serializer import Serializer 2 | 3 | 4 | class Fee: 5 | def __init__(self, shift, fee): 6 | self.shift = shift 7 | self.fee = fee 8 | 9 | # operators 10 | 11 | def __eq__(self, other): 12 | return self.fee == other.getFee() and self.shift == other.getShift() 13 | 14 | def __ne__(self, other): 15 | return self.fee != other.getFee() or self.shift != other.getShift() 16 | 17 | # getters 18 | 19 | def getShift(self): 20 | return self.shift 21 | 22 | def getFee(self): 23 | return self.fee 24 | 25 | # serialization / deserialization 26 | 27 | def serialize(self, serializer: Serializer): 28 | serializer.write((0).to_bytes(2, 'big')) 29 | serializer.write(self.getShift().to_bytes(1, 'big')) 30 | serializer.write((self.getFee() >> 32).to_bytes(1, 'big')) 31 | serializer.write((self.getFee() & 0xffffffff).to_bytes(4, 'big')) 32 | 33 | @classmethod 34 | def deserialize(self, serializer: Serializer): 35 | serializer.read(2) 36 | shift = int.from_bytes(serializer.read(1), 'big') & 0x0f 37 | fee = int.from_bytes(serializer.read(1), 'big') << 32 38 | fee += int.from_bytes(serializer.read(4), 'big') 39 | return Fee(shift, fee) 40 | 41 | def toJSON(self): 42 | return int(self.fee) 43 | 44 | @classmethod 45 | def fromJSON(self, feeJSON): 46 | byteBuffer = BytesIO(feeJSON.to_bytes(64)) 47 | return Fee().deserialize(byteBuffer) 48 | 49 | @classmethod 50 | def fromInt(self, fee: int): 51 | return Fee(0, fee) 52 | 53 | 54 | -------------------------------------------------------------------------------- /mimblewimble/models/short_id.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | 3 | from io import BytesIO 4 | from siphash import siphash_64 5 | 6 | 7 | class ShortId: 8 | def __init__(self, _id): 9 | self._id = _id 10 | 11 | @classmethod 12 | def create(self, _hash: bytes, _blockHash: bytes, nonce: int): 13 | serializer = BytesIO() 14 | serializer.write(_blockHash) 15 | serializer.write(nonce.to_bytes(8, byteorder='big')) 16 | serialized = serializer.getvalue() 17 | _hashWithNonce = hashlib.blake2b(serialized, digest_size=32).digest() 18 | 19 | # extract k0/k1 from the block hash 20 | byteBuffer = BytesIO(_hashWithNonce) 21 | k0 = bytearray(byteBuffer.read(8)) 22 | k0 = bytes(k0) 23 | k0 = int.from_bytes(k0, byteorder='little') 24 | k1 = bytearray(byteBuffer.read(8)) 25 | k1 = int.from_bytes(k1, byteorder='little') 26 | key = k0.to_bytes(8, byteorder='little') + k1.to_bytes(8, byteorder='little') 27 | # SipHash24 our hash using the k0 and k1 keys 28 | sipHash = siphash_64(key, _hash) 29 | 30 | # construct short_id from the resulting bytes (dropping 2 most significant bytes) 31 | _id = int.from_bytes(sipHash[:-2], byteorder='little') 32 | return ShortId(_id) 33 | 34 | 35 | # operators 36 | 37 | def __lt__(self, other): 38 | return self.getId() < other.getId() 39 | 40 | def __eq__(self, other): 41 | return self.getId() == other.getId() 42 | 43 | # getters 44 | 45 | def getId(self): 46 | return self._id 47 | 48 | # serialization / deserialization 49 | 50 | def serialize(self, serializer: BytesIO): 51 | serializer.write(self._id.to_bytes(6)) 52 | 53 | @classmethod 54 | def deserializer(self, byteBuffer: BytesIO): 55 | return ShortId(int(byteBuffer.read(6))) 56 | 57 | # traits 58 | 59 | def __hash__(self): 60 | serializer = BytesIO() 61 | self.serialize(serializer) 62 | return hashlib.blake2b(serializer.read()).digest() 63 | -------------------------------------------------------------------------------- /mimblewimble/models/slatepack/address.py: -------------------------------------------------------------------------------- 1 | import os 2 | import base64 3 | import hashlib 4 | 5 | from bip_utils import Bech32Encoder, Bech32Decoder 6 | 7 | from nacl.bindings import crypto_sign_ed25519_pk_to_curve25519 8 | 9 | from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PublicKey 10 | 11 | from age.keys.agekey import AgePublicKey 12 | 13 | from mimblewimble.serializer import Serializer 14 | from mimblewimble.helpers.tor import TorAddress 15 | 16 | 17 | class SlatepackAddress: 18 | def __init__(self, ed25519_pk: bytes): 19 | self.ed25519_pk = ed25519_pk 20 | 21 | def toED25519(self): 22 | return self.ed25519_pk 23 | 24 | def toX25519(self): 25 | x25519_pk = crypto_sign_ed25519_pk_to_curve25519(self.ed25519_pk) 26 | return X25519PublicKey.from_public_bytes(x25519_pk) 27 | 28 | def toAge(self): 29 | x25519_pk = self.toX25519() 30 | return AgePublicKey(x25519_pk) 31 | 32 | def toBech32(self, testnet=False): 33 | public_key = self.toED25519() 34 | # compute the slatepack address 35 | network = 'grin' 36 | if testnet: 37 | network = 'tgrin' 38 | slatepack_address = Bech32Encoder.Encode(network, public_key) 39 | return slatepack_address 40 | 41 | # https://gitweb.torproject.org/torspec.git/tree/rend-spec-v3.txt#n2013 42 | # base32(PUBKEY | CHECKSUM | VERSION) + ".onion" 43 | def toOnion(self, version=3): 44 | public_key_bytes = self.toED25519() 45 | tor = TorAddress(public_key_bytes) 46 | return tor.toOnion(version=version) 47 | 48 | def toNostr(self): 49 | pass # TODO 50 | 51 | def serialize(self, serializer: Serializer): 52 | address = self.toBech32() 53 | address_length = len(address) 54 | serializer.write(address_length.to_bytes(1, 'big')) 55 | serializer.writeString(address) 56 | 57 | @classmethod 58 | def deserialize(self, serializer: Serializer): 59 | address_length = int.from_bytes(serializer.read(1), 'big') 60 | address = serializer.readString(address_length) 61 | return SlatepackAddress.fromBech32(address) 62 | 63 | @classmethod 64 | def fromBech32(self, address: str, testnet=False): 65 | # compute the slatepack address 66 | network = 'grin' 67 | if testnet: 68 | network = 'tgrin' 69 | public_key = Bech32Decoder.Decode(network, address) 70 | return SlatepackAddress(public_key) 71 | 72 | @classmethod 73 | def random(self): 74 | public_key = os.urandom(32) 75 | return SlatepackAddress(public_key) 76 | -------------------------------------------------------------------------------- /mimblewimble/models/slatepack/message.py: -------------------------------------------------------------------------------- 1 | import base58 2 | import hashlib 3 | 4 | from enum import IntEnum 5 | from typing import List 6 | 7 | from mimblewimble.serializer import Serializer 8 | from mimblewimble.codecs import Base64Codec 9 | 10 | from mimblewimble.helpers.encryption import ageX25519Encrypt, ageX25519Decrypt 11 | 12 | from mimblewimble.slatebuilder.slate import Slate 13 | 14 | from mimblewimble.models.slatepack.address import SlatepackAddress 15 | from mimblewimble.models.slatepack.metadata import SlatepackVersion 16 | from mimblewimble.models.slatepack.metadata import SlatepackMetadata 17 | 18 | 19 | SLATEPACK_HEADER = 'BEGINSLATEPACK' 20 | SLATEPACK_FOOTER = 'ENDSLATEPACK' 21 | SLATEPACK_WORD_LENGTH = 15 22 | SLATEPACK_WORDS_PER_LINE = 200 23 | 24 | 25 | def slatepack_spacing(data: str, word_length=None): 26 | if word_length is None: 27 | word_length = SLATEPACK_WORD_LENGTH 28 | chunks = [ 29 | data[i:i+word_length] for i in range( 30 | 0, len(data), word_length) 31 | ] 32 | chunks[-1] = chunks[-1] + '.' 33 | return chunks 34 | 35 | 36 | def slatepack_pack( 37 | data: bytes, 38 | word_length=None, 39 | words_per_line=None, 40 | string_encoding='ascii') -> str: 41 | encoded = base58.b58encode(data).decode(string_encoding) 42 | words = [SLATEPACK_HEADER + '.'] 43 | words += slatepack_spacing(encoded, word_length=word_length) 44 | words += [SLATEPACK_FOOTER + '.'] 45 | packed = '' 46 | line = [] 47 | if words_per_line is None: 48 | words_per_line = SLATEPACK_WORDS_PER_LINE 49 | for word in words: 50 | if len(line) >= words_per_line: 51 | packed += ' '.join(line) 52 | packed += '\n' 53 | line = [] 54 | line.append(word) 55 | if len(line) >= 0: 56 | packed += ' '.join(line) 57 | packed += '\n' 58 | return packed 59 | 60 | 61 | def slatepack_unpack(data: str) -> bytes: 62 | processed = data.replace(SLATEPACK_HEADER, '') 63 | processed = processed.replace(SLATEPACK_FOOTER, '') 64 | processed = processed.replace('.', '') 65 | processed = processed.replace(' ', '') 66 | processed = processed.replace('\n', '') 67 | return base58.b58decode(processed) 68 | 69 | 70 | class EMode(IntEnum): 71 | PLAINTEXT = 0 72 | ENCRYPTED = 1 73 | 74 | 75 | class SlatepackMessage: 76 | def __init__( 77 | self, 78 | version: SlatepackVersion, 79 | metadata: SlatepackMetadata, 80 | emode: EMode, 81 | payload: bytes): 82 | self.version = version 83 | self.metadata = metadata 84 | self.emode = emode 85 | self.payload = payload 86 | 87 | def pack(self, word_length=None, words_per_line=None, string_encoding='ascii'): 88 | preimage = self.serialize() 89 | 90 | hashed = hashlib.sha256(preimage).digest() 91 | hashed = hashlib.sha256(hashed).digest() 92 | checksum = hashed[0:4] 93 | 94 | s = checksum + preimage 95 | 96 | packed = slatepack_pack( 97 | s, 98 | word_length=word_length, 99 | words_per_line=words_per_line, 100 | string_encoding=string_encoding) 101 | return packed 102 | 103 | @classmethod 104 | def unpack(self, packed: str): 105 | unpacked = slatepack_unpack(packed) 106 | 107 | s = Serializer() 108 | s.write(unpacked) 109 | 110 | error_check_code = s.read(4) 111 | preimage = s.readremaining() 112 | 113 | hashed = hashlib.sha256(preimage).digest() 114 | hashed = hashlib.sha256(hashed).digest() 115 | checksum = hashed[0:4] 116 | 117 | if checksum != error_check_code: 118 | raise ValueError('Invalid slatepack checkum') 119 | 120 | return preimage 121 | 122 | def serialize(self): 123 | serializer = Serializer() 124 | 125 | self.version.serialize(serializer) 126 | serializer.write(int(self.emode).to_bytes(1, 'big')) 127 | 128 | opt_flags = 0x00 129 | if self.emode == EMode.PLAINTEXT and self.metadata.sender is not None: 130 | opt_flags |= 0x01 131 | 132 | if self.emode == EMode.PLAINTEXT and len(self.metadata.recipients) > 0: 133 | opt_flags |= 0x02 134 | 135 | serializer.write(opt_flags.to_bytes(2, 'big')) 136 | 137 | opt_fields_len = 0 # what should it be? 138 | serializer.write(opt_fields_len.to_bytes(4, 'big')) 139 | 140 | if opt_flags & 0x01 == 0x01: 141 | self.metadata.sender.serialize(serializer) 142 | 143 | if opt_flags & 0x02 == 0x02: 144 | num_recipients = len(self.metadata.recipients) 145 | serializer.write(num_recipients.to_bytes(2, 'big')) 146 | for recipient in self.metadata.recipients: 147 | recipient.serialize(serializer) 148 | 149 | serializer.write(self.payload) 150 | 151 | return serializer.readall() 152 | 153 | @classmethod 154 | def deserialize(self, unpacked: bytes): 155 | serializer = Serializer() 156 | serializer.write(unpacked) 157 | 158 | version = SlatepackVersion.deserialize(serializer) 159 | emode = EMode(int.from_bytes(serializer.read(1), 'big')) 160 | 161 | opt_flags = int.from_bytes(serializer.read(2), 'big') 162 | opt_fields_len = int.from_bytes(serializer.read(4), 'big') 163 | 164 | sender = None 165 | recipients = [] 166 | 167 | if opt_flags & 0x01 == 0x01: 168 | sender = SlatepackAddress.deserialize(serializer) 169 | 170 | if opt_flags & 0x02 == 0x02: 171 | recipients = [] 172 | num_recipients = int.from_bytes(serializer.read(2), 'big') 173 | for i in range(num_recipients): 174 | recipient = SlatepackAddress.deserialize(serializer) 175 | recipients.append(recipient) 176 | 177 | metadata = SlatepackMetadata(sender=sender, recipients=recipients) 178 | 179 | payload_size = int.from_bytes(serializer.read(8), 'big') 180 | payload = serializer.read(payload_size) 181 | 182 | return SlatepackMessage(version, metadata, emode, payload) 183 | 184 | def armor(self): 185 | pass 186 | 187 | @classmethod 188 | def unarmor(self, armored_slatepack: str): 189 | unpacked = SlatepackMessage.unpack(armored_slatepack) 190 | return SlatepackMessage.deserialize(unpacked) 191 | 192 | def is_encrypted(self): 193 | return self.emode != EMode.PLAINTEXT 194 | 195 | def getPayload(self) -> bytes: 196 | return self.payload 197 | 198 | def setPayload(self, payload: bytes, emode: EMode): 199 | self.emode = emode 200 | if self.emode == EMode.PLAINTEXT: 201 | s = Serializer() 202 | s.write(payload) 203 | 204 | self.metadata = SlatepackMetadata.deserialize(s) 205 | self.payload = s.readremaining() 206 | else: 207 | self.metadata = None 208 | self.payload = payload 209 | 210 | def getSlate(self): 211 | if self.emode == EMode.ENCRYPTED: 212 | raise Exception( 213 | 'requesting a slate but payload is still encrypted') 214 | return Slate.deserialize(self.payload) 215 | 216 | def encryptPayload(self, recipients: List[SlatepackAddress]): 217 | if self.emode != EMode.PLAINTEXT: 218 | raise ValueError( 219 | 'requesting encrypting an already encrypted payload in SlatepackMessage') 220 | keys = [ 221 | recipient.toAge() for recipient in recipients 222 | ] 223 | self.payload = ageX25519Encrypt(self.payload, keys) 224 | self.emode = EMode.ENCRYPTED 225 | 226 | def decryptPayload(self, age_secret_key: str): 227 | if self.emode != EMode.ENCRYPTED: 228 | raise ValueError( 229 | 'requesting decrypting an already decrypted payload in SlatepackMessage') 230 | decrypted_payload = ageX25519Decrypt( 231 | self.payload, age_secret_key) 232 | 233 | s = Serializer() 234 | s.write(decrypted_payload) 235 | 236 | self.metadata = SlatepackMetadata.deserialize(s) 237 | self.emode = EMode.PLAINTEXT 238 | self.payload = s.readremaining() 239 | 240 | ''' 241 | def encrypt(self): 242 | serializer = Serializer() 243 | 244 | serializer.write(int(self.emode).to_bytes(1, 'big')) 245 | 246 | opt_flags = 0x00 247 | if self.emode == EMode.PLAINTEXT and self.sender is not None: 248 | opt_flags |= 0x01 249 | serializer.write(opt_flags.to_bytes(2, 'big')) 250 | 251 | if opt_flags & 0x01 == 0x01: 252 | self.sender.serialize(serializer) 253 | 254 | if self.emode == EMode.PLAINTEXT: 255 | serializer.write(self.payload) 256 | else: 257 | serializer.write(self.encryptPayload(self.metadata.recipients)) 258 | 259 | return serializer.readall() 260 | 261 | def decrypt(self, serializer: Serializer): 262 | version = SlatepackVersion.deserialize(serializer) 263 | emode = EMode(int.from_bytes(serializer.read(1), 'big')) 264 | 265 | opt_flags = int.from_bytes(serializer.read(2), 'big') 266 | opt_fields_len = int.from_bytes(serializer.read(4), 'big') 267 | 268 | metadata = SlatepackMetadata() 269 | if opt_flags & 0x01 == 0x01: 270 | sender = SlatepackAddress.deserialize(serializer) 271 | metadata = SlatepackMetadata(sender=sender) 272 | 273 | payload_size = int.from_bytes(serializer.read(8), 'big') 274 | payload = serializer.read(payload_size) 275 | 276 | if emode == EMode.ENCRYPTED: 277 | decrypted_payload = self.decryptPayload(payload, key) 278 | return SlatepackMessage(version, metadata, emode, decrypted_payload) 279 | 280 | return SlatepackMessage(version, metadata, emode, payload) 281 | ''' 282 | 283 | def __str__(self): 284 | return '' 285 | 286 | def toJSON(self, testnet=False, codec=Base64Codec, encoding='utf-8'): 287 | payload = codec.encode(self.payload, encoding=encoding) 288 | slatepack = { 289 | 'slatepack': [self.version.major, self.version.minor], 290 | 'mode': int(self.emode), 291 | 'payload': payload 292 | } 293 | version = str(self.version) 294 | if self.emode == EMode.PLAINTEXT: 295 | slatepack['sender'] = '0' 296 | if self.metadata.sender is not None: 297 | slatepack['sender'] = self.metadata.sender.toBech32(testnet=testnet) 298 | return slatepack 299 | return slatepack 300 | -------------------------------------------------------------------------------- /mimblewimble/models/slatepack/metadata.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional 2 | 3 | from mimblewimble.serializer import Serializer 4 | 5 | from mimblewimble.models.slatepack.address import SlatepackAddress 6 | 7 | 8 | class SlatepackVersion: 9 | def __init__(self, major, minor): 10 | self.major = major 11 | self.minor = minor 12 | 13 | def serialize(self, serializer: Serializer): 14 | serializer.write(self.major.to_bytes(1, 'big')) 15 | serializer.write(self.minor.to_bytes(1, 'big')) 16 | 17 | @classmethod 18 | def deserialize(self, serializer: Serializer): 19 | major = int.from_bytes(serializer.read(1), 'big') 20 | minor = int.from_bytes(serializer.read(1), 'big') 21 | return SlatepackVersion(major, minor) 22 | 23 | def __str__(self): 24 | return '{0}:{1}'.format(str(self.major), str(self.minor)) 25 | 26 | 27 | # https://github.com/GrinPlusPlus/GrinPlusPlus/blob/master/include/Wallet/Models/Slatepack/SlatepackMessage.h#L52 28 | # https://github.com/mimblewimble/grin-wallet/blob/75363a9a258bc1fb0cf60bfb4c88a8a653b122f2/libwallet/src/slatepack/types.rs#L503 29 | 30 | class SlatepackMetadata: 31 | def __init__( 32 | self, 33 | sender: Optional[SlatepackAddress] = None, 34 | recipients: Optional[List[SlatepackAddress]] = []): 35 | self.sender = sender 36 | self.recipients = recipients 37 | 38 | def serialize(self, serializer: Serializer): 39 | opt_flags = 0x00 40 | if self.sender is not None: 41 | opt_flags |= 0x01 42 | if len(self.recipients) == 0: 43 | opt_flags |= 0x02 44 | serializer.write(opt_flags.to_bytes(2, 'big')) 45 | 46 | num_recipients = len(self.recipients) 47 | if num_recipients > 0: 48 | serializer.write(num_recipients.to_bytes(2, 'big')) 49 | for recipient in self.recipients: 50 | recipient.serialize(serializer) 51 | 52 | @classmethod 53 | def deserialize(self, serializer: Serializer): 54 | size = int.from_bytes(serializer.read(4), 'big') 55 | 56 | inner_buffer = Serializer() 57 | inner_buffer.write(serializer.read(size)) 58 | 59 | opt_flags = int.from_bytes(inner_buffer.read(2), 'big') 60 | 61 | sender = None 62 | if opt_flags & 0x01 == 0x01: 63 | sender = SlatepackAddress.deserialize(inner_buffer) 64 | 65 | recipients = [] 66 | if opt_flags & 0x02 == 0x02: 67 | num_recipients = int.from_bytes(inner_buffer.read(2), 'big') 68 | for i in range(num_recipients): 69 | recipient = SlatepackAddress.deserialize(inner_buffer) 70 | recipients.append(recipient) 71 | 72 | return SlatepackMetadata(sender, recipients) 73 | 74 | def encrypt(self): 75 | pass 76 | 77 | def decrypt(self): 78 | pass 79 | -------------------------------------------------------------------------------- /mimblewimble/serializer.py: -------------------------------------------------------------------------------- 1 | from io import BytesIO 2 | from enum import Enum 3 | 4 | class EProtocolVersion(Enum): 5 | V1 = 1 6 | V2 = 2 7 | V3 = 3 8 | V1000 = 1000 9 | 10 | class Serializer: 11 | def __init__(self, protocol=EProtocolVersion.V1): 12 | self.pnt = 0 13 | self.protocol = protocol 14 | self.raw = BytesIO() 15 | 16 | def resetPointer(self, n=None): 17 | if n is None: 18 | self.pnt = 0 19 | else: 20 | self.pnt = n 21 | 22 | def read(self, n): 23 | self.raw.seek(self.pnt) 24 | self.pnt += n 25 | return self.raw.read(n) 26 | 27 | def write(self, b: bytes): 28 | self.raw.write(b) 29 | 30 | def readline(self, clean_newline=False): 31 | self.raw.seek(self.pnt) 32 | line = self.raw.readline() 33 | self.pnt = self.raw.tell() 34 | if clean_newline: 35 | if line[-1] == ord('\n'): 36 | return line[0:-1] 37 | return line 38 | 39 | def readlines(self): 40 | lines = [] 41 | line = self.readline() 42 | while line != b'': 43 | lines.append(line) 44 | line = self.readline() 45 | if line != b'': 46 | lines.append(line) 47 | return lines 48 | 49 | def readString(self, n, encoding='ascii'): 50 | as_bytes = self.read(n) 51 | return as_bytes.decode(encoding) 52 | 53 | def writeString(self, value: str, encoding='ascii'): 54 | as_bytes = value.encode(encoding) 55 | self.write(as_bytes) 56 | 57 | def getvalue(self): 58 | return self.raw.getvalue() 59 | 60 | def getProtocol(self): 61 | return self.protocol 62 | 63 | def readremaining(self): 64 | self.raw.seek(self.pnt) 65 | value = self.raw.read() 66 | self.pnt = self.raw.tell() 67 | return value 68 | 69 | def readall(self): 70 | self.raw.seek(0) 71 | value = self.raw.read() 72 | self.raw.seek(self.pnt) 73 | return value 74 | 75 | def getStream(self): 76 | return self.raw 77 | 78 | def __len__(self): 79 | return self.raw.getbuffer().nbytes 80 | -------------------------------------------------------------------------------- /mimblewimble/slatebuilder/__init__.py: -------------------------------------------------------------------------------- 1 | from mimblewimble.slatebuilder.slate_payment_proof import SlatePaymentProof 2 | from mimblewimble.slatebuilder.slate import Slate, ESlateStage, SlateSignature 3 | from mimblewimble.slatebuilder.send import SendSlateBuilder 4 | from mimblewimble.slatebuilder.receive import ReceiveSlateBuilder 5 | from mimblewimble.slatebuilder.finalize import FinalizeSlateBuilder 6 | -------------------------------------------------------------------------------- /mimblewimble/slatebuilder/finalize.py: -------------------------------------------------------------------------------- 1 | from mimblewimble.serializer import Serializer 2 | from mimblewimble.keychain import KeyChain 3 | 4 | from mimblewimble.crypto.aggsig import AggSig 5 | from mimblewimble.crypto.secret_key import SecretKey 6 | 7 | from mimblewimble.models.transaction import TransactionKernel 8 | 9 | from mimblewimble.slatebuilder import ESlateStage 10 | from mimblewimble.slatebuilder import Slate 11 | 12 | 13 | class FinalizeSlateBuilder: 14 | def __init__( 15 | self, 16 | master_seed: bytes): 17 | self.master_seed = master_seed 18 | 19 | def finalize( 20 | self, 21 | receive_slate: Slate, 22 | secret_key: SecretKey, 23 | secret_nonce: SecretKey, 24 | sender_address: bytes, 25 | testnet=False): 26 | # renaming the slate object 27 | finalized_slate = receive_slate 28 | finalized_slate.setStage(ESlateStage.STANDARD_FINALIZED) 29 | 30 | # generate the sender's partial signature 31 | kernel_message = TransactionKernel.getSignatureMessage( 32 | finalized_slate.getKernelFeatures(), 33 | finalized_slate.getFee(), 34 | finalized_slate.getLockHeight()) 35 | signature = finalized_slate.getSignature(0) 36 | agg = AggSig() 37 | partial_signature = agg.calculatePartialSignature( 38 | signature.getExcess(), 39 | signature.getNonce(), 40 | finalized_slate.calculateTotalExcess(), 41 | finalized_slate.calculateTotalNonce(), 42 | kernel_message) 43 | del agg 44 | signature.setSignature(partial_signature) 45 | finalized_slate.setSignature(0, signature) 46 | 47 | # reproduce the payment proof message 48 | # (amount | kernel commitment | sender address) 49 | amount = finalized_slate.getAmount() 50 | kernel_commitment = finalized_slate.getKernelCommitment().getBytes() 51 | 52 | serializer = Serializer() 53 | serializer.write(amount.to_bytes(8, 'big')) 54 | serializer.write(kernel_commitment) 55 | serializer.write(sender_address) 56 | message = serializer.readall() 57 | 58 | # verify payment proof addresses & signatures 59 | payment_proof = finalized_slate.getPaymentProof() 60 | 61 | receiver_address = payment_proof.getReceiverAddress() 62 | receiver_signature = payment_proof.getReceiverSignature() 63 | 64 | try: 65 | KeyChain.verifyED25519( 66 | receiver_address, receiver_signature, message) 67 | except Exception as e: 68 | print(e) 69 | raise ValueError('Invalid payment proof') 70 | 71 | # done! 72 | return finalized_slate 73 | -------------------------------------------------------------------------------- /mimblewimble/slatebuilder/receive.py: -------------------------------------------------------------------------------- 1 | from mimblewimble.entity import OutputDataEntity 2 | 3 | from mimblewimble.crypto.aggsig import AggSig 4 | 5 | from mimblewimble.models.transaction import BlindingFactor 6 | from mimblewimble.models.transaction import TransactionKernel 7 | 8 | from mimblewimble.helpers.slate import calculateSigningKeys 9 | 10 | from mimblewimble.slatebuilder import ESlateStage 11 | from mimblewimble.slatebuilder import Slate 12 | from mimblewimble.slatebuilder import SlateSignature 13 | from mimblewimble.slatebuilder import SlatePaymentProof 14 | 15 | 16 | class ReceiveSlateBuilder: 17 | def __init__( 18 | self, master_seed): 19 | self.master_seed = master_seed 20 | 21 | def addReceiverData( 22 | self, 23 | send_slate: Slate, 24 | output: OutputDataEntity, 25 | testnet=False) -> Slate: 26 | # renaming the slate object 27 | receive_slate = send_slate 28 | receive_slate.setStage(ESlateStage.STANDARD_RECEIVED) 29 | 30 | # create the receiver offset 31 | receiver_offset = BlindingFactor.random() 32 | signing_keys = calculateSigningKeys( 33 | [], [output], receiver_offset) 34 | secret_key, public_key, secret_nonce, public_nonce = signing_keys 35 | 36 | # adjust the receiver offset 37 | receive_slate.addOffset(receiver_offset) 38 | 39 | # generate the receiver's partial signature 40 | kernel_message = TransactionKernel.getSignatureMessage( 41 | receive_slate.getKernelFeatures(), 42 | receive_slate.getFee(), 43 | receive_slate.getLockHeight()) 44 | signature = SlateSignature(public_key, public_nonce) 45 | agg = AggSig() 46 | partial_signature = agg.calculatePartialSignature( 47 | secret_key, 48 | secret_nonce, 49 | receive_slate.calculateTotalExcess(), 50 | receive_slate.calculateTotalNonce(), 51 | kernel_message) 52 | del agg 53 | signature.setSignature(partial_signature) 54 | 55 | # add the receiver participant data 56 | receive_slate.appendSignature(signature) 57 | 58 | # add the receiver's tx output 59 | receive_slate.appendOutput( 60 | output.getFeatures(), 61 | output.getCommitment(), 62 | output.getRangeProof()) 63 | 64 | # done! 65 | return receive_slate 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /mimblewimble/slatebuilder/send.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from typing import List, Tuple 4 | from uuid import UUID 5 | 6 | from mimblewimble.entity import OutputDataEntity 7 | 8 | from mimblewimble.consensus import Consensus 9 | 10 | from mimblewimble.crypto.secret_key import SecretKey 11 | 12 | from mimblewimble.models.transaction import BlindingFactor 13 | from mimblewimble.models.transaction import EKernelFeatures 14 | from mimblewimble.models.fee import Fee 15 | 16 | from mimblewimble.helpers.slate import calculateSigningKeys 17 | 18 | from mimblewimble.slatebuilder import ESlateStage 19 | from mimblewimble.slatebuilder import Slate 20 | from mimblewimble.slatebuilder import SlateSignature 21 | from mimblewimble.slatebuilder import SlatePaymentProof 22 | 23 | 24 | class SendSlateBuilder: 25 | def __init__( 26 | self, 27 | master_seed: bytes): 28 | self.master_seed = master_seed 29 | 30 | 31 | def build( 32 | self, 33 | amount: int, 34 | fee: int, 35 | block_height: int, 36 | inputs: List[OutputDataEntity], 37 | change_outputs: List[OutputDataEntity], 38 | slate_version=0, testnet=False, 39 | sender_address=None, 40 | receiver_address=None) -> Tuple[ 41 | Slate, SecretKey, SecretKey]: 42 | # select random transaction offset, 43 | # and calculate secret key used in kernel signature 44 | transaction_offset = BlindingFactor.random() 45 | signing_keys = calculateSigningKeys( 46 | inputs, change_outputs, transaction_offset) 47 | secret_key, public_key, secret_nonce, public_nonce = signing_keys 48 | signature = SlateSignature(public_key, public_nonce) 49 | 50 | # payment proof 51 | payment_proof = None 52 | if None not in [sender_address, receiver_address]: 53 | payment_proof = SlatePaymentProof(sender_address, receiver_address) 54 | 55 | # add values to Slate for passing to other participants: 56 | # UUID, inputs, change_outputs, fee, amount, lock_height, kSG, xSG, oS 57 | 58 | slate_id_bytes = os.urandom(16) 59 | slate_id = str(UUID(bytes=slate_id_bytes)) 60 | 61 | stage = ESlateStage.STANDARD_SENT 62 | 63 | block_version = Consensus.getHeaderVersion(block_height) 64 | slate = Slate( 65 | slate_id, 66 | slate_version, 67 | block_version, 68 | amount, 69 | Fee.fromInt(fee), 70 | payment_proof, 71 | EKernelFeatures.DEFAULT_KERNEL, 72 | transaction_offset, 73 | signatures=[signature], 74 | stage=stage) 75 | for inp in inputs: 76 | slate.appendInput( 77 | inp.getFeatures(), 78 | inp.getCommitment()) 79 | for out in change_outputs: 80 | slate.appendOutput( 81 | out.getFeatures(), 82 | out.getCommitment(), 83 | out.getRangeProof()) 84 | return slate, secret_key, secret_nonce 85 | 86 | 87 | def buildWalletTx( 88 | self, 89 | tx_offset: BlindingFactor, 90 | inputs: List[OutputDataEntity], 91 | change_outputs: List[OutputDataEntity], 92 | slate: Slate, 93 | receiver_address=None, 94 | payment_proof=None): 95 | if receiver_address is not None and isinstance(payment_proof, bytes): 96 | raise TypeError( 97 | 'Expected bytes type argument or None for the receipient address') 98 | if payment_proof is not None and isinstance(payment_proof, SlatePaymentProof): 99 | raise TypeError('Expected SlatePaymentProof type argument or None') 100 | 101 | # TODO 102 | pass 103 | -------------------------------------------------------------------------------- /mimblewimble/slatebuilder/slate_payment_proof.py: -------------------------------------------------------------------------------- 1 | from mimblewimble.serializer import Serializer 2 | 3 | # https://github.com/GrinPlusPlus/GrinPlusPlus/blob/fa3705f5558f0372410aa4222f5f1d97a0ab2247/include/Wallet/Models/Slate/SlatePaymentProof.h#L8 4 | # TODO define types: 5 | # ed25519_public_key_t for the addresses 6 | # ed25519_signature_t for the signature 7 | class SlatePaymentProof: 8 | def __init__(self, sender_address, receiver_address, receiver_signature=None): 9 | self.sender_address = sender_address 10 | self.receiver_address = receiver_address 11 | self.receiver_signature = receiver_signature 12 | 13 | def getSenderAddress(self): 14 | return self.sender_address 15 | 16 | def getReceiverAddress(self): 17 | return self.receiver_address 18 | 19 | def getReceiverSignature(self): 20 | return self.receiver_signature 21 | 22 | def setSenderAddress(self, sender_address): 23 | self.sender_address = sender_address 24 | 25 | def setReceiverAddress(self, receiver_address): 26 | self.receiver_address = receiver_address 27 | 28 | def setReceiverSignature(self, receiver_signature): 29 | self.receiver_signature = receiver_signature 30 | 31 | def serialize(self, serializer: Serializer): 32 | serializer.write(self.sender_address) 33 | serializer.write(self.receiver_address) 34 | has_sig = 0 35 | if self.receiver_signature is not None: 36 | has_sig = 1 37 | serializer.write(has_sig.to_bytes(1, 'big')) 38 | if has_sig > 0: 39 | serializer.write(self.receiver_signature) 40 | 41 | @classmethod 42 | def deserialize(self, serializer: Serializer): 43 | ed25519_sender = serializer.read(32) 44 | ed25519_receiver = serializer.read(32) 45 | ed25519_signature = None 46 | has_sig = int.from_bytes(serializer.read(1), 'big') 47 | if has_sig > 0: 48 | ed25519_signature = serializer.read(64) 49 | return SlatePaymentProof( 50 | ed25519_sender, 51 | ed25519_receiver, 52 | receiver_signature=ed25519_signature) 53 | 54 | def toJSON(self): 55 | obj = {} 56 | if self.sender_address is not None: 57 | obj['saddr'] = self.sender_address.hex() 58 | if self.receiver_address is not None: 59 | obj['raddr'] = self.receiver_address.hex() 60 | if self.receiver_signature is not None: 61 | obj['rsig'] = self.receiver_signature.hex() 62 | return obj 63 | 64 | @classmethod 65 | def fromJSON(self): 66 | raise Exception('unimplemented') 67 | -------------------------------------------------------------------------------- /mimblewimble/static/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grinventions/mimblewimble-py/5d904d2f0a0f3629210db5e0f634599b97e5283a/mimblewimble/static/__init__.py -------------------------------------------------------------------------------- /mimblewimble/wallet.py: -------------------------------------------------------------------------------- 1 | import hmac 2 | import os 3 | 4 | from typing import Tuple, List 5 | 6 | from nacl import bindings 7 | 8 | from Crypto.Cipher import ChaCha20_Poly1305 9 | from Crypto.Random import get_random_bytes 10 | 11 | from hashlib import blake2b, pbkdf2_hmac, sha512 12 | 13 | from bip32 import BIP32 14 | from bip_utils import Bech32Encoder 15 | 16 | from mimblewimble.serializer import Serializer 17 | from mimblewimble.mnemonic import Mnemonic 18 | from mimblewimble.keychain import KeyChain 19 | 20 | from mimblewimble.crypto.aggsig import AggSig 21 | from mimblewimble.crypto.commitment import Commitment 22 | from mimblewimble.crypto.bulletproof import EBulletproofType 23 | from mimblewimble.crypto.pedersen import Pedersen 24 | from mimblewimble.crypto.secret_key import SecretKey 25 | 26 | from mimblewimble.models.transaction import EOutputStatus 27 | from mimblewimble.models.transaction import EOutputFeatures 28 | from mimblewimble.models.transaction import EKernelFeatures 29 | from mimblewimble.models.transaction import BlindingFactor 30 | from mimblewimble.models.transaction import TransactionOutput 31 | from mimblewimble.models.transaction import TransactionKernel 32 | from mimblewimble.models.fee import Fee 33 | 34 | from mimblewimble.models.slatepack.address import SlatepackAddress 35 | from mimblewimble.models.slatepack.message import SlatepackMessage, EMode 36 | 37 | from mimblewimble.entity import OutputDataEntity 38 | from mimblewimble.helpers.fee import calculateFee 39 | 40 | from mimblewimble.slatebuilder import Slate 41 | from mimblewimble.slatebuilder import SendSlateBuilder 42 | from mimblewimble.slatebuilder import ReceiveSlateBuilder 43 | from mimblewimble.slatebuilder import FinalizeSlateBuilder 44 | 45 | # temp 46 | from age.keys.agekey import AgePrivateKey, AgePublicKey 47 | 48 | 49 | class Wallet: 50 | def __init__(self, encrypted_seed=None, salt=None, nonce=None, master_seed=None, tag=None): 51 | self.encrypted_seed = encrypted_seed 52 | self.salt = salt 53 | self.nonce = nonce 54 | self.master_seed = master_seed # 32 bytes 55 | 56 | 57 | def shieldWallet(self, passphrase: str, salt=None, nonce=None): 58 | if self.master_seed is None: 59 | raise Exception('The wallet is already shielded') 60 | if self.salt is None: 61 | if salt is None: 62 | self.salt = os.urandom(8) 63 | else: 64 | self.salt = salt 65 | if self.nonce is None: 66 | if nonce is None: 67 | self.nonce = os.urandom(12) 68 | else: 69 | self.nonce = nonce 70 | # compute encrypted_seed from master_seed 71 | key = pbkdf2_hmac('sha512', bytes(passphrase, 'utf-8'), self.salt, 100) 72 | cipher = ChaCha20_Poly1305.new(key=key[0:32], nonce=self.nonce) 73 | ciphertext = cipher.encrypt(self.master_seed) 74 | tag = cipher.digest() 75 | self.encrypted_seed = ciphertext + tag 76 | self.master_seed = None 77 | 78 | 79 | def unshieldWallet(self, passphrase: str, salt=None, nonce=None): 80 | if self.salt is None and salt is None: 81 | raise Exception('Missing salt') 82 | elif self.salt is None and salt is not None: 83 | self.salt = salt 84 | if self.nonce is None and nonce is None: 85 | raise Exception('Missing nonce') 86 | elif self.nonce is None and nonce is not None: 87 | self.nonce = nonce 88 | # compute master_seed from encrypted_seed 89 | key = pbkdf2_hmac('sha512', bytes(passphrase, 'utf-8'), salt, 100) 90 | cipher = ChaCha20_Poly1305.new(key=key[0:32], nonce=nonce) 91 | ciphertext = self.encrypted_seed[:32] 92 | tag = self.encrypted_seed[32:] 93 | plaintext = cipher.decrypt_and_verify(ciphertext, tag) 94 | self.master_seed = plaintext 95 | del plaintext 96 | 97 | 98 | def getSeedPhrase(self): 99 | if self.master_seed is None: 100 | raise Exception('The wallet is shielded') 101 | M = Mnemonic() 102 | return M.mnemonicFromEntropy(self.master_seed) 103 | 104 | 105 | def getEncryptedSeed(self): 106 | if None in [self.encrypted_seed, self.salt, self.nonce]: 107 | raise Exception('The wallet is not shielded') 108 | return { 109 | 'encrypted_seed': self.encrypted_seed.hex(), 110 | 'salt': self.salt.hex(), 111 | 'nonce': self.nonce.hex() 112 | } 113 | 114 | 115 | def getSlatepackAddress(self, path='m/0/1/0', testnet=False): 116 | if self.master_seed is None: 117 | raise Exception('The wallet is shielded') 118 | 119 | keychain = KeyChain.fromSeed(self.master_seed) 120 | slatepack_address = keychain.deriveSlatepackAddress(path) 121 | 122 | return slatepack_address 123 | 124 | 125 | def createCoinbase(self, amount, path='m/0/1/0'): 126 | if self.master_seed is None: 127 | raise Exception('The wallet is shielded') 128 | 129 | # instantiate the keychain for this wallet seed 130 | keychain = KeyChain.fromSeed(self.master_seed) 131 | 132 | # we will be needing Pedersen commitments 133 | # and signature aggregation 134 | p = Pedersen() 135 | agg = AggSig() 136 | 137 | # blinding factor and Pedersend commitment 138 | blinding_factor = keychain.derivePrivateKeyAmount( 139 | path, amount) 140 | commitment = p.commit(amount, blinding_factor) 141 | 142 | # bulletproof 143 | rangeproof = keychain.generateRangeProof( 144 | path, amount, commitment, 145 | blinding_factor, EBulletproofType.ENHANCED) 146 | 147 | # add the commitments 148 | transparent_factor = BlindingFactor(bytes([0x00 for j in range(32)])) 149 | transparent_commitment = p.commit(amount, transparent_factor) 150 | kernel_commitment = p.commitSum([commitment], [transparent_commitment]) 151 | 152 | # build the output 153 | transaction_output = TransactionOutput( 154 | EOutputFeatures.COINBASE_OUTPUT, 155 | commitment, 156 | rangeproof) 157 | 158 | # build the output data entity 159 | output_data_entity = OutputDataEntity( 160 | path, blinding_factor, transaction_output, amount, 161 | EOutputStatus.NO_CONFIRMATIONS) 162 | 163 | # build the coinbase signature 164 | message = blake2b( 165 | EKernelFeatures.COINBASE_KERNEL.to_bytes(1, 'big'), 166 | digest_size=32).digest() 167 | coinbase_signature = agg.buildSignature( 168 | blinding_factor, kernel_commitment, message) 169 | 170 | # build the kernel 171 | shift = 0 172 | fee = 0 173 | transaction_kernel = TransactionKernel( 174 | EKernelFeatures.COINBASE_KERNEL, 175 | Fee(shift, fee), 176 | 0, 177 | kernel_commitment, 178 | coinbase_signature) 179 | 180 | # clean-up 181 | del p 182 | del agg 183 | 184 | # return the result 185 | return transaction_kernel, output_data_entity 186 | 187 | 188 | def createBlindedOutput( 189 | self, 190 | amount: int, 191 | bulletproof_type: EBulletproofType, 192 | path='m/0/1/0', 193 | wallet_tx_id=None): 194 | # Pedersen commitment class 195 | p = Pedersen() 196 | 197 | # instantiate the keychain for this wallet seed 198 | keychain = KeyChain.fromSeed(self.master_seed) 199 | 200 | blinding_factor = keychain.derivePrivateKeyAmount( 201 | path, amount) 202 | commitment = p.commit(amount, blinding_factor) 203 | 204 | # bulletproof 205 | rangeproof = keychain.generateRangeProof( 206 | path, amount, commitment, 207 | blinding_factor, bulletproof_type) 208 | 209 | # build the output 210 | transaction_output = TransactionOutput( 211 | EOutputFeatures.DEFAULT, 212 | commitment, 213 | rangeproof) 214 | 215 | # clean-up 216 | del p 217 | 218 | return OutputDataEntity( 219 | path, blinding_factor, transaction_output, amount, 220 | EOutputStatus.NO_CONFIRMATIONS, wallet_tx_id=wallet_tx_id) 221 | 222 | 223 | def send( 224 | self, 225 | inputs: List[OutputDataEntity], 226 | num_change_outputs: int, 227 | amount: int, 228 | fee_base: int, 229 | block_height: int, 230 | send_entire_balance=False, 231 | receiver_address=None, 232 | path='m/0/1/0', 233 | wallet_tx_id=None, 234 | testnet=False) -> Tuple[Slate, SecretKey, SecretKey]: 235 | # if entire balance is sent, no change outputs 236 | if send_entire_balance: 237 | change_outputs = 0 238 | 239 | # the total number of outputs and number of kernels 240 | num_total_outputs = 1 + num_change_outputs 241 | num_kernels = 1 242 | 243 | # calculate fee 244 | fee = calculateFee(fee_base, len(inputs), num_total_outputs, num_kernels) 245 | 246 | # compute the total input 247 | input_total = 0 248 | for input_entry in inputs: 249 | input_total += input_entry.getAmount() 250 | 251 | # adjust the send amount if entire balance is being sent 252 | if send_entire_balance: 253 | amount = input_total - fee 254 | 255 | # compute the change amount and prepare the change outputs 256 | # each with own blinding factor xC 257 | change_amount = input_total - (amount + fee) 258 | change_outputs = [] 259 | for i in range(num_change_outputs): 260 | coin_amount = int(change_amount / num_change_outputs) 261 | if i == 0: 262 | coin_amount += change_amount % num_change_outputs 263 | change_outputs.append(self.createBlindedOutput( 264 | coin_amount, EBulletproofType.ENHANCED, 265 | path=path, wallet_tx_id=wallet_tx_id)) 266 | 267 | # prepare sender and receiver address for the payment proof 268 | keychain = KeyChain.fromSeed(self.master_seed) 269 | sender_address_bytes = keychain.deriveED25519PublicKey(path) 270 | receiver_address_bytes = None 271 | if receiver_address is not None: 272 | receiver = SlatepackAddress.fromBech32(receiver_address) 273 | receiver_address_bytes = receiver.toED25519() 274 | 275 | # build send slate 276 | slate_builder = SendSlateBuilder(self.master_seed) 277 | slate, secret_key, secret_nonce = slate_builder.build( 278 | amount, 279 | fee, 280 | block_height, 281 | inputs, 282 | change_outputs, 283 | sender_address=sender_address_bytes, 284 | receiver_address=receiver_address_bytes, 285 | testnet=testnet) 286 | return slate, secret_key, secret_nonce 287 | 288 | 289 | def receive( 290 | self, 291 | send_slate: Slate, 292 | path='m/0/1/0', 293 | wallet_tx_id=None, 294 | testnet=False) -> Slate: 295 | output = self.createBlindedOutput( 296 | send_slate.getAmount(), 297 | EBulletproofType.ENHANCED, 298 | path=path, 299 | wallet_tx_id=wallet_tx_id) 300 | # build the receive slate 301 | slate_builder = ReceiveSlateBuilder(self.master_seed) 302 | receive_slate = slate_builder.addReceiverData( 303 | send_slate, 304 | output, 305 | testnet=testnet) 306 | 307 | # update the payment proof 308 | payment_proof = receive_slate.getPaymentProof() 309 | if payment_proof is not None: 310 | sender_address = payment_proof.getSenderAddress() 311 | # prepare the receiver address to be able to update 312 | # the payment proof 313 | keychain = KeyChain.fromSeed(self.master_seed) 314 | receiver_address = keychain.deriveED25519PublicKey(path) 315 | 316 | # prepare the kernel commitment 317 | kernel_commitment = receive_slate.getKernelCommitment().getBytes() 318 | 319 | # message to be signed: 320 | # (amount | kernel commitment | sender address) 321 | serializer = Serializer() 322 | serializer.write(receive_slate.getAmount().to_bytes(8, 'big')) 323 | serializer.write(kernel_commitment) 324 | serializer.write(sender_address) 325 | message = serializer.readall() 326 | 327 | # produce the signature 328 | receiver_signature = keychain.signED25519(message, path) 329 | 330 | # update the payment proof data 331 | receive_slate.proof_opt.setReceiverAddress(receiver_address) 332 | receive_slate.proof_opt.setReceiverSignature(receiver_signature) 333 | 334 | # done! 335 | return receive_slate 336 | 337 | 338 | def invoice(self): 339 | raise Exception('unimplemented') 340 | 341 | 342 | def pay(self): 343 | raise Exception('unimplemented') 344 | 345 | 346 | def finalize( 347 | self, 348 | receive_slate: Slate, 349 | secret_key: SecretKey, 350 | secret_nonce: SecretKey, 351 | path='m/0/1/0', 352 | wallet_tx_id=None, 353 | testnet=False): 354 | # prepare the original sender address for the payment proof validation 355 | keychain = KeyChain.fromSeed(self.master_seed) 356 | sender_address_bytes = keychain.deriveED25519PublicKey(path) 357 | 358 | # build the receive slate 359 | slate_builder = FinalizeSlateBuilder(self.master_seed) 360 | finalized_slate = slate_builder.finalize( 361 | receive_slate, 362 | secret_key, 363 | secret_nonce, 364 | sender_address_bytes, 365 | testnet=testnet) 366 | return finalized_slate 367 | 368 | 369 | def ageDecrypt(self, ciphertext: bytes, path='m/0/1/0'): 370 | keychain = KeyChain.fromSeed(self.master_seed) 371 | return keychain.ageDecrypt(ciphertext, path) 372 | 373 | 374 | def decryptSlatepack(self, armored_slatepack: str, path='m/0/1/0'): 375 | slatepack_message = SlatepackMessage.unarmor(armored_slatepack) 376 | if not slatepack_message.is_encrypted(): 377 | # TODO raise a warning it was not encrypted 378 | return slatepack_message 379 | 380 | keychain = KeyChain.fromSeed(self.master_seed) 381 | age_secret_key = keychain.deriveAgeSecretKey(path) 382 | 383 | slatepack_message.decryptPayload(age_secret_key) 384 | 385 | return slatepack_message 386 | 387 | 388 | @classmethod 389 | def initialize(self): 390 | master_seed = os.urandom(32) 391 | return Wallet(master_seed=master_seed) 392 | 393 | 394 | @classmethod 395 | def fromSeedPhrase(self, phrase: str): 396 | M = Mnemonic() 397 | master_seed = M.entropyFromMnemonic(phrase) 398 | return Wallet(master_seed=master_seed) 399 | 400 | 401 | @classmethod 402 | def fromEncryptedSeedDict(self, seed: dict, passphrase=None): 403 | return self.fromEncryptedSeed( 404 | seed['encrypted_seed'], seed['salt'], seed['nonce'], 405 | passphrase=passphrase) 406 | 407 | 408 | @classmethod 409 | def fromEncryptedSeed( 410 | self, encrypted_seed_hex: str, salt_hex: str, nonce_hex: str, 411 | passphrase=None): 412 | encrypted_seed = bytes.fromhex(encrypted_seed_hex) 413 | nonce = bytes.fromhex(nonce_hex) 414 | salt = bytes.fromhex(salt_hex) 415 | w = Wallet(encrypted_seed=encrypted_seed, salt=salt, nonce=nonce) 416 | if passphrase is not None: 417 | w.unshieldWallet(passphrase, nonce=nonce, salt=salt) 418 | return w 419 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | age 2 | base58 3 | siphash-cffi 4 | secp256k1-zkp-mw 5 | bip32 6 | bip_utils 7 | cryptography 8 | pynacl 9 | pyrage 10 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | setuptools.setup( 4 | name='mimblewimble', 5 | version='0.4.0', 6 | packages=['mimblewimble'], 7 | license='MIT', 8 | description = 'A toolset for processing Grin Mimblewimble data structures', 9 | long_description=open('README.md').read(), 10 | long_description_content_type="text/markdown", 11 | author = 'Marek Narozniak', 12 | author_email = 'marek.yggdrasil@gmail.com', 13 | install_requires=[ 14 | 'base58', 15 | 'siphash-cffi', 16 | 'secp256k1-zkp-mw', 17 | 'bip_utils', 18 | 'bip32', 19 | 'pynacl', 20 | 'pyrage'], 21 | url = 'https://github.com/grinventions/mimblewimble-py', 22 | classifiers=[ 23 | "Programming Language :: Python :: 3", 24 | "Programming Language :: Python :: 2", 25 | "License :: OSI Approved :: MIT License", 26 | "Operating System :: OS Independent", 27 | ], 28 | include_package_data=True, 29 | package_data = { 30 | '': ['static/__init__.py', 'static/wordlist.json'], 31 | } 32 | ) 33 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grinventions/mimblewimble-py/5d904d2f0a0f3629210db5e0f634599b97e5283a/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_age.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | import base64 3 | import math 4 | 5 | from io import BytesIO 6 | 7 | from age.file import Decryptor as ageStreamDecrypt 8 | from age.keys.agekey import AgePrivateKey 9 | 10 | from bip_utils import Bech32Encoder 11 | 12 | from mimblewimble.serializer import Serializer 13 | from mimblewimble.crypto.age import AgeMessage 14 | 15 | from mimblewimble.helpers.encryption import ageX25519Decrypt 16 | 17 | from mimblewimble.models.slatepack.address import SlatepackAddress 18 | from mimblewimble.models.slatepack.metadata import SlatepackVersion 19 | from mimblewimble.models.slatepack.metadata import SlatepackMetadata 20 | 21 | from mimblewimble.models.slatepack.message import EMode 22 | 23 | raw = b'\xfb\xcf6\xfd\x01\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02{age-encryption.org/v1\n-> X25519 F/H3hf63Hrq2MH+O1naWm6+M8184u3S7oeHQ4VHiMzY\npbUcLLHdT5PGn/oYeiD+tsUO1oexurSZmSVu8Hcv7mc\n-> Es[-grease )vK1eC lVxuS`,6\nSkmgyCDG0wPb0wvGeYp6XbdSyMONfXYoRPLyQSlkse8tDxkTHKwjoz6Y3t2IIqHv\nU/dxyW2iZyZ1UVFRrlNAVbXbEBwqhpaFjDdRcivhbDo1uTL5CDNvJz89w+ivaoUc\n\n--- nq0hJv/bV3kLRrqKU9wlkexF2STZDmBf0hlees/HyWw\n\x8cA\xc3+\x1a\xaf\x10\x93T\x18\x0f\xf9J\x14\xaf\xb0\xcdw\xb9\x10\xa7\x90>V-\xc3=D\xafwc\xdeo\x08\xefx\xdd\x19\x85DtG\xad\xc8\x88*\x84\xe5Z\x07G\x95\x08\xad\xca\xb8\x81N\x1aJ\xfc\x97z\xa8\xfcR\xd8Y\xa4\x90\xcc[\x05\x989\xb1\x9d\xd1U\xdcsJu\xc7\xc4\xb9\x91\x03\x07\x08C\xd5\xfb>s\x80"\t\x85)\x8c(\x1dL\x9f\xa2\xea\xae\xf2f\xf8~\xfe\xc8\xc8\x14\xdeKD\xd8\x8b2\xd2\xca\xf4\xbdM\xcf\xe8_\x88\x03$=\xe1\x11(5g\xde\x9cF,S\xec0#*.\xa0\xe6H\x0f\x94\x8b\x02\xd1\xd9v\xea\xb8u%Dp\xb4\xb9\x88Jvz\xb9\x07\x1e$\x0f\xf5\x80\xe5\xbc\xe5I\x8b\x9f\xc8\xe1\x04\xec0T\x99\xf1cC\xd5\x90\xb27\x18\x1d\x88\x9d\x85\xea\x07\x11\xc4\x92\x0c\x07\xee\xc5\xb8\x1f\xf2&\xf29\x00R\xd4\xb6\xbfe\x18\xfd\xdd\x1a\x8f\xf0\xc3$\xfc0\xf3\xdc`\xed\xd0\xbd\xaa\xbd\t\x9d\xf1\x93.\xa6\xa6C\x0f\x0b;\x9er~P\xbd\xe1\xd5\xf5\x1f\xda>}\xe5J\x93\xc8\xde\xf4^\x9f\xbd\xbblK\xf3\xe3hP\xef\xdc\xb2\xbd\x05\xf1\x08\x9e\xe0\x96\xaeg\xd9\x1b\xc7\xf2/\xb8\xbe%aY\xdf\xc6T' 24 | s = Serializer() 25 | s.write(raw) 26 | 27 | 28 | def test_age_message_serialization(): 29 | serializer = s 30 | 31 | error_check_code = serializer.read(4) 32 | 33 | version = SlatepackVersion.deserialize(serializer) 34 | emode = EMode(int.from_bytes(serializer.read(1), 'big')) 35 | 36 | opt_flags = int.from_bytes(serializer.read(2), 'big') 37 | opt_fields_len = int.from_bytes(serializer.read(4), 'big') 38 | 39 | metadata = SlatepackMetadata() 40 | if opt_flags & 0x01 == 0x01: 41 | sender = SlatepackAddress.deserialize(serializer) 42 | metadata = SlatepackMetadata(sender=sender) 43 | 44 | payload_size = int.from_bytes(serializer.read(8), 'big') 45 | payload = serializer.read(payload_size) 46 | 47 | # checksum 48 | 49 | print() 50 | serializer.resetPointer() 51 | serializer.read(4) 52 | preimage = serializer.readremaining() 53 | hashed = hashlib.sha256(preimage).digest() 54 | hashed = hashlib.sha256(hashed).digest() 55 | checksum = hashed[0:4] 56 | if checksum == error_check_code: 57 | print('checksum correct') 58 | else: 59 | print('checksum mismatch') 60 | 61 | print('emode is', emode) 62 | if emode == EMode.ENCRYPTED: 63 | print('is encrypted') 64 | else: 65 | print('is not encrypted') 66 | 67 | # m/0/1/0 alice x25519 secret key 68 | x25519sk = bytes.fromhex('90ad4d97dd2f7dd5360375f2ad72011489baa8beb84f1109b2bdb79b4183476a') 69 | 70 | ciphertext = payload 71 | 72 | age_secret_key = Bech32Encoder.Encode( 73 | 'age-secret-key-', x25519sk) 74 | decrypted = ageX25519Decrypt(ciphertext, age_secret_key) 75 | 76 | print('decrypted payload') 77 | print(decrypted) 78 | 79 | d = Serializer() 80 | d.write(decrypted) 81 | 82 | size = int.from_bytes(d.read(4), 'big') 83 | 84 | inner_buffer = Serializer() 85 | inner_buffer.write(d.read(size)) 86 | 87 | opt_flags = int.from_bytes(inner_buffer.read(2), 'big') 88 | 89 | sender = None 90 | if opt_flags & 0x01 == 0x01: 91 | sender = SlatepackAddress.deserialize(inner_buffer) 92 | 93 | recipients = [] 94 | if opt_flags & 0x02 == 0x02: 95 | num_recipients = int.from_bytes(inner_buffer.read(2), 'big') 96 | for i in range(num_recipients): 97 | recipient = SlatepackAddress.deserialize(inner_buffer) 98 | recipients.append(recipient) 99 | 100 | print('sender', sender.toBech32()) 101 | print('recipients', recipients) 102 | 103 | 104 | ''' 105 | serializer = Serializer() 106 | 107 | age_message = AgeMessage.deserialize(s) 108 | 109 | serializer = Serializer() 110 | age_message.serialize(serializer) 111 | 112 | assert serializer.readall() == raw 113 | 114 | print() 115 | for recipient in age_message.header.recipients: 116 | print(recipient._type.decode(), [arg.decode() for arg in recipient.args]) 117 | ''' 118 | 119 | 120 | def test_age_recipient_stanza(): 121 | path = 'm/0/1/0' 122 | slatepack_address = 'grin14kgku7l5x6te3arast3p59zk4rteznq2ug6kmmypf2d6z8md76eqg3su35' 123 | ed25519pk = 'ad916e7bf4369798f47d82e21a1456a8d7914c0ae2356dec814a9ba11f6df6b2' 124 | ed25519sk = '7f7394cd934ba03a26d5774ef45b5969670d0ce1e4bbc1174e6a871f507af0ddad916e7bf4369798f47d82e21a1456a8d7914c0ae2356dec814a9ba11f6df6b2' 125 | x25519pk = '585b020389181ac540a9e268eb5f5d7d86297bad2ee0c8fb5eb533f92ce66d4d' 126 | x25519sk = '90ad4d97dd2f7dd5360375f2ad72011489baa8beb84f1109b2bdb79b4183476a' 127 | agepk = 'age1tpdsyqufrqdv2s9fuf5wkh6a0krzj7ad9msv3767k5eljt8xd4xscfzxhm' 128 | agesk = 'AGE-SECRET-KEY-1JZK5M97A9A7A2DSRWHE26USPZJYM4297HP83ZZDJHKMEKSVRGA4QG7GS5A' 129 | 130 | # header data 131 | ephemeral_share = b'F/H3hf63Hrq2MH+O1naWm6+M8184u3S7oeHQ4VHiMzY' + b'==' 132 | encrypted_file_key = b'pbUcLLHdT5PGn/oYeiD+tsUO1oexurSZmSVu8Hcv7mc' + b'==' 133 | header_mac = b'nq0hJv/bV3kLRrqKU9wlkexF2STZDmBf0hlees/HyWw' + b'==' 134 | 135 | # payload data 136 | encrypted_payload = b'\x8cA\xc3+\x1a\xaf\x10\x93T\x18\x0f\xf9J\x14\xaf\xb0\xcdw\xb9\x10\xa7\x90>V-\xc3=D\xafwc\xdeo\x08\xefx\xdd\x19\x85DtG\xad\xc8\x88*\x84\xe5Z\x07G\x95\x08\xad\xca\xb8\x81N\x1aJ\xfc\x97z\xa8\xfcR\xd8Y\xa4\x90\xcc[\x05\x989\xb1\x9d\xd1U\xdcsJu\xc7\xc4\xb9\x91\x03\x07\x08C\xd5\xfb>s\x80"\t\x85)\x8c(\x1dL\x9f\xa2\xea\xae\xf2f\xf8~\xfe\xc8\xc8\x14\xdeKD\xd8\x8b2\xd2\xca\xf4\xbdM\xcf\xe8_\x88\x03$=\xe1\x11(5g\xde\x9cF,S\xec0#*.\xa0\xe6H\x0f\x94\x8b\x02\xd1\xd9v\xea\xb8u%Dp\xb4\xb9\x88Jvz\xb9\x07\x1e$\x0f\xf5\x80\xe5\xbc\xe5I\x8b\x9f\xc8\xe1\x04\xec0T\x99\xf1cC\xd5\x90\xb27\x18\x1d\x88\x9d\x85\xea\x07\x11\xc4\x92\x0c\x07\xee\xc5\xb8\x1f\xf2&\xf29\x00R\xd4\xb6\xbfe\x18\xfd\xdd\x1a\x8f\xf0\xc3$\xfc0\xf3\xdc`\xed\xd0\xbd\xaa\xbd\t\x9d\xf1\x93.\xa6\xa6C\x0f\x0b;\x9er~P\xbd\xe1\xd5\xf5\x1f\xda>}\xe5J\x93\xc8\xde\xf4^\x9f\xbd\xbblK\xf3\xe3hP\xef\xdc\xb2\xbd\x05\xf1\x08\x9e\xe0\x96\xaeg\xd9\x1b\xc7\xf2/\xb8\xbe%aY\xdf\xc6T' 137 | 138 | # decode data from the header 139 | decoded_ephemeral_share = base64.b64decode( 140 | ephemeral_share) 141 | assert decoded_ephemeral_share.hex() == '17f1f785feb71ebab6307f8ed676969baf8cf35f38bb74bba1e1d0e151e23336' 142 | 143 | 144 | decoded_encrypted_file_key = base64.b64decode( 145 | encrypted_file_key) 146 | assert decoded_encrypted_file_key.hex() == 'a5b51c2cb1dd4f93c69ffa187a20feb6c50ed687b1bab49999256ef0772fee67' 147 | 148 | decoded_header_mac = base64.b64decode( 149 | header_mac) 150 | assert decoded_header_mac.hex() == '9ead2126ffdb57790b46ba8a53dc2591ec45d924d90e605fd2195e7acfc7c96c' 151 | 152 | public_key = bytes.fromhex(x25519pk) 153 | private_key = bytes.fromhex(x25519sk) 154 | 155 | salt = decoded_ephemeral_share + public_key 156 | 157 | from nacl.bindings import crypto_scalarmult 158 | 159 | key_material = crypto_scalarmult( 160 | private_key, decoded_ephemeral_share) 161 | 162 | assert key_material.hex() == '5ab5690070158a90a7dfefa4639381ff676782cd6fce9c2d50d31f2fed389c77' 163 | 164 | from cryptography.hazmat.backends import default_backend 165 | from cryptography.hazmat.primitives import hashes 166 | from cryptography.hazmat.primitives.kdf.hkdf import HKDF 167 | from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305 168 | 169 | AGE_X25519_HKDF_LABEL = b'age-encryption.org/v1/X25519' 170 | 171 | key = HKDF( 172 | algorithm=hashes.SHA256(), 173 | length=32, 174 | salt=salt, 175 | info=AGE_X25519_HKDF_LABEL, 176 | backend=default_backend() 177 | ).derive(key_material) 178 | 179 | assert key.hex() == 'd008d262cc77afdcf96c0a0363ec9894e1bc840a218a5afb86eacb1c5168ba0a' 180 | 181 | ZERO_NONCE = b'\00' * 12 182 | 183 | decrypted_file_key = ChaCha20Poly1305(key).decrypt( 184 | ZERO_NONCE, 185 | decoded_encrypted_file_key, 186 | None) 187 | 188 | # print('decrypted_file_key', decrypted_file_key.hex()) 189 | 190 | assert decrypted_file_key.hex() == 'd236c4a664b6e1d909a06bb8b7cdbd47' 191 | assert len(decrypted_file_key) == 16 192 | 193 | # assert decrypted_file_key == bytes.fromhex('d008d262cc77afdcf96c0a0363ec9894e1bc840a218a5afb86eacb1c5168ba0a') 194 | 195 | 196 | from cryptography.hazmat.primitives.hmac import HMAC 197 | 198 | HEADER_HKDF_LABEL = b'header' 199 | salt = b'' 200 | hkdfval = HKDF( 201 | algorithm=hashes.SHA256(), 202 | length=32, 203 | salt=salt, 204 | info=HEADER_HKDF_LABEL, 205 | backend=default_backend() 206 | ).derive(decrypted_file_key) 207 | assert hkdfval.hex() == 'e9e4ff52ce1c1c2f7c8d280fc6f869ade9bc005b28c07a36c17ee8839e59527f' 208 | 209 | hmac = HMAC( 210 | key=hkdfval, 211 | algorithm=hashes.SHA256(), 212 | backend=default_backend() 213 | ) 214 | 215 | mac_message = b'age-encryption.org/v1\n-> X25519 F/H3hf63Hrq2MH+O1naWm6+M8184u3S7oeHQ4VHiMzY\npbUcLLHdT5PGn/oYeiD+tsUO1oexurSZmSVu8Hcv7mc\n-> Es[-grease )vK1eC lVxuS`,6\nSkmgyCDG0wPb0wvGeYp6XbdSyMONfXYoRPLyQSlkse8tDxkTHKwjoz6Y3t2IIqHv\nU/dxyW2iZyZ1UVFRrlNAVbXbEBwqhpaFjDdRcivhbDo1uTL5CDNvJz89w+ivaoUc\n---' 216 | hmac.update(mac_message) 217 | 218 | # THIS FAILS TODO 219 | # hmac.verify(decoded_header_mac) 220 | 221 | # proceed to decrypt and prove decrypted payload is correct 222 | stream = BytesIO() 223 | stream.write(encrypted_payload) 224 | 225 | stream.seek(0) 226 | nonce = stream.read(16) 227 | assert nonce.hex() == '8c41c32b1aaf109354180ff94a14afb0' 228 | PAYLOAD_HKDF_LABEL = b'payload' 229 | stream_key = HKDF( 230 | algorithm=hashes.SHA256(), 231 | length=32, 232 | salt=nonce, 233 | info=PAYLOAD_HKDF_LABEL, 234 | backend=default_backend() 235 | ).derive(decrypted_file_key) 236 | 237 | assert stream_key.hex() == '8d1229aaaf0cdf5e7917ab2a725ff68f3338d76bd2734bc9e63fde486e12cdf9' 238 | assert len(stream_key) == 32 239 | 240 | ciphertext = stream.read() 241 | assert ciphertext == b'\xcdw\xb9\x10\xa7\x90>V-\xc3=D\xafwc\xdeo\x08\xefx\xdd\x19\x85DtG\xad\xc8\x88*\x84\xe5Z\x07G\x95\x08\xad\xca\xb8\x81N\x1aJ\xfc\x97z\xa8\xfcR\xd8Y\xa4\x90\xcc[\x05\x989\xb1\x9d\xd1U\xdcsJu\xc7\xc4\xb9\x91\x03\x07\x08C\xd5\xfb>s\x80"\t\x85)\x8c(\x1dL\x9f\xa2\xea\xae\xf2f\xf8~\xfe\xc8\xc8\x14\xdeKD\xd8\x8b2\xd2\xca\xf4\xbdM\xcf\xe8_\x88\x03$=\xe1\x11(5g\xde\x9cF,S\xec0#*.\xa0\xe6H\x0f\x94\x8b\x02\xd1\xd9v\xea\xb8u%Dp\xb4\xb9\x88Jvz\xb9\x07\x1e$\x0f\xf5\x80\xe5\xbc\xe5I\x8b\x9f\xc8\xe1\x04\xec0T\x99\xf1cC\xd5\x90\xb27\x18\x1d\x88\x9d\x85\xea\x07\x11\xc4\x92\x0c\x07\xee\xc5\xb8\x1f\xf2&\xf29\x00R\xd4\xb6\xbfe\x18\xfd\xdd\x1a\x8f\xf0\xc3$\xfc0\xf3\xdc`\xed\xd0\xbd\xaa\xbd\t\x9d\xf1\x93.\xa6\xa6C\x0f\x0b;\x9er~P\xbd\xe1\xd5\xf5\x1f\xda>}\xe5J\x93\xc8\xde\xf4^\x9f\xbd\xbblK\xf3\xe3hP\xef\xdc\xb2\xbd\x05\xf1\x08\x9e\xe0\x96\xaeg\xd9\x1b\xc7\xf2/\xb8\xbe%aY\xdf\xc6T' 242 | plaintext = b'' 243 | 244 | PLAINTEXT_BLOCK_SIZE = 64 * 1024 245 | CIPHERTEXT_BLOCK_SIZE = PLAINTEXT_BLOCK_SIZE + 16 246 | 247 | aead = ChaCha20Poly1305(stream_key) 248 | blocks = math.ceil(len(ciphertext) / CIPHERTEXT_BLOCK_SIZE) 249 | 250 | def _chunk(data, size): 251 | for i in range(0, len(data), size): 252 | yield data[i : i + size] 253 | 254 | 255 | for nonce, block in enumerate(_chunk(ciphertext, CIPHERTEXT_BLOCK_SIZE)): 256 | last_block = nonce == blocks - 1 257 | packed_nonce = nonce.to_bytes( 258 | 11, 259 | byteorder='big', 260 | signed=False 261 | ) + (b'\x01' if last_block else b'\x00') 262 | 263 | plaintext += aead.decrypt( 264 | nonce=packed_nonce, 265 | data=block, 266 | associated_data=None) 267 | 268 | assert plaintext == b'\x00\x00\x00B\x00\x01?grin1m4krnajw792zxfyldu79jssh0d3kzjtwpdn2wy7fysrfw4ej0waskurq76\x00\x04\x00\x03S\xfe\x05\xcd\x07\x8bK\x8f\x96\x0bV\xafo\x1a3\xb1\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x06\x00\x00\x00\x00:\xdc\r\xe0\x00\x00\x00\x00\x00\xbe\xbc \x01\x00\x02S3\xf6~\x84^\xdbV\xe3\t\xd6\x9c\x1b\xc5k\x89T\xde\t\x82\xba,\xe4!\x13\x0br\x15\x19\xa7\xa7\x9a\x022\xd9\xb4\x99\x04=\xfb5\xce\x97R;9\xa6\r\x1b"\xca.\x972bpKu\xb3\xd6\x1e\x05\xd4\x96\xe9\x02\xddl9\xf6N\xf1T#$\x9fo