Go SDK for the Multi-Chain Multisig (MCM) Solana program.
The SDK includes mcmctl
, a command-line tool for managing MCM multisigs:
go install github.com/base/mcm-go/cmd/mcmctl@latest
# 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 <hex32> --chain-id 1
# Transfer ownership (two-step process)
mcmctl ownership transfer --multisig-id <hex32> --proposed-owner <pubkey>
mcmctl ownership accept --multisig-id <hex32> --authority <new-owner-keypair>
# Manage signers
mcmctl signers init --multisig-id <hex32> --total 10
mcmctl signers append --multisig-id <hex32> --signers <addr1>,<addr2>,...
mcmctl signers finalize --multisig-id <hex32>
mcmctl signers clear --multisig-id <hex32>
mcmctl signers set-config --multisig-id <hex32> --signer-groups <groups> --group-quorums <quorums> --group-parents <parents>
See cmd/mcmctl/README.md for complete documentation.
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
}
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
- loader-v3:
- Includes specialized commands organized by category:
See cmd/mcmctl/README.md for detailed usage examples.
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)
Proposals contain instructions and metadata. The SDK provides a fluent API for computing cryptographic components:
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.
Keccak256-based Merkle tree with automatic proof generation:
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]
Derive Program Derived Addresses:
import "github.com/base/mcm-go/pkg/pda"
configPDA, _, _ := pda.MultisigConfigPDA(programID, multisigID)
rootMetadataPDA, _, _ := pda.RootMetadataPDA(programID, multisigID)
High-level services for common workflows:
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)
Save and load proposals to/from JSON:
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()
import "github.com/base/mcm-go/pkg/instructions"
ix, _ := instructions.Initialize(instructions.InitializeParams{
ChainID: 1,
MultisigID: multisigID,
Authority: authority,
ProgramID: programID,
})
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,
})
ix, _ := instructions.SetConfig(instructions.SetConfigParams{
MultisigID: multisigID,
SignerGroups: groups,
GroupQuorums: quorums,
GroupParents: parents,
ClearRoot: false,
Authority: authority,
ProgramID: programID,
})
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,
})
// 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,
})
Fetch on-chain account state:
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)
The SDK is organized in layers:
- Bindings (
pkg/bindings
) - Anchor-generated types from IDL - Core Utilities (
pkg/pda
,pkg/crypto
) - PDAs and Merkle trees - Proposal Layer (
pkg/proposal
) - Proposal construction and cryptography - Instructions (
pkg/instructions
) - MCM instruction builders - State (
pkg/state
) - On-chain account fetchers - Services (
pkg/services
) - High-level workflows - Client (
pkg/client
) - RPC, WebSocket, and transaction handling
go test ./...
The mcm.json
IDL is sourced from the MCM Solana program and updated to align with Anchor >= 0.30.0.
MIT