diff --git a/.circleci/config.yml b/.circleci/config.yml index 97a9a7acd55..2d9e0987e1d 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -12,8 +12,8 @@ reference: shell: /bin/bash --login -eo pipefail environment: TERM: dumb - _JAVA_OPTIONS: "-Xmx7g -XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap" - GRADLE_OPTS: '-Dorg.gradle.daemon=false -Dorg.gradle.configureondemand=true -Dorg.gradle.jvmargs="-Xmx6g -XX:MaxPermSize=3g -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8"' + _JAVA_OPTIONS: "-XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap" + GRADLE_OPTS: "-Dorg.gradle.daemon=false -Dorg.gradle.configureondemand=true" defaults: &defaults working_directory: ~/app @@ -28,8 +28,8 @@ android-defaults: &android-defaults docker: - image: circleci/android:api-28-node environment: - _JAVA_OPTIONS: "-Xmx7g -XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap" - GRADLE_OPTS: '-Dorg.gradle.daemon=false -Dorg.gradle.configureondemand=true -Dorg.gradle.jvmargs="-Xmx6g -XX:MaxPermSize=3g -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8"' + _JAVA_OPTIONS: "-XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap" + GRADLE_OPTS: "-Dorg.gradle.daemon=false -Dorg.gradle.configureondemand=true" e2e-defaults: &e2e-defaults <<: *defaults @@ -240,7 +240,7 @@ jobs: - run: name: Start pidcat logging command: pidcat -t "GoLog" -t "Go" # React logs are on metro step since RN 61 - background: true + background: true - run: name: Run yarn dev command: cd ~/src/packages/mobile && ENVFILE=".env.test" yarn dev @@ -249,7 +249,7 @@ jobs: command: adb kill-server && adb start-server - run: name: Run test itself - + command: | cd ~/src/packages/mobile # detox sometimes without releasing the terminal and thus making the CI timout @@ -450,7 +450,7 @@ jobs: - run: name: Generate DevChain command: | - (cd packages/cli && yarn test:reset) + (cd packages/cli && yarn test:reset) - run: name: Run Tests command: yarn --cwd=packages/cli test @@ -468,15 +468,16 @@ jobs: command: | yarn --cwd=packages/cli run celocli account:new - - run: - name: Install and test the npm package - command: | - set -euo pipefail - cd packages/cli - yarn pack - cd /tmp - npm install ~/app/packages/cli/celo-celocli-*.tgz - ./node_modules/.bin/celocli account:new # Small test + # Won't work when cli uses git dependencies! + # - run: + # name: Install and test the npm package + # command: | + # set -euo pipefail + # cd packages/cli + # yarn pack + # cd /tmp + # npm install ~/app/packages/cli/celo-celocli-*.tgz + # ./node_modules/.bin/celocli account:new # Small test typescript-test: <<: *defaults @@ -583,41 +584,6 @@ jobs: cd packages/celotool ./ci_test_sync.sh checkout master - end-to-end-geth-integration-sync-test: - <<: *e2e-defaults - steps: - - attach_workspace: - at: ~/app - - run: - name: Check if the test should run - command: | - FILES_TO_CHECK="${PWD}/packages/celotool,${PWD}/packages/protocol,${PWD}/.circleci/config.yml" - ./scripts/ci_check_if_test_should_run_v2.sh ${FILES_TO_CHECK} - - run: - name: Run test - command: | - set -e - cd packages/celotool - ./ci_test_sync_with_network.sh checkout master - - end-to-end-geth-attestations-test: - <<: *e2e-defaults - resource_class: medium+ - steps: - - attach_workspace: - at: ~/app - - run: - name: Check if the test should run - command: | - FILES_TO_CHECK="${PWD}/packages/celotool,${PWD}/packages/protocol,${PWD}/.circleci/config.yml" - ./scripts/ci_check_if_test_should_run_v2.sh ${FILES_TO_CHECK} - - run: - name: Run test - command: | - set -e - cd packages/celotool - ./ci_test_attestations.sh checkout master - end-to-end-geth-validator-order-test: <<: *e2e-defaults resource_class: large @@ -760,14 +726,6 @@ workflows: requires: - lint-checks - contractkit-test - - end-to-end-geth-integration-sync-test: - requires: - - lint-checks - - contractkit-test - - end-to-end-geth-attestations-test: - requires: - - lint-checks - - contractkit-test - end-to-end-geth-validator-order-test: requires: - lint-checks diff --git a/SETUP.md b/SETUP.md index bf65ec99d44..13dbf50865d 100644 --- a/SETUP.md +++ b/SETUP.md @@ -14,6 +14,7 @@ - [Installing OpenJDK 8](#installing-openjdk-8) - [Install Android Dev Tools](#install-android-dev-tools-1) - [Some common stuff](#some-common-stuff) + - [Install Go](#install-go) - [Optional: Install Rust](#optional-install-rust) - [Optional: Install an Android Emulator](#optional-install-an-android-emulator) - [Optional: Genymotion](#optional-genymotion) @@ -176,6 +177,23 @@ You can find the complete instructions about how to install the tools in Linux e ### Some common stuff +#### Install Go + +We need Go for [celo-blockchain](https://github.com/celo-org/celo-blockchain), the Go Celo implementation, and `gobind` to build Java language bindings to Go code for the Android Geth client). + +Note: We currently use Go 1.11. Brew installs Go 1.12 by default, which is not entirely compatible with our repositories. [Install Go 1.11 manually](https://golang.org/dl/), then run + +``` +go get golang.org/x/mobile/cmd/gobind +``` + +Execute the following (and make sure the lines are in your `~/.bash_profile`): + +``` +export GOPATH=$HOME/go +export PATH=$PATH:$GOPATH/bin +``` + #### Optional: Install Rust We use Rust to build the [bls-zexe](https://github.com/celo-org/bls-zexe) repo, which Geth depends on. If you only use the monorepo, you probably don't need this. diff --git a/package.json b/package.json index c8adb6b06f1..c0dea5702f4 100644 --- a/package.json +++ b/package.json @@ -74,6 +74,7 @@ "**/codecov/**/js-yaml": "^3.13.1", "**/deep-extend": "^0.5.1", "**/extend": "^3.0.2", + "**/cross-fetch": "^3.0.2", "sha3": "1.2.3" } } diff --git a/packages/attestation-service/README.md b/packages/attestation-service/README.md new file mode 100644 index 00000000000..84893323b1a --- /dev/null +++ b/packages/attestation-service/README.md @@ -0,0 +1,33 @@ +# Attestation Service + +A service run by validators on the Celo network to send SMS messages, enabling attestations of user phone numbers and their accounts on the Celo network. + +### Configuration + +You can use the following environment variables to configure the attestation service: + +- `DATABASE_URL` - The URL under which your database is accessible, currently supported are `postgres://`, `mysql://` and `sqlite://` +- `CELO_PROVIDER` - The URL under which a celo blockchain node is reachable, i.e. something like `https://integration-forno.celo-testnet.org` +- `ACCOUNT_ADDRESS` - The address of the account on the `Accounts` smart contract +- `ATTESTATION_KEY` - The private key with which attestations should be signed. You could use your account key for attestations, but really you should authorize a dedicated attestation key +- `APP_SIGNATURE` - The hash with which clients can auto-read SMS messages on android +- `SMS_PROVIDERS` - A comma-separated list of providers you want to configure, we currently support: + +`nexmo` + +- `NEXMO_KEY` - The API key to the Nexmo API +- `NEXMO_SECRET` - The API secret to the Nexmo API +- `NEXMO_BLACKLIST` - A comma-sperated list of country codes you do not want to serve + +### Running locally + +After checking out the source, you should create a local sqlite database by running: + +```sh +yarn run db:create:dev +yarn run db:migrate:dev +``` + +You will also have to set the environment variables in `.env.development` + +Then start the service with `yarn run dev` (you'll have to add the appropriate credentials for the text providers) diff --git a/packages/attestation-service/config/.env.development b/packages/attestation-service/config/.env.development index a8b1540c59f..e5d83040ec2 100644 --- a/packages/attestation-service/config/.env.development +++ b/packages/attestation-service/config/.env.development @@ -3,5 +3,7 @@ CELO_PROVIDER=https://integration-forno.celo-testnet.org ACCOUNT_ADDRESS=0xE6e53b5fc2e18F51781f14a3ce5E7FD468247a15 ATTESTATION_KEY=x APP_SIGNATURE=x +SMS_PROVIDERS=nexmo NEXMO_KEY=x -NEXMO_SECRET=x \ No newline at end of file +NEXMO_SECRET=x +NEXMO_BLACKLIST= diff --git a/packages/attestation-service/migrations/20191015211858-create-attestation.js b/packages/attestation-service/migrations/20191015211858-create-attestation.js index aca5106d69c..e57863ec515 100644 --- a/packages/attestation-service/migrations/20191015211858-create-attestation.js +++ b/packages/attestation-service/migrations/20191015211858-create-attestation.js @@ -23,6 +23,14 @@ module.exports = { allowNull: false, type: Sequelize.STRING, }, + status: { + allowNull: false, + type: Sequelize.STRING, + }, + smsProvider: { + allowNull: false, + type: Sequelize.STRING, + }, createdAt: { allowNull: false, type: Sequelize.DATE, diff --git a/packages/attestation-service/nodemon.json b/packages/attestation-service/nodemon.json index fa8dc8662d2..6e1eeec2d3e 100644 --- a/packages/attestation-service/nodemon.json +++ b/packages/attestation-service/nodemon.json @@ -1,6 +1,6 @@ { "ignore": ["**/*.test.ts", "**/*.spec.ts", ".git", "node_modules"], "watch": ["src"], - "exec": "yarn start", + "exec": "yarn start-ts", "ext": "ts" } diff --git a/packages/attestation-service/package.json b/packages/attestation-service/package.json index 99c3564d902..d5bf54f11cd 100644 --- a/packages/attestation-service/package.json +++ b/packages/attestation-service/package.json @@ -27,7 +27,7 @@ "lint": "tslint -c tslint.json --project ." }, "dependencies": { - "@celo/contractkit": "0.1.6", + "@celo/contractkit": "0.2.1-dev", "@celo/utils": "^0.1.0", "bignumber.js": "^7.2.0", "body-parser": "1.19.0", diff --git a/packages/attestation-service/src/attestation.ts b/packages/attestation-service/src/attestation.ts index 37411ea33f7..42a2b651e77 100644 --- a/packages/attestation-service/src/attestation.ts +++ b/packages/attestation-service/src/attestation.ts @@ -1,12 +1,27 @@ import { AttestationState } from '@celo/contractkit/lib/wrappers/Attestations' import { attestToIdentifier, SignatureUtils } from '@celo/utils' -import { privateKeyToAddress } from '@celo/utils/lib/address' -import { retryAsyncWithBackOff } from '@celo/utils/lib/async' +import { + Address, + isValidPrivateKey, + privateKeyToAddress, + toChecksumAddress, +} from '@celo/utils/lib/address' +import { AddressType, E164Number, E164PhoneNumberType } from '@celo/utils/lib/io' +import { isValidAddress } from 'ethereumjs-util' import express from 'express' import * as t from 'io-ts' -import { existingAttestationRequest, kit, persistAttestationRequest } from './db' -import { Address, AddressType, E164Number, E164PhoneNumberType } from './request' -import { sendSms } from './sms' +import { Transaction } from 'sequelize' +import { existingAttestationRequestRecord, getAttestationTable, kit, sequelize } from './db' +import { AttestationModel, AttestationStatus } from './models/attestation' +import { respondWithError } from './request' +import { smsProviderFor } from './sms' +import { SmsProviderType } from './sms/base' + +const SMS_SENDING_ERROR = 'Something went wrong while attempting to send SMS, try again later' +const ATTESTATION_ERROR = 'Valid attestation could not be provided' +const NO_INCOMPLETE_ATTESTATION_FOUND_ERROR = 'No incomplete attestation found' +const ATTESTATION_ALREADY_SENT_ERROR = 'Attestation already sent' +const COUNTRY_CODE_NOT_SERVED_ERROR = 'Your country code is not being served by this service' export const AttestationRequestType = t.type({ phoneNumber: E164PhoneNumberType, @@ -16,25 +31,39 @@ export const AttestationRequestType = t.type({ export type AttestationRequest = t.TypeOf -function getAttestationKey() { - if (process.env.ATTESTATION_KEY === undefined) { - console.error('Did not specify ATTESTATION_KEY') - throw new Error('Did not specify ATTESTATION_KEY') +export function getAttestationKey() { + if ( + process.env.ATTESTATION_KEY === undefined || + !isValidPrivateKey(process.env.ATTESTATION_KEY) + ) { + console.error('Did not specify valid ATTESTATION_KEY') + throw new Error('Did not specify valid ATTESTATION_KEY') } return process.env.ATTESTATION_KEY } +export function getAccountAddress() { + if (process.env.ACCOUNT_ADDRESS === undefined || !isValidAddress(process.env.ACCOUNT_ADDRESS)) { + console.error('Did not specify valid ACCOUNT_ADDRESS') + throw new Error('Did not specify valid ACCOUNT_ADDRESS') + } + + return toChecksumAddress(process.env.ACCOUNT_ADDRESS) +} + async function validateAttestationRequest(request: AttestationRequest) { + const attestationRecord = await existingAttestationRequestRecord( + request.phoneNumber, + request.account, + request.issuer + ) // check if it exists in the database - if ( - (await existingAttestationRequest(request.phoneNumber, request.account, request.issuer)) !== - null - ) { - throw new Error('Attestation already sent') + if (attestationRecord && !attestationRecord.canSendSms()) { + console.log(attestationRecord.canSendSms()) + throw new Error(ATTESTATION_ALREADY_SENT_ERROR) } - const key = getAttestationKey() - const address = privateKeyToAddress(key) + const address = getAccountAddress() // TODO: Check with the new Accounts.sol if (address.toLowerCase() !== request.issuer.toLowerCase()) { @@ -49,7 +78,7 @@ async function validateAttestationRequest(request: AttestationRequest) { ) if (state.attestationState !== AttestationState.Incomplete) { - throw new Error('No incomplete attestation found') + throw new Error(NO_INCOMPLETE_ATTESTATION_FOUND_ERROR) } // TODO: Check expiration @@ -60,15 +89,18 @@ async function validateAttestation( attestationRequest: AttestationRequest, attestationCode: string ) { + const key = getAttestationKey() + const address = privateKeyToAddress(key) const attestations = await kit.contracts.getAttestations() const isValid = await attestations.validateAttestationCode( attestationRequest.phoneNumber, attestationRequest.account, - attestationRequest.issuer, + address, attestationCode ) + if (!isValid) { - throw new Error('Valid attestation could not be provided') + throw new Error(ATTESTATION_ERROR) } return } @@ -87,6 +119,115 @@ function createAttestationTextMessage(attestationCode: string) { return `<#> ${toBase64(attestationCode)} ${process.env.APP_SIGNATURE}` } +async function ensureLockedRecord( + attestationRequest: AttestationRequest, + transaction: Transaction +) { + const AttestationTable = await getAttestationTable() + await AttestationTable.findOrCreate({ + where: { + phoneNumber: attestationRequest.phoneNumber, + account: attestationRequest.account, + issuer: attestationRequest.issuer, + }, + defaults: { + smsProvider: SmsProviderType.UNKNOWN, + status: AttestationStatus.DISPATCHING, + }, + transaction, + }) + + // Query to lock the record + const attestationRecord = await existingAttestationRequestRecord( + attestationRequest.phoneNumber, + attestationRequest.account, + attestationRequest.issuer, + { transaction, lock: Transaction.LOCK.UPDATE } + ) + + if (!attestationRecord) { + // This should never happen + throw new Error(`Somehow we did not get an attestation record`) + } + + if (!attestationRecord.canSendSms()) { + // Another transaction has locked on the record before we did + throw new Error(`Another process has already sent the sms`) + } + + return attestationRecord +} + +async function sendSmsAndPersistAttestation( + attestationRequest: AttestationRequest, + attestationCode: string +) { + const textMessage = createAttestationTextMessage(attestationCode) + let attestationRecord: AttestationModel | null = null + + const transaction = await sequelize!.transaction() + + try { + attestationRecord = await ensureLockedRecord(attestationRequest, transaction) + const provider = smsProviderFor(attestationRequest.phoneNumber) + + if (!provider) { + await attestationRecord.update( + { status: AttestationStatus.UNABLE_TO_SERVE, smsProvider: SmsProviderType.UNKNOWN }, + { transaction } + ) + await transaction.commit() + return attestationRecord + } + + try { + await provider.sendSms(attestationRequest.phoneNumber, textMessage) + await attestationRecord.update( + { status: AttestationStatus.SENT, smsProvider: provider.type }, + { transaction } + ) + } catch (error) { + await attestationRecord.update( + { status: AttestationStatus.FAILED, smsProvider: provider.type }, + { transaction } + ) + } + + await transaction.commit() + } catch (error) { + console.error(error) + await transaction.rollback() + } + + return attestationRecord +} + +function respondAfterSendingSms(res: express.Response, attestationRecord: AttestationModel | null) { + if (!attestationRecord) { + console.error('Attestation Record was not created') + respondWithError(res, 500, SMS_SENDING_ERROR) + return + } + + switch (attestationRecord.status) { + case AttestationStatus.SENT: + res.status(201).json({ success: true }) + return + case AttestationStatus.FAILED: + respondWithError(res, 500, SMS_SENDING_ERROR) + return + case AttestationStatus.UNABLE_TO_SERVE: + respondWithError(res, 422, COUNTRY_CODE_NOT_SERVED_ERROR) + default: + console.error( + 'Attestation Record should either be failed or sent, but was ', + attestationRecord.status + ) + respondWithError(res, 500, SMS_SENDING_ERROR) + return + } +} + export async function handleAttestationRequest( _req: express.Request, res: express.Response, @@ -99,26 +240,19 @@ export async function handleAttestationRequest( await validateAttestation(attestationRequest, attestationCode) } catch (error) { console.error(error) - res.status(422).json({ success: false, error: error.toString() }) + respondWithError(res, 422, error.toString()) return } try { - const textMessage = createAttestationTextMessage(attestationCode) - await persistAttestationRequest( - attestationRequest.phoneNumber, - attestationRequest.account, - attestationRequest.issuer + const attestationRecord = await sendSmsAndPersistAttestation( + attestationRequest, + attestationCode ) - await retryAsyncWithBackOff(sendSms, 10, [attestationRequest.phoneNumber, textMessage], 1000) + respondAfterSendingSms(res, attestationRecord) } catch (error) { console.error(error) - res.status(500).json({ - success: false, - error: 'Something went wrong while attempting to send SMS, try again later', - }) + respondWithError(res, 500, SMS_SENDING_ERROR) return } - - res.json({ success: true }) } diff --git a/packages/attestation-service/src/db.ts b/packages/attestation-service/src/db.ts index 5a75590d85c..feec25c5bfd 100644 --- a/packages/attestation-service/src/db.ts +++ b/packages/attestation-service/src/db.ts @@ -1,7 +1,7 @@ import { ContractKit, newKit } from '@celo/contractkit' -import { Sequelize } from 'sequelize' +import { FindOptions, Sequelize } from 'sequelize' import { fetchEnv } from './env' -import Attestation, { AttestationStatic } from './models/attestation' +import Attestation, { AttestationModel, AttestationStatic } from './models/attestation' export let sequelize: Sequelize | undefined @@ -15,15 +15,21 @@ export function initializeDB() { export let kit: ContractKit -export function initializeKit() { +export async function initializeKit() { if (kit === undefined) { kit = newKit(fetchEnv('CELO_PROVIDER')) + const blockNumber = await kit.web3.eth.getBlockNumber() + if (blockNumber === 0) { + throw new Error( + 'Could not fetch latest block from web3 provider ' + fetchEnv('CELO_PROVIDER') + ) + } } } let AttestationTable: AttestationStatic -async function getAttestationTable() { +export async function getAttestationTable() { if (AttestationTable) { return AttestationTable } @@ -31,18 +37,14 @@ async function getAttestationTable() { return AttestationTable } -export async function existingAttestationRequest( +export async function existingAttestationRequestRecord( phoneNumber: string, account: string, - issuer: string -): Promise { - return (await getAttestationTable()).findOne({ where: { phoneNumber, account, issuer } }) -} - -export async function persistAttestationRequest( - phoneNumber: string, - account: string, - issuer: string -) { - return (await getAttestationTable()).create({ phoneNumber, account, issuer }) + issuer: string, + options: FindOptions = {} +): Promise { + return (await getAttestationTable()).findOne({ + where: { phoneNumber, account, issuer }, + ...options, + }) } diff --git a/packages/attestation-service/src/index.ts b/packages/attestation-service/src/index.ts index 8b7d059f1f1..38cce839c13 100644 --- a/packages/attestation-service/src/index.ts +++ b/packages/attestation-service/src/index.ts @@ -1,17 +1,20 @@ import * as dotenv from 'dotenv' import express from 'express' -import { AttestationRequestType, handleAttestationRequest } from './attestation' +import { AttestationRequestType, getAttestationKey, handleAttestationRequest } from './attestation' import { initializeDB, initializeKit } from './db' import { createValidatedHandler } from './request' import { initializeSmsProviders } from './sms' async function init() { + console.info(process.env.CONFIG) if (process.env.CONFIG) { dotenv.config({ path: process.env.CONFIG }) } await initializeDB() await initializeKit() + // TODO: Validate that the attestation key has been authorized by the account + getAttestationKey() await initializeSmsProviders() const app = express() diff --git a/packages/attestation-service/src/models/attestation.ts b/packages/attestation-service/src/models/attestation.ts index a2b79e765b7..24e24e7d092 100644 --- a/packages/attestation-service/src/models/attestation.ts +++ b/packages/attestation-service/src/models/attestation.ts @@ -1,18 +1,52 @@ import { BuildOptions, DataTypes, Model, Sequelize } from 'sequelize' +import { SmsProviderType } from '../sms/base' -interface AttestationModel extends Model { +export interface AttestationModel extends Model { readonly id: number account: string phoneNumber: string issuer: string + status: AttestationStatus + smsProvider: SmsProviderType + + canSendSms: () => boolean +} + +export enum AttestationStatus { + DISPATCHING = 'DISPATCHING', + UNABLE_TO_SERVE = 'UNABLE_TO_SERVE', + FAILED = 'FAILED', + SENT = 'SMS_SEND_SUCCESS', + COMPLETE = 'COMPLETE', } export type AttestationStatic = typeof Model & (new (values?: object, options?: BuildOptions) => AttestationModel) -export default (sequelize: Sequelize) => - sequelize.define('Attestations', { +export default (sequelize: Sequelize) => { + const model = sequelize.define('Attestations', { account: DataTypes.STRING, phoneNumber: DataTypes.STRING, issuer: DataTypes.STRING, + status: DataTypes.STRING, + smsProvider: DataTypes.STRING, }) as AttestationStatic + + model.prototype.canSendSms = function() { + console.log( + this.status, + [ + AttestationStatus.DISPATCHING, + AttestationStatus.FAILED, + AttestationStatus.UNABLE_TO_SERVE, + ].includes(this.status) + ) + return [ + AttestationStatus.DISPATCHING, + AttestationStatus.FAILED, + AttestationStatus.UNABLE_TO_SERVE, + ].includes(this.status) + } + + return model +} diff --git a/packages/attestation-service/src/request.ts b/packages/attestation-service/src/request.ts index dfd5ae13910..ea46ee6f515 100644 --- a/packages/attestation-service/src/request.ts +++ b/packages/attestation-service/src/request.ts @@ -1,7 +1,5 @@ -import { isE164NumberStrict } from '@celo/utils/lib/phoneNumbers' -import { isValidAddress } from '@celo/utils/lib/signatureUtils' import express from 'express' -import { either, isLeft } from 'fp-ts/lib/Either' +import { isLeft } from 'fp-ts/lib/Either' import * as t from 'io-ts' export function createValidatedHandler( @@ -27,37 +25,6 @@ export function createValidatedHandler( } } -export const E164PhoneNumberType = new t.Type( - 'E164Number', - t.string.is, - (input, context) => - either.chain( - t.string.validate(input, context), - (stringValue) => - isE164NumberStrict(stringValue) - ? t.success(stringValue) - : t.failure(stringValue, context, 'is not a valid e164 number') - ), - String -) - -export const AddressType = new t.Type( - 'Address', - t.string.is, - (input, context) => - either.chain( - t.string.validate(input, context), - (stringValue) => - isValidAddress(stringValue) - ? t.success(stringValue) - : t.failure(stringValue, context, 'is not a valid address') - ), - String -) - -export type Address = t.TypeOf -export type E164Number = t.TypeOf - function serializeErrors(errors: t.Errors) { let serializedErrors: any = {} errors.map((error) => { @@ -81,3 +48,7 @@ function serializeErrors(errors: t.Errors) { }) return serializedErrors } + +export function respondWithError(res: express.Response, statusCode: number, error: string) { + res.status(statusCode).json({ success: false, error }) +} diff --git a/packages/attestation-service/src/sms.ts b/packages/attestation-service/src/sms.ts deleted file mode 100644 index 96faca6500f..00000000000 --- a/packages/attestation-service/src/sms.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { PhoneNumberUtil } from 'google-libphonenumber' -import Nexmo from 'nexmo' -import { fetchEnv } from './env' - -const phoneUtil = PhoneNumberUtil.getInstance() - -let nexmoClient: any -let nexmoNumbers: Array<{ - code: string - phoneNumber: string -}> = [] - -export async function initializeSmsProviders() { - nexmoClient = new Nexmo({ - apiKey: fetchEnv('NEXMO_KEY'), - apiSecret: fetchEnv('NEXMO_SECRET'), - }) - - const availableNumbers = await getAvailableNumbers() - - nexmoNumbers = availableNumbers.map((number: any) => ({ - phoneNumber: number.msisdn, - code: phoneUtil.getRegionCodeForNumber(phoneUtil.parse('+' + number.msisdn)), - })) - - console.log(nexmoNumbers) -} - -async function getAvailableNumbers(): Promise { - return new Promise((resolve, reject) => { - nexmoClient.number.get(null, (err: Error, responseData: any) => { - if (err) { - reject(err) - } else { - resolve(responseData.numbers) - } - }) - }) -} - -function getMatchingNumber(countryCode: string) { - const matchingNumber = nexmoNumbers.find((number) => number.code === countryCode) - if (matchingNumber !== undefined) { - return matchingNumber.phoneNumber - } - return nexmoNumbers[0].phoneNumber -} - -export async function sendSms(phoneNumber: string, message: string): Promise { - const countryCode = phoneUtil.getRegionCodeForNumber(phoneUtil.parse(phoneNumber)) - - if (!countryCode) { - throw new Error('could not extract country code') - } - - return new Promise((resolve, reject) => { - nexmoClient.message.sendSms( - getMatchingNumber(countryCode), - phoneNumber, - message, - (err: Error, responseData: any) => { - if (err) { - reject(err) - } else { - if (responseData.messages[0].status === '0') { - resolve(responseData.messages[0]) - } else { - reject(responseData.messages[0]['error-text']) - } - } - } - ) - }) -} diff --git a/packages/attestation-service/src/sms/base.ts b/packages/attestation-service/src/sms/base.ts new file mode 100644 index 00000000000..326aa1c6900 --- /dev/null +++ b/packages/attestation-service/src/sms/base.ts @@ -0,0 +1,20 @@ +import { E164Number } from '@celo/utils/lib/io' +import { PhoneNumberUtil } from 'google-libphonenumber' +const phoneUtil = PhoneNumberUtil.getInstance() + +export abstract class SmsProvider { + abstract type: SmsProviderType + blacklistedRegionCodes: string[] = [] + + canServePhoneNumber(phoneNumber: E164Number) { + const countryCode = phoneUtil.getRegionCodeForNumber(phoneUtil.parse(phoneNumber)) + return !!countryCode && !this.blacklistedRegionCodes.includes(countryCode) + } + // Should throw Error when unsuccesful, return if successful + abstract sendSms(phoneNumber: E164Number, message: string): Promise +} + +export enum SmsProviderType { + NEXMO = 'nexmo', + UNKNOWN = 'unknown', +} diff --git a/packages/attestation-service/src/sms/index.ts b/packages/attestation-service/src/sms/index.ts new file mode 100644 index 00000000000..58c6a701e03 --- /dev/null +++ b/packages/attestation-service/src/sms/index.ts @@ -0,0 +1,32 @@ +import { E164Number } from '@celo/utils/lib/io' +import { fetchEnv } from '../env' +import { SmsProvider, SmsProviderType } from './base' +import { NexmoSmsProvider } from './nexmo' + +const smsProviders: SmsProvider[] = [] + +export async function initializeSmsProviders() { + const configuredSmsProviders = fetchEnv('SMS_PROVIDERS').split(',') as Array< + SmsProviderType | string + > + + if (configuredSmsProviders.length === 0) { + throw new Error('You have to specify at least one sms provider') + } + + for (const configuredSmsProvider of configuredSmsProviders) { + switch (configuredSmsProvider) { + case SmsProviderType.NEXMO: + const provider = NexmoSmsProvider.fromEnv() + await provider.initialize() + smsProviders.push(provider) + break + default: + break + } + } +} + +export function smsProviderFor(phoneNumber: E164Number) { + return smsProviders.find((provider) => provider.canServePhoneNumber(phoneNumber)) +} diff --git a/packages/attestation-service/src/sms/nexmo.ts b/packages/attestation-service/src/sms/nexmo.ts new file mode 100644 index 00000000000..90d4107a898 --- /dev/null +++ b/packages/attestation-service/src/sms/nexmo.ts @@ -0,0 +1,102 @@ +import { retryAsyncWithBackOff } from '@celo/utils/lib/async' +import { E164Number } from '@celo/utils/lib/io' +import { PhoneNumberUtil } from 'google-libphonenumber' +import Nexmo from 'nexmo' +import { fetchEnv } from '../env' +import { SmsProvider, SmsProviderType } from './base' + +const phoneUtil = PhoneNumberUtil.getInstance() + +export class NexmoSmsProvider extends SmsProvider { + static fromEnv() { + return new NexmoSmsProvider( + fetchEnv('NEXMO_KEY'), + fetchEnv('NEXMO_SECRET'), + fetchEnv('NEXMO_BLACKLIST').split(',') + ) + } + type = SmsProviderType.NEXMO + client: any + nexmoNumbers: Array<{ + code: string + phoneNumber: string + }> = [] + + constructor(apiKey: string, apiSecret: string, blacklistedRegionCodes: string[]) { + super() + this.client = new Nexmo({ + apiKey, + apiSecret, + }) + + this.blacklistedRegionCodes = blacklistedRegionCodes + } + + initialize = async () => { + const availableNumbers = await this.getAvailableNumbers() + this.nexmoNumbers = availableNumbers.map((number: any) => ({ + phoneNumber: number.msisdn, + code: phoneUtil.getRegionCodeForNumber(phoneUtil.parse('+' + number.msisdn)), + })) + } + + sendSms = async (phoneNumber: E164Number, message: string): Promise => { + const countryCode = phoneUtil.getRegionCodeForNumber(phoneUtil.parse(phoneNumber)) + + if (!countryCode) { + throw new Error('could not extract country code') + } + + const nexmoNumber = this.getMatchingNumber(countryCode) + // Nexmo does not support sending more than 1 text message a second from some phone numbers, so just + // repeat with backoff + await retryAsyncWithBackOff( + () => this.sendSmsViaNexmo(nexmoNumber, phoneNumber, message), + 10, + [], + 1000 + ) + return + } + + private sendSmsViaNexmo(nexmoNumber: string, phoneNumber: string, message: string) { + return new Promise((resolve, reject) => { + this.client.message.sendSms( + nexmoNumber, + phoneNumber, + message, + (err: Error, responseData: any) => { + if (err) { + reject(err) + } else { + if (responseData.messages[0].status === '0') { + resolve(responseData.messages[0]) + } else { + reject(responseData.messages[0]['error-text']) + } + } + } + ) + }) + } + + private getAvailableNumbers = async (): Promise => { + return new Promise((resolve, reject) => { + this.client.number.get(null, (err: Error, responseData: any) => { + if (err) { + reject(err) + } else { + resolve(responseData.numbers) + } + }) + }) + } + + private getMatchingNumber = (countryCode: string) => { + const matchingNumber = this.nexmoNumbers.find((number) => number.code === countryCode) + if (matchingNumber !== undefined) { + return matchingNumber.phoneNumber + } + return this.nexmoNumbers[0].phoneNumber + } +} diff --git a/packages/blockchain-api/jest.config.js b/packages/blockchain-api/jest.config.js index f7a5eb26ad3..10171e016ac 100644 --- a/packages/blockchain-api/jest.config.js +++ b/packages/blockchain-api/jest.config.js @@ -2,7 +2,7 @@ module.exports = { moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], setupFiles: ['./setupJest.ts'], testMatch: ['**/?(*.)(spec|test).ts?(x)'], - testResultsProcessor: 'jest-junit', + // testResultsProcessor: 'jest-junit', transform: { '^.+\\.tsx?$': 'ts-jest', }, diff --git a/packages/blockchain-api/package.json b/packages/blockchain-api/package.json index 49d3ba36bfc..fcd26172452 100644 --- a/packages/blockchain-api/package.json +++ b/packages/blockchain-api/package.json @@ -17,7 +17,7 @@ "deploy": "./deploy.sh" }, "dependencies": { - "@celo/contractkit": "0.1.6", + "@celo/contractkit": "0.2.0", "apollo-datasource-rest": "^0.3.1", "apollo-server-express": "^2.4.2", "bignumber.js": "^7.2.0", @@ -36,5 +36,8 @@ "jest-fetch-mock": "^2.1.2", "tsc-watch": "^1.0.31", "typescript": "^3.5.3" + }, + "resolutions": { + "**/cross-fetch": "3.0.4" } } diff --git a/packages/celotool/ci_test_sync_with_network.sh b/packages/celotool/ci_test_sync_with_network.sh deleted file mode 100755 index 99645d25d71..00000000000 --- a/packages/celotool/ci_test_sync_with_network.sh +++ /dev/null @@ -1,62 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# This test starts a local node which tries to sync with remotely running nodes and -# verifies that the sync works. - -# For testing a particular commit hash of Geth repo (usually, on Circle CI) -# Usage: ci_test_sync_with_network.sh checkout -# For testing the local Geth dir (usually, for manual testing) -# Usage: ci_test_sync_with_network.sh local - -if [ "${1}" == "checkout" ]; then - export GETH_DIR="/tmp/geth" - # Test master by default. - COMMIT_HASH_TO_TEST=${2:-"master"} - echo "Checking out geth at commit hash ${COMMIT_HASH_TO_TEST}..." - # Shallow clone up to depth of 20. If the COMMIT_HASH_TO_TEST is not within the last 20 hashes then - # this test will fail. This will force someone to keep updating the COMMIT_HASH_TO_TEST we are - # testing. Clone up to 20 takes about 4 seconds on my machine and a full clone is - # about 60 seconds as of May 20, 2019. The difference will only grow over time. - git clone --depth 20 https://github.com/celo-org/celo-blockchain.git ${GETH_DIR} && cd ${GETH_DIR} && git checkout ${COMMIT_HASH_TO_TEST} && cd - -elif [ "${1}" == "local" ]; then - export GETH_DIR="${2}" - echo "Testing using local geth dir ${GETH_DIR}..." -fi - -# For now, the script assumes that it runs from a sub-dir of sub-dir of monorepo directory. -CELO_MONOREPO_DIR="${PWD}/../.." -# Assume that the logs are in /tmp/geth_stdout -GETH_LOG_FILE=/tmp/geth_stdout - -# usage: test_ultralight_sync -test_ultralight_sync () { - NETWORK_NAME=$1 - echo "Testing ultralight sync with '${NETWORK_NAME}' network" - # Run the sync in ultralight mode - geth_tests/network_sync_test.sh ${NETWORK_NAME} ultralight - # Verify what happened by reading the logs. - ${CELO_MONOREPO_DIR}/node_modules/.bin/mocha -r ts-node/register ${CELO_MONOREPO_DIR}/packages/celotool/src/e2e-tests/verify_ultralight_geth_logs.ts --network "${NETWORK_NAME}" --gethlogfile ${GETH_LOG_FILE} -} - -# Some code in celotool requires this file to contain the MNEMONOIC. -# The value of MNEMONOIC does not matter. -if [[ ! -e ${CELO_MONOREPO_DIR}/.env.mnemonic ]]; then - echo "MNEMONOIC=anything random" > ${CELO_MONOREPO_DIR}/.env.mnemonic -fi - -# Test syncing -export NETWORK_NAME="integration" -# Add an extra echo at the end to dump a new line, this makes the results a bit more readable. -geth_tests/network_sync_test.sh ${NETWORK_NAME} full && echo -# This is broken, I am not sure why, therefore, commented for now. -# geth_tests/network_sync_test.sh ${NETWORK_NAME} fast && echo -geth_tests/network_sync_test.sh ${NETWORK_NAME} light && echo -test_ultralight_sync ${NETWORK_NAME} && echo - -export NETWORK_NAME="alfajoresstaging" -geth_tests/network_sync_test.sh ${NETWORK_NAME} full && echo -# This is broken, I am not sure why, therefore, commented for now. -# geth_tests/network_sync_test.sh ${NETWORK_NAME} fast && echo -geth_tests/network_sync_test.sh ${NETWORK_NAME} light && echo -test_ultralight_sync ${NETWORK_NAME} && echo diff --git a/packages/celotool/geth_tests/network_sync_test.sh b/packages/celotool/geth_tests/network_sync_test.sh deleted file mode 100755 index 2997b623f55..00000000000 --- a/packages/celotool/geth_tests/network_sync_test.sh +++ /dev/null @@ -1,51 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# Usage: geth_tests/integration_network_sync_test.sh [network name] [sync mode] -# Default to testing the integration network -NETWORK_NAME=${1:-"integration"} -# Default to testing the full sync mode -SYNCMODE=${2:-"full"} - -echo "This test will start a local node in '${SYNCMODE}' sync mode which will connect to network '${NETWORK_NAME}' and verify that syncing works" - -echo "Setting constants..." -# For now, the script assumes that it runs from a sub-dir of sub-dir of monorepo directory. -CELO_MONOREPO_DIR="${PWD}/../.." - -DATA_DIR="/tmp/tmp1" -GENESIS_FILE_PATH="/tmp/genesis_ibft.json" - -GETH_BINARY="${GETH_DIR}/build/bin/geth --datadir ${DATA_DIR}" -CELOTOOLJS="${CELO_MONOREPO_DIR}/packages/celotool/bin/celotooljs.sh" - -curl "https://www.googleapis.com/storage/v1/b/genesis_blocks/o/${NETWORK_NAME}?alt=media" --output ${GENESIS_FILE_PATH} - -${CELOTOOLJS} geth build --geth-dir ${GETH_DIR} - -rm -rf ${DATA_DIR} -${GETH_BINARY} init ${GENESIS_FILE_PATH} 1>/dev/null 2>/dev/null -curl "https://www.googleapis.com/storage/v1/b/static_nodes/o/${NETWORK_NAME}?alt=media" --output ${DATA_DIR}/static-nodes.json - -echo "Running geth in the background..." -LOG_FILE="/tmp/geth_stdout" -# Run geth in the background -${CELOTOOLJS} geth run \ - --geth-dir ${GETH_DIR} \ - --data-dir ${DATA_DIR} \ - --sync-mode ${SYNCMODE} 1>${LOG_FILE} 2>/tmp/geth_stderr & -# let it sync -sleep 20 -latestBlock=$(${GETH_BINARY} attach -exec eth.blockNumber) -echo "Latest block number is ${latestBlock}" - -pkill -9 geth - -if [ "$latestBlock" -eq "0" ]; then - echo "Sync is not working with network '${NETWORK_NAME}' in mode '${SYNCMODE}', see logs in ${LOG_FILE}" - if test ${CI}; then - echo "Running on CI, dumping logs from ${LOG_FILE}..." - cat ${LOG_FILE} - fi - exit 1 -fi diff --git a/packages/celotool/package.json b/packages/celotool/package.json index 4d0b5fb07b1..569b0fd5bca 100644 --- a/packages/celotool/package.json +++ b/packages/celotool/package.json @@ -9,7 +9,7 @@ "@celo/verification-pool-api": "^1.0.0", "@celo/utils": "^0.1.0", "@celo/walletkit": "^0.0.14", - "@celo/contractkit": "0.1.6", + "@celo/contractkit": "0.2.1-dev", "@google-cloud/monitoring": "0.7.1", "@google-cloud/pubsub": "^0.28.1", "@google-cloud/storage": "^2.4.3", diff --git a/packages/celotool/src/cmds/account/faucet.ts b/packages/celotool/src/cmds/account/faucet.ts index 168eb3b7892..bea3ad7f493 100644 --- a/packages/celotool/src/cmds/account/faucet.ts +++ b/packages/celotool/src/cmds/account/faucet.ts @@ -1,5 +1,6 @@ /* tslint:disable no-console */ import { newKit } from '@celo/contractkit' +import { switchToClusterFromEnv } from 'src/lib/cluster' import { convertToContractDecimals } from 'src/lib/contract-utils' import { portForwardAnd } from 'src/lib/port_forward' import { validateAccountAddress } from 'src/lib/utils' @@ -29,6 +30,8 @@ export const builder = (argv: yargs.Argv) => { } export const handler = async (argv: FaucetArgv) => { + await switchToClusterFromEnv() + const address = argv.account const cb = async () => { diff --git a/packages/celotool/src/e2e-tests/attestations_tests.ts b/packages/celotool/src/e2e-tests/attestations_tests.ts index 4e30437bc01..0787da38173 100644 --- a/packages/celotool/src/e2e-tests/attestations_tests.ts +++ b/packages/celotool/src/e2e-tests/attestations_tests.ts @@ -6,7 +6,7 @@ import { getContext, GethTestConfig, sleep } from './utils' const validatorAddress = '0x47e172f6cfb6c7d01c1574fa3e2be7cc73269d95' const phoneNumber = '+15555555555' -describe('governance tests', () => { +describe('attestations tests', () => { const gethConfig: GethTestConfig = { migrate: true, instances: [ diff --git a/packages/celotool/src/e2e-tests/governance_tests.ts b/packages/celotool/src/e2e-tests/governance_tests.ts index 785c7809188..39aea87e9b4 100644 --- a/packages/celotool/src/e2e-tests/governance_tests.ts +++ b/packages/celotool/src/e2e-tests/governance_tests.ts @@ -1,5 +1,7 @@ +// tslint:disable-next-line: no-reference (Required to make this work w/ ts-node) +/// import { ContractKit, newKitFromWeb3 } from '@celo/contractkit' -import { AccountsWrapper } from '@celo/contractkit/lib/wrappers/Accounts' +import { getBlsPoP, getBlsPublicKey } from '@celo/utils/lib/bls' import { fromFixed, toFixed } from '@celo/utils/lib/fixidity' import BigNumber from 'bignumber.js' import { assert } from 'chai' @@ -8,17 +10,115 @@ import { assertAlmostEqual, getContext, getEnode, + GethInstanceConfig, importGenesis, initAndStartGeth, sleep, } from './utils' +interface MemberSwapper { + swap(): Promise +} + +async function newMemberSwapper(kit: ContractKit, members: string[]): Promise { + let index = 0 + const group = (await kit.web3.eth.getAccounts())[0] + await Promise.all(members.slice(1).map((member) => removeMember(member))) + + async function removeMember(member: string) { + return (await kit.contracts.getValidators()) + .removeMember(member) + .sendAndWaitForReceipt({ from: group }) + } + + async function addMember(member: string) { + return (await (await kit.contracts.getValidators()).addMember( + group, + member + )).sendAndWaitForReceipt({ from: group }) + } + + async function getGroupMembers() { + const groupInfo = await (await kit._web3Contracts.getValidators()).methods + .getValidatorGroup(group) + .call() + return groupInfo[0] + } + + return { + async swap() { + const removedMember = members[index % members.length] + await removeMember(members[index % members.length]) + index = index + 1 + const addedMember = members[index % members.length] + await addMember(members[index % members.length]) + const groupMembers = await getGroupMembers() + assert.include(groupMembers, addedMember) + assert.notInclude(groupMembers, removedMember) + }, + } +} + +interface KeyRotator { + rotate(): Promise +} + +async function newKeyRotator( + kit: ContractKit, + web3s: Web3[], + privateKeys: string[] +): Promise { + let index = 0 + const validator = (await kit.web3.eth.getAccounts())[0] + const accountsWrapper = await kit.contracts.getAccounts() + + async function authorizeValidatorSigner(signer: string, signerWeb3: any) { + const signerKit = newKitFromWeb3(signerWeb3) + const pop = await (await signerKit.contracts.getAccounts()).generateProofOfSigningKeyPossession( + validator, + signer + ) + return (await accountsWrapper.authorizeValidatorSigner(signer, pop)).sendAndWaitForReceipt({ + from: validator, + }) + } + + async function updateValidatorBlsKey(signerPrivateKey: string) { + const blsPublicKey = getBlsPublicKey(signerPrivateKey) + const blsPop = getBlsPoP(validator, signerPrivateKey) + // TODO(asa): Send this from the signer instead. + const validatorsWrapper = await kit.contracts.getValidators() + return validatorsWrapper + .updateBlsPublicKey(blsPublicKey, blsPop) + .sendAndWaitForReceipt({ from: validator }) + } + + return { + async rotate() { + if (index < web3s.length) { + const signerWeb3 = web3s[index] + const signer: string = (await signerWeb3.eth.getAccounts())[0] + const signerPrivateKey = privateKeys[index] + await Promise.all([ + authorizeValidatorSigner(signer, signerWeb3), + updateValidatorBlsKey(signerPrivateKey), + ]) + index += 1 + assert.equal(await accountsWrapper.getValidatorSigner(validator), signer) + } + }, + } +} + +// TODO(asa): Test independent rotation of ecdsa, bls keys. describe('governance tests', () => { const gethConfig = { migrate: true, instances: [ + // Validators 0 and 1 are swapped in and out of the group. { name: 'validator0', validating: true, syncmode: 'full', port: 30303, rpcport: 8545 }, { name: 'validator1', validating: true, syncmode: 'full', port: 30305, rpcport: 8547 }, + // Validator 2 will authorize a validating key every other epoch. { name: 'validator2', validating: true, syncmode: 'full', port: 30307, rpcport: 8549 }, { name: 'validator3', validating: true, syncmode: 'full', port: 30309, rpcport: 8551 }, { name: 'validator4', validating: true, syncmode: 'full', port: 30311, rpcport: 8553 }, @@ -34,7 +134,7 @@ describe('governance tests', () => { let goldToken: any let registry: any let validators: any - let accounts: AccountsWrapper + let accounts: any let kit: ContractKit before(async function(this: any) { @@ -55,12 +155,7 @@ describe('governance tests', () => { registry = await kit._web3Contracts.getRegistry() election = await kit._web3Contracts.getElection() epochRewards = await kit._web3Contracts.getEpochRewards() - accounts = await kit.contracts.getAccounts() - } - - const unlockAccount = async (address: string, theWeb3: any) => { - // Assuming empty password - await theWeb3.eth.personal.unlockAccount(address, '', 1000) + accounts = await kit._web3Contracts.getAccounts() } const getValidatorGroupMembers = async (blockNumber?: number) => { @@ -79,9 +174,17 @@ describe('governance tests', () => { } } - const getValidatorGroupKeys = async () => { + const getValidatorSigner = async (address: string, blockNumber?: number) => { + if (blockNumber) { + return accounts.methods.getValidatorSigner(address).call({}, blockNumber) + } else { + return accounts.methods.getValidatorSigner(address).call() + } + } + + const getValidatorGroupPrivateKey = async () => { const [groupAddress] = await validators.methods.getRegisteredValidatorGroups().call() - const name = await accounts.getName(groupAddress) + const name = await accounts.methods.getName(groupAddress).call() const encryptedKeystore64 = name.split(' ')[1] const encryptedKeystore = JSON.parse(Buffer.from(encryptedKeystore64, 'base64').toString()) // The validator group ID is the validator group keystore encrypted with validator 0's @@ -89,70 +192,101 @@ describe('governance tests', () => { // @ts-ignore const encryptionKey = `0x${gethConfig.instances[0].privateKey}` const decryptedKeystore = web3.eth.accounts.decrypt(encryptedKeystore, encryptionKey) - return [groupAddress, decryptedKeystore.privateKey] + return decryptedKeystore.privateKey } const activate = async (account: string, txOptions: any = {}) => { - await unlockAccount(account, web3) const [group] = await validators.methods.getRegisteredValidatorGroups().call() const tx = election.methods.activate(group) let gas = txOptions.gas if (!gas) { - gas = await tx.estimateGas({ ...txOptions }) + gas = (await tx.estimateGas({ ...txOptions })) * 2 } return tx.send({ from: account, ...txOptions, gas }) } - const removeMember = async ( - groupWeb3: any, - group: string, - member: string, - txOptions: any = {} - ) => { - await unlockAccount(group, groupWeb3) - const tx = validators.methods.removeMember(member) - let gas = txOptions.gas - if (!gas) { - gas = await tx.estimateGas({ ...txOptions }) - } - return tx.send({ from: group, ...txOptions, gas }) - } - - const addMember = async (groupWeb3: any, group: string, member: string, txOptions: any = {}) => { - await unlockAccount(group, groupWeb3) - const tx = validators.methods.addMember(member) - let gas = txOptions.gas - if (!gas) { - gas = await tx.estimateGas({ ...txOptions }) - } - return tx.send({ from: group, ...txOptions, gas }) - } - const isLastBlockOfEpoch = (blockNumber: number, epochSize: number) => { return blockNumber % epochSize === 0 } + const assertBalanceChanged = async ( + address: string, + blockNumber: number, + expected: BigNumber, + token: any + ) => { + const currentBalance = new BigNumber( + await token.methods.balanceOf(address).call({}, blockNumber) + ) + const previousBalance = new BigNumber( + await token.methods.balanceOf(address).call({}, blockNumber - 1) + ) + assert.isFalse(currentBalance.isNaN()) + assert.isFalse(previousBalance.isNaN()) + assertAlmostEqual(currentBalance.minus(previousBalance), expected) + } + describe('when the validator set is changing', () => { let epoch: number const blockNumbers: number[] = [] - let allValidators: string[] + let validatorAccounts: string[] before(async function(this: any) { this.timeout(0) // Disable test timeout await restart() - const [groupAddress, groupPrivateKey] = await getValidatorGroupKeys() - - const groupInstance = { - name: 'validatorGroup', - validating: false, - syncmode: 'full', - port: 30325, - wsport: 8567, - privateKey: groupPrivateKey.slice(2), - peers: [await getEnode(8545)], - } - await initAndStartGeth(context.hooks.gethBinaryPath, groupInstance) - allValidators = await getValidatorGroupMembers() - assert.equal(allValidators.length, 5) + const groupPrivateKey = await getValidatorGroupPrivateKey() + const rotation0PrivateKey = + '0xa42ac9c99f6ab2c96ee6cae1b40d36187f65cd878737f6623cd363fb94ba7087' + const rotation1PrivateKey = + '0x4519cae145fb9499358be484ca60c80d8f5b7f9c13ff82c88ec9e13283e9de1a' + const additionalNodes: GethInstanceConfig[] = [ + { + name: 'validatorGroup', + validating: false, + syncmode: 'full', + port: 30313, + wsport: 8555, + rpcport: 8557, + privateKey: groupPrivateKey.slice(2), + peers: [await getEnode(8545)], + }, + ] + await Promise.all( + additionalNodes.map((nodeConfig) => + initAndStartGeth(context.hooks.gethBinaryPath, nodeConfig) + ) + ) + // Connect the validating nodes to the non-validating nodes, to test that announce messages + // are properly gossiped. + const additionalValidatingNodes = [ + { + name: 'validator2KeyRotation0', + validating: true, + syncmode: 'full', + lightserv: false, + port: 30315, + wsport: 8559, + privateKey: rotation0PrivateKey.slice(2), + peers: [await getEnode(8557)], + }, + { + name: 'validator2KeyRotation1', + validating: true, + syncmode: 'full', + lightserv: false, + port: 30317, + wsport: 8561, + privateKey: rotation1PrivateKey.slice(2), + peers: [await getEnode(8557)], + }, + ] + await Promise.all( + additionalValidatingNodes.map((nodeConfig) => + initAndStartGeth(context.hooks.gethBinaryPath, nodeConfig) + ) + ) + + validatorAccounts = await getValidatorGroupMembers() + assert.equal(validatorAccounts.length, 5) epoch = new BigNumber(await validators.methods.getEpochSize().call()).toNumber() assert.equal(epoch, 10) @@ -163,26 +297,39 @@ describe('governance tests', () => { await sleep(0.1) } while (blockNumber % epoch !== 1) - await activate(allValidators[0]) - const groupWeb3 = new Web3('ws://localhost:8567') + await activate(validatorAccounts[0]) + + // Prepare for member swapping. + const groupWeb3 = new Web3('ws://localhost:8555') const groupKit = newKitFromWeb3(groupWeb3) validators = await groupKit._web3Contracts.getValidators() - const membersToSwap = [allValidators[0], allValidators[1]] - let includedMemberIndex = 1 - await removeMember(groupWeb3, groupAddress, membersToSwap[0]) + const membersToSwap = [validatorAccounts[0], validatorAccounts[1]] + const memberSwapper = await newMemberSwapper(groupKit, membersToSwap) + + // Prepare for key rotation. + const validatorWeb3 = new Web3('http://localhost:8549') + const authorizedWeb3s = [new Web3('ws://localhost:8559'), new Web3('ws://localhost:8561')] + const authorizedPrivateKeys = [rotation0PrivateKey, rotation1PrivateKey] + const keyRotator = await newKeyRotator( + newKitFromWeb3(validatorWeb3), + authorizedWeb3s, + authorizedPrivateKeys + ) + let errorWhileChangingValidatorSet = '' const changeValidatorSet = async (header: any) => { - blockNumbers.push(header.number) - // At the start of epoch N, swap members so the validator set is different for epoch N + 1. - if (header.number % epoch === 1) { - const memberToRemove = membersToSwap[includedMemberIndex] - const memberToAdd = membersToSwap[(includedMemberIndex + 1) % 2] - await removeMember(groupWeb3, groupAddress, memberToRemove) - await addMember(groupWeb3, groupAddress, memberToAdd) - includedMemberIndex = (includedMemberIndex + 1) % 2 - const newMembers = await getValidatorGroupMembers() - assert.include(newMembers, memberToAdd) - assert.notInclude(newMembers, memberToRemove) + try { + blockNumbers.push(header.number) + // At the start of epoch N, perform actions so the validator set is different for epoch N + 1. + // Note that all of these actions MUST complete within the epoch. + if (header.number % epoch === 0 && errorWhileChangingValidatorSet === '') { + // 1. Swap validator0 and validator1 so one is a member of the group and the other is not. + // 2. Rotate keys for validator 2 by authorizing a new validating key. + await Promise.all([memberSwapper.swap(), keyRotator.rotate()]) + } + } catch (e) { + console.error(e) + errorWhileChangingValidatorSet = e } } @@ -193,19 +340,20 @@ describe('governance tests', () => { ;(subscription as any).unsubscribe() // Wait for the current epoch to complete. await sleep(epoch) + assert.equal(errorWhileChangingValidatorSet, '') }) - const getValidatorSetAtBlock = async (blockNumber: number) => { - const validatorSetSize = await election.methods - .numberValidatorsInCurrentSet() - .call({}, blockNumber) - const validatorSet = [] - for (let i = 0; i < validatorSetSize; i++) { - validatorSet.push( - await election.methods.validatorAddressFromCurrentSet(i).call({}, blockNumber) + const getValidatorSetSignersAtBlock = async (blockNumber: number): Promise => { + return election.methods.currentValidators().call({}, blockNumber) + } + + const getValidatorSetAccountsAtBlock = async (blockNumber: number) => { + const signingKeys = await getValidatorSetSignersAtBlock(blockNumber) + return Promise.all( + signingKeys.map((address: string) => + accounts.methods.signerToAccount(address).call({}, blockNumber) ) - } - return validatorSet + ) } const getLastEpochBlock = (blockNumber: number) => { @@ -224,24 +372,44 @@ describe('governance tests', () => { } }) - it('should always return a validator set equal to the group members at the end of the last epoch', async () => { + it('should always return a validator set equal to the signing keys of the group members at the end of the last epoch', async function(this: any) { + this.timeout(0) for (const blockNumber of blockNumbers) { const lastEpochBlock = getLastEpochBlock(blockNumber) - const groupMembership = await getValidatorGroupMembers(lastEpochBlock) - const validatorSet = await getValidatorSetAtBlock(blockNumber) - assert.sameMembers(groupMembership, validatorSet) + const memberAccounts = await getValidatorGroupMembers(lastEpochBlock) + const memberSigners = await Promise.all( + memberAccounts.map((v: string) => getValidatorSigner(v, lastEpochBlock)) + ) + const validatorSetSigners = await getValidatorSetSignersAtBlock(blockNumber) + const validatorSetAccounts = await getValidatorSetAccountsAtBlock(blockNumber) + assert.sameMembers(memberSigners, validatorSetSigners) + assert.sameMembers(memberAccounts, validatorSetAccounts) } }) - it('should only have created blocks whose miner was in the current validator set', async () => { + it('should block propose in a round robin fashion', async () => { + let roundRobinOrder: string[] = [] for (const blockNumber of blockNumbers) { - const validatorSet = await getValidatorSetAtBlock(blockNumber) + const lastEpochBlock = getLastEpochBlock(blockNumber) + // Fetch the round robin order if it hasn't already been set for this epoch. + if (roundRobinOrder.length === 0 || blockNumber === lastEpochBlock + 1) { + const validatorSet = await getValidatorSetSignersAtBlock(blockNumber) + roundRobinOrder = await Promise.all( + validatorSet.map( + async (_, i) => (await web3.eth.getBlock(lastEpochBlock + i + 1)).miner + ) + ) + assert.sameMembers(roundRobinOrder, validatorSet) + } + const indexInEpoch = blockNumber - lastEpochBlock - 1 + const expectedProposer = roundRobinOrder[indexInEpoch % roundRobinOrder.length] const block = await web3.eth.getBlock(blockNumber) - assert.include(validatorSet.map((x) => x.toLowerCase()), block.miner.toLowerCase()) + assert.equal(block.miner.toLowerCase(), expectedProposer.toLowerCase()) } }) - it('should update the validator scores at the end of each epoch', async () => { + it('should update the validator scores at the end of each epoch', async function(this: any) { + this.timeout(0) const adjustmentSpeed = fromFixed( new BigNumber((await validators.methods.getValidatorScoreParameters().call())[1]) ) @@ -249,28 +417,28 @@ describe('governance tests', () => { const assertScoreUnchanged = async (validator: string, blockNumber: number) => { const score = new BigNumber( - (await validators.methods.getValidator(validator).call({}, blockNumber))[3] + (await validators.methods.getValidator(validator).call({}, blockNumber)).score ) const previousScore = new BigNumber( - (await validators.methods.getValidator(validator).call({}, blockNumber - 1))[3] + (await validators.methods.getValidator(validator).call({}, blockNumber - 1)).score ) - assert.isNotNaN(score) - assert.isNotNaN(previousScore) + assert.isFalse(score.isNaN()) + assert.isFalse(previousScore.isNaN()) assert.equal(score.toFixed(), previousScore.toFixed()) } const assertScoreChanged = async (validator: string, blockNumber: number) => { const score = new BigNumber( - (await validators.methods.getValidator(validator).call({}, blockNumber))[3] + (await validators.methods.getValidator(validator).call({}, blockNumber)).score ) const previousScore = new BigNumber( - (await validators.methods.getValidator(validator).call({}, blockNumber - 1))[3] + (await validators.methods.getValidator(validator).call({}, blockNumber - 1)).score ) + assert.isFalse(score.isNaN()) + assert.isFalse(previousScore.isNaN()) const expectedScore = adjustmentSpeed .times(uptime) .plus(new BigNumber(1).minus(adjustmentSpeed).times(fromFixed(previousScore))) - assert.isNotNaN(score) - assert.isNotNaN(previousScore) assert.equal(score.toFixed(), toFixed(expectedScore).toFixed()) } @@ -278,10 +446,10 @@ describe('governance tests', () => { let expectUnchangedScores: string[] let expectChangedScores: string[] if (isLastBlockOfEpoch(blockNumber, epoch)) { - expectChangedScores = await getValidatorSetAtBlock(blockNumber) - expectUnchangedScores = allValidators.filter((x) => !expectChangedScores.includes(x)) + expectChangedScores = await getValidatorSetAccountsAtBlock(blockNumber) + expectUnchangedScores = validatorAccounts.filter((x) => !expectChangedScores.includes(x)) } else { - expectUnchangedScores = allValidators + expectUnchangedScores = validatorAccounts expectChangedScores = [] } @@ -295,38 +463,23 @@ describe('governance tests', () => { } }) - it('should distribute epoch payments at the end of each epoch', async () => { + it('should distribute epoch payments at the end of each epoch', async function(this: any) { + this.timeout(0) const commission = 0.1 const targetValidatorEpochPayment = new BigNumber( await epochRewards.methods.targetValidatorEpochPayment().call() ) const [group] = await validators.methods.getRegisteredValidatorGroups().call() - const assertBalanceChanged = async ( - validator: string, - blockNumber: number, - expected: BigNumber - ) => { - const currentBalance = new BigNumber( - await stableToken.methods.balanceOf(validator).call({}, blockNumber) - ) - const previousBalance = new BigNumber( - await stableToken.methods.balanceOf(validator).call({}, blockNumber - 1) - ) - assert.isNotNaN(currentBalance) - assert.isNotNaN(previousBalance) - assertAlmostEqual(currentBalance.minus(previousBalance), expected) - } - const assertBalanceUnchanged = async (validator: string, blockNumber: number) => { - await assertBalanceChanged(validator, blockNumber, new BigNumber(0)) + await assertBalanceChanged(validator, blockNumber, new BigNumber(0), stableToken) } const getExpectedTotalPayment = async (validator: string, blockNumber: number) => { const score = new BigNumber( - (await validators.methods.getValidator(validator).call({}, blockNumber))[2] + (await validators.methods.getValidator(validator).call({}, blockNumber)).score ) - assert.isNotNaN(score) + assert.isFalse(score.isNaN()) // We need to calculate the rewards multiplier for the previous block, before // the rewards actually are awarded. const rewardsMultiplier = new BigNumber( @@ -341,10 +494,12 @@ describe('governance tests', () => { let expectUnchangedBalances: string[] let expectChangedBalances: string[] if (isLastBlockOfEpoch(blockNumber, epoch)) { - expectChangedBalances = await getValidatorSetAtBlock(blockNumber) - expectUnchangedBalances = allValidators.filter((x) => !expectChangedBalances.includes(x)) + expectChangedBalances = await getValidatorSetAccountsAtBlock(blockNumber) + expectUnchangedBalances = validatorAccounts.filter( + (x) => !expectChangedBalances.includes(x) + ) } else { - expectUnchangedBalances = allValidators + expectUnchangedBalances = validatorAccounts expectChangedBalances = [] } @@ -359,17 +514,20 @@ describe('governance tests', () => { await assertBalanceChanged( validator, blockNumber, - expectedTotalPayment.minus(groupPayment) + expectedTotalPayment.minus(groupPayment), + stableToken ) expectedGroupPayment = expectedGroupPayment.plus(groupPayment) } - await assertBalanceChanged(group, blockNumber, expectedGroupPayment) + await assertBalanceChanged(group, blockNumber, expectedGroupPayment, stableToken) } }) - it('should distribute epoch rewards at the end of each epoch', async () => { + it('should distribute epoch rewards at the end of each epoch', async function(this: any) { + this.timeout(0) const lockedGold = await kit._web3Contracts.getLockedGold() const governance = await kit._web3Contracts.getGovernance() + const gasPriceMinimum = await kit._web3Contracts.getGasPriceMinimum() const [group] = await validators.methods.getRegisteredValidatorGroups().call() const assertVotesChanged = async (blockNumber: number, expected: BigNumber) => { @@ -382,6 +540,13 @@ describe('governance tests', () => { assertAlmostEqual(currentVotes.minus(previousVotes), expected) } + // Returns the gas fee base for a given block, which is distributed to the governance contract. + const blockBaseGasFee = async (blockNumber: number): Promise => { + const gas = (await web3.eth.getBlock(blockNumber)).gasUsed + const gpm = await gasPriceMinimum.methods.gasPriceMinimum().call({}, blockNumber) + return new BigNumber(gpm).times(new BigNumber(gas)) + } + const assertGoldTokenTotalSupplyChanged = async ( blockNumber: number, expected: BigNumber @@ -395,26 +560,12 @@ describe('governance tests', () => { assertAlmostEqual(currentSupply.minus(previousSupply), expected) } - const assertBalanceChanged = async ( - address: string, - blockNumber: number, - expected: BigNumber - ) => { - const currentBalance = new BigNumber( - await goldToken.methods.balanceOf(address).call({}, blockNumber) - ) - const previousBalance = new BigNumber( - await goldToken.methods.balanceOf(address).call({}, blockNumber - 1) - ) - assertAlmostEqual(currentBalance.minus(previousBalance), expected) - } - const assertLockedGoldBalanceChanged = async (blockNumber: number, expected: BigNumber) => { - await assertBalanceChanged(lockedGold.options.address, blockNumber, expected) + await assertBalanceChanged(lockedGold.options.address, blockNumber, expected, goldToken) } const assertGovernanceBalanceChanged = async (blockNumber: number, expected: BigNumber) => { - await assertBalanceChanged(governance.options.address, blockNumber, expected) + await assertBalanceChanged(governance.options.address, blockNumber, expected, goldToken) } const assertVotesUnchanged = async (blockNumber: number) => { @@ -429,10 +580,6 @@ describe('governance tests', () => { await assertLockedGoldBalanceChanged(blockNumber, new BigNumber(0)) } - const assertGovernanceBalanceUnchanged = async (blockNumber: number) => { - await assertGovernanceBalanceChanged(blockNumber, new BigNumber(0)) - } - const getStableTokenSupplyChange = async (blockNumber: number) => { const currentSupply = new BigNumber( await stableToken.methods.totalSupply().call({}, blockNumber) @@ -477,13 +624,16 @@ describe('governance tests', () => { .plus(stableTokenSupplyChange.div(exchangeRate)) await assertVotesChanged(blockNumber, expectedEpochReward) await assertLockedGoldBalanceChanged(blockNumber, expectedEpochReward) - await assertGovernanceBalanceChanged(blockNumber, expectedInfraReward) + await assertGovernanceBalanceChanged( + blockNumber, + expectedInfraReward.plus(await blockBaseGasFee(blockNumber)) + ) await assertGoldTokenTotalSupplyChanged(blockNumber, expectedGoldTotalSupplyChange) } else { await assertVotesUnchanged(blockNumber) await assertGoldTokenTotalSupplyUnchanged(blockNumber) await assertLockedGoldBalanceUnchanged(blockNumber) - await assertGovernanceBalanceUnchanged(blockNumber) + await assertGovernanceBalanceChanged(blockNumber, await blockBaseGasFee(blockNumber)) } } }) @@ -498,13 +648,13 @@ describe('governance tests', () => { ) const difference = currentTarget.minus(previousTarget) - // Assert equal to 10 decimal places due to rounding errors. + // Assert equal to 9 decimal places due to rounding errors. assert.equal( fromFixed(difference) - .dp(10) + .dp(9) .toFixed(), fromFixed(expected) - .dp(10) + .dp(9) .toFixed() ) } diff --git a/packages/celotool/src/e2e-tests/sync_tests.ts b/packages/celotool/src/e2e-tests/sync_tests.ts index 2d99ad4944b..965bd1a45d8 100644 --- a/packages/celotool/src/e2e-tests/sync_tests.ts +++ b/packages/celotool/src/e2e-tests/sync_tests.ts @@ -93,7 +93,7 @@ describe('sync tests', function(this: any) { const instance: GethInstanceConfig = gethConfig.instances[0] await killInstance(instance) await initAndStartGeth(hooks.gethBinaryPath, instance) - await sleep(60) // wait for round change / resync + await sleep(120) // wait for round change / resync const address = (await web3.eth.getAccounts())[0] const currentBlock = await web3.eth.getBlock('latest') for (let i = 0; i < gethConfig.instances.length; i++) { diff --git a/packages/celotool/src/e2e-tests/transfer_tests.ts b/packages/celotool/src/e2e-tests/transfer_tests.ts index eab473053ae..25f994d2578 100644 --- a/packages/celotool/src/e2e-tests/transfer_tests.ts +++ b/packages/celotool/src/e2e-tests/transfer_tests.ts @@ -18,35 +18,36 @@ import { sleep, } from './utils' -const nowSeconds = () => Math.floor(Date.now() / 1000) - /** * Helper Class to change StableToken Inflation in tests */ class InflationManager { private kit: ContractKit + private readonly minUpdateDelay = 10 + constructor(readonly validatorUri: string, readonly validatorAddress: string) { this.kit = newKit(validatorUri) this.kit.defaultAccount = validatorAddress } + now = async (): Promise => { + return (await this.kit.web3.eth.getBlock('pending')).timestamp + } + getNextUpdateRate = async (): Promise => { const stableToken = await this.kit.contracts.getStableToken() // Compute necessary `updateRate` so inflationFactor adjusment takes place on next operation const { factorLastUpdated } = await stableToken.getInflationParameters() - const timeSinceLastUpdated = nowSeconds() - factorLastUpdated.toNumber() - if (timeSinceLastUpdated < 10) { - // tslint:disable-next-line: no-console - console.log( - `Last inflation change too close, waiting ${10 - - timeSinceLastUpdated} seconds before doing it again` - ) - await sleep(10 - timeSinceLastUpdated) - return this.getNextUpdateRate() - } else { - return timeSinceLastUpdated + // Wait until the minimum update delay has passed so we can set a rate that gives us some + // buffer time to make the transaction in the next availiable update window. + let timeSinceLastUpdated = (await this.now()) - factorLastUpdated.toNumber() + while (timeSinceLastUpdated < this.minUpdateDelay) { + await sleep(this.minUpdateDelay - timeSinceLastUpdated) + timeSinceLastUpdated = (await this.now()) - factorLastUpdated.toNumber() } + + return timeSinceLastUpdated } getParameters = async () => { @@ -54,16 +55,12 @@ class InflationManager { return stableToken.getInflationParameters() } - changeInflationFactorOnNextTransfer = async (desiredFactor: BigNumber) => { - const parameters = await this.getParameters() - if (desiredFactor.eq(parameters.factor)) { - return - } + setInflationRateForNextTransfer = async (rate: BigNumber) => { + // Possibly update the inflation factor and ensure it won't update again. + await this.setInflationParameters(new BigNumber(1), Number.MAX_SAFE_INTEGER) - // desiredFactor = factor * rate - const nextRate = desiredFactor.div(parameters.factor) const updateRate = await this.getNextUpdateRate() - await this.setInflationParameters(nextRate, updateRate) + await this.setInflationParameters(rate, updateRate) } setInflationParameters = async (rate: BigNumber, updatePeriod: number) => { @@ -72,19 +69,6 @@ class InflationManager { .setInflationParameters(toFixed(rate).toString(), updatePeriod) .sendAndWaitForReceipt({ from: this.validatorAddress }) } - - resetInflation = async () => { - await this.changeInflationFactorOnNextTransfer(new BigNumber('1')) - - const ONE = new BigNumber('1') - const ONE_WEEK = 7 * 24 * 60 * 60 - - // Reset factor, and change updatePeriod so no new inflation is added - await this.setInflationParameters(ONE, ONE_WEEK) - - const parametersPost = await this.getParameters() - assertEqualBN(parametersPost.factor, ONE) - } } const setIntrinsicGas = async (validatorUri: string, validatorAddress: string, gasCost: number) => { @@ -149,12 +133,6 @@ async function newBalanceWatcher(kit: ContractKit, accounts: string[]): Promise< } } -interface Fees { - total: BigNumber - proposer: BigNumber - recipient: BigNumber -} - function assertEqualBN(value: BigNumber, expected: BigNumber) { assert.equal(value.toString(), expected.toString()) } @@ -175,6 +153,7 @@ describe('Transfer tests', function(this: any) { const FromAddress = '0x5409ed021d9299bf6814279a6a1411a7e866a631' // Arbitrary addresses. + const governanceAddress = '0x00000000000000000000000000000000DeaDBeef' const ToAddress = '0xbBae99F0E1EE565404465638d40827b54D343638' const FeeRecipientAddress = '0x4f5f8a3f45d179553e7b95119ce296010f50f6f1' @@ -193,7 +172,7 @@ describe('Transfer tests', function(this: any) { await hooks.restart() kit = newKitFromWeb3(new Web3('http://localhost:8545')) - kit.gasInflactionFactor = 1 + kit.gasInflationFactor = 1 // TODO(mcortesi): magic sleep. without it unlockAccount sometimes fails await sleep(2) @@ -215,6 +194,14 @@ describe('Transfer tests', function(this: any) { } await initAndStartGeth(hooks.gethBinaryPath, fullInstance) + // Install an arbitrary address as the goverance address to act as the infrastructure fund. + // This is chosen instead of full migration for speed and to avoid the need for a governance + // proposal, as all contracts are owned by governance once the migration is complete. + const registry = await kit._web3Contracts.getRegistry() + const tx = registry.methods.setAddressFor(CeloContract.Governance, governanceAddress) + const gas = await tx.estimateGas({ from: validatorAddress }) + await tx.send({ from: validatorAddress, gas }) + // Give the account we will send transfers as sufficient gold and dollars. const startBalance = TransferAmount.times(500) const resDollars = await transferCeloDollars(validatorAddress, FromAddress, startBalance) @@ -237,13 +224,15 @@ describe('Transfer tests', function(this: any) { peers: [await getEnode(8547)], }) - // TODO(asa): Reduce this to speed tests up. - // Give the node time to sync the latest block. - await sleep(10) - // Reset contracts to send RPCs through transferring node. kit.web3.currentProvider = new kit.web3.providers.HttpProvider('http://localhost:8549') + // Give the node time to sync the latest block. + const upstream = await new Web3('http://localhost:8545').eth.getBlock('latest') + while ((await kit.web3.eth.getBlock('latest')).number < upstream.number) { + await sleep(0.5) + } + // Unlock Node account await kit.web3.eth.personal.unlockAccount(FromAddress, '', 1000000) } @@ -297,9 +286,21 @@ describe('Transfer tests', function(this: any) { } } + interface Fees { + total: BigNumber + tip: BigNumber + base: BigNumber + } + + interface GasUsage { + used?: number + expected: number + } + interface TestTxResults { - txOk: boolean - txFees: Fees + ok: boolean + fees: Fees + gas: GasUsage } const runTestTransaction = async ( @@ -310,44 +311,35 @@ describe('Transfer tests', function(this: any) { const minGasPrice = await getGasPriceMinimum(gasCurrency) assert.isAbove(parseInt(minGasPrice, 10), 0) - let txOk = false - let receipt: undefined | TransactionReceipt + let ok = false + let receipt: TransactionReceipt | undefined try { receipt = await txResult.waitReceipt() - txOk = true + ok = true } catch (err) { - txOk = false - } - - let usedGas = expectedGasUsed - if (receipt) { - if (receipt.gasUsed !== expectedGasUsed) { - // tslint:disable-next-line: no-console - console.log('OOPSS: Different Gas', receipt.gasUsed, expectedGasUsed) - } - // assert.equal(receipt.gasUsed, expectedGasUsed, 'Expected gas doesnt match') - usedGas = receipt.gasUsed + ok = false } + const gasVal = receipt ? receipt.gasUsed : expectedGasUsed + assert.isAbove(gasVal, 0) const txHash = await txResult.getHash() const tx = await kit.web3.eth.getTransaction(txHash) const gasPrice = tx.gasPrice assert.isAbove(parseInt(gasPrice, 10), 0) - const expectedTransactionFee = new BigNumber(usedGas).times(gasPrice) - const expectedProposerFeeFraction = 0.5 - const expectedTransactionFeeToProposer = new BigNumber(usedGas) - .times(minGasPrice) - .times(expectedProposerFeeFraction) - const expectedTransactionFeeToRecipient = expectedTransactionFee.minus( - expectedTransactionFeeToProposer - ) - const txFees = { - total: expectedTransactionFee, - proposer: expectedTransactionFeeToProposer, - recipient: expectedTransactionFeeToRecipient, + const txFee = new BigNumber(gasVal).times(gasPrice) + const txFeeBase = new BigNumber(gasVal).times(minGasPrice) + const txFeeTip = txFee.minus(txFeeBase) + + const fees = { + total: txFee, + base: txFeeBase, + tip: txFeeTip, } - - return { txOk, txFees } + const gas = { + used: receipt && receipt.gasUsed, + expected: expectedGasUsed, + } + return { ok, fees, gas } } function testTransferToken({ @@ -375,7 +367,13 @@ describe('Transfer tests', function(this: any) { ? await kit.registry.addressFor(CeloContract.StableToken) : undefined - const accounts = [FromAddress, ToAddress, validatorAddress, FeeRecipientAddress] + const accounts = [ + FromAddress, + ToAddress, + validatorAddress, + FeeRecipientAddress, + governanceAddress, + ] balances = await newBalanceWatcher(kit, accounts) const transferFn = @@ -385,20 +383,31 @@ describe('Transfer tests', function(this: any) { gasCurrency, }) + // Writing to an empty storage location (e.g. an uninitialized ERC20 account) costs 15k extra gas. + if ( + transferToken === CeloContract.StableToken && + balances.initial(ToAddress, transferToken).eq(0) + ) { + expectedGas += 15000 + } + txRes = await runTestTransaction(txResult, expectedGas, gasCurrency) await balances.update() }) if (expectSuccess) { - it(`should succeed`, () => assert.isTrue(txRes.txOk)) + it(`should succeed`, () => assert.isTrue(txRes.ok)) + + it(`should use the expected amount of gas`, () => + assert.equal(txRes.gas.used, txRes.gas.expected)) it(`should increment the receiver's ${transferToken} balance by the transfer amount`, () => assertEqualBN(balances.delta(ToAddress, transferToken), TransferAmount)) if (transferToken === feeToken) { it(`should decrement the sender's ${transferToken} balance by the transfer amount plus the gas fee`, () => { - const expectedBalanceChange = txRes.txFees.total.plus(TransferAmount) + const expectedBalanceChange = txRes.fees.total.plus(TransferAmount) assertEqualBN(balances.delta(FromAddress, transferToken).negated(), expectedBalanceChange) }) } else { @@ -406,13 +415,13 @@ describe('Transfer tests', function(this: any) { assertEqualBN(balances.delta(FromAddress, transferToken).negated(), TransferAmount)) it(`should decrement the sender's ${feeToken} balance by the gas fee`, () => - assertEqualBN(balances.delta(FromAddress, feeToken).negated(), txRes.txFees.total)) + assertEqualBN(balances.delta(FromAddress, feeToken).negated(), txRes.fees.total)) } } else { - it(`should fail`, () => assert.isFalse(txRes.txOk)) + it(`should fail`, () => assert.isFalse(txRes.ok)) it(`should decrement the sender's ${feeToken} balance by the gas fee`, () => - assertEqualBN(balances.delta(FromAddress, feeToken).negated(), txRes.txFees.total)) + assertEqualBN(balances.delta(FromAddress, feeToken).negated(), txRes.fees.total)) it(`should not change the receiver's ${transferToken} balance`, () => { assertEqualBN( @@ -431,13 +440,17 @@ describe('Transfer tests', function(this: any) { } } - it(`should increment the gas fee recipient's ${feeToken} balance by a portion of the gas fee`, () => - assertEqualBN(balances.delta(FeeRecipientAddress, feeToken), txRes.txFees.recipient)) + // TODO(nategraf): Replace gas fee recipient with gateway fee and adjust this check. + it.skip(`should increment the gas fee recipient's ${feeToken} balance by a portion of the gas fee`, () => + assertEqualBN(balances.delta(FeeRecipientAddress, feeToken), new BigNumber(0))) + + it(`should increment the infrastructure fund's ${feeToken} balance by the base portion of the gas fee`, () => + assertEqualBN(balances.delta(governanceAddress, feeToken), txRes.fees.base)) it(`should increment the proposers's ${feeToken} balance by the rest of the gas fee`, () => { assertEqualBN( balances.delta(validatorAddress, feeToken).mod(expectedProposerBlockReward), - txRes.txFees.proposer + txRes.fees.tip ) }) } @@ -450,8 +463,8 @@ describe('Transfer tests', function(this: any) { before(`start geth on sync: ${syncMode}`, () => startSyncNode(syncMode)) describe('Transfer CeloGold >', () => { - const GOLD_TRANSACTION_GAS_COST = 29180 - describe('gasCurrency = CeloGold >', () => { + const GOLD_TRANSACTION_GAS_COST = 30005 + describe('with gasCurrency = CeloGold >', () => { if (syncMode === 'light' || syncMode === 'ultralight') { describe('when running in light/ultralight sync mode', () => { describe('when not explicitly specifying a gas fee recipient', () => @@ -506,7 +519,7 @@ describe('Transfer tests', function(this: any) { describe('when there is no demurrage', () => { describe('when setting a gas amount greater than the amount of gas necessary', () => testTransferToken({ - expectedGas: 163180, + expectedGas: 164005, transferToken: CeloContract.GoldToken, feeToken: CeloContract.StableToken, txOptions: { @@ -550,7 +563,7 @@ describe('Transfer tests', function(this: any) { describe('Transfer CeloDollars', () => { describe('gasCurrency = CeloDollars >', () => { testTransferToken({ - expectedGas: 189456, + expectedGas: 175303, transferToken: CeloContract.StableToken, feeToken: CeloContract.StableToken, txOptions: { @@ -561,7 +574,7 @@ describe('Transfer tests', function(this: any) { describe('gasCurrency = CeloGold >', () => { testTransferToken({ - expectedGas: 40456, + expectedGas: 41303, transferToken: CeloContract.StableToken, feeToken: CeloContract.GoldToken, txOptions: { @@ -596,7 +609,7 @@ describe('Transfer tests', function(this: any) { describe('when there is no demurrage', () => { describe('when setting a gas amount greater than the amount of gas necessary', () => testTransferToken({ - expectedGas: 63180, + expectedGas: 64005, transferToken: CeloContract.GoldToken, feeToken: CeloContract.StableToken, txOptions: { @@ -640,7 +653,7 @@ describe('Transfer tests', function(this: any) { describe('Transfer CeloDollars', () => { describe('gasCurrency = CeloDollars >', () => { testTransferToken({ - expectedGas: 89456, + expectedGas: 75303, transferToken: CeloContract.StableToken, feeToken: CeloContract.StableToken, txOptions: { @@ -654,34 +667,34 @@ describe('Transfer tests', function(this: any) { }) describe('Transfer with Demurrage >', () => { - let inflationManager: InflationManager - for (const syncMode of syncModes) { describe(`${syncMode} Node >`, () => { - const restart = async () => { + let inflationManager: InflationManager + before(`start geth on sync: ${syncMode}`, async () => { await restartWithCleanNodes() - await startSyncNode(syncMode) inflationManager = new InflationManager('http://localhost:8545', validatorAddress) - } + await startSyncNode(syncMode) + }) describe('when there is demurrage of 50% applied', () => { describe('when setting a gas amount greater than the amount of gas necessary', () => { let balances: BalanceWatcher let expectedFees: Fees + let txRes: TestTxResults before(async () => { - await restart() balances = await newBalanceWatcher(kit, [ FromAddress, ToAddress, validatorAddress, FeeRecipientAddress, + governanceAddress, ]) - await inflationManager.changeInflationFactorOnNextTransfer(new BigNumber(2)) + await inflationManager.setInflationRateForNextTransfer(new BigNumber(2)) const stableTokenAddress = await kit.registry.addressFor(CeloContract.StableToken) - const expectedGasUsed = 163180 - const txRes = await runTestTransaction( + const expectedGasUsed = 164005 + txRes = await runTestTransaction( await transferCeloGold(FromAddress, ToAddress, TransferAmount, { gasCurrency: stableTokenAddress, gasFeeRecipient: FeeRecipientAddress, @@ -689,12 +702,16 @@ describe('Transfer tests', function(this: any) { expectedGasUsed, stableTokenAddress ) - assert.isTrue(txRes.txOk) await balances.update() - expectedFees = txRes.txFees + expectedFees = txRes.fees }) + it('should succeed', () => assert.isTrue(txRes.ok)) + + it('should use the expected amount of gas', () => + assert.equal(txRes.gas.used, txRes.gas.expected)) + it("should decrement the sender's Celo Gold balance by the transfer amount", () => { assertEqualBN( balances.delta(FromAddress, CeloContract.GoldToken).negated(), @@ -710,51 +727,51 @@ describe('Transfer tests', function(this: any) { assertEqualBN( balances .initial(FromAddress, CeloContract.StableToken) - .div(2) + .idiv(2) .minus(balances.current(FromAddress, CeloContract.StableToken)), expectedFees.total ) }) - it("should increment the fee receipient's Celo Dollar balance by a portion of the gas fee", () => { + // TODO(nategraf): Replace gas fee recipient with gateway fee and adjust this check. + it.skip("should increment the fee receipient's Celo Dollar balance by a portion of the gas fee", () => { assertEqualBN( balances .current(FeeRecipientAddress, CeloContract.StableToken) - .minus(balances.initial(FeeRecipientAddress, CeloContract.StableToken).div(2)), - - // balances.delta(FeeRecipientAddress, CeloContract.StableToken), - expectedFees.recipient + .minus(balances.initial(FeeRecipientAddress, CeloContract.StableToken).idiv(2)), + new BigNumber(0) ) }) - // TODO mcortesi - // it("should increment the infrastructure fund's Celo Dollar balance by the rest of the gas fee", () => { - // assertEqualBN( - // newBalances[CeloContract.StableToken][governanceAddress] - // .minus(initialBalances[CeloContract.StableToken][governanceAddress]) - // , - // expectedFees.infrastructure - // ) - // }) + it("should halve the infrastructure fund's Celo Dollar balance then increment it by the base portion of the gas fee", () => { + assertEqualBN( + balances + .current(governanceAddress, CeloContract.StableToken) + .minus(balances.initial(governanceAddress, CeloContract.StableToken).idiv(2)), + expectedFees.base + ) + }) }) describe('when setting a gas amount less than the amount of gas necessary but more than the intrinsic gas amount', () => { let balances: BalanceWatcher let expectedFees: Fees + let txRes: TestTxResults + before(async () => { - await restart() balances = await newBalanceWatcher(kit, [ FromAddress, ToAddress, validatorAddress, FeeRecipientAddress, + governanceAddress, ]) - await inflationManager.changeInflationFactorOnNextTransfer(new BigNumber(2)) + await inflationManager.setInflationRateForNextTransfer(new BigNumber(2)) const intrinsicGas = 155000 const gas = intrinsicGas + 1000 - const txRes = await runTestTransaction( + txRes = await runTestTransaction( await transferCeloGold(FromAddress, ToAddress, TransferAmount, { gas, gasCurrency: await kit.registry.addressFor(CeloContract.StableToken), @@ -763,12 +780,13 @@ describe('Transfer tests', function(this: any) { gas, await kit.registry.addressFor(CeloContract.StableToken) ) - assert.isFalse(txRes.txOk) await balances.update() - expectedFees = txRes.txFees + expectedFees = txRes.fees }) + it('should fail', () => assert.isFalse(txRes.ok)) + it("should not change the sender's Celo Gold balance", () => { assertEqualBN(balances.delta(FromAddress, CeloContract.GoldToken), new BigNumber(0)) }) @@ -781,28 +799,37 @@ describe('Transfer tests', function(this: any) { assertEqualBN( balances .initial(FromAddress, CeloContract.StableToken) - .div(2) + .idiv(2) .minus(balances.current(FromAddress, CeloContract.StableToken)), expectedFees.total ) }) - it("should increment the fee recipient's Celo Dollar balance by a portion of the gas fee", () => { + // TODO(nategraf): Replace gas fee recipient with gateway fee and adjust this check. + it.skip("should increment the fee recipient's Celo Dollar balance by a portion of the gas fee", () => { assertEqualBN( balances.delta(FeeRecipientAddress, CeloContract.StableToken), - expectedFees.recipient + new BigNumber(0) ) }) - // TODO(mcortesi) - // it("should increment the proposers Celo Dollar balance by the rest of the gas fee", () => { - // assertEqualBN( - // newBalances[CeloContract.StableToken][governanceAddress] - // .minus(initialBalances[CeloContract.StableToken][governanceAddress]) - // , - // expectedFees.infrastructure - // ) - // }) + it(`should halve the infrastructure fund's Celo Dollar balance then increment it by the base portion of the gas fee`, () => { + assertEqualBN( + balances + .current(governanceAddress, CeloContract.StableToken) + .minus(balances.initial(governanceAddress, CeloContract.StableToken).idiv(2)), + expectedFees.base + ) + }) + + it('should halve the proposers Celo Dollar balance the increment it by the rest of the gas fee', () => { + assertEqualBN( + balances + .current(validatorAddress, CeloContract.StableToken) + .minus(balances.initial(validatorAddress, CeloContract.StableToken).idiv(2)), + expectedFees.tip + ) + }) }) }) }) diff --git a/packages/celotool/src/e2e-tests/utils.ts b/packages/celotool/src/e2e-tests/utils.ts index bfe5e018db0..02941a966bb 100644 --- a/packages/celotool/src/e2e-tests/utils.ts +++ b/packages/celotool/src/e2e-tests/utils.ts @@ -213,7 +213,7 @@ export async function init(gethBinaryPath: string, datadir: string, genesisPath: } export async function importPrivateKey(gethBinaryPath: string, instance: GethInstanceConfig) { - const keyFile = '/tmp/key.txt' + const keyFile = `/${getDatadir(instance)}/key.txt` fs.writeFileSync(keyFile, instance.privateKey) console.info(`geth:${instance.name}: import account`) await execCmdWithExitOnFailure( @@ -243,13 +243,12 @@ async function isPortOpen(host: string, port: number) { } async function waitForPortOpen(host: string, port: number, seconds: number) { - while (seconds > 0) { + const deadline = Date.now() + seconds * 1000 + do { if (await isPortOpen(host, port)) { return true } - seconds -= 1 - await sleep(1) - } + } while (Date.now() < deadline) return false } @@ -317,9 +316,13 @@ export async function startGeth(gethBinaryPath: string, instance: GethInstanceCo } if (validating) { - gethArgs.push('--password=/dev/null', `--unlock=0`) gethArgs.push('--mine', '--minerthreads=10', `--nodekeyhex=${privateKey}`) } + + if (privateKey) { + gethArgs.push('--password=/dev/null', `--unlock=0`) + } + const gethProcess = spawnWithLog(gethBinaryPath, gethArgs, `${datadir}/logs.txt`) instance.pid = gethProcess.pid diff --git a/packages/celotool/src/e2e-tests/verify_ultralight_geth_logs.ts b/packages/celotool/src/e2e-tests/verify_ultralight_geth_logs.ts deleted file mode 100644 index 0d79f7b608b..00000000000 --- a/packages/celotool/src/e2e-tests/verify_ultralight_geth_logs.ts +++ /dev/null @@ -1,109 +0,0 @@ -import GenesisBlockUtils from '@celo/walletkit/lib/src/genesis-block-utils' -import { equal, notEqual } from 'assert' -import * as fs from 'fs' - -// These tests read logs from a client which was running in Ultralight sync mode and verifies that -// only epoch headers are fetched till the height block and all headers are fetched afrerwards. -describe('Ultralight client', () => { - let epoch: number - - before(async () => { - const genesis = JSON.parse(await GenesisBlockUtils.getGenesisBlockAsync(argv.network)) - if (genesis.config.istanbul.epoch) { - epoch = Number(genesis.config.istanbul.epoch) - } else { - throw Error('epoch not found in genesis block') - } - }) - - beforeEach(function(this: any) { - this.timeout(0) - }) - - const argv = require('minimist')(process.argv.slice(2)) - const logfile = argv.gethlogfile - - let origin: number = -1 - let height: number = 0 - const insertedHeaderNumbers: number[] = [] - - console.debug('Reading logs from ' + logfile) - const fileContents = fs.readFileSync(logfile, 'utf8') - - // Fetch origin - const originInfo = fileContents.match('After the check origin is \\d+') - if (originInfo === null) { - throw Error('Origin is null') - } - const arr1 = originInfo[0].split(' ') - origin = parseInt(arr1[arr1.length - 1], 10) - console.debug('origin is ' + origin) - - // Fetch height - const heightInfo = fileContents.match('height is \\d+') - if (heightInfo === null) { - throw Error('Height is null') - } - const arr2 = heightInfo[0].split(' ') - height = parseInt(arr2[arr2.length - 1], 10) - console.debug('Height is ' + height) - - // Fetch all inserted headers - const insertedHeadersInfo = fileContents.match( - new RegExp('Inserted new header.*?number=\\d+', 'g') - ) - if (insertedHeadersInfo === null) { - throw Error('insertedHeadersInfo is null') - } - insertedHeadersInfo.forEach((insertedHeader) => { - const arr3 = insertedHeader.split('=') - const headerNumber = parseInt(arr3[arr3.length - 1], 10) - console.debug('Inserted header is ' + headerNumber) - insertedHeaderNumbers.push(headerNumber) - }) - - it('sync must start from 0', () => { - equal(origin, 0, 'Start header is not zero, it is ' + origin) - }) - - it('latest known header must be non-zero', () => { - notEqual(height, 0, 'Latest known header is zero') - }) - - it('height header must be fetched', () => { - let heightHeaderFetched: boolean = false - for (const headerNumber of insertedHeaderNumbers) { - if (headerNumber === height) { - heightHeaderFetched = true - break - } - } - equal(heightHeaderFetched, true, 'height header ' + height + ' not fetched') - }) - - it('must only download epoch blocks till height', () => { - for (const headerNumber of insertedHeaderNumbers) { - if (headerNumber < height) { - equal(headerNumber % epoch, 0, 'Non-epoch header below height fetched') - } - } - }) - - it('must fetch all headers after height', () => { - for ( - let i = insertedHeaderNumbers.length - 1; - i >= 0 && insertedHeaderNumbers[i] > height; - i++ - ) { - equal( - insertedHeaderNumbers[i] - insertedHeaderNumbers[i - 1], - 1, - 'Header(s) between ' + - insertedHeaderNumbers[i] + - ' and ' + - insertedHeaderNumbers[i - 1] + - ' are missing' - ) - } - }) -}) diff --git a/packages/celotool/src/lib/generate_utils.ts b/packages/celotool/src/lib/generate_utils.ts index 14c6f147a72..c9dbc6d5130 100644 --- a/packages/celotool/src/lib/generate_utils.ts +++ b/packages/celotool/src/lib/generate_utils.ts @@ -158,19 +158,36 @@ export const generateGenesisFromEnv = (enablePetersburg: boolean = true) => { const generateIstanbulExtraData = (validators: Validator[]) => { const istanbulVanity = 32 const blsSignatureVanity = 192 - return ( '0x' + repeat('0', istanbulVanity * 2) + rlp // @ts-ignore .encode([ + // Added validators validators.map((validator) => Buffer.from(validator.address, 'hex')), validators.map((validator) => Buffer.from(validator.blsPublicKey, 'hex')), + // Removed validators new Buffer(0), + // Seal Buffer.from(repeat('0', blsSignatureVanity * 2), 'hex'), - new Buffer(0), - Buffer.from(repeat('0', blsSignatureVanity * 2), 'hex'), + [ + // AggregatedSeal.Bitmap + new Buffer(0), + // AggregatedSeal.Signature + Buffer.from(repeat('0', blsSignatureVanity * 2), 'hex'), + // AggregatedSeal.Round + new Buffer(0), + ], + [ + // ParentAggregatedSeal.Bitmap + new Buffer(0), + // ParentAggregatedSeal.Signature + Buffer.from(repeat('0', blsSignatureVanity * 2), 'hex'), + // ParentAggregatedSeal.Round + new Buffer(0), + ], + // EpochData new Buffer(0), ]) .toString('hex') diff --git a/packages/cli/package.json b/packages/cli/package.json index 9b79107c054..3e7444c9503 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -31,13 +31,14 @@ "test": "TZ=UTC jest --runInBand" }, "dependencies": { - "@celo/contractkit": "0.1.6", + "@celo/contractkit": "0.2.1-dev", "@celo/utils": "^0.1.0", "@oclif/command": "^1", "@oclif/config": "^1", "@oclif/plugin-help": "^2", "bip32": "^1.0.2", "bip39": "^2.5.0", + "bls12377js": "https://github.com/celo-org/bls12377js#cada1105f4a5e4c2ddd239c1874df3bf33144a10", "chalk": "^2.4.2", "cli-table": "^0.3.1", "cli-ux": "^5.3.1", diff --git a/packages/cli/src/commands/account/authorize.test.ts b/packages/cli/src/commands/account/authorize.test.ts index 1127bd6afcf..fada8a235c7 100644 --- a/packages/cli/src/commands/account/authorize.test.ts +++ b/packages/cli/src/commands/account/authorize.test.ts @@ -8,14 +8,32 @@ process.env.NO_SYNCCHECK = 'true' testWithGanache('account:authorize cmd', (web3: Web3) => { test('can authorize account', async () => { const accounts = await web3.eth.getAccounts() - await Register.run(['--from', accounts[0], '--name', 'Chapulin Colorado']) - await Authorize.run(['--from', accounts[0], '--role', 'validation', '--to', accounts[1]]) + await Register.run(['--from', accounts[0]]) + await Authorize.run([ + '--from', + accounts[0], + '--role', + 'validator', + '--signer', + accounts[1], + '--pop', + '0x1b9fca4bbb5bfb1dbe69ef1cddbd9b4202dcb6b134c5170611e1e36ecfa468d7b46c85328d504934fce6c2a1571603a50ae224d2b32685e84d4d1a1eebad8452eb', + ]) }) test('fails if from is not an account', async () => { const accounts = await web3.eth.getAccounts() await expect( - Authorize.run(['--from', accounts[0], '--role', 'validation', '--to', accounts[1]]) + Authorize.run([ + '--from', + accounts[0], + '--role', + 'validator', + '--signer', + accounts[1], + '--pop', + '0x1b9fca4bbb5bfb1dbe69ef1cddbd9b4202dcb6b134c5170611e1e36ecfa468d7b46c85328d504934fce6c2a1571603a50ae224d2b32685e84d4d1a1eebad8452eb', + ]) ).rejects.toThrow() }) }) diff --git a/packages/cli/src/commands/account/authorize.ts b/packages/cli/src/commands/account/authorize.ts index f34239dae7c..4878f7e4013 100644 --- a/packages/cli/src/commands/account/authorize.ts +++ b/packages/cli/src/commands/account/authorize.ts @@ -5,40 +5,35 @@ import { displaySendTx } from '../../utils/cli' import { Flags } from '../../utils/command' export default class Authorize extends BaseCommand { - static description = 'Authorize an attestation, validation or vote signing key' + static description = 'Authorize an attestation, validator, or vote signer' static flags = { ...BaseCommand.flags, from: Flags.address({ required: true }), role: flags.string({ char: 'r', - options: ['vote', 'validation', 'attestation'], + options: ['vote', 'validator', 'attestation'], description: 'Role to delegate', + required: true, }), - to: Flags.address({ required: true }), + pop: flags.string({ + description: 'Proof-of-possession of the signer key', + required: true, + }), + signer: Flags.address({ required: true }), } static args = [] static examples = [ - 'authorize --from 0x5409ED021D9299bf6814279A6A1411A7e866A631 --role vote --to 0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d', + 'authorize --from 0x5409ED021D9299bf6814279A6A1411A7e866A631 --role vote --signer 0x6ecbe1db9ef729cbe972c83fb886247691fb6beb --pop 0x1b9fca4bbb5bfb1dbe69ef1cddbd9b4202dcb6b134c5170611e1e36ecfa468d7b46c85328d504934fce6c2a1571603a50ae224d2b32685e84d4d1a1eebad8452eb', ] async run() { const res = this.parse(Authorize) - - if (!res.flags.role) { - this.error(`Specify role with --role`) - return - } - - if (!res.flags.to) { - this.error(`Specify authorized address with --to`) - return - } - this.kit.defaultAccount = res.flags.from const accounts = await this.kit.contracts.getAccounts() + const sig = accounts.parseSignatureOfAddress(res.flags.from, res.flags.signer, res.flags.pop) await newCheckBuilder(this) .isAccount(res.flags.from) @@ -46,11 +41,11 @@ export default class Authorize extends BaseCommand { let tx: any if (res.flags.role === 'vote') { - tx = await accounts.authorizeVoteSigner(res.flags.from, res.flags.to) - } else if (res.flags.role === 'validation') { - tx = await accounts.authorizeValidationSigner(res.flags.from, res.flags.to) + tx = await accounts.authorizeVoteSigner(res.flags.signer, sig) + } else if (res.flags.role === 'validator') { + tx = await accounts.authorizeValidatorSigner(res.flags.signer, sig) } else if (res.flags.role === 'attestation') { - tx = await accounts.authorizeAttestationSigner(res.flags.from, res.flags.to) + tx = await accounts.authorizeAttestationSigner(res.flags.signer, sig) } else { this.error(`Invalid role provided`) return diff --git a/packages/cli/src/commands/account/claim-account.ts b/packages/cli/src/commands/account/claim-account.ts new file mode 100644 index 00000000000..dd64ed030a7 --- /dev/null +++ b/packages/cli/src/commands/account/claim-account.ts @@ -0,0 +1,27 @@ +import { createAccountClaim } from '@celo/contractkit/lib/identity/claims/account' +import { flags } from '@oclif/command' +import { ClaimCommand } from '../../utils/identity' + +export default class ClaimAccount extends ClaimCommand { + static description = 'Claim another account in a local metadata file' + static flags = { + ...ClaimCommand.flags, + address: flags.string({ + required: true, + description: 'The address of the account you want to claim', + }), + publicKey: flags.string({ + default: undefined, + description: 'The public key of the account if you want others to encrypt messages to you', + }), + } + static args = ClaimCommand.args + static examples = ['claim-account ~/metadata.json --address test.com --from 0x0'] + self = ClaimAccount + async run() { + const res = this.parse(ClaimAccount) + const metadata = this.readMetadata() + await this.addClaim(metadata, createAccountClaim(res.flags.address, res.flags.publicKey)) + this.writeMetadata(metadata) + } +} diff --git a/packages/cli/src/commands/account/claims.test.ts b/packages/cli/src/commands/account/claims.test.ts index 3f993f5841b..6cb91fb8ce9 100644 --- a/packages/cli/src/commands/account/claims.test.ts +++ b/packages/cli/src/commands/account/claims.test.ts @@ -4,17 +4,18 @@ import { readFileSync, writeFileSync } from 'fs' import { tmpdir } from 'os' import Web3 from 'web3' import { testWithGanache } from '../../test-utils/ganache-test' +import ClaimAccount from './claim-account' import ClaimDomain from './claim-domain' import ClaimName from './claim-name' import CreateMetadata from './create-metadata' import RegisterMetadata from './register-metadata' process.env.NO_SYNCCHECK = 'true' -testWithGanache('account:authorize cmd', (web3: Web3) => { +testWithGanache('account metadata cmds', (web3: Web3) => { let account: string - + let accounts: string[] beforeEach(async () => { - const accounts = await web3.eth.getAccounts() + accounts = await web3.eth.getAccounts() account = accounts[0] }) @@ -54,6 +55,16 @@ testWithGanache('account:authorize cmd', (web3: Web3) => { expect(claim).toBeDefined() expect(claim!.domain).toEqual(domain) }) + + test('account:claim-account cmd', async () => { + generateEmptyMetadataFile() + const otherAccount = accounts[1] + await ClaimAccount.run(['--from', account, '--address', otherAccount, emptyFilePath]) + const metadata = readFile() + const claim = metadata.findClaim(ClaimTypes.ACCOUNT) + expect(claim).toBeDefined() + expect(claim!.address).toEqual(otherAccount) + }) }) describe('account:register-metadata cmd', () => { diff --git a/packages/cli/src/commands/account/get-metadata.ts b/packages/cli/src/commands/account/get-metadata.ts index a45d9b66e60..490e10c26ba 100644 --- a/packages/cli/src/commands/account/get-metadata.ts +++ b/packages/cli/src/commands/account/get-metadata.ts @@ -29,9 +29,9 @@ export default class GetMetadata extends BaseCommand { try { const metadata = await IdentityMetadataWrapper.fetchFromURL(metadataURL) console.info('Metadata contains the following claims: \n') - await displayMetadata(metadata) + await displayMetadata(metadata, this.kit) } catch (error) { - console.error('Metadata could not be retrieved from ', metadataURL) + console.error(`Metadata could not be retrieved from ${metadataURL}: ${error.toString()}`) } } } diff --git a/packages/cli/src/commands/account/proof-of-possession.ts b/packages/cli/src/commands/account/proof-of-possession.ts new file mode 100644 index 00000000000..24b0ab206ba --- /dev/null +++ b/packages/cli/src/commands/account/proof-of-possession.ts @@ -0,0 +1,28 @@ +import { serializeSignature } from '@celo/utils/lib/signatureUtils' +import { BaseCommand } from '../../base' +import { printValueMap } from '../../utils/cli' +import { Flags } from '../../utils/command' + +export default class ProofOfPossession extends BaseCommand { + static description = 'Generate proof-of-possession to be used to authorize a signer' + + static flags = { + ...BaseCommand.flags, + signer: Flags.address({ required: true }), + account: Flags.address({ required: true }), + } + + static examples = [ + 'proof-of-possession --account 0x5409ed021d9299bf6814279a6a1411a7e866a631 --signer 0x6ecbe1db9ef729cbe972c83fb886247691fb6beb', + ] + + async run() { + const res = this.parse(ProofOfPossession) + const accounts = await this.kit.contracts.getAccounts() + const pop = await accounts.generateProofOfSigningKeyPossession( + res.flags.account, + res.flags.signer + ) + printValueMap({ signature: serializeSignature(pop) }) + } +} diff --git a/packages/cli/src/commands/account/register.ts b/packages/cli/src/commands/account/register.ts index ea121e9cdfc..9dd445c5838 100644 --- a/packages/cli/src/commands/account/register.ts +++ b/packages/cli/src/commands/account/register.ts @@ -9,13 +9,16 @@ export default class Register extends BaseCommand { static flags = { ...BaseCommand.flags, - name: flags.string({ required: true }), + name: flags.string(), from: Flags.address({ required: true }), } static args = [] - static examples = ['register'] + static examples = [ + 'register --from 0x5409ed021d9299bf6814279a6a1411a7e866a631', + 'register --from 0x5409ed021d9299bf6814279a6a1411a7e866a631 --name test-account', + ] async run() { const res = this.parse(Register) @@ -26,6 +29,8 @@ export default class Register extends BaseCommand { .isNotAccount(res.flags.from) .runChecks() await displaySendTx('register', accounts.createAccount()) - await displaySendTx('setName', accounts.setName(res.flags.name)) + if (res.flags.name) { + await displaySendTx('setName', accounts.setName(res.flags.name)) + } } } diff --git a/packages/cli/src/commands/account/show-metadata.ts b/packages/cli/src/commands/account/show-metadata.ts index bdb502c4e58..8c72e0845ae 100644 --- a/packages/cli/src/commands/account/show-metadata.ts +++ b/packages/cli/src/commands/account/show-metadata.ts @@ -17,6 +17,6 @@ export default class ShowMetadata extends BaseCommand { const res = this.parse(ShowMetadata) const metadata = IdentityMetadataWrapper.fromFile(res.args.file) console.info(`Metadata at ${res.args.file} contains the following claims: \n`) - await displayMetadata(metadata) + await displayMetadata(metadata, this.kit) } } diff --git a/packages/cli/src/commands/account/unlock.ts b/packages/cli/src/commands/account/unlock.ts index 1544757fef0..5036b501811 100644 --- a/packages/cli/src/commands/account/unlock.ts +++ b/packages/cli/src/commands/account/unlock.ts @@ -1,4 +1,5 @@ import { flags } from '@oclif/command' +import { cli } from 'cli-ux' import { BaseCommand } from '../../base' import { Flags } from '../../utils/command' @@ -8,16 +9,18 @@ export default class Unlock extends BaseCommand { static flags = { ...BaseCommand.flags, account: Flags.address({ required: true }), - password: flags.string({ required: true }), + password: flags.string({ required: false }), } - static examples = ['unlock --account 0x5409ed021d9299bf6814279a6a1411a7e866a631 --password 1234'] + static examples = ['unlock --account 0x5409ed021d9299bf6814279a6a1411a7e866a631'] async run() { const res = this.parse(Unlock) // Unlock till geth exits // Source: https://github.com/ethereum/go-ethereum/wiki/Management-APIs#personal_unlockaccount const unlockDurationInMs = 0 - this.web3.eth.personal.unlockAccount(res.flags.account, res.flags.password, unlockDurationInMs) + const password = res.flags.password || (await cli.prompt('Password', { type: 'hide' })) + + this.web3.eth.personal.unlockAccount(res.flags.account, password, unlockDurationInMs) } } diff --git a/packages/cli/src/commands/node/synced.ts b/packages/cli/src/commands/node/synced.ts index f01e991f8f9..c66556f0cce 100644 --- a/packages/cli/src/commands/node/synced.ts +++ b/packages/cli/src/commands/node/synced.ts @@ -1,3 +1,4 @@ +import { flags } from '@oclif/command' import { BaseCommand } from '../../base' import { nodeIsSynced } from '../../utils/helpers' @@ -6,12 +7,22 @@ export default class NodeSynced extends BaseCommand { static flags = { ...BaseCommand.flags, + verbose: flags.boolean({ + description: 'output the full status if syncing', + }), } requireSynced = false async run() { - this.parse(NodeSynced) + const res = this.parse(NodeSynced) + + if (res.flags.verbose) { + const status = await this.web3.eth.isSyncing() + if (typeof status !== 'boolean') { + console.log(status) + } + } console.log(await nodeIsSynced(this.web3)) } } diff --git a/packages/cli/src/commands/validator/publicKey.ts b/packages/cli/src/commands/validator/publicKey.ts deleted file mode 100644 index cfe11ab125a..00000000000 --- a/packages/cli/src/commands/validator/publicKey.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { BaseCommand } from '../../base' -import { newCheckBuilder } from '../../utils/checks' -import { displaySendTx } from '../../utils/cli' -import { Flags } from '../../utils/command' -import { getPubKeyFromAddrAndWeb3 } from '../../utils/helpers' - -export default class ValidatorPublicKey extends BaseCommand { - static description = 'Manage BLS public key data for a validator' - - static flags = { - ...BaseCommand.flags, - from: Flags.address({ required: true, description: "Validator's address" }), - publicKey: Flags.publicKey({ required: true }), - } - - static examples = [ - 'publickey --from 0x47e172F6CfB6c7D01C1574fa3E2Be7CC73269D95 --publicKey 0xc52f3fab06e22a54915a8765c4f6826090cfac5e40282b43844bf1c0df83aaa632e55b67869758f2291d1aabe0ebecc7cbf4236aaa45e3e0cfbf997eda082ae19d3e1d8f49f6b0d8e9a03d80ca07b1d24cf1cc0557bdcc04f5e17a46e35d02d0d411d956dbd5d2d2464eebd7b74ae30005d223780d785d2abc5644fac7ac29fb0e302bdc80c81a5d45018b68b1045068a4b3a4861c93037685fd0d252d7405011220a66a6257562d0c26dabf64485a1d96bad27bb1c0fd6080a75b0ec9f75b50298a2a8e04b02b2688c8104fca61fb00', - ] - async run() { - const res = this.parse(ValidatorPublicKey) - this.kit.defaultAccount = res.flags.from - const validators = await this.kit.contracts.getValidators() - const accounts = await this.kit.contracts.getAccounts() - - await newCheckBuilder(this, res.flags.from) - .isSignerOrAccount() - .canSignValidatorTxs() - .signerAccountIsValidator() - .runChecks() - - await displaySendTx( - 'updatePublicKeysData', - validators.updatePublicKeysData(res.flags.publicKey as any) - ) - - // register encryption key on accounts contract - // TODO: Use a different key data encryption - const pubKey = await getPubKeyFromAddrAndWeb3(res.flags.from, this.web3) - // TODO fix typing - const setKeyTx = accounts.setAccountDataEncryptionKey(pubKey as any) - await displaySendTx('Set encryption key', setKeyTx) - } -} diff --git a/packages/cli/src/commands/validator/register.ts b/packages/cli/src/commands/validator/register.ts index bc50dd33dd6..0753ae27088 100644 --- a/packages/cli/src/commands/validator/register.ts +++ b/packages/cli/src/commands/validator/register.ts @@ -1,8 +1,8 @@ +import { addressToPublicKey } from '@celo/utils/lib/signatureUtils' import { BaseCommand } from '../../base' import { newCheckBuilder } from '../../utils/checks' import { displaySendTx } from '../../utils/cli' import { Flags } from '../../utils/command' -import { getPubKeyFromAddrAndWeb3 } from '../../utils/helpers' export default class ValidatorRegister extends BaseCommand { static description = 'Register a new Validator' @@ -10,12 +10,15 @@ export default class ValidatorRegister extends BaseCommand { static flags = { ...BaseCommand.flags, from: Flags.address({ required: true, description: 'Address for the Validator' }), - publicKey: Flags.publicKey({ required: true }), + ecdsaKey: Flags.ecdsaPublicKey({ required: true }), + blsKey: Flags.blsPublicKey({ required: true }), + blsPop: Flags.blsProofOfPossession({ required: true }), } static examples = [ - 'register --from 0x47e172F6CfB6c7D01C1574fa3E2Be7CC73269D95 --publicKey 0xc52f3fab06e22a54915a8765c4f6826090cfac5e40282b43844bf1c0df83aaa632e55b67869758f2291d1aabe0ebecc7cbf4236aaa45e3e0cfbf997eda082ae19d3e1d8f49f6b0d8e9a03d80ca07b1d24cf1cc0557bdcc04f5e17a46e35d02d0d411d956dbd5d2d2464eebd7b74ae30005d223780d785d2abc5644fac7ac29fb0e302bdc80c81a5d45018b68b1045068a4b3a4861c93037685fd0d252d7405011220a66a6257562d0c26dabf64485a1d96bad27bb1c0fd6080a75b0ec9f75b50298a2a8e04b02b2688c8104fca61fb00', + 'register --from 0x47e172F6CfB6c7D01C1574fa3E2Be7CC73269D95 --ecdsaKey 0xc52f3fab06e22a54915a8765c4f6826090cfac5e40282b43844bf1c0df83aaa632e55b67869758f2291d1aabe0ebecc7cbf4236aaa45e3e0cfbf997eda082ae1 --blsKey 0x9d3e1d8f49f6b0d8e9a03d80ca07b1d24cf1cc0557bdcc04f5e17a46e35d02d0d411d956dbd5d2d2464eebd7b74ae300 --blsPop 0x05d223780d785d2abc5644fac7ac29fb0e302bdc80c81a5d45018b68b1045068a4b3a4861c93037685fd0d252d7405011220a66a6257562d0c26dabf64485a1d96bad27bb1c0fd6080a75b0ec9f75b50298a2a8e04b02b2688c8104fca61fb00', ] + async run() { const res = this.parse(ValidatorRegister) this.kit.defaultAccount = res.flags.from @@ -31,12 +34,16 @@ export default class ValidatorRegister extends BaseCommand { await displaySendTx( 'registerValidator', - validators.registerValidator(res.flags.publicKey as any) + validators.registerValidator( + res.flags.ecdsaKey as any, + res.flags.blsKey as any, + res.flags.blsPop as any + ) ) // register encryption key on accounts contract // TODO: Use a different key data encryption - const pubKey = await getPubKeyFromAddrAndWeb3(res.flags.from, this.web3) + const pubKey = await addressToPublicKey(res.flags.from, this.web3.eth.sign) // TODO fix typing const setKeyTx = accounts.setAccountDataEncryptionKey(pubKey as any) await displaySendTx('Set encryption key', setKeyTx) diff --git a/packages/cli/src/commands/validator/update-bls-public-key.ts b/packages/cli/src/commands/validator/update-bls-public-key.ts new file mode 100644 index 00000000000..70521743dea --- /dev/null +++ b/packages/cli/src/commands/validator/update-bls-public-key.ts @@ -0,0 +1,34 @@ +import { BaseCommand } from '../../base' +import { newCheckBuilder } from '../../utils/checks' +import { displaySendTx } from '../../utils/cli' +import { Flags } from '../../utils/command' + +export default class ValidatorUpdateBlsPublicKey extends BaseCommand { + static description = 'Update BLS key for a validator' + + static flags = { + ...BaseCommand.flags, + from: Flags.address({ required: true, description: "Validator's address" }), + blsKey: Flags.blsPublicKey({ required: true }), + blsPop: Flags.blsProofOfPossession({ required: true }), + } + + static examples = [ + 'update-bls-key --from 0x47e172F6CfB6c7D01C1574fa3E2Be7CC73269D95 --blsKey 0x9d3e1d8f49f6b0d8e9a03d80ca07b1d24cf1cc0557bdcc04f5e17a46e35d02d0d411d956dbd5d2d2464eebd7b74ae300 --blsPop 0x05d223780d785d2abc5644fac7ac29fb0e302bdc80c81a5d45018b68b1045068a4b3a4861c93037685fd0d252d7405011220a66a6257562d0c26dabf64485a1d96bad27bb1c0fd6080a75b0ec9f75b50298a2a8e04b02b2688c8104fca61fb00', + ] + async run() { + const res = this.parse(ValidatorUpdateBlsPublicKey) + this.kit.defaultAccount = res.flags.from + const validators = await this.kit.contracts.getValidators() + await newCheckBuilder(this, res.flags.from) + .isSignerOrAccount() + .canSignValidatorTxs() + .signerAccountIsValidator() + .runChecks() + + await displaySendTx( + 'updateBlsPublicKey', + validators.updateBlsPublicKey(res.flags.blsKey as any, res.flags.blsPop as any) + ) + } +} diff --git a/packages/cli/src/utils/checks.ts b/packages/cli/src/utils/checks.ts index 0e79ab09874..d51d3b21df4 100644 --- a/packages/cli/src/utils/checks.ts +++ b/packages/cli/src/utils/checks.ts @@ -73,7 +73,7 @@ class CheckBuilder { 'Signer can sign Validator Txs', this.withAccounts((lg) => lg - .activeValidationSignerToAccount(this.signer!) + .validatorSignerToAccount(this.signer!) .then(() => true) .catch(() => false) ) diff --git a/packages/cli/src/utils/command.ts b/packages/cli/src/utils/command.ts index c1ad8113bfa..7268010b70c 100644 --- a/packages/cli/src/utils/command.ts +++ b/packages/cli/src/utils/command.ts @@ -1,17 +1,29 @@ +import { BLS_POP_SIZE, BLS_PUBLIC_KEY_SIZE } from '@celo/utils/lib/bls' +import { URL_REGEX } from '@celo/utils/lib/io' import { flags } from '@oclif/command' import { CLIError } from '@oclif/errors' import { IArg, ParseFn } from '@oclif/parser/lib/args' import { pathExistsSync } from 'fs-extra' import Web3 from 'web3' -const parsePublicKey: ParseFn = (input) => { - // Check that the string starts with 0x and has byte length of ecdsa pub key (64 bytes) + bls pub key (48 bytes) + proof of pos (96 bytes) - if (Web3.utils.isHex(input) && input.length === 418 && input.startsWith('0x')) { +const parseBytes = (input: string, length: number, msg: string) => { + // Check that the string starts with 0x and has byte length of `length`. + if (Web3.utils.isHex(input) && input.length === length && input.startsWith('0x')) { return input } else { - throw new CLIError(`${input} is not a public key`) + throw new CLIError(msg) } } + +const parseEcdsaPublicKey: ParseFn = (input) => { + return parseBytes(input, 64, `${input} is not an ECDSA public key`) +} +const parseBlsPublicKey: ParseFn = (input) => { + return parseBytes(input, BLS_PUBLIC_KEY_SIZE, `${input} is not a BLS public key`) +} +const parseBlsProofOfPossession: ParseFn = (input) => { + return parseBytes(input, BLS_POP_SIZE, `${input} is not a BLS proof-of-possession`) +} const parseAddress: ParseFn = (input) => { if (Web3.utils.isAddress(input)) { return input @@ -28,11 +40,6 @@ const parsePath: ParseFn = (input) => { } } -// from http://urlregex.com/ -const URL_REGEX = new RegExp( - /((([A-Za-z]{3,9}:(?:\/\/)?)(?:[\-;:&=\+\$,\w]+@)?[A-Za-z0-9\.\-]+|(?:www\.|[\-;:&=\+\$,\w]+@)[A-Za-z0-9\.\-]+)((?:\/[\+~%\/\.\w\-_]*)?\??(?:[\-\+=&;%@\.\w_]*)#?(?:[\.\!\/\\\w]*))?)/ -) - const parseUrl: ParseFn = (input) => { if (URL_REGEX.test(input)) { return input @@ -58,9 +65,19 @@ export const Flags = { description: 'Account Address', helpValue: '0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d', }), - publicKey: flags.build({ - parse: parsePublicKey, - description: 'Public Key', + ecdsaPublicKey: flags.build({ + parse: parseEcdsaPublicKey, + description: 'ECDSA Public Key', + helpValue: '0x', + }), + blsPublicKey: flags.build({ + parse: parseBlsPublicKey, + description: 'BLS Public Key', + helpValue: '0x', + }), + blsProofOfPossession: flags.build({ + parse: parseBlsProofOfPossession, + description: 'BLS Proof-of-Possession', helpValue: '0x', }), url: flags.build({ diff --git a/packages/cli/src/utils/helpers.ts b/packages/cli/src/utils/helpers.ts index d95c1354c57..4d8c7c0f768 100644 --- a/packages/cli/src/utils/helpers.ts +++ b/packages/cli/src/utils/helpers.ts @@ -1,29 +1,7 @@ -import { eqAddress } from '@celo/utils/lib/address' -import ethjsutil from 'ethereumjs-util' import Web3 from 'web3' import { Block } from 'web3/eth/types' import { failWith } from './cli' -import assert = require('assert') - -export async function getPubKeyFromAddrAndWeb3(addr: string, web3: Web3) { - const msg = new Buffer('dummy_msg_data') - const data = '0x' + msg.toString('hex') - // Note: Eth.sign typing displays incorrect parameter order - const sig = await web3.eth.sign(data, addr) - - const rawsig = ethjsutil.fromRpcSig(sig) - - const prefix = new Buffer('\x19Ethereum Signed Message:\n') - const prefixedMsg = ethjsutil.sha3(Buffer.concat([prefix, new Buffer(String(msg.length)), msg])) - const pubKey = ethjsutil.ecrecover(prefixedMsg, rawsig.v, rawsig.r, rawsig.s) - - const computedAddr = ethjsutil.pubToAddress(pubKey).toString('hex') - assert(eqAddress(computedAddr, addr), 'computed address !== addr') - - return pubKey -} - export async function nodeIsSynced(web3: Web3): Promise { if (process.env.NO_SYNCCHECK) { return true diff --git a/packages/cli/src/utils/identity.ts b/packages/cli/src/utils/identity.ts index b949db6c058..3fe62723fd4 100644 --- a/packages/cli/src/utils/identity.ts +++ b/packages/cli/src/utils/identity.ts @@ -1,3 +1,4 @@ +import { ContractKit } from '@celo/contractkit' import { ClaimTypes, IdentityMetadataWrapper } from '@celo/contractkit/lib/identity' import { Claim, hashOfClaim, verifyClaim } from '@celo/contractkit/lib/identity/claims/claim' import { VERIFIABLE_CLAIM_TYPES } from '@celo/contractkit/lib/identity/claims/types' @@ -78,10 +79,11 @@ export const claimFlags = { export const claimArgs = [Args.file('file', { description: 'Path of the metadata file' })] -export const displayMetadata = async (metadata: IdentityMetadataWrapper) => { +export const displayMetadata = async (metadata: IdentityMetadataWrapper, kit: ContractKit) => { + const accounts = await kit.contracts.getAccounts() const data = await concurrentMap(5, metadata.claims, async (claim) => { const verifiable = VERIFIABLE_CLAIM_TYPES.includes(claim.payload.type) - const status = await verifyClaim(claim, metadata.data.meta.address) + const status = await verifyClaim(claim, metadata.data.meta.address, accounts.getMetadataURL) let extra = '' switch (claim.payload.type) { case ClaimTypes.ATTESTATION_SERVICE_URL: @@ -104,7 +106,7 @@ export const displayMetadata = async (metadata: IdentityMetadataWrapper) => { type: claim.payload.type, extra, verifiable: verifiable ? 'Yes' : 'No', - status: verifiable ? (status ? `Invalid: ${status}` : 'Valid!') : '', + status: verifiable ? (status ? `Invalid: ${status}` : 'Valid!') : 'N/A', createdAt: moment.unix(claim.payload.timestamp).fromNow(), hash: hashOfClaim(claim.payload), } diff --git a/packages/contractkit/package.json b/packages/contractkit/package.json index becf434c741..f20cff2dfd8 100644 --- a/packages/contractkit/package.json +++ b/packages/contractkit/package.json @@ -1,6 +1,6 @@ { "name": "@celo/contractkit", - "version": "0.1.6", + "version": "0.2.1-dev", "description": "Celo's ContractKit to interact with Celo network", "main": "./lib/index.js", "types": "./lib/index.d.ts", @@ -31,13 +31,13 @@ "bignumber.js": "^7.2.0", "cross-fetch": "3.0.4", "debug": "^4.1.1", - "fp-ts": "2.1.1", "eth-lib": "^0.2.8", + "fp-ts": "2.1.1", "io-ts": "2.0.1", "web3": "1.0.0-beta.37", "web3-core-helpers": "1.0.0-beta.37", - "web3-utils": "1.0.0-beta.37", - "web3-eth-abi": "1.0.0-beta.37" + "web3-eth-abi": "1.0.0-beta.37", + "web3-utils": "1.0.0-beta.37" }, "devDependencies": { "@celo/ganache-cli": "git+https://github.com/celo-org/ganache-cli.git#9d77e02", diff --git a/packages/contractkit/src/identity/claims/account.test.ts b/packages/contractkit/src/identity/claims/account.test.ts new file mode 100644 index 00000000000..ca61a57e86c --- /dev/null +++ b/packages/contractkit/src/identity/claims/account.test.ts @@ -0,0 +1,113 @@ +import { privateKeyToAddress, privateKeyToPublicKey } from '@celo/utils/lib/address' +import { NativeSigner } from '@celo/utils/lib/signatureUtils' +import { newKitFromWeb3 } from '../../kit' +import { testWithGanache } from '../../test-utils/ganache-test' +import { ACCOUNT_ADDRESSES, ACCOUNT_PRIVATE_KEYS } from '../../test-utils/ganache.setup' +import { IdentityMetadataWrapper } from '../metadata' +import { createAccountClaim, MetadataURLGetter } from './account' +import { SignedClaim, verifyClaim } from './claim' + +testWithGanache('Account claims', (web3) => { + const kit = newKitFromWeb3(web3) + const address = ACCOUNT_ADDRESSES[0] + const otherAddress = ACCOUNT_ADDRESSES[1] + + it('can make an account claim', async () => { + const metadata = IdentityMetadataWrapper.fromEmpty(address) + await metadata.addClaim( + createAccountClaim(otherAddress), + NativeSigner(kit.web3.eth.sign, address) + ) + }) + + it('can make an account claim with the public key', async () => { + const metadata = IdentityMetadataWrapper.fromEmpty(address) + const otherKey = ACCOUNT_PRIVATE_KEYS[1] + await metadata.addClaim( + createAccountClaim(privateKeyToAddress(otherKey), privateKeyToPublicKey(otherKey)), + NativeSigner(kit.web3.eth.sign, address) + ) + }) + + it("can't claim itself", async () => { + const metadata = IdentityMetadataWrapper.fromEmpty(address) + await expect( + metadata.addClaim(createAccountClaim(address), NativeSigner(kit.web3.eth.sign, address)) + ).rejects.toEqual(new Error("Can't claim self")) + }) + + it('fails to create a claim with in invalid address', () => { + expect(() => { + createAccountClaim('notanaddress') + }).toThrow() + }) + + it('fails when passing a public key that is derived from a different private key', async () => { + const key1 = ACCOUNT_PRIVATE_KEYS[0] + const key2 = ACCOUNT_PRIVATE_KEYS[1] + + expect(() => + createAccountClaim(privateKeyToAddress(key1), privateKeyToPublicKey(key2)) + ).toThrow() + }) + + describe('verifying', () => { + let signedClaim: SignedClaim + let otherMetadata: IdentityMetadataWrapper + let metadataUrlGetter: MetadataURLGetter + + // Mocking static function calls was too difficult, so manually mocking it + const originalFetchFromURLImplementation = IdentityMetadataWrapper.fetchFromURL + + beforeEach(async () => { + otherMetadata = IdentityMetadataWrapper.fromEmpty(otherAddress) + + const myUrl = 'https://www.test.com/' + metadataUrlGetter = (_addr: string) => Promise.resolve(myUrl) + + IdentityMetadataWrapper.fetchFromURL = () => Promise.resolve(otherMetadata) + + const metadata = IdentityMetadataWrapper.fromEmpty(address) + signedClaim = await metadata.addClaim( + createAccountClaim(otherAddress), + NativeSigner(kit.web3.eth.sign, address) + ) + }) + + afterEach(() => { + IdentityMetadataWrapper.fetchFromURL = originalFetchFromURLImplementation + }) + + describe('when the metadata URL of the other account has not been set', () => { + beforeEach(() => { + metadataUrlGetter = (_addr: string) => Promise.resolve('') + }) + + it('indicates that the metadata url could not be retrieved', async () => { + const error = await verifyClaim(signedClaim, address, metadataUrlGetter) + expect(error).toContain('could not be retrieved') + }) + }) + + describe('when the metadata URL is set, but does not contain the address claim', () => { + it('indicates that the metadata does not contain the counter claim', async () => { + const error = await verifyClaim(signedClaim, address, metadataUrlGetter) + expect(error).toContain('did not claim') + }) + }) + + describe('when the other account correctly counter-claims', () => { + beforeEach(async () => { + await otherMetadata.addClaim( + createAccountClaim(address), + NativeSigner(kit.web3.eth.sign, otherAddress) + ) + }) + + it('returns undefined succesfully', async () => { + const error = await verifyClaim(signedClaim, address, metadataUrlGetter) + expect(error).toBeUndefined() + }) + }) + }) +}) diff --git a/packages/contractkit/src/identity/claims/account.ts b/packages/contractkit/src/identity/claims/account.ts new file mode 100644 index 00000000000..4f15d37d1da --- /dev/null +++ b/packages/contractkit/src/identity/claims/account.ts @@ -0,0 +1,89 @@ +import { AddressType, isValidUrl, PublicKeyType } from '@celo/utils/lib/io' +import { pubToAddress, toChecksumAddress } from 'ethereumjs-util' +import { either, isLeft } from 'fp-ts/lib/Either' +import * as t from 'io-ts' +import { Address } from '../../base' +import { IdentityMetadataWrapper } from '../metadata' +import { ClaimTypes, now, TimestampType } from './types' + +// Provide the type minus the validation that the public key and address are derived from the same private key +export const AccountClaimTypeH = t.type({ + type: t.literal(ClaimTypes.ACCOUNT), + timestamp: TimestampType, + address: AddressType, + // io-ts way of defining optional key-value pair + publicKey: t.union([t.undefined, PublicKeyType]), +}) + +export const AccountClaimType = new t.Type( + 'AccountClaimType', + AccountClaimTypeH.is, + (unknownValue, context) => + either.chain(AccountClaimTypeH.validate(unknownValue, context), (claim) => { + if (claim.publicKey === undefined) { + return t.success(claim) + } + const derivedAddress = toChecksumAddress( + '0x' + pubToAddress(Buffer.from(claim.publicKey.slice(2), 'hex'), true).toString('hex') + ) + return derivedAddress === claim.address + ? t.success(claim) + : t.failure(claim, context, 'public key did not match the address in the claim') + }), + (x) => x +) + +export type AccountClaim = t.TypeOf + +export const createAccountClaim = (address: string, publicKey?: string): AccountClaim => { + const claim = { + timestamp: now(), + type: ClaimTypes.ACCOUNT, + address, + publicKey, + } + + const parsedClaim = AccountClaimType.decode(claim) + + if (isLeft(parsedClaim)) { + throw new Error(`A valid claim could not be created`) + } + + return parsedClaim.right +} + +/** + * A function that can asynchronously fetch the metadata URL for an account address + * Should virtually always be Accounts#getMetadataURL + */ +export type MetadataURLGetter = (address: Address) => Promise + +export const verifyAccountClaim = async ( + claim: AccountClaim, + address: string, + metadataURLGetter: MetadataURLGetter +) => { + const metadataURL = await metadataURLGetter(claim.address) + + console.info(JSON.stringify(metadataURL)) + if (!isValidUrl(metadataURL)) { + return `Metadata URL of ${claim.address} could not be retrieved` + } + + let metadata: IdentityMetadataWrapper + try { + metadata = await IdentityMetadataWrapper.fetchFromURL(metadataURL) + } catch (error) { + return `Metadata could not be fetched for ${ + claim.address + } at ${metadataURL}: ${error.toString()}` + } + + const accountClaims = metadata.filterClaims(ClaimTypes.ACCOUNT) + + if (accountClaims.find((x) => x.address === address) === undefined) { + return `${claim.address} did not claim ${address}` + } + + return +} diff --git a/packages/contractkit/src/identity/claims/claim.ts b/packages/contractkit/src/identity/claims/claim.ts index 337b0041348..c3258d4e4dd 100644 --- a/packages/contractkit/src/identity/claims/claim.ts +++ b/packages/contractkit/src/identity/claims/claim.ts @@ -1,7 +1,9 @@ +import { JSONStringType, UrlType } from '@celo/utils/lib/io' import { hashMessage, parseSignature } from '@celo/utils/lib/signatureUtils' import * as t from 'io-ts' +import { AccountClaim, AccountClaimType, MetadataURLGetter, verifyAccountClaim } from './account' import { KeybaseClaim, KeybaseClaimType, verifyKeybaseClaim } from './keybase' -import { ClaimTypes, JSONStringType, now, SignatureType, TimestampType, UrlType } from './types' +import { ClaimTypes, now, SignatureType, TimestampType } from './types' const AttestationServiceURLClaimType = t.type({ type: t.literal(ClaimTypes.ATTESTATION_SERVICE_URL), @@ -23,6 +25,7 @@ const NameClaimType = t.type({ export const ClaimType = t.union([ AttestationServiceURLClaimType, + AccountClaimType, DomainClaimType, KeybaseClaimType, NameClaimType, @@ -41,13 +44,22 @@ export type SignedClaim = t.TypeOf export type AttestationServiceURLClaim = t.TypeOf export type DomainClaim = t.TypeOf export type NameClaim = t.TypeOf -export type Claim = AttestationServiceURLClaim | DomainClaim | KeybaseClaim | NameClaim +export type Claim = + | AttestationServiceURLClaim + | DomainClaim + | KeybaseClaim + | NameClaim + | AccountClaim export type ClaimPayload = K extends typeof ClaimTypes.DOMAIN ? DomainClaim : K extends typeof ClaimTypes.NAME ? NameClaim - : K extends typeof ClaimTypes.KEYBASE ? KeybaseClaim : AttestationServiceURLClaim + : K extends typeof ClaimTypes.KEYBASE + ? KeybaseClaim + : K extends typeof ClaimTypes.ATTESTATION_SERVICE_URL + ? AttestationServiceURLClaim + : AccountClaim export const isOfType = (type: K) => ( data: SignedClaim['payload'] @@ -63,10 +75,24 @@ export function verifySignature(serializedPayload: string, signature: string, si } } -export async function verifyClaim(claim: SignedClaim, address: string) { +/** + * Verifies a claim made by an account + * @param claim The claim to verify + * @param address The address that is making the claim + * @param metadataURLGetter A function that can retrieve the metadata URL for a given account address, + * should be Accounts.getMetadataURL() + * @returns If valid, returns undefined. If invalid or unable to verify, returns a string with the error + */ +export async function verifyClaim( + claim: SignedClaim, + address: string, + metadataURLGetter: MetadataURLGetter +) { switch (claim.payload.type) { case ClaimTypes.KEYBASE: return verifyKeybaseClaim(claim.payload, address) + case ClaimTypes.ACCOUNT: + return verifyAccountClaim(claim.payload, address, metadataURLGetter) default: break } diff --git a/packages/contractkit/src/identity/claims/types.ts b/packages/contractkit/src/identity/claims/types.ts index 525545ab1b0..9f87682785b 100644 --- a/packages/contractkit/src/identity/claims/types.ts +++ b/packages/contractkit/src/identity/claims/types.ts @@ -1,30 +1,13 @@ -import { either } from 'fp-ts/lib/Either' import * as t from 'io-ts' -export const UrlType = t.string export const SignatureType = t.string export const TimestampType = t.number -export const AddressType = t.string - -export const JSONStringType = new t.Type( - 'JSONString', - t.string.is, - (input, context) => - either.chain(t.string.validate(input, context), (stringValue) => { - try { - JSON.parse(stringValue) - return t.success(stringValue) - } catch (error) { - return t.failure(stringValue, context, 'can not be parsed as JSON') - } - }), - String -) export const now = () => Math.round(new Date().getTime() / 1000) export enum ClaimTypes { ATTESTATION_SERVICE_URL = 'ATTESTATION_SERVICE_URL', + ACCOUNT = 'ACCOUNT', DOMAIN = 'DOMAIN', KEYBASE = 'KEYBASE', NAME = 'NAME', @@ -32,4 +15,4 @@ export enum ClaimTypes { TWITTER = 'TWITTER', } -export const VERIFIABLE_CLAIM_TYPES = [ClaimTypes.KEYBASE] +export const VERIFIABLE_CLAIM_TYPES = [ClaimTypes.KEYBASE, ClaimTypes.ACCOUNT] diff --git a/packages/contractkit/src/identity/metadata.ts b/packages/contractkit/src/identity/metadata.ts index c33a5af2c7c..47ce0383192 100644 --- a/packages/contractkit/src/identity/metadata.ts +++ b/packages/contractkit/src/identity/metadata.ts @@ -1,3 +1,4 @@ +import { AddressType } from '@celo/utils/lib/io' import { Signer } from '@celo/utils/lib/signatureUtils' import fetch from 'cross-fetch' import { isLeft } from 'fp-ts/lib/Either' @@ -15,7 +16,7 @@ import { SignedClaimType, verifySignature, } from './claims/claim' -import { AddressType, ClaimTypes } from './claims/types' +import { ClaimTypes } from './claims/types' export { ClaimTypes } from './claims/types' const MetaType = t.type({ @@ -112,6 +113,16 @@ export class IdentityMetadataWrapper { } async addClaim(claim: Claim, signer: Signer) { + switch (claim.type) { + case ClaimTypes.ACCOUNT: + if (claim.address === this.data.meta.address) { + throw new Error("Can't claim self") + } + break + + default: + break + } const signedClaim = await this.signClaim(claim, signer) this.data.claims.push(signedClaim) return signedClaim @@ -121,6 +132,10 @@ export class IdentityMetadataWrapper { return this.data.claims.map((x) => x.payload).find(isOfType(type)) } + filterClaims(type: K): Array> { + return this.data.claims.map((x) => x.payload).filter(isOfType(type)) + } + private signClaim = async (claim: Claim, signer: Signer): Promise => { const messageHash = hashOfClaim(claim) const signature = await signer.sign(messageHash) diff --git a/packages/contractkit/src/kit.test.ts b/packages/contractkit/src/kit.test.ts index 54adbf0354d..f0c0008e065 100644 --- a/packages/contractkit/src/kit.test.ts +++ b/packages/contractkit/src/kit.test.ts @@ -62,7 +62,7 @@ describe('kit.sendTransactionObject()', () => { test('should use inflation factor on gas', async () => { const txo = txoStub() txo.estimateGasMock.mockResolvedValue(1000) - kit.gasInflactionFactor = 2 + kit.gasInflationFactor = 2 await kit.sendTransactionObject(txo) expect(txo.send).toBeCalledWith( expect.objectContaining({ diff --git a/packages/contractkit/src/kit.ts b/packages/contractkit/src/kit.ts index a997ff88e06..d2d2c0e30df 100644 --- a/packages/contractkit/src/kit.ts +++ b/packages/contractkit/src/kit.ts @@ -144,7 +144,7 @@ export class ContractKit { return this.web3.eth.defaultAccount } - set gasInflactionFactor(factor: number) { + set gasInflationFactor(factor: number) { this.config.gasInflationFactor = factor } diff --git a/packages/contractkit/src/wrappers/Accounts.test.ts b/packages/contractkit/src/wrappers/Accounts.test.ts new file mode 100644 index 00000000000..95bd22d4a18 --- /dev/null +++ b/packages/contractkit/src/wrappers/Accounts.test.ts @@ -0,0 +1,74 @@ +import { addressToPublicKey, parseSignature } from '@celo/utils/lib/signatureUtils' +import Web3 from 'web3' +import { newKitFromWeb3 } from '../kit' +import { testWithGanache } from '../test-utils/ganache-test' +import { AccountsWrapper } from './Accounts' +import { LockedGoldWrapper } from './LockedGold' +import { ValidatorsWrapper } from './Validators' + +/* +TEST NOTES: +- In migrations: The only account that has cUSD is accounts[0] +*/ + +const minLockedGoldValue = Web3.utils.toWei('10', 'ether') // 10 gold + +// Random hex strings +const blsPublicKey = + '0x4d23d8cd06f30b1fa7cf368e2f5399ab04bb6846c682f493a98a607d3dfb7e53a712bb79b475c57b0ac2785460f91301' +const blsPoP = + '0x9d3e1d8f49f6b0d8e9a03d80ca07b1d24cf1cc0557bdcc04f5e17a46e35d02d0d411d956dbd5d2d2464eebd7b74ae30005d223780d785d2abc5644fac7ac29fb0e302bdc80c81a5d45018b68b1045068a4b3a4861c93037685fd0d252d740501' + +testWithGanache('Accounts Wrapper', (web3) => { + const kit = newKitFromWeb3(web3) + let accounts: string[] = [] + let accountsInstance: AccountsWrapper + let validators: ValidatorsWrapper + let lockedGold: LockedGoldWrapper + + const registerAccountWithLockedGold = async (account: string) => { + if (!(await accountsInstance.isAccount(account))) { + await accountsInstance.createAccount().sendAndWaitForReceipt({ from: account }) + } + await lockedGold.lock().sendAndWaitForReceipt({ from: account, value: minLockedGoldValue }) + } + + const getParsedSignatureOfAddress = async (address: string, signer: string) => { + const addressHash = web3.utils.soliditySha3({ type: 'address', value: address }) + const signature = await web3.eth.sign(addressHash, signer) + return parseSignature(addressHash, signature, signer) + } + + beforeAll(async () => { + accounts = await web3.eth.getAccounts() + validators = await kit.contracts.getValidators() + lockedGold = await kit.contracts.getLockedGold() + accountsInstance = await kit.contracts.getAccounts() + }) + + const setupValidator = async (validatorAccount: string) => { + const publicKey = await addressToPublicKey(validatorAccount, web3.eth.sign) + await registerAccountWithLockedGold(validatorAccount) + await validators + // @ts-ignore + .registerValidator(publicKey, blsPublicKey, blsPoP) + .sendAndWaitForReceipt({ from: validatorAccount }) + } + + test('SBAT authorize validator key when not a validator', async () => { + const account = accounts[0] + const signer = accounts[1] + await accountsInstance.createAccount() + const sig = await getParsedSignatureOfAddress(account, signer) + await accountsInstance.authorizeValidatorSigner(signer, sig) + }) + + test('SBAT authorize validator key when a validator', async () => { + const account = accounts[0] + const signer = accounts[1] + await accountsInstance.createAccount() + await setupValidator(account) + const sig = await getParsedSignatureOfAddress(account, signer) + await accountsInstance.authorizeValidatorSigner(signer, sig) + }) +}) diff --git a/packages/contractkit/src/wrappers/Accounts.ts b/packages/contractkit/src/wrappers/Accounts.ts index 01afb340612..a995bc664e6 100644 --- a/packages/contractkit/src/wrappers/Accounts.ts +++ b/packages/contractkit/src/wrappers/Accounts.ts @@ -1,3 +1,9 @@ +import { + hashMessageWithPrefix, + parseSignature, + Signature, + signedMessageToPublicKey, +} from '@celo/utils/lib/signatureUtils' import Web3 from 'web3' import { Address } from '../base' import { Accounts } from '../generated/types/Accounts' @@ -9,11 +15,6 @@ import { toTransactionObject, } from '../wrappers/BaseWrapper' -enum SignerRole { - Attestation, - Validation, - Vote, -} /** * Contract for handling deposits needed for voting. */ @@ -40,12 +41,12 @@ export class AccountsWrapper extends BaseWrapper { this.contract.methods.getVoteSigner ) /** - * Returns the validation signere for the specified account. + * Returns the validator signer for the specified account. * @param account The address of the account. * @return The address with which the account can register a validator or group. */ - getValidationSigner: (account: string) => Promise
= proxyCall( - this.contract.methods.getValidationSigner + getValidatorSigner: (account: string) => Promise
= proxyCall( + this.contract.methods.getValidatorSigner ) /** @@ -53,8 +54,8 @@ export class AccountsWrapper extends BaseWrapper { * @param signer Address that is authorized to sign the tx as validator * @return The Account address */ - activeValidationSignerToAccount: (signer: Address) => Promise
= proxyCall( - this.contract.methods.activeValidationSignerToAccount + validatorSignerToAccount: (signer: Address) => Promise
= proxyCall( + this.contract.methods.validatorSignerToAccount ) /** @@ -69,44 +70,98 @@ export class AccountsWrapper extends BaseWrapper { * @param address The address of the account * @return Returns `true` if account exists. Returns `false` otherwise. */ - isSigner: (address: string) => Promise = proxyCall(this.contract.methods.isAuthorized) + isSigner: (address: string) => Promise = proxyCall( + this.contract.methods.isAuthorizedSigner + ) /** * Authorize an attestation signing key on behalf of this account to another address. - * @param account Address of the active account. - * @param attestationSigner The address of the signing key to authorize. + * @param signer The address of the signing key to authorize. + * @param proofOfSigningKeyPossession The account address signed by the signer address. * @return A CeloTransactionObject */ async authorizeAttestationSigner( - account: Address, - attestationSigner: Address + signer: Address, + proofOfSigningKeyPossession: Signature ): Promise> { - return this.authorizeSigner(SignerRole.Attestation, account, attestationSigner) + return toTransactionObject( + this.kit, + this.contract.methods.authorizeAttestationSigner( + signer, + proofOfSigningKeyPossession.v, + proofOfSigningKeyPossession.r, + proofOfSigningKeyPossession.s + ) + ) } /** * Authorizes an address to sign votes on behalf of the account. - * @param account Address of the active account. - * @param voteSigner The address of the vote signing key to authorize. + * @param signer The address of the vote signing key to authorize. + * @param proofOfSigningKeyPossession The account address signed by the signer address. * @return A CeloTransactionObject */ async authorizeVoteSigner( - account: Address, - voteSigner: Address + signer: Address, + proofOfSigningKeyPossession: Signature ): Promise> { - return this.authorizeSigner(SignerRole.Vote, account, voteSigner) + return toTransactionObject( + this.kit, + this.contract.methods.authorizeVoteSigner( + signer, + proofOfSigningKeyPossession.v, + proofOfSigningKeyPossession.r, + proofOfSigningKeyPossession.s + ) + ) } /** * Authorizes an address to sign consensus messages on behalf of the account. - * @param account Address of the active account. - * @param validationSigner The address of the signing key to authorize. + * @param signer The address of the signing key to authorize. + * @param proofOfSigningKeyPossession The account address signed by the signer address. * @return A CeloTransactionObject */ - async authorizeValidationSigner( - account: Address, - validationSigner: Address + async authorizeValidatorSigner( + signer: Address, + proofOfSigningKeyPossession: Signature ): Promise> { - return this.authorizeSigner(SignerRole.Validation, account, validationSigner) + const validators = await this.kit.contracts.getValidators() + const account = this.kit.defaultAccount || (await this.kit.web3.eth.getAccounts())[0] + if (await validators.isValidator(account)) { + const message = this.kit.web3.utils.soliditySha3({ type: 'address', value: account }) + const prefixedMsg = hashMessageWithPrefix(message) + const pubKey = signedMessageToPublicKey( + prefixedMsg, + proofOfSigningKeyPossession.v, + proofOfSigningKeyPossession.r, + proofOfSigningKeyPossession.s + ) + return toTransactionObject( + this.kit, + this.contract.methods.authorizeValidatorSigner( + signer, + pubKey, + proofOfSigningKeyPossession.v, + proofOfSigningKeyPossession.r, + // @ts-ignore Typescript does not support overloading. + proofOfSigningKeyPossession.s + ) + ) + } else { + return toTransactionObject( + this.kit, + this.contract.methods.authorizeValidatorSigner( + signer, + proofOfSigningKeyPossession.v, + proofOfSigningKeyPossession.r, + proofOfSigningKeyPossession.s + ) + ) + } + } + + async generateProofOfSigningKeyPossession(account: Address, signer: Address) { + return this.getParsedSignatureOfAddress(account, signer) } /** @@ -168,25 +223,14 @@ export class AccountsWrapper extends BaseWrapper { */ setWalletAddress = proxySend(this.kit, this.contract.methods.setWalletAddress) - private authorizeFns = { - [SignerRole.Attestation]: this.contract.methods.authorizeAttestationSigner, - [SignerRole.Validation]: this.contract.methods.authorizeValidationSigner, - [SignerRole.Vote]: this.contract.methods.authorizeVoteSigner, - } - - private async authorizeSigner(role: SignerRole, account: Address, signer: Address) { - const sig = await this.getParsedSignatureOfAddress(account, signer) - // TODO(asa): Pass default tx "from" argument. - return toTransactionObject(this.kit, this.authorizeFns[role](signer, sig.v, sig.r, sig.s)) + parseSignatureOfAddress(address: Address, signer: string, signature: string) { + const hash = Web3.utils.soliditySha3({ type: 'address', value: address }) + return parseSignature(hash, signature, signer) } private async getParsedSignatureOfAddress(address: Address, signer: string) { const hash = Web3.utils.soliditySha3({ type: 'address', value: address }) - const signature = (await this.kit.web3.eth.sign(hash, signer)).slice(2) - return { - r: `0x${signature.slice(0, 64)}`, - s: `0x${signature.slice(64, 128)}`, - v: Web3.utils.hexToNumber(signature.slice(128, 130)) + 27, - } + const signature = await this.kit.web3.eth.sign(hash, signer) + return parseSignature(hash, signature, signer) } } diff --git a/packages/contractkit/src/wrappers/GasPriceMinimum.ts b/packages/contractkit/src/wrappers/GasPriceMinimum.ts index 23c4cdfe998..dc97d22726e 100644 --- a/packages/contractkit/src/wrappers/GasPriceMinimum.ts +++ b/packages/contractkit/src/wrappers/GasPriceMinimum.ts @@ -6,7 +6,6 @@ export interface GasPriceMinimumConfig { gasPriceMinimum: BigNumber targetDensity: BigNumber adjustmentSpeed: BigNumber - proposerFraction: BigNumber } /** @@ -28,12 +27,6 @@ export class GasPriceMinimumWrapper extends BaseWrapper { * @returns multiplier that impacts how quickly gas price minimum is adjusted. */ adjustmentSpeed = proxyCall(this.contract.methods.adjustmentSpeed, undefined, toBigNumber) - /** - * Query infrastructure fraction parameter. - * @returns current fraction of the gas price minimum which is sent to - * the infrastructure fund - */ - proposerFraction = proxyCall(this.contract.methods.proposerFraction, undefined, toBigNumber) /** * Returns current configuration parameters. */ @@ -42,13 +35,11 @@ export class GasPriceMinimumWrapper extends BaseWrapper { this.gasPriceMinimum(), this.targetDensity(), this.adjustmentSpeed(), - this.proposerFraction(), ]) return { gasPriceMinimum: res[0], targetDensity: res[1], adjustmentSpeed: res[2], - proposerFraction: res[3], } } } diff --git a/packages/contractkit/src/wrappers/Validators.test.ts b/packages/contractkit/src/wrappers/Validators.test.ts index c8b0dfcde1c..e90aec9cdfe 100644 --- a/packages/contractkit/src/wrappers/Validators.test.ts +++ b/packages/contractkit/src/wrappers/Validators.test.ts @@ -1,3 +1,4 @@ +import { addressToPublicKey } from '@celo/utils/lib/signatureUtils' import BigNumber from 'bignumber.js' import Web3 from 'web3' import { newKitFromWeb3 } from '../kit' @@ -13,15 +14,10 @@ TEST NOTES: const minLockedGoldValue = Web3.utils.toWei('10', 'ether') // 10 gold -// A random 64 byte hex string. -const publicKey = - 'ea0733ad275e2b9e05541341a97ee82678c58932464fad26164657a111a7e37a9fa0300266fb90e2135a1f1512350cb4e985488a88809b14e3cbe415e76e82b2' const blsPublicKey = - '4d23d8cd06f30b1fa7cf368e2f5399ab04bb6846c682f493a98a607d3dfb7e53a712bb79b475c57b0ac2785460f91301' + '0x4d23d8cd06f30b1fa7cf368e2f5399ab04bb6846c682f493a98a607d3dfb7e53a712bb79b475c57b0ac2785460f91301' const blsPoP = - '9d3e1d8f49f6b0d8e9a03d80ca07b1d24cf1cc0557bdcc04f5e17a46e35d02d0d411d956dbd5d2d2464eebd7b74ae30005d223780d785d2abc5644fac7ac29fb0e302bdc80c81a5d45018b68b1045068a4b3a4861c93037685fd0d252d740501' - -const publicKeysData = '0x' + publicKey + blsPublicKey + blsPoP + '0x9d3e1d8f49f6b0d8e9a03d80ca07b1d24cf1cc0557bdcc04f5e17a46e35d02d0d411d956dbd5d2d2464eebd7b74ae30005d223780d785d2abc5644fac7ac29fb0e302bdc80c81a5d45018b68b1045068a4b3a4861c93037685fd0d252d740501' testWithGanache('Validators Wrapper', (web3) => { const kit = newKitFromWeb3(web3) @@ -52,13 +48,12 @@ testWithGanache('Validators Wrapper', (web3) => { } const setupValidator = async (validatorAccount: string) => { + const publicKey = await addressToPublicKey(validatorAccount, web3.eth.sign) await registerAccountWithLockedGold(validatorAccount) // set account1 as the validator await validators - .registerValidator( - // @ts-ignore - publicKeysData - ) + // @ts-ignore + .registerValidator(publicKey, blsPublicKey, blsPoP) .sendAndWaitForReceipt({ from: validatorAccount }) } diff --git a/packages/contractkit/src/wrappers/Validators.ts b/packages/contractkit/src/wrappers/Validators.ts index 548f877d823..0cd4d737365 100644 --- a/packages/contractkit/src/wrappers/Validators.ts +++ b/packages/contractkit/src/wrappers/Validators.ts @@ -18,7 +18,8 @@ import { export interface Validator { address: Address - publicKey: string + ecdsaPublicKey: string + blsPublicKey: string affiliation: string | null score: BigNumber } @@ -57,7 +58,6 @@ export class ValidatorsWrapper extends BaseWrapper { this.contract.methods.updateCommission(toFixed(commission).toFixed()) ) } - updatePublicKeysData = proxySend(this.kit, this.contract.methods.updatePublicKeysData) /** * Returns the Locked Gold requirements for validators. * @returns The Locked Gold requirements for validators. @@ -100,9 +100,26 @@ export class ValidatorsWrapper extends BaseWrapper { async signerToAccount(signerAddress: Address) { const accounts = await this.kit.contracts.getAccounts() - return accounts.activeValidationSignerToAccount(signerAddress) + return accounts.validatorSignerToAccount(signerAddress) } + /** + * Updates a validator's BLS key. + * @param blsPublicKey The BLS public key that the validator is using for consensus, should pass proof + * of possession. 48 bytes. + * @param blsPop The BLS public key proof-of-possession, which consists of a signature on the + * account address. 96 bytes. + * @return True upon success. + */ + updateBlsPublicKey: ( + blsPublicKey: string, + blsPop: string + ) => CeloTransactionObject = proxySend( + this.kit, + this.contract.methods.updateBlsPublicKey, + tupleParser(parseBytes, parseBytes) + ) + /** * Returns whether a particular account has a registered validator. * @param account The account. @@ -147,9 +164,10 @@ export class ValidatorsWrapper extends BaseWrapper { const res = await this.contract.methods.getValidator(address).call() return { address, - publicKey: res[0] as any, - affiliation: res[1], - score: fromFixed(new BigNumber(res[2])), + ecdsaPublicKey: res[0] as any, + blsPublicKey: res[1] as any, + affiliation: res[2], + score: fromFixed(new BigNumber(res[3])), } } @@ -214,20 +232,23 @@ export class ValidatorsWrapper extends BaseWrapper { * Registers a validator unaffiliated with any validator group. * * Fails if the account is already a validator or validator group. - * Fails if the account does not have sufficient weight. * - * @param publicKeysData Comprised of three tightly-packed elements: - * - publicKey - The public key that the validator is using for consensus, should match - * msg.sender. 64 bytes. - * - blsPublicKey - The BLS public key that the validator is using for consensus, should pass - * proof of possession. 48 bytes. - * - blsPoP - The BLS public key proof of possession. 96 bytes. + * @param ecdsaPublicKey The ECDSA public key that the validator is using for consensus, should match + * the validator signer. 64 bytes. + * @param blsPublicKey The BLS public key that the validator is using for consensus, should pass proof + * of possession. 48 bytes. + * @param blsPop The BLS public key proof-of-possession, which consists of a signature on the + * account address. 96 bytes. */ - registerValidator: (publicKeysData: string) => CeloTransactionObject = proxySend( + registerValidator: ( + ecdsaPublicKey: string, + blsPublicKey: string, + blsPop: string + ) => CeloTransactionObject = proxySend( this.kit, this.contract.methods.registerValidator, - tupleParser(parseBytes) + tupleParser(parseBytes, parseBytes, parseBytes) ) /** diff --git a/packages/docs/command-line-interface/account.md b/packages/docs/command-line-interface/account.md index 5967feae68b..c22e3daa1e3 100644 --- a/packages/docs/command-line-interface/account.md +++ b/packages/docs/command-line-interface/account.md @@ -6,20 +6,23 @@ description: Manage your account, send and receive Celo Gold and Celo Dollars ### Authorize -Authorize an attestation, validation or vote signing key +Authorize an attestation, validator, or vote signer ``` USAGE $ celocli account:authorize OPTIONS - -r, --role=vote|validation|attestation Role to delegate - --from=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Account Address - --to=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Account Address + -r, --role=vote|validator|attestation (required) Role to delegate + --from=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Account Address + --pop=pop (required) Proof-of-possession of the signer key + --signer=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Account Address EXAMPLE - authorize --from 0x5409ED021D9299bf6814279A6A1411A7e866A631 --role vote --to - 0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d + authorize --from 0x5409ED021D9299bf6814279A6A1411A7e866A631 --role vote --signer + 0x6ecbe1db9ef729cbe972c83fb886247691fb6beb --pop + 0x1b9fca4bbb5bfb1dbe69ef1cddbd9b4202dcb6b134c5170611e1e36ecfa468d7b46c85328d504934fce6c2a1571603a50ae224d2b32685e84d4d + 1a1eebad8452eb ``` _See code: [packages/cli/src/commands/account/authorize.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/account/authorize.ts)_ @@ -38,6 +41,30 @@ EXAMPLE _See code: [packages/cli/src/commands/account/balance.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/account/balance.ts)_ +### Claim-account + +Claim another account in a local metadata file + +``` +USAGE + $ celocli account:claim-account FILE + +ARGUMENTS + FILE Path of the metadata file + +OPTIONS + --address=address (required) The address of the account you want to claim + --from=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Addess of the account to set metadata for + + --publicKey=publicKey The public key of the account if you want others to encrypt + messages to you + +EXAMPLE + claim-account ~/metadata.json --address test.com --from 0x0 +``` + +_See code: [packages/cli/src/commands/account/claim-account.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/account/claim-account.ts)_ + ### Claim-attestation-service-url Claim the URL of the attestation service in a local metadata file @@ -205,6 +232,25 @@ EXAMPLE _See code: [packages/cli/src/commands/account/new.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/account/new.ts)_ +### Proof-of-possession + +Generate proof-of-possession to be used to authorize a signer + +``` +USAGE + $ celocli account:proof-of-possession + +OPTIONS + --account=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Account Address + --signer=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Account Address + +EXAMPLE + proof-of-possession --account 0x5409ed021d9299bf6814279a6a1411a7e866a631 --signer + 0x6ecbe1db9ef729cbe972c83fb886247691fb6beb +``` + +_See code: [packages/cli/src/commands/account/proof-of-possession.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/account/proof-of-possession.ts)_ + ### Register Register an account @@ -215,10 +261,11 @@ USAGE OPTIONS --from=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Account Address - --name=name (required) + --name=name -EXAMPLE - register +EXAMPLES + register --from 0x5409ed021d9299bf6814279a6a1411a7e866a631 + register --from 0x5409ed021d9299bf6814279a6a1411a7e866a631 --name test-account ``` _See code: [packages/cli/src/commands/account/register.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/account/register.ts)_ @@ -308,10 +355,10 @@ USAGE OPTIONS --account=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Account Address - --password=password (required) + --password=password EXAMPLE - unlock --account 0x5409ed021d9299bf6814279a6a1411a7e866a631 --password 1234 + unlock --account 0x5409ed021d9299bf6814279a6a1411a7e866a631 ``` _See code: [packages/cli/src/commands/account/unlock.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/account/unlock.ts)_ diff --git a/packages/docs/command-line-interface/node.md b/packages/docs/command-line-interface/node.md index 6e5197f61c2..63a938881ca 100644 --- a/packages/docs/command-line-interface/node.md +++ b/packages/docs/command-line-interface/node.md @@ -22,6 +22,9 @@ Check if the node is synced ``` USAGE $ celocli node:synced + +OPTIONS + --verbose output the full status if syncing ``` _See code: [packages/cli/src/commands/node/synced.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/node/synced.ts)_ diff --git a/packages/docs/command-line-interface/validator.md b/packages/docs/command-line-interface/validator.md index 308a79300f3..8bfdea3e57d 100644 --- a/packages/docs/command-line-interface/validator.md +++ b/packages/docs/command-line-interface/validator.md @@ -72,28 +72,6 @@ EXAMPLE _See code: [packages/cli/src/commands/validator/list.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/validator/list.ts)_ -### PublicKey - -Manage BLS public key data for a validator - -``` -USAGE - $ celocli validator:publicKey - -OPTIONS - --from=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Validator's address - --publicKey=0x (required) Public Key - -EXAMPLE - publickey --from 0x47e172F6CfB6c7D01C1574fa3E2Be7CC73269D95 --publicKey - 0xc52f3fab06e22a54915a8765c4f6826090cfac5e40282b43844bf1c0df83aaa632e55b67869758f2291d1aabe0ebecc7cbf4236aaa45e3e0cfbf - 997eda082ae19d3e1d8f49f6b0d8e9a03d80ca07b1d24cf1cc0557bdcc04f5e17a46e35d02d0d411d956dbd5d2d2464eebd7b74ae30005d223780d - 785d2abc5644fac7ac29fb0e302bdc80c81a5d45018b68b1045068a4b3a4861c93037685fd0d252d7405011220a66a6257562d0c26dabf64485a1d - 96bad27bb1c0fd6080a75b0ec9f75b50298a2a8e04b02b2688c8104fca61fb00 -``` - -_See code: [packages/cli/src/commands/validator/publicKey.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/validator/publicKey.ts)_ - ### Register Register a new Validator @@ -103,15 +81,18 @@ USAGE $ celocli validator:register OPTIONS + --blsKey=0x (required) BLS Public Key + --blsPop=0x (required) BLS Proof-of-Possession + --ecdsaKey=0x (required) ECDSA Public Key --from=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Address for the Validator - --publicKey=0x (required) Public Key EXAMPLE - register --from 0x47e172F6CfB6c7D01C1574fa3E2Be7CC73269D95 --publicKey + register --from 0x47e172F6CfB6c7D01C1574fa3E2Be7CC73269D95 --ecdsaKey 0xc52f3fab06e22a54915a8765c4f6826090cfac5e40282b43844bf1c0df83aaa632e55b67869758f2291d1aabe0ebecc7cbf4236aaa45e3e0cfbf - 997eda082ae19d3e1d8f49f6b0d8e9a03d80ca07b1d24cf1cc0557bdcc04f5e17a46e35d02d0d411d956dbd5d2d2464eebd7b74ae30005d223780d - 785d2abc5644fac7ac29fb0e302bdc80c81a5d45018b68b1045068a4b3a4861c93037685fd0d252d7405011220a66a6257562d0c26dabf64485a1d - 96bad27bb1c0fd6080a75b0ec9f75b50298a2a8e04b02b2688c8104fca61fb00 + 997eda082ae1 --blsKey + 0x9d3e1d8f49f6b0d8e9a03d80ca07b1d24cf1cc0557bdcc04f5e17a46e35d02d0d411d956dbd5d2d2464eebd7b74ae300 --blsPop + 0x05d223780d785d2abc5644fac7ac29fb0e302bdc80c81a5d45018b68b1045068a4b3a4861c93037685fd0d252d7405011220a66a6257562d0c26 + dabf64485a1d96bad27bb1c0fd6080a75b0ec9f75b50298a2a8e04b02b2688c8104fca61fb00 ``` _See code: [packages/cli/src/commands/validator/register.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/validator/register.ts)_ @@ -146,3 +127,25 @@ EXAMPLE ``` _See code: [packages/cli/src/commands/validator/show.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/validator/show.ts)_ + +### Update-bls-public-key + +Update BLS key for a validator + +``` +USAGE + $ celocli validator:update-bls-public-key + +OPTIONS + --blsKey=0x (required) BLS Public Key + --blsPop=0x (required) BLS Proof-of-Possession + --from=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Validator's address + +EXAMPLE + update-bls-key --from 0x47e172F6CfB6c7D01C1574fa3E2Be7CC73269D95 --blsKey + 0x9d3e1d8f49f6b0d8e9a03d80ca07b1d24cf1cc0557bdcc04f5e17a46e35d02d0d411d956dbd5d2d2464eebd7b74ae300 --blsPop + 0x05d223780d785d2abc5644fac7ac29fb0e302bdc80c81a5d45018b68b1045068a4b3a4861c93037685fd0d252d7405011220a66a6257562d0c26 + dabf64485a1d96bad27bb1c0fd6080a75b0ec9f75b50298a2a8e04b02b2688c8104fca61fb00 +``` + +_See code: [packages/cli/src/commands/validator/update-bls-public-key.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/validator/update-bls-public-key.ts)_ diff --git a/packages/docs/getting-started/running-a-full-node.md b/packages/docs/getting-started/running-a-full-node.md index cf1c2c91292..95aae11f3b2 100644 --- a/packages/docs/getting-started/running-a-full-node.md +++ b/packages/docs/getting-started/running-a-full-node.md @@ -52,7 +52,7 @@ Save this address to an environment variables, so that you can reference it belo `$ export CELO_ACCOUNT_ADDRESS=` -_Note: this environment variable will only persist while you have this terminal window open. If you want this environment variable to be available in the future, you can add it to your `~/.bash_profile_` +_Note: this environment variable will only persist while you have this terminal window open. If you want this environment variable to be available in the future, you can add it to your `~/.bash_profile_ ## **Configure the node** diff --git a/packages/docs/getting-started/running-a-validator.md b/packages/docs/getting-started/running-a-validator.md index ce3fafb0b91..7763df91187 100644 --- a/packages/docs/getting-started/running-a-validator.md +++ b/packages/docs/getting-started/running-a-validator.md @@ -43,8 +43,6 @@ It is recommended to run the validator node in an environment that facilitates a A note about conventions: The code you'll see on this page is bash commands and their output. -A $ signifies the bash prompt. Everything following it is the command you should run in a terminal. The $ isn't part of the command, so don't copy it. - When you see text in angle brackets <>, replace them and the text inside with your own value of what it refers to. Don't include the <> in the command. {% endhint %} @@ -56,102 +54,125 @@ If you are re-running these instructions, the Celo Docker image may have been up Run: -`$ docker pull us.gcr.io/celo-testnet/celo-node:alfajores` +```bash +docker pull us.gcr.io/celo-testnet/celo-node:alfajores` +``` ## **Create accounts** Create and cd into the directory where you want to store the data and any other files needed to run your node. You can name this whatever you’d like, but here’s a default you can use: -``` -$ mkdir celo-data-dir -$ cd celo-data-dir +```bash +mkdir celo-data-dir +cd celo-data-dir ``` Create two accounts, one for the Validator and one for Validator Group, and get their addresses if you don’t already have them. If you already have your accounts, you can skip this step. To create your two accounts, run this command twice: -`` $ docker run -v `pwd`:/root/.celo --entrypoint /bin/sh -it us.gcr.io/celo-testnet/celo-node:alfajores -c "geth account new" `` +```bash +docker run -v $PWD:/root/.celo --entrypoint /bin/sh -it us.gcr.io/celo-testnet/celo-node:alfajores -c "geth account new" +``` It will prompt you for a passphrase, ask you to confirm it, and then will output your account address: `Address: {}` Let's save these addresses to environment variables, so that you can reference it later (don't include the braces): -``` -$ export CELO_VALIDATOR_GROUP_ADDRESS= -$ export CELO_VALIDATOR_ADDRESS= +```bash +export CELO_VALIDATOR_GROUP_ADDRESS= +export CELO_VALIDATOR_ADDRESS= ``` In order to register the validator later on, generate a "proof of possession" - a signature proving you know your validator's BLS private key. Run this command: -`` $ docker run -v `pwd`:/root/.celo --entrypoint /bin/sh -it us.gcr.io/celo-testnet/celo-node:alfajores -c "geth account proof-of-possession $CELO_VALIDATOR_ADDRESS" `` +```bash +docker run -v $PWD:/root/.celo --entrypoint /bin/sh -it us.gcr.io/celo-testnet/celo-node:alfajores -c "geth account proof-of-possession $CELO_VALIDATOR_ADDRESS" +``` It will prompt you for the passphrase you've chosen for the validator account. Let's save the resulting proof-of-possession to an environment variable: -``` -$ export CELO_VALIDATOR_POP= +```bash +export CELO_VALIDATOR_POP= ``` ## Deploy the validator node Initialize the docker container, building from an image for the network and initializing Celo with the genesis block: -`` $ docker run -v `pwd`:/root/.celo us.gcr.io/celo-testnet/celo-node:alfajores init /celo/genesis.json `` +```bash +docker run -v $PWD:/root/.celo us.gcr.io/celo-testnet/celo-node:alfajores init /celo/genesis.json +``` To participate in consensus, we need to set up our nodekey for our account. We can do so via the following command \(it will prompt you for your passphrase\): -`` $ docker run -v `pwd`:/root/.celo --entrypoint /bin/sh -it us.gcr.io/celo-testnet/celo-node:alfajores -c "geth account set-node-key $CELO_VALIDATOR_ADDRESS" `` +```bash +docker run -v $PWD:/root/.celo --entrypoint /bin/sh -it us.gcr.io/celo-testnet/celo-node:alfajores -c "geth account set-node-key $CELO_VALIDATOR_ADDRESS" +``` In order to allow the node to sync with the network, give it the address of existing nodes in the network: -`` $ docker run -v `pwd`:/root/.celo --entrypoint cp us.gcr.io/celo-testnet/celo-node:alfajores /celo/static-nodes.json /root/.celo/ `` +```bash +docker run -v $PWD:/root/.celo --entrypoint cp us.gcr.io/celo-testnet/celo-node:alfajores /celo/static-nodes.json /root/.celo/ +``` Start up the node: -`` $ docker run -p 127.0.0.1:8545:8545 -p 127.0.0.1:8546:8546 -p 30303:30303 -p 30303:30303/udp -v `pwd`:/root/.celo us.gcr.io/celo-testnet/celo-node:alfajores --verbosity 3 --networkid 44785 --syncmode full --rpc --rpcaddr 0.0.0.0 --rpcapi eth,net,web3,debug,admin,personal --maxpeers 1100 --mine --miner.verificationpool=https://us-central1-celo-testnet-production.cloudfunctions.net/handleVerificationRequestalfajores/v0.1/sms/ --etherbase $CELO_VALIDATOR_ADDRESS `` +```bash +docker run -p 127.0.0.1:8545:8545 -p 127.0.0.1:8546:8546 -p 30303:30303 -p 30303:30303/udp -v $PWD:/root/.celo us.gcr.io/celo-testnet/celo-node:alfajores --verbosity 3 --networkid 44785 --syncmode full --rpc --rpcaddr 0.0.0.0 --rpcapi eth,net,web3,debug,admin,personal --maxpeers 1100 --mine --miner.verificationpool=https://us-central1-celo-testnet-production.cloudfunctions.net/handleVerificationRequestalfajores/v0.1/sms/ --etherbase $CELO_VALIDATOR_ADDRESS +``` {% hint style="danger" %} **Security**: The command line above includes the parameter `--rpcaddr 0.0.0.0` which makes the Celo Blockchain software listen for incoming RPC requests on all network adaptors. Exercise extreme caution in doing this when running outside Docker, as it means that any unlocked accounts and their funds may be accessed from other machines on the Internet. In the context of running a Docker container on your local machine, this together with the `docker -p` flags allows you to make RPC calls from outside the container, i.e from your local host, but not from outside your machine. Read more about [Docker Networking](https://docs.docker.com/network/network-tutorial-standalone/#use-user-defined-bridge-networks) here. {% endhint %} -The `mine` flag does not mean the node starts mining blocks, but rather starts trying to participate in the BFT consensus protocol. It cannot do this until it gets elected -- so next we need to stand for election. +The `mine` flag will tell geth to try participating in the BFT consensus protocol, which is analogous to mining on the Ethereum PoW network. It will not be allowed to validate until it gets elected -- so next we need to stand for election. The `networkid` parameter value of `44785` indicates we are connecting the Alfajores Testnet. +Now you may need to wait for your node to complete a full sync. You can check on the sync status with `celocli node:synced`. Your node will be fully synced when it has downloaded and processed the latest block, which you can see on the [Alfajores Testnet Stats](https://alfajores-ethstats.celo-testnet.org/) page. + ## Obtain and lock up some Celo Gold for staking Visit the [Alfajores Faucet](https://celo.org/build/faucet) to send **both** of your accounts some funds. In a new tab, unlock your accounts so that you can send transactions. This only unlocks the accounts for the lifetime of the validator that's running, so be sure to unlock `$CELO_VALIDATOR_ADDRESS` again if your node gets restarted: -``` -$ celocli account:unlock --account $CELO_VALIDATOR_GROUP_ADDRESS --password -$ celocli account:unlock --account $CELO_VALIDATOR_ADDRESS --password +```bash +# You will be prompted for your password. +celocli account:unlock --account $CELO_VALIDATOR_GROUP_ADDRESS +celocli account:unlock --account $CELO_VALIDATOR_ADDRESS ``` In a new tab, make a locked Gold account for both of your addresses by running the Celo CLI. This will allow you to stake Celo Gold, which is required to register a validator and validator groups: -``` -$ celocli account:register --from $CELO_VALIDATOR_GROUP_ADDRESS --name -$ celocli account:register --from $CELO_VALIDATOR_ADDRESS --name +```bash +celocli account:register --from $CELO_VALIDATOR_GROUP_ADDRESS --name +celocli account:register --from $CELO_VALIDATOR_ADDRESS --name ``` Make a locked Gold commitment for both accounts in order to secure the right to register a validator and validator group. The current requirement is 1 Celo Gold with a notice period of 60 days. If you choose to stake more gold, or a longer notice period, be sure to use those values below: -``` -$ celocli lockedgold:lockup --from $CELO_VALIDATOR_GROUP_ADDRESS --goldAmount 1000000000000000000 --noticePeriod 5184000 -$ celocli lockedgold:lockup --from $CELO_VALIDATOR_ADDRESS --goldAmount 1000000000000000000 --noticePeriod 5184000 +```bash +celocli lockedgold:lockup --from $CELO_VALIDATOR_GROUP_ADDRESS --goldAmount 1000000000000000000 --noticePeriod 5184000 +celocli lockedgold:lockup --from $CELO_VALIDATOR_ADDRESS --goldAmount 1000000000000000000 --noticePeriod 5184000 ``` ## Run for election +In order to be elected as a validator, you will first need to register your group and validator and give them each an an ID, which people will know them by (e.g. `Awesome Validators Inc.` and `Alice's Awesome Validator`). + Register your validator group: -`$ celocli validatorgroup:register --id --from $CELO_VALIDATOR_GROUP_ADDRESS --noticePeriod 5184000` +```bash +celocli validatorgroup:register --id --from $CELO_VALIDATOR_GROUP_ADDRESS --noticePeriod 5184000 +``` Register your validator: -`` $ celocli validator:register --id --from $CELO_VALIDATOR_ADDRESS --noticePeriod 5184000 --publicKey 0x`openssl rand -hex 64`$CELO_VALIDATOR_POP `` +```bash +celocli validator:register --id --from $CELO_VALIDATOR_ADDRESS --noticePeriod 5184000 --publicKey 0x`openssl rand -hex 64`$CELO_VALIDATOR_POP +``` {% hint style="info" %} **Roadmap**: Note that the “publicKey” first part of the public key field is currently ignored, and thus can be set to any 128 character hex value. The rest is used for the BLS public key and proof-of-possession. @@ -159,25 +180,29 @@ Register your validator: Affiliate your validator with your validator group. Note that you will not be a member of this group until the validator group accepts you: -`$ celocli validator:affiliation --set $CELO_VALIDATOR_GROUP_ADDRESS --from $CELO_VALIDATOR_ADDRESS` +```bash +celocli validator:affiliation --set $CELO_VALIDATOR_GROUP_ADDRESS --from $CELO_VALIDATOR_ADDRESS +``` Accept the affiliation: -`$ celocli validatorgroup:member --accept $CELO_VALIDATOR_ADDRESS --from $CELO_VALIDATOR_GROUP_ADDRESS` +```bash +celocli validatorgroup:member --accept $CELO_VALIDATOR_ADDRESS --from $CELO_VALIDATOR_GROUP_ADDRESS +``` Use both accounts to vote for your validator group: -``` -$ celocli validatorgroup:vote --from $CELO_VALIDATOR_ADDRESS --for $CELO_VALIDATOR_GROUP_ADDRESS -$ celocli validatorgroup:vote --from $CELO_VALIDATOR_GROUP_ADDRESS --for $CELO_VALIDATOR_GROUP_ADDRESS +```bash +celocli validatorgroup:vote --from $CELO_VALIDATOR_ADDRESS --for $CELO_VALIDATOR_GROUP_ADDRESS +celocli validatorgroup:vote --from $CELO_VALIDATOR_GROUP_ADDRESS --for $CELO_VALIDATOR_GROUP_ADDRESS ``` You’re all set! Note that elections are finalized at the end of each epoch, roughly once an hour in the Alfajores Testnet. After that hour, if you get elected, your node will start participating BFT consensus and validating blocks. You can inspect the current state of voting by running: -```text -$ celocli validatorgroup:list +```bash +celocli validatorgroup:list ``` If you find your validator still not getting elected you may need to faucet yourself more funds and bond a greater deposit to command more voting weight! diff --git a/packages/mobile/android/gradle.properties b/packages/mobile/android/gradle.properties index fe07d833dce..bcfd0e64c99 100644 --- a/packages/mobile/android/gradle.properties +++ b/packages/mobile/android/gradle.properties @@ -11,11 +11,12 @@ # The setting is particularly useful for tweaking memory settings. # Default value: -Xmx10248m -XX:MaxPermSize=256m # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 +org.gradle.jvmargs=-Xmx4096m -XX:+HeapDumpOnOutOfMemoryError # When configured, Gradle will run in incubating parallel mode. # This option should only be used with decoupled projects. More details, visit # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects -# org.gradle.parallel=true +org.gradle.parallel=true CELO_RELEASE_STORE_FILE=celo-release-key.keystore CELO_RELEASE_KEY_ALIAS=celo-key-alias diff --git a/packages/mobile/locales/en-US/accountScreen10.json b/packages/mobile/locales/en-US/accountScreen10.json index 1e0faa1839d..2d07dbfcdfd 100644 --- a/packages/mobile/locales/en-US/accountScreen10.json +++ b/packages/mobile/locales/en-US/accountScreen10.json @@ -13,6 +13,11 @@ "enableCeloLite": "Enable Celo Lite", "celoLiteDetail": "Celo Lite mode allows you to communicate with the Celo Network through a trusted node. You can always change this mode in app settings.", + "restartModal": { + "header": "Restart To Switch Off Celo Lite", + "body": "To switch Celo Lite on and off repeatedly, you will need to restart the app.", + "restart": "Restart to Switch" + }, "testFaqLink": "Celo Wallet FAQ", "termsOfServiceLink": "Terms of service", "editProfile": "Edit Profile", diff --git a/packages/mobile/locales/en-US/nuxVerification2.json b/packages/mobile/locales/en-US/nuxVerification2.json index d7a8653dd63..2ad59cba206 100644 --- a/packages/mobile/locales/en-US/nuxVerification2.json +++ b/packages/mobile/locales/en-US/nuxVerification2.json @@ -69,8 +69,8 @@ "skip": "Skip for now" }, "failModal": { - "header": "Verification has failed :(", - "body1": "An issue has occured while verifying your phone number. Sorry for the inconvenience!", + "header": "Verification failed", + "body1": "An issue has occured while verifying your phone number.", "body2": "You can skip verification for now and try again later." }, "congratsVerified": "Congratulations you are now verified!" diff --git a/packages/mobile/locales/es-419/accountScreen10.json b/packages/mobile/locales/es-419/accountScreen10.json index 785c8454e71..40ceecc0bb6 100755 --- a/packages/mobile/locales/es-419/accountScreen10.json +++ b/packages/mobile/locales/es-419/accountScreen10.json @@ -13,6 +13,11 @@ "enableCeloLite": "Habilitar Celo Lite", "celoLiteDetail": "El modo Celo Lite te permite comunicarte con la Red Celo a través de un nodo confiable. Puedes cambiar este modo en la configuración de la aplicación.", + "restartModal": { + "header": "Reiniciar para apagar Celo Lite", + "body": "Para encender y apagar Celo Lite repetidamente, deberá reiniciar la aplicación.", + "restart": "Reiniciar para alternar" + }, "testFaqLink": "Las Preguntas Frecuentes del Monedero Celo", "termsOfServiceLink": "Las Condiciones de Servicio", "editProfile": "Editar perfil", diff --git a/packages/mobile/locales/es-419/nuxVerification2.json b/packages/mobile/locales/es-419/nuxVerification2.json index 58daf7fa924..a68e321ebb1 100755 --- a/packages/mobile/locales/es-419/nuxVerification2.json +++ b/packages/mobile/locales/es-419/nuxVerification2.json @@ -11,68 +11,69 @@ "pleaseRetry": "Por favor reinicie la verificación.", "retryVerification": "Reintentar la verificación", "education": { - "header": "~~Verify Your Phone", - "body1": "~~Next, please verify your phone number.", + "header": "Verificar el teléfono", + "body1": "Le solicitamos que verifique su número telefónico.", "body2": - "~~Verifying makes it easier to send and receive with friends. You can also skip this step and return to it later.", - "learnMore": "~~Learn more about phone verification", - "start": "~~Start Verification", - "skip": "~~Skip For Now" + "Esto facilita el envío y la recepción de valores entre amigos. Puede omitir este paso ahora y hacerlo después.", + "learnMore": "Más información sobre la verificación telefónica", + "start": "Comenzar la verificación", + "skip": "Omitir por ahora" }, "learnMore": { - "header": "~~Phone Number Verification", + "header": "Verificación del teléfono", "intro": - "~~Celo Phone number verification works by associating your Celo Wallet with your phone number.", - "section1Header": "~~Do I need to complete this?", + "La verificación del número telefónico de Celo consiste en asociar su billetera de Celo con su teléfono.", + "section1Header": "¿Tengo que completar este paso?", "section1Body": - "~~Verification is not required. However, if you do not verify, others on the Celo network cannot send value to you using your phone number. They must use QR codes or addresses.", - "section2Header": "~~Security and Privacy", + "La verificación no es obligatoria. Cuando la completa, las personas de la red de Celo le podrán enviar valores usando su número telefónico. De lo contrario, deberán usar códigos QR o direcciones.", + "section2Header": "Seguridad y privacidad", "section2Body": - "~~To protect your privacy, only an obfuscated version of your phone number is stored on the Celo blockchain." + "Para proteger su privacidad, solo se guarda una versión oculta de su teléfono en la cadena de bloques de Celo." }, "loading": { - "verifyingNumber": "~~Verifying {{number}}", - "keepOpen": "~~Please keep the app open", - "card1": "~~Verifying your phone number helps your friends find you on the Celo Network.", - "card2": "~~On Celo, you can send money to your anyone using just their phone number.", - "card3": "~~Celo is now requesting three text messages to confirm you own your phone number." + "verifyingNumber": "Verificando {{number}}", + "keepOpen": "Deje abierta la aplicación", + "card1": + "Verificar su número telefónico permite que sus amigos lo encuentren en la red de Celo.", + "card2": "Con Celo, puede enviar dinero a cualquier persona con tan solo el número telefónico.", + "card3": + "Celo ahora envía tres mensajes de texto para confirmar que es dueño de su propia línea telefónica." }, "skipModal": { - "header": "~~Skip Verification?", - "body1": "~~Verifying allows others to send value to your phone number.", + "header": "¿Omitir la verificación?", + "body1": "La verificación permite que otras personas le envíen valores a su número telefónico.", "body2": - "~~Without verification, you can still receive payments but only using Celo addresses or QR codes." + "Sin la verificación, igual podrá recibir pagos, pero solo mediante las direcciones de Celo o códigos QR." }, "interstitial": { - "header": "~~Almost Done", + "header": "Ya casi estamos", "body1": - "~~Your text messages are on the way! Please enter your three codes as you receive them.", - "body2": "~~This may take a minute." + "¡Estamos enviando sus mensajes de texto! Ingrese los tres códigos a medida que los reciba.", + "body2": "Esto puede tardar un poco." }, "input": { - "header": "~~Submit Codes", - "body1": "~~Copy and Paste ", - "body2": "~~the verification codes from your Messages (SMS) app.", - "codeHeader1": "~~First Code", - "codeHeader2": "~~Second Code", - "codeHeader3": "~~Third Code", - "codesMissing": "~~I didn’t receive three codes", - "tip": "~~Typing? Try copying and pasting the code.", + "header": "Enviar códigos", + "body1": "Copiar y pegar ", + "body2": "los códigos de verificación de su aplicación de mensajes de texto (SMS).", + "codeHeader1": "Primer código", + "codeHeader2": "Segundo código", + "codeHeader3": "Tercer código", + "codesMissing": "No recibí tres códigos", + "tip": "¿Está escribiendo el código? ¿Por qué no lo copia y pega?", "codeAccepted": "Aceptado", - "sendingCodes": "~~Sending verification codes..." + "sendingCodes": "Envío de códigos de verificación..." }, "missingCodesModal": { - "header": "~~Missing Codes?", + "header": "¿Falta algún código?", "body": - "~~If you haven’t received all your codes yet after 60 seconds, you can skip verification and try again later.", - "wait": "~~Wait for codes", - "skip": "~~Skip for now" + "Si no recibe todos los códigos después de 60 segundos, puede saltar la verificación y volver a intentarlo más tarde.", + "wait": "Esperar los códigos.", + "skip": "Omitir por ahora" }, "failModal": { - "header": "~~Verification has failed :(", - "body1": - "~~An issue has occured while verifying your phone number. Sorry for the inconvenience!", - "body2": "~~You can skip verification for now and try again later." + "header": "No se pudo verificar", + "body1": "Se produjo un problema al verificar su número de teléfono.", + "body2": "Puede saltar la verificación y volver a intentarlo más tarde." }, "congratsVerified": "¡Felicitaciones se ha verificado tu suario!" } diff --git a/packages/mobile/package.json b/packages/mobile/package.json index 9d6707df455..ec7b799ec48 100644 --- a/packages/mobile/package.json +++ b/packages/mobile/package.json @@ -44,7 +44,7 @@ }, "dependencies": { "@celo/client": "9575a01", - "@celo/contractkit": "^0.1.6", + "@celo/contractkit": "0.2.1-dev", "@celo/react-components": "1.0.0", "@celo/react-native-sms-retriever": "git+https://github.com/celo-org/react-native-sms-retriever#b88e502", "@celo/utils": "^0.1.1", diff --git a/packages/mobile/src/account/CeloLite.tsx b/packages/mobile/src/account/CeloLite.tsx index cfeeb6763e2..7faaa3704e0 100644 --- a/packages/mobile/src/account/CeloLite.tsx +++ b/packages/mobile/src/account/CeloLite.tsx @@ -1,62 +1,139 @@ import SettingsSwitchItem from '@celo/react-components/components/SettingsSwitchItem' +import TextButton from '@celo/react-components/components/TextButton' import colors from '@celo/react-components/styles/colors' import fontStyles from '@celo/react-components/styles/fonts' import * as React from 'react' import { WithNamespaces, withNamespaces } from 'react-i18next' -import { ScrollView, StyleSheet, Text } from 'react-native' +import { ScrollView, StyleSheet, Text, View } from 'react-native' +import Modal from 'react-native-modal' import { connect } from 'react-redux' import i18n, { Namespaces } from 'src/i18n' import { headerWithBackButton } from 'src/navigator/Headers' import { RootState } from 'src/redux/reducers' -import { setZeroSyncMode } from 'src/web3/actions' +import { toggleZeroSyncMode } from 'src/web3/actions' interface StateProps { zeroSyncEnabled: boolean + gethStartedThisSession: boolean } interface DispatchProps { - setZeroSyncMode: typeof setZeroSyncMode + toggleZeroSyncMode: typeof toggleZeroSyncMode } type Props = StateProps & DispatchProps & WithNamespaces const mapDispatchToProps = { - setZeroSyncMode, + toggleZeroSyncMode, } const mapStateToProps = (state: RootState): StateProps => { return { zeroSyncEnabled: state.web3.zeroSyncMode, + gethStartedThisSession: state.web3.gethStartedThisSession, } } -export class CeloLite extends React.Component { +interface State { + modalVisible: boolean +} + +export class CeloLite extends React.Component { static navigationOptions = () => ({ ...headerWithBackButton, headerTitle: i18n.t('accountScreen10:celoLite'), }) + state = { + modalVisible: false, + } + + showModal = () => { + this.setState({ modalVisible: true }) + } + + hideModal = () => { + this.setState({ modalVisible: false }) + } + + onPressToggleWithRestartModal = () => { + this.props.toggleZeroSyncMode(false) + this.hideModal() + } + + handleZeroSyncToggle = (zeroSyncMode: boolean) => { + if (!zeroSyncMode && this.props.gethStartedThisSession) { + // Starting geth a second time this app session which will + // require an app restart, so show restart modal + this.showModal() + } else { + this.props.toggleZeroSyncMode(zeroSyncMode) + } + } + render() { const { zeroSyncEnabled, t } = this.props return ( - + {t('enableCeloLite')} + + + {t('restartModal.header')} + {t('restartModal.body')} + + + {t('global:cancel')} + + + {t('restartModal.restart')} + + + + ) } } -const style = StyleSheet.create({ +const styles = StyleSheet.create({ scrollView: { flex: 1, backgroundColor: colors.background, }, + modalContainer: { + backgroundColor: colors.background, + padding: 20, + marginHorizontal: 10, + borderRadius: 4, + }, + modalHeader: { + ...fontStyles.h2, + ...fontStyles.bold, + marginVertical: 15, + }, + modalButtonsContainer: { + marginTop: 25, + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-evenly', + }, + modalCancelText: { + ...fontStyles.body, + ...fontStyles.semiBold, + paddingRight: 20, + }, + modalSkipText: { + ...fontStyles.body, + ...fontStyles.semiBold, + color: colors.celoGreen, + paddingLeft: 20, + }, }) export default connect( diff --git a/packages/mobile/src/account/__snapshots__/Analytics.test.tsx.snap b/packages/mobile/src/account/__snapshots__/Analytics.test.tsx.snap index 74b75f43f3e..13eab11ea6d 100644 --- a/packages/mobile/src/account/__snapshots__/Analytics.test.tsx.snap +++ b/packages/mobile/src/account/__snapshots__/Analytics.test.tsx.snap @@ -68,7 +68,7 @@ exports[`Analytics renders correctly 1`] = ` onResponderTerminationRequest={[Function]} onStartShouldSetResponder={[Function]} thumbTintColor="#FFFFFF" - trackColorForFalse="#E3E3E5" + trackColorForFalse="#D1D5D8" trackColorForTrue="#42D689" trackTintColor="#42D689" /> diff --git a/packages/mobile/src/account/__snapshots__/CeloLite.test.tsx.snap b/packages/mobile/src/account/__snapshots__/CeloLite.test.tsx.snap index 0a3080730d9..54a43f1b50d 100644 --- a/packages/mobile/src/account/__snapshots__/CeloLite.test.tsx.snap +++ b/packages/mobile/src/account/__snapshots__/CeloLite.test.tsx.snap @@ -68,9 +68,9 @@ exports[`CeloLite renders correctly 1`] = ` onResponderTerminationRequest={[Function]} onStartShouldSetResponder={[Function]} thumbTintColor="#FFFFFF" - trackColorForFalse="#E3E3E5" + trackColorForFalse="#D1D5D8" trackColorForTrue="#42D689" - trackTintColor="#E3E3E5" + trackTintColor="#D1D5D8" /> @@ -88,6 +88,211 @@ exports[`CeloLite renders correctly 1`] = ` + + + + + + restartModal.header + + + restartModal.body + + + + + global:cancel + + + + + restartModal.restart + + + + + + `; diff --git a/packages/mobile/src/app/AppLoading.tsx b/packages/mobile/src/app/AppLoading.tsx index accef6de110..90c19afde5c 100644 --- a/packages/mobile/src/app/AppLoading.tsx +++ b/packages/mobile/src/app/AppLoading.tsx @@ -5,7 +5,7 @@ import { withNamespaces, WithNamespaces } from 'react-i18next' import { StyleSheet, View } from 'react-native' import SafeAreaView from 'react-native-safe-area-view' import { Namespaces } from 'src/i18n' -import { RESTART_APP_I18N_KEY, restartApp } from 'src/utils/AppRestart' +import { deleteChainDataAndRestartApp, RESTART_APP_I18N_KEY } from 'src/utils/AppRestart' const SHOW_RESTART_BUTTON_TIMEOUT = 10000 @@ -46,7 +46,7 @@ export class AppLoading extends React.Component { {this.state.showRestartButton && (