How to Deal with transfer memo

1. Construct memo

Let's review the transfer message constructed in the How to send a transaction to GXChain? section.

{
  "ref_block_num": 54701, // an exist block num
  "ref_block_prefix": 2861949695, // The exist block id
  "expiration": "2019-01-18T03:35:54", // transaction expiration date, max expiration = current + 24hours
  "operations": [
   [
      0, // operation id
      { // operation
        "fee": {
          "amount": 1179,
          "asset_id": "1.3.1"
        },
        "from": "1.2.937396",
        "to": "1.2.19645",
        "amount": {
          "amount": 173600000,
          "asset_id": "1.3.1"
        },
        "memo": {
          "from": "GXC6cUP6LvdpcfC9G4TMre4yxB3PxttUjVK1ybybgt63ZtEKCXamC", // memo_key of from account
          "to": "GXC7o71VExYFoFJKtduFXEF15jgPdbmC1tdyT8BPCpnzCTeFiXEog", // memo_key of to account
          "nonce": 1, // nonce of encryption key
          "message": "15e06e19346d8f9c2d5355abcf5fffaa" // ECIES encrypted message
        },
        "extensions": []
      }
    ]
  ],
  "extensions": [],
  "signatures": []
}

We can see that the memo of the transfer transaction consists of the following four parts:

  • from: memo public key of from account
  • to: memo public key of to account
  • nonce: random uint64
  • message: Encrypted message

Step1: Get memo public key of target account

request:

curl --data '{
    "jsonrpc": "2.0",
    "method": "call",
    "params": [0, "get_account_by_name", [<to_account>]],
    "id": 1
}' https://node1.gxb.io/rpc

response:

{
        "id": "1.2.19645",
        "membership_expiration_date": "1970-01-01T00:00:00",
        "merchant_expiration_date": "1970-01-01T00:00:00",
        "datasource_expiration_date": "1970-01-01T00:00:00",
        "data_transaction_member_expiration_date": "1970-01-01T00:00:00",
        "registrar": "1.2.26",
        "referrer": "1.2.26",
        "lifetime_referrer": "1.2.26",
        "merchant_auth_referrer": "1.2.0",
        "datasource_auth_referrer": "1.2.0",
        "network_fee_percentage": 2000,
        "lifetime_referrer_fee_percentage": 3000,
        "referrer_rewards_percentage": 0,
        "name": "binance-1",
        "vm_type": "",
        "vm_version": "",
        "code": "",
        "code_version": "",
        "abi": {
            "version": "gxc::abi/1.0",
            "types": [],
            "structs": [],
            "actions": [],
            "tables": [],
            "error_messages": [],
            "abi_extensions": []
        },
        "owner": {
            "weight_threshold": 1,
            "account_auths": [],
            "key_auths": [
                [
                    "GXC8XBces1ohTTVNeWB97HoyCHmRaTWNcFKdMWfdJqvMw8FmWjNqK",
                    1
                ]
            ],
            "address_auths": []
        },
        "active": {
            "weight_threshold": 1,
            "account_auths": [],
            "key_auths": [
                [
                    "GXC8FNySGvDBzRLTHyLTewWpiKbC718y7nZ3jU2Vbcx7XxF5z2SK3",
                    1
                ]
            ],
            "address_auths": []
        },
        "options": {
            "memo_key": "GXC7o71VExYFoFJKtduFXEF15jgPdbmC1tdyT8BPCpnzCTeFiXEog",
            "voting_account": "1.2.5",
            "num_witness": 0,
            "num_committee": 0,
            "votes": [],
            "extensions": []
        },
        "statistics": "2.6.19645",
        "whitelisting_accounts": [],
        "blacklisting_accounts": [],
        "whitelisted_accounts": [],
        "blacklisted_accounts": [],
        "owner_special_authority": [
            0,
            {}
        ],
        "active_special_authority": [
            0,
            {}
        ],
        "top_n_control_flags": 0
    }

we get:

let to_memo_key = to_account.options.memo_key

Step2: Calculate Shared Secret

let shared = secp256k1.ecdhUnsafe(to_memo_key, from_private_key)
let S = shared.Q.x.toBuffer({size: 32}); // Q is instance of Point(x,y), get x axis as shared seed
if (S.length < 32) { // fill by 0 if length < 32
    let pad = new Buffer (32 - S.length).fill (0);
    S = Buffer.concat ([pad, S]);
}
let shared_secret = sha512(S);

Step3: Generate Nonce

let nonce = randomUint64().toString();

Step4: AES encrypt with checksum

let keyHash = sha512(nonce.toBytes().concat(shared_secret.toBytes()));
let iv = keyHash.slice(64, 96);
let key = keyHash.slice(0, 64);
let aes = new AES(iv,key);
let checksum = sha256(message).slice(0,4);
let payload = checksum.concat(message.toBytes());
let encryptedMessage = aes.encrypt(payload);

2. Decrypt memo

Step1: Get memo public key of from account

let from_memo_key = transaction.operations[0][1].memo.from;

Step2: Calculate Shared Secret

let shared = secp256k1.ecdhUnsafe(from_memo_key, to_private_key)
let S = shared.Q.x.toBuffer({size: 32}); // Q is instance of Point(x,y), get x axis as shared seed
if (S.length < 32) { // fill by 0 if length < 32
    let pad = new Buffer (32 - S.length).fill (0);
    S = Buffer.concat ([pad, S]);
}
let shared_secret = sha512(S);

Step3: AES decrypt with checksum

let keyHash = sha512(transaction.operations[0][1].memo.nonce.toBytes().concat(shared_secret.toBytes()));
let iv = keyHash.slice(64, 96);
let key = keyHash.slice(0, 64);
let aes = new AES(iv,key);
let payload = aes.decrypt(transaction.operations[0][1].memo.message);
let checksum = payload.slice(0, 4);
let message = payload.slice(4).toString('utf8');
assert(sha256(message).slice(0,4) == checksum,'Invalid checksum');
Last Updated: 2/15/2019, 5:59:10 PM