From 70855a05117dddabdec90fcf56f89869b07aa39e Mon Sep 17 00:00:00 2001
From: Novus Nota <68142933+novusnota@users.noreply.github.com>
Date: Thu, 20 Feb 2025 19:37:05 +0100
Subject: [PATCH 1/4] docs: mark `sha256()` as gas-expensive and rework its
description
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Also made the passed Slices use the same function, otherwise they'd
still have all the same problems we've fixed before for Strings — the
benchmarks didn't show those issues.
---
dev-docs/CHANGELOG.md | 2 +-
docs/src/content/docs/ref/core-math.mdx | 24 ++++--
docs/src/content/docs/ref/core-strings.mdx | 2 +-
src/abi/global.ts | 2 +-
.../__snapshots__/benchmarks.spec.ts.snap | 8 +-
src/test/benchmarks/benchmarks.spec.ts | 2 +-
.../e2e-emulated/contracts/intrinsics.tact | 20 +++++
src/test/e2e-emulated/intrinsics.spec.ts | 85 +++++++++++++++++--
8 files changed, 123 insertions(+), 22 deletions(-)
diff --git a/dev-docs/CHANGELOG.md b/dev-docs/CHANGELOG.md
index d023cbf4e..8b3898e0f 100644
--- a/dev-docs/CHANGELOG.md
+++ b/dev-docs/CHANGELOG.md
@@ -88,7 +88,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Incorrect call generation to a mutation function: PR [#1608](https://github.com/tact-lang/tact/pull/1608)
- Allow constant/trait constants depend on each other: PR [#1622](https://github.com/tact-lang/tact/pull/1622)
- Combine all generated FunC code into a single file: PR [#1698](https://github.com/tact-lang/tact/pull/1698)
-- Runtime `sha256` now work for arbitrary strings with length >= 128: PR [#1626](https://github.com/tact-lang/tact/pull/1626)
+- Runtime `sha256` now work for arbitrary strings with length >= 128: PR [#1626](https://github.com/tact-lang/tact/pull/1626), PR [#1936](https://github.com/tact-lang/tact/pull/1936)
- Generated code in TypeScript wrappers for contract with `init(init: Init)`: PR [#1709](https://github.com/tact-lang/tact/pull/1709)
- Error message for comment (text) receivers with 124 bytes or more: PR [#1711](https://github.com/tact-lang/tact/pull/1711)
- Support overriding constants and methods of BaseTrait: PR [#1591](https://github.com/tact-lang/tact/pull/1591)
diff --git a/docs/src/content/docs/ref/core-math.mdx b/docs/src/content/docs/ref/core-math.mdx
index 8edbedaa6..7c2243c21 100644
--- a/docs/src/content/docs/ref/core-math.mdx
+++ b/docs/src/content/docs/ref/core-math.mdx
@@ -425,22 +425,20 @@ let check: Bool = checkSignature(data, signature, publicKey);
## sha256
+
+
```tact
fun sha256(data: Slice): Int;
fun sha256(data: String): Int;
```
-Computes and returns the [SHA-256][sha-2] hash as a $256$-bit unsigned [`Int{:tact}`][int] from a passed [`Slice{:tact}`][slice] or [`String{:tact}`][p] `data`.
-
-In case `data` is a [`String{:tact}`][p] it should have a number of bits divisible by $8$, and in case it's a [`Slice{:tact}`][slice] it must **also** have no references (i.e. only up to $1023$ bits of data in total). This function tries to resolve constant string values at [compile-time](/ref/core-comptime) whenever possible.
+Computes and returns the [SHA-256][sha-2] hash as a $256$-bit unsigned [`Int{:tact}`][int] from a passed [`Slice{:tact}`][slice] or [`String{:tact}`][p] `data`, which should have a number of bits divisible by $8$.
-:::caution
+In case `data` is a [`Slice{:tact}`][slice] it must have no more than a single reference per cell, because only the first reference of each nested cell will be taken into account.
- If the [`String{:tact}`][p] value cannot be resolved during [compilation time](/ref/core-comptime), then the hash is calculated at runtime by the [TVM][tvm] itself. Note, that hashing strings with more than $128$ bytes by the [TVM][tvm] can cause collisions if their first $128$ bytes are the same.
+This function tries to resolve constant string values at [compile-time](/ref/core-comptime) whenever possible.
- Therefore, prefer using statically known strings whenever possible. When in doubt, use strings of up to $128$ bytes long.
-
-:::
+Attempts to specify a [`Slice{:tact}`][slice] or [`String{:tact}`][p] with number of bits **not** divisible by $8$ throw an exception with [exit code 9](/book/exit-codes#9): `Cell underflow`.
Usage examples:
@@ -451,6 +449,16 @@ sha256(someVariableElsewhere); // will try to resolve at compile-time,
// and fallback to run-time evaluation
```
+:::tip[Before Tact 1.6]
+
+ Previously, if a [`String{:tact}`][p] value couldn't be resolved during [compilation time](/ref/core-comptime), the hash was calculated at runtime by the [TVM][tvm] itself. And this caused collisions of strings with more than $127$ bytes if their first $127$ bytes were the same.
+
+ That's because all [SHA-256][sha-2]-related instructions of the [TVM][tvm] consider only the data bits, ignoring possible references to other cells needed to form larger strings.
+
+ Therefore, in general, and in versions of Tact prior to 1.6, it is preferable to use statically known strings whenever possible. When in doubt, use strings of up to $127$ bytes long.
+
+:::
+
[p]: /book/types#primitive-types
[bool]: /book/types#booleans
[int]: /book/integers
diff --git a/docs/src/content/docs/ref/core-strings.mdx b/docs/src/content/docs/ref/core-strings.mdx
index b256b16ff..0d85b765c 100644
--- a/docs/src/content/docs/ref/core-strings.mdx
+++ b/docs/src/content/docs/ref/core-strings.mdx
@@ -31,7 +31,7 @@ let fizz: StringBuilder = beginString();
fun beginComment(): StringBuilder;
```
-Creates and returns an empty [`StringBuilder{:tact}`][p] for building a comment string, which prefixes the resulting [`String{:tact}`][p] with four null bytes. This format is used for passing text comments as message bodies.
+Creates and returns an empty [`StringBuilder{:tact}`][p] for building a comment string, which prefixes the resulting [`String{:tact}`][p] with four null bytes. [This format](https://docs.ton.org/v3/guidelines/dapps/asset-processing/nft-processing/metadata-parsing#snake-data-encoding) is used for passing text comments as message bodies.
Usage example:
diff --git a/src/abi/global.ts b/src/abi/global.ts
index 79952eacb..0dd2c9dd0 100644
--- a/src/abi/global.ts
+++ b/src/abi/global.ts
@@ -395,7 +395,7 @@ export const GlobalFunctions: Map = new Map([
// Slice case
if (arg0.name === "Slice") {
const exp = writeExpression(resolved[0]!, ctx);
- return `string_hash(${exp})`;
+ return `__tact_sha256(${exp})`;
}
throwCompilationError(
diff --git a/src/test/benchmarks/__snapshots__/benchmarks.spec.ts.snap b/src/test/benchmarks/__snapshots__/benchmarks.spec.ts.snap
index cd860c7e1..588f2421b 100644
--- a/src/test/benchmarks/__snapshots__/benchmarks.spec.ts.snap
+++ b/src/test/benchmarks/__snapshots__/benchmarks.spec.ts.snap
@@ -20,14 +20,14 @@ exports[`benchmarks benchmark sha256: gas hash string big repeated 1`] = `2809`;
exports[`benchmarks benchmark sha256: gas hash string big repeated more 1`] = `2811`;
-exports[`benchmarks benchmark sha256: gas hash string slice 1`] = `2285`;
+exports[`benchmarks benchmark sha256: gas hash string slice 1`] = `2808`;
-exports[`benchmarks benchmark sha256: gas hash string slice repeated 1`] = `2285`;
+exports[`benchmarks benchmark sha256: gas hash string slice repeated 1`] = `2809`;
+
+exports[`benchmarks benchmark sha256: gas hash string slice repeated more 1`] = `2811`;
exports[`benchmarks benchmark sha256: gas hash string small 1`] = `2285`;
exports[`benchmarks benchmark sha256: gas hash string small repeated 1`] = `2285`;
exports[`benchmarks benchmark sha256: gas hash string small repeated more 1`] = `2285`;
-
-exports[`benchmarks benchmark sha256: gas hash string string repeated more 1`] = `2285`;
diff --git a/src/test/benchmarks/benchmarks.spec.ts b/src/test/benchmarks/benchmarks.spec.ts
index 8ad1d861c..2744cde10 100644
--- a/src/test/benchmarks/benchmarks.spec.ts
+++ b/src/test/benchmarks/benchmarks.spec.ts
@@ -167,7 +167,7 @@ describe("benchmarks", () => {
).toMatchSnapshot("gas hash string small repeated more");
expect(
await hashStringAsSLice(sha256AsSlice, "hello world".repeat(10)),
- ).toMatchSnapshot("gas hash string string repeated more");
+ ).toMatchSnapshot("gas hash string slice repeated more");
});
it("benchmark cells creation", async () => {
diff --git a/src/test/e2e-emulated/contracts/intrinsics.tact b/src/test/e2e-emulated/contracts/intrinsics.tact
index 5f45cd563..b8db63f64 100644
--- a/src/test/e2e-emulated/contracts/intrinsics.tact
+++ b/src/test/e2e-emulated/contracts/intrinsics.tact
@@ -80,6 +80,10 @@ contract IntrinsicsTester {
return sha256("hello world");
}
+ get fun getHashSlice(): Int {
+ return sha256("hello world".asSlice());
+ }
+
get fun getHash2(): Int {
return self.f;
}
@@ -100,6 +104,18 @@ contract IntrinsicsTester {
return sha256(src);
}
+ get fun getHashLongRuntimeSlice(src: Slice): Int {
+ return sha256(src);
+ }
+
+ get fun getHashSHA256U(src: Slice): Int {
+ return hashSHA256U(src);
+ }
+
+ get fun getHashHASHEXTSHA256(src: Slice): Int {
+ return hashHASHEXTSHA256(src);
+ }
+
receive("emit_1") {
emit("Hello world".asComment());
}
@@ -240,3 +256,7 @@ contract IntrinsicsTester {
return self.v;
}
}
+
+asm fun hashSHA256U(src: Slice): Int { SHA256U }
+
+asm fun hashHASHEXTSHA256(src: Slice): Int { ONE HASHEXT_SHA256 }
diff --git a/src/test/e2e-emulated/intrinsics.spec.ts b/src/test/e2e-emulated/intrinsics.spec.ts
index 5f37f08bd..a7c7b8e95 100644
--- a/src/test/e2e-emulated/intrinsics.spec.ts
+++ b/src/test/e2e-emulated/intrinsics.spec.ts
@@ -270,6 +270,14 @@ describe("intrinsics", () => {
expect(actual.toString(16)).toEqual(expected.toString(16));
};
+ const checkSha256Slice = async (input: string) => {
+ const expected = sha256(input).value;
+ const actual = await contract.getGetHashLongRuntimeSlice(
+ beginCell().storeStringTail(input).asSlice(),
+ );
+ expect(actual.toString(16)).toEqual(expected.toString(16));
+ };
+
const generateString = (length: number): string => {
const chars =
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
@@ -285,6 +293,7 @@ describe("intrinsics", () => {
return sha256(src).value;
}
expect(await contract.getGetHash()).toBe(sha256Hex("hello world"));
+ expect(await contract.getGetHashSlice()).toBe(sha256Hex("hello world"));
expect(await contract.getGetHash2()).toBe(sha256Hex("hello world"));
expect(
await contract.getGetHash3(
@@ -302,13 +311,29 @@ describe("intrinsics", () => {
const input256bytes = generateString(256);
- // check various length input
- await checkSha256(generateString(15));
- await checkSha256(generateString(127));
- await checkSha256(generateString(128));
+ // check various length input for strings and slices
+ const s1 = generateString(15);
+ await checkSha256(s1);
+ await checkSha256Slice(s1);
+
+ const s2 = generateString(127);
+ await checkSha256(s2);
+ await checkSha256Slice(s2);
+
+ const s3 = generateString(128);
+ await checkSha256(s3);
+ await checkSha256Slice(s3);
+
await checkSha256(input256bytes);
- await checkSha256(generateString(1024));
- await checkSha256(generateString(16999));
+ await checkSha256Slice(input256bytes);
+
+ const s5 = generateString(1024);
+ await checkSha256(s5);
+ await checkSha256Slice(s5);
+
+ const s6 = generateString(16999);
+ await checkSha256(s6);
+ await checkSha256Slice(s6);
// check that we hash all string, not just first 127 bytes
const first128bytesOf256bytesString = input256bytes.slice(0, 128);
@@ -320,5 +345,53 @@ describe("intrinsics", () => {
expect(first128bytesOf256bytesStringHash).not.toEqual(
input256bytesStringHash,
);
+
+ // check that we hash all slice, not just first 127 bytes
+ const first128bytesOf256bytesSliceHash =
+ await contract.getGetHashLongRuntimeSlice(
+ beginCell()
+ .storeStringTail(first128bytesOf256bytesString)
+ .asSlice(),
+ );
+ const input256bytesSliceHash =
+ await contract.getGetHashLongRuntimeSlice(
+ beginCell().storeStringTail(input256bytes).asSlice(),
+ );
+
+ expect(first128bytesOf256bytesSliceHash).not.toEqual(
+ input256bytesSliceHash,
+ );
+
+ // NOTE:
+ // The SHA256U instruction is used by string_hash() from FunC stdlib,
+ // which was previously used by Tact for runtime hashing of String and Slice values.
+
+ // check that SHA256U instruction hashes ONLY first 127 bytes
+ const first128bytesOf256bytesSHA256U = await contract.getGetHashSha256U(
+ beginCell()
+ .storeStringTail(first128bytesOf256bytesString)
+ .asSlice(),
+ );
+ const input256bytesSHA256U = await contract.getGetHashSha256U(
+ beginCell().storeStringTail(input256bytes).asSlice(),
+ );
+
+ expect(first128bytesOf256bytesSHA256U).toEqual(input256bytesSHA256U);
+
+ // check that HASHEXT_SHA256 instruction hashes ONLY first 127 bytes
+ const first128bytesOf256bytesHASHEXTSHA256 =
+ await contract.getGetHashHashextsha256(
+ beginCell()
+ .storeStringTail(first128bytesOf256bytesString)
+ .asSlice(),
+ );
+ const input256bytesHASHEXTSHA256 =
+ await contract.getGetHashHashextsha256(
+ beginCell().storeStringTail(input256bytes).asSlice(),
+ );
+
+ expect(first128bytesOf256bytesHASHEXTSHA256).toEqual(
+ input256bytesHASHEXTSHA256,
+ );
});
});
From f4c4f323d36033aab866dd2a1ae9403662f1ce1f Mon Sep 17 00:00:00 2001
From: Novus Nota <68142933+novusnota@users.noreply.github.com>
Date: Thu, 20 Feb 2025 21:16:23 +0100
Subject: [PATCH 2/4] chore: changelog
---
dev-docs/CHANGELOG.md | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/dev-docs/CHANGELOG.md b/dev-docs/CHANGELOG.md
index 3e634fe05..7d6a0e18c 100644
--- a/dev-docs/CHANGELOG.md
+++ b/dev-docs/CHANGELOG.md
@@ -88,7 +88,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Incorrect call generation to a mutation function: PR [#1608](https://github.com/tact-lang/tact/pull/1608)
- Allow constant/trait constants depend on each other: PR [#1622](https://github.com/tact-lang/tact/pull/1622)
- Combine all generated FunC code into a single file: PR [#1698](https://github.com/tact-lang/tact/pull/1698)
-- Runtime `sha256` now work for arbitrary strings with length >= 128: PR [#1626](https://github.com/tact-lang/tact/pull/1626), PR [#1936](https://github.com/tact-lang/tact/pull/1936)
+- Runtime calls to `sha256()` now work for arbitrary strings with length >= 128: PR [#1626](https://github.com/tact-lang/tact/pull/1626)
- Generated code in TypeScript wrappers for contract with `init(init: Init)`: PR [#1709](https://github.com/tact-lang/tact/pull/1709)
- Error message for comment (text) receivers with 124 bytes or more: PR [#1711](https://github.com/tact-lang/tact/pull/1711)
- Support overriding constants and methods of BaseTrait: PR [#1591](https://github.com/tact-lang/tact/pull/1591)
@@ -99,6 +99,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Check map types for `deepEquals` method: PR [#1718](https://github.com/tact-lang/tact/pull/1718)
- Bump used `@tact-lang/opcode` version to `0.2` which fixes many issues in CI runs: PR [#1922](https://github.com/tact-lang/tact/pull/1922)
- Generation of the fallback receiver for external messages: PR [#1926](https://github.com/tact-lang/tact/pull/1926)
+- Runtime calls to `sha256()` now work for arbitrary slices with length >= 128: PR [#1936](https://github.com/tact-lang/tact/pull/1936)
### Docs
From 0fc7fa6d93ab26f6bfcdc476f41b657edeabad5f Mon Sep 17 00:00:00 2001
From: Novus Nota <68142933+novusnota@users.noreply.github.com>
Date: Fri, 21 Feb 2025 13:44:06 +0100
Subject: [PATCH 3/4] oh snap
---
src/test/benchmarks/__snapshots__/benchmarks.spec.ts.snap | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/src/test/benchmarks/__snapshots__/benchmarks.spec.ts.snap b/src/test/benchmarks/__snapshots__/benchmarks.spec.ts.snap
index b000ba523..47bb086c3 100644
--- a/src/test/benchmarks/__snapshots__/benchmarks.spec.ts.snap
+++ b/src/test/benchmarks/__snapshots__/benchmarks.spec.ts.snap
@@ -20,14 +20,14 @@ exports[`benchmarks benchmark sha256: gas hash string big repeated 1`] = `2789`;
exports[`benchmarks benchmark sha256: gas hash string big repeated more 1`] = `2791`;
-exports[`benchmarks benchmark sha256: gas hash string slice 1`] = `2808`;
+exports[`benchmarks benchmark sha256: gas hash string slice 1`] = `2788`;
-exports[`benchmarks benchmark sha256: gas hash string slice repeated 1`] = `2809`;
+exports[`benchmarks benchmark sha256: gas hash string slice repeated 1`] = `2789`;
-exports[`benchmarks benchmark sha256: gas hash string slice repeated more 1`] = `2811`;
+exports[`benchmarks benchmark sha256: gas hash string slice repeated more 1`] = `2791`;
exports[`benchmarks benchmark sha256: gas hash string small 1`] = `2265`;
exports[`benchmarks benchmark sha256: gas hash string small repeated 1`] = `2265`;
-exports[`benchmarks benchmark sha256: gas hash string small repeated more 1`] = `2285`;
+exports[`benchmarks benchmark sha256: gas hash string small repeated more 1`] = `2265`;
From 7d81ca9a4fdda81e582202960f758b6e1711d0e9 Mon Sep 17 00:00:00 2001
From: Novus Nota <68142933+novusnota@users.noreply.github.com>
Date: Mon, 24 Feb 2025 10:05:56 +0100
Subject: [PATCH 4/4] benches!
---
src/test/benchmarks/__snapshots__/benchmarks.spec.ts.snap | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/src/test/benchmarks/__snapshots__/benchmarks.spec.ts.snap b/src/test/benchmarks/__snapshots__/benchmarks.spec.ts.snap
index 401f4918a..0295ddf37 100644
--- a/src/test/benchmarks/__snapshots__/benchmarks.spec.ts.snap
+++ b/src/test/benchmarks/__snapshots__/benchmarks.spec.ts.snap
@@ -20,14 +20,14 @@ exports[`benchmarks benchmark sha256: gas hash string big repeated 1`] = `2671`;
exports[`benchmarks benchmark sha256: gas hash string big repeated more 1`] = `2673`;
-exports[`benchmarks benchmark sha256: gas hash string slice 1`] = `2788`;
+exports[`benchmarks benchmark sha256: gas hash string slice 1`] = `2670`;
-exports[`benchmarks benchmark sha256: gas hash string slice repeated 1`] = `2789`;
+exports[`benchmarks benchmark sha256: gas hash string slice repeated 1`] = `2671`;
-exports[`benchmarks benchmark sha256: gas hash string slice repeated more 1`] = `2791`;
+exports[`benchmarks benchmark sha256: gas hash string slice repeated more 1`] = `2673`;
exports[`benchmarks benchmark sha256: gas hash string small 1`] = `2147`;
exports[`benchmarks benchmark sha256: gas hash string small repeated 1`] = `2147`;
-exports[`benchmarks benchmark sha256: gas hash string small repeated more 1`] = `2265`;
+exports[`benchmarks benchmark sha256: gas hash string small repeated more 1`] = `2147`;