Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

KomenciKit #5436

Merged
merged 44 commits into from
Oct 30, 2020
Merged

KomenciKit #5436

merged 44 commits into from
Oct 30, 2020

Conversation

bowd
Copy link
Collaborator

@bowd bowd commented Oct 19, 2020

Description

KomenciKit is a wrapper for the Komenci service.
It will be consumed by Valora and used during fee-less onboarding.

Other changes

I've extended the BaseWrapper with some convenience attributed:

eventTypes

This will essentially act as an enum of events defined in the contract.
This is useful because we're using the getPastEvents which wasn't strictly typed, it used a string as argument.
And we were using it with strings all over the place which is error prone.
The extension allows the compiler to verify that the event passed in exists on the contract.

methodIds

This is a mapping between method name and method ID. It's used in Komenci to have a more ergonomic way to define meta transaction filters.


## User Guide

To start working with Komencikit you need a to pass in a ContractKit instance, the external account
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: incomplete sentence

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I drifted off to sleep 😅

return
}

const events = await attestations.getPastEvents('AttestationsRequested', {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we make these a constant enum defined somewhere?
AttestationsRequested AttestationIssuerSelected WalletDeployed

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah it's one thing I wanted to look into. Theoretically they should be autogenerated somewhere. I was expecting to see them on the contract wrapper but either that's not expected or there's something weird going on in the solidity->generated types->contract->wrapper. But I'll look into this some more.


export const startSession = action<ActionTypes.StartSession, StartSessionPayload, StartSessionResp>(
ActionTypes.StartSession,
'POST',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we define this as an enum type if it doesn't exist already?

while (receipt == null && waited < this.options.txRetryTimeoutMs) {
receipt = await this.contractKit.web3.eth.getTransactionReceipt(txHash)
if (receipt == null) {
await sleep(100)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can also make this a property under KomenciOptions: txRetryPollingIntervalMs

})

const deployWalletLog = events.find(
(event) => normalizeAddressWith0x(event.returnValues.owner) === this.externalAccount
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should also verify that the implementationAddress in the event matches the one sent to the API. This prevents a compromised Komenci node from deploying a malicious MTWProxy instance and Valora using it as its own.

const attestations = await this.contractKit.contracts.getAttestations()

const approveTx = await attestations.approveAttestationFee(attestationsRequested)
const approveTxSig = await wallet.signMetaTransaction(approveTx.txo, nonce)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: it might be better to increment nonce nonce++ here in case we add more txs in the future since we won't need to worry about ordering.

console.debug(`${TAG}/requestAttestations attempt#${attempt} error: `, error)
},
})
public async requestAttestations(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can add the DEK registration to this API as well.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 to DEK registration but think that's now accounted for in the setAccount wrapper. I like that approach because it will allow Valora control over when to submit the transaction

Copy link
Contributor

@tarikbellamine tarikbellamine left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work! Left a couple of comments and questions.

One thing that may make sense to think a little about now is analytics. We've got the Valora verification flow pretty well measured at this point and Komenci will be a relative black box. I'm not sure how much tracking makes sense for the first iteration but just wanted to put it on your radar as a means to collect data for debugging and performance measurement.

import { Address } from '@celo/contractkit'
import { EIP712TypedData } from '@celo/utils/lib/sign-typed-data-utils'

export const buildLoginTypedData = (
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gnardini is looking into incorporating DeviceCheck and SafetyNet. At least SafetyNet outputs a verifiable token but not sure about DeviceCheck yet. Do we want to accept these as inputs if they are available for the first iteration of Komenci?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah we can add them, not necessarily to the login struct because I think having the captcha token is enough for replay protection. But currently I stripped these from the client under the impression that we'll stick only with captcha. If we feel confident in adding those I can do that but I'd merge this as is in the mean time to have a starting point for the mobile work.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good. Feel free to merge as is and we can always add those in once we have more clarity.

): Promise<Result<true, WalletValidationError>> => {
const code = await contractKit.web3.eth.getCode(walletAddress)
// XXX: I'm unsure whether this is safe or if we should store the
// bytecode as a constant in `mobile` and pass it into KomenciKit
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Based on my convos with @asaj, I think we landed on needing a constant bytecode to be available for comparison to ensure the deployed wallet meets expectations. I can't really speak to what other guardrails might be in place though.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I guess that was more about how to track the implementation's bytecode itself. We were between using bytecode digests or the actual address of the contract. I was inclining towards bytecode digest because I was under the wrong impression that it would be constant across networks and be easier to manage on Valora - it wouldn't need a different mapping per network. But realistically because we're using library linking it's gonna have different bytecodes therefore we're ok with Valora referring to a MTW implementation by the address, and Komenci verifying server side that address maps to a known implementation.

Here I'm actually verifying the Proxy contract bytecode to ensure that the Proxy itself is what we expect. I suspect we won't need to change the Proxy anytime soon so I think it's safe to extract that from the compilation output. But I'll ping the CAP team to figure out if there are holes in this approach.

*/
getDistributedBlindedPepper = async (
e164Number: string,
clientVersion: string
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's clientVersion and how will Valora know it?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@codyborn, keep me honest but AFAIK client version is the actual version of Valora, and it's something that Odis requires for tracking I guess. It's currently passed in as DeviceInfo.getVersion() in identity/privateHashing.ts.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's right. It's purely for monitoring and lets us know on the ODIS side how many users are still using a given version. This can be helpful when deprecating old code paths.

* pointing it to the implementation passed as an argument
* The function takes care of waiting for retrying, waiting for receipt and log parsing
*
* @param implementationAddress the implementation address Valora requires
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: I think the description would be more clear if you referenced the fact that this will be Valora's EOA address

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But it's not - but that's also more to your point 😅
This is the address of the MetaTransactionWallet implementation that Valora will have saved as a constant.
Komenci (api) will verify that the implementation is a known implementation that we deployed, and will then deploy a Proxy and set the implementation of that proxy to this address.
Valora then needs to verify:

  • That it's MTW is actually a Proxy (by verifying the bytecode)
  • That it's pointing to the chosen implementation
  • That Valora's EOA is the signer
  • That the MTW is its own Owner

(these are all handled in KomenciKit)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah gotcha that makes sense

console.debug(`${TAG}/requestAttestations attempt#${attempt} error: `, error)
},
})
public async requestAttestations(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 to DEK registration but think that's now accounted for in the setAccount wrapper. I like that approach because it will allow Valora control over when to submit the transaction

identifier: string,
walletAddress: string,
attestationsRequested: number
): Promise<Result<TransactionReceipt, FetchError | TxError>> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It may be worth adding a separate error type to symbolize the "attestation request limit reached" error, which I believe we are initially setting to 10. Perhaps you can add as an error type within KomenciErrorTypes?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I didn't add that in because I wanted to do it in a separate PR when I implement it on the server side as well.

*/
_wallet?: MetaTransactionWalletWrapper
private async getWallet(address: string): Promise<MetaTransactionWalletWrapper> {
if (this._wallet === undefined || this._wallet.address !== address) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Consider using optional chaining

const wallet = await this.getWallet(walletAddress)
let nonce = await wallet.nonce()

const approveTx = await attestations.approveAttestationFee(attestationsRequested)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like there's a check you can do beforehand in the event that attestations have been requested but not revealed and save yourself from submitting these transactions. Example of how we are using it in the Valora flow: https://github.com/celo-org/celo-monorepo/blob/master/packages/mobile/src/identity/verification.ts#L481

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this depends on how we think about KomenciKit. I would leave this logic in Valora and only think of the requestAttestations function here as a drop-in replacement for the transaction that Valora was sending before. When and if to request additional attestations should be up to Valora.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense, I agree

const accounts = await this.contractKit.contracts.getAccounts()
return this.submitMetaTransaction(
walletAddress,
accounts.setAccount(name, dataEncryptionKey, walletAddress, proofOfPossession).txo
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't you need to await this method call like you're doing elsewhere?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It depends on the implementation in the wrapper - these calls result in a CeloTransactionObject but some are async, some are not.

The KomenciKit library is a wrapper for the Komenci Service API, which is used for fee-less onboarding by allowing
the consumer to execute fee-less attestations with the help of MetaTransactions.

The main actions that KomenciKit exposes are:
Copy link
Contributor

@tarikbellamine tarikbellamine Oct 28, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need a wrapper for escrow.withdraw or are you thinking to just submit through the generic function? This that's the only onboarding on-chain transaction not represented in this list.

And in general, must a transaction have a wrapper in KomenciKit in order to be subsidized?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A transaction doesn't need to have a wrapper. I was actually inclined to remove some of the wrappers and rely on the submitMetaTransaction action that takes in an arbitrary MetaTransaction and pushes it.
That will only work, however with transaction that we allow on the API side by filtering on contract address and methodID.

The only transaction that we actually must wrap is the Attestations.request logic because there we're doing the actual subsidy batch and not just executing a simple meta transaction.

Copy link
Contributor

@codyborn codyborn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Copy link
Contributor

@tarikbellamine tarikbellamine left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small request regarding terminology and a needed change to the setAccount method.

const accounts = await this.contractKit.contracts.getAccounts()
return this.submitMetaTransaction(
walletAddress,
accounts.setAccount(name, dataEncryptionKey, walletAddress, proofOfPossession).txo
Copy link
Contributor

@tarikbellamine tarikbellamine Oct 28, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The walletAddress expected by the accounts.setAccount method is actually the EOA not the MTW.

In general, the use of walletAddress in KomenciKit to refer to the MTW is confusing because in a broader context we are trying to move towards a world where accountAddress === MTW proxy address and walletAddress === EOA address. I understand if you want to keep the terminology as is for the rest of Komenci, but I'd suggest changing it in KomenciKit as it will be confusing for future Valora devs

Copy link
Contributor

@tarikbellamine tarikbellamine left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes look good. Thanks!

Left one small optional request.

@@ -193,11 +210,11 @@ export class KomenciKit {
})
public async requestAttestations(
identifier: string,
walletAddress: string,
metaTxWalletAddress: string,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: this is the only transaction wrapper that has identifier as the first argument and the address as the second. Can we swap these to avoid mistakes when calling?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I actually thought of this as well I'll make all the function take the metaTxWalletAddress as the first argument.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants