Some calls to the management canister can be made as an ingress message. Then obviously the target canister has to pay for them. The spec hast his to say:
It is possible to use the management canister via external requests (a.k.a. ingress messages). The cost of processing that request is charged to the canister that is being managed.
Is it documented somewhere what the calls actually cost? Is it equal to the cost that a canister would pay for an outgoing inter-canister call? Is there any charge for the response size?
Do all of the management canister calls have a cost or are any of them free? Especially the ones that one may want to do if the canister is low on cycles such as update_settings (for changing freezing threshold) or uninstall_code, do they also have a cost?
Some of the calls can also be made from another canister instead of via an ingress message. What happens then? Does the caller now pay his own cost or is it still the managed canister that pays?
The cost is the same as any other ingress message sent to any other canister. It depends on the size of the ingress message. More details in the usual spot in Fee breakdown | Internet Computer.
Do all of the management canister calls have a cost or are any of them free? Especially the ones that one may want to do if the canister is low on cycles such as update_settings (for changing freezing threshold) or uninstall_code , do they also have a cost?
Yes, they all have a cost. Exception for update_settings is that we attempt to apply the cost after the call is processed to allow for some edge cases when setting the freezing threshold (e.g. you have a very high one and you want to drop it, this could become super difficult otherwise because your canister would be considered frozen).
Some of the calls can also be made from another canister instead of via an ingress message. What happens then? Does the caller now pay his own cost or is it still the managed canister that pays?
Ok. So let me summarize my understanding. Is this correct?
If I call the management canister to manage something on target canister X then X bears the same cost as if I called X with an update message of the same size.
The difference is in execution:
there are no “instructions” in the management canister to which per-instruction cost would apply
there is no reserve taken for instructions in a management canister call (like the 40 billion reserved in an update call to X)
The exception is update-settings when setting the freezing threshold, but update-settings for other purposes like changing the controller is not an exception.
What does attempt mean? It means you charge it but if the balance is too low then you forego it?
And is there a reason that delete_canister is not an exception? It might seem counter-intuitive to some people that deletion has a cost and that it can fail on low balance.
there are no “instructions” in the management canister to which per-instruction cost would apply
Not exactly true. Some requests (e.g. install_code, take_canister_snapshot and a couple others) can actually be heavy and we count instructions whose cost is applied to the target X.
there is no reserve taken for instructions in a management canister call (like the 40 billion reserved in an update call to X)
There’s a reservation but in most cases it would be given back to you in full unless we’re in one of the above cases that cost you something for execution. It can actually be even more than 40B because install_code for example has a 300B total limit.
The exception is update-settings when setting the freezing threshold, but update-settings for other purposes like changing the controller is not an exception.
I believe the exception is universal for update_settings, i.e. we do not check whether you try to update the freezing threshold.
What does attempt mean? It means you charge it but if the balance is too low then you forego it?
Correct.
And is there a reason that delete_canister is not an exception? It might seem counter-intuitive to some people that deletion has a cost and that it can fail on low balance.
Ideally, there are no exceptions. Requests are charged because as usual they consume capacity of the system that needs to be accounted for somehow. The update_settings exception was added post-fact when users faced difficulties fixing their threshold due to a very large value. I was not a fan of this change personally but there was also not a better idea at the time.
It would be nice to have a place of documentation for this. A table of all the calls with the amount of reservation and the cost (if any). For example I see that there are various calls which require a minimum balance in the order of 1.3m cycles (e.g. update-settings, status, delete). I can’t tell in detail what this amount is composed of. Maybe it is just the cost of an update call plus some reservation for response bytes. I don’t know if there is a reservation for instructions involved and if there are any instructions billed in the end. A table would really be nice. And the reservations are important to have in the table for understanding how code behaves at a low cycle balance.
I confirmed by experimentation that update-settings --freezing-threshold work on any cycle balance but update-settings --add-controller requires a minimum balance in the order of 1.3m.
Ok, understood. In the case of delete_canister it is just a little weird. After all it is a call that frees system resources. Since it can only be called once per canister there isn’t really an attack vector either. It is weird that if we have a depleted canister we cannot simply delete it. We have to top it up first to delete. That requires more system resources that simply deleting it would use.
This would apply to all calls. There’s nothing special really in terms of the cost. The regular ingress induction applies which depends on the size of the request per the page I linked earlier. The base fee for ingress induction is 1.2M cycles and then you have 2K per byte.
I don’t know if there is a reservation for instructions involved and if there are any instructions billed in the end. A table would really be nice.
This indeed is different for different management calls. We can add something in the docs about it. Again as I said earlier, there’s a class of calls that you are getting charged some instructions but some do not cost any. Btw, it seems like you were also asking whether there’s some reservation of instructions upfront when the message is accepted – this is not the case. The instructions will be reserved before we start executing the message (after accepting it), if there are not enough, you’ll get an error for the call, otherwise we proceed and refund what was not used in the end (again no special thing here, this happens for any other update call as well).
I confirmed by experimentation that update-settings --freezing-threshold work on any cycle balance but update-settings --add-controller requires a minimum balance in the order of 1.3m.
Ok, I spoke from memory without checking the implementation. Indeed, we only allow a specific case of update_settings to be charged later which is when you set only freezing threshold.
Ok, understood. In the case of delete_canister it is just a little weird. After all it is a call that frees system resources. Since it can only be called once per canister there isn’t really an attack vector either. It is weird that if we have a depleted canister we cannot simply delete it. We have to top it up first to delete. That requires more system resources that simply deleting it would use.
I’m afraid things are not that simple. You’re assuming non-malicious behaviour which unfortunately we have to deal with.
delete_canister can fail (e.g. the canister is not stopped), which means that a malicious actor can keep sending such messages for free in a loop without the target being deleted and taking up ingress bandwidth of the subnet without paying any cycles for it.
Is that correct? What about start_canister and stop_canister?
I see. So if the balance is below 1.2M then the call gets filtered early, by the accepting replica, and doesn’t make it into a block proposal.
Btw, I also assume that mgmt canister calls are subject to the freezing limit like all other calls, i.e. they can only access the liquid balance. Is that right?
I have a comment about canister deletion. I see a problem with a race condition between reading a canister balance, deletion and incoming cycle deposits. Cycle deposits can come from anywhere, for example from automated places such as CycleOps. Before deleting a canister I sweep it. But reading the balance and deletion isn’t atomic. Hence cycles can come in between the two steps. Then I don’t see the cycles and accidentally delete them. There is no way to prevent the acceptance of a cycle deposit. And there is no way to make a canister deletion conditional on a maximum balance.
Correct, start_canister and stop_canister are the same in this regard.
Btw, I also assume that mgmt canister calls are subject to the freezing limit like all other calls, i.e. they can only access the liquid balance. Is that right?
Correct.
I have a comment about canister deletion. I see a problem with a race condition between reading a canister balance, deletion and incoming cycle deposits. Cycle deposits can come from anywhere, for example from automated places such as CycleOps. Before deleting a canister I sweep it. But reading the balance and deletion isn’t atomic. Hence cycles can come in between the two steps. Then I don’t see the cycles and accidentally delete them. There is no way to prevent the acceptance of a cycle deposit. And there is no way to make a canister deletion conditional on a maximum balance.
Indeed, this is something that could happen. Let me bring up this point internally. I don’t think you can do much right now without the protocol adjusting something.