https://github.com/base/mcm-go
https://github.com/base/mcm-go
Last synced: 8 months ago
JSON representation
- Host: GitHub
- URL: https://github.com/base/mcm-go
- Owner: base
- Created: 2025-10-02T14:10:19.000Z (9 months ago)
- Default Branch: master
- Last Pushed: 2025-10-07T08:44:29.000Z (8 months ago)
- Last Synced: 2025-10-07T10:24:23.238Z (8 months ago)
- Language: Go
- Size: 10.5 MB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
# MCM Go SDK
Go SDK for the Multi-Chain Multisig (MCM) Solana program.
## CLI Tool
The SDK includes `mcmctl`, a command-line tool for managing MCM multisigs:
```bash
go install github.com/base/mcm-go/cmd/mcmctl@latest
```
```bash
# Set environment variables
export RPC_URL="devnet"
export WS_URL="devnet"
export MCM_PROGRAM_ID="YourProgramID"
# Initialize a multisig (hex values must use 0x prefix)
mcmctl multisig init --multisig-id --chain-id 1
# Transfer ownership (two-step process)
mcmctl ownership transfer --multisig-id --proposed-owner
mcmctl ownership accept --multisig-id --authority
# Manage signers
mcmctl signers init --multisig-id --total 10
mcmctl signers append --multisig-id --signers ,,...
mcmctl signers finalize --multisig-id
mcmctl signers clear --multisig-id
mcmctl signers set-config --multisig-id --signer-groups --group-quorums --group-parents
```
See [cmd/mcmctl/README.md](cmd/mcmctl/README.md) for complete documentation.
## Quick Start
```go
package main
import (
"context"
"github.com/gagliardetto/solana-go"
"github.com/base/mcm-go/pkg/client"
"github.com/base/mcm-go/pkg/proposal"
"github.com/base/mcm-go/pkg/services"
)
func main() {
// Setup client
payer := solana.MustPrivateKeyFromBase58("your-private-key")
programID := solana.MustPublicKeyFromBase58("YourProgramID")
cfg := client.Config{
RPCURL: "https://api.devnet.solana.com",
WSURL: "wss://api.devnet.solana.com",
ProgramID: programID,
Payer: &payer,
}
mcmClient, _ := client.New(cfg)
defer mcmClient.Close()
// Create proposal from on-chain state
var multisigID [32]byte // Your multisig ID
var validUntil uint32 = 1800000000
var instructions []solana.Instruction // Your instructions
proposalSvc := services.NewProposalService(mcmClient)
ctx := context.Background()
p, _ := proposalSvc.CreateProposalFromChain(ctx, services.CreateProposalFromChainParams{
MultisigID: multisigID,
ValidUntil: validUntil,
Instructions: instructions,
OverridePreviousRoot: false,
})
// Compute Merkle root and hash to sign
pwr, _ := p.WithRoot()
pts, _ := pwr.WithHashToSign()
// Distribute pts.HashToSign to signers for ECDSA signing
}
```
## CLI Examples
The `cmd/mcmctl` directory provides a complete command-line interface demonstrating SDK usage:
- **Multisig operations** - Initialize multisig accounts on Solana
- **Ownership management** - Transfer multisig ownership securely (two-step process)
- **Signers management** - Configure signer addresses and groups
- **Signatures management** - Submit ECDSA signatures for proposal approval
- **Proposal operations** - Create proposals from instructions, compute hash for signing, set roots, and execute operations on-chain
- Includes specialized commands organized by category:
- **loader-v3**: `proposal loader-v3 upgrade` for Solana program upgrades, `proposal loader-v3 set-authority` for changing/removing upgrade authorities
- **mcm**: `proposal mcm update-signers` for complete signers configuration updates
- **mcm**: `proposal mcm accept-ownership` for accepting ownership transfers
- **bridge**: `proposal bridge pause` for pausing/unpausing bridge operations
- **bridge**: `proposal bridge set-partner-oracle-config` for updating bridge oracle configuration
See [cmd/mcmctl/README.md](cmd/mcmctl/README.md) for detailed usage examples.
## Package Structure
```
mcm-go/
├── pkg/
│ ├── bindings/ # Anchor-generated types from mcm.json IDL
│ ├── client/ # Solana RPC/WebSocket client wrapper with transaction helpers
│ ├── crypto/ # Keccak256 Merkle tree implementation with proof generation
│ ├── pda/ # Program Derived Address utilities
│ ├── proposal/ # Proposal types, builder, Merkle computation, signing
│ │ ├── io/ # JSON persistence (save/load proposals)
│ │ ├── types.go # Core types (Proposal, ProposalWithRoot, ProposalToSign)
│ │ ├── builder.go # Builder pattern for constructing proposals
│ │ ├── merkle.go # Merkle root computation (p.WithRoot())
│ │ └── signing.go # Hash to sign computation (pwr.WithHashToSign())
│ ├── instructions/ # MCM instruction builders (Initialize, SetConfig, etc.)
│ ├── state/ # On-chain account fetchers
│ └── services/ # High-level services (ProposalService, SignersService, etc.)
├── cmd/mcmctl/ # CLI demonstrating SDK usage
└── mcm.json # MCM program IDL (Anchor >= 0.30.0)
```
## Core Concepts
### 1. Proposals
Proposals contain instructions and metadata. The SDK provides a fluent API for computing cryptographic components:
```go
import "github.com/base/mcm-go/pkg/proposal"
// Option 1: Using Builder
builder := proposal.NewBuilder(multisigID, validUntil)
builder.SetRootMetadata(metadata)
builder.AddInstruction(instruction)
p, _ := builder.Build()
// Option 2: Direct construction
p := &proposal.Proposal{
MultisigID: multisigID,
ValidUntil: validUntil,
Instructions: instructions,
RootMetadata: metadata,
}
// Compute Merkle root and proofs
pwr, _ := p.WithRoot()
// Compute hash for ECDSA signing (keccak256(root || validUntil))
pts, _ := pwr.WithHashToSign()
// Distribute pts.HashToSign to signers
```
**IMPORTANT - Execution Authority:**
When creating proposals, ensure that the account used as `authority` when executing operations is NOT present in the accounts of any proposal operation. If the same account appears in both places, the program may fail with `ProofCannotBeVerified` error due to signer flag inconsistencies during Merkle proof verification.
**Recommended:** Use a dedicated account as execution authority that never appears in operation accounts.
### 2. Merkle Trees
Keccak256-based Merkle tree with automatic proof generation:
```go
import "github.com/base/mcm-go/pkg/crypto"
leaves := [][32]byte{leaf1, leaf2, leaf3}
tree, _ := crypto.BuildMerkleTreeFromLeaves(leaves)
// tree.Root is the Merkle root
// tree.Proofs[i] is the proof for leaves[i]
```
### 3. PDA Derivation
Derive Program Derived Addresses:
```go
import "github.com/base/mcm-go/pkg/pda"
configPDA, _, _ := pda.MultisigConfigPDA(programID, multisigID)
rootMetadataPDA, _, _ := pda.RootMetadataPDA(programID, multisigID)
```
### 4. Services
High-level services for common workflows:
```go
import "github.com/base/mcm-go/pkg/services"
// Signers management
signersSvc := services.NewSignersService(client)
signersSvc.InitSigners(ctx, params)
signersSvc.AppendSigners(ctx, params)
signersSvc.FinalizeSigners(ctx, params)
signersSvc.SetConfig(ctx, params)
// Signatures management
sigsSvc := services.NewSignaturesService(client)
sigsSvc.InitSignatures(ctx, params)
sigsSvc.AppendSignatures(ctx, params)
sigsSvc.FinalizeSignatures(ctx, params)
// Proposal service (includes creation, root setting, and execution)
proposalSvc := services.NewProposalService(client)
p, _ := proposalSvc.CreateProposalFromChain(ctx, params)
proposalSvc.SetRoot(ctx, params)
proposalSvc.Execute(ctx, params) // Execute operations (single, multiple, or all)
```
### 5. Persistence
Save and load proposals to/from JSON:
```go
import "github.com/base/mcm-go/pkg/proposal/io"
// Save proposal to file
io.SaveProposal(p, "proposal.json")
// Load proposal from file
p, _ := io.LoadProposal("proposal.json")
// Compute root and hash after loading
pwr, _ := p.WithRoot()
pts, _ := pwr.WithHashToSign()
```
## Complete Workflow
### 1. Initialize Multisig
```go
import "github.com/base/mcm-go/pkg/instructions"
ix, _ := instructions.Initialize(instructions.InitializeParams{
ChainID: 1,
MultisigID: multisigID,
Authority: authority,
ProgramID: programID,
})
```
### 2. Configure Signers
```go
signersSvc := services.NewSignersService(client)
// Initialize signer storage
signersSvc.InitSigners(ctx, services.InitSignersParams{
MultisigID: multisigID,
TotalSigners: 10,
})
// Add signers
signersSvc.AppendSigners(ctx, services.AppendSignersParams{
MultisigID: multisigID,
SignersBatch: signerAddresses,
})
// Finalize
signersSvc.FinalizeSigners(ctx, services.FinalizeSignersParams{
MultisigID: multisigID,
})
```
### 3. Set Configuration
```go
ix, _ := instructions.SetConfig(instructions.SetConfigParams{
MultisigID: multisigID,
SignerGroups: groups,
GroupQuorums: quorums,
GroupParents: parents,
ClearRoot: false,
Authority: authority,
ProgramID: programID,
})
```
### 4. Create Proposal and Collect Signatures
```go
proposalSvc := services.NewProposalService(client)
// Create proposal from on-chain state
p, _ := proposalSvc.CreateProposalFromChain(ctx, services.CreateProposalFromChainParams{
MultisigID: multisigID,
ValidUntil: validUntil,
Instructions: instructions,
OverridePreviousRoot: false,
})
// Compute root and hash
pwr, _ := p.WithRoot()
pts, _ := pwr.WithHashToSign()
// Distribute pts.HashToSign to signers for off-chain ECDSA signing
// Collect signatures...
// Submit signatures on-chain
sigsSvc := services.NewSignaturesService(client)
sigsSvc.InitSignatures(ctx, services.InitSignaturesParams{
MultisigID: multisigID,
Root: pwr.Root,
ValidUntil: validUntil,
TotalSignatures: uint8(len(signatures)),
})
sigsSvc.AppendSignatures(ctx, services.AppendSignaturesParams{
MultisigID: multisigID,
Root: pwr.Root,
ValidUntil: validUntil,
SignaturesBatch: signatures,
})
sigsSvc.FinalizeSignatures(ctx, services.FinalizeSignaturesParams{
MultisigID: multisigID,
Root: pwr.Root,
ValidUntil: validUntil,
})
```
### 5. Set Root and Execute
```go
// Set root on-chain
proposalSvc.SetRoot(ctx, services.SetRootParams{
MultisigID: multisigID,
Proposal: pwr,
})
// Execute all operations
proposalSvc.Execute(ctx, services.ExecuteParams{
MultisigID: multisigID,
ProposalWithRoot: pwr,
StartIndex: 0,
OperationCount: len(pwr.Instructions),
})
// Execute first operation
proposalSvc.Execute(ctx, services.ExecuteParams{
MultisigID: multisigID,
ProposalWithRoot: pwr,
StartIndex: 0,
OperationCount: 1,
})
// Execute next operation
proposalSvc.Execute(ctx, services.ExecuteParams{
MultisigID: multisigID,
ProposalWithRoot: pwr,
StartIndex: 1,
OperationCount: 1,
})
// Execute first two operations together
proposalSvc.Execute(ctx, services.ExecuteParams{
MultisigID: multisigID,
ProposalWithRoot: pwr,
StartIndex: 0,
OperationCount: 2,
})
```
## State Fetching
Fetch on-chain account state:
```go
import "github.com/base/mcm-go/pkg/state"
fetcher := state.NewFetcher(rpcClient, programID)
config, _ := fetcher.GetMultisigConfig(ctx, multisigID)
rootAndOpCount, _ := fetcher.GetExpiringRootAndOpCount(ctx, multisigID)
rootMetadata, _ := fetcher.GetRootMetadata(ctx, multisigID)
```
## Architecture
The SDK is organized in layers:
1. **Bindings** (`pkg/bindings`) - Anchor-generated types from IDL
2. **Core Utilities** (`pkg/pda`, `pkg/crypto`) - PDAs and Merkle trees
3. **Proposal Layer** (`pkg/proposal`) - Proposal construction and cryptography
4. **Instructions** (`pkg/instructions`) - MCM instruction builders
5. **State** (`pkg/state`) - On-chain account fetchers
6. **Services** (`pkg/services`) - High-level workflows
7. **Client** (`pkg/client`) - RPC, WebSocket, and transaction handling
## Testing
```bash
go test ./...
```
## Dependencies
- [solana-go](https://github.com/gagliardetto/solana-go) - Solana Go SDK
- [anchor-go](https://github.com/gagliardetto/anchor-go) - Used to generate bindings from `mcm.json` IDL
## IDL Source
The `mcm.json` IDL is sourced from the [MCM Solana program](https://github.com/smartcontractkit/chainlink-ccip/blob/main/chains/solana/contracts/target/idl/mcm.json) and updated to align with Anchor >= 0.30.0.
## Links
- [MCM Solana Program](https://github.com/smartcontractkit/chainlink-ccip/tree/main/chains/solana/contracts/programs/mcm)
## License
MIT