Skip to content

Instantly share code, notes, and snippets.

@ArturGajowy
Last active May 27, 2019 08:19
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ArturGajowy/4aad90414d1e5ee0c4bc39ef28322a04 to your computer and use it in GitHub Desktop.
Save ArturGajowy/4aad90414d1e5ee0c4bc39ef28322a04 to your computer and use it in GitHub Desktop.
Multisig vault design

Multisig vault design

Table of contents generated with markdown-toc

Problem / use cases

We need a contract that allows for transferring REV only after n of m parties owning the contract confirm the transfer.

Prior work

TODO describe how BitCoin does that TODO describe how Ethereum does that

Requirements

  • there can be multiple Multisig instances for any given set of owners
  • the Multisig instance itself should be safe to share at least with the other owners, and - if we use registry or Multisig!find - with anyone
  • the Multisig instance must be capable of supporting multiple to-be-confirmed transfers at once, but doesn't need to provide means for explicitly differentiating between transfers with same (to, amount) tuple (like e.g. a 'transfer title/id').
    • ❗ This means there's no way of knowing that the transfer will never happen b/c of participants voting otherwise / doing a different transfer for the same purpose to a different party
  • multiple confirmations from same participant on the same (to, amount) pair must not allow for forcing the transfer singlehandedly, but rather should be treated as confirmations for two different transfers

API variants consideration

1. Creation

Variants:

A) (dismissed) Multisig is created, the participants are enlisted to it

Dismissed, as this increases complexity and raises lots of questions:

  • how to ensure only the intended parties are there
  • this makes the multisig mutable, or spawns new multisigs based on previous instances. How to ensure only the inteded parties are enlisted?
  • how do the current parties agree on adding a new participant?
  • is removing participants possible?

At the same time, given answers to above, one should be able to facade the Mutlisig with needed logic and use variant B) underneath.

B) Multisig is created based on given participants list and threshold

Multisig!("create", 2, Set("%A_REV_ADDR", "%B_REV_ADDR" , "%C_REV_ADDR"), *ret)

2. Owners identification

A) RevAddress

  • more safety due to hashing and a checksum being included
  • shorter on-chain strings, at least when using hex encoding

B) PublicKey

C) AuthKey-like contract instance

From the variants above, A) and B) both seem feasible and exchangeable.

C) means more contracts written, more load on the tuplespace and the chain in general, and more cumbersome API. C) also means exchange of OCAPs before Multisig creation, which in general is an unsolved problem, and cumbersome even in the light of POBox (see 3. Accessing the created instance)

A) and B) though pose a risk of creating an unusable Multisig instance, b/c of passing an invalid public key, or non-accessible RevAdress (a unforgable-name-based one, or based on random PublicKey). This could be avoided in C), but at a cost of exchanging OCAPs.

Types of PubKey-s and RevAddress-es

In both A) and B) we should consider there are going to be 2 types of PublicKey-s, and the RevAddress types corresponding to them need a discriminator in them. We should also consider prefixing public keys with their type in our APIs and CLIs, e.g.

ed25519:%PUB_KEY_BYTES_IN_HEX
secp256k1:%PUB_KEY_BYTES_IN_HEX

and ideally, even their encoding:

ed25519:hex:%PUB_KEY_BYTES_IN_HEX
secp256k1:base64:%PUB_KEY_BYTES_IN_BASE64

3. Accessing the created instance

The creator of the vault has it on the ret channel. The other participants and the creator in their next deploy need a way to grab it though.

A) Use registry

  • πŸ‘Ž isnertSigned is hard, insertArbitrary (and signed too, to some extent) requires communicating the resulting unpredictable uri
  • πŸ‘Ž bloats the registry, makes it a contention point
  • πŸ‘Ž a Multisig can never be garbage-collected
    • registry has no delete operation
    • who's to say delete is OK to do? would need coordination from all owners...

B) Multisig.find(multiSigRevAddr)

  • πŸ‘Ž requires communicating the multiSigRevAddr, the revAddr is unpredictable
  • πŸ‘Ž makes the underlying map MVar (multisigRegistry) a contention point for all users, causing block conflicts
  • πŸ‘Ž a Multisig can never be garbage-collected (who's to say delete is OK to do? would need coordination from all owners...)
  • πŸ‘ easy to use

C) Use POBox

  • πŸ‘Ž requires communicating the messageId to listen for
  • πŸ‘ the messageId can be agreed on upfront
  • πŸ‘Ž requires designing a POBox api
  • πŸ‘ reduces contention and block conflicts by making them local to the users and the peers they exchange messages with
  • πŸ‘ allows for Multisig instance garbage collection once all users decide to delete their copy of the message
  • πŸ‘ solves the Safe OCAP storage and distribution problem cleanly, and for all future contracts
