Skip to content

Commit 91dc028

Browse files
raymondkviviveevee
andauthored
chore: merge asset canister feature from master (#4430)
* feat!: verifiable asset canister state (#4428) * chore: update changelog * fix: handle asset canister pagination (#4432) * chore: update changelog (#4431) --------- Co-authored-by: Vivienne Siffert <[email protected]>
1 parent bfc6523 commit 91dc028

File tree

22 files changed

+1459
-75
lines changed

22 files changed

+1459
-75
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ result*
22
e2e/assets/installed/
33
e2e/tests-dfx/.bats/
44

5+
.cursor
6+
57
# Building
68
build/
79
target/

CHANGELOG.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44

55
# 0.30.1
66

7+
### feat: asset sync now prints the target asset canister state hash in `--verbose` mode
8+
9+
If an asset canister is updated and `--verbose` is enabled, `dfx` will now print the state hash of the local assets before syncing. Calling `compute_state_hash` on the asset canister after syncing will eventually return the same hash.
10+
711
### feat: support dogecoin for the local dev environment
812

913
You can now launch a network with `dfx start --enable-dogeoin` to run the dogecoin
@@ -40,6 +44,22 @@ This incorporates the following executed proposals:
4044
- [139192](https://dashboard.internetcomputer.org/proposal/139192)
4145
- [139079](https://dashboard.internetcomputer.org/proposal/139079)
4246

47+
### Frontend canister
48+
49+
#### feat: `list` returns more info about assets
50+
51+
Asset info now contains the fields `max_age: opt nat64;`, `headers: opt vec HeaderField;`, `allow_raw_access: opt bool;`, and ``is_aliased: opt bool;` in addition to the previously returned ones.
52+
53+
#### feat!: `list` is now paginated
54+
55+
`list` now returns info about up to 100 assets instead of all assets in the canister. `start` allows specifying the offset at which the list of assets should start. `length` allows specifying a smaller limit if e.g. headers are too large to return the default number of assets. The full argument to `list` is now `(record { start: opt nat; length: opt nat })`.
56+
57+
#### feat: `compute_state_hash`
58+
59+
The function `compute_state_hash` works similar to `compute_evidence`, but instead of computing a hash over a batch of changes, it computes a hash over the full asset canister content. This can be used to verify the integrity of assets e.g. between a live and a local deployment. (This will only work if builds are deterministic. If there are e.g. timestamps hidden in filenames then hashes will not match.)
60+
61+
- Module hash: 51e80aa7ecbb94ba477bbc910c934794db674d9c441c3f013b8e09390facb389
62+
- https://github.com/dfinity/sdk/pull/4428
4363

4464
# 0.30.0
4565

e2e/tests-dfx/assetscanister.bash

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1063,6 +1063,53 @@ CHERRIES" "$stdout"
10631063
assert_match 'length = 24'
10641064
}
10651065

1066+
@test "asset sync works with more than 100 assets" {
1067+
install_asset assetscanister
1068+
1069+
dfx_start
1070+
dfx canister create --all
1071+
dfx build
1072+
dfx canister install e2e_project_frontend
1073+
1074+
# Create 150 assets to test that pagination works correctly during sync
1075+
for i in $(seq 1 150); do
1076+
echo "test content $i" > "src/e2e_project_frontend/assets/test$(printf "%03d" "$i").txt"
1077+
done
1078+
1079+
# Initial deploy should sync all 150 assets
1080+
assert_command dfx deploy -v
1081+
assert_match 'test001.txt.*1/1'
1082+
assert_match 'test100.txt.*1/1'
1083+
assert_match 'test150.txt.*1/1'
1084+
1085+
# Verify all assets were uploaded by checking a few across the range
1086+
assert_command dfx canister call --query e2e_project_frontend get '(record{key="/test001.txt";accept_encodings=vec{"identity"}})'
1087+
assert_match "test content 1"
1088+
assert_command dfx canister call --query e2e_project_frontend get '(record{key="/test100.txt";accept_encodings=vec{"identity"}})'
1089+
assert_match "test content 100"
1090+
assert_command dfx canister call --query e2e_project_frontend get '(record{key="/test150.txt";accept_encodings=vec{"identity"}})'
1091+
assert_match "test content 150"
1092+
1093+
# Modify only one asset
1094+
echo "modified content 075" > "src/e2e_project_frontend/assets/test075.txt"
1095+
1096+
# Redeploy - should only sync the modified asset
1097+
assert_command dfx deploy -v
1098+
assert_match 'test075.txt.*1/1'
1099+
assert_match 'test001.txt.*is already installed'
1100+
assert_match 'test150.txt.*is already installed'
1101+
1102+
# Verify the modified asset was updated
1103+
assert_command dfx canister call --query e2e_project_frontend get '(record{key="/test075.txt";accept_encodings=vec{"identity"}})'
1104+
assert_match "modified content 075"
1105+
1106+
# Verify other assets remain unchanged
1107+
assert_command dfx canister call --query e2e_project_frontend get '(record{key="/test074.txt";accept_encodings=vec{"identity"}})'
1108+
assert_match "test content 74"
1109+
assert_command dfx canister call --query e2e_project_frontend get '(record{key="/test076.txt";accept_encodings=vec{"identity"}})'
1110+
assert_match "test content 76"
1111+
}
1112+
10661113
@test "identifies content type" {
10671114
install_asset assetscanister
10681115

@@ -2244,3 +2291,53 @@ EOF
22442291
assert_command curl -v "http://$ID.localhost:$PORT/app.html"
22452292
assert_match "set-cookie: $IC_ENV_COOKIE_REGEX_2"
22462293
}
2294+
2295+
@test "local state hash matches canister state hash" {
2296+
install_asset assetscanister
2297+
dfx_start
2298+
2299+
# Create some test assets
2300+
echo "test file 1" > src/e2e_project_frontend/assets/test1.txt
2301+
echo "test file 2" > src/e2e_project_frontend/assets/test2.txt
2302+
mkdir -p src/e2e_project_frontend/assets/subdir
2303+
echo "nested file" > src/e2e_project_frontend/assets/subdir/nested.txt
2304+
2305+
# Deploy with debug logging to capture local state hash
2306+
assert_command dfx deploy --verbose
2307+
LOCAL_HASH=$(echo "$stderr" | grep "Computed state hash of assets:" | sed 's/.*Computed state hash of assets: //')
2308+
2309+
# Call the canister's compute_state_hash method
2310+
assert_command dfx canister call e2e_project_frontend compute_state_hash '()'
2311+
CANISTER_HASH=$(echo "$stdout" | grep -o '"[0-9a-f]*"' | tr -d '"')
2312+
2313+
# Verify hashes
2314+
echo "Local hash: $LOCAL_HASH"
2315+
echo "Canister hash: $CANISTER_HASH"
2316+
assert_eq "${#LOCAL_HASH}" "64"
2317+
assert_eq "${#CANISTER_HASH}" "64"
2318+
assert_eq "$LOCAL_HASH" "$CANISTER_HASH"
2319+
2320+
# Now modify an existing asset and add a new one
2321+
echo "modified test file 1" > src/e2e_project_frontend/assets/test1.txt
2322+
echo "brand new file" > src/e2e_project_frontend/assets/test3.txt
2323+
mkdir -p src/e2e_project_frontend/assets/another
2324+
echo "another nested file" > src/e2e_project_frontend/assets/another/deep.txt
2325+
2326+
# Redeploy with the changes
2327+
assert_command dfx deploy --verbose
2328+
LOCAL_HASH_2=$(echo "$stderr" | grep "Computed state hash of assets:" | sed 's/.*Computed state hash of assets: //')
2329+
2330+
# Call the canister's compute_state_hash method again
2331+
assert_command dfx canister call e2e_project_frontend compute_state_hash '()'
2332+
CANISTER_HASH_2=$(echo "$stdout" | grep -o '"[0-9a-f]*"' | tr -d '"')
2333+
2334+
# Verify new hashes
2335+
echo "Local hash after changes: $LOCAL_HASH_2"
2336+
echo "Canister hash after changes: $CANISTER_HASH_2"
2337+
assert_eq "${#LOCAL_HASH_2}" "64"
2338+
assert_eq "${#CANISTER_HASH_2}" "64"
2339+
assert_eq "$LOCAL_HASH_2" "$CANISTER_HASH_2"
2340+
2341+
# Verify the hash changed after modifications
2342+
assert_neq "$LOCAL_HASH" "$LOCAL_HASH_2"
2343+
}

e2e/tests-icx-asset/icx-asset.bash

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,31 @@ icx_asset_upload() {
6363
assert_match "notreally.js.*text/javascript.*identity"
6464
}
6565

66+
@test "lists all assets when more than 100" {
67+
# Create 150 assets to test pagination
68+
for i in $(seq 1 150); do
69+
echo "test content $i" > "src/e2e_project_frontend/assets/test$(printf "%03d" "$i").txt"
70+
done
71+
icx_asset_sync
72+
73+
icx_asset_list
74+
75+
# Verify we get assets from the first page
76+
assert_match "test001.txt.*text/plain.*identity"
77+
assert_match "test050.txt.*text/plain.*identity"
78+
assert_match "test100.txt.*text/plain.*identity"
79+
80+
# Verify we get assets from the second page (beyond first 100)
81+
assert_match "test101.txt.*text/plain.*identity"
82+
assert_match "test125.txt.*text/plain.*identity"
83+
assert_match "test150.txt.*text/plain.*identity"
84+
85+
# Count total number of test*.txt assets listed
86+
# shellcheck disable=SC2154
87+
TEST_COUNT=$(echo "$stderr" | grep -c "test[0-9][0-9][0-9].txt")
88+
assert_eq "150" "$TEST_COUNT"
89+
}
90+
6691
@test "creates new files" {
6792
echo "new file content" >src/e2e_project_frontend/assets/new-asset.txt
6893
icx_asset_sync

src/canisters/frontend/ic-asset/src/batch_upload/plumbing.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ const CONTENT_ENCODING_IDENTITY: &str = "identity";
3030
// Any file counts as at least 1 mb.
3131
const MAX_COST_SINGLE_FILE_MB: usize = 45;
3232

33-
const MAX_CHUNK_SIZE: usize = 1_900_000;
33+
pub(crate) const MAX_CHUNK_SIZE: usize = 1_900_000;
3434

3535
#[derive(Clone, Debug)]
3636
pub(crate) struct AssetDescriptor {
Lines changed: 46 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,58 @@
11
use crate::canister_api::methods::method_names::LIST;
22
use crate::canister_api::types::{asset::AssetDetails, list::ListAssetsRequest};
3+
use candid::Nat;
34
use ic_agent::AgentError;
45
use ic_utils::Canister;
56
use ic_utils::call::SyncCall;
67
use std::collections::HashMap;
78

8-
pub(crate) async fn list_assets(
9+
/// Lists all assets in the canister. Handles pagination transparently.
10+
pub async fn list_assets(
911
canister: &Canister<'_>,
1012
) -> Result<HashMap<String, AssetDetails>, AgentError> {
11-
let (entries,): (Vec<AssetDetails>,) = canister
12-
.query(LIST)
13-
.with_arg(ListAssetsRequest {})
14-
.build()
15-
.call()
16-
.await?;
17-
18-
let assets: HashMap<_, _> = entries.into_iter().map(|d| (d.key.clone(), d)).collect();
13+
let mut all_entries: Vec<AssetDetails> = Vec::new();
14+
let mut start = 0u64;
15+
let mut prev_page_size: Option<usize> = None;
16+
17+
// Fetch assets in pages until we get 0 items or fewer items than the previous page
18+
loop {
19+
let (entries,): (Vec<AssetDetails>,) = canister
20+
.query(LIST)
21+
.with_arg(ListAssetsRequest {
22+
start: Some(Nat::from(start)),
23+
length: None,
24+
})
25+
.build()
26+
.call()
27+
.await?;
28+
29+
let num_entries = entries.len();
30+
if num_entries == 0 {
31+
break;
32+
}
33+
34+
// If we're on a subsequent page but got the same data as the first page,
35+
// the canister doesn't support pagination and is returning all entries every time
36+
if start > 0 && entries == all_entries {
37+
break;
38+
}
39+
40+
start += num_entries as u64;
41+
all_entries.extend(entries);
42+
43+
// If we got fewer items than the previous page, we've reached the end
44+
if let Some(prev_size) = prev_page_size {
45+
if num_entries < prev_size {
46+
break;
47+
}
48+
}
49+
prev_page_size = Some(num_entries);
50+
}
51+
52+
let assets: HashMap<_, _> = all_entries
53+
.into_iter()
54+
.map(|d| (d.key.clone(), d))
55+
.collect();
1956

2057
Ok(assets)
2158
}

src/canisters/frontend/ic-asset/src/canister_api/types/asset.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use serde::Deserialize;
33
use std::collections::HashMap;
44

55
/// Information about a content encoding stored for an asset.
6-
#[derive(CandidType, Debug, Deserialize)]
6+
#[derive(CandidType, Debug, Deserialize, PartialEq, Eq)]
77
pub struct AssetEncodingDetails {
88
/// A content encoding, such as "gzip".
99
pub content_encoding: String,
@@ -14,7 +14,7 @@ pub struct AssetEncodingDetails {
1414
}
1515

1616
/// Information about an asset stored in the canister.
17-
#[derive(CandidType, Debug, Deserialize)]
17+
#[derive(CandidType, Debug, Deserialize, PartialEq, Eq)]
1818
pub struct AssetDetails {
1919
/// The key identifies the asset.
2020
pub key: String,
Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1-
use candid::CandidType;
1+
use candid::{CandidType, Nat};
22

33
/// Return a list of all assets in the canister.
44
#[derive(CandidType, Debug)]
5-
pub struct ListAssetsRequest {}
5+
pub struct ListAssetsRequest {
6+
pub start: Option<Nat>,
7+
pub length: Option<Nat>,
8+
}

0 commit comments

Comments
 (0)