Skip to content

Commit

Permalink
docs: mark sha256() as gas-expensive and rework its description
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
novusnota committed Feb 20, 2025
1 parent dda9336 commit 70855a0
Show file tree
Hide file tree
Showing 8 changed files with 123 additions and 22 deletions.
2 changes: 1 addition & 1 deletion dev-docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
24 changes: 16 additions & 8 deletions docs/src/content/docs/ref/core-math.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -425,22 +425,20 @@ let check: Bool = checkSignature(data, signature, publicKey);

## sha256

<Badge text="Gas-expensive" title="Uses 500 gas units or more" variant="danger" size="medium"/><p/>

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

Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion docs/src/content/docs/ref/core-strings.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
2 changes: 1 addition & 1 deletion src/abi/global.ts
Original file line number Diff line number Diff line change
Expand Up @@ -395,7 +395,7 @@ export const GlobalFunctions: Map<string, AbiFunction> = new Map([
// Slice case
if (arg0.name === "Slice") {
const exp = writeExpression(resolved[0]!, ctx);
return `string_hash(${exp})`;
return `__tact_sha256(${exp})`;
}

throwCompilationError(
Expand Down
8 changes: 4 additions & 4 deletions src/test/benchmarks/__snapshots__/benchmarks.spec.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -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`;
2 changes: 1 addition & 1 deletion src/test/benchmarks/benchmarks.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down
20 changes: 20 additions & 0 deletions src/test/e2e-emulated/contracts/intrinsics.tact
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand All @@ -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());
}
Expand Down Expand Up @@ -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 }
85 changes: 79 additions & 6 deletions src/test/e2e-emulated/intrinsics.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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(
Expand All @@ -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);
Expand All @@ -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,
);
});
});

0 comments on commit 70855a0

Please sign in to comment.