Skip to content

Instantly share code, notes, and snippets.

@pyrocto
Created November 14, 2019 22:43
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save pyrocto/62716d4bb36d5d83f138ea5c398a5172 to your computer and use it in GitHub Desktop.
Save pyrocto/62716d4bb36d5d83f138ea5c398a5172 to your computer and use it in GitHub Desktop.
Updated MultiSigRevVault design doc

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

// Returns (false, errorMsg) or (true, (multiSigVault, revAddress, revVault))
MultiSigRevVault!("create", pubKeysList, unsealersList, quorumSize, *ret)

The multiSigVault is the contract wrapping the underlying RevVault. revAddress is the RevAddress of the underlying RevVault; it can be given to people so they can pay into it. revVault is the contract of the underlying RevVault.

See below for description of unsealers.

2. Owners identification

PublicKeys for humans, Unsealers for contracts

All deployments are signed by the deployer; deployments containing confirmations make use of the signature via the injected deployerId.

The ocap pattern analogous to public keys is the sealer/unsealer pair:

contract MultiSigRevVault(@"makeSealerUnsealer", ret) = {
  new mapStore, sealer, unsealer in {
    mapStore!({}) |
    contract sealer(@value, retS) = {
      for (@map <- mapStore) {
        new box in {
          mapStore!(map.set(*box, value)) |
          retS!(*box)
        }
      }
    } |
    contract unsealer(@box, retU) = {
      for (@map <<- mapStore) {
        if (map.contains(box)) {
          retU!((true, map.get(box)))
        } else {
          retU!((false, "Invalid box"))
        }
      }
    } |
    ret!((*sealer, *unsealer))
  }
}

A client contract calls makeSealerUnsealer and receives a sealer (to be thought of as a signing key) and an unsealer (to be thought of as a verifying key). Invoking sealer on value returns a name box, to be thought of as the signature. Invoking unsealer on box will return either (true, value) or (false, errorMsg).

The unsealer itself conveys only enough authority to verify the signature, exactly as a public key does, but instead of relying on a cryptographic hardness assumption, it relies on unforgeable names.

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, but this is outside the scope of the design. Here are some possibilities:

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

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

D) Separate transfer(auth) and confirm(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
  • πŸ‘ matches Bitcoin and Ethereum usage

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:  recommend C) POBox for most use cases, but should not be part of implementation
-4. Initiating and Confirming a transfer: D) Separate `transfer(auth)` and `confirm(auth)` calls

API example: genesis pays 1000 into multisig owned by contracts Carol, Dave, Eve with quorum size 2. Carol creates transfer back to genesis vault of 500 Rev and Eve confirms it.

new genesisVaultCh,
    carolUnf, carolAuthCh,
    daveUnf, daveAuthCh,
    eveUnf, eveAuthCh,
    msVaultCh,
    ret1, ret2, ret3, now in {
  withVaultAndIdentityOf!(genesisPubKey, *genesisVaultCh) |
  @MultiSigRevVault!("makeSealerUnsealer", *carolAuthCh) |
  @MultiSigRevVault!("makeSealerUnsealer", *daveAuthCh) |
  @MultiSigRevVault!("makeSealerUnsealer", *eveAuthCh) |
  for(@(carolSealer, carolUnsealer) <- carolAuthCh;
      @(daveSealer, daveUnsealer) <- daveAuthCh;
      @(eveSealer, eveUnsealer) <- eveAuthCh) {
    @MultiSigRevVault!("create", [], [carolUnsealer, daveUnsealer, eveUnsealer], 2, *msVaultCh) |
    for (genesisVault, @genesisVaultKey <- genesisVaultCh; @maybeVault <- msVaultCh) {
      match maybeVault {
        (false, msg) => {
          rhoSpec!("assert", (false, "==", true), msg, *ackCh)
        }
        (true, (msVault, msRevAddr, msUnderlyingRevVault)) => {
          genesisVault!("transfer", msRevAddr, 1000, genesisVaultKey, *ret1) |
          for (@result <- ret1) {
            match result {
              (false, msg) => {
                rhoSpec!("assert", (false, "==", true), msg, *ackCh)
              }
              (true, Nil) => {
                new retCarolBox in {
                  @carolSealer!((msVault, genesisRevAddress, 500, *ret2), *retCarolBox) |
                  for (@carolBox <- retCarolBox) {
                    @msVault!("transfer", genesisRevAddress, 500, carolBox, *ret2) |
                    for (@result2 <- ret2) {
                      match result2 {
                        (false, msg) => {
                          rhoSpec!("assert", (false, "==", true), msg, *ackCh)
                        }
                        (true, result3) => {
                          match result3 {
                            (true, nonce) => {
                              new retEveBox in {
                                @eveSealer!((msVault, genesisRevAddress, 500, nonce, *ret3), *retEveBox) |
                                for (@eveBox <- retEveBox) {
                                  @msVault!("confirm", genesisRevAddress, 500, eveBox, nonce, *ret3) |
                                  rhoSpec!("assert", ((true, (false, "done")), "== <-", *ret3), "transfer successful", *now) |
                                }
                              }
                            }
                            (false, msg) => {
                              rhoSpec!("assert", (false, "==", true), msg, *ackCh)
                            }
                            x => {
                              rhoSpec!("assert", (false, "==", true), x, *ackCh)
                            }
                          }
                        }
                        _ => {
                          rhoSpec!("assert", (false, "==", true), "transfer returned something else", *ackCh)
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}

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)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment