From f0bedbc04c720080eed3de75802b5218c8c407cb Mon Sep 17 00:00:00 2001 From: Marcin Dziadus Date: Fri, 29 Oct 2021 11:54:55 +0200 Subject: [PATCH 1/6] Support system API functions returning cycles as a 128 bit number --- default.nix | 4 +-- src/IC/Canister/Imp.hs | 47 ++++++++++++++++++++++++++++------ src/IC/Test/Agent.hs | 3 +++ src/IC/Test/Spec.hs | 11 +++++--- src/IC/Test/Universal.hs | 19 ++++++++++++-- universal-canister/src/api.rs | 20 +++++++++++++++ universal-canister/src/main.rs | 36 ++++++++++++++++++++++++++ 7 files changed, 125 insertions(+), 15 deletions(-) diff --git a/default.nix b/default.nix index 9bf759d1..f8f929c6 100644 --- a/default.nix +++ b/default.nix @@ -11,8 +11,8 @@ let universal-canister = (naersk.buildPackage rec { name = "universal-canister"; src = subpath ./universal-canister; root = ./universal-canister; - CARGO_TARGET_WASM32_UNKNOWN_UNKNOWN_LINKER = "${nixpkgs.llvmPackages_11.lld}/bin/lld"; - RUSTFLAGS = "-C link-arg=-s"; # much smaller wasm + CARGO_TARGET_WASM32_UNKNOWN_UNKNOWN_LINKER = "${nixpkgs.llvmPackages_12.lld}/bin/lld"; + RUSTFLAGS = "-C link-arg=-s -C target-feature=+multivalue"; # much smaller wasm cargoBuildOptions = x : x ++ [ "--target wasm32-unknown-unknown" ]; doCheck = false; release = true; diff --git a/src/IC/Canister/Imp.hs b/src/IC/Canister/Imp.hs index 2f418d65..cc00d8d3 100644 --- a/src/IC/Canister/Imp.hs +++ b/src/IC/Canister/Imp.hs @@ -33,6 +33,7 @@ import qualified Data.ByteString.Lazy.UTF8 as BSU import Control.Monad.Primitive import Control.Monad.ST import Control.Monad.Except +import Data.Bits import Data.STRef import Data.Maybe import Data.Int -- TODO: Should be Word32 in most cases @@ -204,6 +205,11 @@ systemAPI esref = , toImport "ic0" "msg_cycles_accept" msg_cycles_accept , toImport "ic0" "canister_cycle_balance" canister_cycle_balance + , toImport "ic0" "msg_cycles_available128" msg_cycles_available128 + , toImport "ic0" "msg_cycles_refunded128" msg_cycles_refunded128 + , toImport "ic0" "msg_cycles_accept128" msg_cycles_accept128 + , toImport "ic0" "canister_cycle_balance128" canister_cycle_balance128 + , toImport "ic0" "call_new" call_new , toImport "ic0" "call_on_cleanup" call_on_cleanup , toImport "ic0" "call_data_append" call_data_append @@ -335,27 +341,52 @@ systemAPI esref = Stopping -> 2 Stopped -> 3 + splitIntoHighAndLowBits :: Natural -> (Word64, Word64) + splitIntoHighAndLowBits n = (fromIntegral $ highBits n, fromIntegral $ lowBits n) + where highBits = flip shiftR 64 + lowBits = (.&.) (1 `shiftL` 64 - 1) + + combineHighAndLowBits :: (Natural, Natural) -> Natural + combineHighAndLowBits (high, low) = 2^(64 :: Int) * high + low + + low64BitsOrErr :: (Word64, Word64) -> HostM s Word64 + low64BitsOrErr (0, low) = return low + low64BitsOrErr (high, low) = throwError $ "The number of cycles does not fit in 64 bits: " ++ show (combineHighAndLowBits (fromIntegral high, fromIntegral low)) + msg_cycles_refunded :: () -> HostM s Word64 - msg_cycles_refunded () = fromIntegral <$> getRefunded esref + msg_cycles_refunded () = msg_cycles_refunded128 () >>= low64BitsOrErr msg_cycles_available :: () -> HostM s Word64 - msg_cycles_available () = fromIntegral <$> getAvailable esref + msg_cycles_available () = msg_cycles_available128 () >>= low64BitsOrErr msg_cycles_accept :: Word64 -> HostM s Word64 - msg_cycles_accept max_amount = do - available <- fromIntegral <$> getAvailable esref + msg_cycles_accept max_amount = msg_cycles_accept128 (0, max_amount) >>= low64BitsOrErr + + canister_cycle_balance :: () -> HostM s Word64 + canister_cycle_balance () = canister_cycle_balance128 () >>= low64BitsOrErr + + msg_cycles_refunded128 :: () -> HostM s (Word64, Word64) + msg_cycles_refunded128 () = splitIntoHighAndLowBits <$> getRefunded esref + + msg_cycles_available128 :: () -> HostM s (Word64, Word64) + msg_cycles_available128 () = splitIntoHighAndLowBits <$> getAvailable esref + + msg_cycles_accept128 :: (Word64, Word64) -> HostM s (Word64, Word64) + msg_cycles_accept128 (max_amount_high, max_amount_low) = do + available <- getAvailable esref balance <- gets balance + let max_amount = combineHighAndLowBits (fromIntegral max_amount_high, fromIntegral max_amount_low) let amount = minimum - [ fromIntegral max_amount + [ max_amount , available , cMAX_CANISTER_BALANCE - balance] subtractAvailable esref amount addBalance esref amount addAccepted esref amount - return (fromIntegral amount) + return $ splitIntoHighAndLowBits amount - canister_cycle_balance :: () -> HostM s Word64 - canister_cycle_balance () = fromIntegral <$> gets balance + canister_cycle_balance128 :: () -> HostM s (Word64, Word64) + canister_cycle_balance128 () = splitIntoHighAndLowBits <$> gets balance call_new :: ( Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32 ) -> HostM s () call_new ( callee_src, callee_size, name_src, name_size diff --git a/src/IC/Test/Agent.hs b/src/IC/Test/Agent.hs index 8fcc849c..c1c9f882 100644 --- a/src/IC/Test/Agent.hs +++ b/src/IC/Test/Agent.hs @@ -526,6 +526,9 @@ asWord64 = runGet Get.getWord64le as2Word64 :: HasCallStack => Blob -> IO (Word64, Word64) as2Word64 = runGet $ (,) <$> Get.getWord64le <*> Get.getWord64le +asPairI64 :: HasCallStack => Blob -> IO (Word64, Word64) +asPairI64 = runGet $ flip (,) <$> Get.getWord64le <*> Get.getWord64le + bothSame :: (Eq a, Show a) => (a, a) -> Assertion bothSame (x,y) = x @?= y diff --git a/src/IC/Test/Spec.hs b/src/IC/Test/Spec.hs index 2b11df59..ace77f60 100644 --- a/src/IC/Test/Spec.hs +++ b/src/IC/Test/Spec.hs @@ -1545,6 +1545,7 @@ icTests = withAgentConfig $ testGroup "Interface Spec acceptance tests" recallBalance = replyData (stableRead (int 0) (int 8)) acceptAll = ignore (acceptCycles getAvailableCycles) queryBalance cid = query cid replyBalance >>= asWord64 + queryAvailable cid = query cid (replyData (pairToB getBalance128)) >>= as2Word64 -- At the time of writing, creating a canister needs at least 1T -- and the freezing limit is 5T @@ -1603,13 +1604,17 @@ icTests = withAgentConfig $ testGroup "Interface Spec acceptance tests" call cid (replyData (i64tob (acceptCycles (int64 0)))) >>= asWord64 >>= is 0 , simpleTestCase "can accept more than available cycles" $ \cid -> call cid (replyData (i64tob (acceptCycles (int64 1)))) >>= asWord64 >>= is 0 - , simpleTestCase "cant accept absurd amount of cycles" $ \cid -> - call cid (replyData (i64tob (acceptCycles (int64 maxBound)))) >>= asWord64 >>= is 0 + -- , simpleTestCase "cant accept absurd amount of cycles" $ \cid -> + -- call cid (replyData (i64tob (acceptCycles (int64 maxBound)))) >>= asWord64 >>= is 0 , testGroup "provisional_create_canister_with_cycles" [ testCase "balance as expected" $ do cid <- create noop - queryBalance cid >>= isRoughly def_cycles + queryBalance cid >>= isRoughly def_cycles, + + testCase "available as expected" $ do + cid <- create noop + queryAvailable cid >>= is (1,2) , testCaseSteps "default (i.e. max) balance" $ \step -> do cid <- ic_provisional_create ic00 Nothing empty diff --git a/src/IC/Test/Universal.hs b/src/IC/Test/Universal.hs index 94acaebe..df137401 100644 --- a/src/IC/Test/Universal.hs +++ b/src/IC/Test/Universal.hs @@ -24,9 +24,9 @@ import Data.ByteString.Builder import Data.Word import Data.String --- The types of our little language are i32, i64 and blobs +-- The types of our little language are i32, i64, pair of i64s and blobs -data T = I | I64 | B +data T = I | I64 | B | PairI64 -- We deal with expressions (return a value, thus have a type) and programs (do @@ -228,6 +228,21 @@ onHeartbeat = op 49 performanceCounter :: Exp 'I64 performanceCounter = op 50 +getBalance128 :: Exp 'PairI64 +getBalance128 = op 51 + +getAvailableCycles128 :: Exp 'PairI64 +getAvailableCycles128 = op 52 + +getRefund128 :: Exp 'PairI64 +getRefund128 = op 53 + +acceptCycles128 :: Exp 'I64 -> Exp 'I64 -> Exp 'PairI64 +acceptCycles128 = op 54 + +pairToB :: Exp 'PairI64 -> Exp 'B +pairToB = op 55 + -- Some convenience combinators -- This allows us to write byte expressions as plain string literals diff --git a/universal-canister/src/api.rs b/universal-canister/src/api.rs index 8aef4dee..f0d3ac1a 100644 --- a/universal-canister/src/api.rs +++ b/universal-canister/src/api.rs @@ -9,6 +9,7 @@ mod ic0 { extern "C" { pub fn accept_message() -> (); pub fn canister_cycle_balance() -> u64; + pub fn canister_cycle_balance128() -> (u64, u64); pub fn canister_self_copy(dst: u32, offset: u32, size: u32) -> (); pub fn canister_self_size() -> u32; pub fn canister_status() -> u32; @@ -20,6 +21,9 @@ mod ic0 { pub fn msg_cycles_accept(max_amount: u64) -> u64; pub fn msg_cycles_available() -> u64; pub fn msg_cycles_refunded() -> u64; + pub fn msg_cycles_accept128(max_amount_high: u64, max_amount_low: u64) -> (u64, u64); + pub fn msg_cycles_available128() -> (u64, u64); + pub fn msg_cycles_refunded128() -> (u64, u64); pub fn msg_method_name_copy(dst: u32, offset: u32, size: u32) -> (); pub fn msg_method_name_size() -> u32; pub fn msg_reject_code() -> u32; @@ -176,18 +180,34 @@ pub fn cycles_available() -> u64 { unsafe { ic0::msg_cycles_available() } } +pub fn cycles_available128() -> (u64, u64){ + unsafe { ic0::msg_cycles_available128() } +} + pub fn cycles_refunded() -> u64 { unsafe { ic0::msg_cycles_refunded() } } +pub fn cycles_refunded128() -> (u64, u64) { + unsafe { ic0::msg_cycles_refunded128() } +} + pub fn accept(amount: u64) -> u64 { unsafe { ic0::msg_cycles_accept(amount) } } +pub fn accept128(high: u64, low: u64) -> (u64, u64) { + unsafe { ic0::msg_cycles_accept128(high, low) } +} + pub fn balance() -> u64 { unsafe { ic0::canister_cycle_balance() } } +pub fn balance128() -> (u64, u64) { + unsafe { ic0::canister_cycle_balance128() } +} + pub fn stable_size() -> u32 { unsafe { ic0::stable_size() } } diff --git a/universal-canister/src/main.rs b/universal-canister/src/main.rs index 110aec02..6b500089 100644 --- a/universal-canister/src/main.rs +++ b/universal-canister/src/main.rs @@ -8,6 +8,7 @@ enum Val { I32(u32), I64(u64), Blob(Vec), + PairI64(u64, u64), } struct Stack(Vec); @@ -33,6 +34,10 @@ impl Stack { self.0.push(Val::Blob(x)); } + fn push_pair_int64(self: &mut Self, p: (u64, u64)) { + self.0.push(Val::PairI64(p.0, p.1)); + } + fn pop_int(self: &mut Self) -> u32 { if let Some(Val::I32(i)) = self.0.pop() { i @@ -56,6 +61,14 @@ impl Stack { api::trap_with("did not find blob on stack") } } + + fn pop_pair_int64(self: &mut Self) -> (u64, u64) { + if let Some(Val::PairI64(a,b)) = self.0.pop() { + (a, b) + } else { + api::trap_with("did not find pair of I64s on stack") + } + } } // Reading data from the operations stream @@ -286,6 +299,29 @@ fn eval(ops: Ops) { 50 => stack.push_int64(api::performance_counter()), + 51 => { + stack.push_pair_int64(api::balance128()) + }, + 52 => { + stack.push_pair_int64(api::cycles_available128()) + }, + 53 => { + stack.push_pair_int64(api::cycles_refunded128()) + }, + 54 => { + let low = stack.pop_int64(); + let high = stack.pop_int64(); + stack.push_pair_int64(api::accept128(high, low)) + }, + + // pair to blob + 55 => { + let p = stack.pop_pair_int64(); + let mut bytes = p.1.to_le_bytes().to_vec(); + bytes.append(&mut p.0.to_le_bytes().to_vec()); + stack.push_blob(bytes) + } + _ => api::trap_with(&format!("unknown op {}", op)), } } From a0fd6c24039a26eb4ae81ffbe81c73ae15246ee0 Mon Sep 17 00:00:00 2001 From: Marcin Dziadus Date: Fri, 29 Oct 2021 14:50:14 +0200 Subject: [PATCH 2/6] Add call_cycles_add128 --- src/IC/Canister/Imp.hs | 16 ++++++++++------ src/IC/Constants.hs | 2 +- src/IC/Test/Agent.hs | 4 ++-- src/IC/Test/Spec.hs | 26 +++++++++++++++++++++----- src/IC/Test/Universal.hs | 5 ++++- universal-canister/src/api.rs | 7 +++++++ universal-canister/src/main.rs | 8 +++++++- 7 files changed, 52 insertions(+), 16 deletions(-) diff --git a/src/IC/Canister/Imp.hs b/src/IC/Canister/Imp.hs index cc00d8d3..be1c2120 100644 --- a/src/IC/Canister/Imp.hs +++ b/src/IC/Canister/Imp.hs @@ -214,6 +214,7 @@ systemAPI esref = , toImport "ic0" "call_on_cleanup" call_on_cleanup , toImport "ic0" "call_data_append" call_data_append , toImport "ic0" "call_cycles_add" call_cycles_add + , toImport "ic0" "call_cycles_add128" call_cycles_add128 , toImport "ic0" "call_perform" call_perform , toImport "ic0" "stable_size" stable_size @@ -346,12 +347,12 @@ systemAPI esref = where highBits = flip shiftR 64 lowBits = (.&.) (1 `shiftL` 64 - 1) - combineHighAndLowBits :: (Natural, Natural) -> Natural - combineHighAndLowBits (high, low) = 2^(64 :: Int) * high + low + combineHighAndLowBits :: Integral a => (a, a) -> Natural + combineHighAndLowBits (high, low) = fromIntegral $ 2^(64 :: Int) * high + low low64BitsOrErr :: (Word64, Word64) -> HostM s Word64 low64BitsOrErr (0, low) = return low - low64BitsOrErr (high, low) = throwError $ "The number of cycles does not fit in 64 bits: " ++ show (combineHighAndLowBits (fromIntegral high, fromIntegral low)) + low64BitsOrErr (high, low) = throwError $ "The number of cycles does not fit in 64 bits: " ++ show (combineHighAndLowBits (high, low)) msg_cycles_refunded :: () -> HostM s Word64 msg_cycles_refunded () = msg_cycles_refunded128 () >>= low64BitsOrErr @@ -375,7 +376,7 @@ systemAPI esref = msg_cycles_accept128 (max_amount_high, max_amount_low) = do available <- getAvailable esref balance <- gets balance - let max_amount = combineHighAndLowBits (fromIntegral max_amount_high, fromIntegral max_amount_low) + let max_amount = combineHighAndLowBits (max_amount_high, max_amount_low) let amount = minimum [ max_amount , available @@ -419,8 +420,11 @@ systemAPI esref = changePendingCall $ \pc -> return $ pc { call_arg = call_arg pc <> arg } call_cycles_add :: Word64 -> HostM s () - call_cycles_add amount = do - let cycles = fromIntegral amount + call_cycles_add amount = call_cycles_add128 (0, amount) + + call_cycles_add128 :: (Word64, Word64) -> HostM s () + call_cycles_add128 amount = do + let cycles = combineHighAndLowBits amount changePendingCall $ \pc -> do subtractBalance esref cycles return $ pc { call_transferred_cycles = call_transferred_cycles pc + cycles } diff --git a/src/IC/Constants.hs b/src/IC/Constants.hs index 5c774811..effcb862 100644 --- a/src/IC/Constants.hs +++ b/src/IC/Constants.hs @@ -3,4 +3,4 @@ module IC.Constants where import Numeric.Natural cMAX_CANISTER_BALANCE :: Natural -cMAX_CANISTER_BALANCE = 2^(60::Int) +cMAX_CANISTER_BALANCE = 2^(120::Int) diff --git a/src/IC/Test/Agent.hs b/src/IC/Test/Agent.hs index c1c9f882..942f76c2 100644 --- a/src/IC/Test/Agent.hs +++ b/src/IC/Test/Agent.hs @@ -526,8 +526,8 @@ asWord64 = runGet Get.getWord64le as2Word64 :: HasCallStack => Blob -> IO (Word64, Word64) as2Word64 = runGet $ (,) <$> Get.getWord64le <*> Get.getWord64le -asPairI64 :: HasCallStack => Blob -> IO (Word64, Word64) -asPairI64 = runGet $ flip (,) <$> Get.getWord64le <*> Get.getWord64le +asPairWord64 :: HasCallStack => Blob -> IO (Word64, Word64) +asPairWord64 = runGet $ flip (,) <$> Get.getWord64le <*> Get.getWord64le bothSame :: (Eq a, Show a) => (a, a) -> Assertion bothSame (x,y) = x @?= y diff --git a/src/IC/Test/Spec.hs b/src/IC/Test/Spec.hs index ace77f60..9557bdd1 100644 --- a/src/IC/Test/Spec.hs +++ b/src/IC/Test/Spec.hs @@ -643,10 +643,14 @@ icTests = withAgentConfig $ testGroup "Interface Spec acceptance tests" , t "msg_reply" never reply -- due to double reply , t "msg_reject" never $ reject "rejecting" -- due to double reply , t "msg_cycles_available" "U Rt Ry" $ ignore getAvailableCycles + , t "msg_cycles_available128" "U Rt Ry" $ ignore getAvailableCycles128 , t "msg_cycles_refunded" "Rt Ry" $ ignore getRefund + , t "msg_cycles_refunded128" "Rt Ry" $ ignore getRefund128 , t "msg_cycles_accept" "U Rt Ry" $ ignore (acceptCycles (int64 0)) + , t "msg_cycles_accept128" "U Rt Ry" $ ignore (acceptCycles128 (int64 0) (int64 0)) , t "canister_self" star $ ignore self , t "canister_cycle_balance" star $ ignore getBalance + , t "canister_cycle_balance128" star $ ignore getBalance128 , t "call_new…call_perform" "U Rt Ry H" $ callNew "foo" "bar" "baz" "quux" >>> callDataAppend "foo" >>> @@ -655,6 +659,7 @@ icTests = withAgentConfig $ testGroup "Interface Spec acceptance tests" , t "call_set_cleanup" never $ callOnCleanup (callback noop) , t "call_data_append" never $ callDataAppend "foo" , t "call_cycles_add" never $ callCyclesAdd (int64 0) + , t "call_cycles_add128" never $ callCyclesAdd (int64 0) (int64 0) , t "call_perform" never callPerform , t "stable_size" star $ ignore stableSize , t "stable_grow" star $ ignore $ stableGrow (int 1) @@ -1545,7 +1550,7 @@ icTests = withAgentConfig $ testGroup "Interface Spec acceptance tests" recallBalance = replyData (stableRead (int 0) (int 8)) acceptAll = ignore (acceptCycles getAvailableCycles) queryBalance cid = query cid replyBalance >>= asWord64 - queryAvailable cid = query cid (replyData (pairToB getBalance128)) >>= as2Word64 + queryAvailable cid = query cid (replyData (pairToB getBalance128)) >>= asPairWord64 -- At the time of writing, creating a canister needs at least 1T -- and the freezing limit is 5T @@ -1577,7 +1582,18 @@ icTests = withAgentConfig $ testGroup "Interface Spec acceptance tests" ic_install (ic00via cid) (enum #install) cid2 universal_wasm (run noop) return cid2 in - [ testGroup "can use balance API" $ + [ testGroup "cycles API - backward compatibility" $ + [ simpleTestCase "canister_cycle_balance = canister_cycle_balance128 for numbers firring in 64 bits" $ \cid -> do + a <- query cid (replyData (i64tob getBalance)) >>= asWord64 + b <- query cid (replyData (pairToB getBalance128)) >>= asPairWord64 + bothSame ((0,a),b) + , simpleTestCase "msg_cycles_accept128" $ \cid -> do + call' cid (callCyclesAdd128 (int64 1) (int64 1) >>> reply) >>= isReject [5] + a <- query cid (replyData (i64tob (acceptCycles (int64 0)))) >>= asWord64 + b <- query cid (replyData (pairToB (acceptCycles128 (int64 0) (int64 0)))) >>= asPairWord64 + bothSame ((0,a),b) + ] + , testGroup "can use balance API" $ let getBalanceTwice = join cat (i64tob getBalance) test = replyData getBalanceTwice in @@ -1604,8 +1620,8 @@ icTests = withAgentConfig $ testGroup "Interface Spec acceptance tests" call cid (replyData (i64tob (acceptCycles (int64 0)))) >>= asWord64 >>= is 0 , simpleTestCase "can accept more than available cycles" $ \cid -> call cid (replyData (i64tob (acceptCycles (int64 1)))) >>= asWord64 >>= is 0 - -- , simpleTestCase "cant accept absurd amount of cycles" $ \cid -> - -- call cid (replyData (i64tob (acceptCycles (int64 maxBound)))) >>= asWord64 >>= is 0 + , simpleTestCase "cant accept absurd amount of cycles" $ \cid -> + call cid (replyData (i64tob (acceptCycles (int64 maxBound)))) >>= asWord64 >>= is 0 , testGroup "provisional_create_canister_with_cycles" [ testCase "balance as expected" $ do @@ -2351,7 +2367,7 @@ install prog = do return cid create :: (HasCallStack, HasAgentConfig) => IO Blob -create = ic_provisional_create ic00 Nothing empty +create = ic_provisional_create ic00 (Just (2^(60::Int))) empty upgrade' :: (HasCallStack, HasAgentConfig) => Blob -> Prog -> IO ReqResponse upgrade' cid prog = do diff --git a/src/IC/Test/Universal.hs b/src/IC/Test/Universal.hs index df137401..cc2b6889 100644 --- a/src/IC/Test/Universal.hs +++ b/src/IC/Test/Universal.hs @@ -240,8 +240,11 @@ getRefund128 = op 53 acceptCycles128 :: Exp 'I64 -> Exp 'I64 -> Exp 'PairI64 acceptCycles128 = op 54 +callCyclesAdd128 :: Exp 'I64 -> Exp 'I64 -> Prog +callCyclesAdd128 = op 55 + pairToB :: Exp 'PairI64 -> Exp 'B -pairToB = op 55 +pairToB = op 56 -- Some convenience combinators diff --git a/universal-canister/src/api.rs b/universal-canister/src/api.rs index f0d3ac1a..c595a643 100644 --- a/universal-canister/src/api.rs +++ b/universal-canister/src/api.rs @@ -46,6 +46,7 @@ mod ic0 { pub fn call_on_cleanup(fun: u32, env: u32) -> (); pub fn call_data_append(src: u32, size: u32) -> (); pub fn call_cycles_add(amount: u64) -> (); + pub fn call_cycles_add128(amount_high: u64, amount_low: u64) -> (); pub fn call_perform() -> u32; pub fn stable_size() -> u32; pub fn stable_grow(additional_pages: u32) -> u32; @@ -105,6 +106,12 @@ pub fn call_cycles_add(amount: u64) { } } +pub fn call_cycles_add128(amount_high: u64, amount_low: u64) { + unsafe { + ic0::call_cycles_add128(amount_high, amount_low); + } +} + pub fn call_perform() -> u32 { unsafe { ic0::call_perform() } } diff --git a/universal-canister/src/main.rs b/universal-canister/src/main.rs index 6b500089..563628b6 100644 --- a/universal-canister/src/main.rs +++ b/universal-canister/src/main.rs @@ -299,6 +299,7 @@ fn eval(ops: Ops) { 50 => stack.push_int64(api::performance_counter()), + // 128-bit cycles API 51 => { stack.push_pair_int64(api::balance128()) }, @@ -313,9 +314,14 @@ fn eval(ops: Ops) { let high = stack.pop_int64(); stack.push_pair_int64(api::accept128(high, low)) }, + 55 => { + let low = stack.pop_int64(); + let high = stack.pop_int64(); + api::call_cycles_add128(high, low) + }, // pair to blob - 55 => { + 56 => { let p = stack.pop_pair_int64(); let mut bytes = p.1.to_le_bytes().to_vec(); bytes.append(&mut p.0.to_le_bytes().to_vec()); From 70a1305e8ac3b212eb4f36ca2322bbc00a6cbb9d Mon Sep 17 00:00:00 2001 From: Marcin Dziadus Date: Fri, 29 Oct 2021 18:06:41 +0200 Subject: [PATCH 3/6] Minor --- src/IC/Canister/Imp.hs | 22 +++++++++++----------- src/IC/Test/Agent.hs | 4 ++++ src/IC/Test/Spec.hs | 36 +++++++++++++++++------------------- 3 files changed, 32 insertions(+), 30 deletions(-) diff --git a/src/IC/Canister/Imp.hs b/src/IC/Canister/Imp.hs index be1c2120..285cefca 100644 --- a/src/IC/Canister/Imp.hs +++ b/src/IC/Canister/Imp.hs @@ -342,17 +342,17 @@ systemAPI esref = Stopping -> 2 Stopped -> 3 - splitIntoHighAndLowBits :: Natural -> (Word64, Word64) - splitIntoHighAndLowBits n = (fromIntegral $ highBits n, fromIntegral $ lowBits n) + splitBitsIntoHalves :: Natural -> (Word64, Word64) + splitBitsIntoHalves n = (fromIntegral $ highBits n, fromIntegral $ lowBits n) where highBits = flip shiftR 64 lowBits = (.&.) (1 `shiftL` 64 - 1) - combineHighAndLowBits :: Integral a => (a, a) -> Natural - combineHighAndLowBits (high, low) = fromIntegral $ 2^(64 :: Int) * high + low + combineBitHalves :: (Word64, Word64) -> Natural + combineBitHalves (high, low) = fromIntegral high `shiftL` 64 + fromIntegral low low64BitsOrErr :: (Word64, Word64) -> HostM s Word64 low64BitsOrErr (0, low) = return low - low64BitsOrErr (high, low) = throwError $ "The number of cycles does not fit in 64 bits: " ++ show (combineHighAndLowBits (high, low)) + low64BitsOrErr (high, low) = throwError $ "The number of cycles does not fit in 64 bits: " ++ show (combineBitHalves (high, low)) msg_cycles_refunded :: () -> HostM s Word64 msg_cycles_refunded () = msg_cycles_refunded128 () >>= low64BitsOrErr @@ -367,16 +367,16 @@ systemAPI esref = canister_cycle_balance () = canister_cycle_balance128 () >>= low64BitsOrErr msg_cycles_refunded128 :: () -> HostM s (Word64, Word64) - msg_cycles_refunded128 () = splitIntoHighAndLowBits <$> getRefunded esref + msg_cycles_refunded128 () = splitBitsIntoHalves <$> getRefunded esref msg_cycles_available128 :: () -> HostM s (Word64, Word64) - msg_cycles_available128 () = splitIntoHighAndLowBits <$> getAvailable esref + msg_cycles_available128 () = splitBitsIntoHalves <$> getAvailable esref msg_cycles_accept128 :: (Word64, Word64) -> HostM s (Word64, Word64) msg_cycles_accept128 (max_amount_high, max_amount_low) = do available <- getAvailable esref balance <- gets balance - let max_amount = combineHighAndLowBits (max_amount_high, max_amount_low) + let max_amount = combineBitHalves (max_amount_high, max_amount_low) let amount = minimum [ max_amount , available @@ -384,10 +384,10 @@ systemAPI esref = subtractAvailable esref amount addBalance esref amount addAccepted esref amount - return $ splitIntoHighAndLowBits amount + return $ splitBitsIntoHalves amount canister_cycle_balance128 :: () -> HostM s (Word64, Word64) - canister_cycle_balance128 () = splitIntoHighAndLowBits <$> gets balance + canister_cycle_balance128 () = splitBitsIntoHalves <$> gets balance call_new :: ( Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32 ) -> HostM s () call_new ( callee_src, callee_size, name_src, name_size @@ -424,7 +424,7 @@ systemAPI esref = call_cycles_add128 :: (Word64, Word64) -> HostM s () call_cycles_add128 amount = do - let cycles = combineHighAndLowBits amount + let cycles = combineBitHalves amount changePendingCall $ \pc -> do subtractBalance esref cycles return $ pc { call_transferred_cycles = call_transferred_cycles pc + cycles } diff --git a/src/IC/Test/Agent.hs b/src/IC/Test/Agent.hs index 942f76c2..8f16eb0f 100644 --- a/src/IC/Test/Agent.hs +++ b/src/IC/Test/Agent.hs @@ -57,6 +57,7 @@ import Data.Time.Clock.POSIX import Codec.Candid (Principal(..), prettyPrincipal) import qualified Data.Binary.Get as Get import qualified Codec.Candid as Candid +import Data.Bits import Data.Row import qualified Data.Row.Records as R import qualified Data.Row.Variants as V @@ -842,3 +843,6 @@ textual = T.unpack . prettyPrincipal . Principal shorten :: Int -> String -> String shorten n s = a ++ (if null b then "" else "…") where (a,b) = splitAt n s + +toI128 :: (Word64, Word64) -> Natural +toI128 (high, low) = fromIntegral high `shiftL` 64 + fromIntegral low \ No newline at end of file diff --git a/src/IC/Test/Spec.hs b/src/IC/Test/Spec.hs index 9557bdd1..93ff70d6 100644 --- a/src/IC/Test/Spec.hs +++ b/src/IC/Test/Spec.hs @@ -659,7 +659,7 @@ icTests = withAgentConfig $ testGroup "Interface Spec acceptance tests" , t "call_set_cleanup" never $ callOnCleanup (callback noop) , t "call_data_append" never $ callDataAppend "foo" , t "call_cycles_add" never $ callCyclesAdd (int64 0) - , t "call_cycles_add128" never $ callCyclesAdd (int64 0) (int64 0) + , t "call_cycles_add128" never $ callCyclesAdd128 (int64 0) (int64 0) , t "call_perform" never callPerform , t "stable_size" star $ ignore stableSize , t "stable_grow" star $ ignore $ stableGrow (int 1) @@ -1544,13 +1544,14 @@ icTests = withAgentConfig $ testGroup "Interface Spec acceptance tests" , testGroup "cycles" $ let replyBalance = replyData (i64tob getBalance) + replyBalance128 = replyData (pairToB getBalance128) rememberBalance = ignore (stableGrow (int 1)) >>> stableWrite (int 0) (i64tob getBalance) recallBalance = replyData (stableRead (int 0) (int 8)) acceptAll = ignore (acceptCycles getAvailableCycles) queryBalance cid = query cid replyBalance >>= asWord64 - queryAvailable cid = query cid (replyData (pairToB getBalance128)) >>= asPairWord64 + queryBalance128 cid = query cid replyBalance128 >>= asPairWord64 -- At the time of writing, creating a canister needs at least 1T -- and the freezing limit is 5T @@ -1583,15 +1584,16 @@ icTests = withAgentConfig $ testGroup "Interface Spec acceptance tests" return cid2 in [ testGroup "cycles API - backward compatibility" $ - [ simpleTestCase "canister_cycle_balance = canister_cycle_balance128 for numbers firring in 64 bits" $ \cid -> do - a <- query cid (replyData (i64tob getBalance)) >>= asWord64 - b <- query cid (replyData (pairToB getBalance128)) >>= asPairWord64 - bothSame ((0,a),b) - , simpleTestCase "msg_cycles_accept128" $ \cid -> do - call' cid (callCyclesAdd128 (int64 1) (int64 1) >>> reply) >>= isReject [5] - a <- query cid (replyData (i64tob (acceptCycles (int64 0)))) >>= asWord64 - b <- query cid (replyData (pairToB (acceptCycles128 (int64 0) (int64 0)))) >>= asPairWord64 + [ simpleTestCase "canister_cycle_balance = canister_cycle_balance128 for numbers fitting in 64 bits" $ \cid -> do + a <- queryBalance cid + b <- queryBalance128 cid bothSame ((0,a),b) + , testCase "lagacy API traps when a result is too big" $ do + cid <- create noop + let large = 2^(65::Int) + ic_top_up ic00 cid large + query' cid replyBalance >>= isReject [5] + toI128 <$> queryBalance128 cid >>= isRoughly (large + fromIntegral def_cycles) ] , testGroup "can use balance API" $ let getBalanceTwice = join cat (i64tob getBalance) @@ -1626,22 +1628,18 @@ icTests = withAgentConfig $ testGroup "Interface Spec acceptance tests" , testGroup "provisional_create_canister_with_cycles" [ testCase "balance as expected" $ do cid <- create noop - queryBalance cid >>= isRoughly def_cycles, - - testCase "available as expected" $ do - cid <- create noop - queryAvailable cid >>= is (1,2) + queryBalance cid >>= isRoughly def_cycles - , testCaseSteps "default (i.e. max) balance" $ \step -> do + , testCaseSteps "default (i.e. max) (balance" $ \step -> do cid <- ic_provisional_create ic00 Nothing empty installAt cid noop - cycles <- queryBalance cid + cycles <- queryBalance128 cid step $ "Cycle balance now at " ++ show cycles , testCaseSteps "> 2^128 succeeds" $ \step -> do cid <- ic_provisional_create ic00 (Just (10 * 2^(128::Int))) empty installAt cid noop - cycles <- queryBalance cid + cycles <- queryBalance128 cid step $ "Cycle balance now at " ++ show cycles ] @@ -1842,7 +1840,7 @@ icTests = withAgentConfig $ testGroup "Interface Spec acceptance tests" , testCaseSteps "more than 2^128" $ \step -> do cid <- create noop ic_top_up ic00 cid (10 * 2^(128::Int)) - cycles <- queryBalance cid + cycles <- queryBalance128 cid step $ "Cycle balance now at " ++ show cycles , testCase "nonexisting canister" $ do ic_top_up' ic00 doesn'tExist (fromIntegral def_cycles) From 1fb14d533407cb73804ddafd353800565e5bbfc4 Mon Sep 17 00:00:00 2001 From: Marcin Dziadus Date: Fri, 29 Oct 2021 18:26:07 +0200 Subject: [PATCH 4/6] Minor cleanup --- src/IC/Test/Spec.hs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/IC/Test/Spec.hs b/src/IC/Test/Spec.hs index 93ff70d6..9c7e4cf4 100644 --- a/src/IC/Test/Spec.hs +++ b/src/IC/Test/Spec.hs @@ -1622,15 +1622,15 @@ icTests = withAgentConfig $ testGroup "Interface Spec acceptance tests" call cid (replyData (i64tob (acceptCycles (int64 0)))) >>= asWord64 >>= is 0 , simpleTestCase "can accept more than available cycles" $ \cid -> call cid (replyData (i64tob (acceptCycles (int64 1)))) >>= asWord64 >>= is 0 - , simpleTestCase "cant accept absurd amount of cycles" $ \cid -> - call cid (replyData (i64tob (acceptCycles (int64 maxBound)))) >>= asWord64 >>= is 0 + , simpleTestCase "can accept absurd amount of cycles" $ \cid -> + call cid (replyData (pairToB (acceptCycles128 (int64 maxBound) (int64 maxBound)))) >>= asPairWord64 >>= is (0,0) , testGroup "provisional_create_canister_with_cycles" [ testCase "balance as expected" $ do cid <- create noop queryBalance cid >>= isRoughly def_cycles - , testCaseSteps "default (i.e. max) (balance" $ \step -> do + , testCaseSteps "default (i.e. max) balance" $ \step -> do cid <- ic_provisional_create ic00 Nothing empty installAt cid noop cycles <- queryBalance128 cid From 5a9081a36853e1acc286c87cc216b614c570e511 Mon Sep 17 00:00:00 2001 From: Marcin Dziadus Date: Sat, 30 Oct 2021 08:54:29 +0200 Subject: [PATCH 5/6] Apply Joachim's suggestions --- src/IC/Canister/Imp.hs | 5 +++-- src/IC/Test/Agent.hs | 2 +- src/IC/Test/Spec.hs | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/IC/Canister/Imp.hs b/src/IC/Canister/Imp.hs index 285cefca..d7ebf9fe 100644 --- a/src/IC/Canister/Imp.hs +++ b/src/IC/Canister/Imp.hs @@ -5,6 +5,7 @@ {-# LANGUAGE TypeApplications #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE NumericUnderscores #-} {-| The canister interface, presented imperatively (or impurely), i.e. without rollback @@ -345,10 +346,10 @@ systemAPI esref = splitBitsIntoHalves :: Natural -> (Word64, Word64) splitBitsIntoHalves n = (fromIntegral $ highBits n, fromIntegral $ lowBits n) where highBits = flip shiftR 64 - lowBits = (.&.) (1 `shiftL` 64 - 1) + lowBits = (0xFFFFFFFF_FFFFFFFF .&.) combineBitHalves :: (Word64, Word64) -> Natural - combineBitHalves (high, low) = fromIntegral high `shiftL` 64 + fromIntegral low + combineBitHalves (high, low) = fromIntegral high `shiftL` 64 .|. fromIntegral low low64BitsOrErr :: (Word64, Word64) -> HostM s Word64 low64BitsOrErr (0, low) = return low diff --git a/src/IC/Test/Agent.hs b/src/IC/Test/Agent.hs index 8f16eb0f..a5066c34 100644 --- a/src/IC/Test/Agent.hs +++ b/src/IC/Test/Agent.hs @@ -845,4 +845,4 @@ shorten n s = a ++ (if null b then "" else "…") where (a,b) = splitAt n s toI128 :: (Word64, Word64) -> Natural -toI128 (high, low) = fromIntegral high `shiftL` 64 + fromIntegral low \ No newline at end of file +toI128 (high, low) = fromIntegral high `shiftL` 64 .|. fromIntegral low \ No newline at end of file diff --git a/src/IC/Test/Spec.hs b/src/IC/Test/Spec.hs index 9c7e4cf4..996026a5 100644 --- a/src/IC/Test/Spec.hs +++ b/src/IC/Test/Spec.hs @@ -1588,7 +1588,7 @@ icTests = withAgentConfig $ testGroup "Interface Spec acceptance tests" a <- queryBalance cid b <- queryBalance128 cid bothSame ((0,a),b) - , testCase "lagacy API traps when a result is too big" $ do + , testCase "legacy API traps when a result is too big" $ do cid <- create noop let large = 2^(65::Int) ic_top_up ic00 cid large From 91de24e3ff9816318aa7c43a405f5e4e60ce614f Mon Sep 17 00:00:00 2001 From: Marcin Dziadus Date: Mon, 1 Nov 2021 10:16:00 +0100 Subject: [PATCH 6/6] Get rid of compiler warnings --- universal-canister/src/api.rs | 23 +++++++++++++++-------- universal-canister/src/main.rs | 12 ++++++------ 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/universal-canister/src/api.rs b/universal-canister/src/api.rs index c595a643..081085cd 100644 --- a/universal-canister/src/api.rs +++ b/universal-canister/src/api.rs @@ -4,12 +4,19 @@ extern crate wee_alloc; #[global_allocator] static ALLOC: wee_alloc::WeeAlloc<'_> = wee_alloc::WeeAlloc::INIT; +#[repr(C)] +// Note: tuples are not FFI-safe causing the compiler to complain. To avoid this, +// we represent pair as a tuple struct which has known memory layout and the same semantics as +// a plain pair. +pub struct Pair(pub u64, pub u64); + mod ic0 { + use api::Pair; #[link(wasm_import_module = "ic0")] extern "C" { pub fn accept_message() -> (); pub fn canister_cycle_balance() -> u64; - pub fn canister_cycle_balance128() -> (u64, u64); + pub fn canister_cycle_balance128() -> Pair; pub fn canister_self_copy(dst: u32, offset: u32, size: u32) -> (); pub fn canister_self_size() -> u32; pub fn canister_status() -> u32; @@ -21,9 +28,9 @@ mod ic0 { pub fn msg_cycles_accept(max_amount: u64) -> u64; pub fn msg_cycles_available() -> u64; pub fn msg_cycles_refunded() -> u64; - pub fn msg_cycles_accept128(max_amount_high: u64, max_amount_low: u64) -> (u64, u64); - pub fn msg_cycles_available128() -> (u64, u64); - pub fn msg_cycles_refunded128() -> (u64, u64); + pub fn msg_cycles_accept128(max_amount_high: u64, max_amount_low: u64) -> Pair; + pub fn msg_cycles_available128() -> Pair; + pub fn msg_cycles_refunded128() -> Pair; pub fn msg_method_name_copy(dst: u32, offset: u32, size: u32) -> (); pub fn msg_method_name_size() -> u32; pub fn msg_reject_code() -> u32; @@ -187,7 +194,7 @@ pub fn cycles_available() -> u64 { unsafe { ic0::msg_cycles_available() } } -pub fn cycles_available128() -> (u64, u64){ +pub fn cycles_available128() -> Pair{ unsafe { ic0::msg_cycles_available128() } } @@ -195,7 +202,7 @@ pub fn cycles_refunded() -> u64 { unsafe { ic0::msg_cycles_refunded() } } -pub fn cycles_refunded128() -> (u64, u64) { +pub fn cycles_refunded128() -> Pair { unsafe { ic0::msg_cycles_refunded128() } } @@ -203,7 +210,7 @@ pub fn accept(amount: u64) -> u64 { unsafe { ic0::msg_cycles_accept(amount) } } -pub fn accept128(high: u64, low: u64) -> (u64, u64) { +pub fn accept128(high: u64, low: u64) -> Pair { unsafe { ic0::msg_cycles_accept128(high, low) } } @@ -211,7 +218,7 @@ pub fn balance() -> u64 { unsafe { ic0::canister_cycle_balance() } } -pub fn balance128() -> (u64, u64) { +pub fn balance128() -> Pair { unsafe { ic0::canister_cycle_balance128() } } diff --git a/universal-canister/src/main.rs b/universal-canister/src/main.rs index 563628b6..5d6e36ef 100644 --- a/universal-canister/src/main.rs +++ b/universal-canister/src/main.rs @@ -8,7 +8,7 @@ enum Val { I32(u32), I64(u64), Blob(Vec), - PairI64(u64, u64), + PairI64(api::Pair), } struct Stack(Vec); @@ -34,8 +34,8 @@ impl Stack { self.0.push(Val::Blob(x)); } - fn push_pair_int64(self: &mut Self, p: (u64, u64)) { - self.0.push(Val::PairI64(p.0, p.1)); + fn push_pair_int64(self: &mut Self, p: api::Pair) { + self.0.push(Val::PairI64(p)); } fn pop_int(self: &mut Self) -> u32 { @@ -62,9 +62,9 @@ impl Stack { } } - fn pop_pair_int64(self: &mut Self) -> (u64, u64) { - if let Some(Val::PairI64(a,b)) = self.0.pop() { - (a, b) + fn pop_pair_int64(self: &mut Self) -> api::Pair { + if let Some(Val::PairI64(p)) = self.0.pop() { + p } else { api::trap_with("did not find pair of I64s on stack") }