- Multisig vault design
Table of contents generated with markdown-toc
We need a contract that allows for transferring REV only after n
of m
parties owning the contract confirm the transfer.
TODO describe how BitCoin does that TODO describe how Ethereum does that
- 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
orMultisig!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
Variants:
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.
// 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.
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.
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:
- π
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...
- registry has no
- π 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
- π 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
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.
- π 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
- π 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 toconfirm
- π allows the confirmers to know if/when the transfer was concluded
- π 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
- π 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
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)
}
}
}
}
}
}
}
}
}
}
}
}
}
As described in Drain Vault Attack, an on-chain deployer's identity token is required for auth APIs to be safe.
Just as is done in the Auth.rho
contract, the deployerAuth
should only ever be exchanged for authentication after wrapping in a bundle0
.
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.
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)
}