Skip to content

Commit 35a7b63

Browse files
committed
Update casting docs, release & migration notes and other related docs
1 parent 43f16a2 commit 35a7b63

5 files changed

Lines changed: 126 additions & 33 deletions

File tree

website/docs/guides/covenants.md

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,11 @@ Smart contracts which persist for multiple transactions might want to keep data
143143
Covenants can also use 'simulated state', where state is kept in the contract script and the contract enforces a new P2SH locking bytecode of the contract with a different state update. This method causes the contract address to change with each state update.
144144
:::
145145

146-
### Keeping local State in NFTs
146+
### Keeping local state in NFTs
147+
148+
When we want to share local state between multiple transactions or contracts, we can use the NFT commitment field in an NFT. This state is accessible in any transaction that includes the NFT.
149+
150+
#### Example: Streaming Mecenas
147151

148152
To demonstrate the concept of 'local state' we consider the Mecenas contract again, and focus on a drawback of this contract: you have to claim the funds at exactly the right moment or you're leaving money on the table. Every time you claim money from the contract, the `this.age` counter is reset, so the next claim is possible 30 days after the previous claim. So if we wait a few days to claim, **these days are basically wasted**.
149153

@@ -194,7 +198,7 @@ contract StreamingMecenas(
194198
require(tx.outputs[1].lockingBytecode == tx.inputs[0].lockingBytecode);
195199
196200
// Update the block height of the previous pledge, kept in the NFT commitment
197-
bytes blockHeightNewPledge = bytes8(tx.locktime);
201+
bytes blockHeightNewPledge = toPaddedBytes(tx.locktime, 8);
198202
require(tx.outputs[1].nftCommitment == blockHeightNewPledge);
199203
}
200204
}
@@ -212,6 +216,21 @@ Instead of having a pledge per 30 day period, we define a pledge per block. At a
212216
We use `tx.locktime` to introspect the value of the timelock, and to write the value to the contract local state: the NFT commitment field.
213217
:::
214218

219+
#### Integer padding for local state
220+
221+
Padding an integer to a fixed-size byte-length is a very important when storing local state in an nftCommitment. We can use the `toPaddedBytes(int, length)` function to pad the integer to the desired length. When casting a script number to bytes, developers need to consider what the preferable fixed-size length is for each individual case depending on the integer range. Below we add a table with info on the maximum integer size for common cases:
222+
223+
| Integer Type | Max integer value | Max Byte Size in Script Number Format |
224+
| -------------- | -----------------------------------| ---------------------------------------|
225+
| Satoshis | 2.1 quadrillion (21,000,000 BCH) | 7 bytes |
226+
| CashTokens | 9.2 quintillion (`2^63 - 1`) | 8 bytes for max supply token |
227+
| Locktime | 4 bytes uInt (`2^32 - 1`) | 5 bytes |
228+
| SequenceNumber | 4 bytes uInt (`2^32 - 1`) | 5 bytes |
229+
230+
:::info
231+
VM numbers follow Script Number format (A.K.A. CSCriptNum), to convert VM number to bytes or the reverse, it's recommended to use helper functions for these conversions from libraries like Libauth.
232+
:::
233+
215234
### Issuing NFTs as receipts
216235

