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`;