NNS Proposal: Make canister_status public to anyone

Proposal

There is a function on the management canister called canister_status which gives you information about the canister like its controllers, wasm hash, cycle balance. Currently this can only be called by the controllers of a canister. I propose we make this callable by anyone.

Background

You can get some of the information from canister_status off-chain already like the wasm hash, and the controllers of a canister.

The topic of getting this information on-chain has been raised on the forum multiple times, but always resulted in a discussion without a definitive answer on what to do next. (I hope we see a more formalized ICRC process soon!)

There is also this RFC but it seems like that never really made it farther then initial thoughts / MVP

I have seen people post hacks on how to get around this. Like using the outbound http requests or calling the canister_status as a non controller and then parsing the error message to get the controllers. But needing to use these hacks is a horrendous dev experience.

During my time at DFINTY I was always in favor of making more canister info public, but now that Iā€™m working on a DAO framework not being able to get this info easily on-chain is especially annoying.

Arguments in favor

  • Prevent incidents like the lost IC turtles NFT collection. With canister_status being public anybody could have monitored the cycles and could have warned the owners or send in some cycles themselves. Imagine this happening to a token with a huge market cap.

  • Being able to trust a canister by confirming it has no controllers, the hash matches the code you expect, and it wonā€™t run out of cycles soon or that you know when to top it of yourself if the original devs abandon it.

Arguments against

  • You can just use something like the blackhole canister to expose the canister_status if you want to expose it.

I personally find this again a terrible developer experience needing to manage an entire extra canister and making sure that doesnā€™t run out of cycles as well, just to expose this one call that I think should be public by default anyways.

  • An attacker could use knowing the cycles balance to initiate a cycle drain attack right when it gets low.

I think the freezing threshold prevents this pretty well and I think important canisters running out of cycles because they get forgotten or nobody can monitor their cycles poses a far bigger risk to the ecosystem.

options

  • option A : Making canister_status public to anyone.
  • option B: Adding a new public API to the management canister that gives you the same information you can get off-chain like wasm hash, controllers, and running status without needing to resort to work arounds.
  • option C: Make canister_status something that the controller can choose to expose or not with a flag which is set to public by default
  • option D: Make canister_status something that the controller can choose to expose or not with a flag which is set to private by default
  • option E: Do nothing

I suspect that option A would be the least engineering effort, but I would love to hear the opinion from the foundation as that is just a guess.

I canā€™t imagine anybody being against option B at the minimum, but I really hope we can just go with A and potentially add a private flag in the future if there is sufficient demand.

Next steps

The first step is for devs to voice their opinions here about which option they would prefer and why. I would really appreciate it if an engineer DFINITY who knows the management canister well could offer their thoughts as well with regards to the engineering effort.

If we reach consensus on which option seems to be the most popular I would love it if one of the existing named neurons who is used to making proposals could do it for me. This way I donā€™t have to set up a 10 ICP neuron for making the proposal (Iā€™d rather keep my personal one private). I can transfer the 10 ICP if it gets rejected.

14 Likes

If you lead the deliberation on this topic and arrive at the final text you want to use in the proposal, then Iā€™d be happy to submit the Governance Motion proposal for you.

Also, are you prepared to implement the code changes that would be needed for whatever form of the proposal passes the Governance Motion proposal stage? In my experience, itā€™s difficult to tell if proposals that are passed by the community make it onto a priority list at DFINITY. In this particular case, the idea has been discussed and is supposedly already on a backlog, so it doesnā€™t seem that passing a Governance Motion proposal will change that status. However, it you develop the code, I suspect you can collaborate with DFINITY to get permission to implement it yourself through a proposal topic that actually changes the code. I would love to see this happen and you may have the resources/connections to pull it off.

1 Like

Thank you! that is greatly appreciated.

I donā€™t think coding it up is the main effort here. I believe option A would just be the removal of these lines in the code in canister_manager.rs:

// Skip the controller check if the canister itself is requesting its
// own status, as the canister is considered in the same trust domain.
if sender != canister.canister_id().get() {
    if let Err(err) = validate_controller(canister, &sender) {
        return Err(err);
    }
}

But the foundation would probably want to do a security review before removing those lines.

I donā€™t know if it would make sense for me to make the non-governance proposal(s). I would need to lockup 10 ICP (maybe more if every subnet needs an update) and go through the effort of creating the proposal. Something the foundation is already very experienced in.

I donā€™t know if this is on the backlog currently. This motion would be meant to get it on there or give it a higher priority. If it isnā€™t something they would want to do in the near future weā€™ll just have to resort to using workarounds like the outgoing http requests.

My 2 cents: Frankly, something like this option D is what I prefer.

