Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Multisig batching #406

Closed
wants to merge 46 commits into from
Closed
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
eaa9d66
gauntlet: multisig: Allow batch approving transactions
archseer Oct 7, 2022
de66a46
Merge proposePayees into proposeConfig
archseer Nov 17, 2022
350db25
Merge proposeOffchainConfig into proposeConfig
archseer Nov 17, 2022
e6c8c94
Merge in finalizeProposal
archseer Nov 17, 2022
e9e87b7
Merge in createProposal
archseer Nov 17, 2022
c4628b6
fix: if signing without payer, need to use partialSign
archseer Nov 18, 2022
a7b133c
nix: Fix include libudev in library path
archseer Nov 18, 2022
b3807b7
anchor_shell: Expose RPC ports on the host
archseer Nov 18, 2022
0b39301
nix: Update flake dependencies
archseer Nov 18, 2022
191ba75
gauntlet: Update typescript
archseer Nov 18, 2022
69a59a3
gauntlet: More reliable tx submission and confirmation
archseer Nov 18, 2022
cefe93b
gauntlet: Simplify local env, preload programs with canonical program…
archseer Nov 18, 2022
98377f5
Update flake, node and rust versions
archseer Oct 26, 2022
d1e831b
Use step identifiers, don't resolve inputs too early
archseer Nov 18, 2022
21f1ddf
gauntlet: setup.dev.flow: Pass link address into subsequent steps
archseer Nov 18, 2022
e7fc822
Execute createProposal + SystemProgram.createAccount in same tx
archseer Nov 18, 2022
c916a85
Restructure proposeConfig simulation/execution
archseer Nov 18, 2022
65e673a
createProposal must be signed by the proposal account too
archseer Nov 18, 2022
7bd7bd8
wip
archseer Nov 21, 2022
2d3b207
fix: Confirmation level only worked with `confirmed`
archseer Nov 21, 2022
3a0fb85
proposeConfig steps unfortunately need to be fully sequential
archseer Nov 21, 2022
4f00fd6
Create payee accounts before using them, change create_account to sup…
archseer Nov 21, 2022
60b6fbe
yarn format (simulateTransaction)
archseer Nov 21, 2022
2bee824
fix: need to pass in proposalId as part of input too
archseer Nov 21, 2022
1e58980
fix: The flow needs to pass through correct secret to approveProposal
archseer Nov 21, 2022
6c3b2fa
nix: Fully bump go (instead of just GOPATH)
archseer Nov 21, 2022
8e06e41
Remove sendTx
archseer Nov 22, 2022
c7db9ef
Don't call sendAndConfirm directly
archseer Nov 22, 2022
cf2aefd
Stop manulaly calling simulateTx everywhere...
archseer Nov 22, 2022
5f2dfeb
Update us to web3.js 1.64.0 and v0 transactions
archseer Nov 22, 2022
b4cf8de
Stop extracting transmissions address from raw tx
archseer Nov 22, 2022
57126d2
minor cleanup
archseer Nov 22, 2022
77815b1
Use confirmLevel of 'confirmed'
archseer Nov 22, 2022
117e1df
Bump gauntlet-core with the fix
archseer Nov 22, 2022
e4abd19
Fix approveProposal, correctly passing in the secret
archseer Nov 22, 2022
e564886
Add missing await
archseer Nov 22, 2022
b47c491
proposeConfig: figure out what the remaining steps are if resuming
archseer Nov 24, 2022
b2aab8c
fix: randomSecret output was undefined
archseer Nov 25, 2022
9b6ec84
Fix jest tests (https://github.com/LedgerHQ/ledger-live/issues/763)
archseer Nov 25, 2022
e6bdee3
gauntlet: store.setWriter doesn't need to load both programs
archseer Nov 25, 2022
551c510
fix: multisig:approve needed to actually execute the tx
archseer Nov 29, 2022
41659cb
solana/web3.js 1.67 is now more reliable with confirmations
archseer Nov 29, 2022
b9e022f
We already simulate the tx beforehand
archseer Nov 30, 2022
783c334
Use legacy transactions with Ledger until supported upstream
archseer Nov 30, 2022
036a247
fix: Ledger codepath needs to set blockhash & feePayer
archseer Dec 7, 2022
b9c1353
wip
archseer Jan 27, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .tool-versions
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
nodejs 14.19.1
rust 1.59.0
nodejs 18.6.0
rust 1.64.0
golang 1.19.3
golangci-lint 1.45.2
pulumi 3.40.1
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ projectserum_version:
@echo "${PROJECT_SERUM_VERSION}"

anchor_shell:
docker run --rm -it -v $(shell pwd):/workdir --entrypoint bash ${PROJECT_SERUM_IMAGE}
docker run --rm -it -v $(shell pwd):/workdir -p 8899:8899 -p 8900:8900 --entrypoint bash ${PROJECT_SERUM_IMAGE}

build_js:
cd gauntlet && yarn install --frozen-lockfile && yarn bundle
Expand Down
30 changes: 15 additions & 15 deletions flake.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion gauntlet/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,6 @@
"secp256k1": "^4.0.2",
"ts-jest": "^26.4.3",
"ts-node": "^8.3.0",
"typescript": "4.3.5"
"typescript": "4.9.3"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { SolanaCommand, TransactionResponse } from '@chainlink/gauntlet-solana'
import { PublicKey } from '@solana/web3.js'
import { Result } from '@chainlink/gauntlet-core'
import { logger } from '@chainlink/gauntlet-core/dist/utils'
import { CONTRACT_LIST, getContract } from '../lib/contracts'

export default class Approve extends SolanaCommand {
static id = 'serum_multisig:approve'
Copy link
Member

Choose a reason for hiding this comment

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

When is this going to be used?

Copy link
Member

Choose a reason for hiding this comment

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

Reopening this, the whole purpose of the multisig command is validation. Here the users will approve proposals they don't know what they do.

If approval batching is required, it should be built within the multisig wrapper, so Gauntlet can make proper validations and inform the users about the actions they'll do.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is a quick workaround from an immediate need: without batching we need a ledger approval per feed per owner. This at least reduces it to a command (per 6-7 feeds) per owner.

It would be great to fix this in the multisig wrapper, but it's a larger job since we'd need to provide inputs for multiple invocations, verify them then pack that into a single tx. This is slightly tricky UX-wise too, since each command can take additional flags, for example --secret per ocr2:accept_proposal:multisig.

I think the multisig wrapper needs a general re-work to stop being so stateful too. It's very easy to accidentally call the code without a proposal (or worse yet, the code is faulty and fetches no proposal), which ends up creating new proposals rather than approving.

We've already had that happen on starknet: proposalId was missing in the implementation but the command was running successfully (and tests passing), creating new proposals over and over again: smartcontractkit/chainlink-starknet@dacaae3#diff-8257597e02534a1f29c2fcb789a25491779bd7ac33359412616550f5c422d83cR208

static category = CONTRACT_LIST.MULTISIG

static examples = ['yarn gauntlet serum_multisig:approve --network=local [IDS...]']

constructor(flags, args) {
super(flags, args)
}
makeRawTransaction = async (signer: PublicKey) => {
const multisigAddress = new PublicKey(process.env.MULTISIG_ADDRESS || '')
const multisig = getContract(CONTRACT_LIST.MULTISIG)
const address = multisig.programId.toString()
const program = this.loadProgram(multisig.idl, address)

logger.info(`Approving transactions: ${this.args}`)

// map ids over this
const ixs = await Promise.all(
this.args.map((tx) =>
program.methods
.approve()
.accounts({
multisig: multisigAddress,
transaction: new PublicKey(tx),
owner: signer,
})
.instruction(),
),
)

return ixs
}

//execute not needed, this command cannot be ran outside of multisig
archseer marked this conversation as resolved.
Show resolved Hide resolved
execute = async () => {
return {} as Result<TransactionResponse>
}
}
24 changes: 2 additions & 22 deletions gauntlet/packages/gauntlet-solana-contracts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,31 +62,11 @@ yarn gauntlet <contract_name>:<contract_function> --help

## Testing Locally

### Preparation

- Include program keypairs under `/packages/gauntlet-solana-contracts/artifacts/programId/*.json`, resulting in:

```
packages/gauntlet-solana-contracts/artifacts/programId
| access_controller.json
| store.json
| ocr2.json
```

- Make sure these accounts public keys correspond to the ones declared on each contract `declare_id`. If they don't, compile the contracts (`anchor build`) with the correct `declare_id` and move the generated binaries from `/target/deploy/*.so` to `/packages/gauntlet-solana-contracts/artifacts/bin/*.so`

- Run a local store node

```
solana config set --url http://127.0.0.1:8899
solana-test-store -r
scripts/localnet.sh
```

- Get some SOL on your account. This account needs to be the same specified on gauntlet `.env` `PRIVATE_KEY`

```
solana airdrop 100 9ohrpVDVNKKW1LipksFrmq6wa1oLLYL9QSoYUn4pAQ2v
```
Starts a `solana-test-validator` with the programs pre-loaded and airdrops funds to the default PRIVATE_KEY used by `.env.local`.

### Running

Expand Down
17 changes: 10 additions & 7 deletions gauntlet/packages/gauntlet-solana-contracts/networks/.env.local
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
NODE_URL=http://127.0.0.1:8899
PROGRAM_ID_OCR2=E3j24rx12SyVsG6quKuZPbQqZPkhAUCh8Uek4XrKYD2x
PROGRAM_ID_ACCESS_CONTROLLER=2ckhep7Mvy1dExenBqpcdevhRu7CLuuctMcx7G9mWEvo
PROGRAM_ID_STORE=9kRNTZmoZSiTBuXC62dzK9E7gC7huYgcmRRhYv3i4osC

LINK=9TD23DYGTgUSVGWMjQVZ9cqLrvFRbzJ7MguUncfDLbSG
export PRIVATE_KEY=[9,218,36,113,218,176,180,196,27,75,171,187,105,81,84,58,52,79,85,169,125,13,0,102,214,246,82,252,133,222,160,252,193,218,154,28,253,34,136,185,53,68,165,141,248,188,247,143,17,100,91,130,75,49,212,131,37,18,151,175,201,153,131,185]

BILLING_ACCESS_CONTROLLER=2KeBZNtEhe9ws5n7czJxYyAYvTfzD6vMEYWHvACxMGWd
REQUESTER_ACCESS_CONTROLLER=5UF9xKW9rjJJTzwSmvf6EKTkwTnp8RspVCXNtE4xX59d
LOWERING_ACCESS_CONTROLLER=5UF9xKW9rjJJTzwSmvf6EKTkwTnp8RspVCXNtE4xX59d
PROGRAM_ID_OCR2=cjg3oHmg9uuPsP8D6g29NWvhySJkdYdAo9D25PRbKXJ
PROGRAM_ID_ACCESS_CONTROLLER=9xi644bRR8birboDGdTiwBq3C7VEeR7VuamRYYXCubUW
PROGRAM_ID_STORE=HEvSKofvBgfaexv23kMabbYqxasxU3mQ4ibBMEmJWHny

# LINK=9TD23DYGTgUSVGWMjQVZ9cqLrvFRbzJ7MguUncfDLbSG

# BILLING_ACCESS_CONTROLLER=2KeBZNtEhe9ws5n7czJxYyAYvTfzD6vMEYWHvACxMGWd
# REQUESTER_ACCESS_CONTROLLER=5UF9xKW9rjJJTzwSmvf6EKTkwTnp8RspVCXNtE4xX59d
# LOWERING_ACCESS_CONTROLLER=5UF9xKW9rjJJTzwSmvf6EKTkwTnp8RspVCXNtE4xX59d
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ const parseInstruction = async (instruction: string, version: string): Promise<A
const command = instruction.split(':')
if (!command.length || command.length > 2) return

const contract = isValidContract(command[0]) && (await getDeploymentContract(command[0] as CONTRACT_LIST, version))
const contract = isValidContract(command[0]) && getDeploymentContract(command[0] as CONTRACT_LIST, version)
if (!contract) throw new Error(`Abstract: Contract ${command[0]} not found`)

if (command[1] === SOLANA_OPERATIONS.HELP) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,7 @@ import { CONTRACT_LIST } from '../../../lib/contracts'
import { makeTransferOwnershipCommand } from '../ownership/transferOwnership'
import { makeUpgradeProgramCommand } from '../../abstract/upgrade'
import Fund from './fund'
import CreateProposal from './proposal/createProposal'
import ProposeConfig from './proposeConfig'
import ProposeOffchainConfig from './proposeOffchainConfig'
import ProposePayees from './proposePayees'
import FinalizeProposal from './proposal/finalizeProposal'
import Close from './close'
import WithdrawFunds from './withdrawFunds'
import WithdrawPayment from './withdrawPayment'
Expand All @@ -36,11 +32,7 @@ export default [
OCR2InitializeFlow,
SetBilling,
AcceptProposal,
CreateProposal,
FinalizeProposal,
ProposeConfig,
ProposeOffchainConfig,
ProposePayees,
ReadState,
SetBillingAccessController,
SetRequesterAccessController,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,7 @@ import { logger, prompt } from '@chainlink/gauntlet-core/dist/utils'
import OCR2Inspect from './inspection/inspect'
import CreateFeed from '../store/createFeed'
import SetWriter from '../store/setWriter'
import CreateProposal from './proposal/createProposal'
import ProposeOffchainConfig from './proposeOffchainConfig'
import ProposeConfig from './proposeConfig'
import ProposePayees from './proposePayees'
import FinalizeProposal from './proposal/finalizeProposal'
import AcceptProposal from './proposal/acceptProposal'

export default class OCR2InitializeFlow extends FlowCommand<TransactionResponse> {
Expand Down Expand Up @@ -70,40 +66,8 @@ export default class OCR2InitializeFlow extends FlowCommand<TransactionResponse>
},
{
id: this.stepIds.PROPOSAL,
name: 'Create Proposal',
command: CreateProposal,
},
{
name: 'Propose Config',
command: ProposeConfig,
flags: {
proposalId: FlowCommand.ID.data(this.stepIds.PROPOSAL, 'proposal'),
},
args: [FlowCommand.ID.contract(this.stepIds.OCR_2)],
},
{
id: this.stepIds.PROPOSE_OFFCHAIN,
name: 'Propose Offchain Config',
command: ProposeOffchainConfig,
flags: {
proposalId: FlowCommand.ID.data(this.stepIds.PROPOSAL, 'proposal'),
},
args: [FlowCommand.ID.contract(this.stepIds.OCR_2)],
},
{
name: 'Propose Payees',
command: ProposePayees,
flags: {
proposalId: FlowCommand.ID.data(this.stepIds.PROPOSAL, 'proposal'),
},
args: [FlowCommand.ID.contract(this.stepIds.OCR_2)],
},
{
name: 'Finalize Proposal',
command: FinalizeProposal,
flags: {
proposalId: FlowCommand.ID.data(this.stepIds.PROPOSAL, 'proposal'),
},
args: [FlowCommand.ID.contract(this.stepIds.OCR_2)],
},
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { SolanaCommand, TransactionResponse } from '@chainlink/gauntlet-solana'
import { Keypair, PublicKey, SystemProgram } from '@solana/web3.js'
import { CONTRACT_LIST, getContract } from '../../../../lib/contracts'
import { deserializeConfig } from '../../../../lib/encoding'
import WriteOffchainConfig, { OffchainConfig } from '../proposeOffchainConfig'
import WriteOffchainConfig, { OffchainConfig } from '../proposeConfig'
import { toComparableLongNumber, toComparableNumber, toComparablePubKey } from '../../../../lib/inspection'
import RDD from '../../../../lib/rdd'

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ import { PublicKey } from '@solana/web3.js'
import { getAssociatedTokenAddress, TOKEN_PROGRAM_ID } from '@solana/spl-token'
import { utils } from '@project-serum/anchor'
import { CONTRACT_LIST, getContract } from '../../../../lib/contracts'
import ProposeOffchainConfig, { OffchainConfig } from '../proposeOffchainConfig'
import ProposeConfig, { OffchainConfig } from '../proposeConfig'
import { serializeOffchainConfig, deserializeConfig } from '../../../../lib/encoding'
import { prepareOffchainConfigForDiff } from '../proposeOffchainConfig'
import { prepareOffchainConfigForDiff } from '../proposeConfig'
import RDD from '../../../../lib/rdd'
import { printDiff } from '../../../../lib/diff'

Expand Down Expand Up @@ -98,7 +98,7 @@ export default class AcceptProposal extends SolanaCommand {
}))
.sort((a, b) => Buffer.compare(_toHex(a.signer), _toHex(b.signer)))

const offchainConfig = ProposeOffchainConfig.makeInputFromRDD(rdd, this.args[0])
const offchainConfig = ProposeConfig.makeInputFromRDD(rdd, this.args[0])

const f = aggregator.config.f

Expand Down Expand Up @@ -159,15 +159,13 @@ export default class AcceptProposal extends SolanaCommand {
}

makeDigestInputFromProposal = (proposalInfo: Proposal): DigestInput => {
const oracles = proposalInfo.oracles.xs
.map((oracle) => {
return {
transmitter: new PublicKey(oracle.transmitter),
signer: Buffer.from(oracle.signer.key),
payee: new PublicKey(oracle.payee),
}
})
.slice(0, proposalInfo.oracles.len)
const oracles = proposalInfo.oracles.xs.slice(0, proposalInfo.oracles.len).map((oracle) => {
return {
transmitter: new PublicKey(oracle.transmitter),
signer: Buffer.from(oracle.signer.key),
payee: new PublicKey(oracle.payee),
}
})
return {
version: new BN(proposalInfo.offchainConfig.version),
f: new BN(proposalInfo.f),
Expand Down
Loading