├── .gitattributes ├── LICENSE ├── README.rst ├── openassets ├── __init__.py ├── protocol.py └── transactions.py ├── setup.py └── tests ├── __init__.py ├── helpers.py ├── test_protocol.py └── test_transactions.py /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Flavien Charlon 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 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Open Assets Reference Implementation 2 | ==================================== 3 | 4 | The ``openassets`` Python package is the reference implementation of the colored coins `Open Assets Protocol `_. 5 | 6 | Open Assets is a protocol for issuing and transferring custom digital tokens in a secure way on the Bitcoin blockchain (or any compatible blockchain). 7 | 8 | Requirements 9 | ============ 10 | 11 | The following items are required for using the ``openassets`` package: 12 | 13 | * Python 3.4 14 | * The `python-bitcoinlib `_ package 15 | 16 | Installation 17 | ============ 18 | 19 | Linux, OSX 20 | ---------- 21 | 22 | Using pip:: 23 | 24 | $ pip install openassets 25 | 26 | Or manually from source, assuming all required modules are installed on your system:: 27 | 28 | $ python ./setup.py install 29 | 30 | Windows 31 | ------- 32 | 33 | 1) Make sure you have `Python 3.4 and pip `_ installed 34 | 2) Open the command prompt: Start Menu > Accessories > Command Prompt 35 | 3) Run the following command:: 36 | 37 | pip install openassets 38 | 39 | Overview 40 | ======== 41 | 42 | The ``openassets`` package contains two submodules: the ``protocol`` submodule and the ``transactions`` submodule. 43 | 44 | ``protocol`` submodule 45 | ---------------------- 46 | 47 | The ``protocol`` submodule implements the specification in order to interpret Bitcoin transactions as Open Assets transactions. 48 | 49 | Usage 50 | ^^^^^ 51 | 52 | This example requires a Bitcoin Core instance running with RPC enabled and the ``-txindex=1`` parameter:: 53 | 54 | import asyncio 55 | import bitcoin.rpc 56 | import openassets.protocol 57 | 58 | @asyncio.coroutine 59 | def main(): 60 | bitcoin.SelectParams('testnet') 61 | 62 | # Create a RPC client for Bitcoin Core 63 | rpc_client = bitcoin.rpc.Proxy('http://user:pass@localhost:18332') 64 | # OutputCache implements the interface required for an output cache provider, but does not perform any caching 65 | cache = openassets.protocol.OutputCache() 66 | # The transaction provider is a function returning a transaction given its hash 67 | transaction_provider = asyncio.coroutine(rpc_client.getrawtransaction) 68 | # Instantiate the coloring engine 69 | coloring_engine = openassets.protocol.ColoringEngine(transaction_provider, cache, loop) 70 | 71 | transaction_hash = bitcoin.core.lx('864cbcb4b5e083a98aaeaf94443815025bdfb0d35a6fd00817034018b6752ff5') 72 | output_index = 1 73 | colored_output = yield from coloring_engine.get_output(transaction_hash, output_index) 74 | 75 | print(colored_output) 76 | 77 | loop = asyncio.get_event_loop() 78 | loop.run_until_complete(main()) 79 | 80 | ``transactions`` submodule 81 | -------------------------- 82 | 83 | The ``transactions`` submodule contains functions that can be used to build unsigned Open Assets transactions for various purposes. 84 | 85 | Usage 86 | ^^^^^ 87 | 88 | This example requires a Bitcoin Core instance running with RPC enabled and the ``-txindex=1`` parameter:: 89 | 90 | import asyncio 91 | import bitcoin.rpc 92 | import openassets.protocol 93 | import openassets.transactions 94 | 95 | @asyncio.coroutine 96 | def main(): 97 | bitcoin.SelectParams('testnet') 98 | 99 | # Create a RPC client for Bitcoin Core 100 | rpc_client = bitcoin.rpc.Proxy('http://user:pass@localhost:18332') 101 | 102 | # Output script corresponding to address myLPe3P8SE2DyqRwABRwqezxdZxhkYxXYu (in testnet) 103 | output_script = bitcoin.core.x('76a914c372d85bc2c54384dbc2cb9ef365eb7f15d4a9b688ac') 104 | 105 | # Initialize the coloring engine 106 | transaction_provider = asyncio.coroutine(rpc_client.getrawtransaction) 107 | engine = openassets.protocol.ColoringEngine(transaction_provider, openassets.protocol.OutputCache(), loop) 108 | 109 | # Obtain the unspent output for the local wallet 110 | unspent_outputs = [] 111 | for output in rpc_client.listunspent(): 112 | if output['scriptPubKey'] == output_script: 113 | unspent_outputs.append(openassets.transactions.SpendableOutput( 114 | bitcoin.core.COutPoint(output['outpoint'].hash, output['outpoint'].n), 115 | (yield from engine.get_output(output['outpoint'].hash, output['outpoint'].n)) 116 | )) 117 | 118 | # The minimum valid value for an output is set to 600 satoshis 119 | builder = openassets.transactions.TransactionBuilder(600) 120 | 121 | # Create the issuance parameters 122 | issuance_parameters = openassets.transactions.TransferParameters( 123 | unspent_outputs=unspent_outputs, # Unspent outputs the coins are issued from 124 | to_script=output_script, # The issued coins are sent back to the same address 125 | change_script=output_script, # The bitcoin change is sent back to the same address 126 | amount=1500) # Issue 1,500 units of the asset 127 | 128 | # Create the issuance transaction 129 | # The metadata is left empty and the fees are set to 0.0001 BTC 130 | transaction = builder.issue(issuance_parameters, metadata=b'', fees=10000) 131 | 132 | print(transaction) 133 | 134 | loop = asyncio.get_event_loop() 135 | loop.run_until_complete(main()) 136 | 137 | License 138 | ======= 139 | 140 | The MIT License (MIT) 141 | 142 | Copyright (c) 2014 Flavien Charlon 143 | 144 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 145 | 146 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 147 | 148 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 149 | -------------------------------------------------------------------------------- /openassets/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8; -*- 2 | # 3 | # The MIT License (MIT) 4 | # 5 | # Copyright (c) 2014 Flavien Charlon 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to deal 9 | # in the Software without restriction, including without limitation the rights 10 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | # copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in all 15 | # copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | # SOFTWARE. 24 | 25 | """ 26 | Reference implementation of the Open Assets Protocol. 27 | """ 28 | 29 | __version__ = '1.3' -------------------------------------------------------------------------------- /openassets/protocol.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8; -*- 2 | # 3 | # The MIT License (MIT) 4 | # 5 | # Copyright (c) 2014 Flavien Charlon 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to deal 9 | # in the Software without restriction, including without limitation the rights 10 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | # copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in all 15 | # copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | # SOFTWARE. 24 | 25 | """ 26 | Provides the infrastructure for calculating the asset ID and asset quantity of Bitcoin outputs, 27 | according to the Open Assets Protocol. 28 | """ 29 | 30 | import asyncio 31 | import bitcoin.core 32 | import bitcoin.core.script 33 | import enum 34 | import hashlib 35 | import io 36 | 37 | 38 | class ColoringEngine(object): 39 | """The backtracking engine used to find the asset ID and asset quantity of any output.""" 40 | 41 | def __init__(self, transaction_provider, cache, event_loop): 42 | """ 43 | Constructs an instance of the ColorEngine class. 44 | 45 | :param bytes -> Future[CTransaction] transaction_provider: A function returning a transaction given its hash. 46 | :param OutputCache cache: The cache object to use. 47 | :param BaseEventLoop | None event_loop: The event loop used to schedule asynchronous tasks. 48 | """ 49 | self._transaction_provider = transaction_provider 50 | self._cache = cache 51 | self._loop = event_loop 52 | 53 | @asyncio.coroutine 54 | def get_output(self, transaction_hash, output_index): 55 | """ 56 | Gets an output and information about its asset ID and asset quantity. 57 | 58 | :param bytes transaction_hash: The hash of the transaction containing the output. 59 | :param int output_index: The index of the output. 60 | :return: An object containing the output as well as its asset ID and asset quantity. 61 | :rtype: Future[TransactionOutput] 62 | """ 63 | cached_output = yield from self._cache.get(transaction_hash, output_index) 64 | 65 | if cached_output is not None: 66 | return cached_output 67 | 68 | transaction = yield from self._transaction_provider(transaction_hash) 69 | 70 | if transaction is None: 71 | raise ValueError('Transaction {0} could not be retrieved'.format(bitcoin.core.b2lx(transaction_hash))) 72 | 73 | colored_outputs = yield from self.color_transaction(transaction) 74 | 75 | for index, output in enumerate(colored_outputs): 76 | yield from self._cache.put(transaction_hash, index, output) 77 | 78 | return colored_outputs[output_index] 79 | 80 | @asyncio.coroutine 81 | def color_transaction(self, transaction): 82 | """ 83 | Computes the asset ID and asset quantity of every output in the transaction. 84 | 85 | :param CTransaction transaction: The transaction to color. 86 | :return: A list containing all the colored outputs of the transaction. 87 | :rtype: Future[list[TransactionOutput]] 88 | """ 89 | # If the transaction is a coinbase transaction, the marker output is always invalid 90 | if not transaction.is_coinbase(): 91 | for i, output in enumerate(transaction.vout): 92 | # Parse the OP_RETURN script 93 | marker_output_payload = MarkerOutput.parse_script(output.scriptPubKey) 94 | 95 | if marker_output_payload is not None: 96 | # Deserialize the payload as a marker output 97 | marker_output = MarkerOutput.deserialize_payload(marker_output_payload) 98 | 99 | if marker_output is not None: 100 | # Fetch the colored outputs for previous transactions 101 | inputs = [] 102 | for input in transaction.vin: 103 | inputs.append((yield from asyncio.async( 104 | self.get_output(input.prevout.hash, input.prevout.n), loop=self._loop))) 105 | 106 | asset_ids = self._compute_asset_ids( 107 | inputs, 108 | i, 109 | transaction.vout, 110 | marker_output.asset_quantities) 111 | 112 | if asset_ids is not None: 113 | return asset_ids 114 | 115 | # If no valid marker output was found in the transaction, all outputs are considered uncolored 116 | return [ 117 | TransactionOutput(output.nValue, output.scriptPubKey, None, 0, OutputType.uncolored) 118 | for output in transaction.vout] 119 | 120 | @classmethod 121 | def _compute_asset_ids(cls, inputs, marker_output_index, outputs, asset_quantities): 122 | """ 123 | Computes the asset IDs of every output in a transaction. 124 | 125 | :param list[TransactionOutput] inputs: The outputs referenced by the inputs of the transaction. 126 | :param int marker_output_index: The position of the marker output in the transaction. 127 | :param list[CTxOut] outputs: The outputs of the transaction. 128 | :param list[int] asset_quantities: The list of asset quantities of the outputs. 129 | :return: A list of outputs with asset ID and asset quantity information. 130 | :rtype: list[TransactionOutput] 131 | """ 132 | # If there are more items in the asset quantities list than outputs in the transaction (excluding the 133 | # marker output), the marker output is deemed invalid 134 | if len(asset_quantities) > len(outputs) - 1: 135 | return None 136 | 137 | # If there is no input in the transaction, the marker output is always invalid 138 | if len(inputs) == 0: 139 | return None 140 | 141 | result = [] 142 | 143 | # Add the issuance outputs 144 | issuance_asset_id = cls.hash_script(bytes(inputs[0].script)) 145 | 146 | for i in range(0, marker_output_index): 147 | value, script = outputs[i].nValue, outputs[i].scriptPubKey 148 | if i < len(asset_quantities) and asset_quantities[i] > 0: 149 | output = TransactionOutput(value, script, issuance_asset_id, asset_quantities[i], OutputType.issuance) 150 | else: 151 | output = TransactionOutput(value, script, None, 0, OutputType.issuance) 152 | 153 | result.append(output) 154 | 155 | # Add the marker output 156 | issuance_output = outputs[marker_output_index] 157 | result.append(TransactionOutput( 158 | issuance_output.nValue, issuance_output.scriptPubKey, None, 0, OutputType.marker_output)) 159 | 160 | # Add the transfer outputs 161 | input_iterator = iter(inputs) 162 | input_units_left = 0 163 | for i in range(marker_output_index + 1, len(outputs)): 164 | if i <= len(asset_quantities): 165 | output_asset_quantity = asset_quantities[i - 1] 166 | else: 167 | output_asset_quantity = 0 168 | 169 | output_units_left = output_asset_quantity 170 | asset_id = None 171 | 172 | while output_units_left > 0: 173 | 174 | # Move to the next input if the current one is depleted 175 | if input_units_left == 0: 176 | current_input = next(input_iterator, None) 177 | if current_input is None: 178 | # There are less asset units available in the input than in the outputs: 179 | # the marker output is considered invalid 180 | return None 181 | else: 182 | input_units_left = current_input.asset_quantity 183 | 184 | # If the current input is colored, assign its asset ID to the current output 185 | if current_input.asset_id is not None: 186 | progress = min(input_units_left, output_units_left) 187 | output_units_left -= progress 188 | input_units_left -= progress 189 | 190 | if asset_id is None: 191 | # This is the first input to map to this output 192 | asset_id = current_input.asset_id 193 | elif asset_id != current_input.asset_id: 194 | # Another different asset ID has already been assigned to that output: 195 | # the marker output is considered invalid 196 | return None 197 | 198 | result.append(TransactionOutput( 199 | outputs[i].nValue, outputs[i].scriptPubKey, asset_id, output_asset_quantity, OutputType.transfer)) 200 | 201 | return result 202 | 203 | @staticmethod 204 | def hash_script(data): 205 | """ 206 | Hashes a script into an asset ID using SHA256 followed by RIPEMD160. 207 | 208 | :param bytes data: The data to hash. 209 | """ 210 | sha256 = hashlib.sha256() 211 | ripemd = hashlib.new('ripemd160') 212 | 213 | sha256.update(data) 214 | ripemd.update(sha256.digest()) 215 | return ripemd.digest() 216 | 217 | 218 | class OutputType(enum.Enum): 219 | uncolored = 0 220 | marker_output = 1 221 | issuance = 2 222 | transfer = 3 223 | 224 | 225 | class TransactionOutput(object): 226 | """Represents a transaction output and its asset ID and asset quantity.""" 227 | 228 | def __init__( 229 | self, 230 | value=-1, 231 | script=bitcoin.core.script.CScript(), 232 | asset_id=None, 233 | asset_quantity=0, 234 | output_type=OutputType.uncolored): 235 | """ 236 | Initializes a new instance of the TransactionOutput class. 237 | 238 | :param int value: The satoshi value of the output. 239 | :param CScript script: The script controlling redemption of the output. 240 | :param bytes | None asset_id: The asset ID of the output. 241 | :param int asset_quantity: The asset quantity of the output. 242 | :param OutputType output_type: The type of the output. 243 | """ 244 | assert 0 <= asset_quantity <= MarkerOutput.MAX_ASSET_QUANTITY 245 | 246 | self._value = value 247 | self._script = script 248 | self._asset_id = asset_id 249 | self._asset_quantity = asset_quantity 250 | self._output_type = output_type 251 | 252 | @property 253 | def value(self): 254 | """ 255 | Gets the number of satoshis in the output. 256 | 257 | :return: The value of the output in satoshis. 258 | :rtype: int 259 | """ 260 | return self._value 261 | 262 | @property 263 | def script(self): 264 | """ 265 | Gets the script of the output. 266 | 267 | :return: The output script. 268 | :rtype: CScript 269 | """ 270 | return self._script 271 | 272 | @property 273 | def asset_id(self): 274 | """ 275 | Gets the asset ID of the output. 276 | 277 | :return: The asset ID of the output, or None of the output is uncolored. 278 | :rtype: bytes | None 279 | """ 280 | return self._asset_id 281 | 282 | @property 283 | def asset_quantity(self): 284 | """ 285 | Gets the asset quantity of the output. 286 | 287 | :return: The asset quantity of the output (zero if the output is uncolored). 288 | :rtype: int 289 | """ 290 | return self._asset_quantity 291 | 292 | @property 293 | def output_type(self): 294 | """ 295 | Gets the type of the output. 296 | 297 | :return: The type of the output. 298 | :rtype: OutputType 299 | """ 300 | return self._output_type 301 | 302 | def __repr__(self): 303 | return 'TransactionOutput(value=%r, script=%r, asset_id=%r, asset_quantity=%r, output_type=%r)' % \ 304 | (self.value, self.script, self.asset_id, self.asset_quantity, self.output_type) 305 | 306 | 307 | class OutputCache(object): 308 | """Represents the interface for an object capable of storing the result of output coloring.""" 309 | 310 | @asyncio.coroutine 311 | def get(self, transaction_hash, output_index): 312 | """ 313 | Returns a cached output. 314 | 315 | :param bytes transaction_hash: The hash of the transaction the output belongs to. 316 | :param int output_index: The index of the output in the transaction. 317 | :return: The output for the transaction hash and output index provided if it is found in the cache, or None 318 | otherwise. 319 | :rtype: TransactionOutput 320 | """ 321 | return None 322 | 323 | @asyncio.coroutine 324 | def put(self, transaction_hash, output_index, output): 325 | """ 326 | Saves an output in cache. 327 | 328 | :param bytes transaction_hash: The hash of the transaction the output belongs to. 329 | :param int output_index: The index of the output in the transaction. 330 | :param TransactionOutput output: The output to save. 331 | """ 332 | pass 333 | 334 | 335 | class MarkerOutput(object): 336 | """Represents an Open Assets marker output.""" 337 | 338 | MAX_ASSET_QUANTITY = 2 ** 63 - 1 339 | OPEN_ASSETS_TAG = b'OA\x01\x00' 340 | 341 | def __init__(self, asset_quantities, metadata): 342 | """ 343 | Initializes a new instance of the MarkerOutput class. 344 | 345 | :param list[int] asset_quantities: The list of asset quantities. 346 | :param bytes metadata: The metadata in the marker output. 347 | """ 348 | self._asset_quantities = asset_quantities 349 | self._metadata = metadata 350 | 351 | @property 352 | def asset_quantities(self): 353 | """ 354 | Gets the asset quantity list. 355 | 356 | :return: The asset quantity list of the output. 357 | :rtype: list[int] 358 | """ 359 | return self._asset_quantities 360 | 361 | @property 362 | def metadata(self): 363 | """ 364 | Gets the metadata contained in the marker output. 365 | 366 | :return: The metadata contained in the marker output. 367 | :rtype: bytes 368 | """ 369 | return self._metadata 370 | 371 | @classmethod 372 | def deserialize_payload(cls, payload): 373 | """ 374 | Deserializes the marker output payload. 375 | 376 | :param bytes payload: A buffer containing the marker output payload. 377 | :return: The marker output object. 378 | :rtype: MarkerOutput 379 | """ 380 | with io.BytesIO(payload) as stream: 381 | 382 | # The OAP marker and protocol version 383 | oa_version = stream.read(4) 384 | if oa_version != cls.OPEN_ASSETS_TAG: 385 | return None 386 | 387 | try: 388 | # Deserialize the expected number of items in the asset quantity list 389 | output_count = bitcoin.core.VarIntSerializer.stream_deserialize(stream) 390 | 391 | # LEB128-encoded unsigned integers representing the asset quantity of every output in order 392 | asset_quantities = [] 393 | for i in range(0, output_count): 394 | asset_quantity = cls.leb128_decode(stream) 395 | 396 | # If the LEB128-encoded asset quantity of any output exceeds 9 bytes, 397 | # the marker output is deemed invalid 398 | if asset_quantity > cls.MAX_ASSET_QUANTITY: 399 | return None 400 | 401 | asset_quantities.append(asset_quantity) 402 | 403 | # The var-integer encoded length of the metadata field. 404 | metadata_length = bitcoin.core.VarIntSerializer.stream_deserialize(stream) 405 | 406 | # The actual metadata 407 | metadata = stream.read(metadata_length) 408 | 409 | # If the metadata string wasn't long enough, the marker output is malformed 410 | if len(metadata) != metadata_length: 411 | return None 412 | 413 | # If there are bytes left to read, the marker output is malformed 414 | last_byte = stream.read(1) 415 | if len(last_byte) > 0: 416 | return None 417 | 418 | except bitcoin.core.SerializationTruncationError: 419 | return None 420 | 421 | return MarkerOutput(asset_quantities, metadata) 422 | 423 | def serialize_payload(self): 424 | """ 425 | Serializes the marker output data into a payload buffer. 426 | 427 | :return: The serialized payload. 428 | :rtype: bytes 429 | """ 430 | with io.BytesIO() as stream: 431 | stream.write(self.OPEN_ASSETS_TAG) 432 | 433 | bitcoin.core.VarIntSerializer.stream_serialize(len(self.asset_quantities), stream) 434 | for asset_quantity in self.asset_quantities: 435 | stream.write(self.leb128_encode(asset_quantity)) 436 | 437 | bitcoin.core.VarIntSerializer.stream_serialize(len(self.metadata), stream) 438 | 439 | stream.write(self.metadata) 440 | 441 | return stream.getvalue() 442 | 443 | @staticmethod 444 | def parse_script(output_script): 445 | """ 446 | Parses an output and returns the payload if the output matches the right pattern for a marker output, 447 | or None otherwise. 448 | 449 | :param CScript output_script: The output script to be parsed. 450 | :return: The marker output payload if the output fits the pattern, None otherwise. 451 | :rtype: bytes 452 | """ 453 | script_iterator = output_script.raw_iter() 454 | 455 | try: 456 | first_opcode, _, _ = next(script_iterator, (None, None, None)) 457 | _, data, _ = next(script_iterator, (None, None, None)) 458 | remainder = next(script_iterator, None) 459 | except bitcoin.core.script.CScriptTruncatedPushDataError: 460 | return None 461 | except bitcoin.core.script.CScriptInvalidError: 462 | return None 463 | 464 | if first_opcode == bitcoin.core.script.OP_RETURN and data is not None and remainder is None: 465 | return data 466 | else: 467 | return None 468 | 469 | @staticmethod 470 | def build_script(data): 471 | """ 472 | Creates an output script containing an OP_RETURN and a PUSHDATA. 473 | 474 | :param bytes data: The content of the PUSHDATA. 475 | :return: The final script. 476 | :rtype: CScript 477 | """ 478 | return bitcoin.core.script.CScript( 479 | bytes([bitcoin.core.script.OP_RETURN]) + bitcoin.core.script.CScriptOp.encode_op_pushdata(data)) 480 | 481 | @staticmethod 482 | def leb128_decode(data): 483 | """ 484 | Decodes a LEB128-encoded unsigned integer. 485 | 486 | :param BufferedIOBase data: The buffer containing the LEB128-encoded integer to decode. 487 | :return: The decoded integer. 488 | :rtype: int 489 | """ 490 | result = 0 491 | shift = 0 492 | 493 | while True: 494 | character = data.read(1) 495 | if len(character) == 0: 496 | raise bitcoin.core.SerializationTruncationError('Invalid LEB128 integer') 497 | 498 | b = ord(character) 499 | result |= (b & 0x7f) << shift 500 | if b & 0x80 == 0: 501 | break 502 | shift += 7 503 | return result 504 | 505 | @staticmethod 506 | def leb128_encode(value): 507 | """ 508 | Encodes an integer using LEB128. 509 | 510 | :param int value: The value to encode. 511 | :return: The LEB128-encoded integer. 512 | :rtype: bytes 513 | """ 514 | if value == 0: 515 | return b'\x00' 516 | 517 | result = [] 518 | while value != 0: 519 | byte = value & 0x7f 520 | value >>= 7 521 | if value != 0: 522 | byte |= 0x80 523 | result.append(byte) 524 | 525 | return bytes(result) 526 | 527 | def __repr__(self): 528 | return 'MarkerOutput(asset_quantities=%r, metadata=%r)' % (self.asset_quantities, self.metadata) 529 | -------------------------------------------------------------------------------- /openassets/transactions.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8; -*- 2 | # 3 | # The MIT License (MIT) 4 | # 5 | # Copyright (c) 2014 Flavien Charlon 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to deal 9 | # in the Software without restriction, including without limitation the rights 10 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | # copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in all 15 | # copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | # SOFTWARE. 24 | 25 | """ 26 | Provides functions for constructing unsigned Open Assets transactions. 27 | """ 28 | 29 | import bitcoin.core 30 | import openassets.protocol 31 | 32 | 33 | class TransactionBuilder(object): 34 | """Provides methods for constructing Open Assets transactions.""" 35 | 36 | def __init__(self, dust_amount): 37 | """ 38 | Initializes a new instance of the TransactionBuilder class. 39 | 40 | :param int dust_amount: The minimum allowed output value. 41 | """ 42 | self._dust_amount = dust_amount 43 | 44 | def issue(self, issuance_spec, metadata, fees): 45 | """ 46 | Creates a transaction for issuing an asset. 47 | 48 | :param TransferParameters issuance_spec: The parameters of the issuance. 49 | :param bytes metadata: The metadata to be embedded in the transaction. 50 | :param int fees: The fees to include in the transaction. 51 | :return: An unsigned transaction for issuing an asset. 52 | :rtype: CTransaction 53 | """ 54 | inputs, total_amount = self._collect_uncolored_outputs( 55 | issuance_spec.unspent_outputs, 2 * self._dust_amount + fees) 56 | 57 | return bitcoin.core.CTransaction( 58 | vin=[bitcoin.core.CTxIn(item.out_point, item.output.script) for item in inputs], 59 | vout=[ 60 | self._get_colored_output(issuance_spec.to_script), 61 | self._get_marker_output([issuance_spec.amount], metadata), 62 | self._get_uncolored_output(issuance_spec.change_script, total_amount - self._dust_amount - fees) 63 | ] 64 | ) 65 | 66 | def transfer(self, asset_transfer_specs, btc_transfer_spec, fees): 67 | """ 68 | Creates a transaction for sending assets and bitcoins. 69 | 70 | :param list[(bytes, TransferParameters)] asset_transfer_specs: A list of tuples. In each tuple: 71 | - The first element is the ID of an asset. 72 | - The second element is the parameters of the transfer. 73 | :param TransferParameters btc_transfer_spec: The parameters of the bitcoins being transferred. 74 | :param int fees: The fees to include in the transaction. 75 | :return: An unsigned transaction for sending assets and bitcoins. 76 | :rtype: CTransaction 77 | """ 78 | inputs = [] 79 | outputs = [] 80 | asset_quantities = [] 81 | for asset_id, transfer_spec in asset_transfer_specs: 82 | colored_outputs, collected_amount = self._collect_colored_outputs( 83 | transfer_spec.unspent_outputs, asset_id, transfer_spec.amount) 84 | inputs.extend(colored_outputs) 85 | outputs.append(self._get_colored_output(transfer_spec.to_script)) 86 | asset_quantities.append(transfer_spec.amount) 87 | 88 | if collected_amount > transfer_spec.amount: 89 | outputs.append(self._get_colored_output(transfer_spec.change_script)) 90 | asset_quantities.append(collected_amount - transfer_spec.amount) 91 | 92 | btc_excess = sum([input.output.value for input in inputs]) - sum([output.nValue for output in outputs]) 93 | 94 | if btc_excess < btc_transfer_spec.amount + fees: 95 | # Not enough bitcoin inputs 96 | uncolored_outputs, total_amount = self._collect_uncolored_outputs( 97 | btc_transfer_spec.unspent_outputs, btc_transfer_spec.amount + fees - btc_excess) 98 | inputs.extend(uncolored_outputs) 99 | btc_excess += total_amount 100 | 101 | change = btc_excess - btc_transfer_spec.amount - fees 102 | if change > 0: 103 | # Too much bitcoin in input, send it back as change 104 | outputs.append(self._get_uncolored_output(btc_transfer_spec.change_script, change)) 105 | 106 | if btc_transfer_spec.amount > 0: 107 | outputs.append(self._get_uncolored_output(btc_transfer_spec.to_script, btc_transfer_spec.amount)) 108 | 109 | if asset_quantities: 110 | outputs.insert(0, self._get_marker_output(asset_quantities, b'')) 111 | 112 | return bitcoin.core.CTransaction( 113 | vin=[bitcoin.core.CTxIn(item.out_point, item.output.script) for item in inputs], 114 | vout=outputs 115 | ) 116 | 117 | def transfer_bitcoin(self, transfer_spec, fees): 118 | """ 119 | Creates a transaction for sending bitcoins. 120 | 121 | :param TransferParameters transfer_spec: The parameters of the bitcoins being transferred. 122 | :param int fees: The fees to include in the transaction. 123 | :return: The resulting unsigned transaction. 124 | :rtype: CTransaction 125 | """ 126 | return self.transfer([], transfer_spec, fees) 127 | 128 | def transfer_assets(self, asset_id, transfer_spec, btc_change_script, fees): 129 | """ 130 | Creates a transaction for sending an asset. 131 | 132 | :param bytes asset_id: The ID of the asset being sent. 133 | :param TransferParameters transfer_spec: The parameters of the asset being transferred. 134 | :param bytes btc_change_script: The script where to send bitcoin change, if any. 135 | :param int fees: The fees to include in the transaction. 136 | :return: The resulting unsigned transaction. 137 | :rtype: CTransaction 138 | """ 139 | return self.transfer( 140 | [(asset_id, transfer_spec)], 141 | TransferParameters(transfer_spec.unspent_outputs, None, btc_change_script, 0), 142 | fees) 143 | 144 | def btc_asset_swap(self, btc_transfer_spec, asset_id, asset_transfer_spec, fees): 145 | """ 146 | Creates a transaction for swapping assets for bitcoins. 147 | 148 | :param TransferParameters btc_transfer_spec: The parameters of the bitcoins being transferred. 149 | :param bytes asset_id: The ID of the asset being sent. 150 | :param TransferParameters asset_transfer_spec: The parameters of the asset being transferred. 151 | :param int fees: The fees to include in the transaction. 152 | :return: The resulting unsigned transaction. 153 | :rtype: CTransaction 154 | """ 155 | return self.transfer([(asset_id, asset_transfer_spec)], btc_transfer_spec, fees) 156 | 157 | def asset_asset_swap( 158 | self, asset1_id, asset1_transfer_spec, asset2_id, asset2_transfer_spec, fees): 159 | """ 160 | Creates a transaction for swapping an asset for another asset. 161 | 162 | :param bytes asset1_id: The ID of the first asset. 163 | :param TransferParameters asset1_transfer_spec: The parameters of the first asset being transferred. 164 | It is also used for paying fees and/or receiving change if any. 165 | :param bytes asset2_id: The ID of the second asset. 166 | :param TransferDetails asset2_transfer_spec: The parameters of the second asset being transferred. 167 | :param int fees: The fees to include in the transaction. 168 | :return: The resulting unsigned transaction. 169 | :rtype: CTransaction 170 | """ 171 | btc_transfer_spec = TransferParameters( 172 | asset1_transfer_spec.unspent_outputs, asset1_transfer_spec.to_script, asset1_transfer_spec.change_script, 0) 173 | 174 | return self.transfer( 175 | [(asset1_id, asset1_transfer_spec), (asset2_id, asset2_transfer_spec)], btc_transfer_spec, fees) 176 | 177 | @staticmethod 178 | def _collect_uncolored_outputs(unspent_outputs, amount): 179 | """ 180 | Returns a list of uncolored outputs for the specified amount. 181 | 182 | :param list[SpendableOutput] unspent_outputs: The list of available outputs. 183 | :param int amount: The amount to collect. 184 | :return: A list of outputs, and the total amount collected. 185 | :rtype: (list[SpendableOutput], int) 186 | """ 187 | total_amount = 0 188 | result = [] 189 | for output in unspent_outputs: 190 | if output.output.asset_id is None: 191 | result.append(output) 192 | total_amount += output.output.value 193 | 194 | if total_amount >= amount: 195 | return result, total_amount 196 | 197 | raise InsufficientFundsError 198 | 199 | @staticmethod 200 | def _collect_colored_outputs(unspent_outputs, asset_id, asset_quantity): 201 | """ 202 | Returns a list of colored outputs for the specified quantity. 203 | 204 | :param list[SpendableOutput] unspent_outputs: The list of available outputs. 205 | :param bytes asset_id: The ID of the asset to collect. 206 | :param int asset_quantity: The asset quantity to collect. 207 | :return: A list of outputs, and the total asset quantity collected. 208 | :rtype: (list[SpendableOutput], int) 209 | """ 210 | total_amount = 0 211 | result = [] 212 | for output in unspent_outputs: 213 | if output.output.asset_id == asset_id: 214 | result.append(output) 215 | total_amount += output.output.asset_quantity 216 | 217 | if total_amount >= asset_quantity: 218 | return result, total_amount 219 | 220 | raise InsufficientAssetQuantityError 221 | 222 | def _get_uncolored_output(self, script, value): 223 | """ 224 | Creates an uncolored output. 225 | 226 | :param bytes script: The output script. 227 | :param int value: The satoshi value of the output. 228 | :return: An object representing the uncolored output. 229 | :rtype: TransactionOutput 230 | """ 231 | if value < self._dust_amount: 232 | raise DustOutputError 233 | 234 | return bitcoin.core.CTxOut(value, bitcoin.core.CScript(script)) 235 | 236 | def _get_colored_output(self, script): 237 | """ 238 | Creates a colored output. 239 | 240 | :param bytes script: The output script. 241 | :return: An object representing the colored output. 242 | :rtype: TransactionOutput 243 | """ 244 | return bitcoin.core.CTxOut(self._dust_amount, bitcoin.core.CScript(script)) 245 | 246 | def _get_marker_output(self, asset_quantities, metadata): 247 | """ 248 | Creates a marker output. 249 | 250 | :param list[int] asset_quantities: The asset quantity list. 251 | :param bytes metadata: The metadata contained in the output. 252 | :return: An object representing the marker output. 253 | :rtype: TransactionOutput 254 | """ 255 | payload = openassets.protocol.MarkerOutput(asset_quantities, metadata).serialize_payload() 256 | script = openassets.protocol.MarkerOutput.build_script(payload) 257 | return bitcoin.core.CTxOut(0, script) 258 | 259 | 260 | class SpendableOutput(object): 261 | """Represents a transaction output with information about the asset ID and asset quantity associated to it.""" 262 | 263 | def __init__(self, out_point, output): 264 | """ 265 | Initializes a new instance of the TransactionOutput class. 266 | 267 | :param COutPoint out_point: An object that can be used to locate the output. 268 | :param TransactionOutput output: The actual output object. 269 | """ 270 | self._out_point = out_point 271 | self._output = output 272 | 273 | @property 274 | def out_point(self): 275 | """ 276 | Gets an object that can be used to locate the output. 277 | 278 | :return: An object that can be used to locate the output. 279 | :rtype: COutPoint 280 | """ 281 | return self._out_point 282 | 283 | @property 284 | def output(self): 285 | """ 286 | Gets the output object. 287 | 288 | :return: The actual output object. 289 | :rtype: TransactionOutput 290 | """ 291 | return self._output 292 | 293 | 294 | class TransferParameters(object): 295 | """Encapsulates the details of a bitcoin or asset transfer.""" 296 | 297 | def __init__(self, unspent_outputs, to_script, change_script, amount): 298 | """ 299 | Initializes an instance of the TransferParameters class. 300 | 301 | :param list[SpendableOutput] unspent_outputs: The unspent outputs available for the transaction. 302 | :param bytes to_script: The output script to which to send the assets or bitcoins. 303 | :param bytes change_script: The output script to which to send any remaining change. 304 | :param int amount: The asset quantity or amount of satoshis sent in the transaction. 305 | """ 306 | self._unspent_outputs = unspent_outputs 307 | self._to_script = to_script 308 | self._change_script = change_script 309 | self._amount = amount 310 | 311 | @property 312 | def unspent_outputs(self): 313 | """ 314 | Gets the unspent outputs available for the transaction. 315 | 316 | :return: The list of unspent outputs. 317 | :rtype: list[SpendableOutput] 318 | """ 319 | return self._unspent_outputs 320 | 321 | @property 322 | def to_script(self): 323 | """ 324 | Gets the output script to which to send the assets or bitcoins. 325 | 326 | :return: The output script. 327 | :rtype: bytes 328 | """ 329 | return self._to_script 330 | 331 | @property 332 | def change_script(self): 333 | """ 334 | Gets the output script to which to send any remaining change. 335 | 336 | :return: The output script. 337 | :rtype: bytes 338 | """ 339 | return self._change_script 340 | 341 | @property 342 | def amount(self): 343 | """ 344 | Gets either the asset quantity or amount of satoshis sent in the transaction. 345 | 346 | :return: The asset quantity or amount of satoshis. 347 | :rtype: int 348 | """ 349 | return self._amount 350 | 351 | 352 | class TransactionBuilderError(Exception): 353 | """The transaction could not be built.""" 354 | pass 355 | 356 | 357 | class InsufficientFundsError(TransactionBuilderError): 358 | """An insufficient amount of bitcoins is available.""" 359 | pass 360 | 361 | 362 | class InsufficientAssetQuantityError(TransactionBuilderError): 363 | """An insufficient amount of assets is available.""" 364 | pass 365 | 366 | 367 | class DustOutputError(TransactionBuilderError): 368 | """The value of an output would be too small, and the output would be considered as dust.""" 369 | pass -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import openassets 4 | import os 5 | import setuptools 6 | 7 | here = os.path.abspath(os.path.dirname(__file__)) 8 | with open(os.path.join(here, 'README.rst')) as f: 9 | README = f.read() 10 | 11 | setuptools.setup( 12 | name = 'openassets', 13 | version = openassets.__version__, 14 | packages = [ 'openassets' ], 15 | description = 'Reference implementation of the Open Assets Protocol', 16 | author = 'Flavien Charlon', 17 | author_email = 'flavien@charlon.net', 18 | url = 'https://github.com/OpenAssets/openassets', 19 | license = 'MIT License', 20 | install_requires = [ 21 | 'python-bitcoinlib == 0.2.1' 22 | ], 23 | test_suite = 'tests', 24 | classifiers = [ 25 | 'Development Status :: 5 - Production/Stable', 26 | 'License :: OSI Approved :: MIT License', 27 | 'Operating System :: OS Independent', 28 | 'Programming Language :: Python :: 3', 29 | 'Topic :: Software Development :: Libraries :: Python Modules' 30 | ] 31 | ) -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8; -*- 2 | # 3 | # The MIT License (MIT) 4 | # 5 | # Copyright (c) 2014 Flavien Charlon 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to deal 9 | # in the Software without restriction, including without limitation the rights 10 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | # copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in all 15 | # copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | # SOFTWARE. 24 | -------------------------------------------------------------------------------- /tests/helpers.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8; -*- 2 | # 3 | # The MIT License (MIT) 4 | # 5 | # Copyright (c) 2014 Flavien Charlon 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to deal 9 | # in the Software without restriction, including without limitation the rights 10 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | # copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in all 15 | # copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | # SOFTWARE. 24 | 25 | import asyncio 26 | 27 | 28 | @asyncio.coroutine 29 | def assert_coroutine_raises(test, exception_type, target, *args, **kwargs): 30 | try: 31 | yield from target(*args, **kwargs) 32 | test.fail() 33 | except exception_type: 34 | return 35 | except: 36 | test.fail() 37 | 38 | 39 | def async_test(function): 40 | def wrapper(*args, **kwargs): 41 | coroutine = asyncio.coroutine(function) 42 | loop = asyncio.new_event_loop() 43 | kwargs['loop'] = loop 44 | future = coroutine(*args, **kwargs) 45 | loop.run_until_complete(future) 46 | loop.close() 47 | return wrapper 48 | -------------------------------------------------------------------------------- /tests/test_protocol.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8; -*- 2 | # 3 | # The MIT License (MIT) 4 | # 5 | # Copyright (c) 2014 Flavien Charlon 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to deal 9 | # in the Software without restriction, including without limitation the rights 10 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | # copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in all 15 | # copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | # SOFTWARE. 24 | 25 | import asyncio 26 | import binascii 27 | import bitcoin.core 28 | import io 29 | import openassets.protocol 30 | import tests.helpers 31 | import unittest 32 | import unittest.mock 33 | 34 | from openassets.protocol import OutputType 35 | 36 | 37 | class ColoringEngineTests(unittest.TestCase): 38 | 39 | # get_output 40 | 41 | @unittest.mock.patch('openassets.protocol.ColoringEngine.color_transaction', autospec=True) 42 | @unittest.mock.patch('openassets.protocol.OutputCache.get', autospec=True) 43 | @unittest.mock.patch('openassets.protocol.OutputCache.put', autospec=True) 44 | @tests.helpers.async_test 45 | def test_get_output_success(self, put_mock, get_mock, color_transaction_mock, loop): 46 | get_mock.return_value = self.as_future(None, loop) 47 | color_transaction_mock.return_value = self.as_future(self.create_test_outputs(), loop) 48 | 49 | @asyncio.coroutine 50 | def transaction_provider(transaction_hash): 51 | return self.create_test_transaction(b'') 52 | 53 | target = openassets.protocol.ColoringEngine(transaction_provider, openassets.protocol.OutputCache(), loop) 54 | 55 | result = yield from target.get_output(b'abcd', 2) 56 | 57 | self.assert_output(result, 3, b'\x30', b'b', 1, OutputType.transfer) 58 | self.assertEqual(get_mock.call_args_list[0][0][1:], (b'abcd', 2)) 59 | self.assertEqual(3, len(put_mock.call_args_list)) 60 | self.assertEqual(put_mock.call_args_list[0][0][1:3], (b'abcd', 0)) 61 | self.assert_output(put_mock.call_args_list[0][0][3], 1, b'\x10', b'a', 6, OutputType.issuance) 62 | self.assertEqual(put_mock.call_args_list[1][0][1:3], (b'abcd', 1)) 63 | self.assert_output(put_mock.call_args_list[1][0][3], 2, b'\x20', b'a', 2, OutputType.marker_output) 64 | self.assertEqual(put_mock.call_args_list[2][0][1:3], (b'abcd', 2)) 65 | self.assert_output(put_mock.call_args_list[2][0][3], 3, b'\x30', b'b', 1, OutputType.transfer) 66 | 67 | @unittest.mock.patch('openassets.protocol.OutputCache.get', autospec=True) 68 | @unittest.mock.patch('openassets.protocol.OutputCache.put', autospec=True) 69 | @tests.helpers.async_test 70 | def test_get_output_not_found(self, put_mock, get_mock, loop): 71 | get_mock.return_value = self.as_future(None, loop) 72 | 73 | @asyncio.coroutine 74 | def transaction_provider(transaction_hash): 75 | return None 76 | 77 | target = openassets.protocol.ColoringEngine(transaction_provider, openassets.protocol.OutputCache(), loop) 78 | 79 | yield from tests.helpers.assert_coroutine_raises(self, ValueError, target.get_output, b'abcd', 2) 80 | 81 | self.assertEqual(get_mock.call_args_list[0][0][1:], (b'abcd', 2)) 82 | 83 | @unittest.mock.patch('openassets.protocol.OutputCache.get', autospec=True) 84 | @unittest.mock.patch('openassets.protocol.OutputCache.put', autospec=True) 85 | @tests.helpers.async_test 86 | def test_get_output_cached(self, put_mock, get_mock, loop): 87 | get_mock.return_value = self.as_future(self.create_test_outputs()[2], loop) 88 | 89 | target = openassets.protocol.ColoringEngine(None, openassets.protocol.OutputCache(), loop) 90 | 91 | result = yield from target.get_output(b'abcd', 2) 92 | 93 | self.assert_output(result, 3, b'\x30', b'b', 1, OutputType.transfer) 94 | self.assertEqual(get_mock.call_args_list[0][0][1:], (b'abcd', 2)) 95 | 96 | @unittest.mock.patch('openassets.protocol.OutputCache.get', autospec=True) 97 | @unittest.mock.patch('openassets.protocol.OutputCache.put', autospec=True) 98 | @tests.helpers.async_test 99 | def test_get_output_deep_chain(self, put_mock, get_mock, loop): 100 | get_mock.return_value = self.as_future(None, loop) 101 | 102 | depth_counter = [1000] 103 | def transaction_provider(transaction_hash): 104 | depth_counter[0] -= 1 105 | if depth_counter[0] > 1: 106 | result = bitcoin.core.CTransaction( 107 | [ 108 | bitcoin.core.CTxIn(bitcoin.core.COutPoint(b'\x01' * 32, 1)) 109 | ], 110 | [ 111 | bitcoin.core.CTxOut(0, bitcoin.core.CScript(b'\x6a\x07OA\x01\x00' + b'\x01\x05' + b'\00')), 112 | bitcoin.core.CTxOut(10, bitcoin.core.CScript(b'\x10')) 113 | ]) 114 | elif depth_counter[0] == 1: 115 | result = bitcoin.core.CTransaction( 116 | [ 117 | bitcoin.core.CTxIn(bitcoin.core.COutPoint(b'\x01' * 32, 0)) 118 | ], 119 | [ 120 | bitcoin.core.CTxOut(20, bitcoin.core.CScript(b'\x20')), 121 | bitcoin.core.CTxOut(30, bitcoin.core.CScript(b'\x30')), 122 | bitcoin.core.CTxOut(0, bitcoin.core.CScript(b'\x6a\x08OA\x01\x00' + b'\x02\x00\x05' + b'\00')), 123 | ]) 124 | else: 125 | result = bitcoin.core.CTransaction([], [bitcoin.core.CTxOut(40, bitcoin.core.CScript(b'\x40'))]) 126 | 127 | return self.as_future(result, loop) 128 | 129 | target = openassets.protocol.ColoringEngine(transaction_provider, openassets.protocol.OutputCache(), loop) 130 | 131 | result = yield from target.get_output(b'\x01', 1) 132 | 133 | issuance_asset_id = openassets.protocol.ColoringEngine.hash_script(b'\x40') 134 | self.assert_output(result, 10, b'\x10', issuance_asset_id, 5, OutputType.transfer) 135 | self.assertEqual(0, depth_counter[0]) 136 | self.assertEqual(1000, len(get_mock.call_args_list)) 137 | self.assertEqual(2 * 998 + 3 + 1, len(put_mock.call_args_list)) 138 | 139 | # color_transaction 140 | 141 | @unittest.mock.patch('openassets.protocol.ColoringEngine.get_output', autospec=True) 142 | @tests.helpers.async_test 143 | def test_color_transaction_success(self, get_output_mock, loop): 144 | get_output_mock.side_effect = lambda _, __, index: self.as_future(self.create_test_outputs()[index - 1], loop) 145 | 146 | target = openassets.protocol.ColoringEngine(None, None, loop) 147 | 148 | # Valid transaction 149 | outputs = yield from target.color_transaction(self.create_test_transaction( 150 | b'\x6a\x08' + b'OA\x01\x00' + b'\x02\x05\x07' + b'\00')) 151 | 152 | issuance_asset_id = openassets.protocol.ColoringEngine.hash_script(b'\x10') 153 | self.assert_output(outputs[0], 10, b'\x10', issuance_asset_id, 5, OutputType.issuance) 154 | self.assert_output(outputs[1], 20, b'\x6a\x08' + b'OA\x01\x00' + b'\x02\x05\x07' + b'\00', 155 | None, 0, OutputType.marker_output) 156 | self.assert_output(outputs[2], 30, b'\x20', b'a', 7, OutputType.transfer) 157 | 158 | # Invalid payload 159 | outputs = yield from target.color_transaction(self.create_test_transaction( 160 | b'\x6a\x04' + b'OA\x01\x00')) 161 | 162 | self.assert_output(outputs[0], 10, b'\x10', None, 0, OutputType.uncolored) 163 | self.assert_output(outputs[1], 20, b'\x6a\x04' + b'OA\x01\x00', None, 0, OutputType.uncolored) 164 | self.assert_output(outputs[2], 30, b'\x20', None, 0, OutputType.uncolored) 165 | 166 | # Invalid coloring (the asset quantity count is larger than the number of items in the asset quantity list) 167 | outputs = yield from target.color_transaction(self.create_test_transaction( 168 | b'\x6a\x08' + b'OA\x01\x00' + b'\x03\x05\x09\x08' + b'\00')) 169 | 170 | self.assert_output(outputs[0], 10, b'\x10', None, 0, OutputType.uncolored) 171 | self.assert_output(outputs[1], 20, b'\x6a\x08' + b'OA\x01\x00' + b'\x03\x05\x09\x08' + b'\00', 172 | None, 0, OutputType.uncolored) 173 | self.assert_output(outputs[2], 30, b'\x20', None, 0, OutputType.uncolored) 174 | 175 | @unittest.mock.patch('openassets.protocol.ColoringEngine.get_output', autospec=True) 176 | @tests.helpers.async_test 177 | def test_color_transaction_invalid_marker_outputs(self, get_output_mock, loop): 178 | get_output_mock.side_effect = lambda _, __, index: self.as_future(self.create_test_outputs()[index - 1], loop) 179 | 180 | target = openassets.protocol.ColoringEngine(None, None, loop) 181 | 182 | transaction = bitcoin.core.CTransaction( 183 | [ 184 | bitcoin.core.CTxIn(bitcoin.core.COutPoint(b'\x01' * 32, 1)), 185 | bitcoin.core.CTxIn(bitcoin.core.COutPoint(b'\x02' * 32, 2)), 186 | bitcoin.core.CTxIn(bitcoin.core.COutPoint(b'\x03' * 32, 3)) 187 | ], 188 | [ 189 | # If the LEB128-encoded asset quantity of any output exceeds 9 bytes, 190 | # the marker output is deemed invalid 191 | bitcoin.core.CTxOut(10, bitcoin.core.CScript( 192 | b'\x6a\x10' + b'OA\x01\x00' + b'\x01' + b'\x80\x80\x80\x80\x80\x80\x80\x80\x80\x02' + b'\x00')), 193 | # If the marker output is malformed, it is considered invalid. 194 | bitcoin.core.CTxOut(20, bitcoin.core.CScript( 195 | b'\x6a\x07' + b'OA\x01\x00' + b'\x01' + b'\x04' + b'\x02')), 196 | # If there are more items in the asset quantity list than the number of colorable outputs, 197 | # the marker output is deemed invalid 198 | bitcoin.core.CTxOut(30, bitcoin.core.CScript( 199 | b'\x6a\x0D' + b'OA\x01\x00' + b'\x07' + b'\x01\x01\x01\x01\x01\x01\x01' + b'\x00')), 200 | # If there are less asset units in the input sequence than in the output sequence, 201 | # the marker output is considered invalid. 202 | bitcoin.core.CTxOut(40, bitcoin.core.CScript( 203 | b'\x6a\x0B' + b'OA\x01\x00' + b'\x05' + b'\x00\x00\x00\x08\x02' + b'\x00')), 204 | # If any output contains units from more than one distinct asset ID, 205 | # the marker output is considered invalid 206 | bitcoin.core.CTxOut(50, bitcoin.core.CScript( 207 | b'\x6a\x0B' + b'OA\x01\x00' + b'\x05' + b'\x00\x00\x00\x00\x09' + b'\x00')), 208 | # Valid marker output 209 | bitcoin.core.CTxOut(60, bitcoin.core.CScript( 210 | b'\x6a\x0C' + b'OA\x01\x00' + b'\x06' + b'\x01\x01\x01\x01\x01\x08' + b'\x00')), 211 | bitcoin.core.CTxOut(70, bitcoin.core.CScript(b'\x20')) 212 | ] 213 | ) 214 | 215 | outputs = yield from target.color_transaction(transaction) 216 | 217 | issuance_asset_id = openassets.protocol.ColoringEngine.hash_script(b'\x10') 218 | vout = transaction.vout 219 | self.assert_output(outputs[0], 10, vout[0].scriptPubKey, issuance_asset_id, 1, OutputType.issuance) 220 | self.assert_output(outputs[1], 20, vout[1].scriptPubKey, issuance_asset_id, 1, OutputType.issuance) 221 | self.assert_output(outputs[2], 30, vout[2].scriptPubKey, issuance_asset_id, 1, OutputType.issuance) 222 | self.assert_output(outputs[3], 40, vout[3].scriptPubKey, issuance_asset_id, 1, OutputType.issuance) 223 | self.assert_output(outputs[4], 50, vout[4].scriptPubKey, issuance_asset_id, 1, OutputType.issuance) 224 | self.assert_output(outputs[5], 60, vout[5].scriptPubKey, None, 0, OutputType.marker_output) 225 | self.assert_output(outputs[6], 70, vout[6].scriptPubKey, b'a', 8, OutputType.transfer) 226 | 227 | @unittest.mock.patch('openassets.protocol.ColoringEngine.get_output', autospec=True) 228 | @tests.helpers.async_test 229 | def test_color_transaction_coinbase(self, get_output_mock, loop): 230 | get_output_mock.side_effect = lambda _, __, index: self.as_future(self.create_test_outputs()[index - 1], loop) 231 | 232 | target = openassets.protocol.ColoringEngine(None, None, loop) 233 | 234 | transaction = bitcoin.core.CTransaction( 235 | [ 236 | bitcoin.core.CTxIn(bitcoin.core.COutPoint(b'\x00' * 32, 0xffffffff)) 237 | ], 238 | [ 239 | bitcoin.core.CTxOut(10, bitcoin.core.CScript(b'\x10')), 240 | bitcoin.core.CTxOut(20, bitcoin.core.CScript(b'\x6a\x07' + b'OA\x01\x00' + b'\x01\x05' + b'\00')), 241 | bitcoin.core.CTxOut(30, bitcoin.core.CScript(b'\x20')) 242 | ] 243 | ) 244 | 245 | outputs = yield from target.color_transaction(transaction) 246 | 247 | self.assert_output(outputs[0], 10, transaction.vout[0].scriptPubKey, None, 0, OutputType.uncolored) 248 | self.assert_output(outputs[1], 20, transaction.vout[1].scriptPubKey, None, 0, OutputType.uncolored) 249 | self.assert_output(outputs[2], 30, transaction.vout[2].scriptPubKey, None, 0, OutputType.uncolored) 250 | 251 | # _compute_asset_ids 252 | 253 | def test_compute_asset_ids_issuance(self): 254 | # Issue an asset 255 | outputs = self.color_outputs( 256 | inputs=[ 257 | {'asset_id': None, 'asset_quantity': 0, 'output_script': b'abcdef'}, 258 | {'asset_id': None, 'asset_quantity': 0, 'output_script': b'ghijkl'} 259 | ], 260 | asset_quantities=[1, 3], 261 | marker_index=2, 262 | output_count=3 263 | ) 264 | 265 | issuance_asset_id = openassets.protocol.ColoringEngine.hash_script(b'abcdef') 266 | self.assert_output(outputs[0], 0, b'0', issuance_asset_id, 1, OutputType.issuance) 267 | self.assert_output(outputs[1], 1, b'1', issuance_asset_id, 3, OutputType.issuance) 268 | self.assert_output(outputs[2], 2, b'2', None, 0, OutputType.marker_output) 269 | 270 | def test_compute_asset_ids_transfer(self): 271 | # No asset quantity defined 272 | outputs = self.color_outputs( 273 | inputs=[ 274 | {'asset_id': b'a', 'asset_quantity': 2} 275 | ], 276 | asset_quantities=[], 277 | output_count=1 278 | ) 279 | self.assert_output(outputs[0], 0, b'0', None, 0, OutputType.marker_output) 280 | 281 | # More asset quantity values than outputs 282 | outputs = self.color_outputs( 283 | inputs=[ 284 | {'asset_id': b'a', 'asset_quantity': 2} 285 | ], 286 | asset_quantities=[1], 287 | output_count=1 288 | ) 289 | self.assertIsNone(outputs) 290 | 291 | # Single input and single output 292 | outputs = self.color_outputs( 293 | inputs=[ 294 | {'asset_id': b'a', 'asset_quantity': 2} 295 | ], 296 | asset_quantities=[2], 297 | output_count=2 298 | ) 299 | self.assert_output(outputs[0], 0, b'0', None, 0, OutputType.marker_output) 300 | self.assert_output(outputs[1], 1, b'1', b'a', 2, OutputType.transfer) 301 | 302 | # Empty outputs 303 | outputs = self.color_outputs( 304 | inputs=[ 305 | {'asset_id': b'a', 'asset_quantity': 2} 306 | ], 307 | asset_quantities=[0, 1, 0, 1], 308 | output_count=6 309 | ) 310 | self.assert_output(outputs[0], 0, b'0', None, 0, OutputType.marker_output) 311 | self.assert_output(outputs[1], 1, b'1', None, 0, OutputType.transfer) 312 | self.assert_output(outputs[2], 2, b'2', b'a', 1, OutputType.transfer) 313 | self.assert_output(outputs[3], 3, b'3', None, 0, OutputType.transfer) 314 | self.assert_output(outputs[4], 4, b'4', b'a', 1, OutputType.transfer) 315 | self.assert_output(outputs[5], 5, b'5', None, 0, OutputType.transfer) 316 | 317 | # Empty inputs 318 | outputs = self.color_outputs( 319 | inputs=[ 320 | {'asset_id': None, 'asset_quantity': 0}, 321 | {'asset_id': b'a', 'asset_quantity': 1}, 322 | {'asset_id': None, 'asset_quantity': 0}, 323 | {'asset_id': b'a', 'asset_quantity': 1}, 324 | {'asset_id': None, 'asset_quantity': 0} 325 | ], 326 | asset_quantities=[2], 327 | output_count=2 328 | ) 329 | self.assert_output(outputs[0], 0, b'0', None, 0, OutputType.marker_output) 330 | self.assert_output(outputs[1], 1, b'1', b'a', 2, OutputType.transfer) 331 | 332 | # Input partially unassigned 333 | outputs = self.color_outputs( 334 | inputs=[ 335 | {'asset_id': b'a', 'asset_quantity': 1}, 336 | {'asset_id': b'a', 'asset_quantity': 3} 337 | ], 338 | asset_quantities=[1, 2], 339 | output_count=3 340 | ) 341 | self.assert_output(outputs[0], 0, b'0', None, 0, OutputType.marker_output) 342 | self.assert_output(outputs[1], 1, b'1', b'a', 1, OutputType.transfer) 343 | self.assert_output(outputs[2], 2, b'2', b'a', 2, OutputType.transfer) 344 | 345 | # Entire input unassigned 346 | outputs = self.color_outputs( 347 | inputs=[ 348 | {'asset_id': b'a', 'asset_quantity': 1}, 349 | {'asset_id': b'a', 'asset_quantity': 3} 350 | ], 351 | asset_quantities=[1], 352 | output_count=3 353 | ) 354 | self.assert_output(outputs[0], 0, b'0', None, 0, OutputType.marker_output) 355 | self.assert_output(outputs[1], 1, b'1', b'a', 1, OutputType.transfer) 356 | self.assert_output(outputs[2], 2, b'2', None, 0, OutputType.transfer) 357 | 358 | # Output partially unassigned 359 | outputs = self.color_outputs( 360 | inputs=[ 361 | {'asset_id': b'a', 'asset_quantity': 1}, 362 | {'asset_id': b'a', 'asset_quantity': 2} 363 | ], 364 | asset_quantities=[1, 3], 365 | output_count=3 366 | ) 367 | self.assertIsNone(outputs) 368 | 369 | # Entire output unassigned 370 | outputs = self.color_outputs( 371 | inputs=[ 372 | {'asset_id': b'a', 'asset_quantity': 1} 373 | ], 374 | asset_quantities=[1, 3], 375 | output_count=3 376 | ) 377 | self.assertIsNone(outputs) 378 | 379 | # Multiple inputs and outputs - Matching asset quantities 380 | outputs = self.color_outputs( 381 | inputs=[ 382 | {'asset_id': b'a', 'asset_quantity': 1}, 383 | {'asset_id': b'b', 'asset_quantity': 2}, 384 | {'asset_id': b'c', 'asset_quantity': 3} 385 | ], 386 | asset_quantities=[1, 2, 3], 387 | output_count=4 388 | ) 389 | self.assert_output(outputs[0], 0, b'0', None, 0, OutputType.marker_output) 390 | self.assert_output(outputs[1], 1, b'1', b'a', 1, OutputType.transfer) 391 | self.assert_output(outputs[2], 2, b'2', b'b', 2, OutputType.transfer) 392 | self.assert_output(outputs[3], 3, b'3', b'c', 3, OutputType.transfer) 393 | 394 | # Multiple inputs and outputs - Mixing same asset 395 | outputs = self.color_outputs( 396 | inputs=[ 397 | {'asset_id': b'a', 'asset_quantity': 2}, 398 | {'asset_id': b'a', 'asset_quantity': 1}, 399 | {'asset_id': b'a', 'asset_quantity': 2} 400 | ], 401 | asset_quantities=[1, 3, 1], 402 | output_count=4 403 | ) 404 | self.assert_output(outputs[0], 0, b'0', None, 0, OutputType.marker_output) 405 | self.assert_output(outputs[1], 1, b'1', b'a', 1, OutputType.transfer) 406 | self.assert_output(outputs[2], 2, b'2', b'a', 3, OutputType.transfer) 407 | self.assert_output(outputs[3], 3, b'3', b'a', 1, OutputType.transfer) 408 | 409 | # Multiple inputs and outputs - Mixing different assets 410 | outputs = self.color_outputs( 411 | inputs=[ 412 | {'asset_id': b'a', 'asset_quantity': 2}, 413 | {'asset_id': b'b', 'asset_quantity': 1}, 414 | {'asset_id': b'c', 'asset_quantity': 2} 415 | ], 416 | asset_quantities=[1, 3, 1], 417 | output_count=4 418 | ) 419 | self.assertIsNone(outputs) 420 | 421 | def test_compute_asset_ids_issuance_transfer(self): 422 | # Transaction mixing both issuance and transfer 423 | outputs = self.color_outputs( 424 | inputs=[ 425 | {'asset_id': b'a', 'asset_quantity': 3, 'output_script': b'abcdef'}, 426 | {'asset_id': b'a', 'asset_quantity': 2, 'output_script': b'ghijkl'} 427 | ], 428 | asset_quantities=[1, 4, 2, 3], 429 | marker_index=2, 430 | output_count=5 431 | ) 432 | 433 | issuance_asset_id = openassets.protocol.ColoringEngine.hash_script(b'abcdef') 434 | self.assert_output(outputs[0], 0, b'0', issuance_asset_id, 1, OutputType.issuance) 435 | self.assert_output(outputs[1], 1, b'1', issuance_asset_id, 4, OutputType.issuance) 436 | self.assert_output(outputs[2], 2, b'2', None, 0, OutputType.marker_output) 437 | self.assert_output(outputs[3], 3, b'3', b'a', 2, OutputType.transfer) 438 | self.assert_output(outputs[4], 4, b'4', b'a', 3, OutputType.transfer) 439 | 440 | def test_compute_asset_ids_no_input(self): 441 | # Transaction with no input 442 | outputs = self.color_outputs( 443 | inputs=[], 444 | asset_quantities=[3], 445 | marker_index=1, 446 | output_count=2 447 | ) 448 | 449 | self.assertIsNone(outputs) 450 | 451 | def test_compute_asset_ids_no_asset_quantity(self): 452 | # Issue an asset 453 | outputs = self.color_outputs( 454 | inputs=[ 455 | {'asset_id': None, 'asset_quantity': 0, 'output_script': b'abcdef'}, 456 | {'asset_id': None, 'asset_quantity': 0, 'output_script': b'ghijkl'} 457 | ], 458 | asset_quantities=[], 459 | marker_index=1, 460 | output_count=3 461 | ) 462 | 463 | self.assert_output(outputs[0], 0, b'0', None, 0, OutputType.issuance) 464 | self.assert_output(outputs[1], 1, b'1', None, 0, OutputType.marker_output) 465 | self.assert_output(outputs[2], 2, b'2', None, 0, OutputType.transfer) 466 | 467 | def test_compute_asset_ids_zero_asset_quantity(self): 468 | # Issue an asset 469 | outputs = self.color_outputs( 470 | inputs=[ 471 | {'asset_id': None, 'asset_quantity': 0, 'output_script': b'abcdef'}, 472 | {'asset_id': None, 'asset_quantity': 0, 'output_script': b'ghijkl'} 473 | ], 474 | asset_quantities=[0], 475 | marker_index=1, 476 | output_count=3 477 | ) 478 | 479 | self.assert_output(outputs[0], 0, b'0', None, 0, OutputType.issuance) 480 | self.assert_output(outputs[1], 1, b'1', None, 0, OutputType.marker_output) 481 | self.assert_output(outputs[2], 2, b'2', None, 0, OutputType.transfer) 482 | 483 | # hash_script 484 | 485 | def test_hash_script(self): 486 | previous_output = binascii.unhexlify('76a914010966776006953D5567439E5E39F86A0D273BEE88AC') 487 | output = openassets.protocol.ColoringEngine.hash_script(previous_output) 488 | self.assertEqual(binascii.unhexlify('36e0ea8e93eaa0285d641305f4c81e563aa570a2'), output) 489 | 490 | # Test helpers 491 | 492 | def color_outputs(self, inputs, asset_quantities, output_count, marker_index=0): 493 | previous_outputs = [ 494 | openassets.protocol.TransactionOutput( 495 | 10, bitcoin.core.CScript(item.get('output_script', b'\x01\x02')), 496 | item['asset_id'], 497 | item['asset_quantity'], 498 | None) 499 | for item in inputs] 500 | 501 | outputs = [] 502 | for i in range(0, output_count): 503 | outputs.append(bitcoin.core.CTxOut(i, bitcoin.core.CScript(bytes(str(i), encoding='UTF-8')))) 504 | 505 | return openassets.protocol.ColoringEngine._compute_asset_ids( 506 | previous_outputs, 507 | marker_index, 508 | outputs, 509 | asset_quantities) 510 | 511 | def assert_output(self, output, value, script, asset_id, asset_quantity, output_type): 512 | self.assertEqual(value, output.value) 513 | self.assertEqual(script, bytes(output.script)) 514 | self.assertEqual(asset_id, output.asset_id) 515 | self.assertEqual(asset_quantity, output.asset_quantity) 516 | self.assertEqual(output_type, output.output_type) 517 | 518 | def create_test_transaction(self, marker_output): 519 | return bitcoin.core.CTransaction( 520 | [ 521 | bitcoin.core.CTxIn(bitcoin.core.COutPoint(b'\x01' * 32, 1)), 522 | bitcoin.core.CTxIn(bitcoin.core.COutPoint(b'\x02' * 32, 2)), 523 | bitcoin.core.CTxIn(bitcoin.core.COutPoint(b'\x03' * 32, 3)) 524 | ], 525 | [ 526 | bitcoin.core.CTxOut(10, bitcoin.core.CScript(b'\x10')), 527 | bitcoin.core.CTxOut(20, bitcoin.core.CScript(marker_output)), 528 | bitcoin.core.CTxOut(30, bitcoin.core.CScript(b'\x20')) 529 | ] 530 | ) 531 | 532 | def create_test_outputs(self): 533 | return [ 534 | openassets.protocol.TransactionOutput(1, bitcoin.core.CScript(b'\x10'), b'a', 6, OutputType.issuance), 535 | openassets.protocol.TransactionOutput(2, bitcoin.core.CScript(b'\x20'), b'a', 2, OutputType.marker_output), 536 | openassets.protocol.TransactionOutput(3, bitcoin.core.CScript(b'\x30'), b'b', 1, OutputType.transfer) 537 | ] 538 | 539 | @staticmethod 540 | def as_future(result, loop): 541 | future = asyncio.Future(loop=loop) 542 | future.set_result(result) 543 | return future 544 | 545 | 546 | class MarkerOutputTests(unittest.TestCase): 547 | def test_leb128_encode_decode_success(self): 548 | def assert_leb128(value, data): 549 | # Check encoding 550 | encoded = openassets.protocol.MarkerOutput.leb128_encode(value) 551 | self.assertEqual(data, encoded) 552 | 553 | # Check decoding 554 | with io.BytesIO(data) as stream: 555 | result = openassets.protocol.MarkerOutput.leb128_decode(stream) 556 | self.assertEqual(value, result) 557 | 558 | assert_leb128(0, b'\x00') 559 | assert_leb128(1, b'\x01') 560 | assert_leb128(127, b'\x7F') 561 | assert_leb128(128, b'\x80\x01') 562 | assert_leb128(0xff, b'\xff\x01') 563 | assert_leb128(0x100, b'\x80\x02') 564 | assert_leb128(300, b'\xac\x02') 565 | assert_leb128(624485, b'\xe5\x8e\x26') 566 | assert_leb128(0xffffff, b'\xff\xff\xff\x07') 567 | assert_leb128(0x1000000, b'\x80\x80\x80\x08') 568 | assert_leb128(2 ** 64, b'\x80\x80\x80\x80\x80\x80\x80\x80\x80\x02') 569 | 570 | def test_leb128_decode_invalid(self): 571 | data = b'\xe5\x8e' 572 | 573 | with io.BytesIO(data) as stream: 574 | self.assertRaises(bitcoin.core.SerializationTruncationError, 575 | openassets.protocol.MarkerOutput.leb128_decode, stream) 576 | 577 | def test_parse_script_success(self): 578 | def assert_parse_script(expected, data): 579 | script = bitcoin.core.CScript(data) 580 | self.assertEqual(expected, openassets.protocol.MarkerOutput.parse_script(script)) 581 | 582 | assert_parse_script(b'', b'\x6a\x00') 583 | assert_parse_script(b'abcdef', b'\x6a\x06abcdef') 584 | assert_parse_script(b'abcdef', b'\x6a\x4c\x06abcdef') 585 | assert_parse_script(b'abcdef', b'\x6a\x4d\x06\x00abcdef') 586 | assert_parse_script(b'abcdef', b'\x6a\x4e\x06\x00\x00\x00abcdef') 587 | 588 | def test_parse_script_invalid(self): 589 | def assert_parse_script(data): 590 | self.assertIsNone(openassets.protocol.MarkerOutput.parse_script(bitcoin.core.CScript(data))) 591 | 592 | # The first operator is not OP_RETURN 593 | assert_parse_script(b'\x6b\x00') 594 | # No PUSHDATA 595 | assert_parse_script(b'\x6a') 596 | assert_parse_script(b'\x6a\x75') 597 | # Invalid PUSHDATA 598 | assert_parse_script(b'\x6a\x06') 599 | assert_parse_script(b'\x6a\x05abcdef') 600 | assert_parse_script(b'\x6a\x4d') 601 | # Additional operators 602 | assert_parse_script(b'\x6a\x06abcdef\x01a') 603 | assert_parse_script(b'\x6a\x06abcdef\x75') 604 | 605 | def test_build_script(self): 606 | def assert_build_script(expected_script, data): 607 | script = openassets.protocol.MarkerOutput.build_script(data) 608 | self.assertEqual(expected_script, bytes(script)) 609 | 610 | assert_build_script(b'\x6a\00', b'') 611 | assert_build_script(b'\x6a\05abcde', b'abcde') 612 | assert_build_script(b'\x6a\x4c\x4c' + (b'a' * 76), b'a' * 76) 613 | assert_build_script(b'\x6a\x4d\x00\x01' + (b'a' * 256), b'a' * 256) 614 | 615 | def test_serialize_deserialize_payload_success(self): 616 | def assert_deserialize_payload(asset_quantities, metadata, data): 617 | # Check serialization 618 | serialized_output = openassets.protocol.MarkerOutput(asset_quantities, metadata).serialize_payload() 619 | self.assertEqual(data, serialized_output) 620 | 621 | # Check deserialization 622 | marker_output = openassets.protocol.MarkerOutput.deserialize_payload(data) 623 | self.assertEqual(asset_quantities, marker_output.asset_quantities) 624 | self.assertEqual(metadata, marker_output.metadata) 625 | 626 | assert_deserialize_payload([5, 300], b'abcdef', b'OA\x01\x00' + b'\x02\x05\xac\x02' + b'\06abcdef') 627 | # Large number of asset quantities 628 | assert_deserialize_payload([5] * 256, b'abcdef', 629 | b'OA\x01\x00' + b'\xfd\x00\x01' + (b'\x05' * 256) + b'\06abcdef') 630 | # Large metadata 631 | assert_deserialize_payload([5], b'\x01' * 256, 632 | b'OA\x01\x00' + b'\x01\x05' + b'\xfd\x00\x01' + b'\x01' * 256) 633 | # Biggest valid output quantity 634 | assert_deserialize_payload([2 ** 63 - 1], b'', 635 | b'OA\x01\x00' + b'\x01' + (b'\xFF' * 8) + b'\x7F' + b'\x00') 636 | 637 | def test_deserialize_payload_invalid(self): 638 | def assert_deserialize_payload(data): 639 | self.assertIsNone(openassets.protocol.MarkerOutput.deserialize_payload(data)) 640 | 641 | # Invalid OAP tag 642 | assert_deserialize_payload(b'OB\x01\x00' + b'\x02\x01\xac\x02' + b'\06abcdef') 643 | assert_deserialize_payload(b'OA\x02\x00' + b'\x02\x01\xac\x02' + b'\06abcdef') 644 | # Invalid length 645 | assert_deserialize_payload(b'O') 646 | assert_deserialize_payload(b'OA\x01') 647 | assert_deserialize_payload(b'OA\x01\x00' + b'\x02\x01') 648 | assert_deserialize_payload(b'OA\x01\x00' + b'\x02\x01\xac') 649 | assert_deserialize_payload(b'OA\x01\x00' + b'\x02\x01\xac\x02' + b'\06abcd') 650 | assert_deserialize_payload(b'OA\x01\x00' + b'\x02\x01\xac\x02' + b'\06abcdefgh') 651 | assert_deserialize_payload(b'OA\x01\x00' + b'\xfd\x00') 652 | # Asset quantity too large 653 | assert_deserialize_payload(b'OA\x01\x00' + b'\x01' + (b'\x80' * 9) + b'\01' + b'\x00') 654 | 655 | def test_repr(self): 656 | target = openassets.protocol.MarkerOutput([5, 100, 0], b'abcd') 657 | self.assertEqual('MarkerOutput(asset_quantities=[5, 100, 0], metadata=b\'abcd\')', str(target)) 658 | 659 | 660 | class TransactionOutputTests(unittest.TestCase): 661 | def test_init_success(self): 662 | target = openassets.protocol.TransactionOutput( 663 | 100, bitcoin.core.CScript(b'abcd'), b'efgh', 2 ** 63 - 1, OutputType.transfer) 664 | 665 | self.assertEqual(100, target.value) 666 | self.assertEqual(b'abcd', bytes(target.script)) 667 | self.assertEqual(b'efgh', target.asset_id) 668 | self.assertEqual(2 ** 63 - 1, target.asset_quantity) 669 | self.assertEqual(OutputType.transfer, target.output_type) 670 | 671 | def test_init_invalid_asset_quantity(self): 672 | # The asset quantity must be between 0 and 2**63 - 1 673 | self.assertRaises(AssertionError, openassets.protocol.TransactionOutput, 674 | 100, bitcoin.core.CScript(b'abcd'), b'efgh', 2 ** 63, OutputType.transfer) 675 | self.assertRaises(AssertionError, openassets.protocol.TransactionOutput, 676 | 100, bitcoin.core.CScript(b'abcd'), b'efgh', -1, OutputType.transfer) 677 | 678 | def test_repr(self): 679 | target = openassets.protocol.TransactionOutput( 680 | 100, bitcoin.core.CScript(b'abcd'), b'efgh', 1500, OutputType.transfer) 681 | 682 | self.assertEqual('TransactionOutput(' + 683 | 'value=100, ' + 684 | 'script=CScript([OP_NOP, OP_VER, OP_IF, OP_NOTIF]), ' + 685 | 'asset_id=b\'efgh\', ' + 686 | 'asset_quantity=1500, ' + 687 | 'output_type=)', 688 | str(target)) 689 | -------------------------------------------------------------------------------- /tests/test_transactions.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8; -*- 2 | # 3 | # The MIT License (MIT) 4 | # 5 | # Copyright (c) 2014 Flavien Charlon 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to deal 9 | # in the Software without restriction, including without limitation the rights 10 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | # copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in all 15 | # copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | # SOFTWARE. 24 | 25 | import bitcoin.core 26 | import bitcoin.core.script 27 | import openassets.protocol 28 | import openassets.transactions 29 | import unittest 30 | import unittest.mock 31 | 32 | 33 | class TransactionBuilderTests(unittest.TestCase): 34 | def setUp(self): 35 | self.target = openassets.transactions.TransactionBuilder(10) 36 | 37 | # issue_asset 38 | 39 | def test_issue_asset_success(self): 40 | outputs = self.generate_outputs([ 41 | (20, b'source', b'a1', 50), 42 | (15, b'source', None, 0), 43 | (10, b'source', None, 0) 44 | ]) 45 | 46 | spec = openassets.transactions.TransferParameters(outputs, b'target', b'change', 1000) 47 | result = self.target.issue(spec, b'metadata', 5) 48 | 49 | self.assertEqual(2, len(result.vin)) 50 | self.assert_input(result.vin[0], b'1' * 32, 1, b'source') 51 | self.assert_input(result.vin[1], b'2' * 32, 2, b'source') 52 | self.assertEqual(3, len(result.vout)) 53 | # Asset issued 54 | self.assert_output(result.vout[0], 10, b'target') 55 | # Marker output 56 | self.assert_marker(result.vout[1], [1000], b'metadata') 57 | # Bitcoin change 58 | self.assert_output(result.vout[2], 10, b'change') 59 | 60 | def test_issue_asset_insufficient_funds(self): 61 | outputs = self.generate_outputs([ 62 | (20, b'source', b'a1', 50), 63 | (15, b'source', None, 0), 64 | (5, b'source', None, 0) 65 | ]) 66 | 67 | spec = openassets.transactions.TransferParameters(outputs, b'target', b'change', 1000) 68 | self.assertRaises( 69 | openassets.transactions.InsufficientFundsError, 70 | self.target.issue, spec, b'metadata', 5) 71 | 72 | def test_transfer_bitcoin_with_change(self): 73 | outputs = self.generate_outputs([ 74 | (150, b'source', b'a1', 50), 75 | (150, b'source', None, 0), 76 | (150, b'source', None, 0) 77 | ]) 78 | 79 | spec = openassets.transactions.TransferParameters(outputs, b'target', b'change', 200) 80 | result = self.target.transfer_bitcoin(spec, 10) 81 | 82 | self.assertEqual(2, len(result.vin)) 83 | self.assert_input(result.vin[0], b'1' * 32, 1, b'source') 84 | self.assert_input(result.vin[1], b'2' * 32, 2, b'source') 85 | self.assertEqual(2, len(result.vout)) 86 | # Bitcoin change 87 | self.assert_output(result.vout[0], 90, b'change') 88 | # Bitcoins sent 89 | self.assert_output(result.vout[1], 200, b'target') 90 | 91 | def test_transfer_bitcoin_no_change(self): 92 | outputs = self.generate_outputs([ 93 | (150, b'source', b'a1', 50), 94 | (60, b'source', None, 0), 95 | (150, b'source', None, 0) 96 | ]) 97 | 98 | spec = openassets.transactions.TransferParameters(outputs, b'target', b'change', 200) 99 | result = self.target.transfer_bitcoin(spec, 10) 100 | 101 | self.assertEqual(2, len(result.vin)) 102 | self.assert_input(result.vin[0], b'1' * 32, 1, b'source') 103 | self.assert_input(result.vin[1], b'2' * 32, 2, b'source') 104 | self.assertEqual(1, len(result.vout)) 105 | # Bitcoins sent 106 | self.assert_output(result.vout[0], 200, b'target') 107 | 108 | def test_transfer_bitcoin_dust_limit(self): 109 | outputs = self.generate_outputs([ 110 | (25, b'source', None, 0), 111 | ]) 112 | 113 | spec = openassets.transactions.TransferParameters(outputs, b'target', b'change', 10) 114 | result = self.target.transfer_bitcoin(spec, 5) 115 | 116 | self.assertEqual(1, len(result.vin)) 117 | self.assert_input(result.vin[0], b'0' * 32, 0, b'source') 118 | self.assertEqual(2, len(result.vout)) 119 | # Bitcoin change 120 | self.assert_output(result.vout[0], 10, b'change') 121 | # Bitcoins sent 122 | self.assert_output(result.vout[1], 10, b'target') 123 | 124 | def test_transfer_bitcoin_insufficient_funds(self): 125 | outputs = self.generate_outputs([ 126 | (150, b'source', b'a1', 50), 127 | (60, b'source', None, 0), 128 | (150, b'source', None, 0) 129 | ]) 130 | 131 | spec = openassets.transactions.TransferParameters(outputs, b'target', b'change', 201) 132 | self.assertRaises( 133 | openassets.transactions.InsufficientFundsError, 134 | self.target.transfer_bitcoin, spec, 10) 135 | 136 | def test_transfer_bitcoin_dust_output(self): 137 | outputs = self.generate_outputs([ 138 | (19, b'source', None, 0) 139 | ]) 140 | 141 | spec = openassets.transactions.TransferParameters(outputs, b'target', b'change', 9) 142 | self.assertRaises( 143 | openassets.transactions.DustOutputError, 144 | self.target.transfer_bitcoin, spec, 10) 145 | 146 | def test_transfer_bitcoin_dust_change(self): 147 | outputs = self.generate_outputs([ 148 | (150, b'source', None, 0) 149 | ]) 150 | 151 | spec = openassets.transactions.TransferParameters(outputs, b'target', b'change', 150 - 10 - 9) 152 | self.assertRaises( 153 | openassets.transactions.DustOutputError, 154 | self.target.transfer_bitcoin, spec, 10) 155 | 156 | def test_transfer_assets_with_change(self): 157 | outputs = self.generate_outputs([ 158 | (10, b'source', b'a1', 50), 159 | (80, b'source', None, 0), 160 | (20, b'source', b'a1', 100) 161 | ]) 162 | 163 | spec = openassets.transactions.TransferParameters(outputs, b'target', b'asset_change', 120) 164 | result = self.target.transfer_assets(b'a1', spec, b'bitcoin_change', 40) 165 | 166 | self.assertEqual(3, len(result.vin)) 167 | self.assert_input(result.vin[0], b'0' * 32, 0, b'source') 168 | self.assert_input(result.vin[1], b'2' * 32, 2, b'source') 169 | self.assert_input(result.vin[2], b'1' * 32, 1, b'source') 170 | self.assertEqual(4, len(result.vout)) 171 | # Marker output 172 | self.assert_marker(result.vout[0], [120, 30], b'') 173 | # Asset sent 174 | self.assert_output(result.vout[1], 10, b'target') 175 | # Asset change 176 | self.assert_output(result.vout[2], 10, b'asset_change') 177 | # Bitcoin change 178 | self.assert_output(result.vout[3], 50, b'bitcoin_change') 179 | 180 | def test_transfer_assets_no_change(self): 181 | outputs = self.generate_outputs([ 182 | (10, b'source', b'a1', 50), 183 | (80, b'source', None, 0), 184 | (10, b'source', b'a1', 70) 185 | ]) 186 | 187 | spec = openassets.transactions.TransferParameters(outputs, b'target', b'asset_change', 120) 188 | result = self.target.transfer_assets(b'a1', spec, b'bitcoin_change', 40) 189 | 190 | self.assertEqual(3, len(result.vin)) 191 | self.assert_input(result.vin[0], b'0' * 32, 0, b'source') 192 | self.assert_input(result.vin[1], b'2' * 32, 2, b'source') 193 | self.assert_input(result.vin[2], b'1' * 32, 1, b'source') 194 | self.assertEqual(3, len(result.vout)) 195 | # Marker output 196 | self.assert_marker(result.vout[0], [120], b'') 197 | # Asset sent 198 | self.assert_output(result.vout[1], 10, b'target') 199 | # Bitcoin change 200 | self.assert_output(result.vout[2], 50, b'bitcoin_change') 201 | 202 | def test_transfer_assets_insufficient_asset_quantity(self): 203 | outputs = self.generate_outputs([ 204 | (10, b'source', b'a1', 50), 205 | (80, b'source', None, 0), 206 | (10, b'other', None, 0), 207 | (10, b'source', b'a1', 70) 208 | ]) 209 | 210 | spec = openassets.transactions.TransferParameters(outputs, b'target', b'asset_change', 121) 211 | self.assertRaises( 212 | openassets.transactions.InsufficientAssetQuantityError, 213 | self.target.transfer_assets, b'a1', spec, b'bitcoin_change', 40) 214 | 215 | def test_btc_asset_swap(self): 216 | outputs = self.generate_outputs([ 217 | (90, b'source_btc', None, 0), 218 | (100, b'source_btc', None, 0), 219 | (10, b'source_asset', b'a1', 50), 220 | (10, b'source_asset', b'a1', 100), 221 | ]) 222 | 223 | btc_spec = openassets.transactions.TransferParameters(outputs[0:2], b'source_asset', b'source_btc', 160) 224 | asset_spec = openassets.transactions.TransferParameters(outputs[2:4], b'source_btc', b'source_asset', 120) 225 | result = self.target.btc_asset_swap(btc_spec, b'a1', asset_spec, 10) 226 | 227 | self.assertEqual(4, len(result.vin)) 228 | self.assert_input(result.vin[0], b'2' * 32, 2, b'source_asset') 229 | self.assert_input(result.vin[1], b'3' * 32, 3, b'source_asset') 230 | self.assert_input(result.vin[2], b'0' * 32, 0, b'source_btc') 231 | self.assert_input(result.vin[3], b'1' * 32, 1, b'source_btc') 232 | self.assertEqual(5, len(result.vout)) 233 | # Marker output 234 | self.assert_marker(result.vout[0], [120, 30], b'') 235 | # Asset sent 236 | self.assert_output(result.vout[1], 10, b'source_btc') 237 | # Asset change 238 | self.assert_output(result.vout[2], 10, b'source_asset') 239 | # Bitcoin change 240 | self.assert_output(result.vout[3], 20, b'source_btc') 241 | # Bitcoins sent 242 | self.assert_output(result.vout[4], 160, b'source_asset') 243 | 244 | def test_asset_asset_swap(self): 245 | outputs = self.generate_outputs([ 246 | (10, b'source_1', b'a1', 100), 247 | (10, b'source_1', b'a1', 80), 248 | (80, b'source_1', None, 0), 249 | (10, b'source_2', b'a2', 600), 250 | (100, b'source_2', None, 0), 251 | ]) 252 | 253 | asset1_spec = openassets.transactions.TransferParameters(outputs[0:3], b'source_2', b'source_1', 120) 254 | asset2_spec = openassets.transactions.TransferParameters(outputs[3:4], b'source_1', b'source_2', 260) 255 | result = self.target.asset_asset_swap(b'a1', asset1_spec, b'a2', asset2_spec, 20) 256 | 257 | self.assertEqual(4, len(result.vin)) 258 | self.assert_input(result.vin[0], b'0' * 32, 0, b'source_1') 259 | self.assert_input(result.vin[1], b'1' * 32, 1, b'source_1') 260 | self.assert_input(result.vin[2], b'3' * 32, 3, b'source_2') 261 | self.assert_input(result.vin[3], b'2' * 32, 2, b'source_1') 262 | self.assertEqual(6, len(result.vout)) 263 | # Marker output 264 | self.assert_marker(result.vout[0], [120, 60, 260, 340], b'') 265 | # Asset 1 sent 266 | self.assert_output(result.vout[1], 10, b'source_2') 267 | # Asset 1 change 268 | self.assert_output(result.vout[2], 10, b'source_1') 269 | # Asset 2 sent 270 | self.assert_output(result.vout[3], 10, b'source_1') 271 | # Asset 2 sent 272 | self.assert_output(result.vout[4], 10, b'source_2') 273 | # Bitcoin change 274 | self.assert_output(result.vout[5], 50, b'source_1') 275 | 276 | # Test helpers 277 | 278 | def generate_outputs(self, definitions): 279 | result = [] 280 | # Each definition has the following format: 281 | # (value, output_script, asset_id, asset_quantity) 282 | for i, item in enumerate(definitions): 283 | byte = bytes(str(i), encoding='UTF-8') 284 | result.append(openassets.transactions.SpendableOutput( 285 | out_point=bitcoin.core.COutPoint(byte * 32, i), 286 | output=openassets.protocol.TransactionOutput( 287 | item[0], bitcoin.core.script.CScript(item[1]), item[2], item[3]) 288 | )) 289 | 290 | return result 291 | 292 | def assert_input(self, input, transaction_hash, output_index, script): 293 | self.assertEqual(transaction_hash, input.prevout.hash) 294 | self.assertEqual(output_index, input.prevout.n) 295 | self.assertEqual(script, bytes(input.scriptSig)) 296 | 297 | def assert_output(self, output, nValue, scriptPubKey): 298 | self.assertEqual(nValue, output.nValue) 299 | self.assertEqual(scriptPubKey, bytes(output.scriptPubKey)) 300 | 301 | def assert_marker(self, output, asset_quantities, metadata): 302 | payload = openassets.protocol.MarkerOutput.parse_script(output.scriptPubKey) 303 | marker_output = openassets.protocol.MarkerOutput.deserialize_payload(payload) 304 | 305 | self.assertEqual(0, output.nValue) 306 | self.assertEqual(asset_quantities, marker_output.asset_quantities) 307 | self.assertEqual(metadata, marker_output.metadata) 308 | 309 | 310 | class SpendableOutputTests(unittest.TestCase): 311 | def test_init_success(self): 312 | target = openassets.transactions.SpendableOutput( 313 | bitcoin.core.COutPoint('\x01' * 32), 314 | openassets.protocol.TransactionOutput(100)) 315 | 316 | self.assertEqual('\x01' * 32, target.out_point.hash) 317 | self.assertEqual(100, target.output.value) 318 | --------------------------------------------------------------------------------