I always rather start from a point of privacy as default when it comes to state. Inversely, if a developer or canister does NOT expose their status, their respective community should think hard about how much to trust it.

This would also work well now so it does not have to grandfather all the old previous canisters.

4 Likes

I like option D too.
Showing hash & controllers :+1:
But there is another argument against showing cycles. An attacker can check which one of your functions costs the most cycles to run. Not good info to give.

3 Likes

Fully support this. At a minimum, I think controllers and wasm hash should be easily obtainable on chain.
Currently also, if we make the canister_status call, the error already includes half the information we need. This is obviously hacky and a workaround, as weā€™re doing string manipulation to extract principal ids from the error response, but still goes to show its silly to block that info or not provide it in another way

1 Like

I think option D is appealing because it both preserves the existing behavior and enables your use case.

On board with D, except without providing public access to cycles remaining.

I believe one of the DFINITY teams is currently working on a project to expose the controller and wasm hash history. Iā€™m on board with exposing these first two (verifying integrity of the canister code/owners), but @infu brings up a good point with respect to cycles.

Not to mention other inference attacks, such as potentially being able to infer sensitive information/logic in code by inferring a difference in cycles available to the canister between API calls.

If anything, the owner of the canister can implement a common API interface to provide additional metrics such as cycles if they choose to do so.

I think this option is also what I would prefer too. The benefits to provide the status in general, however, outweighed to drawbacks. With option we have the benefit that it allows to preserve privacy as default.

On a side note, yesterday I tried to get the controller with this
[ The Internet Computer Interface Specification | Internet Computer Home ]
but I could only get the module_hash, the controller was null. Any ideas?
Considering hash and controller are already public data, I wonā€™t mind if they just go public inside IC without additional configuration if thatā€™s hard to do.

To clarify:

You think canister_status should never expose the amount of cycles left even when the dev needs to explicitly opt in to expose this?

I think exposing the amount of cycles left is one of the core reasons you would want to have canister_status public. For example to create:

  • automatic cycle top-up services
  • reassuring tokenholders that the token canister still has plenty of cycles left

Alternatively instead of option D being a simple boolean flag it could also be a variant with something like:

variant {
   Private;
   PublicWithCycles; 
   PublicWithoutCycles;
}
1 Like

The cycle data would still be available to the developer and controller(s), just not publicly available. I guess Iā€™d be open to the opt-in if the developer can later retract this and opt-out at any time of their choosing (say some vulnerability pops up that hasnā€™t yet been discovered).

Another option would be to make the cycles data publicly available to through canister_status once the freezing canister threshold is hit, since at this point community assets could be at risk and there is no further risk of a cycle drain attack once the canister is frozen.

If the canister wishes to provide cycles data access to a top-up service, they can expose this cycles data as an API.

If weā€™re talking about a NFT token collection and you want transparency, then the developer might open source the code, and one could match up the code with the wasm hash to verify the API is returning the correct amount of cycles.

I donā€™t have enough knowledge on the matter to give an informed opinion but this is what @rossberg had to say on the topic months ago:

1 Like

Fwiw Andreasā€™s posts are the ones that convinced me that a privacy-by-default approach may make sense in this case (with an option for developers to expose).

1 Like

Why does it worry you?

My bad, was going to write something else initially and forgot to delete that part :sweat_smile:

Hey everyone, Iā€™d like to share some of the current thinking on this topic. Btw, thanks @Fulco for bringing this up in a constructive way.

First of all, letā€™s take a look at the result returned by canister_status (taken from the interface spec):

canister_status : (record {canister_id : canister_id}) -> (record {
      status : variant { running; stopping; stopped };
      settings: definite_canister_settings;
      module_hash: opt blob;
      memory_size: nat;
      cycles: nat;
      idle_cycles_burned_per_day: nat;
  });

In the result there are certain fields that are private to the canister and its controllers, like cycles, the rate of burning, memory size and settings. The reason I say these are private is because they can reveal information about the canisterā€™s status and an attacker could even exploit some of them to learn patterns about the canisterā€™s usage or even force specific cycle drain attacks.

I think itā€™s a slippery slope if we take an approach where such private fields become public by default: it would break potentially existing assumptions some canister developers have and (worse imo) can surprise many people that never even thought that such attacks could be possible. In a sense, I very much agree with the opinions others have voiced in this thread about keeping the privacy of these fields and potential issues if we change them to public.

That pretty much excludes options A and C imo right from the get go. Even if we believed that everything should be public, we cannot break existing applications which all they know so far is that this information is accessible to controllers ā€“ that would also be very bad user experience.

