Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update system API for 128 bit cycles tests #57

Merged
merged 6 commits into from Nov 20, 2021
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion default.nix
Expand Up @@ -12,7 +12,7 @@ let universal-canister = (naersk.buildPackage rec {
src = subpath ./universal-canister;
root = ./universal-canister;
CARGO_TARGET_WASM32_UNKNOWN_UNKNOWN_LINKER = "${nixpkgs.llvmPackages_12.lld}/bin/lld";
RUSTFLAGS = "-C link-arg=-s -C target-feature=+multivalue"; # much smaller wasm
RUSTFLAGS = "-C link-arg=-s"; # much smaller wasm
cargoBuildOptions = x : x ++ [ "--target wasm32-unknown-unknown" ];
doCheck = false;
release = true;
Expand Down
82 changes: 53 additions & 29 deletions src/IC/Canister/Imp.hs
Expand Up @@ -30,6 +30,7 @@ import qualified Data.Text as T
import qualified Data.ByteString.Lazy as BS
import qualified Data.ByteString.Lazy.Char8 as BSC
import qualified Data.ByteString.Lazy.UTF8 as BSU
import qualified Data.Binary.Put as Put
import Control.Monad.Primitive
import Control.Monad.ST
import Control.Monad.Except
Expand Down Expand Up @@ -274,6 +275,19 @@ systemAPI esref =
get_blob >>= \blob -> copy_to_canister dst offset size blob
)

cycles_accept :: Natural -> HostM s Natural
cycles_accept max_amount = do
available <- getAvailable esref
balance <- gets balance
let amount = minimum
[ max_amount
, available
, cMAX_CANISTER_BALANCE - balance]
subtractAvailable esref amount
addBalance esref amount
addAccepted esref amount
return amount

-- Unsafely print
putBytes :: BS.ByteString -> HostM s ()
putBytes bytes =
Expand Down Expand Up @@ -343,52 +357,62 @@ systemAPI esref =
Stopping -> 2
Stopped -> 3

splitBitsIntoHalves :: Natural -> (Word64, Word64)
splitBitsIntoHalves n = (fromIntegral $ highBits n, fromIntegral $ lowBits n)
where highBits = flip shiftR 64
lowBits = (0xFFFFFFFF_FFFFFFFF .&.)
highBits :: Natural -> Word64
highBits = fromIntegral . (flip shiftR 64)

lowBits :: Natural -> Word64
lowBits = fromIntegral . (0xFFFFFFFF_FFFFFFFF .&.)

to128le :: Natural -> BS.ByteString
to128le n = Put.runPut $ do
Put.putWord64le (lowBits n)
Put.putWord64le (highBits n)

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 (combineBitHalves (high, low))
low64BitsOrErr :: Natural -> HostM s Word64
low64BitsOrErr n = do
unless (highBits n == 0) $
throwError $ "The number of cycles does not fit in 64 bits: " ++ show n
return $ fromIntegral $ lowBits n

msg_cycles_refunded :: () -> HostM s Word64
msg_cycles_refunded () = msg_cycles_refunded128 () >>= low64BitsOrErr
msg_cycles_refunded () = getRefunded esref >>= low64BitsOrErr

msg_cycles_available :: () -> HostM s Word64
msg_cycles_available () = msg_cycles_available128 () >>= low64BitsOrErr
msg_cycles_available () = getAvailable esref >>= low64BitsOrErr

msg_cycles_accept :: Word64 -> HostM s Word64
msg_cycles_accept max_amount = msg_cycles_accept128 (0, max_amount) >>= low64BitsOrErr
msg_cycles_accept max_amount = cycles_accept (fromIntegral max_amount) >>= low64BitsOrErr

canister_cycle_balance :: () -> HostM s Word64
canister_cycle_balance () = canister_cycle_balance128 () >>= low64BitsOrErr
canister_cycle_balance () = gets balance >>= low64BitsOrErr

msg_cycles_refunded128 :: () -> HostM s (Word64, Word64)
msg_cycles_refunded128 () = splitBitsIntoHalves <$> getRefunded esref
msg_cycles_refunded128 :: Int32 -> HostM s ()
msg_cycles_refunded128 dst = do
i <- getsES esref inst
amount <- getRefunded esref
setBytes i (fromIntegral dst) (to128le amount)

msg_cycles_available128 :: () -> HostM s (Word64, Word64)
msg_cycles_available128 () = splitBitsIntoHalves <$> getAvailable esref
msg_cycles_available128 :: Int32 -> HostM s ()
msg_cycles_available128 dst = do
i <- getsES esref inst
amount <- getAvailable esref
setBytes i (fromIntegral dst) (to128le amount)

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
msg_cycles_accept128 :: (Word64, Word64, Int32) -> HostM s ()
msg_cycles_accept128 (max_amount_high, max_amount_low, dst) = do
let max_amount = combineBitHalves (max_amount_high, max_amount_low)
let amount = minimum
[ max_amount
, available
, cMAX_CANISTER_BALANCE - balance]
subtractAvailable esref amount
addBalance esref amount
addAccepted esref amount
return $ splitBitsIntoHalves amount
amount <- cycles_accept max_amount
i <- getsES esref inst
setBytes i (fromIntegral dst) (to128le amount)

canister_cycle_balance128 :: () -> HostM s (Word64, Word64)
canister_cycle_balance128 () = splitBitsIntoHalves <$> gets balance
canister_cycle_balance128 :: Int32 -> HostM s ()
canister_cycle_balance128 dst = do
i <- getsES esref inst
amount <- gets balance
setBytes i (fromIntegral dst) (to128le amount)

call_new :: ( Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32 ) -> HostM s ()
call_new ( callee_src, callee_size, name_src, name_size
Expand Down
10 changes: 5 additions & 5 deletions src/IC/Test/Agent.hs
Expand Up @@ -527,8 +527,11 @@ asWord64 = runGet Get.getWord64le
as2Word64 :: HasCallStack => Blob -> IO (Word64, Word64)
as2Word64 = runGet $ (,) <$> Get.getWord64le <*> Get.getWord64le

asPairWord64 :: HasCallStack => Blob -> IO (Word64, Word64)
asPairWord64 = runGet $ flip (,) <$> Get.getWord64le <*> Get.getWord64le
asWord128 :: HasCallStack => Blob -> IO Natural
asWord128 = runGet $ do
low <- Get.getWord64le
high <- Get.getWord64le
return $ fromIntegral high `shiftL` 64 .|. fromIntegral low

bothSame :: (Eq a, Show a) => (a, a) -> Assertion
bothSame (x,y) = x @?= y
Expand Down Expand Up @@ -843,6 +846,3 @@ 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
10 changes: 5 additions & 5 deletions src/IC/Test/Spec.hs
Expand Up @@ -1604,14 +1604,14 @@ icTests = withAgentConfig $ testGroup "Interface Spec acceptance tests"

, testGroup "cycles" $
let replyBalance = replyData (i64tob getBalance)
replyBalance128 = replyData (pairToB getBalance128)
replyBalance128 = replyData 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
queryBalance128 cid = query cid replyBalance128 >>= asPairWord64
queryBalance128 cid = query cid replyBalance128 >>= asWord128

-- At the time of writing, creating a canister needs at least 1T
-- and the freezing limit is 5T
Expand Down Expand Up @@ -1647,13 +1647,13 @@ icTests = withAgentConfig $ testGroup "Interface Spec acceptance tests"
[ 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)
bothSame (a, fromIntegral b)
, 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
query' cid replyBalance >>= isReject [5]
toI128 <$> queryBalance128 cid >>= isRoughly (large + fromIntegral def_cycles)
queryBalance128 cid >>= isRoughly (large + fromIntegral def_cycles)
]
, testGroup "can use balance API" $
let getBalanceTwice = join cat (i64tob getBalance)
Expand Down Expand Up @@ -1683,7 +1683,7 @@ icTests = withAgentConfig $ testGroup "Interface Spec acceptance tests"
, simpleTestCase "can accept more than available cycles" $ \cid ->
call cid (replyData (i64tob (acceptCycles (int64 1)))) >>= asWord64 >>= is 0
, simpleTestCase "can accept absurd amount of cycles" $ \cid ->
call cid (replyData (pairToB (acceptCycles128 (int64 maxBound) (int64 maxBound)))) >>= asPairWord64 >>= is (0,0)
call cid (replyData (acceptCycles128 (int64 maxBound) (int64 maxBound))) >>= asWord128 >>= is 0

, testGroup "provisional_create_canister_with_cycles"
[ testCase "balance as expected" $ do
Expand Down
14 changes: 5 additions & 9 deletions src/IC/Test/Universal.hs
Expand Up @@ -26,8 +26,7 @@ import Data.String

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

data T = I | I64 | B | PairI64

data T = I | I64 | B

-- We deal with expressions (return a value, thus have a type) and programs (do
-- something, but do not return a type). They are represented simply
Expand Down Expand Up @@ -228,24 +227,21 @@ onHeartbeat = op 49
performanceCounter :: Exp 'I64
performanceCounter = op 50

getBalance128 :: Exp 'PairI64
getBalance128 :: Exp 'B
getBalance128 = op 51

getAvailableCycles128 :: Exp 'PairI64
getAvailableCycles128 :: Exp 'B
getAvailableCycles128 = op 52

getRefund128 :: Exp 'PairI64
getRefund128 :: Exp 'B
getRefund128 = op 53

acceptCycles128 :: Exp 'I64 -> Exp 'I64 -> Exp 'PairI64
acceptCycles128 :: Exp 'I64 -> Exp 'I64 -> Exp 'B
acceptCycles128 = op 54

callCyclesAdd128 :: Exp 'I64 -> Exp 'I64 -> Prog
callCyclesAdd128 = op 55

pairToB :: Exp 'PairI64 -> Exp 'B
pairToB = op 56

-- Some convenience combinators

-- This allows us to write byte expressions as plain string literals
Expand Down
43 changes: 24 additions & 19 deletions universal-canister/src/api.rs
Expand Up @@ -4,19 +4,12 @@ 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() -> Pair;
pub fn canister_cycle_balance128(dst: u32) -> ();
pub fn canister_self_copy(dst: u32, offset: u32, size: u32) -> ();
pub fn canister_self_size() -> u32;
pub fn canister_status() -> u32;
Expand All @@ -28,9 +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) -> Pair;
pub fn msg_cycles_available128() -> Pair;
pub fn msg_cycles_refunded128() -> Pair;
pub fn msg_cycles_accept128(max_amount_high: u64, max_amount_low: u64, dst: u32) -> ();
pub fn msg_cycles_available128(dst: u32) -> ();
pub fn msg_cycles_refunded128(dst: u32) -> ();
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;
Expand Down Expand Up @@ -194,32 +187,44 @@ pub fn cycles_available() -> u64 {
unsafe { ic0::msg_cycles_available() }
}

pub fn cycles_available128() -> Pair{
unsafe { ic0::msg_cycles_available128() }
pub fn cycles_available128() -> Vec<u8> {
let size = 16;
let mut bytes = vec![0u8; size];
unsafe { ic0::msg_cycles_available128(bytes.as_mut_ptr() as u32) }
bytes
}

pub fn cycles_refunded() -> u64 {
unsafe { ic0::msg_cycles_refunded() }
}

pub fn cycles_refunded128() -> Pair {
unsafe { ic0::msg_cycles_refunded128() }
pub fn cycles_refunded128() -> Vec<u8> {
let size = 16;
let mut bytes = vec![0u8; size];
unsafe { ic0::msg_cycles_refunded128(bytes.as_mut_ptr() as u32) }
bytes
}

pub fn accept(amount: u64) -> u64 {
unsafe { ic0::msg_cycles_accept(amount) }
}

pub fn accept128(high: u64, low: u64) -> Pair {
unsafe { ic0::msg_cycles_accept128(high, low) }
pub fn accept128(high: u64, low: u64) -> Vec<u8> {
let size = 16;
let mut bytes = vec![0u8; size];
unsafe { ic0::msg_cycles_accept128(high, low, bytes.as_mut_ptr() as u32) }
bytes
}

pub fn balance() -> u64 {
unsafe { ic0::canister_cycle_balance() }
}

pub fn balance128() -> Pair {
unsafe { ic0::canister_cycle_balance128() }
pub fn balance128() -> Vec<u8> {
let size = 16;
let mut bytes = vec![0u8; size];
unsafe { ic0::canister_cycle_balance128(bytes.as_mut_ptr() as u32) }
bytes
}

pub fn stable_size() -> u32 {
Expand Down
29 changes: 4 additions & 25 deletions universal-canister/src/main.rs
Expand Up @@ -8,7 +8,6 @@ enum Val {
I32(u32),
I64(u64),
Blob(Vec<u8>),
PairI64(api::Pair),
}

struct Stack(Vec<Val>);
Expand All @@ -34,10 +33,6 @@ impl Stack {
self.0.push(Val::Blob(x));
}

fn push_pair_int64(self: &mut Self, p: api::Pair) {
self.0.push(Val::PairI64(p));
}

fn pop_int(self: &mut Self) -> u32 {
if let Some(Val::I32(i)) = self.0.pop() {
i
Expand All @@ -61,14 +56,6 @@ impl Stack {
api::trap_with("did not find blob on stack")
}
}

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")
}
}
}

// Reading data from the operations stream
Expand Down Expand Up @@ -301,33 +288,25 @@ fn eval(ops: Ops) {

// 128-bit cycles API
51 => {
stack.push_pair_int64(api::balance128())
stack.push_blob(api::balance128())
},
52 => {
stack.push_pair_int64(api::cycles_available128())
stack.push_blob(api::cycles_available128())
},
53 => {
stack.push_pair_int64(api::cycles_refunded128())
stack.push_blob(api::cycles_refunded128())
},
54 => {
let low = stack.pop_int64();
let high = stack.pop_int64();
stack.push_pair_int64(api::accept128(high, low))
stack.push_blob(api::accept128(high, low))
},
55 => {
let low = stack.pop_int64();
let high = stack.pop_int64();
api::call_cycles_add128(high, low)
},

// pair to blob
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());
stack.push_blob(bytes)
}

_ => api::trap_with(&format!("unknown op {}", op)),
}
}
Expand Down