217236
A covenant that manages funds (BCH + fungible tokens of a certain category) which are pooled together from different people often wants to enable its participants to also exit the covenants with their funds. It would be incredibly hard continuously updating a data structure to keep track of which address contributed how much in the local state of the contract. A much better solution is to issue receipts each time funds are added to the pool! This way the contract does not have a 'global view' of who owns what at any time, but it can validate the receipts when invoking a withdrawal.
@@ -251,12 +270,12 @@ contract PooledFunds(
251270
if (amountTokensAdded > 0) {
252271
// Require 1000 sats to pay for future withdrawal fee
253272
require(amountSatsAdded == 1000);
254-
receiptCommitment = 0x01 + bytes8(amountTokensAdded);
273+
receiptCommitment = 0x01 + toPaddedBytes(amountTokensAdded, 8);
255274
} else {
256275
// Place a minimum on the amount of funds that can be added
257276
// Implicitly requires tx.outputs[0].value > tx.inputs[0].value
258277
require(amountSatsAdded > 10000);
259-
receiptCommitment = 0x00 + bytes8(amountSatsAdded);
278+
receiptCommitment = 0x00 + toPaddedBytes(amountSatsAdded, 8);
260279
}
261280
262281
// Require there to be at most three outputs so no additional NFTs can be minted

website/docs/language/functions.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,4 +113,19 @@ bool checkDataSig(datasig s, bytes msg, pubkey pk)
113113

114114
Checks that sig `s` is a valid signature for message `msg` and matches with public key `pk`.
115115

116+
## Other functions
117+
118+
### toPaddedBytes()
119+
```solidity
120+
bytes toPaddedBytes(int value, int length)
121+
```
122+
123+
Pads the integer `value` with zeros to the specified `length`. This is most useful when storing integer values in local state (see [local state guide][local-state-guide]).
124+
125+
:::tip
126+
Using `bytes20 placeholderPkh = toPaddedBytes(0, 20)` will generate a 20 byte zero-array at runtime, whereas
127+
`bytes20 placeholderPkh = 0x0000000000000000000000000000000000000000` will actually take 20 bytes of space in your contract.
128+
:::
129+
116130
[bip146]: https://github.com/bitcoin/bips/blob/master/bip-0146.mediawiki
131+
[local-state-guide]: /docs/guides/covenants#keeping-local-state-in-nfts

website/docs/language/types.md

Lines changed: 22 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ Members:
7070
- `reverse()`: Reverses the string.
7171

7272
:::caution
73-
The script will fail if `split()`or `slice()` is called with an index that is out of bounds.
73+
The script will fail if `split()` or `slice()` is called with an index that is out of bounds.
7474
:::
7575

7676
## Bytes
@@ -167,7 +167,7 @@ Type casting can be done both explicitly and implicitly depending on the type. `
167167
```solidity
168168
pubkey pk = pubkey(0x0000);
169169
bytes editedPk = bytes(pk) + 0x1234;
170-
bytes4 zeroBytes = bytes4(0); // 0x00000000
170+
bool b = bool(5); // true
171171
```
172172

173173
### Casting Table
@@ -179,45 +179,39 @@ See the following table for information on which types can be cast to other whic
179179
| int | | bytes, bool |
180180
| bool | | int |
181181
| string | | bytes |
182-
| bytes | | sig, pubkey, int |
182+
| bytes | | sig, datasig, pubkey, int |
183183
| pubkey | bytes | bytes |
184184
| sig | bytes | bytes |
185185
| datasig | bytes | bytes |
186186

187-
### Int to Byte Casting
188-
189-
When casting integer types to bytes of a certain size, the integer value is padded with zeros, e.g. `bytes4(0) == 0x00000000`. It is also possible to pad with a variable number of zeros by passing in a `size` parameter, which indicates the size of the output, e.g. `bytes(0, 4 - 2) == 0x0000`.
190-
191-
:::tip
192-
Using `bytes20 placeholderPkh= bytes20(0)` will generate a 20 byte zero-array programmatically, whereas
193-
`bytes20 placeholderPkh= 0x0000000000000000000000000000000000000000` will actually take 20 bytes of space in your contract.
194-
:::
195-
196-
Casting an integer to a fixed-size byte-length can be a very important when storing local state in an nftCommitment. When casting a script number to bytes, developers need to consider what the preferable fixed-size length is for each individual case depending on the integer range. Below we add a table with info on the maximum integer size for common cases:
197-
198-
| Integer Type | Max integer value | Max Byte Size in Script Number Format |
199-
| -------------- | -----------------------------------| ---------------------------------------|
200-
| Satoshis | 2.1 quadrillion (21,000,000 BCH) | 7 bytes |
201-
| CashTokens | 9.2 quintillion (`2^63 - 1`) | 8 bytes for max supply token |
202-
| Locktime | 4 bytes uInt (`2^32 - 1`) | 5 bytes |
203-
| SequenceNumber | 4 bytes uInt (`2^32 - 1`) | 5 bytes |
204-
205-
:::info
206-
VM numbers follow Script Number format (A.K.A. CSCriptNum), to convert VM number to bytes or the reverse, it's recommended to use helper functions for these conversions from libraries like Libauth.
207-
:::
208-
209-
### Semantic Byte Casting
187+
### Semantic Bytes Casting
210188

211-
When casting unbounded `bytes` types to bounded `bytes` types (such as `bytes20` or `bytes32`), this is a purely semantic cast. The bytes are not padded with zeros, and no checks are performed to ensure the cast bytes are of the correct length. This can be helpful in certain cases, such as `LockingBytecode`, which expects a specific length input.
189+
When casting unbounded `bytes` types to bounded `bytes` types (such as `bytes20` or `bytes32`), this is a purely semantic cast. The bytes are not padded with zeros, and no checks are performed to ensure the cast bytes are of the correct length. This is why this cast is marked with the `unsafe_` prefix. This can be helpful in certain cases, such as `LockingBytecode`, which expects a specific length input.
212190

213191
#### Example
214192
```solidity
215193
bytes pkh = tx.inputs[0].nftCommitment; // (type = bytes, content = 20 bytes)
216194
// Typecast the variable to be able to use it for 'new LockingBytecodeP2PKH()'
217-
bytes20 bytes20Pkh = bytes20(pkh); // (type = bytes20, content = 20 bytes)
195+
bytes20 bytes20Pkh = unsafe_bytes20(pkh); // (type = bytes20, content = 20 bytes)
218196
bytes25 lockingBytecode = new LockingBytecodeP2PKH(bytes20Pkh);
219197
```
220198

199+
### Other Semantic Casting
200+
When casting a `bytes` to an `int` or when casting an `int` to a `bool`, opcodes are added to the script to perform a conversion between the two types. If you are an advanced user and want to perform these casts without the added opcodes, you can use the `unsafe_` prefix.
201+
202+
#### Example
203+
```solidity
204+
bytes bytesValue = 0x123456000000; // not a valid minimally encoded integer
205+
206+
int(bytesValue); // (type = int, content = 0x123456)
207+
unsafe_int(bytesValue); // (type = int, content = 0x123456000000)
208+
209+
int intValue = 25;
210+
211+
bool(intValue); // (type = bool, content = true / 0x01)
212+
unsafe_bool(intValue); // (type = bool, content = 25 / 0x19)
213+
```
214+
221215
## Operators
222216
An overview of all supported operators and their precedence is included below. Notable is a lack of exponentiation, since these operations are not supported by the underlying Bitcoin Script.
223217

website/docs/releases/migration-notes.md

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,56 @@
22
title: Migration Notes
33
---
44

5+
## v0.12 to v0.13
6+
7+
### cashc compiler
8+
9+
#### bounded bytes casting
10+
11+
To indicate that `bytes4(bytes)` casting is purely semantic (and does not offer any type safety), we have renamed it to `unsafe_bytes4(bytes)`. We have also disallowed `bytes4(int)` casting (see section *int to padded bytes casting*).
12+
13+
```solidity
14+
bytes x = 0x12345678;
15+
16+
// before
17+
bytes4(x); // => 0x12345678 (correct semantic cast)
18+
bytes5(x); // => 0x12345678 (incorrect semantic cast)
19+
20+
// after
21+
// marked as unsafe to indicate that this is a purely semantic cast
22+
unsafe_bytes4(x); // => 0x12345678 (correct semantic cast)
23+
unsafe_bytes5(x); // => 0x12345678 (incorrect semantic cast)
24+
```
25+
26+
#### int to padded bytes casting
27+
28+
In the past, `bytes4(int)` or `bytes(int, 4)` would perform a `NUM2BIN` operation, padding the value to 4 bytes, while `bytes4(bytes)` was a purely semantic type cast. This caused confusion, so instead you can now use the `toPaddedBytes(int, length)` function to perform the same padding (`NUM2BIN`) operation.
29+
30+
```solidity
31+
// before
32+
bytes4(5); // => 0x05000000
33+
bytes(5, 4); // => 0x05000000
34+
35+
// after
36+
toPaddedBytes(5, 4); // => 0x05000000
37+
```
38+
39+
#### bool casting
40+
41+
The `bool()` casting function now correctly changes the value of the argument to `true` for non-zero values and `false` for zero values, instead of only semantically treating the value as a boolean. This worked correctly when using the boolean directly inside `require` or `if` statements, but not when using it in a comparison.
42+
43+
```solidity
44+
// before
45+
require(bool(5)); // => true
46+
require(bool(5) == true); // => false || compiles to 0x05 0x01 OP_NUMEQUALVERIFY
47+
48+
// after
49+
require(bool(5)); // => still true
50+
require(bool(5) == true); // => true || compiles to 0x05 OP_0NOTEQUAL 0x01 OP_NUMEQUALVERIFY
51+
```
52+
53+
If you want to keep the old behaviour (without added opcodes), you can use the `unsafe_bool()` casting function instead.
54+
555
## v0.11 to v0.12
656

757
There are several breaking changes to the SDK in this release.

website/docs/releases/release-notes.md

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,17 @@
22
title: Release Notes
33
---
44

5-
## v0.13.0-next.2
5+
## v0.13.0-next.3
6+
7+
This release contains several breaking changes, please refer to the [migration notes](/docs/releases/migration-notes) for more information.
68

79
#### cashc compiler
810
- :sparkles: Add support for `do {} while ()` loops.
911
- :sparkles: Add support for bitwise and arithmetic shift operators (`<<`, `>>`) and bitwise inversion (`~`).
12+
- :sparkles: Add `unsafe_bool()` and `unsafe_int()` casting for semantic-only casts.
13+
- :bug: **BREAKING**: Fix issue where `bool()` casting did not change the value of the argument.
14+
- :boom: **BREAKING**: Rename `bytes4(int)` and `bytes(int, 4)` to `toPaddedBytes(int, 4)`.
15+
- :boom: **BREAKING**: Rename `bytes4(bytes)` to `unsafe_bytes4(bytes)`.
1016
- :racehorse: Add optimisations for negated number comparisons.
1117

1218
#### CashScript SDK
@@ -15,13 +21,22 @@ title: Release Notes
1521
- :hammer_and_wrench: Update default VM target to `BCH_2026_05`.
1622
- :hammer_and_wrench: Improve package size by tidying up dependencies.
1723

24+
#### Testing Suite
25+
26+
- :hammer_and_wrench: Add README.md to help guide users on how to use the testing suite.
27+
- :hammer_and_wrench: Compile all contracts in the `contracts/` directory and save the artifacts in the `artifacts/` directory.
28+
- :hammer_and_wrench: Compile TS artifacts as well as JSON artifacts.
29+
- :hammer_and_wrench: Add key management utilities for testing.
30+
1831
## v0.12.1
1932

2033
#### CashScript SDK
2134
- :sparkles: Add Vitest extensions for automated testing.
2235

2336
## v0.12.0
2437

38+
This release contains several breaking changes, please refer to the [migration notes](/docs/releases/migration-notes) for more information.
39+
2540
#### CashScript SDK
2641
- :sparkles: Add `getVmResourceUsage` method to `TransactionBuilder`.
2742
- :sparkles: Add `maximumFeeSatsPerByte` and `allowImplicitFungibleTokenBurn` options to `TransactionBuilder` constructor.

0 commit comments

Comments
 (0)