Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ The limits depend on the message type as shown in the following table.

| <Tooltip text="A collection of nodes that run their own instance of the ICP [consensus](https://learn.internetcomputer.org/hc/en-us/articles/34207558615956-Consensus) algorithm.">Subnet</Tooltip> limits                                                                        | Constraint  |
| ------------------------------------------------------------------------------------ | ----------- |
| Subnet capacity (total memory available per subnet)                                  | 1TiB        |
| Subnet capacity (total memory available per subnet)                                  | {{SUBNET_CAPACITY_TIB}}        |
| Maximum number of snapshots per canister                                              | 1           |

| Memory resource limits                                                               | Constraint  |
Expand Down
2 changes: 1 addition & 1 deletion docs/building-apps/developer-tools/dfx-json.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ Other fields applicable to all canister types include:

- **`memory_allocation`** (`Byte`): Max memory (in bytes) for the canister. Can be integer or string with units (i.e. `72`, "2KB", or "4 MiB").

- **`reserved_cycles_limit`** (`uint128 | null`): Upper limit of reserved cycles. Reserved cycles are cycles that the system sets aside for future use by the canister. If a subnet's storage exceeds 450 GiB, then every time a canister allocates new storage bytes, the system sets aside some amount of cycles from the main balance of the canister. These reserved cycles will be used to cover future payments for the newly allocated bytes. The reserved cycles are not transferable and the amount of reserved cycles depends on how full the subnet is.\n\nA setting of 0 means that the canister will trap if it tries to allocate new storage while the subnet's memory usage exceeds 450 GiB.
- **`reserved_cycles_limit`** (`uint128 | null`): Upper limit of reserved cycles. Reserved cycles are cycles that the system sets aside for future use by the canister. If a subnet's storage exceeds {{SUBNET_THRESHOLD_GIB}}, then every time a canister allocates new storage bytes, the system sets aside some amount of cycles from the main balance of the canister. These reserved cycles will be used to cover future payments for the newly allocated bytes. The reserved cycles are not transferable and the amount of reserved cycles depends on how full the subnet is.\n\nA setting of 0 means that the canister will trap if it tries to allocate new storage while the subnet's memory usage exceeds {{SUBNET_THRESHOLD_GIB}}.

