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

Increase max application program size beyond 8kb #6155

Open
SilentRhetoric opened this issue Oct 25, 2024 · 21 comments
Open

Increase max application program size beyond 8kb #6155

SilentRhetoric opened this issue Oct 25, 2024 · 21 comments

Comments

@SilentRhetoric
Copy link

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.

@joe-p
Copy link
Contributor

joe-p commented Oct 25, 2024

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.

@jannotti
Copy link
Contributor

jannotti commented Oct 25, 2024

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.

@joe-p
Copy link
Contributor

joe-p commented Oct 25, 2024

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.

@pbennett
Copy link

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.

@emg110
Copy link
Contributor

emg110 commented Oct 27, 2024

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.

@pbennett
Copy link

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.

@emg110
Copy link
Contributor

emg110 commented Oct 28, 2024

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.

@cusma
Copy link

cusma commented Oct 28, 2024

I've gone through the discussion and I've mixed feelings, so I'll leave my (not so strong) opinions:

  1. 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.

  1. 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.

  1. 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.

@SilentRhetoric
Copy link
Author

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.

@pbennett
Copy link

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.

@jannotti
Copy link
Contributor

jannotti commented Oct 28, 2024

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.

@joe-p
Copy link
Contributor

joe-p commented Oct 28, 2024

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)

@pbennett
Copy link

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'

@pbennett
Copy link

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.

@SilentRhetoric
Copy link
Author

@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.

@kylebeee
Copy link

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.

@pbennett
Copy link

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.

@jannotti
Copy link
Contributor

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.

@emg110
Copy link
Contributor

emg110 commented Oct 29, 2024

@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).

@emg110
Copy link
Contributor

emg110 commented Oct 29, 2024

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.

@cusma
Copy link

cusma commented Oct 29, 2024

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:

  1. At top-level we can have up to 16 AppCalls in a group, each of them calling up to 8kb of program size. This would suggest a pattern of 8kb of program/AppCall.
  2. Currently a top-level group of 16 AppCalls, using all the 8 shared references per transaction, could potentially fetch either 16 * 8 * 8kb = 1024kb of total program bytecode or 16 * 8 * 1kb = 128kb of total box storage.
  3. As a rule of thumb, if we consider the 8kb of program/AppCall (point 1.), then an inner-level group would potentially access up to 256 * 8kb = 2048kb of 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 0 App ID that would extend your program bytes by 8kb (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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

7 participants