diff --git a/programs/vote/Authorize.go b/programs/vote/Authorize.go index 734d9c23..7cc3b6d6 100644 --- a/programs/vote/Authorize.go +++ b/programs/vote/Authorize.go @@ -15,19 +15,152 @@ package vote import ( + "encoding/binary" + "errors" + "fmt" + + bin "github.com/gagliardetto/binary" "github.com/gagliardetto/solana-go" + "github.com/gagliardetto/solana-go/text/format" + "github.com/gagliardetto/treeout" ) +// Authorize is the Authorize instruction. +// Data: (Pubkey, VoteAuthorize) type Authorize struct { - // TODO + NewAuthority *solana.PublicKey + VoteAuthorize *VoteAuthorizeKind // [0] = [WRITE] VoteAccount - // ··········· Unitialized vote account // // [1] = [] SysVarClock - // ··········· Clock sysvar // // [2] = [SIGNER] Authority - // ··········· Vote or withdraw authority + // ··········· Current vote or withdraw authority. solana.AccountMetaSlice `bin:"-" borsh_skip:"true"` } + +func NewAuthorizeInstructionBuilder() *Authorize { + return &Authorize{ + AccountMetaSlice: make(solana.AccountMetaSlice, 3), + } +} + +func NewAuthorizeInstruction( + newAuthority solana.PublicKey, + kind VoteAuthorizeKind, + voteAccount solana.PublicKey, + currentAuthority solana.PublicKey, +) *Authorize { + return NewAuthorizeInstructionBuilder(). + SetNewAuthority(newAuthority). + SetVoteAuthorize(kind). + SetVoteAccount(voteAccount). + SetClockSysvar(solana.SysVarClockPubkey). + SetAuthority(currentAuthority) +} + +func (inst *Authorize) SetNewAuthority(pk solana.PublicKey) *Authorize { + inst.NewAuthority = &pk + return inst +} + +func (inst *Authorize) SetVoteAuthorize(kind VoteAuthorizeKind) *Authorize { + inst.VoteAuthorize = &kind + return inst +} + +func (inst *Authorize) SetVoteAccount(pk solana.PublicKey) *Authorize { + inst.AccountMetaSlice[0] = solana.Meta(pk).WRITE() + return inst +} + +func (inst *Authorize) SetClockSysvar(pk solana.PublicKey) *Authorize { + inst.AccountMetaSlice[1] = solana.Meta(pk) + return inst +} + +func (inst *Authorize) SetAuthority(pk solana.PublicKey) *Authorize { + inst.AccountMetaSlice[2] = solana.Meta(pk).SIGNER() + return inst +} + +func (inst *Authorize) GetVoteAccount() *solana.AccountMeta { return inst.AccountMetaSlice[0] } +func (inst *Authorize) GetClockSysvar() *solana.AccountMeta { return inst.AccountMetaSlice[1] } +func (inst *Authorize) GetAuthority() *solana.AccountMeta { return inst.AccountMetaSlice[2] } + +func (inst Authorize) Build() *Instruction { + return &Instruction{BaseVariant: bin.BaseVariant{ + Impl: inst, + TypeID: bin.TypeIDFromUint32(Instruction_Authorize, bin.LE), + }} +} + +func (inst Authorize) ValidateAndBuild() (*Instruction, error) { + if err := inst.Validate(); err != nil { + return nil, err + } + return inst.Build(), nil +} + +func (inst *Authorize) Validate() error { + if inst.NewAuthority == nil { + return errors.New("NewAuthority parameter is not set") + } + if inst.VoteAuthorize == nil { + return errors.New("VoteAuthorize parameter is not set") + } + for i, acc := range inst.AccountMetaSlice { + if acc == nil { + return fmt.Errorf("accounts[%d] is not set", i) + } + } + return nil +} + +func (inst *Authorize) UnmarshalWithDecoder(dec *bin.Decoder) error { + b, err := dec.ReadNBytes(32) + if err != nil { + return err + } + pk := solana.PublicKeyFromBytes(b) + inst.NewAuthority = &pk + raw, err := dec.ReadUint32(binary.LittleEndian) + if err != nil { + return err + } + kind := VoteAuthorizeKind(raw) + inst.VoteAuthorize = &kind + return nil +} + +func (inst Authorize) MarshalWithEncoder(enc *bin.Encoder) error { + if inst.NewAuthority == nil { + return errors.New("Authorize.NewAuthority is nil") + } + if inst.VoteAuthorize == nil { + return errors.New("Authorize.VoteAuthorize is nil") + } + if err := enc.WriteBytes(inst.NewAuthority[:], false); err != nil { + return err + } + return enc.WriteUint32(uint32(*inst.VoteAuthorize), binary.LittleEndian) +} + +func (inst *Authorize) EncodeToTree(parent treeout.Branches) { + parent.Child(format.Program(ProgramName, ProgramID)). + ParentFunc(func(programBranch treeout.Branches) { + programBranch.Child(format.Instruction("Authorize")). + ParentFunc(func(instructionBranch treeout.Branches) { + instructionBranch.Child("Params").ParentFunc(func(paramsBranch treeout.Branches) { + paramsBranch.Child(format.Param(" NewAuthority", inst.NewAuthority)) + paramsBranch.Child(format.Param("VoteAuthorize", inst.VoteAuthorize)) + }) + instructionBranch.Child("Accounts").ParentFunc(func(accountsBranch treeout.Branches) { + accountsBranch.Child(format.Meta("VoteAccount", inst.AccountMetaSlice[0])) + accountsBranch.Child(format.Meta("ClockSysvar", inst.AccountMetaSlice[1])) + accountsBranch.Child(format.Meta(" Authority", inst.AccountMetaSlice[2])) + }) + }) + }) +} diff --git a/programs/vote/AuthorizeChecked.go b/programs/vote/AuthorizeChecked.go new file mode 100644 index 00000000..e917eb9c --- /dev/null +++ b/programs/vote/AuthorizeChecked.go @@ -0,0 +1,126 @@ +package vote + +import ( + "encoding/binary" + "errors" + "fmt" + + bin "github.com/gagliardetto/binary" + "github.com/gagliardetto/solana-go" + "github.com/gagliardetto/solana-go/text/format" + "github.com/gagliardetto/treeout" +) + +// AuthorizeChecked is the checked variant of Authorize. +// Data: VoteAuthorize (u32 LE, plus BLS fields if kind=2). +type AuthorizeChecked struct { + VoteAuthorize *VoteAuthorizeKind + + // [0] = [WRITE] VoteAccount + // [1] = [] SysVarClock + // [2] = [SIGNER] CurrentAuthority + // [3] = [SIGNER] NewAuthority + solana.AccountMetaSlice `bin:"-" borsh_skip:"true"` +} + +func NewAuthorizeCheckedInstructionBuilder() *AuthorizeChecked { + return &AuthorizeChecked{ + AccountMetaSlice: make(solana.AccountMetaSlice, 4), + } +} + +func NewAuthorizeCheckedInstruction( + kind VoteAuthorizeKind, + voteAccount solana.PublicKey, + currentAuthority solana.PublicKey, + newAuthority solana.PublicKey, +) *AuthorizeChecked { + return NewAuthorizeCheckedInstructionBuilder(). + SetVoteAuthorize(kind). + SetVoteAccount(voteAccount). + SetClockSysvar(solana.SysVarClockPubkey). + SetCurrentAuthority(currentAuthority). + SetNewAuthority(newAuthority) +} + +func (inst *AuthorizeChecked) SetVoteAuthorize(k VoteAuthorizeKind) *AuthorizeChecked { + inst.VoteAuthorize = &k + return inst +} +func (inst *AuthorizeChecked) SetVoteAccount(pk solana.PublicKey) *AuthorizeChecked { + inst.AccountMetaSlice[0] = solana.Meta(pk).WRITE() + return inst +} +func (inst *AuthorizeChecked) SetClockSysvar(pk solana.PublicKey) *AuthorizeChecked { + inst.AccountMetaSlice[1] = solana.Meta(pk) + return inst +} +func (inst *AuthorizeChecked) SetCurrentAuthority(pk solana.PublicKey) *AuthorizeChecked { + inst.AccountMetaSlice[2] = solana.Meta(pk).SIGNER() + return inst +} +func (inst *AuthorizeChecked) SetNewAuthority(pk solana.PublicKey) *AuthorizeChecked { + inst.AccountMetaSlice[3] = solana.Meta(pk).SIGNER() + return inst +} + +func (inst AuthorizeChecked) Build() *Instruction { + return &Instruction{BaseVariant: bin.BaseVariant{ + Impl: inst, + TypeID: bin.TypeIDFromUint32(Instruction_AuthorizeChecked, bin.LE), + }} +} + +func (inst AuthorizeChecked) ValidateAndBuild() (*Instruction, error) { + if err := inst.Validate(); err != nil { + return nil, err + } + return inst.Build(), nil +} + +func (inst *AuthorizeChecked) Validate() error { + if inst.VoteAuthorize == nil { + return errors.New("VoteAuthorize parameter is not set") + } + for i, a := range inst.AccountMetaSlice { + if a == nil { + return fmt.Errorf("accounts[%d] is not set", i) + } + } + return nil +} + +func (inst *AuthorizeChecked) UnmarshalWithDecoder(dec *bin.Decoder) error { + raw, err := dec.ReadUint32(binary.LittleEndian) + if err != nil { + return err + } + k := VoteAuthorizeKind(raw) + inst.VoteAuthorize = &k + return nil +} + +func (inst AuthorizeChecked) MarshalWithEncoder(enc *bin.Encoder) error { + if inst.VoteAuthorize == nil { + return errors.New("AuthorizeChecked.VoteAuthorize is nil") + } + return enc.WriteUint32(uint32(*inst.VoteAuthorize), binary.LittleEndian) +} + +func (inst *AuthorizeChecked) EncodeToTree(parent treeout.Branches) { + parent.Child(format.Program(ProgramName, ProgramID)). + ParentFunc(func(programBranch treeout.Branches) { + programBranch.Child(format.Instruction("AuthorizeChecked")). + ParentFunc(func(instructionBranch treeout.Branches) { + instructionBranch.Child("Params").ParentFunc(func(paramsBranch treeout.Branches) { + paramsBranch.Child(format.Param("VoteAuthorize", inst.VoteAuthorize)) + }) + instructionBranch.Child("Accounts").ParentFunc(func(accountsBranch treeout.Branches) { + accountsBranch.Child(format.Meta(" VoteAccount", inst.AccountMetaSlice[0])) + accountsBranch.Child(format.Meta(" ClockSysvar", inst.AccountMetaSlice[1])) + accountsBranch.Child(format.Meta("CurrentAuthority", inst.AccountMetaSlice[2])) + accountsBranch.Child(format.Meta(" NewAuthority", inst.AccountMetaSlice[3])) + }) + }) + }) +} diff --git a/programs/vote/AuthorizeChecked_test.go b/programs/vote/AuthorizeChecked_test.go new file mode 100644 index 00000000..3b096604 --- /dev/null +++ b/programs/vote/AuthorizeChecked_test.go @@ -0,0 +1,22 @@ +package vote + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestRoundTrip_AuthorizeChecked(t *testing.T) { + inst := NewAuthorizeCheckedInstruction(VoteAuthorizeVoter, pubkeyOf(1), pubkeyOf(2), pubkeyOf(3)) + data, err := encodeInst(inst) + require.NoError(t, err) + require.Equal(t, u32LE(Instruction_AuthorizeChecked), data[:4]) + + expected := concat(u32LE(Instruction_AuthorizeChecked), u32LE(uint32(VoteAuthorizeVoter))) + require.Equal(t, expected, data) + + decoded, err := DecodeInstruction(nil, data) + require.NoError(t, err) + ac := decoded.Impl.(*AuthorizeChecked) + require.Equal(t, VoteAuthorizeVoter, *ac.VoteAuthorize) +} diff --git a/programs/vote/AuthorizeWithSeed.go b/programs/vote/AuthorizeWithSeed.go new file mode 100644 index 00000000..b29f209f --- /dev/null +++ b/programs/vote/AuthorizeWithSeed.go @@ -0,0 +1,179 @@ +package vote + +import ( + "errors" + "fmt" + + bin "github.com/gagliardetto/binary" + "github.com/gagliardetto/solana-go" + "github.com/gagliardetto/solana-go/text/format" + "github.com/gagliardetto/treeout" +) + +// AuthorizeWithSeed authorizes using a derived key. +// Data: VoteAuthorizeWithSeedArgs. +type AuthorizeWithSeed struct { + Args *VoteAuthorizeWithSeedArgs + + // [0] = [WRITE] VoteAccount + // [1] = [] SysVarClock + // [2] = [SIGNER] AuthorityBase + solana.AccountMetaSlice `bin:"-" borsh_skip:"true"` +} + +func NewAuthorizeWithSeedInstructionBuilder() *AuthorizeWithSeed { + return &AuthorizeWithSeed{ + AccountMetaSlice: make(solana.AccountMetaSlice, 3), + } +} + +func NewAuthorizeWithSeedInstruction( + args VoteAuthorizeWithSeedArgs, + voteAccount solana.PublicKey, + authorityBase solana.PublicKey, +) *AuthorizeWithSeed { + inst := NewAuthorizeWithSeedInstructionBuilder() + inst.Args = &args + inst.AccountMetaSlice[0] = solana.Meta(voteAccount).WRITE() + inst.AccountMetaSlice[1] = solana.Meta(solana.SysVarClockPubkey) + inst.AccountMetaSlice[2] = solana.Meta(authorityBase).SIGNER() + return inst +} + +func (inst AuthorizeWithSeed) Build() *Instruction { + return &Instruction{BaseVariant: bin.BaseVariant{ + Impl: inst, + TypeID: bin.TypeIDFromUint32(Instruction_AuthorizeWithSeed, bin.LE), + }} +} + +func (inst AuthorizeWithSeed) ValidateAndBuild() (*Instruction, error) { + if err := inst.Validate(); err != nil { + return nil, err + } + return inst.Build(), nil +} + +func (inst *AuthorizeWithSeed) Validate() error { + if inst.Args == nil { + return errors.New("Args parameter is not set") + } + for i, a := range inst.AccountMetaSlice { + if a == nil { + return fmt.Errorf("accounts[%d] is not set", i) + } + } + return nil +} + +func (inst *AuthorizeWithSeed) UnmarshalWithDecoder(dec *bin.Decoder) error { + inst.Args = new(VoteAuthorizeWithSeedArgs) + return inst.Args.UnmarshalWithDecoder(dec) +} + +func (inst AuthorizeWithSeed) MarshalWithEncoder(enc *bin.Encoder) error { + if inst.Args == nil { + return errors.New("AuthorizeWithSeed.Args is nil") + } + return inst.Args.MarshalWithEncoder(enc) +} + +func (inst *AuthorizeWithSeed) EncodeToTree(parent treeout.Branches) { + parent.Child(format.Program(ProgramName, ProgramID)). + ParentFunc(func(programBranch treeout.Branches) { + programBranch.Child(format.Instruction("AuthorizeWithSeed")). + ParentFunc(func(instructionBranch treeout.Branches) { + instructionBranch.Child("Params").ParentFunc(func(paramsBranch treeout.Branches) { + if inst.Args != nil { + paramsBranch.Child(format.Param("Seed", inst.Args.CurrentAuthorityDerivedKeySeed)) + } + }) + }) + }) +} + +// AuthorizeCheckedWithSeed is the checked variant of AuthorizeWithSeed. +// The new authority is also passed as a signer. +// Data: VoteAuthorizeCheckedWithSeedArgs. +type AuthorizeCheckedWithSeed struct { + Args *VoteAuthorizeCheckedWithSeedArgs + + // [0] = [WRITE] VoteAccount + // [1] = [] SysVarClock + // [2] = [SIGNER] AuthorityBase + // [3] = [SIGNER] NewAuthority + solana.AccountMetaSlice `bin:"-" borsh_skip:"true"` +} + +func NewAuthorizeCheckedWithSeedInstructionBuilder() *AuthorizeCheckedWithSeed { + return &AuthorizeCheckedWithSeed{ + AccountMetaSlice: make(solana.AccountMetaSlice, 4), + } +} + +func NewAuthorizeCheckedWithSeedInstruction( + args VoteAuthorizeCheckedWithSeedArgs, + voteAccount solana.PublicKey, + authorityBase solana.PublicKey, + newAuthority solana.PublicKey, +) *AuthorizeCheckedWithSeed { + inst := NewAuthorizeCheckedWithSeedInstructionBuilder() + inst.Args = &args + inst.AccountMetaSlice[0] = solana.Meta(voteAccount).WRITE() + inst.AccountMetaSlice[1] = solana.Meta(solana.SysVarClockPubkey) + inst.AccountMetaSlice[2] = solana.Meta(authorityBase).SIGNER() + inst.AccountMetaSlice[3] = solana.Meta(newAuthority).SIGNER() + return inst +} + +func (inst AuthorizeCheckedWithSeed) Build() *Instruction { + return &Instruction{BaseVariant: bin.BaseVariant{ + Impl: inst, + TypeID: bin.TypeIDFromUint32(Instruction_AuthorizeCheckedWithSeed, bin.LE), + }} +} + +func (inst AuthorizeCheckedWithSeed) ValidateAndBuild() (*Instruction, error) { + if err := inst.Validate(); err != nil { + return nil, err + } + return inst.Build(), nil +} + +func (inst *AuthorizeCheckedWithSeed) Validate() error { + if inst.Args == nil { + return errors.New("Args parameter is not set") + } + for i, a := range inst.AccountMetaSlice { + if a == nil { + return fmt.Errorf("accounts[%d] is not set", i) + } + } + return nil +} + +func (inst *AuthorizeCheckedWithSeed) UnmarshalWithDecoder(dec *bin.Decoder) error { + inst.Args = new(VoteAuthorizeCheckedWithSeedArgs) + return inst.Args.UnmarshalWithDecoder(dec) +} + +func (inst AuthorizeCheckedWithSeed) MarshalWithEncoder(enc *bin.Encoder) error { + if inst.Args == nil { + return errors.New("AuthorizeCheckedWithSeed.Args is nil") + } + return inst.Args.MarshalWithEncoder(enc) +} + +func (inst *AuthorizeCheckedWithSeed) EncodeToTree(parent treeout.Branches) { + parent.Child(format.Program(ProgramName, ProgramID)). + ParentFunc(func(programBranch treeout.Branches) { + programBranch.Child(format.Instruction("AuthorizeCheckedWithSeed")). + ParentFunc(func(instructionBranch treeout.Branches) { + instructionBranch.Child("Params").ParentFunc(func(paramsBranch treeout.Branches) { + if inst.Args != nil { + paramsBranch.Child(format.Param("Seed", inst.Args.CurrentAuthorityDerivedKeySeed)) + } + }) + }) + }) +} diff --git a/programs/vote/AuthorizeWithSeed_test.go b/programs/vote/AuthorizeWithSeed_test.go new file mode 100644 index 00000000..2b1f6134 --- /dev/null +++ b/programs/vote/AuthorizeWithSeed_test.go @@ -0,0 +1,46 @@ +package vote + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestRoundTrip_AuthorizeWithSeed(t *testing.T) { + args := VoteAuthorizeWithSeedArgs{ + AuthorizationType: VoteAuthorize{Kind: VoteAuthorizeVoter}, + CurrentAuthorityDerivedKeyOwner: pubkeyOf(5), + CurrentAuthorityDerivedKeySeed: "seed-string", + NewAuthority: pubkeyOf(6), + } + inst := NewAuthorizeWithSeedInstruction(args, pubkeyOf(1), pubkeyOf(2)) + data, err := encodeInst(inst) + require.NoError(t, err) + require.Equal(t, u32LE(Instruction_AuthorizeWithSeed), data[:4]) + + decoded, err := DecodeInstruction(nil, data) + require.NoError(t, err) + a := decoded.Impl.(*AuthorizeWithSeed) + require.Equal(t, VoteAuthorizeVoter, a.Args.AuthorizationType.Kind) + require.Equal(t, "seed-string", a.Args.CurrentAuthorityDerivedKeySeed) + require.Equal(t, pubkeyOf(5), a.Args.CurrentAuthorityDerivedKeyOwner) + require.Equal(t, pubkeyOf(6), a.Args.NewAuthority) +} + +func TestRoundTrip_AuthorizeCheckedWithSeed(t *testing.T) { + args := VoteAuthorizeCheckedWithSeedArgs{ + AuthorizationType: VoteAuthorize{Kind: VoteAuthorizeWithdrawer}, + CurrentAuthorityDerivedKeyOwner: pubkeyOf(5), + CurrentAuthorityDerivedKeySeed: "another-seed", + } + inst := NewAuthorizeCheckedWithSeedInstruction(args, pubkeyOf(1), pubkeyOf(2), pubkeyOf(3)) + data, err := encodeInst(inst) + require.NoError(t, err) + require.Equal(t, u32LE(Instruction_AuthorizeCheckedWithSeed), data[:4]) + + decoded, err := DecodeInstruction(nil, data) + require.NoError(t, err) + a := decoded.Impl.(*AuthorizeCheckedWithSeed) + require.Equal(t, VoteAuthorizeWithdrawer, a.Args.AuthorizationType.Kind) + require.Equal(t, "another-seed", a.Args.CurrentAuthorityDerivedKeySeed) +} diff --git a/programs/vote/Authorize_test.go b/programs/vote/Authorize_test.go new file mode 100644 index 00000000..a9542d04 --- /dev/null +++ b/programs/vote/Authorize_test.go @@ -0,0 +1,25 @@ +package vote + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestRoundTrip_Authorize(t *testing.T) { + newAuth := pubkeyOf(5) + inst := NewAuthorizeInstruction(newAuth, VoteAuthorizeWithdrawer, pubkeyOf(1), pubkeyOf(2)) + data, err := encodeInst(inst) + require.NoError(t, err) + require.Equal(t, u32LE(Instruction_Authorize), data[:4]) + + // Data: 32 bytes pubkey + 4 bytes kind + expected := concat(u32LE(Instruction_Authorize), newAuth[:], u32LE(uint32(VoteAuthorizeWithdrawer))) + require.Equal(t, expected, data) + + decoded, err := DecodeInstruction(nil, data) + require.NoError(t, err) + a := decoded.Impl.(*Authorize) + require.Equal(t, newAuth, *a.NewAuthority) + require.Equal(t, VoteAuthorizeWithdrawer, *a.VoteAuthorize) +} diff --git a/programs/vote/InitializeAccount.go b/programs/vote/InitializeAccount.go index ac35b135..4afea078 100644 --- a/programs/vote/InitializeAccount.go +++ b/programs/vote/InitializeAccount.go @@ -15,22 +15,147 @@ package vote import ( + "errors" + "fmt" + + bin "github.com/gagliardetto/binary" "github.com/gagliardetto/solana-go" + "github.com/gagliardetto/solana-go/text/format" + "github.com/gagliardetto/treeout" ) +// InitializeAccount initializes a vote account. type InitializeAccount struct { - // TODO + VoteInit *VoteInit // [0] = [WRITE] VoteAccount - // ··········· Vote account to vote with + // ··········· Uninitialized vote account to initialize. // - // [1] = [] SysVarSlotHashes - // ··········· Slot hashes sysvar + // [1] = [] SysVarRent + // ··········· Rent sysvar. // // [2] = [] SysVarClock - // ··········· Clock sysvar + // ··········· Clock sysvar. // - // [3] = [SIGNER] VoteAuthority - // ··········· New validator identity (node_pubkey) + // [3] = [SIGNER] NodePubkey + // ··········· New validator identity (node_pubkey). solana.AccountMetaSlice `bin:"-" borsh_skip:"true"` } + +func NewInitializeAccountInstructionBuilder() *InitializeAccount { + return &InitializeAccount{ + AccountMetaSlice: make(solana.AccountMetaSlice, 4), + VoteInit: &VoteInit{}, + } +} + +func NewInitializeAccountInstruction( + nodePubkey solana.PublicKey, + authorizedVoter solana.PublicKey, + authorizedWithdrawer solana.PublicKey, + commission uint8, + voteAccount solana.PublicKey, +) *InitializeAccount { + return NewInitializeAccountInstructionBuilder(). + SetVoteInit(VoteInit{ + NodePubkey: nodePubkey, + AuthorizedVoter: authorizedVoter, + AuthorizedWithdrawer: authorizedWithdrawer, + Commission: commission, + }). + SetVoteAccount(voteAccount). + SetRentSysvar(solana.SysVarRentPubkey). + SetClockSysvar(solana.SysVarClockPubkey). + SetNodePubkey(nodePubkey) +} + +func (inst *InitializeAccount) SetVoteInit(v VoteInit) *InitializeAccount { + inst.VoteInit = &v + return inst +} + +func (inst *InitializeAccount) SetVoteAccount(voteAccount solana.PublicKey) *InitializeAccount { + inst.AccountMetaSlice[0] = solana.Meta(voteAccount).WRITE() + return inst +} + +func (inst *InitializeAccount) SetRentSysvar(rent solana.PublicKey) *InitializeAccount { + inst.AccountMetaSlice[1] = solana.Meta(rent) + return inst +} + +func (inst *InitializeAccount) SetClockSysvar(clock solana.PublicKey) *InitializeAccount { + inst.AccountMetaSlice[2] = solana.Meta(clock) + return inst +} + +func (inst *InitializeAccount) SetNodePubkey(node solana.PublicKey) *InitializeAccount { + inst.AccountMetaSlice[3] = solana.Meta(node).SIGNER() + return inst +} + +func (inst *InitializeAccount) GetVoteAccount() *solana.AccountMeta { return inst.AccountMetaSlice[0] } +func (inst *InitializeAccount) GetRentSysvar() *solana.AccountMeta { return inst.AccountMetaSlice[1] } +func (inst *InitializeAccount) GetClockSysvar() *solana.AccountMeta { return inst.AccountMetaSlice[2] } +func (inst *InitializeAccount) GetNodePubkey() *solana.AccountMeta { return inst.AccountMetaSlice[3] } + +func (inst InitializeAccount) Build() *Instruction { + return &Instruction{BaseVariant: bin.BaseVariant{ + Impl: inst, + TypeID: bin.TypeIDFromUint32(Instruction_InitializeAccount, bin.LE), + }} +} + +func (inst InitializeAccount) ValidateAndBuild() (*Instruction, error) { + if err := inst.Validate(); err != nil { + return nil, err + } + return inst.Build(), nil +} + +func (inst *InitializeAccount) Validate() error { + if inst.VoteInit == nil { + return errors.New("VoteInit parameter is not set") + } + for i, acc := range inst.AccountMetaSlice { + if acc == nil { + return fmt.Errorf("accounts[%d] is not set", i) + } + } + return nil +} + +func (inst *InitializeAccount) UnmarshalWithDecoder(dec *bin.Decoder) error { + inst.VoteInit = new(VoteInit) + return inst.VoteInit.UnmarshalWithDecoder(dec) +} + +func (inst InitializeAccount) MarshalWithEncoder(enc *bin.Encoder) error { + if inst.VoteInit == nil { + return errors.New("InitializeAccount.VoteInit is nil") + } + return inst.VoteInit.MarshalWithEncoder(enc) +} + +func (inst *InitializeAccount) EncodeToTree(parent treeout.Branches) { + parent.Child(format.Program(ProgramName, ProgramID)). + ParentFunc(func(programBranch treeout.Branches) { + programBranch.Child(format.Instruction("InitializeAccount")). + ParentFunc(func(instructionBranch treeout.Branches) { + instructionBranch.Child("Params").ParentFunc(func(paramsBranch treeout.Branches) { + if inst.VoteInit != nil { + paramsBranch.Child(format.Param(" NodePubkey", inst.VoteInit.NodePubkey)) + paramsBranch.Child(format.Param(" AuthorizedVoter", inst.VoteInit.AuthorizedVoter)) + paramsBranch.Child(format.Param("AuthorizedWithdrawer", inst.VoteInit.AuthorizedWithdrawer)) + paramsBranch.Child(format.Param(" Commission", inst.VoteInit.Commission)) + } + }) + instructionBranch.Child("Accounts").ParentFunc(func(accountsBranch treeout.Branches) { + accountsBranch.Child(format.Meta("VoteAccount", inst.AccountMetaSlice[0])) + accountsBranch.Child(format.Meta(" RentSysvar", inst.AccountMetaSlice[1])) + accountsBranch.Child(format.Meta("ClockSysvar", inst.AccountMetaSlice[2])) + accountsBranch.Child(format.Meta(" NodePubkey", inst.AccountMetaSlice[3])) + }) + }) + }) +} diff --git a/programs/vote/InitializeAccountV2.go b/programs/vote/InitializeAccountV2.go new file mode 100644 index 00000000..b39b9246 --- /dev/null +++ b/programs/vote/InitializeAccountV2.go @@ -0,0 +1,96 @@ +package vote + +import ( + "errors" + "fmt" + + bin "github.com/gagliardetto/binary" + "github.com/gagliardetto/solana-go" + "github.com/gagliardetto/solana-go/text/format" + "github.com/gagliardetto/treeout" +) + +// InitializeAccountV2 initializes a vote account with V2 (Alpenglow) parameters. +// Data: VoteInitV2 +type InitializeAccountV2 struct { + VoteInit *VoteInitV2 + + // [0] = [WRITE] VoteAccount + // [1] = [] SysVarRent + // [2] = [] SysVarClock + // [3] = [SIGNER] NodePubkey + solana.AccountMetaSlice `bin:"-" borsh_skip:"true"` +} + +func NewInitializeAccountV2InstructionBuilder() *InitializeAccountV2 { + return &InitializeAccountV2{ + AccountMetaSlice: make(solana.AccountMetaSlice, 4), + } +} + +func NewInitializeAccountV2Instruction( + voteInit VoteInitV2, + voteAccount solana.PublicKey, +) *InitializeAccountV2 { + inst := NewInitializeAccountV2InstructionBuilder() + inst.VoteInit = &voteInit + inst.AccountMetaSlice[0] = solana.Meta(voteAccount).WRITE() + inst.AccountMetaSlice[1] = solana.Meta(solana.SysVarRentPubkey) + inst.AccountMetaSlice[2] = solana.Meta(solana.SysVarClockPubkey) + inst.AccountMetaSlice[3] = solana.Meta(voteInit.NodePubkey).SIGNER() + return inst +} + +func (inst InitializeAccountV2) Build() *Instruction { + return &Instruction{BaseVariant: bin.BaseVariant{ + Impl: inst, + TypeID: bin.TypeIDFromUint32(Instruction_InitializeAccountV2, bin.LE), + }} +} + +func (inst InitializeAccountV2) ValidateAndBuild() (*Instruction, error) { + if err := inst.Validate(); err != nil { + return nil, err + } + return inst.Build(), nil +} + +func (inst *InitializeAccountV2) Validate() error { + if inst.VoteInit == nil { + return errors.New("VoteInit parameter is not set") + } + for i, a := range inst.AccountMetaSlice { + if a == nil { + return fmt.Errorf("accounts[%d] is not set", i) + } + } + return nil +} + +func (inst *InitializeAccountV2) UnmarshalWithDecoder(dec *bin.Decoder) error { + inst.VoteInit = new(VoteInitV2) + return inst.VoteInit.UnmarshalWithDecoder(dec) +} + +func (inst InitializeAccountV2) MarshalWithEncoder(enc *bin.Encoder) error { + if inst.VoteInit == nil { + return errors.New("InitializeAccountV2.VoteInit is nil") + } + return inst.VoteInit.MarshalWithEncoder(enc) +} + +func (inst *InitializeAccountV2) EncodeToTree(parent treeout.Branches) { + parent.Child(format.Program(ProgramName, ProgramID)). + ParentFunc(func(programBranch treeout.Branches) { + programBranch.Child(format.Instruction("InitializeAccountV2")). + ParentFunc(func(instructionBranch treeout.Branches) { + instructionBranch.Child("Params").ParentFunc(func(paramsBranch treeout.Branches) { + if inst.VoteInit != nil { + paramsBranch.Child(format.Param("NodePubkey", inst.VoteInit.NodePubkey)) + paramsBranch.Child(format.Param("InflationRewardsBps", inst.VoteInit.InflationRewardsCommissionBps)) + paramsBranch.Child(format.Param("BlockRevenueBps", inst.VoteInit.BlockRevenueCommissionBps)) + } + }) + }) + }) +} diff --git a/programs/vote/InitializeAccountV2_test.go b/programs/vote/InitializeAccountV2_test.go new file mode 100644 index 00000000..d34d71d3 --- /dev/null +++ b/programs/vote/InitializeAccountV2_test.go @@ -0,0 +1,47 @@ +package vote + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestRoundTrip_InitializeAccountV2(t *testing.T) { + var blsPk [BLS_PUBLIC_KEY_COMPRESSED_SIZE]byte + var blsProof [BLS_PROOF_OF_POSSESSION_COMPRESSED_SIZE]byte + for i := range blsPk { + blsPk[i] = 0xAB + } + for i := range blsProof { + blsProof[i] = 0xCD + } + + init := VoteInitV2{ + NodePubkey: pubkeyOf(1), + AuthorizedVoter: pubkeyOf(2), + AuthorizedVoterBLSPubkey: blsPk, + AuthorizedVoterBLSProofOfPossession: blsProof, + AuthorizedWithdrawer: pubkeyOf(3), + InflationRewardsCommissionBps: 500, + BlockRevenueCommissionBps: 1000, + } + + inst := NewInitializeAccountV2Instruction(init, pubkeyOf(10)) + data, err := encodeInst(inst) + require.NoError(t, err) + require.Equal(t, u32LE(Instruction_InitializeAccountV2), data[:4]) + + // Data: 32+32+48+96+32+2+2 = 244 bytes + 4 tag + require.Len(t, data, 4+244) + + decoded, err := DecodeInstruction(nil, data) + require.NoError(t, err) + ia := decoded.Impl.(*InitializeAccountV2) + require.Equal(t, pubkeyOf(1), ia.VoteInit.NodePubkey) + require.Equal(t, pubkeyOf(2), ia.VoteInit.AuthorizedVoter) + require.Equal(t, blsPk, ia.VoteInit.AuthorizedVoterBLSPubkey) + require.Equal(t, blsProof, ia.VoteInit.AuthorizedVoterBLSProofOfPossession) + require.Equal(t, pubkeyOf(3), ia.VoteInit.AuthorizedWithdrawer) + require.Equal(t, uint16(500), ia.VoteInit.InflationRewardsCommissionBps) + require.Equal(t, uint16(1000), ia.VoteInit.BlockRevenueCommissionBps) +} diff --git a/programs/vote/InitializeAccount_test.go b/programs/vote/InitializeAccount_test.go new file mode 100644 index 00000000..32435855 --- /dev/null +++ b/programs/vote/InitializeAccount_test.go @@ -0,0 +1,29 @@ +package vote + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestRoundTrip_InitializeAccount(t *testing.T) { + node := pubkeyOf(1) + voter := pubkeyOf(2) + withdrawer := pubkeyOf(3) + + inst := NewInitializeAccountInstruction(node, voter, withdrawer, 42, pubkeyOf(10)) + data, err := encodeInst(inst) + require.NoError(t, err) + require.Equal(t, u32LE(Instruction_InitializeAccount), data[:4]) + + // Data payload: node(32) + voter(32) + withdrawer(32) + commission(1) = 97 bytes + require.Len(t, data, 4+97) + + decoded, err := DecodeInstruction(nil, data) + require.NoError(t, err) + ia := decoded.Impl.(*InitializeAccount) + require.Equal(t, node, ia.VoteInit.NodePubkey) + require.Equal(t, voter, ia.VoteInit.AuthorizedVoter) + require.Equal(t, withdrawer, ia.VoteInit.AuthorizedWithdrawer) + require.Equal(t, uint8(42), ia.VoteInit.Commission) +} diff --git a/programs/vote/TowerSync.go b/programs/vote/TowerSync.go new file mode 100644 index 00000000..43ff4b89 --- /dev/null +++ b/programs/vote/TowerSync.go @@ -0,0 +1,197 @@ +package vote + +import ( + "errors" + "fmt" + + bin "github.com/gagliardetto/binary" + "github.com/gagliardetto/solana-go" + "github.com/gagliardetto/solana-go/text/format" + "github.com/gagliardetto/treeout" +) + +// TowerSync is the TowerSync instruction (the current consensus mechanism, +// replacing UpdateVoteState). +// +// The data field is a TowerSyncUpdate which is serialized using the Solana +// compact serde format (short_vec lengths, varint lockout offsets, delta +// encoding) plus a trailing block_id hash. +// +// Data: TowerSyncUpdate (instruction ID 14). +type TowerSync struct { + Sync *TowerSyncUpdate + + // [0] = [WRITE] VoteAccount + // [1] = [SIGNER] VoteAuthority + solana.AccountMetaSlice `bin:"-" borsh_skip:"true"` +} + +// NewTowerSyncInstructionBuilder creates a new TowerSync instruction builder. +func NewTowerSyncInstructionBuilder() *TowerSync { + return &TowerSync{ + AccountMetaSlice: make(solana.AccountMetaSlice, 2), + } +} + +// NewTowerSyncInstruction builds a ready-to-use TowerSync instruction. +func NewTowerSyncInstruction( + sync TowerSyncUpdate, + voteAccount solana.PublicKey, + voteAuthority solana.PublicKey, +) *TowerSync { + inst := NewTowerSyncInstructionBuilder() + inst.Sync = &sync + inst.AccountMetaSlice[0] = solana.Meta(voteAccount).WRITE() + inst.AccountMetaSlice[1] = solana.Meta(voteAuthority).SIGNER() + return inst +} + +func (inst TowerSync) Build() *Instruction { + return &Instruction{BaseVariant: bin.BaseVariant{ + Impl: inst, + TypeID: bin.TypeIDFromUint32(Instruction_TowerSync, bin.LE), + }} +} + +func (inst TowerSync) ValidateAndBuild() (*Instruction, error) { + if err := inst.Validate(); err != nil { + return nil, err + } + return inst.Build(), nil +} + +func (inst *TowerSync) Validate() error { + if inst.Sync == nil { + return errors.New("Sync parameter is not set") + } + for i, a := range inst.AccountMetaSlice { + if a == nil { + return fmt.Errorf("accounts[%d] is not set", i) + } + } + return nil +} + +func (inst *TowerSync) UnmarshalWithDecoder(dec *bin.Decoder) error { + inst.Sync = new(TowerSyncUpdate) + return inst.Sync.UnmarshalWithDecoder(dec) +} + +func (inst TowerSync) MarshalWithEncoder(enc *bin.Encoder) error { + if inst.Sync == nil { + return errors.New("TowerSync.Sync is nil") + } + return inst.Sync.MarshalWithEncoder(enc) +} + +func (inst *TowerSync) EncodeToTree(parent treeout.Branches) { + parent.Child(format.Program(ProgramName, ProgramID)). + ParentFunc(func(programBranch treeout.Branches) { + programBranch.Child(format.Instruction("TowerSync")). + ParentFunc(func(instructionBranch treeout.Branches) { + instructionBranch.Child("Params").ParentFunc(func(paramsBranch treeout.Branches) { + if inst.Sync != nil { + paramsBranch.Child(format.Param("#Lockouts", len(inst.Sync.Lockouts))) + paramsBranch.Child(format.Param(" BlockID", inst.Sync.BlockID)) + } + }) + }) + }) +} + +// ---- TowerSyncSwitch ---- + +// TowerSyncSwitch is TowerSync with an additional proof hash for fork switching. +// Data: (TowerSyncUpdate, Hash) — same compact wire format as TowerSync +// followed by a 32-byte proof hash. +type TowerSyncSwitch struct { + Sync *TowerSyncUpdate + Hash solana.Hash + + // [0] = [WRITE] VoteAccount + // [1] = [SIGNER] VoteAuthority + solana.AccountMetaSlice `bin:"-" borsh_skip:"true"` +} + +// NewTowerSyncSwitchInstructionBuilder creates a new TowerSyncSwitch instruction builder. +func NewTowerSyncSwitchInstructionBuilder() *TowerSyncSwitch { + return &TowerSyncSwitch{ + AccountMetaSlice: make(solana.AccountMetaSlice, 2), + } +} + +// NewTowerSyncSwitchInstruction builds a ready-to-use TowerSyncSwitch instruction. +func NewTowerSyncSwitchInstruction( + sync TowerSyncUpdate, + proofHash solana.Hash, + voteAccount solana.PublicKey, + voteAuthority solana.PublicKey, +) *TowerSyncSwitch { + inst := NewTowerSyncSwitchInstructionBuilder() + inst.Sync = &sync + inst.Hash = proofHash + inst.AccountMetaSlice[0] = solana.Meta(voteAccount).WRITE() + inst.AccountMetaSlice[1] = solana.Meta(voteAuthority).SIGNER() + return inst +} + +func (inst TowerSyncSwitch) Build() *Instruction { + return &Instruction{BaseVariant: bin.BaseVariant{ + Impl: inst, + TypeID: bin.TypeIDFromUint32(Instruction_TowerSyncSwitch, bin.LE), + }} +} + +func (inst TowerSyncSwitch) ValidateAndBuild() (*Instruction, error) { + if err := inst.Validate(); err != nil { + return nil, err + } + return inst.Build(), nil +} + +func (inst *TowerSyncSwitch) Validate() error { + if inst.Sync == nil { + return errors.New("Sync parameter is not set") + } + for i, a := range inst.AccountMetaSlice { + if a == nil { + return fmt.Errorf("accounts[%d] is not set", i) + } + } + return nil +} + +func (inst *TowerSyncSwitch) UnmarshalWithDecoder(dec *bin.Decoder) error { + inst.Sync = new(TowerSyncUpdate) + if err := inst.Sync.UnmarshalWithDecoder(dec); err != nil { + return err + } + b, err := dec.ReadNBytes(32) + if err != nil { + return err + } + copy(inst.Hash[:], b) + return nil +} + +func (inst TowerSyncSwitch) MarshalWithEncoder(enc *bin.Encoder) error { + if inst.Sync == nil { + return errors.New("TowerSyncSwitch.Sync is nil") + } + if err := inst.Sync.MarshalWithEncoder(enc); err != nil { + return err + } + return enc.WriteBytes(inst.Hash[:], false) +} + +func (inst *TowerSyncSwitch) EncodeToTree(parent treeout.Branches) { + parent.Child(format.Program(ProgramName, ProgramID)). + ParentFunc(func(programBranch treeout.Branches) { + programBranch.Child(format.Instruction("TowerSyncSwitch")). + ParentFunc(func(instructionBranch treeout.Branches) { + instructionBranch.Child("Params").ParentFunc(func(paramsBranch treeout.Branches) { + paramsBranch.Child(format.Param("ProofHash", inst.Hash)) + }) + }) + }) +} diff --git a/programs/vote/TowerSync_test.go b/programs/vote/TowerSync_test.go new file mode 100644 index 00000000..ce502f1f --- /dev/null +++ b/programs/vote/TowerSync_test.go @@ -0,0 +1,52 @@ +package vote + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestRoundTrip_TowerSync(t *testing.T) { + root := uint64(50) + sync := TowerSyncUpdate{ + Lockouts: []Lockout{ + {Slot: 100, ConfirmationCount: 3}, + {Slot: 101, ConfirmationCount: 2}, + }, + Root: &root, + Hash: hashOf(0xAA), + BlockID: hashOf(0xBB), + } + inst := NewTowerSyncInstruction(sync, pubkeyOf(1), pubkeyOf(2)) + data, err := encodeInst(inst) + require.NoError(t, err) + require.Equal(t, u32LE(Instruction_TowerSync), data[:4]) + + decoded, err := DecodeInstruction(nil, data) + require.NoError(t, err) + ts := decoded.Impl.(*TowerSync) + require.Len(t, ts.Sync.Lockouts, 2) + require.Equal(t, hashOf(0xAA), ts.Sync.Hash) + require.Equal(t, hashOf(0xBB), ts.Sync.BlockID) + require.NotNil(t, ts.Sync.Root) + require.Equal(t, uint64(50), *ts.Sync.Root) +} + +func TestRoundTrip_TowerSyncSwitch(t *testing.T) { + sync := TowerSyncUpdate{ + Lockouts: []Lockout{{Slot: 1, ConfirmationCount: 1}}, + Hash: hashOf(0x11), + BlockID: hashOf(0x22), + } + proof := hashOf(0x33) + inst := NewTowerSyncSwitchInstruction(sync, proof, pubkeyOf(1), pubkeyOf(2)) + data, err := encodeInst(inst) + require.NoError(t, err) + require.Equal(t, u32LE(Instruction_TowerSyncSwitch), data[:4]) + + decoded, err := DecodeInstruction(nil, data) + require.NoError(t, err) + tss := decoded.Impl.(*TowerSyncSwitch) + require.Equal(t, proof, tss.Hash) + require.Equal(t, hashOf(0x22), tss.Sync.BlockID) +} diff --git a/programs/vote/UpdateCommission.go b/programs/vote/UpdateCommission.go new file mode 100644 index 00000000..9f13a4bb --- /dev/null +++ b/programs/vote/UpdateCommission.go @@ -0,0 +1,111 @@ +package vote + +import ( + "errors" + "fmt" + + bin "github.com/gagliardetto/binary" + "github.com/gagliardetto/solana-go" + "github.com/gagliardetto/solana-go/text/format" + "github.com/gagliardetto/treeout" +) + +// UpdateCommission updates the commission of a vote account. +// Data: u8 commission. +type UpdateCommission struct { + Commission *uint8 + + // [0] = [WRITE] VoteAccount + // [1] = [SIGNER] WithdrawAuthority + solana.AccountMetaSlice `bin:"-" borsh_skip:"true"` +} + +func NewUpdateCommissionInstructionBuilder() *UpdateCommission { + return &UpdateCommission{ + AccountMetaSlice: make(solana.AccountMetaSlice, 2), + } +} + +func NewUpdateCommissionInstruction( + commission uint8, + voteAccount solana.PublicKey, + withdrawAuthority solana.PublicKey, +) *UpdateCommission { + return NewUpdateCommissionInstructionBuilder(). + SetCommission(commission). + SetVoteAccount(voteAccount). + SetWithdrawAuthority(withdrawAuthority) +} + +func (inst *UpdateCommission) SetCommission(c uint8) *UpdateCommission { + inst.Commission = &c + return inst +} + +func (inst *UpdateCommission) SetVoteAccount(pk solana.PublicKey) *UpdateCommission { + inst.AccountMetaSlice[0] = solana.Meta(pk).WRITE() + return inst +} + +func (inst *UpdateCommission) SetWithdrawAuthority(pk solana.PublicKey) *UpdateCommission { + inst.AccountMetaSlice[1] = solana.Meta(pk).SIGNER() + return inst +} + +func (inst UpdateCommission) Build() *Instruction { + return &Instruction{BaseVariant: bin.BaseVariant{ + Impl: inst, + TypeID: bin.TypeIDFromUint32(Instruction_UpdateCommission, bin.LE), + }} +} + +func (inst UpdateCommission) ValidateAndBuild() (*Instruction, error) { + if err := inst.Validate(); err != nil { + return nil, err + } + return inst.Build(), nil +} + +func (inst *UpdateCommission) Validate() error { + if inst.Commission == nil { + return errors.New("Commission parameter is not set") + } + for i, a := range inst.AccountMetaSlice { + if a == nil { + return fmt.Errorf("accounts[%d] is not set", i) + } + } + return nil +} + +func (inst *UpdateCommission) UnmarshalWithDecoder(dec *bin.Decoder) error { + v, err := dec.ReadUint8() + if err != nil { + return err + } + inst.Commission = &v + return nil +} + +func (inst UpdateCommission) MarshalWithEncoder(enc *bin.Encoder) error { + if inst.Commission == nil { + return errors.New("UpdateCommission.Commission is nil") + } + return enc.WriteUint8(*inst.Commission) +} + +func (inst *UpdateCommission) EncodeToTree(parent treeout.Branches) { + parent.Child(format.Program(ProgramName, ProgramID)). + ParentFunc(func(programBranch treeout.Branches) { + programBranch.Child(format.Instruction("UpdateCommission")). + ParentFunc(func(instructionBranch treeout.Branches) { + instructionBranch.Child("Params").ParentFunc(func(paramsBranch treeout.Branches) { + paramsBranch.Child(format.Param("Commission", inst.Commission)) + }) + instructionBranch.Child("Accounts").ParentFunc(func(accountsBranch treeout.Branches) { + accountsBranch.Child(format.Meta(" VoteAccount", inst.AccountMetaSlice[0])) + accountsBranch.Child(format.Meta("WithdrawAuthority", inst.AccountMetaSlice[1])) + }) + }) + }) +} diff --git a/programs/vote/UpdateCommissionCollector.go b/programs/vote/UpdateCommissionCollector.go new file mode 100644 index 00000000..74cabc21 --- /dev/null +++ b/programs/vote/UpdateCommissionCollector.go @@ -0,0 +1,281 @@ +package vote + +import ( + "encoding/binary" + "errors" + "fmt" + + bin "github.com/gagliardetto/binary" + "github.com/gagliardetto/solana-go" + "github.com/gagliardetto/solana-go/text/format" + "github.com/gagliardetto/treeout" +) + +// UpdateCommissionCollector updates the collector account for a commission bucket. +// Data: CommissionKind (u8) +type UpdateCommissionCollector struct { + Kind *CommissionKind + + // [0] = [WRITE] VoteAccount + // [1] = [WRITE] NewCollector + // [2] = [SIGNER] WithdrawAuthority + solana.AccountMetaSlice `bin:"-" borsh_skip:"true"` +} + +func NewUpdateCommissionCollectorInstructionBuilder() *UpdateCommissionCollector { + return &UpdateCommissionCollector{ + AccountMetaSlice: make(solana.AccountMetaSlice, 3), + } +} + +func NewUpdateCommissionCollectorInstruction( + kind CommissionKind, + voteAccount solana.PublicKey, + newCollector solana.PublicKey, + withdrawAuthority solana.PublicKey, +) *UpdateCommissionCollector { + inst := NewUpdateCommissionCollectorInstructionBuilder() + inst.Kind = &kind + inst.AccountMetaSlice[0] = solana.Meta(voteAccount).WRITE() + inst.AccountMetaSlice[1] = solana.Meta(newCollector).WRITE() + inst.AccountMetaSlice[2] = solana.Meta(withdrawAuthority).SIGNER() + return inst +} + +func (inst UpdateCommissionCollector) Build() *Instruction { + return &Instruction{BaseVariant: bin.BaseVariant{ + Impl: inst, + TypeID: bin.TypeIDFromUint32(Instruction_UpdateCommissionCollector, bin.LE), + }} +} + +func (inst UpdateCommissionCollector) ValidateAndBuild() (*Instruction, error) { + if err := inst.Validate(); err != nil { + return nil, err + } + return inst.Build(), nil +} + +func (inst *UpdateCommissionCollector) Validate() error { + if inst.Kind == nil { + return errors.New("Kind parameter is not set") + } + for i, a := range inst.AccountMetaSlice { + if a == nil { + return fmt.Errorf("accounts[%d] is not set", i) + } + } + return nil +} + +func (inst *UpdateCommissionCollector) UnmarshalWithDecoder(dec *bin.Decoder) error { + v, err := dec.ReadUint8() + if err != nil { + return err + } + k := CommissionKind(v) + inst.Kind = &k + return nil +} + +func (inst UpdateCommissionCollector) MarshalWithEncoder(enc *bin.Encoder) error { + if inst.Kind == nil { + return errors.New("UpdateCommissionCollector.Kind is nil") + } + return enc.WriteUint8(uint8(*inst.Kind)) +} + +func (inst *UpdateCommissionCollector) EncodeToTree(parent treeout.Branches) { + parent.Child(format.Program(ProgramName, ProgramID)). + ParentFunc(func(programBranch treeout.Branches) { + programBranch.Child(format.Instruction("UpdateCommissionCollector")). + ParentFunc(func(instructionBranch treeout.Branches) { + instructionBranch.Child("Params").ParentFunc(func(paramsBranch treeout.Branches) { + paramsBranch.Child(format.Param("Kind", inst.Kind)) + }) + }) + }) +} + +// UpdateCommissionBps updates the commission rate (in basis points) for a bucket. +// Data: { commission_bps: u16, kind: CommissionKind } +type UpdateCommissionBps struct { + CommissionBps *uint16 + Kind *CommissionKind + + // [0] = [WRITE] VoteAccount + // [1] = [SIGNER] WithdrawAuthority + solana.AccountMetaSlice `bin:"-" borsh_skip:"true"` +} + +func NewUpdateCommissionBpsInstructionBuilder() *UpdateCommissionBps { + return &UpdateCommissionBps{ + AccountMetaSlice: make(solana.AccountMetaSlice, 2), + } +} + +func NewUpdateCommissionBpsInstruction( + commissionBps uint16, + kind CommissionKind, + voteAccount solana.PublicKey, + withdrawAuthority solana.PublicKey, +) *UpdateCommissionBps { + inst := NewUpdateCommissionBpsInstructionBuilder() + inst.CommissionBps = &commissionBps + inst.Kind = &kind + inst.AccountMetaSlice[0] = solana.Meta(voteAccount).WRITE() + inst.AccountMetaSlice[1] = solana.Meta(withdrawAuthority).SIGNER() + return inst +} + +func (inst UpdateCommissionBps) Build() *Instruction { + return &Instruction{BaseVariant: bin.BaseVariant{ + Impl: inst, + TypeID: bin.TypeIDFromUint32(Instruction_UpdateCommissionBps, bin.LE), + }} +} + +func (inst UpdateCommissionBps) ValidateAndBuild() (*Instruction, error) { + if err := inst.Validate(); err != nil { + return nil, err + } + return inst.Build(), nil +} + +func (inst *UpdateCommissionBps) Validate() error { + if inst.CommissionBps == nil { + return errors.New("CommissionBps parameter is not set") + } + if inst.Kind == nil { + return errors.New("Kind parameter is not set") + } + for i, a := range inst.AccountMetaSlice { + if a == nil { + return fmt.Errorf("accounts[%d] is not set", i) + } + } + return nil +} + +func (inst *UpdateCommissionBps) UnmarshalWithDecoder(dec *bin.Decoder) error { + bps, err := dec.ReadUint16(binary.LittleEndian) + if err != nil { + return err + } + inst.CommissionBps = &bps + v, err := dec.ReadUint8() + if err != nil { + return err + } + k := CommissionKind(v) + inst.Kind = &k + return nil +} + +func (inst UpdateCommissionBps) MarshalWithEncoder(enc *bin.Encoder) error { + if inst.CommissionBps == nil { + return errors.New("UpdateCommissionBps.CommissionBps is nil") + } + if inst.Kind == nil { + return errors.New("UpdateCommissionBps.Kind is nil") + } + if err := enc.WriteUint16(*inst.CommissionBps, binary.LittleEndian); err != nil { + return err + } + return enc.WriteUint8(uint8(*inst.Kind)) +} + +func (inst *UpdateCommissionBps) EncodeToTree(parent treeout.Branches) { + parent.Child(format.Program(ProgramName, ProgramID)). + ParentFunc(func(programBranch treeout.Branches) { + programBranch.Child(format.Instruction("UpdateCommissionBps")). + ParentFunc(func(instructionBranch treeout.Branches) { + instructionBranch.Child("Params").ParentFunc(func(paramsBranch treeout.Branches) { + paramsBranch.Child(format.Param("CommissionBps", inst.CommissionBps)) + paramsBranch.Child(format.Param(" Kind", inst.Kind)) + }) + }) + }) +} + +// DepositDelegatorRewards deposits delegator rewards into a vote account. +// Data: { deposit: u64 } +type DepositDelegatorRewards struct { + Deposit *uint64 + + // [0] = [WRITE] VoteAccount + // [1] = [WRITE, SIGNER] Depositor + solana.AccountMetaSlice `bin:"-" borsh_skip:"true"` +} + +func NewDepositDelegatorRewardsInstructionBuilder() *DepositDelegatorRewards { + return &DepositDelegatorRewards{ + AccountMetaSlice: make(solana.AccountMetaSlice, 2), + } +} + +func NewDepositDelegatorRewardsInstruction( + deposit uint64, + voteAccount solana.PublicKey, + depositor solana.PublicKey, +) *DepositDelegatorRewards { + inst := NewDepositDelegatorRewardsInstructionBuilder() + inst.Deposit = &deposit + inst.AccountMetaSlice[0] = solana.Meta(voteAccount).WRITE() + inst.AccountMetaSlice[1] = solana.Meta(depositor).WRITE().SIGNER() + return inst +} + +func (inst DepositDelegatorRewards) Build() *Instruction { + return &Instruction{BaseVariant: bin.BaseVariant{ + Impl: inst, + TypeID: bin.TypeIDFromUint32(Instruction_DepositDelegatorRewards, bin.LE), + }} +} + +func (inst DepositDelegatorRewards) ValidateAndBuild() (*Instruction, error) { + if err := inst.Validate(); err != nil { + return nil, err + } + return inst.Build(), nil +} + +func (inst *DepositDelegatorRewards) Validate() error { + if inst.Deposit == nil { + return errors.New("Deposit parameter is not set") + } + for i, a := range inst.AccountMetaSlice { + if a == nil { + return fmt.Errorf("accounts[%d] is not set", i) + } + } + return nil +} + +func (inst *DepositDelegatorRewards) UnmarshalWithDecoder(dec *bin.Decoder) error { + v, err := dec.ReadUint64(binary.LittleEndian) + if err != nil { + return err + } + inst.Deposit = &v + return nil +} + +func (inst DepositDelegatorRewards) MarshalWithEncoder(enc *bin.Encoder) error { + if inst.Deposit == nil { + return errors.New("DepositDelegatorRewards.Deposit is nil") + } + return enc.WriteUint64(*inst.Deposit, binary.LittleEndian) +} + +func (inst *DepositDelegatorRewards) EncodeToTree(parent treeout.Branches) { + parent.Child(format.Program(ProgramName, ProgramID)). + ParentFunc(func(programBranch treeout.Branches) { + programBranch.Child(format.Instruction("DepositDelegatorRewards")). + ParentFunc(func(instructionBranch treeout.Branches) { + instructionBranch.Child("Params").ParentFunc(func(paramsBranch treeout.Branches) { + paramsBranch.Child(format.Param("Deposit", inst.Deposit)) + }) + }) + }) +} diff --git a/programs/vote/UpdateCommissionCollector_test.go b/programs/vote/UpdateCommissionCollector_test.go new file mode 100644 index 00000000..1a67b488 --- /dev/null +++ b/programs/vote/UpdateCommissionCollector_test.go @@ -0,0 +1,50 @@ +package vote + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestRoundTrip_UpdateCommissionCollector(t *testing.T) { + inst := NewUpdateCommissionCollectorInstruction(CommissionKindBlockRevenue, pubkeyOf(1), pubkeyOf(2), pubkeyOf(3)) + data, err := encodeInst(inst) + require.NoError(t, err) + require.Equal(t, u32LE(Instruction_UpdateCommissionCollector), data[:4]) + + expected := concat(u32LE(Instruction_UpdateCommissionCollector), []byte{uint8(CommissionKindBlockRevenue)}) + require.Equal(t, expected, data) + + decoded, err := DecodeInstruction(nil, data) + require.NoError(t, err) + u := decoded.Impl.(*UpdateCommissionCollector) + require.Equal(t, CommissionKindBlockRevenue, *u.Kind) +} + +func TestRoundTrip_UpdateCommissionBps(t *testing.T) { + inst := NewUpdateCommissionBpsInstruction(500, CommissionKindInflationRewards, pubkeyOf(1), pubkeyOf(2)) + data, err := encodeInst(inst) + require.NoError(t, err) + require.Equal(t, u32LE(Instruction_UpdateCommissionBps), data[:4]) + + decoded, err := DecodeInstruction(nil, data) + require.NoError(t, err) + u := decoded.Impl.(*UpdateCommissionBps) + require.Equal(t, uint16(500), *u.CommissionBps) + require.Equal(t, CommissionKindInflationRewards, *u.Kind) +} + +func TestRoundTrip_DepositDelegatorRewards(t *testing.T) { + inst := NewDepositDelegatorRewardsInstruction(5_000_000, pubkeyOf(1), pubkeyOf(2)) + data, err := encodeInst(inst) + require.NoError(t, err) + require.Equal(t, u32LE(Instruction_DepositDelegatorRewards), data[:4]) + + expected := concat(u32LE(Instruction_DepositDelegatorRewards), u64LE(5_000_000)) + require.Equal(t, expected, data) + + decoded, err := DecodeInstruction(nil, data) + require.NoError(t, err) + d := decoded.Impl.(*DepositDelegatorRewards) + require.Equal(t, uint64(5_000_000), *d.Deposit) +} diff --git a/programs/vote/UpdateCommission_test.go b/programs/vote/UpdateCommission_test.go new file mode 100644 index 00000000..19d62cad --- /dev/null +++ b/programs/vote/UpdateCommission_test.go @@ -0,0 +1,22 @@ +package vote + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestRoundTrip_UpdateCommission(t *testing.T) { + inst := NewUpdateCommissionInstruction(50, pubkeyOf(1), pubkeyOf(2)) + data, err := encodeInst(inst) + require.NoError(t, err) + require.Equal(t, u32LE(Instruction_UpdateCommission), data[:4]) + + expected := concat(u32LE(Instruction_UpdateCommission), []byte{50}) + require.Equal(t, expected, data) + + decoded, err := DecodeInstruction(nil, data) + require.NoError(t, err) + u := decoded.Impl.(*UpdateCommission) + require.Equal(t, uint8(50), *u.Commission) +} diff --git a/programs/vote/UpdateValidatorIdentity.go b/programs/vote/UpdateValidatorIdentity.go new file mode 100644 index 00000000..4167eeda --- /dev/null +++ b/programs/vote/UpdateValidatorIdentity.go @@ -0,0 +1,92 @@ +package vote + +import ( + "fmt" + + bin "github.com/gagliardetto/binary" + "github.com/gagliardetto/solana-go" + "github.com/gagliardetto/solana-go/text/format" + "github.com/gagliardetto/treeout" +) + +// UpdateValidatorIdentity updates the validator identity of a vote account. +// No instruction data. +type UpdateValidatorIdentity struct { + // [0] = [WRITE] VoteAccount + // [1] = [SIGNER] NewIdentity + // [2] = [SIGNER] WithdrawAuthority + solana.AccountMetaSlice `bin:"-" borsh_skip:"true"` +} + +func NewUpdateValidatorIdentityInstructionBuilder() *UpdateValidatorIdentity { + return &UpdateValidatorIdentity{ + AccountMetaSlice: make(solana.AccountMetaSlice, 3), + } +} + +func NewUpdateValidatorIdentityInstruction( + voteAccount solana.PublicKey, + newIdentity solana.PublicKey, + withdrawAuthority solana.PublicKey, +) *UpdateValidatorIdentity { + return NewUpdateValidatorIdentityInstructionBuilder(). + SetVoteAccount(voteAccount). + SetNewIdentity(newIdentity). + SetWithdrawAuthority(withdrawAuthority) +} + +func (inst *UpdateValidatorIdentity) SetVoteAccount(pk solana.PublicKey) *UpdateValidatorIdentity { + inst.AccountMetaSlice[0] = solana.Meta(pk).WRITE() + return inst +} + +func (inst *UpdateValidatorIdentity) SetNewIdentity(pk solana.PublicKey) *UpdateValidatorIdentity { + inst.AccountMetaSlice[1] = solana.Meta(pk).SIGNER() + return inst +} + +func (inst *UpdateValidatorIdentity) SetWithdrawAuthority(pk solana.PublicKey) *UpdateValidatorIdentity { + inst.AccountMetaSlice[2] = solana.Meta(pk).SIGNER() + return inst +} + +func (inst UpdateValidatorIdentity) Build() *Instruction { + return &Instruction{BaseVariant: bin.BaseVariant{ + Impl: inst, + TypeID: bin.TypeIDFromUint32(Instruction_UpdateValidatorIdentity, bin.LE), + }} +} + +func (inst UpdateValidatorIdentity) ValidateAndBuild() (*Instruction, error) { + if err := inst.Validate(); err != nil { + return nil, err + } + return inst.Build(), nil +} + +func (inst *UpdateValidatorIdentity) Validate() error { + for i, a := range inst.AccountMetaSlice { + if a == nil { + return fmt.Errorf("accounts[%d] is not set", i) + } + } + return nil +} + +func (inst *UpdateValidatorIdentity) UnmarshalWithDecoder(dec *bin.Decoder) error { return nil } +func (inst UpdateValidatorIdentity) MarshalWithEncoder(enc *bin.Encoder) error { return nil } + +func (inst *UpdateValidatorIdentity) EncodeToTree(parent treeout.Branches) { + parent.Child(format.Program(ProgramName, ProgramID)). + ParentFunc(func(programBranch treeout.Branches) { + programBranch.Child(format.Instruction("UpdateValidatorIdentity")). + ParentFunc(func(instructionBranch treeout.Branches) { + instructionBranch.Child("Params").ParentFunc(func(paramsBranch treeout.Branches) {}) + instructionBranch.Child("Accounts").ParentFunc(func(accountsBranch treeout.Branches) { + accountsBranch.Child(format.Meta(" VoteAccount", inst.AccountMetaSlice[0])) + accountsBranch.Child(format.Meta(" NewIdentity", inst.AccountMetaSlice[1])) + accountsBranch.Child(format.Meta("WithdrawAuthority", inst.AccountMetaSlice[2])) + }) + }) + }) +} diff --git a/programs/vote/UpdateValidatorIdentity_test.go b/programs/vote/UpdateValidatorIdentity_test.go new file mode 100644 index 00000000..f1d68e79 --- /dev/null +++ b/programs/vote/UpdateValidatorIdentity_test.go @@ -0,0 +1,19 @@ +package vote + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestRoundTrip_UpdateValidatorIdentity(t *testing.T) { + inst := NewUpdateValidatorIdentityInstruction(pubkeyOf(1), pubkeyOf(2), pubkeyOf(3)) + data, err := encodeInst(inst) + require.NoError(t, err) + require.Equal(t, u32LE(Instruction_UpdateValidatorIdentity), data[:4]) + require.Len(t, data, 4) + + decoded, err := DecodeInstruction(nil, data) + require.NoError(t, err) + require.IsType(t, &UpdateValidatorIdentity{}, decoded.Impl) +} diff --git a/programs/vote/UpdateVoteState.go b/programs/vote/UpdateVoteState.go new file mode 100644 index 00000000..59ea907e --- /dev/null +++ b/programs/vote/UpdateVoteState.go @@ -0,0 +1,375 @@ +package vote + +import ( + "errors" + "fmt" + + bin "github.com/gagliardetto/binary" + "github.com/gagliardetto/solana-go" + "github.com/gagliardetto/solana-go/text/format" + "github.com/gagliardetto/treeout" +) + +// UpdateVoteState is the instruction that updates the vote state tower. +// Data: VoteStateUpdate. +type UpdateVoteState struct { + Update *VoteStateUpdate + + // [0] = [WRITE] VoteAccount + // [1] = [SIGNER] VoteAuthority + solana.AccountMetaSlice `bin:"-" borsh_skip:"true"` +} + +func NewUpdateVoteStateInstructionBuilder() *UpdateVoteState { + return &UpdateVoteState{ + AccountMetaSlice: make(solana.AccountMetaSlice, 2), + } +} + +func NewUpdateVoteStateInstruction( + update VoteStateUpdate, + voteAccount solana.PublicKey, + voteAuthority solana.PublicKey, +) *UpdateVoteState { + return NewUpdateVoteStateInstructionBuilder(). + SetUpdate(update). + SetVoteAccount(voteAccount). + SetVoteAuthority(voteAuthority) +} + +func (inst *UpdateVoteState) SetUpdate(u VoteStateUpdate) *UpdateVoteState { + inst.Update = &u + return inst +} +func (inst *UpdateVoteState) SetVoteAccount(pk solana.PublicKey) *UpdateVoteState { + inst.AccountMetaSlice[0] = solana.Meta(pk).WRITE() + return inst +} +func (inst *UpdateVoteState) SetVoteAuthority(pk solana.PublicKey) *UpdateVoteState { + inst.AccountMetaSlice[1] = solana.Meta(pk).SIGNER() + return inst +} + +func (inst UpdateVoteState) Build() *Instruction { + return &Instruction{BaseVariant: bin.BaseVariant{ + Impl: inst, + TypeID: bin.TypeIDFromUint32(Instruction_UpdateVoteState, bin.LE), + }} +} + +func (inst UpdateVoteState) ValidateAndBuild() (*Instruction, error) { + if err := inst.Validate(); err != nil { + return nil, err + } + return inst.Build(), nil +} + +func (inst *UpdateVoteState) Validate() error { + if inst.Update == nil { + return errors.New("Update parameter is not set") + } + for i, a := range inst.AccountMetaSlice { + if a == nil { + return fmt.Errorf("accounts[%d] is not set", i) + } + } + return nil +} + +func (inst *UpdateVoteState) UnmarshalWithDecoder(dec *bin.Decoder) error { + inst.Update = new(VoteStateUpdate) + return inst.Update.UnmarshalWithDecoder(dec) +} + +func (inst UpdateVoteState) MarshalWithEncoder(enc *bin.Encoder) error { + if inst.Update == nil { + return errors.New("UpdateVoteState.Update is nil") + } + return inst.Update.MarshalWithEncoder(enc) +} + +func (inst *UpdateVoteState) EncodeToTree(parent treeout.Branches) { + parent.Child(format.Program(ProgramName, ProgramID)). + ParentFunc(func(programBranch treeout.Branches) { + programBranch.Child(format.Instruction("UpdateVoteState")). + ParentFunc(func(instructionBranch treeout.Branches) { + instructionBranch.Child("Params").ParentFunc(func(paramsBranch treeout.Branches) { + if inst.Update != nil { + paramsBranch.Child(format.Param("#Lockouts", len(inst.Update.Lockouts))) + paramsBranch.Child(format.Param(" Hash", inst.Update.Hash)) + } + }) + instructionBranch.Child("Accounts").ParentFunc(func(accountsBranch treeout.Branches) { + accountsBranch.Child(format.Meta(" VoteAccount", inst.AccountMetaSlice[0])) + accountsBranch.Child(format.Meta("VoteAuthority", inst.AccountMetaSlice[1])) + }) + }) + }) +} + +// UpdateVoteStateSwitch is UpdateVoteState with a proof hash for fork switching. +// Data: (VoteStateUpdate, Hash) +type UpdateVoteStateSwitch struct { + Update *VoteStateUpdate + Hash solana.Hash + + // [0] = [WRITE] VoteAccount + // [1] = [SIGNER] VoteAuthority + solana.AccountMetaSlice `bin:"-" borsh_skip:"true"` +} + +func NewUpdateVoteStateSwitchInstructionBuilder() *UpdateVoteStateSwitch { + return &UpdateVoteStateSwitch{ + AccountMetaSlice: make(solana.AccountMetaSlice, 2), + } +} + +func NewUpdateVoteStateSwitchInstruction( + update VoteStateUpdate, + proofHash solana.Hash, + voteAccount solana.PublicKey, + voteAuthority solana.PublicKey, +) *UpdateVoteStateSwitch { + inst := NewUpdateVoteStateSwitchInstructionBuilder() + inst.Update = &update + inst.Hash = proofHash + inst.AccountMetaSlice[0] = solana.Meta(voteAccount).WRITE() + inst.AccountMetaSlice[1] = solana.Meta(voteAuthority).SIGNER() + return inst +} + +func (inst UpdateVoteStateSwitch) Build() *Instruction { + return &Instruction{BaseVariant: bin.BaseVariant{ + Impl: inst, + TypeID: bin.TypeIDFromUint32(Instruction_UpdateVoteStateSwitch, bin.LE), + }} +} + +func (inst UpdateVoteStateSwitch) ValidateAndBuild() (*Instruction, error) { + if err := inst.Validate(); err != nil { + return nil, err + } + return inst.Build(), nil +} + +func (inst *UpdateVoteStateSwitch) Validate() error { + if inst.Update == nil { + return errors.New("Update parameter is not set") + } + for i, a := range inst.AccountMetaSlice { + if a == nil { + return fmt.Errorf("accounts[%d] is not set", i) + } + } + return nil +} + +func (inst *UpdateVoteStateSwitch) UnmarshalWithDecoder(dec *bin.Decoder) error { + inst.Update = new(VoteStateUpdate) + if err := inst.Update.UnmarshalWithDecoder(dec); err != nil { + return err + } + b, err := dec.ReadNBytes(32) + if err != nil { + return err + } + copy(inst.Hash[:], b) + return nil +} + +func (inst UpdateVoteStateSwitch) MarshalWithEncoder(enc *bin.Encoder) error { + if inst.Update == nil { + return errors.New("UpdateVoteStateSwitch.Update is nil") + } + if err := inst.Update.MarshalWithEncoder(enc); err != nil { + return err + } + return enc.WriteBytes(inst.Hash[:], false) +} + +func (inst *UpdateVoteStateSwitch) EncodeToTree(parent treeout.Branches) { + parent.Child(format.Program(ProgramName, ProgramID)). + ParentFunc(func(programBranch treeout.Branches) { + programBranch.Child(format.Instruction("UpdateVoteStateSwitch")). + ParentFunc(func(instructionBranch treeout.Branches) { + instructionBranch.Child("Params").ParentFunc(func(paramsBranch treeout.Branches) { + paramsBranch.Child(format.Param("ProofHash", inst.Hash)) + }) + instructionBranch.Child("Accounts").ParentFunc(func(accountsBranch treeout.Branches) { + accountsBranch.Child(format.Meta(" VoteAccount", inst.AccountMetaSlice[0])) + accountsBranch.Child(format.Meta("VoteAuthority", inst.AccountMetaSlice[1])) + }) + }) + }) +} + +// CompactUpdateVoteState uses the Solana compact serde wire format: +// short_vec lockout offsets, varint deltas, u64 root (u64::MAX = None). +// Absolute lockout slots are delta-encoded at marshal time and reconstructed +// at unmarshal time. Users supply absolute slots in the VoteStateUpdate. +type CompactUpdateVoteState struct { + Update *VoteStateUpdate + + solana.AccountMetaSlice `bin:"-" borsh_skip:"true"` +} + +func NewCompactUpdateVoteStateInstructionBuilder() *CompactUpdateVoteState { + return &CompactUpdateVoteState{ + AccountMetaSlice: make(solana.AccountMetaSlice, 2), + } +} + +func NewCompactUpdateVoteStateInstruction( + update VoteStateUpdate, + voteAccount solana.PublicKey, + voteAuthority solana.PublicKey, +) *CompactUpdateVoteState { + inst := NewCompactUpdateVoteStateInstructionBuilder() + inst.Update = &update + inst.AccountMetaSlice[0] = solana.Meta(voteAccount).WRITE() + inst.AccountMetaSlice[1] = solana.Meta(voteAuthority).SIGNER() + return inst +} + +func (inst CompactUpdateVoteState) Build() *Instruction { + return &Instruction{BaseVariant: bin.BaseVariant{ + Impl: inst, + TypeID: bin.TypeIDFromUint32(Instruction_CompactUpdateVoteState, bin.LE), + }} +} + +func (inst CompactUpdateVoteState) ValidateAndBuild() (*Instruction, error) { + if err := inst.Validate(); err != nil { + return nil, err + } + return inst.Build(), nil +} + +func (inst *CompactUpdateVoteState) Validate() error { + if inst.Update == nil { + return errors.New("Update parameter is not set") + } + for i, a := range inst.AccountMetaSlice { + if a == nil { + return fmt.Errorf("accounts[%d] is not set", i) + } + } + return nil +} + +func (inst *CompactUpdateVoteState) UnmarshalWithDecoder(dec *bin.Decoder) error { + inst.Update = new(VoteStateUpdate) + return unmarshalCompactVoteStateUpdate(dec, inst.Update) +} + +func (inst CompactUpdateVoteState) MarshalWithEncoder(enc *bin.Encoder) error { + if inst.Update == nil { + return errors.New("CompactUpdateVoteState.Update is nil") + } + return marshalCompactVoteStateUpdate(enc, inst.Update) +} + +func (inst *CompactUpdateVoteState) EncodeToTree(parent treeout.Branches) { + parent.Child(format.Program(ProgramName, ProgramID)). + ParentFunc(func(programBranch treeout.Branches) { + programBranch.Child(format.Instruction("CompactUpdateVoteState")). + ParentFunc(func(instructionBranch treeout.Branches) { + instructionBranch.Child("Params").ParentFunc(func(paramsBranch treeout.Branches) { + if inst.Update != nil { + paramsBranch.Child(format.Param("#Lockouts", len(inst.Update.Lockouts))) + } + }) + }) + }) +} + +// CompactUpdateVoteStateSwitch is CompactUpdateVoteState with a trailing +// proof hash for fork switching. Uses the same compact wire format as +// CompactUpdateVoteState. +type CompactUpdateVoteStateSwitch struct { + Update *VoteStateUpdate + Hash solana.Hash + + solana.AccountMetaSlice `bin:"-" borsh_skip:"true"` +} + +func NewCompactUpdateVoteStateSwitchInstructionBuilder() *CompactUpdateVoteStateSwitch { + return &CompactUpdateVoteStateSwitch{ + AccountMetaSlice: make(solana.AccountMetaSlice, 2), + } +} + +func NewCompactUpdateVoteStateSwitchInstruction( + update VoteStateUpdate, + proofHash solana.Hash, + voteAccount solana.PublicKey, + voteAuthority solana.PublicKey, +) *CompactUpdateVoteStateSwitch { + inst := NewCompactUpdateVoteStateSwitchInstructionBuilder() + inst.Update = &update + inst.Hash = proofHash + inst.AccountMetaSlice[0] = solana.Meta(voteAccount).WRITE() + inst.AccountMetaSlice[1] = solana.Meta(voteAuthority).SIGNER() + return inst +} + +func (inst CompactUpdateVoteStateSwitch) Build() *Instruction { + return &Instruction{BaseVariant: bin.BaseVariant{ + Impl: inst, + TypeID: bin.TypeIDFromUint32(Instruction_CompactUpdateVoteStateSwitch, bin.LE), + }} +} + +func (inst CompactUpdateVoteStateSwitch) ValidateAndBuild() (*Instruction, error) { + if err := inst.Validate(); err != nil { + return nil, err + } + return inst.Build(), nil +} + +func (inst *CompactUpdateVoteStateSwitch) Validate() error { + if inst.Update == nil { + return errors.New("Update parameter is not set") + } + for i, a := range inst.AccountMetaSlice { + if a == nil { + return fmt.Errorf("accounts[%d] is not set", i) + } + } + return nil +} + +func (inst *CompactUpdateVoteStateSwitch) UnmarshalWithDecoder(dec *bin.Decoder) error { + inst.Update = new(VoteStateUpdate) + if err := unmarshalCompactVoteStateUpdate(dec, inst.Update); err != nil { + return err + } + b, err := dec.ReadNBytes(32) + if err != nil { + return err + } + copy(inst.Hash[:], b) + return nil +} + +func (inst CompactUpdateVoteStateSwitch) MarshalWithEncoder(enc *bin.Encoder) error { + if inst.Update == nil { + return errors.New("CompactUpdateVoteStateSwitch.Update is nil") + } + if err := marshalCompactVoteStateUpdate(enc, inst.Update); err != nil { + return err + } + return enc.WriteBytes(inst.Hash[:], false) +} + +func (inst *CompactUpdateVoteStateSwitch) EncodeToTree(parent treeout.Branches) { + parent.Child(format.Program(ProgramName, ProgramID)). + ParentFunc(func(programBranch treeout.Branches) { + programBranch.Child(format.Instruction("CompactUpdateVoteStateSwitch")). + ParentFunc(func(instructionBranch treeout.Branches) { + instructionBranch.Child("Params").ParentFunc(func(paramsBranch treeout.Branches) { + paramsBranch.Child(format.Param("ProofHash", inst.Hash)) + }) + }) + }) +} diff --git a/programs/vote/UpdateVoteState_test.go b/programs/vote/UpdateVoteState_test.go new file mode 100644 index 00000000..4b171c80 --- /dev/null +++ b/programs/vote/UpdateVoteState_test.go @@ -0,0 +1,87 @@ +package vote + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestRoundTrip_UpdateVoteState(t *testing.T) { + root := uint64(99) + ts := int64(1700000000) + update := VoteStateUpdate{ + Lockouts: []Lockout{ + {Slot: 100, ConfirmationCount: 3}, + {Slot: 101, ConfirmationCount: 2}, + {Slot: 102, ConfirmationCount: 1}, + }, + Root: &root, + Hash: hashOf(0xAB), + Timestamp: &ts, + } + + inst := NewUpdateVoteStateInstruction(update, pubkeyOf(1), pubkeyOf(2)) + data, err := encodeInst(inst) + require.NoError(t, err) + require.Equal(t, u32LE(Instruction_UpdateVoteState), data[:4]) + + decoded, err := DecodeInstruction(nil, data) + require.NoError(t, err) + u := decoded.Impl.(*UpdateVoteState) + require.Len(t, u.Update.Lockouts, 3) + require.Equal(t, uint64(100), u.Update.Lockouts[0].Slot) + require.Equal(t, uint32(3), u.Update.Lockouts[0].ConfirmationCount) + require.NotNil(t, u.Update.Root) + require.Equal(t, uint64(99), *u.Update.Root) + require.Equal(t, hashOf(0xAB), u.Update.Hash) + require.NotNil(t, u.Update.Timestamp) + require.Equal(t, ts, *u.Update.Timestamp) +} + +func TestRoundTrip_UpdateVoteState_NoRoot_NoTs(t *testing.T) { + update := VoteStateUpdate{ + Lockouts: []Lockout{{Slot: 1, ConfirmationCount: 1}}, + Hash: hashOf(0xEE), + } + inst := NewUpdateVoteStateInstruction(update, pubkeyOf(1), pubkeyOf(2)) + data, err := encodeInst(inst) + require.NoError(t, err) + + decoded, err := DecodeInstruction(nil, data) + require.NoError(t, err) + u := decoded.Impl.(*UpdateVoteState) + require.Nil(t, u.Update.Root) + require.Nil(t, u.Update.Timestamp) +} + +func TestRoundTrip_UpdateVoteStateSwitch(t *testing.T) { + update := VoteStateUpdate{ + Lockouts: []Lockout{{Slot: 500, ConfirmationCount: 1}}, + Hash: hashOf(0x11), + } + proof := hashOf(0x22) + inst := NewUpdateVoteStateSwitchInstruction(update, proof, pubkeyOf(1), pubkeyOf(2)) + data, err := encodeInst(inst) + require.NoError(t, err) + require.Equal(t, u32LE(Instruction_UpdateVoteStateSwitch), data[:4]) + + decoded, err := DecodeInstruction(nil, data) + require.NoError(t, err) + us := decoded.Impl.(*UpdateVoteStateSwitch) + require.Equal(t, proof, us.Hash) +} + +func TestRoundTrip_CompactUpdateVoteState(t *testing.T) { + update := VoteStateUpdate{ + Lockouts: []Lockout{{Slot: 1, ConfirmationCount: 1}}, + Hash: hashOf(0x33), + } + inst := NewCompactUpdateVoteStateInstruction(update, pubkeyOf(1), pubkeyOf(2)) + data, err := encodeInst(inst) + require.NoError(t, err) + require.Equal(t, u32LE(Instruction_CompactUpdateVoteState), data[:4]) + + decoded, err := DecodeInstruction(nil, data) + require.NoError(t, err) + require.IsType(t, &CompactUpdateVoteState{}, decoded.Impl) +} diff --git a/programs/vote/Vote.go b/programs/vote/Vote.go index 651ebd01..95c3b437 100644 --- a/programs/vote/Vote.go +++ b/programs/vote/Vote.go @@ -15,6 +15,7 @@ package vote import ( + "encoding/binary" "fmt" "time" @@ -43,46 +44,147 @@ type Vote struct { solana.AccountMetaSlice `bin:"-" borsh_skip:"true"` } +func NewVoteInstructionBuilder() *Vote { + return &Vote{ + AccountMetaSlice: make(solana.AccountMetaSlice, 4), + } +} + +func NewVoteInstruction( + slots []uint64, + hash solana.Hash, + timestamp *int64, + voteAccount solana.PublicKey, + voteAuthority solana.PublicKey, +) *Vote { + v := NewVoteInstructionBuilder(). + SetSlots(slots). + SetHash(hash). + SetVoteAccount(voteAccount). + SetSlotHashesSysvar(solana.SysVarSlotHashesPubkey). + SetClockSysvar(solana.SysVarClockPubkey). + SetVoteAuthority(voteAuthority) + if timestamp != nil { + v.SetTimestamp(*timestamp) + } + return v +} + +func (v *Vote) SetSlots(slots []uint64) *Vote { + v.Slots = slots + return v +} + +func (v *Vote) SetHash(hash solana.Hash) *Vote { + v.Hash = hash + return v +} + +func (v *Vote) SetTimestamp(ts int64) *Vote { + v.Timestamp = &ts + return v +} + +func (v *Vote) SetVoteAccount(pk solana.PublicKey) *Vote { + v.AccountMetaSlice[0] = solana.Meta(pk).WRITE() + return v +} + +func (v *Vote) SetSlotHashesSysvar(pk solana.PublicKey) *Vote { + v.AccountMetaSlice[1] = solana.Meta(pk) + return v +} + +func (v *Vote) SetClockSysvar(pk solana.PublicKey) *Vote { + v.AccountMetaSlice[2] = solana.Meta(pk) + return v +} + +func (v *Vote) SetVoteAuthority(pk solana.PublicKey) *Vote { + v.AccountMetaSlice[3] = solana.Meta(pk).SIGNER() + return v +} + +func (v *Vote) GetVoteAccount() *solana.AccountMeta { return v.AccountMetaSlice[0] } +func (v *Vote) GetSlotHashesSysvar() *solana.AccountMeta { return v.AccountMetaSlice[1] } +func (v *Vote) GetClockSysvar() *solana.AccountMeta { return v.AccountMetaSlice[2] } +func (v *Vote) GetVoteAuthority() *solana.AccountMeta { return v.AccountMetaSlice[3] } + +func (v Vote) Build() *Instruction { + return &Instruction{BaseVariant: bin.BaseVariant{ + Impl: v, + TypeID: bin.TypeIDFromUint32(Instruction_Vote, bin.LE), + }} +} + +func (v Vote) ValidateAndBuild() (*Instruction, error) { + if err := v.Validate(); err != nil { + return nil, err + } + return v.Build(), nil +} + func (v *Vote) UnmarshalWithDecoder(dec *bin.Decoder) error { v.Slots = nil - var numSlots uint64 - if err := dec.Decode(&numSlots); err != nil { + numSlots, err := dec.ReadUint64(binary.LittleEndian) + if err != nil { return err } for i := uint64(0); i < numSlots; i++ { - var slot uint64 - if err := dec.Decode(&slot); err != nil { + slot, err := dec.ReadUint64(binary.LittleEndian) + if err != nil { return err } v.Slots = append(v.Slots, slot) } - if err := dec.Decode(&v.Hash); err != nil { + b, err := dec.ReadNBytes(32) + if err != nil { return err } - var timestampVariant uint8 - if err := dec.Decode(×tampVariant); err != nil { + copy(v.Hash[:], b) + timestampVariant, err := dec.ReadUint8() + if err != nil { return err } switch timestampVariant { case 0: - break case 1: - var ts int64 - if err := dec.Decode(&ts); err != nil { + ts, err := dec.ReadInt64(binary.LittleEndian) + if err != nil { return err } v.Timestamp = &ts default: - return fmt.Errorf("invalid vote timestamp variant %#08x", timestampVariant) + return fmt.Errorf("invalid vote timestamp variant %#x", timestampVariant) } return nil } +func (v Vote) MarshalWithEncoder(enc *bin.Encoder) error { + if err := enc.WriteUint64(uint64(len(v.Slots)), binary.LittleEndian); err != nil { + return err + } + for _, s := range v.Slots { + if err := enc.WriteUint64(s, binary.LittleEndian); err != nil { + return err + } + } + if err := enc.WriteBytes(v.Hash[:], false); err != nil { + return err + } + if v.Timestamp == nil { + return enc.WriteUint8(0) + } + if err := enc.WriteUint8(1); err != nil { + return err + } + return enc.WriteInt64(*v.Timestamp, binary.LittleEndian) +} + func (inst *Vote) Validate() error { - // Check whether all accounts are set: for accIndex, acc := range inst.AccountMetaSlice { if acc == nil { - return fmt.Errorf("ins.AccountMetaSlice[%v] is not set", accIndex) + return fmt.Errorf("accounts[%d] is not set", accIndex) } } return nil @@ -93,23 +195,20 @@ func (inst *Vote) EncodeToTree(parent treeout.Branches) { ParentFunc(func(programBranch treeout.Branches) { programBranch.Child(format.Instruction("Vote")). ParentFunc(func(instructionBranch treeout.Branches) { - // Parameters of the instruction: instructionBranch.Child("Params").ParentFunc(func(paramsBranch treeout.Branches) { paramsBranch.Child(format.Param("Slots", inst.Slots)) - paramsBranch.Child(format.Param("Hash", inst.Hash)) + paramsBranch.Child(format.Param(" Hash", inst.Hash)) var ts time.Time if inst.Timestamp != nil { ts = time.Unix(*inst.Timestamp, 0).UTC() } paramsBranch.Child(format.Param("Timestamp", ts)) }) - - // Accounts of the instruction: instructionBranch.Child("Accounts").ParentFunc(func(accountsBranch treeout.Branches) { - accountsBranch.Child(format.Meta("Vote Account ", inst.AccountMetaSlice[0])) - accountsBranch.Child(format.Meta("Slot Hashes Sysvar", inst.AccountMetaSlice[1])) - accountsBranch.Child(format.Meta("Clock Sysvar ", inst.AccountMetaSlice[2])) - accountsBranch.Child(format.Meta("Vote Authority ", inst.AccountMetaSlice[3])) + accountsBranch.Child(format.Meta(" VoteAccount", inst.AccountMetaSlice[0])) + accountsBranch.Child(format.Meta("SlotHashesSysvar", inst.AccountMetaSlice[1])) + accountsBranch.Child(format.Meta(" ClockSysvar", inst.AccountMetaSlice[2])) + accountsBranch.Child(format.Meta(" VoteAuthority", inst.AccountMetaSlice[3])) }) }) }) diff --git a/programs/vote/VoteSwitch.go b/programs/vote/VoteSwitch.go new file mode 100644 index 00000000..8e6f45ca --- /dev/null +++ b/programs/vote/VoteSwitch.go @@ -0,0 +1,148 @@ +package vote + +import ( + "errors" + "fmt" + + bin "github.com/gagliardetto/binary" + "github.com/gagliardetto/solana-go" + "github.com/gagliardetto/solana-go/text/format" + "github.com/gagliardetto/treeout" +) + +// VoteSwitch is a Vote with an additional proof hash for fork switching. +// Data: (Vote, Hash) +type VoteSwitch struct { + Vote *Vote + Hash solana.Hash + + // Same accounts as Vote. + // [0] = [WRITE] VoteAccount + // [1] = [] SysVarSlotHashes + // [2] = [] SysVarClock + // [3] = [SIGNER] VoteAuthority + solana.AccountMetaSlice `bin:"-" borsh_skip:"true"` +} + +func NewVoteSwitchInstructionBuilder() *VoteSwitch { + return &VoteSwitch{ + AccountMetaSlice: make(solana.AccountMetaSlice, 4), + } +} + +func NewVoteSwitchInstruction( + slots []uint64, + voteHash solana.Hash, + timestamp *int64, + proofHash solana.Hash, + voteAccount solana.PublicKey, + voteAuthority solana.PublicKey, +) *VoteSwitch { + v := &Vote{Slots: slots, Hash: voteHash, Timestamp: timestamp} + inst := NewVoteSwitchInstructionBuilder(). + SetVote(v). + SetProofHash(proofHash). + SetVoteAccount(voteAccount). + SetSlotHashesSysvar(solana.SysVarSlotHashesPubkey). + SetClockSysvar(solana.SysVarClockPubkey). + SetVoteAuthority(voteAuthority) + return inst +} + +func (inst *VoteSwitch) SetVote(v *Vote) *VoteSwitch { + inst.Vote = v + return inst +} + +func (inst *VoteSwitch) SetProofHash(h solana.Hash) *VoteSwitch { + inst.Hash = h + return inst +} + +func (inst *VoteSwitch) SetVoteAccount(pk solana.PublicKey) *VoteSwitch { + inst.AccountMetaSlice[0] = solana.Meta(pk).WRITE() + return inst +} +func (inst *VoteSwitch) SetSlotHashesSysvar(pk solana.PublicKey) *VoteSwitch { + inst.AccountMetaSlice[1] = solana.Meta(pk) + return inst +} +func (inst *VoteSwitch) SetClockSysvar(pk solana.PublicKey) *VoteSwitch { + inst.AccountMetaSlice[2] = solana.Meta(pk) + return inst +} +func (inst *VoteSwitch) SetVoteAuthority(pk solana.PublicKey) *VoteSwitch { + inst.AccountMetaSlice[3] = solana.Meta(pk).SIGNER() + return inst +} + +func (inst VoteSwitch) Build() *Instruction { + return &Instruction{BaseVariant: bin.BaseVariant{ + Impl: inst, + TypeID: bin.TypeIDFromUint32(Instruction_VoteSwitch, bin.LE), + }} +} + +func (inst VoteSwitch) ValidateAndBuild() (*Instruction, error) { + if err := inst.Validate(); err != nil { + return nil, err + } + return inst.Build(), nil +} + +func (inst *VoteSwitch) Validate() error { + if inst.Vote == nil { + return errors.New("Vote parameter is not set") + } + for i, a := range inst.AccountMetaSlice { + if a == nil { + return fmt.Errorf("accounts[%d] is not set", i) + } + } + return nil +} + +func (inst *VoteSwitch) UnmarshalWithDecoder(dec *bin.Decoder) error { + inst.Vote = new(Vote) + if err := inst.Vote.UnmarshalWithDecoder(dec); err != nil { + return err + } + b, err := dec.ReadNBytes(32) + if err != nil { + return err + } + copy(inst.Hash[:], b) + return nil +} + +func (inst VoteSwitch) MarshalWithEncoder(enc *bin.Encoder) error { + if inst.Vote == nil { + return errors.New("VoteSwitch.Vote is nil") + } + if err := inst.Vote.MarshalWithEncoder(enc); err != nil { + return err + } + return enc.WriteBytes(inst.Hash[:], false) +} + +func (inst *VoteSwitch) EncodeToTree(parent treeout.Branches) { + parent.Child(format.Program(ProgramName, ProgramID)). + ParentFunc(func(programBranch treeout.Branches) { + programBranch.Child(format.Instruction("VoteSwitch")). + ParentFunc(func(instructionBranch treeout.Branches) { + instructionBranch.Child("Params").ParentFunc(func(paramsBranch treeout.Branches) { + if inst.Vote != nil { + paramsBranch.Child(format.Param("Slots", inst.Vote.Slots)) + paramsBranch.Child(format.Param(" Hash", inst.Vote.Hash)) + } + paramsBranch.Child(format.Param("ProofHash", inst.Hash)) + }) + instructionBranch.Child("Accounts").ParentFunc(func(accountsBranch treeout.Branches) { + accountsBranch.Child(format.Meta(" VoteAccount", inst.AccountMetaSlice[0])) + accountsBranch.Child(format.Meta("SlotHashesSysvar", inst.AccountMetaSlice[1])) + accountsBranch.Child(format.Meta(" ClockSysvar", inst.AccountMetaSlice[2])) + accountsBranch.Child(format.Meta(" VoteAuthority", inst.AccountMetaSlice[3])) + }) + }) + }) +} diff --git a/programs/vote/VoteSwitch_test.go b/programs/vote/VoteSwitch_test.go new file mode 100644 index 00000000..22c74dd8 --- /dev/null +++ b/programs/vote/VoteSwitch_test.go @@ -0,0 +1,25 @@ +package vote + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestRoundTrip_VoteSwitch(t *testing.T) { + slots := []uint64{100, 101} + voteHash := hashOf(0xAB) + proofHash := hashOf(0xCD) + + inst := NewVoteSwitchInstruction(slots, voteHash, nil, proofHash, pubkeyOf(1), pubkeyOf(2)) + data, err := encodeInst(inst) + require.NoError(t, err) + require.Equal(t, u32LE(Instruction_VoteSwitch), data[:4]) + + decoded, err := DecodeInstruction(nil, data) + require.NoError(t, err) + vs := decoded.Impl.(*VoteSwitch) + require.Equal(t, slots, vs.Vote.Slots) + require.Equal(t, voteHash, vs.Vote.Hash) + require.Equal(t, proofHash, vs.Hash) +} diff --git a/programs/vote/Vote_test.go b/programs/vote/Vote_test.go new file mode 100644 index 00000000..21e86913 --- /dev/null +++ b/programs/vote/Vote_test.go @@ -0,0 +1,41 @@ +package vote + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestRoundTrip_Vote_NoTimestamp(t *testing.T) { + slots := []uint64{100, 101, 102} + hash := hashOf(0xAB) + + inst := NewVoteInstruction(slots, hash, nil, pubkeyOf(1), pubkeyOf(2)) + data, err := encodeInst(inst) + require.NoError(t, err) + require.Equal(t, u32LE(Instruction_Vote), data[:4]) + + decoded, err := DecodeInstruction(nil, data) + require.NoError(t, err) + v := decoded.Impl.(*Vote) + require.Equal(t, slots, v.Slots) + require.Equal(t, hash, v.Hash) + require.Nil(t, v.Timestamp) +} + +func TestRoundTrip_Vote_WithTimestamp(t *testing.T) { + slots := []uint64{500} + hash := hashOf(0xCD) + ts := int64(1700000000) + + inst := NewVoteInstruction(slots, hash, &ts, pubkeyOf(1), pubkeyOf(2)) + data, err := encodeInst(inst) + require.NoError(t, err) + + decoded, err := DecodeInstruction(nil, data) + require.NoError(t, err) + v := decoded.Impl.(*Vote) + require.Equal(t, slots, v.Slots) + require.NotNil(t, v.Timestamp) + require.Equal(t, ts, *v.Timestamp) +} diff --git a/programs/vote/Withdraw.go b/programs/vote/Withdraw.go index 67517845..6751922a 100644 --- a/programs/vote/Withdraw.go +++ b/programs/vote/Withdraw.go @@ -110,6 +110,20 @@ func (inst *Withdraw) SetLamports(lamports uint64) *Withdraw { return inst } +func (inst Withdraw) Build() *Instruction { + return &Instruction{BaseVariant: bin.BaseVariant{ + Impl: inst, + TypeID: bin.TypeIDFromUint32(Instruction_Withdraw, bin.LE), + }} +} + +func (inst Withdraw) ValidateAndBuild() (*Instruction, error) { + if err := inst.Validate(); err != nil { + return nil, err + } + return inst.Build(), nil +} + func (inst *Withdraw) EncodeToTree(parent treeout.Branches) { parent.Child(format.Program(ProgramName, ProgramID)). // diff --git a/programs/vote/Withdraw_test.go b/programs/vote/Withdraw_test.go new file mode 100644 index 00000000..c1dba18a --- /dev/null +++ b/programs/vote/Withdraw_test.go @@ -0,0 +1,22 @@ +package vote + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestRoundTrip_Withdraw(t *testing.T) { + inst := NewWithdrawInstruction(1_000_000_000, pubkeyOf(1), pubkeyOf(2), pubkeyOf(3)) + data, err := encodeInst(inst) + require.NoError(t, err) + require.Equal(t, u32LE(Instruction_Withdraw), data[:4]) + + expected := concat(u32LE(Instruction_Withdraw), u64LE(1_000_000_000)) + require.Equal(t, expected, data) + + decoded, err := DecodeInstruction(nil, data) + require.NoError(t, err) + w := decoded.Impl.(*Withdraw) + require.Equal(t, uint64(1_000_000_000), *w.Lamports) +} diff --git a/programs/vote/compact.go b/programs/vote/compact.go new file mode 100644 index 00000000..c4aa9ed9 --- /dev/null +++ b/programs/vote/compact.go @@ -0,0 +1,173 @@ +// Copyright 2024 github.com/gagliardetto +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. + +package vote + +import ( + "encoding/binary" + "fmt" + + bin "github.com/gagliardetto/binary" +) + +// writeCompactU16Len writes a Solana compact-u16 (short_vec) length prefix. +// 1 byte if n < 0x80, 2 bytes if n < 0x4000, 3 bytes otherwise (max 2^16-1). +func writeCompactU16Len(enc *bin.Encoder, n int) error { + if n < 0 || n > 0xffff { + return fmt.Errorf("compact-u16 length out of range: %d", n) + } + rem := uint16(n) + for { + elem := uint8(rem & 0x7f) + rem >>= 7 + if rem == 0 { + return enc.WriteUint8(elem) + } + if err := enc.WriteUint8(elem | 0x80); err != nil { + return err + } + } +} + +// writeVarintU64 writes a LEB128-encoded u64 (as used by serde_varint). +func writeVarintU64(enc *bin.Encoder, v uint64) error { + for { + b := uint8(v & 0x7f) + v >>= 7 + if v == 0 { + return enc.WriteUint8(b) + } + if err := enc.WriteUint8(b | 0x80); err != nil { + return err + } + } +} + +// readVarintU64 reads a LEB128-encoded u64. +func readVarintU64(dec *bin.Decoder) (uint64, error) { + var v uint64 + var shift uint + for i := 0; i < 10; i++ { + b, err := dec.ReadUint8() + if err != nil { + return 0, err + } + v |= uint64(b&0x7f) << shift + if b&0x80 == 0 { + return v, nil + } + shift += 7 + } + return 0, fmt.Errorf("varint u64 overflow") +} + +// marshalCompactVoteStateUpdate encodes a VoteStateUpdate using the +// serde_compact_vote_state_update wire format: +// +// root: u64 (u64::MAX means None) +// lockout_offsets: short_vec of { varint offset, u8 confirmation_count } +// hash: [u8; 32] +// timestamp: Option +// +// Offsets are delta-encoded: each offset is saturating_sub(slot, previous_slot) +// where the initial previous_slot is the root (or u64::MAX if None). +// Lockouts are assumed to be sorted by slot (ascending). +func marshalCompactVoteStateUpdate(enc *bin.Encoder, u *VoteStateUpdate) error { + root := ^uint64(0) + if u.Root != nil { + root = *u.Root + } + if err := enc.WriteUint64(root, binary.LittleEndian); err != nil { + return err + } + if err := writeCompactU16Len(enc, len(u.Lockouts)); err != nil { + return err + } + prev := root + for _, l := range u.Lockouts { + var offset uint64 + if l.Slot > prev { + offset = l.Slot - prev + } // else saturating_sub -> 0 + if err := writeVarintU64(enc, offset); err != nil { + return err + } + if l.ConfirmationCount > 0xff { + return fmt.Errorf("confirmation_count %d exceeds u8 max", l.ConfirmationCount) + } + if err := enc.WriteUint8(uint8(l.ConfirmationCount)); err != nil { + return err + } + prev = l.Slot + } + if err := enc.WriteBytes(u.Hash[:], false); err != nil { + return err + } + if u.Timestamp == nil { + return enc.WriteUint8(0) + } + if err := enc.WriteUint8(1); err != nil { + return err + } + return enc.WriteInt64(*u.Timestamp, binary.LittleEndian) +} + +// unmarshalCompactVoteStateUpdate decodes a VoteStateUpdate from the +// serde_compact_vote_state_update wire format. Absolute lockout slots are +// reconstructed by cumulative sum over the delta offsets. +func unmarshalCompactVoteStateUpdate(dec *bin.Decoder, u *VoteStateUpdate) error { + root, err := dec.ReadUint64(binary.LittleEndian) + if err != nil { + return err + } + if root == ^uint64(0) { + u.Root = nil + } else { + r := root + u.Root = &r + } + count, err := dec.ReadCompactU16Length() + if err != nil { + return err + } + u.Lockouts = make([]Lockout, count) + prev := root + for i := range count { + offset, err := readVarintU64(dec) + if err != nil { + return err + } + cc, err := dec.ReadUint8() + if err != nil { + return err + } + // checked_add semantics: overflow produces a wrapped slot, but the + // on-chain program rejects such values at runtime. We preserve the + // bits so callers can detect the problem. + prev += offset + u.Lockouts[i] = Lockout{Slot: prev, ConfirmationCount: uint32(cc)} + } + b, err := dec.ReadNBytes(32) + if err != nil { + return err + } + copy(u.Hash[:], b) + hasTs, err := dec.ReadUint8() + if err != nil { + return err + } + switch hasTs { + case 0: + case 1: + ts, err := dec.ReadInt64(binary.LittleEndian) + if err != nil { + return err + } + u.Timestamp = &ts + default: + return fmt.Errorf("invalid Option discriminant: %d", hasTs) + } + return nil +} diff --git a/programs/vote/compact_test.go b/programs/vote/compact_test.go new file mode 100644 index 00000000..deee568b --- /dev/null +++ b/programs/vote/compact_test.go @@ -0,0 +1,269 @@ +package vote + +import ( + "bytes" + "testing" + + bin "github.com/gagliardetto/binary" + "github.com/stretchr/testify/require" +) + +// --- short_vec length encoding --- + +func TestCompactU16Len_OneByte(t *testing.T) { + cases := []int{0, 1, 0x7f} + for _, n := range cases { + buf := new(bytes.Buffer) + require.NoError(t, writeCompactU16Len(bin.NewBinEncoder(buf), n)) + require.Equal(t, []byte{byte(n)}, buf.Bytes(), "n=%d", n) + + dec := bin.NewBinDecoder(buf.Bytes()) + got, err := dec.ReadCompactU16Length() + require.NoError(t, err) + require.Equal(t, n, got) + } +} + +func TestCompactU16Len_TwoBytes(t *testing.T) { + // 0x80 encodes as [0x80, 0x01] + buf := new(bytes.Buffer) + require.NoError(t, writeCompactU16Len(bin.NewBinEncoder(buf), 0x80)) + require.Equal(t, []byte{0x80, 0x01}, buf.Bytes()) + + // 0x3fff encodes as [0xff, 0x7f] + buf.Reset() + require.NoError(t, writeCompactU16Len(bin.NewBinEncoder(buf), 0x3fff)) + require.Equal(t, []byte{0xff, 0x7f}, buf.Bytes()) + + // Round-trip both + for _, n := range []int{0x80, 0x3fff} { + buf := new(bytes.Buffer) + require.NoError(t, writeCompactU16Len(bin.NewBinEncoder(buf), n)) + got, err := bin.NewBinDecoder(buf.Bytes()).ReadCompactU16Length() + require.NoError(t, err) + require.Equal(t, n, got) + } +} + +func TestCompactU16Len_ThreeBytes(t *testing.T) { + // 0x4000 encodes as [0x80, 0x80, 0x01] + buf := new(bytes.Buffer) + require.NoError(t, writeCompactU16Len(bin.NewBinEncoder(buf), 0x4000)) + require.Equal(t, []byte{0x80, 0x80, 0x01}, buf.Bytes()) + + // 0xffff encodes as [0xff, 0xff, 0x03] + buf.Reset() + require.NoError(t, writeCompactU16Len(bin.NewBinEncoder(buf), 0xffff)) + require.Equal(t, []byte{0xff, 0xff, 0x03}, buf.Bytes()) + + // Round-trip + for _, n := range []int{0x4000, 0xffff} { + buf := new(bytes.Buffer) + require.NoError(t, writeCompactU16Len(bin.NewBinEncoder(buf), n)) + got, err := bin.NewBinDecoder(buf.Bytes()).ReadCompactU16Length() + require.NoError(t, err) + require.Equal(t, n, got) + } +} + +// --- varint u64 encoding --- + +func TestVarintU64_Zero(t *testing.T) { + buf := new(bytes.Buffer) + require.NoError(t, writeVarintU64(bin.NewBinEncoder(buf), 0)) + require.Equal(t, []byte{0x00}, buf.Bytes()) + + got, err := readVarintU64(bin.NewBinDecoder(buf.Bytes())) + require.NoError(t, err) + require.Equal(t, uint64(0), got) +} + +func TestVarintU64_Small(t *testing.T) { + // 0x7f is the largest single-byte varint + buf := new(bytes.Buffer) + require.NoError(t, writeVarintU64(bin.NewBinEncoder(buf), 0x7f)) + require.Equal(t, []byte{0x7f}, buf.Bytes()) +} + +func TestVarintU64_TwoBytes(t *testing.T) { + // 0x80 -> [0x80, 0x01] + buf := new(bytes.Buffer) + require.NoError(t, writeVarintU64(bin.NewBinEncoder(buf), 0x80)) + require.Equal(t, []byte{0x80, 0x01}, buf.Bytes()) + + got, err := readVarintU64(bin.NewBinDecoder(buf.Bytes())) + require.NoError(t, err) + require.Equal(t, uint64(0x80), got) +} + +func TestVarintU64_MaxU64(t *testing.T) { + buf := new(bytes.Buffer) + require.NoError(t, writeVarintU64(bin.NewBinEncoder(buf), ^uint64(0))) + // Round-trip + got, err := readVarintU64(bin.NewBinDecoder(buf.Bytes())) + require.NoError(t, err) + require.Equal(t, ^uint64(0), got) +} + +func TestVarintU64_RoundTripRange(t *testing.T) { + values := []uint64{ + 0, 1, 127, 128, 255, 16383, 16384, 1 << 20, 1 << 30, 1 << 62, ^uint64(0) - 1, + } + for _, v := range values { + buf := new(bytes.Buffer) + require.NoError(t, writeVarintU64(bin.NewBinEncoder(buf), v)) + got, err := readVarintU64(bin.NewBinDecoder(buf.Bytes())) + require.NoError(t, err) + require.Equal(t, v, got, "v=%d", v) + } +} + +// --- CompactVoteStateUpdate wire format --- + +// Verifies the delta encoding: given lockouts at absolute slots [100, 101, 105] +// with root=99, the encoded offsets should be [1, 1, 4]. +func TestCompactVoteStateUpdate_DeltaOffsets(t *testing.T) { + root := uint64(99) + update := &VoteStateUpdate{ + Lockouts: []Lockout{ + {Slot: 100, ConfirmationCount: 3}, + {Slot: 101, ConfirmationCount: 2}, + {Slot: 105, ConfirmationCount: 1}, + }, + Root: &root, + Hash: hashOf(0x00), + } + buf := new(bytes.Buffer) + require.NoError(t, marshalCompactVoteStateUpdate(bin.NewBinEncoder(buf), update)) + + // Expected layout: + // root: u64 LE = 99, 00, 00, 00, 00, 00, 00, 00 (8 bytes) + // lockout_count: u8 = 3 (fits in one byte) + // offset(1): varint = 0x01 + // conf_count: u8 = 3 + // offset(1): varint = 0x01 + // conf_count: u8 = 2 + // offset(4): varint = 0x04 + // conf_count: u8 = 1 + // hash: 32 bytes of 0x00 + // timestamp: u8 = 0 (None) + expected := []byte{ + 99, 0, 0, 0, 0, 0, 0, 0, // root + 3, // short_vec length + 1, 3, // offset 1, confirmation 3 + 1, 2, // offset 1, confirmation 2 + 4, 1, // offset 4, confirmation 1 + } + expected = append(expected, make([]byte, 32)...) // hash + expected = append(expected, 0) // timestamp None + + require.Equal(t, expected, buf.Bytes()) + + // Round-trip reconstructs absolute slots + decoded := new(VoteStateUpdate) + require.NoError(t, unmarshalCompactVoteStateUpdate(bin.NewBinDecoder(buf.Bytes()), decoded)) + require.Len(t, decoded.Lockouts, 3) + require.Equal(t, uint64(100), decoded.Lockouts[0].Slot) + require.Equal(t, uint64(101), decoded.Lockouts[1].Slot) + require.Equal(t, uint64(105), decoded.Lockouts[2].Slot) + require.NotNil(t, decoded.Root) + require.Equal(t, uint64(99), *decoded.Root) +} + +func TestCompactVoteStateUpdate_NoRoot(t *testing.T) { + update := &VoteStateUpdate{ + Lockouts: []Lockout{}, + Root: nil, + Hash: hashOf(0x00), + } + buf := new(bytes.Buffer) + require.NoError(t, marshalCompactVoteStateUpdate(bin.NewBinEncoder(buf), update)) + + // Root should be u64::MAX when nil + expected := []byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0} + expected = append(expected, make([]byte, 32)...) + expected = append(expected, 0) + require.Equal(t, expected, buf.Bytes()) + + decoded := new(VoteStateUpdate) + require.NoError(t, unmarshalCompactVoteStateUpdate(bin.NewBinDecoder(buf.Bytes()), decoded)) + require.Nil(t, decoded.Root) + require.Empty(t, decoded.Lockouts) +} + +// TestTowerSync_CompactWireFormat verifies that TowerSync's wire format +// differs from the non-compact VoteStateUpdate wire format (smaller) and +// that it includes the trailing block_id. +func TestTowerSync_CompactWireFormat(t *testing.T) { + root := uint64(99) + sync := TowerSyncUpdate{ + Lockouts: []Lockout{ + {Slot: 100, ConfirmationCount: 3}, + {Slot: 101, ConfirmationCount: 2}, + }, + Root: &root, + Hash: hashOf(0xAA), + BlockID: hashOf(0xBB), + } + buf := new(bytes.Buffer) + require.NoError(t, sync.MarshalWithEncoder(bin.NewBinEncoder(buf))) + + // Layout: + // u64 root = 99 (8 bytes) + // u8 lockout_count = 2 (1 byte) + // varint 1, u8 3 (2 bytes) + // varint 1, u8 2 (2 bytes) + // hash (32 bytes of 0xAA) + // u8 timestamp None (1 byte) + // block_id (32 bytes of 0xBB) + // Total: 8 + 1 + 2 + 2 + 32 + 1 + 32 = 78 bytes + require.Len(t, buf.Bytes(), 78) + + // Last 32 bytes should be block_id + blockID := hashOf(0xBB) + require.Equal(t, blockID[:], buf.Bytes()[len(buf.Bytes())-32:]) + + // Round-trip + decoded := new(TowerSyncUpdate) + require.NoError(t, decoded.UnmarshalWithDecoder(bin.NewBinDecoder(buf.Bytes()))) + require.Len(t, decoded.Lockouts, 2) + require.Equal(t, uint64(100), decoded.Lockouts[0].Slot) + require.Equal(t, uint64(101), decoded.Lockouts[1].Slot) + require.Equal(t, hashOf(0xAA), decoded.Hash) + require.Equal(t, hashOf(0xBB), decoded.BlockID) +} + +// --- nil marshal safety --- + +func TestMarshal_NilFields_ReturnError(t *testing.T) { + // Each of these has a nil required field — MarshalWithEncoder should + // return an error instead of panicking. + cases := []struct { + name string + inst interface{ Build() *Instruction } + }{ + {"InitializeAccount", &InitializeAccount{}}, + {"Authorize", &Authorize{}}, + {"UpdateVoteState", &UpdateVoteState{}}, + {"UpdateVoteStateSwitch", &UpdateVoteStateSwitch{}}, + {"CompactUpdateVoteState", &CompactUpdateVoteState{}}, + {"CompactUpdateVoteStateSwitch", &CompactUpdateVoteStateSwitch{}}, + {"TowerSync", &TowerSync{}}, + {"TowerSyncSwitch", &TowerSyncSwitch{}}, + {"AuthorizeWithSeed", &AuthorizeWithSeed{}}, + {"AuthorizeCheckedWithSeed", &AuthorizeCheckedWithSeed{}}, + {"InitializeAccountV2", &InitializeAccountV2{}}, + {"UpdateCommission", &UpdateCommission{}}, + {"UpdateCommissionCollector", &UpdateCommissionCollector{}}, + {"UpdateCommissionBps", &UpdateCommissionBps{}}, + {"DepositDelegatorRewards", &DepositDelegatorRewards{}}, + {"AuthorizeChecked", &AuthorizeChecked{}}, + {"VoteSwitch", &VoteSwitch{}}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + _, err := tc.inst.Build().Data() + require.Error(t, err, "expected error for nil marshal of %s", tc.name) + }) + } +} diff --git a/programs/vote/instructions.go b/programs/vote/instructions.go index ed518c3b..1b21420f 100644 --- a/programs/vote/instructions.go +++ b/programs/vote/instructions.go @@ -54,18 +54,26 @@ func (inst *Instruction) EncodeToTree(parent treeout.Branches) { var InstructionImplDef = bin.NewVariantDefinition( bin.Uint32TypeIDEncoding, []bin.VariantType{ - { - "InitializeAccount", (*InitializeAccount)(nil), - }, - { - "Authorize", (*Authorize)(nil), - }, - { - "Vote", (*Vote)(nil), - }, - { - "Withdraw", (*Withdraw)(nil), - }, + {Name: "InitializeAccount", Type: (*InitializeAccount)(nil)}, + {Name: "Authorize", Type: (*Authorize)(nil)}, + {Name: "Vote", Type: (*Vote)(nil)}, + {Name: "Withdraw", Type: (*Withdraw)(nil)}, + {Name: "UpdateValidatorIdentity", Type: (*UpdateValidatorIdentity)(nil)}, + {Name: "UpdateCommission", Type: (*UpdateCommission)(nil)}, + {Name: "VoteSwitch", Type: (*VoteSwitch)(nil)}, + {Name: "AuthorizeChecked", Type: (*AuthorizeChecked)(nil)}, + {Name: "UpdateVoteState", Type: (*UpdateVoteState)(nil)}, + {Name: "UpdateVoteStateSwitch", Type: (*UpdateVoteStateSwitch)(nil)}, + {Name: "AuthorizeWithSeed", Type: (*AuthorizeWithSeed)(nil)}, + {Name: "AuthorizeCheckedWithSeed", Type: (*AuthorizeCheckedWithSeed)(nil)}, + {Name: "CompactUpdateVoteState", Type: (*CompactUpdateVoteState)(nil)}, + {Name: "CompactUpdateVoteStateSwitch", Type: (*CompactUpdateVoteStateSwitch)(nil)}, + {Name: "TowerSync", Type: (*TowerSync)(nil)}, + {Name: "TowerSyncSwitch", Type: (*TowerSyncSwitch)(nil)}, + {Name: "InitializeAccountV2", Type: (*InitializeAccountV2)(nil)}, + {Name: "UpdateCommissionCollector", Type: (*UpdateCommissionCollector)(nil)}, + {Name: "UpdateCommissionBps", Type: (*UpdateCommissionBps)(nil)}, + {Name: "DepositDelegatorRewards", Type: (*DepositDelegatorRewards)(nil)}, }, ) diff --git a/programs/vote/instructions_test.go b/programs/vote/instructions_test.go new file mode 100644 index 00000000..43bd3d53 --- /dev/null +++ b/programs/vote/instructions_test.go @@ -0,0 +1,37 @@ +package vote + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestInstructionIDValues(t *testing.T) { + require.Equal(t, uint32(0), Instruction_InitializeAccount) + require.Equal(t, uint32(1), Instruction_Authorize) + require.Equal(t, uint32(2), Instruction_Vote) + require.Equal(t, uint32(3), Instruction_Withdraw) + require.Equal(t, uint32(4), Instruction_UpdateValidatorIdentity) + require.Equal(t, uint32(5), Instruction_UpdateCommission) + require.Equal(t, uint32(6), Instruction_VoteSwitch) + require.Equal(t, uint32(7), Instruction_AuthorizeChecked) + require.Equal(t, uint32(8), Instruction_UpdateVoteState) + require.Equal(t, uint32(9), Instruction_UpdateVoteStateSwitch) + require.Equal(t, uint32(10), Instruction_AuthorizeWithSeed) + require.Equal(t, uint32(11), Instruction_AuthorizeCheckedWithSeed) + require.Equal(t, uint32(12), Instruction_CompactUpdateVoteState) + require.Equal(t, uint32(13), Instruction_CompactUpdateVoteStateSwitch) + require.Equal(t, uint32(14), Instruction_TowerSync) + require.Equal(t, uint32(15), Instruction_TowerSyncSwitch) + require.Equal(t, uint32(16), Instruction_InitializeAccountV2) + require.Equal(t, uint32(17), Instruction_UpdateCommissionCollector) + require.Equal(t, uint32(18), Instruction_UpdateCommissionBps) + require.Equal(t, uint32(19), Instruction_DepositDelegatorRewards) +} + +func TestInstructionIDToName(t *testing.T) { + require.Equal(t, "InitializeAccount", InstructionIDToName(0)) + require.Equal(t, "TowerSync", InstructionIDToName(14)) + require.Equal(t, "DepositDelegatorRewards", InstructionIDToName(19)) + require.Equal(t, "", InstructionIDToName(99)) +} diff --git a/programs/vote/state.go b/programs/vote/state.go new file mode 100644 index 00000000..fe6523c2 --- /dev/null +++ b/programs/vote/state.go @@ -0,0 +1,644 @@ +// Copyright 2024 github.com/gagliardetto +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package vote + +import ( + "encoding/binary" + "fmt" + + bin "github.com/gagliardetto/binary" + "github.com/gagliardetto/solana-go" +) + +// VoteStateVersion identifies which version of VoteState is stored. +type VoteStateVersion uint32 + +const ( + VoteStateVersionV0_23_5 VoteStateVersion = 0 // rejected on deserialize + VoteStateVersionV1_14_11 VoteStateVersion = 1 + VoteStateVersionV3 VoteStateVersion = 2 + VoteStateVersionV4 VoteStateVersion = 3 +) + +// BlockTimestamp pairs a slot with a unix timestamp. +type BlockTimestamp struct { + Slot uint64 + Timestamp int64 +} + +func (b *BlockTimestamp) UnmarshalWithDecoder(dec *bin.Decoder) error { + var err error + b.Slot, err = dec.ReadUint64(binary.LittleEndian) + if err != nil { + return err + } + b.Timestamp, err = dec.ReadInt64(binary.LittleEndian) + return err +} + +func (b BlockTimestamp) MarshalWithEncoder(enc *bin.Encoder) error { + if err := enc.WriteUint64(b.Slot, binary.LittleEndian); err != nil { + return err + } + return enc.WriteInt64(b.Timestamp, binary.LittleEndian) +} + +// AuthorizedVoters is a map of epoch to authorized voter public key. +// In the on-chain representation this is a BTreeMap with u64 length prefix. +type AuthorizedVoters struct { + Voters []AuthorizedVoter +} + +// AuthorizedVoter is a single epoch -> pubkey mapping. +type AuthorizedVoter struct { + Epoch uint64 + Pubkey solana.PublicKey +} + +func (av *AuthorizedVoters) UnmarshalWithDecoder(dec *bin.Decoder) error { + count, err := dec.ReadUint64(binary.LittleEndian) + if err != nil { + return err + } + av.Voters = make([]AuthorizedVoter, count) + for i := range count { + av.Voters[i].Epoch, err = dec.ReadUint64(binary.LittleEndian) + if err != nil { + return err + } + b, err := dec.ReadNBytes(32) + if err != nil { + return err + } + av.Voters[i].Pubkey = solana.PublicKeyFromBytes(b) + } + return nil +} + +func (av AuthorizedVoters) MarshalWithEncoder(enc *bin.Encoder) error { + if err := enc.WriteUint64(uint64(len(av.Voters)), binary.LittleEndian); err != nil { + return err + } + for _, v := range av.Voters { + if err := enc.WriteUint64(v.Epoch, binary.LittleEndian); err != nil { + return err + } + if err := enc.WriteBytes(v.Pubkey[:], false); err != nil { + return err + } + } + return nil +} + +// PriorVoter is a single entry in the prior voters circular buffer. +type PriorVoter struct { + Pubkey solana.PublicKey + StartEpoch uint64 + EndEpoch uint64 +} + +// PriorVotersCircBuf is a circular buffer of 32 prior voters. +// +// The zero value is NOT a valid empty buffer — use NewPriorVotersCircBuf() +// to construct a correctly-defaulted instance (Idx = MAX_ITEMS - 1, IsEmpty = true). +// This matches the Rust CircBuf::default() behavior. +type PriorVotersCircBuf struct { + Buf [CircBufMaxItems]PriorVoter + Idx uint64 + IsEmpty bool +} + +// NewPriorVotersCircBuf returns an empty PriorVotersCircBuf with the same +// default values as the Rust CircBuf::default(): idx = MAX_ITEMS - 1, +// is_empty = true. +func NewPriorVotersCircBuf() PriorVotersCircBuf { + return PriorVotersCircBuf{ + Idx: CircBufMaxItems - 1, + IsEmpty: true, + } +} + +func (c *PriorVotersCircBuf) UnmarshalWithDecoder(dec *bin.Decoder) error { + for i := range CircBufMaxItems { + b, err := dec.ReadNBytes(32) + if err != nil { + return err + } + c.Buf[i].Pubkey = solana.PublicKeyFromBytes(b) + c.Buf[i].StartEpoch, err = dec.ReadUint64(binary.LittleEndian) + if err != nil { + return err + } + c.Buf[i].EndEpoch, err = dec.ReadUint64(binary.LittleEndian) + if err != nil { + return err + } + } + var err error + c.Idx, err = dec.ReadUint64(binary.LittleEndian) + if err != nil { + return err + } + c.IsEmpty, err = dec.ReadBool() + return err +} + +func (c PriorVotersCircBuf) MarshalWithEncoder(enc *bin.Encoder) error { + for i := range CircBufMaxItems { + if err := enc.WriteBytes(c.Buf[i].Pubkey[:], false); err != nil { + return err + } + if err := enc.WriteUint64(c.Buf[i].StartEpoch, binary.LittleEndian); err != nil { + return err + } + if err := enc.WriteUint64(c.Buf[i].EndEpoch, binary.LittleEndian); err != nil { + return err + } + } + if err := enc.WriteUint64(c.Idx, binary.LittleEndian); err != nil { + return err + } + return enc.WriteBool(c.IsEmpty) +} + +// EpochCredit is a single (epoch, credits, prev_credits) tuple. +type EpochCredit struct { + Epoch uint64 + Credits uint64 + PrevCredits uint64 +} + +// decodeEpochCredits decodes a Vec<(Epoch, u64, u64)> with a u64 length prefix. +func decodeEpochCredits(dec *bin.Decoder) ([]EpochCredit, error) { + count, err := dec.ReadUint64(binary.LittleEndian) + if err != nil { + return nil, err + } + out := make([]EpochCredit, count) + for i := range count { + out[i].Epoch, err = dec.ReadUint64(binary.LittleEndian) + if err != nil { + return nil, err + } + out[i].Credits, err = dec.ReadUint64(binary.LittleEndian) + if err != nil { + return nil, err + } + out[i].PrevCredits, err = dec.ReadUint64(binary.LittleEndian) + if err != nil { + return nil, err + } + } + return out, nil +} + +func encodeEpochCredits(enc *bin.Encoder, credits []EpochCredit) error { + if err := enc.WriteUint64(uint64(len(credits)), binary.LittleEndian); err != nil { + return err + } + for _, c := range credits { + if err := enc.WriteUint64(c.Epoch, binary.LittleEndian); err != nil { + return err + } + if err := enc.WriteUint64(c.Credits, binary.LittleEndian); err != nil { + return err + } + if err := enc.WriteUint64(c.PrevCredits, binary.LittleEndian); err != nil { + return err + } + } + return nil +} + +// VoteState1_14_11 is the legacy vote state (tag 1, size 3731). +// Uses VecDeque without latency bytes. +type VoteState1_14_11 struct { + NodePubkey solana.PublicKey + AuthorizedWithdrawer solana.PublicKey + Commission uint8 + Votes []Lockout + RootSlot *uint64 + AuthorizedVoters AuthorizedVoters + PriorVoters PriorVotersCircBuf + EpochCredits []EpochCredit + LastTimestamp BlockTimestamp +} + +// VoteStateV3 is the current production vote state (tag 2, size 3762). +// Uses VecDeque. +type VoteStateV3 struct { + NodePubkey solana.PublicKey + AuthorizedWithdrawer solana.PublicKey + Commission uint8 + Votes []LandedVote + RootSlot *uint64 + AuthorizedVoters AuthorizedVoters + PriorVoters PriorVotersCircBuf + EpochCredits []EpochCredit + LastTimestamp BlockTimestamp +} + +// VoteStateV4 is the newest vote state (tag 3, size 3762). +// Adds BLS pubkey, commission basis points, collector accounts, removes prior voters. +type VoteStateV4 struct { + NodePubkey solana.PublicKey + AuthorizedWithdrawer solana.PublicKey + InflationRewardsCollector solana.PublicKey + BlockRevenueCollector solana.PublicKey + InflationRewardsCommissionBps uint16 + BlockRevenueCommissionBps uint16 + PendingDelegatorRewards uint64 + BLSPubkeyCompressed *[BLS_PUBLIC_KEY_COMPRESSED_SIZE]byte + Votes []LandedVote + RootSlot *uint64 + AuthorizedVoters AuthorizedVoters + EpochCredits []EpochCredit + LastTimestamp BlockTimestamp +} + +// VoteStateVersions is a tagged union holding one of the vote state versions. +type VoteStateVersions struct { + Version VoteStateVersion + V1_14_11 *VoteState1_14_11 + V3 *VoteStateV3 + V4 *VoteStateV4 +} + +func decodeOptionU64(dec *bin.Decoder) (*uint64, error) { + has, err := dec.ReadUint8() + if err != nil { + return nil, err + } + if has == 0 { + return nil, nil + } + if has != 1 { + return nil, fmt.Errorf("invalid Option discriminant: %d", has) + } + v, err := dec.ReadUint64(binary.LittleEndian) + if err != nil { + return nil, err + } + return &v, nil +} + +func encodeOptionU64(enc *bin.Encoder, v *uint64) error { + if v == nil { + return enc.WriteUint8(0) + } + if err := enc.WriteUint8(1); err != nil { + return err + } + return enc.WriteUint64(*v, binary.LittleEndian) +} + +func (s *VoteState1_14_11) unmarshalBody(dec *bin.Decoder) error { + b, err := dec.ReadNBytes(32) + if err != nil { + return err + } + s.NodePubkey = solana.PublicKeyFromBytes(b) + b, err = dec.ReadNBytes(32) + if err != nil { + return err + } + s.AuthorizedWithdrawer = solana.PublicKeyFromBytes(b) + s.Commission, err = dec.ReadUint8() + if err != nil { + return err + } + count, err := dec.ReadUint64(binary.LittleEndian) + if err != nil { + return err + } + s.Votes = make([]Lockout, count) + for i := range count { + if err := s.Votes[i].UnmarshalWithDecoder(dec); err != nil { + return err + } + } + s.RootSlot, err = decodeOptionU64(dec) + if err != nil { + return err + } + if err := s.AuthorizedVoters.UnmarshalWithDecoder(dec); err != nil { + return err + } + if err := s.PriorVoters.UnmarshalWithDecoder(dec); err != nil { + return err + } + s.EpochCredits, err = decodeEpochCredits(dec) + if err != nil { + return err + } + return s.LastTimestamp.UnmarshalWithDecoder(dec) +} + +func (s VoteState1_14_11) marshalBody(enc *bin.Encoder) error { + if err := enc.WriteBytes(s.NodePubkey[:], false); err != nil { + return err + } + if err := enc.WriteBytes(s.AuthorizedWithdrawer[:], false); err != nil { + return err + } + if err := enc.WriteUint8(s.Commission); err != nil { + return err + } + if err := enc.WriteUint64(uint64(len(s.Votes)), binary.LittleEndian); err != nil { + return err + } + for _, v := range s.Votes { + if err := v.MarshalWithEncoder(enc); err != nil { + return err + } + } + if err := encodeOptionU64(enc, s.RootSlot); err != nil { + return err + } + if err := s.AuthorizedVoters.MarshalWithEncoder(enc); err != nil { + return err + } + if err := s.PriorVoters.MarshalWithEncoder(enc); err != nil { + return err + } + if err := encodeEpochCredits(enc, s.EpochCredits); err != nil { + return err + } + return s.LastTimestamp.MarshalWithEncoder(enc) +} + +func (s *VoteStateV3) unmarshalBody(dec *bin.Decoder) error { + b, err := dec.ReadNBytes(32) + if err != nil { + return err + } + s.NodePubkey = solana.PublicKeyFromBytes(b) + b, err = dec.ReadNBytes(32) + if err != nil { + return err + } + s.AuthorizedWithdrawer = solana.PublicKeyFromBytes(b) + s.Commission, err = dec.ReadUint8() + if err != nil { + return err + } + count, err := dec.ReadUint64(binary.LittleEndian) + if err != nil { + return err + } + s.Votes = make([]LandedVote, count) + for i := range count { + if err := s.Votes[i].UnmarshalWithDecoder(dec); err != nil { + return err + } + } + s.RootSlot, err = decodeOptionU64(dec) + if err != nil { + return err + } + if err := s.AuthorizedVoters.UnmarshalWithDecoder(dec); err != nil { + return err + } + if err := s.PriorVoters.UnmarshalWithDecoder(dec); err != nil { + return err + } + s.EpochCredits, err = decodeEpochCredits(dec) + if err != nil { + return err + } + return s.LastTimestamp.UnmarshalWithDecoder(dec) +} + +func (s VoteStateV3) marshalBody(enc *bin.Encoder) error { + if err := enc.WriteBytes(s.NodePubkey[:], false); err != nil { + return err + } + if err := enc.WriteBytes(s.AuthorizedWithdrawer[:], false); err != nil { + return err + } + if err := enc.WriteUint8(s.Commission); err != nil { + return err + } + if err := enc.WriteUint64(uint64(len(s.Votes)), binary.LittleEndian); err != nil { + return err + } + for _, v := range s.Votes { + if err := v.MarshalWithEncoder(enc); err != nil { + return err + } + } + if err := encodeOptionU64(enc, s.RootSlot); err != nil { + return err + } + if err := s.AuthorizedVoters.MarshalWithEncoder(enc); err != nil { + return err + } + if err := s.PriorVoters.MarshalWithEncoder(enc); err != nil { + return err + } + if err := encodeEpochCredits(enc, s.EpochCredits); err != nil { + return err + } + return s.LastTimestamp.MarshalWithEncoder(enc) +} + +func (s *VoteStateV4) unmarshalBody(dec *bin.Decoder) error { + b, err := dec.ReadNBytes(32) + if err != nil { + return err + } + s.NodePubkey = solana.PublicKeyFromBytes(b) + b, err = dec.ReadNBytes(32) + if err != nil { + return err + } + s.AuthorizedWithdrawer = solana.PublicKeyFromBytes(b) + b, err = dec.ReadNBytes(32) + if err != nil { + return err + } + s.InflationRewardsCollector = solana.PublicKeyFromBytes(b) + b, err = dec.ReadNBytes(32) + if err != nil { + return err + } + s.BlockRevenueCollector = solana.PublicKeyFromBytes(b) + s.InflationRewardsCommissionBps, err = dec.ReadUint16(binary.LittleEndian) + if err != nil { + return err + } + s.BlockRevenueCommissionBps, err = dec.ReadUint16(binary.LittleEndian) + if err != nil { + return err + } + s.PendingDelegatorRewards, err = dec.ReadUint64(binary.LittleEndian) + if err != nil { + return err + } + // Option<[u8; 48]> + hasBls, err := dec.ReadUint8() + if err != nil { + return err + } + if hasBls == 1 { + pk := new([BLS_PUBLIC_KEY_COMPRESSED_SIZE]byte) + b, err := dec.ReadNBytes(BLS_PUBLIC_KEY_COMPRESSED_SIZE) + if err != nil { + return err + } + copy(pk[:], b) + s.BLSPubkeyCompressed = pk + } else if hasBls != 0 { + return fmt.Errorf("invalid Option discriminant: %d", hasBls) + } + count, err := dec.ReadUint64(binary.LittleEndian) + if err != nil { + return err + } + s.Votes = make([]LandedVote, count) + for i := range count { + if err := s.Votes[i].UnmarshalWithDecoder(dec); err != nil { + return err + } + } + s.RootSlot, err = decodeOptionU64(dec) + if err != nil { + return err + } + if err := s.AuthorizedVoters.UnmarshalWithDecoder(dec); err != nil { + return err + } + s.EpochCredits, err = decodeEpochCredits(dec) + if err != nil { + return err + } + return s.LastTimestamp.UnmarshalWithDecoder(dec) +} + +func (s VoteStateV4) marshalBody(enc *bin.Encoder) error { + if err := enc.WriteBytes(s.NodePubkey[:], false); err != nil { + return err + } + if err := enc.WriteBytes(s.AuthorizedWithdrawer[:], false); err != nil { + return err + } + if err := enc.WriteBytes(s.InflationRewardsCollector[:], false); err != nil { + return err + } + if err := enc.WriteBytes(s.BlockRevenueCollector[:], false); err != nil { + return err + } + if err := enc.WriteUint16(s.InflationRewardsCommissionBps, binary.LittleEndian); err != nil { + return err + } + if err := enc.WriteUint16(s.BlockRevenueCommissionBps, binary.LittleEndian); err != nil { + return err + } + if err := enc.WriteUint64(s.PendingDelegatorRewards, binary.LittleEndian); err != nil { + return err + } + if s.BLSPubkeyCompressed == nil { + if err := enc.WriteUint8(0); err != nil { + return err + } + } else { + if err := enc.WriteUint8(1); err != nil { + return err + } + if err := enc.WriteBytes(s.BLSPubkeyCompressed[:], false); err != nil { + return err + } + } + if err := enc.WriteUint64(uint64(len(s.Votes)), binary.LittleEndian); err != nil { + return err + } + for _, v := range s.Votes { + if err := v.MarshalWithEncoder(enc); err != nil { + return err + } + } + if err := encodeOptionU64(enc, s.RootSlot); err != nil { + return err + } + if err := s.AuthorizedVoters.MarshalWithEncoder(enc); err != nil { + return err + } + if err := encodeEpochCredits(enc, s.EpochCredits); err != nil { + return err + } + return s.LastTimestamp.MarshalWithEncoder(enc) +} + +// UnmarshalWithDecoder reads the version discriminator and dispatches to the appropriate variant. +func (v *VoteStateVersions) UnmarshalWithDecoder(dec *bin.Decoder) error { + raw, err := dec.ReadUint32(binary.LittleEndian) + if err != nil { + return err + } + v.Version = VoteStateVersion(raw) + switch v.Version { + case VoteStateVersionV1_14_11: + v.V1_14_11 = new(VoteState1_14_11) + return v.V1_14_11.unmarshalBody(dec) + case VoteStateVersionV3: + v.V3 = new(VoteStateV3) + return v.V3.unmarshalBody(dec) + case VoteStateVersionV4: + v.V4 = new(VoteStateV4) + return v.V4.unmarshalBody(dec) + case VoteStateVersionV0_23_5: + return fmt.Errorf("vote state version V0_23_5 is not supported") + default: + return fmt.Errorf("unknown vote state version: %d", v.Version) + } +} + +// MarshalWithEncoder writes the version discriminator and the selected variant. +func (v VoteStateVersions) MarshalWithEncoder(enc *bin.Encoder) error { + if err := enc.WriteUint32(uint32(v.Version), binary.LittleEndian); err != nil { + return err + } + switch v.Version { + case VoteStateVersionV1_14_11: + if v.V1_14_11 == nil { + return fmt.Errorf("V1_14_11 field is nil") + } + return v.V1_14_11.marshalBody(enc) + case VoteStateVersionV3: + if v.V3 == nil { + return fmt.Errorf("V3 field is nil") + } + return v.V3.marshalBody(enc) + case VoteStateVersionV4: + if v.V4 == nil { + return fmt.Errorf("V4 field is nil") + } + return v.V4.marshalBody(enc) + default: + return fmt.Errorf("cannot marshal vote state version: %d", v.Version) + } +} + +// DecodeVoteAccount decodes a vote account's raw data into a VoteStateVersions. +func DecodeVoteAccount(data []byte) (*VoteStateVersions, error) { + if len(data) < 4 { + return nil, fmt.Errorf("vote account data too short: %d", len(data)) + } + state := new(VoteStateVersions) + if err := state.UnmarshalWithDecoder(bin.NewBinDecoder(data)); err != nil { + return nil, fmt.Errorf("unable to decode vote account: %w", err) + } + return state, nil +} diff --git a/programs/vote/state_test.go b/programs/vote/state_test.go new file mode 100644 index 00000000..1b54e27f --- /dev/null +++ b/programs/vote/state_test.go @@ -0,0 +1,216 @@ +package vote + +import ( + "bytes" + "testing" + + bin "github.com/gagliardetto/binary" + "github.com/stretchr/testify/require" +) + +func TestVoteStateV3_RoundTrip(t *testing.T) { + root := uint64(100) + state := VoteStateVersions{ + Version: VoteStateVersionV3, + V3: &VoteStateV3{ + NodePubkey: pubkeyOf(1), + AuthorizedWithdrawer: pubkeyOf(2), + Commission: 10, + Votes: []LandedVote{ + {Latency: 2, Lockout: Lockout{Slot: 100, ConfirmationCount: 3}}, + {Latency: 1, Lockout: Lockout{Slot: 101, ConfirmationCount: 2}}, + }, + RootSlot: &root, + AuthorizedVoters: AuthorizedVoters{ + Voters: []AuthorizedVoter{ + {Epoch: 42, Pubkey: pubkeyOf(3)}, + }, + }, + PriorVoters: PriorVotersCircBuf{IsEmpty: true}, + EpochCredits: []EpochCredit{ + {Epoch: 1, Credits: 1000, PrevCredits: 0}, + {Epoch: 2, Credits: 2000, PrevCredits: 1000}, + }, + LastTimestamp: BlockTimestamp{Slot: 500, Timestamp: 1700000000}, + }, + } + + buf := new(bytes.Buffer) + require.NoError(t, state.MarshalWithEncoder(bin.NewBinEncoder(buf))) + + decoded, err := DecodeVoteAccount(buf.Bytes()) + require.NoError(t, err) + require.Equal(t, VoteStateVersionV3, decoded.Version) + require.NotNil(t, decoded.V3) + require.Equal(t, pubkeyOf(1), decoded.V3.NodePubkey) + require.Equal(t, pubkeyOf(2), decoded.V3.AuthorizedWithdrawer) + require.Equal(t, uint8(10), decoded.V3.Commission) + require.Len(t, decoded.V3.Votes, 2) + require.Equal(t, uint8(2), decoded.V3.Votes[0].Latency) + require.Equal(t, uint64(100), decoded.V3.Votes[0].Lockout.Slot) + require.NotNil(t, decoded.V3.RootSlot) + require.Equal(t, uint64(100), *decoded.V3.RootSlot) + require.Len(t, decoded.V3.AuthorizedVoters.Voters, 1) + require.Equal(t, uint64(42), decoded.V3.AuthorizedVoters.Voters[0].Epoch) + require.Len(t, decoded.V3.EpochCredits, 2) + require.Equal(t, uint64(1000), decoded.V3.EpochCredits[0].Credits) + require.Equal(t, uint64(500), decoded.V3.LastTimestamp.Slot) + require.Equal(t, int64(1700000000), decoded.V3.LastTimestamp.Timestamp) +} + +func TestVoteStateV1_14_11_RoundTrip(t *testing.T) { + state := VoteStateVersions{ + Version: VoteStateVersionV1_14_11, + V1_14_11: &VoteState1_14_11{ + NodePubkey: pubkeyOf(1), + AuthorizedWithdrawer: pubkeyOf(2), + Commission: 5, + Votes: []Lockout{ + {Slot: 100, ConfirmationCount: 3}, + }, + AuthorizedVoters: AuthorizedVoters{Voters: []AuthorizedVoter{}}, + PriorVoters: PriorVotersCircBuf{IsEmpty: true}, + EpochCredits: []EpochCredit{}, + LastTimestamp: BlockTimestamp{}, + }, + } + + buf := new(bytes.Buffer) + require.NoError(t, state.MarshalWithEncoder(bin.NewBinEncoder(buf))) + + decoded, err := DecodeVoteAccount(buf.Bytes()) + require.NoError(t, err) + require.Equal(t, VoteStateVersionV1_14_11, decoded.Version) + require.NotNil(t, decoded.V1_14_11) + require.Equal(t, uint8(5), decoded.V1_14_11.Commission) + require.Len(t, decoded.V1_14_11.Votes, 1) + require.Equal(t, uint64(100), decoded.V1_14_11.Votes[0].Slot) +} + +func TestVoteStateV4_RoundTrip(t *testing.T) { + blsPk := [BLS_PUBLIC_KEY_COMPRESSED_SIZE]byte{} + for i := range blsPk { + blsPk[i] = 0xAB + } + state := VoteStateVersions{ + Version: VoteStateVersionV4, + V4: &VoteStateV4{ + NodePubkey: pubkeyOf(1), + AuthorizedWithdrawer: pubkeyOf(2), + InflationRewardsCollector: pubkeyOf(3), + BlockRevenueCollector: pubkeyOf(4), + InflationRewardsCommissionBps: 500, + BlockRevenueCommissionBps: 1000, + PendingDelegatorRewards: 123456, + BLSPubkeyCompressed: &blsPk, + Votes: []LandedVote{}, + AuthorizedVoters: AuthorizedVoters{Voters: []AuthorizedVoter{}}, + EpochCredits: []EpochCredit{}, + LastTimestamp: BlockTimestamp{}, + }, + } + + buf := new(bytes.Buffer) + require.NoError(t, state.MarshalWithEncoder(bin.NewBinEncoder(buf))) + + decoded, err := DecodeVoteAccount(buf.Bytes()) + require.NoError(t, err) + require.Equal(t, VoteStateVersionV4, decoded.Version) + require.NotNil(t, decoded.V4) + require.Equal(t, pubkeyOf(3), decoded.V4.InflationRewardsCollector) + require.Equal(t, pubkeyOf(4), decoded.V4.BlockRevenueCollector) + require.Equal(t, uint16(500), decoded.V4.InflationRewardsCommissionBps) + require.Equal(t, uint16(1000), decoded.V4.BlockRevenueCommissionBps) + require.Equal(t, uint64(123456), decoded.V4.PendingDelegatorRewards) + require.NotNil(t, decoded.V4.BLSPubkeyCompressed) + require.Equal(t, blsPk, *decoded.V4.BLSPubkeyCompressed) +} + +func TestVoteStateV4_NoBLSKey(t *testing.T) { + state := VoteStateVersions{ + Version: VoteStateVersionV4, + V4: &VoteStateV4{ + NodePubkey: pubkeyOf(1), + AuthorizedWithdrawer: pubkeyOf(2), + Votes: []LandedVote{}, + AuthorizedVoters: AuthorizedVoters{Voters: []AuthorizedVoter{}}, + EpochCredits: []EpochCredit{}, + LastTimestamp: BlockTimestamp{}, + }, + } + buf := new(bytes.Buffer) + require.NoError(t, state.MarshalWithEncoder(bin.NewBinEncoder(buf))) + + decoded, err := DecodeVoteAccount(buf.Bytes()) + require.NoError(t, err) + require.Nil(t, decoded.V4.BLSPubkeyCompressed) +} + +// Ported from vote_state_v3.rs::test_invalid_option_bool_discriminants: +// In V3, root_slot Option starts at byte offset 77. +func TestVoteStateV3_RootSlotOffset(t *testing.T) { + const rootSlotOffset = 77 + state := VoteStateVersions{ + Version: VoteStateVersionV3, + V3: &VoteStateV3{ + NodePubkey: pubkeyOf(0), + AuthorizedWithdrawer: pubkeyOf(0), + Votes: []LandedVote{}, + AuthorizedVoters: AuthorizedVoters{Voters: []AuthorizedVoter{}}, + PriorVoters: PriorVotersCircBuf{IsEmpty: true}, + EpochCredits: []EpochCredit{}, + LastTimestamp: BlockTimestamp{}, + }, + } + buf := new(bytes.Buffer) + require.NoError(t, state.MarshalWithEncoder(bin.NewBinEncoder(buf))) + data := buf.Bytes() + + // At offset 77 we should have the Option discriminator for root_slot. + // With RootSlot == nil, discriminator should be 0. + require.GreaterOrEqual(t, len(data), rootSlotOffset+1) + require.Equal(t, uint8(0), data[rootSlotOffset]) +} + +// Ported from vote_state_v4.rs::test_invalid_option_discriminants: +// In V4, BLS Option discriminant is at byte offset 144. +func TestVoteStateV4_BLSOffset(t *testing.T) { + const blsOffset = 144 + state := VoteStateVersions{ + Version: VoteStateVersionV4, + V4: &VoteStateV4{ + NodePubkey: pubkeyOf(0), + AuthorizedWithdrawer: pubkeyOf(0), + Votes: []LandedVote{}, + AuthorizedVoters: AuthorizedVoters{Voters: []AuthorizedVoter{}}, + EpochCredits: []EpochCredit{}, + LastTimestamp: BlockTimestamp{}, + }, + } + buf := new(bytes.Buffer) + require.NoError(t, state.MarshalWithEncoder(bin.NewBinEncoder(buf))) + data := buf.Bytes() + + require.GreaterOrEqual(t, len(data), blsOffset+1) + // BLSPubkeyCompressed is nil => discriminator = 0 + require.Equal(t, uint8(0), data[blsOffset]) +} + +func TestDecodeVoteAccount_Uninitialized_Rejected(t *testing.T) { + data := make([]byte, 4) // tag = 0 (V0_23_5) + _, err := DecodeVoteAccount(data) + require.Error(t, err) + require.Contains(t, err.Error(), "V0_23_5") +} + +func TestDecodeVoteAccount_UnknownVersion(t *testing.T) { + data := []byte{99, 0, 0, 0} + _, err := DecodeVoteAccount(data) + require.Error(t, err) + require.Contains(t, err.Error(), "unknown vote state version") +} + +func TestDecodeVoteAccount_TooShort(t *testing.T) { + _, err := DecodeVoteAccount([]byte{1, 2}) + require.Error(t, err) +} diff --git a/programs/vote/testing_utils_test.go b/programs/vote/testing_utils_test.go new file mode 100644 index 00000000..34793fea --- /dev/null +++ b/programs/vote/testing_utils_test.go @@ -0,0 +1,47 @@ +package vote + +import ( + "encoding/binary" + + "github.com/gagliardetto/solana-go" +) + +func pubkeyOf(v byte) solana.PublicKey { + var pk solana.PublicKey + for i := range pk { + pk[i] = v + } + return pk +} + +func hashOf(v byte) solana.Hash { + var h solana.Hash + for i := range h { + h[i] = v + } + return h +} + +func u32LE(v uint32) []byte { + b := make([]byte, 4) + binary.LittleEndian.PutUint32(b, v) + return b +} + +func u64LE(v uint64) []byte { + b := make([]byte, 8) + binary.LittleEndian.PutUint64(b, v) + return b +} + +func concat(parts ...[]byte) []byte { + var out []byte + for _, p := range parts { + out = append(out, p...) + } + return out +} + +func encodeInst(inst interface{ Build() *Instruction }) ([]byte, error) { + return inst.Build().Data() +} diff --git a/programs/vote/types.go b/programs/vote/types.go new file mode 100644 index 00000000..8d2ee3af --- /dev/null +++ b/programs/vote/types.go @@ -0,0 +1,549 @@ +// Copyright 2024 github.com/gagliardetto +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package vote + +import ( + "encoding/binary" + "errors" + "fmt" + + bin "github.com/gagliardetto/binary" + "github.com/gagliardetto/solana-go" +) + +// Constants from the official vote program. +const ( + MAX_LOCKOUT_HISTORY = 31 + INITIAL_LOCKOUT = 2 + MAX_EPOCH_CREDITS_HISTORY = 64 + VOTE_CREDITS_GRACE_SLOTS = 2 + VOTE_CREDITS_MAXIMUM_PER_SLOT = 16 + CircBufMaxItems = 32 + BLS_PUBLIC_KEY_COMPRESSED_SIZE = 48 + BLS_PROOF_OF_POSSESSION_COMPRESSED_SIZE = 96 + VoteStateV1_14_11Size = 3731 + VoteStateV3Size = 3762 + VoteStateV4Size = 3762 +) + +// Instruction IDs for VoteInstruction variants. +const ( + Instruction_InitializeAccount uint32 = iota + Instruction_Authorize + Instruction_Vote + Instruction_Withdraw + Instruction_UpdateValidatorIdentity + Instruction_UpdateCommission + Instruction_VoteSwitch + Instruction_AuthorizeChecked + Instruction_UpdateVoteState + Instruction_UpdateVoteStateSwitch + Instruction_AuthorizeWithSeed + Instruction_AuthorizeCheckedWithSeed + Instruction_CompactUpdateVoteState + Instruction_CompactUpdateVoteStateSwitch + Instruction_TowerSync + Instruction_TowerSyncSwitch + Instruction_InitializeAccountV2 + Instruction_UpdateCommissionCollector + Instruction_UpdateCommissionBps + Instruction_DepositDelegatorRewards +) + +// InstructionIDToName returns the name of the instruction given its ID. +func InstructionIDToName(id uint32) string { + switch id { + case Instruction_InitializeAccount: + return "InitializeAccount" + case Instruction_Authorize: + return "Authorize" + case Instruction_Vote: + return "Vote" + case Instruction_Withdraw: + return "Withdraw" + case Instruction_UpdateValidatorIdentity: + return "UpdateValidatorIdentity" + case Instruction_UpdateCommission: + return "UpdateCommission" + case Instruction_VoteSwitch: + return "VoteSwitch" + case Instruction_AuthorizeChecked: + return "AuthorizeChecked" + case Instruction_UpdateVoteState: + return "UpdateVoteState" + case Instruction_UpdateVoteStateSwitch: + return "UpdateVoteStateSwitch" + case Instruction_AuthorizeWithSeed: + return "AuthorizeWithSeed" + case Instruction_AuthorizeCheckedWithSeed: + return "AuthorizeCheckedWithSeed" + case Instruction_CompactUpdateVoteState: + return "CompactUpdateVoteState" + case Instruction_CompactUpdateVoteStateSwitch: + return "CompactUpdateVoteStateSwitch" + case Instruction_TowerSync: + return "TowerSync" + case Instruction_TowerSyncSwitch: + return "TowerSyncSwitch" + case Instruction_InitializeAccountV2: + return "InitializeAccountV2" + case Instruction_UpdateCommissionCollector: + return "UpdateCommissionCollector" + case Instruction_UpdateCommissionBps: + return "UpdateCommissionBps" + case Instruction_DepositDelegatorRewards: + return "DepositDelegatorRewards" + default: + return "" + } +} + +// VoteAuthorizeKind identifies the type of authorization being set. +type VoteAuthorizeKind uint32 + +const ( + VoteAuthorizeVoter VoteAuthorizeKind = 0 + VoteAuthorizeWithdrawer VoteAuthorizeKind = 1 + VoteAuthorizeVoterWithBLS VoteAuthorizeKind = 2 +) + +// VoteAuthorize represents the bincode-encoded VoteAuthorize enum. +// For Voter and Withdrawer variants, only Kind is set. +// For VoterWithBLS, BLSPubkey and BLSProofOfPossession are also set. +// +// Serialization is handled by custom MarshalWithEncoder/UnmarshalWithDecoder +// methods — struct tags are not used by this type. +type VoteAuthorize struct { + Kind VoteAuthorizeKind + BLSPubkey *[BLS_PUBLIC_KEY_COMPRESSED_SIZE]byte + BLSProofOfPossession *[BLS_PROOF_OF_POSSESSION_COMPRESSED_SIZE]byte +} + +func (va *VoteAuthorize) UnmarshalWithDecoder(dec *bin.Decoder) error { + raw, err := dec.ReadUint32(binary.LittleEndian) + if err != nil { + return err + } + va.Kind = VoteAuthorizeKind(raw) + if va.Kind == VoteAuthorizeVoterWithBLS { + pk := new([BLS_PUBLIC_KEY_COMPRESSED_SIZE]byte) + v, err := dec.ReadNBytes(BLS_PUBLIC_KEY_COMPRESSED_SIZE) + if err != nil { + return err + } + copy(pk[:], v) + va.BLSPubkey = pk + + proof := new([BLS_PROOF_OF_POSSESSION_COMPRESSED_SIZE]byte) + v, err = dec.ReadNBytes(BLS_PROOF_OF_POSSESSION_COMPRESSED_SIZE) + if err != nil { + return err + } + copy(proof[:], v) + va.BLSProofOfPossession = proof + } + return nil +} + +func (va VoteAuthorize) MarshalWithEncoder(enc *bin.Encoder) error { + if err := enc.WriteUint32(uint32(va.Kind), binary.LittleEndian); err != nil { + return err + } + if va.Kind == VoteAuthorizeVoterWithBLS { + if va.BLSPubkey == nil || va.BLSProofOfPossession == nil { + return errors.New("VoterWithBLS requires BLSPubkey and BLSProofOfPossession") + } + if err := enc.WriteBytes(va.BLSPubkey[:], false); err != nil { + return err + } + if err := enc.WriteBytes(va.BLSProofOfPossession[:], false); err != nil { + return err + } + } + return nil +} + +// CommissionKind identifies which commission bucket an operation targets. +type CommissionKind uint8 + +const ( + CommissionKindInflationRewards CommissionKind = 0 + CommissionKindBlockRevenue CommissionKind = 1 +) + +// VoteInit is the data for the InitializeAccount instruction. +type VoteInit struct { + NodePubkey solana.PublicKey + AuthorizedVoter solana.PublicKey + AuthorizedWithdrawer solana.PublicKey + Commission uint8 +} + +func (v *VoteInit) UnmarshalWithDecoder(dec *bin.Decoder) error { + b, err := dec.ReadNBytes(32) + if err != nil { + return err + } + v.NodePubkey = solana.PublicKeyFromBytes(b) + b, err = dec.ReadNBytes(32) + if err != nil { + return err + } + v.AuthorizedVoter = solana.PublicKeyFromBytes(b) + b, err = dec.ReadNBytes(32) + if err != nil { + return err + } + v.AuthorizedWithdrawer = solana.PublicKeyFromBytes(b) + v.Commission, err = dec.ReadUint8() + return err +} + +func (v VoteInit) MarshalWithEncoder(enc *bin.Encoder) error { + if err := enc.WriteBytes(v.NodePubkey[:], false); err != nil { + return err + } + if err := enc.WriteBytes(v.AuthorizedVoter[:], false); err != nil { + return err + } + if err := enc.WriteBytes(v.AuthorizedWithdrawer[:], false); err != nil { + return err + } + return enc.WriteUint8(v.Commission) +} + +// VoteInitV2 is the data for the InitializeAccountV2 instruction (Alpenglow). +type VoteInitV2 struct { + NodePubkey solana.PublicKey + AuthorizedVoter solana.PublicKey + AuthorizedVoterBLSPubkey [BLS_PUBLIC_KEY_COMPRESSED_SIZE]byte + AuthorizedVoterBLSProofOfPossession [BLS_PROOF_OF_POSSESSION_COMPRESSED_SIZE]byte + AuthorizedWithdrawer solana.PublicKey + InflationRewardsCommissionBps uint16 + BlockRevenueCommissionBps uint16 +} + +func (v *VoteInitV2) UnmarshalWithDecoder(dec *bin.Decoder) error { + b, err := dec.ReadNBytes(32) + if err != nil { + return err + } + v.NodePubkey = solana.PublicKeyFromBytes(b) + b, err = dec.ReadNBytes(32) + if err != nil { + return err + } + v.AuthorizedVoter = solana.PublicKeyFromBytes(b) + b, err = dec.ReadNBytes(BLS_PUBLIC_KEY_COMPRESSED_SIZE) + if err != nil { + return err + } + copy(v.AuthorizedVoterBLSPubkey[:], b) + b, err = dec.ReadNBytes(BLS_PROOF_OF_POSSESSION_COMPRESSED_SIZE) + if err != nil { + return err + } + copy(v.AuthorizedVoterBLSProofOfPossession[:], b) + b, err = dec.ReadNBytes(32) + if err != nil { + return err + } + v.AuthorizedWithdrawer = solana.PublicKeyFromBytes(b) + v.InflationRewardsCommissionBps, err = dec.ReadUint16(binary.LittleEndian) + if err != nil { + return err + } + v.BlockRevenueCommissionBps, err = dec.ReadUint16(binary.LittleEndian) + return err +} + +func (v VoteInitV2) MarshalWithEncoder(enc *bin.Encoder) error { + if err := enc.WriteBytes(v.NodePubkey[:], false); err != nil { + return err + } + if err := enc.WriteBytes(v.AuthorizedVoter[:], false); err != nil { + return err + } + if err := enc.WriteBytes(v.AuthorizedVoterBLSPubkey[:], false); err != nil { + return err + } + if err := enc.WriteBytes(v.AuthorizedVoterBLSProofOfPossession[:], false); err != nil { + return err + } + if err := enc.WriteBytes(v.AuthorizedWithdrawer[:], false); err != nil { + return err + } + if err := enc.WriteUint16(v.InflationRewardsCommissionBps, binary.LittleEndian); err != nil { + return err + } + return enc.WriteUint16(v.BlockRevenueCommissionBps, binary.LittleEndian) +} + +// Lockout represents a vote lockout with slot and confirmation count. +type Lockout struct { + Slot uint64 + ConfirmationCount uint32 +} + +func (l *Lockout) UnmarshalWithDecoder(dec *bin.Decoder) error { + var err error + l.Slot, err = dec.ReadUint64(binary.LittleEndian) + if err != nil { + return err + } + l.ConfirmationCount, err = dec.ReadUint32(binary.LittleEndian) + return err +} + +func (l Lockout) MarshalWithEncoder(enc *bin.Encoder) error { + if err := enc.WriteUint64(l.Slot, binary.LittleEndian); err != nil { + return err + } + return enc.WriteUint32(l.ConfirmationCount, binary.LittleEndian) +} + +// LandedVote combines vote latency with a Lockout (used in VoteStateV3 and V4). +type LandedVote struct { + Latency uint8 + Lockout Lockout +} + +func (lv *LandedVote) UnmarshalWithDecoder(dec *bin.Decoder) error { + var err error + lv.Latency, err = dec.ReadUint8() + if err != nil { + return err + } + return lv.Lockout.UnmarshalWithDecoder(dec) +} + +func (lv LandedVote) MarshalWithEncoder(enc *bin.Encoder) error { + if err := enc.WriteUint8(lv.Latency); err != nil { + return err + } + return lv.Lockout.MarshalWithEncoder(enc) +} + +// VoteStateUpdate is the data for the UpdateVoteState and related instructions. +type VoteStateUpdate struct { + Lockouts []Lockout + Root *uint64 + Hash solana.Hash + Timestamp *int64 +} + +func (u *VoteStateUpdate) UnmarshalWithDecoder(dec *bin.Decoder) error { + count, err := dec.ReadUint64(binary.LittleEndian) + if err != nil { + return err + } + u.Lockouts = make([]Lockout, count) + for i := range count { + if err := u.Lockouts[i].UnmarshalWithDecoder(dec); err != nil { + return err + } + } + // Root: Option + hasRoot, err := dec.ReadUint8() + if err != nil { + return err + } + if hasRoot == 1 { + root, err := dec.ReadUint64(binary.LittleEndian) + if err != nil { + return err + } + u.Root = &root + } else if hasRoot != 0 { + return fmt.Errorf("invalid Option discriminant: %d", hasRoot) + } + // Hash + b, err := dec.ReadNBytes(32) + if err != nil { + return err + } + copy(u.Hash[:], b) + // Timestamp: Option + hasTs, err := dec.ReadUint8() + if err != nil { + return err + } + if hasTs == 1 { + ts, err := dec.ReadInt64(binary.LittleEndian) + if err != nil { + return err + } + u.Timestamp = &ts + } else if hasTs != 0 { + return fmt.Errorf("invalid Option discriminant: %d", hasTs) + } + return nil +} + +func (u VoteStateUpdate) MarshalWithEncoder(enc *bin.Encoder) error { + if err := enc.WriteUint64(uint64(len(u.Lockouts)), binary.LittleEndian); err != nil { + return err + } + for _, l := range u.Lockouts { + if err := l.MarshalWithEncoder(enc); err != nil { + return err + } + } + if u.Root == nil { + if err := enc.WriteUint8(0); err != nil { + return err + } + } else { + if err := enc.WriteUint8(1); err != nil { + return err + } + if err := enc.WriteUint64(*u.Root, binary.LittleEndian); err != nil { + return err + } + } + if err := enc.WriteBytes(u.Hash[:], false); err != nil { + return err + } + if u.Timestamp == nil { + return enc.WriteUint8(0) + } + if err := enc.WriteUint8(1); err != nil { + return err + } + return enc.WriteInt64(*u.Timestamp, binary.LittleEndian) +} + +// TowerSyncUpdate is the data for the TowerSync and TowerSyncSwitch instructions. +// It represents a tower sync update (the current consensus mechanism) and +// adds a BlockID field to VoteStateUpdate. +// +// The wire format is the compact serde format (short_vec + delta-encoded +// lockout offsets + varint + trailing block_id) — see compact.go. +// Lockouts are stored here as absolute slots; delta encoding happens at +// marshal time. +type TowerSyncUpdate struct { + Lockouts []Lockout + Root *uint64 + Hash solana.Hash + Timestamp *int64 + BlockID solana.Hash +} + +func (t *TowerSyncUpdate) UnmarshalWithDecoder(dec *bin.Decoder) error { + upd := VoteStateUpdate{} + if err := unmarshalCompactVoteStateUpdate(dec, &upd); err != nil { + return err + } + t.Lockouts = upd.Lockouts + t.Root = upd.Root + t.Hash = upd.Hash + t.Timestamp = upd.Timestamp + b, err := dec.ReadNBytes(32) + if err != nil { + return err + } + copy(t.BlockID[:], b) + return nil +} + +func (t TowerSyncUpdate) MarshalWithEncoder(enc *bin.Encoder) error { + upd := VoteStateUpdate{ + Lockouts: t.Lockouts, + Root: t.Root, + Hash: t.Hash, + Timestamp: t.Timestamp, + } + if err := marshalCompactVoteStateUpdate(enc, &upd); err != nil { + return err + } + return enc.WriteBytes(t.BlockID[:], false) +} + +// VoteAuthorizeWithSeedArgs is the data for AuthorizeWithSeed. +type VoteAuthorizeWithSeedArgs struct { + AuthorizationType VoteAuthorize + CurrentAuthorityDerivedKeyOwner solana.PublicKey + CurrentAuthorityDerivedKeySeed string + NewAuthority solana.PublicKey +} + +func (a *VoteAuthorizeWithSeedArgs) UnmarshalWithDecoder(dec *bin.Decoder) error { + if err := a.AuthorizationType.UnmarshalWithDecoder(dec); err != nil { + return err + } + b, err := dec.ReadNBytes(32) + if err != nil { + return err + } + a.CurrentAuthorityDerivedKeyOwner = solana.PublicKeyFromBytes(b) + seed, err := dec.ReadRustString() + if err != nil { + return err + } + a.CurrentAuthorityDerivedKeySeed = seed + b, err = dec.ReadNBytes(32) + if err != nil { + return err + } + a.NewAuthority = solana.PublicKeyFromBytes(b) + return nil +} + +func (a VoteAuthorizeWithSeedArgs) MarshalWithEncoder(enc *bin.Encoder) error { + if err := a.AuthorizationType.MarshalWithEncoder(enc); err != nil { + return err + } + if err := enc.WriteBytes(a.CurrentAuthorityDerivedKeyOwner[:], false); err != nil { + return err + } + if err := enc.WriteRustString(a.CurrentAuthorityDerivedKeySeed); err != nil { + return err + } + return enc.WriteBytes(a.NewAuthority[:], false) +} + +// VoteAuthorizeCheckedWithSeedArgs is the data for AuthorizeCheckedWithSeed. +type VoteAuthorizeCheckedWithSeedArgs struct { + AuthorizationType VoteAuthorize + CurrentAuthorityDerivedKeyOwner solana.PublicKey + CurrentAuthorityDerivedKeySeed string +} + +func (a *VoteAuthorizeCheckedWithSeedArgs) UnmarshalWithDecoder(dec *bin.Decoder) error { + if err := a.AuthorizationType.UnmarshalWithDecoder(dec); err != nil { + return err + } + b, err := dec.ReadNBytes(32) + if err != nil { + return err + } + a.CurrentAuthorityDerivedKeyOwner = solana.PublicKeyFromBytes(b) + seed, err := dec.ReadRustString() + if err != nil { + return err + } + a.CurrentAuthorityDerivedKeySeed = seed + return nil +} + +func (a VoteAuthorizeCheckedWithSeedArgs) MarshalWithEncoder(enc *bin.Encoder) error { + if err := a.AuthorizationType.MarshalWithEncoder(enc); err != nil { + return err + } + if err := enc.WriteBytes(a.CurrentAuthorityDerivedKeyOwner[:], false); err != nil { + return err + } + return enc.WriteRustString(a.CurrentAuthorityDerivedKeySeed) +}