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

Add design proposal for ProgramInstruction procedural macro #10763

Conversation

CriesofCarrots
Copy link
Contributor

Problem

It's difficult to parse on-chain instructions and get useful information about the expected accounts, as that information is buried in Instruction enum. We could make it easier by using a procedural macro to generate a 2nd enum that exposes account information. This would also have the benefit of enforcing consistency on program Instruction enum docs.

Summary of Changes

  • Add design proposal for ProgramInstruction procedural macro

@CriesofCarrots CriesofCarrots force-pushed the instruction-macro-design-proposal branch 2 times, most recently from ba9fc4c to fa02ce2 Compare June 24, 2020 01:53
@garious
Copy link
Contributor

garious commented Jun 24, 2020

The new syntax looks much nicer, thanks. Seems like there should be an option to generate all the instruction functions. Looks like a reasonable alg would be to snake case the instruction name, then one parameter per account, followed by one per instruction parameter. Coincidentally, those account names will allow us to generate readable parameter names.

@garious
Copy link
Contributor

garious commented Jun 24, 2020

Also, consider generating the enum from an annotated trait definition instead of generating functions from an annotated enum definition. Starting with a trait would better reflect that each instruction effectively defines a function on some set of accounts. Also, individual parameter docs would map cleanly to those account descriptions.

@CriesofCarrots
Copy link
Contributor Author

Also, consider generating the enum from an annotated trait definition

@garious Do you mind sharing some stub code of what you're thinking here?

@CriesofCarrots CriesofCarrots force-pushed the instruction-macro-design-proposal branch from fa02ce2 to c2e8069 Compare June 24, 2020 03:43
@garious
Copy link
Contributor

garious commented Jun 24, 2020

Ideally, something like this:

#[program]
pub trait Test {
     /// Consumes a stored nonce, replacing it with a successor
     fn advance_nonce_account(
         /// Nonce account
         #[signer] #[writable]
         nonce_account: &KeyedAccount,

         /// RecentBlockhashes sysvar
         recent_blockhashes_sysvar: &KeyedAccount,
     );
 }

This would generate enum TestInstruction, the instruction::advance_nonce_account(&Pubkey, &Pubkey) -> Instruction constructor, a trait Test with Test::advance_nonce_account, and an implementation of process_instruction that deserializes Instruction::data and calls one of those methods. One the user implements the trait, they're all done - all instruction construction and deserialization is automated.

Anyway, that's probably a lot more than you signed up for, so please just consider that to be the long-term goal, not the first step. If you only add the enum attribute at this time, imagine how that code would be generated from an annotated trait definition. Feels like the annotated enum might be a reasonable target for the annotated trait, such that the annotated enum is responsible for generating the instruction constructors. If so, it'd make sense to move forward with the annotated enum.

@CriesofCarrots
Copy link
Contributor Author

Thanks, @garious! One question: what would the user implement trait Test on?

To me, it makes sense to switch to whatever input type we want to land on now, and add output types incrementally.

That is to say, if we prefer the trait attribute long-term, I think it would work to implement that attribute now to output the regular and verbose enums. Instruction constructors could happen now as well, or as a second step. Automatically generating serializers and/or process_instruction is a bit thornier, since we aren't using the same serialization between native programs and SPL. So I imagine that might be a longer-term goal.

@garious
Copy link
Contributor

garious commented Jun 24, 2020

The trait would be for an empty struct.

I don't think we need to take on the trait approach just yet. Adding attributes to the enum would be an immediate win, because then we could generate most of the functions in today's "instruction" modules. I think I'd prioritize that deduplication before docs generation. Sound like docs generation is then the next step after that. A trait to generate the enum is probably a ways down the road.

@garious
Copy link
Contributor

garious commented Jun 24, 2020

By the way, instead of a trait, adding attributes to a module of functions might be more natural. Then there's no trait to implement. Parity's ink! is a neat example of that.

@CriesofCarrots
Copy link
Contributor Author

By the way, instead of a trait, adding attributes to a module of functions might be more natural. Then there's no trait to implement. Parity's ink! is a neat example of that.

Oh, nice. That seems much cleaner to me than having to implement the trait and declare a bunch of empty methods for no reason.

It seems to me that it would save a bit of work to settle on the input format now. That way we'll only need to write one parser, instead of writing a parser for the tagged enum now and another parser down the road. And it's not any more difficult to parse, say, a module of functions than an enum.
Various deduplication can still happen incrementally by using the parsed data to generate new output and adding it to the stream.

But if you feel strongly about pushing that off, I'll concede in the interest of getting partner work unblocked.

Do you have any thoughts about the considerations in my proposal?

@jackcmay
Copy link
Contributor

+1 on consideration #1 depending on what that means for backward compatibility

For consideration #2 how are multi-sig like programs handled? A new instruction for each vaue of M?

@CriesofCarrots
Copy link
Contributor Author

+1 on consideration #1 depending on what that means for backward compatibility

I believe this change should be backward compatible, since SystemInstruction::WithdrawNonceAccount { lamports: u64 } serializes the same as WithdrawNonceAccount::Transfer(u64), etc.

For consideration #2 how are multi-sig like programs handled? A new instruction for each vaue of M?

Great question. I sure would love some brilliant suggestions in this area.
If we went the mod attribute route, lists of accounts could perhaps be supported with a: ident: Vec<&KeyedAccount> parameter.
For the enum attribute route, maybe something like:

#[accounts(
        signers(is_signer = true, desc = "Funding account", allow_multiple = true),
    )]

