User-defined objects #1068
Replies: 5 comments 20 replies
-
|
Thank you for such a detailed write up! I really like the idea of adding more customizability/programmability to assets, and have been trying to think in this direction as well. There are a few things, however, that are not yet clear to me in the proposal. One such thing is who/how keeps track of the "bookkeeping data" about created objects. By bookkeeping data I mean things like the total number of issued coins for a fungible asset, or all issued non-fungible assets. Currently, we have a dedicated storage slot in the faucet accounts to keep track of this data - but how would this be handled in the model where any account can create objects of different types? Somewhat related to the above: in the current model, the assets are "static". That is, once issued they don't really change (an exception to this is the amount part of the fungible asset). One reason for this is that the original issuer needs to make sure they don't create "duplicate" assets. But if an asset can be modified after it is issued, this may become complicated. Another way to think about this is that we need to make sure that an asset's "vault key" remains stable throughout its lifetime while some other attributes of an asset are allowed to change. Also, it seems to me that we need to route most interactions with the objects through the vault. That is, instead of calling procedures on an object directly, we'd call a procedure on an object that is stored in a vault. This way, we can ensure that an object can be modified only via its "native" procedures. So, for example, instead of first removing an asset from the vault and then adding it to a note (via two separate procedures), we'd have to provide kernel procedures like: With this setup it would be possible to define assets generically. For example, the asset definition itself would know how to insert an asset into the vault and so, for example, for fungible assets Another question is what should be the capabilities of the native asset procedures. For example, it would be great to have the ability to define an asset that can only be transferred to accounts which meet certain criteria (e.g., the owner of the account is 18 years old or older). For this, native asset procedures should be able to call methods on the account itself (maybe via FPI?). |
Beta Was this translation helpful? Give feedback.
-
|
In discussing the approach from #1271 with others, I think some things did not become clear so I want to try and clarify some of these. Here is an overview of the current approach, depicting the three "components" involved in creating a treasury capability and token asset. Here we're executing a transaction against some user's account, which is the "native account". It calls out to the protocol-deployed Miden Account (via foreign procedure invocation), providing the standard functionality of token minting. It's called the Miden Account because it is protocol-owned and provides standard functionality needed in Miden, such as defining a standard token type. And then there's the transaction kernel, which exposes generic asset management APIs such as One of the things that wasn't clear is why we need the "Miden Account". Why can't we write the "create treasury" or "mint token" procedures as a library and run it from the native account directly? To illustrate further, suppose we have this type in Rust: pub struct Token {
amount: u64
}Because amount is not public, the field cannot be read or written to outside of the module it is declared in. This mechanism is what we're trying to get for assets as well, so the issuer of an asset can define exactly what can be done with a Pure MASM libraryLet's entertain the option of using a pure MASM library for this, though. To restrict the set of allowed procedures of an asset we could store the hash of the MASM library instead of the issuer as part of its bookkeeping data. The tx kernel could then check whether a mutation operation originates from one of the procedures in this library. The main problem with this is that the library hash is part of the globally unique identifier of the asset. E.g. if the library hash is Maybe there is a middle-ground here. Can we use an account ID as the asset issuer and use it to retrieve its account code which is then used as the library for asset operations? This is similar to FPI, but could perhaps be optimized to avoid context switching into the foreign account. Instead the tx kernel gets the calling procedure's hash using the Asset Faucet CallOne core issue we want to solve with asset programmability is to restrict transferability. One alternative idea from @Dominik1999 was (please correct me if I'm misrepresenting the idea) to specify a MAST root in the faucet account of the asset. When an asset is moved to/from an account it is checked if that root is This does solve the problem of restricting transferability. Being able to modify an asset on a transfer is also useful functionality, and we could enable this by making the signature of the called procedure look something like General ProgrammabilityOne of the downsides I see with this is that this solution is somewhat limited in its functionality but simple, which is good. Having a more general programmable asset model still comes with some advantages. For example, say we want to issue a token of flavor POL from account A and we want account B to be able to issue more such tokens. Right now, we could go with at least these two ways:
If we had programmable assets, faucet account A could send a capability asset that represents the ability to issue tokens (e.g. the treasury cap) to account B. Account B can issue more tokens using the treasury cap (e.g. This example shows that such general programmability can be a really good fit for the edge architecture of Miden. In such an architecture, a design where state lives in individual accounts rather than being public and shared is ideal because one doesn't have to solve the shared state problem in the first place. It also enables that functionality privately and is much lighter on network resources. Some other examples where this distribution of capabilities could be powerful:
Relationship to atomic composabilityThis asset model is most powerful if there is atomic composability, which Miden does not have at this time or perhaps won't ever have. The reason is that, at least in block chains with atomic composability such as Sui, such capability objects are often used when calling into foreign smart contracts. E.g. to update the So one question I'm asking myself is whether this asset model is useful enough without atomic composability and useful enough within Miden's architecture. |
Beta Was this translation helpful? Give feedback.
-
|
I've been thinking about an alternative design to the current (static) asset model that achieves the following:
I think that conceptually the below proposal sits somewhere between Philipp's user-defined objects (most flexible) and the current static asset model (not flexible and very coupled to the kernel). Notably, unless I missed something (quite likely!) it should lend itself to a significantly simpler solution that we could probably pull off in a few weeks. To illustrate how such generic assets could work, consider the proposed new approach for how an account would mint an asset and add it to the note's vault:
The vault key is just
Some MASM pseudo-code for the above: Now when we add or remove assets from notes (here The issuer MUST have procedures with the following signatures: and can define whatever logic they want: I think very similar logic could apply across the other dimensions:
Major implications and changes needed:
|
Beta Was this translation helpful? Give feedback.
-
|
At the offsite, we discussed various approaches to "programmable assets" which combine different aspects of @mmagician's and @PhilippGackstatter proposals above. The outcome was 3 different approaches varying in their complexity of implementation and flexibility that they would add to the protocol. Below is a quick summary of these approaches - @mmagician (and others) please correct if anything is wrong/missing. Approach 1This is the simplest approach needed to support something like "compliant private stablecoins". This approach does not change the current representation of assets - we still have fungible and non-fungible assets as we have now. But, a fungible asset issuer (i.e., a fungible faucet) can define "callbacks" which would be called when a fungible asset issued from this faucet is to be added or removed from an account's vault. These callbacks would act as predicates that would specify whether a given action is permitted. This would allow enforcing such things as blocklists for fungible asset transfers - i.e., if the callbacks return an error, the asset could not be added to or removed form an account. The main benefit of this approach is that it would be pretty simple to implement. The main drawback is that it probably wouldn't allow us to support much more beyond blocklists or something very similar. Approach 2This builds on the first approach but makes fungible assets more flexible. That is, callbacks would be used not as simple predicates to check whether an asset can be added/removed from the vault, but also to execute fungible asset merging and splitting logic. This would allow defining more complex fungible/semi-fungible assets. The main drawbacks here are:
The main benefit is that this approach keeps asset representations in a single word, and thus, is still relatively easy to implement. Approach 3This is the most flexible approach that is more along the lines of the discussion between @mmagician and @PhilippGackstatter. In this approach, we no longer differentiate between fungible and non-fungible assets - though, there are still two types of assets simple and complex (this is temporary naming which we can/should change). All assets are defined using 6 field elements (encoded as 2 words) such that:
The main difference between simple and complex assets is how the assets are stored in the asset vault:
The faucets would still be expected to provide the logic for adding/removing assets from the vault. Using Rust, the interfaces of these procedures would look something like: impl MyFaucet {
/// Builds a value that needs to be inserted into the asset vault resulting from adding
/// the provided asset data to the vault.
fn build_merged_value(
account_id: AccountId, // ID of the account to which we are adding the asset
vault: &AssetVault, // reference to the vault of the target account
asset: AssetData, // data of the asset we are adding
) -> Word {
// here, we'd be able to read the current value of the asset from the vault
// modify it as needed based on the asset data, and then return the new value.
//
// - For fungible assets the logic here could be as returning current_amount + new_amount
// - For NFTs this could be reading the current SMT root from the vault, and adding
// the new asset to the SMT, and the returning the root of the new SMT
//
// we can perform a number of checks here
// - check if the account is on some blocklist.
// - check if the vault contains some other assets.
}
/// Builds a value that needs to be inserted into the asset vault resulting from removing
/// the provided asset data from the vault.
fn build_split_value(
account_id: AccountId, // ID of the account from which we are removing the asset
vault: &AssetVault, // reference to the vault of the target account
asset: AssetData, // data of the asset we are removing
) -> Word {
...
}
}Another way to think about this is is that asset vault kind of becomes like "storage" where the storage logic is defined by the asset issuer. For "simple" assets, the issuer gets 32 bytes of storage (i.e., a word), for complex assets, the issuer gets a full SMT worth of storage. This approach is very flexible, but it is also the one that would require the most effort to implement. Main challenges are:
One somewhat annoying thing is that we can't allocate more than 4 field elements of data for asset values, which means that assets that require, say, 64 bytes to represent cannot be represented natively - but i'm not sure there is a way to get around this. |
Beta Was this translation helpful? Give feedback.
-
From an implementation perspective, the encrypted token is a great example for the double-tree. I think Penumbra’s current usage mainly turns a public token into a shielded token to make it usable as a privacy token. But since we can already have naturally private tokens on Miden, I’m not sure this would add any extra value from a privacy perspective.
I believe there is no issue issue with exchange-rate–based tokens since (I don’t think it makes sense to merge based on the exchange rate, since it can change every second, even every millisecond) we can already merge by I think a private yield-bearing stablecoin is a very fitting example here. The idea is that users hold a stablecoin and are earning a small return over time. This already exists in many banks as well, where you get an interest rate just for holding a regular currency such as USD/EURO. There are already non-private versions of this use-case in Ethereum.
These two versions can be implemented using Approach 3. There is also a version of this implemented via an exchange rate: "syrupUSDC". But when we look at it, the current rate is around $1.14 (functionally it behaves like a yield-bearing stablecoin, but visually it no longer looks like a stablecoin). In other words, it is implemented like a simple coin using an exchange-rate. However, I don't think every token issuer desire to implement it with a similar approach. That’s why I think enabling both styles of implementation is the right approach if we want more variation and flexibility. @Dominik1999 @mmagician I have created a table below to compare the use cases with approach 1 & approach 3.
|
Beta Was this translation helpful? Give feedback.

Uh oh!
There was an error while loading. Please reload this page.
-
Motivation
The current asset model in Miden supports fungible and non-fungible assets, which are a way for users to represent value and ownership on-chain. This model couples the transaction kernel to these specific asset types as it handles construction, modification, and destruction of these assets. I think this coupling is undesirable as it makes the protocol hardcoded to those two asset types and makes extension from users impossible.
A more generic and decoupled approach would be to adopt an object-based model, where asset types are represented as user-defined objects defined outside the kernel but managed by it. The kernel would operate on these objects as opaque entities, focusing solely on object management rather than specific asset logic. This change would make the programming model more adaptable and better at handling diverse use cases. At the same time it would make the transaction kernel and protocol at large agnostic to the concrete assets/objects that are represented.
Idea
The basic idea is to have the transaction kernel manage objects. An object represents value or ownership, just like assets, but is user-defined. The details of each object would be implemented outside the kernel, keeping the kernel decoupled. In this model the kernel would only know about objects as black boxes and offer generic methods for creating, setting and getting fields as well as destroying them.
To illustrate this further, here are a few bullet points to hopefully get the idea across. I might be missing some details of how the current asset model has evolved and whether some of its limitations are byproducts of constraints I don't know about. I might even be thinking incorrectly about the asset model. It's intended both as a proposal but also as a way to learn more about the design choices that led to the current model in case I have misunderstandings.
I'll try to explain what I mean in more detail with an example. Let's say we want to represent a fungible asset issued by a faucet in this object model.
miden_stdlibrary (i.e. aMastForest) which is published as a package on-chain. It could contain amiden_std::Cointype to represent what are now fungible assets.0to differentiate between different object types from the same package.miden_std::Coinobjects have different IDs.miden_std::Coincan only be modified throughmiden_std::Coin::*procedures.Here is a more concrete visual example for how issuing a new type of
Coinmight work, which builds on the proposed VM component model.In this example, a user wants to issue a
Coinof theExampleflavor, i.e. a type they defined. To do so, they execute a transaction against their own account whose code package isuser_package. Here, every package is run in a separate VM component whose ID is the package ID (assuming that's feasible). When the account callsmiden_std::Coin::new, it passes a pre-existingExampleobject (just to keep this simple) from its vault andmiden_std::Coin::newwould verify that the issuer of thisuser_package::Exampleobject is the calling component, i.e. that the issuer account ID and the component ID are equal. This might still require something like thecallerinstruction. This is something only theCoin::newprocedure would do. ACoin::get_amountprocedure could be executed by any caller as it does not require any kind of authentication.In the second step,
kernel::create_objectwould create a newCoinobject whose issuer is set to the component ID ofmiden_stdwhich is the same as themiden_stdpackage ID. So more generally, this allows objects of typepackage_id::Typeto be created/modified/destroyed only by procedures that are defined in the package with IDpackage_id.The
Coinobject would store in one of its fields that it is of flavorExample. Then, twoCoin<Example>objects could be merged into one by aCoin::mergeprocedure, but it would fail if called onCoin<Example>andCoin<OtherType>.The
Coinobject is returned to the control of the user account component and it can choose to store it in its vault or transfer it to a note via a call to another kernel procedure.I hope this covers a rough, high level idea of this approach.
Advantages
NameObject { name: Word }type which represents ownership over a name and the capability to update it.StakedExampleobject which represents a receipt for having stakedCoin<Example>and represents the capability to unstake and retrieve the staked coins.Cointype there could be aTreasuryCapabilityobject type which gives whoever owns it, the right to increase or decrease the supply of aCoin<T>. Burning the object would make the supply verifiably fixed. This is the kind of stuff we could technically have with the asset model as well, by introducing a "fungible faucet treasury capability", but we wouldn't realistically do it because it would be a manual extension of the protocol. With an object based model this would be comparatively easy.CoinandTreasuryCapability).NameObjectas a non-fungible asset, which must be hashed first to verify it is the actual pre-image. This must be done for every user-defined asset in the current model, iiuc.NameObjectrather than a name object which is hashed into aNonFungibleAsset. E.g. a name service could describe in its API docs that to change the ID to which a name points, aNameObjectmust be provided to prove ownership over it rather thanNonFungibleAsset, which is less clear.miden_stdand the kernel can evolve independently over time.NonFungibleAssetobject, that works the same as the current one, i.e. it commits to something. In the future, new object types could be defined without having to upgrade the protocol.Drawbacks
Inspiration
The above is an idea for an object-based model on Miden, replacing the current asset model. It is inspired by Sui Move's object-based programming model. I personally really enjoyed using their object model as a developer and think that Miden could benefit from something similar, so the above is an adaptation to hopefully fit into Miden's architecture. Here is a link to Sui's
Cointype to get an idea of what methods could be possible and how different object types could be used together.Conclusion
There are probably many details I haven't though of, as this would be a pretty big change. I would be interested if anyone sees the merits in something like this, especially from a developer perspective and if we should explore this further. Thanks for taking the time to read and I would be happy to receive feedback on this!
Beta Was this translation helpful? Give feedback.
All reactions