Skip to content

Commit

Permalink
fix: add stacks transaction memo equality by auto-removing trailing n…
Browse files Browse the repository at this point in the history
…ull bytes (#1630)

* fix: add stacks transaction memo equality by removing trailing empty unicode characters

* test: add more cases to make clear

---------

Co-authored-by: janniks <janniks@users.noreply.github.com>
  • Loading branch information
janniks and janniks committed Feb 29, 2024
1 parent 8b9a20c commit bb0b85d
Show file tree
Hide file tree
Showing 2 changed files with 120 additions and 22 deletions.
3 changes: 2 additions & 1 deletion packages/transactions/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,8 @@ export function serializeMemoString(memoString: MemoString): Uint8Array {
}

export function deserializeMemoString(bytesReader: BytesReader): MemoString {
const content = bytesToUtf8(bytesReader.readBytes(MEMO_MAX_LENGTH_BYTES));
let content = bytesToUtf8(bytesReader.readBytes(MEMO_MAX_LENGTH_BYTES));
content = content.replace(/\u0000*$/, ''); // remove all trailing unicode null characters
return { type: StacksMessageType.MemoString, content };
}

Expand Down
139 changes: 118 additions & 21 deletions packages/transactions/tests/builder.test.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,33 @@
import { bytesToHex, utf8ToBytes } from '@stacks/common';
import { bytesToHex, bytesToUtf8, hexToBytes, utf8ToBytes } from '@stacks/common';
import {
createApiKeyMiddleware,
createFetchFn,
StacksMainnet,
StacksTestnet,
createApiKeyMiddleware,
createFetchFn,
} from '@stacks/network';
import * as fs from 'fs';
import fetchMock from 'jest-fetch-mock';
import {
createFungiblePostCondition,
createSTXPostCondition,
serializePostCondition,
} from '../src';
import {
MultiSigSpendingCondition,
SingleSigSpendingCondition,
SponsoredAuthorization,
StandardAuthorization,
createSingleSigSpendingCondition,
createSponsoredAuth,
emptyMessageSignature,
isSingleSig,
MultiSigSpendingCondition,
nextSignature,
SingleSigSpendingCondition,
SponsoredAuthorization,
StandardAuthorization,
} from '../src/authorization';
import {
SignedTokenTransferOptions,
TxBroadcastResult,
TxBroadcastResultOk,
TxBroadcastResultRejected,
broadcastTransaction,
callReadOnlyFunction,
estimateTransaction,
Expand All @@ -31,28 +40,24 @@ import {
makeContractFungiblePostCondition,
makeContractNonFungiblePostCondition,
makeContractSTXPostCondition,
makeSTXTokenTransfer,
makeStandardFungiblePostCondition,
makeStandardNonFungiblePostCondition,
makeStandardSTXPostCondition,
makeSTXTokenTransfer,
makeUnsignedContractCall,
makeUnsignedContractDeploy,
makeUnsignedSTXTokenTransfer,
SignedTokenTransferOptions,
sponsorTransaction,
TxBroadcastResult,
TxBroadcastResultOk,
TxBroadcastResultRejected,
} from '../src/builders';
import { BytesReader } from '../src/bytesReader';
import {
ClarityType,
UIntCV,
bufferCV,
bufferCVFromString,
ClarityType,
noneCV,
serializeCV,
standardPrincipalCV,
UIntCV,
uintCV,
} from '../src/clarity';
import { principalCV } from '../src/clarity/types/principalCV';
Expand All @@ -66,6 +71,7 @@ import {
DEFAULT_CORE_NODE_API_URL,
FungibleConditionCode,
NonFungibleConditionCode,
PayloadType,
PostConditionMode,
PubKeyEncoding,
TransactionVersion,
Expand All @@ -78,17 +84,12 @@ import {
pubKeyfromPrivKey,
publicKeyToString,
} from '../src/keys';
import { createTokenTransferPayload, serializePayload, TokenTransferPayload } from '../src/payload';
import { TokenTransferPayload, createTokenTransferPayload, serializePayload } from '../src/payload';
import { createAssetInfo } from '../src/postcondition-types';
import { createTransactionAuthField } from '../src/signature';
import { TransactionSigner } from '../src/signer';
import { deserializeTransaction, StacksTransaction } from '../src/transaction';
import { StacksTransaction, deserializeTransaction } from '../src/transaction';
import { cloneDeep } from '../src/utils';
import {
createFungiblePostCondition,
createSTXPostCondition,
serializePostCondition,
} from '../src';

function setSignature(
unsignedTransaction: StacksTransaction,
Expand Down Expand Up @@ -2187,3 +2188,99 @@ test('Post-conditions with amount larger than 8 bytes throw an error', () => {
serializePostCondition(fungiblePc);
}).toThrowError('The post-condition amount may not be larger than 8 bytes');
});

test('StacksTransaction serialize/deserialize equality with an empty memo', async () => {
const options = {
recipient: 'SP3FGQ8Z7JY9BWYZ5WM53E0M9NK7WHJF0691NZ159',
amount: 12345n,
fee: 100n,
nonce: 0n,
memo: '', // empty memo
network: new StacksMainnet(),
anchorMode: AnchorMode.Any,
senderKey: 'edf9aee84d9b7abc145504dde6726c64f369d37ee34ded868fabd876c26570bc01',
};
const tx = await makeSTXTokenTransfer(options);

const txHex = tx.serialize();
const txDecoded = deserializeTransaction(txHex);

expect(txDecoded).toEqual(tx);
});

test('StacksTransaction serialize/deserialize equality with a memo', async () => {
const options = {
recipient: 'SP3FGQ8Z7JY9BWYZ5WM53E0M9NK7WHJF0691NZ159',
amount: 12345n,
fee: 100n,
nonce: 0n,
memo: 'Memento Mori',
network: new StacksMainnet(),
anchorMode: AnchorMode.Any,
senderKey: 'edf9aee84d9b7abc145504dde6726c64f369d37ee34ded868fabd876c26570bc01',
};
const tx = await makeSTXTokenTransfer(options);

const txHex = tx.serialize();
const txDecoded = deserializeTransaction(txHex);

expect(txDecoded).toEqual(tx);
});

test('StacksTransaction serialize/deserialize equality with a memo ending in a zero byte', async () => {
const options = {
recipient: 'SP3FGQ8Z7JY9BWYZ5WM53E0M9NK7WHJF0691NZ159',
amount: 12345n,
fee: 100n,
nonce: 0n,
memo: bytesToUtf8(hexToBytes('0')), // null character
network: new StacksMainnet(),
anchorMode: AnchorMode.Any,
senderKey: 'edf9aee84d9b7abc145504dde6726c64f369d37ee34ded868fabd876c26570bc01',
};
const tx = await makeSTXTokenTransfer(options);

const txHex = tx.serialize();
const txDecoded = deserializeTransaction(txHex);

expect(txDecoded).not.toEqual(tx); // we are expecting the zero byte to be stripped (null character)
});

test('StacksTransaction serialize/deserialize equality with an invalid utf-8 byte sequence', () => {
// custom memo with `c200` included; invalid utf-8 (`c200`; `c2` indicating two bytes, but is followed by a null character)
const txHex =
'000000000104006b323aaec89736bc54f751991daeb2e18fe60d2e00000000000000060000000000002710000195deaac518e5e9f910b9214690af80fea0dd13745e392f33392e959208c3e8393e61b355608e4bc23248253c8b6286de63d782bb65dd5f4da5741288095c478c030200000000000516f0f57e9989bca0cf5269db54a4ea809b0291c3fa000000000112a8806d656d6fc20000000000000000000000000000000000000000000000000000000000';
const txDecoded = deserializeTransaction(txHex);

if (txDecoded.payload.payloadType !== PayloadType.TokenTransfer) throw Error;

expect(txDecoded.payload.memo.content).toContain('memo'); // _m_e_m_o--
expect(utf8ToBytes(txDecoded.payload.memo.content)).not.toEqual(hexToBytes('6d656d6fc2'));
expect(utf8ToBytes(txDecoded.payload.memo.content)).not.toEqual(hexToBytes('6d656d6fc200'));

expect(utf8ToBytes(txDecoded.payload.memo.content)).toEqual(hexToBytes('6d656d6fefbfbd')); // javascript may replace invalid utf-8 with `efbfbd` (�)
});

test('StacksTransaction serialize/deserialize equality with an invalid utf-8 byte sequence', () => {
// custom memo with `81` at the start; invalid utf-8 (`81` is a continuation byte, but is not preceded)
const txHex =
'000000000104006b323aaec89736bc54f751991daeb2e18fe60d2e00000000000000060000000000002710000195deaac518e5e9f910b9214690af80fea0dd13745e392f33392e959208c3e8393e61b355608e4bc23248253c8b6286de63d782bb65dd5f4da5741288095c478c030200000000000516f0f57e9989bca0cf5269db54a4ea809b0291c3fa000000000112a880816d656d6f0000000000000000000000000000000000000000000000000000000000';
const txDecoded = deserializeTransaction(txHex);

if (txDecoded.payload.payloadType !== PayloadType.TokenTransfer) throw Error;

expect(txDecoded.payload.memo.content).toContain('memo');
expect(utf8ToBytes(txDecoded.payload.memo.content)).toEqual(hexToBytes('efbfbd6d656d6f')); // javascript may replace invalid utf-8 with `efbfbd` (�)
});

test('StacksTransaction serialize/deserialize equality with an invalid utf-8 byte sequence', () => {
// custom memo with `f0f00000` included; invalid utf-8 (`f0f00000`; `f0` indicating four bytes, but is followed by null characters)
const txHex =
'000000000104006b323aaec89736bc54f751991daeb2e18fe60d2e00000000000000060000000000002710000195deaac518e5e9f910b9214690af80fea0dd13745e392f33392e959208c3e8393e61b355608e4bc23248253c8b6286de63d782bb65dd5f4da5741288095c478c030200000000000516f0f57e9989bca0cf5269db54a4ea809b0291c3fa000000000112a8806d656d6ff0f000000000000000000000000000000000000000000000000000000000';
const txDecoded = deserializeTransaction(txHex);

if (txDecoded.payload.payloadType !== PayloadType.TokenTransfer) throw Error;

expect(txDecoded.payload.memo.content).toContain('memo');
expect(bytesToHex(utf8ToBytes(txDecoded.payload.memo.content))).toContain('efbfbd'); // javascript may replace invalid utf-8 with `efbfbd` (�) twice
});

0 comments on commit bb0b85d

Please sign in to comment.