//Multisig creator
Multisig!("create", 2, Set("%A_REV_ADDR", "%B_REV_ADDR" , "%C_REV_ADDR"), *ret) |
for (@multisig <- ret) {
  POBox!("send", participants, "%MESSAGE_ID", multisig, deployerAuth, *done)
}

//Multisig receiver
POBox!("receive", senderRevAddr, participants, "%MESSAGE_ID", deployerAuth, *ret) |
for (@multisig <- ret) {
  //use multisig
}
Idea: key the OCAPs in the POBox using the code creating them

Because rholang terms are comparable values, one can imagine a POBox API where the code used for creating the multisig is used as an ID for the exchanged message. This way the receivers can ensure that the contents of the message are as expected:

//Multisig creator
POBox!("send", participants,
  new Multisig(`rho:sys:Multisig`), ret(`rho:POBox:ret`) in {
    Multisig!("create", 2, Set("%A_REV_ADDR", "%B_REV_ADDR" , "%C_REV_ADDR"), *ret)
  },
  deployerAuth,
  *done
)

//Multisig receivers
POBox!("receive", participants,
  new Multisig(`rho:sys:Multisig`), ret(`rho:POBox:ret`) in {
    Multisig!("create", 2, Set("%A_REV_ADDR", "%B_REV_ADDR" , "%C_REV_ADDR"), *ret)
  },
  deployerAuth,
  *receivedMultisig
)

The ret(`rho:POBox:ret`) would need to be somewhat special though, and lambdas would probably be needed to implement that idea cleanly.

4. Initiating and Confirming a transfer

While off-chain signing is conceivable, there's no need for it from security standpoint - and it's easier for the user to confirm the transaction on-chain. This leverages the off-chain signature that is being done for the deploy anyway.

Only one transfer with same details at a time is allowed / designed-for, as a facade-contract taking care of such scenario is conceivable. For long-lived Multisig instances (e.g. coop vault) this should not be a problem, as their operation will be directly supervised. For merchandise purposes (buy a cofree, and then another one) each transaction would use a different Multisig instance.

A) Pass signature OCAPs, call transfer(to, amount, authA, authB, authC) once

  • πŸ‘Ž requires arranging a POBox messageId / other means for exchanging the OCAP (custom contract?)
  • (:+1:) requires no 'transfer map' MVar-s in Multisig instances (negligible due to small size and contention)
  • πŸ‘Ž prevents the confirmers from knowing on-chain if/when the transfer was concluded, unless a confirmation channel is passed

B) Pass a Transfer OCAP to the confirmers, call transfer!confirm(someAuth)

  • πŸ‘Ž requires arranging a POBox messageId / other means for exchanging the OCAP (custom contract? "MultisigTransfer.find"?)
  • πŸ‘Ž requires a Transfer contract instance that will linger until we have garbage collection
  • ❗ the confirmers need to either ask the Transfer for its details, or pass the expected details (to, amount) in call to confirm
  • πŸ‘ allows the confirmers to know if/when the transfer was concluded

C) Make Multisig instance count transfer(auth) calls

  • πŸ‘ reduces OCAP exchange to just the Multisig instance
  • (:-1:) requires a 'transfer map' MVar-s in Multisig instances (negligible due to small size and contention)
  • πŸ‘ allows the confirmers to know if/when the transfer was concluded

API recommendation

Given the above consideration, we recommend an API consisting of variants:

-1. Creation: B) Multisig is created based on given participants list and threshold
-2. Owners identification: A) RevAddress
-3. Accessing the created instance:  C) Use POBox or B) `Multisig.find(multiSigRevAddr)` and later transition to C) 
-4. Initiating and Confirming a transfer: C) Make Multisig instance count  `transfer(auth)`  calls

API usage, in a "A pays C with conflicts resolved by B" scenario would then be:

//excuse my psuedo-rho...
//one would use a `match` to bind participants to a variable in real-rho
val participants = Set("%A_REV_ADDR", "%B_REV_ADDR" , "%C_REV_ADDR")

