go-algorand
go-algorand copied to clipboard
Increase max application program size beyond 8kb
Status
The maximum program size for an application is currently 8kb. Once a contract is deployed, the program size is immutable.
This limit is currently constraining multiple builders in the ecosystem who have hit the limit and are using creative approaches to optimize program size. The limit could also constrain future upgradability of contracts that are deployed today with max program pages but may need to add additional logic in the future.
Expected
Raising the maximum program size would enable developers to create more capable applications on Algorand without resorting to contortive optimizations.
Solution
Increase the maximum program size substantively.
One approach that could be elegant is to align the max program size to the max box size, which is currently 32KB. This way, an app factory pattern could store program bytes in one box and use that to create new applications. If this approach was followed, any future expansions of the box storage limit would also apply to the maximum program size.
Dependencies
Unknown
Urgency
Relatively urgent. Three different ecosystem projects have recently escalated to DevRel that they have hit the program size limit, so the current limit is actively constraining builders in the ecosystem.
I just want to point out that right now the max amount of data the AVM loads for a single app eval is 16KB (8KB for program, 8KB for boxes). I imagine increasing that amount would have performance implications.
We could do something simmilar to #6057 for called programs in a group, but I think that would really hurt composability because it's not immediately obvious to developers which apps can be called together.
I just want to point out that right now the max amount of data the AVM loads for a single app eval is 16KB (8KB for program, 8KB for boxes). I imagine increasing that amount would have performance implications.
This is the problem. We don't put in arbitrary limits for the fun of it. When boxes were introduced, we created a system to ensure that, even if the boxes were big, the I/O caused by evaluating a transaction was not greater than it used to be.
Unfortunately, we have no such quota system on programs. So, very large programs would, potentially, cause a great deal of I/O. Consider, for example, that one transaction can call 8 different foreign apps. If each of those apps was 32kb, we need to read 9*32kb for a single transaction.
I do not have a great idea for fixing this. Using such large programs is pretty intensive for a chain that intends to run txns at 10,000 TPS. I have the unfounded belief that programs bigger than 8kb probably have a lot of room for optimization, so I would like someone to spend some time trying to understand if there are improvements to puya's compilation (presumably we're not talking pyteal anymore) that could save space. That could include adding opcodes for very frequent sequences that could be shortened.
My not so great idea for fixing this more directly would be try and come up with a way to bring large programs into the box quota system. If you invoke a large program, that would count against your I/O quota somehow. I suspect people would quickly run into other limits because of that choice. A completely unfleshed out thought is to somehow make it possible to invoke a program in a box. Then we let the quota system work as is.
The solution using what we currently have availible to us is just breaking down your logic into multiple on-chain apps with different apps implenenting different methods. The main problem with this approach is that application can't share state access, so you need a lot of inter-app communication (ie. scratch, global/local, itxns, etc.) which can get rather complex.
Rather than allowing one program to be larger than 8kb, what if we allowed apps to share state access (both read + write, including boxes)? Presumably this would be an optional field in appl that can be used when the programs are first deployed on-chain.
Edit: So I suppose I'm asking if developers really want programs larger than 8kb or if they just want > 8kb of logic to be able to interact with the same state. I would suspect it's the latter. I am aware though that this would also introduce a lot of challenges with regards to compilation, deployment, composability, etc.
Correct - because box storage is opaque, to have independent 'helper' contracts expected to act as app a + (helpers b, c, d) doesn't work. You can't call A which calls B which then has to call back to A to read box state - no re-entrancy allowed. Alternative then becomes passing everything B might need as part of its call. There's also the issue of B knowing its being called from a 'true' A and the various machinations that might involve (and extra calls). So even ignoring the extra work for contracts (if even possible tbh), the extra work for the AVM with all these workarounds would I think be far greater than just allowing larger contracts. Having to burn up extra transactions to cover the size would probably be ok - we often have to burn transactions already to cover the various references.
I disagree with increasing the maximum program size at least for now that we do not have any super complex logic app on Algorand! Instead, I encourage better TEAL code optimization as we know higher-level compilers do not generate optimized TEAL code! Also trying to use better logic architecture and breaking down processes in DAG structure usually helps greatly with this IMHO. Can we have some of App Ids of those projects in the ecosystem that told you about reaching the program size limit @SilentRhetoric? By checking their on-chain TEAL code may be we can have some good insights about if it could be done in more optimized way or not and by what percentage. Just a thought.
I disagree with increasing the maximum program size at least for now that we do not have any super complex logic app on Algorand!
I'm hitting these limits right now and they are a constraint for me. The request stands.
Would you mind sharing your App IDs experiencing such limitations @pbennett? Or a repo with contract codes if you haven't deployed yet. Also you @kylebeee , please mention App IDs if you have some smart contract code facing such limitations.
I've gone through the discussion and I've mixed feelings, so I'll leave my (not so strong) opinions:
- I think we should strive to optimize compiler's bytecode anyway (both in size and opcode budget), as there is no "Moore's Law" for the AVM (I know the metaphor is not that accurate but still...).
I'm hitting these limits right now and they are a constraint for me. The request stands.
- Supposing that current 8kb limit is being hit by Puya's/TEALScript programs, those would still be very valuable feedback for the teams working on the compilers, regardless if we end up increasing the bytecode size or not (cc: @pbennett).
Unfortunately, we have no such quota system on programs. So, very large programs would, potentially, cause a great deal of I/O. Consider, for example, that one transaction can call 8 different foreign apps. If each of those apps was 32kb, we need to read 9*32kb for a single transaction.
- I totally agree we should protect from the worst case scenario (8 inner calls to big programs) but I think also we should not sub-optimize or limit more frequent/common scenarios (e.g. no nested calls to larger programs) due to potential worst cases.
This make me think that the "AVM data pooling" and "Program size quota" approach suggested by @joe-p and @jannotti could be a path to explore, in two ways (not mutually exclusive):
- Program size pooled as 8kb/AppCall: if I have a 16kb program, it must be called by 2 AppCalls grouped;
- Program size pooled with Box access: if I have 16kb program I have no "available" Box references in that same AppCall.
Compiler optimizations should be discussed as a separate matter. The marginal gains in usable program space achievable through code optimization would be an order of magnitude smaller than what is proposed here to enable solutions which require substantially more room for business logic.
Would you mind sharing your App IDs experiencing such limitations @pbennett? Or a repo with contract codes if you haven't deployed yet. Also you @kylebeee , please mention App IDs if you have some smart contract code facing such limitations.
No, because it has nothing to do with this issue and you're sidetracking this issue. People asking for 4x the size isn't some 'well, you could save 30 bytes here...' issue.
This isn't a gee, if only the optimizer was better and I could get my 10K bytecode down to 2K !
Just adding one new feature to a contract I'm at 8286 now. Getting that below 8192 still wouldn't address the 'next' feature. Active developers are asking for the sizes to be increased - myself (multiple times over the years and just twice recently in the discord) and I'm told apparently some who don't want to be public. Let's keep this issue to the actual issue.
Let's talk these through.
Program size pooled as 8kb/AppCall: if I have a 16kb program, it must be called by 2 AppCalls grouped;
At the top-level, you have to make two app calls to the same ID in order to have them (both) execute? I think that could work, it seems pretty straightforward to implement. I suspect I will hear how ugly it is forever, but I'm used to that by now.
How does one create and update such large programs? It's already a bit worrisome that you can create enormous transactions with 8kb code size. It's certainly the single biggest opportunity to spam block space for min-fee. I suppose update is somewhat handled by the "two app calls" idea. You can only perform an update of 16kb if you have two calls to the app. (Need to make sure you can't update it twice with two 16kb programs - using double block space.). How do you "pay for" creating such a large program? You can't include two calls in the create, because the app has no id yet.
Program size pooled with Box access: if I have 16kb program I have no "available" Box references in that same AppCall.
If I understand the idea here, this is a separate, different approach, right? I think I prefer this approach, unifying the quota system a bit, rather than adding another separate "please send an empty transaction" pooling mechanism. People seem to enjoy cringing about that kind of pooling. I might prefer to be explicit. A transaction would include a number, from 1 to 8, which is how many resource slots it's using for program access/update quota beyond 8kb. Under current limits, if you want to call a 10kb program, you would set that value to 2 (10kb-8k)/1k (because a resource slot gets you 1kb of box quota). I would be amenable to raising that 1kb quota to 2kb, to make this a bit more palatable, I think we were overly conservative there. It would also be nice that the rule is "use 1 resource slot for each extra program page over 3".
There are still creation problems to work out, because now you can send one creation transaction with a 16kb payload. Maybe we can live with that. If we bump the amount per slot, you could send a 24kb payload. I'm not sure how much to worry about block space.
There should also be a larger min balance requirement for the program's account holder, it is using more ledger space. We (well, I) forgot to add that when extra pages were introduced.
Is it fair to say nobody wants to mechanism for incremental program updates? You could patch together a program piece by piece?
I'm again a bit taken by the idea of using boxes to hold programs. I don't see a way to make that simple enough though.
You could patch together a program piece by piece?
I actually think this would be the best approach. Essentially allow apps to give state read/write access to other apps. I imagine the implementation would be a new OnComplete likeShareState that allows another app to read/write all state (incl. boxes) in the called app (assuming program returns successfully). This allows larger programs to be assembled piecewise and also has interesting implciations for updateability since you can choose certain pieces of the program to be updateable or not.
Any sort of pooling solutions runs into a potential problem with composability (ie. two large apps might not be able to interact with eachother, even if they implement specific ARCs)
How does one create and update such large programs? It's already a bit worrisome that you can create enormous transactions with 8kb code size.
I already have to do this now for Reti as well as NFDs. You have to load from box storage because nothing larger than 4k can exist in scratch/stack/etc. So in box storage alone, the (one-time) MBR cost is pretty high just like 'extra pages' cost is fairly high. I have factory contract that has init/load/complete methods and load pages of bytecode into box storage. Later, the create app or update app loads the pages from box storage, ~4k at a time. In my case, I happen to have two controlling <8k contracts that act as a factory but ideally we should be able to create larger contracts (and there might need to be new mechanism there I admit).
Switching app storage completely to boxes would be a nice unifier tbh - certainly in costs and 'access'
Changing how contracts are even 'called' seems like a big change though as there are big ripple-affects that can be felt for a long time beyond the change. Wallets need to change (particularly hardware wallets which always take a long time for updates), SDKs, and everything using them. It's already a bit awkward as-is with the reference model where even with ARC56 and an ABI method, you can't just compose a call to app X method Y and expect it to 'just work' even w/ simulate. You might need 3 dummy transactions just to cover the box references simulate/populate have to fill in. So if even just calling app X with no other references needed more because app X was >8k - that could get interesting. Perhaps if it can be encoded into things like arc56(+) so composing of the calls can account for the extra transactions or new reference types - that'd cover it. If even 'part of the reference models' were going to be reconsidered I'd almost want the entire reference model to be re-evaluated for something more composable. There's always 'costs' - I just think the costs shouldn't be on the user to figure it all out. If simulate does it all - I'm cool with that - but instead of 1 app call but 4 txns being required with a litany of box references, app/account/asset/extra bytecode page references... I'd just prefer 1 app call 1 txn that happened to require all those references and it 'cost' the equivalent of those 4 txns in terms of what can fit into a block (to account for those references). This partly even flows into one of the huge gotchas with boxes today - being unable to use them in an atomic group where a created contract uses boxes.
@jannotti When you say "unifying the quota system a bit," are you contemplating a change that would allow apps to have read/write access to the state, including boxes, of other apps?
Joe has suggested extending box read/write to other apps' boxes, which I think would also be a transformational capability, so I want to clarify if that is on the table as we discuss application capabilities somewhat more broadly here.
Regarding @SilentRhetoric's comment just above, if that is what is being referred to I would also support it.
Extra transaction calls simply for accessing state from other apps in box storage is a pain.
Regarding @SilentRhetoric's comment just above, if that is what is being referred to I would also support it.
Extra transaction calls simply for accessing state from other apps in box storage is a pain.
It's not even that - because of re-entrancy not being allowed and depending on app design, it may be impossible. can't call app A which then uses helper B which then has to call back to A just to get data from box storage.
Joe has suggested extending box read/write to other apps' boxes, which I think would also be a transformational capability, so I want to clarify if that is on the table as we discuss application capabilities somewhat more broadly here.
No, that is not what I meant. I meant something closer to my response to @cusma, in which extra program space is explicitly paid for by MBR for ledger space, and resource references at run-time. And/or programs can be run from boxes, so code space is explicitly managed with the exact same quota system as boxes.
I don't think making boxes readable/writable by other apps is such a great idea for a few reasons. 1) It doesn't fix the actual problem here (code size), it just makes a difficult work-around work slightly better. 2) It will require a whole new set of opcodes, because each access of a box will require specifying the app. 3) People will (rightly) conclude that since box state is readable, it is part of the public API of an app, so they will demand a standardization effort. I think that's bad for creating apps that can manage their own internals however they like, a basic principle of building reliable systems. 4) Writable boxes from other apps would require yet another extension that I think would have to be quite elaborate, managing which apps can write another app's storage, and maybe which boxes are writable? Incredibly dangerous, yet surely the complaints about code space are not going to stop just because boxes become readable. 5) It recreates some of the problems of re-entrancy (which we disallow) because app A can call app B, which can then look into A's state. But A may not be in a consistent state, since it is in the middle of execution.
@pbennett , instead of attacking toward and accusing any opposite opinion to "sidetrack things" , may I suggest to keep it professional, adult and technical here (if you are able to).
1- Code and compilers optimization 2- Architecture Optimization (try to lean more on SOA approach rather than monolithic) I suggested these two not only compiler optimization.
Let's talk these through.
@jannotti thanks for expanding on this!
As a first step I just want to reach a point in which we have clear trade-offs about feasibility (performance) and complexity (both implementation and usage).
Some observations:
- At top-level we can have up to
16AppCalls in a group, each of them calling up to8kbof program size. This would suggest a pattern of8kb of program/AppCall. - Currently a top-level group of 16 AppCalls, using all the 8 shared references per transaction, could potentially fetch either
16 * 8 * 8kb = 1024kbof total program bytecode or16 * 8 * 1kb = 128kbof total box storage. - As a rule of thumb, if we consider the
8kb of program/AppCall(point 1.), then an inner-level group would potentially access up to256 * 8kb = 2048kbof bytecode, which is too much.
This leads to my second suggested approach based on a "quota system": treating the foreign App ID references similarly to the Box ID references, to limit program size accessible at runtime.
In this scenario 1 App ID would allow the access up to 8 kb of program. So to interact with a program of, say, 16kb the AppCall should "consume" 1 additional App ID reference (maybe to itself).
This being said, if we want to "generalize" the quota system to the App program and "unifying" the role of App ID and Box ID references, we would have a bit of disproportion between App bytecode and Box storage. If possible, increasing a Box "slot" to 2kb (equal to an App page size) would close a bit this gap, but it's just a nice-to-have.
I admit I've not spent so much time thinking about a "nice API" to use this App ID based approach, a rough idea would be:
- On creation you can specify a
0App ID that would extend your program bytes by8kb(of course the maximum number of extra pages must be incremented as well); - On interaction, you must specify the App ID of the "big" program, consuming foreign App references. Example: to call an App with a program of
24kb, your App call must consume 2 additional App references.
@jannotti this being said, I don't know if an approach based directly on Box IDs (as opposed to one based on App IDs) would be easier to implement / more elegant and usable. It seems to me that, in any case, the most elegant solution is introducing a quota system on program sizes.
Any updates or plans on this? I've basically stopped development on some things because of it.
I have been thinking about this some more, especially in light of #6286 which is intended to make it a little easier to talk about how resources are touched. Let's break down all of the places large programs consume resources, and try to account for them, while minimizing changes.
- The ledger space for programs needs to be accounted for with MBR. This is mostly easy. If extra-pages was allowed to be bigger, MBR on the creator would be bigger. No problem. Except along with this change, people will surely want the ability to grow program size. It's unfortunate that today you must set extra-pages to 3 in case you someday want to use 3 extra pages. I think that ought to be allowed (as well as growing/shrinking global, but not local, state schema). It has the unfortunate effect that a later update to a program by some other account can affect the MBR of the creator. I am going to make a separate issue #6386 for discussing and handling these changes, and I'll consider this "accounted for" here.
- The I/O to read large programs at call time (and write them for program and global var updates). Today, unfortunately, we allow program + globals reads of size 8kb + 64*128 = 16kb. We did this before we thought much about I/O quotas. Even if we deem the original program size (2kb) and global size (8kb) as "free", we "should" be taking away 3 resource refs to pay for the 3 extra pages that are each 2kb. (This assume the new quota per ref of 2kb, which should be in #6286.). At the very least, I think if we allow even more pages, that should come out of your available references. #6286 allows 16 references in
tx.AccessWhen you call a program with 5 extra pages, I think you should have to use 2 of them for the extra pages beyond 3. In fact, sincetx.Accessis not in yet, maybe we can retrofit a bit, and force you to use 5 refs for those 5 extra pages. - The transaction size for creates and updates needs to be big enough to hold these larger programs. You are consuming a lot of block space if you send a 16kb transaction to build your app. Again, our 8kb app creates are already an unfortunate outlier in this regard. We could require extra fee for these large transactions. It's hard to decide what to charge, we don't have anything that we currently charge per byte. (In theory, we do under congestion, but that never happens and needs to be reconsidered anyway.) I split this into a separate issue too. #6388
Extra references and that possibly leading to extra fees is fine, where things get uglier is if that also leads to extra transactions (notably the much hated, 'dummy' no-op app call transactions so we can get extra references). If we're heading to this going away and txn.Access is part of those changes then I don't see an issue with it at all, including closing up of gaps in original costing.
Just to make sure it's clear, we would want apps to initially be just 1 page (no extra pages), small global state keys, and later upgrade beyond 8k or more keys - so make sure that use-case is covered. This would allow initial creation at only 1 page cost maybe a few keys, but then future growth to be via the (now sounding like app account mbr) 'added' pages.
Understood. My main concern is changing the rules on entities that previously "knew" they would not have a greater MBR for reasons outside their control. One such entity is the creator. They did not sign up to have the MBR grow because the app later wanted it to. That seems to preclude using opcodes for the growth, or if opcodes are used, the MBR must be against the app account. On the other hand, there's the app itself. If we're going to charge MBR to the app account, we don't really want it to happen from txn fields, because existing apps do not know to check them for acceptibility.
So one possibility is that only the creator can use the transaction fields that would grow the extra pages with MBR charged to creator, while only the app (by executing opcodes) can grow the program in a way that gets charges against its MBR. It's all a bit gross, and I hope to think of something simpler.
There are some forms of the above where maybe the creator (or app) only has to make an explicit opt-in, and then the other growth technique is allowed.
I suppose if all growth MBR is charged to the app, and it can only happen in an update (in txn fields), we're probably not breaking any promises. If the update is possible, then the app implicitly is agreeing to the cost. And the creator is unaffected, so what do they care?
One thing I haven't said outloud yet - it'll be quite annoying to recover MBR used to grow the program. First you would need to shrink the app, then move the algos out, then delete. But I suppose it's non-trivial to recover box MBR too.
At least in terms of how boxes work (today 😀) the 'app account' getting the extra mbr for additional things the app itself does is in line with how boxes already work. They're all post create, so once app is standalone, new changes count against the app itself. That view at least makes it seem clean.
(and yes, MBR recovery (and MBR funding to be frank) is quite convoluted at times).