From b3b25b1f0e0b1473c481a76c7ae46c59e65375a0 Mon Sep 17 00:00:00 2001 From: Samer Afach Date: Thu, 14 Oct 2021 09:23:44 +0200 Subject: [PATCH 1/3] Fix tests --- test/functional/custom-types.json | 165 +++++++++++------- test/functional/example_test.py | 34 ++-- test/functional/feature_alice_bob_test.py | 18 +- .../test_framework/mintlayer/utxo.py | 84 +++++---- 4 files changed, 179 insertions(+), 122 deletions(-) diff --git a/test/functional/custom-types.json b/test/functional/custom-types.json index 170254c..566a5cc 100644 --- a/test/functional/custom-types.json +++ b/test/functional/custom-types.json @@ -1,67 +1,102 @@ { - "runtime_id": 2, - "types": { - "DestinationCreatePP": { - "type": "struct", - "type_mapping": [ - [ "code", "Vec" ], - [ "data", "Vec" ] - ] - }, - "DestinationCallPP": { - "type": "struct", - "type_mapping": [ - [ "dest_account", "AccountId" ], - [ "input_data", "Vec" ] - ] - }, - "Destination": { - "type": "enum", - "type_mapping": [ - [ "Pubkey", "H256" ], - [ "CreatePP", "DestinationCreatePP" ], - [ "CallPP", "DestinationCallPP" ], - [ "ScriptHash", "H256" ], - [ "PubkeyHash", "Vec" ] - ] - }, - "TransactionInput": { - "type": "struct", - "type_mapping": [ - [ "outpoint", "H256" ], - [ "lock", "Vec" ], - [ "witness", "Vec" ] - ] - }, - "TransactionOutput": { - "type": "struct", - "type_mapping": [ - [ "value", "Value" ], - [ "header", "TXOutputHeader"], - [ "destination", "Destination" ] - ] - }, - "Transaction": { - "type": "struct", - "type_mapping": [ - [ "inputs", "Vec" ], - [ "outputs", "Vec>" ] - ] - }, - "Outpoint": { - "type": "struct", - "type_mapping": [ - [ "transaction", "Transaction" ], - [ "index", "u64" ] - ] - }, - "TransactionOutputFor": "TransactionOutput", - "TransactionFor": "Transaction", - "Address": "MultiAddress", - "Value": "u128", - "TXOutputHeader": "u128", - "header": "TXOutputHeader", - "Public": "H256" - }, - "versioning": [ ] + "runtime_id": 2, + "types": { + "Value": "u128", + "DestinationCreatePP": { + "type": "struct", + "type_mapping": [ + [ "code", "Vec" ], + [ "data", "Vec" ] + ] + }, + "DestinationCallPP": { + "type": "struct", + "type_mapping": [ + [ "dest_account", "AccountId" ], + [ "input_data", "Vec" ] + ] + }, + "Destination": { + "type": "enum", + "type_mapping": [ + [ "Pubkey", "Pubkey" ], + [ "CreatePP", "DestinationCreatePP" ], + [ "CallPP", "DestinationCallPP" ] + ] + }, + "TransactionInput": { + "type": "struct", + "type_mapping": [ + [ "outpoint", "Hash" ], + [ "lock", "Vec" ], + [ "witness", "Vec" ] + ] + }, + "TransactionOutput": { + "type": "struct", + "type_mapping": [ + [ "value", "Value" ], + [ "header", "TXOutputHeader"], + [ "destination", "Destination" ] + ] + }, + "Transaction": { + "type": "struct", + "type_mapping": [ + [ "inputs", "Vec" ], + [ "outputs", "Vec" ] + ] + }, + "Outpoint": { + "type": "struct", + "type_mapping": [ + [ "transaction", "Transaction" ], + [ "index", "u64" ] + ] + }, + "TransactionOutputFor": "TransactionOutput", + "TransactionFor": "Transaction", + "Address": "MultiAddress", + "LookupSource": "MultiAddress", + "Value": "u128", + "TXOutputHeader": "u128", + "value": "Value", + "pub_key": "H256", + "header": "TXOutputHeader", + "Difficulty": "U256", + "DifficultyAndTimestamp": { + "type": "struct", + "type_mapping": [ + ["difficulty", "Difficulty"], + ["timestamp", "Moment"] + ] + }, + "Pubkey": "H256", + "SignatureData": { + "type": "struct", + "type_mapping": [ + [ "sighash", "u8" ], + [ "inputs", "SignatureDataInputs" ], + [ "outputs", "SignatureDataOutputs" ], + [ "codesep_pos", "u32" ] + ] + }, + "SignatureDataInputs": { + "type": "enum", + "type_mapping": [ + [ "SpecifiedPay", "(H256, H256, u64)" ], + [ "AnyoneCanPay", "(H256, H256)" ] + ] + }, + "SignatureDataOutputs": { + "type": "enum", + "type_mapping": [ + [ "Unused", "()" ], + [ "All", "H256" ], + [ "None", "()" ], + [ "Single", "H256" ] + ] + } + }, + "versioning": [ ] } diff --git a/test/functional/example_test.py b/test/functional/example_test.py index 1323fea..6f390c0 100755 --- a/test/functional/example_test.py +++ b/test/functional/example_test.py @@ -97,16 +97,17 @@ def custom_method(self): def run_test(self): """Main test logic""" - alice = Keypair.create_from_uri('//Alice') + client = self.nodes[0].rpc_client - available_utxos = self.nodes[0].rpc_client.utxos_for(alice) + alice = Keypair.create_from_uri('//Alice') - utxos = [h for (h, o) in available_utxos if o.value >= 150] + # Find an utxo with enough funds + utxos = [u for u in client.utxos_for(alice) if u[1].value >= 150] tx1 = utxo.Transaction( - self.nodes[0].rpc_client, + client, inputs=[ - utxo.Input(utxos[0]), + utxo.Input(utxos[0][0]), ], outputs=[ utxo.Output( @@ -121,13 +122,24 @@ def run_test(self): ), ] - ).sign(alice) - extrinsic = self.nodes[0].rpc_client.submit(alice, tx1) - self.log.info("extrinsic submitted... '{}'".format(extrinsic)) + ).sign(alice, [utxos[0][1]]) + client.submit(alice, tx1) - receipt = self.nodes[0].rpc_client.get_receipt(extrinsic, True) - self.log.info("Extrinsic '{}' sent and included in block '{}'".format(receipt[0], receipt[1])) - assert_equal(receipt[0].replace("0x", ""), extrinsic.extrinsic_hash.replace("0x", "")) + tx2 = utxo.Transaction( + client, + inputs=[ + # spend the 100 utxo output (index 1) + utxo.Input(tx1.outpoint(1)), + ], + outputs=[ + utxo.Output( + value=60, + header=0, + destination=utxo.DestPubkey(alice.public_key) + ), + ] + ).sign(alice, [tx1.outputs[1]]) + client.submit(alice, tx2) if __name__ == '__main__': diff --git a/test/functional/feature_alice_bob_test.py b/test/functional/feature_alice_bob_test.py index bef5581..21d58ac 100755 --- a/test/functional/feature_alice_bob_test.py +++ b/test/functional/feature_alice_bob_test.py @@ -62,12 +62,12 @@ def run_test(self): bob = Keypair.create_from_uri('//Bob') # fetch the genesis utxo from storage - utxos = [h for (h, o) in client.utxos_for(alice)] + utxos = list(client.utxos_for(alice)) - tx = utxo.Transaction( + tx1 = utxo.Transaction( client, inputs=[ - utxo.Input(utxos[0]), + utxo.Input(utxos[0][0]), ], outputs=[ utxo.Output( @@ -76,13 +76,13 @@ def run_test(self): destination=utxo.DestPubkey(bob.public_key) ), ] - ).sign(alice) - client.submit(alice, tx) + ).sign(alice, [utxos[0][1]]) + client.submit(alice, tx1) - tx = utxo.Transaction( + tx2 = utxo.Transaction( client, inputs=[ - utxo.Input(tx.outpoint(0)), + utxo.Input(tx1.outpoint(0)), ], outputs=[ utxo.Output( @@ -96,8 +96,8 @@ def run_test(self): destination=utxo.DestPubkey(bob.public_key) ), ] - ).sign(bob) - client.submit(bob, tx) + ).sign(bob, tx1.outputs) + client.submit(bob, tx2) if __name__ == '__main__': diff --git a/test/functional/test_framework/mintlayer/utxo.py b/test/functional/test_framework/mintlayer/utxo.py index 3e7003d..39b49fb 100644 --- a/test/functional/test_framework/mintlayer/utxo.py +++ b/test/functional/test_framework/mintlayer/utxo.py @@ -5,8 +5,6 @@ import os """ Client. A thin wrapper over SubstrateInterface """ - - class Client: def __init__(self, url="ws://127.0.0.1", port=9944): source_dir = os.path.dirname(os.path.abspath(__file__)) @@ -20,13 +18,20 @@ def __init__(self, url="ws://127.0.0.1", port=9944): type_registry=custom_type_registry ) - """ SCALE-encode given object """ + """ SCALE-encode given object in JSON format """ + def encode_obj(self, ty, obj): + return self.substrate.encode_scale(ty, obj) + """ SCALE-encode given object """ def encode(self, obj): - return self.substrate.encode_scale(obj.type_string(), obj.json()) + return self.encode_obj(obj.type_string(), obj.json()) - """ Query the node for the list of utxos """ + """ Hash of a SCALE-encoded version of given JSON object """ + def hash_of(self, ty, obj): + encoded = self.encode_obj(ty, obj).data + return '0x' + str(substrateinterface.utils.hasher.blake2_256(encoded)) + """ Query the node for the list of utxos """ def utxos(self): query = self.substrate.query_map( module="Utxo", @@ -37,28 +42,28 @@ def utxos(self): return ((h, Output.load(o.value)) for (h, o) in query) """ Get UTXOs for given key """ - def utxos_for(self, keypair): matching = lambda e: e[1].destination.get_pubkey() == keypair.public_key return filter(matching, self.utxos()) """ Submit a transaction onto the blockchain """ - def submit(self, keypair, tx): call = self.substrate.compose_call( - call_module='Utxo', - call_function='spend', - call_params={'tx': tx.json()}, + call_module = 'Utxo', + call_function = 'spend', + call_params = { 'tx': tx.json() }, ) extrinsic = self.substrate.create_signed_extrinsic(call=call, keypair=keypair) - return extrinsic - - def get_receipt(self, extrinsic, wait_for_inclusion): - receipt = self.substrate.submit_extrinsic(extrinsic, wait_for_inclusion=wait_for_inclusion) - return receipt.extrinsic_hash, receipt.block_hash + print("extrinsic submitted...") + try: + receipt = self.substrate.submit_extrinsic(extrinsic, wait_for_inclusion=True) + print("Extrinsic '{}' sent and included in block '{}'".format(receipt.extrinsic_hash, receipt.block_hash)) + return (receipt.extrinsic_hash, receipt.block_hash) + except SubstrateRequestException as e: + print("Failed to send: {}".format(e)) -class Destination: +class Destination(): @staticmethod def load(obj): if 'Pubkey' in obj: @@ -75,7 +80,7 @@ def type_string(self): def get_pubkey(self): return None - +# Only Schnorr pubkey type supported now. class DestPubkey(Destination): def __init__(self, pubkey): self.pubkey = pubkey @@ -85,12 +90,11 @@ def load(obj): return DestPubkey(obj) def json(self): - return {'Pubkey': self.pubkey} + return { 'Pubkey': self.pubkey } def get_pubkey(self): return self.pubkey - class DestCreatePP(Destination): def __init__(self, code, data): if type(code) == str: @@ -104,8 +108,7 @@ def load(obj): return DestCreatePP(obj['code'], obj['data']) def json(self): - return {'CreatePP': {'code': self.code, 'data': self.data}} - + return { 'CreatePP': { 'code': self.code, 'data': self.data } } class DestCallPP(Destination): def __init__(self, dest_account, input_data): @@ -117,8 +120,7 @@ def load(obj): return DestCallPP(obj['dest_account'], obj['input_data']) def json(self): - return {'CallPP': {'dest_account': self.acct, 'input_data': self.data}} - + return { 'CallPP': { 'dest_account': self.acct, 'input_data': self.data } } class Output(): def __init__(self, value, header, destination): @@ -143,7 +145,7 @@ def json(self): class Input(): - def __init__(self, outpoint, lock='0x', witness='0x'): + def __init__(self, outpoint, lock = '0x', witness = '0x'): self.outpoint = outpoint self.lock = lock self.witness = witness @@ -158,7 +160,6 @@ def json(self): 'witness': self.witness, } - class Transaction(): def __init__(self, client, inputs, outputs): self.client = client @@ -170,29 +171,38 @@ def type_string(self): def json(self): return { - 'inputs': [i.json() for i in self.inputs], - 'outputs': [o.json() for o in self.outputs], + 'inputs': [ i.json() for i in self.inputs ], + 'outputs': [ o.json() for o in self.outputs ], } """ Get data to be signed for this transaction """ - - def signature_data(self): - # Create another transaction with no witness fields in inputs. - inputs = [Input(i.outpoint, i.lock) for i in self.inputs] - tx = Transaction(self.client, inputs, self.outputs) - return self.client.encode(tx) + def signature_data(self, spent_utxos, idx): + # Create the signature message. Only the default sighash supported for now. + utxos_hash = self.client.hash_of('Vec', + [ u.json() for u in spent_utxos ]) + outpoints_hash = self.client.hash_of('Vec', + [ str(i.outpoint) for i in self.inputs ]) + outputs_hash = self.client.hash_of('Vec', + [ o.json() for o in self.outputs ]) + + sigdata = { + 'sighash': 0, + 'inputs': { 'SpecifiedPay': (outpoints_hash, utxos_hash, idx) }, + 'outputs': { 'All': outputs_hash }, + 'codesep_pos': 0xffffffff + } + return self.client.encode_obj('SignatureData', sigdata) """ Sigh the transaction inputs listed in input_idxs (all if missing) """ - - def sign(self, keypair, input_idxs=None): + def sign(self, keypair, spent_utxos, input_idxs = None): + assert len(self.inputs) == len(spent_utxos), "1 utxo per input required" input_idxs = input_idxs or range(len(self.inputs)) - signature = keypair.sign(self.signature_data()) for idx in input_idxs: + signature = keypair.sign(self.signature_data(spent_utxos, idx)) self.inputs[idx].witness = signature return self """ Get UTXO ID of n-th output of this transaction """ - def outpoint(self, n): outpt = { 'transaction': self.json(), From e788f691848533b6bbcb178984c3a8c8b301de74 Mon Sep 17 00:00:00 2001 From: sinitcin Date: Thu, 14 Oct 2021 17:33:54 +0300 Subject: [PATCH 2/3] DO NOT MERGE Signed-off-by: sinitcin --- README_eng.md | 299 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 299 insertions(+) create mode 100644 README_eng.md diff --git a/README_eng.md b/README_eng.md new file mode 100644 index 0000000..82eca37 --- /dev/null +++ b/README_eng.md @@ -0,0 +1,299 @@ +# Description + +We cannot sign transactions on the node side, all transactions must be signed on the client side. In the future, there will be changes to the structures of transactions and we will be required to track them. The ability to input any input data should be covered by tests. + +## Enum TransactionOutput.data + +Each transaction output must carry a `data` field. This field is the designation of the purpose of the transaction, we must highlight the following at the moment: + +* Transfer Tokens or NFT +* Token issueance +* Burning token +* NFT creation + +This field must be `Enum`. All of the `TransactionOutput` structure and related types ​​must be derived `Encode` and `Decode`. This will allow SCALE Codec to correctly decode data fields. That there is no confusion due to changes in data places, we must mark each element of the type with the index `#[codec(index = NUM)]`. + +And there is a requirement for testing, each version must be covered with tests that are guaranteed to break if someone changes the internal data in any version. + +```rust + #[derive(Encode, Decode...)] + pub struct TransactionOutput { + #[codec(index = 1)] + pub(crate) value: Value, + #[codec(index = 2)] + pub(crate) destination: Destination, + #[codec(index = 3)] + pub(crate) data: Option, + } + + #[derive(Encode, Decode...)] + pub enum TxData { + // TransferToken data to another user. If it is a token, then the token data must also be transferred to the recipient. + #[codec(index = 1)] + TransferTokenV1(..), + // A new token creation + #[codec(index = 2)] + TokenIssuanceV1(..), + // Burning a token or NFT + #[codec(index = 3)] + TokenBurnV1(..), + // A new NFT creation + #[codec(index = 4)] + NftMintV1(..), + ... + // We do not change the data in TransferTokenV1, we just create a new version + #[codec(index = 5)] + TransferTokenV2(..), + } +``` + +## Detailed description of Enum TxData fields + +### TransferTokenV1 +--- + +Transfer funds to another person in a given UTXO. To send MLT we will use `TokenID :: MLT`, which is actually zero. If the `token_id` is equal to the ID of the MLS-01 token, then the amount of token is transferred to the recipient. The commission is taken only in MLT. If the `token_id` is equal to the id of any NFT, then the data of this NFT should be transferred to the recipient without changing the creator field. The UTXO model itself allows you to determine the owner of the NFT. + +```rust + pub enum TxData { + TransferTokenV1{ + token_id: TokenID, + amount: Value, + } + ... + } +``` + +### TokenIssuanceV1 +--- + +When issuing a new token, we specify the data for creating a new token in the transaction input, where the `token_id` is: + +```rust + let token_id = hash(&(inputs[0].hash, inputs[0].index)); +``` + +`token_ticker` - might be not unique. Should be limited to 10 chars. In fact, it's an ASCII string. + +```rust + pub enum TxData { + TokenIssuanceV1{ + token_id: TokenID, + token_ticker: Vec, + amount_to_issue: Value, + // Should be not more than 18 numbers + number_of_decimals: u8, + metadata_URI: Vec, + } + ... + } +``` +See the `metada_URI` format below. + +### TokenBurnV1 +--- + +Burning a token, as an input is used by UTXO containing tokens, and as an output of BurnV1, in this case, you can burn any number of tokens. After this operation, you can use UTXO for the remaining amount of tokens. + +```rust + type String = Vec; + pub enum TxData { + BurnV1{ + token_id: TokenID, + amount_to_burn: Value, + } + ... + } +``` + +### NftMintV1 +--- +When minting a new NFT token, we specify the data for creating a new token in the transaction input, where the `token_id` is: + +```rust + let token_id = hash(&(inputs[0].hash, inputs[0].index)); +``` + +For the seek a creation UTXO, we should make a new Storage where: +* Key - token_id +* Value - hash of UTXO + +It allows us to find the information about the `creator`, and it won't be changed. It is suitable for the MLS-01 tokens too. + +The `data_hash` field is a hash of external data, which should be taken from the digital data for which the NFT is being created. This field should also not be changed when sent to a new owner. + +The `metadata_URI` field can contain the name of the asset and its description, as well as an image with its data for preview. + +It is also possible to add different types of hashes and owners. + +```rust + #[derive(Encode, Decode, ...)] + pub enum TxData { + MintV1{ + token_id: TokenID, + data_hash: NftDataHash, + metadata_URI: Vec, + } + ... + } + + #[derive(Encode, Decode, ...)] + pub enum NftDataHash { + #[codec(index = 1)] + Hash32([u8; 32]), + #[codec(index = 2)] + Raw(Vec), + // Or any type that you want to implement + } +``` + +### Error Handling + +We should use `"chain-error"` feature for the SCALE Codec. It allows us to get a more detailed description of errors. + +```rust +[dependencies.codec] +default-features = false +features = ["derive", "chain-error"] +``` + +However, this kind of error might show only place in data that didn't decode or encoded correctly. Example: + +```bash +"Could not decode `TransactionOutputV1::data`:\n\tCould not decode `TxDataV1`, variant doesn't exist\n" +``` +Anyway, the correctness of decoded data we should check additionally. + +### Adding a new version + +Adding a new version of data is in fact adding a new field to the enum, if the names match, add the version number at the end, for example: + +* TransferTokenV1 +* TransferTokenV2 +* etc + +The order of the fields is not important, but each field must be marked with a unique codec index - `# [codec (index = your index)]`. Example: + +```rust +#[derive(Encode, Decode, ...)] +pub enum TxDataV2 { + #[codec(index = 2)] + NftMintV2 { + id: u64, + token_name: Vec, + // other fields that you wish + }, + #[codec(index = 1)] + NftMintV1 { id: u64 }, +} +``` + +You also need to add an appropriate test to track changes. + +Example: [check_immutability test](https://github.com/sinitcin/scale_test/blob/b95a19708c3f65a0b9499fcd19f1e081a843cc4a/src/main.rs#L124) + +This test will compare against the structure template and if someone accidentally changes the data fields, the test will indicate this. + +### What happens if the old version of the node reads the new transaction format? + +The transaction can not be processing. Prove: [an_old_node_read_a_new_data test](https://github.com/sinitcin/scale_test/blob/b95a19708c3f65a0b9499fcd19f1e081a843cc4a/src/main.rs#L93) + +### What happens if the new version of the node reads the old transaction format? + +Transaction data will be correctly read and, depending on the blockchain logic, interpreted or discarded. Example: + +```rust +match data { + TransferTokenV1(...) => pallet::transfer_v1(...), + TransferTokenV2(...) => pallet::transfer_v2(...), +} +``` + +Prove: [a_new_node_read_an_old_data test](https://github.com/sinitcin/scale_test/blob/b95a19708c3f65a0b9499fcd19f1e081a843cc4a/src/main.rs#L109) + +### Format of data located by reference `metadata_URI` + +This is a link to a third-party server that will contain a json format similar to “ERC721 Metadata JSON Schema”: + +```json +{ + "title": "Asset Metadata", + "properties": { + "name": { + "type": "string", + "description": "Identifies the asset to which this token represents" + }, + "description": { + "type": "string", + "description": "Describes the asset to which this token represents" + }, + "image": { + "type": "string", + "description": "A URI pointing to a resource with mime type image/* representing the asset to which this token represents. Consider making any images at a width between 320 and 1080 pixels and aspect ratio between 1.91:1 and 4:5 inclusive." + } + } +} +``` + +This file will be used on blockchain explorers. + +### Unit testing + +Over here is suggested about test plan for tokens and data field: +* All tests must apply to all possible versions of the data field. +* Also, tests should be carried out in the mode of one input - one output, as well as multiple inputs - multiple outputs. +* Also the tests below should be applied to cases without tokens, for MLS-01, as well as for NFT. + +**General checks to be repeated for each type of token:** + + 1. **Testing token creation**: + * Creation a token with a pre-existing ID or re-creation of an already created token. + * The action could not be completed. The error must be handled correctly. + + * Creating a token with corrupted data + * Data field of zero length + * The data field of the maximum allowed length filled with random garbage + * Creation of a token with 0 issue amount + * Generating a token with a long URI string + + * Creation of a token without input with MLT to pay commission + * Test tx where Input with token and without MLT, output has token (without MLT) + * Test tx where Input with token and without MLT, output has MLT (without token) + * Test tx where Input without token but with MLT, output has MLT and token + * Test tx where no inputs for token + * Test where less MLT at the input than you need to pay the commission + * Test tx where Input and output have a token but with zero value + + 2. **Testing token transfer** + * Standard creation of a token and sending it to a chain of persons, and from them collecting the token into one UTXO and checking that the token data has not changed, has not been burned or lost in any way. + * All data must be correct for this test. + * The token must be sent through multiple account groups. + * The total amount of the token must be equal to the created one. + * Incorrect amount of token in one input and one output + * The input contains the correct amount of token, but the output is incorrect + * In the input, the number is incorrect, but the output is correct + * Entry and exit with incorrect number of tokens + * Testing UTXO for token and return funds change + * Use in one MLT input to pay the commission and transfer the token at the same time + * Check possibility to cause overflow. For example, let's take a few inputs where value is a maximum possible number, and one input should have the sum of these inputs values. + + 3. **Testing the compatibility of the old version with the new one** + * Testing the compatibility of the new version with the old new + * Testing data encoding in a loop + * Testing the processing of junk and random data + * Testing the processing of fields that are written in a different order + * Testing the immutability of old versions + + 4. **Testing burning tokens** + * Trying to burn none-existing token + * Trying to burn more token value than exist in inputs + * Trying to burn MLT + * Trying to burn MLS-01 + * Trying to burn NFT + * Trying to burn token without inputs for that + * Trying to burn existing token, but which is not in the input + +**What we shall test additionally?** +1. I can't make any limits on data fields sizes through SCALE. I'm pretty sure that Substrate checks size limits for the whole transactions because I take from framework already decoded structures. I can't see raw bytes. But I can't prove it without testing. +2. All maths with `u128`/`i128` should be double-checked and tested to prevent overflow and underflow. +3. Functional tests - will be planned later. \ No newline at end of file From 26e924f541d1c350d82015a1af7dd8cbc4a7845b Mon Sep 17 00:00:00 2001 From: sinitcin Date: Fri, 15 Oct 2021 21:13:52 +0300 Subject: [PATCH 3/3] Fixed TransactionOutput.data arms names. Misprints. And intro in NFT part Signed-off-by: sinitcin --- README_eng.md | 35 +++++++++++++++++------------------ 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/README_eng.md b/README_eng.md index 82eca37..1d9565d 100644 --- a/README_eng.md +++ b/README_eng.md @@ -7,7 +7,7 @@ We cannot sign transactions on the node side, all transactions must be signed on Each transaction output must carry a `data` field. This field is the designation of the purpose of the transaction, we must highlight the following at the moment: * Transfer Tokens or NFT -* Token issueance +* Token Issuance * Burning token * NFT creation @@ -28,9 +28,9 @@ And there is a requirement for testing, each version must be covered with tests #[derive(Encode, Decode...)] pub enum TxData { - // TransferToken data to another user. If it is a token, then the token data must also be transferred to the recipient. + // TokenTransfer data to another user. If it is a token, then the token data must also be transferred to the recipient. #[codec(index = 1)] - TransferTokenV1(..), + TokenTransferV1(..), // A new token creation #[codec(index = 2)] TokenIssuanceV1(..), @@ -41,22 +41,22 @@ And there is a requirement for testing, each version must be covered with tests #[codec(index = 4)] NftMintV1(..), ... - // We do not change the data in TransferTokenV1, we just create a new version + // We do not change the data in TokenTransferV1, we just create a new version #[codec(index = 5)] - TransferTokenV2(..), + TokenTransferV2(..), } ``` ## Detailed description of Enum TxData fields -### TransferTokenV1 +### TokenTransferV1 --- Transfer funds to another person in a given UTXO. To send MLT we will use `TokenID :: MLT`, which is actually zero. If the `token_id` is equal to the ID of the MLS-01 token, then the amount of token is transferred to the recipient. The commission is taken only in MLT. If the `token_id` is equal to the id of any NFT, then the data of this NFT should be transferred to the recipient without changing the creator field. The UTXO model itself allows you to determine the owner of the NFT. ```rust pub enum TxData { - TransferTokenV1{ + TokenTransferV1{ token_id: TokenID, amount: Value, } @@ -70,10 +70,10 @@ Transfer funds to another person in a given UTXO. To send MLT we will use `Token When issuing a new token, we specify the data for creating a new token in the transaction input, where the `token_id` is: ```rust - let token_id = hash(&(inputs[0].hash, inputs[0].index)); + let token_id = BlakeTwo256::hash_of(&(&tx.inputs[0], &tx.inputs[0].index)); ``` -`token_ticker` - might be not unique. Should be limited to 10 chars. In fact, it's an ASCII string. +`token_ticker` - might be not unique. Should be limited to 5 chars. In fact, it's an ASCII string. ```rust pub enum TxData { @@ -93,12 +93,11 @@ See the `metada_URI` format below. ### TokenBurnV1 --- -Burning a token, as an input is used by UTXO containing tokens, and as an output of BurnV1, in this case, you can burn any number of tokens. After this operation, you can use UTXO for the remaining amount of tokens. - +A token burning - as an input is used by UTXO that containing tokens. As an output, the data field should contain the TokenBurnV1 arm. If the amount in burning the output is less than in the input then should exist at least one output for returning the funds change. In this case, you can burn any existing number of tokens. After this operation, you can use UTXO for the remaining amount of tokens. ```rust type String = Vec; pub enum TxData { - BurnV1{ + TokenBurnV1{ token_id: TokenID, amount_to_burn: Value, } @@ -111,14 +110,14 @@ Burning a token, as an input is used by UTXO containing tokens, and as an output When minting a new NFT token, we specify the data for creating a new token in the transaction input, where the `token_id` is: ```rust - let token_id = hash(&(inputs[0].hash, inputs[0].index)); + let token_id = BlakeTwo256::hash_of(&(&tx.inputs[0], &tx.inputs[0].index)); ``` For the seek a creation UTXO, we should make a new Storage where: * Key - token_id * Value - hash of UTXO -It allows us to find the information about the `creator`, and it won't be changed. It is suitable for the MLS-01 tokens too. +It allows us to find the whole information about the NFT including `creator`, and it won't be changed. It is suitable for the MLS-01 tokens too. The `data_hash` field is a hash of external data, which should be taken from the digital data for which the NFT is being created. This field should also not be changed when sent to a new owner. @@ -168,8 +167,8 @@ Anyway, the correctness of decoded data we should check additionally. Adding a new version of data is in fact adding a new field to the enum, if the names match, add the version number at the end, for example: -* TransferTokenV1 -* TransferTokenV2 +* TokenTransferV1 +* TokenTransferV2 * etc The order of the fields is not important, but each field must be marked with a unique codec index - `# [codec (index = your index)]`. Example: @@ -204,8 +203,8 @@ Transaction data will be correctly read and, depending on the blockchain logic, ```rust match data { - TransferTokenV1(...) => pallet::transfer_v1(...), - TransferTokenV2(...) => pallet::transfer_v2(...), + TokenTransferV1(...) => pallet::transfer_v1(...), + TokenTransferV2(...) => pallet::transfer_v2(...), } ```