08 Aug Updating Symbol account metadata in Python
This guide is translated from the original Qiita article written by nem_takanobu aka @xembook
This article follows on from the previous Python post “Registering Symbol account metadata in Python” please read this first or what I will write here won’t make sense! 😊 Both articles are based on xembook’s Qiita article but I have split this into two posts (this being 2/2). The reason that I did this was as being both a Python and cryptography noob, I needed to get my head around what the code was doing and try to explain in a bit more detail to you guys.
Apologies if I have made mistakes in my interpretation or explanation of the code – please let me know and I will update the articles!
Getting started
As mentioned previously, I will assume that you have worked through the previous article and have successfully managed to assign metadata to another account. I will include the code from the last article below and also in the Jupyter Notebook but it is the last part of this post will deal with updating the account metadata that we have previously assigned.
Assigning account metadata (from the previous example)
import sha3
import json
import http.client
import datetime
from binascii import hexlify
from binascii import unhexlify
from symbolchain.core.CryptoTypes import PrivateKey
from symbolchain.core.sym.KeyPair import KeyPair
from symbolchain.core.facade.SymFacade import SymFacade
from symbolchain.core.CryptoTypes import PublicKey
from symbolchain.core.sym.IdGenerator import generate_namespace_id
from symbolchain.core.sym.MerkleHashBuilder import MerkleHashBuilder
from symbolchain.core.CryptoTypes import Hash256
This block contains the code we ran previously to assign a metadata key:value pair to Bob’s account.
hasher = sha3.sha3_256()
hasher.update('certificate'.encode('utf8'))
digest = hasher.digest()
metadataKey = int.from_bytes(digest[0:8], 'little')
metadataValue = 'aaa'
Here was the transaction we sent to assign the account metadata.
facade = SymFacade('public_test')
# Enter you own private key for Alice's account here
b = unhexlify("6BF3866928991FA3D918E64A6F057A2A9441989EBF6A6C39133A9180********")
alicePrikey = PrivateKey(b)
aliceKeypair = KeyPair(alicePrikey)
alicePubkey = aliceKeypair.public_key
aliceAddress = facade.network.public_key_to_address(alicePubkey)
str(aliceAddress)
# Create a new random account for Bob
bobPrikey = PrivateKey.random()
bobKeypair = SymFacade.KeyPair(bobPrikey)
print(str(facade.network.public_key_to_address(bobKeypair.public_key)))
print(str(bobKeypair.public_key))
strBobPubkey = str(bobKeypair.public_key)
strBobAddress = str(facade.network.public_key_to_address(bobKeypair.public_key))
bobPubkey = PublicKey(unhexlify(strBobPubkey))
bobAddress = SymFacade.Address(strBobAddress)
# Set up the transaction of type 'accountMetadata' and enter your metadata key, key length and value
aliceTx = facade.transaction_factory.create_embedded({
'type': 'accountMetadata',
'signer_public_key': alicePubkey,
'target_address': bobAddress,
'scoped_metadata_key': metadataKey,
'value_size_delta': len(metadataValue),
'value': metadataValue
})
hash_builder = MerkleHashBuilder()
hash_builder.update(Hash256(sha3.sha3_256(aliceTx.serialize()).digest()))
merkle_hash = hash_builder.final()
deadline = (int((datetime.datetime.today() + datetime.timedelta(hours=2)).timestamp()) - 1616694977) * 1000
aggregate = facade.transaction_factory.create({
'type': 'aggregateComplete',
'signer_public_key': alicePubkey,
'fee': 1000000,
'deadline': deadline,
'transactions_hash': merkle_hash,
'transactions': [aliceTx]
})
signature = facade.sign_transaction(aliceKeypair, aggregate)
aggregate.signature = signature.bytes
hash = facade.hash_transaction(aggregate).bytes
hexlifiedHash = hexlify(hash)
print(hexlifiedHash)
# Bob cosigns
hexlifiedSignedHash = str(bobKeypair.sign(unhexlify(hexlifiedHash)))
cosignature = (0, bobPubkey.bytes, unhexlify(hexlifiedSignedHash))
aggregate.cosignatures.append(cosignature)
payload = {"payload": hexlify(aggregate.serialize()).decode('utf8').upper()}
strJson = json.dumps(payload)
headers = {'Content-type': 'application/json'}
conn = http.client.HTTPConnection("sym-test-01.opening-line.jp",3000)
conn.request("PUT", "/transactions", strJson,headers)
response = conn.getresponse()
print(response.status, response.reason)
hash = facade.hash_transaction(aggregate)
print('https://sym-test-01.opening-line.jp:3001/transactionStatus/' + str(hash))
In this example Bob’s account address is TDLC7F-74SVUQ-MUQPVY-4EHDQE-PEMKV2-2RHK2H-S6A
.
Modifying account metadata
First we need to set a new metadata value that we want to assign to the account. We previously set a value of ‘aaa’ and here we want to update it to ‘bbb’. This seems simple but as I found out we don’t just update the string, we pass the XOR of the unicode values of each character in the old string to each character in the new string. Using the XOR value from the transaction will allow the metadata value to be updated and according to xembook’s article it also enables traceability. This means that we query the blockchain and see the history of how the account metadata value was changed over time.
Here we set a new string ‘bbb’ which will be the value that we want to update the account metadata to. Then we compute the XOR value of the binary representation of each character in ‘aaa’ with ‘bbb’.
newMetadataValue = 'bbb'
xorValue = "".join([chr(ord(data) ^ ord(code))
for (data, code) in zip(metadataValue, newMetadataValue)]).encode().hex()
Note: this code confused me so I simplified and worked through it myself. I have included a section at the end of this article trying to explain what is happening! 😁
Sending and signing the transaction
We are pretty familiar with the code below now so I won’t explain everything in detail (you can find these explanations in previous articles). What I will mention is that in aliceTx
we just need to specify the key for the metadata (set up previously) and in the value we pass the binary encoding of our XOR value xorValue
.
The value_size_delta
is the change in value size in bytes. In our case it is zero but it could be positive or negative depending on what your new value is. According to xembook’s article there is a bug currently that does not let you set a negative value_size_delta
value.
aliceTx = facade.transaction_factory.create_embedded({
'type': 'accountMetadata',
'signer_public_key': alicePubkey,
'target_address': bobAddress,
'scoped_metadata_key': metadataKey,
'value_size_delta': 0,
'value': unhexlify(xorValue)
})
hash_builder = MerkleHashBuilder()
hash_builder.update(Hash256(sha3.sha3_256(aliceTx.serialize()).digest()))
merkle_hash = hash_builder.final()
deadline = (int((datetime.datetime.today() + datetime.timedelta(hours=2)).timestamp()) - 1616694977) * 1000
aggregate = facade.transaction_factory.create({
'type': 'aggregateComplete',
'signer_public_key': alicePubkey,
'fee': 1000000,
'deadline': deadline,
'transactions_hash': merkle_hash,
'transactions': [aliceTx]
})
signature = facade.sign_transaction(aliceKeypair, aggregate)
aggregate.signature = signature.bytes
hash = facade.hash_transaction(aggregate).bytes
hexlifiedHash = hexlify(hash)
print(hexlifiedHash)
# Bob signs
hexlifiedSignedHash = str(bobKeypair.sign(unhexlify(hexlifiedHash)))
cosignature = (0, bobPubkey.bytes, unhexlify(hexlifiedSignedHash))
aggregate.cosignatures.append(cosignature)
payload = {"payload": hexlify(aggregate.serialize()).decode('utf8').upper()}
strJson = json.dumps(payload)
headers = {'Content-type': 'application/json'}
conn = http.client.HTTPConnection("sym-test-01.opening-line.jp",3000)
conn.request("PUT", "/transactions", strJson,headers)
response = conn.getresponse()
print(response.status, response.reason)
hash = facade.hash_transaction(aggregate)
print('https://sym-test-01.opening-line.jp:3001/transactionStatus/' + str(hash))
The account metadata has been updated!
So if we look at Bob’s account on the testnet explorer. You will see that the metadata value has been updated by Alice.
Original ‘aaa’ (link to transaction):
Updated ‘bbb’ (link to transaction):
You can download the Jupyter Notebook here and have a play around! 😊
Working through the code
I just want to simplify the XOR code slightly and try to explain what is going on. Let’s imagine that our original value was ‘a’ and we want to update it to ‘b’. In order to do this we can get the XOR value using the XOR operator ^
. We can’t compare two characters directly so we use the ord
function to convert the character to a unicode value and the chr
function to obtain the character that represents the unicode value obtained. We then get the hexadecimal value of this character.
updated = 'b'
original = 'a'
c = chr(ord(original) ^ ord(updated))
c.encode().hex()
Lets see what this looks like step by step.
ord(original)
97
ord(updated)
98
ord(original) ^ ord(updated)
3
chr(3)
'\x03'
'\x03'.encode().hex()
'03'
So the final value for the update of ‘a’ to ‘b’ would be the string ’03’. It seems that we can work out what the original value given the new value updated
and the XOR value c
.
ord(updated) ^ ord(c)
97
chr(97)
a
As we see ‘a’ was our original value.
If we run xembook’s code:
metadataValue = 'aaa'
newMetadataValue = 'bbb'
xorValue = "".join([chr(ord(data) ^ ord(code))
for (data, code) in zip(metadataValue, newMetadataValue)]).encode().hex()
And print xorValue
we see that the XOR of ‘aaa’ ‘bbb’ is ‘030303’ so it is simply the same as our toy example ‘a’ vs ‘b’ with the three values concatenated into a single XOR value string.
Thanks!
And that’s it for today’s post, I learnt a lot writing this (or at least I think I did if I understood everything correctly! 😆). Thank you once again to the amazingly talented @xembook for his original article – you are an inspiration to us all!
I’m a Symbol and NEM enthusiast and run this blog to try to grow awareness of the platform in the English-speaking world. If you have any Symbol news you would like me to report on or you have an article that you would like to publish then please let me know!
Sorry, the comment form is closed at this time.