- 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.
Multisig!("create", 2, Set("%A_REV_ADDR", "%B_REV_ADDR" , "%C_REV_ADDR"), *ret)
- more safety due to hashing and a checksum being included
- shorter on-chain strings, at least when using hex encoding
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.
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
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.
- π
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
//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
}
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.
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
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)
}
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)
}
Small typo in the example in the API recommedation section:
Should be
aRevVault!("transfer", 45, to, aRevAuthKey, *done)
instead ofaRevVault!("transfer", 45, multisigRevAddr, aRevAuthKey, *done)
.