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

Specify time or event for note consumption in network transaction #582

Closed
Dominik1999 opened this issue Apr 9, 2024 · 10 comments
Closed
Assignees
Labels
enhancement New feature or request
Milestone

Comments

@Dominik1999
Copy link
Collaborator

Dominik1999 commented Apr 9, 2024

Feature description

As a user, I want to be able to specify certain conditions under which my note should be consumed. This is important for network transactions in which the operator picks notes for consumption and executes transactions.

For example, I want to specify that my note should be consumed at block height 1111 or only in odd blocks.

One way to communicate the intent for future consumption is using note metadata. A dedicated tag for future network execution could be created, and the user could define the rest via the aux_data field. There might be other more sophisticated ways.

However, the implementation also depends on the Node's mempool design. We should wait until #350 is finalized.

Why is this feature needed?

We need this feature as a building block for network transactions.

@Dominik1999 Dominik1999 added the enhancement New feature or request label Apr 9, 2024
@bobbinth
Copy link
Contributor

bobbinth commented Apr 9, 2024

I think, more broadly, we need to define a mechanism (or maybe a few) of how the node should decide when to include some note into a transaction. At the high-level, we have the following "triggers":

  1. Immediate execution: a note can be included into a transaction as soon as it is received by the node. Such notes could enter some "ready" pool and the node could pick notes from there based on some internal logic (e.g., fee maximization).
  2. Simple delayed execution: a note can be included into a transaction after block height $x$, or on every even block etc. This is basically when execution condition can be expressed and checked very cheaply. Such notes could be in some "pending" pool, and migrate into the "ready" pool once the condition is met.
  3. Complex delayed execution: a note can be included into a transaction after some arbitrary condition is met (e.g., slot $0$ in account $x$ is set to $y$). Such notes could also go into a "pending" pool, but what triggers them to move from "pending" into "ready" pool is not yet clear to me (primarily because checking such complex conditions proactively will probably be prohibitively expensive for the node).

The first 2 types of triggers can probably be encoded in the note metadata. The mechanism for the 3rd trigger still needs to be defined (e.g., a user may ping a node to indicate that the note is ready for execution - but this may lead to griefing attacks, and so this action should not be "free" for the user).

@greenhat
Copy link

As a user, I want to be able to specify certain conditions under which my note should be consumed. This is important for network transactions in which the operator picks notes for consumption and executes transactions.

Should we consider encoding such conditions in the note script itself rather than letting node to decide. Like if current_height() > 1111 {...}. Some malicious actor can run a tampered node without any checks.

@bobbinth
Copy link
Contributor

Should we consider encoding such conditions in the note script itself rather than letting node to decide. Like if current_height() > 1111 {...}. Some malicious actor can run a tampered node without any checks.

The conditions would be encoded in the script. I think the main challenge is how the node can learn about these conditions without analyzing or executing the script. To give a concrete example:

Let's say there is a note that can be consumed 1000 blocks from now. That is, if the node tries to execute the note's now, the execution will fail, but if the same note's script is executed after 1000 blocks, it will all work fine. Without knowing this condition the node has two options:

  1. Try to execute the note's script after every new block. For the first 1000 blocks the execution will fail, but on the 1001st block, it will all work out. This will result in a lot of waste and would be prohibitively expensive.
  2. Run some kind of analysis on the note's script to learn under which conditions it can be executed. This would be very cool but will require some sophisticated analysis tools. It will also be somewhat expensive, but this expense is paid once and maybe there is a way to compensate the node for this work (though, this requires more thought).

I feel like we'll need to do something like the option 2 above further down the road, but for now, I'm trying to figure out how we can minimize the number of cases where such sophisticated analysis would be needed. So, if we can tell the node (via note metadata) that the note's script is supposed to be executed in 1000 blocks, it would wait 1000 blocks and only then try to execute the script.

@greenhat
Copy link

