Signer type and move_to

Hello again!

I’ve noticed significant changes in standard library, specifically new move_to built-in and signer type.

  1. How does signer work? What profits this type carries and how does it help manage addresses and accounts? More practical one: how can I get myself as signer in script is there auto-substitution? Is signer always a reference?
  2. LibraAccount module contains a lot of ‘magic’, such as creating new signer. Again - how do these methods work (create_child_vasp_account as example) and does it mean that I can use any address in account creation? Will it actually create a new account? If so, can I move my resources to it?
  3. Are you going to deprecate move_to_sender or will it still be there along with new move_to? Do they conflict? What can move_to do that move_to_sender+few native functions cannot?

Thanks in advance.

2 Likes

Good questions! Here is a longer writeup on the rationale and plan for move_to. I think this answers most of your specific questions except for one, which I’ll answer here:

Again - how do these methods work ( create_child_vasp_account as example) and does it mean that I can use any address in account creation?

Yes. This was actually true before–you can use any address in account creation, but you’ll only be able to send transactions from the address if you uses a key to derive it.

Will it actually create a new account?

Yes. Move itself does not have any notion of “accounts”; the fact that every nonempty address has a LibraAccount resource is a Libra-specific convention. It would be possible to use a different approach.

If so, can I move my resources to it?

Yes, but only if you can send a transaction from the account.

The Move Signer Type

This is a more in-depth explanation of the new signer type originally proposed in https://github.com/libra/libra/issues/3679.

Overview

Old way

move_to_sender<R>(r: R) // publish R under the transaction sender's address
Transaction::sender(): address // return the transaction sender's address

New way

// signer is a native (like u64, bool, etc.) resource type with one field (addr)
move_to<R>(&signer, R) // publish R under signer.addr
Signer::address_of(&signer): address // return signer.addr

Where do you get a signer?

// If a transaction script declares a `&signer` argument, the Move VM creates a 
// signer resource whose addr is the sender address of the current transaction.
// It then passes a reference to that `signer` into the tx script.
// A script need not have a &signer argument, but if it does have a &signer, it must 
// be the first argument. A script can have at most one &signer (for now).
fun main(account: &signer, arg1: u64, arg2: address, ...) {

Migration

  • Short term (next week): all uses of move_to_sender and Transaction::sender eliminated from Move standard library
  • Medium term (next ~month): eliminate all uses in Move lang tests, work with Prover team to eliminate all uses in prover tests
  • Long term (but hopefully before V0 launch): eliminate Transaction::sender native function, remove move_to_sender source syntax and bytecode instruction

Why

Simplified Account Creation

  • Bootstrapping problem: need to add a LibraAccount::T resource (and other resources) to an address in order to create an account there.
    • But transaction to create account at address A must be sent from some existing account at A’ != A.
    • move_to_sender can only put resources under sender A’. And can’t send from A because it doesn’t exist yet. We’re stuck.
  • Old solution: native fun save_account(address, resource1, resource2, ...)
    • Where resource1 and resource2 are the resources that all account types require
    • Becomes very cumbersome with the variety of account role in whitepaper V2 Libra: ParentVASP, ChildVASP, Association, DesignatedDealer, Validator, and so on.
    • For all role-specific resources, use a two transaction publish-approve pattern: account publishes (e.g.) ChildVASP resource, relevant authority (e.g., ParentVASP) approves by flipping flag to true in separate tx.
    • Account setup is (at least!) three steps: create account, publish, approve
      • Big pain for wallets
  • New solution: signer + two private native functions
    • fun create_signer(address): signer
    • fun destroy_signer(signer)
    • This allows single-tx creation and initialization with relevant resources. There is a separate function like the example below for each account role
let s = create_signer(address_to_create)
move_to(&s, resource1) // if resource1 declared in LibraAccount
M1::publish(&s); // to publish resource declared in M1
s = M2::publish(s);
// ^ to publish resource declared in M2 that should only be created by LibraAccount
destroy_signer(s);

Explicit representation of sender authority

In the old Move, there is problem that many folks have pointed out/been troubled by (more details here): a function signature doesn’t give you any hint about whether it is going to read the sender address or use your sender authority. As an extreme example:

fun some_innocent_looking_thing() {
  // steals all your money and sends it to 0x101
  LibraAccount:pay_from_sender(0x101, LibraAccount::balance(Transaction::sender()));
}

This is a odd/unfortunate in a language whose purpose is to make everything explicit.

The introduction of signer does not completely solve this problem, but it helps. All code (except the special account creation code mentions) must be written in a functional style that threads the &signer from the transaction script through to any code that wants to look at/use it. That means the function above must now look like:

fun some_innocent_looking_thing(account: &signer) { // could
  // steals all your money and sends it to 0x101
  LibraAccount:pay_from(account, 0x101, LibraAccount::balance(account));
}

This helps because functions that don’t take a signer are guaranteed[1] not to change the sender’s account state. Functions that do require more scrutiny, and we may want to encourage design patterns to make things even more explicit (e.g., pay_from might require a WithdrawCapability instead of a &signer).

Multi-sender transactions scripts aka atomic scripts (future work)

There are many compelling applications of multi-sender transactions: atomic exchange of currencies, escrow without trusted third party or a complex contract, sending a transaction from one account with a different account paying for gas, cleaner implementation of the travel rule protocol, and so on. It is not clear how to implement this in the presence of move_to_sender or Transaction::sender; which sender the code is talking about will always be ambiguous. However, signer suggests a very simple solution to this problem:

// Extend the transaction format to accept multiple sender addresses +
// multiple signatures. The VM can check signatures and create a &signer for each
fun main(account1: &signer, account2: &signer, account3: &signer, ...)

There are some minor logistical issues to work out (deciding how/if to split gas fees, which sequence numbers get incremented, etc.), but overall the path forward seems clear.

[1] Unless they happen to hardcode the sender’s address

4 Likes

Wow! I didn’t expect so detailed response and it’s definitely worth bookmarking.

I see the point in using signer, it does provide more safety and I like the approach. Having a pair of move_to and move_from is also nice. But what about that good-old move_from? Will it be changed? I can’t imagine move_from being used with &signer argument - 0x0::Offer is a perfect example for it - sometimes you need to leave the other side ability to take resource from you. Does move_from fit into explicit representation of sender authority?

2 Likes

Good question–there are no plans to change move_from (i.e., its type will still be move_from<T>(address): T). As you point out, there are compelling use-cases for move_from on an address value.

2 Likes