- **`wasm_memory_limit`** (`Byte | null`): Specifies a soft limit (in bytes) on the Wasm memory usage of the canister.Update calls, timers, heartbeats, installs, and post-upgrades fail if the Wasm memory usage exceeds this limit. The main purpose of this setting is to protect against the case when the canister reaches the hard 4GiB limit. Must be a number of bytes between 0 and 2^48 (i.e. 256 TiB), inclusive. Can be specified as an integer, or as an SI unit string (e.g. "4KB", "2 MiB").

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1010,7 +1010,7 @@
},
"reserved_cycles_limit": {
"title": "Reserved Cycles Limit",
"description": "Specifies the upper limit of the canister's reserved cycles balance.\n\nReserved cycles are cycles that the system sets aside for future use by the canister. If a subnet's storage exceeds 450 GiB, then every time a canister allocates new storage bytes, the system sets aside some amount of cycles from the main balance of the canister. These reserved cycles will be used to cover future payments for the newly allocated bytes. The reserved cycles are not transferable and the amount of reserved cycles depends on how full the subnet is.\n\nA setting of 0 means that the canister will trap if it tries to allocate new storage while the subnet's memory usage exceeds 450 GiB.",
"description": "Specifies the upper limit of the canister's reserved cycles balance.\n\nReserved cycles are cycles that the system sets aside for future use by the canister. If a subnet's storage exceeds {{SUBNET_THRESHOLD_GIB}}, then every time a canister allocates new storage bytes, the system sets aside some amount of cycles from the main balance of the canister. These reserved cycles will be used to cover future payments for the newly allocated bytes. The reserved cycles are not transferable and the amount of reserved cycles depends on how full the subnet is.\n\nA setting of 0 means that the canister will trap if it tries to allocate new storage while the subnet's memory usage exceeds {{SUBNET_THRESHOLD_GIB}}.",
"default": null,
"type": [
"integer",
Expand Down
6 changes: 3 additions & 3 deletions docs/building-apps/essentials/gas-cost.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -119,11 +119,11 @@ Operations that potentially allocate new storage bytes are:

The reserved cycles are not transferable, and the amount depends on how full the subnet’s memory is.

- If subnet usage is below `450GiB`, then the amount of reserved cycles per allocated byte is `0`.
- If subnet usage is below `{{SUBNET_THRESHOLD_GIB}}`, then the amount of reserved cycles per allocated byte is `0`.

- If subnet usage is above `450GiB`, then the amount of reserved cycles per allocated byte grows linearly depending on the subnet usage, from `0` to `10` years worth of payments at the subnet capacity (`1TiB`).
- If subnet usage is above `{{SUBNET_THRESHOLD_GIB}}`, then the amount of reserved cycles per allocated byte grows linearly depending on the subnet usage, from `0` to `10` years worth of payments at the subnet capacity (`{{SUBNET_CAPACITY_TIB}}`).

A controller of a canister can disable resource reservation by setting the `reserved_cycles_limit=0` in the canister’s settings. Such opted-out canisters are not able to allocate if the subnet usage is above `450GiB`.
A controller of a canister can disable resource reservation by setting the `reserved_cycles_limit=0` in the canister’s settings. Such opted-out canisters are not able to allocate if the subnet usage is above `{{SUBNET_THRESHOLD_GIB}}`.

To prevent repeated allocations resulting in repeated increases of the canister’s reserved cycles balance, a memory allocation can be specified in the canister settings. This way, the canister will be charged as if the entire amount of allocated storage is being used, but no additional allocations will be performed.
Hence, the canister’s reserved cycles balance might only increase when setting the memory allocation, but does not increase afterwards.
Expand Down
4 changes: 2 additions & 2 deletions docs/building-apps/security/dos.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ Learn more about managing memory and compute resources in the [storage](/docs/bu
* **Subnet and canister distribution**: Implement a smart canister deployment strategy by monitoring the load on subnets. You can choose to deploy new canisters on less busy subnets or adopt a multi-canister architecture that balances the load across subnets. Be mindful to minimize inter-subnet communication for canisters that frequently interact with each other. Additionally, avoid deploying to known high-traffic subnets where possible, though keep in mind that resource usage can change unexpectedly with new dapps.

:::info
When the subnet grows above 450GiB, then the new reservation mechanism activates. Every time a canister allocates new storage bytes, the system sets aside some amount of cycles from the main balance of the canister. These reserved cycles will be used to cover future payments for the newly allocated bytes. The reserved cycles are not transferable, and the amount of reserved cycles depends on how full the subnet is. For example, it may cover days, months, or even years of payments for the newly allocated bytes. It is important to note that the reservation mechanism applies only to the newly allocated bytes and does not apply to the storage already in use by the canister. See more at [resource reservations](https://forum.dfinity.org/t/increasing-subnet-storage-capacity-and-introducing-resource-reservation-mechanism/23447).
When the subnet grows above {{SUBNET_THRESHOLD_GIB}}, then the new reservation mechanism activates. Every time a canister allocates new storage bytes, the system sets aside some amount of cycles from the main balance of the canister. These reserved cycles will be used to cover future payments for the newly allocated bytes. The reserved cycles are not transferable, and the amount of reserved cycles depends on how full the subnet is. For example, it may cover days, months, or even years of payments for the newly allocated bytes. It is important to note that the reservation mechanism applies only to the newly allocated bytes and does not apply to the storage already in use by the canister. See more at [resource reservations](https://forum.dfinity.org/t/increasing-subnet-storage-capacity-and-introducing-resource-reservation-mechanism/23447).
:::

## Handle expensive calls
Expand All @@ -60,4 +60,4 @@ An attacker will target expensive calls to drain the cycles balance or available
- Automatically monitor cycles consumption and set appropriate alerts for cycles consumption rate and balance. Sudden spikes in cycles consumption could indicate an attack.
- Implement early authentication and rate limiting for your canisters.
- Be aware of attacks targeting high cycles-consuming calls.
- See the "Cycle balance drain attacks section" in [How to audit an ICP canister](https://www.joachim-breitner.de/blog/788-How_to_audit_an_Internet_Computer_canister).
- See the "Cycle balance drain attacks section" in [How to audit an ICP canister](https://www.joachim-breitner.de/blog/788-How_to_audit_an_Internet_Computer_canister).
5 changes: 5 additions & 0 deletions docs/building-apps/site-constants.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"SUBNET_CAPACITY_TIB": "2TiB",
"SUBNET_THRESHOLD_GIB": "750GiB"
}

1 change: 1 addition & 0 deletions docusaurus.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ const remarkPlugins = [
simplePlantUML,
require("remark-code-import"),
require("./plugins/remark/validate-links.js"),
[require("./plugins/remark/constants.js"), { constantsPath: "docs/building-apps/site-constants.json" }],
];
const rehypePlugins = [katex];

Expand Down
89 changes: 89 additions & 0 deletions plugins/constants-replacer/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
const fs = require("fs");
const path = require("path");

function readConstants(constantsPath) {
const resolvedPath = path.isAbsolute(constantsPath)
? constantsPath
: path.join(process.cwd(), constantsPath);
const raw = fs.readFileSync(resolvedPath, "utf8");
return JSON.parse(raw);
}

function buildTokenRegex(constantsKeys) {
const escapedKeys = constantsKeys.map((k) => k.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&"));
return new RegExp(`\\\\{\\\\{(${escapedKeys.join("|")})\\\\}\\\\}`, "g");
}

function listFilesRecursive(dir, shouldProcessFile) {
const results = [];
const entries = fs.readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
results.push(...listFilesRecursive(fullPath, shouldProcessFile));
} else if (shouldProcessFile(fullPath)) {
results.push(fullPath);
}
}
return results;
}


module.exports = function constantsReplacerPlugin(context, options) {
const constantsPath = options?.constantsPath || "site-constants.json";
// Only process built routes for building-apps, including versioned paths
const includeBuildSubdir = options?.includeBuildSubdir || "docs";
const includeExtensions = options?.includeExtensions || [
".html",
".js",
".css",
".json",
".xml",
".txt",
];
const excludePaths = options?.excludePaths || [
"/img/",
"/fonts/",
"/static/",
];

return {
name: "constants-replacer",
async postBuild({ outDir }) {
const constants = readConstants(constantsPath);
const keys = Object.keys(constants);
if (keys.length === 0) return;
const tokenRegex = buildTokenRegex(keys);

const shouldProcessFile = (filePath) => {
const rel = path.relative(outDir, filePath);
if (!rel.startsWith(includeBuildSubdir)) return false; // outside /docs
// Allow only these route patterns within /docs
const inBuildingApps =
rel.startsWith("docs/building-apps/") ||
rel.startsWith("docs/next/building-apps/") ||
rel.includes("/docs/version-") && rel.includes("/building-apps/");
if (!inBuildingApps) return false;
if (!includeExtensions.includes(path.extname(filePath))) return false;
return !excludePaths.some((p) => rel.includes(p));
};

const files = listFilesRecursive(outDir, shouldProcessFile);
for (const file of files) {
try {
const content = fs.readFileSync(file, "utf8");
if (!tokenRegex.test(content)) continue;
const replaced = content.replace(tokenRegex, (_, key) => String(constants[key] ?? ""));
if (replaced !== content) {
fs.writeFileSync(file, replaced, "utf8");
}
} catch (err) {
console.warn(`[constants-replacer] Failed to process ${file}:`, err.message);
}
}
},
};
};



66 changes: 66 additions & 0 deletions plugins/remark/constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
const fs = require("fs");
const path = require("path");

function readConstants(constantsPath) {
const resolvedPath = path.isAbsolute(constantsPath)
? constantsPath
: path.join(process.cwd(), constantsPath);
const raw = fs.readFileSync(resolvedPath, "utf8");
return JSON.parse(raw);
}

function buildTokenRegex(constantsKeys) {
const escapedKeys = constantsKeys.map((k) => k.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&"));
return new RegExp(`\\\\{\\\\{(${escapedKeys.join("|")})\\\\}\\\\}`, "g");
}

module.exports = function remarkConstants(options = {}) {
const constantsPath = options.constantsPath || "site-constants.json";
const includePathPrefix = options.includePathPrefix || "docs/building-apps/";
const constants = readConstants(constantsPath);
const keys = Object.keys(constants);
if (keys.length === 0) return () => {};
const tokenRegex = buildTokenRegex(keys);

function replaceInValue(value) {
if (typeof value !== "string" || value.length === 0) return value;
return value.replace(tokenRegex, (_, key) => String(constants[key] ?? ""));
}

return function transformer(tree, file) {
const filePath = file && (file.path || (file.history && file.history[0]));
const isVersioned = filePath && filePath.includes("versioned_docs") && filePath.includes("/building-apps/");
const isCurrent = filePath && filePath.includes(includePathPrefix);
if (filePath && !(isCurrent || isVersioned)) {
return; // skip files outside the allowed area
}
visitNodes(tree, (node) => {
if (node.type === "text" || node.type === "html" || node.type === "inlineCode" || node.type === "code") {
node.value = replaceInValue(node.value);
}
// Replace in MDX JSX element attributes as well
if (node.type === "mdxJsxFlowElement" || node.type === "mdxJsxTextElement") {
if (Array.isArray(node.attributes)) {
for (const attr of node.attributes) {
if (attr && attr.type === "mdxJsxAttribute") {
if (typeof attr.value === "string") {
attr.value = replaceInValue(attr.value);
} else if (attr.value && typeof attr.value.value === "string") {
attr.value.value = replaceInValue(attr.value.value);
}
}
}
}
}
});
};
};

function visitNodes(node, visitor) {
visitor(node);
const children = node.children || [];
for (const child of children) visitNodes(child, visitor);
}