In either case, mixing a multiple account list with other account declarations would get complicated (around indexing for docs and rpc decoding).

@jackcmay
Copy link
Contributor

@CriesofCarrots Referring back to Greg's comments here: #10783

I agree with Greg that tying the comments, the enum, and the instruction constructor together via a generator ensures that they are in sync and stay that way and we should probably approach this effort via that lens.

For the most part, optional accounts should only be used for simple situations and more complex optional account logic should be broken into multiple instructions.

A couple of cases:

  • Optional additional accounts can be marked as optional in the docs and the constructor can take an Option, where if Some push an AccountMeta for it
  • Be nice for multiple optional accounts that fall under the same category (multiple signers for example) to be handled as a list and documented together. Doing so would avoid ugly docs and function signatures that have explicit entries for each M.
  • More complex instructions that require logic to figure out order/representation could either result in the use of Option and leave the logic to the implementation of the instruction, or should probably be made into a separate instruction. Token's NewToken is a good example of that, it a single instruction now but could easily be made into two different instructions, and doing so might actually bring more clarity.

@CriesofCarrots CriesofCarrots force-pushed the instruction-macro-design-proposal branch from c5b4418 to 3c1d7bf Compare June 24, 2020 23:17
@CriesofCarrots
Copy link
Contributor Author

* Optional additional accounts can be marked as optional in the docs and the constructor can take an `Option`, where if `Some` push an `AccountMeta` for it

Agreed. However, I think we can only support one named optional account per instruction, unless we include some extra data for process_instruction to use to determine which optional account is present, when something between all and none are provided.

* Be nice for multiple optional accounts that fall under the same category (multiple signers for example) to be handled as a list and documented together.  Doing so would avoid ugly docs and function signatures that have explicit entries for each `M`.

Yep, this will also work, but similar to the Optional account, I think we can only support one multiple account collection and no named optional accounts, unless extra data etc.

* More complex instructions that require logic to figure out order/representation could either result in the use of `Option` and leave the logic to the implementation of the instruction, or should probably be made into a separate instruction.  Token's `NewToken` is a good example of that, it a single instruction now but could easily be made into two different instructions, and doing so might actually bring more clarity.

+1, I do think different instructions would add clarity in the NewToken case.

I've updated the doc with a possible example of optional/multiple handling. How is this looking?


/// Provide M of N required signatures
#[accounts(
data_account(writable, desc = "Data account"),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This inclusion of this account is just to demonstrate how parsing might work with some named accounts and a multiple account set, not to indicate anything about how multisig ought to work.

#[accounts(
nonce_account(signer, writable, desc = "Nonce account"),
recent_blockhashes_sysvar(signer, writable, desc = "RecentBlockhashes sysvar"),
nonce_authority(signer, optional, desc = "Nonce authority"),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just made this account optional for demonstration purposes.

@CriesofCarrots CriesofCarrots force-pushed the instruction-macro-design-proposal branch from 3c1d7bf to aeb8d06 Compare June 24, 2020 23:24
pub enum TestInstruction {
/// Transfer lamports
#[accounts(
from_account(signer, writable, desc = "Funding account"),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: (from_account, signer, ... instead of from_account(? Something to distinguish it from the function call syntax? Also, signer instead of SIGNER looks like an identifier, not a constant.

Copy link
Contributor Author

@CriesofCarrots CriesofCarrots Jun 25, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I used the from_account(.. syntax because it is a standard attribute syntax, and a bit easier to parse in the macro. But I'll give your suggestion a go in implementation.
signer/writer updated to SIGNER/WRITER

///
/// * `from_account` - `[writable, signer]` Funding account
/// * `to_account` - `[writable]` Recipient account
pub fn transfer(from_account: &Pubkey, to_account: &Pubkey, lamports: u64) -> Instruction {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think Pubkey will play out better than &Pubkey. It'll be a bunch of changes to the existing code, but I don't see a problem with that.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All right. I'd like to implement that change separately from the macro application. Does that work?

///
/// * Accounts expected by this instruction:
/// 0. `[writable]` Data account
/// * (Multiple) `[signer]` Signers
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does that * get rendered well by cargo doc?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It stacks an unordered list underneath the ordered list. I think it looks not-terrible:

image

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Better than a big header line in the middle of the description :-)

/// Provide M of N required signatures
///
/// * `data_account` - `[writable]` Data account
/// * `signers` - (Multiple) `[signer]` Signers
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't look very general. I think you could use this example as an opportunity to show how to opt out of code generation.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps we want a variant-level #[ignore] tag? I'll have to think about how that should operate

garious
garious previously approved these changes Jun 25, 2020
Copy link
Contributor

@garious garious left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just the nits. I think we could move back to implementation now and see how this first phase plays out before doing further design work.

@mergify mergify bot dismissed garious’s stale review June 25, 2020 15:29

Pull request has been modified.

@CriesofCarrots CriesofCarrots force-pushed the instruction-macro-design-proposal branch from 310ae7a to 046e8b9 Compare June 25, 2020 18:43
@CriesofCarrots CriesofCarrots merged commit 72b6349 into solana-labs:master Jun 25, 2020
@CriesofCarrots CriesofCarrots deleted the instruction-macro-design-proposal branch July 1, 2020 00:19
danpaul000 pushed a commit to danpaul000/solana that referenced this pull request Jul 8, 2020
…abs#10763)

* Add design proposal for ProgramInstruction procedural macro

* Update examples and some verbiage

* More constant-like

* Generated helpers expect Pubkey by value
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants