diff --git a/cli/MultiSig.md b/cli/MultiSig.md index f7afa0f..1ddc6fe 100644 --- a/cli/MultiSig.md +++ b/cli/MultiSig.md @@ -1,12 +1,15 @@ ## Prerequisites -- Install golang: https://go.dev/doc/install +- Install golang (v1.22): https://go.dev/doc/install ## Clone and Build ```bash git clone https://gitlab.com/thorchain/thornode.git -cd thornode/cmd/thornode +cd thornode +git checkout develop +git pull +cd cmd/thornode go build --tags cgo,ledger ``` @@ -22,8 +25,12 @@ go build --tags cgo,ledger ``` - Import signer pubkeys: ```bash - ./thornode keys add {person2} --pubkey {pubkey} - ./thornode keys add {person3} --pubkey {pubkey} + ./thornode keys add {person2} --pubkey '{person2_pubkey}' + ./thornode keys add {person3} --pubkey '{person3_pubkey}' + ``` +- View keys to validate: + ```bash + ./thornode keys list ``` - Add multisig key: ```bash @@ -38,15 +45,15 @@ go build --tags cgo,ledger - Person 1 signs: ```bash - ./thornode tx sign --from {person1} --multisig multisig {unsignedTx_epoch-N.json} --chain-id thorchain-mainnet-v1 --node https://daemon.thorchain.shapeshift.com:443/rpc --from ledger --ledger --sign-mode amino-json > signedTx_{person1}.json + ./thornode tx sign --from {person1} --multisig multisig ~/rfox/unsignedTx_epoch-{N}.json --chain-id thorchain-mainnet-v1 --node https://daemon.thorchain.shapeshift.com:443/rpc --ledger --sign-mode amino-json > ~/rfox/signedTx_epoch-{N}_{person1}.json ``` - Person 2 signs: ```bash - ./thornode tx sign --from {person2} --multisig multisig {unsignedTx_epoch-N.json} --chain-id thorchain-mainnet-v1 --node https://daemon.thorchain.shapeshift.com:443/rpc --from ledger --ledger --sign-mode amino-json > signedTx_{person2}.json + ./thornode tx sign --from {person2} --multisig multisig ~/rfox/unsignedTx_epoch-{N}.json --chain-id thorchain-mainnet-v1 --node https://daemon.thorchain.shapeshift.com:443/rpc --ledger --sign-mode amino-json > ~/rfox/signedTx_epoch-{N}_{person2}.json ``` - Multisign: ```bash - ./thornode tx multisign {unsignedTx_epoch-N.json} multisig signedTx_{person1}.json signedTx_{person2}.json --from multisig --chain-id thorchain-mainnet-v1 --node https://daemon.thorchain.shapeshift.com:443/rpc > signedTx_multisig.json + ./thornode tx multisign ~/rfox/unsignedTx_epoch-{N}.json multisig ~/rfox/signedTx_epoch-{N}_{person1}.json ~/rfox/signedTx_epoch-{N}_{person2}.json --from multisig --chain-id thorchain-mainnet-v1 --node https://daemon.thorchain.shapeshift.com:443/rpc > ~/rfox/signedTx_epoch-{N}_multisig.json ``` ## Send Transaction @@ -54,13 +61,14 @@ go build --tags cgo,ledger - Simulate transaction: ```bash - ./thornode tx broadcast signedTx_multisig.json --chain-id thorchain-mainnet-v1 --node https://daemon.thorchain.shapeshift.com:443/rpc --gas auto --dry-run > simulatedTx.json + ./thornode tx broadcast ~/rfox/signedTx_epoch-{N}_multisig.json --chain-id thorchain-mainnet-v1 --node https://daemon.thorchain.shapeshift.com:443/rpc --dry-run > ~/rfox/simulatedTx_epoch-{N}.json ``` - Validate contents of `simulatedTx.json` for accuracy before broadcasting - Broadcast transaction: ```bash - ./thornode tx broadcast signedTx_multisig.json --chain-id thorchain-mainnet-v1 --node https://daemon.thorchain.shapeshift.com:443/rpc --gas auto > tx.json + ./thornode tx broadcast ~/rfox/signedTx_epoch-{N}_multisig.json --chain-id thorchain-mainnet-v1 --node https://daemon.thorchain.shapeshift.com:443/rpc --gas auto > tx.json ``` - - Copy the `txhash` value from `tx.json` to supply to the cli in order to continue + +At this point, the cli should pick up the funding transaction and continue running the distribution from the hot wallet. diff --git a/cli/src/index.ts b/cli/src/index.ts index 594aa9a..e6bfde9 100644 --- a/cli/src/index.ts +++ b/cli/src/index.ts @@ -10,7 +10,7 @@ import { isEpochDistributionStarted } from './file' import { IPFS } from './ipfs' import { error, info, success, warn } from './logging' import { create, recoverKeystore } from './mnemonic' -import { EpochWithHash } from './types' +import { Epoch, RFOXMetadata } from './types' import { Wallet } from './wallet' const processEpoch = async () => { @@ -109,14 +109,15 @@ const processEpoch = async () => { const run = async () => { const ipfs = await IPFS.new() - const epoch = await ipfs.getEpochFromMetadata() + const metadata = await ipfs.getMetadata('process') + const epoch = await ipfs.getEpochFromMetadata(metadata) if (isEpochDistributionStarted(epoch.number)) { const confirmed = await prompts.confirm({ message: 'It looks like you have already started a distribution for this epoch. Do you want to continue? ', }) - if (confirmed) return recover(epoch) + if (confirmed) return recover(metadata) info(`Please move or delete all existing files for epoch-${epoch.number} from ${RFOX_DIR} before re-running.`) warn('This action should never be taken unless you are absolutely sure you know what you are doing!!!') @@ -137,22 +138,24 @@ const run = async () => { const wallet = await Wallet.new(mnemonic) - await processDistribution(epoch, wallet, ipfs) + await processDistribution(metadata, epoch, wallet, ipfs) } -const recover = async (epoch?: EpochWithHash) => { +const recover = async (metadata?: RFOXMetadata) => { const ipfs = await IPFS.new() - if (!epoch) { - epoch = await ipfs.getEpochFromMetadata() + if (!metadata) { + metadata = await ipfs.getMetadata('process') } + const epoch = await ipfs.getEpochFromMetadata(metadata) + const keystoreFile = path.join(RFOX_DIR, `keystore_epoch-${epoch.number}.txt`) const mnemonic = await recoverKeystore(keystoreFile) const wallet = await Wallet.new(mnemonic) - await processDistribution(epoch, wallet, ipfs) + await processDistribution(metadata, epoch, wallet, ipfs) } const update = async () => { @@ -170,18 +173,22 @@ const update = async () => { ) } -const processDistribution = async (epoch: EpochWithHash, wallet: Wallet, ipfs: IPFS) => { - await wallet.fund(epoch) - const processedEpoch = await wallet.distribute(epoch) +const processDistribution = async (metadata: RFOXMetadata, epoch: Epoch, wallet: Wallet, ipfs: IPFS) => { + const epochHash = metadata.ipfsHashByEpoch[epoch.number] - const processedEpochHash = await ipfs.addEpoch(processedEpoch) - const metadata = await ipfs.getMetadata('process') + await wallet.fund(epoch, epochHash) + const processedEpoch = await wallet.distribute(epoch, epochHash) - const hash = await ipfs.updateMetadata(metadata, { + const processedEpochHash = await ipfs.addEpoch({ + ...processedEpoch, + distributionStatus: 'complete', + }) + + const metadataHash = await ipfs.updateMetadata(metadata, { epoch: { number: processedEpoch.number, hash: processedEpochHash }, }) - if (!hash) return + if (!metadataHash) return success(`rFOX reward distribution for Epoch #${processedEpoch.number} has been completed!`) diff --git a/cli/src/ipfs.ts b/cli/src/ipfs.ts index 9c86991..e62890f 100644 --- a/cli/src/ipfs.ts +++ b/cli/src/ipfs.ts @@ -3,7 +3,7 @@ import PinataClient from '@pinata/sdk' import axios from 'axios' import BigNumber from 'bignumber.js' import { error, info } from './logging' -import { Epoch, EpochWithHash, RFOXMetadata, RewardDistribution } from './types' +import { Epoch, RFOXMetadata, RewardDistribution } from './types' import { MONTHS } from './constants' const PINATA_API_KEY = process.env['PINATA_API_KEY'] @@ -99,7 +99,7 @@ export class IPFS { } } - async getEpoch(hash?: string): Promise { + async getEpoch(hash?: string): Promise { if (!hash) { hash = await prompts.input({ message: 'What is the IPFS hash for the rFOX epoch you want to process? ', @@ -127,7 +127,7 @@ export class IPFS { `Running ${month} rFOX reward distribution for Epoch #${data.number}:\n - Total Rewards: ${totalRewards} RUNE\n - Total Addresses: ${totalAddresses}`, ) - return { ...data, hash } + return data } else { error(`The contents of IPFS hash (${hash}) are not valid epoch contents, exiting.`) process.exit(1) @@ -153,18 +153,6 @@ export class IPFS { metadata.epochEndTimestamp = overrides.metadata.epochEndTimestamp if (overrides.epoch) { - const hash = metadata.ipfsHashByEpoch[overrides.epoch.number] - - if (hash) { - info(`The metadata already contains an IPFS hash for this epoch: ${hash}`) - - const confirmed = await prompts.confirm({ - message: `Do you want to update the metadata with the new IPFS hash: ${overrides.epoch.hash}?`, - }) - - if (!confirmed) return - } - metadata.ipfsHashByEpoch[overrides.epoch.number] = overrides.epoch.hash const { IpfsHash } = await this.client.pinJSONToIPFS(metadata, { @@ -308,9 +296,7 @@ export class IPFS { } } - async getEpochFromMetadata(): Promise { - const metadata = await this.getMetadata('process') - + async getEpochFromMetadata(metadata: RFOXMetadata): Promise { const hash = metadata.ipfsHashByEpoch[metadata.epoch - 1] if (!hash) { diff --git a/cli/src/types.ts b/cli/src/types.ts index 45aecda..9ddcd72 100644 --- a/cli/src/types.ts +++ b/cli/src/types.ts @@ -88,5 +88,3 @@ export type Epoch = { /** A record of staking address to reward distribution for this epoch */ distributionsByStakingAddress: Record } - -export type EpochWithHash = Epoch & { hash: string } diff --git a/cli/src/wallet.ts b/cli/src/wallet.ts index 6777a4d..a04aea1 100644 --- a/cli/src/wallet.ts +++ b/cli/src/wallet.ts @@ -8,7 +8,7 @@ import ora, { Ora } from 'ora' import { RFOX_DIR } from './constants' import { read, write } from './file' import { error, info, success } from './logging' -import { EpochWithHash } from './types' +import { Epoch } from './types' const BIP32_PATH = `m/44'/931'/0'/0/0` const SHAPESHIFT_MULTISIG_ADDRESS = 'thor1xmaggkcln5m5fnha2780xrdrulmplvfrz6wj3l' @@ -70,7 +70,7 @@ export class Wallet { } } - private async buildFundingTransaction(amount: string, epoch: EpochWithHash) { + private async buildFundingTransaction(amount: string, epoch: Epoch, hash: string) { const { address } = await this.getAddress() return { @@ -88,7 +88,7 @@ export class Wallet { ], }, ], - memo: `Fund rFOX rewards distribution - Epoch #${epoch.number} (IPFS Hash: ${epoch.hash})`, + memo: `Fund rFOX rewards distribution - Epoch #${epoch.number} (IPFS Hash: ${hash})`, timeout_height: '0', extension_options: [], non_critical_extension_options: [], @@ -106,7 +106,7 @@ export class Wallet { } } - async fund(epoch: EpochWithHash) { + async fund(epoch: Epoch, epochHash: string) { const { address } = await this.getAddress() const distributions = Object.values(epoch.distributionsByStakingAddress) @@ -152,7 +152,7 @@ export class Wallet { if (await isFunded()) return - const unsignedTx = await this.buildFundingTransaction(totalAmount, epoch) + const unsignedTx = await this.buildFundingTransaction(totalAmount, epoch, epochHash) const unsignedTxFile = path.join(RFOX_DIR, `unsignedTx_epoch-${epoch.number}.json`) write(unsignedTxFile, JSON.stringify(unsignedTx, null, 2)) @@ -173,7 +173,7 @@ export class Wallet { })() } - private async signTransactions(epoch: EpochWithHash): Promise { + private async signTransactions(epoch: Epoch, epochHash: string): Promise { const txsFile = path.join(RFOX_DIR, `txs_epoch-${epoch.number}.json`) const txs = read(txsFile) @@ -230,7 +230,7 @@ export class Wallet { amount: [], gas: '0', }, - memo: `rFOX reward (Staking Address: ${stakingAddress}) - Epoch #${epoch.number} (IPFS Hash: ${epoch.hash})`, + memo: `rFOX reward (Staking Address: ${stakingAddress}) - Epoch #${epoch.number} (IPFS Hash: ${epochHash})`, signatures: [], }, } @@ -273,7 +273,7 @@ export class Wallet { return txsByStakingAddress } - async broadcastTransactions(epoch: EpochWithHash, txsByStakingAddress: TxsByStakingAddress): Promise { + async broadcastTransactions(epoch: Epoch, txsByStakingAddress: TxsByStakingAddress): Promise { const totalTxs = Object.values(epoch.distributionsByStakingAddress).length const spinner = ora(`Broadcasting ${totalTxs} transactions...`).start() @@ -330,8 +330,8 @@ export class Wallet { return epoch } - async distribute(epoch: EpochWithHash): Promise { - const txsByStakingAddress = await this.signTransactions(epoch) + async distribute(epoch: Epoch, epochHash: string): Promise { + const txsByStakingAddress = await this.signTransactions(epoch, epochHash) return this.broadcastTransactions(epoch, txsByStakingAddress) } }