//Multisig creation
Multisig!("create", 2, participants, *ret) |
for (multisig <- ret) {

  //top-up the multisig to support the transfer (optional in general)
  multisig!("revAddress", *multisigRevAddr) | 
  for (@to <- multisigRevAddr) {

    aRevVault!("transfer", 45, multisigRevAddr, aRevAuthKey, *done) |
	for ((@true, _) <- done) {

	  POBox!("send", participants, "%MESSAGE_ID", @multisig, deployerAuth, *donedone) |
	}
  }
}

//Issuing / confirming a transfer:
//Notice it's the same for the creator - they receive the POBox message too!
POBox!("receive", senderRevAddr, participants, "%MESSAGE_ID", deployerAuth, *ret) |
Multisig!("deployerAuthKey", deployerAuth, *multisigAuthCh) |
for (multisig <- ret; @multisigAuth <- multisigAuthCh) {
  multisig!("transfer", "%C_REV_ADDR", 42, multisigAuth, *status)
}

Implementation guidance

Lean on deployerAuth(`rho:deployer:auth`)

As described in Drain Vault Attack, an on-chain deployer's identity token is required for auth APIs to be safe.

Never present the deployerAuth to contracts directly

Just as is done in the Auth.rho contract, the deployerAuth should only ever be exchanged for authentication after wrapping in a bundle0.

Make the auth OCAPs usable-once, and leaving no garbage

Currently, each AuthKey contract instance (used as auth OCAPs for RevVault) has a persistent receive (contract), resulting in tuplespace pollution till we have GC. Given recent blowup issues, until we have GC, we should strive for designing short-lived instances without the use of persistent receives (or sends). So, instead of:

contract AuthKey(@"make", @shape, ret) = {  
  new authKey in {  
	contract authKey(@"challenge", retCh) = {  
	  retCh!(bundle0{ (*_authKey, shape)})  
	} |  
    ret ! (bundle+{*authKey})  
  }
}

we should rather do

contract Signature(@"make", @shape, ret) = {  
  new signature in {  
	for (@"challenge", @retCh <- signature) {  
	  retCh!(bundle0{ (*_authKey, shape)})  
	} |  
    ret ! (bundle+{*authKey})  
  }
}

This way the challenge method is usable-once, and leaves no garbage after use.

We can further make interception of the challenge's response impossible by introducing a nonce, as described in AuthKey.rho' s sources.

Signature / AuthKey used in POBox / Multisig must not contain deployerAuth in shape

Just like the current implementation of RevVault!deployerAuthKey (and the future one, after switching to deployerAuth usage) uses revAddress as part of the shape, so should POBox and Multisig when creating their AuthKey variants for the deployer.

We could think of a util in AuthKey that, given a deployerAuth, would obtain the corresponding RevAddress and mix it with the provided shape. This in turn, would allow for third-party contracts to expose safe auth mechanisms:

CustomContract!("authSeed", *seed) | //returns `bundle0{ privateUnforgableName }`
AuthKey!("forDeployer", *seed, *deployerAuth, *custromContractDeployersAuthKey) |
for (key <- custromContractDeployersAuthKey) {
	CustomContract!("use", key, 42, *ret)
}
@tymm
Copy link

tymm commented May 3, 2019

Small typo in the example in the API recommedation section:
Should be aRevVault!("transfer", 45, to, aRevAuthKey, *done) instead of aRevVault!("transfer", 45, multisigRevAddr, aRevAuthKey, *done).

@ArturGajowy
Copy link
Author

Nope, it is correct :) We're topping up the newly-created multisig vault, the to transfer is done below, using C_REV_ADDR as the to

@odeac
Copy link

odeac commented May 24, 2019

I would decouple the concept of Multisig from the concept of Vault. A properly designed multisig can be a generic tool with other applications too.

@ArturGajowy
Copy link
Author

Not without lambdas. I'd need to get acquainted with the idiom Greg has shown us and make sure it works in our rholang impl to tell wether a generic (lambda-based) design is possible.

@odeac
Copy link

odeac commented May 25, 2019

We can certainly design it without lambdas. Just that lambdas will make the code easier to write

@ArturGajowy
Copy link
Author

I'm all ears. I'm proceeding with this until I hear a concrete proposal.

@odeac
Copy link

odeac commented May 27, 2019

I added here a page with a quick sketch for my idea of multi-sig: https://rchain.atlassian.net/wiki/spaces/CORE/pages/727121921/Multisig+Vault+proposal

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment