Skip to content

Commit 81dce50

Browse files
Support 128-bit system API for cycles (#54)
* Support system API functions returning cycles as a 128 bit number * Add call_cycles_add128 * Get rid of compiler warnings
1 parent 62be3c6 commit 81dce50

File tree

8 files changed

+178
-22
lines changed

8 files changed

+178
-22
lines changed

default.nix

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ let universal-canister = (naersk.buildPackage rec {
1111
name = "universal-canister";
1212
src = subpath ./universal-canister;
1313
root = ./universal-canister;
14-
CARGO_TARGET_WASM32_UNKNOWN_UNKNOWN_LINKER = "${nixpkgs.llvmPackages_11.lld}/bin/lld";
15-
RUSTFLAGS = "-C link-arg=-s"; # much smaller wasm
14+
CARGO_TARGET_WASM32_UNKNOWN_UNKNOWN_LINKER = "${nixpkgs.llvmPackages_12.lld}/bin/lld";
15+
RUSTFLAGS = "-C link-arg=-s -C target-feature=+multivalue"; # much smaller wasm
1616
cargoBuildOptions = x : x ++ [ "--target wasm32-unknown-unknown" ];
1717
doCheck = false;
1818
release = true;

src/IC/Canister/Imp.hs

Lines changed: 46 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
{-# LANGUAGE TypeApplications #-}
66
{-# LANGUAGE GADTs #-}
77
{-# LANGUAGE FlexibleContexts #-}
8+
{-# LANGUAGE NumericUnderscores #-}
89

910
{-|
1011
The canister interface, presented imperatively (or impurely), i.e. without rollback
@@ -33,6 +34,7 @@ import qualified Data.ByteString.Lazy.UTF8 as BSU
3334
import Control.Monad.Primitive
3435
import Control.Monad.ST
3536
import Control.Monad.Except
37+
import Data.Bits
3638
import Data.STRef
3739
import Data.Maybe
3840
import Data.Int -- TODO: Should be Word32 in most cases
@@ -204,10 +206,16 @@ systemAPI esref =
204206
, toImport "ic0" "msg_cycles_accept" msg_cycles_accept
205207
, toImport "ic0" "canister_cycle_balance" canister_cycle_balance
206208

209+
, toImport "ic0" "msg_cycles_available128" msg_cycles_available128
210+
, toImport "ic0" "msg_cycles_refunded128" msg_cycles_refunded128
211+
, toImport "ic0" "msg_cycles_accept128" msg_cycles_accept128
212+
, toImport "ic0" "canister_cycle_balance128" canister_cycle_balance128
213+
207214
, toImport "ic0" "call_new" call_new
208215
, toImport "ic0" "call_on_cleanup" call_on_cleanup
209216
, toImport "ic0" "call_data_append" call_data_append
210217
, toImport "ic0" "call_cycles_add" call_cycles_add
218+
, toImport "ic0" "call_cycles_add128" call_cycles_add128
211219
, toImport "ic0" "call_perform" call_perform
212220

213221
, toImport "ic0" "stable_size" stable_size
@@ -335,27 +343,52 @@ systemAPI esref =
335343
Stopping -> 2
336344
Stopped -> 3
337345

346+
splitBitsIntoHalves :: Natural -> (Word64, Word64)
347+
splitBitsIntoHalves n = (fromIntegral $ highBits n, fromIntegral $ lowBits n)
348+
where highBits = flip shiftR 64
349+
lowBits = (0xFFFFFFFF_FFFFFFFF .&.)
350+
351+
combineBitHalves :: (Word64, Word64) -> Natural
352+
combineBitHalves (high, low) = fromIntegral high `shiftL` 64 .|. fromIntegral low
353+
354+
low64BitsOrErr :: (Word64, Word64) -> HostM s Word64
355+
low64BitsOrErr (0, low) = return low
356+
low64BitsOrErr (high, low) = throwError $ "The number of cycles does not fit in 64 bits: " ++ show (combineBitHalves (high, low))
357+
338358
msg_cycles_refunded :: () -> HostM s Word64
339-
msg_cycles_refunded () = fromIntegral <$> getRefunded esref
359+
msg_cycles_refunded () = msg_cycles_refunded128 () >>= low64BitsOrErr
340360

341361
msg_cycles_available :: () -> HostM s Word64
342-
msg_cycles_available () = fromIntegral <$> getAvailable esref
362+
msg_cycles_available () = msg_cycles_available128 () >>= low64BitsOrErr
343363

344364
msg_cycles_accept :: Word64 -> HostM s Word64
345-
msg_cycles_accept max_amount = do
346-
available <- fromIntegral <$> getAvailable esref
365+
msg_cycles_accept max_amount = msg_cycles_accept128 (0, max_amount) >>= low64BitsOrErr
366+
367+
canister_cycle_balance :: () -> HostM s Word64
368+
canister_cycle_balance () = canister_cycle_balance128 () >>= low64BitsOrErr
369+
370+
msg_cycles_refunded128 :: () -> HostM s (Word64, Word64)
371+
msg_cycles_refunded128 () = splitBitsIntoHalves <$> getRefunded esref
372+
373+
msg_cycles_available128 :: () -> HostM s (Word64, Word64)
374+
msg_cycles_available128 () = splitBitsIntoHalves <$> getAvailable esref
375+
376+
msg_cycles_accept128 :: (Word64, Word64) -> HostM s (Word64, Word64)
377+
msg_cycles_accept128 (max_amount_high, max_amount_low) = do
378+
available <- getAvailable esref
347379
balance <- gets balance
380+
let max_amount = combineBitHalves (max_amount_high, max_amount_low)
348381
let amount = minimum
349-
[ fromIntegral max_amount
382+
[ max_amount
350383
, available
351384
, cMAX_CANISTER_BALANCE - balance]
352385
subtractAvailable esref amount
353386
addBalance esref amount
354387
addAccepted esref amount
355-
return (fromIntegral amount)
388+
return $ splitBitsIntoHalves amount
356389

357-
canister_cycle_balance :: () -> HostM s Word64
358-
canister_cycle_balance () = fromIntegral <$> gets balance
390+
canister_cycle_balance128 :: () -> HostM s (Word64, Word64)
391+
canister_cycle_balance128 () = splitBitsIntoHalves <$> gets balance
359392

360393
call_new :: ( Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32 ) -> HostM s ()
361394
call_new ( callee_src, callee_size, name_src, name_size
@@ -388,8 +421,11 @@ systemAPI esref =
388421
changePendingCall $ \pc -> return $ pc { call_arg = call_arg pc <> arg }
389422

390423
call_cycles_add :: Word64 -> HostM s ()
391-
call_cycles_add amount = do
392-
let cycles = fromIntegral amount
424+
call_cycles_add amount = call_cycles_add128 (0, amount)
425+
426+
call_cycles_add128 :: (Word64, Word64) -> HostM s ()
427+
call_cycles_add128 amount = do
428+
let cycles = combineBitHalves amount
393429
changePendingCall $ \pc -> do
394430
subtractBalance esref cycles
395431
return $ pc { call_transferred_cycles = call_transferred_cycles pc + cycles }

src/IC/Constants.hs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@ module IC.Constants where
33
import Numeric.Natural
44

55
cMAX_CANISTER_BALANCE :: Natural
6-
cMAX_CANISTER_BALANCE = 2^(60::Int)
6+
cMAX_CANISTER_BALANCE = 2^(120::Int)

src/IC/Test/Agent.hs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ import Data.Time.Clock.POSIX
5757
import Codec.Candid (Principal(..), prettyPrincipal)
5858
import qualified Data.Binary.Get as Get
5959
import qualified Codec.Candid as Candid
60+
import Data.Bits
6061
import Data.Row
6162
import qualified Data.Row.Records as R
6263
import qualified Data.Row.Variants as V
@@ -526,6 +527,9 @@ asWord64 = runGet Get.getWord64le
526527
as2Word64 :: HasCallStack => Blob -> IO (Word64, Word64)
527528
as2Word64 = runGet $ (,) <$> Get.getWord64le <*> Get.getWord64le
528529

530+
asPairWord64 :: HasCallStack => Blob -> IO (Word64, Word64)
531+
asPairWord64 = runGet $ flip (,) <$> Get.getWord64le <*> Get.getWord64le
532+
529533
bothSame :: (Eq a, Show a) => (a, a) -> Assertion
530534
bothSame (x,y) = x @?= y
531535

@@ -839,3 +843,6 @@ textual = T.unpack . prettyPrincipal . Principal
839843
shorten :: Int -> String -> String
840844
shorten n s = a ++ (if null b then "" else "")
841845
where (a,b) = splitAt n s
846+
847+
toI128 :: (Word64, Word64) -> Natural
848+
toI128 (high, low) = fromIntegral high `shiftL` 64 .|. fromIntegral low

src/IC/Test/Spec.hs

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -643,10 +643,14 @@ icTests = withAgentConfig $ testGroup "Interface Spec acceptance tests"
643643
, t "msg_reply" never reply -- due to double reply
644644
, t "msg_reject" never $ reject "rejecting" -- due to double reply
645645
, t "msg_cycles_available" "U Rt Ry" $ ignore getAvailableCycles
646+
, t "msg_cycles_available128" "U Rt Ry" $ ignore getAvailableCycles128
646647
, t "msg_cycles_refunded" "Rt Ry" $ ignore getRefund
648+
, t "msg_cycles_refunded128" "Rt Ry" $ ignore getRefund128
647649
, t "msg_cycles_accept" "U Rt Ry" $ ignore (acceptCycles (int64 0))
650+
, t "msg_cycles_accept128" "U Rt Ry" $ ignore (acceptCycles128 (int64 0) (int64 0))
648651
, t "canister_self" star $ ignore self
649652
, t "canister_cycle_balance" star $ ignore getBalance
653+
, t "canister_cycle_balance128" star $ ignore getBalance128
650654
, t "call_new…call_perform" "U Rt Ry H" $
651655
callNew "foo" "bar" "baz" "quux" >>>
652656
callDataAppend "foo" >>>
@@ -655,6 +659,7 @@ icTests = withAgentConfig $ testGroup "Interface Spec acceptance tests"
655659
, t "call_set_cleanup" never $ callOnCleanup (callback noop)
656660
, t "call_data_append" never $ callDataAppend "foo"
657661
, t "call_cycles_add" never $ callCyclesAdd (int64 0)
662+
, t "call_cycles_add128" never $ callCyclesAdd128 (int64 0) (int64 0)
658663
, t "call_perform" never callPerform
659664
, t "stable_size" star $ ignore stableSize
660665
, t "stable_grow" star $ ignore $ stableGrow (int 1)
@@ -1542,12 +1547,14 @@ icTests = withAgentConfig $ testGroup "Interface Spec acceptance tests"
15421547

15431548
, testGroup "cycles" $
15441549
let replyBalance = replyData (i64tob getBalance)
1550+
replyBalance128 = replyData (pairToB getBalance128)
15451551
rememberBalance =
15461552
ignore (stableGrow (int 1)) >>>
15471553
stableWrite (int 0) (i64tob getBalance)
15481554
recallBalance = replyData (stableRead (int 0) (int 8))
15491555
acceptAll = ignore (acceptCycles getAvailableCycles)
15501556
queryBalance cid = query cid replyBalance >>= asWord64
1557+
queryBalance128 cid = query cid replyBalance128 >>= asPairWord64
15511558

15521559
-- At the time of writing, creating a canister needs at least 1T
15531560
-- and the freezing limit is 5T
@@ -1579,7 +1586,19 @@ icTests = withAgentConfig $ testGroup "Interface Spec acceptance tests"
15791586
ic_install (ic00via cid) (enum #install) cid2 universal_wasm (run noop)
15801587
return cid2
15811588
in
1582-
[ testGroup "can use balance API" $
1589+
[ testGroup "cycles API - backward compatibility" $
1590+
[ simpleTestCase "canister_cycle_balance = canister_cycle_balance128 for numbers fitting in 64 bits" $ \cid -> do
1591+
a <- queryBalance cid
1592+
b <- queryBalance128 cid
1593+
bothSame ((0,a),b)
1594+
, testCase "legacy API traps when a result is too big" $ do
1595+
cid <- create noop
1596+
let large = 2^(65::Int)
1597+
ic_top_up ic00 cid large
1598+
query' cid replyBalance >>= isReject [5]
1599+
toI128 <$> queryBalance128 cid >>= isRoughly (large + fromIntegral def_cycles)
1600+
]
1601+
, testGroup "can use balance API" $
15831602
let getBalanceTwice = join cat (i64tob getBalance)
15841603
test = replyData getBalanceTwice
15851604
in
@@ -1606,8 +1625,8 @@ icTests = withAgentConfig $ testGroup "Interface Spec acceptance tests"
16061625
call cid (replyData (i64tob (acceptCycles (int64 0)))) >>= asWord64 >>= is 0
16071626
, simpleTestCase "can accept more than available cycles" $ \cid ->
16081627
call cid (replyData (i64tob (acceptCycles (int64 1)))) >>= asWord64 >>= is 0
1609-
, simpleTestCase "cant accept absurd amount of cycles" $ \cid ->
1610-
call cid (replyData (i64tob (acceptCycles (int64 maxBound)))) >>= asWord64 >>= is 0
1628+
, simpleTestCase "can accept absurd amount of cycles" $ \cid ->
1629+
call cid (replyData (pairToB (acceptCycles128 (int64 maxBound) (int64 maxBound)))) >>= asPairWord64 >>= is (0,0)
16111630

16121631
, testGroup "provisional_create_canister_with_cycles"
16131632
[ testCase "balance as expected" $ do
@@ -1617,13 +1636,13 @@ icTests = withAgentConfig $ testGroup "Interface Spec acceptance tests"
16171636
, testCaseSteps "default (i.e. max) balance" $ \step -> do
16181637
cid <- ic_provisional_create ic00 Nothing empty
16191638
installAt cid noop
1620-
cycles <- queryBalance cid
1639+
cycles <- queryBalance128 cid
16211640
step $ "Cycle balance now at " ++ show cycles
16221641

16231642
, testCaseSteps "> 2^128 succeeds" $ \step -> do
16241643
cid <- ic_provisional_create ic00 (Just (10 * 2^(128::Int))) empty
16251644
installAt cid noop
1626-
cycles <- queryBalance cid
1645+
cycles <- queryBalance128 cid
16271646
step $ "Cycle balance now at " ++ show cycles
16281647
]
16291648

@@ -1824,7 +1843,7 @@ icTests = withAgentConfig $ testGroup "Interface Spec acceptance tests"
18241843
, testCaseSteps "more than 2^128" $ \step -> do
18251844
cid <- create noop
18261845
ic_top_up ic00 cid (10 * 2^(128::Int))
1827-
cycles <- queryBalance cid
1846+
cycles <- queryBalance128 cid
18281847
step $ "Cycle balance now at " ++ show cycles
18291848
, testCase "nonexisting canister" $ do
18301849
ic_top_up' ic00 doesn'tExist (fromIntegral def_cycles)
@@ -2349,7 +2368,7 @@ install prog = do
23492368
return cid
23502369

23512370
create :: (HasCallStack, HasAgentConfig) => IO Blob
2352-
create = ic_provisional_create ic00 Nothing empty
2371+
create = ic_provisional_create ic00 (Just (2^(60::Int))) empty
23532372

23542373
upgrade' :: (HasCallStack, HasAgentConfig) => Blob -> Prog -> IO ReqResponse
23552374
upgrade' cid prog = do

src/IC/Test/Universal.hs

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,9 @@ import Data.ByteString.Builder
2424
import Data.Word
2525
import Data.String
2626

27-
-- The types of our little language are i32, i64 and blobs
27+
-- The types of our little language are i32, i64, pair of i64s and blobs
2828

29-
data T = I | I64 | B
29+
data T = I | I64 | B | PairI64
3030

3131

3232
-- We deal with expressions (return a value, thus have a type) and programs (do
@@ -228,6 +228,24 @@ onHeartbeat = op 49
228228
performanceCounter :: Exp 'I64
229229
performanceCounter = op 50
230230

231+
getBalance128 :: Exp 'PairI64
232+
getBalance128 = op 51
233+
234+
getAvailableCycles128 :: Exp 'PairI64
235+
getAvailableCycles128 = op 52
236+
237+
getRefund128 :: Exp 'PairI64
238+
getRefund128 = op 53
239+
240+
acceptCycles128 :: Exp 'I64 -> Exp 'I64 -> Exp 'PairI64
241+
acceptCycles128 = op 54
242+
243+
callCyclesAdd128 :: Exp 'I64 -> Exp 'I64 -> Prog
244+
callCyclesAdd128 = op 55
245+
246+
pairToB :: Exp 'PairI64 -> Exp 'B
247+
pairToB = op 56
248+
231249
-- Some convenience combinators
232250

233251
-- This allows us to write byte expressions as plain string literals

universal-canister/src/api.rs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,19 @@ extern crate wee_alloc;
44
#[global_allocator]
55
static ALLOC: wee_alloc::WeeAlloc<'_> = wee_alloc::WeeAlloc::INIT;
66

7+
#[repr(C)]
8+
// Note: tuples are not FFI-safe causing the compiler to complain. To avoid this,
9+
// we represent pair as a tuple struct which has known memory layout and the same semantics as
10+
// a plain pair.
11+
pub struct Pair(pub u64, pub u64);
12+
713
mod ic0 {
14+
use api::Pair;
815
#[link(wasm_import_module = "ic0")]
916
extern "C" {
1017
pub fn accept_message() -> ();
1118
pub fn canister_cycle_balance() -> u64;
19+
pub fn canister_cycle_balance128() -> Pair;
1220
pub fn canister_self_copy(dst: u32, offset: u32, size: u32) -> ();
1321
pub fn canister_self_size() -> u32;
1422
pub fn canister_status() -> u32;
@@ -20,6 +28,9 @@ mod ic0 {
2028
pub fn msg_cycles_accept(max_amount: u64) -> u64;
2129
pub fn msg_cycles_available() -> u64;
2230
pub fn msg_cycles_refunded() -> u64;
31+
pub fn msg_cycles_accept128(max_amount_high: u64, max_amount_low: u64) -> Pair;
32+
pub fn msg_cycles_available128() -> Pair;
33+
pub fn msg_cycles_refunded128() -> Pair;
2334
pub fn msg_method_name_copy(dst: u32, offset: u32, size: u32) -> ();
2435
pub fn msg_method_name_size() -> u32;
2536
pub fn msg_reject_code() -> u32;
@@ -42,6 +53,7 @@ mod ic0 {
4253
pub fn call_on_cleanup(fun: u32, env: u32) -> ();
4354
pub fn call_data_append(src: u32, size: u32) -> ();
4455
pub fn call_cycles_add(amount: u64) -> ();
56+
pub fn call_cycles_add128(amount_high: u64, amount_low: u64) -> ();
4557
pub fn call_perform() -> u32;
4658
pub fn stable_size() -> u32;
4759
pub fn stable_grow(additional_pages: u32) -> u32;
@@ -101,6 +113,12 @@ pub fn call_cycles_add(amount: u64) {
101113
}
102114
}
103115

116+
pub fn call_cycles_add128(amount_high: u64, amount_low: u64) {
117+
unsafe {
118+
ic0::call_cycles_add128(amount_high, amount_low);
119+
}
120+
}
121+
104122
pub fn call_perform() -> u32 {
105123
unsafe { ic0::call_perform() }
106124
}
@@ -176,18 +194,34 @@ pub fn cycles_available() -> u64 {
176194
unsafe { ic0::msg_cycles_available() }
177195
}
178196

197+
pub fn cycles_available128() -> Pair{
198+
unsafe { ic0::msg_cycles_available128() }
199+
}
200+
179201
pub fn cycles_refunded() -> u64 {
180202
unsafe { ic0::msg_cycles_refunded() }
181203
}
182204

205+
pub fn cycles_refunded128() -> Pair {
206+
unsafe { ic0::msg_cycles_refunded128() }
207+
}
208+
183209
pub fn accept(amount: u64) -> u64 {
184210
unsafe { ic0::msg_cycles_accept(amount) }
185211
}
186212

213+
pub fn accept128(high: u64, low: u64) -> Pair {
214+
unsafe { ic0::msg_cycles_accept128(high, low) }
215+
}
216+
187217
pub fn balance() -> u64 {
188218
unsafe { ic0::canister_cycle_balance() }
189219
}
190220

221+
pub fn balance128() -> Pair {
222+
unsafe { ic0::canister_cycle_balance128() }
223+
}
224+
191225
pub fn stable_size() -> u32 {
192226
unsafe { ic0::stable_size() }
193227
}

0 commit comments

Comments
 (0)