Between B and D: we have discussed this internally a lot I must say. We are currently planning to provide B (essentially give access to the information thatā€™s already public like the controllers and wasm hash to anyone). The reason D should not be preferred imo is that itā€™s better for the system to not provide an option that can backfire in some cases (think of attacks that attempt to analyze your canisterā€™s cycles usage) and leave the option to expose this completely to the application level (e.g. with the blackhole approach that Fulco mentioned or if the canister decides to expose an endpoint that returns its own balance).

Lastly, I must also admit I have not heard compelling arguments why there should be an option at the system level to expose the cycles balance of canisters publicly. The use case of making sure that a canister does not run out of cycles can be covered nicely with e.g. tipjar purely on the application side. Iā€™m also a big fan of the idea that if something can be built on the canister-land it should be built there and not add more options to the system.

9 Likes

The cycles part can be solved if all canisters using a refilling service use a function like __refill which instead of giving cycles balance will specify how many cycles it wants and it can have its own threshold while rounding the number of requested cycles. Since you are all here, you can just agree on something and create the ā€˜IC refill protocolā€™.

2 Likes

Thank you for weighing in @dsarlis!

I think most comments here have said they would be in favor of D, so I think when this comes to a proposal that would certainly be the one to be suggested.

Iā€™m not convinced that a sophisticated cycles drain attack is such a huge issue compared with all the other footguns the IC has. Cycles are very cheap compared to other blockchains, there is an instruction limit per call, and there are a limited amount of messages per second that a canister can handle. There is a nice protection mechanism with the freezing_threshold and just depositing a boatload of cycles into anything critical.

Personally when developing a canister Iā€™m far more worried about exposing a single function somewhere that can be used to freely add state and can be used to fill my canister up with junk.

With regards to doing anything that can be done at a canister level there I have to disagree. I heard the same arguments with adding variable intervals to the heartbeat functionality. Needing to rely on another canister is a pain for something so small. And after some feedback from the community it seems like DFINITY wants to provide it at the system level now.

With ethereum and other EVM chains smartcontracts are immutable, canā€™t run out of cycles, and they donā€™t suddenly get errors when they reach a certain amount of state.

When you rely on any other service on the IC for something critical you have to make sure that all these things are taken care of which comes down to needing to do a full security audit. And even then you still have to monitor the cycles of every service you are using yourself if you want to be absolutely sure that it wonā€™t break.

I think anytime the system can provide something that is fairly basic that is a far superior option. Option D seem intuitively like far less work to me than the new heartbeat API. Itā€™s just adding an extra permission to an api that already exists.

One thing that would be especially nice compared to the blackhole canister is that everybody calling this system API would have to pay the cycles for it themselves as opposed to the owner of the blackhole canister.

I could just write a script that starts spamming the blackhole e3mmv-5qaaa-aaaah-aadma-cai for free right now. It could make any other canister relying on it way slower and also at the time of writing it only seems to have 4,019,244,002,015 / 1,000,000,000,000 = 4.019244 Trillion cycles.

Iā€™m not convinced that a sophisticated cycles drain attack is such a huge issue compared with all the other footguns the IC has.

I think we should be careful to not add more footguns if we can avoid it.

With regards to doing anything that can be done at a canister level there I have to disagree. I heard the same arguments with adding variable intervals to the heartbeat functionality. Needing to rely on another canister is a pain for something so small. And after some feedback from the community it seems like DFINITY wants to provide it at the system level now.

I believe a big selling point of the IC is the composability and interoperability among dapps. Having the system provide the minimum functionality to enable dapps should be the guiding principle and then people can build solutions on top of the basics (potentially multiple of them that can also cater for different use cases). The heartbeat example is actually a good example of the basic system functionality turning out to be incomplete.

I think anytime the system can provide something that is fairly basic that is a far superior option. Option D seem intuitively like far less work to me than the new heartbeat API. Itā€™s just adding an extra permission to an api that already exists.

I suppose this is the main part where we seem to come from different angles. I do not consider exposing private state of the canisters ā€œfairly basicā€. I think itā€™s something thatā€™s useful in certain use cases (like some you described in this thread) and not necessarily something that any dapp would want. The ability to optionally expose this for certain dapps already exists, so itā€™s unclear to me what we gain by adding the option to the system as well.

One thing that would be especially nice compared to the blackhole canister is that everybody calling this system API would have to pay the cycles for it themselves as opposed to the owner of the blackhole canister.

Thatā€™s not entirely true. If canister_status is public, what would prevent a random user to send such requests? The canister itself would still need to pay for those requests. The management api is not available through query calls so you have to send ingress messages that are charged to the canister whose status you requested ā€“ even if you could access it through query calls it would still be charged eventually when queries are charged in general and it would charge the canister. If D is to be implemented, we would also need to be careful about this aspect and not introduce potential attacks. So, to make it more clear, no I donā€™t think itā€™s about just adding a flag that allows canister_status to be read by anyone if the flag is true.

3 Likes