Programs

Nana introduces the concept of "programs" to the original Runes protocol.

Every program has its own unique Nana balance, just like UTXOs. A program can be defined in one sentence: A programmable script with an administrative ability manipulate the unallocated Nana payload in a Banana process, in which they can allocate Nanas to themselves, to the payload, or to another programs balance.

Essentially, programmable nanas

The idea behind programs starts with the realization that the very nature of an indexer allows a programmable layer to be built into the processing of a banana; in which additional functionality can be introduced. If you have a good think about it, a lot of the rules and functionality built into the original runes protocol are all very arbitrary. Mints, Etchings , Edicts that transfer to every input by simply setting the amount to the amount of outputs present in a transaction. The philosophy of Nanas is rather than bake in more and more features into the protocol and overcomplicate the core of the protocol, we do something different..

Meet the Call field. The call field, as the name suggests, calls an arbitrary script (or program) in the Nana ecosystem, which has the ability to add whatever arbitrary functionality they wish to the protocol through the manipulation of the unallocated Nana payload.

enum CallValue {
    Int(u8),
    Int(u16),
    Int(u32),
    Int(u64),
    Int(u128)
}

struct Call{
  programId: ProgramId
  methodId: u8,
  iteration: u8,
  //These are handled in a special manner, see tags (5 tags exposed)
  data: (
    Option<CallValue>, 
    Option<CallValue>, 
    Option<CallValue>,
    Option<CallValue>
    Option<CallValue>
  ),
}

Edicts and unallocated Nanas

The functionality of programs starts with "the unallocated nana payload" , but what does that actually mean? Well, just like runes - an unallocated balance is created during the processing of a Banana, which edicts transfer out from. The functionality of this can be seen below:

Essentially, nana programs are fed the nanas from etchings, mints and edicts (with position=0) which they can do whatever they want with. After the program finishes running, a new unallocated nana payload is created, which edicts at the end of a Banana process (position=1 ) can transfer out of.

An example use case could be something like a swap program ( (NIP1) bananaswap) where you mint an imaginary nana called Dog , swap it for another Nana (could be our imaginary NBTC nana mentioned in Concepts) and then transfer the NBTC out of the Banana with an edict with position=1 , which would process after the swap. You essentially did 3 nana operations (mint, call (which did the swap) and transfer) in one transaction.

Methods

Programs are essentially like smart contracts, they expose methods that add specific functionality to a program. There are two types of methods in nanas: "view" and "state_changing"

"state_changing" methods change the Nana state, and can only be called through the execution of a Banana.

A deep insight into this functionality can be found here: Nips

The "call" parameter can decide what state_changing method to call by passing its method id (a u8).

Executable Calls that call a view method are skipped immediately, as these have no change to the Nana state.

Balances and Boxes

Every program on Nana has its own balance, and a vector of "boxes", each with its own balance.

A box is a balance group, controlled by the program, representing a specific subset of the programs entire balance.

The programs available balance is its main balance plus the aggregated Nanas of all boxes it owns.

struct NanaId{
  block: u32,
  tx: u16,
}

struct ProgramId {
  block: u32,
  tx: u16,
}

struct BoxId { 
    program: ProgramId,
    id: u32
}

enum Destination {
    Str(String), //unallocated
    ProgramId, //Transfer to a programs main balance
    Box //Transfer to a box within a program.
}

Example:

  • Banana calls program A

  • Program A calls program B

  • Program B can alloc() to itself any nanas from banana's unallocated buffer or from program A's balance (or any box owned by Program A)

This allows for complex transfer logic to exist on Nanas, potentially bringing DeFi to the protocol, where programs can coexist and transfer nanas between each other.

Storage

Along with their Balance, nana programs also have their own storage, represented as a JSON blob.

About stateful protocols

A protocol being "stateful" refers to the concept that anyone can run the entire history of all call()'s on Nanas and get the same Nana state back, that is every single Balance, UTXO, storage blob, etc has to be the same and agreed upon by all Nana indexers.

This is because ANY VARIABILITY pinetiivably produces disagreements about the Nana state between indexers, and creates two completely different transfer and call history. Because of this, programs must not be able to add any variability into their methods. Example of this could be a random function that uses UNIX time as a seed (such as Math.random()) where UNIX time might be different on indexers depending on when they build the program.

Another example of variability could be Programs making inferences on whether they should drop a hanging call. Example lets say a method takes 200ms to run on one indexer, and 300ms on another. If there is a rule used by indexers to error a transaction if the transaction takes any longer than 250ms. Indexer 1 will consider the transaction valid and indexer 2 will not, and just like that an indexer disagreement is created. There's two ways to solve this issue:

Adding a runtime complexity tracker

The Ethereum virtual machine (EVM) solves this with the concept of "gas", where a solidity method is broken down to assembly instructions - and a gas tracker is kept based on the runtime complexity of a call. The higher the complexity, the higher the gas and the more the transaction cost.

  • This has two main problems specifically with Nanas. The first is that nanas is not a blockchain within itself, it doesn't have access to BTC transfers. We cant charge based on complexity because this functionality is available to the protocol.

  • Building a compiler and a language from the ground up that compiles down to assembly instructions for tracking runtime complexity is hard lol. Macros in Macros specificationwould need to have their own assembly instruction that communicates directly with the indexer, and then macros would have a gas price associated just like on the ethereum virtual machine.

Creating our own compiler also adds thousands of potential scenarios and edge cases where an indexer could break. However, with all that said - this is how programs should be implemented. A runtime complexity tracker that caps a transaction at x complexity at the assembly level would fix the hanging problem described, and allow indexers to un-anonymously agree on which transactions to drop or keep. People would inscribe bytecode on Bitcoin, indexers would fetch and compile the bytecode and then run each assembly instruction line by line, capping and dropping the transaction if x complexity is reached.

Nanas will allow for programs like these to exist on the protocol in the future - but such implementation should not be rushed and be developed with much care.

Whitelisted programs

The other solution (and the intial solution the protocol will use) is to add new programs directly to the indexers source code - with only programs that we can verify won't break indexers or cause invariability issues being added. Anyone can create a whitelisted program, named Nana Improvement Programs ( Nips ) by creating PRs on the main Nana repo: https://github.com/runeapespad/Nana These will be reviewed to check that:

  • The NIP has the protocols community best interest in mind (we won't approve rug nips :P )

  • The NIP has Bitcoins best interest in mind (example: that the nip is anti-mev)

  • The NIP does not cause invariability issues between indexers and is stateful

  • The NIP follows the design structure specified through all pages in this documentation.

Last updated