The second option looks promising, but we should be prepared to deal with the false negatives (note can be executed, but analysis says it's not).

@Dominik1999 Dominik1999 added this to the v0.3 milestone Apr 11, 2024
@bobbinth
Copy link
Contributor

bobbinth commented Apr 16, 2024

I'm thinking we could add another field to the NoteMetadata struct called something like hint (but I'm open to other names as well). For now, this field, would encode the following enum:

pub enum NoteExecutionHint {
    /// Note execution hit is not specified.
    None,
    /// The note can be executed at any time.
    Always,
    /// The note can be executed after the specified block height.
    AfterBlock { block_num: u32 },
    /// The note can be executed in the specified slot within the specified epoch.
    ///
    /// The slot is defined as follows:
    /// - First we define the length of the epoch in powers of 2. For example, epoch_len = 10 is an epoch
    ///   of 1024 blocks.
    /// - Then we define the length of a slot within the epoch also using powers of 2. For example,
    ///   slot_len = 7 is a slot of 128 blocks.
    /// - Lastly, the offset specifies the index of the slot within the epoch - i.e., 0 is the first slot,
    ///   1 is the second slot etc.
    ///
    /// Putting this all together { epoch_len: 10, slot_len: 7, slot_offset: 1 } means that the note can
    /// be executed in any second 128 block slot of a 1024 block epoch. These would be blocks 128..256,
    /// 1152..1280, 2176..2304 etc.
    OnBlockSlot { epoch_len: u8, slot_len: u8, slot_offset: u8 }
}

The reasons why we may want to start with these set of variants:

  • None would be reserved for future situations when we may be able to do more sophisticated analysis on the scripts as mentioned in one of the previous comments.
  • Always is something we need to support for P2ID and SWAP scripts.
  • AfterBlock is not something we have now, but it is also a pretty basic thing which I think we'll need to support.
  • OnBlockSlot is something we can add later, but I wanted to list it here to illustrate what other conditions we could put into the hint. I do think that something like this would be quite useful as it would enable hybrid accounts where both the network and the owner of the account can update the account on predictable schedules (and thus would prevent race conditions). One potential use case for this could be centralized non-custodial exchange where most of the time the operator of the exchange would be processing orders, but in some slots, the network would be able to execute transactions against the account directly (e.g., to support forced withdrawals).

In terms of encoding, we could use 36 bits to encode the enum as follows:

  • 4 bits would be used to encode enum tag. This would give us up to 16 possible variants. This number is intentionally kept low not to make the hints overly complicated.
  • Up to 32 bits to encode enum payload, primarily to support specifying exact block height.

We can also reduce the overall size to just 32 bits by giving up some precision on block height for the AfterBlock variant - but not sure yet if this is worth it.

@bobbinth bobbinth modified the milestones: v0.3, v0.4 Apr 29, 2024
@bobbinth bobbinth modified the milestones: v0.4, v0.5 Jun 8, 2024
@igamigo
Copy link
Collaborator

igamigo commented Jul 31, 2024

Adding a new field to NoteMetadata means it doesn't fit nicely into a Word now. Would this be a problem? Or were you thinking it should be encoded into aux?

@bobbinth
Copy link
Contributor

bobbinth commented Jul 31, 2024

We can fit multiple logical elements into a single field element. For example, note_type is only 4 bits, and note_tag is 32 bits. Overall, we should keep note metadata at 4 field elements - and adjust encoding so that we could fit all the required data into these elements. Given the above, we have about 90 bits of data still available.

One thing to keep in mind though, we will need to increase the size of account ID and so the sender field will need to occupy more space. In the worst case, it would be 120 bits, but maybe we can have it be as small as 80 bits.

@igamigo
Copy link
Collaborator

igamigo commented Jul 31, 2024

Yeah, for some reason I was thinking that note_tag was u64 and I was keeping in mind that AccountId would grow in size, but what you said makes sense and so it should be no problem.
I pushed a draft PR that adds a sketch of the enum you described earlier for now.

@igamigo
Copy link
Collaborator

igamigo commented Aug 7, 2024

Created #816 with the remaining changes to implement this.

@bobbinth
Copy link
Contributor

Closed by #812 and #816.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
Status: Done
Development

No branches or pull requests

4 participants